From 25abb9368d98ad84a174f530e02f4228d2269062 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:25:54 -0700 Subject: [PATCH 01/42] fix(stats): wrap usage breakdown model names (#14065) --- .../stats/UsageBreakdownSection.test.tsx | 35 +++++++++++++++++++ .../stats/UsageBreakdownSection.tsx | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/components/stats/UsageBreakdownSection.test.tsx diff --git a/src/renderer/src/components/stats/UsageBreakdownSection.test.tsx b/src/renderer/src/components/stats/UsageBreakdownSection.test.tsx new file mode 100644 index 00000000000..c6e5ba63fa0 --- /dev/null +++ b/src/renderer/src/components/stats/UsageBreakdownSection.test.tsx @@ -0,0 +1,35 @@ +// @vitest-environment happy-dom + +import '@testing-library/jest-dom/vitest' + +import { cleanup, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it } from 'vitest' +import { UsageBreakdownSection } from './UsageBreakdownSection' + +afterEach(cleanup) + +describe('UsageBreakdownSection', () => { + it('wraps long model identifiers instead of truncating them', () => { + const firstModel = 'fireworks-ai/accounts/fireworks/models/deepseek-v3p2' + const secondModel = 'fireworks-ai/accounts/fireworks/models/qwen3-coder-480b' + + render( + + ) + + for (const model of [firstModel, secondModel]) { + const label = screen.getByText(model) + expect(label).toHaveClass('min-w-0', 'break-words') + expect(label).not.toHaveClass('truncate') + } + }) +}) diff --git a/src/renderer/src/components/stats/UsageBreakdownSection.tsx b/src/renderer/src/components/stats/UsageBreakdownSection.tsx index 84186694ca3..19b3ad7c224 100644 --- a/src/renderer/src/components/stats/UsageBreakdownSection.tsx +++ b/src/renderer/src/components/stats/UsageBreakdownSection.tsx @@ -44,7 +44,7 @@ export function UsageBreakdownSection({ {rows.slice(0, 5).map((row) => (
- {row.label} + {row.label} {formatTokens(row.tokens)}
From aecce221bd82d82cb04d4e2d5e05190c7b762df5 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:51:55 -0700 Subject: [PATCH 02/42] fix(test): deflake relay exec env, project boundary, and speech resume tests (#14446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three test-suite problems, all root-caused in the tests rather than in production behavior. 1. src/relay/agent-exec-handler.test.ts (real failure, not a flake) The two spawn-argument assertions failed with "Number of calls: 1" — spawn ran, but the env differed. Cause: both assert `expect.objectContaining({ ...process.env, ... })`, which demands that every ambient variable reach the child verbatim. #7986 (1a6abc87d11) changed both sides at once: it rewrote the assertion from `env: process.env` to that objectContaining form, and in the same commit made the handler apply `applyTerminalGitCredentialPromptGuard`, which appends its own entries to Git's indexed-config protocol (GIT_CONFIG_COUNT / KEY_n / VALUE_n). So whenever the test runner's own environment already carries that protocol — exactly what Orca exports into its agent terminals — the snapshot expects GIT_CONFIG_COUNT=2 while the correctly guarded child gets 4. The test passes on a bare CI shell and fails when run from a guarded terminal. The implementation is right: appending the guard after the caller's config is the documented contract, and "guards wrapped agents after atomically replacing inherited indexed config" already covers it. Fixed the test instead, by clearing the guard-owned keys (GIT_CONFIG_* protocol and WSLENV) from the ambient env for the duration of the suite and restoring them afterwards, so the passthrough baseline is deterministic. No assertion was weakened or removed. 2. project-view-wrapper-source-context-boundary.test.ts (flake: 30s timeout) `buildProjectWorkItem` is a pure function, but it lived in ProjectViewWrapper.tsx, so importing it pulled in the store, sonner, lucide, and the whole UI kit — ~8.8s of transform and module evaluation for one assertion, which tipped past the 30s limit under parallel load. Extracted it to project-work-item.ts (its only dependency is githubProjectHost) and pointed the test there. Both test cases are unchanged. Also dropped the now-unneeded happy-dom environment, since nothing in the file touches the DOM any more. 9.15s -> 0.12s. 3. model-manager-download-resume.test.ts (flake: 30s timeout) "bounds a server that advances by pathologically tiny segments forever" drives the loop to the MAX_TOTAL_DOWNLOAD_REQUESTS ceiling of 4096. Each iteration did a real writeFileSync plus two statSync calls through getPartialDownloadBytes — ~12k synchronous filesystem syscalls in a tight loop. Fast on an idle disk, but it serializes against every other vitest worker on a loaded machine, which is what blew the per-test timeout. Stubbed getPartialDownloadBytes to read the byte counter the test already maintains, so the loop is pure CPU. The file was only ever a stand-in for that counter. Ceiling and rejection assertions are unchanged: 332ms -> 15ms. The two remaining ~1.1s cases in that file spend their time in the real 1s retry backoff around real stream and file-write plumbing; they are left on real timers because faking them would mean faking the transport too, and 1.1s leaves ample headroom. --- .../model-manager-download-resume.test.ts | 5 ++- src/relay/agent-exec-handler.test.ts | 21 +++++++++- .../github-project/ProjectViewWrapper.tsx | 38 +----------------- ...ew-wrapper-source-context-boundary.test.ts | 4 +- .../github-project/project-work-item.ts | 40 +++++++++++++++++++ 5 files changed, 66 insertions(+), 42 deletions(-) create mode 100644 src/renderer/src/components/github-project/project-work-item.ts diff --git a/src/main/speech/model-manager-download-resume.test.ts b/src/main/speech/model-manager-download-resume.test.ts index 60645d1aa28..2fb4329e5c5 100644 --- a/src/main/speech/model-manager-download-resume.test.ts +++ b/src/main/speech/model-manager-download-resume.test.ts @@ -37,6 +37,7 @@ type ModelManagerInternals = { redirectCount?: number, resumeOffset?: number ) => Promise + getPartialDownloadBytes: (filePath: string) => number } type ScriptedResponse = { @@ -288,11 +289,13 @@ describe('ModelManager download resume', () => { const manager = new ModelManager(dir) as unknown as ModelManagerInternals const filePath = join(dir, 'model.bin') let bytesWritten = 0 + // Why: the ceiling costs 4096 iterations, so read progress from memory — + // real per-iteration file I/O stalls this test under parallel load. + vi.spyOn(manager, 'getPartialDownloadBytes').mockImplementation(() => bytesWritten) // Advances one byte per request against a total larger than the request // ceiling, so it makes forward progress forever without ever completing. const downloadFileMock = vi.spyOn(manager, 'downloadFile').mockImplementation(() => { bytesWritten += 1 - writeFileSync(filePath, Buffer.alloc(bytesWritten)) return Promise.resolve() }) diff --git a/src/relay/agent-exec-handler.test.ts b/src/relay/agent-exec-handler.test.ts index c884c499dac..42a79469d42 100644 --- a/src/relay/agent-exec-handler.test.ts +++ b/src/relay/agent-exec-handler.test.ts @@ -1,5 +1,5 @@ import { execFile, spawn } from 'node:child_process' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as ChildProcess from 'node:child_process' import { createFakeChild, createHandlers, requestContext } from './agent-exec-handler-test-harness' import { TERMINAL_GIT_CREDENTIAL_GUARD_POLICY_ENV } from '../shared/terminal-git-credential-guard' @@ -18,10 +18,29 @@ const execFileMock = vi.mocked(execFile) type AgentExecResult = { exitCode: number | null; timedOut: boolean } +const GUARD_OWNED_ENV_RE = /^(?:GIT_CONFIG_(?:COUNT|KEY_\d+|VALUE_\d+)|WSLENV)$/ + describe('AgentExecHandler', () => { + let ambientGuardEnv: Record = {} + beforeEach(() => { spawnMock.mockReset() execFileMock.mockReset() + // Why: the guard rewrites these, so an already-guarded runner (Orca guards + // its own agent terminals) would not see its ambient values passed through. + ambientGuardEnv = {} + for (const key of Object.keys(process.env).filter((name) => GUARD_OWNED_ENV_RE.test(name))) { + ambientGuardEnv[key] = process.env[key] + delete process.env[key] + } + }) + + afterEach(() => { + for (const [key, value] of Object.entries(ambientGuardEnv)) { + if (value !== undefined) { + process.env[key] = value + } + } }) it('executes a non-interactive command with captured output and stdin', async () => { diff --git a/src/renderer/src/components/github-project/ProjectViewWrapper.tsx b/src/renderer/src/components/github-project/ProjectViewWrapper.tsx index 7cac639ba8a..24941730c94 100644 --- a/src/renderer/src/components/github-project/ProjectViewWrapper.tsx +++ b/src/renderer/src/components/github-project/ProjectViewWrapper.tsx @@ -65,6 +65,7 @@ import { githubProjectHost, githubProjectIdentityKey } from '../../../../shared/github/project-identity' +import { buildProjectWorkItem } from './project-work-item' type Props = { selectedRepoIds: ReadonlySet @@ -94,43 +95,6 @@ function getProjectViewSourceScope(settings: Parameters label.name), - updatedAt: row.updatedAt, - author: null, - repoId, - prRepo - } -} - export default function ProjectViewWrapper({ selectedRepoIds }: Props): React.JSX.Element { const settings = useAppStore((s) => s.settings) const projectViewCache = useAppStore((s) => s.projectViewCache) diff --git a/src/renderer/src/components/github-project/project-view-wrapper-source-context-boundary.test.ts b/src/renderer/src/components/github-project/project-view-wrapper-source-context-boundary.test.ts index b7273b5e306..0ee72795a97 100644 --- a/src/renderer/src/components/github-project/project-view-wrapper-source-context-boundary.test.ts +++ b/src/renderer/src/components/github-project/project-view-wrapper-source-context-boundary.test.ts @@ -1,5 +1,3 @@ -// @vitest-environment happy-dom - import { readFileSync } from 'node:fs' import { join } from 'node:path' import { describe, expect, it } from 'vitest' @@ -21,7 +19,7 @@ function sourceBetween(source: string, startPattern: string, endPattern: string) describe('ProjectViewWrapper GitHub source context boundary', () => { it('builds project work items with a host-pinned repository identity', async () => { - const { buildProjectWorkItem } = await import('./ProjectViewWrapper') + const { buildProjectWorkItem } = await import('./project-work-item') const row: GitHubProjectRow = { id: 'PVTI_1', itemType: 'PULL_REQUEST', diff --git a/src/renderer/src/components/github-project/project-work-item.ts b/src/renderer/src/components/github-project/project-work-item.ts new file mode 100644 index 00000000000..cc68266fe88 --- /dev/null +++ b/src/renderer/src/components/github-project/project-work-item.ts @@ -0,0 +1,40 @@ +import { githubProjectHost } from '../../../../shared/github/project-identity' +import type { GitHubProjectRow } from '../../../../shared/github/project-types' +import type { GitHubWorkItem } from '../../../../shared/types' + +export function buildProjectWorkItem( + row: GitHubProjectRow, + repoId: string, + host?: string +): GitHubWorkItem | null { + if (row.itemType !== 'ISSUE' && row.itemType !== 'PULL_REQUEST') { + return null + } + if (row.content.number == null || !row.content.url) { + return null + } + const [owner, repo] = row.content.repository?.split('/') ?? [] + // Why: Project rows can reach mutation controls before detail hydration, so + // preserve their host-bearing repository identity on the initial item. + const prRepo = owner && repo ? { owner, repo, host: githubProjectHost(host) } : undefined + return { + id: `${row.itemType === 'PULL_REQUEST' ? 'pr' : 'issue'}:${row.content.number}`, + type: row.itemType === 'PULL_REQUEST' ? 'pr' : 'issue', + number: row.content.number, + title: row.content.title, + state: + row.content.state === 'MERGED' + ? 'merged' + : row.content.state === 'CLOSED' + ? 'closed' + : row.content.isDraft + ? 'draft' + : 'open', + url: row.content.url, + labels: row.content.labels.map((label) => label.name), + updatedAt: row.updatedAt, + author: null, + repoId, + prRepo + } +} From eb9ac440aabb1d4539fec788d6a160bf1c807065 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:07:53 -0700 Subject: [PATCH 03/42] Revert "Center newly opened tabs at end, keep close controls visible" (#14450) --- .../components/tab-bar/tab-bar-surface.tsx | 2 - .../tab-strip-active-tab-scroll.test.ts | 197 ------------------ .../tab-bar/tab-strip-active-tab-scroll.ts | 102 --------- .../tab-bar/tab-strip-overflow-navigation.ts | 36 +--- 4 files changed, 6 insertions(+), 331 deletions(-) delete mode 100644 src/renderer/src/components/tab-bar/tab-strip-active-tab-scroll.test.ts delete mode 100644 src/renderer/src/components/tab-bar/tab-strip-active-tab-scroll.ts diff --git a/src/renderer/src/components/tab-bar/tab-bar-surface.tsx b/src/renderer/src/components/tab-bar/tab-bar-surface.tsx index c37597e2932..6e1c49ddc94 100644 --- a/src/renderer/src/components/tab-bar/tab-bar-surface.tsx +++ b/src/renderer/src/components/tab-bar/tab-bar-surface.tsx @@ -163,8 +163,6 @@ export function renderTabBarSurface({ .join(' ')} > {renderedItems} - {/* Why: a short end inset keeps a last tab's close control off the chevron and fade. */} -
diff --git a/src/renderer/src/components/tab-bar/tab-strip-active-tab-scroll.test.ts b/src/renderer/src/components/tab-bar/tab-strip-active-tab-scroll.test.ts deleted file mode 100644 index f520afaf568..00000000000 --- a/src/renderer/src/components/tab-bar/tab-strip-active-tab-scroll.test.ts +++ /dev/null @@ -1,197 +0,0 @@ -// @vitest-environment happy-dom -import { describe, expect, it } from 'vitest' -import { - computeTabStripEndInset, - computeTabStripScrollLeft, - findLastTabStripTab, - isLastTabStripTab, - syncTabStripEndPad, - TAB_STRIP_REVEAL_INSET_PX -} from './tab-strip-active-tab-scroll' - -function mockBox( - el: HTMLElement, - box: { - offsetLeft?: number - offsetWidth?: number - clientWidth?: number - scrollWidth?: number - } -): void { - Object.defineProperties(el, { - offsetLeft: { configurable: true, value: box.offsetLeft ?? 0 }, - offsetWidth: { configurable: true, value: box.offsetWidth ?? 0 }, - clientWidth: { configurable: true, value: box.clientWidth ?? 0 }, - scrollWidth: { configurable: true, value: box.scrollWidth ?? 0 } - }) -} - -describe('computeTabStripEndInset', () => { - it('is zero when tabs already fit', () => { - expect( - computeTabStripEndInset({ - stripClientWidth: 400, - lastTabWidth: 180, - contentWidth: 360 - }) - ).toBe(0) - }) - - it('uses the fade-sized inset when the last tab overflows', () => { - expect( - computeTabStripEndInset({ - stripClientWidth: 400, - lastTabWidth: 180, - contentWidth: 900 - }) - ).toBe(TAB_STRIP_REVEAL_INSET_PX) - }) - - it('shrinks the inset when the last tab almost fills the strip', () => { - expect( - computeTabStripEndInset({ - stripClientWidth: 200, - lastTabWidth: 190, - contentWidth: 400 - }) - ).toBe(10) - }) - - it('is zero when the last tab is at least as wide as the strip', () => { - expect( - computeTabStripEndInset({ - stripClientWidth: 180, - lastTabWidth: 180, - contentWidth: 400 - }) - ).toBe(0) - }) -}) - -// Derives scrollWidth from the live pad policy so tests can't assert unreachable geometry. -function lastTabGeometry({ - stripClientWidth, - lastTabWidth, - lastTabOffsetLeft -}: { - stripClientWidth: number - lastTabWidth: number - lastTabOffsetLeft: number -}): { - stripScrollWidth: number - stripClientWidth: number - tabOffsetLeft: number - tabWidth: number -} { - const contentWidth = lastTabOffsetLeft + lastTabWidth - const pad = computeTabStripEndInset({ stripClientWidth, lastTabWidth, contentWidth }) - return { - stripScrollWidth: contentWidth + pad, - stripClientWidth, - tabOffsetLeft: lastTabOffsetLeft, - tabWidth: lastTabWidth - } -} - -describe('computeTabStripScrollLeft', () => { - it('stops a normal-width last tab at the fade inset because the pad caps the range', () => { - const geometry = lastTabGeometry({ - stripClientWidth: 400, - lastTabWidth: 180, - lastTabOffsetLeft: 720 - }) - const scrollLeft = computeTabStripScrollLeft({ - ...geometry, - stripScrollLeft: 0, - inline: 'center' - }) - - expect(scrollLeft).toBe(geometry.stripScrollWidth - geometry.stripClientWidth) - const tabEnd = geometry.tabOffsetLeft + geometry.tabWidth - scrollLeft - expect(geometry.stripClientWidth - tabEnd).toBe(TAB_STRIP_REVEAL_INSET_PX) - }) - - it('centers a last tab once the pad covers the required trailing range', () => { - const geometry = lastTabGeometry({ - stripClientWidth: 200, - lastTabWidth: 190, - lastTabOffsetLeft: 210 - }) - - expect(computeTabStripScrollLeft({ ...geometry, stripScrollLeft: 0, inline: 'center' })).toBe( - geometry.tabOffsetLeft + geometry.tabWidth / 2 - geometry.stripClientWidth / 2 - ) - }) - - it('keeps a fully visible middle tab in place', () => { - expect( - computeTabStripScrollLeft({ - stripScrollWidth: 900, - stripClientWidth: 400, - stripScrollLeft: 200, - tabOffsetLeft: 280, - tabWidth: 120, - inline: 'nearest' - }) - ).toBe(200) - }) - - it('scrolls far enough that a clipped trailing tab clears the fade inset', () => { - expect( - computeTabStripScrollLeft({ - stripScrollWidth: 900, - stripClientWidth: 400, - stripScrollLeft: 0, - tabOffsetLeft: 320, - tabWidth: 180, - inline: 'nearest' - }) - ).toBe(320 + 180 - 400 + TAB_STRIP_REVEAL_INSET_PX) - }) -}) - -describe('tab strip last-tab helpers', () => { - it('treats the last data-tab-id node as the last tab and ignores the end pad', () => { - const strip = document.createElement('div') - const first = document.createElement('div') - first.setAttribute('data-tab-id', 'one') - const last = document.createElement('div') - last.setAttribute('data-tab-id', 'two') - const pad = document.createElement('div') - pad.setAttribute('data-tab-strip-end-pad', '') - strip.append(first, last, pad) - - expect(findLastTabStripTab(strip)).toBe(last) - expect(isLastTabStripTab(strip, last)).toBe(true) - expect(isLastTabStripTab(strip, first)).toBe(false) - }) - - it('sizes the end pad to the fade inset when the last tab overflows', () => { - const strip = document.createElement('div') - const last = document.createElement('div') - last.setAttribute('data-tab-id', 'end') - const pad = document.createElement('div') - pad.setAttribute('data-tab-strip-end-pad', '') - strip.append(last, pad) - mockBox(strip, { clientWidth: 400 }) - mockBox(last, { offsetLeft: 720, offsetWidth: 180 }) - - expect(syncTabStripEndPad(strip)).toBe(TAB_STRIP_REVEAL_INSET_PX) - expect(pad.style.width).toBe(`${TAB_STRIP_REVEAL_INSET_PX}px`) - }) - - it('clears the end pad when every tab already fits', () => { - const strip = document.createElement('div') - const last = document.createElement('div') - last.setAttribute('data-tab-id', 'end') - const pad = document.createElement('div') - pad.setAttribute('data-tab-strip-end-pad', '') - pad.style.width = '110px' - strip.append(last, pad) - mockBox(strip, { clientWidth: 400 }) - mockBox(last, { offsetLeft: 0, offsetWidth: 180 }) - - expect(syncTabStripEndPad(strip)).toBe(0) - expect(pad.style.width).toBe('0px') - }) -}) diff --git a/src/renderer/src/components/tab-bar/tab-strip-active-tab-scroll.ts b/src/renderer/src/components/tab-bar/tab-strip-active-tab-scroll.ts deleted file mode 100644 index fd9551885d4..00000000000 --- a/src/renderer/src/components/tab-bar/tab-strip-active-tab-scroll.ts +++ /dev/null @@ -1,102 +0,0 @@ -export const TAB_STRIP_TAB_ID_SELECTOR = '[data-tab-id]' -export const TAB_STRIP_END_PAD_ATTR = 'data-tab-strip-end-pad' - -// Why: matches the 1.5rem fade mask so nearest-reveal keeps the close control out of the fade. -export const TAB_STRIP_REVEAL_INSET_PX = 24 - -export function findLastTabStripTab(strip: ParentNode): HTMLElement | null { - const tabs = strip.querySelectorAll(TAB_STRIP_TAB_ID_SELECTOR) - return [...tabs].at(-1) ?? null -} - -export function isLastTabStripTab(strip: ParentNode, tab: Element): boolean { - return findLastTabStripTab(strip) === tab -} - -export function computeTabStripEndInset({ - stripClientWidth, - lastTabWidth, - contentWidth -}: { - stripClientWidth: number - lastTabWidth: number - contentWidth: number -}): number { - if (stripClientWidth <= 0 || lastTabWidth <= 0 || contentWidth <= stripClientWidth) { - return 0 - } - // Why: a half-viewport pad centered the last tab but hid earlier tabs and - // created a drop dead-zone; a fade-sized inset keeps the close control clear. - // Trade-off: a center request only lands centered while the strip is within - // 2x this inset of the last tab's width; wider gaps settle at end + inset. - return Math.min(TAB_STRIP_REVEAL_INSET_PX, Math.max(0, stripClientWidth - lastTabWidth)) -} - -export function computeTabStripScrollLeft({ - stripScrollWidth, - stripClientWidth, - stripScrollLeft, - tabOffsetLeft, - tabWidth, - inline -}: { - stripScrollWidth: number - stripClientWidth: number - stripScrollLeft: number - tabOffsetLeft: number - tabWidth: number - inline: 'nearest' | 'center' -}): number { - const maxScrollLeft = Math.max(0, stripScrollWidth - stripClientWidth) - if (inline === 'center') { - return clamp(tabOffsetLeft + tabWidth / 2 - stripClientWidth / 2, 0, maxScrollLeft) - } - - const tabStart = tabOffsetLeft - const tabEnd = tabOffsetLeft + tabWidth - const viewStart = stripScrollLeft + TAB_STRIP_REVEAL_INSET_PX - const viewEnd = stripScrollLeft + stripClientWidth - TAB_STRIP_REVEAL_INSET_PX - if (tabStart >= viewStart && tabEnd <= viewEnd) { - return stripScrollLeft - } - if (tabEnd > viewEnd) { - return clamp(tabEnd - stripClientWidth + TAB_STRIP_REVEAL_INSET_PX, 0, maxScrollLeft) - } - return clamp(tabStart - TAB_STRIP_REVEAL_INSET_PX, 0, maxScrollLeft) -} - -export function syncTabStripEndPad(strip: HTMLElement): number { - const lastTab = findLastTabStripTab(strip) - const pad = computeTabStripEndInset({ - stripClientWidth: strip.clientWidth, - lastTabWidth: lastTab?.offsetWidth ?? 0, - contentWidth: lastTab ? lastTab.offsetLeft + lastTab.offsetWidth : 0 - }) - const spacer = strip.querySelector(`[${TAB_STRIP_END_PAD_ATTR}]`) - if (spacer) { - const nextWidth = `${pad}px` - if (spacer.style.width !== nextWidth) { - spacer.style.width = nextWidth - } - } - return pad -} - -export function scrollTabStripTabIntoView( - strip: HTMLElement, - tab: HTMLElement, - inline: 'nearest' | 'center' -): void { - strip.scrollLeft = computeTabStripScrollLeft({ - stripScrollWidth: strip.scrollWidth, - stripClientWidth: strip.clientWidth, - stripScrollLeft: strip.scrollLeft, - tabOffsetLeft: tab.offsetLeft, - tabWidth: tab.offsetWidth, - inline - }) -} - -function clamp(value: number, min: number, max: number): number { - return Math.min(max, Math.max(min, value)) -} diff --git a/src/renderer/src/components/tab-bar/tab-strip-overflow-navigation.ts b/src/renderer/src/components/tab-bar/tab-strip-overflow-navigation.ts index 1d5b78df7c7..77af550b748 100644 --- a/src/renderer/src/components/tab-bar/tab-strip-overflow-navigation.ts +++ b/src/renderer/src/components/tab-bar/tab-strip-overflow-navigation.ts @@ -6,11 +6,6 @@ import { type TabStripScrollMetrics } from './tab-strip-scroll-metrics' import { isTabStripPointerGestureActive } from './tab-strip-pointer-gesture' -import { - findLastTabStripTab, - scrollTabStripTabIntoView, - syncTabStripEndPad -} from './tab-strip-active-tab-scroll' const TAB_STRIP_SCROLL_FRACTION = 0.75 const TAB_STRIP_MIN_SCROLL_STEP_PX = 120 @@ -56,10 +51,6 @@ export function useTabStripOverflowNavigation({ const tabStripRef = useRef(null) const prevStripLenRef = useRef<{ worktreeId: string; len: number } | null>(null) const stickToEndRef = useRef(false) - const activeVisibleTabIdRef = useRef(activeVisibleTabId) - useLayoutEffect(() => { - activeVisibleTabIdRef.current = activeVisibleTabId - }) const [tabStripOverflowState, setTabStripOverflowState] = useState( EMPTY_TAB_STRIP_OVERFLOW_STATE ) @@ -118,7 +109,6 @@ export function useTabStripOverflowNavigation({ onScroll() const handleStripResize = (): void => { - syncTabStripEndPad(el) updateTabStripOverflowState() // If the user is pinned to the right edge, keep it pinned even as tab // labels (e.g. "Terminal 5" -> branch name) expand and change scrollWidth. @@ -148,7 +138,6 @@ export function useTabStripOverflowNavigation({ } if (!prev || prev.worktreeId !== worktreeId) { prevStripLenRef.current = { worktreeId, len: tabCount } - syncTabStripEndPad(strip) updateTabStripOverflowState() return } @@ -159,7 +148,6 @@ export function useTabStripOverflowNavigation({ if (!el) { return } - syncTabStripEndPad(el) el.scrollLeft = Math.max(0, el.scrollWidth - el.clientWidth) updateTabStripOverflowState() } @@ -167,28 +155,17 @@ export function useTabStripOverflowNavigation({ requestAnimationFrame(scrollToEnd) } if (tabCount > prev.len && !pointerGestureActive) { - const revealNewEndTab = (): void => { + const scrollToEnd = (): void => { const el = tabStripRef.current if (!el) { return } - syncTabStripEndPad(el) - const lastTab = findLastTabStripTab(el) - const activeId = activeVisibleTabIdRef.current - const activeTab = - activeId == null - ? null - : el.querySelector(`[data-tab-id="${CSS.escape(activeId)}"]`) - // Why: only a newly opened last tab should pin to the end; a restored - // mid-strip tab must not yank the viewport to the last tab. - if (lastTab && (activeTab == null || lastTab === activeTab)) { - scrollTabStripTabIntoView(el, lastTab, 'center') - stickToEndRef.current = true - } + el.scrollLeft = Math.max(0, el.scrollWidth - el.clientWidth) + stickToEndRef.current = true updateTabStripOverflowState() } - revealNewEndTab() - requestAnimationFrame(revealNewEndTab) + scrollToEnd() + requestAnimationFrame(scrollToEnd) } prevStripLenRef.current = { worktreeId, len: tabCount } updateTabStripOverflowState() @@ -212,8 +189,7 @@ export function useTabStripOverflowNavigation({ requestAnimationFrame(updateTabStripOverflowState) return } - syncTabStripEndPad(strip) - scrollTabStripTabIntoView(strip, activeTab, 'nearest') + activeTab.scrollIntoView({ block: 'nearest', inline: 'nearest' }) requestAnimationFrame(updateTabStripOverflowState) }, [activeVisibleTabId, updateTabStripOverflowState]) From d6e6b9e3036bade99eead725a36f4425b3c21554 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:09:23 -0700 Subject: [PATCH 04/42] fix(native-chat): keep caller abort ahead of WSL gate refusal (#14445) Why: a hook-path or scan refusal that races the caller's abort was converted into a WSL unavailability error, so cancellation looked like a stalled distro. --- ...ession-file-resolver-wsl-scan-gate.test.ts | 44 +++++++++++++++++++ src/main/native-chat/session-file-resolver.ts | 4 ++ 2 files changed, 48 insertions(+) diff --git a/src/main/native-chat/session-file-resolver-wsl-scan-gate.test.ts b/src/main/native-chat/session-file-resolver-wsl-scan-gate.test.ts index af14882b371..4d84e49b3c5 100644 --- a/src/main/native-chat/session-file-resolver-wsl-scan-gate.test.ts +++ b/src/main/native-chat/session-file-resolver-wsl-scan-gate.test.ts @@ -130,4 +130,48 @@ describe('Codex WSL scan gate', () => { }) ).rejects.toBe(refusal) }) + + it('reports the abort, not a refusal, when the hook-path probe races a caller abort', async () => { + const controller = new AbortController() + const abortReason = new Error('caller went away') + mocks.gate.mockImplementation(async (options: { operation?: string; path: string }) => { + if (options.operation === 'access') { + controller.abort(abortReason) + throw new WslTranscriptFsError('timeout', 'slow share') + } + return [] + }) + + await expect( + resolveSessionFilePath( + 'codex', + 'session-id', + { + transcriptPath: `${WSL_SESSIONS_DIR}\\2026\\rollout-1-session-id.jsonl`, + codexSessionsDirs: [LOCAL_SESSIONS_DIR] + }, + controller.signal + ) + ).rejects.toBe(abortReason) + }) + + // The scan layer's own waiter already wins this race; the resolver's post-catch + // signal check is the backstop if that ever stops holding. + it('reports the abort, not a refusal, when a scanned root is aborted mid-scan', async () => { + const controller = new AbortController() + const abortReason = new Error('caller went away') + mocks.gate.mockImplementation(async () => { + controller.abort(abortReason) + throw new WslTranscriptFsError('timeout', 'slow share') + }) + + await expect( + resolveSessionFilePath( + 'codex', + 'session-id', + { codexSessionsDirs: [WSL_SESSIONS_DIR] }, + controller.signal + ) + ).rejects.toBe(abortReason) + }) }) diff --git a/src/main/native-chat/session-file-resolver.ts b/src/main/native-chat/session-file-resolver.ts index f0376e01f02..a45850f2d46 100644 --- a/src/main/native-chat/session-file-resolver.ts +++ b/src/main/native-chat/session-file-resolver.ts @@ -108,6 +108,8 @@ export async function resolveSessionFilePath( return hostReadable } } catch (error) { + // A caller abort that races the refusal stays authoritative. + signal?.throwIfAborted() // Why: the id-based search may still hit; surface the refusal only when // it does not, so a stalled distro reads as unavailable, never "missing". unavailable = wslTranscriptFsRefusal(error) @@ -232,6 +234,8 @@ async function findCodexRolloutInDirs( return files } } catch (error) { + // A caller abort that races the refusal stays authoritative. + signal?.throwIfAborted() // Why: one stalled distro must not hide another root's hit. unavailable = wslTranscriptFsRefusal(error) } From 5a6837fce4773d90f7aab1291f662aabf77dbc5c Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:15:20 -0700 Subject: [PATCH 05/42] refactor(store): unify the duplicated catalog equality and identity-key helpers (#13804) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(store): unify the catalog structural-equality walks Three near-identical structural deep-equality walks had landed independently in the same window: areValuesEqual (#13744, repo-identity-reconcile.ts), areCatalogEntriesEqual (#13770, repos.ts — already folded into the first on this branch's base) and catalogValuesEqual (#13662, worktree-catalog-reconciliation.ts). All three walk plain records and arrays and fall back to reference equality for anything exotic. They are not interchangeable. Two axes genuinely differ, and each caller depends on its own side: - Own-key set. #13744/#13770 require equal own-key counts plus hasOwnProperty, so an absent key differs from a key present and holding `undefined`. #13662 compares the union of both sides' keys, so those are equal. The strict side is load-bearing: the repo/project merges branch on `'localWindowsRuntimePreference' in project` (repos-project-runtime.test.ts "clears stale local runtime preferences"), and projects are now reconciled with this comparator. The loose side is test-pinned by worktree-catalog-reconciliation.test.ts "reuses rows with equivalent nested catalog data", where a locally built row carries `optional: undefined` that the host omits. - Leaf comparison. #13744/#13770 use `===` (NaN never equal, 0 equals -0); #13662 uses `Object.is` (the reverse). So instead of picking a winner, src/shared/structural-value-equality.ts holds one walk parameterised by those two axes and exports the two policies as `structuralValuesEqual` and `structuralValuesEqualIgnoringUndefined`. Every caller keeps its exact current semantics; the ~40 duplicated lines and the silent divergence go away. src/shared/persisted-ui-equality.ts (a fourth copy with a Set branch and no plain-object guard) is deliberately left alone: it gates a disk write in main with no direct test coverage. Also folded, all provably behaviour-identical: - The `${hostId}\0${repoId}` composite key had three copies (getRepoHostIdentityForParts, repoOwnerKey, getEntryKey) that must agree or repos silently stop reconciling. Moved to src/shared/repo-host-identity.ts because one of them lives in src/shared; the renderer module re-exports it. - mergeFetchedReposForHost's hand-inlined upsert loop now calls mergeByIdentity. mergeByIdentity additionally skips replacing a structurally equal row, which cannot change the result here: reconcileFetchedRepos runs immediately after over the same identities in the same order and restores exactly those rows. - Renamed repos.ts's `catalogRowsUnchanged` to `arrayElementsUnchanged`. It is a pure element-identity compare, two files away from `catalogRowsEqual`, which is a full structural compare. src/shared/structural-value-equality.test.ts pins both policies over arrays, nested records, null-prototype records, absent-vs-undefined keys, symbol keys, and non-plain objects (Date/Map/Set/class) falling back to reference equality. * fix(store): keep merged sourceRepoIds order host-independent Prefixing the cross-host remainder made a cross-host project's sourceRepoIds order a function of the refreshing host, so the projects reconcile never reused the row. Also pins the repo-derived host-id contribution the new per-project slice feeds the host-id resolvers. Co-authored-by: Orca * refactor(store): migrate call sites that landed after this branch github.ts and ai-vault-session-identity.ts began using areValuesEqual on main while this branch was stale, and repo-identity-reconcile's record reconciler still called its own deleted walker. All three now use structuralValuesEqual; reuseEqualCatalogRows keeps its duplicate-id cap and calls the ignoring-undefined variant, which is the key-union semantics catalogValuesEqual had. --------- Co-authored-by: Orca --- .../ai-vault-session-identity.ts | 6 +- src/renderer/src/store/slices/github.ts | 7 +- .../src/store/slices/repo-host-identity.ts | 9 +- .../store/slices/repo-identity-reconcile.ts | 46 +---- .../repos-cross-host-refresh-identity.test.ts | 181 ++++++++++++++++++ .../slices/repos-refresh-identity.test.ts | 51 +++++ src/renderer/src/store/slices/repos.ts | 48 ++--- .../store/slices/superseded-ssh-repo-rows.ts | 19 +- .../slices/worktree-catalog-reconciliation.ts | 41 +--- src/shared/manual-repo-order.ts | 3 +- src/shared/repo-host-identity.ts | 5 + src/shared/structural-value-equality.test.ts | 96 ++++++++++ src/shared/structural-value-equality.ts | 82 ++++++++ 13 files changed, 472 insertions(+), 122 deletions(-) create mode 100644 src/renderer/src/store/slices/repos-cross-host-refresh-identity.test.ts create mode 100644 src/shared/repo-host-identity.ts create mode 100644 src/shared/structural-value-equality.test.ts create mode 100644 src/shared/structural-value-equality.ts diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-identity.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-identity.ts index f2101f75011..a4effa09096 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-identity.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-identity.ts @@ -1,5 +1,5 @@ import type { AiVaultListResult, AiVaultSession } from '../../../../shared/ai-vault-types' -import { areValuesEqual } from '@/store/slices/repo-identity-reconcile' +import { structuralValuesEqual } from '../../../../shared/structural-value-equality' import { reuseEqualCatalogRows } from '@/store/slices/worktree-catalog-reconciliation' // One instance is shared by every mounted hook, so it is frozen: an in-place @@ -24,7 +24,9 @@ export function reuseAiVaultListResult( return incoming } const sessions = reuseEqualCatalogRows(current.sessions, incoming.sessions) - const issues = areValuesEqual(current.issues, incoming.issues) ? current.issues : incoming.issues + const issues = structuralValuesEqual(current.issues, incoming.issues) + ? current.issues + : incoming.issues if ( sessions === current.sessions && issues === current.issues && diff --git a/src/renderer/src/store/slices/github.ts b/src/renderer/src/store/slices/github.ts index 6134198b1c1..71cf753e662 100644 --- a/src/renderer/src/store/slices/github.ts +++ b/src/renderer/src/store/slices/github.ts @@ -78,7 +78,8 @@ import { normalizeGitHubPRForBranchOutcome } from '../../../../shared/github/pul import { restoreReactionOnSubject, setReactionOnSubject } from '@/lib/pr-comment-reactions' import { withGitHubCheckDetailsTimeout } from '@/runtime/github-check-details-timeout' import { getGitHubRepoLookupIndex } from './github-repo-lookup-index' -import { areValuesEqual, reconcileCatalogRows } from './repo-identity-reconcile' +import { reconcileCatalogRows } from './repo-identity-reconcile' +import { structuralValuesEqual } from '../../../../shared/structural-value-equality' // ─── ProjectV2 cache types ──────────────────────────────────────────── // Why: separate from CacheEntry — project-view has a single GraphQL source (no issue/PR fallback) and a distinct error union. @@ -2759,8 +2760,8 @@ export const createGitHubSlice: StateCreator = (s (row) => `${row.repoId}\0${row.id}` ) const nextFellBack = envelope.issueSourceFellBack ? true : undefined - const sourcesUnchanged = areValuesEqual(previousEntry?.sources, envelope.sources) - const errorUnchanged = areValuesEqual(previousEntry?.error, errorForCache) + const sourcesUnchanged = structuralValuesEqual(previousEntry?.sources, envelope.sources) + const errorUnchanged = structuralValuesEqual(previousEntry?.error, errorForCache) const fellBackUnchanged = previousEntry?.issueSourceFellBack === nextFellBack if ( previousEntry && diff --git a/src/renderer/src/store/slices/repo-host-identity.ts b/src/renderer/src/store/slices/repo-host-identity.ts index f21da2b153a..772d5dc2925 100644 --- a/src/renderer/src/store/slices/repo-host-identity.ts +++ b/src/renderer/src/store/slices/repo-host-identity.ts @@ -4,6 +4,9 @@ import { getSettingsFocusedExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host' +import { getRepoHostIdentityForParts } from '../../../../shared/repo-host-identity' + +export { getRepoHostIdentityForParts } type RepoIdentityParts = Pick @@ -11,12 +14,6 @@ export function getRepoHostIdentity(repo: RepoIdentityParts): string { return getRepoHostIdentityForParts(repo.id, getRepoExecutionHostId(repo)) } -export function getRepoHostIdentityForParts(repoId: string, hostId: string): string { - // Why: host ids and repo ids can contain punctuation; NUL keeps the composite - // key collision-free without escaping user/provider-owned strings. - return `${hostId}\0${repoId}` -} - export function repoMatchesHostIdentity( repo: RepoIdentityParts, repoId: string, diff --git a/src/renderer/src/store/slices/repo-identity-reconcile.ts b/src/renderer/src/store/slices/repo-identity-reconcile.ts index db59e619d97..ea118295754 100644 --- a/src/renderer/src/store/slices/repo-identity-reconcile.ts +++ b/src/renderer/src/store/slices/repo-identity-reconcile.ts @@ -1,4 +1,5 @@ import type { Repo } from '../../../../shared/types' +import { structuralValuesEqual } from '../../../../shared/structural-value-equality' import { getRepoHostIdentity } from './repo-host-identity' // Why: after a drag-reorder we optimistically set `repos`, persist, and main @@ -8,44 +9,9 @@ import { getRepoHostIdentity } from './repo-host-identity' // virtualizer to rebuild + re-measure a tick after the drop — the visible jump. // Reusing equal objects (and the whole array when nothing moved) makes the echo // a no-op render. -// Why: `Repo` carries nested records (hookSettings, upstream, gitRemoteIdentity, repoIcon, path -// arrays). IPC structured-clone rebuilds those every fetch, and main's hydrateRepo always -// reconstructs hookSettings — so a reference compare reports every repo as changed and no repo -// ever reconciles. Compare nested plain values structurally; they are small sanitized records. -export function areValuesEqual(a: unknown, b: unknown): boolean { - if (a === b) { - return true - } - if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) { - return false - } - if (Array.isArray(a) || Array.isArray(b)) { - return ( - Array.isArray(a) && - Array.isArray(b) && - a.length === b.length && - a.every((item, index) => areValuesEqual(item, b[index])) - ) - } - // Why: only plain records are safe to walk — anything exotic falls back to reference equality. - const aPrototype = Object.getPrototypeOf(a) - const bPrototype = Object.getPrototypeOf(b) - if ( - (aPrototype !== Object.prototype && aPrototype !== null) || - (bPrototype !== Object.prototype && bPrototype !== null) - ) { - return false - } - const aRecord = a as Record - const bRecord = b as Record - const keys = Object.keys(aRecord) - if (keys.length !== Object.keys(bRecord).length) { - return false - } - return keys.every( - (key) => Object.hasOwn(bRecord, key) && areValuesEqual(aRecord[key], bRecord[key]) - ) -} +// Why the structural compare: `Repo` carries nested records (hookSettings, upstream, +// gitRemoteIdentity, repoIcon, path arrays) that structured-clone and main's hydrateRepo rebuild +// every fetch, so a reference compare would report every repo as changed. /** * Reuses equal rows from `previous` — and the whole array when nothing moved — so a refetch that @@ -61,7 +27,7 @@ export function reconcileCatalogRows( let identical = next.length === previous.length const reconciled = next.map((row, index) => { const existing = previousByIdentity.get(getIdentity(row)) - if (existing !== undefined && areValuesEqual(existing, row)) { + if (existing !== undefined && structuralValuesEqual(existing, row)) { if (existing !== previous[index]) { identical = false } @@ -95,7 +61,7 @@ export function reuseEqualRecordMap( const reconciled: Record = {} for (const key of nextKeys) { const existing = Object.hasOwn(previous, key) ? previous[key] : undefined - if (existing !== undefined && areValuesEqual(existing, next[key])) { + if (existing !== undefined && structuralValuesEqual(existing, next[key])) { reconciled[key] = existing continue } diff --git a/src/renderer/src/store/slices/repos-cross-host-refresh-identity.test.ts b/src/renderer/src/store/slices/repos-cross-host-refresh-identity.test.ts new file mode 100644 index 00000000000..6218d251607 --- /dev/null +++ b/src/renderer/src/store/slices/repos-cross-host-refresh-identity.test.ts @@ -0,0 +1,181 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createTestStore } from './store-test-helpers' +import type { Project, ProjectHostSetup, Repo } from '../../../../shared/types' +import { + createCompatibleRuntimeStatusResponseIfNeeded, + type RuntimeEnvironmentCallRequest +} from '../../runtime/runtime-compatibility-test-fixture' +import { clearRuntimeCompatibilityCacheForTests } from '../../runtime/runtime-rpc-client' + +// One project cloned on the local Mac and on a remote Orca server under distinct repo ids — +// the shape the compat merge exists to serve, and the only shape whose sourceRepoIds are +// assembled from two hosts. +const SHARED_PROJECT_ID = 'github:stablyai/orca' + +const localRepo: Repo = { + id: 'local-repo', + path: '/local/orca', + displayName: 'orca', + badgeColor: '#22c55e', + upstream: { owner: 'stablyai', repo: 'orca' }, + addedAt: 1_700_000_000_000 +} + +const remoteRepo: Repo = { + id: 'remote-repo', + path: '/srv/orca', + displayName: 'orca', + badgeColor: '#737373', + upstream: { owner: 'stablyai', repo: 'orca' }, + addedAt: 1_700_000_001_000 +} + +const localProject: Project = { + id: SHARED_PROJECT_ID, + displayName: 'orca', + badgeColor: '#22c55e', + sourceRepoIds: ['local-repo'], + createdAt: 1, + updatedAt: 1 +} + +const remoteProject: Project = { + ...localProject, + badgeColor: '#737373', + sourceRepoIds: ['remote-repo'], + createdAt: 2, + updatedAt: 2 +} + +function setup(repoId: string, path: string): ProjectHostSetup { + return { + id: `${repoId}-setup`, + projectId: SHARED_PROJECT_ID, + hostId: 'local', + repoId, + path, + displayName: 'orca', + setupState: 'ready', + setupMethod: 'imported-existing-folder', + createdAt: 1, + updatedAt: 1 + } +} + +const reposList = vi.fn() +const projectsList = vi.fn() +const listHostSetups = vi.fn() +const runtimeEnvironmentsList = vi.fn() +const runtimeEnvironmentTransportCall = vi.fn() + +// Why: catalogs arrive over IPC, so every fetch must hand back freshly allocated rows — +// otherwise identity would match by accident and prove nothing. +function clone(value: T): T { + return structuredClone(value) +} + +function runtimeResult(method: string): unknown { + if (method === 'repo.list') { + return { repos: [clone(remoteRepo)] } + } + if (method === 'project.list') { + return { projects: [clone(remoteProject)] } + } + if (method === 'projectHostSetup.list') { + return { setups: [clone(setup('remote-repo', '/srv/orca'))] } + } + return {} +} + +beforeEach(() => { + clearRuntimeCompatibilityCacheForTests() + reposList.mockReset() + projectsList.mockReset() + listHostSetups.mockReset() + runtimeEnvironmentsList.mockReset() + runtimeEnvironmentTransportCall.mockReset() + + reposList.mockImplementation(async () => [clone(localRepo)]) + projectsList.mockImplementation(async () => [clone(localProject)]) + listHostSetups.mockImplementation(async () => [clone(setup('local-repo', '/local/orca'))]) + runtimeEnvironmentsList.mockResolvedValue([{ id: 'env-1', name: 'awin' }]) + runtimeEnvironmentTransportCall.mockImplementation((args: RuntimeEnvironmentCallRequest) => { + const compatible = createCompatibleRuntimeStatusResponseIfNeeded(args) + if (compatible) { + return compatible + } + return { + id: `rpc-${args.method}`, + ok: true, + result: runtimeResult(args.method), + _meta: { runtimeId: 'runtime-remote' } + } + }) + + vi.stubGlobal('window', { + api: { + repos: { list: reposList }, + projects: { list: projectsList, listHostSetups }, + projectGroups: { list: vi.fn().mockResolvedValue([]) }, + folderWorkspaces: { list: vi.fn().mockResolvedValue([]) }, + runtimeEnvironments: { + call: runtimeEnvironmentTransportCall, + list: runtimeEnvironmentsList + } + }, + dispatchEvent: vi.fn() + }) +}) + +function sharedProject(store: ReturnType): Project | undefined { + return store.getState().projects.find((project) => project.id === SHARED_PROJECT_ID) +} + +describe('cross-host project refresh identity', () => { + it('keeps sourceRepoIds ordered the same whichever host refreshed', async () => { + const store = createTestStore() + // Why: with no active env, `fetchRepos` targets local, so the two calls below really do + // alternate hosts. + store.setState({ settings: { activeRuntimeEnvironmentId: null } as never }) + await store.getState().fetchReposForAllHosts() + const afterAllHosts = sharedProject(store)?.sourceRepoIds + expect(afterAllHosts).toHaveLength(2) + + await store.getState().fetchRepos() + const afterLocal = sharedProject(store)?.sourceRepoIds + + await store.getState().fetchRuntimeEnvironmentRepos('env-1') + const afterRuntime = sharedProject(store)?.sourceRepoIds + + expect(afterLocal).toEqual(afterAllHosts) + expect(afterRuntime).toEqual(afterAllHosts) + }) + + it('reuses the cross-host project row across alternating single-host refreshes', async () => { + const store = createTestStore() + store.setState({ settings: { activeRuntimeEnvironmentId: null } as never }) + await store.getState().fetchReposForAllHosts() + // Why: the first local pass lands before the runtime catalog exists, so settle onto the + // two-host fixed point before measuring identity. + await store.getState().fetchRepos() + const settled = sharedProject(store) + + await store.getState().fetchRuntimeEnvironmentRepos('env-1') + expect(sharedProject(store)).toBe(settled) + + await store.getState().fetchRepos() + expect(sharedProject(store)).toBe(settled) + }) + + it('reuses the cross-host project row across a no-op all-hosts refresh', async () => { + const store = createTestStore() + store.setState({ settings: { activeRuntimeEnvironmentId: 'env-1' } as never }) + await store.getState().fetchReposForAllHosts() + await store.getState().fetchReposForAllHosts() + const settled = sharedProject(store) + + await store.getState().fetchReposForAllHosts() + + expect(sharedProject(store)).toBe(settled) + }) +}) diff --git a/src/renderer/src/store/slices/repos-refresh-identity.test.ts b/src/renderer/src/store/slices/repos-refresh-identity.test.ts index 193bd045ede..98750177831 100644 --- a/src/renderer/src/store/slices/repos-refresh-identity.test.ts +++ b/src/renderer/src/store/slices/repos-refresh-identity.test.ts @@ -229,6 +229,57 @@ describe('repo catalog refresh identity', () => { expect(store.getState().projectHostSetups).toHaveLength(1) }) + it('keeps a project owned only through a repo on another host', async () => { + // Why: no setup row names this project, so the refreshing host can only be ruled out from the + // project's own source repos. Feeding the host-id resolvers anything but this project's repo + // slice prunes it on a local refresh. + const sshRepo: Repo = { ...secondRepo, executionHostId: 'ssh:host-a', connectionId: 'host-a' } + mockRepos(repo, sshRepo) + const store = createTestStore() + await store.getState().fetchRepos() + const sshOwned: Project = { + id: 'ssh-owned', + displayName: 'SSH Owned', + badgeColor: '#000000', + sourceRepoIds: [sshRepo.id], + createdAt: 1, + updatedAt: 1 + } + store.setState({ + projects: [...store.getState().projects, sshOwned], + projectHostSetups: [] + }) + + await store.getState().fetchRepos() + + expect(store.getState().projects.map((project) => project.id)).toContain('ssh-owned') + }) + + it('keeps a project whose repo id is cloned onto a second host', async () => { + // Why: both rows share `repo.id`, so the project's repo slice must carry every duplicate — + // keeping only the first drops the remote host and the local refresh prunes the project. + const sshRepo: Repo = { ...repo, executionHostId: 'ssh:host-a', connectionId: 'host-a' } + mockRepos(repo, sshRepo) + const store = createTestStore() + await store.getState().fetchRepos() + const dualHostOwned: Project = { + id: 'dual-host-owned', + displayName: 'Dual Host Owned', + badgeColor: '#000000', + sourceRepoIds: [repo.id], + createdAt: 1, + updatedAt: 1 + } + store.setState({ + projects: [...store.getState().projects, dualHostOwned], + projectHostSetups: [] + }) + + await store.getState().fetchRepos() + + expect(store.getState().projects.map((project) => project.id)).toContain('dual-host-owned') + }) + it('adds a project and its setup when a repo appears', async () => { const store = createTestStore() await store.getState().fetchRepos() diff --git a/src/renderer/src/store/slices/repos.ts b/src/renderer/src/store/slices/repos.ts index 059baa6dde5..3cfa784209d 100644 --- a/src/renderer/src/store/slices/repos.ts +++ b/src/renderer/src/store/slices/repos.ts @@ -48,12 +48,9 @@ import { applyManualRepoOrder, getManualRepoOrder } from '../../../../shared/man import { getProjectGroupSubtreeIds } from '../../../../shared/project-groups' import { isPathInsideOrEqual } from '../../../../shared/cross-platform-path' import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id' +import { structuralValuesEqual } from '../../../../shared/structural-value-equality' import { selectProjectGroupRemovalTargets } from './project-group-removal-targets' -import { - areValuesEqual, - reconcileCatalogRows, - reconcileFetchedRepos -} from './repo-identity-reconcile' +import { reconcileCatalogRows, reconcileFetchedRepos } from './repo-identity-reconcile' import { retainValidFilterRepoIds } from './repo-filter-selection' import { mergeSshRepoReadoptions, @@ -675,10 +672,13 @@ function getMergedSourceRepoIdsForHostRefresh( reposById: ReadonlyMap, hostId: string ): string[] { + // Why: current-first keeps the order independent of which host refreshed. Prefixing the + // cross-host remainder made it a function of hostId, so a cross-host project's ids oscillated + // between refreshes and the projects reconcile could never reuse the row. return [ ...new Set([ - ...getSourceRepoIdsOutsideHost(previous, reposById, hostId), - ...getCurrentSourceRepoIds(current, new Set(reposById.keys())) + ...getCurrentSourceRepoIds(current, new Set(reposById.keys())), + ...getSourceRepoIdsOutsideHost(previous, reposById, hostId) ]) ] } @@ -1022,7 +1022,7 @@ function mergeByIdentity( changed = true continue } - if (areValuesEqual(merged[index], entry)) { + if (structuralValuesEqual(merged[index], entry)) { continue } merged[index] = entry @@ -1043,7 +1043,8 @@ function unchangedMergeSource( // Why: the sidebar effect watching these catalog arrays is the only thing that refills the // folder path-status cache, so a no-op refetch must not wipe it — nothing would repopulate it. -function catalogRowsUnchanged(next: readonly T[], previous: readonly T[]): boolean { +// Element identity only; `structuralValuesEqual` is the structural counterpart. +function arrayElementsUnchanged(next: readonly T[], previous: readonly T[]): boolean { return ( next === previous || (next.length === previous.length && next.every((row, index) => row === previous[index])) @@ -1061,19 +1062,10 @@ function mergeFetchedReposForHost( const existingHostId = getRepoExecutionHostId(repo) return existingHostId !== hostId || fetchedIdentities.has(getRepoHostIdentity(repo)) }) - const merged = [...preserved] - const indexByIdentity = new Map(merged.map((repo, index) => [getRepoHostIdentity(repo), index])) - for (const repo of fetchedWithProjectGroups) { - const identity = getRepoHostIdentity(repo) - const existingIndex = indexByIdentity.get(identity) - if (existingIndex === undefined) { - indexByIdentity.set(identity, merged.length) - merged.push(repo) - continue - } - merged[existingIndex] = repo - } - return reconcileFetchedRepos(previous, merged) + return reconcileFetchedRepos( + previous, + mergeByIdentity(preserved, fetchedWithProjectGroups, getRepoHostIdentity) + ) } function applyInheritedProjectGroups(previous: readonly Repo[], fetched: readonly Repo[]): Repo[] { @@ -2135,7 +2127,7 @@ export const createRepoSlice: StateCreator = (set, pendingSshRepoReadoptions: reconciliation.pendingReadoptions, ...reconcileReadoptedSshWorktreeState(s, s.pendingSshRepoReadoptions), ...mergedProjectCompatibility, - ...(catalogRowsUnchanged(prunedRepos, s.repos) + ...(arrayElementsUnchanged(prunedRepos, s.repos) ? {} : { folderWorkspacePathStatuses: {} }), activeRepoId: s.activeRepoId && validRepoIds.has(s.activeRepoId) ? s.activeRepoId : null, @@ -2288,7 +2280,7 @@ export const createRepoSlice: StateCreator = (set, pendingSshRepoReadoptions: reconciliation.pendingReadoptions, ...reconcileReadoptedSshWorktreeState(s, s.pendingSshRepoReadoptions), ...mergedProjectCompatibility, - ...(catalogRowsUnchanged(finalizedRepos, s.repos) + ...(arrayElementsUnchanged(finalizedRepos, s.repos) ? {} : { folderWorkspacePathStatuses: {} }), activeRepoId: s.activeRepoId, @@ -2377,7 +2369,7 @@ export const createRepoSlice: StateCreator = (set, const { projectGroups } = mergeFetchedProjectGroupCatalog(catalog, current.projectGroups) return { projectGroups, - ...(catalogRowsUnchanged(projectGroups, current.projectGroups) + ...(arrayElementsUnchanged(projectGroups, current.projectGroups) ? {} : { folderWorkspacePathStatuses: {} }) } @@ -2400,7 +2392,7 @@ export const createRepoSlice: StateCreator = (set, const { projectGroups } = mergeFetchedProjectGroupCatalog(catalog, s.projectGroups) return { projectGroups, - ...(catalogRowsUnchanged(projectGroups, s.projectGroups) + ...(arrayElementsUnchanged(projectGroups, s.projectGroups) ? {} : { folderWorkspacePathStatuses: {} }) } @@ -2464,7 +2456,7 @@ export const createRepoSlice: StateCreator = (set, ) return { folderWorkspaces, - ...(catalogRowsUnchanged(folderWorkspaces, current.folderWorkspaces) + ...(arrayElementsUnchanged(folderWorkspaces, current.folderWorkspaces) ? {} : { folderWorkspacePathStatuses: {} }) } @@ -2502,7 +2494,7 @@ export const createRepoSlice: StateCreator = (set, ) return { folderWorkspaces, - ...(catalogRowsUnchanged(folderWorkspaces, current.folderWorkspaces) + ...(arrayElementsUnchanged(folderWorkspaces, current.folderWorkspaces) ? {} : { folderWorkspacePathStatuses: {} }) } diff --git a/src/renderer/src/store/slices/superseded-ssh-repo-rows.ts b/src/renderer/src/store/slices/superseded-ssh-repo-rows.ts index c18e0f6e5af..a9880577f60 100644 --- a/src/renderer/src/store/slices/superseded-ssh-repo-rows.ts +++ b/src/renderer/src/store/slices/superseded-ssh-repo-rows.ts @@ -1,6 +1,7 @@ import type { SshRepoReadoption } from '../../../../shared/ssh-types' import type { Repo } from '../../../../shared/types' import { getRepoExecutionHostId, toSshExecutionHostId } from '../../../../shared/execution-host' +import { getRepoHostIdentityForParts } from '../../../../shared/repo-host-identity' export type SshRepoReconciliation = { repos: readonly Repo[] @@ -14,10 +15,6 @@ function repoBelongsToTarget(repo: Repo, targetId: string): boolean { ) } -function repoOwnerKey(hostId: string, repoId: string): string { - return `${hostId}\0${repoId}` -} - /** * Drops old-host rows only when main reports the exact repo re-adoption and the * renderer has received the corresponding new-host row. Evidence stays pending @@ -32,7 +29,7 @@ export function reconcileReadoptedSshRepoRows( const directSshOwners = new Set( repos.flatMap((repo) => repo.connectionId && repoBelongsToTarget(repo, repo.connectionId) - ? [repoOwnerKey(getRepoExecutionHostId(repo), repo.id)] + ? [getRepoHostIdentityForParts(repo.id, getRepoExecutionHostId(repo))] : [] ) ) @@ -40,13 +37,18 @@ export function reconcileReadoptedSshRepoRows( for (const readoption of readoptions) { const pendingRepoIds: string[] = [] for (const repoId of readoption.repoIds) { - const newOwner = repoOwnerKey(toSshExecutionHostId(readoption.newTargetId), repoId) + const newOwner = getRepoHostIdentityForParts( + repoId, + toSshExecutionHostId(readoption.newTargetId) + ) const hasNewRow = directSshOwners.has(newOwner) if (!hasNewRow) { pendingRepoIds.push(repoId) continue } - prunedOwners.add(repoOwnerKey(toSshExecutionHostId(readoption.oldTargetId), repoId)) + prunedOwners.add( + getRepoHostIdentityForParts(repoId, toSshExecutionHostId(readoption.oldTargetId)) + ) } if (pendingRepoIds.length > 0) { pendingReadoptions.push({ ...readoption, repoIds: pendingRepoIds }) @@ -60,7 +62,8 @@ export function reconcileReadoptedSshRepoRows( } return { repos: repos.filter( - (repo) => !prunedOwners.has(repoOwnerKey(getRepoExecutionHostId(repo), repo.id)) + (repo) => + !prunedOwners.has(getRepoHostIdentityForParts(repo.id, getRepoExecutionHostId(repo))) ), pendingReadoptions } diff --git a/src/renderer/src/store/slices/worktree-catalog-reconciliation.ts b/src/renderer/src/store/slices/worktree-catalog-reconciliation.ts index 1ef339a1808..500f002dc59 100644 --- a/src/renderer/src/store/slices/worktree-catalog-reconciliation.ts +++ b/src/renderer/src/store/slices/worktree-catalog-reconciliation.ts @@ -1,41 +1,14 @@ +import { structuralValuesEqualIgnoringUndefined } from '../../../../shared/structural-value-equality' + type CatalogRow = { id: string } -function isPlainRecord(value: unknown): value is Record { - if (typeof value !== 'object' || value === null) { - return false - } - const prototype = Object.getPrototypeOf(value) - return prototype === Object.prototype || prototype === null -} - -function catalogValuesEqual(left: unknown, right: unknown): boolean { - if (Object.is(left, right)) { - return true - } - if (Array.isArray(left) || Array.isArray(right)) { - if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) { - return false - } - return left.every((value, index) => catalogValuesEqual(value, right[index])) - } - if (!isPlainRecord(left) || !isPlainRecord(right)) { - return false - } - const keys = new Set([...Object.keys(left), ...Object.keys(right)]) - for (const key of keys) { - if (!catalogValuesEqual(left[key], right[key])) { - return false - } - } - return true -} - // NOTHING HITS THIS TODAY: both callers key on ids that are unique by // construction, so buckets stay at 1-3. It only bounds the damage if that ever // changes — a same-id bucket costs one deep compare per candidate, so an -// unbounded one is O(k^2). A cap beats a second index that would have to stay in -// step with catalogValuesEqual, and reuse is only an optimization: dropping a -// match past the window costs object identity, never correctness. +// unbounded one is O(k^2). A cap beats an index keyed on a second walk that +// would have to stay in step with structuralValuesEqualIgnoringUndefined, and +// reuse is only an optimization: dropping a match past the window costs object +// identity, never correctness. const MAX_DUPLICATE_ID_SCAN = 8 export function reuseEqualCatalogRows( @@ -62,7 +35,7 @@ export function reuseEqualCatalogRows( const scanLimit = Math.min(candidates.length, MAX_DUPLICATE_ID_SCAN) for (let index = 0; index < scanLimit; index++) { const candidate = candidates[index] - if (candidate !== undefined && catalogValuesEqual(candidate, row)) { + if (candidate !== undefined && structuralValuesEqualIgnoringUndefined(candidate, row)) { candidates.splice(index, 1) return candidate } diff --git a/src/shared/manual-repo-order.ts b/src/shared/manual-repo-order.ts index 9b5bfee39a1..9813412c676 100644 --- a/src/shared/manual-repo-order.ts +++ b/src/shared/manual-repo-order.ts @@ -1,8 +1,9 @@ import { getRepoExecutionHostId, normalizeExecutionHostId } from './execution-host' +import { getRepoHostIdentityForParts } from './repo-host-identity' import type { ManualRepoOrderEntry, Repo } from './types' function getEntryKey(entry: ManualRepoOrderEntry): string { - return `${entry.hostId}\0${entry.repoId}` + return getRepoHostIdentityForParts(entry.repoId, entry.hostId) } export function normalizeManualRepoOrder(value: unknown): ManualRepoOrderEntry[] { diff --git a/src/shared/repo-host-identity.ts b/src/shared/repo-host-identity.ts new file mode 100644 index 00000000000..8309899d4e6 --- /dev/null +++ b/src/shared/repo-host-identity.ts @@ -0,0 +1,5 @@ +export function getRepoHostIdentityForParts(repoId: string, hostId: string): string { + // Why: host ids and repo ids can contain punctuation; NUL keeps the composite + // key collision-free without escaping user/provider-owned strings. + return `${hostId}\0${repoId}` +} diff --git a/src/shared/structural-value-equality.test.ts b/src/shared/structural-value-equality.test.ts new file mode 100644 index 00000000000..57a7a471dd5 --- /dev/null +++ b/src/shared/structural-value-equality.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest' +import { + structuralValuesEqual, + structuralValuesEqualIgnoringUndefined +} from './structural-value-equality' + +const comparators = [ + ['structuralValuesEqual', structuralValuesEqual], + ['structuralValuesEqualIgnoringUndefined', structuralValuesEqualIgnoringUndefined] +] as const + +describe.each(comparators)('%s', (_name, valuesEqual) => { + it('compares primitives and identical references', () => { + expect(valuesEqual('a', 'a')).toBe(true) + expect(valuesEqual('a', 'b')).toBe(false) + expect(valuesEqual(null, null)).toBe(true) + expect(valuesEqual(null, {})).toBe(false) + expect(valuesEqual(undefined, null)).toBe(false) + const fn = (): void => {} + expect(valuesEqual(fn, fn)).toBe(true) + expect(valuesEqual(fn, (): void => {})).toBe(false) + }) + + it('walks arrays element-wise and never mixes them with records', () => { + expect(valuesEqual([1, [2, { a: 'b' }]], [1, [2, { a: 'b' }]])).toBe(true) + expect(valuesEqual([1, 2], [1, 2, 3])).toBe(false) + expect(valuesEqual([1, 2], [2, 1])).toBe(false) + expect(valuesEqual([], {})).toBe(false) + expect(valuesEqual({ 0: 1, length: 1 }, [1])).toBe(false) + }) + + it('walks nested plain records rebuilt by structured clone', () => { + const record = { id: 'a', nested: { labels: ['one', 'two'], flags: { on: true } } } + expect(valuesEqual(record, structuredClone(record))).toBe(true) + expect( + valuesEqual(record, { ...record, nested: { labels: ['one'], flags: { on: true } } }) + ).toBe(false) + }) + + it('walks null-prototype records like literals', () => { + const nullProto: Record = Object.create(null) + nullProto.a = 1 + expect(valuesEqual(nullProto, { a: 1 })).toBe(true) + expect(valuesEqual(nullProto, { a: 2 })).toBe(false) + }) + + it('falls back to reference equality for non-plain objects', () => { + const date = new Date(0) + expect(valuesEqual(date, date)).toBe(true) + expect(valuesEqual(date, new Date(0))).toBe(false) + expect(valuesEqual(new Map([['a', 1]]), new Map([['a', 1]]))).toBe(false) + expect(valuesEqual(new Set([1]), new Set([1]))).toBe(false) + class Row { + x = 1 + } + expect(valuesEqual(new Row(), new Row())).toBe(false) + expect(valuesEqual(new Row(), { x: 1 })).toBe(false) + }) + + it('ignores symbol keys', () => { + const key = Symbol('marker') + expect(valuesEqual({ [key]: 1, a: 1 }, { [key]: 2, a: 1 })).toBe(true) + }) +}) + +describe('structuralValuesEqual', () => { + it('treats an absent key as different from a key holding undefined', () => { + // Why: repo/project merges branch on `'key' in project`, so key presence is load-bearing. + expect(structuralValuesEqual({ a: 1 }, { a: 1, b: undefined })).toBe(false) + expect(structuralValuesEqual({ a: 1, b: undefined }, { a: 1 })).toBe(false) + expect(structuralValuesEqual({ a: 1, b: undefined }, { a: 1, c: undefined })).toBe(false) + expect(structuralValuesEqual({ a: 1, b: undefined }, { a: 1, b: undefined })).toBe(true) + }) + + it('compares leaves with === so NaN is never equal and -0 matches 0', () => { + expect(structuralValuesEqual({ a: Number.NaN }, { a: Number.NaN })).toBe(false) + expect(structuralValuesEqual({ a: 0 }, { a: -0 })).toBe(true) + }) +}) + +describe('structuralValuesEqualIgnoringUndefined', () => { + it('treats an absent key as equal to a key holding undefined', () => { + // Why: locally built worktree rows carry explicit undefined fields the host omits. + expect(structuralValuesEqualIgnoringUndefined({ a: 1 }, { a: 1, b: undefined })).toBe(true) + expect(structuralValuesEqualIgnoringUndefined({ a: 1, b: undefined }, { a: 1 })).toBe(true) + expect( + structuralValuesEqualIgnoringUndefined({ a: 1, b: undefined }, { a: 1, c: undefined }) + ).toBe(true) + expect(structuralValuesEqualIgnoringUndefined({ a: 1 }, { a: 1, b: 2 })).toBe(false) + }) + + it('compares leaves with Object.is so NaN is equal and -0 differs from 0', () => { + expect(structuralValuesEqualIgnoringUndefined({ a: Number.NaN }, { a: Number.NaN })).toBe(true) + expect(structuralValuesEqualIgnoringUndefined({ a: 0 }, { a: -0 })).toBe(false) + }) +}) diff --git a/src/shared/structural-value-equality.ts b/src/shared/structural-value-equality.ts new file mode 100644 index 00000000000..5714bb7f1ed --- /dev/null +++ b/src/shared/structural-value-equality.ts @@ -0,0 +1,82 @@ +// Why: catalog reconcilers compare rows that IPC structured-clone (and main's hydration) rebuild on +// every fetch, so a reference compare reports every row as changed and nothing ever reconciles. +// Only plain records and arrays are walked; anything exotic (Date, Map, class instance) falls back +// to reference equality rather than being mistaken for an empty record. + +type ValueEqualityPolicy = { + // Why: `Object.is` makes NaN equal NaN but 0 unequal -0; `===` does the opposite. + readonly sameValueLeaves: boolean + readonly absentKeyEqualsUndefined: boolean +} + +const STRICT_OWN_KEYS: ValueEqualityPolicy = { + sameValueLeaves: false, + absentKeyEqualsUndefined: false +} + +const UNION_OF_KEYS: ValueEqualityPolicy = { + sameValueLeaves: true, + absentKeyEqualsUndefined: true +} + +function isPlainRecord(value: unknown): value is Record { + if (typeof value !== 'object' || value === null) { + return false + } + const prototype = Object.getPrototypeOf(value) + return prototype === Object.prototype || prototype === null +} + +function valuesEqual(a: unknown, b: unknown, policy: ValueEqualityPolicy): boolean { + if (policy.sameValueLeaves ? Object.is(a, b) : a === b) { + return true + } + if (Array.isArray(a) || Array.isArray(b)) { + return ( + Array.isArray(a) && + Array.isArray(b) && + a.length === b.length && + a.every((item, index) => valuesEqual(item, b[index], policy)) + ) + } + if (!isPlainRecord(a) || !isPlainRecord(b)) { + return false + } + if (policy.absentKeyEqualsUndefined) { + for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) { + if (!valuesEqual(a[key], b[key], policy)) { + return false + } + } + return true + } + const keys = Object.keys(a) + if (keys.length !== Object.keys(b).length) { + return false + } + return keys.every( + (key) => Object.hasOwn(b, key) && valuesEqual(a[key], b[key], policy) + ) +} + +/** + * Structural compare where an absent own key differs from a key that is present and holds + * `undefined`, and leaves compare with `===`. + * + * Why the strict key set: the repo/project merges branch on `'localWindowsRuntimePreference' in + * project`, so a key appearing or disappearing is a real change even when its value is `undefined`. + */ +export function structuralValuesEqual(a: unknown, b: unknown): boolean { + return valuesEqual(a, b, STRICT_OWN_KEYS) +} + +/** + * Structural compare that treats an absent own key as equal to a key holding `undefined`, and + * compares leaves with `Object.is`. + * + * Why the loose key set: locally constructed worktree catalog rows carry explicit `undefined` + * fields that the host simply omits, and no consumer of those rows uses the `in` operator. + */ +export function structuralValuesEqualIgnoringUndefined(a: unknown, b: unknown): boolean { + return valuesEqual(a, b, UNION_OF_KEYS) +} From bac21ee64def57cddcc20998d88264b49b891887 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:20:59 -0700 Subject: [PATCH 06/42] fix(worktree-scan): keep the admin-fingerprint probe inside the caller's per-repo budget (#14454) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(worktree-scan): keep the admin-fingerprint wait inside the caller's per-repo budget The awaited probe was capped at 10s while `computeResolvedWorktrees` gives each repo 5s, so a slow mount always blew the budget: the caller gave up and republished persisted rows. The resolved snapshot was then stamped from the *start* of the compute, so a compute longer than its 1s TTL published an already-expired entry and the next poll repeated the whole 5s wait — deterministically, on every TTL expiry. Cap the probe at 2s so the remaining budget still covers the fallback `git worktree list`, and stamp the snapshot on completion. * fix(worktree-scan): derive the probe deadline from the caller budget A flat 2s cut reuse for hosts whose probe lands between 2s and 5s, which used to fit the caller's budget — trading the stall for a repeating `git worktree list`. Subtract a fallback-scan allowance from RESOLVED_WORKTREE_REPO_TIMEOUT_MS instead, so the invariant holds by construction and only probes that could not have fitted are cut. Tests now pin both ends: too large fails the budget invariant, too small fails reuse for a slow-but-healthy probe. --- src/main/runtime/orca-runtime.ts | 26 ++++-- ...rktree-scan-admin-fingerprint-gate.test.ts | 91 ++++++++++++++++++- 2 files changed, 106 insertions(+), 11 deletions(-) diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 807eef35c05..be28a589c70 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -29448,7 +29448,6 @@ export class OrcaRuntimeService { if (!this.store) { return { worktrees: [], platformByRepoId: new Map() } } - const now = Date.now() const metaById = this.store.getAllWorktreeMeta() ?? {} const repos = this.store.getRepos() const projectRuntimeByRepoId = resolveLocalProjectRuntimesForRepos(this.requireStore(), repos) @@ -29529,11 +29528,13 @@ export class OrcaRuntimeService { this.store?.getAllWorktreeLineage?.() ?? {} ) // Why: short TTL avoids shelling out on every frequent poll while still catching worktree changes made outside Orca. + // Why stamped on completion, not entry: a compute that spent longer than the TTL would otherwise publish an + // already-expired entry, so the very next poll recomputes and every caller repeats the same slow path. if (generation === this.resolvedWorktreeGeneration) { this.resolvedWorktreeCache = { worktrees, platformByRepoId, - expiresAt: now + RESOLVED_WORKTREE_CACHE_TTL_MS + expiresAt: Date.now() + RESOLVED_WORKTREE_CACHE_TTL_MS } } return { worktrees, platformByRepoId } @@ -36055,13 +36056,20 @@ const WORKTREE_SCAN_AGENT_SCRATCH_TTL_MS = 5 * 60_000 // edits are invisible to it and a tip living in packed-refs or reftable only gets an mtime + size // stamp, so a real scan still runs on this interval even while the probe reports "unchanged". export const WORKTREE_SCAN_ADMIN_RECONCILE_INTERVAL_MS = 5 * 60_000 -// Why so generous: a healthy but slow host (100+ linked worktrees, Windows Defender, cold dentry -// cache, cloud placeholders) must still get to reuse its scan. Well under WORKTREE_LIST_TIMEOUT_MS, -// and expiring yields `null` — the existing "cannot prove unchanged" sentinel, so a real scan runs. -// Exceeding RESOLVED_WORKTREE_REPO_TIMEOUT_MS is deliberate, not a bug: the payoff is skipping a -// `git worktree list` subprocess, not cutting this caller's latency — that caller is already capped. -export const WORKTREE_SCAN_ADMIN_FINGERPRINT_TIMEOUT_MS = 10_000 -const RESOLVED_WORKTREE_REPO_TIMEOUT_MS = 5000 +export const RESOLVED_WORKTREE_REPO_TIMEOUT_MS = 5000 +// Why reserved rather than spent on the probe: when the probe expires the caller still has to run +// `git worktree list` and answer inside the same budget, so the fallback needs its own room. Sized +// for a healthy Git on a busy host, well above the tens of milliseconds a warm list costs. +const WORKTREE_SCAN_FALLBACK_ALLOWANCE_MS = 1500 +// Why derived from the caller's budget instead of a generous absolute: this wait runs *inside* +// RESOLVED_WORKTREE_REPO_TIMEOUT_MS, so outlasting it buys nothing — the caller has already given up +// and restored persisted rows — while turning a reusable scan into a full-budget stall that repeats +// on every TTL expiry. Subtracting keeps that invariant true by construction if either side moves. +// Why not smaller: the probe reads a subset of what the fallback scan reads, so a probe too slow to +// fit is a scan that will not fit either — waiting is strictly better right up to the budget. +// Expiring yields `null`, the existing "cannot prove unchanged" sentinel, so a real scan runs. +export const WORKTREE_SCAN_ADMIN_FINGERPRINT_TIMEOUT_MS = + RESOLVED_WORKTREE_REPO_TIMEOUT_MS - WORKTREE_SCAN_FALLBACK_ALLOWANCE_MS export function resolveWorktreeScanCacheTtlMs(repo: Pick): number { return !repo.connectionId && isAgentScratchRepoRootPath(repo.path) diff --git a/src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts b/src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts index e1bf370de1d..ae845e42e67 100644 --- a/src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts +++ b/src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts @@ -39,6 +39,7 @@ vi.mock('./repo-worktree-admin-fingerprint', () => ({ import { OrcaRuntimeService, + RESOLVED_WORKTREE_REPO_TIMEOUT_MS, WORKTREE_SCAN_ADMIN_FINGERPRINT_TIMEOUT_MS, WORKTREE_SCAN_ADMIN_RECONCILE_INTERVAL_MS } from './orca-runtime' @@ -49,6 +50,10 @@ const WORKTREE_PATH = '/Users/me/dev/app-feature' const WORKTREE_ID = `${REPO_ID}::${WORKTREE_PATH}` const MAIN_WORKTREE_ID = `${REPO_ID}::${REPO_PATH}` const SCAN_TTL_MS = 30_000 +// A busy host (100+ linked worktrees, Defender, cold dentry cache, cloud placeholders) can spend +// seconds in pure stat time. Reuse has to survive that, or trimming the probe's deadline to fit the +// caller budget just trades a repeating stall for a repeating `git worktree list`. +const SLOW_BUT_HEALTHY_PROBE_MS = 3_000 function makeMeta(overrides: Record = {}) { return { @@ -116,11 +121,14 @@ function makeRuntime( ): { runtime: OrcaRuntimeService list: () => Promise + store: ReturnType } { - const runtime = new OrcaRuntimeService(makeStore(options) as never) + const store = makeStore(options) + const runtime = new OrcaRuntimeService(store as never) return { runtime, - list: () => (runtime as unknown as RuntimeInternals).listResolvedWorktrees() + list: () => (runtime as unknown as RuntimeInternals).listResolvedWorktrees(), + store } } @@ -128,6 +136,11 @@ function scanCount(): number { return listWorktreesStrictMock.mock.calls.length } +/** Scanned rows carry the mocked tips; the persisted-row fallback publishes empty ones. */ +function heads(worktrees: unknown): string[] { + return (worktrees as { git: { head: string } }[]).map((worktree) => worktree.git.head).sort() +} + /** Let every already-settled promise chain run without advancing the clock, so a stall stays a stall. */ async function drainMicrotasks(): Promise { for (let tick = 0; tick < 200; tick += 1) { @@ -455,6 +468,80 @@ describe('worktree scan admin-fingerprint gate', () => { } }) + it('bounds the awaited probe by the caller per-repo budget', () => { + // The wait runs inside that budget, so outlasting it can only strand the caller on persisted rows. + expect(WORKTREE_SCAN_ADMIN_FINGERPRINT_TIMEOUT_MS).toBeLessThan( + RESOLVED_WORKTREE_REPO_TIMEOUT_MS + ) + }) + + it('still reuses the scan when a slow probe answers inside its deadline', async () => { + vi.useFakeTimers() + try { + const { list } = makeRuntime() + await list() + expect(scanCount()).toBe(1) + + const releaseSlowProbe = stallProbeOnce() + vi.advanceTimersByTime(SCAN_TTL_MS + 1_000) + const second = list() + + // Deliberately an absolute figure, not one derived from the deadline: this is the lower end of + // the band, and a self-referential advance would still pass however far the deadline is cut. + await vi.advanceTimersByTimeAsync(SLOW_BUT_HEALTHY_PROBE_MS) + releaseSlowProbe('fp-1') + await second + expect(scanCount()).toBe(1) + } finally { + vi.useRealTimers() + } + }) + + it('still returns scanned rows within the per-repo budget when the probe stalls', async () => { + vi.useFakeTimers() + try { + const { list } = makeRuntime() + await list() + + stallProbeOnce() + vi.advanceTimersByTime(SCAN_TTL_MS + 1_000) + const second = list() + const secondSettled = trackSettled(second) + + // The probe's own deadline has to end the wait; if the caller's fallback gets there first the + // repo answers with persisted rows on every TTL expiry. + await vi.advanceTimersByTimeAsync(WORKTREE_SCAN_ADMIN_FINGERPRINT_TIMEOUT_MS) + await drainMicrotasks() + expect(secondSettled()).toBe(true) + expect(scanCount()).toBe(2) + expect(heads(await second)).toEqual(['abc', 'def']) + } finally { + vi.useRealTimers() + } + }) + + it('does not publish an already-expired snapshot after a slow compute', async () => { + vi.useFakeTimers() + try { + const { list, store } = makeRuntime() + await list() + + stallProbeOnce() + vi.advanceTimersByTime(SCAN_TTL_MS + 1_000) + const second = list() + await vi.advanceTimersByTimeAsync(WORKTREE_SCAN_ADMIN_FINGERPRINT_TIMEOUT_MS) + await second + + // The probe deadline outlasts the snapshot TTL, so a start-stamped entry is born expired and + // the next poll repeats the whole wait instead of reading the answer that just landed. + const getRepos = vi.spyOn(store, 'getRepos') + await list() + expect(getRepos).not.toHaveBeenCalled() + } finally { + vi.useRealTimers() + } + }) + it('shares one probe and one scan across concurrent callers', async () => { const { list } = makeRuntime() From b04da03ffcd06bc85303b288a8e9423d8ce263cf Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:23:52 -0700 Subject: [PATCH 07/42] Require an absolute Orca CLI for the agent-teams tmux shim (#14438) The generated tmux shim fell back to a bare orca / orca.cmd / orca-ide, and cmd.exe resolves an unqualified command against the current directory before PATH (sh does the same via ./empty PATH entries), so a stray orca.cmd in an agent's checkout could run with the agent-teams team id and token in its environment. Resolve only absolute paths, honor the Windows Path env spelling, degrade to in-process teammates when no CLI can be qualified, and make both shims exit 127 instead of guessing. Verified on macOS, Linux (dash + bash), and Windows (cmd.exe + Git Bash). Fixes STA-4215. --- .../runtime/claude-agent-teams-service.ts | 9 +- .../claude-agent-teams-shim-env.test.ts | 106 +++++++++++++++++- .../runtime/claude-agent-teams-shim-env.ts | 67 ++++++++--- src/main/runtime/orca-runtime.test.ts | 4 + 4 files changed, 166 insertions(+), 20 deletions(-) diff --git a/src/main/runtime/claude-agent-teams-service.ts b/src/main/runtime/claude-agent-teams-service.ts index 1b0199c6bd0..34ef35ecb36 100644 --- a/src/main/runtime/claude-agent-teams-service.ts +++ b/src/main/runtime/claude-agent-teams-service.ts @@ -26,7 +26,8 @@ export class ClaudeAgentTeamsService { leaderHandle: string baseEnv: Record shimDir: string - shimBin: string + /** Absolute path only; null leaves the var unset so the shim refuses to guess a cwd-relative CLI. */ + shimBin: string | null }): AgentTeamsLaunchEnv { const teamId = `team-${randomUUID()}` const token = randomBytes(32).toString('base64url') @@ -47,8 +48,10 @@ export class ClaudeAgentTeamsService { ORCA_AGENT_TEAMS_TEAM_ID: teamId, ORCA_AGENT_TEAMS_TOKEN: token, ORCA_AGENT_TEAMS_LEADER_PANE: leaderPane, - ORCA_AGENT_TEAMS_SHIM_DIR: args.shimDir, - ORCA_AGENT_TEAMS_SHIM_BIN: args.shimBin + ORCA_AGENT_TEAMS_SHIM_DIR: args.shimDir + } + if (args.shimBin) { + env.ORCA_AGENT_TEAMS_SHIM_BIN = args.shimBin } if (args.baseEnv.ORCA_PAIRING_CODE) { env.ORCA_PAIRING_CODE = args.baseEnv.ORCA_PAIRING_CODE diff --git a/src/main/runtime/claude-agent-teams-shim-env.test.ts b/src/main/runtime/claude-agent-teams-shim-env.test.ts index 362291e3c26..7c62d2ae01a 100644 --- a/src/main/runtime/claude-agent-teams-shim-env.test.ts +++ b/src/main/runtime/claude-agent-teams-shim-env.test.ts @@ -1,3 +1,5 @@ +import { spawnSync } from 'node:child_process' +import { existsSync } from 'node:fs' import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -5,7 +7,8 @@ import { afterEach, describe, expect, it } from 'vitest' import { buildClaudeAgentTeamsLaunchPlan, ensureClaudeAgentTeamsShimDir, - resolveClaudeAgentTeamsShimBin + resolveClaudeAgentTeamsShimBin, + windowsClaudeAgentTeamsShimScript } from './claude-agent-teams-shim-env' const roots: string[] = [] @@ -88,4 +91,105 @@ describe('claude agent teams shim env', () => { expect(resolveClaudeAgentTeamsShimBin({ PATH: root })).toBe(cliPath) }) + + it('refuses to resolve a CLI through relative PATH entries or a bare override', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-agent-teams-cli-')) + roots.push(root) + for (const name of ['orca', 'orca-ide', 'orca.cmd']) { + const path = join(root, name) + await writeFile(path, '#!/usr/bin/env sh\n', 'utf8') + if (process.platform !== 'win32') { + await chmod(path, 0o755) + } + } + + expect(resolveClaudeAgentTeamsShimBin({ PATH: '.' })).toBeNull() + expect(resolveClaudeAgentTeamsShimBin({ PATH: '' })).toBeNull() + expect( + resolveClaudeAgentTeamsShimBin({ PATH: '.', ORCA_AGENT_TEAMS_SHIM_BIN: 'orca' }) + ).toBeNull() + // Why: a bare override is still honored when it maps to a real absolute PATH entry. + expect(resolveClaudeAgentTeamsShimBin({ PATH: root, ORCA_AGENT_TEAMS_SHIM_BIN: 'orca' })).toBe( + join(root, 'orca') + ) + }) + + it.skipIf(process.platform !== 'win32')( + 'resolves through the Windows `Path` env spelling', + async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-agent-teams-cli-')) + roots.push(root) + const cliPath = join(root, 'orca.cmd') + await writeFile(cliPath, '@echo off\r\n', 'utf8') + + expect(resolveClaudeAgentTeamsShimBin({ Path: root })).toBe(cliPath) + } + ) + + it('falls back to in-process teammates when no absolute CLI can be qualified', async () => { + const createTeamEnv = (): Record => { + throw new Error('native shim env must not be built without a qualified CLI') + } + + await expect( + buildClaudeAgentTeamsLaunchPlan({ + command: 'claude', + mode: 'native-panes-shim', + baseEnv: { PATH: '.' }, + createTeamEnv + }) + ).resolves.toEqual({ + command: 'claude --teammate-mode in-process', + env: { CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1' } + }) + }) + + it.skipIf(process.platform === 'win32')( + 'never runs a cwd-resolved orca when the shim bin is unqualified', + async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-agent-teams-shim-')) + roots.push(root) + await ensureClaudeAgentTeamsShimDir(root) + const cwd = await mkdtemp(join(tmpdir(), 'orca-agent-teams-cwd-')) + roots.push(cwd) + const marker = join(cwd, 'hijacked') + for (const name of ['orca', 'orca-ide']) { + const decoy = join(cwd, name) + await writeFile(decoy, `#!/usr/bin/env sh\ntouch ${JSON.stringify(marker)}\n`, 'utf8') + await chmod(decoy, 0o755) + } + + const hijack = spawnSync(join(root, 'tmux'), ['display-message', '-p', '#{pane_id}'], { + cwd, + env: { PATH: `.:${process.env.PATH ?? ''}` }, + encoding: 'utf8' + }) + + expect(hijack.status).toBe(127) + expect(hijack.stderr).toContain('absolute path') + expect(existsSync(marker)).toBe(false) + + const cli = join(cwd, 'fake-orca') + await writeFile(cli, '#!/usr/bin/env sh\necho "ran $*"\n', 'utf8') + await chmod(cli, 0o755) + const qualified = spawnSync(join(root, 'tmux'), ['list-panes'], { + cwd, + env: { PATH: `.:${process.env.PATH ?? ''}`, ORCA_AGENT_TEAMS_SHIM_BIN: cli }, + encoding: 'utf8' + }) + + expect(qualified.status).toBe(0) + expect(qualified.stdout.trim()).toBe('ran agent-teams-tmux list-panes') + } + ) + + it('writes a Windows shim that rejects an unqualified shim bin', () => { + const script = windowsClaudeAgentTeamsShimScript() + + expect(script).not.toMatch(/^set "ORCA_AGENT_TEAMS_SHIM_BIN=orca/m) + expect(script).toContain('if "%ORCA_SHIM_BIN:~1,1%"==":" goto :run') + // Why: `call` would re-expand `%2`-style tmux pane args as batch parameters. + expect(script).toContain('\r\n"%ORCA_SHIM_BIN%" agent-teams-tmux %*\r\n') + expect(script).toContain('exit /b 127') + }) }) diff --git a/src/main/runtime/claude-agent-teams-shim-env.ts b/src/main/runtime/claude-agent-teams-shim-env.ts index 7f6c3a1922e..b8ff1b5a5c0 100644 --- a/src/main/runtime/claude-agent-teams-shim-env.ts +++ b/src/main/runtime/claude-agent-teams-shim-env.ts @@ -1,7 +1,7 @@ import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises' import { accessSync, constants, existsSync } from 'node:fs' import { homedir } from 'node:os' -import { delimiter, dirname, join } from 'node:path' +import { delimiter, dirname, isAbsolute, join } from 'node:path' import { addClaudeTeammateModeAuto, addClaudeTeammateModeInProcess, @@ -9,6 +9,7 @@ import { type ClaudeAgentTeamsMode } from '../../shared/claude-agent-teams-tmux-compat' import { getOrcaCliCommandNameForPlatform } from '../../shared/orca-cli-command-name' +import { resolvePathEnvKey } from '../pty/windows-path-segment-merge' export type ClaudeAgentTeamsLaunchPlan = { command: string @@ -20,7 +21,7 @@ export async function ensureClaudeAgentTeamsShimDir(root = defaultShimRoot()): P await mkdir(root, { recursive: true }) await writeIfChanged(join(root, 'tmux'), unixShimScript()) if (process.platform === 'win32') { - await writeIfChanged(join(root, 'tmux.cmd'), windowsShimScript()) + await writeIfChanged(join(root, 'tmux.cmd'), windowsClaudeAgentTeamsShimScript()) } return root } @@ -41,8 +42,15 @@ export async function buildClaudeAgentTeamsLaunchPlan(args: { env: { CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1' } } } - const shimDir = await ensureClaudeAgentTeamsShimDir() const shimBin = resolveClaudeAgentTeamsShimBin(args.baseEnv) + if (!shimBin) { + // Why: without an absolute CLI path the shim would resolve a bare `orca` against the pane cwd, so degrade instead. + return { + command: addClaudeTeammateModeInProcess(args.command), + env: { CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1' } + } + } + const shimDir = await ensureClaudeAgentTeamsShimDir() const env = args.createTeamEnv(shimDir, shimBin) return { command: addClaudeTeammateModeAuto(args.command), @@ -51,20 +59,27 @@ export async function buildClaudeAgentTeamsLaunchPlan(args: { } } +/** Absolute path to the Orca CLI that backs the tmux shim, or null when none can be qualified. */ export function resolveClaudeAgentTeamsShimBin( env: Record = process.env -): string { - if (env.ORCA_AGENT_TEAMS_SHIM_BIN) { - return env.ORCA_AGENT_TEAMS_SHIM_BIN +): string | null { + // Why: Windows callers pass an env spelt `Path`; reading only `PATH` there would find no CLI at all. + const pathValue = env[resolvePathEnvKey(env, process.platform)] + const override = env.ORCA_AGENT_TEAMS_SHIM_BIN + if (override) { + // Why: a bare override name would be resolved by the shim's shell against its cwd, so qualify it or ignore it. + const qualified = isAbsolute(override) ? override : findExecutableOnPath(override, pathValue) + if (qualified) { + return qualified + } } const bundled = bundledLauncherPath() if (bundled && isExecutableFile(bundled)) { return bundled } return ( - findExecutableOnPath(process.platform === 'win32' ? 'orca-dev.cmd' : 'orca-dev', env.PATH) ?? - findExecutableOnPath(getOrcaCliCommandNameForPlatform(process.platform), env.PATH) ?? - getOrcaCliCommandNameForPlatform(process.platform) + findExecutableOnPath(process.platform === 'win32' ? 'orca-dev.cmd' : 'orca-dev', pathValue) ?? + findExecutableOnPath(getOrcaCliCommandNameForPlatform(process.platform), pathValue) ) } @@ -90,7 +105,8 @@ function bundledLauncherPath(): string | null { function findExecutableOnPath(command: string, pathValue: string | undefined): string | null { for (const directory of pathValue?.split(delimiter) ?? []) { - if (!directory) { + // Why: empty and relative PATH entries resolve against a cwd we do not control, which is the hijack we are avoiding. + if (!directory || !isAbsolute(directory)) { continue } const candidate = join(directory, command) @@ -113,23 +129,42 @@ function isExecutableFile(candidate: string): boolean { } } +// Why: an unqualified command name is resolved against the invoking pane's cwd (always on cmd.exe, and via `.`/empty +// PATH entries on POSIX), so a stray `orca` next to the agent's files would run with the team token. Demand a +// fully-qualified binary instead of guessing one. function unixShimScript(): string { return [ '#!/usr/bin/env sh', 'set -eu', - `exec "\${ORCA_AGENT_TEAMS_SHIM_BIN:-${getOrcaCliCommandNameForPlatform(process.platform)}}" agent-teams-tmux "$@"`, + 'orca_bin=${ORCA_AGENT_TEAMS_SHIM_BIN:-}', + 'case $orca_bin in', + ' /*|[A-Za-z]:[\\\\/]*) ;;', + ' *)', + ' echo "orca agent-teams tmux shim: ORCA_AGENT_TEAMS_SHIM_BIN must be an absolute path" >&2', + ' exit 127', + ' ;;', + 'esac', + 'exec "$orca_bin" agent-teams-tmux "$@"', '' ].join('\n') } -function windowsShimScript(): string { +export function windowsClaudeAgentTeamsShimScript(): string { return [ '@echo off', 'setlocal', - 'if "%ORCA_AGENT_TEAMS_SHIM_BIN%"=="" (', - ` set "ORCA_AGENT_TEAMS_SHIM_BIN=${getOrcaCliCommandNameForPlatform(process.platform)}"`, - ')', - '"%ORCA_AGENT_TEAMS_SHIM_BIN%" agent-teams-tmux %*', + 'set "ORCA_SHIM_BIN=%ORCA_AGENT_TEAMS_SHIM_BIN%"', + 'if not defined ORCA_SHIM_BIN goto :unqualified', + 'if "%ORCA_SHIM_BIN:~1,1%"==":" goto :run', + 'if "%ORCA_SHIM_BIN:~0,2%"=="\\\\" goto :run', + 'goto :unqualified', + ':run', + // Why: no `call` — its extra percent-expansion pass would rewrite tmux pane args such as `%2` into batch parameters. + '"%ORCA_SHIM_BIN%" agent-teams-tmux %*', + 'exit /b %ERRORLEVEL%', + ':unqualified', + 'echo orca agent-teams tmux shim: ORCA_AGENT_TEAMS_SHIM_BIN must be an absolute path 1>&2', + 'exit /b 127', '' ].join('\r\n') } diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 9beb782cebc..18880defc77 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -14263,6 +14263,8 @@ describe('OrcaRuntimeService', () => { command: 'claude --resume claude-session', env: { CLAUDE_PROFILE: 'captured', + // Why: native panes need an absolute CLI; without one the plan degrades to in-process teammates. + ORCA_AGENT_TEAMS_SHIM_BIN: '/opt/orca/bin/orca-ide', ORCA_AGENT_TEAMS_TEAM_ID: 'stale-team', ORCA_AGENT_TEAMS_TOKEN: 'stale-token', TMUX: '/tmp/orca-claude-agent-teams/stale-team,0,1' @@ -14273,6 +14275,7 @@ describe('OrcaRuntimeService', () => { agentArgs: '--teammate-mode auto', agentEnv: { CLAUDE_PROFILE: 'captured', + ORCA_AGENT_TEAMS_SHIM_BIN: '/opt/orca/bin/orca-ide', ORCA_AGENT_TEAMS_TEAM_ID: 'stale-team', ORCA_AGENT_TEAMS_TOKEN: 'stale-token', TMUX: '/tmp/orca-claude-agent-teams/stale-team,0,1' @@ -14287,6 +14290,7 @@ describe('OrcaRuntimeService', () => { expect(spawnCall?.env).toMatchObject({ CLAUDE_PROFILE: 'captured', CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1', + ORCA_AGENT_TEAMS_SHIM_BIN: '/opt/orca/bin/orca-ide', TMUX_PANE: '%1' }) expect(spawnCall?.env?.ORCA_AGENT_TEAMS_TEAM_ID).toMatch(/^team-/) From cd6114ab7e95e5c680e719e28c3b769ab43d7608 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:40:58 -0700 Subject: [PATCH 08/42] fix(browser): acknowledge paired tab before navigation (#14402) --- .github/workflows/e2e.yml | 6 +- config/reliability-gates.jsonc | 69 +++- config/scripts/pr-e2e-gate-contract.test.mjs | 6 +- src/main/browser/browser-backend.ts | 1 + src/main/browser/offscreen-browser-backend.ts | 5 +- src/main/runtime/orca-runtime-browser.test.ts | 127 +++++- src/main/runtime/orca-runtime-browser.ts | 83 ++-- src/main/runtime/orca-runtime.ts | 4 +- src/main/runtime/rpc/methods/browser-core.ts | 4 +- .../runtime/rpc/methods/browser-schemas.ts | 13 - .../rpc/methods/browser-tab-create-schema.ts | 12 + src/preload/api/ui-command-event-api.ts | 7 +- src/preload/index.ts | 8 +- src/renderer/src/hooks/useIpcEvents.test.ts | 60 ++- src/renderer/src/hooks/useIpcEvents.ts | 37 +- .../src/runtime/web-runtime-session.test.ts | 104 +++++ .../src/runtime/web-runtime-session.ts | 62 ++- src/renderer/src/store/slices/browser.test.ts | 16 + src/renderer/src/store/slices/browser.ts | 17 +- src/shared/protocol-version.ts | 5 + ...browser-create-navigation-deadline.spec.ts | 383 ++++++++++++++++++ 21 files changed, 939 insertions(+), 90 deletions(-) create mode 100644 src/main/runtime/rpc/methods/browser-tab-create-schema.ts create mode 100644 tests/e2e/paired-browser-create-navigation-deadline.spec.ts diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 4f36b83972d..d06bbeb298b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -244,8 +244,12 @@ jobs: if printf '%s\n' "${TEST_FILES[@]}" | grep -qx 'tests/e2e/ephemeral-vm-provisioned-root.spec.ts'; then E2E_ENV+=(ORCA_E2E_SSH_DOCKER=1) fi + E2E_PROJECT_ARGS=() + if grep -l '@headful' "${TEST_FILES[@]}" >/dev/null; then + E2E_PROJECT_ARGS+=(--project=electron-headful) + fi xvfb-run --auto-servernum env "${E2E_ENV[@]}" \ - pnpm run test:e2e "${TEST_FILES[@]}" --workers=1 + pnpm run test:e2e "${TEST_FILES[@]}" --workers=1 "${E2E_PROJECT_ARGS[@]}" - name: Upload Playwright traces if: failure() diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 8d20a00df05..45c7aed8242 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "updatedAt": "2026-08-12", + "updatedAt": "2026-08-13", "policy": { "maturityLevels": ["experimental", "soak", "blocking", "accepted-gap", "deprecated"], "blockingPromotion": { @@ -1817,19 +1817,24 @@ "providers": ["remote-runtime"], "coveredPlatforms": ["macos"], "coveredProviders": ["remote-runtime"], - "coverageNotes": "A headed macOS desktop host and separate paired client exercise the Explorer eye-button path with a real remote file URL, host browser DOM, renderer split layout, background preview creation, explicit browser activation, browser close, forced capability rejection after caller preflight, and a forced post-create reconciliation timeout. The same fault oracles pass against a live headless server. Unit contracts cover absent, unknown, and mixed-version capabilities, visible rejection, empty-split cleanup, owning-runtime routing, exact client materialization, confirmed rollback, ambiguous cleanup, and teardown. Linux and Windows live journeys remain gaps.", + "coverageNotes": "A headed macOS desktop host and separate paired client exercise the Explorer eye-button path with a real remote file URL, host browser DOM, renderer split layout, background preview creation, explicit browser activation, browser close, forced capability rejection after caller preflight, a forced post-create reconciliation timeout, and navigation held beyond the client create deadline. The same reconciliation fault oracles pass against a live headless server. Unit contracts cover absent, unknown, and mixed-version capabilities, visible rejection, empty-split cleanup, owning-runtime routing, canonical preallocated page identity, navigation-independent acknowledgement, exact client materialization, confirmed rollback, ambiguous cleanup, and teardown. Linux and Windows live journeys remain gaps.", "motivatingLinks": [ "https://linear.app/stably/issue/STA-3872/unable-to-open-html-files-in-remote-server", "https://linear.app/stably/issue/STA-4025/p1-browser-create-reports-success-when-tab-reconcile-fails-after-pr", "https://linear.app/stably/issue/STA-4063/p1-paired-html-preview-rejects-outside-cleanup-after-pr-13909", + "https://linear.app/stably/issue/STA-4231/p1-owner-pinned-paired-browser-create-can-time-out-after-host-tab", + "https://github.com/stablyai/orca/pull/14363", "https://github.com/stablyai/orca/pull/13876" ], - "invariant": "Opening one HTML preview from a paired client succeeds only after exactly one browser page on the worktree-owning runtime materializes as exactly one client browser workspace and unified tab in the requested group. Capability rejection before host mutation must preserve the original error, issue no RPC, surface a failure toast, and remove only a caller-declared newly-created empty split. Post-create reconciliation failure requires exact rollback; ambiguous rollback rejects without local fallback. Preview focus and close preserve the source editor and terminal.", - "oracle": "Run one unchanged contract oracle for direct create and side-preview callers with absent status, unknown capabilities, and a mixed-version host. Require the original unsupported error or visible toast, zero RPCs, and no retained new split. Run it on the scan target, latest main, candidate, and candidate with the cleanup boundary disabled. In headed paired Electron, arm capability rejection after the caller preflight, invoke the visible HTML side-preview action, require the failure toast, and compare exact host/client browser, editor, terminal, and group inventories to baseline. Repeat against headless serve. Retain the separate exact-page reconciliation rollback oracle.", + "invariant": "Opening one HTML preview from a paired client succeeds only after exactly one browser page on the worktree-owning runtime materializes as exactly one client browser workspace and unified tab in the requested group. Owner-pinned creation returns the canonical host page identity before navigation readiness; delayed navigation cannot turn a created page into an unidentifiable failure or a duplicate retry. Capability rejection before host mutation must preserve the original error, issue no RPC, surface a failure toast, and remove only a caller-declared newly-created empty split. Post-create reconciliation failure requires exact rollback; ambiguous rollback rejects without local fallback. Preview focus and close preserve the source editor and terminal.", + "oracle": "Run one unchanged contract oracle for direct create and side-preview callers with absent status, unknown capabilities, and a mixed-version host. Require the original unsupported error or visible toast, zero RPCs, and no retained new split. Run it on the scan target, latest main, candidate, and candidate with the cleanup boundary disabled. In headed paired Electron, arm capability rejection after the caller preflight, invoke the visible HTML side-preview action, require the failure toast, and compare exact host/client browser, editor, terminal, and group inventories to baseline. Hold a real navigation response beyond the 15-second client deadline after host creation; require the first RPC to return the exact host inventory page ID, one host page, and no retry. Repeat reconciliation faults against headless serve. Retain the separate exact-page reconciliation rollback oracle.", "commands": [ "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime-browser.test.ts src/main/runtime/rpc/methods/browser.test.ts src/renderer/src/lib/file-preview.test.ts src/renderer/src/runtime/web-session-browser-placement.test.ts src/renderer/src/runtime/web-runtime-session.test.ts src/renderer/src/runtime/web-session-tabs-sync.test.ts src/renderer/src/runtime/remote-server-parity.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/runtime/web-runtime-browser-materialization.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/runtime/web-runtime-browser-capability-cleanup.test.ts src/renderer/src/lib/file-preview-capability-cleanup.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime-browser.test.ts src/renderer/src/runtime/web-runtime-session.test.ts", + "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/orca-runtime-browser.test.ts src/renderer/src/hooks/useIpcEvents.test.ts src/renderer/src/store/slices/browser.test.ts src/renderer/src/runtime/web-runtime-session.test.ts", + "SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-browser-create-navigation-deadline.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1", "ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", "ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --repeat-each=3", "SKIP_BUILD=1 ORCA_E2E_WEB_CLIENT=1 ORCA_E2E_FORCE_HEADFUL=1 pnpm exec playwright test tests/e2e/paired-remote-html-browser-focus.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", @@ -1848,7 +1853,10 @@ "src/renderer/src/runtime/web-runtime-session.test.ts", "src/renderer/src/runtime/web-session-tabs-sync.test.ts", "src/renderer/src/runtime/remote-server-parity.test.ts", + "src/renderer/src/hooks/useIpcEvents.test.ts", + "src/renderer/src/store/slices/browser.test.ts", "tests/e2e/paired-remote-html-browser-focus.spec.ts", + "tests/e2e/paired-browser-create-navigation-deadline.spec.ts", "tests/e2e/paired-browser-creation-reconciliation-failure.spec.ts" ], "assertionRefs": [ @@ -1884,12 +1892,36 @@ "file": "src/renderer/src/runtime/web-runtime-session.test.ts", "assertions": [ "a post-create timeout returns false only after exact host close and a confirming snapshot", + "owner-pinned creation launches navigation separately and materializes without awaiting it", + "a lost create acknowledgement cleans the capability-gated canonical page ID", "unconfirmed cleanup rejects so callers cannot create a competing local fallback", "the canonical placement and a newly-created empty split are released on rollback", "one failed preview cannot remove a shared split still reserved after an environment ownership change", "if every overlapping creation fails, the last reservation closes the caller-created empty split" ] }, + { + "file": "src/main/runtime/orca-runtime-browser.test.ts", + "assertions": [ + "explicit renderer cleanup bypasses guest registration while retaining typed missing-page behavior", + "offscreen cleanup rejects a page outside the requested worktree" + ] + }, + { + "file": "src/renderer/src/hooks/useIpcEvents.test.ts", + "assertions": [ + "renderer-backed creation preserves the requested canonical page ID while keeping its workspace identity independent", + "closing an absent renderer page returns browser_tab_not_found instead of an untyped failure", + "explicit cleanup cannot close a page owned by another worktree" + ] + }, + { + "file": "src/renderer/src/store/slices/browser.test.ts", + "assertions": [ + "a requested canonical page ID is stored on the page rather than reused as its workspace ID", + "duplicate canonical page IDs are rejected before a second tab can materialize" + ] + }, { "file": "src/renderer/src/runtime/web-session-browser-placement.test.ts", "assertions": [ @@ -1921,6 +1953,13 @@ "preview creation leaves the editor active, an explicit browser click is authoritative, and close preserves the editor and terminal" ] }, + { + "file": "tests/e2e/paired-browser-create-navigation-deadline.spec.ts", + "assertions": [ + "a held navigation exceeds the former create deadline while the first RPC returns the exact authoritative host page ID", + "host inventory contains exactly one new page and the user retry branch is never entered" + ] + }, { "file": "tests/e2e/paired-browser-creation-reconciliation-failure.spec.ts", "assertions": [ @@ -2075,6 +2114,24 @@ "result": "failed", "durationSeconds": 12.3, "summary": "Candidate d05217c40d with only the side-preview focusOnCreate:false call disabled failed the byte-identical 2ec1dcf1 oracle because preview creation activated the browser instead of preserving editor focus." + }, + { + "date": "2026-08-13", + "runner": "local", + "platform": "macos", + "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/orca-runtime-browser.test.ts src/renderer/src/hooks/useIpcEvents.test.ts src/renderer/src/store/slices/browser.test.ts src/renderer/src/runtime/web-runtime-session.test.ts", + "result": "passed", + "durationSeconds": 10.83, + "summary": "Five post-review host, IPC, store, and client lifecycle files passed 1,356 tests with one pre-existing skip, including both shards that failed before the canonical page/workspace identity and typed missing-page cleanup corrections." + }, + { + "date": "2026-08-13", + "runner": "local", + "platform": "macos", + "command": "SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-browser-create-navigation-deadline.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1", + "result": "passed", + "durationSeconds": 18.1, + "summary": "Two isolated headed Electron/CDP journeys passed: the direct owner-pinned call returned one canonical host ID before held navigation, and the real remote-pane context-menu route materialized that one page without retry or duplicate." } ], "runtimeBudget": { @@ -2087,11 +2144,11 @@ }, "redGreenEvidence": { "status": "complete", - "evidence": "The unchanged STA-4063 five-assertion oracle is red 5/5 on scan e4c278eb52 and latest main bc2e30000b, partially green 2/5 on pre-fix PR #14100, green 5/5 on candidate 14b5877a75, and red 3/5 when only the new transaction cleanup boundary is disabled on final candidate a9e6fb7716. The earlier STA-4025 transaction oracle remains red on the post-#13909 base and green on this candidate. The byte-identical 2ec1dcf1 Electron oracle remains red on latest main 346e59c879, green on candidate d05217c40d, and red with only side-preview focus disabled; historical 5806b23f evidence separately proves owning-runtime routing." + "evidence": "The unchanged STA-4063 five-assertion oracle is red 5/5 on scan e4c278eb52 and latest main bc2e30000b, partially green 2/5 on pre-fix PR #14100, green 5/5 on candidate 14b5877a75, and red 3/5 when only the new transaction cleanup boundary is disabled on final candidate a9e6fb7716. The earlier STA-4025 transaction oracle remains red on the post-#13909 base and green on this candidate. The byte-identical STA-4231 headed oracle is red on latest-main b19d99bd00: both 15-second creates return runtime_timeout without a page ID while the host retains two distinct pages after retry. The relevant behavior is unchanged through 0a0ae974d4. The candidate is green with one returned canonical ID, one host/client page, and no retry; disabling only the navigation-independent acknowledgement is red with the same two orphaned pages. The byte-identical 2ec1dcf1 Electron oracle remains red on latest main 346e59c879, green on candidate d05217c40d, and red with only side-preview focus disabled; historical 5806b23f evidence separately proves owning-runtime routing." }, "performanceBudget": { "required": true, - "evidence": "The transaction adds no timer, polling, retry, subprocess, RPC method, stream opcode, or host publication. Capability rejection performs zero RPCs and one bounded empty-group close. Successful creation adds one post-operation list; known-page failure adds one exact close and one confirming list. The E2E-only one-shot faults are build-gated, retain one promise/flag and a bounded page-id set, and reset after each run. Pending client placements remain capped at 128 and clear on adoption, failure, or teardown." + "evidence": "The transaction adds no timer, polling, retry, subprocess, RPC method, stream opcode, or global host publication. Owner-pinned success performs one create RPC followed by one asynchronous goto, retains one page, and performs zero retry or cleanup calls. A lost create acknowledgement performs one exact close by the preallocated canonical ID and at most one confirming snapshot. Headless goto publishes one worktree-scoped session snapshot after navigation. Capability rejection performs zero RPCs and one bounded empty-group close. The E2E-only one-shot faults are build-gated, retain one promise/flag and a bounded page-id set, and reset after each run. Pending client placements remain capped at 128 and clear on adoption, failure, or teardown." }, "promotionCriteria": [ "Collect Linux and physical Windows paired-server evidence.", diff --git a/config/scripts/pr-e2e-gate-contract.test.mjs b/config/scripts/pr-e2e-gate-contract.test.mjs index d44039a2ca8..c600d233437 100644 --- a/config/scripts/pr-e2e-gate-contract.test.mjs +++ b/config/scripts/pr-e2e-gate-contract.test.mjs @@ -75,7 +75,11 @@ describe('PR E2E gate contract', () => { expect(changedRun.run).toContain('. != "tests/e2e/ssh-startup-exec-readiness.spec.ts"') expect(changedRun.run).toContain('. != "tests/e2e/paired-startup-exec-readiness.spec.ts"') expect(changedRun.run).toContain('if [ "${#TEST_FILES[@]}" -eq 0 ]') - expect(changedRun.run).toContain('pnpm run test:e2e "${TEST_FILES[@]}" --workers=1') + expect(changedRun.run).toContain('grep -l \'@headful\' "${TEST_FILES[@]}"') + expect(changedRun.run).toContain('E2E_PROJECT_ARGS+=(--project=electron-headful)') + expect(changedRun.run).toContain( + 'pnpm run test:e2e "${TEST_FILES[@]}" --workers=1 "${E2E_PROJECT_ARGS[@]}"' + ) }) it('keeps startup-exec live parity in the isolated SSH lane', () => { diff --git a/src/main/browser/browser-backend.ts b/src/main/browser/browser-backend.ts index 697eb7c178f..939684e49b3 100644 --- a/src/main/browser/browser-backend.ts +++ b/src/main/browser/browser-backend.ts @@ -10,6 +10,7 @@ export type BrowserBackendCreateTab = { url: string worktreeId?: string profileId?: string + browserPageId?: string } export type BrowserBackend = { diff --git a/src/main/browser/offscreen-browser-backend.ts b/src/main/browser/offscreen-browser-backend.ts index 5eb6e9b8435..64e96ede29d 100644 --- a/src/main/browser/offscreen-browser-backend.ts +++ b/src/main/browser/offscreen-browser-backend.ts @@ -23,7 +23,10 @@ export class OffscreenBrowserBackend implements BrowserBackend { constructor(private readonly browserManager: BrowserManager) {} async createTab(params: BrowserBackendCreateTab): Promise<{ browserPageId: string }> { - const browserPageId = randomUUID() + const browserPageId = params.browserPageId ?? randomUUID() + if (this.windowsByPageId.has(browserPageId)) { + throw new Error(`Browser page ${browserPageId} already exists`) + } // Why: profiles map to Electron partitions; using the profile's partition // makes cookies/storage persist in the same SQLite DB the desktop path uses. const profile = params.profileId diff --git a/src/main/runtime/orca-runtime-browser.test.ts b/src/main/runtime/orca-runtime-browser.test.ts index 5cc74aa4ad4..007a091745d 100644 --- a/src/main/runtime/orca-runtime-browser.test.ts +++ b/src/main/runtime/orca-runtime-browser.test.ts @@ -329,6 +329,56 @@ describe('RuntimeBrowserCommands browser screencast', () => { ) }) + it('returns the renderer page identity before navigation settles', async () => { + const { RuntimeBrowserCommands } = await import('./orca-runtime-browser') + const navigation = deferred<{ title: string; url: string }>() + const webContents = { send: vi.fn() } + webContents.send = vi.fn((_channel: string, data: { requestId: string }) => { + const handler = ipcMainOnMock.mock.calls.find( + ([eventName]) => eventName === 'browser:tabCreateReply' + )?.[1] as + | ((event: unknown, reply: { requestId: string; browserPageId?: string }) => void) + | undefined + handler?.({ sender: webContents } as never, { + requestId: data.requestId, + browserPageId: 'page-slow-navigation' + }) + }) + const bridge = { + getRegisteredTabs: vi.fn(() => new Map([['page-slow-navigation', 101]])), + goto: vi.fn(() => navigation.promise), + setActiveTab: vi.fn() + } as unknown as AgentBrowserBridge + const commands = new RuntimeBrowserCommands( + createHost({ + getAgentBrowserBridge: () => bridge, + getAvailableAuthoritativeWindow: vi.fn(() => ({}) as never), + getAuthoritativeWindow: vi.fn(() => ({ webContents }) as never) + }) + ) + + let created: { browserPageId: string } | null = null + const creation = commands + .browserTabCreate({ + worktree: 'id:wt-1', + url: 'https://example.com/slow', + waitForRegistration: true + }) + .then((result) => { + created = result + return result + }) + await vi.waitFor(() => expect(bridge.goto).toHaveBeenCalledOnce()) + await new Promise((resolve) => setImmediate(resolve)) + + try { + expect(created).toEqual({ browserPageId: 'page-slow-navigation' }) + } finally { + navigation.resolve({ title: 'Slow page', url: 'https://example.com/slow' }) + await creation + } + }) + it('rejects unknown explicit profile ids before requesting a renderer tab', async () => { const { RuntimeBrowserCommands } = await import('./orca-runtime-browser') const send = vi.fn() @@ -509,7 +559,7 @@ describe('RuntimeBrowserCommands browser screencast', () => { expect(result.browserPageId).toBe('page-target') }) - it('wakes the requested page before closing a page-scoped tab', async () => { + it('closes the requested renderer page without waiting for guest registration', async () => { const { RuntimeBrowserCommands } = await import('./orca-runtime-browser') webContentsFromIdMock.mockReturnValue({ isDestroyed: () => true }) const send = vi.fn((channel: string, data: { requestId?: string }) => { @@ -537,11 +587,8 @@ describe('RuntimeBrowserCommands browser screencast', () => { commands.browserTabClose({ worktree: 'id:wt-1', page: 'page-target' }) ).resolves.toEqual({ closed: true }) - expect(send).toHaveBeenCalledWith('browser:activateView', { - worktreeId: 'wt-1', - browserPageId: 'page-target' - }) - expect(waitForTabRegistrationMock).toHaveBeenCalledWith('page-target') + expect(send).not.toHaveBeenCalledWith('browser:activateView', expect.anything()) + expect(waitForTabRegistrationMock).not.toHaveBeenCalled() expect(waitForWorktreeTabRegistrationMock).not.toHaveBeenCalled() expect(send).toHaveBeenCalledWith( 'browser:requestTabClose', @@ -549,6 +596,42 @@ describe('RuntimeBrowserCommands browser screencast', () => { ) }) + it('lets the renderer close an acknowledged page whose guest never registered', async () => { + const { RuntimeBrowserCommands } = await import('./orca-runtime-browser') + const send = vi.fn((channel: string, data: { requestId?: string }) => { + if (channel !== 'browser:requestTabClose') { + return + } + const handler = ipcMainOnMock.mock.calls.find( + ([eventName]) => eventName === 'browser:tabCloseReply' + )?.[1] as ((event: unknown, reply: { requestId: string }) => void) | undefined + handler?.({} as never, { requestId: data.requestId ?? '' }) + }) + const bridge = { + getRegisteredTabs: vi.fn(() => new Map()), + getActivePageId: vi.fn(() => null), + getActiveWebContentsId: vi.fn(() => null) + } as unknown as AgentBrowserBridge + const authoritativeWindow = { webContents: { send } } as never + const commands = new RuntimeBrowserCommands( + createHost({ + getAgentBrowserBridge: () => bridge, + getAvailableAuthoritativeWindow: vi.fn(() => authoritativeWindow), + getAuthoritativeWindow: vi.fn(() => authoritativeWindow) + }) + ) + + await expect( + commands.browserTabClose({ worktree: 'id:wt-1', page: 'page-canonical' }) + ).resolves.toEqual({ closed: true }) + + expect(waitForTabRegistrationMock).not.toHaveBeenCalled() + expect(send).toHaveBeenCalledWith( + 'browser:requestTabClose', + expect.objectContaining({ tabId: 'page-canonical', worktreeId: 'wt-1' }) + ) + }) + it('lets a new same-page stream take over an active stale stream', async () => { const { RuntimeBrowserCommands } = await import('./orca-runtime-browser') webContentsFromIdMock.mockReturnValue({ isDestroyed: () => false }) @@ -648,6 +731,34 @@ describe('RuntimeBrowserCommands headless offscreen routing', () => { expect(setActiveTab).toHaveBeenCalledWith(202, 'wt-1') }) + it('publishes a headless session snapshot after explicit navigation', async () => { + const { RuntimeBrowserCommands } = await import('./orca-runtime-browser') + webContentsFromIdMock.mockReturnValue({ isDestroyed: () => false }) + const notifyHeadlessBrowserSessionTabsChanged = vi.fn() + const bridge = { + getRegisteredTabs: vi.fn(() => new Map([['page-offscreen', 202]])), + getActivePageId: vi.fn(() => 'page-offscreen'), + goto: vi.fn(async () => ({ title: 'Loaded', url: 'https://example.com/loaded' })) + } as unknown as AgentBrowserBridge + const commands = new RuntimeBrowserCommands( + createHost({ + getAgentBrowserBridge: () => bridge, + getAvailableAuthoritativeWindow: vi.fn(() => null), + notifyHeadlessBrowserSessionTabsChanged + }) + ) + + await expect( + commands.browserGoto({ + worktree: 'id:wt-1', + page: 'page-offscreen', + url: 'https://example.com/loaded' + }) + ).resolves.toEqual({ title: 'Loaded', url: 'https://example.com/loaded' }) + + expect(notifyHeadlessBrowserSessionTabsChanged).toHaveBeenCalledWith('wt-1') + }) + it('rejects tab creation when neither a renderer nor an offscreen backend is available', async () => { const { RuntimeBrowserCommands } = await import('./orca-runtime-browser') const commands = new RuntimeBrowserCommands( @@ -686,6 +797,10 @@ describe('RuntimeBrowserCommands headless offscreen routing', () => { expect(closeTab).toHaveBeenCalledWith('page-offscreen') // The renderer close IPC must not be used in headless mode. expect(ipcMainOnMock).not.toHaveBeenCalledWith('browser:tabCloseReply', expect.anything()) + await expect( + commands.browserTabClose({ worktree: 'id:wt-1', page: 'page-other' }) + ).rejects.toMatchObject({ code: 'browser_tab_not_found' }) + expect(closeTab).toHaveBeenCalledTimes(1) }) it('closes the active headless tab on an implicit close', async () => { diff --git a/src/main/runtime/orca-runtime-browser.ts b/src/main/runtime/orca-runtime-browser.ts index 6070cefd374..3e5df279344 100644 --- a/src/main/runtime/orca-runtime-browser.ts +++ b/src/main/runtime/orca-runtime-browser.ts @@ -156,6 +156,7 @@ export type RuntimeBrowserCommandHost = { browserPageId: string, targetGroupId?: string ): void + notifyHeadlessBrowserSessionTabsChanged?(worktreeId: string): void } export class RuntimeBrowserCommands { @@ -358,6 +359,9 @@ export class RuntimeBrowserCommands { if (pageId) { this.notifyRendererNavigation(pageId, result.url, result.title) } + if (!this.host.getAvailableAuthoritativeWindow() && target.worktreeId) { + this.host.notifyHeadlessBrowserSessionTabsChanged?.(target.worktreeId) + } return result } @@ -1294,6 +1298,7 @@ export class RuntimeBrowserCommands { async browserTabCreate(params: { url?: string worktree?: string + page?: string profileId?: string waitForRegistration?: boolean activate?: boolean @@ -1322,7 +1327,8 @@ export class RuntimeBrowserCommands { worktreeId, params.profileId, params.activate, - params.targetGroupId + params.targetGroupId, + params.page ) } const { browserPageId } = await this.createBrowserTabInRenderer( @@ -1330,7 +1336,8 @@ export class RuntimeBrowserCommands { worktreeId, params.profileId, params.profileId ? sessionPartition : undefined, - params.activate + params.activate, + params.page ) // Why: the webview must mount and register before the tab is operable, so wait here (returning the ID anyway on timeout). @@ -1351,9 +1358,19 @@ export class RuntimeBrowserCommands { // Why: the webview loads about:blank first; route navigation through the bridge so its registered owner remains authoritative. if (url && url !== 'about:blank') { - try { + const navigate = async (): Promise => { const result = await bridge.goto(url, worktreeId, browserPageId) this.notifyRendererNavigation(browserPageId, result.url, result.title) + if (!this.host.getAvailableAuthoritativeWindow() && worktreeId) { + this.host.notifyHeadlessBrowserSessionTabsChanged?.(worktreeId) + } + } + if (params.waitForRegistration === true) { + void navigate().catch(() => {}) + return { browserPageId } + } + try { + await navigate() } catch { // Tab exists but navigation failed — caller can retry with explicit goto } @@ -1587,22 +1604,15 @@ export class RuntimeBrowserCommands { worktree?: string }): Promise<{ closed: boolean }> { const bridge = this.requireAgentBrowserBridge() - const pageTarget = - typeof params.page === 'string' && params.page.length > 0 - ? await this.resolveBrowserCommandTarget({ worktree: params.worktree, page: params.page }) - : null - const worktreeId = - pageTarget?.worktreeId ?? (await this.resolveBrowserWorktreeId(params.worktree)) + const explicitPage = typeof params.page === 'string' && params.page.length > 0 + const worktreeId = explicitPage + ? params.worktree + ? (await this.host.resolveWorktreeSelector(params.worktree)).id + : undefined + : await this.resolveBrowserWorktreeId(params.worktree) let tabId: string | null = null if (typeof params.page === 'string' && params.page.length > 0) { - if (!bridge.getRegisteredTabs(worktreeId).has(params.page)) { - const scope = worktreeId ? ' in this worktree' : '' - throw new BrowserError( - 'browser_tab_not_found', - `Browser page ${params.page} was not found${scope}` - ) - } tabId = params.page } else if (params.index !== undefined) { const tabs = bridge.getRegisteredTabs(worktreeId) @@ -1622,20 +1632,31 @@ export class RuntimeBrowserCommands { } // Why: headless serve has no renderer to ask, so destroy the offscreen page directly. - const offscreen = this.host.getAvailableAuthoritativeWindow() - ? null - : this.host.getOffscreenBrowserBackend() + const authoritativeWindow = this.host.getAvailableAuthoritativeWindow() + const offscreen = authoritativeWindow ? null : this.host.getOffscreenBrowserBackend() if (offscreen) { // Why: resolve the active page for implicit close so we don't report success while closing nothing. const resolvedTabId = tabId ?? bridge.getActivePageId(worktreeId) if (!resolvedTabId) { return { closed: false } } + if (explicitPage && !bridge.getRegisteredTabs(worktreeId).has(resolvedTabId)) { + const scope = worktreeId ? ' in this worktree' : '' + throw new BrowserError( + 'browser_tab_not_found', + `Browser page ${resolvedTabId} was not found${scope}` + ) + } await offscreen.closeTab(resolvedTabId) return { closed: true } } - const win = this.host.getAuthoritativeWindow() + if (!authoritativeWindow && tabId && !bridge.getRegisteredTabs(worktreeId).has(tabId)) { + const scope = worktreeId ? ' in this worktree' : '' + throw new BrowserError('browser_tab_not_found', `Browser page ${tabId} was not found${scope}`) + } + + const win = authoritativeWindow ?? this.host.getAuthoritativeWindow() const requestId = randomUUID() await new Promise((resolve, reject) => { const timer = setTimeout(() => { @@ -1645,7 +1666,7 @@ export class RuntimeBrowserCommands { const handler = ( _event: Electron.IpcMainEvent, - reply: { requestId: string; error?: string } + reply: { requestId: string; error?: string; code?: 'browser_tab_not_found' } ): void => { if (reply.requestId !== requestId) { return @@ -1653,7 +1674,11 @@ export class RuntimeBrowserCommands { clearTimeout(timer) ipcMain.removeListener('browser:tabCloseReply', handler) if (reply.error) { - reject(new Error(reply.error)) + reject( + reply.code === 'browser_tab_not_found' + ? new BrowserError('browser_tab_not_found', reply.error) + : new Error(reply.error) + ) } else { resolve() } @@ -1706,9 +1731,15 @@ export class RuntimeBrowserCommands { worktreeId?: string, profileId?: string, activate?: boolean, - targetGroupId?: string + targetGroupId?: string, + requestedPageId?: string ): Promise<{ browserPageId: string }> { - const { browserPageId } = await offscreen.createTab({ url, worktreeId, profileId }) + const { browserPageId } = await offscreen.createTab({ + url, + worktreeId, + profileId, + ...(requestedPageId ? { browserPageId: requestedPageId } : {}) + }) const bridge = this.host.getAgentBrowserBridge() const wcId = bridge?.getRegisteredTabs(worktreeId).get(browserPageId) if (bridge && wcId != null) { @@ -1726,7 +1757,8 @@ export class RuntimeBrowserCommands { worktreeId: string | undefined, profileId: string | undefined, sessionPartition: string | undefined, - activate?: boolean + activate?: boolean, + requestedPageId?: string ): Promise<{ browserPageId: string }> { const win = this.host.getAuthoritativeWindow() const requestId = randomUUID() @@ -1757,6 +1789,7 @@ export class RuntimeBrowserCommands { requestId, url, worktreeId, + ...(requestedPageId ? { browserPageId: requestedPageId } : {}), // Why: keep these undefined (not null) when no profile is chosen so the renderer still applies default-profile inheritance. sessionProfileId: profileId, sessionPartition, diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index be28a589c70..5a0577166fc 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -35541,7 +35541,9 @@ export class OrcaRuntimeService { getAvailableAuthoritativeWindow: () => this.getAvailableAuthoritativeWindow(), getOffscreenBrowserBackend: () => this.offscreenBrowserBackend, // Why: bind directly, not a wrapper arrow — a hand-listed wrapper dropped targetGroupId, so a right-split browser landed in the left. - markHeadlessBrowserSessionTabActive: this.markHeadlessBrowserSessionTabActive.bind(this) + markHeadlessBrowserSessionTabActive: this.markHeadlessBrowserSessionTabActive.bind(this), + notifyHeadlessBrowserSessionTabsChanged: (worktreeId) => + this.notifyMobileSessionTabsChanged(worktreeId) }) private readonly emulatorCommands = new RuntimeEmulatorCommands({ diff --git a/src/main/runtime/rpc/methods/browser-core.ts b/src/main/runtime/rpc/methods/browser-core.ts index 41484d20768..8e8478c7b39 100644 --- a/src/main/runtime/rpc/methods/browser-core.ts +++ b/src/main/runtime/rpc/methods/browser-core.ts @@ -24,7 +24,6 @@ import { TabCurrent, TabSetProfile, TabClose, - TabCreate, TabList, TabProfileClone, TabShow, @@ -32,6 +31,7 @@ import { Upload, Wait } from './browser-schemas' +import { BrowserTabCreateParams } from './browser-tab-create-schema' import { BROWSER_TEXT_METHODS } from './browser-text-rpc-methods' const CertificateProceed = BrowserTarget.extend({ @@ -112,7 +112,7 @@ export const BROWSER_CORE_METHODS: RpcMethod[] = [ }), defineMethod({ name: 'browser.tabCreate', - params: TabCreate, + params: BrowserTabCreateParams, handler: async (params, { runtime }) => runtime.browserTabCreate(params) }), defineMethod({ diff --git a/src/main/runtime/rpc/methods/browser-schemas.ts b/src/main/runtime/rpc/methods/browser-schemas.ts index a9a2a440f8f..47027451aeb 100644 --- a/src/main/runtime/rpc/methods/browser-schemas.ts +++ b/src/main/runtime/rpc/methods/browser-schemas.ts @@ -84,7 +84,6 @@ export const Eval = BrowserTarget.extend({ }) export const TabList = z.object({ worktree: OptionalString }) - // Why: --index xor --page must be present. The refine guards that invariant // so the dispatcher surfaces a single legible error instead of either shape // leaking into the runtime. @@ -111,18 +110,6 @@ export const TabSwitch = BrowserTarget.extend({ { message: 'Missing required --index (non-negative integer) or --page' } ) -export const TabCreate = z.object({ - url: OptionalString, - worktree: OptionalString, - profileId: OptionalString, - waitForRegistration: z.boolean().optional(), - // User-initiated opens focus the tab; agent/automation opens stay background. - activate: z.boolean().optional(), - // Why: the split group whose "+" was clicked, so a headless host places the - // new browser tab there instead of coalescing into the first/active group. - targetGroupId: OptionalString -}) - export const TabShow = z.object({ page: requiredString('Missing required --page'), worktree: OptionalString diff --git a/src/main/runtime/rpc/methods/browser-tab-create-schema.ts b/src/main/runtime/rpc/methods/browser-tab-create-schema.ts new file mode 100644 index 00000000000..f431b0ec225 --- /dev/null +++ b/src/main/runtime/rpc/methods/browser-tab-create-schema.ts @@ -0,0 +1,12 @@ +import { z } from 'zod' +import { OptionalString } from '../schemas' + +export const BrowserTabCreateParams = z.object({ + url: OptionalString, + worktree: OptionalString, + page: OptionalString, + profileId: OptionalString, + waitForRegistration: z.boolean().optional(), + activate: z.boolean().optional(), + targetGroupId: OptionalString +}) diff --git a/src/preload/api/ui-command-event-api.ts b/src/preload/api/ui-command-event-api.ts index e46c09139b7..4de95063d42 100644 --- a/src/preload/api/ui-command-event-api.ts +++ b/src/preload/api/ui-command-event-api.ts @@ -63,6 +63,7 @@ export type UiCommandEventApi = { requestId: string url: string worktreeId?: string + browserPageId?: string sessionProfileId?: string | null sessionPartition?: string activate?: boolean @@ -81,7 +82,11 @@ export type UiCommandEventApi = { onRequestTabClose: ( callback: (data: { requestId: string; tabId: string | null; worktreeId?: string }) => void ) => () => void - replyTabClose: (reply: { requestId: string; error?: string }) => void + replyTabClose: (reply: { + requestId: string + error?: string + code?: 'browser_tab_not_found' + }) => void onNewTerminalTab: (callback: () => void) => () => void onFocusBrowserAddressBar: (callback: () => void) => () => void onFindInBrowserPage: (source: BrowserFindSource, callback: () => void) => () => void diff --git a/src/preload/index.ts b/src/preload/index.ts index 24e692088e0..d90bd2a978e 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -3652,6 +3652,7 @@ const api = { requestId: string url: string worktreeId?: string + browserPageId?: string sessionProfileId?: string | null sessionPartition?: string activate?: boolean @@ -3663,6 +3664,7 @@ const api = { requestId: string url: string worktreeId?: string + browserPageId?: string sessionProfileId?: string | null sessionPartition?: string activate?: boolean @@ -3711,7 +3713,11 @@ const api = { ipcRenderer.on('browser:requestTabClose', listener) return () => ipcRenderer.removeListener('browser:requestTabClose', listener) }, - replyTabClose: (reply: { requestId: string; error?: string }): void => { + replyTabClose: (reply: { + requestId: string + error?: string + code?: 'browser_tab_not_found' + }): void => { ipcRenderer.send('browser:tabCloseReply', reply) }, onNewTerminalTab: (callback: () => void): (() => void) => { diff --git a/src/renderer/src/hooks/useIpcEvents.test.ts b/src/renderer/src/hooks/useIpcEvents.test.ts index c19fe942461..064f816516f 100644 --- a/src/renderer/src/hooks/useIpcEvents.test.ts +++ b/src/renderer/src/hooks/useIpcEvents.test.ts @@ -900,6 +900,7 @@ describe('useIpcEvents browser tab create routing', () => { | ((data: { requestId: string worktreeId?: string | null + browserPageId?: string url: string sessionProfileId?: string }) => void) @@ -961,10 +962,15 @@ describe('useIpcEvents browser tab create routing', () => { ] }, createBrowserTab: vi.fn( - (_worktreeId: string, _url: string, options: { activate?: boolean }) => { - const workspace = { id: 'workspace-new', activePageId: 'page-new', pageIds: ['page-new'] } + ( + _worktreeId: string, + _url: string, + options: { activate?: boolean; browserPageId?: string } + ) => { + const pageId = options.browserPageId ?? 'page-new' + const workspace = { id: 'workspace-new', activePageId: pageId, pageIds: [pageId] } state.browserTabsByWorktree['wt-1'].push(workspace) - state.browserPagesByWorkspace['workspace-new'] = [{ id: 'page-new', worktreeId: 'wt-1' }] + state.browserPagesByWorkspace['workspace-new'] = [{ id: pageId, worktreeId: 'wt-1' }] expect(options.activate).toBe(false) return workspace } @@ -1128,20 +1134,21 @@ describe('useIpcEvents browser tab create routing', () => { requestTabCreateListenerRef.current?.({ requestId: 'req-create', worktreeId: 'wt-1', + browserPageId: 'page-canonical', url: 'https://example.com' }) - expect(acquireBrowserAutomationVisibility).toHaveBeenCalledWith('page-new') + expect(acquireBrowserAutomationVisibility).toHaveBeenCalledWith('page-canonical') expect(acquireBrowserAutomationVisibility).not.toHaveBeenCalledWith('page-active') expect(state.createBrowserTab).toHaveBeenCalledWith( 'wt-1', 'https://example.com', - expect.objectContaining({ activate: false }) + expect.objectContaining({ activate: false, browserPageId: 'page-canonical' }) ) expect(state.createBrowserTab.mock.calls[0]?.[2]).not.toHaveProperty('allowWindowClose') expect(replyTabCreate).toHaveBeenCalledWith({ requestId: 'req-create', - browserPageId: 'page-new' + browserPageId: 'page-canonical' }) expect(dispatchEvent).toHaveBeenCalled() expect(releaseBrowserAutomationVisibility).not.toHaveBeenCalled() @@ -3707,6 +3714,46 @@ describe('useIpcEvents browser tab close routing', () => { ) }) + it('refuses to close a browser page outside the requested worktree', async () => { + const requestTabCloseListenerRef: { current: RequestTabCloseListener | null } = { + current: null + } + const closeBrowserTab = vi.fn() + const closeBrowserPage = vi.fn() + const replyTabClose = vi.fn() + + await useIpcEventsForCloseRouting({ + requestTabCloseListenerRef, + replyTabClose, + getState: () => ({ + closeBrowserTab, + closeBrowserPage, + browserTabsByWorktree: { + 'wt-1': [{ id: 'workspace-1' }], + 'wt-2': [{ id: 'workspace-2' }] + }, + browserPagesByWorkspace: { + 'workspace-1': [{ id: 'page-1', workspaceId: 'workspace-1' }], + 'workspace-2': [{ id: 'page-2', workspaceId: 'workspace-2' }] + } + }) + }) + + requestTabCloseListenerRef.current?.({ + requestId: 'req-wrong-worktree', + tabId: 'page-2', + worktreeId: 'wt-1' + }) + + expect(closeBrowserPage).not.toHaveBeenCalled() + expect(closeBrowserTab).not.toHaveBeenCalled() + expect(replyTabClose).toHaveBeenCalledWith({ + requestId: 'req-wrong-worktree', + code: 'browser_tab_not_found', + error: 'Browser tab page-2 not found' + }) + }) + it('closes the active browser tab for the requested worktree when main does not provide a tab id', async () => { const closeBrowserTab = vi.fn() const closeBrowserPage = vi.fn() @@ -4361,6 +4408,7 @@ describe('useIpcEvents browser tab close routing', () => { expect(closeBrowserTab).not.toHaveBeenCalled() expect(replyTabClose).toHaveBeenCalledWith({ requestId: 'req-3', + code: 'browser_tab_not_found', error: 'Browser tab missing-page not found' }) }) diff --git a/src/renderer/src/hooks/useIpcEvents.ts b/src/renderer/src/hooks/useIpcEvents.ts index eb53914e3a9..9ed8d279660 100644 --- a/src/renderer/src/hooks/useIpcEvents.ts +++ b/src/renderer/src/hooks/useIpcEvents.ts @@ -2316,6 +2316,7 @@ export function useIpcEvents(): void { // Agent/automation opens stay in the background (activate:false) in the active browser group. const workspace = store.createBrowserTab(worktreeId, data.url, { title: data.url, + browserPageId: data.browserPageId, targetGroupId: data.activate ? undefined : activeBrowserUnifiedTab?.groupId, sessionProfileId: data.sessionProfileId, sessionPartition: data.sessionPartition, @@ -2404,6 +2405,17 @@ export function useIpcEvents(): void { } const store = useAppStore.getState() const explicitTargetId = data.tabId ?? null + const replyBrowserTabNotFound = (tabId: string): void => { + window.api.ui.replyTabClose({ + requestId: data.requestId, + code: 'browser_tab_not_found', + error: translate( + 'auto.hooks.useIpcEvents.0e3cf53060', + 'Browser tab {{value0}} not found', + { value0: tabId } + ) + }) + } const replyPinnedBrowserCloseCanceled = (tabId: string): void => { window.api.ui.replyTabClose({ requestId: data.requestId, @@ -2455,11 +2467,15 @@ export function useIpcEvents(): void { ) if (owningWorkspace) { const [workspaceId, pages] = owningWorkspace + const owningWorktreeId = + Object.entries(store.browserTabsByWorktree).find(([, tabs]) => + tabs.some((tab) => tab.id === workspaceId) + )?.[0] ?? null + if (data.worktreeId && owningWorktreeId !== data.worktreeId) { + replyBrowserTabNotFound(tabToClose) + return + } if (pages.length <= 1) { - const owningWorktreeId = - Object.entries(store.browserTabsByWorktree).find(([, tabs]) => - tabs.some((tab) => tab.id === workspaceId) - )?.[0] ?? null if (owningWorktreeId) { closeBrowserWorkspaceWithReply(owningWorktreeId, workspaceId) return @@ -2477,18 +2493,15 @@ export function useIpcEvents(): void { tabs.some((tab) => tab.id === tabToClose) )?.[0] ?? null if (owningWorktreeId) { + if (data.worktreeId && owningWorktreeId !== data.worktreeId) { + replyBrowserTabNotFound(tabToClose) + return + } closeBrowserWorkspaceWithReply(owningWorktreeId, tabToClose) return } if (explicitTargetId) { - window.api.ui.replyTabClose({ - requestId: data.requestId, - error: translate( - 'auto.hooks.useIpcEvents.0e3cf53060', - 'Browser tab {{value0}} not found', - { value0: explicitTargetId } - ) - }) + replyBrowserTabNotFound(explicitTargetId) return } store.closeBrowserTab(tabToClose) diff --git a/src/renderer/src/runtime/web-runtime-session.test.ts b/src/renderer/src/runtime/web-runtime-session.test.ts index ba309a39f91..115edd0ce99 100644 --- a/src/renderer/src/runtime/web-runtime-session.test.ts +++ b/src/renderer/src/runtime/web-runtime-session.test.ts @@ -638,6 +638,110 @@ describe('createWebRuntimeSessionBrowserTab', () => { expect(mocks.createBrowserTab).not.toHaveBeenCalled() }) + it('cleans up a known page identity when the create acknowledgement is lost', async () => { + mocks.getState.mockReturnValue({ + ...mocks.getState(), + runtimeStatusByEnvironmentId: new Map([ + [ + ENVIRONMENT_ID, + { + status: { + capabilities: ['browser.screencast.v1', 'browser.tab-create-known-id.v1'] + }, + checkedAt: 1 + } + ] + ]) + }) + const runtimeCall = vi + .fn() + .mockResolvedValueOnce({ + id: 'create-lost', + ok: false, + error: { code: 'runtime_timeout', message: 'response lost' } + }) + .mockResolvedValueOnce({ + id: 'close-missing', + ok: false, + error: { code: 'browser_tab_not_found', message: 'page was not created' } + }) + vi.stubGlobal('window', { api: { runtimeEnvironments: { call: runtimeCall } } }) + + await expect( + createWebRuntimeSessionBrowserTab({ + worktreeId: WORKTREE_ID, + url: 'https://example.com/', + waitForRegistration: true + }) + ).resolves.toBe(false) + + const createPage = runtimeCall.mock.calls[0]?.[0].params.page + expect(createPage).toMatch(/^[0-9a-f-]{36}$/) + expect(runtimeCall).toHaveBeenNthCalledWith(2, { + selector: ENVIRONMENT_ID, + method: 'browser.tabClose', + params: { worktree: `id:${WORKTREE_ID}`, page: createPage }, + timeoutMs: 15_000 + }) + }) + + it('materializes an owner-pinned tab without waiting for its navigation', async () => { + mocks.getState.mockReturnValue({ + ...mocks.getState(), + runtimeStatusByEnvironmentId: new Map([ + [ + ENVIRONMENT_ID, + { + status: { + capabilities: ['browser.screencast.v1', 'browser.tab-create-known-id.v1'] + }, + checkedAt: 1 + } + ] + ]) + }) + let resolveNavigation!: (value: unknown) => void + const navigation = new Promise((resolve) => { + resolveNavigation = resolve + }) + const runtimeCall = vi.fn((request: { method: string; params: { page?: string } }) => { + if (request.method === 'browser.tabCreate') { + return Promise.resolve({ + id: 'create', + ok: true, + result: { browserPageId: request.params.page } + }) + } + if (request.method === 'browser.goto') { + return navigation + } + return Promise.resolve({ id: 'list', ok: true, result: makeSnapshot() }) + }) + vi.stubGlobal('window', { api: { runtimeEnvironments: { call: runtimeCall } } }) + + const created = createWebRuntimeSessionBrowserTab({ + worktreeId: WORKTREE_ID, + url: 'https://example.com/slow', + waitForRegistration: true + }) + + await expect(created).resolves.toBe(true) + expect(runtimeCall.mock.calls.map(([request]) => request.method)).toEqual([ + 'browser.tabCreate', + 'browser.goto', + 'session.tabs.list' + ]) + expect(runtimeCall.mock.calls[0]?.[0].params).toMatchObject({ + url: undefined, + waitForRegistration: true + }) + expect(runtimeCall.mock.calls[1]?.[0].params).toMatchObject({ + page: runtimeCall.mock.calls[0]?.[0].params.page, + url: 'https://example.com/slow' + }) + resolveNavigation({ id: 'goto', ok: true, result: { url: 'https://example.com/slow' } }) + }) + it.each(['browser_error', 'invalid_argument', 'method_not_found'])( 'reports a definitive browser-create %s as a soft failure', async (code) => { diff --git a/src/renderer/src/runtime/web-runtime-session.ts b/src/renderer/src/runtime/web-runtime-session.ts index 164edbed4b7..691202eae08 100644 --- a/src/renderer/src/runtime/web-runtime-session.ts +++ b/src/renderer/src/runtime/web-runtime-session.ts @@ -19,7 +19,10 @@ import type { SleepingAgentLaunchConfig, AgentProviderSessionMetadata } from '../../../shared/agent-session-resume' -import { AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY } from '../../../shared/protocol-version' +import { + AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY, + BROWSER_TAB_CREATE_KNOWN_ID_RUNTIME_CAPABILITY +} from '../../../shared/protocol-version' import type { AgentLaunchPreferences, AgentPromptDelivery, @@ -482,6 +485,10 @@ export async function createWebRuntimeSessionBrowserTab(args: { const shouldFocusOnCreate = args.focusOnCreate !== false const shouldSelectWorktree = args.selectWorktree !== false const provisionalPageId = createBrowserUuid() + const hostSupportsKnownPageId = useAppStore + .getState() + .runtimeStatusByEnvironmentId?.get(environmentId) + ?.status?.capabilities?.includes(BROWSER_TAB_CREATE_KNOWN_ID_RUNTIME_CAPABILITY) let unsubscribeFocusGuard = (): void => {} let guardedPageId = provisionalPageId let createdPageId: string | null = null @@ -540,12 +547,15 @@ export async function createWebRuntimeSessionBrowserTab(args: { }) } createAttempted = true + const navigateAfterCreate = + args.waitForRegistration === true && args.url && args.url !== 'about:blank' const created = unwrapRuntimeRpcResult( (await callEnvironment({ method: 'browser.tabCreate', params: { worktree: toRuntimeWorktreeSelector(args.worktreeId), - url: args.url, + url: navigateAfterCreate ? undefined : args.url, + ...(hostSupportsKnownPageId ? { page: provisionalPageId } : {}), profileId: args.profileId ?? undefined, activate: shouldFocusOnCreate, // Why: place the new browser in the clicked split group so the host snapshot is authoritative for it (no left-snap). @@ -557,6 +567,24 @@ export async function createWebRuntimeSessionBrowserTab(args: { })) as RuntimeRpcResponse ) createdPageId = created.browserPageId + if (navigateAfterCreate) { + void callEnvironment({ + method: 'browser.goto', + params: { + worktree: toRuntimeWorktreeSelector(args.worktreeId), + page: created.browserPageId, + url: args.url + }, + timeoutMs: 15_000 + }) + .then((response) => unwrapRuntimeRpcResult(response)) + .catch((error) => { + console.warn( + '[web-runtime-session] created browser tab navigation failed:', + error instanceof Error ? error.message : String(error) + ) + }) + } await pauseAfterE2eWebRuntimeBrowserCreate(created.browserPageId) if (created.browserPageId !== provisionalPageId) { moveWebSessionBrowserPlacement({ @@ -635,7 +663,11 @@ export async function createWebRuntimeSessionBrowserTab(args: { } catch (error) { unsubscribeFocusGuard() let recoveryError: unknown = null - const createOutcomeUnknown = !createdPageId && !isDefinitiveBrowserCreateFailure(error) + const createFailureDefinitive = isDefinitiveBrowserCreateFailure(error) + const cleanupPageId = + createdPageId ?? + (!createFailureDefinitive && hostSupportsKnownPageId ? provisionalPageId : null) + const createOutcomeUnknown = !cleanupPageId && !createFailureDefinitive const ownsClientGroupCleanup = args.clientTargetGroupId ? releaseWebSessionBrowserPlacementGroup({ environmentId, @@ -652,14 +684,14 @@ export async function createWebRuntimeSessionBrowserTab(args: { remotePageId: guardedPageId }) } - if (createdPageId) { + if (cleanupPageId) { try { const closeResult = unwrapRuntimeRpcResult( (await callEnvironment({ method: 'browser.tabClose', params: { worktree: toRuntimeWorktreeSelector(args.worktreeId), - page: createdPageId + page: cleanupPageId }, timeoutMs: 15_000 })) as RuntimeRpcResponse<{ closed: boolean }> @@ -677,17 +709,25 @@ export async function createWebRuntimeSessionBrowserTab(args: { useAppStore.getState(), environmentId, args.worktreeId, - createdPageId + cleanupPageId ) ) { throw new Error('The closed browser tab remained materialized in the client.') } } catch (cleanupError) { - recoveryError = cleanupError - console.warn( - '[web-runtime-session] failed to clean up unreconciled browser tab:', - cleanupError instanceof Error ? cleanupError.message : String(cleanupError) - ) + if ( + !createdPageId && + hostSupportsKnownPageId && + hasRuntimeRpcErrorCode(cleanupError, 'browser_tab_not_found') + ) { + recoveryError = null + } else { + recoveryError = cleanupError + console.warn( + '[web-runtime-session] failed to clean up unreconciled browser tab:', + cleanupError instanceof Error ? cleanupError.message : String(cleanupError) + ) + } } } if (shouldFocusOnCreate) { diff --git a/src/renderer/src/store/slices/browser.test.ts b/src/renderer/src/store/slices/browser.test.ts index 5239a5537ab..abccad13dc4 100644 --- a/src/renderer/src/store/slices/browser.test.ts +++ b/src/renderer/src/store/slices/browser.test.ts @@ -164,6 +164,22 @@ function makeAnnotation(pageId: string, id = 'annotation-1'): BrowserPageAnnotat } describe('createBrowserSlice annotations', () => { + it('keeps a requested canonical page identity distinct from its workspace', () => { + const store = createTestStore() + const tab = store.getState().createBrowserTab('wt-1', 'about:blank', { + browserPageId: 'page-canonical' + }) + + expect(tab.id).not.toBe('page-canonical') + expect(tab.activePageId).toBe('page-canonical') + expect(store.getState().browserPagesByWorkspace[tab.id]?.[0]?.id).toBe('page-canonical') + expect(() => + store.getState().createBrowserTab('wt-1', 'about:blank', { + browserPageId: 'page-canonical' + }) + ).toThrow('Browser page page-canonical already exists') + }) + it('records browser-tab-created only for the explicit new-tab action', async () => { const store = createTestStore() diff --git a/src/renderer/src/store/slices/browser.ts b/src/renderer/src/store/slices/browser.ts index 6f3c739308a..946fdd4e853 100644 --- a/src/renderer/src/store/slices/browser.ts +++ b/src/renderer/src/store/slices/browser.ts @@ -68,6 +68,7 @@ import { type CreateBrowserTabOptions = { activate?: boolean + browserPageId?: string title?: string sessionProfileId?: string | null sessionPartition?: string | null @@ -390,11 +391,12 @@ function buildBrowserPage( worktreeId: string, url: string, title?: string, - browserRuntimeEnvironmentId?: string | null + browserRuntimeEnvironmentId?: string | null, + browserPageId?: string ): BrowserPage { const normalizedUrl = normalizeUrl(url) return { - id: createBrowserUuid(), + id: browserPageId ?? createBrowserUuid(), workspaceId, worktreeId, url: normalizedUrl, @@ -599,12 +601,21 @@ export const createBrowserSlice: StateCreator = createBrowserTab: (worktreeId, url, options) => { assertManagedBrowserMaterializationAllowed(get(), options?.browserRuntimeEnvironmentId) const workspaceId = createBrowserUuid() + const browserPageId = options?.browserPageId + if ( + browserPageId && + (findWorkspace(get().browserTabsByWorktree, browserPageId) || + findPage(get().browserPagesByWorkspace, browserPageId)) + ) { + throw new Error(`Browser page ${browserPageId} already exists`) + } const page = buildBrowserPage( workspaceId, worktreeId, url, options?.title, - options?.browserRuntimeEnvironmentId + options?.browserRuntimeEnvironmentId, + browserPageId ) // Why: with no explicit profile, inherit the user's default so a Settings preference applies to new tabs. const sessionProfileId = diff --git a/src/shared/protocol-version.ts b/src/shared/protocol-version.ts index eb61be79c97..a2b20a8b99e 100644 --- a/src/shared/protocol-version.ts +++ b/src/shared/protocol-version.ts @@ -56,6 +56,10 @@ export const AI_VAULT_SESSION_TITLES_RUNTIME_CAPABILITY = 'aiVault.session-title export const BROWSER_HEADLESS_RUNTIME_CAPABILITY = 'browser.headless.v1' as const export const BROWSER_SCREENCAST_RUNTIME_CAPABILITY = 'browser.screencast.v1' as const export const BROWSER_CERTIFICATE_TRUST_RUNTIME_CAPABILITY = 'browser.certificate-trust.v1' as const +// Why: older hosts discard browser.tabCreate's page field, so clients may only +// treat a preallocated page ID as canonical when this is advertised. +export const BROWSER_TAB_CREATE_KNOWN_ID_RUNTIME_CAPABILITY = + 'browser.tab-create-known-id.v1' as const // Why: hosts without this strip terminal.send's inputKind (zod object drops // unknown keys), so a mobile xterm query reply would land as ordinary // floor-taking input. Mobile must not forward replies unless advertised. @@ -99,6 +103,7 @@ export const RUNTIME_CAPABILITIES = [ ORCHESTRATION_WORKER_LAUNCH_PREFERENCES_RUNTIME_CAPABILITY, ORCHESTRATION_CONTRACT_RUNTIME_CAPABILITY, BROWSER_SCREENCAST_RUNTIME_CAPABILITY, + BROWSER_TAB_CREATE_KNOWN_ID_RUNTIME_CAPABILITY, 'terminal.binary-stream.v1', 'terminal.multiplex.v1', 'workspace-ports.v1', diff --git a/tests/e2e/paired-browser-create-navigation-deadline.spec.ts b/tests/e2e/paired-browser-create-navigation-deadline.spec.ts new file mode 100644 index 00000000000..c46cf94056e --- /dev/null +++ b/tests/e2e/paired-browser-create-navigation-deadline.spec.ts @@ -0,0 +1,383 @@ +import { createServer, type Server, type ServerResponse } from 'node:http' +import type { AddressInfo } from 'node:net' +import type { Page, TestInfo } from '@stablyai/playwright-test' +import { RuntimeClient } from '../../src/cli/runtime/client' +import type { RuntimeMobileSessionTabsResult } from '../../src/shared/runtime-types' +import { expect, test } from './helpers/orca-app' +import { + createRuntimeDesktopPairingOffer, + launchPairedElectronClient, + type PairedElectronClient +} from './helpers/paired-electron-client' +import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store' + +const OPERATION_ID = 'sta-4231-owner-pinned-navigation-hold' + +type HeldNavigationServer = { + close: () => Promise + pendingCount: () => number + release: () => void + sourceUrl: string + url: string +} + +type CreateOutcome = { + error: string | null + ok: boolean + pageId: string | null +} + +async function startHeldNavigationServer(): Promise { + const pending = new Set() + const server: Server = createServer((request, response) => { + if (request.url?.startsWith('/source')) { + response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }) + response.end( + `open held page` + ) + return + } + if (!request.url?.startsWith('/hold')) { + response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' }) + response.end('not found') + return + } + pending.add(response) + response.once('close', () => pending.delete(response)) + }) + await new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', () => { + server.off('error', reject) + resolve() + }) + }) + const { port } = server.address() as AddressInfo + const release = (): void => { + for (const response of pending) { + if (!response.destroyed && !response.writableEnded) { + response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }) + response.end('released') + } + } + pending.clear() + } + return { + close: () => + new Promise((resolve, reject) => { + release() + server.closeAllConnections() + server.close((error) => (error ? reject(error) : resolve())) + }), + pendingCount: () => pending.size, + release, + sourceUrl: `http://127.0.0.1:${port}/source`, + url: `http://127.0.0.1:${port}/hold?operation=${OPERATION_ID}` + } +} + +async function readHostBrowserPageIds( + hostClient: RuntimeClient, + worktreeId: string +): Promise { + const response = await hostClient.call('session.tabs.list', { + worktree: `id:${worktreeId}` + }) + return response.result.tabs.flatMap((tab) => + tab.type === 'browser' && tab.browserPageId ? [tab.browserPageId] : [] + ) +} + +async function createOwnerPinnedBrowser( + page: Page, + environmentId: string, + worktreeId: string, + url: string +): Promise { + return page.evaluate( + async ({ environmentId, url, worktreeId }) => { + try { + const response = await window.api.runtimeEnvironments.call({ + selector: environmentId, + method: 'browser.tabCreate', + params: { + activate: true, + url, + waitForRegistration: true, + worktree: `id:${worktreeId}` + }, + timeoutMs: 15_000 + }) + if (!response.ok) { + return { + error: `${response.error.code}: ${response.error.message}`, + ok: false, + pageId: null + } + } + const result = response.result as { browserPageId: string } + return { error: null, ok: true, pageId: result.browserPageId } + } catch (error) { + return { + error: error instanceof Error ? error.message : String(error), + ok: false, + pageId: null + } + } + }, + { environmentId, url, worktreeId } + ) +} + +async function findPairedWorktreeId( + client: PairedElectronClient, + repoPath: string +): Promise { + const worktreeId = await expect + .poll( + () => + client.page.evaluate( + (path) => + window.__store + ?.getState() + .allWorktrees() + .find((worktree) => worktree.path === path)?.id ?? null, + repoPath + ), + { timeout: 60_000, message: 'paired client never received the host worktree' } + ) + .not.toBeNull() + .then(() => + client.page.evaluate( + (path) => + window.__store + ?.getState() + .allWorktrees() + .find((worktree) => worktree.path === path)?.id ?? null, + repoPath + ) + ) + if (!worktreeId) { + throw new Error('Paired worktree disappeared after discovery') + } + return worktreeId +} + +async function readClientBrowserPageIds(page: Page, worktreeId: string): Promise { + return page.evaluate((worktreeId) => { + const state = window.__store?.getState() + return (state?.browserTabsByWorktree[worktreeId] ?? []).flatMap((workspace) => + (state?.browserPagesByWorkspace[workspace.id] ?? []).map( + (browserPage) => + state?.remoteBrowserPageHandlesByPageId[browserPage.id]?.remotePageId ?? browserPage.id + ) + ) + }, worktreeId) +} + +async function findMirroredPageId( + page: Page, + worktreeId: string, + url: string +): Promise { + return page.evaluate( + ({ url, worktreeId }) => { + const state = window.__store?.getState() + for (const workspace of state?.browserTabsByWorktree[worktreeId] ?? []) { + const browserPage = (state?.browserPagesByWorkspace[workspace.id] ?? []).find((candidate) => + candidate.url.startsWith(url) + ) + if (browserPage) { + return browserPage.id + } + } + return null + }, + { url, worktreeId } + ) +} + +async function openLinkFromRemotePane(page: Page, testInfo: TestInfo): Promise { + const frame = page.locator('[data-testid="remote-browser-frame"]:visible').first() + await expect(frame).toBeVisible({ timeout: 60_000 }) + await frame.click({ button: 'right', position: { x: 60, y: 60 }, force: true }) + const open = page.getByRole('menuitem', { name: 'Open Link In Orca Browser' }) + await expect(open).toBeVisible({ timeout: 30_000 }) + await page.screenshot({ + path: testInfo.outputPath('sta-4231-owner-pinned-link-route.png'), + fullPage: true + }) + await open.click() +} + +test('returns a headed host page identity before owner-pinned navigation can time out @headful', async ({ + electronApp, + orcaPage, + testRepoPath +}, testInfo: TestInfo) => { + test.setTimeout(300_000) + const fixture = await startHeldNavigationServer() + let client: PairedElectronClient | null = null + try { + await waitForSessionReady(orcaPage) + await waitForActiveWorktree(orcaPage) + await ensureTerminalVisible(orcaPage) + const offer = await createRuntimeDesktopPairingOffer(orcaPage) + const userDataDir = await electronApp.evaluate(({ app }) => app.getPath('userData')) + const hostClient = new RuntimeClient(userDataDir, 5_000) + client = await launchPairedElectronClient(offer, testInfo, 'STA-4231 navigation deadline') + const cdp = await client.page.context().newCDPSession(client.page) + const visibility = await cdp.send('Runtime.evaluate', { + expression: 'document.visibilityState', + returnByValue: true + }) + expect(visibility.result.value).toBe('visible') + const worktreeId = await findPairedWorktreeId(client, testRepoPath) + const baselineHostPageIds = await readHostBrowserPageIds(hostClient, worktreeId) + await client.page.evaluate( + ({ environmentId, worktreeId }) => { + window.__store?.getState().setActiveWorktree(worktreeId, `runtime:${environmentId}`) + }, + { environmentId: client.environmentId, worktreeId } + ) + + const firstPromise = createOwnerPinnedBrowser( + client.page, + client.environmentId, + worktreeId, + fixture.url + ) + await expect.poll(fixture.pendingCount, { timeout: 30_000 }).toBe(1) + const firstHostPageId = await expect + .poll( + async () => { + const ids = await readHostBrowserPageIds(hostClient, worktreeId) + return ids.find((id) => !baselineHostPageIds.includes(id)) ?? null + }, + { timeout: 30_000, message: 'host never registered the owner-pinned page' } + ) + .not.toBeNull() + .then(async () => { + const ids = await readHostBrowserPageIds(hostClient, worktreeId) + return ids.find((id) => !baselineHostPageIds.includes(id)) ?? null + }) + const first = await firstPromise + + let retry: CreateOutcome | null = null + if (!first.ok) { + const retryPromise = createOwnerPinnedBrowser( + client.page, + client.environmentId, + worktreeId, + fixture.url + ) + await expect.poll(fixture.pendingCount, { timeout: 30_000 }).toBe(2) + retry = await retryPromise + } + const hostPageIds = (await readHostBrowserPageIds(hostClient, worktreeId)).filter( + (id) => !baselineHostPageIds.includes(id) + ) + await expect + .poll(() => readClientBrowserPageIds(client!.page, worktreeId), { + timeout: 30_000, + message: 'paired client did not materialize the canonical host page' + }) + .toContain(firstHostPageId) + await client.page.screenshot({ + path: testInfo.outputPath('sta-4231-headed-canonical-page.png'), + fullPage: true + }) + + expect({ first, firstHostPageId, hostPageIds, retry }).toEqual({ + first: { error: null, ok: true, pageId: firstHostPageId }, + firstHostPageId, + hostPageIds: [firstHostPageId], + retry: null + }) + } finally { + fixture.release() + await client?.dispose() + await fixture.close() + } +}) + +test('opens the held URL through the owner-pinned remote-pane link route @headful', async ({ + electronApp, + orcaPage, + testRepoPath +}, testInfo: TestInfo) => { + test.setTimeout(300_000) + const fixture = await startHeldNavigationServer() + let client: PairedElectronClient | null = null + try { + await waitForSessionReady(orcaPage) + await waitForActiveWorktree(orcaPage) + await ensureTerminalVisible(orcaPage) + const offer = await createRuntimeDesktopPairingOffer(orcaPage) + const userDataDir = await electronApp.evaluate(({ app }) => app.getPath('userData')) + const hostClient = new RuntimeClient(userDataDir, 5_000) + client = await launchPairedElectronClient(offer, testInfo, 'STA-4231 owner-pinned link route') + const worktreeId = await findPairedWorktreeId(client, testRepoPath) + await client.page.evaluate( + ({ environmentId, worktreeId }) => { + window.__store?.getState().setActiveWorktree(worktreeId, `runtime:${environmentId}`) + }, + { environmentId: client.environmentId, worktreeId } + ) + await hostClient.call('browser.tabCreate', { + activate: true, + url: fixture.sourceUrl, + worktree: `id:${worktreeId}` + }) + const sourcePageId = await expect + .poll(() => findMirroredPageId(client!.page, worktreeId, fixture.sourceUrl), { + timeout: 60_000, + message: 'paired client never mirrored the source browser pane' + }) + .not.toBeNull() + .then(() => findMirroredPageId(client!.page, worktreeId, fixture.sourceUrl)) + if (!sourcePageId) { + throw new Error('Source browser page disappeared') + } + await client.page.evaluate( + ({ pageId, worktreeId }) => + window.__store?.getState().focusBrowserTabInWorktree(worktreeId, pageId, { + surfacePane: true + }), + { pageId: sourcePageId, worktreeId } + ) + const baselineHostPageIds = await readHostBrowserPageIds(hostClient, worktreeId) + + await openLinkFromRemotePane(client.page, testInfo) + await expect.poll(fixture.pendingCount, { timeout: 30_000 }).toBe(1) + const createdPageId = await expect + .poll( + async () => { + const ids = await readHostBrowserPageIds(hostClient, worktreeId) + return ids.find((id) => !baselineHostPageIds.includes(id)) ?? null + }, + { timeout: 30_000, message: 'link route never registered a host page' } + ) + .not.toBeNull() + .then(async () => { + const ids = await readHostBrowserPageIds(hostClient, worktreeId) + return ids.find((id) => !baselineHostPageIds.includes(id)) ?? null + }) + expect(createdPageId).not.toBeNull() + await expect + .poll(() => readClientBrowserPageIds(client!.page, worktreeId), { + timeout: 30_000, + message: 'link route did not materialize the canonical host page' + }) + .toContain(createdPageId) + expect( + (await readHostBrowserPageIds(hostClient, worktreeId)).filter( + (id) => !baselineHostPageIds.includes(id) + ) + ).toEqual([createdPageId]) + } finally { + fixture.release() + await client?.dispose() + await fixture.close() + } +}) From 77f23b013f0206a53becad24736acba87b171ce2 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:48:24 -0700 Subject: [PATCH 09/42] refactor(shared): drop the shared/types barrel and import from the real modules (#14447) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #14397 split `shared/types.ts` into 46 per-domain modules but kept the path as a re-export barrel so the import sites did not have to change. This removes the barrel: every consumer now imports from the module that actually declares the type, and `src/shared/types.ts` is deleted. Barrels hide where a type lives, make every consumer look like it depends on the whole domain, and let an unrelated edit invalidate a module that ~2,000 files transitively import. 2,323 import declarations across 2,321 files. Rewritten mechanically: each specifier was resolved to an absolute path via the TypeScript AST and recomputed, rather than string-substituted, so alias forms (`@/../../shared/ types`) and per-specifier `type` modifiers survive. Four cases the mechanical pass had to handle, each found by a gate rather than by reading the diff: - Modules inside `src/shared` import the barrel as `./types`, not `shared/types`. A pre-filter on the latter string skipped 176 of them and left imports dangling at a deleted file, which surfaced as confusing `Property 'x' is optional in type 'Repo' but required in Pick` errors rather than "module not found". - The barrel RENAMED one type on the way through (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name in the owning module has to be re-aliased at each consumer. - Three test files put `;(globalThis as ...)` on the line after the import. TypeScript parses that `;` as the import statement's terminator, so replacing through `statement.getEnd()` deletes it and breaks ASI. The rewrite now stops at the module specifier. - A file that already imported directly from a module got a SECOND import from it, because the barrel re-exported those same names — which trips `import/no-duplicates` under `--deny-warnings`. A post-pass merges declarations sharing a specifier and type-only-ness; the `import type` plus `import` pair from one module is left alone, since that form is allowed. Splitting one barrel import into several genuinely adds lines, which pushed `terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character import must wrap, and neither local type collapses onto one line (101 and 116 characters). Rather than contort a type declaration to fit a line budget, `collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` — they are pure structural operations on the layout tree and independent of PTY ownership. `visible-worktrees.ts` similarly loses its own mini-barrel re-export of `isDefaultBranchWorkspace`, with the four real consumers repointed at the declaring module. No `max-lines` bypass added. Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted first — these projects are `composite: true` and reuse stale caches); the full `pnpm lint` green, not just bare oxlint — the narrower local check is what let the duplicate imports reach CI; max-lines ratchet OK at 344. --- .../remote-shared-control-retirement-probe.ts | 2 +- mobile/app/h/[hostId]/index.tsx | 2 +- .../app/h/[hostId]/session/[worktreeId].tsx | 3 +- mobile/app/h/[hostId]/tasks.tsx | 12 +- mobile/scripts/mock-server-rpc-handlers.ts | 2 +- mobile/src/components/MobileAgentIcon.tsx | 2 +- .../src/components/MobileDiffReviewBody.tsx | 2 +- .../components/MobileDiffReviewDrawers.tsx | 2 +- .../MobileDiffReviewFileSummary.tsx | 2 +- .../src/components/MobileDiffReviewLine.tsx | 2 +- mobile/src/components/NewWorktreeModal.tsx | 3 +- .../mobile-agent-icon-assets.test.ts | 2 +- .../components/mobile-agent-icon-assets.ts | 2 +- .../new-worktree-agent-selection.ts | 2 +- .../pr-sidebar/PRActionsSection.tsx | 2 +- .../components/pr-sidebar/PRCheckDetail.tsx | 2 +- .../components/pr-sidebar/PRChecksSection.tsx | 2 +- .../components/pr-sidebar/PRCommentCard.tsx | 6 +- .../pr-sidebar/PRCommentsSection.test.ts | 3 +- .../pr-sidebar/PRCommentsSection.tsx | 3 +- .../pr-sidebar/PRConflictingFilesSection.tsx | 2 +- .../pr-sidebar/PRReviewersSection.tsx | 2 +- .../components/pr-sidebar/PRSidebarHeader.tsx | 3 +- .../pr-sidebar/ReviewerPickerDrawer.tsx | 2 +- .../components/pr-sidebar/pr-actions-state.ts | 2 +- .../pr-sidebar/pr-auto-merge-availability.ts | 2 +- .../pr-check-detail-content.test.ts | 2 +- .../pr-sidebar/pr-check-detail-content.ts | 2 +- .../pr-sidebar/pr-checks-presentation.test.ts | 2 +- .../pr-sidebar/pr-checks-presentation.ts | 6 +- .../pr-conflict-presentation.test.ts | 2 +- .../pr-sidebar/pr-conflict-presentation.ts | 2 +- mobile/src/session/QuickCommandRow.tsx | 2 +- mobile/src/session/QuickCommandsList.tsx | 3 +- mobile/src/session/QuickCommandsSheet.test.ts | 2 +- mobile/src/session/QuickCommandsSheet.tsx | 2 +- mobile/src/session/ai-vault-resume-launch.ts | 2 +- .../src/session/github-pr-comment-parsers.ts | 6 +- mobile/src/session/github-pr-mutations.ts | 2 +- mobile/src/session/github-pr-parsers.ts | 16 +- mobile/src/session/github-pr-rpc.ts | 10 +- mobile/src/session/github-pr-value-readers.ts | 8 +- .../session/mobile-diff-comment-edit.test.ts | 2 +- .../src/session/mobile-diff-comment-edit.ts | 2 +- .../src/session/mobile-diff-comments.test.ts | 2 +- mobile/src/session/mobile-diff-comments.ts | 2 +- .../session/mobile-diff-review-positioning.ts | 2 +- .../session/mobile-diff-review-queue.test.ts | 2 +- .../src/session/mobile-diff-review-queue.ts | 6 +- .../mobile-diff-review-screen-model.ts | 2 +- .../src/session/mobile-diff-review-state.ts | 2 +- .../session/mobile-new-tab-agent-options.ts | 2 +- mobile/src/session/mobile-pr-sidebar-state.ts | 4 +- .../src/session/mobile-session-route-types.ts | 3 +- .../session/mobile-terminal-tab-agent.test.ts | 2 +- .../src/session/mobile-terminal-tab-agent.ts | 2 +- .../session/pr-action-mutation-contract.ts | 2 +- mobile/src/session/pr-actions-engine.ts | 2 +- .../src/session/pr-ai-triage-prompt.test.ts | 2 +- mobile/src/session/pr-ai-triage-prompt.ts | 2 +- mobile/src/session/pr-comment-actions.test.ts | 2 +- mobile/src/session/pr-comment-actions.ts | 3 +- mobile/src/session/pr-title-edit.ts | 2 +- mobile/src/session/quick-command-draft.ts | 6 +- .../use-mobile-diff-review-comment-actions.ts | 2 +- .../use-mobile-diff-review-controller.ts | 2 +- ...se-mobile-diff-review-send-actions.test.ts | 2 +- .../use-mobile-diff-review-send-actions.ts | 2 +- .../session/use-mobile-pr-comment-actions.ts | 2 +- .../use-mobile-pr-sidebar-controller.test.ts | 4 +- mobile/src/session/use-quick-commands.test.ts | 2 +- mobile/src/session/use-quick-commands.ts | 2 +- .../source-control/MobileGitHistoryList.tsx | 2 +- .../mobile-branch-compare.test.ts | 2 +- .../source-control/mobile-branch-compare.ts | 2 +- .../mobile-pr-chip-summary.test.ts | 4 +- .../source-control/mobile-pr-chip-summary.ts | 2 +- mobile/src/tasks/blank-workspace-create.ts | 2 +- .../tasks/composer-linked-work-item.test.ts | 4 +- mobile/src/tasks/composer-linked-work-item.ts | 4 +- .../src/tasks/composer-source-base-resolve.ts | 2 +- mobile/src/tasks/github-check-summary.test.ts | 2 +- mobile/src/tasks/github-check-summary.ts | 2 +- mobile/src/tasks/gitlab-check-summary.ts | 2 +- mobile/src/tasks/mobile-agent-catalog.ts | 2 +- .../src/tasks/mobile-hosted-check-status.ts | 5 +- mobile/src/tasks/mobile-tui-agents.ts | 2 +- mobile/src/tasks/setup-hook-trust.test.ts | 2 +- mobile/src/tasks/setup-hook-trust.ts | 2 +- mobile/src/tasks/smart-source-fan-out.ts | 10 +- mobile/src/tasks/smart-source-paste-intent.ts | 3 +- .../src/tasks/smart-source-search-requests.ts | 10 +- .../src/tasks/use-mobile-composer-source.ts | 4 +- .../src/tasks/use-smart-workspace-source.ts | 3 +- mobile/src/tasks/workspace-agent-selection.ts | 2 +- mobile/src/tasks/workspace-create-params.ts | 8 +- mobile/src/terminal/quick-commands.test.ts | 2 +- mobile/src/terminal/quick-commands.ts | 3 +- .../src/worktree/mobile-workspace-statuses.ts | 2 +- mobile/src/worktree/use-workspace-sections.ts | 2 +- .../src/worktree/workspace-list-sections.ts | 2 +- .../src/worktree/workspace-view-settings.ts | 2 +- src/cli/handlers/account.ts | 5 +- src/cli/handlers/agent-hooks.test.ts | 2 +- src/cli/handlers/agent-hooks.ts | 3 +- src/cli/handlers/automations.ts | 3 +- src/cli/handlers/diagnostics.ts | 2 +- src/cli/handlers/project.ts | 6 +- src/cli/handlers/vm.ts | 2 +- src/cli/project-format.ts | 2 +- src/cli/workspace-format.ts | 2 +- src/cli/worktree-project-target.ts | 2 +- src/main/active-view-preference.ts | 2 +- .../first-work-branch-rename-test-harness.ts | 3 +- .../first-work-branch-rename.test.ts | 3 +- .../agent-hooks/first-work-branch-rename.ts | 3 +- .../first-work-folder-rename.test.ts | 3 +- .../agent-hooks/first-work-folder-rename.ts | 3 +- .../agent-hooks/local-agent-cli-presence.ts | 2 +- .../managed-agent-hook-controls.ts | 2 +- .../managed-hook-detection-commands.ts | 2 +- .../restored-subagent-liveness-sweep.ts | 3 +- src/main/agent-hooks/server.ts | 2 +- .../remote-session-scanner-test-fixtures.ts | 2 +- .../headless-workspace-create.test.ts | 2 +- .../automations/headless-workspace-create.ts | 2 +- .../automations/run-target-resolution.test.ts | 3 +- src/main/automations/run-target-resolution.ts | 3 +- src/main/automations/service-precheck.test.ts | 2 +- src/main/automations/service.test.ts | 2 +- src/main/automations/workspace-provenance.ts | 6 +- src/main/azure-devops/pull-request-mappers.ts | 2 +- src/main/bitbucket/client.ts | 2 +- src/main/bitbucket/pull-request-mappers.ts | 2 +- .../browser/browser-certificate-challenge.ts | 2 +- .../browser-certificate-trust-controller.ts | 5 +- src/main/browser/browser-cookie-import.ts | 2 +- src/main/browser/browser-manager.ts | 6 +- src/main/browser/browser-session-registry.ts | 2 +- .../browser-session-user-agent-mode.ts | 2 +- .../claude-duplicate-account.test.ts | 2 +- .../claude-duplicate-account.ts | 2 +- .../runtime-auth-service.test.ts | 3 +- .../claude-accounts/runtime-auth-service.ts | 2 +- .../claude-accounts/runtime-selection.test.ts | 3 +- src/main/claude-accounts/runtime-selection.ts | 6 +- src/main/claude-accounts/service.test.ts | 2 +- src/main/claude-accounts/service.ts | 2 +- .../codex-accounts/codex-auth-identity.ts | 2 +- .../legacy-shared-auth-migration.test.ts | 2 +- .../legacy-shared-auth-migration.ts | 2 +- .../managed-codex-auth-readiness.test.ts | 3 +- .../managed-codex-auth-readiness.ts | 2 +- .../runtime-home-mirrored-status-home.test.ts | 3 +- ...home-service-per-account-migration.test.ts | 3 +- .../runtime-home-service.test.ts | 3 +- .../codex-accounts/runtime-home-service.ts | 2 +- ...ime-home-windows-profile-ownership.test.ts | 2 +- .../codex-accounts/runtime-selection.test.ts | 3 +- src/main/codex-accounts/runtime-selection.ts | 6 +- src/main/codex-accounts/service.test.ts | 3 +- src/main/codex-accounts/service.ts | 2 +- .../codex/codex-pane-launch-account.test.ts | 3 +- src/main/codex/codex-pane-launch-account.ts | 2 +- src/main/codex/codex-session-source-home.ts | 2 +- .../codex/codex-stale-pane-accounts.test.ts | 2 +- src/main/codex/codex-stale-pane-accounts.ts | 2 +- .../daemon/daemon-create-or-attach-result.ts | 2 +- src/main/daemon/pty-subprocess.ts | 2 +- src/main/daemon/session.test.ts | 2 +- src/main/daemon/session.ts | 2 +- .../daemon/terminal-host-create-contract.ts | 2 +- src/main/daemon/terminal-host-options.ts | 2 +- src/main/daemon/terminal-host.test.ts | 2 +- src/main/daemon/types.ts | 2 +- src/main/ephemeral-vm-recipe-runner.test.ts | 2 +- src/main/ephemeral-vm-runtime-service.test.ts | 2 +- src/main/ephemeral-vm-runtime-service.ts | 2 +- src/main/folder-workspace-diff-comments.ts | 3 +- src/main/ghostty/index.test.ts | 2 +- src/main/ghostty/index.ts | 2 +- src/main/ghostty/mapper.ts | 7 +- src/main/ghostty/theme-import.test.ts | 2 +- src/main/git/push-target-validation.ts | 2 +- src/main/git/remote.ts | 2 +- src/main/git/repo.ts | 2 +- src/main/git/status.ts | 8 +- src/main/git/upstream.ts | 3 +- src/main/git/worktree-shared-directories.ts | 2 +- src/main/git/worktree.ts | 8 +- src/main/gitea/pull-request-mappers.ts | 2 +- src/main/github/client.ts | 43 +- src/main/github/comment-reactions.ts | 2 +- src/main/github/conflict-summary-cache.ts | 2 +- src/main/github/conflict-summary.ts | 2 +- src/main/github/gh-error-classification.ts | 2 +- src/main/github/github-api-repository.ts | 3 +- .../github/github-enterprise-repository.ts | 2 +- .../github/github-owner-repo-selection.ts | 2 +- .../github/github-pr-stack-async-merge.ts | 2 +- src/main/github/github-pr-stack.test.ts | 2 +- src/main/github/github-pr-stack.ts | 2 +- .../github/github-remote-identity-parsing.ts | 2 +- src/main/github/github-repository-identity.ts | 2 +- .../github-ssh-host-alias-resolution.ts | 2 +- src/main/github/issues.ts | 14 +- src/main/github/mappers.ts | 3 +- .../github/pr-refresh-coordinator.test.ts | 3 +- src/main/github/pr-refresh-coordinator.ts | 2 +- .../github/pr-refresh-error-classification.ts | 2 +- src/main/github/pr-start-point.ts | 3 +- src/main/github/project-view/mutations.ts | 4 +- src/main/github/rate-limit.ts | 2 +- src/main/github/review-head-remote.ts | 2 +- src/main/github/work-item-details.ts | 19 +- src/main/gitlab/client.ts | 12 +- src/main/gitlab/gitlab-preload-args.ts | 2 +- src/main/gitlab/gitlab-project-recents.ts | 2 +- .../gitlab/gitlab-project-ref-resolution.ts | 2 +- src/main/gitlab/glab-error-classification.ts | 2 +- src/main/gitlab/issues.ts | 6 +- src/main/gitlab/mappers.ts | 9 +- src/main/gitlab/project-ref-parser.ts | 2 +- src/main/gitlab/work-item-details.ts | 4 +- src/main/hooks-runner.test.ts | 2 +- src/main/hooks.test.ts | 2 +- src/main/hooks.ts | 10 +- src/main/index.ts | 2 +- src/main/ipc/app.ts | 3 +- src/main/ipc/browser.ts | 4 +- src/main/ipc/codex-accounts.ts | 2 +- src/main/ipc/ephemeral-vm-recipe-context.ts | 2 +- src/main/ipc/filesystem-auth.test.ts | 5 +- src/main/ipc/filesystem-auth.ts | 4 +- src/main/ipc/filesystem-search-git.ts | 2 +- .../filesystem-watcher-large-batch.test.ts | 2 +- .../filesystem-watcher-remote-batch.test.ts | 2 +- src/main/ipc/filesystem-watcher.ts | 2 +- src/main/ipc/filesystem.ts | 25 +- .../ipc/floating-workspace-directory.test.ts | 2 +- src/main/ipc/floating-workspace-directory.ts | 3 +- src/main/ipc/github-work-item-args.ts | 2 +- src/main/ipc/github.ts | 19 +- src/main/ipc/gitlab.test.ts | 2 +- src/main/ipc/gitlab.ts | 6 +- src/main/ipc/hosted-review.ts | 2 +- src/main/ipc/jira.ts | 2 +- src/main/ipc/linear.ts | 8 +- .../ipc/local-worktree-runtime-options.ts | 2 +- src/main/ipc/markdown-documents.ts | 2 +- src/main/ipc/memory.ts | 2 +- src/main/ipc/notification-options.ts | 2 +- src/main/ipc/notifications.ts | 4 +- src/main/ipc/onboarding.ts | 2 +- src/main/ipc/pet-bundle.ts | 2 +- src/main/ipc/pet.test.ts | 2 +- src/main/ipc/pet.ts | 2 +- src/main/ipc/preflight.ts | 5 +- src/main/ipc/pty-hidden-delivery-gate.ts | 2 +- src/main/ipc/pty.test.ts | 2 +- src/main/ipc/pty.ts | 3 +- .../ipc/remote-watcher-event-batch.test.ts | 2 +- src/main/ipc/remote-watcher-event-batch.ts | 2 +- .../ipc/remote-workspace-patch-queue.test.ts | 2 +- src/main/ipc/remote-workspace.test.ts | 2 +- src/main/ipc/remote-workspace.ts | 2 +- src/main/ipc/renderer-shutdown-checkpoint.ts | 3 +- .../ipc/repos-add-linked-worktree.test.ts | 2 +- src/main/ipc/repos-sparse-presets.test.ts | 2 +- src/main/ipc/repos.ts | 20 +- src/main/ipc/session.ts | 5 +- src/main/ipc/settings.ts | 3 +- .../ipc/source-control-ai-linked-issue.ts | 2 +- src/main/ipc/ssh-browse.ts | 2 +- src/main/ipc/telemetry.test.ts | 2 +- src/main/ipc/ui.ts | 2 +- .../ipc/workspace-cleanup-activity.test.ts | 3 +- src/main/ipc/workspace-cleanup-activity.ts | 3 +- src/main/ipc/workspace-cleanup-candidate.ts | 3 +- .../ipc/workspace-cleanup-disconnected-ssh.ts | 3 +- .../ipc/workspace-cleanup-git-evidence.ts | 4 +- ...orkspace-cleanup-local-git-routing.test.ts | 2 +- .../ipc/workspace-cleanup-scan-primitives.ts | 2 +- src/main/ipc/workspace-cleanup-scan.ts | 3 +- src/main/ipc/workspace-cleanup.test.ts | 12 +- ...orktree-base-directory-change-collector.ts | 2 +- .../worktree-base-directory-watch-targets.ts | 3 +- .../worktree-base-directory-watcher.test.ts | 3 +- src/main/ipc/worktree-common-git-directory.ts | 2 +- src/main/ipc/worktree-folder-rename-target.ts | 2 +- src/main/ipc/worktree-head-identity-reader.ts | 2 +- .../ipc/worktree-linked-work-item-metadata.ts | 3 +- src/main/ipc/worktree-logic.ts | 3 +- src/main/ipc/worktree-metadata-merge.ts | 3 +- .../ipc/worktree-push-target-cleanup.test.ts | 3 +- src/main/ipc/worktree-push-target-cleanup.ts | 2 +- .../ipc/worktree-push-target-setup.test.ts | 2 +- src/main/ipc/worktree-push-target-setup.ts | 2 +- src/main/ipc/worktree-remote.ts | 24 +- src/main/ipc/worktrees.test.ts | 4 +- src/main/ipc/worktrees.ts | 23 +- src/main/jira/client.ts | 2 +- src/main/jira/issues.ts | 2 +- src/main/kimi/kimi-runtime-home.test.ts | 2 +- src/main/kimi/kimi-runtime-home.ts | 2 +- src/main/linear/client.ts | 2 +- src/main/linear/issue-context-client.ts | 2 +- src/main/linear/issue-context-current.test.ts | 2 +- src/main/linear/issue-context-fanout.ts | 2 +- src/main/linear/issue-context-workspaces.ts | 2 +- src/main/linear/issue-context.test.ts | 2 +- src/main/linear/issues.ts | 7 +- src/main/linear/linear-team-pages.ts | 7 +- src/main/linear/mappers.ts | 2 +- src/main/linear/projects.ts | 16 +- src/main/linear/teams.ts | 6 +- src/main/linux-package-install-command.ts | 2 +- src/main/linux-package-install-diagnostic.ts | 2 +- .../linux-package-update-recovery.test.ts | 2 +- src/main/linux-package-update-recovery.ts | 5 +- src/main/linux-update-package-type.ts | 2 +- src/main/local-project-runtime-resolution.ts | 3 +- src/main/local-worktree-removal-recovery.ts | 3 +- src/main/memory/collector.ts | 7 +- src/main/memory/host-memory.ts | 2 +- .../memory/hydrate-local-pty-registry.test.ts | 2 +- src/main/memory/process-memory-metric.ts | 2 +- src/main/menu/register-app-menu.ts | 2 +- src/main/orca-profiles/profile-index-store.ts | 2 +- .../profile-project-presence.test.ts | 3 +- .../profile-project-session-state.ts | 2 +- .../profile-project-session-transfer.ts | 10 +- .../profile-project-source-removal.ts | 2 +- .../profile-project-state-file.test.ts | 4 +- .../profile-project-state-file.ts | 13 +- .../profile-project-transfer-payload.ts | 5 +- .../profile-project-transfer.test.ts | 4 +- .../profile-project-worktree-identity.ts | 3 +- ...tence-duplicate-repo-id-host-scope.test.ts | 3 +- ...re-interaction-broadcast.benchmark.test.ts | 2 +- src/main/persistence.test.ts | 21 +- src/main/persistence.ts | 49 +-- .../plugins/plugin-approved-vm-recipes.ts | 2 +- src/main/plugins/plugin-enablement.test.ts | 2 +- src/main/plugins/plugin-vm-recipe-registry.ts | 2 +- .../folder-workspace-path-status.test.ts | 3 +- .../folder-workspace-path-status.ts | 4 +- .../project-groups/nested-repo-discovery.ts | 2 +- .../nested-repo-import-target.test.ts | 2 +- .../nested-repo-import-target.ts | 2 +- .../project-groups/nested-repo-import.test.ts | 2 +- src/main/project-groups/nested-repo-import.ts | 6 +- src/main/project-runtime-git-options.test.ts | 3 +- src/main/project-runtime-git-options.ts | 2 +- .../providers/filesystem-provider-contract.ts | 3 +- src/main/providers/git-provider-contract.ts | 18 +- src/main/providers/pty-provider-contract.ts | 2 +- src/main/providers/pty-spawn-result.ts | 2 +- .../ssh-filesystem-provider-watch.ts | 2 +- src/main/providers/ssh-filesystem-provider.ts | 3 +- ...ssh-filesystem-watch-notifications.test.ts | 2 +- .../ssh-filesystem-watch-notifications.ts | 2 +- src/main/providers/ssh-git-provider.ts | 18 +- .../provisioned-root-ssh-adoption.test.ts | 5 +- src/main/provisioned-root-ssh-adoption.ts | 10 +- .../account-runtime-target-sync.ts | 2 +- .../rate-limits/claude-rate-limit-target.ts | 2 +- .../rate-limits/codex-rate-limit-target.ts | 2 +- .../initial-account-rate-limit-target.test.ts | 2 +- .../initial-account-rate-limit-target.ts | 2 +- ...epo-git-remote-identity-enrichment.test.ts | 2 +- .../repo-git-remote-identity-enrichment.ts | 2 +- src/main/repo-git-username-enrichment.test.ts | 2 +- src/main/repo-git-username-enrichment.ts | 2 +- src/main/repo-icon-autodetect.ts | 3 +- src/main/repo-worktrees.ts | 3 +- ...lient-session-tab-selection-persistence.ts | 2 +- .../client-session-tab-selection.test.ts | 2 +- .../runtime/client-session-tab-selection.ts | 2 +- src/main/runtime/file-watcher-host.test.ts | 2 +- src/main/runtime/file-watcher-host.ts | 2 +- .../folder-workspace-pty-identity.test.ts | 2 +- .../graph-sync-mobile-snapshot-gating.test.ts | 2 +- .../graph-sync-payload-partition.test.ts | 2 +- .../headless-tab-group-split-layout.ts | 2 +- .../headless-terminal-query-reply-policy.ts | 2 +- .../headless-terminal-split-layout.test.ts | 2 +- .../runtime/headless-terminal-split-layout.ts | 5 +- ...g-worktree-terminal-reconciliation.test.ts | 2 +- ...issing-worktree-terminal-reconciliation.ts | 2 +- ...session-terminal-persistence-retirement.ts | 3 +- .../mobile-session-terminal-retirement.ts | 4 +- ...t-navigation-isolation.integration.test.ts | 2 +- .../runtime/orca-runtime-automations.test.ts | 2 +- src/main/runtime/orca-runtime-browser.ts | 2 +- ...-runtime-emulator-folder-workspace.test.ts | 2 +- src/main/runtime/orca-runtime-emulator.ts | 2 +- .../orca-runtime-files-ssh-rearm.test.ts | 2 +- .../runtime/orca-runtime-files-watch.test.ts | 2 +- src/main/runtime/orca-runtime-files.ts | 12 +- .../orca-runtime-git-branch-diff.test.ts | 2 +- src/main/runtime/orca-runtime-git.test.ts | 2 +- src/main/runtime/orca-runtime-git.ts | 21 +- ...ntime-headless-hydration-repo-gate.test.ts | 2 +- ...linked-issue-live-meta.integration.test.ts | 2 +- ...terminal-retirement-host-partition.test.ts | 2 +- .../orca-runtime-terminal-retirement.test.ts | 2 +- ...a-runtime-terminal-split-authority.test.ts | 3 +- src/main/runtime/orca-runtime.test.ts | 17 +- src/main/runtime/orca-runtime.ts | 147 +++---- ...ration-worker-workspace-resolution.test.ts | 5 +- ...ime-request-connection.integration.test.ts | 2 +- src/main/runtime/remote-server-updater.ts | 2 +- .../runtime/repo-icon-fork-backfill.test.ts | 2 +- .../runtime/repo-worktree-resolution-scan.ts | 2 +- .../dispatcher-feature-interactions.test.ts | 2 +- .../runtime/rpc/methods/client-ui-schemas.ts | 2 +- .../runtime/rpc/methods/client-ui.test.ts | 2 +- src/main/runtime/rpc/methods/client-ui.ts | 2 +- .../methods/file-watch-event-batcher.test.ts | 2 +- .../rpc/methods/file-watch-event-batcher.ts | 2 +- src/main/runtime/rpc/methods/git.ts | 2 +- .../rpc/methods/orchestration-federation.ts | 2 +- ...orchestration-worker-launch-preferences.ts | 2 +- .../orchestration-worker-start-validation.ts | 2 +- .../methods/orchestration-worker-topology.ts | 2 +- .../rpc/methods/orchestration-workers.ts | 2 +- .../rpc/methods/session-tabs-schemas.ts | 2 +- .../rpc/methods/task-resume-state-schema.ts | 2 +- .../runtime/rpc/methods/terminal-orphan.ts | 2 +- .../terminal-quick-command-rpc-schema.ts | 2 +- .../methods/ui-state-schema-parity-checks.ts | 2 +- .../runtime/rpc/methods/worktree-schemas.ts | 2 +- .../runtime/rpc/workspace-creator-context.ts | 2 +- .../runtime/runtime-folder-workspace.test.ts | 3 +- src/main/runtime/runtime-folder-workspace.ts | 4 +- .../runtime-remove-project-host-scope.test.ts | 2 +- src/main/runtime/selected-review-branch.ts | 2 +- .../terminal-list-stale-leaf-liveness.test.ts | 2 +- .../runtime/terminal-model-query-authority.ts | 2 +- src/main/runtime/terminal-orphan-topology.ts | 2 +- .../terminal-restore-record-seed.test.ts | 2 +- .../terminal-send-stale-leaf-liveness.test.ts | 2 +- src/main/runtime/windows-drive-listing.ts | 2 +- ...workspace-session-failed-write-rollback.ts | 2 +- ...e-session-terminal-membership-authority.ts | 9 +- src/main/skills/discovery.test.ts | 2 +- src/main/skills/discovery.ts | 2 +- src/main/skills/skill-discovery-sources.ts | 2 +- .../skills/skill-discovery-target.test.ts | 2 +- src/main/skills/skill-discovery-target.ts | 2 +- .../skills/skill-freshness-inventory.test.ts | 2 +- src/main/skills/skill-freshness-inventory.ts | 2 +- .../source-control/forge-review-mappers.ts | 3 +- .../source-control/hosted-review-creation.ts | 2 +- .../pull-request-linked-issue.ts | 2 +- src/main/speech/speech-model-deletion.test.ts | 2 +- src/main/ssh/ssh-target-id-migration.test.ts | 4 +- src/main/ssh/ssh-target-id-migration.ts | 3 +- src/main/star-nag/service.test.ts | 2 +- .../startup/dev-education-suppression.test.ts | 3 +- src/main/startup/dev-education-suppression.ts | 2 +- src/main/startup/hydrate-shell-path.ts | 2 +- src/main/stats/collector.ts | 2 +- src/main/synthetic-title-frame-routing.ts | 2 +- src/main/telemetry/client-test-harness.ts | 2 +- src/main/telemetry/cohort-classifier.test.ts | 2 +- src/main/telemetry/consent.test.ts | 2 +- src/main/telemetry/consent.ts | 2 +- src/main/telemetry/install-id.test.ts | 2 +- .../onboarding-cohort-classifier.test.ts | 3 +- ...nal-scrollback-snapshot-async-migration.ts | 2 +- src/main/terminal-scrollback-snapshots.ts | 2 +- .../commit-message-text-generation.test.ts | 2 +- .../commit-message-text-generation.ts | 4 +- src/main/updater-changelog.ts | 2 +- src/main/updater-events.ts | 2 +- src/main/updater-fallback.ts | 2 +- ...ter-linux-package-recovery-actions.test.ts | 2 +- src/main/updater-mac-install.ts | 2 +- src/main/updater.fallback.test.ts | 2 +- src/main/updater.test.ts | 2 +- src/main/updater.ts | 2 +- src/main/usage-worktree-metadata.ts | 2 +- src/main/usage/usage-worktree-refs.ts | 2 +- src/main/warp-themes/parser.ts | 2 +- .../window/attach-main-window-services.ts | 9 +- .../workspace-space-analysis-capacity.test.ts | 2 +- ...orkspace-space-analysis-du-timeout.test.ts | 2 +- src/main/workspace-space-analysis.test.ts | 2 +- src/main/workspace-space-analysis.ts | 4 +- src/main/worktree-create-base-prefetch.ts | 2 +- src/main/worktree-create-timing.ts | 5 +- src/main/worktree-lineage-pruning.test.ts | 4 +- src/main/worktree-lineage-pruning.ts | 4 +- src/main/worktree-removal-authority.test.ts | 2 +- src/main/worktree-removal-safety.test.ts | 2 +- src/main/worktree-removal-safety.ts | 4 +- ...-removal-session-partition-fencing.test.ts | 2 +- src/main/worktree-root-preparation.test.ts | 2 +- src/main/worktree-root-preparation.ts | 3 +- src/main/worktree-trash.test.ts | 2 +- src/main/worktree-trash.ts | 3 +- src/preload/api/agent-account-api.ts | 5 +- src/preload/api/agent-hook-api.ts | 3 +- src/preload/api/app-api.ts | 10 +- src/preload/api/browser-api.ts | 4 +- src/preload/api/ephemeral-vm-api.ts | 2 +- src/preload/api/filesystem-api.ts | 7 +- src/preload/api/git-inspection-api.ts | 8 +- src/preload/api/git-operation-api.ts | 9 +- src/preload/api/github-pull-request-api.ts | 27 +- src/preload/api/github-work-item-api.ts | 11 +- src/preload/api/gitlab-api.ts | 4 +- src/preload/api/jira-api.ts | 2 +- src/preload/api/linear-api.ts | 17 +- src/preload/api/onboarding-api.ts | 2 +- src/preload/api/os-permission-api.ts | 6 +- src/preload/api/pet-api.ts | 2 +- src/preload/api/preflight-api.ts | 5 +- src/preload/api/pty-api.ts | 2 +- src/preload/api/repository-api.ts | 14 +- src/preload/api/settings-api.ts | 2 +- src/preload/api/ssh-api.ts | 2 +- src/preload/api/telemetry-api.ts | 2 +- src/preload/api/ui-command-event-api.ts | 6 +- src/preload/api/updater-api.ts | 2 +- src/preload/api/workspace-session-api.ts | 5 +- src/preload/api/worktree-api.ts | 25 +- src/preload/index.ts | 73 ++-- src/preload/renderer-restart-wiring.ts | 2 +- src/preload/ssh-authority-forwarding.test.ts | 2 +- src/relay/fs-handler-utils.ts | 2 +- src/relay/git-handler-push-target.ts | 2 +- src/relay/git-handler-status-ops.ts | 2 +- src/relay/git-handler-worktree-remove.ts | 2 +- src/relay/git-handler.ts | 2 +- .../git-status-upstream-negative-cache.ts | 2 +- src/relay/pty-handler.ts | 2 +- src/renderer/src/App.tsx | 3 +- .../src/components/GitHubItemDialog.tsx | 22 +- .../src/components/GitLabItemDialog.tsx | 4 +- .../src/components/JiraIssueWorkspace.tsx | 2 +- src/renderer/src/components/Landing.tsx | 2 +- .../src/components/LinearIssueTextEditor.tsx | 2 +- .../src/components/LinearIssueWorkspace.tsx | 6 +- .../src/components/LinearItemDrawer.tsx | 2 +- .../LinuxPackageInstallRecoveryCard.test.tsx | 2 +- .../LinuxPackageInstallRecoveryCard.tsx | 2 +- .../components/NewWorkspaceComposerCard.tsx | 17 +- .../components/NewWorkspaceComposerModal.tsx | 10 +- .../src/components/PullRequestPage.tsx | 16 +- src/renderer/src/components/TaskPage.tsx | 42 +- src/renderer/src/components/Terminal.tsx | 4 +- .../components/UpdateCard.error-card.test.tsx | 2 +- .../src/components/UpdateCard.test.ts | 2 +- src/renderer/src/components/UpdateCard.tsx | 2 +- .../WorktreeJumpPalette.linear-url.test.tsx | 4 +- .../src/components/WorktreeJumpPalette.tsx | 7 +- .../activity/ActivityPrototypePage.test.ts | 4 +- .../activity/ActivityPrototypePage.tsx | 4 +- .../AgentSessionContinuationDialog.tsx | 2 +- .../agent-session-continuation-selection.ts | 2 +- .../components/agent/AgentCombobox.test.tsx | 2 +- .../src/components/agent/AgentCombobox.tsx | 2 +- .../agent/AgentSettingsDialog.test.tsx | 2 +- .../automations/AutomationEditorDialog.tsx | 16 +- .../AutomationEditorDialogFooter.tsx | 12 +- .../automations/AutomationListLocalRows.tsx | 4 +- .../automations/AutomationProjectCombobox.tsx | 2 +- .../automations/AutomationRunHistory.tsx | 2 +- .../AutomationSetupDecisionField.tsx | 4 +- .../automations/AutomationsDetailPane.tsx | 2 +- .../automations/AutomationsListPanel.tsx | 4 +- .../automations/AutomationsPage.tsx | 3 +- .../automations/CreateFromPicker.test.tsx | 2 +- .../automations/CreateFromPicker.tsx | 3 +- .../automations/WorkspaceCombobox.tsx | 2 +- .../automations/automation-draft-model.ts | 2 +- .../automations/automation-host-client.ts | 2 +- .../automation-project-groups.test.ts | 2 +- .../automations/automation-project-groups.ts | 2 +- .../automation-run-context.test.ts | 3 +- .../automations/automation-run-context.ts | 3 +- .../automations/automation-run-open-target.ts | 5 +- .../automation-run-workspace-display.ts | 2 +- .../automation-setup-decision.test.ts | 3 +- .../automations/automation-setup-decision.ts | 11 +- .../automation-target-availability.test.ts | 4 +- .../automation-target-availability.ts | 4 +- .../automations/automation-templates.ts | 2 +- .../automations/use-automation-list-search.ts | 2 +- .../browser-pane/BrowserAddressBar.test.tsx | 2 +- .../components/browser-pane/BrowserPane.tsx | 2 +- .../BrowserPaneOverlayLayer.test.tsx | 3 +- .../browser-pane/BrowserPaneOverlayLayer.tsx | 3 +- .../browser-pane/BrowserToolbarMenu.tsx | 2 +- .../browser-address-bar-suggestions.test.ts | 2 +- .../browser-address-bar-suggestions.ts | 2 +- .../browser-guest-worktree-retention.test.ts | 2 +- .../browser-guest-worktree-retention.ts | 2 +- .../browser-load-failure-overlay.tsx | 2 +- .../browser-pane/browser-notices.ts | 2 +- .../browser-pane-page-selection.test.ts | 2 +- .../browser-pane-page-selection.ts | 2 +- .../browser-toolbar-menu-dropdown.tsx | 5 +- .../cmd-j/PaletteCreateWorktreeRow.tsx | 2 +- .../cmd-j/palette-filter-options.test.ts | 4 +- .../cmd-j/palette-filter-options.ts | 4 +- .../src/components/cmd-j/palette-filter.ts | 2 +- .../components/cmd-j/palette-host-badge.ts | 2 +- .../cmd-j/palette-live-status.test.tsx | 2 +- .../components/cmd-j/palette-live-status.tsx | 4 +- .../cmd-j/palette-project-results.ts | 4 +- .../components/cmd-j/palette-results.test.ts | 4 +- .../cmd-j/quick-action-context.test.ts | 2 +- .../components/cmd-j/quick-action-context.ts | 2 +- .../worktree-checks-review-index.test.ts | 4 +- .../cmd-j/worktree-checks-review-index.ts | 3 +- .../confirmation-skip-preference.ts | 2 +- .../crash-report/CrashReportDialogSurface.tsx | 2 +- .../components/dashboard-popout/AgentMap.tsx | 2 +- .../dashboard-popout/AgentMapCanvas.tsx | 2 +- .../dashboard-popout/AgentMapScene.tsx | 2 +- .../AgentMapSnapshotWorkspaceMenu.tsx | 2 +- .../AgentMapWorkspaceContextMenu.test.tsx | 4 +- .../AgentMapWorktreeRingNode.tsx | 2 +- .../agent-map-render-test-harness.tsx | 2 +- .../preview-terminal-compatibility.ts | 2 +- .../preview-terminal-ligatures.ts | 2 +- .../preview-terminal-options.test.ts | 2 +- .../preview-terminal-options.ts | 2 +- .../useAgentMapContextMenus.tsx | 2 +- .../dashboard/AgentDashboardSettingsMenu.tsx | 2 +- .../dashboard/DashboardAgentRow.test.tsx | 2 +- .../dashboard/agent-row-lineage-model.test.ts | 2 +- ...ashboard-snapshot-folder-workspace.test.ts | 4 +- ...ard-snapshot-orchestration-routing.test.ts | 3 +- .../build-dashboard-snapshot.test.ts | 3 +- .../dashboard/dashboard-card-context.test.ts | 4 +- .../dashboard/dashboard-card-context.ts | 3 +- .../dashboard-card-terminal-input.ts | 2 +- .../dashboard-worktree-launch-options.test.ts | 3 +- .../dashboard-worktree-launch-options.ts | 2 +- .../components/dashboard/useDashboardData.ts | 2 +- .../useLiveDashboardSnapshot.test.ts | 5 +- .../dashboard/useRetainedAgents.test.ts | 4 +- .../components/dashboard/useRetainedAgents.ts | 5 +- .../dashboard/useRetainedAgentsSync.test.ts | 5 +- .../use-hold-dictation-gesture.test.tsx | 2 +- .../dictation/use-hold-dictation-gesture.ts | 2 +- .../diff-comments/useDiffCommentDecorator.tsx | 2 +- .../src/components/editor/ChangesModeView.tsx | 3 +- .../components/editor/CheckRunAnnotations.tsx | 2 +- .../editor/CheckRunDetailsPanel.tsx | 2 +- .../src/components/editor/CheckRunJobs.tsx | 2 +- .../editor/CombinedDiffFileTree.test.ts | 3 +- .../editor/CombinedDiffFileTree.tsx | 3 +- .../components/editor/CombinedDiffViewer.tsx | 9 +- .../components/editor/ConflictComponents.tsx | 2 +- .../editor/ConflictReviewFileTree.tsx | 2 +- .../components/editor/DiffNotesSendMenu.tsx | 2 +- .../src/components/editor/DiffSectionItem.tsx | 2 +- .../src/components/editor/DiffViewer.tsx | 2 +- .../src/components/editor/EditorContent.tsx | 3 +- .../components/editor/MarkdownPreview.test.ts | 2 +- .../src/components/editor/MarkdownPreview.tsx | 4 +- .../src/components/editor/MonacoEditor.tsx | 3 +- .../ReviewNotesSendMenuContent.test.tsx | 2 +- .../components/editor/RichMarkdownEditor.tsx | 2 +- .../editor/RichMarkdownEditorSurface.tsx | 2 +- .../editor/RichMarkdownReviewNoteLayer.tsx | 2 +- .../editor/check-annotation-path.test.ts | 2 +- .../editor/check-annotation-path.ts | 2 +- .../editor/check-job-step-status.ts | 2 +- .../editor/check-run-details-fix-context.ts | 3 +- .../check-run-details-fix-with-ai.test.ts | 11 +- .../editor/check-run-details-fix-with-ai.ts | 2 +- .../editor/check-run-details-tab.ts | 7 +- .../editor/combined-diff-entries.test.ts | 3 +- .../editor/combined-diff-entries.ts | 3 +- .../editor/combined-diff-file-tree-model.ts | 3 +- .../editor/combined-diff-file-tree-row.tsx | 4 +- .../combined-diff-section-cache-match.test.ts | 3 +- .../combined-diff-section-cache-match.ts | 3 +- .../combined-diff-section-connection.test.ts | 4 +- .../combined-diff-section-load-state.test.ts | 2 +- .../editor/diff-section-layout.test.ts | 2 +- .../components/editor/diff-section-layout.ts | 2 +- .../components/editor/diff-section-types.ts | 3 +- .../editor/editor-cmd-save-target.test.ts | 2 +- .../editor/editor-cmd-save-target.ts | 3 +- .../editor-external-watch-path-index.ts | 2 +- .../src/components/editor/editor-header.ts | 3 +- .../editor/editor-panel-content-types.ts | 2 +- .../editor/editor-panel-diff-reload.test.ts | 2 +- .../editor/editor-panel-diff-reload.ts | 2 +- .../editor-panel-git-entry-selector.test.ts | 3 +- .../editor/editor-panel-render-model.test.ts | 2 +- .../editor/large-diff-section-content.ts | 2 +- .../editor/markdown-doc-completions.test.ts | 2 +- .../editor/markdown-doc-completions.ts | 2 +- .../editor/markdown-doc-links.test.ts | 2 +- .../components/editor/markdown-doc-links.ts | 2 +- .../markdown-document-list-request.test.ts | 2 +- .../editor/markdown-document-list-request.ts | 2 +- ...wn-document-worktree-path-selector.test.ts | 2 +- .../editor/monaco-markdown-doc-completions.ts | 2 +- ...h-markdown-annotation-submit-highlights.ts | 2 +- .../editor/rich-markdown-commands.tsx | 2 +- .../editor/rich-markdown-doc-link.ts | 2 +- .../rich-markdown-editor-click-routing.ts | 2 +- .../editor/rich-markdown-editor-config.ts | 2 +- .../editor/rich-markdown-editor-props.ts | 2 +- ...ich-markdown-html-superscript-link.test.ts | 2 +- .../rich-markdown-review-annotations.test.ts | 2 +- .../rich-markdown-review-annotations.ts | 2 +- .../rich-markdown-review-note-layout.test.ts | 2 +- .../rich-markdown-review-note-layout.ts | 2 +- .../rich-markdown-review-note-positioning.ts | 2 +- .../useEditorPanelContentState.test.tsx | 2 +- ...EditorPanelVisibilityContentState.test.tsx | 2 +- .../components/editor/useMarkdownDocuments.ts | 2 +- .../editor/useRichMarkdownMenuController.ts | 2 +- .../editor/useRichMarkdownProgrammaticSync.ts | 2 +- .../editor/useRichMarkdownReviewController.ts | 2 +- .../editor/useRichMarkdownReviewData.ts | 2 +- .../useRichMarkdownReviewEditorEffects.ts | 2 +- .../useRichMarkdownReviewRailController.ts | 2 +- .../components/emulator-pane/EmulatorPane.tsx | 2 +- .../EmulatorPaneOverlayLayer.tsx | 2 +- .../feature-tip-modal-state.test.ts | 2 +- .../feature-tips/feature-tip-modal-state.ts | 2 +- .../feature-tip-startup-gate.test.ts | 3 +- .../feature-tips/feature-tip-startup-gate.ts | 3 +- .../feature-wall/AiCommitPrSettingsFields.tsx | 2 +- .../feature-wall/FeatureWallBody.tsx | 2 +- .../FeatureWallSetupChecklist.tsx | 2 +- .../FeatureWallSetupWorkflowActions.test.tsx | 2 +- .../FeatureWallSetupWorkflowActions.tsx | 4 +- .../feature-wall/FeatureWallTourPanel.tsx | 2 +- .../components/feature-wall/KeepAwakeCard.tsx | 2 +- .../UsageAccountsCard.tsx | 2 +- .../ai-commit-pr-settings-helpers.ts | 4 +- .../feature-wall-setup-progress.test.ts | 2 +- .../feature-wall-setup-progress.ts | 3 +- .../feature-wall/useAiCommitPrSettings.ts | 3 +- .../FloatingBrowserSlot.test.tsx | 2 +- .../floating-terminal/FloatingBrowserSlot.tsx | 2 +- .../FloatingTerminalIconContextMenu.tsx | 2 +- .../FloatingTerminalPanel.test.tsx | 4 +- .../FloatingTerminalPanel.tsx | 4 +- .../github-checks-tab-state.test.ts | 2 +- .../src/components/github-checks-tab-state.ts | 2 +- .../src/components/github-pr-merge-state.ts | 6 +- .../github-pr-reviewer-display.test.ts | 2 +- .../components/github-pr-reviewer-display.ts | 3 +- .../components/github-project/ProjectCell.tsx | 2 +- .../github-project/ProjectItemSlugDialog.tsx | 2 +- .../components/github-project/ProjectRow.tsx | 2 +- .../github-project/ProjectViewList.tsx | 2 +- .../github-project/ProjectViewWrapper.tsx | 2 +- .../project-row-filtering.test.ts | 2 +- .../github-project/project-row-filtering.ts | 2 +- .../github-project/project-work-item.ts | 2 +- .../slug-dialog/AssigneesEditor.tsx | 2 +- .../github-project/slug-dialog/Comments.tsx | 3 +- .../slug-dialog/LabelsEditor.tsx | 2 +- .../slug-dialog/SlugDialogBody.tsx | 3 +- .../components/github/CommentCodeContext.tsx | 6 +- .../components/github/CommentReactions.tsx | 2 +- .../github/GitHubMarkdownComposer.tsx | 2 +- .../github/IssueSourceIndicator.tsx | 2 +- .../components/github/IssueSourceSelector.tsx | 3 +- .../components/github/PRAssigneesPanel.tsx | 3 +- .../components/github/PRFilterDropdowns.tsx | 6 +- .../github-markdown-composer-preview-pane.tsx | 2 +- ...ithub-pr-reviewer-candidate-filter.test.ts | 2 +- .../github-pr-reviewer-candidate-filter.ts | 2 +- .../github/github-rate-limit-display.tsx | 5 +- .../github-work-item-assignee-filter.ts | 2 +- .../github-work-item-comment-mutations.ts | 2 +- .../github/github-work-item-edit-mutations.ts | 3 +- .../github/github-work-item-identity.ts | 3 +- .../github/pr-check-presentation.ts | 2 +- .../github/pr-file-content-size.test.ts | 2 +- .../components/github/pr-file-content-size.ts | 5 +- .../components/github/pr-file-diff-mapping.ts | 9 +- .../github/work-item-state-presentation.tsx | 3 +- .../gitlab/gitlab-rate-limit-display.tsx | 5 +- .../src/components/jira-issue-sorter.ts | 2 +- .../jira-project-picker-filter.test.ts | 2 +- .../components/jira-project-picker-filter.ts | 2 +- .../landing-preflight-dismissal.test.ts | 2 +- .../components/landing-preflight-dismissal.ts | 2 +- .../landing-preflight-issues.test.ts | 2 +- .../components/landing-preflight-issues.ts | 2 +- ...landing-preflight-runtime-boundary.test.ts | 2 +- .../src/components/linear-api-key-dialog.tsx | 2 +- ...-issue-attribute-filter-dropdowns.test.tsx | 2 +- ...inear-issue-attribute-filter-dropdowns.tsx | 2 +- ...ssue-attribute-filter-primary-team.test.ts | 2 +- ...ear-issue-attribute-filter-primary-team.ts | 2 +- ...ar-issue-attribute-filter-team-ids.test.ts | 2 +- .../linear-issue-attribute-filter-team-ids.ts | 2 +- .../linear-issue-text-draft-state.ts | 2 +- .../components/linear-issue-text-save-plan.ts | 2 +- .../linear-issue-workspace-text.test.ts | 2 +- .../components/linear-issue-workspace-text.ts | 2 +- .../linear-project-view-surfaces.tsx | 6 +- .../components/linear-scope-selector.test.ts | 2 +- .../src/components/linear-scope-selector.tsx | 6 +- .../claude-model-switch-confirmation.ts | 2 +- .../native-chat-attachment-upload.test.ts | 2 +- .../native-chat/native-chat-availability.ts | 3 +- .../native-chat/native-chat-file-link.test.ts | 2 +- .../native-chat/native-chat-file-link.ts | 2 +- .../native-chat-launch-session-options.ts | 3 +- .../native-chat/native-chat-leaf-routing.ts | 6 +- .../native-chat-pane-resolution.test.ts | 2 +- .../native-chat-pane-resolution.ts | 2 +- .../native-chat-runtime-owner.test.ts | 2 +- .../native-chat/native-chat-view-types.ts | 2 +- .../use-native-chat-toggle-shortcut.ts | 2 +- ...spaceNameField.jira-accessibility.test.tsx | 2 +- .../new-workspace/SmartWorkspaceNameField.tsx | 13 +- .../new-workspace/run-target-options.ts | 2 +- .../use-jira-source-connection.test.tsx | 2 +- .../use-jira-source-connection.ts | 2 +- .../use-jira-url-source.test.tsx | 2 +- .../new-workspace/use-jira-url-source.ts | 2 +- .../use-recent-project-ids.test.ts | 2 +- .../new-workspace/use-recent-project-ids.ts | 2 +- .../components/notification-sound-options.ts | 2 +- .../mac-notification-permission-card.tsx | 2 +- .../src/components/onboarding/AgentStep.tsx | 2 +- .../onboarding/GhosttyDiscoveryRow.tsx | 2 +- .../onboarding/NotificationStep.test.tsx | 2 +- .../onboarding/NotificationStep.tsx | 2 +- .../components/onboarding/OnboardingFlow.tsx | 2 +- .../src/components/onboarding/ThemeStep.tsx | 7 +- .../onboarding/WindowsTerminalStep.test.tsx | 2 +- .../onboarding/WindowsTerminalStep.tsx | 2 +- .../onboarding/agent-picked-payload.ts | 6 +- .../onboarding-settings-hydration.test.ts | 2 +- .../onboarding-settings-hydration.ts | 3 +- .../onboarding/should-show-onboarding.ts | 2 +- .../onboarding/show-onboarding-event.ts | 2 +- .../onboarding/theme-chrome-preview.tsx | 2 +- .../use-onboarding-flow-persistence.test.ts | 2 +- .../use-onboarding-flow-persistence.ts | 4 +- .../onboarding/use-onboarding-flow.ts | 4 +- .../windows-terminal-onboarding-telemetry.ts | 2 +- .../src/components/pet/PetOverlay.tsx | 2 +- .../pet/pet-overlay-hit-area.test.tsx | 2 +- src/renderer/src/components/pet/usePetUrl.ts | 2 +- .../src/components/pr-check-counts.test.ts | 2 +- .../src/components/pr-check-counts.ts | 2 +- .../components/pr-checks-fix-prompt.test.ts | 2 +- .../pr-comments-resolution-prompt.test.ts | 2 +- .../pr-comments-resolution-prompt.ts | 2 +- ...ovider-check-classification-parity.test.ts | 3 +- .../quick-open-file-list.react.test.tsx | 3 +- .../components/quick-open-file-list.test.ts | 2 +- .../src/components/quick-open-file-list.ts | 2 +- .../repo/NestedRepoChecklist.test.tsx | 2 +- .../components/repo/NestedRepoChecklist.tsx | 2 +- .../repo/NestedRepoScanLimitNotice.test.ts | 2 +- .../repo/NestedRepoScanLimitNotice.tsx | 2 +- .../src/components/repo/RepoCombobox.tsx | 2 +- .../components/repo/repo-fork-indicator.tsx | 2 +- .../components/right-sidebar/ChecksPanel.tsx | 14 +- .../components/right-sidebar/FileExplorer.tsx | 2 +- .../right-sidebar/FileExplorerQueryStrip.tsx | 2 +- .../right-sidebar/FileExplorerRow.tsx | 2 +- .../right-sidebar/FileExplorerViewSwitch.tsx | 2 +- .../right-sidebar/FileExplorerVirtualRows.tsx | 2 +- .../FolderWorkspacePrChecksPanel.test.tsx | 12 +- .../FolderWorkspacePrChecksPanel.tsx | 2 +- .../FolderWorkspacePrChecksRow.test.tsx | 4 +- .../FolderWorkspacePrChecksRow.tsx | 2 +- .../FolderWorkspaceWorktreesPanel.test.tsx | 4 +- .../FolderWorkspaceWorktreesPanel.tsx | 2 +- .../right-sidebar/GitHistoryCommitFiles.tsx | 3 +- .../right-sidebar/GitHistoryPanel.tsx | 2 +- .../right-sidebar/GitHubPRStackMap.test.tsx | 2 +- .../right-sidebar/GitHubPRStackMap.tsx | 5 +- .../right-sidebar/HostedReviewActions.tsx | 4 +- .../right-sidebar/SearchResultItems.test.tsx | 2 +- .../right-sidebar/SearchResultItems.tsx | 2 +- .../right-sidebar/SearchResultsPane.tsx | 6 +- .../SourceControl.branch-line-total.test.tsx | 2 +- .../SourceControl.compare-summary.test.ts | 3 +- ...SourceControl.open-file-highlight.test.tsx | 2 +- .../SourceControl.preview-open.test.tsx | 6 +- .../right-sidebar/SourceControl.tsx | 16 +- .../SourceControl.virtual-file-list.test.tsx | 2 +- .../SourceControlAgentActionDialog.test.tsx | 4 +- .../SourceControlAgentActionDialog.tsx | 2 +- ...ourceControlAgentActionDialogForm.test.tsx | 3 +- .../SourceControlAgentActionDialogForm.tsx | 4 +- .../SourceControlTextGenerationDialog.tsx | 3 +- .../SourceControlTextGenerationDialogForm.tsx | 4 +- .../SourceControlTextGenerationParams.ts | 2 +- .../active-checks-status.test.ts | 2 +- .../right-sidebar/active-checks-status.ts | 2 +- .../right-sidebar/activity-bar-buttons.tsx | 2 +- .../right-sidebar/ai-vault-original-pane.ts | 5 +- .../ai-vault-scope-paths.test.ts | 2 +- .../right-sidebar/ai-vault-scope-paths.ts | 3 +- .../ai-vault-session-launch-actions.ts | 2 +- .../ai-vault-session-projects.test.ts | 4 +- .../ai-vault-session-projects.ts | 4 +- .../ai-vault-session-resume.test.ts | 3 +- .../right-sidebar/ai-vault-session-resume.ts | 3 +- .../ai-vault-session-worktree-map.test.tsx | 3 +- .../ai-vault-session-worktree.test.ts | 3 +- .../ai-vault-session-worktree.ts | 3 +- .../buildSourceControlAgentDeliveryPlan.ts | 2 +- .../checks-list-expanded-details.test.tsx | 2 +- .../checks-panel-async-result-key.ts | 2 +- .../checks-panel-content.test.tsx | 4 +- .../right-sidebar/checks-panel-content.tsx | 10 +- .../checks-panel-git-status-snapshot.ts | 3 +- .../checks-panel-polling.test.ts | 2 +- .../right-sidebar/checks-panel-polling.ts | 2 +- .../checks-panel-pr-refresh-request.ts | 2 +- .../right-sidebar/checks-panel-review-copy.ts | 2 +- .../checks-panel-review-creation.ts | 2 +- .../checks-panel-review-state-model.ts | 5 +- .../right-sidebar/checks-panel-review.test.ts | 2 +- .../right-sidebar/checks-panel-review.ts | 2 +- .../checks-panel-terminal-worktree.test.ts | 2 +- .../checks-panel-terminal-worktree.ts | 3 +- .../discard-all-sequence.test.ts | 2 +- .../right-sidebar/discard-all-sequence.ts | 2 +- .../file-explorer-add-project-action.test.ts | 2 +- .../file-explorer-add-project-action.ts | 2 +- .../file-explorer-directory-listing.ts | 2 +- .../right-sidebar/file-explorer-entries.ts | 2 +- ...ile-explorer-expanded-dirs-refresh.test.ts | 2 +- ...file-explorer-operation-generation.test.ts | 2 +- .../right-sidebar/file-explorer-reset.ts | 2 +- .../file-explorer-runtime-owner.ts | 2 +- .../file-explorer-watch-drive-root.test.ts | 2 +- .../file-explorer-watch-reconcile.ts | 2 +- ...older-workspace-attached-worktrees.test.ts | 9 +- .../folder-workspace-attached-worktrees.ts | 9 +- .../fork-push-target-label.test.ts | 2 +- .../right-sidebar/fork-push-target-label.ts | 2 +- .../git-status-file-watch-refresh.ts | 4 +- ...t-status-refresh-branch-line-total.test.ts | 2 +- .../right-sidebar/git-status-refresh.test.ts | 2 +- .../right-sidebar/git-status-refresh.ts | 9 +- .../github-pr-stack-confirmation.ts | 5 +- .../github-pr-stack-merge.test.ts | 2 +- .../right-sidebar/github-pr-stack-merge.ts | 5 +- .../github-refresh-error-copy.ts | 2 +- .../hosted-review-github-actions.ts | 3 +- .../parent-pr-checks-github-pr-cache.ts | 4 +- .../parent-pr-checks-hosted-review-cache.ts | 3 +- ...rent-pr-checks-projection-selector.test.ts | 3 +- .../parent-pr-checks-refresh.test.ts | 3 +- .../right-sidebar/parent-pr-checks-refresh.ts | 10 +- .../parent-pr-checks-row-status.ts | 2 +- .../parent-pr-checks-row-types.ts | 10 +- .../parent-pr-checks-rows.test.ts | 5 +- .../right-sidebar/parent-pr-checks-rows.ts | 4 +- .../pr-comment-fixing-reply-body.ts | 2 +- .../pr-comment-thread-resolution.test.ts | 2 +- .../pr-comment-thread-resolution.ts | 2 +- .../pr-comments-ai-launch-ack.test.ts | 2 +- .../pr-comments-ai-launch-ack.ts | 3 +- .../pr-comments-list-selection.test.tsx | 2 +- .../pr-comments-list-selection.ts | 2 +- ...push-target-upstream-refresh-cache.test.ts | 3 +- .../push-target-upstream-refresh-cache.ts | 9 +- .../runSourceControlAgentActionStart.ts | 4 +- .../right-sidebar/search-match-open.ts | 2 +- .../components/right-sidebar/search-rows.ts | 6 +- .../source-control-action-error.ts | 2 +- ...source-control-action-recipe-match.test.ts | 3 +- .../source-control-action-recipe-match.ts | 3 +- .../right-sidebar/source-control-actions.ts | 2 +- ...urce-control-agent-action-dialog-result.ts | 2 +- ...rce-control-agent-action-dialog-support.ts | 2 +- .../source-control-ai-controller-types.ts | 9 +- .../source-control-ai-settings-navigation.ts | 2 +- ...source-control-branch-context-row.test.tsx | 2 +- .../source-control-branch-context-row.tsx | 4 +- ...ource-control-branch-context-stats.test.ts | 2 +- .../source-control-branch-context-stats.ts | 3 +- ...urce-control-create-pr-intent-flow.test.ts | 2 +- .../source-control-create-pr-intent-flow.ts | 2 +- ...ource-control-discard-confirmation.test.ts | 2 +- .../source-control-discard-confirmation.ts | 2 +- .../source-control-discard-dialog.test.tsx | 2 +- .../source-control-discard-dialog.tsx | 2 +- .../source-control-dropdown-items.ts | 2 +- .../source-control-entry-actions.test.ts | 2 +- .../source-control-entry-actions.ts | 2 +- .../source-control-fix-split-button.tsx | 2 +- .../source-control-header-overflow-menu.tsx | 2 +- ...e-control-header-toolbar-identity.test.tsx | 2 +- .../source-control-header-toolbar.test.ts | 2 +- .../source-control-header-toolbar.tsx | 9 +- ...ource-control-hosted-review-push-target.ts | 3 +- .../source-control-manual-review-url.ts | 2 +- .../source-control-primary-action-types.ts | 3 +- .../source-control-section-order.test.ts | 2 +- .../source-control-section-order.ts | 3 +- .../source-control-status-sort.test.ts | 2 +- .../source-control-status-sort.ts | 2 +- ...source-control-submodule-expansion.test.ts | 2 +- .../source-control-submodule-expansion.ts | 2 +- ...e-control-text-generation-defaults.test.ts | 3 +- ...source-control-text-generation-defaults.ts | 3 +- .../right-sidebar/source-control-tree.test.ts | 3 +- .../right-sidebar/source-control-tree.ts | 2 +- .../stale-conflict-operation-poll.ts | 3 +- .../right-sidebar/status-display.ts | 2 +- ...use-checks-panel-terminal-worktree.test.ts | 3 +- .../use-checks-panel-terminal-worktree.ts | 2 +- .../use-git-status-upstream-ref-watch.ts | 2 +- .../use-hosted-review-actions.test.tsx | 3 +- .../use-hosted-review-actions.ts | 3 +- .../use-source-control-recovery-ai.ts | 2 +- .../useCreatePullRequestDialogFields.ts | 2 +- .../useFileExplorerTree.stale-dirs.test.tsx | 2 +- ...FileExplorerWatch.pending-refresh.test.tsx | 2 +- .../right-sidebar/useFileExplorerWatch.ts | 2 +- .../right-sidebar/useFileSearchPanel.ts | 6 +- .../useFileSearchRunner.test.tsx | 2 +- .../right-sidebar/useFileSearchRunner.ts | 2 +- .../useGitHistoryCommitActions.ts | 5 +- .../useGitStatusPolling.rerender.test.ts | 3 +- .../right-sidebar/useGitStatusPolling.test.ts | 4 +- ...eSavedSourceControlAgentActionAutoStart.ts | 4 +- .../useSourceControlAgentActionDialog.ts | 2 +- .../useSourceControlAgentActionStart.ts | 4 +- .../useSourceControlSelection.test.ts | 2 +- .../useSourceControlSelection.ts | 2 +- .../useSourceControlSubmoduleStatus.test.tsx | 2 +- .../useSourceControlSubmoduleStatus.ts | 2 +- .../components/settings/AccountsPane.test.tsx | 2 +- .../src/components/settings/AccountsPane.tsx | 6 +- .../AdvancedNetworkSettingsSection.test.ts | 2 +- .../AdvancedNetworkSettingsSection.tsx | 2 +- .../src/components/settings/AdvancedPane.tsx | 2 +- .../components/settings/AgentAwakeSetting.tsx | 2 +- .../settings/AgentCacheTimerSection.tsx | 2 +- .../AgentDashboardExperimentalSetting.tsx | 2 +- .../settings/AgentRuntimeSetting.tsx | 2 +- .../components/settings/AgentsPane.test.tsx | 3 +- .../src/components/settings/AgentsPane.tsx | 3 +- .../settings/AppearanceInterfaceSection.tsx | 2 +- .../settings/AppearancePane.test.tsx | 3 +- .../components/settings/AppearancePane.tsx | 2 +- .../AppearanceWindowSidebarSection.tsx | 3 +- .../settings/ArtifactsSettingsPane.tsx | 2 +- .../AutoRenameBranchFromWorkSetting.tsx | 2 +- .../settings/AutomationsSettingsPane.tsx | 2 +- .../BrowserLinkRoutingModifierSetting.tsx | 2 +- .../settings/BrowserLinkRoutingSetting.tsx | 2 +- .../BrowserLocalhostWorktreeLabelsSetting.tsx | 2 +- .../src/components/settings/BrowserPane.tsx | 2 +- .../components/settings/BrowserProfileRow.tsx | 5 +- .../settings/BrowserSessionCookiesSection.tsx | 2 +- .../BrowserTerminalLinkActionsSetting.tsx | 2 +- .../src/components/settings/CliSection.tsx | 2 +- .../settings/CliSkillRuntimeSetup.tsx | 2 +- .../settings/CommitMessageAiPane.test.tsx | 2 +- .../settings/CommitMessageAiPane.tsx | 3 +- .../CompareAgainstUpstreamSetting.tsx | 2 +- .../DefaultWindowsProjectRuntimeSetting.tsx | 2 +- .../settings/EditorFontFamilySetting.tsx | 2 +- .../settings/EditorWordWrapSetting.tsx | 2 +- .../settings/EphemeralVmRecipeRow.tsx | 2 +- .../EphemeralVmsExperimentalSetting.tsx | 2 +- .../settings/ExperimentalPane.test.tsx | 2 +- .../components/settings/ExperimentalPane.tsx | 2 +- .../settings/FloatingWorkspacePane.tsx | 3 +- .../settings/GeneralEditorSettingsSection.tsx | 2 +- .../src/components/settings/GeneralPane.tsx | 2 +- .../GeneralWorkspaceSettingsSection.tsx | 2 +- .../settings/GhosttyImportModal.test.ts | 2 +- .../settings/GhosttyImportModal.tsx | 2 +- .../src/components/settings/GitPane.test.tsx | 2 +- .../src/components/settings/GitPane.tsx | 3 +- .../src/components/settings/InputPane.tsx | 2 +- .../settings/LeftSidebarAppearanceSetting.tsx | 3 +- .../components/settings/McpConfigSection.tsx | 3 +- .../settings/MobileEmulatorSettingsPane.tsx | 2 +- .../NativeChatExperimentalSetting.tsx | 2 +- .../settings/NotificationSoundSection.tsx | 2 +- .../settings/NotificationsPane.test.tsx | 3 +- .../components/settings/NotificationsPane.tsx | 2 +- .../components/settings/OpenInMenuSetting.tsx | 3 +- .../PluginsSettingsSection.lifecycle.test.tsx | 2 +- .../settings/PluginsSettingsSection.tsx | 2 +- .../components/settings/PrivacyPane.test.ts | 2 +- .../src/components/settings/PrivacyPane.tsx | 2 +- .../ProjectWindowsRuntimeSetting.test.tsx | 2 +- .../settings/ProjectWindowsRuntimeSetting.tsx | 3 +- .../components/settings/QuickCommandsList.tsx | 4 +- .../components/settings/QuickCommandsPane.tsx | 3 +- .../settings/QuickCommandsScopeFilter.tsx | 2 +- .../settings/QuickCommandsToolbar.tsx | 2 +- .../settings/RecentTabOrderControl.tsx | 2 +- .../settings/RepositoryForkSyncSection.tsx | 3 +- .../settings/RepositoryHooksSection.test.ts | 2 +- .../settings/RepositoryHooksSection.tsx | 4 +- .../settings/RepositoryHostSetupActions.tsx | 2 +- .../RepositoryHostSetupsSection.test.tsx | 3 +- .../settings/RepositoryHostSetupsSection.tsx | 3 +- ...ostSetupsSection.workspace-window.test.tsx | 3 +- ...yIconPicker.github-avatar-refresh.test.tsx | 2 +- .../settings/RepositoryIconPicker.tsx | 2 +- .../settings/RepositoryPane.test.ts | 4 +- .../components/settings/RepositoryPane.tsx | 10 +- .../RepositorySourceControlAiActionRows.tsx | 2 +- .../RepositorySourceControlAiSection.tsx | 2 +- .../RepositoryWindowsRuntimeSection.tsx | 3 +- ...RepositoryWorktreeDefaultsSection.test.tsx | 2 +- .../RepositoryWorktreeDefaultsSection.tsx | 3 +- .../RichMarkdownSpellcheckSetting.tsx | 2 +- .../settings/RuntimeEnvironmentsPane.tsx | 2 +- .../src/components/settings/Settings.tsx | 5 +- .../settings/SettingsSidebar.test.tsx | 2 +- .../components/settings/SettingsSidebar.tsx | 3 +- .../settings/SourceControlActionRecipeRow.tsx | 3 +- .../SourceControlAiActionRecipeDefaults.tsx | 3 +- .../SparsePresetSettingsSection.test.tsx | 2 +- .../settings/SparsePresetSettingsSection.tsx | 2 +- .../components/settings/TasksPane.test.tsx | 3 +- .../src/components/settings/TasksPane.tsx | 3 +- .../settings/TerminalAdvancedSection.test.tsx | 2 +- .../settings/TerminalAdvancedSection.tsx | 2 +- .../TerminalAdvancedTypographyControls.tsx | 2 +- .../settings/TerminalAppearanceSection.tsx | 2 +- .../TerminalCursorAppearanceSection.tsx | 2 +- .../settings/TerminalFontSizeSetting.test.tsx | 2 +- .../settings/TerminalFontSizeSetting.tsx | 2 +- .../settings/TerminalInteractionSection.tsx | 2 +- .../settings/TerminalMacKeyboardSection.tsx | 2 +- .../src/components/settings/TerminalPane.tsx | 2 +- .../TerminalPaneAppearanceSection.tsx | 2 +- .../settings/TerminalRenderingSection.tsx | 2 +- ...TerminalSettingsPreview.lifecycle.test.tsx | 2 +- .../settings/TerminalSettingsPreview.tsx | 2 +- .../settings/TerminalSetupScriptSection.tsx | 3 +- .../TerminalThemeSections.lifecycle.test.ts | 2 +- .../settings/TerminalThemeSections.tsx | 2 +- .../settings/TerminalWindowSection.tsx | 2 +- .../settings/TerminalWindowsShellSection.tsx | 2 +- .../components/settings/VoicePane.test.tsx | 2 +- .../src/components/settings/VoicePane.tsx | 2 +- .../WorkspaceDirectorySetting.test.tsx | 2 +- .../settings/WorkspaceDirectorySetting.tsx | 2 +- .../settings/WorktreeSymlinksSection.tsx | 2 +- .../settings/appearance-interface-summary.ts | 2 +- ...ce-status-bar-antigravity-toggle-search.ts | 2 +- ...ppearance-status-bar-grok-toggle-search.ts | 2 +- .../settings/appearance-status-bar-search.ts | 2 +- .../settings/codex-account-auth-warning.ts | 2 +- ...codex-session-source-home-control.test.tsx | 2 +- .../codex-session-source-home-control.tsx | 2 +- .../settings/notification-settings-copy.ts | 2 +- .../settings/provider-account-scope.ts | 2 +- .../provider-account-visibility.test.ts | 2 +- .../settings/provider-account-visibility.ts | 2 +- .../repository-git-author-search-entries.ts | 2 +- .../repository-git-hooks-search-entries.ts | 2 +- .../repository-git-worktree-search-entries.ts | 2 +- .../settings/repository-host-setup-options.ts | 2 +- .../settings/repository-icon-github.test.ts | 2 +- .../settings/repository-icon-github.ts | 3 +- .../settings/repository-identity-search.ts | 2 +- .../components/settings/repository-search.ts | 2 +- .../repository-source-control-ai-draft.ts | 2 +- .../repository-source-control-ai-global-ux.ts | 3 +- .../repository-source-control-ai-labels.ts | 2 +- .../src/components/settings/setting-labels.ts | 2 +- .../settings/settings-project-list.test.ts | 3 +- .../settings/settings-project-list.ts | 3 +- .../settings/shortcut-definition-catalog.ts | 2 +- .../components/settings/shortcut-groups.ts | 2 +- .../source-control-action-recipe-options.ts | 2 +- .../settings/sparse-preset-settings-row.tsx | 2 +- .../settings/task-source-setup-state.test.ts | 2 +- .../settings/task-source-setup-state.ts | 2 +- .../settings/terminal-window-color-groups.tsx | 2 +- ...se-task-source-provider-readiness.test.tsx | 2 +- .../use-task-source-provider-readiness.ts | 2 +- .../settings/useGhosttyImport.test.ts | 2 +- .../components/settings/useGhosttyImport.ts | 2 +- .../settings/useWarpThemeImport.test.ts | 2 +- .../components/settings/useWarpThemeImport.ts | 2 +- .../setup-guide-progress-readiness.ts | 3 +- .../AddProjectFromFolderDialog.test.tsx | 2 +- .../sidebar/AddProjectFromFolderDialog.tsx | 2 +- .../sidebar/AddRepoDialogStepContent.test.tsx | 2 +- .../sidebar/AddRepoDialogStepContent.tsx | 2 +- .../sidebar/AddRepoNestedImportStep.test.tsx | 2 +- .../sidebar/AddRepoNestedImportStep.tsx | 2 +- .../AddRepoSteps.default-checkout.test.ts | 2 +- .../src/components/sidebar/AddRepoSteps.tsx | 2 +- .../sidebar/DeleteWorktreeDialog.test.tsx | 4 +- .../sidebar/DeleteWorktreeLineageNotice.tsx | 2 +- .../sidebar/DeleteWorktreeTargetPreview.tsx | 2 +- .../sidebar/HiddenWorktreeRecoveryList.tsx | 3 +- .../sidebar/NonGitFolderDialog.test.tsx | 3 +- .../sidebar/ProjectAddedDialog.test.tsx | 2 +- .../components/sidebar/RemoteFileBrowser.tsx | 2 +- .../sidebar/SetupScriptPromptCard.test.ts | 2 +- .../src/components/sidebar/Sidebar.test.tsx | 2 +- .../sidebar/SidebarFeedbackDialog.tsx | 2 +- .../sidebar/SidebarHostScopeMenuSection.tsx | 5 +- .../components/sidebar/SidebarNav.test.tsx | 3 +- .../src/components/sidebar/SidebarNav.tsx | 2 +- .../sidebar/SidebarProjectFilterPanel.tsx | 2 +- .../SidebarRepositoryFilterSection.tsx | 2 +- .../sidebar/WorkspaceKanbanCard.tsx | 3 +- .../WorkspaceKanbanDrawer.search.test.tsx | 4 +- ...paceKanbanDrawer.task-status-sync.test.tsx | 3 +- .../sidebar/WorkspaceKanbanDrawer.tsx | 3 +- .../WorkspaceKanbanDrawerHeader.test.tsx | 2 +- .../sidebar/WorkspaceKanbanDrawerHeader.tsx | 2 +- .../WorkspaceKanbanLaneCardList.test.tsx | 3 +- .../sidebar/WorkspaceKanbanLaneCardList.tsx | 3 +- .../sidebar/WorkspaceKanbanLaneGrid.test.tsx | 3 +- .../sidebar/WorkspaceKanbanLaneGrid.tsx | 4 +- .../WorkspaceKanbanSettingsMenu.test.tsx | 2 +- .../sidebar/WorkspaceKanbanSettingsMenu.tsx | 2 +- .../WorkspaceKanbanStatusLane.test.tsx | 3 +- .../sidebar/WorkspaceKanbanStatusLane.tsx | 4 +- .../WorkspaceStatusAppearancePopover.tsx | 2 +- .../WorktreeCard.affiliate-list-mode.test.tsx | 5 +- .../WorktreeCard.compact-hover.test.tsx | 5 +- ....compact-ports-hover-independence.test.tsx | 5 +- ...orktreeCard.hosted-review-refresh.test.tsx | 4 +- .../sidebar/WorktreeCard.lineage.test.tsx | 4 +- .../WorktreeCard.merged-pr-display.test.tsx | 12 +- .../WorktreeCard.pinned-repo-icon.test.tsx | 5 +- .../sidebar/WorktreeCard.pr-display.test.tsx | 12 +- .../WorktreeCard.quick-actions.test.tsx | 12 +- ...WorktreeCard.ssh-reconnect-prompt.test.tsx | 4 +- .../components/sidebar/WorktreeCard.test.ts | 2 +- .../WorktreeCardAutomationDetailSection.tsx | 2 +- .../sidebar/WorktreeCardCliDetailSection.tsx | 2 +- .../WorktreeCardDisplayMenuSection.tsx | 5 +- .../sidebar/WorktreeCardHelpers.tsx | 4 +- .../WorktreeCardMetadataStatusBadges.tsx | 2 +- .../sidebar/WorktreeContextMenu.test.ts | 3 +- .../sidebar/WorktreeContextMenu.tsx | 8 +- .../WorktreeDeveloperMenuReveal.test.tsx | 2 +- .../WorktreeList.card-memo-stability.test.tsx | 4 +- ....lineage-agent-expansion-coupling.test.tsx | 12 +- .../WorktreeList.lineage-child-card.test.ts | 12 +- ...ktreeList.lineage-child-real-card.test.tsx | 10 +- ...treeList.status-lane-lineage-drop.test.tsx | 13 +- .../src/components/sidebar/WorktreeList.tsx | 19 +- .../sidebar/WorktreeMetaDialog.test.tsx | 12 +- .../components/sidebar/WorktreeOpenInMenu.tsx | 3 +- .../WorktreeParentPickerPopover.test.ts | 2 +- .../sidebar/WorktreeParentPickerRow.tsx | 3 +- .../sidebar/WorktreeVisibilityDialog.test.tsx | 6 +- .../sidebar/WorktreeVisibilityDialog.tsx | 2 +- .../sidebar/WorktreeVisibilitySourceList.tsx | 4 +- .../active-worktree-focus-after-delete.ts | 3 +- ...repo-existing-workspaces-telemetry.test.ts | 2 +- .../add-repo-existing-workspaces-telemetry.ts | 2 +- .../add-repo-skip-finalization.test.ts | 2 +- .../sidebar/add-repo-skip-finalization.ts | 4 +- .../sidebar/add-repo-store-upsert.ts | 2 +- .../sidebar/complete-nested-folder-open.ts | 2 +- ...branch-visible-under-hide-sleeping.test.ts | 10 +- .../sidebar/default-branch-workspace.ts | 2 +- .../sidebar/delete-worktree-dialog-copy.ts | 3 +- .../delete-worktree-dirty-change-counts.ts | 3 +- .../delete-worktree-preference-toast.ts | 2 +- .../empty-project-placeholder-repos.test.ts | 3 +- .../empty-project-placeholder-repos.ts | 3 +- .../focused-agent-row-highlight.test.ts | 2 +- .../folder-workspace-card-pr-display.test.ts | 12 +- .../folder-workspace-card-pr-display.ts | 4 +- .../folder-workspace-composer-helpers.test.ts | 3 +- .../folder-workspace-composer-helpers.ts | 14 +- ...er-workspace-composer-path-status.test.tsx | 2 +- .../folder-workspace-composer-path-status.ts | 2 +- .../folder-workspace-composer-submit.test.ts | 3 +- .../folder-workspace-composer-submit.ts | 4 +- .../components/sidebar/host-rename-remove.ts | 3 +- .../sidebar/host-section-rows.test.ts | 5 +- .../components/sidebar/host-section-rows.ts | 4 +- .../imported-worktrees-card-actions.ts | 2 +- ...imported-worktrees-card-candidates.test.ts | 4 +- .../imported-worktrees-card-candidates.ts | 3 +- .../sidebar/linear-agent-skill-runtime.ts | 2 +- .../local-base-ref-suggestion-toast.test.tsx | 2 +- .../local-base-ref-suggestion-toast.tsx | 2 +- ...w-external-worktrees-inbox-actions.test.ts | 2 +- .../new-external-worktrees-inbox-actions.ts | 2 +- ...xternal-worktrees-inbox-candidates.test.ts | 4 +- ...new-external-worktrees-inbox-candidates.ts | 3 +- .../sidebar/preserved-branch-toast.test.tsx | 2 +- .../sidebar/preserved-branch-toast.tsx | 3 +- .../project-added-default-checkout.test.ts | 4 +- .../sidebar/project-added-default-checkout.ts | 2 +- .../project-group-header-drag-commit.test.ts | 2 +- .../project-group-header-drag-commit.ts | 2 +- .../project-group-header-drag-contract.ts | 2 +- .../project-group-header-drag-start.test.ts | 2 +- .../project-group-header-drag-start.ts | 2 +- .../sidebar/project-group-header-drop.test.ts | 3 +- .../sidebar/project-group-header-drop.ts | 2 +- .../project-header-drag-commit.test.ts | 2 +- .../sidebar/project-header-drag-commit.ts | 2 +- .../sidebar/project-header-drag-contract.ts | 2 +- .../sidebar/project-header-drag-start.test.ts | 2 +- .../sidebar/project-header-drag-start.ts | 2 +- .../sidebar/project-header-drop.test.ts | 2 +- .../components/sidebar/project-header-drop.ts | 2 +- .../sidebar/prompt-cache-timer-selection.ts | 2 +- .../remote-file-browser-drive-paths.ts | 2 +- .../sidebar/remote-file-browser-helpers.ts | 2 +- .../rendered-sidebar-worktree-order.test.ts | 5 +- .../rendered-sidebar-worktree-order.ts | 2 +- .../sidebar/repo-header-create-state.test.ts | 2 +- .../sidebar/repo-header-create-state.ts | 2 +- .../setup-script-prompt-render-state.ts | 4 +- .../sidebar/sidebar-filter-state.test.ts | 4 +- .../sidebar/sidebar-host-options.ts | 4 +- .../sidebar/sidebar-project-drop.ts | 2 +- .../sidebar/sidebar-workspace-option-items.ts | 5 +- .../sidebar/smart-attention.test.ts | 3 +- .../src/components/sidebar/smart-attention.ts | 3 +- .../src/components/sidebar/smart-sort.test.ts | 4 +- .../src/components/sidebar/smart-sort.ts | 4 +- .../sidebar/ssh-host-remove-resolution.ts | 3 +- .../ssh-workspace-forget-resolution.ts | 2 +- .../sidebar/track-nested-folder-open.ts | 2 +- .../use-add-repo-remote-nested-scan.ts | 2 +- .../use-delete-worktree-status-hydration.ts | 3 +- ...e-visible-workspace-kanban-worktree-ids.ts | 3 +- .../use-workspace-kanban-card-pointer-drag.ts | 2 +- .../use-workspace-kanban-create-worktree.ts | 2 +- .../sidebar/use-workspace-kanban-search.ts | 3 +- .../use-workspace-kanban-selection.test.tsx | 2 +- .../sidebar/use-workspace-kanban-selection.ts | 2 +- .../sidebar/use-workspace-status-drop.ts | 2 +- .../use-worktree-activity-status.test.tsx | 2 +- .../use-worktree-card-linked-details.ts | 3 +- ...ndary-details.store-subscriptions.test.tsx | 3 +- .../use-worktree-card-secondary-details.ts | 2 +- .../sidebar/use-worktree-meta-workspace.ts | 2 +- .../sidebar/useAddRepoCloneFlow.test.ts | 2 +- .../components/sidebar/useAddRepoCloneFlow.ts | 2 +- .../sidebar/useAddRepoLocalFolderFlow.test.ts | 3 +- .../sidebar/useAddRepoLocalFolderFlow.ts | 3 +- .../useAddRepoNestedImportFlow.test.ts | 6 +- .../sidebar/useAddRepoNestedImportFlow.ts | 5 +- .../useAddRepoNestedReviewController.ts | 2 +- .../sidebar/useAddRepoNestedReviewState.ts | 2 +- .../sidebar/useAddRepoServerPathFlow.test.ts | 2 +- .../sidebar/useAddRepoServerPathFlow.ts | 3 +- .../useCreateRepo.default-checkout.test.ts | 2 +- .../src/components/sidebar/useCreateRepo.ts | 2 +- .../useSetupScriptPromptRevalidation.test.tsx | 2 +- .../useSetupScriptPromptRevalidation.ts | 2 +- .../sidebar/useWorktreeAgentRows.test.ts | 2 +- .../visible-worktree-activity-inputs.test.ts | 3 +- .../visible-worktree-activity-inputs.ts | 3 +- .../sidebar/visible-worktrees.test.ts | 5 +- .../components/sidebar/visible-worktrees.ts | 7 +- .../workspace-board-task-status-sync.test.ts | 9 +- .../workspace-board-task-status-sync.ts | 6 +- .../workspace-creator-visibility.test.ts | 2 +- .../sidebar/workspace-creator-visibility.ts | 3 +- .../sidebar/workspace-delete-lineage.test.ts | 3 +- .../sidebar/workspace-delete-lineage.ts | 3 +- .../workspace-kanban-card-pointer-drag-dom.ts | 2 +- .../sidebar/workspace-kanban-search.test.ts | 3 +- .../sidebar/workspace-kanban-search.ts | 3 +- .../workspace-kanban-sidebar-drop.test.ts | 2 +- .../sidebar/workspace-kanban-sidebar-drop.ts | 6 +- .../workspace-kanban-worktree-groups.test.ts | 2 +- .../workspace-kanban-worktree-groups.ts | 6 +- .../workspace-lineage-menu-actions.test.ts | 3 +- .../sidebar/workspace-lineage-menu-actions.ts | 3 +- .../components/sidebar/workspace-status.ts | 2 +- .../worktree-agent-activity-summary.test.ts | 2 +- .../worktree-agent-freshness-selector.test.ts | 2 +- ...worktree-agent-orchestration-batch.test.ts | 2 +- ...worktree-agent-orchestration-index.test.ts | 2 +- .../worktree-agent-row-fallback-tab.ts | 2 +- .../worktree-agent-row-selectors.test.ts | 2 +- .../sidebar/worktree-agent-row-selectors.ts | 2 +- .../components/sidebar/worktree-agent-rows.ts | 2 +- .../worktree-card-display-property-options.ts | 2 +- .../sidebar/worktree-card-header.tsx | 2 +- .../worktree-card-jira-issue-display.ts | 3 +- .../sidebar/worktree-card-meta-types.ts | 6 +- .../components/sidebar/worktree-card-model.ts | 3 +- .../sidebar/worktree-card-pr-display.test.ts | 2 +- .../sidebar/worktree-card-pr-display.ts | 3 +- .../worktree-card-status-inputs.test.ts | 2 +- .../sidebar/worktree-card-status-inputs.ts | 2 +- .../sidebar/worktree-delete-execution.ts | 2 +- .../sidebar/worktree-delete-request.ts | 2 +- .../worktree-lineage-drag-drop.test.ts | 3 +- .../sidebar/worktree-lineage-drag-drop.ts | 3 +- .../sidebar/worktree-lineage-projection.ts | 3 +- .../worktree-list-folder-reveal.test.ts | 4 +- .../sidebar/worktree-list-folder-reveal.ts | 4 +- .../sidebar/worktree-list-groups.test.ts | 16 +- .../sidebar/worktree-list-groups.ts | 19 +- .../worktree-list-host-filtering.test.ts | 3 +- .../sidebar/worktree-list-host-filtering.ts | 3 +- .../worktree-list-imported-rows.test.ts | 3 +- .../worktree-list-review-cache-inputs.test.ts | 2 +- .../worktree-list-review-cache-inputs.ts | 2 +- .../worktree-list-scroll-adjustment.test.ts | 2 +- .../worktree-list-sticky-headers.test.ts | 4 +- .../sidebar/worktree-meta-updates.test.ts | 2 +- .../sidebar/worktree-meta-updates.ts | 3 +- .../sidebar/worktree-parent-candidates.ts | 4 +- .../worktree-parent-eligibility.test.ts | 4 +- .../sidebar/worktree-parent-eligibility.ts | 3 +- .../worktree-parent-picker-filtering.ts | 2 +- .../worktree-sidebar-row-preference.test.ts | 5 +- .../worktree-sidebar-row-preference.ts | 2 +- .../sidebar/worktree-subagent-child-rows.ts | 2 +- .../worktree-title-derived-agent-rows.test.ts | 3 +- .../worktree-title-derived-agent-rows.ts | 2 +- .../worktree-visibility-update-error.ts | 2 +- .../sparse/SparseCheckoutPresetSelect.tsx | 2 +- .../status-bar/PetStatusSegment.tsx | 2 +- .../ResourceUsageStatusSegment.rows.test.tsx | 2 +- .../status-bar/ResourceUsageStatusSegment.tsx | 4 +- .../src/components/status-bar/StatusBar.tsx | 6 +- .../status-bar/WorkspaceSpaceManagerPanel.tsx | 5 +- .../mergeSnapshotAndSessions.test.ts | 11 +- .../status-bar/mergeSnapshotAndSessions.ts | 6 +- .../status-bar/resource-memory-metric-copy.ts | 2 +- .../resource-session-bindings.test.ts | 2 +- .../status-bar/resource-session-bindings.ts | 2 +- .../status-bar/resource-usage-merge-types.ts | 9 +- .../status-bar/status-bar-agent-gating.ts | 3 +- .../status-bar-provider-visibility.ts | 2 +- .../status-bar-runtime-groups.test.ts | 6 +- .../use-available-status-bar-toggles.ts | 2 +- .../workspace-space-presentation.test.ts | 3 +- .../workspace-space-presentation.ts | 2 +- .../components/tab-bar/BrowserTab.test.tsx | 2 +- .../src/components/tab-bar/BrowserTab.tsx | 2 +- .../src/components/tab-bar/EditorFileTab.tsx | 2 +- .../components/tab-bar/QuickLaunchButton.tsx | 2 +- .../tab-bar/RecentTabSwitcher.test.tsx | 2 +- .../src/components/tab-bar/SortableTab.tsx | 2 +- .../tab-bar/SortableTabContextMenu.tsx | 2 +- .../TabBarCreateEntry.keyboard.test.tsx | 4 +- .../tab-bar/TabBarQuickCommandsButton.tsx | 2 +- .../tab-bar/TerminalTabLeadingIcon.tsx | 3 +- .../tab-bar/editor-tab-local-open-guard.ts | 2 +- .../tab-bar/group-tab-order.test.ts | 2 +- .../src/components/tab-bar/group-tab-order.ts | 2 +- .../tab-bar/open-tab-search-entries.ts | 3 +- .../tab-bar/open-tab-search.test.ts | 10 +- .../tab-bar/recent-tab-switching.test.ts | 2 +- .../tab-bar/recent-tab-switching.ts | 2 +- .../tab-bar/tab-agent-launch-options.ts | 2 +- .../tab-bar/tab-agent-types-by-tab-id.test.ts | 2 +- .../tab-bar/tab-agent-types-by-tab-id.ts | 2 +- .../components/tab-bar/tab-bar-item-model.ts | 9 +- .../src/components/tab-bar/tab-bar-props.ts | 8 +- .../tab-create-entry-local-path.test.ts | 4 +- .../tab-bar/tab-create-entry-props.ts | 2 +- .../tab-bar/tab-move-to-pane-column.test.ts | 2 +- .../tab-bar/tab-title-tooltip.test.tsx | 10 +- .../terminal-tab-activity-status.test.ts | 2 +- .../tab-bar/terminal-tab-activity-status.ts | 2 +- .../terminal-tab-spinner-launch-agent.test.ts | 2 +- .../tab-bar/use-open-tab-search.test.ts | 15 +- .../use-tab-bar-create-menu-controller.ts | 2 +- .../tab-bar/use-tab-bar-item-projection.ts | 3 +- ...tab-bar-quick-command-search-input.test.ts | 2 +- .../tab-bar/use-tab-bar-runtime-model.ts | 5 +- .../TabGroupPanel.context-menu.test.ts | 2 +- .../tab-group/TabGroupSplitLayout.tsx | 2 +- .../src/components/tab-group/tab-drag-data.ts | 3 +- .../tab-drag-preview-activation.test.ts | 2 +- .../tab-group-panel-split-target.test.ts | 2 +- .../tab-group/tab-group-panel-split-target.ts | 2 +- .../tab-group/tab-group-visible-id.ts | 2 +- .../tab-group/useTabDragSplit.test.ts | 2 +- .../components/tab-group/useTabDragSplit.ts | 2 +- .../tab-group/useTabGroupWorkspaceModel.ts | 9 +- .../task-page-cache-selectors.test.ts | 4 +- .../components/task-page-cache-selectors.ts | 2 +- .../src/components/task-page-checks-pill.ts | 2 +- .../task-page-default-repo-selection.test.ts | 2 +- .../task-page-default-repo-selection.ts | 2 +- ...page-github-dialog-state-authority.test.ts | 2 +- ...task-page-github-dialog-state-authority.ts | 2 +- .../task-page-github-resume-cache.ts | 2 +- .../task-page-github-status-actions.test.ts | 2 +- .../task-page-github-status-actions.ts | 6 +- .../task-page-github-status-state.ts | 2 +- .../components/task-page-github-task-kind.ts | 2 +- ...page-github-work-item-filter-membership.ts | 3 +- ...e-github-work-item-mutation-composition.ts | 3 +- ...age-github-work-item-mutation-lifecycle.ts | 2 +- ...sk-page-github-work-item-mutation-pages.ts | 2 +- ...-github-work-item-mutation-patches.test.ts | 2 +- ...-page-github-work-item-mutation-patches.ts | 3 +- ...page-github-work-item-mutation-registry.ts | 2 +- ...hub-work-item-mutation-regressions.test.ts | 2 +- ...sk-page-github-work-item-mutation-types.ts | 2 +- ...sk-page-github-work-item-mutations.test.ts | 2 +- .../task-page-github-work-item-mutations.ts | 2 +- .../task-page-github-work-item-quiet-adopt.ts | 2 +- ...-page-github-work-item-quiet-revalidate.ts | 2 +- ...sk-page-github-work-item-registry-types.ts | 3 +- .../task-page-github-work-item-status.ts | 2 +- .../task-page-jira-cache-selectors.test.ts | 2 +- .../task-page-jira-cache-selectors.ts | 2 +- .../task-page-jira-create-fields.test.ts | 2 +- .../task-page-jira-create-fields.ts | 2 +- .../task-page-jira-grouping.test.ts | 2 +- .../components/task-page-jira-issue-list.tsx | 2 +- ...task-page-jira-item-source-context.test.ts | 2 +- .../task-page-jira-item-source-context.ts | 2 +- .../components/task-page-jira-load-state.ts | 2 +- .../task-page-jira-project-selection.test.ts | 2 +- .../task-page-jira-project-selection.ts | 2 +- .../components/task-page-jira-sorting.test.ts | 2 +- .../components/task-page-jira-status-order.ts | 2 +- .../task-page-linear-cache-selectors.ts | 3 +- .../task-page-linear-in-orca-issues.test.ts | 2 +- .../task-page-linear-in-orca-issues.ts | 4 +- .../task-page-linear-team-selection.test.ts | 2 +- .../task-page-linear-team-selection.ts | 2 +- .../task-page-list-chrome-visibility.ts | 2 +- .../task-page-localized-options.tsx | 3 +- .../task-page-new-issue-draft.test.ts | 2 +- .../components/task-page-new-issue-draft.ts | 2 +- .../task-page-pr-check-summary.test.ts | 2 +- .../components/task-page-pr-check-summary.ts | 3 +- .../components/task-page-pr-delta-summary.ts | 2 +- .../task-page-work-item-pagination.test.ts | 2 +- .../task-page-work-item-pagination.ts | 3 +- .../task-page-work-item-signatures.ts | 2 +- .../task-project-source-combobox-model.ts | 2 +- .../task-project-source-combobox.tsx | 2 +- .../components/task-source-context-summary.ts | 2 +- .../task-source-provider-availability.ts | 2 +- .../terminal-pane/TerminalContextMenu.tsx | 2 +- .../components/terminal-pane/TerminalPane.tsx | 5 +- .../TerminalPaneOverlayLayer.tsx | 3 +- .../TerminalQuickCommandsSubmenu.tsx | 2 +- .../agent-completion-coordinator-types.ts | 2 +- .../agent-task-complete-policy.ts | 2 +- .../codex-detached-pane-restart.ts | 2 +- .../command-code-output-ownership.ts | 2 +- .../compose-active-terminal-theme.test.ts | 2 +- .../layout-serialization.test.ts | 2 +- .../terminal-pane/layout-serialization.ts | 2 +- .../native-chat-leaf-title-agent.ts | 2 +- .../pane-foreground-agent-tracker.ts | 2 +- .../terminal-pane/pty-connection-types.ts | 3 +- .../terminal-pane/pty-connection.test.ts | 3 +- .../terminal-pane/pty-connection.ts | 3 +- .../terminal-pane/pty-transport-types.ts | 2 +- .../remote-pane-layout-push.test.ts | 2 +- .../terminal-pane/remote-pane-layout-push.ts | 2 +- .../terminal-agent-session-continuation.ts | 2 +- .../terminal-agent-session-fork.ts | 2 +- .../terminal-pane/terminal-appearance.ts | 2 +- ...-cold-park-pre-gate-loop.react185.test.tsx | 3 +- .../terminal-cold-park-verdict-loop.test.tsx | 2 +- .../terminal-cold-park-withheld-tabs.test.ts | 2 +- .../terminal-cold-park-withheld-tabs.ts | 2 +- .../terminal-pane/terminal-fit-restore.ts | 2 +- .../terminal-hidden-delivery-gate.ts | 2 +- .../terminal-hidden-view-parking.ts | 2 +- .../terminal-hidden-worktree-retention.ts | 2 +- .../terminal-keyboard-protocol-pane-agent.ts | 2 +- .../terminal-layout-leaf-detach.test.ts | 2 +- .../terminal-layout-leaf-detach.ts | 5 +- .../terminal-layout-leaf-ids.test.ts | 5 +- .../terminal-pane/terminal-layout-leaf-ids.ts | 5 +- ...erminal-layout-pty-ownership-depth.test.ts | 2 +- .../terminal-layout-pty-ownership.test.ts | 2 +- .../terminal-layout-pty-ownership.ts | 65 +--- ...erminal-live-layout-reconciliation.test.ts | 2 +- .../terminal-live-layout-reconciliation.ts | 5 +- .../terminal-notification-pane-visibility.ts | 2 +- .../terminal-notification-state.ts | 2 +- .../terminal-pane-close-identity.ts | 2 +- .../terminal-pane-layout-tree.ts | 63 +++ .../terminal-pane-tab-detach.test.ts | 2 +- .../terminal-pane/terminal-pane-tab-detach.ts | 2 +- .../terminal-parked-watcher-reconciliation.ts | 2 +- .../terminal-quick-command-dispatch.ts | 2 +- .../terminal-pane/terminal-renderer-policy.ts | 2 +- .../terminal-shutdown-layout-capture.test.ts | 2 +- .../terminal-shutdown-layout-capture.ts | 2 +- .../terminal-side-effect-facts-handler.ts | 2 +- .../terminal-pane/terminal-tab-lookup.test.ts | 2 +- .../terminal-pane/terminal-tab-lookup.ts | 2 +- .../terminal-tab-park-candidates.ts | 2 +- .../terminal-unified-tab-lookup.test.ts | 2 +- .../terminal-unified-tab-lookup.ts | 2 +- .../terminal-view-attributes-publisher.ts | 2 +- .../use-notification-dispatch.test.ts | 2 +- ...-watcher-synchronization.react185.test.tsx | 2 +- ...parked-terminal-watcher-synchronization.ts | 2 +- .../use-terminal-pane-context-menu.ts | 2 +- .../use-terminal-pane-lifecycle.ts | 11 +- .../use-terminal-tab-cold-parking.test.ts | 2 +- .../use-terminal-tab-cold-parking.ts | 2 +- .../TerminalQuickCommandAdvancedSection.tsx | 3 +- .../TerminalQuickCommandContentSection.tsx | 3 +- .../TerminalQuickCommandDialog.test.tsx | 2 +- .../TerminalQuickCommandDialog.tsx | 8 +- .../TerminalQuickCommandLabelField.tsx | 2 +- .../TerminalQuickCommandScopeField.tsx | 4 +- .../terminal-quick-command-agent-options.ts | 2 +- ...erminal-quick-command-dialog-draft.test.ts | 2 +- .../terminal-quick-command-dialog-draft.ts | 3 +- ...ive-terminal-repair-loop.react185.test.tsx | 3 +- .../terminal/active-terminal-repair.test.ts | 2 +- .../terminal/active-terminal-repair.ts | 3 +- .../terminal/split-group-mount.test.ts | 2 +- .../components/terminal/split-group-mount.ts | 2 +- .../terminal/terminal-tab-bulk-actions.ts | 2 +- .../terminal/use-active-terminal-repair.ts | 3 +- .../components/ui/repo-multi-combobox.test.ts | 2 +- .../src/components/ui/repo-multi-combobox.tsx | 2 +- .../inactive-workspace-estimate.test.ts | 3 +- .../inactive-workspace-estimate.ts | 3 +- ...workspace-cleanup-presentation-fixtures.ts | 3 +- .../workspace-cleanup-presentation.ts | 3 +- .../worktree-base-fallback-notice.ts | 2 +- ...jump-palette-interleaved-sections.test.tsx | 5 +- ...ktree-jump-palette-sleeping-filter.test.ts | 2 +- .../worktree-jump-palette-test-fixtures.ts | 5 +- .../hooks/direct-ssh-host-hydration.test.ts | 3 +- .../src/hooks/ipc-events-test-harness.ts | 2 +- .../hooks/remote-workspace-session-merge.ts | 3 +- .../src/hooks/remote-workspace-target-sync.ts | 2 +- src/renderer/src/hooks/resolve-zoom-target.ts | 2 +- .../use-terminal-quick-command-hosts.test.ts | 2 +- .../hooks/use-terminal-quick-command-hosts.ts | 3 +- .../src/hooks/useAgentDetectionTarget.test.ts | 4 +- .../src/hooks/useAutoAckViewedAgent.ts | 2 +- src/renderer/src/hooks/useComposerState.ts | 33 +- src/renderer/src/hooks/useDetectedAgents.ts | 2 +- .../useEditorExternalWatch-complexity.test.ts | 2 +- .../useEditorExternalWatch-self-move.test.ts | 2 +- .../useEditorExternalWatch-wsl-repro.test.ts | 2 +- .../src/hooks/useEditorExternalWatch.test.ts | 2 +- .../src/hooks/useEditorExternalWatch.ts | 2 +- .../src/hooks/useEphemeralVmRecipeOptions.ts | 2 +- .../src/hooks/useGitHubSlugMetadata.ts | 3 +- src/renderer/src/hooks/useGlobalFileDrop.ts | 2 +- .../useInstalledAgentSkills.react.test.tsx | 2 +- src/renderer/src/hooks/useIpcEvents.test.ts | 3 +- src/renderer/src/hooks/useIpcEvents.ts | 6 +- src/renderer/src/hooks/useIssueMetadata.ts | 8 +- .../hooks/useLinearProviderConnected.test.tsx | 2 +- ...vigationMetadata.capability-owner.test.tsx | 2 +- .../useSettingsNavigationMetadata.test.ts | 2 +- .../hooks/useSettingsNavigationMetadata.ts | 2 +- .../useTaskPageGitHubWorkItemMutation.ts | 2 +- .../src/hooks/worktree-file-change-event.ts | 2 +- .../worktree-head-identity-apply.test.ts | 2 +- .../src/hooks/worktree-head-identity-apply.ts | 2 +- .../src/lib/active-agent-note-send.test.ts | 2 +- .../src/lib/active-agent-note-target.ts | 2 +- src/renderer/src/lib/active-view-persist.ts | 3 +- .../src/lib/activity-thread-display.ts | 3 +- .../lib/agent-background-draft-delivery.ts | 2 +- .../lib/agent-background-session-contract.ts | 2 +- .../agent-background-session-timeout-toast.ts | 2 +- src/renderer/src/lib/agent-catalog.tsx | 2 +- .../src/lib/agent-draft-paste-content.ts | 2 +- src/renderer/src/lib/agent-draft-readiness.ts | 2 +- src/renderer/src/lib/agent-favicon-assets.ts | 2 +- .../src/lib/agent-followup-delivery.ts | 2 +- .../lib/agent-hibernation-coordinator.test.ts | 2 +- .../src/lib/agent-hibernation-planner.test.ts | 2 +- .../src/lib/agent-hibernation-planner.ts | 3 +- .../src/lib/agent-launch-prompt-delivery.ts | 2 +- .../src/lib/agent-native-draft-prefill.ts | 2 +- src/renderer/src/lib/agent-paste-draft.ts | 3 +- .../src/lib/agent-session-continuation.ts | 2 +- .../src/lib/agent-status-count.test.ts | 3 +- src/renderer/src/lib/agent-status.ts | 4 +- src/renderer/src/lib/agent-tab-shortcuts.ts | 2 +- .../src/lib/ai-vault-resume-target.ts | 2 +- .../src/lib/ai-vault-tab-title-requests.ts | 2 +- .../src/lib/ai-vault-tab-title-sync-inputs.ts | 2 +- .../src/lib/ai-vault-tab-title-sync.test.ts | 2 +- .../src/lib/automation-session-reuse.ts | 2 +- .../src/lib/automation-terminal-ownership.ts | 2 +- .../lib/browser-cookie-import-toast.test.ts | 2 +- .../src/lib/browser-cookie-import-toast.ts | 2 +- .../browser-page-palette-activation.test.ts | 12 +- .../lib/browser-palette-page-entries.test.ts | 3 +- .../src/lib/browser-palette-page-entries.ts | 3 +- .../src/lib/browser-palette-search.test.ts | 3 +- .../src/lib/browser-palette-search.ts | 3 +- .../src/lib/cmd-j-github-url-lookup.test.ts | 2 +- .../src/lib/cmd-j-github-url-lookup.ts | 3 +- .../src/lib/cmd-j-match-relevance.test.ts | 2 +- src/renderer/src/lib/cmd-j-match-relevance.ts | 2 +- .../src/lib/codex-pane-restart-eligibility.ts | 2 +- .../src/lib/codex-pane-selection-lane.ts | 3 +- .../src/lib/codex-session-restart.test.ts | 2 +- src/renderer/src/lib/codex-session-restart.ts | 2 +- .../src/lib/composer-issue-command.ts | 2 +- .../src/lib/connection-context.test.ts | 5 +- .../src/lib/create-untitled-markdown.ts | 2 +- .../src/lib/diff-comment-compat.test.ts | 2 +- src/renderer/src/lib/diff-comment-compat.ts | 2 +- .../src/lib/diff-comments-format.test.ts | 2 +- .../src/lib/direct-ssh-target-scope-types.ts | 11 +- src/renderer/src/lib/document-theme.ts | 2 +- .../src/lib/duplicate-browser-tab-options.ts | 2 +- .../src/lib/emulator-right-split-target.ts | 2 +- .../src/lib/ensure-hooks-confirmed.test.ts | 2 +- .../src/lib/ensure-hooks-confirmed.ts | 2 +- .../lib/ephemeral-vm-workspace-target.test.ts | 2 +- .../src/lib/ephemeral-vm-workspace-target.ts | 2 +- .../src/lib/ephemeral-vm-worktree-creation.ts | 2 +- .../lib/external-editor-open-capability.ts | 2 +- .../src/lib/file-search-result-owner.ts | 2 +- .../src/lib/fix-checks-agent-launch.ts | 8 +- .../lib/floating-workspace-tab-creation.ts | 3 +- ...loating-workspace-terminal-actions.test.ts | 3 +- .../floating-workspace-terminal-actions.ts | 3 +- .../src/lib/folder-workspace-connection.ts | 4 +- .../src/lib/folder-workspace-runtime-owner.ts | 3 +- src/renderer/src/lib/github-pr-start-point.ts | 3 +- .../src/lib/github-source-runtime-context.ts | 2 +- .../src/lib/github-work-item-source-lookup.ts | 2 +- ...hub-work-item-workspace-attachment.test.ts | 2 +- .../github-work-item-workspace-attachment.ts | 3 +- .../gitlab-work-item-source-lookup.test.ts | 2 +- .../src/lib/gitlab-work-item-source-lookup.ts | 2 +- src/renderer/src/lib/http-link-routing.ts | 2 +- .../src/lib/launch-agent-in-new-tab.ts | 2 +- .../lib/launch-agent-paste-timeout-notice.ts | 2 +- .../lib/launch-agent-session-continuation.ts | 2 +- .../src/lib/launch-agent-web-host-tab.ts | 3 +- .../src/lib/launch-work-item-direct-agent.ts | 2 +- .../lib/launch-work-item-direct-preflight.ts | 11 +- .../src/lib/launch-work-item-direct-types.ts | 4 +- .../src/lib/launch-work-item-direct.ts | 4 +- .../launch-worktree-background-terminals.ts | 6 +- .../src/lib/left-sidebar-appearance.ts | 2 +- .../lib/linear-issue-context-snapshot.test.ts | 2 +- .../src/lib/linear-issue-context-snapshot.ts | 2 +- .../src/lib/linear-issue-url-lookup.test.ts | 3 +- .../src/lib/linear-issue-url-lookup.ts | 3 +- .../linear-issue-workspace-attachment.test.ts | 2 +- .../lib/linear-issue-workspace-attachment.ts | 3 +- .../lib/linear-issue-workspace-open.test.ts | 3 +- .../src/lib/linear-linked-work-item.test.ts | 2 +- .../src/lib/linear-linked-work-item.ts | 2 +- .../src/lib/linked-work-item-context.ts | 2 +- src/renderer/src/lib/local-path-open-guard.ts | 2 +- .../src/lib/local-preflight-context.test.ts | 3 +- .../src/lib/local-preflight-context.ts | 3 +- .../src/lib/markdown-comment-blocks.test.ts | 2 +- .../src/lib/markdown-comment-blocks.ts | 2 +- .../lib/markdown-document-templates.test.ts | 2 +- .../src/lib/markdown-document-templates.ts | 3 +- .../src/lib/markdown-review-note-copy.test.ts | 2 +- .../src/lib/markdown-review-notes.test.ts | 2 +- src/renderer/src/lib/markdown-review-notes.ts | 2 +- .../lib/native-chat-initial-view-mode.test.ts | 2 +- .../src/lib/native-chat-initial-view-mode.ts | 4 +- .../src/lib/native-chat-launch-prompt.ts | 2 +- src/renderer/src/lib/native-chat-telemetry.ts | 2 +- .../src/lib/nested-repo-selected-paths.ts | 2 +- .../lib/new-workspace-composer-repo.test.ts | 2 +- .../src/lib/new-workspace-composer-repo.ts | 2 +- .../lib/new-workspace-project-options.test.ts | 4 +- .../src/lib/new-workspace-project-options.ts | 4 +- src/renderer/src/lib/new-workspace.ts | 3 +- .../src/lib/notes-send-agent-targets.test.ts | 2 +- .../src/lib/notes-send-agent-targets.ts | 2 +- .../lib/onboarding-folder-agent-startup.ts | 4 +- .../src/lib/onboarding-project-checklist.ts | 2 +- src/renderer/src/lib/open-in-app-catalog.tsx | 2 +- .../src/lib/open-in-application-selection.ts | 2 +- .../lib/orchestration-skill-coverage.test.ts | 2 +- .../src/lib/orchestration-skill-coverage.ts | 2 +- .../lib/order-empty-query-worktrees.test.ts | 2 +- .../src/lib/order-empty-query-worktrees.ts | 2 +- src/renderer/src/lib/pane-agent-evidence.ts | 2 +- .../lib/pane-manager/pane-manager-types.ts | 2 +- .../terminal-keyboard-protocol.ts | 2 +- .../src/lib/pending-worktree-creation.ts | 14 +- .../src/lib/pi-session-resume-wake.test.ts | 2 +- .../src/lib/pr-comment-action-state.test.ts | 2 +- .../src/lib/pr-comment-action-state.ts | 2 +- .../src/lib/pr-comment-reactions.test.ts | 2 +- src/renderer/src/lib/pr-comment-reactions.ts | 6 +- .../src/lib/project-host-clone-url.test.ts | 2 +- .../src/lib/project-host-clone-url.ts | 2 +- .../lib/project-host-setup-options.test.ts | 3 +- .../src/lib/project-host-setup-options.ts | 3 +- .../lib/project-host-workspace-target.test.ts | 3 +- .../src/lib/project-host-workspace-target.ts | 3 +- src/renderer/src/lib/project-skill-runtime.ts | 2 +- .../src/lib/provider-runtime-context.ts | 2 +- .../lib/quick-workspace-agent-selection.ts | 2 +- .../src/lib/recent-workspace-tab-rows.test.ts | 2 +- .../src/lib/recent-workspace-tab-rows.ts | 3 +- .../src/lib/repo-runtime-owner.test.ts | 2 +- src/renderer/src/lib/repo-runtime-owner.ts | 3 +- src/renderer/src/lib/repo-search.test.ts | 2 +- src/renderer/src/lib/repo-search.ts | 2 +- src/renderer/src/lib/repo-slug-cache.test.ts | 2 +- src/renderer/src/lib/repo-slug-cache.ts | 3 +- src/renderer/src/lib/repo-slug-index.test.ts | 2 +- src/renderer/src/lib/repo-slug-index.ts | 3 +- .../src/lib/run-quick-command-in-new-tab.ts | 2 +- .../src/lib/running-agent-targets.test.ts | 2 +- src/renderer/src/lib/running-agent-targets.ts | 2 +- .../lib/runtime-agent-background-create.ts | 2 +- .../lib/runtime-pane-title-leaf-id.test.ts | 2 +- .../src/lib/runtime-pane-title-leaf-id.ts | 5 +- .../src/lib/session-write-subscriber.ts | 2 +- .../src/lib/setup-script-prompt.test.ts | 7 +- src/renderer/src/lib/setup-script-prompt.ts | 3 +- src/renderer/src/lib/setup-script-status.ts | 2 +- .../src/lib/simulator-palette-search.test.ts | 3 +- .../src/lib/simulator-palette-search.ts | 3 +- .../simulator-tab-palette-activation.test.ts | 3 +- .../src/lib/sleeping-agent-pane-ownership.ts | 2 +- src/renderer/src/lib/smart-github-submit.ts | 3 +- .../lib/source-control-agent-action-plan.ts | 2 +- ...rce-control-launch-agent-selection.test.ts | 4 +- .../source-control-launch-agent-selection.ts | 4 +- .../src/lib/startup-ui-hydration.test.ts | 2 +- src/renderer/src/lib/startup-ui-hydration.ts | 2 +- .../src/lib/tab-agent-status-index.test.ts | 3 +- .../src/lib/tab-agent-status-index.ts | 2 +- src/renderer/src/lib/tab-agent.test.ts | 3 +- src/renderer/src/lib/tab-agent.ts | 3 +- .../src/lib/tab-number-shortcuts.test.ts | 2 +- src/renderer/src/lib/tab-number-shortcuts.ts | 2 +- .../src/lib/terminal-layout-equality.ts | 5 +- .../terminal-pane-title-sanitization.test.ts | 2 +- .../lib/terminal-pane-title-sanitization.ts | 2 +- ...rminal-quick-command-project-scope.test.ts | 4 +- .../terminal-quick-command-project-scope.ts | 3 +- .../lib/terminal-quick-command-search.test.ts | 2 +- .../src/lib/terminal-quick-command-search.ts | 2 +- src/renderer/src/lib/terminal-theme.ts | 2 +- .../src/lib/unread-badge-count.test.ts | 3 +- src/renderer/src/lib/unread-badge-count.ts | 3 +- .../src/lib/update-check-click-options.ts | 2 +- ...se-tab-agent-opencode-native-title.test.ts | 3 +- .../lib/use-tab-agent-process-signals.test.ts | 3 +- .../use-tab-agent-retained-identity.test.ts | 3 +- .../use-tab-agent-sleeping-session.test.ts | 3 +- src/renderer/src/lib/use-tab-agent.test.ts | 3 +- src/renderer/src/lib/use-tab-agent.ts | 3 +- .../workspace-activation-path-gate.test.ts | 3 +- ...workspace-port-localhost-label-selector.ts | 4 +- .../src/lib/workspace-port-localhost-label.ts | 4 +- .../lib/workspace-runtime-host-ownership.ts | 2 +- .../workspace-session-browser-history.test.ts | 2 +- .../lib/workspace-session-focus-recency.ts | 2 +- .../workspace-session-host-field-ownership.ts | 2 +- ...workspace-session-host-persistence.test.ts | 2 +- .../lib/workspace-session-host-persistence.ts | 6 +- .../src/lib/workspace-session-host-records.ts | 2 +- .../lib/workspace-session-host-split.test.ts | 11 +- .../src/lib/workspace-session-host-split.ts | 2 +- .../workspace-session-hydration-keys.test.ts | 2 +- .../lib/workspace-session-hydration-keys.ts | 3 +- .../src/lib/workspace-session-patch.test.ts | 2 +- .../src/lib/workspace-session-patch.ts | 5 +- .../workspace-session-reconnect-targets.ts | 2 +- .../lib/workspace-session-sleeping-agents.ts | 2 +- .../src/lib/workspace-session-unified-tabs.ts | 8 +- src/renderer/src/lib/workspace-session.ts | 9 +- .../lib/workspace-tab-palette-search.test.ts | 4 +- .../src/lib/workspace-tab-palette-search.ts | 4 +- ...ktree-activation-automation-filter.test.ts | 2 +- ...ree-activation-created-agent-test-state.ts | 2 +- .../worktree-activation-empty-remote.test.ts | 2 +- .../src/lib/worktree-activation.test.ts | 2 +- src/renderer/src/lib/worktree-activation.ts | 10 +- .../src/lib/worktree-activity-state.test.ts | 2 +- .../src/lib/worktree-activity-state.ts | 2 +- .../src/lib/worktree-attachment-label.ts | 2 +- .../src/lib/worktree-creation-agent-seeds.ts | 2 +- .../src/lib/worktree-creation-flow.ts | 2 +- .../src/lib/worktree-default-display-name.ts | 2 +- .../lib/worktree-display-name-order.test.ts | 2 +- .../src/lib/worktree-display-name-order.ts | 2 +- .../src/lib/worktree-operation-route.test.ts | 2 +- .../lib/worktree-palette-gitlab-url-match.ts | 3 +- .../lib/worktree-palette-review-match.test.ts | 3 +- .../src/lib/worktree-palette-search.test.ts | 3 +- .../src/lib/worktree-palette-search.ts | 3 +- .../worktree-palette-task-url-match.test.ts | 3 +- .../lib/worktree-palette-task-url-match.ts | 3 +- .../src/lib/worktree-runtime-owner-index.ts | 5 +- .../src/lib/worktree-runtime-owner-state.ts | 12 +- .../src/lib/worktree-runtime-owner.ts | 3 +- ...rktree-status-spinner-launch-agent.test.ts | 2 +- ...ktree-status-terminal-layout-roots.test.ts | 2 +- src/renderer/src/lib/worktree-status.test.ts | 2 +- src/renderer/src/lib/worktree-status.ts | 6 +- src/renderer/src/popout.tsx | 2 +- .../src/runtime/close-mirrored-editor-tab.ts | 2 +- ...e-explorer-delete-owner-provenance.test.ts | 5 +- .../runtime/gitlab-job-trace-client.test.ts | 2 +- .../src/runtime/gitlab-job-trace-client.ts | 2 +- .../runtime/local-jira-search-cancellation.ts | 2 +- .../remote-host-file-delete-repro.test.ts | 4 +- .../remote-host-file-open-repro.test.ts | 2 +- .../src/runtime/remote-server-parity.test.ts | 2 +- .../remote-server-update-coordinator.ts | 2 +- .../remote-terminal-layout-resolution.test.ts | 2 +- .../remote-terminal-layout-resolution.ts | 2 +- .../src/runtime/runtime-client-target.ts | 2 +- .../src/runtime/runtime-file-client.ts | 9 +- .../src/runtime/runtime-file-search-bounds.ts | 2 +- .../src/runtime/runtime-git-client.ts | 15 +- .../src/runtime/runtime-hooks-client.ts | 3 +- .../src/runtime/runtime-jira-client.ts | 2 +- .../runtime/runtime-jira-summary-client.ts | 2 +- .../src/runtime/runtime-jira-target.ts | 2 +- .../src/runtime/runtime-linear-client.ts | 23 +- .../runtime-provider-accounts-client.test.ts | 2 +- .../runtime-provider-accounts-client.ts | 6 +- .../src/runtime/runtime-repo-client.ts | 3 +- .../runtime-server-directory-browser.ts | 2 +- .../runtime/runtime-terminal-inspection.ts | 2 +- .../src/runtime/runtime-terminal-stream.ts | 2 +- ...sync-runtime-graph-automation-leaf.test.ts | 2 +- .../sync-runtime-graph-scheduling.test.ts | 2 +- .../src/runtime/sync-runtime-graph.ts | 6 +- ...ime-session-mirror-environment-key.test.ts | 3 +- .../src/runtime/web-runtime-session.ts | 3 +- ...-session-tabs-sync-mirror-identity.test.ts | 2 +- .../src/runtime/web-session-tabs-sync.test.ts | 10 +- .../src/runtime/web-session-tabs-sync.ts | 11 +- .../web-session-terminal-orphan-recovery.ts | 2 +- .../web-session-terminal-orphan-topology.ts | 5 +- ...oject-host-setup-selector-normalization.ts | 3 +- .../src/store/project-host-setup-selector.ts | 2 +- src/renderer/src/store/right-sidebar-route.ts | 5 +- src/renderer/src/store/selectors.test.ts | 4 +- src/renderer/src/store/selectors.ts | 4 +- .../slices/active-tab-owner-worktree.test.ts | 2 +- .../store/slices/active-tab-owner-worktree.ts | 2 +- .../slices/agent-status-drop-ipc.test.ts | 2 +- .../store/slices/agent-status-drop.test.ts | 2 +- .../slices/agent-status-retained-leak.test.ts | 2 +- src/renderer/src/store/slices/agent-status.ts | 2 +- .../slices/browser-webview-cleanup.test.ts | 2 +- .../store/slices/browser-webview-cleanup.ts | 2 +- src/renderer/src/store/slices/browser.ts | 8 +- .../store/slices/commit-message-generation.ts | 2 +- .../slices/degraded-repo-hydration.test.ts | 3 +- .../slices/degraded-repo-worktree-validity.ts | 9 +- .../src/store/slices/detected-agents.test.ts | 3 +- .../src/store/slices/detected-agents.ts | 2 +- .../src/store/slices/diffComments.test.ts | 3 +- src/renderer/src/store/slices/diffComments.ts | 4 +- .../slices/direct-ssh-pane-retry-ledger.ts | 2 +- .../direct-ssh-terminal-authority-ledger.ts | 2 +- .../direct-ssh-terminal-recovery-types.ts | 2 +- .../direct-ssh-terminal-recovery.test.ts | 2 +- .../slices/direct-ssh-terminal-recovery.ts | 2 +- ...irect-ssh-terminal-workspace-scope.test.ts | 2 +- .../direct-ssh-terminal-workspace-scope.ts | 2 +- .../slices/editor-branch-line-total.test.ts | 2 +- .../slices/editor-rekey-open-files.test.ts | 2 +- src/renderer/src/store/slices/editor.test.ts | 8 +- src/renderer/src/store/slices/editor.ts | 32 +- ...-workspace-activation-and-activity.test.ts | 3 +- .../folder-workspace-diff-comments.test.ts | 4 +- ...r-workspace-owner-routed-mutations.test.ts | 3 +- .../src/store/slices/github-cache-key.ts | 2 +- .../store/slices/github-checks-cache.test.ts | 3 +- .../src/store/slices/github-checks.test.ts | 2 +- .../src/store/slices/github-checks.ts | 3 +- .../github-pr-refresh-owner-routing.test.ts | 4 +- .../github-pr-refresh-states-leak.test.ts | 5 +- .../slices/github-project-row-owner.test.ts | 2 +- .../store/slices/github-project-row-owner.ts | 3 +- .../slices/github-repo-lookup-index.test.ts | 2 +- .../store/slices/github-repo-lookup-index.ts | 2 +- src/renderer/src/store/slices/github.test.ts | 4 +- src/renderer/src/store/slices/github.ts | 33 +- .../slices/hosted-review-cache-identity.ts | 2 +- .../src/store/slices/hosted-review.ts | 2 +- src/renderer/src/store/slices/jira.test.ts | 2 +- src/renderer/src/store/slices/jira.ts | 2 +- .../store/slices/linear-invalidation.test.ts | 3 +- src/renderer/src/store/slices/linear.test.ts | 12 +- src/renderer/src/store/slices/linear.ts | 14 +- .../slices/local-detected-agent-state.test.ts | 2 +- .../slices/local-detected-agent-state.ts | 2 +- .../local-detected-agent-store-state.ts | 6 +- src/renderer/src/store/slices/memory.test.ts | 2 +- src/renderer/src/store/slices/memory.ts | 2 +- .../src/store/slices/new-issue-draft.test.ts | 2 +- .../src/store/slices/new-issue-draft.ts | 2 +- .../src/store/slices/new-markdown.test.ts | 3 +- .../pane-column-split-drop-no-op.test.ts | 2 +- .../slices/pane-column-split-drop-no-op.ts | 2 +- .../slices/pane-foreground-agent.test.ts | 2 +- .../src/store/slices/pane-foreground-agent.ts | 2 +- .../src/store/slices/preflight.test.ts | 3 +- .../project-group-removal-targets.test.ts | 3 +- .../slices/project-group-removal-targets.ts | 3 +- .../store/slices/pull-request-generation.ts | 2 +- .../purge-stale-runtime-host-state.test.ts | 3 +- .../src/store/slices/remote-server-updates.ts | 2 +- .../src/store/slices/repo-host-identity.ts | 3 +- .../slices/repo-identity-reconcile.test.ts | 2 +- .../store/slices/repo-identity-reconcile.ts | 2 +- .../slices/repo-reorder-host-split.test.ts | 2 +- .../store/slices/repo-reorder-host-split.ts | 3 +- .../repos-all-hosts-folder-workspaces.test.ts | 4 +- .../slices/repos-all-hosts-generation.test.ts | 2 +- .../src/store/slices/repos-all-hosts.test.ts | 11 +- .../repos-catalog-merge-identity.test.ts | 4 +- ...epos-cross-host-project-collisions.test.ts | 2 +- .../repos-cross-host-refresh-identity.test.ts | 3 +- .../repos-host-identity-routing.test.ts | 3 +- .../repos-manual-order-hydration.test.ts | 2 +- .../repos-nested-ssh-projection.test.ts | 2 +- .../repos-project-groups-delete.test.ts | 4 +- .../store/slices/repos-project-groups.test.ts | 9 +- .../repos-project-host-capability.test.ts | 2 +- .../repos-project-host-lifecycle.test.ts | 3 +- .../slices/repos-project-runtime.test.ts | 3 +- .../slices/repos-refresh-identity.test.ts | 12 +- ...epos-remove-missing-remote-project.test.ts | 2 +- .../repos-remove-project-purge-leak.test.ts | 2 +- .../repos-runtime-project-groups.test.ts | 2 +- .../slices/repos-runtime-routing-fixture.ts | 2 +- .../repos-selected-owner-routing.test.ts | 4 +- .../repos-setup-script-dismissals.test.ts | 2 +- .../repos-shared-project-badge-color.test.ts | 3 +- .../repos-ssh-host-reconciliation.test.ts | 4 +- .../store/slices/repos-stale-fetch.test.ts | 3 +- .../slices/repos-update-serialization.test.ts | 2 +- .../slices/repos.runtime-fallback.test.ts | 2 +- src/renderer/src/store/slices/repos.test.ts | 3 +- src/renderer/src/store/slices/repos.ts | 22 +- .../restored-editor-owner-reparent.test.ts | 2 +- ...me-catalog-merge-removed-env-guard.test.ts | 2 +- .../store/slices/runtime-detected-agents.ts | 2 +- ...host-purge-session-partition-split.test.ts | 2 +- ...ted-host-active-workspace-identity.test.ts | 2 +- ...-runtime-environments-purge-wiring.test.ts | 2 +- .../src/store/slices/settings.test.ts | 2 +- src/renderer/src/store/slices/settings.ts | 2 +- ...se-presets-repo-removal-purge-leak.test.ts | 3 +- .../src/store/slices/sparse-presets.test.ts | 2 +- .../src/store/slices/sparse-presets.ts | 2 +- src/renderer/src/store/slices/stats.ts | 2 +- .../slices/store-session-cascades.test.ts | 8 +- .../src/store/slices/store-test-helpers.ts | 10 +- .../slices/superseded-ssh-repo-rows.test.ts | 2 +- .../store/slices/superseded-ssh-repo-rows.ts | 2 +- .../slices/tab-group-reference-repair.ts | 2 +- .../src/store/slices/tab-group-state.test.ts | 2 +- .../src/store/slices/tab-group-state.ts | 3 +- .../tabs-hydration-generated-title.test.ts | 2 +- .../tabs-hydration-group-validation.test.ts | 2 +- .../src/store/slices/tabs-hydration.test.ts | 2 +- .../src/store/slices/tabs-hydration.ts | 8 +- src/renderer/src/store/slices/tabs.test.ts | 2 +- src/renderer/src/store/slices/tabs.ts | 8 +- .../terminal-activation-spawn-suppression.ts | 5 +- .../src/store/slices/terminal-helpers.test.ts | 2 +- .../src/store/slices/terminal-helpers.ts | 2 +- .../slices/terminal-orphan-helpers.test.ts | 2 +- ...rminal-pane-detach-agent-retention.test.ts | 2 +- .../terminal-quick-command-hosts.test.ts | 2 +- .../slices/terminal-quick-command-hosts.ts | 2 +- .../slices/terminal-session-row-hydration.ts | 7 +- .../slices/terminal-tab-id-hydration.test.ts | 2 +- .../slices/terminal-tab-retirement.test.ts | 2 +- .../store/slices/terminal-tab-title-batch.ts | 3 +- .../terminal-write-identity-bailouts.test.ts | 2 +- ...ls-hydration-canonical-pty-overlap.test.ts | 4 +- ...terminals-hydration-canonical-rows.test.ts | 2 +- .../store/slices/terminals-hydration.test.ts | 2 +- src/renderer/src/store/slices/terminals.ts | 19 +- src/renderer/src/store/slices/ui.test.ts | 16 +- src/renderer/src/store/slices/ui.ts | 37 +- .../store/slices/worktree-by-id-index.test.ts | 2 +- .../src/store/slices/worktree-by-id-index.ts | 2 +- .../src/store/slices/worktree-helpers.test.ts | 2 +- .../src/store/slices/worktree-helpers.ts | 35 +- .../worktree-listing-branch-switch.test.ts | 2 +- .../slices/worktree-listing-branch-switch.ts | 2 +- .../worktree-nav-history-view-entries.test.ts | 4 +- .../store/slices/worktree-nav-history.test.ts | 3 +- .../src/store/slices/worktree-nav-history.ts | 4 +- .../slices/worktree-removal-maps-leak.test.ts | 2 +- .../src/store/slices/worktrees.test.ts | 14 +- src/renderer/src/store/slices/worktrees.ts | 25 +- .../worktree-diff-comments-selector.test.ts | 4 +- .../store/worktree-diff-comments-selector.ts | 2 +- src/renderer/src/store/worktree-repo-index.ts | 3 +- .../src/web/web-file-mutation-methods.test.ts | 2 +- .../src/web/web-file-mutation-methods.ts | 2 +- src/renderer/src/web/web-preload-api.ts | 29 +- ...time-direct-proxy-live.integration.test.ts | 3 +- .../src/web/web-workspace-session.test.ts | 2 +- src/renderer/src/web/web-workspace-session.ts | 2 +- src/shared/agent-headless-command.ts | 2 +- src/shared/agent-kind.test.ts | 2 +- src/shared/agent-kind.ts | 2 +- src/shared/agent-model-probe-spec.ts | 2 +- .../agent-node-entrypoint-identities.ts | 2 +- src/shared/agent-process-recognition.ts | 2 +- src/shared/agent-row-conversation-name.ts | 2 +- src/shared/agent-session-host-authority.ts | 2 +- src/shared/agent-session-resume.ts | 2 +- src/shared/ai-vault-types.ts | 2 +- src/shared/artifact-sharing-gate.ts | 2 +- .../auto-rename-branch-from-work-settings.ts | 2 +- src/shared/automation-workspace-provenance.ts | 3 +- src/shared/automations-types.ts | 3 +- src/shared/base-ref-search-result.ts | 2 +- src/shared/branch-prefix.ts | 2 +- src/shared/browser-viewport-presets.ts | 2 +- src/shared/cli-workspace-provenance.ts | 3 +- src/shared/codex-pet-sprite-defaults.ts | 2 +- src/shared/codex-reset-credit-scope.test.ts | 2 +- src/shared/codex-reset-credit-scope.ts | 2 +- src/shared/commit-message-agent-spec.ts | 2 +- src/shared/commit-message-generation.ts | 2 +- src/shared/commit-message-plan.ts | 2 +- src/shared/composer-branch-selection.ts | 2 +- src/shared/constants.ts | 19 +- src/shared/dashboard-snapshot.ts | 2 +- .../detected-worktree-provider-contract.ts | 2 +- src/shared/diff-comments-format.ts | 2 +- .../ephemeral-vm-recipe-checkout-mode.test.ts | 2 +- .../ephemeral-vm-recipe-checkout-mode.ts | 2 +- src/shared/ephemeral-vm-recipe-doctor.test.ts | 2 +- src/shared/ephemeral-vm-recipe-doctor.ts | 2 +- .../ephemeral-vm-recipe-lifecycle-payload.ts | 2 +- src/shared/ephemeral-vm-recipe-repo-url.ts | 2 +- src/shared/ephemeral-vm-recipe-runner.ts | 2 +- src/shared/execution-host-registry.ts | 3 +- src/shared/execution-host.ts | 4 +- src/shared/external-worktree-inbox.test.ts | 10 +- src/shared/external-worktree-inbox.ts | 3 +- src/shared/external-worktree-visibility.ts | 2 +- src/shared/folder-workspace-worktree.test.ts | 2 +- src/shared/folder-workspace-worktree.ts | 3 +- src/shared/folder-workspaces.ts | 3 +- src/shared/git-effective-upstream.ts | 2 +- src/shared/git-publish-target-status.ts | 2 +- src/shared/git-push-target-validation.ts | 2 +- src/shared/github/project-types.ts | 10 +- .../pull-request-auto-merge-availability.ts | 2 +- .../pull-request-for-branch-outcome.test.ts | 3 +- .../github/pull-request-for-branch-outcome.ts | 3 +- .../github/pull-request-merge-methods.ts | 2 +- .../gitlab-job-trace-check-details.test.ts | 2 +- src/shared/gitlab-job-trace-check-details.ts | 2 +- src/shared/gitlab-pipeline-checks.test.ts | 2 +- src/shared/gitlab-pipeline-checks.ts | 2 +- src/shared/gitlab-projects.ts | 2 +- src/shared/gitlab-types.ts | 7 +- src/shared/hook-command-source-policy.ts | 2 +- src/shared/host-lineage-contract.ts | 2 +- src/shared/host-repo-catalog-contract.ts | 2 +- src/shared/host-setting-overrides.test.ts | 2 +- src/shared/host-setting-overrides.ts | 2 +- src/shared/hosted-review-github.test.ts | 2 +- src/shared/hosted-review-github.ts | 2 +- src/shared/hosted-review.ts | 2 +- src/shared/keybindings.ts | 2 +- src/shared/linear/links.ts | 3 +- src/shared/local-account-runtime.ts | 2 +- src/shared/local-windows-terminal-runtime.ts | 2 +- src/shared/managed-agent-hook-targets.ts | 2 +- src/shared/manual-repo-order.test.ts | 3 +- src/shared/manual-repo-order.ts | 3 +- .../markdown-document-listing-limits.test.ts | 2 +- .../markdown-document-listing-limits.ts | 2 +- src/shared/nested-repo-telemetry.test.ts | 2 +- src/shared/nested-repo-telemetry.ts | 2 +- src/shared/new-workspace-dialog-repo.test.ts | 2 +- src/shared/new-workspace-dialog-repo.ts | 2 +- src/shared/new-workspace/fork-push-warning.ts | 2 +- .../smart-workspace-linear-intent.ts | 2 +- .../smart-workspace-source-results.ts | 14 +- src/shared/new-workspace/workspace-source.ts | 4 +- src/shared/open-in-applications.ts | 2 +- src/shared/orca-yaml.ts | 2 +- src/shared/osc52-clipboard-settings.ts | 2 +- .../plugins/plugin-vm-recipe-artifact.ts | 2 +- src/shared/pr-check-severity-order.test.ts | 2 +- src/shared/pr-check-severity-order.ts | 2 +- src/shared/pr-check-status.test.ts | 2 +- src/shared/pr-check-status.ts | 3 +- src/shared/pr-checks-fix-prompt.ts | 2 +- src/shared/pr-comment-audience.test.ts | 2 +- src/shared/pr-comment-audience.ts | 2 +- src/shared/pr-comment-groups.test.ts | 2 +- src/shared/pr-comment-groups.ts | 2 +- src/shared/project-groups.test.ts | 2 +- src/shared/project-groups.ts | 3 +- .../project-host-setup-projection.test.ts | 2 +- src/shared/project-host-setup-projection.ts | 10 +- .../project-identity-succession.test.ts | 2 +- src/shared/project-identity-succession.ts | 2 +- src/shared/provider-check-summary.ts | 2 +- src/shared/remote-server-update.ts | 2 +- .../remote-workspace-session-projection.ts | 3 +- src/shared/remote-workspace-types.ts | 2 +- .../renderer-restart-preparation.test.ts | 2 +- src/shared/renderer-restart-preparation.ts | 2 +- src/shared/repo-kind.ts | 2 +- .../repro-7732-gitlab-job-id-dropped.test.ts | 2 +- src/shared/resolved-worktree-lineage.test.ts | 3 +- src/shared/resolved-worktree-lineage.ts | 3 +- src/shared/runtime-client-events.ts | 4 +- src/shared/runtime-types.ts | 30 +- src/shared/search-match-count.test.ts | 2 +- src/shared/search-match-count.ts | 2 +- src/shared/setup-agent-startup-policy.ts | 2 +- src/shared/skills-cli-agent-keys.ts | 2 +- .../source-control-ai-action-recipes.test.ts | 2 +- src/shared/source-control-ai-actions.ts | 2 +- .../source-control-ai-recipe-save.test.ts | 3 +- src/shared/source-control-ai-recipe-save.ts | 3 +- src/shared/source-control-ai-types.ts | 3 +- src/shared/source-control-ai.test.ts | 2 +- src/shared/source-control-ai.ts | 10 +- src/shared/source-control-commit-failure.ts | 2 +- src/shared/source-control-conflict-prompts.ts | 2 +- src/shared/source-control-group-order.ts | 2 +- ...e-control-primary-action-decision-types.ts | 2 +- src/shared/source-control-push-failure.ts | 2 +- src/shared/status-bar-defaults.ts | 2 +- src/shared/tab-title-resolution.ts | 3 +- src/shared/task-preset-query.ts | 2 +- src/shared/task-provider-identity.ts | 2 +- src/shared/task-source-context.ts | 3 +- src/shared/telemetry-events.ts | 10 +- src/shared/terminal-color-scheme-protocol.ts | 2 +- src/shared/terminal-cursor-style-settings.ts | 2 +- src/shared/terminal-custom-themes.ts | 2 +- src/shared/terminal-quick-commands.ts | 2 +- src/shared/terminal-title-agent-type.ts | 2 +- src/shared/text-search.ts | 7 +- src/shared/top-level-view.ts | 2 +- src/shared/tui-agent-config.ts | 2 +- src/shared/tui-agent-detection-commands.ts | 2 +- src/shared/tui-agent-display-names.ts | 2 +- src/shared/tui-agent-launch-command.ts | 2 +- src/shared/tui-agent-launch-defaults.ts | 2 +- src/shared/tui-agent-permissions.ts | 2 +- src/shared/tui-agent-selection.ts | 2 +- src/shared/tui-agent-startup.ts | 2 +- src/shared/types.ts | 366 ------------------ src/shared/workspace-creator-provenance.ts | 2 +- .../workspace-linked-item-equality.test.ts | 2 +- src/shared/workspace-linked-item-schema.ts | 2 +- ...rkspace-linked-item-source-context.test.ts | 2 +- .../workspace-linked-item-source-context.ts | 2 +- src/shared/workspace-linked-item.ts | 2 +- src/shared/workspace-scope.ts | 2 +- .../workspace-session-browser-history.ts | 3 +- .../workspace-session-browser-schema.ts | 2 +- src/shared/workspace-session-salvage.ts | 2 +- src/shared/workspace-session-schema.ts | 12 +- ...workspace-session-terminal-buffers.test.ts | 2 +- .../workspace-session-terminal-buffers.ts | 3 +- ...rkspace-session-terminal-tab-close.test.ts | 4 +- .../workspace-session-terminal-tab-close.ts | 9 +- src/shared/workspace-status-defaults.ts | 2 +- src/shared/workspace-statuses.ts | 2 +- src/shared/worktree/card-properties.ts | 9 +- .../ownership-worktree-base-path.test.ts | 4 +- src/shared/worktree/ownership.test.ts | 5 +- src/shared/worktree/ownership.ts | 13 +- src/shared/worktree/removal.ts | 2 +- .../worktree/visibility-sources.test.ts | 2 +- src/shared/worktree/visibility-sources.ts | 2 +- ...eted-worker-retirement-resume.unit.test.ts | 3 +- .../helpers/pr-comments-sidebar-fixture.ts | 3 +- tests/e2e/helpers/runtime-types.ts | 12 +- tests/e2e/helpers/worktree-registration.ts | 2 +- tests/e2e/linear-url-workspace-entry.spec.ts | 2 +- ...native-chat-ask-user-question-card.spec.ts | 2 +- .../e2e/native-chat-first-flush-race.spec.ts | 2 +- tests/e2e/notification-settings.spec.ts | 2 +- tests/e2e/onboarding.spec.ts | 3 +- .../paired-remote-pane-layout-retry.spec.ts | 2 +- .../e2e/pr11346-selected-runtime-add.spec.ts | 4 +- ...emote-terminal-tab-retirement.unit.test.ts | 2 +- tests/e2e/repo-icon-emoji-picker.spec.ts | 2 +- tests/e2e/settings-agent-awake.spec.ts | 2 +- tests/e2e/settings-display-name-ime.spec.ts | 2 +- .../settings-search-responsiveness.spec.ts | 2 +- ...inal-duplicate-pty-renderer-reveal.spec.ts | 2 +- tests/e2e/worktree-smart-sort.spec.ts | 2 +- 2323 files changed, 4297 insertions(+), 3950 deletions(-) create mode 100644 src/renderer/src/components/terminal-pane/terminal-pane-layout-tree.ts delete mode 100644 src/shared/types.ts diff --git a/config/scripts/remote-shared-control-retirement-probe.ts b/config/scripts/remote-shared-control-retirement-probe.ts index 30edeb9802a..629ba1fddde 100644 --- a/config/scripts/remote-shared-control-retirement-probe.ts +++ b/config/scripts/remote-shared-control-retirement-probe.ts @@ -5,7 +5,7 @@ import { resolveEnvironment, resolveEnvironmentPairingOffer } from '../../src/shared/runtime-environment-store' -import type { MemorySnapshot } from '../../src/shared/types' +import type { MemorySnapshot } from '../../src/shared/process-stats-types' import type { RuntimeStatus } from '../../src/shared/runtime-types' async function main(): Promise { diff --git a/mobile/app/h/[hostId]/index.tsx b/mobile/app/h/[hostId]/index.tsx index 5fd6fb39d43..9cf10c94356 100644 --- a/mobile/app/h/[hostId]/index.tsx +++ b/mobile/app/h/[hostId]/index.tsx @@ -96,7 +96,7 @@ import { WORKSPACE_SORT_OPTIONS as SORT_OPTIONS } from '../../../src/worktree/workspace-list-picker-options' import type { RepoSummary } from '../../../src/worktree/host-worktree-rpc-types' -import type { WorkspaceStatusDefinition } from '../../../../src/shared/types' +import type { WorkspaceStatusDefinition } from '../../../../src/shared/worktree/types' import { DEFAULT_MOBILE_WORKSPACE_STATUSES } from '../../../src/worktree/mobile-workspace-statuses' function isErrorVerdict(v: ConnectionVerdict): boolean { diff --git a/mobile/app/h/[hostId]/session/[worktreeId].tsx b/mobile/app/h/[hostId]/session/[worktreeId].tsx index c0bcca64b98..0ef8a42ee92 100644 --- a/mobile/app/h/[hostId]/session/[worktreeId].tsx +++ b/mobile/app/h/[hostId]/session/[worktreeId].tsx @@ -277,7 +277,8 @@ import { import { colors } from '../../../../src/theme/mobile-theme' import { QuickCommandsTabButton } from '../../../../src/session/QuickCommandsTabButton' import { styles } from '../../../../src/session/mobile-session-styles' -import type { DiffComment, TerminalQuickCommand } from '../../../../../src/shared/types' +import type { DiffComment } from '../../../../../src/shared/diff-comment-types' +import type { TerminalQuickCommand } from '../../../../../src/shared/terminal-quick-command-types' import type { DiffCommentActions, DiffNotesDelivery, diff --git a/mobile/app/h/[hostId]/tasks.tsx b/mobile/app/h/[hostId]/tasks.tsx index 7ef451df8aa..717e4640516 100644 --- a/mobile/app/h/[hostId]/tasks.tsx +++ b/mobile/app/h/[hostId]/tasks.tsx @@ -149,13 +149,13 @@ import { scheduleMobileTaskCopyFeedbackReset } from '../../../src/tasks/mobile-task-copy-feedback-timer' import type { - BaseRefSearchResult, GitHubOwnerRepo, - ProviderCheckSummary, - PersistedTrustedOrcaHooks, - SparsePreset, - TuiAgent -} from '../../../../src/shared/types' + ProviderCheckSummary +} from '../../../../src/shared/github/pull-request-types' +import type { PersistedTrustedOrcaHooks } from '../../../../src/shared/orca-yaml-hook-types' +import type { BaseRefSearchResult } from '../../../../src/shared/repo-types' +import type { TuiAgent } from '../../../../src/shared/tui-agent' +import type { SparsePreset } from '../../../../src/shared/worktree/create-types' import type { SshConnectionState } from '../../../../src/shared/ssh-types' import type { HostedReviewDecision } from '../../../../src/shared/hosted-review' import { diff --git a/mobile/scripts/mock-server-rpc-handlers.ts b/mobile/scripts/mock-server-rpc-handlers.ts index 5971b9dee94..f67db5bc2f2 100644 --- a/mobile/scripts/mock-server-rpc-handlers.ts +++ b/mobile/scripts/mock-server-rpc-handlers.ts @@ -7,7 +7,7 @@ import { applyTerminalQuickCommandMutation, type TerminalQuickCommandMutation } from '../../src/shared/terminal-quick-commands' -import type { TerminalQuickCommand } from '../../src/shared/types' +import type { TerminalQuickCommand } from '../../src/shared/terminal-quick-command-types' import { handleMockFilePreviewRequest } from './mock-server-file-preview-data' import { handleMockGitRequest } from './mock-server-git-state' import { handleMockAccountRequest } from './mock-server-account-rpc' diff --git a/mobile/src/components/MobileAgentIcon.tsx b/mobile/src/components/MobileAgentIcon.tsx index c5557ab3934..7f0cf0e0f91 100644 --- a/mobile/src/components/MobileAgentIcon.tsx +++ b/mobile/src/components/MobileAgentIcon.tsx @@ -1,7 +1,7 @@ import { Image, StyleSheet, Text, View } from 'react-native' import type { ImageSourcePropType } from 'react-native' import { Terminal } from 'lucide-react-native' -import type { TuiAgent } from '../../../src/shared/types' +import type { TuiAgent } from '../../../src/shared/tui-agent' import Svg, { Defs, G, LinearGradient, Path, Stop } from 'react-native-svg' import { colors } from '../theme/mobile-theme' import { MOBILE_AGENT_CATALOG } from '../tasks/mobile-agent-catalog' diff --git a/mobile/src/components/MobileDiffReviewBody.tsx b/mobile/src/components/MobileDiffReviewBody.tsx index 76555aeadc6..cc0a7aa95bd 100644 --- a/mobile/src/components/MobileDiffReviewBody.tsx +++ b/mobile/src/components/MobileDiffReviewBody.tsx @@ -1,7 +1,7 @@ import { ActivityIndicator, FlatList, Pressable, Text, View } from 'react-native' import { RefreshCw } from 'lucide-react-native' import type { RefObject } from 'react' -import type { DiffComment } from '../../../src/shared/types' +import type { DiffComment } from '../../../src/shared/diff-comment-types' import { colors } from '../theme/mobile-theme' import { MobileDiffReviewLine } from './MobileDiffReviewLine' import type { diff --git a/mobile/src/components/MobileDiffReviewDrawers.tsx b/mobile/src/components/MobileDiffReviewDrawers.tsx index c6e05b1cd0a..1ff763fc9b8 100644 --- a/mobile/src/components/MobileDiffReviewDrawers.tsx +++ b/mobile/src/components/MobileDiffReviewDrawers.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react' import { KeyboardAvoidingView, Platform, Pressable, Text, TextInput, View } from 'react-native' import { Check, Copy, FileText, Plus, Send, Trash2, X } from 'lucide-react-native' -import type { DiffComment } from '../../../src/shared/types' +import type { DiffComment } from '../../../src/shared/diff-comment-types' import { colors } from '../theme/mobile-theme' import type { ActionSheetAction } from './ActionSheetModal' import { ActionSheetModal } from './ActionSheetModal' diff --git a/mobile/src/components/MobileDiffReviewFileSummary.tsx b/mobile/src/components/MobileDiffReviewFileSummary.tsx index a27910f4d6f..3645ed60161 100644 --- a/mobile/src/components/MobileDiffReviewFileSummary.tsx +++ b/mobile/src/components/MobileDiffReviewFileSummary.tsx @@ -1,6 +1,6 @@ import { Pressable, Text, View } from 'react-native' import { ArrowDown, ArrowUp } from 'lucide-react-native' -import type { DiffComment } from '../../../src/shared/types' +import type { DiffComment } from '../../../src/shared/diff-comment-types' import { colors } from '../theme/mobile-theme' import type { MobileDiffReviewQueueItem } from '../session/mobile-diff-review-queue' import { MOBILE_GIT_STATUS_LABELS } from '../source-control/mobile-git-status' diff --git a/mobile/src/components/MobileDiffReviewLine.tsx b/mobile/src/components/MobileDiffReviewLine.tsx index 1c1008321f6..ae882d56525 100644 --- a/mobile/src/components/MobileDiffReviewLine.tsx +++ b/mobile/src/components/MobileDiffReviewLine.tsx @@ -1,6 +1,6 @@ import { Pressable, StyleSheet, Text, View } from 'react-native' import { MessageSquare } from 'lucide-react-native' -import type { DiffComment } from '../../../src/shared/types' +import type { DiffComment } from '../../../src/shared/diff-comment-types' import type { MobileDiffLine } from '../session/mobile-diff-lines' import type { MobileHighlightedDiffLine } from '../session/mobile-file-syntax' import { mobileDiffLineNumber, mobileDiffLinePrefix } from '../source-control/mobile-diff-format' diff --git a/mobile/src/components/NewWorktreeModal.tsx b/mobile/src/components/NewWorktreeModal.tsx index 3fca3261ecc..8e484141fbf 100644 --- a/mobile/src/components/NewWorktreeModal.tsx +++ b/mobile/src/components/NewWorktreeModal.tsx @@ -29,7 +29,8 @@ import { type SetupHookTrust } from '../tasks/setup-hook-trust' import { isMobileTuiAgentEnabled } from '../tasks/mobile-tui-agents' -import type { PersistedTrustedOrcaHooks, TuiAgent } from '../../../src/shared/types' +import type { PersistedTrustedOrcaHooks } from '../../../src/shared/orca-yaml-hook-types' +import type { TuiAgent } from '../../../src/shared/tui-agent' import type { SshConnectionState } from '../../../src/shared/ssh-types' import { NEW_WORKTREE_AGENT_OPTIONS as AGENT_OPTIONS, diff --git a/mobile/src/components/mobile-agent-icon-assets.test.ts b/mobile/src/components/mobile-agent-icon-assets.test.ts index edbe71760c7..f4f6325d806 100644 --- a/mobile/src/components/mobile-agent-icon-assets.test.ts +++ b/mobile/src/components/mobile-agent-icon-assets.test.ts @@ -1,7 +1,7 @@ import { existsSync, readFileSync } from 'node:fs' import path from 'node:path' import { describe, expect, it } from 'vitest' -import type { TuiAgent } from '../../../src/shared/types' +import type { TuiAgent } from '../../../src/shared/tui-agent' import { MOBILE_TUI_AGENT_FAVICON_DOMAINS } from '../tasks/mobile-tui-agents' // Agents that render a hand-authored glyph in MobileAgentIcon and therefore diff --git a/mobile/src/components/mobile-agent-icon-assets.ts b/mobile/src/components/mobile-agent-icon-assets.ts index 3f93b3dfaeb..38a48373734 100644 --- a/mobile/src/components/mobile-agent-icon-assets.ts +++ b/mobile/src/components/mobile-agent-icon-assets.ts @@ -1,5 +1,5 @@ import type { ImageSourcePropType } from 'react-native' -import type { TuiAgent } from '../../../src/shared/types' +import type { TuiAgent } from '../../../src/shared/tui-agent' // Why: mobile previously rendered these agent icons from Google's favicon // service, which is unreachable in some regions (e.g. mainland China) and diff --git a/mobile/src/components/new-worktree-agent-selection.ts b/mobile/src/components/new-worktree-agent-selection.ts index 22101c00a30..1e8836a5fed 100644 --- a/mobile/src/components/new-worktree-agent-selection.ts +++ b/mobile/src/components/new-worktree-agent-selection.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../src/shared/types' +import type { TuiAgent } from '../../../src/shared/tui-agent' import { MOBILE_AGENT_CATALOG } from '../tasks/mobile-agent-catalog' import { isMobileTuiAgentEnabled } from '../tasks/mobile-tui-agents' import { pickWorkspaceAgent } from '../tasks/workspace-agent-selection' diff --git a/mobile/src/components/pr-sidebar/PRActionsSection.tsx b/mobile/src/components/pr-sidebar/PRActionsSection.tsx index 5fae6803889..8f331ef11ba 100644 --- a/mobile/src/components/pr-sidebar/PRActionsSection.tsx +++ b/mobile/src/components/pr-sidebar/PRActionsSection.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from 'react' import { ActivityIndicator, Pressable, Text, View } from 'react-native' import { GitMerge, Link2Off } from 'lucide-react-native' import { colors } from '../../theme/mobile-theme' -import type { GitHubPRMergeMethod, PRInfo } from '../../../../src/shared/types' +import type { GitHubPRMergeMethod, PRInfo } from '../../../../src/shared/github/pull-request-types' import type { RpcClient } from '../../transport/rpc-client' import type { MobilePrActions } from '../../session/use-mobile-pr-actions' import { unlinkMobilePr } from '../../source-control/mobile-pr-link' diff --git a/mobile/src/components/pr-sidebar/PRCheckDetail.tsx b/mobile/src/components/pr-sidebar/PRCheckDetail.tsx index 7086681c434..3023cdce72c 100644 --- a/mobile/src/components/pr-sidebar/PRCheckDetail.tsx +++ b/mobile/src/components/pr-sidebar/PRCheckDetail.tsx @@ -1,6 +1,6 @@ import { ActivityIndicator, ScrollView, Text, View } from 'react-native' import { colors } from '../../theme/mobile-theme' -import type { PRCheckRunDetails } from '../../../../src/shared/types' +import type { PRCheckRunDetails } from '../../../../src/shared/github/check-types' import { presentCheckDetail, type CheckDetailJob } from './pr-check-detail-content' import { mobilePrSidebarStyles as styles } from './mobile-pr-sidebar-styles' diff --git a/mobile/src/components/pr-sidebar/PRChecksSection.tsx b/mobile/src/components/pr-sidebar/PRChecksSection.tsx index 53ac41655c5..12c08929c41 100644 --- a/mobile/src/components/pr-sidebar/PRChecksSection.tsx +++ b/mobile/src/components/pr-sidebar/PRChecksSection.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { ActivityIndicator, Linking, Pressable, Text, View } from 'react-native' import { ChevronDown, ChevronRight, ExternalLink, RotateCw, Sparkles } from 'lucide-react-native' import { colors } from '../../theme/mobile-theme' -import type { PRCheckDetail } from '../../../../src/shared/types' +import type { PRCheckDetail } from '../../../../src/shared/github/check-types' import type { RpcClient } from '../../transport/rpc-client' import { fetchPRCheckDetails, type GitHubPrRepoSlug } from '../../session/github-pr-rpc' import type { MobilePrActions } from '../../session/use-mobile-pr-actions' diff --git a/mobile/src/components/pr-sidebar/PRCommentCard.tsx b/mobile/src/components/pr-sidebar/PRCommentCard.tsx index 23d768b7814..5aee8cc0a00 100644 --- a/mobile/src/components/pr-sidebar/PRCommentCard.tsx +++ b/mobile/src/components/pr-sidebar/PRCommentCard.tsx @@ -1,7 +1,11 @@ import { memo, useState } from 'react' import { Image, Linking, Pressable, Text, View } from 'react-native' import { Check, CornerDownRight, ExternalLink, Pencil, Trash2, Undo2 } from 'lucide-react-native' -import type { GitHubReaction, GitHubReactionContent, PRComment } from '../../../../src/shared/types' +import type { + GitHubReaction, + GitHubReactionContent, + PRComment +} from '../../../../src/shared/github/comment-types' import { colors } from '../../theme/mobile-theme' import { canEditComment, isResolvableComment } from '../../session/pr-comment-actions' import { ConfirmModal } from '../ConfirmModal' diff --git a/mobile/src/components/pr-sidebar/PRCommentsSection.test.ts b/mobile/src/components/pr-sidebar/PRCommentsSection.test.ts index 08fc02c357b..e01b251ce05 100644 --- a/mobile/src/components/pr-sidebar/PRCommentsSection.test.ts +++ b/mobile/src/components/pr-sidebar/PRCommentsSection.test.ts @@ -1,7 +1,8 @@ import { createElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { GitHubWorkItemDetails, PRComment } from '../../../../src/shared/types' +import type { PRComment } from '../../../../src/shared/github/comment-types' +import type { GitHubWorkItemDetails } from '../../../../src/shared/github/work-item-types' import { PRCommentsSection } from './PRCommentsSection' vi.mock('react-native', () => ({ diff --git a/mobile/src/components/pr-sidebar/PRCommentsSection.tsx b/mobile/src/components/pr-sidebar/PRCommentsSection.tsx index 8fb5de31037..b8ce48362f2 100644 --- a/mobile/src/components/pr-sidebar/PRCommentsSection.tsx +++ b/mobile/src/components/pr-sidebar/PRCommentsSection.tsx @@ -1,7 +1,8 @@ import { useMemo, useState } from 'react' import { ActivityIndicator, Pressable, Text, View } from 'react-native' import { ChevronDown, ChevronRight } from 'lucide-react-native' -import type { GitHubWorkItemDetails, PRState } from '../../../../src/shared/types' +import type { PRState } from '../../../../src/shared/github/pull-request-types' +import type { GitHubWorkItemDetails } from '../../../../src/shared/github/work-item-types' import type { GitHubPrRepoSlug } from '../../session/github-pr-rpc' import { colors } from '../../theme/mobile-theme' import { canAddRootComment } from '../../session/pr-comment-actions' diff --git a/mobile/src/components/pr-sidebar/PRConflictingFilesSection.tsx b/mobile/src/components/pr-sidebar/PRConflictingFilesSection.tsx index 95c95d30573..0a95854d4fd 100644 --- a/mobile/src/components/pr-sidebar/PRConflictingFilesSection.tsx +++ b/mobile/src/components/pr-sidebar/PRConflictingFilesSection.tsx @@ -3,7 +3,7 @@ import { ActivityIndicator, Pressable, ScrollView, Text, View } from 'react-nati import * as Clipboard from 'expo-clipboard' import { Check, Copy, FileWarning, Sparkles } from 'lucide-react-native' import { colors } from '../../theme/mobile-theme' -import type { PRInfo } from '../../../../src/shared/types' +import type { PRInfo } from '../../../../src/shared/github/pull-request-types' import { PRSection } from './PRSection' import { resolveConflictDisplay } from './pr-conflict-presentation' import { prConflictStyles as styles } from './pr-conflict-styles' diff --git a/mobile/src/components/pr-sidebar/PRReviewersSection.tsx b/mobile/src/components/pr-sidebar/PRReviewersSection.tsx index bc09ecd3502..d374dd629c8 100644 --- a/mobile/src/components/pr-sidebar/PRReviewersSection.tsx +++ b/mobile/src/components/pr-sidebar/PRReviewersSection.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react' import { ActivityIndicator, Pressable, Text, View } from 'react-native' import { UserPlus, X } from 'lucide-react-native' import { colors } from '../../theme/mobile-theme' -import type { GitHubWorkItemDetails } from '../../../../src/shared/types' +import type { GitHubWorkItemDetails } from '../../../../src/shared/github/work-item-types' import type { RpcClient } from '../../transport/rpc-client' import type { MobilePrActions } from '../../session/use-mobile-pr-actions' import { isPrSidebarDetailsPlaceholder } from '../../session/mobile-pr-sidebar-state' diff --git a/mobile/src/components/pr-sidebar/PRSidebarHeader.tsx b/mobile/src/components/pr-sidebar/PRSidebarHeader.tsx index 335108e07ae..c053e9b2a65 100644 --- a/mobile/src/components/pr-sidebar/PRSidebarHeader.tsx +++ b/mobile/src/components/pr-sidebar/PRSidebarHeader.tsx @@ -2,7 +2,8 @@ import { useState } from 'react' import { ActivityIndicator, Pressable, Text, TextInput, View } from 'react-native' import { ArrowRight, ExternalLink, Pencil } from 'lucide-react-native' import { colors } from '../../theme/mobile-theme' -import type { GitHubWorkItemDetails, PRInfo } from '../../../../src/shared/types' +import type { PRInfo } from '../../../../src/shared/github/pull-request-types' +import type { GitHubWorkItemDetails } from '../../../../src/shared/github/work-item-types' import type { MobilePrTitleAction } from '../../session/use-mobile-pr-title-action' import { prStateBadge } from './pr-checks-presentation' import { statusColor } from './pr-sidebar-status-color' diff --git a/mobile/src/components/pr-sidebar/ReviewerPickerDrawer.tsx b/mobile/src/components/pr-sidebar/ReviewerPickerDrawer.tsx index 8a3a161f358..6cdb63ca38e 100644 --- a/mobile/src/components/pr-sidebar/ReviewerPickerDrawer.tsx +++ b/mobile/src/components/pr-sidebar/ReviewerPickerDrawer.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react' import { ActivityIndicator, Pressable, Text, TextInput, View } from 'react-native' import { Check } from 'lucide-react-native' import { colors } from '../../theme/mobile-theme' -import type { GitHubAssignableUser } from '../../../../src/shared/types' +import type { GitHubAssignableUser } from '../../../../src/shared/github/pull-request-types' import type { RpcClient } from '../../transport/rpc-client' import { fetchAssignableUsers } from '../../session/github-pr-rpc' import { BottomDrawer } from '../BottomDrawer' diff --git a/mobile/src/components/pr-sidebar/pr-actions-state.ts b/mobile/src/components/pr-sidebar/pr-actions-state.ts index 7aa1456bc55..f2bd679af1a 100644 --- a/mobile/src/components/pr-sidebar/pr-actions-state.ts +++ b/mobile/src/components/pr-sidebar/pr-actions-state.ts @@ -1,4 +1,4 @@ -import type { GitHubPRMergeMethod, PRState } from '../../../../src/shared/types' +import type { GitHubPRMergeMethod, PRState } from '../../../../src/shared/github/pull-request-types' // Which actions the PR actions section may offer for a given PR state. Merged PRs // expose only unlink (+ open-on-host elsewhere); closed PRs add reopen; open/draft diff --git a/mobile/src/components/pr-sidebar/pr-auto-merge-availability.ts b/mobile/src/components/pr-sidebar/pr-auto-merge-availability.ts index 5453a0209b6..166e6e0c189 100644 --- a/mobile/src/components/pr-sidebar/pr-auto-merge-availability.ts +++ b/mobile/src/components/pr-sidebar/pr-auto-merge-availability.ts @@ -1,4 +1,4 @@ -import type { PRInfo } from '../../../../src/shared/types' +import type { PRInfo } from '../../../../src/shared/github/pull-request-types' type MobilePRAutoMergeAvailabilityInput = Pick< PRInfo, diff --git a/mobile/src/components/pr-sidebar/pr-check-detail-content.test.ts b/mobile/src/components/pr-sidebar/pr-check-detail-content.test.ts index 8956a90b4b8..5876405d1bb 100644 --- a/mobile/src/components/pr-sidebar/pr-check-detail-content.test.ts +++ b/mobile/src/components/pr-sidebar/pr-check-detail-content.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { PRCheckRunDetails } from '../../../../src/shared/types' +import type { PRCheckRunDetails } from '../../../../src/shared/github/check-types' import { presentCheckDetail } from './pr-check-detail-content' function details(over: Partial): PRCheckRunDetails { diff --git a/mobile/src/components/pr-sidebar/pr-check-detail-content.ts b/mobile/src/components/pr-sidebar/pr-check-detail-content.ts index b1f63cbd370..0a99fc23709 100644 --- a/mobile/src/components/pr-sidebar/pr-check-detail-content.ts +++ b/mobile/src/components/pr-sidebar/pr-check-detail-content.ts @@ -3,7 +3,7 @@ import type { PRCheckJob, PRCheckRunDetails, PRCheckStep -} from '../../../../src/shared/types' +} from '../../../../src/shared/github/check-types' // Pure mapping from the github.prCheckDetails payload to the rows the mobile // expanded check detail renders. No React/native imports so it stays unit-testable diff --git a/mobile/src/components/pr-sidebar/pr-checks-presentation.test.ts b/mobile/src/components/pr-sidebar/pr-checks-presentation.test.ts index 044644b7dc4..cf40a6b323e 100644 --- a/mobile/src/components/pr-sidebar/pr-checks-presentation.test.ts +++ b/mobile/src/components/pr-sidebar/pr-checks-presentation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { PRCheckDetail } from '../../../../src/shared/types' +import type { PRCheckDetail } from '../../../../src/shared/github/check-types' import { checkOutcome, firstFailingCheckKey, diff --git a/mobile/src/components/pr-sidebar/pr-checks-presentation.ts b/mobile/src/components/pr-sidebar/pr-checks-presentation.ts index e929a13bf40..277680fb9a5 100644 --- a/mobile/src/components/pr-sidebar/pr-checks-presentation.ts +++ b/mobile/src/components/pr-sidebar/pr-checks-presentation.ts @@ -1,4 +1,8 @@ -import type { PRCheckDetail, PRState, ProviderCheckSummary } from '../../../../src/shared/types' +import type { PRCheckDetail } from '../../../../src/shared/github/check-types' +import type { + PRState, + ProviderCheckSummary +} from '../../../../src/shared/github/pull-request-types' import { classifyCheckOutcome, summarizeProviderChecks, diff --git a/mobile/src/components/pr-sidebar/pr-conflict-presentation.test.ts b/mobile/src/components/pr-sidebar/pr-conflict-presentation.test.ts index 07172a52ae3..8f68ead0d6f 100644 --- a/mobile/src/components/pr-sidebar/pr-conflict-presentation.test.ts +++ b/mobile/src/components/pr-sidebar/pr-conflict-presentation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { PRInfo } from '../../../../src/shared/types' +import type { PRInfo } from '../../../../src/shared/github/pull-request-types' import { buildMergeabilityRefreshCommands, hasMergeConflicts, diff --git a/mobile/src/components/pr-sidebar/pr-conflict-presentation.ts b/mobile/src/components/pr-sidebar/pr-conflict-presentation.ts index 0b227806519..54d2e33cb80 100644 --- a/mobile/src/components/pr-sidebar/pr-conflict-presentation.ts +++ b/mobile/src/components/pr-sidebar/pr-conflict-presentation.ts @@ -1,4 +1,4 @@ -import type { PRInfo } from '../../../../src/shared/types' +import type { PRInfo } from '../../../../src/shared/github/pull-request-types' // Pure presentation logic for the PR sidebar's conflicting-files section. No // React/native imports so it is unit-testable under the node Vitest config (KTD5). diff --git a/mobile/src/session/QuickCommandRow.tsx b/mobile/src/session/QuickCommandRow.tsx index 401305b298c..8815d4da5f0 100644 --- a/mobile/src/session/QuickCommandRow.tsx +++ b/mobile/src/session/QuickCommandRow.tsx @@ -4,7 +4,7 @@ import * as Clipboard from 'expo-clipboard' import { Check, Copy, Pencil, Play, Trash2 } from 'lucide-react-native' import { colors, spacing, typography } from '../theme/mobile-theme' import { MobileAgentIcon } from '../components/MobileAgentIcon' -import type { TerminalQuickCommand } from '../../../src/shared/types' +import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types' import { getQuickCommandDisplayPreview, getTerminalQuickCommandBody, diff --git a/mobile/src/session/QuickCommandsList.tsx b/mobile/src/session/QuickCommandsList.tsx index e8781163691..02a6809bb60 100644 --- a/mobile/src/session/QuickCommandsList.tsx +++ b/mobile/src/session/QuickCommandsList.tsx @@ -3,7 +3,8 @@ import { Check, Plus, Search } from 'lucide-react-native' import { colors, spacing } from '../theme/mobile-theme' import { MobileAgentIcon } from '../components/MobileAgentIcon' import { MOBILE_AGENT_CATALOG } from '../tasks/mobile-agent-catalog' -import type { TerminalQuickCommand, TuiAgent } from '../../../src/shared/types' +import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types' +import type { TuiAgent } from '../../../src/shared/tui-agent' import { supportsTerminalAgentQuickCommand } from '../terminal/quick-commands' import { QuickCommandRow } from './QuickCommandRow' diff --git a/mobile/src/session/QuickCommandsSheet.test.ts b/mobile/src/session/QuickCommandsSheet.test.ts index 9951742b64f..2ce520a4eaf 100644 --- a/mobile/src/session/QuickCommandsSheet.test.ts +++ b/mobile/src/session/QuickCommandsSheet.test.ts @@ -1,7 +1,7 @@ import { createElement, type ReactNode } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { TerminalQuickCommand } from '../../../src/shared/types' +import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types' import type { RpcClient } from '../transport/rpc-client' import { MAX_QUICK_COMMANDS } from '../terminal/quick-commands' import { QuickCommandsSheet } from './QuickCommandsSheet' diff --git a/mobile/src/session/QuickCommandsSheet.tsx b/mobile/src/session/QuickCommandsSheet.tsx index c4dbff9473d..3342f122665 100644 --- a/mobile/src/session/QuickCommandsSheet.tsx +++ b/mobile/src/session/QuickCommandsSheet.tsx @@ -4,7 +4,7 @@ import { ChevronLeft } from 'lucide-react-native' import { colors, spacing } from '../theme/mobile-theme' import { BottomDrawer } from '../components/BottomDrawer' import type { RpcClient } from '../transport/rpc-client' -import type { TerminalQuickCommand } from '../../../src/shared/types' +import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types' import { getQuickCommandPreview, MAX_QUICK_COMMANDS, diff --git a/mobile/src/session/ai-vault-resume-launch.ts b/mobile/src/session/ai-vault-resume-launch.ts index aa8c1bf98a6..a1a21565037 100644 --- a/mobile/src/session/ai-vault-resume-launch.ts +++ b/mobile/src/session/ai-vault-resume-launch.ts @@ -13,7 +13,7 @@ import { resolveTuiAgentLaunchEnv } from '../../../src/shared/tui-agent-launch-defaults' import { normalizeAiVaultResumeFilePath } from '../../../src/shared/ai-vault-resume-path' -import type { TuiAgent } from '../../../src/shared/types' +import type { TuiAgent } from '../../../src/shared/tui-agent' import { parseWslUncPath } from '../../../src/shared/wsl-paths' import { resolveWindowsShellStartupFamily } from '../../../src/shared/windows-terminal-shell' import type { RpcClient } from '../transport/rpc-client' diff --git a/mobile/src/session/github-pr-comment-parsers.ts b/mobile/src/session/github-pr-comment-parsers.ts index 93f6eab6f31..5d5d49b746f 100644 --- a/mobile/src/session/github-pr-comment-parsers.ts +++ b/mobile/src/session/github-pr-comment-parsers.ts @@ -1,4 +1,8 @@ -import type { GitHubReaction, GitHubReactionContent, PRComment } from '../../../src/shared/types' +import type { + GitHubReaction, + GitHubReactionContent, + PRComment +} from '../../../src/shared/github/comment-types' import { isRecord, readBoolean, readNumber, readString } from './github-pr-value-readers' // Defensive parsers for the PR conversation comments carried by diff --git a/mobile/src/session/github-pr-mutations.ts b/mobile/src/session/github-pr-mutations.ts index d9fa04ce06a..bd1d9c423b9 100644 --- a/mobile/src/session/github-pr-mutations.ts +++ b/mobile/src/session/github-pr-mutations.ts @@ -1,4 +1,4 @@ -import type { GitHubPRMergeMethod } from '../../../src/shared/types' +import type { GitHubPRMergeMethod } from '../../../src/shared/github/pull-request-types' import type { RpcClient } from '../transport/rpc-client' import { buildGithubPrParams, githubPrRepoSlugParam, type GitHubPrRepoSlug } from './github-pr-rpc' diff --git a/mobile/src/session/github-pr-parsers.ts b/mobile/src/session/github-pr-parsers.ts index 25c516f7106..05a59200d1c 100644 --- a/mobile/src/session/github-pr-parsers.ts +++ b/mobile/src/session/github-pr-parsers.ts @@ -1,15 +1,19 @@ import type { - GitHubAssignableUser, - GitHubPRReviewSummary, - GitHubWorkItem, - GitHubWorkItemDetails, PRCheckAnnotation, PRCheckDetail, PRCheckJob, PRCheckRunDetails, - PRCheckStep, + PRCheckStep +} from '../../../src/shared/github/check-types' +import type { + GitHubAssignableUser, + GitHubPRReviewSummary, PRInfo -} from '../../../src/shared/types' +} from '../../../src/shared/github/pull-request-types' +import type { + GitHubWorkItem, + GitHubWorkItemDetails +} from '../../../src/shared/github/work-item-types' import { readPRComments } from './github-pr-comment-parsers' import type { HostedReviewInfo } from '../../../src/shared/hosted-review' import { diff --git a/mobile/src/session/github-pr-rpc.ts b/mobile/src/session/github-pr-rpc.ts index a34ed49581c..485184993ec 100644 --- a/mobile/src/session/github-pr-rpc.ts +++ b/mobile/src/session/github-pr-rpc.ts @@ -1,10 +1,6 @@ -import type { - GitHubAssignableUser, - GitHubWorkItemDetails, - PRCheckDetail, - PRCheckRunDetails, - PRInfo -} from '../../../src/shared/types' +import type { PRCheckDetail, PRCheckRunDetails } from '../../../src/shared/github/check-types' +import type { GitHubAssignableUser, PRInfo } from '../../../src/shared/github/pull-request-types' +import type { GitHubWorkItemDetails } from '../../../src/shared/github/work-item-types' import type { HostedReviewInfo } from '../../../src/shared/hosted-review' import { normalizeGitHubPRForBranchOutcome, diff --git a/mobile/src/session/github-pr-value-readers.ts b/mobile/src/session/github-pr-value-readers.ts index 9074d94142c..083601923b8 100644 --- a/mobile/src/session/github-pr-value-readers.ts +++ b/mobile/src/session/github-pr-value-readers.ts @@ -1,16 +1,16 @@ +import type { PRCheckDetail } from '../../../src/shared/github/check-types' import type { CheckStatus, GitHubAssignableUser, - ProviderCheckSummary, GitHubPRMergeMethod, GitHubPRMergeMethodSettings, GitHubPRReviewSummary, GitHubRepositoryIdentity, - PRCheckDetail, PRMergeableState, PRReviewDecision, - PRState -} from '../../../src/shared/types' + PRState, + ProviderCheckSummary +} from '../../../src/shared/github/pull-request-types' import type { HostedReviewProvider } from '../../../src/shared/hosted-review' // Primitive + enum value readers shared by the github.* PR parsers. Each narrows diff --git a/mobile/src/session/mobile-diff-comment-edit.test.ts b/mobile/src/session/mobile-diff-comment-edit.test.ts index 8e6482d8bb0..49328064cd5 100644 --- a/mobile/src/session/mobile-diff-comment-edit.test.ts +++ b/mobile/src/session/mobile-diff-comment-edit.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { DiffComment } from '../../../src/shared/types' +import type { DiffComment } from '../../../src/shared/diff-comment-types' import { clearSentMobileDiffComments, countUnsentMobileDiffComments, diff --git a/mobile/src/session/mobile-diff-comment-edit.ts b/mobile/src/session/mobile-diff-comment-edit.ts index 3da63af3e1e..93ba15c0844 100644 --- a/mobile/src/session/mobile-diff-comment-edit.ts +++ b/mobile/src/session/mobile-diff-comment-edit.ts @@ -1,4 +1,4 @@ -import type { DiffComment } from '../../../src/shared/types' +import type { DiffComment } from '../../../src/shared/diff-comment-types' export type UpdateMobileDiffCommentInput = { id: string diff --git a/mobile/src/session/mobile-diff-comments.test.ts b/mobile/src/session/mobile-diff-comments.test.ts index e86d6e81bab..8897b5e32e4 100644 --- a/mobile/src/session/mobile-diff-comments.test.ts +++ b/mobile/src/session/mobile-diff-comments.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { DiffComment } from '../../../src/shared/types' +import type { DiffComment } from '../../../src/shared/diff-comment-types' import { addMobileDiffComment, formatDiffComments, diff --git a/mobile/src/session/mobile-diff-comments.ts b/mobile/src/session/mobile-diff-comments.ts index 7fc7140fd42..333895ea95a 100644 --- a/mobile/src/session/mobile-diff-comments.ts +++ b/mobile/src/session/mobile-diff-comments.ts @@ -1,4 +1,4 @@ -import type { DiffComment, DiffReviewScope } from '../../../src/shared/types' +import type { DiffComment, DiffReviewScope } from '../../../src/shared/diff-comment-types' import { formatDiffComment, formatDiffComments } from '../../../src/shared/diff-comments-format' export { formatDiffComment, formatDiffComments } diff --git a/mobile/src/session/mobile-diff-review-positioning.ts b/mobile/src/session/mobile-diff-review-positioning.ts index afb03381a2d..63932310767 100644 --- a/mobile/src/session/mobile-diff-review-positioning.ts +++ b/mobile/src/session/mobile-diff-review-positioning.ts @@ -1,4 +1,4 @@ -import type { DiffReviewScope } from '../../../src/shared/types' +import type { DiffReviewScope } from '../../../src/shared/diff-comment-types' import type { MobileGitStagingArea } from '../source-control/mobile-git-status' import { createMobileDiffReviewFileKey, diff --git a/mobile/src/session/mobile-diff-review-queue.test.ts b/mobile/src/session/mobile-diff-review-queue.test.ts index 551f378d142..1286e22cce4 100644 --- a/mobile/src/session/mobile-diff-review-queue.test.ts +++ b/mobile/src/session/mobile-diff-review-queue.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/types' +import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/diff-comment-types' import type { MobileGitBranchChangeEntry } from '../source-control/mobile-branch-compare' import type { MobileGitStatusEntry } from '../source-control/mobile-git-status' import { diff --git a/mobile/src/session/mobile-diff-review-queue.ts b/mobile/src/session/mobile-diff-review-queue.ts index a87c56dab3b..f66c7c04bb7 100644 --- a/mobile/src/session/mobile-diff-review-queue.ts +++ b/mobile/src/session/mobile-diff-review-queue.ts @@ -1,4 +1,8 @@ -import type { DiffComment, DiffReviewScope, MobileDiffReviewState } from '../../../src/shared/types' +import type { + DiffComment, + DiffReviewScope, + MobileDiffReviewState +} from '../../../src/shared/diff-comment-types' import type { MobileGitBranchChangeEntry } from '../source-control/mobile-branch-compare' import { isMobileGitDiscardableEntry, diff --git a/mobile/src/session/mobile-diff-review-screen-model.ts b/mobile/src/session/mobile-diff-review-screen-model.ts index f8f1ceedf99..ecf09bf3efb 100644 --- a/mobile/src/session/mobile-diff-review-screen-model.ts +++ b/mobile/src/session/mobile-diff-review-screen-model.ts @@ -1,4 +1,4 @@ -import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/types' +import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/diff-comment-types' import type { MobileGitBranchCompareResult } from '../source-control/mobile-branch-compare' import type { MobileGitStatusResult } from '../source-control/mobile-git-status' import type { MobileDiffLine } from './mobile-diff-lines' diff --git a/mobile/src/session/mobile-diff-review-state.ts b/mobile/src/session/mobile-diff-review-state.ts index a9680a66df5..01361b9dfb9 100644 --- a/mobile/src/session/mobile-diff-review-state.ts +++ b/mobile/src/session/mobile-diff-review-state.ts @@ -2,7 +2,7 @@ import type { DiffReviewScope, MobileDiffReviewFileState, MobileDiffReviewState -} from '../../../src/shared/types' +} from '../../../src/shared/diff-comment-types' export type MobileDiffReviewFileDescriptor = { key: string diff --git a/mobile/src/session/mobile-new-tab-agent-options.ts b/mobile/src/session/mobile-new-tab-agent-options.ts index 51de5b610f2..82e06bd8f30 100644 --- a/mobile/src/session/mobile-new-tab-agent-options.ts +++ b/mobile/src/session/mobile-new-tab-agent-options.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../src/shared/types' +import type { TuiAgent } from '../../../src/shared/tui-agent' import { filterEnabledMobileTuiAgents, isMobileTuiAgent, diff --git a/mobile/src/session/mobile-pr-sidebar-state.ts b/mobile/src/session/mobile-pr-sidebar-state.ts index 1b4d9a1b50b..4ec65a7e2d7 100644 --- a/mobile/src/session/mobile-pr-sidebar-state.ts +++ b/mobile/src/session/mobile-pr-sidebar-state.ts @@ -1,4 +1,6 @@ -import type { GitHubWorkItemDetails, PRCheckDetail, PRInfo } from '../../../src/shared/types' +import type { PRCheckDetail } from '../../../src/shared/github/check-types' +import type { PRInfo } from '../../../src/shared/github/pull-request-types' +import type { GitHubWorkItemDetails } from '../../../src/shared/github/work-item-types' import type { GitHubPrReadOutcome, GitHubPrRepoSlug } from './github-pr-rpc' import { resolveLinkedPrNumber } from './mobile-pr-sidebar-resolve' diff --git a/mobile/src/session/mobile-session-route-types.ts b/mobile/src/session/mobile-session-route-types.ts index 7e74a5d04c0..65d4d14d3fe 100644 --- a/mobile/src/session/mobile-session-route-types.ts +++ b/mobile/src/session/mobile-session-route-types.ts @@ -1,4 +1,5 @@ -import type { DiffComment, TuiAgent } from '../../../src/shared/types' +import type { DiffComment } from '../../../src/shared/diff-comment-types' +import type { TuiAgent } from '../../../src/shared/tui-agent' import type { AgentStatusEntry } from '../../../src/shared/agent-status-types' import type { MobileBrowserTab } from '../browser/MobileBrowserPane' import type { MobileTerminalTheme } from '../terminal/terminal-webview-contract' diff --git a/mobile/src/session/mobile-terminal-tab-agent.test.ts b/mobile/src/session/mobile-terminal-tab-agent.test.ts index 8449f988e24..db6d95c228e 100644 --- a/mobile/src/session/mobile-terminal-tab-agent.test.ts +++ b/mobile/src/session/mobile-terminal-tab-agent.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { AgentStatusEntry } from '../../../src/shared/agent-status-types' -import type { TuiAgent } from '../../../src/shared/types' +import type { TuiAgent } from '../../../src/shared/tui-agent' import type { MobileSessionTab } from './mobile-session-route-types' import { getMobileSessionTabTitle, diff --git a/mobile/src/session/mobile-terminal-tab-agent.ts b/mobile/src/session/mobile-terminal-tab-agent.ts index 731d5bf4a24..f28d9b3fd98 100644 --- a/mobile/src/session/mobile-terminal-tab-agent.ts +++ b/mobile/src/session/mobile-terminal-tab-agent.ts @@ -1,7 +1,7 @@ import { stripLeadingAgentTitleDecorationOrEmpty } from '../../../src/shared/agent-title-decoration' import { resolveExplicitTerminalTitleAgentType } from '../../../src/shared/terminal-title-agent-type' import type { AgentStatusEntry } from '../../../src/shared/agent-status-types' -import type { TuiAgent } from '../../../src/shared/types' +import type { TuiAgent } from '../../../src/shared/tui-agent' import { isBlankBrowserUrl } from '../browser/browser-url' import type { MobileSessionTab } from './mobile-session-route-types' diff --git a/mobile/src/session/pr-action-mutation-contract.ts b/mobile/src/session/pr-action-mutation-contract.ts index a2fcdb271df..0cc8b29b109 100644 --- a/mobile/src/session/pr-action-mutation-contract.ts +++ b/mobile/src/session/pr-action-mutation-contract.ts @@ -1,4 +1,4 @@ -import type { GitHubPRMergeMethod } from '../../../src/shared/types' +import type { GitHubPRMergeMethod } from '../../../src/shared/github/pull-request-types' import type { GitHubPrMutationOutcome } from './github-pr-mutations' import type { GitHubPrRepoSlug } from './github-pr-rpc' diff --git a/mobile/src/session/pr-actions-engine.ts b/mobile/src/session/pr-actions-engine.ts index cc5bfde80a3..18058c71c26 100644 --- a/mobile/src/session/pr-actions-engine.ts +++ b/mobile/src/session/pr-actions-engine.ts @@ -1,4 +1,4 @@ -import type { GitHubPRMergeMethod, PRState } from '../../../src/shared/types' +import type { GitHubPRMergeMethod, PRState } from '../../../src/shared/github/pull-request-types' import { githubRepoIdentityKey } from '../../../src/shared/github/repository-identity-key' import { classifyPrSidebarFailure } from './mobile-pr-sidebar-state' import { createOptimisticField, type OptimisticField } from './optimistic-write-sequence' diff --git a/mobile/src/session/pr-ai-triage-prompt.test.ts b/mobile/src/session/pr-ai-triage-prompt.test.ts index 0ebd02f6a57..4ed8af5ff23 100644 --- a/mobile/src/session/pr-ai-triage-prompt.test.ts +++ b/mobile/src/session/pr-ai-triage-prompt.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { PRCheckDetail } from '../../../src/shared/types' +import type { PRCheckDetail } from '../../../src/shared/github/check-types' import { buildFixChecksPrompt, buildResolveConflictsPrompt, diff --git a/mobile/src/session/pr-ai-triage-prompt.ts b/mobile/src/session/pr-ai-triage-prompt.ts index b1dad03d82c..45fde38daa4 100644 --- a/mobile/src/session/pr-ai-triage-prompt.ts +++ b/mobile/src/session/pr-ai-triage-prompt.ts @@ -3,7 +3,7 @@ import { getBrokenChecks } from '../../../src/shared/pr-checks-fix-prompt' import { buildResolvePullRequestConflictsPrompt } from '../../../src/shared/source-control-conflict-prompts' -import type { PRCheckDetail } from '../../../src/shared/types' +import type { PRCheckDetail } from '../../../src/shared/github/check-types' // Pure prompt builders for the mobile PR sidebar's "Fix checks with AI" / // "Resolve conflicts with AI" triage actions. Kept free of React/native imports diff --git a/mobile/src/session/pr-comment-actions.test.ts b/mobile/src/session/pr-comment-actions.test.ts index bb6f1758b24..6f5d5778084 100644 --- a/mobile/src/session/pr-comment-actions.test.ts +++ b/mobile/src/session/pr-comment-actions.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { PRComment } from '../../../src/shared/types' +import type { PRComment } from '../../../src/shared/github/comment-types' import { buildAddRootCommentParams, buildDeleteCommentParams, diff --git a/mobile/src/session/pr-comment-actions.ts b/mobile/src/session/pr-comment-actions.ts index 012500c62da..0e4e8b8c39b 100644 --- a/mobile/src/session/pr-comment-actions.ts +++ b/mobile/src/session/pr-comment-actions.ts @@ -1,4 +1,5 @@ -import type { GitHubOwnerRepo, PRComment, PRState } from '../../../src/shared/types' +import type { PRComment } from '../../../src/shared/github/comment-types' +import type { GitHubOwnerRepo, PRState } from '../../../src/shared/github/pull-request-types' // Pure helpers for the interactive PR comment timeline (reply / resolve / add // root comment). Kept free of React/native imports so they unit-test under the diff --git a/mobile/src/session/pr-title-edit.ts b/mobile/src/session/pr-title-edit.ts index 9d62b5a0dcd..57cf0b28ac5 100644 --- a/mobile/src/session/pr-title-edit.ts +++ b/mobile/src/session/pr-title-edit.ts @@ -1,4 +1,4 @@ -import type { PRState } from '../../../src/shared/types' +import type { PRState } from '../../../src/shared/github/pull-request-types' // Pure helpers for the inline PR-title edit affordance. Kept free of React/native // imports so they unit-test under the node Vitest config, mirroring the other diff --git a/mobile/src/session/quick-command-draft.ts b/mobile/src/session/quick-command-draft.ts index 2afdcf74620..c1bf3fb5e35 100644 --- a/mobile/src/session/quick-command-draft.ts +++ b/mobile/src/session/quick-command-draft.ts @@ -1,9 +1,9 @@ import type { TerminalQuickCommand, TerminalQuickCommandAction, - TerminalQuickCommandScope, - TuiAgent -} from '../../../src/shared/types' + TerminalQuickCommandScope +} from '../../../src/shared/terminal-quick-command-types' +import type { TuiAgent } from '../../../src/shared/tui-agent' import { isAgentQuickCommand, MAX_QUICK_COMMAND_AGENT_PROMPT_LENGTH, diff --git a/mobile/src/session/use-mobile-diff-review-comment-actions.ts b/mobile/src/session/use-mobile-diff-review-comment-actions.ts index 252eaf0843e..730f1e83354 100644 --- a/mobile/src/session/use-mobile-diff-review-comment-actions.ts +++ b/mobile/src/session/use-mobile-diff-review-comment-actions.ts @@ -1,5 +1,5 @@ import { useCallback, type Dispatch, type SetStateAction } from 'react' -import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/types' +import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/diff-comment-types' import { triggerError, triggerSuccess } from '../platform/haptics' import type { ConnectionState } from '../transport/types' import type { RpcClient } from '../transport/rpc-client' diff --git a/mobile/src/session/use-mobile-diff-review-controller.ts b/mobile/src/session/use-mobile-diff-review-controller.ts index d57276f1f43..03f4b5711a9 100644 --- a/mobile/src/session/use-mobile-diff-review-controller.ts +++ b/mobile/src/session/use-mobile-diff-review-controller.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import type { FlatList } from 'react-native' -import type { DiffComment } from '../../../src/shared/types' +import type { DiffComment } from '../../../src/shared/diff-comment-types' import type { ConnectionState } from '../transport/types' import type { RpcClient } from '../transport/rpc-client' import { getWorktreeLabel } from './worktree-label' diff --git a/mobile/src/session/use-mobile-diff-review-send-actions.test.ts b/mobile/src/session/use-mobile-diff-review-send-actions.test.ts index 1d06190948c..d84fa008be4 100644 --- a/mobile/src/session/use-mobile-diff-review-send-actions.test.ts +++ b/mobile/src/session/use-mobile-diff-review-send-actions.test.ts @@ -1,7 +1,7 @@ import { createElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { DiffComment } from '../../../src/shared/types' +import type { DiffComment } from '../../../src/shared/diff-comment-types' import type { RpcClient } from '../transport/rpc-client' import type { ReviewScreenState } from './mobile-diff-review-screen-model' import { diff --git a/mobile/src/session/use-mobile-diff-review-send-actions.ts b/mobile/src/session/use-mobile-diff-review-send-actions.ts index 0867a3884b3..40423f95664 100644 --- a/mobile/src/session/use-mobile-diff-review-send-actions.ts +++ b/mobile/src/session/use-mobile-diff-review-send-actions.ts @@ -1,6 +1,6 @@ import { useCallback, type Dispatch, type SetStateAction } from 'react' import * as Clipboard from 'expo-clipboard' -import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/types' +import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/diff-comment-types' import type { ConnectionState } from '../transport/types' import type { RpcClient } from '../transport/rpc-client' import { triggerSuccess } from '../platform/haptics' diff --git a/mobile/src/session/use-mobile-pr-comment-actions.ts b/mobile/src/session/use-mobile-pr-comment-actions.ts index 38096bad833..187dc32dbdb 100644 --- a/mobile/src/session/use-mobile-pr-comment-actions.ts +++ b/mobile/src/session/use-mobile-pr-comment-actions.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo, useRef, useState } from 'react' -import type { PRComment } from '../../../src/shared/types' +import type { PRComment } from '../../../src/shared/github/comment-types' import type { ConnectionState } from '../transport/types' import type { RpcClient } from '../transport/rpc-client' import type { GitHubPrRepoSlug } from './github-pr-rpc' diff --git a/mobile/src/session/use-mobile-pr-sidebar-controller.test.ts b/mobile/src/session/use-mobile-pr-sidebar-controller.test.ts index f402f7b58ad..daf4d241fcc 100644 --- a/mobile/src/session/use-mobile-pr-sidebar-controller.test.ts +++ b/mobile/src/session/use-mobile-pr-sidebar-controller.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it, vi } from 'vitest' -import type { GitHubWorkItemDetails, PRCheckDetail, PRInfo } from '../../../src/shared/types' +import type { PRCheckDetail } from '../../../src/shared/github/check-types' +import type { PRInfo } from '../../../src/shared/github/pull-request-types' +import type { GitHubWorkItemDetails } from '../../../src/shared/github/work-item-types' import type { HostedReviewInfo } from '../../../src/shared/hosted-review' import type { GitHubPrReadOutcome } from './github-pr-rpc' import { diff --git a/mobile/src/session/use-quick-commands.test.ts b/mobile/src/session/use-quick-commands.test.ts index a4f93b326fc..b375a20bd97 100644 --- a/mobile/src/session/use-quick-commands.test.ts +++ b/mobile/src/session/use-quick-commands.test.ts @@ -1,7 +1,7 @@ import { createElement } from 'react' import { act, create, type ReactTestRenderer } from 'react-test-renderer' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { TerminalQuickCommand } from '../../../src/shared/types' +import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types' import type { RpcClient } from '../transport/rpc-client' import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client' import type { RpcResponse } from '../transport/types' diff --git a/mobile/src/session/use-quick-commands.ts b/mobile/src/session/use-quick-commands.ts index 6a6604a5c6f..6537551be7a 100644 --- a/mobile/src/session/use-quick-commands.ts +++ b/mobile/src/session/use-quick-commands.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import type { RpcClient } from '../transport/rpc-client' import { isLogicalClientCutoverError } from '../transport/stable-logical-rpc-client' import type { RpcFailure, RpcResponse, RpcSuccess } from '../transport/types' -import type { TerminalQuickCommand } from '../../../src/shared/types' +import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types' import { applyTerminalQuickCommandMutation, parseNormalizedTerminalQuickCommands, diff --git a/mobile/src/source-control/MobileGitHistoryList.tsx b/mobile/src/source-control/MobileGitHistoryList.tsx index dd1a626fdfb..457f0580156 100644 --- a/mobile/src/source-control/MobileGitHistoryList.tsx +++ b/mobile/src/source-control/MobileGitHistoryList.tsx @@ -11,7 +11,7 @@ import { type MobileCommitRow } from './mobile-git-history' import { resolveMobileHistoryScreenView } from './mobile-history-screen-state' -import type { GitBranchChangeEntry } from '../../../src/shared/types' +import type { GitBranchChangeEntry } from '../../../src/shared/git-diff-compare-types' type Props = { client: RpcClient | null diff --git a/mobile/src/source-control/mobile-branch-compare.test.ts b/mobile/src/source-control/mobile-branch-compare.test.ts index b96090faece..cb7a5d7e86a 100644 --- a/mobile/src/source-control/mobile-branch-compare.test.ts +++ b/mobile/src/source-control/mobile-branch-compare.test.ts @@ -1,5 +1,5 @@ import { describe, expect, expectTypeOf, it } from 'vitest' -import type { GitBranchCompareResult } from '../../../src/shared/types' +import type { GitBranchCompareResult } from '../../../src/shared/git-diff-compare-types' import { buildMobileBranchCompareSection, canOpenMobileBranchCompareDiff, diff --git a/mobile/src/source-control/mobile-branch-compare.ts b/mobile/src/source-control/mobile-branch-compare.ts index 060ee768ec4..228156b18b7 100644 --- a/mobile/src/source-control/mobile-branch-compare.ts +++ b/mobile/src/source-control/mobile-branch-compare.ts @@ -2,7 +2,7 @@ import type { GitBranchChangeEntry, GitBranchCompareResult, GitBranchCompareSummary -} from '../../../src/shared/types' +} from '../../../src/shared/git-diff-compare-types' export type MobileGitBranchChangeEntry = GitBranchChangeEntry export type MobileGitBranchCompareSummary = GitBranchCompareSummary diff --git a/mobile/src/source-control/mobile-pr-chip-summary.test.ts b/mobile/src/source-control/mobile-pr-chip-summary.test.ts index f9e21b31d73..36f04eb832c 100644 --- a/mobile/src/source-control/mobile-pr-chip-summary.test.ts +++ b/mobile/src/source-control/mobile-pr-chip-summary.test.ts @@ -1,5 +1,7 @@ import { describe, it, expect } from 'vitest' -import type { PRCheckDetail, PRComment, PRInfo } from '../../../src/shared/types' +import type { PRCheckDetail } from '../../../src/shared/github/check-types' +import type { PRComment } from '../../../src/shared/github/comment-types' +import type { PRInfo } from '../../../src/shared/github/pull-request-types' import type { PrSidebarState } from '../session/mobile-pr-sidebar-state' import { buildMobilePrChipSummary, countUnresolvedReviewThreads } from './mobile-pr-chip-summary' diff --git a/mobile/src/source-control/mobile-pr-chip-summary.ts b/mobile/src/source-control/mobile-pr-chip-summary.ts index c281d882b1c..d1d7adf7acf 100644 --- a/mobile/src/source-control/mobile-pr-chip-summary.ts +++ b/mobile/src/source-control/mobile-pr-chip-summary.ts @@ -1,4 +1,4 @@ -import type { PRComment } from '../../../src/shared/types' +import type { PRComment } from '../../../src/shared/github/comment-types' import type { PrSidebarState } from '../session/mobile-pr-sidebar-state' import { summarizeProviderChecks } from '../../../src/shared/provider-check-summary' import { diff --git a/mobile/src/tasks/blank-workspace-create.ts b/mobile/src/tasks/blank-workspace-create.ts index 2cbed698276..6b6da283dcc 100644 --- a/mobile/src/tasks/blank-workspace-create.ts +++ b/mobile/src/tasks/blank-workspace-create.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../src/shared/types' +import type { TuiAgent } from '../../../src/shared/tui-agent' import type { RpcClient } from '../transport/rpc-client' import { createWorktreeWithNameRetry, type WorktreeCreateResult } from './worktree-create-retry' import { diff --git a/mobile/src/tasks/composer-linked-work-item.test.ts b/mobile/src/tasks/composer-linked-work-item.test.ts index d08d1388a0e..f3ce9299e92 100644 --- a/mobile/src/tasks/composer-linked-work-item.test.ts +++ b/mobile/src/tasks/composer-linked-work-item.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from 'vitest' -import type { GitHubWorkItem, GitLabWorkItem, LinearIssue } from '../../../src/shared/types' +import type { GitHubWorkItem } from '../../../src/shared/github/work-item-types' +import type { GitLabWorkItem } from '../../../src/shared/gitlab-types' +import type { LinearIssue } from '../../../src/shared/linear/issue-types' import { buildGitHubLinkedWorkItem, buildGitLabLinkedWorkItem, diff --git a/mobile/src/tasks/composer-linked-work-item.ts b/mobile/src/tasks/composer-linked-work-item.ts index ed2bc4dbf2d..13d5c845b37 100644 --- a/mobile/src/tasks/composer-linked-work-item.ts +++ b/mobile/src/tasks/composer-linked-work-item.ts @@ -1,4 +1,6 @@ -import type { GitHubWorkItem, GitLabWorkItem, LinearIssue } from '../../../src/shared/types' +import type { GitHubWorkItem } from '../../../src/shared/github/work-item-types' +import type { GitLabWorkItem } from '../../../src/shared/gitlab-types' +import type { LinearIssue } from '../../../src/shared/linear/issue-types' import { getLinearIssueWorkspaceName } from '../../../src/shared/workspace-name' import { buildGitHubWorkspaceSource, diff --git a/mobile/src/tasks/composer-source-base-resolve.ts b/mobile/src/tasks/composer-source-base-resolve.ts index 73c7eaa1839..423c3b05294 100644 --- a/mobile/src/tasks/composer-source-base-resolve.ts +++ b/mobile/src/tasks/composer-source-base-resolve.ts @@ -1,6 +1,6 @@ import type { RpcClient } from '../transport/rpc-client' import type { RpcSuccess } from '../transport/types' -import type { GitHubPrStartPoint } from '../../../src/shared/types' +import type { GitHubPrStartPoint } from '../../../src/shared/worktree/types' // The resolved start point for a linked PR/MR: the base branch to create from // plus the optional review-compare ref, push target, and exact branch name. diff --git a/mobile/src/tasks/github-check-summary.test.ts b/mobile/src/tasks/github-check-summary.test.ts index 3eafcc7661d..8abdee4c7dd 100644 --- a/mobile/src/tasks/github-check-summary.test.ts +++ b/mobile/src/tasks/github-check-summary.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { buildGitHubCheckSummary, type GitHubCheckLike } from './github-check-summary' import { buildGitLabCheckSummary } from './gitlab-check-summary' import { summarizeProviderChecks } from '../../../src/shared/provider-check-summary' -import type { ProviderCheckSummary } from '../../../src/shared/types' +import type { ProviderCheckSummary } from '../../../src/shared/github/pull-request-types' describe('buildGitHubCheckSummary', () => { it('returns none for empty check lists', () => { diff --git a/mobile/src/tasks/github-check-summary.ts b/mobile/src/tasks/github-check-summary.ts index 8a69db32291..413c41c7c31 100644 --- a/mobile/src/tasks/github-check-summary.ts +++ b/mobile/src/tasks/github-check-summary.ts @@ -1,5 +1,5 @@ import { summarizeProviderChecks } from '../../../src/shared/provider-check-summary' -import type { ProviderCheckSummary } from '../../../src/shared/types' +import type { ProviderCheckSummary } from '../../../src/shared/github/pull-request-types' export type GitHubCheckLike = { status: string diff --git a/mobile/src/tasks/gitlab-check-summary.ts b/mobile/src/tasks/gitlab-check-summary.ts index b5834a1d8b9..cdb82d543cd 100644 --- a/mobile/src/tasks/gitlab-check-summary.ts +++ b/mobile/src/tasks/gitlab-check-summary.ts @@ -3,7 +3,7 @@ import { mapGitLabPipelineJobStatusToConclusion } from '../../../src/shared/gitlab-pipeline-checks' import { summarizeProviderChecks } from '../../../src/shared/provider-check-summary' -import type { ProviderCheckSummary } from '../../../src/shared/types' +import type { ProviderCheckSummary } from '../../../src/shared/github/pull-request-types' type GitLabPipelineJobLike = { status: string } diff --git a/mobile/src/tasks/mobile-agent-catalog.ts b/mobile/src/tasks/mobile-agent-catalog.ts index 0f6e0e370a3..c5da71ee272 100644 --- a/mobile/src/tasks/mobile-agent-catalog.ts +++ b/mobile/src/tasks/mobile-agent-catalog.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../src/shared/types' +import type { TuiAgent } from '../../../src/shared/tui-agent' import { MOBILE_TUI_AGENT_AUTO_PICK_ORDER, MOBILE_TUI_AGENT_FAVICON_DOMAINS, diff --git a/mobile/src/tasks/mobile-hosted-check-status.ts b/mobile/src/tasks/mobile-hosted-check-status.ts index c1b6076c969..141e830dec1 100644 --- a/mobile/src/tasks/mobile-hosted-check-status.ts +++ b/mobile/src/tasks/mobile-hosted-check-status.ts @@ -1,4 +1,7 @@ -import type { ProviderCheckSummary, PRMergeableState } from '../../../src/shared/types' +import type { + PRMergeableState, + ProviderCheckSummary +} from '../../../src/shared/github/pull-request-types' import { getProviderChecksLabel } from '../../../src/shared/provider-check-summary' export type MobileHostedReviewStatus = { diff --git a/mobile/src/tasks/mobile-tui-agents.ts b/mobile/src/tasks/mobile-tui-agents.ts index 2da0184e0e7..598e762aa77 100644 --- a/mobile/src/tasks/mobile-tui-agents.ts +++ b/mobile/src/tasks/mobile-tui-agents.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../src/shared/types' +import type { TuiAgent } from '../../../src/shared/tui-agent' // Why: mobile tests run from the mobile package only, so runtime imports of // desktop shared modules can break Vitest transforms in CI. Keep this list diff --git a/mobile/src/tasks/setup-hook-trust.test.ts b/mobile/src/tasks/setup-hook-trust.test.ts index 0e2d91bb7ac..81222ce16ee 100644 --- a/mobile/src/tasks/setup-hook-trust.test.ts +++ b/mobile/src/tasks/setup-hook-trust.test.ts @@ -6,7 +6,7 @@ import { trustedOrcaHooksWithSetupApproval, wasSetupHookPreviouslyApproved } from './setup-hook-trust' -import type { PersistedTrustedOrcaHooks } from '../../../src/shared/types' +import type { PersistedTrustedOrcaHooks } from '../../../src/shared/orca-yaml-hook-types' import type { RpcClient } from '../transport/rpc-client' describe('setup hook trust', () => { diff --git a/mobile/src/tasks/setup-hook-trust.ts b/mobile/src/tasks/setup-hook-trust.ts index 04c52152f45..72381393989 100644 --- a/mobile/src/tasks/setup-hook-trust.ts +++ b/mobile/src/tasks/setup-hook-trust.ts @@ -1,4 +1,4 @@ -import type { PersistedTrustedOrcaHooks } from '../../../src/shared/types' +import type { PersistedTrustedOrcaHooks } from '../../../src/shared/orca-yaml-hook-types' import type { RpcClient } from '../transport/rpc-client' export type SetupHookTrust = { diff --git a/mobile/src/tasks/smart-source-fan-out.ts b/mobile/src/tasks/smart-source-fan-out.ts index e2217597ab9..3050effb7fc 100644 --- a/mobile/src/tasks/smart-source-fan-out.ts +++ b/mobile/src/tasks/smart-source-fan-out.ts @@ -1,9 +1,7 @@ -import type { - BaseRefSearchResult, - GitHubWorkItem, - GitLabWorkItem, - LinearIssue -} from '../../../src/shared/types' +import type { GitHubWorkItem } from '../../../src/shared/github/work-item-types' +import type { GitLabWorkItem } from '../../../src/shared/gitlab-types' +import type { LinearIssue } from '../../../src/shared/linear/issue-types' +import type { BaseRefSearchResult } from '../../../src/shared/repo-types' import { isSmartWorkspaceSourceQueryWithinLimit, type SmartNameMode diff --git a/mobile/src/tasks/smart-source-paste-intent.ts b/mobile/src/tasks/smart-source-paste-intent.ts index 534d36fd0f4..21afd4157de 100644 --- a/mobile/src/tasks/smart-source-paste-intent.ts +++ b/mobile/src/tasks/smart-source-paste-intent.ts @@ -1,4 +1,5 @@ -import type { GitHubWorkItem, GitLabWorkItem } from '../../../src/shared/types' +import type { GitHubWorkItem } from '../../../src/shared/github/work-item-types' +import type { GitLabWorkItem } from '../../../src/shared/gitlab-types' import { normalizeGitHubLinkQuery, parseGitHubIssueOrPRLink, diff --git a/mobile/src/tasks/smart-source-search-requests.ts b/mobile/src/tasks/smart-source-search-requests.ts index 5875cd4d642..876e2ef1a20 100644 --- a/mobile/src/tasks/smart-source-search-requests.ts +++ b/mobile/src/tasks/smart-source-search-requests.ts @@ -1,9 +1,7 @@ -import type { - BaseRefSearchResult, - GitHubWorkItem, - GitLabWorkItem, - LinearIssue -} from '../../../src/shared/types' +import type { GitHubWorkItem } from '../../../src/shared/github/work-item-types' +import type { GitLabWorkItem } from '../../../src/shared/gitlab-types' +import type { LinearIssue } from '../../../src/shared/linear/issue-types' +import type { BaseRefSearchResult } from '../../../src/shared/repo-types' import type { RpcClient } from '../transport/rpc-client' import type { RpcSuccess } from '../transport/types' import { extractLinearIssueReadItems } from './linear-mobile-issue-read' diff --git a/mobile/src/tasks/use-mobile-composer-source.ts b/mobile/src/tasks/use-mobile-composer-source.ts index 6785e2fe30b..5d6c373d454 100644 --- a/mobile/src/tasks/use-mobile-composer-source.ts +++ b/mobile/src/tasks/use-mobile-composer-source.ts @@ -1,5 +1,7 @@ import { useCallback, useMemo, useRef, useState } from 'react' -import type { GitHubWorkItem, GitLabWorkItem, LinearIssue } from '../../../src/shared/types' +import type { GitHubWorkItem } from '../../../src/shared/github/work-item-types' +import type { GitLabWorkItem } from '../../../src/shared/gitlab-types' +import type { LinearIssue } from '../../../src/shared/linear/issue-types' import { resolveComposerManualBranchNameChange } from '../../../src/shared/composer-branch-selection' import { resolveGitHubWorkItemIdentity } from '../../../src/shared/new-workspace/github-work-item-identity' import { getForkPushWarning } from '../../../src/shared/new-workspace/fork-push-warning' diff --git a/mobile/src/tasks/use-smart-workspace-source.ts b/mobile/src/tasks/use-smart-workspace-source.ts index 42edcd48e8a..86c6ca2204a 100644 --- a/mobile/src/tasks/use-smart-workspace-source.ts +++ b/mobile/src/tasks/use-smart-workspace-source.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import type { GitHubWorkItem, GitLabWorkItem } from '../../../src/shared/types' +import type { GitHubWorkItem } from '../../../src/shared/github/work-item-types' +import type { GitLabWorkItem } from '../../../src/shared/gitlab-types' import { buildSmartWorkspaceSourceRows, getSmartWorkspaceEmptyHint, diff --git a/mobile/src/tasks/workspace-agent-selection.ts b/mobile/src/tasks/workspace-agent-selection.ts index da406452882..24dc376a663 100644 --- a/mobile/src/tasks/workspace-agent-selection.ts +++ b/mobile/src/tasks/workspace-agent-selection.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../src/shared/types' +import type { TuiAgent } from '../../../src/shared/tui-agent' import { filterEnabledMobileTuiAgents, isMobileTuiAgent, diff --git a/mobile/src/tasks/workspace-create-params.ts b/mobile/src/tasks/workspace-create-params.ts index 546f15efe75..8693944230a 100644 --- a/mobile/src/tasks/workspace-create-params.ts +++ b/mobile/src/tasks/workspace-create-params.ts @@ -1,9 +1,9 @@ +import type { TuiAgent } from '../../../src/shared/tui-agent' import type { CreateSparseCheckoutRequest, - GitPushTarget, - SetupDecision, - TuiAgent -} from '../../../src/shared/types' + SetupDecision +} from '../../../src/shared/worktree/create-types' +import type { GitPushTarget } from '../../../src/shared/worktree/types' import { getWorkspaceSourceName } from '../../../src/shared/new-workspace/workspace-source' import { resolveMobileWorkspaceCreateName } from './mobile-workspace-name' import type { WorkspaceAgentChoice } from './workspace-agent-selection' diff --git a/mobile/src/terminal/quick-commands.test.ts b/mobile/src/terminal/quick-commands.test.ts index 964fbc9b4d2..584dd8f4812 100644 --- a/mobile/src/terminal/quick-commands.test.ts +++ b/mobile/src/terminal/quick-commands.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalQuickCommand } from '../../../src/shared/types' +import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types' import { buildMobileQuickCommandLaunch, getQuickCommandDisplayPreview, diff --git a/mobile/src/terminal/quick-commands.ts b/mobile/src/terminal/quick-commands.ts index d6e6b86caf3..625d2702bc7 100644 --- a/mobile/src/terminal/quick-commands.ts +++ b/mobile/src/terminal/quick-commands.ts @@ -1,4 +1,5 @@ -import type { TerminalQuickCommand, TuiAgent } from '../../../src/shared/types' +import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types' +import type { TuiAgent } from '../../../src/shared/tui-agent' import { applyTerminalQuickCommandMutation, flattenTerminalQuickCommand, diff --git a/mobile/src/worktree/mobile-workspace-statuses.ts b/mobile/src/worktree/mobile-workspace-statuses.ts index 2c1322d8258..1fedbb6ef73 100644 --- a/mobile/src/worktree/mobile-workspace-statuses.ts +++ b/mobile/src/worktree/mobile-workspace-statuses.ts @@ -1,4 +1,4 @@ -import type { WorkspaceStatusDefinition } from '../../../src/shared/types' +import type { WorkspaceStatusDefinition } from '../../../src/shared/worktree/types' export const DEFAULT_MOBILE_WORKSPACE_STATUS_ID = 'in-progress' diff --git a/mobile/src/worktree/use-workspace-sections.ts b/mobile/src/worktree/use-workspace-sections.ts index bc30bc3f91e..d28546b80bb 100644 --- a/mobile/src/worktree/use-workspace-sections.ts +++ b/mobile/src/worktree/use-workspace-sections.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import type { WorkspaceStatusDefinition } from '../../../src/shared/types' +import type { WorkspaceStatusDefinition } from '../../../src/shared/worktree/types' import type { MobileGroupMode, MobileSortMode } from './workspace-view-settings' import { buildSections, diff --git a/mobile/src/worktree/workspace-list-sections.ts b/mobile/src/worktree/workspace-list-sections.ts index d798a7fe5fa..2ff06f4fee3 100644 --- a/mobile/src/worktree/workspace-list-sections.ts +++ b/mobile/src/worktree/workspace-list-sections.ts @@ -1,4 +1,4 @@ -import type { WorkspaceStatusDefinition } from '../../../src/shared/types' +import type { WorkspaceStatusDefinition } from '../../../src/shared/worktree/types' import { DEFAULT_MOBILE_WORKSPACE_STATUSES, coerceMobileWorkspaceStatuses, diff --git a/mobile/src/worktree/workspace-view-settings.ts b/mobile/src/worktree/workspace-view-settings.ts index 32a4ac73a63..a90b54590d9 100644 --- a/mobile/src/worktree/workspace-view-settings.ts +++ b/mobile/src/worktree/workspace-view-settings.ts @@ -3,7 +3,7 @@ // RPCs). Keeping these settings in the same global store is what lets a grouping // or filter change on the phone show up on desktop and vice-versa. -import type { WorkspaceStatusDefinition } from '../../../src/shared/types' +import type { WorkspaceStatusDefinition } from '../../../src/shared/worktree/types' import { coerceMobileWorkspaceStatuses } from './mobile-workspace-statuses' export type MobileGroupMode = 'none' | 'workspaceStatus' | 'repo' | 'prStatus' diff --git a/src/cli/handlers/account.ts b/src/cli/handlers/account.ts index fd2c7b20402..0f7f95bc17e 100644 --- a/src/cli/handlers/account.ts +++ b/src/cli/handlers/account.ts @@ -23,7 +23,10 @@ import { } from '../../shared/windows-batch-spawn' import { ACCOUNT_IMPORT_RUNTIME_CAPABILITY } from '../../shared/protocol-version' import type { RuntimeStatus } from '../../shared/runtime-types' -import type { ClaudeRateLimitAccountsState, CodexRateLimitAccountsState } from '../../shared/types' +import type { + ClaudeRateLimitAccountsState, + CodexRateLimitAccountsState +} from '../../shared/managed-account-types' import { type InteractiveLoginSession, withInteractiveLoginCleanup diff --git a/src/cli/handlers/agent-hooks.test.ts b/src/cli/handlers/agent-hooks.test.ts index c7f62461303..c42531364a8 100644 --- a/src/cli/handlers/agent-hooks.test.ts +++ b/src/cli/handlers/agent-hooks.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultPersistedState } from '../../shared/constants' -import type { PersistedState } from '../../shared/types' +import type { PersistedState } from '../../shared/persisted-state-types' const { applyAgentStatusHooksEnabledMock, diff --git a/src/cli/handlers/agent-hooks.ts b/src/cli/handlers/agent-hooks.ts index 800493f53c5..7e21c4973a2 100644 --- a/src/cli/handlers/agent-hooks.ts +++ b/src/cli/handlers/agent-hooks.ts @@ -13,7 +13,8 @@ import { import type { AgentHookInstallStatus } from '../../shared/agent-hook-types' import { getDefaultPersistedState } from '../../shared/constants' import { normalizeDisabledTuiAgents } from '../../shared/tui-agent-selection' -import type { GlobalSettings, PersistedState } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { PersistedState } from '../../shared/persisted-state-types' import { applyAgentStatusHooksEnabled, getManagedAgentHookStatuses, diff --git a/src/cli/handlers/automations.ts b/src/cli/handlers/automations.ts index cbbe7517a06..ab592504c00 100644 --- a/src/cli/handlers/automations.ts +++ b/src/cli/handlers/automations.ts @@ -13,7 +13,8 @@ import { type TaskSourceContext, type WorkspaceRunContext } from '../../shared/task-source-context' -import type { ProjectHostSetup, TuiAgent } from '../../shared/types' +import type { ProjectHostSetup } from '../../shared/project-types' +import type { TuiAgent } from '../../shared/tui-agent' import { DEFAULT_AUTOMATION_PRECHECK_TIMEOUT_SECONDS, MAX_AUTOMATION_PRECHECK_TIMEOUT_SECONDS diff --git a/src/cli/handlers/diagnostics.ts b/src/cli/handlers/diagnostics.ts index ca2ffc36855..340bd6afa16 100644 --- a/src/cli/handlers/diagnostics.ts +++ b/src/cli/handlers/diagnostics.ts @@ -1,4 +1,4 @@ -import type { MemorySnapshot } from '../../shared/types' +import type { MemorySnapshot } from '../../shared/process-stats-types' import type { CommandHandler } from '../dispatch' import { formatMemorySnapshot, printResult } from '../format' diff --git a/src/cli/handlers/project.ts b/src/cli/handlers/project.ts index cc5f32e2cd2..c3868001f4c 100644 --- a/src/cli/handlers/project.ts +++ b/src/cli/handlers/project.ts @@ -8,9 +8,9 @@ import type { ProjectHostSetupExistingFolderArgs, ProjectHostSetupResult, ProjectHostSetupUpdateArgs, - ProjectHostSetupUpdateResult, - RepoKind -} from '../../shared/types' + ProjectHostSetupUpdateResult +} from '../../shared/project-types' +import type { RepoKind } from '../../shared/repo-types' import type { CommandHandler } from '../dispatch' import { formatProjectHostSetupCreateResult, diff --git a/src/cli/handlers/vm.ts b/src/cli/handlers/vm.ts index e000ec15afe..1bffd99f00e 100644 --- a/src/cli/handlers/vm.ts +++ b/src/cli/handlers/vm.ts @@ -19,7 +19,7 @@ import { runEphemeralVmRecipeCleanup, runEphemeralVmRecipeStart } from '../../shared/ephemeral-vm-recipe-runner' -import type { OrcaVmRecipe } from '../../shared/types' +import type { OrcaVmRecipe } from '../../shared/orca-yaml-hook-types' export const VM_HANDLERS: Record = { 'vm recipe doctor': async ({ flags, cwd, json }) => { diff --git a/src/cli/project-format.ts b/src/cli/project-format.ts index d5569a0306b..82265411146 100644 --- a/src/cli/project-format.ts +++ b/src/cli/project-format.ts @@ -5,7 +5,7 @@ import type { ProjectHostSetupDeleteResult, ProjectHostSetupResult, ProjectHostSetupUpdateResult -} from '../shared/types' +} from '../shared/project-types' export function formatProjectList(result: { projects: Project[] }): string { if (result.projects.length === 0) { diff --git a/src/cli/workspace-format.ts b/src/cli/workspace-format.ts index 613e6f1ee7e..01dd0dbabcf 100644 --- a/src/cli/workspace-format.ts +++ b/src/cli/workspace-format.ts @@ -10,7 +10,7 @@ import type { RuntimeWorktreePsResult, RuntimeWorktreeRecord } from '../shared/runtime-types' -import type { MemorySnapshot, WorktreeMemory } from '../shared/types' +import type { MemorySnapshot, WorktreeMemory } from '../shared/process-stats-types' export function formatMemorySnapshot(snapshot: MemorySnapshot): string { const topWorktrees = [...snapshot.worktrees].sort((a, b) => b.memory - a.memory).slice(0, 10) diff --git a/src/cli/worktree-project-target.ts b/src/cli/worktree-project-target.ts index 6626270655a..6f4541f3783 100644 --- a/src/cli/worktree-project-target.ts +++ b/src/cli/worktree-project-target.ts @@ -1,4 +1,4 @@ -import type { ProjectHostSetup } from '../shared/types' +import type { ProjectHostSetup } from '../shared/project-types' import type { RuntimeClient } from './runtime-client' import { RuntimeClientError } from './runtime-client' diff --git a/src/main/active-view-preference.ts b/src/main/active-view-preference.ts index 5bd78b14713..eb6098f8c1d 100644 --- a/src/main/active-view-preference.ts +++ b/src/main/active-view-preference.ts @@ -1,7 +1,7 @@ import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs' import { mkdir, rename, rm, writeFile } from 'node:fs/promises' import { dirname, join } from 'node:path' -import type { TopLevelView } from '../shared/types' +import type { TopLevelView } from '../shared/ui-chrome-types' import { isTopLevelView } from '../shared/top-level-view' const ACTIVE_VIEW_FILE_NAME = 'active-view.json' diff --git a/src/main/agent-hooks/first-work-branch-rename-test-harness.ts b/src/main/agent-hooks/first-work-branch-rename-test-harness.ts index 427e2539722..86276a038f5 100644 --- a/src/main/agent-hooks/first-work-branch-rename-test-harness.ts +++ b/src/main/agent-hooks/first-work-branch-rename-test-harness.ts @@ -1,5 +1,6 @@ import type { vi } from 'vitest' -import type { GlobalSettings, Repo } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { Repo } from '../../shared/repo-types' import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree/id' import type { FirstWorkBranchRenameDeps, diff --git a/src/main/agent-hooks/first-work-branch-rename.test.ts b/src/main/agent-hooks/first-work-branch-rename.test.ts index 7690215eedb..cb1107a8622 100644 --- a/src/main/agent-hooks/first-work-branch-rename.test.ts +++ b/src/main/agent-hooks/first-work-branch-rename.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings, Repo } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { Repo } from '../../shared/repo-types' import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree/id' const { diff --git a/src/main/agent-hooks/first-work-branch-rename.ts b/src/main/agent-hooks/first-work-branch-rename.ts index ea2614a344a..45f78e75e8a 100644 --- a/src/main/agent-hooks/first-work-branch-rename.ts +++ b/src/main/agent-hooks/first-work-branch-rename.ts @@ -1,5 +1,6 @@ // On first agent work in a fresh workspace, replace the auto-generated creature branch (e.g. `you/Nautilus`) with a short work-derived name. -import type { GlobalSettings, Repo } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { Repo } from '../../shared/repo-types' import { getRepoIdFromWorktreeId, splitWorktreeIdForFilesystem } from '../../shared/worktree/id' import { parseWorkspaceKey } from '../../shared/workspace-scope' import { parsePaneKey } from '../../shared/stable-pane-id' diff --git a/src/main/agent-hooks/first-work-folder-rename.test.ts b/src/main/agent-hooks/first-work-folder-rename.test.ts index e9a88921943..d8a8a54161e 100644 --- a/src/main/agent-hooks/first-work-folder-rename.test.ts +++ b/src/main/agent-hooks/first-work-folder-rename.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings, Repo } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { Repo } from '../../shared/repo-types' import { renameWorktreeFolderOnFirstWork, type FirstWorkFolderRenameDeps diff --git a/src/main/agent-hooks/first-work-folder-rename.ts b/src/main/agent-hooks/first-work-folder-rename.ts index 79b3f98493e..df1a46b53ee 100644 --- a/src/main/agent-hooks/first-work-folder-rename.ts +++ b/src/main/agent-hooks/first-work-folder-rename.ts @@ -5,7 +5,8 @@ // worktree identity so meta, tabs, and the live PTY session carry over. It is // best-effort and local-only — remote/Windows/locked/dest-taken all degrade to // "folder kept" without disturbing the rename that already succeeded. -import type { GlobalSettings, Repo } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { Repo } from '../../shared/repo-types' import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR, getRepoIdFromWorktreeId, diff --git a/src/main/agent-hooks/local-agent-cli-presence.ts b/src/main/agent-hooks/local-agent-cli-presence.ts index 256e6520456..5810362c27a 100644 --- a/src/main/agent-hooks/local-agent-cli-presence.ts +++ b/src/main/agent-hooks/local-agent-cli-presence.ts @@ -9,7 +9,7 @@ import { isSafeExecutableBasename } from '../../shared/managed-agent-command-token' import type { ManagedAgentHookTarget } from '../../shared/managed-agent-hook-targets' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { hydrateShellPath, mergePathSegments } from '../startup/hydrate-shell-path' export type LocalCliPresenceState = 'found' | 'missing' | 'unknown' diff --git a/src/main/agent-hooks/managed-agent-hook-controls.ts b/src/main/agent-hooks/managed-agent-hook-controls.ts index e99f5659339..6f9f969668c 100644 --- a/src/main/agent-hooks/managed-agent-hook-controls.ts +++ b/src/main/agent-hooks/managed-agent-hook-controls.ts @@ -4,7 +4,7 @@ import { isManagedAgentHookTarget } from '../../shared/managed-agent-hook-targets' import { normalizeDisabledTuiAgents } from '../../shared/tui-agent-selection' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { detectLocalManagedAgentCliPresence } from './local-agent-cli-presence' import { MANAGED_AGENT_HOOK_INSTALLERS, diff --git a/src/main/agent-hooks/managed-hook-detection-commands.ts b/src/main/agent-hooks/managed-hook-detection-commands.ts index b7358807801..2200d183c2e 100644 --- a/src/main/agent-hooks/managed-hook-detection-commands.ts +++ b/src/main/agent-hooks/managed-hook-detection-commands.ts @@ -5,7 +5,7 @@ import { } from '../../shared/managed-agent-command-token' import { MANAGED_AGENT_HOOK_TARGETS } from '../../shared/managed-agent-hook-targets' import { normalizeDisabledTuiAgents } from '../../shared/tui-agent-selection' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import type { TuiAgentDetectionCommand } from '../ipc/tui-agent-detection-commands' export type ManagedHookDetectionSettings = Partial< diff --git a/src/main/agent-hooks/restored-subagent-liveness-sweep.ts b/src/main/agent-hooks/restored-subagent-liveness-sweep.ts index e44a47f11d0..3402ab7bf5b 100644 --- a/src/main/agent-hooks/restored-subagent-liveness-sweep.ts +++ b/src/main/agent-hooks/restored-subagent-liveness-sweep.ts @@ -4,7 +4,8 @@ import { toSshExecutionHostId, type ExecutionHostId } from '../../shared/execution-host' -import type { FolderWorkspace, ProjectGroup } from '../../shared/types' +import type { FolderWorkspace } from '../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../shared/project-group-types' import { getRepoIdFromWorktreeId } from '../../shared/worktree/id' import { parseWorkspaceKey } from '../../shared/workspace-scope' diff --git a/src/main/agent-hooks/server.ts b/src/main/agent-hooks/server.ts index 8a10080168c..71495245bb1 100644 --- a/src/main/agent-hooks/server.ts +++ b/src/main/agent-hooks/server.ts @@ -76,7 +76,7 @@ import { type AgentQuestionAnsweredInferenceRequest } from '../../shared/agent-question-answered-intent' import { parseLegacyNumericPaneKey, parsePaneKey } from '../../shared/stable-pane-id' -import type { LegacyPaneKeyAliasEntry } from '../../shared/types' +import type { LegacyPaneKeyAliasEntry } from '../../shared/persisted-state-types' import { getAgentResumeArgv, normalizeAgentProviderSession, diff --git a/src/main/ai-vault/remote-session-scanner-test-fixtures.ts b/src/main/ai-vault/remote-session-scanner-test-fixtures.ts index c3496590168..b113a298f84 100644 --- a/src/main/ai-vault/remote-session-scanner-test-fixtures.ts +++ b/src/main/ai-vault/remote-session-scanner-test-fixtures.ts @@ -1,4 +1,4 @@ -import type { DirEntry } from '../../shared/types' +import type { DirEntry } from '../../shared/filesystem-entry-types' import type { FileReadResult, FileStat, IFilesystemProvider } from '../providers/types' // Shared by the remote AI Vault scanner suites: an in-memory filesystem whose diff --git a/src/main/automations/headless-workspace-create.test.ts b/src/main/automations/headless-workspace-create.test.ts index b29496eb283..3e25ad87440 100644 --- a/src/main/automations/headless-workspace-create.test.ts +++ b/src/main/automations/headless-workspace-create.test.ts @@ -1,7 +1,7 @@ import path from 'node:path' import { describe, expect, it } from 'vitest' import type { Automation } from '../../shared/automations-types' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import { buildHeadlessAutomationWorktreeCreateArgs } from './headless-workspace-create' const repoPath = path.join('tmp', 'orca') diff --git a/src/main/automations/headless-workspace-create.ts b/src/main/automations/headless-workspace-create.ts index 7e6e2f43e18..2047a105c44 100644 --- a/src/main/automations/headless-workspace-create.ts +++ b/src/main/automations/headless-workspace-create.ts @@ -1,6 +1,6 @@ import type { Automation, AutomationRun } from '../../shared/automations-types' import { buildAutomationWorkspaceProvenance } from '../../shared/automation-workspace-provenance' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import type { OrcaRuntimeService } from '../runtime/orca-runtime' type HeadlessAutomationRunForWorkspace = Pick diff --git a/src/main/automations/run-target-resolution.test.ts b/src/main/automations/run-target-resolution.test.ts index 6e48fb0aac0..73820e316c8 100644 --- a/src/main/automations/run-target-resolution.test.ts +++ b/src/main/automations/run-target-resolution.test.ts @@ -2,7 +2,8 @@ import { describe, expect, it } from 'vitest' import type { Store } from '../persistence' import type { Automation } from '../../shared/automations-types' import type { WorkspaceRunContext } from '../../shared/task-source-context' -import type { ProjectHostSetup, Repo } from '../../shared/types' +import type { ProjectHostSetup } from '../../shared/project-types' +import type { Repo } from '../../shared/repo-types' import { resolveAutomationRunTarget } from './run-target-resolution' function makeRepo(overrides: Partial = {}): Repo { diff --git a/src/main/automations/run-target-resolution.ts b/src/main/automations/run-target-resolution.ts index 1168bf8fb6a..fcd96007ea0 100644 --- a/src/main/automations/run-target-resolution.ts +++ b/src/main/automations/run-target-resolution.ts @@ -2,7 +2,8 @@ import type { Store } from '../persistence' import type { Automation } from '../../shared/automations-types' import { getAutomationLegacyRepoId } from '../../shared/automation-run-identity' import { getRepoExecutionHostId, parseExecutionHostId } from '../../shared/execution-host' -import type { ProjectHostSetup, Repo } from '../../shared/types' +import type { ProjectHostSetup } from '../../shared/project-types' +import type { Repo } from '../../shared/repo-types' import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id' export type AutomationRunTargetResult = diff --git a/src/main/automations/service-precheck.test.ts b/src/main/automations/service-precheck.test.ts index 49122511184..a9f7d336b78 100644 --- a/src/main/automations/service-precheck.test.ts +++ b/src/main/automations/service-precheck.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import { AutomationService } from './service' const runAutomationPrecheckMock = vi.hoisted(() => vi.fn()) diff --git a/src/main/automations/service.test.ts b/src/main/automations/service.test.ts index 1c9c314b41b..27f96400258 100644 --- a/src/main/automations/service.test.ts +++ b/src/main/automations/service.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import { toRuntimeExecutionHostId } from '../../shared/execution-host' import { AutomationService } from './service' diff --git a/src/main/automations/workspace-provenance.ts b/src/main/automations/workspace-provenance.ts index 44936edc888..8174ca11293 100644 --- a/src/main/automations/workspace-provenance.ts +++ b/src/main/automations/workspace-provenance.ts @@ -2,11 +2,11 @@ import { z } from 'zod' import type { Automation, AutomationRun } from '../../shared/automations-types' import { getAutomationRunRepoId } from '../../shared/automation-run-identity' import { buildAutomationWorkspaceProvenance } from '../../shared/automation-workspace-provenance' +import type { Repo } from '../../shared/repo-types' import type { AutomationWorkspaceProvenance, - AutomationWorkspaceProvenanceRequest, - Repo -} from '../../shared/types' + AutomationWorkspaceProvenanceRequest +} from '../../shared/worktree/types' import { beginAutomationDispatchTokenUse, finishAutomationDispatchTokenUse, diff --git a/src/main/azure-devops/pull-request-mappers.ts b/src/main/azure-devops/pull-request-mappers.ts index 062a99fa0b4..132f98260cf 100644 --- a/src/main/azure-devops/pull-request-mappers.ts +++ b/src/main/azure-devops/pull-request-mappers.ts @@ -1,4 +1,4 @@ -import type { CheckStatus, PRMergeableState } from '../../shared/types' +import type { CheckStatus, PRMergeableState } from '../../shared/github/pull-request-types' export type RawAzureDevOpsStatus = { state?: string | null diff --git a/src/main/bitbucket/client.ts b/src/main/bitbucket/client.ts index deb856a66d1..86c22aeb193 100644 --- a/src/main/bitbucket/client.ts +++ b/src/main/bitbucket/client.ts @@ -1,4 +1,4 @@ -import type { CheckStatus } from '../../shared/types' +import type { CheckStatus } from '../../shared/github/pull-request-types' import { deriveBitbucketBuildStatus, mapBitbucketPullRequest, diff --git a/src/main/bitbucket/pull-request-mappers.ts b/src/main/bitbucket/pull-request-mappers.ts index 4f68c320235..7b9c5a07e84 100644 --- a/src/main/bitbucket/pull-request-mappers.ts +++ b/src/main/bitbucket/pull-request-mappers.ts @@ -1,4 +1,4 @@ -import type { CheckStatus, PRMergeableState } from '../../shared/types' +import type { CheckStatus, PRMergeableState } from '../../shared/github/pull-request-types' export type RawBitbucketPullRequest = { id?: number diff --git a/src/main/browser/browser-certificate-challenge.ts b/src/main/browser/browser-certificate-challenge.ts index 2e49685b5d3..94c1192a887 100644 --- a/src/main/browser/browser-certificate-challenge.ts +++ b/src/main/browser/browser-certificate-challenge.ts @@ -1,4 +1,4 @@ -import type { BrowserCertificateFailure } from '../../shared/types' +import type { BrowserCertificateFailure } from '../../shared/browser-workspace-types' import { SUPPORTED_CERTIFICATE_ERROR_CODE } from './browser-certificate-identity' export const CERTIFICATE_CHALLENGE_TTL_MS = 5 * 60_000 diff --git a/src/main/browser/browser-certificate-trust-controller.ts b/src/main/browser/browser-certificate-trust-controller.ts index 53d4810e169..9a2fad879ed 100644 --- a/src/main/browser/browser-certificate-trust-controller.ts +++ b/src/main/browser/browser-certificate-trust-controller.ts @@ -1,5 +1,8 @@ import { randomUUID } from 'node:crypto' -import type { BrowserCertificateFailure, BrowserCertificateProceedResult } from '../../shared/types' +import type { + BrowserCertificateFailure, + BrowserCertificateProceedResult +} from '../../shared/browser-workspace-types' import { isEligibleLocalCertificateHost, toSecureCertificateEndpoint diff --git a/src/main/browser/browser-cookie-import.ts b/src/main/browser/browser-cookie-import.ts index e841f3168d8..b54badd0593 100644 --- a/src/main/browser/browser-cookie-import.ts +++ b/src/main/browser/browser-cookie-import.ts @@ -72,7 +72,7 @@ import type { BrowserCookieImportResult, BrowserCookieImportSummary, BrowserSessionProfileSource -} from '../../shared/types' +} from '../../shared/browser-workspace-types' import { browserSessionRegistry } from './browser-session-registry' import { isGoogleSourceBoundCookie, diff --git a/src/main/browser/browser-manager.ts b/src/main/browser/browser-manager.ts index fbcac378412..0b10e9e189c 100644 --- a/src/main/browser/browser-manager.ts +++ b/src/main/browser/browser-manager.ts @@ -47,11 +47,11 @@ import { getBrowserSessionUserAgentMode } from './browser-session-user-agent-mod import { googleAuthUserAgent, isGoogleAuthUrl } from './browser-google-auth-ua' import { buildViewportUserAgentOverride } from './browser-viewport-user-agent' import type { - BrowserViewportOverride, BrowserCertificateFailure, BrowserLoadError, - BrowserSessionUserAgentMode -} from '../../shared/types' + BrowserSessionUserAgentMode, + BrowserViewportOverride +} from '../../shared/browser-workspace-types' import { type BrowserAnnotationViewportBridgeOptions, BROWSER_ANNOTATION_VIEWPORT_BRIDGE_WORLD_ID, diff --git a/src/main/browser/browser-session-registry.ts b/src/main/browser/browser-session-registry.ts index b0b477a8ef8..79296a0cdbf 100644 --- a/src/main/browser/browser-session-registry.ts +++ b/src/main/browser/browser-session-registry.ts @@ -22,7 +22,7 @@ import type { BrowserSessionProfile, BrowserSessionProfileCreateOptions, BrowserSessionProfileScope -} from '../../shared/types' +} from '../../shared/browser-workspace-types' import { browserManager } from './browser-manager' import { hasSystemMediaAccess, requestSystemMediaAccess } from './browser-media-access' import { cleanElectronUserAgent, setupClientHintsOverride } from './browser-session-ua' diff --git a/src/main/browser/browser-session-user-agent-mode.ts b/src/main/browser/browser-session-user-agent-mode.ts index 8fa8f71d1a9..94051d12ff7 100644 --- a/src/main/browser/browser-session-user-agent-mode.ts +++ b/src/main/browser/browser-session-user-agent-mode.ts @@ -1,6 +1,6 @@ import type { Session } from 'electron' -import type { BrowserSessionUserAgentMode } from '../../shared/types' +import type { BrowserSessionUserAgentMode } from '../../shared/browser-workspace-types' const userAgentModeBySession = new WeakMap() diff --git a/src/main/claude-accounts/claude-duplicate-account.test.ts b/src/main/claude-accounts/claude-duplicate-account.test.ts index 23b2fd2f994..99ab959689a 100644 --- a/src/main/claude-accounts/claude-duplicate-account.test.ts +++ b/src/main/claude-accounts/claude-duplicate-account.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { ClaudeManagedAccount } from '../../shared/types' +import type { ClaudeManagedAccount } from '../../shared/managed-account-types' import { findDuplicateClaudeAccount, type ClaudeAccountIdentityCandidate diff --git a/src/main/claude-accounts/claude-duplicate-account.ts b/src/main/claude-accounts/claude-duplicate-account.ts index 55e57165541..4546556e5ac 100644 --- a/src/main/claude-accounts/claude-duplicate-account.ts +++ b/src/main/claude-accounts/claude-duplicate-account.ts @@ -1,4 +1,4 @@ -import type { ClaudeManagedAccount } from '../../shared/types' +import type { ClaudeManagedAccount } from '../../shared/managed-account-types' import { getClaudeWslSelectionKey } from './runtime-selection' export type ClaudeAccountIdentityCandidate = { diff --git a/src/main/claude-accounts/runtime-auth-service.test.ts b/src/main/claude-accounts/runtime-auth-service.test.ts index a78bc99ec1c..61dabbf3be9 100644 --- a/src/main/claude-accounts/runtime-auth-service.test.ts +++ b/src/main/claude-accounts/runtime-auth-service.test.ts @@ -15,7 +15,8 @@ import { import { tmpdir } from 'node:os' import { join } from 'node:path' import { getDefaultSettings } from '../../shared/constants' -import type { ClaudeManagedAccount, GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { ClaudeManagedAccount } from '../../shared/managed-account-types' import { isOauthTokenExpiring, refreshClaudeOauthCredentials } from './oauth-refresh' const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform') diff --git a/src/main/claude-accounts/runtime-auth-service.ts b/src/main/claude-accounts/runtime-auth-service.ts index 58328222528..a855f9c10ee 100644 --- a/src/main/claude-accounts/runtime-auth-service.ts +++ b/src/main/claude-accounts/runtime-auth-service.ts @@ -3,7 +3,7 @@ import { execFileSync } from 'node:child_process' import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs' import { dirname, join } from 'node:path' import { app } from 'electron' -import type { ClaudeManagedAccount } from '../../shared/types' +import type { ClaudeManagedAccount } from '../../shared/managed-account-types' import type { Store } from '../persistence' import { writeFileAtomically } from '../codex-accounts/fs-utils' import type { ClaudeEnvPatch } from './environment' diff --git a/src/main/claude-accounts/runtime-selection.test.ts b/src/main/claude-accounts/runtime-selection.test.ts index 997da720de1..b7f64aede99 100644 --- a/src/main/claude-accounts/runtime-selection.test.ts +++ b/src/main/claude-accounts/runtime-selection.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { ClaudeManagedAccount, GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { ClaudeManagedAccount } from '../../shared/managed-account-types' import { getSelectedClaudeAccountIdForTarget, pruneInvalidClaudeRuntimeSelection, diff --git a/src/main/claude-accounts/runtime-selection.ts b/src/main/claude-accounts/runtime-selection.ts index 8f3a3a4397b..eda5b271cb5 100644 --- a/src/main/claude-accounts/runtime-selection.ts +++ b/src/main/claude-accounts/runtime-selection.ts @@ -1,8 +1,8 @@ +import type { GlobalSettings } from '../../shared/global-settings-types' import type { ClaudeManagedAccount, - ClaudeManagedAccountRuntimeSelection, - GlobalSettings -} from '../../shared/types' + ClaudeManagedAccountRuntimeSelection +} from '../../shared/managed-account-types' export type ClaudeAccountSelectionTarget = { runtime?: 'host' | 'wsl' diff --git a/src/main/claude-accounts/service.test.ts b/src/main/claude-accounts/service.test.ts index 6572bae7fca..0bff847063b 100644 --- a/src/main/claude-accounts/service.test.ts +++ b/src/main/claude-accounts/service.test.ts @@ -6,7 +6,7 @@ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSyn import { tmpdir } from 'node:os' import { join } from 'node:path' import { PassThrough } from 'node:stream' -import type { ClaudeManagedAccount } from '../../shared/types' +import type { ClaudeManagedAccount } from '../../shared/managed-account-types' import { deleteActiveClaudeKeychainCredentialsStrict, readActiveClaudeKeychainCredentials, diff --git a/src/main/claude-accounts/service.ts b/src/main/claude-accounts/service.ts index 3e3a8ec5f47..6bb5dae1cca 100644 --- a/src/main/claude-accounts/service.ts +++ b/src/main/claude-accounts/service.ts @@ -9,7 +9,7 @@ import type { ClaudeManagedAccount, ClaudeManagedAccountSummary, ClaudeRateLimitAccountsState -} from '../../shared/types' +} from '../../shared/managed-account-types' import type { Store } from '../persistence' import type { RateLimitService } from '../rate-limits/service' import { resolveClaudeCommand } from '../codex-cli/command' diff --git a/src/main/codex-accounts/codex-auth-identity.ts b/src/main/codex-accounts/codex-auth-identity.ts index 5c62dbfe272..2bad1469536 100644 --- a/src/main/codex-accounts/codex-auth-identity.ts +++ b/src/main/codex-accounts/codex-auth-identity.ts @@ -1,4 +1,4 @@ -import type { CodexManagedAccount } from '../../shared/types' +import type { CodexManagedAccount } from '../../shared/managed-account-types' type CodexAuthIdentity = { email: string | null diff --git a/src/main/codex-accounts/legacy-shared-auth-migration.test.ts b/src/main/codex-accounts/legacy-shared-auth-migration.test.ts index 353535fe3c9..8481ede1431 100644 --- a/src/main/codex-accounts/legacy-shared-auth-migration.test.ts +++ b/src/main/codex-accounts/legacy-shared-auth-migration.test.ts @@ -11,7 +11,7 @@ import { } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import type { CodexManagedAccount } from '../../shared/types' +import type { CodexManagedAccount } from '../../shared/managed-account-types' import type * as CodexAccountFs from './fs-utils' const writeFailure = vi.hoisted(() => ({ failNextAuthWrite: false })) diff --git a/src/main/codex-accounts/legacy-shared-auth-migration.ts b/src/main/codex-accounts/legacy-shared-auth-migration.ts index b21d858cf04..a9ecd4c2ea5 100644 --- a/src/main/codex-accounts/legacy-shared-auth-migration.ts +++ b/src/main/codex-accounts/legacy-shared-auth-migration.ts @@ -1,6 +1,6 @@ import { lstatSync, readFileSync } from 'node:fs' import { join } from 'node:path' -import type { CodexManagedAccount } from '../../shared/types' +import type { CodexManagedAccount } from '../../shared/managed-account-types' import { writeFileAtomically } from './fs-utils' import { assertOwnedHostCodexManagedHomePath } from './host-codex-managed-home-ownership' import { codexAuthMatchesManagedAccount, compareCodexAuthFreshness } from './codex-auth-identity' diff --git a/src/main/codex-accounts/managed-codex-auth-readiness.test.ts b/src/main/codex-accounts/managed-codex-auth-readiness.test.ts index abd6a4881ee..d65a9344acc 100644 --- a/src/main/codex-accounts/managed-codex-auth-readiness.test.ts +++ b/src/main/codex-accounts/managed-codex-auth-readiness.test.ts @@ -3,7 +3,8 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { CodexManagedAccount, GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { CodexManagedAccount } from '../../shared/managed-account-types' import { readStoredCodexCredentialState, waitForManagedCodexAuthReady diff --git a/src/main/codex-accounts/managed-codex-auth-readiness.ts b/src/main/codex-accounts/managed-codex-auth-readiness.ts index 5b7938ea30c..3aea6cbd526 100644 --- a/src/main/codex-accounts/managed-codex-auth-readiness.ts +++ b/src/main/codex-accounts/managed-codex-auth-readiness.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs' import { join } from 'node:path' import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import type { CodexAccountSelectionTarget } from './runtime-selection' const AUTH_READY_TIMEOUT_MS = 1_500 diff --git a/src/main/codex-accounts/runtime-home-mirrored-status-home.test.ts b/src/main/codex-accounts/runtime-home-mirrored-status-home.test.ts index d137a65632e..d771ab9cbfd 100644 --- a/src/main/codex-accounts/runtime-home-mirrored-status-home.test.ts +++ b/src/main/codex-accounts/runtime-home-mirrored-status-home.test.ts @@ -3,7 +3,8 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import type * as NodeOs from 'node:os' -import type { CodexManagedAccount, GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { CodexManagedAccount } from '../../shared/managed-account-types' const testState = { userData: '', home: '' } const previousEnv: Record = {} diff --git a/src/main/codex-accounts/runtime-home-service-per-account-migration.test.ts b/src/main/codex-accounts/runtime-home-service-per-account-migration.test.ts index 0efe90bb77c..be9901799ac 100644 --- a/src/main/codex-accounts/runtime-home-service-per-account-migration.test.ts +++ b/src/main/codex-accounts/runtime-home-service-per-account-migration.test.ts @@ -2,7 +2,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import type { CodexManagedAccount, GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { CodexManagedAccount } from '../../shared/managed-account-types' import type * as NodeOs from 'node:os' import { readHookTrustEntries } from '../codex/config-toml-trust' diff --git a/src/main/codex-accounts/runtime-home-service.test.ts b/src/main/codex-accounts/runtime-home-service.test.ts index 639d690cb06..22f5be4d53a 100644 --- a/src/main/codex-accounts/runtime-home-service.test.ts +++ b/src/main/codex-accounts/runtime-home-service.test.ts @@ -16,7 +16,8 @@ import { } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import type { CodexManagedAccount, GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { CodexManagedAccount } from '../../shared/managed-account-types' import type * as ShellStartupEnv from '../pty/shell-startup-env' const testState = { diff --git a/src/main/codex-accounts/runtime-home-service.ts b/src/main/codex-accounts/runtime-home-service.ts index 2d4b8239aad..195e2a719df 100644 --- a/src/main/codex-accounts/runtime-home-service.ts +++ b/src/main/codex-accounts/runtime-home-service.ts @@ -28,7 +28,7 @@ import { win32 as pathWin32 } from 'node:path' import { app } from 'electron' -import type { CodexManagedAccount } from '../../shared/types' +import type { CodexManagedAccount } from '../../shared/managed-account-types' import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path' import type { Store } from '../persistence' import { WSL_CODEX_RUNTIME_HOME_SEGMENTS } from '../pty/codex-home-wsl-env' diff --git a/src/main/codex-accounts/runtime-home-windows-profile-ownership.test.ts b/src/main/codex-accounts/runtime-home-windows-profile-ownership.test.ts index 337aaaf5375..a299d32ee98 100644 --- a/src/main/codex-accounts/runtime-home-windows-profile-ownership.test.ts +++ b/src/main/codex-accounts/runtime-home-windows-profile-ownership.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { CodexRuntimeHomeService } from './runtime-home-service' const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform') diff --git a/src/main/codex-accounts/runtime-selection.test.ts b/src/main/codex-accounts/runtime-selection.test.ts index df98d20242b..bd007fdc3b4 100644 --- a/src/main/codex-accounts/runtime-selection.test.ts +++ b/src/main/codex-accounts/runtime-selection.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { CodexManagedAccount, GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { CodexManagedAccount } from '../../shared/managed-account-types' import { getSelectedCodexAccountIdForTarget, pruneInvalidCodexRuntimeSelection, diff --git a/src/main/codex-accounts/runtime-selection.ts b/src/main/codex-accounts/runtime-selection.ts index 5ac9f91b013..c60dbdb4c9d 100644 --- a/src/main/codex-accounts/runtime-selection.ts +++ b/src/main/codex-accounts/runtime-selection.ts @@ -1,8 +1,8 @@ +import type { GlobalSettings } from '../../shared/global-settings-types' import type { CodexManagedAccount, - CodexManagedAccountRuntimeSelection, - GlobalSettings -} from '../../shared/types' + CodexManagedAccountRuntimeSelection +} from '../../shared/managed-account-types' // Why: the renderer's switch-time lane guard has to key panes the same way a // launch does, so the lane vocabulary lives in shared rather than in main. import { diff --git a/src/main/codex-accounts/service.test.ts b/src/main/codex-accounts/service.test.ts index 789b864159b..d9a1c1125ff 100644 --- a/src/main/codex-accounts/service.test.ts +++ b/src/main/codex-accounts/service.test.ts @@ -14,7 +14,8 @@ import { import { tmpdir } from 'node:os' import { join } from 'node:path' import { PassThrough } from 'node:stream' -import type { CodexRateLimitAccountsState, GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { CodexRateLimitAccountsState } from '../../shared/managed-account-types' import type { ProviderRateLimits, RateLimitState } from '../../shared/rate-limit-types' import { buildCodexResetCreditExpectedScope } from '../../shared/codex-reset-credit-scope' import type { CodexResetCreditAttemptLedger } from '../../shared/codex-reset-credit-attempt-ledger' diff --git a/src/main/codex-accounts/service.ts b/src/main/codex-accounts/service.ts index eef834af323..54eaacef7ef 100644 --- a/src/main/codex-accounts/service.ts +++ b/src/main/codex-accounts/service.ts @@ -11,7 +11,7 @@ import type { CodexManagedAccountSummary, CodexRateLimitAccountsState, CodexSystemDefaultIdentity -} from '../../shared/types' +} from '../../shared/managed-account-types' import type { CodexRateLimitResetOutcome, CodexRateLimitResetResult, diff --git a/src/main/codex/codex-pane-launch-account.test.ts b/src/main/codex/codex-pane-launch-account.test.ts index 51a9a47508d..456b9bc08be 100644 --- a/src/main/codex/codex-pane-launch-account.test.ts +++ b/src/main/codex/codex-pane-launch-account.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { CodexManagedAccount, GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { CodexManagedAccount } from '../../shared/managed-account-types' import { resolveCodexPaneLaunchAccount } from './codex-pane-launch-account' const SYSTEM_HOME = '/Users/example/.codex' diff --git a/src/main/codex/codex-pane-launch-account.ts b/src/main/codex/codex-pane-launch-account.ts index 1bb3cca9c28..b99700da081 100644 --- a/src/main/codex/codex-pane-launch-account.ts +++ b/src/main/codex/codex-pane-launch-account.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path' import { getCodexSelectionLaneKey, diff --git a/src/main/codex/codex-session-source-home.ts b/src/main/codex/codex-session-source-home.ts index 0060051e924..959cb7ab468 100644 --- a/src/main/codex/codex-session-source-home.ts +++ b/src/main/codex/codex-session-source-home.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' /** * Resolves the user-configured Codex *session history* source home, if any. diff --git a/src/main/codex/codex-stale-pane-accounts.test.ts b/src/main/codex/codex-stale-pane-accounts.test.ts index ebc953a4be7..bf2f7e5b5ea 100644 --- a/src/main/codex/codex-stale-pane-accounts.test.ts +++ b/src/main/codex/codex-stale-pane-accounts.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { _internals, forgetCodexPaneAccount, diff --git a/src/main/codex/codex-stale-pane-accounts.ts b/src/main/codex/codex-stale-pane-accounts.ts index b74529abc29..e3c63697cb9 100644 --- a/src/main/codex/codex-stale-pane-accounts.ts +++ b/src/main/codex/codex-stale-pane-accounts.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { getSelectedCodexAccountIdForTarget } from '../codex-accounts/runtime-selection' import { forgetCodexPaneAccount, diff --git a/src/main/daemon/daemon-create-or-attach-result.ts b/src/main/daemon/daemon-create-or-attach-result.ts index d7a2ac64ba8..d6668342487 100644 --- a/src/main/daemon/daemon-create-or-attach-result.ts +++ b/src/main/daemon/daemon-create-or-attach-result.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../shared/types' +import type { TuiAgent } from '../../shared/tui-agent' import type { ShellReadyState, TerminalSnapshot } from './types' import type { AgentSessionClaimedSpawnResult } from '../../shared/agent-session-host-authority' import type { PtyIncarnationId } from '../../shared/pty-incarnation' diff --git a/src/main/daemon/pty-subprocess.ts b/src/main/daemon/pty-subprocess.ts index a6eade9f621..d529c883fee 100644 --- a/src/main/daemon/pty-subprocess.ts +++ b/src/main/daemon/pty-subprocess.ts @@ -69,7 +69,7 @@ import { parsePtySessionId } from './pty-session-id' import { getAgentForegroundContextPaths } from '../providers/agent-foreground-context-paths' import { assertSafeAgentStartupCwd, resolveSafePtyDefaultCwd } from '../providers/pty-default-cwd' import { ORCA_HERMES_STARTUP_QUERY_ENV } from '../../shared/hermes-startup-query' -import type { TuiAgent } from '../../shared/types' +import type { TuiAgent } from '../../shared/tui-agent' import { expandWindowsEnvironmentVariables, expandWindowsPathEnvironmentVariables diff --git a/src/main/daemon/session.test.ts b/src/main/daemon/session.test.ts index c0c2a119618..2da1b700ec1 100644 --- a/src/main/daemon/session.test.ts +++ b/src/main/daemon/session.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { SESSION_FORCE_KILL_RETRY_MS, Session } from './session' import { HeadlessEmulator } from './headless-emulator' import type { SessionState, ShellReadyState } from './types' -import type { TuiAgent } from '../../shared/types' +import type { TuiAgent } from '../../shared/tui-agent' const killWithDescendantSweepMock = vi.hoisted(() => vi.fn()) vi.mock('../pty-descendant-termination', () => ({ diff --git a/src/main/daemon/session.ts b/src/main/daemon/session.ts index 840ea36449b..84eb7fef15d 100644 --- a/src/main/daemon/session.ts +++ b/src/main/daemon/session.ts @@ -19,7 +19,7 @@ import { } from '../shell-prompt-readiness-probe' import { isPowerShellProcess } from '../../shared/shell-process-detection' import { killWithDescendantSweep } from '../pty-descendant-termination' -import type { TuiAgent } from '../../shared/types' +import type { TuiAgent } from '../../shared/tui-agent' import { randomUUID } from 'node:crypto' import { PhysicalExitTracker } from '../../shared/physical-exit-tracker' import { diff --git a/src/main/daemon/terminal-host-create-contract.ts b/src/main/daemon/terminal-host-create-contract.ts index 114aae91ad2..dce8d052cef 100644 --- a/src/main/daemon/terminal-host-create-contract.ts +++ b/src/main/daemon/terminal-host-create-contract.ts @@ -1,5 +1,5 @@ import type { StartupCommandDelivery } from '../../shared/codex-startup-delivery' -import type { TuiAgent } from '../../shared/types' +import type { TuiAgent } from '../../shared/tui-agent' import type { ShellReadyState, TerminalSnapshot } from './types' import type { PtyStartupIngressIntent } from '../../shared/pty-startup-ingress' import type { diff --git a/src/main/daemon/terminal-host-options.ts b/src/main/daemon/terminal-host-options.ts index b483914bba3..241f333e815 100644 --- a/src/main/daemon/terminal-host-options.ts +++ b/src/main/daemon/terminal-host-options.ts @@ -1,5 +1,5 @@ import type { StartupCommandDelivery } from '../../shared/codex-startup-delivery' -import type { TuiAgent } from '../../shared/types' +import type { TuiAgent } from '../../shared/tui-agent' import type { SubprocessHandle } from './session' import type { TakePendingOutputResult, TerminalSnapshot } from './types' diff --git a/src/main/daemon/terminal-host.test.ts b/src/main/daemon/terminal-host.test.ts index ea7302dcc6b..d324d080d42 100644 --- a/src/main/daemon/terminal-host.test.ts +++ b/src/main/daemon/terminal-host.test.ts @@ -6,7 +6,7 @@ import { type SubprocessHandle } from './session' import { TerminalHost } from './terminal-host' -import type { TuiAgent } from '../../shared/types' +import type { TuiAgent } from '../../shared/tui-agent' const killWithDescendantSweepMock = vi.hoisted(() => vi.fn()) vi.mock('../pty-descendant-termination', () => ({ diff --git a/src/main/daemon/types.ts b/src/main/daemon/types.ts index 3908b4508ac..09e96f6d1c7 100644 --- a/src/main/daemon/types.ts +++ b/src/main/daemon/types.ts @@ -12,7 +12,7 @@ export type { // ─── Protocol Version ──────────────────────────────────────────────── import type { StartupCommandDelivery } from '../../shared/codex-startup-delivery' -import type { TuiAgent } from '../../shared/types' +import type { TuiAgent } from '../../shared/tui-agent' import type { PtyStartupIngressIntent } from '../../shared/pty-startup-ingress' import type { AgentSessionExecutionClaim, diff --git a/src/main/ephemeral-vm-recipe-runner.test.ts b/src/main/ephemeral-vm-recipe-runner.test.ts index f5c3f4fc9fa..239eebac97f 100644 --- a/src/main/ephemeral-vm-recipe-runner.test.ts +++ b/src/main/ephemeral-vm-recipe-runner.test.ts @@ -12,7 +12,7 @@ import { runEphemeralVmRecipeStart, runEphemeralVmRecipeSuspend } from './ephemeral-vm-recipe-runner' -import type { OrcaVmRecipe } from '../shared/types' +import type { OrcaVmRecipe } from '../shared/orca-yaml-hook-types' const tmpRoots: string[] = [] diff --git a/src/main/ephemeral-vm-runtime-service.test.ts b/src/main/ephemeral-vm-runtime-service.test.ts index 80cc500b284..b3516d0e791 100644 --- a/src/main/ephemeral-vm-runtime-service.test.ts +++ b/src/main/ephemeral-vm-runtime-service.test.ts @@ -12,7 +12,7 @@ import { provisionEphemeralVmRuntime, resumeEphemeralVmRuntime } from './ephemeral-vm-runtime-service' -import type { OrcaVmRecipe } from '../shared/types' +import type { OrcaVmRecipe } from '../shared/orca-yaml-hook-types' const tempDirs: string[] = [] diff --git a/src/main/ephemeral-vm-runtime-service.ts b/src/main/ephemeral-vm-runtime-service.ts index ddbecb7cf6b..ed419b2f950 100644 --- a/src/main/ephemeral-vm-runtime-service.ts +++ b/src/main/ephemeral-vm-runtime-service.ts @@ -1,4 +1,4 @@ -import type { OrcaVmRecipe } from '../shared/types' +import type { OrcaVmRecipe } from '../shared/orca-yaml-hook-types' import { listEphemeralVmRuntimes, updateEphemeralVmRuntimeStatus, diff --git a/src/main/folder-workspace-diff-comments.ts b/src/main/folder-workspace-diff-comments.ts index 1a7435e8930..cc4481cbccf 100644 --- a/src/main/folder-workspace-diff-comments.ts +++ b/src/main/folder-workspace-diff-comments.ts @@ -1,4 +1,5 @@ -import type { DiffComment, FolderWorkspace } from '../shared/types' +import type { DiffComment } from '../shared/diff-comment-types' +import type { FolderWorkspace } from '../shared/folder-workspace-types' // Why shape-only: this replaces folder-workspaces.ts's verbatim `Array.isArray(raw.diffComments)` read. // Filtering members would make the fix itself a new deletion path for user-authored prose. diff --git a/src/main/ghostty/index.test.ts b/src/main/ghostty/index.test.ts index 08902b5375d..9247cb450c6 100644 --- a/src/main/ghostty/index.test.ts +++ b/src/main/ghostty/index.test.ts @@ -1,5 +1,5 @@ import type { Store } from '../persistence' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { afterEach, describe, expect, it, vi } from 'vitest' const { statMock, readFileMock } = vi.hoisted(() => ({ diff --git a/src/main/ghostty/index.ts b/src/main/ghostty/index.ts index 4827a29741d..e6e041aacfb 100644 --- a/src/main/ghostty/index.ts +++ b/src/main/ghostty/index.ts @@ -1,6 +1,6 @@ import { readFile, stat } from 'node:fs/promises' import { platform } from 'node:os' -import type { GlobalSettings, GhosttyImportPreview } from '../../shared/types' +import type { GhosttyImportPreview, GlobalSettings } from '../../shared/global-settings-types' import type { Store } from '../persistence' import { findGhosttyConfigPaths } from './discovery' import { parseGhosttyConfig } from './parser' diff --git a/src/main/ghostty/mapper.ts b/src/main/ghostty/mapper.ts index c3734cd04c5..ad8b8d050c8 100644 --- a/src/main/ghostty/mapper.ts +++ b/src/main/ghostty/mapper.ts @@ -1,8 +1,5 @@ -import type { - GlobalSettings, - TerminalColorOverrides, - GhosttyImportPreview -} from '../../shared/types' +import type { GhosttyImportPreview, GlobalSettings } from '../../shared/global-settings-types' +import type { TerminalColorOverrides } from '../../shared/terminal-color-overrides' import { HEX_COLOR_RE } from '../../shared/color-validation' import { parsePaddingValue, parseStrictInt } from './numeric-config-values' diff --git a/src/main/ghostty/theme-import.test.ts b/src/main/ghostty/theme-import.test.ts index c2d2ec61ab9..dff669ee7b1 100644 --- a/src/main/ghostty/theme-import.test.ts +++ b/src/main/ghostty/theme-import.test.ts @@ -1,5 +1,5 @@ import type { Store } from '../persistence' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { statMock, readFileMock } = vi.hoisted(() => ({ diff --git a/src/main/git/push-target-validation.ts b/src/main/git/push-target-validation.ts index bfb85cc4a27..8e3375c100f 100644 --- a/src/main/git/push-target-validation.ts +++ b/src/main/git/push-target-validation.ts @@ -1,4 +1,4 @@ -import type { GitPushTarget } from '../../shared/types' +import type { GitPushTarget } from '../../shared/worktree/types' import { assertGitPushTargetShape } from '../../shared/git-push-target-validation' import { gitExecFileAsync } from './runner' diff --git a/src/main/git/remote.ts b/src/main/git/remote.ts index 5c3f40cafe0..687102b1184 100644 --- a/src/main/git/remote.ts +++ b/src/main/git/remote.ts @@ -5,7 +5,7 @@ import { import { resolveEffectiveGitUpstream } from '../../shared/git-effective-upstream' import { gitRefTargetsBranchOnRemote } from '../../shared/git-remote-branch-name' import { resolveGitRemoteRebaseSource } from '../../shared/git-rebase-source' -import type { GitPushTarget } from '../../shared/types' +import type { GitPushTarget } from '../../shared/worktree/types' import type { GitRuntimeOptions } from './git-runtime-options' import { gitOptionsForWorktree } from './git-runtime-options' import { validateGitPushTarget } from './push-target-validation' diff --git a/src/main/git/repo.ts b/src/main/git/repo.ts index 26291ec5d39..50d8a278be2 100644 --- a/src/main/git/repo.ts +++ b/src/main/git/repo.ts @@ -2,7 +2,7 @@ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs' import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path' import { gitExecFileSync, gitExecFileAsync } from './runner' -import type { BaseRefSearchResult } from '../../shared/types' +import type { BaseRefSearchResult } from '../../shared/repo-types' import { parseGitRevListAheadBehindCounts } from '../../shared/git-rev-list-output' import { normalizeRuntimePathSeparators } from '../../shared/cross-platform-path' import { isForEachRefExcludeUnsupportedError } from '../../shared/git-ref-command-capabilities' diff --git a/src/main/git/status.ts b/src/main/git/status.ts index 31f364789de..378bbd56dc2 100644 --- a/src/main/git/status.ts +++ b/src/main/git/status.ts @@ -4,18 +4,20 @@ import { readFile, stat } from 'node:fs/promises' import * as path from 'node:path' import type { GitBranchChangeEntry, - GitBranchChangeStatus, GitBranchCompareResult, GitBranchCompareSummary, GitCommitCompareResult, + GitDiffResult +} from '../../shared/git-diff-compare-types' +import type { + GitBranchChangeStatus, GitConflictKind, GitConflictOperation, - GitDiffResult, GitFileStatus, GitStatusEntry, GitStatusResult, GitUpstreamStatus -} from '../../shared/types' +} from '../../shared/git-status-types' import type { CommitMessageDraftContext } from '../../shared/commit-message-generation' import { getEffectiveGitUpstreamStatus, diff --git a/src/main/git/upstream.ts b/src/main/git/upstream.ts index d36b5f0a53d..18c2037493f 100644 --- a/src/main/git/upstream.ts +++ b/src/main/git/upstream.ts @@ -1,4 +1,5 @@ -import type { GitPushTarget, GitUpstreamStatus } from '../../shared/types' +import type { GitUpstreamStatus } from '../../shared/git-status-types' +import type { GitPushTarget } from '../../shared/worktree/types' import { upstreamOnlyCommitsArePatchEquivalent } from '../../shared/git-upstream-status' import { isNoUpstreamError, normalizeGitErrorMessage } from '../../shared/git-remote-error' import { getEffectiveGitUpstreamStatus } from '../../shared/git-effective-upstream' diff --git a/src/main/git/worktree-shared-directories.ts b/src/main/git/worktree-shared-directories.ts index 6ad9e6096ed..d725e80c8f8 100644 --- a/src/main/git/worktree-shared-directories.ts +++ b/src/main/git/worktree-shared-directories.ts @@ -3,7 +3,7 @@ import { join } from 'node:path' import { checkIgnoredPaths } from './check-ignored-paths' import type { GitRuntimeOptions } from './git-runtime-options' import { loadHooks } from '../hooks' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' // Why: a fresh worktree has no node_modules/.cache, and copying them is slow and // duplicates disk; `orca.yaml` names the ones every worktree should share instead. diff --git a/src/main/git/worktree.ts b/src/main/git/worktree.ts index 11439bff0ae..005be57351f 100644 --- a/src/main/git/worktree.ts +++ b/src/main/git/worktree.ts @@ -15,11 +15,11 @@ import { } from '../worktree-trash' import { parseWslPath } from '../wsl' import type { - GitWorktreeInfo, LocalBaseRefRefreshResult, - LocalBaseRefUpdateSuggestion, - RemoveWorktreeResult -} from '../../shared/types' + LocalBaseRefUpdateSuggestion +} from '../../shared/worktree/base-ref-drift-types' +import type { RemoveWorktreeResult } from '../../shared/worktree/create-types' +import type { GitWorktreeInfo } from '../../shared/worktree/types' import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree/removal' import { isSubmoduleWorktreeRemovalRefusal } from '../../shared/worktree/submodule-removal' import { decodeGitCQuotedPath } from '../../shared/git-cquoted-path' diff --git a/src/main/gitea/pull-request-mappers.ts b/src/main/gitea/pull-request-mappers.ts index eee0bd5d946..9e877f14bb8 100644 --- a/src/main/gitea/pull-request-mappers.ts +++ b/src/main/gitea/pull-request-mappers.ts @@ -1,4 +1,4 @@ -import type { CheckStatus, PRMergeableState } from '../../shared/types' +import type { CheckStatus, PRMergeableState } from '../../shared/github/pull-request-types' export type RawGiteaPullRequest = { number?: number diff --git a/src/main/github/client.ts b/src/main/github/client.ts index 2457c7f21f4..5391539d353 100644 --- a/src/main/github/client.ts +++ b/src/main/github/client.ts @@ -1,28 +1,31 @@ /* eslint-disable max-lines -- Why: co-locating all GitHub client functions keeps acquire/release and error handling consistent. */ +import type { ClassifiedError } from '../../shared/classified-error' import type { - ClassifiedError, - GitPushTarget, - IssueSourcePreference, - ListWorkItemsResult, - PRInfo, - PRConflictSummary, - PRRefreshOutcome, - PRMergeableState, - PRReviewDecision, - PRCheckDetail, - PRCheckRunDetails, - GitHubCommentResult, - GitHubReactionContent, - GitHubPRReviewCommentInput, - PRComment, - GitHubViewer, - GitHubWorkItem, - GitHubPullRequestStateUpdate, GitHubRerunPRChecksResult, + PRCheckDetail, + PRCheckRunDetails +} from '../../shared/github/check-types' +import type { + GitHubCommentResult, + GitHubPRReviewCommentInput, + GitHubReactionContent, + PRComment +} from '../../shared/github/comment-types' +import type { PRRefreshOutcome } from '../../shared/github/pull-request-refresh-types' +import type { GitHubPRMergeMethod, GitHubPRMergeMethodSettings, - GitHubPRStack -} from '../../shared/types' + GitHubPRStack, + GitHubViewer, + PRConflictSummary, + PRInfo, + PRMergeableState, + PRReviewDecision +} from '../../shared/github/pull-request-types' +import type { GitHubWorkItem, ListWorkItemsResult } from '../../shared/github/work-item-types' +import type { GitHubPullRequestStateUpdate } from '../../shared/issue-mutation-types' +import type { IssueSourcePreference } from '../../shared/repo-types' +import type { GitPushTarget } from '../../shared/worktree/types' import type { CreateHostedReviewInput, CreateHostedReviewResult } from '../../shared/hosted-review' import { normalizeHostedReviewBaseRef, diff --git a/src/main/github/comment-reactions.ts b/src/main/github/comment-reactions.ts index 1bb831f30ca..89ee5ee8d6b 100644 --- a/src/main/github/comment-reactions.ts +++ b/src/main/github/comment-reactions.ts @@ -1,4 +1,4 @@ -import type { GitHubReaction, GitHubReactionContent } from '../../shared/types' +import type { GitHubReaction, GitHubReactionContent } from '../../shared/github/comment-types' type GitHubGraphQLReactionContent = | 'THUMBS_UP' diff --git a/src/main/github/conflict-summary-cache.ts b/src/main/github/conflict-summary-cache.ts index 99a2715ce6d..0980bf2a8eb 100644 --- a/src/main/github/conflict-summary-cache.ts +++ b/src/main/github/conflict-summary-cache.ts @@ -1,4 +1,4 @@ -import type { PRConflictSummary } from '../../shared/types' +import type { PRConflictSummary } from '../../shared/github/pull-request-types' // Why 60s: the hottest coordinator cadences that re-derive a CONFLICTING PR // (10s mergeability-pending, 2.5s manual-pending) previously each ran a diff --git a/src/main/github/conflict-summary.ts b/src/main/github/conflict-summary.ts index a8c52c46085..b6d19ce1d2a 100644 --- a/src/main/github/conflict-summary.ts +++ b/src/main/github/conflict-summary.ts @@ -1,4 +1,4 @@ -import type { PRConflictSummary } from '../../shared/types' +import type { PRConflictSummary } from '../../shared/github/pull-request-types' import { isUnsupportedMergeTreeMergeBaseError, isUnsupportedMergeTreeWriteTreeError diff --git a/src/main/github/gh-error-classification.ts b/src/main/github/gh-error-classification.ts index 4d2453899ac..551365f2c9c 100644 --- a/src/main/github/gh-error-classification.ts +++ b/src/main/github/gh-error-classification.ts @@ -1,4 +1,4 @@ -import type { ClassifiedError } from '../../shared/types' +import type { ClassifiedError } from '../../shared/classified-error' // Why: gh CLI surfaces API errors as unstructured stderr. Map known // patterns to typed errors so callers can show user-friendly messages. diff --git a/src/main/github/github-api-repository.ts b/src/main/github/github-api-repository.ts index 24037c4f73c..d7fabcd4f5b 100644 --- a/src/main/github/github-api-repository.ts +++ b/src/main/github/github-api-repository.ts @@ -1,4 +1,5 @@ -import type { GitHubOwnerRepo, IssueSourcePreference } from '../../shared/types' +import type { GitHubOwnerRepo } from '../../shared/github/pull-request-types' +import type { IssueSourcePreference } from '../../shared/repo-types' import { githubRepoIdentityKey, isDefaultGitHubHost diff --git a/src/main/github/github-enterprise-repository.ts b/src/main/github/github-enterprise-repository.ts index 89f8bf7bbd7..f91975097fc 100644 --- a/src/main/github/github-enterprise-repository.ts +++ b/src/main/github/github-enterprise-repository.ts @@ -1,5 +1,5 @@ import { ghExecFileAsync } from '../git/runner' -import type { GitHubOwnerRepo } from '../../shared/types' +import type { GitHubOwnerRepo } from '../../shared/github/pull-request-types' import { getHostedReviewLocalGitOptions, type HostedReviewExecutionOptions diff --git a/src/main/github/github-owner-repo-selection.ts b/src/main/github/github-owner-repo-selection.ts index 2e7c6aa0f4c..39cd0d2da0e 100644 --- a/src/main/github/github-owner-repo-selection.ts +++ b/src/main/github/github-owner-repo-selection.ts @@ -1,4 +1,4 @@ -import type { IssueSourcePreference } from '../../shared/types' +import type { IssueSourcePreference } from '../../shared/repo-types' import { githubRepoIdentityKey } from '../../shared/github/repository-identity-key' import { getOwnerRepoForRemote, diff --git a/src/main/github/github-pr-stack-async-merge.ts b/src/main/github/github-pr-stack-async-merge.ts index 1b3909af035..d5375a468ac 100644 --- a/src/main/github/github-pr-stack-async-merge.ts +++ b/src/main/github/github-pr-stack-async-merge.ts @@ -1,4 +1,4 @@ -import type { GitHubPRMergeMethod } from '../../shared/types' +import type { GitHubPRMergeMethod } from '../../shared/github/pull-request-types' import { ghExecFileAsync } from '../git/runner' import { acquire, release } from './gh-utils' import { diff --git a/src/main/github/github-pr-stack.test.ts b/src/main/github/github-pr-stack.test.ts index 17cd6475941..243fdc057ab 100644 --- a/src/main/github/github-pr-stack.test.ts +++ b/src/main/github/github-pr-stack.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { GitHubPRStack } from '../../shared/types' +import type { GitHubPRStack } from '../../shared/github/pull-request-types' const { ghExecFileAsyncMock, rateLimitGuardMock, noteRateLimitSpendMock } = vi.hoisted(() => ({ ghExecFileAsyncMock: vi.fn(), diff --git a/src/main/github/github-pr-stack.ts b/src/main/github/github-pr-stack.ts index a11cf597769..3dc65f6175a 100644 --- a/src/main/github/github-pr-stack.ts +++ b/src/main/github/github-pr-stack.ts @@ -5,7 +5,7 @@ import type { PRMergeableState, PRReviewDecision, PRState -} from '../../shared/types' +} from '../../shared/github/pull-request-types' import { githubRepoIdentityKey } from '../../shared/github/repository-identity-key' import { ghExecFileAsync } from '../git/runner' import { diff --git a/src/main/github/github-remote-identity-parsing.ts b/src/main/github/github-remote-identity-parsing.ts index 02c21fb8983..32c640b8796 100644 --- a/src/main/github/github-remote-identity-parsing.ts +++ b/src/main/github/github-remote-identity-parsing.ts @@ -1,5 +1,5 @@ import { normalizeGitHubRemoteHost } from '../../shared/git-remote-host-alias' -import type { GitHubOwnerRepo } from '../../shared/types' +import type { GitHubOwnerRepo } from '../../shared/github/pull-request-types' export type GitHubRemoteIdentity = GitHubOwnerRepo & { host: string } diff --git a/src/main/github/github-repository-identity.ts b/src/main/github/github-repository-identity.ts index e348fbc6459..b860c7b7d31 100644 --- a/src/main/github/github-repository-identity.ts +++ b/src/main/github/github-repository-identity.ts @@ -1,6 +1,6 @@ import { runCoalescedProbe, type CoalescedProbes } from '../git/coalesced-probe' import { readRemoteUrl } from '../git/remote-url-probe' -import type { GitHubOwnerRepo } from '../../shared/types' +import type { GitHubOwnerRepo } from '../../shared/github/pull-request-types' import { getSshGitProviderGeneration } from '../providers/ssh-git-dispatch' import { readLocalGitConfigSignature } from './local-git-config-signature' import { diff --git a/src/main/github/github-ssh-host-alias-resolution.ts b/src/main/github/github-ssh-host-alias-resolution.ts index a4988471977..82c0f8b3fb4 100644 --- a/src/main/github/github-ssh-host-alias-resolution.ts +++ b/src/main/github/github-ssh-host-alias-resolution.ts @@ -1,4 +1,4 @@ -import type { GitHubOwnerRepo } from '../../shared/types' +import type { GitHubOwnerRepo } from '../../shared/github/pull-request-types' import { commandExecFileAsync } from '../git/runner' import { getSshGitProvider, getSshGitProviderGeneration } from '../providers/ssh-git-dispatch' import { parseWslPath } from '../wsl' diff --git a/src/main/github/issues.ts b/src/main/github/issues.ts index e2a4dd52677..40cf4e5fa26 100644 --- a/src/main/github/issues.ts +++ b/src/main/github/issues.ts @@ -2,17 +2,15 @@ comment operations keeps the shared acquire/release + error-classification pattern obvious. Each function is short; the file is long because the surface is broad. */ +import type { ClassifiedError } from '../../shared/classified-error' +import type { GitHubCommentResult, PRComment } from '../../shared/github/comment-types' +import type { GitHubAssignableUser, IssueInfo } from '../../shared/github/pull-request-types' import type { - ClassifiedError, - GitHubAssignableUser, GitHubCreateIssueFields, GitHubCreateIssueResult, - GitHubCommentResult, - GitHubIssueUpdate, - IssueInfo, - IssueSourcePreference, - PRComment -} from '../../shared/types' + GitHubIssueUpdate +} from '../../shared/issue-mutation-types' +import type { IssueSourcePreference } from '../../shared/repo-types' import { mapIssueInfo } from './mappers' import type { LocalGitExecOptions, OwnerRepo } from './gh-utils' import { diff --git a/src/main/github/mappers.ts b/src/main/github/mappers.ts index 11ce858cdfb..84fa2b4d43d 100644 --- a/src/main/github/mappers.ts +++ b/src/main/github/mappers.ts @@ -1,4 +1,5 @@ -import type { PRInfo, IssueInfo, CheckStatus, PRCheckDetail } from '../../shared/types' +import type { PRCheckDetail } from '../../shared/github/check-types' +import type { CheckStatus, IssueInfo, PRInfo } from '../../shared/github/pull-request-types' import { derivePRCheckStatusFromRollup } from '../../shared/pr-check-status' // ── REST API check-runs mapping ─────────────────────────────────────── diff --git a/src/main/github/pr-refresh-coordinator.test.ts b/src/main/github/pr-refresh-coordinator.test.ts index 30fc44b3c66..aca21c7d146 100644 --- a/src/main/github/pr-refresh-coordinator.test.ts +++ b/src/main/github/pr-refresh-coordinator.test.ts @@ -1,7 +1,8 @@ /* eslint-disable max-lines -- Why: coordinator tests cover queueing, coalescing, request timestamps, and follow-up scheduling against shared module state. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { GitHubPRRefreshCandidate, PRInfo } from '../../shared/types' +import type { GitHubPRRefreshCandidate } from '../../shared/github/pull-request-refresh-types' +import type { PRInfo } from '../../shared/github/pull-request-types' import { isWslUncPath } from '../../shared/wsl-paths' const { diff --git a/src/main/github/pr-refresh-coordinator.ts b/src/main/github/pr-refresh-coordinator.ts index 190cc82c6f9..2ff9d0e410b 100644 --- a/src/main/github/pr-refresh-coordinator.ts +++ b/src/main/github/pr-refresh-coordinator.ts @@ -7,7 +7,7 @@ import type { GitHubPRRefreshReason, GitHubPRRefreshSkippedReason, PRRefreshOutcome -} from '../../shared/types' +} from '../../shared/github/pull-request-refresh-types' import { getPRForBranchOutcome, type GitHubPRBranchLookupOptions } from './client' import { getOriginGitHubApiRepository } from './github-api-repository' import { ghRepoExecOptions, githubRepoContext } from './gh-utils' diff --git a/src/main/github/pr-refresh-error-classification.ts b/src/main/github/pr-refresh-error-classification.ts index 12c42d092b2..894ddc729fd 100644 --- a/src/main/github/pr-refresh-error-classification.ts +++ b/src/main/github/pr-refresh-error-classification.ts @@ -1,4 +1,4 @@ -import type { PRRefreshErrorType } from '../../shared/types' +import type { PRRefreshErrorType } from '../../shared/github/pull-request-refresh-types' import { classifyGitHubUnavailable } from '../../shared/github/api-availability' import { extractExecError } from '../git/exec-error' diff --git a/src/main/github/pr-start-point.ts b/src/main/github/pr-start-point.ts index 05c012c1ab6..118cbc474ef 100644 --- a/src/main/github/pr-start-point.ts +++ b/src/main/github/pr-start-point.ts @@ -1,4 +1,5 @@ -import type { GitHubPrStartPoint, GitPushTarget, IssueSourcePreference } from '../../shared/types' +import type { IssueSourcePreference } from '../../shared/repo-types' +import type { GitHubPrStartPoint, GitPushTarget } from '../../shared/worktree/types' import { fetchCompareBaseRefWithLocalFallback } from '../git/compare-base-ref-fetch' import { isMissingRemoteRefGitError, diff --git a/src/main/github/project-view/mutations.ts b/src/main/github/project-view/mutations.ts index 5f6044debbf..efe27d452e2 100644 --- a/src/main/github/project-view/mutations.ts +++ b/src/main/github/project-view/mutations.ts @@ -18,7 +18,9 @@ import { } from './internals' import { classifyProjectError, rateLimitedError } from './project-error-classification' import { githubProjectHost } from '../../../shared/github/project-identity' -import type { GitHubAssignableUser, GitHubWorkItemDetails, PRComment } from '../../../shared/types' +import type { PRComment } from '../../../shared/github/comment-types' +import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' +import type { GitHubWorkItemDetails } from '../../../shared/github/work-item-types' import type { AddIssueCommentBySlugArgs, ClearProjectItemFieldArgs, diff --git a/src/main/github/rate-limit.ts b/src/main/github/rate-limit.ts index 4a48823164d..f75d2810321 100644 --- a/src/main/github/rate-limit.ts +++ b/src/main/github/rate-limit.ts @@ -8,7 +8,7 @@ import type { GetRateLimitResult, GitHubRateLimitBucket, GitHubRateLimitSnapshot -} from '../../shared/types' +} from '../../shared/github/rate-limit-types' import { isDefaultGitHubHost } from '../../shared/github/repository-identity-key' import { isWslUncPath } from '../../shared/wsl-paths' import { acquire, release } from './gh-utils' diff --git a/src/main/github/review-head-remote.ts b/src/main/github/review-head-remote.ts index 2a0dd53ca7f..e408d626ad2 100644 --- a/src/main/github/review-head-remote.ts +++ b/src/main/github/review-head-remote.ts @@ -1,4 +1,4 @@ -import type { IssueSourcePreference } from '../../shared/types' +import type { IssueSourcePreference } from '../../shared/repo-types' import { pickPreferredGitRemote } from '../../shared/preferred-git-remote' import { getDefaultRemote } from '../git/repo' import { getGitHubApiRepositoryForRemote } from './github-api-repository' diff --git a/src/main/github/work-item-details.ts b/src/main/github/work-item-details.ts index 2df6d9200e9..276ee9d33eb 100644 --- a/src/main/github/work-item-details.ts +++ b/src/main/github/work-item-details.ts @@ -1,17 +1,18 @@ /* eslint-disable max-lines -- Why: groups the PR/Issue fetch paths and file-contents resolver so caching/rate-limit strategy lives in one place. */ +import type { PRCheckDetail } from '../../shared/github/check-types' +import type { + GitHubIssueTimelineItem, + GitHubIssueTimelineTarget, + PRComment +} from '../../shared/github/comment-types' import type { GitHubAssignableUser, GitHubPRFile, GitHubPRFileContents, - GitHubPRFileViewedState, - GitHubIssueTimelineItem, - GitHubIssueTimelineTarget, - GitHubWorkItem, - GitHubWorkItemDetails, - IssueSourcePreference, - PRCheckDetail, - PRComment -} from '../../shared/types' + GitHubPRFileViewedState +} from '../../shared/github/pull-request-types' +import type { GitHubWorkItem, GitHubWorkItemDetails } from '../../shared/github/work-item-types' +import type { IssueSourcePreference } from '../../shared/repo-types' import { ghExecFileAsync, acquire, diff --git a/src/main/gitlab/client.ts b/src/main/gitlab/client.ts index 138b2cac93d..9e7d03c01b2 100644 --- a/src/main/gitlab/client.ts +++ b/src/main/gitlab/client.ts @@ -1,26 +1,26 @@ /* eslint-disable max-lines -- co-locates GitLab MR/issue/work-item operations sharing one acquire/release pattern. */ +import type { ClassifiedError } from '../../shared/classified-error' import type { - ClassifiedError, + GetGitLabRateLimitResult, GitLabAssignableUser, GitLabAuthDiagnostic, GitLabDiscussionResolveResult, GitLabJobTraceResult, + GitLabMRInlineCommentInput, + GitLabMRReviewersUpdateResult, GitLabPagedResult, GitLabPipelineJob, GitLabRateLimitSnapshot, - GitLabMRInlineCommentInput, - GitLabMRReviewersUpdateResult, GitLabRetryJobResult, GitLabTodo, GitLabViewer, GitLabWorkItem, - GetGitLabRateLimitResult, - IssueSourcePreference, ListMergeRequestsResult, MRComment, MRInfo, MRListState -} from '../../shared/types' +} from '../../shared/gitlab-types' +import type { IssueSourcePreference } from '../../shared/repo-types' import { derivePipelineStatus, mapIssueToWorkItem, mapMRInfo, mapMRToWorkItem } from './mappers' import { acquire, diff --git a/src/main/gitlab/gitlab-preload-args.ts b/src/main/gitlab/gitlab-preload-args.ts index be59907b41f..483f02f9c11 100644 --- a/src/main/gitlab/gitlab-preload-args.ts +++ b/src/main/gitlab/gitlab-preload-args.ts @@ -1,4 +1,4 @@ -import type { MRListState } from '../../shared/types' +import type { MRListState } from '../../shared/gitlab-types' export type GitLabIssueListState = 'opened' | 'closed' | 'all' diff --git a/src/main/gitlab/gitlab-project-recents.ts b/src/main/gitlab/gitlab-project-recents.ts index 7d81f2f3667..44a919262e6 100644 --- a/src/main/gitlab/gitlab-project-recents.ts +++ b/src/main/gitlab/gitlab-project-recents.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { computeNextGitLabRecents } from '../../shared/gitlab-projects' export type GitLabProjectRecentsStore = { diff --git a/src/main/gitlab/gitlab-project-ref-resolution.ts b/src/main/gitlab/gitlab-project-ref-resolution.ts index ee7203ce668..96186f1ccf2 100644 --- a/src/main/gitlab/gitlab-project-ref-resolution.ts +++ b/src/main/gitlab/gitlab-project-ref-resolution.ts @@ -2,7 +2,7 @@ import { glabExecFileAsync } from '../git/runner' import { isTransientGitProbeError, readRemoteUrl } from '../git/remote-url-probe' import { NEGATIVE_ENTRY_TTL_MS } from '../git/remote-ref-probe-cache' import { getSshGitProviderGeneration } from '../providers/ssh-git-dispatch' -import type { IssueSourcePreference } from '../../shared/types' +import type { IssueSourcePreference } from '../../shared/repo-types' import { clearProjectRefInFlight, runProjectRefProbeOnce } from './project-ref-inflight' import { _resetGlabUnauthenticatedHosts, diff --git a/src/main/gitlab/glab-error-classification.ts b/src/main/gitlab/glab-error-classification.ts index 75af66ca4ea..1c8eb6212b8 100644 --- a/src/main/gitlab/glab-error-classification.ts +++ b/src/main/gitlab/glab-error-classification.ts @@ -1,4 +1,4 @@ -import type { ClassifiedError } from '../../shared/types' +import type { ClassifiedError } from '../../shared/classified-error' import { GlabNonListResponseError } from './glab-api-response' // Why: glab CLI surfaces API errors as unstructured stderr. Map known diff --git a/src/main/gitlab/issues.ts b/src/main/gitlab/issues.ts index 8635805337d..9bf57d6beb9 100644 --- a/src/main/gitlab/issues.ts +++ b/src/main/gitlab/issues.ts @@ -2,15 +2,15 @@ co-locating issue list/create/update/comment operations keeps the shared acquire/release + error-classification pattern obvious. Each function is short; the file is long because the surface is broad. */ +import type { ClassifiedError } from '../../shared/classified-error' import type { - ClassifiedError, GitLabAssignableUser, GitLabCommentResult, GitLabIssueInfo, GitLabIssueUpdate, - IssueSourcePreference, MRComment -} from '../../shared/types' +} from '../../shared/gitlab-types' +import type { IssueSourcePreference } from '../../shared/repo-types' import { mapGitLabIssueInfo } from './mappers' // prettier-ignore import { glabExecFileAsync, acquire, release, getIssueProjectRef, resolveIssueSource, classifyGlabError, classifyListFetchError, getGlabKnownHosts, glabRepoExecOptions, glabHostnameArgs, parseGlabJsonList, type LocalGitExecOptions, type ProjectRef } from './gl-utils' diff --git a/src/main/gitlab/mappers.ts b/src/main/gitlab/mappers.ts index cbc47eef6ae..645392ef2cb 100644 --- a/src/main/gitlab/mappers.ts +++ b/src/main/gitlab/mappers.ts @@ -1,10 +1,5 @@ -import type { - CheckStatus, - GitLabIssueInfo, - GitLabWorkItem, - MRInfo, - MRState -} from '../../shared/types' +import type { CheckStatus } from '../../shared/github/pull-request-types' +import type { GitLabIssueInfo, GitLabWorkItem, MRInfo, MRState } from '../../shared/gitlab-types' import { mapGitLabPipelineJobStatusToCheckStatus, mapGitLabPipelineJobStatusToConclusion diff --git a/src/main/gitlab/project-ref-parser.ts b/src/main/gitlab/project-ref-parser.ts index 9477cb8baa0..575e6093179 100644 --- a/src/main/gitlab/project-ref-parser.ts +++ b/src/main/gitlab/project-ref-parser.ts @@ -1,4 +1,4 @@ -import type { GitLabProjectRef } from '../../shared/types' +import type { GitLabProjectRef } from '../../shared/gitlab-types' export type ProjectRef = GitLabProjectRef diff --git a/src/main/gitlab/work-item-details.ts b/src/main/gitlab/work-item-details.ts index 638a35dd847..3155727eeaf 100644 --- a/src/main/gitlab/work-item-details.ts +++ b/src/main/gitlab/work-item-details.ts @@ -10,9 +10,9 @@ import type { GitLabPipelineJob, GitLabWorkItem, GitLabWorkItemDetails, - IssueSourcePreference, MRComment -} from '../../shared/types' +} from '../../shared/gitlab-types' +import type { IssueSourcePreference } from '../../shared/repo-types' import { mapIssueToWorkItem, mapMRToWorkItem } from './mappers' import { acquire, diff --git a/src/main/hooks-runner.test.ts b/src/main/hooks-runner.test.ts index 5ca5903d66f..61ba70d85ed 100644 --- a/src/main/hooks-runner.test.ts +++ b/src/main/hooks-runner.test.ts @@ -3,7 +3,7 @@ import type * as NodeFs from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import type { Repo } from '../shared/types' +import type { Repo } from '../shared/repo-types' import { describe, expect, it, vi } from 'vitest' diff --git a/src/main/hooks.test.ts b/src/main/hooks.test.ts index 6cb9878f79c..0f8a1817e7a 100644 --- a/src/main/hooks.test.ts +++ b/src/main/hooks.test.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines -- Why: hook parsing, shell selection, and execution-path regressions are tightly coupled, so these cases stay in one file to preserve the behavior matrix across platforms. */ -import type { Repo } from '../shared/types' +import type { Repo } from '../shared/repo-types' import type * as GitRunner from './git/runner' import { join } from 'node:path' diff --git a/src/main/hooks.ts b/src/main/hooks.ts index 21b56b8a597..cbbb2edddee 100644 --- a/src/main/hooks.ts +++ b/src/main/hooks.ts @@ -23,12 +23,14 @@ import { addWorktreeSetupWslInteropEnv } from './pty/wsl-orca-env' import type { HookCommandSourcePolicy, OrcaHooks, - Repo, - SetupDecision, - SetupRunPolicy, + SetupRunPolicy +} from '../shared/orca-yaml-hook-types' +import type { Repo } from '../shared/repo-types' +import type { SetupDecision } from '../shared/worktree/create-types' +import type { WorktreeDefaultTabsLaunch, WorktreeSetupLaunch -} from '../shared/types' +} from '../shared/worktree/launch-types' import type { ProjectExecutionRuntimeResolution } from '../shared/project-execution-runtime' import type { SetupRunnerShell } from '../shared/setup-runner-command' diff --git a/src/main/index.ts b/src/main/index.ts index 6b7cafa79e4..2e816ba4c89 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -109,7 +109,7 @@ import { resolveUpdateInstallMode } from './updater' import { configureRemoteServerUpdater } from './runtime/remote-server-updater' -import type { UpdateCheckOptions } from '../shared/types' +import type { UpdateCheckOptions } from '../shared/update-status-types' import { recordUpdaterLifecycle } from './updater-lifecycle-diagnostics' import { installServeSupervisorDisconnectQuit, diff --git a/src/main/ipc/app.ts b/src/main/ipc/app.ts index dfa7fa3d1eb..d976382c288 100644 --- a/src/main/ipc/app.ts +++ b/src/main/ipc/app.ts @@ -5,7 +5,8 @@ import { pathToFileURL } from 'node:url' import { app, BrowserWindow, dialog, ipcMain, type IpcMainInvokeEvent } from 'electron' import { is } from '@electron-toolkit/utils' import type { AppIdentity } from '../../shared/app-identity' -import type { FloatingTerminalCwdRequest, MarkdownDocument } from '../../shared/types' +import type { MarkdownDocument } from '../../shared/filesystem-entry-types' +import type { FloatingTerminalCwdRequest } from '../../shared/ui-chrome-types' import { relaunchApp } from '../app-relaunch' import type { Store } from '../persistence' import { getDevInstanceIdentity } from '../startup/dev-instance-identity' diff --git a/src/main/ipc/browser.ts b/src/main/ipc/browser.ts index 3d2800040ea..88c6e140afa 100644 --- a/src/main/ipc/browser.ts +++ b/src/main/ipc/browser.ts @@ -23,13 +23,13 @@ import type { BrowserExtractHoverResult } from '../../shared/browser-grab-types' import type { - BrowserCookieImportResult, BrowserCertificateProceedResult, + BrowserCookieImportResult, BrowserSessionProfile, BrowserSessionProfileCreateOptions, BrowserSessionProfileScope, BrowserViewportOverride -} from '../../shared/types' +} from '../../shared/browser-workspace-types' import { isValidBrowserAnnotationViewportBridgeMarkers, isValidBrowserAnnotationViewportBridgeToken, diff --git a/src/main/ipc/codex-accounts.ts b/src/main/ipc/codex-accounts.ts index 0c111a5f6d8..f8c81fe0189 100644 --- a/src/main/ipc/codex-accounts.ts +++ b/src/main/ipc/codex-accounts.ts @@ -3,7 +3,7 @@ import type { CodexAccountAddTarget, CodexAccountService } from '../codex-accoun import type { CodexAccountSelectionTarget } from '../codex-accounts/runtime-selection' import { listRecordedCodexPaneLanes } from '../codex/codex-pane-account-registry' import { forgetStaleCodexPanes, listStaleCodexPanes } from '../codex/codex-stale-pane-accounts' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' export function registerCodexAccountHandlers( codexAccounts: CodexAccountService, diff --git a/src/main/ipc/ephemeral-vm-recipe-context.ts b/src/main/ipc/ephemeral-vm-recipe-context.ts index c922d6f13b1..6fa591da9c1 100644 --- a/src/main/ipc/ephemeral-vm-recipe-context.ts +++ b/src/main/ipc/ephemeral-vm-recipe-context.ts @@ -4,7 +4,7 @@ import type { EphemeralVmRecipeDoctorResult } from '../../shared/ephemeral-vm-re import { listEphemeralVmRuntimes } from '../../shared/ephemeral-vm-runtime-store' import type { EphemeralVmRuntimeRecord } from '../../shared/ephemeral-vm-runtimes' import { isFolderRepo, isGitRepoKind } from '../../shared/repo-kind' -import type { OrcaVmRecipe } from '../../shared/types' +import type { OrcaVmRecipe } from '../../shared/orca-yaml-hook-types' export type EphemeralVmRecipeListResult = { status: 'ok' | 'error' diff --git a/src/main/ipc/filesystem-auth.test.ts b/src/main/ipc/filesystem-auth.test.ts index c6fa78b126d..396337a01c8 100644 --- a/src/main/ipc/filesystem-auth.test.ts +++ b/src/main/ipc/filesystem-auth.test.ts @@ -6,7 +6,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { Store } from '../persistence' import type * as RepoWorktrees from '../repo-worktrees' import { listRepoWorktrees } from '../repo-worktrees' -import type { FolderWorkspace, GitWorktreeInfo, ProjectGroup, Repo } from '../../shared/types' +import type { FolderWorkspace } from '../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../shared/project-group-types' +import type { Repo } from '../../shared/repo-types' +import type { GitWorktreeInfo } from '../../shared/worktree/types' import { AUTHORIZED_EXTERNAL_PATHS_MAX, authorizeExternalPath, diff --git a/src/main/ipc/filesystem-auth.ts b/src/main/ipc/filesystem-auth.ts index b172c2c1894..fb93ac7f8d7 100644 --- a/src/main/ipc/filesystem-auth.ts +++ b/src/main/ipc/filesystem-auth.ts @@ -7,7 +7,9 @@ import { isRepoRoot, listRepoWorktrees } from '../repo-worktrees' import { computeWorkspaceRoot, getWorktreePathSettings } from './worktree-logic' import { isPathInsideOrEqual } from '../../shared/cross-platform-path' import { getProjectGroupSubtreeIds } from '../../shared/project-groups' -import type { FolderWorkspace, ProjectGroup, Repo } from '../../shared/types' +import type { FolderWorkspace } from '../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../shared/project-group-types' +import type { Repo } from '../../shared/repo-types' export const PATH_ACCESS_DENIED_MESSAGE = 'Access denied: path resolves outside allowed directories. If this blocks a legitimate workflow, please file a GitHub issue.' diff --git a/src/main/ipc/filesystem-search-git.ts b/src/main/ipc/filesystem-search-git.ts index e997ddd2e02..0bff6336f66 100644 --- a/src/main/ipc/filesystem-search-git.ts +++ b/src/main/ipc/filesystem-search-git.ts @@ -1,4 +1,4 @@ -import type { SearchOptions, SearchResult } from '../../shared/types' +import type { SearchOptions, SearchResult } from '../../shared/code-search-types' import { buildGitGrepArgs, buildSubmatchRegex, diff --git a/src/main/ipc/filesystem-watcher-large-batch.test.ts b/src/main/ipc/filesystem-watcher-large-batch.test.ts index e605cd6167c..cfb99c217c1 100644 --- a/src/main/ipc/filesystem-watcher-large-batch.test.ts +++ b/src/main/ipc/filesystem-watcher-large-batch.test.ts @@ -32,7 +32,7 @@ import { closeAllWatchers, registerFilesystemWatcherHandlers } from './filesyste import { stat } from 'node:fs/promises' import { subscribe as subscribeParcelWatcher } from '@parcel/watcher' import type { Event as WatcherEvent } from '@parcel/watcher' -import type { FsChangedPayload } from '../../shared/types' +import type { FsChangedPayload } from '../../shared/filesystem-entry-types' import { WATCH_BATCH_MAX_WAIT_MS, WATCH_BATCH_TRAILING_MS diff --git a/src/main/ipc/filesystem-watcher-remote-batch.test.ts b/src/main/ipc/filesystem-watcher-remote-batch.test.ts index 8666a40c906..befc9a194e2 100644 --- a/src/main/ipc/filesystem-watcher-remote-batch.test.ts +++ b/src/main/ipc/filesystem-watcher-remote-batch.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { FsChangeEvent } from '../../shared/types' +import type { FsChangeEvent } from '../../shared/filesystem-entry-types' const { handleMock, getSshFilesystemProviderMock, providerRegistrationListeners } = vi.hoisted( () => ({ diff --git a/src/main/ipc/filesystem-watcher.ts b/src/main/ipc/filesystem-watcher.ts index 571f6c46b3a..0a337967386 100644 --- a/src/main/ipc/filesystem-watcher.ts +++ b/src/main/ipc/filesystem-watcher.ts @@ -3,7 +3,7 @@ import { ipcMain, type WebContents } from 'electron' import * as path from 'node:path' import { stat } from 'node:fs/promises' import type { Event as WatcherEvent } from '@parcel/watcher' -import type { FsChangeEvent, FsChangedPayload } from '../../shared/types' +import type { FsChangeEvent, FsChangedPayload } from '../../shared/filesystem-entry-types' import { isWindowsAbsolutePathLike, normalizeRuntimePathForComparison diff --git a/src/main/ipc/filesystem.ts b/src/main/ipc/filesystem.ts index 44239bdac74..2a5111e5d13 100644 --- a/src/main/ipc/filesystem.ts +++ b/src/main/ipc/filesystem.ts @@ -9,25 +9,24 @@ import { awaitWindowsHostGitEnvironmentReady, gitExecFileAsync, wslAwareSpawn } import { parseWslPath, toWindowsWslPath } from '../wsl' import { tryDeleteWslUncPath } from '../wsl-unc-delete' import type { Store } from '../persistence' +import type { SearchOptions, SearchResult } from '../../shared/code-search-types' +import type { DirEntry, MarkdownDocument } from '../../shared/filesystem-entry-types' import type { - DirEntry, GitBranchCompareResult, GitCommitCompareResult, + GitDiffResult +} from '../../shared/git-diff-compare-types' +import type { GitForkSyncExpectedUpstream, GitForkSyncResult } from '../../shared/git-fork-sync' +import type { GitConflictOperation, - GitDiffResult, - GitForkSyncExpectedUpstream, - GitForkSyncResult, - GlobalSettings, GitStagingArea, - GitPushTarget, - GitUpstreamStatus, GitStatusResult, - MarkdownDocument, - SearchOptions, - SearchResult, - Repo, - TuiAgent -} from '../../shared/types' + GitUpstreamStatus +} from '../../shared/git-status-types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { Repo } from '../../shared/repo-types' +import type { TuiAgent } from '../../shared/tui-agent' +import type { GitPushTarget } from '../../shared/worktree/types' import type { GitHistoryOptions, GitHistoryResult } from '../../shared/git-history' import type { SshMutationExpectation } from '../../shared/ssh-types' import { sortDirEntries } from '../../shared/file-name-sort' diff --git a/src/main/ipc/floating-workspace-directory.test.ts b/src/main/ipc/floating-workspace-directory.test.ts index 335bf1e96bc..2b372586cf1 100644 --- a/src/main/ipc/floating-workspace-directory.test.ts +++ b/src/main/ipc/floating-workspace-directory.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, mkdir, realpath, rm, symlink, unlink } from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' const { appGetPathMock, authorizeExternalPathMock } = vi.hoisted(() => ({ appGetPathMock: vi.fn(), diff --git a/src/main/ipc/floating-workspace-directory.ts b/src/main/ipc/floating-workspace-directory.ts index bf1373e9f50..9e5f7d34cb7 100644 --- a/src/main/ipc/floating-workspace-directory.ts +++ b/src/main/ipc/floating-workspace-directory.ts @@ -2,7 +2,8 @@ import { constants as fsConstants } from 'node:fs' import { access, mkdir, realpath, stat } from 'node:fs/promises' import path from 'node:path' import { app } from 'electron' -import type { GlobalSettings, FloatingTerminalCwdRequest } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { FloatingTerminalCwdRequest } from '../../shared/ui-chrome-types' import type { Store } from '../persistence' import { authorizeExternalPath } from './filesystem-auth' diff --git a/src/main/ipc/github-work-item-args.ts b/src/main/ipc/github-work-item-args.ts index 0aff921ee24..2497c8e93f9 100644 --- a/src/main/ipc/github-work-item-args.ts +++ b/src/main/ipc/github-work-item-args.ts @@ -1,5 +1,5 @@ import type { TaskSourceContext } from '../../shared/task-source-context' -import type { IssueSourcePreference } from '../../shared/types' +import type { IssueSourcePreference } from '../../shared/repo-types' export type WorkItemArgs = { repoPath: string diff --git a/src/main/ipc/github.ts b/src/main/ipc/github.ts index eda32115b19..36516790446 100644 --- a/src/main/ipc/github.ts +++ b/src/main/ipc/github.ts @@ -4,19 +4,20 @@ reviewable as one surface. Splitting by feature area would risk drifting validation/gate conventions across handler files. */ import { ipcMain } from 'electron' import { resolve } from 'node:path' +import type { GitHubReactionContent } from '../../shared/github/comment-types' import type { - Repo, - GitHubCreateIssueFields, - GitHubIssueUpdate, - GitHubOwnerRepo, - GitHubPullRequestStateUpdate, GitHubPRRefreshCandidate, GitHubPRRefreshEnqueueResult, GitHubPRRefreshReason, - GitHubReactionContent, - PRRefreshOutcome, - GitHubPRFile -} from '../../shared/types' + PRRefreshOutcome +} from '../../shared/github/pull-request-refresh-types' +import type { GitHubOwnerRepo, GitHubPRFile } from '../../shared/github/pull-request-types' +import type { + GitHubCreateIssueFields, + GitHubIssueUpdate, + GitHubPullRequestStateUpdate +} from '../../shared/issue-mutation-types' +import type { Repo } from '../../shared/repo-types' import { getRepoExecutionHostId } from '../../shared/execution-host' import type { TaskSourceContext } from '../../shared/task-source-context' import type { Store } from '../persistence' diff --git a/src/main/ipc/gitlab.test.ts b/src/main/ipc/gitlab.test.ts index 2ff888a7bd8..87a298e56aa 100644 --- a/src/main/ipc/gitlab.test.ts +++ b/src/main/ipc/gitlab.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { Store } from '../persistence' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import { toSshExecutionHostId } from '../../shared/execution-host' const ORIGINAL_PLATFORM = process.platform diff --git a/src/main/ipc/gitlab.ts b/src/main/ipc/gitlab.ts index e68c4b8f6d8..cc668841038 100644 --- a/src/main/ipc/gitlab.ts +++ b/src/main/ipc/gitlab.ts @@ -8,9 +8,9 @@ import type { GitLabIssueUpdate, GitLabMRInlineCommentInput, GitLabMRUpdate, - GitLabWorkItem, - Repo -} from '../../shared/types' + GitLabWorkItem +} from '../../shared/gitlab-types' +import type { Repo } from '../../shared/repo-types' import { getRepoExecutionHostId } from '../../shared/execution-host' import type { TaskSourceContext } from '../../shared/task-source-context' import type { Store } from '../persistence' diff --git a/src/main/ipc/hosted-review.ts b/src/main/ipc/hosted-review.ts index 736668e1f0d..119647d9957 100644 --- a/src/main/ipc/hosted-review.ts +++ b/src/main/ipc/hosted-review.ts @@ -6,7 +6,7 @@ import type { HostedReviewCreationEligibilityArgs, HostedReviewForBranchArgs } from '../../shared/hosted-review' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import type { Store } from '../persistence' import type { StatsCollector } from '../stats/collector' import { diff --git a/src/main/ipc/jira.ts b/src/main/ipc/jira.ts index c8edc638a3b..b7faa6fd333 100644 --- a/src/main/ipc/jira.ts +++ b/src/main/ipc/jira.ts @@ -25,7 +25,7 @@ import type { JiraIssueFilter, JiraIssueUpdate, JiraSiteSelection -} from '../../shared/types' +} from '../../shared/jira-types' const VALID_FILTERS = new Set(['assigned', 'reported', 'all', 'done']) const issueSummaryRequests = new JiraCancellableRequests() diff --git a/src/main/ipc/linear.ts b/src/main/ipc/linear.ts index 0905c691f4c..dd64a777ab4 100644 --- a/src/main/ipc/linear.ts +++ b/src/main/ipc/linear.ts @@ -26,11 +26,9 @@ import { listTeams, getTeamStates, getTeamLabels, getTeamMembers } from '../line import type { LinearListFilter } from '../linear/issues' import { clampLinearIssueListLimit } from '../../shared/linear/issue-read-limits' import { optionalParsedLinearIssueAttributeFilter } from '../../shared/linear/issue-attribute-filter' -import type { - LinearCustomViewModel, - LinearIssueUpdate, - LinearWorkspaceSelection -} from '../../shared/types' +import type { LinearIssueUpdate } from '../../shared/issue-mutation-types' +import type { LinearCustomViewModel } from '../../shared/linear/project-types' +import type { LinearWorkspaceSelection } from '../../shared/linear/workspace-types' const VALID_FILTERS = new Set(['assigned', 'created', 'all', 'completed']) diff --git a/src/main/ipc/local-worktree-runtime-options.ts b/src/main/ipc/local-worktree-runtime-options.ts index f9850a7a399..d0d1a67e3cd 100644 --- a/src/main/ipc/local-worktree-runtime-options.ts +++ b/src/main/ipc/local-worktree-runtime-options.ts @@ -5,7 +5,7 @@ import { type LocalProjectWorktreeGitOptions } from '../project-runtime-git-options' import { splitWorktreeId } from '../../shared/worktree/id' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' function comparableLocalPath(value: string): string { const normalized = resolve(value) diff --git a/src/main/ipc/markdown-documents.ts b/src/main/ipc/markdown-documents.ts index 713adc9e0fb..38466da6087 100644 --- a/src/main/ipc/markdown-documents.ts +++ b/src/main/ipc/markdown-documents.ts @@ -1,6 +1,6 @@ import { readdir } from 'node:fs/promises' import { basename as pathBasename, extname, isAbsolute, join, relative, resolve } from 'node:path' -import type { MarkdownDocument } from '../../shared/types' +import type { MarkdownDocument } from '../../shared/filesystem-entry-types' function normalizeRelativePath(path: string): string { return path.replace(/[\\/]+/g, '/').replace(/^\/+/, '') diff --git a/src/main/ipc/memory.ts b/src/main/ipc/memory.ts index 1c57d4d6fe3..f8bf96c1967 100644 --- a/src/main/ipc/memory.ts +++ b/src/main/ipc/memory.ts @@ -1,5 +1,5 @@ import { ipcMain } from 'electron' -import type { MemorySnapshot } from '../../shared/types' +import type { MemorySnapshot } from '../../shared/process-stats-types' import type { Store } from '../persistence' import { collectMemorySnapshot } from '../memory/collector' diff --git a/src/main/ipc/notification-options.ts b/src/main/ipc/notification-options.ts index 1213e8f1d92..a2553f05a3c 100644 --- a/src/main/ipc/notification-options.ts +++ b/src/main/ipc/notification-options.ts @@ -1,4 +1,4 @@ -import type { NotificationDispatchRequest } from '../../shared/types' +import type { NotificationDispatchRequest } from '../../shared/notification-settings-types' const NOTIFICATION_AGENT_LABEL_MAX_LENGTH = 40 const NOTIFICATION_TITLE_CONTEXT_MAX_LENGTH = 80 diff --git a/src/main/ipc/notifications.ts b/src/main/ipc/notifications.ts index 031cdf63378..26d87f8f323 100644 --- a/src/main/ipc/notifications.ts +++ b/src/main/ipc/notifications.ts @@ -14,13 +14,13 @@ import twoToneSoundPath from '../../../resources/notification-sounds/two-tone.mp import type { Store } from '../persistence' import type { NotificationDeliveryProbeResult, + NotificationDismissResult, NotificationDispatchRequest, NotificationDispatchResult, - NotificationDismissResult, NotificationPermissionStatusResult, NotificationSettings, NotificationSoundDataResult -} from '../../shared/types' +} from '../../shared/notification-settings-types' import { getRepoIdFromWorktreeId } from '../../shared/worktree/id' import type { OrcaRuntimeService } from '../runtime/orca-runtime' import { buildNotificationOptions } from './notification-options' diff --git a/src/main/ipc/onboarding.ts b/src/main/ipc/onboarding.ts index c5449d6a80b..840f2fc695f 100644 --- a/src/main/ipc/onboarding.ts +++ b/src/main/ipc/onboarding.ts @@ -1,6 +1,6 @@ import { ipcMain } from 'electron' import { sanitizeOnboardingUpdate, type Store } from '../persistence' -import type { OnboardingState } from '../../shared/types' +import type { OnboardingState } from '../../shared/onboarding-state-types' export function registerOnboardingHandlers(store: Store): void { ipcMain.removeHandler('onboarding:get') diff --git a/src/main/ipc/pet-bundle.ts b/src/main/ipc/pet-bundle.ts index 1b701a7a105..6a07647cfae 100644 --- a/src/main/ipc/pet-bundle.ts +++ b/src/main/ipc/pet-bundle.ts @@ -1,4 +1,4 @@ -import type { SpriteAnimation } from '../../shared/types' +import type { SpriteAnimation } from '../../shared/pet-types' import { CODEX_PET_ANIMATIONS, CODEX_PET_DEFAULT_ANIMATION, diff --git a/src/main/ipc/pet.test.ts b/src/main/ipc/pet.test.ts index 5e857bb1244..64e5126eedd 100644 --- a/src/main/ipc/pet.test.ts +++ b/src/main/ipc/pet.test.ts @@ -39,7 +39,7 @@ vi.mock('electron', () => ({ })) import { registerPetHandlers } from './pet' -import type { CustomPet } from '../../shared/types' +import type { CustomPet } from '../../shared/pet-types' describe('registerPetHandlers', () => { let tempDir: string diff --git a/src/main/ipc/pet.ts b/src/main/ipc/pet.ts index df48249f001..8b10079ba0e 100644 --- a/src/main/ipc/pet.ts +++ b/src/main/ipc/pet.ts @@ -6,7 +6,7 @@ import { pipeline } from 'node:stream/promises' import { randomUUID } from 'node:crypto' import { basename, dirname, extname, isAbsolute, join, normalize, resolve, sep } from 'node:path' import { z } from 'zod' -import type { CustomPet } from '../../shared/types' +import type { CustomPet } from '../../shared/pet-types' import { applyCodexPetDefaults, readWebpDimensionsFromBuffer, diff --git a/src/main/ipc/preflight.ts b/src/main/ipc/preflight.ts index 5619495c1cf..dc125b7a9a5 100644 --- a/src/main/ipc/preflight.ts +++ b/src/main/ipc/preflight.ts @@ -1,5 +1,8 @@ import { ipcMain } from 'electron' -import type { PathSource, ShellHydrationFailureReason } from '../../shared/types' +import type { + PathSource, + ShellHydrationFailureReason +} from '../../shared/shell-path-hydration-types' import { hydrateShellPath, mergePathSegments } from '../startup/hydrate-shell-path' import { getAzureDevOpsAuthStatus } from '../azure-devops/client' import { getBitbucketAuthStatus } from '../bitbucket/client' diff --git a/src/main/ipc/pty-hidden-delivery-gate.ts b/src/main/ipc/pty-hidden-delivery-gate.ts index 43fa8567b88..0b199d0d1ab 100644 --- a/src/main/ipc/pty-hidden-delivery-gate.ts +++ b/src/main/ipc/pty-hidden-delivery-gate.ts @@ -9,7 +9,7 @@ * bytes (dispatcher sidecars) registers delivery * interest, which suppresses the gate for that PTY. */ -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' export type HiddenPtyDeliveryGateSettings = Pick< GlobalSettings, diff --git a/src/main/ipc/pty.test.ts b/src/main/ipc/pty.test.ts index 88306e95a0d..5c1516679d1 100644 --- a/src/main/ipc/pty.test.ts +++ b/src/main/ipc/pty.test.ts @@ -11,7 +11,7 @@ import { import { CLIPBOARD_TEXT_MEASURE_YIELD_CODE_UNITS } from '../../shared/clipboard-text' import { redactPtyIdForDiagnostics } from '../../shared/pty-delivery-diagnostics' import { FLOATING_TERMINAL_WORKTREE_ID, getDefaultWorkspaceSession } from '../../shared/constants' -import type { TuiAgent } from '../../shared/types' +import type { TuiAgent } from '../../shared/tui-agent' import type { AgentSessionOwnerBinding } from '../../shared/agent-session-host-authority' import { AGENT_SESSION_CLAIM_DIGEST_VERSION } from '../../shared/agent-session-host-authority' import { PtyWriteUnavailableError } from '../providers/pty-write-unavailable-error' diff --git a/src/main/ipc/pty.ts b/src/main/ipc/pty.ts index 680bc78b4ec..6c38d513859 100644 --- a/src/main/ipc/pty.ts +++ b/src/main/ipc/pty.ts @@ -15,7 +15,8 @@ export { getBashShellReadyRcfileContent } from '../providers/local-pty-shell-rea import type { OrcaRuntimeService } from '../runtime/orca-runtime' import type { PtyBindingSourceExpectation, Store } from '../persistence' import { retireTerminalSurfaceFromPersistence } from '../runtime/mobile-session-terminal-persistence-retirement' -import type { GlobalSettings, TuiAgent } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { TuiAgent } from '../../shared/tui-agent' import { toSshExecutionHostId } from '../../shared/execution-host' import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path' import { terminalOutputBacklogCapChars } from '../../shared/terminal-scrollback-policy' diff --git a/src/main/ipc/remote-watcher-event-batch.test.ts b/src/main/ipc/remote-watcher-event-batch.test.ts index 19e946d4007..99f7afee014 100644 --- a/src/main/ipc/remote-watcher-event-batch.test.ts +++ b/src/main/ipc/remote-watcher-event-batch.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { FsChangeEvent } from '../../shared/types' +import type { FsChangeEvent } from '../../shared/filesystem-entry-types' import { createRemoteWatcherEventBatch } from './remote-watcher-event-batch' const ROOT = '/home/u/repo' diff --git a/src/main/ipc/remote-watcher-event-batch.ts b/src/main/ipc/remote-watcher-event-batch.ts index 713c6068032..85e78d2045e 100644 --- a/src/main/ipc/remote-watcher-event-batch.ts +++ b/src/main/ipc/remote-watcher-event-batch.ts @@ -1,4 +1,4 @@ -import type { FsChangeEvent } from '../../shared/types' +import type { FsChangeEvent } from '../../shared/filesystem-entry-types' import { isWindowsAbsolutePathLike, normalizeRuntimePathForComparison diff --git a/src/main/ipc/remote-workspace-patch-queue.test.ts b/src/main/ipc/remote-workspace-patch-queue.test.ts index 1c97ad6b724..5ce4ac6b0b1 100644 --- a/src/main/ipc/remote-workspace-patch-queue.test.ts +++ b/src/main/ipc/remote-workspace-patch-queue.test.ts @@ -6,7 +6,7 @@ import type { RemoteWorkspaceSnapshot } from '../../shared/remote-workspace-types' import type { SshTarget } from '../../shared/ssh-types' -import type { WorkspaceSessionState } from '../../shared/types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' const { getActiveMultiplexerMock, diff --git a/src/main/ipc/remote-workspace.test.ts b/src/main/ipc/remote-workspace.test.ts index 6da587ac86c..38dc15c4af7 100644 --- a/src/main/ipc/remote-workspace.test.ts +++ b/src/main/ipc/remote-workspace.test.ts @@ -6,7 +6,7 @@ import type { RemoteWorkspaceSnapshot } from '../../shared/remote-workspace-types' import type { SshTarget } from '../../shared/ssh-types' -import type { WorkspaceSessionState } from '../../shared/types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' const { getActiveMultiplexerMock, diff --git a/src/main/ipc/remote-workspace.ts b/src/main/ipc/remote-workspace.ts index 361f66bb21f..1e6e402e3bc 100644 --- a/src/main/ipc/remote-workspace.ts +++ b/src/main/ipc/remote-workspace.ts @@ -14,7 +14,7 @@ import type { RemoteWorkspaceSnapshot } from '../../shared/remote-workspace-types' import type { SshTarget } from '../../shared/ssh-types' -import type { WorkspaceSessionState } from '../../shared/types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { getRepoIdFromWorktreeId } from '../../shared/worktree/id' import { getRemoteWorkspaceNamespace } from './remote-workspace-namespace' import { registerRemoteWorkspaceNotificationHandler } from './remote-workspace-events' diff --git a/src/main/ipc/renderer-shutdown-checkpoint.ts b/src/main/ipc/renderer-shutdown-checkpoint.ts index 121ff86b2e3..1e5df10f1a2 100644 --- a/src/main/ipc/renderer-shutdown-checkpoint.ts +++ b/src/main/ipc/renderer-shutdown-checkpoint.ts @@ -1,6 +1,7 @@ import { ipcMain } from 'electron' import type { ExecutionHostId } from '../../shared/execution-host' -import type { PersistedUIState, WorkspaceSessionState } from '../../shared/types' +import type { PersistedUIState } from '../../shared/persisted-ui-state-types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import type { Store } from '../persistence' type StageBeforeUnloadSyncArgs = { diff --git a/src/main/ipc/repos-add-linked-worktree.test.ts b/src/main/ipc/repos-add-linked-worktree.test.ts index d6880848850..cdcaffd8357 100644 --- a/src/main/ipc/repos-add-linked-worktree.test.ts +++ b/src/main/ipc/repos-add-linked-worktree.test.ts @@ -8,7 +8,7 @@ */ import { describe, expect, it, vi, beforeEach } from 'vitest' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' const { handleMock, diff --git a/src/main/ipc/repos-sparse-presets.test.ts b/src/main/ipc/repos-sparse-presets.test.ts index 049db10c5e3..5b52fbca393 100644 --- a/src/main/ipc/repos-sparse-presets.test.ts +++ b/src/main/ipc/repos-sparse-presets.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as CryptoModule from 'node:crypto' -import type { SparsePreset } from '../../shared/types' +import type { SparsePreset } from '../../shared/worktree/create-types' const { handleMock, randomUUIDMock, mockStore } = vi.hoisted(() => ({ handleMock: vi.fn(), diff --git a/src/main/ipc/repos.ts b/src/main/ipc/repos.ts index 97d31dc36bf..edb9cded4da 100644 --- a/src/main/ipc/repos.ts +++ b/src/main/ipc/repos.ts @@ -6,14 +6,14 @@ import { homedir } from 'node:os' import { z } from 'zod' import type { Store } from '../persistence' import type { OrcaRuntimeService } from '../runtime/orca-runtime' +import type { FolderWorkspace } from '../../shared/folder-workspace-types' import type { - BaseRefSearchResult, - Project, - Repo, + NestedRepoScanResult, ProjectGroup, - FolderWorkspace, - ProjectGroupImportResult, - ProjectUpdateArgs, + ProjectGroupImportResult +} from '../../shared/project-group-types' +import type { + Project, ProjectHostSetupCreateArgs, ProjectHostSetupCreateResult, ProjectHostSetupDeleteArgs, @@ -22,10 +22,10 @@ import type { ProjectHostSetupResult, ProjectHostSetupUpdateArgs, ProjectHostSetupUpdateResult, - NestedRepoScanResult, - BaseRefDefaultResult, - SparsePreset -} from '../../shared/types' + ProjectUpdateArgs +} from '../../shared/project-types' +import type { BaseRefDefaultResult, BaseRefSearchResult, Repo } from '../../shared/repo-types' +import type { SparsePreset } from '../../shared/worktree/create-types' import type { FolderWorkspacePathStatusRequest } from '../../shared/folder-workspace-path-status' import { isFolderRepo } from '../../shared/repo-kind' import { DEFAULT_REPO_BADGE_COLOR } from '../../shared/constants' diff --git a/src/main/ipc/session.ts b/src/main/ipc/session.ts index 98015aa64a7..7c70acb4f45 100644 --- a/src/main/ipc/session.ts +++ b/src/main/ipc/session.ts @@ -1,6 +1,9 @@ import { ipcMain } from 'electron' import type { Store } from '../persistence' -import type { WorkspaceSessionPatch, WorkspaceSessionState } from '../../shared/types' +import type { + WorkspaceSessionPatch, + WorkspaceSessionState +} from '../../shared/workspace-session-state-types' export function registerSessionHandlers(store: Store): void { // Why: hostId is an optional second arg so an older renderer that invokes diff --git a/src/main/ipc/settings.ts b/src/main/ipc/settings.ts index 98e918300ab..ddf9272240b 100644 --- a/src/main/ipc/settings.ts +++ b/src/main/ipc/settings.ts @@ -1,6 +1,7 @@ import { app, BrowserWindow, ipcMain, nativeTheme } from 'electron' import type { Store } from '../persistence' -import type { GlobalSettings, PersistedState } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { PersistedState } from '../../shared/persisted-state-types' import { listSystemFontFamilies } from '../system-fonts' import { previewGhosttyImport } from '../ghostty/index' import { previewWarpThemeImport } from '../warp-themes' diff --git a/src/main/ipc/source-control-ai-linked-issue.ts b/src/main/ipc/source-control-ai-linked-issue.ts index aa0696dd913..bd8a28d075b 100644 --- a/src/main/ipc/source-control-ai-linked-issue.ts +++ b/src/main/ipc/source-control-ai-linked-issue.ts @@ -1,7 +1,7 @@ import { resolve } from 'node:path' import type { Store } from '../persistence' import { isLinkedIssueNumber } from '../../shared/source-control-ai-action-variables' -import type { WorktreeMeta } from '../../shared/types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id' export type LinkedIssueLookupArgs = { diff --git a/src/main/ipc/ssh-browse.ts b/src/main/ipc/ssh-browse.ts index f2a779ee412..c7270e50b03 100644 --- a/src/main/ipc/ssh-browse.ts +++ b/src/main/ipc/ssh-browse.ts @@ -2,7 +2,7 @@ import { ipcMain } from 'electron' import type { SshConnectionManager } from '../ssh/ssh-connection-manager' import type { SshExecOptions } from '../ssh/ssh-connection-utils' import { powerShellCommand, powerShellLiteral } from '../ssh/ssh-remote-powershell' -import type { FilesystemPathFlavor } from '../../shared/types' +import type { FilesystemPathFlavor } from '../../shared/filesystem-entry-types' import { sortDirEntries } from '../../shared/file-name-sort' export type RemoteDirEntry = { diff --git a/src/main/ipc/telemetry.test.ts b/src/main/ipc/telemetry.test.ts index 2645d98dff5..9b28270551a 100644 --- a/src/main/ipc/telemetry.test.ts +++ b/src/main/ipc/telemetry.test.ts @@ -1,7 +1,7 @@ // Telemetry IPC boundary tests: the renderer is in the threat model, so handlers drop malformed calls, cap consent mutations, and derive `via` main-side. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import type { Store } from '../persistence' const handlers = new Map unknown>() diff --git a/src/main/ipc/ui.ts b/src/main/ipc/ui.ts index 8d2abe92d84..0e967381160 100644 --- a/src/main/ipc/ui.ts +++ b/src/main/ipc/ui.ts @@ -1,6 +1,6 @@ import { BrowserWindow, ipcMain, webContents, type WebContents } from 'electron' import type { Store } from '../persistence' -import type { PersistedUIState } from '../../shared/types' +import type { PersistedUIState } from '../../shared/persisted-ui-state-types' import { isFeatureInteractionId } from '../../shared/feature-interactions' let trustedUIRendererWebContentsId: number | null = null diff --git a/src/main/ipc/workspace-cleanup-activity.test.ts b/src/main/ipc/workspace-cleanup-activity.test.ts index bfb1c080db3..a7d1e980266 100644 --- a/src/main/ipc/workspace-cleanup-activity.test.ts +++ b/src/main/ipc/workspace-cleanup-activity.test.ts @@ -1,6 +1,7 @@ import path from 'node:path' import { describe, expect, it, vi } from 'vitest' -import type { Repo, Worktree } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' +import type { Worktree } from '../../shared/worktree/types' import { resolveWorkspaceCleanupActivityWorktree } from './workspace-cleanup-activity' const REPO: Repo = { diff --git a/src/main/ipc/workspace-cleanup-activity.ts b/src/main/ipc/workspace-cleanup-activity.ts index 56ac36f6b29..93a6b5673cd 100644 --- a/src/main/ipc/workspace-cleanup-activity.ts +++ b/src/main/ipc/workspace-cleanup-activity.ts @@ -1,6 +1,7 @@ import { lstat, readFile } from 'node:fs/promises' import path from 'node:path' -import type { Repo, Worktree } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' +import type { Worktree } from '../../shared/worktree/types' import { getPersistedWorkspaceCleanupActivityAt } from '../../shared/workspace-cleanup' import { parseWslUncPath } from '../../shared/wsl-paths' import { toWindowsWslPath } from '../wsl' diff --git a/src/main/ipc/workspace-cleanup-candidate.ts b/src/main/ipc/workspace-cleanup-candidate.ts index e25af90964c..d6e93429b8a 100644 --- a/src/main/ipc/workspace-cleanup-candidate.ts +++ b/src/main/ipc/workspace-cleanup-candidate.ts @@ -1,6 +1,7 @@ import type { IGitProvider } from '../providers/types' import { isFolderRepo } from '../../shared/repo-kind' -import type { Repo, Worktree } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' +import type { Worktree } from '../../shared/worktree/types' import { applyWorkspaceCleanupPolicy, createWorkspaceCleanupFingerprint, diff --git a/src/main/ipc/workspace-cleanup-disconnected-ssh.ts b/src/main/ipc/workspace-cleanup-disconnected-ssh.ts index f7128d26827..264e1eac9e2 100644 --- a/src/main/ipc/workspace-cleanup-disconnected-ssh.ts +++ b/src/main/ipc/workspace-cleanup-disconnected-ssh.ts @@ -1,6 +1,7 @@ import { basename } from 'node:path' import type { Store } from '../persistence' -import type { Repo, WorktreeMeta } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' import { applyWorkspaceCleanupPolicy, createWorkspaceCleanupFingerprint, diff --git a/src/main/ipc/workspace-cleanup-git-evidence.ts b/src/main/ipc/workspace-cleanup-git-evidence.ts index 04945327f98..91aca462c7a 100644 --- a/src/main/ipc/workspace-cleanup-git-evidence.ts +++ b/src/main/ipc/workspace-cleanup-git-evidence.ts @@ -1,7 +1,9 @@ import { getStatus } from '../git/status' import { gitExecFileAsync } from '../git/runner' import type { IGitProvider } from '../providers/types' -import type { GitStatusResult, Repo, Worktree } from '../../shared/types' +import type { GitStatusResult } from '../../shared/git-status-types' +import type { Repo } from '../../shared/repo-types' +import type { Worktree } from '../../shared/worktree/types' import type { WorkspaceCleanupBlocker } from '../../shared/workspace-cleanup' import { WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS, diff --git a/src/main/ipc/workspace-cleanup-local-git-routing.test.ts b/src/main/ipc/workspace-cleanup-local-git-routing.test.ts index 656b904d0cd..a10085fae77 100644 --- a/src/main/ipc/workspace-cleanup-local-git-routing.test.ts +++ b/src/main/ipc/workspace-cleanup-local-git-routing.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { Store } from '../persistence' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' const { listRepoWorktreesMock, getLocalProjectWorktreeGitOptionsMock } = vi.hoisted(() => ({ listRepoWorktreesMock: vi.fn(), diff --git a/src/main/ipc/workspace-cleanup-scan-primitives.ts b/src/main/ipc/workspace-cleanup-scan-primitives.ts index 4f0a84c8a86..8b4c5c839b4 100644 --- a/src/main/ipc/workspace-cleanup-scan-primitives.ts +++ b/src/main/ipc/workspace-cleanup-scan-primitives.ts @@ -1,5 +1,5 @@ import { basename } from 'node:path' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import type { WorkspaceCleanupScanError } from '../../shared/workspace-cleanup' export const WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS = 8_000 diff --git a/src/main/ipc/workspace-cleanup-scan.ts b/src/main/ipc/workspace-cleanup-scan.ts index 41f94a07814..7072199ffcd 100644 --- a/src/main/ipc/workspace-cleanup-scan.ts +++ b/src/main/ipc/workspace-cleanup-scan.ts @@ -3,7 +3,8 @@ import { listRepoWorktrees, createFolderWorktree } from '../repo-worktrees' import { getSshGitProvider } from '../providers/ssh-git-dispatch' import type { IGitProvider } from '../providers/types' import { isFolderRepo } from '../../shared/repo-kind' -import type { GitWorktreeInfo, Repo, Worktree } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' +import type { GitWorktreeInfo, Worktree } from '../../shared/worktree/types' import { mergeWorktree } from './worktree-logic' import { splitWorktreeId } from '../../shared/worktree/id' import type { diff --git a/src/main/ipc/workspace-cleanup.test.ts b/src/main/ipc/workspace-cleanup.test.ts index 5d5538ca3df..96f0ce821f8 100644 --- a/src/main/ipc/workspace-cleanup.test.ts +++ b/src/main/ipc/workspace-cleanup.test.ts @@ -2,13 +2,11 @@ import path from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ipcMain } from 'electron' import type { Store } from '../persistence' -import type { - DiffComment, - GitStatusResult, - GitWorktreeInfo, - Repo, - WorktreeMeta -} from '../../shared/types' +import type { DiffComment } from '../../shared/diff-comment-types' +import type { GitStatusResult } from '../../shared/git-status-types' +import type { Repo } from '../../shared/repo-types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' +import type { GitWorktreeInfo } from '../../shared/worktree/types' import type { WorkspaceCleanupScanProgress } from '../../shared/workspace-cleanup' const { diff --git a/src/main/ipc/worktree-base-directory-change-collector.ts b/src/main/ipc/worktree-base-directory-change-collector.ts index c77f12104ee..1f1f37dcedd 100644 --- a/src/main/ipc/worktree-base-directory-change-collector.ts +++ b/src/main/ipc/worktree-base-directory-change-collector.ts @@ -1,4 +1,4 @@ -import type { FsChangeEvent } from '../../shared/types' +import type { FsChangeEvent } from '../../shared/filesystem-entry-types' import { classifyWorktreeBaseChange, type WorktreeBaseWatchTarget diff --git a/src/main/ipc/worktree-base-directory-watch-targets.ts b/src/main/ipc/worktree-base-directory-watch-targets.ts index 1283333ee95..cfa825e09ab 100644 --- a/src/main/ipc/worktree-base-directory-watch-targets.ts +++ b/src/main/ipc/worktree-base-directory-watch-targets.ts @@ -3,7 +3,8 @@ import { realpath, stat } from 'node:fs/promises' import type { Stats } from 'node:fs' import type { Store } from '../persistence' import type { FileStat, IFilesystemProvider } from '../providers/types' -import type { GlobalSettings, Repo } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { Repo } from '../../shared/repo-types' import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' import { isFolderRepo } from '../../shared/repo-kind' import { diff --git a/src/main/ipc/worktree-base-directory-watcher.test.ts b/src/main/ipc/worktree-base-directory-watcher.test.ts index d8f58ff8503..9ce0712c9a0 100644 --- a/src/main/ipc/worktree-base-directory-watcher.test.ts +++ b/src/main/ipc/worktree-base-directory-watcher.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { join, sep } from 'node:path' -import type { GlobalSettings, Repo } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { Repo } from '../../shared/repo-types' import type { WorktreeBasePollEvent, WorktreeBasePollerOptions diff --git a/src/main/ipc/worktree-common-git-directory.ts b/src/main/ipc/worktree-common-git-directory.ts index 7fb464f3c94..2a334055c17 100644 --- a/src/main/ipc/worktree-common-git-directory.ts +++ b/src/main/ipc/worktree-common-git-directory.ts @@ -1,6 +1,6 @@ import type { Stats } from 'node:fs' import { readFile, stat } from 'node:fs/promises' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import { getRuntimePathBasename, normalizeRuntimePathSeparators, diff --git a/src/main/ipc/worktree-folder-rename-target.ts b/src/main/ipc/worktree-folder-rename-target.ts index 9cdbe7188c2..d471d41b2c7 100644 --- a/src/main/ipc/worktree-folder-rename-target.ts +++ b/src/main/ipc/worktree-folder-rename-target.ts @@ -1,5 +1,5 @@ import { posix } from 'node:path' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree/id' import { computeWorktreePath } from './worktree-logic' diff --git a/src/main/ipc/worktree-head-identity-reader.ts b/src/main/ipc/worktree-head-identity-reader.ts index e99de115480..0e44376c00b 100644 --- a/src/main/ipc/worktree-head-identity-reader.ts +++ b/src/main/ipc/worktree-head-identity-reader.ts @@ -1,6 +1,6 @@ import { readdir, readFile } from 'node:fs/promises' import { basename, dirname, isAbsolute, join } from 'node:path' -import type { WorktreeHeadIdentity } from '../../shared/types' +import type { WorktreeHeadIdentity } from '../../shared/worktree/types' // Why: the whole point of this reader is replacing `git worktree list` fanout // with bounded metadata-file reads, so head freshness never re-creates the diff --git a/src/main/ipc/worktree-linked-work-item-metadata.ts b/src/main/ipc/worktree-linked-work-item-metadata.ts index e0b4259a2fc..e07e78ddfcb 100644 --- a/src/main/ipc/worktree-linked-work-item-metadata.ts +++ b/src/main/ipc/worktree-linked-work-item-metadata.ts @@ -1,4 +1,5 @@ -import type { Worktree, WorktreeMeta } from '../../shared/types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' +import type { Worktree } from '../../shared/worktree/types' type LinkedWorkItemMetadata = Pick< Worktree, diff --git a/src/main/ipc/worktree-logic.ts b/src/main/ipc/worktree-logic.ts index 45c6cbdff03..5e3878924f2 100644 --- a/src/main/ipc/worktree-logic.ts +++ b/src/main/ipc/worktree-logic.ts @@ -1,5 +1,6 @@ import { resolve, relative, isAbsolute, posix, sep, win32 } from 'node:path' -import type { GlobalSettings, OrcaWorkspaceLayout, Repo } from '../../shared/types' +import type { GlobalSettings, OrcaWorkspaceLayout } from '../../shared/global-settings-types' +import type { Repo } from '../../shared/repo-types' import { isWindowsAbsolutePathLike, resolveRuntimePath } from '../../shared/cross-platform-path' import { isWslUncPath } from '../../shared/wsl-paths' import { splitWorktreeId } from '../../shared/worktree/id' diff --git a/src/main/ipc/worktree-metadata-merge.ts b/src/main/ipc/worktree-metadata-merge.ts index 6b6accf724f..4fdfe07c72a 100644 --- a/src/main/ipc/worktree-metadata-merge.ts +++ b/src/main/ipc/worktree-metadata-merge.ts @@ -1,5 +1,6 @@ import { basename } from 'node:path' -import type { GitWorktreeInfo, Worktree, WorktreeMeta } from '../../shared/types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' +import type { GitWorktreeInfo, Worktree } from '../../shared/worktree/types' import { DEFAULT_WORKSPACE_STATUS_ID } from '../../shared/workspace-statuses' import { getLinkedWorkItemMetadata } from './worktree-linked-work-item-metadata' import { normalizeWorkspaceCreatorProvenance } from '../../shared/workspace-creator-provenance' diff --git a/src/main/ipc/worktree-push-target-cleanup.test.ts b/src/main/ipc/worktree-push-target-cleanup.test.ts index 24234aaa0ab..b24d98c39f6 100644 --- a/src/main/ipc/worktree-push-target-cleanup.test.ts +++ b/src/main/ipc/worktree-push-target-cleanup.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi, type Mock } from 'vitest' -import type { GitPushTarget, WorktreeMeta } from '../../shared/types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' +import type { GitPushTarget } from '../../shared/worktree/types' import { cleanupUnusedWorktreePushTargetRemoteWithExec, sameGitHubRemoteUrl, diff --git a/src/main/ipc/worktree-push-target-cleanup.ts b/src/main/ipc/worktree-push-target-cleanup.ts index 0fd1faf10ad..2bda01c5a89 100644 --- a/src/main/ipc/worktree-push-target-cleanup.ts +++ b/src/main/ipc/worktree-push-target-cleanup.ts @@ -4,7 +4,7 @@ // the multi-fork cleanup matrix is unit-testable without a real repo. import type { Store } from '../persistence' -import type { GitPushTarget } from '../../shared/types' +import type { GitPushTarget } from '../../shared/worktree/types' import { parseGitHubOwnerRepo } from '../github/gh-utils' import { getRepoIdFromWorktreeId } from '../../shared/worktree/id' import { iterateProcessOutputLines } from '../../shared/process-output-field-scanner' diff --git a/src/main/ipc/worktree-push-target-setup.test.ts b/src/main/ipc/worktree-push-target-setup.test.ts index 429c660e6e9..a93a22bb4be 100644 --- a/src/main/ipc/worktree-push-target-setup.test.ts +++ b/src/main/ipc/worktree-push-target-setup.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi, type Mock } from 'vitest' -import type { GitPushTarget } from '../../shared/types' +import type { GitPushTarget } from '../../shared/worktree/types' import type { GitRemoteExec } from './worktree-push-target-cleanup' import { configureCreatedWorktreePushTargetWithExec, diff --git a/src/main/ipc/worktree-push-target-setup.ts b/src/main/ipc/worktree-push-target-setup.ts index e6a99ab4443..4101055ba65 100644 --- a/src/main/ipc/worktree-push-target-setup.ts +++ b/src/main/ipc/worktree-push-target-setup.ts @@ -4,7 +4,7 @@ // remote-reuse / unique-naming / fetch behavior is unit-testable without a real // repo. The store-aware ownership decision stays with the caller via a predicate. -import type { GitPushTarget } from '../../shared/types' +import type { GitPushTarget } from '../../shared/worktree/types' import { parseGitHubOwnerRepo } from '../github/gh-utils' import type { GitRemoteExec } from './worktree-push-target-cleanup' diff --git a/src/main/ipc/worktree-remote.ts b/src/main/ipc/worktree-remote.ts index 8d9e70481cd..d4d91dcd79b 100644 --- a/src/main/ipc/worktree-remote.ts +++ b/src/main/ipc/worktree-remote.ts @@ -6,21 +6,25 @@ import { posix, win32 } from 'node:path' import { existsSync } from 'node:fs' import { randomUUID } from 'node:crypto' import type { Store } from '../persistence' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { Repo } from '../../shared/repo-types' +import type { + LocalBaseRefRefreshResult, + LocalBaseRefUpdateSuggestion +} from '../../shared/worktree/base-ref-drift-types' +import type { + CreateWorktreeArgs, + CreateWorktreeResult, + WorktreeCreateBaseFallback +} from '../../shared/worktree/create-types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' import type { AutomationWorkspaceProvenance, CliWorkspaceProvenance, - CreateWorktreeArgs, - CreateWorktreeResult, GitPushTarget, - GlobalSettings, - LocalBaseRefRefreshResult, - LocalBaseRefUpdateSuggestion, - Repo, Worktree, - WorktreeCreateBaseFallback, - WorktreeHeadIdentity, - WorktreeMeta -} from '../../shared/types' + WorktreeHeadIdentity +} from '../../shared/worktree/types' import { getPRForBranch } from '../github/client' import { listWorktrees, addWorktree, addSparseWorktree } from '../git/worktree' import type { AddWorktreeOptions, AddWorktreeResult } from '../git/worktree' diff --git a/src/main/ipc/worktrees.test.ts b/src/main/ipc/worktrees.test.ts index 02f23bc094a..9e2ae8ea8aa 100644 --- a/src/main/ipc/worktrees.test.ts +++ b/src/main/ipc/worktrees.test.ts @@ -4,7 +4,9 @@ import type * as GitUsernameModule from '../git/git-username' import { lstat, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' -import type { CreateWorktreeResult, GitWorktreeInfo, Repo, Worktree } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' +import type { CreateWorktreeResult } from '../../shared/worktree/create-types' +import type { GitWorktreeInfo, Worktree } from '../../shared/worktree/types' import type { ProviderRequestId } from '../../shared/detected-worktree-provider-contract' import { LOCAL_EXECUTION_HOST_ID, toSshExecutionHostId } from '../../shared/execution-host' import * as localWorktreeFilesystem from '../local-worktree-filesystem' diff --git a/src/main/ipc/worktrees.ts b/src/main/ipc/worktrees.ts index e56dd3cc13d..32c85af85a0 100644 --- a/src/main/ipc/worktrees.ts +++ b/src/main/ipc/worktrees.ts @@ -17,26 +17,27 @@ import { getProjectGroupSubtreeIds } from '../../shared/project-groups' import { projectResolvedWorktreeLineage } from '../../shared/resolved-worktree-lineage' import { isPathInsideOrEqual, isWindowsAbsolutePathLike } from '../../shared/cross-platform-path' import { deleteWorktreeHistoryDir } from '../terminal-history-deletion' +import type { OrcaHooks } from '../../shared/orca-yaml-hook-types' +import type { Repo } from '../../shared/repo-types' import type { - AutomationWorkspaceProvenance, AdoptProvisionedRootArgs, - CliWorkspaceProvenance, CreateWorktreeArgs, CreateWorktreeResult, + ForceDeleteWorktreeBranchResult, + RemoveWorktreeResult +} from '../../shared/worktree/create-types' +import type { WorkspaceLineage, WorktreeLineage } from '../../shared/worktree/lineage-types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' +import type { + AutomationWorkspaceProvenance, + CliWorkspaceProvenance, DetectedWorktree, DetectedWorktreeListResult, - ForceDeleteWorktreeBranchResult, GitHubPrStartPoint, GitPushTarget, GitWorktreeInfo, - OrcaHooks, - Repo, - RemoveWorktreeResult, - Worktree, - WorktreeLineage, - WorkspaceLineage, - WorktreeMeta -} from '../../shared/types' + Worktree +} from '../../shared/worktree/types' import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree/removal' import { getRepoExecutionHostId, diff --git a/src/main/jira/client.ts b/src/main/jira/client.ts index cd3497fb2be..f9c4e8943c0 100644 --- a/src/main/jira/client.ts +++ b/src/main/jira/client.ts @@ -20,7 +20,7 @@ import type { JiraSite, JiraSiteSelection, JiraViewer -} from '../../shared/types' +} from '../../shared/jira-types' import { clearAttachmentImagesForSite } from './attachment-image-cache' // Why: Atlassian's XSRF filter rejects POST/PUT REST calls that carry a browser diff --git a/src/main/jira/issues.ts b/src/main/jira/issues.ts index e2eade0924e..0629f6abfd3 100644 --- a/src/main/jira/issues.ts +++ b/src/main/jira/issues.ts @@ -20,7 +20,7 @@ import type { JiraStatus, JiraTransition, JiraUser -} from '../../shared/types' +} from '../../shared/jira-types' import { acquire, apiBasePath, diff --git a/src/main/kimi/kimi-runtime-home.test.ts b/src/main/kimi/kimi-runtime-home.test.ts index d56abb658d1..a1977e8d857 100644 --- a/src/main/kimi/kimi-runtime-home.test.ts +++ b/src/main/kimi/kimi-runtime-home.test.ts @@ -9,7 +9,7 @@ vi.mock('../wsl', () => wslMocks) vi.mock('node:os', () => ({ homedir: () => 'C:\\Users\\neil' })) import { getHostKimiHome, getKimiRuntimeTarget, resolveKimiHome } from './kimi-runtime-home' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' function settings(overrides: Partial): GlobalSettings { return overrides as GlobalSettings diff --git a/src/main/kimi/kimi-runtime-home.ts b/src/main/kimi/kimi-runtime-home.ts index 9b0cf799638..709cb755879 100644 --- a/src/main/kimi/kimi-runtime-home.ts +++ b/src/main/kimi/kimi-runtime-home.ts @@ -5,7 +5,7 @@ import { resolveLocalAccountRuntimeTarget, type LocalAccountRuntimeTarget } from '../../shared/local-account-runtime' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { getWslHomeAsync, listWslDistrosAsync } from '../wsl' export type KimiHomeResolution = LocalAccountRuntimeTarget & { diff --git a/src/main/linear/client.ts b/src/main/linear/client.ts index 7058d130597..714fb6c165e 100644 --- a/src/main/linear/client.ts +++ b/src/main/linear/client.ts @@ -17,7 +17,7 @@ import type { LinearViewer, LinearWorkspace, LinearWorkspaceSelection -} from '../../shared/types' +} from '../../shared/linear/workspace-types' // ── Concurrency limiter — max 4 parallel Linear API calls ──────────── const MAX_CONCURRENT = 4 diff --git a/src/main/linear/issue-context-client.ts b/src/main/linear/issue-context-client.ts index 1e3a45bd301..47c2bb219dc 100644 --- a/src/main/linear/issue-context-client.ts +++ b/src/main/linear/issue-context-client.ts @@ -1,6 +1,6 @@ import type { LinearSearchIssueSummary, LinearSearchResult } from '../../shared/linear/agent-access' import { clampLinearSearchLimit } from '../../shared/linear/agent-access' -import type { LinearWorkspace } from '../../shared/types' +import type { LinearWorkspace } from '../../shared/linear/workspace-types' import { acquire, clearToken, diff --git a/src/main/linear/issue-context-current.test.ts b/src/main/linear/issue-context-current.test.ts index fd3804bbe9b..022b75525ff 100644 --- a/src/main/linear/issue-context-current.test.ts +++ b/src/main/linear/issue-context-current.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { LinearWorkspace } from '../../shared/types' +import type { LinearWorkspace } from '../../shared/linear/workspace-types' const { connectedWorkspaces } = vi.hoisted(() => ({ connectedWorkspaces: [] as LinearWorkspace[] diff --git a/src/main/linear/issue-context-fanout.ts b/src/main/linear/issue-context-fanout.ts index c28f03a2c9c..77b6186b37e 100644 --- a/src/main/linear/issue-context-fanout.ts +++ b/src/main/linear/issue-context-fanout.ts @@ -1,5 +1,5 @@ import type { LinearErrorCode, LinearWorkspaceCandidate } from '../../shared/linear/agent-access' -import type { LinearWorkspace } from '../../shared/types' +import type { LinearWorkspace } from '../../shared/linear/workspace-types' import { getClients, getStatus, type LinearClientForWorkspace } from './client' import { LinearAgentAccessError, diff --git a/src/main/linear/issue-context-workspaces.ts b/src/main/linear/issue-context-workspaces.ts index 47ad620a0a3..e1c70d2f37f 100644 --- a/src/main/linear/issue-context-workspaces.ts +++ b/src/main/linear/issue-context-workspaces.ts @@ -1,5 +1,5 @@ import type { LinearWorkspaceCandidate } from '../../shared/linear/agent-access' -import type { LinearWorkspace } from '../../shared/types' +import type { LinearWorkspace } from '../../shared/linear/workspace-types' import { linearError } from './issue-context-errors' export function resolveWorkspaceSelector( diff --git a/src/main/linear/issue-context.test.ts b/src/main/linear/issue-context.test.ts index b88dff4563e..e94c490dbc1 100644 --- a/src/main/linear/issue-context.test.ts +++ b/src/main/linear/issue-context.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { LinearClientForWorkspace } from './client' -import type { LinearWorkspace } from '../../shared/types' +import type { LinearWorkspace } from '../../shared/linear/workspace-types' const getClients = vi.fn() const getStatus = vi.fn() diff --git a/src/main/linear/issues.ts b/src/main/linear/issues.ts index 301bc66024a..43e9cf7b699 100644 --- a/src/main/linear/issues.ts +++ b/src/main/linear/issues.ts @@ -1,14 +1,13 @@ /* eslint-disable max-lines -- Why: Linear issue reads and mutations share the same workspace fan-out/error handling, so keeping them together avoids drifting auth-clearing behavior between operations. */ +import type { LinearIssueUpdate } from '../../shared/issue-mutation-types' +import type { LinearComment, LinearIssue } from '../../shared/linear/issue-types' import type { - LinearIssue, - LinearIssueUpdate, - LinearComment, LinearCollectionResult, LinearWorkspaceError, LinearWorkspaceSelection -} from '../../shared/types' +} from '../../shared/linear/workspace-types' import type { LinearClient } from '@linear/sdk' import { loadLinearSdk } from './linear-sdk' import { diff --git a/src/main/linear/linear-team-pages.ts b/src/main/linear/linear-team-pages.ts index abc1c485a8b..624f57c6d8e 100644 --- a/src/main/linear/linear-team-pages.ts +++ b/src/main/linear/linear-team-pages.ts @@ -1,4 +1,9 @@ -import type { LinearLabel, LinearMember, LinearTeam, LinearWorkflowState } from '../../shared/types' +import type { + LinearLabel, + LinearMember, + LinearTeam, + LinearWorkflowState +} from '../../shared/linear/workspace-types' import { buildLinearTeamUrl } from '../../shared/linear/links' import type { LinearClientForWorkspace } from './client' diff --git a/src/main/linear/mappers.ts b/src/main/linear/mappers.ts index 7ea55be7fca..026d70400bf 100644 --- a/src/main/linear/mappers.ts +++ b/src/main/linear/mappers.ts @@ -1,5 +1,5 @@ import type { Issue, IssueSearchResult } from '@linear/sdk' -import type { LinearIssue, LinearIssueChildSummary } from '../../shared/types' +import type { LinearIssue, LinearIssueChildSummary } from '../../shared/linear/issue-types' type IssueWithChildren = Issue & { children: Issue['children'] diff --git a/src/main/linear/projects.ts b/src/main/linear/projects.ts index d55d2f6e6a6..5063a87b17c 100644 --- a/src/main/linear/projects.ts +++ b/src/main/linear/projects.ts @@ -1,17 +1,19 @@ /* eslint-disable max-lines -- Why: Linear project and custom-view reads share raw GraphQL selection sets, workspace fan-out, and partial-failure mapping. */ +import type { LinearIssue } from '../../shared/linear/issue-types' +import type { + LinearCustomViewModel, + LinearCustomViewSummary, + LinearProjectDetail, + LinearProjectMemberSummary, + LinearProjectSummary +} from '../../shared/linear/project-types' import type { LinearCollectionResult, LinearConcreteWorkspaceId, - LinearCustomViewModel, - LinearCustomViewSummary, - LinearIssue, - LinearProjectDetail, - LinearProjectMemberSummary, - LinearProjectSummary, LinearWorkspaceError, LinearWorkspaceSelection -} from '../../shared/types' +} from '../../shared/linear/workspace-types' import { LINEAR_ISSUE_API_PAGE_SIZE_MAX, clampLinearIssueListLimit diff --git a/src/main/linear/teams.ts b/src/main/linear/teams.ts index 004c8fbd92a..8a103929b7e 100644 --- a/src/main/linear/teams.ts +++ b/src/main/linear/teams.ts @@ -1,12 +1,12 @@ import type { Team } from '@linear/sdk' import type { - LinearTeam, - LinearWorkflowState, LinearLabel, LinearMember, + LinearTeam, + LinearWorkflowState, LinearWorkspaceError, LinearWorkspaceSelection -} from '../../shared/types' +} from '../../shared/linear/workspace-types' import { acquire, release, getClients, isAuthError, clearToken } from './client' import { fetchAllTeamLabels, diff --git a/src/main/linux-package-install-command.ts b/src/main/linux-package-install-command.ts index 6a0c6d2d526..60a8ecdca00 100644 --- a/src/main/linux-package-install-command.ts +++ b/src/main/linux-package-install-command.ts @@ -1,6 +1,6 @@ import { statSync } from 'node:fs' import path from 'node:path' -import type { LinuxRootPackageType } from '../shared/types' +import type { LinuxRootPackageType } from '../shared/update-status-types' // Why: an absolute but user-writable PATH entry must never be treated as a trusted package manager. const TRUSTED_EXECUTABLE_DIRECTORIES = ['/usr/bin', '/bin', '/usr/sbin', '/sbin'] diff --git a/src/main/linux-package-install-diagnostic.ts b/src/main/linux-package-install-diagnostic.ts index 3d0afd7fdf9..bdef7c5450e 100644 --- a/src/main/linux-package-install-diagnostic.ts +++ b/src/main/linux-package-install-diagnostic.ts @@ -1,5 +1,5 @@ import os from 'node:os' -import type { LinuxPackageInstallFailureReason } from '../shared/types' +import type { LinuxPackageInstallFailureReason } from '../shared/update-status-types' /** The redacted text shown locally, paired with the reason classified from the ORIGINAL output. */ export type LinuxPackageInstallDiagnostic = { diff --git a/src/main/linux-package-update-recovery.test.ts b/src/main/linux-package-update-recovery.test.ts index 1d9bf8d8650..ec2738b823b 100644 --- a/src/main/linux-package-update-recovery.test.ts +++ b/src/main/linux-package-update-recovery.test.ts @@ -4,7 +4,7 @@ import os from 'node:os' import path from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as NodeFs from 'node:fs' -import type { LinuxPackageInstallRecovery } from '../shared/types' +import type { LinuxPackageInstallRecovery } from '../shared/update-status-types' import type * as RecoveryModule from './linux-package-update-recovery' const { showItemInFolderMock, getPackageTypeMock, buildCommandMock, hashPasses } = vi.hoisted( diff --git a/src/main/linux-package-update-recovery.ts b/src/main/linux-package-update-recovery.ts index f854b63c5b8..e5269a2077b 100644 --- a/src/main/linux-package-update-recovery.ts +++ b/src/main/linux-package-update-recovery.ts @@ -4,7 +4,10 @@ import fsp from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import { shell } from 'electron' -import type { LinuxPackageInstallRecovery, LinuxRootPackageType } from '../shared/types' +import type { + LinuxPackageInstallRecovery, + LinuxRootPackageType +} from '../shared/update-status-types' import { getLinuxRootPackageType } from './linux-update-package-type' import { buildLinuxPackageInstallCommand } from './linux-package-install-command' diff --git a/src/main/linux-update-package-type.ts b/src/main/linux-update-package-type.ts index c8af66649a5..5acc83a4cf3 100644 --- a/src/main/linux-update-package-type.ts +++ b/src/main/linux-update-package-type.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs' import path from 'node:path' import { app } from 'electron' -import type { LinuxRootPackageType } from '../shared/types' +import type { LinuxRootPackageType } from '../shared/update-status-types' export type { LinuxRootPackageType } diff --git a/src/main/local-project-runtime-resolution.ts b/src/main/local-project-runtime-resolution.ts index e86a2f31927..6f3e7b57f09 100644 --- a/src/main/local-project-runtime-resolution.ts +++ b/src/main/local-project-runtime-resolution.ts @@ -1,5 +1,6 @@ import type { Store } from './persistence' -import type { Project, Repo } from '../shared/types' +import type { Project } from '../shared/project-types' +import type { Repo } from '../shared/repo-types' import { resolveProjectExecutionRuntime, type ProjectExecutionRuntimeResolution diff --git a/src/main/local-worktree-removal-recovery.ts b/src/main/local-worktree-removal-recovery.ts index 578743029f6..630dd338c20 100644 --- a/src/main/local-worktree-removal-recovery.ts +++ b/src/main/local-worktree-removal-recovery.ts @@ -1,4 +1,5 @@ -import type { GitWorktreeInfo, RemoveWorktreeResult } from '../shared/types' +import type { RemoveWorktreeResult } from '../shared/worktree/create-types' +import type { GitWorktreeInfo } from '../shared/worktree/types' import { assertWorktreeUnlockedForRemoval } from '../shared/worktree/removal' import { areWorktreePathsEqual, formatWorktreeRemovalError } from './ipc/worktree-logic' import { gitExecFileAsync } from './git/runner' diff --git a/src/main/memory/collector.ts b/src/main/memory/collector.ts index c1fe58989ad..1f000706c1e 100644 --- a/src/main/memory/collector.ts +++ b/src/main/memory/collector.ts @@ -27,7 +27,12 @@ import { iterateProcessOutputLines } from '../../shared/process-output-field-scanner' import { app } from 'electron' -import type { AppMemory, MemorySnapshot, SessionMemory, WorktreeMemory } from '../../shared/types' +import type { + AppMemory, + MemorySnapshot, + SessionMemory, + WorktreeMemory +} from '../../shared/process-stats-types' import type { Store } from '../persistence' import { ORPHAN_WORKTREE_ID } from '../../shared/constants' import { listRegisteredPtys } from './pty-registry' diff --git a/src/main/memory/host-memory.ts b/src/main/memory/host-memory.ts index 5622bd27d72..5d7ee54746d 100644 --- a/src/main/memory/host-memory.ts +++ b/src/main/memory/host-memory.ts @@ -2,7 +2,7 @@ import { execFile } from 'node:child_process' import { readFile } from 'node:fs/promises' import os from 'node:os' import path from 'node:path' -import type { HostAvailableMemorySource, HostMemory } from '../../shared/types' +import type { HostAvailableMemorySource, HostMemory } from '../../shared/process-stats-types' const MEMORY_PRESSURE_TIMEOUT_MS = 1_000 const MEMORY_PRESSURE_MAX_BUFFER = 64 * 1024 diff --git a/src/main/memory/hydrate-local-pty-registry.test.ts b/src/main/memory/hydrate-local-pty-registry.test.ts index 714f18b3cba..7b9a90bfb73 100644 --- a/src/main/memory/hydrate-local-pty-registry.test.ts +++ b/src/main/memory/hydrate-local-pty-registry.test.ts @@ -11,7 +11,7 @@ */ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import type { SessionInfo } from '../daemon/types' import type { DaemonPtyAdapter } from '../daemon/daemon-pty-adapter' import type { Store } from '../persistence' diff --git a/src/main/memory/process-memory-metric.ts b/src/main/memory/process-memory-metric.ts index 79716cb1f8d..49d9fded195 100644 --- a/src/main/memory/process-memory-metric.ts +++ b/src/main/memory/process-memory-metric.ts @@ -1,5 +1,5 @@ import os from 'node:os' -import type { ProcessMemoryMetric } from '../../shared/types' +import type { ProcessMemoryMetric } from '../../shared/process-stats-types' export function getProcessMemoryMetric( platform: NodeJS.Platform = os.platform() diff --git a/src/main/menu/register-app-menu.ts b/src/main/menu/register-app-menu.ts index cc99c69cbc7..2f77e7f8941 100644 --- a/src/main/menu/register-app-menu.ts +++ b/src/main/menu/register-app-menu.ts @@ -5,7 +5,7 @@ import { type KeybindingActionId, type KeybindingOverrides } from '../../shared/keybindings' -import type { UpdateCheckOptions } from '../../shared/types' +import type { UpdateCheckOptions } from '../../shared/update-status-types' import { translateMain } from '../i18n/main-i18n' import { createAppMenuSelectionItem } from './app-menu-selection-item' diff --git a/src/main/orca-profiles/profile-index-store.ts b/src/main/orca-profiles/profile-index-store.ts index 8c1b4490f45..7897e299a21 100644 --- a/src/main/orca-profiles/profile-index-store.ts +++ b/src/main/orca-profiles/profile-index-store.ts @@ -9,7 +9,7 @@ import { import { randomUUID } from 'node:crypto' import { dirname } from 'node:path' import { bestEffortFsyncDirectorySync, fsyncFileSync } from '../../shared/secure-file' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { createDefaultLocalOrcaProfile, DEFAULT_LOCAL_ORCA_PROFILE_ID, diff --git a/src/main/orca-profiles/profile-project-presence.test.ts b/src/main/orca-profiles/profile-project-presence.test.ts index ebbc05b8e14..56bfed5f500 100644 --- a/src/main/orca-profiles/profile-project-presence.test.ts +++ b/src/main/orca-profiles/profile-project-presence.test.ts @@ -8,7 +8,8 @@ import { type OrcaProfileIndex, type OrcaProfileKind } from '../../shared/orca-profiles' -import type { PersistedState, Repo } from '../../shared/types' +import type { PersistedState } from '../../shared/persisted-state-types' +import type { Repo } from '../../shared/repo-types' const testState = { dir: '' } diff --git a/src/main/orca-profiles/profile-project-session-state.ts b/src/main/orca-profiles/profile-project-session-state.ts index 7b5eb1e4491..acb252007ca 100644 --- a/src/main/orca-profiles/profile-project-session-state.ts +++ b/src/main/orca-profiles/profile-project-session-state.ts @@ -1,6 +1,6 @@ import { getDefaultWorkspaceSession } from '../../shared/constants' import type { ExecutionHostId } from '../../shared/execution-host' -import type { WorkspaceSessionState } from '../../shared/types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { parseWorkspaceKey } from '../../shared/workspace-scope' import { isRepoWorktreeId, diff --git a/src/main/orca-profiles/profile-project-session-transfer.ts b/src/main/orca-profiles/profile-project-session-transfer.ts index 4736599df23..d5884096bd9 100644 --- a/src/main/orca-profiles/profile-project-session-transfer.ts +++ b/src/main/orca-profiles/profile-project-session-transfer.ts @@ -1,14 +1,12 @@ import { getDefaultWorkspaceSession } from '../../shared/constants' import type { ExecutionHostId } from '../../shared/execution-host' +import type { BrowserPage, BrowserWorkspace } from '../../shared/browser-workspace-types' +import type { Tab, TabGroup } from '../../shared/tab-types' +import type { TerminalTab } from '../../shared/terminal-tab-types' import type { - BrowserPage, - BrowserWorkspace, PersistedOpenFile, - Tab, - TabGroup, - TerminalTab, WorkspaceSessionState -} from '../../shared/types' +} from '../../shared/workspace-session-state-types' import { parseWorkspaceKey, worktreeWorkspaceKey } from '../../shared/workspace-scope' import { isRepoWorktreeId, diff --git a/src/main/orca-profiles/profile-project-source-removal.ts b/src/main/orca-profiles/profile-project-source-removal.ts index edb923e6293..c698491eb19 100644 --- a/src/main/orca-profiles/profile-project-source-removal.ts +++ b/src/main/orca-profiles/profile-project-source-removal.ts @@ -1,4 +1,4 @@ -import type { WorkspaceKey } from '../../shared/types' +import type { WorkspaceKey } from '../../shared/folder-workspace-types' import { parseWorkspaceKey } from '../../shared/workspace-scope' import { rebuildRepoBackedProjectState, diff --git a/src/main/orca-profiles/profile-project-state-file.test.ts b/src/main/orca-profiles/profile-project-state-file.test.ts index 223d1229555..305ba53d869 100644 --- a/src/main/orca-profiles/profile-project-state-file.test.ts +++ b/src/main/orca-profiles/profile-project-state-file.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest' import { getDefaultPersistedState } from '../../shared/constants' -import type { PersistedState, Project, ProjectHostSetup, Repo } from '../../shared/types' +import type { PersistedState } from '../../shared/persisted-state-types' +import type { Project, ProjectHostSetup } from '../../shared/project-types' +import type { Repo } from '../../shared/repo-types' import { rebuildRepoBackedProjectState } from './profile-project-state-file' const upstreamIdentity = { diff --git a/src/main/orca-profiles/profile-project-state-file.ts b/src/main/orca-profiles/profile-project-state-file.ts index 7e00533350f..f7dedcff179 100644 --- a/src/main/orca-profiles/profile-project-state-file.ts +++ b/src/main/orca-profiles/profile-project-state-file.ts @@ -6,14 +6,11 @@ import { getDefaultPersistedState, getDefaultWorkspaceSession } from '../../shar import type { ExecutionHostId } from '../../shared/execution-host' import { projectHostSetupProjectionFromRepos } from '../../shared/project-host-setup-projection' import { carryProjectStateThroughIdentityChange } from '../../shared/project-identity-succession' -import type { - PersistedState, - Project, - ProjectHostSetup, - Repo, - SparsePreset, - WorkspaceSessionState -} from '../../shared/types' +import type { PersistedState } from '../../shared/persisted-state-types' +import type { Project, ProjectHostSetup } from '../../shared/project-types' +import type { Repo } from '../../shared/repo-types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' +import type { SparsePreset } from '../../shared/worktree/create-types' import { getOrcaProfileDataFile } from './profile-index-store' export type TransferProfileState = PersistedState diff --git a/src/main/orca-profiles/profile-project-transfer-payload.ts b/src/main/orca-profiles/profile-project-transfer-payload.ts index f8e458df4df..de7c8b73171 100644 --- a/src/main/orca-profiles/profile-project-transfer-payload.ts +++ b/src/main/orca-profiles/profile-project-transfer-payload.ts @@ -2,7 +2,10 @@ import { randomUUID } from 'node:crypto' import { getRepoExecutionHostId, type ExecutionHostId } from '../../shared/execution-host' import { projectHostSetupProjectionFromRepos } from '../../shared/project-host-setup-projection' import type { SshTarget } from '../../shared/ssh-types' -import type { PersistedState, Repo, SparsePreset, WorkspaceKey } from '../../shared/types' +import type { WorkspaceKey } from '../../shared/folder-workspace-types' +import type { PersistedState } from '../../shared/persisted-state-types' +import type { Repo } from '../../shared/repo-types' +import type { SparsePreset } from '../../shared/worktree/create-types' import { parseWorkspaceKey } from '../../shared/workspace-scope' import type { TransferProfileState } from './profile-project-state-file' import { rebuildRepoBackedProjectState } from './profile-project-state-file' diff --git a/src/main/orca-profiles/profile-project-transfer.test.ts b/src/main/orca-profiles/profile-project-transfer.test.ts index 3c3a4643387..ecb61684d62 100644 --- a/src/main/orca-profiles/profile-project-transfer.test.ts +++ b/src/main/orca-profiles/profile-project-transfer.test.ts @@ -7,7 +7,9 @@ import { ORCA_PROFILE_INDEX_SCHEMA_VERSION, type OrcaProfileIndex } from '../../shared/orca-profiles' -import type { PersistedState, Repo, WorktreeMeta } from '../../shared/types' +import type { PersistedState } from '../../shared/persisted-state-types' +import type { Repo } from '../../shared/repo-types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' import type { SshTarget } from '../../shared/ssh-types' const testState = { dir: '' } diff --git a/src/main/orca-profiles/profile-project-worktree-identity.ts b/src/main/orca-profiles/profile-project-worktree-identity.ts index 58aa55da8b3..cdc8d9182d2 100644 --- a/src/main/orca-profiles/profile-project-worktree-identity.ts +++ b/src/main/orca-profiles/profile-project-worktree-identity.ts @@ -1,6 +1,7 @@ import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path' import { getRepoExecutionHostId } from '../../shared/execution-host' -import type { Repo, WorkspaceKey } from '../../shared/types' +import type { WorkspaceKey } from '../../shared/folder-workspace-types' +import type { Repo } from '../../shared/repo-types' import { parseWorkspaceKey, worktreeWorkspaceKey } from '../../shared/workspace-scope' import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree/id' diff --git a/src/main/persistence-duplicate-repo-id-host-scope.test.ts b/src/main/persistence-duplicate-repo-id-host-scope.test.ts index 2f46dc9ca26..ade0038ee9b 100644 --- a/src/main/persistence-duplicate-repo-id-host-scope.test.ts +++ b/src/main/persistence-duplicate-repo-id-host-scope.test.ts @@ -8,7 +8,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' -import type { Project, ProjectHostSetup, Repo } from '../shared/types' +import type { Project, ProjectHostSetup } from '../shared/project-types' +import type { Repo } from '../shared/repo-types' import { getDefaultPersistedState } from '../shared/constants' const testState = { dir: '' } diff --git a/src/main/persistence-feature-interaction-broadcast.benchmark.test.ts b/src/main/persistence-feature-interaction-broadcast.benchmark.test.ts index 4c04ab22d01..cfb7f555b08 100644 --- a/src/main/persistence-feature-interaction-broadcast.benchmark.test.ts +++ b/src/main/persistence-feature-interaction-broadcast.benchmark.test.ts @@ -4,7 +4,7 @@ import { join } from 'node:path' import { performance } from 'node:perf_hooks' import { afterEach, describe, expect, it, vi } from 'vitest' import { normalizeFeatureInteractions } from '../shared/feature-interactions' -import type { PersistedState } from '../shared/types' +import type { PersistedState } from '../shared/persisted-state-types' vi.mock('electron', () => ({ app: { getPath: () => tmpdir() }, diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts index b2c56cf1aca..b92ed0c86c0 100644 --- a/src/main/persistence.test.ts +++ b/src/main/persistence.test.ts @@ -13,19 +13,14 @@ import { } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' -import type { - PersistedState, - Project, - ProjectGroup, - ProjectHostSetup, - Repo, - GlobalSettings, - TerminalPaneLayoutNode, - TerminalTab, - WorktreeLineage, - WorkspaceLineage, - WorkspaceSessionState -} from '../shared/types' +import type { GlobalSettings } from '../shared/global-settings-types' +import type { PersistedState } from '../shared/persisted-state-types' +import type { ProjectGroup } from '../shared/project-group-types' +import type { Project, ProjectHostSetup } from '../shared/project-types' +import type { Repo } from '../shared/repo-types' +import type { TerminalPaneLayoutNode, TerminalTab } from '../shared/terminal-tab-types' +import type { WorkspaceSessionState } from '../shared/workspace-session-state-types' +import type { WorkspaceLineage, WorktreeLineage } from '../shared/worktree/lineage-types' import { isTerminalLeafId, makePaneKey } from '../shared/stable-pane-id' import { TERMINAL_SCROLLBACK_REPLAY_BYTE_LIMIT } from '../shared/terminal-scrollback-limits' import { MAX_BROWSER_HISTORY_ENTRIES } from '../shared/workspace-session-browser-history' diff --git a/src/main/persistence.ts b/src/main/persistence.ts index d0699cea658..b01a298d62f 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -40,10 +40,22 @@ import { getAutomationLegacyRepoId } from '../shared/automation-run-identity' import { normalizeAutomationPrecheck } from '../shared/automation-precheck' import { normalizeProxyUrl } from '../shared/network-proxy' import { normalizeKagiSessionLink } from '../shared/browser-url' +import type { FolderWorkspace, WorkspaceKey } from '../shared/folder-workspace-types' +import type { GlobalSettings, OrcaWorkspaceLayout } from '../shared/global-settings-types' +import type { NotificationSettings } from '../shared/notification-settings-types' +import type { + OnboardingChecklistState, + OnboardingOutcome, + OnboardingState +} from '../shared/onboarding-state-types' +import type { + LegacyPaneKeyAliasEntry, + PersistedMobileClientTabSelections, + PersistedState +} from '../shared/persisted-state-types' +import type { ProjectGroup } from '../shared/project-group-types' import type { - PersistedState, Project, - ProjectUpdateArgs, ProjectHostSetup, ProjectHostSetupCreateArgs, ProjectHostSetupCreateResult, @@ -51,29 +63,22 @@ import type { ProjectHostSetupDeleteResult, ProjectHostSetupUpdateArgs, ProjectHostSetupUpdateResult, - RepoProjectHostSetupMethod, - Repo, - ProjectGroup, - FolderWorkspace, - SparsePreset, - PersistedMobileClientTabSelections, - WorktreeMeta, - WorktreeLineage, - WorkspaceLineage, - WorkspaceKey, - GlobalSettings, - OrcaWorkspaceLayout, - NotificationSettings, - OnboardingChecklistState, - OnboardingOutcome, - OnboardingState, - LegacyPaneKeyAliasEntry, - TerminalPaneLayoutNode, + ProjectUpdateArgs, + RepoProjectHostSetupMethod +} from '../shared/project-types' +import type { Repo } from '../shared/repo-types' +import type { TerminalLayoutSnapshot, - TerminalTab, + TerminalPaneLayoutNode, + TerminalTab +} from '../shared/terminal-tab-types' +import type { WorkspaceSessionPatch, WorkspaceSessionState -} from '../shared/types' +} from '../shared/workspace-session-state-types' +import type { SparsePreset } from '../shared/worktree/create-types' +import type { WorkspaceLineage, WorktreeLineage } from '../shared/worktree/lineage-types' +import type { WorktreeMeta } from '../shared/worktree/meta-types' import { deriveGlobalWindowsRuntimeDefaultFromLegacySettings, normalizeProjectRuntimePreference diff --git a/src/main/plugins/plugin-approved-vm-recipes.ts b/src/main/plugins/plugin-approved-vm-recipes.ts index ab193a5280f..2ee20c63714 100644 --- a/src/main/plugins/plugin-approved-vm-recipes.ts +++ b/src/main/plugins/plugin-approved-vm-recipes.ts @@ -1,4 +1,4 @@ -import type { OrcaVmRecipe } from '../../shared/types' +import type { OrcaVmRecipe } from '../../shared/orca-yaml-hook-types' import type { PluginService } from './plugin-service' export async function getApprovedPluginVmRecipes( diff --git a/src/main/plugins/plugin-enablement.test.ts b/src/main/plugins/plugin-enablement.test.ts index 0ce3013d8a9..f0698b80a1b 100644 --- a/src/main/plugins/plugin-enablement.test.ts +++ b/src/main/plugins/plugin-enablement.test.ts @@ -1,7 +1,7 @@ import { tmpdir } from 'node:os' import { describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../shared/constants' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { pluginManifestSchema } from '../../shared/plugins/plugin-manifest' import type { Store } from '../persistence' import { applyPluginConsent, applyPluginEnablement } from './plugin-enablement' diff --git a/src/main/plugins/plugin-vm-recipe-registry.ts b/src/main/plugins/plugin-vm-recipe-registry.ts index ac2b90d9b64..eb7204fef7e 100644 --- a/src/main/plugins/plugin-vm-recipe-registry.ts +++ b/src/main/plugins/plugin-vm-recipe-registry.ts @@ -1,4 +1,4 @@ -import type { OrcaVmRecipe } from '../../shared/types' +import type { OrcaVmRecipe } from '../../shared/orca-yaml-hook-types' import { parsePluginVmRecipeArtifact } from '../../shared/plugins/plugin-vm-recipe-artifact' import { PLUGIN_VM_RECIPE_MAX_BYTES, diff --git a/src/main/project-groups/folder-workspace-path-status.test.ts b/src/main/project-groups/folder-workspace-path-status.test.ts index 495f4690434..25314576bc3 100644 --- a/src/main/project-groups/folder-workspace-path-status.test.ts +++ b/src/main/project-groups/folder-workspace-path-status.test.ts @@ -9,7 +9,8 @@ import { inferFolderWorkspacePathConnection } from './folder-workspace-path-status' import type { IFilesystemProvider } from '../providers/types' -import type { ProjectGroup, Repo } from '../../shared/types' +import type { ProjectGroup } from '../../shared/project-group-types' +import type { Repo } from '../../shared/repo-types' function makeGroup(overrides: Partial = {}): ProjectGroup { return { diff --git a/src/main/project-groups/folder-workspace-path-status.ts b/src/main/project-groups/folder-workspace-path-status.ts index d8df44da72c..dbbd34a7034 100644 --- a/src/main/project-groups/folder-workspace-path-status.ts +++ b/src/main/project-groups/folder-workspace-path-status.ts @@ -5,7 +5,9 @@ import type { FolderWorkspacePathStatusRequest } from '../../shared/folder-workspace-path-status' import { getProjectGroupSubtreeIds } from '../../shared/project-groups' -import type { FolderWorkspace, ProjectGroup, Repo } from '../../shared/types' +import type { FolderWorkspace } from '../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../shared/project-group-types' +import type { Repo } from '../../shared/repo-types' import type { IFilesystemProvider } from '../providers/types' type FolderWorkspacePathStatusStore = { diff --git a/src/main/project-groups/nested-repo-discovery.ts b/src/main/project-groups/nested-repo-discovery.ts index 30453748767..d2df48e7970 100644 --- a/src/main/project-groups/nested-repo-discovery.ts +++ b/src/main/project-groups/nested-repo-discovery.ts @@ -6,7 +6,7 @@ import type { NestedRepoCandidate, NestedRepoScanOptions, NestedRepoScanResult -} from '../../shared/types' +} from '../../shared/project-group-types' import { isGitRepo } from '../git/repo' type NestedRepoDirectoryEntry = { diff --git a/src/main/project-groups/nested-repo-import-target.test.ts b/src/main/project-groups/nested-repo-import-target.test.ts index 60b1cf9b526..c630e102fb3 100644 --- a/src/main/project-groups/nested-repo-import-target.test.ts +++ b/src/main/project-groups/nested-repo-import-target.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi, beforeEach } from 'vitest' import { join } from 'node:path' -import type { GitWorktreeInfo } from '../../shared/types' +import type { GitWorktreeInfo } from '../../shared/worktree/types' import { listWorktreeGraph } from '../git/worktree' import { createNestedRepoImportTargetResolver, diff --git a/src/main/project-groups/nested-repo-import-target.ts b/src/main/project-groups/nested-repo-import-target.ts index bde2abb0ba9..be722716856 100644 --- a/src/main/project-groups/nested-repo-import-target.ts +++ b/src/main/project-groups/nested-repo-import-target.ts @@ -1,4 +1,4 @@ -import type { GitWorktreeInfo } from '../../shared/types' +import type { GitWorktreeInfo } from '../../shared/worktree/types' import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path' import { listWorktreeGraph } from '../git/worktree' diff --git a/src/main/project-groups/nested-repo-import.test.ts b/src/main/project-groups/nested-repo-import.test.ts index be7a46e81ee..2b591fa9a71 100644 --- a/src/main/project-groups/nested-repo-import.test.ts +++ b/src/main/project-groups/nested-repo-import.test.ts @@ -4,7 +4,7 @@ import { resolveNestedRepoImportPaths, resolveNestedRepoSelection } from './nested-repo-import' -import type { ProjectGroup } from '../../shared/types' +import type { ProjectGroup } from '../../shared/project-group-types' function createGroupRecorder(): { groups: ProjectGroup[] diff --git a/src/main/project-groups/nested-repo-import.ts b/src/main/project-groups/nested-repo-import.ts index bcaf3ec25dd..17aee4a2994 100644 --- a/src/main/project-groups/nested-repo-import.ts +++ b/src/main/project-groups/nested-repo-import.ts @@ -1,4 +1,8 @@ -import type { NestedRepoScanResult, ProjectGroup, ProjectGroupImportMode } from '../../shared/types' +import type { + NestedRepoScanResult, + ProjectGroup, + ProjectGroupImportMode +} from '../../shared/project-group-types' import { getRuntimePathBasename, isPathInsideOrEqual, diff --git a/src/main/project-runtime-git-options.test.ts b/src/main/project-runtime-git-options.test.ts index 077bd43afd8..bff4694acb0 100644 --- a/src/main/project-runtime-git-options.test.ts +++ b/src/main/project-runtime-git-options.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, it } from 'vitest' import type { Store } from './persistence' -import type { Project, Repo } from '../shared/types' +import type { Project } from '../shared/project-types' +import type { Repo } from '../shared/repo-types' import { getLocalProjectGitExecOptions, resolveLocalProjectRuntimeForRepo diff --git a/src/main/project-runtime-git-options.ts b/src/main/project-runtime-git-options.ts index 1c4e4945f32..32399aa5976 100644 --- a/src/main/project-runtime-git-options.ts +++ b/src/main/project-runtime-git-options.ts @@ -1,5 +1,5 @@ import type { Store } from './persistence' -import type { Repo } from '../shared/types' +import type { Repo } from '../shared/repo-types' import { resolveLocalProjectRuntimeForRepo } from './local-project-runtime-resolution' import type { ProjectExecutionRuntimeResolution } from '../shared/project-execution-runtime' diff --git a/src/main/providers/filesystem-provider-contract.ts b/src/main/providers/filesystem-provider-contract.ts index 855f36da98c..ab467a7d89b 100644 --- a/src/main/providers/filesystem-provider-contract.ts +++ b/src/main/providers/filesystem-provider-contract.ts @@ -1,4 +1,5 @@ -import type { DirEntry, FsChangeEvent, SearchOptions, SearchResult } from '../../shared/types' +import type { SearchOptions, SearchResult } from '../../shared/code-search-types' +import type { DirEntry, FsChangeEvent } from '../../shared/filesystem-entry-types' import type { WorkspaceSpaceDirectoryScanResult } from '../../shared/workspace-space-types' export type FileStat = { diff --git a/src/main/providers/git-provider-contract.ts b/src/main/providers/git-provider-contract.ts index d13bf5addf9..f70f3ddcc72 100644 --- a/src/main/providers/git-provider-contract.ts +++ b/src/main/providers/git-provider-contract.ts @@ -1,17 +1,17 @@ import type { - GitStatusResult, - GitDiffResult, GitBranchCompareResult, GitCommitCompareResult, + GitDiffResult +} from '../../shared/git-diff-compare-types' +import type { GitForkSyncExpectedUpstream, GitForkSyncResult } from '../../shared/git-fork-sync' +import type { GitConflictOperation, - GitForkSyncExpectedUpstream, - GitForkSyncResult, - GitPushTarget, GitStagingArea, - GitUpstreamStatus, - GitWorktreeInfo, - RemoveWorktreeResult -} from '../../shared/types' + GitStatusResult, + GitUpstreamStatus +} from '../../shared/git-status-types' +import type { RemoveWorktreeResult } from '../../shared/worktree/create-types' +import type { GitPushTarget, GitWorktreeInfo } from '../../shared/worktree/types' import type { GitHistoryOptions, GitHistoryResult } from '../../shared/git-history' import type { CommitMessageDraftContext } from '../../shared/commit-message-generation' import type { GitProviderStatusOptions } from './git-provider-status-options' diff --git a/src/main/providers/pty-provider-contract.ts b/src/main/providers/pty-provider-contract.ts index 09f3cda62fd..cade47d84d2 100644 --- a/src/main/providers/pty-provider-contract.ts +++ b/src/main/providers/pty-provider-contract.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../shared/types' +import type { TuiAgent } from '../../shared/tui-agent' import type { PtyStartupIngressIntent } from '../../shared/pty-startup-ingress' import type { StartupCommandDelivery } from '../../shared/codex-startup-delivery' import type { TerminalOscLinkRange } from '../../shared/terminal-osc-link-ranges' diff --git a/src/main/providers/pty-spawn-result.ts b/src/main/providers/pty-spawn-result.ts index ebed657ee21..76bb4179069 100644 --- a/src/main/providers/pty-spawn-result.ts +++ b/src/main/providers/pty-spawn-result.ts @@ -1,5 +1,5 @@ import type { TerminalOscLinkRange } from '../../shared/terminal-osc-link-ranges' -import type { TuiAgent } from '../../shared/types' +import type { TuiAgent } from '../../shared/tui-agent' import type { AgentSessionClaimedSpawnResult } from '../../shared/agent-session-host-authority' import type { PtyIncarnationId } from '../../shared/pty-incarnation' import type { PtySourceReceivingActivation } from '../../shared/pty-source-receiving-activation' diff --git a/src/main/providers/ssh-filesystem-provider-watch.ts b/src/main/providers/ssh-filesystem-provider-watch.ts index 8ef523860bd..95ef73632eb 100644 --- a/src/main/providers/ssh-filesystem-provider-watch.ts +++ b/src/main/providers/ssh-filesystem-provider-watch.ts @@ -1,5 +1,5 @@ import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer' -import type { FsChangeEvent } from '../../shared/types' +import type { FsChangeEvent } from '../../shared/filesystem-entry-types' import { isMethodNotFoundError } from '../ssh/ssh-filesystem-stream-reader' import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path' import { PromiseSettlementWaiters } from '../../shared/promise-settlement-waiters' diff --git a/src/main/providers/ssh-filesystem-provider.ts b/src/main/providers/ssh-filesystem-provider.ts index 74f31b8e6bf..7fa2deb85b9 100644 --- a/src/main/providers/ssh-filesystem-provider.ts +++ b/src/main/providers/ssh-filesystem-provider.ts @@ -21,7 +21,8 @@ import type { FileUploadSession, TerminalArtifactAccessOptions } from './types' -import type { DirEntry, FsChangeEvent, SearchOptions, SearchResult } from '../../shared/types' +import type { SearchOptions, SearchResult } from '../../shared/code-search-types' +import type { DirEntry, FsChangeEvent } from '../../shared/filesystem-entry-types' import { routeSshFilesystemWatchNotification } from './ssh-filesystem-watch-notifications' import type { WorkspaceSpaceDirectoryScanResult } from '../../shared/workspace-space-types' import { isWindowsRemoteHost, type RemoteHostPlatform } from '../ssh/ssh-remote-platform' diff --git a/src/main/providers/ssh-filesystem-watch-notifications.test.ts b/src/main/providers/ssh-filesystem-watch-notifications.test.ts index 3380fe42e89..d50b32bc826 100644 --- a/src/main/providers/ssh-filesystem-watch-notifications.test.ts +++ b/src/main/providers/ssh-filesystem-watch-notifications.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { FsChangeEvent } from '../../shared/types' +import type { FsChangeEvent } from '../../shared/filesystem-entry-types' import { isPathInsideOrEqual } from '../../shared/cross-platform-path' import { routeSshFilesystemWatchNotification } from './ssh-filesystem-watch-notifications' import type { WatchRegistration } from './ssh-filesystem-provider-watch' diff --git a/src/main/providers/ssh-filesystem-watch-notifications.ts b/src/main/providers/ssh-filesystem-watch-notifications.ts index c08afb21095..aed30f16473 100644 --- a/src/main/providers/ssh-filesystem-watch-notifications.ts +++ b/src/main/providers/ssh-filesystem-watch-notifications.ts @@ -1,4 +1,4 @@ -import type { FsChangeEvent } from '../../shared/types' +import type { FsChangeEvent } from '../../shared/filesystem-entry-types' import { createNormalizedPathInsideOrEqualMatcher, normalizeRuntimePathForComparison diff --git a/src/main/providers/ssh-git-provider.ts b/src/main/providers/ssh-git-provider.ts index 51e2b91e1c2..1875e479f12 100644 --- a/src/main/providers/ssh-git-provider.ts +++ b/src/main/providers/ssh-git-provider.ts @@ -5,19 +5,19 @@ import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer' import type { GitProviderStatusOptions, IGitProvider } from './types' import type { - GitStatusResult, - GitDiffResult, GitBranchCompareResult, GitCommitCompareResult, + GitDiffResult +} from '../../shared/git-diff-compare-types' +import type { GitForkSyncExpectedUpstream, GitForkSyncResult } from '../../shared/git-fork-sync' +import type { GitConflictOperation, - GitForkSyncExpectedUpstream, - GitForkSyncResult, - GitPushTarget, GitStagingArea, - GitUpstreamStatus, - GitWorktreeInfo, - RemoveWorktreeResult -} from '../../shared/types' + GitStatusResult, + GitUpstreamStatus +} from '../../shared/git-status-types' +import type { RemoveWorktreeResult } from '../../shared/worktree/create-types' +import type { GitPushTarget, GitWorktreeInfo } from '../../shared/worktree/types' import type { GitHistoryOptions, GitHistoryResult } from '../../shared/git-history' import { buildHostedRemoteCommitUrl, buildHostedRemoteFileUrl } from '../git/hosted-remote-url' import { JsonRpcErrorCode } from '../ssh/relay-protocol' diff --git a/src/main/provisioned-root-ssh-adoption.test.ts b/src/main/provisioned-root-ssh-adoption.test.ts index d8d2c73c2ef..9d89fb27217 100644 --- a/src/main/provisioned-root-ssh-adoption.test.ts +++ b/src/main/provisioned-root-ssh-adoption.test.ts @@ -3,7 +3,10 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { Store } from './persistence' -import type { AdoptProvisionedRootArgs, GitWorktreeInfo, Repo, WorktreeMeta } from '../shared/types' +import type { Repo } from '../shared/repo-types' +import type { AdoptProvisionedRootArgs } from '../shared/worktree/create-types' +import type { WorktreeMeta } from '../shared/worktree/meta-types' +import type { GitWorktreeInfo } from '../shared/worktree/types' import { listEphemeralVmRuntimes, upsertEphemeralVmRuntime diff --git a/src/main/provisioned-root-ssh-adoption.ts b/src/main/provisioned-root-ssh-adoption.ts index 0b67772a2e7..c8ba0bd340c 100644 --- a/src/main/provisioned-root-ssh-adoption.ts +++ b/src/main/provisioned-root-ssh-adoption.ts @@ -1,12 +1,12 @@ import { randomUUID } from 'node:crypto' import type { Store } from './persistence' +import type { Repo } from '../shared/repo-types' import type { AdoptProvisionedRootArgs, - AutomationWorkspaceProvenance, - CreateWorktreeResult, - Repo, - WorktreeMeta -} from '../shared/types' + CreateWorktreeResult +} from '../shared/worktree/create-types' +import type { WorktreeMeta } from '../shared/worktree/meta-types' +import type { AutomationWorkspaceProvenance } from '../shared/worktree/types' import { isRuntimeOwnedSshTargetId, toSshExecutionHostId } from '../shared/execution-host' import { normalizeRuntimePathForComparison } from '../shared/cross-platform-path' import { diff --git a/src/main/rate-limits/account-runtime-target-sync.ts b/src/main/rate-limits/account-runtime-target-sync.ts index d78a4b68910..5b59b19552d 100644 --- a/src/main/rate-limits/account-runtime-target-sync.ts +++ b/src/main/rate-limits/account-runtime-target-sync.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import type { RateLimitState } from '../../shared/rate-limit-types' import type { RateLimitService } from './service' import { getInitialClaudeRateLimitTarget } from './claude-rate-limit-target' diff --git a/src/main/rate-limits/claude-rate-limit-target.ts b/src/main/rate-limits/claude-rate-limit-target.ts index dba2e396669..b28e8cfed8a 100644 --- a/src/main/rate-limits/claude-rate-limit-target.ts +++ b/src/main/rate-limits/claude-rate-limit-target.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { getClaudeWslSelectionKey, normalizeClaudeRuntimeSelection, diff --git a/src/main/rate-limits/codex-rate-limit-target.ts b/src/main/rate-limits/codex-rate-limit-target.ts index 61c116bab2b..7d7232a3e87 100644 --- a/src/main/rate-limits/codex-rate-limit-target.ts +++ b/src/main/rate-limits/codex-rate-limit-target.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { getWslSelectionKey, normalizeCodexRuntimeSelection, diff --git a/src/main/rate-limits/initial-account-rate-limit-target.test.ts b/src/main/rate-limits/initial-account-rate-limit-target.test.ts index 528a7527968..485fa8bcef6 100644 --- a/src/main/rate-limits/initial-account-rate-limit-target.test.ts +++ b/src/main/rate-limits/initial-account-rate-limit-target.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { getDefaultSettings } from '../../shared/constants' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { getInitialClaudeRateLimitTarget } from './claude-rate-limit-target' import { getInitialCodexRateLimitTarget } from './codex-rate-limit-target' diff --git a/src/main/rate-limits/initial-account-rate-limit-target.ts b/src/main/rate-limits/initial-account-rate-limit-target.ts index d60dab69daf..729f8f17407 100644 --- a/src/main/rate-limits/initial-account-rate-limit-target.ts +++ b/src/main/rate-limits/initial-account-rate-limit-target.ts @@ -1,5 +1,5 @@ import { resolveLocalAccountRuntimeTarget } from '../../shared/local-account-runtime' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' type AccountRuntimeSelection = { host: string | null diff --git a/src/main/repo-git-remote-identity-enrichment.test.ts b/src/main/repo-git-remote-identity-enrichment.test.ts index 24f498b03fc..b396e8a7e38 100644 --- a/src/main/repo-git-remote-identity-enrichment.test.ts +++ b/src/main/repo-git-remote-identity-enrichment.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import type { GitRemoteIdentity } from '../shared/git-remote-identity' -import type { Repo } from '../shared/types' +import type { Repo } from '../shared/repo-types' import { type GitRemoteIdentityProbe, probeGitRemoteIdentity } from './repo-git-remote-identity' import { enrichMissingRepoGitRemoteIdentities, diff --git a/src/main/repo-git-remote-identity-enrichment.ts b/src/main/repo-git-remote-identity-enrichment.ts index e48177f2e62..145e33ddf90 100644 --- a/src/main/repo-git-remote-identity-enrichment.ts +++ b/src/main/repo-git-remote-identity-enrichment.ts @@ -1,4 +1,4 @@ -import type { Repo } from '../shared/types' +import type { Repo } from '../shared/repo-types' import { probeGitRemoteIdentity } from './repo-git-remote-identity' const NO_IDENTITY_RETRY_TTL_MS = 5 * 60 * 1000 diff --git a/src/main/repo-git-username-enrichment.test.ts b/src/main/repo-git-username-enrichment.test.ts index bcecb8a2340..06ba252a6e6 100644 --- a/src/main/repo-git-username-enrichment.test.ts +++ b/src/main/repo-git-username-enrichment.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../shared/types' +import type { Repo } from '../shared/repo-types' import type { ResolvedGitUsername } from './git/git-username' const resolveLocalGitUsernameDetailedMock = vi.hoisted(() => vi.fn()) diff --git a/src/main/repo-git-username-enrichment.ts b/src/main/repo-git-username-enrichment.ts index c5ae5c6d7b9..44b08ea73b1 100644 --- a/src/main/repo-git-username-enrichment.ts +++ b/src/main/repo-git-username-enrichment.ts @@ -1,4 +1,4 @@ -import type { Repo } from '../shared/types' +import type { Repo } from '../shared/repo-types' import { resolveLocalGitUsernameDetailed } from './git/git-username' type RepoUsernameStore = { diff --git a/src/main/repo-icon-autodetect.ts b/src/main/repo-icon-autodetect.ts index 5fa931f06c5..4ea5f5d59d0 100644 --- a/src/main/repo-icon-autodetect.ts +++ b/src/main/repo-icon-autodetect.ts @@ -1,5 +1,6 @@ import { readFile, stat } from 'node:fs/promises' -import type { GitHubRepositoryIdentity, RepoKind } from '../shared/types' +import type { GitHubRepositoryIdentity } from '../shared/github/pull-request-types' +import type { RepoKind } from '../shared/repo-types' import { faviconUrlFromWebsite, githubAvatarIcon, diff --git a/src/main/repo-worktrees.ts b/src/main/repo-worktrees.ts index 4f45e7d8564..f4123c18889 100644 --- a/src/main/repo-worktrees.ts +++ b/src/main/repo-worktrees.ts @@ -1,4 +1,5 @@ -import type { GitWorktreeInfo, Repo } from '../shared/types' +import type { Repo } from '../shared/repo-types' +import type { GitWorktreeInfo } from '../shared/worktree/types' import { listWorktrees } from './git/worktree' import { isFolderRepo } from '../shared/repo-kind' import { getSshGitProvider } from './providers/ssh-git-dispatch' diff --git a/src/main/runtime/client-session-tab-selection-persistence.ts b/src/main/runtime/client-session-tab-selection-persistence.ts index 358144cb49d..7ae391a040d 100644 --- a/src/main/runtime/client-session-tab-selection-persistence.ts +++ b/src/main/runtime/client-session-tab-selection-persistence.ts @@ -1,7 +1,7 @@ import type { PersistedMobileClientTabSelection, PersistedMobileClientTabSelections -} from '../../shared/types' +} from '../../shared/persisted-state-types' function normalizeClientSessionTabSelection( raw: unknown diff --git a/src/main/runtime/client-session-tab-selection.test.ts b/src/main/runtime/client-session-tab-selection.test.ts index b0db68c5ca5..eb83e6144ca 100644 --- a/src/main/runtime/client-session-tab-selection.test.ts +++ b/src/main/runtime/client-session-tab-selection.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { RuntimeMobileSessionTabsResult } from '../../shared/runtime-types' -import type { PersistedMobileClientTabSelections } from '../../shared/types' +import type { PersistedMobileClientTabSelections } from '../../shared/persisted-state-types' import { activateClientSessionTabSelection, ClientSessionTabSelectionStore, diff --git a/src/main/runtime/client-session-tab-selection.ts b/src/main/runtime/client-session-tab-selection.ts index 7c8f84f88d2..94aa42419de 100644 --- a/src/main/runtime/client-session-tab-selection.ts +++ b/src/main/runtime/client-session-tab-selection.ts @@ -2,7 +2,7 @@ import type { RuntimeMobileSessionClientTab, RuntimeMobileSessionTabsResult } from '../../shared/runtime-types' -import type { PersistedMobileClientTabSelections } from '../../shared/types' +import type { PersistedMobileClientTabSelections } from '../../shared/persisted-state-types' import { normalizePersistedMobileClientTabSelections } from './client-session-tab-selection-persistence' export type ClientSessionTabSelection = { diff --git a/src/main/runtime/file-watcher-host.test.ts b/src/main/runtime/file-watcher-host.test.ts index 10f88f228f5..72608084a9a 100644 --- a/src/main/runtime/file-watcher-host.test.ts +++ b/src/main/runtime/file-watcher-host.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { FsChangeEvent } from '../../shared/types' +import type { FsChangeEvent } from '../../shared/filesystem-entry-types' import type { WatcherProcessCallback, WatcherProcessHooks, diff --git a/src/main/runtime/file-watcher-host.ts b/src/main/runtime/file-watcher-host.ts index 59a8071bd58..e7f1d29e41d 100644 --- a/src/main/runtime/file-watcher-host.ts +++ b/src/main/runtime/file-watcher-host.ts @@ -1,4 +1,4 @@ -import type { FsChangeEvent } from '../../shared/types' +import type { FsChangeEvent } from '../../shared/filesystem-entry-types' import { forgetRuntimeWatcherProcessRoot, resetRuntimeWatcherProcessForTest, diff --git a/src/main/runtime/folder-workspace-pty-identity.test.ts b/src/main/runtime/folder-workspace-pty-identity.test.ts index db6b1c1927a..5c3c28e7ff6 100644 --- a/src/main/runtime/folder-workspace-pty-identity.test.ts +++ b/src/main/runtime/folder-workspace-pty-identity.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { getDefaultWorkspaceSession } from '../../shared/constants' import type { RuntimeClientEvent } from '../../shared/runtime-client-events' import { makePaneKey } from '../../shared/stable-pane-id' -import type { WorkspaceSessionState } from '../../shared/types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR } from '../../shared/worktree/id' import { OrcaRuntimeService } from './orca-runtime' diff --git a/src/main/runtime/graph-sync-mobile-snapshot-gating.test.ts b/src/main/runtime/graph-sync-mobile-snapshot-gating.test.ts index 338535562a4..5f846fee2d7 100644 --- a/src/main/runtime/graph-sync-mobile-snapshot-gating.test.ts +++ b/src/main/runtime/graph-sync-mobile-snapshot-gating.test.ts @@ -9,7 +9,7 @@ import type { RuntimeMobileSessionTabsResult, RuntimeMobileSessionTabsSnapshot } from '../../shared/runtime-types' -import type { WorkspaceSessionState } from '../../shared/types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { OrcaRuntimeService } from './orca-runtime' // Freshness predicate of shouldApplyWebSessionTabsSnapshot in diff --git a/src/main/runtime/graph-sync-payload-partition.test.ts b/src/main/runtime/graph-sync-payload-partition.test.ts index 6533a53f9fb..fdf0bce733c 100644 --- a/src/main/runtime/graph-sync-payload-partition.test.ts +++ b/src/main/runtime/graph-sync-payload-partition.test.ts @@ -9,7 +9,7 @@ import type { RuntimeMobileSessionTabsSnapshot, RuntimeSyncWindowGraphResult } from '../../shared/runtime-types' -import type { WorkspaceSessionState } from '../../shared/types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { OrcaRuntimeService } from './orca-runtime' const WT_A = 'repo-1::/tmp/worktree-a' diff --git a/src/main/runtime/headless-tab-group-split-layout.ts b/src/main/runtime/headless-tab-group-split-layout.ts index da7351d49bd..9e3d56b5f73 100644 --- a/src/main/runtime/headless-tab-group-split-layout.ts +++ b/src/main/runtime/headless-tab-group-split-layout.ts @@ -1,5 +1,5 @@ import type { RuntimeMobileSessionTabGroup } from '../../shared/runtime-types' -import type { TabGroupLayoutNode } from '../../shared/types' +import type { TabGroupLayoutNode } from '../../shared/tab-types' /** * Headless ("Orca server") tab-GROUP split operations (distinct from terminal diff --git a/src/main/runtime/headless-terminal-query-reply-policy.ts b/src/main/runtime/headless-terminal-query-reply-policy.ts index 05b1883efff..fca7c623331 100644 --- a/src/main/runtime/headless-terminal-query-reply-policy.ts +++ b/src/main/runtime/headless-terminal-query-reply-policy.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../shared/types' +import type { TuiAgent } from '../../shared/tui-agent' /* oxlint-disable no-control-regex -- XTVERSION replies are DCS control sequences. */ const XTVERSION_REPLY = new RegExp('^\u001bP>\\|[^\u001b]*\u001b\\\\$') diff --git a/src/main/runtime/headless-terminal-split-layout.test.ts b/src/main/runtime/headless-terminal-split-layout.test.ts index 0fa0d33b1c0..bb745d49c61 100644 --- a/src/main/runtime/headless-terminal-split-layout.test.ts +++ b/src/main/runtime/headless-terminal-split-layout.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalLayoutSnapshot } from '../../shared/types' +import type { TerminalLayoutSnapshot } from '../../shared/terminal-tab-types' import { buildHeadlessTerminalSplitLayout, countTerminalLayoutLeaves, diff --git a/src/main/runtime/headless-terminal-split-layout.ts b/src/main/runtime/headless-terminal-split-layout.ts index 834625f9ef3..681d70451e8 100644 --- a/src/main/runtime/headless-terminal-split-layout.ts +++ b/src/main/runtime/headless-terminal-split-layout.ts @@ -1,4 +1,7 @@ -import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode } from '../../shared/types' +import type { + TerminalLayoutSnapshot, + TerminalPaneLayoutNode +} from '../../shared/terminal-tab-types' export function terminalLayoutContainsLeaf( node: TerminalPaneLayoutNode | null | undefined, diff --git a/src/main/runtime/missing-worktree-terminal-reconciliation.test.ts b/src/main/runtime/missing-worktree-terminal-reconciliation.test.ts index 22cf484a13e..329dd2319d2 100644 --- a/src/main/runtime/missing-worktree-terminal-reconciliation.test.ts +++ b/src/main/runtime/missing-worktree-terminal-reconciliation.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import type { IPtyProvider } from '../providers/types' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import type { OrcaRuntimeService } from './orca-runtime' import { stopMissingWorktreeTerminals } from './missing-worktree-terminal-reconciliation' diff --git a/src/main/runtime/missing-worktree-terminal-reconciliation.ts b/src/main/runtime/missing-worktree-terminal-reconciliation.ts index 25cd1f2129d..11f888a5404 100644 --- a/src/main/runtime/missing-worktree-terminal-reconciliation.ts +++ b/src/main/runtime/missing-worktree-terminal-reconciliation.ts @@ -1,5 +1,5 @@ import type { IPtyProvider } from '../providers/types' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import { splitWorktreeId } from '../../shared/worktree/id' import { mapWithConcurrency } from '../../shared/map-with-concurrency' import type { OrcaRuntimeService } from './orca-runtime' diff --git a/src/main/runtime/mobile-session-terminal-persistence-retirement.ts b/src/main/runtime/mobile-session-terminal-persistence-retirement.ts index 017b0f6ab47..e94155f342c 100644 --- a/src/main/runtime/mobile-session-terminal-persistence-retirement.ts +++ b/src/main/runtime/mobile-session-terminal-persistence-retirement.ts @@ -1,4 +1,5 @@ -import type { WorkspaceSessionState, WorkspaceVisibleTabType } from '../../shared/types' +import type { WorkspaceVisibleTabType } from '../../shared/tab-types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { pruneTabGroupLayoutAfterRetirement, repairMobileSessionTabGroupsAfterRetirement, diff --git a/src/main/runtime/mobile-session-terminal-retirement.ts b/src/main/runtime/mobile-session-terminal-retirement.ts index acd75fb5580..9aaf69c7fb4 100644 --- a/src/main/runtime/mobile-session-terminal-retirement.ts +++ b/src/main/runtime/mobile-session-terminal-retirement.ts @@ -4,11 +4,11 @@ import type { RuntimeMobileSessionTabsSnapshot, RuntimeMobileSessionTerminalTab } from '../../shared/runtime-types' +import type { TabGroupLayoutNode } from '../../shared/tab-types' import type { - TabGroupLayoutNode, TerminalLayoutSnapshot, TerminalPaneLayoutNode -} from '../../shared/types' +} from '../../shared/terminal-tab-types' export type RetiredTerminalSurface = { worktreeId: string diff --git a/src/main/runtime/multi-client-navigation-isolation.integration.test.ts b/src/main/runtime/multi-client-navigation-isolation.integration.test.ts index 50fd09a4639..5d683dc77dd 100644 --- a/src/main/runtime/multi-client-navigation-isolation.integration.test.ts +++ b/src/main/runtime/multi-client-navigation-isolation.integration.test.ts @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import WebSocket from 'ws' import { parsePairingCode } from '../../shared/pairing' import type { RuntimeMobileSessionTabsResult } from '../../shared/runtime-types' -import type { PersistedMobileClientTabSelections } from '../../shared/types' +import type { PersistedMobileClientTabSelections } from '../../shared/persisted-state-types' import { OrcaRuntimeService } from './orca-runtime' import { decrypt, deriveSharedKey, encrypt, generateKeyPair } from './rpc/e2ee-crypto' import { OrcaRuntimeRpcServer } from './runtime-rpc' diff --git a/src/main/runtime/orca-runtime-automations.test.ts b/src/main/runtime/orca-runtime-automations.test.ts index 0c6d32d215d..14236869a29 100644 --- a/src/main/runtime/orca-runtime-automations.test.ts +++ b/src/main/runtime/orca-runtime-automations.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { OrcaRuntimeService } from './orca-runtime' import type { Automation } from '../../shared/automations-types' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' const repo: Repo = { id: 'repo-1', diff --git a/src/main/runtime/orca-runtime-browser.ts b/src/main/runtime/orca-runtime-browser.ts index 3e5df279344..e076eeadad0 100644 --- a/src/main/runtime/orca-runtime-browser.ts +++ b/src/main/runtime/orca-runtime-browser.ts @@ -52,7 +52,7 @@ import type { import type { BrowserCertificateProceedResult, BrowserSessionUserAgentMode -} from '../../shared/types' +} from '../../shared/browser-workspace-types' import type { AgentBrowserBridge } from '../browser/agent-browser-bridge' import type { BrowserBackend } from '../browser/browser-backend' import { browserCertificateTrustController, browserManager } from '../browser/browser-manager' diff --git a/src/main/runtime/orca-runtime-emulator-folder-workspace.test.ts b/src/main/runtime/orca-runtime-emulator-folder-workspace.test.ts index 23f7adc4df8..5735b6dabc1 100644 --- a/src/main/runtime/orca-runtime-emulator-folder-workspace.test.ts +++ b/src/main/runtime/orca-runtime-emulator-folder-workspace.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import type { EmulatorSessionInfo } from '../emulator/emulator-types' -import type { FolderWorkspace } from '../../shared/types' +import type { FolderWorkspace } from '../../shared/folder-workspace-types' import { OrcaRuntimeService } from './orca-runtime' const FOLDER_WORKSPACE_ID = 'folder-workspace-1' diff --git a/src/main/runtime/orca-runtime-emulator.ts b/src/main/runtime/orca-runtime-emulator.ts index 7c82980c2a3..2e08614b8df 100644 --- a/src/main/runtime/orca-runtime-emulator.ts +++ b/src/main/runtime/orca-runtime-emulator.ts @@ -11,7 +11,7 @@ import type { EmulatorGesturePoint } from '../emulator/emulator-gesture-sender' import type { EmulatorSessionInfo } from '../emulator/emulator-types' import type { SimulatorDevice } from '../emulator/simctl-simulator-devices' import type { EmulatorDevice } from '../emulator/backends/emulator-backend' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' // Settings slice the emulator surface needs; keeps the host contract honest (no widening cast). type EmulatorHostSettings = Pick< diff --git a/src/main/runtime/orca-runtime-files-ssh-rearm.test.ts b/src/main/runtime/orca-runtime-files-ssh-rearm.test.ts index f25f6c9051a..28d734cab63 100644 --- a/src/main/runtime/orca-runtime-files-ssh-rearm.test.ts +++ b/src/main/runtime/orca-runtime-files-ssh-rearm.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { FsChangeEvent } from '../../shared/types' +import type { FsChangeEvent } from '../../shared/filesystem-entry-types' const { resolveAuthorizedPathMock, diff --git a/src/main/runtime/orca-runtime-files-watch.test.ts b/src/main/runtime/orca-runtime-files-watch.test.ts index 41c57b5c44f..e89bc26958b 100644 --- a/src/main/runtime/orca-runtime-files-watch.test.ts +++ b/src/main/runtime/orca-runtime-files-watch.test.ts @@ -3,7 +3,7 @@ import { EventEmitter } from 'node:events' import type * as Fs from 'node:fs' import type * as FsPromises from 'node:fs/promises' import type * as FilesystemAuth from '../ipc/filesystem-auth' -import type { FsChangeEvent } from '../../shared/types' +import type { FsChangeEvent } from '../../shared/filesystem-entry-types' import { WatcherProcessFailure } from '../ipc/parcel-watcher-process-failure' import { acquireWatcherRemovalGate } from '../ipc/watcher-removal-gate' diff --git a/src/main/runtime/orca-runtime-files.ts b/src/main/runtime/orca-runtime-files.ts index fd8f76796a6..826f8a9e2d3 100644 --- a/src/main/runtime/orca-runtime-files.ts +++ b/src/main/runtime/orca-runtime-files.ts @@ -20,15 +20,9 @@ import { } from 'node:fs/promises' import { homedir, tmpdir } from 'node:os' import { basename, dirname, extname, join } from 'node:path' -import type { - DirEntry, - FsChangeEvent, - GitWorktreeInfo, - MarkdownDocument, - SearchOptions, - SearchResult, - Worktree -} from '../../shared/types' +import type { SearchOptions, SearchResult } from '../../shared/code-search-types' +import type { DirEntry, FsChangeEvent, MarkdownDocument } from '../../shared/filesystem-entry-types' +import type { GitWorktreeInfo, Worktree } from '../../shared/worktree/types' import { isPathInsideOrEqual, isRuntimePathAbsolute, diff --git a/src/main/runtime/orca-runtime-git-branch-diff.test.ts b/src/main/runtime/orca-runtime-git-branch-diff.test.ts index 4c469758760..aba155a6895 100644 --- a/src/main/runtime/orca-runtime-git-branch-diff.test.ts +++ b/src/main/runtime/orca-runtime-git-branch-diff.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { RuntimeGitCommands, type ResolvedRuntimeGitWorktree } from './orca-runtime-git' const getSshGitProviderMock = vi.hoisted(() => vi.fn()) diff --git a/src/main/runtime/orca-runtime-git.test.ts b/src/main/runtime/orca-runtime-git.test.ts index 68c6ff798c9..26b993704f2 100644 --- a/src/main/runtime/orca-runtime-git.test.ts +++ b/src/main/runtime/orca-runtime-git.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import type * as GitStatusModule from '../git/status' import type * as CommitMessageTextGenerationModule from '../text-generation/commit-message-text-generation' import type * as PullRequestContextModule from '../text-generation/pull-request-context' diff --git a/src/main/runtime/orca-runtime-git.ts b/src/main/runtime/orca-runtime-git.ts index 67812240e43..3d802d219ea 100644 --- a/src/main/runtime/orca-runtime-git.ts +++ b/src/main/runtime/orca-runtime-git.ts @@ -2,20 +2,19 @@ import type { GitBranchCompareResult, GitCommitCompareResult, + GitDiffResult +} from '../../shared/git-diff-compare-types' +import type { GitForkSyncExpectedUpstream, GitForkSyncResult } from '../../shared/git-fork-sync' +import type { GitConflictOperation, - GitDiffResult, - GitForkSyncExpectedUpstream, - GitForkSyncResult, - GitPushTarget, GitStagingArea, GitStatusResult, - GitUpstreamStatus, - GitWorktreeInfo, - GlobalSettings, - Repo, - TuiAgent, - Worktree -} from '../../shared/types' + GitUpstreamStatus +} from '../../shared/git-status-types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { Repo } from '../../shared/repo-types' +import type { TuiAgent } from '../../shared/tui-agent' +import type { GitPushTarget, GitWorktreeInfo, Worktree } from '../../shared/worktree/types' import type { CommitMessageDraftContext } from '../../shared/commit-message-generation' import { getCommitMessageModelDiscoveryHostKey } from '../../shared/commit-message-host-key' import type { GitHistoryOptions, GitHistoryResult } from '../../shared/git-history' diff --git a/src/main/runtime/orca-runtime-headless-hydration-repo-gate.test.ts b/src/main/runtime/orca-runtime-headless-hydration-repo-gate.test.ts index 8bca2c77ed4..75f0c4fd31e 100644 --- a/src/main/runtime/orca-runtime-headless-hydration-repo-gate.test.ts +++ b/src/main/runtime/orca-runtime-headless-hydration-repo-gate.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { FLOATING_TERMINAL_WORKTREE_ID, getDefaultWorkspaceSession } from '../../shared/constants' -import type { WorkspaceSessionState } from '../../shared/types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { OrcaRuntimeService } from './orca-runtime' const WORKTREE_ID = 'repo::/worktree' diff --git a/src/main/runtime/orca-runtime-linked-issue-live-meta.integration.test.ts b/src/main/runtime/orca-runtime-linked-issue-live-meta.integration.test.ts index 39298111388..9b9bcfac784 100644 --- a/src/main/runtime/orca-runtime-linked-issue-live-meta.integration.test.ts +++ b/src/main/runtime/orca-runtime-linked-issue-live-meta.integration.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { WorktreeMeta } from '../../shared/types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' import type * as GitStatusModule from '../git/status' import type * as CommitMessageTextGenerationModule from '../text-generation/commit-message-text-generation' import type * as WorktreeModule from '../git/worktree' diff --git a/src/main/runtime/orca-runtime-terminal-retirement-host-partition.test.ts b/src/main/runtime/orca-runtime-terminal-retirement-host-partition.test.ts index 1534be0deaa..30ef460b587 100644 --- a/src/main/runtime/orca-runtime-terminal-retirement-host-partition.test.ts +++ b/src/main/runtime/orca-runtime-terminal-retirement-host-partition.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest' import { getDefaultWorkspaceSession } from '../../shared/constants' import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../shared/execution-host' import type { RuntimeMobileSessionTabsSnapshot } from '../../shared/runtime-types' -import type { WorkspaceSessionState } from '../../shared/types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { OrcaRuntimeService } from './orca-runtime' const CONNECTION_ID = 'conn-1' diff --git a/src/main/runtime/orca-runtime-terminal-retirement.test.ts b/src/main/runtime/orca-runtime-terminal-retirement.test.ts index 342017557cc..ae441b48d44 100644 --- a/src/main/runtime/orca-runtime-terminal-retirement.test.ts +++ b/src/main/runtime/orca-runtime-terminal-retirement.test.ts @@ -5,7 +5,7 @@ import type { RuntimeMobileSessionTabsResult, RuntimeMobileSessionTabsSnapshot } from '../../shared/runtime-types' -import type { WorkspaceSessionState } from '../../shared/types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { sanitizeWorkspaceSessionTerminalRetirements } from './mobile-session-terminal-persistence-retirement' import { OrcaRuntimeService } from './orca-runtime' diff --git a/src/main/runtime/orca-runtime-terminal-split-authority.test.ts b/src/main/runtime/orca-runtime-terminal-split-authority.test.ts index 3aa6ae47603..3d4bd91536a 100644 --- a/src/main/runtime/orca-runtime-terminal-split-authority.test.ts +++ b/src/main/runtime/orca-runtime-terminal-split-authority.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it, vi } from 'vitest' import { getDefaultWorkspaceSession } from '../../shared/constants' import type { RuntimeMobileSessionTabsSnapshot } from '../../shared/runtime-types' -import type { TerminalLayoutSnapshot, WorkspaceSessionState } from '../../shared/types' +import type { TerminalLayoutSnapshot } from '../../shared/terminal-tab-types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { makePaneKey } from '../../shared/stable-pane-id' import { OrcaRuntimeService } from './orca-runtime' diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 18880defc77..49a5a3db094 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -10,16 +10,13 @@ import { lstat, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { homedir, tmpdir } from 'node:os' import { basename, join, win32 } from 'node:path' import { ipcMain } from 'electron' -import type { - FolderWorkspace, - ProjectGroup, - Tab, - TerminalLayoutSnapshot, - WorktreeLineage, - WorktreeMeta, - WorkspaceLineage, - WorkspaceSessionState -} from '../../shared/types' +import type { FolderWorkspace } from '../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../shared/project-group-types' +import type { Tab } from '../../shared/tab-types' +import type { TerminalLayoutSnapshot } from '../../shared/terminal-tab-types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' +import type { WorkspaceLineage, WorktreeLineage } from '../../shared/worktree/lineage-types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' import { AGENT_STATUS_STALE_AFTER_MS } from '../../shared/agent-status-types' import { reviewHeadRemoteRefComponent, diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 5a0577166fc..5948a450f41 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -177,24 +177,52 @@ import type { AutomationUpdateInput, AutomationWorkspaceMode } from '../../shared/automations-types' +import type { DirEntry, FilesystemPathFlavor } from '../../shared/filesystem-entry-types' +import type { FolderWorkspace, WorkspaceKey } from '../../shared/folder-workspace-types' +import type { + GitHubPRReviewCommentInput, + GitHubReactionContent +} from '../../shared/github/comment-types' +import type { PRRefreshOutcome } from '../../shared/github/pull-request-refresh-types' +import type { GitHubOwnerRepo, GitHubPRFile } from '../../shared/github/pull-request-types' +import type { ListWorkItemsResult } from '../../shared/github/work-item-types' +import type { + GitLabIssueUpdate, + GitLabMRInlineCommentInput, + GitLabProjectRef, + GitLabWorkItem, + MRListState +} from '../../shared/gitlab-types' +import type { GlobalSettings } from '../../shared/global-settings-types' import type { - AutomationWorkspaceProvenance, - CliWorkspaceProvenance, - BaseRefSearchResult, - CreateWorktreeResult, - DetectedWorktree, - DetectedWorktreeListResult, - ForceDeleteWorktreeBranchResult, - GitHubPrStartPoint, - GitPushTarget, - BranchPrefixStrategy, - GitWorktreeInfo, GitHubCreateIssueFields, - GitHubOwnerRepo, - GlobalSettings, - PersistedUIState, + GitHubIssueUpdate, + GitHubPullRequestStateUpdate, + LinearIssueUpdate +} from '../../shared/issue-mutation-types' +import type { + JiraConnectArgs, + JiraCreateIssueArgs, + JiraIssueFilter, + JiraIssueUpdate, + JiraSiteSelection +} from '../../shared/jira-types' +import type { LinearCustomViewModel, LinearProjectSummary } from '../../shared/linear/project-types' +import type { LinearWorkspaceSelection } from '../../shared/linear/workspace-types' +import type { + ClaudeRateLimitAccountsState, + CodexRateLimitAccountsState +} from '../../shared/managed-account-types' +import type { PersistedUIState } from '../../shared/persisted-ui-state-types' +import type { MemorySnapshot, StatsSummary } from '../../shared/process-stats-types' +import type { + NestedRepoScanResult, + ProjectGroup, + ProjectGroupImportMode, + ProjectGroupImportResult +} from '../../shared/project-group-types' +import type { Project, - ProjectUpdateArgs, ProjectHostSetup, ProjectHostSetupCloneArgs, ProjectHostSetupCreateArgs, @@ -205,60 +233,47 @@ import type { ProjectHostSetupResult, ProjectHostSetupUpdateArgs, ProjectHostSetupUpdateResult, - Repo, - RemoveWorktreeResult, - StatsSummary, - Worktree, - WorktreeLineage, - WorkspaceLineage, - WorkspaceKey, - WorktreeLineageWarning, - WorktreeMeta, - WorktreeBaseStatusEvent, - WorktreeRemoteBranchConflictEvent, - WorktreeStartupLaunch, - LinearCustomViewModel, - JiraConnectArgs, - JiraCreateIssueArgs, - JiraIssueFilter, - JiraIssueUpdate, - JiraSiteSelection, - LinearIssueUpdate, - LinearProjectSummary, - LinearWorkspaceSelection, - NestedRepoScanResult, - ProjectGroup, - FolderWorkspace, - ProjectGroupImportMode, - ProjectGroupImportResult, - MemorySnapshot, - Tab, - TabGroupLayoutNode, - TerminalQuickCommand, + ProjectUpdateArgs +} from '../../shared/project-types' +import type { BaseRefSearchResult, Repo } from '../../shared/repo-types' +import type { Tab, TabGroupLayoutNode } from '../../shared/tab-types' +import type { TerminalQuickCommand } from '../../shared/terminal-quick-command-types' +import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode, - TerminalTab, - TuiAgent, - WorkspaceCreateTelemetrySource, - WorkspaceSessionState, + TerminalTab +} from '../../shared/terminal-tab-types' +import type { TuiAgent } from '../../shared/tui-agent' +import type { BranchPrefixStrategy } from '../../shared/ui-chrome-types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' +import type { WorkspaceSource as WorkspaceCreateTelemetrySource } from '../../shared/workspace-source' +import type { + WorktreeBaseStatusEvent, + WorktreeRemoteBranchConflictEvent +} from '../../shared/worktree/base-ref-drift-types' +import type { + CreateWorktreeResult, + ForceDeleteWorktreeBranchResult, + RemoveWorktreeResult +} from '../../shared/worktree/create-types' +import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types' +import type { + WorkspaceLineage, + WorktreeLineage, + WorktreeLineageWarning +} from '../../shared/worktree/lineage-types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' +import type { + AutomationWorkspaceProvenance, + CliWorkspaceProvenance, + DetectedWorktree, + DetectedWorktreeListResult, + GitHubPrStartPoint, + GitPushTarget, + GitWorktreeInfo, WorkspaceLinkedItem, - DirEntry, - FilesystemPathFlavor, - GitHubIssueUpdate, - GitHubPullRequestStateUpdate, - GitHubPRFile, - GitHubPRReviewCommentInput, - GitHubReactionContent, - GitLabIssueUpdate, - GitLabMRInlineCommentInput, - GitLabProjectRef, - GitLabWorkItem, - ListWorkItemsResult, - MRListState, - PRRefreshOutcome, - ClaudeRateLimitAccountsState, - CodexRateLimitAccountsState -} from '../../shared/types' + Worktree +} from '../../shared/worktree/types' import type { TaskSourceContext } from '../../shared/task-source-context' import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree/removal' import { diff --git a/src/main/runtime/orchestration-worker-workspace-resolution.test.ts b/src/main/runtime/orchestration-worker-workspace-resolution.test.ts index 4ab96e249c7..d95139045a7 100644 --- a/src/main/runtime/orchestration-worker-workspace-resolution.test.ts +++ b/src/main/runtime/orchestration-worker-workspace-resolution.test.ts @@ -29,7 +29,10 @@ vi.mock('../providers/ssh-git-dispatch', () => ({ })) import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants' -import type { FolderWorkspace, ProjectGroup, Repo, WorktreeMeta } from '../../shared/types' +import type { FolderWorkspace } from '../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../shared/project-group-types' +import type { Repo } from '../../shared/repo-types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' import { registerSshFilesystemProvider, unregisterSshFilesystemProvider diff --git a/src/main/runtime/remote-runtime-request-connection.integration.test.ts b/src/main/runtime/remote-runtime-request-connection.integration.test.ts index a4c10384735..dcf4bccdb24 100644 --- a/src/main/runtime/remote-runtime-request-connection.integration.test.ts +++ b/src/main/runtime/remote-runtime-request-connection.integration.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { describe, expect, it, vi } from 'vitest' import { getDefaultRepoHookSettings } from '../../shared/constants' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import { parsePairingCode } from '../../shared/pairing' import { RemoteRuntimeRequestConnection } from '../../shared/remote-runtime-request-connection' import { RemoteRuntimeSharedControlConnection } from '../../shared/remote-runtime-shared-control-connection' diff --git a/src/main/runtime/remote-server-updater.ts b/src/main/runtime/remote-server-updater.ts index a61caa3d591..736713f4424 100644 --- a/src/main/runtime/remote-server-updater.ts +++ b/src/main/runtime/remote-server-updater.ts @@ -2,7 +2,7 @@ import type { RemoteServerUpdateInstallResult, RemoteServerUpdaterSnapshot } from '../../shared/remote-server-update' -import type { UpdateCheckOptions } from '../../shared/types' +import type { UpdateCheckOptions } from '../../shared/update-status-types' type RemoteServerUpdaterAdapter = { getSnapshot: (runtimeId: string) => RemoteServerUpdaterSnapshot diff --git a/src/main/runtime/repo-icon-fork-backfill.test.ts b/src/main/runtime/repo-icon-fork-backfill.test.ts index dc7ffb85463..aacba27f191 100644 --- a/src/main/runtime/repo-icon-fork-backfill.test.ts +++ b/src/main/runtime/repo-icon-fork-backfill.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import * as client from '../github/client' import { OrcaRuntimeService } from './orca-runtime' diff --git a/src/main/runtime/repo-worktree-resolution-scan.ts b/src/main/runtime/repo-worktree-resolution-scan.ts index 7c84e87ad84..052118cf2bf 100644 --- a/src/main/runtime/repo-worktree-resolution-scan.ts +++ b/src/main/runtime/repo-worktree-resolution-scan.ts @@ -1,4 +1,4 @@ -import type { GitWorktreeInfo } from '../../shared/types' +import type { GitWorktreeInfo } from '../../shared/worktree/types' import { listWorktreesStrict } from '../git/worktree' import type { LocalProjectWorktreeGitOptions } from '../project-runtime-git-options' diff --git a/src/main/runtime/rpc/dispatcher-feature-interactions.test.ts b/src/main/runtime/rpc/dispatcher-feature-interactions.test.ts index e33da2b2661..d0f1e6fbc39 100644 --- a/src/main/runtime/rpc/dispatcher-feature-interactions.test.ts +++ b/src/main/runtime/rpc/dispatcher-feature-interactions.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { z } from 'zod' -import type { PersistedUIState } from '../../../shared/types' +import type { PersistedUIState } from '../../../shared/persisted-ui-state-types' import { getDefaultUIState } from '../../../shared/constants' import { ORCHESTRATION_CONTRACT_VERSION } from '../../../shared/protocol-version' import { diff --git a/src/main/runtime/rpc/methods/client-ui-schemas.ts b/src/main/runtime/rpc/methods/client-ui-schemas.ts index 3d9e0e2e69e..2e2555038ed 100644 --- a/src/main/runtime/rpc/methods/client-ui-schemas.ts +++ b/src/main/runtime/rpc/methods/client-ui-schemas.ts @@ -18,7 +18,7 @@ import { WORKTREE_CARD_PROPERTIES } from '../../../../shared/worktree/card-properties' import { isPluginPanelTabKey } from '../../../../shared/plugins/plugin-manifest' -import type { TaskProvider } from '../../../../shared/types' +import type { TaskProvider } from '../../../../shared/task-providers' import { ClientUiWorkspaceFilterFields } from './client-ui-workspace-filter-fields' import { TaskResumeState } from './task-resume-state-schema' import { omitUndefinedValues, tolerateUnknownValues } from './ui-update-value-tolerance' diff --git a/src/main/runtime/rpc/methods/client-ui.test.ts b/src/main/runtime/rpc/methods/client-ui.test.ts index a8323413b23..dc9afe1caaf 100644 --- a/src/main/runtime/rpc/methods/client-ui.test.ts +++ b/src/main/runtime/rpc/methods/client-ui.test.ts @@ -8,7 +8,7 @@ import { MAX_QUICK_COMMAND_TERMINAL_TEXT_LENGTH } from '../../../../shared/terminal-quick-commands' import { DEFAULT_WORKTREE_CARD_PROPERTIES } from '../../../../shared/worktree/card-properties' -import type { PersistedUIState } from '../../../../shared/types' +import type { PersistedUIState } from '../../../../shared/persisted-ui-state-types' import type { OrcaRuntimeService } from '../../orca-runtime' import type { RpcRequest } from '../core' import { RpcDispatcher } from '../dispatcher' diff --git a/src/main/runtime/rpc/methods/client-ui.ts b/src/main/runtime/rpc/methods/client-ui.ts index 73326756cd9..70ad9e9df57 100644 --- a/src/main/runtime/rpc/methods/client-ui.ts +++ b/src/main/runtime/rpc/methods/client-ui.ts @@ -1,4 +1,4 @@ -import type { PersistedUIState } from '../../../../shared/types' +import type { PersistedUIState } from '../../../../shared/persisted-ui-state-types' import { defineMethod, type RpcMethod } from '../core' import { FeatureInteractionIdParam, diff --git a/src/main/runtime/rpc/methods/file-watch-event-batcher.test.ts b/src/main/runtime/rpc/methods/file-watch-event-batcher.test.ts index 75af9a11bd8..81cd8b7814a 100644 --- a/src/main/runtime/rpc/methods/file-watch-event-batcher.test.ts +++ b/src/main/runtime/rpc/methods/file-watch-event-batcher.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { FsChangeEvent } from '../../../../shared/types' +import type { FsChangeEvent } from '../../../../shared/filesystem-entry-types' import { createFileWatchEventBatcher } from './file-watch-event-batcher' describe('createFileWatchEventBatcher', () => { diff --git a/src/main/runtime/rpc/methods/file-watch-event-batcher.ts b/src/main/runtime/rpc/methods/file-watch-event-batcher.ts index 24e93e2affc..da2a21d3d9b 100644 --- a/src/main/runtime/rpc/methods/file-watch-event-batcher.ts +++ b/src/main/runtime/rpc/methods/file-watch-event-batcher.ts @@ -2,7 +2,7 @@ import { WATCH_BATCH_MAX_WAIT_MS, WATCH_BATCH_TRAILING_MS } from '../../../../shared/filesystem-watch-batch-window' -import type { FsChangeEvent } from '../../../../shared/types' +import type { FsChangeEvent } from '../../../../shared/filesystem-entry-types' import { MAX_BATCHED_WATCHER_EVENTS } from '../../../ipc/filesystem-watcher-event-batch' export function createFileWatchEventBatcher( diff --git a/src/main/runtime/rpc/methods/git.ts b/src/main/runtime/rpc/methods/git.ts index 9363551507e..167b82966f5 100644 --- a/src/main/runtime/rpc/methods/git.ts +++ b/src/main/runtime/rpc/methods/git.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines -- Why: this table is the runtime git RPC contract; splitting it would make method coverage harder to audit. */ import { defineMethod, type RpcMethod } from '../core' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { ResolvedSourceControlAiGenerationParams } from '../../../../shared/source-control-ai' import { GitBranchCompare, diff --git a/src/main/runtime/rpc/methods/orchestration-federation.ts b/src/main/runtime/rpc/methods/orchestration-federation.ts index f1c68310931..83cbbfe385f 100644 --- a/src/main/runtime/rpc/methods/orchestration-federation.ts +++ b/src/main/runtime/rpc/methods/orchestration-federation.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { buildDispatchPreamble } from '../../orchestration/preamble' import { OrchestrationError } from '../../orchestration/orchestration-error' import { defineMethod, type RpcMethod } from '../core' diff --git a/src/main/runtime/rpc/methods/orchestration-worker-launch-preferences.ts b/src/main/runtime/rpc/methods/orchestration-worker-launch-preferences.ts index 76c60064691..f907571e2bc 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-launch-preferences.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-launch-preferences.ts @@ -6,7 +6,7 @@ import { } from '../../../../shared/agent-session-option-catalog' import { resolveAgentSessionOptionLaunch } from '../../../../shared/agent-session-option-launch' import { ORCHESTRATION_WORKER_LAUNCH_PREFERENCES_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { OrchestrationError } from '../../orchestration/orchestration-error' export type OrchestrationWorkerLaunchSelection = { diff --git a/src/main/runtime/rpc/methods/orchestration-worker-start-validation.ts b/src/main/runtime/rpc/methods/orchestration-worker-start-validation.ts index f6bb103f37e..dad044732ce 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-start-validation.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-start-validation.ts @@ -1,5 +1,5 @@ import { isTuiAgent } from '../../../../shared/tui-agent-config' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { OrcaRuntimeService } from '../../orca-runtime' import { OrchestrationError } from '../../orchestration/orchestration-error' import type { FederationAttachStartInput } from './orchestration-federation-start-schema' diff --git a/src/main/runtime/rpc/methods/orchestration-worker-topology.ts b/src/main/runtime/rpc/methods/orchestration-worker-topology.ts index 9672560ab1e..3d4a0b6bf55 100644 --- a/src/main/runtime/rpc/methods/orchestration-worker-topology.ts +++ b/src/main/runtime/rpc/methods/orchestration-worker-topology.ts @@ -1,5 +1,5 @@ import type { AgentLaunchPreferences } from '../../../../shared/agent-session-host-authority' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { OrcaRuntimeService } from '../../orca-runtime' import type { OrchestrationDb } from '../../orchestration/db' diff --git a/src/main/runtime/rpc/methods/orchestration-workers.ts b/src/main/runtime/rpc/methods/orchestration-workers.ts index a71e3299929..d1d94d897a5 100644 --- a/src/main/runtime/rpc/methods/orchestration-workers.ts +++ b/src/main/runtime/rpc/methods/orchestration-workers.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { buildDispatchPreamble } from '../../orchestration/preamble' import { OrchestrationError } from '../../orchestration/orchestration-error' import { defineMethod, type RpcMethod } from '../core' diff --git a/src/main/runtime/rpc/methods/session-tabs-schemas.ts b/src/main/runtime/rpc/methods/session-tabs-schemas.ts index fc7bdc2adc3..1f44e17ea0b 100644 --- a/src/main/runtime/rpc/methods/session-tabs-schemas.ts +++ b/src/main/runtime/rpc/methods/session-tabs-schemas.ts @@ -1,7 +1,7 @@ import { z } from 'zod' import { MAX_QUICK_COMMAND_AGENT_PROMPT_LENGTH } from '../../../../shared/terminal-quick-commands' import { isTuiAgent } from '../../../../shared/tui-agent-config' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { sleepingAgentLaunchConfigSchema } from '../../../../shared/workspace-session-sleeping-agents' import { RUNTIME_NAVIGATION_TARGETS } from '../../../../shared/runtime-navigation' import { TAB_ACTIVATION_INTENTS } from '../../../../shared/tab-activation-intent' diff --git a/src/main/runtime/rpc/methods/task-resume-state-schema.ts b/src/main/runtime/rpc/methods/task-resume-state-schema.ts index 06e90f8a811..fc85cad6270 100644 --- a/src/main/runtime/rpc/methods/task-resume-state-schema.ts +++ b/src/main/runtime/rpc/methods/task-resume-state-schema.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import type { TaskResumeState as TaskResumeStateType } from '../../../../shared/types' +import type { TaskResumeState as TaskResumeStateType } from '../../../../shared/ui-chrome-types' import type { AssertNoMissingKeys } from './ui-state-schema-parity' /** diff --git a/src/main/runtime/rpc/methods/terminal-orphan.ts b/src/main/runtime/rpc/methods/terminal-orphan.ts index 979c6aa747c..97296d0fac5 100644 --- a/src/main/runtime/rpc/methods/terminal-orphan.ts +++ b/src/main/runtime/rpc/methods/terminal-orphan.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import type { TabGroupLayoutNode } from '../../../../shared/types' +import type { TabGroupLayoutNode } from '../../../../shared/tab-types' import { isPtyIncarnationId, type PtyIncarnationId } from '../../../../shared/pty-incarnation' import { defineMethod, type RpcAnyMethod } from '../core' import { OptionalString, requiredString } from '../schemas' diff --git a/src/main/runtime/rpc/methods/terminal-quick-command-rpc-schema.ts b/src/main/runtime/rpc/methods/terminal-quick-command-rpc-schema.ts index 396710af3d3..18661f10b57 100644 --- a/src/main/runtime/rpc/methods/terminal-quick-command-rpc-schema.ts +++ b/src/main/runtime/rpc/methods/terminal-quick-command-rpc-schema.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import type { TerminalQuickCommand } from '../../../../shared/types' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' import { MAX_QUICK_COMMAND_AGENT_PROMPT_LENGTH, MAX_QUICK_COMMAND_ID_LENGTH, diff --git a/src/main/runtime/rpc/methods/ui-state-schema-parity-checks.ts b/src/main/runtime/rpc/methods/ui-state-schema-parity-checks.ts index a1ccf86c254..2af2ffe0ed8 100644 --- a/src/main/runtime/rpc/methods/ui-state-schema-parity-checks.ts +++ b/src/main/runtime/rpc/methods/ui-state-schema-parity-checks.ts @@ -1,5 +1,5 @@ import type { z } from 'zod' -import type { PersistedUIState } from '../../../../shared/types' +import type { PersistedUIState } from '../../../../shared/persisted-ui-state-types' import type { UiUpdateFieldsSchema } from './client-ui-schemas' import type { AssertNoMissingKeys, AssertNoMissingValues } from './ui-state-schema-parity' diff --git a/src/main/runtime/rpc/methods/worktree-schemas.ts b/src/main/runtime/rpc/methods/worktree-schemas.ts index a3e562e8516..d401ddd414d 100644 --- a/src/main/runtime/rpc/methods/worktree-schemas.ts +++ b/src/main/runtime/rpc/methods/worktree-schemas.ts @@ -1,6 +1,6 @@ import { z } from 'zod' import { isTuiAgent } from '../../../../shared/tui-agent-config' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { workspaceSourceSchema } from '../../../../shared/telemetry-events' import { sleepingAgentLaunchConfigSchema } from '../../../../shared/workspace-session-sleeping-agents' import { RUNTIME_NAVIGATION_TARGETS } from '../../../../shared/runtime-navigation' diff --git a/src/main/runtime/rpc/workspace-creator-context.ts b/src/main/runtime/rpc/workspace-creator-context.ts index de0b27965fd..6bf1adf9949 100644 --- a/src/main/runtime/rpc/workspace-creator-context.ts +++ b/src/main/runtime/rpc/workspace-creator-context.ts @@ -1,4 +1,4 @@ -import type { WorkspaceCreatorProvenance } from '../../../shared/types' +import type { WorkspaceCreatorProvenance } from '../../../shared/worktree/types' import type { RpcContext } from './core' export function resolveRpcWorkspaceCreatorProvenance( diff --git a/src/main/runtime/runtime-folder-workspace.test.ts b/src/main/runtime/runtime-folder-workspace.test.ts index 06d97809c90..a801fdc6031 100644 --- a/src/main/runtime/runtime-folder-workspace.test.ts +++ b/src/main/runtime/runtime-folder-workspace.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { Repo, WorktreeMeta } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR } from '../../shared/worktree/id' import { getRuntimeFolderWorkspaceInstanceId, diff --git a/src/main/runtime/runtime-folder-workspace.ts b/src/main/runtime/runtime-folder-workspace.ts index 63f548a1037..18c226221bd 100644 --- a/src/main/runtime/runtime-folder-workspace.ts +++ b/src/main/runtime/runtime-folder-workspace.ts @@ -1,5 +1,7 @@ import { DEFAULT_WORKSPACE_STATUS_ID } from '../../shared/workspace-statuses' -import type { Repo, Worktree, WorktreeMeta } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' +import type { Worktree } from '../../shared/worktree/types' import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR } from '../../shared/worktree/id' import { normalizeWorkspaceCreatorProvenance } from '../../shared/workspace-creator-provenance' diff --git a/src/main/runtime/runtime-remove-project-host-scope.test.ts b/src/main/runtime/runtime-remove-project-host-scope.test.ts index 50bc15d13c9..ab9a781d0db 100644 --- a/src/main/runtime/runtime-remove-project-host-scope.test.ts +++ b/src/main/runtime/runtime-remove-project-host-scope.test.ts @@ -6,7 +6,7 @@ * is reachable — and since #11994 every paired device now learns about it immediately. */ import { describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import { OrcaRuntimeService } from './orca-runtime' function makeRepos(): Repo[] { diff --git a/src/main/runtime/selected-review-branch.ts b/src/main/runtime/selected-review-branch.ts index f874ab8a1ab..5feda3bd55f 100644 --- a/src/main/runtime/selected-review-branch.ts +++ b/src/main/runtime/selected-review-branch.ts @@ -1,4 +1,4 @@ -import type { GitPushTarget } from '../../shared/types' +import type { GitPushTarget } from '../../shared/worktree/types' import type { ForgeProviderId } from '../source-control/forge-provider' import type { getPRForBranch } from '../github/client' diff --git a/src/main/runtime/terminal-list-stale-leaf-liveness.test.ts b/src/main/runtime/terminal-list-stale-leaf-liveness.test.ts index 5cbb967da76..158dff47c37 100644 --- a/src/main/runtime/terminal-list-stale-leaf-liveness.test.ts +++ b/src/main/runtime/terminal-list-stale-leaf-liveness.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { OrcaRuntimeService } from './orca-runtime' import { getDefaultWorkspaceSession } from '../../shared/constants' -import type { WorkspaceSessionState } from '../../shared/types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' // run6-review-pr-11959 repro: leaf.connected mirrors the graph (`ptyId !== null`), // so a restored leaf whose PTY no provider owns must be demoted from the diff --git a/src/main/runtime/terminal-model-query-authority.ts b/src/main/runtime/terminal-model-query-authority.ts index df8f05798a4..a1dd4aac2d8 100644 --- a/src/main/runtime/terminal-model-query-authority.ts +++ b/src/main/runtime/terminal-model-query-authority.ts @@ -8,7 +8,7 @@ * renderer's native-Windows-ConPTY determination, recorded per PTY at spawn * so the runtime emulator can register the DA1 override before byte zero. */ -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { isWslUncPath } from '../../shared/wsl-paths' import { isHiddenPtyDeliveryGateEnabled, diff --git a/src/main/runtime/terminal-orphan-topology.ts b/src/main/runtime/terminal-orphan-topology.ts index bcb0b6e60ca..b55a351a1b0 100644 --- a/src/main/runtime/terminal-orphan-topology.ts +++ b/src/main/runtime/terminal-orphan-topology.ts @@ -1,4 +1,4 @@ -import type { TabGroupLayoutNode } from '../../shared/types' +import type { TabGroupLayoutNode } from '../../shared/tab-types' function collectLayoutGroupIds(node: TabGroupLayoutNode | null | undefined, ids: string[]): void { if (!node) { diff --git a/src/main/runtime/terminal-restore-record-seed.test.ts b/src/main/runtime/terminal-restore-record-seed.test.ts index 8e3b49620d4..bdc98f6ef92 100644 --- a/src/main/runtime/terminal-restore-record-seed.test.ts +++ b/src/main/runtime/terminal-restore-record-seed.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { OrcaRuntimeService } from './orca-runtime' import { getDefaultWorkspaceSession } from '../../shared/constants' -import type { WorkspaceSessionState } from '../../shared/types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' // STA repro (post-restart blind orchestrator incident): after an app relaunch // the daemon survives and spawn reattaches silently, but the restore payload diff --git a/src/main/runtime/terminal-send-stale-leaf-liveness.test.ts b/src/main/runtime/terminal-send-stale-leaf-liveness.test.ts index 8394140cb54..f64e960615b 100644 --- a/src/main/runtime/terminal-send-stale-leaf-liveness.test.ts +++ b/src/main/runtime/terminal-send-stale-leaf-liveness.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { OrcaRuntimeService } from './orca-runtime' import { getDefaultWorkspaceSession } from '../../shared/constants' -import type { WorkspaceSessionState } from '../../shared/types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' // STA repro (silent-send incident): `orca terminal send` to a leaf whose ptyId // no provider in this process owns was a silent no-op reported as success — diff --git a/src/main/runtime/windows-drive-listing.ts b/src/main/runtime/windows-drive-listing.ts index 1b9dbac062c..af4eda39f94 100644 --- a/src/main/runtime/windows-drive-listing.ts +++ b/src/main/runtime/windows-drive-listing.ts @@ -1,6 +1,6 @@ import { stat } from 'node:fs/promises' import type { Stats } from 'node:fs' -import type { FilesystemPathFlavor } from '../../shared/types' +import type { FilesystemPathFlavor } from '../../shared/filesystem-entry-types' export type DriveListing = { resolvedPath: string diff --git a/src/main/runtime/workspace-session-failed-write-rollback.ts b/src/main/runtime/workspace-session-failed-write-rollback.ts index f770511bb27..4f9e79a03d7 100644 --- a/src/main/runtime/workspace-session-failed-write-rollback.ts +++ b/src/main/runtime/workspace-session-failed-write-rollback.ts @@ -1,5 +1,5 @@ import { isDeepStrictEqual } from 'node:util' -import type { WorkspaceSessionState } from '../../shared/types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' const MISSING = Symbol('missing') type RollbackValue = unknown diff --git a/src/main/runtime/workspace-session-terminal-membership-authority.ts b/src/main/runtime/workspace-session-terminal-membership-authority.ts index 556d488edc2..c4d258d8317 100644 --- a/src/main/runtime/workspace-session-terminal-membership-authority.ts +++ b/src/main/runtime/workspace-session-terminal-membership-authority.ts @@ -1,10 +1,9 @@ +import type { Tab, TabGroup } from '../../shared/tab-types' import type { - Tab, - TabGroup, TerminalLayoutSnapshot, - TerminalPaneLayoutNode, - WorkspaceSessionState -} from '../../shared/types' + TerminalPaneLayoutNode +} from '../../shared/terminal-tab-types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { getRepoIdFromWorktreeId } from '../../shared/worktree/id' import { pruneTabGroupLayoutAfterRetirement } from './mobile-session-terminal-retirement' diff --git a/src/main/skills/discovery.test.ts b/src/main/skills/discovery.test.ts index 55dcb55ed36..2130b5a6ee8 100644 --- a/src/main/skills/discovery.test.ts +++ b/src/main/skills/discovery.test.ts @@ -4,7 +4,7 @@ import { join } from 'node:path' import { beforeEach, describe, expect, it, vi } from 'vitest' import { buildSkillDiscoverySources, clearSkillRootScanCache, discoverSkills } from './discovery' import { TUI_AGENT_CONFIG } from '../../shared/tui-agent-config' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' function makeRepo(path: string, connectionId: string | null = null): Repo { return { diff --git a/src/main/skills/discovery.ts b/src/main/skills/discovery.ts index 4fc04ea5d20..882ab23d1cf 100644 --- a/src/main/skills/discovery.ts +++ b/src/main/skills/discovery.ts @@ -2,7 +2,7 @@ import { open, realpath, stat } from 'node:fs/promises' import { homedir } from 'node:os' import { basename, dirname, relative, sep } from 'node:path' import { summarizeSkillMarkdown } from '../../shared/skill-metadata' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import type { DiscoveredSkill, SkillDiscoveryResult, diff --git a/src/main/skills/skill-discovery-sources.ts b/src/main/skills/skill-discovery-sources.ts index ad457d00e81..78a858e697a 100644 --- a/src/main/skills/skill-discovery-sources.ts +++ b/src/main/skills/skill-discovery-sources.ts @@ -8,7 +8,7 @@ import type { SkillSourceKind } from '../../shared/skills' import type { AgentType } from '../../shared/agent-status-types' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' export type SkillScanRoot = Omit diff --git a/src/main/skills/skill-discovery-target.test.ts b/src/main/skills/skill-discovery-target.test.ts index f136536b189..18cb430bb68 100644 --- a/src/main/skills/skill-discovery-target.test.ts +++ b/src/main/skills/skill-discovery-target.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { SkillDiscoveryResult } from '../../shared/skills' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' const { nativeScans, wslScans } = vi.hoisted(() => ({ nativeScans: [] as unknown[], diff --git a/src/main/skills/skill-discovery-target.ts b/src/main/skills/skill-discovery-target.ts index 3670aef905e..112c8151978 100644 --- a/src/main/skills/skill-discovery-target.ts +++ b/src/main/skills/skill-discovery-target.ts @@ -1,4 +1,4 @@ -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import type { SkillDiscoveryResult, SkillDiscoveryTarget } from '../../shared/skills' import { getDefaultWslDistro, getWslHome, parseWslPath, toLinuxPath } from '../wsl' import { clearSkillRootScanCache, discoverSkills } from './discovery' diff --git a/src/main/skills/skill-freshness-inventory.test.ts b/src/main/skills/skill-freshness-inventory.test.ts index f2cb31e1357..04e778c42dc 100644 --- a/src/main/skills/skill-freshness-inventory.test.ts +++ b/src/main/skills/skill-freshness-inventory.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { promisify } from 'node:util' import { afterEach, describe, expect, it } from 'vitest' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import type { SkillBundleFileIdentity, SkillCurrentBundleEntry, diff --git a/src/main/skills/skill-freshness-inventory.ts b/src/main/skills/skill-freshness-inventory.ts index f502707321e..f9c051988b3 100644 --- a/src/main/skills/skill-freshness-inventory.ts +++ b/src/main/skills/skill-freshness-inventory.ts @@ -1,6 +1,6 @@ import { lstat } from 'node:fs/promises' import { join } from 'node:path' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import { SUPPORTED_GLOBAL_SKILL_TOPOLOGIES, type SkillFreshnessInstallation, diff --git a/src/main/source-control/forge-review-mappers.ts b/src/main/source-control/forge-review-mappers.ts index 5d13599ccc2..c8e3fdf3899 100644 --- a/src/main/source-control/forge-review-mappers.ts +++ b/src/main/source-control/forge-review-mappers.ts @@ -1,6 +1,7 @@ import type { HostedReviewInfo } from '../../shared/hosted-review' import { hostedReviewInfoFromGitHubPRInfo } from '../../shared/hosted-review-github' -import type { MRInfo, PRInfo } from '../../shared/types' +import type { PRInfo } from '../../shared/github/pull-request-types' +import type { MRInfo } from '../../shared/gitlab-types' import type { AzureDevOpsPullRequestInfo } from '../azure-devops/pull-request-mappers' import type { BitbucketPullRequestInfo } from '../bitbucket/pull-request-mappers' import type { GiteaPullRequestInfo } from '../gitea/pull-request-mappers' diff --git a/src/main/source-control/hosted-review-creation.ts b/src/main/source-control/hosted-review-creation.ts index 2149c1ab000..9c9be75ab24 100644 --- a/src/main/source-control/hosted-review-creation.ts +++ b/src/main/source-control/hosted-review-creation.ts @@ -24,7 +24,7 @@ import { getRepoSlug } from '../github/client' import { isDefaultGitHubHost } from '../../shared/github/repository-identity-key' import { acquire, ghExecFileAsync, gitExecFileAsync, release } from '../github/gh-utils' import { isNoUpstreamError, normalizeGitErrorMessage } from '../../shared/git-remote-error' -import type { GitUpstreamStatus } from '../../shared/types' +import type { GitUpstreamStatus } from '../../shared/git-status-types' import { gitOptionalLocksDisabledEnv } from '../git/runner' import { parsePorcelainV1Records, type PorcelainV1Record } from '../git/porcelain-v1-records' import { findExistingWorktreeSymlinkPaths } from '../git/worktree-symlink-detection' diff --git a/src/main/source-control/pull-request-linked-issue.ts b/src/main/source-control/pull-request-linked-issue.ts index 2e73cd30d8d..94ea1432473 100644 --- a/src/main/source-control/pull-request-linked-issue.ts +++ b/src/main/source-control/pull-request-linked-issue.ts @@ -1,7 +1,7 @@ import type { HostedReviewProvider } from '../../shared/hosted-review' import type { PullRequestLinkedIssue } from '../../shared/pull-request-generation' import { isLinkedIssueNumber } from '../../shared/source-control-ai-action-variables' -import type { WorkspaceLinkedItem } from '../../shared/types' +import type { WorkspaceLinkedItem } from '../../shared/worktree/types' import { getIssue as getGitHubIssue } from '../github/issues' import { getIssue as getGitLabIssue } from '../gitlab/issues' diff --git a/src/main/speech/speech-model-deletion.test.ts b/src/main/speech/speech-model-deletion.test.ts index 5ce3788d7e1..e78fc0d175d 100644 --- a/src/main/speech/speech-model-deletion.test.ts +++ b/src/main/speech/speech-model-deletion.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { getDefaultVoiceSettings } from '../../shared/constants' import type { VoiceSettings } from '../../shared/speech-types' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { SPEECH_MODEL_CATALOG } from './model-catalog' import { deleteLocalSpeechModel } from './speech-model-deletion' diff --git a/src/main/ssh/ssh-target-id-migration.test.ts b/src/main/ssh/ssh-target-id-migration.test.ts index 7628428654e..59ba19d4dc9 100644 --- a/src/main/ssh/ssh-target-id-migration.test.ts +++ b/src/main/ssh/ssh-target-id-migration.test.ts @@ -1,5 +1,7 @@ import { describe, it, expect } from 'vitest' -import type { PersistedUIState, TerminalTab, WorkspaceSessionState } from '../../shared/types' +import type { PersistedUIState } from '../../shared/persisted-ui-state-types' +import type { TerminalTab } from '../../shared/terminal-tab-types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { migrateUiHostScopeSshTargetId, migrateWorkspaceSessionSshTargetId diff --git a/src/main/ssh/ssh-target-id-migration.ts b/src/main/ssh/ssh-target-id-migration.ts index f7fe153a2be..18d3f075bf0 100644 --- a/src/main/ssh/ssh-target-id-migration.ts +++ b/src/main/ssh/ssh-target-id-migration.ts @@ -1,4 +1,5 @@ -import type { PersistedUIState, WorkspaceSessionState } from '../../shared/types' +import type { PersistedUIState } from '../../shared/persisted-ui-state-types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { parseAppSshPtyId, toAppSshPtyId } from '../../shared/ssh-pty-id' import { toSshExecutionHostId } from '../../shared/execution-host' import { normalizeManualRepoOrder } from '../../shared/manual-repo-order' diff --git a/src/main/star-nag/service.test.ts b/src/main/star-nag/service.test.ts index 60f166ff200..2e52ada5442 100644 --- a/src/main/star-nag/service.test.ts +++ b/src/main/star-nag/service.test.ts @@ -3,7 +3,7 @@ Electron/IPC harness; splitting the narrow service suite would duplicate setup and make the prompt-session edge cases harder to compare. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { STAR_NAG_INITIAL_THRESHOLD } from '../../shared/constants' -import type { PersistedUIState } from '../../shared/types' +import type { PersistedUIState } from '../../shared/persisted-ui-state-types' import type { Store } from '../persistence' import type { StatsCollector } from '../stats/collector' import { StarNagService } from './service' diff --git a/src/main/startup/dev-education-suppression.test.ts b/src/main/startup/dev-education-suppression.test.ts index ab969b3faf4..e84541f0f68 100644 --- a/src/main/startup/dev-education-suppression.test.ts +++ b/src/main/startup/dev-education-suppression.test.ts @@ -8,7 +8,8 @@ import { import { CONTEXTUAL_TOUR_IDS } from '../../shared/contextual-tours' import { FEATURE_INTERACTION_IDS } from '../../shared/feature-interactions' import { FEATURE_TIP_IDS } from '../../shared/feature-tips' -import type { OnboardingState, PersistedUIState } from '../../shared/types' +import type { OnboardingState } from '../../shared/onboarding-state-types' +import type { PersistedUIState } from '../../shared/persisted-ui-state-types' import { DEV_SHOW_FIRST_RUN_EDUCATION_ENV, shouldSuppressDevEducation, diff --git a/src/main/startup/dev-education-suppression.ts b/src/main/startup/dev-education-suppression.ts index 9b3897169b8..bea97e7bbc8 100644 --- a/src/main/startup/dev-education-suppression.ts +++ b/src/main/startup/dev-education-suppression.ts @@ -5,7 +5,7 @@ import { type FeatureInteractionState } from '../../shared/feature-interactions' import { FEATURE_TIP_IDS } from '../../shared/feature-tips' -import type { PersistedUIState } from '../../shared/types' +import type { PersistedUIState } from '../../shared/persisted-ui-state-types' import type { Store } from '../persistence' export const DEV_SHOW_FIRST_RUN_EDUCATION_ENV = 'ORCA_DEV_SHOW_FIRST_RUN_EDUCATION' diff --git a/src/main/startup/hydrate-shell-path.ts b/src/main/startup/hydrate-shell-path.ts index 38a16816507..db41fe3f462 100644 --- a/src/main/startup/hydrate-shell-path.ts +++ b/src/main/startup/hydrate-shell-path.ts @@ -1,6 +1,6 @@ import { spawn } from 'node:child_process' import { delimiter, win32 as pathWin32 } from 'node:path' -import type { ShellHydrationFailureReason } from '../../shared/types' +import type { ShellHydrationFailureReason } from '../../shared/shell-path-hydration-types' import { resolveWindowsShellStartupFamily } from '../../shared/windows-terminal-shell' import { WindowsShellPathOwnership, windowsPathSegmentKey } from './windows-shell-path-ownership' diff --git a/src/main/stats/collector.ts b/src/main/stats/collector.ts index ccc516f95f4..27647af20ad 100644 --- a/src/main/stats/collector.ts +++ b/src/main/stats/collector.ts @@ -1,6 +1,6 @@ import { app } from 'electron' import { join } from 'node:path' -import type { StatsSummary } from '../../shared/types' +import type { StatsSummary } from '../../shared/process-stats-types' import type { StatsEvent, StatsAggregates } from './types' import { loadStatsFile, STATS_SCHEMA_VERSION } from './stats-file-loader' import { StatsSnapshotWriter } from './stats-snapshot-writer' diff --git a/src/main/synthetic-title-frame-routing.ts b/src/main/synthetic-title-frame-routing.ts index 3bb10a030fa..551e871fd01 100644 --- a/src/main/synthetic-title-frame-routing.ts +++ b/src/main/synthetic-title-frame-routing.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../shared/types' +import type { GlobalSettings } from '../shared/global-settings-types' /** * Why: with the side-effect kill switch off, renderer byte parsers are the diff --git a/src/main/telemetry/client-test-harness.ts b/src/main/telemetry/client-test-harness.ts index 8e0dbb58230..4f3e4054be1 100644 --- a/src/main/telemetry/client-test-harness.ts +++ b/src/main/telemetry/client-test-harness.ts @@ -1,7 +1,7 @@ import type { PostHog } from 'posthog-node' import { vi } from 'vitest' import type { CommonProps } from '../../shared/telemetry-events' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import type { Store } from '../persistence' import { resetBurstCapsForSession } from './burst-cap' import { diff --git a/src/main/telemetry/cohort-classifier.test.ts b/src/main/telemetry/cohort-classifier.test.ts index 2d1593cde8e..ac4f8d14173 100644 --- a/src/main/telemetry/cohort-classifier.test.ts +++ b/src/main/telemetry/cohort-classifier.test.ts @@ -3,7 +3,7 @@ // at most one warn per session. See docs/onboarding-funnel-cohort-addendum.md. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import type { Store } from '../persistence' import { _resetSessionWarnFlagForTests, diff --git a/src/main/telemetry/consent.test.ts b/src/main/telemetry/consent.test.ts index 12ee8ad0058..46a54c04b6d 100644 --- a/src/main/telemetry/consent.test.ts +++ b/src/main/telemetry/consent.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { resolveConsent, _resetMisconfigWarnCacheForTests } from './consent' // A minimal GlobalSettings stub — the resolver only reads `settings.telemetry`, diff --git a/src/main/telemetry/consent.ts b/src/main/telemetry/consent.ts index 85ed0dfd481..20b78526406 100644 --- a/src/main/telemetry/consent.ts +++ b/src/main/telemetry/consent.ts @@ -6,7 +6,7 @@ // runtime only and never mutate `GlobalSettings.telemetry.optedIn`. Unsetting // the variable on the next launch restores the user's stored preference. -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import type { TelemetryConsentState } from '../../shared/telemetry-consent-types' // Discriminated union instead of a boolean: the Privacy pane (PR 3) needs the diff --git a/src/main/telemetry/install-id.test.ts b/src/main/telemetry/install-id.test.ts index 69591a72663..c2fa51f2c1d 100644 --- a/src/main/telemetry/install-id.test.ts +++ b/src/main/telemetry/install-id.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi } from 'vitest' import type { Store } from '../persistence' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { generateInstallId, readInstallId } from './install-id' // Minimal in-memory store stand-in. The real `Store` pulls in Electron + fs, diff --git a/src/main/telemetry/onboarding-cohort-classifier.test.ts b/src/main/telemetry/onboarding-cohort-classifier.test.ts index 25aa5e2f67e..e74b0deb8d5 100644 --- a/src/main/telemetry/onboarding-cohort-classifier.test.ts +++ b/src/main/telemetry/onboarding-cohort-classifier.test.ts @@ -5,7 +5,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ONBOARDING_FINAL_STEP } from '../../shared/constants' -import type { GlobalSettings, OnboardingState } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { OnboardingState } from '../../shared/onboarding-state-types' import type { Store } from '../persistence' import { _resetSessionWarnFlagForTests, diff --git a/src/main/terminal-scrollback-snapshot-async-migration.ts b/src/main/terminal-scrollback-snapshot-async-migration.ts index b1a064657c1..1768c0a20a1 100644 --- a/src/main/terminal-scrollback-snapshot-async-migration.ts +++ b/src/main/terminal-scrollback-snapshot-async-migration.ts @@ -1,4 +1,4 @@ -import type { WorkspaceSessionState } from '../shared/types' +import type { WorkspaceSessionState } from '../shared/workspace-session-state-types' import { collectTerminalScrollbackSnapshotRefs, deleteTerminalScrollbackSnapshot, diff --git a/src/main/terminal-scrollback-snapshots.ts b/src/main/terminal-scrollback-snapshots.ts index de387c356c9..296dd4a081b 100644 --- a/src/main/terminal-scrollback-snapshots.ts +++ b/src/main/terminal-scrollback-snapshots.ts @@ -12,7 +12,7 @@ import { import { mkdir, rename, rm, writeFile } from 'node:fs/promises' import { dirname, join } from 'node:path' import { app } from 'electron' -import type { WorkspaceSessionState } from '../shared/types' +import type { WorkspaceSessionState } from '../shared/workspace-session-state-types' import { TERMINAL_SCROLLBACK_REPLAY_BYTE_LIMIT, TERMINAL_SCROLLBACK_STORE_BYTE_LIMIT diff --git a/src/main/text-generation/commit-message-text-generation.test.ts b/src/main/text-generation/commit-message-text-generation.test.ts index 1163f54bb42..2da0f012479 100644 --- a/src/main/text-generation/commit-message-text-generation.test.ts +++ b/src/main/text-generation/commit-message-text-generation.test.ts @@ -8,7 +8,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../shared/constants' import { sourceControlAiSettingsFromLegacy } from '../../shared/source-control-ai' import { SSH_MUX_REQUEST_TIMEOUT_CODE } from '../ssh/ssh-channel-multiplexer' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { cancelGenerateCommitMessageLocal, cancelGeneratePullRequestFieldsLocal, diff --git a/src/main/text-generation/commit-message-text-generation.ts b/src/main/text-generation/commit-message-text-generation.ts index a13372de858..655c6de0e7d 100644 --- a/src/main/text-generation/commit-message-text-generation.ts +++ b/src/main/text-generation/commit-message-text-generation.ts @@ -2,7 +2,9 @@ spawn failure handling, and output normalization; keeping them together prevents those paths from drifting. */ import { spawn, type ChildProcess } from 'node:child_process' -import type { GlobalSettings, Repo, TuiAgent } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { Repo } from '../../shared/repo-types' +import type { TuiAgent } from '../../shared/tui-agent' import { buildCommitMessagePrompt, splitGeneratedCommitMessage, diff --git a/src/main/updater-changelog.ts b/src/main/updater-changelog.ts index a534075cfc6..3af55b872de 100644 --- a/src/main/updater-changelog.ts +++ b/src/main/updater-changelog.ts @@ -1,5 +1,5 @@ import { net } from 'electron' -import type { ChangelogData } from '../shared/types' +import type { ChangelogData } from '../shared/update-status-types' import { compareVersions } from './updater-fallback' type ChangelogEntry = { diff --git a/src/main/updater-events.ts b/src/main/updater-events.ts index eb1ca193510..b77cd8a8cc5 100644 --- a/src/main/updater-events.ts +++ b/src/main/updater-events.ts @@ -1,5 +1,5 @@ import { app, autoUpdater as nativeUpdater } from 'electron' -import type { UpdateStatus } from '../shared/types' +import type { UpdateStatus } from '../shared/update-status-types' import { consumeMacInstallGuardBypass, deferMacQuitUntilInstallerReady, diff --git a/src/main/updater-fallback.ts b/src/main/updater-fallback.ts index a1c77b315d6..22cfb049555 100644 --- a/src/main/updater-fallback.ts +++ b/src/main/updater-fallback.ts @@ -1,4 +1,4 @@ -import type { UpdateStatus } from '../shared/types' +import type { UpdateStatus } from '../shared/update-status-types' import { compareAppVersions, isPrereleaseAppVersion, diff --git a/src/main/updater-linux-package-recovery-actions.test.ts b/src/main/updater-linux-package-recovery-actions.test.ts index 75f593b7f57..4e9d50ecdf5 100644 --- a/src/main/updater-linux-package-recovery-actions.test.ts +++ b/src/main/updater-linux-package-recovery-actions.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { UpdateStatus } from '../shared/types' +import type { UpdateStatus } from '../shared/update-status-types' import type * as UpdaterModule from './updater' const { diff --git a/src/main/updater-mac-install.ts b/src/main/updater-mac-install.ts index 8012f79d8fc..e2441d64180 100644 --- a/src/main/updater-mac-install.ts +++ b/src/main/updater-mac-install.ts @@ -1,5 +1,5 @@ import { app } from 'electron' -import type { UpdateStatus } from '../shared/types' +import type { UpdateStatus } from '../shared/update-status-types' import { recordUpdaterLifecycle } from './updater-lifecycle-diagnostics' const MAC_INSTALL_READY_TIMEOUT_MS = 15000 diff --git a/src/main/updater.fallback.test.ts b/src/main/updater.fallback.test.ts index f946aafeb6f..9cbcc97c85e 100644 --- a/src/main/updater.fallback.test.ts +++ b/src/main/updater.fallback.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { UpdateStatus } from '../shared/types' +import type { UpdateStatus } from '../shared/update-status-types' import { compareVersions, isBenignCheckFailure, diff --git a/src/main/updater.test.ts b/src/main/updater.test.ts index 1f5cdc06994..f42d58b5c8e 100644 --- a/src/main/updater.test.ts +++ b/src/main/updater.test.ts @@ -6,7 +6,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import type * as UpdaterModule from './updater' import type * as RecoveryModule from './linux-package-update-recovery' -import type { UpdateStatus } from '../shared/types' +import type { UpdateStatus } from '../shared/update-status-types' type RevalidationVerdict = Awaited< ReturnType diff --git a/src/main/updater.ts b/src/main/updater.ts index 96bf163e54e..f89c00331a7 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -7,7 +7,7 @@ import type { UpdateCheckOptions, UpdateSource, UpdateStatus -} from '../shared/types' +} from '../shared/update-status-types' import type { RemoteServerUpdateInstallResult, RemoteServerUpdaterSnapshot, diff --git a/src/main/usage-worktree-metadata.ts b/src/main/usage-worktree-metadata.ts index 8c4f3a94ba6..ed6889a800b 100644 --- a/src/main/usage-worktree-metadata.ts +++ b/src/main/usage-worktree-metadata.ts @@ -1,5 +1,5 @@ import { basename } from 'node:path' -import type { Repo } from '../shared/types' +import type { Repo } from '../shared/repo-types' import { splitWorktreeId, splitWorktreeIdForFilesystem } from '../shared/worktree/id' import { isFolderRepo } from '../shared/repo-kind' import type { Store } from './persistence' diff --git a/src/main/usage/usage-worktree-refs.ts b/src/main/usage/usage-worktree-refs.ts index f76ed7c0a58..f9ea5b7b9f6 100644 --- a/src/main/usage/usage-worktree-refs.ts +++ b/src/main/usage/usage-worktree-refs.ts @@ -1,4 +1,4 @@ -import type { Repo } from '../../shared/types' +import type { Repo } from '../../shared/repo-types' import type { UsageWorktreeRef } from '../usage-worktree-metadata' import type { UsageScanWorktreeRef } from './usage-provider-contract' diff --git a/src/main/warp-themes/parser.ts b/src/main/warp-themes/parser.ts index fa4ad55c40b..e37731a8e85 100644 --- a/src/main/warp-themes/parser.ts +++ b/src/main/warp-themes/parser.ts @@ -1,6 +1,6 @@ import path from 'node:path' import { parseDocument } from 'yaml' -import type { TerminalColorOverrides } from '../../shared/types' +import type { TerminalColorOverrides } from '../../shared/terminal-color-overrides' import { hasUsableTerminalThemeColors, makeCustomTerminalThemeSelection, diff --git a/src/main/window/attach-main-window-services.ts b/src/main/window/attach-main-window-services.ts index ad8551acb6d..0fbd850f06b 100644 --- a/src/main/window/attach-main-window-services.ts +++ b/src/main/window/attach-main-window-services.ts @@ -4,12 +4,9 @@ import { randomUUID } from 'node:crypto' import { app, ipcMain } from 'electron' import type { BrowserWindow, IpcMainInvokeEvent } from 'electron' import type { Store } from '../persistence' -import type { - CreateWorktreeResult, - ReleaseBuildListResult, - UpdateCheckOptions, - WorktreeStartupLaunch -} from '../../shared/types' +import type { ReleaseBuildListResult, UpdateCheckOptions } from '../../shared/update-status-types' +import type { CreateWorktreeResult } from '../../shared/worktree/create-types' +import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types' import { RELEASE_CHANNELS, type ReleaseChannel } from '../../shared/release-channel' import { acknowledgePendingTccPromptNotice, diff --git a/src/main/workspace-space-analysis-capacity.test.ts b/src/main/workspace-space-analysis-capacity.test.ts index ebf1e0ae634..42a2a2cee48 100644 --- a/src/main/workspace-space-analysis-capacity.test.ts +++ b/src/main/workspace-space-analysis-capacity.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import type * as NodeProcess from 'node:process' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../shared/types' +import type { Repo } from '../shared/repo-types' import type * as WorkspaceSpaceScanBudgetModule from '../shared/workspace-space-scan-budget' import type { Store } from './persistence' diff --git a/src/main/workspace-space-analysis-du-timeout.test.ts b/src/main/workspace-space-analysis-du-timeout.test.ts index ecd2e97eb8b..de467d2a7d2 100644 --- a/src/main/workspace-space-analysis-du-timeout.test.ts +++ b/src/main/workspace-space-analysis-du-timeout.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as NodeProcess from 'node:process' -import type { Repo } from '../shared/types' +import type { Repo } from '../shared/repo-types' import type { Store } from './persistence' const { execFileMock, listRepoWorktreesMock } = vi.hoisted(() => ({ diff --git a/src/main/workspace-space-analysis.test.ts b/src/main/workspace-space-analysis.test.ts index 15baa04fc04..53082b73c7f 100644 --- a/src/main/workspace-space-analysis.test.ts +++ b/src/main/workspace-space-analysis.test.ts @@ -2,7 +2,7 @@ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../shared/types' +import type { Repo } from '../shared/repo-types' import type { Store } from './persistence' const { diff --git a/src/main/workspace-space-analysis.ts b/src/main/workspace-space-analysis.ts index bdb8d953aab..56c9a7525cd 100644 --- a/src/main/workspace-space-analysis.ts +++ b/src/main/workspace-space-analysis.ts @@ -7,7 +7,9 @@ import { platform } from 'node:process' import type { Dirent } from 'node:fs' import type { Store } from './persistence' import { isFolderRepo } from '../shared/repo-kind' -import type { DirEntry, GitWorktreeInfo, Repo, Worktree } from '../shared/types' +import type { DirEntry } from '../shared/filesystem-entry-types' +import type { Repo } from '../shared/repo-types' +import type { GitWorktreeInfo, Worktree } from '../shared/worktree/types' import type { WorkspaceSpaceAnalysis, WorkspaceSpaceDirectoryScanResult, diff --git a/src/main/worktree-create-base-prefetch.ts b/src/main/worktree-create-base-prefetch.ts index fc43980202e..a7665fc4e15 100644 --- a/src/main/worktree-create-base-prefetch.ts +++ b/src/main/worktree-create-base-prefetch.ts @@ -1,5 +1,5 @@ import { isFolderRepo } from '../shared/repo-kind' -import type { Repo } from '../shared/types' +import type { Repo } from '../shared/repo-types' import { hasLocalCommitObject, isFullGitObjectId } from './git/commit-object-ref' import { hasWorktreeBaseCommitRef } from './git/worktree-base-ref-probe' import { getBaseRefDefault } from './git/repo' diff --git a/src/main/worktree-create-timing.ts b/src/main/worktree-create-timing.ts index 3dc5546c53c..433a9ac0098 100644 --- a/src/main/worktree-create-timing.ts +++ b/src/main/worktree-create-timing.ts @@ -1,4 +1,7 @@ -import type { WorktreeCreateTiming, WorktreeCreateTimingPhase } from '../shared/types' +import type { + WorktreeCreateTiming, + WorktreeCreateTimingPhase +} from '../shared/worktree/create-types' type TimingClock = () => number diff --git a/src/main/worktree-lineage-pruning.test.ts b/src/main/worktree-lineage-pruning.test.ts index e9465a9f6cc..db393e864d5 100644 --- a/src/main/worktree-lineage-pruning.test.ts +++ b/src/main/worktree-lineage-pruning.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it, vi } from 'vitest' -import type { Repo, WorktreeLineage, WorkspaceLineage, WorktreeMeta } from '../shared/types' +import type { Repo } from '../shared/repo-types' +import type { WorkspaceLineage, WorktreeLineage } from '../shared/worktree/lineage-types' +import type { WorktreeMeta } from '../shared/worktree/meta-types' import { worktreeWorkspaceKey } from '../shared/workspace-scope' import { pruneLineageForMissingRepoWorktrees } from './worktree-lineage-pruning' diff --git a/src/main/worktree-lineage-pruning.ts b/src/main/worktree-lineage-pruning.ts index b9dbe956f57..34a99ef6e95 100644 --- a/src/main/worktree-lineage-pruning.ts +++ b/src/main/worktree-lineage-pruning.ts @@ -1,5 +1,7 @@ import { randomUUID } from 'node:crypto' -import type { GitWorktreeInfo, Repo, WorktreeLineage, WorkspaceLineage } from '../shared/types' +import type { Repo } from '../shared/repo-types' +import type { WorkspaceLineage, WorktreeLineage } from '../shared/worktree/lineage-types' +import type { GitWorktreeInfo } from '../shared/worktree/types' import { getRepoExecutionHostId } from '../shared/execution-host' import { isWorkspaceKey, parseWorkspaceKey, worktreeWorkspaceKey } from '../shared/workspace-scope' import type { Store } from './persistence' diff --git a/src/main/worktree-removal-authority.test.ts b/src/main/worktree-removal-authority.test.ts index cbc9e2ed24c..4d8229f3f1f 100644 --- a/src/main/worktree-removal-authority.test.ts +++ b/src/main/worktree-removal-authority.test.ts @@ -4,7 +4,7 @@ import { isWorktreePathMissing, stripOrcaProvenanceMetaUpdates } from './worktree-removal-safety' -import type { WorktreeMeta } from '../shared/types' +import type { WorktreeMeta } from '../shared/worktree/meta-types' describe('isWorktreePathMissing', () => { it('recognizes missing-path errors from local and remote stat providers', async () => { diff --git a/src/main/worktree-removal-safety.test.ts b/src/main/worktree-removal-safety.test.ts index 5a64b8c90e5..968bbd96bb5 100644 --- a/src/main/worktree-removal-safety.test.ts +++ b/src/main/worktree-removal-safety.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { homedir } from 'node:os' -import type { GitWorktreeInfo } from '../shared/types' +import type { GitWorktreeInfo } from '../shared/worktree/types' import { canCleanupUnregisteredOrcaLeftoverDirectory, canSafelyRemoveOrphanedWorktreeDirectory, diff --git a/src/main/worktree-removal-safety.ts b/src/main/worktree-removal-safety.ts index ef303c18220..b37a2477bc2 100644 --- a/src/main/worktree-removal-safety.ts +++ b/src/main/worktree-removal-safety.ts @@ -2,7 +2,9 @@ import { lstat, readFile } from 'node:fs/promises' import { homedir } from 'node:os' import { posix, win32 } from 'node:path' import { isWindowsAbsolutePathLike } from '../shared/cross-platform-path' -import type { GitWorktreeInfo, Repo, WorktreeMeta } from '../shared/types' +import type { Repo } from '../shared/repo-types' +import type { WorktreeMeta } from '../shared/worktree/meta-types' +import type { GitWorktreeInfo } from '../shared/worktree/types' import { areWorktreePathsEqual } from './ipc/worktree-logic' import { gitFileProvesOrphanedWorktreeDirectory, diff --git a/src/main/worktree-removal-session-partition-fencing.test.ts b/src/main/worktree-removal-session-partition-fencing.test.ts index 53b4dc805a6..89b7feafee6 100644 --- a/src/main/worktree-removal-session-partition-fencing.test.ts +++ b/src/main/worktree-removal-session-partition-fencing.test.ts @@ -5,7 +5,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { rmSync, mkdtempSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' -import type { TerminalTab } from '../shared/types' +import type { TerminalTab } from '../shared/terminal-tab-types' import { getDefaultWorkspaceSession } from '../shared/constants' import { toRuntimeExecutionHostId } from '../shared/execution-host' diff --git a/src/main/worktree-root-preparation.test.ts b/src/main/worktree-root-preparation.test.ts index 16adf035efe..36ad1426a79 100644 --- a/src/main/worktree-root-preparation.test.ts +++ b/src/main/worktree-root-preparation.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../shared/types' +import type { Repo } from '../shared/repo-types' const { mkdirMock, authorizeExternalPathMock } = vi.hoisted(() => ({ mkdirMock: vi.fn(), diff --git a/src/main/worktree-root-preparation.ts b/src/main/worktree-root-preparation.ts index 3edcf311012..d4cf7ead15a 100644 --- a/src/main/worktree-root-preparation.ts +++ b/src/main/worktree-root-preparation.ts @@ -1,5 +1,6 @@ import { mkdir } from 'node:fs/promises' -import type { GlobalSettings, Repo } from '../shared/types' +import type { GlobalSettings } from '../shared/global-settings-types' +import type { Repo } from '../shared/repo-types' import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../shared/execution-host' import { isFolderRepo } from '../shared/repo-kind' import { computeWorkspaceRoot, getWorktreePathSettings } from './ipc/worktree-logic' diff --git a/src/main/worktree-trash.test.ts b/src/main/worktree-trash.test.ts index 49a8af75a1d..5bec9f53475 100644 --- a/src/main/worktree-trash.test.ts +++ b/src/main/worktree-trash.test.ts @@ -3,7 +3,7 @@ import { mkdir, mkdtemp, readdir, rm, symlink, writeFile } from 'node:fs/promise import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import type { Repo } from '../shared/types' +import type { Repo } from '../shared/repo-types' import { collectWorktreeTrashSweepRoots, getWorktreeTrashRoot, diff --git a/src/main/worktree-trash.ts b/src/main/worktree-trash.ts index 5c0aa32be2b..cec17bf87cd 100644 --- a/src/main/worktree-trash.ts +++ b/src/main/worktree-trash.ts @@ -8,7 +8,8 @@ import { dirname, join } from 'node:path' import { removeHostTree } from './host-tree-removal' import { isFolderRepo } from '../shared/repo-kind' import { computeWorkspaceRoot, getWorktreePathSettings } from './ipc/worktree-logic' -import type { GlobalSettings, Repo } from '../shared/types' +import type { GlobalSettings } from '../shared/global-settings-types' +import type { Repo } from '../shared/repo-types' import { parseWslPath } from './wsl' export const WORKTREE_TRASH_DIR_NAME = '.orca-worktree-trash' diff --git a/src/preload/api/agent-account-api.ts b/src/preload/api/agent-account-api.ts index 43e8542cddd..e3688b57f5b 100644 --- a/src/preload/api/agent-account-api.ts +++ b/src/preload/api/agent-account-api.ts @@ -1,4 +1,7 @@ -import type { ClaudeRateLimitAccountsState, CodexRateLimitAccountsState } from '../../shared/types' +import type { + ClaudeRateLimitAccountsState, + CodexRateLimitAccountsState +} from '../../shared/managed-account-types' import type { CodexConfigSyncStatus } from '../../shared/codex-config-sync-types' import type { GrokAccountStatus } from '../../shared/rate-limit-types' diff --git a/src/preload/api/agent-hook-api.ts b/src/preload/api/agent-hook-api.ts index df782419c5c..c4116aa2b6d 100644 --- a/src/preload/api/agent-hook-api.ts +++ b/src/preload/api/agent-hook-api.ts @@ -1,4 +1,5 @@ -import type { OrcaHooks, WorktreeSetupLaunch } from '../../shared/types' +import type { OrcaHooks } from '../../shared/orca-yaml-hook-types' +import type { WorktreeSetupLaunch } from '../../shared/worktree/launch-types' import type { ExecutionHostId } from '../../shared/execution-host' import type { SetupScriptImportCandidate } from '../../shared/setup-script-imports' import type { AgentHookInstallStatus } from '../../shared/agent-hook-types' diff --git a/src/preload/api/app-api.ts b/src/preload/api/app-api.ts index a03960fd531..ec18110e4a9 100644 --- a/src/preload/api/app-api.ts +++ b/src/preload/api/app-api.ts @@ -5,12 +5,10 @@ import type { WriteTerminalRenderDesyncEvidenceArgs, WriteTerminalRenderDesyncEvidenceResult } from '../../shared/terminal-render-desync-evidence' -import type { - FloatingTerminalCwdRequest, - MarkdownDocument, - PersistedUIState, - WorkspaceSessionState -} from '../../shared/types' +import type { MarkdownDocument } from '../../shared/filesystem-entry-types' +import type { PersistedUIState } from '../../shared/persisted-ui-state-types' +import type { FloatingTerminalCwdRequest } from '../../shared/ui-chrome-types' +import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' export type AppApi = { /** Returns the app identity currently exposed to native chrome and the titlebar. */ diff --git a/src/preload/api/browser-api.ts b/src/preload/api/browser-api.ts index b6591162d19..6b352eff527 100644 --- a/src/preload/api/browser-api.ts +++ b/src/preload/api/browser-api.ts @@ -20,16 +20,16 @@ import type { BrowserPopupEvent } from '../../shared/browser-guest-events' import type { - BrowserCookieImportResult, BrowserCertificateFailure, BrowserCertificateProceedResult, + BrowserCookieImportResult, BrowserLoadError, BrowserSessionProfile, BrowserSessionProfileCreateOptions, BrowserSessionProfileScope, BrowserSessionProfileSource, BrowserViewportOverride -} from '../../shared/types' +} from '../../shared/browser-workspace-types' export type BrowserApi = { registerGuest: (args: { diff --git a/src/preload/api/ephemeral-vm-api.ts b/src/preload/api/ephemeral-vm-api.ts index e3b6ea202fd..96d0eae30c4 100644 --- a/src/preload/api/ephemeral-vm-api.ts +++ b/src/preload/api/ephemeral-vm-api.ts @@ -1,4 +1,4 @@ -import type { OrcaHooks } from '../../shared/types' +import type { OrcaHooks } from '../../shared/orca-yaml-hook-types' import type { PublicKnownRuntimeEnvironment } from '../../shared/runtime-environments' import type { EphemeralVmRecipeDoctorResult } from '../../shared/ephemeral-vm-recipes' import type { EphemeralVmRecipeResultWarning } from '../../shared/ephemeral-vm-recipe-diagnostics' diff --git a/src/preload/api/filesystem-api.ts b/src/preload/api/filesystem-api.ts index fdc1ac73250..63dd919b76d 100644 --- a/src/preload/api/filesystem-api.ts +++ b/src/preload/api/filesystem-api.ts @@ -1,10 +1,9 @@ +import type { SearchOptions, SearchResult } from '../../shared/code-search-types' import type { DirEntry, FsChangedPayload, - MarkdownDocument, - SearchOptions, - SearchResult -} from '../../shared/types' + MarkdownDocument +} from '../../shared/filesystem-entry-types' import type { LocalLogTailChangedPayload, LocalLogTailReadArgs, diff --git a/src/preload/api/git-inspection-api.ts b/src/preload/api/git-inspection-api.ts index c23bcdeecaa..b71234d2d0c 100644 --- a/src/preload/api/git-inspection-api.ts +++ b/src/preload/api/git-inspection-api.ts @@ -1,13 +1,15 @@ import type { GitBranchCompareResult, GitCommitCompareResult, + GitDiffResult +} from '../../shared/git-diff-compare-types' +import type { GitConflictOperation, - GitDiffResult, - GitPushTarget, GitStagingArea, GitStatusResult, GitUpstreamStatus -} from '../../shared/types' +} from '../../shared/git-status-types' +import type { GitPushTarget } from '../../shared/worktree/types' import type { GitHistoryOptions, GitHistoryResult } from '../../shared/git-history' import type { CommitMessageAgentCapability, diff --git a/src/preload/api/git-operation-api.ts b/src/preload/api/git-operation-api.ts index fee33db4efd..d71f95c8537 100644 --- a/src/preload/api/git-operation-api.ts +++ b/src/preload/api/git-operation-api.ts @@ -1,9 +1,6 @@ -import type { - GitForkSyncExpectedUpstream, - GitForkSyncResult, - GitPushTarget, - TuiAgent -} from '../../shared/types' +import type { GitForkSyncExpectedUpstream, GitForkSyncResult } from '../../shared/git-fork-sync' +import type { TuiAgent } from '../../shared/tui-agent' +import type { GitPushTarget } from '../../shared/worktree/types' import type { HostedReviewProvider } from '../../shared/hosted-review' import type { ResolvedSourceControlAiGenerationParams } from '../../shared/source-control-ai' import type { SourceControlAiSettings } from '../../shared/source-control-ai-types' diff --git a/src/preload/api/github-pull-request-api.ts b/src/preload/api/github-pull-request-api.ts index 245453cb18f..76f407598b2 100644 --- a/src/preload/api/github-pull-request-api.ts +++ b/src/preload/api/github-pull-request-api.ts @@ -1,25 +1,28 @@ import type { AppStarSource } from '../../shared/gh-star-source' import type { GhAuthDiagnostic } from '../../shared/github/auth-types' import type { TaskSourceContext } from '../../shared/task-source-context' +import type { PRCheckDetail, PRCheckRunDetails } from '../../shared/github/check-types' import type { - GetRateLimitResult, GitHubCommentResult, - GitHubOwnerRepo, - GitHubPRFile, - GitHubPRFileContents, + GitHubPRReviewCommentInput, + GitHubReactionContent, + PRComment +} from '../../shared/github/comment-types' +import type { GitHubPRRefreshCandidate, GitHubPRRefreshEnqueueResult, GitHubPRRefreshEvent, GitHubPRRefreshReason, - GitHubPRReviewCommentInput, - GitHubReactionContent, - GitHubViewer, - PRCheckDetail, - PRCheckRunDetails, - PRComment, - PRInfo, PRRefreshOutcome -} from '../../shared/types' +} from '../../shared/github/pull-request-refresh-types' +import type { + GitHubOwnerRepo, + GitHubPRFile, + GitHubPRFileContents, + GitHubViewer, + PRInfo +} from '../../shared/github/pull-request-types' +import type { GetRateLimitResult } from '../../shared/github/rate-limit-types' import type { GitHubRepoSelectorArgs } from './github-work-item-api' export type GithubPullRequestApi = { diff --git a/src/preload/api/github-work-item-api.ts b/src/preload/api/github-work-item-api.ts index e302e4517a8..064a8ed890b 100644 --- a/src/preload/api/github-work-item-api.ts +++ b/src/preload/api/github-work-item-api.ts @@ -27,17 +27,18 @@ import type { UpdatePullRequestBySlugArgs } from '../../shared/github/project-types' import type { TaskSourceContext } from '../../shared/task-source-context' +import type { GitHubCommentResult } from '../../shared/github/comment-types' import type { GitHubAssignableUser, - GitHubCommentResult, - GitHubCreateIssueResult, - GitHubIssueUpdate, GitHubOwnerRepo, + IssueInfo +} from '../../shared/github/pull-request-types' +import type { GitHubWorkItem, GitHubWorkItemDetails, - IssueInfo, ListWorkItemsResult -} from '../../shared/types' +} from '../../shared/github/work-item-types' +import type { GitHubCreateIssueResult, GitHubIssueUpdate } from '../../shared/issue-mutation-types' export type GitHubRepoSelectorArgs = { repoPath: string diff --git a/src/preload/api/gitlab-api.ts b/src/preload/api/gitlab-api.ts index b1467a891f4..ecde3880083 100644 --- a/src/preload/api/gitlab-api.ts +++ b/src/preload/api/gitlab-api.ts @@ -1,6 +1,6 @@ import type { TaskSourceContext } from '../../shared/task-source-context' +import type { ClassifiedError } from '../../shared/classified-error' import type { - ClassifiedError, GetGitLabRateLimitResult, GitLabAssignableUser, GitLabAuthDiagnostic, @@ -21,7 +21,7 @@ import type { ListMergeRequestsResult, MRInfo, MRListState -} from '../../shared/types' +} from '../../shared/gitlab-types' export type GitLabRepoSelectorArgs = { repoPath: string diff --git a/src/preload/api/jira-api.ts b/src/preload/api/jira-api.ts index d452bd9728d..1294850c284 100644 --- a/src/preload/api/jira-api.ts +++ b/src/preload/api/jira-api.ts @@ -14,7 +14,7 @@ import type { JiraTransition, JiraUser, JiraViewer -} from '../../shared/types' +} from '../../shared/jira-types' export type JiraApi = { connect: (args: { diff --git a/src/preload/api/linear-api.ts b/src/preload/api/linear-api.ts index 613837c4fa4..0b924953d86 100644 --- a/src/preload/api/linear-api.ts +++ b/src/preload/api/linear-api.ts @@ -1,21 +1,22 @@ import type { LinearIssueAttributeFilter } from '../../shared/linear/issue-attribute-filter' +import type { LinearIssueUpdate } from '../../shared/issue-mutation-types' +import type { LinearComment, LinearIssue } from '../../shared/linear/issue-types' import type { - LinearCollectionResult, - LinearComment, - LinearConnectionStatus, LinearCustomViewModel, LinearCustomViewSummary, - LinearIssue, - LinearIssueUpdate, + LinearProjectDetail, + LinearProjectSummary +} from '../../shared/linear/project-types' +import type { + LinearCollectionResult, + LinearConnectionStatus, LinearLabel, LinearMember, - LinearProjectDetail, - LinearProjectSummary, LinearTeam, LinearViewer, LinearWorkflowState, LinearWorkspaceSelection -} from '../../shared/types' +} from '../../shared/linear/workspace-types' export type LinearApi = { connect: (args: { diff --git a/src/preload/api/onboarding-api.ts b/src/preload/api/onboarding-api.ts index 678a6b38294..ba9f10c1a18 100644 --- a/src/preload/api/onboarding-api.ts +++ b/src/preload/api/onboarding-api.ts @@ -1,4 +1,4 @@ -import type { OnboardingState } from '../../shared/types' +import type { OnboardingState } from '../../shared/onboarding-state-types' export type OnboardingApi = { get: () => Promise diff --git a/src/preload/api/os-permission-api.ts b/src/preload/api/os-permission-api.ts index fc452df1196..8718cfc29a5 100644 --- a/src/preload/api/os-permission-api.ts +++ b/src/preload/api/os-permission-api.ts @@ -11,13 +11,13 @@ import type { LocalNetworkConnectionTestResult } from '../../shared/developer-permissions-types' import type { - NotificationDispatchRequest, - NotificationDispatchResult, NotificationDeliveryProbeResult, NotificationDismissResult, + NotificationDispatchRequest, + NotificationDispatchResult, NotificationPermissionStatusResult, NotificationSoundResult -} from '../../shared/types' +} from '../../shared/notification-settings-types' export type NotificationsApi = { dispatch: (args: NotificationDispatchRequest) => Promise diff --git a/src/preload/api/pet-api.ts b/src/preload/api/pet-api.ts index e0031cb8e36..8dffdda300b 100644 --- a/src/preload/api/pet-api.ts +++ b/src/preload/api/pet-api.ts @@ -1,4 +1,4 @@ -import type { CustomPet } from '../../shared/types' +import type { CustomPet } from '../../shared/pet-types' export type PetApi = { import: () => Promise diff --git a/src/preload/api/preflight-api.ts b/src/preload/api/preflight-api.ts index f63099a07ec..8535bb35f8c 100644 --- a/src/preload/api/preflight-api.ts +++ b/src/preload/api/preflight-api.ts @@ -1,5 +1,8 @@ import type { ProjectExecutionRuntimeResolution } from '../../shared/project-execution-runtime' -import type { PathSource, ShellHydrationFailureReason } from '../../shared/types' +import type { + PathSource, + ShellHydrationFailureReason +} from '../../shared/shell-path-hydration-types' export type PreflightStatus = { git: { installed: boolean } diff --git a/src/preload/api/pty-api.ts b/src/preload/api/pty-api.ts index 774196b6a01..d69d906047d 100644 --- a/src/preload/api/pty-api.ts +++ b/src/preload/api/pty-api.ts @@ -14,7 +14,7 @@ import type { import type { AgentKind, LaunchSource, RequestKind } from '../../shared/telemetry-events' import type { TerminalSideEffectBatch } from '../../shared/terminal-side-effect-facts' import type { TerminalViewAttributes } from '../../shared/terminal-view-attributes' -import type { TuiAgent } from '../../shared/types' +import type { TuiAgent } from '../../shared/tui-agent' import type { PtyManagementApi } from './pty-management-api' export type PtyApi = { diff --git a/src/preload/api/repository-api.ts b/src/preload/api/repository-api.ts index 1734c35bdc7..91f4834a938 100644 --- a/src/preload/api/repository-api.ts +++ b/src/preload/api/repository-api.ts @@ -4,13 +4,13 @@ import type { ListReposForExecutionHostArgs } from '../../shared/host-repo-catalog-contract' import type { - BaseRefDefaultResult, - BaseRefSearchResult, NestedRepoScanResult, - Project, ProjectGroup, ProjectGroupImportMode, - ProjectGroupImportResult, + ProjectGroupImportResult +} from '../../shared/project-group-types' +import type { + Project, ProjectHostSetup, ProjectHostSetupCreateArgs, ProjectHostSetupCreateResult, @@ -20,9 +20,9 @@ import type { ProjectHostSetupResult, ProjectHostSetupUpdateArgs, ProjectHostSetupUpdateResult, - ProjectUpdateArgs, - Repo -} from '../../shared/types' + ProjectUpdateArgs +} from '../../shared/project-types' +import type { BaseRefDefaultResult, BaseRefSearchResult, Repo } from '../../shared/repo-types' export type RepositoryApi = { list: () => Promise diff --git a/src/preload/api/settings-api.ts b/src/preload/api/settings-api.ts index b50843160dd..30f84c59b39 100644 --- a/src/preload/api/settings-api.ts +++ b/src/preload/api/settings-api.ts @@ -3,7 +3,7 @@ import type { WarpThemeImportPreview, WarpThemeImportSource } from '../../shared/terminal-custom-themes' -import type { GhosttyImportPreview, GlobalSettings } from '../../shared/types' +import type { GhosttyImportPreview, GlobalSettings } from '../../shared/global-settings-types' export type SettingsApi = { get: () => Promise diff --git a/src/preload/api/ssh-api.ts b/src/preload/api/ssh-api.ts index b25f1713e16..6740819fa19 100644 --- a/src/preload/api/ssh-api.ts +++ b/src/preload/api/ssh-api.ts @@ -9,7 +9,7 @@ import type { SshTarget, SshTargetAddResult } from '../../shared/ssh-types' -import type { FilesystemPathFlavor } from '../../shared/types' +import type { FilesystemPathFlavor } from '../../shared/filesystem-entry-types' export type SshApi = { listTargets: () => Promise diff --git a/src/preload/api/telemetry-api.ts b/src/preload/api/telemetry-api.ts index 9fc70f908b7..99f9fbad4b0 100644 --- a/src/preload/api/telemetry-api.ts +++ b/src/preload/api/telemetry-api.ts @@ -1,5 +1,5 @@ import type { TelemetryConsentState } from '../../shared/telemetry-consent-types' -import type { MemorySnapshot, StatsSummary } from '../../shared/types' +import type { MemorySnapshot, StatsSummary } from '../../shared/process-stats-types' export type StatsApi = { getSummary: () => Promise diff --git a/src/preload/api/ui-command-event-api.ts b/src/preload/api/ui-command-event-api.ts index 4de95063d42..6ec32503057 100644 --- a/src/preload/api/ui-command-event-api.ts +++ b/src/preload/api/ui-command-event-api.ts @@ -1,10 +1,10 @@ +import type { PersistedUIState } from '../../shared/persisted-ui-state-types' +import type { TuiAgent } from '../../shared/tui-agent' import type { - PersistedUIState, - TuiAgent, WorktreeDefaultTabsLaunch, WorktreeSetupLaunch, WorktreeStartupLaunch -} from '../../shared/types' +} from '../../shared/worktree/launch-types' import type { FeatureInteractionId } from '../../shared/feature-interactions' import type { KeybindingActionId } from '../../shared/keybindings' import type { BrowserFindSource } from '../../shared/browser-find-source' diff --git a/src/preload/api/updater-api.ts b/src/preload/api/updater-api.ts index f6a43da9834..0a3590e8035 100644 --- a/src/preload/api/updater-api.ts +++ b/src/preload/api/updater-api.ts @@ -3,7 +3,7 @@ import type { ReleaseBuildListResult, UpdateCheckOptions, UpdateStatus -} from '../../shared/types' +} from '../../shared/update-status-types' import type { ReleaseChannel } from '../../shared/release-channel' export type UpdaterApi = { diff --git a/src/preload/api/workspace-session-api.ts b/src/preload/api/workspace-session-api.ts index 35eb17486d7..1d2a87bfeb1 100644 --- a/src/preload/api/workspace-session-api.ts +++ b/src/preload/api/workspace-session-api.ts @@ -1,9 +1,8 @@ +import type { IssueInfo, PRInfo } from '../../shared/github/pull-request-types' import type { - IssueInfo, - PRInfo, WorkspaceSessionPatch, WorkspaceSessionState -} from '../../shared/types' +} from '../../shared/workspace-session-state-types' import type { ExecutionHostId } from '../../shared/execution-host' import type { RemoteWorkspaceChangedEvent, diff --git a/src/preload/api/worktree-api.ts b/src/preload/api/worktree-api.ts index 96089d10e88..20c352af9d3 100644 --- a/src/preload/api/worktree-api.ts +++ b/src/preload/api/worktree-api.ts @@ -17,25 +17,28 @@ import type { HostLineageSnapshot, ListDesktopLineageForHostArgs } from '../../shared/host-lineage-contract' +import type { FolderWorkspace } from '../../shared/folder-workspace-types' +import type { + WorktreeBaseStatusEvent, + WorktreeRemoteBranchConflictEvent +} from '../../shared/worktree/base-ref-drift-types' import type { AdoptProvisionedRootArgs, CreateWorktreeArgs, CreateWorktreeResult, - DetectedWorktreeListResult, - FolderWorkspace, ForceDeleteWorktreeBranchResult, + RemoveWorktreeResult, + SparsePreset +} from '../../shared/worktree/create-types' +import type { WorkspaceLineage, WorktreeLineage } from '../../shared/worktree/lineage-types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' +import type { + DetectedWorktreeListResult, GitHubPrStartPoint, GitPushTarget, - RemoveWorktreeResult, - SparsePreset, - WorkspaceLineage, Worktree, - WorktreeBaseStatusEvent, - WorktreeHeadIdentity, - WorktreeLineage, - WorktreeMeta, - WorktreeRemoteBranchConflictEvent -} from '../../shared/types' + WorktreeHeadIdentity +} from '../../shared/worktree/types' export type WorktreeApi = { list: (args: { repoId: string }) => Promise diff --git a/src/preload/index.ts b/src/preload/index.ts index d90bd2a978e..17db7429632 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -61,54 +61,55 @@ import type { } from '../shared/plugins/plugin-panel-bridge' import type { PluginConsentRequest } from '../shared/plugins/plugin-consent-request' import type { PluginChangeEvent } from '../shared/plugins/plugin-change-event' +import type { BrowserViewportOverride } from '../shared/browser-workspace-types' +import type { SearchResult } from '../shared/code-search-types' import type { - BaseRefSearchResult, - BaseRefDefaultResult, - BrowserViewportOverride, - CustomPet, - FsChangedPayload, FilesystemPathFlavor, - GetRateLimitResult, + FsChangedPayload, + MarkdownDocument +} from '../shared/filesystem-entry-types' +import type { GitForkSyncExpectedUpstream, GitForkSyncResult } from '../shared/git-fork-sync' +import type { GitStagingArea, GitUpstreamStatus } from '../shared/git-status-types' +import type { GitHubCommentResult, GitHubReactionContent } from '../shared/github/comment-types' +import type { GitHubPRRefreshCandidate, GitHubPRRefreshEvent, - GitHubPRRefreshReason, - GitHubAssignableUser, - GitHubCommentResult, - GitHubCreateIssueResult, - GitHubOwnerRepo, - GitHubReactionContent, - GitHubWorkItem, - JiraProjectStatusOrder, - GitPushTarget, - GitStagingArea, - GitForkSyncExpectedUpstream, - GitForkSyncResult, - GitUpstreamStatus, - GhosttyImportPreview, - ListWorkItemsResult, - LinearProjectDetail, - MemorySnapshot, + GitHubPRRefreshReason +} from '../shared/github/pull-request-refresh-types' +import type { GitHubAssignableUser, GitHubOwnerRepo } from '../shared/github/pull-request-types' +import type { GetRateLimitResult } from '../shared/github/rate-limit-types' +import type { GitHubWorkItem, ListWorkItemsResult } from '../shared/github/work-item-types' +import type { GhosttyImportPreview } from '../shared/global-settings-types' +import type { GitHubCreateIssueResult } from '../shared/issue-mutation-types' +import type { JiraProjectStatusOrder } from '../shared/jira-types' +import type { LinearProjectDetail } from '../shared/linear/project-types' +import type { + NotificationDeliveryProbeResult, NotificationDismissResult, NotificationDispatchResult, - NotificationDeliveryProbeResult, NotificationPermissionStatusResult, NotificationSoundDataResult, NotificationSoundPathResult, - NotificationSoundResult, - NestedRepoScanResult, - OnboardingState, - PersistedUIState, - FloatingTerminalCwdRequest, - MarkdownDocument, - SearchResult, - TuiAgent, - UpdateStatus, + NotificationSoundResult +} from '../shared/notification-settings-types' +import type { OnboardingState } from '../shared/onboarding-state-types' +import type { PersistedUIState } from '../shared/persisted-ui-state-types' +import type { CustomPet } from '../shared/pet-types' +import type { MemorySnapshot } from '../shared/process-stats-types' +import type { NestedRepoScanResult } from '../shared/project-group-types' +import type { BaseRefDefaultResult, BaseRefSearchResult } from '../shared/repo-types' +import type { TuiAgent } from '../shared/tui-agent' +import type { FloatingTerminalCwdRequest } from '../shared/ui-chrome-types' +import type { UpdateStatus } from '../shared/update-status-types' +import type { WorktreeBaseStatusEvent, + WorktreeRemoteBranchConflictEvent +} from '../shared/worktree/base-ref-drift-types' +import type { WorktreeDefaultTabsLaunch, - WorktreeHeadIdentity, - WorktreeRemoteBranchConflictEvent, WorktreeSetupLaunch -} from '../shared/types' +} from '../shared/worktree/launch-types' +import type { GitPushTarget, WorktreeHeadIdentity } from '../shared/worktree/types' import type { PtyModelRestoreNeededEvent } from '../shared/pty-model-restore-marker' import type { PtyListedSession } from '../shared/pty-listed-session' import type { diff --git a/src/preload/renderer-restart-wiring.ts b/src/preload/renderer-restart-wiring.ts index 0a177dd55b0..2dd0ad316aa 100644 --- a/src/preload/renderer-restart-wiring.ts +++ b/src/preload/renderer-restart-wiring.ts @@ -4,7 +4,7 @@ import { prepareRendererForAppRestart, type UpdaterQuitAbortRelay } from '../shared/renderer-restart-preparation' -import type { UpdateStatus } from '../shared/types' +import type { UpdateStatus } from '../shared/update-status-types' import { ORCA_APP_RESTART_ABORTED_EVENT, ORCA_APP_RESTART_STARTED_EVENT, diff --git a/src/preload/ssh-authority-forwarding.test.ts b/src/preload/ssh-authority-forwarding.test.ts index 1292d09e032..34179e1e7fa 100644 --- a/src/preload/ssh-authority-forwarding.test.ts +++ b/src/preload/ssh-authority-forwarding.test.ts @@ -5,7 +5,7 @@ import type { HostQualifiedDetectedWorktreeResult, ListDetectedWorktreesArgs } from '../shared/detected-worktree-provider-contract' -import type { DetectedWorktreeListResult } from '../shared/types' +import type { DetectedWorktreeListResult } from '../shared/worktree/types' function listDetectedVariableTypeProbe(api: PreloadApi, args: ListDetectedWorktreesArgs) { return api.worktrees.listDetected(args) diff --git a/src/relay/fs-handler-utils.ts b/src/relay/fs-handler-utils.ts index 83726c686bf..a9df29dd2f2 100644 --- a/src/relay/fs-handler-utils.ts +++ b/src/relay/fs-handler-utils.ts @@ -15,7 +15,7 @@ import { SEARCH_TIMEOUT_MS as SHARED_SEARCH_TIMEOUT_MS } from '../shared/text-search' import { IMAGE_FILE_MIME_TYPES } from '../shared/image-file-extensions' -import type { SearchResult as SharedSearchResult } from '../shared/types' +import type { SearchResult as SharedSearchResult } from '../shared/code-search-types' import { absorbPendingRipgrepSpawnError, isRipgrepUnavailableAfterLaunchFailure, diff --git a/src/relay/git-handler-push-target.ts b/src/relay/git-handler-push-target.ts index dfbed1a9186..40765491c56 100644 --- a/src/relay/git-handler-push-target.ts +++ b/src/relay/git-handler-push-target.ts @@ -1,6 +1,6 @@ import { assertGitPushTargetShape } from '../shared/git-push-target-validation' import { gitRefTargetsBranchOnRemote } from '../shared/git-remote-branch-name' -import type { GitPushTarget } from '../shared/types' +import type { GitPushTarget } from '../shared/worktree/types' type RelayGit = (args: string[], cwd: string) => Promise<{ stdout: string; stderr: string }> diff --git a/src/relay/git-handler-status-ops.ts b/src/relay/git-handler-status-ops.ts index a9d0b63ea8a..f87311c2cb3 100644 --- a/src/relay/git-handler-status-ops.ts +++ b/src/relay/git-handler-status-ops.ts @@ -8,7 +8,7 @@ import { readFile } from 'node:fs/promises' import { parseUnmergedEntry } from './git-handler-utils' import type { GitExec } from './git-handler-ops' import type { RelayGitStreamExec } from './git-stdout-stream' -import type { GitUpstreamStatus } from '../shared/types' +import type { GitUpstreamStatus } from '../shared/git-status-types' import { StatusPorcelainParser } from '../shared/git-status-porcelain-parser' import { splitRemoteBranchName } from '../shared/git-effective-upstream' import { readOrProbeNoEffectiveUpstreamStatus } from './git-status-upstream-negative-cache' diff --git a/src/relay/git-handler-worktree-remove.ts b/src/relay/git-handler-worktree-remove.ts index 80bf08c25fe..474e03bab88 100644 --- a/src/relay/git-handler-worktree-remove.ts +++ b/src/relay/git-handler-worktree-remove.ts @@ -1,5 +1,5 @@ import * as path from 'node:path' -import type { RemoveWorktreeResult } from '../shared/types' +import type { RemoveWorktreeResult } from '../shared/worktree/create-types' import { assertWorktreeUnlockedForRemoval } from '../shared/worktree/removal' import { isSubmoduleWorktreeRemovalRefusal } from '../shared/worktree/submodule-removal' import { deleteAlreadyMergedRelayBranchAfterSafeDeleteFailure } from './git-handler-branch-cleanup' diff --git a/src/relay/git-handler.ts b/src/relay/git-handler.ts index 3e8a20ea992..0558f710970 100644 --- a/src/relay/git-handler.ts +++ b/src/relay/git-handler.ts @@ -61,7 +61,7 @@ import { upstreamOnlyCommitsArePatchEquivalent } from '../shared/git-upstream-st import { assertGitPushTargetShape } from '../shared/git-push-target-validation' import { getPublishTargetStatus, type GitCommandRunner } from '../shared/git-publish-target-status' import { resolveGitRemoteRebaseSource } from '../shared/git-rebase-source' -import type { GitPushTarget } from '../shared/types' +import type { GitPushTarget } from '../shared/worktree/types' import { getEffectiveGitUpstreamStatus, resolveEffectiveGitUpstream diff --git a/src/relay/git-status-upstream-negative-cache.ts b/src/relay/git-status-upstream-negative-cache.ts index e3ad9239cc9..f3dcd2df663 100644 --- a/src/relay/git-status-upstream-negative-cache.ts +++ b/src/relay/git-status-upstream-negative-cache.ts @@ -1,7 +1,7 @@ import { createGitConfigSnapshotRunner } from '../shared/git-config-snapshot-runner' import { getEffectiveGitUpstreamStatus } from '../shared/git-effective-upstream' import type { GitCommandRunner } from '../shared/git-effective-upstream' -import type { GitUpstreamStatus } from '../shared/types' +import type { GitUpstreamStatus } from '../shared/git-status-types' const NO_EFFECTIVE_UPSTREAM_CACHE_TTL_MS = 5 * 60_000 const MAX_NO_EFFECTIVE_UPSTREAM_CACHE_ENTRIES = 512 diff --git a/src/relay/pty-handler.ts b/src/relay/pty-handler.ts index faa12d5093b..c077378ebda 100644 --- a/src/relay/pty-handler.ts +++ b/src/relay/pty-handler.ts @@ -44,7 +44,7 @@ import { mergeGitConfigEnvProtocol } from '../shared/git-credential-prompt-env' import { isTuiAgent } from '../shared/tui-agent-config' -import type { TuiAgent } from '../shared/types' +import type { TuiAgent } from '../shared/tui-agent' import { forceKillPosixPtyProcessGroups } from '../main/pty/posix-pty-process-groups' import { stripInheritedBuildModeEnv } from '../main/pty/build-mode-env' import { stripLegacyTerminalShimEnv } from '../main/pty/legacy-terminal-shim-dir' diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 02d9a2c38ba..f85eb212265 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -176,7 +176,8 @@ import { selectFloatingVisibleTabCount } from './store/selectors' import { selectActiveTerminalChromeState } from './store/active-terminal-chrome-selector' import type { VirtualizedScrollAnchor } from './hooks/useVirtualizedScrollAnchor' import type { RemoteWorkspacePatchResult } from '../../shared/remote-workspace-types' -import type { OnboardingState, UpdateStatus } from '../../shared/types' +import type { OnboardingState } from '../../shared/onboarding-state-types' +import type { UpdateStatus } from '../../shared/update-status-types' import { getFeatureTipsAppOpenDecision, isCliFeatureTipCompleted diff --git a/src/renderer/src/components/GitHubItemDialog.tsx b/src/renderer/src/components/GitHubItemDialog.tsx index bba079e9f6d..fea34534873 100644 --- a/src/renderer/src/components/GitHubItemDialog.tsx +++ b/src/renderer/src/components/GitHubItemDialog.tsx @@ -186,22 +186,22 @@ import { import { startFixChecksAgent } from '@/lib/fix-checks-agent-launch' import { activateAndRevealWorktree } from '@/lib/worktree-activation' import { buildFixBrokenChecksPrompt, getBrokenChecks } from '@/components/pr-checks-fix-prompt' +import type { GitBranchChangeEntry, GitDiffResult } from '../../../shared/git-diff-compare-types' +import type { PRCheckDetail } from '../../../shared/github/check-types' import type { + GitHubIssueTimelineItem, + GitHubIssueTimelineTarget, + PRComment +} from '../../../shared/github/comment-types' +import type { + GitHubAssignableUser, GitHubOwnerRepo, GitHubPRFile, GitHubPRFileContents, GitHubPRFileViewedState, - GitHubWorkItem, - GitHubWorkItemDetails, - GitHubIssueTimelineItem, - GitHubIssueTimelineTarget, - GitHubAssignableUser, - GitHubPRMergeMethod, - GitBranchChangeEntry, - GitDiffResult, - PRCheckDetail, - PRComment -} from '../../../shared/types' + GitHubPRMergeMethod +} from '../../../shared/github/pull-request-types' +import type { GitHubWorkItem, GitHubWorkItemDetails } from '../../../shared/github/work-item-types' import { getTaskSourceCacheScope, getTaskSourceRuntimeSettings, diff --git a/src/renderer/src/components/GitLabItemDialog.tsx b/src/renderer/src/components/GitLabItemDialog.tsx index 4d3b0f0d722..7dcf2fba06d 100644 --- a/src/renderer/src/components/GitLabItemDialog.tsx +++ b/src/renderer/src/components/GitLabItemDialog.tsx @@ -44,12 +44,12 @@ import { import { useAppStore } from '@/store' import type { GitLabAssignableUser, - GitLabPipelineJob, GitLabMRUpdate, + GitLabPipelineJob, GitLabWorkItem, GitLabWorkItemDetails, MRComment -} from '../../../shared/types' +} from '../../../shared/gitlab-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/JiraIssueWorkspace.tsx b/src/renderer/src/components/JiraIssueWorkspace.tsx index 969765c4dca..b1f2078900c 100644 --- a/src/renderer/src/components/JiraIssueWorkspace.tsx +++ b/src/renderer/src/components/JiraIssueWorkspace.tsx @@ -45,7 +45,7 @@ import type { JiraPriority, JiraTransition, JiraUser -} from '../../../shared/types' +} from '../../../shared/jira-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { translate } from '@/i18n/i18n' import { formatUiRelativeTimeFromDate } from '@/i18n/relative-time-format' diff --git a/src/renderer/src/components/Landing.tsx b/src/renderer/src/components/Landing.tsx index bcf5c66ec1f..701863a1ef9 100644 --- a/src/renderer/src/components/Landing.tsx +++ b/src/renderer/src/components/Landing.tsx @@ -3,7 +3,7 @@ import { AlertTriangle, ExternalLink, FolderPlus, GitBranchPlus, Star, X } from import { cn } from '../lib/utils' import { useAppStore } from '../store' import { isGitRepoKind } from '../../../shared/repo-kind' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import { dismissPreflightIssue, githubProjectKeys, diff --git a/src/renderer/src/components/LinearIssueTextEditor.tsx b/src/renderer/src/components/LinearIssueTextEditor.tsx index e00f508a7ac..42db6a5c4c8 100644 --- a/src/renderer/src/components/LinearIssueTextEditor.tsx +++ b/src/renderer/src/components/LinearIssueTextEditor.tsx @@ -8,7 +8,7 @@ import { cn } from '@/lib/utils' import { useAppStore } from '@/store' import { getScreenSubmitShortcutLabel, isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' import { linearUpdateIssue } from '@/runtime/runtime-linear-client' -import type { LinearIssue } from '../../../shared/types' +import type { LinearIssue } from '../../../shared/linear/issue-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { getLinearIssueTextSavePlan, diff --git a/src/renderer/src/components/LinearIssueWorkspace.tsx b/src/renderer/src/components/LinearIssueWorkspace.tsx index 690f2d42655..7c92ca1d618 100644 --- a/src/renderer/src/components/LinearIssueWorkspace.tsx +++ b/src/renderer/src/components/LinearIssueWorkspace.tsx @@ -70,9 +70,9 @@ import { import type { LinearComment, LinearIssue, - LinearIssueChildSummary, - LinearProjectSummary -} from '../../../shared/types' + LinearIssueChildSummary +} from '../../../shared/linear/issue-types' +import type { LinearProjectSummary } from '../../../shared/linear/project-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/LinearItemDrawer.tsx b/src/renderer/src/components/LinearItemDrawer.tsx index 82053896d36..5cbd0cc2a3b 100644 --- a/src/renderer/src/components/LinearItemDrawer.tsx +++ b/src/renderer/src/components/LinearItemDrawer.tsx @@ -57,7 +57,7 @@ import { getLinearStatePillStyle } from '@/components/linear-state-pill-style' import { LinearPriorityIcon } from '@/components/linear-priority-icon' -import type { LinearIssue, LinearComment } from '../../../shared/types' +import type { LinearComment, LinearIssue } from '../../../shared/linear/issue-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { linearAddIssueComment, diff --git a/src/renderer/src/components/LinuxPackageInstallRecoveryCard.test.tsx b/src/renderer/src/components/LinuxPackageInstallRecoveryCard.test.tsx index e0685b55fbe..0c44cf153e5 100644 --- a/src/renderer/src/components/LinuxPackageInstallRecoveryCard.test.tsx +++ b/src/renderer/src/components/LinuxPackageInstallRecoveryCard.test.tsx @@ -2,7 +2,7 @@ import { act, cleanup, fireEvent, render, screen, type RenderResult } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { LinuxPackageInstallRecovery } from '../../../shared/types' +import type { LinuxPackageInstallRecovery } from '../../../shared/update-status-types' import { LinuxPackageInstallRecoveryCard } from './LinuxPackageInstallRecoveryCard' const RELEASE_URL = 'https://github.com/stablyai/orca/releases/tag/v1.4.200' diff --git a/src/renderer/src/components/LinuxPackageInstallRecoveryCard.tsx b/src/renderer/src/components/LinuxPackageInstallRecoveryCard.tsx index 3d89b7ee6b7..69a0f73d627 100644 --- a/src/renderer/src/components/LinuxPackageInstallRecoveryCard.tsx +++ b/src/renderer/src/components/LinuxPackageInstallRecoveryCard.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import type { LinuxPackageInstallInstructions, LinuxPackageInstallRecovery -} from '../../../shared/types' +} from '../../../shared/update-status-types' import { UpdateErrorCardContent } from './UpdateErrorCardContent' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/NewWorkspaceComposerCard.tsx b/src/renderer/src/components/NewWorkspaceComposerCard.tsx index b9683150d17..4606a625c07 100644 --- a/src/renderer/src/components/NewWorkspaceComposerCard.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerCard.tsx @@ -34,16 +34,13 @@ import { } from '@/lib/text-control-paste' import { getScreenSubmitModifierLabel } from '@/lib/screen-submit-shortcut' import { useContextualTour } from '@/components/contextual-tours/use-contextual-tour' -import type { - GitHubWorkItem, - GitLabWorkItem, - JiraIssue, - LinearIssue, - SetupAgentStartupPolicy, - OrcaHooks, - SparsePreset, - TuiAgent -} from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import type { GitLabWorkItem } from '../../../shared/gitlab-types' +import type { JiraIssue } from '../../../shared/jira-types' +import type { LinearIssue } from '../../../shared/linear/issue-types' +import type { OrcaHooks, SetupAgentStartupPolicy } from '../../../shared/orca-yaml-hook-types' +import type { TuiAgent } from '../../../shared/tui-agent' +import type { SparsePreset } from '../../../shared/worktree/create-types' import SparseCheckoutPresetSelect from '@/components/sparse/SparseCheckoutPresetSelect' import SmartWorkspaceNameField, { type SmartWorkspaceNameSelection diff --git a/src/renderer/src/components/NewWorkspaceComposerModal.tsx b/src/renderer/src/components/NewWorkspaceComposerModal.tsx index aa46a10f7bf..77b9cf64e52 100644 --- a/src/renderer/src/components/NewWorkspaceComposerModal.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerModal.tsx @@ -19,12 +19,10 @@ import { import type { LinkedWorkItemSummary } from '@/lib/new-workspace' import { shouldAllowComposerEnterSubmitTarget } from '@/lib/new-workspace-enter-guard' import { isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' -import type { - GitHubWorkItem, - TuiAgent, - WorkspaceCreateTelemetrySource, - WorkspaceStatus -} from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import type { TuiAgent } from '../../../shared/tui-agent' +import type { WorkspaceSource as WorkspaceCreateTelemetrySource } from '../../../shared/workspace-source' +import type { WorkspaceStatus } from '../../../shared/worktree/types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { translate } from '@/i18n/i18n' import { getWorkspaceComposerInitialFocusTarget } from '@/lib/workspace-composer-initial-focus' diff --git a/src/renderer/src/components/PullRequestPage.tsx b/src/renderer/src/components/PullRequestPage.tsx index f47b452b573..e495583ab4d 100644 --- a/src/renderer/src/components/PullRequestPage.tsx +++ b/src/renderer/src/components/PullRequestPage.tsx @@ -193,20 +193,18 @@ import { saveSourceControlActionRecipe, type SourceControlAiWriteTarget } from '../../../shared/source-control-ai-recipe-save' +import type { GitBranchChangeEntry, GitDiffResult } from '../../../shared/git-diff-compare-types' +import type { PRCheckDetail } from '../../../shared/github/check-types' +import type { PRComment } from '../../../shared/github/comment-types' import type { + GitHubAssignableUser, GitHubOwnerRepo, GitHubPRFile, GitHubPRFileContents, GitHubPRFileViewedState, - GitHubWorkItem, - GitHubWorkItemDetails, - GitHubAssignableUser, - GitHubPRMergeMethod, - GitBranchChangeEntry, - GitDiffResult, - PRCheckDetail, - PRComment -} from '../../../shared/types' + GitHubPRMergeMethod +} from '../../../shared/github/pull-request-types' +import type { GitHubWorkItem, GitHubWorkItemDetails } from '../../../shared/github/work-item-types' import { getTaskSourceCacheScope, getTaskSourceRuntimeSettings, diff --git a/src/renderer/src/components/TaskPage.tsx b/src/renderer/src/components/TaskPage.tsx index d7cfc3e9e5e..cf6e884b7b0 100644 --- a/src/renderer/src/components/TaskPage.tsx +++ b/src/renderer/src/components/TaskPage.tsx @@ -351,34 +351,38 @@ import { resolveGitHubPRMergeMethods } from '../../../shared/github/pull-request-merge-methods' import type { - GitHubOwnerRepo, GitHubAssignableUser, - GitHubPRMergeMethod, - GitHubIssueUpdate, - GitHubWorkItem, - GitLabTodo, - GitLabWorkItem, + GitHubOwnerRepo, + GitHubPRMergeMethod +} from '../../../shared/github/pull-request-types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import type { GitLabProjectRef, GitLabTodo, GitLabWorkItem } from '../../../shared/gitlab-types' +import type { GitHubIssueUpdate } from '../../../shared/issue-mutation-types' +import type { JiraCreateField, - LinearCollectionResult, - LinearCustomViewModel, - LinearCustomViewSummary, JiraIssue, JiraIssueType, - JiraProject, - JiraProjectStatusOrder, JiraPriority, - LinearIssue, + JiraProject, + JiraProjectStatusOrder +} from '../../../shared/jira-types' +import type { LinearIssue } from '../../../shared/linear/issue-types' +import type { + LinearCustomViewModel, + LinearCustomViewSummary, LinearProjectDetail, - LinearProjectSummary, + LinearProjectSummary +} from '../../../shared/linear/project-types' +import type { + LinearCollectionResult, LinearTeam, - LinearWorkspaceSelection, LinearWorkflowState, - Repo, - TaskProvider, - TaskViewPresetId -} from '../../../shared/types' + LinearWorkspaceSelection +} from '../../../shared/linear/workspace-types' +import type { Repo } from '../../../shared/repo-types' +import type { TaskProvider } from '../../../shared/task-providers' +import type { TaskViewPresetId } from '../../../shared/ui-chrome-types' import type { PreflightStatus } from '../../../preload/api-types' -import type { GitLabProjectRef } from '../../../shared/gitlab-types' import { LINEAR_ISSUE_LIST_MAX, clampLinearIssueListLimit diff --git a/src/renderer/src/components/Terminal.tsx b/src/renderer/src/components/Terminal.tsx index d555a92d22c..d6878135189 100644 --- a/src/renderer/src/components/Terminal.tsx +++ b/src/renderer/src/components/Terminal.tsx @@ -38,7 +38,9 @@ import { import { isIntentionalAppRestartInProgress } from '@/lib/updater-beforeunload' import { preventUnloadAndScheduleShutdownCheckpointReset } from '@/lib/shutdown-checkpoint-guard' import EditorAutosaveController from './editor/EditorAutosaveController' -import type { Tab, TabGroupLayoutNode, TerminalTab, TuiAgent } from '../../../shared/types' +import type { Tab, TabGroupLayoutNode } from '../../../shared/tab-types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' import { hasFeatureInteraction } from '../../../shared/feature-interactions' import BrowserPane from './browser-pane/BrowserPane' import { RetainedBrowserPaneOverlayLayer } from './browser-pane/BrowserPaneOverlayLayer' diff --git a/src/renderer/src/components/UpdateCard.error-card.test.tsx b/src/renderer/src/components/UpdateCard.error-card.test.tsx index 4e4aec4d2b5..4cb297b7ca4 100644 --- a/src/renderer/src/components/UpdateCard.error-card.test.tsx +++ b/src/renderer/src/components/UpdateCard.error-card.test.tsx @@ -2,7 +2,7 @@ import { act, cleanup, fireEvent, render, screen, type RenderResult } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { LinuxPackageInstallRecovery } from '../../../shared/types' +import type { LinuxPackageInstallRecovery } from '../../../shared/update-status-types' import { useAppStore } from '../store' import { UpdateCard } from './UpdateCard' diff --git a/src/renderer/src/components/UpdateCard.test.ts b/src/renderer/src/components/UpdateCard.test.ts index 6eed56fad3e..d5103ed30fc 100644 --- a/src/renderer/src/components/UpdateCard.test.ts +++ b/src/renderer/src/components/UpdateCard.test.ts @@ -1,7 +1,7 @@ import { createStore, type StoreApi } from 'zustand/vanilla' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultUIState } from '../../../shared/constants' -import type { ChangelogData, UpdateStatus } from '../../../shared/types' +import type { ChangelogData, UpdateStatus } from '../../../shared/update-status-types' import { createUISlice } from '../store/slices/ui' import type { AppState } from '../store/types' import { isHttp2ProtocolError } from './UpdateCard' diff --git a/src/renderer/src/components/UpdateCard.tsx b/src/renderer/src/components/UpdateCard.tsx index b47e9fedd72..bb2b2d194c3 100644 --- a/src/renderer/src/components/UpdateCard.tsx +++ b/src/renderer/src/components/UpdateCard.tsx @@ -6,7 +6,7 @@ import { Card } from './ui/card' import { Button } from './ui/button' import { Progress } from './ui/progress' import { AlertCircle, Check, Loader2, Minus, X } from 'lucide-react' -import type { ChangelogData } from '../../../shared/types' +import type { ChangelogData } from '../../../shared/update-status-types' import { UpdateErrorCardContent, type UpdateErrorCardModel } from './UpdateErrorCardContent' import { LinuxPackageInstallRecoveryCard } from './LinuxPackageInstallRecoveryCard' import { diff --git a/src/renderer/src/components/WorktreeJumpPalette.linear-url.test.tsx b/src/renderer/src/components/WorktreeJumpPalette.linear-url.test.tsx index 709d57777c7..0637cad8d42 100644 --- a/src/renderer/src/components/WorktreeJumpPalette.linear-url.test.tsx +++ b/src/renderer/src/components/WorktreeJumpPalette.linear-url.test.tsx @@ -7,7 +7,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as ReactI18Next from 'react-i18next' import { useAppStore } from '@/store' import type { AppState } from '@/store/types' -import type { GitHubWorkItem, LinearIssue, Repo } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import type { LinearIssue } from '../../../shared/linear/issue-types' +import type { Repo } from '../../../shared/repo-types' import { projectHostSetupProjectionFromRepos } from '../../../shared/project-host-setup-projection' import { resolveWorkspaceCreationTarget } from '@/lib/project-host-workspace-target' import { WORKTREE_PALETTE_QUERY_MAX_BYTES } from '@/lib/worktree-palette-query-bounds' diff --git a/src/renderer/src/components/WorktreeJumpPalette.tsx b/src/renderer/src/components/WorktreeJumpPalette.tsx index 4f29ebb5c24..f3a393589b6 100644 --- a/src/renderer/src/components/WorktreeJumpPalette.tsx +++ b/src/renderer/src/components/WorktreeJumpPalette.tsx @@ -51,9 +51,9 @@ import { isAutomationGeneratedWorkspace, isCliCreatedWorkspace, isDetachedHeadWorkspace, - isDefaultBranchWorkspace, isSleepingSweepExemptWorkspace } from '@/components/sidebar/visible-worktrees' +import { isDefaultBranchWorkspace } from '@/components/sidebar/default-branch-workspace' import { EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT, getPairedDeviceIdsByEnvironment, @@ -200,7 +200,10 @@ import { getSettingsFocusedExecutionHostId, isRuntimeOwnedSshTargetId } from '../../../shared/execution-host' -import type { GitHubWorkItem, LinearIssue, TerminalTab, Worktree } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import type { LinearIssue } from '../../../shared/linear/issue-types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { Worktree } from '../../../shared/worktree/types' import { isGitRepoKind } from '../../../shared/repo-kind' import { buildTaskSourceContextFromRepo, diff --git a/src/renderer/src/components/activity/ActivityPrototypePage.test.ts b/src/renderer/src/components/activity/ActivityPrototypePage.test.ts index 05aec890947..e20c9455261 100644 --- a/src/renderer/src/components/activity/ActivityPrototypePage.test.ts +++ b/src/renderer/src/components/activity/ActivityPrototypePage.test.ts @@ -6,7 +6,9 @@ import { type AgentStatusEntry } from '../../../../shared/agent-status-types' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' -import type { Repo, TerminalTab, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import { formatAgentTypeLabel } from '@/lib/agent-status' import type { RetainedAgentEntry } from '@/store/slices/agent-status' import { diff --git a/src/renderer/src/components/activity/ActivityPrototypePage.tsx b/src/renderer/src/components/activity/ActivityPrototypePage.tsx index 8c8058fbeeb..6157c796d71 100644 --- a/src/renderer/src/components/activity/ActivityPrototypePage.tsx +++ b/src/renderer/src/components/activity/ActivityPrototypePage.tsx @@ -61,7 +61,9 @@ import { type ActivityPortalReadinessLatch, type ActivityPortalReadinessStatus } from './activity-portal-readiness-oscillation' -import type { Repo, TerminalTab, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' import { AGENT_STATUS_STALE_AFTER_MS, diff --git a/src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.tsx b/src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.tsx index a668162753a..aa752c4b50f 100644 --- a/src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.tsx +++ b/src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.tsx @@ -31,7 +31,7 @@ import { } from '@/lib/launch-agent-session-continuation' import { useAppStore } from '@/store' import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { chooseInitialContinuationAgent } from './agent-session-continuation-selection' type AgentSessionContinuationDialogProps = { diff --git a/src/renderer/src/components/agent-session-continuation/agent-session-continuation-selection.ts b/src/renderer/src/components/agent-session-continuation/agent-session-continuation-selection.ts index 3a7617caacd..8297a104d3e 100644 --- a/src/renderer/src/components/agent-session-continuation/agent-session-continuation-selection.ts +++ b/src/renderer/src/components/agent-session-continuation/agent-session-continuation-selection.ts @@ -1,5 +1,5 @@ import { isTuiAgent } from '../../../../shared/tui-agent-config' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' export function chooseInitialContinuationAgent(args: { availableAgents: TuiAgent[] diff --git a/src/renderer/src/components/agent/AgentCombobox.test.tsx b/src/renderer/src/components/agent/AgentCombobox.test.tsx index 08b8b2d37c1..629aea0ee15 100644 --- a/src/renderer/src/components/agent/AgentCombobox.test.tsx +++ b/src/renderer/src/components/agent/AgentCombobox.test.tsx @@ -3,7 +3,7 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { renderToStaticMarkup } from 'react-dom/server' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { AGENT_CATALOG, AgentIcon } from '@/lib/agent-catalog' import { AGENT_FAVICON_ASSETS } from '@/lib/agent-favicon-assets' import AgentCombobox from './AgentCombobox' diff --git a/src/renderer/src/components/agent/AgentCombobox.tsx b/src/renderer/src/components/agent/AgentCombobox.tsx index a4fa9202543..1c2ef729db9 100644 --- a/src/renderer/src/components/agent/AgentCombobox.tsx +++ b/src/renderer/src/components/agent/AgentCombobox.tsx @@ -22,7 +22,7 @@ import { searchAgentPickerEntries } from '@/lib/agent-picker-search' import { cn } from '@/lib/utils' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { createAgentComboboxCommandState, resolveAgentComboboxCommandState, diff --git a/src/renderer/src/components/agent/AgentSettingsDialog.test.tsx b/src/renderer/src/components/agent/AgentSettingsDialog.test.tsx index 9c53f03f7b7..ee45c71c30e 100644 --- a/src/renderer/src/components/agent/AgentSettingsDialog.test.tsx +++ b/src/renderer/src/components/agent/AgentSettingsDialog.test.tsx @@ -4,7 +4,7 @@ import { act, createElement, type ComponentProps, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../../shared/constants' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { AgentsPane } from '@/components/settings/AgentsPane' import { resetWindowsTerminalCapabilitiesForTests } from '@/lib/windows-terminal-capabilities' diff --git a/src/renderer/src/components/automations/AutomationEditorDialog.tsx b/src/renderer/src/components/automations/AutomationEditorDialog.tsx index 6ac0c7bc66c..569d1a8a2ea 100644 --- a/src/renderer/src/components/automations/AutomationEditorDialog.tsx +++ b/src/renderer/src/components/automations/AutomationEditorDialog.tsx @@ -6,15 +6,13 @@ import type { AutomationSchedulePreset, AutomationWorkspaceMode } from '../../../../shared/automations-types' -import type { - GlobalSettings, - OrcaHooks, - ProjectHostSetup, - Repo, - SetupDecision, - TuiAgent, - Worktree -} from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { OrcaHooks } from '../../../../shared/orca-yaml-hook-types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' +import type { TuiAgent } from '../../../../shared/tui-agent' +import type { SetupDecision } from '../../../../shared/worktree/create-types' +import type { Worktree } from '../../../../shared/worktree/types' import { isValidAutomationCronSchedule, isValidAutomationSchedule diff --git a/src/renderer/src/components/automations/AutomationEditorDialogFooter.tsx b/src/renderer/src/components/automations/AutomationEditorDialogFooter.tsx index 160dac72d8b..2c68beee9b3 100644 --- a/src/renderer/src/components/automations/AutomationEditorDialogFooter.tsx +++ b/src/renderer/src/components/automations/AutomationEditorDialogFooter.tsx @@ -7,13 +7,11 @@ import AgentCombobox from '@/components/agent/AgentCombobox' import { cn } from '@/lib/utils' import { translate } from '@/i18n/i18n' import type { AutomationWorkspaceMode } from '../../../../shared/automations-types' -import type { - GlobalSettings, - OrcaHooks, - ProjectHostSetup, - Repo, - Worktree -} from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { OrcaHooks } from '../../../../shared/orca-yaml-hook-types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { AgentCatalogEntry } from '@/lib/agent-catalog' import { Field } from './automation-page-parts' import { AutomationMissedRunGraceField } from './AutomationMissedRunGraceField' diff --git a/src/renderer/src/components/automations/AutomationListLocalRows.tsx b/src/renderer/src/components/automations/AutomationListLocalRows.tsx index 93019241d63..cde61bd6201 100644 --- a/src/renderer/src/components/automations/AutomationListLocalRows.tsx +++ b/src/renderer/src/components/automations/AutomationListLocalRows.tsx @@ -27,7 +27,9 @@ import { getRepoExecutionHostId } from '../../../../shared/execution-host' import type { SshConnectionState } from '../../../../shared/ssh-types' -import type { ProjectHostSetup, Repo, Worktree } from '../../../../shared/types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { RuntimeStatus } from '../../../../shared/runtime-types' import type { TaskSourceHostAvailability } from '../task-source-context-summary' import type { AutomationHostTarget } from './automation-host-client' diff --git a/src/renderer/src/components/automations/AutomationProjectCombobox.tsx b/src/renderer/src/components/automations/AutomationProjectCombobox.tsx index 65d0176bf29..73fc44bddb5 100644 --- a/src/renderer/src/components/automations/AutomationProjectCombobox.tsx +++ b/src/renderer/src/components/automations/AutomationProjectCombobox.tsx @@ -11,7 +11,7 @@ import { isRepoSearchQueryTooLarge, searchRepos } from '@/lib/repo-search' import { cn } from '@/lib/utils' import { useMountedRef } from '@/hooks/useMountedRef' import { translate } from '@/i18n/i18n' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { getAutomationProjectGroupForRepo, getAutomationProjectGroups, diff --git a/src/renderer/src/components/automations/AutomationRunHistory.tsx b/src/renderer/src/components/automations/AutomationRunHistory.tsx index 777eaff3fa2..323accb7259 100644 --- a/src/renderer/src/components/automations/AutomationRunHistory.tsx +++ b/src/renderer/src/components/automations/AutomationRunHistory.tsx @@ -2,7 +2,7 @@ import React, { useMemo, useState } from 'react' import { Badge } from '@/components/ui/badge' import { cn } from '@/lib/utils' import type { AutomationRun } from '../../../../shared/automations-types' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { formatAutomationDateTime, getAutomationRunStatusLabel, diff --git a/src/renderer/src/components/automations/AutomationSetupDecisionField.tsx b/src/renderer/src/components/automations/AutomationSetupDecisionField.tsx index 2731dba8164..56a05f881b4 100644 --- a/src/renderer/src/components/automations/AutomationSetupDecisionField.tsx +++ b/src/renderer/src/components/automations/AutomationSetupDecisionField.tsx @@ -5,7 +5,9 @@ import { cn } from '@/lib/utils' import { translate } from '@/i18n/i18n' import { getVisibleAutomationSetupDecision } from './automation-setup-decision' import type { AutomationCreateTarget, AutomationDraft } from './AutomationEditorDialog' -import type { OrcaHooks, ProjectHostSetup, Repo } from '../../../../shared/types' +import type { OrcaHooks } from '../../../../shared/orca-yaml-hook-types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' type AutomationSetupDecisionFieldProps = { createTarget: AutomationCreateTarget diff --git a/src/renderer/src/components/automations/AutomationsDetailPane.tsx b/src/renderer/src/components/automations/AutomationsDetailPane.tsx index 48ffceba88f..4de2a9ed795 100644 --- a/src/renderer/src/components/automations/AutomationsDetailPane.tsx +++ b/src/renderer/src/components/automations/AutomationsDetailPane.tsx @@ -11,7 +11,7 @@ import type { ExternalAutomationRun, AutomationRun } from '../../../../shared/automations-types' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import CommentMarkdown from '@/components/sidebar/CommentMarkdown' import { AutomationDetail } from './AutomationDetail' import { HermesCronOutputView } from './HermesCronOutputView' diff --git a/src/renderer/src/components/automations/AutomationsListPanel.tsx b/src/renderer/src/components/automations/AutomationsListPanel.tsx index a792839885c..b946d846b05 100644 --- a/src/renderer/src/components/automations/AutomationsListPanel.tsx +++ b/src/renderer/src/components/automations/AutomationsListPanel.tsx @@ -10,7 +10,9 @@ import type { ExternalAutomationManager } from '../../../../shared/automations-types' import type { SshConnectionState } from '../../../../shared/ssh-types' -import type { ProjectHostSetup, Repo, Worktree } from '../../../../shared/types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { RuntimeStatus } from '../../../../shared/runtime-types' import type { TaskSourceHostAvailability } from '../task-source-context-summary' import type { AutomationHostTarget } from './automation-host-client' diff --git a/src/renderer/src/components/automations/AutomationsPage.tsx b/src/renderer/src/components/automations/AutomationsPage.tsx index a17d79e4197..4445cf21eeb 100644 --- a/src/renderer/src/components/automations/AutomationsPage.tsx +++ b/src/renderer/src/components/automations/AutomationsPage.tsx @@ -28,7 +28,8 @@ import { import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-overrides' import type { PreflightStatus } from '../../../../preload/api-types' import type { TaskSourceContext } from '../../../../shared/task-source-context' -import type { OrcaHooks, Repo } from '../../../../shared/types' +import type { OrcaHooks } from '../../../../shared/orca-yaml-hook-types' +import type { Repo } from '../../../../shared/repo-types' import { getWorktreePathBasenameFromId } from '../../../../shared/worktree/id' import { buildAutomationRrule, diff --git a/src/renderer/src/components/automations/CreateFromPicker.test.tsx b/src/renderer/src/components/automations/CreateFromPicker.test.tsx index 46cb4ded084..3aaeb3f2a7f 100644 --- a/src/renderer/src/components/automations/CreateFromPicker.test.tsx +++ b/src/renderer/src/components/automations/CreateFromPicker.test.tsx @@ -3,7 +3,7 @@ import React, { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { CreateFromPicker } from './CreateFromPicker' import { getRuntimeRepoBaseRefDefault, diff --git a/src/renderer/src/components/automations/CreateFromPicker.tsx b/src/renderer/src/components/automations/CreateFromPicker.tsx index 083ba015da7..a392627ee18 100644 --- a/src/renderer/src/components/automations/CreateFromPicker.tsx +++ b/src/renderer/src/components/automations/CreateFromPicker.tsx @@ -11,7 +11,8 @@ import { } from '@/components/ui/command' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { cn } from '@/lib/utils' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { useAppStore } from '@/store' import { getRuntimeEnvironmentIdForRepo } from '@/lib/repo-runtime-owner' import { diff --git a/src/renderer/src/components/automations/WorkspaceCombobox.tsx b/src/renderer/src/components/automations/WorkspaceCombobox.tsx index 0951a2e23b2..ffad3e6ccbb 100644 --- a/src/renderer/src/components/automations/WorkspaceCombobox.tsx +++ b/src/renderer/src/components/automations/WorkspaceCombobox.tsx @@ -10,7 +10,7 @@ import { } from '@/components/ui/command' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { cn } from '@/lib/utils' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { translate } from '@/i18n/i18n' export function WorkspaceCombobox({ diff --git a/src/renderer/src/components/automations/automation-draft-model.ts b/src/renderer/src/components/automations/automation-draft-model.ts index 2abf4511388..827f84543a4 100644 --- a/src/renderer/src/components/automations/automation-draft-model.ts +++ b/src/renderer/src/components/automations/automation-draft-model.ts @@ -1,7 +1,7 @@ import { getAgentCatalog } from '@/lib/agent-catalog' import type { AutomationPrecheck } from '../../../../shared/automations-types' import { buildAutomationCronSchedule } from '../../../../shared/automation-schedules' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import type { AutomationDraft } from './AutomationEditorDialog' export const AUTOMATION_DEFAULT_TIME = '09:00' diff --git a/src/renderer/src/components/automations/automation-host-client.ts b/src/renderer/src/components/automations/automation-host-client.ts index 36e1ff0560a..b5e28fcb6b1 100644 --- a/src/renderer/src/components/automations/automation-host-client.ts +++ b/src/renderer/src/components/automations/automation-host-client.ts @@ -6,7 +6,7 @@ import type { AutomationUpdateInput } from '../../../../shared/automations-types' import { parseExecutionHostId } from '../../../../shared/execution-host' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' type RuntimeAutomationCreateInput = Omit< AutomationCreateInput, diff --git a/src/renderer/src/components/automations/automation-project-groups.test.ts b/src/renderer/src/components/automations/automation-project-groups.test.ts index 4423c381505..b8039169496 100644 --- a/src/renderer/src/components/automations/automation-project-groups.test.ts +++ b/src/renderer/src/components/automations/automation-project-groups.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { getAutomationProjectGroupForRepo, getAutomationProjectGroups, diff --git a/src/renderer/src/components/automations/automation-project-groups.ts b/src/renderer/src/components/automations/automation-project-groups.ts index eacea1f8c41..4dd4c2a6c39 100644 --- a/src/renderer/src/components/automations/automation-project-groups.ts +++ b/src/renderer/src/components/automations/automation-project-groups.ts @@ -1,6 +1,6 @@ import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' import { getProjectIdentityKey } from '../../../../shared/project-host-setup-projection' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' export type AutomationProjectGroup = { projectKey: string diff --git a/src/renderer/src/components/automations/automation-run-context.test.ts b/src/renderer/src/components/automations/automation-run-context.test.ts index 345b03d56f7..b2f1dc2f4ae 100644 --- a/src/renderer/src/components/automations/automation-run-context.test.ts +++ b/src/renderer/src/components/automations/automation-run-context.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { ProjectHostSetup, Repo } from '../../../../shared/types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { buildAutomationRunContextForRepo } from './automation-run-context' function repo(id: string, path = `/repos/${id}`): Repo { diff --git a/src/renderer/src/components/automations/automation-run-context.ts b/src/renderer/src/components/automations/automation-run-context.ts index 65bad47c9f3..49894f78159 100644 --- a/src/renderer/src/components/automations/automation-run-context.ts +++ b/src/renderer/src/components/automations/automation-run-context.ts @@ -2,7 +2,8 @@ import { buildWorkspaceRunContext, type WorkspaceRunContext } from '../../../../shared/task-source-context' -import type { ProjectHostSetup, Repo } from '../../../../shared/types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' export function buildAutomationRunContextForRepo(args: { repoId: string diff --git a/src/renderer/src/components/automations/automation-run-open-target.ts b/src/renderer/src/components/automations/automation-run-open-target.ts index 9ee8087be85..d3de0eb7fc7 100644 --- a/src/renderer/src/components/automations/automation-run-open-target.ts +++ b/src/renderer/src/components/automations/automation-run-open-target.ts @@ -1,6 +1,9 @@ import type { AutomationRun } from '../../../../shared/automations-types' import { parsePaneKey } from '../../../../shared/stable-pane-id' -import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode } from '../../../../shared/types' +import type { + TerminalLayoutSnapshot, + TerminalPaneLayoutNode +} from '../../../../shared/terminal-tab-types' export type AutomationRunPaneTarget = { tabId: string diff --git a/src/renderer/src/components/automations/automation-run-workspace-display.ts b/src/renderer/src/components/automations/automation-run-workspace-display.ts index 7d7a5e81be3..5ea6d23230e 100644 --- a/src/renderer/src/components/automations/automation-run-workspace-display.ts +++ b/src/renderer/src/components/automations/automation-run-workspace-display.ts @@ -1,5 +1,5 @@ import type { AutomationRun } from '../../../../shared/automations-types' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' export type AutomationRunWorkspaceDisplay = { rowLabel: string diff --git a/src/renderer/src/components/automations/automation-setup-decision.test.ts b/src/renderer/src/components/automations/automation-setup-decision.test.ts index b46d76da980..87d1bf19a14 100644 --- a/src/renderer/src/components/automations/automation-setup-decision.test.ts +++ b/src/renderer/src/components/automations/automation-setup-decision.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { ProjectHostSetup, Repo } from '../../../../shared/types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { getAutomationSetupDecisionDraftValue, getVisibleAutomationSetupDecision, diff --git a/src/renderer/src/components/automations/automation-setup-decision.ts b/src/renderer/src/components/automations/automation-setup-decision.ts index f565e439f34..9df3f0298fc 100644 --- a/src/renderer/src/components/automations/automation-setup-decision.ts +++ b/src/renderer/src/components/automations/automation-setup-decision.ts @@ -1,11 +1,8 @@ import type { AutomationWorkspaceMode } from '../../../../shared/automations-types' -import type { - OrcaHooks, - ProjectHostSetup, - Repo, - SetupDecision, - SetupRunPolicy -} from '../../../../shared/types' +import type { OrcaHooks, SetupRunPolicy } from '../../../../shared/orca-yaml-hook-types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' +import type { SetupDecision } from '../../../../shared/worktree/create-types' import { getSetupConfig } from '@/lib/new-workspace' type AutomationSetupSource = { diff --git a/src/renderer/src/components/automations/automation-target-availability.test.ts b/src/renderer/src/components/automations/automation-target-availability.test.ts index 8c9ab713e88..2b94251144a 100644 --- a/src/renderer/src/components/automations/automation-target-availability.test.ts +++ b/src/renderer/src/components/automations/automation-target-availability.test.ts @@ -1,7 +1,9 @@ import { describe, expect, it } from 'vitest' import type { Automation } from '../../../../shared/automations-types' import type { RuntimeStatus } from '../../../../shared/runtime-types' -import type { ProjectHostSetup, Repo, Worktree } from '../../../../shared/types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getAutomationTargetAvailability } from './automation-target-availability' function makeAutomation(overrides: Partial = {}): Automation { diff --git a/src/renderer/src/components/automations/automation-target-availability.ts b/src/renderer/src/components/automations/automation-target-availability.ts index ca7c166b5a8..40a6b3dc8f6 100644 --- a/src/renderer/src/components/automations/automation-target-availability.ts +++ b/src/renderer/src/components/automations/automation-target-availability.ts @@ -12,7 +12,9 @@ import type { AutomationHostTarget } from './automation-host-client' import type { SshConnectionState } from '../../../../shared/ssh-types' import type { TaskSourceContext } from '../../../../shared/task-source-context' import type { RuntimeStatus } from '../../../../shared/runtime-types' -import type { ProjectHostSetup, Repo, Worktree } from '../../../../shared/types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { TaskSourceHostAvailability } from '../task-source-context-summary' export type AutomationTargetAvailability = diff --git a/src/renderer/src/components/automations/automation-templates.ts b/src/renderer/src/components/automations/automation-templates.ts index 5f4af77b7e4..b4abe3017df 100644 --- a/src/renderer/src/components/automations/automation-templates.ts +++ b/src/renderer/src/components/automations/automation-templates.ts @@ -1,5 +1,5 @@ import type { AutomationSchedulePreset } from '../../../../shared/automations-types' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { translate } from '@/i18n/i18n' import { createLocalizedCatalog } from '@/i18n/localized-catalog' diff --git a/src/renderer/src/components/automations/use-automation-list-search.ts b/src/renderer/src/components/automations/use-automation-list-search.ts index 235c08cdb1e..95ac89262a0 100644 --- a/src/renderer/src/components/automations/use-automation-list-search.ts +++ b/src/renderer/src/components/automations/use-automation-list-search.ts @@ -1,7 +1,7 @@ import { useDeferredValue, useEffect, useMemo } from 'react' import type { Automation } from '../../../../shared/automations-types' import { getAutomationRunRepoId } from '../../../../shared/automation-run-identity' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { automationListSearchIndexMatches, buildAutomationListSearchIndex, diff --git a/src/renderer/src/components/browser-pane/BrowserAddressBar.test.tsx b/src/renderer/src/components/browser-pane/BrowserAddressBar.test.tsx index 0cf202ff2d8..2f147fc98cc 100644 --- a/src/renderer/src/components/browser-pane/BrowserAddressBar.test.tsx +++ b/src/renderer/src/components/browser-pane/BrowserAddressBar.test.tsx @@ -3,7 +3,7 @@ import { act, type ReactNode, useRef, useState } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { BrowserHistoryEntry } from '../../../../shared/types' +import type { BrowserHistoryEntry } from '../../../../shared/browser-workspace-types' import BrowserAddressBar from './BrowserAddressBar' const mocks = vi.hoisted(() => ({ diff --git a/src/renderer/src/components/browser-pane/BrowserPane.tsx b/src/renderer/src/components/browser-pane/BrowserPane.tsx index 9158962d88c..064d2e46d30 100644 --- a/src/renderer/src/components/browser-pane/BrowserPane.tsx +++ b/src/renderer/src/components/browser-pane/BrowserPane.tsx @@ -68,7 +68,7 @@ import type { BrowserLoadError, BrowserPage as BrowserPageState, BrowserWorkspace as BrowserWorkspaceState -} from '../../../../shared/types' +} from '../../../../shared/browser-workspace-types' import { normalizeBrowserNavigationUrl, normalizeExternalBrowserUrl, diff --git a/src/renderer/src/components/browser-pane/BrowserPaneOverlayLayer.test.tsx b/src/renderer/src/components/browser-pane/BrowserPaneOverlayLayer.test.tsx index 111890288f4..13aa6d109a3 100644 --- a/src/renderer/src/components/browser-pane/BrowserPaneOverlayLayer.test.tsx +++ b/src/renderer/src/components/browser-pane/BrowserPaneOverlayLayer.test.tsx @@ -3,7 +3,8 @@ import { cleanup, render } from '@testing-library/react' import { Suspense } from 'react' import { renderToStaticMarkup } from 'react-dom/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { BrowserTab as BrowserTabState, Tab, TabGroup } from '../../../../shared/types' +import type { BrowserTab as BrowserTabState } from '../../../../shared/browser-workspace-types' +import type { Tab, TabGroup } from '../../../../shared/tab-types' type MockAppState = { browserTabsByWorktree: Record diff --git a/src/renderer/src/components/browser-pane/BrowserPaneOverlayLayer.tsx b/src/renderer/src/components/browser-pane/BrowserPaneOverlayLayer.tsx index 67d6aa4da9e..f0ea554f9fe 100644 --- a/src/renderer/src/components/browser-pane/BrowserPaneOverlayLayer.tsx +++ b/src/renderer/src/components/browser-pane/BrowserPaneOverlayLayer.tsx @@ -2,7 +2,8 @@ import { memo, useCallback, useLayoutEffect, useMemo, useState } from 'react' import { registerBrowserOverlaySlotViewport } from './browser-page-viewport' import { useShallow } from 'zustand/react/shallow' import { useAppStore } from '../../store' -import type { BrowserTab as BrowserTabState, Tab, TabGroup } from '../../../../shared/types' +import type { BrowserTab as BrowserTabState } from '../../../../shared/browser-workspace-types' +import type { Tab, TabGroup } from '../../../../shared/tab-types' import BrowserPane, { type BrowserFindShortcutScope } from './BrowserPane' import { tabGroupBodyAnchorName } from '../tab-group/tab-group-body-anchor' import { useBrowserAutomationVisibilityForAny } from './browser-automation-visibility' diff --git a/src/renderer/src/components/browser-pane/BrowserToolbarMenu.tsx b/src/renderer/src/components/browser-pane/BrowserToolbarMenu.tsx index f712d33abd2..46362bbf93e 100644 --- a/src/renderer/src/components/browser-pane/BrowserToolbarMenu.tsx +++ b/src/renderer/src/components/browser-pane/BrowserToolbarMenu.tsx @@ -4,7 +4,7 @@ import { emitBrowserCookieImportToast } from '@/lib/browser-cookie-import-toast' import { useAppStore } from '@/store' import { useMountedRef } from '@/hooks/useMountedRef' import { shouldShowBrowserImportHint } from './browser-import-hint-visibility' -import type { BrowserViewportPresetId } from '../../../../shared/types' +import type { BrowserViewportPresetId } from '../../../../shared/browser-workspace-types' import { browserViewportPresetToOverride, getBrowserViewportPreset diff --git a/src/renderer/src/components/browser-pane/browser-address-bar-suggestions.test.ts b/src/renderer/src/components/browser-pane/browser-address-bar-suggestions.test.ts index 5da8a237944..65ba9a3809c 100644 --- a/src/renderer/src/components/browser-pane/browser-address-bar-suggestions.test.ts +++ b/src/renderer/src/components/browser-pane/browser-address-bar-suggestions.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { BrowserHistoryEntry } from '../../../../shared/types' +import type { BrowserHistoryEntry } from '../../../../shared/browser-workspace-types' import { BROWSER_ADDRESS_BAR_QUERY_MAX_BYTES, buildBrowserAddressBarSuggestions, diff --git a/src/renderer/src/components/browser-pane/browser-address-bar-suggestions.ts b/src/renderer/src/components/browser-pane/browser-address-bar-suggestions.ts index 6ac0fe12c52..cff0a3b54ca 100644 --- a/src/renderer/src/components/browser-pane/browser-address-bar-suggestions.ts +++ b/src/renderer/src/components/browser-pane/browser-address-bar-suggestions.ts @@ -6,7 +6,7 @@ import { SEARCH_ENGINE_LABELS, type SearchEngine } from '../../../../shared/browser-url' -import type { BrowserHistoryEntry } from '../../../../shared/types' +import type { BrowserHistoryEntry } from '../../../../shared/browser-workspace-types' import { isClipboardTextByteLengthOverLimit } from '../../../../shared/clipboard-text' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/browser-pane/browser-guest-worktree-retention.test.ts b/src/renderer/src/components/browser-pane/browser-guest-worktree-retention.test.ts index 5557664f602..9d1f2bc411d 100644 --- a/src/renderer/src/components/browser-pane/browser-guest-worktree-retention.test.ts +++ b/src/renderer/src/components/browser-pane/browser-guest-worktree-retention.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { BrowserPage, BrowserWorkspace } from '../../../../shared/types' +import type { BrowserPage, BrowserWorkspace } from '../../../../shared/browser-workspace-types' import { BROWSER_GUEST_HIDDEN_WORKTREE_RETENTION_LIMIT, browserTabVisibilityPageIds, diff --git a/src/renderer/src/components/browser-pane/browser-guest-worktree-retention.ts b/src/renderer/src/components/browser-pane/browser-guest-worktree-retention.ts index 322899c279f..22cd358d4fc 100644 --- a/src/renderer/src/components/browser-pane/browser-guest-worktree-retention.ts +++ b/src/renderer/src/components/browser-pane/browser-guest-worktree-retention.ts @@ -1,4 +1,4 @@ -import type { BrowserPage, BrowserWorkspace } from '../../../../shared/types' +import type { BrowserPage, BrowserWorkspace } from '../../../../shared/browser-workspace-types' // Why 4: every hidden worktree that retains browser guests keeps one Electron // guest process per page alive purely for instant revisits, so guest memory diff --git a/src/renderer/src/components/browser-pane/browser-load-failure-overlay.tsx b/src/renderer/src/components/browser-pane/browser-load-failure-overlay.tsx index af760df4a6b..7c2e41d2904 100644 --- a/src/renderer/src/components/browser-pane/browser-load-failure-overlay.tsx +++ b/src/renderer/src/components/browser-pane/browser-load-failure-overlay.tsx @@ -8,7 +8,7 @@ import type { BrowserCertificateProceedFailureReason, BrowserCertificateProceedResult, BrowserLoadError -} from '../../../../shared/types' +} from '../../../../shared/browser-workspace-types' import { isEligibleLocalCertificateHost } from '../../../../shared/browser-url' import { normalizeCertificateError } from '../../../../shared/browser-certificate-errors' import { diff --git a/src/renderer/src/components/browser-pane/browser-notices.ts b/src/renderer/src/components/browser-pane/browser-notices.ts index 3c644571ec5..701a074186e 100644 --- a/src/renderer/src/components/browser-pane/browser-notices.ts +++ b/src/renderer/src/components/browser-pane/browser-notices.ts @@ -3,7 +3,7 @@ import type { BrowserPermissionDeniedEvent, BrowserPopupEvent } from '../../../../shared/browser-guest-events' -import type { BrowserLoadError } from '../../../../shared/types' +import type { BrowserLoadError } from '../../../../shared/browser-workspace-types' import { isChromiumCertificateErrorCode } from '../../../../shared/browser-certificate-errors' import { translate } from '@/i18n/i18n' import { BROWSER_GUEST_RECOVERY_ERROR_CODE } from './browser-page-guest-recovery' diff --git a/src/renderer/src/components/browser-pane/browser-pane-page-selection.test.ts b/src/renderer/src/components/browser-pane/browser-pane-page-selection.test.ts index 54455d706cc..f97398619f5 100644 --- a/src/renderer/src/components/browser-pane/browser-pane-page-selection.test.ts +++ b/src/renderer/src/components/browser-pane/browser-pane-page-selection.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { getBrowserPagesForWorkspace } from './browser-pane-page-selection' -import type { BrowserPage } from '../../../../shared/types' +import type { BrowserPage } from '../../../../shared/browser-workspace-types' function makeBrowserPage(id: string): BrowserPage { return { diff --git a/src/renderer/src/components/browser-pane/browser-pane-page-selection.ts b/src/renderer/src/components/browser-pane/browser-pane-page-selection.ts index 240b0dec6e9..38b8b7d3f95 100644 --- a/src/renderer/src/components/browser-pane/browser-pane-page-selection.ts +++ b/src/renderer/src/components/browser-pane/browser-pane-page-selection.ts @@ -1,4 +1,4 @@ -import type { BrowserPage } from '../../../../shared/types' +import type { BrowserPage } from '../../../../shared/browser-workspace-types' const EMPTY_BROWSER_PAGES: BrowserPage[] = [] diff --git a/src/renderer/src/components/browser-pane/browser-toolbar-menu-dropdown.tsx b/src/renderer/src/components/browser-pane/browser-toolbar-menu-dropdown.tsx index 508eb059ca2..88d63df7dd6 100644 --- a/src/renderer/src/components/browser-pane/browser-toolbar-menu-dropdown.tsx +++ b/src/renderer/src/components/browser-pane/browser-toolbar-menu-dropdown.tsx @@ -16,7 +16,10 @@ import { import { BrowserCookieImportDisclosure } from '@/components/BrowserCookieImportDisclosure' import { useAppStore } from '@/store' import { BROWSER_FAMILY_LABELS } from '../../../../shared/constants' -import type { BrowserSessionProfile, BrowserViewportPresetId } from '../../../../shared/types' +import type { + BrowserSessionProfile, + BrowserViewportPresetId +} from '../../../../shared/browser-workspace-types' type DetectedBrowserEntry = { family: string diff --git a/src/renderer/src/components/cmd-j/PaletteCreateWorktreeRow.tsx b/src/renderer/src/components/cmd-j/PaletteCreateWorktreeRow.tsx index 2079a0c8fbf..e2d126beae4 100644 --- a/src/renderer/src/components/cmd-j/PaletteCreateWorktreeRow.tsx +++ b/src/renderer/src/components/cmd-j/PaletteCreateWorktreeRow.tsx @@ -5,7 +5,7 @@ import { JiraIcon } from '@/components/icons/JiraIcon' import { CommandItem } from '@/components/ui/command' import { CREATE_WORKTREE_ITEM_ID } from '@/lib/worktree-palette-create-action' import type { CmdJTaskUrlCreatePreview } from '@/lib/worktree-palette-task-url-match' -import type { LinearIssue } from '../../../../shared/types' +import type { LinearIssue } from '../../../../shared/linear/issue-types' import { translate } from '@/i18n/i18n' function TaskUrlProviderIcon({ diff --git a/src/renderer/src/components/cmd-j/palette-filter-options.test.ts b/src/renderer/src/components/cmd-j/palette-filter-options.test.ts index bdfaef30ad8..021e92a2359 100644 --- a/src/renderer/src/components/cmd-j/palette-filter-options.test.ts +++ b/src/renderer/src/components/cmd-j/palette-filter-options.test.ts @@ -1,7 +1,9 @@ import path from 'node:path' import { describe, expect, it } from 'vitest' import { getWorktreeExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host' -import type { Project, ProjectHostSetup, Repo, Worktree } from '../../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { buildSidebarHostOptions } from '../sidebar/sidebar-host-options' import { buildPaletteFilterModel, diff --git a/src/renderer/src/components/cmd-j/palette-filter-options.ts b/src/renderer/src/components/cmd-j/palette-filter-options.ts index 9eb5fdce4ac..2670eb80e28 100644 --- a/src/renderer/src/components/cmd-j/palette-filter-options.ts +++ b/src/renderer/src/components/cmd-j/palette-filter-options.ts @@ -3,7 +3,9 @@ import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../shared/execution-host' -import type { Project, ProjectHostSetup, Repo, Worktree } from '../../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getProjectHeaderRevealTarget, type ProjectGroupingModel diff --git a/src/renderer/src/components/cmd-j/palette-filter.ts b/src/renderer/src/components/cmd-j/palette-filter.ts index 075af323799..f521a6ad92a 100644 --- a/src/renderer/src/components/cmd-j/palette-filter.ts +++ b/src/renderer/src/components/cmd-j/palette-filter.ts @@ -1,5 +1,5 @@ import type { ExecutionHostId } from '../../../../shared/execution-host' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { resolveRepoFilterHostId, resolveWorktreeFilterHostId, diff --git a/src/renderer/src/components/cmd-j/palette-host-badge.ts b/src/renderer/src/components/cmd-j/palette-host-badge.ts index 01cdc72a60f..8ac94adeae4 100644 --- a/src/renderer/src/components/cmd-j/palette-host-badge.ts +++ b/src/renderer/src/components/cmd-j/palette-host-badge.ts @@ -1,4 +1,4 @@ -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID, diff --git a/src/renderer/src/components/cmd-j/palette-live-status.test.tsx b/src/renderer/src/components/cmd-j/palette-live-status.test.tsx index f19bf3a91d2..9e8a51a6e80 100644 --- a/src/renderer/src/components/cmd-j/palette-live-status.test.tsx +++ b/src/renderer/src/components/cmd-j/palette-live-status.test.tsx @@ -7,7 +7,7 @@ import { useAppStore } from '@/store' import type { AppState } from '@/store/types' import type { AgentStatusEntry, AgentStatusState } from '../../../../shared/agent-status-types' import { makePaneKey } from '../../../../shared/stable-pane-id' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { PaletteLiveStatusProvider, PaletteRecentTabStatusDot, diff --git a/src/renderer/src/components/cmd-j/palette-live-status.tsx b/src/renderer/src/components/cmd-j/palette-live-status.tsx index 03a2af8928d..5c755ad4164 100644 --- a/src/renderer/src/components/cmd-j/palette-live-status.tsx +++ b/src/renderer/src/components/cmd-j/palette-live-status.tsx @@ -26,7 +26,9 @@ import { } from '@/components/tab-bar/terminal-tab-activity-status' import { translate } from '@/i18n/i18n' import type { LiveAgentWorktreeStatus } from '@/lib/worktree-activity-state' -import type { BrowserWorkspace, TerminalTab, Worktree } from '../../../../shared/types' +import type { BrowserWorkspace } from '../../../../shared/browser-workspace-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' /** Confines the app's hottest status subscriptions here so only the dots re-render on their churn. */ type PaletteLiveStatus = { diff --git a/src/renderer/src/components/cmd-j/palette-project-results.ts b/src/renderer/src/components/cmd-j/palette-project-results.ts index d0fcc15636c..e45e2a8e29f 100644 --- a/src/renderer/src/components/cmd-j/palette-project-results.ts +++ b/src/renderer/src/components/cmd-j/palette-project-results.ts @@ -4,7 +4,9 @@ import { normalizeCmdJPaletteQuery, uniqueNormalizedCmdJPaletteKeywords } from './palette-query-tokens' -import type { Project, ProjectGroup, ProjectHostSetup, Repo } from '../../../../shared/types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { translate } from '@/i18n/i18n' import { getProjectGroupHeaderKey, diff --git a/src/renderer/src/components/cmd-j/palette-results.test.ts b/src/renderer/src/components/cmd-j/palette-results.test.ts index bf875ebe9a2..e9f7b3ec3fa 100644 --- a/src/renderer/src/components/cmd-j/palette-results.test.ts +++ b/src/renderer/src/components/cmd-j/palette-results.test.ts @@ -13,7 +13,9 @@ import { } from './palette-results' import { hasCmdJProjectSearchCandidates, searchCmdJProjectResults } from './palette-project-results' import type { SettingsNavSection } from '@/lib/settings-navigation-types' -import type { Project, ProjectGroup, ProjectHostSetup, Repo } from '../../../../shared/types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' const noopRun: CmdJQuickAction['run'] = async () => ({ status: 'ok' }) const available: CmdJQuickAction['isAvailable'] = () => ({ available: true }) diff --git a/src/renderer/src/components/cmd-j/quick-action-context.test.ts b/src/renderer/src/components/cmd-j/quick-action-context.test.ts index 7ce2e107613..325252c4dc2 100644 --- a/src/renderer/src/components/cmd-j/quick-action-context.test.ts +++ b/src/renderer/src/components/cmd-j/quick-action-context.test.ts @@ -8,7 +8,7 @@ import { } from './quick-action-context' import { getCmdJQuickActions } from './quick-actions' import type { AppState } from '@/store/types' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' type GroupState = Pick diff --git a/src/renderer/src/components/cmd-j/quick-action-context.ts b/src/renderer/src/components/cmd-j/quick-action-context.ts index 79007eae750..88a8cfad533 100644 --- a/src/renderer/src/components/cmd-j/quick-action-context.ts +++ b/src/renderer/src/components/cmd-j/quick-action-context.ts @@ -1,6 +1,6 @@ import type { AppState } from '@/store/types' import { findWorktreeById } from '@/store/slices/worktree-helpers' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import type { SshConnectionStatus } from '../../../../shared/ssh-types' import { getClientCreationActionPolicy } from '@/lib/client-creation-action-policy' diff --git a/src/renderer/src/components/cmd-j/worktree-checks-review-index.test.ts b/src/renderer/src/components/cmd-j/worktree-checks-review-index.test.ts index e401aa0665b..250830fd2d7 100644 --- a/src/renderer/src/components/cmd-j/worktree-checks-review-index.test.ts +++ b/src/renderer/src/components/cmd-j/worktree-checks-review-index.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest' import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { PRInfo, Repo, Worktree } from '../../../../shared/types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getGitHubPRCacheKey } from '@/store/slices/github-cache-key' import { getHostedReviewCacheKey } from '@/store/slices/hosted-review-cache-identity' import { getRepoHostIdentity } from '@/store/slices/repo-host-identity' diff --git a/src/renderer/src/components/cmd-j/worktree-checks-review-index.ts b/src/renderer/src/components/cmd-j/worktree-checks-review-index.ts index 992fab427a9..f78f566c247 100644 --- a/src/renderer/src/components/cmd-j/worktree-checks-review-index.ts +++ b/src/renderer/src/components/cmd-j/worktree-checks-review-index.ts @@ -5,7 +5,8 @@ import { getRepoHostIdentityForParts } from '@/store/slices/repo-host-identity' import type { AppState } from '@/store/types' import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { selectChecksPanelReview } from '../right-sidebar/checks-panel-review' type WorktreeChecksReviewIndexArgs = { diff --git a/src/renderer/src/components/confirmation-skip-preference.ts b/src/renderer/src/components/confirmation-skip-preference.ts index 8311f2428b2..c7eea096fd4 100644 --- a/src/renderer/src/components/confirmation-skip-preference.ts +++ b/src/renderer/src/components/confirmation-skip-preference.ts @@ -1,7 +1,7 @@ import { toast } from 'sonner' import { translate } from '@/i18n/i18n' import type { SettingsNavTarget } from '@/lib/settings-navigation-types' -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' /** Persists the preference and keeps its reversal one click away. */ export function persistConfirmationSkipPreference({ diff --git a/src/renderer/src/components/crash-report/CrashReportDialogSurface.tsx b/src/renderer/src/components/crash-report/CrashReportDialogSurface.tsx index 841c6c4bb44..94ffb8d342d 100644 --- a/src/renderer/src/components/crash-report/CrashReportDialogSurface.tsx +++ b/src/renderer/src/components/crash-report/CrashReportDialogSurface.tsx @@ -19,7 +19,7 @@ import { type CrashReportDiagnosticBundle, type CrashReportRecord } from '../../../../shared/crash-reporting' -import type { GitHubViewer } from '../../../../shared/types' +import type { GitHubViewer } from '../../../../shared/github/pull-request-types' import { translate } from '@/i18n/i18n' import { CRASH_REPORT_SUBMIT_FAILURE_TOAST_ID, diff --git a/src/renderer/src/components/dashboard-popout/AgentMap.tsx b/src/renderer/src/components/dashboard-popout/AgentMap.tsx index db5741e5e07..7b7afc56bc7 100644 --- a/src/renderer/src/components/dashboard-popout/AgentMap.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentMap.tsx @@ -8,7 +8,7 @@ import type { DashboardWorkspace } from '../../../../shared/dashboard-snapshot' import type { RepoIcon } from '../../../../shared/repo-icon' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { AgentMapCanvas, type AgentMapCanvasHandle } from './AgentMapCanvas' import { ALL_AGENT_MAP_HOSTS, filterAgentMapCards, type AgentMapState } from './agent-map-filter' import { updateAgentMapLayout, type AgentMapLayoutCache } from './agent-map-layout' diff --git a/src/renderer/src/components/dashboard-popout/AgentMapCanvas.tsx b/src/renderer/src/components/dashboard-popout/AgentMapCanvas.tsx index 62439665fc7..84290f36f40 100644 --- a/src/renderer/src/components/dashboard-popout/AgentMapCanvas.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentMapCanvas.tsx @@ -15,7 +15,7 @@ import type { DashboardSpawnAgentArgs } from '../../../../shared/dashboard-snapshot' import type { RepoIcon } from '../../../../shared/repo-icon' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { AgentMapAgentNode, AgentMapProjectRing, AgentMapLayout } from './agent-map-layout' import type { AgentMapFlareStatus } from './agent-map-node-metadata' import { AgentMapScene } from './AgentMapScene' diff --git a/src/renderer/src/components/dashboard-popout/AgentMapScene.tsx b/src/renderer/src/components/dashboard-popout/AgentMapScene.tsx index c4a298ed48e..a4914bbdad5 100644 --- a/src/renderer/src/components/dashboard-popout/AgentMapScene.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentMapScene.tsx @@ -3,7 +3,7 @@ import { RepoIconGlyph } from '@/components/repo/repo-icon' import { translate } from '@/i18n/i18n' import type { DashboardCard, DashboardSpawnAgentArgs } from '../../../../shared/dashboard-snapshot' import type { RepoIcon } from '../../../../shared/repo-icon' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { AgentMapAgentNode, AgentMapLayout, diff --git a/src/renderer/src/components/dashboard-popout/AgentMapSnapshotWorkspaceMenu.tsx b/src/renderer/src/components/dashboard-popout/AgentMapSnapshotWorkspaceMenu.tsx index e2208e0be0d..17b992aaaff 100644 --- a/src/renderer/src/components/dashboard-popout/AgentMapSnapshotWorkspaceMenu.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentMapSnapshotWorkspaceMenu.tsx @@ -17,7 +17,7 @@ import type { DashboardSleepWorkspaceArgs, DashboardSpawnAgentArgs } from '../../../../shared/dashboard-snapshot' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' export type AgentMapSnapshotWorkspaceMenuRequest = { id: number diff --git a/src/renderer/src/components/dashboard-popout/AgentMapWorkspaceContextMenu.test.tsx b/src/renderer/src/components/dashboard-popout/AgentMapWorkspaceContextMenu.test.tsx index 057a593da7e..c5477a997e9 100644 --- a/src/renderer/src/components/dashboard-popout/AgentMapWorkspaceContextMenu.test.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentMapWorkspaceContextMenu.test.tsx @@ -5,7 +5,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { TooltipProvider } from '@/components/ui/tooltip' import { useAppStore } from '@/store' -import type { ProjectGroup, Repo, Worktree } from '../../../../shared/types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { DashboardCard } from '../../../../shared/dashboard-snapshot' import { AgentMap } from './AgentMap' import * as StoreSelectors from '@/store/selectors' diff --git a/src/renderer/src/components/dashboard-popout/AgentMapWorktreeRingNode.tsx b/src/renderer/src/components/dashboard-popout/AgentMapWorktreeRingNode.tsx index fd362f45609..f4999a012bd 100644 --- a/src/renderer/src/components/dashboard-popout/AgentMapWorktreeRingNode.tsx +++ b/src/renderer/src/components/dashboard-popout/AgentMapWorktreeRingNode.tsx @@ -7,7 +7,7 @@ import { translate } from '@/i18n/i18n' import { AgentIcon, getAgentLabel } from '@/lib/agent-catalog' import { agentTypeToIconAgent } from '@/lib/agent-status' import type { DashboardCard, DashboardSpawnAgentArgs } from '../../../../shared/dashboard-snapshot' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { AgentMapAgentNode, AgentMapProjectRing, diff --git a/src/renderer/src/components/dashboard-popout/agent-map-render-test-harness.tsx b/src/renderer/src/components/dashboard-popout/agent-map-render-test-harness.tsx index a7c0b08f64e..e428d43f5f6 100644 --- a/src/renderer/src/components/dashboard-popout/agent-map-render-test-harness.tsx +++ b/src/renderer/src/components/dashboard-popout/agent-map-render-test-harness.tsx @@ -6,7 +6,7 @@ import type { DashboardSleepWorkspaceArgs, DashboardSpawnAgentArgs } from '../../../../shared/dashboard-snapshot' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { AgentMap } from './AgentMap' import type { AgentMapState } from './agent-map-filter' diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-compatibility.ts b/src/renderer/src/components/dashboard-popout/preview-terminal-compatibility.ts index 0693bdff81a..96db5963714 100644 --- a/src/renderer/src/components/dashboard-popout/preview-terminal-compatibility.ts +++ b/src/renderer/src/components/dashboard-popout/preview-terminal-compatibility.ts @@ -1,6 +1,6 @@ import type { Terminal } from '@xterm/xterm' import { Unicode11Addon } from '@xterm/addon-unicode11' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { activateOrcaTerminalUnicodeProvider } from '../../../../shared/terminal-unicode-provider' import { installWindowsCtrlAltChordRepair } from '@/lib/pane-manager/terminal-windows-ctrl-alt-chord-classification' import { attachTerminalMouseWheelMultiplier } from '@/lib/pane-manager/pane-terminal-mouse-wheel' diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-ligatures.ts b/src/renderer/src/components/dashboard-popout/preview-terminal-ligatures.ts index cc0cb2036fd..354edfeead0 100644 --- a/src/renderer/src/components/dashboard-popout/preview-terminal-ligatures.ts +++ b/src/renderer/src/components/dashboard-popout/preview-terminal-ligatures.ts @@ -1,5 +1,5 @@ import type { Terminal } from '@xterm/xterm' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { resolveTerminalLigaturesEnabled } from '../../../../shared/terminal-ligatures' import { TerminalLigaturesAddon } from '@/lib/pane-manager/terminal-ligatures-addon' diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-options.test.ts b/src/renderer/src/components/dashboard-popout/preview-terminal-options.test.ts index af4974cdef3..319ec241293 100644 --- a/src/renderer/src/components/dashboard-popout/preview-terminal-options.test.ts +++ b/src/renderer/src/components/dashboard-popout/preview-terminal-options.test.ts @@ -1,7 +1,7 @@ // @vitest-environment happy-dom import { describe, expect, it } from 'vitest' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { buildPreviewAppearanceOptions, buildPreviewTerminalOptions diff --git a/src/renderer/src/components/dashboard-popout/preview-terminal-options.ts b/src/renderer/src/components/dashboard-popout/preview-terminal-options.ts index aaf13a8c4bc..284f6510558 100644 --- a/src/renderer/src/components/dashboard-popout/preview-terminal-options.ts +++ b/src/renderer/src/components/dashboard-popout/preview-terminal-options.ts @@ -1,5 +1,5 @@ import type { ITerminalInitOnlyOptions, ITerminalOptions, ITheme } from '@xterm/xterm' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { DashboardCardTerminalInput } from '../../../../shared/dashboard-snapshot' import { resolveTerminalFontWeights } from '../../../../shared/terminal-fonts' import { normalizeTerminalLineHeight } from '../../../../shared/terminal-line-height-settings' diff --git a/src/renderer/src/components/dashboard-popout/useAgentMapContextMenus.tsx b/src/renderer/src/components/dashboard-popout/useAgentMapContextMenus.tsx index a20c534e4d3..198885bdf67 100644 --- a/src/renderer/src/components/dashboard-popout/useAgentMapContextMenus.tsx +++ b/src/renderer/src/components/dashboard-popout/useAgentMapContextMenus.tsx @@ -3,7 +3,7 @@ import type { DashboardSleepWorkspaceArgs, DashboardSpawnAgentArgs } from '../../../../shared/dashboard-snapshot' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { AgentMapProjectRing, AgentMapWorktreeRing } from './agent-map-layout' import { AgentMapSnapshotWorkspaceMenu, diff --git a/src/renderer/src/components/dashboard/AgentDashboardSettingsMenu.tsx b/src/renderer/src/components/dashboard/AgentDashboardSettingsMenu.tsx index 1cc3b6ed03b..eb462a2d3a9 100644 --- a/src/renderer/src/components/dashboard/AgentDashboardSettingsMenu.tsx +++ b/src/renderer/src/components/dashboard/AgentDashboardSettingsMenu.tsx @@ -9,7 +9,7 @@ import { } from '@/components/ui/dropdown-menu' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { SettingsSegmentedControl, SettingsSwitch } from '../settings/SettingsFormControls' -import type { AgentDashboardMode } from '../../../../shared/types' +import type { AgentDashboardMode } from '../../../../shared/ui-chrome-types' import { translate } from '@/i18n/i18n' type AgentDashboardSettingsMenuProps = { diff --git a/src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx b/src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx index 0fb978de62b..5be1c78d574 100644 --- a/src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx +++ b/src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx @@ -2,7 +2,7 @@ import { renderToStaticMarkup } from 'react-dom/server' import type { ComponentProps } from 'react' import { describe, expect, it, vi } from 'vitest' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { TooltipProvider } from '../ui/tooltip' import DashboardAgentRow from './DashboardAgentRow' import type { DashboardAgentRow as DashboardAgentRowData } from './useDashboardData' diff --git a/src/renderer/src/components/dashboard/agent-row-lineage-model.test.ts b/src/renderer/src/components/dashboard/agent-row-lineage-model.test.ts index 028670a91a3..dcc5d754de2 100644 --- a/src/renderer/src/components/dashboard/agent-row-lineage-model.test.ts +++ b/src/renderer/src/components/dashboard/agent-row-lineage-model.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { DashboardAgentRow } from './useDashboardData' import { buildAgentRowLineageTree } from './agent-row-lineage-model' diff --git a/src/renderer/src/components/dashboard/build-dashboard-snapshot-folder-workspace.test.ts b/src/renderer/src/components/dashboard/build-dashboard-snapshot-folder-workspace.test.ts index 822a3b80e96..51a3aada421 100644 --- a/src/renderer/src/components/dashboard/build-dashboard-snapshot-folder-workspace.test.ts +++ b/src/renderer/src/components/dashboard/build-dashboard-snapshot-folder-workspace.test.ts @@ -1,7 +1,9 @@ import { describe, expect, it } from 'vitest' import { makePaneKey } from '../../../../shared/stable-pane-id' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' -import type { FolderWorkspace, ProjectGroup, TerminalTab } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' import { buildDashboardSnapshot, type DashboardSnapshotState } from './build-dashboard-snapshot' diff --git a/src/renderer/src/components/dashboard/build-dashboard-snapshot-orchestration-routing.test.ts b/src/renderer/src/components/dashboard/build-dashboard-snapshot-orchestration-routing.test.ts index d2b85f6ced0..424c936ab0f 100644 --- a/src/renderer/src/components/dashboard/build-dashboard-snapshot-orchestration-routing.test.ts +++ b/src/renderer/src/components/dashboard/build-dashboard-snapshot-orchestration-routing.test.ts @@ -1,7 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { AgentStatusOrchestrationContext } from '../../../../shared/agent-status-types' import { makePaneKey } from '../../../../shared/stable-pane-id' -import type { TerminalTab, Worktree } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' const batchMocks = vi.hoisted(() => ({ release: vi.fn(), diff --git a/src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts b/src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts index d4e9bdc565a..884a8ebcaf9 100644 --- a/src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts +++ b/src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts @@ -7,7 +7,8 @@ import { } from '../../../../shared/agent-status-types' import { DASHBOARD_MAX_LABEL_LENGTH } from '../../../../shared/dashboard-snapshot' import { makePaneKey } from '../../../../shared/stable-pane-id' -import type { TerminalTab, Worktree } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import { selectRuntimeAgentOrchestrationBatch } from '../sidebar/worktree-agent-orchestration-batch' import type * as DashboardSnapshotWorkspacesModule from './dashboard-snapshot-workspaces' import type * as AgentRowLineageModule from './agent-row-lineage' diff --git a/src/renderer/src/components/dashboard/dashboard-card-context.test.ts b/src/renderer/src/components/dashboard/dashboard-card-context.test.ts index ab61f16d857..f4152c20701 100644 --- a/src/renderer/src/components/dashboard/dashboard-card-context.test.ts +++ b/src/renderer/src/components/dashboard/dashboard-card-context.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest' import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { PRInfo, Repo, Worktree } from '../../../../shared/types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getGitHubPRCacheKey } from '@/store/slices/github-cache-key' import { getHostedReviewCacheKey } from '@/store/slices/hosted-review-cache-identity' import { diff --git a/src/renderer/src/components/dashboard/dashboard-card-context.ts b/src/renderer/src/components/dashboard/dashboard-card-context.ts index f376a52163c..a0ed03d4d85 100644 --- a/src/renderer/src/components/dashboard/dashboard-card-context.ts +++ b/src/renderer/src/components/dashboard/dashboard-card-context.ts @@ -4,7 +4,8 @@ import type { AppState } from '@/store/types' import type { DashboardCardReview } from '../../../../shared/dashboard-snapshot' import { hostedReviewInfoFromGitHubPRInfo } from '../../../../shared/hosted-review-github' import { isPositiveHostedReviewNumber } from '../../../../shared/hosted-review' -import type { Repo, Worktree, WorkspaceStatusDefinition } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorkspaceStatusDefinition, Worktree } from '../../../../shared/worktree/types' import { DEFAULT_WORKSPACE_STATUSES, getWorkspaceStatus diff --git a/src/renderer/src/components/dashboard/dashboard-card-terminal-input.ts b/src/renderer/src/components/dashboard/dashboard-card-terminal-input.ts index b1b0f16c3ee..74ca8250315 100644 --- a/src/renderer/src/components/dashboard/dashboard-card-terminal-input.ts +++ b/src/renderer/src/components/dashboard/dashboard-card-terminal-input.ts @@ -1,6 +1,6 @@ import type { AppState } from '@/store/types' import type { DashboardCardTerminalInput } from '../../../../shared/dashboard-snapshot' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { toRuntimeExecutionHostId, toSshExecutionHostId } from '../../../../shared/execution-host' import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id' import { getConnectionIdFromState } from '@/lib/connection-context' diff --git a/src/renderer/src/components/dashboard/dashboard-worktree-launch-options.test.ts b/src/renderer/src/components/dashboard/dashboard-worktree-launch-options.test.ts index a0aacdcc504..c160b4dd37c 100644 --- a/src/renderer/src/components/dashboard/dashboard-worktree-launch-options.test.ts +++ b/src/renderer/src/components/dashboard/dashboard-worktree-launch-options.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from 'vitest' import { DASHBOARD_MAX_LAUNCH_WORKTREES } from '../../../../shared/dashboard-snapshot' import type { DashboardCard, DashboardWorkspace } from '../../../../shared/dashboard-snapshot' -import type { FolderWorkspace, ProjectGroup } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import { buildDashboardWorktreeLaunchOptions } from './dashboard-worktree-launch-options' diff --git a/src/renderer/src/components/dashboard/dashboard-worktree-launch-options.ts b/src/renderer/src/components/dashboard/dashboard-worktree-launch-options.ts index 16ca6c1f795..44c76a74fee 100644 --- a/src/renderer/src/components/dashboard/dashboard-worktree-launch-options.ts +++ b/src/renderer/src/components/dashboard/dashboard-worktree-launch-options.ts @@ -10,7 +10,7 @@ import { filterEnabledTuiAgents, TUI_AGENT_AUTO_PICK_ORDER } from '../../../../shared/tui-agent-selection' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { parseWorkspaceKey } from '../../../../shared/workspace-scope' export type DashboardLaunchDetectionState = Pick< diff --git a/src/renderer/src/components/dashboard/useDashboardData.ts b/src/renderer/src/components/dashboard/useDashboardData.ts index 06926f919a0..2433ea0e88f 100644 --- a/src/renderer/src/components/dashboard/useDashboardData.ts +++ b/src/renderer/src/components/dashboard/useDashboardData.ts @@ -3,7 +3,7 @@ import type { AgentStatusState, AgentType } from '../../../../shared/agent-status-types' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' export type DashboardAgentRow = { /** Row identity. For 'subagent' rows this is a synthetic key (the child has diff --git a/src/renderer/src/components/dashboard/useLiveDashboardSnapshot.test.ts b/src/renderer/src/components/dashboard/useLiveDashboardSnapshot.test.ts index d2481a9201e..f3b7da6254e 100644 --- a/src/renderer/src/components/dashboard/useLiveDashboardSnapshot.test.ts +++ b/src/renderer/src/components/dashboard/useLiveDashboardSnapshot.test.ts @@ -4,7 +4,10 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { useAppStore } from '@/store' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' import { makePaneKey } from '../../../../shared/stable-pane-id' -import type { GlobalSettings, Repo, TerminalTab, Worktree } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import { useLiveDashboardSnapshot } from './useLiveDashboardSnapshot' const NOW = 1_000_000_000 diff --git a/src/renderer/src/components/dashboard/useRetainedAgents.test.ts b/src/renderer/src/components/dashboard/useRetainedAgents.test.ts index 095bdedcb67..2e3b60f1157 100644 --- a/src/renderer/src/components/dashboard/useRetainedAgents.test.ts +++ b/src/renderer/src/components/dashboard/useRetainedAgents.test.ts @@ -5,7 +5,9 @@ import { useAppStore } from '@/store' import { resetAgentPaneAuthorityAliasesForTests } from '@/store/slices/agent-pane-authority' import type { AgentStatusEntry, AgentStatusState } from '../../../../shared/agent-status-types' import { makePaneKey } from '../../../../shared/stable-pane-id' -import type { Repo, TerminalTab, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import { collectRetainedAgentsOnDisappear, useRetainedAgentsSync } from './useRetainedAgents' const initialAppState = useAppStore.getInitialState() diff --git a/src/renderer/src/components/dashboard/useRetainedAgents.ts b/src/renderer/src/components/dashboard/useRetainedAgents.ts index 6fa1f94fda7..883da974142 100644 --- a/src/renderer/src/components/dashboard/useRetainedAgents.ts +++ b/src/renderer/src/components/dashboard/useRetainedAgents.ts @@ -4,7 +4,10 @@ import { useAppStore } from '@/store' import { isExplicitAgentStatusFresh } from '@/lib/agent-status' import type { DashboardAgentRow } from './useDashboardData' import type { RetainedAgentEntry } from '@/store/slices/agent-status' -import type { FolderWorkspace, Repo, TerminalTab, Worktree } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { Repo } from '../../../../shared/repo-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import { resolveAgentPaneAuthorityKey } from '@/store/slices/agent-pane-authority' import { diff --git a/src/renderer/src/components/dashboard/useRetainedAgentsSync.test.ts b/src/renderer/src/components/dashboard/useRetainedAgentsSync.test.ts index 3d33575d649..88bb584a18e 100644 --- a/src/renderer/src/components/dashboard/useRetainedAgentsSync.test.ts +++ b/src/renderer/src/components/dashboard/useRetainedAgentsSync.test.ts @@ -4,7 +4,10 @@ import { type AgentStatusEntry, type AgentStatusState } from '../../../../shared/agent-status-types' -import type { FolderWorkspace, Repo, TerminalTab, Worktree } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { Repo } from '../../../../shared/repo-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import { makePaneKey } from '../../../../shared/stable-pane-id' import { buildRetainedAgentsSyncSnapshot } from './useRetainedAgents' diff --git a/src/renderer/src/components/dictation/use-hold-dictation-gesture.test.tsx b/src/renderer/src/components/dictation/use-hold-dictation-gesture.test.tsx index ef83b076201..d71e608e525 100644 --- a/src/renderer/src/components/dictation/use-hold-dictation-gesture.test.tsx +++ b/src/renderer/src/components/dictation/use-hold-dictation-gesture.test.tsx @@ -4,7 +4,7 @@ import { createRef, type MutableRefObject, act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { DictationState } from '../../../../shared/speech-types' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { DictationInsertionTarget } from './dictation-insertion-target' import { useHoldDictationGesture } from './use-hold-dictation-gesture' diff --git a/src/renderer/src/components/dictation/use-hold-dictation-gesture.ts b/src/renderer/src/components/dictation/use-hold-dictation-gesture.ts index f1316b9a4ac..554af340c9c 100644 --- a/src/renderer/src/components/dictation/use-hold-dictation-gesture.ts +++ b/src/renderer/src/components/dictation/use-hold-dictation-gesture.ts @@ -2,7 +2,7 @@ import { useEffect, useRef, type MutableRefObject } from 'react' import { getShortcutPlatform } from '@/lib/shortcut-platform' import { keybindingMatchesAction, type KeybindingOverrides } from '../../../../shared/keybindings' import type { DictationState } from '../../../../shared/speech-types' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { DictationInsertionTarget } from './dictation-insertion-target' type HoldDictationGestureOptions = { diff --git a/src/renderer/src/components/diff-comments/useDiffCommentDecorator.tsx b/src/renderer/src/components/diff-comments/useDiffCommentDecorator.tsx index 2c3214070d9..833099b3ad4 100644 --- a/src/renderer/src/components/diff-comments/useDiffCommentDecorator.tsx +++ b/src/renderer/src/components/diff-comments/useDiffCommentDecorator.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react' import * as monaco from 'monaco-editor' import type { editor as monacoEditor, IDisposable } from 'monaco-editor' import { createRoot, type Root } from 'react-dom/client' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' import { getCommentBodyLayoutLineCount } from '@/lib/comment-body-line-count' import { getDiffCommentLineLabel } from '@/lib/diff-comment-compat' import { formatDiffComments } from '@/lib/diff-comments-format' diff --git a/src/renderer/src/components/editor/ChangesModeView.tsx b/src/renderer/src/components/editor/ChangesModeView.tsx index 9fac8364e80..2004f576d5c 100644 --- a/src/renderer/src/components/editor/ChangesModeView.tsx +++ b/src/renderer/src/components/editor/ChangesModeView.tsx @@ -1,7 +1,8 @@ import React from 'react' import { lazyWithRetry as lazy } from '@/lib/lazy-with-retry' import type { OpenFile } from '@/store/slices/editor' -import type { GitDiffResult, GitStatusEntry } from '../../../../shared/types' +import type { GitDiffResult } from '../../../../shared/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { ConflictBanner } from './ConflictComponents' import { getDiffContentSignature } from './diff-content-signature' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/editor/CheckRunAnnotations.tsx b/src/renderer/src/components/editor/CheckRunAnnotations.tsx index f7b53071508..5d7dfe0d878 100644 --- a/src/renderer/src/components/editor/CheckRunAnnotations.tsx +++ b/src/renderer/src/components/editor/CheckRunAnnotations.tsx @@ -1,7 +1,7 @@ import React from 'react' import { ExternalLink } from 'lucide-react' import { translate } from '@/i18n/i18n' -import type { PRCheckAnnotation } from '../../../../shared/types' +import type { PRCheckAnnotation } from '../../../../shared/github/check-types' import { cancelAnnotationRevealFrame, getOpenableAnnotationLine, diff --git a/src/renderer/src/components/editor/CheckRunDetailsPanel.tsx b/src/renderer/src/components/editor/CheckRunDetailsPanel.tsx index 6003be19e05..edf79624cc5 100644 --- a/src/renderer/src/components/editor/CheckRunDetailsPanel.tsx +++ b/src/renderer/src/components/editor/CheckRunDetailsPanel.tsx @@ -3,7 +3,7 @@ import { ExternalLink, LoaderCircle, RefreshCw } from 'lucide-react' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import CommentMarkdown from '@/components/sidebar/CommentMarkdown' -import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/types' +import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/github/check-types' import { SourceControlFixSplitButton } from '@/components/right-sidebar/source-control-fix-split-button' import { translate } from '@/i18n/i18n' import { useCheckRunDetailsFixWithAI } from './check-run-details-fix-with-ai' diff --git a/src/renderer/src/components/editor/CheckRunJobs.tsx b/src/renderer/src/components/editor/CheckRunJobs.tsx index 6b3bfa0816e..3fedd9f7569 100644 --- a/src/renderer/src/components/editor/CheckRunJobs.tsx +++ b/src/renderer/src/components/editor/CheckRunJobs.tsx @@ -3,7 +3,7 @@ import { CheckCircle2, ChevronDown, CircleDashed, MinusCircle, XCircle } from 'l import { CheckJobLogTail } from '@/components/right-sidebar/check-job-log-tail' import { translate } from '@/i18n/i18n' import { cn } from '@/lib/utils' -import type { PRCheckJob, PRCheckStep } from '../../../../shared/types' +import type { PRCheckJob, PRCheckStep } from '../../../../shared/github/check-types' import { resolveStepOutcome, summarizeJobSteps, type StepOutcome } from './check-job-step-status' function StepOutcomeIcon({ outcome }: { outcome: StepOutcome }): React.JSX.Element { diff --git a/src/renderer/src/components/editor/CombinedDiffFileTree.test.ts b/src/renderer/src/components/editor/CombinedDiffFileTree.test.ts index 97721b1b6b7..7a21281213d 100644 --- a/src/renderer/src/components/editor/CombinedDiffFileTree.test.ts +++ b/src/renderer/src/components/editor/CombinedDiffFileTree.test.ts @@ -11,7 +11,8 @@ import { getFilteredCombinedDiffFileTreeEntries, isCombinedDiffFileTreeQueryTooLarge } from './combined-diff-file-tree-model' -import type { GitBranchChangeEntry, GitStatusEntry } from '../../../../shared/types' +import type { GitBranchChangeEntry } from '../../../../shared/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' describe('CombinedDiffFileTree navigation mapping', () => { it('disambiguates uncommitted entries with the same path by area', () => { diff --git a/src/renderer/src/components/editor/CombinedDiffFileTree.tsx b/src/renderer/src/components/editor/CombinedDiffFileTree.tsx index fc59c2dd75c..99a53af8795 100644 --- a/src/renderer/src/components/editor/CombinedDiffFileTree.tsx +++ b/src/renderer/src/components/editor/CombinedDiffFileTree.tsx @@ -10,7 +10,8 @@ import { compactSourceControlTree, flattenSourceControlTree } from '@/components/right-sidebar/source-control-tree' -import type { GitBranchChangeEntry, GitStagingArea, GitStatusEntry } from '../../../../shared/types' +import type { GitBranchChangeEntry } from '../../../../shared/git-diff-compare-types' +import type { GitStagingArea, GitStatusEntry } from '../../../../shared/git-status-types' import { getEntryExtension, getFilteredCombinedDiffFileTreeEntries, diff --git a/src/renderer/src/components/editor/CombinedDiffViewer.tsx b/src/renderer/src/components/editor/CombinedDiffViewer.tsx index fd87b515349..b004ef3a799 100644 --- a/src/renderer/src/components/editor/CombinedDiffViewer.tsx +++ b/src/renderer/src/components/editor/CombinedDiffViewer.tsx @@ -42,12 +42,9 @@ import { import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import type { OpenFile } from '@/store/slices/editor' -import type { - DiffComment, - GitBranchChangeEntry, - GitDiffResult, - GitStatusEntry -} from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' +import type { GitBranchChangeEntry, GitDiffResult } from '../../../../shared/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { Check, Copy, MessageSquare, PanelLeftOpen, Sparkles, Trash2, WrapText } from 'lucide-react' import { toast } from 'sonner' import { DiffSectionItem } from './DiffSectionItem' diff --git a/src/renderer/src/components/editor/ConflictComponents.tsx b/src/renderer/src/components/editor/ConflictComponents.tsx index 32159e23f00..b89db1280ea 100644 --- a/src/renderer/src/components/editor/ConflictComponents.tsx +++ b/src/renderer/src/components/editor/ConflictComponents.tsx @@ -13,7 +13,7 @@ import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' import type { ConflictReviewEntry, OpenFile } from '@/store/slices/editor' -import type { GitConflictKind, GitStatusEntry } from '../../../../shared/types' +import type { GitConflictKind, GitStatusEntry } from '../../../../shared/git-status-types' import { ConflictReviewFileTree } from './ConflictReviewFileTree' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/editor/ConflictReviewFileTree.tsx b/src/renderer/src/components/editor/ConflictReviewFileTree.tsx index f3e5e02c425..6a3ce686694 100644 --- a/src/renderer/src/components/editor/ConflictReviewFileTree.tsx +++ b/src/renderer/src/components/editor/ConflictReviewFileTree.tsx @@ -10,7 +10,7 @@ import { type SourceControlTreeNode } from '@/components/right-sidebar/source-control-tree' import type { ConflictReviewEntry } from '@/store/slices/editor' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { translate } from '@/i18n/i18n' type ConflictReviewTreeEntry = ConflictReviewEntry & { diff --git a/src/renderer/src/components/editor/DiffNotesSendMenu.tsx b/src/renderer/src/components/editor/DiffNotesSendMenu.tsx index 8fc9daefbe8..8e49dceaf9f 100644 --- a/src/renderer/src/components/editor/DiffNotesSendMenu.tsx +++ b/src/renderer/src/components/editor/DiffNotesSendMenu.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useMemo } from 'react' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' import { useAppStore } from '@/store' import { formatDiffComments } from '@/lib/diff-comments-format' import { NotesSendMenu, type NotesSendMenuScope } from './NotesSendMenu' diff --git a/src/renderer/src/components/editor/DiffSectionItem.tsx b/src/renderer/src/components/editor/DiffSectionItem.tsx index c6931fe5240..22905daf464 100644 --- a/src/renderer/src/components/editor/DiffSectionItem.tsx +++ b/src/renderer/src/components/editor/DiffSectionItem.tsx @@ -13,7 +13,7 @@ import { } from '../diff-comments/diff-comment-popover-position' import { applyDiffEditorLineNumberOptions } from './diff-editor-line-number-options' import { DiffSectionHeader } from './DiffSectionHeader' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' import { isDiffComment } from '@/lib/diff-comment-compat' import { installEditorSaveShortcut, installMonacoEditorFindShortcut } from './editor-shortcuts' import { DiffSectionBody } from './DiffSectionBody' diff --git a/src/renderer/src/components/editor/DiffViewer.tsx b/src/renderer/src/components/editor/DiffViewer.tsx index e2f364a2c81..36b515b8f53 100644 --- a/src/renderer/src/components/editor/DiffViewer.tsx +++ b/src/renderer/src/components/editor/DiffViewer.tsx @@ -14,7 +14,7 @@ import { getDiffCommentPopoverTop } from '../diff-comments/diff-comment-popover-position' import { applyDiffEditorLineNumberOptions } from './diff-editor-line-number-options' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' import { isDiffComment } from '@/lib/diff-comment-compat' import { installEditorSaveShortcut, installMonacoEditorFindShortcut } from './editor-shortcuts' import { diffEditorScrollbarOptions } from './diff-editor-scrollbar-options' diff --git a/src/renderer/src/components/editor/EditorContent.tsx b/src/renderer/src/components/editor/EditorContent.tsx index a9da4e54065..01be65d3465 100644 --- a/src/renderer/src/components/editor/EditorContent.tsx +++ b/src/renderer/src/components/editor/EditorContent.tsx @@ -14,7 +14,8 @@ import { getNextConflictNavigationIndex } from './ConflictComponents' import type { MarkdownViewMode, OpenFile, PendingEditorReveal } from '@/store/slices/editor' -import type { GitStatusEntry, GitDiffResult } from '../../../../shared/types' +import type { GitDiffResult } from '../../../../shared/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { getMarkdownRenderMode } from './markdown-render-mode' import { getMarkdownRichModeUnsupportedMessage } from './markdown-rich-mode' import { exceedsMarkdownRichModeSizeLimit } from './markdown-rich-size-limit' diff --git a/src/renderer/src/components/editor/MarkdownPreview.test.ts b/src/renderer/src/components/editor/MarkdownPreview.test.ts index b6fd98c6467..12f29758a83 100644 --- a/src/renderer/src/components/editor/MarkdownPreview.test.ts +++ b/src/renderer/src/components/editor/MarkdownPreview.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { decodeMarkdownPreviewAnchor, deriveMarkdownPreviewSourceRoot, diff --git a/src/renderer/src/components/editor/MarkdownPreview.tsx b/src/renderer/src/components/editor/MarkdownPreview.tsx index fe73dc07858..519072c63c8 100644 --- a/src/renderer/src/components/editor/MarkdownPreview.tsx +++ b/src/renderer/src/components/editor/MarkdownPreview.tsx @@ -41,7 +41,9 @@ import { getConnectionIdForFile } from '@/lib/connection-context' import { createConnectionIdForFileSelector } from '@/lib/connection-owner-resolution' import { scrollTopCache, setWithLRU } from '@/lib/scroll-cache' import { detectLanguage } from '@/lib/language-detect' -import type { DiffComment, MarkdownDocument, Worktree } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' +import type { MarkdownDocument } from '../../../../shared/filesystem-entry-types' +import type { Worktree } from '../../../../shared/worktree/types' import { fileUrlToAbsolutePath, getMarkdownPreviewLinkTarget, diff --git a/src/renderer/src/components/editor/MonacoEditor.tsx b/src/renderer/src/components/editor/MonacoEditor.tsx index 15718697a54..ba1a967746b 100644 --- a/src/renderer/src/components/editor/MonacoEditor.tsx +++ b/src/renderer/src/components/editor/MonacoEditor.tsx @@ -4,7 +4,8 @@ import React, { useRef, useCallback, useEffect, useLayoutEffect, useMemo, useSta import Editor, { type OnMount } from '@monaco-editor/react' import type { editor } from 'monaco-editor' import { toast } from 'sonner' -import type { MarkdownDocument, DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' +import type { MarkdownDocument } from '../../../../shared/filesystem-entry-types' import { useAppStore } from '@/store' import { scrollTopCache, cursorPositionCache, setWithLRU } from '@/lib/scroll-cache' import '@/lib/monaco-setup' diff --git a/src/renderer/src/components/editor/ReviewNotesSendMenuContent.test.tsx b/src/renderer/src/components/editor/ReviewNotesSendMenuContent.test.tsx index 92e7e21da80..1ae74985923 100644 --- a/src/renderer/src/components/editor/ReviewNotesSendMenuContent.test.tsx +++ b/src/renderer/src/components/editor/ReviewNotesSendMenuContent.test.tsx @@ -1,7 +1,7 @@ import React from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { AgentStatusEntry, AgentStatusState } from '../../../../shared/agent-status-types' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { makePaneKey } from '../../../../shared/stable-pane-id' import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData' import { ReviewNotesSendMenuContent } from './ReviewNotesSendMenuContent' diff --git a/src/renderer/src/components/editor/RichMarkdownEditor.tsx b/src/renderer/src/components/editor/RichMarkdownEditor.tsx index 56eb1e9d728..6678cda3569 100644 --- a/src/renderer/src/components/editor/RichMarkdownEditor.tsx +++ b/src/renderer/src/components/editor/RichMarkdownEditor.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' import { useEditorState, type Editor } from '@tiptap/react' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' import { useAppStore } from '@/store' import { selectWorktreeDiffComments } from '@/store/worktree-diff-comments-selector' import { useLocalImagePick } from './useLocalImagePick' diff --git a/src/renderer/src/components/editor/RichMarkdownEditorSurface.tsx b/src/renderer/src/components/editor/RichMarkdownEditorSurface.tsx index d3a7b423daa..12bc5cc35c3 100644 --- a/src/renderer/src/components/editor/RichMarkdownEditorSurface.tsx +++ b/src/renderer/src/components/editor/RichMarkdownEditorSurface.tsx @@ -19,7 +19,7 @@ import type { NotesSendMenuScope } from './NotesSendMenu' import type { MarkdownReviewNote } from '@/lib/markdown-review-notes' import type { RichMarkdownAnnotationTarget } from './rich-markdown-review-annotations' import type { RichMarkdownReviewNotePosition } from './rich-markdown-review-note-layout' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' function shouldFocusEmptyEditorFromSurfaceClick( event: React.MouseEvent, diff --git a/src/renderer/src/components/editor/RichMarkdownReviewNoteLayer.tsx b/src/renderer/src/components/editor/RichMarkdownReviewNoteLayer.tsx index 881cef3ff1d..0aa90569f98 100644 --- a/src/renderer/src/components/editor/RichMarkdownReviewNoteLayer.tsx +++ b/src/renderer/src/components/editor/RichMarkdownReviewNoteLayer.tsx @@ -1,5 +1,5 @@ import { Check, Copy } from 'lucide-react' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' import { DiffCommentCard } from '../diff-comments/DiffCommentCard' import { NotesSendMenu } from './NotesSendMenu' import { diff --git a/src/renderer/src/components/editor/check-annotation-path.test.ts b/src/renderer/src/components/editor/check-annotation-path.test.ts index 95f23378473..b28fa77214a 100644 --- a/src/renderer/src/components/editor/check-annotation-path.test.ts +++ b/src/renderer/src/components/editor/check-annotation-path.test.ts @@ -3,7 +3,7 @@ import { getOpenableAnnotationLine, resolveAnnotationPathInsideWorktree } from './check-annotation-path' -import type { PRCheckAnnotation } from '../../../../shared/types' +import type { PRCheckAnnotation } from '../../../../shared/github/check-types' function annotation(path: string | null, startLine = 1): PRCheckAnnotation { return { diff --git a/src/renderer/src/components/editor/check-annotation-path.ts b/src/renderer/src/components/editor/check-annotation-path.ts index b18baea1475..4087d22669c 100644 --- a/src/renderer/src/components/editor/check-annotation-path.ts +++ b/src/renderer/src/components/editor/check-annotation-path.ts @@ -1,4 +1,4 @@ -import type { PRCheckAnnotation } from '../../../../shared/types' +import type { PRCheckAnnotation } from '../../../../shared/github/check-types' import { relativePathInsideRoot, resolveRuntimePath } from '../../../../shared/cross-platform-path' const WORKFLOW_PSEUDO_ANNOTATION_PATH = '.github' diff --git a/src/renderer/src/components/editor/check-job-step-status.ts b/src/renderer/src/components/editor/check-job-step-status.ts index a6d4d07007d..c81797818ae 100644 --- a/src/renderer/src/components/editor/check-job-step-status.ts +++ b/src/renderer/src/components/editor/check-job-step-status.ts @@ -1,4 +1,4 @@ -import type { PRCheckJob, PRCheckStep } from '../../../../shared/types' +import type { PRCheckJob, PRCheckStep } from '../../../../shared/github/check-types' export type StepOutcome = 'success' | 'failure' | 'skipped' | 'pending' diff --git a/src/renderer/src/components/editor/check-run-details-fix-context.ts b/src/renderer/src/components/editor/check-run-details-fix-context.ts index f0e99b12e2f..d794265ec60 100644 --- a/src/renderer/src/components/editor/check-run-details-fix-context.ts +++ b/src/renderer/src/components/editor/check-run-details-fix-context.ts @@ -10,7 +10,8 @@ import { getGitHubPRCacheKey } from '@/store/slices/github-cache-key' import { getHostedReviewCacheKey } from '@/store/slices/hosted-review-cache-identity' import { findWorktreeById } from '@/store/slices/worktree-helpers' import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { PRCheckDetail, PRCheckRunDetails, Repo } from '../../../../shared/types' +import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/github/check-types' +import type { Repo } from '../../../../shared/repo-types' import { translate } from '@/i18n/i18n' export function resolveCheckRunDetailsFixCheck( diff --git a/src/renderer/src/components/editor/check-run-details-fix-with-ai.test.ts b/src/renderer/src/components/editor/check-run-details-fix-with-ai.test.ts index 10531e2224a..ae46dc37187 100644 --- a/src/renderer/src/components/editor/check-run-details-fix-with-ai.test.ts +++ b/src/renderer/src/components/editor/check-run-details-fix-with-ai.test.ts @@ -1,11 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { - PRCheckDetail, - PRCheckRunDetails, - PRInfo, - Repo, - Worktree -} from '../../../../shared/types' +import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/github/check-types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getCheckRunDetailsFixDisabledReason, isCheckRunDetailsFixCandidate, diff --git a/src/renderer/src/components/editor/check-run-details-fix-with-ai.ts b/src/renderer/src/components/editor/check-run-details-fix-with-ai.ts index 80059758ca7..48cae27613a 100644 --- a/src/renderer/src/components/editor/check-run-details-fix-with-ai.ts +++ b/src/renderer/src/components/editor/check-run-details-fix-with-ai.ts @@ -24,7 +24,7 @@ import type { SourceControlActionRecipe, SourceControlLaunchActionId } from '../../../../shared/source-control-ai-actions' -import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/types' +import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/github/check-types' import { translate } from '@/i18n/i18n' export { diff --git a/src/renderer/src/components/editor/check-run-details-tab.ts b/src/renderer/src/components/editor/check-run-details-tab.ts index 02e833b61b6..b274b1564c0 100644 --- a/src/renderer/src/components/editor/check-run-details-tab.ts +++ b/src/renderer/src/components/editor/check-run-details-tab.ts @@ -1,9 +1,6 @@ import type { GitLabProjectRef } from '../../../../shared/gitlab-types' -import type { - GitHubRepositoryIdentity, - PRCheckDetail, - PRCheckRunDetails -} from '../../../../shared/types' +import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/github/check-types' +import type { GitHubRepositoryIdentity } from '../../../../shared/github/pull-request-types' export type OpenCheckRunDetailsState = { contextKey: string diff --git a/src/renderer/src/components/editor/combined-diff-entries.test.ts b/src/renderer/src/components/editor/combined-diff-entries.test.ts index c3ff00ecab0..312d6baa8c2 100644 --- a/src/renderer/src/components/editor/combined-diff-entries.test.ts +++ b/src/renderer/src/components/editor/combined-diff-entries.test.ts @@ -5,7 +5,8 @@ import { resolveCombinedUncommittedSnapshotEntries, shouldAutoReloadCombinedDiffFromGitStatus } from './combined-diff-entries' -import type { GitBranchChangeEntry, GitStatusEntry } from '../../../../shared/types' +import type { GitBranchChangeEntry } from '../../../../shared/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' describe('getCombinedUncommittedEntries', () => { it('filters unresolved conflicts from live entries', () => { diff --git a/src/renderer/src/components/editor/combined-diff-entries.ts b/src/renderer/src/components/editor/combined-diff-entries.ts index 73d2fe44a70..ab64df67d47 100644 --- a/src/renderer/src/components/editor/combined-diff-entries.ts +++ b/src/renderer/src/components/editor/combined-diff-entries.ts @@ -1,5 +1,6 @@ import type { OpenFile } from '@/store/slices/editor' -import type { GitBranchChangeEntry, GitStatusEntry } from '../../../../shared/types' +import type { GitBranchChangeEntry } from '../../../../shared/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import type { CombinedDiffFileTreeMode } from './combined-diff-file-tree-model' /** diff --git a/src/renderer/src/components/editor/combined-diff-file-tree-model.ts b/src/renderer/src/components/editor/combined-diff-file-tree-model.ts index 93d38853647..fc3c440fc95 100644 --- a/src/renderer/src/components/editor/combined-diff-file-tree-model.ts +++ b/src/renderer/src/components/editor/combined-diff-file-tree-model.ts @@ -1,5 +1,6 @@ import { basename } from '@/lib/path' -import type { GitBranchChangeEntry, GitStatusEntry } from '../../../../shared/types' +import type { GitBranchChangeEntry } from '../../../../shared/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { isClipboardTextByteLengthOverLimit } from '../../../../shared/clipboard-text' import { buildSourceControlTree, diff --git a/src/renderer/src/components/editor/combined-diff-file-tree-row.tsx b/src/renderer/src/components/editor/combined-diff-file-tree-row.tsx index 7f47909cb10..eb6dc11fbf0 100644 --- a/src/renderer/src/components/editor/combined-diff-file-tree-row.tsx +++ b/src/renderer/src/components/editor/combined-diff-file-tree-row.tsx @@ -6,12 +6,12 @@ import { getFileTypeIcon } from '@/lib/file-type-icons' import { basename, dirname, joinPath } from '@/lib/path' import { cn } from '@/lib/utils' import { WORKSPACE_FILE_PATH_MIME } from '@/lib/workspace-file-drag' +import type { GitBranchChangeEntry } from '../../../../shared/git-diff-compare-types' import type { - GitBranchChangeEntry, GitFileStatus, GitStagingArea, GitStatusEntry -} from '../../../../shared/types' +} from '../../../../shared/git-status-types' import { getCombinedDiffFileTreeSectionKey, type CombinedDiffBranchTreeArea, diff --git a/src/renderer/src/components/editor/combined-diff-section-cache-match.test.ts b/src/renderer/src/components/editor/combined-diff-section-cache-match.test.ts index 970a2155d00..1eea28d9d89 100644 --- a/src/renderer/src/components/editor/combined-diff-section-cache-match.test.ts +++ b/src/renderer/src/components/editor/combined-diff-section-cache-match.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { GitBranchChangeEntry, GitStatusEntry } from '../../../../shared/types' +import type { GitBranchChangeEntry } from '../../../../shared/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import type { DiffSection } from './diff-section-types' import { combinedDiffSectionsMatchEntryMetadata } from './combined-diff-section-cache-match' diff --git a/src/renderer/src/components/editor/combined-diff-section-cache-match.ts b/src/renderer/src/components/editor/combined-diff-section-cache-match.ts index a53e3a1aa43..7f25e96f7c3 100644 --- a/src/renderer/src/components/editor/combined-diff-section-cache-match.ts +++ b/src/renderer/src/components/editor/combined-diff-section-cache-match.ts @@ -1,4 +1,5 @@ -import type { GitBranchChangeEntry, GitStatusEntry } from '../../../../shared/types' +import type { GitBranchChangeEntry } from '../../../../shared/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import type { CombinedDiffFileTreeMode } from './combined-diff-file-tree-model' import { getCombinedDiffFileTreeSectionKey } from './combined-diff-file-tree-model' import type { DiffSection } from './diff-section-types' diff --git a/src/renderer/src/components/editor/combined-diff-section-connection.test.ts b/src/renderer/src/components/editor/combined-diff-section-connection.test.ts index 894f6df7830..89254a6f6b1 100644 --- a/src/renderer/src/components/editor/combined-diff-section-connection.test.ts +++ b/src/renderer/src/components/editor/combined-diff-section-connection.test.ts @@ -1,5 +1,7 @@ import { afterEach, describe, expect, it } from 'vitest' -import type { FolderWorkspace, ProjectGroup, Repo } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' import { useAppStore } from '@/store' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import { getCombinedDiffSectionConnectionId } from './combined-diff-section-connection' diff --git a/src/renderer/src/components/editor/combined-diff-section-load-state.test.ts b/src/renderer/src/components/editor/combined-diff-section-load-state.test.ts index 996e66c2f5a..0b647561738 100644 --- a/src/renderer/src/components/editor/combined-diff-section-load-state.test.ts +++ b/src/renderer/src/components/editor/combined-diff-section-load-state.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { GitDiffResult } from '../../../../shared/types' +import type { GitDiffResult } from '../../../../shared/git-diff-compare-types' import type { LargeDiffRenderLimit } from './large-diff-render-limit' import { isUnchangedDiffSectionReload, diff --git a/src/renderer/src/components/editor/diff-section-layout.test.ts b/src/renderer/src/components/editor/diff-section-layout.test.ts index 809ca80cab3..f5b4d4145f6 100644 --- a/src/renderer/src/components/editor/diff-section-layout.test.ts +++ b/src/renderer/src/components/editor/diff-section-layout.test.ts @@ -5,7 +5,7 @@ import { getDiffSectionEstimatedHeight, isIntrinsicHeightImageDiff } from './diff-section-layout' -import type { GitDiffResult } from '../../../../shared/types' +import type { GitDiffResult } from '../../../../shared/git-diff-compare-types' describe('diff section layout', () => { it('uses Monaco measured content height for text diffs', () => { diff --git a/src/renderer/src/components/editor/diff-section-layout.ts b/src/renderer/src/components/editor/diff-section-layout.ts index 17e642641ee..6fb4c6c46b0 100644 --- a/src/renderer/src/components/editor/diff-section-layout.ts +++ b/src/renderer/src/components/editor/diff-section-layout.ts @@ -1,4 +1,4 @@ -import type { GitDiffResult } from '../../../../shared/types' +import type { GitDiffResult } from '../../../../shared/git-diff-compare-types' import { countLinesLikeSplit, type DiffLineCounts } from './large-diff-render-limit' const DIFF_LINE_HEIGHT = 19 diff --git a/src/renderer/src/components/editor/diff-section-types.ts b/src/renderer/src/components/editor/diff-section-types.ts index 0c5d659f4be..fe2d23bf273 100644 --- a/src/renderer/src/components/editor/diff-section-types.ts +++ b/src/renderer/src/components/editor/diff-section-types.ts @@ -1,4 +1,5 @@ -import type { GitDiffResult, GitStatusEntry } from '../../../../shared/types' +import type { GitDiffResult } from '../../../../shared/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import type { LargeDiffRenderLimit } from './large-diff-render-limit' export type DiffSection = { diff --git a/src/renderer/src/components/editor/editor-cmd-save-target.test.ts b/src/renderer/src/components/editor/editor-cmd-save-target.test.ts index 518f8c8b859..f53e6c5f1f8 100644 --- a/src/renderer/src/components/editor/editor-cmd-save-target.test.ts +++ b/src/renderer/src/components/editor/editor-cmd-save-target.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' -import type { Tab } from '../../../../shared/types' +import type { Tab } from '../../../../shared/tab-types' import { getEditorCmdSaveFileId } from './editor-cmd-save-target' function makeTab(contentType: Tab['contentType'], entityId: string): Tab { diff --git a/src/renderer/src/components/editor/editor-cmd-save-target.ts b/src/renderer/src/components/editor/editor-cmd-save-target.ts index 0d021a87790..75159e0dbff 100644 --- a/src/renderer/src/components/editor/editor-cmd-save-target.ts +++ b/src/renderer/src/components/editor/editor-cmd-save-target.ts @@ -1,5 +1,6 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' -import type { Tab, TabContentType, TopLevelView } from '../../../../shared/types' +import type { Tab, TabContentType } from '../../../../shared/tab-types' +import type { TopLevelView } from '../../../../shared/ui-chrome-types' export const EDITOR_TAB_CONTENT_TYPES = new Set([ 'editor', diff --git a/src/renderer/src/components/editor/editor-external-watch-path-index.ts b/src/renderer/src/components/editor/editor-external-watch-path-index.ts index f1cb7054c67..3ce5f2a432b 100644 --- a/src/renderer/src/components/editor/editor-external-watch-path-index.ts +++ b/src/renderer/src/components/editor/editor-external-watch-path-index.ts @@ -1,7 +1,7 @@ import { joinPath } from '@/lib/path' import { getExternalFileChangeRelativePath } from '@/components/right-sidebar/useFileExplorerWatch' import type { OpenFile } from '@/store/slices/editor' -import type { FsChangedPayload } from '../../../../shared/types' +import type { FsChangedPayload } from '../../../../shared/filesystem-entry-types' import { getLocalWindowsWslPathIdentity, normalizeRuntimePathForComparison, diff --git a/src/renderer/src/components/editor/editor-header.ts b/src/renderer/src/components/editor/editor-header.ts index a3fbafbd6da..c511d401d3b 100644 --- a/src/renderer/src/components/editor/editor-header.ts +++ b/src/renderer/src/components/editor/editor-header.ts @@ -1,5 +1,6 @@ import type { OpenFile } from '@/store/slices/editor' -import type { GitBranchChangeEntry, GitStatusEntry } from '../../../../shared/types' +import type { GitBranchChangeEntry } from '../../../../shared/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { getEditorDisplayLabel } from './editor-labels' export type EditorHeaderCopyState = { diff --git a/src/renderer/src/components/editor/editor-panel-content-types.ts b/src/renderer/src/components/editor/editor-panel-content-types.ts index 1c137bfb632..dcbccfbc640 100644 --- a/src/renderer/src/components/editor/editor-panel-content-types.ts +++ b/src/renderer/src/components/editor/editor-panel-content-types.ts @@ -1,4 +1,4 @@ -import type { GitDiffResult } from '../../../../shared/types' +import type { GitDiffResult } from '../../../../shared/git-diff-compare-types' /** * Thrown when a worktree's host owner is not yet known (the backing repo has diff --git a/src/renderer/src/components/editor/editor-panel-diff-reload.test.ts b/src/renderer/src/components/editor/editor-panel-diff-reload.test.ts index 2463ce680d8..35a3d32e20c 100644 --- a/src/renderer/src/components/editor/editor-panel-diff-reload.test.ts +++ b/src/renderer/src/components/editor/editor-panel-diff-reload.test.ts @@ -4,7 +4,7 @@ import { isReloadableSingleFileDiffTab, shouldReloadDiffOnGitStatusChange } from './editor-panel-diff-reload' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' function makeDiffFile(overrides: Partial = {}): OpenFile { return { diff --git a/src/renderer/src/components/editor/editor-panel-diff-reload.ts b/src/renderer/src/components/editor/editor-panel-diff-reload.ts index 4b58a471239..8bcf59ab38b 100644 --- a/src/renderer/src/components/editor/editor-panel-diff-reload.ts +++ b/src/renderer/src/components/editor/editor-panel-diff-reload.ts @@ -1,5 +1,5 @@ import type { OpenFile } from '@/store/slices/editor' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' export function isReloadableSingleFileDiffTab(file: OpenFile): boolean { return ( diff --git a/src/renderer/src/components/editor/editor-panel-git-entry-selector.test.ts b/src/renderer/src/components/editor/editor-panel-git-entry-selector.test.ts index 524776853d5..0dedf058495 100644 --- a/src/renderer/src/components/editor/editor-panel-git-entry-selector.test.ts +++ b/src/renderer/src/components/editor/editor-panel-git-entry-selector.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { GitBranchChangeEntry, GitStatusEntry } from '../../../../shared/types' +import type { GitBranchChangeEntry } from '../../../../shared/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { selectEditorPanelGitBranchEntries, selectEditorPanelGitStatusEntries diff --git a/src/renderer/src/components/editor/editor-panel-render-model.test.ts b/src/renderer/src/components/editor/editor-panel-render-model.test.ts index fc7e66a78ea..1475ce35bf3 100644 --- a/src/renderer/src/components/editor/editor-panel-render-model.test.ts +++ b/src/renderer/src/components/editor/editor-panel-render-model.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import type { OpenFile } from '@/store/slices/editor' import { RICH_MARKDOWN_MAX_SIZE_BYTES } from '../../../../shared/constants' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import type { FileContent } from './editor-panel-content-types' import { getEditorPanelRenderModel } from './editor-panel-render-model' diff --git a/src/renderer/src/components/editor/large-diff-section-content.ts b/src/renderer/src/components/editor/large-diff-section-content.ts index df60ada45ac..f83ef383bcc 100644 --- a/src/renderer/src/components/editor/large-diff-section-content.ts +++ b/src/renderer/src/components/editor/large-diff-section-content.ts @@ -1,4 +1,4 @@ -import type { GitDiffResult } from '../../../../shared/types' +import type { GitDiffResult } from '../../../../shared/git-diff-compare-types' import type { LargeDiffRenderLimit } from './large-diff-render-limit' export function shouldPruneLargeDiffContent( diff --git a/src/renderer/src/components/editor/markdown-doc-completions.test.ts b/src/renderer/src/components/editor/markdown-doc-completions.test.ts index 9a6a8278af5..a08261e2ff6 100644 --- a/src/renderer/src/components/editor/markdown-doc-completions.test.ts +++ b/src/renderer/src/components/editor/markdown-doc-completions.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { MarkdownDocument } from '../../../../shared/types' +import type { MarkdownDocument } from '../../../../shared/filesystem-entry-types' import { getMarkdownDocCompletionContext, getMarkdownDocCompletionDocuments, diff --git a/src/renderer/src/components/editor/markdown-doc-completions.ts b/src/renderer/src/components/editor/markdown-doc-completions.ts index c41965ad96a..b7b81a72eee 100644 --- a/src/renderer/src/components/editor/markdown-doc-completions.ts +++ b/src/renderer/src/components/editor/markdown-doc-completions.ts @@ -1,4 +1,4 @@ -import type { MarkdownDocument } from '../../../../shared/types' +import type { MarkdownDocument } from '../../../../shared/filesystem-entry-types' import { isClipboardTextByteLengthOverLimit } from '../../../../shared/clipboard-text' export type MarkdownDocCompletionContext = { diff --git a/src/renderer/src/components/editor/markdown-doc-links.test.ts b/src/renderer/src/components/editor/markdown-doc-links.test.ts index d5cfaade6e7..9a7f9859b83 100644 --- a/src/renderer/src/components/editor/markdown-doc-links.test.ts +++ b/src/renderer/src/components/editor/markdown-doc-links.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import React from 'react' import { renderToStaticMarkup } from 'react-dom/server' import Markdown from 'react-markdown' -import type { MarkdownDocument } from '../../../../shared/types' +import type { MarkdownDocument } from '../../../../shared/filesystem-entry-types' import { createMarkdownDocumentIndex, createMarkdownDocLinkHref, diff --git a/src/renderer/src/components/editor/markdown-doc-links.ts b/src/renderer/src/components/editor/markdown-doc-links.ts index 82cd7133b38..3f5e4f7f6af 100644 --- a/src/renderer/src/components/editor/markdown-doc-links.ts +++ b/src/renderer/src/components/editor/markdown-doc-links.ts @@ -1,4 +1,4 @@ -import type { MarkdownDocument } from '../../../../shared/types' +import type { MarkdownDocument } from '../../../../shared/filesystem-entry-types' import { slugMarkdownHeading } from './markdown-heading-slug' export const MARKDOWN_DOC_LINK_PREFIX = '#orca-doc-link=' diff --git a/src/renderer/src/components/editor/markdown-document-list-request.test.ts b/src/renderer/src/components/editor/markdown-document-list-request.test.ts index 085b8599fb8..e94501a9140 100644 --- a/src/renderer/src/components/editor/markdown-document-list-request.test.ts +++ b/src/renderer/src/components/editor/markdown-document-list-request.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { MarkdownDocument } from '../../../../shared/types' +import type { MarkdownDocument } from '../../../../shared/filesystem-entry-types' import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client' import { getMarkdownDocumentListRequestKey, diff --git a/src/renderer/src/components/editor/markdown-document-list-request.ts b/src/renderer/src/components/editor/markdown-document-list-request.ts index 7e9cc0f32b3..a97ada9abbf 100644 --- a/src/renderer/src/components/editor/markdown-document-list-request.ts +++ b/src/renderer/src/components/editor/markdown-document-list-request.ts @@ -1,4 +1,4 @@ -import type { MarkdownDocument } from '../../../../shared/types' +import type { MarkdownDocument } from '../../../../shared/filesystem-entry-types' import { listRuntimeMarkdownDocuments, type RuntimeFileOperationArgs diff --git a/src/renderer/src/components/editor/markdown-document-worktree-path-selector.test.ts b/src/renderer/src/components/editor/markdown-document-worktree-path-selector.test.ts index b8b19f085e8..20a387648db 100644 --- a/src/renderer/src/components/editor/markdown-document-worktree-path-selector.test.ts +++ b/src/renderer/src/components/editor/markdown-document-worktree-path-selector.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { findWorktreeById } from '@/store/slices/worktree-helpers' import { selectMarkdownDocumentWorktreePath } from './markdown-document-worktree-path-selector' diff --git a/src/renderer/src/components/editor/monaco-markdown-doc-completions.ts b/src/renderer/src/components/editor/monaco-markdown-doc-completions.ts index 4d33a1f3985..3f6861c7897 100644 --- a/src/renderer/src/components/editor/monaco-markdown-doc-completions.ts +++ b/src/renderer/src/components/editor/monaco-markdown-doc-completions.ts @@ -1,6 +1,6 @@ import type { OnMount } from '@monaco-editor/react' import type { IDisposable } from 'monaco-editor' -import type { MarkdownDocument } from '../../../../shared/types' +import type { MarkdownDocument } from '../../../../shared/filesystem-entry-types' import { getMarkdownDocCompletionContext, getMarkdownDocCompletionDocuments diff --git a/src/renderer/src/components/editor/rich-markdown-annotation-submit-highlights.ts b/src/renderer/src/components/editor/rich-markdown-annotation-submit-highlights.ts index fba5d07bdc4..973b2198c40 100644 --- a/src/renderer/src/components/editor/rich-markdown-annotation-submit-highlights.ts +++ b/src/renderer/src/components/editor/rich-markdown-annotation-submit-highlights.ts @@ -1,5 +1,5 @@ import type { Editor } from '@tiptap/react' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' import { richMarkdownAnnotationHighlightPluginKey } from './rich-markdown-annotation-highlight' import { getRichMarkdownAnnotationHighlightRanges, diff --git a/src/renderer/src/components/editor/rich-markdown-commands.tsx b/src/renderer/src/components/editor/rich-markdown-commands.tsx index 5da14987ab4..607885d43e0 100644 --- a/src/renderer/src/components/editor/rich-markdown-commands.tsx +++ b/src/renderer/src/components/editor/rich-markdown-commands.tsx @@ -1,6 +1,6 @@ import type React from 'react' import type { Editor } from '@tiptap/react' -import type { MarkdownDocument } from '../../../../shared/types' +import type { MarkdownDocument } from '../../../../shared/filesystem-entry-types' import { stripMarkdownExtension } from './markdown-doc-links' export type DocLinkMenuState = { diff --git a/src/renderer/src/components/editor/rich-markdown-doc-link.ts b/src/renderer/src/components/editor/rich-markdown-doc-link.ts index bbc1b26b294..e2245ec472a 100644 --- a/src/renderer/src/components/editor/rich-markdown-doc-link.ts +++ b/src/renderer/src/components/editor/rich-markdown-doc-link.ts @@ -1,7 +1,7 @@ import { Node } from '@tiptap/core' import { type EditorState, Plugin, PluginKey, TextSelection } from '@tiptap/pm/state' import { Decoration, DecorationSet } from '@tiptap/pm/view' -import type { MarkdownDocument } from '../../../../shared/types' +import type { MarkdownDocument } from '../../../../shared/filesystem-entry-types' import type { MarkdownDocumentIndex } from './markdown-doc-links' import { createMarkdownDocumentIndex, diff --git a/src/renderer/src/components/editor/rich-markdown-editor-click-routing.ts b/src/renderer/src/components/editor/rich-markdown-editor-click-routing.ts index 9078f0e769d..3555330fac3 100644 --- a/src/renderer/src/components/editor/rich-markdown-editor-click-routing.ts +++ b/src/renderer/src/components/editor/rich-markdown-editor-click-routing.ts @@ -11,7 +11,7 @@ import { } from './markdown-internal-links' import { scrollToAnchorInEditor } from './markdown-anchor-scroll' import { getRichMarkdownCommentAtPos } from './rich-markdown-review-annotations' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' import { translate } from '@/i18n/i18n' import { classifyHtmlSuperscriptLinkAction, diff --git a/src/renderer/src/components/editor/rich-markdown-editor-config.ts b/src/renderer/src/components/editor/rich-markdown-editor-config.ts index 275720cb5ab..7fd8385eeb8 100644 --- a/src/renderer/src/components/editor/rich-markdown-editor-config.ts +++ b/src/renderer/src/components/editor/rich-markdown-editor-config.ts @@ -29,7 +29,7 @@ import { } from './rich-markdown-image-context' import { getRichMarkdownSpellcheckAttribute } from './rich-markdown-spellcheck' import type { MutableRefObject, Dispatch, SetStateAction } from 'react' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' import type { RichMarkdownEditorCodec } from './rich-markdown-source-transport' import type { RichMarkdownHtmlSuperscriptLinkContext } from './rich-markdown-html-superscript-link-context' import { diff --git a/src/renderer/src/components/editor/rich-markdown-editor-props.ts b/src/renderer/src/components/editor/rich-markdown-editor-props.ts index 04a20c13c75..93ed3310212 100644 --- a/src/renderer/src/components/editor/rich-markdown-editor-props.ts +++ b/src/renderer/src/components/editor/rich-markdown-editor-props.ts @@ -1,5 +1,5 @@ import type React from 'react' -import type { MarkdownDocument } from '../../../../shared/types' +import type { MarkdownDocument } from '../../../../shared/filesystem-entry-types' export type RichMarkdownEditorProps = { fileId: string diff --git a/src/renderer/src/components/editor/rich-markdown-html-superscript-link.test.ts b/src/renderer/src/components/editor/rich-markdown-html-superscript-link.test.ts index 90591cfdb17..8e3b4dead62 100644 --- a/src/renderer/src/components/editor/rich-markdown-html-superscript-link.test.ts +++ b/src/renderer/src/components/editor/rich-markdown-html-superscript-link.test.ts @@ -29,7 +29,7 @@ import { handleRichMarkdownCitationKey } from './rich-markdown-citation-keyboard import { resolveRichMarkdownWorktreeRoot } from './useRichMarkdownSuperscriptLinkSetup' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import type { AppState } from '@/store/types' -import type { FolderWorkspace } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' import { inspectRichMarkdownSourceOwningSlice, RICH_MARKDOWN_SOURCE_OWNING_PASTE_LIMIT diff --git a/src/renderer/src/components/editor/rich-markdown-review-annotations.test.ts b/src/renderer/src/components/editor/rich-markdown-review-annotations.test.ts index 95c1fa67b8d..28688d73b59 100644 --- a/src/renderer/src/components/editor/rich-markdown-review-annotations.test.ts +++ b/src/renderer/src/components/editor/rich-markdown-review-annotations.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import type { Editor } from '@tiptap/core' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' import { countRichMarkdownReviewMarkdownLines, getRichMarkdownAnnotationButtonLeft, diff --git a/src/renderer/src/components/editor/rich-markdown-review-annotations.ts b/src/renderer/src/components/editor/rich-markdown-review-annotations.ts index a7ef2307401..619d128bb65 100644 --- a/src/renderer/src/components/editor/rich-markdown-review-annotations.ts +++ b/src/renderer/src/components/editor/rich-markdown-review-annotations.ts @@ -1,7 +1,7 @@ import type { Dispatch, SetStateAction } from 'react' import type { Editor } from '@tiptap/react' import type { JSONContent } from '@tiptap/core' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' import type { RichMarkdownAnnotationHighlightRange } from './rich-markdown-annotation-highlight' import { getRichMarkdownLineRangeFromBlocks, diff --git a/src/renderer/src/components/editor/rich-markdown-review-note-layout.test.ts b/src/renderer/src/components/editor/rich-markdown-review-note-layout.test.ts index 98e47f5214e..4327f65d007 100644 --- a/src/renderer/src/components/editor/rich-markdown-review-note-layout.test.ts +++ b/src/renderer/src/components/editor/rich-markdown-review-note-layout.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' import { shouldExpandRichMarkdownReviewRail, stackRichMarkdownReviewNotePositions diff --git a/src/renderer/src/components/editor/rich-markdown-review-note-layout.ts b/src/renderer/src/components/editor/rich-markdown-review-note-layout.ts index 1a24de6dd21..7f8cab5244e 100644 --- a/src/renderer/src/components/editor/rich-markdown-review-note-layout.ts +++ b/src/renderer/src/components/editor/rich-markdown-review-note-layout.ts @@ -1,4 +1,4 @@ -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' import { getCommentBodyLayoutLineCount } from '@/lib/comment-body-line-count' export type RichMarkdownReviewNotePosition = { diff --git a/src/renderer/src/components/editor/rich-markdown-review-note-positioning.ts b/src/renderer/src/components/editor/rich-markdown-review-note-positioning.ts index d458312f6a1..c54996d0ffa 100644 --- a/src/renderer/src/components/editor/rich-markdown-review-note-positioning.ts +++ b/src/renderer/src/components/editor/rich-markdown-review-note-positioning.ts @@ -1,5 +1,5 @@ import type { Editor } from '@tiptap/react' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' import { buildRichMarkdownCommentBlocks, getRichMarkdownCommentAnchorTop diff --git a/src/renderer/src/components/editor/useEditorPanelContentState.test.tsx b/src/renderer/src/components/editor/useEditorPanelContentState.test.tsx index e72ff75858e..14b70465087 100644 --- a/src/renderer/src/components/editor/useEditorPanelContentState.test.tsx +++ b/src/renderer/src/components/editor/useEditorPanelContentState.test.tsx @@ -4,7 +4,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { OpenFile } from '@/store/slices/editor' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import type { DiffContent, FileContent } from './editor-panel-content-types' const mocks = vi.hoisted(() => ({ diff --git a/src/renderer/src/components/editor/useEditorPanelVisibilityContentState.test.tsx b/src/renderer/src/components/editor/useEditorPanelVisibilityContentState.test.tsx index 560bc217d86..317fa625779 100644 --- a/src/renderer/src/components/editor/useEditorPanelVisibilityContentState.test.tsx +++ b/src/renderer/src/components/editor/useEditorPanelVisibilityContentState.test.tsx @@ -4,7 +4,7 @@ import { act, useLayoutEffect, useMemo } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { OpenFile } from '@/store/slices/editor' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import type { DiffContent, FileContent } from './editor-panel-content-types' const mocks = vi.hoisted(() => ({ diff --git a/src/renderer/src/components/editor/useMarkdownDocuments.ts b/src/renderer/src/components/editor/useMarkdownDocuments.ts index 5e8e5a17fb8..40694193c75 100644 --- a/src/renderer/src/components/editor/useMarkdownDocuments.ts +++ b/src/renderer/src/components/editor/useMarkdownDocuments.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import type { MarkdownDocument } from '../../../../shared/types' +import type { MarkdownDocument } from '../../../../shared/filesystem-entry-types' import { useAppStore } from '@/store' import { getConnectionId } from '@/lib/connection-context' import { statRuntimePath } from '@/runtime/runtime-file-client' diff --git a/src/renderer/src/components/editor/useRichMarkdownMenuController.ts b/src/renderer/src/components/editor/useRichMarkdownMenuController.ts index d62a47d0308..6d73e618d03 100644 --- a/src/renderer/src/components/editor/useRichMarkdownMenuController.ts +++ b/src/renderer/src/components/editor/useRichMarkdownMenuController.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo, useRef, useState } from 'react' import type React from 'react' -import type { MarkdownDocument } from '../../../../shared/types' +import type { MarkdownDocument } from '../../../../shared/filesystem-entry-types' import { getMarkdownDocCompletionDocuments } from './markdown-doc-completions' import type { DocLinkMenuRow, DocLinkMenuState } from './rich-markdown-commands' import { filterRichMarkdownSlashCommands } from './rich-markdown-slash-command-filter' diff --git a/src/renderer/src/components/editor/useRichMarkdownProgrammaticSync.ts b/src/renderer/src/components/editor/useRichMarkdownProgrammaticSync.ts index 585cec52c0b..6094570d39e 100644 --- a/src/renderer/src/components/editor/useRichMarkdownProgrammaticSync.ts +++ b/src/renderer/src/components/editor/useRichMarkdownProgrammaticSync.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react' import type { Dispatch, MutableRefObject, SetStateAction } from 'react' import type { Editor } from '@tiptap/react' -import type { MarkdownDocument } from '../../../../shared/types' +import type { MarkdownDocument } from '../../../../shared/filesystem-entry-types' import { encodeRawMarkdownHtmlForRichEditor } from './raw-markdown-html' import { syncDocLinkMenu, type DocLinkMenuState } from './rich-markdown-commands' import { normalizeEmptyListItems } from './rich-markdown-normalize' diff --git a/src/renderer/src/components/editor/useRichMarkdownReviewController.ts b/src/renderer/src/components/editor/useRichMarkdownReviewController.ts index cc4e828bc94..f78b403e1c1 100644 --- a/src/renderer/src/components/editor/useRichMarkdownReviewController.ts +++ b/src/renderer/src/components/editor/useRichMarkdownReviewController.ts @@ -15,7 +15,7 @@ import { flushPendingProseMirrorSelection } from './rich-markdown-selection-flus import { useRichMarkdownReviewData } from './useRichMarkdownReviewData' import { useRichMarkdownReviewCopyFeedback } from './useRichMarkdownReviewCopyFeedback' import { useRichMarkdownReviewRailController } from './useRichMarkdownReviewRailController' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' type UseRichMarkdownReviewControllerOptions = { addDiffComment: AppState['addDiffComment'] diff --git a/src/renderer/src/components/editor/useRichMarkdownReviewData.ts b/src/renderer/src/components/editor/useRichMarkdownReviewData.ts index e0dc9286034..458dce00035 100644 --- a/src/renderer/src/components/editor/useRichMarkdownReviewData.ts +++ b/src/renderer/src/components/editor/useRichMarkdownReviewData.ts @@ -7,7 +7,7 @@ import { type MarkdownReviewNote } from '@/lib/markdown-review-notes' import type { NotesSendMenuScope } from './NotesSendMenu' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' import { translate } from '@/i18n/i18n' type UseRichMarkdownReviewDataOptions = { diff --git a/src/renderer/src/components/editor/useRichMarkdownReviewEditorEffects.ts b/src/renderer/src/components/editor/useRichMarkdownReviewEditorEffects.ts index b79cb031753..7a8abdec3c9 100644 --- a/src/renderer/src/components/editor/useRichMarkdownReviewEditorEffects.ts +++ b/src/renderer/src/components/editor/useRichMarkdownReviewEditorEffects.ts @@ -2,7 +2,7 @@ import { useEffect, type RefObject } from 'react' import type { Editor } from '@tiptap/react' import { richMarkdownAnnotationHighlightPluginKey } from './rich-markdown-annotation-highlight' import { getRichMarkdownAnnotationHighlightRanges } from './rich-markdown-review-annotations' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' type UseRichMarkdownReviewEditorEffectsOptions = { canAnnotateRichMarkdown: boolean diff --git a/src/renderer/src/components/editor/useRichMarkdownReviewRailController.ts b/src/renderer/src/components/editor/useRichMarkdownReviewRailController.ts index 0004f3aab87..45603510138 100644 --- a/src/renderer/src/components/editor/useRichMarkdownReviewRailController.ts +++ b/src/renderer/src/components/editor/useRichMarkdownReviewRailController.ts @@ -12,7 +12,7 @@ import { } from './rich-markdown-review-annotations' import type { RichMarkdownReviewNotePosition } from './rich-markdown-review-note-layout' import { measureRichMarkdownReviewNotePositions } from './rich-markdown-review-note-positioning' -import type { DiffComment } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' type UseRichMarkdownReviewRailControllerOptions = { canAnnotateRichMarkdown: boolean diff --git a/src/renderer/src/components/emulator-pane/EmulatorPane.tsx b/src/renderer/src/components/emulator-pane/EmulatorPane.tsx index 476c79e232d..17b5de71fc3 100644 --- a/src/renderer/src/components/emulator-pane/EmulatorPane.tsx +++ b/src/renderer/src/components/emulator-pane/EmulatorPane.tsx @@ -1,4 +1,4 @@ -import type { Tab } from '../../../../shared/types' +import type { Tab } from '../../../../shared/tab-types' import { EmulatorPaneToolbar } from './emulator-pane-toolbar' import { EmulatorDeviceFrame } from './emulator-device-frame' import { MobileEmulatorAgentSetupGuideLayer } from './MobileEmulatorAgentSetupGuideLayer' diff --git a/src/renderer/src/components/emulator-pane/EmulatorPaneOverlayLayer.tsx b/src/renderer/src/components/emulator-pane/EmulatorPaneOverlayLayer.tsx index 8d15f611003..410d9d3ac66 100644 --- a/src/renderer/src/components/emulator-pane/EmulatorPaneOverlayLayer.tsx +++ b/src/renderer/src/components/emulator-pane/EmulatorPaneOverlayLayer.tsx @@ -1,7 +1,7 @@ import { memo, useCallback, useMemo } from 'react' import { useShallow } from 'zustand/react/shallow' import { useAppStore } from '@/store' -import type { Tab, TabGroup } from '../../../../shared/types' +import type { Tab, TabGroup } from '../../../../shared/tab-types' import EmulatorPane from './EmulatorPane' import { tabGroupBodyAnchorName } from '../tab-group/tab-group-body-anchor' diff --git a/src/renderer/src/components/feature-tips/feature-tip-modal-state.test.ts b/src/renderer/src/components/feature-tips/feature-tip-modal-state.test.ts index a2e39a00841..c224d5e1c8c 100644 --- a/src/renderer/src/components/feature-tips/feature-tip-modal-state.test.ts +++ b/src/renderer/src/components/feature-tips/feature-tip-modal-state.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { getDefaultVoiceSettings } from '../../../../shared/constants' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { getFeatureTipForModal } from './feature-tip-modal-state' function makeSettings(voiceEnabled = false): Pick { diff --git a/src/renderer/src/components/feature-tips/feature-tip-modal-state.ts b/src/renderer/src/components/feature-tips/feature-tip-modal-state.ts index ca54215e73d..780b40894b1 100644 --- a/src/renderer/src/components/feature-tips/feature-tip-modal-state.ts +++ b/src/renderer/src/components/feature-tips/feature-tip-modal-state.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { FeatureInteractionState } from '../../../../shared/feature-interactions' import { FEATURE_TIPS, diff --git a/src/renderer/src/components/feature-tips/feature-tip-startup-gate.test.ts b/src/renderer/src/components/feature-tips/feature-tip-startup-gate.test.ts index 99b281aab47..cac13f108fd 100644 --- a/src/renderer/src/components/feature-tips/feature-tip-startup-gate.test.ts +++ b/src/renderer/src/components/feature-tips/feature-tip-startup-gate.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from 'vitest' import { getDefaultOnboardingState, getDefaultVoiceSettings } from '../../../../shared/constants' import type { CliInstallStatus } from '../../../../shared/cli-install-types' -import type { GlobalSettings, OnboardingState } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { OnboardingState } from '../../../../shared/onboarding-state-types' import { getFeatureTipsAppOpenDecision, isCliFeatureTipCompleted } from './feature-tip-startup-gate' const existingUserOnboarding: OnboardingState = { diff --git a/src/renderer/src/components/feature-tips/feature-tip-startup-gate.ts b/src/renderer/src/components/feature-tips/feature-tip-startup-gate.ts index a7eef0b8ead..4ad80028fd8 100644 --- a/src/renderer/src/components/feature-tips/feature-tip-startup-gate.ts +++ b/src/renderer/src/components/feature-tips/feature-tip-startup-gate.ts @@ -5,7 +5,8 @@ import { } from '../../../../shared/feature-tips' import type { CliInstallStatus } from '../../../../shared/cli-install-types' import type { FeatureInteractionState } from '../../../../shared/feature-interactions' -import type { GlobalSettings, OnboardingState } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { OnboardingState } from '../../../../shared/onboarding-state-types' import { shouldShowOnboarding } from '../onboarding/should-show-onboarding' export type FeatureTipsAppOpenDecision = diff --git a/src/renderer/src/components/feature-wall/AiCommitPrSettingsFields.tsx b/src/renderer/src/components/feature-wall/AiCommitPrSettingsFields.tsx index 52460dbadbc..7560b4a6cb3 100644 --- a/src/renderer/src/components/feature-wall/AiCommitPrSettingsFields.tsx +++ b/src/renderer/src/components/feature-wall/AiCommitPrSettingsFields.tsx @@ -7,7 +7,7 @@ import { type CommitMessageAgentCapability, type CommitMessageModelCapability } from '../../../../shared/commit-message-agent-spec' -import type { CommitMessageAiSettings } from '../../../../shared/types' +import type { CommitMessageAiSettings } from '../../../../shared/commit-message-ai-types' import { AgentIcon } from '@/lib/agent-catalog' import { cn } from '@/lib/utils' import { Input } from '../ui/input' diff --git a/src/renderer/src/components/feature-wall/FeatureWallBody.tsx b/src/renderer/src/components/feature-wall/FeatureWallBody.tsx index 0eb0314d89e..b08fc2f669c 100644 --- a/src/renderer/src/components/feature-wall/FeatureWallBody.tsx +++ b/src/renderer/src/components/feature-wall/FeatureWallBody.tsx @@ -4,7 +4,7 @@ import type { FeatureWallOpenSourceTelemetry } from '../../../../shared/telemetr import type { AgentsStep } from '../../../../shared/agents-orchestration-steps' import type { WorkbenchStep } from '../../../../shared/workbench-steps' import type { ReviewStep } from '../../../../shared/review-steps' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { InstalledAgentSkillState } from '@/hooks/useInstalledAgentSkills' import { cn } from '@/lib/utils' import { PreviewMedia, RelatedFeatures } from './FeatureWallPreview' diff --git a/src/renderer/src/components/feature-wall/FeatureWallSetupChecklist.tsx b/src/renderer/src/components/feature-wall/FeatureWallSetupChecklist.tsx index dadf8f1134d..0fcea197023 100644 --- a/src/renderer/src/components/feature-wall/FeatureWallSetupChecklist.tsx +++ b/src/renderer/src/components/feature-wall/FeatureWallSetupChecklist.tsx @@ -23,7 +23,7 @@ import { import { AgentStep } from '../onboarding/AgentStep' import { NotificationStep } from '../onboarding/NotificationStep' import { useAppStore } from '@/store' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { getProviderRuntimeContextKey } from '@/lib/provider-runtime-context' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.test.tsx b/src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.test.tsx index b7b64877156..0a0b4f0ce46 100644 --- a/src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.test.tsx +++ b/src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.test.tsx @@ -14,7 +14,7 @@ import { } from './FeatureWallSetupWorkflowActions' import { useAppStore } from '@/store' import { toast } from 'sonner' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' vi.mock('sonner', () => ({ toast: { diff --git a/src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.tsx b/src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.tsx index e92264899ee..2649029c9b5 100644 --- a/src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.tsx +++ b/src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.tsx @@ -7,7 +7,9 @@ import { useAppStore } from '@/store' import { useAllWorktrees } from '@/store/selectors' import { getDefaultRepoHookSettings } from '../../../../shared/constants' import { isGitRepoKind } from '../../../../shared/repo-kind' -import type { Repo, RepoHookSettings, Worktree } from '../../../../shared/types' +import type { RepoHookSettings } from '../../../../shared/orca-yaml-hook-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getRepositoryLocalCommandsSectionId } from '../settings/repository-settings-targets' import { requestContextualTourWhenReady, diff --git a/src/renderer/src/components/feature-wall/FeatureWallTourPanel.tsx b/src/renderer/src/components/feature-wall/FeatureWallTourPanel.tsx index 93646baa2f8..bb620423115 100644 --- a/src/renderer/src/components/feature-wall/FeatureWallTourPanel.tsx +++ b/src/renderer/src/components/feature-wall/FeatureWallTourPanel.tsx @@ -6,7 +6,7 @@ import type { } from '../../../../shared/feature-wall-workflows' import type { ReviewStep, ReviewStepId } from '../../../../shared/review-steps' import type { FeatureWallOpenSourceTelemetry } from '../../../../shared/telemetry-events' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { WorkbenchStep, WorkbenchStepId } from '../../../../shared/workbench-steps' import type { InstalledAgentSkillState } from '@/hooks/useInstalledAgentSkills' import { cn } from '@/lib/utils' diff --git a/src/renderer/src/components/feature-wall/KeepAwakeCard.tsx b/src/renderer/src/components/feature-wall/KeepAwakeCard.tsx index a201b6f60cd..0520cc05564 100644 --- a/src/renderer/src/components/feature-wall/KeepAwakeCard.tsx +++ b/src/renderer/src/components/feature-wall/KeepAwakeCard.tsx @@ -1,7 +1,7 @@ import type { JSX } from 'react' import { Switch } from '@/components/ui/switch' import { getAgentAwakeDescription, getAgentAwakeTitle } from '../settings/agent-awake-copy' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' import { computerAwakeSettingsForMode, diff --git a/src/renderer/src/components/feature-wall/agents-orchestration/UsageAccountsCard.tsx b/src/renderer/src/components/feature-wall/agents-orchestration/UsageAccountsCard.tsx index a32fd8c38f9..bda09726912 100644 --- a/src/renderer/src/components/feature-wall/agents-orchestration/UsageAccountsCard.tsx +++ b/src/renderer/src/components/feature-wall/agents-orchestration/UsageAccountsCard.tsx @@ -10,7 +10,7 @@ import { useMountedRef } from '@/hooks/useMountedRef' import type { ClaudeRateLimitAccountsState, CodexRateLimitAccountsState -} from '../../../../../shared/types' +} from '../../../../../shared/managed-account-types' import { getFeatureWallUsageProviderConnection } from '../feature-wall-usage-tracking' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/feature-wall/ai-commit-pr-settings-helpers.ts b/src/renderer/src/components/feature-wall/ai-commit-pr-settings-helpers.ts index 8ed5d238b01..5dc505a0a10 100644 --- a/src/renderer/src/components/feature-wall/ai-commit-pr-settings-helpers.ts +++ b/src/renderer/src/components/feature-wall/ai-commit-pr-settings-helpers.ts @@ -1,4 +1,6 @@ -import type { CommitMessageAiSettings, GlobalSettings, TuiAgent } from '../../../../shared/types' +import type { CommitMessageAiSettings } from '../../../../shared/commit-message-ai-types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { getCommitMessageAgentCapability, isCustomAgentId, diff --git a/src/renderer/src/components/feature-wall/feature-wall-setup-progress.test.ts b/src/renderer/src/components/feature-wall/feature-wall-setup-progress.test.ts index ae23c2876ab..d46fd15ac91 100644 --- a/src/renderer/src/components/feature-wall/feature-wall-setup-progress.test.ts +++ b/src/renderer/src/components/feature-wall/feature-wall-setup-progress.test.ts @@ -8,7 +8,7 @@ import { getFeatureWallSetupStepsForSection, getFirstIncompleteFeatureWallSetupStepId } from '../../../../shared/feature-wall-setup-steps' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' function makeInput( overrides: Partial = {} diff --git a/src/renderer/src/components/feature-wall/feature-wall-setup-progress.ts b/src/renderer/src/components/feature-wall/feature-wall-setup-progress.ts index 35196cb4fb2..585df18a9ac 100644 --- a/src/renderer/src/components/feature-wall/feature-wall-setup-progress.ts +++ b/src/renderer/src/components/feature-wall/feature-wall-setup-progress.ts @@ -4,7 +4,8 @@ import { FEATURE_WALL_SETUP_STEPS, type FeatureWallSetupStepId } from '../../../../shared/feature-wall-setup-steps' -import type { GlobalSettings, Worktree } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Worktree } from '../../../../shared/worktree/types' export type FeatureWallSetupProgressInput = { ready?: boolean diff --git a/src/renderer/src/components/feature-wall/useAiCommitPrSettings.ts b/src/renderer/src/components/feature-wall/useAiCommitPrSettings.ts index c187d6f18ec..1bdda194061 100644 --- a/src/renderer/src/components/feature-wall/useAiCommitPrSettings.ts +++ b/src/renderer/src/components/feature-wall/useAiCommitPrSettings.ts @@ -1,5 +1,6 @@ import { useCallback, useState } from 'react' -import type { CommitMessageAiSettings, TuiAgent } from '../../../../shared/types' +import type { CommitMessageAiSettings } from '../../../../shared/commit-message-ai-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { CUSTOM_AGENT_ID, getCommitMessageAgentCapability, diff --git a/src/renderer/src/components/floating-terminal/FloatingBrowserSlot.test.tsx b/src/renderer/src/components/floating-terminal/FloatingBrowserSlot.test.tsx index 01a12086c7c..7e1b8d7a7a2 100644 --- a/src/renderer/src/components/floating-terminal/FloatingBrowserSlot.test.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingBrowserSlot.test.tsx @@ -5,7 +5,7 @@ import { createRoot, type Root } from 'react-dom/client' import { afterEach, describe, expect, it, vi } from 'vitest' import { FloatingBrowserSlot } from './FloatingBrowserSlot' import { getBrowserOverlaySlotViewport } from '@/components/browser-pane/browser-page-viewport' -import type { BrowserTab } from '../../../../shared/types' +import type { BrowserTab } from '../../../../shared/browser-workspace-types' // Why: BrowserPane mounts a real Electron we can't run in jsdom; stub // it so the test isolates the slot-root registration that BrowserPane depends on. diff --git a/src/renderer/src/components/floating-terminal/FloatingBrowserSlot.tsx b/src/renderer/src/components/floating-terminal/FloatingBrowserSlot.tsx index 2013ffe4993..76b819f0876 100644 --- a/src/renderer/src/components/floating-terminal/FloatingBrowserSlot.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingBrowserSlot.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react' import BrowserPane from '@/components/browser-pane/BrowserPane' import { registerBrowserOverlaySlotViewport } from '@/components/browser-pane/browser-page-viewport' -import type { BrowserTab as BrowserTabState } from '../../../../shared/types' +import type { BrowserTab as BrowserTabState } from '../../../../shared/browser-workspace-types' // Why: BrowserPane mounts its persistent Electron into a slot viewport // root keyed by the browser tab id. The main workspace registers that root via diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalIconContextMenu.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalIconContextMenu.tsx index 8947efc01f3..abd460a80fd 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalIconContextMenu.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalIconContextMenu.tsx @@ -8,7 +8,7 @@ import { DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { useAppStore } from '@/store' -import type { FloatingTerminalTriggerLocation } from '../../../../shared/types' +import type { FloatingTerminalTriggerLocation } from '../../../../shared/ui-chrome-types' import { translate } from '@/i18n/i18n' type FloatingTerminalIconContextMenuProps = { diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.test.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.test.tsx index 43e74374d9d..4e1913eb967 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.test.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.test.tsx @@ -4,7 +4,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' import type { KeybindingOverrides, TerminalShortcutPolicy } from '../../../../shared/keybindings' -import type { BrowserTab, Tab, TabGroup, TerminalTab } from '../../../../shared/types' +import type { BrowserTab } from '../../../../shared/browser-workspace-types' +import type { Tab, TabGroup } from '../../../../shared/tab-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { OpenFile } from '@/store/slices/editor' import { createUntitledMarkdownFileWithTemplateSelection } from '@/lib/create-untitled-markdown' import { diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx index 2f60cb89902..f37a6968c36 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx @@ -85,7 +85,9 @@ import { ModifierDoubleTapDetector, toModifierDoubleTapEvent } from '../../../../shared/modifier-double-tap-detector' -import type { BrowserTab as BrowserTabState, Tab, TerminalTab } from '../../../../shared/types' +import type { BrowserTab as BrowserTabState } from '../../../../shared/browser-workspace-types' +import type { Tab } from '../../../../shared/tab-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { resolveUnifiedTabLabel } from '../../../../shared/tab-title-resolution' import { FloatingBrowserSlot } from './FloatingBrowserSlot' import { FloatingTerminalOrchestrationDialog } from './FloatingTerminalOrchestrationDialog' diff --git a/src/renderer/src/components/github-checks-tab-state.test.ts b/src/renderer/src/components/github-checks-tab-state.test.ts index 66a0c06b722..f9b3a3c8001 100644 --- a/src/renderer/src/components/github-checks-tab-state.test.ts +++ b/src/renderer/src/components/github-checks-tab-state.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { PRCheckDetail, PRCheckRunDetails } from '../../../shared/types' +import type { PRCheckDetail, PRCheckRunDetails } from '../../../shared/github/check-types' import { beginGitHubChecksTabDetails, createGitHubChecksTabState, diff --git a/src/renderer/src/components/github-checks-tab-state.ts b/src/renderer/src/components/github-checks-tab-state.ts index e408eb6ef9d..18c12cf089b 100644 --- a/src/renderer/src/components/github-checks-tab-state.ts +++ b/src/renderer/src/components/github-checks-tab-state.ts @@ -1,4 +1,4 @@ -import type { PRCheckDetail, PRCheckRunDetails } from '../../../shared/types' +import type { PRCheckDetail, PRCheckRunDetails } from '../../../shared/github/check-types' export type CheckDetailsLoadState = { requestId?: number diff --git a/src/renderer/src/components/github-pr-merge-state.ts b/src/renderer/src/components/github-pr-merge-state.ts index d67a3b8a3ef..f26edd5896b 100644 --- a/src/renderer/src/components/github-pr-merge-state.ts +++ b/src/renderer/src/components/github-pr-merge-state.ts @@ -1,10 +1,10 @@ import type { CheckStatus, - ProviderCheckSummary, PRMergeableState, PRReviewDecision, - PRState -} from '../../../shared/types' + PRState, + ProviderCheckSummary +} from '../../../shared/github/pull-request-types' import { canEnableGitHubPRAutoMerge } from '../../../shared/github/pull-request-auto-merge-availability' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/github-pr-reviewer-display.test.ts b/src/renderer/src/components/github-pr-reviewer-display.test.ts index fca899f118c..3512b578e29 100644 --- a/src/renderer/src/components/github-pr-reviewer-display.test.ts +++ b/src/renderer/src/components/github-pr-reviewer-display.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import { GITHUB_PR_REVIEWER_INPUT_MAX_BYTES, appendGitHubPRRequestedReviewers, diff --git a/src/renderer/src/components/github-pr-reviewer-display.ts b/src/renderer/src/components/github-pr-reviewer-display.ts index 695c5eab90a..01ba370e0a9 100644 --- a/src/renderer/src/components/github-pr-reviewer-display.ts +++ b/src/renderer/src/components/github-pr-reviewer-display.ts @@ -1,4 +1,5 @@ -import type { GitHubAssignableUser, GitHubWorkItem } from '../../../shared/types' +import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import { isClipboardTextByteLengthOverLimit } from '../../../shared/clipboard-text' type ReviewDisplayItem = Pick diff --git a/src/renderer/src/components/github-project/ProjectCell.tsx b/src/renderer/src/components/github-project/ProjectCell.tsx index 877f426311a..ef22f2ffcaa 100644 --- a/src/renderer/src/components/github-project/ProjectCell.tsx +++ b/src/renderer/src/components/github-project/ProjectCell.tsx @@ -25,7 +25,7 @@ import type { GitHubProjectUser, ListIssueTypesBySlugResult } from '../../../../shared/github/project-types' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' type Props = { diff --git a/src/renderer/src/components/github-project/ProjectItemSlugDialog.tsx b/src/renderer/src/components/github-project/ProjectItemSlugDialog.tsx index 2b778e29c10..bf176e802c3 100644 --- a/src/renderer/src/components/github-project/ProjectItemSlugDialog.tsx +++ b/src/renderer/src/components/github-project/ProjectItemSlugDialog.tsx @@ -12,7 +12,7 @@ import { VisuallyHidden } from 'radix-ui' import { Sheet, SheetContent, SheetDescription, SheetTitle } from '@/components/ui/sheet' import type { GitHubItemDialogProjectOrigin } from '@/components/GitHubItemDialog' import { SlugDialogBody } from './slug-dialog/SlugDialogBody' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' type Props = { diff --git a/src/renderer/src/components/github-project/ProjectRow.tsx b/src/renderer/src/components/github-project/ProjectRow.tsx index d57a807702e..57ad63784c3 100644 --- a/src/renderer/src/components/github-project/ProjectRow.tsx +++ b/src/renderer/src/components/github-project/ProjectRow.tsx @@ -12,7 +12,7 @@ import type { GitHubProjectFieldMutationValue, GitHubProjectRow as GitHubProjectRowType } from '../../../../shared/github/project-types' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' const PROJECT_FROZEN_COLUMN_SURFACE_CLASS = diff --git a/src/renderer/src/components/github-project/ProjectViewList.tsx b/src/renderer/src/components/github-project/ProjectViewList.tsx index 8bd6963b4c7..e54493418d6 100644 --- a/src/renderer/src/components/github-project/ProjectViewList.tsx +++ b/src/renderer/src/components/github-project/ProjectViewList.tsx @@ -22,7 +22,7 @@ import type { GitHubProjectSortDirection, GitHubProjectTable } from '../../../../shared/github/project-types' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' type SortOverride = { fieldId: string; direction: GitHubProjectSortDirection } diff --git a/src/renderer/src/components/github-project/ProjectViewWrapper.tsx b/src/renderer/src/components/github-project/ProjectViewWrapper.tsx index 24941730c94..3996a562715 100644 --- a/src/renderer/src/components/github-project/ProjectViewWrapper.tsx +++ b/src/renderer/src/components/github-project/ProjectViewWrapper.tsx @@ -41,7 +41,7 @@ import type { GitHubProjectViewSummary, ListProjectViewsResult } from '../../../../shared/github/project-types' -import type { GitHubWorkItem } from '../../../../shared/types' +import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' import ProjectPicker, { type ResolvedProjectSelection } from './ProjectPicker' import ProjectViewList from './ProjectViewList' import ProjectItemSlugDialog from './ProjectItemSlugDialog' diff --git a/src/renderer/src/components/github-project/project-row-filtering.test.ts b/src/renderer/src/components/github-project/project-row-filtering.test.ts index 636382aa53c..fae4ecf6ffc 100644 --- a/src/renderer/src/components/github-project/project-row-filtering.test.ts +++ b/src/renderer/src/components/github-project/project-row-filtering.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import type { GitHubProjectRow, GitHubProjectTable } from '../../../../shared/github/project-types' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { filterProjectTableRowsByOpenRepos, filterProjectTableRowsBySelectedRepos, diff --git a/src/renderer/src/components/github-project/project-row-filtering.ts b/src/renderer/src/components/github-project/project-row-filtering.ts index b4c1b3738a3..0e9a3108043 100644 --- a/src/renderer/src/components/github-project/project-row-filtering.ts +++ b/src/renderer/src/components/github-project/project-row-filtering.ts @@ -1,5 +1,5 @@ import type { GitHubProjectRow, GitHubProjectTable } from '../../../../shared/github/project-types' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' export type ProjectRowSlugLookup = ( slug: string | null | undefined, diff --git a/src/renderer/src/components/github-project/project-work-item.ts b/src/renderer/src/components/github-project/project-work-item.ts index cc68266fe88..692e0fa487b 100644 --- a/src/renderer/src/components/github-project/project-work-item.ts +++ b/src/renderer/src/components/github-project/project-work-item.ts @@ -1,6 +1,6 @@ import { githubProjectHost } from '../../../../shared/github/project-identity' import type { GitHubProjectRow } from '../../../../shared/github/project-types' -import type { GitHubWorkItem } from '../../../../shared/types' +import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' export function buildProjectWorkItem( row: GitHubProjectRow, diff --git a/src/renderer/src/components/github-project/slug-dialog/AssigneesEditor.tsx b/src/renderer/src/components/github-project/slug-dialog/AssigneesEditor.tsx index b609030dc03..cccdefb3f31 100644 --- a/src/renderer/src/components/github-project/slug-dialog/AssigneesEditor.tsx +++ b/src/renderer/src/components/github-project/slug-dialog/AssigneesEditor.tsx @@ -2,7 +2,7 @@ import React, { useMemo, useState } from 'react' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { cn } from '@/lib/utils' import { useRepoAssigneesBySlug } from '@/hooks/useGitHubSlugMetadata' -import type { GlobalSettings } from '../../../../../shared/types' +import type { GlobalSettings } from '../../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' export function AssigneesEditor({ diff --git a/src/renderer/src/components/github-project/slug-dialog/Comments.tsx b/src/renderer/src/components/github-project/slug-dialog/Comments.tsx index 748edc1fe74..d5ebf6f5740 100644 --- a/src/renderer/src/components/github-project/slug-dialog/Comments.tsx +++ b/src/renderer/src/components/github-project/slug-dialog/Comments.tsx @@ -12,7 +12,8 @@ import { } from '@/lib/comment-body-submit-state' import { useRepoSlugIndex } from '@/lib/repo-slug-index' import { getSettingsForRepoRuntimeOwner } from '@/lib/repo-runtime-owner' -import type { GlobalSettings, PRComment } from '../../../../../shared/types' +import type { PRComment } from '../../../../../shared/github/comment-types' +import type { GlobalSettings } from '../../../../../shared/global-settings-types' import type { GitHubProjectCommentMutationResult, GitHubProjectMutationResult diff --git a/src/renderer/src/components/github-project/slug-dialog/LabelsEditor.tsx b/src/renderer/src/components/github-project/slug-dialog/LabelsEditor.tsx index 877cf47ca7c..96d64b7ad2a 100644 --- a/src/renderer/src/components/github-project/slug-dialog/LabelsEditor.tsx +++ b/src/renderer/src/components/github-project/slug-dialog/LabelsEditor.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { cn } from '@/lib/utils' import { useRepoLabelsBySlug } from '@/hooks/useGitHubSlugMetadata' -import type { GlobalSettings } from '../../../../../shared/types' +import type { GlobalSettings } from '../../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' export function LabelsEditor({ diff --git a/src/renderer/src/components/github-project/slug-dialog/SlugDialogBody.tsx b/src/renderer/src/components/github-project/slug-dialog/SlugDialogBody.tsx index a8b20151a34..e04c3129958 100644 --- a/src/renderer/src/components/github-project/slug-dialog/SlugDialogBody.tsx +++ b/src/renderer/src/components/github-project/slug-dialog/SlugDialogBody.tsx @@ -7,7 +7,8 @@ import { Input } from '@/components/ui/input' import CommentMarkdown from '@/components/sidebar/CommentMarkdown' import { useAppStore } from '@/store' import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' -import type { GitHubWorkItemDetails, GlobalSettings } from '../../../../../shared/types' +import type { GitHubWorkItemDetails } from '../../../../../shared/github/work-item-types' +import type { GlobalSettings } from '../../../../../shared/global-settings-types' import type { GitHubItemDialogProjectOrigin } from '@/components/GitHubItemDialog' import { LabelsEditor } from './LabelsEditor' import { AssigneesEditor } from './AssigneesEditor' diff --git a/src/renderer/src/components/github/CommentCodeContext.tsx b/src/renderer/src/components/github/CommentCodeContext.tsx index 8dd390f9586..8539ba9fad3 100644 --- a/src/renderer/src/components/github/CommentCodeContext.tsx +++ b/src/renderer/src/components/github/CommentCodeContext.tsx @@ -16,12 +16,12 @@ import { import { getPrCommentCodeContext } from '@/components/github/pr-comment-code-context' import { getPRFileContentsRenderLimit } from '@/components/github/pr-file-diff-mapping' import type { TaskSourceContext } from '../../../../shared/task-source-context' +import type { PRComment } from '../../../../shared/github/comment-types' import type { GitHubOwnerRepo, GitHubPRFile, - GitHubPRFileContents, - PRComment -} from '../../../../shared/types' + GitHubPRFileContents +} from '../../../../shared/github/pull-request-types' const MonacoCodeExcerpt = lazy(() => import('@/components/editor/MonacoCodeExcerpt')) diff --git a/src/renderer/src/components/github/CommentReactions.tsx b/src/renderer/src/components/github/CommentReactions.tsx index 21aa4ac2316..fc9027e89ca 100644 --- a/src/renderer/src/components/github/CommentReactions.tsx +++ b/src/renderer/src/components/github/CommentReactions.tsx @@ -6,7 +6,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip import { translate } from '@/i18n/i18n' import { cn } from '@/lib/utils' import { GITHUB_REACTION_ORDER } from '@/lib/pr-comment-reactions' -import type { GitHubReaction, GitHubReactionContent } from '../../../../shared/types' +import type { GitHubReaction, GitHubReactionContent } from '../../../../shared/github/comment-types' const REACTION_EMOJI: Record = { '+1': '👍', diff --git a/src/renderer/src/components/github/GitHubMarkdownComposer.tsx b/src/renderer/src/components/github/GitHubMarkdownComposer.tsx index 9fa818066fd..52d7906df08 100644 --- a/src/renderer/src/components/github/GitHubMarkdownComposer.tsx +++ b/src/renderer/src/components/github/GitHubMarkdownComposer.tsx @@ -27,7 +27,7 @@ import { } from '@/components/github/github-markdown-composer-tabbar' import { hasBoundedGitHubMarkdownImageUrlText } from '@/components/github/github-markdown-image-url' import { useImageInput } from '@/components/github/use-image-input' -import type { GitHubOwnerRepo } from '../../../../shared/types' +import type { GitHubOwnerRepo } from '../../../../shared/github/pull-request-types' import { translate } from '@/i18n/i18n' import { useAppStore } from '@/store' import { diff --git a/src/renderer/src/components/github/IssueSourceIndicator.tsx b/src/renderer/src/components/github/IssueSourceIndicator.tsx index c9615562e81..264b4ef7242 100644 --- a/src/renderer/src/components/github/IssueSourceIndicator.tsx +++ b/src/renderer/src/components/github/IssueSourceIndicator.tsx @@ -1,5 +1,5 @@ import React from 'react' -import type { GitHubOwnerRepo } from '../../../../shared/types' +import type { GitHubOwnerRepo } from '../../../../shared/github/pull-request-types' import RepoBadgeLabel from '@/components/repo/RepoBadgeLabel' import { cn } from '@/lib/utils' import { diff --git a/src/renderer/src/components/github/IssueSourceSelector.tsx b/src/renderer/src/components/github/IssueSourceSelector.tsx index e428d53f525..fc20eae5c4c 100644 --- a/src/renderer/src/components/github/IssueSourceSelector.tsx +++ b/src/renderer/src/components/github/IssueSourceSelector.tsx @@ -1,5 +1,6 @@ import React from 'react' -import type { GitHubOwnerRepo, IssueSourcePreference } from '../../../../shared/types' +import type { GitHubOwnerRepo } from '../../../../shared/github/pull-request-types' +import type { IssueSourcePreference } from '../../../../shared/repo-types' import { sameGitHubOwnerRepo } from '@/components/github/IssueSourceIndicator' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' diff --git a/src/renderer/src/components/github/PRAssigneesPanel.tsx b/src/renderer/src/components/github/PRAssigneesPanel.tsx index 24391745120..d759d93880a 100644 --- a/src/renderer/src/components/github/PRAssigneesPanel.tsx +++ b/src/renderer/src/components/github/PRAssigneesPanel.tsx @@ -19,7 +19,8 @@ import { getTaskSourceRuntimeSettings, type TaskSourceContext } from '../../../../shared/task-source-context' -import type { GitHubAssignableUser, GitHubWorkItem } from '../../../../shared/types' +import type { GitHubAssignableUser } from '../../../../shared/github/pull-request-types' +import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' export function PRAssigneesPanel({ item, diff --git a/src/renderer/src/components/github/PRFilterDropdowns.tsx b/src/renderer/src/components/github/PRFilterDropdowns.tsx index d22cdbb8f4b..ddf74929515 100644 --- a/src/renderer/src/components/github/PRFilterDropdowns.tsx +++ b/src/renderer/src/components/github/PRFilterDropdowns.tsx @@ -17,9 +17,9 @@ import { } from '@/components/github/PRFilterSections' import type { GitHubAssignableUser, - GitHubOwnerRepo, - GlobalSettings -} from '../../../../shared/types' + GitHubOwnerRepo +} from '../../../../shared/github/pull-request-types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { ParsedTaskQuery } from '../../../../shared/task-query' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/github/github-markdown-composer-preview-pane.tsx b/src/renderer/src/components/github/github-markdown-composer-preview-pane.tsx index 706f8785f8e..01fc334b2a6 100644 --- a/src/renderer/src/components/github/github-markdown-composer-preview-pane.tsx +++ b/src/renderer/src/components/github/github-markdown-composer-preview-pane.tsx @@ -1,5 +1,5 @@ import CommentMarkdown from '@/components/sidebar/CommentMarkdown' -import type { GitHubOwnerRepo } from '../../../../shared/types' +import type { GitHubOwnerRepo } from '../../../../shared/github/pull-request-types' import { translate } from '@/i18n/i18n' export function GitHubMarkdownComposerPreviewPane({ diff --git a/src/renderer/src/components/github/github-pr-reviewer-candidate-filter.test.ts b/src/renderer/src/components/github/github-pr-reviewer-candidate-filter.test.ts index e20b151f747..c8ef5e977e3 100644 --- a/src/renderer/src/components/github/github-pr-reviewer-candidate-filter.test.ts +++ b/src/renderer/src/components/github/github-pr-reviewer-candidate-filter.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { GitHubAssignableUser } from '../../../../shared/types' +import type { GitHubAssignableUser } from '../../../../shared/github/pull-request-types' import { GITHUB_PR_REVIEWER_QUERY_MAX_BYTES, filterGitHubPRReviewerCandidates, diff --git a/src/renderer/src/components/github/github-pr-reviewer-candidate-filter.ts b/src/renderer/src/components/github/github-pr-reviewer-candidate-filter.ts index 3ae5bc4a236..f38bd80e9a0 100644 --- a/src/renderer/src/components/github/github-pr-reviewer-candidate-filter.ts +++ b/src/renderer/src/components/github/github-pr-reviewer-candidate-filter.ts @@ -1,5 +1,5 @@ import { isClipboardTextByteLengthOverLimit } from '../../../../shared/clipboard-text' -import type { GitHubAssignableUser } from '../../../../shared/types' +import type { GitHubAssignableUser } from '../../../../shared/github/pull-request-types' export type GitHubPRReviewerQueryState = { query: string diff --git a/src/renderer/src/components/github/github-rate-limit-display.tsx b/src/renderer/src/components/github/github-rate-limit-display.tsx index 9b7002476ce..17d81a90532 100644 --- a/src/renderer/src/components/github/github-rate-limit-display.tsx +++ b/src/renderer/src/components/github/github-rate-limit-display.tsx @@ -4,7 +4,10 @@ import { cn } from '@/lib/utils' import { installWindowVisibilityInterval } from '@/lib/window-visibility-interval' import { useAppStore } from '@/store' import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' -import type { GetRateLimitResult, GitHubRateLimitSnapshot } from '../../../../shared/types' +import type { + GetRateLimitResult, + GitHubRateLimitSnapshot +} from '../../../../shared/github/rate-limit-types' import { getProviderRateLimitScope } from '@/components/settings/provider-account-scope' import { ProviderHostScopeControl } from '@/components/settings/ProviderHostScopeControl' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/github/github-work-item-assignee-filter.ts b/src/renderer/src/components/github/github-work-item-assignee-filter.ts index cda49753314..7d30ce248b1 100644 --- a/src/renderer/src/components/github/github-work-item-assignee-filter.ts +++ b/src/renderer/src/components/github/github-work-item-assignee-filter.ts @@ -1,4 +1,4 @@ -import type { GitHubAssignableUser } from '../../../../shared/types' +import type { GitHubAssignableUser } from '../../../../shared/github/pull-request-types' import { isGitHubWorkItemOptionFilterQueryTooLarge } from './github-work-item-option-filter-bounds' export function filterGitHubWorkItemAssignees( diff --git a/src/renderer/src/components/github/github-work-item-comment-mutations.ts b/src/renderer/src/components/github/github-work-item-comment-mutations.ts index a42804365bf..f0f6fda3b82 100644 --- a/src/renderer/src/components/github/github-work-item-comment-mutations.ts +++ b/src/renderer/src/components/github/github-work-item-comment-mutations.ts @@ -5,7 +5,7 @@ import { getGitHubSourceRuntimeHost } from '@/lib/github-source-runtime-context' import type { TaskSourceContext } from '../../../../shared/task-source-context' -import type { GitHubOwnerRepo } from '../../../../shared/types' +import type { GitHubOwnerRepo } from '../../../../shared/github/pull-request-types' export function addIssueCommentForRepo(args: { repoId?: string diff --git a/src/renderer/src/components/github/github-work-item-edit-mutations.ts b/src/renderer/src/components/github/github-work-item-edit-mutations.ts index e81adba1f0e..5c40aeb2328 100644 --- a/src/renderer/src/components/github/github-work-item-edit-mutations.ts +++ b/src/renderer/src/components/github/github-work-item-edit-mutations.ts @@ -13,7 +13,8 @@ import { getTaskSourceRuntimeSettings, type TaskSourceContext } from '../../../../shared/task-source-context' -import type { GitHubOwnerRepo, GitHubWorkItem } from '../../../../shared/types' +import type { GitHubOwnerRepo } from '../../../../shared/github/pull-request-types' +import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' // Why: for a Project row whose repo differs from the active workspace, mutations must target the row's actual repo via slug-addressed IPCs, else edits silently apply to the workspace's repo. // Why: these edit IPCs return `{ ok, error }`; callers throw on `!ok` so useImmediateMutation (which expects throws on failure) works unchanged. diff --git a/src/renderer/src/components/github/github-work-item-identity.ts b/src/renderer/src/components/github/github-work-item-identity.ts index f4808e17f3f..b4b06754406 100644 --- a/src/renderer/src/components/github/github-work-item-identity.ts +++ b/src/renderer/src/components/github/github-work-item-identity.ts @@ -1,5 +1,6 @@ import { githubProjectHost } from '../../../../shared/github/project-identity' -import type { GitHubOwnerRepo, GitHubWorkItem } from '../../../../shared/types' +import type { GitHubOwnerRepo } from '../../../../shared/github/pull-request-types' +import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' // Why: the dialog lacks repository context, so recover its host-aware identity from the canonical item URL. export function parseOwnerRepoFromItemUrl(url: string): GitHubOwnerRepo | null { diff --git a/src/renderer/src/components/github/pr-check-presentation.ts b/src/renderer/src/components/github/pr-check-presentation.ts index 8b500171866..fb50d0a780a 100644 --- a/src/renderer/src/components/github/pr-check-presentation.ts +++ b/src/renderer/src/components/github/pr-check-presentation.ts @@ -1,5 +1,5 @@ import { getCheckConclusion } from '@/components/pr-check-counts' -import type { PRCheckDetail } from '../../../../shared/types' +import type { PRCheckDetail } from '../../../../shared/github/check-types' export function getCheckStatusLabel(check: PRCheckDetail): string { const conclusion = getCheckConclusion(check) diff --git a/src/renderer/src/components/github/pr-file-content-size.test.ts b/src/renderer/src/components/github/pr-file-content-size.test.ts index 700a9579ec4..d63bb18798d 100644 --- a/src/renderer/src/components/github/pr-file-content-size.test.ts +++ b/src/renderer/src/components/github/pr-file-content-size.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { GitHubPRFileContents } from '../../../../shared/types' +import type { GitHubPRFileContents } from '../../../../shared/github/pull-request-types' import { getPRFileContentsCacheByteCount } from './pr-file-content-size' describe('PR file content size', () => { diff --git a/src/renderer/src/components/github/pr-file-content-size.ts b/src/renderer/src/components/github/pr-file-content-size.ts index 0e20c92bd1d..164dae1b8eb 100644 --- a/src/renderer/src/components/github/pr-file-content-size.ts +++ b/src/renderer/src/components/github/pr-file-content-size.ts @@ -1,5 +1,8 @@ import { MAX_RENDERED_DIFF_COMBINED_CHARACTERS } from '@/components/editor/large-diff-render-limit' -import type { GitHubPRFile, GitHubPRFileContents } from '../../../../shared/types' +import type { + GitHubPRFile, + GitHubPRFileContents +} from '../../../../shared/github/pull-request-types' import { getUtf8ByteLength } from '../../../../shared/utf8-byte-limits' export const PR_FILE_CONTENT_CACHE_MAX_BYTES = MAX_RENDERED_DIFF_COMBINED_CHARACTERS * 4 diff --git a/src/renderer/src/components/github/pr-file-diff-mapping.ts b/src/renderer/src/components/github/pr-file-diff-mapping.ts index d592a0d8140..dc990ed8f69 100644 --- a/src/renderer/src/components/github/pr-file-diff-mapping.ts +++ b/src/renderer/src/components/github/pr-file-diff-mapping.ts @@ -5,14 +5,13 @@ import { type LargeDiffRenderLimit } from '@/components/editor/large-diff-render-limit' import type { TaskSourceContext } from '../../../../shared/task-source-context' +import type { GitBranchChangeEntry, GitDiffResult } from '../../../../shared/git-diff-compare-types' +import type { PRComment } from '../../../../shared/github/comment-types' import type { - GitBranchChangeEntry, - GitDiffResult, GitHubOwnerRepo, GitHubPRFile, - GitHubPRFileContents, - PRComment -} from '../../../../shared/types' + GitHubPRFileContents +} from '../../../../shared/github/pull-request-types' export const PR_DIFF_OVERSCAN = 5 diff --git a/src/renderer/src/components/github/work-item-state-presentation.tsx b/src/renderer/src/components/github/work-item-state-presentation.tsx index fadc502fb91..b7f24298e94 100644 --- a/src/renderer/src/components/github/work-item-state-presentation.tsx +++ b/src/renderer/src/components/github/work-item-state-presentation.tsx @@ -1,6 +1,7 @@ import React from 'react' import { GitHubUserAvatar } from '@/components/github/github-user-avatar' -import type { GitHubAssignableUser, GitHubWorkItem } from '../../../../shared/types' +import type { GitHubAssignableUser } from '../../../../shared/github/pull-request-types' +import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' import { formatUiRelativeTimeFromDate } from '@/i18n/relative-time-format' export function formatRelativeTime(input: string): string { diff --git a/src/renderer/src/components/gitlab/gitlab-rate-limit-display.tsx b/src/renderer/src/components/gitlab/gitlab-rate-limit-display.tsx index a85c566b0d6..66e18cc4fca 100644 --- a/src/renderer/src/components/gitlab/gitlab-rate-limit-display.tsx +++ b/src/renderer/src/components/gitlab/gitlab-rate-limit-display.tsx @@ -5,7 +5,10 @@ import { cn } from '@/lib/utils' import { installWindowVisibilityInterval } from '@/lib/window-visibility-interval' import { useAppStore } from '@/store' import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' -import type { GetGitLabRateLimitResult, GitLabRateLimitSnapshot } from '../../../../shared/types' +import type { + GetGitLabRateLimitResult, + GitLabRateLimitSnapshot +} from '../../../../shared/gitlab-types' import { getProviderRateLimitScope } from '@/components/settings/provider-account-scope' import { ProviderHostScopeControl } from '@/components/settings/ProviderHostScopeControl' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/jira-issue-sorter.ts b/src/renderer/src/components/jira-issue-sorter.ts index 875aaff0ef0..7fe071dc61e 100644 --- a/src/renderer/src/components/jira-issue-sorter.ts +++ b/src/renderer/src/components/jira-issue-sorter.ts @@ -1,4 +1,4 @@ -import type { JiraIssue, JiraPriority } from '../../../shared/types' +import type { JiraIssue, JiraPriority } from '../../../shared/jira-types' import { compareNumericLocaleText } from '@/lib/locale-text-collators' export type JiraIssueSortColumn = 'key' | 'title' | 'status' | 'priority' | 'assignee' | 'updated' diff --git a/src/renderer/src/components/jira-project-picker-filter.test.ts b/src/renderer/src/components/jira-project-picker-filter.test.ts index c270f361e4e..2c41742c5fc 100644 --- a/src/renderer/src/components/jira-project-picker-filter.test.ts +++ b/src/renderer/src/components/jira-project-picker-filter.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { JiraProject } from '../../../shared/types' +import type { JiraProject } from '../../../shared/jira-types' import { JIRA_PROJECT_PICKER_QUERY_MAX_BYTES, filterJiraProjectPickerProjects, diff --git a/src/renderer/src/components/jira-project-picker-filter.ts b/src/renderer/src/components/jira-project-picker-filter.ts index 0a5ee32176a..0e8d060bf50 100644 --- a/src/renderer/src/components/jira-project-picker-filter.ts +++ b/src/renderer/src/components/jira-project-picker-filter.ts @@ -1,5 +1,5 @@ import { isClipboardTextByteLengthOverLimit } from '../../../shared/clipboard-text' -import type { JiraProject } from '../../../shared/types' +import type { JiraProject } from '../../../shared/jira-types' export const JIRA_PROJECT_PICKER_QUERY_MAX_BYTES = 2 * 1024 diff --git a/src/renderer/src/components/landing-preflight-dismissal.test.ts b/src/renderer/src/components/landing-preflight-dismissal.test.ts index 20b50442241..cfc316bc4b4 100644 --- a/src/renderer/src/components/landing-preflight-dismissal.test.ts +++ b/src/renderer/src/components/landing-preflight-dismissal.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import { dismissPreflightIssue, githubProjectKeys, diff --git a/src/renderer/src/components/landing-preflight-dismissal.ts b/src/renderer/src/components/landing-preflight-dismissal.ts index 1fae17b0a3d..d688d5a0c4c 100644 --- a/src/renderer/src/components/landing-preflight-dismissal.ts +++ b/src/renderer/src/components/landing-preflight-dismissal.ts @@ -2,7 +2,7 @@ import { getProjectIdentityKey, isGitHubBackedRepo } from '../../../shared/project-host-setup-projection' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' // Why: GitHub-CLI setup nudges are dismissible, but the dismissal must lapse // when the user adds a NEW GitHub-backed project — that's the moment the CLI diff --git a/src/renderer/src/components/landing-preflight-issues.test.ts b/src/renderer/src/components/landing-preflight-issues.test.ts index 99c315088f0..75ba7a5e006 100644 --- a/src/renderer/src/components/landing-preflight-issues.test.ts +++ b/src/renderer/src/components/landing-preflight-issues.test.ts @@ -4,7 +4,7 @@ import { hasGitHubBackedProject, type LandingPreflightStatus } from './landing-preflight-issues' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' function repo(overrides: Partial & Pick): Repo { return { diff --git a/src/renderer/src/components/landing-preflight-issues.ts b/src/renderer/src/components/landing-preflight-issues.ts index 73324510a08..10e7ec81baf 100644 --- a/src/renderer/src/components/landing-preflight-issues.ts +++ b/src/renderer/src/components/landing-preflight-issues.ts @@ -1,6 +1,6 @@ import { translate } from '@/i18n/i18n' import { projectHostSetupProjectionFromRepos } from '../../../shared/project-host-setup-projection' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' export type PreflightIssue = { id: string diff --git a/src/renderer/src/components/landing-preflight-runtime-boundary.test.ts b/src/renderer/src/components/landing-preflight-runtime-boundary.test.ts index a52c516f161..f282ca5f15a 100644 --- a/src/renderer/src/components/landing-preflight-runtime-boundary.test.ts +++ b/src/renderer/src/components/landing-preflight-runtime-boundary.test.ts @@ -2,7 +2,7 @@ import { act, cleanup, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { PreflightStatus } from '../../../preload/api-types' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import { useAppStore } from '../store' import { useLandingPreflightRuntime } from './landing-preflight-runtime' diff --git a/src/renderer/src/components/linear-api-key-dialog.tsx b/src/renderer/src/components/linear-api-key-dialog.tsx index 0f00205d26e..272814e901b 100644 --- a/src/renderer/src/components/linear-api-key-dialog.tsx +++ b/src/renderer/src/components/linear-api-key-dialog.tsx @@ -1,6 +1,6 @@ import { useId, useMemo, useState } from 'react' import { ExternalLink, LoaderCircle, Lock } from 'lucide-react' -import type { LinearWorkspace } from '../../../shared/types' +import type { LinearWorkspace } from '../../../shared/linear/workspace-types' import { buildLinearPersonalApiKeySettingsUrl, buildLinearWorkspaceApiSettingsUrl diff --git a/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx b/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx index 2352f25b876..65b08bbc168 100644 --- a/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx +++ b/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx @@ -3,7 +3,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { LinearTeam } from '../../../shared/types' +import type { LinearTeam } from '../../../shared/linear/workspace-types' import { clearLinearIssueAttributeFacet, countLinearIssueAttributeFilters, diff --git a/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.tsx b/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.tsx index e914a40c064..a8fa09cbf12 100644 --- a/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.tsx +++ b/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.tsx @@ -12,7 +12,7 @@ import { emptyLinearIssueAttributeFilter, type LinearIssueAttributeFilter } from '../../../shared/linear/issue-attribute-filter' -import type { LinearTeam } from '../../../shared/types' +import type { LinearTeam } from '../../../shared/linear/workspace-types' import { LinearIssueFilterSectionDetail, LinearIssueFilterSectionMenu, diff --git a/src/renderer/src/components/linear-issue-attribute-filter-primary-team.test.ts b/src/renderer/src/components/linear-issue-attribute-filter-primary-team.test.ts index 1b7cdf164d2..91532889d37 100644 --- a/src/renderer/src/components/linear-issue-attribute-filter-primary-team.test.ts +++ b/src/renderer/src/components/linear-issue-attribute-filter-primary-team.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { resolveLinearIssueAttributeFilterPrimaryTeam } from './linear-issue-attribute-filter-primary-team' -import type { LinearTeam } from '../../../shared/types' +import type { LinearTeam } from '../../../shared/linear/workspace-types' const teams: LinearTeam[] = [ { id: 't-b', name: 'Backend', key: 'BE' }, diff --git a/src/renderer/src/components/linear-issue-attribute-filter-primary-team.ts b/src/renderer/src/components/linear-issue-attribute-filter-primary-team.ts index 53fe674b04b..75264f69d49 100644 --- a/src/renderer/src/components/linear-issue-attribute-filter-primary-team.ts +++ b/src/renderer/src/components/linear-issue-attribute-filter-primary-team.ts @@ -1,4 +1,4 @@ -import type { LinearTeam } from '../../../shared/types' +import type { LinearTeam } from '../../../shared/linear/workspace-types' function compareTeamNameId(a: LinearTeam, b: LinearTeam): number { const nameCmp = a.name.localeCompare(b.name) diff --git a/src/renderer/src/components/linear-issue-attribute-filter-team-ids.test.ts b/src/renderer/src/components/linear-issue-attribute-filter-team-ids.test.ts index 167080f61b0..bd56b936d88 100644 --- a/src/renderer/src/components/linear-issue-attribute-filter-team-ids.test.ts +++ b/src/renderer/src/components/linear-issue-attribute-filter-team-ids.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { LinearTeam } from '../../../shared/types' +import type { LinearTeam } from '../../../shared/linear/workspace-types' import { resolveLinearIssueAttributeFilterTeamIds, unionLinearMetadataById diff --git a/src/renderer/src/components/linear-issue-attribute-filter-team-ids.ts b/src/renderer/src/components/linear-issue-attribute-filter-team-ids.ts index ba586332e4c..dea77e52595 100644 --- a/src/renderer/src/components/linear-issue-attribute-filter-team-ids.ts +++ b/src/renderer/src/components/linear-issue-attribute-filter-team-ids.ts @@ -1,4 +1,4 @@ -import type { LinearTeam } from '../../../shared/types' +import type { LinearTeam } from '../../../shared/linear/workspace-types' /** * Resolve which Linear team ids should feed attribute-filter metadata. diff --git a/src/renderer/src/components/linear-issue-text-draft-state.ts b/src/renderer/src/components/linear-issue-text-draft-state.ts index 44b5eb08fbb..867a5eede9f 100644 --- a/src/renderer/src/components/linear-issue-text-draft-state.ts +++ b/src/renderer/src/components/linear-issue-text-draft-state.ts @@ -1,4 +1,4 @@ -import type { LinearIssue } from '../../../shared/types' +import type { LinearIssue } from '../../../shared/linear/issue-types' type LinearIssueTextDraftSource = Pick diff --git a/src/renderer/src/components/linear-issue-text-save-plan.ts b/src/renderer/src/components/linear-issue-text-save-plan.ts index 13553ea301d..43a28d360f9 100644 --- a/src/renderer/src/components/linear-issue-text-save-plan.ts +++ b/src/renderer/src/components/linear-issue-text-save-plan.ts @@ -1,4 +1,4 @@ -import type { LinearIssue } from '../../../shared/types' +import type { LinearIssue } from '../../../shared/linear/issue-types' export type LinearIssueTextField = 'title' | 'description' diff --git a/src/renderer/src/components/linear-issue-workspace-text.test.ts b/src/renderer/src/components/linear-issue-workspace-text.test.ts index eecc60bddf5..42eb037916d 100644 --- a/src/renderer/src/components/linear-issue-workspace-text.test.ts +++ b/src/renderer/src/components/linear-issue-workspace-text.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { LinearIssue } from '../../../shared/types' +import type { LinearIssue } from '../../../shared/linear/issue-types' import { buildLinearIssueBranchName } from './linear-issue-workspace-text' function makeIssue(branchName?: string): LinearIssue { diff --git a/src/renderer/src/components/linear-issue-workspace-text.ts b/src/renderer/src/components/linear-issue-workspace-text.ts index e047495d40b..57f7aae1945 100644 --- a/src/renderer/src/components/linear-issue-workspace-text.ts +++ b/src/renderer/src/components/linear-issue-workspace-text.ts @@ -1,5 +1,5 @@ import { getLinearIssueWorkspaceName } from '../../../shared/workspace-name' -import type { LinearIssue } from '../../../shared/types' +import type { LinearIssue } from '../../../shared/linear/issue-types' import { getUsableLinearBranchName } from '../../../shared/new-workspace/workspace-source' import { formatUiRelativeTimeFromDate } from '@/i18n/relative-time-format' diff --git a/src/renderer/src/components/linear-project-view-surfaces.tsx b/src/renderer/src/components/linear-project-view-surfaces.tsx index 58970a8215a..4717375b182 100644 --- a/src/renderer/src/components/linear-project-view-surfaces.tsx +++ b/src/renderer/src/components/linear-project-view-surfaces.tsx @@ -22,9 +22,9 @@ import { cn } from '@/lib/utils' import type { LinearCustomViewSummary, LinearProjectDetail, - LinearProjectSummary, - LinearWorkspaceError -} from '../../../shared/types' + LinearProjectSummary +} from '../../../shared/linear/project-types' +import type { LinearWorkspaceError } from '../../../shared/linear/workspace-types' import { translate } from '@/i18n/i18n' type LinearProjectLike = LinearProjectSummary & { diff --git a/src/renderer/src/components/linear-scope-selector.test.ts b/src/renderer/src/components/linear-scope-selector.test.ts index 65c752c6fe0..3e8d69f69c7 100644 --- a/src/renderer/src/components/linear-scope-selector.test.ts +++ b/src/renderer/src/components/linear-scope-selector.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { LinearTeam, LinearWorkspace } from '../../../shared/types' +import type { LinearTeam, LinearWorkspace } from '../../../shared/linear/workspace-types' import { LINEAR_SCOPE_TEAM_FILTER_QUERY_MAX_BYTES, filterLinearScopeTeams, diff --git a/src/renderer/src/components/linear-scope-selector.tsx b/src/renderer/src/components/linear-scope-selector.tsx index 26bfb33df2e..e6a1712fab1 100644 --- a/src/renderer/src/components/linear-scope-selector.tsx +++ b/src/renderer/src/components/linear-scope-selector.tsx @@ -1,6 +1,10 @@ import { useCallback, useMemo, useState } from 'react' import { Check, ChevronDown, KeyRound } from 'lucide-react' -import type { LinearTeam, LinearWorkspace, LinearWorkspaceSelection } from '../../../shared/types' +import type { + LinearTeam, + LinearWorkspace, + LinearWorkspaceSelection +} from '../../../shared/linear/workspace-types' import { isClipboardTextByteLengthOverLimit } from '../../../shared/clipboard-text' import { Command, CommandInput, CommandItem, CommandList } from '@/components/ui/command' import { Button } from '@/components/ui/button' diff --git a/src/renderer/src/components/native-chat/claude-model-switch-confirmation.ts b/src/renderer/src/components/native-chat/claude-model-switch-confirmation.ts index 826d70aea9f..f9664bacfa5 100644 --- a/src/renderer/src/components/native-chat/claude-model-switch-confirmation.ts +++ b/src/renderer/src/components/native-chat/claude-model-switch-confirmation.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { subscribeToPtyData } from '../terminal-pane/pty-data-sidecar-subscriptions' import { isRemoteRuntimePtyId, sendRuntimePtyInput } from '@/runtime/runtime-terminal-inspection' import { subscribeToRuntimeTerminalData } from '@/runtime/runtime-terminal-stream' diff --git a/src/renderer/src/components/native-chat/native-chat-attachment-upload.test.ts b/src/renderer/src/components/native-chat/native-chat-attachment-upload.test.ts index 5f84fc4b7ba..3dde6373391 100644 --- a/src/renderer/src/components/native-chat/native-chat-attachment-upload.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-attachment-upload.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { AppState } from '@/store/types' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' const mocks = vi.hoisted(() => ({ toastLoading: vi.fn(() => 'toast-1'), diff --git a/src/renderer/src/components/native-chat/native-chat-availability.ts b/src/renderer/src/components/native-chat/native-chat-availability.ts index 11b9e5af7ad..3cad7fc157f 100644 --- a/src/renderer/src/components/native-chat/native-chat-availability.ts +++ b/src/renderer/src/components/native-chat/native-chat-availability.ts @@ -1,4 +1,5 @@ -import type { Tab, TuiAgent } from '../../../../shared/types' +import type { Tab } from '../../../../shared/tab-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { AgentType } from '../../../../shared/agent-status-types' import { isNativeChatSupportedAgent, diff --git a/src/renderer/src/components/native-chat/native-chat-file-link.test.ts b/src/renderer/src/components/native-chat/native-chat-file-link.test.ts index 1022d8e8441..17dae96d98d 100644 --- a/src/renderer/src/components/native-chat/native-chat-file-link.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-file-link.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { AppState } from '@/store/types' import { resolveNativeChatFileLink, diff --git a/src/renderer/src/components/native-chat/native-chat-file-link.ts b/src/renderer/src/components/native-chat/native-chat-file-link.ts index eee08ceae2b..44c1e83b6ab 100644 --- a/src/renderer/src/components/native-chat/native-chat-file-link.ts +++ b/src/renderer/src/components/native-chat/native-chat-file-link.ts @@ -1,5 +1,5 @@ import { routeNativeChatHref } from '../../../../shared/native-chat-href-routing' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { parseExplicitFileLinkTarget, resolveExplicitFileLinkTarget diff --git a/src/renderer/src/components/native-chat/native-chat-launch-session-options.ts b/src/renderer/src/components/native-chat/native-chat-launch-session-options.ts index 2257bb77120..2271d4bd1d8 100644 --- a/src/renderer/src/components/native-chat/native-chat-launch-session-options.ts +++ b/src/renderer/src/components/native-chat/native-chat-launch-session-options.ts @@ -1,4 +1,5 @@ -import type { GlobalSettings, TuiAgent } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { SessionOptionValue } from '../../../../shared/native-chat-session-options' import { decideInitialAgentTabViewMode, diff --git a/src/renderer/src/components/native-chat/native-chat-leaf-routing.ts b/src/renderer/src/components/native-chat/native-chat-leaf-routing.ts index 1262f010ca5..2dfc25e77f8 100644 --- a/src/renderer/src/components/native-chat/native-chat-leaf-routing.ts +++ b/src/renderer/src/components/native-chat/native-chat-leaf-routing.ts @@ -1,8 +1,8 @@ import type { TerminalLayoutSnapshot, - TerminalPaneLayoutNode, - TuiAgent -} from '../../../../shared/types' + TerminalPaneLayoutNode +} from '../../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../../shared/tui-agent' function layoutNodeContainsLeaf(node: TerminalPaneLayoutNode | null, leafId: string): boolean { if (!node) { diff --git a/src/renderer/src/components/native-chat/native-chat-pane-resolution.test.ts b/src/renderer/src/components/native-chat/native-chat-pane-resolution.test.ts index 1193450e3cb..42f9605fdea 100644 --- a/src/renderer/src/components/native-chat/native-chat-pane-resolution.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-pane-resolution.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { resolveNativeChatSession } from './native-chat-pane-resolution' function entry( diff --git a/src/renderer/src/components/native-chat/native-chat-pane-resolution.ts b/src/renderer/src/components/native-chat/native-chat-pane-resolution.ts index 181895a4878..a363b977ff1 100644 --- a/src/renderer/src/components/native-chat/native-chat-pane-resolution.ts +++ b/src/renderer/src/components/native-chat/native-chat-pane-resolution.ts @@ -1,5 +1,5 @@ import type { AgentStatusEntry, AgentType } from '../../../../shared/agent-status-types' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { isNativeChatSupportedAgent } from './native-chat-availability' /** Inputs that resolve the active pane to the agent/session/pty triple the diff --git a/src/renderer/src/components/native-chat/native-chat-runtime-owner.test.ts b/src/renderer/src/components/native-chat/native-chat-runtime-owner.test.ts index 3f3b3cf8e7b..6841d9a0685 100644 --- a/src/renderer/src/components/native-chat/native-chat-runtime-owner.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-runtime-owner.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { selectNativeChatRuntimeEnvironmentId, type NativeChatRuntimeOwnerState diff --git a/src/renderer/src/components/native-chat/native-chat-view-types.ts b/src/renderer/src/components/native-chat/native-chat-view-types.ts index ca374bab11c..de095f9b232 100644 --- a/src/renderer/src/components/native-chat/native-chat-view-types.ts +++ b/src/renderer/src/components/native-chat/native-chat-view-types.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { NativeChatSession } from '../../../../shared/native-chat-types' import type { NativeChatContextMenuActions } from './use-native-chat-context-menu' diff --git a/src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts b/src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts index 3096077e820..90c84470cee 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react' import { useAppStore } from '../../store' import type { AgentType } from '../../../../shared/agent-status-types' -import type { TerminalLayoutSnapshot } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' import { resolveCommittedTitleAgentType } from '@/lib/pane-agent-evidence' import { canToggleNativeChat } from './native-chat-availability' import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' diff --git a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.jira-accessibility.test.tsx b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.jira-accessibility.test.tsx index 062101e7545..b11b8b20ac1 100644 --- a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.jira-accessibility.test.tsx +++ b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.jira-accessibility.test.tsx @@ -3,7 +3,7 @@ import React, { act } from 'react' import { cleanup, createEvent, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { JiraIssue } from '../../../../shared/types' +import type { JiraIssue } from '../../../../shared/jira-types' import SmartWorkspaceNameField, { type SmartWorkspaceNameSelection } from './SmartWorkspaceNameField' diff --git a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx index 66dd756e18f..58e771477dd 100644 --- a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx +++ b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx @@ -74,14 +74,11 @@ import { } from '../../../../shared/new-workspace/smart-workspace-linear-intent' import { isSmartWorkspaceSourceQueryWithinLimit } from '../../../../shared/new-workspace/smart-workspace-source-query' import { filterAvailableTaskProviders } from '../../../../shared/task-providers' -import type { - BaseRefSearchResult, - GitHubWorkItem, - GitLabWorkItem, - JiraIssue, - JiraSite, - LinearIssue -} from '../../../../shared/types' +import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' +import type { GitLabWorkItem } from '../../../../shared/gitlab-types' +import type { JiraIssue, JiraSite } from '../../../../shared/jira-types' +import type { LinearIssue } from '../../../../shared/linear/issue-types' +import type { BaseRefSearchResult } from '../../../../shared/repo-types' import { resolveSmartWorkspaceCommandValue } from './smart-workspace-command-value' import { isComposerFieldToFieldFocus } from './smart-workspace-source-popover-focus' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/new-workspace/run-target-options.ts b/src/renderer/src/components/new-workspace/run-target-options.ts index a07a094cf80..b95d128831d 100644 --- a/src/renderer/src/components/new-workspace/run-target-options.ts +++ b/src/renderer/src/components/new-workspace/run-target-options.ts @@ -1,4 +1,4 @@ -import type { OrcaHooks } from '../../../../shared/types' +import type { OrcaHooks } from '../../../../shared/orca-yaml-hook-types' import type { NeedsSetupProjectHostOption, ProjectHostSetupOption, diff --git a/src/renderer/src/components/new-workspace/use-jira-source-connection.test.tsx b/src/renderer/src/components/new-workspace/use-jira-source-connection.test.tsx index e29f5bd7acf..1da6ffaad00 100644 --- a/src/renderer/src/components/new-workspace/use-jira-source-connection.test.tsx +++ b/src/renderer/src/components/new-workspace/use-jira-source-connection.test.tsx @@ -4,7 +4,7 @@ import { act, renderHook } from '@testing-library/react' import type * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { TaskSourceContext } from '../../../../shared/task-source-context' -import type { JiraConnectionStatus } from '../../../../shared/types' +import type { JiraConnectionStatus } from '../../../../shared/jira-types' import { getJiraSourceConnectionRevisionKey, useJiraSourceConnection diff --git a/src/renderer/src/components/new-workspace/use-jira-source-connection.ts b/src/renderer/src/components/new-workspace/use-jira-source-connection.ts index 49884020d1b..703e87cf9e7 100644 --- a/src/renderer/src/components/new-workspace/use-jira-source-connection.ts +++ b/src/renderer/src/components/new-workspace/use-jira-source-connection.ts @@ -5,7 +5,7 @@ import { getTaskSourceRuntimeSettings, type TaskSourceContext } from '../../../../shared/task-source-context' -import type { JiraConnectionStatus } from '../../../../shared/types' +import type { JiraConnectionStatus } from '../../../../shared/jira-types' import { getProviderRuntimeContextKey } from '@/lib/provider-runtime-context' export type JiraSourceConnection = { diff --git a/src/renderer/src/components/new-workspace/use-jira-url-source.test.tsx b/src/renderer/src/components/new-workspace/use-jira-url-source.test.tsx index 1f6df54bfb2..fe4adf51e85 100644 --- a/src/renderer/src/components/new-workspace/use-jira-url-source.test.tsx +++ b/src/renderer/src/components/new-workspace/use-jira-url-source.test.tsx @@ -2,7 +2,7 @@ import { act, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { JiraConnectionStatus, JiraIssue, JiraSite } from '../../../../shared/types' +import type { JiraConnectionStatus, JiraIssue, JiraSite } from '../../../../shared/jira-types' import type { TaskSourceContext } from '../../../../shared/task-source-context' import { useJiraUrlSource } from './use-jira-url-source' diff --git a/src/renderer/src/components/new-workspace/use-jira-url-source.ts b/src/renderer/src/components/new-workspace/use-jira-url-source.ts index 0441e37edc9..f3a922a7171 100644 --- a/src/renderer/src/components/new-workspace/use-jira-url-source.ts +++ b/src/renderer/src/components/new-workspace/use-jira-url-source.ts @@ -15,7 +15,7 @@ import { } from '../../../../shared/task-source-context' import { parseExecutionHostId } from '../../../../shared/execution-host' import { WORKTREE_LINKED_WORK_ITEM_CONTEXT_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version' -import type { JiraIssue, JiraSite } from '../../../../shared/types' +import type { JiraIssue, JiraSite } from '../../../../shared/jira-types' import { canReuseLoadedJiraStatus, type JiraSourceConnection } from './use-jira-source-connection' const LOOKUP_DEBOUNCE_MS = 200 diff --git a/src/renderer/src/components/new-workspace/use-recent-project-ids.test.ts b/src/renderer/src/components/new-workspace/use-recent-project-ids.test.ts index 44fddd40d08..aa82e46ea46 100644 --- a/src/renderer/src/components/new-workspace/use-recent-project-ids.test.ts +++ b/src/renderer/src/components/new-workspace/use-recent-project-ids.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { orderProjectIdsByRecency } from './use-recent-project-ids' function worktree(partial: Partial): Worktree { diff --git a/src/renderer/src/components/new-workspace/use-recent-project-ids.ts b/src/renderer/src/components/new-workspace/use-recent-project-ids.ts index 3c68485ac52..e4f4c26a017 100644 --- a/src/renderer/src/components/new-workspace/use-recent-project-ids.ts +++ b/src/renderer/src/components/new-workspace/use-recent-project-ids.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { useAllWorktrees } from '@/store/selectors' import { NEW_WORKSPACE_PROJECT_GROUP_OPTION_PREFIX } from '@/lib/new-workspace-project-options' diff --git a/src/renderer/src/components/notification-sound-options.ts b/src/renderer/src/components/notification-sound-options.ts index 1f0ca95597b..b1485939d9c 100644 --- a/src/renderer/src/components/notification-sound-options.ts +++ b/src/renderer/src/components/notification-sound-options.ts @@ -13,7 +13,7 @@ import { type LucideIcon } from 'lucide-react' import { basename } from '@/lib/path' -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' import { createLocalizedCatalog } from '@/i18n/localized-catalog' diff --git a/src/renderer/src/components/notifications/mac-notification-permission-card.tsx b/src/renderer/src/components/notifications/mac-notification-permission-card.tsx index 2bc94664d1c..fca7aab97d5 100644 --- a/src/renderer/src/components/notifications/mac-notification-permission-card.tsx +++ b/src/renderer/src/components/notifications/mac-notification-permission-card.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { BellRing, Check, Settings, TriangleAlert } from 'lucide-react' -import type { NotificationDeliveryProbeResult } from '../../../../shared/types' +import type { NotificationDeliveryProbeResult } from '../../../../shared/notification-settings-types' import { Button } from '@/components/ui/button' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/onboarding/AgentStep.tsx b/src/renderer/src/components/onboarding/AgentStep.tsx index d5e18e8070f..234d854d27e 100644 --- a/src/renderer/src/components/onboarding/AgentStep.tsx +++ b/src/renderer/src/components/onboarding/AgentStep.tsx @@ -5,7 +5,7 @@ import { cn } from '@/lib/utils' import { Checkbox } from '@/components/ui/checkbox' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { translate } from '@/i18n/i18n' const AGENT_GRID_MAX_ROWS = 4 diff --git a/src/renderer/src/components/onboarding/GhosttyDiscoveryRow.tsx b/src/renderer/src/components/onboarding/GhosttyDiscoveryRow.tsx index a9e44f218b6..5c5d8641f53 100644 --- a/src/renderer/src/components/onboarding/GhosttyDiscoveryRow.tsx +++ b/src/renderer/src/components/onboarding/GhosttyDiscoveryRow.tsx @@ -1,5 +1,5 @@ import { Check } from 'lucide-react' -import type { GhosttyImportPreview } from '../../../../shared/types' +import type { GhosttyImportPreview } from '../../../../shared/global-settings-types' import ghosttyIcon from '../../../../../resources/ghostty.svg' import { translate } from '@/i18n/i18n' import type { DiscoveryState } from './ThemeStep' diff --git a/src/renderer/src/components/onboarding/NotificationStep.test.tsx b/src/renderer/src/components/onboarding/NotificationStep.test.tsx index 04bd27f9833..854dcbe763c 100644 --- a/src/renderer/src/components/onboarding/NotificationStep.test.tsx +++ b/src/renderer/src/components/onboarding/NotificationStep.test.tsx @@ -1,6 +1,6 @@ import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it, vi } from 'vitest' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { NotificationStep } from './NotificationStep' import { resolveMacNotificationPermissionState } from '../notifications/mac-notification-permission-card' diff --git a/src/renderer/src/components/onboarding/NotificationStep.tsx b/src/renderer/src/components/onboarding/NotificationStep.tsx index 711677552dd..2f37329ace1 100644 --- a/src/renderer/src/components/onboarding/NotificationStep.tsx +++ b/src/renderer/src/components/onboarding/NotificationStep.tsx @@ -1,7 +1,7 @@ import { useCallback, useRef, useState } from 'react' import { BellRing, FileAudio, Upload } from 'lucide-react' import { toast } from 'sonner' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { Button } from '@/components/ui/button' import { Select, diff --git a/src/renderer/src/components/onboarding/OnboardingFlow.tsx b/src/renderer/src/components/onboarding/OnboardingFlow.tsx index f32b401fff2..9e82e42547c 100644 --- a/src/renderer/src/components/onboarding/OnboardingFlow.tsx +++ b/src/renderer/src/components/onboarding/OnboardingFlow.tsx @@ -3,7 +3,7 @@ import { cn } from '@/lib/utils' import { isEditableTarget } from '@/lib/editable-target' import { getScreenSubmitModifierLabel, isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' -import type { OnboardingState } from '../../../../shared/types' +import type { OnboardingState } from '../../../../shared/onboarding-state-types' import { AgentStep } from './AgentStep' import { ThemeStep } from './ThemeStep' import { NotificationStep } from './NotificationStep' diff --git a/src/renderer/src/components/onboarding/ThemeStep.tsx b/src/renderer/src/components/onboarding/ThemeStep.tsx index afb0bded49a..bb797a12a6e 100644 --- a/src/renderer/src/components/onboarding/ThemeStep.tsx +++ b/src/renderer/src/components/onboarding/ThemeStep.tsx @@ -5,11 +5,8 @@ import { cn } from '@/lib/utils' import { track } from '@/lib/telemetry' import { useMountedRef } from '@/hooks/useMountedRef' import { GhosttyDiscoveryRow } from './GhosttyDiscoveryRow' -import type { - DiscoveryStatusEmitted, - GhosttyImportPreview, - GlobalSettings -} from '../../../../shared/types' +import type { GhosttyImportPreview, GlobalSettings } from '../../../../shared/global-settings-types' +import type { DiscoveryStatusEmitted } from '../../../../shared/onboarding-state-types' import { translate } from '@/i18n/i18n' import { ChromePreview } from './theme-chrome-preview' diff --git a/src/renderer/src/components/onboarding/WindowsTerminalStep.test.tsx b/src/renderer/src/components/onboarding/WindowsTerminalStep.test.tsx index f7047d37bbb..aee8ce2aa2d 100644 --- a/src/renderer/src/components/onboarding/WindowsTerminalStep.test.tsx +++ b/src/renderer/src/components/onboarding/WindowsTerminalStep.test.tsx @@ -1,7 +1,7 @@ import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it, vi } from 'vitest' import { WINDOWS_GIT_BASH_SHELL } from '../../../../shared/windows-terminal-shell' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { WindowsTerminalStep } from './WindowsTerminalStep' function createSettings(overrides: Partial = {}): GlobalSettings { diff --git a/src/renderer/src/components/onboarding/WindowsTerminalStep.tsx b/src/renderer/src/components/onboarding/WindowsTerminalStep.tsx index a821dd22bb5..257e9d47b9c 100644 --- a/src/renderer/src/components/onboarding/WindowsTerminalStep.tsx +++ b/src/renderer/src/components/onboarding/WindowsTerminalStep.tsx @@ -2,7 +2,7 @@ import { Check } from 'lucide-react' import { useCallback, useState } from 'react' import type { BuiltInWindowsTerminalShell } from '../../../../shared/windows-terminal-shell' import { WINDOWS_GIT_BASH_SHELL } from '../../../../shared/windows-terminal-shell' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { cn } from '@/lib/utils' import { useWindowsTerminalCapabilities } from '@/lib/windows-terminal-capabilities' import { SettingsSegmentedControl } from '../settings/SettingsFormControls' diff --git a/src/renderer/src/components/onboarding/agent-picked-payload.ts b/src/renderer/src/components/onboarding/agent-picked-payload.ts index 90d146effeb..2c2cabc9441 100644 --- a/src/renderer/src/components/onboarding/agent-picked-payload.ts +++ b/src/renderer/src/components/onboarding/agent-picked-payload.ts @@ -7,7 +7,11 @@ import { tuiAgentToAgentKind } from '@/lib/telemetry' import type { EventProps } from '../../../../shared/telemetry-events' -import type { PathSource, ShellHydrationFailureReason, TuiAgent } from '../../../../shared/types' +import type { + PathSource, + ShellHydrationFailureReason +} from '../../../../shared/shell-path-hydration-types' +import type { TuiAgent } from '../../../../shared/tui-agent' export type AgentPickedSnapshot = { agent: TuiAgent diff --git a/src/renderer/src/components/onboarding/onboarding-settings-hydration.test.ts b/src/renderer/src/components/onboarding/onboarding-settings-hydration.test.ts index 33dff5e8409..8e11d1d9eec 100644 --- a/src/renderer/src/components/onboarding/onboarding-settings-hydration.test.ts +++ b/src/renderer/src/components/onboarding/onboarding-settings-hydration.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { resolveOnboardingSettingsHydration } from './onboarding-settings-hydration' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' const settings = { theme: 'light', diff --git a/src/renderer/src/components/onboarding/onboarding-settings-hydration.ts b/src/renderer/src/components/onboarding/onboarding-settings-hydration.ts index 8c0a9e3536d..fc4241d0d1b 100644 --- a/src/renderer/src/components/onboarding/onboarding-settings-hydration.ts +++ b/src/renderer/src/components/onboarding/onboarding-settings-hydration.ts @@ -1,4 +1,5 @@ -import type { GlobalSettings, TuiAgent } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { TuiAgent } from '../../../../shared/tui-agent' export type OnboardingSettingsHydrationUpdate = { settingsHydrated: boolean diff --git a/src/renderer/src/components/onboarding/should-show-onboarding.ts b/src/renderer/src/components/onboarding/should-show-onboarding.ts index c50352a5653..d96f2f8e6f0 100644 --- a/src/renderer/src/components/onboarding/should-show-onboarding.ts +++ b/src/renderer/src/components/onboarding/should-show-onboarding.ts @@ -1,4 +1,4 @@ -import type { OnboardingState } from '../../../../shared/types' +import type { OnboardingState } from '../../../../shared/onboarding-state-types' // Why: split out so App.tsx can gate the lazy without an // eager static import path that pulls the whole flow into the main chunk. diff --git a/src/renderer/src/components/onboarding/show-onboarding-event.ts b/src/renderer/src/components/onboarding/show-onboarding-event.ts index 716c3b4d22f..0e6f139b676 100644 --- a/src/renderer/src/components/onboarding/show-onboarding-event.ts +++ b/src/renderer/src/components/onboarding/show-onboarding-event.ts @@ -1,4 +1,4 @@ -import type { OnboardingState } from '../../../../shared/types' +import type { OnboardingState } from '../../../../shared/onboarding-state-types' const ONBOARDING_REOPENED_EVENT = 'orca:onboarding-reopened' diff --git a/src/renderer/src/components/onboarding/theme-chrome-preview.tsx b/src/renderer/src/components/onboarding/theme-chrome-preview.tsx index 68ef3120ef8..49fdb60a680 100644 --- a/src/renderer/src/components/onboarding/theme-chrome-preview.tsx +++ b/src/renderer/src/components/onboarding/theme-chrome-preview.tsx @@ -1,5 +1,5 @@ import { cn } from '@/lib/utils' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' export function ChromePreview({ variant }: { variant: GlobalSettings['theme'] }) { if (variant === 'system') { diff --git a/src/renderer/src/components/onboarding/use-onboarding-flow-persistence.test.ts b/src/renderer/src/components/onboarding/use-onboarding-flow-persistence.test.ts index 1dfbc0c708a..9a35faac37a 100644 --- a/src/renderer/src/components/onboarding/use-onboarding-flow-persistence.test.ts +++ b/src/renderer/src/components/onboarding/use-onboarding-flow-persistence.test.ts @@ -4,7 +4,7 @@ import { createElement, useEffect, act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultOnboardingState } from '../../../../shared/constants' -import type { OnboardingState } from '../../../../shared/types' +import type { OnboardingState } from '../../../../shared/onboarding-state-types' const trackMock = vi.hoisted(() => vi.fn()) diff --git a/src/renderer/src/components/onboarding/use-onboarding-flow-persistence.ts b/src/renderer/src/components/onboarding/use-onboarding-flow-persistence.ts index 12a8c6e22a2..93b4b62c153 100644 --- a/src/renderer/src/components/onboarding/use-onboarding-flow-persistence.ts +++ b/src/renderer/src/components/onboarding/use-onboarding-flow-persistence.ts @@ -3,7 +3,9 @@ import { track } from '@/lib/telemetry' import { useAppStore } from '@/store' import { ONBOARDING_FINAL_STEP, ONBOARDING_FLOW_VERSION } from '../../../../shared/constants' import type { EventProps } from '../../../../shared/telemetry-events' -import type { GlobalSettings, OnboardingState, TuiAgent } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { OnboardingState } from '../../../../shared/onboarding-state-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { applyAgentPermissionMode } from '../../../../shared/tui-agent-permissions' import type { StepId, StepNumber } from './use-onboarding-flow-types' diff --git a/src/renderer/src/components/onboarding/use-onboarding-flow.ts b/src/renderer/src/components/onboarding/use-onboarding-flow.ts index b5376b363f1..33f7e94d1e2 100644 --- a/src/renderer/src/components/onboarding/use-onboarding-flow.ts +++ b/src/renderer/src/components/onboarding/use-onboarding-flow.ts @@ -8,7 +8,9 @@ import { track } from '@/lib/telemetry' import { buildAgentPickedPayload } from './agent-picked-payload' import { ONBOARDING_FINAL_STEP, ONBOARDING_FLOW_VERSION } from '../../../../shared/constants' import type { EventProps } from '../../../../shared/telemetry-events' -import type { GlobalSettings, OnboardingState, TuiAgent } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { OnboardingState } from '../../../../shared/onboarding-state-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { STEPS, type StepNumber } from './use-onboarding-flow-types' import { persistStep, useCloseWith, usePersistCurrentStep } from './use-onboarding-flow-persistence' import { resolveOnboardingSettingsHydration } from './onboarding-settings-hydration' diff --git a/src/renderer/src/components/onboarding/windows-terminal-onboarding-telemetry.ts b/src/renderer/src/components/onboarding/windows-terminal-onboarding-telemetry.ts index 341f8b73fe6..922f7b866a0 100644 --- a/src/renderer/src/components/onboarding/windows-terminal-onboarding-telemetry.ts +++ b/src/renderer/src/components/onboarding/windows-terminal-onboarding-telemetry.ts @@ -1,6 +1,6 @@ import { WINDOWS_GIT_BASH_SHELL } from '../../../../shared/windows-terminal-shell' import type { EventProps } from '../../../../shared/telemetry-events' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' type WindowsTerminalSnapshot = EventProps<'onboarding_windows_terminal_snapshot'> diff --git a/src/renderer/src/components/pet/PetOverlay.tsx b/src/renderer/src/components/pet/PetOverlay.tsx index 052ff64750a..5f16da6986d 100644 --- a/src/renderer/src/components/pet/PetOverlay.tsx +++ b/src/renderer/src/components/pet/PetOverlay.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' import { usePrefersReducedMotion } from '@/hooks/usePrefersReducedMotion' import { usePetUrl } from './usePetUrl' import type { DetectedSpriteCacheEntry } from './pet-blob-cache' -import type { CustomPet } from '../../../../shared/types' +import type { CustomPet } from '../../../../shared/pet-types' import { useAppStore } from '../../store' import { AGENT_STATUS_STALE_AFTER_MS } from '../../../../shared/agent-status-types' import { diff --git a/src/renderer/src/components/pet/pet-overlay-hit-area.test.tsx b/src/renderer/src/components/pet/pet-overlay-hit-area.test.tsx index adb257ce4b5..360bfdea7c1 100644 --- a/src/renderer/src/components/pet/pet-overlay-hit-area.test.tsx +++ b/src/renderer/src/components/pet/pet-overlay-hit-area.test.tsx @@ -4,7 +4,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, describe, expect, it, vi } from 'vitest' import type { DetectedSpriteCacheEntry } from './pet-blob-cache' -import type { CustomPet } from '../../../../shared/types' +import type { CustomPet } from '../../../../shared/pet-types' type PetUrlState = | { url: string; ready: boolean; sprite: null; detected: null } diff --git a/src/renderer/src/components/pet/usePetUrl.ts b/src/renderer/src/components/pet/usePetUrl.ts index efc9fc05568..9809b228532 100644 --- a/src/renderer/src/components/pet/usePetUrl.ts +++ b/src/renderer/src/components/pet/usePetUrl.ts @@ -1,5 +1,5 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react' -import type { CustomPet } from '../../../../shared/types' +import type { CustomPet } from '../../../../shared/pet-types' import { applyCodexSpriteTimingDefaults } from '../../../../shared/codex-pet-sprite-defaults' import { useAppStore } from '../../store' import { BUNDLED_PET, findBundledPet, isBundledPetId } from './pet-models' diff --git a/src/renderer/src/components/pr-check-counts.test.ts b/src/renderer/src/components/pr-check-counts.test.ts index 8f2fa37c2ef..782597e3231 100644 --- a/src/renderer/src/components/pr-check-counts.test.ts +++ b/src/renderer/src/components/pr-check-counts.test.ts @@ -4,7 +4,7 @@ import { getProviderChecksLabel, summarizeProviderChecks } from '../../../shared/provider-check-summary' -import type { PRCheckDetail } from '../../../shared/types' +import type { PRCheckDetail } from '../../../shared/github/check-types' function check(conclusion: PRCheckDetail['conclusion'], name = String(conclusion)): PRCheckDetail { return { name, status: 'completed', conclusion, url: null } diff --git a/src/renderer/src/components/pr-check-counts.ts b/src/renderer/src/components/pr-check-counts.ts index e5b6d65e482..341c96141c1 100644 --- a/src/renderer/src/components/pr-check-counts.ts +++ b/src/renderer/src/components/pr-check-counts.ts @@ -1,6 +1,6 @@ import { translate } from '../i18n/i18n' import { summarizeProviderChecks } from '../../../shared/provider-check-summary' -import type { PRCheckDetail } from '../../../shared/types' +import type { PRCheckDetail } from '../../../shared/github/check-types' export type PRCheckCounts = { passing: number diff --git a/src/renderer/src/components/pr-checks-fix-prompt.test.ts b/src/renderer/src/components/pr-checks-fix-prompt.test.ts index b3f2eadf600..10623b46df9 100644 --- a/src/renderer/src/components/pr-checks-fix-prompt.test.ts +++ b/src/renderer/src/components/pr-checks-fix-prompt.test.ts @@ -6,7 +6,7 @@ import { getCheckDetailsPromptKey, truncateLogTailForPrompt } from './pr-checks-fix-prompt' -import type { PRCheckDetail, PRCheckRunDetails } from '../../../shared/types' +import type { PRCheckDetail, PRCheckRunDetails } from '../../../shared/github/check-types' const failingCheck: PRCheckDetail = { name: 'unit', diff --git a/src/renderer/src/components/pr-comments-resolution-prompt.test.ts b/src/renderer/src/components/pr-comments-resolution-prompt.test.ts index 902481e9ba3..9ff4fad596c 100644 --- a/src/renderer/src/components/pr-comments-resolution-prompt.test.ts +++ b/src/renderer/src/components/pr-comments-resolution-prompt.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { PRComment } from '../../../shared/types' +import type { PRComment } from '../../../shared/github/comment-types' import { groupPRComments } from '../../../shared/pr-comment-groups' import { buildPRCommentsResolutionPrompt, diff --git a/src/renderer/src/components/pr-comments-resolution-prompt.ts b/src/renderer/src/components/pr-comments-resolution-prompt.ts index d86a4e8d653..8f83fabc3d3 100644 --- a/src/renderer/src/components/pr-comments-resolution-prompt.ts +++ b/src/renderer/src/components/pr-comments-resolution-prompt.ts @@ -1,4 +1,4 @@ -import type { PRComment } from '../../../shared/types' +import type { PRComment } from '../../../shared/github/comment-types' import type { PRCommentGroup } from '../../../shared/pr-comment-groups' export type PRCommentsResolutionReviewKind = 'PR' | 'MR' diff --git a/src/renderer/src/components/provider-check-classification-parity.test.ts b/src/renderer/src/components/provider-check-classification-parity.test.ts index 688f61a978e..bedb0663739 100644 --- a/src/renderer/src/components/provider-check-classification-parity.test.ts +++ b/src/renderer/src/components/provider-check-classification-parity.test.ts @@ -9,7 +9,8 @@ import { summarizeProviderChecks } from '../../../shared/provider-check-summary' import type { GitLabPipelineJob } from '../../../shared/gitlab-types' -import type { PRCheckDetail, ProviderCheckSummary } from '../../../shared/types' +import type { PRCheckDetail } from '../../../shared/github/check-types' +import type { ProviderCheckSummary } from '../../../shared/github/pull-request-types' function completed(conclusion: string): PRCheckDetail { return { diff --git a/src/renderer/src/components/quick-open-file-list.react.test.tsx b/src/renderer/src/components/quick-open-file-list.react.test.tsx index fc00fac9891..fa2a288d7ac 100644 --- a/src/renderer/src/components/quick-open-file-list.react.test.tsx +++ b/src/renderer/src/components/quick-open-file-list.react.test.tsx @@ -3,7 +3,8 @@ import { act, createElement } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { FolderWorkspace, ProjectGroup } from '../../../shared/types' +import type { FolderWorkspace } from '../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../shared/project-group-types' import { folderWorkspaceKey } from '../../../shared/workspace-scope' import { useAppStore } from '@/store' import type { AppState } from '@/store/types' diff --git a/src/renderer/src/components/quick-open-file-list.test.ts b/src/renderer/src/components/quick-open-file-list.test.ts index fa6e695e998..be5370b4d8c 100644 --- a/src/renderer/src/components/quick-open-file-list.test.ts +++ b/src/renderer/src/components/quick-open-file-list.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' import { buildExcludePathPrefixes } from '../../../shared/quick-open-filter' import { getRuntimeFileListTarget, diff --git a/src/renderer/src/components/quick-open-file-list.ts b/src/renderer/src/components/quick-open-file-list.ts index bb539749353..80a8dbac6d9 100644 --- a/src/renderer/src/components/quick-open-file-list.ts +++ b/src/renderer/src/components/quick-open-file-list.ts @@ -1,7 +1,7 @@ /* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: quick-open file lists are fetched over local or SSH runtime IPC, so loading/error/results track the request lifecycle. */ import { useEffect, useMemo, useRef, useState } from 'react' import { useShallow } from 'zustand/react/shallow' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' import { isWindowsAbsolutePathLike } from '../../../shared/cross-platform-path' import { createBrowserUuid } from '@/lib/browser-uuid' import { cancelRuntimeFileList, listRuntimeFiles } from '@/runtime/runtime-file-client' diff --git a/src/renderer/src/components/repo/NestedRepoChecklist.test.tsx b/src/renderer/src/components/repo/NestedRepoChecklist.test.tsx index ab8f3dda663..05864c4a050 100644 --- a/src/renderer/src/components/repo/NestedRepoChecklist.test.tsx +++ b/src/renderer/src/components/repo/NestedRepoChecklist.test.tsx @@ -1,7 +1,7 @@ import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it, vi } from 'vitest' import { NestedRepoChecklist } from './NestedRepoChecklist' -import type { NestedRepoScanResult } from '../../../../shared/types' +import type { NestedRepoScanResult } from '../../../../shared/project-group-types' const scan: NestedRepoScanResult = { selectedPath: '/workspace/platform', diff --git a/src/renderer/src/components/repo/NestedRepoChecklist.tsx b/src/renderer/src/components/repo/NestedRepoChecklist.tsx index 95ea1a41897..77bb3378900 100644 --- a/src/renderer/src/components/repo/NestedRepoChecklist.tsx +++ b/src/renderer/src/components/repo/NestedRepoChecklist.tsx @@ -1,6 +1,6 @@ import { useCallback, useMemo, type Dispatch, type SetStateAction } from 'react' import { GitBranch } from 'lucide-react' -import type { NestedRepoScanResult } from '../../../../shared/types' +import type { NestedRepoScanResult } from '../../../../shared/project-group-types' import { cn } from '@/lib/utils' import { getRepoDisplayLabelKey, getRepoDisplayLabelsByPath } from '@/lib/repo-display-labels' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/repo/NestedRepoScanLimitNotice.test.ts b/src/renderer/src/components/repo/NestedRepoScanLimitNotice.test.ts index 44506bcd5e6..f3279d16939 100644 --- a/src/renderer/src/components/repo/NestedRepoScanLimitNotice.test.ts +++ b/src/renderer/src/components/repo/NestedRepoScanLimitNotice.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { NestedRepoScanResult } from '../../../../shared/types' +import type { NestedRepoScanResult } from '../../../../shared/project-group-types' import { nestedRepoScanLimitText } from './NestedRepoScanLimitNotice' describe('nestedRepoScanLimitText', () => { diff --git a/src/renderer/src/components/repo/NestedRepoScanLimitNotice.tsx b/src/renderer/src/components/repo/NestedRepoScanLimitNotice.tsx index 59066a6675b..38fa6524a18 100644 --- a/src/renderer/src/components/repo/NestedRepoScanLimitNotice.tsx +++ b/src/renderer/src/components/repo/NestedRepoScanLimitNotice.tsx @@ -1,6 +1,6 @@ import { CircleHelp } from 'lucide-react' import { useState } from 'react' -import type { NestedRepoScanResult } from '../../../../shared/types' +import type { NestedRepoScanResult } from '../../../../shared/project-group-types' import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/repo/RepoCombobox.tsx b/src/renderer/src/components/repo/RepoCombobox.tsx index 868c7be2d22..f47833eeca9 100644 --- a/src/renderer/src/components/repo/RepoCombobox.tsx +++ b/src/renderer/src/components/repo/RepoCombobox.tsx @@ -14,7 +14,7 @@ import { isGitRepoKind } from '../../../../shared/repo-kind' import { searchRepos } from '@/lib/repo-search' import { cn } from '@/lib/utils' import { useMountedRef } from '@/hooks/useMountedRef' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import RepoBadgeLabel from './RepoBadgeLabel' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/repo/repo-fork-indicator.tsx b/src/renderer/src/components/repo/repo-fork-indicator.tsx index 896a5217730..b1dcf93fa15 100644 --- a/src/renderer/src/components/repo/repo-fork-indicator.tsx +++ b/src/renderer/src/components/repo/repo-fork-indicator.tsx @@ -1,6 +1,6 @@ import React from 'react' import { GitFork } from 'lucide-react' -import type { GitHubRepositoryIdentity } from '../../../../shared/types' +import type { GitHubRepositoryIdentity } from '../../../../shared/github/pull-request-types' import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip' import { cn } from '@/lib/utils' diff --git a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx index 2d63be3bacf..cfa9030d5f0 100644 --- a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx +++ b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx @@ -58,17 +58,15 @@ import { type PRCommentsListSelectionClearRequest } from './pr-comments-list-selection' import { ENTRY_REFRESH_GRACE_MS, shouldEntryRefresh } from './checks-entry-refresh' +import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/github/check-types' +import type { GitHubReactionContent, PRComment } from '../../../../shared/github/comment-types' +import type { PRRefreshErrorType } from '../../../../shared/github/pull-request-refresh-types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' import type { GitLabDiscussionResolveResult, GitLabProjectRef, - GitLabWorkItemDetails, - PRInfo, - PRCheckDetail, - PRCheckRunDetails, - PRComment, - GitHubReactionContent, - PRRefreshErrorType -} from '../../../../shared/types' + GitLabWorkItemDetails +} from '../../../../shared/gitlab-types' import { getConnectionId } from '@/lib/connection-context' import { buildResolvePullRequestConflictsPrompt, diff --git a/src/renderer/src/components/right-sidebar/FileExplorer.tsx b/src/renderer/src/components/right-sidebar/FileExplorer.tsx index e8191d1b3e7..d52457324c0 100644 --- a/src/renderer/src/components/right-sidebar/FileExplorer.tsx +++ b/src/renderer/src/components/right-sidebar/FileExplorer.tsx @@ -54,7 +54,7 @@ import { useFileExplorerSelection } from './useFileExplorerSelection' import { useFileExplorerVisibleRowProjection } from './useFileExplorerVisibleRowProjection' import { translate } from '@/i18n/i18n' import { CLOSE_ALL_CONTEXT_MENUS_EVENT } from '@/components/tab-bar/SortableTab' -import type { RightSidebarExplorerView } from '../../../../shared/types' +import type { RightSidebarExplorerView } from '../../../../shared/ui-chrome-types' import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' import { createNewTerminalTab } from '@/components/terminal/terminal-tab-create' import { useWorkspaceFileBrowserActionPredicate } from '@/lib/file-preview' diff --git a/src/renderer/src/components/right-sidebar/FileExplorerQueryStrip.tsx b/src/renderer/src/components/right-sidebar/FileExplorerQueryStrip.tsx index 23b89e096a0..cfa1c1a30e1 100644 --- a/src/renderer/src/components/right-sidebar/FileExplorerQueryStrip.tsx +++ b/src/renderer/src/components/right-sidebar/FileExplorerQueryStrip.tsx @@ -1,6 +1,6 @@ import React from 'react' import { FileExplorerViewSwitch } from './FileExplorerViewSwitch' -import type { RightSidebarExplorerView } from '../../../../shared/types' +import type { RightSidebarExplorerView } from '../../../../shared/ui-chrome-types' type FileExplorerQueryStripProps = { view: RightSidebarExplorerView diff --git a/src/renderer/src/components/right-sidebar/FileExplorerRow.tsx b/src/renderer/src/components/right-sidebar/FileExplorerRow.tsx index a5e232cb29e..545dc284da8 100644 --- a/src/renderer/src/components/right-sidebar/FileExplorerRow.tsx +++ b/src/renderer/src/components/right-sidebar/FileExplorerRow.tsx @@ -43,7 +43,7 @@ import { WORKSPACE_FILE_PATH_MIME, WORKSPACE_FILE_PATHS_MIME } from '@/lib/workspace-file-drag' -import type { GitFileStatus } from '../../../../shared/types' +import type { GitFileStatus } from '../../../../shared/git-status-types' import { STATUS_LABELS } from './status-display' import { RENAME_HOTSPOT_ATTR } from './file-explorer-dir-toggle-timing' import type { TreeNode } from './file-explorer-types' diff --git a/src/renderer/src/components/right-sidebar/FileExplorerViewSwitch.tsx b/src/renderer/src/components/right-sidebar/FileExplorerViewSwitch.tsx index ec64942f1cf..f7e32b63c98 100644 --- a/src/renderer/src/components/right-sidebar/FileExplorerViewSwitch.tsx +++ b/src/renderer/src/components/right-sidebar/FileExplorerViewSwitch.tsx @@ -1,7 +1,7 @@ import type React from 'react' import { translate } from '@/i18n/i18n' import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' -import type { RightSidebarExplorerView } from '../../../../shared/types' +import type { RightSidebarExplorerView } from '../../../../shared/ui-chrome-types' type FileExplorerViewSwitchProps = { view: RightSidebarExplorerView diff --git a/src/renderer/src/components/right-sidebar/FileExplorerVirtualRows.tsx b/src/renderer/src/components/right-sidebar/FileExplorerVirtualRows.tsx index d72e7a5ca19..59327faddf5 100644 --- a/src/renderer/src/components/right-sidebar/FileExplorerVirtualRows.tsx +++ b/src/renderer/src/components/right-sidebar/FileExplorerVirtualRows.tsx @@ -2,7 +2,7 @@ import React from 'react' import type { Virtualizer } from '@tanstack/react-virtual' import { dirname, normalizeRelativePath } from '@/lib/path' import { cn } from '@/lib/utils' -import type { GitFileStatus } from '../../../../shared/types' +import type { GitFileStatus } from '../../../../shared/git-status-types' import { FileExplorerRow, InlineInputRow, type InlineInput } from './FileExplorerRow' import { shouldShowIgnoredDecoration, STATUS_COLORS } from './status-display' import type { DirCache, TreeNode } from './file-explorer-types' diff --git a/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksPanel.test.tsx b/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksPanel.test.tsx index 3cd868a5baf..4ee957c5001 100644 --- a/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksPanel.test.tsx +++ b/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksPanel.test.tsx @@ -3,13 +3,11 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { - FolderWorkspace, - PRCheckDetail, - Repo, - Worktree, - WorkspaceLineage -} from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { PRCheckDetail } from '../../../../shared/github/check-types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorkspaceLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { HostedReviewInfo } from '../../../../shared/hosted-review' import { folderWorkspaceKey, worktreeWorkspaceKey } from '../../../../shared/workspace-scope' import { getHostedReviewCacheKey } from '@/store/slices/hosted-review' diff --git a/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksPanel.tsx b/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksPanel.tsx index f2e3bdda640..4c03a998d79 100644 --- a/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksPanel.tsx +++ b/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksPanel.tsx @@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' import { translate } from '@/i18n/i18n' -import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/types' +import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/github/check-types' import { getAttachedWorktreesForFolderWorkspace } from './folder-workspace-attached-worktrees' import { FolderWorkspacePrChecksRow } from './FolderWorkspacePrChecksRow' import type { ParentPrChecksRefreshOutcome, ParentPrChecksRow } from './parent-pr-checks-rows' diff --git a/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.test.tsx b/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.test.tsx index de88bd8454d..67c3a0fc222 100644 --- a/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.test.tsx +++ b/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.test.tsx @@ -3,7 +3,9 @@ import { act, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { CheckStatus, Repo, Worktree } from '../../../../shared/types' +import type { CheckStatus } from '../../../../shared/github/pull-request-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { ParentPrChecksRow, ParentPrChecksRowStatus } from './parent-pr-checks-rows' vi.mock('@/i18n/i18n', () => ({ diff --git a/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.tsx b/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.tsx index cd07701bde7..ac87481de9b 100644 --- a/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.tsx +++ b/src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.tsx @@ -3,7 +3,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip import { cn } from '@/lib/utils' import { openHttpLink } from '@/lib/http-link-routing' import { translate } from '@/i18n/i18n' -import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/types' +import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/github/check-types' import { CHECK_COLOR, CHECK_ICON, diff --git a/src/renderer/src/components/right-sidebar/FolderWorkspaceWorktreesPanel.test.tsx b/src/renderer/src/components/right-sidebar/FolderWorkspaceWorktreesPanel.test.tsx index ddeb4704433..2fcdd1cea93 100644 --- a/src/renderer/src/components/right-sidebar/FolderWorkspaceWorktreesPanel.test.tsx +++ b/src/renderer/src/components/right-sidebar/FolderWorkspaceWorktreesPanel.test.tsx @@ -3,7 +3,9 @@ import { act, type CSSProperties, type MouseEvent, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo, Worktree, WorktreeLineage, WorkspaceLineage } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorkspaceLineage, WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { folderWorkspaceKey, worktreeWorkspaceKey } from '../../../../shared/workspace-scope' import { LINEAGE_CHILDREN_INLINE_OFFSET, diff --git a/src/renderer/src/components/right-sidebar/FolderWorkspaceWorktreesPanel.tsx b/src/renderer/src/components/right-sidebar/FolderWorkspaceWorktreesPanel.tsx index 2a2a5a78588..94c46a545d2 100644 --- a/src/renderer/src/components/right-sidebar/FolderWorkspaceWorktreesPanel.tsx +++ b/src/renderer/src/components/right-sidebar/FolderWorkspaceWorktreesPanel.tsx @@ -5,7 +5,7 @@ import { } from '@/components/sidebar/worktree-list-indentation' import { useAppStore } from '@/store' import { translate } from '@/i18n/i18n' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { getAttachedWorktreesForFolderWorkspace } from './folder-workspace-attached-worktrees' import { useState } from 'react' diff --git a/src/renderer/src/components/right-sidebar/GitHistoryCommitFiles.tsx b/src/renderer/src/components/right-sidebar/GitHistoryCommitFiles.tsx index 4bf5da10cb8..d32e82cdd0f 100644 --- a/src/renderer/src/components/right-sidebar/GitHistoryCommitFiles.tsx +++ b/src/renderer/src/components/right-sidebar/GitHistoryCommitFiles.tsx @@ -10,7 +10,8 @@ import { getFileTypeIcon } from '@/lib/file-type-icons' import { basename, dirname } from '@/lib/path' import { translate } from '@/i18n/i18n' import { formatGitHistoryTimestamp } from './git-history-format' -import type { GitBranchChangeEntry, GitFileStatus } from '../../../../shared/types' +import type { GitBranchChangeEntry } from '../../../../shared/git-diff-compare-types' +import type { GitFileStatus } from '../../../../shared/git-status-types' // State for a single commit's lazily-loaded file list. Owned by GitHistoryPanel, // populated through the onLoadCommitFiles loader supplied by SourceControl. diff --git a/src/renderer/src/components/right-sidebar/GitHistoryPanel.tsx b/src/renderer/src/components/right-sidebar/GitHistoryPanel.tsx index bc60150cd77..c7cb02f4247 100644 --- a/src/renderer/src/components/right-sidebar/GitHistoryPanel.tsx +++ b/src/renderer/src/components/right-sidebar/GitHistoryPanel.tsx @@ -5,7 +5,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip import { cn } from '@/lib/utils' import { ContextMenu, ContextMenuTrigger } from '@/components/ui/context-menu' import type { GitHistoryItem, GitHistoryResult } from '../../../../shared/git-history' -import type { GitBranchChangeEntry } from '../../../../shared/types' +import type { GitBranchChangeEntry } from '../../../../shared/git-diff-compare-types' import { buildDefaultGitHistoryColorMap, buildGitHistoryViewModels diff --git a/src/renderer/src/components/right-sidebar/GitHubPRStackMap.test.tsx b/src/renderer/src/components/right-sidebar/GitHubPRStackMap.test.tsx index e29d0b74361..e33e952f2fc 100644 --- a/src/renderer/src/components/right-sidebar/GitHubPRStackMap.test.tsx +++ b/src/renderer/src/components/right-sidebar/GitHubPRStackMap.test.tsx @@ -3,7 +3,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { GitHubPRStack } from '../../../../shared/types' +import type { GitHubPRStack } from '../../../../shared/github/pull-request-types' import { GitHubPRStackMap } from './GitHubPRStackMap' const stack: GitHubPRStack = { diff --git a/src/renderer/src/components/right-sidebar/GitHubPRStackMap.tsx b/src/renderer/src/components/right-sidebar/GitHubPRStackMap.tsx index e0187b37709..1bbaf2b019b 100644 --- a/src/renderer/src/components/right-sidebar/GitHubPRStackMap.tsx +++ b/src/renderer/src/components/right-sidebar/GitHubPRStackMap.tsx @@ -1,6 +1,9 @@ import { useMemo, useState } from 'react' import { ChevronDown, GitBranch, GitPullRequest } from 'lucide-react' -import type { GitHubPRStack, GitHubPRStackEntry } from '../../../../shared/types' +import type { + GitHubPRStack, + GitHubPRStackEntry +} from '../../../../shared/github/pull-request-types' import { cn } from '@/lib/utils' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx b/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx index cace5ee7d98..a98ea57ab7a 100644 --- a/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx +++ b/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx @@ -12,7 +12,9 @@ import { DropdownMenuSeparator } from '@/components/ui/dropdown-menu' import { presentGitHubPRMergeState } from '@/components/github-pr-merge-state' -import type { PRInfo, Repo, Worktree } from '../../../../shared/types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { resolveGitHubPRMergeMethods } from '../../../../shared/github/pull-request-merge-methods' import { runWorktreeDelete } from '../sidebar/delete-worktree-flow' import { presentGitLabMRMergeState } from './gitlab-mr-merge-state' diff --git a/src/renderer/src/components/right-sidebar/SearchResultItems.test.tsx b/src/renderer/src/components/right-sidebar/SearchResultItems.test.tsx index 1364bdc87d0..18599c352c3 100644 --- a/src/renderer/src/components/right-sidebar/SearchResultItems.test.tsx +++ b/src/renderer/src/components/right-sidebar/SearchResultItems.test.tsx @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { Button } from '@/components/ui/button' import { FileResultRow } from './SearchResultItems' -import type { SearchFileResult } from '../../../../shared/types' +import type { SearchFileResult } from '../../../../shared/code-search-types' type ReactElementLike = { type: unknown diff --git a/src/renderer/src/components/right-sidebar/SearchResultItems.tsx b/src/renderer/src/components/right-sidebar/SearchResultItems.tsx index a7499929ea9..2dc3d1e2f52 100644 --- a/src/renderer/src/components/right-sidebar/SearchResultItems.tsx +++ b/src/renderer/src/components/right-sidebar/SearchResultItems.tsx @@ -12,7 +12,7 @@ import { ContextMenuItem } from '@/components/ui/context-menu' import { normalizeSearchFileMatchCount } from '../../../../shared/search-match-count' -import type { SearchFileResult, SearchMatch } from '../../../../shared/types' +import type { SearchFileResult, SearchMatch } from '../../../../shared/code-search-types' import { translate } from '@/i18n/i18n' // ─── Toggle Button ──────────────────────────────────────── diff --git a/src/renderer/src/components/right-sidebar/SearchResultsPane.tsx b/src/renderer/src/components/right-sidebar/SearchResultsPane.tsx index 952e1a0d99b..b1fc60b7d5e 100644 --- a/src/renderer/src/components/right-sidebar/SearchResultsPane.tsx +++ b/src/renderer/src/components/right-sidebar/SearchResultsPane.tsx @@ -1,6 +1,10 @@ import React from 'react' import { useVirtualizer } from '@tanstack/react-virtual' -import type { SearchFileResult, SearchMatch, SearchResult } from '../../../../shared/types' +import type { + SearchFileResult, + SearchMatch, + SearchResult +} from '../../../../shared/code-search-types' import type { SearchRow } from './search-rows' import { FileResultRow, MatchResultRow } from './SearchResultItems' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/right-sidebar/SourceControl.branch-line-total.test.tsx b/src/renderer/src/components/right-sidebar/SourceControl.branch-line-total.test.tsx index 9f2529973dc..02f6ce6f027 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.branch-line-total.test.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.branch-line-total.test.tsx @@ -4,7 +4,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { TooltipProvider } from '@/components/ui/tooltip' -import type { GitBranchCompareSummary } from '../../../../shared/types' +import type { GitBranchCompareSummary } from '../../../../shared/git-diff-compare-types' import { clearBranchLineTotalRequestGateForTests, getBranchLineTotalMergeBase diff --git a/src/renderer/src/components/right-sidebar/SourceControl.compare-summary.test.ts b/src/renderer/src/components/right-sidebar/SourceControl.compare-summary.test.ts index f00bbe9d78c..e740362f60d 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.compare-summary.test.ts +++ b/src/renderer/src/components/right-sidebar/SourceControl.compare-summary.test.ts @@ -12,7 +12,8 @@ import { shouldRefreshBranchCompareForStatusHead, shouldShowCompareSummary } from './SourceControl' -import type { GitBranchCompareSummary, GitUpstreamStatus } from '../../../../shared/types' +import type { GitBranchCompareSummary } from '../../../../shared/git-diff-compare-types' +import type { GitUpstreamStatus } from '../../../../shared/git-status-types' type ReactElementLike = { type: unknown diff --git a/src/renderer/src/components/right-sidebar/SourceControl.open-file-highlight.test.tsx b/src/renderer/src/components/right-sidebar/SourceControl.open-file-highlight.test.tsx index d3125ee86c7..c1dc570d558 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.open-file-highlight.test.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.open-file-highlight.test.tsx @@ -4,7 +4,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { TooltipProvider } from '@/components/ui/tooltip' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import SourceControl from './SourceControl' const mocks = vi.hoisted(() => { diff --git a/src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx b/src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx index 13dc2479894..0afa6769ea1 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx @@ -6,9 +6,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { TooltipProvider } from '@/components/ui/tooltip' import type { GitBranchChangeEntry, - GitBranchCompareSummary, - GitStatusEntry -} from '../../../../shared/types' + GitBranchCompareSummary +} from '../../../../shared/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import SourceControl from './SourceControl' const mocks = vi.hoisted(() => { diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx index bbfe20317f5..1bab1037bde 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx @@ -201,17 +201,19 @@ import { isBehindOnlyUpstream, shouldForcePushWithLeaseForUpstream } from '../../../../shared/git-upstream-status' +import type { DiffComment } from '../../../../shared/diff-comment-types' import type { - DiffComment, GitBranchChangeEntry, - GitBranchCompareSummary, + GitBranchCompareSummary +} from '../../../../shared/git-diff-compare-types' +import type { GitConflictOperation, - GitPushTarget, GitStatusEntry, - GitUpstreamStatus, - SourceControlViewMode, - TuiAgent -} from '../../../../shared/types' + GitUpstreamStatus +} from '../../../../shared/git-status-types' +import type { TuiAgent } from '../../../../shared/tui-agent' +import type { SourceControlViewMode } from '../../../../shared/ui-chrome-types' +import type { GitPushTarget } from '../../../../shared/worktree/types' import type { HostedReviewCreationEligibility, HostedReviewInfo, diff --git a/src/renderer/src/components/right-sidebar/SourceControl.virtual-file-list.test.tsx b/src/renderer/src/components/right-sidebar/SourceControl.virtual-file-list.test.tsx index 5ff61b17d43..2625ab54441 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.virtual-file-list.test.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.virtual-file-list.test.tsx @@ -4,7 +4,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { TooltipProvider } from '@/components/ui/tooltip' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import SourceControl from './SourceControl' import { SOURCE_CONTROL_FILE_ROW_HEIGHT_PX, diff --git a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.test.tsx b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.test.tsx index 2b87618bd5e..610b6d74b20 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.test.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.test.tsx @@ -6,7 +6,9 @@ import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../../shared/constants' import type { SourceControlActionRecipe } from '../../../../shared/source-control-ai-actions' -import type { GlobalSettings, Repo, TuiAgent } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' +import type { TuiAgent } from '../../../../shared/tui-agent' const mocks = vi.hoisted(() => ({ ensureDetectedAgents: vi.fn(), diff --git a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.tsx b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.tsx index 3a431ea03e0..e6532d3eba3 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.tsx @@ -10,7 +10,7 @@ import type { SourceControlActionRecipe, SourceControlLaunchActionId } from '../../../../shared/source-control-ai-actions' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { LaunchSource } from '../../../../shared/telemetry-events' import type { SourceControlAiWriteTarget } from '../../../../shared/source-control-ai-recipe-save' import { SourceControlAgentActionDialogForm } from './SourceControlAgentActionDialogForm' diff --git a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.test.tsx b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.test.tsx index 6b923a0f58a..8e506ec9ca4 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.test.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.test.tsx @@ -2,7 +2,8 @@ import React, { type ReactNode } from 'react' import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it, vi } from 'vitest' import { SourceControlAgentActionDialogForm } from './SourceControlAgentActionDialogForm' -import type { GlobalSettings, Repo } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' vi.mock('@/components/agent/AgentCombobox', () => ({ default: ({ value }: { value: string | null }) => diff --git a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx index 8f9fff3e657..596a1c19f0f 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx @@ -24,7 +24,9 @@ import type { AgentCatalogEntry } from '@/lib/agent-catalog' import { cn } from '@/lib/utils' import type { SourceControlLaunchActionId } from '../../../../shared/source-control-ai-actions' import type { SourceControlAiWriteTarget } from '../../../../shared/source-control-ai-recipe-save' -import type { GlobalSettings, Repo, TuiAgent } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { SourceControlActionVariableChips } from '../source-control/SourceControlActionVariableChips' import { sourceControlActionRecipeMatchesTarget } from './source-control-action-recipe-match' import type { SourceControlAgentScopeNote } from './source-control-agent-action-dialog-result' diff --git a/src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialog.tsx b/src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialog.tsx index 56c3e52f8b9..f48431b1ef2 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialog.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialog.tsx @@ -12,7 +12,8 @@ import { type ResolvedSourceControlAiGenerationParams } from '../../../../shared/source-control-ai' import type { SourceControlTextActionId } from '../../../../shared/source-control-ai-actions' -import type { GlobalSettings, Repo } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' import type { SourceControlAiWriteTarget } from '../../../../shared/source-control-ai-recipe-save' import { buildBranchNamePrompt } from '../../../../shared/branch-name-from-work' import { buildCommitMessagePrompt } from '../../../../shared/commit-message-generation' diff --git a/src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialogForm.tsx b/src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialogForm.tsx index dc59ac3da85..56402b58850 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialogForm.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialogForm.tsx @@ -22,7 +22,9 @@ import type { ResolvedSourceControlAiGenerationParams } from '../../../../shared import { formatLinkedIssueTemplateValue } from '../../../../shared/source-control-ai-action-variables' import type { SourceControlTextActionId } from '../../../../shared/source-control-ai-actions' import type { SourceControlAiWriteTarget } from '../../../../shared/source-control-ai-recipe-save' -import type { GlobalSettings, Repo, TuiAgent } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { toast } from 'sonner' import { SourceControlActionVariableChips } from '../source-control/SourceControlActionVariableChips' import { sourceControlTextGenerationDefaultsMatchTarget } from './source-control-text-generation-defaults' diff --git a/src/renderer/src/components/right-sidebar/SourceControlTextGenerationParams.ts b/src/renderer/src/components/right-sidebar/SourceControlTextGenerationParams.ts index eb1a5a98fb6..e3c75f3175e 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlTextGenerationParams.ts +++ b/src/renderer/src/components/right-sidebar/SourceControlTextGenerationParams.ts @@ -4,7 +4,7 @@ import { isCustomAgentId } from '../../../../shared/commit-message-agent-spec' import type { ResolvedSourceControlAiGenerationParams } from '../../../../shared/source-control-ai' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' export type CommitMessageGenerationAgentChoice = | ResolvedSourceControlAiGenerationParams['agentId'] diff --git a/src/renderer/src/components/right-sidebar/active-checks-status.test.ts b/src/renderer/src/components/right-sidebar/active-checks-status.test.ts index 7ee80e0e8c5..c81c059a78c 100644 --- a/src/renderer/src/components/right-sidebar/active-checks-status.test.ts +++ b/src/renderer/src/components/right-sidebar/active-checks-status.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { getActiveChecksStatus } from './active-checks-status' import type { AppState } from '../../store/types' -import type { PRInfo } from '../../../../shared/types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' function makePR(status: PRInfo['checksStatus']): PRInfo { return { diff --git a/src/renderer/src/components/right-sidebar/active-checks-status.ts b/src/renderer/src/components/right-sidebar/active-checks-status.ts index a7af63812ca..b2a0a540a5b 100644 --- a/src/renderer/src/components/right-sidebar/active-checks-status.ts +++ b/src/renderer/src/components/right-sidebar/active-checks-status.ts @@ -1,6 +1,6 @@ import type { AppState } from '../../store/types' import { getRepoMapFromState, getWorktreeMapFromState } from '../../store/selectors' -import type { CheckStatus } from '../../../../shared/types' +import type { CheckStatus } from '../../../../shared/github/pull-request-types' import { getGitHubPRCacheKey } from '../../store/slices/github-cache-key' import { getHostedReviewCacheKey } from '../../store/slices/hosted-review-cache-identity' diff --git a/src/renderer/src/components/right-sidebar/activity-bar-buttons.tsx b/src/renderer/src/components/right-sidebar/activity-bar-buttons.tsx index dd67762bbb7..4bab9fd907e 100644 --- a/src/renderer/src/components/right-sidebar/activity-bar-buttons.tsx +++ b/src/renderer/src/components/right-sidebar/activity-bar-buttons.tsx @@ -1,7 +1,7 @@ import React from 'react' import { MoreHorizontal } from 'lucide-react' import type { ActiveRightSidebarTab } from '@/store/slices/editor' -import type { CheckStatus } from '../../../../shared/types' +import type { CheckStatus } from '../../../../shared/github/pull-request-types' import { cn } from '@/lib/utils' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { RIGHT_SIDEBAR_HEADER_NO_DRAG_CLASS_NAME } from './right-sidebar-titlebar-drag-regions' diff --git a/src/renderer/src/components/right-sidebar/ai-vault-original-pane.ts b/src/renderer/src/components/right-sidebar/ai-vault-original-pane.ts index 013684c7870..7cb9c647af3 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-original-pane.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-original-pane.ts @@ -3,7 +3,10 @@ import { resolveRuntimePaneTitleLeafId } from '@/lib/runtime-pane-title-leaf-id' import type { AgentStatusState } from '../../../../shared/agent-status-types' import type { AiVaultSession } from '../../../../shared/ai-vault-types' import { parseLegacyNumericPaneKey, parsePaneKey } from '../../../../shared/stable-pane-id' -import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode } from '../../../../shared/types' +import type { + TerminalLayoutSnapshot, + TerminalPaneLayoutNode +} from '../../../../shared/terminal-tab-types' export type AiVaultOriginalPaneTarget = { paneKey: string diff --git a/src/renderer/src/components/right-sidebar/ai-vault-scope-paths.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-scope-paths.test.ts index bf11a0af23b..237da2fecc8 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-scope-paths.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-scope-paths.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { deriveAiVaultScopeSessionPaths, deriveAiVaultWorkspaceScopePaths diff --git a/src/renderer/src/components/right-sidebar/ai-vault-scope-paths.ts b/src/renderer/src/components/right-sidebar/ai-vault-scope-paths.ts index bcd3391a4fc..cfdef42f3e0 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-scope-paths.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-scope-paths.ts @@ -3,7 +3,8 @@ import { normalizeRuntimePathForComparison } from '../../../../shared/cross-platform-path' import type { ProjectHostSetupProjection } from '../../../../shared/project-host-setup-projection' -import type { ProjectHostSetup, Worktree } from '../../../../shared/types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Worktree } from '../../../../shared/worktree/types' import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree/id' export function deriveAiVaultWorkspaceScopePaths( diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts index 43504842aaa..4ede3cafe71 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts @@ -18,7 +18,7 @@ import { } from '@/lib/ai-vault-resume-target' import type { AiVaultAgent, AiVaultSession } from '../../../../shared/ai-vault-types' import { prepareAiVaultSessionForResume } from '@/lib/ai-vault-session-resume-preparation' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { translate } from '@/i18n/i18n' import { agentLabel } from './ai-vault-session-filters' import { parseWorkspaceKey } from '../../../../shared/workspace-scope' diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-projects.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-projects.test.ts index 6c46a144f86..b3c5d0aa841 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-projects.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-projects.test.ts @@ -1,7 +1,9 @@ import { describe, expect, it } from 'vitest' import type { ProjectHostSetupProjection } from '../../../../shared/project-host-setup-projection' import type { AiVaultSession } from '../../../../shared/ai-vault-types' -import type { Project, ProjectHostSetup, Repo, Worktree } from '../../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { buildAiVaultProjectContext, buildAiVaultSessionProjectById, diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-projects.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-projects.ts index 80ed7f3ced7..f9ad52dd198 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-projects.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-projects.ts @@ -6,7 +6,9 @@ import { } from '../../../../shared/execution-host' import type { ProjectHostSetupProjection } from '../../../../shared/project-host-setup-projection' import type { AiVaultSession } from '../../../../shared/ai-vault-types' -import type { ProjectHostSetup, Repo, Worktree } from '../../../../shared/types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { createNormalizedPathInsideOrEqualMatcher, normalizeRuntimePathForComparison diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-resume.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-resume.test.ts index 97803382577..583e668cd2a 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-resume.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-resume.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { AiVaultSessionWorktreeInfo } from './ai-vault-session-worktree' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import { resolveAiVaultSessionLaunchTarget } from './ai-vault-session-launch-actions' diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-resume.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-resume.ts index 9465144b705..d021fc66b1f 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-resume.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-resume.ts @@ -1,4 +1,5 @@ -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { canResumeAiVaultSessionOnTarget, getAiVaultResumeWorkspaceExecutionHostId, diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-worktree-map.test.tsx b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree-map.test.tsx index 90ea7cd900c..cc71cc9b55a 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-worktree-map.test.tsx +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree-map.test.tsx @@ -2,7 +2,8 @@ import { renderHook } from '@testing-library/react' import { describe, expect, it } from 'vitest' import type { AiVaultSession } from '../../../../shared/ai-vault-types' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { resolveAiVaultSessionWorktreeDisplay, useAiVaultSessionWorktreeMap, diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts index 7f5f0d07b0c..5ceeb12523f 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import type { AiVaultSession } from '../../../../shared/ai-vault-types' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { aiVaultWorktreeCompactPath, aiVaultWorktreeJumpTooltip, diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.ts index a82a68c8f1e..15e489d1b44 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.ts @@ -13,7 +13,8 @@ import { normalizeRuntimePathForComparison } from '../../../../shared/cross-platform-path' import type { AiVaultSession } from '../../../../shared/ai-vault-types' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { aiVaultWorktreeCompactPath } from './ai-vault-session-worktree-affordances' export { diff --git a/src/renderer/src/components/right-sidebar/buildSourceControlAgentDeliveryPlan.ts b/src/renderer/src/components/right-sidebar/buildSourceControlAgentDeliveryPlan.ts index 7093101eeee..10d17bd5dc8 100644 --- a/src/renderer/src/components/right-sidebar/buildSourceControlAgentDeliveryPlan.ts +++ b/src/renderer/src/components/right-sidebar/buildSourceControlAgentDeliveryPlan.ts @@ -1,6 +1,6 @@ import { planSourceControlAgentActionLaunch } from '@/lib/source-control-agent-action-plan' import { useAppStore } from '@/store' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { SourceControlAgentActionDeliveryPlanState } from './SourceControlAgentActionDialogForm' import { buildSourceControlAgentConnectionErrorPlan } from './source-control-agent-action-dialog-support' import { resolveInitialNativeChatSessionOptions } from '@/components/native-chat/native-chat-launch-session-options' diff --git a/src/renderer/src/components/right-sidebar/checks-list-expanded-details.test.tsx b/src/renderer/src/components/right-sidebar/checks-list-expanded-details.test.tsx index 8555d53b345..4fac43a4ea1 100644 --- a/src/renderer/src/components/right-sidebar/checks-list-expanded-details.test.tsx +++ b/src/renderer/src/components/right-sidebar/checks-list-expanded-details.test.tsx @@ -4,7 +4,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { TooltipProvider } from '@/components/ui/tooltip' -import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/types' +import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/github/check-types' import { ChecksList } from './checks-panel-content' globalThis.IS_REACT_ACT_ENVIRONMENT = true diff --git a/src/renderer/src/components/right-sidebar/checks-panel-async-result-key.ts b/src/renderer/src/components/right-sidebar/checks-panel-async-result-key.ts index fc95acb9a7c..7cdc9326a36 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-async-result-key.ts +++ b/src/renderer/src/components/right-sidebar/checks-panel-async-result-key.ts @@ -1,4 +1,4 @@ -import type { GitHubOwnerRepo } from '../../../../shared/types' +import type { GitHubOwnerRepo } from '../../../../shared/github/pull-request-types' import { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key' function normalizedPRRepoIdentity(prRepo?: GitHubOwnerRepo | null): string { diff --git a/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx b/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx index 69fd873c89b..78f6eac58f7 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx @@ -2,7 +2,9 @@ import React from 'react' import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it } from 'vitest' import { TooltipProvider } from '@/components/ui/tooltip' -import type { PRCheckDetail, PRComment, PRInfo } from '../../../../shared/types' +import type { PRCheckDetail } from '../../../../shared/github/check-types' +import type { PRComment } from '../../../../shared/github/comment-types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' import { buildMergeabilityRecalculationCommands, CheckJobLogTail, diff --git a/src/renderer/src/components/right-sidebar/checks-panel-content.tsx b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx index ad0824e424f..222fb8f0a0a 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-content.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx @@ -88,16 +88,14 @@ import { type PRCommentPresentationClasses } from './pr-comment-presentation' import type { GitLabProjectRef } from '../../../../shared/gitlab-types' +import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/github/check-types' +import type { GitHubReactionContent, PRComment } from '../../../../shared/github/comment-types' import type { - PRInfo, - PRCheckDetail, - PRCheckRunDetails, - PRComment, - GitHubReactionContent, GitHubRepositoryIdentity, PRConflictSummary, + PRInfo, PRMergeableState -} from '../../../../shared/types' +} from '../../../../shared/github/pull-request-types' import { useCheckDetailsResize } from './check-details-resize' import { RightPanelCommentComposer, diff --git a/src/renderer/src/components/right-sidebar/checks-panel-git-status-snapshot.ts b/src/renderer/src/components/right-sidebar/checks-panel-git-status-snapshot.ts index 78063f3b211..22078dfda37 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-git-status-snapshot.ts +++ b/src/renderer/src/components/right-sidebar/checks-panel-git-status-snapshot.ts @@ -1,4 +1,5 @@ -import type { GitPushTarget, GitStatusEntry, GitUpstreamStatus } from '../../../../shared/types' +import type { GitStatusEntry, GitUpstreamStatus } from '../../../../shared/git-status-types' +import type { GitPushTarget } from '../../../../shared/worktree/types' export type ChecksPanelGitStatusContextInput = { repoId: string | null | undefined diff --git a/src/renderer/src/components/right-sidebar/checks-panel-polling.test.ts b/src/renderer/src/components/right-sidebar/checks-panel-polling.test.ts index 55e453ded19..c4a8568526a 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-polling.test.ts +++ b/src/renderer/src/components/right-sidebar/checks-panel-polling.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { PRCheckDetail } from '../../../../shared/types' +import type { PRCheckDetail } from '../../../../shared/github/check-types' import { CHECKS_PANEL_BASE_POLL_INTERVAL_MS, CHECKS_PANEL_MAX_POLL_INTERVAL_MS, diff --git a/src/renderer/src/components/right-sidebar/checks-panel-polling.ts b/src/renderer/src/components/right-sidebar/checks-panel-polling.ts index 42573079584..89d234a745e 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-polling.ts +++ b/src/renderer/src/components/right-sidebar/checks-panel-polling.ts @@ -1,4 +1,4 @@ -import type { PRCheckDetail } from '../../../../shared/types' +import type { PRCheckDetail } from '../../../../shared/github/check-types' export const CHECKS_PANEL_BASE_POLL_INTERVAL_MS = 30_000 export const CHECKS_PANEL_MAX_POLL_INTERVAL_MS = 120_000 diff --git a/src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-request.ts b/src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-request.ts index 2ade777964d..3558b4bad06 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-request.ts +++ b/src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-request.ts @@ -1,5 +1,5 @@ import type { HostedReviewProvider } from '../../../../shared/hosted-review' -import type { GitHubPRRefreshReason } from '../../../../shared/types' +import type { GitHubPRRefreshReason } from '../../../../shared/github/pull-request-refresh-types' type ChecksPanelPRRefreshRequestInput = { cachedHasPR: boolean | null diff --git a/src/renderer/src/components/right-sidebar/checks-panel-review-copy.ts b/src/renderer/src/components/right-sidebar/checks-panel-review-copy.ts index b4486bec769..33b1d0f9168 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-review-copy.ts +++ b/src/renderer/src/components/right-sidebar/checks-panel-review-copy.ts @@ -1,4 +1,4 @@ -import type { GitHubPRRefreshSkippedReason } from '../../../../shared/types' +import type { GitHubPRRefreshSkippedReason } from '../../../../shared/github/pull-request-refresh-types' import { translate } from '@/i18n/i18n' import { getGitHubUnavailableEmptyStateCopy } from './github-refresh-error-copy' import { diff --git a/src/renderer/src/components/right-sidebar/checks-panel-review-creation.ts b/src/renderer/src/components/right-sidebar/checks-panel-review-creation.ts index d0534124397..7c4260463fa 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-review-creation.ts +++ b/src/renderer/src/components/right-sidebar/checks-panel-review-creation.ts @@ -3,7 +3,7 @@ import type { HostedReviewCreationEligibility, HostedReviewLookupOutcome } from '../../../../shared/hosted-review' -import type { PRRefreshErrorType } from '../../../../shared/types' +import type { PRRefreshErrorType } from '../../../../shared/github/pull-request-refresh-types' import { normalizeHostedReviewBaseRef } from '../../../../shared/hosted-review-refs' import type { ChecksPanelReviewLookup } from './checks-panel-review-lookup-authority' // Single source of truth for the hard-refresh error set, shared with the diff --git a/src/renderer/src/components/right-sidebar/checks-panel-review-state-model.ts b/src/renderer/src/components/right-sidebar/checks-panel-review-state-model.ts index 4b43615ebd9..647f9303190 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-review-state-model.ts +++ b/src/renderer/src/components/right-sidebar/checks-panel-review-state-model.ts @@ -1,5 +1,8 @@ import type { HostedReviewCreationBlockedReason } from '../../../../shared/hosted-review' -import type { GitHubPRRefreshSkippedReason, PRRefreshErrorType } from '../../../../shared/types' +import type { + GitHubPRRefreshSkippedReason, + PRRefreshErrorType +} from '../../../../shared/github/pull-request-refresh-types' import type { ChecksPanelReviewLookup } from './checks-panel-review-lookup-authority' /** diff --git a/src/renderer/src/components/right-sidebar/checks-panel-review.test.ts b/src/renderer/src/components/right-sidebar/checks-panel-review.test.ts index decd6585c8a..6f741b5889f 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-review.test.ts +++ b/src/renderer/src/components/right-sidebar/checks-panel-review.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { gitHubPRToChecksPanelReview, selectChecksPanelReview } from './checks-panel-review' -import type { PRInfo } from '../../../../shared/types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' import type { HostedReviewInfo } from '../../../../shared/hosted-review' function makePR(overrides: Partial = {}): PRInfo { diff --git a/src/renderer/src/components/right-sidebar/checks-panel-review.ts b/src/renderer/src/components/right-sidebar/checks-panel-review.ts index a51ad79079e..17f34421174 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-review.ts +++ b/src/renderer/src/components/right-sidebar/checks-panel-review.ts @@ -1,4 +1,4 @@ -import type { PRInfo } from '../../../../shared/types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' import type { HostedReviewInfo } from '../../../../shared/hosted-review' import { hostedReviewInfoFromGitHubPRInfo } from '../../../../shared/hosted-review-github' diff --git a/src/renderer/src/components/right-sidebar/checks-panel-terminal-worktree.test.ts b/src/renderer/src/components/right-sidebar/checks-panel-terminal-worktree.test.ts index fe817238eb9..5ca81c860da 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-terminal-worktree.test.ts +++ b/src/renderer/src/components/right-sidebar/checks-panel-terminal-worktree.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { resolveChecksPanelTerminalPtyId, resolveChecksPanelWorktreeFromTerminalCwd diff --git a/src/renderer/src/components/right-sidebar/checks-panel-terminal-worktree.ts b/src/renderer/src/components/right-sidebar/checks-panel-terminal-worktree.ts index ab92bbd1469..6aedcdab0dd 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-terminal-worktree.ts +++ b/src/renderer/src/components/right-sidebar/checks-panel-terminal-worktree.ts @@ -5,7 +5,8 @@ import { isRuntimePathAbsolute, normalizeRuntimePathForComparison } from '../../../../shared/cross-platform-path' -import type { TerminalLayoutSnapshot, Worktree } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' type TerminalPtyContext = { activeTabId: string | null diff --git a/src/renderer/src/components/right-sidebar/discard-all-sequence.test.ts b/src/renderer/src/components/right-sidebar/discard-all-sequence.test.ts index e808af6da27..0faedae8114 100644 --- a/src/renderer/src/components/right-sidebar/discard-all-sequence.test.ts +++ b/src/renderer/src/components/right-sidebar/discard-all-sequence.test.ts @@ -8,7 +8,7 @@ import { runDiscardAllForArea, type DiscardAllArea } from './discard-all-sequence' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' function entry(partial: Partial & { path: string }): GitStatusEntry { return { diff --git a/src/renderer/src/components/right-sidebar/discard-all-sequence.ts b/src/renderer/src/components/right-sidebar/discard-all-sequence.ts index c52854fcd91..24f0093c6e1 100644 --- a/src/renderer/src/components/right-sidebar/discard-all-sequence.ts +++ b/src/renderer/src/components/right-sidebar/discard-all-sequence.ts @@ -1,4 +1,4 @@ -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' export type DiscardAllArea = 'staged' | 'unstaged' | 'untracked' diff --git a/src/renderer/src/components/right-sidebar/file-explorer-add-project-action.test.ts b/src/renderer/src/components/right-sidebar/file-explorer-add-project-action.test.ts index 3f36fb04672..86ff9cef435 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer-add-project-action.test.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer-add-project-action.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import type { TreeNode } from './file-explorer-types' import { buildAddProjectFromFolderModalData, diff --git a/src/renderer/src/components/right-sidebar/file-explorer-add-project-action.ts b/src/renderer/src/components/right-sidebar/file-explorer-add-project-action.ts index b8af63bcb6c..162dcac7575 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer-add-project-action.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer-add-project-action.ts @@ -1,6 +1,6 @@ import { isFolderRepo } from '../../../../shared/repo-kind' import { getRepoExecutionHostId, parseExecutionHostId } from '../../../../shared/execution-host' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import type { TreeNode } from './file-explorer-types' export type AddProjectFromFolderModalData = { diff --git a/src/renderer/src/components/right-sidebar/file-explorer-directory-listing.ts b/src/renderer/src/components/right-sidebar/file-explorer-directory-listing.ts index eb8f7142846..d904c910e0b 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer-directory-listing.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer-directory-listing.ts @@ -1,5 +1,5 @@ import { joinPath, normalizeRelativePath } from '@/lib/path' -import type { DirEntry } from '../../../../shared/types' +import type { DirEntry } from '../../../../shared/filesystem-entry-types' import { sortDirEntries } from '../../../../shared/file-name-sort' import { readRuntimeDirectory } from '@/runtime/runtime-file-client' import type { FileExplorerOperationOwner, TreeNode } from './file-explorer-types' diff --git a/src/renderer/src/components/right-sidebar/file-explorer-entries.ts b/src/renderer/src/components/right-sidebar/file-explorer-entries.ts index dfb8591fd2f..4e5b6e7b423 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer-entries.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer-entries.ts @@ -1,4 +1,4 @@ -import type { DirEntry } from '../../../../shared/types' +import type { DirEntry } from '../../../../shared/filesystem-entry-types' export function shouldIncludeFileExplorerEntry(entry: DirEntry): boolean { return entry.name !== '.git' && entry.name !== 'node_modules' diff --git a/src/renderer/src/components/right-sidebar/file-explorer-expanded-dirs-refresh.test.ts b/src/renderer/src/components/right-sidebar/file-explorer-expanded-dirs-refresh.test.ts index b3fdc583522..cbb6b6bb25f 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer-expanded-dirs-refresh.test.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer-expanded-dirs-refresh.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import type { SetStateAction } from 'react' -import type { DirEntry } from '../../../../shared/types' +import type { DirEntry } from '../../../../shared/filesystem-entry-types' import type { DirCache } from './file-explorer-types' import { createFileExplorerDirLoadTracker } from './file-explorer-dir-load-tracker' import { refreshFileExplorerExpandedDirs } from './file-explorer-expanded-dirs-refresh' diff --git a/src/renderer/src/components/right-sidebar/file-explorer-operation-generation.test.ts b/src/renderer/src/components/right-sidebar/file-explorer-operation-generation.test.ts index 0a6c2606b01..d5bb840feeb 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer-operation-generation.test.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer-operation-generation.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { useAppStore } from '@/store' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' diff --git a/src/renderer/src/components/right-sidebar/file-explorer-reset.ts b/src/renderer/src/components/right-sidebar/file-explorer-reset.ts index 9ed03fecda9..83b17724310 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer-reset.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer-reset.ts @@ -1,4 +1,4 @@ -import type { RightSidebarExplorerView } from '../../../../shared/types' +import type { RightSidebarExplorerView } from '../../../../shared/ui-chrome-types' export function getVisibleFileExplorerWorktreePath({ explorerView, diff --git a/src/renderer/src/components/right-sidebar/file-explorer-runtime-owner.ts b/src/renderer/src/components/right-sidebar/file-explorer-runtime-owner.ts index eddc593d31f..a98a38d4edc 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer-runtime-owner.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer-runtime-owner.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { getSettingsForWorktreeRuntimeOwner } from '@/lib/worktree-runtime-owner' import { useAppStore } from '@/store' diff --git a/src/renderer/src/components/right-sidebar/file-explorer-watch-drive-root.test.ts b/src/renderer/src/components/right-sidebar/file-explorer-watch-drive-root.test.ts index 259d3da312c..d7b1380aefa 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer-watch-drive-root.test.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer-watch-drive-root.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { FsChangeEvent } from '../../../../shared/types' +import type { FsChangeEvent } from '../../../../shared/filesystem-entry-types' import type { DirCache } from './file-explorer-types' import { processFileExplorerFsPayload } from './file-explorer-watch-reconcile' diff --git a/src/renderer/src/components/right-sidebar/file-explorer-watch-reconcile.ts b/src/renderer/src/components/right-sidebar/file-explorer-watch-reconcile.ts index 538657edb76..1d3905911ef 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer-watch-reconcile.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer-watch-reconcile.ts @@ -1,5 +1,5 @@ import type { Dispatch, SetStateAction } from 'react' -import type { FsChangedPayload } from '../../../../shared/types' +import type { FsChangedPayload } from '../../../../shared/filesystem-entry-types' import type { DirCache } from './file-explorer-types' import { isPathInsideOrEqual, diff --git a/src/renderer/src/components/right-sidebar/folder-workspace-attached-worktrees.test.ts b/src/renderer/src/components/right-sidebar/folder-workspace-attached-worktrees.test.ts index ed765b0886d..80bf23187a6 100644 --- a/src/renderer/src/components/right-sidebar/folder-workspace-attached-worktrees.test.ts +++ b/src/renderer/src/components/right-sidebar/folder-workspace-attached-worktrees.test.ts @@ -1,10 +1,7 @@ import { describe, expect, it } from 'vitest' -import type { - FolderWorkspace, - Worktree, - WorktreeLineage, - WorkspaceLineage -} from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { WorkspaceLineage, WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { folderWorkspaceKey, worktreeWorkspaceKey } from '../../../../shared/workspace-scope' import { LOCAL_EXECUTION_HOST_ID, toSshExecutionHostId } from '../../../../shared/execution-host' import { getAttachedWorktreesForFolderWorkspace } from './folder-workspace-attached-worktrees' diff --git a/src/renderer/src/components/right-sidebar/folder-workspace-attached-worktrees.ts b/src/renderer/src/components/right-sidebar/folder-workspace-attached-worktrees.ts index ec65782b488..577f21aa1f1 100644 --- a/src/renderer/src/components/right-sidebar/folder-workspace-attached-worktrees.ts +++ b/src/renderer/src/components/right-sidebar/folder-workspace-attached-worktrees.ts @@ -1,10 +1,7 @@ import { folderWorkspaceKey, parseWorkspaceKey } from '../../../../shared/workspace-scope' -import type { - FolderWorkspace, - Worktree, - WorktreeLineage, - WorkspaceLineage -} from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { WorkspaceLineage, WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { compareWorktreeDisplayName } from '@/lib/worktree-display-name-order' import { getProjectedWorktreeLineageChildrenByParentId } from '../sidebar/worktree-lineage-projection' diff --git a/src/renderer/src/components/right-sidebar/fork-push-target-label.test.ts b/src/renderer/src/components/right-sidebar/fork-push-target-label.test.ts index 17c0992b3e3..f2e4ccf0d60 100644 --- a/src/renderer/src/components/right-sidebar/fork-push-target-label.test.ts +++ b/src/renderer/src/components/right-sidebar/fork-push-target-label.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { GitPushTarget } from '../../../../shared/types' +import type { GitPushTarget } from '../../../../shared/worktree/types' import { describeForkPushTarget } from './fork-push-target-label' function target(overrides: Partial): GitPushTarget { diff --git a/src/renderer/src/components/right-sidebar/fork-push-target-label.ts b/src/renderer/src/components/right-sidebar/fork-push-target-label.ts index 7967dcf152a..daff8d89b3c 100644 --- a/src/renderer/src/components/right-sidebar/fork-push-target-label.ts +++ b/src/renderer/src/components/right-sidebar/fork-push-target-label.ts @@ -1,4 +1,4 @@ -import type { GitPushTarget } from '../../../../shared/types' +import type { GitPushTarget } from '../../../../shared/worktree/types' // Why: a fork-PR worktree pushes to a contributor's fork, not origin. Render // "owner:branch" from the fork remote URL when available so the maintainer can diff --git a/src/renderer/src/components/right-sidebar/git-status-file-watch-refresh.ts b/src/renderer/src/components/right-sidebar/git-status-file-watch-refresh.ts index 034cc9e570c..8ab2492f4aa 100644 --- a/src/renderer/src/components/right-sidebar/git-status-file-watch-refresh.ts +++ b/src/renderer/src/components/right-sidebar/git-status-file-watch-refresh.ts @@ -7,11 +7,11 @@ import { normalizeRuntimePathForComparison, relativePathInsideRoot } from '../../../../shared/cross-platform-path' +import type { FsChangedPayload } from '../../../../shared/filesystem-entry-types' import type { ActiveRightSidebarTab, - FsChangedPayload, RightSidebarExplorerView -} from '../../../../shared/types' +} from '../../../../shared/ui-chrome-types' import type { OpenFile } from '@/store/slices/editor' import { ORCA_WORKTREE_FILE_CHANGE_EVENT, diff --git a/src/renderer/src/components/right-sidebar/git-status-refresh-branch-line-total.test.ts b/src/renderer/src/components/right-sidebar/git-status-refresh-branch-line-total.test.ts index 3437d8d227a..4727ebd173b 100644 --- a/src/renderer/src/components/right-sidebar/git-status-refresh-branch-line-total.test.ts +++ b/src/renderer/src/components/right-sidebar/git-status-refresh-branch-line-total.test.ts @@ -9,7 +9,7 @@ import { refreshGitStatusForWorktreeStrict, type GitStatusRefreshDeps } from './git-status-refresh' -import type { GitStatusResult } from '../../../../shared/types' +import type { GitStatusResult } from '../../../../shared/git-status-types' const MERGE_BASE = '1f3c0d9a5b6e7f8091a2b3c4d5e6f708192a3b4c' diff --git a/src/renderer/src/components/right-sidebar/git-status-refresh.test.ts b/src/renderer/src/components/right-sidebar/git-status-refresh.test.ts index 2a3f8ba564e..dd03ae54f87 100644 --- a/src/renderer/src/components/right-sidebar/git-status-refresh.test.ts +++ b/src/renderer/src/components/right-sidebar/git-status-refresh.test.ts @@ -5,7 +5,7 @@ import { refreshGitStatusForWorktreeStrict, type GitStatusRefreshDeps } from './git-status-refresh' -import type { GitStatusResult, GitUpstreamStatus } from '../../../../shared/types' +import type { GitStatusResult, GitUpstreamStatus } from '../../../../shared/git-status-types' function makeDeps(): GitStatusRefreshDeps { return { diff --git a/src/renderer/src/components/right-sidebar/git-status-refresh.ts b/src/renderer/src/components/right-sidebar/git-status-refresh.ts index 50f2cf068c7..ec654b2c175 100644 --- a/src/renderer/src/components/right-sidebar/git-status-refresh.ts +++ b/src/renderer/src/components/right-sidebar/git-status-refresh.ts @@ -6,12 +6,9 @@ import { invalidateAutomaticPushTargetUpstreamStatusCache, storeCachedAutomaticPushTargetUpstreamStatus } from './push-target-upstream-refresh-cache' -import type { - GitPushTarget, - GitStatusResult, - GitUpstreamStatus, - GlobalSettings -} from '../../../../shared/types' +import type { GitStatusResult, GitUpstreamStatus } from '../../../../shared/git-status-types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { GitPushTarget } from '../../../../shared/worktree/types' import { beginAutomaticUpstreamRefresh, beginStrictUpstreamRefresh, diff --git a/src/renderer/src/components/right-sidebar/github-pr-stack-confirmation.ts b/src/renderer/src/components/right-sidebar/github-pr-stack-confirmation.ts index e219f68952d..e3e1e84f1a9 100644 --- a/src/renderer/src/components/right-sidebar/github-pr-stack-confirmation.ts +++ b/src/renderer/src/components/right-sidebar/github-pr-stack-confirmation.ts @@ -1,6 +1,9 @@ import type { ConfirmationDialogOptions } from '@/components/confirmation-dialog-context' import { translate } from '@/i18n/i18n' -import type { GitHubPRMergeMethod, GitHubPRStack } from '../../../../shared/types' +import type { + GitHubPRMergeMethod, + GitHubPRStack +} from '../../../../shared/github/pull-request-types' import { getGitHubPRStackMergeScope } from './github-pr-stack-merge' export function buildGitHubPRStackMergeConfirmation({ diff --git a/src/renderer/src/components/right-sidebar/github-pr-stack-merge.test.ts b/src/renderer/src/components/right-sidebar/github-pr-stack-merge.test.ts index be889a5eb74..44145a61bc0 100644 --- a/src/renderer/src/components/right-sidebar/github-pr-stack-merge.test.ts +++ b/src/renderer/src/components/right-sidebar/github-pr-stack-merge.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { GitHubPRStack } from '../../../../shared/types' +import type { GitHubPRStack } from '../../../../shared/github/pull-request-types' import { getGitHubPRStackMergeBlocker, getGitHubPRStackMergeScope, diff --git a/src/renderer/src/components/right-sidebar/github-pr-stack-merge.ts b/src/renderer/src/components/right-sidebar/github-pr-stack-merge.ts index 8eacfb0380b..5d127d5f736 100644 --- a/src/renderer/src/components/right-sidebar/github-pr-stack-merge.ts +++ b/src/renderer/src/components/right-sidebar/github-pr-stack-merge.ts @@ -1,4 +1,7 @@ -import type { GitHubPRStack, GitHubPRStackEntry } from '../../../../shared/types' +import type { + GitHubPRStack, + GitHubPRStackEntry +} from '../../../../shared/github/pull-request-types' import { translate } from '@/i18n/i18n' export type GitHubPRStackMergeScope = { diff --git a/src/renderer/src/components/right-sidebar/github-refresh-error-copy.ts b/src/renderer/src/components/right-sidebar/github-refresh-error-copy.ts index 298c892e5b0..b22ed9ce23c 100644 --- a/src/renderer/src/components/right-sidebar/github-refresh-error-copy.ts +++ b/src/renderer/src/components/right-sidebar/github-refresh-error-copy.ts @@ -1,4 +1,4 @@ -import type { PRRefreshUpstreamErrorType } from '../../../../shared/types' +import type { PRRefreshUpstreamErrorType } from '../../../../shared/github/pull-request-refresh-types' import { translate } from '@/i18n/i18n' export type ChecksPanelErrorCopy = { title: string; description: string } diff --git a/src/renderer/src/components/right-sidebar/hosted-review-github-actions.ts b/src/renderer/src/components/right-sidebar/hosted-review-github-actions.ts index 2481e5b4293..8940f8adf5b 100644 --- a/src/renderer/src/components/right-sidebar/hosted-review-github-actions.ts +++ b/src/renderer/src/components/right-sidebar/hosted-review-github-actions.ts @@ -1,4 +1,5 @@ -import type { GitHubPRMergeMethod, PRInfo, Repo } from '../../../../shared/types' +import type { GitHubPRMergeMethod, PRInfo } from '../../../../shared/github/pull-request-types' +import type { Repo } from '../../../../shared/repo-types' import { getRepoExecutionHostId, parseExecutionHostId } from '../../../../shared/execution-host' import { callRuntimeRpc, type RuntimeClientTarget } from '@/runtime/runtime-rpc-client' diff --git a/src/renderer/src/components/right-sidebar/parent-pr-checks-github-pr-cache.ts b/src/renderer/src/components/right-sidebar/parent-pr-checks-github-pr-cache.ts index 8487c6619ef..ac8d9223fc4 100644 --- a/src/renderer/src/components/right-sidebar/parent-pr-checks-github-pr-cache.ts +++ b/src/renderer/src/components/right-sidebar/parent-pr-checks-github-pr-cache.ts @@ -1,5 +1,7 @@ import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { PRInfo, Repo, Worktree } from '../../../../shared/types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { LOCAL_EXECUTION_HOST_ID, normalizeExecutionHostId diff --git a/src/renderer/src/components/right-sidebar/parent-pr-checks-hosted-review-cache.ts b/src/renderer/src/components/right-sidebar/parent-pr-checks-hosted-review-cache.ts index e6db09a2736..854af41074a 100644 --- a/src/renderer/src/components/right-sidebar/parent-pr-checks-hosted-review-cache.ts +++ b/src/renderer/src/components/right-sidebar/parent-pr-checks-hosted-review-cache.ts @@ -1,5 +1,6 @@ import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { PRInfo, Worktree } from '../../../../shared/types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getWorktreeCardPrDisplay, isCachedMergedBranchPRCurrentForWorktree diff --git a/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.test.ts b/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.test.ts index 75c288da7fd..025817b1160 100644 --- a/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.test.ts +++ b/src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getHostedReviewCacheKey } from '@/store/slices/hosted-review' import { buildParentPrChecksProjection } from './parent-pr-checks-rows' import { createParentPrChecksProjectionSelector } from './parent-pr-checks-projection-selector' diff --git a/src/renderer/src/components/right-sidebar/parent-pr-checks-refresh.test.ts b/src/renderer/src/components/right-sidebar/parent-pr-checks-refresh.test.ts index 9cf77aa2565..df7f6ff4e2d 100644 --- a/src/renderer/src/components/right-sidebar/parent-pr-checks-refresh.test.ts +++ b/src/renderer/src/components/right-sidebar/parent-pr-checks-refresh.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { HostedReviewInfo } from '../../../../shared/hosted-review' import { getParentPrChecksRefreshCandidates, diff --git a/src/renderer/src/components/right-sidebar/parent-pr-checks-refresh.ts b/src/renderer/src/components/right-sidebar/parent-pr-checks-refresh.ts index 2404950b422..edde3fcd750 100644 --- a/src/renderer/src/components/right-sidebar/parent-pr-checks-refresh.ts +++ b/src/renderer/src/components/right-sidebar/parent-pr-checks-refresh.ts @@ -1,9 +1,7 @@ -import type { - GitHubRepositoryIdentity, - PRCheckDetail, - Repo, - Worktree -} from '../../../../shared/types' +import type { PRCheckDetail } from '../../../../shared/github/check-types' +import type { GitHubRepositoryIdentity } from '../../../../shared/github/pull-request-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { HostedReviewInfo } from '../../../../shared/hosted-review' import { isFolderRepo } from '../../../../shared/repo-kind' import { getWorktreeGitIdentityDisplay } from '@/lib/worktree-git-identity-display' diff --git a/src/renderer/src/components/right-sidebar/parent-pr-checks-row-status.ts b/src/renderer/src/components/right-sidebar/parent-pr-checks-row-status.ts index 4f48f655cf6..478ccec9a49 100644 --- a/src/renderer/src/components/right-sidebar/parent-pr-checks-row-status.ts +++ b/src/renderer/src/components/right-sidebar/parent-pr-checks-row-status.ts @@ -1,4 +1,4 @@ -import type { CheckStatus } from '../../../../shared/types' +import type { CheckStatus } from '../../../../shared/github/pull-request-types' import type { HostedReviewInfo } from '../../../../shared/hosted-review' import { translate } from '@/i18n/i18n' import type { diff --git a/src/renderer/src/components/right-sidebar/parent-pr-checks-row-types.ts b/src/renderer/src/components/right-sidebar/parent-pr-checks-row-types.ts index 18fc4459da6..2904b5565fc 100644 --- a/src/renderer/src/components/right-sidebar/parent-pr-checks-row-types.ts +++ b/src/renderer/src/components/right-sidebar/parent-pr-checks-row-types.ts @@ -1,11 +1,11 @@ +import type { PRCheckDetail } from '../../../../shared/github/check-types' import type { CheckStatus, GitHubRepositoryIdentity, - PRCheckDetail, - PRInfo, - Repo, - Worktree -} from '../../../../shared/types' + PRInfo +} from '../../../../shared/github/pull-request-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { HostedReviewInfo } from '../../../../shared/hosted-review' import type { AppState } from '@/store' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/right-sidebar/parent-pr-checks-rows.test.ts b/src/renderer/src/components/right-sidebar/parent-pr-checks-rows.test.ts index c9e2ebc18cc..4a071f6638e 100644 --- a/src/renderer/src/components/right-sidebar/parent-pr-checks-rows.test.ts +++ b/src/renderer/src/components/right-sidebar/parent-pr-checks-rows.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from 'vitest' -import type { PRCheckDetail, PRInfo, Repo, Worktree } from '../../../../shared/types' +import type { PRCheckDetail } from '../../../../shared/github/check-types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { HostedReviewInfo } from '../../../../shared/hosted-review' import { getHostedReviewCacheKey } from '@/store/slices/hosted-review' import { diff --git a/src/renderer/src/components/right-sidebar/parent-pr-checks-rows.ts b/src/renderer/src/components/right-sidebar/parent-pr-checks-rows.ts index 72f4ab921bc..10d0367f620 100644 --- a/src/renderer/src/components/right-sidebar/parent-pr-checks-rows.ts +++ b/src/renderer/src/components/right-sidebar/parent-pr-checks-rows.ts @@ -1,4 +1,6 @@ -import type { PRCheckDetail, Repo, Worktree } from '../../../../shared/types' +import type { PRCheckDetail } from '../../../../shared/github/check-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { HostedReviewInfo } from '../../../../shared/hosted-review' import { hostedReviewInfoFromGitHubPRInfo } from '../../../../shared/hosted-review-github' import { isFolderRepo } from '../../../../shared/repo-kind' diff --git a/src/renderer/src/components/right-sidebar/pr-comment-fixing-reply-body.ts b/src/renderer/src/components/right-sidebar/pr-comment-fixing-reply-body.ts index 757c5abc303..5122a653ef9 100644 --- a/src/renderer/src/components/right-sidebar/pr-comment-fixing-reply-body.ts +++ b/src/renderer/src/components/right-sidebar/pr-comment-fixing-reply-body.ts @@ -1,4 +1,4 @@ -import type { PRComment } from '../../../../shared/types' +import type { PRComment } from '../../../../shared/github/comment-types' /** Posted when a selected review comment is sent to AI. */ export const PR_COMMENT_AI_FIXING_REPLY = 'Fixing. Will be in the next commit' diff --git a/src/renderer/src/components/right-sidebar/pr-comment-thread-resolution.test.ts b/src/renderer/src/components/right-sidebar/pr-comment-thread-resolution.test.ts index 5cbd180ed3c..2ee6834ecb6 100644 --- a/src/renderer/src/components/right-sidebar/pr-comment-thread-resolution.test.ts +++ b/src/renderer/src/components/right-sidebar/pr-comment-thread-resolution.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { PRComment } from '../../../../shared/types' +import type { PRComment } from '../../../../shared/github/comment-types' import { markPRCommentThreadResolved, restorePRCommentThreadSnapshot diff --git a/src/renderer/src/components/right-sidebar/pr-comment-thread-resolution.ts b/src/renderer/src/components/right-sidebar/pr-comment-thread-resolution.ts index 2af2a42c3d0..eb5b947f77a 100644 --- a/src/renderer/src/components/right-sidebar/pr-comment-thread-resolution.ts +++ b/src/renderer/src/components/right-sidebar/pr-comment-thread-resolution.ts @@ -1,4 +1,4 @@ -import type { PRComment } from '../../../../shared/types' +import type { PRComment } from '../../../../shared/github/comment-types' export function markPRCommentThreadResolved( comments: PRComment[], diff --git a/src/renderer/src/components/right-sidebar/pr-comments-ai-launch-ack.test.ts b/src/renderer/src/components/right-sidebar/pr-comments-ai-launch-ack.test.ts index 3ae9663f314..a0b5a0b19e2 100644 --- a/src/renderer/src/components/right-sidebar/pr-comments-ai-launch-ack.test.ts +++ b/src/renderer/src/components/right-sidebar/pr-comments-ai-launch-ack.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { PRComment } from '../../../../shared/types' +import type { PRComment } from '../../../../shared/github/comment-types' import type { PRCommentGroup } from '../../../../shared/pr-comment-groups' import { acknowledgePRCommentsAfterAiLaunch, diff --git a/src/renderer/src/components/right-sidebar/pr-comments-ai-launch-ack.ts b/src/renderer/src/components/right-sidebar/pr-comments-ai-launch-ack.ts index 6c427f84acc..8f21d3c7920 100644 --- a/src/renderer/src/components/right-sidebar/pr-comments-ai-launch-ack.ts +++ b/src/renderer/src/components/right-sidebar/pr-comments-ai-launch-ack.ts @@ -1,4 +1,5 @@ -import type { PRComment, PRInfo } from '../../../../shared/types' +import type { PRComment } from '../../../../shared/github/comment-types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' import type { HostedReviewInfo } from '../../../../shared/hosted-review' import { getPRCommentGroupRoot, type PRCommentGroup } from '../../../../shared/pr-comment-groups' import { isResolvablePRCommentGroup } from '../pr-comments-resolution-prompt' diff --git a/src/renderer/src/components/right-sidebar/pr-comments-list-selection.test.tsx b/src/renderer/src/components/right-sidebar/pr-comments-list-selection.test.tsx index c6f0130d1c1..94a2c20b45e 100644 --- a/src/renderer/src/components/right-sidebar/pr-comments-list-selection.test.tsx +++ b/src/renderer/src/components/right-sidebar/pr-comments-list-selection.test.tsx @@ -65,7 +65,7 @@ vi.mock('@/components/ui/dropdown-menu', () => { DropdownMenuTrigger: ({ children }: { children: ReactNode }) => <>{children} } }) -import type { PRComment } from '../../../../shared/types' +import type { PRComment } from '../../../../shared/github/comment-types' import type { PRCommentGroup } from '../../../../shared/pr-comment-groups' import { MAX_PERSISTED_PR_COMMENTS_LIST_SELECTIONS, diff --git a/src/renderer/src/components/right-sidebar/pr-comments-list-selection.ts b/src/renderer/src/components/right-sidebar/pr-comments-list-selection.ts index 6245f6ff131..b82c2b4c277 100644 --- a/src/renderer/src/components/right-sidebar/pr-comments-list-selection.ts +++ b/src/renderer/src/components/right-sidebar/pr-comments-list-selection.ts @@ -5,7 +5,7 @@ import { type PRCommentGroup } from '../../../../shared/pr-comment-groups' import { isPRCommentGroupQueueableForAI } from '@/lib/pr-comment-action-state' -import type { PRComment } from '../../../../shared/types' +import type { PRComment } from '../../../../shared/github/comment-types' export type PRCommentsListSelection = { isSelectingForAI: boolean diff --git a/src/renderer/src/components/right-sidebar/push-target-upstream-refresh-cache.test.ts b/src/renderer/src/components/right-sidebar/push-target-upstream-refresh-cache.test.ts index 6ec8514351c..50b76dd829b 100644 --- a/src/renderer/src/components/right-sidebar/push-target-upstream-refresh-cache.test.ts +++ b/src/renderer/src/components/right-sidebar/push-target-upstream-refresh-cache.test.ts @@ -10,7 +10,8 @@ import { invalidateAutomaticPushTargetUpstreamStatusCache, storeCachedAutomaticPushTargetUpstreamStatus } from './push-target-upstream-refresh-cache' -import type { GitPushTarget, GitStatusResult, GitUpstreamStatus } from '../../../../shared/types' +import type { GitStatusResult, GitUpstreamStatus } from '../../../../shared/git-status-types' +import type { GitPushTarget } from '../../../../shared/worktree/types' const pushTarget: GitPushTarget = { remoteName: 'fork', diff --git a/src/renderer/src/components/right-sidebar/push-target-upstream-refresh-cache.ts b/src/renderer/src/components/right-sidebar/push-target-upstream-refresh-cache.ts index bb79bd2a2c2..e8f0a8c39fb 100644 --- a/src/renderer/src/components/right-sidebar/push-target-upstream-refresh-cache.ts +++ b/src/renderer/src/components/right-sidebar/push-target-upstream-refresh-cache.ts @@ -1,9 +1,6 @@ -import type { - GitPushTarget, - GitStatusResult, - GitUpstreamStatus, - GlobalSettings -} from '../../../../shared/types' +import type { GitStatusResult, GitUpstreamStatus } from '../../../../shared/git-status-types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { GitPushTarget } from '../../../../shared/worktree/types' const AUTOMATIC_PUSH_TARGET_UPSTREAM_REFRESH_TTL_MS = 60_000 const MAX_AUTOMATIC_PUSH_TARGET_UPSTREAM_CACHE_ENTRIES = 1024 diff --git a/src/renderer/src/components/right-sidebar/runSourceControlAgentActionStart.ts b/src/renderer/src/components/right-sidebar/runSourceControlAgentActionStart.ts index 278117bb68b..b9b86e93228 100644 --- a/src/renderer/src/components/right-sidebar/runSourceControlAgentActionStart.ts +++ b/src/renderer/src/components/right-sidebar/runSourceControlAgentActionStart.ts @@ -1,6 +1,8 @@ import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface' import { launchAgentInNewTab } from '@/lib/launch-agent-in-new-tab' -import type { GlobalSettings, Repo, TuiAgent } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { LaunchSource } from '../../../../shared/telemetry-events' import type { SourceControlActionRecipe, diff --git a/src/renderer/src/components/right-sidebar/search-match-open.ts b/src/renderer/src/components/right-sidebar/search-match-open.ts index 0e2bf3b74ac..3e870396428 100644 --- a/src/renderer/src/components/right-sidebar/search-match-open.ts +++ b/src/renderer/src/components/right-sidebar/search-match-open.ts @@ -1,6 +1,6 @@ import { detectLanguage } from '@/lib/language-detect' import type { FileSearchResultOwner } from '@/lib/file-search-result-owner' -import type { SearchFileResult, SearchMatch } from '../../../../shared/types' +import type { SearchFileResult, SearchMatch } from '../../../../shared/code-search-types' export function cancelRevealFrame(frameRef: React.RefObject): void { if (frameRef.current !== null) { diff --git a/src/renderer/src/components/right-sidebar/search-rows.ts b/src/renderer/src/components/right-sidebar/search-rows.ts index e9697febea3..031cca36710 100644 --- a/src/renderer/src/components/right-sidebar/search-rows.ts +++ b/src/renderer/src/components/right-sidebar/search-rows.ts @@ -1,4 +1,8 @@ -import type { SearchFileResult, SearchMatch, SearchResult } from '../../../../shared/types' +import type { + SearchFileResult, + SearchMatch, + SearchResult +} from '../../../../shared/code-search-types' export type SearchRow = | { diff --git a/src/renderer/src/components/right-sidebar/source-control-action-error.ts b/src/renderer/src/components/right-sidebar/source-control-action-error.ts index 3e54a65d9a0..2e3ab396df1 100644 --- a/src/renderer/src/components/right-sidebar/source-control-action-error.ts +++ b/src/renderer/src/components/right-sidebar/source-control-action-error.ts @@ -1,5 +1,5 @@ import type { RemoteOpKind } from './source-control-primary-action' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' export type AbortActionErrorKind = 'abort_merge' | 'abort_rebase' export type SourceControlActionErrorKind = RemoteOpKind | AbortActionErrorKind diff --git a/src/renderer/src/components/right-sidebar/source-control-action-recipe-match.test.ts b/src/renderer/src/components/right-sidebar/source-control-action-recipe-match.test.ts index 3c806438ceb..398ce251cd9 100644 --- a/src/renderer/src/components/right-sidebar/source-control-action-recipe-match.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control-action-recipe-match.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { getDefaultSettings } from '../../../../shared/constants' -import type { GlobalSettings, Repo } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' import { sourceControlActionRecipeMatchesTarget } from './source-control-action-recipe-match' function settings(): GlobalSettings { diff --git a/src/renderer/src/components/right-sidebar/source-control-action-recipe-match.ts b/src/renderer/src/components/right-sidebar/source-control-action-recipe-match.ts index 2c1078e9a06..83055b58e77 100644 --- a/src/renderer/src/components/right-sidebar/source-control-action-recipe-match.ts +++ b/src/renderer/src/components/right-sidebar/source-control-action-recipe-match.ts @@ -10,7 +10,8 @@ import { type SourceControlActionRecipe } from '../../../../shared/source-control-ai-actions' import type { SourceControlAiWriteTarget } from '../../../../shared/source-control-ai-recipe-save' -import type { GlobalSettings, Repo } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' type NormalizedSourceControlActionRecipe = { agentId: SourceControlActionRecipe['agentId'] | null diff --git a/src/renderer/src/components/right-sidebar/source-control-actions.ts b/src/renderer/src/components/right-sidebar/source-control-actions.ts index 3ae829cd710..db4efa16b82 100644 --- a/src/renderer/src/components/right-sidebar/source-control-actions.ts +++ b/src/renderer/src/components/right-sidebar/source-control-actions.ts @@ -1,4 +1,4 @@ -import type { GitStagingArea } from '../../../../shared/types' +import type { GitStagingArea } from '../../../../shared/git-status-types' export type SourceControlAction = 'discard' | 'stage' | 'unstage' diff --git a/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-result.ts b/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-result.ts index 5609880477a..6078d17f4b3 100644 --- a/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-result.ts +++ b/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-result.ts @@ -1,7 +1,7 @@ import type { getAgentCatalog } from '@/lib/agent-catalog' import type { useAppStore } from '@/store' import type { useRepoById } from '@/store/selectors' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { SourceControlAgentActionDeliveryPlanState } from './SourceControlAgentActionDialogForm' export type SourceControlAgentScopeNote = { diff --git a/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.ts b/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.ts index ac0448b70d2..a88cc65617d 100644 --- a/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.ts +++ b/src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.ts @@ -1,6 +1,6 @@ import { getAgentCatalog } from '@/lib/agent-catalog' import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { SourceControlAiWriteTarget } from '../../../../shared/source-control-ai-recipe-save' import { translate } from '@/i18n/i18n' import type { SourceControlAgentActionDeliveryPlanState } from './SourceControlAgentActionDialogForm' diff --git a/src/renderer/src/components/right-sidebar/source-control-ai-controller-types.ts b/src/renderer/src/components/right-sidebar/source-control-ai-controller-types.ts index a3c851be789..6a76186f851 100644 --- a/src/renderer/src/components/right-sidebar/source-control-ai-controller-types.ts +++ b/src/renderer/src/components/right-sidebar/source-control-ai-controller-types.ts @@ -1,10 +1,7 @@ import type { AppState } from '@/store' -import type { - GitConflictOperation, - GitStatusEntry, - GlobalSettings, - Repo -} from '../../../../shared/types' +import type { GitConflictOperation, GitStatusEntry } from '../../../../shared/git-status-types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' export type SourceControlAiStoreSnapshot = Pick< AppState, diff --git a/src/renderer/src/components/right-sidebar/source-control-ai-settings-navigation.ts b/src/renderer/src/components/right-sidebar/source-control-ai-settings-navigation.ts index 259bd6f43c0..a5352c08795 100644 --- a/src/renderer/src/components/right-sidebar/source-control-ai-settings-navigation.ts +++ b/src/renderer/src/components/right-sidebar/source-control-ai-settings-navigation.ts @@ -1,6 +1,6 @@ import { getRepositorySourceControlAiSectionId } from '@/components/settings/repository-settings-targets' import type { AppState } from '@/store' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' export function openSourceControlAiSettingsTarget({ activeRepo, diff --git a/src/renderer/src/components/right-sidebar/source-control-branch-context-row.test.tsx b/src/renderer/src/components/right-sidebar/source-control-branch-context-row.test.tsx index 9ccad4f3acc..3d3f6ce6549 100644 --- a/src/renderer/src/components/right-sidebar/source-control-branch-context-row.test.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-branch-context-row.test.tsx @@ -2,7 +2,7 @@ import { renderToStaticMarkup } from 'react-dom/server' import type { ReactNode } from 'react' import { describe, expect, it, vi } from 'vitest' import { SourceControlBranchContextRow } from './source-control-branch-context-row' -import type { GitBranchCompareSummary } from '../../../../shared/types' +import type { GitBranchCompareSummary } from '../../../../shared/git-diff-compare-types' import type { GitBranchLineTotal } from '../../../../shared/git-status-types' vi.mock('@/components/ui/tooltip', () => ({ diff --git a/src/renderer/src/components/right-sidebar/source-control-branch-context-row.tsx b/src/renderer/src/components/right-sidebar/source-control-branch-context-row.tsx index 42a531a10d3..f22348a6f3f 100644 --- a/src/renderer/src/components/right-sidebar/source-control-branch-context-row.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-branch-context-row.tsx @@ -1,7 +1,7 @@ import React from 'react' import { ExternalLink, Loader2, RefreshCw } from 'lucide-react' -import type { GitBranchCompareSummary, GitUpstreamStatus } from '../../../../shared/types' -import type { GitBranchLineTotal } from '../../../../shared/git-status-types' +import type { GitBranchCompareSummary } from '../../../../shared/git-diff-compare-types' +import type { GitBranchLineTotal, GitUpstreamStatus } from '../../../../shared/git-status-types' import { cn } from '@/lib/utils' import { translate } from '@/i18n/i18n' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' diff --git a/src/renderer/src/components/right-sidebar/source-control-branch-context-stats.test.ts b/src/renderer/src/components/right-sidebar/source-control-branch-context-stats.test.ts index be3823e6c3f..25384eb6cd9 100644 --- a/src/renderer/src/components/right-sidebar/source-control-branch-context-stats.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control-branch-context-stats.test.ts @@ -6,7 +6,7 @@ import { shouldShowSourceControlBranchContextChrome, shouldShowSourceControlBranchContextRow } from './source-control-branch-context-stats' -import type { GitBranchCompareSummary } from '../../../../shared/types' +import type { GitBranchCompareSummary } from '../../../../shared/git-diff-compare-types' const readySummary: GitBranchCompareSummary = { baseRef: 'origin/main', diff --git a/src/renderer/src/components/right-sidebar/source-control-branch-context-stats.ts b/src/renderer/src/components/right-sidebar/source-control-branch-context-stats.ts index ed4b801897d..e724a51d366 100644 --- a/src/renderer/src/components/right-sidebar/source-control-branch-context-stats.ts +++ b/src/renderer/src/components/right-sidebar/source-control-branch-context-stats.ts @@ -1,4 +1,5 @@ -import type { GitBranchCompareSummary, GitUpstreamStatus } from '../../../../shared/types' +import type { GitBranchCompareSummary } from '../../../../shared/git-diff-compare-types' +import type { GitUpstreamStatus } from '../../../../shared/git-status-types' import type { WorktreeGitIdentityDisplay } from '@/lib/worktree-git-identity-display' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.test.ts b/src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.test.ts index e71afce388c..0a1dfd8607c 100644 --- a/src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.test.ts @@ -13,7 +13,7 @@ import { shouldAttemptCreateHostedReviewForIntent, shouldGenerateHostedReviewDetailsForIntent } from './source-control-create-pr-intent-flow' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' describe('source-control Create PR intent flow helpers', () => { it('matches async completions only to the original repo, worktree, path, branch, and base', () => { diff --git a/src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.ts b/src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.ts index 20d8e002ba0..3a392535ef5 100644 --- a/src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.ts +++ b/src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.ts @@ -10,7 +10,7 @@ import { normalizeHostedReviewBaseRef, normalizeHostedReviewHeadRef } from '../../../../shared/hosted-review-refs' -import type { GitStatusEntry, GitUpstreamStatus } from '../../../../shared/types' +import type { GitStatusEntry, GitUpstreamStatus } from '../../../../shared/git-status-types' import { summarizeCommitFailure } from './commit-failure-summary' import { getStageAllPaths } from './discard-all-sequence' diff --git a/src/renderer/src/components/right-sidebar/source-control-discard-confirmation.test.ts b/src/renderer/src/components/right-sidebar/source-control-discard-confirmation.test.ts index 48e28160af6..496924874f2 100644 --- a/src/renderer/src/components/right-sidebar/source-control-discard-confirmation.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control-discard-confirmation.test.ts @@ -3,7 +3,7 @@ import { getDiscardAreaConfirmationCopy, getDiscardEntryConfirmationCopy } from './source-control-discard-confirmation' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' function entry(partial: Partial & { path: string }): GitStatusEntry { return { diff --git a/src/renderer/src/components/right-sidebar/source-control-discard-confirmation.ts b/src/renderer/src/components/right-sidebar/source-control-discard-confirmation.ts index ff020c434ec..d65a9d86ab2 100644 --- a/src/renderer/src/components/right-sidebar/source-control-discard-confirmation.ts +++ b/src/renderer/src/components/right-sidebar/source-control-discard-confirmation.ts @@ -1,5 +1,5 @@ import { basename } from '@/lib/path' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import type { DiscardAllArea } from './discard-all-sequence' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/right-sidebar/source-control-discard-dialog.test.tsx b/src/renderer/src/components/right-sidebar/source-control-discard-dialog.test.tsx index 3dd4246a719..a445ee0da72 100644 --- a/src/renderer/src/components/right-sidebar/source-control-discard-dialog.test.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-discard-dialog.test.tsx @@ -1,7 +1,7 @@ import { renderToStaticMarkup } from 'react-dom/server' import type { ButtonHTMLAttributes, ReactNode } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' type CapturedButtonProps = ButtonHTMLAttributes & { children?: ReactNode diff --git a/src/renderer/src/components/right-sidebar/source-control-discard-dialog.tsx b/src/renderer/src/components/right-sidebar/source-control-discard-dialog.tsx index 6741a72ac55..52a63734c51 100644 --- a/src/renderer/src/components/right-sidebar/source-control-discard-dialog.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-discard-dialog.tsx @@ -10,7 +10,7 @@ import { DialogHeader, DialogTitle } from '@/components/ui/dialog' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import type { DiscardAllArea } from './discard-all-sequence' import { getDiscardAreaConfirmationCopy, diff --git a/src/renderer/src/components/right-sidebar/source-control-dropdown-items.ts b/src/renderer/src/components/right-sidebar/source-control-dropdown-items.ts index 8ee0f549aa0..8c8b3b0905b 100644 --- a/src/renderer/src/components/right-sidebar/source-control-dropdown-items.ts +++ b/src/renderer/src/components/right-sidebar/source-control-dropdown-items.ts @@ -3,7 +3,7 @@ import type { PrimaryActionInputs } from './source-control-primary-action' import { canSubmitCommit, resolveCommitDisabledReason } from './source-control-commit-eligibility' -import type { GitConflictOperation } from '../../../../shared/types' +import type { GitConflictOperation } from '../../../../shared/git-status-types' import { shouldForcePushWithLeaseForUpstream } from '../../../../shared/git-upstream-status' import { supportsHostedReviewCreation } from '../../../../shared/hosted-review-creation-providers' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/right-sidebar/source-control-entry-actions.test.ts b/src/renderer/src/components/right-sidebar/source-control-entry-actions.test.ts index f9f3bcba206..214e9387f46 100644 --- a/src/renderer/src/components/right-sidebar/source-control-entry-actions.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control-entry-actions.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { canDiscardStatusEntry, canStageStatusEntry, diff --git a/src/renderer/src/components/right-sidebar/source-control-entry-actions.ts b/src/renderer/src/components/right-sidebar/source-control-entry-actions.ts index b9bd2fe6a57..8495f231188 100644 --- a/src/renderer/src/components/right-sidebar/source-control-entry-actions.ts +++ b/src/renderer/src/components/right-sidebar/source-control-entry-actions.ts @@ -1,4 +1,4 @@ -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { isStageableStatusEntry } from './discard-all-sequence' /** diff --git a/src/renderer/src/components/right-sidebar/source-control-fix-split-button.tsx b/src/renderer/src/components/right-sidebar/source-control-fix-split-button.tsx index 26fed796035..9879af5b5f1 100644 --- a/src/renderer/src/components/right-sidebar/source-control-fix-split-button.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-fix-split-button.tsx @@ -12,7 +12,7 @@ import type { SourceControlActionRecipe, SourceControlLaunchActionId } from '../../../../shared/source-control-ai-actions' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { LaunchSource } from '../../../../shared/telemetry-events' import type { SourceControlAiWriteTarget } from '../../../../shared/source-control-ai-recipe-save' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/right-sidebar/source-control-header-overflow-menu.tsx b/src/renderer/src/components/right-sidebar/source-control-header-overflow-menu.tsx index 8959c50efa0..55db724a5af 100644 --- a/src/renderer/src/components/right-sidebar/source-control-header-overflow-menu.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-header-overflow-menu.tsx @@ -1,6 +1,6 @@ import React from 'react' import { List, ListTree, MessageSquare, MoreHorizontal, RefreshCw, Settings2 } from 'lucide-react' -import type { SourceControlViewMode } from '../../../../shared/types' +import type { SourceControlViewMode } from '../../../../shared/ui-chrome-types' import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { diff --git a/src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx b/src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx index fac0f0b6ded..19ccba393fd 100644 --- a/src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx @@ -2,7 +2,7 @@ import { renderToStaticMarkup } from 'react-dom/server' import type { ReactNode } from 'react' import { describe, expect, it, vi } from 'vitest' import { SourceControlHeaderToolbar } from './source-control-header-toolbar' -import type { GitBranchCompareSummary } from '../../../../shared/types' +import type { GitBranchCompareSummary } from '../../../../shared/git-diff-compare-types' import type { GitBranchLineTotal } from '../../../../shared/git-status-types' import type { WorktreeGitIdentityDisplay } from '@/lib/worktree-git-identity-display' import type { PrimaryAction } from './source-control-primary-action' diff --git a/src/renderer/src/components/right-sidebar/source-control-header-toolbar.test.ts b/src/renderer/src/components/right-sidebar/source-control-header-toolbar.test.ts index 667445e0244..11b79d1b6a0 100644 --- a/src/renderer/src/components/right-sidebar/source-control-header-toolbar.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control-header-toolbar.test.ts @@ -3,7 +3,7 @@ import { getNextSourceControlViewMode, shouldShowSourceControlCompareUnavailableCard } from './source-control-header-toolbar' -import type { GitBranchCompareSummary } from '../../../../shared/types' +import type { GitBranchCompareSummary } from '../../../../shared/git-diff-compare-types' const readySummary: GitBranchCompareSummary = { baseRef: 'origin/main', diff --git a/src/renderer/src/components/right-sidebar/source-control-header-toolbar.tsx b/src/renderer/src/components/right-sidebar/source-control-header-toolbar.tsx index e04ca32eaca..41a2ca38273 100644 --- a/src/renderer/src/components/right-sidebar/source-control-header-toolbar.tsx +++ b/src/renderer/src/components/right-sidebar/source-control-header-toolbar.tsx @@ -1,11 +1,8 @@ import React, { useCallback, useEffect, useRef } from 'react' import { GitPullRequestArrow, Loader2, Search, X } from 'lucide-react' -import type { - GitBranchCompareSummary, - GitUpstreamStatus, - SourceControlViewMode -} from '../../../../shared/types' -import type { GitBranchLineTotal } from '../../../../shared/git-status-types' +import type { GitBranchCompareSummary } from '../../../../shared/git-diff-compare-types' +import type { GitBranchLineTotal, GitUpstreamStatus } from '../../../../shared/git-status-types' +import type { SourceControlViewMode } from '../../../../shared/ui-chrome-types' import type { HostedReviewInfo } from '../../../../shared/hosted-review' import type { PrimaryAction } from './source-control-primary-action' import { Button } from '@/components/ui/button' diff --git a/src/renderer/src/components/right-sidebar/source-control-hosted-review-push-target.ts b/src/renderer/src/components/right-sidebar/source-control-hosted-review-push-target.ts index bd87f24c4c0..131e7b22733 100644 --- a/src/renderer/src/components/right-sidebar/source-control-hosted-review-push-target.ts +++ b/src/renderer/src/components/right-sidebar/source-control-hosted-review-push-target.ts @@ -1,4 +1,5 @@ -import type { GitPushTarget, GitUpstreamStatus } from '../../../../shared/types' +import type { GitUpstreamStatus } from '../../../../shared/git-status-types' +import type { GitPushTarget } from '../../../../shared/worktree/types' import type { HostedReviewState } from '../../../../shared/hosted-review' import { isPositiveHostedReviewNumber } from '../../../../shared/hosted-review' import { getPublishTargetDisplayName } from '../../../../shared/git-publish-target-status' diff --git a/src/renderer/src/components/right-sidebar/source-control-manual-review-url.ts b/src/renderer/src/components/right-sidebar/source-control-manual-review-url.ts index 4b5f44227f3..70e9cd6e877 100644 --- a/src/renderer/src/components/right-sidebar/source-control-manual-review-url.ts +++ b/src/renderer/src/components/right-sidebar/source-control-manual-review-url.ts @@ -1,5 +1,5 @@ import type { HostedReviewProvider } from '../../../../shared/hosted-review' -import type { GitPushTarget } from '../../../../shared/types' +import type { GitPushTarget } from '../../../../shared/worktree/types' import { branchFromRef, normalizeProvider, diff --git a/src/renderer/src/components/right-sidebar/source-control-primary-action-types.ts b/src/renderer/src/components/right-sidebar/source-control-primary-action-types.ts index da34c8a253d..d4043333442 100644 --- a/src/renderer/src/components/right-sidebar/source-control-primary-action-types.ts +++ b/src/renderer/src/components/right-sidebar/source-control-primary-action-types.ts @@ -1,5 +1,6 @@ import type { HostedReviewCreationEligibility } from '../../../../shared/hosted-review' -import type { GitUpstreamStatus, PRState } from '../../../../shared/types' +import type { GitUpstreamStatus } from '../../../../shared/git-status-types' +import type { PRState } from '../../../../shared/github/pull-request-types' import type { SourceControlPrimaryActionKind, SourceControlRemoteOpKind diff --git a/src/renderer/src/components/right-sidebar/source-control-section-order.test.ts b/src/renderer/src/components/right-sidebar/source-control-section-order.test.ts index fa563470d66..e63df719130 100644 --- a/src/renderer/src/components/right-sidebar/source-control-section-order.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control-section-order.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { buildSourceControlDisplaySections, getConflictReviewEntries, diff --git a/src/renderer/src/components/right-sidebar/source-control-section-order.ts b/src/renderer/src/components/right-sidebar/source-control-section-order.ts index 557db90b556..02e03a646ea 100644 --- a/src/renderer/src/components/right-sidebar/source-control-section-order.ts +++ b/src/renderer/src/components/right-sidebar/source-control-section-order.ts @@ -1,5 +1,6 @@ import { normalizeSourceControlGroupOrder } from '../../../../shared/source-control-group-order' -import type { GitStatusEntry, SourceControlGroupOrder } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' +import type { SourceControlGroupOrder } from '../../../../shared/ui-chrome-types' export const SOURCE_CONTROL_AREAS = ['unstaged', 'staged', 'untracked'] as const export type SourceControlSectionArea = (typeof SOURCE_CONTROL_AREAS)[number] diff --git a/src/renderer/src/components/right-sidebar/source-control-status-sort.test.ts b/src/renderer/src/components/right-sidebar/source-control-status-sort.test.ts index 595ba62fe34..a8c3b2acf59 100644 --- a/src/renderer/src/components/right-sidebar/source-control-status-sort.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control-status-sort.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { compareGitStatusEntries } from './source-control-status-sort' function entry(path: string, conflictStatus?: GitStatusEntry['conflictStatus']): GitStatusEntry { diff --git a/src/renderer/src/components/right-sidebar/source-control-status-sort.ts b/src/renderer/src/components/right-sidebar/source-control-status-sort.ts index ccedb093ffb..ff188a6cf76 100644 --- a/src/renderer/src/components/right-sidebar/source-control-status-sort.ts +++ b/src/renderer/src/components/right-sidebar/source-control-status-sort.ts @@ -1,4 +1,4 @@ -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { compareFileNames } from '../../../../shared/file-name-sort' export function compareGitStatusEntries(a: GitStatusEntry, b: GitStatusEntry): number { diff --git a/src/renderer/src/components/right-sidebar/source-control-submodule-expansion.test.ts b/src/renderer/src/components/right-sidebar/source-control-submodule-expansion.test.ts index c17d057f5c8..c93cd05203f 100644 --- a/src/renderer/src/components/right-sidebar/source-control-submodule-expansion.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control-submodule-expansion.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { buildSubmoduleChildNodes, collectListSelectionEntries, diff --git a/src/renderer/src/components/right-sidebar/source-control-submodule-expansion.ts b/src/renderer/src/components/right-sidebar/source-control-submodule-expansion.ts index aab15360590..c7796eeaf28 100644 --- a/src/renderer/src/components/right-sidebar/source-control-submodule-expansion.ts +++ b/src/renderer/src/components/right-sidebar/source-control-submodule-expansion.ts @@ -1,4 +1,4 @@ -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { basename } from '@/lib/path' import type { SourceControlSectionArea } from './source-control-section-order' import type { SourceControlTreeNode } from './source-control-tree' diff --git a/src/renderer/src/components/right-sidebar/source-control-text-generation-defaults.test.ts b/src/renderer/src/components/right-sidebar/source-control-text-generation-defaults.test.ts index b52e87482ab..acba48929f1 100644 --- a/src/renderer/src/components/right-sidebar/source-control-text-generation-defaults.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control-text-generation-defaults.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { getDefaultSettings } from '../../../../shared/constants' -import type { GlobalSettings, Repo } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' import { generationParamsToActionRecipe, sourceControlTextGenerationDefaultsMatchTarget diff --git a/src/renderer/src/components/right-sidebar/source-control-text-generation-defaults.ts b/src/renderer/src/components/right-sidebar/source-control-text-generation-defaults.ts index 08bc9fb6d9d..33f3e572d0c 100644 --- a/src/renderer/src/components/right-sidebar/source-control-text-generation-defaults.ts +++ b/src/renderer/src/components/right-sidebar/source-control-text-generation-defaults.ts @@ -8,7 +8,8 @@ import { type SourceControlTextActionId } from '../../../../shared/source-control-ai-actions' import type { SourceControlAiWriteTarget } from '../../../../shared/source-control-ai-recipe-save' -import type { GlobalSettings, Repo } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' import { sourceControlActionRecipeMatchesTarget } from './source-control-action-recipe-match' type TextGenerationRecipeConfiguration = { diff --git a/src/renderer/src/components/right-sidebar/source-control-tree.test.ts b/src/renderer/src/components/right-sidebar/source-control-tree.test.ts index 421b5f3cd06..6d9251801d2 100644 --- a/src/renderer/src/components/right-sidebar/source-control-tree.test.ts +++ b/src/renderer/src/components/right-sidebar/source-control-tree.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { GitBranchChangeEntry, GitStatusEntry } from '../../../../shared/types' +import type { GitBranchChangeEntry } from '../../../../shared/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { buildGitStatusSourceControlTree, buildSourceControlTree, diff --git a/src/renderer/src/components/right-sidebar/source-control-tree.ts b/src/renderer/src/components/right-sidebar/source-control-tree.ts index 2ecca32c0bd..eb4a01a7ea3 100644 --- a/src/renderer/src/components/right-sidebar/source-control-tree.ts +++ b/src/renderer/src/components/right-sidebar/source-control-tree.ts @@ -1,5 +1,5 @@ import { normalizeRelativePath } from '@/lib/path' -import type { GitStatusEntry, GitStagingArea } from '../../../../shared/types' +import type { GitStagingArea, GitStatusEntry } from '../../../../shared/git-status-types' import { compareFileNames } from '../../../../shared/file-name-sort' import { splitPathSegments } from './path-tree' import { compareGitStatusEntries } from './source-control-status-sort' diff --git a/src/renderer/src/components/right-sidebar/stale-conflict-operation-poll.ts b/src/renderer/src/components/right-sidebar/stale-conflict-operation-poll.ts index eaf61d70be7..413f1c51c0a 100644 --- a/src/renderer/src/components/right-sidebar/stale-conflict-operation-poll.ts +++ b/src/renderer/src/components/right-sidebar/stale-conflict-operation-poll.ts @@ -1,5 +1,6 @@ import { useEffect, useMemo } from 'react' -import type { GitConflictOperation, Repo } from '../../../../shared/types' +import type { GitConflictOperation } from '../../../../shared/git-status-types' +import type { Repo } from '../../../../shared/repo-types' import { isGitRepoKind } from '../../../../shared/repo-kind' import { getConnectionId } from '@/lib/connection-context' import { getRuntimeGitConflictOperation } from '@/runtime/runtime-git-client' diff --git a/src/renderer/src/components/right-sidebar/status-display.ts b/src/renderer/src/components/right-sidebar/status-display.ts index 92fa3e7769b..17ce74ab4f5 100644 --- a/src/renderer/src/components/right-sidebar/status-display.ts +++ b/src/renderer/src/components/right-sidebar/status-display.ts @@ -1,4 +1,4 @@ -import type { GitFileStatus, GitStatusEntry } from '../../../../shared/types' +import type { GitFileStatus, GitStatusEntry } from '../../../../shared/git-status-types' import { joinPath, normalizeRelativePath } from '@/lib/path' import { splitPathSegments } from './path-tree' diff --git a/src/renderer/src/components/right-sidebar/use-checks-panel-terminal-worktree.test.ts b/src/renderer/src/components/right-sidebar/use-checks-panel-terminal-worktree.test.ts index 7d194d2bf00..d79f9a16fbe 100644 --- a/src/renderer/src/components/right-sidebar/use-checks-panel-terminal-worktree.test.ts +++ b/src/renderer/src/components/right-sidebar/use-checks-panel-terminal-worktree.test.ts @@ -3,7 +3,8 @@ import { act, createElement } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { useAppStore } from '@/store' import type { AppState } from '@/store/types' import { makeWorktree, TEST_REPO } from '@/store/slices/store-test-helpers' diff --git a/src/renderer/src/components/right-sidebar/use-checks-panel-terminal-worktree.ts b/src/renderer/src/components/right-sidebar/use-checks-panel-terminal-worktree.ts index eb902500547..a0754151973 100644 --- a/src/renderer/src/components/right-sidebar/use-checks-panel-terminal-worktree.ts +++ b/src/renderer/src/components/right-sidebar/use-checks-panel-terminal-worktree.ts @@ -6,7 +6,7 @@ import { useAllWorktrees, useRepoMap } from '@/store/selectors' import { isRemoteRuntimePtyId } from '@/runtime/runtime-terminal-inspection' import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id' import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { resolveChecksPanelTerminalPtyId, resolveChecksPanelWorktreeFromTerminalCwd diff --git a/src/renderer/src/components/right-sidebar/use-git-status-upstream-ref-watch.ts b/src/renderer/src/components/right-sidebar/use-git-status-upstream-ref-watch.ts index c28ae16f9b0..8508d61f5e7 100644 --- a/src/renderer/src/components/right-sidebar/use-git-status-upstream-ref-watch.ts +++ b/src/renderer/src/components/right-sidebar/use-git-status-upstream-ref-watch.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect } from 'react' import { getConnectionId } from '@/lib/connection-context' import { setRuntimeGitStatusUpstreamRefWatch } from '@/runtime/runtime-git-client' -import type { GitStatusResult } from '../../../../shared/types' +import type { GitStatusResult } from '../../../../shared/git-status-types' import { getRightSidebarWorktreeRuntimeSettings } from './file-explorer-runtime-owner' export function useGitStatusUpstreamRefWatch(args: { diff --git a/src/renderer/src/components/right-sidebar/use-hosted-review-actions.test.tsx b/src/renderer/src/components/right-sidebar/use-hosted-review-actions.test.tsx index 09fd8fe4133..daf0dffe1b7 100644 --- a/src/renderer/src/components/right-sidebar/use-hosted-review-actions.test.tsx +++ b/src/renderer/src/components/right-sidebar/use-hosted-review-actions.test.tsx @@ -3,7 +3,8 @@ import { act, createElement } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { PRInfo, Repo } from '../../../../shared/types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' +import type { Repo } from '../../../../shared/repo-types' import { useHostedReviewActions, type HostedReviewActionInfo } from './use-hosted-review-actions' const confirmationMocks = vi.hoisted(() => ({ diff --git a/src/renderer/src/components/right-sidebar/use-hosted-review-actions.ts b/src/renderer/src/components/right-sidebar/use-hosted-review-actions.ts index 6830fe86eee..44b8eb7940f 100644 --- a/src/renderer/src/components/right-sidebar/use-hosted-review-actions.ts +++ b/src/renderer/src/components/right-sidebar/use-hosted-review-actions.ts @@ -3,7 +3,8 @@ import { toast } from 'sonner' import { useConfirmationDialog } from '@/components/confirmation-dialog-context' import type { GitHubPRAutoMergeAction } from '@/components/github-pr-merge-state' import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { PRInfo, Repo, GitHubPRMergeMethod } from '../../../../shared/types' +import type { GitHubPRMergeMethod, PRInfo } from '../../../../shared/github/pull-request-types' +import type { Repo } from '../../../../shared/repo-types' import { mergeGitHubHostedReview, setGitHubHostedReviewAutoMerge, diff --git a/src/renderer/src/components/right-sidebar/use-source-control-recovery-ai.ts b/src/renderer/src/components/right-sidebar/use-source-control-recovery-ai.ts index 3d679a55789..27ec43a3323 100644 --- a/src/renderer/src/components/right-sidebar/use-source-control-recovery-ai.ts +++ b/src/renderer/src/components/right-sidebar/use-source-control-recovery-ai.ts @@ -3,7 +3,7 @@ import type { SourceControlActionRecipe, SourceControlLaunchActionId } from '../../../../shared/source-control-ai-actions' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { buildFixCommitFailurePrompt } from './source-control-ai-prompts' import { summarizeCommitFailure } from './commit-failure-summary' import { diff --git a/src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.ts b/src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.ts index c9720c751bf..85347b2e6bd 100644 --- a/src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.ts +++ b/src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.ts @@ -13,7 +13,7 @@ import { getRuntimeRepoBaseRefDefault, searchRuntimeRepoBaseRefDetails } from '@/runtime/runtime-repo-client' -import type { Repo, BaseRefSearchResult } from '../../../../shared/types' +import type { BaseRefSearchResult, Repo } from '../../../../shared/repo-types' import type { HostedReviewCreationEligibility } from '../../../../shared/hosted-review' import { normalizeHostedReviewBaseRef } from '../../../../shared/hosted-review-refs' import { diff --git a/src/renderer/src/components/right-sidebar/useFileExplorerTree.stale-dirs.test.tsx b/src/renderer/src/components/right-sidebar/useFileExplorerTree.stale-dirs.test.tsx index 9c665f4cce6..49ca1844cba 100644 --- a/src/renderer/src/components/right-sidebar/useFileExplorerTree.stale-dirs.test.tsx +++ b/src/renderer/src/components/right-sidebar/useFileExplorerTree.stale-dirs.test.tsx @@ -2,7 +2,7 @@ import { act, cleanup, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { DirEntry } from '../../../../shared/types' +import type { DirEntry } from '../../../../shared/filesystem-entry-types' import { useFileExplorerTree } from './useFileExplorerTree' const readDirectoryMock = vi.hoisted(() => vi.fn()) diff --git a/src/renderer/src/components/right-sidebar/useFileExplorerWatch.pending-refresh.test.tsx b/src/renderer/src/components/right-sidebar/useFileExplorerWatch.pending-refresh.test.tsx index 13b7b1c1c5a..4bf3de4b551 100644 --- a/src/renderer/src/components/right-sidebar/useFileExplorerWatch.pending-refresh.test.tsx +++ b/src/renderer/src/components/right-sidebar/useFileExplorerWatch.pending-refresh.test.tsx @@ -2,7 +2,7 @@ import { act, cleanup, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { FsChangedPayload } from '../../../../shared/types' +import type { FsChangedPayload } from '../../../../shared/filesystem-entry-types' import type { FileExplorerOperationOwner, FileExplorerTreeRefreshOutcome diff --git a/src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts b/src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts index d8b218b6409..33e439e4a11 100644 --- a/src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts +++ b/src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, type Dispatch, type SetStateAction } from 'react' -import type { FsChangedPayload } from '../../../../shared/types' +import type { FsChangedPayload } from '../../../../shared/filesystem-entry-types' import type { DirCache, FileExplorerOperationOwner, diff --git a/src/renderer/src/components/right-sidebar/useFileSearchPanel.ts b/src/renderer/src/components/right-sidebar/useFileSearchPanel.ts index f0935b625a0..e66e647c26f 100644 --- a/src/renderer/src/components/right-sidebar/useFileSearchPanel.ts +++ b/src/renderer/src/components/right-sidebar/useFileSearchPanel.ts @@ -2,7 +2,11 @@ import type React from 'react' import { useCallback, useDeferredValue, useEffect, useMemo, useRef } from 'react' import { useAppStore } from '@/store' import { useActiveWorktree } from '@/store/selectors' -import type { SearchFileResult, SearchMatch, SearchResult } from '../../../../shared/types' +import type { + SearchFileResult, + SearchMatch, + SearchResult +} from '../../../../shared/code-search-types' import { buildSearchRows } from './search-rows' import { cancelRevealFrame, openMatchResult } from './search-match-open' import type { SearchQueryRowProps } from './SearchQueryRow' diff --git a/src/renderer/src/components/right-sidebar/useFileSearchRunner.test.tsx b/src/renderer/src/components/right-sidebar/useFileSearchRunner.test.tsx index 62bbf4f9bec..c68f5effa4e 100644 --- a/src/renderer/src/components/right-sidebar/useFileSearchRunner.test.tsx +++ b/src/renderer/src/components/right-sidebar/useFileSearchRunner.test.tsx @@ -2,7 +2,7 @@ import { act, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { SearchResult } from '../../../../shared/types' +import type { SearchResult } from '../../../../shared/code-search-types' import { useFileSearchRunner } from './useFileSearchRunner' const mocks = vi.hoisted(() => ({ diff --git a/src/renderer/src/components/right-sidebar/useFileSearchRunner.ts b/src/renderer/src/components/right-sidebar/useFileSearchRunner.ts index 20feac4a610..7950a86114f 100644 --- a/src/renderer/src/components/right-sidebar/useFileSearchRunner.ts +++ b/src/renderer/src/components/right-sidebar/useFileSearchRunner.ts @@ -10,7 +10,7 @@ import { } from '@/runtime/runtime-file-search-bounds' import { searchRuntimeFiles } from '@/runtime/runtime-file-client' import { useAppStore } from '@/store' -import type { SearchResult } from '../../../../shared/types' +import type { SearchResult } from '../../../../shared/code-search-types' import { getRightSidebarWorktreeRuntimeSettings } from './file-explorer-runtime-owner' const SEARCH_DEBOUNCE_MS = 300 diff --git a/src/renderer/src/components/right-sidebar/useGitHistoryCommitActions.ts b/src/renderer/src/components/right-sidebar/useGitHistoryCommitActions.ts index c60c6f1f3e0..532912c65eb 100644 --- a/src/renderer/src/components/right-sidebar/useGitHistoryCommitActions.ts +++ b/src/renderer/src/components/right-sidebar/useGitHistoryCommitActions.ts @@ -12,7 +12,10 @@ import { launchAgentInNewTab } from '@/lib/launch-agent-in-new-tab' import { resolveDefaultAgentForNewTab } from '@/lib/agent-tab-shortcuts' import { translate } from '@/i18n/i18n' import type { GitHistoryItem } from '../../../../shared/git-history' -import type { GitBranchChangeEntry, GitCommitCompareResult } from '../../../../shared/types' +import type { + GitBranchChangeEntry, + GitCommitCompareResult +} from '../../../../shared/git-diff-compare-types' import { shouldOpenSourceControlRowAsPreview, type SourceControlRowOpenEvent diff --git a/src/renderer/src/components/right-sidebar/useGitStatusPolling.rerender.test.ts b/src/renderer/src/components/right-sidebar/useGitStatusPolling.rerender.test.ts index 348bf8a1f36..7d565090d69 100644 --- a/src/renderer/src/components/right-sidebar/useGitStatusPolling.rerender.test.ts +++ b/src/renderer/src/components/right-sidebar/useGitStatusPolling.rerender.test.ts @@ -3,7 +3,8 @@ import { act, createElement } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { useAppStore } from '@/store' import type { AppState } from '@/store/types' import { makeOpenFile, makeWorktree, TEST_REPO } from '@/store/slices/store-test-helpers' diff --git a/src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts b/src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts index 47d8d5e8fde..b1c6d3586d1 100644 --- a/src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts +++ b/src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts @@ -1,6 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as React from 'react' -import type { FsChangedPayload, GitPushTarget, GitStatusResult } from '../../../../shared/types' +import type { FsChangedPayload } from '../../../../shared/filesystem-entry-types' +import type { GitStatusResult } from '../../../../shared/git-status-types' +import type { GitPushTarget } from '../../../../shared/worktree/types' import { DEFAULT_GIT_STATUS_LIMIT } from '../../../../shared/git-status-limit' const worktree = { id: 'repo-1::/repo', repoId: 'repo-1', path: '/repo' } diff --git a/src/renderer/src/components/right-sidebar/useSavedSourceControlAgentActionAutoStart.ts b/src/renderer/src/components/right-sidebar/useSavedSourceControlAgentActionAutoStart.ts index c3dd7a10a9c..0d582ca508c 100644 --- a/src/renderer/src/components/right-sidebar/useSavedSourceControlAgentActionAutoStart.ts +++ b/src/renderer/src/components/right-sidebar/useSavedSourceControlAgentActionAutoStart.ts @@ -3,7 +3,9 @@ import type { SourceControlActionRecipe, SourceControlLaunchActionId } from '../../../../shared/source-control-ai-actions' -import type { GlobalSettings, Repo, TuiAgent } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { isSourceControlAgentDetectedAndEnabled } from './source-control-agent-action-dialog-support' import { sourceControlActionRecipeMatchesTarget } from './source-control-action-recipe-match' diff --git a/src/renderer/src/components/right-sidebar/useSourceControlAgentActionDialog.ts b/src/renderer/src/components/right-sidebar/useSourceControlAgentActionDialog.ts index 4c129d862e4..d002d9d3a34 100644 --- a/src/renderer/src/components/right-sidebar/useSourceControlAgentActionDialog.ts +++ b/src/renderer/src/components/right-sidebar/useSourceControlAgentActionDialog.ts @@ -8,7 +8,7 @@ import { useAppStore } from '@/store' import { useRepoById } from '@/store/selectors' import { renderSourceControlActionCommandTemplate } from '../../../../shared/source-control-ai-actions' import { isTuiAgentEnabled } from '../../../../shared/tui-agent-selection' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { SourceControlAgentActionDialogProps } from './SourceControlAgentActionDialog' import type { UseSourceControlAgentActionDialogResult } from './source-control-agent-action-dialog-result' import { useSavedSourceControlAgentActionAutoStart } from './useSavedSourceControlAgentActionAutoStart' diff --git a/src/renderer/src/components/right-sidebar/useSourceControlAgentActionStart.ts b/src/renderer/src/components/right-sidebar/useSourceControlAgentActionStart.ts index 3336348a2e5..7af2c97f8c6 100644 --- a/src/renderer/src/components/right-sidebar/useSourceControlAgentActionStart.ts +++ b/src/renderer/src/components/right-sidebar/useSourceControlAgentActionStart.ts @@ -5,7 +5,9 @@ import type { SourceControlLaunchActionId } from '../../../../shared/source-control-ai-actions' import type { SourceControlAiWriteTarget } from '../../../../shared/source-control-ai-recipe-save' -import type { GlobalSettings, Repo, TuiAgent } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { buildSourceControlAgentDeliveryPlan } from './buildSourceControlAgentDeliveryPlan' import type { SourceControlAgentActionDeliveryPlanState } from './SourceControlAgentActionDialogForm' import { runSourceControlAgentActionStart } from './runSourceControlAgentActionStart' diff --git a/src/renderer/src/components/right-sidebar/useSourceControlSelection.test.ts b/src/renderer/src/components/right-sidebar/useSourceControlSelection.test.ts index 666e406e1cb..ccafa2ddc99 100644 --- a/src/renderer/src/components/right-sidebar/useSourceControlSelection.test.ts +++ b/src/renderer/src/components/right-sidebar/useSourceControlSelection.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { getSelectionRangeKeys, reconcileSelectionKeys, diff --git a/src/renderer/src/components/right-sidebar/useSourceControlSelection.ts b/src/renderer/src/components/right-sidebar/useSourceControlSelection.ts index e14ee5cdc5d..0fa525724a5 100644 --- a/src/renderer/src/components/right-sidebar/useSourceControlSelection.ts +++ b/src/renderer/src/components/right-sidebar/useSourceControlSelection.ts @@ -1,5 +1,5 @@ import { useState, useCallback, useEffect, useRef, type RefObject } from 'react' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import type { SourceControlRowOpenEvent } from './source-control-split-open' function isMacPlatform(): boolean { diff --git a/src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.test.tsx b/src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.test.tsx index 5b97bb8068f..527d641b843 100644 --- a/src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.test.tsx +++ b/src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.test.tsx @@ -15,7 +15,7 @@ import { useSourceControlSubmoduleStatus, type UseSourceControlSubmoduleStatusResult } from './useSourceControlSubmoduleStatus' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' const roots: Root[] = [] diff --git a/src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.ts b/src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.ts index fc91420b7eb..c7c799561f6 100644 --- a/src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.ts +++ b/src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react' -import type { GitStatusEntry } from '../../../../shared/types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' import { getConnectionId } from '@/lib/connection-context' import { getRuntimeGitSubmoduleStatus, type RuntimeGitContext } from '@/runtime/runtime-git-client' import { diff --git a/src/renderer/src/components/settings/AccountsPane.test.tsx b/src/renderer/src/components/settings/AccountsPane.test.tsx index 795caf9b5cf..bf7656856d5 100644 --- a/src/renderer/src/components/settings/AccountsPane.test.tsx +++ b/src/renderer/src/components/settings/AccountsPane.test.tsx @@ -2,7 +2,7 @@ import React from 'react' import { renderToStaticMarkup } from 'react-dom/server' import { beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../../shared/constants' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { i18n } from '../../i18n/i18n' import { useAppStore } from '../../store' import { AccountsPane } from './AccountsPane' diff --git a/src/renderer/src/components/settings/AccountsPane.tsx b/src/renderer/src/components/settings/AccountsPane.tsx index c8500b99cfa..13b93c24596 100644 --- a/src/renderer/src/components/settings/AccountsPane.tsx +++ b/src/renderer/src/components/settings/AccountsPane.tsx @@ -4,12 +4,12 @@ error handling and restart prompts below; splitting them into separate files would scatter those flows without a meaningful abstraction boundary. */ import { useEffect, useRef, useState } from 'react' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { ClaudeRateLimitAccountsState, CodexRateLimitAccountsState, - CodexSystemDefaultIdentity, - GlobalSettings -} from '../../../../shared/types' + CodexSystemDefaultIdentity +} from '../../../../shared/managed-account-types' import { resolveLocalAccountRuntimeTarget } from '../../../../shared/local-account-runtime' import { getRendererAppPlatform } from '../../lib/renderer-app-platform' import { Badge } from '../ui/badge' diff --git a/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.test.ts b/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.test.ts index 2de229d49d0..08a392fdecc 100644 --- a/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.test.ts +++ b/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { createHttpProxyBypassRulesDraftState, createHttpProxyUrlDraftState, diff --git a/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.tsx b/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.tsx index e5b8a3b81fe..6597dd1e15b 100644 --- a/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.tsx +++ b/src/renderer/src/components/settings/AdvancedNetworkSettingsSection.tsx @@ -1,7 +1,7 @@ import type React from 'react' import { useState } from 'react' import { ChevronDown } from 'lucide-react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { normalizeProxyBypassRules, normalizeProxyUrl } from '../../../../shared/network-proxy' import { cn } from '@/lib/utils' import { useAppStore } from '../../store' diff --git a/src/renderer/src/components/settings/AdvancedPane.tsx b/src/renderer/src/components/settings/AdvancedPane.tsx index f2b014e2805..df05dc71507 100644 --- a/src/renderer/src/components/settings/AdvancedPane.tsx +++ b/src/renderer/src/components/settings/AdvancedPane.tsx @@ -1,6 +1,6 @@ import { useRef, useState } from 'react' import { Info, Loader2, RotateCw } from 'lucide-react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { useMountedRef } from '@/hooks/useMountedRef' import { Button } from '../ui/button' import { Label } from '../ui/label' diff --git a/src/renderer/src/components/settings/AgentAwakeSetting.tsx b/src/renderer/src/components/settings/AgentAwakeSetting.tsx index 216dff6facf..4d27b9ab9c6 100644 --- a/src/renderer/src/components/settings/AgentAwakeSetting.tsx +++ b/src/renderer/src/components/settings/AgentAwakeSetting.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { Label } from '../ui/label' import { getAgentAwakeDescription, diff --git a/src/renderer/src/components/settings/AgentCacheTimerSection.tsx b/src/renderer/src/components/settings/AgentCacheTimerSection.tsx index 554e9541090..2e99b4f7e6a 100644 --- a/src/renderer/src/components/settings/AgentCacheTimerSection.tsx +++ b/src/renderer/src/components/settings/AgentCacheTimerSection.tsx @@ -1,6 +1,6 @@ import type React from 'react' import { Timer } from 'lucide-react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { useAppStore } from '../../store' import { Label } from '../ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' diff --git a/src/renderer/src/components/settings/AgentDashboardExperimentalSetting.tsx b/src/renderer/src/components/settings/AgentDashboardExperimentalSetting.tsx index 9c5f495b883..c22f5a9050a 100644 --- a/src/renderer/src/components/settings/AgentDashboardExperimentalSetting.tsx +++ b/src/renderer/src/components/settings/AgentDashboardExperimentalSetting.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' import { Label } from '../ui/label' import { SearchableSetting } from './SearchableSetting' diff --git a/src/renderer/src/components/settings/AgentRuntimeSetting.tsx b/src/renderer/src/components/settings/AgentRuntimeSetting.tsx index 220a380b943..877ce8d0065 100644 --- a/src/renderer/src/components/settings/AgentRuntimeSetting.tsx +++ b/src/renderer/src/components/settings/AgentRuntimeSetting.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { GlobalWindowsRuntimeDefault } from '../../../../shared/project-execution-runtime' import { normalizeGlobalWindowsRuntimeDefault } from '../../../../shared/project-execution-runtime' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' diff --git a/src/renderer/src/components/settings/AgentsPane.test.tsx b/src/renderer/src/components/settings/AgentsPane.test.tsx index 3b2f7bb964b..1081f5d4553 100644 --- a/src/renderer/src/components/settings/AgentsPane.test.tsx +++ b/src/renderer/src/components/settings/AgentsPane.test.tsx @@ -2,7 +2,8 @@ import React from 'react' import { renderToStaticMarkup } from 'react-dom/server' import { beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../../shared/constants' -import type { GlobalSettings, TuiAgent } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { AGENT_CATALOG } from '@/lib/agent-catalog' import { useAppStore } from '../../store' import { getAgentGeneratedTabTitlesTitle } from './agent-generated-tab-title-copy' diff --git a/src/renderer/src/components/settings/AgentsPane.tsx b/src/renderer/src/components/settings/AgentsPane.tsx index 70ba5f64c3f..682468c5d06 100644 --- a/src/renderer/src/components/settings/AgentsPane.tsx +++ b/src/renderer/src/components/settings/AgentsPane.tsx @@ -11,7 +11,8 @@ import { RefreshCw, Terminal } from 'lucide-react' -import type { GlobalSettings, TuiAgent } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { getAgentCatalog, AgentIcon } from '@/lib/agent-catalog' import { useDetectedAgents, type AgentDetectionTarget } from '@/hooks/useDetectedAgents' import { useAppStore } from '@/store' diff --git a/src/renderer/src/components/settings/AppearanceInterfaceSection.tsx b/src/renderer/src/components/settings/AppearanceInterfaceSection.tsx index 551485cb317..b78e9de335d 100644 --- a/src/renderer/src/components/settings/AppearanceInterfaceSection.tsx +++ b/src/renderer/src/components/settings/AppearanceInterfaceSection.tsx @@ -1,6 +1,6 @@ import type React from 'react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' import { UIZoomControl } from './UIZoomControl' import { SearchableSetting } from './SearchableSetting' diff --git a/src/renderer/src/components/settings/AppearancePane.test.tsx b/src/renderer/src/components/settings/AppearancePane.test.tsx index 139ade8064f..4a89d66cd03 100644 --- a/src/renderer/src/components/settings/AppearancePane.test.tsx +++ b/src/renderer/src/components/settings/AppearancePane.test.tsx @@ -6,7 +6,8 @@ import { I18nextProvider } from 'react-i18next' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { i18n } from '@/i18n/i18n' import { getDefaultSettings } from '../../../../shared/constants' -import type { GlobalSettings, StatusBarItem } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { StatusBarItem } from '../../../../shared/ui-chrome-types' const mocks = vi.hoisted(() => ({ state: { diff --git a/src/renderer/src/components/settings/AppearancePane.tsx b/src/renderer/src/components/settings/AppearancePane.tsx index f6c71840654..3c460b32694 100644 --- a/src/renderer/src/components/settings/AppearancePane.tsx +++ b/src/renderer/src/components/settings/AppearancePane.tsx @@ -2,7 +2,7 @@ import type React from 'react' import { useLayoutEffect, useState } from 'react' import { AppWindow, PanelLeft, TerminalSquare } from 'lucide-react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { AppearanceSection } from './AppearanceSection' import { AppearanceInterfaceSection } from './AppearanceInterfaceSection' diff --git a/src/renderer/src/components/settings/AppearanceWindowSidebarSection.tsx b/src/renderer/src/components/settings/AppearanceWindowSidebarSection.tsx index 1d7e5d8ec2c..c8d63b3866a 100644 --- a/src/renderer/src/components/settings/AppearanceWindowSidebarSection.tsx +++ b/src/renderer/src/components/settings/AppearanceWindowSidebarSection.tsx @@ -1,6 +1,7 @@ import type React from 'react' -import type { GlobalSettings, StatusBarItem } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { StatusBarItem } from '../../../../shared/ui-chrome-types' import type { FeatureInteractionId } from '../../../../shared/feature-interaction-catalog' import { SearchableSetting } from './SearchableSetting' import { AppearanceAdvancedDisclosure } from './AppearanceAdvancedDisclosure' diff --git a/src/renderer/src/components/settings/ArtifactsSettingsPane.tsx b/src/renderer/src/components/settings/ArtifactsSettingsPane.tsx index f561908d50a..60dd3e0c0c9 100644 --- a/src/renderer/src/components/settings/ArtifactsSettingsPane.tsx +++ b/src/renderer/src/components/settings/ArtifactsSettingsPane.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react' import { ArrowRight, Files } from 'lucide-react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { Button } from '@/components/ui/button' import { SettingsSwitchRow } from './SettingsFormControls' import { useAppStore } from '@/store' diff --git a/src/renderer/src/components/settings/AutoRenameBranchFromWorkSetting.tsx b/src/renderer/src/components/settings/AutoRenameBranchFromWorkSetting.tsx index d31d9b69533..d44bc20c887 100644 --- a/src/renderer/src/components/settings/AutoRenameBranchFromWorkSetting.tsx +++ b/src/renderer/src/components/settings/AutoRenameBranchFromWorkSetting.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { ChevronDown } from 'lucide-react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { SourceControlAiSettingsPatch, SourceControlAiSettings diff --git a/src/renderer/src/components/settings/AutomationsSettingsPane.tsx b/src/renderer/src/components/settings/AutomationsSettingsPane.tsx index 5984698b65b..991081e4739 100644 --- a/src/renderer/src/components/settings/AutomationsSettingsPane.tsx +++ b/src/renderer/src/components/settings/AutomationsSettingsPane.tsx @@ -1,5 +1,5 @@ import { ArrowRight, CalendarClock } from 'lucide-react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { Button } from '@/components/ui/button' import { SettingsSwitchRow } from './SettingsFormControls' import { useAppStore } from '@/store' diff --git a/src/renderer/src/components/settings/BrowserLinkRoutingModifierSetting.tsx b/src/renderer/src/components/settings/BrowserLinkRoutingModifierSetting.tsx index b0c55e87204..55edebeea9a 100644 --- a/src/renderer/src/components/settings/BrowserLinkRoutingModifierSetting.tsx +++ b/src/renderer/src/components/settings/BrowserLinkRoutingModifierSetting.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { SearchableSetting } from './SearchableSetting' import { SettingsSwitchRow } from './SettingsFormControls' import { diff --git a/src/renderer/src/components/settings/BrowserLinkRoutingSetting.tsx b/src/renderer/src/components/settings/BrowserLinkRoutingSetting.tsx index fa7f26f135b..7ec1bc47ff3 100644 --- a/src/renderer/src/components/settings/BrowserLinkRoutingSetting.tsx +++ b/src/renderer/src/components/settings/BrowserLinkRoutingSetting.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { Label } from '../ui/label' import { Switch } from '../ui/switch' import { SearchableSetting } from './SearchableSetting' diff --git a/src/renderer/src/components/settings/BrowserLocalhostWorktreeLabelsSetting.tsx b/src/renderer/src/components/settings/BrowserLocalhostWorktreeLabelsSetting.tsx index 1dfc0492318..fb18663e497 100644 --- a/src/renderer/src/components/settings/BrowserLocalhostWorktreeLabelsSetting.tsx +++ b/src/renderer/src/components/settings/BrowserLocalhostWorktreeLabelsSetting.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' import { SearchableSetting } from './SearchableSetting' import { SettingsSwitchRow } from './SettingsFormControls' diff --git a/src/renderer/src/components/settings/BrowserPane.tsx b/src/renderer/src/components/settings/BrowserPane.tsx index 1bc078cfcca..c0b2a370e67 100644 --- a/src/renderer/src/components/settings/BrowserPane.tsx +++ b/src/renderer/src/components/settings/BrowserPane.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState, type MutableRefObject } from 'react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { useAppStore } from '../../store' import { matchesSettingsSearch } from './settings-search' import { getBrowserPaneSearchEntries } from './browser-search' diff --git a/src/renderer/src/components/settings/BrowserProfileRow.tsx b/src/renderer/src/components/settings/BrowserProfileRow.tsx index a6324661901..05447bb7758 100644 --- a/src/renderer/src/components/settings/BrowserProfileRow.tsx +++ b/src/renderer/src/components/settings/BrowserProfileRow.tsx @@ -1,7 +1,10 @@ import { Import, Loader2, Trash2 } from 'lucide-react' import { toast } from 'sonner' import { emitBrowserCookieImportToast } from '@/lib/browser-cookie-import-toast' -import type { BrowserCookieImportSummary, BrowserSessionProfile } from '../../../../shared/types' +import type { + BrowserCookieImportSummary, + BrowserSessionProfile +} from '../../../../shared/browser-workspace-types' import { Button } from '../ui/button' import { DropdownMenu, diff --git a/src/renderer/src/components/settings/BrowserSessionCookiesSection.tsx b/src/renderer/src/components/settings/BrowserSessionCookiesSection.tsx index 8fa1e6c9a60..3f4fbbb992d 100644 --- a/src/renderer/src/components/settings/BrowserSessionCookiesSection.tsx +++ b/src/renderer/src/components/settings/BrowserSessionCookiesSection.tsx @@ -1,5 +1,5 @@ import { Plus } from 'lucide-react' -import type { BrowserSessionProfile } from '../../../../shared/types' +import type { BrowserSessionProfile } from '../../../../shared/browser-workspace-types' import { Button } from '../ui/button' import { Label } from '../ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' diff --git a/src/renderer/src/components/settings/BrowserTerminalLinkActionsSetting.tsx b/src/renderer/src/components/settings/BrowserTerminalLinkActionsSetting.tsx index 2ba4fa50a00..b658052db81 100644 --- a/src/renderer/src/components/settings/BrowserTerminalLinkActionsSetting.tsx +++ b/src/renderer/src/components/settings/BrowserTerminalLinkActionsSetting.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' import { BROWSER_TERMINAL_LINK_ACTIONS_SETTINGS_TARGET_ID } from '@/lib/settings-navigation-types' import { SearchableSetting } from './SearchableSetting' diff --git a/src/renderer/src/components/settings/CliSection.tsx b/src/renderer/src/components/settings/CliSection.tsx index e37a45ef3d6..32cbb3e5766 100644 --- a/src/renderer/src/components/settings/CliSection.tsx +++ b/src/renderer/src/components/settings/CliSection.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { FolderOpen, RefreshCw } from 'lucide-react' import { toast } from 'sonner' import type { CliInstallStatus } from '../../../../shared/cli-install-types' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { ORCA_CLI_SKILL_INSTALL_COMMAND, ORCA_CLI_SKILL_NAME, diff --git a/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx b/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx index 2b68c2e0945..913b6356322 100644 --- a/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx +++ b/src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { deriveGlobalWindowsRuntimeDefaultFromLegacySettings, normalizeGlobalWindowsRuntimeDefault diff --git a/src/renderer/src/components/settings/CommitMessageAiPane.test.tsx b/src/renderer/src/components/settings/CommitMessageAiPane.test.tsx index fba530089bf..95c3e2567b8 100644 --- a/src/renderer/src/components/settings/CommitMessageAiPane.test.tsx +++ b/src/renderer/src/components/settings/CommitMessageAiPane.test.tsx @@ -1,7 +1,7 @@ import React from 'react' import { renderToStaticMarkup } from 'react-dom/server' import { beforeEach, describe, expect, it } from 'vitest' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { SourceControlAiSettings } from '../../../../shared/source-control-ai-types' import { getCommitMessageModelDiscoveryHostKey, diff --git a/src/renderer/src/components/settings/CommitMessageAiPane.tsx b/src/renderer/src/components/settings/CommitMessageAiPane.tsx index ecc01cc4c11..d14ff189c72 100644 --- a/src/renderer/src/components/settings/CommitMessageAiPane.tsx +++ b/src/renderer/src/components/settings/CommitMessageAiPane.tsx @@ -1,6 +1,7 @@ import { useRef } from 'react' import type React from 'react' -import type { GlobalSettings, TuiAgent } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { SourceControlAiSettingsPatch, SourceControlAiSettings diff --git a/src/renderer/src/components/settings/CompareAgainstUpstreamSetting.tsx b/src/renderer/src/components/settings/CompareAgainstUpstreamSetting.tsx index 0fb266867b0..532e25be0d2 100644 --- a/src/renderer/src/components/settings/CompareAgainstUpstreamSetting.tsx +++ b/src/renderer/src/components/settings/CompareAgainstUpstreamSetting.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' import { SearchableSetting } from './SearchableSetting' import { SettingsRow, SettingsSegmentedControl } from './SettingsFormControls' diff --git a/src/renderer/src/components/settings/DefaultWindowsProjectRuntimeSetting.tsx b/src/renderer/src/components/settings/DefaultWindowsProjectRuntimeSetting.tsx index 827def4c2b2..a952e37eea8 100644 --- a/src/renderer/src/components/settings/DefaultWindowsProjectRuntimeSetting.tsx +++ b/src/renderer/src/components/settings/DefaultWindowsProjectRuntimeSetting.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { GlobalWindowsRuntimeDefault } from '../../../../shared/project-execution-runtime' import { normalizeGlobalWindowsRuntimeDefault } from '../../../../shared/project-execution-runtime' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' diff --git a/src/renderer/src/components/settings/EditorFontFamilySetting.tsx b/src/renderer/src/components/settings/EditorFontFamilySetting.tsx index 911a4e2fde1..f771fa24c7a 100644 --- a/src/renderer/src/components/settings/EditorFontFamilySetting.tsx +++ b/src/renderer/src/components/settings/EditorFontFamilySetting.tsx @@ -1,5 +1,5 @@ import type React from 'react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' import { SearchableSetting } from './SearchableSetting' import { FontAutocomplete, SettingsRow } from './SettingsFormControls' diff --git a/src/renderer/src/components/settings/EditorWordWrapSetting.tsx b/src/renderer/src/components/settings/EditorWordWrapSetting.tsx index b3dfbb853c4..c9283daf731 100644 --- a/src/renderer/src/components/settings/EditorWordWrapSetting.tsx +++ b/src/renderer/src/components/settings/EditorWordWrapSetting.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' import { SearchableSetting } from './SearchableSetting' import { Label } from '../ui/label' diff --git a/src/renderer/src/components/settings/EphemeralVmRecipeRow.tsx b/src/renderer/src/components/settings/EphemeralVmRecipeRow.tsx index 1e09e65c4ab..8a42283ff51 100644 --- a/src/renderer/src/components/settings/EphemeralVmRecipeRow.tsx +++ b/src/renderer/src/components/settings/EphemeralVmRecipeRow.tsx @@ -1,6 +1,6 @@ import { Play } from 'lucide-react' import type React from 'react' -import type { OrcaHooks } from '../../../../shared/types' +import type { OrcaHooks } from '../../../../shared/orca-yaml-hook-types' import { Button } from '../ui/button' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/settings/EphemeralVmsExperimentalSetting.tsx b/src/renderer/src/components/settings/EphemeralVmsExperimentalSetting.tsx index 543744136cc..dd4db727bc4 100644 --- a/src/renderer/src/components/settings/EphemeralVmsExperimentalSetting.tsx +++ b/src/renderer/src/components/settings/EphemeralVmsExperimentalSetting.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' import { Label } from '../ui/label' import { EphemeralVmsPane } from './EphemeralVmsPane' diff --git a/src/renderer/src/components/settings/ExperimentalPane.test.tsx b/src/renderer/src/components/settings/ExperimentalPane.test.tsx index e230ba7b670..54264e1a4f6 100644 --- a/src/renderer/src/components/settings/ExperimentalPane.test.tsx +++ b/src/renderer/src/components/settings/ExperimentalPane.test.tsx @@ -4,7 +4,7 @@ import { act } from 'react' import { renderToStaticMarkup } from 'react-dom/server' import { createRoot, type Root } from 'react-dom/client' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { getDefaultSettings } from '../../../../shared/constants' import { ExperimentalPane } from './ExperimentalPane' import { getExperimentalPaneSearchEntries } from './experimental-search' diff --git a/src/renderer/src/components/settings/ExperimentalPane.tsx b/src/renderer/src/components/settings/ExperimentalPane.tsx index 25c161d0da9..bccd7989a90 100644 --- a/src/renderer/src/components/settings/ExperimentalPane.tsx +++ b/src/renderer/src/components/settings/ExperimentalPane.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { Label } from '../ui/label' import { Switch } from '../ui/switch' import { useAppStore } from '../../store' diff --git a/src/renderer/src/components/settings/FloatingWorkspacePane.tsx b/src/renderer/src/components/settings/FloatingWorkspacePane.tsx index 14ace880d92..16c7b55c9cd 100644 --- a/src/renderer/src/components/settings/FloatingWorkspacePane.tsx +++ b/src/renderer/src/components/settings/FloatingWorkspacePane.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react' import { FolderOpen } from 'lucide-react' -import type { FloatingTerminalTriggerLocation, GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { FloatingTerminalTriggerLocation } from '../../../../shared/ui-chrome-types' import { Button } from '../ui/button' import { Input } from '../ui/input' import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group' diff --git a/src/renderer/src/components/settings/GeneralEditorSettingsSection.tsx b/src/renderer/src/components/settings/GeneralEditorSettingsSection.tsx index b8d710b6886..6b3b211f628 100644 --- a/src/renderer/src/components/settings/GeneralEditorSettingsSection.tsx +++ b/src/renderer/src/components/settings/GeneralEditorSettingsSection.tsx @@ -1,6 +1,6 @@ import type React from 'react' import { useState } from 'react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { DEFAULT_EDITOR_AUTO_SAVE_DELAY_MS, MAX_EDITOR_AUTO_SAVE_DELAY_MS, diff --git a/src/renderer/src/components/settings/GeneralPane.tsx b/src/renderer/src/components/settings/GeneralPane.tsx index 021fc5d7111..f688f500567 100644 --- a/src/renderer/src/components/settings/GeneralPane.tsx +++ b/src/renderer/src/components/settings/GeneralPane.tsx @@ -1,5 +1,5 @@ import type React from 'react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { useAppStore } from '../../store' import { Separator } from '../ui/separator' import { CliSection } from './CliSection' diff --git a/src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.tsx b/src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.tsx index 474628a5cf0..7531790a7a1 100644 --- a/src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.tsx +++ b/src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.tsx @@ -1,5 +1,5 @@ import type React from 'react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { OpenInMenuSetting } from './OpenInMenuSetting' import { SearchableSetting } from './SearchableSetting' import { SettingsSubsectionHeader, SettingsSwitchRow } from './SettingsFormControls' diff --git a/src/renderer/src/components/settings/GhosttyImportModal.test.ts b/src/renderer/src/components/settings/GhosttyImportModal.test.ts index 67590e710f2..bad4d4118fc 100644 --- a/src/renderer/src/components/settings/GhosttyImportModal.test.ts +++ b/src/renderer/src/components/settings/GhosttyImportModal.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { GhosttyImportPreview } from '../../../../shared/types' +import type { GhosttyImportPreview } from '../../../../shared/global-settings-types' import { GhosttyImportModal } from './GhosttyImportModal' import { Button } from '../ui/button' diff --git a/src/renderer/src/components/settings/GhosttyImportModal.tsx b/src/renderer/src/components/settings/GhosttyImportModal.tsx index 7bdcc08ad6a..cc515405c96 100644 --- a/src/renderer/src/components/settings/GhosttyImportModal.tsx +++ b/src/renderer/src/components/settings/GhosttyImportModal.tsx @@ -1,4 +1,4 @@ -import type { GhosttyImportPreview } from '../../../../shared/types' +import type { GhosttyImportPreview } from '../../../../shared/global-settings-types' import { Button } from '../ui/button' import { Dialog, diff --git a/src/renderer/src/components/settings/GitPane.test.tsx b/src/renderer/src/components/settings/GitPane.test.tsx index 6edf003c370..70cc28c3289 100644 --- a/src/renderer/src/components/settings/GitPane.test.tsx +++ b/src/renderer/src/components/settings/GitPane.test.tsx @@ -1,7 +1,7 @@ import React from 'react' import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it } from 'vitest' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { getDefaultSettings } from '../../../../shared/constants' import { TooltipProvider } from '../ui/tooltip' import { GitPane } from './GitPane' diff --git a/src/renderer/src/components/settings/GitPane.tsx b/src/renderer/src/components/settings/GitPane.tsx index 72eb70f15c2..af739b6aa56 100644 --- a/src/renderer/src/components/settings/GitPane.tsx +++ b/src/renderer/src/components/settings/GitPane.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from 'react' -import type { GlobalSettings, SourceControlGroupOrder } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { SourceControlGroupOrder } from '../../../../shared/ui-chrome-types' import type { SourceControlAiSettingsPatch } from '../../../../shared/source-control-ai-types' import { DEFAULT_SOURCE_CONTROL_GROUP_ORDER } from '../../../../shared/source-control-group-order' import { Input } from '../ui/input' diff --git a/src/renderer/src/components/settings/InputPane.tsx b/src/renderer/src/components/settings/InputPane.tsx index 55f64d550da..286c7ebe351 100644 --- a/src/renderer/src/components/settings/InputPane.tsx +++ b/src/renderer/src/components/settings/InputPane.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { Label } from '../ui/label' import { Switch } from '../ui/switch' import { SearchableSetting } from './SearchableSetting' diff --git a/src/renderer/src/components/settings/LeftSidebarAppearanceSetting.tsx b/src/renderer/src/components/settings/LeftSidebarAppearanceSetting.tsx index bf4cb1a81b7..165d92a2817 100644 --- a/src/renderer/src/components/settings/LeftSidebarAppearanceSetting.tsx +++ b/src/renderer/src/components/settings/LeftSidebarAppearanceSetting.tsx @@ -1,5 +1,6 @@ import type React from 'react' -import type { GlobalSettings, LeftSidebarAppearanceMode } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { LeftSidebarAppearanceMode } from '../../../../shared/ui-chrome-types' import { DEFAULT_LEFT_SIDEBAR_TINT_COLOR, DEFAULT_LEFT_SIDEBAR_TINT_OPACITY, diff --git a/src/renderer/src/components/settings/McpConfigSection.tsx b/src/renderer/src/components/settings/McpConfigSection.tsx index ce9b3d8db4a..e4ceac4c892 100644 --- a/src/renderer/src/components/settings/McpConfigSection.tsx +++ b/src/renderer/src/components/settings/McpConfigSection.tsx @@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { AlertCircle, FileCode2, LoaderCircle, Plus, RefreshCw } from 'lucide-react' import { toast } from 'sonner' import { useMountedRef } from '@/hooks/useMountedRef' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id' import { canInspectLocalMcpConfigRoot, diff --git a/src/renderer/src/components/settings/MobileEmulatorSettingsPane.tsx b/src/renderer/src/components/settings/MobileEmulatorSettingsPane.tsx index e4f5adaa47f..6453e27d3f7 100644 --- a/src/renderer/src/components/settings/MobileEmulatorSettingsPane.tsx +++ b/src/renderer/src/components/settings/MobileEmulatorSettingsPane.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { Loader2, RefreshCw } from 'lucide-react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { cn } from '@/lib/utils' import { callRuntimeRpc } from '@/runtime/runtime-rpc-client' import { Badge } from '../ui/badge' diff --git a/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx b/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx index 464b1ca7797..d7ea704c553 100644 --- a/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx +++ b/src/renderer/src/components/settings/NativeChatExperimentalSetting.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' import { Label } from '../ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' diff --git a/src/renderer/src/components/settings/NotificationSoundSection.tsx b/src/renderer/src/components/settings/NotificationSoundSection.tsx index da1fe4e11cc..e90489741ae 100644 --- a/src/renderer/src/components/settings/NotificationSoundSection.tsx +++ b/src/renderer/src/components/settings/NotificationSoundSection.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { toast } from 'sonner' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { Label } from '../ui/label' import { Slider } from '../ui/slider' import { diff --git a/src/renderer/src/components/settings/NotificationsPane.test.tsx b/src/renderer/src/components/settings/NotificationsPane.test.tsx index 08d43709a3e..7828efad6ce 100644 --- a/src/renderer/src/components/settings/NotificationsPane.test.tsx +++ b/src/renderer/src/components/settings/NotificationsPane.test.tsx @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { renderToStaticMarkup } from 'react-dom/server' -import type { GlobalSettings, NotificationDispatchRequest } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { NotificationDispatchRequest } from '../../../../shared/notification-settings-types' import { getNotificationSoundOptions } from '@/components/notification-sound-options' import { createNotificationVolumeDraftState, diff --git a/src/renderer/src/components/settings/NotificationsPane.tsx b/src/renderer/src/components/settings/NotificationsPane.tsx index 5d1bb6a23d3..e80f3e49512 100644 --- a/src/renderer/src/components/settings/NotificationsPane.tsx +++ b/src/renderer/src/components/settings/NotificationsPane.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { Button } from '../ui/button' import { Separator } from '../ui/separator' import { BellRing, Bot, Siren } from 'lucide-react' diff --git a/src/renderer/src/components/settings/OpenInMenuSetting.tsx b/src/renderer/src/components/settings/OpenInMenuSetting.tsx index b6c3f50f4ae..aa18ec951f1 100644 --- a/src/renderer/src/components/settings/OpenInMenuSetting.tsx +++ b/src/renderer/src/components/settings/OpenInMenuSetting.tsx @@ -1,7 +1,8 @@ import type React from 'react' import { useState } from 'react' import { Check, ChevronDown, Pencil, Trash2 } from 'lucide-react' -import type { GlobalSettings, OpenInApplication } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { OpenInApplication } from '../../../../shared/ui-chrome-types' import { OPEN_IN_APPLICATIONS_MAX } from '../../../../shared/open-in-applications' import { Button } from '../ui/button' import { diff --git a/src/renderer/src/components/settings/PluginsSettingsSection.lifecycle.test.tsx b/src/renderer/src/components/settings/PluginsSettingsSection.lifecycle.test.tsx index db731518101..74f12f0779c 100644 --- a/src/renderer/src/components/settings/PluginsSettingsSection.lifecycle.test.tsx +++ b/src/renderer/src/components/settings/PluginsSettingsSection.lifecycle.test.tsx @@ -5,7 +5,7 @@ import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { PluginHostListEntry } from '../../../../preload/api-types' import { getDefaultSettings } from '../../../../shared/constants' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { PluginsSettingsSection } from './PluginsSettingsSection' vi.mock('./SettingsSection', () => ({ diff --git a/src/renderer/src/components/settings/PluginsSettingsSection.tsx b/src/renderer/src/components/settings/PluginsSettingsSection.tsx index 30b54aa4284..c76fd6b846f 100644 --- a/src/renderer/src/components/settings/PluginsSettingsSection.tsx +++ b/src/renderer/src/components/settings/PluginsSettingsSection.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react' import { Plus } from 'lucide-react' import type { PluginHostInstallSource, PluginHostListEntry } from '../../../../preload/api-types' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' import { Button } from '../ui/button' import { PluginConsentDialog } from './PluginConsentDialog' diff --git a/src/renderer/src/components/settings/PrivacyPane.test.ts b/src/renderer/src/components/settings/PrivacyPane.test.ts index 751e1b5627a..5739e39e1d5 100644 --- a/src/renderer/src/components/settings/PrivacyPane.test.ts +++ b/src/renderer/src/components/settings/PrivacyPane.test.ts @@ -15,7 +15,7 @@ import React from 'react' import { renderToStaticMarkup } from 'react-dom/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { TelemetryConsentState } from '../../../../shared/telemetry-consent-types' const { getConsentStateMock, setOptInMock } = vi.hoisted(() => ({ diff --git a/src/renderer/src/components/settings/PrivacyPane.tsx b/src/renderer/src/components/settings/PrivacyPane.tsx index f36765e4ffc..9d24b34ffd3 100644 --- a/src/renderer/src/components/settings/PrivacyPane.tsx +++ b/src/renderer/src/components/settings/PrivacyPane.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import { ShieldCheck } from 'lucide-react' import { useMountedRef } from '@/hooks/useMountedRef' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { TelemetryConsentState } from '../../../../shared/telemetry-consent-types' import { Label } from '../ui/label' import { Switch } from '../ui/switch' diff --git a/src/renderer/src/components/settings/ProjectWindowsRuntimeSetting.test.tsx b/src/renderer/src/components/settings/ProjectWindowsRuntimeSetting.test.tsx index 94ea998bb8e..6caa188c9d1 100644 --- a/src/renderer/src/components/settings/ProjectWindowsRuntimeSetting.test.tsx +++ b/src/renderer/src/components/settings/ProjectWindowsRuntimeSetting.test.tsx @@ -5,7 +5,7 @@ import { createRoot } from 'react-dom/client' import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../../shared/constants' -import type { Project } from '../../../../shared/types' +import type { Project } from '../../../../shared/project-types' import { ProjectWindowsRuntimeSetting } from './ProjectWindowsRuntimeSetting' const project: Project = { diff --git a/src/renderer/src/components/settings/ProjectWindowsRuntimeSetting.tsx b/src/renderer/src/components/settings/ProjectWindowsRuntimeSetting.tsx index 84beb4296e5..8dc75a4bf16 100644 --- a/src/renderer/src/components/settings/ProjectWindowsRuntimeSetting.tsx +++ b/src/renderer/src/components/settings/ProjectWindowsRuntimeSetting.tsx @@ -1,4 +1,5 @@ -import type { GlobalSettings, Project, ProjectUpdateArgs } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Project, ProjectUpdateArgs } from '../../../../shared/project-types' import type { LocalWindowsRuntimePreference } from '../../../../shared/project-execution-runtime' import { normalizeProjectRuntimePreference, diff --git a/src/renderer/src/components/settings/QuickCommandsList.tsx b/src/renderer/src/components/settings/QuickCommandsList.tsx index 3a0042b32a8..d86fe657010 100644 --- a/src/renderer/src/components/settings/QuickCommandsList.tsx +++ b/src/renderer/src/components/settings/QuickCommandsList.tsx @@ -1,9 +1,9 @@ import { Check, Copy, Pencil, TerminalSquare, Trash2 } from 'lucide-react' +import type { Repo } from '../../../../shared/repo-types' import type { - Repo, TerminalQuickCommand, TerminalQuickCommandScope -} from '../../../../shared/types' +} from '../../../../shared/terminal-quick-command-types' import { getTerminalQuickCommandBody, getTerminalQuickCommandScope, diff --git a/src/renderer/src/components/settings/QuickCommandsPane.tsx b/src/renderer/src/components/settings/QuickCommandsPane.tsx index a4f1ea85fe4..2305a034227 100644 --- a/src/renderer/src/components/settings/QuickCommandsPane.tsx +++ b/src/renderer/src/components/settings/QuickCommandsPane.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import type { GlobalSettings, TerminalQuickCommand } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' import { getTerminalQuickCommandScope } from '../../../../shared/terminal-quick-commands' import { createTerminalQuickCommandDraft, diff --git a/src/renderer/src/components/settings/QuickCommandsScopeFilter.tsx b/src/renderer/src/components/settings/QuickCommandsScopeFilter.tsx index 749d554b783..c878d34b015 100644 --- a/src/renderer/src/components/settings/QuickCommandsScopeFilter.tsx +++ b/src/renderer/src/components/settings/QuickCommandsScopeFilter.tsx @@ -1,6 +1,6 @@ import { Check, ChevronsUpDown } from 'lucide-react' import type { Dispatch, SetStateAction } from 'react' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { Button } from '../ui/button' import { Command, CommandItem, CommandList } from '../ui/command' import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover' diff --git a/src/renderer/src/components/settings/QuickCommandsToolbar.tsx b/src/renderer/src/components/settings/QuickCommandsToolbar.tsx index 65e034eeb3f..202e148b1e3 100644 --- a/src/renderer/src/components/settings/QuickCommandsToolbar.tsx +++ b/src/renderer/src/components/settings/QuickCommandsToolbar.tsx @@ -1,6 +1,6 @@ import type { Dispatch, SetStateAction } from 'react' import { Plus, Search } from 'lucide-react' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import type { ExecutionHostId } from '../../../../shared/execution-host' import { Button } from '../ui/button' import { Input } from '../ui/input' diff --git a/src/renderer/src/components/settings/RecentTabOrderControl.tsx b/src/renderer/src/components/settings/RecentTabOrderControl.tsx index 84e54c617b5..adb8432973b 100644 --- a/src/renderer/src/components/settings/RecentTabOrderControl.tsx +++ b/src/renderer/src/components/settings/RecentTabOrderControl.tsx @@ -1,5 +1,5 @@ import React from 'react' -import type { CtrlTabOrderMode } from '../../../../shared/types' +import type { CtrlTabOrderMode } from '../../../../shared/tab-types' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' import { SearchableSetting } from './SearchableSetting' import { SettingsRow } from './SettingsFormControls' diff --git a/src/renderer/src/components/settings/RepositoryForkSyncSection.tsx b/src/renderer/src/components/settings/RepositoryForkSyncSection.tsx index f3279d59f1e..3a399f16e38 100644 --- a/src/renderer/src/components/settings/RepositoryForkSyncSection.tsx +++ b/src/renderer/src/components/settings/RepositoryForkSyncSection.tsx @@ -1,7 +1,8 @@ import { useRef, useState } from 'react' import { RefreshCw } from 'lucide-react' import { toast } from 'sonner' -import type { ForkSyncMode, GitForkSyncResult, Repo } from '../../../../shared/types' +import type { ForkSyncMode, GitForkSyncResult } from '../../../../shared/git-fork-sync' +import type { Repo } from '../../../../shared/repo-types' import { Button } from '../ui/button' import { SearchableSetting } from './SearchableSetting' import { SettingsSegmentedControl } from './SettingsFormControls' diff --git a/src/renderer/src/components/settings/RepositoryHooksSection.test.ts b/src/renderer/src/components/settings/RepositoryHooksSection.test.ts index 6ca17d9fca3..114c7195e82 100644 --- a/src/renderer/src/components/settings/RepositoryHooksSection.test.ts +++ b/src/renderer/src/components/settings/RepositoryHooksSection.test.ts @@ -3,7 +3,7 @@ import React, { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { getLocalCommandSourcePolicyNotice, RepositoryHooksSection } from './RepositoryHooksSection' vi.mock('@/store', () => ({ diff --git a/src/renderer/src/components/settings/RepositoryHooksSection.tsx b/src/renderer/src/components/settings/RepositoryHooksSection.tsx index bec7ba52a28..e6270dce42b 100644 --- a/src/renderer/src/components/settings/RepositoryHooksSection.tsx +++ b/src/renderer/src/components/settings/RepositoryHooksSection.tsx @@ -4,11 +4,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import type { HookCommandSourcePolicy, OrcaHooks, - Repo, RepoHookSettings, SetupAgentStartupPolicy, SetupRunPolicy -} from '../../../../shared/types' +} from '../../../../shared/orca-yaml-hook-types' +import type { Repo } from '../../../../shared/repo-types' import { AlertTriangle, ChevronRight, Plus } from 'lucide-react' import { toast } from 'sonner' import { useTranslation } from 'react-i18next' diff --git a/src/renderer/src/components/settings/RepositoryHostSetupActions.tsx b/src/renderer/src/components/settings/RepositoryHostSetupActions.tsx index e6edcbe68dc..a3527747681 100644 --- a/src/renderer/src/components/settings/RepositoryHostSetupActions.tsx +++ b/src/renderer/src/components/settings/RepositoryHostSetupActions.tsx @@ -5,7 +5,7 @@ import type { ProjectHostSetup, ProjectHostSetupCreateResult, ProjectHostSetupResult -} from '../../../../shared/types' +} from '../../../../shared/project-types' import { translate } from '@/i18n/i18n' import { Button } from '../ui/button' import { Label } from '../ui/label' diff --git a/src/renderer/src/components/settings/RepositoryHostSetupsSection.test.tsx b/src/renderer/src/components/settings/RepositoryHostSetupsSection.test.tsx index c448002aa6b..7af24b68947 100644 --- a/src/renderer/src/components/settings/RepositoryHostSetupsSection.test.tsx +++ b/src/renderer/src/components/settings/RepositoryHostSetupsSection.test.tsx @@ -9,7 +9,8 @@ import { RUNTIME_PROTOCOL_VERSION, WORKSPACE_RUN_CONTEXT_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version' -import type { Project, ProjectHostSetup, Repo } from '../../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { useAppStore } from '../../store' import { RepositoryHostSetupsSection } from './RepositoryHostSetupsSection' diff --git a/src/renderer/src/components/settings/RepositoryHostSetupsSection.tsx b/src/renderer/src/components/settings/RepositoryHostSetupsSection.tsx index 9fa983138bf..71b292059a4 100644 --- a/src/renderer/src/components/settings/RepositoryHostSetupsSection.tsx +++ b/src/renderer/src/components/settings/RepositoryHostSetupsSection.tsx @@ -7,7 +7,8 @@ import { } from '../../../../shared/execution-host' import { buildExecutionHostRegistry } from '../../../../shared/execution-host-registry' import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-overrides' -import type { ProjectHostSetup, Repo } from '../../../../shared/types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { useAppStore } from '../../store' import { getProjectHostSetupProjectionFromState } from '../../store/selectors' import { cn } from '../../lib/utils' diff --git a/src/renderer/src/components/settings/RepositoryHostSetupsSection.workspace-window.test.tsx b/src/renderer/src/components/settings/RepositoryHostSetupsSection.workspace-window.test.tsx index 81cdefa0a55..f6a4badabbb 100644 --- a/src/renderer/src/components/settings/RepositoryHostSetupsSection.workspace-window.test.tsx +++ b/src/renderer/src/components/settings/RepositoryHostSetupsSection.workspace-window.test.tsx @@ -9,7 +9,8 @@ import { WORKSPACE_RUN_CONTEXT_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version' import type { RuntimeStatus } from '../../../../shared/runtime-types' -import type { Project, ProjectHostSetup, Repo } from '../../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { useAppStore } from '../../store' import { RepositoryHostSetupsSection } from './RepositoryHostSetupsSection' diff --git a/src/renderer/src/components/settings/RepositoryIconPicker.github-avatar-refresh.test.tsx b/src/renderer/src/components/settings/RepositoryIconPicker.github-avatar-refresh.test.tsx index 996261ab3e9..2d2da75d483 100644 --- a/src/renderer/src/components/settings/RepositoryIconPicker.github-avatar-refresh.test.tsx +++ b/src/renderer/src/components/settings/RepositoryIconPicker.github-avatar-refresh.test.tsx @@ -3,7 +3,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { RepositoryIconPicker } from './RepositoryIconPicker' vi.mock('@/runtime/runtime-rpc-client', () => ({ diff --git a/src/renderer/src/components/settings/RepositoryIconPicker.tsx b/src/renderer/src/components/settings/RepositoryIconPicker.tsx index 51edc955fb5..033bf24391e 100644 --- a/src/renderer/src/components/settings/RepositoryIconPicker.tsx +++ b/src/renderer/src/components/settings/RepositoryIconPicker.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' import { RotateCcw } from 'lucide-react' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import type { RepoIcon } from '../../../../shared/repo-icon' import { DEFAULT_REPO_BADGE_COLOR } from '../../../../shared/constants' import { normalizeRepoBadgeColor } from '../../../../shared/repo-badge-color' diff --git a/src/renderer/src/components/settings/RepositoryPane.test.ts b/src/renderer/src/components/settings/RepositoryPane.test.ts index 47c00a0a601..54518e4932b 100644 --- a/src/renderer/src/components/settings/RepositoryPane.test.ts +++ b/src/renderer/src/components/settings/RepositoryPane.test.ts @@ -5,7 +5,9 @@ import { createRoot, type Root } from 'react-dom/client' import { renderToStaticMarkup } from 'react-dom/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../../shared/constants' -import type { Project, Repo, TerminalTab } from '../../../../shared/types' +import type { Project } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' import { useAppStore } from '../../store' import { diff --git a/src/renderer/src/components/settings/RepositoryPane.tsx b/src/renderer/src/components/settings/RepositoryPane.tsx index ecec034cec2..6b0963ef009 100644 --- a/src/renderer/src/components/settings/RepositoryPane.tsx +++ b/src/renderer/src/components/settings/RepositoryPane.tsx @@ -1,11 +1,7 @@ import { useCallback, useRef, useState } from 'react' -import type { - OrcaHooks, - Project, - ProjectUpdateArgs, - Repo, - RepoHookSettings -} from '../../../../shared/types' +import type { OrcaHooks, RepoHookSettings } from '../../../../shared/orca-yaml-hook-types' +import type { Project, ProjectUpdateArgs } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { getRepoKindLabel, isFolderRepo } from '../../../../shared/repo-kind' import { getRepoExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host' import { Button } from '../ui/button' diff --git a/src/renderer/src/components/settings/RepositorySourceControlAiActionRows.tsx b/src/renderer/src/components/settings/RepositorySourceControlAiActionRows.tsx index 9fcfe68c077..a9a766d8b83 100644 --- a/src/renderer/src/components/settings/RepositorySourceControlAiActionRows.tsx +++ b/src/renderer/src/components/settings/RepositorySourceControlAiActionRows.tsx @@ -1,6 +1,6 @@ import type React from 'react' import { Terminal } from 'lucide-react' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { CUSTOM_AGENT_ID } from '../../../../shared/commit-message-agent-spec' import type { RepoSourceControlAiOverrides, diff --git a/src/renderer/src/components/settings/RepositorySourceControlAiSection.tsx b/src/renderer/src/components/settings/RepositorySourceControlAiSection.tsx index 7e600b11bf6..9eec7bf2319 100644 --- a/src/renderer/src/components/settings/RepositorySourceControlAiSection.tsx +++ b/src/renderer/src/components/settings/RepositorySourceControlAiSection.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react' import type React from 'react' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { normalizeSourceControlAiSettings } from '../../../../shared/source-control-ai' import type { SourceControlAiRepoUpdate } from '../../../../shared/source-control-ai-recipe-save' import { useAppStore } from '../../store' diff --git a/src/renderer/src/components/settings/RepositoryWindowsRuntimeSection.tsx b/src/renderer/src/components/settings/RepositoryWindowsRuntimeSection.tsx index 23831b7f601..7c5743c196b 100644 --- a/src/renderer/src/components/settings/RepositoryWindowsRuntimeSection.tsx +++ b/src/renderer/src/components/settings/RepositoryWindowsRuntimeSection.tsx @@ -1,4 +1,5 @@ -import type { GlobalSettings, Project, ProjectUpdateArgs } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Project, ProjectUpdateArgs } from '../../../../shared/project-types' import { SearchableSetting } from './SearchableSetting' import type { SettingsSearchEntry } from './settings-search' import { matchesSettingsSearch } from './settings-search' diff --git a/src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.test.tsx b/src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.test.tsx index eaf2b453988..03164eeb261 100644 --- a/src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.test.tsx +++ b/src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.test.tsx @@ -3,7 +3,7 @@ import React, { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { RepositoryWorktreeDefaultsSection } from './RepositoryWorktreeDefaultsSection' vi.mock('../../store', () => ({ diff --git a/src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.tsx b/src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.tsx index e9bef371510..1aaa4662a4c 100644 --- a/src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.tsx +++ b/src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.tsx @@ -1,4 +1,5 @@ -import type { GlobalSettings, Repo } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' import { Button } from '../ui/button' import { Label } from '../ui/label' import { BaseRefPicker } from './BaseRefPicker' diff --git a/src/renderer/src/components/settings/RichMarkdownSpellcheckSetting.tsx b/src/renderer/src/components/settings/RichMarkdownSpellcheckSetting.tsx index c59a6302386..214bff3282d 100644 --- a/src/renderer/src/components/settings/RichMarkdownSpellcheckSetting.tsx +++ b/src/renderer/src/components/settings/RichMarkdownSpellcheckSetting.tsx @@ -1,5 +1,5 @@ import type React from 'react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' import { SearchableSetting } from './SearchableSetting' import { SettingsSwitchRow } from './SettingsFormControls' diff --git a/src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx b/src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx index bd7d31f9652..a9e46b647ad 100644 --- a/src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx +++ b/src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx @@ -15,7 +15,7 @@ import { import { useCallback, useEffect, useRef, useState } from 'react' import { toast } from 'sonner' import { useMountedRef } from '@/hooks/useMountedRef' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { isUserManagedRuntimeEnvironment, type PublicKnownRuntimeEnvironment diff --git a/src/renderer/src/components/settings/Settings.tsx b/src/renderer/src/components/settings/Settings.tsx index cf07ff934cd..e346de234ef 100644 --- a/src/renderer/src/components/settings/Settings.tsx +++ b/src/renderer/src/components/settings/Settings.tsx @@ -10,7 +10,10 @@ import { type MutableRefObject } from 'react' import { toast } from 'sonner' -import type { GlobalSettings, OrcaHooks, ProjectHostSetup, Repo } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { OrcaHooks } from '../../../../shared/orca-yaml-hook-types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import type { SpeechModelState } from '../../../../shared/speech-types' import type { SourceControlAiSettings, diff --git a/src/renderer/src/components/settings/SettingsSidebar.test.tsx b/src/renderer/src/components/settings/SettingsSidebar.test.tsx index 75b5785be36..c7125b1109e 100644 --- a/src/renderer/src/components/settings/SettingsSidebar.test.tsx +++ b/src/renderer/src/components/settings/SettingsSidebar.test.tsx @@ -7,7 +7,7 @@ import { getDefaultSettings } from '../../../../shared/constants' import { SettingsSidebar } from './SettingsSidebar' import { TooltipProvider } from '../ui/tooltip' import type { SettingsSetupGuideProgress } from './settings-setup-guide-progress' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' const mocks = vi.hoisted(() => ({ useSettingsSetupGuideProgress: vi.fn() diff --git a/src/renderer/src/components/settings/SettingsSidebar.tsx b/src/renderer/src/components/settings/SettingsSidebar.tsx index c56e71b96d0..11f73b3016d 100644 --- a/src/renderer/src/components/settings/SettingsSidebar.tsx +++ b/src/renderer/src/components/settings/SettingsSidebar.tsx @@ -3,7 +3,8 @@ import { useMemo } from 'react' import { ArrowLeft, Search, Server } from 'lucide-react' import type { RepoIcon } from '../../../../shared/repo-icon' import type { SettingsNavIcon, SettingsNavInstallStatus } from '@/lib/settings-navigation-types' -import type { GitHubRepositoryIdentity, GlobalSettings } from '../../../../shared/types' +import type { GitHubRepositoryIdentity } from '../../../../shared/github/pull-request-types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { useShortcutKeyComboDetails } from '@/hooks/useShortcutLabel' import { ShortcutKeyCombo } from '../ShortcutKeyCombo' import { cn } from '@/lib/utils' diff --git a/src/renderer/src/components/settings/SourceControlActionRecipeRow.tsx b/src/renderer/src/components/settings/SourceControlActionRecipeRow.tsx index cb0b4c8dc5a..1bdacd6e024 100644 --- a/src/renderer/src/components/settings/SourceControlActionRecipeRow.tsx +++ b/src/renderer/src/components/settings/SourceControlActionRecipeRow.tsx @@ -1,5 +1,6 @@ import { Terminal } from 'lucide-react' -import type { GlobalSettings, TuiAgent } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { CustomAgentId } from '../../../../shared/commit-message-agent-spec' import { CUSTOM_AGENT_ID, isCustomAgentId } from '../../../../shared/commit-message-agent-spec' import { diff --git a/src/renderer/src/components/settings/SourceControlAiActionRecipeDefaults.tsx b/src/renderer/src/components/settings/SourceControlAiActionRecipeDefaults.tsx index 56906be96d3..9d00e6505cd 100644 --- a/src/renderer/src/components/settings/SourceControlAiActionRecipeDefaults.tsx +++ b/src/renderer/src/components/settings/SourceControlAiActionRecipeDefaults.tsx @@ -1,7 +1,8 @@ import { useMemo } from 'react' import type React from 'react' import { toast } from 'sonner' -import type { GlobalSettings, TuiAgent } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { isFolderRepo } from '../../../../shared/repo-kind' import type { SourceControlAiSettings, diff --git a/src/renderer/src/components/settings/SparsePresetSettingsSection.test.tsx b/src/renderer/src/components/settings/SparsePresetSettingsSection.test.tsx index bcdfccf6e51..271f66d6570 100644 --- a/src/renderer/src/components/settings/SparsePresetSettingsSection.test.tsx +++ b/src/renderer/src/components/settings/SparsePresetSettingsSection.test.tsx @@ -1,6 +1,6 @@ import { renderToStaticMarkup } from 'react-dom/server' import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { SparsePreset } from '../../../../shared/types' +import type { SparsePreset } from '../../../../shared/worktree/create-types' import { SparsePresetSettingsSection } from './SparsePresetSettingsSection' const storeMock = vi.hoisted(() => ({ diff --git a/src/renderer/src/components/settings/SparsePresetSettingsSection.tsx b/src/renderer/src/components/settings/SparsePresetSettingsSection.tsx index a59f8344e44..4c641bb8125 100644 --- a/src/renderer/src/components/settings/SparsePresetSettingsSection.tsx +++ b/src/renderer/src/components/settings/SparsePresetSettingsSection.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { Plus } from 'lucide-react' -import type { SparsePreset } from '../../../../shared/types' +import type { SparsePreset } from '../../../../shared/worktree/create-types' import { useAppStore } from '../../store' import { parseSparsePresetDirectories } from '@/lib/sparse-preset-draft' import { useMountedRef } from '@/hooks/useMountedRef' diff --git a/src/renderer/src/components/settings/TasksPane.test.tsx b/src/renderer/src/components/settings/TasksPane.test.tsx index e87df95c0db..80376ab87d7 100644 --- a/src/renderer/src/components/settings/TasksPane.test.tsx +++ b/src/renderer/src/components/settings/TasksPane.test.tsx @@ -4,7 +4,8 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { renderToStaticMarkup } from 'react-dom/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings, TaskProvider } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { TaskProvider } from '../../../../shared/task-providers' import type { TaskProviderReadiness } from './task-source-setup-state' import { TasksPane } from './TasksPane' diff --git a/src/renderer/src/components/settings/TasksPane.tsx b/src/renderer/src/components/settings/TasksPane.tsx index c5b547b3850..8b1eb14929a 100644 --- a/src/renderer/src/components/settings/TasksPane.tsx +++ b/src/renderer/src/components/settings/TasksPane.tsx @@ -1,6 +1,7 @@ import { useState } from 'react' import { Github, Gitlab } from 'lucide-react' -import type { GlobalSettings, TaskProvider } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { TaskProvider } from '../../../../shared/task-providers' import { TASK_PROVIDERS, normalizeVisibleTaskProviders, diff --git a/src/renderer/src/components/settings/TerminalAdvancedSection.test.tsx b/src/renderer/src/components/settings/TerminalAdvancedSection.test.tsx index a9f361d7ce3..f06fe63775a 100644 --- a/src/renderer/src/components/settings/TerminalAdvancedSection.test.tsx +++ b/src/renderer/src/components/settings/TerminalAdvancedSection.test.tsx @@ -3,7 +3,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { TerminalAdvancedSection } from './TerminalAdvancedSection' vi.mock('@/i18n/i18n', () => ({ diff --git a/src/renderer/src/components/settings/TerminalAdvancedSection.tsx b/src/renderer/src/components/settings/TerminalAdvancedSection.tsx index 27d29ed4241..a0a2df5c569 100644 --- a/src/renderer/src/components/settings/TerminalAdvancedSection.tsx +++ b/src/renderer/src/components/settings/TerminalAdvancedSection.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { DESKTOP_TERMINAL_SCROLLBACK_ROWS_MAX, DESKTOP_TERMINAL_SCROLLBACK_ROWS_MIN, diff --git a/src/renderer/src/components/settings/TerminalAdvancedTypographyControls.tsx b/src/renderer/src/components/settings/TerminalAdvancedTypographyControls.tsx index b6fd19db228..870314eb64e 100644 --- a/src/renderer/src/components/settings/TerminalAdvancedTypographyControls.tsx +++ b/src/renderer/src/components/settings/TerminalAdvancedTypographyControls.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { DEFAULT_TERMINAL_FONT_WEIGHT, DEFAULT_TERMINAL_FONT_WEIGHT_BOLD, diff --git a/src/renderer/src/components/settings/TerminalAppearanceSection.tsx b/src/renderer/src/components/settings/TerminalAppearanceSection.tsx index d0ee90eb03c..b13f535a83a 100644 --- a/src/renderer/src/components/settings/TerminalAppearanceSection.tsx +++ b/src/renderer/src/components/settings/TerminalAppearanceSection.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { matchesSettingsSearch, normalizeSettingsSearchQuery, diff --git a/src/renderer/src/components/settings/TerminalCursorAppearanceSection.tsx b/src/renderer/src/components/settings/TerminalCursorAppearanceSection.tsx index 09219834edd..5188f0e3424 100644 --- a/src/renderer/src/components/settings/TerminalCursorAppearanceSection.tsx +++ b/src/renderer/src/components/settings/TerminalCursorAppearanceSection.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { NumberField, SettingsRow, diff --git a/src/renderer/src/components/settings/TerminalFontSizeSetting.test.tsx b/src/renderer/src/components/settings/TerminalFontSizeSetting.test.tsx index d5e6ecc54bd..949dffbb6a7 100644 --- a/src/renderer/src/components/settings/TerminalFontSizeSetting.test.tsx +++ b/src/renderer/src/components/settings/TerminalFontSizeSetting.test.tsx @@ -3,7 +3,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { TerminalFontSizeSetting } from './TerminalFontSizeSetting' vi.mock('@/i18n/i18n', () => ({ diff --git a/src/renderer/src/components/settings/TerminalFontSizeSetting.tsx b/src/renderer/src/components/settings/TerminalFontSizeSetting.tsx index 61a451c7377..a879136717f 100644 --- a/src/renderer/src/components/settings/TerminalFontSizeSetting.tsx +++ b/src/renderer/src/components/settings/TerminalFontSizeSetting.tsx @@ -1,5 +1,5 @@ import { Minus, Plus } from 'lucide-react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { Button } from '../ui/button' import { Input } from '../ui/input' import { SettingsRow } from './SettingsFormControls' diff --git a/src/renderer/src/components/settings/TerminalInteractionSection.tsx b/src/renderer/src/components/settings/TerminalInteractionSection.tsx index 4ad9d67e762..f7c92e998a4 100644 --- a/src/renderer/src/components/settings/TerminalInteractionSection.tsx +++ b/src/renderer/src/components/settings/TerminalInteractionSection.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { RotateCcw } from 'lucide-react' import { Slider } from '../ui/slider' import { Button } from '../ui/button' diff --git a/src/renderer/src/components/settings/TerminalMacKeyboardSection.tsx b/src/renderer/src/components/settings/TerminalMacKeyboardSection.tsx index 0e4036f2478..f02cbf3d614 100644 --- a/src/renderer/src/components/settings/TerminalMacKeyboardSection.tsx +++ b/src/renderer/src/components/settings/TerminalMacKeyboardSection.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { useDetectedOptionAsAlt } from '@/lib/keyboard-layout/use-effective-mac-option-as-alt' import { translate } from '@/i18n/i18n' import { SearchableSetting } from './SearchableSetting' diff --git a/src/renderer/src/components/settings/TerminalPane.tsx b/src/renderer/src/components/settings/TerminalPane.tsx index a2e463edfa2..aacaeffab13 100644 --- a/src/renderer/src/components/settings/TerminalPane.tsx +++ b/src/renderer/src/components/settings/TerminalPane.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { Separator } from '../ui/separator' import { matchesSettingsSearch } from './settings-search' import { useAppStore } from '../../store' diff --git a/src/renderer/src/components/settings/TerminalPaneAppearanceSection.tsx b/src/renderer/src/components/settings/TerminalPaneAppearanceSection.tsx index bb4e3f79397..f9d07b8d63f 100644 --- a/src/renderer/src/components/settings/TerminalPaneAppearanceSection.tsx +++ b/src/renderer/src/components/settings/TerminalPaneAppearanceSection.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { DEFAULT_TERMINAL_INACTIVE_PANE_OPACITY } from '../../../../shared/constants' import { NumberField, SettingsSubsectionHeader } from './SettingsFormControls' import { SearchableSetting } from './SearchableSetting' diff --git a/src/renderer/src/components/settings/TerminalRenderingSection.tsx b/src/renderer/src/components/settings/TerminalRenderingSection.tsx index 5917247171f..47e3017d353 100644 --- a/src/renderer/src/components/settings/TerminalRenderingSection.tsx +++ b/src/renderer/src/components/settings/TerminalRenderingSection.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { SettingsRow, SettingsSegmentedControl, diff --git a/src/renderer/src/components/settings/TerminalSettingsPreview.lifecycle.test.tsx b/src/renderer/src/components/settings/TerminalSettingsPreview.lifecycle.test.tsx index 211424b09be..fd2ffa0345e 100644 --- a/src/renderer/src/components/settings/TerminalSettingsPreview.lifecycle.test.tsx +++ b/src/renderer/src/components/settings/TerminalSettingsPreview.lifecycle.test.tsx @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { Mock } from 'vitest' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' type Cleanup = () => void diff --git a/src/renderer/src/components/settings/TerminalSettingsPreview.tsx b/src/renderer/src/components/settings/TerminalSettingsPreview.tsx index 20b70ca2139..b3a881f9cd0 100644 --- a/src/renderer/src/components/settings/TerminalSettingsPreview.tsx +++ b/src/renderer/src/components/settings/TerminalSettingsPreview.tsx @@ -14,7 +14,7 @@ import { resolveTerminalLigaturesEnabled } from '../../../../shared/terminal-lig import { normalizeTerminalLineHeight } from '../../../../shared/terminal-line-height-settings' import { PREVIEW_BUFFER } from './terminal-preview-content' import { SettingsSwitch } from './SettingsFormControls' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' // Why: pinned so PREVIEW_BUFFER never wraps; 36 cols fits the 32-char longest line + margin (larger fonts clip, not wrap). diff --git a/src/renderer/src/components/settings/TerminalSetupScriptSection.tsx b/src/renderer/src/components/settings/TerminalSetupScriptSection.tsx index c3b6f549cd8..92bb6fa44a3 100644 --- a/src/renderer/src/components/settings/TerminalSetupScriptSection.tsx +++ b/src/renderer/src/components/settings/TerminalSetupScriptSection.tsx @@ -1,4 +1,5 @@ -import type { GlobalSettings, SetupScriptLaunchMode } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { SetupScriptLaunchMode } from '../../../../shared/worktree/launch-types' import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group' import { SettingsRow, SettingsSubsectionHeader } from './SettingsFormControls' import { SearchableSetting } from './SearchableSetting' diff --git a/src/renderer/src/components/settings/TerminalThemeSections.lifecycle.test.ts b/src/renderer/src/components/settings/TerminalThemeSections.lifecycle.test.ts index 0114ea3ebf4..39cd8763001 100644 --- a/src/renderer/src/components/settings/TerminalThemeSections.lifecycle.test.ts +++ b/src/renderer/src/components/settings/TerminalThemeSections.lifecycle.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { UseWarpThemeImportReturn } from './useWarpThemeImport' let themeTarget: 'dark' | 'light' | undefined = 'dark' diff --git a/src/renderer/src/components/settings/TerminalThemeSections.tsx b/src/renderer/src/components/settings/TerminalThemeSections.tsx index 1fa765bb271..133801aa497 100644 --- a/src/renderer/src/components/settings/TerminalThemeSections.tsx +++ b/src/renderer/src/components/settings/TerminalThemeSections.tsx @@ -1,5 +1,5 @@ import { useState, type Dispatch, type ReactNode, type SetStateAction } from 'react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { ColorField, SettingsSegmentedControl, diff --git a/src/renderer/src/components/settings/TerminalWindowSection.tsx b/src/renderer/src/components/settings/TerminalWindowSection.tsx index 0c38f5c00b6..863d7fa11b0 100644 --- a/src/renderer/src/components/settings/TerminalWindowSection.tsx +++ b/src/renderer/src/components/settings/TerminalWindowSection.tsx @@ -1,6 +1,6 @@ import { useRef, useState } from 'react' import { RotateCw } from 'lucide-react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { Button } from '../ui/button' import { Label } from '../ui/label' import { Switch } from '../ui/switch' diff --git a/src/renderer/src/components/settings/TerminalWindowsShellSection.tsx b/src/renderer/src/components/settings/TerminalWindowsShellSection.tsx index 8a191087092..6009e551653 100644 --- a/src/renderer/src/components/settings/TerminalWindowsShellSection.tsx +++ b/src/renderer/src/components/settings/TerminalWindowsShellSection.tsx @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { WINDOWS_GIT_BASH_SHELL } from '../../../../shared/windows-terminal-shell' import { SettingsRow, diff --git a/src/renderer/src/components/settings/VoicePane.test.tsx b/src/renderer/src/components/settings/VoicePane.test.tsx index 84f6bbd5393..1cf41781125 100644 --- a/src/renderer/src/components/settings/VoicePane.test.tsx +++ b/src/renderer/src/components/settings/VoicePane.test.tsx @@ -5,7 +5,7 @@ import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { DeveloperPermissionRequestResult } from '../../../../shared/developer-permissions-types' import type { SpeechModelManifest } from '../../../../shared/speech-types' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { getDefaultVoiceSettings } from '../../../../shared/constants' import { handleVoiceDictationToggle, VoicePane } from './VoicePane' diff --git a/src/renderer/src/components/settings/VoicePane.tsx b/src/renderer/src/components/settings/VoicePane.tsx index f6729301702..7d4d938325f 100644 --- a/src/renderer/src/components/settings/VoicePane.tsx +++ b/src/renderer/src/components/settings/VoicePane.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { getDefaultVoiceSettings } from '../../../../shared/constants' import type { SpeechModelManifest, VoiceSettings } from '../../../../shared/speech-types' import { Separator } from '../ui/separator' diff --git a/src/renderer/src/components/settings/WorkspaceDirectorySetting.test.tsx b/src/renderer/src/components/settings/WorkspaceDirectorySetting.test.tsx index d55a2336af7..26b86e50373 100644 --- a/src/renderer/src/components/settings/WorkspaceDirectorySetting.test.tsx +++ b/src/renderer/src/components/settings/WorkspaceDirectorySetting.test.tsx @@ -4,7 +4,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../../shared/constants' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { WorkspaceDirectorySetting } from './WorkspaceDirectorySetting' vi.mock('../sidebar/use-sidebar-host-scope-options', () => ({ diff --git a/src/renderer/src/components/settings/WorkspaceDirectorySetting.tsx b/src/renderer/src/components/settings/WorkspaceDirectorySetting.tsx index 8633b735a96..6ca5e985170 100644 --- a/src/renderer/src/components/settings/WorkspaceDirectorySetting.tsx +++ b/src/renderer/src/components/settings/WorkspaceDirectorySetting.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useId, useRef, useState } from 'react' import { FolderOpen, RotateCcw } from 'lucide-react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { getEffectiveHostSetting, getHostSettingOverride, diff --git a/src/renderer/src/components/settings/WorktreeSymlinksSection.tsx b/src/renderer/src/components/settings/WorktreeSymlinksSection.tsx index 33a918a9cdf..02f90518df3 100644 --- a/src/renderer/src/components/settings/WorktreeSymlinksSection.tsx +++ b/src/renderer/src/components/settings/WorktreeSymlinksSection.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react' import { Folder, Link2, Plus, X } from 'lucide-react' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { Button } from '../ui/button' import { Command, CommandEmpty, CommandInput, CommandItem, CommandList } from '../ui/command' import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover' diff --git a/src/renderer/src/components/settings/appearance-interface-summary.ts b/src/renderer/src/components/settings/appearance-interface-summary.ts index e37a4f2aa66..ee2497be488 100644 --- a/src/renderer/src/components/settings/appearance-interface-summary.ts +++ b/src/renderer/src/components/settings/appearance-interface-summary.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { getUiLanguageChoiceLabel, SHOW_UI_LANGUAGE_SETTING, diff --git a/src/renderer/src/components/settings/appearance-status-bar-antigravity-toggle-search.ts b/src/renderer/src/components/settings/appearance-status-bar-antigravity-toggle-search.ts index 68c6513bb72..0b0474e85e7 100644 --- a/src/renderer/src/components/settings/appearance-status-bar-antigravity-toggle-search.ts +++ b/src/renderer/src/components/settings/appearance-status-bar-antigravity-toggle-search.ts @@ -1,4 +1,4 @@ -import type { StatusBarItem } from '../../../../shared/types' +import type { StatusBarItem } from '../../../../shared/ui-chrome-types' import { translate } from '@/i18n/i18n' import { translateSearchKeyword } from './settings-search-keywords' diff --git a/src/renderer/src/components/settings/appearance-status-bar-grok-toggle-search.ts b/src/renderer/src/components/settings/appearance-status-bar-grok-toggle-search.ts index 3ff4295238e..f2709bbe8a7 100644 --- a/src/renderer/src/components/settings/appearance-status-bar-grok-toggle-search.ts +++ b/src/renderer/src/components/settings/appearance-status-bar-grok-toggle-search.ts @@ -1,4 +1,4 @@ -import type { StatusBarItem } from '../../../../shared/types' +import type { StatusBarItem } from '../../../../shared/ui-chrome-types' import { translate } from '@/i18n/i18n' import { translateSearchKeyword } from './settings-search-keywords' diff --git a/src/renderer/src/components/settings/appearance-status-bar-search.ts b/src/renderer/src/components/settings/appearance-status-bar-search.ts index 02403c24f4b..9be73da6a91 100644 --- a/src/renderer/src/components/settings/appearance-status-bar-search.ts +++ b/src/renderer/src/components/settings/appearance-status-bar-search.ts @@ -1,4 +1,4 @@ -import type { StatusBarItem } from '../../../../shared/types' +import type { StatusBarItem } from '../../../../shared/ui-chrome-types' import { createLocalizedCatalog } from '@/i18n/localized-catalog' import { translate } from '@/i18n/i18n' import { translateSearchKeyword } from './settings-search-keywords' diff --git a/src/renderer/src/components/settings/codex-account-auth-warning.ts b/src/renderer/src/components/settings/codex-account-auth-warning.ts index 9db1e3ae8e5..85fce048857 100644 --- a/src/renderer/src/components/settings/codex-account-auth-warning.ts +++ b/src/renderer/src/components/settings/codex-account-auth-warning.ts @@ -2,7 +2,7 @@ import type { ProviderRateLimits, RateLimitRuntimeTarget } from '../../../../shared/rate-limit-types' -import type { CodexSystemDefaultIdentity } from '../../../../shared/types' +import type { CodexSystemDefaultIdentity } from '../../../../shared/managed-account-types' import { isCodexAuthError } from '../../../../shared/codex-auth-errors' type AccountRuntime = { diff --git a/src/renderer/src/components/settings/codex-session-source-home-control.test.tsx b/src/renderer/src/components/settings/codex-session-source-home-control.test.tsx index 731398c3d0b..fdf27134754 100644 --- a/src/renderer/src/components/settings/codex-session-source-home-control.test.tsx +++ b/src/renderer/src/components/settings/codex-session-source-home-control.test.tsx @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { buildCodexSessionSourceHomeControl } from './codex-session-source-home-control' function settingsWith( diff --git a/src/renderer/src/components/settings/codex-session-source-home-control.tsx b/src/renderer/src/components/settings/codex-session-source-home-control.tsx index 6a49a7b6042..b459d9ffb9d 100644 --- a/src/renderer/src/components/settings/codex-session-source-home-control.tsx +++ b/src/renderer/src/components/settings/codex-session-source-home-control.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { Info } from 'lucide-react' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { normalizeGlobalWindowsRuntimeDefault } from '../../../../shared/project-execution-runtime' import { Button } from '../ui/button' import { Input } from '../ui/input' diff --git a/src/renderer/src/components/settings/notification-settings-copy.ts b/src/renderer/src/components/settings/notification-settings-copy.ts index f2d06aeab16..266e2183d75 100644 --- a/src/renderer/src/components/settings/notification-settings-copy.ts +++ b/src/renderer/src/components/settings/notification-settings-copy.ts @@ -1,5 +1,5 @@ import { toast } from 'sonner' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' type SystemNotificationSettingsCopy = { diff --git a/src/renderer/src/components/settings/provider-account-scope.ts b/src/renderer/src/components/settings/provider-account-scope.ts index 5687de9adf8..f6745b3ed9c 100644 --- a/src/renderer/src/components/settings/provider-account-scope.ts +++ b/src/renderer/src/components/settings/provider-account-scope.ts @@ -1,6 +1,6 @@ import { translate } from '@/i18n/i18n' import { getLocalExecutionHostLabel } from '../../../../shared/execution-host' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' export type ProviderAccountScope = { label: string diff --git a/src/renderer/src/components/settings/provider-account-visibility.test.ts b/src/renderer/src/components/settings/provider-account-visibility.test.ts index 7c18062c5de..551584779ca 100644 --- a/src/renderer/src/components/settings/provider-account-visibility.test.ts +++ b/src/renderer/src/components/settings/provider-account-visibility.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import type { ClaudeRateLimitAccountsState, CodexRateLimitAccountsState -} from '../../../../shared/types' +} from '../../../../shared/managed-account-types' import { getProviderAccountActiveIdForView, getProviderAccountRuntime, diff --git a/src/renderer/src/components/settings/provider-account-visibility.ts b/src/renderer/src/components/settings/provider-account-visibility.ts index a572d2f4ab2..cb6ac050814 100644 --- a/src/renderer/src/components/settings/provider-account-visibility.ts +++ b/src/renderer/src/components/settings/provider-account-visibility.ts @@ -3,7 +3,7 @@ import type { ClaudeRateLimitAccountsState, CodexManagedAccountRuntimeSelection, CodexRateLimitAccountsState -} from '../../../../shared/types' +} from '../../../../shared/managed-account-types' type ProviderAccount = | ClaudeRateLimitAccountsState['accounts'][number] diff --git a/src/renderer/src/components/settings/repository-git-author-search-entries.ts b/src/renderer/src/components/settings/repository-git-author-search-entries.ts index eb202d55209..e18437c05cc 100644 --- a/src/renderer/src/components/settings/repository-git-author-search-entries.ts +++ b/src/renderer/src/components/settings/repository-git-author-search-entries.ts @@ -1,4 +1,4 @@ -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import type { SettingsSearchEntry } from './settings-search' import { translate } from '@/i18n/i18n' import { translateSearchKeyword } from './settings-search-keywords' diff --git a/src/renderer/src/components/settings/repository-git-hooks-search-entries.ts b/src/renderer/src/components/settings/repository-git-hooks-search-entries.ts index a5323265f5b..281622b66db 100644 --- a/src/renderer/src/components/settings/repository-git-hooks-search-entries.ts +++ b/src/renderer/src/components/settings/repository-git-hooks-search-entries.ts @@ -1,4 +1,4 @@ -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import type { SettingsSearchEntry } from './settings-search' import { translate } from '@/i18n/i18n' import { translateSearchKeyword } from './settings-search-keywords' diff --git a/src/renderer/src/components/settings/repository-git-worktree-search-entries.ts b/src/renderer/src/components/settings/repository-git-worktree-search-entries.ts index 2aacea52b61..46d6e3ff529 100644 --- a/src/renderer/src/components/settings/repository-git-worktree-search-entries.ts +++ b/src/renderer/src/components/settings/repository-git-worktree-search-entries.ts @@ -1,4 +1,4 @@ -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import type { SettingsSearchEntry } from './settings-search' import { translate } from '@/i18n/i18n' import { translateSearchKeyword } from './settings-search-keywords' diff --git a/src/renderer/src/components/settings/repository-host-setup-options.ts b/src/renderer/src/components/settings/repository-host-setup-options.ts index c66d8fd1442..987bcd80f53 100644 --- a/src/renderer/src/components/settings/repository-host-setup-options.ts +++ b/src/renderer/src/components/settings/repository-host-setup-options.ts @@ -4,7 +4,7 @@ import { PROJECT_HOST_SETUP_RUNTIME_CAPABILITY, WORKSPACE_RUN_CONTEXT_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version' -import type { ProjectHostSetup, ProjectHostSetupState } from '../../../../shared/types' +import type { ProjectHostSetup, ProjectHostSetupState } from '../../../../shared/project-types' import { translate } from '@/i18n/i18n' export type SetupHostOption = { diff --git a/src/renderer/src/components/settings/repository-icon-github.test.ts b/src/renderer/src/components/settings/repository-icon-github.test.ts index 2926d0fd2b4..3abb42ae89d 100644 --- a/src/renderer/src/components/settings/repository-icon-github.test.ts +++ b/src/renderer/src/components/settings/repository-icon-github.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { githubAvatarIcon } from '../../../../shared/repo-icon' import { buildRepositoryGitHubAvatarUpdate, diff --git a/src/renderer/src/components/settings/repository-icon-github.ts b/src/renderer/src/components/settings/repository-icon-github.ts index 0a44a88f030..f3dbee71fe7 100644 --- a/src/renderer/src/components/settings/repository-icon-github.ts +++ b/src/renderer/src/components/settings/repository-icon-github.ts @@ -1,4 +1,5 @@ -import type { GitHubRepositoryIdentity, Repo } from '../../../../shared/types' +import type { GitHubRepositoryIdentity } from '../../../../shared/github/pull-request-types' +import type { Repo } from '../../../../shared/repo-types' import { githubAvatarIcon, githubAvatarSlug, type RepoIcon } from '../../../../shared/repo-icon' import { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key' import { callRuntimeRpc, type getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' diff --git a/src/renderer/src/components/settings/repository-identity-search.ts b/src/renderer/src/components/settings/repository-identity-search.ts index 239090948ec..0be209f66d1 100644 --- a/src/renderer/src/components/settings/repository-identity-search.ts +++ b/src/renderer/src/components/settings/repository-identity-search.ts @@ -1,4 +1,4 @@ -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { normalizeSettingsSearchQuery } from './settings-search' export function matchesRepositoryIdentitySearch(query: string, repo: Repo): boolean { diff --git a/src/renderer/src/components/settings/repository-search.ts b/src/renderer/src/components/settings/repository-search.ts index ea878ee2b8e..c9d2a91682f 100644 --- a/src/renderer/src/components/settings/repository-search.ts +++ b/src/renderer/src/components/settings/repository-search.ts @@ -1,4 +1,4 @@ -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { isFolderRepo } from '../../../../shared/repo-kind' import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' import type { SettingsSearchEntry } from './settings-search' diff --git a/src/renderer/src/components/settings/repository-source-control-ai-draft.ts b/src/renderer/src/components/settings/repository-source-control-ai-draft.ts index 370f60f3d2f..079b3d2cb43 100644 --- a/src/renderer/src/components/settings/repository-source-control-ai-draft.ts +++ b/src/renderer/src/components/settings/repository-source-control-ai-draft.ts @@ -7,7 +7,7 @@ import { type SourceControlActionId } from '../../../../shared/source-control-ai-actions' import type { RepoSourceControlAiOverrides } from '../../../../shared/source-control-ai-types' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { completeRepoActionRecipe } from './repository-source-control-ai-labels' import { SOURCE_CONTROL_TEXT_ACTION_ID_SET } from './source-control-action-recipe-options' diff --git a/src/renderer/src/components/settings/repository-source-control-ai-global-ux.ts b/src/renderer/src/components/settings/repository-source-control-ai-global-ux.ts index 9cce9bc30fe..a27db245e6d 100644 --- a/src/renderer/src/components/settings/repository-source-control-ai-global-ux.ts +++ b/src/renderer/src/components/settings/repository-source-control-ai-global-ux.ts @@ -1,5 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react' -import type { GlobalSettings, TuiAgent } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { CUSTOM_AGENT_ID } from '../../../../shared/commit-message-agent-spec' import type { RepoSourceControlAiOverrides, diff --git a/src/renderer/src/components/settings/repository-source-control-ai-labels.ts b/src/renderer/src/components/settings/repository-source-control-ai-labels.ts index 64f0a111080..afaa7c9e9eb 100644 --- a/src/renderer/src/components/settings/repository-source-control-ai-labels.ts +++ b/src/renderer/src/components/settings/repository-source-control-ai-labels.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { DEFAULT_SOURCE_CONTROL_ACTION_COMMAND_TEMPLATES, resolveSourceControlActionCommandTemplate, diff --git a/src/renderer/src/components/settings/setting-labels.ts b/src/renderer/src/components/settings/setting-labels.ts index 95eb3a26c7c..c2cec96322f 100644 --- a/src/renderer/src/components/settings/setting-labels.ts +++ b/src/renderer/src/components/settings/setting-labels.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' export const SETTING_LABELS: Partial> = { terminalFontSize: 'Font Size', diff --git a/src/renderer/src/components/settings/settings-project-list.test.ts b/src/renderer/src/components/settings/settings-project-list.test.ts index 41f82a095bc..2b60d077ef3 100644 --- a/src/renderer/src/components/settings/settings-project-list.test.ts +++ b/src/renderer/src/components/settings/settings-project-list.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest' -import type { ProjectHostSetup, Repo } from '../../../../shared/types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { buildRepoIdToHostSelection, buildRepoIdToRepresentative, diff --git a/src/renderer/src/components/settings/settings-project-list.ts b/src/renderer/src/components/settings/settings-project-list.ts index e963c034822..14de9f8d848 100644 --- a/src/renderer/src/components/settings/settings-project-list.ts +++ b/src/renderer/src/components/settings/settings-project-list.ts @@ -1,4 +1,5 @@ -import type { Project, ProjectHostSetup, Repo } from '../../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID, diff --git a/src/renderer/src/components/settings/shortcut-definition-catalog.ts b/src/renderer/src/components/settings/shortcut-definition-catalog.ts index 882f74ab7fc..758fcbb2202 100644 --- a/src/renderer/src/components/settings/shortcut-definition-catalog.ts +++ b/src/renderer/src/components/settings/shortcut-definition-catalog.ts @@ -5,7 +5,7 @@ import { type KeybindingDefinition, type KeybindingOverrides } from '../../../../shared/keybindings' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { ActivePluginCommand } from '@/store/plugin-panels' import { buildPluginCommandKeybindingDefinitions } from '@/lib/plugin-command-keybindings' import { disabledAgentTabActionIds, groupDefinitions, type ShortcutGroup } from './shortcut-groups' diff --git a/src/renderer/src/components/settings/shortcut-groups.ts b/src/renderer/src/components/settings/shortcut-groups.ts index 054a95baf27..495dbbfdd41 100644 --- a/src/renderer/src/components/settings/shortcut-groups.ts +++ b/src/renderer/src/components/settings/shortcut-groups.ts @@ -5,7 +5,7 @@ import { type KeybindingDefinition } from '../../../../shared/keybindings' import { normalizeDisabledTuiAgents } from '../../../../shared/tui-agent-selection' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' export type ShortcutGroup = { title: string diff --git a/src/renderer/src/components/settings/source-control-action-recipe-options.ts b/src/renderer/src/components/settings/source-control-action-recipe-options.ts index 1f109d5ed75..dfeaeda128b 100644 --- a/src/renderer/src/components/settings/source-control-action-recipe-options.ts +++ b/src/renderer/src/components/settings/source-control-action-recipe-options.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { SOURCE_CONTROL_TEXT_ACTION_IDS, type SourceControlActionId diff --git a/src/renderer/src/components/settings/sparse-preset-settings-row.tsx b/src/renderer/src/components/settings/sparse-preset-settings-row.tsx index 44cc9775a06..3c0fa0c4a0c 100644 --- a/src/renderer/src/components/settings/sparse-preset-settings-row.tsx +++ b/src/renderer/src/components/settings/sparse-preset-settings-row.tsx @@ -1,5 +1,5 @@ import { Bookmark, LoaderCircle, Pencil, Trash2 } from 'lucide-react' -import type { SparsePreset } from '../../../../shared/types' +import type { SparsePreset } from '../../../../shared/worktree/create-types' import { cn } from '@/lib/utils' import { Button } from '../ui/button' import { formatSparsePresetUpdatedAt } from './sparse-preset-date' diff --git a/src/renderer/src/components/settings/task-source-setup-state.test.ts b/src/renderer/src/components/settings/task-source-setup-state.test.ts index d2e4e6a5c27..6eec46a34db 100644 --- a/src/renderer/src/components/settings/task-source-setup-state.test.ts +++ b/src/renderer/src/components/settings/task-source-setup-state.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TaskProvider } from '../../../../shared/types' +import type { TaskProvider } from '../../../../shared/task-providers' import { getAutoExpandedTaskProvider, getIncompleteVisibleTaskProviders, diff --git a/src/renderer/src/components/settings/task-source-setup-state.ts b/src/renderer/src/components/settings/task-source-setup-state.ts index 3ad27d48fa9..628de69dbf0 100644 --- a/src/renderer/src/components/settings/task-source-setup-state.ts +++ b/src/renderer/src/components/settings/task-source-setup-state.ts @@ -1,4 +1,4 @@ -import type { TaskProvider } from '../../../../shared/types' +import type { TaskProvider } from '../../../../shared/task-providers' import type { IntegrationStatusTone } from '@/components/integration-status-pill' export type TaskProviderReadiness = { diff --git a/src/renderer/src/components/settings/terminal-window-color-groups.tsx b/src/renderer/src/components/settings/terminal-window-color-groups.tsx index 652d9ffc04c..41021681b06 100644 --- a/src/renderer/src/components/settings/terminal-window-color-groups.tsx +++ b/src/renderer/src/components/settings/terminal-window-color-groups.tsx @@ -1,4 +1,4 @@ -import type { TerminalColorOverrides } from '../../../../shared/types' +import type { TerminalColorOverrides } from '../../../../shared/terminal-color-overrides' import { translate } from '@/i18n/i18n' export const COLOR_OVERRIDE_GROUPS: { diff --git a/src/renderer/src/components/settings/use-task-source-provider-readiness.test.tsx b/src/renderer/src/components/settings/use-task-source-provider-readiness.test.tsx index cb2cb636692..d00a358996f 100644 --- a/src/renderer/src/components/settings/use-task-source-provider-readiness.test.tsx +++ b/src/renderer/src/components/settings/use-task-source-provider-readiness.test.tsx @@ -3,7 +3,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { TaskProvider } from '../../../../shared/types' +import type { TaskProvider } from '../../../../shared/task-providers' import type { TaskProviderReadiness } from './task-source-setup-state' import { useTaskSourceProviderReadiness } from './use-task-source-provider-readiness' diff --git a/src/renderer/src/components/settings/use-task-source-provider-readiness.ts b/src/renderer/src/components/settings/use-task-source-provider-readiness.ts index 97db089a677..6b9fa6171ba 100644 --- a/src/renderer/src/components/settings/use-task-source-provider-readiness.ts +++ b/src/renderer/src/components/settings/use-task-source-provider-readiness.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import type { TaskProvider } from '../../../../shared/types' +import type { TaskProvider } from '../../../../shared/task-providers' import { GLOBAL_AGENT_SKILL_SOURCE_KINDS, useInstalledAgentSkillNames diff --git a/src/renderer/src/components/settings/useGhosttyImport.test.ts b/src/renderer/src/components/settings/useGhosttyImport.test.ts index 26326b99286..ee3cb6560b6 100644 --- a/src/renderer/src/components/settings/useGhosttyImport.test.ts +++ b/src/renderer/src/components/settings/useGhosttyImport.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { GhosttyImportPreview, GlobalSettings } from '../../../../shared/types' +import type { GhosttyImportPreview, GlobalSettings } from '../../../../shared/global-settings-types' const mockStateValues: unknown[] = [] let mockStateIndex = 0 diff --git a/src/renderer/src/components/settings/useGhosttyImport.ts b/src/renderer/src/components/settings/useGhosttyImport.ts index 8031008d21a..39e25f9d044 100644 --- a/src/renderer/src/components/settings/useGhosttyImport.ts +++ b/src/renderer/src/components/settings/useGhosttyImport.ts @@ -1,5 +1,5 @@ import { useState } from 'react' -import type { GhosttyImportPreview, GlobalSettings } from '../../../../shared/types' +import type { GhosttyImportPreview, GlobalSettings } from '../../../../shared/global-settings-types' import { useMountedRef } from '../../hooks/useMountedRef' export type UseGhosttyImportReturn = { diff --git a/src/renderer/src/components/settings/useWarpThemeImport.test.ts b/src/renderer/src/components/settings/useWarpThemeImport.test.ts index cc0a31288dd..c76850cfbda 100644 --- a/src/renderer/src/components/settings/useWarpThemeImport.test.ts +++ b/src/renderer/src/components/settings/useWarpThemeImport.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { MAX_TERMINAL_CUSTOM_THEMES, type WarpThemeImportPreview diff --git a/src/renderer/src/components/settings/useWarpThemeImport.ts b/src/renderer/src/components/settings/useWarpThemeImport.ts index 615df1a0f54..94eadcda7ba 100644 --- a/src/renderer/src/components/settings/useWarpThemeImport.ts +++ b/src/renderer/src/components/settings/useWarpThemeImport.ts @@ -1,6 +1,6 @@ import { useState } from 'react' import { toast } from 'sonner' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { MAX_TERMINAL_CUSTOM_THEMES, normalizeTerminalCustomThemes, diff --git a/src/renderer/src/components/setup-guide/setup-guide-progress-readiness.ts b/src/renderer/src/components/setup-guide/setup-guide-progress-readiness.ts index f74ffeaa612..629e9ff0a23 100644 --- a/src/renderer/src/components/setup-guide/setup-guide-progress-readiness.ts +++ b/src/renderer/src/components/setup-guide/setup-guide-progress-readiness.ts @@ -1,6 +1,7 @@ import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' import type { ComputerUsePermissionStatusResult } from '../../../../shared/computer-use-permissions-types' -import type { GlobalSettings, Repo } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' export type SetupScriptProbeState = { signature: string | null diff --git a/src/renderer/src/components/sidebar/AddProjectFromFolderDialog.test.tsx b/src/renderer/src/components/sidebar/AddProjectFromFolderDialog.test.tsx index 7310d450a69..cb793b2bab6 100644 --- a/src/renderer/src/components/sidebar/AddProjectFromFolderDialog.test.tsx +++ b/src/renderer/src/components/sidebar/AddProjectFromFolderDialog.test.tsx @@ -1,7 +1,7 @@ import { renderToStaticMarkup } from 'react-dom/server' import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' type ButtonCapture = { label: string diff --git a/src/renderer/src/components/sidebar/AddProjectFromFolderDialog.tsx b/src/renderer/src/components/sidebar/AddProjectFromFolderDialog.tsx index f3ef3b43c22..a7873acf0fc 100644 --- a/src/renderer/src/components/sidebar/AddProjectFromFolderDialog.tsx +++ b/src/renderer/src/components/sidebar/AddProjectFromFolderDialog.tsx @@ -12,7 +12,7 @@ import { import { Button } from '@/components/ui/button' import { useMountedRef } from '@/hooks/useMountedRef' import { useAppStore } from '@/store' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { isGitRepoKind } from '../../../../shared/repo-kind' import { finishProjectAddWithDefaultCheckout } from './project-added-default-checkout' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx b/src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx index abdd169d4fd..4e3bcdc17fd 100644 --- a/src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx +++ b/src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx @@ -4,7 +4,7 @@ import type { ComponentProps } from 'react' import { Dialog } from '@/components/ui/dialog' import { TooltipProvider } from '@/components/ui/tooltip' import { AddRepoDialogStepContent } from './AddRepoDialogStepContent' -import type { NestedRepoScanResult } from '../../../../shared/types' +import type { NestedRepoScanResult } from '../../../../shared/project-group-types' const nestedScan: NestedRepoScanResult = { selectedPath: '/workspace/platform', diff --git a/src/renderer/src/components/sidebar/AddRepoDialogStepContent.tsx b/src/renderer/src/components/sidebar/AddRepoDialogStepContent.tsx index 9a0da649f8c..5e405d7c134 100644 --- a/src/renderer/src/components/sidebar/AddRepoDialogStepContent.tsx +++ b/src/renderer/src/components/sidebar/AddRepoDialogStepContent.tsx @@ -6,7 +6,7 @@ import { AddRepoLocalStartStep } from './AddRepoStartSteps' import { AddRepoServerPathStartStep } from './AddRepoServerStartStep' import { AddRepoNestedImportStep } from './AddRepoNestedImportStep' import type { AddRepoDialogStep } from './add-repo-dialog-types' -import type { NestedRepoScanResult } from '../../../../shared/types' +import type { NestedRepoScanResult } from '../../../../shared/project-group-types' import type { SshConnectionState, SshTarget } from '../../../../shared/ssh-types' import type { GitAvailability } from './create-project-defaults' diff --git a/src/renderer/src/components/sidebar/AddRepoNestedImportStep.test.tsx b/src/renderer/src/components/sidebar/AddRepoNestedImportStep.test.tsx index ff243a3a5cc..437f9e4d875 100644 --- a/src/renderer/src/components/sidebar/AddRepoNestedImportStep.test.tsx +++ b/src/renderer/src/components/sidebar/AddRepoNestedImportStep.test.tsx @@ -7,7 +7,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { AddRepoNestedImportStep } from './AddRepoNestedImportStep' import { TooltipProvider } from '@/components/ui/tooltip' import { Dialog } from '@/components/ui/dialog' -import type { NestedRepoScanResult } from '../../../../shared/types' +import type { NestedRepoScanResult } from '../../../../shared/project-group-types' const scan: NestedRepoScanResult = { selectedPath: '/workspace/platform', diff --git a/src/renderer/src/components/sidebar/AddRepoNestedImportStep.tsx b/src/renderer/src/components/sidebar/AddRepoNestedImportStep.tsx index ef41edbf1a1..6366214bb66 100644 --- a/src/renderer/src/components/sidebar/AddRepoNestedImportStep.tsx +++ b/src/renderer/src/components/sidebar/AddRepoNestedImportStep.tsx @@ -6,7 +6,7 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { NestedRepoChecklist } from '@/components/repo/NestedRepoChecklist' -import type { NestedRepoScanResult } from '../../../../shared/types' +import type { NestedRepoScanResult } from '../../../../shared/project-group-types' import { NestedRepoScanLimitNotice } from '../repo/NestedRepoScanLimitNotice' import { getRuntimePathBasename } from '../../../../shared/cross-platform-path' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/sidebar/AddRepoSteps.default-checkout.test.ts b/src/renderer/src/components/sidebar/AddRepoSteps.default-checkout.test.ts index fc8829474ee..6ca5d28f16f 100644 --- a/src/renderer/src/components/sidebar/AddRepoSteps.default-checkout.test.ts +++ b/src/renderer/src/components/sidebar/AddRepoSteps.default-checkout.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as ReactModule from 'react' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' const mocks = vi.hoisted(() => ({ stateValues: [] as unknown[], diff --git a/src/renderer/src/components/sidebar/AddRepoSteps.tsx b/src/renderer/src/components/sidebar/AddRepoSteps.tsx index 0acdaa888b1..248bcd87058 100644 --- a/src/renderer/src/components/sidebar/AddRepoSteps.tsx +++ b/src/renderer/src/components/sidebar/AddRepoSteps.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { toast } from 'sonner' import { useAppStore } from '@/store' import { useMountedRef } from '@/hooks/useMountedRef' -import type { NestedRepoScanResult } from '../../../../shared/types' +import type { NestedRepoScanResult } from '../../../../shared/project-group-types' import type { SshTarget, SshConnectionState } from '../../../../shared/ssh-types' import { createNestedRepoTelemetryAttemptId } from '../../../../shared/nested-repo-telemetry' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/sidebar/DeleteWorktreeDialog.test.tsx b/src/renderer/src/components/sidebar/DeleteWorktreeDialog.test.tsx index 65700ef574b..a36eca15297 100644 --- a/src/renderer/src/components/sidebar/DeleteWorktreeDialog.test.tsx +++ b/src/renderer/src/components/sidebar/DeleteWorktreeDialog.test.tsx @@ -1,7 +1,9 @@ import { renderToStaticMarkup } from 'react-dom/server' import type { ButtonHTMLAttributes, ReactNode } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo, Worktree, WorktreeLineage } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' const mocks = vi.hoisted(() => { const state = { diff --git a/src/renderer/src/components/sidebar/DeleteWorktreeLineageNotice.tsx b/src/renderer/src/components/sidebar/DeleteWorktreeLineageNotice.tsx index 427b96308cc..8e442f27d39 100644 --- a/src/renderer/src/components/sidebar/DeleteWorktreeLineageNotice.tsx +++ b/src/renderer/src/components/sidebar/DeleteWorktreeLineageNotice.tsx @@ -1,6 +1,6 @@ import { Workflow } from 'lucide-react' import type { JSX } from 'react' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { DeleteWorktreeDirtyChangeHint } from './DeleteWorktreeDirtyChangeHint' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/sidebar/DeleteWorktreeTargetPreview.tsx b/src/renderer/src/components/sidebar/DeleteWorktreeTargetPreview.tsx index e6165e8c437..9f557af4324 100644 --- a/src/renderer/src/components/sidebar/DeleteWorktreeTargetPreview.tsx +++ b/src/renderer/src/components/sidebar/DeleteWorktreeTargetPreview.tsx @@ -1,7 +1,7 @@ import type { JSX } from 'react' import { LoaderCircle } from 'lucide-react' import { ScrollArea } from '@/components/ui/scroll-area' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { DeleteWorktreeDirtyChangeHint } from './DeleteWorktreeDirtyChangeHint' type DeleteState = { diff --git a/src/renderer/src/components/sidebar/HiddenWorktreeRecoveryList.tsx b/src/renderer/src/components/sidebar/HiddenWorktreeRecoveryList.tsx index 8f842bee693..567428702b8 100644 --- a/src/renderer/src/components/sidebar/HiddenWorktreeRecoveryList.tsx +++ b/src/renderer/src/components/sidebar/HiddenWorktreeRecoveryList.tsx @@ -4,7 +4,8 @@ import { useVirtualizer } from '@tanstack/react-virtual' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { translate } from '@/i18n/i18n' -import type { DetectedWorktreeListResult, Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { DetectedWorktreeListResult } from '../../../../shared/worktree/types' import { getHiddenImportableExternalWorktrees, getVisibleNonOrcaWorktrees diff --git a/src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx b/src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx index a392374762d..5b78c067d46 100644 --- a/src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx +++ b/src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx @@ -1,7 +1,8 @@ import { renderToStaticMarkup } from 'react-dom/server' import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' type ButtonCapture = { label: string diff --git a/src/renderer/src/components/sidebar/ProjectAddedDialog.test.tsx b/src/renderer/src/components/sidebar/ProjectAddedDialog.test.tsx index f3580472439..8a9651aa6a8 100644 --- a/src/renderer/src/components/sidebar/ProjectAddedDialog.test.tsx +++ b/src/renderer/src/components/sidebar/ProjectAddedDialog.test.tsx @@ -1,7 +1,7 @@ import { renderToStaticMarkup } from 'react-dom/server' import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' const mocks = vi.hoisted(() => ({ state: { diff --git a/src/renderer/src/components/sidebar/RemoteFileBrowser.tsx b/src/renderer/src/components/sidebar/RemoteFileBrowser.tsx index dc43f2bb8fe..9ec07ccf651 100644 --- a/src/renderer/src/components/sidebar/RemoteFileBrowser.tsx +++ b/src/renderer/src/components/sidebar/RemoteFileBrowser.tsx @@ -20,7 +20,7 @@ import { import { driveBreadcrumbPath, splitBrowsePath } from './remote-file-browser-drive-paths' import { browseRuntimeServerDirectory } from '@/runtime/runtime-server-directory-browser' import { translate } from '@/i18n/i18n' -import type { FilesystemPathFlavor } from '../../../../shared/types' +import type { FilesystemPathFlavor } from '../../../../shared/filesystem-entry-types' type RemoteFileBrowserProps = ( | { targetId: string; runtimeEnvironmentId?: never } diff --git a/src/renderer/src/components/sidebar/SetupScriptPromptCard.test.ts b/src/renderer/src/components/sidebar/SetupScriptPromptCard.test.ts index aa3dc5bc07d..18a1941298c 100644 --- a/src/renderer/src/components/sidebar/SetupScriptPromptCard.test.ts +++ b/src/renderer/src/components/sidebar/SetupScriptPromptCard.test.ts @@ -6,7 +6,7 @@ import { } from './setup-script-prompt-render-state' import type { SetupScriptPromptInspection } from '@/lib/setup-script-prompt' import { getRepoHostIdentityForParts } from '@/store/slices/repo-host-identity' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' function repoIdentity(repoId: string, hostId: string): string { return getRepoHostIdentityForParts(repoId, hostId) diff --git a/src/renderer/src/components/sidebar/Sidebar.test.tsx b/src/renderer/src/components/sidebar/Sidebar.test.tsx index 6704020676b..098ed7f203c 100644 --- a/src/renderer/src/components/sidebar/Sidebar.test.tsx +++ b/src/renderer/src/components/sidebar/Sidebar.test.tsx @@ -6,7 +6,7 @@ import { tmpdir } from 'node:os' import { cleanup, render } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../../shared/constants' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' const mocks = vi.hoisted(() => ({ state: {} as Record, diff --git a/src/renderer/src/components/sidebar/SidebarFeedbackDialog.tsx b/src/renderer/src/components/sidebar/SidebarFeedbackDialog.tsx index 1bdd1050a68..f0d034edefe 100644 --- a/src/renderer/src/components/sidebar/SidebarFeedbackDialog.tsx +++ b/src/renderer/src/components/sidebar/SidebarFeedbackDialog.tsx @@ -13,7 +13,7 @@ import { } from '@/components/ui/dialog' import { useMountedRef } from '@/hooks/useMountedRef' import { cn } from '@/lib/utils' -import type { GitHubViewer } from '../../../../shared/types' +import type { GitHubViewer } from '../../../../shared/github/pull-request-types' import { translate } from '@/i18n/i18n' import { extractImageFilesFromDataTransfer, diff --git a/src/renderer/src/components/sidebar/SidebarHostScopeMenuSection.tsx b/src/renderer/src/components/sidebar/SidebarHostScopeMenuSection.tsx index b310ba988c6..9f3920fa615 100644 --- a/src/renderer/src/components/sidebar/SidebarHostScopeMenuSection.tsx +++ b/src/renderer/src/components/sidebar/SidebarHostScopeMenuSection.tsx @@ -6,7 +6,10 @@ import { DropdownMenuSubTrigger } from '@/components/ui/dropdown-menu' import { ALL_EXECUTION_HOSTS_SCOPE, type ExecutionHostId } from '../../../../shared/execution-host' -import type { VisibleWorkspaceHostIds, WorkspaceHostScope } from '../../../../shared/types' +import type { + VisibleWorkspaceHostIds, + WorkspaceHostScope +} from '../../../../shared/ui-chrome-types' import { getSidebarHostHealthLabel, type SidebarHostOption } from './sidebar-host-options' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/sidebar/SidebarNav.test.tsx b/src/renderer/src/components/sidebar/SidebarNav.test.tsx index d76022b77ac..87b51ee5b23 100644 --- a/src/renderer/src/components/sidebar/SidebarNav.test.tsx +++ b/src/renderer/src/components/sidebar/SidebarNav.test.tsx @@ -6,7 +6,8 @@ import { waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { TooltipProvider } from '@/components/ui/tooltip' import { getDefaultSettings } from '../../../../shared/constants' -import type { GlobalSettings, Repo } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' import { i18n } from '../../i18n/i18n' import { PSEUDO_LOCALIZATION_LOCALE } from '../../i18n/pseudo-localization' diff --git a/src/renderer/src/components/sidebar/SidebarNav.tsx b/src/renderer/src/components/sidebar/SidebarNav.tsx index 98333ba6287..45ea99e30f7 100644 --- a/src/renderer/src/components/sidebar/SidebarNav.tsx +++ b/src/renderer/src/components/sidebar/SidebarNav.tsx @@ -3,7 +3,7 @@ import { Bell, CalendarClock, EyeOff, Files, Search, Smartphone } from 'lucide-r import { useTranslation } from 'react-i18next' import { useAppStore } from '@/store' import { cn } from '@/lib/utils' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { useActivityUnreadCount } from '@/components/activity/useActivityUnreadCount' import { useShortcutKeyComboDetails } from '@/hooks/useShortcutLabel' import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo' diff --git a/src/renderer/src/components/sidebar/SidebarProjectFilterPanel.tsx b/src/renderer/src/components/sidebar/SidebarProjectFilterPanel.tsx index b9a705a9295..9057038b427 100644 --- a/src/renderer/src/components/sidebar/SidebarProjectFilterPanel.tsx +++ b/src/renderer/src/components/sidebar/SidebarProjectFilterPanel.tsx @@ -11,7 +11,7 @@ import { } from '@/components/ui/command' import RepoBadgeLabel from '@/components/repo/RepoBadgeLabel' import { searchRepos } from '@/lib/repo-search' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { translate } from '@/i18n/i18n' function projectCommandFilter(_value: string, search: string, keywords?: string[]): number { diff --git a/src/renderer/src/components/sidebar/SidebarRepositoryFilterSection.tsx b/src/renderer/src/components/sidebar/SidebarRepositoryFilterSection.tsx index 8aa62ac9f61..65623ee8983 100644 --- a/src/renderer/src/components/sidebar/SidebarRepositoryFilterSection.tsx +++ b/src/renderer/src/components/sidebar/SidebarRepositoryFilterSection.tsx @@ -6,7 +6,7 @@ import { DropdownMenuSubTrigger } from '@/components/ui/dropdown-menu' import { SidebarProjectFilterPanel } from './SidebarProjectFilterPanel' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { translate } from '@/i18n/i18n' function getProjectFilterVisibilityLabel({ diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanCard.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanCard.tsx index cd7b94dd725..aa515e40eff 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanCard.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanCard.tsx @@ -1,7 +1,8 @@ import React from 'react' import { Pin } from 'lucide-react' import { Badge } from '@/components/ui/badge' -import type { Repo, WorkspaceStatus, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorkspaceStatus, Worktree } from '../../../../shared/worktree/types' import WorktreeCard from './WorktreeCard' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.search.test.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.search.test.tsx index f1dd7234eaf..f7fcb4d4bc8 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.search.test.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.search.test.tsx @@ -5,7 +5,9 @@ import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { WORKTREE_PALETTE_QUERY_MAX_BYTES } from '@/lib/worktree-palette-query-bounds' import { useAppStore } from '@/store' -import type { Repo, Worktree, WorktreeMeta } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeMeta } from '../../../../shared/worktree/meta-types' +import type { Worktree } from '../../../../shared/worktree/types' import WorkspaceKanbanDrawer from './WorkspaceKanbanDrawer' import type { WorkspaceKanbanLaneView } from './workspace-kanban-search' diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.task-status-sync.test.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.task-status-sync.test.tsx index 25ae48868db..cf56ebe275f 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.task-status-sync.test.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.task-status-sync.test.tsx @@ -4,7 +4,8 @@ import React, { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAppStore } from '@/store' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import WorkspaceKanbanDrawer from './WorkspaceKanbanDrawer' import type * as WorkspaceBoardTaskStatusSync from './workspace-board-task-status-sync' diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx index b457c378a15..1158e844abd 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx @@ -49,7 +49,8 @@ import { shouldWriteManualOrderForGroupDrop, type WorktreeDragGroup } from './worktree-manual-order' -import type { WorkspaceStatus, Worktree, WorktreeMeta } from '../../../../shared/types' +import type { WorktreeMeta } from '../../../../shared/worktree/meta-types' +import type { WorkspaceStatus, Worktree } from '../../../../shared/worktree/types' import { makeWorkspaceStatusId } from '../../../../shared/workspace-statuses' import { STATUS_BAR_RESERVE_HEIGHT, WORKSPACE_TOP_CHROME_HEIGHT } from './workspace-chrome-metrics' import { useContextualTour } from '@/components/contextual-tours/use-contextual-tour' diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawerHeader.test.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawerHeader.test.tsx index ce59a22718b..fd0f12c45a2 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawerHeader.test.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawerHeader.test.tsx @@ -1,7 +1,7 @@ import React, { isValidElement } from 'react' import { describe, expect, it, vi } from 'vitest' import { SheetHeader, SheetTitle } from '@/components/ui/sheet' -import type { WorkspaceStatusDefinition } from '../../../../shared/types' +import type { WorkspaceStatusDefinition } from '../../../../shared/worktree/types' import SidebarFilter from './SidebarFilter' import WorkspaceKanbanDrawerHeader from './WorkspaceKanbanDrawerHeader' import WorkspaceKanbanSearchField from './WorkspaceKanbanSearchField' diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawerHeader.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawerHeader.tsx index 051a46057c7..0df7680333c 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawerHeader.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawerHeader.tsx @@ -2,7 +2,7 @@ import React from 'react' import { X } from 'lucide-react' import { Button } from '@/components/ui/button' import { SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet' -import type { WorkspaceStatusDefinition } from '../../../../shared/types' +import type { WorkspaceStatusDefinition } from '../../../../shared/worktree/types' import SidebarFilter from './SidebarFilter' import WorkspaceKanbanSearchField from './WorkspaceKanbanSearchField' import WorkspaceKanbanSettingsMenu from './WorkspaceKanbanSettingsMenu' diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanLaneCardList.test.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanLaneCardList.test.tsx index d5b9b5c4176..b1ccfcccd3e 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanLaneCardList.test.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanLaneCardList.test.tsx @@ -11,7 +11,8 @@ import { cleanup, render } from '@testing-library/react' import { createRef } from 'react' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' const WINDOW_START = 20 const WINDOW_END = 25 diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanLaneCardList.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanLaneCardList.tsx index 49b9232839b..6b3ce32689a 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanLaneCardList.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanLaneCardList.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useLayoutEffect, useMemo, useRef } from 'react' import { useVirtualizer } from '@tanstack/react-virtual' -import type { Repo, WorkspaceStatus, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorkspaceStatus, Worktree } from '../../../../shared/worktree/types' import WorkspaceKanbanCard from './WorkspaceKanbanCard' import { registerWorkspaceKanbanVirtualLaneLayout } from './workspace-kanban-virtual-lane-layout' diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanLaneGrid.test.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanLaneGrid.test.tsx index 161bb9ff597..f5a81bdb3bc 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanLaneGrid.test.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanLaneGrid.test.tsx @@ -2,7 +2,8 @@ import { act, cleanup, fireEvent, render } from '@testing-library/react' import React, { createRef } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo, WorkspaceStatusDefinition, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorkspaceStatusDefinition, Worktree } from '../../../../shared/worktree/types' const virtualWindow = vi.hoisted(() => ({ startIndex: 0, visibleCount: 4 })) const animationFrames = new Map() diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanLaneGrid.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanLaneGrid.tsx index 48d77c9b7f7..a13468fc340 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanLaneGrid.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanLaneGrid.tsx @@ -8,12 +8,12 @@ import React, { useState } from 'react' import { useVirtualizer, type Range } from '@tanstack/react-virtual' +import type { Repo } from '../../../../shared/repo-types' import type { - Repo, WorkspaceStatus, WorkspaceStatusDefinition, Worktree -} from '../../../../shared/types' +} from '../../../../shared/worktree/types' import type { WorkspaceKanbanLaneView } from './workspace-kanban-search' import { extractWorkspaceKanbanLaneRange } from './workspace-kanban-lane-range' import WorkspaceKanbanStatusLane from './WorkspaceKanbanStatusLane' diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanSettingsMenu.test.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanSettingsMenu.test.tsx index bba46379d77..40dead0e341 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanSettingsMenu.test.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanSettingsMenu.test.tsx @@ -2,7 +2,7 @@ import { createRoot, type Root } from 'react-dom/client' import { act, type ReactNode } from 'react' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { WorkspaceStatusDefinition } from '../../../../shared/types' +import type { WorkspaceStatusDefinition } from '../../../../shared/worktree/types' const statuses: WorkspaceStatusDefinition[] = [{ id: 'todo', label: 'Todo' }] ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanSettingsMenu.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanSettingsMenu.tsx index a7fd4693143..d4bceb02494 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanSettingsMenu.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanSettingsMenu.tsx @@ -10,7 +10,7 @@ import { import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' import { SettingsSwitch } from '../settings/SettingsFormControls' -import type { WorkspaceStatusDefinition } from '../../../../shared/types' +import type { WorkspaceStatusDefinition } from '../../../../shared/worktree/types' import { getWorkspaceStatusVisualMeta } from './workspace-status' import WorkspaceStatusAppearancePopover from './WorkspaceStatusAppearancePopover' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanStatusLane.test.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanStatusLane.test.tsx index 91987985d59..fc2a3e5e9bc 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanStatusLane.test.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanStatusLane.test.tsx @@ -3,7 +3,8 @@ import React, { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { serializeWorkspaceLaneFullIds } from './workspace-kanban-filtered-drop-index' import WorkspaceKanbanStatusLane from './WorkspaceKanbanStatusLane' diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanStatusLane.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanStatusLane.tsx index cceda5e12ee..1379246c5be 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanStatusLane.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanStatusLane.tsx @@ -1,11 +1,11 @@ import React, { useMemo, useRef } from 'react' import { Plus } from 'lucide-react' +import type { Repo } from '../../../../shared/repo-types' import type { - Repo, WorkspaceStatus, WorkspaceStatusDefinition, Worktree -} from '../../../../shared/types' +} from '../../../../shared/worktree/types' import { WORKSPACE_BOARD_COLUMN_WIDTH_MAX, WORKSPACE_BOARD_COLUMN_WIDTH_MIN diff --git a/src/renderer/src/components/sidebar/WorkspaceStatusAppearancePopover.tsx b/src/renderer/src/components/sidebar/WorkspaceStatusAppearancePopover.tsx index c018d7ae048..fb0ececf03a 100644 --- a/src/renderer/src/components/sidebar/WorkspaceStatusAppearancePopover.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceStatusAppearancePopover.tsx @@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' -import type { WorkspaceStatusDefinition } from '../../../../shared/types' +import type { WorkspaceStatusDefinition } from '../../../../shared/worktree/types' import { getWorkspaceStatusColorOptions, getWorkspaceStatusIconOptions, diff --git a/src/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsx index f9b3976a826..73927976237 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsx @@ -3,7 +3,10 @@ import { act, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings, Repo, Worktree, WorktreeCardProperty } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' +import type { Worktree } from '../../../../shared/worktree/types' const openModal = vi.fn() const setRenamingWorktreeId = vi.fn() diff --git a/src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx index 707ce16ec61..a3097460ea7 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx @@ -3,7 +3,10 @@ import React, { type ReactNode } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData' import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { GlobalSettings, Repo, Worktree, WorktreeCardProperty } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { WorkspacePortScanResult } from '../../../../shared/workspace-ports' const fetchHostedReviewForBranch = vi.fn() diff --git a/src/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsx index 5aee22c8b16..e8376a64f08 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsx @@ -4,7 +4,10 @@ import React, { act, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData' -import type { GlobalSettings, Repo, Worktree, WorktreeCardProperty } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { WorkspacePortScanResult } from '../../../../shared/workspace-ports' const fetchHostedReviewForBranch = vi.fn() diff --git a/src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx index 98c2a1b3712..6d9fa6d1b5d 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx @@ -3,7 +3,9 @@ import type { ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { act } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo, Worktree, WorktreeCardProperty } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' +import type { Worktree } from '../../../../shared/worktree/types' const fetchHostedReviewForBranch = vi.fn() const fetchIssue = vi.fn() diff --git a/src/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsx index cbbf5a35b86..1e26254e712 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsx @@ -1,7 +1,9 @@ import { renderToStaticMarkup } from 'react-dom/server' import type { ReactNode } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo, Worktree, WorktreeCardProperty } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' +import type { Worktree } from '../../../../shared/worktree/types' const fetchHostedReviewForBranch = vi.fn() const fetchIssue = vi.fn() diff --git a/src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx index 02927202e29..7c9669a22f9 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx @@ -2,13 +2,11 @@ import { renderToStaticMarkup } from 'react-dom/server' import type { ReactNode } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { - GlobalSettings, - PRInfo, - Repo, - Worktree, - WorktreeCardProperty -} from '../../../../shared/types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' +import type { Worktree } from '../../../../shared/worktree/types' const fetchHostedReviewForBranch = vi.fn() const fetchIssue = vi.fn() diff --git a/src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx index 9da7873b388..47017f87fab 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx @@ -1,7 +1,10 @@ import { renderToStaticMarkup } from 'react-dom/server' import type { ReactNode } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { GlobalSettings, Repo, Worktree, WorktreeCardProperty } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' +import type { Worktree } from '../../../../shared/worktree/types' const fetchHostedReviewForBranch = vi.fn() const fetchIssue = vi.fn() diff --git a/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx index 4c31d82e153..1ae9026cc74 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx @@ -2,13 +2,11 @@ import { renderToStaticMarkup } from 'react-dom/server' import type { ReactNode } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { - GlobalSettings, - PRInfo, - Repo, - Worktree, - WorktreeCardProperty -} from '../../../../shared/types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' +import type { Worktree } from '../../../../shared/worktree/types' import { COMPACT_WORKTREE_CARD_PROPERTIES } from '../../../../shared/worktree/card-properties' import type { WorkspacePortScanResult } from '../../../../shared/workspace-ports' diff --git a/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx index 7456cce2faa..ffef859d9ca 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx @@ -1,13 +1,11 @@ import { renderToStaticMarkup } from 'react-dom/server' import type { ReactNode } from 'react' import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' -import type { - GitConflictOperation, - GlobalSettings, - Repo, - Worktree, - WorktreeCardProperty -} from '../../../../shared/types' +import type { GitConflictOperation } from '../../../../shared/git-status-types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' +import type { Worktree } from '../../../../shared/worktree/types' import type WorktreeCardComponent from './WorktreeCard' import type * as WorkspaceDeleteQuickAction from './workspace-delete-quick-action' diff --git a/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx index 19ca3a4e441..1fe2f4b9530 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx @@ -1,7 +1,9 @@ import { renderToStaticMarkup } from 'react-dom/server' import type { ReactNode } from 'react' import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo, Worktree, WorktreeCardProperty } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' +import type { Worktree } from '../../../../shared/worktree/types' import type WorktreeCardComponent from './WorktreeCard' const fetchHostedReviewForBranch = vi.fn() diff --git a/src/renderer/src/components/sidebar/WorktreeCard.test.ts b/src/renderer/src/components/sidebar/WorktreeCard.test.ts index abc03a3c4fd..35c351f3457 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.test.ts +++ b/src/renderer/src/components/sidebar/WorktreeCard.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' vi.mock('@/lib/agent-status', () => ({ detectAgentStatusFromTitle: vi.fn((title: string) => { diff --git a/src/renderer/src/components/sidebar/WorktreeCardAutomationDetailSection.tsx b/src/renderer/src/components/sidebar/WorktreeCardAutomationDetailSection.tsx index 7d8ebd2de99..1ba6a45ce48 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardAutomationDetailSection.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardAutomationDetailSection.tsx @@ -1,7 +1,7 @@ import React from 'react' import { CalendarClock, PlayCircle } from 'lucide-react' import type { ExecutionHostId } from '../../../../shared/execution-host' -import type { AutomationWorkspaceProvenance } from '../../../../shared/types' +import type { AutomationWorkspaceProvenance } from '../../../../shared/worktree/types' import { WorktreeCardDetailSection, WorktreeCardDetailSectionContent diff --git a/src/renderer/src/components/sidebar/WorktreeCardCliDetailSection.tsx b/src/renderer/src/components/sidebar/WorktreeCardCliDetailSection.tsx index c5903340002..b636fc015e6 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardCliDetailSection.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardCliDetailSection.tsx @@ -1,6 +1,6 @@ import React from 'react' import { SquareTerminal } from 'lucide-react' -import type { CliWorkspaceProvenance } from '../../../../shared/types' +import type { CliWorkspaceProvenance } from '../../../../shared/worktree/types' import { formatAgentTypeLabel } from '../../../../shared/agent-type-label' import { WorktreeCardDetailSection, diff --git a/src/renderer/src/components/sidebar/WorktreeCardDisplayMenuSection.tsx b/src/renderer/src/components/sidebar/WorktreeCardDisplayMenuSection.tsx index 535ab541435..1c9cd781052 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardDisplayMenuSection.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardDisplayMenuSection.tsx @@ -10,7 +10,10 @@ import { DropdownMenuSubContent, DropdownMenuSubTrigger } from '@/components/ui/dropdown-menu' -import type { AgentActivityDisplayMode, WorktreeCardProperty } from '../../../../shared/types' +import type { + AgentActivityDisplayMode, + WorktreeCardProperty +} from '../../../../shared/ui-chrome-types' import { AGENT_ACTIVITY_DISPLAY_OPTIONS, CARD_LAYOUT_OPTIONS, diff --git a/src/renderer/src/components/sidebar/WorktreeCardHelpers.tsx b/src/renderer/src/components/sidebar/WorktreeCardHelpers.tsx index 2d2d081b067..5d5900af933 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardHelpers.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardHelpers.tsx @@ -1,5 +1,7 @@ import React from 'react' -import type { CheckStatus, GitConflictOperation, TerminalTab } from '../../../../shared/types' +import type { GitConflictOperation } from '../../../../shared/git-status-types' +import type { CheckStatus } from '../../../../shared/github/pull-request-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' // ── Pure helper functions ──────────────────────────────────────────── diff --git a/src/renderer/src/components/sidebar/WorktreeCardMetadataStatusBadges.tsx b/src/renderer/src/components/sidebar/WorktreeCardMetadataStatusBadges.tsx index 3cd3fece64c..21b064cc323 100644 --- a/src/renderer/src/components/sidebar/WorktreeCardMetadataStatusBadges.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCardMetadataStatusBadges.tsx @@ -4,7 +4,7 @@ import { CircleCheck, CircleDot, CircleX, Clock, GitMerge } from 'lucide-react' import { cn } from '@/lib/utils' import { PullRequestIcon, checksLabel } from './WorktreeCardHelpers' import type { WorktreeCardPrDisplay } from './worktree-card-pr-display' -import type { IssueInfo } from '../../../../shared/types' +import type { IssueInfo } from '../../../../shared/github/pull-request-types' import { translate } from '@/i18n/i18n' function MetadataStatusBadge({ diff --git a/src/renderer/src/components/sidebar/WorktreeContextMenu.test.ts b/src/renderer/src/components/sidebar/WorktreeContextMenu.test.ts index 31f2e297f68..b4fc426a79e 100644 --- a/src/renderer/src/components/sidebar/WorktreeContextMenu.test.ts +++ b/src/renderer/src/components/sidebar/WorktreeContextMenu.test.ts @@ -14,7 +14,8 @@ import { selectMenuScopedMap, shouldRevealWorktreeDeveloperMenu } from './WorktreeContextMenu' -import type { Worktree, WorktreeLineage, WorkspaceStatusDefinition } from '../../../../shared/types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { WorkspaceStatusDefinition, Worktree } from '../../../../shared/worktree/types' describe('shouldRevealWorktreeDeveloperMenu', () => { it('stays hidden for an ordinary right-click', () => { diff --git a/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx b/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx index be61dd64d3d..d2c20fff721 100644 --- a/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx +++ b/src/renderer/src/components/sidebar/WorktreeContextMenu.tsx @@ -34,12 +34,12 @@ import { useAppStore } from '@/store' import type { AppState } from '@/store/types' import { useAllWorktrees, useRepoById, useRepoMap, useWorktreeMap } from '@/store/selectors' import { cn } from '@/lib/utils' +import type { Repo } from '../../../../shared/repo-types' import type { - Repo, - Worktree, WorkspaceStatus, - WorkspaceStatusDefinition -} from '../../../../shared/types' + WorkspaceStatusDefinition, + Worktree +} from '../../../../shared/worktree/types' import { deferWorktreeContextMenuDeleteIntent, type WorktreeContextMenuDeleteIntent diff --git a/src/renderer/src/components/sidebar/WorktreeDeveloperMenuReveal.test.tsx b/src/renderer/src/components/sidebar/WorktreeDeveloperMenuReveal.test.tsx index 7816939efb2..6955e14ac60 100644 --- a/src/renderer/src/components/sidebar/WorktreeDeveloperMenuReveal.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeDeveloperMenuReveal.test.tsx @@ -8,7 +8,7 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react' import type { ReactNode } from 'react' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' vi.mock('@/store', () => { const state = { diff --git a/src/renderer/src/components/sidebar/WorktreeList.card-memo-stability.test.tsx b/src/renderer/src/components/sidebar/WorktreeList.card-memo-stability.test.tsx index 5c9dda301d3..8278eb3482b 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.card-memo-stability.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.card-memo-stability.test.tsx @@ -4,7 +4,9 @@ import { act, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' -import type { Repo, Worktree, WorktreeCardProperty } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' +import type { Worktree } from '../../../../shared/worktree/types' globalThis.IS_REACT_ACT_ENVIRONMENT = true diff --git a/src/renderer/src/components/sidebar/WorktreeList.lineage-agent-expansion-coupling.test.tsx b/src/renderer/src/components/sidebar/WorktreeList.lineage-agent-expansion-coupling.test.tsx index 5a644f0d265..01c9847d980 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.lineage-agent-expansion-coupling.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.lineage-agent-expansion-coupling.test.tsx @@ -20,13 +20,11 @@ import type { AgentStatusEntry, AgentStatusOrchestrationContext } from '../../../../shared/agent-status-types' -import type { - Repo, - TerminalTab, - Worktree, - WorktreeCardProperty, - WorktreeLineage -} from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { clearWorktreeAgentExpansionStateForTests } from './worktree-card-agents-expansion-state' globalThis.IS_REACT_ACT_ENVIRONMENT = true diff --git a/src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts b/src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts index 4e8896fb44d..756c99d8522 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts +++ b/src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts @@ -2,13 +2,11 @@ import React from 'react' import { renderToStaticMarkup } from 'react-dom/server' import { beforeAll, describe, expect, it, vi } from 'vitest' -import type { - FolderWorkspace, - ProjectGroup, - Repo, - Worktree, - WorktreeLineage -} from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import type * as WorktreeListModule from './WorktreeList' import { cloneDefaultWorkspaceStatuses } from '../../../../shared/workspace-statuses' diff --git a/src/renderer/src/components/sidebar/WorktreeList.lineage-child-real-card.test.tsx b/src/renderer/src/components/sidebar/WorktreeList.lineage-child-real-card.test.tsx index 52f3a591473..d36aa264f02 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.lineage-child-real-card.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.lineage-child-real-card.test.tsx @@ -4,12 +4,10 @@ import { act, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { - Repo, - Worktree, - WorktreeCardProperty, - WorktreeLineage -} from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { FLUSH_CARD_MIN_CONTENT_INSET, LINEAGE_CHILDREN_INLINE_OFFSET, diff --git a/src/renderer/src/components/sidebar/WorktreeList.status-lane-lineage-drop.test.tsx b/src/renderer/src/components/sidebar/WorktreeList.status-lane-lineage-drop.test.tsx index 00c9e47a0c8..47cc038799d 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.status-lane-lineage-drop.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.status-lane-lineage-drop.test.tsx @@ -3,14 +3,11 @@ import { act, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' -import type { - Repo, - WorkspaceStatus, - Worktree, - WorktreeCardProperty, - WorktreeLineage, - WorktreeMeta -} from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { WorktreeMeta } from '../../../../shared/worktree/meta-types' +import type { WorkspaceStatus, Worktree } from '../../../../shared/worktree/types' import { DEFAULT_WORKSPACE_STATUSES } from '../../../../shared/workspace-status-defaults' import { WORKSPACE_STATUS_DRAG_IDS_TYPE, diff --git a/src/renderer/src/components/sidebar/WorktreeList.tsx b/src/renderer/src/components/sidebar/WorktreeList.tsx index 0202dd41d70..138f36a3a2f 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.tsx @@ -57,18 +57,17 @@ import { DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { cn } from '@/lib/utils' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' +import type { ProjectOrderBy } from '../../../../shared/ui-chrome-types' +import type { WorkspaceLineage, WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { WorktreeMeta } from '../../../../shared/worktree/meta-types' import type { - Worktree, - Repo, - FolderWorkspace, - ProjectGroup, - ProjectOrderBy, - WorktreeLineage, - WorktreeMeta, - WorkspaceLineage, WorkspaceStatus, - WorkspaceStatusDefinition -} from '../../../../shared/types' + WorkspaceStatusDefinition, + Worktree +} from '../../../../shared/worktree/types' import { DEFAULT_SHOW_SLEEPING_WORKSPACES } from '../../../../shared/constants' import { buildWorktreeComparator, compareWorktreeSortLabel } from './smart-sort' import { diff --git a/src/renderer/src/components/sidebar/WorktreeMetaDialog.test.tsx b/src/renderer/src/components/sidebar/WorktreeMetaDialog.test.tsx index 66de27601e5..13b4bbac1b8 100644 --- a/src/renderer/src/components/sidebar/WorktreeMetaDialog.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeMetaDialog.test.tsx @@ -4,13 +4,11 @@ import { act, type ReactNode } from 'react' import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAppStore } from '@/store' -import type { - FolderWorkspace, - LinearIssue, - Repo, - Worktree, - WorktreeMeta -} from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { LinearIssue } from '../../../../shared/linear/issue-types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeMeta } from '../../../../shared/worktree/meta-types' +import type { Worktree } from '../../../../shared/worktree/types' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' // Why: Radix tooltips need a provider the dialog does not own, and the menu's diff --git a/src/renderer/src/components/sidebar/WorktreeOpenInMenu.tsx b/src/renderer/src/components/sidebar/WorktreeOpenInMenu.tsx index d5242211256..ad4636860df 100644 --- a/src/renderer/src/components/sidebar/WorktreeOpenInMenu.tsx +++ b/src/renderer/src/components/sidebar/WorktreeOpenInMenu.tsx @@ -15,7 +15,8 @@ import { OpenInApplicationIcon } from '@/lib/open-in-app-catalog' import { getExternalEditorOpenCapability } from '@/lib/external-editor-open-capability' import { NO_OPEN_IN_APPLICATIONS } from '@/lib/open-in-application-selection' import type { ShellOpenExternalEditorResult } from '../../../../shared/shell-open-types' -import type { GlobalSettings, OpenInApplication } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { OpenInApplication } from '../../../../shared/ui-chrome-types' import { translate } from '@/i18n/i18n' export { getLocalFileManagerLabel } from '@/lib/local-file-manager-label' diff --git a/src/renderer/src/components/sidebar/WorktreeParentPickerPopover.test.ts b/src/renderer/src/components/sidebar/WorktreeParentPickerPopover.test.ts index 4f7df50e9d3..f26f6f96f5d 100644 --- a/src/renderer/src/components/sidebar/WorktreeParentPickerPopover.test.ts +++ b/src/renderer/src/components/sidebar/WorktreeParentPickerPopover.test.ts @@ -1,7 +1,7 @@ // @vitest-environment happy-dom import { afterEach, describe, expect, it, vi } from 'vitest' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { getWorktreeParentPickerFocusRestoreTarget, handleWorktreeParentPickerKeyDown, diff --git a/src/renderer/src/components/sidebar/WorktreeParentPickerRow.tsx b/src/renderer/src/components/sidebar/WorktreeParentPickerRow.tsx index 1d78102cfb7..7a747061449 100644 --- a/src/renderer/src/components/sidebar/WorktreeParentPickerRow.tsx +++ b/src/renderer/src/components/sidebar/WorktreeParentPickerRow.tsx @@ -5,7 +5,8 @@ import { getWorktreeStatusLabel, type WorktreeStatus } from '@/lib/worktree-stat import { translate } from '@/i18n/i18n' import { branchDisplayName } from './WorktreeCardHelpers' import StatusIndicator from './StatusIndicator' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' // Why: presentational and memoized — the picker resolves every row's status // from one batched store read so the rows themselves hold no subscriptions. diff --git a/src/renderer/src/components/sidebar/WorktreeVisibilityDialog.test.tsx b/src/renderer/src/components/sidebar/WorktreeVisibilityDialog.test.tsx index 7dfd864ce95..7b3a3eb6e5d 100644 --- a/src/renderer/src/components/sidebar/WorktreeVisibilityDialog.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeVisibilityDialog.test.tsx @@ -4,7 +4,11 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import type { ReactNode } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { DetectedWorktree, DetectedWorktreeListResult, Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { + DetectedWorktree, + DetectedWorktreeListResult +} from '../../../../shared/worktree/types' globalThis.IS_REACT_ACT_ENVIRONMENT = true diff --git a/src/renderer/src/components/sidebar/WorktreeVisibilityDialog.tsx b/src/renderer/src/components/sidebar/WorktreeVisibilityDialog.tsx index baa73875d79..e4a8ce45616 100644 --- a/src/renderer/src/components/sidebar/WorktreeVisibilityDialog.tsx +++ b/src/renderer/src/components/sidebar/WorktreeVisibilityDialog.tsx @@ -35,7 +35,7 @@ import WorktreeVisibilitySourceList, { type WorktreeVisibilitySourceAddResult, type WorktreeVisibilitySourceRow } from './WorktreeVisibilitySourceList' -import type { CustomWorktreeVisibilitySource, Repo } from '../../../../shared/types' +import type { CustomWorktreeVisibilitySource, Repo } from '../../../../shared/repo-types' import { MAX_CUSTOM_WORKTREE_VISIBILITY_SOURCES, buildWorktreeSourcePreferenceUpdate, diff --git a/src/renderer/src/components/sidebar/WorktreeVisibilitySourceList.tsx b/src/renderer/src/components/sidebar/WorktreeVisibilitySourceList.tsx index f7caa8bdee6..ca23f122550 100644 --- a/src/renderer/src/components/sidebar/WorktreeVisibilitySourceList.tsx +++ b/src/renderer/src/components/sidebar/WorktreeVisibilitySourceList.tsx @@ -9,9 +9,9 @@ import { translate } from '@/i18n/i18n' import type { BuiltInWorktreeVisibilitySourceId, CustomWorktreeVisibilitySource, - DetectedWorktree, Repo -} from '../../../../shared/types' +} from '../../../../shared/repo-types' +import type { DetectedWorktree } from '../../../../shared/worktree/types' import { createWorktreeVisibilitySourceMatcher, effectiveBuiltInWorktreeSourceVisibility, diff --git a/src/renderer/src/components/sidebar/active-worktree-focus-after-delete.ts b/src/renderer/src/components/sidebar/active-worktree-focus-after-delete.ts index 18b24dcfafe..f1042c8466c 100644 --- a/src/renderer/src/components/sidebar/active-worktree-focus-after-delete.ts +++ b/src/renderer/src/components/sidebar/active-worktree-focus-after-delete.ts @@ -2,7 +2,8 @@ import { useAppStore } from '@/store' import { getRepoMapFromState, getWorktreeMapFromState } from '@/store/selectors' import { activateAndRevealWorktree } from '@/lib/worktree-activation' import { isRuntimeOwnedSshTargetId, parseExecutionHostId } from '../../../../shared/execution-host' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' type AppStoreState = ReturnType diff --git a/src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.test.ts b/src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.test.ts index 57a202a57b4..2c33f406f62 100644 --- a/src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.test.ts +++ b/src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { buildAddRepoExistingWorkspacesDetectedEvent, buildAddRepoExistingWorkspacesTelemetry, diff --git a/src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.ts b/src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.ts index 695cb983f43..d74cf898923 100644 --- a/src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.ts +++ b/src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.ts @@ -2,7 +2,7 @@ import type { AddRepoExistingWorkspaceSource, EventProps } from '../../../../shared/telemetry-events' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { compareWorktreeDisplayName } from '@/lib/worktree-display-name-order' type ExistingWorkspacesDetectedProps = EventProps<'add_repo_existing_workspaces_detected'> diff --git a/src/renderer/src/components/sidebar/add-repo-skip-finalization.test.ts b/src/renderer/src/components/sidebar/add-repo-skip-finalization.test.ts index de9c6f430cf..8f1808f4c24 100644 --- a/src/renderer/src/components/sidebar/add-repo-skip-finalization.test.ts +++ b/src/renderer/src/components/sidebar/add-repo-skip-finalization.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { finalizeImportedRepoAfterSkip, type AddRepoSkipFinalizationState diff --git a/src/renderer/src/components/sidebar/add-repo-skip-finalization.ts b/src/renderer/src/components/sidebar/add-repo-skip-finalization.ts index 7c9c69357ce..f22a8d9d251 100644 --- a/src/renderer/src/components/sidebar/add-repo-skip-finalization.ts +++ b/src/renderer/src/components/sidebar/add-repo-skip-finalization.ts @@ -1,5 +1,5 @@ -import type { Worktree } from '../../../../shared/types' -import { isDefaultBranchWorkspace } from './visible-worktrees' +import type { Worktree } from '../../../../shared/worktree/types' +import { isDefaultBranchWorkspace } from './default-branch-workspace' export type AddRepoSkipFinalizationState = { activeRepoId: string | null diff --git a/src/renderer/src/components/sidebar/add-repo-store-upsert.ts b/src/renderer/src/components/sidebar/add-repo-store-upsert.ts index 1593b3cda1c..4a085462948 100644 --- a/src/renderer/src/components/sidebar/add-repo-store-upsert.ts +++ b/src/renderer/src/components/sidebar/add-repo-store-upsert.ts @@ -4,7 +4,7 @@ import { toRuntimeExecutionHostId, toSshExecutionHostId } from '../../../../shared/execution-host' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { useAppStore } from '@/store' import { getRepoHostIdentity } from '@/store/slices/repo-host-identity' diff --git a/src/renderer/src/components/sidebar/complete-nested-folder-open.ts b/src/renderer/src/components/sidebar/complete-nested-folder-open.ts index 329027b6af2..f02fff1c428 100644 --- a/src/renderer/src/components/sidebar/complete-nested-folder-open.ts +++ b/src/renderer/src/components/sidebar/complete-nested-folder-open.ts @@ -1,6 +1,6 @@ import { toast } from 'sonner' import { useAppStore } from '@/store' -import type { NestedRepoScanResult } from '../../../../shared/types' +import type { NestedRepoScanResult } from '../../../../shared/project-group-types' import type { CapturedRuntimeOwner } from './add-repo-runtime-owner' import { trackNestedFolderOpen } from './track-nested-folder-open' diff --git a/src/renderer/src/components/sidebar/default-branch-visible-under-hide-sleeping.test.ts b/src/renderer/src/components/sidebar/default-branch-visible-under-hide-sleeping.test.ts index 9f06dda9df2..a0452e2f508 100644 --- a/src/renderer/src/components/sidebar/default-branch-visible-under-hide-sleeping.test.ts +++ b/src/renderer/src/components/sidebar/default-branch-visible-under-hide-sleeping.test.ts @@ -1,10 +1,8 @@ import { describe, expect, it } from 'vitest' -import { - computeVisibleWorktreeIds, - isDefaultBranchWorkspace, - type SidebarFilterState -} from './visible-worktrees' -import type { Repo, Worktree } from '../../../../shared/types' +import { computeVisibleWorktreeIds, type SidebarFilterState } from './visible-worktrees' +import { isDefaultBranchWorkspace } from './default-branch-workspace' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' /** diff --git a/src/renderer/src/components/sidebar/default-branch-workspace.ts b/src/renderer/src/components/sidebar/default-branch-workspace.ts index 5289b3d2d4c..43404232066 100644 --- a/src/renderer/src/components/sidebar/default-branch-workspace.ts +++ b/src/renderer/src/components/sidebar/default-branch-workspace.ts @@ -1,4 +1,4 @@ -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' /** Keeps provisioned roots visible because they are the recipe-created workspace, not a source-repo row. */ export function isDefaultBranchWorkspace(worktree: Worktree): boolean { diff --git a/src/renderer/src/components/sidebar/delete-worktree-dialog-copy.ts b/src/renderer/src/components/sidebar/delete-worktree-dialog-copy.ts index 7f761fcbd8d..862df883b54 100644 --- a/src/renderer/src/components/sidebar/delete-worktree-dialog-copy.ts +++ b/src/renderer/src/components/sidebar/delete-worktree-dialog-copy.ts @@ -1,5 +1,6 @@ import { isFolderRepo } from '../../../../shared/repo-kind' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' type WorktreeRepoRef = Pick diff --git a/src/renderer/src/components/sidebar/delete-worktree-dirty-change-counts.ts b/src/renderer/src/components/sidebar/delete-worktree-dirty-change-counts.ts index 3a0f709caae..339251046d5 100644 --- a/src/renderer/src/components/sidebar/delete-worktree-dirty-change-counts.ts +++ b/src/renderer/src/components/sidebar/delete-worktree-dirty-change-counts.ts @@ -1,4 +1,5 @@ -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { WorktreeDeleteState } from '../../store/slices/worktree-helpers' import { isFolderWorkspaceDelete } from './delete-worktree-dialog-copy' diff --git a/src/renderer/src/components/sidebar/delete-worktree-preference-toast.ts b/src/renderer/src/components/sidebar/delete-worktree-preference-toast.ts index 188e1bdbbaf..9c048e0829c 100644 --- a/src/renderer/src/components/sidebar/delete-worktree-preference-toast.ts +++ b/src/renderer/src/components/sidebar/delete-worktree-preference-toast.ts @@ -1,7 +1,7 @@ import { toast } from 'sonner' import { translate } from '@/i18n/i18n' import type { SettingsNavTarget } from '@/lib/settings-navigation-types' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' export function persistDeleteWorktreeConfirmSkipPreference({ updateSettings, diff --git a/src/renderer/src/components/sidebar/empty-project-placeholder-repos.test.ts b/src/renderer/src/components/sidebar/empty-project-placeholder-repos.test.ts index 8a33151f73a..c8042ed6b4a 100644 --- a/src/renderer/src/components/sidebar/empty-project-placeholder-repos.test.ts +++ b/src/renderer/src/components/sidebar/empty-project-placeholder-repos.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getEmptyProjectPlaceholderRepoIds } from './empty-project-placeholder-repos' const repo: Repo = { diff --git a/src/renderer/src/components/sidebar/empty-project-placeholder-repos.ts b/src/renderer/src/components/sidebar/empty-project-placeholder-repos.ts index 57aee59c1bd..29e30d196bf 100644 --- a/src/renderer/src/components/sidebar/empty-project-placeholder-repos.ts +++ b/src/renderer/src/components/sidebar/empty-project-placeholder-repos.ts @@ -1,4 +1,5 @@ -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { WorktreeGroupBy } from './worktree-list-groups' export function getEmptyProjectPlaceholderRepoIds(args: { diff --git a/src/renderer/src/components/sidebar/focused-agent-row-highlight.test.ts b/src/renderer/src/components/sidebar/focused-agent-row-highlight.test.ts index c3cd300892e..465f1362a59 100644 --- a/src/renderer/src/components/sidebar/focused-agent-row-highlight.test.ts +++ b/src/renderer/src/components/sidebar/focused-agent-row-highlight.test.ts @@ -4,7 +4,7 @@ import type { MigrationUnsupportedPtyEntry } from '../../../../shared/agent-status-types' import { makePaneKey } from '../../../../shared/stable-pane-id' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { getFocusedAgentPaneKeyForWorktree, type FocusedAgentRowHighlightState diff --git a/src/renderer/src/components/sidebar/folder-workspace-card-pr-display.test.ts b/src/renderer/src/components/sidebar/folder-workspace-card-pr-display.test.ts index dc971a0603e..4ca0078e2b3 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-card-pr-display.test.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-card-pr-display.test.ts @@ -1,12 +1,8 @@ import { describe, expect, it } from 'vitest' -import type { - CheckStatus, - PRInfo, - Repo, - Worktree, - WorktreeLineage, - WorkspaceLineage -} from '../../../../shared/types' +import type { CheckStatus, PRInfo } from '../../../../shared/github/pull-request-types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorkspaceLineage, WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { folderWorkspaceKey, worktreeWorkspaceKey } from '../../../../shared/workspace-scope' import { getFolderWorkspaceCardPrDisplay } from './folder-workspace-card-pr-display' diff --git a/src/renderer/src/components/sidebar/folder-workspace-card-pr-display.ts b/src/renderer/src/components/sidebar/folder-workspace-card-pr-display.ts index 3f71970ec33..5914520603e 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-card-pr-display.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-card-pr-display.ts @@ -1,5 +1,7 @@ import type { AppState } from '@/store/types' -import type { Repo, Worktree, WorktreeLineage, WorkspaceLineage } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorkspaceLineage, WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { folderWorkspaceKey, parseWorkspaceKey } from '../../../../shared/workspace-scope' import { buildParentPrChecksRows, diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-helpers.test.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-helpers.test.ts index 0f43496ee54..73648ebfd02 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-composer-helpers.test.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-composer-helpers.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { ProjectGroup, Repo } from '../../../../shared/types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' import { getFolderSourceRepos, getFolderWorkspacePrimaryActionLabel diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-helpers.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-helpers.ts index c6b920bd9d8..a63a2caf920 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-composer-helpers.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-composer-helpers.ts @@ -17,14 +17,12 @@ import { } from '../../../../shared/execution-host' import { getProjectGroupSubtreeIds } from '../../../../shared/project-groups' import { isGitRepoKind } from '../../../../shared/repo-kind' -import type { - FolderWorkspace, - GitHubWorkItem, - GitLabWorkItem, - LinearIssue, - ProjectGroup, - Repo -} from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' +import type { GitLabWorkItem } from '../../../../shared/gitlab-types' +import type { LinearIssue } from '../../../../shared/linear/issue-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' import type { SmartWorkspaceNameSelection } from '@/components/new-workspace/SmartWorkspaceNameField' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-path-status.test.tsx b/src/renderer/src/components/sidebar/folder-workspace-composer-path-status.test.tsx index 6c5f3523561..e13c5c2c14c 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-composer-path-status.test.tsx +++ b/src/renderer/src/components/sidebar/folder-workspace-composer-path-status.test.tsx @@ -3,7 +3,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { ProjectGroup } from '../../../../shared/types' +import type { ProjectGroup } from '../../../../shared/project-group-types' import { useAppStore } from '@/store' import { useFolderWorkspaceComposerPathStatus } from './folder-workspace-composer-path-status' diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-path-status.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-path-status.ts index 51369b6b8ea..911c373191d 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-composer-path-status.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-composer-path-status.ts @@ -7,7 +7,7 @@ import { getFolderWorkspacePathStatusTitle } from '@/lib/folder-workspace-path-status' import { isConfirmedStaleFolderPathStatus } from '../../../../shared/folder-workspace-path-status' -import type { ProjectGroup } from '../../../../shared/types' +import type { ProjectGroup } from '../../../../shared/project-group-types' export function useFolderWorkspaceComposerPathStatus( projectGroup: ProjectGroup | null, diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts index 4f129edf853..51432bc1e5f 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts @@ -1,7 +1,8 @@ // @vitest-environment happy-dom import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { FolderWorkspace, ProjectGroup } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import type * as NewWorkspaceModule from '@/lib/new-workspace' diff --git a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts index 2b64f589e68..35b3240ccb6 100644 --- a/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts +++ b/src/renderer/src/components/sidebar/folder-workspace-composer-submit.ts @@ -16,7 +16,9 @@ import { activateAndRevealFolderWorkspace } from '@/lib/worktree-activation' import { isWorkItemLookupText } from '@/lib/work-item-lookup-text' import { TUI_AGENT_CONFIG } from '../../../../shared/tui-agent-config' import { isWindowsAbsolutePathLike } from '../../../../shared/cross-platform-path' -import type { FolderWorkspace, ProjectGroup, TuiAgent } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { isWslUncPath } from '../../../../shared/wsl-paths' import { resolveLocalWindowsAgentStartupShell } from '../../../../shared/windows-terminal-shell' import type { AgentStartupShell } from '../../../../shared/tui-agent-startup-shell' diff --git a/src/renderer/src/components/sidebar/host-rename-remove.ts b/src/renderer/src/components/sidebar/host-rename-remove.ts index dc1dfec6c84..03ade9762fe 100644 --- a/src/renderer/src/components/sidebar/host-rename-remove.ts +++ b/src/renderer/src/components/sidebar/host-rename-remove.ts @@ -4,7 +4,8 @@ import { getHostSettingOverride, setHostSettingOverride } from '../../../../shared/host-setting-overrides' -import type { GlobalSettings, HostSettingOverrides } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { HostSettingOverrides } from '../../../../shared/host-setting-overrides' type OverridesSlice = Pick type OverridesMap = Partial> diff --git a/src/renderer/src/components/sidebar/host-section-rows.test.ts b/src/renderer/src/components/sidebar/host-section-rows.test.ts index 702356e26a6..1d8cc55a14b 100644 --- a/src/renderer/src/components/sidebar/host-section-rows.test.ts +++ b/src/renderer/src/components/sidebar/host-section-rows.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from 'vitest' -import type { FolderWorkspace, ProjectGroup, Repo, Worktree } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { PINNED_GROUP_KEY, type Row } from './worktree-list-groups' import { addHostSectionRows, type HostSectionRow } from './host-section-rows' diff --git a/src/renderer/src/components/sidebar/host-section-rows.ts b/src/renderer/src/components/sidebar/host-section-rows.ts index 7ca084d7520..1371f57ed05 100644 --- a/src/renderer/src/components/sidebar/host-section-rows.ts +++ b/src/renderer/src/components/sidebar/host-section-rows.ts @@ -11,7 +11,9 @@ import { import type { ExecutionHostHealth } from '../../../../shared/execution-host-registry' import type { RuntimeCompatVerdict } from '../../../../shared/protocol-compat' import type { SshConnectionStatus } from '../../../../shared/ssh-types' -import type { FolderWorkspace, ProjectGroup, Repo } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' import type { Row } from './worktree-list-groups' export type HostHeaderRow = { diff --git a/src/renderer/src/components/sidebar/imported-worktrees-card-actions.ts b/src/renderer/src/components/sidebar/imported-worktrees-card-actions.ts index ea4f758d3df..a40688cbb8a 100644 --- a/src/renderer/src/components/sidebar/imported-worktrees-card-actions.ts +++ b/src/renderer/src/components/sidebar/imported-worktrees-card-actions.ts @@ -1,4 +1,4 @@ -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { mergeExternalWorktreeInboxPaths } from '../../../../shared/external-worktree-inbox' export type ImportedWorktreeCardActionState = { diff --git a/src/renderer/src/components/sidebar/imported-worktrees-card-candidates.test.ts b/src/renderer/src/components/sidebar/imported-worktrees-card-candidates.test.ts index 5a6ed56abfc..1ea7886d55a 100644 --- a/src/renderer/src/components/sidebar/imported-worktrees-card-candidates.test.ts +++ b/src/renderer/src/components/sidebar/imported-worktrees-card-candidates.test.ts @@ -4,12 +4,12 @@ import { buildImportedWorktreesCardCandidates, getHiddenImportedWorktrees } from './imported-worktrees-card-candidates' +import type { Repo } from '../../../../shared/repo-types' import type { DetectedWorktree, DetectedWorktreeListResult, - Repo, Worktree -} from '../../../../shared/types' +} from '../../../../shared/worktree/types' const repo: Repo = { id: 'repo-1', diff --git a/src/renderer/src/components/sidebar/imported-worktrees-card-candidates.ts b/src/renderer/src/components/sidebar/imported-worktrees-card-candidates.ts index e827d1662ef..d530d2c6d17 100644 --- a/src/renderer/src/components/sidebar/imported-worktrees-card-candidates.ts +++ b/src/renderer/src/components/sidebar/imported-worktrees-card-candidates.ts @@ -1,4 +1,5 @@ -import type { DetectedWorktreeListResult, Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { DetectedWorktreeListResult, Worktree } from '../../../../shared/worktree/types' import { getHiddenExternalWorktrees } from '../../../../shared/external-worktree-inbox' import { isGitRepoKind } from '../../../../shared/repo-kind' import { diff --git a/src/renderer/src/components/sidebar/linear-agent-skill-runtime.ts b/src/renderer/src/components/sidebar/linear-agent-skill-runtime.ts index 46b047a4bc1..d816c387313 100644 --- a/src/renderer/src/components/sidebar/linear-agent-skill-runtime.ts +++ b/src/renderer/src/components/sidebar/linear-agent-skill-runtime.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { ProjectExecutionRuntimeResolution } from '../../../../shared/project-execution-runtime' import type { SkillDiscoveryTarget } from '../../../../shared/skills' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/sidebar/local-base-ref-suggestion-toast.test.tsx b/src/renderer/src/components/sidebar/local-base-ref-suggestion-toast.test.tsx index 88e5b8cb236..8c31110242c 100644 --- a/src/renderer/src/components/sidebar/local-base-ref-suggestion-toast.test.tsx +++ b/src/renderer/src/components/sidebar/local-base-ref-suggestion-toast.test.tsx @@ -5,7 +5,7 @@ import { createRoot, type Root } from 'react-dom/client' import { afterEach, describe, expect, it, vi } from 'vitest' import { toast } from 'sonner' import type { AppState } from '@/store/types' -import type { LocalBaseRefUpdateSuggestion } from '../../../../shared/types' +import type { LocalBaseRefUpdateSuggestion } from '../../../../shared/worktree/base-ref-drift-types' import { showLocalBaseRefUpdateSuggestionToast } from './local-base-ref-suggestion-toast' vi.mock('sonner', () => ({ diff --git a/src/renderer/src/components/sidebar/local-base-ref-suggestion-toast.tsx b/src/renderer/src/components/sidebar/local-base-ref-suggestion-toast.tsx index 97bd71e196a..18b0e619cd7 100644 --- a/src/renderer/src/components/sidebar/local-base-ref-suggestion-toast.tsx +++ b/src/renderer/src/components/sidebar/local-base-ref-suggestion-toast.tsx @@ -1,6 +1,6 @@ import { toast } from 'sonner' import type { AppState } from '@/store/types' -import type { LocalBaseRefUpdateSuggestion } from '../../../../shared/types' +import type { LocalBaseRefUpdateSuggestion } from '../../../../shared/worktree/base-ref-drift-types' import { Button } from '../ui/button' import { KEEP_LOCAL_MAIN_UP_TO_DATE_SECTION_ID, diff --git a/src/renderer/src/components/sidebar/new-external-worktrees-inbox-actions.test.ts b/src/renderer/src/components/sidebar/new-external-worktrees-inbox-actions.test.ts index 3435870dbd4..9f3abcc281a 100644 --- a/src/renderer/src/components/sidebar/new-external-worktrees-inbox-actions.test.ts +++ b/src/renderer/src/components/sidebar/new-external-worktrees-inbox-actions.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { importNewExternalWorktreeInboxPaths, keepNewExternalWorktreeInboxHidden, diff --git a/src/renderer/src/components/sidebar/new-external-worktrees-inbox-actions.ts b/src/renderer/src/components/sidebar/new-external-worktrees-inbox-actions.ts index 289d068d97f..f9a4534410d 100644 --- a/src/renderer/src/components/sidebar/new-external-worktrees-inbox-actions.ts +++ b/src/renderer/src/components/sidebar/new-external-worktrees-inbox-actions.ts @@ -1,4 +1,4 @@ -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { mergeExternalWorktreeInboxPaths } from '../../../../shared/external-worktree-inbox' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/sidebar/new-external-worktrees-inbox-candidates.test.ts b/src/renderer/src/components/sidebar/new-external-worktrees-inbox-candidates.test.ts index d099a63945c..cfab500f410 100644 --- a/src/renderer/src/components/sidebar/new-external-worktrees-inbox-candidates.test.ts +++ b/src/renderer/src/components/sidebar/new-external-worktrees-inbox-candidates.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from 'vitest' +import type { Repo } from '../../../../shared/repo-types' import type { DetectedWorktree, DetectedWorktreeListResult, - Repo, Worktree -} from '../../../../shared/types' +} from '../../../../shared/worktree/types' import { buildNewExternalWorktreesInboxCandidates } from './new-external-worktrees-inbox-candidates' const repo: Repo = { diff --git a/src/renderer/src/components/sidebar/new-external-worktrees-inbox-candidates.ts b/src/renderer/src/components/sidebar/new-external-worktrees-inbox-candidates.ts index aeda87cf710..8b7e3423dc7 100644 --- a/src/renderer/src/components/sidebar/new-external-worktrees-inbox-candidates.ts +++ b/src/renderer/src/components/sidebar/new-external-worktrees-inbox-candidates.ts @@ -1,4 +1,5 @@ -import type { DetectedWorktreeListResult, Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { DetectedWorktreeListResult, Worktree } from '../../../../shared/worktree/types' import { getNewExternalWorktreeInboxWorktrees } from '../../../../shared/external-worktree-inbox' import { isGitRepoKind } from '../../../../shared/repo-kind' import type { NewExternalWorktreesInboxCandidate } from './worktree-list-groups' diff --git a/src/renderer/src/components/sidebar/preserved-branch-toast.test.tsx b/src/renderer/src/components/sidebar/preserved-branch-toast.test.tsx index 24f768af4af..3424c372581 100644 --- a/src/renderer/src/components/sidebar/preserved-branch-toast.test.tsx +++ b/src/renderer/src/components/sidebar/preserved-branch-toast.test.tsx @@ -4,7 +4,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, describe, expect, it, vi } from 'vitest' import { toast } from 'sonner' -import type { RemoveWorktreeResult } from '../../../../shared/types' +import type { RemoveWorktreeResult } from '../../../../shared/worktree/create-types' import { showPreservedBranchToast } from './preserved-branch-toast' vi.mock('sonner', () => ({ diff --git a/src/renderer/src/components/sidebar/preserved-branch-toast.tsx b/src/renderer/src/components/sidebar/preserved-branch-toast.tsx index 03529ad4657..43b01513f53 100644 --- a/src/renderer/src/components/sidebar/preserved-branch-toast.tsx +++ b/src/renderer/src/components/sidebar/preserved-branch-toast.tsx @@ -1,6 +1,7 @@ import { Trash2 } from 'lucide-react' import { toast } from 'sonner' -import type { RemoveWorktreeResult, Worktree } from '../../../../shared/types' +import type { RemoveWorktreeResult } from '../../../../shared/worktree/create-types' +import type { Worktree } from '../../../../shared/worktree/types' import { translate } from '@/i18n/i18n' import { Button } from '../ui/button' diff --git a/src/renderer/src/components/sidebar/project-added-default-checkout.test.ts b/src/renderer/src/components/sidebar/project-added-default-checkout.test.ts index 8b4b42d8d44..24d1fd7b76a 100644 --- a/src/renderer/src/components/sidebar/project-added-default-checkout.test.ts +++ b/src/renderer/src/components/sidebar/project-added-default-checkout.test.ts @@ -1,11 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultOnboardingState } from '../../../../shared/constants' +import type { Repo } from '../../../../shared/repo-types' import type { DetectedWorktree, DetectedWorktreeListResult, - Repo, Worktree -} from '../../../../shared/types' +} from '../../../../shared/worktree/types' import { finishProjectAddWithDefaultCheckout, getProjectDefaultCheckout, diff --git a/src/renderer/src/components/sidebar/project-added-default-checkout.ts b/src/renderer/src/components/sidebar/project-added-default-checkout.ts index dabf089a8a0..33ab7dadd07 100644 --- a/src/renderer/src/components/sidebar/project-added-default-checkout.ts +++ b/src/renderer/src/components/sidebar/project-added-default-checkout.ts @@ -5,7 +5,7 @@ import type { AddRepoDefaultCheckoutHandoffSource, EventProps } from '../../../../shared/telemetry-events' -import type { DetectedWorktreeListResult, Worktree } from '../../../../shared/types' +import type { DetectedWorktreeListResult, Worktree } from '../../../../shared/worktree/types' import { relativePathInsideRoot } from '../../../../shared/cross-platform-path' import { markOnboardingProjectAdded } from '@/lib/onboarding-project-checklist' import { finalizeImportedRepoAfterSkip } from './add-repo-skip-finalization' diff --git a/src/renderer/src/components/sidebar/project-group-header-drag-commit.test.ts b/src/renderer/src/components/sidebar/project-group-header-drag-commit.test.ts index 3c1b0384f41..43ff9581712 100644 --- a/src/renderer/src/components/sidebar/project-group-header-drag-commit.test.ts +++ b/src/renderer/src/components/sidebar/project-group-header-drag-commit.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest' import { commitProjectGroupHeaderDragDrop } from './project-group-header-drag-commit' import type { ProjectGroupHeaderDragSession } from './project-group-header-drag-contract' -import type { ProjectGroup } from '../../../../shared/types' +import type { ProjectGroup } from '../../../../shared/project-group-types' function group(id: string, overrides: Partial = {}): ProjectGroup { return { diff --git a/src/renderer/src/components/sidebar/project-group-header-drag-commit.ts b/src/renderer/src/components/sidebar/project-group-header-drag-commit.ts index 6f3db2f1260..225b1792c95 100644 --- a/src/renderer/src/components/sidebar/project-group-header-drag-commit.ts +++ b/src/renderer/src/components/sidebar/project-group-header-drag-commit.ts @@ -1,6 +1,6 @@ import { getProjectGroupTabOrderUpdatesForSidebarDrop } from './project-group-header-drop' import type { ProjectGroupHeaderDragSession } from './project-group-header-drag-contract' -import type { ProjectGroup } from '../../../../shared/types' +import type { ProjectGroup } from '../../../../shared/project-group-types' export function commitProjectGroupHeaderDragDrop(args: { session: ProjectGroupHeaderDragSession diff --git a/src/renderer/src/components/sidebar/project-group-header-drag-contract.ts b/src/renderer/src/components/sidebar/project-group-header-drag-contract.ts index bedcefcfc59..9021d9c7636 100644 --- a/src/renderer/src/components/sidebar/project-group-header-drag-contract.ts +++ b/src/renderer/src/components/sidebar/project-group-header-drag-contract.ts @@ -5,7 +5,7 @@ import type { ProjectGroupHeaderDragRect } from './project-group-header-drop' import { REPO_HEADER_ACTION_SELECTOR } from './project-header-drag-contract' -import type { ProjectGroup } from '../../../../shared/types' +import type { ProjectGroup } from '../../../../shared/project-group-types' export type ProjectGroupDragState = { draggingGroupId: string | null diff --git a/src/renderer/src/components/sidebar/project-group-header-drag-start.test.ts b/src/renderer/src/components/sidebar/project-group-header-drag-start.test.ts index ef53dc54175..efb8c7b3959 100644 --- a/src/renderer/src/components/sidebar/project-group-header-drag-start.test.ts +++ b/src/renderer/src/components/sidebar/project-group-header-drag-start.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest' import { createProjectGroupHeaderDragSession } from './project-group-header-drag-start' -import type { ProjectGroup } from '../../../../shared/types' +import type { ProjectGroup } from '../../../../shared/project-group-types' function group(id: string, overrides: Partial = {}): ProjectGroup { return { diff --git a/src/renderer/src/components/sidebar/project-group-header-drag-start.ts b/src/renderer/src/components/sidebar/project-group-header-drag-start.ts index 89eb5bd9dd0..46616c2053d 100644 --- a/src/renderer/src/components/sidebar/project-group-header-drag-start.ts +++ b/src/renderer/src/components/sidebar/project-group-header-drag-start.ts @@ -10,7 +10,7 @@ import { isProjectGroupHeaderDragHandleTarget, type ProjectGroupHeaderDragSession } from './project-group-header-drag-contract' -import type { ProjectGroup } from '../../../../shared/types' +import type { ProjectGroup } from '../../../../shared/project-group-types' export function createProjectGroupHeaderDragSession(args: { event: PointerEvent diff --git a/src/renderer/src/components/sidebar/project-group-header-drop.test.ts b/src/renderer/src/components/sidebar/project-group-header-drop.test.ts index 8fa20c47b49..3e843cf7543 100644 --- a/src/renderer/src/components/sidebar/project-group-header-drop.test.ts +++ b/src/renderer/src/components/sidebar/project-group-header-drop.test.ts @@ -9,7 +9,8 @@ import { mapSidebarProjectGroupDropIndexToSiblingInsertIndex } from './project-group-header-drop' import type { Row } from './worktree-list-groups' -import type { ProjectGroup, Repo } from '../../../../shared/types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' function group(id: string, overrides: Partial = {}): ProjectGroup { return { diff --git a/src/renderer/src/components/sidebar/project-group-header-drop.ts b/src/renderer/src/components/sidebar/project-group-header-drop.ts index 81382372784..73f4415cd00 100644 --- a/src/renderer/src/components/sidebar/project-group-header-drop.ts +++ b/src/renderer/src/components/sidebar/project-group-header-drop.ts @@ -3,7 +3,7 @@ import { type WorktreeSidebarHeaderDropPreview } from './worktree-sidebar-header-drop-preview' import type { Row } from './worktree-list-groups' -import type { ProjectGroup } from '../../../../shared/types' +import type { ProjectGroup } from '../../../../shared/project-group-types' export type ProjectGroupHeaderDragBucketKey = string diff --git a/src/renderer/src/components/sidebar/project-header-drag-commit.test.ts b/src/renderer/src/components/sidebar/project-header-drag-commit.test.ts index 7b5a72def55..32fc47acef3 100644 --- a/src/renderer/src/components/sidebar/project-header-drag-commit.test.ts +++ b/src/renderer/src/components/sidebar/project-header-drag-commit.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest' import { commitProjectHeaderDragDrop } from './project-header-drag-commit' import type { ProjectHeaderDragSession } from './project-header-drag-contract' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' function makeRepo(id: string, overrides: Partial = {}): Repo { return { diff --git a/src/renderer/src/components/sidebar/project-header-drag-commit.ts b/src/renderer/src/components/sidebar/project-header-drag-commit.ts index 2ab63663d71..0a89742097b 100644 --- a/src/renderer/src/components/sidebar/project-header-drag-commit.ts +++ b/src/renderer/src/components/sidebar/project-header-drag-commit.ts @@ -6,7 +6,7 @@ import { mapSidebarRepoDropIndexToAllRepoInsertAt } from './project-header-drop' import type { ProjectHeaderDragSession } from './project-header-drag-contract' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' export function commitProjectHeaderDragDrop(args: { session: ProjectHeaderDragSession diff --git a/src/renderer/src/components/sidebar/project-header-drag-contract.ts b/src/renderer/src/components/sidebar/project-header-drag-contract.ts index 9923c8c0979..f28f34eaffb 100644 --- a/src/renderer/src/components/sidebar/project-header-drag-contract.ts +++ b/src/renderer/src/components/sidebar/project-header-drag-contract.ts @@ -1,7 +1,7 @@ import type { PointerEvent } from 'react' import type { ProjectHeaderDragBucketKey, ProjectHeaderDragRect } from './project-header-drop' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' export type RepoDragState = { draggingRepoId: string | null diff --git a/src/renderer/src/components/sidebar/project-header-drag-start.test.ts b/src/renderer/src/components/sidebar/project-header-drag-start.test.ts index 6fa80295e67..da1a07c26cf 100644 --- a/src/renderer/src/components/sidebar/project-header-drag-start.test.ts +++ b/src/renderer/src/components/sidebar/project-header-drag-start.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest' import { createProjectHeaderDragSession } from './project-header-drag-start' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' function createRepo(id: string, projectGroupId: string | null = null): Repo { return { diff --git a/src/renderer/src/components/sidebar/project-header-drag-start.ts b/src/renderer/src/components/sidebar/project-header-drag-start.ts index 04c306a869e..0a0429f1bf4 100644 --- a/src/renderer/src/components/sidebar/project-header-drag-start.ts +++ b/src/renderer/src/components/sidebar/project-header-drag-start.ts @@ -10,7 +10,7 @@ import { isRepoHeaderActionTarget, type ProjectHeaderDragSession } from './project-header-drag-contract' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' export function createProjectHeaderDragSession(args: { event: PointerEvent diff --git a/src/renderer/src/components/sidebar/project-header-drop.test.ts b/src/renderer/src/components/sidebar/project-header-drop.test.ts index c0382a5f162..92baaf72b73 100644 --- a/src/renderer/src/components/sidebar/project-header-drop.test.ts +++ b/src/renderer/src/components/sidebar/project-header-drop.test.ts @@ -12,7 +12,7 @@ import { mapSidebarRepoDropIndexToAllRepoInsertAt } from './project-header-drop' import type { Row } from './worktree-list-groups' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' describe('getProjectHeaderDragBucketKey', () => { it('uses ungrouped for repos without a project group', () => { diff --git a/src/renderer/src/components/sidebar/project-header-drop.ts b/src/renderer/src/components/sidebar/project-header-drop.ts index 1f7418f805c..c2de72bcaf3 100644 --- a/src/renderer/src/components/sidebar/project-header-drop.ts +++ b/src/renderer/src/components/sidebar/project-header-drop.ts @@ -4,7 +4,7 @@ import { type WorktreeSidebarHeaderDropPreview } from './worktree-sidebar-header-drop-preview' import type { Row } from './worktree-list-groups' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' export type ProjectHeaderDragBucketKey = string diff --git a/src/renderer/src/components/sidebar/prompt-cache-timer-selection.ts b/src/renderer/src/components/sidebar/prompt-cache-timer-selection.ts index 69232f64f6f..d568c1d8508 100644 --- a/src/renderer/src/components/sidebar/prompt-cache-timer-selection.ts +++ b/src/renderer/src/components/sidebar/prompt-cache-timer-selection.ts @@ -1,4 +1,4 @@ -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { parsePaneKey } from '../../../../shared/stable-pane-id' export type PromptCacheCountdownSelection = { diff --git a/src/renderer/src/components/sidebar/remote-file-browser-drive-paths.ts b/src/renderer/src/components/sidebar/remote-file-browser-drive-paths.ts index 52e507aadf2..7e68874dfae 100644 --- a/src/renderer/src/components/sidebar/remote-file-browser-drive-paths.ts +++ b/src/renderer/src/components/sidebar/remote-file-browser-drive-paths.ts @@ -59,4 +59,4 @@ export function driveBreadcrumbPath( const kept = segments.slice(0, endIndex + 1) return kept.length === 0 ? driveRoot : `${driveRoot}${kept.join('\\')}` } -import type { FilesystemPathFlavor } from '../../../../shared/types' +import type { FilesystemPathFlavor } from '../../../../shared/filesystem-entry-types' diff --git a/src/renderer/src/components/sidebar/remote-file-browser-helpers.ts b/src/renderer/src/components/sidebar/remote-file-browser-helpers.ts index a9508a76364..54c0107376d 100644 --- a/src/renderer/src/components/sidebar/remote-file-browser-helpers.ts +++ b/src/renderer/src/components/sidebar/remote-file-browser-helpers.ts @@ -1,7 +1,7 @@ import { translate } from '@/i18n/i18n' import { shouldHandleTextControlPaste } from '@/lib/text-control-paste' import { isClipboardTextByteLengthOverLimit } from '../../../../shared/clipboard-text' -import type { FilesystemPathFlavor } from '../../../../shared/types' +import type { FilesystemPathFlavor } from '../../../../shared/filesystem-entry-types' import { driveRootOf, isDrivePath, diff --git a/src/renderer/src/components/sidebar/rendered-sidebar-worktree-order.test.ts b/src/renderer/src/components/sidebar/rendered-sidebar-worktree-order.test.ts index 0f549db9f23..2cb14d6894d 100644 --- a/src/renderer/src/components/sidebar/rendered-sidebar-worktree-order.test.ts +++ b/src/renderer/src/components/sidebar/rendered-sidebar-worktree-order.test.ts @@ -2,7 +2,10 @@ import { afterEach, describe, expect, it } from 'vitest' import { useAppStore } from '@/store' import { getVisibleWorktreeIds, setVisibleWorktreeIds } from './visible-worktrees' import type { AppState } from '@/store/types' -import type { FolderWorkspace, ProjectGroup, Repo, Worktree } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' const initialState = useAppStore.getInitialState() diff --git a/src/renderer/src/components/sidebar/rendered-sidebar-worktree-order.ts b/src/renderer/src/components/sidebar/rendered-sidebar-worktree-order.ts index fccdc79955c..bc2c038e8d2 100644 --- a/src/renderer/src/components/sidebar/rendered-sidebar-worktree-order.ts +++ b/src/renderer/src/components/sidebar/rendered-sidebar-worktree-order.ts @@ -1,4 +1,4 @@ -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import type { AppState } from '@/store/types' import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-overrides' import { diff --git a/src/renderer/src/components/sidebar/repo-header-create-state.test.ts b/src/renderer/src/components/sidebar/repo-header-create-state.test.ts index a6421b15c5b..6f4a121cb4c 100644 --- a/src/renderer/src/components/sidebar/repo-header-create-state.test.ts +++ b/src/renderer/src/components/sidebar/repo-header-create-state.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { SshConnectionStatus } from '../../../../shared/ssh-types' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { getRepoHeaderCreateState } from './repo-header-create-state' function makeRepo(overrides: Partial = {}): Repo { diff --git a/src/renderer/src/components/sidebar/repo-header-create-state.ts b/src/renderer/src/components/sidebar/repo-header-create-state.ts index 6b369f1e287..6b061932a9e 100644 --- a/src/renderer/src/components/sidebar/repo-header-create-state.ts +++ b/src/renderer/src/components/sidebar/repo-header-create-state.ts @@ -1,5 +1,5 @@ import type { SshConnectionStatus } from '../../../../shared/ssh-types' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { isGitRepoKind } from '../../../../shared/repo-kind' import { getSelectedRepoSshGate } from '../../lib/new-workspace-ssh-gate' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/sidebar/setup-script-prompt-render-state.ts b/src/renderer/src/components/sidebar/setup-script-prompt-render-state.ts index 0e80337118d..2b7a6c79900 100644 --- a/src/renderer/src/components/sidebar/setup-script-prompt-render-state.ts +++ b/src/renderer/src/components/sidebar/setup-script-prompt-render-state.ts @@ -1,4 +1,6 @@ -import type { GlobalSettings, Repo, Worktree } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { SetupScriptPromptInspection } from '@/lib/setup-script-prompt' import { findRepoForHost } from '@/store/slices/repo-host-identity' import { toRuntimeExecutionHostId } from '../../../../shared/execution-host' diff --git a/src/renderer/src/components/sidebar/sidebar-filter-state.test.ts b/src/renderer/src/components/sidebar/sidebar-filter-state.test.ts index e46f6163985..c6945a7f041 100644 --- a/src/renderer/src/components/sidebar/sidebar-filter-state.test.ts +++ b/src/renderer/src/components/sidebar/sidebar-filter-state.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from 'vitest' import { computeClearFilterActions, - isDefaultBranchWorkspace, isSleepingSweepExemptionNarrowingList, sidebarHasActiveFilters } from './visible-worktrees' -import type { Worktree } from '../../../../shared/types' +import { isDefaultBranchWorkspace } from './default-branch-workspace' +import type { Worktree } from '../../../../shared/worktree/types' function makeWorktree(id: string, repoId = 'repo1'): Worktree { return { diff --git a/src/renderer/src/components/sidebar/sidebar-host-options.ts b/src/renderer/src/components/sidebar/sidebar-host-options.ts index dc764112bcc..29d03ec1419 100644 --- a/src/renderer/src/components/sidebar/sidebar-host-options.ts +++ b/src/renderer/src/components/sidebar/sidebar-host-options.ts @@ -1,4 +1,6 @@ -import type { GlobalSettings, Repo, WorkspaceHostScope } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorkspaceHostScope } from '../../../../shared/ui-chrome-types' import { ALL_EXECUTION_HOSTS_SCOPE, LOCAL_EXECUTION_HOST_ID, diff --git a/src/renderer/src/components/sidebar/sidebar-project-drop.ts b/src/renderer/src/components/sidebar/sidebar-project-drop.ts index fc61c049e38..1793bfc9bcc 100644 --- a/src/renderer/src/components/sidebar/sidebar-project-drop.ts +++ b/src/renderer/src/components/sidebar/sidebar-project-drop.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' export type SidebarProjectDropPathResolution = diff --git a/src/renderer/src/components/sidebar/sidebar-workspace-option-items.ts b/src/renderer/src/components/sidebar/sidebar-workspace-option-items.ts index e804131b6ae..717dd2302d3 100644 --- a/src/renderer/src/components/sidebar/sidebar-workspace-option-items.ts +++ b/src/renderer/src/components/sidebar/sidebar-workspace-option-items.ts @@ -1,4 +1,7 @@ -import type { AgentActivityDisplayMode, WorktreeCardProperty } from '../../../../shared/types' +import type { + AgentActivityDisplayMode, + WorktreeCardProperty +} from '../../../../shared/ui-chrome-types' import { TASK_WORKTREE_CARD_PROPERTIES } from '../../../../shared/constants' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/sidebar/smart-attention.test.ts b/src/renderer/src/components/sidebar/smart-attention.test.ts index 8723cf937fc..d04aed078bf 100644 --- a/src/renderer/src/components/sidebar/smart-attention.test.ts +++ b/src/renderer/src/components/sidebar/smart-attention.test.ts @@ -11,7 +11,8 @@ import { resolveAttention, type PaneInput } from './smart-attention' -import type { TerminalLayoutSnapshot, TerminalTab, Worktree } from '../../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' function hookPane(entry: AgentStatusEntry): PaneInput { return { kind: 'hook', entry } diff --git a/src/renderer/src/components/sidebar/smart-attention.ts b/src/renderer/src/components/sidebar/smart-attention.ts index 5238a9091eb..44abd4082b7 100644 --- a/src/renderer/src/components/sidebar/smart-attention.ts +++ b/src/renderer/src/components/sidebar/smart-attention.ts @@ -4,7 +4,8 @@ import { migrationUnsupportedToAgentStatusEntry } from '@/lib/migration-unsuppor import { tabHasLivePty } from '@/lib/tab-has-live-pty' import { resolveRuntimePaneTitleLeafId } from '@/lib/runtime-pane-title-leaf-id' import type { AgentStatus } from '../../../../shared/agent-detection' -import type { TerminalLayoutSnapshot, TerminalTab, Worktree } from '../../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import { AGENT_STATUS_STALE_AFTER_MS, type AgentStateHistoryEntry, diff --git a/src/renderer/src/components/sidebar/smart-sort.test.ts b/src/renderer/src/components/sidebar/smart-sort.test.ts index 75af8fae1cc..60700e59c95 100644 --- a/src/renderer/src/components/sidebar/smart-sort.test.ts +++ b/src/renderer/src/components/sidebar/smart-sort.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from 'vitest' -import type { Repo, TerminalTab, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import { buildWorktreeComparator, CREATE_GRACE_MS, diff --git a/src/renderer/src/components/sidebar/smart-sort.ts b/src/renderer/src/components/sidebar/smart-sort.ts index 84474df4047..b30d60f755a 100644 --- a/src/renderer/src/components/sidebar/smart-sort.ts +++ b/src/renderer/src/components/sidebar/smart-sort.ts @@ -1,4 +1,6 @@ -import type { Worktree, Repo, TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { AgentStatusEntry, MigrationUnsupportedPtyEntry diff --git a/src/renderer/src/components/sidebar/ssh-host-remove-resolution.ts b/src/renderer/src/components/sidebar/ssh-host-remove-resolution.ts index 4949ab3db6c..3c0d41d8cc6 100644 --- a/src/renderer/src/components/sidebar/ssh-host-remove-resolution.ts +++ b/src/renderer/src/components/sidebar/ssh-host-remove-resolution.ts @@ -1,4 +1,5 @@ -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { SshConnectionState } from '../../../../shared/ssh-types' /** diff --git a/src/renderer/src/components/sidebar/ssh-workspace-forget-resolution.ts b/src/renderer/src/components/sidebar/ssh-workspace-forget-resolution.ts index 72243c5f370..6a723349809 100644 --- a/src/renderer/src/components/sidebar/ssh-workspace-forget-resolution.ts +++ b/src/renderer/src/components/sidebar/ssh-workspace-forget-resolution.ts @@ -1,4 +1,4 @@ -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import type { SshConnectionState } from '../../../../shared/ssh-types' import { isRuntimeOwnedSshTargetId } from '../../../../shared/execution-host' diff --git a/src/renderer/src/components/sidebar/track-nested-folder-open.ts b/src/renderer/src/components/sidebar/track-nested-folder-open.ts index add9c4aae19..19871701d3f 100644 --- a/src/renderer/src/components/sidebar/track-nested-folder-open.ts +++ b/src/renderer/src/components/sidebar/track-nested-folder-open.ts @@ -3,7 +3,7 @@ import { buildNestedRepoImportActionTelemetry, type NestedRepoTelemetryRuntimeKind } from '../../../../shared/nested-repo-telemetry' -import type { NestedRepoScanResult } from '../../../../shared/types' +import type { NestedRepoScanResult } from '../../../../shared/project-group-types' export function trackNestedFolderOpen(args: { attemptId: string | null diff --git a/src/renderer/src/components/sidebar/use-add-repo-remote-nested-scan.ts b/src/renderer/src/components/sidebar/use-add-repo-remote-nested-scan.ts index c57557f0cc1..436a0618ff0 100644 --- a/src/renderer/src/components/sidebar/use-add-repo-remote-nested-scan.ts +++ b/src/renderer/src/components/sidebar/use-add-repo-remote-nested-scan.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react' import { track } from '@/lib/telemetry' import { buildNestedRepoScanTelemetry } from '../../../../shared/nested-repo-telemetry' -import type { NestedRepoScanResult } from '../../../../shared/types' +import type { NestedRepoScanResult } from '../../../../shared/project-group-types' export function useAddRepoRemoteNestedScan({ setActiveNestedScanId, diff --git a/src/renderer/src/components/sidebar/use-delete-worktree-status-hydration.ts b/src/renderer/src/components/sidebar/use-delete-worktree-status-hydration.ts index 2a88a430c50..02972d6a907 100644 --- a/src/renderer/src/components/sidebar/use-delete-worktree-status-hydration.ts +++ b/src/renderer/src/components/sidebar/use-delete-worktree-status-hydration.ts @@ -3,7 +3,8 @@ import { useAppStore } from '@/store' import { getConnectionId } from '@/lib/connection-context' import { getSettingsForWorktreeRuntimeOwner } from '@/lib/worktree-runtime-owner' import { getRuntimeGitStatus } from '@/runtime/runtime-git-client' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { isFolderWorkspaceDelete } from './delete-worktree-dialog-copy' export function useDeleteWorktreeStatusHydration({ diff --git a/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts b/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts index 180ef7d6daf..1262dc92edb 100644 --- a/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts +++ b/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts @@ -1,6 +1,7 @@ import { useMemo } from 'react' import { useAppStore } from '@/store' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { computeVisibleWorktreeIds } from './visible-worktrees' import { getWorktreeIdsWithLiveAgent } from '@/lib/worktree-activity-state' import { getSettingsFocusedExecutionHostId } from '../../../../shared/execution-host' diff --git a/src/renderer/src/components/sidebar/use-workspace-kanban-card-pointer-drag.ts b/src/renderer/src/components/sidebar/use-workspace-kanban-card-pointer-drag.ts index 19a67a31663..d03add3da13 100644 --- a/src/renderer/src/components/sidebar/use-workspace-kanban-card-pointer-drag.ts +++ b/src/renderer/src/components/sidebar/use-workspace-kanban-card-pointer-drag.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef } from 'react' import type React from 'react' -import type { WorkspaceStatus, Worktree } from '../../../../shared/types' +import type { WorkspaceStatus, Worktree } from '../../../../shared/worktree/types' import { CARD_SELECTOR, getCardDropTarget, diff --git a/src/renderer/src/components/sidebar/use-workspace-kanban-create-worktree.ts b/src/renderer/src/components/sidebar/use-workspace-kanban-create-worktree.ts index aae79fc9d8b..bd218580cb9 100644 --- a/src/renderer/src/components/sidebar/use-workspace-kanban-create-worktree.ts +++ b/src/renderer/src/components/sidebar/use-workspace-kanban-create-worktree.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react' import { useAppStore } from '@/store' -import type { WorkspaceStatus } from '../../../../shared/types' +import type { WorkspaceStatus } from '../../../../shared/worktree/types' export function useWorkspaceKanbanCreateWorktree(): { canCreateWorktree: boolean diff --git a/src/renderer/src/components/sidebar/use-workspace-kanban-search.ts b/src/renderer/src/components/sidebar/use-workspace-kanban-search.ts index df5f3e0455f..e782b419ab3 100644 --- a/src/renderer/src/components/sidebar/use-workspace-kanban-search.ts +++ b/src/renderer/src/components/sidebar/use-workspace-kanban-search.ts @@ -1,6 +1,7 @@ import { useCallback, useDeferredValue, useMemo, useState } from 'react' import { isWorktreePaletteQueryTooLarge } from '@/lib/worktree-palette-query-bounds' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { matchWorkspaceBoardWorktrees } from './workspace-kanban-search' function areWorktreeIdSetsEqual(a: ReadonlySet, b: ReadonlySet): boolean { diff --git a/src/renderer/src/components/sidebar/use-workspace-kanban-selection.test.tsx b/src/renderer/src/components/sidebar/use-workspace-kanban-selection.test.tsx index 7d20d7c7523..abf1067895e 100644 --- a/src/renderer/src/components/sidebar/use-workspace-kanban-selection.test.tsx +++ b/src/renderer/src/components/sidebar/use-workspace-kanban-selection.test.tsx @@ -3,7 +3,7 @@ import React, { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { useWorkspaceKanbanSelection } from './use-workspace-kanban-selection' ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true diff --git a/src/renderer/src/components/sidebar/use-workspace-kanban-selection.ts b/src/renderer/src/components/sidebar/use-workspace-kanban-selection.ts index 54efeccdec3..f1a20a57147 100644 --- a/src/renderer/src/components/sidebar/use-workspace-kanban-selection.ts +++ b/src/renderer/src/components/sidebar/use-workspace-kanban-selection.ts @@ -1,5 +1,5 @@ import React, { useCallback, useMemo, useState } from 'react' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { areWorktreeSelectionsEqual, getWorktreeSelectionIntent, diff --git a/src/renderer/src/components/sidebar/use-workspace-status-drop.ts b/src/renderer/src/components/sidebar/use-workspace-status-drop.ts index eac45bb668d..5a5b441521d 100644 --- a/src/renderer/src/components/sidebar/use-workspace-status-drop.ts +++ b/src/renderer/src/components/sidebar/use-workspace-status-drop.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' import type React from 'react' -import type { WorkspaceStatus } from '../../../../shared/types' +import type { WorkspaceStatus } from '../../../../shared/worktree/types' import { hasWorkspaceDragData, readWorkspaceDragDataIds } from './workspace-status' const WORKSPACE_STATUS_DROP_TARGET = '[data-workspace-status-drop-target]' diff --git a/src/renderer/src/components/sidebar/use-worktree-activity-status.test.tsx b/src/renderer/src/components/sidebar/use-worktree-activity-status.test.tsx index aac20f34b97..17ff8d18cd3 100644 --- a/src/renderer/src/components/sidebar/use-worktree-activity-status.test.tsx +++ b/src/renderer/src/components/sidebar/use-worktree-activity-status.test.tsx @@ -2,7 +2,7 @@ import { renderToStaticMarkup } from 'react-dom/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' import { makePaneKey } from '../../../../shared/stable-pane-id' -import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' import { useWorktreeActivityStatus } from './use-worktree-activity-status' const LEAF_ID = '11111111-1111-4111-8111-111111111111' diff --git a/src/renderer/src/components/sidebar/use-worktree-card-linked-details.ts b/src/renderer/src/components/sidebar/use-worktree-card-linked-details.ts index fcf91b71a93..a2ec8e0774e 100644 --- a/src/renderer/src/components/sidebar/use-worktree-card-linked-details.ts +++ b/src/renderer/src/components/sidebar/use-worktree-card-linked-details.ts @@ -2,7 +2,8 @@ import React from 'react' import { translate } from '@/i18n/i18n' import { useAppStore } from '@/store' -import type { IssueInfo, LinearIssue } from '../../../../shared/types' +import type { IssueInfo } from '../../../../shared/github/pull-request-types' +import type { LinearIssue } from '../../../../shared/linear/issue-types' import { getWorktreeCardJiraIssueDisplay } from './worktree-card-jira-issue-display' import type { WorktreeCardIssueDisplay } from './WorktreeCardMeta' import { diff --git a/src/renderer/src/components/sidebar/use-worktree-card-secondary-details.store-subscriptions.test.tsx b/src/renderer/src/components/sidebar/use-worktree-card-secondary-details.store-subscriptions.test.tsx index 693dba8f412..146f4e10916 100644 --- a/src/renderer/src/components/sidebar/use-worktree-card-secondary-details.store-subscriptions.test.tsx +++ b/src/renderer/src/components/sidebar/use-worktree-card-secondary-details.store-subscriptions.test.tsx @@ -5,7 +5,8 @@ import { createRoot, type Root } from 'react-dom/client' import { afterEach, describe, expect, it } from 'vitest' import { useAppStore } from '@/store' import { readStoreListenerCount } from '@/store/store-listener-census' -import type { GlobalSettings, Worktree } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Worktree } from '../../../../shared/worktree/types' import { usePromptCacheCountdownStartedAt } from './CacheTimer' import { useWorktreeCardSecondaryDetails } from './use-worktree-card-secondary-details' import { useWorktreeAgentRows } from './useWorktreeAgentRows' diff --git a/src/renderer/src/components/sidebar/use-worktree-card-secondary-details.ts b/src/renderer/src/components/sidebar/use-worktree-card-secondary-details.ts index ebc7cf483c8..64e9da831a6 100644 --- a/src/renderer/src/components/sidebar/use-worktree-card-secondary-details.ts +++ b/src/renderer/src/components/sidebar/use-worktree-card-secondary-details.ts @@ -1,6 +1,6 @@ import React, { useCallback } from 'react' -import type { GitHubWorkItem } from '../../../../shared/types' +import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' import { hasWorktreeCardDetails } from './WorktreeCardMeta' import { usePromptCacheCountdownStartedAt } from './CacheTimer' import { useWorktreeAgentRows } from './useWorktreeAgentRows' diff --git a/src/renderer/src/components/sidebar/use-worktree-meta-workspace.ts b/src/renderer/src/components/sidebar/use-worktree-meta-workspace.ts index 336725efc1e..6e581d9c77e 100644 --- a/src/renderer/src/components/sidebar/use-worktree-meta-workspace.ts +++ b/src/renderer/src/components/sidebar/use-worktree-meta-workspace.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react' import { useAppStore } from '@/store' import { findIndexedWorktreeOwner } from '@/lib/worktree-runtime-owner-index' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { parseWorkspaceKey } from '../../../../shared/workspace-scope' import { folderWorkspaceToWorktree } from '../../../../shared/folder-workspace-worktree' import type { IssueLinkProvider } from '../../../../shared/issue-link-input' diff --git a/src/renderer/src/components/sidebar/useAddRepoCloneFlow.test.ts b/src/renderer/src/components/sidebar/useAddRepoCloneFlow.test.ts index 57958126563..b9cd2bdd976 100644 --- a/src/renderer/src/components/sidebar/useAddRepoCloneFlow.test.ts +++ b/src/renderer/src/components/sidebar/useAddRepoCloneFlow.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as ReactModule from 'react' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' const mocks = vi.hoisted(() => ({ stateValues: [] as unknown[], diff --git a/src/renderer/src/components/sidebar/useAddRepoCloneFlow.ts b/src/renderer/src/components/sidebar/useAddRepoCloneFlow.ts index 24d27523942..f181929ddff 100644 --- a/src/renderer/src/components/sidebar/useAddRepoCloneFlow.ts +++ b/src/renderer/src/components/sidebar/useAddRepoCloneFlow.ts @@ -3,7 +3,7 @@ import { toast } from 'sonner' import { useAppStore } from '@/store' import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' import type { AddRepoExistingWorkspaceSource } from '../../../../shared/telemetry-events' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { getCloneDestinationAutoFill } from './clone-defaults' import type { AddRepoDialogStep } from './add-repo-dialog-types' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.test.ts b/src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.test.ts index acc96258cb6..e3759cc13a0 100644 --- a/src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.test.ts +++ b/src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as ReactModule from 'react' -import type { NestedRepoScanResult, Repo } from '../../../../shared/types' +import type { NestedRepoScanResult } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' vi.mock('react', async (importOriginal) => { const actual = await importOriginal() diff --git a/src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.ts b/src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.ts index 859698f19f8..cf4c6b13ba5 100644 --- a/src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.ts +++ b/src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.ts @@ -8,7 +8,8 @@ import { type NestedRepoTelemetryRuntimeKind } from '../../../../shared/nested-repo-telemetry' import type { AddRepoExistingWorkspaceSource } from '../../../../shared/telemetry-events' -import type { NestedRepoScanResult, Repo } from '../../../../shared/types' +import type { NestedRepoScanResult } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' import type { WorktreeFetchOptions } from '@/store/slices/worktree-helpers' import type { RepoSlice } from '@/store/slices/repos' import { createNestedRepoScanId } from './add-repo-dialog-types' diff --git a/src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.test.ts b/src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.test.ts index 6f56f454edd..4ed1f739f1a 100644 --- a/src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.test.ts +++ b/src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.test.ts @@ -1,6 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as ReactModule from 'react' -import type { NestedRepoScanResult, ProjectGroupImportResult, Repo } from '../../../../shared/types' +import type { + NestedRepoScanResult, + ProjectGroupImportResult +} from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' vi.mock('react', async (importOriginal) => { const actual = await importOriginal() diff --git a/src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.ts b/src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.ts index c0c505cc97c..d52109bf0d7 100644 --- a/src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.ts +++ b/src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.ts @@ -10,7 +10,10 @@ import { type NestedRepoTelemetryRuntimeKind } from '../../../../shared/nested-repo-telemetry' import type { AddRepoExistingWorkspaceSource } from '../../../../shared/telemetry-events' -import type { NestedRepoScanResult, ProjectGroupImportResult } from '../../../../shared/types' +import type { + NestedRepoScanResult, + ProjectGroupImportResult +} from '../../../../shared/project-group-types' import type { WorktreeFetchOptions } from '@/store/slices/worktree-helpers' import { translate } from '@/i18n/i18n' import { worktreeRefreshOptions, type CapturedRuntimeOwner } from './add-repo-runtime-owner' diff --git a/src/renderer/src/components/sidebar/useAddRepoNestedReviewController.ts b/src/renderer/src/components/sidebar/useAddRepoNestedReviewController.ts index a5f47153736..f015883762b 100644 --- a/src/renderer/src/components/sidebar/useAddRepoNestedReviewController.ts +++ b/src/renderer/src/components/sidebar/useAddRepoNestedReviewController.ts @@ -1,6 +1,6 @@ import type { Dispatch, SetStateAction } from 'react' import type { AddRepoExistingWorkspaceSource } from '../../../../shared/telemetry-events' -import type { ProjectGroupImportResult } from '../../../../shared/types' +import type { ProjectGroupImportResult } from '../../../../shared/project-group-types' import type { WorktreeFetchOptions } from '@/store/slices/worktree-helpers' import { useAddRepoNestedImportFlow } from './useAddRepoNestedImportFlow' import { useAddRepoNestedReviewState } from './useAddRepoNestedReviewState' diff --git a/src/renderer/src/components/sidebar/useAddRepoNestedReviewState.ts b/src/renderer/src/components/sidebar/useAddRepoNestedReviewState.ts index a8f41592fbd..8e44b5a4076 100644 --- a/src/renderer/src/components/sidebar/useAddRepoNestedReviewState.ts +++ b/src/renderer/src/components/sidebar/useAddRepoNestedReviewState.ts @@ -1,6 +1,6 @@ import { useCallback, useRef, useState, type Dispatch, type SetStateAction } from 'react' import type { NestedRepoTelemetryRuntimeKind } from '../../../../shared/nested-repo-telemetry' -import type { NestedRepoScanResult } from '../../../../shared/types' +import type { NestedRepoScanResult } from '../../../../shared/project-group-types' import { defaultProjectGroupNameForPath, type AddRepoDialogStep } from './add-repo-dialog-types' type ShowNestedRepoReviewArgs = { diff --git a/src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts b/src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts index 53d2b334112..2d0135bb420 100644 --- a/src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts +++ b/src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as ReactModule from 'react' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' const mocks = vi.hoisted(() => ({ stateValues: [] as unknown[], diff --git a/src/renderer/src/components/sidebar/useAddRepoServerPathFlow.ts b/src/renderer/src/components/sidebar/useAddRepoServerPathFlow.ts index a02bdbcf3f4..9810be00be8 100644 --- a/src/renderer/src/components/sidebar/useAddRepoServerPathFlow.ts +++ b/src/renderer/src/components/sidebar/useAddRepoServerPathFlow.ts @@ -8,7 +8,8 @@ import { type NestedRepoTelemetryRuntimeKind } from '../../../../shared/nested-repo-telemetry' import type { AddRepoExistingWorkspaceSource } from '../../../../shared/telemetry-events' -import type { NestedRepoScanResult, Repo } from '../../../../shared/types' +import type { NestedRepoScanResult } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' import type { WorktreeFetchOptions } from '@/store/slices/worktree-helpers' import { createNestedRepoScanId } from './add-repo-dialog-types' import { worktreeRefreshOptions } from './add-repo-runtime-owner' diff --git a/src/renderer/src/components/sidebar/useCreateRepo.default-checkout.test.ts b/src/renderer/src/components/sidebar/useCreateRepo.default-checkout.test.ts index 832ce5b26dc..117e09d6fbe 100644 --- a/src/renderer/src/components/sidebar/useCreateRepo.default-checkout.test.ts +++ b/src/renderer/src/components/sidebar/useCreateRepo.default-checkout.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as ReactModule from 'react' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' const mocks = vi.hoisted(() => ({ stateValues: [] as unknown[], diff --git a/src/renderer/src/components/sidebar/useCreateRepo.ts b/src/renderer/src/components/sidebar/useCreateRepo.ts index a9c6c8c6525..103fb59f966 100644 --- a/src/renderer/src/components/sidebar/useCreateRepo.ts +++ b/src/renderer/src/components/sidebar/useCreateRepo.ts @@ -8,7 +8,7 @@ import { activateAndRevealWorktree } from '@/lib/worktree-activation' import { markOnboardingProjectAdded } from '@/lib/onboarding-project-checklist' import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' import { isGitRepoKind } from '../../../../shared/repo-kind' -import type { Repo } from '../../../../shared/types' +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' diff --git a/src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.test.tsx b/src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.test.tsx index cb4e364d1b2..672d7130b5b 100644 --- a/src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.test.tsx +++ b/src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.test.tsx @@ -4,7 +4,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAppStore } from '@/store' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { useSetupScriptPromptRevalidation } from './useSetupScriptPromptRevalidation' import type { SetupScriptPromptState } from './setup-script-prompt-render-state' import { getRepoHostIdentityForParts } from '@/store/slices/repo-host-identity' diff --git a/src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.ts b/src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.ts index ce6a5e78fce..867b21052e7 100644 --- a/src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.ts +++ b/src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.ts @@ -1,7 +1,7 @@ import { useEffect, useRef } from 'react' import { useAppStore } from '@/store' import { isGitRepoKind } from '../../../../shared/repo-kind' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { getRepoHostIdentity } from '@/store/slices/repo-host-identity' import { getRepoExecutionHostId, parseExecutionHostId } from '../../../../shared/execution-host' import type { SetupScriptPromptState } from './setup-script-prompt-render-state' diff --git a/src/renderer/src/components/sidebar/useWorktreeAgentRows.test.ts b/src/renderer/src/components/sidebar/useWorktreeAgentRows.test.ts index 913e197691a..bd410ddc2a2 100644 --- a/src/renderer/src/components/sidebar/useWorktreeAgentRows.test.ts +++ b/src/renderer/src/components/sidebar/useWorktreeAgentRows.test.ts @@ -3,7 +3,7 @@ import { AGENT_STATUS_STALE_AFTER_MS, type AgentStatusEntry } from '../../../../shared/agent-status-types' -import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' import type { RetainedAgentEntry } from '@/store/slices/agent-status' import { applyAgentRowLineage } from '@/components/dashboard/agent-row-lineage' import { makePaneKey } from '../../../../shared/stable-pane-id' diff --git a/src/renderer/src/components/sidebar/visible-worktree-activity-inputs.test.ts b/src/renderer/src/components/sidebar/visible-worktree-activity-inputs.test.ts index c96aa38d50d..c18c3cbceed 100644 --- a/src/renderer/src/components/sidebar/visible-worktree-activity-inputs.test.ts +++ b/src/renderer/src/components/sidebar/visible-worktree-activity-inputs.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { BrowserWorkspace, TerminalTab } from '../../../../shared/types' +import type { BrowserWorkspace } from '../../../../shared/browser-workspace-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { getVisibleWorktreeBrowserActivityTabs, getVisibleWorktreeTerminalActivityTabs diff --git a/src/renderer/src/components/sidebar/visible-worktree-activity-inputs.ts b/src/renderer/src/components/sidebar/visible-worktree-activity-inputs.ts index d843e8d20c1..683b984cca8 100644 --- a/src/renderer/src/components/sidebar/visible-worktree-activity-inputs.ts +++ b/src/renderer/src/components/sidebar/visible-worktree-activity-inputs.ts @@ -1,4 +1,5 @@ -import type { BrowserWorkspace, TerminalTab } from '../../../../shared/types' +import type { BrowserWorkspace } from '../../../../shared/browser-workspace-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' export type TerminalActivityTab = Pick export type BrowserActivityTab = Pick diff --git a/src/renderer/src/components/sidebar/visible-worktrees.test.ts b/src/renderer/src/components/sidebar/visible-worktrees.test.ts index 07e2d32a4b3..597d4b91e61 100644 --- a/src/renderer/src/components/sidebar/visible-worktrees.test.ts +++ b/src/renderer/src/components/sidebar/visible-worktrees.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it } from 'vitest' import { computeVisibleWorktreeIds } from './visible-worktrees' import { getPairedDeviceIdsByEnvironment } from './workspace-creator-visibility' -import type { Repo, TerminalTab, Worktree, WorktreeLineage } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' function makeTab(id: string, worktreeId: string, ptyId: string | null): TerminalTab { diff --git a/src/renderer/src/components/sidebar/visible-worktrees.ts b/src/renderer/src/components/sidebar/visible-worktrees.ts index 927033c5bfa..270809d49da 100644 --- a/src/renderer/src/components/sidebar/visible-worktrees.ts +++ b/src/renderer/src/components/sidebar/visible-worktrees.ts @@ -1,4 +1,7 @@ -import type { Worktree, Repo, TerminalTab, WorktreeLineage } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { buildWorktreeComparator, sortWorktreesSmart } from './smart-sort' import { getWorktreeIdsWithLiveAgent, isInactiveWorkspace } from '@/lib/worktree-activity-state' import { useAppStore } from '@/store' @@ -28,8 +31,6 @@ import { } from './workspace-creator-visibility' import { isDefaultBranchWorkspace } from './default-branch-workspace' -export { isDefaultBranchWorkspace } from './default-branch-workspace' - /** * Whether the "Hide sleeping" sweep must keep this row (#8873). * diff --git a/src/renderer/src/components/sidebar/workspace-board-task-status-sync.test.ts b/src/renderer/src/components/sidebar/workspace-board-task-status-sync.test.ts index 17c0c2d2f14..880b03289d9 100644 --- a/src/renderer/src/components/sidebar/workspace-board-task-status-sync.test.ts +++ b/src/renderer/src/components/sidebar/workspace-board-task-status-sync.test.ts @@ -1,10 +1,7 @@ import { describe, expect, it, vi } from 'vitest' -import type { - LinearIssue, - LinearWorkflowState, - WorkspaceStatusDefinition, - Worktree -} from '../../../../shared/types' +import type { LinearIssue } from '../../../../shared/linear/issue-types' +import type { LinearWorkflowState } from '../../../../shared/linear/workspace-types' +import type { WorkspaceStatusDefinition, Worktree } from '../../../../shared/worktree/types' import type { LinearMutationResult } from '@/runtime/runtime-linear-client' import { getWorkspaceBoardTaskStatusSyncRequest, diff --git a/src/renderer/src/components/sidebar/workspace-board-task-status-sync.ts b/src/renderer/src/components/sidebar/workspace-board-task-status-sync.ts index 15409ddcfe6..505459a99fd 100644 --- a/src/renderer/src/components/sidebar/workspace-board-task-status-sync.ts +++ b/src/renderer/src/components/sidebar/workspace-board-task-status-sync.ts @@ -5,13 +5,13 @@ import { type LinearMutationResult, type RuntimeLinearSettings } from '@/runtime/runtime-linear-client' +import type { LinearIssue } from '../../../../shared/linear/issue-types' +import type { LinearWorkflowState } from '../../../../shared/linear/workspace-types' import type { - LinearIssue, - LinearWorkflowState, WorkspaceStatus, WorkspaceStatusDefinition, Worktree -} from '../../../../shared/types' +} from '../../../../shared/worktree/types' import { getWorkspaceStatus } from '../../../../shared/workspace-statuses' export type WorkspaceBoardTaskStatusSyncResult = { diff --git a/src/renderer/src/components/sidebar/workspace-creator-visibility.test.ts b/src/renderer/src/components/sidebar/workspace-creator-visibility.test.ts index 1cbd5eedb08..f21c61c5279 100644 --- a/src/renderer/src/components/sidebar/workspace-creator-visibility.test.ts +++ b/src/renderer/src/components/sidebar/workspace-creator-visibility.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { FolderWorkspace } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' import { filterFolderWorkspacesFromOtherDevices } from './workspace-creator-visibility' function makeFolderWorkspace( diff --git a/src/renderer/src/components/sidebar/workspace-creator-visibility.ts b/src/renderer/src/components/sidebar/workspace-creator-visibility.ts index 1cc4e208cfd..ca0b929a40c 100644 --- a/src/renderer/src/components/sidebar/workspace-creator-visibility.ts +++ b/src/renderer/src/components/sidebar/workspace-creator-visibility.ts @@ -1,6 +1,7 @@ import type { RuntimeStatus } from '../../../../shared/runtime-types' import type { PublicKnownRuntimeEnvironment } from '../../../../shared/runtime-environments' -import type { FolderWorkspace, Worktree } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { Worktree } from '../../../../shared/worktree/types' import { folderWorkspaceToWorktree } from '../../../../shared/folder-workspace-worktree' import { normalizeWorkspaceCreatorProvenance } from '../../../../shared/workspace-creator-provenance' diff --git a/src/renderer/src/components/sidebar/workspace-delete-lineage.test.ts b/src/renderer/src/components/sidebar/workspace-delete-lineage.test.ts index b5679409cc5..b368cb93133 100644 --- a/src/renderer/src/components/sidebar/workspace-delete-lineage.test.ts +++ b/src/renderer/src/components/sidebar/workspace-delete-lineage.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { LOCAL_EXECUTION_HOST_ID, toSshExecutionHostId } from '../../../../shared/execution-host' -import type { Worktree, WorktreeLineage } from '../../../../shared/types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getWorkspaceDeleteLineage } from './workspace-delete-lineage' function makeWorktree(id: string, path: string): Worktree { diff --git a/src/renderer/src/components/sidebar/workspace-delete-lineage.ts b/src/renderer/src/components/sidebar/workspace-delete-lineage.ts index ca6c78c1b67..55d62fc71fd 100644 --- a/src/renderer/src/components/sidebar/workspace-delete-lineage.ts +++ b/src/renderer/src/components/sidebar/workspace-delete-lineage.ts @@ -1,4 +1,5 @@ -import type { Worktree, WorktreeLineage } from '../../../../shared/types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getProjectedWorktreeLineageChildrenByParentId } from './worktree-lineage-projection' type WorkspaceDeleteLineage = { diff --git a/src/renderer/src/components/sidebar/workspace-kanban-card-pointer-drag-dom.ts b/src/renderer/src/components/sidebar/workspace-kanban-card-pointer-drag-dom.ts index dd59d430ab9..768fd3cd2bd 100644 --- a/src/renderer/src/components/sidebar/workspace-kanban-card-pointer-drag-dom.ts +++ b/src/renderer/src/components/sidebar/workspace-kanban-card-pointer-drag-dom.ts @@ -1,4 +1,4 @@ -import type { WorkspaceStatus } from '../../../../shared/types' +import type { WorkspaceStatus } from '../../../../shared/worktree/types' import { resolveWorkspaceKanbanVirtualLaneDropIndex, resolveWorkspaceKanbanVirtualLaneDropIndicatorY diff --git a/src/renderer/src/components/sidebar/workspace-kanban-search.test.ts b/src/renderer/src/components/sidebar/workspace-kanban-search.test.ts index d5b42c45cde..0eb1d42d377 100644 --- a/src/renderer/src/components/sidebar/workspace-kanban-search.test.ts +++ b/src/renderer/src/components/sidebar/workspace-kanban-search.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { WORKTREE_PALETTE_QUERY_MAX_BYTES } from '@/lib/worktree-palette-query-bounds' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { buildWorkspaceKanbanLaneViews, matchWorkspaceBoardWorktrees diff --git a/src/renderer/src/components/sidebar/workspace-kanban-search.ts b/src/renderer/src/components/sidebar/workspace-kanban-search.ts index a7f8b0840d0..c76fb230610 100644 --- a/src/renderer/src/components/sidebar/workspace-kanban-search.ts +++ b/src/renderer/src/components/sidebar/workspace-kanban-search.ts @@ -1,6 +1,7 @@ import { isWorktreePaletteQueryTooLarge } from '@/lib/worktree-palette-query-bounds' import { searchWorktrees, type PaletteMatchedField } from '@/lib/worktree-palette-search' -import type { Repo, WorkspaceStatus, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorkspaceStatus, Worktree } from '../../../../shared/worktree/types' export type WorkspaceKanbanLaneView = { items: readonly Worktree[] diff --git a/src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.test.ts b/src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.test.ts index 5a4a3d62b4c..bdd9928ff17 100644 --- a/src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.test.ts +++ b/src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { WorkspaceStatusDefinition, Worktree } from '../../../../shared/types' +import type { WorkspaceStatusDefinition, Worktree } from '../../../../shared/worktree/types' import { serializeWorkspaceLaneFullIds } from './workspace-kanban-filtered-drop-index' import { buildWorkspaceKanbanSidebarDropUpdates, diff --git a/src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.ts b/src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.ts index 8475d91cdd8..53600b5b20b 100644 --- a/src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.ts +++ b/src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.ts @@ -1,9 +1,9 @@ +import type { WorktreeMeta } from '../../../../shared/worktree/meta-types' import type { WorkspaceStatus, WorkspaceStatusDefinition, - Worktree, - WorktreeMeta -} from '../../../../shared/types' + Worktree +} from '../../../../shared/worktree/types' import { parseWorkspaceLaneFullIds, resolveFullLaneDropIndex diff --git a/src/renderer/src/components/sidebar/workspace-kanban-worktree-groups.test.ts b/src/renderer/src/components/sidebar/workspace-kanban-worktree-groups.test.ts index 00fbbe84067..e705a12f9fe 100644 --- a/src/renderer/src/components/sidebar/workspace-kanban-worktree-groups.test.ts +++ b/src/renderer/src/components/sidebar/workspace-kanban-worktree-groups.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { groupWorkspaceKanbanWorktrees } from './workspace-kanban-worktree-groups' function worktree({ diff --git a/src/renderer/src/components/sidebar/workspace-kanban-worktree-groups.ts b/src/renderer/src/components/sidebar/workspace-kanban-worktree-groups.ts index 1f85cf3682c..b26ba6e7367 100644 --- a/src/renderer/src/components/sidebar/workspace-kanban-worktree-groups.ts +++ b/src/renderer/src/components/sidebar/workspace-kanban-worktree-groups.ts @@ -1,4 +1,8 @@ -import type { WorkspaceStatus, WorkspaceStatusDefinition, Worktree } from '../../../../shared/types' +import type { + WorkspaceStatus, + WorkspaceStatusDefinition, + Worktree +} from '../../../../shared/worktree/types' import type { SortBy } from './smart-sort' import { getWorkspaceStatus } from './workspace-status' import { compareWorktreeDisplayName } from '@/lib/worktree-display-name-order' diff --git a/src/renderer/src/components/sidebar/workspace-lineage-menu-actions.test.ts b/src/renderer/src/components/sidebar/workspace-lineage-menu-actions.test.ts index cc9d4646584..f4a06ccfada 100644 --- a/src/renderer/src/components/sidebar/workspace-lineage-menu-actions.test.ts +++ b/src/renderer/src/components/sidebar/workspace-lineage-menu-actions.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { Worktree, WorktreeLineage } from '../../../../shared/types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getWorkspaceLineageMenuActions, hasSleepableWorkspaceActivity diff --git a/src/renderer/src/components/sidebar/workspace-lineage-menu-actions.ts b/src/renderer/src/components/sidebar/workspace-lineage-menu-actions.ts index 64dd1260534..1ff8c20ea6c 100644 --- a/src/renderer/src/components/sidebar/workspace-lineage-menu-actions.ts +++ b/src/renderer/src/components/sidebar/workspace-lineage-menu-actions.ts @@ -1,6 +1,7 @@ import { useMemo } from 'react' import { tabHasLivePty } from '@/lib/tab-has-live-pty' -import type { Worktree, WorktreeLineage } from '../../../../shared/types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getWorkspaceDeleteLineage } from './workspace-delete-lineage' type WorkspaceActivityMaps = { diff --git a/src/renderer/src/components/sidebar/workspace-status.ts b/src/renderer/src/components/sidebar/workspace-status.ts index a35f7dff4fd..5229831dcac 100644 --- a/src/renderer/src/components/sidebar/workspace-status.ts +++ b/src/renderer/src/components/sidebar/workspace-status.ts @@ -1,6 +1,6 @@ import React from 'react' import { CircleDot } from 'lucide-react' -import type { WorkspaceStatus, WorkspaceStatusDefinition } from '../../../../shared/types' +import type { WorkspaceStatus, WorkspaceStatusDefinition } from '../../../../shared/worktree/types' import { DEFAULT_WORKSPACE_STATUS_COLOR_ID, DEFAULT_WORKSPACE_STATUS_ICON_ID, diff --git a/src/renderer/src/components/sidebar/worktree-agent-activity-summary.test.ts b/src/renderer/src/components/sidebar/worktree-agent-activity-summary.test.ts index bc1f06cc7b5..ed086d7c94e 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-activity-summary.test.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-activity-summary.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { shallow } from 'zustand/shallow' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' import { makePaneKey } from '../../../../shared/stable-pane-id' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { selectWorktreeAgentActivitySummary, type AgentActivityInput diff --git a/src/renderer/src/components/sidebar/worktree-agent-freshness-selector.test.ts b/src/renderer/src/components/sidebar/worktree-agent-freshness-selector.test.ts index b52b0dd9024..098c7eadcb4 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-freshness-selector.test.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-freshness-selector.test.ts @@ -4,7 +4,7 @@ import { type AgentStatusEntry } from '../../../../shared/agent-status-types' import { makePaneKey } from '../../../../shared/stable-pane-id' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { createWorktreeAgentFreshnessSelector } from './worktree-agent-freshness-selector' const WT_1_PANE = makePaneKey('tab-1', '11111111-1111-4111-8111-111111111111') diff --git a/src/renderer/src/components/sidebar/worktree-agent-orchestration-batch.test.ts b/src/renderer/src/components/sidebar/worktree-agent-orchestration-batch.test.ts index 808664cbc7c..665cbb07896 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-orchestration-batch.test.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-orchestration-batch.test.ts @@ -5,7 +5,7 @@ import type { AgentStatusOrchestrationContext } from '../../../../shared/agent-status-types' import { makePaneKey } from '../../../../shared/stable-pane-id' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { EMPTY_WORKTREE_AGENT_ORCHESTRATION, selectRuntimeAgentOrchestrationBatch diff --git a/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts b/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts index 2bd549481c0..e7881d95aff 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts @@ -4,7 +4,7 @@ import type { AgentStatusOrchestrationContext } from '../../../../shared/agent-status-types' import type { RetainedAgentEntry } from '@/store/slices/agent-status' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { makePaneKey, parsePaneKey } from '../../../../shared/stable-pane-id' import { EMPTY_WORKTREE_AGENT_ORCHESTRATION, diff --git a/src/renderer/src/components/sidebar/worktree-agent-row-fallback-tab.ts b/src/renderer/src/components/sidebar/worktree-agent-row-fallback-tab.ts index d722d39c4bb..9a0b6185a1a 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-row-fallback-tab.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-row-fallback-tab.ts @@ -1,6 +1,6 @@ import type { AgentStatusEntry } from '../../../../shared/agent-status-types' import { parsePaneKey } from '../../../../shared/stable-pane-id' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' export function effectiveWorktreeAgentRowStartedAt(entry: AgentStatusEntry): number { return entry.stateHistory[0]?.startedAt ?? entry.stateStartedAt diff --git a/src/renderer/src/components/sidebar/worktree-agent-row-selectors.test.ts b/src/renderer/src/components/sidebar/worktree-agent-row-selectors.test.ts index 46f75858ee9..29b30fa34ba 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-row-selectors.test.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-row-selectors.test.ts @@ -3,7 +3,7 @@ import type { AgentStatusEntry, MigrationUnsupportedPtyEntry } from '../../../../shared/agent-status-types' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { RetainedAgentEntry } from '@/store/slices/agent-status' import { makePaneKey } from '../../../../shared/stable-pane-id' import { getLiveEntriesFullRebuildCountForTests } from './worktree-agent-live-index-patch' diff --git a/src/renderer/src/components/sidebar/worktree-agent-row-selectors.ts b/src/renderer/src/components/sidebar/worktree-agent-row-selectors.ts index 75de25923f2..e9a6b5fde0b 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-row-selectors.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-row-selectors.ts @@ -13,7 +13,7 @@ import { recordLiveEntriesFullRebuild } from './worktree-agent-live-index-patch' import { selectWorktreeAgentOrchestration } from './worktree-agent-orchestration-index' -import type { TerminalLayoutSnapshot } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' const EMPTY_LIVE_ENTRIES: AgentStatusEntry[] = [] const EMPTY_MIGRATION_UNSUPPORTED_ENTRIES: MigrationUnsupportedPtyEntry[] = [] diff --git a/src/renderer/src/components/sidebar/worktree-agent-rows.ts b/src/renderer/src/components/sidebar/worktree-agent-rows.ts index da4a33650e6..1fbca0fc743 100644 --- a/src/renderer/src/components/sidebar/worktree-agent-rows.ts +++ b/src/renderer/src/components/sidebar/worktree-agent-rows.ts @@ -16,7 +16,7 @@ import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode, TerminalTab -} from '../../../../shared/types' +} from '../../../../shared/terminal-tab-types' import { resolveRuntimePaneTitleLeafId } from '@/lib/runtime-pane-title-leaf-id' import { buildTitleDerivedAgentRows, diff --git a/src/renderer/src/components/sidebar/worktree-card-display-property-options.ts b/src/renderer/src/components/sidebar/worktree-card-display-property-options.ts index e5256b48312..7a009b384fa 100644 --- a/src/renderer/src/components/sidebar/worktree-card-display-property-options.ts +++ b/src/renderer/src/components/sidebar/worktree-card-display-property-options.ts @@ -1,4 +1,4 @@ -import type { WorktreeCardProperty } from '../../../../shared/types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' import { translate } from '@/i18n/i18n' export const PROPERTY_OPTIONS: { id: WorktreeCardProperty; label: string }[] = [ diff --git a/src/renderer/src/components/sidebar/worktree-card-header.tsx b/src/renderer/src/components/sidebar/worktree-card-header.tsx index 0c063e28dee..8fb715b01ca 100644 --- a/src/renderer/src/components/sidebar/worktree-card-header.tsx +++ b/src/renderer/src/components/sidebar/worktree-card-header.tsx @@ -7,7 +7,7 @@ import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { translate } from '@/i18n/i18n' import { cn } from '@/lib/utils' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { resolveRepoHeaderColor } from './project-header-color' import { formatSparseDirectoryPreview, shouldBeginWorktreeRename } from './worktree-card-model' import type { WorktreeCardPresentation } from './worktree-card-presentation' diff --git a/src/renderer/src/components/sidebar/worktree-card-jira-issue-display.ts b/src/renderer/src/components/sidebar/worktree-card-jira-issue-display.ts index b9373e1092a..a072c51c33f 100644 --- a/src/renderer/src/components/sidebar/worktree-card-jira-issue-display.ts +++ b/src/renderer/src/components/sidebar/worktree-card-jira-issue-display.ts @@ -1,4 +1,5 @@ -import type { Worktree, WorktreeCardProperty } from '../../../../shared/types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { WorktreeCardJiraIssueDisplay } from './worktree-card-meta-types' function withoutRepeatedJiraIdentifier(title: string, identifier: string): string { diff --git a/src/renderer/src/components/sidebar/worktree-card-meta-types.ts b/src/renderer/src/components/sidebar/worktree-card-meta-types.ts index 75373a14699..0be771e7534 100644 --- a/src/renderer/src/components/sidebar/worktree-card-meta-types.ts +++ b/src/renderer/src/components/sidebar/worktree-card-meta-types.ts @@ -1,9 +1,9 @@ import type { ExecutionHostId } from '../../../../shared/execution-host' +import type { IssueInfo } from '../../../../shared/github/pull-request-types' import type { AutomationWorkspaceProvenance, - CliWorkspaceProvenance, - IssueInfo -} from '../../../../shared/types' + CliWorkspaceProvenance +} from '../../../../shared/worktree/types' import type { WorktreeCardPrDisplay } from './worktree-card-pr-display' import type { WorktreeCardDetailsHoverControl } from './worktree-card-details-hover-state' diff --git a/src/renderer/src/components/sidebar/worktree-card-model.ts b/src/renderer/src/components/sidebar/worktree-card-model.ts index 2331b92d2cd..e10cf2da703 100644 --- a/src/renderer/src/components/sidebar/worktree-card-model.ts +++ b/src/renderer/src/components/sidebar/worktree-card-model.ts @@ -1,6 +1,7 @@ import type React from 'react' -import type { Repo, WorkspaceStatus, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorkspaceStatus, Worktree } from '../../../../shared/worktree/types' import type { WorktreeCardPrDisplay } from './worktree-card-pr-display' export type WorktreeRenameRequest = { diff --git a/src/renderer/src/components/sidebar/worktree-card-pr-display.test.ts b/src/renderer/src/components/sidebar/worktree-card-pr-display.test.ts index 8cc6d45ac5a..7e677708b50 100644 --- a/src/renderer/src/components/sidebar/worktree-card-pr-display.test.ts +++ b/src/renderer/src/components/sidebar/worktree-card-pr-display.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { PRInfo } from '../../../../shared/types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' import { getWorktreeCardPrDisplay, isCachedMergedBranchPRCurrentForWorktree diff --git a/src/renderer/src/components/sidebar/worktree-card-pr-display.ts b/src/renderer/src/components/sidebar/worktree-card-pr-display.ts index 27027e09183..12dcddfac03 100644 --- a/src/renderer/src/components/sidebar/worktree-card-pr-display.ts +++ b/src/renderer/src/components/sidebar/worktree-card-pr-display.ts @@ -1,5 +1,6 @@ import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { PRInfo, Worktree } from '../../../../shared/types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' +import type { Worktree } from '../../../../shared/worktree/types' type LinkedReviewMetadataProvider = Exclude diff --git a/src/renderer/src/components/sidebar/worktree-card-status-inputs.test.ts b/src/renderer/src/components/sidebar/worktree-card-status-inputs.test.ts index 0030e7d931e..7d366b845e8 100644 --- a/src/renderer/src/components/sidebar/worktree-card-status-inputs.test.ts +++ b/src/renderer/src/components/sidebar/worktree-card-status-inputs.test.ts @@ -4,7 +4,7 @@ import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode, TerminalTab -} from '../../../../shared/types' +} from '../../../../shared/terminal-tab-types' import { selectLivePtyIdsForWorktree, selectTerminalLayoutRootsForWorktree, diff --git a/src/renderer/src/components/sidebar/worktree-card-status-inputs.ts b/src/renderer/src/components/sidebar/worktree-card-status-inputs.ts index e06894441cf..128afafe03d 100644 --- a/src/renderer/src/components/sidebar/worktree-card-status-inputs.ts +++ b/src/renderer/src/components/sidebar/worktree-card-status-inputs.ts @@ -1,5 +1,5 @@ import type { AppState } from '@/store/types' -import type { TerminalPaneLayoutNode } from '../../../../shared/types' +import type { TerminalPaneLayoutNode } from '../../../../shared/terminal-tab-types' // Why: these selectors return fresh maps whose top-level values preserve // underlying per-tab references, so callers must compare them shallowly. diff --git a/src/renderer/src/components/sidebar/worktree-delete-execution.ts b/src/renderer/src/components/sidebar/worktree-delete-execution.ts index 889587306d0..f105acedcdf 100644 --- a/src/renderer/src/components/sidebar/worktree-delete-execution.ts +++ b/src/renderer/src/components/sidebar/worktree-delete-execution.ts @@ -7,7 +7,7 @@ import { isPathInsideOrEqual, normalizeRuntimePathForComparison } from '../../../../shared/cross-platform-path' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { prepareActiveWorktreeFocusAfterDelete } from './active-worktree-focus-after-delete' import { showDeleteWorktreeFailureToast } from './delete-worktree-failure-toast' import { showWorkspaceListChangedToast } from './stale-workspace-list-toast' diff --git a/src/renderer/src/components/sidebar/worktree-delete-request.ts b/src/renderer/src/components/sidebar/worktree-delete-request.ts index 71ef9e87cda..df357d8c55e 100644 --- a/src/renderer/src/components/sidebar/worktree-delete-request.ts +++ b/src/renderer/src/components/sidebar/worktree-delete-request.ts @@ -1,4 +1,4 @@ -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import type { PreservedBranchCleanup } from '@/lib/preserved-branch-cleanup' export type WorktreeBatchDeleteOptions = { diff --git a/src/renderer/src/components/sidebar/worktree-lineage-drag-drop.test.ts b/src/renderer/src/components/sidebar/worktree-lineage-drag-drop.test.ts index 9241dd2cf11..65ae3befbc7 100644 --- a/src/renderer/src/components/sidebar/worktree-lineage-drag-drop.test.ts +++ b/src/renderer/src/components/sidebar/worktree-lineage-drag-drop.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { Worktree, WorktreeLineage } from '../../../../shared/types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getCyclicProjectedWorktreeLineageIds } from './worktree-lineage-projection' import { getReorderedWorktreeIdsToUnnest, diff --git a/src/renderer/src/components/sidebar/worktree-lineage-drag-drop.ts b/src/renderer/src/components/sidebar/worktree-lineage-drag-drop.ts index f171ce5d2a6..5f63768aac3 100644 --- a/src/renderer/src/components/sidebar/worktree-lineage-drag-drop.ts +++ b/src/renderer/src/components/sidebar/worktree-lineage-drag-drop.ts @@ -1,4 +1,5 @@ -import type { Worktree, WorktreeLineage } from '../../../../shared/types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getLineageRenderInfo } from './worktree-lineage-projection' const WORKTREE_CARD_CONTENT_TARGET_SELECTOR = '[data-worktree-card-hover-trigger]' diff --git a/src/renderer/src/components/sidebar/worktree-lineage-projection.ts b/src/renderer/src/components/sidebar/worktree-lineage-projection.ts index 040a40eb737..337140f586c 100644 --- a/src/renderer/src/components/sidebar/worktree-lineage-projection.ts +++ b/src/renderer/src/components/sidebar/worktree-lineage-projection.ts @@ -2,7 +2,8 @@ import { getCyclicWorktreeLineageChildIds, isValidResolvedWorktreeLineageEdge } from '../../../../shared/resolved-worktree-lineage' -import type { Worktree, WorktreeLineage } from '../../../../shared/types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' export type LineageRenderInfo = | { state: 'none' } diff --git a/src/renderer/src/components/sidebar/worktree-list-folder-reveal.test.ts b/src/renderer/src/components/sidebar/worktree-list-folder-reveal.test.ts index 9b472ab7be7..61eca2889de 100644 --- a/src/renderer/src/components/sidebar/worktree-list-folder-reveal.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-folder-reveal.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from 'vitest' -import type { FolderWorkspace, ProjectGroup, Worktree } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Worktree } from '../../../../shared/worktree/types' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import { getFolderWorkspaceRevealGroupKeys, diff --git a/src/renderer/src/components/sidebar/worktree-list-folder-reveal.ts b/src/renderer/src/components/sidebar/worktree-list-folder-reveal.ts index 21c70db9384..de8ca2c278c 100644 --- a/src/renderer/src/components/sidebar/worktree-list-folder-reveal.ts +++ b/src/renderer/src/components/sidebar/worktree-list-folder-reveal.ts @@ -1,4 +1,6 @@ -import type { FolderWorkspace, ProjectGroup, Worktree } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Worktree } from '../../../../shared/worktree/types' import { folderWorkspaceToWorktree } from '../../../../shared/folder-workspace-worktree' import { parseWorkspaceKey } from '../../../../shared/workspace-scope' import { getProjectGroupHeaderKey } from './worktree-list-groups' diff --git a/src/renderer/src/components/sidebar/worktree-list-groups.test.ts b/src/renderer/src/components/sidebar/worktree-list-groups.test.ts index c5a7f9606b2..dacfcb3a139 100644 --- a/src/renderer/src/components/sidebar/worktree-list-groups.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-groups.test.ts @@ -20,16 +20,12 @@ import { REPO_HEADER_ACTION_REVEAL_CLASS } from './repo-header-action-button-class' import { getWorktreeLineageAncestors } from './worktree-lineage-projection' -import type { - DetectedWorktree, - Project, - ProjectHostSetup, - FolderWorkspace, - Repo, - ProjectGroup, - Worktree, - WorktreeLineage -} from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { DetectedWorktree, Worktree } from '../../../../shared/worktree/types' const LOCAL_HOST_LABEL = getExecutionHostLabel('local') diff --git a/src/renderer/src/components/sidebar/worktree-list-groups.ts b/src/renderer/src/components/sidebar/worktree-list-groups.ts index a154eea8263..068da94edb7 100644 --- a/src/renderer/src/components/sidebar/worktree-list-groups.ts +++ b/src/renderer/src/components/sidebar/worktree-list-groups.ts @@ -1,18 +1,17 @@ /* eslint-disable max-lines -- Why: sidebar row construction keeps every grouping mode in one pure module so reveal, virtualized rendering, and tests share the same flat row contract. */ import { CircleX, FolderTree, List, Pin } from 'lucide-react' import type React from 'react' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' +import type { ProjectOrderBy } from '../../../../shared/ui-chrome-types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' import type { DetectedWorktree, - Project, - ProjectHostSetup, - FolderWorkspace, - Repo, - ProjectGroup, - ProjectOrderBy, - Worktree, - WorktreeLineage, - WorkspaceStatusDefinition -} from '../../../../shared/types' + WorkspaceStatusDefinition, + Worktree +} from '../../../../shared/worktree/types' import { branchName } from '../../lib/git-utils' import { getWorkspaceStatus, diff --git a/src/renderer/src/components/sidebar/worktree-list-host-filtering.test.ts b/src/renderer/src/components/sidebar/worktree-list-host-filtering.test.ts index 2321971231e..179e3ee8379 100644 --- a/src/renderer/src/components/sidebar/worktree-list-host-filtering.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-host-filtering.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { FolderWorkspace, ProjectGroup } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' import { getFolderPathStatusRouteOptionsForRows, getFolderWorkspaceExecutionHostIdForRows, diff --git a/src/renderer/src/components/sidebar/worktree-list-host-filtering.ts b/src/renderer/src/components/sidebar/worktree-list-host-filtering.ts index 655c4e4f2ff..8071b56bcb7 100644 --- a/src/renderer/src/components/sidebar/worktree-list-host-filtering.ts +++ b/src/renderer/src/components/sidebar/worktree-list-host-filtering.ts @@ -7,7 +7,8 @@ import { type ExecutionHostScope } from '../../../../shared/execution-host' import type { FolderWorkspacePathStatusRequest } from '../../../../shared/folder-workspace-path-status' -import type { FolderWorkspace, ProjectGroup } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' /** null means "no host filter" — every host is visible. */ export function getVisibleSidebarHostIdSet( diff --git a/src/renderer/src/components/sidebar/worktree-list-imported-rows.test.ts b/src/renderer/src/components/sidebar/worktree-list-imported-rows.test.ts index 67f3760c91c..9cda62a93b4 100644 --- a/src/renderer/src/components/sidebar/worktree-list-imported-rows.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-imported-rows.test.ts @@ -6,7 +6,8 @@ import { getWorktreeDragIndexes, renderRowContainsWorktree } from './WorktreeList' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { Row } from './worktree-list-groups' const repo: Repo = { diff --git a/src/renderer/src/components/sidebar/worktree-list-review-cache-inputs.test.ts b/src/renderer/src/components/sidebar/worktree-list-review-cache-inputs.test.ts index 4960445c342..714fe0e64b4 100644 --- a/src/renderer/src/components/sidebar/worktree-list-review-cache-inputs.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-review-cache-inputs.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { shallow } from 'zustand/shallow' -import type { FolderWorkspace } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' import { EMPTY_WORKTREE_LIST_REVIEW_CACHE_INPUTS, selectWorktreeListReviewCacheInputs, diff --git a/src/renderer/src/components/sidebar/worktree-list-review-cache-inputs.ts b/src/renderer/src/components/sidebar/worktree-list-review-cache-inputs.ts index 9ea780c89ea..690da091f3c 100644 --- a/src/renderer/src/components/sidebar/worktree-list-review-cache-inputs.ts +++ b/src/renderer/src/components/sidebar/worktree-list-review-cache-inputs.ts @@ -1,5 +1,5 @@ import type { AppState } from '@/store/types' -import type { WorktreeCardProperty } from '../../../../shared/types' +import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types' import type { WorktreeGroupBy } from './worktree-list-groups' export type WorktreeListReviewCacheState = Pick< diff --git a/src/renderer/src/components/sidebar/worktree-list-scroll-adjustment.test.ts b/src/renderer/src/components/sidebar/worktree-list-scroll-adjustment.test.ts index 924a4edc88a..f5b9d586f63 100644 --- a/src/renderer/src/components/sidebar/worktree-list-scroll-adjustment.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-scroll-adjustment.test.ts @@ -12,7 +12,7 @@ import { GROUP_HEADER_ROW_HEIGHT, getActiveStickyHeaderIndexForScroll } from './worktree-list-virtual-rows' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import type { Row } from './worktree-list-groups' const repo: Repo = { diff --git a/src/renderer/src/components/sidebar/worktree-list-sticky-headers.test.ts b/src/renderer/src/components/sidebar/worktree-list-sticky-headers.test.ts index 777560a9484..1c8efdfe9a0 100644 --- a/src/renderer/src/components/sidebar/worktree-list-sticky-headers.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-sticky-headers.test.ts @@ -1,7 +1,9 @@ import { describe, expect, it } from 'vitest' import { buildRows } from './worktree-list-groups' import { getStickyHeaderIndexes } from './worktree-list-virtual-rows' -import type { ProjectGroup, Repo, Worktree } from '../../../../shared/types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { Row } from './worktree-list-groups' const repo: Repo = { diff --git a/src/renderer/src/components/sidebar/worktree-meta-updates.test.ts b/src/renderer/src/components/sidebar/worktree-meta-updates.test.ts index 3ce6ffb74b4..38985cf7330 100644 --- a/src/renderer/src/components/sidebar/worktree-meta-updates.test.ts +++ b/src/renderer/src/components/sidebar/worktree-meta-updates.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { WorktreeMeta } from '../../../../shared/types' +import type { WorktreeMeta } from '../../../../shared/worktree/meta-types' import { buildWorktreeMetaUpdates, type WorktreeMetaDraft, diff --git a/src/renderer/src/components/sidebar/worktree-meta-updates.ts b/src/renderer/src/components/sidebar/worktree-meta-updates.ts index 20afdf8af26..b8f06c43d8c 100644 --- a/src/renderer/src/components/sidebar/worktree-meta-updates.ts +++ b/src/renderer/src/components/sidebar/worktree-meta-updates.ts @@ -5,7 +5,8 @@ import { } from '../../../../shared/linear/links' import { parseIssueLinkInput, type IssueLinkProvider } from '../../../../shared/issue-link-input' import type { WorkspaceSourceProvider } from '../../../../shared/new-workspace/workspace-source' -import type { WorkspaceLinkedItem, WorktreeMeta } from '../../../../shared/types' +import type { WorktreeMeta } from '../../../../shared/worktree/meta-types' +import type { WorkspaceLinkedItem } from '../../../../shared/worktree/types' export type WorktreeMetaSavedPayload = { worktreeId: string diff --git a/src/renderer/src/components/sidebar/worktree-parent-candidates.ts b/src/renderer/src/components/sidebar/worktree-parent-candidates.ts index 981ea7e3a7d..b95d2c1fbd3 100644 --- a/src/renderer/src/components/sidebar/worktree-parent-candidates.ts +++ b/src/renderer/src/components/sidebar/worktree-parent-candidates.ts @@ -1,5 +1,7 @@ import { getWorktreeExecutionHostId } from '../../../../shared/execution-host' -import type { Repo, Worktree, WorktreeLineage } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { canAssignWorktreeParent } from './worktree-parent-eligibility' import { getCyclicProjectedWorktreeLineageIds } from './worktree-lineage-projection' diff --git a/src/renderer/src/components/sidebar/worktree-parent-eligibility.test.ts b/src/renderer/src/components/sidebar/worktree-parent-eligibility.test.ts index 7dfe0360f02..64400b7dedd 100644 --- a/src/renderer/src/components/sidebar/worktree-parent-eligibility.test.ts +++ b/src/renderer/src/components/sidebar/worktree-parent-eligibility.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest' import { join } from 'node:path' -import type { Repo, Worktree, WorktreeLineage } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { canAssignWorktreeParent } from './worktree-parent-eligibility' import { getEligibleWorktreeParents, isEligibleWorktreeParent } from './worktree-parent-candidates' diff --git a/src/renderer/src/components/sidebar/worktree-parent-eligibility.ts b/src/renderer/src/components/sidebar/worktree-parent-eligibility.ts index fe2c4261064..f620f19133d 100644 --- a/src/renderer/src/components/sidebar/worktree-parent-eligibility.ts +++ b/src/renderer/src/components/sidebar/worktree-parent-eligibility.ts @@ -1,4 +1,5 @@ -import type { Worktree, WorktreeLineage } from '../../../../shared/types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../shared/worktree/types' import { getCyclicProjectedWorktreeLineageIds, getLineageRenderInfo diff --git a/src/renderer/src/components/sidebar/worktree-parent-picker-filtering.ts b/src/renderer/src/components/sidebar/worktree-parent-picker-filtering.ts index 3c07c35b1e4..b15154440e6 100644 --- a/src/renderer/src/components/sidebar/worktree-parent-picker-filtering.ts +++ b/src/renderer/src/components/sidebar/worktree-parent-picker-filtering.ts @@ -1,6 +1,6 @@ import { defaultFilter } from 'cmdk' import { branchDisplayName } from './WorktreeCardHelpers' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' export function getWorktreeParentPickerItemValue(candidate: Worktree): string { return `${candidate.displayName} ${branchDisplayName(candidate.branch)} ${candidate.path}` diff --git a/src/renderer/src/components/sidebar/worktree-sidebar-row-preference.test.ts b/src/renderer/src/components/sidebar/worktree-sidebar-row-preference.test.ts index cd4672dc9a7..c8b49f10b1f 100644 --- a/src/renderer/src/components/sidebar/worktree-sidebar-row-preference.test.ts +++ b/src/renderer/src/components/sidebar/worktree-sidebar-row-preference.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from 'vitest' -import type { FolderWorkspace, ProjectGroup, Repo, Worktree } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { HostSectionRow } from './host-section-rows' import { getRenderedWorktreesInSidebarOrder } from './worktree-sidebar-row-preference' diff --git a/src/renderer/src/components/sidebar/worktree-sidebar-row-preference.ts b/src/renderer/src/components/sidebar/worktree-sidebar-row-preference.ts index c73107cbed8..0495375b691 100644 --- a/src/renderer/src/components/sidebar/worktree-sidebar-row-preference.ts +++ b/src/renderer/src/components/sidebar/worktree-sidebar-row-preference.ts @@ -1,5 +1,5 @@ import { folderWorkspaceToWorktree } from '../../../../shared/folder-workspace-worktree' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import type { HostSectionRow } from './host-section-rows' import { PINNED_GROUP_KEY, diff --git a/src/renderer/src/components/sidebar/worktree-subagent-child-rows.ts b/src/renderer/src/components/sidebar/worktree-subagent-child-rows.ts index 2a76bb0799c..5135eea6933 100644 --- a/src/renderer/src/components/sidebar/worktree-subagent-child-rows.ts +++ b/src/renderer/src/components/sidebar/worktree-subagent-child-rows.ts @@ -1,6 +1,6 @@ import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' /** Row-identity key for an in-process subagent child row. The NUL separator * cannot appear in real pane keys, so synthetic keys can never collide with diff --git a/src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.test.ts b/src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.test.ts index 44c3e7f2639..6fcd0a6719c 100644 --- a/src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.test.ts +++ b/src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { applyAgentRowLineage } from '@/components/dashboard/agent-row-lineage' -import type { TerminalLayoutSnapshot, TerminalTab, TuiAgent } from '../../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { makePaneKey } from '../../../../shared/stable-pane-id' import { buildWorktreeAgentRows } from './worktree-agent-rows' diff --git a/src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.ts b/src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.ts index 9224fe2cba3..70289b4b3fb 100644 --- a/src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.ts +++ b/src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.ts @@ -14,7 +14,7 @@ import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode, TerminalTab -} from '../../../../shared/types' +} from '../../../../shared/terminal-tab-types' import { normalizeCompatibleAgentTitleForOwner, resolveCompatibleAgentTypeForOwner diff --git a/src/renderer/src/components/sidebar/worktree-visibility-update-error.ts b/src/renderer/src/components/sidebar/worktree-visibility-update-error.ts index 74605160e68..3e82a24d0ac 100644 --- a/src/renderer/src/components/sidebar/worktree-visibility-update-error.ts +++ b/src/renderer/src/components/sidebar/worktree-visibility-update-error.ts @@ -1,5 +1,5 @@ import { translate } from '@/i18n/i18n' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' export function worktreeVisibilityUpdateError( updateSucceeded: boolean, diff --git a/src/renderer/src/components/sparse/SparseCheckoutPresetSelect.tsx b/src/renderer/src/components/sparse/SparseCheckoutPresetSelect.tsx index 8ff4f7bfed8..a25f4337c9c 100644 --- a/src/renderer/src/components/sparse/SparseCheckoutPresetSelect.tsx +++ b/src/renderer/src/components/sparse/SparseCheckoutPresetSelect.tsx @@ -7,7 +7,7 @@ import { useAppStore } from '@/store' import { cn } from '@/lib/utils' import { parseSparsePresetDirectories } from '@/lib/sparse-preset-draft' import { useMountedRef } from '@/hooks/useMountedRef' -import type { SparsePreset } from '../../../../shared/types' +import type { SparsePreset } from '../../../../shared/worktree/create-types' import { translate } from '@/i18n/i18n' import { SparseCheckoutPresetDraftForm, diff --git a/src/renderer/src/components/status-bar/PetStatusSegment.tsx b/src/renderer/src/components/status-bar/PetStatusSegment.tsx index b927e29351e..48cd2f0292b 100644 --- a/src/renderer/src/components/status-bar/PetStatusSegment.tsx +++ b/src/renderer/src/components/status-bar/PetStatusSegment.tsx @@ -15,7 +15,7 @@ import { } from '@/components/ui/dropdown-menu' import { useAppStore } from '../../store' import { BUNDLED_PET, BUNDLED_PETS, findBundledPet, isBundledPetId } from '../pet/pet-models' -import { PET_SIZE_MAX, PET_SIZE_MIN } from '../../../../shared/types' +import { PET_SIZE_MAX, PET_SIZE_MIN } from '../../../../shared/pet-types' import { translate } from '@/i18n/i18n' // Why: cluster pet-related controls (show/hide, character picker, custom diff --git a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx index 3bf507646da..783202de0cb 100644 --- a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx +++ b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx @@ -4,7 +4,7 @@ import React, { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, describe, expect, it, vi } from 'vitest' import { ORPHAN_WORKTREE_ID } from '../../../../shared/constants' -import type { BrowserWorkspace } from '../../../../shared/types' +import type { BrowserWorkspace } from '../../../../shared/browser-workspace-types' import type { UnifiedSessionRow, UnifiedWorktreeRow } from './resource-usage-merge-types' vi.mock('@/store', () => { diff --git a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx index c4690a1c79a..688d59c9a14 100644 --- a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx +++ b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx @@ -31,7 +31,9 @@ import { useAppStore } from '../../store' import { useWorktreeMap } from '../../store/selectors' import { runWorktreeDelete } from '../sidebar/delete-worktree-flow' import { useDaemonActions, DaemonActionDialog } from '../shared/useDaemonActions' -import type { AppMemory, BrowserWorkspace, UsageValues, Worktree } from '../../../../shared/types' +import type { BrowserWorkspace } from '../../../../shared/browser-workspace-types' +import type { AppMemory, UsageValues } from '../../../../shared/process-stats-types' +import type { Worktree } from '../../../../shared/worktree/types' import { ORPHAN_WORKTREE_ID } from '../../../../shared/constants' import { getRepoExecutionHostId, parseExecutionHostId } from '../../../../shared/execution-host' import { countEstimatedInactiveWorkspaces } from '../workspace-cleanup/inactive-workspace-estimate' diff --git a/src/renderer/src/components/status-bar/StatusBar.tsx b/src/renderer/src/components/status-bar/StatusBar.tsx index 1c4af8fdff8..0243c5f8dee 100644 --- a/src/renderer/src/components/status-bar/StatusBar.tsx +++ b/src/renderer/src/components/status-bar/StatusBar.tsx @@ -38,11 +38,11 @@ import { } from '@/components/ui/dropdown-menu' import { useAppStore } from '../../store' import { selectFloatingWorkspaceHasUnread } from '../../store/selectors' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { ClaudeRateLimitAccountsState, - CodexRateLimitAccountsState, - GlobalSettings -} from '../../../../shared/types' + CodexRateLimitAccountsState +} from '../../../../shared/managed-account-types' import type { ProviderRateLimits, RateLimitRuntimeTarget, diff --git a/src/renderer/src/components/status-bar/WorkspaceSpaceManagerPanel.tsx b/src/renderer/src/components/status-bar/WorkspaceSpaceManagerPanel.tsx index 453e3553792..8a4c23c6a38 100644 --- a/src/renderer/src/components/status-bar/WorkspaceSpaceManagerPanel.tsx +++ b/src/renderer/src/components/status-bar/WorkspaceSpaceManagerPanel.tsx @@ -29,7 +29,10 @@ import type { AgentStatusEntry, MigrationUnsupportedPtyEntry } from '../../../../shared/agent-status-types' -import type { GitStatusResult, Repo, TerminalTab, Worktree } from '../../../../shared/types' +import type { GitStatusResult } from '../../../../shared/git-status-types' +import type { Repo } from '../../../../shared/repo-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { WorkspaceSpaceItem, WorkspaceSpaceWorktree diff --git a/src/renderer/src/components/status-bar/mergeSnapshotAndSessions.test.ts b/src/renderer/src/components/status-bar/mergeSnapshotAndSessions.test.ts index a2fb18c1193..8536518728e 100644 --- a/src/renderer/src/components/status-bar/mergeSnapshotAndSessions.test.ts +++ b/src/renderer/src/components/status-bar/mergeSnapshotAndSessions.test.ts @@ -1,11 +1,8 @@ import { describe, expect, it } from 'vitest' -import type { - BrowserWorkspace, - MemorySnapshot, - TerminalTab, - Worktree, - WorktreeMemory -} from '../../../../shared/types' +import type { BrowserWorkspace } from '../../../../shared/browser-workspace-types' +import type { MemorySnapshot, WorktreeMemory } from '../../../../shared/process-stats-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import { mergeSnapshotAndSessions, UNATTRIBUTED_REPO_ID } from './mergeSnapshotAndSessions' import { requiresKillConfirmation } from './resource-session-kill-confirmation' import type { DaemonSession, MergeContext } from './resource-usage-merge-types' diff --git a/src/renderer/src/components/status-bar/mergeSnapshotAndSessions.ts b/src/renderer/src/components/status-bar/mergeSnapshotAndSessions.ts index 672e7c622c2..7f858242952 100644 --- a/src/renderer/src/components/status-bar/mergeSnapshotAndSessions.ts +++ b/src/renderer/src/components/status-bar/mergeSnapshotAndSessions.ts @@ -17,7 +17,11 @@ * See docs/resource-usage-merge-spec.md for the full design. */ -import type { MemorySnapshot, SessionMemory, WorktreeMemory } from '../../../../shared/types' +import type { + MemorySnapshot, + SessionMemory, + WorktreeMemory +} from '../../../../shared/process-stats-types' import { parsePtySessionId } from '../../../../shared/pty-session-id-format' import { parsePaneKey as parseStablePaneKey } from '../../../../shared/stable-pane-id' import { diff --git a/src/renderer/src/components/status-bar/resource-memory-metric-copy.ts b/src/renderer/src/components/status-bar/resource-memory-metric-copy.ts index a847f7d53a9..50448bc4ae6 100644 --- a/src/renderer/src/components/status-bar/resource-memory-metric-copy.ts +++ b/src/renderer/src/components/status-bar/resource-memory-metric-copy.ts @@ -1,4 +1,4 @@ -import type { ProcessMemoryMetric } from '../../../../shared/types' +import type { ProcessMemoryMetric } from '../../../../shared/process-stats-types' import { translate } from '@/i18n/i18n' export type ResourceMemoryMetricCopy = { diff --git a/src/renderer/src/components/status-bar/resource-session-bindings.test.ts b/src/renderer/src/components/status-bar/resource-session-bindings.test.ts index 3845234d6ed..9cfefa99f3b 100644 --- a/src/renderer/src/components/status-bar/resource-session-bindings.test.ts +++ b/src/renderer/src/components/status-bar/resource-session-bindings.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' import { buildResourceSessionBindingIndex, countUnboundDaemonSessions, diff --git a/src/renderer/src/components/status-bar/resource-session-bindings.ts b/src/renderer/src/components/status-bar/resource-session-bindings.ts index 96240f82268..a0a46140072 100644 --- a/src/renderer/src/components/status-bar/resource-session-bindings.ts +++ b/src/renderer/src/components/status-bar/resource-session-bindings.ts @@ -1,4 +1,4 @@ -import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' import { mayDestroyWithoutOwnerEvidence } from '../../../../shared/pty-listed-session' import type { DaemonSession } from './resource-usage-merge-types' diff --git a/src/renderer/src/components/status-bar/resource-usage-merge-types.ts b/src/renderer/src/components/status-bar/resource-usage-merge-types.ts index c365329c110..5943b744dfd 100644 --- a/src/renderer/src/components/status-bar/resource-usage-merge-types.ts +++ b/src/renderer/src/components/status-bar/resource-usage-merge-types.ts @@ -1,9 +1,6 @@ -import type { - BrowserWorkspace, - TerminalLayoutSnapshot, - TerminalTab, - Worktree -} from '../../../../shared/types' +import type { BrowserWorkspace } from '../../../../shared/browser-workspace-types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { AgentOwnershipEvidence, PtyListedSession diff --git a/src/renderer/src/components/status-bar/status-bar-agent-gating.ts b/src/renderer/src/components/status-bar/status-bar-agent-gating.ts index 4ded63147db..145abef3a90 100644 --- a/src/renderer/src/components/status-bar/status-bar-agent-gating.ts +++ b/src/renderer/src/components/status-bar/status-bar-agent-gating.ts @@ -1,4 +1,5 @@ -import type { StatusBarItem, TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' +import type { StatusBarItem } from '../../../../shared/ui-chrome-types' // Why: CLI-backed usage bars are surface noise when the underlying // CLI isn't installed (e.g. a fresh Ubuntu install showing "Gemini Usage" diff --git a/src/renderer/src/components/status-bar/status-bar-provider-visibility.ts b/src/renderer/src/components/status-bar/status-bar-provider-visibility.ts index e96fa09550a..f1afd97f5f4 100644 --- a/src/renderer/src/components/status-bar/status-bar-provider-visibility.ts +++ b/src/renderer/src/components/status-bar/status-bar-provider-visibility.ts @@ -1,5 +1,5 @@ import type { ProviderRateLimits } from '../../../../shared/rate-limit-types' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' export type UsageProviderSettings = Pick< GlobalSettings, diff --git a/src/renderer/src/components/status-bar/status-bar-runtime-groups.test.ts b/src/renderer/src/components/status-bar/status-bar-runtime-groups.test.ts index 262cf1595f1..5e0c4a162de 100644 --- a/src/renderer/src/components/status-bar/status-bar-runtime-groups.test.ts +++ b/src/renderer/src/components/status-bar/status-bar-runtime-groups.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from 'vitest' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { ClaudeRateLimitAccountsState, - CodexRateLimitAccountsState, - GlobalSettings -} from '../../../../shared/types' + CodexRateLimitAccountsState +} from '../../../../shared/managed-account-types' import { buildClaudeStatusSwitchGroups, buildCodexStatusSwitchGroups, diff --git a/src/renderer/src/components/status-bar/use-available-status-bar-toggles.ts b/src/renderer/src/components/status-bar/use-available-status-bar-toggles.ts index f693aa84d60..cda54a7b8bc 100644 --- a/src/renderer/src/components/status-bar/use-available-status-bar-toggles.ts +++ b/src/renderer/src/components/status-bar/use-available-status-bar-toggles.ts @@ -1,5 +1,5 @@ import { useAppStore } from '../../store' -import type { StatusBarItem } from '../../../../shared/types' +import type { StatusBarItem } from '../../../../shared/ui-chrome-types' import { isStatusBarItemAvailable } from './status-bar-agent-gating' /** Subscribes to detected-agent state and returns the toggles filtered to diff --git a/src/renderer/src/components/status-bar/workspace-space-presentation.test.ts b/src/renderer/src/components/status-bar/workspace-space-presentation.test.ts index 702f3c1d70a..1d600faed2a 100644 --- a/src/renderer/src/components/status-bar/workspace-space-presentation.test.ts +++ b/src/renderer/src/components/status-bar/workspace-space-presentation.test.ts @@ -18,7 +18,8 @@ import { } from './workspace-space-presentation' import { getWorkspaceDecisionDetails } from './WorkspaceSpaceManagerPanel' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' function row(overrides: Partial): WorkspaceSpaceWorktree { return { diff --git a/src/renderer/src/components/status-bar/workspace-space-presentation.ts b/src/renderer/src/components/status-bar/workspace-space-presentation.ts index ad09532a652..5aab2dcefc5 100644 --- a/src/renderer/src/components/status-bar/workspace-space-presentation.ts +++ b/src/renderer/src/components/status-bar/workspace-space-presentation.ts @@ -6,7 +6,7 @@ import { type MigrationUnsupportedPtyEntry } from '../../../../shared/agent-status-types' import { parsePaneKey } from '../../../../shared/stable-pane-id' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { isClipboardTextByteLengthOverLimit } from '../../../../shared/clipboard-text' import type { WorkspaceSpaceItem, diff --git a/src/renderer/src/components/tab-bar/BrowserTab.test.tsx b/src/renderer/src/components/tab-bar/BrowserTab.test.tsx index c1e994301b9..951f2f9f132 100644 --- a/src/renderer/src/components/tab-bar/BrowserTab.test.tsx +++ b/src/renderer/src/components/tab-bar/BrowserTab.test.tsx @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { BrowserTab as BrowserTabState } from '../../../../shared/types' +import type { BrowserTab as BrowserTabState } from '../../../../shared/browser-workspace-types' const reactHookRuntime = vi.hoisted(() => ({ states: [] as unknown[], diff --git a/src/renderer/src/components/tab-bar/BrowserTab.tsx b/src/renderer/src/components/tab-bar/BrowserTab.tsx index e0ecca579d2..2ea3d2d42e5 100644 --- a/src/renderer/src/components/tab-bar/BrowserTab.tsx +++ b/src/renderer/src/components/tab-bar/BrowserTab.tsx @@ -21,7 +21,7 @@ import { import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { ORCA_BROWSER_BLANK_URL } from '../../../../shared/constants' import { redactKagiSessionToken } from '../../../../shared/browser-url' -import type { BrowserTab as BrowserTabState } from '../../../../shared/types' +import type { BrowserTab as BrowserTabState } from '../../../../shared/browser-workspace-types' import { CLOSE_ALL_CONTEXT_MENUS_EVENT } from './SortableTab' import { getLiveBrowserUrl } from '../browser-pane/browser-runtime' import type { TabDragItemData } from '../tab-group/useTabDragSplit' diff --git a/src/renderer/src/components/tab-bar/EditorFileTab.tsx b/src/renderer/src/components/tab-bar/EditorFileTab.tsx index 4af4e4fcfc4..1dcc0e96c16 100644 --- a/src/renderer/src/components/tab-bar/EditorFileTab.tsx +++ b/src/renderer/src/components/tab-bar/EditorFileTab.tsx @@ -12,7 +12,7 @@ import { getFileTypeIcon } from '@/lib/file-type-icons' import { useRepoById, useWorktreeById } from '@/store/selectors' import { useAppStore } from '@/store' import { STATUS_COLORS, STATUS_LABELS } from '../right-sidebar/status-display' -import type { GitFileStatus } from '../../../../shared/types' +import type { GitFileStatus } from '../../../../shared/git-status-types' import type { OpenFile } from '../../store/slices/editor' import { getUntitledFileRoot } from '@/components/editor/untitled-file-rename-path' import { preventMiddleButtonDefault } from './middle-button-default-guard' diff --git a/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx b/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx index d2e086f7496..6af2508048c 100644 --- a/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx +++ b/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx @@ -8,7 +8,7 @@ import { useAgentDetectionTargetForWorktree } from '@/hooks/useAgentDetectionTar import { useDetectedAgents } from '@/hooks/useDetectedAgents' import { useOptionalShortcutLabel } from '@/hooks/useShortcutLabel' import { launchAgentInNewTab } from '@/lib/launch-agent-in-new-tab' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { LaunchSource } from '../../../../shared/telemetry-events' import { DEFAULT_DISABLED_TUI_AGENTS, diff --git a/src/renderer/src/components/tab-bar/RecentTabSwitcher.test.tsx b/src/renderer/src/components/tab-bar/RecentTabSwitcher.test.tsx index e0a0ab9f7f0..31f01c65996 100644 --- a/src/renderer/src/components/tab-bar/RecentTabSwitcher.test.tsx +++ b/src/renderer/src/components/tab-bar/RecentTabSwitcher.test.tsx @@ -3,7 +3,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Tab } from '../../../../shared/types' +import type { Tab } from '../../../../shared/tab-types' import type { AppState } from '../../store/types' const { activateCyclableTabMock, getStateMock } = vi.hoisted(() => ({ diff --git a/src/renderer/src/components/tab-bar/SortableTab.tsx b/src/renderer/src/components/tab-bar/SortableTab.tsx index cec2a0f0082..b1417b57c13 100644 --- a/src/renderer/src/components/tab-bar/SortableTab.tsx +++ b/src/renderer/src/components/tab-bar/SortableTab.tsx @@ -6,7 +6,7 @@ import { useTabAgent } from '@/lib/use-tab-agent' import { isImeCompositionKeyDown } from '@/lib/ime-composition-keyboard-event' import { Input } from '@/components/ui/input' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { TabDragItemData } from '../tab-group/useTabDragSplit' import { useAppStore } from '../../store' import { diff --git a/src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx b/src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx index 86d48961284..c10118dc45d 100644 --- a/src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx +++ b/src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx @@ -17,7 +17,7 @@ import { DropdownMenuShortcut, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { useAppStore } from '../../store' import { formatShortcutLabel, useOptionalShortcutLabel } from '@/hooks/useShortcutLabel' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/tab-bar/TabBarCreateEntry.keyboard.test.tsx b/src/renderer/src/components/tab-bar/TabBarCreateEntry.keyboard.test.tsx index bb51c99d14c..0480bbcac87 100644 --- a/src/renderer/src/components/tab-bar/TabBarCreateEntry.keyboard.test.tsx +++ b/src/renderer/src/components/tab-bar/TabBarCreateEntry.keyboard.test.tsx @@ -3,7 +3,9 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Tab, TabGroup, TerminalTab, Worktree } from '../../../../shared/types' +import type { Tab, TabGroup } from '../../../../shared/tab-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { TabEntryOption } from './tab-create-entry-action' import type { TabAgentLaunchOption } from './tab-agent-launch-options' // Unmocked on purpose: the empty-query message must stay in step with the diff --git a/src/renderer/src/components/tab-bar/TabBarQuickCommandsButton.tsx b/src/renderer/src/components/tab-bar/TabBarQuickCommandsButton.tsx index 776e7939b3e..ae831a418f9 100644 --- a/src/renderer/src/components/tab-bar/TabBarQuickCommandsButton.tsx +++ b/src/renderer/src/components/tab-bar/TabBarQuickCommandsButton.tsx @@ -13,7 +13,7 @@ import { import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' import { runQuickCommandInNewTab } from '@/lib/run-quick-command-in-new-tab' -import type { TerminalQuickCommand } from '../../../../shared/types' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' import { useConfirmationDialog } from '@/components/confirmation-dialog-context' import { translate } from '@/i18n/i18n' import { TabBarQuickCommandsMenu } from './TabBarQuickCommandsMenu' diff --git a/src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.tsx b/src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.tsx index 44c75c39586..431157b0731 100644 --- a/src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.tsx +++ b/src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.tsx @@ -1,7 +1,8 @@ import { AgentStateDot } from '@/components/AgentStateDot' import { AgentIcon } from '@/lib/agent-catalog' import { cn } from '@/lib/utils' -import type { TerminalTab, TuiAgent } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { FilledBellIcon } from '../sidebar/WorktreeCardHelpers' import { ShellIcon } from './shell-icons' import { diff --git a/src/renderer/src/components/tab-bar/editor-tab-local-open-guard.ts b/src/renderer/src/components/tab-bar/editor-tab-local-open-guard.ts index 37302887f6d..574d6507232 100644 --- a/src/renderer/src/components/tab-bar/editor-tab-local-open-guard.ts +++ b/src/renderer/src/components/tab-bar/editor-tab-local-open-guard.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { isLocalPathOpenBlocked } from '@/lib/local-path-open-guard' import { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client' diff --git a/src/renderer/src/components/tab-bar/group-tab-order.test.ts b/src/renderer/src/components/tab-bar/group-tab-order.test.ts index 5835288bbc8..797fea97848 100644 --- a/src/renderer/src/components/tab-bar/group-tab-order.test.ts +++ b/src/renderer/src/components/tab-bar/group-tab-order.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Tab, TabGroup } from '../../../../shared/types' +import type { Tab, TabGroup } from '../../../../shared/tab-types' import type { AppState } from '../../store/types' import { getActiveTabNavOrder, getGroupVisibleTabOrder } from './group-tab-order' diff --git a/src/renderer/src/components/tab-bar/group-tab-order.ts b/src/renderer/src/components/tab-bar/group-tab-order.ts index 8a16e51786b..d9396e3a53c 100644 --- a/src/renderer/src/components/tab-bar/group-tab-order.ts +++ b/src/renderer/src/components/tab-bar/group-tab-order.ts @@ -1,4 +1,4 @@ -import type { Tab, TabGroup } from '../../../../shared/types' +import type { Tab, TabGroup } from '../../../../shared/tab-types' import type { AppState } from '../../store/types' import { reconcileTabOrder } from './reconcile-order' diff --git a/src/renderer/src/components/tab-bar/open-tab-search-entries.ts b/src/renderer/src/components/tab-bar/open-tab-search-entries.ts index 1b16a7d845f..4a808bcf6ce 100644 --- a/src/renderer/src/components/tab-bar/open-tab-search-entries.ts +++ b/src/renderer/src/components/tab-bar/open-tab-search-entries.ts @@ -1,7 +1,8 @@ // Builds one worktree's tabs, browser pages and simulator tabs into the shapes // the three Cmd+J engines search. -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { buildSearchableBrowserPages } from '@/lib/browser-palette-page-entries' import type { SearchableBrowserPage } from '@/lib/browser-palette-search' import { diff --git a/src/renderer/src/components/tab-bar/open-tab-search.test.ts b/src/renderer/src/components/tab-bar/open-tab-search.test.ts index 1476bdaa3bb..75103fe80c3 100644 --- a/src/renderer/src/components/tab-bar/open-tab-search.test.ts +++ b/src/renderer/src/components/tab-bar/open-tab-search.test.ts @@ -1,11 +1,7 @@ import { describe, expect, it } from 'vitest' -import type { - BrowserPage, - BrowserWorkspace, - Tab, - TabContentType, - Worktree -} from '../../../../shared/types' +import type { BrowserPage, BrowserWorkspace } from '../../../../shared/browser-workspace-types' +import type { Tab, TabContentType } from '../../../../shared/tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { SearchableBrowserPage } from '@/lib/browser-palette-search' import type { SearchableSimulatorTab } from '@/lib/simulator-palette-search' import type { SearchableWorkspaceTab } from '@/lib/workspace-tab-palette-search' diff --git a/src/renderer/src/components/tab-bar/recent-tab-switching.test.ts b/src/renderer/src/components/tab-bar/recent-tab-switching.test.ts index 7f52e4ae1d1..49e0a4da7f6 100644 --- a/src/renderer/src/components/tab-bar/recent-tab-switching.test.ts +++ b/src/renderer/src/components/tab-bar/recent-tab-switching.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { AppState } from '../../store/types' -import type { Tab } from '../../../../shared/types' +import type { Tab } from '../../../../shared/tab-types' import { buildRecentTabSwitcherModel, getNextRecentTabSwitcherIndex, diff --git a/src/renderer/src/components/tab-bar/recent-tab-switching.ts b/src/renderer/src/components/tab-bar/recent-tab-switching.ts index 89070ba14ec..a2b5fba9fab 100644 --- a/src/renderer/src/components/tab-bar/recent-tab-switching.ts +++ b/src/renderer/src/components/tab-bar/recent-tab-switching.ts @@ -1,4 +1,4 @@ -import type { CtrlTabOrderMode, Tab, TabContentType, TabGroup } from '../../../../shared/types' +import type { CtrlTabOrderMode, Tab, TabContentType, TabGroup } from '../../../../shared/tab-types' import { resolveUnifiedTabLabel } from '../../../../shared/tab-title-resolution' import type { AppState } from '../../store/types' import { sanitizeRecentTabIds } from '../../store/slices/tab-group-state' diff --git a/src/renderer/src/components/tab-bar/tab-agent-launch-options.ts b/src/renderer/src/components/tab-bar/tab-agent-launch-options.ts index b15f6f7c004..bc9fc575c20 100644 --- a/src/renderer/src/components/tab-bar/tab-agent-launch-options.ts +++ b/src/renderer/src/components/tab-bar/tab-agent-launch-options.ts @@ -1,6 +1,6 @@ import { getAgentCatalog } from '@/lib/agent-catalog' import { normalizeMatchQuery, tokenizeMatchValue } from './query-token-match' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' export type TabAgentLaunchOption = { agent: TuiAgent diff --git a/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts b/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts index e460318e609..38d6f2e267f 100644 --- a/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts +++ b/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { shallow } from 'zustand/shallow' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' -import type { TerminalLayoutSnapshot } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' import { findTabAgentEntry } from '../native-chat/native-chat-tab-agent-entry' import { createTabBarAgentProjectionSelector, diff --git a/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.ts b/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.ts index b320d5e0ddc..420949992d8 100644 --- a/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.ts +++ b/src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.ts @@ -1,5 +1,5 @@ import type { AgentStatusEntry, AgentType } from '../../../../shared/agent-status-types' -import type { TerminalLayoutSnapshot } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' import { isNativeChatTabWideFallbackSafe, resolveNativeChatActiveLayoutLeafId diff --git a/src/renderer/src/components/tab-bar/tab-bar-item-model.ts b/src/renderer/src/components/tab-bar/tab-bar-item-model.ts index 89e2891908d..0eee66d7b68 100644 --- a/src/renderer/src/components/tab-bar/tab-bar-item-model.ts +++ b/src/renderer/src/components/tab-bar/tab-bar-item-model.ts @@ -1,9 +1,6 @@ -import type { - BrowserTab as BrowserTabState, - Tab, - TerminalTab, - WorkspaceVisibleTabType -} from '../../../../shared/types' +import type { BrowserTab as BrowserTabState } from '../../../../shared/browser-workspace-types' +import type { Tab, WorkspaceVisibleTabType } from '../../../../shared/tab-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { resolveTerminalTabTitle } from '../../../../shared/tab-title-resolution' import type { OpenFile } from '../../store/slices/editor' import { getEditorDisplayLabel } from '@/components/editor/editor-labels' diff --git a/src/renderer/src/components/tab-bar/tab-bar-props.ts b/src/renderer/src/components/tab-bar/tab-bar-props.ts index 6f8d93ec0a7..59d2ef12fff 100644 --- a/src/renderer/src/components/tab-bar/tab-bar-props.ts +++ b/src/renderer/src/components/tab-bar/tab-bar-props.ts @@ -1,8 +1,6 @@ -import type { - BrowserTab as BrowserTabState, - TerminalTab, - WorkspaceVisibleTabType -} from '../../../../shared/types' +import type { BrowserTab as BrowserTabState } from '../../../../shared/browser-workspace-types' +import type { WorkspaceVisibleTabType } from '../../../../shared/tab-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { OpenFile } from '../../store/slices/editor' import type { HoveredTabInsertion } from '../tab-group/useTabDragSplit' import type { TabCreateEntryArgs } from './tab-create-entry-action' diff --git a/src/renderer/src/components/tab-bar/tab-create-entry-local-path.test.ts b/src/renderer/src/components/tab-bar/tab-create-entry-local-path.test.ts index 8a11f2262ee..5f4a887574a 100644 --- a/src/renderer/src/components/tab-bar/tab-create-entry-local-path.test.ts +++ b/src/renderer/src/components/tab-bar/tab-create-entry-local-path.test.ts @@ -1,5 +1,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import type { FolderWorkspace, ProjectGroup, Repo } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import { useAppStore } from '@/store' import { diff --git a/src/renderer/src/components/tab-bar/tab-create-entry-props.ts b/src/renderer/src/components/tab-bar/tab-create-entry-props.ts index 56a4bca3abc..ba44b1d932e 100644 --- a/src/renderer/src/components/tab-bar/tab-create-entry-props.ts +++ b/src/renderer/src/components/tab-bar/tab-create-entry-props.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { TabCreateEntryArgs } from './tab-create-entry-action' import type { TabAgentLaunchOption } from './tab-agent-launch-options' import type { TabCreateMenuOption } from './tab-create-menu-options' diff --git a/src/renderer/src/components/tab-bar/tab-move-to-pane-column.test.ts b/src/renderer/src/components/tab-bar/tab-move-to-pane-column.test.ts index 5c9abb972a7..01dae6c0f98 100644 --- a/src/renderer/src/components/tab-bar/tab-move-to-pane-column.test.ts +++ b/src/renderer/src/components/tab-bar/tab-move-to-pane-column.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { useAppStore } from '../../store' -import type { Tab } from '../../../../shared/types' +import type { Tab } from '../../../../shared/tab-types' import { canMoveTabToNewPaneColumn, moveTabToNewPaneColumn } from './tab-move-to-pane-column' const WT = 'wt-1' diff --git a/src/renderer/src/components/tab-bar/tab-title-tooltip.test.tsx b/src/renderer/src/components/tab-bar/tab-title-tooltip.test.tsx index 8105e0f3263..05579cb7b4d 100644 --- a/src/renderer/src/components/tab-bar/tab-title-tooltip.test.tsx +++ b/src/renderer/src/components/tab-bar/tab-title-tooltip.test.tsx @@ -1,12 +1,10 @@ import { cloneElement, isValidElement, type ReactElement, type ReactNode } from 'react' import { renderToStaticMarkup } from 'react-dom/server' import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { - BrowserTab as BrowserTabState, - GitFileStatus, - TerminalTab, - TuiAgent -} from '../../../../shared/types' +import type { BrowserTab as BrowserTabState } from '../../../../shared/browser-workspace-types' +import type { GitFileStatus } from '../../../../shared/git-status-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { OpenFile } from '../../store/slices/editor' import type { TabDragItemData } from '../tab-group/useTabDragSplit' import BrowserTab from './BrowserTab' diff --git a/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts b/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts index 133597431e0..e475ae49a4d 100644 --- a/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts +++ b/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { hasUnreadAgentCompletionForTerminalTab, resetTerminalTabActivityFlagsCacheForTest, diff --git a/src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts b/src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts index abdcc584881..afba5dc4c90 100644 --- a/src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts +++ b/src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts @@ -5,7 +5,7 @@ import { type AgentStatusEntry } from '../../../../shared/agent-status-types' import { parseLegacyNumericPaneKey, parsePaneKey } from '../../../../shared/stable-pane-id' -import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' // Why: a terminal tab is a container of panes, exactly like a worktree card is // a container of tabs. Reuse the WorktreeCard status vocabulary and resolver so diff --git a/src/renderer/src/components/tab-bar/terminal-tab-spinner-launch-agent.test.ts b/src/renderer/src/components/tab-bar/terminal-tab-spinner-launch-agent.test.ts index 4259ff8ade3..caa14841166 100644 --- a/src/renderer/src/components/tab-bar/terminal-tab-spinner-launch-agent.test.ts +++ b/src/renderer/src/components/tab-bar/terminal-tab-spinner-launch-agent.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { resetTerminalTabActivityFlagsCacheForTest, resolveTerminalTabActivityStatus diff --git a/src/renderer/src/components/tab-bar/use-open-tab-search.test.ts b/src/renderer/src/components/tab-bar/use-open-tab-search.test.ts index 43a92cbf1f4..65c5cdb0908 100644 --- a/src/renderer/src/components/tab-bar/use-open-tab-search.test.ts +++ b/src/renderer/src/components/tab-bar/use-open-tab-search.test.ts @@ -2,16 +2,11 @@ import { act, renderHook } from '@testing-library/react' import { beforeEach, describe, expect, it } from 'vitest' -import type { - BrowserPage, - BrowserWorkspace, - Repo, - Tab, - TabContentType, - TabGroup, - TerminalTab, - Worktree -} from '../../../../shared/types' +import type { BrowserPage, BrowserWorkspace } from '../../../../shared/browser-workspace-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Tab, TabContentType, TabGroup } from '../../../../shared/tab-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import { useAppStore } from '@/store' import type { AppState } from '@/store/types' import { useOpenTabSearch } from './use-open-tab-search' diff --git a/src/renderer/src/components/tab-bar/use-tab-bar-create-menu-controller.ts b/src/renderer/src/components/tab-bar/use-tab-bar-create-menu-controller.ts index 7eb28f2bcc8..d12b275b556 100644 --- a/src/renderer/src/components/tab-bar/use-tab-bar-create-menu-controller.ts +++ b/src/renderer/src/components/tab-bar/use-tab-bar-create-menu-controller.ts @@ -1,6 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { translate } from '@/i18n/i18n' import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface' import { launchAgentInNewTab } from '@/lib/launch-agent-in-new-tab' diff --git a/src/renderer/src/components/tab-bar/use-tab-bar-item-projection.ts b/src/renderer/src/components/tab-bar/use-tab-bar-item-projection.ts index 0137c1f7cd7..3da80876b98 100644 --- a/src/renderer/src/components/tab-bar/use-tab-bar-item-projection.ts +++ b/src/renderer/src/components/tab-bar/use-tab-bar-item-projection.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react' -import type { GitFileStatus, Tab } from '../../../../shared/types' +import type { GitFileStatus } from '../../../../shared/git-status-types' +import type { Tab } from '../../../../shared/tab-types' import type { TabBarProps } from './tab-bar-props' import { buildOrderedTabItems, diff --git a/src/renderer/src/components/tab-bar/use-tab-bar-quick-command-search-input.test.ts b/src/renderer/src/components/tab-bar/use-tab-bar-quick-command-search-input.test.ts index df75eeae463..25058e3f44a 100644 --- a/src/renderer/src/components/tab-bar/use-tab-bar-quick-command-search-input.test.ts +++ b/src/renderer/src/components/tab-bar/use-tab-bar-quick-command-search-input.test.ts @@ -4,7 +4,7 @@ import { renderHook } from '@testing-library/react' import { describe, expect, it, vi } from 'vitest' import { useTabBarQuickCommandSearchInput } from './use-tab-bar-quick-command-search-input' -import type { TerminalQuickCommand } from '../../../../shared/types' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' const command = { id: 'cmd-1', label: 'Run tests' } as unknown as TerminalQuickCommand diff --git a/src/renderer/src/components/tab-bar/use-tab-bar-runtime-model.ts b/src/renderer/src/components/tab-bar/use-tab-bar-runtime-model.ts index 4515de2a071..298c77f8777 100644 --- a/src/renderer/src/components/tab-bar/use-tab-bar-runtime-model.ts +++ b/src/renderer/src/components/tab-bar/use-tab-bar-runtime-model.ts @@ -1,6 +1,9 @@ import { useMemo } from 'react' import { useShallow } from 'zustand/react/shallow' -import type { GitFileStatus, GlobalSettings, Tab, TuiAgent } from '../../../../shared/types' +import type { GitFileStatus } from '../../../../shared/git-status-types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Tab } from '../../../../shared/tab-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { ProjectExecutionRuntimeResolution } from '../../../../shared/project-execution-runtime' import { useAppStore } from '../../store' import { buildStatusMap } from '../right-sidebar/status-display' diff --git a/src/renderer/src/components/tab-group/TabGroupPanel.context-menu.test.ts b/src/renderer/src/components/tab-group/TabGroupPanel.context-menu.test.ts index 58aa57400c4..1c90210820e 100644 --- a/src/renderer/src/components/tab-group/TabGroupPanel.context-menu.test.ts +++ b/src/renderer/src/components/tab-group/TabGroupPanel.context-menu.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Tab, TabContentType } from '../../../../shared/types' +import type { Tab, TabContentType } from '../../../../shared/tab-types' import { resolveGroupTabFromVisibleId } from './tab-group-visible-id' function tab(id: string, entityId: string, contentType: TabContentType, sortOrder: number): Tab { diff --git a/src/renderer/src/components/tab-group/TabGroupSplitLayout.tsx b/src/renderer/src/components/tab-group/TabGroupSplitLayout.tsx index 9a97ae673e4..77f04fb3023 100644 --- a/src/renderer/src/components/tab-group/TabGroupSplitLayout.tsx +++ b/src/renderer/src/components/tab-group/TabGroupSplitLayout.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { DndContext, DragOverlay } from '@dnd-kit/core' -import type { TabGroupLayoutNode } from '../../../../shared/types' +import type { TabGroupLayoutNode } from '../../../../shared/tab-types' import { useAppStore } from '../../store' import TabGroupPanel from './TabGroupPanel' import TabDragPreview from '../tab-bar/TabDragPreview' diff --git a/src/renderer/src/components/tab-group/tab-drag-data.ts b/src/renderer/src/components/tab-group/tab-drag-data.ts index ad55f0edd10..572385a0d14 100644 --- a/src/renderer/src/components/tab-group/tab-drag-data.ts +++ b/src/renderer/src/components/tab-group/tab-drag-data.ts @@ -1,4 +1,5 @@ -import type { TabGroup, TuiAgent } from '../../../../shared/types' +import type { TabGroup } from '../../../../shared/tab-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { TabSplitDirection } from '../../store/slices/tabs' export type TabDropZone = 'center' | TabSplitDirection diff --git a/src/renderer/src/components/tab-group/tab-drag-preview-activation.test.ts b/src/renderer/src/components/tab-group/tab-drag-preview-activation.test.ts index d6a373fc865..019b3e297ee 100644 --- a/src/renderer/src/components/tab-group/tab-drag-preview-activation.test.ts +++ b/src/renderer/src/components/tab-group/tab-drag-preview-activation.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest' -import type { Tab } from '../../../../shared/types' +import type { Tab } from '../../../../shared/tab-types' import { useAppStore } from '../../store' import { applyDragPreviewTab, diff --git a/src/renderer/src/components/tab-group/tab-group-panel-split-target.test.ts b/src/renderer/src/components/tab-group/tab-group-panel-split-target.test.ts index ceca4d16962..7f72c716fe3 100644 --- a/src/renderer/src/components/tab-group/tab-group-panel-split-target.test.ts +++ b/src/renderer/src/components/tab-group/tab-group-panel-split-target.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { TabGroup, TabGroupLayoutNode } from '../../../../shared/types' +import type { TabGroup, TabGroupLayoutNode } from '../../../../shared/tab-types' import type { TabDragItemData } from './useTabDragSplit' import { captureTabGroupPanelGeometrySnapshot, diff --git a/src/renderer/src/components/tab-group/tab-group-panel-split-target.ts b/src/renderer/src/components/tab-group/tab-group-panel-split-target.ts index 80fefad4080..d7bab07f12f 100644 --- a/src/renderer/src/components/tab-group/tab-group-panel-split-target.ts +++ b/src/renderer/src/components/tab-group/tab-group-panel-split-target.ts @@ -1,5 +1,5 @@ import type { DragEndEvent, DragMoveEvent, DragOverEvent } from '@dnd-kit/core' -import type { TabGroup, TabGroupLayoutNode } from '../../../../shared/types' +import type { TabGroup, TabGroupLayoutNode } from '../../../../shared/tab-types' import { isPaneColumnSplitDropNoOp } from '../../store/slices/pane-column-split-drop-no-op' import { resolvePaneColumnEdgeZone, diff --git a/src/renderer/src/components/tab-group/tab-group-visible-id.ts b/src/renderer/src/components/tab-group/tab-group-visible-id.ts index 1e94a3df964..92cca52df7d 100644 --- a/src/renderer/src/components/tab-group/tab-group-visible-id.ts +++ b/src/renderer/src/components/tab-group/tab-group-visible-id.ts @@ -1,4 +1,4 @@ -import type { Tab } from '../../../../shared/types' +import type { Tab } from '../../../../shared/tab-types' export function resolveGroupTabFromVisibleId( groupTabs: readonly Tab[], diff --git a/src/renderer/src/components/tab-group/useTabDragSplit.test.ts b/src/renderer/src/components/tab-group/useTabDragSplit.test.ts index fa825031762..2b372c2c2c4 100644 --- a/src/renderer/src/components/tab-group/useTabDragSplit.test.ts +++ b/src/renderer/src/components/tab-group/useTabDragSplit.test.ts @@ -4,7 +4,7 @@ import { act, createElement } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Tab, TabGroup, TabGroupLayoutNode } from '../../../../shared/types' +import type { Tab, TabGroup, TabGroupLayoutNode } from '../../../../shared/tab-types' import { useAppStore } from '../../store' import type { TabDragItemData } from './useTabDragSplit' import { shouldActivateTabDragFromDistanceSample } from './tab-drag-pointer-sensor' diff --git a/src/renderer/src/components/tab-group/useTabDragSplit.ts b/src/renderer/src/components/tab-group/useTabDragSplit.ts index ce79cf03281..a3c5c49ce9c 100644 --- a/src/renderer/src/components/tab-group/useTabDragSplit.ts +++ b/src/renderer/src/components/tab-group/useTabDragSplit.ts @@ -14,7 +14,7 @@ import { useSensor, useSensors } from '@dnd-kit/core' -import type { TabGroup } from '../../../../shared/types' +import type { TabGroup } from '../../../../shared/tab-types' import { useAppStore } from '../../store' import { mirrorWebRuntimeTabMove } from '../tab-bar/web-runtime-tab-move-mirror' import { diff --git a/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts b/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts index d2dcdcb965b..6bb2f7a5992 100644 --- a/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts +++ b/src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts @@ -3,12 +3,9 @@ import { useCallback, useMemo } from 'react' import { toast } from 'sonner' import { useShallow } from 'zustand/react/shallow' import type { OpenFile } from '@/store/slices/editor' -import type { - BrowserTab as BrowserTabState, - Tab, - TabGroup, - TerminalTab -} from '../../../../shared/types' +import type { BrowserTab as BrowserTabState } from '../../../../shared/browser-workspace-types' +import type { Tab, TabGroup } from '../../../../shared/tab-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { resolveUnifiedTabLabel } from '../../../../shared/tab-title-resolution' import { useAppStore } from '../../store' import { destroyWorkspaceWebviews } from '../../store/slices/browser-webview-cleanup' diff --git a/src/renderer/src/components/task-page-cache-selectors.test.ts b/src/renderer/src/components/task-page-cache-selectors.test.ts index d06bf9ee623..968189a5417 100644 --- a/src/renderer/src/components/task-page-cache-selectors.test.ts +++ b/src/renderer/src/components/task-page-cache-selectors.test.ts @@ -2,7 +2,9 @@ import { describe, expect, it } from 'vitest' import { shallow } from 'zustand/shallow' import { workItemsCacheKey, type CacheEntry } from '@/store/slices/github' -import type { GitHubWorkItem, LinearCollectionResult, LinearIssue } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import type { LinearIssue } from '../../../shared/linear/issue-types' +import type { LinearCollectionResult } from '../../../shared/linear/workspace-types' import { buildTaskPageRepoSourceState, selectTaskPageUnresolvedSourceRepos, diff --git a/src/renderer/src/components/task-page-cache-selectors.ts b/src/renderer/src/components/task-page-cache-selectors.ts index 525fa51a336..403a8a97dd1 100644 --- a/src/renderer/src/components/task-page-cache-selectors.ts +++ b/src/renderer/src/components/task-page-cache-selectors.ts @@ -4,7 +4,7 @@ import { type WorkItemsCacheError, type WorkItemsCacheSources } from '@/store/slices/github' -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import { taskPageWorkItemKey, taskPageWorkItemStatusSignature, diff --git a/src/renderer/src/components/task-page-checks-pill.ts b/src/renderer/src/components/task-page-checks-pill.ts index ba3252a46f3..d119f1eb69e 100644 --- a/src/renderer/src/components/task-page-checks-pill.ts +++ b/src/renderer/src/components/task-page-checks-pill.ts @@ -1,5 +1,5 @@ import { getProviderChecksLabel } from '../../../shared/provider-check-summary' -import type { ProviderCheckSummary } from '../../../shared/types' +import type { ProviderCheckSummary } from '../../../shared/github/pull-request-types' type ChecksPillItem = { checksSummary?: ProviderCheckSummary } diff --git a/src/renderer/src/components/task-page-default-repo-selection.test.ts b/src/renderer/src/components/task-page-default-repo-selection.test.ts index 957cd67a6e5..d87ceeec5ba 100644 --- a/src/renderer/src/components/task-page-default-repo-selection.test.ts +++ b/src/renderer/src/components/task-page-default-repo-selection.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import { getDefaultTaskRepoSelection, getTaskEligibleRepos, diff --git a/src/renderer/src/components/task-page-default-repo-selection.ts b/src/renderer/src/components/task-page-default-repo-selection.ts index e1b2eca2ebd..e5c41c08b18 100644 --- a/src/renderer/src/components/task-page-default-repo-selection.ts +++ b/src/renderer/src/components/task-page-default-repo-selection.ts @@ -5,7 +5,7 @@ import { isProjectRemoteIdentityPending } from '../../../shared/project-host-setup-projection' import { isGitRepoKind } from '../../../shared/repo-kind' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' export type TaskProjectPickerGroup = { projectKey: string diff --git a/src/renderer/src/components/task-page-github-dialog-state-authority.test.ts b/src/renderer/src/components/task-page-github-dialog-state-authority.test.ts index 2f4d1646d21..b0bde81eb82 100644 --- a/src/renderer/src/components/task-page-github-dialog-state-authority.test.ts +++ b/src/renderer/src/components/task-page-github-dialog-state-authority.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest' -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import { getTaskSourceCacheScope } from '../../../shared/task-source-context' import { assertTaskPageGitHubDialogStateAuthority } from './task-page-github-dialog-state-authority' import { diff --git a/src/renderer/src/components/task-page-github-dialog-state-authority.ts b/src/renderer/src/components/task-page-github-dialog-state-authority.ts index a372762e141..2918e1eb4d0 100644 --- a/src/renderer/src/components/task-page-github-dialog-state-authority.ts +++ b/src/renderer/src/components/task-page-github-dialog-state-authority.ts @@ -1,4 +1,4 @@ -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import { getTaskSourceCacheScope, type TaskSourceContext diff --git a/src/renderer/src/components/task-page-github-resume-cache.ts b/src/renderer/src/components/task-page-github-resume-cache.ts index c42ac9a0c3d..8cd4d14b029 100644 --- a/src/renderer/src/components/task-page-github-resume-cache.ts +++ b/src/renderer/src/components/task-page-github-resume-cache.ts @@ -1,4 +1,4 @@ -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' export const TASK_PAGE_GITHUB_RESUME_CACHE_LIMIT = 5 export const TASK_PAGE_GITHUB_RESUME_CACHE_TTL_MS = 10 * 60_000 diff --git a/src/renderer/src/components/task-page-github-status-actions.test.ts b/src/renderer/src/components/task-page-github-status-actions.test.ts index b18226f6f88..d2105e722dd 100644 --- a/src/renderer/src/components/task-page-github-status-actions.test.ts +++ b/src/renderer/src/components/task-page-github-status-actions.test.ts @@ -5,7 +5,7 @@ import { getTaskPageGitHubDuplicateTargetErrorMessage, validateTaskPageGitHubDuplicateTarget } from './task-page-github-status-actions' -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' describe('TaskPage GitHub status actions', () => { it('builds completed and not planned close updates', () => { diff --git a/src/renderer/src/components/task-page-github-status-actions.ts b/src/renderer/src/components/task-page-github-status-actions.ts index 1d3ed687767..3722678a310 100644 --- a/src/renderer/src/components/task-page-github-status-actions.ts +++ b/src/renderer/src/components/task-page-github-status-actions.ts @@ -1,8 +1,8 @@ +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import type { GitHubIssueCloseReason, - GitHubIssueUpdate, - GitHubWorkItem -} from '../../../shared/types' + GitHubIssueUpdate +} from '../../../shared/issue-mutation-types' export type TaskPageGitHubCloseAction = | { stateReason: Exclude } diff --git a/src/renderer/src/components/task-page-github-status-state.ts b/src/renderer/src/components/task-page-github-status-state.ts index 45adc29016d..a61cb20ed09 100644 --- a/src/renderer/src/components/task-page-github-status-state.ts +++ b/src/renderer/src/components/task-page-github-status-state.ts @@ -1,4 +1,4 @@ -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' type GitHubStatusItem = Pick diff --git a/src/renderer/src/components/task-page-github-task-kind.ts b/src/renderer/src/components/task-page-github-task-kind.ts index 772fa2d1a6e..ab1b59bf896 100644 --- a/src/renderer/src/components/task-page-github-task-kind.ts +++ b/src/renderer/src/components/task-page-github-task-kind.ts @@ -1,6 +1,6 @@ import { getTaskPresetQuery } from '../../../shared/task-preset-query' import { parseTaskQuery } from '../../../shared/task-query' -import type { TaskViewPresetId } from '../../../shared/types' +import type { TaskViewPresetId } from '../../../shared/ui-chrome-types' import type { GitHubTaskKind } from '@/components/task-page-localized-options' export function isPRFocusedTaskView(preset: TaskViewPresetId | null, query: string): boolean { diff --git a/src/renderer/src/components/task-page-github-work-item-filter-membership.ts b/src/renderer/src/components/task-page-github-work-item-filter-membership.ts index b51ddd8bda3..e3607396b7b 100644 --- a/src/renderer/src/components/task-page-github-work-item-filter-membership.ts +++ b/src/renderer/src/components/task-page-github-work-item-filter-membership.ts @@ -1,5 +1,6 @@ import type { ParsedTaskQuery } from '../../../shared/task-query' -import type { GitHubAssignableUser, GitHubWorkItem } from '../../../shared/types' +import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import type { StickyHideEntry } from './task-page-github-work-item-mutation-registry' function includesLogin(users: readonly GitHubAssignableUser[] | undefined, login: string): boolean { diff --git a/src/renderer/src/components/task-page-github-work-item-mutation-composition.ts b/src/renderer/src/components/task-page-github-work-item-mutation-composition.ts index 25616c3c059..6e98c29cb9b 100644 --- a/src/renderer/src/components/task-page-github-work-item-mutation-composition.ts +++ b/src/renderer/src/components/task-page-github-work-item-mutation-composition.ts @@ -1,5 +1,6 @@ import type { ParsedTaskQuery } from '../../../shared/task-query' -import type { GitHubAssignableUser, GitHubWorkItem } from '../../../shared/types' +import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import { recomputeTaskPageGitHubItemSoftHide, shouldSoftHideTaskPageGitHubWorkItem diff --git a/src/renderer/src/components/task-page-github-work-item-mutation-lifecycle.ts b/src/renderer/src/components/task-page-github-work-item-mutation-lifecycle.ts index 3bad977b3be..8a693381628 100644 --- a/src/renderer/src/components/task-page-github-work-item-mutation-lifecycle.ts +++ b/src/renderer/src/components/task-page-github-work-item-mutation-lifecycle.ts @@ -1,5 +1,5 @@ import type { ParsedTaskQuery } from '../../../shared/task-query' -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { applyTaskPageGitHubListOps } from './task-page-github-work-item-mutation-patches' import { diff --git a/src/renderer/src/components/task-page-github-work-item-mutation-pages.ts b/src/renderer/src/components/task-page-github-work-item-mutation-pages.ts index 65b9ffba648..6b62509a05b 100644 --- a/src/renderer/src/components/task-page-github-work-item-mutation-pages.ts +++ b/src/renderer/src/components/task-page-github-work-item-mutation-pages.ts @@ -1,4 +1,4 @@ -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { getRegistryMergedTaskPageGitHubWorkItem } from './task-page-github-work-item-mutation-composition' import { diff --git a/src/renderer/src/components/task-page-github-work-item-mutation-patches.test.ts b/src/renderer/src/components/task-page-github-work-item-mutation-patches.test.ts index dd5a305ccea..f32558adf09 100644 --- a/src/renderer/src/components/task-page-github-work-item-mutation-patches.test.ts +++ b/src/renderer/src/components/task-page-github-work-item-mutation-patches.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import { applyTaskPageGitHubListOps, buildTaskPageGitHubWorkItemMutationPatch, diff --git a/src/renderer/src/components/task-page-github-work-item-mutation-patches.ts b/src/renderer/src/components/task-page-github-work-item-mutation-patches.ts index f06db3f1ef8..c72d0026105 100644 --- a/src/renderer/src/components/task-page-github-work-item-mutation-patches.ts +++ b/src/renderer/src/components/task-page-github-work-item-mutation-patches.ts @@ -1,4 +1,5 @@ -import type { GitHubAssignableUser, GitHubWorkItem } from '../../../shared/types' +import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import type { TaskPageGitHubCloseAction } from './task-page-github-status-actions' import { taskPageGitHubListOpKey, diff --git a/src/renderer/src/components/task-page-github-work-item-mutation-registry.ts b/src/renderer/src/components/task-page-github-work-item-mutation-registry.ts index bfd575ea4ab..6988ff7f7b3 100644 --- a/src/renderer/src/components/task-page-github-work-item-mutation-registry.ts +++ b/src/renderer/src/components/task-page-github-work-item-mutation-registry.ts @@ -1,4 +1,4 @@ -import type { GitHubAssignableUser } from '../../../shared/types' +import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' import type { PendingOp, StickyHideEntry, diff --git a/src/renderer/src/components/task-page-github-work-item-mutation-regressions.test.ts b/src/renderer/src/components/task-page-github-work-item-mutation-regressions.test.ts index fd931a8045b..a24a20e33b6 100644 --- a/src/renderer/src/components/task-page-github-work-item-mutation-regressions.test.ts +++ b/src/renderer/src/components/task-page-github-work-item-mutation-regressions.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it } from 'vitest' import type { ParsedTaskQuery } from '../../../shared/task-query' -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import { adoptQuietSearchFieldsForItem, advanceTaskPageQuietRevalidateScope, diff --git a/src/renderer/src/components/task-page-github-work-item-mutation-types.ts b/src/renderer/src/components/task-page-github-work-item-mutation-types.ts index f12310afc88..2aaa09e3bd0 100644 --- a/src/renderer/src/components/task-page-github-work-item-mutation-types.ts +++ b/src/renderer/src/components/task-page-github-work-item-mutation-types.ts @@ -1,5 +1,5 @@ import type { ParsedTaskQuery } from '../../../shared/task-query' -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import type { TaskPageGitHubMutationIntent } from './task-page-github-work-item-mutation-patches' import type { TaskPageGitHubMutationKey } from './task-page-github-work-item-mutation-registry' diff --git a/src/renderer/src/components/task-page-github-work-item-mutations.test.ts b/src/renderer/src/components/task-page-github-work-item-mutations.test.ts index ad59620ef68..f50c40d4021 100644 --- a/src/renderer/src/components/task-page-github-work-item-mutations.test.ts +++ b/src/renderer/src/components/task-page-github-work-item-mutations.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { ParsedTaskQuery } from '../../../shared/task-query' -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import { adoptQuietSearchFieldsForItem, applyPendingTaskPageGitHubMutationsToItems, diff --git a/src/renderer/src/components/task-page-github-work-item-mutations.ts b/src/renderer/src/components/task-page-github-work-item-mutations.ts index 450c0abc4e9..28e05a1ccb4 100644 --- a/src/renderer/src/components/task-page-github-work-item-mutations.ts +++ b/src/renderer/src/components/task-page-github-work-item-mutations.ts @@ -1,4 +1,4 @@ -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import { getTaskSourceCacheScope, type TaskSourceContext diff --git a/src/renderer/src/components/task-page-github-work-item-quiet-adopt.ts b/src/renderer/src/components/task-page-github-work-item-quiet-adopt.ts index 269f07164ae..5547986ab2e 100644 --- a/src/renderer/src/components/task-page-github-work-item-quiet-adopt.ts +++ b/src/renderer/src/components/task-page-github-work-item-quiet-adopt.ts @@ -1,4 +1,4 @@ -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { loginSetOfUsers, loginSetsEqual } from './task-page-github-work-item-mutation-patches' import { diff --git a/src/renderer/src/components/task-page-github-work-item-quiet-revalidate.ts b/src/renderer/src/components/task-page-github-work-item-quiet-revalidate.ts index 5895eb28f85..a482726f8c8 100644 --- a/src/renderer/src/components/task-page-github-work-item-quiet-revalidate.ts +++ b/src/renderer/src/components/task-page-github-work-item-quiet-revalidate.ts @@ -1,4 +1,4 @@ -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { adoptQuietSearchFieldsForItem, diff --git a/src/renderer/src/components/task-page-github-work-item-registry-types.ts b/src/renderer/src/components/task-page-github-work-item-registry-types.ts index a1e93064812..fecd93c4465 100644 --- a/src/renderer/src/components/task-page-github-work-item-registry-types.ts +++ b/src/renderer/src/components/task-page-github-work-item-registry-types.ts @@ -1,4 +1,5 @@ -import type { GitHubAssignableUser, GitHubWorkItem } from '../../../shared/types' +import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' export type TaskPageGitHubListFamily = 'assignees' | 'reviewRequests' export type TaskPageGitHubMutationKey = { diff --git a/src/renderer/src/components/task-page-github-work-item-status.ts b/src/renderer/src/components/task-page-github-work-item-status.ts index 5e54688305b..d5eb8cf7102 100644 --- a/src/renderer/src/components/task-page-github-work-item-status.ts +++ b/src/renderer/src/components/task-page-github-work-item-status.ts @@ -1,5 +1,5 @@ import { translate } from '@/i18n/i18n' -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' export type GitHubWorkItemStatusItem = Pick diff --git a/src/renderer/src/components/task-page-jira-cache-selectors.test.ts b/src/renderer/src/components/task-page-jira-cache-selectors.test.ts index f5f60d3cbe2..bc4a4c07ffa 100644 --- a/src/renderer/src/components/task-page-jira-cache-selectors.test.ts +++ b/src/renderer/src/components/task-page-jira-cache-selectors.test.ts @@ -3,7 +3,7 @@ import { getTaskSourceCacheScope, type TaskSourceContext } from '../../../shared/task-source-context' -import type { JiraIssue } from '../../../shared/types' +import type { JiraIssue } from '../../../shared/jira-types' import { findTaskPageJiraIssue } from './task-page-jira-cache-selectors' function jiraSourceContext(environmentId: string): TaskSourceContext { diff --git a/src/renderer/src/components/task-page-jira-cache-selectors.ts b/src/renderer/src/components/task-page-jira-cache-selectors.ts index 5426c404dbf..969160f13f8 100644 --- a/src/renderer/src/components/task-page-jira-cache-selectors.ts +++ b/src/renderer/src/components/task-page-jira-cache-selectors.ts @@ -3,7 +3,7 @@ import { getTaskSourceCacheScope, type TaskSourceContext } from '../../../shared/task-source-context' -import type { JiraIssue } from '../../../shared/types' +import type { JiraIssue } from '../../../shared/jira-types' type JiraIssueCache = Record> type JiraSearchCache = Record> diff --git a/src/renderer/src/components/task-page-jira-create-fields.test.ts b/src/renderer/src/components/task-page-jira-create-fields.test.ts index 8cc24872386..11210e347db 100644 --- a/src/renderer/src/components/task-page-jira-create-fields.test.ts +++ b/src/renderer/src/components/task-page-jira-create-fields.test.ts @@ -8,7 +8,7 @@ import { getJiraCreateOptionPayload, isVisibleJiraCreateField } from './task-page-jira-create-fields' -import type { JiraCreateField } from '../../../shared/types' +import type { JiraCreateField } from '../../../shared/jira-types' function field(overrides: Partial = {}): JiraCreateField { return { key: 'customfield_1', name: 'Custom', required: true, ...overrides } diff --git a/src/renderer/src/components/task-page-jira-create-fields.ts b/src/renderer/src/components/task-page-jira-create-fields.ts index 568621ee3e7..932e64c2bb1 100644 --- a/src/renderer/src/components/task-page-jira-create-fields.ts +++ b/src/renderer/src/components/task-page-jira-create-fields.ts @@ -1,5 +1,5 @@ import { buildJiraCreateTextAdf } from '@/components/jira-create-adf' -import type { JiraCreateField } from '../../../shared/types' +import type { JiraCreateField } from '../../../shared/jira-types' const JIRA_CREATE_SYSTEM_FIELD_KEYS = new Set(['project', 'issuetype', 'summary', 'description']) diff --git a/src/renderer/src/components/task-page-jira-grouping.test.ts b/src/renderer/src/components/task-page-jira-grouping.test.ts index 44d8bce4fbf..fbfa5e5b069 100644 --- a/src/renderer/src/components/task-page-jira-grouping.test.ts +++ b/src/renderer/src/components/task-page-jira-grouping.test.ts @@ -14,7 +14,7 @@ import { getSingleJiraProjectScope, loadTaskPageJiraProjectStatusOrder } from './task-page-jira-status-order' -import type { JiraIssue, JiraProjectStatusOrder } from '../../../shared/types' +import type { JiraIssue, JiraProjectStatusOrder } from '../../../shared/jira-types' const { jiraGetProjectStatusOrderMock } = vi.hoisted(() => ({ jiraGetProjectStatusOrderMock: vi.fn() diff --git a/src/renderer/src/components/task-page-jira-issue-list.tsx b/src/renderer/src/components/task-page-jira-issue-list.tsx index 104f591ab76..989e0911591 100644 --- a/src/renderer/src/components/task-page-jira-issue-list.tsx +++ b/src/renderer/src/components/task-page-jira-issue-list.tsx @@ -6,7 +6,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/component import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { translate } from '@/i18n/i18n' import { cn } from '@/lib/utils' -import type { JiraIssue, JiraProjectStatusOrder } from '../../../shared/types' +import type { JiraIssue, JiraProjectStatusOrder } from '../../../shared/jira-types' export type TaskPageJiraIssueSection = { key: string diff --git a/src/renderer/src/components/task-page-jira-item-source-context.test.ts b/src/renderer/src/components/task-page-jira-item-source-context.test.ts index 53917453655..a635fb86f30 100644 --- a/src/renderer/src/components/task-page-jira-item-source-context.test.ts +++ b/src/renderer/src/components/task-page-jira-item-source-context.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { JiraIssue, JiraSite } from '../../../shared/types' +import type { JiraIssue, JiraSite } from '../../../shared/jira-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { bindTaskPageJiraItemSourceContext } from './task-page-jira-item-source-context' diff --git a/src/renderer/src/components/task-page-jira-item-source-context.ts b/src/renderer/src/components/task-page-jira-item-source-context.ts index 1790797e3e6..be8ec6d4006 100644 --- a/src/renderer/src/components/task-page-jira-item-source-context.ts +++ b/src/renderer/src/components/task-page-jira-item-source-context.ts @@ -1,4 +1,4 @@ -import type { JiraIssue, JiraSite } from '../../../shared/types' +import type { JiraIssue, JiraSite } from '../../../shared/jira-types' import { normalizeTaskSourceContext, type TaskSourceContext diff --git a/src/renderer/src/components/task-page-jira-load-state.ts b/src/renderer/src/components/task-page-jira-load-state.ts index 77826b78126..0a542095ab0 100644 --- a/src/renderer/src/components/task-page-jira-load-state.ts +++ b/src/renderer/src/components/task-page-jira-load-state.ts @@ -1,4 +1,4 @@ -import type { JiraIssue } from '../../../shared/types' +import type { JiraIssue } from '../../../shared/jira-types' export type TaskPageJiraLoadError = { title: string diff --git a/src/renderer/src/components/task-page-jira-project-selection.test.ts b/src/renderer/src/components/task-page-jira-project-selection.test.ts index 82638f607cb..5f0bd24c884 100644 --- a/src/renderer/src/components/task-page-jira-project-selection.test.ts +++ b/src/renderer/src/components/task-page-jira-project-selection.test.ts @@ -4,7 +4,7 @@ import { compareJiraProjectsByDisplayLabel, getJiraProjectSelectionKey } from './task-page-jira-project-selection' -import type { JiraProject } from '../../../shared/types' +import type { JiraProject } from '../../../shared/jira-types' function project(overrides: Partial = {}): JiraProject { return { id: '1', key: 'AA', name: 'Alpha', ...overrides } diff --git a/src/renderer/src/components/task-page-jira-project-selection.ts b/src/renderer/src/components/task-page-jira-project-selection.ts index fe004b6cdf4..48a9af0dfc2 100644 --- a/src/renderer/src/components/task-page-jira-project-selection.ts +++ b/src/renderer/src/components/task-page-jira-project-selection.ts @@ -1,4 +1,4 @@ -import type { JiraProject } from '../../../shared/types' +import type { JiraProject } from '../../../shared/jira-types' export function getJiraProjectSelectionKey(project: JiraProject): string { return `${project.siteId ?? 'selected'}:${project.id}` diff --git a/src/renderer/src/components/task-page-jira-sorting.test.ts b/src/renderer/src/components/task-page-jira-sorting.test.ts index 767fbf7c1cb..0cdca7a6fe6 100644 --- a/src/renderer/src/components/task-page-jira-sorting.test.ts +++ b/src/renderer/src/components/task-page-jira-sorting.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { JiraIssue, JiraPriority } from '../../../shared/types' +import type { JiraIssue, JiraPriority } from '../../../shared/jira-types' import { getJiraPriorityWeight, sortJiraIssues } from './jira-issue-sorter' describe('TaskPage Jira sorting functionality', () => { diff --git a/src/renderer/src/components/task-page-jira-status-order.ts b/src/renderer/src/components/task-page-jira-status-order.ts index 8cb6ed39c37..e064f5732dc 100644 --- a/src/renderer/src/components/task-page-jira-status-order.ts +++ b/src/renderer/src/components/task-page-jira-status-order.ts @@ -1,4 +1,4 @@ -import type { JiraIssue, JiraProjectStatusOrder } from '../../../shared/types' +import type { JiraIssue, JiraProjectStatusOrder } from '../../../shared/jira-types' import { jiraGetProjectStatusOrder, type RuntimeJiraSettings } from '@/runtime/runtime-jira-client' import { createMetadataRequestStore, loadMetadata } from '@/hooks/metadata-request-cache' diff --git a/src/renderer/src/components/task-page-linear-cache-selectors.ts b/src/renderer/src/components/task-page-linear-cache-selectors.ts index d13536ea93f..31ac2730b27 100644 --- a/src/renderer/src/components/task-page-linear-cache-selectors.ts +++ b/src/renderer/src/components/task-page-linear-cache-selectors.ts @@ -1,5 +1,6 @@ import type { CacheEntry } from '@/store/slices/github' -import type { LinearCollectionResult, LinearIssue } from '../../../shared/types' +import type { LinearIssue } from '../../../shared/linear/issue-types' +import type { LinearCollectionResult } from '../../../shared/linear/workspace-types' import { sortedStrings } from './task-page-work-item-signatures' type LinearIssueCache = Record> diff --git a/src/renderer/src/components/task-page-linear-in-orca-issues.test.ts b/src/renderer/src/components/task-page-linear-in-orca-issues.test.ts index 28804568fec..818bc9e60e5 100644 --- a/src/renderer/src/components/task-page-linear-in-orca-issues.test.ts +++ b/src/renderer/src/components/task-page-linear-in-orca-issues.test.ts @@ -7,7 +7,7 @@ import { linkedLinearIssueRefsSignature, readLinkedLinearIssuesWithLimit } from './task-page-linear-in-orca-issues' -import type { LinearIssue } from '../../../shared/types' +import type { LinearIssue } from '../../../shared/linear/issue-types' function issue( partial: Partial & Pick diff --git a/src/renderer/src/components/task-page-linear-in-orca-issues.ts b/src/renderer/src/components/task-page-linear-in-orca-issues.ts index 55ae9293b85..36c66ea2c4c 100644 --- a/src/renderer/src/components/task-page-linear-in-orca-issues.ts +++ b/src/renderer/src/components/task-page-linear-in-orca-issues.ts @@ -1,4 +1,6 @@ -import type { LinearIssue, LinearWorkspace, Worktree } from '../../../shared/types' +import type { LinearIssue } from '../../../shared/linear/issue-types' +import type { LinearWorkspace } from '../../../shared/linear/workspace-types' +import type { Worktree } from '../../../shared/worktree/types' import { parseLinearIssueInput } from '../../../shared/linear/links' import { getTaskSourceCacheScope, diff --git a/src/renderer/src/components/task-page-linear-team-selection.test.ts b/src/renderer/src/components/task-page-linear-team-selection.test.ts index a3d1a207b12..6eb1878fbbd 100644 --- a/src/renderer/src/components/task-page-linear-team-selection.test.ts +++ b/src/renderer/src/components/task-page-linear-team-selection.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { LinearTeam } from '../../../shared/types' +import type { LinearTeam } from '../../../shared/linear/workspace-types' import { reconcileLinearTeamSelection } from './task-page-linear-team-selection' function team(id: string): LinearTeam { diff --git a/src/renderer/src/components/task-page-linear-team-selection.ts b/src/renderer/src/components/task-page-linear-team-selection.ts index f6d7e52fa1f..3ec82284a59 100644 --- a/src/renderer/src/components/task-page-linear-team-selection.ts +++ b/src/renderer/src/components/task-page-linear-team-selection.ts @@ -1,4 +1,4 @@ -import type { LinearTeam } from '../../../shared/types' +import type { LinearTeam } from '../../../shared/linear/workspace-types' export function reconcileLinearTeamSelection( availableTeams: LinearTeam[], diff --git a/src/renderer/src/components/task-page-list-chrome-visibility.ts b/src/renderer/src/components/task-page-list-chrome-visibility.ts index e8b95e96288..e589ef2e579 100644 --- a/src/renderer/src/components/task-page-list-chrome-visibility.ts +++ b/src/renderer/src/components/task-page-list-chrome-visibility.ts @@ -1,4 +1,4 @@ -import type { TaskProvider } from '../../../shared/types' +import type { TaskProvider } from '../../../shared/task-providers' export type TaskPageListChromeVisibilityState = { taskSource: TaskProvider diff --git a/src/renderer/src/components/task-page-localized-options.tsx b/src/renderer/src/components/task-page-localized-options.tsx index 506f27add17..cd14317489d 100644 --- a/src/renderer/src/components/task-page-localized-options.tsx +++ b/src/renderer/src/components/task-page-localized-options.tsx @@ -15,7 +15,8 @@ import { type LinearViewMode } from '../../../shared/linear/issue-view-resume-state' import { getTaskPresetQuery } from '../../../shared/task-preset-query' -import type { TaskProvider, TaskViewPresetId } from '../../../shared/types' +import type { TaskProvider } from '../../../shared/task-providers' +import type { TaskViewPresetId } from '../../../shared/ui-chrome-types' export type GitLabTaskFilter = 'opened' | 'merged' | 'closed' | 'all' export type GitLabIssueFilter = 'opened' | 'assigned-to-me' diff --git a/src/renderer/src/components/task-page-new-issue-draft.test.ts b/src/renderer/src/components/task-page-new-issue-draft.test.ts index c5058f50989..42a63a68568 100644 --- a/src/renderer/src/components/task-page-new-issue-draft.test.ts +++ b/src/renderer/src/components/task-page-new-issue-draft.test.ts @@ -6,7 +6,7 @@ import { resolveVanishedNewIssueRepoReset } from './task-page-new-issue-draft' import type { NewIssueDraft } from '@/store/slices/new-issue-draft' -import type { GitHubAssignableUser } from '../../../shared/types' +import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' const assignee: GitHubAssignableUser = { login: 'octocat', name: 'Octo', avatarUrl: '' } diff --git a/src/renderer/src/components/task-page-new-issue-draft.ts b/src/renderer/src/components/task-page-new-issue-draft.ts index e252692f379..8b6de307fb7 100644 --- a/src/renderer/src/components/task-page-new-issue-draft.ts +++ b/src/renderer/src/components/task-page-new-issue-draft.ts @@ -1,4 +1,4 @@ -import type { GitHubAssignableUser } from '../../../shared/types' +import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' import type { NewIssueDraft } from '@/store/slices/new-issue-draft' export type NewIssueOpenSeed = { diff --git a/src/renderer/src/components/task-page-pr-check-summary.test.ts b/src/renderer/src/components/task-page-pr-check-summary.test.ts index ccd63d94c04..bc9360d4d4f 100644 --- a/src/renderer/src/components/task-page-pr-check-summary.test.ts +++ b/src/renderer/src/components/task-page-pr-check-summary.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { deriveTaskPagePRCheckSummary } from './task-page-pr-check-summary' -import type { PRCheckDetail } from '../../../shared/types' +import type { PRCheckDetail } from '../../../shared/github/check-types' function check(patch: Partial): PRCheckDetail { return { diff --git a/src/renderer/src/components/task-page-pr-check-summary.ts b/src/renderer/src/components/task-page-pr-check-summary.ts index c7531040923..156e92b70ab 100644 --- a/src/renderer/src/components/task-page-pr-check-summary.ts +++ b/src/renderer/src/components/task-page-pr-check-summary.ts @@ -1,5 +1,6 @@ import { summarizeProviderChecks } from '../../../shared/provider-check-summary' -import type { PRCheckDetail, ProviderCheckSummary } from '../../../shared/types' +import type { PRCheckDetail } from '../../../shared/github/check-types' +import type { ProviderCheckSummary } from '../../../shared/github/pull-request-types' export function deriveTaskPagePRCheckSummary(checks: PRCheckDetail[]): ProviderCheckSummary { return summarizeProviderChecks(checks) diff --git a/src/renderer/src/components/task-page-pr-delta-summary.ts b/src/renderer/src/components/task-page-pr-delta-summary.ts index 9711d087cb0..091011a38ed 100644 --- a/src/renderer/src/components/task-page-pr-delta-summary.ts +++ b/src/renderer/src/components/task-page-pr-delta-summary.ts @@ -1,4 +1,4 @@ -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' export function formatPRDelta(item: GitHubWorkItem): string | null { const parts: string[] = [] diff --git a/src/renderer/src/components/task-page-work-item-pagination.test.ts b/src/renderer/src/components/task-page-work-item-pagination.test.ts index 670c862be98..50b88579158 100644 --- a/src/renderer/src/components/task-page-work-item-pagination.test.ts +++ b/src/renderer/src/components/task-page-work-item-pagination.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import { accumulateWorkItemPages, applyEmptyPageClamp, diff --git a/src/renderer/src/components/task-page-work-item-pagination.ts b/src/renderer/src/components/task-page-work-item-pagination.ts index 8209b61bdf0..2876831f000 100644 --- a/src/renderer/src/components/task-page-work-item-pagination.ts +++ b/src/renderer/src/components/task-page-work-item-pagination.ts @@ -1,4 +1,5 @@ -import type { ClassifiedError, GitHubWorkItem } from '../../../shared/types' +import type { ClassifiedError } from '../../../shared/classified-error' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' /** * Cross-repo Tasks pagination is cursor-based on `updatedAt`: each page's oldest diff --git a/src/renderer/src/components/task-page-work-item-signatures.ts b/src/renderer/src/components/task-page-work-item-signatures.ts index 67875e2e15e..8abb2f07a19 100644 --- a/src/renderer/src/components/task-page-work-item-signatures.ts +++ b/src/renderer/src/components/task-page-work-item-signatures.ts @@ -1,4 +1,4 @@ -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' export function taskPageWorkItemKey(item: GitHubWorkItem): string { return `${item.repoId}\u0000${item.id}` diff --git a/src/renderer/src/components/task-project-source-combobox-model.ts b/src/renderer/src/components/task-project-source-combobox-model.ts index 248adfcfa11..8229014e362 100644 --- a/src/renderer/src/components/task-project-source-combobox-model.ts +++ b/src/renderer/src/components/task-project-source-combobox-model.ts @@ -1,5 +1,5 @@ import { getRepoExecutionHostId } from '../../../shared/execution-host' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import type { TaskProjectPickerGroup } from './task-page-default-repo-selection' export function selectedTaskProjectGroups( diff --git a/src/renderer/src/components/task-project-source-combobox.tsx b/src/renderer/src/components/task-project-source-combobox.tsx index 0f3d0e33697..555089e75f7 100644 --- a/src/renderer/src/components/task-project-source-combobox.tsx +++ b/src/renderer/src/components/task-project-source-combobox.tsx @@ -7,7 +7,7 @@ import RepoBadgeLabel from '@/components/repo/RepoBadgeLabel' import { isRepoSearchQueryTooLarge, searchRepos } from '@/lib/repo-search' import { cn } from '@/lib/utils' import { translate } from '@/i18n/i18n' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import type { TaskProjectPickerGroup } from './task-page-default-repo-selection' import { getSelectedTaskProjectSource, diff --git a/src/renderer/src/components/task-source-context-summary.ts b/src/renderer/src/components/task-source-context-summary.ts index be1d5fa0790..af5e2879d0c 100644 --- a/src/renderer/src/components/task-source-context-summary.ts +++ b/src/renderer/src/components/task-source-context-summary.ts @@ -3,7 +3,7 @@ import { getExecutionHostLabel } from '../../../shared/execution-host' import type { ExecutionHostScope } from '../../../shared/execution-host' import type { ExecutionHostHealth } from '../../../shared/execution-host-registry' import type { SshConnectionStatus } from '../../../shared/ssh-types' -import type { TaskProvider } from '../../../shared/types' +import type { TaskProvider } from '../../../shared/task-providers' import type { TaskProviderIdentity, TaskSourceContext } from '../../../shared/task-source-context' export type TaskSourceContextSummary = { diff --git a/src/renderer/src/components/task-source-provider-availability.ts b/src/renderer/src/components/task-source-provider-availability.ts index e01e0fef057..5038ab7ea6a 100644 --- a/src/renderer/src/components/task-source-provider-availability.ts +++ b/src/renderer/src/components/task-source-provider-availability.ts @@ -1,5 +1,5 @@ import { parseExecutionHostId } from '../../../shared/execution-host' -import type { TaskProvider } from '../../../shared/types' +import type { TaskProvider } from '../../../shared/task-providers' import type { PreflightStatus } from '../../../preload/api-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import type { TaskSourceHostAvailability } from './task-source-context-summary' diff --git a/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx b/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx index c983b389760..73a9d508f4c 100644 --- a/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx @@ -25,7 +25,7 @@ import { DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { shouldIgnoreTerminalMenuPointerDownOutside } from './terminal-context-menu-dismiss' -import type { TerminalQuickCommand } from '../../../../shared/types' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' import type { ExecutionHostId } from '../../../../shared/execution-host' import { formatPrimaryShortcutLabel } from '@/hooks/useShortcutLabel' import type { KeybindingOverrides } from '../../../../shared/keybindings' diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx index 5546390b6db..79b7ab6bf13 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx @@ -152,7 +152,10 @@ import { isHostAuthoritativeLayout, planTerminalLiveLayoutInsertions } from './terminal-live-layout-reconciliation' -import type { TerminalQuickCommand, TerminalQuickCommandScope } from '../../../../shared/types' +import type { + TerminalQuickCommand, + TerminalQuickCommandScope +} from '../../../../shared/terminal-quick-command-types' import { createRemotePaneLayoutPusher, type RemotePaneLayoutPusher diff --git a/src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.tsx b/src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.tsx index 2196f7ed094..2cb20783f53 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.tsx @@ -1,6 +1,7 @@ import { memo, useCallback, useMemo } from 'react' import { useShallow } from 'zustand/react/shallow' -import type { Tab, TabGroup, TerminalTab } from '../../../../shared/types' +import type { Tab, TabGroup } from '../../../../shared/tab-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { useAppStore } from '../../store' import { findActivityTerminalPortal, diff --git a/src/renderer/src/components/terminal-pane/TerminalQuickCommandsSubmenu.tsx b/src/renderer/src/components/terminal-pane/TerminalQuickCommandsSubmenu.tsx index ed650dc1463..df32a3d2505 100644 --- a/src/renderer/src/components/terminal-pane/TerminalQuickCommandsSubmenu.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalQuickCommandsSubmenu.tsx @@ -8,7 +8,7 @@ import { DropdownMenuSubContent, DropdownMenuSubTrigger } from '@/components/ui/dropdown-menu' -import type { TerminalQuickCommand } from '../../../../shared/types' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' import type { ExecutionHostId } from '../../../../shared/execution-host' import { isTerminalAgentQuickCommand } from '../../../../shared/terminal-quick-commands' import { AgentIcon } from '@/lib/agent-catalog' diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-types.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-types.ts index 097ecd792d5..a388ec17457 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-types.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-types.ts @@ -1,5 +1,5 @@ import type { ParsedAgentStatusPayload } from '../../../../shared/agent-status-types' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { RecognizedAgentProcess } from '../../../../shared/agent-process-recognition' import type { RuntimeTerminalProcessInspection } from '@/runtime/runtime-terminal-inspection' diff --git a/src/renderer/src/components/terminal-pane/agent-task-complete-policy.ts b/src/renderer/src/components/terminal-pane/agent-task-complete-policy.ts index 6275cc9553c..4ba348d3f32 100644 --- a/src/renderer/src/components/terminal-pane/agent-task-complete-policy.ts +++ b/src/renderer/src/components/terminal-pane/agent-task-complete-policy.ts @@ -8,7 +8,7 @@ * consumers can use it. */ import type { AgentStatusEntry } from '../../../../shared/agent-status-types' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' /** Delay before BEL/completion OS notifications so the richer * agent-task-complete notification can win a same-burst BEL race. */ diff --git a/src/renderer/src/components/terminal-pane/codex-detached-pane-restart.ts b/src/renderer/src/components/terminal-pane/codex-detached-pane-restart.ts index 1ad6528c935..b1d0ddb6747 100644 --- a/src/renderer/src/components/terminal-pane/codex-detached-pane-restart.ts +++ b/src/renderer/src/components/terminal-pane/codex-detached-pane-restart.ts @@ -10,7 +10,7 @@ */ import { isTerminalLeafId, makePaneKey } from '../../../../shared/stable-pane-id' import { parseWorkspaceKey } from '../../../../shared/workspace-scope' -import type { TerminalPaneLayoutNode, TerminalTab } from '../../../../shared/types' +import type { TerminalPaneLayoutNode, TerminalTab } from '../../../../shared/terminal-tab-types' import type { AppState } from '@/store' import { useAppStore } from '@/store' import { getWorktreeMapFromState } from '@/store/selectors' diff --git a/src/renderer/src/components/terminal-pane/command-code-output-ownership.ts b/src/renderer/src/components/terminal-pane/command-code-output-ownership.ts index 65f5f196ca0..ec0b65cd49a 100644 --- a/src/renderer/src/components/terminal-pane/command-code-output-ownership.ts +++ b/src/renderer/src/components/terminal-pane/command-code-output-ownership.ts @@ -1,5 +1,5 @@ import type { AgentType } from '../../../../shared/agent-status-types' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' export function canCommandCodeOutputOwnPane(args: { foregroundAgent?: TuiAgent | null diff --git a/src/renderer/src/components/terminal-pane/compose-active-terminal-theme.test.ts b/src/renderer/src/components/terminal-pane/compose-active-terminal-theme.test.ts index 97e81b97135..b15bae2ec5b 100644 --- a/src/renderer/src/components/terminal-pane/compose-active-terminal-theme.test.ts +++ b/src/renderer/src/components/terminal-pane/compose-active-terminal-theme.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { composeActiveTerminalTheme } from './terminal-appearance' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' describe('composeActiveTerminalTheme', () => { function settingsWith(partial: Partial): GlobalSettings { diff --git a/src/renderer/src/components/terminal-pane/layout-serialization.test.ts b/src/renderer/src/components/terminal-pane/layout-serialization.test.ts index 626168ef265..597d5e54b58 100644 --- a/src/renderer/src/components/terminal-pane/layout-serialization.test.ts +++ b/src/renderer/src/components/terminal-pane/layout-serialization.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, beforeAll, vi } from 'vitest' -import type { TerminalPaneLayoutNode } from '../../../../shared/types' +import type { TerminalPaneLayoutNode } from '../../../../shared/terminal-tab-types' // --------------------------------------------------------------------------- // Provide a minimal HTMLElement so `instanceof HTMLElement` passes in Node env diff --git a/src/renderer/src/components/terminal-pane/layout-serialization.ts b/src/renderer/src/components/terminal-pane/layout-serialization.ts index 2f8d0fea2f9..c3807cffeac 100644 --- a/src/renderer/src/components/terminal-pane/layout-serialization.ts +++ b/src/renderer/src/components/terminal-pane/layout-serialization.ts @@ -2,7 +2,7 @@ import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode, TerminalPaneSplitDirection -} from '../../../../shared/types' +} from '../../../../shared/terminal-tab-types' import { isTerminalLeafId } from '../../../../shared/stable-pane-id' import { POST_REPLAY_MODE_RESET } from '../../../../shared/terminal-mode-reset-profiles' import type { PaneManager } from '@/lib/pane-manager/pane-manager' diff --git a/src/renderer/src/components/terminal-pane/native-chat-leaf-title-agent.ts b/src/renderer/src/components/terminal-pane/native-chat-leaf-title-agent.ts index 20f24fb6986..4bf7b230ffa 100644 --- a/src/renderer/src/components/terminal-pane/native-chat-leaf-title-agent.ts +++ b/src/renderer/src/components/terminal-pane/native-chat-leaf-title-agent.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { resolveCommittedTitleAgentType } from '@/lib/pane-agent-evidence' export type NativeChatLeafTitlePane = { diff --git a/src/renderer/src/components/terminal-pane/pane-foreground-agent-tracker.ts b/src/renderer/src/components/terminal-pane/pane-foreground-agent-tracker.ts index 189b91573b6..2a279abd110 100644 --- a/src/renderer/src/components/terminal-pane/pane-foreground-agent-tracker.ts +++ b/src/renderer/src/components/terminal-pane/pane-foreground-agent-tracker.ts @@ -3,7 +3,7 @@ import { recognizeAgentProcess } from '../../../../shared/agent-process-recognition' import { isShellProcess } from '../../../../shared/shell-process-detection' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { PaneForegroundAgentEntry } from '@/store/slices/pane-foreground-agent' // Why: settle after exec, then place the final generic retry beyond sequential diff --git a/src/renderer/src/components/terminal-pane/pty-connection-types.ts b/src/renderer/src/components/terminal-pane/pty-connection-types.ts index b2b5163ac93..9fef3423cbf 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-types.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-types.ts @@ -6,7 +6,8 @@ import type { AgentCompletionStatusSnapshot } from './agent-completion-coordinat import type { EventProps } from '../../../../shared/telemetry-events' import type { TerminalColorSchemeMode } from '../../../../shared/terminal-color-scheme-protocol' import type { StartupCommandDelivery } from '../../../../shared/codex-startup-delivery' -import type { SetupSplitDirection, TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' +import type { SetupSplitDirection } from '../../../../shared/worktree/launch-types' import type { AgentProviderSessionMetadata, SleepingAgentLaunchConfig diff --git a/src/renderer/src/components/terminal-pane/pty-connection.test.ts b/src/renderer/src/components/terminal-pane/pty-connection.test.ts index 6fbe70501f8..f4bd512fda9 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.test.ts @@ -23,7 +23,8 @@ import type { AgentType } from '../../../../shared/agent-status-types' import { makePaneKey } from '../../../../shared/stable-pane-id' import { toAppSshPtyId } from '../../../../shared/ssh-pty-id' import type { SshConnectionState } from '../../../../shared/ssh-types' -import type { TerminalLayoutSnapshot, TuiAgent } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { YOLO_TUI_AGENT_ARGS } from '../../../../shared/tui-agent-permissions' import { SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV } from '../../../../shared/setup-agent-sequencing' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index e2efbb0a9c4..8a67ba6b324 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -310,7 +310,8 @@ import { isExpectedAgentProcess, recognizeAgentProcessFromCommandLine } from '../../../../shared/agent-process-recognition' -import type { SetupSplitDirection, TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' +import type { SetupSplitDirection } from '../../../../shared/worktree/launch-types' import { isTuiAgent, TUI_AGENT_CONFIG } from '../../../../shared/tui-agent-config' import { createDraftPasteReadyScanner } from '../../../../shared/draft-paste-ready-scanner' import { sendAgentDraftPasteContent } from '@/lib/agent-draft-paste-content' diff --git a/src/renderer/src/components/terminal-pane/pty-transport-types.ts b/src/renderer/src/components/terminal-pane/pty-transport-types.ts index 1ba841bd823..57e0c9e2e4e 100644 --- a/src/renderer/src/components/terminal-pane/pty-transport-types.ts +++ b/src/renderer/src/components/terminal-pane/pty-transport-types.ts @@ -11,7 +11,7 @@ import type { StartupCommandDelivery } from '../../../../shared/codex-startup-de import type { ProjectExecutionRuntimeResolution } from '../../../../shared/project-execution-runtime' import type { EventProps } from '../../../../shared/telemetry-events' import type { TerminalOscColorQueryReplyColors } from '../../../../shared/terminal-osc-color-reply' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { ExecutionHostId } from '../../../../shared/execution-host' import type { PtyDataMeta } from './pty-dispatcher' import type { RemoteRuntimeSnapshotOutcome } from '../../runtime/remote-runtime-terminal-multiplexer' diff --git a/src/renderer/src/components/terminal-pane/remote-pane-layout-push.test.ts b/src/renderer/src/components/terminal-pane/remote-pane-layout-push.test.ts index d6d777f728c..737339f4705 100644 --- a/src/renderer/src/components/terminal-pane/remote-pane-layout-push.test.ts +++ b/src/renderer/src/components/terminal-pane/remote-pane-layout-push.test.ts @@ -6,7 +6,7 @@ * before the dedupe and 1 after. */ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { TerminalLayoutSnapshot } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' const updateWebRuntimePaneLayout = vi.fn() vi.mock('@/runtime/web-runtime-session', () => ({ diff --git a/src/renderer/src/components/terminal-pane/remote-pane-layout-push.ts b/src/renderer/src/components/terminal-pane/remote-pane-layout-push.ts index 506bd7ea42f..963d4b78355 100644 --- a/src/renderer/src/components/terminal-pane/remote-pane-layout-push.ts +++ b/src/renderer/src/components/terminal-pane/remote-pane-layout-push.ts @@ -1,4 +1,4 @@ -import type { TerminalLayoutSnapshot } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' import { terminalLayoutEqual } from '@/lib/terminal-layout-equality' import { updateWebRuntimePaneLayout } from '@/runtime/web-runtime-session' diff --git a/src/renderer/src/components/terminal-pane/terminal-agent-session-continuation.ts b/src/renderer/src/components/terminal-pane/terminal-agent-session-continuation.ts index f9f9a63792b..806197d5323 100644 --- a/src/renderer/src/components/terminal-pane/terminal-agent-session-continuation.ts +++ b/src/renderer/src/components/terminal-pane/terminal-agent-session-continuation.ts @@ -7,7 +7,7 @@ import { import { useAppStore } from '@/store' import { makePaneKey } from '../../../../shared/stable-pane-id' import { isTuiAgent } from '../../../../shared/tui-agent-config' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { translate } from '@/i18n/i18n' type PrepareAgentSessionContinuationFromPaneArgs = { diff --git a/src/renderer/src/components/terminal-pane/terminal-agent-session-fork.ts b/src/renderer/src/components/terminal-pane/terminal-agent-session-fork.ts index 959b4a9a7e4..211c9589b64 100644 --- a/src/renderer/src/components/terminal-pane/terminal-agent-session-fork.ts +++ b/src/renderer/src/components/terminal-pane/terminal-agent-session-fork.ts @@ -11,7 +11,7 @@ import { makePaneKey } from '../../../../shared/stable-pane-id' import { TUI_AGENT_CONFIG } from '../../../../shared/tui-agent-config' import { slugifyForWorkspaceName } from '../../../../shared/workspace-name' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { isWslUncPath } from '../../../../shared/wsl-paths' import type { ProjectExecutionRuntimeResolution } from '../../../../shared/project-execution-runtime' import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context' diff --git a/src/renderer/src/components/terminal-pane/terminal-appearance.ts b/src/renderer/src/components/terminal-pane/terminal-appearance.ts index a0d5bff3a04..bb61a50fbbc 100644 --- a/src/renderer/src/components/terminal-pane/terminal-appearance.ts +++ b/src/renderer/src/components/terminal-pane/terminal-appearance.ts @@ -1,6 +1,6 @@ import type { ITheme } from '@xterm/xterm' import type { PaneManager } from '@/lib/pane-manager/pane-manager' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { resolveTerminalFontWeights } from '../../../../shared/terminal-fonts' import { resolveTerminalLigaturesEnabled } from '../../../../shared/terminal-ligatures' import { diff --git a/src/renderer/src/components/terminal-pane/terminal-cold-park-pre-gate-loop.react185.test.tsx b/src/renderer/src/components/terminal-pane/terminal-cold-park-pre-gate-loop.react185.test.tsx index 902c3254557..5f4db2d3878 100644 --- a/src/renderer/src/components/terminal-pane/terminal-cold-park-pre-gate-loop.react185.test.tsx +++ b/src/renderer/src/components/terminal-pane/terminal-cold-park-pre-gate-loop.react185.test.tsx @@ -3,7 +3,8 @@ import { act, StrictMode } from 'react' import type * as ReactModule from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Tab, TabGroup, TerminalTab } from '../../../../shared/types' +import type { Tab, TabGroup } from '../../../../shared/tab-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true diff --git a/src/renderer/src/components/terminal-pane/terminal-cold-park-verdict-loop.test.tsx b/src/renderer/src/components/terminal-pane/terminal-cold-park-verdict-loop.test.tsx index 08fadd307fe..769c7e45c67 100644 --- a/src/renderer/src/components/terminal-pane/terminal-cold-park-verdict-loop.test.tsx +++ b/src/renderer/src/components/terminal-pane/terminal-cold-park-verdict-loop.test.tsx @@ -17,7 +17,7 @@ import { act, useEffect, useRef, useState } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true diff --git a/src/renderer/src/components/terminal-pane/terminal-cold-park-withheld-tabs.test.ts b/src/renderer/src/components/terminal-pane/terminal-cold-park-withheld-tabs.test.ts index a72867e3c22..75ca204c5cd 100644 --- a/src/renderer/src/components/terminal-pane/terminal-cold-park-withheld-tabs.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-cold-park-withheld-tabs.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { ParkVerdictFlipRecord } from './terminal-park-verdict-flip-telemetry' import { withholdUnparkableTerminalTabs } from './terminal-cold-park-withheld-tabs' diff --git a/src/renderer/src/components/terminal-pane/terminal-cold-park-withheld-tabs.ts b/src/renderer/src/components/terminal-pane/terminal-cold-park-withheld-tabs.ts index 35be4446cb5..d35270bd170 100644 --- a/src/renderer/src/components/terminal-pane/terminal-cold-park-withheld-tabs.ts +++ b/src/renderer/src/components/terminal-pane/terminal-cold-park-withheld-tabs.ts @@ -6,7 +6,7 @@ * is returned because it is also the only remaining recheck wakeup once damping * has stopped the churn that was waking the parking effect. */ -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { getParkVerdictUnparkPinUntilMs, type ParkVerdictFlipRecord diff --git a/src/renderer/src/components/terminal-pane/terminal-fit-restore.ts b/src/renderer/src/components/terminal-pane/terminal-fit-restore.ts index 6503d13f58c..685ba20b9c9 100644 --- a/src/renderer/src/components/terminal-pane/terminal-fit-restore.ts +++ b/src/renderer/src/components/terminal-pane/terminal-fit-restore.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { mapWithConcurrency } from '../../../../shared/map-with-concurrency' import { TERMINAL_FIT_RESTORE_DEADLINE_MS } from '../../../../shared/terminal-fit-restore-deadline' import { callRuntimeRpc } from '@/runtime/runtime-rpc-client' diff --git a/src/renderer/src/components/terminal-pane/terminal-hidden-delivery-gate.ts b/src/renderer/src/components/terminal-pane/terminal-hidden-delivery-gate.ts index 0950b05508e..96d6a0eed07 100644 --- a/src/renderer/src/components/terminal-pane/terminal-hidden-delivery-gate.ts +++ b/src/renderer/src/components/terminal-pane/terminal-hidden-delivery-gate.ts @@ -7,7 +7,7 @@ * picks which observer records mode-2031 subscriptions (byte scan vs fact), * so it must never flip per chunk. */ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' // Why: cached once per session — the blocking read should only ever run on // the pre-hydration startup path, never per pane bind. diff --git a/src/renderer/src/components/terminal-pane/terminal-hidden-view-parking.ts b/src/renderer/src/components/terminal-pane/terminal-hidden-view-parking.ts index 2efcff11f4d..a6aa4d676a7 100644 --- a/src/renderer/src/components/terminal-pane/terminal-hidden-view-parking.ts +++ b/src/renderer/src/components/terminal-pane/terminal-hidden-view-parking.ts @@ -3,7 +3,7 @@ import { getRemoteRuntimePtyEnvironmentId } from '@/runtime/runtime-terminal-str import { PTY_SESSION_ID_SEPARATOR } from '../../../../shared/pty-session-id-format' import { TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version' import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' // Why: cold-park hysteresis keeps a hidden pane mounted for 30s so quick tab // flips never pay a re-hydrate; hot-retain keeps a bounded recently-visible diff --git a/src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.ts b/src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.ts index 856f976cce0..105ca4fd43e 100644 --- a/src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.ts +++ b/src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.ts @@ -8,7 +8,7 @@ import { type ColdParkRetainCandidate, type TerminalColdParkPolicyOverrides } from './terminal-hidden-view-parking' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' // Why these sizes: a retained hidden pane costs a measured ~2.5MB of V8 heap // at the 5k-row default scrollback and ~19MB at 50k (plus per-pane queues), diff --git a/src/renderer/src/components/terminal-pane/terminal-keyboard-protocol-pane-agent.ts b/src/renderer/src/components/terminal-pane/terminal-keyboard-protocol-pane-agent.ts index 89fb02cbd67..674d0620c1b 100644 --- a/src/renderer/src/components/terminal-pane/terminal-keyboard-protocol-pane-agent.ts +++ b/src/renderer/src/components/terminal-pane/terminal-keyboard-protocol-pane-agent.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' type PaneKeyboardProtocolStartup = { launchAgent?: TuiAgent diff --git a/src/renderer/src/components/terminal-pane/terminal-layout-leaf-detach.test.ts b/src/renderer/src/components/terminal-pane/terminal-layout-leaf-detach.test.ts index 782113e1641..8a610702558 100644 --- a/src/renderer/src/components/terminal-pane/terminal-layout-leaf-detach.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-layout-leaf-detach.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalLayoutSnapshot } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' import { detachTerminalLayoutLeaf } from './terminal-layout-leaf-detach' const LEAF_1 = '11111111-1111-4111-8111-111111111111' diff --git a/src/renderer/src/components/terminal-pane/terminal-layout-leaf-detach.ts b/src/renderer/src/components/terminal-pane/terminal-layout-leaf-detach.ts index 7ca537b014d..4287e4eeee2 100644 --- a/src/renderer/src/components/terminal-pane/terminal-layout-leaf-detach.ts +++ b/src/renderer/src/components/terminal-pane/terminal-layout-leaf-detach.ts @@ -1,4 +1,7 @@ -import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode } from '../../../../shared/types' +import type { + TerminalLayoutSnapshot, + TerminalPaneLayoutNode +} from '../../../../shared/terminal-tab-types' import { collectLeafIdsInOrder, normalizeTerminalLayoutSnapshot, diff --git a/src/renderer/src/components/terminal-pane/terminal-layout-leaf-ids.test.ts b/src/renderer/src/components/terminal-pane/terminal-layout-leaf-ids.test.ts index 5f05376fd58..187f0a0d081 100644 --- a/src/renderer/src/components/terminal-pane/terminal-layout-leaf-ids.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-layout-leaf-ids.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from 'vitest' -import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode } from '../../../../shared/types' +import type { + TerminalLayoutSnapshot, + TerminalPaneLayoutNode +} from '../../../../shared/terminal-tab-types' import { normalizeTerminalLayoutSnapshot, resolvePtyBoundActiveLeafId, diff --git a/src/renderer/src/components/terminal-pane/terminal-layout-leaf-ids.ts b/src/renderer/src/components/terminal-pane/terminal-layout-leaf-ids.ts index 9f21c39056b..37870c03d70 100644 --- a/src/renderer/src/components/terminal-pane/terminal-layout-leaf-ids.ts +++ b/src/renderer/src/components/terminal-pane/terminal-layout-leaf-ids.ts @@ -1,4 +1,7 @@ -import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode } from '../../../../shared/types' +import type { + TerminalLayoutSnapshot, + TerminalPaneLayoutNode +} from '../../../../shared/terminal-tab-types' import { isTerminalLeafId, type TerminalLeafId } from '../../../../shared/stable-pane-id' import { mintStablePaneId } from '@/lib/pane-manager/mint-stable-pane-id' import { normalizeTerminalLayoutPtyOwnership } from './terminal-layout-pty-ownership' diff --git a/src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership-depth.test.ts b/src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership-depth.test.ts index a7e3fa4fc5a..a22eb50b97f 100644 --- a/src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership-depth.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership-depth.test.ts @@ -1,5 +1,5 @@ import { expect, it } from 'vitest' -import type { TerminalPaneLayoutNode } from '../../../../shared/types' +import type { TerminalPaneLayoutNode } from '../../../../shared/terminal-tab-types' import { normalizeTerminalLayoutPtyOwnership } from './terminal-layout-pty-ownership' it('prunes deeply nested duplicate ownership without recursive stack growth', () => { diff --git a/src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership.test.ts b/src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership.test.ts index 92a0def1df1..ddacb05ccd8 100644 --- a/src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalLayoutSnapshot } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' import { normalizeTerminalLayoutSnapshot } from './terminal-layout-leaf-ids' import { normalizeTerminalLayoutPtyOwnership } from './terminal-layout-pty-ownership' diff --git a/src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership.ts b/src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership.ts index 4c44c3e6ead..3687621f151 100644 --- a/src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership.ts +++ b/src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership.ts @@ -1,4 +1,5 @@ -import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' +import { collectLeafIds, pruneLeaves } from './terminal-pane-layout-tree' type TerminalLayoutPtyOwnershipNormalization = { snapshot: TerminalLayoutSnapshot @@ -10,68 +11,6 @@ type DuplicatePtyLeafReplacements = { retainedLeafIdByRemovedLeafId: Map } -function collectLeafIds(node: TerminalPaneLayoutNode | null | undefined): string[] { - if (!node) { - return [] - } - const leafIds: string[] = [] - const pending = [node] - while (pending.length > 0) { - const current = pending.pop()! - if (current.type === 'leaf') { - leafIds.push(current.leafId) - continue - } - pending.push(current.second, current.first) - } - return leafIds -} - -function pruneLeaves( - node: TerminalPaneLayoutNode, - retainedLeafIdByRemovedLeafId: ReadonlyMap, - retainedSelfLeafIds: Set -): TerminalPaneLayoutNode | null { - const pending: { node: TerminalPaneLayoutNode; visited: boolean }[] = [{ node, visited: false }] - const pruned: (TerminalPaneLayoutNode | null)[] = [] - while (pending.length > 0) { - const current = pending.pop()! - if (current.node.type === 'leaf') { - const retainedLeafId = retainedLeafIdByRemovedLeafId.get(current.node.leafId) - if (!retainedLeafId) { - pruned.push(current.node) - } else if ( - retainedLeafId === current.node.leafId && - !retainedSelfLeafIds.has(current.node.leafId) - ) { - retainedSelfLeafIds.add(current.node.leafId) - pruned.push(current.node) - } else { - pruned.push(null) - } - continue - } - if (!current.visited) { - pending.push({ node: current.node, visited: true }) - pending.push({ node: current.node.second, visited: false }) - pending.push({ node: current.node.first, visited: false }) - continue - } - const second = pruned.pop() ?? null - const first = pruned.pop() ?? null - if (first && second) { - pruned.push( - first === current.node.first && second === current.node.second - ? current.node - : { ...current.node, first, second } - ) - } else { - pruned.push(first ?? second) - } - } - return pruned[0] ?? null -} - function resolveRetainedLeafId( leafId: string, retainedLeafIdByRemovedLeafId: ReadonlyMap diff --git a/src/renderer/src/components/terminal-pane/terminal-live-layout-reconciliation.test.ts b/src/renderer/src/components/terminal-pane/terminal-live-layout-reconciliation.test.ts index 50b51941701..06b321d0d04 100644 --- a/src/renderer/src/components/terminal-pane/terminal-live-layout-reconciliation.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-live-layout-reconciliation.test.ts @@ -3,7 +3,7 @@ import { isHostAuthoritativeLayout, planTerminalLiveLayoutInsertions } from './terminal-live-layout-reconciliation' -import type { TerminalPaneLayoutNode } from '../../../../shared/types' +import type { TerminalPaneLayoutNode } from '../../../../shared/terminal-tab-types' const LOCAL_PTY = 'pty-local-123' const REMOTE_PTY = 'remote:env-1@@term_abc' diff --git a/src/renderer/src/components/terminal-pane/terminal-live-layout-reconciliation.ts b/src/renderer/src/components/terminal-pane/terminal-live-layout-reconciliation.ts index 152c90f82e1..7de00009a73 100644 --- a/src/renderer/src/components/terminal-pane/terminal-live-layout-reconciliation.ts +++ b/src/renderer/src/components/terminal-pane/terminal-live-layout-reconciliation.ts @@ -1,4 +1,7 @@ -import type { TerminalPaneLayoutNode, TerminalPaneSplitDirection } from '../../../../shared/types' +import type { + TerminalPaneLayoutNode, + TerminalPaneSplitDirection +} from '../../../../shared/terminal-tab-types' import { isRemoteRuntimePtyId } from '@/runtime/runtime-terminal-inspection' /** diff --git a/src/renderer/src/components/terminal-pane/terminal-notification-pane-visibility.ts b/src/renderer/src/components/terminal-pane/terminal-notification-pane-visibility.ts index ac4eb9717d0..00166365084 100644 --- a/src/renderer/src/components/terminal-pane/terminal-notification-pane-visibility.ts +++ b/src/renderer/src/components/terminal-pane/terminal-notification-pane-visibility.ts @@ -1,5 +1,5 @@ import { parsePaneKey } from '../../../../shared/stable-pane-id' -import type { TerminalLayoutSnapshot } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' type NotificationPaneVisibilityState = { activeWorktreeId: string | null diff --git a/src/renderer/src/components/terminal-pane/terminal-notification-state.ts b/src/renderer/src/components/terminal-pane/terminal-notification-state.ts index a14b5b8e39e..ac2d1c6d227 100644 --- a/src/renderer/src/components/terminal-pane/terminal-notification-state.ts +++ b/src/renderer/src/components/terminal-pane/terminal-notification-state.ts @@ -3,7 +3,7 @@ import type { useAppStore } from '@/store' import { getWorktreeMapFromState } from '@/store/selectors' import { AGENT_STATUS_STALE_AFTER_MS } from '../../../../shared/agent-status-types' import { parsePaneKey } from '../../../../shared/stable-pane-id' -import type { TerminalPaneLayoutNode } from '../../../../shared/types' +import type { TerminalPaneLayoutNode } from '../../../../shared/terminal-tab-types' type StoreSnapshot = ReturnType diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-close-identity.ts b/src/renderer/src/components/terminal-pane/terminal-pane-close-identity.ts index 2325241098f..4d0487b5bad 100644 --- a/src/renderer/src/components/terminal-pane/terminal-pane-close-identity.ts +++ b/src/renderer/src/components/terminal-pane/terminal-pane-close-identity.ts @@ -1,4 +1,4 @@ -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' export function shouldClearLaunchAgentForClosedPane( tab: Pick | null | undefined, diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-layout-tree.ts b/src/renderer/src/components/terminal-pane/terminal-pane-layout-tree.ts new file mode 100644 index 00000000000..5e36a9904ba --- /dev/null +++ b/src/renderer/src/components/terminal-pane/terminal-pane-layout-tree.ts @@ -0,0 +1,63 @@ +import type { TerminalPaneLayoutNode } from '../../../../shared/terminal-tab-types' + +export function collectLeafIds(node: TerminalPaneLayoutNode | null | undefined): string[] { + if (!node) { + return [] + } + const leafIds: string[] = [] + const pending = [node] + while (pending.length > 0) { + const current = pending.pop()! + if (current.type === 'leaf') { + leafIds.push(current.leafId) + continue + } + pending.push(current.second, current.first) + } + return leafIds +} + +export function pruneLeaves( + node: TerminalPaneLayoutNode, + retainedLeafIdByRemovedLeafId: ReadonlyMap, + retainedSelfLeafIds: Set +): TerminalPaneLayoutNode | null { + const pending: { node: TerminalPaneLayoutNode; visited: boolean }[] = [{ node, visited: false }] + const pruned: (TerminalPaneLayoutNode | null)[] = [] + while (pending.length > 0) { + const current = pending.pop()! + if (current.node.type === 'leaf') { + const retainedLeafId = retainedLeafIdByRemovedLeafId.get(current.node.leafId) + if (!retainedLeafId) { + pruned.push(current.node) + } else if ( + retainedLeafId === current.node.leafId && + !retainedSelfLeafIds.has(current.node.leafId) + ) { + retainedSelfLeafIds.add(current.node.leafId) + pruned.push(current.node) + } else { + pruned.push(null) + } + continue + } + if (!current.visited) { + pending.push({ node: current.node, visited: true }) + pending.push({ node: current.node.second, visited: false }) + pending.push({ node: current.node.first, visited: false }) + continue + } + const second = pruned.pop() ?? null + const first = pruned.pop() ?? null + if (first && second) { + pruned.push( + first === current.node.first && second === current.node.second + ? current.node + : { ...current.node, first, second } + ) + } else { + pruned.push(first ?? second) + } + } + return pruned[0] ?? null +} diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts b/src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts index 8cfbc886925..d421ca04102 100644 --- a/src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import type { AppState } from '@/store' -import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' import { detachTerminalPaneToTab, resolveTerminalTabStripDropTarget, diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.ts b/src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.ts index f6cac5c5aca..0d8f7d72de9 100644 --- a/src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.ts +++ b/src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.ts @@ -1,6 +1,6 @@ import type { PaneExternalDropTarget } from '@/lib/pane-manager/pane-manager' import type { AppState } from '@/store' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { detachTerminalLayoutLeaf } from './terminal-layout-leaf-detach' const TAB_GROUP_STRIP_SELECTOR = '[data-tab-group-strip-id][data-worktree-id]' diff --git a/src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.ts b/src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.ts index 5376cb9ed13..0b2939aade8 100644 --- a/src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.ts +++ b/src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.ts @@ -1,4 +1,4 @@ -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { useAppStore } from '@/store' import { collectLeafIdsInOrder, diff --git a/src/renderer/src/components/terminal-pane/terminal-quick-command-dispatch.ts b/src/renderer/src/components/terminal-pane/terminal-quick-command-dispatch.ts index e8e96e8e58c..39d49d02c86 100644 --- a/src/renderer/src/components/terminal-pane/terminal-quick-command-dispatch.ts +++ b/src/renderer/src/components/terminal-pane/terminal-quick-command-dispatch.ts @@ -1,4 +1,4 @@ -import type { TerminalQuickCommand } from '../../../../shared/types' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' import { buildTerminalQuickCommandInput, flattenTerminalQuickCommand, diff --git a/src/renderer/src/components/terminal-pane/terminal-renderer-policy.ts b/src/renderer/src/components/terminal-pane/terminal-renderer-policy.ts index f6d225dcd09..4ae94968f75 100644 --- a/src/renderer/src/components/terminal-pane/terminal-renderer-policy.ts +++ b/src/renderer/src/components/terminal-pane/terminal-renderer-policy.ts @@ -1,6 +1,6 @@ import { isGeminiTerminalTitle } from '@/lib/agent-status' import type { AgentType } from '../../../../shared/agent-status-types' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' export type TerminalGpuAccelerationMode = GlobalSettings['terminalGpuAcceleration'] diff --git a/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts b/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts index dcbf3782f4b..9e2b375939c 100644 --- a/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts @@ -1,6 +1,6 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT } from '../../../../shared/terminal-scrollback-limits' -import type { TerminalLayoutSnapshot } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' import { getUtf8ByteLength } from '../../../../shared/utf8-byte-limits' const LEAF_ID = '11111111-1111-4111-8111-111111111111' as const diff --git a/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.ts b/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.ts index 006a76b913f..f43d8026bd0 100644 --- a/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.ts +++ b/src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.ts @@ -1,4 +1,4 @@ -import type { TerminalLayoutSnapshot } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' import type { ManagedPane } from '@/lib/pane-manager/pane-manager' import type { PtyTransport } from './pty-transport' import { flushTerminalOutput } from '@/lib/pane-manager/pane-terminal-output-scheduler' diff --git a/src/renderer/src/components/terminal-pane/terminal-side-effect-facts-handler.ts b/src/renderer/src/components/terminal-pane/terminal-side-effect-facts-handler.ts index 578a4147375..527fbb46b95 100644 --- a/src/renderer/src/components/terminal-pane/terminal-side-effect-facts-handler.ts +++ b/src/renderer/src/components/terminal-pane/terminal-side-effect-facts-handler.ts @@ -12,7 +12,7 @@ * where pre-mount output produces no attention side effects. The one exception * is a PTY whose consumer just unregistered: see the handoff buffer below. */ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { ParsedAgentStatusPayload } from '../../../../shared/agent-status-types' import type { TerminalGitHubPRLink } from '../../../../shared/terminal-github-pr-link-detector' import type { diff --git a/src/renderer/src/components/terminal-pane/terminal-tab-lookup.test.ts b/src/renderer/src/components/terminal-pane/terminal-tab-lookup.test.ts index a31fc9b0ec4..a3f7d7320f1 100644 --- a/src/renderer/src/components/terminal-pane/terminal-tab-lookup.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-tab-lookup.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { getCachedTerminalTabForWorktree } from './terminal-tab-lookup' function makeTab(id: string): TerminalTab { diff --git a/src/renderer/src/components/terminal-pane/terminal-tab-lookup.ts b/src/renderer/src/components/terminal-pane/terminal-tab-lookup.ts index 74447f0a87e..576ad570e00 100644 --- a/src/renderer/src/components/terminal-pane/terminal-tab-lookup.ts +++ b/src/renderer/src/components/terminal-pane/terminal-tab-lookup.ts @@ -1,4 +1,4 @@ -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' const terminalTabLookupByArray = new WeakMap>() diff --git a/src/renderer/src/components/terminal-pane/terminal-tab-park-candidates.ts b/src/renderer/src/components/terminal-pane/terminal-tab-park-candidates.ts index cb0e357b280..527bbac536d 100644 --- a/src/renderer/src/components/terminal-pane/terminal-tab-park-candidates.ts +++ b/src/renderer/src/components/terminal-pane/terminal-tab-park-candidates.ts @@ -1,5 +1,5 @@ /** Builds the stateful per-tab cold-park candidate list. */ -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { TerminalTabColdParkCandidate } from './terminal-hidden-view-parking' import type { TerminalTabActivationOrder } from './terminal-tab-activation-order' diff --git a/src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.test.ts b/src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.test.ts index 18872bf04bb..c4eaff387dc 100644 --- a/src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { Tab } from '../../../../shared/types' +import type { Tab } from '../../../../shared/tab-types' import { getCachedTerminalGroupIdForWorktree, getCachedUnifiedTerminalTabForWorktree diff --git a/src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.ts b/src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.ts index 2113121b29f..11889344b75 100644 --- a/src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.ts +++ b/src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.ts @@ -1,4 +1,4 @@ -import type { Tab } from '../../../../shared/types' +import type { Tab } from '../../../../shared/tab-types' const terminalTabLookupByUnifiedTabs = new WeakMap>() diff --git a/src/renderer/src/components/terminal-pane/terminal-view-attributes-publisher.ts b/src/renderer/src/components/terminal-pane/terminal-view-attributes-publisher.ts index aeecb1a6f48..f785b59ff12 100644 --- a/src/renderer/src/components/terminal-pane/terminal-view-attributes-publisher.ts +++ b/src/renderer/src/components/terminal-pane/terminal-view-attributes-publisher.ts @@ -9,7 +9,7 @@ * are app-global, so identical snapshots publish once. */ import type { ITheme } from '@xterm/xterm' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { TerminalColorSchemeMode } from '../../../../shared/terminal-color-scheme-protocol' import type { TerminalViewAttributes, diff --git a/src/renderer/src/components/terminal-pane/use-notification-dispatch.test.ts b/src/renderer/src/components/terminal-pane/use-notification-dispatch.test.ts index 1ca3490ef56..0fa4e74dd88 100644 --- a/src/renderer/src/components/terminal-pane/use-notification-dispatch.test.ts +++ b/src/renderer/src/components/terminal-pane/use-notification-dispatch.test.ts @@ -4,7 +4,7 @@ import { AGENT_STATUS_STALE_AFTER_MS, type AgentStatusEntry } from '../../../../shared/agent-status-types' -import type { TerminalLayoutSnapshot } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' import { buildAgentNotificationId } from '../../../../shared/agent-notification-id' type MockState = { diff --git a/src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx b/src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx index f3ea986601f..5d364aba721 100644 --- a/src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx +++ b/src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx @@ -2,7 +2,7 @@ import { act, StrictMode, useEffect } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true diff --git a/src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.ts b/src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.ts index 1ee7ba8c2d0..ca533cccd88 100644 --- a/src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.ts +++ b/src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.ts @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react' import { useShallow } from 'zustand/react/shallow' -import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' import { useAppStore } from '../../store' import { disposeParkedTerminalWatchersForWorktree, diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-context-menu.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-context-menu.ts index c8264945744..b785d085772 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-context-menu.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-context-menu.ts @@ -7,7 +7,7 @@ import type { PtyTransport } from './pty-transport' import { getConnectionId } from '@/lib/connection-context' import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' import type { PaneCwdMap } from './resolve-split-cwd' -import type { TerminalQuickCommand } from '../../../../shared/types' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' import { isTerminalAgentQuickCommand } from '../../../../shared/terminal-quick-commands' import { sendTerminalQuickCommandToPane } from './terminal-quick-command-dispatch' import { pasteTerminalText } from './terminal-bracketed-paste' diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts index 97e6b7f31bd..de5cc7aeadd 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts @@ -57,13 +57,10 @@ import { } from '@/lib/http-link-routing' import { resolveTerminalHttpLinkSourceOwner } from './terminal-http-link-source-owner' import { canOpenWorkspaceBrowserTabOnRuntime } from '@/lib/workspace-browser-tab-open' -import type { - GlobalSettings, - SetupSplitDirection, - TerminalTab, - TerminalLayoutSnapshot, - TuiAgent -} from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../../shared/tui-agent' +import type { SetupSplitDirection } from '../../../../shared/worktree/launch-types' import type { TerminalPaneSplitSource } from '../../../../shared/feature-education-telemetry' import type { EventProps } from '../../../../shared/telemetry-events' import type { StartupCommandDelivery } from '../../../../shared/codex-startup-delivery' diff --git a/src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.test.ts b/src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.test.ts index 783ff7af574..58f4cbd2c46 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.test.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.test.ts @@ -1,7 +1,7 @@ // @vitest-environment happy-dom import { act, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' const mocks = vi.hoisted(() => ({ storeState: { diff --git a/src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.ts b/src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.ts index 94df8daf738..c078eed66c9 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.ts @@ -7,7 +7,7 @@ * render a slot as null. */ import { useEffect, useMemo, useRef, useState } from 'react' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { useAppStore } from '../../store' import { findActivityTerminalPortal, diff --git a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandAdvancedSection.tsx b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandAdvancedSection.tsx index af196b2c628..cca68ca54aa 100644 --- a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandAdvancedSection.tsx +++ b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandAdvancedSection.tsx @@ -1,6 +1,7 @@ import type { Dispatch, MutableRefObject, SetStateAction } from 'react' import { ChevronDown } from 'lucide-react' -import type { Repo, TerminalQuickCommand } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' import type { getTerminalQuickCommandScope } from '../../../../shared/terminal-quick-commands' import { isTerminalAgentQuickCommand } from '../../../../shared/terminal-quick-commands' import { Button } from '@/components/ui/button' diff --git a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandContentSection.tsx b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandContentSection.tsx index 19967b0daba..5402ddb8dc4 100644 --- a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandContentSection.tsx +++ b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandContentSection.tsx @@ -1,5 +1,6 @@ import type { Dispatch, MutableRefObject, SetStateAction } from 'react' -import type { TerminalQuickCommand, TuiAgent } from '../../../../shared/types' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { isTerminalAgentQuickCommand, supportsTerminalAgentQuickCommand diff --git a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.test.tsx b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.test.tsx index 050dc3a1dfa..3b9bd8095e3 100644 --- a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.test.tsx +++ b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.test.tsx @@ -3,7 +3,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { TerminalQuickCommand } from '../../../../shared/types' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' import { TerminalQuickCommandDialog } from './TerminalQuickCommandDialog' const mountedRoots: Root[] = [] diff --git a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.tsx b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.tsx index 1c9c615d72d..0e8eadc2ff3 100644 --- a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.tsx +++ b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.tsx @@ -1,10 +1,10 @@ import { useRef, useState } from 'react' +import type { Repo } from '../../../../shared/repo-types' import type { - Repo, TerminalQuickCommand, - TerminalQuickCommandScope, - TuiAgent -} from '../../../../shared/types' + TerminalQuickCommandScope +} from '../../../../shared/terminal-quick-command-types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { getTerminalQuickCommandAction, getTerminalQuickCommandScope, diff --git a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandLabelField.tsx b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandLabelField.tsx index a337be4361e..7fb30e8d683 100644 --- a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandLabelField.tsx +++ b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandLabelField.tsx @@ -1,5 +1,5 @@ import type { Dispatch, SetStateAction } from 'react' -import type { TerminalQuickCommand } from '../../../../shared/types' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandScopeField.tsx b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandScopeField.tsx index 58cd0ea8fd2..7af1f31b53f 100644 --- a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandScopeField.tsx +++ b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandScopeField.tsx @@ -1,9 +1,9 @@ import type { Dispatch, SetStateAction } from 'react' +import type { Repo } from '../../../../shared/repo-types' import type { - Repo, TerminalQuickCommand, TerminalQuickCommandScope -} from '../../../../shared/types' +} from '../../../../shared/terminal-quick-command-types' import { Label } from '@/components/ui/label' import { Select, diff --git a/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-agent-options.ts b/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-agent-options.ts index c0ad04b4159..82b44ae990a 100644 --- a/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-agent-options.ts +++ b/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-agent-options.ts @@ -1,7 +1,7 @@ import type { AgentCatalogEntry } from '@/lib/agent-catalog' import { getAgentCatalog } from '@/lib/agent-catalog' import { supportsTerminalAgentQuickCommand } from '../../../../shared/terminal-quick-commands' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' const QUICK_COMMAND_AGENT_PRESENTATION_ORDER = [ 'claude', diff --git a/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.test.ts b/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.test.ts index d4397d4fd2e..becc121fb15 100644 --- a/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.test.ts +++ b/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalQuickCommand } from '../../../../shared/types' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' import { createTerminalQuickCommandDialogDraftMemory, switchTerminalQuickCommandDialogAction diff --git a/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.ts b/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.ts index ce61efb4d1a..01dc86367c7 100644 --- a/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.ts +++ b/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.ts @@ -2,7 +2,8 @@ import { getTerminalQuickCommandScope, isTerminalAgentQuickCommand } from '../../../../shared/terminal-quick-commands' -import type { TerminalQuickCommand, TuiAgent } from '../../../../shared/types' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' +import type { TuiAgent } from '../../../../shared/tui-agent' export type TerminalQuickCommandDialogAction = 'terminal-command' | 'agent-prompt' diff --git a/src/renderer/src/components/terminal/active-terminal-repair-loop.react185.test.tsx b/src/renderer/src/components/terminal/active-terminal-repair-loop.react185.test.tsx index ab3ff3ac45e..3e8395b1327 100644 --- a/src/renderer/src/components/terminal/active-terminal-repair-loop.react185.test.tsx +++ b/src/renderer/src/components/terminal/active-terminal-repair-loop.react185.test.tsx @@ -3,7 +3,8 @@ import { act, useMemo } from 'react' import { createRoot } from 'react-dom/client' import { afterEach, describe, expect, it } from 'vitest' import { useAppStore } from '@/store' -import type { Tab, TabGroup, TerminalTab } from '../../../../shared/types' +import type { Tab, TabGroup } from '../../../../shared/tab-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { useActiveTerminalRepair } from './use-active-terminal-repair' ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true diff --git a/src/renderer/src/components/terminal/active-terminal-repair.test.ts b/src/renderer/src/components/terminal/active-terminal-repair.test.ts index f62cd0e4da7..4dfb08cf084 100644 --- a/src/renderer/src/components/terminal/active-terminal-repair.test.ts +++ b/src/renderer/src/components/terminal/active-terminal-repair.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { resolveRepairedActiveTerminalTabId, shouldRepairActiveTerminalTab diff --git a/src/renderer/src/components/terminal/active-terminal-repair.ts b/src/renderer/src/components/terminal/active-terminal-repair.ts index 81dc5b2c6d3..539644b6fc8 100644 --- a/src/renderer/src/components/terminal/active-terminal-repair.ts +++ b/src/renderer/src/components/terminal/active-terminal-repair.ts @@ -1,4 +1,5 @@ -import type { TerminalTab, WorkspaceVisibleTabType } from '../../../../shared/types' +import type { WorkspaceVisibleTabType } from '../../../../shared/tab-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' export function shouldRepairActiveTerminalTab(args: { activeTabType: WorkspaceVisibleTabType diff --git a/src/renderer/src/components/terminal/split-group-mount.test.ts b/src/renderer/src/components/terminal/split-group-mount.test.ts index 79316fbe6b1..4a66f6d6f80 100644 --- a/src/renderer/src/components/terminal/split-group-mount.test.ts +++ b/src/renderer/src/components/terminal/split-group-mount.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { getEffectiveLayoutForWorktree, anyMountedWorktreeHasLayout } from './split-group-mount' -import type { TabGroup, TabGroupLayoutNode } from '../../../../shared/types' +import type { TabGroup, TabGroupLayoutNode } from '../../../../shared/tab-types' function makeGroup(id: string, worktreeId: string): TabGroup { return { id, worktreeId, activeTabId: null, tabOrder: [] } diff --git a/src/renderer/src/components/terminal/split-group-mount.ts b/src/renderer/src/components/terminal/split-group-mount.ts index ca10c331bc5..f9fccd56683 100644 --- a/src/renderer/src/components/terminal/split-group-mount.ts +++ b/src/renderer/src/components/terminal/split-group-mount.ts @@ -1,4 +1,4 @@ -import type { TabGroup, TabGroupLayoutNode } from '../../../../shared/types' +import type { TabGroup, TabGroupLayoutNode } from '../../../../shared/tab-types' /** * Derive the effective layout for a worktree: either its explicit layout diff --git a/src/renderer/src/components/terminal/terminal-tab-bulk-actions.ts b/src/renderer/src/components/terminal/terminal-tab-bulk-actions.ts index f9a649eb2ad..3bca59bd476 100644 --- a/src/renderer/src/components/terminal/terminal-tab-bulk-actions.ts +++ b/src/renderer/src/components/terminal/terminal-tab-bulk-actions.ts @@ -1,4 +1,4 @@ -import type { TabContentType } from '../../../../shared/types' +import type { TabContentType } from '../../../../shared/tab-types' import { hasUnroutableTerminalWorktreeOwner, resolveTerminalWorktreeRoute diff --git a/src/renderer/src/components/terminal/use-active-terminal-repair.ts b/src/renderer/src/components/terminal/use-active-terminal-repair.ts index d495c80c330..81197f29fd2 100644 --- a/src/renderer/src/components/terminal/use-active-terminal-repair.ts +++ b/src/renderer/src/components/terminal/use-active-terminal-repair.ts @@ -1,5 +1,6 @@ import { useEffect } from 'react' -import type { TerminalTab, WorkspaceVisibleTabType } from '../../../../shared/types' +import type { WorkspaceVisibleTabType } from '../../../../shared/tab-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { resolveRepairedActiveTerminalTabId } from './active-terminal-repair' type ActiveTerminalRepairInput = { diff --git a/src/renderer/src/components/ui/repo-multi-combobox.test.ts b/src/renderer/src/components/ui/repo-multi-combobox.test.ts index a0a4a747e31..3248e743afc 100644 --- a/src/renderer/src/components/ui/repo-multi-combobox.test.ts +++ b/src/renderer/src/components/ui/repo-multi-combobox.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { getRepoMultiComboboxDetail } from './repo-multi-combobox' function repo(overrides: Partial = {}): Repo { diff --git a/src/renderer/src/components/ui/repo-multi-combobox.tsx b/src/renderer/src/components/ui/repo-multi-combobox.tsx index f3dfef437d0..5af4313ebda 100644 --- a/src/renderer/src/components/ui/repo-multi-combobox.tsx +++ b/src/renderer/src/components/ui/repo-multi-combobox.tsx @@ -11,7 +11,7 @@ import { import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { searchRepos } from '@/lib/repo-search' import { cn } from '@/lib/utils' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import RepoBadgeLabel from '@/components/repo/RepoBadgeLabel' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.test.ts b/src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.test.ts index e2546210158..0cb2b0329d0 100644 --- a/src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.test.ts +++ b/src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { countEstimatedInactiveWorkspaces } from './inactive-workspace-estimate' const NOW = 1_800_000_000_000 diff --git a/src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.ts b/src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.ts index a0103360f5f..23524b5e904 100644 --- a/src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.ts +++ b/src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.ts @@ -1,4 +1,5 @@ -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { isFolderRepo } from '../../../../shared/repo-kind' import { getPersistedWorkspaceCleanupActivityAt, diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation-fixtures.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation-fixtures.ts index 4538c9619c5..17f5f2f7c05 100644 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation-fixtures.ts +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation-fixtures.ts @@ -1,7 +1,8 @@ import type { AppState } from '@/store/types' import { translate } from '@/i18n/i18n' import type { HostedReviewInfo } from '../../../../shared/hosted-review' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' import type { WorkspaceCleanupFilters } from './workspace-cleanup-presentation' diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation.ts index bfcc64cc0fa..1accef346ca 100644 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation.ts +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation.ts @@ -3,7 +3,8 @@ import { getHostedReviewCacheKey } from '@/store/slices/hosted-review' import type { AppState } from '@/store/types' import { translate } from '@/i18n/i18n' import type { HostedReviewInfo, HostedReviewProvider } from '../../../../shared/hosted-review' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' export type WorkspaceCleanupSortKey = 'activity' | 'name' | 'repo' | 'review' | 'git' diff --git a/src/renderer/src/components/worktree-base-fallback-notice.ts b/src/renderer/src/components/worktree-base-fallback-notice.ts index fc314732b4a..1d31517b224 100644 --- a/src/renderer/src/components/worktree-base-fallback-notice.ts +++ b/src/renderer/src/components/worktree-base-fallback-notice.ts @@ -1,4 +1,4 @@ -import type { WorktreeCreateBaseFallback } from '../../../shared/types' +import type { WorktreeCreateBaseFallback } from '../../../shared/worktree/create-types' const notices: WorktreeCreateBaseFallback[] = [] const listeners = new Set<() => void>() diff --git a/src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx b/src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx index 425522acbb3..7ed47d72512 100644 --- a/src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx +++ b/src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx @@ -4,7 +4,10 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as ReactI18Next from 'react-i18next' -import type { Repo, Tab, TabGroup, TerminalTab, Worktree } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' +import type { Tab, TabGroup } from '../../../shared/tab-types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { Worktree } from '../../../shared/worktree/types' import { useAppStore } from '@/store' import type { AppState } from '@/store/types' import { diff --git a/src/renderer/src/components/worktree-jump-palette-sleeping-filter.test.ts b/src/renderer/src/components/worktree-jump-palette-sleeping-filter.test.ts index 05ac5130e36..a5410ec7dbb 100644 --- a/src/renderer/src/components/worktree-jump-palette-sleeping-filter.test.ts +++ b/src/renderer/src/components/worktree-jump-palette-sleeping-filter.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs' import { join } from 'node:path' import { describe, expect, it } from 'vitest' import { isSleepingSweepExemptWorkspace } from './sidebar/visible-worktrees' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' const source = readFileSync(join(__dirname, 'WorktreeJumpPalette.tsx'), 'utf8') diff --git a/src/renderer/src/components/worktree-jump-palette-test-fixtures.ts b/src/renderer/src/components/worktree-jump-palette-test-fixtures.ts index f707b445d5a..5dfbcdaf6a8 100644 --- a/src/renderer/src/components/worktree-jump-palette-test-fixtures.ts +++ b/src/renderer/src/components/worktree-jump-palette-test-fixtures.ts @@ -1,4 +1,7 @@ -import type { Repo, Tab, TabGroup, TerminalTab, Worktree } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' +import type { Tab, TabGroup } from '../../../shared/tab-types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { Worktree } from '../../../shared/worktree/types' import type { AgentStatusEntry, AgentStatusState } from '../../../shared/agent-status-types' import { makePaneKey } from '../../../shared/stable-pane-id' import type { AppState } from '@/store/types' diff --git a/src/renderer/src/hooks/direct-ssh-host-hydration.test.ts b/src/renderer/src/hooks/direct-ssh-host-hydration.test.ts index 3f8a00813eb..4aa654cf33a 100644 --- a/src/renderer/src/hooks/direct-ssh-host-hydration.test.ts +++ b/src/renderer/src/hooks/direct-ssh-host-hydration.test.ts @@ -3,7 +3,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import type { HostLineageSnapshot } from '../../../shared/host-lineage-contract' import type { HostRepoCatalogSnapshot } from '../../../shared/host-repo-catalog-contract' import type { DirectSshAuthority, SshProviderEpoch } from '../../../shared/ssh-types' -import type { Repo, WorktreeLineage, WorkspaceLineage } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' +import type { WorkspaceLineage, WorktreeLineage } from '../../../shared/worktree/lineage-types' import { folderWorkspaceKey, worktreeWorkspaceKey } from '../../../shared/workspace-scope' import type { AppState } from '../store/types' import { createDirectSshHostHydration } from './direct-ssh-host-hydration' diff --git a/src/renderer/src/hooks/ipc-events-test-harness.ts b/src/renderer/src/hooks/ipc-events-test-harness.ts index e2226460a7b..509aff00b04 100644 --- a/src/renderer/src/hooks/ipc-events-test-harness.ts +++ b/src/renderer/src/hooks/ipc-events-test-harness.ts @@ -1,6 +1,6 @@ import { vi } from 'vitest' import type * as ReactModule from 'react' -import type { TerminalPaneLayoutNode } from '../../../shared/types' +import type { TerminalPaneLayoutNode } from '../../../shared/terminal-tab-types' export type CreateTerminalRequest = { requestId?: string diff --git a/src/renderer/src/hooks/remote-workspace-session-merge.ts b/src/renderer/src/hooks/remote-workspace-session-merge.ts index 0f772ff8794..c5a31073184 100644 --- a/src/renderer/src/hooks/remote-workspace-session-merge.ts +++ b/src/renderer/src/hooks/remote-workspace-session-merge.ts @@ -1,4 +1,5 @@ -import type { TerminalTab, WorkspaceSessionState } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' import { worktreeWorkspaceKey } from '../../../shared/workspace-scope' import { splitWorktreeId } from '../../../shared/worktree/id' import type { AppState } from '../store/types' diff --git a/src/renderer/src/hooks/remote-workspace-target-sync.ts b/src/renderer/src/hooks/remote-workspace-target-sync.ts index 79bc3d9690e..836fd763fc6 100644 --- a/src/renderer/src/hooks/remote-workspace-target-sync.ts +++ b/src/renderer/src/hooks/remote-workspace-target-sync.ts @@ -3,7 +3,7 @@ import type { RemoteWorkspacePatchResult, RemoteWorkspaceSnapshot } from '../../../shared/remote-workspace-types' -import type { WorkspaceSessionState } from '../../../shared/types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' import type { DirectSshAuthority } from '../../../shared/ssh-types' import { translate } from '@/i18n/i18n' import { buildWorkspaceSessionPayload } from '../lib/workspace-session' diff --git a/src/renderer/src/hooks/resolve-zoom-target.ts b/src/renderer/src/hooks/resolve-zoom-target.ts index 5e3c2171a3c..cc727f4c8a7 100644 --- a/src/renderer/src/hooks/resolve-zoom-target.ts +++ b/src/renderer/src/hooks/resolve-zoom-target.ts @@ -54,4 +54,4 @@ export function resolveZoomTarget(args: { } return 'ui' } -import type { TopLevelView } from '../../../shared/types' +import type { TopLevelView } from '../../../shared/ui-chrome-types' diff --git a/src/renderer/src/hooks/use-terminal-quick-command-hosts.test.ts b/src/renderer/src/hooks/use-terminal-quick-command-hosts.test.ts index c3014b35d8c..fe14513ecf3 100644 --- a/src/renderer/src/hooks/use-terminal-quick-command-hosts.test.ts +++ b/src/renderer/src/hooks/use-terminal-quick-command-hosts.test.ts @@ -5,7 +5,7 @@ import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../shared/constants' import type { ExecutionHostId } from '../../../shared/execution-host' -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import type { RuntimeTerminalQuickCommands } from '@/store/slices/terminal-quick-command-hosts' const testState = vi.hoisted(() => ({ diff --git a/src/renderer/src/hooks/use-terminal-quick-command-hosts.ts b/src/renderer/src/hooks/use-terminal-quick-command-hosts.ts index df70bb58fdc..28483b4ecb1 100644 --- a/src/renderer/src/hooks/use-terminal-quick-command-hosts.ts +++ b/src/renderer/src/hooks/use-terminal-quick-command-hosts.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo } from 'react' import { useShallow } from 'zustand/react/shallow' -import type { GlobalSettings, TerminalQuickCommand } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { TerminalQuickCommand } from '../../../shared/terminal-quick-command-types' import { LOCAL_EXECUTION_HOST_ID, parseExecutionHostId, diff --git a/src/renderer/src/hooks/useAgentDetectionTarget.test.ts b/src/renderer/src/hooks/useAgentDetectionTarget.test.ts index dd8f1b8176c..16cf58acf1f 100644 --- a/src/renderer/src/hooks/useAgentDetectionTarget.test.ts +++ b/src/renderer/src/hooks/useAgentDetectionTarget.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from 'vitest' -import type { FolderWorkspace, ProjectGroup, Repo } from '../../../shared/types' +import type { FolderWorkspace } from '../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../shared/project-group-types' +import type { Repo } from '../../../shared/repo-types' import { folderWorkspaceKey } from '../../../shared/workspace-scope' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' import { diff --git a/src/renderer/src/hooks/useAutoAckViewedAgent.ts b/src/renderer/src/hooks/useAutoAckViewedAgent.ts index 1e6052342ba..4e471aa5eed 100644 --- a/src/renderer/src/hooks/useAutoAckViewedAgent.ts +++ b/src/renderer/src/hooks/useAutoAckViewedAgent.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react' import { useAppStore } from '@/store' import type { AgentStatusEntry } from '../../../shared/agent-status-types' import type { RetainedAgentEntry } from '@/store/slices/agent-status' -import type { TerminalLayoutSnapshot } from '../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../shared/terminal-tab-types' import { isTerminalLeafId, makePaneKey, parsePaneKey } from '../../../shared/stable-pane-id' function resolveActiveLeafId( diff --git a/src/renderer/src/hooks/useComposerState.ts b/src/renderer/src/hooks/useComposerState.ts index aa83700baf2..1b26a98dfe5 100644 --- a/src/renderer/src/hooks/useComposerState.ts +++ b/src/renderer/src/hooks/useComposerState.ts @@ -39,26 +39,27 @@ import { normalizeTaskSourceContext, type TaskSourceContext } from '../../../shared/task-source-context' +import type { GitHubRepositoryIdentity } from '../../../shared/github/pull-request-types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import type { GitLabWorkItem } from '../../../shared/gitlab-types' +import type { JiraIssue } from '../../../shared/jira-types' +import type { LinearIssue } from '../../../shared/linear/issue-types' import type { - GitHubRepositoryIdentity, - GitHubWorkItem, - GitHubPrStartPoint, - GitPushTarget, - GitLabWorkItem, - JiraIssue, - LinearIssue, OrcaHooks, RepoHookSettings, SetupAgentStartupPolicy, - SetupDecision, - SetupRunPolicy, - SparsePreset, - TuiAgent, - WorktreeMeta, - WorkspaceStatus, - WorkspaceCreateTelemetrySource, - ProjectGroup -} from '../../../shared/types' + SetupRunPolicy +} from '../../../shared/orca-yaml-hook-types' +import type { ProjectGroup } from '../../../shared/project-group-types' +import type { TuiAgent } from '../../../shared/tui-agent' +import type { WorkspaceSource as WorkspaceCreateTelemetrySource } from '../../../shared/workspace-source' +import type { SetupDecision, SparsePreset } from '../../../shared/worktree/create-types' +import type { WorktreeMeta } from '../../../shared/worktree/meta-types' +import type { + GitHubPrStartPoint, + GitPushTarget, + WorkspaceStatus +} from '../../../shared/worktree/types' import { githubRepoIdentityKey } from '../../../shared/github/repository-identity-key' import { isWorkspaceStatusId } from '../../../shared/workspace-statuses' import { diff --git a/src/renderer/src/hooks/useDetectedAgents.ts b/src/renderer/src/hooks/useDetectedAgents.ts index 3015e524aa4..284083b287d 100644 --- a/src/renderer/src/hooks/useDetectedAgents.ts +++ b/src/renderer/src/hooks/useDetectedAgents.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef } from 'react' import { useAppStore } from '@/store' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' export type UseDetectedAgentsResult = { /** Null while detection is in flight on first load. */ diff --git a/src/renderer/src/hooks/useEditorExternalWatch-complexity.test.ts b/src/renderer/src/hooks/useEditorExternalWatch-complexity.test.ts index cfcb973d968..f6e726f5a1f 100644 --- a/src/renderer/src/hooks/useEditorExternalWatch-complexity.test.ts +++ b/src/renderer/src/hooks/useEditorExternalWatch-complexity.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as EditorAutosaveModule from '@/components/editor/editor-autosave' import type * as CrossPlatformPathModule from '../../../shared/cross-platform-path' -import type { FsChangedPayload } from '../../../shared/types' +import type { FsChangedPayload } from '../../../shared/filesystem-entry-types' const pathOperationCounts = vi.hoisted(() => ({ aliasComparisons: 0, diff --git a/src/renderer/src/hooks/useEditorExternalWatch-self-move.test.ts b/src/renderer/src/hooks/useEditorExternalWatch-self-move.test.ts index a9fbd0e2c61..523e00420ca 100644 --- a/src/renderer/src/hooks/useEditorExternalWatch-self-move.test.ts +++ b/src/renderer/src/hooks/useEditorExternalWatch-self-move.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as EditorAutosaveModule from '@/components/editor/editor-autosave' -import type { FsChangedPayload } from '../../../shared/types' +import type { FsChangedPayload } from '../../../shared/filesystem-entry-types' vi.mock('@/store', () => ({ useAppStore: { getState: vi.fn() } diff --git a/src/renderer/src/hooks/useEditorExternalWatch-wsl-repro.test.ts b/src/renderer/src/hooks/useEditorExternalWatch-wsl-repro.test.ts index 256632dfd58..930dc1167f6 100644 --- a/src/renderer/src/hooks/useEditorExternalWatch-wsl-repro.test.ts +++ b/src/renderer/src/hooks/useEditorExternalWatch-wsl-repro.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as EditorAutosaveModule from '@/components/editor/editor-autosave' -import type { FsChangedPayload } from '../../../shared/types' +import type { FsChangedPayload } from '../../../shared/filesystem-entry-types' vi.mock('@/store', () => ({ useAppStore: { getState: vi.fn() } })) vi.mock('@/components/editor/editor-autosave', async (importOriginal) => { diff --git a/src/renderer/src/hooks/useEditorExternalWatch.test.ts b/src/renderer/src/hooks/useEditorExternalWatch.test.ts index 891bbc35e4e..c006d2612d5 100644 --- a/src/renderer/src/hooks/useEditorExternalWatch.test.ts +++ b/src/renderer/src/hooks/useEditorExternalWatch.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi, afterEach } from 'vitest' import type * as EditorAutosaveModule from '@/components/editor/editor-autosave' -import type { FsChangedPayload } from '../../../shared/types' +import type { FsChangedPayload } from '../../../shared/filesystem-entry-types' vi.mock('@/store', () => ({ useAppStore: { diff --git a/src/renderer/src/hooks/useEditorExternalWatch.ts b/src/renderer/src/hooks/useEditorExternalWatch.ts index c07f9bd25c1..304390a6b42 100644 --- a/src/renderer/src/hooks/useEditorExternalWatch.ts +++ b/src/renderer/src/hooks/useEditorExternalWatch.ts @@ -22,7 +22,7 @@ import { hasActiveEditorPathMoves, isActiveMoveSourcePath } from '@/components/editor/editor-path-move-inflight' -import type { FsChangedPayload } from '../../../shared/types' +import type { FsChangedPayload } from '../../../shared/filesystem-entry-types' import { findWorktreeById } from '@/store/slices/worktree-helpers' import type { OpenFile } from '@/store/slices/editor' import { readRuntimeFileContent, subscribeRuntimeFileChanges } from '@/runtime/runtime-file-client' diff --git a/src/renderer/src/hooks/useEphemeralVmRecipeOptions.ts b/src/renderer/src/hooks/useEphemeralVmRecipeOptions.ts index 6ebf489f894..3cf47e7a707 100644 --- a/src/renderer/src/hooks/useEphemeralVmRecipeOptions.ts +++ b/src/renderer/src/hooks/useEphemeralVmRecipeOptions.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../shared/execution-host' -import type { OrcaVmRecipe } from '../../../shared/types' +import type { OrcaVmRecipe } from '../../../shared/orca-yaml-hook-types' type EphemeralVmRecipeOptionsArgs = { enabled: boolean diff --git a/src/renderer/src/hooks/useGitHubSlugMetadata.ts b/src/renderer/src/hooks/useGitHubSlugMetadata.ts index a268a7bbc93..8b7ab1b4c62 100644 --- a/src/renderer/src/hooks/useGitHubSlugMetadata.ts +++ b/src/renderer/src/hooks/useGitHubSlugMetadata.ts @@ -1,5 +1,6 @@ import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' -import type { GitHubAssignableUser, GlobalSettings } from '../../../shared/types' +import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import type { ListAssignableUsersBySlugResult, ListLabelsBySlugResult diff --git a/src/renderer/src/hooks/useGlobalFileDrop.ts b/src/renderer/src/hooks/useGlobalFileDrop.ts index b151809fd76..1ddc557f49b 100644 --- a/src/renderer/src/hooks/useGlobalFileDrop.ts +++ b/src/renderer/src/hooks/useGlobalFileDrop.ts @@ -12,7 +12,7 @@ import { statRuntimePath, type RuntimeFileOperationArgs } from '@/runtime/runtime-file-client' -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' import type { WorktreeRuntimeOwnerState } from '@/lib/worktree-runtime-owner' import { diff --git a/src/renderer/src/hooks/useInstalledAgentSkills.react.test.tsx b/src/renderer/src/hooks/useInstalledAgentSkills.react.test.tsx index 61070599652..3c379e1c1f2 100644 --- a/src/renderer/src/hooks/useInstalledAgentSkills.react.test.tsx +++ b/src/renderer/src/hooks/useInstalledAgentSkills.react.test.tsx @@ -9,7 +9,7 @@ import type { SkillDiscoveryTarget } from '../../../shared/skills' import type { ProjectExecutionRuntimeResolution } from '../../../shared/project-execution-runtime' -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import { createCompatibleRuntimeStatusResponseIfNeeded } from '@/runtime/runtime-compatibility-test-fixture' import { clearRuntimeCompatibilityCacheForTests } from '@/runtime/runtime-rpc-client' import { useAppStore } from '@/store' diff --git a/src/renderer/src/hooks/useIpcEvents.test.ts b/src/renderer/src/hooks/useIpcEvents.test.ts index 064f816516f..392750fe861 100644 --- a/src/renderer/src/hooks/useIpcEvents.test.ts +++ b/src/renderer/src/hooks/useIpcEvents.test.ts @@ -33,7 +33,8 @@ import type { AgentStatusClearIpcPayload, MigrationUnsupportedPtyEntry } from '../../../shared/agent-status-types' -import type { TerminalPaneLayoutNode, TuiAgent } from '../../../shared/types' +import type { TerminalPaneLayoutNode } from '../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' import type * as CmdJRowIndexJump from '@/lib/cmd-j-row-index-jump' import { makePaneKey } from '../../../shared/stable-pane-id' import { folderWorkspaceKey } from '../../../shared/workspace-scope' diff --git a/src/renderer/src/hooks/useIpcEvents.ts b/src/renderer/src/hooks/useIpcEvents.ts index 9ed8d279660..d9f3727232d 100644 --- a/src/renderer/src/hooks/useIpcEvents.ts +++ b/src/renderer/src/hooks/useIpcEvents.ts @@ -28,9 +28,9 @@ import { nextEditorFontZoomLevel, computeEditorFontSize } from '@/lib/editor-fon import { canConnectSshStatus } from '@/ssh/ssh-connection-recoverability' import type { TerminalLayoutSnapshot, - TerminalPaneLayoutNode, - UpdateStatus -} from '../../../shared/types' + TerminalPaneLayoutNode +} from '../../../shared/terminal-tab-types' +import type { UpdateStatus } from '../../../shared/update-status-types' import type { RateLimitState } from '../../../shared/rate-limit-types' import type { DirectSshAuthority, SshConnectionState } from '../../../shared/ssh-types' import { diff --git a/src/renderer/src/hooks/useIssueMetadata.ts b/src/renderer/src/hooks/useIssueMetadata.ts index 8d8a44599ef..3dc10686323 100644 --- a/src/renderer/src/hooks/useIssueMetadata.ts +++ b/src/renderer/src/hooks/useIssueMetadata.ts @@ -6,12 +6,12 @@ import { linearTeamStates, type RuntimeLinearSettings } from '@/runtime/runtime-linear-client' +import type { GitHubAssignableUser } from '../../../shared/github/pull-request-types' import type { - GitHubAssignableUser, - LinearWorkflowState, LinearLabel, - LinearMember -} from '../../../shared/types' + LinearMember, + LinearWorkflowState +} from '../../../shared/linear/workspace-types' import { getTaskSourceRuntimeSettings } from '../../../shared/task-source-context' import { unionLinearMetadataById } from '../components/linear-issue-attribute-filter-team-ids' import { diff --git a/src/renderer/src/hooks/useLinearProviderConnected.test.tsx b/src/renderer/src/hooks/useLinearProviderConnected.test.tsx index c10afed44c5..622d6fcd95f 100644 --- a/src/renderer/src/hooks/useLinearProviderConnected.test.tsx +++ b/src/renderer/src/hooks/useLinearProviderConnected.test.tsx @@ -3,7 +3,7 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, describe, expect, it } from 'vitest' -import type { LinearConnectionStatus } from '../../../shared/types' +import type { LinearConnectionStatus } from '../../../shared/linear/workspace-types' import { getProviderRuntimeContextKey } from '@/lib/provider-runtime-context' import { useAppStore } from '@/store' import { useLinearProviderConnected } from './useLinearProviderConnected' diff --git a/src/renderer/src/hooks/useSettingsNavigationMetadata.capability-owner.test.tsx b/src/renderer/src/hooks/useSettingsNavigationMetadata.capability-owner.test.tsx index 1cb259a8578..fc0e4e04464 100644 --- a/src/renderer/src/hooks/useSettingsNavigationMetadata.capability-owner.test.tsx +++ b/src/renderer/src/hooks/useSettingsNavigationMetadata.capability-owner.test.tsx @@ -4,7 +4,7 @@ import { act, createElement } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../shared/constants' -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import type { SettingsNavSection } from '@/lib/settings-navigation-types' import { resetWindowsTerminalCapabilitiesForTests } from '@/lib/windows-terminal-capabilities' diff --git a/src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts b/src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts index 6008db31856..6cc9f641280 100644 --- a/src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts +++ b/src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { describe, expect, it } from 'vitest' import { buildSettingsNavigationMetadata } from './useSettingsNavigationMetadata' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' const repo = { id: 'repo-1', diff --git a/src/renderer/src/hooks/useSettingsNavigationMetadata.ts b/src/renderer/src/hooks/useSettingsNavigationMetadata.ts index c0c85b115ed..77d9a885f7e 100644 --- a/src/renderer/src/hooks/useSettingsNavigationMetadata.ts +++ b/src/renderer/src/hooks/useSettingsNavigationMetadata.ts @@ -39,7 +39,7 @@ import { } from 'lucide-react' import { OrcaLogoSettingsIcon } from '@/components/settings/orca-logo-settings-icon' import { LinearIcon } from '@/components/icons/LinearIcon' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import { getRepoKindLabel } from '../../../shared/repo-kind' import { useAppStore } from '@/store' import { isMacUserAgent, isWindowsUserAgent } from '@/components/terminal-pane/pane-helpers' diff --git a/src/renderer/src/hooks/useTaskPageGitHubWorkItemMutation.ts b/src/renderer/src/hooks/useTaskPageGitHubWorkItemMutation.ts index 9b80be9a6c4..24b6a969c68 100644 --- a/src/renderer/src/hooks/useTaskPageGitHubWorkItemMutation.ts +++ b/src/renderer/src/hooks/useTaskPageGitHubWorkItemMutation.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { toast } from 'sonner' import { parseExecutionHostId } from '../../../shared/execution-host' import type { ParsedTaskQuery } from '../../../shared/task-query' -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { useAppStore } from '@/store' import { diff --git a/src/renderer/src/hooks/worktree-file-change-event.ts b/src/renderer/src/hooks/worktree-file-change-event.ts index 67c234202bb..1cfe6fca1ae 100644 --- a/src/renderer/src/hooks/worktree-file-change-event.ts +++ b/src/renderer/src/hooks/worktree-file-change-event.ts @@ -1,4 +1,4 @@ -import type { FsChangedPayload } from '../../../shared/types' +import type { FsChangedPayload } from '../../../shared/filesystem-entry-types' export const ORCA_WORKTREE_FILE_CHANGE_EVENT = 'orca:worktree-file-change' diff --git a/src/renderer/src/hooks/worktree-head-identity-apply.test.ts b/src/renderer/src/hooks/worktree-head-identity-apply.test.ts index 3df3e92297e..35028c5fa7f 100644 --- a/src/renderer/src/hooks/worktree-head-identity-apply.test.ts +++ b/src/renderer/src/hooks/worktree-head-identity-apply.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' import { applyWorktreeHeadIdentities } from './worktree-head-identity-apply' function makeWorktree(overrides: Partial): Worktree { diff --git a/src/renderer/src/hooks/worktree-head-identity-apply.ts b/src/renderer/src/hooks/worktree-head-identity-apply.ts index 29be23b6022..c419b17fcf6 100644 --- a/src/renderer/src/hooks/worktree-head-identity-apply.ts +++ b/src/renderer/src/hooks/worktree-head-identity-apply.ts @@ -1,4 +1,4 @@ -import type { Worktree, WorktreeHeadIdentity } from '../../../shared/types' +import type { Worktree, WorktreeHeadIdentity } from '../../../shared/worktree/types' import { normalizeRuntimePathForComparison } from '../../../shared/cross-platform-path' export type WorktreeHeadIdentityApplyDeps = { diff --git a/src/renderer/src/lib/active-agent-note-send.test.ts b/src/renderer/src/lib/active-agent-note-send.test.ts index 2abcdd0d772..bad9faf7daf 100644 --- a/src/renderer/src/lib/active-agent-note-send.test.ts +++ b/src/renderer/src/lib/active-agent-note-send.test.ts @@ -10,7 +10,7 @@ import { } from './active-agent-note-send' import type { AgentStatusEntry } from '../../../shared/agent-status-types' import { makePaneKey } from '../../../shared/stable-pane-id' -import type { TerminalLayoutSnapshot } from '../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../shared/terminal-tab-types' const LEAF_ID = '11111111-1111-4111-8111-111111111111' const OTHER_LEAF_ID = '22222222-2222-4222-8222-222222222222' diff --git a/src/renderer/src/lib/active-agent-note-target.ts b/src/renderer/src/lib/active-agent-note-target.ts index 326a44dbb0b..dbacc97e4c1 100644 --- a/src/renderer/src/lib/active-agent-note-target.ts +++ b/src/renderer/src/lib/active-agent-note-target.ts @@ -11,7 +11,7 @@ import { } from '@/lib/worktree-runtime-owner' import { toRuntimeWorktreeSelector } from '@/runtime/runtime-worktree-selector' import { isTerminalLeafId, makePaneKey } from '../../../shared/stable-pane-id' -import type { TerminalLayoutSnapshot } from '../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../shared/terminal-tab-types' import { classifyTitleActivity, isExplicitAgentStatusFresh, diff --git a/src/renderer/src/lib/active-view-persist.ts b/src/renderer/src/lib/active-view-persist.ts index 79f965b33aa..dfd77e70b44 100644 --- a/src/renderer/src/lib/active-view-persist.ts +++ b/src/renderer/src/lib/active-view-persist.ts @@ -1,4 +1,5 @@ -import type { PersistedUIState, TopLevelView } from '../../../shared/types' +import type { PersistedUIState } from '../../../shared/persisted-ui-state-types' +import type { TopLevelView } from '../../../shared/ui-chrome-types' type ActiveViewUnloadState = { activeView: TopLevelView diff --git a/src/renderer/src/lib/activity-thread-display.ts b/src/renderer/src/lib/activity-thread-display.ts index f1933a06146..ea6603edeac 100644 --- a/src/renderer/src/lib/activity-thread-display.ts +++ b/src/renderer/src/lib/activity-thread-display.ts @@ -3,7 +3,8 @@ import type { AgentStatusEntry, AgentStatusState } from '../../../shared/agent-status-types' -import type { TerminalTab, Worktree } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { Worktree } from '../../../shared/worktree/types' import { getAgentRowPrimaryText, isOrcaDispatchPrompt, diff --git a/src/renderer/src/lib/agent-background-draft-delivery.ts b/src/renderer/src/lib/agent-background-draft-delivery.ts index 745248f7239..6976745c30d 100644 --- a/src/renderer/src/lib/agent-background-draft-delivery.ts +++ b/src/renderer/src/lib/agent-background-draft-delivery.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import { pasteDraftWhenAgentReady } from '@/lib/agent-paste-draft' import { showAutomationPromptNotSentToast } from '@/lib/agent-background-session-timeout-toast' diff --git a/src/renderer/src/lib/agent-background-session-contract.ts b/src/renderer/src/lib/agent-background-session-contract.ts index fe793aa24d3..26145d313dd 100644 --- a/src/renderer/src/lib/agent-background-session-contract.ts +++ b/src/renderer/src/lib/agent-background-session-contract.ts @@ -1,6 +1,6 @@ import type { ParsedAgentStatusPayload } from '../../../shared/agent-status-types' import type { LaunchSource } from '../../../shared/telemetry-events' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import type { AgentStartupPlan } from '@/lib/tui-agent-startup' import type { AutomationTerminalOwnership } from '@/lib/automation-terminal-ownership' diff --git a/src/renderer/src/lib/agent-background-session-timeout-toast.ts b/src/renderer/src/lib/agent-background-session-timeout-toast.ts index d6d2751b38a..71870958fd7 100644 --- a/src/renderer/src/lib/agent-background-session-timeout-toast.ts +++ b/src/renderer/src/lib/agent-background-session-timeout-toast.ts @@ -1,7 +1,7 @@ import { toast } from 'sonner' import { translate } from '@/i18n/i18n' import { track, tuiAgentToAgentKind } from '@/lib/telemetry' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' export function showAutomationPromptNotSentToast(agent: TuiAgent): void { toast.message( diff --git a/src/renderer/src/lib/agent-catalog.tsx b/src/renderer/src/lib/agent-catalog.tsx index ffac60fe4ca..650668bc1a0 100644 --- a/src/renderer/src/lib/agent-catalog.tsx +++ b/src/renderer/src/lib/agent-catalog.tsx @@ -1,7 +1,7 @@ import type React from 'react' import { ClaudeIcon, DroidIcon, OpenAIIcon } from '@/components/status-bar/icons' import openClaudeLogoUrl from '../../../../resources/openclaude-logo.png?url' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import { getTuiAgentLaunchCommand, TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' import { AgentLetterIcon, diff --git a/src/renderer/src/lib/agent-draft-paste-content.ts b/src/renderer/src/lib/agent-draft-paste-content.ts index 526e717de27..a76086b6790 100644 --- a/src/renderer/src/lib/agent-draft-paste-content.ts +++ b/src/renderer/src/lib/agent-draft-paste-content.ts @@ -1,5 +1,5 @@ import { yieldToEventLoop } from '../../../shared/event-loop-yield' -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import { getUtf8ByteLengthForCodePoint, readUtf8CodePointAt diff --git a/src/renderer/src/lib/agent-draft-readiness.ts b/src/renderer/src/lib/agent-draft-readiness.ts index 248137ab313..8980f1aeb5c 100644 --- a/src/renderer/src/lib/agent-draft-readiness.ts +++ b/src/renderer/src/lib/agent-draft-readiness.ts @@ -1,5 +1,5 @@ import type { DraftPasteReadySignal } from '../../../shared/tui-agent-config' -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import { subscribeToPtyData } from '@/components/terminal-pane/pty-data-sidecar-subscriptions' import { isRemoteRuntimePtyId } from '@/runtime/runtime-terminal-inspection' import { subscribeToRuntimeTerminalData } from '@/runtime/runtime-terminal-stream' diff --git a/src/renderer/src/lib/agent-favicon-assets.ts b/src/renderer/src/lib/agent-favicon-assets.ts index dd1489e14bd..565437f75be 100644 --- a/src/renderer/src/lib/agent-favicon-assets.ts +++ b/src/renderer/src/lib/agent-favicon-assets.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import grokUrl from '../../../shared/agent-icons/grok.png?url' import mimoCodeUrl from '../../../shared/agent-icons/mimo-code.png?url' import anteUrl from '../../../shared/agent-icons/ante.png?url' diff --git a/src/renderer/src/lib/agent-followup-delivery.ts b/src/renderer/src/lib/agent-followup-delivery.ts index 0042cb0f2e4..bdcf0b4a92f 100644 --- a/src/renderer/src/lib/agent-followup-delivery.ts +++ b/src/renderer/src/lib/agent-followup-delivery.ts @@ -7,7 +7,7 @@ import { isExpectedAgentProcess } from '../../../shared/agent-process-recognition' import { isShellProcess } from '../../../shared/shell-process-detection' -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' type RuntimeOwnerSettings = Pick | null | undefined diff --git a/src/renderer/src/lib/agent-hibernation-coordinator.test.ts b/src/renderer/src/lib/agent-hibernation-coordinator.test.ts index e6b7d41951f..f87c59591b6 100644 --- a/src/renderer/src/lib/agent-hibernation-coordinator.test.ts +++ b/src/renderer/src/lib/agent-hibernation-coordinator.test.ts @@ -2,7 +2,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import type { AgentStatusEntry } from '../../../shared/agent-status-types' -import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types' import { useAppStore } from '@/store' import { DEFAULT_AGENT_HIBERNATION_IDLE_MS } from './agent-hibernation-planner' import { diff --git a/src/renderer/src/lib/agent-hibernation-planner.test.ts b/src/renderer/src/lib/agent-hibernation-planner.test.ts index 4c3a550642f..f825fc9758d 100644 --- a/src/renderer/src/lib/agent-hibernation-planner.test.ts +++ b/src/renderer/src/lib/agent-hibernation-planner.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { AgentStatusEntry } from '../../../shared/agent-status-types' -import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types' import { DEFAULT_AGENT_HIBERNATION_IDLE_MS, MAX_AGENT_HIBERNATION_IDLE_MS, diff --git a/src/renderer/src/lib/agent-hibernation-planner.ts b/src/renderer/src/lib/agent-hibernation-planner.ts index f5631ffc8ed..c350296d9b3 100644 --- a/src/renderer/src/lib/agent-hibernation-planner.ts +++ b/src/renderer/src/lib/agent-hibernation-planner.ts @@ -7,7 +7,8 @@ import { import { parsePaneKey } from '../../../shared/stable-pane-id' import { lastInputBlocksHibernation } from './agent-hibernation-input-guard' import { isCompletedPiCompatibleAgentWithLiveRecoveryRecord } from './pi-compatible-live-recovery-record' -import type { GlobalSettings, TerminalLayoutSnapshot, TerminalTab } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types' import { parseRemoteRuntimePtyId } from '@/runtime/runtime-terminal-stream' export const DEFAULT_AGENT_HIBERNATION_IDLE_MS = 30 * 60 * 1000 diff --git a/src/renderer/src/lib/agent-launch-prompt-delivery.ts b/src/renderer/src/lib/agent-launch-prompt-delivery.ts index de76c6f4d14..87680462cce 100644 --- a/src/renderer/src/lib/agent-launch-prompt-delivery.ts +++ b/src/renderer/src/lib/agent-launch-prompt-delivery.ts @@ -3,7 +3,7 @@ import { pasteDraftWhenAgentReady } from '@/lib/agent-paste-draft' import { canMirrorLaunchDraftToNativeChat } from '@/lib/native-chat-launch-draft-mirrorability' import { isNativeChatSupportedAgent } from '@/lib/native-chat-supported-agent' import { useAppStore } from '@/store' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' /** Seed the chat-composer copy of launch context that reaches only the TUI * input (argv prefill or startup paste). No-op for agents without a diff --git a/src/renderer/src/lib/agent-native-draft-prefill.ts b/src/renderer/src/lib/agent-native-draft-prefill.ts index 797b25e7827..750f68c39ca 100644 --- a/src/renderer/src/lib/agent-native-draft-prefill.ts +++ b/src/renderer/src/lib/agent-native-draft-prefill.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' // Why: agents with a native draft-prefill flag/env launch with the prompt diff --git a/src/renderer/src/lib/agent-paste-draft.ts b/src/renderer/src/lib/agent-paste-draft.ts index 4e53f4ac102..1275535b93f 100644 --- a/src/renderer/src/lib/agent-paste-draft.ts +++ b/src/renderer/src/lib/agent-paste-draft.ts @@ -1,4 +1,5 @@ -import type { TuiAgent, GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { TuiAgent } from '../../../shared/tui-agent' import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' import { useAppStore } from '@/store' import { diff --git a/src/renderer/src/lib/agent-session-continuation.ts b/src/renderer/src/lib/agent-session-continuation.ts index 2747f001cfd..3e910fd5d36 100644 --- a/src/renderer/src/lib/agent-session-continuation.ts +++ b/src/renderer/src/lib/agent-session-continuation.ts @@ -1,6 +1,6 @@ import { buildBoundedSessionTranscript } from '@/lib/agent-session-fork-context' import type { LaunchSource } from '../../../shared/telemetry-events' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' export type AgentSessionContinuationContextMode = 'focused' | 'full' diff --git a/src/renderer/src/lib/agent-status-count.test.ts b/src/renderer/src/lib/agent-status-count.test.ts index dac9294b3e3..1bc880af293 100644 --- a/src/renderer/src/lib/agent-status-count.test.ts +++ b/src/renderer/src/lib/agent-status-count.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { TerminalTab, Worktree } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { Worktree } from '../../../shared/worktree/types' import { countWorkingAgents, getWorkingAgentsPerWorktree } from './agent-status' function makeTab(overrides: Partial = {}): TerminalTab { diff --git a/src/renderer/src/lib/agent-status.ts b/src/renderer/src/lib/agent-status.ts index 11d966cb8bf..94933a05bff 100644 --- a/src/renderer/src/lib/agent-status.ts +++ b/src/renderer/src/lib/agent-status.ts @@ -1,4 +1,6 @@ -import type { TerminalTab, TuiAgent, Worktree } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' +import type { Worktree } from '../../../shared/worktree/types' import type { AgentStatusState, AgentType } from '../../../shared/agent-status-types' import { tabHasLivePty } from './tab-has-live-pty' import type { WorktreeStatus } from './worktree-status' diff --git a/src/renderer/src/lib/agent-tab-shortcuts.ts b/src/renderer/src/lib/agent-tab-shortcuts.ts index f3fac9912bd..55995d88064 100644 --- a/src/renderer/src/lib/agent-tab-shortcuts.ts +++ b/src/renderer/src/lib/agent-tab-shortcuts.ts @@ -5,7 +5,7 @@ import { } from '../../../shared/keybindings' import { ALL_TUI_AGENTS } from '../../../shared/tui-agent-display-names' import { normalizeDisabledTuiAgents, pickTuiAgent } from '../../../shared/tui-agent-selection' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' export type BoundAgentTabAction = { agent: TuiAgent diff --git a/src/renderer/src/lib/ai-vault-resume-target.ts b/src/renderer/src/lib/ai-vault-resume-target.ts index 2cab097ca3d..fffc0c8c4bd 100644 --- a/src/renderer/src/lib/ai-vault-resume-target.ts +++ b/src/renderer/src/lib/ai-vault-resume-target.ts @@ -6,7 +6,7 @@ import { toSshExecutionHostId, type ExecutionHostId } from '../../../shared/execution-host' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id' import { parseWorkspaceKey } from '../../../shared/workspace-scope' import { isWslUncPath } from '../../../shared/wsl-paths' diff --git a/src/renderer/src/lib/ai-vault-tab-title-requests.ts b/src/renderer/src/lib/ai-vault-tab-title-requests.ts index a5a6de5bdd9..1efeb48e001 100644 --- a/src/renderer/src/lib/ai-vault-tab-title-requests.ts +++ b/src/renderer/src/lib/ai-vault-tab-title-requests.ts @@ -4,7 +4,7 @@ import type { AiVaultSessionTitle } from '../../../shared/ai-vault-session-title import { isAiVaultTitleAgent } from '../../../shared/ai-vault-session-title' import type { ExecutionHostId } from '../../../shared/execution-host' import { parsePaneKey } from '../../../shared/stable-pane-id' -import type { TerminalTab } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner' import type { AppState } from '@/store/types' diff --git a/src/renderer/src/lib/ai-vault-tab-title-sync-inputs.ts b/src/renderer/src/lib/ai-vault-tab-title-sync-inputs.ts index 5693b57cdd3..569d79b0dc8 100644 --- a/src/renderer/src/lib/ai-vault-tab-title-sync-inputs.ts +++ b/src/renderer/src/lib/ai-vault-tab-title-sync-inputs.ts @@ -1,6 +1,6 @@ import type { AgentProviderSessionMetadata } from '../../../shared/agent-session-resume' import { isAiVaultTitleAgent } from '../../../shared/ai-vault-session-title' -import type { TerminalTab } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner' import type { AppState } from '@/store/types' import { collectAiVaultTitleRequests } from './ai-vault-tab-title-requests' diff --git a/src/renderer/src/lib/ai-vault-tab-title-sync.test.ts b/src/renderer/src/lib/ai-vault-tab-title-sync.test.ts index 26cecdd0014..a062b9df906 100644 --- a/src/renderer/src/lib/ai-vault-tab-title-sync.test.ts +++ b/src/renderer/src/lib/ai-vault-tab-title-sync.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import type { AiVaultSessionTitlesResult } from '../../../shared/ai-vault-session-title' import { resolveTerminalTabTitle } from '../../../shared/tab-title-resolution' -import type { TerminalTab } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' import { collectAiVaultTitleRequests, type AiVaultTitleRequest diff --git a/src/renderer/src/lib/automation-session-reuse.ts b/src/renderer/src/lib/automation-session-reuse.ts index 0d5c22049d6..0672a86872a 100644 --- a/src/renderer/src/lib/automation-session-reuse.ts +++ b/src/renderer/src/lib/automation-session-reuse.ts @@ -1,6 +1,6 @@ import type { AgentStatusEntry } from '../../../shared/agent-status-types' import type { AutomationRun } from '../../../shared/automations-types' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import { parsePaneKey } from '../../../shared/stable-pane-id' import type { AppState } from '@/store/types' diff --git a/src/renderer/src/lib/automation-terminal-ownership.ts b/src/renderer/src/lib/automation-terminal-ownership.ts index 1cf0c9a4742..9b6ec37d77b 100644 --- a/src/renderer/src/lib/automation-terminal-ownership.ts +++ b/src/renderer/src/lib/automation-terminal-ownership.ts @@ -1,6 +1,6 @@ import { useAppStore } from '@/store' import type { AppState } from '@/store/types' -import type { TerminalTab } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' import { parsePaneKey } from '../../../shared/stable-pane-id' import { singlePaneLayoutSnapshot } from '@/store/slices/terminal-helpers' import { readLastTerminalInputAt } from './terminal-input-activity-coalescing' diff --git a/src/renderer/src/lib/browser-cookie-import-toast.test.ts b/src/renderer/src/lib/browser-cookie-import-toast.test.ts index 40f2800102e..3479887b419 100644 --- a/src/renderer/src/lib/browser-cookie-import-toast.test.ts +++ b/src/renderer/src/lib/browser-cookie-import-toast.test.ts @@ -9,7 +9,7 @@ vi.mock('sonner', () => ({ toast: { success: successToastMock, warning: warningToastMock } })) -import type { BrowserCookieImportSummary } from '../../../shared/types' +import type { BrowserCookieImportSummary } from '../../../shared/browser-workspace-types' import { emitBrowserCookieImportToast } from './browser-cookie-import-toast' const summary: BrowserCookieImportSummary = { diff --git a/src/renderer/src/lib/browser-cookie-import-toast.ts b/src/renderer/src/lib/browser-cookie-import-toast.ts index ced4c3cfdbd..9e18e3c96c8 100644 --- a/src/renderer/src/lib/browser-cookie-import-toast.ts +++ b/src/renderer/src/lib/browser-cookie-import-toast.ts @@ -1,5 +1,5 @@ import { toast } from 'sonner' -import type { BrowserCookieImportSummary } from '../../../shared/types' +import type { BrowserCookieImportSummary } from '../../../shared/browser-workspace-types' import { translate } from '@/i18n/i18n' type CookieImportWarning = NonNullable diff --git a/src/renderer/src/lib/browser-page-palette-activation.test.ts b/src/renderer/src/lib/browser-page-palette-activation.test.ts index d2264698c02..97dbf66ef77 100644 --- a/src/renderer/src/lib/browser-page-palette-activation.test.ts +++ b/src/renderer/src/lib/browser-page-palette-activation.test.ts @@ -1,14 +1,10 @@ // @vitest-environment happy-dom import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { - BrowserPage, - BrowserWorkspace, - FolderWorkspace, - Tab, - TabGroup, - Worktree -} from '../../../shared/types' +import type { BrowserPage, BrowserWorkspace } from '../../../shared/browser-workspace-types' +import type { FolderWorkspace } from '../../../shared/folder-workspace-types' +import type { Tab, TabGroup } from '../../../shared/tab-types' +import type { Worktree } from '../../../shared/worktree/types' import { ORCA_BROWSER_BLANK_URL } from '../../../shared/constants' import { folderWorkspaceKey } from '../../../shared/workspace-scope' import { useAppStore } from '@/store' diff --git a/src/renderer/src/lib/browser-palette-page-entries.test.ts b/src/renderer/src/lib/browser-palette-page-entries.test.ts index 6fefc35b51b..f367ac85e73 100644 --- a/src/renderer/src/lib/browser-palette-page-entries.test.ts +++ b/src/renderer/src/lib/browser-palette-page-entries.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { BrowserPage, BrowserWorkspace, Worktree } from '../../../shared/types' +import type { BrowserPage, BrowserWorkspace } from '../../../shared/browser-workspace-types' +import type { Worktree } from '../../../shared/worktree/types' import { buildSearchableBrowserPages } from './browser-palette-page-entries' import { searchBrowserPages } from './browser-palette-search' diff --git a/src/renderer/src/lib/browser-palette-page-entries.ts b/src/renderer/src/lib/browser-palette-page-entries.ts index 05e30a0d806..226180597ef 100644 --- a/src/renderer/src/lib/browser-palette-page-entries.ts +++ b/src/renderer/src/lib/browser-palette-page-entries.ts @@ -1,4 +1,5 @@ -import type { BrowserPage, BrowserWorkspace, Worktree } from '../../../shared/types' +import type { BrowserPage, BrowserWorkspace } from '../../../shared/browser-workspace-types' +import type { Worktree } from '../../../shared/worktree/types' import type { SearchableBrowserPage } from './browser-palette-search' type BrowserPaletteActiveTabType = 'browser' | 'editor' | 'terminal' | 'simulator' diff --git a/src/renderer/src/lib/browser-palette-search.test.ts b/src/renderer/src/lib/browser-palette-search.test.ts index 23cf0196984..2eb66623abd 100644 --- a/src/renderer/src/lib/browser-palette-search.test.ts +++ b/src/renderer/src/lib/browser-palette-search.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { BrowserPage, BrowserWorkspace, Worktree } from '../../../shared/types' +import type { BrowserPage, BrowserWorkspace } from '../../../shared/browser-workspace-types' +import type { Worktree } from '../../../shared/worktree/types' import { BROWSER_PALETTE_QUERY_MAX_BYTES, searchBrowserPages, diff --git a/src/renderer/src/lib/browser-palette-search.ts b/src/renderer/src/lib/browser-palette-search.ts index 7c76e9f9ead..4bcb044fb5e 100644 --- a/src/renderer/src/lib/browser-palette-search.ts +++ b/src/renderer/src/lib/browser-palette-search.ts @@ -1,5 +1,6 @@ import { ORCA_BROWSER_BLANK_URL } from '../../../shared/constants' -import type { BrowserPage, BrowserWorkspace, Worktree } from '../../../shared/types' +import type { BrowserPage, BrowserWorkspace } from '../../../shared/browser-workspace-types' +import type { Worktree } from '../../../shared/worktree/types' import { isClipboardTextByteLengthOverLimit } from '../../../shared/clipboard-text' import { compareBaseSensitivityLocaleText } from './locale-text-collators' import { resolveWorktreeDisplayName } from './worktree-default-display-name' diff --git a/src/renderer/src/lib/cmd-j-github-url-lookup.test.ts b/src/renderer/src/lib/cmd-j-github-url-lookup.test.ts index 82b533c859d..790122e0d2f 100644 --- a/src/renderer/src/lib/cmd-j-github-url-lookup.test.ts +++ b/src/renderer/src/lib/cmd-j-github-url-lookup.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { GitHubWorkItem } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' const { lookupGitHubWorkItemByOwnerRepoForSource } = vi.hoisted(() => ({ lookupGitHubWorkItemByOwnerRepoForSource: vi.fn() diff --git a/src/renderer/src/lib/cmd-j-github-url-lookup.ts b/src/renderer/src/lib/cmd-j-github-url-lookup.ts index ba7fcc03d5a..260e74afa7c 100644 --- a/src/renderer/src/lib/cmd-j-github-url-lookup.ts +++ b/src/renderer/src/lib/cmd-j-github-url-lookup.ts @@ -1,5 +1,6 @@ import type { GitHubIssueOrPRLink } from '../../../shared/github/links' -import type { GitHubWorkItem, Repo } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import type { Repo } from '../../../shared/repo-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { lookupGitHubWorkItemByOwnerRepoForSource } from './github-work-item-source-lookup' diff --git a/src/renderer/src/lib/cmd-j-match-relevance.test.ts b/src/renderer/src/lib/cmd-j-match-relevance.test.ts index 07711ac97bc..ee4d2f74263 100644 --- a/src/renderer/src/lib/cmd-j-match-relevance.test.ts +++ b/src/renderer/src/lib/cmd-j-match-relevance.test.ts @@ -6,7 +6,7 @@ import { scorePaletteRelevance } from './cmd-j-match-relevance' import type { PaletteSearchResult } from './worktree-palette-search' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' function makeWorktree(overrides: Partial = {}): Worktree { return { diff --git a/src/renderer/src/lib/cmd-j-match-relevance.ts b/src/renderer/src/lib/cmd-j-match-relevance.ts index 44015d0d4f1..58e77d3fe9c 100644 --- a/src/renderer/src/lib/cmd-j-match-relevance.ts +++ b/src/renderer/src/lib/cmd-j-match-relevance.ts @@ -3,7 +3,7 @@ import { resolveWorktreeDisplayName } from './worktree-default-display-name' import type { MatchRange, PaletteSearchResult } from './worktree-palette-search' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' // Why: worktrees and open tabs are searched by separate scorers whose scales encode list position, // so neither can say which section holds the better hit. Relevance is the one shared scale: which diff --git a/src/renderer/src/lib/codex-pane-restart-eligibility.ts b/src/renderer/src/lib/codex-pane-restart-eligibility.ts index dc9f63b6f7b..f986408c5dd 100644 --- a/src/renderer/src/lib/codex-pane-restart-eligibility.ts +++ b/src/renderer/src/lib/codex-pane-restart-eligibility.ts @@ -3,7 +3,7 @@ import { recognizeAgentProcess } from '../../../shared/agent-process-recognition' import { isShellProcess } from '../../../shared/shell-process-detection' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import type { RuntimeTerminalProcessInspection } from '@/runtime/runtime-terminal-inspection' function normalizeProcessName(processName: string | null): string | null { diff --git a/src/renderer/src/lib/codex-pane-selection-lane.ts b/src/renderer/src/lib/codex-pane-selection-lane.ts index e6c8a6877da..56ea2108074 100644 --- a/src/renderer/src/lib/codex-pane-selection-lane.ts +++ b/src/renderer/src/lib/codex-pane-selection-lane.ts @@ -14,7 +14,8 @@ import { } from '../../../shared/local-windows-terminal-runtime' import { parseAppSshPtyId } from '../../../shared/ssh-pty-id' import { resolveTerminalStartupCwd } from '../../../shared/terminal-startup-cwd' -import type { GlobalSettings, TerminalTab } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' import { parseWorkspaceKey } from '../../../shared/workspace-scope' import { parseWslUncPath } from '../../../shared/wsl-paths' import { getLocalProjectExecutionRuntimeContext } from './local-preflight-context' diff --git a/src/renderer/src/lib/codex-session-restart.test.ts b/src/renderer/src/lib/codex-session-restart.test.ts index 0f659aa2521..93bde5ed353 100644 --- a/src/renderer/src/lib/codex-session-restart.test.ts +++ b/src/renderer/src/lib/codex-session-restart.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAppStore } from '@/store' import { shouldUseShellReadyStartupDelivery } from '../../../shared/codex-startup-delivery' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import { CODEX_ACCOUNT_RESTART_STARTUP, markLiveCodexSessionsForRestart, diff --git a/src/renderer/src/lib/codex-session-restart.ts b/src/renderer/src/lib/codex-session-restart.ts index 4b3ed2e43b2..01d6a5fab00 100644 --- a/src/renderer/src/lib/codex-session-restart.ts +++ b/src/renderer/src/lib/codex-session-restart.ts @@ -18,7 +18,7 @@ import { resolveCodexPaneSelectionLane } from './codex-pane-selection-lane' import type { CodexAccountSelectionTarget } from '../../../shared/codex-selection-lane' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' // Why: prompt integrations such as Starship can outlast the daemon's 300ms // Codex fast-path timeout; account restarts must wait until the shell accepts input. diff --git a/src/renderer/src/lib/composer-issue-command.ts b/src/renderer/src/lib/composer-issue-command.ts index ddb841bb7ea..3ac90dcda7b 100644 --- a/src/renderer/src/lib/composer-issue-command.ts +++ b/src/renderer/src/lib/composer-issue-command.ts @@ -3,7 +3,7 @@ import { canUseIssueCommandForLinkedItemProvider, renderIssueCommandTemplate } from '@/lib/new-workspace' -import type { FolderWorkspaceLinkedTask } from '../../../shared/types' +import type { FolderWorkspaceLinkedTask } from '../../../shared/folder-workspace-types' type ComposerIssueCommandInput = { enabled: boolean diff --git a/src/renderer/src/lib/connection-context.test.ts b/src/renderer/src/lib/connection-context.test.ts index 27bfbe3a77a..2ef70ba95e2 100644 --- a/src/renderer/src/lib/connection-context.test.ts +++ b/src/renderer/src/lib/connection-context.test.ts @@ -1,5 +1,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import type { FolderWorkspace, ProjectGroup, Repo, Worktree } from '../../../shared/types' +import type { FolderWorkspace } from '../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../shared/project-group-types' +import type { Repo } from '../../../shared/repo-types' +import type { Worktree } from '../../../shared/worktree/types' import { useAppStore } from '@/store' import type { AppState } from '@/store/types' import { diff --git a/src/renderer/src/lib/create-untitled-markdown.ts b/src/renderer/src/lib/create-untitled-markdown.ts index fc9ff482f06..01c4b7f9a28 100644 --- a/src/renderer/src/lib/create-untitled-markdown.ts +++ b/src/renderer/src/lib/create-untitled-markdown.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import { createRuntimePath, deleteRuntimePath, diff --git a/src/renderer/src/lib/diff-comment-compat.test.ts b/src/renderer/src/lib/diff-comment-compat.test.ts index 48b6e3b22b3..7ab54e94b83 100644 --- a/src/renderer/src/lib/diff-comment-compat.test.ts +++ b/src/renderer/src/lib/diff-comment-compat.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { DiffComment } from '../../../shared/types' +import type { DiffComment } from '../../../shared/diff-comment-types' import { getDiffCommentLineLabel, getDiffCommentSource, diff --git a/src/renderer/src/lib/diff-comment-compat.ts b/src/renderer/src/lib/diff-comment-compat.ts index ed88baf0dd0..2f1ab95ed2a 100644 --- a/src/renderer/src/lib/diff-comment-compat.ts +++ b/src/renderer/src/lib/diff-comment-compat.ts @@ -1,4 +1,4 @@ -import type { DiffComment, DiffCommentSource } from '../../../shared/types' +import type { DiffComment, DiffCommentSource } from '../../../shared/diff-comment-types' export function getDiffCommentSource(comment: Pick): DiffCommentSource { return comment.source === 'markdown' ? 'markdown' : 'diff' diff --git a/src/renderer/src/lib/diff-comments-format.test.ts b/src/renderer/src/lib/diff-comments-format.test.ts index cc3e0256c6b..eaf840f755f 100644 --- a/src/renderer/src/lib/diff-comments-format.test.ts +++ b/src/renderer/src/lib/diff-comments-format.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import type { DiffComment } from '../../../shared/types' +import type { DiffComment } from '../../../shared/diff-comment-types' import { formatDiffComment, formatDiffComments } from './diff-comments-format' function makeComment(overrides: Partial = {}): DiffComment { diff --git a/src/renderer/src/lib/direct-ssh-target-scope-types.ts b/src/renderer/src/lib/direct-ssh-target-scope-types.ts index 1c12e0a25c5..068e723bb9d 100644 --- a/src/renderer/src/lib/direct-ssh-target-scope-types.ts +++ b/src/renderer/src/lib/direct-ssh-target-scope-types.ts @@ -1,10 +1,7 @@ -import type { - FolderWorkspace, - ProjectGroup, - Repo, - WorkspaceKey, - Worktree -} from '../../../shared/types' +import type { FolderWorkspace, WorkspaceKey } from '../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../shared/project-group-types' +import type { Repo } from '../../../shared/repo-types' +import type { Worktree } from '../../../shared/worktree/types' export type DirectSshGitRepoRef = { repoId: string diff --git a/src/renderer/src/lib/document-theme.ts b/src/renderer/src/lib/document-theme.ts index 1880a6e0af0..dc0cbc4eb87 100644 --- a/src/renderer/src/lib/document-theme.ts +++ b/src/renderer/src/lib/document-theme.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' export type DocumentThemePreference = GlobalSettings['theme'] diff --git a/src/renderer/src/lib/duplicate-browser-tab-options.ts b/src/renderer/src/lib/duplicate-browser-tab-options.ts index e2a41c80df8..6b24b2b0417 100644 --- a/src/renderer/src/lib/duplicate-browser-tab-options.ts +++ b/src/renderer/src/lib/duplicate-browser-tab-options.ts @@ -1,4 +1,4 @@ -import type { BrowserTab } from '../../../shared/types' +import type { BrowserTab } from '../../../shared/browser-workspace-types' export function buildDuplicatedBrowserTabOptions( source: Pick diff --git a/src/renderer/src/lib/emulator-right-split-target.ts b/src/renderer/src/lib/emulator-right-split-target.ts index 5d19b0d4e9a..f173a2e155f 100644 --- a/src/renderer/src/lib/emulator-right-split-target.ts +++ b/src/renderer/src/lib/emulator-right-split-target.ts @@ -1,4 +1,4 @@ -import type { TabGroupLayoutNode } from '../../../shared/types' +import type { TabGroupLayoutNode } from '../../../shared/tab-types' type SearchResult = { containsSource: boolean diff --git a/src/renderer/src/lib/ensure-hooks-confirmed.test.ts b/src/renderer/src/lib/ensure-hooks-confirmed.test.ts index bbfbe637cb7..b21f0f39c4c 100644 --- a/src/renderer/src/lib/ensure-hooks-confirmed.test.ts +++ b/src/renderer/src/lib/ensure-hooks-confirmed.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { AppState } from '@/store/types' -import type { PersistedTrustedOrcaHooks } from '../../../shared/types' +import type { PersistedTrustedOrcaHooks } from '../../../shared/orca-yaml-hook-types' import { __resetTrustPromptChainForTests, ensureHooksConfirmed, diff --git a/src/renderer/src/lib/ensure-hooks-confirmed.ts b/src/renderer/src/lib/ensure-hooks-confirmed.ts index c98638deddf..8128e9ae8a0 100644 --- a/src/renderer/src/lib/ensure-hooks-confirmed.ts +++ b/src/renderer/src/lib/ensure-hooks-confirmed.ts @@ -1,5 +1,5 @@ import type { AppState } from '@/store/types' -import type { OrcaHooks } from '../../../shared/types' +import type { OrcaHooks } from '../../../shared/orca-yaml-hook-types' import { resolveHookCommandSourcePolicy } from '../../../shared/hook-command-source-policy' import { hashOrcaHookScript, type OrcaHookScriptKind } from './orca-hook-trust' import { diff --git a/src/renderer/src/lib/ephemeral-vm-workspace-target.test.ts b/src/renderer/src/lib/ephemeral-vm-workspace-target.test.ts index 2a3ab2fefb8..da25a8669eb 100644 --- a/src/renderer/src/lib/ephemeral-vm-workspace-target.test.ts +++ b/src/renderer/src/lib/ephemeral-vm-workspace-target.test.ts @@ -3,7 +3,7 @@ import { prepareEphemeralVmWorkspaceTarget, type PrepareEphemeralVmWorkspaceTargetArgs } from './ephemeral-vm-workspace-target' -import type { ProjectHostSetupResult } from '../../../shared/types' +import type { ProjectHostSetupResult } from '../../../shared/project-types' vi.mock('@/runtime/runtime-rpc-client', () => ({ assertRuntimeEnvironmentCapability: vi.fn() diff --git a/src/renderer/src/lib/ephemeral-vm-workspace-target.ts b/src/renderer/src/lib/ephemeral-vm-workspace-target.ts index fc588d667be..eab9af1ccf4 100644 --- a/src/renderer/src/lib/ephemeral-vm-workspace-target.ts +++ b/src/renderer/src/lib/ephemeral-vm-workspace-target.ts @@ -2,7 +2,7 @@ import { toRuntimeExecutionHostId, toSshExecutionHostId } from '../../../shared/ import type { ProjectHostSetupExistingFolderArgs, ProjectHostSetupResult -} from '../../../shared/types' +} from '../../../shared/project-types' import { getEphemeralVmRecipeResultCheckoutMode, getEphemeralVmRecipeResultProjectRoot diff --git a/src/renderer/src/lib/ephemeral-vm-worktree-creation.ts b/src/renderer/src/lib/ephemeral-vm-worktree-creation.ts index b8f0bd9ba96..8c32bb275bd 100644 --- a/src/renderer/src/lib/ephemeral-vm-worktree-creation.ts +++ b/src/renderer/src/lib/ephemeral-vm-worktree-creation.ts @@ -3,7 +3,7 @@ import { useAppStore } from '@/store' import { prepareEphemeralVmWorkspaceTarget } from '@/lib/ephemeral-vm-workspace-target' import type { WorktreeCreationRequest } from '@/lib/pending-worktree-creation' import { getProjectIdentityKey } from '../../../shared/project-host-setup-projection' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import { translate } from '@/i18n/i18n' import { cleanupFailedEphemeralVmWorkspace } from '@/lib/ephemeral-vm-failed-create-cleanup' diff --git a/src/renderer/src/lib/external-editor-open-capability.ts b/src/renderer/src/lib/external-editor-open-capability.ts index bab7547b9b8..459c8c0061b 100644 --- a/src/renderer/src/lib/external-editor-open-capability.ts +++ b/src/renderer/src/lib/external-editor-open-capability.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import { isVsCodeRemoteSshCommand } from '../../../shared/vscode-remote-ssh-launcher' export type ExternalEditorOpenCapability = diff --git a/src/renderer/src/lib/file-search-result-owner.ts b/src/renderer/src/lib/file-search-result-owner.ts index b2c719f245a..d352ad34246 100644 --- a/src/renderer/src/lib/file-search-result-owner.ts +++ b/src/renderer/src/lib/file-search-result-owner.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' export type FileSearchResultOwner = { worktreeId: string diff --git a/src/renderer/src/lib/fix-checks-agent-launch.ts b/src/renderer/src/lib/fix-checks-agent-launch.ts index 49ad2ccb856..5c6cd7ffca1 100644 --- a/src/renderer/src/lib/fix-checks-agent-launch.ts +++ b/src/renderer/src/lib/fix-checks-agent-launch.ts @@ -20,11 +20,9 @@ import { renderSourceControlActionCommandTemplate } from '../../../shared/source-control-ai-actions' import { isTuiAgentEnabled } from '../../../shared/tui-agent-selection' -import type { - GitHubWorkItem, - TuiAgent, - WorkspaceCreateTelemetrySource -} from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import type { TuiAgent } from '../../../shared/tui-agent' +import type { WorkspaceSource as WorkspaceCreateTelemetrySource } from '../../../shared/workspace-source' import type { LaunchSource } from '../../../shared/telemetry-events' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/lib/floating-workspace-tab-creation.ts b/src/renderer/src/lib/floating-workspace-tab-creation.ts index 58a22ad9320..a443c572106 100644 --- a/src/renderer/src/lib/floating-workspace-tab-creation.ts +++ b/src/renderer/src/lib/floating-workspace-tab-creation.ts @@ -1,5 +1,6 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' -import type { BrowserTab, TerminalTab } from '../../../shared/types' +import type { BrowserTab } from '../../../shared/browser-workspace-types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' import { createUntitledMarkdownFileWithTemplateSelection } from './create-untitled-markdown' import { getConnectionId } from './connection-context' import { detectLanguage } from './language-detect' diff --git a/src/renderer/src/lib/floating-workspace-terminal-actions.test.ts b/src/renderer/src/lib/floating-workspace-terminal-actions.test.ts index 2c556a4c32e..7f2a79ed9a7 100644 --- a/src/renderer/src/lib/floating-workspace-terminal-actions.test.ts +++ b/src/renderer/src/lib/floating-workspace-terminal-actions.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' -import type { Tab, TerminalTab } from '../../../shared/types' +import type { Tab } from '../../../shared/tab-types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' import { createFloatingWorkspaceBrowserTab, createFloatingWorkspaceMarkdownTab, diff --git a/src/renderer/src/lib/floating-workspace-terminal-actions.ts b/src/renderer/src/lib/floating-workspace-terminal-actions.ts index d1133e7fcfa..484143160d9 100644 --- a/src/renderer/src/lib/floating-workspace-terminal-actions.ts +++ b/src/renderer/src/lib/floating-workspace-terminal-actions.ts @@ -1,5 +1,6 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' -import type { BrowserTab, TabGroup } from '../../../shared/types' +import type { BrowserTab } from '../../../shared/browser-workspace-types' +import type { TabGroup } from '../../../shared/tab-types' import { getGroupVisibleTabOrder } from '@/components/tab-bar/group-tab-order' import { getNextTabAcrossAllTypes, diff --git a/src/renderer/src/lib/folder-workspace-connection.ts b/src/renderer/src/lib/folder-workspace-connection.ts index 2c9376fc284..7edfbeedf32 100644 --- a/src/renderer/src/lib/folder-workspace-connection.ts +++ b/src/renderer/src/lib/folder-workspace-connection.ts @@ -1,4 +1,6 @@ -import type { FolderWorkspace, ProjectGroup, Repo } from '../../../shared/types' +import type { FolderWorkspace } from '../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../shared/project-group-types' +import type { Repo } from '../../../shared/repo-types' import { isPathInsideOrEqual } from '../../../shared/cross-platform-path' import { getProjectGroupSubtreeIds } from '../../../shared/project-groups' import { parseExecutionHostId } from '../../../shared/execution-host' diff --git a/src/renderer/src/lib/folder-workspace-runtime-owner.ts b/src/renderer/src/lib/folder-workspace-runtime-owner.ts index fce08c49302..b9b0266ff07 100644 --- a/src/renderer/src/lib/folder-workspace-runtime-owner.ts +++ b/src/renderer/src/lib/folder-workspace-runtime-owner.ts @@ -1,6 +1,7 @@ import { parseExecutionHostId, toSshExecutionHostId } from '../../../shared/execution-host' import type { ExecutionHostId, ParsedExecutionHost } from '../../../shared/execution-host' -import type { FolderWorkspace, ProjectGroup } from '../../../shared/types' +import type { FolderWorkspace } from '../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../shared/project-group-types' import { folderWorkspaceKey } from '../../../shared/workspace-scope' import { findIndexedFolderWorkspaceOwner, diff --git a/src/renderer/src/lib/github-pr-start-point.ts b/src/renderer/src/lib/github-pr-start-point.ts index a0ded7900b8..3984bf32cf3 100644 --- a/src/renderer/src/lib/github-pr-start-point.ts +++ b/src/renderer/src/lib/github-pr-start-point.ts @@ -1,5 +1,6 @@ import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' -import type { GitHubPrStartPoint, GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { GitHubPrStartPoint } from '../../../shared/worktree/types' type PrStartPointSettings = Pick | null | undefined diff --git a/src/renderer/src/lib/github-source-runtime-context.ts b/src/renderer/src/lib/github-source-runtime-context.ts index c36598e3420..0267497f0b7 100644 --- a/src/renderer/src/lib/github-source-runtime-context.ts +++ b/src/renderer/src/lib/github-source-runtime-context.ts @@ -2,7 +2,7 @@ import type { ParsedExecutionHost } from '../../../shared/execution-host' import { parseExecutionHostId } from '../../../shared/execution-host' import type { TaskSourceContext } from '../../../shared/task-source-context' import { getTaskSourceRuntimeSettings } from '../../../shared/task-source-context' -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import type { RuntimeClientTarget } from '@/runtime/runtime-rpc-client' import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' import { diff --git a/src/renderer/src/lib/github-work-item-source-lookup.ts b/src/renderer/src/lib/github-work-item-source-lookup.ts index 046c4af0790..5a49e15ba83 100644 --- a/src/renderer/src/lib/github-work-item-source-lookup.ts +++ b/src/renderer/src/lib/github-work-item-source-lookup.ts @@ -1,4 +1,4 @@ -import type { GitHubWorkItem, GitHubWorkItemDetails } from '../../../shared/types' +import type { GitHubWorkItem, GitHubWorkItemDetails } from '../../../shared/github/work-item-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { callRuntimeRpc } from '@/runtime/runtime-rpc-client' import { diff --git a/src/renderer/src/lib/github-work-item-workspace-attachment.test.ts b/src/renderer/src/lib/github-work-item-workspace-attachment.test.ts index f0cae1ba204..f52808dc8e0 100644 --- a/src/renderer/src/lib/github-work-item-workspace-attachment.test.ts +++ b/src/renderer/src/lib/github-work-item-workspace-attachment.test.ts @@ -7,7 +7,7 @@ import { getGithubWorkItemWorkspaceAttachmentLabel, getGithubPrWorkspaceAttachmentLabel } from './github-work-item-workspace-attachment' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' function worktree(overrides: Partial = {}): Worktree { return { diff --git a/src/renderer/src/lib/github-work-item-workspace-attachment.ts b/src/renderer/src/lib/github-work-item-workspace-attachment.ts index 7a31c32a743..283260913d8 100644 --- a/src/renderer/src/lib/github-work-item-workspace-attachment.ts +++ b/src/renderer/src/lib/github-work-item-workspace-attachment.ts @@ -1,4 +1,5 @@ -import type { GitHubWorkItem, Worktree } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import type { Worktree } from '../../../shared/worktree/types' import { getWorktreeAttachmentLabel } from './worktree-attachment-label' type GitHubWorkItemType = GitHubWorkItem['type'] diff --git a/src/renderer/src/lib/gitlab-work-item-source-lookup.test.ts b/src/renderer/src/lib/gitlab-work-item-source-lookup.test.ts index 82d9b82628f..88c37cf128c 100644 --- a/src/renderer/src/lib/gitlab-work-item-source-lookup.test.ts +++ b/src/renderer/src/lib/gitlab-work-item-source-lookup.test.ts @@ -5,7 +5,7 @@ import { listGitLabMRsForSource, lookupGitLabWorkItemByPathForSource } from './gitlab-work-item-source-lookup' -import type { GitLabWorkItem, ListMergeRequestsResult } from '../../../shared/types' +import type { GitLabWorkItem, ListMergeRequestsResult } from '../../../shared/gitlab-types' import type { TaskSourceContext } from '../../../shared/task-source-context' vi.mock('@/runtime/runtime-rpc-client', async () => { diff --git a/src/renderer/src/lib/gitlab-work-item-source-lookup.ts b/src/renderer/src/lib/gitlab-work-item-source-lookup.ts index 911997a82f9..c848a84b771 100644 --- a/src/renderer/src/lib/gitlab-work-item-source-lookup.ts +++ b/src/renderer/src/lib/gitlab-work-item-source-lookup.ts @@ -1,4 +1,4 @@ -import type { GitLabWorkItem, ListMergeRequestsResult } from '../../../shared/types' +import type { GitLabWorkItem, ListMergeRequestsResult } from '../../../shared/gitlab-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { getTaskSourceRuntimeSettings } from '../../../shared/task-source-context' import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' diff --git a/src/renderer/src/lib/http-link-routing.ts b/src/renderer/src/lib/http-link-routing.ts index 3295183d504..0699a7f6300 100644 --- a/src/renderer/src/lib/http-link-routing.ts +++ b/src/renderer/src/lib/http-link-routing.ts @@ -4,7 +4,7 @@ import { parseLoopbackUrlWithPort, type LocalhostWorktreeLabelRoute } from '../../../shared/localhost-worktree-labels' -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import type { WorkspacePort, WorkspacePortScanResult } from '../../../shared/workspace-ports' import { toast } from 'sonner' diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.ts b/src/renderer/src/lib/launch-agent-in-new-tab.ts index 58e36fd4a33..d46cb530bd6 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.ts @@ -24,7 +24,7 @@ import { resolveLocalWindowsAgentStartupShell } from '../../../shared/windows-te import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' import { repoIsRemote } from '../../../shared/agent-launch-remote' import { seedCommandCodeSubmittedPromptStatus } from '@/lib/command-code-prompt-status-seed' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import type { LaunchSource } from '../../../shared/telemetry-events' import { getConnectionIdFromState } from '@/lib/connection-context' import { resolveInitialNativeChatSessionOptions } from '@/components/native-chat/native-chat-launch-session-options' diff --git a/src/renderer/src/lib/launch-agent-paste-timeout-notice.ts b/src/renderer/src/lib/launch-agent-paste-timeout-notice.ts index a99371f9647..f72f5b8b3b7 100644 --- a/src/renderer/src/lib/launch-agent-paste-timeout-notice.ts +++ b/src/renderer/src/lib/launch-agent-paste-timeout-notice.ts @@ -2,7 +2,7 @@ import { toast } from 'sonner' import { useAppStore } from '@/store' import { track, tuiAgentToAgentKind } from '@/lib/telemetry' import { translate } from '@/i18n/i18n' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' /** * Notice for a post-launch paste that never landed — a stalled readiness wait diff --git a/src/renderer/src/lib/launch-agent-session-continuation.ts b/src/renderer/src/lib/launch-agent-session-continuation.ts index 9b73a3852ee..a1eb70dc0d5 100644 --- a/src/renderer/src/lib/launch-agent-session-continuation.ts +++ b/src/renderer/src/lib/launch-agent-session-continuation.ts @@ -7,7 +7,7 @@ import { useAppStore } from '@/store' import { isTuiAgentEnabled } from '../../../shared/tui-agent-selection' import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' import type { LaunchSource } from '../../../shared/telemetry-events' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import { translate } from '@/i18n/i18n' type LaunchAgentSessionContinuationArgs = { diff --git a/src/renderer/src/lib/launch-agent-web-host-tab.ts b/src/renderer/src/lib/launch-agent-web-host-tab.ts index eb5421bdb86..528798d424d 100644 --- a/src/renderer/src/lib/launch-agent-web-host-tab.ts +++ b/src/renderer/src/lib/launch-agent-web-host-tab.ts @@ -7,7 +7,8 @@ import { isWebTerminalSurfaceTabId } from '@/runtime/web-runtime-session' import type { AgentStartupPlan } from '@/lib/tui-agent-startup' -import type { Tab, TuiAgent } from '../../../shared/types' +import type { Tab } from '../../../shared/tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' import type { AgentPromptDelivery } from '../../../shared/agent-session-host-authority' import { translate } from '@/i18n/i18n' import { toAgentLaunchPreferences } from '@/runtime/agent-session-create-operation' diff --git a/src/renderer/src/lib/launch-work-item-direct-agent.ts b/src/renderer/src/lib/launch-work-item-direct-agent.ts index 467ef5065b6..6073ef5e180 100644 --- a/src/renderer/src/lib/launch-work-item-direct-agent.ts +++ b/src/renderer/src/lib/launch-work-item-direct-agent.ts @@ -10,7 +10,7 @@ import type { AgentStartedTelemetry } from '@/lib/worktree-activation' import type { SleepingAgentLaunchConfig } from '../../../shared/agent-session-resume' import type { LaunchSource } from '../../../shared/telemetry-events' import type { StartupCommandDelivery } from '../../../shared/codex-startup-delivery' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import { resolveTuiAgentLaunchArgs, resolveTuiAgentLaunchEnv diff --git a/src/renderer/src/lib/launch-work-item-direct-preflight.ts b/src/renderer/src/lib/launch-work-item-direct-preflight.ts index 3a09dfc67c1..84cc3033f09 100644 --- a/src/renderer/src/lib/launch-work-item-direct-preflight.ts +++ b/src/renderer/src/lib/launch-work-item-direct-preflight.ts @@ -1,13 +1,10 @@ import { getSetupConfig } from '@/lib/new-workspace' import { checkRuntimeHooks } from '@/runtime/runtime-hooks-client' import { resolveGitHubPrStartPointForRepo } from '@/lib/github-pr-start-point' -import type { - GitHubPrStartPoint, - GlobalSettings, - OrcaHooks, - RepoHookSettings, - SetupDecision -} from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { OrcaHooks, RepoHookSettings } from '../../../shared/orca-yaml-hook-types' +import type { SetupDecision } from '../../../shared/worktree/create-types' +import type { GitHubPrStartPoint } from '../../../shared/worktree/types' // Why: preflight routes by the repo's owner host, which `getSettingsForRepoRuntimeOwner` // hands back as a narrow runtime-scope pick rather than the full GlobalSettings. diff --git a/src/renderer/src/lib/launch-work-item-direct-types.ts b/src/renderer/src/lib/launch-work-item-direct-types.ts index e94b543d687..4d68b4d36c3 100644 --- a/src/renderer/src/lib/launch-work-item-direct-types.ts +++ b/src/renderer/src/lib/launch-work-item-direct-types.ts @@ -1,5 +1,7 @@ import type { LinkedWorkItemContext } from '@/lib/linked-work-item-context' -import type { TaskProvider, TuiAgent, WorkspaceCreateTelemetrySource } from '../../../shared/types' +import type { TaskProvider } from '../../../shared/task-providers' +import type { TuiAgent } from '../../../shared/tui-agent' +import type { WorkspaceSource as WorkspaceCreateTelemetrySource } from '../../../shared/workspace-source' import type { LaunchSource } from '../../../shared/telemetry-events' export type LaunchableWorkItem = { diff --git a/src/renderer/src/lib/launch-work-item-direct.ts b/src/renderer/src/lib/launch-work-item-direct.ts index 6a0fa63a214..d27220bf089 100644 --- a/src/renderer/src/lib/launch-work-item-direct.ts +++ b/src/renderer/src/lib/launch-work-item-direct.ts @@ -16,7 +16,9 @@ import { ensureHooksConfirmed } from '@/lib/ensure-hooks-confirmed' import { seedNativeChatLaunchDraftForAgentTab } from '@/lib/agent-launch-prompt-delivery' import { getConnectionId } from '@/lib/connection-context' import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability' -import type { GitPushTarget, SetupDecision, TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' +import type { SetupDecision } from '../../../shared/worktree/create-types' +import type { GitPushTarget } from '../../../shared/worktree/types' import { getLinearIssueWorkspaceName } from '../../../shared/workspace-name' import { resolveGitHubWorkItemIdentity } from '@/lib/github-work-item-identity' import { diff --git a/src/renderer/src/lib/launch-worktree-background-terminals.ts b/src/renderer/src/lib/launch-worktree-background-terminals.ts index 5725ce397a8..049176c1e81 100644 --- a/src/renderer/src/lib/launch-worktree-background-terminals.ts +++ b/src/renderer/src/lib/launch-worktree-background-terminals.ts @@ -14,12 +14,12 @@ import { buildSetupRunnerCommand, getSetupRunnerCommandPlatformForPath } from '../../../shared/setup-runner-command' +import type { TerminalLayoutSnapshot } from '../../../shared/terminal-tab-types' import type { - TerminalLayoutSnapshot, - Worktree, WorktreeDefaultTabsLaunch, WorktreeSetupLaunch -} from '../../../shared/types' +} from '../../../shared/worktree/launch-types' +import type { Worktree } from '../../../shared/worktree/types' type BackgroundPane = { leafId: string diff --git a/src/renderer/src/lib/left-sidebar-appearance.ts b/src/renderer/src/lib/left-sidebar-appearance.ts index 9966818c36a..4760aac8565 100644 --- a/src/renderer/src/lib/left-sidebar-appearance.ts +++ b/src/renderer/src/lib/left-sidebar-appearance.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import { HEX_COLOR_RE } from '../../../shared/color-validation' import { normalizeLeftSidebarTintColor, diff --git a/src/renderer/src/lib/linear-issue-context-snapshot.test.ts b/src/renderer/src/lib/linear-issue-context-snapshot.test.ts index 5ad8ca68e9b..d8206a7aa9f 100644 --- a/src/renderer/src/lib/linear-issue-context-snapshot.test.ts +++ b/src/renderer/src/lib/linear-issue-context-snapshot.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import type { LinearComment, LinearIssue } from '../../../shared/types' +import type { LinearComment, LinearIssue } from '../../../shared/linear/issue-types' import { buildLinearIssueContextSnapshot, LINEAR_ISSUE_CONTEXT_CAPS diff --git a/src/renderer/src/lib/linear-issue-context-snapshot.ts b/src/renderer/src/lib/linear-issue-context-snapshot.ts index 0cdec67ec1c..37e77c56b60 100644 --- a/src/renderer/src/lib/linear-issue-context-snapshot.ts +++ b/src/renderer/src/lib/linear-issue-context-snapshot.ts @@ -1,4 +1,4 @@ -import type { LinearComment, LinearIssue } from '../../../shared/types' +import type { LinearComment, LinearIssue } from '../../../shared/linear/issue-types' export const LINEAR_ISSUE_CONTEXT_CAPS = { descriptionChars: 3000, diff --git a/src/renderer/src/lib/linear-issue-url-lookup.test.ts b/src/renderer/src/lib/linear-issue-url-lookup.test.ts index 28bc918caed..5cae0b9ae6b 100644 --- a/src/renderer/src/lib/linear-issue-url-lookup.test.ts +++ b/src/renderer/src/lib/linear-issue-url-lookup.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest' -import type { LinearConnectionStatus, LinearIssue } from '../../../shared/types' +import type { LinearIssue } from '../../../shared/linear/issue-types' +import type { LinearConnectionStatus } from '../../../shared/linear/workspace-types' import { lookupLinearIssueUrl } from './linear-issue-url-lookup' const intent = { identifier: 'STA-4084', organizationUrlKey: 'stably' } diff --git a/src/renderer/src/lib/linear-issue-url-lookup.ts b/src/renderer/src/lib/linear-issue-url-lookup.ts index 55493d628f7..e2ea3ed9aef 100644 --- a/src/renderer/src/lib/linear-issue-url-lookup.ts +++ b/src/renderer/src/lib/linear-issue-url-lookup.ts @@ -4,7 +4,8 @@ import { isLinearIssueUrlResolutionMatch, type LinearIssueUrlIntent } from '../../../shared/linear/links' -import type { LinearConnectionStatus, LinearIssue } from '../../../shared/types' +import type { LinearIssue } from '../../../shared/linear/issue-types' +import type { LinearConnectionStatus } from '../../../shared/linear/workspace-types' import type { TaskSourceContext } from '../../../shared/task-source-context' type FetchLinearIssue = ( diff --git a/src/renderer/src/lib/linear-issue-workspace-attachment.test.ts b/src/renderer/src/lib/linear-issue-workspace-attachment.test.ts index 94455731201..9e44943421d 100644 --- a/src/renderer/src/lib/linear-issue-workspace-attachment.test.ts +++ b/src/renderer/src/lib/linear-issue-workspace-attachment.test.ts @@ -6,7 +6,7 @@ import { findLinearIssueWorkspaceAttachmentInIndex, getLinearIssueWorkspaceAttachmentLabel } from './linear-issue-workspace-attachment' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' function worktree(overrides: Partial = {}): Worktree { return { diff --git a/src/renderer/src/lib/linear-issue-workspace-attachment.ts b/src/renderer/src/lib/linear-issue-workspace-attachment.ts index e1904dd02d4..e931b619aab 100644 --- a/src/renderer/src/lib/linear-issue-workspace-attachment.ts +++ b/src/renderer/src/lib/linear-issue-workspace-attachment.ts @@ -1,4 +1,5 @@ -import type { LinearIssue, Worktree } from '../../../shared/types' +import type { LinearIssue } from '../../../shared/linear/issue-types' +import type { Worktree } from '../../../shared/worktree/types' import { getLinearOrganizationUrlKeyFromIssueUrl, parseLinearIssueInput diff --git a/src/renderer/src/lib/linear-issue-workspace-open.test.ts b/src/renderer/src/lib/linear-issue-workspace-open.test.ts index 0d6dd0211bc..86a72ab4df9 100644 --- a/src/renderer/src/lib/linear-issue-workspace-open.test.ts +++ b/src/renderer/src/lib/linear-issue-workspace-open.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { FolderWorkspace, Worktree } from '../../../shared/types' +import type { FolderWorkspace } from '../../../shared/folder-workspace-types' +import type { Worktree } from '../../../shared/worktree/types' const mocks = vi.hoisted(() => ({ activateAndRevealFolderWorkspace: vi.fn(), diff --git a/src/renderer/src/lib/linear-linked-work-item.test.ts b/src/renderer/src/lib/linear-linked-work-item.test.ts index 10c11a9dde1..b8aa6f8fd0a 100644 --- a/src/renderer/src/lib/linear-linked-work-item.test.ts +++ b/src/renderer/src/lib/linear-linked-work-item.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { LinearIssue } from '../../../shared/types' +import type { LinearIssue } from '../../../shared/linear/issue-types' import { buildLinearIssueLinkedWorkItem, getLinearLinkedWorkItemBranchName, diff --git a/src/renderer/src/lib/linear-linked-work-item.ts b/src/renderer/src/lib/linear-linked-work-item.ts index d1219bad8dc..098fed173f4 100644 --- a/src/renderer/src/lib/linear-linked-work-item.ts +++ b/src/renderer/src/lib/linear-linked-work-item.ts @@ -1,4 +1,4 @@ -import type { LinearIssue } from '../../../shared/types' +import type { LinearIssue } from '../../../shared/linear/issue-types' import type { LinkedWorkItemSummary } from '@/lib/new-workspace' import { buildLinearWorkspaceSource, diff --git a/src/renderer/src/lib/linked-work-item-context.ts b/src/renderer/src/lib/linked-work-item-context.ts index bd39e87c453..214b892b35b 100644 --- a/src/renderer/src/lib/linked-work-item-context.ts +++ b/src/renderer/src/lib/linked-work-item-context.ts @@ -1,4 +1,4 @@ -import type { TaskProvider } from '../../../shared/types' +import type { TaskProvider } from '../../../shared/task-providers' export type LinkedWorkItemContext = { provider: TaskProvider diff --git a/src/renderer/src/lib/local-path-open-guard.ts b/src/renderer/src/lib/local-path-open-guard.ts index 7fed28d8e10..21d92a48e5e 100644 --- a/src/renderer/src/lib/local-path-open-guard.ts +++ b/src/renderer/src/lib/local-path-open-guard.ts @@ -1,5 +1,5 @@ import { toast } from 'sonner' -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import { translate } from '@/i18n/i18n' export function isLocalPathOpenBlocked( diff --git a/src/renderer/src/lib/local-preflight-context.test.ts b/src/renderer/src/lib/local-preflight-context.test.ts index 69d38f9ced6..3bce56d5340 100644 --- a/src/renderer/src/lib/local-preflight-context.test.ts +++ b/src/renderer/src/lib/local-preflight-context.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' -import type { Repo, Worktree } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' +import type { Worktree } from '../../../shared/worktree/types' import type { AppState } from '@/store/types' import { getLocalAgentPreflightContext, diff --git a/src/renderer/src/lib/local-preflight-context.ts b/src/renderer/src/lib/local-preflight-context.ts index 28da8a3d5bd..98ac6c1059e 100644 --- a/src/renderer/src/lib/local-preflight-context.ts +++ b/src/renderer/src/lib/local-preflight-context.ts @@ -7,7 +7,8 @@ import { type ProjectExecutionRuntimeResolution } from '../../../shared/project-execution-runtime' import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host' -import type { Repo, Worktree } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' +import type { Worktree } from '../../../shared/worktree/types' import { getProviderRuntimeContextKey } from './provider-runtime-context' import { getRendererAppPlatform } from './renderer-app-platform' import { diff --git a/src/renderer/src/lib/markdown-comment-blocks.test.ts b/src/renderer/src/lib/markdown-comment-blocks.test.ts index f3b0443d6ea..74eb3190b74 100644 --- a/src/renderer/src/lib/markdown-comment-blocks.test.ts +++ b/src/renderer/src/lib/markdown-comment-blocks.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { DiffComment } from '../../../shared/types' +import type { DiffComment } from '../../../shared/diff-comment-types' import { mapMarkdownCommentsToBlocks, type MarkdownCommentBlock } from './markdown-comment-blocks' function makeComment(overrides: Partial = {}): DiffComment { diff --git a/src/renderer/src/lib/markdown-comment-blocks.ts b/src/renderer/src/lib/markdown-comment-blocks.ts index 59d11ed4402..6d12ac6d2c2 100644 --- a/src/renderer/src/lib/markdown-comment-blocks.ts +++ b/src/renderer/src/lib/markdown-comment-blocks.ts @@ -1,4 +1,4 @@ -import type { DiffComment } from '../../../shared/types' +import type { DiffComment } from '../../../shared/diff-comment-types' export type MarkdownCommentBlock = { key: string diff --git a/src/renderer/src/lib/markdown-document-templates.test.ts b/src/renderer/src/lib/markdown-document-templates.test.ts index e787805dfb6..ab17687fcc2 100644 --- a/src/renderer/src/lib/markdown-document-templates.test.ts +++ b/src/renderer/src/lib/markdown-document-templates.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import type { DirEntry } from '../../../shared/types' +import type { DirEntry } from '../../../shared/filesystem-entry-types' import { applyMarkdownTemplatePlaceholders, getMarkdownTemplateTitleForFileName, diff --git a/src/renderer/src/lib/markdown-document-templates.ts b/src/renderer/src/lib/markdown-document-templates.ts index 60414d481f0..b4149389c02 100644 --- a/src/renderer/src/lib/markdown-document-templates.ts +++ b/src/renderer/src/lib/markdown-document-templates.ts @@ -1,4 +1,5 @@ -import type { DirEntry, GlobalSettings } from '../../../shared/types' +import type { DirEntry } from '../../../shared/filesystem-entry-types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client' import { readRuntimeDirectory, diff --git a/src/renderer/src/lib/markdown-review-note-copy.test.ts b/src/renderer/src/lib/markdown-review-note-copy.test.ts index bfac91ff209..ae00b96fe25 100644 --- a/src/renderer/src/lib/markdown-review-note-copy.test.ts +++ b/src/renderer/src/lib/markdown-review-note-copy.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { DiffComment } from '../../../shared/types' +import type { DiffComment } from '../../../shared/diff-comment-types' import { copyMarkdownReviewNotesForAgent, type MarkdownReviewNoteClipboardWriter diff --git a/src/renderer/src/lib/markdown-review-notes.test.ts b/src/renderer/src/lib/markdown-review-notes.test.ts index 9d583df9fdc..80c40089e1f 100644 --- a/src/renderer/src/lib/markdown-review-notes.test.ts +++ b/src/renderer/src/lib/markdown-review-notes.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import type { DiffComment } from '../../../shared/types' +import type { DiffComment } from '../../../shared/diff-comment-types' import { formatMarkdownReviewCardQuote, formatMarkdownReviewNotes, diff --git a/src/renderer/src/lib/markdown-review-notes.ts b/src/renderer/src/lib/markdown-review-notes.ts index 93feeb6099f..c0831ef8edd 100644 --- a/src/renderer/src/lib/markdown-review-notes.ts +++ b/src/renderer/src/lib/markdown-review-notes.ts @@ -1,4 +1,4 @@ -import type { DiffComment } from '../../../shared/types' +import type { DiffComment } from '../../../shared/diff-comment-types' import { getDiffCommentLineLabel } from './diff-comment-compat' const MAX_EXCERPT_LINES = 8 diff --git a/src/renderer/src/lib/native-chat-initial-view-mode.test.ts b/src/renderer/src/lib/native-chat-initial-view-mode.test.ts index 4ef61a5cb78..e3a7a5b7aad 100644 --- a/src/renderer/src/lib/native-chat-initial-view-mode.test.ts +++ b/src/renderer/src/lib/native-chat-initial-view-mode.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import type { Tab } from '../../../shared/types' +import type { Tab } from '../../../shared/tab-types' import { decideInitialAgentTabViewMode, initialAgentTabViewModeProps diff --git a/src/renderer/src/lib/native-chat-initial-view-mode.ts b/src/renderer/src/lib/native-chat-initial-view-mode.ts index 9b06a0a0d38..258fd8f190f 100644 --- a/src/renderer/src/lib/native-chat-initial-view-mode.ts +++ b/src/renderer/src/lib/native-chat-initial-view-mode.ts @@ -1,4 +1,6 @@ -import type { GlobalSettings, Tab, TuiAgent } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { Tab } from '../../../shared/tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' import { canMirrorLaunchDraftToNativeChat } from '@/lib/native-chat-launch-draft-mirrorability' import { isNativeChatSupportedAgent, diff --git a/src/renderer/src/lib/native-chat-launch-prompt.ts b/src/renderer/src/lib/native-chat-launch-prompt.ts index b279730c812..289bc78d6e5 100644 --- a/src/renderer/src/lib/native-chat-launch-prompt.ts +++ b/src/renderer/src/lib/native-chat-launch-prompt.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' export type NativeChatLaunchPrompt = { tabId: string diff --git a/src/renderer/src/lib/native-chat-telemetry.ts b/src/renderer/src/lib/native-chat-telemetry.ts index 1299204b6db..ba3f7e5d9cc 100644 --- a/src/renderer/src/lib/native-chat-telemetry.ts +++ b/src/renderer/src/lib/native-chat-telemetry.ts @@ -9,7 +9,7 @@ import { track, tuiAgentToAgentKind } from './telemetry' import type { NativeChatRuntime } from '../../../shared/telemetry-events' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' /** Loose agent type accepted by these emitters: the strict launch `TuiAgent`, or * the broader `AgentType` string carried by the chat view. Narrowing to the diff --git a/src/renderer/src/lib/nested-repo-selected-paths.ts b/src/renderer/src/lib/nested-repo-selected-paths.ts index 450544bbcd0..1faf58ca430 100644 --- a/src/renderer/src/lib/nested-repo-selected-paths.ts +++ b/src/renderer/src/lib/nested-repo-selected-paths.ts @@ -1,4 +1,4 @@ -import type { NestedRepoScanResult } from '../../../shared/types' +import type { NestedRepoScanResult } from '../../../shared/project-group-types' export function getSelectedNestedRepoPathsInScanOrder( scan: Pick, diff --git a/src/renderer/src/lib/new-workspace-composer-repo.test.ts b/src/renderer/src/lib/new-workspace-composer-repo.test.ts index 575725630dc..3b73d209b15 100644 --- a/src/renderer/src/lib/new-workspace-composer-repo.test.ts +++ b/src/renderer/src/lib/new-workspace-composer-repo.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import { getComposerEligibleRepos, resolveComposerActiveRepoId, diff --git a/src/renderer/src/lib/new-workspace-composer-repo.ts b/src/renderer/src/lib/new-workspace-composer-repo.ts index 1cce749e03b..8e32e1e4bca 100644 --- a/src/renderer/src/lib/new-workspace-composer-repo.ts +++ b/src/renderer/src/lib/new-workspace-composer-repo.ts @@ -5,7 +5,7 @@ import { resolveNewWorkspaceDialogRepoId } from '../../../shared/new-workspace-dialog-repo' import { getProjectIdentityKey } from '../../../shared/project-host-setup-projection' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' export function getComposerEligibleRepos(repos: readonly Repo[]): Repo[] { return getNewWorkspaceDialogEligibleRepos(repos) diff --git a/src/renderer/src/lib/new-workspace-project-options.test.ts b/src/renderer/src/lib/new-workspace-project-options.test.ts index f6130d8f861..b249570fb3d 100644 --- a/src/renderer/src/lib/new-workspace-project-options.test.ts +++ b/src/renderer/src/lib/new-workspace-project-options.test.ts @@ -9,7 +9,9 @@ import { searchNewWorkspaceProjectOptions, type NewWorkspaceProjectOption } from './new-workspace-project-options' -import type { Project, ProjectGroup, ProjectHostSetup, Repo } from '../../../shared/types' +import type { ProjectGroup } from '../../../shared/project-group-types' +import type { Project, ProjectHostSetup } from '../../../shared/project-types' +import type { Repo } from '../../../shared/repo-types' function repo(id: string, overrides: Partial = {}): Repo { return { diff --git a/src/renderer/src/lib/new-workspace-project-options.ts b/src/renderer/src/lib/new-workspace-project-options.ts index 58cab355340..be8261126b8 100644 --- a/src/renderer/src/lib/new-workspace-project-options.ts +++ b/src/renderer/src/lib/new-workspace-project-options.ts @@ -1,5 +1,7 @@ import { projectHostSetupProjectionFromRepos } from '../../../shared/project-host-setup-projection' -import type { Project, ProjectGroup, ProjectHostSetup, Repo } from '../../../shared/types' +import type { ProjectGroup } from '../../../shared/project-group-types' +import type { Project, ProjectHostSetup } from '../../../shared/project-types' +import type { Repo } from '../../../shared/repo-types' import { isClipboardTextByteLengthOverLimit } from '../../../shared/clipboard-text' import type { ExecutionHostRegistryEntry } from '../../../shared/execution-host-registry' import { diff --git a/src/renderer/src/lib/new-workspace.ts b/src/renderer/src/lib/new-workspace.ts index d9b133b5263..d19da5e5de4 100644 --- a/src/renderer/src/lib/new-workspace.ts +++ b/src/renderer/src/lib/new-workspace.ts @@ -13,7 +13,8 @@ import { queuePendingAgentStartupDelivery, resolveAgentStartupTabId } from '@/lib/agent-startup-delayed-delivery' -import type { FolderWorkspaceLinkedTask, OrcaHooks } from '../../../shared/types' +import type { FolderWorkspaceLinkedTask } from '../../../shared/folder-workspace-types' +import type { OrcaHooks } from '../../../shared/orca-yaml-hook-types' import { resolveHookCommandSourcePolicy } from '../../../shared/hook-command-source-policy' import { slugifyForWorkspaceName } from '../../../shared/workspace-name' import { createBrowserUuid } from '@/lib/browser-uuid' diff --git a/src/renderer/src/lib/notes-send-agent-targets.test.ts b/src/renderer/src/lib/notes-send-agent-targets.test.ts index 0b24234c3bf..6dba34d51ce 100644 --- a/src/renderer/src/lib/notes-send-agent-targets.test.ts +++ b/src/renderer/src/lib/notes-send-agent-targets.test.ts @@ -4,7 +4,7 @@ import { type AgentStatusEntry, type AgentStatusState } from '../../../shared/agent-status-types' -import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types' import { makePaneKey } from '../../../shared/stable-pane-id' import { deriveNotesSendAgentTargets, diff --git a/src/renderer/src/lib/notes-send-agent-targets.ts b/src/renderer/src/lib/notes-send-agent-targets.ts index 3c2f6d412c4..ce4e00ae1ed 100644 --- a/src/renderer/src/lib/notes-send-agent-targets.ts +++ b/src/renderer/src/lib/notes-send-agent-targets.ts @@ -2,7 +2,7 @@ import type { AgentType } from '../../../shared/agent-status-types' import type { AppState } from '@/store/types' import { isTerminalLeafId, makePaneKey } from '../../../shared/stable-pane-id' import { resolveTerminalTitleAgentType } from '../../../shared/terminal-title-agent-type' -import type { TerminalTab } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' import { detectAgentSendTitleStatus } from './agent-send-title-status' import { resolveRuntimePaneTitleLeafResolution, diff --git a/src/renderer/src/lib/onboarding-folder-agent-startup.ts b/src/renderer/src/lib/onboarding-folder-agent-startup.ts index 55668937430..adb1a8bfbbf 100644 --- a/src/renderer/src/lib/onboarding-folder-agent-startup.ts +++ b/src/renderer/src/lib/onboarding-folder-agent-startup.ts @@ -8,7 +8,9 @@ import { import type { AgentStartedTelemetry } from '@/lib/worktree-activation' import type { StartupCommandDelivery } from '../../../shared/codex-startup-delivery' import type { SleepingAgentLaunchConfig } from '../../../shared/agent-session-resume' -import type { GlobalSettings, OnboardingState, TuiAgent } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { OnboardingState } from '../../../shared/onboarding-state-types' +import type { TuiAgent } from '../../../shared/tui-agent' import { resolveInitialNativeChatSessionOptions } from '@/components/native-chat/native-chat-launch-session-options' import type { SessionOptionValue } from '../../../shared/native-chat-session-options' diff --git a/src/renderer/src/lib/onboarding-project-checklist.ts b/src/renderer/src/lib/onboarding-project-checklist.ts index a39e8d093a2..742cd164776 100644 --- a/src/renderer/src/lib/onboarding-project-checklist.ts +++ b/src/renderer/src/lib/onboarding-project-checklist.ts @@ -1,5 +1,5 @@ import { track } from '@/lib/telemetry' -import type { OnboardingState } from '../../../shared/types' +import type { OnboardingState } from '../../../shared/onboarding-state-types' export type OnboardingProjectChecklistItem = 'addedRepo' | 'addedFolder' diff --git a/src/renderer/src/lib/open-in-app-catalog.tsx b/src/renderer/src/lib/open-in-app-catalog.tsx index ea54d1d2491..a2ed0526354 100644 --- a/src/renderer/src/lib/open-in-app-catalog.tsx +++ b/src/renderer/src/lib/open-in-app-catalog.tsx @@ -1,6 +1,6 @@ import type React from 'react' import { AppWindow } from 'lucide-react' -import type { OpenInApplication } from '../../../shared/types' +import type { OpenInApplication } from '../../../shared/ui-chrome-types' import { cn } from './utils' import { translate } from '@/i18n/i18n' import { createLocalizedCatalog } from '@/i18n/localized-catalog' diff --git a/src/renderer/src/lib/open-in-application-selection.ts b/src/renderer/src/lib/open-in-application-selection.ts index 09ccaca9bb9..a4c1f53e609 100644 --- a/src/renderer/src/lib/open-in-application-selection.ts +++ b/src/renderer/src/lib/open-in-application-selection.ts @@ -1,3 +1,3 @@ -import type { OpenInApplication } from '../../../shared/types' +import type { OpenInApplication } from '../../../shared/ui-chrome-types' export const NO_OPEN_IN_APPLICATIONS: readonly OpenInApplication[] = [] diff --git a/src/renderer/src/lib/orchestration-skill-coverage.test.ts b/src/renderer/src/lib/orchestration-skill-coverage.test.ts index d7001e7ebd8..d0128396e4e 100644 --- a/src/renderer/src/lib/orchestration-skill-coverage.test.ts +++ b/src/renderer/src/lib/orchestration-skill-coverage.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { DiscoveredSkill, SkillDiscoverySource } from '../../../shared/skills' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import { agentHasOrchestrationSkill, getOrchestrationSkillAgentStatuses diff --git a/src/renderer/src/lib/orchestration-skill-coverage.ts b/src/renderer/src/lib/orchestration-skill-coverage.ts index 4b4e58144c2..4196a50dd18 100644 --- a/src/renderer/src/lib/orchestration-skill-coverage.ts +++ b/src/renderer/src/lib/orchestration-skill-coverage.ts @@ -1,6 +1,6 @@ import type { AgentType } from '../../../shared/agent-status-types' import type { DiscoveredSkill, SkillDiscoverySource } from '../../../shared/skills' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import { ORCHESTRATION_SKILL_NAME } from '@/lib/agent-feature-install-commands' import { getAgentLabel } from '@/lib/agent-catalog' import { TUI_AGENT_AUTO_PICK_ORDER } from '../../../shared/tui-agent-selection' diff --git a/src/renderer/src/lib/order-empty-query-worktrees.test.ts b/src/renderer/src/lib/order-empty-query-worktrees.test.ts index 967863f5d0f..f60ddb04a89 100644 --- a/src/renderer/src/lib/order-empty-query-worktrees.test.ts +++ b/src/renderer/src/lib/order-empty-query-worktrees.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' import { orderEmptyQueryWorktrees } from './order-empty-query-worktrees' function wt(overrides: Partial & { id: string; displayName: string }): Worktree { diff --git a/src/renderer/src/lib/order-empty-query-worktrees.ts b/src/renderer/src/lib/order-empty-query-worktrees.ts index aa165b8c6a4..8362bc03527 100644 --- a/src/renderer/src/lib/order-empty-query-worktrees.ts +++ b/src/renderer/src/lib/order-empty-query-worktrees.ts @@ -1,4 +1,4 @@ -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' import { compareWorktreeDisplayName } from './worktree-display-name-order' export type OrderEmptyQueryInputs = { diff --git a/src/renderer/src/lib/pane-agent-evidence.ts b/src/renderer/src/lib/pane-agent-evidence.ts index 4c9c49ad9bb..8230bb51522 100644 --- a/src/renderer/src/lib/pane-agent-evidence.ts +++ b/src/renderer/src/lib/pane-agent-evidence.ts @@ -1,7 +1,7 @@ import type { AgentStatus } from '../../../shared/agent-detection' import { detectAgentStatusFromTitle, getAgentLabel } from '../../../shared/agent-detection' import { resolveExplicitTerminalTitleAgentType } from '../../../shared/terminal-title-agent-type' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import { AGENT_STATUS_STALE_AFTER_MS, type AgentStatusEntry, diff --git a/src/renderer/src/lib/pane-manager/pane-manager-types.ts b/src/renderer/src/lib/pane-manager/pane-manager-types.ts index 34b42c4b591..62c69836841 100644 --- a/src/renderer/src/lib/pane-manager/pane-manager-types.ts +++ b/src/renderer/src/lib/pane-manager/pane-manager-types.ts @@ -6,7 +6,7 @@ import type { Unicode11Addon } from '@xterm/addon-unicode11' import type { WebLinksAddon } from '@xterm/addon-web-links' import type { WebglAddon } from '@xterm/addon-webgl' import type { SerializeAddon } from '@xterm/addon-serialize' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { TerminalLeafId } from '../../../../shared/stable-pane-id' import type { TerminalWebglAutoDecision } from './terminal-webgl-auto-policy' diff --git a/src/renderer/src/lib/pane-manager/terminal-keyboard-protocol.ts b/src/renderer/src/lib/pane-manager/terminal-keyboard-protocol.ts index 8c804236f9b..66c85d56581 100644 --- a/src/renderer/src/lib/pane-manager/terminal-keyboard-protocol.ts +++ b/src/renderer/src/lib/pane-manager/terminal-keyboard-protocol.ts @@ -1,6 +1,6 @@ import type { ITerminalOptions } from '@xterm/xterm' import type { ExecutionHostId } from '../../../../shared/execution-host' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { isLocalNativeWindowsConpty, type WindowsPtyCompatibilityContext diff --git a/src/renderer/src/lib/pending-worktree-creation.ts b/src/renderer/src/lib/pending-worktree-creation.ts index 10e37abcf89..cbf41f283b7 100644 --- a/src/renderer/src/lib/pending-worktree-creation.ts +++ b/src/renderer/src/lib/pending-worktree-creation.ts @@ -1,13 +1,15 @@ +import type { TuiAgent } from '../../../shared/tui-agent' +import type { WorkspaceSource as WorkspaceCreateTelemetrySource } from '../../../shared/workspace-source' import type { CreateSparseCheckoutRequest, + SetupDecision +} from '../../../shared/worktree/create-types' +import type { WorktreeStartupLaunch } from '../../../shared/worktree/launch-types' +import type { GitPushTarget, - SetupDecision, - TuiAgent, - WorkspaceCreateTelemetrySource, - WorkspaceStatus, WorkspaceLinkedItem, - WorktreeStartupLaunch -} from '../../../shared/types' + WorkspaceStatus +} from '../../../shared/worktree/types' import type { AgentStartupPlan } from '@/lib/tui-agent-startup' import type { AgentStartedTelemetry } from '@/lib/worktree-activation' import type { TaskSourceContext, WorkspaceRunContext } from '../../../shared/task-source-context' diff --git a/src/renderer/src/lib/pi-session-resume-wake.test.ts b/src/renderer/src/lib/pi-session-resume-wake.test.ts index a0f69f68c2a..984a4fd9639 100644 --- a/src/renderer/src/lib/pi-session-resume-wake.test.ts +++ b/src/renderer/src/lib/pi-session-resume-wake.test.ts @@ -2,7 +2,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume' -import type { TerminalTab } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' import { useAppStore } from '@/store' import { resumeSleepingAgentSessionsForWorktree } from './resume-sleeping-agent-session' diff --git a/src/renderer/src/lib/pr-comment-action-state.test.ts b/src/renderer/src/lib/pr-comment-action-state.test.ts index b3038076b87..62f7e3979d1 100644 --- a/src/renderer/src/lib/pr-comment-action-state.test.ts +++ b/src/renderer/src/lib/pr-comment-action-state.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { PRComment } from '../../../shared/types' +import type { PRComment } from '../../../shared/github/comment-types' import { groupPRComments, type PRCommentGroup } from '../../../shared/pr-comment-groups' import { getPRCommentGroupActionState, diff --git a/src/renderer/src/lib/pr-comment-action-state.ts b/src/renderer/src/lib/pr-comment-action-state.ts index 2207141bd06..12f9ef87287 100644 --- a/src/renderer/src/lib/pr-comment-action-state.ts +++ b/src/renderer/src/lib/pr-comment-action-state.ts @@ -1,5 +1,5 @@ import { getPRCommentGroupRoot, type PRCommentGroup } from '../../../shared/pr-comment-groups' -import type { PRComment } from '../../../shared/types' +import type { PRComment } from '../../../shared/github/comment-types' /** How a comment group should read in the PR sidebar triage UI. */ export type PRCommentGroupActionState = 'open' | 'conversation' | 'resolved' diff --git a/src/renderer/src/lib/pr-comment-reactions.test.ts b/src/renderer/src/lib/pr-comment-reactions.test.ts index d742fde85d8..01dcb7e5523 100644 --- a/src/renderer/src/lib/pr-comment-reactions.test.ts +++ b/src/renderer/src/lib/pr-comment-reactions.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { PRComment } from '../../../shared/types' +import type { PRComment } from '../../../shared/github/comment-types' import { restoreCommentReaction, setCommentReaction, diff --git a/src/renderer/src/lib/pr-comment-reactions.ts b/src/renderer/src/lib/pr-comment-reactions.ts index f124f082d17..417b8a9e4bb 100644 --- a/src/renderer/src/lib/pr-comment-reactions.ts +++ b/src/renderer/src/lib/pr-comment-reactions.ts @@ -1,4 +1,8 @@ -import type { GitHubReaction, GitHubReactionContent, PRComment } from '../../../shared/types' +import type { + GitHubReaction, + GitHubReactionContent, + PRComment +} from '../../../shared/github/comment-types' export const GITHUB_REACTION_ORDER: readonly GitHubReactionContent[] = [ '+1', diff --git a/src/renderer/src/lib/project-host-clone-url.test.ts b/src/renderer/src/lib/project-host-clone-url.test.ts index 4850e41a308..f383945ce93 100644 --- a/src/renderer/src/lib/project-host-clone-url.test.ts +++ b/src/renderer/src/lib/project-host-clone-url.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Project } from '../../../shared/types' +import type { Project } from '../../../shared/project-types' import { getProjectHostCloneUrl } from './project-host-clone-url' function createProject(overrides: Partial = {}): Project { diff --git a/src/renderer/src/lib/project-host-clone-url.ts b/src/renderer/src/lib/project-host-clone-url.ts index e32e9a699d1..b7ff42ffbf4 100644 --- a/src/renderer/src/lib/project-host-clone-url.ts +++ b/src/renderer/src/lib/project-host-clone-url.ts @@ -1,4 +1,4 @@ -import type { Project } from '../../../shared/types' +import type { Project } from '../../../shared/project-types' export function getProjectHostCloneUrl(project: Project | null | undefined): string | null { const identity = project?.providerIdentity diff --git a/src/renderer/src/lib/project-host-setup-options.test.ts b/src/renderer/src/lib/project-host-setup-options.test.ts index a5e5e2056d4..9eee19f9725 100644 --- a/src/renderer/src/lib/project-host-setup-options.test.ts +++ b/src/renderer/src/lib/project-host-setup-options.test.ts @@ -8,7 +8,8 @@ import { PROJECT_HOST_SETUP_RUNTIME_CAPABILITY, WORKSPACE_RUN_CONTEXT_RUNTIME_CAPABILITY } from '../../../shared/protocol-version' -import type { ProjectHostSetup, Repo } from '../../../shared/types' +import type { ProjectHostSetup } from '../../../shared/project-types' +import type { Repo } from '../../../shared/repo-types' import { buildProjectHostSetupOptions } from './project-host-setup-options' const FULL_HOST_MODEL_RUNTIME_CAPABILITIES = [ diff --git a/src/renderer/src/lib/project-host-setup-options.ts b/src/renderer/src/lib/project-host-setup-options.ts index 9a8039cf19f..eca4dc397c6 100644 --- a/src/renderer/src/lib/project-host-setup-options.ts +++ b/src/renderer/src/lib/project-host-setup-options.ts @@ -11,7 +11,8 @@ import { PROJECT_HOST_SETUP_RUNTIME_CAPABILITY, WORKSPACE_RUN_CONTEXT_RUNTIME_CAPABILITY } from '../../../shared/protocol-version' -import type { ProjectHostSetup, Repo } from '../../../shared/types' +import type { ProjectHostSetup } from '../../../shared/project-types' +import type { Repo } from '../../../shared/repo-types' export type ProjectHostSetupOption = | { diff --git a/src/renderer/src/lib/project-host-workspace-target.test.ts b/src/renderer/src/lib/project-host-workspace-target.test.ts index 08484c99b5e..d49cb568763 100644 --- a/src/renderer/src/lib/project-host-workspace-target.test.ts +++ b/src/renderer/src/lib/project-host-workspace-target.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import type { ExecutionHostId } from '../../../shared/execution-host' -import type { Project, ProjectHostSetup, Repo } from '../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../shared/project-types' +import type { Repo } from '../../../shared/repo-types' import { resolveWorkspaceCreationRepoId, resolveWorkspaceCreationTarget diff --git a/src/renderer/src/lib/project-host-workspace-target.ts b/src/renderer/src/lib/project-host-workspace-target.ts index 09a63309138..a7b08dc6617 100644 --- a/src/renderer/src/lib/project-host-workspace-target.ts +++ b/src/renderer/src/lib/project-host-workspace-target.ts @@ -5,7 +5,8 @@ import { type ExecutionHostScope } from '../../../shared/execution-host' import { projectHostSetupProjectionFromRepos } from '../../../shared/project-host-setup-projection' -import type { Project, ProjectHostSetup, Repo } from '../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../shared/project-types' +import type { Repo } from '../../../shared/repo-types' import { resolveComposerRepoId } from './new-workspace-composer-repo' export type WorkspaceCreationTarget = { diff --git a/src/renderer/src/lib/project-skill-runtime.ts b/src/renderer/src/lib/project-skill-runtime.ts index 4366d6a9f58..1f10495a4aa 100644 --- a/src/renderer/src/lib/project-skill-runtime.ts +++ b/src/renderer/src/lib/project-skill-runtime.ts @@ -1,6 +1,6 @@ import type { ProjectExecutionRuntimeResolution } from '../../../shared/project-execution-runtime' import type { SkillDiscoveryTarget } from '../../../shared/skills' -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import { resolveWindowsShellStartupFamily } from '../../../shared/windows-terminal-shell' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/lib/provider-runtime-context.ts b/src/renderer/src/lib/provider-runtime-context.ts index 7b949e587ac..772ca7ffb42 100644 --- a/src/renderer/src/lib/provider-runtime-context.ts +++ b/src/renderer/src/lib/provider-runtime-context.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' export function getProviderRuntimeContextKey( settings: Pick | null | undefined diff --git a/src/renderer/src/lib/quick-workspace-agent-selection.ts b/src/renderer/src/lib/quick-workspace-agent-selection.ts index 51fe886e7eb..09801125d0d 100644 --- a/src/renderer/src/lib/quick-workspace-agent-selection.ts +++ b/src/renderer/src/lib/quick-workspace-agent-selection.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import { isTuiAgentEnabled, pickTuiAgent, diff --git a/src/renderer/src/lib/recent-workspace-tab-rows.test.ts b/src/renderer/src/lib/recent-workspace-tab-rows.test.ts index 1e71bb74f63..c36a48dc615 100644 --- a/src/renderer/src/lib/recent-workspace-tab-rows.test.ts +++ b/src/renderer/src/lib/recent-workspace-tab-rows.test.ts @@ -7,7 +7,7 @@ import { } from './recent-workspace-tab-rows' import type { TabPaneInputSources } from '@/components/sidebar/smart-attention' import type { AgentStatusEntry, AgentStatusState } from '../../../shared/agent-status-types' -import type { TabGroup } from '../../../shared/types' +import type { TabGroup } from '../../../shared/tab-types' const NOW = 1_700_000_000_000 const LEAF_ID = '11111111-2222-4333-8444-555555555555' diff --git a/src/renderer/src/lib/recent-workspace-tab-rows.ts b/src/renderer/src/lib/recent-workspace-tab-rows.ts index b990ef1bdd2..5005030b81a 100644 --- a/src/renderer/src/lib/recent-workspace-tab-rows.ts +++ b/src/renderer/src/lib/recent-workspace-tab-rows.ts @@ -8,7 +8,8 @@ import { } from '@/components/sidebar/smart-attention' import { tabHasLivePty } from './tab-has-live-pty' import type { WorktreeStatus } from './worktree-status' -import type { TabGroup, TerminalTab } from '../../../shared/types' +import type { TabGroup } from '../../../shared/tab-types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' /** * Row model for Cmd+J's empty-query "Recent chats & terminals" section. diff --git a/src/renderer/src/lib/repo-runtime-owner.test.ts b/src/renderer/src/lib/repo-runtime-owner.test.ts index c34dda10e0f..b341dbdbd6a 100644 --- a/src/renderer/src/lib/repo-runtime-owner.test.ts +++ b/src/renderer/src/lib/repo-runtime-owner.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest' -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import type { RepoRuntimeOwnerState } from './repo-runtime-owner' import { getExplicitRuntimeOwnerEnvironmentId, diff --git a/src/renderer/src/lib/repo-runtime-owner.ts b/src/renderer/src/lib/repo-runtime-owner.ts index 8cd59142816..81930d72a33 100644 --- a/src/renderer/src/lib/repo-runtime-owner.ts +++ b/src/renderer/src/lib/repo-runtime-owner.ts @@ -3,7 +3,8 @@ import { getSettingsFocusedExecutionHostId, parseExecutionHostId } from '../../../shared/execution-host' -import type { GlobalSettings, Repo } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { Repo } from '../../../shared/repo-types' export type RepoRuntimeOwnerState = { repos?: readonly Pick[] diff --git a/src/renderer/src/lib/repo-search.test.ts b/src/renderer/src/lib/repo-search.test.ts index 884093ac25f..c355807896c 100644 --- a/src/renderer/src/lib/repo-search.test.ts +++ b/src/renderer/src/lib/repo-search.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { REPO_SEARCH_QUERY_MAX_BYTES, isRepoSearchQueryTooLarge, searchRepos } from './repo-search' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' function makeRepo(overrides: Partial = {}): Repo { return { diff --git a/src/renderer/src/lib/repo-search.ts b/src/renderer/src/lib/repo-search.ts index 5a223af6392..c36458e808f 100644 --- a/src/renderer/src/lib/repo-search.ts +++ b/src/renderer/src/lib/repo-search.ts @@ -1,4 +1,4 @@ -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import { isClipboardTextByteLengthOverLimit } from '../../../shared/clipboard-text' // Display-name matches must always outrank path-only matches. This offset is diff --git a/src/renderer/src/lib/repo-slug-cache.test.ts b/src/renderer/src/lib/repo-slug-cache.test.ts index 491afbc22b9..6f01ba8d46e 100644 --- a/src/renderer/src/lib/repo-slug-cache.test.ts +++ b/src/renderer/src/lib/repo-slug-cache.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import { githubRepoIdentityKey } from '../../../shared/github/repository-identity-key' import { REPO_SLUG_FAILURE_TTL_MS, diff --git a/src/renderer/src/lib/repo-slug-cache.ts b/src/renderer/src/lib/repo-slug-cache.ts index 9c2a455f574..7c73372d5ee 100644 --- a/src/renderer/src/lib/repo-slug-cache.ts +++ b/src/renderer/src/lib/repo-slug-cache.ts @@ -1,7 +1,8 @@ // Why: the slug → Repo cache and its synchronous lookup live here (separate from // repo-slug-index.ts) so store slices can import the sync lookup without pulling // in repo-slug-index's `@/store` dependency, which would form an import cycle. -import type { GlobalSettings, Repo } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { Repo } from '../../../shared/repo-types' import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' import { getSettingsForRepoRuntimeOwner } from './repo-runtime-owner' import { diff --git a/src/renderer/src/lib/repo-slug-index.test.ts b/src/renderer/src/lib/repo-slug-index.test.ts index 0b710cf5438..d09d515d627 100644 --- a/src/renderer/src/lib/repo-slug-index.test.ts +++ b/src/renderer/src/lib/repo-slug-index.test.ts @@ -2,7 +2,7 @@ import { act, cleanup, renderHook, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import { useAppStore } from '@/store' import { useRepoSlugIndex } from './repo-slug-index' import { REPO_SLUG_FAILURE_TTL_MS, clearRepoSlugCacheValues } from './repo-slug-cache' diff --git a/src/renderer/src/lib/repo-slug-index.ts b/src/renderer/src/lib/repo-slug-index.ts index 07d6896cb08..51460ceb551 100644 --- a/src/renderer/src/lib/repo-slug-index.ts +++ b/src/renderer/src/lib/repo-slug-index.ts @@ -15,7 +15,8 @@ // process (`repoSlug` reads `.git/config`). import { useEffect, useMemo, useRef, useState } from 'react' import { useAppStore } from '@/store' -import type { Repo, GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { Repo } from '../../../shared/repo-types' import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' import { deleteRepoSlugCacheKey, diff --git a/src/renderer/src/lib/run-quick-command-in-new-tab.ts b/src/renderer/src/lib/run-quick-command-in-new-tab.ts index 526d35e5b9c..1d822ad8fc2 100644 --- a/src/renderer/src/lib/run-quick-command-in-new-tab.ts +++ b/src/renderer/src/lib/run-quick-command-in-new-tab.ts @@ -6,7 +6,7 @@ import { isTerminalAgentQuickCommand, supportsTerminalAgentQuickCommand } from '../../../shared/terminal-quick-commands' -import type { TerminalQuickCommand } from '../../../shared/types' +import type { TerminalQuickCommand } from '../../../shared/terminal-quick-command-types' export type RunQuickCommandInNewTabArgs = { command: TerminalQuickCommand diff --git a/src/renderer/src/lib/running-agent-targets.test.ts b/src/renderer/src/lib/running-agent-targets.test.ts index addf1741fa8..81126bdbda0 100644 --- a/src/renderer/src/lib/running-agent-targets.test.ts +++ b/src/renderer/src/lib/running-agent-targets.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import type { AppState } from '@/store/types' import type { AgentStatusEntry, AgentStatusState } from '../../../shared/agent-status-types' -import type { TerminalTab } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' import { makePaneKey } from '../../../shared/stable-pane-id' import { deriveRunningAgentSendTargets, diff --git a/src/renderer/src/lib/running-agent-targets.ts b/src/renderer/src/lib/running-agent-targets.ts index e483b04b4a9..1eb3f0a227d 100644 --- a/src/renderer/src/lib/running-agent-targets.ts +++ b/src/renderer/src/lib/running-agent-targets.ts @@ -1,6 +1,6 @@ import type { AppState } from '@/store/types' import type { AgentStatusEntry } from '../../../shared/agent-status-types' -import type { TerminalTab } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' import { parsePaneKey } from '../../../shared/stable-pane-id' import { resolvePaneAgentActivity } from '@/lib/pane-agent-evidence' import { detectAgentSendTitleStatus } from './agent-send-title-status' diff --git a/src/renderer/src/lib/runtime-agent-background-create.ts b/src/renderer/src/lib/runtime-agent-background-create.ts index 70915ae63d4..71362fa4dd3 100644 --- a/src/renderer/src/lib/runtime-agent-background-create.ts +++ b/src/renderer/src/lib/runtime-agent-background-create.ts @@ -2,7 +2,7 @@ import type { SleepingAgentLaunchConfig } from '../../../shared/agent-session-re import type { StartupCommandDelivery } from '../../../shared/codex-startup-delivery' import type { SessionOptionValue } from '../../../shared/native-chat-session-options' import type { RuntimeTerminalCreate } from '../../../shared/runtime-types' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import { createAgentSessionCreateOperation, toAgentLaunchPreferences, diff --git a/src/renderer/src/lib/runtime-pane-title-leaf-id.test.ts b/src/renderer/src/lib/runtime-pane-title-leaf-id.test.ts index be00d6b7098..b81d8994eae 100644 --- a/src/renderer/src/lib/runtime-pane-title-leaf-id.test.ts +++ b/src/renderer/src/lib/runtime-pane-title-leaf-id.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalLayoutSnapshot } from '../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../shared/terminal-tab-types' import { resolveRuntimePaneTitleForLeaf, resolveRuntimePaneTitleLeafResolution diff --git a/src/renderer/src/lib/runtime-pane-title-leaf-id.ts b/src/renderer/src/lib/runtime-pane-title-leaf-id.ts index c18e752e883..78c669cef66 100644 --- a/src/renderer/src/lib/runtime-pane-title-leaf-id.ts +++ b/src/renderer/src/lib/runtime-pane-title-leaf-id.ts @@ -1,6 +1,9 @@ import { FIRST_PANE_ID } from '../../../shared/pane-key' import { isTerminalLeafId } from '../../../shared/stable-pane-id' -import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode } from '../../../shared/types' +import type { + TerminalLayoutSnapshot, + TerminalPaneLayoutNode +} from '../../../shared/terminal-tab-types' export type RuntimePaneTitleLeafResolution = { title: string | null diff --git a/src/renderer/src/lib/session-write-subscriber.ts b/src/renderer/src/lib/session-write-subscriber.ts index ba1e2c7cdd0..05e49127e81 100644 --- a/src/renderer/src/lib/session-write-subscriber.ts +++ b/src/renderer/src/lib/session-write-subscriber.ts @@ -1,6 +1,6 @@ import type { AppState } from '../store' import { isDecorativeAgentTitleFrameChange } from '../../../shared/agent-decorative-title-signature' -import type { WorkspaceSessionPatch } from '../../../shared/types' +import type { WorkspaceSessionPatch } from '../../../shared/workspace-session-state-types' import { SESSION_RELEVANT_FIELDS, shouldPersistWorkspaceSession } from './workspace-session' import { buildWorkspaceSessionPatch } from './workspace-session-patch' diff --git a/src/renderer/src/lib/setup-script-prompt.test.ts b/src/renderer/src/lib/setup-script-prompt.test.ts index 605142bc154..b85c0baa66f 100644 --- a/src/renderer/src/lib/setup-script-prompt.test.ts +++ b/src/renderer/src/lib/setup-script-prompt.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import type { SetupScriptImportCandidate } from '../../../shared/setup-script-imports' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import { getRepoHostIdentityForParts } from '@/store/slices/repo-host-identity' import { buildImportedHookSettings, @@ -220,10 +220,7 @@ describe('setup script prompt inspection', () => { getSetupScriptPromptDismissalKey(remoteIdentity) ] expect( - filterSetupScriptPromptDismissalsToValidRepos( - input, - new Set([localIdentity, remoteIdentity]) - ) + filterSetupScriptPromptDismissalsToValidRepos(input, new Set([localIdentity, remoteIdentity])) ).toBe(input) }) diff --git a/src/renderer/src/lib/setup-script-prompt.ts b/src/renderer/src/lib/setup-script-prompt.ts index 6adbc04b092..a1315b1c73f 100644 --- a/src/renderer/src/lib/setup-script-prompt.ts +++ b/src/renderer/src/lib/setup-script-prompt.ts @@ -1,7 +1,8 @@ import { getDefaultRepoHookSettings } from '../../../shared/constants' import { resolveHookCommandSourcePolicy } from '../../../shared/hook-command-source-policy' import type { SetupScriptImportCandidate } from '../../../shared/setup-script-imports' -import type { Repo, RepoHookSettings } from '../../../shared/types' +import type { RepoHookSettings } from '../../../shared/orca-yaml-hook-types' +import type { Repo } from '../../../shared/repo-types' import type { HookCheckResult } from '@/runtime/runtime-hooks-client' import { isRuntimeScopeForbiddenError } from '@/runtime/runtime-rpc-client' import { hasEffectiveSetupCommand } from './setup-script-status' diff --git a/src/renderer/src/lib/setup-script-status.ts b/src/renderer/src/lib/setup-script-status.ts index a23d0d1a06f..33afbfbcc4a 100644 --- a/src/renderer/src/lib/setup-script-status.ts +++ b/src/renderer/src/lib/setup-script-status.ts @@ -1,5 +1,5 @@ import { resolveHookCommandSourcePolicy } from '../../../shared/hook-command-source-policy' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import type { HookCheckResult } from '@/runtime/runtime-hooks-client' export function hasEffectiveSetupCommand(repo: Repo, hooksResult: HookCheckResult): boolean { diff --git a/src/renderer/src/lib/simulator-palette-search.test.ts b/src/renderer/src/lib/simulator-palette-search.test.ts index 989f70554f6..4a0b33fcec5 100644 --- a/src/renderer/src/lib/simulator-palette-search.test.ts +++ b/src/renderer/src/lib/simulator-palette-search.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { Tab, TabGroup, Worktree } from '../../../shared/types' +import type { Tab, TabGroup } from '../../../shared/tab-types' +import type { Worktree } from '../../../shared/worktree/types' import { SIMULATOR_PALETTE_QUERY_MAX_BYTES, buildSearchableSimulatorTabs, diff --git a/src/renderer/src/lib/simulator-palette-search.ts b/src/renderer/src/lib/simulator-palette-search.ts index 148d723f8f2..5919fed1aed 100644 --- a/src/renderer/src/lib/simulator-palette-search.ts +++ b/src/renderer/src/lib/simulator-palette-search.ts @@ -1,5 +1,6 @@ import type { ExecutionHostId } from '../../../shared/execution-host' -import type { Tab, TabGroup, Worktree } from '../../../shared/types' +import type { Tab, TabGroup } from '../../../shared/tab-types' +import type { Worktree } from '../../../shared/worktree/types' import { isClipboardTextByteLengthOverLimit } from '../../../shared/clipboard-text' import { selectPaletteTypeAliasMatch } from './palette-type-alias-match' import { compareBaseSensitivityLocaleText } from './locale-text-collators' diff --git a/src/renderer/src/lib/simulator-tab-palette-activation.test.ts b/src/renderer/src/lib/simulator-tab-palette-activation.test.ts index 07c1090f622..745ed5a33e6 100644 --- a/src/renderer/src/lib/simulator-tab-palette-activation.test.ts +++ b/src/renderer/src/lib/simulator-tab-palette-activation.test.ts @@ -1,7 +1,8 @@ // @vitest-environment happy-dom import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Tab, TabGroup, Worktree } from '../../../shared/types' +import type { Tab, TabGroup } from '../../../shared/tab-types' +import type { Worktree } from '../../../shared/worktree/types' import { useAppStore } from '@/store' import type { AppState } from '@/store/types' diff --git a/src/renderer/src/lib/sleeping-agent-pane-ownership.ts b/src/renderer/src/lib/sleeping-agent-pane-ownership.ts index 29cc0a0e6d9..4ebed782f56 100644 --- a/src/renderer/src/lib/sleeping-agent-pane-ownership.ts +++ b/src/renderer/src/lib/sleeping-agent-pane-ownership.ts @@ -4,7 +4,7 @@ import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode, TerminalTab -} from '../../../shared/types' +} from '../../../shared/terminal-tab-types' import { parseLegacyNumericPaneKey, parsePaneKey } from '../../../shared/stable-pane-id' import { isWebTerminalSurfaceTabId } from '../../../shared/terminal-surface-id' diff --git a/src/renderer/src/lib/smart-github-submit.ts b/src/renderer/src/lib/smart-github-submit.ts index 9d500d98eb9..fee799ce224 100644 --- a/src/renderer/src/lib/smart-github-submit.ts +++ b/src/renderer/src/lib/smart-github-submit.ts @@ -1,4 +1,5 @@ -import type { GitHubWorkItem, GitPushTarget } from '../../../shared/types' +import type { GitHubWorkItem } from '../../../shared/github/work-item-types' +import type { GitPushTarget } from '../../../shared/worktree/types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { getTaskSourceCacheScope } from '../../../shared/task-source-context' import { getLinkedWorkItemWorkspaceName } from '../../../shared/workspace-name' diff --git a/src/renderer/src/lib/source-control-agent-action-plan.ts b/src/renderer/src/lib/source-control-agent-action-plan.ts index 05bc1de3c28..5e6681975e6 100644 --- a/src/renderer/src/lib/source-control-agent-action-plan.ts +++ b/src/renderer/src/lib/source-control-agent-action-plan.ts @@ -7,7 +7,7 @@ import { import { CLIENT_PLATFORM } from '@/lib/new-workspace' import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' import { isTuiAgentEnabled } from '../../../shared/tui-agent-selection' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import { translate } from '@/i18n/i18n' import { resolveLocalWindowsAgentStartupShell } from '../../../shared/windows-terminal-shell' import type { SessionOptionValue } from '../../../shared/native-chat-session-options' diff --git a/src/renderer/src/lib/source-control-launch-agent-selection.test.ts b/src/renderer/src/lib/source-control-launch-agent-selection.test.ts index 63a053f0186..3ef15b580d9 100644 --- a/src/renderer/src/lib/source-control-launch-agent-selection.test.ts +++ b/src/renderer/src/lib/source-control-launch-agent-selection.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest' import { getDefaultSettings } from '../../../shared/constants' -import type { GlobalSettings, Repo, TuiAgent } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { Repo } from '../../../shared/repo-types' +import type { TuiAgent } from '../../../shared/tui-agent' import { resolveSourceControlLaunchAgentScope, summarizeReposOverridingActionRecipe diff --git a/src/renderer/src/lib/source-control-launch-agent-selection.ts b/src/renderer/src/lib/source-control-launch-agent-selection.ts index c72098c9481..379f5640544 100644 --- a/src/renderer/src/lib/source-control-launch-agent-selection.ts +++ b/src/renderer/src/lib/source-control-launch-agent-selection.ts @@ -9,7 +9,9 @@ import type { SourceControlActionRecipe } from '../../../shared/source-control-ai-actions' import { filterEnabledTuiAgents } from '../../../shared/tui-agent-selection' -import type { GlobalSettings, Repo, TuiAgent } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { Repo } from '../../../shared/repo-types' +import type { TuiAgent } from '../../../shared/tui-agent' export function readSourceControlLaunchRecipeAgentId( recipe: Pick | null | undefined diff --git a/src/renderer/src/lib/startup-ui-hydration.test.ts b/src/renderer/src/lib/startup-ui-hydration.test.ts index 13e954ca260..748c2619c9a 100644 --- a/src/renderer/src/lib/startup-ui-hydration.test.ts +++ b/src/renderer/src/lib/startup-ui-hydration.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { getDefaultUIState } from '../../../shared/constants' -import type { PersistedUIState } from '../../../shared/types' +import type { PersistedUIState } from '../../../shared/persisted-ui-state-types' import { getStartupErrorFallbackUI, hydratePersistedUIAfterStartupRead diff --git a/src/renderer/src/lib/startup-ui-hydration.ts b/src/renderer/src/lib/startup-ui-hydration.ts index aeb14f9c73e..fe53e945d0f 100644 --- a/src/renderer/src/lib/startup-ui-hydration.ts +++ b/src/renderer/src/lib/startup-ui-hydration.ts @@ -4,7 +4,7 @@ import { DEFAULT_STATUS_BAR_ITEMS, DEFAULT_WORKTREE_CARD_PROPERTIES } from '../../../shared/constants' -import type { PersistedUIState } from '../../../shared/types' +import type { PersistedUIState } from '../../../shared/persisted-ui-state-types' export function hydratePersistedUIAfterStartupRead({ persistedUI, diff --git a/src/renderer/src/lib/tab-agent-status-index.test.ts b/src/renderer/src/lib/tab-agent-status-index.test.ts index 77ca75e4d2d..2f9f2793d16 100644 --- a/src/renderer/src/lib/tab-agent-status-index.test.ts +++ b/src/renderer/src/lib/tab-agent-status-index.test.ts @@ -14,7 +14,8 @@ import type { AgentStatusState, AgentType } from '../../../shared/agent-status-types' -import type { TerminalLayoutSnapshot, TerminalTab, TuiAgent } from '../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' import type { RetainedAgentEntry } from '@/store/slices/agent-status' // ─── Oracle: the pre-index full-map scans, kept here (not in src) so the diff --git a/src/renderer/src/lib/tab-agent-status-index.ts b/src/renderer/src/lib/tab-agent-status-index.ts index daa1b4fb273..30dff454624 100644 --- a/src/renderer/src/lib/tab-agent-status-index.ts +++ b/src/renderer/src/lib/tab-agent-status-index.ts @@ -1,5 +1,5 @@ import type { AgentStatusEntry } from '../../../shared/agent-status-types' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' import { parsePaneKey } from '../../../shared/stable-pane-id' import type { RetainedAgentEntry } from '@/store/slices/agent-status' import { agentTypeToIconAgent } from './agent-status' diff --git a/src/renderer/src/lib/tab-agent.test.ts b/src/renderer/src/lib/tab-agent.test.ts index 4d1f48837f1..3f564adbfb7 100644 --- a/src/renderer/src/lib/tab-agent.test.ts +++ b/src/renderer/src/lib/tab-agent.test.ts @@ -8,7 +8,8 @@ import { resolveSiblingTabAgent } from './tab-agent' import type { AgentStatusEntry, AgentType } from '../../../shared/agent-status-types' -import type { TerminalLayoutSnapshot, TerminalTab, TuiAgent } from '../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' import type { RetainedAgentEntry } from '@/store/slices/agent-status' // Composed exactly the way useTabAgent layers the resolvers: focused pane diff --git a/src/renderer/src/lib/tab-agent.ts b/src/renderer/src/lib/tab-agent.ts index 5512ce5e8a5..a6b99b4164e 100644 --- a/src/renderer/src/lib/tab-agent.ts +++ b/src/renderer/src/lib/tab-agent.ts @@ -1,5 +1,6 @@ import type { AgentStatusEntry } from '../../../shared/agent-status-types' -import type { TerminalLayoutSnapshot, TuiAgent } from '../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' import { isTerminalLeafId, makePaneKey } from '../../../shared/stable-pane-id' import type { RetainedAgentEntry } from '@/store/slices/agent-status' import { agentTypeToIconAgent } from './agent-status' diff --git a/src/renderer/src/lib/tab-number-shortcuts.test.ts b/src/renderer/src/lib/tab-number-shortcuts.test.ts index 4840e6aafd0..defee201fca 100644 --- a/src/renderer/src/lib/tab-number-shortcuts.test.ts +++ b/src/renderer/src/lib/tab-number-shortcuts.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Tab, TabGroup } from '../../../shared/types' +import type { Tab, TabGroup } from '../../../shared/tab-types' import type { AppState } from '@/store/types' import { resolveTabNumberShortcutTarget } from './tab-number-shortcuts' diff --git a/src/renderer/src/lib/tab-number-shortcuts.ts b/src/renderer/src/lib/tab-number-shortcuts.ts index 27a65675ce8..56968ad4eda 100644 --- a/src/renderer/src/lib/tab-number-shortcuts.ts +++ b/src/renderer/src/lib/tab-number-shortcuts.ts @@ -3,7 +3,7 @@ import { useAppStore } from '@/store' import type { AppState } from '@/store/types' import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' import { dedupeTabOrder } from '@/store/slices/tab-group-state' -import type { Tab } from '../../../shared/types' +import type { Tab } from '../../../shared/tab-types' import { activateWebRuntimeSessionTab, isWebRuntimeSessionActive diff --git a/src/renderer/src/lib/terminal-layout-equality.ts b/src/renderer/src/lib/terminal-layout-equality.ts index 716482764e8..fd023897cab 100644 --- a/src/renderer/src/lib/terminal-layout-equality.ts +++ b/src/renderer/src/lib/terminal-layout-equality.ts @@ -1,4 +1,7 @@ -import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode } from '../../../shared/types' +import type { + TerminalLayoutSnapshot, + TerminalPaneLayoutNode +} from '../../../shared/terminal-tab-types' function sameStringRecord( a: Readonly> | undefined, diff --git a/src/renderer/src/lib/terminal-pane-title-sanitization.test.ts b/src/renderer/src/lib/terminal-pane-title-sanitization.test.ts index d7195203c2d..f65d463941b 100644 --- a/src/renderer/src/lib/terminal-pane-title-sanitization.test.ts +++ b/src/renderer/src/lib/terminal-pane-title-sanitization.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types' import { sanitizeTerminalLayoutPaneTitles } from './terminal-pane-title-sanitization' const TAB: TerminalTab = { diff --git a/src/renderer/src/lib/terminal-pane-title-sanitization.ts b/src/renderer/src/lib/terminal-pane-title-sanitization.ts index 542562ceca9..14f94c9d166 100644 --- a/src/renderer/src/lib/terminal-pane-title-sanitization.ts +++ b/src/renderer/src/lib/terminal-pane-title-sanitization.ts @@ -2,7 +2,7 @@ import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode, TerminalTab -} from '../../../shared/types' +} from '../../../shared/terminal-tab-types' function collectLeafIds(node: TerminalPaneLayoutNode | null): string[] { if (!node) { diff --git a/src/renderer/src/lib/terminal-quick-command-project-scope.test.ts b/src/renderer/src/lib/terminal-quick-command-project-scope.test.ts index bfde748ee85..4b428bafe50 100644 --- a/src/renderer/src/lib/terminal-quick-command-project-scope.test.ts +++ b/src/renderer/src/lib/terminal-quick-command-project-scope.test.ts @@ -1,7 +1,9 @@ import { describe, expect, it } from 'vitest' import type { ExecutionHostId } from '../../../shared/execution-host' import { projectHostSetupProjectionFromRepos } from '../../../shared/project-host-setup-projection' -import type { ProjectHostSetup, Repo, TerminalQuickCommand } from '../../../shared/types' +import type { ProjectHostSetup } from '../../../shared/project-types' +import type { Repo } from '../../../shared/repo-types' +import type { TerminalQuickCommand } from '../../../shared/terminal-quick-command-types' import { terminalQuickCommandMatchesWorkspaceProject } from './terminal-quick-command-project-scope' type ScopeSetup = Pick diff --git a/src/renderer/src/lib/terminal-quick-command-project-scope.ts b/src/renderer/src/lib/terminal-quick-command-project-scope.ts index 7b48a7b3ada..c7422c0228e 100644 --- a/src/renderer/src/lib/terminal-quick-command-project-scope.ts +++ b/src/renderer/src/lib/terminal-quick-command-project-scope.ts @@ -1,6 +1,7 @@ import type { ExecutionHostId } from '../../../shared/execution-host' import { getTerminalQuickCommandScope } from '../../../shared/terminal-quick-commands' -import type { ProjectHostSetup, TerminalQuickCommand } from '../../../shared/types' +import type { ProjectHostSetup } from '../../../shared/project-types' +import type { TerminalQuickCommand } from '../../../shared/terminal-quick-command-types' type TerminalQuickCommandProjectContext = { commandHostId: ExecutionHostId diff --git a/src/renderer/src/lib/terminal-quick-command-search.test.ts b/src/renderer/src/lib/terminal-quick-command-search.test.ts index 4d295e3c73d..6aab45e4ed2 100644 --- a/src/renderer/src/lib/terminal-quick-command-search.test.ts +++ b/src/renderer/src/lib/terminal-quick-command-search.test.ts @@ -5,7 +5,7 @@ import { TERMINAL_QUICK_COMMAND_SEARCH_QUERY_MAX_BYTES, searchTerminalQuickCommands } from './terminal-quick-command-search' -import type { TerminalQuickCommand } from '../../../shared/types' +import type { TerminalQuickCommand } from '../../../shared/terminal-quick-command-types' const commands: TerminalQuickCommand[] = [ { diff --git a/src/renderer/src/lib/terminal-quick-command-search.ts b/src/renderer/src/lib/terminal-quick-command-search.ts index 8530e915d07..4c780dc3808 100644 --- a/src/renderer/src/lib/terminal-quick-command-search.ts +++ b/src/renderer/src/lib/terminal-quick-command-search.ts @@ -3,7 +3,7 @@ import { isTerminalAgentQuickCommand } from '../../../shared/terminal-quick-commands' import { isClipboardTextByteLengthOverLimit } from '../../../shared/clipboard-text' -import type { TerminalQuickCommand } from '../../../shared/types' +import type { TerminalQuickCommand } from '../../../shared/terminal-quick-command-types' type RankedCommand = { command: TerminalQuickCommand diff --git a/src/renderer/src/lib/terminal-theme.ts b/src/renderer/src/lib/terminal-theme.ts index e6db774a54c..365cb63f7b9 100644 --- a/src/renderer/src/lib/terminal-theme.ts +++ b/src/renderer/src/lib/terminal-theme.ts @@ -1,6 +1,6 @@ import type { ITheme } from '@xterm/xterm' import { getTheme, getThemeNames } from './terminal-themes-data' -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import { makeCustomTerminalThemeSelection, normalizeTerminalCustomThemes, diff --git a/src/renderer/src/lib/unread-badge-count.test.ts b/src/renderer/src/lib/unread-badge-count.test.ts index fdf20dfab30..8d031579522 100644 --- a/src/renderer/src/lib/unread-badge-count.test.ts +++ b/src/renderer/src/lib/unread-badge-count.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { TerminalTab, Worktree } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { Worktree } from '../../../shared/worktree/types' import { getUnreadBadgeCount } from './unread-badge-count' function worktree(id: string, isUnread: boolean): Worktree { diff --git a/src/renderer/src/lib/unread-badge-count.ts b/src/renderer/src/lib/unread-badge-count.ts index 79cd2ffa36b..8ffd7e02e2e 100644 --- a/src/renderer/src/lib/unread-badge-count.ts +++ b/src/renderer/src/lib/unread-badge-count.ts @@ -1,4 +1,5 @@ -import type { TerminalTab, Worktree } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { Worktree } from '../../../shared/worktree/types' export function getUnreadBadgeCount({ worktreesByRepo, diff --git a/src/renderer/src/lib/update-check-click-options.ts b/src/renderer/src/lib/update-check-click-options.ts index 1734c4f1968..ffb7fa99900 100644 --- a/src/renderer/src/lib/update-check-click-options.ts +++ b/src/renderer/src/lib/update-check-click-options.ts @@ -1,4 +1,4 @@ -import type { UpdateCheckOptions } from '../../../shared/types' +import type { UpdateCheckOptions } from '../../../shared/update-status-types' import { getShortcutPlatform } from './shortcut-platform' type UpdateCheckClickEvent = Pick diff --git a/src/renderer/src/lib/use-tab-agent-opencode-native-title.test.ts b/src/renderer/src/lib/use-tab-agent-opencode-native-title.test.ts index abf724a67c1..a527a47758f 100644 --- a/src/renderer/src/lib/use-tab-agent-opencode-native-title.test.ts +++ b/src/renderer/src/lib/use-tab-agent-opencode-native-title.test.ts @@ -6,7 +6,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAppStore } from '@/store' import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume' import { makePaneKey } from '../../../shared/stable-pane-id' -import type { TerminalLayoutSnapshot, TerminalTab, TuiAgent } from '../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' import { parseWorkspaceSession } from '../../../shared/workspace-session-schema' import { resolveTabAgentFromSignals, useTabAgent } from './use-tab-agent' diff --git a/src/renderer/src/lib/use-tab-agent-process-signals.test.ts b/src/renderer/src/lib/use-tab-agent-process-signals.test.ts index e69b27a814b..380d5ba1d91 100644 --- a/src/renderer/src/lib/use-tab-agent-process-signals.test.ts +++ b/src/renderer/src/lib/use-tab-agent-process-signals.test.ts @@ -6,7 +6,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAppStore } from '@/store' import { makePaneKey } from '../../../shared/stable-pane-id' import type { PaneForegroundAgentEntry } from '@/store/slices/pane-foreground-agent' -import type { TerminalTab, TuiAgent } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' import { resolveLaunchedAgentExitEvidence, resolveTabAgentFromSignals, diff --git a/src/renderer/src/lib/use-tab-agent-retained-identity.test.ts b/src/renderer/src/lib/use-tab-agent-retained-identity.test.ts index cccc3e662fe..ddb579a6b72 100644 --- a/src/renderer/src/lib/use-tab-agent-retained-identity.test.ts +++ b/src/renderer/src/lib/use-tab-agent-retained-identity.test.ts @@ -7,7 +7,8 @@ import { useAppStore } from '@/store' import type { RetainedAgentEntry } from '@/store/slices/agent-status' import type { AgentStatusEntry, AgentType } from '../../../shared/agent-status-types' import { makePaneKey } from '../../../shared/stable-pane-id' -import type { TerminalLayoutSnapshot, TerminalTab, TuiAgent } from '../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' import { useTabAgent } from './use-tab-agent' const initialAppState = useAppStore.getInitialState() diff --git a/src/renderer/src/lib/use-tab-agent-sleeping-session.test.ts b/src/renderer/src/lib/use-tab-agent-sleeping-session.test.ts index 3963dd881b9..37c7345771e 100644 --- a/src/renderer/src/lib/use-tab-agent-sleeping-session.test.ts +++ b/src/renderer/src/lib/use-tab-agent-sleeping-session.test.ts @@ -9,7 +9,8 @@ import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume' import { makePaneKey } from '../../../shared/stable-pane-id' -import type { TerminalTab, TuiAgent } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' import { resolveTabAgentFromSignals, useTabAgent } from './use-tab-agent' const initialAppState = useAppStore.getInitialState() diff --git a/src/renderer/src/lib/use-tab-agent.test.ts b/src/renderer/src/lib/use-tab-agent.test.ts index a0bcbea57b1..26dbaf201e9 100644 --- a/src/renderer/src/lib/use-tab-agent.test.ts +++ b/src/renderer/src/lib/use-tab-agent.test.ts @@ -6,7 +6,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAppStore } from '@/store' import type { AgentStatusEntry } from '../../../shared/agent-status-types' import { makePaneKey } from '../../../shared/stable-pane-id' -import type { TerminalLayoutSnapshot, TerminalTab, TuiAgent } from '../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' import { resolveTabAgentFromSignals, useTabAgent } from './use-tab-agent' const initialAppState = useAppStore.getInitialState() diff --git a/src/renderer/src/lib/use-tab-agent.ts b/src/renderer/src/lib/use-tab-agent.ts index 017126a860a..09afd63c43d 100644 --- a/src/renderer/src/lib/use-tab-agent.ts +++ b/src/renderer/src/lib/use-tab-agent.ts @@ -19,7 +19,8 @@ import { import { resolveCompatibleAgentTypeForOwner } from '../../../shared/agent-title-owner' import { isOpenCodeNativeTitle } from '../../../shared/opencode-terminal-title' import { resolvePaneAgentOwner } from '../../../shared/pane-agent-owner' -import type { TerminalTab, TuiAgent } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' // A shell name or the tab's neutral default title (where inferred-interrupt reset parks it); blank titles are no evidence. function titleShowsNoAgent(title: string, defaultTitle?: string): boolean { diff --git a/src/renderer/src/lib/workspace-activation-path-gate.test.ts b/src/renderer/src/lib/workspace-activation-path-gate.test.ts index d8110a747fb..8cec38ffa26 100644 --- a/src/renderer/src/lib/workspace-activation-path-gate.test.ts +++ b/src/renderer/src/lib/workspace-activation-path-gate.test.ts @@ -1,6 +1,7 @@ import { readFile } from 'node:fs/promises' import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { FolderWorkspace, ProjectGroup } from '../../../shared/types' +import type { FolderWorkspace } from '../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../shared/project-group-types' import type { FolderWorkspacePathStatusReason } from '../../../shared/folder-workspace-path-status' import { folderWorkspaceKey } from '../../../shared/workspace-scope' import { resolveWindowShortcutAction } from '../../../shared/window-shortcut-policy' diff --git a/src/renderer/src/lib/workspace-port-localhost-label-selector.ts b/src/renderer/src/lib/workspace-port-localhost-label-selector.ts index 8e996cf6690..c5ac96299b5 100644 --- a/src/renderer/src/lib/workspace-port-localhost-label-selector.ts +++ b/src/renderer/src/lib/workspace-port-localhost-label-selector.ts @@ -1,5 +1,7 @@ import type { LocalhostWorktreeLabelRoute } from '../../../shared/localhost-worktree-labels' -import type { GlobalSettings, Project, Repo } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { Project } from '../../../shared/project-types' +import type { Repo } from '../../../shared/repo-types' import type { WorkspacePort } from '../../../shared/workspace-ports' import { useAppStore } from '@/store' import { localhostWorktreeLabelRouteForPort } from './workspace-port-localhost-label' diff --git a/src/renderer/src/lib/workspace-port-localhost-label.ts b/src/renderer/src/lib/workspace-port-localhost-label.ts index 896af05dde0..c060d8ab374 100644 --- a/src/renderer/src/lib/workspace-port-localhost-label.ts +++ b/src/renderer/src/lib/workspace-port-localhost-label.ts @@ -1,5 +1,7 @@ import type { LocalhostWorktreeLabelRoute } from '../../../shared/localhost-worktree-labels' -import type { GlobalSettings, Project, Repo } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { Project } from '../../../shared/project-types' +import type { Repo } from '../../../shared/repo-types' import type { WorkspacePort } from '../../../shared/workspace-ports' import { browserUrlForPort } from './workspace-port-urls' diff --git a/src/renderer/src/lib/workspace-runtime-host-ownership.ts b/src/renderer/src/lib/workspace-runtime-host-ownership.ts index 1b629a2fdc4..330ea86ad16 100644 --- a/src/renderer/src/lib/workspace-runtime-host-ownership.ts +++ b/src/renderer/src/lib/workspace-runtime-host-ownership.ts @@ -1,4 +1,4 @@ -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' import { parseExecutionHostId, toRuntimeExecutionHostId, diff --git a/src/renderer/src/lib/workspace-session-browser-history.test.ts b/src/renderer/src/lib/workspace-session-browser-history.test.ts index 505110ac427..0ae2df2c872 100644 --- a/src/renderer/src/lib/workspace-session-browser-history.test.ts +++ b/src/renderer/src/lib/workspace-session-browser-history.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { BrowserHistoryEntry } from '../../../shared/types' +import type { BrowserHistoryEntry } from '../../../shared/browser-workspace-types' import { MAX_BROWSER_HISTORY_ENTRIES } from '../../../shared/workspace-session-browser-history' import { buildWorkspaceSessionPayload, type WorkspaceSessionSnapshot } from './workspace-session' diff --git a/src/renderer/src/lib/workspace-session-focus-recency.ts b/src/renderer/src/lib/workspace-session-focus-recency.ts index 2db18208975..bc1ed783e46 100644 --- a/src/renderer/src/lib/workspace-session-focus-recency.ts +++ b/src/renderer/src/lib/workspace-session-focus-recency.ts @@ -1,4 +1,4 @@ -import type { WorkspaceSessionState } from '../../../shared/types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' export function buildLastVisitedAtByWorktreeId(snapshot: { lastVisitedAtByWorktreeId: WorkspaceSessionState['lastVisitedAtByWorktreeId'] diff --git a/src/renderer/src/lib/workspace-session-host-field-ownership.ts b/src/renderer/src/lib/workspace-session-host-field-ownership.ts index db1ffe835bd..1b154a00a1d 100644 --- a/src/renderer/src/lib/workspace-session-host-field-ownership.ts +++ b/src/renderer/src/lib/workspace-session-host-field-ownership.ts @@ -1,4 +1,4 @@ -import type { WorkspaceSessionState } from '../../../shared/types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' export type WorkspaceSessionFieldOwnership = | 'global' diff --git a/src/renderer/src/lib/workspace-session-host-persistence.test.ts b/src/renderer/src/lib/workspace-session-host-persistence.test.ts index d86fc3ba927..29d166d0816 100644 --- a/src/renderer/src/lib/workspace-session-host-persistence.test.ts +++ b/src/renderer/src/lib/workspace-session-host-persistence.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { getDefaultWorkspaceSession } from '../../../shared/constants' -import type { WorkspaceSessionState } from '../../../shared/types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' import { folderWorkspaceKey, worktreeWorkspaceKey } from '../../../shared/workspace-scope' import { buildHostIdByWorktreeId, diff --git a/src/renderer/src/lib/workspace-session-host-persistence.ts b/src/renderer/src/lib/workspace-session-host-persistence.ts index 47a93b4d577..b84b50909f7 100644 --- a/src/renderer/src/lib/workspace-session-host-persistence.ts +++ b/src/renderer/src/lib/workspace-session-host-persistence.ts @@ -1,4 +1,8 @@ -import type { Repo, WorkspaceSessionPatch, WorkspaceSessionState } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' +import type { + WorkspaceSessionPatch, + WorkspaceSessionState +} from '../../../shared/workspace-session-state-types' import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID, diff --git a/src/renderer/src/lib/workspace-session-host-records.ts b/src/renderer/src/lib/workspace-session-host-records.ts index bb34dcde12a..766880817b1 100644 --- a/src/renderer/src/lib/workspace-session-host-records.ts +++ b/src/renderer/src/lib/workspace-session-host-records.ts @@ -1,4 +1,4 @@ -import type { WorkspaceSessionState } from '../../../shared/types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' export type WorkspaceSessionRecord = Record diff --git a/src/renderer/src/lib/workspace-session-host-split.test.ts b/src/renderer/src/lib/workspace-session-host-split.test.ts index 6e1448c076f..a3aff69a888 100644 --- a/src/renderer/src/lib/workspace-session-host-split.test.ts +++ b/src/renderer/src/lib/workspace-session-host-split.test.ts @@ -6,13 +6,10 @@ import { } from './workspace-session-host-split' import { getDefaultWorkspaceSession } from '../../../shared/constants' import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../shared/execution-host' -import type { - BrowserPage, - Tab, - TerminalLayoutSnapshot, - TerminalTab, - WorkspaceSessionState -} from '../../../shared/types' +import type { BrowserPage } from '../../../shared/browser-workspace-types' +import type { Tab } from '../../../shared/tab-types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' const RUNTIME_A: ExecutionHostId = 'runtime:env-a' const RUNTIME_B: ExecutionHostId = 'runtime:env-b' diff --git a/src/renderer/src/lib/workspace-session-host-split.ts b/src/renderer/src/lib/workspace-session-host-split.ts index 43343249589..103a1abcf63 100644 --- a/src/renderer/src/lib/workspace-session-host-split.ts +++ b/src/renderer/src/lib/workspace-session-host-split.ts @@ -1,4 +1,4 @@ -import type { WorkspaceSessionState } from '../../../shared/types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../shared/execution-host' import { GLOBAL_WORKSPACE_SESSION_FIELDS, diff --git a/src/renderer/src/lib/workspace-session-hydration-keys.test.ts b/src/renderer/src/lib/workspace-session-hydration-keys.test.ts index 62f97f7b4c2..54248e45a38 100644 --- a/src/renderer/src/lib/workspace-session-hydration-keys.test.ts +++ b/src/renderer/src/lib/workspace-session-hydration-keys.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { WorkspaceSessionState } from '../../../shared/types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' import { collectFolderWorkspaceKeysFromSession, collectWorktreeHydrationRepoIdsFromSession diff --git a/src/renderer/src/lib/workspace-session-hydration-keys.ts b/src/renderer/src/lib/workspace-session-hydration-keys.ts index d4339e30cb6..bcfce2c9eae 100644 --- a/src/renderer/src/lib/workspace-session-hydration-keys.ts +++ b/src/renderer/src/lib/workspace-session-hydration-keys.ts @@ -1,6 +1,7 @@ import type { ExecutionHostId } from '../../../shared/execution-host' import { parseExecutionHostId } from '../../../shared/execution-host' -import type { WorkspaceKey, WorkspaceSessionState } from '../../../shared/types' +import type { WorkspaceKey } from '../../../shared/folder-workspace-types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' import { parseWorkspaceKey } from '../../../shared/workspace-scope' import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id' diff --git a/src/renderer/src/lib/workspace-session-patch.test.ts b/src/renderer/src/lib/workspace-session-patch.test.ts index da578c1ff42..efe16ca0c5b 100644 --- a/src/renderer/src/lib/workspace-session-patch.test.ts +++ b/src/renderer/src/lib/workspace-session-patch.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import type { WorkspaceSessionSnapshot } from './workspace-session' import { buildWorkspaceSessionPatch } from './workspace-session-patch' diff --git a/src/renderer/src/lib/workspace-session-patch.ts b/src/renderer/src/lib/workspace-session-patch.ts index e88f476a8f6..a5ef39c29db 100644 --- a/src/renderer/src/lib/workspace-session-patch.ts +++ b/src/renderer/src/lib/workspace-session-patch.ts @@ -1,4 +1,7 @@ -import type { WorkspaceSessionPatch, WorkspaceSessionState } from '../../../shared/types' +import type { + WorkspaceSessionPatch, + WorkspaceSessionState +} from '../../../shared/workspace-session-state-types' import { pruneLocalTerminalScrollbackBuffers } from '../../../shared/workspace-session-terminal-buffers' import { normalizeBrowserHistoryEntries } from '../../../shared/workspace-session-browser-history' import { diff --git a/src/renderer/src/lib/workspace-session-reconnect-targets.ts b/src/renderer/src/lib/workspace-session-reconnect-targets.ts index 713e4388e53..1dc2ae717e1 100644 --- a/src/renderer/src/lib/workspace-session-reconnect-targets.ts +++ b/src/renderer/src/lib/workspace-session-reconnect-targets.ts @@ -1,4 +1,4 @@ -import type { WorkspaceSessionState } from '../../../shared/types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' import { parseAppSshPtyId } from '../../../shared/ssh-pty-id' import { isRuntimeOwnedSshTargetId } from '../../../shared/execution-host' import type { WorkspaceSessionSnapshot } from './workspace-session' diff --git a/src/renderer/src/lib/workspace-session-sleeping-agents.ts b/src/renderer/src/lib/workspace-session-sleeping-agents.ts index fc54bc1c524..29ee3d0fe6a 100644 --- a/src/renderer/src/lib/workspace-session-sleeping-agents.ts +++ b/src/renderer/src/lib/workspace-session-sleeping-agents.ts @@ -1,4 +1,4 @@ -import type { WorkspaceSessionState } from '../../../shared/types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' export function buildSleepingAgentSessionData(snapshot: { sleepingAgentSessionsByPaneKey?: WorkspaceSessionState['sleepingAgentSessionsByPaneKey'] diff --git a/src/renderer/src/lib/workspace-session-unified-tabs.ts b/src/renderer/src/lib/workspace-session-unified-tabs.ts index 359bdd8b2a8..38660c2ea3a 100644 --- a/src/renderer/src/lib/workspace-session-unified-tabs.ts +++ b/src/renderer/src/lib/workspace-session-unified-tabs.ts @@ -1,9 +1,5 @@ -import type { - Tab, - TabGroup, - TabGroupLayoutNode, - WorkspaceSessionState -} from '../../../shared/types' +import type { Tab, TabGroup, TabGroupLayoutNode } from '../../../shared/tab-types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' import type { WorkspaceSessionSnapshot } from './workspace-session' type PersistedUnifiedTabSessionData = Pick< diff --git a/src/renderer/src/lib/workspace-session.ts b/src/renderer/src/lib/workspace-session.ts index cfe1bbf3034..aae9ce46eeb 100644 --- a/src/renderer/src/lib/workspace-session.ts +++ b/src/renderer/src/lib/workspace-session.ts @@ -1,10 +1,9 @@ +import type { BrowserPage, BrowserWorkspace } from '../../../shared/browser-workspace-types' +import type { WorkspaceVisibleTabType } from '../../../shared/tab-types' import type { - BrowserPage, - BrowserWorkspace, PersistedOpenFile, - WorkspaceSessionState, - WorkspaceVisibleTabType -} from '../../../shared/types' + WorkspaceSessionState +} from '../../../shared/workspace-session-state-types' import { pruneLocalTerminalScrollbackBuffers } from '../../../shared/workspace-session-terminal-buffers' import { normalizeBrowserHistoryEntries } from '../../../shared/workspace-session-browser-history' import type { AppState } from '../store' diff --git a/src/renderer/src/lib/workspace-tab-palette-search.test.ts b/src/renderer/src/lib/workspace-tab-palette-search.test.ts index a2a7e147bc3..875e5f4573b 100644 --- a/src/renderer/src/lib/workspace-tab-palette-search.test.ts +++ b/src/renderer/src/lib/workspace-tab-palette-search.test.ts @@ -4,7 +4,9 @@ import type { RetainedAgentEntry } from '@/store/slices/agent-status' import type { OpenFile } from '@/store/slices/editor' import type { AgentStatusEntry } from '../../../shared/agent-status-types' import type { SleepingAgentSessionRecord } from '../../../shared/agent-session-resume' -import type { Tab, TabGroup, TerminalTab, Worktree } from '../../../shared/types' +import type { Tab, TabGroup } from '../../../shared/tab-types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { Worktree } from '../../../shared/worktree/types' import { buildSearchableWorkspaceTabs, searchWorkspaceTabs } from './workspace-tab-palette-search' const WT_ROOT = path.join('tmp', 'wt-1') diff --git a/src/renderer/src/lib/workspace-tab-palette-search.ts b/src/renderer/src/lib/workspace-tab-palette-search.ts index 458a9b76240..dd33344cf13 100644 --- a/src/renderer/src/lib/workspace-tab-palette-search.ts +++ b/src/renderer/src/lib/workspace-tab-palette-search.ts @@ -4,7 +4,9 @@ import { resolveTerminalTabTitle, resolveUnifiedTabLabel } from '../../../shared/tab-title-resolution' -import type { Tab, TabContentType, TabGroup, TerminalTab, Worktree } from '../../../shared/types' +import type { Tab, TabContentType, TabGroup } from '../../../shared/tab-types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { Worktree } from '../../../shared/worktree/types' import { buildAgentMetadataTabIndex, collectAgentMetadataFromIndex, diff --git a/src/renderer/src/lib/worktree-activation-automation-filter.test.ts b/src/renderer/src/lib/worktree-activation-automation-filter.test.ts index a6968e414e2..9e07ee425aa 100644 --- a/src/renderer/src/lib/worktree-activation-automation-filter.test.ts +++ b/src/renderer/src/lib/worktree-activation-automation-filter.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' import { useAppStore } from '@/store' import { activateAndRevealWorktree } from './worktree-activation' diff --git a/src/renderer/src/lib/worktree-activation-created-agent-test-state.ts b/src/renderer/src/lib/worktree-activation-created-agent-test-state.ts index 5cd35181547..6c0a50906c9 100644 --- a/src/renderer/src/lib/worktree-activation-created-agent-test-state.ts +++ b/src/renderer/src/lib/worktree-activation-created-agent-test-state.ts @@ -1,6 +1,6 @@ import path from 'node:path' import { vi } from 'vitest' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' import { useAppStore } from '@/store' export function makeCreatedAgentWorktree(): Worktree { diff --git a/src/renderer/src/lib/worktree-activation-empty-remote.test.ts b/src/renderer/src/lib/worktree-activation-empty-remote.test.ts index 49f38e7aa7c..ffa5369a845 100644 --- a/src/renderer/src/lib/worktree-activation-empty-remote.test.ts +++ b/src/renderer/src/lib/worktree-activation-empty-remote.test.ts @@ -1,7 +1,7 @@ import path from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../shared/constants' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' import { resetWebRuntimeWakeTerminalRespawnForTests } from '@/runtime/web-runtime-wake-terminal-respawn' import { resetWebSessionTabsSnapshotFreshnessForTests } from '@/runtime/web-session-tabs-sync' import { useAppStore } from '@/store' diff --git a/src/renderer/src/lib/worktree-activation.test.ts b/src/renderer/src/lib/worktree-activation.test.ts index 923cecd5aed..6223026f301 100644 --- a/src/renderer/src/lib/worktree-activation.test.ts +++ b/src/renderer/src/lib/worktree-activation.test.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines -- Why: these activation cases share one mock store and assert ordering across startup, setup, issue commands, and default tabs. */ import { afterEach, describe, expect, it, vi } from 'vitest' -import type { SetupScriptLaunchMode } from '../../../shared/types' +import type { SetupScriptLaunchMode } from '../../../shared/worktree/launch-types' import { SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV } from '../../../shared/setup-agent-sequencing' import { activateAndRevealWorktree, ensureWorktreeHasInitialTerminal } from './worktree-activation' import { resetHookCommandDelayedDeliveryForTests } from './hook-command-delayed-delivery' diff --git a/src/renderer/src/lib/worktree-activation.ts b/src/renderer/src/lib/worktree-activation.ts index d27a18565ef..cd2f1bd1c5a 100644 --- a/src/renderer/src/lib/worktree-activation.ts +++ b/src/renderer/src/lib/worktree-activation.ts @@ -1,13 +1,13 @@ /* eslint-disable max-lines -- Why: worktree activation is a single ordered flow spanning startup, setup, issue commands, and default tabs; splitting it would obscure sequencing guarantees. */ +import type { FolderWorkspace } from '../../../shared/folder-workspace-types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { Tab } from '../../../shared/tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' import type { - FolderWorkspace, - GlobalSettings, SetupSplitDirection, - Tab, - TuiAgent, WorktreeDefaultTabsLaunch, WorktreeSetupLaunch -} from '../../../shared/types' +} from '../../../shared/worktree/launch-types' import type { EventProps } from '../../../shared/telemetry-events' import type { StartupCommandDelivery } from '../../../shared/codex-startup-delivery' import type { diff --git a/src/renderer/src/lib/worktree-activity-state.test.ts b/src/renderer/src/lib/worktree-activity-state.test.ts index 2aec8c2c648..c0e5855a598 100644 --- a/src/renderer/src/lib/worktree-activity-state.test.ts +++ b/src/renderer/src/lib/worktree-activity-state.test.ts @@ -5,7 +5,7 @@ import { hasActiveWorkspaceActivity, isInactiveWorkspace } from './worktree-activity-state' -import type { TerminalTab } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' import type { AgentStatusEntry } from '../../../shared/agent-status-types' const NOW = 10_000_000 diff --git a/src/renderer/src/lib/worktree-activity-state.ts b/src/renderer/src/lib/worktree-activity-state.ts index 827e6c91ceb..2f6e804dcc7 100644 --- a/src/renderer/src/lib/worktree-activity-state.ts +++ b/src/renderer/src/lib/worktree-activity-state.ts @@ -1,5 +1,5 @@ import { tabHasLivePty } from '@/lib/tab-has-live-pty' -import type { TerminalTab } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' import { isFreshNonDoneAgentStatus, type AgentStatusEntry diff --git a/src/renderer/src/lib/worktree-attachment-label.ts b/src/renderer/src/lib/worktree-attachment-label.ts index e79a83521f1..ac9998e16f0 100644 --- a/src/renderer/src/lib/worktree-attachment-label.ts +++ b/src/renderer/src/lib/worktree-attachment-label.ts @@ -1,4 +1,4 @@ -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' import { basename } from './path' /** Shared by every "workspace attached to this work item" surface so GitHub, GitLab, diff --git a/src/renderer/src/lib/worktree-creation-agent-seeds.ts b/src/renderer/src/lib/worktree-creation-agent-seeds.ts index 295b5e5c06c..6abf478bd28 100644 --- a/src/renderer/src/lib/worktree-creation-agent-seeds.ts +++ b/src/renderer/src/lib/worktree-creation-agent-seeds.ts @@ -9,7 +9,7 @@ import { nativeChatRequiresLocalTranscript } from '@/lib/native-chat-supported-a import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' import { toWebTerminalSurfaceTabId } from '@/runtime/web-terminal-surface-id' import type { WorktreeCreationRequest } from '@/lib/pending-worktree-creation' -import type { TuiAgent } from '../../../shared/types' +import type { TuiAgent } from '../../../shared/tui-agent' type AppStoreSnapshot = ReturnType diff --git a/src/renderer/src/lib/worktree-creation-flow.ts b/src/renderer/src/lib/worktree-creation-flow.ts index e8703256ceb..535a1870fe8 100644 --- a/src/renderer/src/lib/worktree-creation-flow.ts +++ b/src/renderer/src/lib/worktree-creation-flow.ts @@ -18,7 +18,7 @@ import { formatWorkspaceCreateError, getWorkspaceCreateErrorToastMessage } from '@/lib/workspace-create-error-format' -import type { CreateWorktreeResult } from '../../../shared/types' +import type { CreateWorktreeResult } from '../../../shared/worktree/create-types' import { findPendingLinkedWorkItemCreationId, type WorktreeCreationPhase, diff --git a/src/renderer/src/lib/worktree-default-display-name.ts b/src/renderer/src/lib/worktree-default-display-name.ts index 765b6d084a1..1214036ce30 100644 --- a/src/renderer/src/lib/worktree-default-display-name.ts +++ b/src/renderer/src/lib/worktree-default-display-name.ts @@ -1,6 +1,6 @@ import { branchName } from '@/lib/git-utils' import { basename } from '@/lib/path' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' type WorktreeDisplayNameSource = Pick diff --git a/src/renderer/src/lib/worktree-display-name-order.test.ts b/src/renderer/src/lib/worktree-display-name-order.test.ts index 9c5875ba821..70477ca8454 100644 --- a/src/renderer/src/lib/worktree-display-name-order.test.ts +++ b/src/renderer/src/lib/worktree-display-name-order.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' import { compareWorktreeDisplayName } from './worktree-display-name-order' // displayName is typed `string`, but crash 99657ab1 proved it arrives undefined diff --git a/src/renderer/src/lib/worktree-display-name-order.ts b/src/renderer/src/lib/worktree-display-name-order.ts index 1b5bab8f4db..738db84d123 100644 --- a/src/renderer/src/lib/worktree-display-name-order.ts +++ b/src/renderer/src/lib/worktree-display-name-order.ts @@ -1,4 +1,4 @@ -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' // Why: displayName is typed non-optional but arrives undefined at runtime for // persisted/discovered worktrees (crash 99657ab1); coalesce so it can't throw. diff --git a/src/renderer/src/lib/worktree-operation-route.test.ts b/src/renderer/src/lib/worktree-operation-route.test.ts index 8f2ef74d3b5..98878845e22 100644 --- a/src/renderer/src/lib/worktree-operation-route.test.ts +++ b/src/renderer/src/lib/worktree-operation-route.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' import { resolveWorktreeOperationRouteResult } from './worktree-operation-route' const WORKTREE_ID = 'repo-1::/srv/worktree' diff --git a/src/renderer/src/lib/worktree-palette-gitlab-url-match.ts b/src/renderer/src/lib/worktree-palette-gitlab-url-match.ts index a40c330208f..6fd291a7576 100644 --- a/src/renderer/src/lib/worktree-palette-gitlab-url-match.ts +++ b/src/renderer/src/lib/worktree-palette-gitlab-url-match.ts @@ -9,7 +9,8 @@ import { parseGitLabIssueOrMRLink, type ProjectSlug } from '../../../shared/new-workspace/gitlab-links' -import type { Repo, Worktree } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' +import type { Worktree } from '../../../shared/worktree/types' export type GitLabIssueOrMRLink = NonNullable> diff --git a/src/renderer/src/lib/worktree-palette-review-match.test.ts b/src/renderer/src/lib/worktree-palette-review-match.test.ts index c72710d2931..7c702522ed1 100644 --- a/src/renderer/src/lib/worktree-palette-review-match.test.ts +++ b/src/renderer/src/lib/worktree-palette-review-match.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from 'vitest' import { matchWorktreePaletteReview } from './worktree-palette-review-match' import { searchWorktrees } from './worktree-palette-search' -import type { Repo, Worktree } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' +import type { Worktree } from '../../../shared/worktree/types' import type { HostedReviewInfo } from '../../../shared/hosted-review' // Regression tests for the production crash (report c5d87873, macOS, Orca 1.4.147): diff --git a/src/renderer/src/lib/worktree-palette-search.test.ts b/src/renderer/src/lib/worktree-palette-search.test.ts index 6e0fa1848f9..2484600d666 100644 --- a/src/renderer/src/lib/worktree-palette-search.test.ts +++ b/src/renderer/src/lib/worktree-palette-search.test.ts @@ -4,7 +4,8 @@ import { WORKTREE_PALETTE_QUERY_MAX_BYTES, isWorktreePaletteQueryTooLarge } from './worktree-palette-query-bounds' -import type { Repo, Worktree } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' +import type { Worktree } from '../../../shared/worktree/types' import type { HostedReviewInfo } from '../../../shared/hosted-review' function makeWorktree(overrides: Partial = {}): Worktree { diff --git a/src/renderer/src/lib/worktree-palette-search.ts b/src/renderer/src/lib/worktree-palette-search.ts index a2fa082456f..7a90aced676 100644 --- a/src/renderer/src/lib/worktree-palette-search.ts +++ b/src/renderer/src/lib/worktree-palette-search.ts @@ -4,7 +4,8 @@ import { resolveWorktreeDisplayName } from './worktree-default-display-name' import type { HostedReviewInfo } from '../../../shared/hosted-review' -import type { Repo, Worktree } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' +import type { Worktree } from '../../../shared/worktree/types' import { extractWorktreePaletteCommentSnippet } from './worktree-palette-comment-snippet' import { isWorktreePaletteQueryTooLarge } from './worktree-palette-query-bounds' import { matchWorktreePaletteReview } from './worktree-palette-review-match' diff --git a/src/renderer/src/lib/worktree-palette-task-url-match.test.ts b/src/renderer/src/lib/worktree-palette-task-url-match.test.ts index 88691ac8bb6..0ccdf75d931 100644 --- a/src/renderer/src/lib/worktree-palette-task-url-match.test.ts +++ b/src/renderer/src/lib/worktree-palette-task-url-match.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { Repo, Worktree } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' +import type { Worktree } from '../../../shared/worktree/types' import { getCmdJTaskUrlCreatePreview, matchWorktreePaletteTaskUrl, diff --git a/src/renderer/src/lib/worktree-palette-task-url-match.ts b/src/renderer/src/lib/worktree-palette-task-url-match.ts index d16d135dcb1..854ba244c71 100644 --- a/src/renderer/src/lib/worktree-palette-task-url-match.ts +++ b/src/renderer/src/lib/worktree-palette-task-url-match.ts @@ -14,7 +14,8 @@ import { githubRepoIdentityKey } from '../../../shared/github/repository-identit import { parseGitLabIssueOrMRLink } from '../../../shared/new-workspace/gitlab-links' import { parseJiraIssueUrl, type ParsedJiraIssueUrl } from '../../../shared/jira-issue-url' import { parseLinearIssueUrlIntent, type LinearIssueUrlIntent } from '../../../shared/linear/links' -import type { Repo, Worktree } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' +import type { Worktree } from '../../../shared/worktree/types' import { normalizeLinearIdentifier } from './linear-issue-workspace-attachment' import { worktreeMatchesGitLabUrl, diff --git a/src/renderer/src/lib/worktree-runtime-owner-index.ts b/src/renderer/src/lib/worktree-runtime-owner-index.ts index 6a66d7e33f4..047976b4587 100644 --- a/src/renderer/src/lib/worktree-runtime-owner-index.ts +++ b/src/renderer/src/lib/worktree-runtime-owner-index.ts @@ -1,4 +1,7 @@ -import type { FolderWorkspace, ProjectGroup, Repo, Worktree } from '../../../shared/types' +import type { FolderWorkspace } from '../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../shared/project-group-types' +import type { Repo } from '../../../shared/repo-types' +import type { Worktree } from '../../../shared/worktree/types' import { getRepoExecutionHostId, parseExecutionHostId, diff --git a/src/renderer/src/lib/worktree-runtime-owner-state.ts b/src/renderer/src/lib/worktree-runtime-owner-state.ts index 6e65f7a1bcf..ee4da2d1569 100644 --- a/src/renderer/src/lib/worktree-runtime-owner-state.ts +++ b/src/renderer/src/lib/worktree-runtime-owner-state.ts @@ -1,11 +1,9 @@ import type { ExecutionHostId } from '../../../shared/execution-host' -import type { - FolderWorkspace, - GlobalSettings, - ProjectGroup, - Repo, - Worktree -} from '../../../shared/types' +import type { FolderWorkspace } from '../../../shared/folder-workspace-types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { ProjectGroup } from '../../../shared/project-group-types' +import type { Repo } from '../../../shared/repo-types' +import type { Worktree } from '../../../shared/worktree/types' export type WorktreeRuntimeOwnerState = { repos?: readonly Pick[] diff --git a/src/renderer/src/lib/worktree-runtime-owner.ts b/src/renderer/src/lib/worktree-runtime-owner.ts index 999ba347c6d..473b306eeaf 100644 --- a/src/renderer/src/lib/worktree-runtime-owner.ts +++ b/src/renderer/src/lib/worktree-runtime-owner.ts @@ -1,6 +1,7 @@ import { getRepoExecutionHostId, parseExecutionHostId } from '../../../shared/execution-host' import type { ExecutionHostId } from '../../../shared/execution-host' -import type { GlobalSettings, Worktree } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { Worktree } from '../../../shared/worktree/types' import { parseWorkspaceKey } from '../../../shared/workspace-scope' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' import { getRepoIdFromWorktreeId } from '@/store/slices/worktree-helpers' diff --git a/src/renderer/src/lib/worktree-status-spinner-launch-agent.test.ts b/src/renderer/src/lib/worktree-status-spinner-launch-agent.test.ts index 34bc7916164..78b71ef25da 100644 --- a/src/renderer/src/lib/worktree-status-spinner-launch-agent.test.ts +++ b/src/renderer/src/lib/worktree-status-spinner-launch-agent.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { buildTitleDerivedAgentRows } from '@/components/sidebar/worktree-title-derived-agent-rows' -import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types' import { getWorktreeStatus } from './worktree-status' const LEAF_ID = '11111111-1111-4111-8111-111111111111' diff --git a/src/renderer/src/lib/worktree-status-terminal-layout-roots.test.ts b/src/renderer/src/lib/worktree-status-terminal-layout-roots.test.ts index 906a44675cb..f66ecfea04d 100644 --- a/src/renderer/src/lib/worktree-status-terminal-layout-roots.test.ts +++ b/src/renderer/src/lib/worktree-status-terminal-layout-roots.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalPaneLayoutNode } from '../../../shared/types' +import type { TerminalPaneLayoutNode } from '../../../shared/terminal-tab-types' import { resolveWorktreeStatus } from './worktree-status' const LEAF_ID_1 = '11111111-1111-4111-8111-111111111111' diff --git a/src/renderer/src/lib/worktree-status.test.ts b/src/renderer/src/lib/worktree-status.test.ts index be406bace19..bdea2105785 100644 --- a/src/renderer/src/lib/worktree-status.test.ts +++ b/src/renderer/src/lib/worktree-status.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalLayoutSnapshot } from '../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../shared/terminal-tab-types' import { getWorktreeStatus, getWorktreeStatusLabel, resolveWorktreeStatus } from './worktree-status' const LEAF_ID_1 = '11111111-1111-4111-8111-111111111111' diff --git a/src/renderer/src/lib/worktree-status.ts b/src/renderer/src/lib/worktree-status.ts index 6f39d8c1e0b..91470f75409 100644 --- a/src/renderer/src/lib/worktree-status.ts +++ b/src/renderer/src/lib/worktree-status.ts @@ -6,9 +6,9 @@ import { containsAgentSpinnerGlyph } from '../../../shared/agent-title-core' import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode, - TerminalTab, - TuiAgent -} from '../../../shared/types' + TerminalTab +} from '../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' import type { LiveAgentWorktreeStatus } from './worktree-activity-state' export type WorktreeStatus = 'active' | 'working' | 'permission' | 'done' | 'inactive' diff --git a/src/renderer/src/popout.tsx b/src/renderer/src/popout.tsx index 15f1be88fb6..c9b8e762d6b 100644 --- a/src/renderer/src/popout.tsx +++ b/src/renderer/src/popout.tsx @@ -13,7 +13,7 @@ import { buildAppFontFamily } from './lib/app-font-family' import { I18nProvider } from './i18n/I18nProvider' import { translate } from './i18n/i18n' import { useAppStore } from './store' -import type { GlobalSettings } from '../../shared/types' +import type { GlobalSettings } from '../../shared/global-settings-types' import { getOrCreateRendererRoot } from './lib/react-renderer-root' // Why: the pop-out window is a separate BrowserWindow with its own React root, diff --git a/src/renderer/src/runtime/close-mirrored-editor-tab.ts b/src/renderer/src/runtime/close-mirrored-editor-tab.ts index 749f7b8a642..3897d9ce2b6 100644 --- a/src/renderer/src/runtime/close-mirrored-editor-tab.ts +++ b/src/renderer/src/runtime/close-mirrored-editor-tab.ts @@ -5,7 +5,7 @@ import { import { recordWebSessionCloseIntent } from './web-session-close-intent' import { toHostSessionTabId } from '../../../shared/terminal-surface-id' import type { OpenFile } from '@/store/slices/editor' -import type { Tab } from '../../../shared/types' +import type { Tab } from '../../../shared/tab-types' export type MirroredEditorCloseState = WorktreeRuntimeOwnerState & { openFiles: readonly OpenFile[] diff --git a/src/renderer/src/runtime/file-explorer-delete-owner-provenance.test.ts b/src/renderer/src/runtime/file-explorer-delete-owner-provenance.test.ts index 20a325a5d7d..320b736ac4e 100644 --- a/src/renderer/src/runtime/file-explorer-delete-owner-provenance.test.ts +++ b/src/renderer/src/runtime/file-explorer-delete-owner-provenance.test.ts @@ -2,7 +2,10 @@ import { act, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { FolderWorkspace, ProjectGroup, Repo, Worktree } from '../../../shared/types' +import type { FolderWorkspace } from '../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../shared/project-group-types' +import type { Repo } from '../../../shared/repo-types' +import type { Worktree } from '../../../shared/worktree/types' import { folderWorkspaceKey } from '../../../shared/workspace-scope' import { useAppStore } from '@/store' import { useFileDeletion } from '@/components/right-sidebar/useFileDeletion' diff --git a/src/renderer/src/runtime/gitlab-job-trace-client.test.ts b/src/renderer/src/runtime/gitlab-job-trace-client.test.ts index 10ecd88884c..37bba45f66c 100644 --- a/src/renderer/src/runtime/gitlab-job-trace-client.test.ts +++ b/src/renderer/src/runtime/gitlab-job-trace-client.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { PRCheckDetail } from '../../../shared/types' +import type { PRCheckDetail } from '../../../shared/github/check-types' import { loadGitLabJobLogDetails } from './gitlab-job-trace-client' const callRuntimeRpc = vi.hoisted(() => vi.fn()) diff --git a/src/renderer/src/runtime/gitlab-job-trace-client.ts b/src/renderer/src/runtime/gitlab-job-trace-client.ts index 929c8581c1f..597b476dc05 100644 --- a/src/renderer/src/runtime/gitlab-job-trace-client.ts +++ b/src/renderer/src/runtime/gitlab-job-trace-client.ts @@ -4,7 +4,7 @@ import { gitLabJobTraceToCheckRunDetails } from '../../../shared/gitlab-job-trace-check-details' import type { GitLabJobTraceResult, GitLabProjectRef } from '../../../shared/gitlab-types' -import type { PRCheckDetail, PRCheckRunDetails } from '../../../shared/types' +import type { PRCheckDetail, PRCheckRunDetails } from '../../../shared/github/check-types' import { getActiveRuntimeTarget } from './runtime-client-target' import { callRuntimeRpc } from './runtime-rpc-client' diff --git a/src/renderer/src/runtime/local-jira-search-cancellation.ts b/src/renderer/src/runtime/local-jira-search-cancellation.ts index c17d0199b63..37524987790 100644 --- a/src/renderer/src/runtime/local-jira-search-cancellation.ts +++ b/src/renderer/src/runtime/local-jira-search-cancellation.ts @@ -1,4 +1,4 @@ -import type { JiraIssue, JiraSiteSelection } from '../../../shared/types' +import type { JiraIssue, JiraSiteSelection } from '../../../shared/jira-types' import { createBrowserUuid } from '@/lib/browser-uuid' type LocalJiraSearchArgs = { jql: string; limit?: number; siteId?: JiraSiteSelection } diff --git a/src/renderer/src/runtime/remote-host-file-delete-repro.test.ts b/src/renderer/src/runtime/remote-host-file-delete-repro.test.ts index a9c144499c9..b91f61b48cd 100644 --- a/src/renderer/src/runtime/remote-host-file-delete-repro.test.ts +++ b/src/renderer/src/runtime/remote-host-file-delete-repro.test.ts @@ -2,7 +2,9 @@ import { act, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { FolderWorkspace, ProjectGroup, Repo } from '../../../shared/types' +import type { FolderWorkspace } from '../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../shared/project-group-types' +import type { Repo } from '../../../shared/repo-types' import { useAppStore } from '@/store' import { folderWorkspaceKey } from '../../../shared/workspace-scope' import { useFileDeletion } from '@/components/right-sidebar/useFileDeletion' diff --git a/src/renderer/src/runtime/remote-host-file-open-repro.test.ts b/src/renderer/src/runtime/remote-host-file-open-repro.test.ts index 8359a602921..1c35465d5e4 100644 --- a/src/renderer/src/runtime/remote-host-file-open-repro.test.ts +++ b/src/renderer/src/runtime/remote-host-file-open-repro.test.ts @@ -21,7 +21,7 @@ * latches permanently even after the SSH repo finishes hydrating. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' import { useAppStore } from '@/store' import { getConnectionIdForFile, isWorktreeConnectionResolved } from '@/lib/connection-context' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' diff --git a/src/renderer/src/runtime/remote-server-parity.test.ts b/src/renderer/src/runtime/remote-server-parity.test.ts index 41318a7248e..3c8b2fb62bb 100644 --- a/src/renderer/src/runtime/remote-server-parity.test.ts +++ b/src/renderer/src/runtime/remote-server-parity.test.ts @@ -15,7 +15,7 @@ import { beforeEach, describe, expect, it } from 'vitest' import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types' import { toWebTerminalSurfaceTabId } from '../../../shared/terminal-surface-id' -import type { Tab } from '../../../shared/types' +import type { Tab } from '../../../shared/tab-types' import { applyFreshWebSessionTabsSnapshot, applyWebSessionTabsSnapshot, diff --git a/src/renderer/src/runtime/remote-server-update-coordinator.ts b/src/renderer/src/runtime/remote-server-update-coordinator.ts index 196699b6eed..2e8d729bd79 100644 --- a/src/renderer/src/runtime/remote-server-update-coordinator.ts +++ b/src/renderer/src/runtime/remote-server-update-coordinator.ts @@ -13,7 +13,7 @@ import type { } from '../../../shared/remote-server-update' import type { PublicKnownRuntimeEnvironment } from '../../../shared/runtime-environments' import type { RuntimeStatus } from '../../../shared/runtime-types' -import type { UpdateCheckOptions } from '../../../shared/types' +import type { UpdateCheckOptions } from '../../../shared/update-status-types' import { waitForReplacementRuntime } from './remote-server-restart-wait' import { remoteServerUpdateErrorMessage } from './remote-server-update-errors' import { pollRemoteServerUpdater } from './remote-server-updater-polling' diff --git a/src/renderer/src/runtime/remote-terminal-layout-resolution.test.ts b/src/renderer/src/runtime/remote-terminal-layout-resolution.test.ts index ce2510771c7..a47d09350ad 100644 --- a/src/renderer/src/runtime/remote-terminal-layout-resolution.test.ts +++ b/src/renderer/src/runtime/remote-terminal-layout-resolution.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { TerminalPaneLayoutNode } from '../../../shared/types' +import type { TerminalPaneLayoutNode } from '../../../shared/terminal-tab-types' import { layoutCoversLeaves, resolveTerminalLayoutRoot } from './remote-terminal-layout-resolution' const verticalSplit: TerminalPaneLayoutNode = { diff --git a/src/renderer/src/runtime/remote-terminal-layout-resolution.ts b/src/renderer/src/runtime/remote-terminal-layout-resolution.ts index 7070f55b57c..1691132227b 100644 --- a/src/renderer/src/runtime/remote-terminal-layout-resolution.ts +++ b/src/renderer/src/runtime/remote-terminal-layout-resolution.ts @@ -1,4 +1,4 @@ -import type { TerminalPaneLayoutNode } from '../../../shared/types' +import type { TerminalPaneLayoutNode } from '../../../shared/terminal-tab-types' /** * Single source of truth for turning a set of terminal leaves into a layout diff --git a/src/renderer/src/runtime/runtime-client-target.ts b/src/renderer/src/runtime/runtime-client-target.ts index 6e553445401..1a0b8e4b8a4 100644 --- a/src/renderer/src/runtime/runtime-client-target.ts +++ b/src/renderer/src/runtime/runtime-client-target.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' export type RuntimeClientTarget = { kind: 'local' } | { kind: 'environment'; environmentId: string } diff --git a/src/renderer/src/runtime/runtime-file-client.ts b/src/renderer/src/runtime/runtime-file-client.ts index 788c1751359..e6df50d1f5b 100644 --- a/src/renderer/src/runtime/runtime-file-client.ts +++ b/src/renderer/src/runtime/runtime-file-client.ts @@ -1,14 +1,13 @@ /* eslint-disable max-lines -- Why: this client intentionally centralizes the file preload API plus remote runtime fallbacks so call sites cannot drift on local-vs-environment routing rules. */ +import type { SearchOptions, SearchResult } from '../../../shared/code-search-types' import type { DirEntry, FsChangedPayload, - GlobalSettings, - MarkdownDocument, - SearchOptions, - SearchResult -} from '../../../shared/types' + MarkdownDocument +} from '../../../shared/filesystem-entry-types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import type { RuntimeFilePreviewResult, RuntimeFileReadChunkResult, diff --git a/src/renderer/src/runtime/runtime-file-search-bounds.ts b/src/renderer/src/runtime/runtime-file-search-bounds.ts index b7823f783fd..6dd99e3c97c 100644 --- a/src/renderer/src/runtime/runtime-file-search-bounds.ts +++ b/src/renderer/src/runtime/runtime-file-search-bounds.ts @@ -1,4 +1,4 @@ -import type { SearchOptions, SearchResult } from '../../../shared/types' +import type { SearchOptions, SearchResult } from '../../../shared/code-search-types' import { isUtf8ByteLengthWithinLimit } from '../../../shared/utf8-byte-limits' export const RUNTIME_FILE_SEARCH_TEXT_MAX_BYTES = 8 * 1024 diff --git a/src/renderer/src/runtime/runtime-git-client.ts b/src/renderer/src/runtime/runtime-git-client.ts index ae5d2d2156d..eeb31ab3124 100644 --- a/src/renderer/src/runtime/runtime-git-client.ts +++ b/src/renderer/src/runtime/runtime-git-client.ts @@ -4,16 +4,17 @@ of reimplementing local-vs-environment branching per operation. */ import type { GitBranchCompareResult, GitCommitCompareResult, + GitDiffResult +} from '../../../shared/git-diff-compare-types' +import type { GitForkSyncExpectedUpstream, GitForkSyncResult } from '../../../shared/git-fork-sync' +import type { GitConflictOperation, - GitDiffResult, - GitForkSyncExpectedUpstream, - GitForkSyncResult, - GitPushTarget, GitStagingArea, GitStatusResult, - GitUpstreamStatus, - GlobalSettings -} from '../../../shared/types' + GitUpstreamStatus +} from '../../../shared/git-status-types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { GitPushTarget } from '../../../shared/worktree/types' import type { CommitMessageAgentCapability, CommitMessageModelCapability diff --git a/src/renderer/src/runtime/runtime-hooks-client.ts b/src/renderer/src/runtime/runtime-hooks-client.ts index 61f67a6b8a0..ee70da86b3c 100644 --- a/src/renderer/src/runtime/runtime-hooks-client.ts +++ b/src/renderer/src/runtime/runtime-hooks-client.ts @@ -1,4 +1,5 @@ -import type { GlobalSettings, OrcaHooks } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { OrcaHooks } from '../../../shared/orca-yaml-hook-types' import { parseExecutionHostId, type ExecutionHostId } from '../../../shared/execution-host' import type { SetupScriptImportCandidate } from '../../../shared/setup-script-imports' import { callRuntimeRpc, getActiveRuntimeTarget } from './runtime-rpc-client' diff --git a/src/renderer/src/runtime/runtime-jira-client.ts b/src/renderer/src/runtime/runtime-jira-client.ts index 708addc5fcf..157b7b33e75 100644 --- a/src/renderer/src/runtime/runtime-jira-client.ts +++ b/src/renderer/src/runtime/runtime-jira-client.ts @@ -17,7 +17,7 @@ import type { JiraTransition, JiraUser, JiraViewer -} from '../../../shared/types' +} from '../../../shared/jira-types' import { searchLocalJiraIssues } from './local-jira-search-cancellation' import { callRuntimeRpc, RuntimeRpcCallError } from './runtime-rpc-client' import { isRuntimeProviderSearchQueryWithinLimit } from './runtime-provider-search-bounds' diff --git a/src/renderer/src/runtime/runtime-jira-summary-client.ts b/src/renderer/src/runtime/runtime-jira-summary-client.ts index 48f8ffbf46d..3276a5765dc 100644 --- a/src/renderer/src/runtime/runtime-jira-summary-client.ts +++ b/src/renderer/src/runtime/runtime-jira-summary-client.ts @@ -1,4 +1,4 @@ -import type { JiraConnectionStatus, JiraIssue } from '../../../shared/types' +import type { JiraConnectionStatus, JiraIssue } from '../../../shared/jira-types' import { createBrowserUuid } from '@/lib/browser-uuid' import { callRuntimeRpc } from './runtime-rpc-client' import { getJiraRuntimeTarget, type RuntimeJiraSettings } from './runtime-jira-target' diff --git a/src/renderer/src/runtime/runtime-jira-target.ts b/src/renderer/src/runtime/runtime-jira-target.ts index b00a37d27ef..610ac82c0b1 100644 --- a/src/renderer/src/runtime/runtime-jira-target.ts +++ b/src/renderer/src/runtime/runtime-jira-target.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import { getTaskSourceRuntimeSettings, type TaskSourceContext diff --git a/src/renderer/src/runtime/runtime-linear-client.ts b/src/renderer/src/runtime/runtime-linear-client.ts index 94d6be6197c..d960923075e 100644 --- a/src/renderer/src/runtime/runtime-linear-client.ts +++ b/src/renderer/src/runtime/runtime-linear-client.ts @@ -1,24 +1,25 @@ /* eslint-disable max-lines -- Why: the renderer Linear client mirrors the preload/RPC Linear namespace so local and remote runtime routing stays in one auditable boundary. */ +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { LinearIssueUpdate } from '../../../shared/issue-mutation-types' +import type { LinearComment, LinearIssue } from '../../../shared/linear/issue-types' import type { - GlobalSettings, - LinearComment, - LinearCollectionResult, - LinearConnectionStatus, LinearCustomViewModel, LinearCustomViewSummary, - LinearIssue, - LinearIssueUpdate, + LinearProjectDetail, + LinearProjectSummary +} from '../../../shared/linear/project-types' +import type { + LinearCollectionResult, + LinearConnectionStatus, LinearLabel, LinearMember, - LinearProjectDetail, - LinearProjectSummary, LinearTeam, LinearViewer, - LinearWorkspaceSelection, - LinearWorkflowState -} from '../../../shared/types' + LinearWorkflowState, + LinearWorkspaceSelection +} from '../../../shared/linear/workspace-types' import { callRuntimeRpc, getActiveRuntimeTarget, diff --git a/src/renderer/src/runtime/runtime-provider-accounts-client.test.ts b/src/renderer/src/runtime/runtime-provider-accounts-client.test.ts index 2bd3f65e8d9..decdaf86e8a 100644 --- a/src/renderer/src/runtime/runtime-provider-accounts-client.test.ts +++ b/src/renderer/src/runtime/runtime-provider-accounts-client.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { ClaudeRateLimitAccountsState, CodexRateLimitAccountsState -} from '../../../shared/types' +} from '../../../shared/managed-account-types' import { fetchProviderAccountsSnapshot, removeClaudeProviderAccount, diff --git a/src/renderer/src/runtime/runtime-provider-accounts-client.ts b/src/renderer/src/runtime/runtime-provider-accounts-client.ts index 6a43ddf8a6e..2620a55cf2f 100644 --- a/src/renderer/src/runtime/runtime-provider-accounts-client.ts +++ b/src/renderer/src/runtime/runtime-provider-accounts-client.ts @@ -1,8 +1,8 @@ +import type { GlobalSettings } from '../../../shared/global-settings-types' import type { ClaudeRateLimitAccountsState, - CodexRateLimitAccountsState, - GlobalSettings -} from '../../../shared/types' + CodexRateLimitAccountsState +} from '../../../shared/managed-account-types' import type { RateLimitState } from '../../../shared/rate-limit-types' import type { RuntimeRpcResponse } from '../../../shared/runtime-rpc-envelope' import { callRuntimeRpc, getActiveRuntimeTarget, RuntimeRpcCallError } from './runtime-rpc-client' diff --git a/src/renderer/src/runtime/runtime-repo-client.ts b/src/renderer/src/runtime/runtime-repo-client.ts index 6a916d1c67c..67f9a24d0cc 100644 --- a/src/renderer/src/runtime/runtime-repo-client.ts +++ b/src/renderer/src/runtime/runtime-repo-client.ts @@ -1,4 +1,5 @@ -import type { BaseRefSearchResult, GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { BaseRefSearchResult } from '../../../shared/repo-types' import { legacyBaseRefSearchResult } from '../../../shared/base-ref-search-result' import { callRuntimeRpc, getActiveRuntimeTarget } from './runtime-rpc-client' import { isRuntimeRepoRefSearchQueryWithinLimit } from './runtime-repo-search-bounds' diff --git a/src/renderer/src/runtime/runtime-server-directory-browser.ts b/src/renderer/src/runtime/runtime-server-directory-browser.ts index b28e7755f16..b420bd70312 100644 --- a/src/renderer/src/runtime/runtime-server-directory-browser.ts +++ b/src/renderer/src/runtime/runtime-server-directory-browser.ts @@ -1,4 +1,4 @@ -import type { DirEntry, FilesystemPathFlavor } from '../../../shared/types' +import type { DirEntry, FilesystemPathFlavor } from '../../../shared/filesystem-entry-types' import { sortDirEntries } from '../../../shared/file-name-sort' import { callRuntimeRpc } from './runtime-rpc-client' diff --git a/src/renderer/src/runtime/runtime-terminal-inspection.ts b/src/renderer/src/runtime/runtime-terminal-inspection.ts index 741d381e11f..29be1aed435 100644 --- a/src/renderer/src/runtime/runtime-terminal-inspection.ts +++ b/src/renderer/src/runtime/runtime-terminal-inspection.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import type { RuntimeTerminalSend } from '../../../shared/runtime-types' import { makePaneKey } from '../../../shared/stable-pane-id' import { isTerminalInputTooLargeWithDeferredMeasurement } from '../../../shared/terminal-input' diff --git a/src/renderer/src/runtime/runtime-terminal-stream.ts b/src/renderer/src/runtime/runtime-terminal-stream.ts index af5ffdd6f97..327e8d3e904 100644 --- a/src/renderer/src/runtime/runtime-terminal-stream.ts +++ b/src/renderer/src/runtime/runtime-terminal-stream.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../shared/types' +import type { GlobalSettings } from '../../../shared/global-settings-types' import { RuntimeRpcCallError, getActiveRuntimeTarget } from './runtime-rpc-client' import { getRemoteRuntimeTerminalMultiplexer } from './remote-runtime-terminal-multiplexer' diff --git a/src/renderer/src/runtime/sync-runtime-graph-automation-leaf.test.ts b/src/renderer/src/runtime/sync-runtime-graph-automation-leaf.test.ts index 1d01c5689d6..eb5767ba854 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-automation-leaf.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-automation-leaf.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import type { RuntimeSyncWindowGraph } from '../../../shared/runtime-types' import type { AppState } from '../store/types' -import type { TerminalTab } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' // Why: Part B publishes never-mounted background automation tabs into the // runtime graph, gated on a live eager buffer. Stub the eager-buffer lookup so diff --git a/src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts b/src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts index f23a0f04a4a..fc000ae31b8 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts @@ -9,7 +9,7 @@ import { setRuntimeGraphSyncEnabled } from './sync-runtime-graph' import type { AppState } from '../store/types' -import type { TerminalTab } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' function makeState(overrides: Partial = {}): AppState { return { diff --git a/src/renderer/src/runtime/sync-runtime-graph.ts b/src/renderer/src/runtime/sync-runtime-graph.ts index 52872628116..2422a31b3c7 100644 --- a/src/renderer/src/runtime/sync-runtime-graph.ts +++ b/src/renderer/src/runtime/sync-runtime-graph.ts @@ -26,14 +26,12 @@ import { isTerminalLeafId, makePaneKey, parsePaneKey } from '../../../shared/sta import { isWebTerminalSurfaceTabId } from '../../../shared/terminal-surface-id' import { isClaudeManagementTitle } from '../../../shared/agent-detection' import { parseWorkspaceKey } from '../../../shared/workspace-scope' +import type { Tab, TabGroup, TabGroupLayoutNode } from '../../../shared/tab-types' import type { - Tab, - TabGroup, - TabGroupLayoutNode, TerminalLayoutSnapshot, TerminalPaneLayoutNode, TerminalTab -} from '../../../shared/types' +} from '../../../shared/terminal-tab-types' import { resolveTerminalTabTitle } from '../../../shared/tab-title-resolution' import { isNativeChatTabWideFallbackSafe, diff --git a/src/renderer/src/runtime/use-runtime-session-mirror-environment-key.test.ts b/src/renderer/src/runtime/use-runtime-session-mirror-environment-key.test.ts index fc28095e136..be02031255c 100644 --- a/src/renderer/src/runtime/use-runtime-session-mirror-environment-key.test.ts +++ b/src/renderer/src/runtime/use-runtime-session-mirror-environment-key.test.ts @@ -4,7 +4,8 @@ import { act, cleanup, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { shallow } from 'zustand/shallow' import { getDefaultSettings } from '../../../shared/constants' -import type { ProjectGroup, Repo } from '../../../shared/types' +import type { ProjectGroup } from '../../../shared/project-group-types' +import type { Repo } from '../../../shared/repo-types' import type * as RuntimeSessionMirrorTargetsModule from '@/lib/runtime-session-mirror-targets' import { makeWorktree } from '@/store/slices/store-test-helpers' diff --git a/src/renderer/src/runtime/web-runtime-session.ts b/src/renderer/src/runtime/web-runtime-session.ts index 691202eae08..0f4bae03f6a 100644 --- a/src/renderer/src/runtime/web-runtime-session.ts +++ b/src/renderer/src/runtime/web-runtime-session.ts @@ -29,7 +29,8 @@ import type { RuntimeCreateAgentSessionResult, RuntimeEnsureAgentSessionResult } from '../../../shared/agent-session-host-authority' -import type { TerminalPaneLayoutNode, TuiAgent } from '../../../shared/types' +import type { TerminalPaneLayoutNode } from '../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../shared/tui-agent' import { createBrowserUuid } from '../lib/browser-uuid' import { getRuntimeEnvironmentIdForWorktree } from '../lib/worktree-runtime-owner' import { useAppStore } from '../store' diff --git a/src/renderer/src/runtime/web-session-tabs-sync-mirror-identity.test.ts b/src/renderer/src/runtime/web-session-tabs-sync-mirror-identity.test.ts index 317065d7c81..c58c6471a16 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync-mirror-identity.test.ts +++ b/src/renderer/src/runtime/web-session-tabs-sync-mirror-identity.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it } from 'vitest' import { createStore } from 'zustand/vanilla' import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types' import { toWebTerminalSurfaceTabId } from '../../../shared/terminal-surface-id' -import type { BrowserCertificateFailure } from '../../../shared/types' +import type { BrowserCertificateFailure } from '../../../shared/browser-workspace-types' import { applyWebSessionTabsSnapshot, applyWebSessionTabsSnapshots, diff --git a/src/renderer/src/runtime/web-session-tabs-sync.test.ts b/src/renderer/src/runtime/web-session-tabs-sync.test.ts index e830e027208..07e95685c23 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync.test.ts +++ b/src/renderer/src/runtime/web-session-tabs-sync.test.ts @@ -20,13 +20,9 @@ import { recordWebSessionReorderIntent, resetWebSessionReorderIntentForTests } from './web-session-reorder-intent' -import type { - BrowserPage, - BrowserWorkspace, - Tab, - TabGroupLayoutNode, - TerminalTab -} from '../../../shared/types' +import type { BrowserPage, BrowserWorkspace } from '../../../shared/browser-workspace-types' +import type { Tab, TabGroupLayoutNode } from '../../../shared/tab-types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' import type { OpenFile } from '../store/slices/editor' import { confirmWebAgentSessionHandoffAfterCreate, diff --git a/src/renderer/src/runtime/web-session-tabs-sync.ts b/src/renderer/src/runtime/web-session-tabs-sync.ts index 8de495919d7..9ee15c8421f 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync.ts +++ b/src/renderer/src/runtime/web-session-tabs-sync.ts @@ -22,13 +22,10 @@ import type { import type { BrowserCertificateFailure, BrowserPage, - BrowserWorkspace, - Tab, - TabGroup, - TabGroupLayoutNode, - TerminalLayoutSnapshot, - TerminalTab -} from '../../../shared/types' + BrowserWorkspace +} from '../../../shared/browser-workspace-types' +import type { Tab, TabGroup, TabGroupLayoutNode } from '../../../shared/tab-types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types' import type { OpenFile } from '../store/slices/editor' import { isTerminalLeafId, makePaneKey, parsePaneKey } from '../../../shared/stable-pane-id' import { getRemoteRuntimePtyEnvironmentId, toRemoteRuntimePtyId } from './runtime-terminal-stream' diff --git a/src/renderer/src/runtime/web-session-terminal-orphan-recovery.ts b/src/renderer/src/runtime/web-session-terminal-orphan-recovery.ts index 5e56142bcf8..ee57dd610e2 100644 --- a/src/renderer/src/runtime/web-session-terminal-orphan-recovery.ts +++ b/src/renderer/src/runtime/web-session-terminal-orphan-recovery.ts @@ -3,7 +3,7 @@ import type { RuntimeTerminalListResult, RuntimeTerminalOrphanAdoptionResult } from '../../../shared/runtime-types' -import type { TerminalTab } from '../../../shared/types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' import type { RuntimeRpcResponse } from '../../../shared/runtime-rpc-envelope' import { parseRemoteRuntimePtyId } from './runtime-terminal-stream' import { toRuntimeWorktreeSelector } from './runtime-worktree-selector' diff --git a/src/renderer/src/runtime/web-session-terminal-orphan-topology.ts b/src/renderer/src/runtime/web-session-terminal-orphan-topology.ts index 5d7e0a9db8f..1d1dd242129 100644 --- a/src/renderer/src/runtime/web-session-terminal-orphan-topology.ts +++ b/src/renderer/src/runtime/web-session-terminal-orphan-topology.ts @@ -1,11 +1,10 @@ import type { RuntimeTerminalOrphanTopology } from '../../../shared/runtime-types' +import type { TabGroup, TabGroupLayoutNode } from '../../../shared/tab-types' import type { - TabGroup, - TabGroupLayoutNode, TerminalLayoutSnapshot, TerminalPaneLayoutNode, TerminalTab -} from '../../../shared/types' +} from '../../../shared/terminal-tab-types' import { toHostSessionTabId } from './web-terminal-surface-id' export type WebTerminalOrphanTopologyState = { diff --git a/src/renderer/src/store/project-host-setup-selector-normalization.ts b/src/renderer/src/store/project-host-setup-selector-normalization.ts index acc7e6f4edf..154ee4e5ea0 100644 --- a/src/renderer/src/store/project-host-setup-selector-normalization.ts +++ b/src/renderer/src/store/project-host-setup-selector-normalization.ts @@ -1,4 +1,5 @@ -import type { Project, ProjectHostSetup, Repo } from '../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../shared/project-types' +import type { Repo } from '../../../shared/repo-types' import { getProjectIdentityKey, type ProjectHostSetupProjection diff --git a/src/renderer/src/store/project-host-setup-selector.ts b/src/renderer/src/store/project-host-setup-selector.ts index c3e1f9269dc..55ba345f5ba 100644 --- a/src/renderer/src/store/project-host-setup-selector.ts +++ b/src/renderer/src/store/project-host-setup-selector.ts @@ -1,4 +1,4 @@ -import type { Project, ProjectHostSetup } from '../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../shared/project-types' import { projectHostSetupProjectionFromRepos, type ProjectHostSetupProjection diff --git a/src/renderer/src/store/right-sidebar-route.ts b/src/renderer/src/store/right-sidebar-route.ts index 38bddf68e1a..e0b2bc0fa1b 100644 --- a/src/renderer/src/store/right-sidebar-route.ts +++ b/src/renderer/src/store/right-sidebar-route.ts @@ -1,4 +1,7 @@ -import type { ActiveRightSidebarTab, RightSidebarExplorerView } from '../../../shared/types' +import type { + ActiveRightSidebarTab, + RightSidebarExplorerView +} from '../../../shared/ui-chrome-types' import { isPluginPanelTabKey } from '../../../shared/plugins/plugin-manifest' export type RightSidebarRoute = { diff --git a/src/renderer/src/store/selectors.test.ts b/src/renderer/src/store/selectors.test.ts index 345a25c0eec..f6b8555925a 100644 --- a/src/renderer/src/store/selectors.test.ts +++ b/src/renderer/src/store/selectors.test.ts @@ -1,5 +1,7 @@ import { beforeEach, describe, expect, it } from 'vitest' -import type { Repo, TerminalTab, Worktree } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { Worktree } from '../../../shared/worktree/types' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' import { toRuntimeExecutionHostId, toSshExecutionHostId } from '../../../shared/execution-host' import { isGitRepoKind } from '../../../shared/repo-kind' diff --git a/src/renderer/src/store/selectors.ts b/src/renderer/src/store/selectors.ts index b955666760f..7b16f29bf4f 100644 --- a/src/renderer/src/store/selectors.ts +++ b/src/renderer/src/store/selectors.ts @@ -1,6 +1,8 @@ import { useAppStore } from './index' import { useShallow } from 'zustand/react/shallow' -import type { Repo, Worktree, TerminalTab } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' +import type { TerminalTab } from '../../../shared/terminal-tab-types' +import type { Worktree } from '../../../shared/worktree/types' import type { AppState } from './types' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' import { diff --git a/src/renderer/src/store/slices/active-tab-owner-worktree.test.ts b/src/renderer/src/store/slices/active-tab-owner-worktree.test.ts index 7e8c092a6c7..c0537bf698d 100644 --- a/src/renderer/src/store/slices/active-tab-owner-worktree.test.ts +++ b/src/renderer/src/store/slices/active-tab-owner-worktree.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' const recordRendererCrashBreadcrumb = vi.fn() vi.mock('../../lib/crash-breadcrumb-recorder', () => ({ diff --git a/src/renderer/src/store/slices/active-tab-owner-worktree.ts b/src/renderer/src/store/slices/active-tab-owner-worktree.ts index 270c5d44f1c..e8f4bbaed03 100644 --- a/src/renderer/src/store/slices/active-tab-owner-worktree.ts +++ b/src/renderer/src/store/slices/active-tab-owner-worktree.ts @@ -1,4 +1,4 @@ -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { recordRendererCrashBreadcrumb } from '../../lib/crash-breadcrumb-recorder' const reportedDuplicateTabVerdicts = new Set() diff --git a/src/renderer/src/store/slices/agent-status-drop-ipc.test.ts b/src/renderer/src/store/slices/agent-status-drop-ipc.test.ts index 06e8e900a2d..53a0a4e131e 100644 --- a/src/renderer/src/store/slices/agent-status-drop-ipc.test.ts +++ b/src/renderer/src/store/slices/agent-status-drop-ipc.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { RetainedAgentEntry } from './agent-status' import { RECENTLY_CLOSED_AGENT_STATUS_TAB_IDS_MAX } from './agent-status' import { createTestStore } from './store-test-helpers' diff --git a/src/renderer/src/store/slices/agent-status-drop.test.ts b/src/renderer/src/store/slices/agent-status-drop.test.ts index 5a4b86838c2..c13a93cd413 100644 --- a/src/renderer/src/store/slices/agent-status-drop.test.ts +++ b/src/renderer/src/store/slices/agent-status-drop.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { RetainedAgentEntry } from './agent-status' import { createTestStore, makeTab } from './store-test-helpers' diff --git a/src/renderer/src/store/slices/agent-status-retained-leak.test.ts b/src/renderer/src/store/slices/agent-status-retained-leak.test.ts index 2259af3cf5c..fd275d4e43d 100644 --- a/src/renderer/src/store/slices/agent-status-retained-leak.test.ts +++ b/src/renderer/src/store/slices/agent-status-retained-leak.test.ts @@ -23,7 +23,7 @@ */ import { afterEach, describe, expect, it, vi } from 'vitest' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { RetainedAgentEntry } from './agent-status' import { createTestStore } from './store-test-helpers' diff --git a/src/renderer/src/store/slices/agent-status.ts b/src/renderer/src/store/slices/agent-status.ts index 0f1ebf66e08..013b98f01a5 100644 --- a/src/renderer/src/store/slices/agent-status.ts +++ b/src/renderer/src/store/slices/agent-status.ts @@ -27,7 +27,7 @@ import { } from '../../../../shared/agent-status-identity' import { isCommandCodeNewTurnWhileWorking } from '../../../../shared/command-code-turn-boundary' import { agentEntryCompletionAt } from '../../../../shared/agent-completion-time' -import type { TerminalPaneLayoutNode, TerminalTab } from '../../../../shared/types' +import type { TerminalPaneLayoutNode, TerminalTab } from '../../../../shared/terminal-tab-types' import { getRepoExecutionHostId, getWorktreeExecutionHostId diff --git a/src/renderer/src/store/slices/browser-webview-cleanup.test.ts b/src/renderer/src/store/slices/browser-webview-cleanup.test.ts index 060fd5d1247..3f7c69a4d29 100644 --- a/src/renderer/src/store/slices/browser-webview-cleanup.test.ts +++ b/src/renderer/src/store/slices/browser-webview-cleanup.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { BrowserPage, BrowserWorkspace } from '../../../../shared/types' +import type { BrowserPage, BrowserWorkspace } from '../../../../shared/browser-workspace-types' vi.mock('../../components/browser-pane/webview-registry', () => ({ destroyPersistentWebview: vi.fn() diff --git a/src/renderer/src/store/slices/browser-webview-cleanup.ts b/src/renderer/src/store/slices/browser-webview-cleanup.ts index 234199560ed..7b835841c3a 100644 --- a/src/renderer/src/store/slices/browser-webview-cleanup.ts +++ b/src/renderer/src/store/slices/browser-webview-cleanup.ts @@ -1,4 +1,4 @@ -import type { BrowserPage, BrowserWorkspace } from '../../../../shared/types' +import type { BrowserPage, BrowserWorkspace } from '../../../../shared/browser-workspace-types' import { destroyPersistentWebview, moveFocusToRendererBeforeFocusedWebviewHidden diff --git a/src/renderer/src/store/slices/browser.ts b/src/renderer/src/store/slices/browser.ts index 946fdd4e853..fe7c3a694a2 100644 --- a/src/renderer/src/store/slices/browser.ts +++ b/src/renderer/src/store/slices/browser.ts @@ -2,18 +2,18 @@ import type { StateCreator } from 'zustand' import type { AppState } from '../types' import type { + BrowserCertificateFailure, BrowserCookieImportResult, BrowserCookieImportSummary, - BrowserCertificateFailure, BrowserHistoryEntry, BrowserLoadError, BrowserPage, BrowserSessionProfile, BrowserSessionProfileCreateOptions, BrowserViewportPresetId, - BrowserWorkspace, - WorkspaceSessionState -} from '../../../../shared/types' + BrowserWorkspace +} from '../../../../shared/browser-workspace-types' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' import { GRAB_BUDGET, type BrowserPageAnnotation } from '../../../../shared/browser-grab-types' import { FLOATING_TERMINAL_WORKTREE_ID, ORCA_BROWSER_BLANK_URL } from '../../../../shared/constants' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' diff --git a/src/renderer/src/store/slices/commit-message-generation.ts b/src/renderer/src/store/slices/commit-message-generation.ts index 77f271781cf..e361958932c 100644 --- a/src/renderer/src/store/slices/commit-message-generation.ts +++ b/src/renderer/src/store/slices/commit-message-generation.ts @@ -1,5 +1,5 @@ import type { StateCreator } from 'zustand' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { AppState } from '../types' export type CommitMessageGenerationRuntimeTargetSettings = Pick< diff --git a/src/renderer/src/store/slices/degraded-repo-hydration.test.ts b/src/renderer/src/store/slices/degraded-repo-hydration.test.ts index 48690c43f51..b8063a48408 100644 --- a/src/renderer/src/store/slices/degraded-repo-hydration.test.ts +++ b/src/renderer/src/store/slices/degraded-repo-hydration.test.ts @@ -1,6 +1,7 @@ import { expect, it, vi } from 'vitest' import type * as AgentStatusModule from '@/lib/agent-status' -import type { BrowserTab, WorkspaceSessionState } from '../../../../shared/types' +import type { BrowserTab } from '../../../../shared/browser-workspace-types' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' import { getDefaultWorkspaceSession } from '../../../../shared/constants' vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } })) diff --git a/src/renderer/src/store/slices/degraded-repo-worktree-validity.ts b/src/renderer/src/store/slices/degraded-repo-worktree-validity.ts index 139312608b2..16b97061df1 100644 --- a/src/renderer/src/store/slices/degraded-repo-worktree-validity.ts +++ b/src/renderer/src/store/slices/degraded-repo-worktree-validity.ts @@ -1,9 +1,6 @@ -import type { - DetectedWorktreeListResult, - Repo, - WorkspaceSessionState, - Worktree -} from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' +import type { DetectedWorktreeListResult, Worktree } from '../../../../shared/worktree/types' import { parseWorkspaceKey } from '../../../../shared/workspace-scope' import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id' diff --git a/src/renderer/src/store/slices/detected-agents.test.ts b/src/renderer/src/store/slices/detected-agents.test.ts index f85b5977960..a343fe2c59c 100644 --- a/src/renderer/src/store/slices/detected-agents.test.ts +++ b/src/renderer/src/store/slices/detected-agents.test.ts @@ -1,7 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { create } from 'zustand' import type { AppState } from '../types' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { _getRemoteDetectPromiseCountForTest, createDetectedAgentsSlice } from './detected-agents' import { _getRuntimeDetectPromiseCountForTest, diff --git a/src/renderer/src/store/slices/detected-agents.ts b/src/renderer/src/store/slices/detected-agents.ts index 87e34b712ff..1ab5daebcdc 100644 --- a/src/renderer/src/store/slices/detected-agents.ts +++ b/src/renderer/src/store/slices/detected-agents.ts @@ -1,6 +1,6 @@ import type { StateCreator } from 'zustand' import type { AppState } from '../types' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { createLocalDetectedAgentState } from './local-detected-agent-state' import type { LocalDetectedAgentState } from './local-detected-agent-store-state' diff --git a/src/renderer/src/store/slices/diffComments.test.ts b/src/renderer/src/store/slices/diffComments.test.ts index 6f230b8665c..ecfa3a1408e 100644 --- a/src/renderer/src/store/slices/diffComments.test.ts +++ b/src/renderer/src/store/slices/diffComments.test.ts @@ -1,7 +1,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { create } from 'zustand' import type { AppState } from '../types' -import type { DiffComment, Worktree } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' +import type { Worktree } from '../../../../shared/worktree/types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/diffComments.ts b/src/renderer/src/store/slices/diffComments.ts index 2bf3b18ceec..fb4a1cb2e9e 100644 --- a/src/renderer/src/store/slices/diffComments.ts +++ b/src/renderer/src/store/slices/diffComments.ts @@ -1,7 +1,9 @@ /* eslint-disable max-lines -- Why: keeps note mutation, rollback, persistence ordering, and sent-state transitions under shared queue/rollback invariants. */ import type { StateCreator } from 'zustand' import type { AppState } from '../types' -import type { DiffComment, FolderWorkspace, Worktree } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { Worktree } from '../../../../shared/worktree/types' import { findWorktreeById, getRepoIdFromWorktreeId } from './worktree-helpers' import { callRuntimeRpc, getActiveRuntimeTarget } from '../../runtime/runtime-rpc-client' import { toRuntimeWorktreeSelector } from '../../runtime/runtime-worktree-selector' diff --git a/src/renderer/src/store/slices/direct-ssh-pane-retry-ledger.ts b/src/renderer/src/store/slices/direct-ssh-pane-retry-ledger.ts index 51730cf70a8..b9b9e3400cc 100644 --- a/src/renderer/src/store/slices/direct-ssh-pane-retry-ledger.ts +++ b/src/renderer/src/store/slices/direct-ssh-pane-retry-ledger.ts @@ -1,5 +1,5 @@ import type { DirectSshAuthority } from '../../../../shared/ssh-types' -import type { TerminalLayoutSnapshot } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' import { shouldRetryPaneSpawnOnSshReconnect } from '../../hooks/ssh-reconnect-pane-retry' import { directSshAuthoritiesEqual, diff --git a/src/renderer/src/store/slices/direct-ssh-terminal-authority-ledger.ts b/src/renderer/src/store/slices/direct-ssh-terminal-authority-ledger.ts index b0e5d5731c1..ac938ce67be 100644 --- a/src/renderer/src/store/slices/direct-ssh-terminal-authority-ledger.ts +++ b/src/renderer/src/store/slices/direct-ssh-terminal-authority-ledger.ts @@ -1,6 +1,6 @@ import type { DirectSshAuthority } from '../../../../shared/ssh-types' import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { DirectSshLivePtyBinding, DirectSshPaneRetryResult, diff --git a/src/renderer/src/store/slices/direct-ssh-terminal-recovery-types.ts b/src/renderer/src/store/slices/direct-ssh-terminal-recovery-types.ts index 2784559f201..f59015b6ee7 100644 --- a/src/renderer/src/store/slices/direct-ssh-terminal-recovery-types.ts +++ b/src/renderer/src/store/slices/direct-ssh-terminal-recovery-types.ts @@ -1,5 +1,5 @@ import type { DirectSshAuthority } from '../../../../shared/ssh-types' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { CodexRestartNotice } from './terminals' export type DirectSshPaneRetryAttemptId = string & { diff --git a/src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts b/src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts index acc1db0ec72..232c2d1bd79 100644 --- a/src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts +++ b/src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { clearDirectSshTerminalBindings, type DirectSshTerminalBindingState diff --git a/src/renderer/src/store/slices/direct-ssh-terminal-recovery.ts b/src/renderer/src/store/slices/direct-ssh-terminal-recovery.ts index 8cc2978379b..4bc678a0437 100644 --- a/src/renderer/src/store/slices/direct-ssh-terminal-recovery.ts +++ b/src/renderer/src/store/slices/direct-ssh-terminal-recovery.ts @@ -1,5 +1,5 @@ import type { DirectSshAuthority } from '../../../../shared/ssh-types' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { CodexRestartNotice } from './terminals' import { directSshAuthoritiesEqual, diff --git a/src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts b/src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts index 063a773e1fa..5f9974966be 100644 --- a/src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts +++ b/src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { resolveDirectSshTerminalWorkspaceKeys } from './direct-ssh-terminal-workspace-scope' function tab(worktreeId: string, ptyId: string): TerminalTab { diff --git a/src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.ts b/src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.ts index e24ca1cdcd1..428069a60ea 100644 --- a/src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.ts +++ b/src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.ts @@ -6,7 +6,7 @@ import { import { isPathInsideOrEqual } from '../../../../shared/cross-platform-path' import { getProjectGroupSubtreeIds } from '../../../../shared/project-groups' import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { folderWorkspaceKey, worktreeWorkspaceKey } from '../../../../shared/workspace-scope' import { resolveDirectSshTargetScope, diff --git a/src/renderer/src/store/slices/editor-branch-line-total.test.ts b/src/renderer/src/store/slices/editor-branch-line-total.test.ts index 2b7132085da..e28f7f3c2bb 100644 --- a/src/renderer/src/store/slices/editor-branch-line-total.test.ts +++ b/src/renderer/src/store/slices/editor-branch-line-total.test.ts @@ -2,7 +2,7 @@ import { createStore, type StoreApi } from 'zustand/vanilla' import { describe, expect, it, vi } from 'vitest' import { createEditorSlice } from './editor' import type { AppState } from '../types' -import type { GitStatusResult } from '../../../../shared/types' +import type { GitStatusResult } from '../../../../shared/git-status-types' const MERGE_BASE = '1f3c0d9a5b6e7f8091a2b3c4d5e6f708192a3b4c' diff --git a/src/renderer/src/store/slices/editor-rekey-open-files.test.ts b/src/renderer/src/store/slices/editor-rekey-open-files.test.ts index e7ea518092e..2099c826e9b 100644 --- a/src/renderer/src/store/slices/editor-rekey-open-files.test.ts +++ b/src/renderer/src/store/slices/editor-rekey-open-files.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it } from 'vitest' import { useAppStore } from '@/store' import type { OpenFilePathRekey } from './editor' -import type { Tab, TabGroup } from '../../../../shared/types' +import type { Tab, TabGroup } from '../../../../shared/tab-types' // Stage-1 foundation: rekeyOpenFilesForPathChange atomically retargets an open // edit session across an Orca-owned move (no close/reopen), preserving all diff --git a/src/renderer/src/store/slices/editor.test.ts b/src/renderer/src/store/slices/editor.test.ts index 50530c90382..13b49c94991 100644 --- a/src/renderer/src/store/slices/editor.test.ts +++ b/src/renderer/src/store/slices/editor.test.ts @@ -13,10 +13,10 @@ import { clearRuntimeCompatibilityCacheForTests } from '../../runtime/runtime-rp import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' import type { GitBranchChangeEntry, - GitBranchCompareSummary, - GitStatusEntry, - Tab -} from '../../../../shared/types' + GitBranchCompareSummary +} from '../../../../shared/git-diff-compare-types' +import type { GitStatusEntry } from '../../../../shared/git-status-types' +import type { Tab } from '../../../../shared/tab-types' import { isSyncPushStageError } from '@/lib/source-control-remote-error' const { toastErrorMock } = vi.hoisted(() => ({ diff --git a/src/renderer/src/store/slices/editor.ts b/src/renderer/src/store/slices/editor.ts index 4b2efe9516a..35fcc7d4633 100644 --- a/src/renderer/src/store/slices/editor.ts +++ b/src/renderer/src/store/slices/editor.ts @@ -28,29 +28,33 @@ import { openHttpLink, type HttpLinkSourceOwner } from '@/lib/http-link-routing' import { getConnectionIdForFileFromState } from '@/lib/connection-owner-resolution' import { isLocalPathOpenBlocked, showLocalPathOpenBlockedToast } from '@/lib/local-path-open-guard' import { detectLanguage } from '@/lib/language-detect' +import type { SearchResult } from '../../../../shared/code-search-types' import type { GitBranchChangeEntry, GitBranchCompareSummary, - GitCommitCompareSummary, + GitCommitCompareSummary +} from '../../../../shared/git-diff-compare-types' +import type { + GitBranchLineTotal, GitConflictKind, GitConflictOperation, GitConflictResolutionStatus, GitConflictStatusSource, - GlobalSettings, - GitPushTarget, GitStatusEntry, GitStatusResult, - PersistedOpenFile, - Tab, - TabGroup, - GitUpstreamStatus, + GitUpstreamStatus +} from '../../../../shared/git-status-types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Tab, TabGroup, WorkspaceVisibleTabType } from '../../../../shared/tab-types' +import type { ActiveRightSidebarTab, - RightSidebarExplorerView, - SearchResult, - WorkspaceSessionState, - WorkspaceVisibleTabType -} from '../../../../shared/types' -import type { GitBranchLineTotal } from '../../../../shared/git-status-types' + RightSidebarExplorerView +} from '../../../../shared/ui-chrome-types' +import type { + PersistedOpenFile, + WorkspaceSessionState +} from '../../../../shared/workspace-session-state-types' +import type { GitPushTarget } from '../../../../shared/worktree/types' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' import { clampMarkdownTocPanelWidth } from '../../../../shared/markdown-toc-panel-width' import { @@ -116,7 +120,7 @@ export type { ActiveRightSidebarTab, RightSidebarExplorerView, RightSidebarTab -} from '../../../../shared/types' +} from '../../../../shared/ui-chrome-types' const DEFAULT_FILE_SEARCH_STATE = { query: '', diff --git a/src/renderer/src/store/slices/folder-workspace-activation-and-activity.test.ts b/src/renderer/src/store/slices/folder-workspace-activation-and-activity.test.ts index aa4efc3754f..35e9397c1a5 100644 --- a/src/renderer/src/store/slices/folder-workspace-activation-and-activity.test.ts +++ b/src/renderer/src/store/slices/folder-workspace-activation-and-activity.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { AppState } from '../types' -import type { FolderWorkspace, ProjectGroup } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import { createCompatibleRuntimeStatusResponseIfNeeded, diff --git a/src/renderer/src/store/slices/folder-workspace-diff-comments.test.ts b/src/renderer/src/store/slices/folder-workspace-diff-comments.test.ts index b03015ec98d..c11ed39868a 100644 --- a/src/renderer/src/store/slices/folder-workspace-diff-comments.test.ts +++ b/src/renderer/src/store/slices/folder-workspace-diff-comments.test.ts @@ -1,7 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { create } from 'zustand' import type { AppState } from '../types' -import type { DiffComment, FolderWorkspace, ProjectGroup } from '../../../../shared/types' +import type { DiffComment } from '../../../../shared/diff-comment-types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import { createCompatibleRuntimeStatusResponseIfNeeded, diff --git a/src/renderer/src/store/slices/folder-workspace-owner-routed-mutations.test.ts b/src/renderer/src/store/slices/folder-workspace-owner-routed-mutations.test.ts index 83ea704c690..4100588a88c 100644 --- a/src/renderer/src/store/slices/folder-workspace-owner-routed-mutations.test.ts +++ b/src/renderer/src/store/slices/folder-workspace-owner-routed-mutations.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { FolderWorkspace, ProjectGroup } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/github-cache-key.ts b/src/renderer/src/store/slices/github-cache-key.ts index e9117f1f69f..1f5a22908c5 100644 --- a/src/renderer/src/store/slices/github-cache-key.ts +++ b/src/renderer/src/store/slices/github-cache-key.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { LOCAL_EXECUTION_HOST_ID, normalizeExecutionHostId, diff --git a/src/renderer/src/store/slices/github-checks-cache.test.ts b/src/renderer/src/store/slices/github-checks-cache.test.ts index 2474ac521e3..0808acba490 100644 --- a/src/renderer/src/store/slices/github-checks-cache.test.ts +++ b/src/renderer/src/store/slices/github-checks-cache.test.ts @@ -5,7 +5,8 @@ import { createGitHubSlice, prChecksCacheSuffix } from './github' import { createHostedReviewSlice } from './hosted-review' import { getHostedReviewCacheKey } from './hosted-review-cache-identity' import type { AppState } from '../types' -import type { PRCheckDetail, PRInfo } from '../../../../shared/types' +import type { PRCheckDetail } from '../../../../shared/github/check-types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' const mockApi = { gh: { diff --git a/src/renderer/src/store/slices/github-checks.test.ts b/src/renderer/src/store/slices/github-checks.test.ts index b5c46ce6882..ce41e0f40af 100644 --- a/src/renderer/src/store/slices/github-checks.test.ts +++ b/src/renderer/src/store/slices/github-checks.test.ts @@ -5,7 +5,7 @@ import { normalizeBranchName } from './github-checks' import type { AppState } from '../types' -import type { PRCheckDetail } from '../../../../shared/types' +import type { PRCheckDetail } from '../../../../shared/github/check-types' describe('deriveCheckStatusFromChecks', () => { it('treats an action_required check as failure so it is not a silent pass', () => { diff --git a/src/renderer/src/store/slices/github-checks.ts b/src/renderer/src/store/slices/github-checks.ts index da36a378ddf..900c482ac7d 100644 --- a/src/renderer/src/store/slices/github-checks.ts +++ b/src/renderer/src/store/slices/github-checks.ts @@ -1,5 +1,6 @@ import type { AppState } from '../types' -import type { PRCheckDetail, GitHubOwnerRepo } from '../../../../shared/types' +import type { PRCheckDetail } from '../../../../shared/github/check-types' +import type { GitHubOwnerRepo } from '../../../../shared/github/pull-request-types' import { getGitHubPRCacheKey } from './github-cache-key' import { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key' import { derivePRCheckStatus } from '../../../../shared/pr-check-status' diff --git a/src/renderer/src/store/slices/github-pr-refresh-owner-routing.test.ts b/src/renderer/src/store/slices/github-pr-refresh-owner-routing.test.ts index f7e49e211d1..f78393f62c7 100644 --- a/src/renderer/src/store/slices/github-pr-refresh-owner-routing.test.ts +++ b/src/renderer/src/store/slices/github-pr-refresh-owner-routing.test.ts @@ -3,7 +3,9 @@ import { create } from 'zustand' import { createGitHubSlice } from './github' import { createHostedReviewSlice } from './hosted-review' import type { AppState } from '../types' -import type { PRInfo, Repo, Worktree } from '../../../../shared/types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/github-pr-refresh-states-leak.test.ts b/src/renderer/src/store/slices/github-pr-refresh-states-leak.test.ts index 74096867101..9ed10018a69 100644 --- a/src/renderer/src/store/slices/github-pr-refresh-states-leak.test.ts +++ b/src/renderer/src/store/slices/github-pr-refresh-states-leak.test.ts @@ -18,7 +18,10 @@ import { create } from 'zustand' import { createGitHubSlice } from './github' import { createHostedReviewSlice } from './hosted-review' import type { AppState } from '../types' -import type { GitHubPRRefreshEvent, GitHubPRRefreshReason } from '../../../../shared/types' +import type { + GitHubPRRefreshEvent, + GitHubPRRefreshReason +} from '../../../../shared/github/pull-request-refresh-types' // MAX_PR_REFRESH_STATE_ENTRIES is module-private; mirror its value here. const MAX_ENTRIES = 2000 diff --git a/src/renderer/src/store/slices/github-project-row-owner.test.ts b/src/renderer/src/store/slices/github-project-row-owner.test.ts index dccaec073e3..89b10e295b6 100644 --- a/src/renderer/src/store/slices/github-project-row-owner.test.ts +++ b/src/renderer/src/store/slices/github-project-row-owner.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi, beforeEach } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { settingsForProjectRowOwner } from './github-project-row-owner' import { lookupReposBySlugFromCache } from '@/lib/repo-slug-cache' diff --git a/src/renderer/src/store/slices/github-project-row-owner.ts b/src/renderer/src/store/slices/github-project-row-owner.ts index db3a4b3333e..9af67dc4d50 100644 --- a/src/renderer/src/store/slices/github-project-row-owner.ts +++ b/src/renderer/src/store/slices/github-project-row-owner.ts @@ -1,4 +1,5 @@ -import type { GlobalSettings, Repo } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' import { getSettingsForRepoRuntimeOwner } from '@/lib/repo-runtime-owner' import { lookupReposBySlugFromCache } from '@/lib/repo-slug-cache' diff --git a/src/renderer/src/store/slices/github-repo-lookup-index.test.ts b/src/renderer/src/store/slices/github-repo-lookup-index.test.ts index 41918b28c07..d964fe18826 100644 --- a/src/renderer/src/store/slices/github-repo-lookup-index.test.ts +++ b/src/renderer/src/store/slices/github-repo-lookup-index.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { getGitHubRepoLookupIndex } from './github-repo-lookup-index' function makeRepo(id: string, path: string): Repo { diff --git a/src/renderer/src/store/slices/github-repo-lookup-index.ts b/src/renderer/src/store/slices/github-repo-lookup-index.ts index 0c21a2fbd50..62d60e44b3a 100644 --- a/src/renderer/src/store/slices/github-repo-lookup-index.ts +++ b/src/renderer/src/store/slices/github-repo-lookup-index.ts @@ -1,4 +1,4 @@ -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' type IndexedRepo = { index: number diff --git a/src/renderer/src/store/slices/github.test.ts b/src/renderer/src/store/slices/github.test.ts index dfcd76ec5d1..140840121f3 100644 --- a/src/renderer/src/store/slices/github.test.ts +++ b/src/renderer/src/store/slices/github.test.ts @@ -18,7 +18,9 @@ import { } from './github' import { createHostedReviewSlice } from './hosted-review' import type { AppState } from '../types' -import type { GitHubWorkItem, PRInfo, Worktree } from '../../../../shared/types' +import type { PRInfo } from '../../../../shared/github/pull-request-types' +import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { HostedReviewInfo } from '../../../../shared/hosted-review' import { GITHUB_WORK_ITEMS_SSH_REMOTE_REQUIRED_MESSAGE } from '../../../../shared/work-items' import { diff --git a/src/renderer/src/store/slices/github.ts b/src/renderer/src/store/slices/github.ts index 71cf753e662..e74e7a35586 100644 --- a/src/renderer/src/store/slices/github.ts +++ b/src/renderer/src/store/slices/github.ts @@ -4,30 +4,31 @@ import { toast } from 'sonner' import type { AppState } from '../types' import { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key' import { githubProjectIdentityKey } from '../../../../shared/github/project-identity' +import type { ClassifiedError } from '../../../../shared/classified-error' +import type { PRCheckDetail, PRCheckRunDetails } from '../../../../shared/github/check-types' +import type { + GitHubCommentResult, + GitHubReactionContent, + PRComment +} from '../../../../shared/github/comment-types' import type { - ClassifiedError, - GitHubOwnerRepo, GitHubPRRefreshAlias, - IssueSourcePreference, - PRInfo, GitHubPRRefreshCandidate, GitHubPRRefreshEvent, GitHubPRRefreshReason, GitHubPRRefreshSkippedReason, PRRefreshErrorType, - PRRefreshOutcome, - GitHubCommentResult, - GitHubReactionContent, + PRRefreshOutcome +} from '../../../../shared/github/pull-request-refresh-types' +import type { + GitHubOwnerRepo, IssueInfo, - PRCheckDetail, - PRCheckRunDetails, - PRComment, - Repo, - Worktree, - GitHubWorkItem, - ListWorkItemsResult, - GlobalSettings -} from '../../../../shared/types' + PRInfo +} from '../../../../shared/github/pull-request-types' +import type { GitHubWorkItem, ListWorkItemsResult } from '../../../../shared/github/work-item-types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { IssueSourcePreference, Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { GetProjectViewTableArgs, GetProjectViewTableResult, diff --git a/src/renderer/src/store/slices/hosted-review-cache-identity.ts b/src/renderer/src/store/slices/hosted-review-cache-identity.ts index f1b852ff752..c53f91a82f4 100644 --- a/src/renderer/src/store/slices/hosted-review-cache-identity.ts +++ b/src/renderer/src/store/slices/hosted-review-cache-identity.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { getSettingsFocusedExecutionHostId, normalizeExecutionHostId, diff --git a/src/renderer/src/store/slices/hosted-review.ts b/src/renderer/src/store/slices/hosted-review.ts index 4e64aa48e84..3c3940b6acc 100644 --- a/src/renderer/src/store/slices/hosted-review.ts +++ b/src/renderer/src/store/slices/hosted-review.ts @@ -10,7 +10,7 @@ import type { HostedReviewCreationEligibilityArgs, HostedReviewInfo } from '../../../../shared/hosted-review' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' import type { AppState } from '../types' import { diff --git a/src/renderer/src/store/slices/jira.test.ts b/src/renderer/src/store/slices/jira.test.ts index a9925e17a61..964d36420a1 100644 --- a/src/renderer/src/store/slices/jira.test.ts +++ b/src/renderer/src/store/slices/jira.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { create } from 'zustand' import type { AppState } from '../types' -import type { JiraConnectionStatus, JiraIssue, JiraViewer } from '../../../../shared/types' +import type { JiraConnectionStatus, JiraIssue, JiraViewer } from '../../../../shared/jira-types' import { getTaskSourceCacheScope, getTaskSourceRuntimeSettings, diff --git a/src/renderer/src/store/slices/jira.ts b/src/renderer/src/store/slices/jira.ts index bb0423a2e38..418e4a7ad25 100644 --- a/src/renderer/src/store/slices/jira.ts +++ b/src/renderer/src/store/slices/jira.ts @@ -10,7 +10,7 @@ import type { JiraIssueFilter, JiraSiteSelection, JiraViewer -} from '../../../../shared/types' +} from '../../../../shared/jira-types' import type { CacheEntry } from './github' import { isIntegrationCredentialDecryptionError } from '../../../../shared/integration-credential-errors' import { diff --git a/src/renderer/src/store/slices/linear-invalidation.test.ts b/src/renderer/src/store/slices/linear-invalidation.test.ts index 95c1d99ffaf..03c917187fa 100644 --- a/src/renderer/src/store/slices/linear-invalidation.test.ts +++ b/src/renderer/src/store/slices/linear-invalidation.test.ts @@ -1,7 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { create } from 'zustand' import type { AppState } from '../types' -import type { LinearConnectionStatus, LinearIssue, LinearTeam } from '../../../../shared/types' +import type { LinearIssue } from '../../../../shared/linear/issue-types' +import type { LinearConnectionStatus, LinearTeam } from '../../../../shared/linear/workspace-types' import { createLinearSlice } from './linear' const linearStatus = vi.fn() diff --git a/src/renderer/src/store/slices/linear.test.ts b/src/renderer/src/store/slices/linear.test.ts index f18f27a3261..8fbf089757c 100644 --- a/src/renderer/src/store/slices/linear.test.ts +++ b/src/renderer/src/store/slices/linear.test.ts @@ -3,15 +3,17 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { create } from 'zustand' import type { AppState } from '../types' +import type { LinearIssue } from '../../../../shared/linear/issue-types' import type { - LinearConnectionStatus, - LinearCollectionResult, - LinearIssue, LinearProjectDetail, - LinearProjectSummary, + LinearProjectSummary +} from '../../../../shared/linear/project-types' +import type { + LinearCollectionResult, + LinearConnectionStatus, LinearTeam, LinearViewer -} from '../../../../shared/types' +} from '../../../../shared/linear/workspace-types' import { getTaskSourceCacheScope, type TaskSourceContext diff --git a/src/renderer/src/store/slices/linear.ts b/src/renderer/src/store/slices/linear.ts index 614020debd5..eddbbcf3086 100644 --- a/src/renderer/src/store/slices/linear.ts +++ b/src/renderer/src/store/slices/linear.ts @@ -3,20 +3,22 @@ boundary so cache invalidation stays coherent. */ import type { StateCreator } from 'zustand' import type { AppState } from '../types' +import type { LinearIssue } from '../../../../shared/linear/issue-types' import type { - LinearViewer, - LinearConnectionStatus, - LinearCollectionResult, LinearCustomViewModel, LinearCustomViewSummary, - LinearIssue, LinearProjectDetail, - LinearProjectSummary, + LinearProjectSummary +} from '../../../../shared/linear/project-types' +import type { + LinearCollectionResult, + LinearConnectionStatus, LinearTeam, + LinearViewer, LinearWorkspace, LinearWorkspaceError, LinearWorkspaceSelection -} from '../../../../shared/types' +} from '../../../../shared/linear/workspace-types' import type { CacheEntry } from './github' import { clampLinearIssueListLimit } from '../../../../shared/linear/issue-read-limits' import { isIntegrationCredentialDecryptionError } from '../../../../shared/integration-credential-errors' diff --git a/src/renderer/src/store/slices/local-detected-agent-state.test.ts b/src/renderer/src/store/slices/local-detected-agent-state.test.ts index 77303b8bc89..dd4636a24a6 100644 --- a/src/renderer/src/store/slices/local-detected-agent-state.test.ts +++ b/src/renderer/src/store/slices/local-detected-agent-state.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { create } from 'zustand' import type { AppState } from '../types' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' import { createDetectedAgentsSlice } from './detected-agents' diff --git a/src/renderer/src/store/slices/local-detected-agent-state.ts b/src/renderer/src/store/slices/local-detected-agent-state.ts index 43b39ff5980..62760a0851b 100644 --- a/src/renderer/src/store/slices/local-detected-agent-state.ts +++ b/src/renderer/src/store/slices/local-detected-agent-state.ts @@ -1,6 +1,6 @@ import type { StateCreator } from 'zustand' import type { AppState } from '../types' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' import { getLocalAgentPreflightContext, diff --git a/src/renderer/src/store/slices/local-detected-agent-store-state.ts b/src/renderer/src/store/slices/local-detected-agent-store-state.ts index c961bb5114f..2bd45db165c 100644 --- a/src/renderer/src/store/slices/local-detected-agent-store-state.ts +++ b/src/renderer/src/store/slices/local-detected-agent-store-state.ts @@ -1,5 +1,9 @@ import type { AppState } from '../types' -import type { PathSource, ShellHydrationFailureReason, TuiAgent } from '../../../../shared/types' +import type { + PathSource, + ShellHydrationFailureReason +} from '../../../../shared/shell-path-hydration-types' +import type { TuiAgent } from '../../../../shared/tui-agent' export type LocalDetectedAgentState = { detectedAgentIds: TuiAgent[] | null diff --git a/src/renderer/src/store/slices/memory.test.ts b/src/renderer/src/store/slices/memory.test.ts index a63b5d92288..cdcb7e454c5 100644 --- a/src/renderer/src/store/slices/memory.test.ts +++ b/src/renderer/src/store/slices/memory.test.ts @@ -2,7 +2,7 @@ import { create } from 'zustand' import { afterEach, describe, expect, it, vi } from 'vitest' import { createMemorySlice } from './memory' import type { AppState } from '../types' -import type { MemorySnapshot } from '../../../../shared/types' +import type { MemorySnapshot } from '../../../../shared/process-stats-types' function makeMemorySnapshot(overrides: Partial = {}): MemorySnapshot { return { diff --git a/src/renderer/src/store/slices/memory.ts b/src/renderer/src/store/slices/memory.ts index bc6b26f5066..3fdc80f4d51 100644 --- a/src/renderer/src/store/slices/memory.ts +++ b/src/renderer/src/store/slices/memory.ts @@ -1,6 +1,6 @@ import type { StateCreator } from 'zustand' import type { AppState } from '../types' -import type { MemorySnapshot } from '../../../../shared/types' +import type { MemorySnapshot } from '../../../../shared/process-stats-types' export type MemorySlice = { memorySnapshot: MemorySnapshot | null diff --git a/src/renderer/src/store/slices/new-issue-draft.test.ts b/src/renderer/src/store/slices/new-issue-draft.test.ts index 459cb1eb1f0..c6809c48560 100644 --- a/src/renderer/src/store/slices/new-issue-draft.test.ts +++ b/src/renderer/src/store/slices/new-issue-draft.test.ts @@ -2,7 +2,7 @@ import { create } from 'zustand' import { describe, expect, it } from 'vitest' import { createNewIssueDraftSlice } from './new-issue-draft' import type { AppState } from '../types' -import type { GitHubAssignableUser } from '../../../../shared/types' +import type { GitHubAssignableUser } from '../../../../shared/github/pull-request-types' function makeStore() { return create>()( diff --git a/src/renderer/src/store/slices/new-issue-draft.ts b/src/renderer/src/store/slices/new-issue-draft.ts index 6ef7bb31b28..1bd9f3a98f8 100644 --- a/src/renderer/src/store/slices/new-issue-draft.ts +++ b/src/renderer/src/store/slices/new-issue-draft.ts @@ -1,6 +1,6 @@ import type { StateCreator } from 'zustand' import type { AppState } from '../types' -import type { GitHubAssignableUser } from '../../../../shared/types' +import type { GitHubAssignableUser } from '../../../../shared/github/pull-request-types' /** In-progress "New GitHub issue" composer draft. Session-only (never * `persist`-wrapped, no disk surface): it exists so an accidental dismissal diff --git a/src/renderer/src/store/slices/new-markdown.test.ts b/src/renderer/src/store/slices/new-markdown.test.ts index b0f1ebb9997..fe592dd7bf9 100644 --- a/src/renderer/src/store/slices/new-markdown.test.ts +++ b/src/renderer/src/store/slices/new-markdown.test.ts @@ -2,7 +2,8 @@ import { createStore, type StoreApi } from 'zustand/vanilla' import { describe, expect, it } from 'vitest' import { createEditorSlice } from './editor' import type { AppState } from '../types' -import type { BrowserTab, Tab, TabContentType, TabGroup } from '../../../../shared/types' +import type { BrowserTab } from '../../../../shared/browser-workspace-types' +import type { Tab, TabContentType, TabGroup } from '../../../../shared/tab-types' function createEditorStore(overrides?: Partial): StoreApi { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/renderer/src/store/slices/pane-column-split-drop-no-op.test.ts b/src/renderer/src/store/slices/pane-column-split-drop-no-op.test.ts index f5c4d68b309..e8e98738241 100644 --- a/src/renderer/src/store/slices/pane-column-split-drop-no-op.test.ts +++ b/src/renderer/src/store/slices/pane-column-split-drop-no-op.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TabGroupLayoutNode } from '../../../../shared/types' +import type { TabGroupLayoutNode } from '../../../../shared/tab-types' import { findLayoutSiblingOnSplitSide, isPaneColumnSplitDropNoOp diff --git a/src/renderer/src/store/slices/pane-column-split-drop-no-op.ts b/src/renderer/src/store/slices/pane-column-split-drop-no-op.ts index 9f87e4a20a5..d149d477d5a 100644 --- a/src/renderer/src/store/slices/pane-column-split-drop-no-op.ts +++ b/src/renderer/src/store/slices/pane-column-split-drop-no-op.ts @@ -1,4 +1,4 @@ -import type { TabGroupLayoutNode } from '../../../../shared/types' +import type { TabGroupLayoutNode } from '../../../../shared/tab-types' import type { TabSplitDirection } from './tabs' function getDirectLayoutSiblingOnSplitSide( diff --git a/src/renderer/src/store/slices/pane-foreground-agent.test.ts b/src/renderer/src/store/slices/pane-foreground-agent.test.ts index 62363eee82c..a56158a99db 100644 --- a/src/renderer/src/store/slices/pane-foreground-agent.test.ts +++ b/src/renderer/src/store/slices/pane-foreground-agent.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { createTestStore } from './store-test-helpers' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' function terminalTab(id: string, worktreeId: string): TerminalTab { return { diff --git a/src/renderer/src/store/slices/pane-foreground-agent.ts b/src/renderer/src/store/slices/pane-foreground-agent.ts index 7adc8fea4ba..ebdd69e6865 100644 --- a/src/renderer/src/store/slices/pane-foreground-agent.ts +++ b/src/renderer/src/store/slices/pane-foreground-agent.ts @@ -1,6 +1,6 @@ import type { StateCreator } from 'zustand' import type { AppState } from '../types' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' export type PaneForegroundAgentEntry = { /** Recognized agent process in the pane's foreground; null when unknown. */ diff --git a/src/renderer/src/store/slices/preflight.test.ts b/src/renderer/src/store/slices/preflight.test.ts index 18ebaf839cd..386bdfdc59a 100644 --- a/src/renderer/src/store/slices/preflight.test.ts +++ b/src/renderer/src/store/slices/preflight.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it, vi } from 'vitest' import { create } from 'zustand' import type { PreflightStatus } from '../../../../preload/api-types' -import type { Repo, Worktree } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { AppState } from '../types' import { createPreflightSlice } from './preflight' import { createRuntimeStatusSlice } from './runtime-status' diff --git a/src/renderer/src/store/slices/project-group-removal-targets.test.ts b/src/renderer/src/store/slices/project-group-removal-targets.test.ts index 0cf5a53ace9..f193730fb5d 100644 --- a/src/renderer/src/store/slices/project-group-removal-targets.test.ts +++ b/src/renderer/src/store/slices/project-group-removal-targets.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { ProjectGroup, Repo } from '../../../../shared/types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' import { selectProjectGroupRemovalTargets } from './project-group-removal-targets' const rootGroup: ProjectGroup = { diff --git a/src/renderer/src/store/slices/project-group-removal-targets.ts b/src/renderer/src/store/slices/project-group-removal-targets.ts index 33bd8ba121a..ee0c11c3c4b 100644 --- a/src/renderer/src/store/slices/project-group-removal-targets.ts +++ b/src/renderer/src/store/slices/project-group-removal-targets.ts @@ -1,4 +1,5 @@ -import type { ProjectGroup, Repo } from '../../../../shared/types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' import { getProjectGroupSubtreeIds } from '../../../../shared/project-groups' export type ProjectGroupRemovalTargets = { diff --git a/src/renderer/src/store/slices/pull-request-generation.ts b/src/renderer/src/store/slices/pull-request-generation.ts index aa8dc76bd5f..cf306b316cf 100644 --- a/src/renderer/src/store/slices/pull-request-generation.ts +++ b/src/renderer/src/store/slices/pull-request-generation.ts @@ -1,5 +1,5 @@ import type { StateCreator } from 'zustand' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { AppState } from '../types' export type PullRequestFieldName = 'base' | 'title' | 'body' | 'draft' diff --git a/src/renderer/src/store/slices/purge-stale-runtime-host-state.test.ts b/src/renderer/src/store/slices/purge-stale-runtime-host-state.test.ts index 0d05535ed2d..36bf4aafc2d 100644 --- a/src/renderer/src/store/slices/purge-stale-runtime-host-state.test.ts +++ b/src/renderer/src/store/slices/purge-stale-runtime-host-state.test.ts @@ -10,7 +10,8 @@ import { describe, it, expect, vi } from 'vitest' import { toRuntimeExecutionHostId } from '../../../../shared/execution-host' import { worktreeWorkspaceKey } from '../../../../shared/workspace-scope' -import type { ProjectHostSetup, DetectedWorktreeListResult } from '../../../../shared/types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { DetectedWorktreeListResult } from '../../../../shared/worktree/types' vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn(), warning: vi.fn() } diff --git a/src/renderer/src/store/slices/remote-server-updates.ts b/src/renderer/src/store/slices/remote-server-updates.ts index 80b8a7370c1..07210ee7f97 100644 --- a/src/renderer/src/store/slices/remote-server-updates.ts +++ b/src/renderer/src/store/slices/remote-server-updates.ts @@ -4,7 +4,7 @@ import type { RemoteServerUpdaterSnapshot } from '../../../../shared/remote-serv import { isUserManagedRuntimeEnvironment } from '../../../../shared/runtime-environments' import type { RuntimeRpcResponse } from '../../../../shared/runtime-rpc-envelope' import type { RuntimeStatus } from '../../../../shared/runtime-types' -import type { UpdateCheckOptions } from '../../../../shared/types' +import type { UpdateCheckOptions } from '../../../../shared/update-status-types' import { unwrapRuntimeRpcResult } from '@/runtime/runtime-rpc-client' import { checkingRemoteServerUpdateEntry, diff --git a/src/renderer/src/store/slices/repo-host-identity.ts b/src/renderer/src/store/slices/repo-host-identity.ts index 772d5dc2925..a55198fd85c 100644 --- a/src/renderer/src/store/slices/repo-host-identity.ts +++ b/src/renderer/src/store/slices/repo-host-identity.ts @@ -1,4 +1,5 @@ -import type { GlobalSettings, Repo } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' import { getRepoExecutionHostId, getSettingsFocusedExecutionHostId, diff --git a/src/renderer/src/store/slices/repo-identity-reconcile.test.ts b/src/renderer/src/store/slices/repo-identity-reconcile.test.ts index ecc1fa7d59e..43afafe717f 100644 --- a/src/renderer/src/store/slices/repo-identity-reconcile.test.ts +++ b/src/renderer/src/store/slices/repo-identity-reconcile.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import fs from 'node:fs' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { reconcileFetchedRepos, reuseEqualRecordMap } from './repo-identity-reconcile' function makeRepo(id: string, overrides: Partial = {}): Repo { diff --git a/src/renderer/src/store/slices/repo-identity-reconcile.ts b/src/renderer/src/store/slices/repo-identity-reconcile.ts index ea118295754..0e2b4aa1147 100644 --- a/src/renderer/src/store/slices/repo-identity-reconcile.ts +++ b/src/renderer/src/store/slices/repo-identity-reconcile.ts @@ -1,4 +1,4 @@ -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { structuralValuesEqual } from '../../../../shared/structural-value-equality' import { getRepoHostIdentity } from './repo-host-identity' diff --git a/src/renderer/src/store/slices/repo-reorder-host-split.test.ts b/src/renderer/src/store/slices/repo-reorder-host-split.test.ts index fd4ace117f3..e3d4311e03f 100644 --- a/src/renderer/src/store/slices/repo-reorder-host-split.test.ts +++ b/src/renderer/src/store/slices/repo-reorder-host-split.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { splitRepoReorderByHost } from './repo-reorder-host-split' function repo(id: string, executionHostId: string | null): Repo { diff --git a/src/renderer/src/store/slices/repo-reorder-host-split.ts b/src/renderer/src/store/slices/repo-reorder-host-split.ts index 9924df17c7f..82ad29bd6dd 100644 --- a/src/renderer/src/store/slices/repo-reorder-host-split.ts +++ b/src/renderer/src/store/slices/repo-reorder-host-split.ts @@ -1,4 +1,5 @@ -import type { GlobalSettings, Repo } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' +import type { Repo } from '../../../../shared/repo-types' import { getRepoExecutionHostId, getSettingsFocusedExecutionHostId diff --git a/src/renderer/src/store/slices/repos-all-hosts-folder-workspaces.test.ts b/src/renderer/src/store/slices/repos-all-hosts-folder-workspaces.test.ts index bbbd90d6440..148ea2209dc 100644 --- a/src/renderer/src/store/slices/repos-all-hosts-folder-workspaces.test.ts +++ b/src/renderer/src/store/slices/repos-all-hosts-folder-workspaces.test.ts @@ -1,6 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createTestStore } from './store-test-helpers' -import type { FolderWorkspace, ProjectGroup, Repo } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/repos-all-hosts-generation.test.ts b/src/renderer/src/store/slices/repos-all-hosts-generation.test.ts index 1aa9ed76072..2d89d53ceac 100644 --- a/src/renderer/src/store/slices/repos-all-hosts-generation.test.ts +++ b/src/renderer/src/store/slices/repos-all-hosts-generation.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/repos-all-hosts.test.ts b/src/renderer/src/store/slices/repos-all-hosts.test.ts index 9d38bc0bf8c..17c9227aa5e 100644 --- a/src/renderer/src/store/slices/repos-all-hosts.test.ts +++ b/src/renderer/src/store/slices/repos-all-hosts.test.ts @@ -1,12 +1,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createTestStore } from './store-test-helpers' -import type { - FolderWorkspace, - Project, - ProjectHostSetup, - ProjectGroup, - Repo -} from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/repos-catalog-merge-identity.test.ts b/src/renderer/src/store/slices/repos-catalog-merge-identity.test.ts index 69f73e589b5..4cf74a63640 100644 --- a/src/renderer/src/store/slices/repos-catalog-merge-identity.test.ts +++ b/src/renderer/src/store/slices/repos-catalog-merge-identity.test.ts @@ -1,5 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { FolderWorkspace, ProjectGroup, Repo } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' import type { FolderWorkspacePathStatusCacheEntry } from './repos' import { createTestStore } from './store-test-helpers' diff --git a/src/renderer/src/store/slices/repos-cross-host-project-collisions.test.ts b/src/renderer/src/store/slices/repos-cross-host-project-collisions.test.ts index 5908c56118a..d3cac1929aa 100644 --- a/src/renderer/src/store/slices/repos-cross-host-project-collisions.test.ts +++ b/src/renderer/src/store/slices/repos-cross-host-project-collisions.test.ts @@ -10,7 +10,7 @@ */ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createTestStore } from './store-test-helpers' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/repos-cross-host-refresh-identity.test.ts b/src/renderer/src/store/slices/repos-cross-host-refresh-identity.test.ts index 6218d251607..9169384b55e 100644 --- a/src/renderer/src/store/slices/repos-cross-host-refresh-identity.test.ts +++ b/src/renderer/src/store/slices/repos-cross-host-refresh-identity.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createTestStore } from './store-test-helpers' -import type { Project, ProjectHostSetup, Repo } from '../../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/repos-host-identity-routing.test.ts b/src/renderer/src/store/slices/repos-host-identity-routing.test.ts index 33ad35355fb..5eba51545d4 100644 --- a/src/renderer/src/store/slices/repos-host-identity-routing.test.ts +++ b/src/renderer/src/store/slices/repos-host-identity-routing.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createTestStore, makeWorktree } from './store-test-helpers' -import type { Project, ProjectHostSetup, Repo } from '../../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/repos-manual-order-hydration.test.ts b/src/renderer/src/store/slices/repos-manual-order-hydration.test.ts index 91374d47701..ce93d07e280 100644 --- a/src/renderer/src/store/slices/repos-manual-order-hydration.test.ts +++ b/src/renderer/src/store/slices/repos-manual-order-hydration.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { createTestStore } from './store-test-helpers' import { createCompatibleRuntimeStatusResponseIfNeeded, diff --git a/src/renderer/src/store/slices/repos-nested-ssh-projection.test.ts b/src/renderer/src/store/slices/repos-nested-ssh-projection.test.ts index bcdc36afdaf..9ced00f07fc 100644 --- a/src/renderer/src/store/slices/repos-nested-ssh-projection.test.ts +++ b/src/renderer/src/store/slices/repos-nested-ssh-projection.test.ts @@ -1,5 +1,5 @@ import { expect, it, vi } from 'vitest' -import type { Project, ProjectHostSetup } from '../../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' import { createTestStore } from './store-test-helpers' import { installReposRuntimeRoutingHarness, diff --git a/src/renderer/src/store/slices/repos-project-groups-delete.test.ts b/src/renderer/src/store/slices/repos-project-groups-delete.test.ts index b1692509c00..4f063f1eba3 100644 --- a/src/renderer/src/store/slices/repos-project-groups-delete.test.ts +++ b/src/renderer/src/store/slices/repos-project-groups-delete.test.ts @@ -1,6 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createTestStore } from './store-test-helpers' -import type { FolderWorkspace, ProjectGroup, Repo } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/repos-project-groups.test.ts b/src/renderer/src/store/slices/repos-project-groups.test.ts index 9fbf455587b..281fbde53e8 100644 --- a/src/renderer/src/store/slices/repos-project-groups.test.ts +++ b/src/renderer/src/store/slices/repos-project-groups.test.ts @@ -1,11 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createTestStore } from './store-test-helpers' -import type { - NestedRepoScanResult, - Repo, - ProjectGroup, - FolderWorkspace -} from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { NestedRepoScanResult, ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' import { createCompatibleRuntimeStatusResponse, createCompatibleRuntimeStatusResponseIfNeeded, diff --git a/src/renderer/src/store/slices/repos-project-host-capability.test.ts b/src/renderer/src/store/slices/repos-project-host-capability.test.ts index 862babeb51c..2bed7180315 100644 --- a/src/renderer/src/store/slices/repos-project-host-capability.test.ts +++ b/src/renderer/src/store/slices/repos-project-host-capability.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { PROJECT_HOST_SETUP_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version' import { clearRuntimeCompatibilityCacheForTests } from '../../runtime/runtime-rpc-client' import type { RuntimeEnvironmentCallRequest } from '../../runtime/runtime-compatibility-test-fixture' diff --git a/src/renderer/src/store/slices/repos-project-host-lifecycle.test.ts b/src/renderer/src/store/slices/repos-project-host-lifecycle.test.ts index 55ea9c1caea..667c1d308bb 100644 --- a/src/renderer/src/store/slices/repos-project-host-lifecycle.test.ts +++ b/src/renderer/src/store/slices/repos-project-host-lifecycle.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Project, ProjectHostSetup, Repo } from '../../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/repos-project-runtime.test.ts b/src/renderer/src/store/slices/repos-project-runtime.test.ts index ba61fd6a552..6df79a4af58 100644 --- a/src/renderer/src/store/slices/repos-project-runtime.test.ts +++ b/src/renderer/src/store/slices/repos-project-runtime.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createTestStore } from './store-test-helpers' -import type { Project, ProjectHostSetup, Repo } from '../../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/repos-refresh-identity.test.ts b/src/renderer/src/store/slices/repos-refresh-identity.test.ts index 98750177831..78cc36eb0e7 100644 --- a/src/renderer/src/store/slices/repos-refresh-identity.test.ts +++ b/src/renderer/src/store/slices/repos-refresh-identity.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Project, Repo } from '../../../../shared/types' +import type { Project } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { getSetupScriptPromptDismissalKey } from '../../lib/setup-script-prompt' import { getRepoHostIdentityForParts } from './repo-host-identity' import { createTestStore } from './store-test-helpers' @@ -472,9 +473,11 @@ describe('SSH readoption catalog identity', () => { expect(oldSetup).toBeDefined() expect(newSetup).toBeDefined() - store.getState().recordSshRepoReadoptions([ - { oldTargetId: 'ssh-old', newTargetId: 'ssh-new', repoIds: [repo.id] } - ]) + store + .getState() + .recordSshRepoReadoptions([ + { oldTargetId: 'ssh-old', newTargetId: 'ssh-new', repoIds: [repo.id] } + ]) const next = store.getState().projectHostSetups expect(next).not.toBe(setups) @@ -486,4 +489,3 @@ describe('SSH readoption catalog identity', () => { expect(store.getState().pendingSshRepoReadoptions).toEqual([]) }) }) - diff --git a/src/renderer/src/store/slices/repos-remove-missing-remote-project.test.ts b/src/renderer/src/store/slices/repos-remove-missing-remote-project.test.ts index ef9c5bd7431..78e7896f7c5 100644 --- a/src/renderer/src/store/slices/repos-remove-missing-remote-project.test.ts +++ b/src/renderer/src/store/slices/repos-remove-missing-remote-project.test.ts @@ -9,7 +9,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { toast } from 'sonner' import { createTestStore } from './store-test-helpers' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/repos-remove-project-purge-leak.test.ts b/src/renderer/src/store/slices/repos-remove-project-purge-leak.test.ts index 69bebba9240..2c35bb35fb8 100644 --- a/src/renderer/src/store/slices/repos-remove-project-purge-leak.test.ts +++ b/src/renderer/src/store/slices/repos-remove-project-purge-leak.test.ts @@ -15,7 +15,7 @@ */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { createTestStore, makeWorktree } from './store-test-helpers' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' const repo1: Repo = { id: 'repo-1', path: '/r1', displayName: 'R1', badgeColor: '#000', addedAt: 1 } const repo2: Repo = { id: 'repo-2', path: '/r2', displayName: 'R2', badgeColor: '#111', addedAt: 2 } diff --git a/src/renderer/src/store/slices/repos-runtime-project-groups.test.ts b/src/renderer/src/store/slices/repos-runtime-project-groups.test.ts index bab610f598b..c01ccf818b7 100644 --- a/src/renderer/src/store/slices/repos-runtime-project-groups.test.ts +++ b/src/renderer/src/store/slices/repos-runtime-project-groups.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/repos-runtime-routing-fixture.ts b/src/renderer/src/store/slices/repos-runtime-routing-fixture.ts index ef96a644e77..5df2606b057 100644 --- a/src/renderer/src/store/slices/repos-runtime-routing-fixture.ts +++ b/src/renderer/src/store/slices/repos-runtime-routing-fixture.ts @@ -1,6 +1,6 @@ import { beforeEach, vi, type Mock } from 'vitest' import { toast } from 'sonner' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/repos-selected-owner-routing.test.ts b/src/renderer/src/store/slices/repos-selected-owner-routing.test.ts index 691a9fbdaaf..37922e968d8 100644 --- a/src/renderer/src/store/slices/repos-selected-owner-routing.test.ts +++ b/src/renderer/src/store/slices/repos-selected-owner-routing.test.ts @@ -1,5 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { FolderWorkspace, ProjectGroup, Repo } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Repo } from '../../../../shared/repo-types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/repos-setup-script-dismissals.test.ts b/src/renderer/src/store/slices/repos-setup-script-dismissals.test.ts index f805b66ac0c..6ddcdf99f9f 100644 --- a/src/renderer/src/store/slices/repos-setup-script-dismissals.test.ts +++ b/src/renderer/src/store/slices/repos-setup-script-dismissals.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { getSetupScriptPromptDismissalKey } from '../../lib/setup-script-prompt' import { createTestStore } from './store-test-helpers' import { getRepoHostIdentityForParts } from './repo-host-identity' diff --git a/src/renderer/src/store/slices/repos-shared-project-badge-color.test.ts b/src/renderer/src/store/slices/repos-shared-project-badge-color.test.ts index 77948ba5ed7..bc143733556 100644 --- a/src/renderer/src/store/slices/repos-shared-project-badge-color.test.ts +++ b/src/renderer/src/store/slices/repos-shared-project-badge-color.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createTestStore } from './store-test-helpers' -import type { Project, ProjectHostSetup, Repo } from '../../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { createCompatibleRuntimeStatusResponseIfNeeded, type RuntimeEnvironmentCallRequest diff --git a/src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts b/src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts index e32f2da3699..58d694c84d0 100644 --- a/src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts +++ b/src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts @@ -8,7 +8,9 @@ import type { SshConnectionState, SshProviderEpoch } from '../../../../shared/ssh-types' -import type { Project, ProjectHostSetup, Repo, Worktree } from '../../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' import { createTestStore } from './store-test-helpers' const repoId = 're-adopted-repo' diff --git a/src/renderer/src/store/slices/repos-stale-fetch.test.ts b/src/renderer/src/store/slices/repos-stale-fetch.test.ts index c3c73d204ce..0c25f864cec 100644 --- a/src/renderer/src/store/slices/repos-stale-fetch.test.ts +++ b/src/renderer/src/store/slices/repos-stale-fetch.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest' import { createTestStore, makeLayout, makeTab } from './store-test-helpers' -import type { Repo, WorkspaceSessionState } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' import { getDefaultWorkspaceSession } from '../../../../shared/constants' import { clearRuntimeCompatibilityCacheForTests } from '../../runtime/runtime-rpc-client' diff --git a/src/renderer/src/store/slices/repos-update-serialization.test.ts b/src/renderer/src/store/slices/repos-update-serialization.test.ts index 811d4c81f07..ebfc23161d5 100644 --- a/src/renderer/src/store/slices/repos-update-serialization.test.ts +++ b/src/renderer/src/store/slices/repos-update-serialization.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createTestStore } from './store-test-helpers' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' const localRepo: Repo = { id: 'local-repo', diff --git a/src/renderer/src/store/slices/repos.runtime-fallback.test.ts b/src/renderer/src/store/slices/repos.runtime-fallback.test.ts index 9847481fd34..a5c38b8917b 100644 --- a/src/renderer/src/store/slices/repos.runtime-fallback.test.ts +++ b/src/renderer/src/store/slices/repos.runtime-fallback.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createTestStore } from './store-test-helpers' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { FOLDER_WORKSPACE_PATH_STATUS_RUNTIME_CAPABILITY, RUNTIME_CAPABILITIES diff --git a/src/renderer/src/store/slices/repos.test.ts b/src/renderer/src/store/slices/repos.test.ts index efd5a998375..83a3564cf5b 100644 --- a/src/renderer/src/store/slices/repos.test.ts +++ b/src/renderer/src/store/slices/repos.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it, vi } from 'vitest' import { createTestStore, makeWorktree } from './store-test-helpers' import { workItemsCacheKey } from './github' -import type { Project, ProjectHostSetup, Repo } from '../../../../shared/types' +import type { Project, ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { toast } from 'sonner' import { installReposRuntimeRoutingHarness, diff --git a/src/renderer/src/store/slices/repos.ts b/src/renderer/src/store/slices/repos.ts index 3cfa784209d..a619f5c0182 100644 --- a/src/renderer/src/store/slices/repos.ts +++ b/src/renderer/src/store/slices/repos.ts @@ -3,16 +3,16 @@ import type { StateCreator } from 'zustand' import { toast } from 'sonner' import type { AppState } from '../types' import type { SshRepoReadoption } from '../../../../shared/ssh-types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import type { - GlobalSettings, - Project, - ProjectUpdateArgs, - Repo, - ProjectGroup, - ProjectHostSetup, - FolderWorkspace, - ProjectGroupImportResult, NestedRepoScanResult, + ProjectGroup, + ProjectGroupImportResult +} from '../../../../shared/project-group-types' +import type { + Project, + ProjectHostSetup, ProjectHostSetupCloneArgs, ProjectHostSetupCreateArgs, ProjectHostSetupCreateResult, @@ -21,8 +21,10 @@ import type { ProjectHostSetupExistingFolderArgs, ProjectHostSetupResult, ProjectHostSetupUpdateArgs, - ProjectHostSetupUpdateResult -} from '../../../../shared/types' + ProjectHostSetupUpdateResult, + ProjectUpdateArgs +} from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' import { getProjectIdentityKey, mergeCatalogCreatedAt, diff --git a/src/renderer/src/store/slices/restored-editor-owner-reparent.test.ts b/src/renderer/src/store/slices/restored-editor-owner-reparent.test.ts index d3e8f37f15f..9e7cf558ba2 100644 --- a/src/renderer/src/store/slices/restored-editor-owner-reparent.test.ts +++ b/src/renderer/src/store/slices/restored-editor-owner-reparent.test.ts @@ -15,7 +15,7 @@ import { getEditorFileOperationContext } from '@/lib/editor-file-operation-owner' import type { AppState } from '@/store/types' -import type { WorkspaceSessionState } from '../../../../shared/types' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' const SOURCE = 'repo-a::/repo-a' const TARGET = 'repo-b::/repo-b' diff --git a/src/renderer/src/store/slices/runtime-catalog-merge-removed-env-guard.test.ts b/src/renderer/src/store/slices/runtime-catalog-merge-removed-env-guard.test.ts index 972a78b1ad2..76a28ae2634 100644 --- a/src/renderer/src/store/slices/runtime-catalog-merge-removed-env-guard.test.ts +++ b/src/renderer/src/store/slices/runtime-catalog-merge-removed-env-guard.test.ts @@ -9,7 +9,7 @@ */ import { describe, it, expect, vi } from 'vitest' import type * as RuntimeRpcClientModule from '@/runtime/runtime-rpc-client' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import type { PublicKnownRuntimeEnvironment } from '../../../../shared/runtime-environments' vi.mock('sonner', () => ({ diff --git a/src/renderer/src/store/slices/runtime-detected-agents.ts b/src/renderer/src/store/slices/runtime-detected-agents.ts index 466c131f4e7..daac8ffada0 100644 --- a/src/renderer/src/store/slices/runtime-detected-agents.ts +++ b/src/renderer/src/store/slices/runtime-detected-agents.ts @@ -1,6 +1,6 @@ import type { StateCreator } from 'zustand' import type { AppState } from '../types' -import type { TuiAgent } from '../../../../shared/types' +import type { TuiAgent } from '../../../../shared/tui-agent' import { callRuntimeRpc, RuntimeRpcCallError } from '@/runtime/runtime-rpc-client' // Why: remote runtime hosts are not SSH connections, but their launch surfaces diff --git a/src/renderer/src/store/slices/runtime-host-purge-session-partition-split.test.ts b/src/renderer/src/store/slices/runtime-host-purge-session-partition-split.test.ts index da09283ffcb..4b3d95b384c 100644 --- a/src/renderer/src/store/slices/runtime-host-purge-session-partition-split.test.ts +++ b/src/renderer/src/store/slices/runtime-host-purge-session-partition-split.test.ts @@ -7,7 +7,7 @@ */ import { describe, it, expect, vi } from 'vitest' import { toRuntimeExecutionHostId } from '../../../../shared/execution-host' -import type { WorkspaceSessionPatch } from '../../../../shared/types' +import type { WorkspaceSessionPatch } from '../../../../shared/workspace-session-state-types' import { buildHostIdByWorktreeId, patchWorkspaceSessionByHost, diff --git a/src/renderer/src/store/slices/selected-host-active-workspace-identity.test.ts b/src/renderer/src/store/slices/selected-host-active-workspace-identity.test.ts index a18fbfe23ce..fa07d59a718 100644 --- a/src/renderer/src/store/slices/selected-host-active-workspace-identity.test.ts +++ b/src/renderer/src/store/slices/selected-host-active-workspace-identity.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it } from 'vitest' import type { ExecutionHostId } from '../../../../shared/execution-host' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner' import { useAppStore } from '@/store' diff --git a/src/renderer/src/store/slices/set-runtime-environments-purge-wiring.test.ts b/src/renderer/src/store/slices/set-runtime-environments-purge-wiring.test.ts index 80d4c24daad..41afe5a067f 100644 --- a/src/renderer/src/store/slices/set-runtime-environments-purge-wiring.test.ts +++ b/src/renderer/src/store/slices/set-runtime-environments-purge-wiring.test.ts @@ -6,7 +6,7 @@ */ import { describe, it, expect, vi } from 'vitest' import { toRuntimeExecutionHostId } from '../../../../shared/execution-host' -import type { ProjectHostSetup } from '../../../../shared/types' +import type { ProjectHostSetup } from '../../../../shared/project-types' import type { PublicKnownRuntimeEnvironment } from '../../../../shared/runtime-environments' vi.mock('sonner', () => ({ diff --git a/src/renderer/src/store/slices/settings.test.ts b/src/renderer/src/store/slices/settings.test.ts index 756e6d9baa8..3f55f065358 100644 --- a/src/renderer/src/store/slices/settings.test.ts +++ b/src/renderer/src/store/slices/settings.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest' import { createTestStore, makeWorktree } from './store-test-helpers' import type { AppState } from '../types' -import type { WorktreeLineage } from '../../../../shared/types' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' import type { PublicKnownRuntimeEnvironment } from '../../../../shared/runtime-environments' import { toast } from 'sonner' import { diff --git a/src/renderer/src/store/slices/settings.ts b/src/renderer/src/store/slices/settings.ts index 593d1650e1e..22cf42b8cc6 100644 --- a/src/renderer/src/store/slices/settings.ts +++ b/src/renderer/src/store/slices/settings.ts @@ -1,6 +1,6 @@ import type { StateCreator } from 'zustand' import type { AppState } from '../types' -import type { GlobalSettings } from '../../../../shared/types' +import type { GlobalSettings } from '../../../../shared/global-settings-types' import { toast } from 'sonner' import { clearRuntimeCompatibilityCache, diff --git a/src/renderer/src/store/slices/sparse-presets-repo-removal-purge-leak.test.ts b/src/renderer/src/store/slices/sparse-presets-repo-removal-purge-leak.test.ts index d03b8418345..429e3e793d8 100644 --- a/src/renderer/src/store/slices/sparse-presets-repo-removal-purge-leak.test.ts +++ b/src/renderer/src/store/slices/sparse-presets-repo-removal-purge-leak.test.ts @@ -9,7 +9,8 @@ */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { createTestStore, makeWorktree } from './store-test-helpers' -import type { Repo, SparsePreset } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' +import type { SparsePreset } from '../../../../shared/worktree/create-types' const repo1: Repo = { id: 'repo-1', path: '/r1', displayName: 'R1', badgeColor: '#000', addedAt: 1 } const repo2: Repo = { id: 'repo-2', path: '/r2', displayName: 'R2', badgeColor: '#111', addedAt: 2 } diff --git a/src/renderer/src/store/slices/sparse-presets.test.ts b/src/renderer/src/store/slices/sparse-presets.test.ts index 5b41551ab21..c8192983c1d 100644 --- a/src/renderer/src/store/slices/sparse-presets.test.ts +++ b/src/renderer/src/store/slices/sparse-presets.test.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { SparsePreset } from '../../../../shared/types' +import type { SparsePreset } from '../../../../shared/worktree/create-types' import type { AppState } from '../types' import { createSparsePresetsSlice } from './sparse-presets' diff --git a/src/renderer/src/store/slices/sparse-presets.ts b/src/renderer/src/store/slices/sparse-presets.ts index af118c7703b..b9e763bf72e 100644 --- a/src/renderer/src/store/slices/sparse-presets.ts +++ b/src/renderer/src/store/slices/sparse-presets.ts @@ -1,7 +1,7 @@ import type { StateCreator } from 'zustand' import { toast } from 'sonner' import type { AppState } from '../types' -import type { SparsePreset } from '../../../../shared/types' +import type { SparsePreset } from '../../../../shared/worktree/create-types' import { translate } from '@/i18n/i18n' const ERROR_TOAST_DURATION = 60_000 diff --git a/src/renderer/src/store/slices/stats.ts b/src/renderer/src/store/slices/stats.ts index 290ee9f44c0..19e87066be3 100644 --- a/src/renderer/src/store/slices/stats.ts +++ b/src/renderer/src/store/slices/stats.ts @@ -1,6 +1,6 @@ import type { StateCreator } from 'zustand' import type { AppState } from '../types' -import type { StatsSummary } from '../../../../shared/types' +import type { StatsSummary } from '../../../../shared/process-stats-types' export type StatsSlice = { statsSummary: StatsSummary | null diff --git a/src/renderer/src/store/slices/store-session-cascades.test.ts b/src/renderer/src/store/slices/store-session-cascades.test.ts index 65474236f67..3ce0ac52a74 100644 --- a/src/renderer/src/store/slices/store-session-cascades.test.ts +++ b/src/renderer/src/store/slices/store-session-cascades.test.ts @@ -1,12 +1,8 @@ /* eslint-disable max-lines */ import { describe, it, expect, vi, beforeEach } from 'vitest' import type * as AgentStatusModule from '@/lib/agent-status' -import type { - BrowserPage, - BrowserTab, - DetectedWorktreeListResult, - Worktree -} from '../../../../shared/types' +import type { BrowserPage, BrowserTab } from '../../../../shared/browser-workspace-types' +import type { DetectedWorktreeListResult, Worktree } from '../../../../shared/worktree/types' import { isTerminalLeafId } from '../../../../shared/stable-pane-id' import { FLOATING_TERMINAL_WORKTREE_ID, diff --git a/src/renderer/src/store/slices/store-test-helpers.ts b/src/renderer/src/store/slices/store-test-helpers.ts index a4117e25260..1878bb3e7ce 100644 --- a/src/renderer/src/store/slices/store-test-helpers.ts +++ b/src/renderer/src/store/slices/store-test-helpers.ts @@ -1,12 +1,8 @@ import { create } from 'zustand' import type { AppState } from '../types' -import type { - Worktree, - TerminalTab, - TerminalLayoutSnapshot, - Tab, - TabGroup -} from '../../../../shared/types' +import type { Tab, TabGroup } from '../../../../shared/tab-types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { OpenFile } from './editor' import { createRepoSlice } from './repos' import { createSparsePresetsSlice } from './sparse-presets' diff --git a/src/renderer/src/store/slices/superseded-ssh-repo-rows.test.ts b/src/renderer/src/store/slices/superseded-ssh-repo-rows.test.ts index 7b50312d7a5..42f89222f35 100644 --- a/src/renderer/src/store/slices/superseded-ssh-repo-rows.test.ts +++ b/src/renderer/src/store/slices/superseded-ssh-repo-rows.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { mergeSshRepoReadoptions, reconcileReadoptedSshRepoRows } from './superseded-ssh-repo-rows' function repo(overrides: Partial): Repo { diff --git a/src/renderer/src/store/slices/superseded-ssh-repo-rows.ts b/src/renderer/src/store/slices/superseded-ssh-repo-rows.ts index a9880577f60..4dc3d9399df 100644 --- a/src/renderer/src/store/slices/superseded-ssh-repo-rows.ts +++ b/src/renderer/src/store/slices/superseded-ssh-repo-rows.ts @@ -1,5 +1,5 @@ import type { SshRepoReadoption } from '../../../../shared/ssh-types' -import type { Repo } from '../../../../shared/types' +import type { Repo } from '../../../../shared/repo-types' import { getRepoExecutionHostId, toSshExecutionHostId } from '../../../../shared/execution-host' import { getRepoHostIdentityForParts } from '../../../../shared/repo-host-identity' diff --git a/src/renderer/src/store/slices/tab-group-reference-repair.ts b/src/renderer/src/store/slices/tab-group-reference-repair.ts index 42a742cc724..e992139cf28 100644 --- a/src/renderer/src/store/slices/tab-group-reference-repair.ts +++ b/src/renderer/src/store/slices/tab-group-reference-repair.ts @@ -1,4 +1,4 @@ -import type { Tab, TabGroup, TabGroupLayoutNode } from '../../../../shared/types' +import type { Tab, TabGroup, TabGroupLayoutNode } from '../../../../shared/tab-types' import { createBrowserUuid } from '@/lib/browser-uuid' function collectLayoutGroupIds(node: TabGroupLayoutNode, groupIds: Set): void { diff --git a/src/renderer/src/store/slices/tab-group-state.test.ts b/src/renderer/src/store/slices/tab-group-state.test.ts index 22529cdea8e..9ec2d00b7ea 100644 --- a/src/renderer/src/store/slices/tab-group-state.test.ts +++ b/src/renderer/src/store/slices/tab-group-state.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import type { Tab, TabGroup } from '../../../../shared/types' +import type { Tab, TabGroup } from '../../../../shared/tab-types' import { findTabAndWorktree, findGroupForTab, diff --git a/src/renderer/src/store/slices/tab-group-state.ts b/src/renderer/src/store/slices/tab-group-state.ts index 6563829f89d..838433651c7 100644 --- a/src/renderer/src/store/slices/tab-group-state.ts +++ b/src/renderer/src/store/slices/tab-group-state.ts @@ -1,4 +1,5 @@ -import type { Tab, TabContentType, TabGroup, WorkspaceSessionState } from '../../../../shared/types' +import type { Tab, TabContentType, TabGroup } from '../../../../shared/tab-types' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' import { createBrowserUuid } from '@/lib/browser-uuid' export function findTabAndWorktree( diff --git a/src/renderer/src/store/slices/tabs-hydration-generated-title.test.ts b/src/renderer/src/store/slices/tabs-hydration-generated-title.test.ts index 1f0b2501174..dc8d7836a2a 100644 --- a/src/renderer/src/store/slices/tabs-hydration-generated-title.test.ts +++ b/src/renderer/src/store/slices/tabs-hydration-generated-title.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { WorkspaceSessionState } from '../../../../shared/types' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' import { buildHydratedTabState } from './tabs-hydration' function makeBaseSession(): WorkspaceSessionState { diff --git a/src/renderer/src/store/slices/tabs-hydration-group-validation.test.ts b/src/renderer/src/store/slices/tabs-hydration-group-validation.test.ts index dca253f4733..70d81deb851 100644 --- a/src/renderer/src/store/slices/tabs-hydration-group-validation.test.ts +++ b/src/renderer/src/store/slices/tabs-hydration-group-validation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { WorkspaceSessionState } from '../../../../shared/types' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' import { buildHydratedTabState } from './tabs-hydration' function makeBaseSession(): WorkspaceSessionState { diff --git a/src/renderer/src/store/slices/tabs-hydration.test.ts b/src/renderer/src/store/slices/tabs-hydration.test.ts index afd5a05eced..bab64605273 100644 --- a/src/renderer/src/store/slices/tabs-hydration.test.ts +++ b/src/renderer/src/store/slices/tabs-hydration.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from 'vitest' -import type { WorkspaceSessionState } from '../../../../shared/types' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' import { buildHydratedTabState } from './tabs-hydration' vi.stubGlobal('crypto', { randomUUID: () => `uuid-${Math.random().toString(36).slice(2, 8)}` }) diff --git a/src/renderer/src/store/slices/tabs-hydration.ts b/src/renderer/src/store/slices/tabs-hydration.ts index 2fb85ab3808..d3a8dd97c01 100644 --- a/src/renderer/src/store/slices/tabs-hydration.ts +++ b/src/renderer/src/store/slices/tabs-hydration.ts @@ -1,9 +1,5 @@ -import type { - Tab, - TabGroup, - TabGroupLayoutNode, - WorkspaceSessionState -} from '../../../../shared/types' +import type { Tab, TabGroup, TabGroupLayoutNode } from '../../../../shared/tab-types' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' import { isValidTerminalTabId } from '../../../../shared/terminal-tab-id' import { createBrowserUuid } from '@/lib/browser-uuid' import { adoptGrouplessTabs, layoutSpanningGroups } from './tab-group-reference-repair' diff --git a/src/renderer/src/store/slices/tabs.test.ts b/src/renderer/src/store/slices/tabs.test.ts index c7bab81ff81..1599c2b8db1 100644 --- a/src/renderer/src/store/slices/tabs.test.ts +++ b/src/renderer/src/store/slices/tabs.test.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ import { describe, it, expect, vi, beforeEach } from 'vitest' -import type { Tab, TabGroup } from '../../../../shared/types' +import type { Tab, TabGroup } from '../../../../shared/tab-types' import type * as AgentStatusModule from '@/lib/agent-status' import { FLOATING_TERMINAL_WORKTREE_ID, getDefaultUIState } from '../../../../shared/constants' import { buildMobileSessionTabSnapshots } from '../../runtime/sync-runtime-graph' diff --git a/src/renderer/src/store/slices/tabs.ts b/src/renderer/src/store/slices/tabs.ts index 10b3ddbda0f..1c318830e0f 100644 --- a/src/renderer/src/store/slices/tabs.ts +++ b/src/renderer/src/store/slices/tabs.ts @@ -6,11 +6,11 @@ import type { TabContentType, TabGroup, TabGroupLayoutNode, - TerminalTab, - TuiAgent, - WorkspaceSessionState, WorkspaceVisibleTabType -} from '../../../../shared/types' +} from '../../../../shared/tab-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../../shared/tui-agent' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' import { emitNativeChatToggled } from '@/lib/native-chat-telemetry' import { dedupeTabOrder, diff --git a/src/renderer/src/store/slices/terminal-activation-spawn-suppression.ts b/src/renderer/src/store/slices/terminal-activation-spawn-suppression.ts index 203736af29d..500b2f4cf35 100644 --- a/src/renderer/src/store/slices/terminal-activation-spawn-suppression.ts +++ b/src/renderer/src/store/slices/terminal-activation-spawn-suppression.ts @@ -1,4 +1,7 @@ -import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode } from '../../../../shared/types' +import type { + TerminalLayoutSnapshot, + TerminalPaneLayoutNode +} from '../../../../shared/terminal-tab-types' function countTerminalLayoutLeaves(node: TerminalPaneLayoutNode | null | undefined): number { if (!node) { diff --git a/src/renderer/src/store/slices/terminal-helpers.test.ts b/src/renderer/src/store/slices/terminal-helpers.test.ts index 93c6f395d3a..ee84ad2faf7 100644 --- a/src/renderer/src/store/slices/terminal-helpers.test.ts +++ b/src/renderer/src/store/slices/terminal-helpers.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { emptyLayoutSnapshot, clearTransientTerminalState } from './terminal-helpers' function makeTab(overrides: Partial = {}): TerminalTab { diff --git a/src/renderer/src/store/slices/terminal-helpers.ts b/src/renderer/src/store/slices/terminal-helpers.ts index 7f42c049ab6..3815849ea14 100644 --- a/src/renderer/src/store/slices/terminal-helpers.ts +++ b/src/renderer/src/store/slices/terminal-helpers.ts @@ -1,4 +1,4 @@ -import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' import { classifyTitleActivity } from '@/lib/pane-agent-evidence' export function emptyLayoutSnapshot(): TerminalLayoutSnapshot { diff --git a/src/renderer/src/store/slices/terminal-orphan-helpers.test.ts b/src/renderer/src/store/slices/terminal-orphan-helpers.test.ts index 0058213701d..00472302e2a 100644 --- a/src/renderer/src/store/slices/terminal-orphan-helpers.test.ts +++ b/src/renderer/src/store/slices/terminal-orphan-helpers.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { getOrphanTerminalIds } from './terminal-orphan-helpers' import { buildTerminalTabRetirementPlan } from './terminal-tab-retirement' diff --git a/src/renderer/src/store/slices/terminal-pane-detach-agent-retention.test.ts b/src/renderer/src/store/slices/terminal-pane-detach-agent-retention.test.ts index 257753fa58d..5545b3678f6 100644 --- a/src/renderer/src/store/slices/terminal-pane-detach-agent-retention.test.ts +++ b/src/renderer/src/store/slices/terminal-pane-detach-agent-retention.test.ts @@ -7,7 +7,7 @@ import { } from './agent-pane-authority' import { createTestStore, makeTab, makeWorktree, seedStore } from './store-test-helpers' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData' const WORKTREE_ID = 'repo::/repo/worktree' diff --git a/src/renderer/src/store/slices/terminal-quick-command-hosts.test.ts b/src/renderer/src/store/slices/terminal-quick-command-hosts.test.ts index 43c8ab5dd74..435f560cfd2 100644 --- a/src/renderer/src/store/slices/terminal-quick-command-hosts.test.ts +++ b/src/renderer/src/store/slices/terminal-quick-command-hosts.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createTestStore } from './store-test-helpers' -import type { TerminalQuickCommand } from '../../../../shared/types' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' import { getDefaultSettings } from '../../../../shared/constants' import { MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION, diff --git a/src/renderer/src/store/slices/terminal-quick-command-hosts.ts b/src/renderer/src/store/slices/terminal-quick-command-hosts.ts index df951c77324..7fd3b299eae 100644 --- a/src/renderer/src/store/slices/terminal-quick-command-hosts.ts +++ b/src/renderer/src/store/slices/terminal-quick-command-hosts.ts @@ -1,7 +1,7 @@ import type { StateCreator } from 'zustand' import { toast } from 'sonner' import type { AppState } from '../types' -import type { TerminalQuickCommand } from '../../../../shared/types' +import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types' import { applyTerminalQuickCommandMutation, parseNormalizedTerminalQuickCommands, diff --git a/src/renderer/src/store/slices/terminal-session-row-hydration.ts b/src/renderer/src/store/slices/terminal-session-row-hydration.ts index bb728f76483..eda4f369725 100644 --- a/src/renderer/src/store/slices/terminal-session-row-hydration.ts +++ b/src/renderer/src/store/slices/terminal-session-row-hydration.ts @@ -1,8 +1,5 @@ -import type { - TerminalLayoutSnapshot, - TerminalTab, - WorkspaceSessionState -} from '../../../../shared/types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' import type { AiVaultSessionTitle } from '../../../../shared/ai-vault-session-title' import { isValidTerminalTabId } from '../../../../shared/terminal-tab-id' import { collectLeafIdsInOrder } from '@/components/terminal-pane/terminal-layout-leaf-ids' diff --git a/src/renderer/src/store/slices/terminal-tab-id-hydration.test.ts b/src/renderer/src/store/slices/terminal-tab-id-hydration.test.ts index cb5e7080ced..7745102c79a 100644 --- a/src/renderer/src/store/slices/terminal-tab-id-hydration.test.ts +++ b/src/renderer/src/store/slices/terminal-tab-id-hydration.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { WorkspaceSessionState } from '../../../../shared/types' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' import { buildHydratedTabState } from './tabs-hydration' vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } })) diff --git a/src/renderer/src/store/slices/terminal-tab-retirement.test.ts b/src/renderer/src/store/slices/terminal-tab-retirement.test.ts index c18b07e27d5..7365707be97 100644 --- a/src/renderer/src/store/slices/terminal-tab-retirement.test.ts +++ b/src/renderer/src/store/slices/terminal-tab-retirement.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { SleepingAgentSessionRecord } from '../../../../shared/agent-session-resume' -import type { TerminalTab } from '../../../../shared/types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' import { brandEphemeralSetupTerminalWorktreeId } from '../../../../shared/ephemeral-setup-terminal-worktree-id' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' diff --git a/src/renderer/src/store/slices/terminal-tab-title-batch.ts b/src/renderer/src/store/slices/terminal-tab-title-batch.ts index 21701f3bd9d..20c51f59967 100644 --- a/src/renderer/src/store/slices/terminal-tab-title-batch.ts +++ b/src/renderer/src/store/slices/terminal-tab-title-batch.ts @@ -1,7 +1,8 @@ import { deriveGeneratedTabTitle } from '../../../../shared/agent-tab-title' import { isDecorativeAgentTitleFrameChange } from '../../../../shared/agent-decorative-title-signature' import { parseLegacyNumericPaneKey, parsePaneKey } from '../../../../shared/stable-pane-id' -import type { Tab, TerminalTab } from '../../../../shared/types' +import type { Tab } from '../../../../shared/tab-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { AppState } from '../types' export type TerminalTabTitleUpdate = { tabId: string; title: string } diff --git a/src/renderer/src/store/slices/terminal-write-identity-bailouts.test.ts b/src/renderer/src/store/slices/terminal-write-identity-bailouts.test.ts index bfd5ab13964..c68f58436c8 100644 --- a/src/renderer/src/store/slices/terminal-write-identity-bailouts.test.ts +++ b/src/renderer/src/store/slices/terminal-write-identity-bailouts.test.ts @@ -13,7 +13,7 @@ * 1_000 (one per call), the post-fix numbers are 0. */ import { describe, expect, it } from 'vitest' -import type { TerminalLayoutSnapshot } from '../../../../shared/types' +import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types' import { createTestStore } from './store-test-helpers' const REPEATS = 1_000 diff --git a/src/renderer/src/store/slices/terminals-hydration-canonical-pty-overlap.test.ts b/src/renderer/src/store/slices/terminals-hydration-canonical-pty-overlap.test.ts index ddea9b058ab..170b1ceac81 100644 --- a/src/renderer/src/store/slices/terminals-hydration-canonical-pty-overlap.test.ts +++ b/src/renderer/src/store/slices/terminals-hydration-canonical-pty-overlap.test.ts @@ -5,7 +5,9 @@ import { describe, expect, it } from 'vitest' import { hydrateWorkspaceTerminalRows } from './terminal-session-row-hydration' import { getOrphanTerminalIds } from './terminal-orphan-helpers' import type { SleepingAgentSessionRecord } from '../../../../shared/agent-session-resume' -import type { Tab, TerminalTab, WorkspaceSessionState } from '../../../../shared/types' +import type { Tab } from '../../../../shared/tab-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' import { getDefaultWorkspaceSession } from '../../../../shared/constants' import { buildWorkspaceSessionPayload } from '@/lib/workspace-session' import { createTestStore, makeLayout, makeTab, makeWorktree, seedStore } from './store-test-helpers' diff --git a/src/renderer/src/store/slices/terminals-hydration-canonical-rows.test.ts b/src/renderer/src/store/slices/terminals-hydration-canonical-rows.test.ts index 6a559c9e114..464159fa219 100644 --- a/src/renderer/src/store/slices/terminals-hydration-canonical-rows.test.ts +++ b/src/renderer/src/store/slices/terminals-hydration-canonical-rows.test.ts @@ -1,7 +1,7 @@ import './terminal-hydration-store-test-bootstrap' import { describe, expect, it } from 'vitest' import type { SleepingAgentSessionRecord } from '../../../../shared/agent-session-resume' -import type { WorkspaceSessionState } from '../../../../shared/types' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' import { getDefaultWorkspaceSession } from '../../../../shared/constants' import { buildWorkspaceSessionPayload } from '@/lib/workspace-session' import { createTestStore, makeLayout, makeTab, makeWorktree, seedStore } from './store-test-helpers' diff --git a/src/renderer/src/store/slices/terminals-hydration.test.ts b/src/renderer/src/store/slices/terminals-hydration.test.ts index 837fde338fd..261acaa3211 100644 --- a/src/renderer/src/store/slices/terminals-hydration.test.ts +++ b/src/renderer/src/store/slices/terminals-hydration.test.ts @@ -91,7 +91,7 @@ const mockApi = { // @ts-expect-error -- mocked browser preload API globalThis.window = { api: mockApi } -import type { WorkspaceSessionState } from '../../../../shared/types' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' import type { SshProviderEpoch } from '../../../../shared/ssh-types' import type { DirectSshPaneRetryAttemptId } from './direct-ssh-terminal-recovery-types' import { diff --git a/src/renderer/src/store/slices/terminals.ts b/src/renderer/src/store/slices/terminals.ts index 9b65267f895..cb7537dfcfe 100644 --- a/src/renderer/src/store/slices/terminals.ts +++ b/src/renderer/src/store/slices/terminals.ts @@ -1,17 +1,14 @@ /* eslint-disable max-lines */ import type { StateCreator } from 'zustand' import type { AppState } from '../types' -import type { - Repo, - SetupSplitDirection, - Tab, - TerminalLayoutSnapshot, - TerminalTab, - TuiAgent, - Worktree, - WorkspaceKey, - WorkspaceSessionState -} from '../../../../shared/types' +import type { WorkspaceKey } from '../../../../shared/folder-workspace-types' +import type { Repo } from '../../../../shared/repo-types' +import type { Tab } from '../../../../shared/tab-types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../../shared/terminal-tab-types' +import type { TuiAgent } from '../../../../shared/tui-agent' +import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types' +import type { SetupSplitDirection } from '../../../../shared/worktree/launch-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { AgentProviderSessionMetadata, SleepingAgentLaunchConfig diff --git a/src/renderer/src/store/slices/ui.test.ts b/src/renderer/src/store/slices/ui.test.ts index 4cb747821a0..a4707693874 100644 --- a/src/renderer/src/store/slices/ui.test.ts +++ b/src/renderer/src/store/slices/ui.test.ts @@ -6,15 +6,13 @@ import { getDefaultUIState, getWorktreeCardModeProperties } from '../../../../shared/constants' -import type { - GitHubWorkItem, - JiraIssue, - LinearIssue, - PersistedUIState, - Repo, - TerminalTab, - Worktree -} from '../../../../shared/types' +import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' +import type { JiraIssue } from '../../../../shared/jira-types' +import type { LinearIssue } from '../../../../shared/linear/issue-types' +import type { PersistedUIState } from '../../../../shared/persisted-ui-state-types' +import type { Repo } from '../../../../shared/repo-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { GitLabWorkItem } from '../../../../shared/gitlab-types' import { createUISlice } from './ui' import { createWorktreeNavHistorySlice } from './worktree-nav-history' diff --git a/src/renderer/src/store/slices/ui.ts b/src/renderer/src/store/slices/ui.ts index 063e8157c02..f9ea3110cb6 100644 --- a/src/renderer/src/store/slices/ui.ts +++ b/src/renderer/src/store/slices/ui.ts @@ -6,31 +6,30 @@ import { findPrevLiveNonTaskStackHistoryIndex, findPrevLiveWorktreeHistoryIndex } from './worktree-nav-history' +import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' +import type { JiraIssue } from '../../../../shared/jira-types' +import type { LinearIssue } from '../../../../shared/linear/issue-types' +import type { PersistedTrustedOrcaHooks } from '../../../../shared/orca-yaml-hook-types' +import type { PersistedUIState } from '../../../../shared/persisted-ui-state-types' +import type { CustomPet } from '../../../../shared/pet-types' +import type { TaskProvider } from '../../../../shared/task-providers' +import type { TuiAgent } from '../../../../shared/tui-agent' import type { - ChangelogData, - CustomPet, - GitHubWorkItem, - JiraIssue, - LinearIssue, + AgentActivityDisplayMode, ManualRepoOrderEntry, - PersistedTrustedOrcaHooks, - PersistedUIState, + ProjectOrderBy, StatusBarItem, - TaskProvider, TaskResumeState, TaskViewPresetId, - TuiAgent, - UpdateStatus, - WorkspaceStatusDefinition, - AgentActivityDisplayMode, - ProjectOrderBy, - WorktreeCardProperty, - WorktreeCardMode, + TopLevelView, + VisibleWorkspaceHostIds, WorkspaceHostOrder, WorkspaceHostScope, - VisibleWorkspaceHostIds, - TopLevelView -} from '../../../../shared/types' + WorktreeCardMode, + WorktreeCardProperty +} from '../../../../shared/ui-chrome-types' +import type { ChangelogData, UpdateStatus } from '../../../../shared/update-status-types' +import type { WorkspaceStatusDefinition } from '../../../../shared/worktree/types' import { applyManualRepoOrder, normalizeManualRepoOrder @@ -50,7 +49,7 @@ import { import type { GitLabWorkItem } from '../../../../shared/gitlab-types' import type { LaunchSource } from '../../../../shared/telemetry-events' import type { TaskSourceContext } from '../../../../shared/task-source-context' -import { PET_SIZE_DEFAULT, PET_SIZE_MAX, PET_SIZE_MIN } from '../../../../shared/types' +import { PET_SIZE_DEFAULT, PET_SIZE_MAX, PET_SIZE_MIN } from '../../../../shared/pet-types' import { WORKSPACE_CLEANUP_CLASSIFIER_VERSION, type WorkspaceCleanupDismissal diff --git a/src/renderer/src/store/slices/worktree-by-id-index.test.ts b/src/renderer/src/store/slices/worktree-by-id-index.test.ts index 1c5d28e6295..70930836731 100644 --- a/src/renderer/src/store/slices/worktree-by-id-index.test.ts +++ b/src/renderer/src/store/slices/worktree-by-id-index.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { buildByIdIndex, buildWorktreeByIdIndex } from './worktree-by-id-index' function worktree(id: string, repoId: string): Worktree { diff --git a/src/renderer/src/store/slices/worktree-by-id-index.ts b/src/renderer/src/store/slices/worktree-by-id-index.ts index a7eaf17239e..3607ce0f339 100644 --- a/src/renderer/src/store/slices/worktree-by-id-index.ts +++ b/src/renderer/src/store/slices/worktree-by-id-index.ts @@ -1,4 +1,4 @@ -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' /** * Flattens worktreesByRepo into an id lookup. diff --git a/src/renderer/src/store/slices/worktree-helpers.test.ts b/src/renderer/src/store/slices/worktree-helpers.test.ts index 2750be4445b..d483c04aeaf 100644 --- a/src/renderer/src/store/slices/worktree-helpers.test.ts +++ b/src/renderer/src/store/slices/worktree-helpers.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { applyWorktreeUpdates } from './worktree-helpers' function makeWorktree(overrides: Partial & { id: string; repoId: string }): Worktree { diff --git a/src/renderer/src/store/slices/worktree-helpers.ts b/src/renderer/src/store/slices/worktree-helpers.ts index 45c64ed8b4c..6d201d03251 100644 --- a/src/renderer/src/store/slices/worktree-helpers.ts +++ b/src/renderer/src/store/slices/worktree-helpers.ts @@ -1,26 +1,29 @@ +import type { WorkspaceKey } from '../../../../shared/folder-workspace-types' +import type { TuiAgent } from '../../../../shared/tui-agent' +import type { WorkspaceSource as WorkspaceCreateTelemetrySource } from '../../../../shared/workspace-source' +import type { + WorktreeBaseStatusEvent, + WorktreeRemoteBranchConflictEvent +} from '../../../../shared/worktree/base-ref-drift-types' import type { - CreateWorktreeResult, - CreateWorktreeArgs, CreateSparseCheckoutRequest, + CreateWorktreeArgs, + CreateWorktreeResult, + ForceDeleteWorktreeBranchResult, + RemoveWorktreeResult, + SetupDecision +} from '../../../../shared/worktree/create-types' +import type { WorktreeStartupLaunch } from '../../../../shared/worktree/launch-types' +import type { WorkspaceLineage, WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { WorktreeMeta } from '../../../../shared/worktree/meta-types' +import type { DetectedWorktree, DetectedWorktreeListResult, - ForceDeleteWorktreeBranchResult, GitPushTarget, - RemoveWorktreeResult, - SetupDecision, - TuiAgent, - WorkspaceCreateTelemetrySource, WorkspaceLinkedItem, WorkspaceStatus, - WorkspaceLineage, - WorktreeStartupLaunch, - Worktree, - WorktreeBaseStatusEvent, - WorktreeLineage, - WorktreeRemoteBranchConflictEvent, - WorktreeMeta, - WorkspaceKey -} from '../../../../shared/types' + Worktree +} from '../../../../shared/worktree/types' import type { TaskSourceContext } from '../../../../shared/task-source-context' import type { WorktreeForceDeleteReason } from '../../../../shared/worktree/removal' import type { TerminalGitHubPRLink } from '../../../../shared/terminal-github-pr-link-detector' diff --git a/src/renderer/src/store/slices/worktree-listing-branch-switch.test.ts b/src/renderer/src/store/slices/worktree-listing-branch-switch.test.ts index 0edf89bfb7b..281fa019e9e 100644 --- a/src/renderer/src/store/slices/worktree-listing-branch-switch.test.ts +++ b/src/renderer/src/store/slices/worktree-listing-branch-switch.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' import { routeListingBranchSwitchesThroughGitIdentity } from './worktree-listing-branch-switch' function makeWorktree(overrides: Partial & { id: string }): Worktree { diff --git a/src/renderer/src/store/slices/worktree-listing-branch-switch.ts b/src/renderer/src/store/slices/worktree-listing-branch-switch.ts index 3782463668c..a72c50b7ce0 100644 --- a/src/renderer/src/store/slices/worktree-listing-branch-switch.ts +++ b/src/renderer/src/store/slices/worktree-listing-branch-switch.ts @@ -1,4 +1,4 @@ -import type { Worktree } from '../../../../shared/types' +import type { Worktree } from '../../../../shared/worktree/types' function indexUnambiguousWorktrees( worktrees: readonly Worktree[], diff --git a/src/renderer/src/store/slices/worktree-nav-history-view-entries.test.ts b/src/renderer/src/store/slices/worktree-nav-history-view-entries.test.ts index 70d4475ba3a..2c5f96fc97c 100644 --- a/src/renderer/src/store/slices/worktree-nav-history-view-entries.test.ts +++ b/src/renderer/src/store/slices/worktree-nav-history-view-entries.test.ts @@ -1,7 +1,9 @@ import { createStore, type StoreApi } from 'zustand/vanilla' import { afterEach, describe, expect, it } from 'vitest' import type { AppState } from '../types' -import type { GitHubWorkItem, JiraIssue, Worktree } from '../../../../shared/types' +import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' +import type { JiraIssue } from '../../../../shared/jira-types' +import type { Worktree } from '../../../../shared/worktree/types' import type { GitLabWorkItem } from '../../../../shared/gitlab-types' import type { TaskSourceContext } from '../../../../shared/task-source-context' import { diff --git a/src/renderer/src/store/slices/worktree-nav-history.test.ts b/src/renderer/src/store/slices/worktree-nav-history.test.ts index bb686e558fc..18863300026 100644 --- a/src/renderer/src/store/slices/worktree-nav-history.test.ts +++ b/src/renderer/src/store/slices/worktree-nav-history.test.ts @@ -1,7 +1,8 @@ import { createStore, type StoreApi } from 'zustand/vanilla' import { afterEach, describe, expect, it } from 'vitest' import type { AppState } from '../types' -import type { FolderWorkspace, Worktree } from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { Worktree } from '../../../../shared/worktree/types' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import { canGoBackWorktreeHistory, diff --git a/src/renderer/src/store/slices/worktree-nav-history.ts b/src/renderer/src/store/slices/worktree-nav-history.ts index 1582e649017..f59485d9801 100644 --- a/src/renderer/src/store/slices/worktree-nav-history.ts +++ b/src/renderer/src/store/slices/worktree-nav-history.ts @@ -1,7 +1,9 @@ import type { StateCreator } from 'zustand' import type { AppState } from '../types' import { findWorktreeById } from './worktree-helpers' -import type { GitHubWorkItem, JiraIssue, LinearIssue } from '../../../../shared/types' +import type { GitHubWorkItem } from '../../../../shared/github/work-item-types' +import type { JiraIssue } from '../../../../shared/jira-types' +import type { LinearIssue } from '../../../../shared/linear/issue-types' import type { GitLabWorkItem } from '../../../../shared/gitlab-types' import { getTaskSourceCacheScope, diff --git a/src/renderer/src/store/slices/worktree-removal-maps-leak.test.ts b/src/renderer/src/store/slices/worktree-removal-maps-leak.test.ts index d379a8ec895..98049554eb8 100644 --- a/src/renderer/src/store/slices/worktree-removal-maps-leak.test.ts +++ b/src/renderer/src/store/slices/worktree-removal-maps-leak.test.ts @@ -21,7 +21,7 @@ */ import { describe, it, expect, vi, beforeEach } from 'vitest' import type * as AgentStatusModule from '@/lib/agent-status' -import type { BrowserPage, BrowserWorkspace } from '../../../../shared/types' +import type { BrowserPage, BrowserWorkspace } from '../../../../shared/browser-workspace-types' import { getAgentHibernationPaneOutputEpoch, recordAgentHibernationPaneOutput, diff --git a/src/renderer/src/store/slices/worktrees.test.ts b/src/renderer/src/store/slices/worktrees.test.ts index 2cb3597da53..b3bc0224513 100644 --- a/src/renderer/src/store/slices/worktrees.test.ts +++ b/src/renderer/src/store/slices/worktrees.test.ts @@ -2,15 +2,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { create } from 'zustand' import type { AppState } from '../types' -import type { - DetectedWorktreeListResult, - FolderWorkspace, - LocalBaseRefRefreshResult, - TerminalTab, - Worktree, - WorktreeLineage, - WorkspaceLineage -} from '../../../../shared/types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { TerminalTab } from '../../../../shared/terminal-tab-types' +import type { LocalBaseRefRefreshResult } from '../../../../shared/worktree/base-ref-drift-types' +import type { WorkspaceLineage, WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { DetectedWorktreeListResult, Worktree } from '../../../../shared/worktree/types' import { toast } from 'sonner' import { createCompatibleRuntimeStatusResponse, diff --git a/src/renderer/src/store/slices/worktrees.ts b/src/renderer/src/store/slices/worktrees.ts index 497e8d3be6b..1a704a53875 100644 --- a/src/renderer/src/store/slices/worktrees.ts +++ b/src/renderer/src/store/slices/worktrees.ts @@ -1,22 +1,23 @@ /* eslint-disable max-lines */ import type { StateCreator, StoreApi } from 'zustand' import type { AppState } from '../types' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { ProjectHostSetup } from '../../../../shared/project-types' +import type { Repo } from '../../../../shared/repo-types' +import type { WorkspaceVisibleTabType } from '../../../../shared/tab-types' +import type { LocalBaseRefRefreshResult } from '../../../../shared/worktree/base-ref-drift-types' +import type { + ForceDeleteWorktreeBranchResult, + RemoveWorktreeResult +} from '../../../../shared/worktree/create-types' +import type { WorkspaceLineage, WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { WorktreeMeta } from '../../../../shared/worktree/meta-types' import type { DetectedWorktreeListResult, - LocalBaseRefRefreshResult, - ForceDeleteWorktreeBranchResult, - FolderWorkspace, GitHubPrStartPoint, - Worktree, - WorkspaceVisibleTabType, GitPushTarget, - RemoveWorktreeResult, - WorktreeLineage, - WorkspaceLineage, - ProjectHostSetup, - Repo, - WorktreeMeta -} from '../../../../shared/types' + Worktree +} from '../../../../shared/worktree/types' import type { RuntimeWorktreeListResult } from '../../../../shared/runtime-types' import { findWorktreeById, diff --git a/src/renderer/src/store/worktree-diff-comments-selector.test.ts b/src/renderer/src/store/worktree-diff-comments-selector.test.ts index db45edec6b3..ce375285d48 100644 --- a/src/renderer/src/store/worktree-diff-comments-selector.test.ts +++ b/src/renderer/src/store/worktree-diff-comments-selector.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from 'vitest' -import type { DiffComment, FolderWorkspace, Worktree } from '../../../shared/types' +import type { DiffComment } from '../../../shared/diff-comment-types' +import type { FolderWorkspace } from '../../../shared/folder-workspace-types' +import type { Worktree } from '../../../shared/worktree/types' import type { AppState } from './types' import { selectWorktreeDiffComments, diff --git a/src/renderer/src/store/worktree-diff-comments-selector.ts b/src/renderer/src/store/worktree-diff-comments-selector.ts index 31e6edfa9ee..3fe9a37a84a 100644 --- a/src/renderer/src/store/worktree-diff-comments-selector.ts +++ b/src/renderer/src/store/worktree-diff-comments-selector.ts @@ -1,4 +1,4 @@ -import type { DiffComment } from '../../../shared/types' +import type { DiffComment } from '../../../shared/diff-comment-types' import type { AppState } from './types' import { getIndexedWorktreeById } from './worktree-repo-index' import { findFolderWorkspaceOwner } from '@/lib/folder-workspace-runtime-owner' diff --git a/src/renderer/src/store/worktree-repo-index.ts b/src/renderer/src/store/worktree-repo-index.ts index 9c459f90f86..670e6b7ceec 100644 --- a/src/renderer/src/store/worktree-repo-index.ts +++ b/src/renderer/src/store/worktree-repo-index.ts @@ -1,4 +1,5 @@ -import type { Repo, Worktree } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' +import type { Worktree } from '../../../shared/worktree/types' import type { AppState } from './types' type WorktreeSnapshot = { diff --git a/src/renderer/src/web/web-file-mutation-methods.test.ts b/src/renderer/src/web/web-file-mutation-methods.test.ts index 7a996e24805..266beec9fb5 100644 --- a/src/renderer/src/web/web-file-mutation-methods.test.ts +++ b/src/renderer/src/web/web-file-mutation-methods.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { SshConnectionState } from '../../../shared/ssh-types' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' import { createWebFileMutationMethods } from './web-file-mutation-methods' function resolvedFile( diff --git a/src/renderer/src/web/web-file-mutation-methods.ts b/src/renderer/src/web/web-file-mutation-methods.ts index 5467f6a8ccf..262b7e30fc9 100644 --- a/src/renderer/src/web/web-file-mutation-methods.ts +++ b/src/renderer/src/web/web-file-mutation-methods.ts @@ -1,7 +1,7 @@ import type { PreloadApi } from '../../../preload/api-types' import { parseExecutionHostId } from '../../../shared/execution-host' import type { SshConnectionState, SshMutationExpectation } from '../../../shared/ssh-types' -import type { Worktree } from '../../../shared/types' +import type { Worktree } from '../../../shared/worktree/types' import { toRuntimeWorktreeSelector } from '../runtime/runtime-worktree-selector' const SSH_OWNER_CHANGED_MESSAGE = diff --git a/src/renderer/src/web/web-preload-api.ts b/src/renderer/src/web/web-preload-api.ts index b142bb0adce..5d209deff38 100644 --- a/src/renderer/src/web/web-preload-api.ts +++ b/src/renderer/src/web/web-preload-api.ts @@ -24,24 +24,23 @@ import type { ComputerUsePermissionSetupResult, ComputerUsePermissionStatusResult } from '../../../shared/computer-use-permissions-types' +import type { SearchResult } from '../../../shared/code-search-types' +import type { DirEntry } from '../../../shared/filesystem-entry-types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { OnboardingState } from '../../../shared/onboarding-state-types' +import type { PersistedUIState } from '../../../shared/persisted-ui-state-types' +import type { MemorySnapshot, StatsSummary } from '../../../shared/process-stats-types' +import type { Repo } from '../../../shared/repo-types' import type { - DetectedWorktreeListResult, - DirEntry, - ForceDeleteWorktreeBranchResult, - GlobalSettings, - MemorySnapshot, - OnboardingState, - PersistedUIState, - Repo, - RemoveWorktreeResult, - SearchResult, - StatsSummary, - Worktree, - WorktreeLineage, - WorkspaceLineage, WorkspaceSessionPatch, WorkspaceSessionState -} from '../../../shared/types' +} from '../../../shared/workspace-session-state-types' +import type { + ForceDeleteWorktreeBranchResult, + RemoveWorktreeResult +} from '../../../shared/worktree/create-types' +import type { WorkspaceLineage, WorktreeLineage } from '../../../shared/worktree/lineage-types' +import type { DetectedWorktreeListResult, Worktree } from '../../../shared/worktree/types' import type { SkillDiscoveryResult } from '../../../shared/skills' import type { SkillFreshnessInventory } from '../../../shared/skill-freshness' import type { SshConnectionState, SshTarget } from '../../../shared/ssh-types' diff --git a/src/renderer/src/web/web-runtime-direct-proxy-live.integration.test.ts b/src/renderer/src/web/web-runtime-direct-proxy-live.integration.test.ts index 25c863c959d..b289c2a080b 100644 --- a/src/renderer/src/web/web-runtime-direct-proxy-live.integration.test.ts +++ b/src/renderer/src/web/web-runtime-direct-proxy-live.integration.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import type { Repo, Worktree } from '../../../shared/types' +import type { Repo } from '../../../shared/repo-types' +import type { Worktree } from '../../../shared/worktree/types' import type { RuntimeRpcResponse } from '../../../shared/runtime-rpc-envelope' import type { WorkspacePortScanResult } from '../../../shared/workspace-ports' import { parseWebPairingInput, type WebPairingOffer } from './web-pairing' diff --git a/src/renderer/src/web/web-workspace-session.test.ts b/src/renderer/src/web/web-workspace-session.test.ts index 95f1686cdbb..cfa19c076a7 100644 --- a/src/renderer/src/web/web-workspace-session.test.ts +++ b/src/renderer/src/web/web-workspace-session.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { WorkspaceSessionState } from '../../../shared/types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' import { sanitizeWebRuntimeWorkspaceSession } from './web-workspace-session' describe('sanitizeWebRuntimeWorkspaceSession', () => { diff --git a/src/renderer/src/web/web-workspace-session.ts b/src/renderer/src/web/web-workspace-session.ts index fbafdd277b6..30cea317a62 100644 --- a/src/renderer/src/web/web-workspace-session.ts +++ b/src/renderer/src/web/web-workspace-session.ts @@ -1,5 +1,5 @@ import { getDefaultWorkspaceSession } from '../../../shared/constants' -import type { WorkspaceSessionState } from '../../../shared/types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' export function sanitizeWebRuntimeWorkspaceSession( session: WorkspaceSessionState diff --git a/src/shared/agent-headless-command.ts b/src/shared/agent-headless-command.ts index 1ab62f2f5b5..9ff40e6af1d 100644 --- a/src/shared/agent-headless-command.ts +++ b/src/shared/agent-headless-command.ts @@ -1,7 +1,7 @@ import { isAnteHeadlessOneShotCommand } from './ante-headless-command' import { isPrimeAgentHeadlessOneShotCommand } from './prime-agent-headless-command' import { isPrintModeHeadlessOneShotCommand } from './print-mode-headless-command' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' // Why: a table (not an if-chain) so adding an agent is one entry; Claude and Trae share // the same `--print` one-shot contract, Ante's `--prompt` form and Prime Agent's diff --git a/src/shared/agent-kind.test.ts b/src/shared/agent-kind.test.ts index d479fd78e50..edea4d20fc4 100644 --- a/src/shared/agent-kind.test.ts +++ b/src/shared/agent-kind.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest' import { agentKindToTuiAgent, tuiAgentToAgentKind } from './agent-kind' import { AGENT_KIND_VALUES, agentKindSchema } from './telemetry-events' import { TUI_AGENT_CONFIG } from './tui-agent-config' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' describe('tuiAgentToAgentKind', () => { it('maps every shipped TuiAgent to a concrete telemetry kind', () => { diff --git a/src/shared/agent-kind.ts b/src/shared/agent-kind.ts index bea996873ca..324a73f2cd8 100644 --- a/src/shared/agent-kind.ts +++ b/src/shared/agent-kind.ts @@ -9,7 +9,7 @@ // not a sweep across renderer + main. import type { AgentKind } from './telemetry-events' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' type ConcreteAgentKind = Exclude diff --git a/src/shared/agent-model-probe-spec.ts b/src/shared/agent-model-probe-spec.ts index e546f151b1f..d60dbda6f9f 100644 --- a/src/shared/agent-model-probe-spec.ts +++ b/src/shared/agent-model-probe-spec.ts @@ -1,6 +1,6 @@ import { getCommitMessageAgentSpec, type CommitMessageAgentSpec } from './commit-message-agent-spec' import { GROK_MODEL_LIST_ARGS, parseGrokModelList } from './grok-model-list-probe' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' /** Why: model discovery reads only these fields; excluding the prompt-delivery * half is what keeps a probe-only agent out of the commit-message registry. */ diff --git a/src/shared/agent-node-entrypoint-identities.ts b/src/shared/agent-node-entrypoint-identities.ts index 9c7f2a9f98c..e71118312d0 100644 --- a/src/shared/agent-node-entrypoint-identities.ts +++ b/src/shared/agent-node-entrypoint-identities.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' // Node CLIs whose shims launch a generic script (cli.js, versioned index.js), so // only the exact install path is an authoritative identity signal. diff --git a/src/shared/agent-process-recognition.ts b/src/shared/agent-process-recognition.ts index 957bfa5cb7b..e0fa3568c68 100644 --- a/src/shared/agent-process-recognition.ts +++ b/src/shared/agent-process-recognition.ts @@ -1,7 +1,7 @@ import { getTuiAgentDetectCommands, TUI_AGENT_CONFIG } from './tui-agent-config' import { EXACT_NODE_ENTRYPOINT_IDENTITIES } from './agent-node-entrypoint-identities' import type { AgentType } from './agent-status-types' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' import { filterHeadlessOneShotAgentCommand } from './agent-headless-command' import { getFirstCommandToken } from './command-token-scanner' diff --git a/src/shared/agent-row-conversation-name.ts b/src/shared/agent-row-conversation-name.ts index 95e5dfcfb28..02ea55b240b 100644 --- a/src/shared/agent-row-conversation-name.ts +++ b/src/shared/agent-row-conversation-name.ts @@ -11,7 +11,7 @@ import { stripLeadingAgentTitleDecorationOrEmpty } from './agent-title-decoratio import { formatAgentTypeLabel } from './agent-type-label' import { isMeaningfulOpenCodeTerminalTitle } from './opencode-terminal-title' import { SYNTHETIC_AGENT_TITLE_PROFILES } from './synthetic-agent-title' -import type { TerminalTab } from './types' +import type { TerminalTab } from './terminal-tab-types' export type ConversationNameTab = Pick< TerminalTab, diff --git a/src/shared/agent-session-host-authority.ts b/src/shared/agent-session-host-authority.ts index 96e079afb63..ec9f5776fc3 100644 --- a/src/shared/agent-session-host-authority.ts +++ b/src/shared/agent-session-host-authority.ts @@ -7,7 +7,7 @@ import { import type { RuntimeTerminalCreate, RuntimeTerminalPresentation } from './runtime-types' import { isTerminalLeafId } from './stable-pane-id' import { isValidTerminalTabId } from './terminal-tab-id' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' export { AGENT_SESSION_HOST_AUTHORITY_RUNTIME_CAPABILITY as AGENT_SESSION_HOST_AUTHORITY_CAPABILITY } from './protocol-version' diff --git a/src/shared/agent-session-resume.ts b/src/shared/agent-session-resume.ts index b6e3ee9900d..66f7168e8ba 100644 --- a/src/shared/agent-session-resume.ts +++ b/src/shared/agent-session-resume.ts @@ -1,6 +1,6 @@ import type { AgentHookSource } from './agent-hook-relay' import type { AgentStatusState } from './agent-status-types' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' export const RESUMABLE_TUI_AGENTS = [ 'claude', diff --git a/src/shared/ai-vault-types.ts b/src/shared/ai-vault-types.ts index 71387faf924..a6dff1b59a9 100644 --- a/src/shared/ai-vault-types.ts +++ b/src/shared/ai-vault-types.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' import type { ExecutionHostId, ExecutionHostScope } from './execution-host' export const AI_VAULT_AGENTS = [ diff --git a/src/shared/artifact-sharing-gate.ts b/src/shared/artifact-sharing-gate.ts index a8d8c743a7e..0d10f7af3f0 100644 --- a/src/shared/artifact-sharing-gate.ts +++ b/src/shared/artifact-sharing-gate.ts @@ -1,6 +1,6 @@ // Why: publishing an artifact mints a URL anyone can open, so agents get the capability only // after the user grants it. The gate lives here so main, the CLI, and Settings share one contract. -import type { GlobalSettings } from './types' +import type { GlobalSettings } from './global-settings-types' export const ARTIFACT_SHARING_DISABLED_CODE = 'artifact_sharing_disabled' diff --git a/src/shared/auto-rename-branch-from-work-settings.ts b/src/shared/auto-rename-branch-from-work-settings.ts index ab2422d9e39..544e4c3833e 100644 --- a/src/shared/auto-rename-branch-from-work-settings.ts +++ b/src/shared/auto-rename-branch-from-work-settings.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from './types' +import type { GlobalSettings } from './global-settings-types' type AutoRenameBranchFromWorkSettings = Pick< GlobalSettings, diff --git a/src/shared/automation-workspace-provenance.ts b/src/shared/automation-workspace-provenance.ts index 7fd42119e22..6f1c0f9894e 100644 --- a/src/shared/automation-workspace-provenance.ts +++ b/src/shared/automation-workspace-provenance.ts @@ -1,6 +1,7 @@ import type { Automation, AutomationRun } from './automations-types' import { getRepoExecutionHostId } from './execution-host' -import type { AutomationWorkspaceProvenance, Repo } from './types' +import type { Repo } from './repo-types' +import type { AutomationWorkspaceProvenance } from './worktree/types' type AutomationProvenanceRun = Pick diff --git a/src/shared/automations-types.ts b/src/shared/automations-types.ts index d2572020ba8..82ebcb2dbae 100644 --- a/src/shared/automations-types.ts +++ b/src/shared/automations-types.ts @@ -1,4 +1,5 @@ -import type { SetupDecision, TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' +import type { SetupDecision } from './worktree/create-types' import type { TaskSourceContext, WorkspaceRunContext } from './task-source-context' export type AutomationWorkspaceMode = 'existing' | 'new_per_run' diff --git a/src/shared/base-ref-search-result.ts b/src/shared/base-ref-search-result.ts index 895ecf28f9a..469c358a05e 100644 --- a/src/shared/base-ref-search-result.ts +++ b/src/shared/base-ref-search-result.ts @@ -1,4 +1,4 @@ -import type { BaseRefSearchResult } from './types' +import type { BaseRefSearchResult } from './repo-types' const LEGACY_REMOTE_REF_PREFIXES = ['origin/', 'upstream/'] diff --git a/src/shared/branch-prefix.ts b/src/shared/branch-prefix.ts index 18a5fbe0ba7..1c2abf92276 100644 --- a/src/shared/branch-prefix.ts +++ b/src/shared/branch-prefix.ts @@ -1,4 +1,4 @@ -import type { BranchPrefixStrategy } from './types' +import type { BranchPrefixStrategy } from './ui-chrome-types' /** The branch-prefix settings slice the prefix helpers read. */ export type BranchPrefixSettings = { diff --git a/src/shared/browser-viewport-presets.ts b/src/shared/browser-viewport-presets.ts index 34cdebc5bb5..411a924213e 100644 --- a/src/shared/browser-viewport-presets.ts +++ b/src/shared/browser-viewport-presets.ts @@ -1,4 +1,4 @@ -import type { BrowserViewportOverride, BrowserViewportPresetId } from './types' +import type { BrowserViewportOverride, BrowserViewportPresetId } from './browser-workspace-types' export type BrowserViewportPreset = { id: BrowserViewportPresetId diff --git a/src/shared/cli-workspace-provenance.ts b/src/shared/cli-workspace-provenance.ts index 3334a7b6516..15de35c4172 100644 --- a/src/shared/cli-workspace-provenance.ts +++ b/src/shared/cli-workspace-provenance.ts @@ -1,4 +1,5 @@ -import type { CliWorkspaceProvenance, TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' +import type { CliWorkspaceProvenance } from './worktree/types' /** Client-supplied context for a `orca worktree create`. Descriptive only — * the host stamps `createdAt` so a skewed client clock can't affect sort order. */ diff --git a/src/shared/codex-pet-sprite-defaults.ts b/src/shared/codex-pet-sprite-defaults.ts index b33625d6202..ba5bd32786a 100644 --- a/src/shared/codex-pet-sprite-defaults.ts +++ b/src/shared/codex-pet-sprite-defaults.ts @@ -1,4 +1,4 @@ -import type { CustomPet, SpriteAnimation } from './types' +import type { CustomPet, SpriteAnimation } from './pet-types' /** Codex pet sprite layout and pacing, mirroring the tables in Codex CLI's * `tui/src/pets`. Shared because main bakes these into imported bundles and diff --git a/src/shared/codex-reset-credit-scope.test.ts b/src/shared/codex-reset-credit-scope.test.ts index 0852612a8c6..134d9c32dae 100644 --- a/src/shared/codex-reset-credit-scope.test.ts +++ b/src/shared/codex-reset-credit-scope.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { ProviderRateLimits } from './rate-limit-types' -import type { CodexManagedAccountSummary } from './types' +import type { CodexManagedAccountSummary } from './managed-account-types' import { buildCodexResetCreditExpectedScope } from './codex-reset-credit-scope' const account: CodexManagedAccountSummary = { diff --git a/src/shared/codex-reset-credit-scope.ts b/src/shared/codex-reset-credit-scope.ts index aec85c73117..390c40dc6cd 100644 --- a/src/shared/codex-reset-credit-scope.ts +++ b/src/shared/codex-reset-credit-scope.ts @@ -3,7 +3,7 @@ import type { RateLimitRuntimeTarget, RateLimitWindow } from './rate-limit-types' -import type { CodexManagedAccountSummary } from './types' +import type { CodexManagedAccountSummary } from './managed-account-types' export type CodexResetCreditExpectedScope = { target: RateLimitRuntimeTarget diff --git a/src/shared/commit-message-agent-spec.ts b/src/shared/commit-message-agent-spec.ts index 7c8b8bbfa9e..2281bd00a2b 100644 --- a/src/shared/commit-message-agent-spec.ts +++ b/src/shared/commit-message-agent-spec.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' import { isTuiAgentEnabled } from './tui-agent-selection' import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit' import { diff --git a/src/shared/commit-message-generation.ts b/src/shared/commit-message-generation.ts index 3c5c92ad835..8cc6820e938 100644 --- a/src/shared/commit-message-generation.ts +++ b/src/shared/commit-message-generation.ts @@ -1,5 +1,5 @@ import { cleanGeneratedCommitMessage, truncateDiffForPrompt } from './commit-message-prompt' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' export type CommitMessageDraftAgent = TuiAgent | 'custom' diff --git a/src/shared/commit-message-plan.ts b/src/shared/commit-message-plan.ts index 1141ecec0e7..dc77b1d4e17 100644 --- a/src/shared/commit-message-plan.ts +++ b/src/shared/commit-message-plan.ts @@ -4,7 +4,7 @@ import { isCustomAgentId } from './commit-message-agent-spec' import { planCustomCommand, tokenizeCustomCommandTemplate } from './commit-message-prompt' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' // Why: planning is a pure transformation from "user request + prompt text" // into "spawn-ready binary + argv". Keeping it in shared lets both the local diff --git a/src/shared/composer-branch-selection.ts b/src/shared/composer-branch-selection.ts index 9342f18bfd7..66ed39f6036 100644 --- a/src/shared/composer-branch-selection.ts +++ b/src/shared/composer-branch-selection.ts @@ -1,4 +1,4 @@ -import type { GitPushTarget } from './types' +import type { GitPushTarget } from './worktree/types' export type ComposerBranchSelection = { baseBranch: string diff --git a/src/shared/constants.ts b/src/shared/constants.ts index b0cf4abffca..99ee48a4e35 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -1,15 +1,12 @@ /* eslint-disable max-lines -- Why: default persisted settings live in one schema-shaped object so migrations and tests compare against one source of truth. */ -import type { - GlobalSettings, - NotificationSettings, - OnboardingChecklistState, - OnboardingState, - PersistedState, - PersistedUIState, - RepoHookSettings, - WorkspaceSessionState, - AgentActivityDisplayMode -} from './types' +import type { GlobalSettings } from './global-settings-types' +import type { NotificationSettings } from './notification-settings-types' +import type { OnboardingChecklistState, OnboardingState } from './onboarding-state-types' +import type { RepoHookSettings } from './orca-yaml-hook-types' +import type { PersistedState } from './persisted-state-types' +import type { PersistedUIState } from './persisted-ui-state-types' +import type { AgentActivityDisplayMode } from './ui-chrome-types' +import type { WorkspaceSessionState } from './workspace-session-state-types' import { EMPTY_CODEX_RESET_CREDIT_ATTEMPT_LEDGER } from './codex-reset-credit-attempt-ledger' import { DEFAULT_STATUS_BAR_ITEMS } from './status-bar-defaults' import { DEFAULT_TERMINAL_FONT_WEIGHT, DEFAULT_TERMINAL_FONT_WEIGHT_BOLD } from './terminal-fonts' diff --git a/src/shared/dashboard-snapshot.ts b/src/shared/dashboard-snapshot.ts index cd59983ae25..a3a888d820f 100644 --- a/src/shared/dashboard-snapshot.ts +++ b/src/shared/dashboard-snapshot.ts @@ -1,7 +1,7 @@ import type { AgentType } from './agent-status-types' import type { ExecutionHostId } from './execution-host' import type { RepoIcon } from './repo-icon' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' /** * Serializable contract for the pop-out agent dashboard. The main renderer owns diff --git a/src/shared/detected-worktree-provider-contract.ts b/src/shared/detected-worktree-provider-contract.ts index 662c2fed3c9..9f146e35778 100644 --- a/src/shared/detected-worktree-provider-contract.ts +++ b/src/shared/detected-worktree-provider-contract.ts @@ -1,6 +1,6 @@ import type { ExecutionHostId, LOCAL_EXECUTION_HOST_ID } from './execution-host' import type { DirectSshAuthority } from './ssh-types' -import type { DetectedWorktreeListResult } from './types' +import type { DetectedWorktreeListResult } from './worktree/types' export const PROVIDER_REQUEST_ID_MAX_UTF8_BYTES = 128 export type ProviderRequestId = string & { readonly __providerRequestId: unique symbol } diff --git a/src/shared/diff-comments-format.ts b/src/shared/diff-comments-format.ts index 4f35f818adc..a66b2a8ec99 100644 --- a/src/shared/diff-comments-format.ts +++ b/src/shared/diff-comments-format.ts @@ -1,4 +1,4 @@ -import type { DiffComment } from './types' +import type { DiffComment } from './diff-comment-types' function isMarkdownComment(comment: Pick): boolean { return comment.source === 'markdown' diff --git a/src/shared/ephemeral-vm-recipe-checkout-mode.test.ts b/src/shared/ephemeral-vm-recipe-checkout-mode.test.ts index ea77181ada0..d49d395afde 100644 --- a/src/shared/ephemeral-vm-recipe-checkout-mode.test.ts +++ b/src/shared/ephemeral-vm-recipe-checkout-mode.test.ts @@ -3,7 +3,7 @@ import { getEphemeralVmRecipeCheckoutModeError, getEphemeralVmRecipeResultSchemaVersion } from './ephemeral-vm-recipe-checkout-mode' -import type { OrcaVmRecipe } from './types' +import type { OrcaVmRecipe } from './orca-yaml-hook-types' const defaultRecipe: OrcaVmRecipe = { id: 'cloud-sandbox', diff --git a/src/shared/ephemeral-vm-recipe-checkout-mode.ts b/src/shared/ephemeral-vm-recipe-checkout-mode.ts index 1b50da88312..67e27f3e89e 100644 --- a/src/shared/ephemeral-vm-recipe-checkout-mode.ts +++ b/src/shared/ephemeral-vm-recipe-checkout-mode.ts @@ -1,6 +1,6 @@ import { getEphemeralVmRecipeResultCheckoutMode } from './ephemeral-vm-recipes' import type { EphemeralVmRecipeResult } from './ephemeral-vm-recipes' -import type { OrcaVmRecipe } from './types' +import type { OrcaVmRecipe } from './orca-yaml-hook-types' export function getEphemeralVmRecipeResultSchemaVersion(recipe: OrcaVmRecipe): 1 | 2 { return recipe.checkoutMode === 'provisioned-root' ? 2 : 1 diff --git a/src/shared/ephemeral-vm-recipe-doctor.test.ts b/src/shared/ephemeral-vm-recipe-doctor.test.ts index 2c6a22a41e8..2c738bd73f3 100644 --- a/src/shared/ephemeral-vm-recipe-doctor.test.ts +++ b/src/shared/ephemeral-vm-recipe-doctor.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { doctorEphemeralVmRecipe } from './ephemeral-vm-recipe-doctor' -import type { OrcaVmRecipe } from './types' +import type { OrcaVmRecipe } from './orca-yaml-hook-types' const roots: string[] = [] diff --git a/src/shared/ephemeral-vm-recipe-doctor.ts b/src/shared/ephemeral-vm-recipe-doctor.ts index 9f0ffc8a91b..35e279c1855 100644 --- a/src/shared/ephemeral-vm-recipe-doctor.ts +++ b/src/shared/ephemeral-vm-recipe-doctor.ts @@ -4,7 +4,7 @@ import type { EphemeralVmRecipeDoctorCheck, EphemeralVmRecipeDoctorResult } from './ephemeral-vm-recipes' -import type { OrcaVmRecipe } from './types' +import type { OrcaVmRecipe } from './orca-yaml-hook-types' export function doctorEphemeralVmRecipe(args: { repoPath: string diff --git a/src/shared/ephemeral-vm-recipe-lifecycle-payload.ts b/src/shared/ephemeral-vm-recipe-lifecycle-payload.ts index ab1a28904f1..4cee67602c2 100644 --- a/src/shared/ephemeral-vm-recipe-lifecycle-payload.ts +++ b/src/shared/ephemeral-vm-recipe-lifecycle-payload.ts @@ -1,7 +1,7 @@ import type { EphemeralVmRecipeResult } from './ephemeral-vm-recipes' import type { EphemeralVmRecipeContext } from './ephemeral-vm-recipe-runner' import { quoteShellToken } from './ephemeral-vm-recipe-process' -import type { OrcaVmRecipe } from './types' +import type { OrcaVmRecipe } from './orca-yaml-hook-types' export type EphemeralVmRecipeLifecycleMode = 'suspend' | 'resume' | 'destroy' diff --git a/src/shared/ephemeral-vm-recipe-repo-url.ts b/src/shared/ephemeral-vm-recipe-repo-url.ts index badd9fa3fbd..d4089591a4d 100644 --- a/src/shared/ephemeral-vm-recipe-repo-url.ts +++ b/src/shared/ephemeral-vm-recipe-repo-url.ts @@ -1,5 +1,5 @@ import { stripCredentialsFromMessage } from './git-remote-error' -import type { OrcaVmRecipe } from './types' +import type { OrcaVmRecipe } from './orca-yaml-hook-types' export function getProvisionedRootRecipeRepoUrl( checkoutMode: OrcaVmRecipe['checkoutMode'], diff --git a/src/shared/ephemeral-vm-recipe-runner.ts b/src/shared/ephemeral-vm-recipe-runner.ts index 496e9573fdb..60d6545abf7 100644 --- a/src/shared/ephemeral-vm-recipe-runner.ts +++ b/src/shared/ephemeral-vm-recipe-runner.ts @@ -1,7 +1,7 @@ import type { spawn } from 'node:child_process' import { randomUUID } from 'node:crypto' import { statSync } from 'node:fs' -import type { OrcaVmRecipe } from './types' +import type { OrcaVmRecipe } from './orca-yaml-hook-types' import { parseEphemeralVmRecipeResult, type EphemeralVmRecipeResult } from './ephemeral-vm-recipes' import { getEphemeralVmRecipeCheckoutModeError, diff --git a/src/shared/execution-host-registry.ts b/src/shared/execution-host-registry.ts index bceddd86a4b..890466ec703 100644 --- a/src/shared/execution-host-registry.ts +++ b/src/shared/execution-host-registry.ts @@ -14,7 +14,8 @@ import { MIN_COMPATIBLE_RUNTIME_SERVER_VERSION, RUNTIME_PROTOCOL_VERSION } from import type { RuntimeStatus } from './runtime-types' import type { SshConnectionState, SshConnectionStatus } from './ssh-types' import type { RuntimeEnvironmentSource } from './runtime-environments' -import type { GlobalSettings, Repo } from './types' +import type { GlobalSettings } from './global-settings-types' +import type { Repo } from './repo-types' export type ExecutionHostHealth = | 'local' diff --git a/src/shared/execution-host.ts b/src/shared/execution-host.ts index 814182c2bae..18d584674bf 100644 --- a/src/shared/execution-host.ts +++ b/src/shared/execution-host.ts @@ -1,4 +1,6 @@ -import type { GlobalSettings, Repo, Worktree } from './types' +import type { GlobalSettings } from './global-settings-types' +import type { Repo } from './repo-types' +import type { Worktree } from './worktree/types' export const LOCAL_EXECUTION_HOST_ID = 'local' export const ALL_EXECUTION_HOSTS_SCOPE = 'all' diff --git a/src/shared/external-worktree-inbox.test.ts b/src/shared/external-worktree-inbox.test.ts index 2c29fe4a117..251b07026a6 100644 --- a/src/shared/external-worktree-inbox.test.ts +++ b/src/shared/external-worktree-inbox.test.ts @@ -1,12 +1,8 @@ import { describe, expect, it } from 'vitest' -import type { - DetectedWorktree, - DetectedWorktreeListResult, - GlobalSettings, - Repo, - Worktree -} from './types' +import type { GlobalSettings } from './global-settings-types' +import type { Repo } from './repo-types' +import type { DetectedWorktree, DetectedWorktreeListResult, Worktree } from './worktree/types' import { getHiddenExternalWorktrees, getHiddenImportableExternalWorktrees, diff --git a/src/shared/external-worktree-inbox.ts b/src/shared/external-worktree-inbox.ts index c42a1249bd7..309da287892 100644 --- a/src/shared/external-worktree-inbox.ts +++ b/src/shared/external-worktree-inbox.ts @@ -1,5 +1,6 @@ import { normalizeRuntimePathForComparison } from './cross-platform-path' -import type { DetectedWorktree, DetectedWorktreeListResult, Repo } from './types' +import type { Repo } from './repo-types' +import type { DetectedWorktree, DetectedWorktreeListResult } from './worktree/types' import { effectiveExternalWorktreeVisibility, isLegacyRepoForExternalWorktreeVisibility diff --git a/src/shared/external-worktree-visibility.ts b/src/shared/external-worktree-visibility.ts index fc4af8c1681..999bfbcbeb4 100644 --- a/src/shared/external-worktree-visibility.ts +++ b/src/shared/external-worktree-visibility.ts @@ -1,5 +1,5 @@ import { normalizeRuntimePathSeparators } from './cross-platform-path' -import type { ExternalWorktreeVisibility, Repo } from './types' +import type { ExternalWorktreeVisibility, Repo } from './repo-types' export const EXTERNAL_WORKTREE_VISIBILITY_ROLLOUT_AT = Date.UTC(2026, 4, 23) export const UNKNOWN_EXTERNAL_WORKTREE_PARENT_PATH = 'Unknown location' diff --git a/src/shared/folder-workspace-worktree.test.ts b/src/shared/folder-workspace-worktree.test.ts index 270e62eb2ce..420da41488c 100644 --- a/src/shared/folder-workspace-worktree.test.ts +++ b/src/shared/folder-workspace-worktree.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { FolderWorkspace } from './types' +import type { FolderWorkspace } from './folder-workspace-types' import { folderWorkspaceToWorktree } from './folder-workspace-worktree' function makeFolderWorkspace(overrides: Partial = {}): FolderWorkspace { diff --git a/src/shared/folder-workspace-worktree.ts b/src/shared/folder-workspace-worktree.ts index 3bc62acb41f..c8715a12b3a 100644 --- a/src/shared/folder-workspace-worktree.ts +++ b/src/shared/folder-workspace-worktree.ts @@ -1,4 +1,5 @@ -import type { FolderWorkspace, Worktree } from './types' +import type { FolderWorkspace } from './folder-workspace-types' +import type { Worktree } from './worktree/types' import { folderWorkspaceKey } from './workspace-scope' import { parseExecutionHostId, toSshExecutionHostId } from './execution-host' import { normalizeWorkspaceCreatorProvenance } from './workspace-creator-provenance' diff --git a/src/shared/folder-workspaces.ts b/src/shared/folder-workspaces.ts index e5badf0d8d6..97409bd271d 100644 --- a/src/shared/folder-workspaces.ts +++ b/src/shared/folder-workspaces.ts @@ -1,4 +1,5 @@ -import type { FolderWorkspace, ProjectGroup } from './types' +import type { FolderWorkspace } from './folder-workspace-types' +import type { ProjectGroup } from './project-group-types' import { isTuiAgent } from './tui-agent-config' import { normalizeStoredTaskSourceContext } from './task-source-context' import { normalizeWorkspaceLinkedItem } from './workspace-linked-item' diff --git a/src/shared/git-effective-upstream.ts b/src/shared/git-effective-upstream.ts index 8292a9299c9..db364c53b20 100644 --- a/src/shared/git-effective-upstream.ts +++ b/src/shared/git-effective-upstream.ts @@ -1,5 +1,5 @@ import { isNoUpstreamError } from './git-remote-error' -import type { GitUpstreamStatus } from './types' +import type { GitUpstreamStatus } from './git-status-types' import { getConfiguredBranchRemoteUpstream, hasConfiguredBranchPushTarget diff --git a/src/shared/git-publish-target-status.ts b/src/shared/git-publish-target-status.ts index a391fb35c7f..392a7f13291 100644 --- a/src/shared/git-publish-target-status.ts +++ b/src/shared/git-publish-target-status.ts @@ -1,5 +1,5 @@ import type { GitUpstreamStatus } from './git-status-types' -import type { GitPushTarget } from './types' +import type { GitPushTarget } from './worktree/types' import { parseGitRevListAheadBehindCounts } from './git-rev-list-output' export type GitCommandRunner = (args: string[]) => Promise<{ stdout: string }> diff --git a/src/shared/git-push-target-validation.ts b/src/shared/git-push-target-validation.ts index e4de8736aad..208874bd10b 100644 --- a/src/shared/git-push-target-validation.ts +++ b/src/shared/git-push-target-validation.ts @@ -1,4 +1,4 @@ -import type { GitPushTarget } from './types' +import type { GitPushTarget } from './worktree/types' const SAFE_REMOTE_NAME_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/ const GITHUB_CLONE_URL = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\.git$/ diff --git a/src/shared/github/project-types.ts b/src/shared/github/project-types.ts index 6560e410474..dc1e20ea7ce 100644 --- a/src/shared/github/project-types.ts +++ b/src/shared/github/project-types.ts @@ -4,12 +4,10 @@ // import from here directly — do not re-export through `./types.ts` just to // match the existing import block; routing through the issue types module // would obscure ownership of the Project surface. -import type { - GitHubAssignableUser, - GitHubIssueUpdate, - GitHubWorkItemDetails, - PRComment -} from '../types' +import type { PRComment } from './comment-types' +import type { GitHubAssignableUser } from './pull-request-types' +import type { GitHubWorkItemDetails } from './work-item-types' +import type { GitHubIssueUpdate } from '../issue-mutation-types' export type GitHubProjectViewLayout = 'TABLE_LAYOUT' | 'BOARD_LAYOUT' | 'ROADMAP_LAYOUT' export type GitHubProjectOwnerType = 'organization' | 'user' diff --git a/src/shared/github/pull-request-auto-merge-availability.ts b/src/shared/github/pull-request-auto-merge-availability.ts index 90500d1618e..867224fc0ee 100644 --- a/src/shared/github/pull-request-auto-merge-availability.ts +++ b/src/shared/github/pull-request-auto-merge-availability.ts @@ -1,4 +1,4 @@ -import type { PRMergeableState, PRReviewDecision, PRState } from '../types' +import type { PRMergeableState, PRReviewDecision, PRState } from './pull-request-types' export type GitHubPRAutoMergeAvailabilityInput = { state: PRState | 'open' | 'closed' | 'merged' | 'draft' diff --git a/src/shared/github/pull-request-for-branch-outcome.test.ts b/src/shared/github/pull-request-for-branch-outcome.test.ts index 2eff1b7ae33..7ad79b1f61c 100644 --- a/src/shared/github/pull-request-for-branch-outcome.test.ts +++ b/src/shared/github/pull-request-for-branch-outcome.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { PRInfo, PRRefreshOutcome } from '../types' +import type { PRRefreshOutcome } from './pull-request-refresh-types' +import type { PRInfo } from './pull-request-types' import { normalizeGitHubPRForBranchOutcome } from './pull-request-for-branch-outcome' const PR = { diff --git a/src/shared/github/pull-request-for-branch-outcome.ts b/src/shared/github/pull-request-for-branch-outcome.ts index b6cdc180609..9e4a4574043 100644 --- a/src/shared/github/pull-request-for-branch-outcome.ts +++ b/src/shared/github/pull-request-for-branch-outcome.ts @@ -1,4 +1,5 @@ -import type { PRInfo, PRRefreshOutcome } from '../types' +import type { PRRefreshOutcome } from './pull-request-refresh-types' +import type { PRInfo } from './pull-request-types' export type GitHubPRForBranchResponse = PRRefreshOutcome | PRInfo | null diff --git a/src/shared/github/pull-request-merge-methods.ts b/src/shared/github/pull-request-merge-methods.ts index 2072706a41b..1cfb709635b 100644 --- a/src/shared/github/pull-request-merge-methods.ts +++ b/src/shared/github/pull-request-merge-methods.ts @@ -1,4 +1,4 @@ -import type { GitHubPRMergeMethod, GitHubPRMergeMethodSettings } from '../types' +import type { GitHubPRMergeMethod, GitHubPRMergeMethodSettings } from './pull-request-types' export const GITHUB_PR_MERGE_METHODS = ['squash', 'merge', 'rebase'] as const diff --git a/src/shared/gitlab-job-trace-check-details.test.ts b/src/shared/gitlab-job-trace-check-details.test.ts index 59cf24cbf79..9cc9240dd36 100644 --- a/src/shared/gitlab-job-trace-check-details.test.ts +++ b/src/shared/gitlab-job-trace-check-details.test.ts @@ -5,7 +5,7 @@ import { } from './gitlab-job-trace-check-details' import { gitLabPipelineJobsToPRChecks } from './gitlab-pipeline-checks' import type { GitLabPipelineJob } from './gitlab-types' -import type { PRCheckDetail } from './types' +import type { PRCheckDetail } from './github/check-types' const STRINGS = { emptyTrace: 'No log is available for this GitLab job.' } diff --git a/src/shared/gitlab-job-trace-check-details.ts b/src/shared/gitlab-job-trace-check-details.ts index fd81789bc0a..4c0129ca6d4 100644 --- a/src/shared/gitlab-job-trace-check-details.ts +++ b/src/shared/gitlab-job-trace-check-details.ts @@ -1,5 +1,5 @@ import { gitLabJobTraceToLogExcerpt } from './gitlab-job-log-excerpt' -import type { PRCheckDetail, PRCheckRunDetails } from './types' +import type { PRCheckDetail, PRCheckRunDetails } from './github/check-types' export type GitLabJobTraceCheckDetailsStrings = { /** Shown for a job with no log: never ran, waiting on a human, or log erased/expired. */ diff --git a/src/shared/gitlab-pipeline-checks.test.ts b/src/shared/gitlab-pipeline-checks.test.ts index 71f2be73d8d..ee97352fcc0 100644 --- a/src/shared/gitlab-pipeline-checks.test.ts +++ b/src/shared/gitlab-pipeline-checks.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { gitLabPipelineJobsToPRChecks } from './gitlab-pipeline-checks' -import type { GitLabPipelineJob } from './types' +import type { GitLabPipelineJob } from './gitlab-types' describe('gitLabPipelineJobsToPRChecks', () => { it('maps GitLab pipeline jobs into right-panel check rows', () => { diff --git a/src/shared/gitlab-pipeline-checks.ts b/src/shared/gitlab-pipeline-checks.ts index 9cb9a2b1ebd..e9beb309853 100644 --- a/src/shared/gitlab-pipeline-checks.ts +++ b/src/shared/gitlab-pipeline-checks.ts @@ -1,5 +1,5 @@ import type { GitLabPipelineJob } from './gitlab-types' -import type { PRCheckDetail } from './types' +import type { PRCheckDetail } from './github/check-types' export function mapGitLabPipelineJobStatusToCheckStatus(status: string): PRCheckDetail['status'] { const s = status.toLowerCase() diff --git a/src/shared/gitlab-projects.ts b/src/shared/gitlab-projects.ts index f09d6fa19f1..6f518febb4e 100644 --- a/src/shared/gitlab-projects.ts +++ b/src/shared/gitlab-projects.ts @@ -1,6 +1,6 @@ // Why: pure helpers for GitLabProjectSettings — kept out of the IPC // handler so the recents logic is testable without mocking the Store. -import type { GitLabProjectSettings } from './types' +import type { GitLabProjectSettings } from './gitlab-types' /** Default max recents kept before older entries fall off. */ export const GITLAB_RECENTS_MAX = 10 diff --git a/src/shared/gitlab-types.ts b/src/shared/gitlab-types.ts index c9f2a0fffba..56a506a0f5a 100644 --- a/src/shared/gitlab-types.ts +++ b/src/shared/gitlab-types.ts @@ -1,5 +1,10 @@ /* GitLab-specific shared types, split from `./types` to avoid merge conflicts on upstream syncs; re-exported from `./types` for import stability. */ -import type { CheckStatus, ClassifiedError, PRConflictSummary, ProviderCheckSummary } from './types' +import type { ClassifiedError } from './classified-error' +import type { + CheckStatus, + PRConflictSummary, + ProviderCheckSummary +} from './github/pull-request-types' import type { HostedReviewDecision } from './hosted-review' // Why: flat owner/repo is inadequate — projects nest (`group/subgroup/project`) and self-hosted hosts must travel with the path for URL/glab targeting. diff --git a/src/shared/hook-command-source-policy.ts b/src/shared/hook-command-source-policy.ts index 2a3b634bb53..32cd81af0e1 100644 --- a/src/shared/hook-command-source-policy.ts +++ b/src/shared/hook-command-source-policy.ts @@ -1,4 +1,4 @@ -import type { HookCommandSourcePolicy } from './types' +import type { HookCommandSourcePolicy } from './orca-yaml-hook-types' export function normalizeHookCommandSourcePolicy(policy: unknown): HookCommandSourcePolicy { if (policy === 'local-only' || policy === 'run-both' || policy === 'shared-only') { diff --git a/src/shared/host-lineage-contract.ts b/src/shared/host-lineage-contract.ts index 6541b84abac..7bb69f02bb9 100644 --- a/src/shared/host-lineage-contract.ts +++ b/src/shared/host-lineage-contract.ts @@ -1,7 +1,7 @@ import type { ExecutionHostId, LOCAL_EXECUTION_HOST_ID } from './execution-host' import type { SshExecutionHostId } from './detected-worktree-provider-contract' import type { DirectSshAuthority } from './ssh-types' -import type { WorktreeLineage, WorkspaceLineage } from './types' +import type { WorkspaceLineage, WorktreeLineage } from './worktree/lineage-types' export type ListDesktopLineageForHostArgs = | { executionHostId: typeof LOCAL_EXECUTION_HOST_ID } diff --git a/src/shared/host-repo-catalog-contract.ts b/src/shared/host-repo-catalog-contract.ts index 6a44bf8bd85..30f88d7f271 100644 --- a/src/shared/host-repo-catalog-contract.ts +++ b/src/shared/host-repo-catalog-contract.ts @@ -1,7 +1,7 @@ import type { ExecutionHostId, LOCAL_EXECUTION_HOST_ID } from './execution-host' import type { SshExecutionHostId } from './detected-worktree-provider-contract' import type { DirectSshAuthority } from './ssh-types' -import type { Repo } from './types' +import type { Repo } from './repo-types' export type ListReposForExecutionHostArgs = | { executionHostId: typeof LOCAL_EXECUTION_HOST_ID } diff --git a/src/shared/host-setting-overrides.test.ts b/src/shared/host-setting-overrides.test.ts index 58871e77892..e1fd5a390f8 100644 --- a/src/shared/host-setting-overrides.test.ts +++ b/src/shared/host-setting-overrides.test.ts @@ -6,7 +6,7 @@ import { getHostSettingOverride, setHostSettingOverride } from './host-setting-overrides' -import type { GlobalSettings } from './types' +import type { GlobalSettings } from './global-settings-types' function settingsWith( overrides: GlobalSettings['hostSettingOverrides'] diff --git a/src/shared/host-setting-overrides.ts b/src/shared/host-setting-overrides.ts index 5d7f0693d3a..840d64682a8 100644 --- a/src/shared/host-setting-overrides.ts +++ b/src/shared/host-setting-overrides.ts @@ -1,5 +1,5 @@ import type { ExecutionHostId } from './execution-host' -import type { GlobalSettings } from './types' +import type { GlobalSettings } from './global-settings-types' /** Per-host overrides for client preferences that genuinely vary by execution * host. NARROW by design: only settings whose value is meaningless to share diff --git a/src/shared/hosted-review-github.test.ts b/src/shared/hosted-review-github.test.ts index bbc17fa785b..1ad43f32b99 100644 --- a/src/shared/hosted-review-github.test.ts +++ b/src/shared/hosted-review-github.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { hostedReviewInfoFromGitHubPRInfo } from './hosted-review-github' -import type { PRInfo } from './types' +import type { PRInfo } from './github/pull-request-types' const pr: PRInfo = { number: 12, diff --git a/src/shared/hosted-review-github.ts b/src/shared/hosted-review-github.ts index 3cf76175bda..26d866d5c09 100644 --- a/src/shared/hosted-review-github.ts +++ b/src/shared/hosted-review-github.ts @@ -1,5 +1,5 @@ import type { HostedReviewInfo } from './hosted-review' -import type { PRInfo } from './types' +import type { PRInfo } from './github/pull-request-types' export function hostedReviewInfoFromGitHubPRInfo(pr: PRInfo): HostedReviewInfo { return { diff --git a/src/shared/hosted-review.ts b/src/shared/hosted-review.ts index c0763cb4184..2d0b218e9b1 100644 --- a/src/shared/hosted-review.ts +++ b/src/shared/hosted-review.ts @@ -4,7 +4,7 @@ import type { PRConflictSummary, PRMergeableState, PRReviewDecision -} from './types' +} from './github/pull-request-types' export type HostedReviewProvider = | 'github' diff --git a/src/shared/keybindings.ts b/src/shared/keybindings.ts index cbb448e030c..43c30d89d90 100644 --- a/src/shared/keybindings.ts +++ b/src/shared/keybindings.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines -- Why: keep the shortcut registry, parser, formatter, and conflict detector in one shared module so main/renderer/browser/Settings can't drift. */ -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' import { ALL_TUI_AGENTS, TUI_AGENT_DISPLAY_NAMES } from './tui-agent-display-names' export type KeybindingScope = diff --git a/src/shared/linear/links.ts b/src/shared/linear/links.ts index ce457f0a419..c4b7f6b2c52 100644 --- a/src/shared/linear/links.ts +++ b/src/shared/linear/links.ts @@ -1,4 +1,5 @@ -import type { LinearConnectionStatus, LinearIssue, LinearWorkspace } from '../types' +import type { LinearIssue } from './issue-types' +import type { LinearConnectionStatus, LinearWorkspace } from './workspace-types' export function buildLinearTeamUrl(args: { organizationUrlKey?: string | null diff --git a/src/shared/local-account-runtime.ts b/src/shared/local-account-runtime.ts index 2e2b8b000da..b52234db538 100644 --- a/src/shared/local-account-runtime.ts +++ b/src/shared/local-account-runtime.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from './types' +import type { GlobalSettings } from './global-settings-types' import { normalizeGlobalWindowsRuntimeDefault } from './project-execution-runtime' export type LocalAccountRuntimeTarget = { diff --git a/src/shared/local-windows-terminal-runtime.ts b/src/shared/local-windows-terminal-runtime.ts index 0eb61d8bf08..ea225752a48 100644 --- a/src/shared/local-windows-terminal-runtime.ts +++ b/src/shared/local-windows-terminal-runtime.ts @@ -1,5 +1,5 @@ import type { ProjectExecutionRuntimeResolution } from './project-execution-runtime' -import type { GlobalSettings } from './types' +import type { GlobalSettings } from './global-settings-types' type LocalWindowsTerminalRuntimeSettings = | Partial> diff --git a/src/shared/managed-agent-hook-targets.ts b/src/shared/managed-agent-hook-targets.ts index a32dc63d411..fd339c52eaa 100644 --- a/src/shared/managed-agent-hook-targets.ts +++ b/src/shared/managed-agent-hook-targets.ts @@ -1,6 +1,6 @@ import { AGENT_HOOK_TARGETS, type AgentHookTarget } from './agent-hook-types' import { getTuiAgentDetectCommands, TUI_AGENT_CONFIG } from './tui-agent-config' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' export type ManagedAgentHookTarget = { agent: AgentHookTarget diff --git a/src/shared/manual-repo-order.test.ts b/src/shared/manual-repo-order.test.ts index 3044a8d5399..616c6209bde 100644 --- a/src/shared/manual-repo-order.test.ts +++ b/src/shared/manual-repo-order.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { ManualRepoOrderEntry, Repo } from './types' +import type { Repo } from './repo-types' +import type { ManualRepoOrderEntry } from './ui-chrome-types' import { applyManualRepoOrder, getManualRepoOrder, diff --git a/src/shared/manual-repo-order.ts b/src/shared/manual-repo-order.ts index 9813412c676..dce66fa2bee 100644 --- a/src/shared/manual-repo-order.ts +++ b/src/shared/manual-repo-order.ts @@ -1,6 +1,7 @@ import { getRepoExecutionHostId, normalizeExecutionHostId } from './execution-host' import { getRepoHostIdentityForParts } from './repo-host-identity' -import type { ManualRepoOrderEntry, Repo } from './types' +import type { Repo } from './repo-types' +import type { ManualRepoOrderEntry } from './ui-chrome-types' function getEntryKey(entry: ManualRepoOrderEntry): string { return getRepoHostIdentityForParts(entry.repoId, entry.hostId) diff --git a/src/shared/markdown-document-listing-limits.test.ts b/src/shared/markdown-document-listing-limits.test.ts index 574e146b4ee..54a3ee18500 100644 --- a/src/shared/markdown-document-listing-limits.test.ts +++ b/src/shared/markdown-document-listing-limits.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { MarkdownDocument } from './types' +import type { MarkdownDocument } from './filesystem-entry-types' import { assertMarkdownDocumentsWithinLimit, createMarkdownDocumentListingBudget, diff --git a/src/shared/markdown-document-listing-limits.ts b/src/shared/markdown-document-listing-limits.ts index 9c9b39c49b3..9b3676bb3d4 100644 --- a/src/shared/markdown-document-listing-limits.ts +++ b/src/shared/markdown-document-listing-limits.ts @@ -1,4 +1,4 @@ -import type { MarkdownDocument } from './types' +import type { MarkdownDocument } from './filesystem-entry-types' import { measureUtf8ByteLength } from './utf8-byte-limits' export const MARKDOWN_DOCUMENT_LISTING_MAX_DOCUMENTS = 20_000 diff --git a/src/shared/nested-repo-telemetry.test.ts b/src/shared/nested-repo-telemetry.test.ts index e2a1a7e579f..bce580bc0cb 100644 --- a/src/shared/nested-repo-telemetry.test.ts +++ b/src/shared/nested-repo-telemetry.test.ts @@ -9,7 +9,7 @@ import { createNestedRepoTelemetryAttemptId, shouldEmitNestedRepoImportSubmitTelemetry } from './nested-repo-telemetry' -import type { NestedRepoScanResult, ProjectGroupImportResult } from './types' +import type { NestedRepoScanResult, ProjectGroupImportResult } from './project-group-types' const scanResult: NestedRepoScanResult = { selectedPath: '/workspace/platform', diff --git a/src/shared/nested-repo-telemetry.ts b/src/shared/nested-repo-telemetry.ts index 44c536a7b3b..7aea296c57a 100644 --- a/src/shared/nested-repo-telemetry.ts +++ b/src/shared/nested-repo-telemetry.ts @@ -2,7 +2,7 @@ import type { NestedRepoScanResult, ProjectGroupImportMode, ProjectGroupImportResult -} from './types' +} from './project-group-types' export const NESTED_REPO_TELEMETRY_MAX_REPO_COUNT = 500 diff --git a/src/shared/new-workspace-dialog-repo.test.ts b/src/shared/new-workspace-dialog-repo.test.ts index cfc65ded826..e14116f915e 100644 --- a/src/shared/new-workspace-dialog-repo.test.ts +++ b/src/shared/new-workspace-dialog-repo.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Repo } from './types' +import type { Repo } from './repo-types' import { getNewWorkspaceDialogEligibleRepos, resolveNewWorkspaceDialogGitRepoId, diff --git a/src/shared/new-workspace-dialog-repo.ts b/src/shared/new-workspace-dialog-repo.ts index 2e016853ce2..ae037513ca8 100644 --- a/src/shared/new-workspace-dialog-repo.ts +++ b/src/shared/new-workspace-dialog-repo.ts @@ -5,7 +5,7 @@ import { type ExecutionHostScope } from './execution-host' import { isGitRepoKind } from './repo-kind' -import type { Repo } from './types' +import type { Repo } from './repo-types' type NewWorkspaceDialogRepo = Pick< Repo, diff --git a/src/shared/new-workspace/fork-push-warning.ts b/src/shared/new-workspace/fork-push-warning.ts index 5c3a7c29e68..1cb634caf61 100644 --- a/src/shared/new-workspace/fork-push-warning.ts +++ b/src/shared/new-workspace/fork-push-warning.ts @@ -1,4 +1,4 @@ -import type { GitHubPrStartPoint } from '../types' +import type { GitHubPrStartPoint } from '../worktree/types' export const FORK_PUSH_NO_MAINTAINER_EDIT_WARNING = 'This PR has "Allow edits from maintainers" off; pushing to the fork may be rejected by GitHub.' diff --git a/src/shared/new-workspace/smart-workspace-linear-intent.ts b/src/shared/new-workspace/smart-workspace-linear-intent.ts index ec77fcb2fbd..1bca3ee822a 100644 --- a/src/shared/new-workspace/smart-workspace-linear-intent.ts +++ b/src/shared/new-workspace/smart-workspace-linear-intent.ts @@ -1,4 +1,4 @@ -import type { LinearIssue } from '../types' +import type { LinearIssue } from '../linear/issue-types' import { parseLinearIssueInput, parseLinearIssueUrlIntent, diff --git a/src/shared/new-workspace/smart-workspace-source-results.ts b/src/shared/new-workspace/smart-workspace-source-results.ts index e17a4029d8e..40de888f150 100644 --- a/src/shared/new-workspace/smart-workspace-source-results.ts +++ b/src/shared/new-workspace/smart-workspace-source-results.ts @@ -1,11 +1,9 @@ -import type { - BaseRefSearchResult, - GitHubWorkItem, - GitLabWorkItem, - JiraIssue, - LinearCollectionResult, - LinearIssue -} from '../types' +import type { GitHubWorkItem } from '../github/work-item-types' +import type { GitLabWorkItem } from '../gitlab-types' +import type { JiraIssue } from '../jira-types' +import type { LinearIssue } from '../linear/issue-types' +import type { LinearCollectionResult } from '../linear/workspace-types' +import type { BaseRefSearchResult } from '../repo-types' import { JIRA_ISSUE_KEY_PATTERN, parseJiraIssueUrl } from '../jira-issue-url' import { isSmartWorkspaceLinearIssueIntentMatch, diff --git a/src/shared/new-workspace/workspace-source.ts b/src/shared/new-workspace/workspace-source.ts index db15573d9ab..42af0804915 100644 --- a/src/shared/new-workspace/workspace-source.ts +++ b/src/shared/new-workspace/workspace-source.ts @@ -1,5 +1,7 @@ import { getLinearOrganizationUrlKeyFromIssueUrl } from '../linear/links' -import type { FolderWorkspaceLinkedTask, JiraIssue, LinearIssue } from '../types' +import type { FolderWorkspaceLinkedTask } from '../folder-workspace-types' +import type { JiraIssue } from '../jira-types' +import type { LinearIssue } from '../linear/issue-types' import { getLinkedWorkItemSuggestedName, getLinkedWorkItemWorkspaceName, diff --git a/src/shared/open-in-applications.ts b/src/shared/open-in-applications.ts index ace0f526bb8..5e48ec4c824 100644 --- a/src/shared/open-in-applications.ts +++ b/src/shared/open-in-applications.ts @@ -1,4 +1,4 @@ -import type { OpenInApplication } from './types' +import type { OpenInApplication } from './ui-chrome-types' export const OPEN_IN_APPLICATIONS_MAX = 8 export const DEFAULT_OPEN_IN_APPLICATIONS: OpenInApplication[] = [ diff --git a/src/shared/orca-yaml.ts b/src/shared/orca-yaml.ts index 35b49b20a7c..b2a84104df2 100644 --- a/src/shared/orca-yaml.ts +++ b/src/shared/orca-yaml.ts @@ -4,7 +4,7 @@ import type { OrcaHooks, OrcaVmRecipe, OrcaVmRecipeDiagnostic -} from './types' +} from './orca-yaml-hook-types' import { isOrcaYamlFieldWithinLimit, isOrcaYamlTextWithinLimit, diff --git a/src/shared/osc52-clipboard-settings.ts b/src/shared/osc52-clipboard-settings.ts index 2beb06dda16..2f1605bfc6d 100644 --- a/src/shared/osc52-clipboard-settings.ts +++ b/src/shared/osc52-clipboard-settings.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from './types' +import type { GlobalSettings } from './global-settings-types' type Osc52ClipboardSettings = Pick< GlobalSettings, diff --git a/src/shared/plugins/plugin-vm-recipe-artifact.ts b/src/shared/plugins/plugin-vm-recipe-artifact.ts index df99b40178c..fb39a7bccb6 100644 --- a/src/shared/plugins/plugin-vm-recipe-artifact.ts +++ b/src/shared/plugins/plugin-vm-recipe-artifact.ts @@ -1,6 +1,6 @@ import { z } from 'zod' import { ORCA_VM_RECIPE_ID_PATTERN, ORCA_VM_RECIPE_ID_RULE } from '../orca-yaml' -import type { OrcaVmRecipe } from '../types' +import type { OrcaVmRecipe } from '../orca-yaml-hook-types' const recipeCommandSchema = z .string() diff --git a/src/shared/pr-check-severity-order.test.ts b/src/shared/pr-check-severity-order.test.ts index 685886513aa..e2b6b39d285 100644 --- a/src/shared/pr-check-severity-order.test.ts +++ b/src/shared/pr-check-severity-order.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { PRCheckDetail } from './types' +import type { PRCheckDetail } from './github/check-types' import { getCheckSeverityRank, sortChecksBySeverity } from './pr-check-severity-order' import { mapGitLabPipelineJobStatusToConclusion } from './gitlab-pipeline-checks' diff --git a/src/shared/pr-check-severity-order.ts b/src/shared/pr-check-severity-order.ts index 0bf9effba63..db0230d37dd 100644 --- a/src/shared/pr-check-severity-order.ts +++ b/src/shared/pr-check-severity-order.ts @@ -1,4 +1,4 @@ -import type { PRCheckDetail } from './types' +import type { PRCheckDetail } from './github/check-types' // Why: `neutral`/`skipped` carry no signal, so they sink below `success` — otherwise a // wall of skipped jobs buries the passing checks a reviewer actually reads. diff --git a/src/shared/pr-check-status.test.ts b/src/shared/pr-check-status.test.ts index b924b68bc19..50f5132b40e 100644 --- a/src/shared/pr-check-status.test.ts +++ b/src/shared/pr-check-status.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { derivePRCheckStatus, derivePRCheckStatusFromRollup } from './pr-check-status' -import type { PRCheckDetail } from './types' +import type { PRCheckDetail } from './github/check-types' const check = ( status: PRCheckDetail['status'], diff --git a/src/shared/pr-check-status.ts b/src/shared/pr-check-status.ts index b2127671d1a..f67fd5cb66b 100644 --- a/src/shared/pr-check-status.ts +++ b/src/shared/pr-check-status.ts @@ -1,5 +1,6 @@ import { summarizeProviderChecks } from './provider-check-summary' -import type { CheckStatus, PRCheckDetail } from './types' +import type { PRCheckDetail } from './github/check-types' +import type { CheckStatus } from './github/pull-request-types' /** Derives the review status from the normalized check contract. */ export function derivePRCheckStatus(checks: readonly PRCheckDetail[]): CheckStatus { diff --git a/src/shared/pr-checks-fix-prompt.ts b/src/shared/pr-checks-fix-prompt.ts index ff50a410942..bf68218240d 100644 --- a/src/shared/pr-checks-fix-prompt.ts +++ b/src/shared/pr-checks-fix-prompt.ts @@ -1,4 +1,4 @@ -import type { PRCheckDetail, PRCheckRunDetails } from './types' +import type { PRCheckDetail, PRCheckRunDetails } from './github/check-types' export const PROMPT_LOG_TAIL_LINES = 150 export const PROMPT_LOG_TAIL_SCAN_CODE_UNITS = 256 * 1024 diff --git a/src/shared/pr-comment-audience.test.ts b/src/shared/pr-comment-audience.test.ts index 7499e791b90..76fb50fa888 100644 --- a/src/shared/pr-comment-audience.test.ts +++ b/src/shared/pr-comment-audience.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { PRComment } from './types' +import type { PRComment } from './github/comment-types' import { createBotAuthorOverrideSet } from './pr-bot-author-overrides' import { filterPRCommentsByAudience, diff --git a/src/shared/pr-comment-audience.ts b/src/shared/pr-comment-audience.ts index 280d0590a7a..4736ab5c372 100644 --- a/src/shared/pr-comment-audience.ts +++ b/src/shared/pr-comment-audience.ts @@ -1,4 +1,4 @@ -import type { PRComment } from './types' +import type { PRComment } from './github/comment-types' import { normalizePRCommentAuthorLogin } from './pr-bot-author-overrides' export type PRCommentAudienceFilter = 'all' | 'human' | 'bot' diff --git a/src/shared/pr-comment-groups.test.ts b/src/shared/pr-comment-groups.test.ts index b2f8de72980..06b801fbf66 100644 --- a/src/shared/pr-comment-groups.test.ts +++ b/src/shared/pr-comment-groups.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { PRComment } from './types' +import type { PRComment } from './github/comment-types' import { getPRCommentGroupCount, getPRCommentGroupId, diff --git a/src/shared/pr-comment-groups.ts b/src/shared/pr-comment-groups.ts index 8d43b9e2b77..119af69c72c 100644 --- a/src/shared/pr-comment-groups.ts +++ b/src/shared/pr-comment-groups.ts @@ -1,4 +1,4 @@ -import type { PRComment } from './types' +import type { PRComment } from './github/comment-types' export type PRCommentGroup = | { kind: 'standalone'; comment: PRComment } diff --git a/src/shared/project-groups.test.ts b/src/shared/project-groups.test.ts index 4c11e8d598b..6b228f698d9 100644 --- a/src/shared/project-groups.test.ts +++ b/src/shared/project-groups.test.ts @@ -8,7 +8,7 @@ import { normalizeProjectGroupName, normalizeProjectGroups } from './project-groups' -import type { Repo } from './types' +import type { Repo } from './repo-types' function repo(overrides: Partial): Repo { return { diff --git a/src/shared/project-groups.ts b/src/shared/project-groups.ts index 6d9b02d8354..67c2897ead6 100644 --- a/src/shared/project-groups.ts +++ b/src/shared/project-groups.ts @@ -1,5 +1,6 @@ import { normalizeExecutionHostId } from './execution-host' -import type { Repo, ProjectGroup, ProjectGroupCreatedFrom } from './types' +import type { ProjectGroup, ProjectGroupCreatedFrom } from './project-group-types' +import type { Repo } from './repo-types' export const UNGROUPED_PROJECT_GROUP_KEY = 'project-group:ungrouped' diff --git a/src/shared/project-host-setup-projection.test.ts b/src/shared/project-host-setup-projection.test.ts index d0456d392de..97ea8cc66b0 100644 --- a/src/shared/project-host-setup-projection.test.ts +++ b/src/shared/project-host-setup-projection.test.ts @@ -7,7 +7,7 @@ import { getProjectIdForProviderIdentity, isProjectRemoteIdentityPending } from './project-host-setup-projection' -import type { Repo } from './types' +import type { Repo } from './repo-types' function repo(overrides: Partial & Pick): Repo { return { diff --git a/src/shared/project-host-setup-projection.ts b/src/shared/project-host-setup-projection.ts index 1db66b2d2c9..537fa5af42b 100644 --- a/src/shared/project-host-setup-projection.ts +++ b/src/shared/project-host-setup-projection.ts @@ -1,13 +1,9 @@ import { getRepoExecutionHostId } from './execution-host' import { normalizeGitHubRemoteHost } from './git-remote-host-alias' import { githubRepoIdentityKey, isDefaultGitHubHost } from './github/repository-identity-key' -import type { - Project, - ProjectHostSetup, - ProjectProviderIdentity, - Repo, - WorktreeMeta -} from './types' +import type { Project, ProjectHostSetup, ProjectProviderIdentity } from './project-types' +import type { Repo } from './repo-types' +import type { WorktreeMeta } from './worktree/meta-types' type ProjectAccumulator = { project: Project diff --git a/src/shared/project-identity-succession.test.ts b/src/shared/project-identity-succession.test.ts index be9cda1c4ed..a5cad899035 100644 --- a/src/shared/project-identity-succession.test.ts +++ b/src/shared/project-identity-succession.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { carryProjectStateThroughIdentityChange } from './project-identity-succession' -import type { Project } from './types' +import type { Project } from './project-types' const makeProject = (overrides: Partial = {}): Project => ({ id: 'project-1', diff --git a/src/shared/project-identity-succession.ts b/src/shared/project-identity-succession.ts index 815d1b58f6d..8afc9aed99a 100644 --- a/src/shared/project-identity-succession.ts +++ b/src/shared/project-identity-succession.ts @@ -1,4 +1,4 @@ -import type { Project } from './types' +import type { Project } from './project-types' export type ProjectIdentitySuccession = { /** Projected projects with user-set state carried forward from their predecessor row. */ diff --git a/src/shared/provider-check-summary.ts b/src/shared/provider-check-summary.ts index a3fe8813458..d191aa5d123 100644 --- a/src/shared/provider-check-summary.ts +++ b/src/shared/provider-check-summary.ts @@ -1,4 +1,4 @@ -import type { ProviderCheckSummary } from './types' +import type { ProviderCheckSummary } from './github/pull-request-types' export type CheckOutcome = 'passed' | 'failed' | 'pending' | 'neutral' diff --git a/src/shared/remote-server-update.ts b/src/shared/remote-server-update.ts index 7817e8fd9f9..917056996af 100644 --- a/src/shared/remote-server-update.ts +++ b/src/shared/remote-server-update.ts @@ -1,4 +1,4 @@ -import type { UpdateStatus } from './types' +import type { UpdateStatus } from './update-status-types' export const REMOTE_SERVER_UPDATE_CAPABILITY = 'updater.remote-control.v1' as const diff --git a/src/shared/remote-workspace-session-projection.ts b/src/shared/remote-workspace-session-projection.ts index dc9a4019a8b..3b60d9044ff 100644 --- a/src/shared/remote-workspace-session-projection.ts +++ b/src/shared/remote-workspace-session-projection.ts @@ -1,6 +1,7 @@ import { getDefaultWorkspaceSession } from './constants' import type { RemoteWorkspaceSession, RemoteWorkspaceTerminalTab } from './remote-workspace-types' -import type { TerminalTab, WorkspaceSessionState } from './types' +import type { TerminalTab } from './terminal-tab-types' +import type { WorkspaceSessionState } from './workspace-session-state-types' import { splitWorktreeId } from './worktree/id' type ExportOptions = { diff --git a/src/shared/remote-workspace-types.ts b/src/shared/remote-workspace-types.ts index 3c08f272864..509ab83b436 100644 --- a/src/shared/remote-workspace-types.ts +++ b/src/shared/remote-workspace-types.ts @@ -1,4 +1,4 @@ -import type { TerminalLayoutSnapshot, TerminalTab } from './types' +import type { TerminalLayoutSnapshot, TerminalTab } from './terminal-tab-types' export type RemoteWorkspaceTerminalTab = Omit & { worktreePath: string diff --git a/src/shared/renderer-restart-preparation.test.ts b/src/shared/renderer-restart-preparation.test.ts index b755496e0c2..ca690ab3cc6 100644 --- a/src/shared/renderer-restart-preparation.test.ts +++ b/src/shared/renderer-restart-preparation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { UpdateStatus } from './types' +import type { UpdateStatus } from './update-status-types' import { ORCA_RENDERER_SHUTDOWN_CHECKPOINT_FAILED_EVENT } from './renderer-shutdown-events' import { createUpdaterQuitAbortRelay, diff --git a/src/shared/renderer-restart-preparation.ts b/src/shared/renderer-restart-preparation.ts index 20a5311cd6a..356008e0f62 100644 --- a/src/shared/renderer-restart-preparation.ts +++ b/src/shared/renderer-restart-preparation.ts @@ -3,7 +3,7 @@ import { type EditorPrepareHotExitDetail } from './editor-save-events' import { ORCA_RENDERER_SHUTDOWN_CHECKPOINT_FAILED_EVENT } from './renderer-shutdown-events' -import type { UpdateStatus } from './types' +import type { UpdateStatus } from './update-status-types' export type AppRestartPrepOptions = { startedEventName: string diff --git a/src/shared/repo-kind.ts b/src/shared/repo-kind.ts index b5c3946ce98..a9d03a78fad 100644 --- a/src/shared/repo-kind.ts +++ b/src/shared/repo-kind.ts @@ -1,4 +1,4 @@ -import type { Repo } from './types' +import type { Repo } from './repo-types' export function getRepoKind(repo: Pick): 'git' | 'folder' { return repo.kind === 'folder' ? 'folder' : 'git' diff --git a/src/shared/repro-7732-gitlab-job-id-dropped.test.ts b/src/shared/repro-7732-gitlab-job-id-dropped.test.ts index 4f1130f26f2..2f108571dcf 100644 --- a/src/shared/repro-7732-gitlab-job-id-dropped.test.ts +++ b/src/shared/repro-7732-gitlab-job-id-dropped.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { gitLabPipelineJobsToPRChecks } from './gitlab-pipeline-checks' -import type { GitLabPipelineJob } from './types' +import type { GitLabPipelineJob } from './gitlab-types' // Repro for #7732: the Checks side panel can only ask for a GitLab job trace if the mapped // check row still carries the numeric GitLab job id (gitlab:jobTrace takes { jobId }). diff --git a/src/shared/resolved-worktree-lineage.test.ts b/src/shared/resolved-worktree-lineage.test.ts index fc42c1ed608..1c8803d28dd 100644 --- a/src/shared/resolved-worktree-lineage.test.ts +++ b/src/shared/resolved-worktree-lineage.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { join } from 'node:path' -import type { Worktree, WorktreeLineage } from './types' +import type { WorktreeLineage } from './worktree/lineage-types' +import type { Worktree } from './worktree/types' import { projectResolvedWorktreeLineage } from './resolved-worktree-lineage' function worktree(id: string, instanceId: string, overrides: Partial = {}): Worktree { diff --git a/src/shared/resolved-worktree-lineage.ts b/src/shared/resolved-worktree-lineage.ts index 4967d8450b4..f0a50ae2266 100644 --- a/src/shared/resolved-worktree-lineage.ts +++ b/src/shared/resolved-worktree-lineage.ts @@ -1,4 +1,5 @@ -import type { Worktree, WorktreeLineage } from './types' +import type { WorktreeLineage } from './worktree/lineage-types' +import type { Worktree } from './worktree/types' export type WorktreeWithResolvedLineage = T & { parentWorktreeId: string | null diff --git a/src/shared/runtime-client-events.ts b/src/shared/runtime-client-events.ts index 242dd44af24..c849f2f6630 100644 --- a/src/shared/runtime-client-events.ts +++ b/src/shared/runtime-client-events.ts @@ -1,9 +1,9 @@ +import type { CreateWorktreeResult } from './worktree/create-types' import type { - CreateWorktreeResult, WorktreeDefaultTabsLaunch, WorktreeSetupLaunch, WorktreeStartupLaunch -} from './types' +} from './worktree/launch-types' import type { SshConnectionState } from './ssh-types' import type { TerminalSideEffectBatch } from './terminal-side-effect-facts' import type { RuntimeNativeChatLaunchDraftResolution } from './runtime-types' diff --git a/src/shared/runtime-types.ts b/src/shared/runtime-types.ts index 079a416749a..aa5ed57d939 100644 --- a/src/shared/runtime-types.ts +++ b/src/shared/runtime-types.ts @@ -6,26 +6,24 @@ import type { AgentType } from './agent-status-types' import type { - BaseRefSearchResult, - BrowserCookieImportResult, BrowserCertificateFailure, + BrowserCookieImportResult, BrowserLoadError, BrowserSessionProfile, - BrowserSessionProfileSource, - CreateWorktreeResult, - GitWorktreeInfo, - RemoveWorktreeResult, - Repo, - TabGroupLayoutNode, - TerminalColorOverrides, - TerminalLayoutSnapshot, - TuiAgent, - Worktree, - WorktreeLineage, + BrowserSessionProfileSource +} from './browser-workspace-types' +import type { BaseRefSearchResult, Repo } from './repo-types' +import type { TabGroupLayoutNode } from './tab-types' +import type { TerminalColorOverrides } from './terminal-color-overrides' +import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode } from './terminal-tab-types' +import type { TuiAgent } from './tui-agent' +import type { CreateWorktreeResult, RemoveWorktreeResult } from './worktree/create-types' +import type { WorkspaceLineage, - WorktreeLineageWarning, - TerminalPaneLayoutNode -} from './types' + WorktreeLineage, + WorktreeLineageWarning +} from './worktree/lineage-types' +import type { GitWorktreeInfo, Worktree } from './worktree/types' import type { RuntimeMarkdownReadTabResult, RuntimeMarkdownSaveTabResult diff --git a/src/shared/search-match-count.test.ts b/src/shared/search-match-count.test.ts index 93f484c8558..bf86fe56527 100644 --- a/src/shared/search-match-count.test.ts +++ b/src/shared/search-match-count.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { normalizeSearchFileMatchCount, normalizeSearchResult } from './search-match-count' -import type { SearchFileResult } from './types' +import type { SearchFileResult } from './code-search-types' const match = { line: 1, column: 1, matchLength: 3, lineContent: 'foo' } diff --git a/src/shared/search-match-count.ts b/src/shared/search-match-count.ts index 6692eca1a1b..a5378f2b77b 100644 --- a/src/shared/search-match-count.ts +++ b/src/shared/search-match-count.ts @@ -1,4 +1,4 @@ -import type { SearchFileResult, SearchResult } from './types' +import type { SearchFileResult, SearchResult } from './code-search-types' function isValidMatchCount(value: unknown): value is number { return ( diff --git a/src/shared/setup-agent-startup-policy.ts b/src/shared/setup-agent-startup-policy.ts index 4c4fe2485e2..aa77da6eedc 100644 --- a/src/shared/setup-agent-startup-policy.ts +++ b/src/shared/setup-agent-startup-policy.ts @@ -1,4 +1,4 @@ -import type { SetupAgentStartupPolicy } from './types' +import type { SetupAgentStartupPolicy } from './orca-yaml-hook-types' // Why: existing repos should keep launching setup and agents side by side unless // the user explicitly opts into waiting for setup completion. diff --git a/src/shared/skills-cli-agent-keys.ts b/src/shared/skills-cli-agent-keys.ts index c74641f095d..3126c0a81e5 100644 --- a/src/shared/skills-cli-agent-keys.ts +++ b/src/shared/skills-cli-agent-keys.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' /** * The community `skills` CLI's own `--agent` key for each agent Orca detects. diff --git a/src/shared/source-control-ai-action-recipes.test.ts b/src/shared/source-control-ai-action-recipes.test.ts index 0df6fa064e5..aba81519d09 100644 --- a/src/shared/source-control-ai-action-recipes.test.ts +++ b/src/shared/source-control-ai-action-recipes.test.ts @@ -6,7 +6,7 @@ import { resolveSourceControlActionRecipe, resolveSourceControlAiForOperation } from './source-control-ai' -import type { GlobalSettings } from './types' +import type { GlobalSettings } from './global-settings-types' function settings(): GlobalSettings { const base = getDefaultSettings('/tmp') diff --git a/src/shared/source-control-ai-actions.ts b/src/shared/source-control-ai-actions.ts index 19dc70aa652..d857ad4b61b 100644 --- a/src/shared/source-control-ai-actions.ts +++ b/src/shared/source-control-ai-actions.ts @@ -1,6 +1,6 @@ import { isCustomAgentId, type CustomAgentId } from './commit-message-agent-spec' import { isTuiAgent } from './tui-agent-config' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' // Why: the variable registry lives in `./source-control-ai-action-variables` for max-lines // headroom. It is deliberately not re-exported here — one import path per symbol keeps a diff --git a/src/shared/source-control-ai-recipe-save.test.ts b/src/shared/source-control-ai-recipe-save.test.ts index 93d30b43881..b4aeaf355b6 100644 --- a/src/shared/source-control-ai-recipe-save.test.ts +++ b/src/shared/source-control-ai-recipe-save.test.ts @@ -5,7 +5,8 @@ import { saveSourceControlActionRecipe, toSourceControlAiRepoUpdate } from './source-control-ai-recipe-save' -import type { GlobalSettings, Repo } from './types' +import type { GlobalSettings } from './global-settings-types' +import type { Repo } from './repo-types' function settings(): GlobalSettings { const base = getDefaultSettings('/tmp') diff --git a/src/shared/source-control-ai-recipe-save.ts b/src/shared/source-control-ai-recipe-save.ts index 013baee46bd..d000fc399f6 100644 --- a/src/shared/source-control-ai-recipe-save.ts +++ b/src/shared/source-control-ai-recipe-save.ts @@ -16,7 +16,8 @@ import type { SourceControlAiSettings, WritableRepoSourceControlAiOverrides } from './source-control-ai-types' -import type { GlobalSettings, Repo } from './types' +import type { GlobalSettings } from './global-settings-types' +import type { Repo } from './repo-types' export type SourceControlAiWriteTarget = { type: 'repo'; repoId: string } | { type: 'global' } diff --git a/src/shared/source-control-ai-types.ts b/src/shared/source-control-ai-types.ts index 020ec4d16c5..888f80d38a0 100644 --- a/src/shared/source-control-ai-types.ts +++ b/src/shared/source-control-ai-types.ts @@ -1,4 +1,5 @@ -import type { CommitMessageAiModelCapability, TuiAgent } from './types' +import type { CommitMessageAiModelCapability } from './commit-message-ai-types' +import type { TuiAgent } from './tui-agent' import type { CustomAgentId } from './commit-message-agent-spec' import type { SourceControlAiActionDefaults, diff --git a/src/shared/source-control-ai.test.ts b/src/shared/source-control-ai.test.ts index 54a35115b87..d839a77c769 100644 --- a/src/shared/source-control-ai.test.ts +++ b/src/shared/source-control-ai.test.ts @@ -17,7 +17,7 @@ import type { RepoSourceControlAiOverrides, SourceControlAiOperation } from './source-control-ai-types' -import type { GlobalSettings } from './types' +import type { GlobalSettings } from './global-settings-types' function settings(): GlobalSettings { const base = getDefaultSettings('/tmp') diff --git a/src/shared/source-control-ai.ts b/src/shared/source-control-ai.ts index 7445ee5b874..72669000186 100644 --- a/src/shared/source-control-ai.ts +++ b/src/shared/source-control-ai.ts @@ -24,11 +24,11 @@ import { } from './source-control-ai-actions' import type { CommitMessageAiModelCapability, - CommitMessageAiSettings, - GlobalSettings, - Repo, - TuiAgent -} from './types' + CommitMessageAiSettings +} from './commit-message-ai-types' +import type { GlobalSettings } from './global-settings-types' +import type { Repo } from './repo-types' +import type { TuiAgent } from './tui-agent' import type { RepoSourceControlAiOverrides, SourceControlAiModelChoice, diff --git a/src/shared/source-control-commit-failure.ts b/src/shared/source-control-commit-failure.ts index a2f9e0dfd44..29dbecd4dda 100644 --- a/src/shared/source-control-commit-failure.ts +++ b/src/shared/source-control-commit-failure.ts @@ -1,4 +1,4 @@ -import type { GitStatusEntry } from './types' +import type { GitStatusEntry } from './git-status-types' const FALLBACK_COMMIT_FAILURE_SUMMARY = 'Commit failed.' const LINT_COMMIT_FAILURE_SUMMARY = 'Lint failed during commit.' diff --git a/src/shared/source-control-conflict-prompts.ts b/src/shared/source-control-conflict-prompts.ts index a9c717eeaef..e67c3266ba5 100644 --- a/src/shared/source-control-conflict-prompts.ts +++ b/src/shared/source-control-conflict-prompts.ts @@ -1,4 +1,4 @@ -import type { GitConflictKind, GitConflictOperation, GitStatusEntry } from './types' +import type { GitConflictKind, GitConflictOperation, GitStatusEntry } from './git-status-types' export const CONFLICT_KIND_LABELS: Record = { both_modified: 'Both modified', diff --git a/src/shared/source-control-group-order.ts b/src/shared/source-control-group-order.ts index acfe5160ac7..f2856c42598 100644 --- a/src/shared/source-control-group-order.ts +++ b/src/shared/source-control-group-order.ts @@ -1,4 +1,4 @@ -import type { SourceControlGroupOrder } from './types' +import type { SourceControlGroupOrder } from './ui-chrome-types' export const DEFAULT_SOURCE_CONTROL_GROUP_ORDER: SourceControlGroupOrder = 'changes-first' diff --git a/src/shared/source-control-primary-action-decision-types.ts b/src/shared/source-control-primary-action-decision-types.ts index c1e2147b315..e9eec6e91ce 100644 --- a/src/shared/source-control-primary-action-decision-types.ts +++ b/src/shared/source-control-primary-action-decision-types.ts @@ -1,6 +1,6 @@ import type { HostedReviewCreationEligibility } from './hosted-review' import type { GitUpstreamStatus } from './git-status-types' -import type { PRState } from './types' +import type { PRState } from './github/pull-request-types' export type SourceControlPrimaryActionKind = | 'commit' diff --git a/src/shared/source-control-push-failure.ts b/src/shared/source-control-push-failure.ts index 3d26e3ad486..41c5549f9b9 100644 --- a/src/shared/source-control-push-failure.ts +++ b/src/shared/source-control-push-failure.ts @@ -1,4 +1,4 @@ -import type { GitStatusEntry } from './types' +import type { GitStatusEntry } from './git-status-types' const FALLBACK_PUSH_FAILURE_SUMMARY = 'Push failed.' const LINT_PUSH_FAILURE_SUMMARY = 'Lint failed during push.' diff --git a/src/shared/status-bar-defaults.ts b/src/shared/status-bar-defaults.ts index b1884262d7d..73d72a70ab2 100644 --- a/src/shared/status-bar-defaults.ts +++ b/src/shared/status-bar-defaults.ts @@ -1,4 +1,4 @@ -import type { StatusBarItem } from './types' +import type { StatusBarItem } from './ui-chrome-types' export const DEFAULT_STATUS_BAR_ITEMS: StatusBarItem[] = [ 'claude', diff --git a/src/shared/tab-title-resolution.ts b/src/shared/tab-title-resolution.ts index 9eea2b62371..ffcab13925d 100644 --- a/src/shared/tab-title-resolution.ts +++ b/src/shared/tab-title-resolution.ts @@ -1,4 +1,5 @@ -import type { Tab, TerminalTab } from './types' +import type { Tab } from './tab-types' +import type { TerminalTab } from './terminal-tab-types' import { isMeaningfulOpenCodeTerminalTitle } from './opencode-terminal-title' export function resolveTerminalTabTitle( diff --git a/src/shared/task-preset-query.ts b/src/shared/task-preset-query.ts index ba89a3b131f..66830c860f1 100644 --- a/src/shared/task-preset-query.ts +++ b/src/shared/task-preset-query.ts @@ -1,4 +1,4 @@ -import type { TaskViewPresetId } from './types' +import type { TaskViewPresetId } from './ui-chrome-types' /** * Why: TaskPage presets, blank-search fallbacks, and openTaskPage prefetch must diff --git a/src/shared/task-provider-identity.ts b/src/shared/task-provider-identity.ts index 5d9dbe8c390..46f81229aa2 100644 --- a/src/shared/task-provider-identity.ts +++ b/src/shared/task-provider-identity.ts @@ -1,6 +1,6 @@ import { githubRepoIdentityKey } from './github/repository-identity-key' import type { TaskProvider } from './task-providers' -import type { ProjectProviderIdentity } from './types' +import type { ProjectProviderIdentity } from './project-types' export type GitHubTaskProviderIdentity = ProjectProviderIdentity & { provider: 'github' diff --git a/src/shared/task-source-context.ts b/src/shared/task-source-context.ts index e1576e2de92..78f5ec9e8cf 100644 --- a/src/shared/task-source-context.ts +++ b/src/shared/task-source-context.ts @@ -14,7 +14,8 @@ import { type TaskProviderIdentity } from './task-provider-identity' import type { TaskProvider } from './task-providers' -import type { GlobalSettings, Repo } from './types' +import type { GlobalSettings } from './global-settings-types' +import type { Repo } from './repo-types' export type { GitHubTaskProviderIdentity, diff --git a/src/shared/telemetry-events.ts b/src/shared/telemetry-events.ts index bbffedaefe0..1d6905c1f8d 100644 --- a/src/shared/telemetry-events.ts +++ b/src/shared/telemetry-events.ts @@ -56,13 +56,9 @@ import { } from './nested-repo-telemetry' import { AGENT_HOOK_TARGETS } from './agent-hook-types' -import type { - DiscoveryStatusEmitted, - GlobalSettings, - OnboardingChecklistState, - PathSource, - ShellHydrationFailureReason -} from './types' +import type { GlobalSettings } from './global-settings-types' +import type { DiscoveryStatusEmitted, OnboardingChecklistState } from './onboarding-state-types' +import type { PathSource, ShellHydrationFailureReason } from './shell-path-hydration-types' // ── Shared property enums ─────────────────────────────────────────────── diff --git a/src/shared/terminal-color-scheme-protocol.ts b/src/shared/terminal-color-scheme-protocol.ts index 1b6d680c2a1..76ac88c62cc 100644 --- a/src/shared/terminal-color-scheme-protocol.ts +++ b/src/shared/terminal-color-scheme-protocol.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from './types' +import type { GlobalSettings } from './global-settings-types' export type TerminalColorSchemeMode = 'dark' | 'light' diff --git a/src/shared/terminal-cursor-style-settings.ts b/src/shared/terminal-cursor-style-settings.ts index ec271a7b972..3d8613b9b80 100644 --- a/src/shared/terminal-cursor-style-settings.ts +++ b/src/shared/terminal-cursor-style-settings.ts @@ -1,4 +1,4 @@ -import type { GlobalSettings } from './types' +import type { GlobalSettings } from './global-settings-types' type TerminalCursorStyleSettings = Pick< GlobalSettings, diff --git a/src/shared/terminal-custom-themes.ts b/src/shared/terminal-custom-themes.ts index f91b7d8e22e..c1c3fdcfc8f 100644 --- a/src/shared/terminal-custom-themes.ts +++ b/src/shared/terminal-custom-themes.ts @@ -1,4 +1,4 @@ -import type { TerminalColorOverrides } from './types' +import type { TerminalColorOverrides } from './terminal-color-overrides' import { HEX_COLOR_RE } from './color-validation' export type TerminalCustomThemeSource = 'warp' | 'ghostty' | 'manual' diff --git a/src/shared/terminal-quick-commands.ts b/src/shared/terminal-quick-commands.ts index cee9cdc9fa9..28d51bc0323 100644 --- a/src/shared/terminal-quick-commands.ts +++ b/src/shared/terminal-quick-commands.ts @@ -5,7 +5,7 @@ import type { TerminalQuickCommand, TerminalQuickCommandAction, TerminalQuickCommandScope -} from './types' +} from './terminal-quick-command-types' export const MAX_QUICK_COMMANDS = 40 export const MAX_QUICK_COMMAND_ID_LENGTH = 80 diff --git a/src/shared/terminal-title-agent-type.ts b/src/shared/terminal-title-agent-type.ts index 0952d96e63f..c679933c88e 100644 --- a/src/shared/terminal-title-agent-type.ts +++ b/src/shared/terminal-title-agent-type.ts @@ -12,7 +12,7 @@ import { getPiCompatibleSyntheticAgentLabel, isLegacyPiCompatibleTitle } from './pi-compatible-synthetic-title' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' export const CLAUDE_IDLE = '\u2733' // ✳ (eight-spoked asterisk — Claude Code idle prefix) const CLAUDE_MANAGEMENT_TITLE_RE = diff --git a/src/shared/text-search.ts b/src/shared/text-search.ts index 780164372cb..7919e50cb47 100644 --- a/src/shared/text-search.ts +++ b/src/shared/text-search.ts @@ -14,7 +14,12 @@ import { posix, win32 } from 'node:path' import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit' import { normalizeSearchResult } from './search-match-count' import { escapeRegex } from './string-utils' -import type { SearchFileResult, SearchMatch, SearchOptions, SearchResult } from './types' +import type { + SearchFileResult, + SearchMatch, + SearchOptions, + SearchResult +} from './code-search-types' export type SearchAccumulator = { fileMap: Map diff --git a/src/shared/top-level-view.ts b/src/shared/top-level-view.ts index 8f0e2ee99c1..479c161130d 100644 --- a/src/shared/top-level-view.ts +++ b/src/shared/top-level-view.ts @@ -1,4 +1,4 @@ -import type { TopLevelView } from './types' +import type { TopLevelView } from './ui-chrome-types' // Record keys are exhaustive so adding a top-level view also updates every // persistence boundary that validates values loaded from disk or IPC. diff --git a/src/shared/tui-agent-config.ts b/src/shared/tui-agent-config.ts index 3437e44e560..ea7cb092c68 100644 --- a/src/shared/tui-agent-config.ts +++ b/src/shared/tui-agent-config.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' import { getOrcaCliCommandNameForPlatform } from './orca-cli-command-name' export type AgentPromptInjectionMode = diff --git a/src/shared/tui-agent-detection-commands.ts b/src/shared/tui-agent-detection-commands.ts index b0ca1b443b8..81ee6589649 100644 --- a/src/shared/tui-agent-detection-commands.ts +++ b/src/shared/tui-agent-detection-commands.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' import { getTuiAgentDetectCommands, TUI_AGENT_CONFIG, diff --git a/src/shared/tui-agent-display-names.ts b/src/shared/tui-agent-display-names.ts index 618ef034a68..0b2078f0479 100644 --- a/src/shared/tui-agent-display-names.ts +++ b/src/shared/tui-agent-display-names.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' /** Why: plain-English agent names for non-localized surfaces (keybinding * titles in the shared registry, which main, renderer, and the keybindings diff --git a/src/shared/tui-agent-launch-command.ts b/src/shared/tui-agent-launch-command.ts index c45ac67bd19..140ef935626 100644 --- a/src/shared/tui-agent-launch-command.ts +++ b/src/shared/tui-agent-launch-command.ts @@ -10,7 +10,7 @@ import { tokenizeStartupCommand, type AgentStartupShell } from './tui-agent-startup-shell' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' export type ResolvedAgentLaunchCommand = | { diff --git a/src/shared/tui-agent-launch-defaults.ts b/src/shared/tui-agent-launch-defaults.ts index f0f5d3e8bd1..6e0dc16f08d 100644 --- a/src/shared/tui-agent-launch-defaults.ts +++ b/src/shared/tui-agent-launch-defaults.ts @@ -1,6 +1,6 @@ import { isTuiAgent } from './tui-agent-config' import { YOLO_TUI_AGENT_ARGS, YOLO_TUI_AGENT_ENV } from './tui-agent-permissions' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' const UNSUPPORTED_TUI_AGENT_ARGS: Partial> = { opencode: ['--dangerously-skip-permissions'], diff --git a/src/shared/tui-agent-permissions.ts b/src/shared/tui-agent-permissions.ts index ae0c34391a2..596ef7a1a60 100644 --- a/src/shared/tui-agent-permissions.ts +++ b/src/shared/tui-agent-permissions.ts @@ -1,5 +1,5 @@ import { TUI_AGENT_CONFIG } from './tui-agent-config' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' export type AgentPermissionMode = 'yolo' | 'manual' | 'mixed' diff --git a/src/shared/tui-agent-selection.ts b/src/shared/tui-agent-selection.ts index e0c93daf47c..86216e589b8 100644 --- a/src/shared/tui-agent-selection.ts +++ b/src/shared/tui-agent-selection.ts @@ -1,4 +1,4 @@ -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' import { isTuiAgent } from './tui-agent-config' // Keep this order in sync with the desktop agent catalog. It defines the diff --git a/src/shared/tui-agent-startup.ts b/src/shared/tui-agent-startup.ts index 4cef246ccdb..7b013c4ffd7 100644 --- a/src/shared/tui-agent-startup.ts +++ b/src/shared/tui-agent-startup.ts @@ -17,7 +17,7 @@ import type { StartupCommandDelivery } from './codex-startup-delivery' import { buildSleepingAgentLaunchConfig } from './sleeping-agent-launch-config' import { planHermesStartupQuery } from './hermes-startup-query' import { inlineAgentDraftFitsPlatform } from './agent-draft-platform-limit' -import type { TuiAgent } from './types' +import type { TuiAgent } from './tui-agent' import type { SessionOptionValue } from './native-chat-session-options' import { resolveAgentLaunchCommand } from './tui-agent-launch-command' import { buildAgentResumeLaunchCommand } from './agent-resume-launch-command' diff --git a/src/shared/types.ts b/src/shared/types.ts deleted file mode 100644 index b08e93a3be8..00000000000 --- a/src/shared/types.ts +++ /dev/null @@ -1,366 +0,0 @@ -/** - * Public barrel for Orca's shared domain types. Declarations live in the - * per-domain modules re-exported below; keep this file re-exports only. - */ - -// Repos, projects, and project groups - -export type { RepoKind, IssueSourcePreference, ExternalWorktreeVisibility } from './repo-types' -export type { BuiltInWorktreeVisibilitySourceId } from './repo-types' -export type { CustomWorktreeVisibilitySource } from './repo-types' -export type { WorktreeVisibilitySourcePreferences, Repo, BaseRefDefaultResult } from './repo-types' -export type { BaseRefSearchResult } from './repo-types' - -export type { ForkSyncMode, GitForkSyncExpectedUpstream, GitForkSyncResult } from './git-fork-sync' - -export type { ProjectProviderIdentity, Project, ProjectUpdateArgs } from './project-types' -export type { ProjectHostSetupState, ProjectHostSetupMethod } from './project-types' -export type { RepoProjectHostSetupMethod, ProjectHostSetup } from './project-types' -export type { ProjectHostSetupExistingFolderArgs } from './project-types' -export type { ProjectHostSetupCreateArgs, ProjectHostSetupCloneArgs } from './project-types' -export type { ProjectHostSetupUpdateArgs, ProjectHostSetupDeleteArgs } from './project-types' -export type { ProjectHostSetupResult, ProjectHostSetupCreateResult } from './project-types' -export type { ProjectHostSetupUpdateResult, ProjectHostSetupDeleteResult } from './project-types' - -export type { ProjectGroupCreatedFrom, ProjectGroup } from './project-group-types' -export type { NestedRepoScanOptions, NestedRepoCandidate } from './project-group-types' -export type { NestedRepoScanResult, ProjectGroupImportMode } from './project-group-types' -export type { ProjectGroupImportProjectResult } from './project-group-types' -export type { ProjectGroupImportResult } from './project-group-types' - -export type { WorkspaceScope, WorkspaceKey, FolderWorkspace } from './folder-workspace-types' -export type { FolderWorkspaceLinkedTask } from './folder-workspace-types' - -// Worktrees and workspace session state - -export type { GitWorktreeInfo, WorktreeHeadIdentity, WorkspaceStatus } from './worktree/types' -export type { WorkspaceStatusDefinition, Worktree, WorkspaceLinkedItem } from './worktree/types' -export type { CliWorkspaceProvenance, WorkspaceCreatorProvenance } from './worktree/types' -export type { AutomationWorkspaceProvenance } from './worktree/types' -export type { AutomationWorkspaceProvenanceRequest, GitPushTarget } from './worktree/types' -export type { GitHubPrStartPoint, WorktreeOwnership } from './worktree/types' -export type { DetectedWorktreeListSource, DetectedWorktree } from './worktree/types' -export type { DetectedWorktreeListResult } from './worktree/types' - -export type { WorktreeMeta } from './worktree/meta-types' - -export type { WorktreeLineageOrigin } from './worktree/lineage-types' -export type { WorktreeLineageCaptureConfidence } from './worktree/lineage-types' -export type { WorktreeLineageCaptureSource, WorktreeLineageCapture } from './worktree/lineage-types' -export type { WorktreeLineage, WorkspaceLineage } from './worktree/lineage-types' -export type { WorktreeLineageWarningCode, WorktreeLineageWarning } from './worktree/lineage-types' - -export type { WorktreeSetupLaunch, WorktreeStartupLaunch } from './worktree/launch-types' -export type { WorktreeDefaultTabsLaunch, SetupScriptLaunchMode } from './worktree/launch-types' -export type { SetupSplitDirection } from './worktree/launch-types' - -export type { SetupDecision, WorktreeCreateTimingPhase } from './worktree/create-types' -export type { WorktreeCreateTiming, CreateSparseCheckoutRequest } from './worktree/create-types' -export type { SparsePreset, CreateWorktreeArgs } from './worktree/create-types' -export type { AdoptProvisionedRootArgs, CreateWorktreeResult } from './worktree/create-types' -export type { WorktreeCreateBaseFallback, PreservedWorktreeBranch } from './worktree/create-types' -export type { RemoveWorktreeResult, ForceDeleteWorktreeBranchResult } from './worktree/create-types' - -export type { LocalBaseRefRefreshResult } from './worktree/base-ref-drift-types' -export type { LocalBaseRefUpdateSuggestion } from './worktree/base-ref-drift-types' -export type { WorktreeBaseStatusKind } from './worktree/base-ref-drift-types' -export type { WorktreeBaseStatusEvent } from './worktree/base-ref-drift-types' -export type { WorktreeRemoteBranchConflictEvent } from './worktree/base-ref-drift-types' - -export type { PersistedOpenFile, WorkspaceSessionState } from './workspace-session-state-types' -export type { WorkspaceSessionPatch } from './workspace-session-state-types' - -// Re-exported for backward compat with renderer call sites that import -// `WorkspaceCreateTelemetrySource` from '../../../shared/types'. -export type { WorkspaceSource as WorkspaceCreateTelemetrySource } from './workspace-source' - -// Tabs, terminals, and the embedded browser - -export type { TabGroupSplitDirection, TabGroupLayoutNode, TabContentType } from './tab-types' -export type { WorkspaceVisibleTabType, CtrlTabOrderMode, Tab, TabGroup } from './tab-types' - -export type { TerminalTab, TerminalPaneSplitDirection } from './terminal-tab-types' -export type { TerminalPaneLayoutNode, TerminalLayoutSnapshot } from './terminal-tab-types' - -export type { TerminalColorOverrides } from './terminal-color-overrides' - -export type { TerminalQuickCommandScope } from './terminal-quick-command-types' -export type { TerminalQuickCommandAction } from './terminal-quick-command-types' -export type { TerminalQuickCommandBase } from './terminal-quick-command-types' -export type { TerminalCommandQuickCommand } from './terminal-quick-command-types' -export type { TerminalAgentQuickCommand } from './terminal-quick-command-types' -export type { TerminalQuickCommand } from './terminal-quick-command-types' - -export type { BrowserHistoryEntry, BrowserLoadError } from './browser-workspace-types' -export type { BrowserCertificateFailure } from './browser-workspace-types' -export type { BrowserCertificateProceedFailureReason } from './browser-workspace-types' -export type { BrowserCertificateProceedResult } from './browser-workspace-types' -export type { BrowserViewportPresetId, BrowserViewportOverride } from './browser-workspace-types' -export type { BrowserPage, BrowserWorkspace, BrowserTab } from './browser-workspace-types' -export type { BrowserSessionProfileScope } from './browser-workspace-types' -export type { BrowserSessionUserAgentMode } from './browser-workspace-types' -export type { BrowserSessionProfileCreateOptions } from './browser-workspace-types' -export type { BrowserSessionProfileSource, BrowserSessionProfile } from './browser-workspace-types' -export type { BrowserCookieImportSummary } from './browser-workspace-types' -export type { BrowserCookieImportResult } from './browser-workspace-types' - -// Git status, diffs, and review comments - -export type { - GitBranchChangeStatus, - GitConflictKind, - GitConflictOperation, - GitConflictResolutionStatus, - GitConflictStatusSource, - GitFileStatus, - GitStagingArea, - GitStatusEntry, - GitStatusResult, - GitSubmoduleStatus, - GitUncommittedEntry, - GitUpstreamStatus -} from './git-status-types' - -export type { GitBranchChangeEntry, GitBranchCompareSummary } from './git-diff-compare-types' -export type { GitBranchCompareResult, GitCommitCompareSummary } from './git-diff-compare-types' -export type { GitCommitCompareResult, GitDiffTextResult } from './git-diff-compare-types' -export type { GitDiffBinaryResult, GitDiffResult } from './git-diff-compare-types' - -export type { DiffCommentSource, DiffReviewScope } from './diff-comment-types' -export type { MobileDiffReviewFileState, MobileDiffReviewState } from './diff-comment-types' -export type { DiffComment } from './diff-comment-types' - -// GitHub - -export type { - PRState, - IssueState, - CheckStatus, - PRMergeableState -} from './github/pull-request-types' -export type { PRReviewDecision, PRConflictSummary } from './github/pull-request-types' -export type { GitHubRepositoryIdentity, GitHubOwnerRepo } from './github/pull-request-types' -export type { GitHubPRMergeMethod, GitHubPRMergeMethodSettings } from './github/pull-request-types' -export type { - GitHubPRStackEntry, - GitHubPRStack, - PRInfo, - IssueInfo -} from './github/pull-request-types' -export type { - GitHubViewer, - GitHubAssignableUser, - ProviderCheckSummary -} from './github/pull-request-types' -export type { GitHubPRReviewSummary, GitHubPRFileViewedState } from './github/pull-request-types' -export type { GitHubPRFile, GitHubPRFileContents } from './github/pull-request-types' - -export type { - PRRefreshErrorType, - PRRefreshUpstreamErrorType -} from './github/pull-request-refresh-types' -export type { PRRefreshOutcome, GitHubPRRefreshReason } from './github/pull-request-refresh-types' -export type { - GitHubPRRefreshEnqueueResult, - GitHubPRRefreshAlias -} from './github/pull-request-refresh-types' -export type { GitHubPRRefreshCandidate } from './github/pull-request-refresh-types' -export type { - GitHubPRRefreshSkippedReason, - GitHubPRRefreshEvent -} from './github/pull-request-refresh-types' - -export type { PRCheckDetail, PRCheckAnnotation, PRCheckStep } from './github/check-types' -export type { PRCheckJob, PRCheckRunDetails, GitHubRerunPRChecksResult } from './github/check-types' - -export type { GitHubReactionContent, GitHubReaction, PRComment } from './github/comment-types' -export type { GitHubCommentResult, GitHubPRReviewCommentInput } from './github/comment-types' -export type { GitHubIssueTimelineTarget, GitHubIssueTimelineItem } from './github/comment-types' - -export type { GitHubWorkItem, GitHubWorkItemDetails } from './github/work-item-types' -export type { ListWorkItemsResult } from './github/work-item-types' - -export type { GitHubRateLimitBucket, GitHubRateLimitSnapshot } from './github/rate-limit-types' -export type { GetRateLimitResult } from './github/rate-limit-types' - -// GitLab - -export type { - GitLabAssignableUser, - GitLabAuthDiagnostic, - GitLabCommentResult, - GitLabDiscussionResolveResult, - GitLabIssueInfo, - GitLabIssueState, - GitLabIssueUpdate, - GitLabJobTraceResult, - GitLabRateLimitBucket, - GitLabRateLimitSnapshot, - GitLabMRApprovalRule, - GitLabMRApprovalState, - GitLabMRFile, - GitLabMRInlineCommentInput, - GitLabMRReviewersUpdateResult, - GitLabMRUpdate, - GitLabPagedResult, - GitLabPipelineJob, - GitLabProjectRef, - GitLabProjectSettings, - GitLabRetryJobResult, - GitLabReaction, - GitLabTodo, - GitLabTodoTargetType, - GitLabViewer, - GitLabWorkItem, - GitLabWorkItemDetails, - GetGitLabRateLimitResult, - ListMergeRequestsResult, - MRCheckDetail, - MRComment, - MRInfo, - MRListState, - MRMergeableState, - MRState -} from './gitlab-types' - -// Jira - -export type { - JiraAuthType, - JiraComment, - JiraConnectArgs, - JiraConnectionStatus, - JiraCreateField, - JiraCreateFieldAllowedValue, - JiraCreateIssueArgs, - JiraCreateIssueResult, - JiraIssue, - JiraIssueFilter, - JiraIssueType, - JiraIssueUpdate, - JiraMutationResult, - JiraPriority, - JiraProject, - JiraProjectStatusOrder, - JiraSite, - JiraSiteSelection, - JiraStatus, - JiraTransition, - JiraUser, - JiraViewer -} from './jira-types' - -// Linear - -export type { LinearViewer, LinearWorkspace } from './linear/workspace-types' -export type { LinearWorkspaceSelection, LinearWorkspaceSelector } from './linear/workspace-types' -export type { LinearConcreteWorkspaceId, LinearWorkspaceError } from './linear/workspace-types' -export type { LinearCollectionResult, LinearConnectionStatus } from './linear/workspace-types' -export type { LinearWorkflowState, LinearLabel, LinearMember } from './linear/workspace-types' -export type { LinearTeam } from './linear/workspace-types' - -export type { LinearIssue, LinearIssueChildSummary, LinearComment } from './linear/issue-types' - -export type { LinearProjectSummary, LinearProjectStatusSummary } from './linear/project-types' -export type { LinearProjectMemberSummary } from './linear/project-types' -export type { LinearProjectMilestoneSummary } from './linear/project-types' -export type { LinearProjectResourceSummary } from './linear/project-types' -export type { LinearProjectUpdateSummary, LinearProjectDetail } from './linear/project-types' -export type { LinearCustomViewModel, LinearCustomViewSummary } from './linear/project-types' - -// Cross-provider issue mutations and task sources - -export type { GitHubCreateIssueFields, GitHubCreateIssueResult } from './issue-mutation-types' -export type { GitHubIssueCloseReason, GitHubIssueUpdate } from './issue-mutation-types' -export type { GitHubPullRequestStateUpdate, LinearIssueUpdate } from './issue-mutation-types' - -export type { TaskProvider } from './task-providers' - -// orca.yaml hooks, setup policy, and VM recipes - -export type { OrcaHooks, OrcaWorktreeDefaults } from './orca-yaml-hook-types' -export type { OrcaDefaultTabTemplate, EphemeralVmCheckoutMode } from './orca-yaml-hook-types' -export type { OrcaVmRecipe, OrcaVmRecipeDiagnostic, RepoHookSettings } from './orca-yaml-hook-types' -export type { SetupRunPolicy, SetupAgentStartupPolicy } from './orca-yaml-hook-types' -export type { HookCommandSourcePolicy, PersistedTrustedOrcaHookEntry } from './orca-yaml-hook-types' -export type { PersistedTrustedOrcaHookRepo } from './orca-yaml-hook-types' -export type { PersistedTrustedOrcaHooks } from './orca-yaml-hook-types' - -// Settings, persisted UI state, and onboarding - -export type { TaskViewPresetId, OpenInApplication, SourceControlViewMode } from './ui-chrome-types' -export type { SourceControlGroupOrder, LeftSidebarAppearanceMode } from './ui-chrome-types' -export type { BranchPrefixStrategy, FloatingTerminalCwdRequest } from './ui-chrome-types' -export type { AgentDashboardMode, WorktreeCardProperty, WorktreeCardMode } from './ui-chrome-types' -export type { AgentActivityDisplayMode, StatusBarItem } from './ui-chrome-types' -export type { FloatingTerminalTriggerLocation, TaskResumeState } from './ui-chrome-types' -export type { RightSidebarTab, ActiveRightSidebarTab } from './ui-chrome-types' -export type { RightSidebarExplorerView, ProjectOrderBy } from './ui-chrome-types' -export type { WorkspaceHostScope, VisibleWorkspaceHostIds } from './ui-chrome-types' -export type { WorkspaceHostOrder, ManualRepoOrderEntry, TopLevelView } from './ui-chrome-types' - -export type { GlobalSettings, OrcaWorkspaceLayout } from './global-settings-types' -export type { GhosttyImportPreview } from './global-settings-types' - -export type { HostSettingOverrides } from './host-setting-overrides' - -export type { CommitMessageAiModelCapability } from './commit-message-ai-types' -export type { CommitMessageAiSettings } from './commit-message-ai-types' - -export type { NotificationSettings, NotificationEventSource } from './notification-settings-types' -export type { NotificationDispatchRequest } from './notification-settings-types' -export type { NotificationDispatchResult } from './notification-settings-types' -export type { NotificationDismissResult } from './notification-settings-types' -export type { NotificationSoundResult } from './notification-settings-types' -export type { NotificationSoundDataResult } from './notification-settings-types' -export type { NotificationSoundPathResult } from './notification-settings-types' -export type { NotificationPermissionStatusResult } from './notification-settings-types' -export type { NotificationDeliveryProbeResult } from './notification-settings-types' - -export type { DiscoveryStatusEmitted, OnboardingOutcome } from './onboarding-state-types' -export type { OnboardingChecklistState, OnboardingState } from './onboarding-state-types' - -export type { PersistedUIState } from './persisted-ui-state-types' - -export type { LegacyPaneKeyAliasEntry } from './persisted-state-types' -export type { PersistedMobileClientTabSelection } from './persisted-state-types' -export type { PersistedMobileClientTabSelections, PersistedState } from './persisted-state-types' - -export { PET_SIZE_MIN, PET_SIZE_MAX, PET_SIZE_DEFAULT } from './pet-types' -export type { CustomPet, SpriteAnimation } from './pet-types' - -// Agents, managed accounts, and updates - -export type { TuiAgent } from './tui-agent' - -export type { CodexManagedAccount, CodexManagedAccountSummary } from './managed-account-types' -export type { CodexSystemDefaultIdentity } from './managed-account-types' -export type { CodexRateLimitAccountsState } from './managed-account-types' -export type { CodexManagedAccountRuntimeSelection } from './managed-account-types' -export type { ClaudeManagedAccount, ClaudeManagedAccountSummary } from './managed-account-types' -export type { ClaudeRateLimitAccountsState } from './managed-account-types' -export type { ClaudeManagedAccountRuntimeSelection } from './managed-account-types' - -export type { ChangelogRelease, ChangelogData, UpdateCheckOptions } from './update-status-types' -export type { UpdateSource, LinuxRootPackageType } from './update-status-types' -export type { LinuxPackageInstallFailureReason } from './update-status-types' -export type { LinuxPackageInstallRecovery } from './update-status-types' -export type { LinuxPackageCommandUnavailableReason } from './update-status-types' -export type { LinuxPackageInstallInstructions, UpdateStatus } from './update-status-types' -export type { ReleaseBuildListResult } from './update-status-types' - -// Filesystem, search, process stats, and shared error shape - -export type { ShellHydrationFailureReason, PathSource } from './shell-path-hydration-types' - -export type { FilesystemPathFlavor, DirEntry, MarkdownDocument } from './filesystem-entry-types' -export type { FsChangeEvent, FsChangedPayload } from './filesystem-entry-types' - -export type { SearchMatch, SearchFileResult, SearchResult } from './code-search-types' -export type { SearchOptions } from './code-search-types' - -export type { StatsSummary, UsageValues, ProcessMemoryMetric } from './process-stats-types' -export type { HostAvailableMemorySource, AppMemory, SessionMemory } from './process-stats-types' -export type { WorktreeMemory, HostMemory, MemorySnapshot } from './process-stats-types' - -export type { ClassifiedError } from './classified-error' diff --git a/src/shared/workspace-creator-provenance.ts b/src/shared/workspace-creator-provenance.ts index 541cf3f2a5e..7fe2802f607 100644 --- a/src/shared/workspace-creator-provenance.ts +++ b/src/shared/workspace-creator-provenance.ts @@ -1,4 +1,4 @@ -import type { WorkspaceCreatorProvenance } from './types' +import type { WorkspaceCreatorProvenance } from './worktree/types' export function normalizeWorkspaceCreatorProvenance( value: unknown diff --git a/src/shared/workspace-linked-item-equality.test.ts b/src/shared/workspace-linked-item-equality.test.ts index 45ff9800093..dba84854e98 100644 --- a/src/shared/workspace-linked-item-equality.test.ts +++ b/src/shared/workspace-linked-item-equality.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { areWorkspaceLinkedItemsEqual } from './workspace-linked-item' -import type { WorkspaceLinkedItem } from './types' +import type { WorkspaceLinkedItem } from './worktree/types' const item: WorkspaceLinkedItem = { provider: 'jira', diff --git a/src/shared/workspace-linked-item-schema.ts b/src/shared/workspace-linked-item-schema.ts index 5e50cc3c40a..d1b648fa58b 100644 --- a/src/shared/workspace-linked-item-schema.ts +++ b/src/shared/workspace-linked-item-schema.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import type { WorkspaceLinkedItem } from './types' +import type { WorkspaceLinkedItem } from './worktree/types' import { normalizeWorkspaceLinkedItem } from './workspace-linked-item' export const WorkspaceLinkedItemSchema = z diff --git a/src/shared/workspace-linked-item-source-context.test.ts b/src/shared/workspace-linked-item-source-context.test.ts index 41288b956d1..aaff22d8155 100644 --- a/src/shared/workspace-linked-item-source-context.test.ts +++ b/src/shared/workspace-linked-item-source-context.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { TaskSourceContext } from './task-source-context' -import type { WorkspaceLinkedItem } from './types' +import type { WorkspaceLinkedItem } from './worktree/types' import { isWorkspaceLinkedItemSourceContextMatch } from './workspace-linked-item-source-context' const JIRA_ITEM: WorkspaceLinkedItem = { diff --git a/src/shared/workspace-linked-item-source-context.ts b/src/shared/workspace-linked-item-source-context.ts index 81d29c4be05..7b4ecd4b60d 100644 --- a/src/shared/workspace-linked-item-source-context.ts +++ b/src/shared/workspace-linked-item-source-context.ts @@ -1,7 +1,7 @@ import { parseJiraIssueUrl } from './jira-issue-url' import { getWorkspaceSourceProvider } from './new-workspace/workspace-source' import type { TaskSourceContext } from './task-source-context' -import type { WorkspaceLinkedItem } from './types' +import type { WorkspaceLinkedItem } from './worktree/types' function resolveLinkedItemProvider( item: Pick & Partial diff --git a/src/shared/workspace-linked-item.ts b/src/shared/workspace-linked-item.ts index bca3929d09b..11f60f1864f 100644 --- a/src/shared/workspace-linked-item.ts +++ b/src/shared/workspace-linked-item.ts @@ -1,4 +1,4 @@ -import type { WorkspaceLinkedItem } from './types' +import type { WorkspaceLinkedItem } from './worktree/types' export function areWorkspaceLinkedItemsEqual( a: WorkspaceLinkedItem | null | undefined, diff --git a/src/shared/workspace-scope.ts b/src/shared/workspace-scope.ts index 4993c0599d7..262f6a82afe 100644 --- a/src/shared/workspace-scope.ts +++ b/src/shared/workspace-scope.ts @@ -1,4 +1,4 @@ -import type { WorkspaceKey, WorkspaceScope } from './types' +import type { WorkspaceKey, WorkspaceScope } from './folder-workspace-types' export function worktreeWorkspaceKey(worktreeId: string): WorkspaceKey { return `worktree:${worktreeId}` diff --git a/src/shared/workspace-session-browser-history.ts b/src/shared/workspace-session-browser-history.ts index 666671a0d04..ad734c17471 100644 --- a/src/shared/workspace-session-browser-history.ts +++ b/src/shared/workspace-session-browser-history.ts @@ -1,4 +1,5 @@ -import type { BrowserHistoryEntry, WorkspaceSessionState } from './types' +import type { BrowserHistoryEntry } from './browser-workspace-types' +import type { WorkspaceSessionState } from './workspace-session-state-types' import { redactKagiSessionToken } from './browser-url' export const MAX_BROWSER_HISTORY_ENTRIES = 200 diff --git a/src/shared/workspace-session-browser-schema.ts b/src/shared/workspace-session-browser-schema.ts index 19aaeb0ae82..0feb744faf4 100644 --- a/src/shared/workspace-session-browser-schema.ts +++ b/src/shared/workspace-session-browser-schema.ts @@ -3,7 +3,7 @@ * schemas themselves are unchanged apart from the per-entry tolerance the * session schema now declares everywhere. */ import { z } from 'zod' -import type { BrowserWorkspace } from './types' +import type { BrowserWorkspace } from './browser-workspace-types' import { normalizeBrowserHistoryEntries } from './workspace-session-browser-history' import { salvagingArray } from './zod-salvage' diff --git a/src/shared/workspace-session-salvage.ts b/src/shared/workspace-session-salvage.ts index 66b6a6374e9..4f3bdf88fa2 100644 --- a/src/shared/workspace-session-salvage.ts +++ b/src/shared/workspace-session-salvage.ts @@ -1,4 +1,4 @@ -import type { WorkspaceSessionState } from './types' +import type { WorkspaceSessionState } from './workspace-session-state-types' import { describeWorkspaceSessionError, safeParseWorkspaceSession, diff --git a/src/shared/workspace-session-schema.ts b/src/shared/workspace-session-schema.ts index 446e19270da..9f65e2a1034 100644 --- a/src/shared/workspace-session-schema.ts +++ b/src/shared/workspace-session-schema.ts @@ -12,13 +12,11 @@ * Only a payload that is not a session at all falls back to defaults. */ import { z } from 'zod' -import type { - TabGroupLayoutNode, - TerminalPaneLayoutNode, - TuiAgent, - WorkspaceKey, - WorkspaceSessionState -} from './types' +import type { WorkspaceKey } from './folder-workspace-types' +import type { TabGroupLayoutNode } from './tab-types' +import type { TerminalPaneLayoutNode } from './terminal-tab-types' +import type { TuiAgent } from './tui-agent' +import type { WorkspaceSessionState } from './workspace-session-state-types' import { isValidTerminalTabId } from './terminal-tab-id' import { parseExecutionHostId, type ExecutionHostId } from './execution-host' import { isTuiAgent } from './tui-agent-config' diff --git a/src/shared/workspace-session-terminal-buffers.test.ts b/src/shared/workspace-session-terminal-buffers.test.ts index e5c868cd691..d1316659440 100644 --- a/src/shared/workspace-session-terminal-buffers.test.ts +++ b/src/shared/workspace-session-terminal-buffers.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { FLOATING_TERMINAL_WORKTREE_ID } from './constants' -import type { WorkspaceSessionState } from './types' +import type { WorkspaceSessionState } from './workspace-session-state-types' import { TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT } from './terminal-scrollback-limits' import { getUtf8ByteLength } from './utf8-byte-limits' import { diff --git a/src/shared/workspace-session-terminal-buffers.ts b/src/shared/workspace-session-terminal-buffers.ts index b2ec9df45dc..96bb76cafe2 100644 --- a/src/shared/workspace-session-terminal-buffers.ts +++ b/src/shared/workspace-session-terminal-buffers.ts @@ -1,4 +1,5 @@ -import type { Repo, WorkspaceSessionState } from './types' +import type { Repo } from './repo-types' +import type { WorkspaceSessionState } from './workspace-session-state-types' import { FLOATING_TERMINAL_WORKTREE_ID } from './constants' import { getRepoIdFromWorktreeId } from './worktree/id' import { TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT } from './terminal-scrollback-limits' diff --git a/src/shared/workspace-session-terminal-tab-close.test.ts b/src/shared/workspace-session-terminal-tab-close.test.ts index 66bb3d5dd63..e8b50c3b71d 100644 --- a/src/shared/workspace-session-terminal-tab-close.test.ts +++ b/src/shared/workspace-session-terminal-tab-close.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest' import { getDefaultWorkspaceSession } from './constants' -import type { Tab, TerminalTab, WorkspaceSessionState } from './types' +import type { Tab } from './tab-types' +import type { TerminalTab } from './terminal-tab-types' +import type { WorkspaceSessionState } from './workspace-session-state-types' import { closeTerminalTabInWorkspaceSession } from './workspace-session-terminal-tab-close' const WORKTREE_ID = 'worktree-1' diff --git a/src/shared/workspace-session-terminal-tab-close.ts b/src/shared/workspace-session-terminal-tab-close.ts index e5f2d6cd28a..fd82abc1dc3 100644 --- a/src/shared/workspace-session-terminal-tab-close.ts +++ b/src/shared/workspace-session-terminal-tab-close.ts @@ -1,10 +1,5 @@ -import type { - Tab, - TabGroup, - TabGroupLayoutNode, - WorkspaceSessionState, - WorkspaceVisibleTabType -} from './types' +import type { Tab, TabGroup, TabGroupLayoutNode, WorkspaceVisibleTabType } from './tab-types' +import type { WorkspaceSessionState } from './workspace-session-state-types' export type WorkspaceSessionTerminalTabCloseResult = { session: WorkspaceSessionState diff --git a/src/shared/workspace-status-defaults.ts b/src/shared/workspace-status-defaults.ts index f0c4f10e426..47aedee7e95 100644 --- a/src/shared/workspace-status-defaults.ts +++ b/src/shared/workspace-status-defaults.ts @@ -1,4 +1,4 @@ -import type { WorkspaceStatusDefinition } from './types' +import type { WorkspaceStatusDefinition } from './worktree/types' export const DEFAULT_STATUS_VISUALS: Record = { todo: { color: 'neutral', icon: 'circle' }, diff --git a/src/shared/workspace-statuses.ts b/src/shared/workspace-statuses.ts index 430595733fc..4635824c809 100644 --- a/src/shared/workspace-statuses.ts +++ b/src/shared/workspace-statuses.ts @@ -1,4 +1,4 @@ -import type { Worktree, WorkspaceStatus, WorkspaceStatusDefinition } from './types' +import type { WorkspaceStatus, WorkspaceStatusDefinition, Worktree } from './worktree/types' import { DEFAULT_STATUS_VISUALS, DEFAULT_WORKSPACE_STATUSES } from './workspace-status-defaults' import { isKnownBadPRReorderedDefaultStatusPayload, diff --git a/src/shared/worktree/card-properties.ts b/src/shared/worktree/card-properties.ts index 240b3ba341e..d0b09a01344 100644 --- a/src/shared/worktree/card-properties.ts +++ b/src/shared/worktree/card-properties.ts @@ -1,9 +1,6 @@ -import type { - GlobalSettings, - PersistedUIState, - WorktreeCardMode, - WorktreeCardProperty -} from '../types' +import type { GlobalSettings } from '../global-settings-types' +import type { PersistedUIState } from '../persisted-ui-state-types' +import type { WorktreeCardMode, WorktreeCardProperty } from '../ui-chrome-types' const FIXED_WORKTREE_CARD_PROPERTIES: WorktreeCardProperty[] = ['status', 'unread'] diff --git a/src/shared/worktree/ownership-worktree-base-path.test.ts b/src/shared/worktree/ownership-worktree-base-path.test.ts index aff3c951285..549a3d46763 100644 --- a/src/shared/worktree/ownership-worktree-base-path.test.ts +++ b/src/shared/worktree/ownership-worktree-base-path.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from 'vitest' -import type { GlobalSettings, Repo, Worktree } from '../types' +import type { GlobalSettings } from '../global-settings-types' +import type { Repo } from '../repo-types' +import type { Worktree } from './types' import { buildKnownOrcaWorkspaceLayouts, classifyWorktreeOwnership } from './ownership' function makeRepo(overrides: Partial = {}): Repo { diff --git a/src/shared/worktree/ownership.test.ts b/src/shared/worktree/ownership.test.ts index 8877a415b53..12e17145a32 100644 --- a/src/shared/worktree/ownership.test.ts +++ b/src/shared/worktree/ownership.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from 'vitest' -import type { GlobalSettings, Repo, Worktree, WorktreeMeta } from '../types' +import type { GlobalSettings } from '../global-settings-types' +import type { Repo } from '../repo-types' +import type { WorktreeMeta } from './meta-types' +import type { Worktree } from './types' import { createAgentScratchWorktreePathMatcher } from '../agent-scratch-worktrees' import { applyMetadataFallbackVisibility, diff --git a/src/shared/worktree/ownership.ts b/src/shared/worktree/ownership.ts index 7e224d0689f..a123e6c9c65 100644 --- a/src/shared/worktree/ownership.ts +++ b/src/shared/worktree/ownership.ts @@ -23,15 +23,10 @@ import { effectiveExternalWorktreeVisibility, isLegacyRepoForExternalWorktreeVisibility } from '../external-worktree-visibility' -import type { - DetectedWorktree, - GlobalSettings, - OrcaWorkspaceLayout, - Repo, - Worktree, - WorktreeMeta, - WorktreeOwnership -} from '../types' +import type { GlobalSettings, OrcaWorkspaceLayout } from '../global-settings-types' +import type { Repo } from '../repo-types' +import type { WorktreeMeta } from './meta-types' +import type { DetectedWorktree, Worktree, WorktreeOwnership } from './types' export { effectiveAgentWorktreeVisibility, diff --git a/src/shared/worktree/removal.ts b/src/shared/worktree/removal.ts index 68d1072b247..8788171ebe0 100644 --- a/src/shared/worktree/removal.ts +++ b/src/shared/worktree/removal.ts @@ -1,4 +1,4 @@ -import type { GitWorktreeInfo } from '../types' +import type { GitWorktreeInfo } from './types' export const LOCKED_WORKTREE_REMOVAL_PREFIX = 'Worktree is locked by Git.' diff --git a/src/shared/worktree/visibility-sources.test.ts b/src/shared/worktree/visibility-sources.test.ts index 280ac0f3082..c0fd4193206 100644 --- a/src/shared/worktree/visibility-sources.test.ts +++ b/src/shared/worktree/visibility-sources.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { Repo } from '../types' +import type { Repo } from '../repo-types' import { buildWorktreeSourcePreferenceUpdate, createWorktreeVisibilitySourceMatcher, diff --git a/src/shared/worktree/visibility-sources.ts b/src/shared/worktree/visibility-sources.ts index 0053bedf2de..9ba158b40d7 100644 --- a/src/shared/worktree/visibility-sources.ts +++ b/src/shared/worktree/visibility-sources.ts @@ -9,7 +9,7 @@ import type { ExternalWorktreeVisibility, Repo, WorktreeVisibilitySourcePreferences -} from '../types' +} from '../repo-types' export const MAX_CUSTOM_WORKTREE_VISIBILITY_SOURCES = 32 const MAX_SOURCE_ID_LENGTH = 128 diff --git a/tests/e2e/completed-worker-retirement-resume.unit.test.ts b/tests/e2e/completed-worker-retirement-resume.unit.test.ts index ac201ab9b32..a9c18acdd1a 100644 --- a/tests/e2e/completed-worker-retirement-resume.unit.test.ts +++ b/tests/e2e/completed-worker-retirement-resume.unit.test.ts @@ -4,7 +4,8 @@ import type { SleepingAgentSessionRecord } from '../../src/shared/agent-session- import { makePaneKey } from '../../src/shared/stable-pane-id' import { tokenizeStartupCommand } from '../../src/shared/tui-agent-startup-shell' import { parseWorkspaceSession } from '../../src/shared/workspace-session-schema' -import type { TerminalTab, Worktree } from '../../src/shared/types' +import type { TerminalTab } from '../../src/shared/terminal-tab-types' +import type { Worktree } from '../../src/shared/worktree/types' import { OrchestrationDb } from '../../src/main/runtime/orchestration/db' import { OrcaRuntimeService } from '../../src/main/runtime/orca-runtime' import type { RpcContext } from '../../src/main/runtime/rpc/core' diff --git a/tests/e2e/helpers/pr-comments-sidebar-fixture.ts b/tests/e2e/helpers/pr-comments-sidebar-fixture.ts index 3fc047ec1bf..f01661f2de8 100644 --- a/tests/e2e/helpers/pr-comments-sidebar-fixture.ts +++ b/tests/e2e/helpers/pr-comments-sidebar-fixture.ts @@ -1,5 +1,6 @@ import type { Page } from '@stablyai/playwright-test' -import type { PRComment, PRInfo } from '../../../src/shared/types' +import type { PRComment } from '../../../src/shared/github/comment-types' +import type { PRInfo } from '../../../src/shared/github/pull-request-types' export type PRCommentsSidebarSeed = { worktreeId: string diff --git a/tests/e2e/helpers/runtime-types.ts b/tests/e2e/helpers/runtime-types.ts index a376e2416da..de18b94a2d9 100644 --- a/tests/e2e/helpers/runtime-types.ts +++ b/tests/e2e/helpers/runtime-types.ts @@ -1,13 +1,11 @@ import type { AppState } from '../../../src/renderer/src/store/types' import type { OpenFile, RightSidebarTab } from '../../../src/renderer/src/store/slices/editor' import type { ManagedPane } from '../../../src/renderer/src/lib/pane-manager/pane-manager-types' -import type { - BrowserWorkspace, - Repo, - TerminalTab, - Worktree, - WorkspaceVisibleTabType -} from '../../../src/shared/types' +import type { BrowserWorkspace } from '../../../src/shared/browser-workspace-types' +import type { Repo } from '../../../src/shared/repo-types' +import type { WorkspaceVisibleTabType } from '../../../src/shared/tab-types' +import type { TerminalTab } from '../../../src/shared/terminal-tab-types' +import type { Worktree } from '../../../src/shared/worktree/types' export type AppStore = { getState(): AppState diff --git a/tests/e2e/helpers/worktree-registration.ts b/tests/e2e/helpers/worktree-registration.ts index 96a4bce5e2f..1fd6a7f9bb9 100644 --- a/tests/e2e/helpers/worktree-registration.ts +++ b/tests/e2e/helpers/worktree-registration.ts @@ -1,6 +1,6 @@ import type { Page } from '@stablyai/playwright-test' import { expect } from './orca-app' -import type { CommitMessageAiSettings } from '../../../src/shared/types' +import type { CommitMessageAiSettings } from '../../../src/shared/commit-message-ai-types' // Why: these specs create worktrees via raw `git worktree add`, which bypasses // Orca's own add/remove path — the one thing that invalidates the main-process diff --git a/tests/e2e/linear-url-workspace-entry.spec.ts b/tests/e2e/linear-url-workspace-entry.spec.ts index ccdf1d00c4a..7cd9ccf30ca 100644 --- a/tests/e2e/linear-url-workspace-entry.spec.ts +++ b/tests/e2e/linear-url-workspace-entry.spec.ts @@ -1,7 +1,7 @@ import type { ElectronApplication, Page } from '@stablyai/playwright-test' import { test, expect } from './helpers/orca-app' import { waitForActiveWorktree, waitForSessionReady } from './helpers/store' -import type { LinearIssue } from '../../src/shared/types' +import type { LinearIssue } from '../../src/shared/linear/issue-types' const LINEAR_URL = 'https://linear.app/stably/issue/STA-4084/restore-osc-133-shell-integration-when-an-exec-in-user-rc-files-strips' diff --git a/tests/e2e/native-chat-ask-user-question-card.spec.ts b/tests/e2e/native-chat-ask-user-question-card.spec.ts index a3788e5aa96..74620ed3104 100644 --- a/tests/e2e/native-chat-ask-user-question-card.spec.ts +++ b/tests/e2e/native-chat-ask-user-question-card.spec.ts @@ -6,7 +6,7 @@ import type { Page } from '@stablyai/playwright-test' import { test, expect } from './helpers/orca-app' import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store' import { waitForActivePaneHookDescriptor, waitForActiveTerminalManager } from './helpers/terminal' -import type { GlobalSettings } from '../../src/shared/types' +import type { GlobalSettings } from '../../src/shared/global-settings-types' const QUESTION = 'Tabs or spaces?' diff --git a/tests/e2e/native-chat-first-flush-race.spec.ts b/tests/e2e/native-chat-first-flush-race.spec.ts index 15ec43dc843..1362a882902 100644 --- a/tests/e2e/native-chat-first-flush-race.spec.ts +++ b/tests/e2e/native-chat-first-flush-race.spec.ts @@ -6,7 +6,7 @@ import type { Page } from '@stablyai/playwright-test' import { test, expect } from './helpers/orca-app' import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store' import { waitForActivePaneHookDescriptor, waitForActiveTerminalManager } from './helpers/terminal' -import type { GlobalSettings } from '../../src/shared/types' +import type { GlobalSettings } from '../../src/shared/global-settings-types' const LOADING_TITLE = 'Loading conversation…' const ERROR_TITLE = 'Could not load conversation' diff --git a/tests/e2e/notification-settings.spec.ts b/tests/e2e/notification-settings.spec.ts index ae33f26d7ae..2b4f78fbc57 100644 --- a/tests/e2e/notification-settings.spec.ts +++ b/tests/e2e/notification-settings.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from './helpers/orca-app' import { waitForSessionReady } from './helpers/store' -import type { GlobalSettings } from '../../src/shared/types' +import type { GlobalSettings } from '../../src/shared/global-settings-types' async function getSettings( page: Parameters[0] diff --git a/tests/e2e/onboarding.spec.ts b/tests/e2e/onboarding.spec.ts index 0fb5ad870c1..a4fcaa8dc1d 100644 --- a/tests/e2e/onboarding.spec.ts +++ b/tests/e2e/onboarding.spec.ts @@ -10,7 +10,8 @@ import { test, expect } from './helpers/orca-app' import { waitForSessionReady } from './helpers/store' import type { Page } from '@stablyai/playwright-test' -import type { GlobalSettings, TuiAgent } from '../../src/shared/types' +import type { GlobalSettings } from '../../src/shared/global-settings-types' +import type { TuiAgent } from '../../src/shared/tui-agent' import { ONBOARDING_FINAL_STEP } from '../../src/shared/constants' import { encodePairingOffer, PAIRING_OFFER_VERSION } from '../../src/shared/pairing' diff --git a/tests/e2e/paired-remote-pane-layout-retry.spec.ts b/tests/e2e/paired-remote-pane-layout-retry.spec.ts index 653cde88add..fd5f347bb69 100644 --- a/tests/e2e/paired-remote-pane-layout-retry.spec.ts +++ b/tests/e2e/paired-remote-pane-layout-retry.spec.ts @@ -1,7 +1,7 @@ import type { Page } from '@stablyai/playwright-test' import type { RuntimeMobileSessionTabsResult } from '../../src/shared/runtime-types' import { toWebTerminalSurfaceTabId } from '../../src/shared/terminal-surface-id' -import type { TerminalLayoutSnapshot } from '../../src/shared/types' +import type { TerminalLayoutSnapshot } from '../../src/shared/terminal-tab-types' import { launchHeadlessPairedRuntimeHost, type HeadlessPairedRuntimeHost diff --git a/tests/e2e/pr11346-selected-runtime-add.spec.ts b/tests/e2e/pr11346-selected-runtime-add.spec.ts index f4e86d8b0f5..20e34121f88 100644 --- a/tests/e2e/pr11346-selected-runtime-add.spec.ts +++ b/tests/e2e/pr11346-selected-runtime-add.spec.ts @@ -2,7 +2,9 @@ import { rmSync } from 'node:fs' import path from 'node:path' import type { ElectronApplication, Locator, Page, TestInfo } from '@stablyai/playwright-test' import { RuntimeClient } from '../../src/cli/runtime/client' -import type { FolderWorkspace, ProjectGroup, Repo } from '../../src/shared/types' +import type { FolderWorkspace } from '../../src/shared/folder-workspace-types' +import type { ProjectGroup } from '../../src/shared/project-group-types' +import type { Repo } from '../../src/shared/repo-types' import { expect, test } from './helpers/orca-app' import { createRuntimeDesktopPairingOffer, diff --git a/tests/e2e/remote-terminal-tab-retirement.unit.test.ts b/tests/e2e/remote-terminal-tab-retirement.unit.test.ts index 6696e2ca7f6..45ac2f8b0a0 100644 --- a/tests/e2e/remote-terminal-tab-retirement.unit.test.ts +++ b/tests/e2e/remote-terminal-tab-retirement.unit.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { getDefaultWorkspaceSession } from '../../src/shared/constants' import type { RuntimeMobileSessionTabsSnapshot } from '../../src/shared/runtime-types' -import type { WorkspaceSessionState } from '../../src/shared/types' +import type { WorkspaceSessionState } from '../../src/shared/workspace-session-state-types' import { acceptReplayedWebSessionTabsSnapshot, applyWebSessionTabsSnapshot, diff --git a/tests/e2e/repo-icon-emoji-picker.spec.ts b/tests/e2e/repo-icon-emoji-picker.spec.ts index 2698615b881..1513365725c 100644 --- a/tests/e2e/repo-icon-emoji-picker.spec.ts +++ b/tests/e2e/repo-icon-emoji-picker.spec.ts @@ -7,7 +7,7 @@ import type { Page, TestInfo } from '@stablyai/playwright-test' import { expect, test } from './helpers/orca-app' import { getStoreState, waitForSessionReady } from './helpers/store' -import type { Repo } from '../../src/shared/types' +import type { Repo } from '../../src/shared/repo-types' /** Opens the repo settings panel and pins the UI language to English. */ async function openRepoSettings(page: Page, repoId: string): Promise { diff --git a/tests/e2e/settings-agent-awake.spec.ts b/tests/e2e/settings-agent-awake.spec.ts index 15c25e2fc4e..8a2ad840a14 100644 --- a/tests/e2e/settings-agent-awake.spec.ts +++ b/tests/e2e/settings-agent-awake.spec.ts @@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto' import type { ElectronApplication, Page } from '@stablyai/playwright-test' import { test, expect } from './helpers/orca-app' import { waitForSessionReady } from './helpers/store' -import type { GlobalSettings } from '../../src/shared/types' +import type { GlobalSettings } from '../../src/shared/global-settings-types' import { readHookEndpoint } from './helpers/agent-hook-endpoint' type AwakeProbeSnapshot = { diff --git a/tests/e2e/settings-display-name-ime.spec.ts b/tests/e2e/settings-display-name-ime.spec.ts index 8001c6d3338..e6da51bdb71 100644 --- a/tests/e2e/settings-display-name-ime.spec.ts +++ b/tests/e2e/settings-display-name-ime.spec.ts @@ -10,7 +10,7 @@ import type { CDPSession, Locator, Page } from '@stablyai/playwright-test' import { test, expect } from './helpers/orca-app' import { getStoreState, waitForSessionReady } from './helpers/store' -import type { Repo } from '../../src/shared/types' +import type { Repo } from '../../src/shared/repo-types' async function openRepoSettings(page: Page, repoId: string): Promise { await page.evaluate((repoId) => { diff --git a/tests/e2e/settings-search-responsiveness.spec.ts b/tests/e2e/settings-search-responsiveness.spec.ts index 05bbb29e284..b3ac59d3320 100644 --- a/tests/e2e/settings-search-responsiveness.spec.ts +++ b/tests/e2e/settings-search-responsiveness.spec.ts @@ -1,7 +1,7 @@ import path from 'node:path' import { test, expect } from './helpers/orca-app' import { waitForSessionReady } from './helpers/store' -import type { Repo } from '../../src/shared/types' +import type { Repo } from '../../src/shared/repo-types' const MATCHING_PROJECT_COUNT = 240 diff --git a/tests/e2e/terminal-duplicate-pty-renderer-reveal.spec.ts b/tests/e2e/terminal-duplicate-pty-renderer-reveal.spec.ts index b11c74c86be..8bc5db7be0b 100644 --- a/tests/e2e/terminal-duplicate-pty-renderer-reveal.spec.ts +++ b/tests/e2e/terminal-duplicate-pty-renderer-reveal.spec.ts @@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto' import { existsSync, readFileSync, writeFileSync } from 'node:fs' import path from 'node:path' import type { ElectronApplication, Page } from '@stablyai/playwright-test' -import type { TerminalLayoutSnapshot } from '../../src/shared/types' +import type { TerminalLayoutSnapshot } from '../../src/shared/terminal-tab-types' import { DEFAULT_LOCAL_ORCA_PROFILE_ID } from '../../src/shared/orca-profiles' import { test, expect } from './helpers/orca-app' import { attachRepoAndOpenTerminal, createRestartSession } from './helpers/orca-restart' diff --git a/tests/e2e/worktree-smart-sort.spec.ts b/tests/e2e/worktree-smart-sort.spec.ts index 0bcadf31623..2c193d3336b 100644 --- a/tests/e2e/worktree-smart-sort.spec.ts +++ b/tests/e2e/worktree-smart-sort.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from './helpers/orca-app' import type { Page } from '@stablyai/playwright-test' -import type { TerminalPaneLayoutNode } from '../../src/shared/types' +import type { TerminalPaneLayoutNode } from '../../src/shared/terminal-tab-types' import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store' import { worktreeRow } from './worktree-row-locators' From b9627e91a9188f2f999690c05a5744c2fa120e0c Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:11:24 -0700 Subject: [PATCH 10/42] perf(renderer): parallelize runtime catalog and worktree refresh (#14138) Co-authored-by: Orca --- src/renderer/src/hooks/useIpcEvents.test.ts | 72 +++++++++++++++++++++ src/renderer/src/hooks/useIpcEvents.ts | 22 ++++--- 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/src/renderer/src/hooks/useIpcEvents.test.ts b/src/renderer/src/hooks/useIpcEvents.test.ts index 392750fe861..e02ff8a9b34 100644 --- a/src/renderer/src/hooks/useIpcEvents.test.ts +++ b/src/renderer/src/hooks/useIpcEvents.test.ts @@ -10328,6 +10328,78 @@ describe('runtime host catalog refresh on reposChanged', () => { vi.useRealTimers() } }) + + // Why: the catalog fetches carry 15s RPC timeouts; serializing them ahead of the + // worktree refresh stalled lineage convergence for 30s on a connected-but-wedged host. + it('refreshes worktrees and lineage without waiting on a stalled catalog fetch', async () => { + vi.resetModules() + vi.useFakeTimers() + try { + const fetchRuntimeEnvironmentRepos = vi.fn(() => Promise.resolve([{ id: 'repo-1' }])) + // Never settles, standing in for a host that accepted the connection but stopped answering. + const fetchProjectGroups = vi.fn(() => new Promise(() => {})) + const fetchFolderWorkspaces = vi.fn(() => Promise.resolve()) + const fetchWorktrees = vi.fn(() => Promise.resolve()) + const fetchWorktreeLineage = vi.fn(() => Promise.resolve()) + const state = { + settings: { activeRuntimeEnvironmentId: 'env-1' as string | null }, + repos: [], + worktreesByRepo: {}, + folderWorkspaces: [], + projectGroups: [], + runtimeEnvironments: [], + runtimeStatusByEnvironmentId: new Map(), + tabsByWorktree: {}, + ptyIdsByTabId: {}, + remountTerminalTabForRecovery: vi.fn(), + markEnvironmentSshStateStale: vi.fn(), + fetchRepos: vi.fn(() => Promise.resolve()), + fetchRuntimeEnvironmentRepos, + fetchProjectGroups, + fetchFolderWorkspaces, + fetchWorktrees, + fetchWorktreeLineage + } + + vi.doMock('react', async () => { + const actual = await vi.importActual('react') + return { ...actual, useEffect: (effect: () => void | (() => void)) => void effect() } + }) + vi.doMock('../store', () => ({ + useAppStore: { subscribe: vi.fn(() => () => {}), getState: () => state } + })) + const noopListener = (): (() => void) => () => {} + const autoStubNamespace = new Proxy( + {}, + { + get: + () => + (...args: unknown[]) => { + if (typeof args[0] === 'function') { + return noopListener() + } + return new Promise(() => {}) + } + } + ) + const api = new Proxy({} as Record, { + get: (target, prop: string) => target[prop] ?? autoStubNamespace + }) + vi.stubGlobal('window', { api }) + + const { useIpcEvents } = await import('./useIpcEvents') + useIpcEvents() + await vi.advanceTimersByTimeAsync(300) + + expect(fetchProjectGroups).toHaveBeenCalled() + // Folder workspaces still wait on their groups; worktrees and lineage must not. + expect(fetchFolderWorkspaces).not.toHaveBeenCalled() + expect(fetchWorktrees).toHaveBeenCalledWith('repo-1', expect.anything()) + expect(fetchWorktreeLineage).toHaveBeenCalled() + } finally { + vi.useRealTimers() + } + }) }) describe('repo catalog refresh on repos:changed', () => { diff --git a/src/renderer/src/hooks/useIpcEvents.ts b/src/renderer/src/hooks/useIpcEvents.ts index d9f3727232d..b91a1372430 100644 --- a/src/renderer/src/hooks/useIpcEvents.ts +++ b/src/renderer/src/hooks/useIpcEvents.ts @@ -940,14 +940,20 @@ export function useIpcEvents(): void { // Why: the host emits one reposChanged for group/folder-workspace edits too, so those // catalogs go stale without this; groups first because folder workspaces resolve owners from them. const runtimeOwner = { runtimeEnvironmentId: environmentId } - await useAppStore.getState().fetchProjectGroups(runtimeOwner) - await useAppStore.getState().fetchFolderWorkspaces(runtimeOwner) - await refreshRuntimeProjectWorktreesAndLineage( - environmentId, - repos, - (repoId, options) => useAppStore.getState().fetchWorktrees(repoId, options), - (options) => useAppStore.getState().fetchWorktreeLineage(options) - ) + // Why: catalogs and worktrees are independent; serializing them put two 15s RPC + // timeouts ahead of worktree/lineage convergence on a wedged host. + await Promise.all([ + (async () => { + await useAppStore.getState().fetchProjectGroups(runtimeOwner) + await useAppStore.getState().fetchFolderWorkspaces(runtimeOwner) + })(), + refreshRuntimeProjectWorktreesAndLineage( + environmentId, + repos, + (repoId, options) => useAppStore.getState().fetchWorktrees(repoId, options), + (options) => useAppStore.getState().fetchWorktreeLineage(options) + ) + ]) }, onError: (error) => { console.error('Failed to refresh runtime projects:', error) From 3035aa92110cdcc383ac197661ca3bd70cbbaba6 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:15:21 -0700 Subject: [PATCH 11/42] fix(browser): restore replaced cookies through CDP identities (#14383) * fix(browser): restore replaced cookies through CDP identities Both remaining callers of the imported-domain replacement rolled back by rebuilding cookies with cookies.set, which silently drops partitionKey. The rollback in importValidatedCookies puts back the user's ORIGINAL cookies that the import already deleted, so a CHIPS cookie came back as an ordinary one and no restart recovered it. Snapshot CDP identities before the first removal and undo through them, the same machinery removeTransplantableCookies already uses. The store type omits 'set' so the lossy reconstruction cannot be reintroduced, and restoreImportedDomainCookies is deleted now that both callers are gone. * fix(browser): skip the CDP rollback when nothing was replaced restoreClearIdentities attaches the debugger before it iterates, so an empty restore set would spin up a hidden BrowserWindow to put nothing back. The old cookies.set restore was a no-op loop in that case. --- .../browser-cookie-import-policy.test.ts | 172 ++++++---- .../browser/browser-cookie-import-policy.ts | 109 ++++--- ...eplace-partition-rollback.electron.test.ts | 293 ++++++++++++++++++ .../browser-cookie-import-replacement.test.ts | 78 ++++- src/main/browser/browser-cookie-import.ts | 193 ++++++------ 5 files changed, 643 insertions(+), 202 deletions(-) create mode 100644 src/main/browser/browser-cookie-import-replace-partition-rollback.electron.test.ts diff --git a/src/main/browser/browser-cookie-import-policy.test.ts b/src/main/browser/browser-cookie-import-policy.test.ts index 980c5f78ddb..25204fcd1c7 100644 --- a/src/main/browser/browser-cookie-import-policy.test.ts +++ b/src/main/browser/browser-cookie-import-policy.test.ts @@ -3,7 +3,8 @@ import { DatabaseSync } from 'node:sqlite' import type { Cookie } from 'electron' import { identitiesFromClearCookies, - removeTransplantableCookies + removeTransplantableCookies, + type CookieClearIdentity } from './browser-cookie-import-clear' import { isGoogleSourceBoundCookie, @@ -49,6 +50,29 @@ describe('isGoogleSourceBoundCookie', () => { }) }) +// Why: mirrors what openCookieClearStore returns — get/remove plus the CDP identity pair, and +// deliberately no 'set', so a partition-dropping reconstruction cannot be written against it. +function replaceStore( + existing: Cookie[], + overrides: { + remove?: Mock + snapshot?: Mock + restore?: Mock + } = {} +) { + const get = vi.fn().mockResolvedValue(existing) + const remove = overrides.remove ?? vi.fn().mockResolvedValue(undefined) + const snapshotClearIdentities = + overrides.snapshot ?? + vi + .fn() + .mockImplementation(async (cookies: readonly { cookie: Cookie; url: string }[]) => + identitiesFromClearCookies(cookies) + ) + const restoreClearIdentities = overrides.restore ?? vi.fn().mockResolvedValue(undefined) + return { get, remove, snapshotClearIdentities, restoreClearIdentities } +} + describe('replaceCookiesForImportedDomains', () => { it('removes parent, exact, and child-domain cookies while preserving unrelated sites', async () => { const existing = [ @@ -59,99 +83,129 @@ describe('replaceCookiesForImportedDomains', () => { cookie('.google.com.evil.example', 'suffix-confusion'), cookie('.example.com', 'unrelated') ] - const get = vi.fn().mockResolvedValue(existing) - const remove = vi.fn().mockResolvedValue(undefined) - const set = vi.fn().mockResolvedValue(undefined) + const store = replaceStore(existing) - const removed = await replaceCookiesForImportedDomains({ get, remove, set }, [ - 'accounts.google.com' - ]) + const { removed } = await replaceCookiesForImportedDomains(store, ['accounts.google.com']) expect(removed).toHaveLength(3) - expect(get).toHaveBeenCalledWith({}) - expect(remove.mock.calls).toEqual([ + expect(store.get).toHaveBeenCalledWith({}) + expect(store.remove.mock.calls).toEqual([ ['https://google.com/', 'parent'], ['https://accounts.google.com/signin', 'exact'], ['http://child.accounts.google.com/nested', 'child'] ]) - expect(set).not.toHaveBeenCalled() + // Why: the snapshot must cover the whole removal plan before the first removal runs. + expect(store.snapshotClearIdentities).toHaveBeenCalledOnce() + expect( + store.snapshotClearIdentities.mock.calls[0]?.[0].map( + ({ cookie: entry }: { cookie: Cookie }) => entry.name + ) + ).toEqual(['parent', 'exact', 'child']) + expect(store.restoreClearIdentities).not.toHaveBeenCalled() }) it('does not replace a private-suffix host cookie for a tenant import', async () => { - const get = vi - .fn() - .mockResolvedValue([ - { ...cookie('github.io', 'host-only-suffix'), hostOnly: true }, - cookie('.user.github.io', 'tenant') - ]) - const remove = vi.fn().mockResolvedValue(undefined) - const set = vi.fn().mockResolvedValue(undefined) + const store = replaceStore([ + { ...cookie('github.io', 'host-only-suffix'), hostOnly: true }, + cookie('.user.github.io', 'tenant') + ]) - const removed = await replaceCookiesForImportedDomains({ get, remove, set }, ['user.github.io']) + const { removed } = await replaceCookiesForImportedDomains(store, ['user.github.io']) expect(removed.map(({ name }) => name)).toEqual(['tenant']) - expect(remove).toHaveBeenCalledWith('https://user.github.io/', 'tenant') + expect(store.remove).toHaveBeenCalledWith('https://user.github.io/', 'tenant') }) it('does not read or mutate the store when no valid domain scope exists', async () => { - const get = vi.fn() - const remove = vi.fn() - const set = vi.fn() + const store = replaceStore([]) await expect( - replaceCookiesForImportedDomains({ get, remove, set }, [ - '', - '...', - 'com', - 'co.uk', - 'github.io' - ]) - ).resolves.toEqual([]) - expect(get).not.toHaveBeenCalled() - expect(remove).not.toHaveBeenCalled() - expect(set).not.toHaveBeenCalled() + replaceCookiesForImportedDomains(store, ['', '...', 'com', 'co.uk', 'github.io']) + ).resolves.toEqual({ removed: [], identities: [] }) + expect(store.get).not.toHaveBeenCalled() + expect(store.remove).not.toHaveBeenCalled() + expect(store.snapshotClearIdentities).not.toHaveBeenCalled() + expect(store.restoreClearIdentities).not.toHaveBeenCalled() }) it('keeps single-label intranet scopes from selecting descendant hosts', async () => { - const get = vi - .fn() - .mockResolvedValue([cookie('local', 'exact'), cookie('.service.local', 'descendant')]) - const remove = vi.fn().mockResolvedValue(undefined) - const set = vi.fn().mockResolvedValue(undefined) + const store = replaceStore([cookie('local', 'exact'), cookie('.service.local', 'descendant')]) - const removed = await replaceCookiesForImportedDomains({ get, remove, set }, ['local']) + const { removed } = await replaceCookiesForImportedDomains(store, ['local']) expect(removed.map(({ name }) => name)).toEqual(['exact']) - expect(remove).toHaveBeenCalledOnce() - expect(remove).toHaveBeenCalledWith('https://local/', 'exact') + expect(store.remove).toHaveBeenCalledOnce() + expect(store.remove).toHaveBeenCalledWith('https://local/', 'exact') }) - it('restores cookies removed before a later removal fails', async () => { + // Why (STA-4097): cookies.get strips partitionKey and cookies.set ignores it, so a rollback + // that rebuilds cookies through the Electron API silently downgrades CHIPS cookies. The undo + // has to travel back through the CDP identities that actually carry the partition. + it('restores removed cookies through CDP identities, keeping partition identity', async () => { const existing = [ cookie('.example.com', 'first', '/one'), cookie('.example.com', 'second', '/two') ] - const get = vi.fn().mockResolvedValue(existing) + const partitionKey = { topLevelSite: 'https://top.example', hasCrossSiteAncestor: true } + const snapshot = vi + .fn() + .mockImplementation(async (cookies: readonly { cookie: Cookie; url: string }[]) => + identitiesFromClearCookies(cookies).map((identity) => + identity.name === 'first' ? { ...identity, partitionKey } : identity + ) + ) const remove = vi .fn() .mockResolvedValueOnce(undefined) .mockRejectedValueOnce(new Error('cookie store unavailable')) - const set = vi.fn().mockResolvedValue(undefined) + const store = replaceStore(existing, { remove, snapshot }) - await expect( - replaceCookiesForImportedDomains({ get, remove, set }, ['example.com']) - ).rejects.toThrow('cookie store unavailable') - expect(set).toHaveBeenCalledOnce() - expect(set).toHaveBeenCalledWith({ - url: 'https://example.com/one', - name: 'first', - value: 'secret', - domain: '.example.com', - path: '/one', - secure: true, - httpOnly: undefined, - sameSite: 'unspecified' - }) + await expect(replaceCookiesForImportedDomains(store, ['example.com'])).rejects.toThrow( + 'cookie store unavailable' + ) + + expect(store.restoreClearIdentities).toHaveBeenCalledOnce() + const restored = store.restoreClearIdentities.mock.calls[0]?.[0] as CookieClearIdentity[] + // Why: the failing coordinate is restored too — a rejected remove cannot prove it survived. + expect(restored.map(({ name }) => name)).toEqual(['second', 'first']) + expect(restored.find(({ name }) => name === 'first')?.partitionKey).toEqual(partitionKey) + expect(restored.find(({ name }) => name === 'first')?.url).toBe('https://example.com/one') + }) + + it('aborts without removing anything when the snapshot cannot cover the removal plan', async () => { + const existing = [ + cookie('.example.com', 'first', '/one'), + cookie('.example.com', 'second', '/two') + ] + const snapshot = vi + .fn() + .mockImplementation(async (cookies: readonly { cookie: Cookie; url: string }[]) => + identitiesFromClearCookies(cookies).filter((identity) => identity.name !== 'second') + ) + const store = replaceStore(existing, { snapshot }) + + await expect(replaceCookiesForImportedDomains(store, ['example.com'])).rejects.toThrow( + 'the session was left unchanged' + ) + expect(store.remove).not.toHaveBeenCalled() + expect(store.restoreClearIdentities).not.toHaveBeenCalled() + }) + + it('reports both failures when the CDP rollback itself fails', async () => { + const remove = vi + .fn() + .mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error('cookie store unavailable')) + const restore = vi.fn().mockRejectedValue(new Error('debugger detached')) + const store = replaceStore( + [cookie('.example.com', 'first', '/one'), cookie('.example.com', 'second', '/two')], + { remove, restore } + ) + + await expect(replaceCookiesForImportedDomains(store, ['example.com'])).rejects.toThrow( + 'Cookie replacement and rollback failed' + ) + expect(restore).toHaveBeenCalledOnce() }) }) diff --git a/src/main/browser/browser-cookie-import-policy.ts b/src/main/browser/browser-cookie-import-policy.ts index 9fa3bc5f9b7..4ca88ed6579 100644 --- a/src/main/browser/browser-cookie-import-policy.ts +++ b/src/main/browser/browser-cookie-import-policy.ts @@ -1,5 +1,7 @@ import type { Cookie, Cookies } from 'electron' import { parse as parseDomain } from 'psl' +// Why: type-only, so this does not create a runtime cycle with the clear module. +import type { CookieClearIdentity } from './browser-cookie-import-clear' const GOOGLE_SOURCE_BOUND_COOKIE_NAMES = new Set([ 'SIDCC', @@ -167,69 +169,102 @@ export function cookieRemovalUrl(cookie: Cookie, domain: string): string | null } } -export async function restoreImportedDomainCookies( - store: Pick, - cookies: readonly Cookie[] -): Promise { - const failures: unknown[] = [] - for (const cookie of cookies) { - try { - const domain = cookie.domain ? normalizeCookieDomain(cookie.domain) : null - const url = domain ? cookieRemovalUrl(cookie, domain) : null - if (!url) { - continue - } - await store.set({ - url, - name: cookie.name, - value: cookie.value, - ...(cookie.hostOnly ? {} : { domain: cookie.domain }), - ...(cookie.path ? { path: cookie.path } : {}), - secure: cookie.secure, - httpOnly: cookie.httpOnly, - sameSite: cookie.sameSite, - ...(cookie.expirationDate ? { expirationDate: cookie.expirationDate } : {}) - }) - } catch (err) { - failures.push(err) +// Why (STA-4097): 'set' stays out so the partition-dropping reconstruction cannot return. +// Undoing a removal is only possible through CDP identities, which carry partitionKey. +export type ImportedDomainReplaceStore = Pick & { + snapshotClearIdentities( + cookies: readonly { cookie: Cookie; url: string }[] + ): Promise + restoreClearIdentities(identities: readonly CookieClearIdentity[]): Promise +} + +export type ReplacedImportedDomainCookies = { + removed: Cookie[] + identities: CookieClearIdentity[] +} + +function replaceRemovalKey(url: string, name: string): string { + return JSON.stringify([url, name]) +} + +function assertIdentitiesCoverRemovable( + removable: readonly { cookie: Cookie; url: string }[], + identities: readonly CookieClearIdentity[] +): void { + const covered = new Set( + identities.map((identity) => replaceRemovalKey(identity.url, identity.name)) + ) + for (const item of removable) { + if (!covered.has(replaceRemovalKey(item.url, item.cookie.name))) { + throw new Error('Could not replace existing cookies; the session was left unchanged') } } - if (failures.length > 0) { - throw new AggregateError(failures, 'Could not restore replaced cookies') - } } export async function replaceCookiesForImportedDomains( - store: Pick, + store: ImportedDomainReplaceStore, importedDomains: readonly string[] -): Promise { +): Promise { const scopes = importedDomainScopes(importedDomains) if (scopes.exact.size === 0) { - return [] + return { removed: [], identities: [] } } + // Why (STA-4170): the removal plan is fixed here, beside the identities that can undo it, so + // the restorable set always equals the mutated set. Re-reading the jar later would widen the + // removal past what the snapshot can restore. const existingCookies = await store.get({}) - const removedCookies: Cookie[] = [] + const removable: { cookie: Cookie; url: string }[] = [] for (const cookie of existingCookies) { const domain = cookie.domain ? normalizeCookieDomain(cookie.domain) : null if (!domain || !overlapsImportedDomain(cookie, domain, scopes)) { continue } const url = cookieRemovalUrl(cookie, domain) - if (!url) { - continue + if (url) { + removable.push({ cookie, url }) + } + } + if (removable.length === 0) { + return { removed: [], identities: [] } + } + + // Why: snapshotting before the first removal is what makes the rollback lossless; an + // incomplete snapshot aborts while the session is still untouched. + const identities = await store.snapshotClearIdentities(removable) + assertIdentitiesCoverRemovable(removable, identities) + const identitiesByKey = new Map() + for (const identity of identities) { + const key = replaceRemovalKey(identity.url, identity.name) + const group = identitiesByKey.get(key) ?? [] + group.push(identity) + identitiesByKey.set(key, group) + } + + const removed: Cookie[] = [] + // Why: one remove(url, name) deletes every cookie at that coordinate, partitioned twins + // included, so the rollback set is tracked per coordinate rather than per cookie. + const attemptedKeys = new Set() + const attemptedIdentities: CookieClearIdentity[] = [] + for (const { cookie, url } of removable) { + const key = replaceRemovalKey(url, cookie.name) + if (!attemptedKeys.has(key)) { + attemptedKeys.add(key) + attemptedIdentities.push(...(identitiesByKey.get(key) ?? [])) } try { await store.remove(url, cookie.name) - removedCookies.push(cookie) + removed.push(cookie) } catch (err) { try { - await restoreImportedDomainCookies(store, removedCookies) + // Why: the failing coordinate is included because a rejected remove cannot prove the + // cookie survived; restoring a live cookie rewrites the value it was snapshotted with. + await store.restoreClearIdentities(attemptedIdentities.toReversed()) } catch (restoreError) { throw new AggregateError([err, restoreError], 'Cookie replacement and rollback failed') } throw err } } - return removedCookies + return { removed, identities } } diff --git a/src/main/browser/browser-cookie-import-replace-partition-rollback.electron.test.ts b/src/main/browser/browser-cookie-import-replace-partition-rollback.electron.test.ts new file mode 100644 index 00000000000..bcb16ad9cfe --- /dev/null +++ b/src/main/browser/browser-cookie-import-replace-partition-rollback.electron.test.ts @@ -0,0 +1,293 @@ +import { spawnSync } from 'node:child_process' +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterAll, describe, expect, it } from 'vitest' +import { build as buildVite } from 'vite' + +const electronBinary = createRequire(import.meta.url)('electron') as string +const fixtureRoots: string[] = [] + +afterAll(() => { + for (const root of fixtureRoots) { + rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }) + } +}) + +type CdpCookie = { name: string; value: string; partitionKey?: Record } + +type FixtureResult = { + beforePartitionKey: Record | undefined + electronGetSawPartitionKey: boolean + removeCalls: number + removeCallsBeforeThrow: number + snapshotCalls: number + restoreCalls: number + importedRemoveCalls: number + replaceError: string | null + chipsAfterReplace: CdpCookie[] + chipsAfter: CdpCookie[] + plainAfter: CdpCookie[] +} + +const EXPECTED_PARTITION_KEY = { + topLevelSite: 'https://top.example', + hasCrossSiteAncestor: true +} + +type FixtureMode = 'site-a' | 'site-b' + +function buildFixtureMain(bundlePath: string, resultPath: string, mode: FixtureMode): string { + return ` +const { app, BrowserWindow, session } = require('electron') +const { writeFileSync } = require('node:fs') +const { openCookieClearStore, replaceCookiesForImportedDomains } = require(${JSON.stringify(bundlePath)}) +const resultPath = ${JSON.stringify(resultPath)} +const mode = ${JSON.stringify(mode)} +const expectedPartitionKey = ${JSON.stringify(EXPECTED_PARTITION_KEY)} +let currentStep = 'starting' +const mark = (step) => { + currentStep = step + writeFileSync(resultPath, JSON.stringify({ step })) +} + +const project = (cookies, name) => cookies + .filter((cookie) => cookie.name === name) + .map((cookie) => ({ name: cookie.name, value: cookie.value, partitionKey: cookie.partitionKey })) + +async function run() { + const timeout = setTimeout(() => { + writeFileSync(resultPath, JSON.stringify({ step: 'timed out after ' + currentStep })) + app.exit(1) + }, 20000) + await app.whenReady() + mark('ready') + const partition = 'persist:replace-partition-rollback-' + mode + const targetSession = session.fromPartition(partition) + const window = new BrowserWindow({ show: false, webPreferences: { partition } }) + mark('window created') + await window.loadURL('data:text/html,replace partition rollback fixture') + mark('window loaded') + const debug = window.webContents.debugger + debug.attach('1.3') + mark('debugger attached') + + // Only CDP can create a partitioned cookie; Electron's cookies API has no partitionKey. + await debug.sendCommand('Network.setCookie', { + url: 'https://app.acme-chips.test/', + name: 'chips-auth', + value: 'keep-me', + secure: true, + sameSite: 'None', + partitionKey: expectedPartitionKey + }) + const beforeChips = (await debug.sendCommand('Network.getAllCookies')).cookies + .find((cookie) => cookie.name === 'chips-auth') + if (!beforeChips || !beforeChips.partitionKey) { + throw new Error('CHIPS fixture cookie was not stored partitioned') + } + mark('partitioned cookie set') + + await targetSession.cookies.set({ + url: 'https://plain.acme-chips.test/', + name: 'plain', + value: 'original', + secure: true + }) + await targetSession.cookies.set({ + url: 'https://victim.acme-chips.test/', + name: 'victim', + value: 'original', + secure: true + }) + mark('ordinary cookies set') + + let removeCalls = 0 + let removeCallsBeforeThrow = 0 + let snapshotCalls = 0 + let restoreCalls = 0 + let importedRemoveCalls = 0 + let chipsRemoved = false + let electronGetSawPartitionKey = false + let replaceError = null + let chipsAfterReplace = [] + + const cookieClearStore = openCookieClearStore(targetSession) + const store = { + get: async (filter) => { + const cookies = await targetSession.cookies.get(filter) + // Why: proves the Electron API cannot see the partition it is about to destroy. + electronGetSawPartitionKey = cookies.some( + (cookie) => cookie.name === 'chips-auth' && cookie.partitionKey !== undefined + ) + // Why: cookies.get order is not a contract; put CHIPS first so it is provably already + // removed by the time a later removal rejects. + return [...cookies].sort((left, right) => { + if (left.name === 'chips-auth') return -1 + if (right.name === 'chips-auth') return 1 + return 0 + }) + }, + remove: async (url, name) => { + removeCalls++ + if (mode === 'site-a' && chipsRemoved) { + removeCallsBeforeThrow = removeCalls - 1 + throw new Error('forced removal failure after chips was removed') + } + await targetSession.cookies.remove(url, name) + if (name === 'chips-auth') chipsRemoved = true + }, + snapshotClearIdentities: async (cookies) => { + snapshotCalls++ + return cookieClearStore.snapshotClearIdentities(cookies) + }, + restoreClearIdentities: async (identities) => { + restoreCalls++ + return cookieClearStore.restoreClearIdentities(identities) + } + } + + try { + if (mode === 'site-a') { + try { + await replaceCookiesForImportedDomains(store, ['acme-chips.test']) + } catch (error) { + replaceError = String(error && error.message ? error.message : error) + } + mark('site-a replace finished') + } else { + const replaced = await replaceCookiesForImportedDomains(store, ['acme-chips.test']) + chipsAfterReplace = project((await debug.sendCommand('Network.getAllCookies')).cookies, 'chips-auth') + mark('site-b replace finished') + // Why: stands in for importValidatedCookies' mid-import cookies.set rejection — the + // imported cookie is removed (lossless) and the user's originals go back through CDP. + await targetSession.cookies.set({ + url: 'https://imported.acme-chips.test/', + name: 'imported-new', + value: 'new', + secure: true + }) + await targetSession.cookies.remove('https://imported.acme-chips.test/', 'imported-new') + importedRemoveCalls = 1 + mark('site-b imported cookies rolled back') + restoreCalls++ + await cookieClearStore.restoreClearIdentities(replaced.identities.toReversed()) + mark('site-b restore finished') + } + } finally { + cookieClearStore.dispose() + } + + const afterCookies = (await debug.sendCommand('Network.getAllCookies')).cookies + clearTimeout(timeout) + writeFileSync(resultPath, JSON.stringify({ + beforePartitionKey: beforeChips.partitionKey, + electronGetSawPartitionKey, + removeCalls, + removeCallsBeforeThrow, + snapshotCalls, + restoreCalls, + importedRemoveCalls, + replaceError, + chipsAfterReplace, + chipsAfter: project(afterCookies, 'chips-auth'), + plainAfter: project(afterCookies, 'plain') + })) + debug.detach() + window.destroy() + app.exit(0) +} + +run().catch((error) => { + writeFileSync(resultPath, JSON.stringify({ step: currentStep, error: String(error && error.stack ? error.stack : error) })) + app.exit(1) +}) +` +} + +async function runFixture(mode: FixtureMode): Promise { + const root = mkdtempSync(join(tmpdir(), `orca-replace-partition-rollback-${mode}-`)) + fixtureRoots.push(root) + const bundlePath = join(root, 'cookie-replace-rollback.cjs') + const bundleEntryPath = join(root, 'cookie-replace-rollback.ts') + const resultPath = join(root, 'result.json') + const fixturePath = join(root, 'main.cjs') + writeFileSync( + bundleEntryPath, + [ + `export { openCookieClearStore } from ${JSON.stringify(join(process.cwd(), 'src/main/browser/browser-cookie-clear-store.ts'))}`, + `export { replaceCookiesForImportedDomains } from ${JSON.stringify(join(process.cwd(), 'src/main/browser/browser-cookie-import-policy.ts'))}` + ].join('\n') + ) + await buildVite({ + configFile: false, + logLevel: 'silent', + build: { + emptyOutDir: false, + lib: { + entry: bundleEntryPath, + formats: ['cjs'], + fileName: () => 'cookie-replace-rollback.cjs' + }, + outDir: root, + target: 'node20', + rollupOptions: { external: ['electron', /^node:/] } + } + }) + writeFileSync(fixturePath, buildFixtureMain(bundlePath, resultPath, mode)) + const { ELECTRON_RUN_AS_NODE: _electronRunAsNode, ...env } = process.env + const electronArgs = [fixturePath, `--user-data-dir=${join(root, 'profile')}`] + const executable = process.platform === 'linux' ? 'xvfb-run' : electronBinary + const args = + process.platform === 'linux' + ? ['--auto-servernum', electronBinary, ...electronArgs, '--no-sandbox'] + : electronArgs + const run = spawnSync(executable, args, { + encoding: 'utf8', + env, + timeout: 60_000 + }) + const fixtureResult = existsSync(resultPath) ? readFileSync(resultPath, 'utf8') : 'no result' + expect(run.error).toBeUndefined() + expect(run.status, `${fixtureResult}\n${run.stdout}\n${run.stderr}`).toBe(0) + return JSON.parse(fixtureResult) as FixtureResult +} + +describe('partitioned cookies under a failed replace-imported-domains import', () => { + // Why (STA-4097): this rollback undoes deletions the import already made to the user's own + // cookies. Rebuilding them with cookies.set dropped partitionKey silently, so a CHIPS cookie + // came back as an ordinary one and no restart recovered it. + it('site A: restores a CHIPS cookie with its partition after a later removal rejects', async () => { + const result = await runFixture('site-a') + + expect(result.beforePartitionKey).toEqual(EXPECTED_PARTITION_KEY) + // Why: without this the fixture could pass on a cookie that was never partitioned. + expect(result.electronGetSawPartitionKey).toBe(false) + // Why: assert the rollback was reached, not just that the jar looks right at the end. + expect(result.snapshotCalls).toBe(1) + expect(result.removeCallsBeforeThrow).toBeGreaterThanOrEqual(1) + expect(result.restoreCalls).toBe(1) + expect(result.replaceError).toContain('forced removal failure after chips was removed') + expect(result.chipsAfter).toEqual([ + { name: 'chips-auth', value: 'keep-me', partitionKey: EXPECTED_PARTITION_KEY } + ]) + expect(result.plainAfter).toEqual([{ name: 'plain', value: 'original' }]) + }, 90_000) + + it('site B: restores a CHIPS cookie with its partition after a mid-import set failure', async () => { + const result = await runFixture('site-b') + + expect(result.beforePartitionKey).toEqual(EXPECTED_PARTITION_KEY) + expect(result.electronGetSawPartitionKey).toBe(false) + expect(result.snapshotCalls).toBe(1) + // Why: the replace really emptied the scope, so the restore below is doing the work. + expect(result.chipsAfterReplace).toEqual([]) + expect(result.importedRemoveCalls).toBe(1) + expect(result.restoreCalls).toBe(1) + expect(result.chipsAfter).toEqual([ + { name: 'chips-auth', value: 'keep-me', partitionKey: EXPECTED_PARTITION_KEY } + ]) + expect(result.plainAfter).toEqual([{ name: 'plain', value: 'original' }]) + }, 90_000) +}) diff --git a/src/main/browser/browser-cookie-import-replacement.test.ts b/src/main/browser/browser-cookie-import-replacement.test.ts index a727a8275f5..d0e79ec1c7f 100644 --- a/src/main/browser/browser-cookie-import-replacement.test.ts +++ b/src/main/browser/browser-cookie-import-replacement.test.ts @@ -3,15 +3,21 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { appGetPathMock, clearPendingCookieImportMock, + disposeClearStoreMock, execFileSyncMock, + restoreClearIdentitiesMock, sessionFromPartitionMock, - setPendingCookieImportMock + setPendingCookieImportMock, + snapshotClearIdentitiesMock } = vi.hoisted(() => ({ appGetPathMock: vi.fn(), clearPendingCookieImportMock: vi.fn(), + disposeClearStoreMock: vi.fn(), execFileSyncMock: vi.fn(), + restoreClearIdentitiesMock: vi.fn(), sessionFromPartitionMock: vi.fn(), - setPendingCookieImportMock: vi.fn() + setPendingCookieImportMock: vi.fn(), + snapshotClearIdentitiesMock: vi.fn() })) vi.mock('./browser-session-registry', () => ({ @@ -26,6 +32,7 @@ vi.mock('electron', () => ({ dialog: { showOpenDialog: vi.fn() }, session: { fromPartition: sessionFromPartitionMock } })) +// Why: snapshot/restore are spies, not no-ops, so a rollback that never ran cannot pass as one. vi.mock('./browser-cookie-clear-store', () => ({ openCookieClearStore: (targetSession: { cookies: { @@ -35,10 +42,9 @@ vi.mock('./browser-cookie-clear-store', () => ({ }) => ({ get: (filter: object) => targetSession.cookies.get(filter), remove: (url: string, name: string) => targetSession.cookies.remove(url, name), - snapshotClearIdentities: async (items: { cookie: Record; url: string }[]) => - items.map(({ cookie, url }) => ({ url, ...cookie })), - restoreClearIdentities: async () => undefined, - dispose: () => undefined + snapshotClearIdentities: snapshotClearIdentitiesMock, + restoreClearIdentities: restoreClearIdentitiesMock, + dispose: disposeClearStoreMock }) })) @@ -48,6 +54,16 @@ import { mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' +beforeEach(() => { + snapshotClearIdentitiesMock + .mockReset() + .mockImplementation(async (items: { cookie: Record; url: string }[]) => + items.map(({ cookie: entry, url }) => ({ url, ...entry })) + ) + restoreClearIdentitiesMock.mockReset().mockResolvedValue(undefined) + disposeClearStoreMock.mockReset() +}) + describe('validated cookie replacement', () => { let cookiesGetMock: ReturnType let cookiesRemoveMock: ReturnType @@ -139,8 +155,17 @@ describe('validated cookie replacement', () => { expect(cookiesSetMock).not.toHaveBeenCalled() }) - it('restores the previous snapshot when an incoming cookie is rejected', async () => { + // Why (STA-4097): this rollback puts back the user's ORIGINAL cookies, which the import had + // already deleted. cookies.get drops partitionKey and cookies.set ignores it, so rebuilding + // them through the Electron API resurrected CHIPS cookies unpartitioned. The undo now travels + // back through the CDP identities, which are the only thing that carries the partition. + it('restores the previous cookies through CDP identities when an incoming cookie is rejected', async () => { + const partitionKey = { topLevelSite: 'https://top.example', hasCrossSiteAncestor: true } cookiesGetMock.mockResolvedValue([cookie('.example.com', 'existing')]) + snapshotClearIdentitiesMock.mockImplementation( + async (items: { cookie: Record; url: string }[]) => + items.map(({ cookie: entry, url }) => ({ url, ...entry, partitionKey })) + ) cookiesSetMock .mockResolvedValue(undefined) .mockResolvedValueOnce(undefined) @@ -157,16 +182,35 @@ describe('validated cookie replacement', () => { ['https://example.com/', 'existing'], ['https://example.com/', 'first'] ]) - expect(cookiesSetMock).toHaveBeenLastCalledWith({ - url: 'https://example.com/', - name: 'existing', - value: 'old', - domain: '.example.com', - path: '/', - secure: true, - httpOnly: undefined, - sameSite: 'unspecified' - }) + expect(restoreClearIdentitiesMock).toHaveBeenCalledOnce() + expect(restoreClearIdentitiesMock.mock.calls[0]?.[0]).toEqual([ + { url: 'https://example.com/', ...cookie('.example.com', 'existing'), partitionKey } + ]) + // Why: cookies.set is only ever the two imported cookies — a third call would mean the + // partition-dropping reconstruction came back. + expect(cookiesSetMock.mock.calls.map(([details]) => details.name)).toEqual(['first', 'second']) + expect(disposeClearStoreMock).toHaveBeenCalledOnce() + }) + + // Why: restoreClearIdentities attaches a debugger before it iterates, so calling it with an + // empty restore set would create a hidden BrowserWindow to put nothing back. + it('does not reach for CDP when the rollback has nothing to restore', async () => { + cookiesGetMock.mockResolvedValue([]) + cookiesSetMock + .mockResolvedValue(undefined) + .mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error('cookie rejected')) + const filePath = writeCookies([ + { domain: '.example.com', name: 'first', value: 'new', secure: true }, + { domain: '.example.com', name: 'second', value: 'new', secure: true } + ]) + + const result = await importCookiesFromFile(filePath, 'persist:test') + + expect(result.ok).toBe(false) + // Why: the imported cookie is still removed — that half of the rollback is lossless. + expect(cookiesRemoveMock.mock.calls).toEqual([['https://example.com/', 'first']]) + expect(restoreClearIdentitiesMock).not.toHaveBeenCalled() }) it('fails closed when existing cookies cannot be replaced', async () => { diff --git a/src/main/browser/browser-cookie-import.ts b/src/main/browser/browser-cookie-import.ts index b54badd0593..2e2f50dbdd1 100644 --- a/src/main/browser/browser-cookie-import.ts +++ b/src/main/browser/browser-cookie-import.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines -- Why: cookie import is one pipeline (detect → decrypt → stage → swap) that must stay together to keep encryption/schema/staging in sync. */ -import { app, type BrowserWindow, type Cookie, dialog, session } from 'electron' +import { app, type BrowserWindow, dialog, session } from 'electron' import { execFileSync } from 'node:child_process' import { createDecipheriv, pbkdf2Sync, randomUUID } from 'node:crypto' import { @@ -81,8 +81,8 @@ import { normalizeCookieDomain, normalizeCookieImportDomain, replaceCookiesForImportedDomains, - restoreImportedDomainCookies, - type CookieImportMode + type CookieImportMode, + type ReplacedImportedDomainCookies } from './browser-cookie-import-policy' import { removeTransplantableCookies, withCookieClearLock } from './browser-cookie-import-clear' import { openCookieClearStore } from './browser-cookie-clear-store' @@ -594,97 +594,112 @@ async function importValidatedCookies( let importedCount = 0 let skipped = totalInput - importableCookies.length const domainSet = new Set() - let replacedCookies: Cookie[] | null = null + let replaced: ReplacedImportedDomainCookies | null = null + // Why (STA-4097): the rollback below has to put back cookies this import already deleted, and + // only CDP identities carry partitionKey — rebuilding them with cookies.set drops it silently. + const cookieClearStore = + mode === 'replace-imported-domains' && importableCookies.length > 0 + ? openCookieClearStore(targetSession) + : null - if (mode === 'replace-imported-domains' && importableCookies.length > 0) { - try { - replacedCookies = await replaceCookiesForImportedDomains( - targetSession.cookies, - importableCookies.map((cookie) => cookie.domain) - ) - diag(` removed ${replacedCookies.length} existing cookies in imported domain scopes`) - } catch (err) { - diag(` existing cookie replacement failed: ${summarizeCookieImportError(err)}`) + try { + if (cookieClearStore) { + try { + replaced = await replaceCookiesForImportedDomains( + cookieClearStore, + importableCookies.map((cookie) => cookie.domain) + ) + diag(` removed ${replaced.removed.length} existing cookies in imported domain scopes`) + } catch (err) { + diag(` existing cookie replacement failed: ${summarizeCookieImportError(err)}`) + return { + ok: false, + reason: reasonWithDiagLog('Could not replace existing cookies for the imported sites.') + } + } + } + + // Why: Electron's cookies.set() rejects any non-printable-ASCII byte; strip as a safety net. + const stripNonPrintable = (s: string): string => s.replace(/[^\x20-\x7E]/g, '') + const importedCookieKeys: { url: string; name: string }[] = [] + let setFailure: unknown = null + + for (const cookie of importableCookies) { + try { + // Why: Chromium rejects __Host- cookies unless they omit domain and use path=/. + const isHostPrefixed = cookie.name.startsWith('__Host-') + const path = isHostPrefixed ? '/' : cookie.path + await targetSession.cookies.set({ + url: cookie.url, + name: cookie.name, + value: stripNonPrintable(cookie.value), + ...(isHostPrefixed ? {} : { domain: cookie.domain }), + path, + secure: cookie.secure, + httpOnly: cookie.httpOnly, + sameSite: cookie.sameSite, + expirationDate: cookie.expirationDate + }) + const removalUrl = new URL(cookie.url) + removalUrl.pathname = path.startsWith('/') ? path : '/' + importedCookieKeys.push({ url: removalUrl.toString(), name: cookie.name }) + importedCount++ + // Why: surface only the domain (never name/value/path) so the summary doesn't leak secret cookie data. + const cleanDomain = cookie.domain.startsWith('.') ? cookie.domain.slice(1) : cookie.domain + domainSet.add(cleanDomain) + } catch (err) { + skipped++ + setFailure = err + if (skipped <= 5) { + // Find the exact offending character position and code + const val = cookie.value + let badInfo = 'none found' + for (let i = 0; i < val.length; i++) { + const code = val.charCodeAt(i) + if (code < 0x20 || code > 0x7e) { + badInfo = `pos=${i} char=U+${code.toString(16).padStart(4, '0')}` + break + } + } + diag( + ` cookie.set FAILED: domain=${cookie.domain} name=${cookie.name} valLen=${val.length} badChar=${badInfo} err=${String(err)}` + ) + } + if (replaced) { + break + } + } + } + + // Why: replaced is only ever set alongside cookieClearStore, which owns the CDP restore. + if (setFailure && replaced && cookieClearStore) { + const rollbackFailures: unknown[] = [] + for (const cookie of importedCookieKeys.toReversed()) { + try { + await targetSession.cookies.remove(cookie.url, cookie.name) + } catch (err) { + rollbackFailures.push(err) + } + } + // Why: restoreClearIdentities attaches the debugger before it iterates, so an empty + // restore set would spin up a hidden BrowserWindow to put nothing back. + if (replaced.identities.length > 0) { + try { + await cookieClearStore.restoreClearIdentities(replaced.identities.toReversed()) + } catch (err) { + rollbackFailures.push(err) + } + } + if (rollbackFailures.length > 0) { + diag(` cookie replacement rollback failed: ${rollbackFailures.length} operation(s)`) + } return { ok: false, - reason: reasonWithDiagLog('Could not replace existing cookies for the imported sites.') + reason: reasonWithDiagLog('Could not safely replace cookies for the imported sites.') } } - } - - // Why: Electron's cookies.set() rejects any non-printable-ASCII byte; strip as a safety net. - const stripNonPrintable = (s: string): string => s.replace(/[^\x20-\x7E]/g, '') - const importedCookieKeys: { url: string; name: string }[] = [] - let setFailure: unknown = null - - for (const cookie of importableCookies) { - try { - // Why: Chromium rejects __Host- cookies unless they omit domain and use path=/. - const isHostPrefixed = cookie.name.startsWith('__Host-') - const path = isHostPrefixed ? '/' : cookie.path - await targetSession.cookies.set({ - url: cookie.url, - name: cookie.name, - value: stripNonPrintable(cookie.value), - ...(isHostPrefixed ? {} : { domain: cookie.domain }), - path, - secure: cookie.secure, - httpOnly: cookie.httpOnly, - sameSite: cookie.sameSite, - expirationDate: cookie.expirationDate - }) - const removalUrl = new URL(cookie.url) - removalUrl.pathname = path.startsWith('/') ? path : '/' - importedCookieKeys.push({ url: removalUrl.toString(), name: cookie.name }) - importedCount++ - // Why: surface only the domain (never name/value/path) so the summary doesn't leak secret cookie data. - const cleanDomain = cookie.domain.startsWith('.') ? cookie.domain.slice(1) : cookie.domain - domainSet.add(cleanDomain) - } catch (err) { - skipped++ - setFailure = err - if (skipped <= 5) { - // Find the exact offending character position and code - const val = cookie.value - let badInfo = 'none found' - for (let i = 0; i < val.length; i++) { - const code = val.charCodeAt(i) - if (code < 0x20 || code > 0x7e) { - badInfo = `pos=${i} char=U+${code.toString(16).padStart(4, '0')}` - break - } - } - diag( - ` cookie.set FAILED: domain=${cookie.domain} name=${cookie.name} valLen=${val.length} badChar=${badInfo} err=${String(err)}` - ) - } - if (replacedCookies) { - break - } - } - } - - if (setFailure && replacedCookies) { - const rollbackFailures: unknown[] = [] - for (const cookie of importedCookieKeys.toReversed()) { - try { - await targetSession.cookies.remove(cookie.url, cookie.name) - } catch (err) { - rollbackFailures.push(err) - } - } - try { - await restoreImportedDomainCookies(targetSession.cookies, replacedCookies) - } catch (err) { - rollbackFailures.push(err) - } - if (rollbackFailures.length > 0) { - diag(` cookie replacement rollback failed: ${rollbackFailures.length} operation(s)`) - } - return { - ok: false, - reason: reasonWithDiagLog('Could not safely replace cookies for the imported sites.') - } + } finally { + cookieClearStore?.dispose() } diag( From e2e7768cc819ee4690f6eb61175e75de6633d2ed Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:18:08 -0700 Subject: [PATCH 12/42] fix(terminal): deliver Ctrl+C when a non-Latin input source rewrites the key (#14462) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With a non-Latin input source the OS reports the layout's own glyph for `key` — a Hangul jamo on Korean 2-Set, Cyrillic es on Russian — while `code` stays KeyC. The interrupt policy read `key` for identity and only consulted `code` when `key` was empty or Unidentified, so a jamo short-circuited it to false: the press missed the ETX path and was CSI-u encoded instead, leaving a TUI running. Trust `key` only when it is a Latin letter, which keeps a Dvorak remap of C authoritative. Otherwise ask the layout map what the physical key produces unmodified: an IME layered over a Latin layout answers 'c', and over a Dvorak base answers 'j', which correctly declines. When the map is itself non-Latin it cannot answer either, so fall back to physical position — how terminals have always resolved control chords. Fixes #14460 --- .../xterm-bypass-policy-interrupt.test.ts | 90 ++++++++++++++++++- .../terminal-pane/xterm-bypass-policy.ts | 28 +++++- 2 files changed, 115 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts b/src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts index 4223ccf13f8..328ffc95739 100644 --- a/src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts +++ b/src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it } from 'vitest' +import { _setLayoutMapForTests } from '../../lib/keyboard-layout/layout-base-character' import { shouldHandleTerminalInterruptKeyboardEvent, shouldSuppressTerminalInterruptKeyup, @@ -21,6 +22,9 @@ function event(overrides: Partial): XtermBypassEvent { } } +// The layout map is module-level cache; leaving one set would leak into later cases. +afterEach(() => _setLayoutMapForTests(null)) + describe('shouldHandleTerminalInterruptKeyboardEvent', () => { it('exports the ETX byte used for terminal interrupts', () => { expect(TERMINAL_INTERRUPT_INPUT).toBe('\x03') @@ -103,6 +107,90 @@ describe('shouldHandleTerminalInterruptKeyboardEvent', () => { ).toBe(false) }) + // #14460: with a non-Latin input source the OS reports the layout's own glyph for `key` + // (Hangul jamo on Korean 2-Set, Cyrillic es on Russian), while `code` stays KeyC. A + // non-Latin script cannot express a control chord in `key`, so `code` is the only signal + // that survives — and without this, Ctrl+C misses the ETX path and gets CSI-u encoded + // instead, leaving a TUI running. + it.each([ + ['Korean 2-Set', 'ㅊ'], + ['Russian', 'с'], + ['Greek', 'ψ'], + ['Hangul syllable', '차'] + ])('handles Ctrl+C when %s reports a non-Latin logical key', (_layout, key) => { + expect( + shouldHandleTerminalInterruptKeyboardEvent(event({ key, code: 'KeyC', ctrlKey: true }), { + isMac: true, + hasSelection: false + }) + ).toBe(true) + }) + + it('suppresses the matching keyup for a non-Latin Ctrl+C', () => { + expect( + shouldSuppressTerminalInterruptKeyup( + event({ type: 'keyup', key: 'ㅊ', code: 'KeyC', ctrlKey: true }) + ) + ).toBe(true) + }) + + // The paired negative, and the reason this cannot simply prefer `code`: a Latin layout that + // moves letters around (Dvorak) reports a real ASCII letter, and that letter is authoritative. + it('still ignores a Latin layout that maps KeyC to a different ASCII letter', () => { + expect( + shouldHandleTerminalInterruptKeyboardEvent(event({ key: 'j', code: 'KeyC', ctrlKey: true }), { + isMac: true, + hasSelection: false + }) + ).toBe(false) + }) + + // And a non-Latin key on some other physical key must not become an interrupt. + it('does not handle a non-Latin logical key on a physical key other than KeyC', () => { + expect( + shouldHandleTerminalInterruptKeyboardEvent(event({ key: 'ㅁ', code: 'KeyA', ctrlKey: true }), { + isMac: true, + hasSelection: false + }) + ).toBe(false) + }) + + // An IME sits on top of a Latin layout, so the layout map still answers for the physical key. + // Consulting it is what keeps the non-Latin path precise rather than merely positional. + it('uses the layout map when an IME is layered over a Latin layout', () => { + _setLayoutMapForTests(new Map([['KeyC', 'c']])) + expect( + shouldHandleTerminalInterruptKeyboardEvent(event({ key: 'ㅊ', code: 'KeyC', ctrlKey: true }), { + isMac: true, + hasSelection: false + }) + ).toBe(true) + }) + + // The case positional matching alone would get wrong: Korean layered over Dvorak, where the + // physical KeyC is not the user's C. The layout map says so, and the interrupt declines. + it('declines when the layout map shows the physical key is not C', () => { + _setLayoutMapForTests(new Map([['KeyC', 'j']])) + expect( + shouldHandleTerminalInterruptKeyboardEvent(event({ key: 'ㅊ', code: 'KeyC', ctrlKey: true }), { + isMac: true, + hasSelection: false + }) + ).toBe(false) + }) + + // A true non-Latin *layout* (not an IME) has a non-Latin map too, so it cannot answer either. + // Fall back to physical position, which is how terminals resolve control chords. + it('falls back to the physical key when the layout map is itself non-Latin', () => { + _setLayoutMapForTests(new Map([['KeyC', 'с']])) + expect( + shouldHandleTerminalInterruptKeyboardEvent(event({ key: 'с', code: 'KeyC', ctrlKey: true }), { + isMac: true, + hasSelection: false + }) + ).toBe(true) + }) + it('does not handle modified Ctrl+C chords', () => { expect( shouldHandleTerminalInterruptKeyboardEvent( diff --git a/src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts b/src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts index 18a50129e9e..d09ffd4b039 100644 --- a/src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts +++ b/src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts @@ -1,4 +1,5 @@ import { keybindingMatchesInput } from '../../../../shared/keybindings' +import { getLayoutBaseCharacterForCode } from '../../lib/keyboard-layout/layout-base-character' import { isTerminalImeCandidateDigitKeyEvent, isTerminalImeCandidateSelectionKeyEvent @@ -151,10 +152,33 @@ export function shouldPreventDefaultTerminalImeCandidateKey( ) } +/** + * A logical key a Latin layout could have produced. Only then is `key` authoritative: + * Dvorak moving `c` elsewhere is a real remap and must be honoured. + */ +function isLatinLetterKey(normalizedKey: string): boolean { + return normalizedKey.length === 1 && normalizedKey >= 'a' && normalizedKey <= 'z' +} + function isTerminalInterruptCKey(event: XtermBypassEvent): boolean { const normalizedKey = event.key.toLowerCase() - const logicalKeyAvailable = normalizedKey !== '' && normalizedKey !== 'unidentified' - return logicalKeyAvailable ? normalizedKey === 'c' : event.code === 'KeyC' || event.keyCode === 67 + if (isLatinLetterKey(normalizedKey)) { + return normalizedKey === 'c' + } + // A non-Latin input source reports its own glyph here — a Hangul jamo on Korean 2-Set, + // Cyrillic es on Russian — and cannot express a control chord in `key` at all. Ask the + // layout map what this physical key produces unmodified: for an IME layered over a Latin + // layout that answers `c`, and for a Dvorak base it answers `j`, which correctly declines. + const layoutBaseKey = event.code + ? getLayoutBaseCharacterForCode(event.code)?.toLowerCase() + : undefined + if (layoutBaseKey !== undefined && isLatinLetterKey(layoutBaseKey)) { + return layoutBaseKey === 'c' + } + // Why the physical fallback: on a true non-Latin *layout* the map is non-Latin too, so it + // cannot answer the question either. Terminals resolve control chords by physical position, + // so KeyC is the interrupt. Empty and Unidentified land here as they always did. + return event.code === 'KeyC' || event.keyCode === 67 } function isPlainCtrlC(event: XtermBypassEvent): boolean { From 08027d0d237608acccce793ed460fe97e94c6202 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:35:41 -0700 Subject: [PATCH 13/42] refactor(sidebar): split WorktreeList into a worktree-list module (#14465) WorktreeList.tsx was 6.8k lines behind an `eslint-disable max-lines`. Break it into `sidebar/worktree-list/`: the container keeps store wiring and composition, the virtualized viewport keeps layout, and the drag, reveal, virtualization, row-model, and row-render concerns each get their own file. Every file now fits the oxlint budget, so the suppression and its baseline entry are gone. Behaviour-preserving. The only deliberate cleanups are duplicate branches folded into shared helpers (drop-preview state updates, status-hover fallback, the two identical scroll-to-index reveal branches) and a dead sticky-header-index ref. Tests that asserted on WorktreeList.tsx source text or imported its named helpers now point at the module that owns them. --- config/max-lines-baseline.txt | 1 - .../WorktreeList.lineage-child-card.test.ts | 4 +- .../src/components/sidebar/WorktreeList.tsx | 6848 +---------------- .../sidebar/project-group-header-dom.test.ts | 14 +- .../sidebar/worktree-keyboard-cycle.test.ts | 4 +- .../sidebar/worktree-list-groups.test.ts | 11 +- .../worktree-list-imported-rows.test.ts | 11 +- .../worktree-list-scroll-adjustment.test.ts | 8 +- .../worktree-list-visible-refresh.test.ts | 2 +- .../FolderPathStatusIndicator.tsx | 47 + .../worktree-list/HostSectionHeader.tsx | 161 + .../SidebarWorktreeListDialogs.tsx | 118 + .../SidebarWorktreeListEmptyState.tsx | 36 + .../VirtualizedWorktreeViewport.tsx | 411 + .../worktree-list/active-descendant-option.ts | 67 + .../folder-workspace-virtual-row.tsx | 129 + .../imported-worktrees-card-visibility.ts | 9 + .../worktree-list/mounted-row-reveal.ts | 35 + .../pending-sidebar-reveal-inputs.ts | 116 + .../pointer-drop-status-target.ts | 56 + .../project-group-header-actions.tsx | 132 + .../worktree-list/render-row-item-rows.ts | 13 + .../render-row-worktree-lookup.ts | 79 + .../sidebar/worktree-list/renderable-rows.ts | 40 + .../worktree-list/repo-header-event-guards.ts | 31 + .../repo-header-project-actions.tsx | 227 + .../worktree-list/sidebar-editable-target.ts | 19 + .../sidebar-row-reveal-ancestors.ts | 112 + .../stable-empty-store-slices.ts | 9 + .../use-agent-send-target-worktree.ts | 63 + .../use-effective-collapsed-groups.ts | 90 + .../use-folder-workspace-path-status-rows.ts | 128 + .../use-group-toggle-with-scroll-anchor.ts | 30 + .../use-pending-sidebar-reveal.ts | 288 + .../use-primary-active-worktree-row.ts | 70 + .../use-project-group-dialogs.ts | 172 + .../use-reused-array-identity.ts | 10 + .../use-sidebar-external-worktree-cards.ts | 187 + .../use-sidebar-host-visible-scope.ts | 69 + .../use-sidebar-reveal-highlight.ts | 63 + .../use-sidebar-reveal-requests.ts | 125 + .../worktree-list/use-sidebar-section-rows.ts | 260 + .../use-sidebar-worktree-filters.ts | 106 + .../use-sidebar-worktree-selection.ts | 140 + .../use-sidebar-worktree-sort-order.ts | 207 + .../use-virtual-row-measurement-sync.ts | 116 + .../use-visible-pr-refresh-reporting.ts | 130 + .../use-visible-sidebar-worktrees.ts | 134 + .../use-workspace-status-row-drag.ts | 147 + .../use-worktree-document-drop.ts | 115 + .../use-worktree-drag-runtime.ts | 123 + .../use-worktree-drag-session.ts | 244 + .../use-worktree-lineage-drop-commit.ts | 127 + .../use-worktree-list-keyboard-navigation.ts | 120 + .../use-worktree-list-virtualizer.ts | 159 + .../use-worktree-native-drag-autoscroll.ts | 108 + .../worktree-list/use-worktree-native-drag.ts | 231 + .../use-worktree-pointer-drag-autoscroll.ts | 79 + ...use-worktree-pointer-drag-window-events.ts | 95 + .../use-worktree-pointer-drag.ts | 259 + .../use-worktree-sidebar-header-drag.ts | 222 + ...use-worktree-sidebar-scroll-suppression.ts | 83 + .../use-worktree-status-mutations.ts | 201 + .../virtual-row-dom-attributes.ts | 15 + .../virtualized-worktree-viewport-props.ts | 116 + .../worktree-list/worktree-drag-groups.ts | 72 + .../worktree-drop-commit-context.ts | 43 + .../worktree-list/worktree-list-item-row.tsx | 251 + .../worktree-notice-virtual-rows.tsx | 116 + .../worktree-list/worktree-option-dom.ts | 58 + .../worktree-pointer-drag-commit.ts | 159 + .../worktree-pointer-drag-flush.ts | 172 + .../worktree-list/worktree-row-drag-state.ts | 132 + .../worktree-section-header-row.tsx | 393 + .../worktree-sidebar-scroll-tuning.ts | 42 + .../worktree-virtual-row-context.ts | 149 + .../worktree-virtual-row-dispatch.tsx | 262 + .../worktree-visibility-menu-label.ts | 13 + .../worktree-visible-refresh-listener.ts | 4 + .../worktree-scroll-to-current-button.test.ts | 2 +- .../xterm-bypass-policy-interrupt.test.ts | 33 +- 81 files changed, 8767 insertions(+), 6716 deletions(-) create mode 100644 src/renderer/src/components/sidebar/worktree-list/FolderPathStatusIndicator.tsx create mode 100644 src/renderer/src/components/sidebar/worktree-list/HostSectionHeader.tsx create mode 100644 src/renderer/src/components/sidebar/worktree-list/SidebarWorktreeListDialogs.tsx create mode 100644 src/renderer/src/components/sidebar/worktree-list/SidebarWorktreeListEmptyState.tsx create mode 100644 src/renderer/src/components/sidebar/worktree-list/VirtualizedWorktreeViewport.tsx create mode 100644 src/renderer/src/components/sidebar/worktree-list/active-descendant-option.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/folder-workspace-virtual-row.tsx create mode 100644 src/renderer/src/components/sidebar/worktree-list/imported-worktrees-card-visibility.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/mounted-row-reveal.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/pending-sidebar-reveal-inputs.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/pointer-drop-status-target.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/project-group-header-actions.tsx create mode 100644 src/renderer/src/components/sidebar/worktree-list/render-row-item-rows.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/render-row-worktree-lookup.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/renderable-rows.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/repo-header-event-guards.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/repo-header-project-actions.tsx create mode 100644 src/renderer/src/components/sidebar/worktree-list/sidebar-editable-target.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/sidebar-row-reveal-ancestors.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/stable-empty-store-slices.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-agent-send-target-worktree.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-effective-collapsed-groups.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-folder-workspace-path-status-rows.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-group-toggle-with-scroll-anchor.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-pending-sidebar-reveal.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-primary-active-worktree-row.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-project-group-dialogs.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-reused-array-identity.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-sidebar-external-worktree-cards.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-sidebar-host-visible-scope.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-sidebar-reveal-highlight.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-sidebar-reveal-requests.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-sidebar-section-rows.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-sidebar-worktree-filters.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-sidebar-worktree-selection.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-sidebar-worktree-sort-order.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-virtual-row-measurement-sync.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-visible-pr-refresh-reporting.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-visible-sidebar-worktrees.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-workspace-status-row-drag.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-worktree-document-drop.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-worktree-drag-runtime.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-worktree-drag-session.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-worktree-lineage-drop-commit.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-worktree-list-keyboard-navigation.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-worktree-list-virtualizer.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-worktree-native-drag-autoscroll.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-worktree-native-drag.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-worktree-pointer-drag-autoscroll.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-worktree-pointer-drag-window-events.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-worktree-pointer-drag.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-worktree-sidebar-header-drag.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-worktree-sidebar-scroll-suppression.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/use-worktree-status-mutations.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/virtual-row-dom-attributes.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/virtualized-worktree-viewport-props.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/worktree-drag-groups.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/worktree-drop-commit-context.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/worktree-list-item-row.tsx create mode 100644 src/renderer/src/components/sidebar/worktree-list/worktree-notice-virtual-rows.tsx create mode 100644 src/renderer/src/components/sidebar/worktree-list/worktree-option-dom.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/worktree-pointer-drag-commit.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/worktree-pointer-drag-flush.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/worktree-row-drag-state.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/worktree-section-header-row.tsx create mode 100644 src/renderer/src/components/sidebar/worktree-list/worktree-sidebar-scroll-tuning.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/worktree-virtual-row-context.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/worktree-virtual-row-dispatch.tsx create mode 100644 src/renderer/src/components/sidebar/worktree-list/worktree-visibility-menu-label.ts create mode 100644 src/renderer/src/components/sidebar/worktree-list/worktree-visible-refresh-listener.ts diff --git a/config/max-lines-baseline.txt b/config/max-lines-baseline.txt index 4375803952e..839595375eb 100644 --- a/config/max-lines-baseline.txt +++ b/config/max-lines-baseline.txt @@ -234,7 +234,6 @@ inline src/renderer/src/components/sidebar/RemoteFileBrowser.tsx inline src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx inline src/renderer/src/components/sidebar/WorktreeContextMenu.tsx inline src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts -inline src/renderer/src/components/sidebar/WorktreeList.tsx inline src/renderer/src/components/sidebar/use-workspace-kanban-area-selection.ts inline src/renderer/src/components/sidebar/worktree-list-groups.test.ts inline src/renderer/src/components/sidebar/worktree-list-groups.ts diff --git a/src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts b/src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts index 756c99d8522..a8c6045463d 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts +++ b/src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts @@ -8,7 +8,7 @@ import type { Repo } from '../../../../shared/repo-types' import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' import type { Worktree } from '../../../../shared/worktree/types' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' -import type * as WorktreeListModule from './WorktreeList' +import { getPinnedWorktreeRevealCollapsedGroupKeys } from './worktree-list/sidebar-row-reveal-ancestors' import { cloneDefaultWorkspaceStatuses } from '../../../../shared/workspace-statuses' const mockStore = vi.hoisted(() => ({ @@ -21,7 +21,6 @@ type WorktreeListComponent = React.ComponentType<{ }> let WorktreeList: WorktreeListComponent -let getPinnedWorktreeRevealCollapsedGroupKeys: typeof WorktreeListModule.getPinnedWorktreeRevealCollapsedGroupKeys function makeFolderWorkspacePathStatusMockState(): Record { return { @@ -828,7 +827,6 @@ describe('WorktreeList lineage child card renderer', () => { beforeAll(async () => { const module = await import('./WorktreeList') WorktreeList = module.default as WorktreeListComponent - getPinnedWorktreeRevealCollapsedGroupKeys = module.getPinnedWorktreeRevealCollapsedGroupKeys }, 60_000) it('renders project group headers when repos import before worktree rows load', async () => { diff --git a/src/renderer/src/components/sidebar/WorktreeList.tsx b/src/renderer/src/components/sidebar/WorktreeList.tsx index 138f36a3a2f..dbbee941a4f 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.tsx @@ -1,5208 +1,42 @@ -/* eslint-disable max-lines */ -import React, { useMemo, useCallback, useRef, useState, useEffect, useLayoutEffect } from 'react' -import { toast } from 'sonner' -import { - measureElement as measureVirtualElementSize, - useVirtualizer -} from '@tanstack/react-virtual' -import type { Range } from '@tanstack/react-virtual' -import { - AlertTriangle, - ChevronDown, - CircleX, - Ellipsis, - Eye, - FolderInput, - FolderPlus, - FolderX, - Loader2, - Plus, - Server, - ServerOff, - Shapes, - SlidersHorizontal, - Trash2 -} from 'lucide-react' +import React, { useCallback, useMemo } from 'react' import { useAppStore } from '@/store' -import { createLineageToggleHandlerCache } from './worktree-lineage-toggle-handler-cache' -import { reuseArrayIfEqual } from './worktree-agent-row-selectors' import { useShallow } from 'zustand/react/shallow' -import type { AppState } from '@/store/types' import { - getAllWorktreesFromState, useAllWorktrees, useProjectHostSetupProjection, useRepoMap, useWorktreeMap } from '@/store/selectors' -import WorktreeCard, { type ActiveSurfaceVariant } from './WorktreeCard' -import { WorktreeSidebarDropIndicator } from './WorktreeSidebarDropIndicator' -import { - getProjectGroupHeaderSectionEndByGroupId, - getRepoHeaderSectionEndByRepoId -} from './worktree-header-section-boundaries' -import { folderWorkspaceToWorktree } from '../../../../shared/folder-workspace-worktree' -import { PendingWorktreeRow } from './PendingWorktreeRow' -import { SUPPRESS_WORKTREE_LIST_SCROLL_ADJUSTMENT_EVENT } from './WorktreeCardAgents' -import { Button } from '@/components/ui/button' -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuTrigger -} from '@/components/ui/dropdown-menu' -import { cn } from '@/lib/utils' -import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' import type { ProjectGroup } from '../../../../shared/project-group-types' import type { Repo } from '../../../../shared/repo-types' -import type { ProjectOrderBy } from '../../../../shared/ui-chrome-types' -import type { WorkspaceLineage, WorktreeLineage } from '../../../../shared/worktree/lineage-types' -import type { WorktreeMeta } from '../../../../shared/worktree/meta-types' -import type { - WorkspaceStatus, - WorkspaceStatusDefinition, - Worktree -} from '../../../../shared/worktree/types' -import { DEFAULT_SHOW_SLEEPING_WORKSPACES } from '../../../../shared/constants' -import { buildWorktreeComparator, compareWorktreeSortLabel } from './smart-sort' -import { - buildAttentionByWorktree, - hasFreshAttributedAgentStatus, - type SmartClass, - type WorktreeAttention -} from './smart-attention' -import { track } from '@/lib/telemetry' -import { tabHasLivePty } from '@/lib/tab-has-live-pty' -import { deriveRunningAgentSendTargets } from '@/lib/running-agent-targets' -import { rightSidebarShowsPullRequestData } from '@/lib/right-sidebar-visibility' -import { - type Row, - type ProjectGroupingModel, - type WorktreeGroupBy, - ALL_GROUP_KEY, - PINNED_GROUP_KEY, - buildRows, - getProjectGroupHeaderKey, - getGroupKeysForWorktree, - getLineageGroupKey, - getPinnedWorktreeDisplayPolicy, - type PinnedWorktreeDisplayPolicy -} from './worktree-list-groups' -import { - buildLineageRowRekeyMap, - estimateRenderRowSize, - extractWorktreeVirtualRowIndexes, - getActiveStickyIndexesForScroll, - getRenderRowKey, - getStickyHeaderIndexes, - getVirtualRowTransform, - pruneStaleVirtualRowElementCache, - shouldUseHeaderTopSpacing, - WORKTREE_SIDEBAR_VIRTUAL_ROW_GAP, - type RenderRow -} from './worktree-list-virtual-rows' -import { - revealElementInScrollContainer, - WORKTREE_SIDEBAR_REVEAL_TOP_INSET -} from './worktree-sidebar-reveal' -import { - createPendingRevealScroll, - isRevealScrollSettling, - type PendingRevealScroll -} from './worktree-sidebar-reveal-scroll-settle' -import { - getWorkspaceStatus, - getWorkspaceStatusFromGroupKey, - getWorkspaceStatusGroupKey, - hasWorkspaceDragData, - readWorkspaceDragDataIds -} from './workspace-status' -import { useWorkspaceStatusDocumentDrop } from './use-workspace-status-drop' -import { - computeClearFilterActions, - computeVisibleWorktreeIds, - setVisibleWorktreeIds, - sidebarHasActiveFilters -} from './visible-worktrees' -import { - EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT, - filterFolderWorkspacesFromOtherDevices, - getPairedDeviceIdsByEnvironment -} from './workspace-creator-visibility' -import { - getCyclicProjectedWorktreeLineageIds, - getWorktreeLineageAncestors -} from './worktree-lineage-projection' -import { getWorktreeIdsWithLiveAgent } from '@/lib/worktree-activity-state' -import { getEmptyProjectPlaceholderRepoIds } from './empty-project-placeholder-repos' -import { - getVisibleWorktreeBrowserActivityTabs, - getVisibleWorktreeTerminalActivityTabs -} from './visible-worktree-activity-inputs' -import { selectWorktreeListReviewCacheInputs } from './worktree-list-review-cache-inputs' -import { - VIRTUALIZED_SCROLL_ANCHOR_RECORD_EVENT, - useVirtualizedScrollAnchor, - type VirtualizedScrollAnchor -} from '@/hooks/useVirtualizedScrollAnchor' -import { activateAndRevealWorktree } from '@/lib/worktree-activation' -import { useFolderWorkspacePathStatusCacheExpiryTick } from '@/lib/folder-workspace-path-status-cache-expiry' -import { useWorktreeListScrollToTop } from './use-worktree-list-scroll-to-top' -import { WorktreeListScrollToTopButton } from './WorktreeListScrollToTopButton' -import { - getFolderWorkspacePathStatusDescription, - getFolderWorkspacePathStatusTitle -} from '@/lib/folder-workspace-path-status' -import { getShortcutPlatform } from '@/lib/shortcut-platform' -import { - SCROLL_TO_CURRENT_WORKSPACE_REVEAL_REQUEST_EVENT, - type ScrollToCurrentWorkspaceRevealRequestDetail -} from '@/lib/scroll-to-current-workspace-status' -import { isRepoHeaderActionTarget, useRepoHeaderDrag } from './project-header-drag' -import { - getLogicalRepoOrderRankById, - getSidebarOrderedRepoHeaderIdsByBucket -} from './project-header-drop' -import { useProjectGroupHeaderDrag } from './project-group-header-drag' -import { getSidebarOrderedProjectGroupHeaderIdsByBucket } from './project-group-header-drop' -import { - buildManualOrderUpdatesForGroupDrop, - buildManualOrderUpdatesForVisibleGroups, - expandDraggedWorktreeIdsForVisibleLineage, - shouldWriteManualOrderForGroupDrop, - type WorktreeDragGroup -} from './worktree-manual-order' -import { - buildWorkspaceKanbanSidebarDropUpdates, - clearWorkspaceKanbanSidebarDropTargetVisual, - getWorkspaceKanbanSidebarDropGroups, - getWorkspaceKanbanSidebarDropTarget, - hasWorkspaceKanbanSidebarDropBoard, - isWorkspaceKanbanSidebarDropPointInBoard, - resolveWorkspaceKanbanSidebarFullLaneDropIndex, - updateWorkspaceKanbanSidebarDropTargetVisual -} from './workspace-kanban-sidebar-drop' -import { - resolveWorkspaceKanbanCardDropCommitTarget, - type WorkspaceKanbanCardTrackedDropTarget -} from './workspace-kanban-card-pointer-drag-dom' -import { - getFullDropIndexForWorktreeDragUnit, - getWorktreeDragUnitGroups -} from './worktree-drag-units' -import { - createSidebarDragPreview, - isSidebarPointerDragBlocked, - setSidebarPointerDragDocumentStyles, - updateSidebarDragPreviewPosition -} from './worktree-sidebar-pointer-drag-dom' -import { - getWorktreeSidebarDragAutoscroll, - getWorktreeSidebarDragRectsForGroup, - refreshWorktreeSidebarDragSession, - type WorktreeSidebarDragRect, - type WorktreeSidebarDragSession, - type WorktreeSidebarDragPoint -} from './worktree-sidebar-drag-autoscroll' -import { - getWorktreeSidebarDragGrab, - shouldReevaluateWorktreeSidebarDropAnchor, - type WorktreeSidebarDragGrab, - type WorktreeSidebarDropAnchor -} from './worktree-sidebar-drag-geometry' -import { - computeWorktreeSidebarDropPreview, - resolveWorktreeSidebarStatusDropCommitTarget, - type WorktreeSidebarStatusDropTarget, - type WorktreeSidebarTrackedStatusDropTarget, - type WorktreeSidebarDropPreview -} from './worktree-sidebar-drop-preview' -import { - getReorderedWorktreeIdsToUnnest, - getWorktreeLineageDropTargetId -} from './worktree-lineage-drag-drop' -import { resolveProjectGroupHeaderColor } from './project-header-color' -import { - REPO_HEADER_ACTION_BUTTON_CLASS, - REPO_HEADER_ACTION_REVEAL_CLASS -} from './repo-header-action-button-class' -import { - areWorktreeSelectionsEqual, - getWorktreeSelectionIntent, - pruneWorktreeSelection, - updateWorktreeSelection -} from './worktree-multi-selection' -import { persistWorktreeSortOrderByHost } from '@/lib/worktree-sort-order-persistence' import { getRepoExecutionHostId, - getSettingsFocusedExecutionHostId, - getWorktreeExecutionHostId, - type ExecutionHostId + getSettingsFocusedExecutionHostId } from '../../../../shared/execution-host' -import { getRepoHeaderCreateState } from './repo-header-create-state' -import type { PendingSidebarRowReveal, PendingSidebarWorktreeReveal } from '@/store/slices/ui' -import { getRepositoryIconSectionId } from '@/components/settings/repository-settings-targets' -import { keybindingMatchesAction } from '../../../../shared/keybindings' -import { ProjectGroupNameDialog } from './ProjectGroupNameDialog' -import { ProjectGroupDeleteDialog } from './ProjectGroupDeleteDialog' -import { selectProjectGroupRemovalTargets } from '@/store/slices/project-group-removal-targets' -import { findRepoForHost } from '@/store/slices/repo-host-identity' -import { isGitRepoKind } from '../../../../shared/repo-kind' +import { getActiveSidebarWorkspaceId } from '../../../../shared/workspace-scope' +import { getPinnedWorktreeDisplayPolicy } from './worktree-list-groups' +import { selectWorktreeListReviewCacheInputs } from './worktree-list-review-cache-inputs' +import type { VirtualizedScrollAnchor } from '@/hooks/useVirtualizedScrollAnchor' +import { SidebarWorktreeListDialogs } from './worktree-list/SidebarWorktreeListDialogs' +import { SidebarWorktreeListEmptyState } from './worktree-list/SidebarWorktreeListEmptyState' +import { VirtualizedWorktreeViewport } from './worktree-list/VirtualizedWorktreeViewport' +import { markSidebarWorktreeActiveImmediately } from './worktree-list/worktree-option-dom' import { - effectiveExternalWorktreeVisibility, - isLegacyRepoForExternalWorktreeVisibility -} from '../../../../shared/worktree/ownership' -import { RepoIconGlyph } from '@/components/repo/repo-icon' -import { RepoForkIndicator } from '@/components/repo/repo-fork-indicator' -import ImportedWorktreesVisibilityLine from './ImportedWorktreesVisibilityLine' -import NewExternalWorktreesInboxLine from './NewExternalWorktreesInboxLine' -import SuppressExternalWorktreeInboxDialog from './SuppressExternalWorktreeInboxDialog' -import { - keepImportedWorktreesHiddenCard, - IMPORTED_WORKTREES_KEEP_HIDDEN_ERROR, - showImportedWorktreesCard, - type ImportedWorktreeCardActionState -} from './imported-worktrees-card-actions' -import { - suppressNewExternalWorktreeInbox, - type NewExternalWorktreesInboxActionState -} from './new-external-worktrees-inbox-actions' -import { isEligibleWorktreeParent } from './worktree-parent-candidates' -import { - buildImportedWorktreesCardCandidates, - getHiddenImportedWorktrees -} from './imported-worktrees-card-candidates' -import { buildNewExternalWorktreesInboxCandidates } from './new-external-worktrees-inbox-candidates' -import { - WORKTREE_SECTION_HEADER_PADDING_LEFT, - LINEAGE_CHILDREN_INLINE_OFFSET, - getFolderBackedRepoWorktreeCardContentIndent, - getFolderBackedRepoWorktreeCardSurfaceInset, - getFolderWorkspaceRowGeometry, - getLineageChildrenInlineStyle, - getLineageNestedRowGeometry, - getProjectGroupHeaderPaddingLeft, - getWorktreeCardContentIndent, - getWorktreeCardSurfaceInset -} from './worktree-list-indentation' -import { addHostSectionRows, type HostHeaderRow, type HostSectionRow } from './host-section-rows' -import { orderHostSectionOptions } from './host-section-order' -import { useHostHeaderDrag } from './host-header-drag' -import { buildSidebarHostOptions } from './sidebar-host-options' -import { HostSectionHeaderMenu } from './HostSectionHeaderMenu' -import { ProjectHeaderActions } from './ProjectHeaderActions' -import { translate } from '@/i18n/i18n' -import { folderWorkspaceKey, getActiveSidebarWorkspaceId } from '../../../../shared/workspace-scope' -import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-overrides' -import { - isConfirmedStaleFolderPathStatus, - type FolderWorkspacePathStatus -} from '../../../../shared/folder-workspace-path-status' -import { - getFolderWorkspaceRevealGroupKeys, - getKnownSidebarWorktreeById, - sidebarWorkspaceStillExists -} from './worktree-list-folder-reveal' -import { - filterFolderWorkspacesForVisibleHosts, - filterProjectGroupsForVisibleHosts, - getFolderPathStatusRouteOptionsForRows, - getVisibleSidebarHostIdSet -} from './worktree-list-host-filtering' -import { getFolderWorkspaceCardPrDisplay } from './folder-workspace-card-pr-display' -import { getRenderedWorktreesInSidebarOrder } from './worktree-sidebar-row-preference' -import { getCyclableWorktreeIds, resolveCycledWorktreeId } from './worktree-keyboard-cycle' - -export { - getScrollTopToRevealBounds, - WORKTREE_SIDEBAR_REVEAL_TOP_INSET -} from './worktree-sidebar-reveal' - -type ProjectGroupNameDialogState = - | { type: 'create-from-repo'; repo: Repo } - | { type: 'rename'; groupId: string; currentName: string } - -type ProjectGroupDeleteDialogState = { - groupId: string - groupName: string - removeContainedProjects: boolean -} - -// Why: epoch-driven recomputes often produce arrays whose contents and order are unchanged; reusing the previous identity when element-wise equal keeps downstream memos and React.memo'd cards bailing out. Safe only because elements (Worktree objects / id strings) are immutably REPLACED on change — never wrap arrays of mutated-in-place objects. -function useReusedArrayIdentity(next: T[]): T[] { - const previousRef = useRef(next) - const result = reuseArrayIfEqual(previousRef.current, next) - previousRef.current = result - return result -} - -// Debounce re-sort after a sortEpoch bump so background score changes don't jar row positions. -const SORT_SETTLE_MS = 3_000 -const USER_SCROLL_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS = 500 -const EMPTY_PROJECT_GROUPS: readonly ProjectGroup[] = [] -const EMPTY_AGENT_STATUS_BY_PANE_KEY: AppState['agentStatusByPaneKey'] = {} -const EMPTY_WORKTREE_ID_SET: ReadonlySet = new Set() -const EMPTY_TABS_BY_WORKTREE: AppState['tabsByWorktree'] = {} -const EMPTY_TERMINAL_LAYOUTS_BY_TAB_ID: AppState['terminalLayoutsByTabId'] = {} -const EMPTY_PTY_IDS_BY_TAB_ID: AppState['ptyIdsByTabId'] = {} -const EMPTY_RUNTIME_PANE_TITLES_BY_TAB_ID: AppState['runtimePaneTitlesByTabId'] = {} -const EXPANDING_CARD_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS = 300 -const NOOP_WORKSPACE_BOARD_DRAG_PREVIEW_CALLBACK = (): void => {} -const WORKTREE_SIDEBAR_SCROLL_STYLE: React.CSSProperties = { - // Why: TanStack Virtual owns scroll correction; native overflow anchoring fights it and causes jumps. - overflowAnchor: 'none' -} - -const recordKeyCountCache = new WeakMap, number>() - -export function countRecordKeysByReference(record: Record): number { - const cached = recordKeyCountCache.get(record) - if (cached !== undefined) { - return cached - } - const count = Object.keys(record).length - recordKeyCountCache.set(record, count) - return count -} - -export function shouldAdjustWorktreeSidebarMeasuredRowScroll(args: { - isScrolling: boolean - now: number - suppressUntil: number -}): boolean { - return !args.isScrolling && args.now >= args.suppressUntil -} - -export function resolvePendingSidebarReveal(args: { - targetIndex: number - targetWorktreeStillExists: boolean -}): 'scroll-and-clear' | 'clear' | 'keep-pending' { - if (args.targetIndex !== -1) { - return 'scroll-and-clear' - } - return args.targetWorktreeStillExists ? 'keep-pending' : 'clear' -} - -function isEditableTarget(target: EventTarget | null): boolean { - if (!(target instanceof HTMLElement)) { - return false - } - - // xterm's hidden input textarea isn't a real text field; treating it as one would block sidebar shortcuts. - if (target.classList.contains('xterm-helper-textarea')) { - return false - } - - if (target.isContentEditable) { - return true - } - - return ( - target.closest('input, textarea, select, [contenteditable=""], [contenteditable="true"]') !== - null - ) -} - -function stopRepoHeaderKeyboardToggle(event: React.KeyboardEvent): void { - if (event.key === 'Enter' || event.key === ' ') { - event.stopPropagation() - } -} - -function stopNestedWorktreeCardBubble(event: React.SyntheticEvent): void { - event.stopPropagation() -} - -function handleRepoHeaderActionPointerDown(event: React.PointerEvent): void { - event.stopPropagation() -} - -function handleRepoHeaderCollapseAffordancePointerDown( - event: React.PointerEvent -): void { - // Why: keep collapse-chevron clicks from arming the repo-header row drag. - event.stopPropagation() -} - -function stopRepoHeaderMenuEvent(event: React.SyntheticEvent): void { - event.stopPropagation() -} - -function shouldIgnoreRepoHeaderToggle(event: React.SyntheticEvent): boolean { - return isRepoHeaderActionTarget(event.target, event.currentTarget) -} - -function getWorktreeOptionId(rowKey: string): string { - return `worktree-list-option-${encodeURIComponent(rowKey)}` -} - -function getMountedWorktreeOptions(worktreeId: string, root?: ParentNode | null): HTMLElement[] { - const scope = root ?? document - const result: HTMLElement[] = [] - scope.querySelectorAll('[data-worktree-id]').forEach((element) => { - if (element.dataset.worktreeId === worktreeId) { - result.push(element) - } - }) - return result -} - -function markSidebarWorktreeActiveImmediately(worktreeId: string, primaryRowKey?: string): void { - const sidebar = document.querySelector('[data-worktree-sidebar]') - const nextOptions = getMountedWorktreeOptions(worktreeId, sidebar) - const nextOption = nextOptions[0] - if (!nextOption) { - return - } - - sidebar - ?.querySelectorAll('[role="option"][aria-current="page"]') - .forEach((option) => option.removeAttribute('aria-current')) - - for (const option of nextOptions) { - option.setAttribute('aria-current', 'page') - } - sidebar - ?.querySelectorAll('[data-worktree-card-surface][data-worktree-card-active]') - .forEach((surface) => { - if (!nextOptions.some((option) => option.contains(surface))) { - surface.removeAttribute('data-worktree-card-active') - } - }) - for (const option of nextOptions) { - const activeSurfaceVariant = - primaryRowKey !== undefined - ? option.dataset.worktreeRowKey === primaryRowKey - ? 'primary' - : 'secondary' - : option === nextOption - ? 'primary' - : 'secondary' - const surface = option.matches('[data-worktree-card-surface]') - ? option - : option.querySelector('[data-worktree-card-surface]') - surface?.setAttribute('data-worktree-card-active', activeSurfaceVariant) - } -} - -function revealMountedWorktreeElement( - container: HTMLElement, - worktreeId: string, - behavior: ScrollBehavior, - optionId?: string, - onScrollIssued?: (targetTop: number) => void -): HTMLElement | null { - const element = optionId - ? document.getElementById(optionId) - : getMountedWorktreeOptions(worktreeId, container)[0] - if (!element || !container.contains(element)) { - return null - } - return revealElementInScrollContainer(container, element, behavior, onScrollIssued) - ? element - : null -} - -function revealMountedSidebarRowElement( - container: HTMLElement, - rowKey: string, - behavior: ScrollBehavior, - onScrollIssued?: (targetTop: number) => void -): HTMLElement | null { - const element = document.getElementById(getWorktreeOptionId(rowKey)) - if (!element || !container.contains(element)) { - return null - } - return revealElementInScrollContainer(container, element, behavior, onScrollIssued) - ? element - : null -} - -function getRenderRowSidebarKey(row: RenderRow): string | null { - if (row.type === 'header') { - return row.key - } - if (row.type === 'item') { - return row.rowKey - } - if (row.type === 'folder-workspace') { - return folderWorkspaceKey(row.folderWorkspace.id) - } - if (row.type === 'pending-creation') { - return `pending:${row.creationId}` - } - if (row.type === 'imported-worktrees-card') { - return row.key - } - if (row.type === 'new-external-worktrees-inbox') { - return row.key - } - return null -} - -function rowKeyMatchesRenderRow(row: RenderRow, rowKey: string): boolean { - if (row.type === 'lineage-group') { - return row.rows.some((item) => item.rowKey === rowKey) - } - return getRenderRowSidebarKey(row) === rowKey -} - -function getProjectIdFromHeaderRowKey(rowKey: string): string | null { - if (!rowKey.startsWith('project:')) { - return null - } - const withoutPrefix = rowKey.slice('project:'.length) - const setupSeparator = withoutPrefix.indexOf('::setup:') - return setupSeparator === -1 ? withoutPrefix : withoutPrefix.slice(0, setupSeparator) -} - -function getRepoIdsFromHeaderRowKey( - rowKey: string, - repoMap: Map, - projectGrouping?: ProjectGroupingModel -): string[] { - if (rowKey.startsWith('repo:')) { - return [rowKey.slice('repo:'.length)] - } - const setupMarker = '::setup:' - const setupIndex = rowKey.indexOf(setupMarker) - if (rowKey.startsWith('project:') && setupIndex !== -1) { - return [rowKey.slice(setupIndex + setupMarker.length)] - } - const projectId = getProjectIdFromHeaderRowKey(rowKey) - if (!projectId) { - return [] - } - const repoIds = new Set() - for (const setup of projectGrouping?.projectHostSetups ?? []) { - if (setup.projectId === projectId && repoMap.has(setup.repoId)) { - repoIds.add(setup.repoId) - } - } - const project = projectGrouping?.projects.find((candidate) => candidate.id === projectId) - for (const repoId of project?.sourceRepoIds ?? []) { - if (repoMap.has(repoId)) { - repoIds.add(repoId) - } - } - return [...repoIds] -} - -function getProjectGroupAncestorKeys( - projectGroupId: string | null | undefined, - projectGroups: readonly ProjectGroup[] -): string[] { - const groupsById = new Map(projectGroups.map((group) => [group.id, group])) - const keys: string[] = [] - const seen = new Set() - let currentGroupId = projectGroupId ?? null - while (currentGroupId && !seen.has(currentGroupId)) { - const group = groupsById.get(currentGroupId) - if (!group) { - break - } - seen.add(currentGroupId) - keys.unshift(getProjectGroupHeaderKey(group.id)) - currentGroupId = group.parentGroupId - } - return keys -} - -function getSidebarRowRevealAncestorKeys(args: { - rowKey: string - repoMap: Map - projectGroups: readonly ProjectGroup[] - projectGrouping?: ProjectGroupingModel -}): string[] { - if (args.rowKey.startsWith('project-group:')) { - const groupId = args.rowKey.slice('project-group:'.length) - const group = args.projectGroups.find((candidate) => candidate.id === groupId) - return getProjectGroupAncestorKeys(group?.parentGroupId, args.projectGroups) - } - const keys = new Set() - for (const repoId of getRepoIdsFromHeaderRowKey( - args.rowKey, - args.repoMap, - args.projectGrouping - )) { - const repo = args.repoMap.get(repoId) - for (const key of getProjectGroupAncestorKeys(repo?.projectGroupId, args.projectGroups)) { - keys.add(key) - } - } - return [...keys] -} - -function getWorktreeVisibilityMenuLabel(repo: Repo): string { - const visibility = effectiveExternalWorktreeVisibility( - repo, - isLegacyRepoForExternalWorktreeVisibility(repo) - ) - return visibility === 'show' ? 'Hide non-Orca worktrees' : 'Show hidden worktrees' -} - -const SIDEBAR_POINTER_DRAG_THRESHOLD_PX = 4 -type VirtualizedWorktreeViewportProps = { - rows: HostSectionRow[] - activeWorktreeId: string | null - currentWorktreeId: string | null - groupBy: WorktreeGroupBy - pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy - projectOrderBy: ProjectOrderBy - toggleGroup: (key: string) => void - collapsedGroups: Set - handleCreateForRepo: (projectId: string) => void - handleOpenRepoSettings: (projectId: string, sectionId?: string) => void - handleOpenWorktreeVisibility: (repo: Repo) => void - handleShowImportedWorktrees: (projectId: string) => void - handleKeepImportedWorktreesHidden: (projectId: string) => void - importedWorktreeCardActionState: ReadonlyMap - handleOpenSuppressExternalWorktreeInbox: (projectId: string) => void - newExternalWorktreeInboxActionState: ReadonlyMap - handleRemoveProject: (repo: Repo) => void - handleCreateGroupFromRepo: (repo: Repo) => void - handleMoveProjectToGroup: (repo: Repo, groupId: string) => void - handleRemoveProjectFromGroup: (repo: Repo) => void - handleRenameProjectGroup: (groupId: string, currentName: string) => void - handleDeleteProjectGroup: (groupId: string, groupName: string) => void - handleCreateFolderWorkspace: (projectGroup: ProjectGroup) => void - activeModal: string - pendingRevealWorktree: PendingSidebarWorktreeReveal | null - pendingRevealSidebarRow: PendingSidebarRowReveal | null - clearPendingRevealWorktreeId: () => void - clearPendingRevealSidebarRow: () => void - agentSendTargetWorktreeId: string | null - worktrees: Worktree[] - folderWorkspaces: readonly FolderWorkspace[] - selectedWorktreeIds: ReadonlySet - selectedWorktrees: readonly Worktree[] - onSelectionGesture: (event: React.MouseEvent, worktreeId: string) => boolean - onImmediateWorktreeActivate: (worktreeId: string, rowKey: string | undefined) => void - onContextMenuSelect: ( - event: React.MouseEvent, - worktree: Worktree - ) => readonly Worktree[] - repoMap: Map - defaultHostId: ExecutionHostId - worktreeMap: Map - worktreeLineageById: Record - workspaceLineageByChildKey: Record - // Full canonical repo-id order; must include hidden repos or a reorder silently drops them. - allRepoIds: string[] - onReorderHostSections: (orderedHostIds: ExecutionHostId[]) => void - onHostDragActiveChange: (active: boolean) => void - prCache: AppState['prCache'] | null - hostedReviewCache: AppState['hostedReviewCache'] | null - workspaceStatuses: readonly WorkspaceStatusDefinition[] - projectGrouping?: ProjectGroupingModel - projectGroups?: readonly ProjectGroup[] - onMoveWorktreeToStatus: (worktreeId: string, status: WorkspaceStatus) => void - onMoveWorktreesToStatus: (worktreeIds: readonly string[], status: WorkspaceStatus) => void - onMoveWorktreesToStatusAtIndex: (args: { - worktreeIds: readonly string[] - status: WorkspaceStatus - dropIndex: number - groups: readonly WorktreeDragGroup[] - }) => void - onPinWorktree: (worktreeId: string) => void - onPinWorktrees: (worktreeIds: readonly string[]) => void - onDropWorktreesOnWorkspaceBoard: (args: { - worktreeIds: readonly string[] - status: WorkspaceStatus - dropIndex: number - groups: readonly WorktreeDragGroup[] - }) => void - workspaceBoardOpen: boolean - onWorkspaceBoardDragPreviewStart: () => void - onWorkspaceBoardDragPreviewCommit: () => void - onWorkspaceBoardDragPreviewCancel: () => void - shouldShowWorkspaceBoardDropIndicator: ( - worktreeIds: readonly string[], - status: WorkspaceStatus - ) => boolean - onReorderWorktrees: (args: { - groups: readonly WorktreeDragGroup[] - sourceGroupKey: string - draggedIds: readonly string[] - dropIndex: number - }) => void - // Why: grouping remounts the viewport, add/delete stays mounted; bridge both so the virtualizer never resets to scrollTop 0. - scrollOffsetRef: React.MutableRefObject - scrollAnchorRef: React.MutableRefObject -} - -type WorktreeItemRow = Extract -type FolderWorkspaceItemRow = Extract - -function formatSectionActivityLabel(count: number, label: string): string { - return `${count} ${label}${count === 1 ? '' : 's'}` -} - -function SectionMetricsBadge({ count }: { count: number }): React.JSX.Element { - const totalLabel = formatSectionActivityLabel(count, 'workspace') - - return ( - - - - - {count} - - - - {totalLabel} - - - - ) -} - -function HostHeaderHealthIcon({ - health -}: { - health: HostHeaderRow['health'] -}): React.JSX.Element | null { - // Why: only surface states needing attention; healthy is the silent default. - if (health === 'connecting') { - return - } - if (health === 'blocked' || health === 'error') { - return - } - return null -} - -function getHostHeaderDetail(row: HostHeaderRow): { text: string; isWarning: boolean } | null { - // Why: a blocked compatibility verdict earns a compact warning so the host stands out. - if (row.health === 'blocked') { - return { - text: translate('auto.components.sidebar.WorktreeList.7a8b9c0d1e', 'Update required'), - isWarning: true - } - } - // Why: auth-failed needs a worded status; the health icon alone doesn't tell the user to re-auth. - if (row.connectionStatus === 'auth-failed') { - return { - text: translate( - 'auto.components.sidebar.WorktreeList.hostAuthNeeded', - 'Authentication needed' - ), - isWarning: true - } - } - if (row.health === 'disconnected') { - return { - text: translate('auto.components.sidebar.WorktreeList.hostDisconnected', 'Disconnected'), - isWarning: false - } - } - // Why: show the transport detail only for remote hosts; it's noise under the local label. - if (row.kind !== 'local') { - return { text: row.detail, isWarning: false } - } - return null -} - -function HostSectionHeader({ - row, - onToggle, - onDragPointerDown, - dragging -}: { - row: HostHeaderRow - onToggle: () => void - onDragPointerDown?: (event: React.PointerEvent) => void - dragging?: boolean -}): React.JSX.Element { - const isBlocked = row.health === 'blocked' - const isDisconnected = row.health === 'disconnected' - const detail = getHostHeaderDetail(row) - return ( -
- {/* Why: outlined card + server glyph marks hosts as machines, not mere groups. */} -
{ - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault() - onToggle() - } - }} - > - {isDisconnected ? ( - - ) : ( - - )} - - {/* Why: badge hugs the label (like repo headers) instead of floating by the hover controls. */} -
- - {row.label} - - {detail ? ( - - {detail.text} - - ) : null} - -
-
- -
- - - -
-
- ) -} - -function FolderPathStatusIndicator({ - status -}: { - status: FolderWorkspacePathStatus | null | undefined -}): React.JSX.Element | null { - const title = getFolderWorkspacePathStatusTitle(status) - if (!status || status.exists || !title) { - return null - } - const destructive = isConfirmedStaleFolderPathStatus(status) - return ( - - - - - - - -
-
{title}
-
- {getFolderWorkspacePathStatusDescription(status)} -
-
-
-
- ) -} - -type WorktreeRowDragState = { - draggingWorktreeId: string | null - sourceGroupKey: string | null - dropIndex: number | null - dropIndicatorY: number | null - previewOffsetsByWorktreeId: ReadonlyMap - pointerY: number | null -} - -const EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS: ReadonlyMap = new Map() - -const WORKTREE_ROW_DRAG_INITIAL_STATE: WorktreeRowDragState = { - draggingWorktreeId: null, - sourceGroupKey: null, - dropIndex: null, - dropIndicatorY: null, - previewOffsetsByWorktreeId: EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS, - pointerY: null -} - -type WorktreePointerDrag = { - pointerId: number - sourceRow: HTMLElement - startX: number - startY: number - currentX: number - currentY: number - worktreeId: string - draggedIds: readonly string[] - reorderDraggedIds: readonly string[] - reorderUnitDraggedIds: readonly string[] - sourceGroupKey: string - rects: readonly WorktreeSidebarDragRect[] - active: boolean - preview: HTMLElement | null - previewOffsetX: number - previewOffsetY: number - workspaceBoardDragPreviewRequested: boolean - frameId: number | null - latestBoardDropTarget: WorkspaceKanbanCardTrackedDropTarget | null - latestStatusDropTarget: WorktreeSidebarTrackedStatusDropTarget | null -} - -function areWorktreeDragPreviewOffsetsEqual( - a: ReadonlyMap, - b: ReadonlyMap -): boolean { - if (a === b) { - return true - } - if (a.size !== b.size) { - return false - } - for (const [key, value] of a) { - if (b.get(key) !== value) { - return false - } - } - return true -} - -function updateLatestWorktreeStatusDropTarget( - drag: WorktreePointerDrag, - target: WorktreeSidebarStatusDropTarget & { lineageParentId: string | null }, - preview: WorktreeSidebarDropPreview | null -): void { - drag.latestStatusDropTarget = - target.status || target.isPinDrop || target.lineageParentId - ? { - target, - preview, - x: drag.currentX, - y: drag.currentY - } - : null -} - -function getWorktreeVirtualRowTransform(start: number, previewOffset: number): string { - const base = getVirtualRowTransform(start) - return previewOffset === 0 ? base : `${base} translateY(${previewOffset}px)` -} - -function getPointerDropStatusTarget(args: { - container: HTMLElement - x: number - y: number -}): WorktreeSidebarStatusDropTarget & { lineageParentId: string | null } { - const target = document.elementFromPoint(args.x, args.y) - if (!(target instanceof Element) || !args.container.contains(target)) { - return { status: null, isPinDrop: false, lineageParentId: null } - } - const pinTarget = target.closest('[data-workspace-pin-drop-target]') - if (pinTarget && args.container.contains(pinTarget)) { - return { status: null, isPinDrop: true, lineageParentId: null } - } - const lineageParentId = getWorktreeLineageDropTargetId({ - container: args.container, - target, - pointerY: args.y - }) - const statusTarget = target.closest('[data-workspace-status-drop-target]') - return { - status: - statusTarget && args.container.contains(statusTarget) - ? ((statusTarget.dataset.workspaceStatus as WorkspaceStatus | undefined) ?? null) - : null, - isPinDrop: false, - lineageParentId - } -} - -function shouldPreferSidebarStatusDropTarget(args: { - sourceGroupKey: string - target: WorktreeSidebarStatusDropTarget - workspaceStatuses: readonly WorkspaceStatusDefinition[] -}): boolean { - if (args.target.isPinDrop) { - return true - } - if (!args.target.status) { - return false - } - const sourceStatus = getWorkspaceStatusFromGroupKey(args.sourceGroupKey, args.workspaceStatuses) - // Why: overlapping edge zones — the section under the pointer must win so guide and drop agree. - return sourceStatus !== null && args.target.status !== sourceStatus -} - -function isWorktreeItemRow(row: HostSectionRow): row is WorktreeItemRow { - return row.type === 'item' -} - -export function renderRowContainsWorktree(row: RenderRow, worktreeId: string | null): boolean { - if (worktreeId === null) { - return false - } - if (row.type === 'folder-workspace') { - return folderWorkspaceKey(row.folderWorkspace.id) === worktreeId - } - if (row.type === 'lineage-group') { - return row.rows.some((item) => item.worktree.id === worktreeId) - } - return row.type === 'item' && row.worktree.id === worktreeId -} - -function isPinnedWorktreeRow(row: WorktreeItemRow): boolean { - return row.sectionKey === PINNED_GROUP_KEY -} - -function getRenderRowWorktreeItem(row: RenderRow, worktreeId: string): WorktreeItemRow | null { - if (row.type === 'lineage-group') { - return row.rows.find((item) => item.worktree.id === worktreeId) ?? null - } - return row.type === 'item' && row.worktree.id === worktreeId ? row : null -} - -function getRenderRowOptionId( - row: RenderRow | undefined, - worktreeId?: string | null -): string | undefined { - if (!row) { - return undefined - } - if (row.type === 'lineage-group') { - const targetRow = worktreeId ? row.rows.find((item) => item.worktree.id === worktreeId) : null - return getWorktreeOptionId((targetRow ?? row.rows[0])?.rowKey ?? row.key) - } - if (row.type === 'item') { - return getWorktreeOptionId(row.rowKey) - } - if (row.type === 'folder-workspace') { - return getWorktreeOptionId(folderWorkspaceKey(row.folderWorkspace.id)) - } - return undefined -} - -function getActiveDescendantOptionId(args: { - activeWorktreeId: string | null - primaryActiveRowKey?: string - pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy - renderRows: readonly RenderRow[] - virtualItems: readonly { index: number }[] -}): string | undefined { - if (args.activeWorktreeId === null) { - return undefined - } - if (args.primaryActiveRowKey) { - const primaryOptionId = getWorktreeOptionId(args.primaryActiveRowKey) - for (const item of args.virtualItems) { - const row = args.renderRows[item.index] - if (row && getRenderRowOptionId(row, args.activeWorktreeId) === primaryOptionId) { - return primaryOptionId - } - } - } - let fallbackOptionId: string | undefined - for (const item of args.virtualItems) { - const row = args.renderRows[item.index] - if (row && renderRowContainsWorktree(row, args.activeWorktreeId)) { - const optionId = getRenderRowOptionId(row, args.activeWorktreeId) - if (!optionId) { - continue - } - const itemRow = getRenderRowWorktreeItem(row, args.activeWorktreeId) - if ( - args.pinnedDisplayPolicy === 'duplicate-in-groups' && - itemRow && - !isPinnedWorktreeRow(itemRow) - ) { - return optionId - } - fallbackOptionId ??= optionId - } - } - return fallbackOptionId -} - -function findPreferredRenderRowIndexForWorktree( - renderRows: readonly RenderRow[], - worktreeId: string, - pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy -): number { - let fallbackIndex = -1 - for (let index = 0; index < renderRows.length; index++) { - const row = renderRows[index] - if (!renderRowContainsWorktree(row, worktreeId)) { - continue - } - if (fallbackIndex === -1) { - fallbackIndex = index - } - const itemRow = getRenderRowWorktreeItem(row, worktreeId) - if (pinnedDisplayPolicy === 'duplicate-in-groups' && itemRow && !isPinnedWorktreeRow(itemRow)) { - return index - } - } - return fallbackIndex -} - -export function getPinnedWorktreeRevealCollapsedGroupKeys({ - worktree, - collapsedGroups -}: { - worktree: Worktree - collapsedGroups: ReadonlySet -}): string[] { - if (!worktree.isPinned) { - return [] - } - const keys: string[] = [] - // Why: the reveal effect already opens this host; re-returning it would toggle it back closed. - if (collapsedGroups.has(PINNED_GROUP_KEY)) { - keys.push(PINNED_GROUP_KEY) - } - return keys -} - -function uniqueWorktreeIds(ids: readonly string[]): string[] { - return Array.from(new Set(ids)) -} - -function buildRenderableRows(rows: HostSectionRow[]): RenderRow[] { - const renderRows: RenderRow[] = [] - for (let index = 0; index < rows.length; index++) { - const row = rows[index] - if ( - !isWorktreeItemRow(row) || - row.lineageChildCount === 0 || - row.lineageCollapsed || - rows[index + 1]?.type !== 'item' || - (rows[index + 1] as WorktreeItemRow).depth <= row.depth - ) { - renderRows.push(row) - continue - } - - const groupRows: WorktreeItemRow[] = [row] - let cursor = index + 1 - while (cursor < rows.length) { - const child = rows[cursor] - if (!isWorktreeItemRow(child) || child.depth <= row.depth) { - break - } - groupRows.push(child) - cursor++ - } - renderRows.push({ - type: 'lineage-group', - key: `${row.sectionKey}:${getLineageGroupKey(row.worktree.id)}`, - rows: groupRows - }) - index = cursor - 1 - } - return renderRows -} - -// Why: getRenderRowKey lives with the other virtual-row helpers now; keep the -// long-standing import path working for callers that reach for it here. -export { getRenderRowKey } - -export function getWorktreeDragGroups(rows: HostSectionRow[]): WorktreeDragGroup[] { - const groups: WorktreeDragGroup[] = [] - let current: { key: string; ids: string[] } | null = null - const naturalWorktreeIds = new Set( - rows.flatMap((row) => - row.type === 'item' && row.sectionKey !== PINNED_GROUP_KEY ? [row.worktree.id] : [] - ) - ) - - for (const row of rows) { - if (row.type === 'header') { - current = { key: row.key, ids: [] } - groups.push({ key: current.key, worktreeIds: current.ids }) - continue - } - if ( - row.type === 'host-header' || - row.type === 'imported-worktrees-card' || - row.type === 'new-external-worktrees-inbox' || - row.type === 'pending-creation' || - row.type === 'folder-workspace' - ) { - continue - } - if (row.sectionKey === PINNED_GROUP_KEY && naturalWorktreeIds.has(row.worktree.id)) { - continue - } - if (!current) { - current = { key: ALL_GROUP_KEY, ids: [] } - groups.push({ key: current.key, worktreeIds: current.ids }) - } - current.ids.push(row.worktree.id) - } - - return groups.filter((group) => group.worktreeIds.length > 0) -} - -export function canKeepImportedWorktreesHidden( - row: Extract, - actionState: ImportedWorktreeCardActionState | undefined -): boolean { - return row.placement === 'repo-group' && actionState?.forceVisible !== true -} - -export function getWorktreeDragIndexes(rows: readonly HostSectionRow[]): { - groupKeyByRowKey: Map - groupIndexByRowKey: Map -} { - const groupKeyByRowKey = new Map() - const groupIndexByRowKey = new Map() - const groupIndexes = new Map() - const naturalWorktreeIds = new Set( - rows.flatMap((row) => - row.type === 'item' && row.sectionKey !== PINNED_GROUP_KEY ? [row.worktree.id] : [] - ) - ) - for (const row of rows) { - if (row.type === 'header') { - groupIndexes.set(row.key, 0) - continue - } - if (row.type !== 'item') { - continue - } - if (row.sectionKey === PINNED_GROUP_KEY && naturalWorktreeIds.has(row.worktree.id)) { - continue - } - const index = groupIndexes.get(row.sectionKey) ?? 0 - groupKeyByRowKey.set(row.rowKey, row.sectionKey) - groupIndexByRowKey.set(row.rowKey, index) - groupIndexes.set(row.sectionKey, index + 1) - } - return { groupKeyByRowKey, groupIndexByRowKey } -} - -function getVirtualRowIndex(element: Element): number | null { - const index = Number.parseInt(element.getAttribute('data-index') ?? '', 10) - return Number.isNaN(index) ? null : index -} - -function getVirtualRowKey(element: Element): string | null { - return element.getAttribute('data-worktree-virtual-row-key') -} - -const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewport({ - rows, - activeWorktreeId, - currentWorktreeId, - groupBy, - pinnedDisplayPolicy, - projectOrderBy, - toggleGroup, - collapsedGroups, - handleCreateForRepo, - handleOpenRepoSettings, - handleOpenWorktreeVisibility, - handleShowImportedWorktrees, - handleKeepImportedWorktreesHidden, - importedWorktreeCardActionState, - handleOpenSuppressExternalWorktreeInbox, - newExternalWorktreeInboxActionState, - handleRemoveProject, - handleCreateGroupFromRepo, - handleMoveProjectToGroup, - handleRemoveProjectFromGroup, - handleRenameProjectGroup, - handleDeleteProjectGroup, - handleCreateFolderWorkspace, - activeModal, - pendingRevealWorktree, - pendingRevealSidebarRow, - clearPendingRevealWorktreeId, - clearPendingRevealSidebarRow, - agentSendTargetWorktreeId, - worktrees, - folderWorkspaces, - selectedWorktreeIds, - selectedWorktrees, - onSelectionGesture, - onImmediateWorktreeActivate, - onContextMenuSelect, - repoMap, - defaultHostId, - worktreeMap, - worktreeLineageById, - workspaceLineageByChildKey, - allRepoIds, - onReorderHostSections, - onHostDragActiveChange, - prCache, - hostedReviewCache, - workspaceStatuses, - projectGrouping, - projectGroups = EMPTY_PROJECT_GROUPS, - onMoveWorktreeToStatus, - onMoveWorktreesToStatus, - onMoveWorktreesToStatusAtIndex, - onPinWorktree, - onPinWorktrees, - onDropWorktreesOnWorkspaceBoard, - workspaceBoardOpen, - onWorkspaceBoardDragPreviewStart, - onWorkspaceBoardDragPreviewCommit, - onWorkspaceBoardDragPreviewCancel, - shouldShowWorkspaceBoardDropIndicator, - onReorderWorktrees, - scrollOffsetRef, - scrollAnchorRef -}: VirtualizedWorktreeViewportProps) { - const scrollRef = useRef(null) - // Why: callback-ref only mutates scrollRef; state re-runs the scroll-to-top listener attach. - const [scrollElement, setScrollElement] = useState(null) - const suppressMeasurementAdjustmentUntilRef = useRef(0) - const directScrollInputUntilRef = useRef(0) - const [dragOverStatus, setDragOverStatus] = useState(null) - const [pinDragOver, setPinDragOver] = useState(false) - const [nativeLineageDropTargetId, setNativeLineageDropTargetId] = useState(null) - const [worktreeDragState, setWorktreeDragState] = useState( - WORKTREE_ROW_DRAG_INITIAL_STATE - ) - const [pendingRevealRetryTick, setPendingRevealRetryTick] = useState(0) - const [documentVisibilityRevision, setDocumentVisibilityRevision] = useState(0) - const [highlightedRevealRowKey, setHighlightedRevealRowKey] = useState(null) - const setRenamingWorktreeId = useAppStore((s) => s.setRenamingWorktreeId) - const assignWorktreeParent = useAppStore((s) => s.assignWorktreeParent) - const updateWorktreeLineage = useAppStore((s) => s.updateWorktreeLineage) - const cyclicLineageIds = useMemo( - () => getCyclicProjectedWorktreeLineageIds(worktreeLineageById, worktreeMap), - [worktreeLineageById, worktreeMap] - ) - const worktreeDragSessionRef = useRef(null) - // Why: cross-group hovers hit-test a group the session never captured, so hold - // that group's drop decision separately or a card expanding in the target group - // moves the insertion line under a still pointer. - const statusDropAnchorsRef = useRef>(new Map()) - const worktreePointerDragRef = useRef(null) - const worktreePointerAutoscrollFrameIdRef = useRef(null) - const worktreePointerAutoscrollLastFrameTimeRef = useRef(null) - const worktreeNativeAutoscrollFrameIdRef = useRef(null) - const worktreeNativeAutoscrollLastFrameTimeRef = useRef(null) - const worktreeNativeLatestPointRef = useRef(null) - const pendingRevealRetryRef = useRef<{ worktreeId: string; count: number } | null>(null) - const pendingRowRevealRetryRef = useRef<{ rowKey: string; count: number } | null>(null) - const pendingRevealFrameIdsRef = useRef>(new Set()) - const pendingRevealScrollRef = useRef(null) - const revealHighlightFrameIdRef = useRef(null) - const revealHighlightTimeoutRef = useRef(null) - const cancelPendingRevealFrames = useCallback(() => { - for (const frameId of pendingRevealFrameIdsRef.current) { - window.cancelAnimationFrame(frameId) - } - pendingRevealFrameIdsRef.current.clear() - }, []) - const schedulePendingRevealFrame = useCallback((callback: FrameRequestCallback) => { - const frameId = window.requestAnimationFrame((time) => { - pendingRevealFrameIdsRef.current.delete(frameId) - callback(time) - }) - pendingRevealFrameIdsRef.current.add(frameId) - }, []) - const clearRevealHighlightFrame = useCallback(() => { - if (revealHighlightFrameIdRef.current !== null) { - window.cancelAnimationFrame(revealHighlightFrameIdRef.current) - revealHighlightFrameIdRef.current = null - } - }, []) - const clearRevealHighlightTimeout = useCallback(() => { - if (revealHighlightTimeoutRef.current !== null) { - window.clearTimeout(revealHighlightTimeoutRef.current) - revealHighlightTimeoutRef.current = null - } - }, []) - const flashRevealedRow = useCallback( - (rowKey: string) => { - clearRevealHighlightTimeout() - clearRevealHighlightFrame() - // Why: clear before set restarts the CSS glow when revealing the same row repeatedly. - setHighlightedRevealRowKey(null) - revealHighlightFrameIdRef.current = window.requestAnimationFrame(() => { - revealHighlightFrameIdRef.current = null - setHighlightedRevealRowKey(rowKey) - revealHighlightTimeoutRef.current = window.setTimeout(() => { - revealHighlightTimeoutRef.current = null - setHighlightedRevealRowKey(null) - }, 1500) - }) - }, - [clearRevealHighlightFrame, clearRevealHighlightTimeout] - ) - const suppressWorktreeClickUntilRef = useRef(0) - const hasProjectGroups = projectGroups.length > 0 - const canReorderRepoHeaders = groupBy === 'repo' && projectOrderBy === 'manual' - const canReorderProjectGroupHeaders = groupBy === 'repo' && hasProjectGroups - const moveProjectToGroup = useAppStore((s) => s.moveProjectToGroup) - const updateProjectGroup = useAppStore((s) => s.updateProjectGroup) - const lastVisibleRefreshKeyRef = useRef('') - const reportVisibleGitHubPRRefreshCandidates = useAppStore( - (s) => s.reportVisibleGitHubPRRefreshCandidates - ) - const cardProps = useAppStore((s) => s.worktreeCardProperties) - const rightSidebarShowsPR = useAppStore((s) => rightSidebarShowsPullRequestData(s)) - const keybindings = useAppStore((s) => s.keybindings) - const sshConnectedGeneration = useAppStore((s) => s.sshConnectedGeneration) - const prVisibleRefreshGeneration = useAppStore((s) => s.prVisibleRefreshGeneration) - const settings = useAppStore((s) => s.settings) - const newCardStyle = settings?.experimentalNewWorktreeCardStyle === true - const reorderRepos = useAppStore((s) => s.reorderRepos) - const folderBackedProjectGroupIds = useMemo( - () => - new Set( - projectGroups - .filter((group) => group.createdFrom === 'folder-scan') - .map((group) => group.id) - ), - [projectGroups] - ) - const projectGroupByIdForHeaderDrag = useMemo( - () => new Map(projectGroups.map((group) => [group.id, group])), - [projectGroups] - ) - - useEffect( - () => - installWorktreeVisibleRefreshVisibilityListener(() => { - if (document.visibilityState !== 'visible') { - // Why: row identity may be unchanged after a hidden window; reset the key so PR/CI rows refresh. - lastVisibleRefreshKeyRef.current = '__document_hidden__' - return - } - setDocumentVisibilityRevision((revision) => revision + 1) - }), - [] - ) - - // Why: reorder keeps scrollTop stable; flag direct scroll input so anchor-restore won't chase the moved row (jumpy drop). - const commitRepoReorder = useCallback( - (orderedIds: string[]) => { - const suppressUntil = - window.performance.now() + USER_SCROLL_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS - suppressMeasurementAdjustmentUntilRef.current = suppressUntil - directScrollInputUntilRef.current = suppressUntil - reorderRepos(orderedIds) - }, - [reorderRepos] - ) - const orderedHostIds = useMemo( - () => - rows - .filter((row): row is HostHeaderRow => row.type === 'host-header') - .map((row) => row.hostId), - [rows] - ) - const hostDrag = useHostHeaderDrag({ - orderedHostIds, - onCommit: onReorderHostSections, - getScrollContainer: () => scrollRef.current - }) - useEffect(() => { - onHostDragActiveChange(hostDrag.state.draggingHostId !== null) - }, [hostDrag.state.draggingHostId, onHostDragActiveChange]) - useEffect(() => () => onHostDragActiveChange(false), [onHostDragActiveChange]) - const worktreeDragGroups = useMemo(() => getWorktreeDragGroups(rows), [rows]) - const worktreeDragUnitGroups = useMemo(() => getWorktreeDragUnitGroups(rows), [rows]) - const naturalDragWorktreeIds = useMemo( - () => - new Set( - rows.flatMap((row) => - row.type === 'item' && row.sectionKey !== PINNED_GROUP_KEY ? [row.worktree.id] : [] - ) - ), - [rows] - ) - const worktreeLineageDragRows = useMemo( - () => - rows - .filter((row): row is WorktreeItemRow => row.type === 'item') - .filter( - (row) => - row.sectionKey !== PINNED_GROUP_KEY || !naturalDragWorktreeIds.has(row.worktree.id) - ) - .map((row) => ({ worktreeId: row.worktree.id, depth: row.depth })), - [naturalDragWorktreeIds, rows] - ) - const getReorderDraggedIds = useCallback( - (draggedIds: readonly string[]) => - expandDraggedWorktreeIdsForVisibleLineage(worktreeLineageDragRows, draggedIds), - [worktreeLineageDragRows] - ) - const getReorderUnitDraggedIds = useCallback( - (sourceGroupKey: string, reorderDraggedIds: readonly string[]) => { - const group = worktreeDragUnitGroups.find((candidate) => candidate.key === sourceGroupKey) - if (!group) { - return reorderDraggedIds - } - const unitIds = new Set(group.worktreeIds) - const filtered = reorderDraggedIds.filter((worktreeId) => unitIds.has(worktreeId)) - return filtered.length > 0 ? filtered : reorderDraggedIds - }, - [worktreeDragUnitGroups] - ) - const { groupKeyByRowKey, groupIndexByRowKey } = useMemo( - () => getWorktreeDragIndexes(rows), - [rows] - ) - const refreshWorktreeDragSession = useCallback((): boolean => { - const session = worktreeDragSessionRef.current - const container = scrollRef.current - if (!session || !container) { - return false - } - - const refreshedSession = refreshWorktreeSidebarDragSession({ - session, - groups: worktreeDragGroups, - unitGroups: worktreeDragUnitGroups, - rects: getWorktreeSidebarDragRectsForGroup(container, session.sourceGroupKey) - }) - worktreeDragSessionRef.current = refreshedSession - return refreshedSession !== null - }, [worktreeDragGroups, worktreeDragUnitGroups]) - const computeWorktreeDropForGroup = useCallback( - (args: { - pointerY: number - groupKey: string - rects: readonly WorktreeSidebarDragRect[] - draggedIds: readonly string[] - draggingWorktreeId?: string | null - grab?: WorktreeSidebarDragGrab | null - anchor?: WorktreeSidebarDropAnchor | null - }): WorktreeSidebarDropPreview | null => { - const container = scrollRef.current - if (!container) { - return null - } - const group = worktreeDragUnitGroups.find((candidate) => candidate.key === args.groupKey) - if (!group) { - return null - } - const containerRect = container.getBoundingClientRect() - return computeWorktreeSidebarDropPreview({ - pointerY: args.pointerY, - containerTop: containerRect.top, - scrollTop: container.scrollTop, - rects: args.rects, - groupIds: group.worktreeIds, - draggedIds: args.draggedIds, - draggingWorktreeId: args.draggingWorktreeId, - fallbackGap: WORKTREE_SIDEBAR_VIRTUAL_ROW_GAP, - grab: args.grab, - anchor: args.anchor - }) - }, - [worktreeDragUnitGroups] - ) - const computeWorktreeDrop = useCallback( - (pointerY: number): WorktreeSidebarDropPreview | null => { - const session = worktreeDragSessionRef.current - const container = scrollRef.current - if (!session || !container) { - return null - } - const scrollTop = container.scrollTop - // Why: only real pointer or scroll movement should re-decide the slot; a - // card growing under a still pointer must not move it. - const anchor = shouldReevaluateWorktreeSidebarDropAnchor({ - anchor: session.anchor, - pointerY, - scrollTop - }) - ? null - : session.anchor - const preview = computeWorktreeDropForGroup({ - pointerY, - groupKey: session.sourceGroupKey, - rects: session.rects, - draggedIds: session.reorderUnitDraggedIds, - draggingWorktreeId: session.draggingWorktreeId, - grab: session.grab, - anchor - }) - worktreeDragSessionRef.current = { - ...session, - anchor: preview ? { beforeWorktreeId: preview.dropAnchorId, pointerY, scrollTop } : null - } - return preview - }, - [computeWorktreeDropForGroup] - ) - const computeWorktreeStatusDrop = useCallback( - (args: { - pointerY: number - status: WorkspaceStatus - draggedIds: readonly string[] - }): WorktreeSidebarDropPreview | null => { - const container = scrollRef.current - if (!container) { - return null - } - const groupKey = getWorkspaceStatusGroupKey(args.status) - const session = worktreeDragSessionRef.current - const scrollTop = container.scrollTop - const heldAnchor = statusDropAnchorsRef.current.get(groupKey) ?? null - const anchor = shouldReevaluateWorktreeSidebarDropAnchor({ - anchor: heldAnchor, - pointerY: args.pointerY, - scrollTop - }) - ? null - : heldAnchor - const preview = computeWorktreeDropForGroup({ - pointerY: args.pointerY, - groupKey, - rects: getWorktreeSidebarDragRectsForGroup(container, groupKey), - draggedIds: args.draggedIds, - draggingWorktreeId: session?.draggingWorktreeId ?? null, - grab: session?.grab ?? null, - anchor - }) - if (preview) { - statusDropAnchorsRef.current.set(groupKey, { - beforeWorktreeId: preview.dropAnchorId, - pointerY: args.pointerY, - scrollTop - }) - } else { - statusDropAnchorsRef.current.delete(groupKey) - } - return preview - }, - [computeWorktreeDropForGroup] - ) - const renderRows = useMemo(() => buildRenderableRows(rows), [rows]) - const sidebarRepoHeaderIdsByBucket = useMemo( - () => - getSidebarOrderedRepoHeaderIdsByBucket( - rows.filter((row): row is Row => row.type !== 'host-header') - ), - [rows] - ) - const sidebarProjectGroupHeaderIdsByBucket = useMemo( - () => - getSidebarOrderedProjectGroupHeaderIdsByBucket( - rows.filter((row): row is Row => row.type !== 'host-header'), - projectGroupByIdForHeaderDrag - ), - [projectGroupByIdForHeaderDrag, rows] - ) - const repoHeaderIndexByRepoId = useMemo(() => { - const map = new Map() - for (const repoIds of sidebarRepoHeaderIdsByBucket.values()) { - repoIds.forEach((repoId, index) => { - map.set(repoId, index) - }) - } - return map - }, [sidebarRepoHeaderIdsByBucket]) - const repoHeaderBucketByRepoId = useMemo(() => { - const map = new Map() - for (const [bucketKey, repoIds] of sidebarRepoHeaderIdsByBucket) { - for (const repoId of repoIds) { - map.set(repoId, bucketKey) - } - } - return map - }, [sidebarRepoHeaderIdsByBucket]) - const projectGroupHeaderIndexByGroupId = useMemo(() => { - const map = new Map() - for (const groupIds of sidebarProjectGroupHeaderIdsByBucket.values()) { - groupIds.forEach((groupId, index) => { - map.set(groupId, index) - }) - } - return map - }, [sidebarProjectGroupHeaderIdsByBucket]) - const projectGroupHeaderBucketByGroupId = useMemo(() => { - const map = new Map() - for (const [bucketKey, groupIds] of sidebarProjectGroupHeaderIdsByBucket) { - for (const groupId of groupIds) { - map.set(groupId, bucketKey) - } - } - return map - }, [sidebarProjectGroupHeaderIdsByBucket]) - const commitProjectGroupOrder = useCallback( - (repoId: string, projectGroupId: string | null, order: number) => { - void moveProjectToGroup(repoId, projectGroupId, order) - }, - [moveProjectToGroup] - ) - const commitProjectGroupHeaderOrder = useCallback( - (groupId: string, tabOrder: number) => { - if (!Number.isFinite(tabOrder)) { - return - } - const suppressUntil = - window.performance.now() + USER_SCROLL_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS - suppressMeasurementAdjustmentUntilRef.current = suppressUntil - directScrollInputUntilRef.current = suppressUntil - void updateProjectGroup(groupId, { tabOrder }) - }, - [updateProjectGroup] - ) - // Drag applies only in manual order; still construct the controller inert for stable hook order. - const repoDrag = useRepoHeaderDrag({ - orderedRepoIds: allRepoIds, - sidebarRepoHeaderIdsByBucket, - repoById: repoMap, - usesProjectGroupOrdering: hasProjectGroups, - onCommitRepoOrder: commitRepoReorder, - onCommitProjectGroupOrder: commitProjectGroupOrder, - getScrollContainer: () => scrollRef.current - }) - const projectGroupDrag = useProjectGroupHeaderDrag({ - sidebarProjectGroupHeaderIdsByBucket, - projectGroupById: projectGroupByIdForHeaderDrag, - onCommitProjectGroupTabOrder: commitProjectGroupHeaderOrder, - getScrollContainer: () => scrollRef.current - }) - const [primaryActiveWorktreeRow, setPrimaryActiveWorktreeRow] = useState<{ - worktreeId: string - rowKey: string - } | null>(null) - useEffect(() => { - if (activeWorktreeId === null) { - setPrimaryActiveWorktreeRow(null) - return - } - setPrimaryActiveWorktreeRow((current) => { - if (current === null || current.worktreeId !== activeWorktreeId) { - return null - } - const rowStillVisible = rows.some( - (row) => - row.type === 'item' && - row.worktree.id === current.worktreeId && - row.rowKey === current.rowKey - ) - return rowStillVisible ? current : null - }) - }, [activeWorktreeId, rows]) - const getActiveSurfaceVariant = useCallback( - (row: WorktreeItemRow): ActiveSurfaceVariant => { - if (primaryActiveWorktreeRow?.worktreeId === row.worktree.id) { - return primaryActiveWorktreeRow.rowKey === row.rowKey ? 'primary' : 'secondary' - } - if ( - pinnedDisplayPolicy === 'duplicate-in-groups' && - activeWorktreeId === row.worktree.id && - isPinnedWorktreeRow(row) - ) { - return 'secondary' - } - return 'primary' - }, - [activeWorktreeId, pinnedDisplayPolicy, primaryActiveWorktreeRow] - ) - const handleImmediateWorktreeRowActivate = useCallback( - (worktreeId: string, rowKey: string | undefined): void => { - setPrimaryActiveWorktreeRow(rowKey ? { worktreeId, rowKey } : null) - onImmediateWorktreeActivate(worktreeId, rowKey) - }, - [onImmediateWorktreeActivate] - ) - const firstHeaderIndex = useMemo( - () => renderRows.findIndex((row) => row.type === 'header' || row.type === 'host-header'), - [renderRows] - ) - const repoHeaderSectionEndByRepoId = useMemo( - () => - getRepoHeaderSectionEndByRepoId({ - rows: renderRows, - firstHeaderIndex, - sidebarRepoHeaderIdsByBucket, - repoHeaderBucketByRepoId - }), - [firstHeaderIndex, renderRows, repoHeaderBucketByRepoId, sidebarRepoHeaderIdsByBucket] - ) - const projectGroupHeaderSectionEndByGroupId = useMemo( - () => - getProjectGroupHeaderSectionEndByGroupId({ - rows: renderRows, - firstHeaderIndex, - sidebarProjectGroupHeaderIdsByBucket, - projectGroupHeaderBucketByGroupId - }), - [ - firstHeaderIndex, - projectGroupHeaderBucketByGroupId, - renderRows, - sidebarProjectGroupHeaderIdsByBucket - ] - ) - const firstHeaderIndexRef = useRef(firstHeaderIndex) - firstHeaderIndexRef.current = firstHeaderIndex - const stickyHeaderIndexes = useMemo(() => getStickyHeaderIndexes(renderRows), [renderRows]) - const stickyHeaderIndexesRef = useRef(stickyHeaderIndexes) - stickyHeaderIndexesRef.current = stickyHeaderIndexes - const activeStickyHeaderIndexRef = useRef(null) - const activeStickyHostIndexRef = useRef(null) - const stickyRangeStartIndexRef = useRef(0) - const sshConnectionStates = useAppStore((s) => s.sshConnectionStates) - const { - folderWorkspacePathStatuses, - fetchFolderWorkspacePathStatus, - getFolderWorkspacePathStatusCacheKey, - getFreshFolderWorkspacePathStatus, - activeRuntimeEnvironmentId - } = useAppStore( - useShallow((s) => ({ - folderWorkspacePathStatuses: s.folderWorkspacePathStatuses, - fetchFolderWorkspacePathStatus: s.fetchFolderWorkspacePathStatus, - getFolderWorkspacePathStatusCacheKey: s.getFolderWorkspacePathStatusCacheKey, - getFreshFolderWorkspacePathStatus: s.getFreshFolderWorkspacePathStatus, - activeRuntimeEnvironmentId: s.settings?.activeRuntimeEnvironmentId ?? null - })) - ) - const folderPathStatusRepoMembershipKey = useMemo( - () => - allRepoIds - .map((repoId) => { - const repo = repoMap.get(repoId) - return `${repoId}:${repo?.path ?? ''}:${repo?.projectGroupId ?? ''}:${repo?.connectionId ?? ''}` - }) - .join('\0'), - [allRepoIds, repoMap] - ) - const folderPathStatusSshConnectionKey = useMemo( - () => - [...sshConnectionStates.entries()] - .map(([connectionId, state]) => `${connectionId}:${state.status}`) - .sort() - .join('\0'), - [sshConnectionStates] - ) - const folderPathStatusCacheExpiryTick = useFolderWorkspacePathStatusCacheExpiryTick( - folderWorkspacePathStatuses - ) - const projectGroupByIdForFolderPathStatus = useMemo( - () => new Map(projectGroups.map((group) => [group.id, group])), - [projectGroups] - ) - const folderWorkspaceByIdForFolderPathStatus = useMemo( - () => new Map(folderWorkspaces.map((workspace) => [workspace.id, workspace])), - [folderWorkspaces] - ) - const getFolderPathStatusRouteOptions = useCallback( - (request: Parameters[0]) => - getFolderPathStatusRouteOptionsForRows({ - request, - projectGroupsById: projectGroupByIdForFolderPathStatus, - folderWorkspacesById: folderWorkspaceByIdForFolderPathStatus - }), - [folderWorkspaceByIdForFolderPathStatus, projectGroupByIdForFolderPathStatus] - ) - useEffect(() => { - const requests = new Map< - string, - { - request: Parameters[0] - options?: { runtimeEnvironmentId: string | null } - } - >() - for (const group of projectGroups) { - if (group.parentPath) { - const request = { scope: 'project-group' as const, projectGroupId: group.id } - const options = getFolderPathStatusRouteOptions(request) - requests.set(getFolderWorkspacePathStatusCacheKey(request, options), { request, options }) - } - } - for (const workspace of folderWorkspaces) { - const request = { scope: 'folder-workspace' as const, folderWorkspaceId: workspace.id } - const options = getFolderPathStatusRouteOptions(request) - requests.set(getFolderWorkspacePathStatusCacheKey(request, options), { request, options }) - } - for (const { request, options } of requests.values()) { - void fetchFolderWorkspacePathStatus(request, { force: true, ...options }) - } - }, [ - activeRuntimeEnvironmentId, - fetchFolderWorkspacePathStatus, - folderPathStatusRepoMembershipKey, - folderPathStatusSshConnectionKey, - folderWorkspaces, - getFolderPathStatusRouteOptions, - getFolderWorkspacePathStatusCacheKey, - projectGroups - ]) - const getCachedFolderWorkspacePathStatus = useCallback( - (request: Parameters[0]) => { - const options = getFolderPathStatusRouteOptions(request) - const cacheKey = getFolderWorkspacePathStatusCacheKey(request, options) - // Why: don't let an expired negative status keep folder workspaces disabled while a refresh is in flight. - void folderWorkspacePathStatuses[cacheKey] - void folderPathStatusCacheExpiryTick - return getFreshFolderWorkspacePathStatus(request, options) - }, - [ - folderWorkspacePathStatuses, - folderPathStatusCacheExpiryTick, - getFolderPathStatusRouteOptions, - getFolderWorkspacePathStatusCacheKey, - getFreshFolderWorkspacePathStatus - ] - ) - const renderRowsRef = useRef(renderRows) - renderRowsRef.current = renderRows - const getVirtualItemKey = useCallback( - (index: number) => { - const row = renderRows[index] - if (!row) { - return `__stale_${index}` - } - return getRenderRowKey(row) - }, - [renderRows] - ) - const getExpectedVirtualRowKey = useCallback((element: Element) => { - const index = getVirtualRowIndex(element) - const row = index === null ? undefined : renderRowsRef.current[index] - return row ? getRenderRowKey(row) : null - }, []) - const isCurrentVirtualRowElement = useCallback( - (element: Element) => { - const expectedKey = getExpectedVirtualRowKey(element) - return ( - element.isConnected && - expectedKey !== null && - element.getAttribute('data-worktree-virtual-row-key') === expectedKey - ) - }, - [getExpectedVirtualRowKey] - ) - const measureCurrentVirtualRowElement = useCallback( - ( - element: HTMLDivElement, - entry: ResizeObserverEntry | undefined, - instance: Parameters>[2] - ) => { - if (!isCurrentVirtualRowElement(element)) { - const index = getVirtualRowIndex(element) - const measured = instance.getVirtualItems().find((item) => item.index === index) - // Why: a stale ResizeObserver row after remount would write a wrong height; return current size to no-op it. - return ( - measured?.size ?? - estimateRenderRowSize( - renderRowsRef.current, - index ?? -1, - firstHeaderIndexRef.current, - activeStickyHeaderIndexRef.current - ) - ) - } - const index = getVirtualRowIndex(element) - if ( - index !== null && - (renderRowsRef.current[index]?.type === 'header' || - renderRowsRef.current[index]?.type === 'host-header') - ) { - return estimateRenderRowSize( - renderRowsRef.current, - index, - firstHeaderIndexRef.current, - activeStickyHeaderIndexRef.current - ) - } - return measureVirtualElementSize(element, entry, instance) - }, - [isCurrentVirtualRowElement] - ) - const markScrollMovement = useCallback(() => { - suppressMeasurementAdjustmentUntilRef.current = - window.performance.now() + USER_SCROLL_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS - }, []) - const markDirectScrollInput = useCallback(() => { - const suppressUntil = window.performance.now() + USER_SCROLL_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS - suppressMeasurementAdjustmentUntilRef.current = suppressUntil - directScrollInputUntilRef.current = suppressUntil - }, []) - const { showScrollToTop, scrollToTop } = useWorktreeListScrollToTop({ - scrollElement, - onUserScrollIntent: markDirectScrollInput - }) - const hasDirectScrollInput = useCallback( - () => window.performance.now() < directScrollInputUntilRef.current, - [] - ) - const markRevealScroll = useCallback((targetTop: number) => { - pendingRevealScrollRef.current = createPendingRevealScroll(targetTop, window.performance.now()) - }, []) - const isRevealScrollSettlingNow = useCallback(() => { - const settling = isRevealScrollSettling({ - now: window.performance.now(), - pending: pendingRevealScrollRef.current, - scrollTop: scrollRef.current?.scrollTop ?? 0 - }) - if (!settling) { - pendingRevealScrollRef.current = null - } - return settling - }, []) - // Why: programmatic scrolls keep measurement correction quiet, but only direct input blocks anchor-restore retries. - // A reveal's smooth scroll is the exception: restoring the anchor mid-animation cancels it a few pixels in. - const shouldSkipScrollAnchorRestore = useCallback( - () => - window.performance.now() < directScrollInputUntilRef.current || isRevealScrollSettlingNow(), - [isRevealScrollSettlingNow] - ) - - const virtualizer = useVirtualizer({ - count: renderRows.length, - getScrollElement: () => scrollRef.current, - estimateSize: (index) => - estimateRenderRowSize( - renderRows, - index, - firstHeaderIndex, - activeStickyHeaderIndexRef.current - ), - measureElement: measureCurrentVirtualRowElement, - // Why: TanStack memoizes rangeExtractor by identity; header indexes must be deps or sticky slots go stale. - rangeExtractor: useCallback( - (range: Range) => { - stickyRangeStartIndexRef.current = range.startIndex - return extractWorktreeVirtualRowIndexes({ - range, - stickyHeaderIndexes, - rows: renderRowsRef.current - }) - }, - [stickyHeaderIndexes] - ), - overscan: 10, - gap: WORKTREE_SIDEBAR_VIRTUAL_ROW_GAP, - // Why: the sticky group header lives inside the virtual list, so scroll math needs the same top inset as the DOM reveal. - scrollPaddingStart: WORKTREE_SIDEBAR_REVEAL_TOP_INSET, - isScrollingResetDelay: USER_SCROLL_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS, - // Why: sync-flushing rich card renders in the scroll listener stalls wheel input; async + overscan keeps rows filled. - useFlushSync: false, - // Why: seed scrollOffset from the ref (not 0) so the first getVirtualItems() after remount picks the right rows. - initialOffset: () => scrollOffsetRef.current, - getItemKey: getVirtualItemKey - }) - // Why: TanStack's default correction writes scrollTop while cards remeasure mid-wheel, which feels like rubber-banding. - // TODO(scroll-origin-migration): wall-clock suppression misclassifies under jank; migrate to programmaticScrollMarks + restoreSignal (see CombinedDiffViewer). - virtualizer.shouldAdjustScrollPositionOnItemSizeChange = (_item, _delta, instance) => - shouldAdjustWorktreeSidebarMeasuredRowScroll({ - isScrolling: instance.isScrolling, - now: window.performance.now(), - suppressUntil: suppressMeasurementAdjustmentUntilRef.current - }) - - useEffect(() => { - const handleSuppress = () => { - // Why: let an expanding agent row grow in place instead of TanStack compensating scrollTop. - suppressMeasurementAdjustmentUntilRef.current = - window.performance.now() + EXPANDING_CARD_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS - } - window.addEventListener(SUPPRESS_WORKTREE_LIST_SCROLL_ADJUSTMENT_EVENT, handleSuppress) - return () => { - window.removeEventListener(SUPPRESS_WORKTREE_LIST_SCROLL_ADJUSTMENT_EVENT, handleSuppress) - } - }, []) - - React.useEffect(() => { - if (!pendingRevealWorktree) { - return - } - - if (agentSendTargetWorktreeId !== pendingRevealWorktree.worktreeId) { - const folderGroupKeys = getFolderWorkspaceRevealGroupKeys( - pendingRevealWorktree.worktreeId, - folderWorkspaces, - projectGroups - ) - if (folderGroupKeys.length > 0) { - for (const groupKey of folderGroupKeys) { - if (collapsedGroups.has(groupKey)) { - toggleGroup(groupKey) - } - } - } else { - const targetWorktree = worktrees.find((w) => w.id === pendingRevealWorktree.worktreeId) - const targetRepo = targetWorktree ? repoMap.get(targetWorktree.repoId) : undefined - if (targetWorktree) { - const hostId = getWorktreeExecutionHostId(targetWorktree, targetRepo, defaultHostId) - const hostGroupKey = `host:${hostId}` - if (collapsedGroups.has(hostGroupKey)) { - toggleGroup(hostGroupKey) - } - - for (const parent of getWorktreeLineageAncestors( - targetWorktree, - worktreeLineageById, - worktreeMap - )) { - const lineageGroupKey = getLineageGroupKey(parent.id) - if (collapsedGroups.has(lineageGroupKey)) { - toggleGroup(lineageGroupKey) - } - } - - const groupKeys = - targetWorktree.isPinned && pinnedDisplayPolicy === 'single-location' - ? getPinnedWorktreeRevealCollapsedGroupKeys({ - worktree: targetWorktree, - collapsedGroups - }) - : getGroupKeysForWorktree( - groupBy, - targetWorktree, - repoMap, - prCache, - workspaceStatuses, - settings, - projectGroups, - projectGrouping - ) - for (const groupKey of groupKeys) { - if (collapsedGroups.has(groupKey)) { - toggleGroup(groupKey) - } - } - } - } - } - - let cancelled = false - schedulePendingRevealFrame(() => { - if (cancelled) { - return - } - const targetWorktreeStillExists = sidebarWorkspaceStillExists( - pendingRevealWorktree.worktreeId, - worktrees, - folderWorkspaces - ) - const targetIndex = findPreferredRenderRowIndexForWorktree( - renderRows, - pendingRevealWorktree.worktreeId, - pinnedDisplayPolicy - ) - const outcome = resolvePendingSidebarReveal({ targetIndex, targetWorktreeStillExists }) - if (outcome === 'scroll-and-clear') { - const targetRow = renderRows[targetIndex] - const container = scrollRef.current - const retryExactRevealOnNextFrame = () => { - const previousRetry = pendingRevealRetryRef.current - const nextRetryCount = - previousRetry?.worktreeId === pendingRevealWorktree.worktreeId - ? previousRetry.count + 1 - : 1 - pendingRevealRetryRef.current = { - worktreeId: pendingRevealWorktree.worktreeId, - count: nextRetryCount - } - if (nextRetryCount <= 8) { - schedulePendingRevealFrame(() => { - if (!cancelled) { - setPendingRevealRetryTick((tick) => tick + 1) - } - }) - } else { - pendingRevealRetryRef.current = null - clearPendingRevealWorktreeId() - } - } - const revealedOption = container - ? revealMountedWorktreeElement( - container, - pendingRevealWorktree.worktreeId, - pendingRevealWorktree.behavior, - getRenderRowOptionId(targetRow, pendingRevealWorktree.worktreeId), - markRevealScroll - ) - : null - if (revealedOption) { - if (pendingRevealWorktree.highlight) { - const revealedRowKey = - revealedOption.dataset.worktreeRowKey ?? getRenderRowSidebarKey(targetRow) - if (revealedRowKey) { - flashRevealedRow(revealedRowKey) - } - } - if (pendingRevealWorktree.beginRename) { - setRenamingWorktreeId({ - worktreeId: pendingRevealWorktree.worktreeId, - rowKey: revealedOption.dataset.worktreeRowKey - }) - } - pendingRevealRetryRef.current = null - clearPendingRevealWorktreeId() - return - } - - if (targetRow?.type !== 'lineage-group') { - // Why: virtual indexing can leave the card edge clipped; stage it into the window, then retry the exact DOM reveal. - virtualizer.scrollToIndex(targetIndex, { - align: 'auto', - behavior: 'auto' - }) - retryExactRevealOnNextFrame() - return - } - - // Why: for lineage groups the virtual row is only a staging target; jump into the window, then retry the exact reveal. - virtualizer.scrollToIndex(targetIndex, { - align: 'auto', - behavior: 'auto' - }) - retryExactRevealOnNextFrame() - return - } - if (outcome === 'clear') { - pendingRevealRetryRef.current = null - clearPendingRevealWorktreeId() - } - }) - return () => { - cancelled = true - cancelPendingRevealFrames() - } - }, [ - pendingRevealWorktree, - agentSendTargetWorktreeId, - groupBy, - worktrees, - folderWorkspaces, - repoMap, - prCache, - worktreeLineageById, - worktreeMap, - renderRows, - virtualizer, - clearPendingRevealWorktreeId, - toggleGroup, - collapsedGroups, - defaultHostId, - workspaceStatuses, - settings, - pinnedDisplayPolicy, - projectGrouping, - projectGroups, - pendingRevealRetryTick, - flashRevealedRow, - markRevealScroll, - setRenamingWorktreeId, - schedulePendingRevealFrame, - cancelPendingRevealFrames - ]) - - React.useEffect(() => { - if (!pendingRevealSidebarRow) { - return - } - - const isProjectHeaderTarget = - pendingRevealSidebarRow.rowKey.startsWith('project-group:') || - pendingRevealSidebarRow.rowKey.startsWith('project:') || - pendingRevealSidebarRow.rowKey.startsWith('repo:') - if (isProjectHeaderTarget && groupBy !== 'repo') { - return - } - - let toggledAncestor = false - for (const groupKey of getSidebarRowRevealAncestorKeys({ - rowKey: pendingRevealSidebarRow.rowKey, - repoMap, - projectGroups, - projectGrouping - })) { - if (collapsedGroups.has(groupKey)) { - toggleGroup(groupKey) - toggledAncestor = true - } - } - if (toggledAncestor) { - return - } - - let cancelled = false - const retryPendingReveal = () => { - const previousRetry = pendingRowRevealRetryRef.current - const nextRetryCount = - previousRetry?.rowKey === pendingRevealSidebarRow.rowKey ? previousRetry.count + 1 : 1 - pendingRowRevealRetryRef.current = { - rowKey: pendingRevealSidebarRow.rowKey, - count: nextRetryCount - } - if (nextRetryCount <= 8) { - schedulePendingRevealFrame(() => { - if (!cancelled) { - setPendingRevealRetryTick((tick) => tick + 1) - } - }) - return true - } - return false - } - schedulePendingRevealFrame(() => { - if (cancelled) { - return - } - const targetIndex = renderRows.findIndex((row) => - rowKeyMatchesRenderRow(row, pendingRevealSidebarRow.rowKey) - ) - if (targetIndex === -1) { - if (retryPendingReveal()) { - return - } - pendingRowRevealRetryRef.current = null - clearPendingRevealSidebarRow() - toast.error( - translate( - 'auto.components.sidebar.WorktreeList.sidebarRowMissing', - 'Target no longer exists' - ) - ) - return - } - - const retryExactRevealOnNextFrame = () => { - if (retryPendingReveal()) { - return - } - pendingRowRevealRetryRef.current = null - clearPendingRevealSidebarRow() - } - - const container = scrollRef.current - const revealedElement = container - ? revealMountedSidebarRowElement( - container, - pendingRevealSidebarRow.rowKey, - pendingRevealSidebarRow.behavior, - markRevealScroll - ) - : null - if (revealedElement) { - if (pendingRevealSidebarRow.highlight) { - flashRevealedRow(pendingRevealSidebarRow.rowKey) - } - pendingRowRevealRetryRef.current = null - clearPendingRevealSidebarRow() - return - } - - virtualizer.scrollToIndex(targetIndex, { - align: 'auto', - behavior: 'auto' - }) - retryExactRevealOnNextFrame() - }) - - return () => { - cancelled = true - cancelPendingRevealFrames() - } - }, [ - pendingRevealSidebarRow, - repoMap, - projectGroups, - projectGrouping, - collapsedGroups, - groupBy, - toggleGroup, - renderRows, - virtualizer, - pendingRevealRetryTick, - flashRevealedRow, - markRevealScroll, - clearPendingRevealSidebarRow, - schedulePendingRevealFrame, - cancelPendingRevealFrames - ]) - - const prCacheLen = useAppStore((s) => countRecordKeysByReference(s.prCache)) - const issueCacheLen = useAppStore((s) => countRecordKeysByReference(s.issueCache)) - const renderRowKeySignature = useMemo( - () => renderRows.map(getRenderRowKey).join('\n'), - [renderRows] - ) - const activeRenderRowKeys = useMemo(() => new Set(renderRows.map(getRenderRowKey)), [renderRows]) - const lineageRowRekeys = useMemo(() => buildLineageRowRekeyMap(renderRows), [renderRows]) - const totalSize = virtualizer.getTotalSize() - const virtualItems = virtualizer.getVirtualItems() - const activeStickyIndexes = getActiveStickyIndexesForScroll({ - rows: renderRows, - rangeStartIndex: stickyRangeStartIndexRef.current, - scrollOffset: virtualizer.scrollOffset ?? scrollOffsetRef.current, - stickyHeaderIndexes, - virtualItems - }) - activeStickyHeaderIndexRef.current = activeStickyIndexes.groupIndex - activeStickyHostIndexRef.current = activeStickyIndexes.hostIndex - - const measureMountedRows = useCallback(() => { - virtualizer.elementsCache.forEach((element) => { - if (!isCurrentVirtualRowElement(element)) { - return - } - virtualizer.measureElement(element) - }) - }, [isCurrentVirtualRowElement, virtualizer]) - const measureVirtualRowElement = useCallback( - (element: HTMLDivElement | null) => { - if (!element) { - virtualizer.measureElement(null) - return - } - if (!isCurrentVirtualRowElement(element)) { - return - } - virtualizer.measureElement(element) - }, - [isCurrentVirtualRowElement, virtualizer] - ) - - useLayoutEffect(() => { - pruneStaleVirtualRowElementCache({ - activeRowKeys: activeRenderRowKeys, - virtualizer - }) - // Why: a stale retained element after delete/collapse measures 0px and corrupts the next slot; measure only key-matched rows. - measureMountedRows() - const frameId = window.requestAnimationFrame(measureMountedRows) - return () => window.cancelAnimationFrame(frameId) - }, [ - activeRenderRowKeys, - prCacheLen, - issueCacheLen, - measureMountedRows, - renderRowKeySignature, - virtualizer - ]) - - useVirtualizedScrollAnchor({ - anchorRef: scrollAnchorRef, - getItemElementKey: getVirtualRowKey, - getRowKey: getRenderRowKey, - itemElementSelector: '[data-worktree-virtual-row]', - rekeyedRowKeys: lineageRowRekeys, - rows: renderRows, - scrollElementRef: scrollRef, - scrollOffsetRef, - hasDirectScrollInput, - shouldSkipRestore: shouldSkipScrollAnchorRestore, - totalSize, - virtualizer - }) - - const recordCurrentScrollAnchor = useCallback(() => { - scrollRef.current?.dispatchEvent(new Event(VIRTUALIZED_SCROLL_ANCHOR_RECORD_EVENT)) - }, []) - const toggleGroupWithScrollAnchor = useCallback( - (groupKey: string) => { - recordCurrentScrollAnchor() - toggleGroup(groupKey) - }, - [recordCurrentScrollAnchor, toggleGroup] - ) - // Why: memo'd WorktreeCard needs a per-group-key stable onLineageToggle - // identity to bail out of re-renders; see worktree-lineage-toggle-handler-cache. - const getLineageToggleHandler = useMemo( - () => createLineageToggleHandlerCache(toggleGroupWithScrollAnchor), - [toggleGroupWithScrollAnchor] - ) - - const navigateWorktree = useCallback( - (direction: 'up' | 'down') => { - // Why: cycle over the rows the sidebar actually rendered — collapsing a group - // means "not now", and a rebuilt near-copy would drift from what is on screen - // (host sections, pinned placement, folder workspaces). - const nextWorktreeId = resolveCycledWorktreeId({ - worktreeIds: getCyclableWorktreeIds(rows, pinnedDisplayPolicy), - activeWorktreeId, - direction - }) - if (nextWorktreeId === null) { - return - } - - // Why: keyboard cycling is real navigation; route through the activation helper that records history. - activateAndRevealWorktree(nextWorktreeId) - - const rowIndex = findPreferredRenderRowIndexForWorktree( - renderRows, - nextWorktreeId, - pinnedDisplayPolicy - ) - if (rowIndex !== -1) { - virtualizer.scrollToIndex(rowIndex, { align: 'auto' }) - } - }, - [rows, renderRows, activeWorktreeId, virtualizer, pinnedDisplayPolicy] - ) - - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (activeModal !== 'none' || isEditableTarget(e.target)) { - return - } - - const platform = getShortcutPlatform() - if (keybindingMatchesAction('sidebar.focusWorktreeList', e, platform, keybindings)) { - scrollRef.current?.focus() - e.preventDefault() - return - } - - const direction = keybindingMatchesAction('worktree.navigateUp', e, platform, keybindings) - ? 'up' - : keybindingMatchesAction('worktree.navigateDown', e, platform, keybindings) - ? 'down' - : null - if (direction) { - markDirectScrollInput() - navigateWorktree(direction) - e.preventDefault() - } - } - - window.addEventListener('keydown', handleKeyDown, { capture: true }) - return () => window.removeEventListener('keydown', handleKeyDown, { capture: true }) - }, [activeModal, keybindings, markDirectScrollInput, navigateWorktree]) - - const handleContainerKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { - if (e.target !== e.currentTarget) { - return - } - markDirectScrollInput() - navigateWorktree(e.key === 'ArrowUp' ? 'up' : 'down') - e.preventDefault() - } else if (e.key === 'Enter') { - const helper = document.querySelector( - '.xterm-helper-textarea' - ) as HTMLTextAreaElement | null - if (helper) { - helper.focus() - } - e.preventDefault() - } else if (['PageUp', 'PageDown', 'Home', 'End', ' '].includes(e.key)) { - markDirectScrollInput() - } - }, - [markDirectScrollInput, navigateWorktree] - ) - - const handleScrollPointerDown = useCallback( - (event: React.PointerEvent) => { - const scrollbarWidth = event.currentTarget.offsetWidth - event.currentTarget.clientWidth - if (scrollbarWidth <= 0) { - return - } - const rect = event.currentTarget.getBoundingClientRect() - if (event.clientX >= rect.right - scrollbarWidth) { - markDirectScrollInput() - } - }, - [markDirectScrollInput] - ) - const handleScroll = useCallback(() => { - markScrollMovement() - }, [markScrollMovement]) - - const cancelWorktreePointerAutoscroll = useCallback(() => { - if (worktreePointerAutoscrollFrameIdRef.current !== null) { - window.cancelAnimationFrame(worktreePointerAutoscrollFrameIdRef.current) - worktreePointerAutoscrollFrameIdRef.current = null - } - worktreePointerAutoscrollLastFrameTimeRef.current = null - }, []) - - const cancelWorktreeNativeAutoscroll = useCallback(() => { - if (worktreeNativeAutoscrollFrameIdRef.current !== null) { - window.cancelAnimationFrame(worktreeNativeAutoscrollFrameIdRef.current) - worktreeNativeAutoscrollFrameIdRef.current = null - } - worktreeNativeAutoscrollLastFrameTimeRef.current = null - worktreeNativeLatestPointRef.current = null - }, []) - - const cleanupWorktreePointerDrag = useCallback(() => { - const drag = worktreePointerDragRef.current - cancelWorktreePointerAutoscroll() - setNativeLineageDropTargetId(null) - if (!drag) { - return - } - if (drag.frameId !== null) { - window.cancelAnimationFrame(drag.frameId) - } - drag.preview?.remove() - worktreePointerDragRef.current = null - setSidebarPointerDragDocumentStyles(false) - setDragOverStatus(null) - setPinDragOver(false) - clearWorkspaceKanbanSidebarDropTargetVisual() - onWorkspaceBoardDragPreviewCancel() - }, [cancelWorktreePointerAutoscroll, onWorkspaceBoardDragPreviewCancel]) - - const clearWorktreeDrag = useCallback(() => { - cleanupWorktreePointerDrag() - cancelWorktreeNativeAutoscroll() - worktreeDragSessionRef.current = null - statusDropAnchorsRef.current.clear() - setWorktreeDragState(WORKTREE_ROW_DRAG_INITIAL_STATE) - }, [cancelWorktreeNativeAutoscroll, cleanupWorktreePointerDrag]) - - const setScrollRootRef = useCallback( - (node: HTMLDivElement | null) => { - if (node === null && scrollRef.current !== null) { - // Why: drag previews, autoscroll frames, and reveal snapshots are tied to the scroll root; clear them before it unmounts. - cancelPendingRevealFrames() - clearRevealHighlightFrame() - clearRevealHighlightTimeout() - clearWorktreeDrag() - } - scrollRef.current = node - setScrollElement(node) - }, - [ - cancelPendingRevealFrames, - clearRevealHighlightFrame, - clearRevealHighlightTimeout, - clearWorktreeDrag - ] - ) - - const getEligibleLineageDropTarget = useCallback( - ( - target: WorktreeSidebarStatusDropTarget & { lineageParentId: string | null }, - draggedIds: readonly string[] - ): WorktreeSidebarStatusDropTarget & { lineageParentId: string | null } => { - const parentId = target.lineageParentId - if (!parentId) { - return target - } - const canAssignAll = draggedIds.every((draggedId) => { - const child = worktreeMap.get(draggedId) - if (!child) { - return false - } - const candidateParent = worktreeMap.get(parentId) - return Boolean( - candidateParent && - isEligibleWorktreeParent({ - child, - candidateParent, - lineageById: worktreeLineageById, - worktreeMap, - repoMap, - cyclicLineageIds - }) - ) - }) - return canAssignAll ? target : { ...target, lineageParentId: null } - }, - [cyclicLineageIds, repoMap, worktreeLineageById, worktreeMap] - ) - - const commitWorktreeLineageParentDrop = useCallback( - (draggedIds: readonly string[], parentId: string): boolean => { - const target = getEligibleLineageDropTarget( - { status: null, isPinDrop: false, lineageParentId: parentId }, - draggedIds - ) - if (!target.lineageParentId) { - return false - } - void Promise.all( - draggedIds.map((id) => assignWorktreeParent(id, { parentWorktreeId: parentId })) - ).catch((err) => { - console.error('Failed to nest workspace:', err) - toast.error( - translate( - 'auto.components.sidebar.WorktreeList.failedNestWorkspace', - 'Failed to nest workspace' - ) - ) - }) - return true - }, - [assignWorktreeParent, getEligibleLineageDropTarget] - ) - - const clearReorderedWorktreeParents = useCallback( - (args: { draggedIds: readonly string[]; sourceGroupKey: string }) => { - const sourceGroup = worktreeDragGroups.find((group) => group.key === args.sourceGroupKey) - if (!sourceGroup) { - return - } - const ids = getReorderedWorktreeIdsToUnnest({ - draggedIds: args.draggedIds, - sourceGroupIds: sourceGroup.worktreeIds, - lineageById: worktreeLineageById, - worktreeMap, - cyclicLineageIds - }) - if (ids.length === 0) { - return - } - // Why: dropping a nested card on a reorder line is the un-nest escape hatch; clear only the dragged children. - void Promise.all(ids.map((id) => updateWorktreeLineage(id, { noParent: true }))).catch( - (err) => { - console.error('Failed to unnest workspace:', err) - toast.error( - translate( - 'auto.components.sidebar.WorktreeList.failedUnnestWorkspace', - 'Failed to unnest workspace' - ) - ) - } - ) - }, - [cyclicLineageIds, updateWorktreeLineage, worktreeDragGroups, worktreeLineageById, worktreeMap] - ) - - const flushWorktreePointerDrag = useCallback(() => { - const drag = worktreePointerDragRef.current - if (!drag) { - return - } - drag.frameId = null - if (!drag.active || !drag.preview) { - return - } - updateSidebarDragPreviewPosition({ - preview: drag.preview, - pointerX: drag.currentX, - pointerY: drag.currentY, - offsetX: drag.previewOffsetX, - offsetY: drag.previewOffsetY - }) - if (!refreshWorktreeDragSession()) { - clearWorktreeDrag() - return - } - // Why: show the board preview as soon as a card drag begins so the drop target is visible up front, not only at the sidebar edge. - if ( - !drag.workspaceBoardDragPreviewRequested && - !workspaceBoardOpen && - !hasWorkspaceKanbanSidebarDropBoard() - ) { - drag.workspaceBoardDragPreviewRequested = true - onWorkspaceBoardDragPreviewStart() - } - const boardTarget = updateWorkspaceKanbanSidebarDropTargetVisual({ - x: drag.currentX, - y: drag.currentY, - shouldShowDropIndicator: (target) => - Boolean( - target.status && - shouldShowWorkspaceBoardDropIndicator(drag.reorderDraggedIds, target.status) - ) - }) - drag.latestBoardDropTarget = { - target: boardTarget, - x: drag.currentX, - y: drag.currentY - } - if (isWorkspaceKanbanSidebarDropPointInBoard(drag.currentX, drag.currentY)) { - onWorkspaceBoardDragPreviewCommit() - } - if (boardTarget.status || boardTarget.isPinDrop) { - drag.latestStatusDropTarget = null - setDragOverStatus(null) - setPinDragOver(false) - setWorktreeDragState((prev) => - prev.dropIndex === null && - prev.dropIndicatorY === null && - prev.pointerY === drag.currentY && - prev.previewOffsetsByWorktreeId.size === 0 - ? prev - : { - ...prev, - dropIndex: null, - dropIndicatorY: null, - previewOffsetsByWorktreeId: EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS, - pointerY: drag.currentY - } - ) - return - } - - const sidebarContainer = scrollRef.current - const preferredStatusTarget = getEligibleLineageDropTarget( - sidebarContainer - ? getPointerDropStatusTarget({ - container: sidebarContainer, - x: drag.currentX, - y: drag.currentY - }) - : { status: null, isPinDrop: false, lineageParentId: null }, - drag.draggedIds - ) - if (preferredStatusTarget.lineageParentId) { - updateLatestWorktreeStatusDropTarget(drag, preferredStatusTarget, null) - clearWorkspaceKanbanSidebarDropTargetVisual() - setDragOverStatus(null) - setPinDragOver(false) - setWorktreeDragState((prev) => - prev.dropIndex === null && - prev.dropIndicatorY === null && - prev.pointerY === drag.currentY && - prev.previewOffsetsByWorktreeId.size === 0 - ? prev - : { - ...prev, - dropIndex: null, - dropIndicatorY: null, - previewOffsetsByWorktreeId: EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS, - pointerY: drag.currentY - } - ) - return - } - if ( - shouldPreferSidebarStatusDropTarget({ - sourceGroupKey: drag.sourceGroupKey, - target: preferredStatusTarget, - workspaceStatuses - }) - ) { - const statusDrop = preferredStatusTarget.status - ? computeWorktreeStatusDrop({ - pointerY: drag.currentY, - status: preferredStatusTarget.status, - draggedIds: drag.reorderDraggedIds - }) - : null - if (statusDrop) { - updateLatestWorktreeStatusDropTarget(drag, preferredStatusTarget, statusDrop) - clearWorkspaceKanbanSidebarDropTargetVisual() - setDragOverStatus(null) - setPinDragOver(false) - setWorktreeDragState((prev) => - prev.dropIndex === statusDrop.dropIndex && - prev.dropIndicatorY === statusDrop.dropIndicatorY && - prev.pointerY === drag.currentY && - areWorktreeDragPreviewOffsetsEqual( - prev.previewOffsetsByWorktreeId, - statusDrop.previewOffsetsByWorktreeId - ) - ? prev - : { ...prev, ...statusDrop, pointerY: drag.currentY } - ) - return - } - updateLatestWorktreeStatusDropTarget(drag, preferredStatusTarget, statusDrop) - setDragOverStatus(preferredStatusTarget.status) - setPinDragOver(preferredStatusTarget.isPinDrop) - setWorktreeDragState((prev) => - prev.dropIndex === null && - prev.dropIndicatorY === null && - prev.pointerY === drag.currentY && - prev.previewOffsetsByWorktreeId.size === 0 - ? prev - : { - ...prev, - dropIndex: null, - dropIndicatorY: null, - previewOffsetsByWorktreeId: EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS, - pointerY: drag.currentY - } - ) - return - } - - const drop = computeWorktreeDrop(drag.currentY) - if (!drop) { - const target = preferredStatusTarget - const statusDrop = target.status - ? computeWorktreeStatusDrop({ - pointerY: drag.currentY, - status: target.status, - draggedIds: drag.reorderDraggedIds - }) - : null - if (statusDrop) { - updateLatestWorktreeStatusDropTarget(drag, target, statusDrop) - clearWorkspaceKanbanSidebarDropTargetVisual() - setDragOverStatus(null) - setPinDragOver(false) - setWorktreeDragState((prev) => - prev.dropIndex === statusDrop.dropIndex && - prev.dropIndicatorY === statusDrop.dropIndicatorY && - prev.pointerY === drag.currentY && - areWorktreeDragPreviewOffsetsEqual( - prev.previewOffsetsByWorktreeId, - statusDrop.previewOffsetsByWorktreeId - ) - ? prev - : { ...prev, ...statusDrop, pointerY: drag.currentY } - ) - return - } - updateLatestWorktreeStatusDropTarget(drag, target, statusDrop) - setDragOverStatus(target.status) - setPinDragOver(target.isPinDrop) - setWorktreeDragState((prev) => - prev.dropIndex === null && - prev.dropIndicatorY === null && - prev.pointerY === drag.currentY && - prev.previewOffsetsByWorktreeId.size === 0 - ? prev - : { - ...prev, - dropIndex: null, - dropIndicatorY: null, - previewOffsetsByWorktreeId: EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS, - pointerY: drag.currentY - } - ) - return - } - drag.latestStatusDropTarget = null - clearWorkspaceKanbanSidebarDropTargetVisual() - setDragOverStatus(null) - setPinDragOver(false) - setWorktreeDragState((prev) => - prev.dropIndex === drop.dropIndex && - prev.dropIndicatorY === drop.dropIndicatorY && - prev.pointerY === drag.currentY && - areWorktreeDragPreviewOffsetsEqual( - prev.previewOffsetsByWorktreeId, - drop.previewOffsetsByWorktreeId - ) - ? prev - : { ...prev, ...drop, pointerY: drag.currentY } - ) - }, [ - clearWorktreeDrag, - computeWorktreeDrop, - computeWorktreeStatusDrop, - onWorkspaceBoardDragPreviewStart, - refreshWorktreeDragSession, - onWorkspaceBoardDragPreviewCommit, - shouldShowWorkspaceBoardDropIndicator, - getEligibleLineageDropTarget, - workspaceBoardOpen, - workspaceStatuses - ]) - - const scheduleWorktreePointerDragFrame = useCallback( - (drag: WorktreePointerDrag) => { - if (drag.frameId !== null) { - return - } - drag.frameId = window.requestAnimationFrame(flushWorktreePointerDrag) - }, - [flushWorktreePointerDrag] - ) - - const runWorktreePointerAutoscrollFrame = useCallback( - (frameTime: number) => { - worktreePointerAutoscrollFrameIdRef.current = null - const drag = worktreePointerDragRef.current - const container = scrollRef.current - const session = worktreeDragSessionRef.current - if (!drag?.active || !container || !session) { - cancelWorktreePointerAutoscroll() - return - } - - const previousFrameTime = worktreePointerAutoscrollLastFrameTimeRef.current ?? frameTime - worktreePointerAutoscrollLastFrameTimeRef.current = frameTime - const autoscroll = getWorktreeSidebarDragAutoscroll({ - point: { clientX: drag.currentX, clientY: drag.currentY }, - containerRect: container.getBoundingClientRect(), - scrollTop: container.scrollTop, - scrollHeight: container.scrollHeight, - clientHeight: container.clientHeight, - elapsedMs: frameTime - previousFrameTime - }) - if (autoscroll) { - markScrollMovement() - container.scrollTop = autoscroll.scrollTop - if (!refreshWorktreeDragSession()) { - clearWorktreeDrag() - return - } - scheduleWorktreePointerDragFrame(drag) - } - - worktreePointerAutoscrollFrameIdRef.current = window.requestAnimationFrame( - runWorktreePointerAutoscrollFrame - ) - }, - [ - cancelWorktreePointerAutoscroll, - clearWorktreeDrag, - markScrollMovement, - refreshWorktreeDragSession, - scheduleWorktreePointerDragFrame - ] - ) - - const startWorktreePointerAutoscroll = useCallback(() => { - if (worktreePointerAutoscrollFrameIdRef.current !== null) { - return - } - worktreePointerAutoscrollLastFrameTimeRef.current = null - worktreePointerAutoscrollFrameIdRef.current = window.requestAnimationFrame( - runWorktreePointerAutoscrollFrame - ) - }, [runWorktreePointerAutoscrollFrame]) - - const beginWorktreePointerDrag = useCallback( - (drag: WorktreePointerDrag) => { - const { preview, offsetX, offsetY, height } = createSidebarDragPreview({ - sourceRow: drag.sourceRow, - pointerX: drag.currentX, - pointerY: drag.currentY, - draggedCount: drag.draggedIds.length - }) - drag.active = true - drag.preview = preview - drag.previewOffsetX = offsetX - drag.previewOffsetY = offsetY - suppressWorktreeClickUntilRef.current = window.performance.now() + 500 - setSidebarPointerDragDocumentStyles(true) - worktreeDragSessionRef.current = { - draggingWorktreeId: drag.worktreeId, - sourceGroupKey: drag.sourceGroupKey, - draggedIds: drag.draggedIds, - reorderDraggedIds: drag.reorderDraggedIds, - reorderUnitDraggedIds: drag.reorderUnitDraggedIds, - rects: drag.rects, - // Why: reuse the floating preview's own offset so the hit test tracks the - // card the user sees, not the raw pointer. - grab: getWorktreeSidebarDragGrab({ offsetY, height }), - anchor: null - } - setWorktreeDragState({ - draggingWorktreeId: drag.worktreeId, - sourceGroupKey: drag.sourceGroupKey, - dropIndex: null, - dropIndicatorY: null, - previewOffsetsByWorktreeId: EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS, - pointerY: drag.currentY - }) - startWorktreePointerAutoscroll() - scheduleWorktreePointerDragFrame(drag) - }, - [scheduleWorktreePointerDragFrame, startWorktreePointerAutoscroll] - ) - - const handleWorktreeRowPointerDown = useCallback( - (event: React.PointerEvent, worktreeId: string, rowKey: string) => { - if (event.button !== 0 || event.pointerType === 'touch') { - return - } - const sourceRow = event.currentTarget - if (isSidebarPointerDragBlocked(event.target, sourceRow)) { - return - } - const sourceGroupKey = groupKeyByRowKey.get(rowKey) - const container = scrollRef.current - if (!sourceGroupKey || !container) { - return - } - const rects = getWorktreeSidebarDragRectsForGroup(container, sourceGroupKey) - const canPreviewWorkspaceBoardOnDrag = - !workspaceBoardOpen && - onWorkspaceBoardDragPreviewStart !== NOOP_WORKSPACE_BOARD_DRAG_PREVIEW_CALLBACK - if ( - rects.length <= 1 && - !hasWorkspaceKanbanSidebarDropBoard() && - !canPreviewWorkspaceBoardOnDrag - ) { - return - } - const draggedIds = - selectedWorktreeIds.has(worktreeId) && selectedWorktrees.length > 1 - ? selectedWorktrees.map((worktree) => worktree.id) - : [worktreeId] - const reorderDraggedIds = getReorderDraggedIds(draggedIds) - const reorderUnitDraggedIds = getReorderUnitDraggedIds(sourceGroupKey, reorderDraggedIds) - worktreePointerDragRef.current = { - pointerId: event.pointerId, - sourceRow, - startX: event.clientX, - startY: event.clientY, - currentX: event.clientX, - currentY: event.clientY, - worktreeId, - draggedIds, - reorderDraggedIds, - reorderUnitDraggedIds, - sourceGroupKey, - rects, - active: false, - preview: null, - previewOffsetX: 0, - previewOffsetY: 0, - workspaceBoardDragPreviewRequested: false, - frameId: null, - latestBoardDropTarget: null, - latestStatusDropTarget: null - } - }, - [ - getReorderDraggedIds, - getReorderUnitDraggedIds, - groupKeyByRowKey, - onWorkspaceBoardDragPreviewStart, - selectedWorktreeIds, - selectedWorktrees, - workspaceBoardOpen - ] - ) - - const handleWorktreeRowClickCapture = useCallback((event: React.MouseEvent) => { - if (window.performance.now() >= suppressWorktreeClickUntilRef.current) { - return - } - event.preventDefault() - event.stopPropagation() - }, []) - - useEffect(() => { - const handlePointerMove = (event: PointerEvent): void => { - const drag = worktreePointerDragRef.current - if (!drag || event.pointerId !== drag.pointerId) { - return - } - drag.currentX = event.clientX - drag.currentY = event.clientY - if (!drag.active) { - const distance = Math.hypot(drag.currentX - drag.startX, drag.currentY - drag.startY) - if (distance < SIDEBAR_POINTER_DRAG_THRESHOLD_PX) { - return - } - beginWorktreePointerDrag(drag) - } - event.preventDefault() - event.stopPropagation() - scheduleWorktreePointerDragFrame(drag) - } - - const handlePointerUp = (event: PointerEvent): void => { - const drag = worktreePointerDragRef.current - if (!drag || event.pointerId !== drag.pointerId) { - return - } - drag.currentX = event.clientX - drag.currentY = event.clientY - if (!drag.active) { - worktreePointerDragRef.current = null - return - } - event.preventDefault() - event.stopPropagation() - if (!refreshWorktreeDragSession()) { - clearWorktreeDrag() - return - } - const boardDropTarget = resolveWorkspaceKanbanCardDropCommitTarget({ - currentTarget: getWorkspaceKanbanSidebarDropTarget(event.clientX, event.clientY), - latestTrackedTarget: drag.latestBoardDropTarget, - x: event.clientX, - y: event.clientY - }) - if (isWorkspaceKanbanSidebarDropPointInBoard(event.clientX, event.clientY)) { - onWorkspaceBoardDragPreviewCommit() - } - if (boardDropTarget.isPinDrop) { - onPinWorktrees(drag.draggedIds) - } else if (boardDropTarget.status) { - onDropWorktreesOnWorkspaceBoard({ - worktreeIds: drag.reorderDraggedIds, - status: boardDropTarget.status, - // Why: the target counts rendered cards, but the groups are the full - // lane. Board search can make those two differ. - dropIndex: resolveWorkspaceKanbanSidebarFullLaneDropIndex( - boardDropTarget.status, - boardDropTarget.dropIndex - ), - groups: getWorkspaceKanbanSidebarDropGroups() - }) - } else { - const preferredStatusTarget = getEligibleLineageDropTarget( - scrollRef.current - ? getPointerDropStatusTarget({ - container: scrollRef.current, - x: event.clientX, - y: event.clientY - }) - : { status: null, isPinDrop: false, lineageParentId: null }, - drag.draggedIds - ) - if (preferredStatusTarget.lineageParentId) { - commitWorktreeLineageParentDrop(drag.draggedIds, preferredStatusTarget.lineageParentId) - clearWorktreeDrag() - return - } - if ( - shouldPreferSidebarStatusDropTarget({ - sourceGroupKey: drag.sourceGroupKey, - target: preferredStatusTarget, - workspaceStatuses - }) - ) { - const statusDrop = preferredStatusTarget.status - ? computeWorktreeStatusDrop({ - pointerY: event.clientY, - status: preferredStatusTarget.status, - draggedIds: drag.reorderDraggedIds - }) - : null - if (preferredStatusTarget.isPinDrop) { - onPinWorktrees(drag.draggedIds) - } else if (preferredStatusTarget.status) { - if (statusDrop) { - onMoveWorktreesToStatusAtIndex({ - worktreeIds: drag.reorderDraggedIds, - status: preferredStatusTarget.status, - dropIndex: statusDrop.dropIndex, - groups: worktreeDragGroups - }) - } else { - onMoveWorktreesToStatus(drag.reorderDraggedIds, preferredStatusTarget.status) - } - } - clearWorktreeDrag() - return - } - const drop = computeWorktreeDrop(event.clientY) - if (drop) { - onReorderWorktrees({ - groups: worktreeDragGroups, - sourceGroupKey: drag.sourceGroupKey, - draggedIds: drag.reorderDraggedIds, - dropIndex: getFullDropIndexForWorktreeDragUnit({ - groups: worktreeDragUnitGroups, - sourceGroupKey: drag.sourceGroupKey, - dropIndex: drop.dropIndex - }) - }) - clearReorderedWorktreeParents({ - draggedIds: drag.draggedIds, - sourceGroupKey: drag.sourceGroupKey - }) - } else if (scrollRef.current) { - const currentTarget = preferredStatusTarget - const currentPreview = currentTarget.status - ? computeWorktreeStatusDrop({ - pointerY: event.clientY, - status: currentTarget.status, - draggedIds: drag.reorderDraggedIds - }) - : null - const { target, preview: statusDrop } = resolveWorktreeSidebarStatusDropCommitTarget({ - currentTarget, - currentPreview, - latestTrackedTarget: drag.latestStatusDropTarget, - x: event.clientX, - y: event.clientY - }) - if (target.lineageParentId) { - commitWorktreeLineageParentDrop(drag.draggedIds, target.lineageParentId) - } else if (target.isPinDrop) { - onPinWorktrees(drag.draggedIds) - } else if (target.status) { - if (statusDrop) { - onMoveWorktreesToStatusAtIndex({ - worktreeIds: drag.reorderDraggedIds, - status: target.status, - dropIndex: statusDrop.dropIndex, - groups: worktreeDragGroups - }) - } else { - onMoveWorktreesToStatus(drag.reorderDraggedIds, target.status) - } - } - } - } - clearWorktreeDrag() - } - - const handlePointerCancel = (event: PointerEvent): void => { - const drag = worktreePointerDragRef.current - if (!drag || event.pointerId !== drag.pointerId) { - return - } - clearWorktreeDrag() - } - - window.addEventListener('pointermove', handlePointerMove, { capture: true }) - window.addEventListener('pointerup', handlePointerUp, { capture: true }) - window.addEventListener('pointercancel', handlePointerCancel, { capture: true }) - return () => { - window.removeEventListener('pointermove', handlePointerMove, { capture: true }) - window.removeEventListener('pointerup', handlePointerUp, { capture: true }) - window.removeEventListener('pointercancel', handlePointerCancel, { capture: true }) - } - }, [ - beginWorktreePointerDrag, - clearWorktreeDrag, - clearReorderedWorktreeParents, - commitWorktreeLineageParentDrop, - computeWorktreeDrop, - computeWorktreeStatusDrop, - getEligibleLineageDropTarget, - onMoveWorktreesToStatus, - onMoveWorktreesToStatusAtIndex, - onDropWorktreesOnWorkspaceBoard, - onPinWorktrees, - onReorderWorktrees, - onWorkspaceBoardDragPreviewCommit, - refreshWorktreeDragSession, - scheduleWorktreePointerDragFrame, - shouldShowWorkspaceBoardDropIndicator, - worktreeDragGroups, - worktreeDragUnitGroups, - workspaceStatuses - ]) - - useEffect(() => { - const handleClick = (event: MouseEvent): void => { - if (window.performance.now() >= suppressWorktreeClickUntilRef.current) { - return - } - event.preventDefault() - event.stopPropagation() - event.stopImmediatePropagation() - } - - document.addEventListener('click', handleClick, true) - return () => document.removeEventListener('click', handleClick, true) - }, []) - - const runWorktreeNativeAutoscrollFrame = useCallback( - (frameTime: number) => { - worktreeNativeAutoscrollFrameIdRef.current = null - const point = worktreeNativeLatestPointRef.current - const container = scrollRef.current - const session = worktreeDragSessionRef.current - if (!point || !container || !session) { - cancelWorktreeNativeAutoscroll() - return - } - - const previousFrameTime = worktreeNativeAutoscrollLastFrameTimeRef.current ?? frameTime - worktreeNativeAutoscrollLastFrameTimeRef.current = frameTime - const autoscroll = getWorktreeSidebarDragAutoscroll({ - point, - containerRect: container.getBoundingClientRect(), - scrollTop: container.scrollTop, - scrollHeight: container.scrollHeight, - clientHeight: container.clientHeight, - elapsedMs: frameTime - previousFrameTime - }) - if (autoscroll) { - markScrollMovement() - container.scrollTop = autoscroll.scrollTop - if (!refreshWorktreeDragSession()) { - clearWorktreeDrag() - return - } - const drop = computeWorktreeDrop(point.clientY) - if (!drop) { - const target = getPointerDropStatusTarget({ - container, - x: point.clientX, - y: point.clientY - }) - const statusDrop = target.status - ? computeWorktreeStatusDrop({ - pointerY: point.clientY, - status: target.status, - draggedIds: session.reorderDraggedIds - }) - : null - if (statusDrop) { - setWorktreeDragState((prev) => - prev.dropIndex === statusDrop.dropIndex && - prev.dropIndicatorY === statusDrop.dropIndicatorY && - areWorktreeDragPreviewOffsetsEqual( - prev.previewOffsetsByWorktreeId, - statusDrop.previewOffsetsByWorktreeId - ) - ? prev - : { ...prev, ...statusDrop, pointerY: point.clientY } - ) - return - } - setWorktreeDragState((prev) => - prev.dropIndex === null && - prev.dropIndicatorY === null && - prev.previewOffsetsByWorktreeId.size === 0 - ? prev - : { - ...prev, - dropIndex: null, - dropIndicatorY: null, - previewOffsetsByWorktreeId: EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS, - pointerY: null - } - ) - } else { - setWorktreeDragState((prev) => - prev.dropIndex === drop.dropIndex && - prev.dropIndicatorY === drop.dropIndicatorY && - areWorktreeDragPreviewOffsetsEqual( - prev.previewOffsetsByWorktreeId, - drop.previewOffsetsByWorktreeId - ) - ? prev - : { ...prev, ...drop, pointerY: point.clientY } - ) - } - } - - worktreeNativeAutoscrollFrameIdRef.current = window.requestAnimationFrame( - runWorktreeNativeAutoscrollFrame - ) - }, - [ - cancelWorktreeNativeAutoscroll, - clearWorktreeDrag, - computeWorktreeDrop, - computeWorktreeStatusDrop, - markScrollMovement, - refreshWorktreeDragSession - ] - ) - - const startWorktreeNativeAutoscroll = useCallback(() => { - if (worktreeNativeAutoscrollFrameIdRef.current !== null) { - return - } - worktreeNativeAutoscrollLastFrameTimeRef.current = null - worktreeNativeAutoscrollFrameIdRef.current = window.requestAnimationFrame( - runWorktreeNativeAutoscrollFrame - ) - }, [runWorktreeNativeAutoscrollFrame]) - - const handleWorktreeCardDragStart = useCallback( - (event: React.DragEvent, worktreeId: string, draggedIds: readonly string[]) => { - const sourceGroupKey = - worktreeDragGroups.find((group) => group.worktreeIds.includes(worktreeId))?.key ?? null - if (!sourceGroupKey) { - return - } - const reorderDraggedIds = getReorderDraggedIds(draggedIds) - const reorderUnitDraggedIds = getReorderUnitDraggedIds(sourceGroupKey, reorderDraggedIds) - const rects = scrollRef.current - ? getWorktreeSidebarDragRectsForGroup(scrollRef.current, sourceGroupKey) - : [] - const sourceRect = event.currentTarget.getBoundingClientRect() - worktreeDragSessionRef.current = { - draggingWorktreeId: worktreeId, - sourceGroupKey, - draggedIds, - reorderDraggedIds, - reorderUnitDraggedIds, - rects, - grab: getWorktreeSidebarDragGrab({ - offsetY: event.clientY - sourceRect.top, - height: sourceRect.height - }), - anchor: null - } - setWorktreeDragState({ - draggingWorktreeId: worktreeId, - sourceGroupKey, - dropIndex: null, - dropIndicatorY: null, - previewOffsetsByWorktreeId: EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS, - pointerY: null - }) - }, - [getReorderDraggedIds, getReorderUnitDraggedIds, worktreeDragGroups] - ) - - const handleWorktreeDragOver = useCallback( - (event: React.DragEvent) => { - const session = worktreeDragSessionRef.current - if (!session) { - return - } - worktreeNativeLatestPointRef.current = { clientX: event.clientX, clientY: event.clientY } - startWorktreeNativeAutoscroll() - if (!refreshWorktreeDragSession()) { - clearWorktreeDrag() - return - } - const target = getEligibleLineageDropTarget( - getPointerDropStatusTarget({ - container: event.currentTarget, - x: event.clientX, - y: event.clientY - }), - session.draggedIds - ) - if (target.lineageParentId) { - event.preventDefault() - event.dataTransfer.dropEffect = 'move' - setNativeLineageDropTargetId(target.lineageParentId) - setWorktreeDragState((prev) => - prev.dropIndex === null && - prev.dropIndicatorY === null && - prev.previewOffsetsByWorktreeId.size === 0 - ? prev - : { - ...prev, - dropIndex: null, - dropIndicatorY: null, - previewOffsetsByWorktreeId: EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS, - pointerY: event.clientY - } - ) - return - } - setNativeLineageDropTargetId(null) - - const drop = computeWorktreeDrop(event.clientY) - if (!drop) { - const statusDrop = target.status - ? computeWorktreeStatusDrop({ - pointerY: event.clientY, - status: target.status, - draggedIds: session.reorderDraggedIds - }) - : null - if (statusDrop) { - event.preventDefault() - event.dataTransfer.dropEffect = 'move' - setWorktreeDragState((prev) => - prev.dropIndex === statusDrop.dropIndex && - prev.dropIndicatorY === statusDrop.dropIndicatorY && - areWorktreeDragPreviewOffsetsEqual( - prev.previewOffsetsByWorktreeId, - statusDrop.previewOffsetsByWorktreeId - ) - ? prev - : { ...prev, ...statusDrop, pointerY: event.clientY } - ) - return - } - setWorktreeDragState((prev) => - prev.dropIndex === null && - prev.dropIndicatorY === null && - prev.previewOffsetsByWorktreeId.size === 0 - ? prev - : { - ...prev, - dropIndex: null, - dropIndicatorY: null, - previewOffsetsByWorktreeId: EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS, - pointerY: null - } - ) - return - } - event.preventDefault() - event.dataTransfer.dropEffect = 'move' - setWorktreeDragState((prev) => - prev.dropIndex === drop.dropIndex && - prev.dropIndicatorY === drop.dropIndicatorY && - areWorktreeDragPreviewOffsetsEqual( - prev.previewOffsetsByWorktreeId, - drop.previewOffsetsByWorktreeId - ) - ? prev - : { ...prev, ...drop, pointerY: event.clientY } - ) - }, - [ - clearWorktreeDrag, - computeWorktreeDrop, - computeWorktreeStatusDrop, - getEligibleLineageDropTarget, - refreshWorktreeDragSession, - startWorktreeNativeAutoscroll - ] - ) - - const handleWorktreeDrop = useCallback( - (event: React.DragEvent) => { - const session = worktreeDragSessionRef.current - if (!session) { - return - } - if (!refreshWorktreeDragSession()) { - clearWorktreeDrag() - return - } - const boardDropTarget = getWorkspaceKanbanSidebarDropTarget(event.clientX, event.clientY) - if (boardDropTarget.status || boardDropTarget.isPinDrop) { - clearWorktreeDrag() - return - } - - const container = scrollRef.current - const target = getEligibleLineageDropTarget( - container - ? getPointerDropStatusTarget({ - container, - x: event.clientX, - y: event.clientY - }) - : { status: null, isPinDrop: false, lineageParentId: null }, - session.draggedIds - ) - - if (target.lineageParentId) { - event.preventDefault() - event.stopPropagation() - commitWorktreeLineageParentDrop(session.draggedIds, target.lineageParentId) - clearWorktreeDrag() - return - } - - const drop = computeWorktreeDrop(event.clientY) - if (!drop) { - const statusDrop = target.status - ? computeWorktreeStatusDrop({ - pointerY: event.clientY, - status: target.status, - draggedIds: session.reorderDraggedIds - }) - : null - if (target.status && statusDrop) { - event.preventDefault() - event.stopPropagation() - onMoveWorktreesToStatusAtIndex({ - worktreeIds: session.reorderDraggedIds, - status: target.status, - dropIndex: statusDrop.dropIndex, - groups: worktreeDragGroups - }) - clearWorktreeDrag() - return - } - clearWorktreeDrag() - return - } - event.preventDefault() - onReorderWorktrees({ - groups: worktreeDragGroups, - sourceGroupKey: session.sourceGroupKey, - draggedIds: session.reorderDraggedIds, - dropIndex: getFullDropIndexForWorktreeDragUnit({ - groups: worktreeDragUnitGroups, - sourceGroupKey: session.sourceGroupKey, - dropIndex: drop.dropIndex - }) - }) - clearReorderedWorktreeParents({ - draggedIds: session.draggedIds, - sourceGroupKey: session.sourceGroupKey - }) - clearWorktreeDrag() - }, - [ - clearWorktreeDrag, - clearReorderedWorktreeParents, - commitWorktreeLineageParentDrop, - computeWorktreeDrop, - computeWorktreeStatusDrop, - getEligibleLineageDropTarget, - onMoveWorktreesToStatusAtIndex, - onReorderWorktrees, - refreshWorktreeDragSession, - worktreeDragGroups, - worktreeDragUnitGroups - ] - ) - - useEffect(() => { - if (document.visibilityState !== 'visible') { - lastVisibleRefreshKeyRef.current = '__document_hidden__' - return - } - const currentWorktree = currentWorktreeId ? (worktreeMap.get(currentWorktreeId) ?? null) : null - // Why: this reporter feeds the GitHub coordinator; GitLab-only MR panels refresh via hosted-review paths. - const sidebarWorktreeHasGitHubReview = - currentWorktree !== null && - ((currentWorktree.linkedGitLabMR ?? null) === null || - (currentWorktree.linkedPR ?? null) !== null) - const shouldTrackSidebarWorktree = rightSidebarShowsPR && sidebarWorktreeHasGitHubReview - const shouldTrackVisibleRows = - groupBy === 'pr-status' || - (newCardStyle - ? cardProps.includes('status') - : cardProps.includes('pr') || cardProps.includes('ci')) - if (!shouldTrackVisibleRows && !shouldTrackSidebarWorktree) { - if (lastVisibleRefreshKeyRef.current !== '__hidden__') { - lastVisibleRefreshKeyRef.current = '__hidden__' - reportVisibleGitHubPRRefreshCandidates([], Date.now()) - } - return - } - const scrollEl = scrollRef.current - if (!scrollEl) { - return - } - const viewportTop = scrollEl.scrollTop - const viewportBottom = viewportTop + scrollEl.clientHeight - const visibleRows = virtualItems - .filter((item) => item.start < viewportBottom && item.end > viewportTop) - .map((item) => renderRows[item.index]) - .filter((row): row is WorktreeItemRow => row?.type === 'item') - .filter((row) => row.repo?.kind === 'git' && !row.worktree.isBare && row.worktree.branch) - const visibleWorktreeIds = new Set(visibleRows.map((row) => row.worktree.id)) - if ( - shouldTrackSidebarWorktree && - currentWorktree && - !currentWorktree.isBare && - currentWorktree.branch - ) { - visibleWorktreeIds.add(currentWorktree.id) - } - const visibleIdentity = visibleRows - .map((row) => `${row.worktree.id}:${row.worktree.branch}:${row.worktree.linkedPR ?? ''}`) - .join('|') - const sidebarIdentity = - shouldTrackSidebarWorktree && currentWorktree - ? `${currentWorktree.id}:${currentWorktree.branch}:${currentWorktree.linkedPR ?? ''}` - : '' - const key = `${visibleIdentity}:${sidebarIdentity}:${sshConnectedGeneration}:${prVisibleRefreshGeneration}:${cardProps.join(',')}` - if (!key || key === lastVisibleRefreshKeyRef.current) { - return - } - lastVisibleRefreshKeyRef.current = key - reportVisibleGitHubPRRefreshCandidates(Array.from(visibleWorktreeIds), Date.now()) - }, [ - cardProps, - currentWorktreeId, - documentVisibilityRevision, - groupBy, - renderRows, - reportVisibleGitHubPRRefreshCandidates, - prVisibleRefreshGeneration, - rightSidebarShowsPR, - sshConnectedGeneration, - newCardStyle, - virtualItems, - worktreeMap - ]) - - const activeDescendantId = getActiveDescendantOptionId({ - activeWorktreeId, - primaryActiveRowKey: - primaryActiveWorktreeRow?.worktreeId === activeWorktreeId - ? primaryActiveWorktreeRow.rowKey - : undefined, - pinnedDisplayPolicy, - renderRows, - virtualItems - }) - - const hasWorkspaceDropTargets = useMemo( - () => - groupBy === 'workspace-status' || - rows.some((row) => row.type === 'header' && row.key === PINNED_GROUP_KEY), - [groupBy, rows] - ) - - const handleWorkspaceStatusDragOver = useCallback( - (event: React.DragEvent, status: WorkspaceStatus) => { - if (!hasWorkspaceDragData(event.dataTransfer)) { - return - } - event.preventDefault() - event.dataTransfer.dropEffect = 'move' - setDragOverStatus(status) - }, - [] - ) - - const handleWorkspaceStatusDragLeave = useCallback((event: React.DragEvent) => { - const relatedTarget = event.relatedTarget - if (relatedTarget instanceof Node && event.currentTarget.contains(relatedTarget)) { - return - } - setDragOverStatus(null) - }, []) - - const handleWorkspacePinDragOver = useCallback((event: React.DragEvent) => { - if (!hasWorkspaceDragData(event.dataTransfer)) { - return - } - event.preventDefault() - event.dataTransfer.dropEffect = 'move' - setPinDragOver(true) - }, []) - - const handleWorkspacePinDragLeave = useCallback((event: React.DragEvent) => { - const relatedTarget = event.relatedTarget - if (relatedTarget instanceof Node && event.currentTarget.contains(relatedTarget)) { - return - } - setPinDragOver(false) - }, []) - - const handleWorkspaceStatusDragFinish = useCallback(() => { - setDragOverStatus(null) - setPinDragOver(false) - }, []) - - const handleWorkspaceStatusDrop = useCallback( - (event: React.DragEvent, status: WorkspaceStatus) => { - const worktreeIds = readWorkspaceDragDataIds(event.dataTransfer) - if (worktreeIds.length === 0) { - return - } - event.preventDefault() - const session = worktreeDragSessionRef.current - const statusDrop = session - ? computeWorktreeStatusDrop({ - pointerY: event.clientY, - status, - draggedIds: session.reorderDraggedIds - }) - : null - setDragOverStatus(null) - if (session && statusDrop) { - event.stopPropagation() - onMoveWorktreesToStatusAtIndex({ - worktreeIds: session.reorderDraggedIds, - status, - dropIndex: statusDrop.dropIndex, - groups: worktreeDragGroups - }) - clearWorktreeDrag() - return - } - // Match status-drop scope to drag-preview scope (#9083): session uses its expanded set, else expand dataTransfer ids live. - onMoveWorktreesToStatus( - session ? session.reorderDraggedIds : getReorderDraggedIds(worktreeIds), - status - ) - }, - [ - clearWorktreeDrag, - computeWorktreeStatusDrop, - getReorderDraggedIds, - onMoveWorktreesToStatus, - onMoveWorktreesToStatusAtIndex, - worktreeDragGroups - ] - ) - - useEffect(() => { - const handleDocumentDrop = (event: DragEvent): void => { - const session = worktreeDragSessionRef.current - if (!session) { - return - } - if (!refreshWorktreeDragSession()) { - clearWorktreeDrag() - return - } - const drop = computeWorktreeDrop(event.clientY) - if (!drop) { - const container = scrollRef.current - const target = getEligibleLineageDropTarget( - container - ? getPointerDropStatusTarget({ - container, - x: event.clientX, - y: event.clientY - }) - : { status: null, isPinDrop: false, lineageParentId: null }, - session.draggedIds - ) - if (target.lineageParentId) { - event.preventDefault() - event.stopPropagation() - commitWorktreeLineageParentDrop(session.draggedIds, target.lineageParentId) - clearWorktreeDrag() - return - } - const statusDrop = target.status - ? computeWorktreeStatusDrop({ - pointerY: event.clientY, - status: target.status, - draggedIds: session.reorderDraggedIds - }) - : null - if (target.status && statusDrop) { - event.preventDefault() - event.stopPropagation() - onMoveWorktreesToStatusAtIndex({ - worktreeIds: session.reorderDraggedIds, - status: target.status, - dropIndex: statusDrop.dropIndex, - groups: worktreeDragGroups - }) - clearWorktreeDrag() - return - } - clearWorktreeDrag() - return - } - // Why: pointer still inside the source group means reorder, not status move; commit here and stop the capture handler. - event.preventDefault() - event.stopPropagation() - onReorderWorktrees({ - groups: worktreeDragGroups, - sourceGroupKey: session.sourceGroupKey, - draggedIds: session.reorderDraggedIds, - dropIndex: getFullDropIndexForWorktreeDragUnit({ - groups: worktreeDragUnitGroups, - sourceGroupKey: session.sourceGroupKey, - dropIndex: drop.dropIndex - }) - }) - clearReorderedWorktreeParents({ - draggedIds: session.draggedIds, - sourceGroupKey: session.sourceGroupKey - }) - clearWorktreeDrag() - } - - document.addEventListener('drop', handleDocumentDrop, true) - return () => document.removeEventListener('drop', handleDocumentDrop, true) - }, [ - clearWorktreeDrag, - clearReorderedWorktreeParents, - commitWorktreeLineageParentDrop, - computeWorktreeDrop, - computeWorktreeStatusDrop, - getEligibleLineageDropTarget, - onMoveWorktreesToStatusAtIndex, - onReorderWorktrees, - refreshWorktreeDragSession, - worktreeDragGroups, - worktreeDragUnitGroups - ]) - - useEffect(() => { - const handleDocumentDragEnd = (): void => { - if (worktreeDragSessionRef.current) { - clearWorktreeDrag() - } - } - - document.addEventListener('dragend', handleDocumentDragEnd, true) - return () => document.removeEventListener('dragend', handleDocumentDragEnd, true) - }, [clearWorktreeDrag]) - - useEffect(() => { - const handleVisibilityChange = (): void => { - if (document.visibilityState !== 'visible' && worktreeDragSessionRef.current) { - clearWorktreeDrag() - } - } - - document.addEventListener('visibilitychange', handleVisibilityChange) - return () => document.removeEventListener('visibilitychange', handleVisibilityChange) - }, [clearWorktreeDrag]) - - // Why: expand here (not the shared hook, used by the flat board) so a dropped parent carries its lineage children (#9083). - const moveWorktreesToStatusForDocumentDrop = useCallback( - (ids: readonly string[], status: WorkspaceStatus) => - onMoveWorktreesToStatus(getReorderDraggedIds(ids), status), - [getReorderDraggedIds, onMoveWorktreesToStatus] - ) - - useWorkspaceStatusDocumentDrop( - scrollRef, - onMoveWorktreeToStatus, - onPinWorktree, - handleWorkspaceStatusDragFinish, - hasWorkspaceDropTargets, - { - onMoveWorktreesToStatus: moveWorktreesToStatusForDocumentDrop, - onPinWorktrees - } - ) - - return ( -
-
-
- {canReorderRepoHeaders && - repoDrag.state.draggingRepoId !== null && - repoDrag.state.dropIndicatorY !== null ? ( - - ) : null} - {canReorderProjectGroupHeaders && - projectGroupDrag.state.draggingGroupId !== null && - projectGroupDrag.state.dropIndicatorY !== null ? ( - - ) : null} - {hostDrag.state.draggingHostId !== null && hostDrag.state.dropIndicatorY !== null ? ( - - ) : null} - {worktreeDragState.draggingWorktreeId !== null && - worktreeDragState.dropIndicatorY !== null ? ( - - ) : null} - {virtualItems.map((vItem) => { - const row = renderRows[vItem.index] - if (!row) { - return null - } - - if (row.type === 'host-header') { - // Why: the host card is the outer tier; it pins above group headers (z-30 vs z-20) and stays put as they hand off. - const isActiveStickyHost = activeStickyHostIndexRef.current === vItem.index - const hasHeaderTopSpacing = shouldUseHeaderTopSpacing({ - rows: renderRows, - index: vItem.index, - firstHeaderIndex - }) - return ( -
- toggleGroupWithScrollAnchor(row.key)} - onDragPointerDown={ - orderedHostIds.length > 1 - ? (e) => hostDrag.onHandlePointerDown(e, row.hostId) - : undefined - } - dragging={hostDrag.state.draggingHostId === row.hostId} - /> -
- ) - } - - if (row.type === 'header') { - const isActiveStickyHeader = activeStickyHeaderIndexRef.current === vItem.index - // Why: when a host card is pinned, the group tier pins flush beneath it, not at the viewport top. - const stickyTopClass = - activeStickyHostIndexRef.current !== null ? 'top-[35px]' : '-top-px' - const hasHeaderTopSpacing = shouldUseHeaderTopSpacing({ - rows: renderRows, - index: vItem.index, - firstHeaderIndex - }) - const isRepoHeader = groupBy === 'repo' && row.repo !== undefined - const isProjectGroupHeader = groupBy === 'repo' && row.projectGroup !== undefined - const projectIdForHeader = isRepoHeader ? row.repo!.id : undefined - const projectGroupIdForHeader = - isProjectGroupHeader && !row.repo && typeof row.projectGroup?.id === 'string' - ? row.projectGroup.id - : undefined - const repoHeaderIndex = - projectIdForHeader !== undefined - ? repoHeaderIndexByRepoId.get(projectIdForHeader) - : undefined - const repoHeaderBucketKey = - projectIdForHeader !== undefined - ? repoHeaderBucketByRepoId.get(projectIdForHeader) - : undefined - const projectGroupHeaderIndex = - projectGroupIdForHeader !== undefined - ? projectGroupHeaderIndexByGroupId.get(projectGroupIdForHeader) - : undefined - const projectGroupHeaderBucketKey = - projectGroupIdForHeader !== undefined - ? projectGroupHeaderBucketByGroupId.get(projectGroupIdForHeader) - : undefined - const isDraggableRepoHeader = Boolean( - canReorderRepoHeaders && - isRepoHeader && - projectIdForHeader && - repoHeaderBucketKey && - (sidebarRepoHeaderIdsByBucket.get(repoHeaderBucketKey)?.length ?? 0) > 1 - ) - const isDraggableProjectGroupHeader = Boolean( - canReorderProjectGroupHeaders && - projectGroupIdForHeader && - projectGroupHeaderBucketKey && - (sidebarProjectGroupHeaderIdsByBucket.get(projectGroupHeaderBucketKey)?.length ?? - 0) > 1 - ) - const isDraggingThis = - canReorderRepoHeaders && - repoDrag.state.draggingRepoId !== null && - repoDrag.state.draggingRepoId === projectIdForHeader - const isDraggingThisProjectGroup = - canReorderProjectGroupHeaders && - projectGroupDrag.state.draggingGroupId !== null && - projectGroupDrag.state.draggingGroupId === projectGroupIdForHeader - const headerWorkspaceStatus = - groupBy === 'workspace-status' - ? getWorkspaceStatusFromGroupKey(row.key, workspaceStatuses) - : null - const isPinnedHeader = row.key === PINNED_GROUP_KEY - const repoHeaderColor = resolveProjectGroupHeaderColor({ - groupBy, - headerKey: row.key, - badgeColor: row.repo?.badgeColor - }) - const createState = row.repo - ? getRepoHeaderCreateState({ - repo: row.repo, - label: row.label, - sshStatus: row.repo.connectionId - ? (sshConnectionStates.get(row.repo.connectionId)?.status ?? null) - : null - }) - : null - const projectGroupPathStatus = - isProjectGroupHeader && - row.projectGroup && - 'parentPath' in row.projectGroup && - row.projectGroup.parentPath - ? getCachedFolderWorkspacePathStatus({ - scope: 'project-group', - projectGroupId: row.projectGroup.id - }) - : null - const folderWorkspaceCreateDisabled = - projectGroupPathStatus?.exists === false && - (isConfirmedStaleFolderPathStatus(projectGroupPathStatus) || - projectGroupPathStatus.reason === 'ambiguous-connection') - const projectGroupDepth = row.projectGroupDepth ?? 0 - const isHeaderCollapsed = collapsedGroups.has(row.key) - // Why: repo/project/status/pinned share compact section chrome; flat "All" stays a simple label. - const showHeaderCollapseAffordance = - row.count > 0 && - (isRepoHeader || - isProjectGroupHeader || - headerWorkspaceStatus !== null || - isPinnedHeader) - // Why: non-project headers like "All" are flat-list labels; don't reserve project hierarchy indent. - const headerPaddingLeft = - isRepoHeader || isProjectGroupHeader - ? getProjectGroupHeaderPaddingLeft(projectGroupDepth) - : WORKTREE_SECTION_HEADER_PADDING_LEFT - return ( -
-
handleWorkspaceStatusDragOver(event, headerWorkspaceStatus) - : undefined - } - onDragLeave={ - isPinnedHeader - ? handleWorkspacePinDragLeave - : headerWorkspaceStatus - ? handleWorkspaceStatusDragLeave - : undefined - } - onDrop={ - headerWorkspaceStatus - ? (event) => handleWorkspaceStatusDrop(event, headerWorkspaceStatus) - : undefined - } - onPointerDown={ - isDraggableRepoHeader && projectIdForHeader - ? (event) => repoDrag.onHandlePointerDown(event, projectIdForHeader) - : isDraggableProjectGroupHeader && projectGroupIdForHeader - ? (event) => - projectGroupDrag.onHandlePointerDown(event, projectGroupIdForHeader) - : undefined - } - onClick={(event) => { - if (shouldIgnoreRepoHeaderToggle(event)) { - return - } - toggleGroupWithScrollAnchor(row.key) - }} - onKeyDown={(e) => { - if (shouldIgnoreRepoHeaderToggle(e)) { - return - } - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault() - toggleGroupWithScrollAnchor(row.key) - } - }} - > - {/* Why: grab cursor on icon+title only. Row still has handle attrs so - indent/padding can arm drag; actions are excluded via data-repo-header-actions. - self-stretch fills h-7 so grab matches the full title column height. */} -
- {row.icon ? ( -
- {row.repo ? ( - - ) : ( - - )} -
- ) : null} - -
-
-
- {row.label} -
- - -
-
-
- - - {showHeaderCollapseAffordance ? ( -
{ - event.preventDefault() - event.stopPropagation() - toggleGroupWithScrollAnchor(row.key) - }} - > - -
- ) : null} - - {isProjectGroupHeader && !row.repo && row.projectGroup?.id ? ( - - - - - - { - if (row.projectGroup?.id) { - handleRenameProjectGroup(row.projectGroup.id, row.label) - } - }} - > - {translate( - 'auto.components.sidebar.WorktreeList.4d7b73658c', - 'Rename group' - )} - - { - if (row.projectGroup?.id) { - handleDeleteProjectGroup(row.projectGroup.id, row.label) - } - }} - > - {translate( - 'auto.components.sidebar.WorktreeList.902115cdbe', - 'Delete group' - )} - - - - ) : null} - - {isProjectGroupHeader && - !row.repo && - row.projectGroup && - 'parentPath' in row.projectGroup && - row.projectGroup.parentPath ? ( - - - - - - {projectGroupPathStatus?.exists === false - ? getFolderWorkspacePathStatusDescription(projectGroupPathStatus) - : translate( - 'auto.components.sidebar.WorktreeList.bd37a57ac8', - 'Create workspace for {{value0}}', - { value0: row.label } - )} - - - ) : null} - - {row.repo && groupBy === 'repo' ? ( - - - - - - - - - {translate( - 'auto.components.sidebar.WorktreeList.2ef41bf9a7', - 'Project actions' - )} - - - - { - if (row.repo) { - handleOpenRepoSettings(row.repo.id) - } - }} - > - - {translate( - 'auto.components.sidebar.WorktreeList.2cdffbc728', - 'Project Settings' - )} - - { - if (row.repo) { - handleOpenRepoSettings( - row.repo.id, - getRepositoryIconSectionId(row.repo.id) - ) - } - }} - > - - {translate( - 'auto.components.sidebar.WorktreeList.e82d3589a1', - 'Change Project Icon' - )} - - {row.repo && isGitRepoKind(row.repo) ? ( - { - if (row.repo) { - handleOpenWorktreeVisibility(row.repo) - } - }} - > - - {getWorktreeVisibilityMenuLabel(row.repo)} - - ) : null} - { - if (row.repo) { - handleCreateGroupFromRepo(row.repo) - } - }} - > - - {translate( - 'auto.components.sidebar.WorktreeList.cbfd565f83', - 'New group from project' - )} - - {projectGroups.length > 0 ? ( - - - - {translate( - 'auto.components.sidebar.WorktreeList.4a08fb55f2', - 'Move to group' - )} - - - {projectGroups.map((group) => ( - { - if (row.repo) { - handleMoveProjectToGroup(row.repo, group.id) - } - }} - > - {group.name} - - ))} - - - ) : null} - {row.repo.projectGroupId ? ( - { - if (row.repo) { - handleRemoveProjectFromGroup(row.repo) - } - }} - > - - {translate( - 'auto.components.sidebar.WorktreeList.64e55f7f01', - 'Remove from group' - )} - - ) : null} - - { - if (row.repo) { - handleRemoveProject(row.repo) - } - }} - > - - {translate( - 'auto.components.sidebar.WorktreeList.c83968f87f', - 'Remove Project' - )} - - - - ) : null} - - {row.repo && groupBy === 'repo' ? ( - - - {createState?.disabled ? ( - event.stopPropagation()} - onPointerDown={handleRepoHeaderActionPointerDown} - > - - - ) : ( - - )} - - - {createState?.tooltip ?? - translate( - 'auto.components.sidebar.WorktreeList.bb85cd86ba', - 'Create workspace for {{value0}}', - { value0: row.label } - )} - - - ) : null} -
-
-
- ) - } - - const renderWorktreeRow = ( - itemRow: WorktreeItemRow, - nested: boolean, - lineageChildren?: React.ReactNode, - forceActiveSurface = false - ) => { - const lineageToggleGroupKey = itemRow.lineageGroupKey - const experimentalNewWorktreeCardStyle = - settings?.experimentalNewWorktreeCardStyle === true - const projectGroupId = itemRow.repo?.projectGroupId - const isFolderBackedRepoChild = - groupBy === 'repo' && - Boolean(projectGroupId && folderBackedProjectGroupIds.has(projectGroupId)) - // Why: experimental in-card lineage inherits the parent surface; legacy cards keep depth-based nested geometry. - const paddingDepth = nested ? Math.max(0, itemRow.depth - 1) : itemRow.depth - const getCardContentIndent = (lineageDepth: number): number => - isFolderBackedRepoChild - ? getFolderBackedRepoWorktreeCardContentIndent({ - groupDepth: itemRow.groupDepth, - lineageDepth - }) - : getWorktreeCardContentIndent({ - isGrouped: groupBy !== 'none', - groupDepth: itemRow.groupDepth, - lineageDepth - }) - const inheritedCardContentIndent = getCardContentIndent(0) - const nestedLineageGeometry = nested - ? getLineageNestedRowGeometry({ - experimentalNewWorktreeCardStyle, - inheritedCardContentIndent, - lineageDepth: itemRow.depth - }) - : null - // Why: grouped rows inherit their header depth, but the card surface still spans the full row. - const paddingLeft = - nested && groupBy !== 'none' - ? getWorktreeCardContentIndent({ - isGrouped: false, - groupDepth: itemRow.groupDepth, - lineageDepth: paddingDepth - }) - : getCardContentIndent(paddingDepth) - const surfaceInset = nested - ? nestedLineageGeometry!.surfaceInset - : isFolderBackedRepoChild - ? getFolderBackedRepoWorktreeCardSurfaceInset({ - groupDepth: itemRow.groupDepth, - lineageDepth: paddingDepth - }) - : getWorktreeCardSurfaceInset({ - isGrouped: groupBy !== 'none', - groupDepth: itemRow.groupDepth - }) - const cardContentIndent = nested - ? nestedLineageGeometry!.cardContentIndent - : Math.max(0, paddingLeft - surfaceInset) - const lineageChildrenStyle = lineageChildren - ? getLineageChildrenInlineStyle( - nestedLineageGeometry?.lineageChildrenInlineOffset ?? - LINEAGE_CHILDREN_INLINE_OFFSET - ) - : undefined - const worktreeDragGroupKey = groupKeyByRowKey.get(itemRow.rowKey) - const worktreeDragGroupIndex = groupIndexByRowKey.get(itemRow.rowKey) - const revealHighlightTone = - agentSendTargetWorktreeId === itemRow.worktree.id ? 'ai' : 'default' - const isLineageDropTarget = - worktreeDragState.draggingWorktreeId && - (worktreePointerDragRef.current?.latestStatusDropTarget?.target.lineageParentId === - itemRow.worktree.id || - nativeLineageDropTargetId === itemRow.worktree.id) - const isPinnedOverlayRow = itemRow.sectionKey === PINNED_GROUP_KEY - const isActiveWorktree = activeWorktreeId === itemRow.worktree.id - const activeSurfaceVariant = getActiveSurfaceVariant(itemRow) - return ( -
{ - if (nested) { - event.stopPropagation() - } - handleWorktreeRowPointerDown(event, itemRow.worktree.id, itemRow.rowKey) - }} - style={{ - paddingLeft: surfaceInset > 0 ? `${surfaceInset}px` : undefined - }} - > - -
- ) - } - - const renderLineageDescendants = ( - parent: WorktreeItemRow, - descendants: readonly WorktreeItemRow[] - ): React.ReactNode | undefined => { - const childNodes: React.ReactNode[] = [] - let cursor = 0 - while (cursor < descendants.length) { - const child = descendants[cursor] - if (!child || child.depth !== parent.depth + 1) { - cursor++ - continue - } - - let nextSiblingIndex = cursor + 1 - while ( - nextSiblingIndex < descendants.length && - descendants[nextSiblingIndex]!.depth > child.depth - ) { - nextSiblingIndex++ - } - - const childLineageChildren = renderLineageDescendants( - child, - descendants.slice(cursor + 1, nextSiblingIndex) - ) - childNodes.push(renderWorktreeRow(child, true, childLineageChildren)) - cursor = nextSiblingIndex - } - return childNodes.length > 0 ? childNodes : undefined - } - - if (row.type === 'lineage-group') { - const [parent, ...children] = row.rows - const childIsActive = children.some((child) => child.worktree.id === activeWorktreeId) - const parentPreviewOffset = parent - ? (worktreeDragState.previewOffsetsByWorktreeId.get(parent.worktree.id) ?? 0) - : 0 - return ( -
-
- {parent - ? renderWorktreeRow( - parent, - false, - renderLineageDescendants(parent, children), - childIsActive - ) - : null} -
-
- ) - } - - if (row.type === 'imported-worktrees-card') { - const actionState = importedWorktreeCardActionState.get(row.repo.id) - return ( -
- handleShowImportedWorktrees(row.repo.id)} - onKeepHidden={ - canKeepImportedWorktreesHidden(row, actionState) - ? () => handleKeepImportedWorktreesHidden(row.repo.id) - : undefined - } - /> -
- ) - } - - if (row.type === 'new-external-worktrees-inbox') { - const actionState = newExternalWorktreeInboxActionState.get(row.repo.id) - return ( -
- handleOpenWorktreeVisibility(row.repo)} - onSuppress={() => handleOpenSuppressExternalWorktreeInbox(row.repo.id)} - /> -
- ) - } - - if (row.type === 'pending-creation') { - return ( -
- -
- ) - } - - if (row.type === 'folder-workspace') { - const folderWorkspaceRow = row as FolderWorkspaceItemRow - const folderWorktree = folderWorkspaceToWorktree(folderWorkspaceRow.folderWorkspace) - const folderWorkspacePathStatus = getCachedFolderWorkspacePathStatus({ - scope: 'folder-workspace', - folderWorkspaceId: folderWorkspaceRow.folderWorkspace.id - }) - const folderWorkspaceActivationDisabled = - folderWorkspacePathStatus?.exists === false && - (isConfirmedStaleFolderPathStatus(folderWorkspacePathStatus) || - folderWorkspacePathStatus.reason === 'ambiguous-connection') - const folderPrDisplay = getFolderWorkspaceCardPrDisplay({ - folderWorkspaceId: folderWorkspaceRow.folderWorkspace.id, - workspaceLineageByChildKey, - worktreeLineageById, - worktreeMap, - repoMap, - hostedReviewCache, - prCache, - settings - }) - const isFolderBackedWorkspaceChild = - groupBy === 'repo' && folderWorkspaceRow.projectGroup.createdFrom === 'folder-scan' - const { surfaceInset, cardContentIndent } = getFolderWorkspaceRowGeometry({ - experimentalNewWorktreeCardStyle: newCardStyle, - isFolderBackedWorkspaceChild, - isGrouped: groupBy !== 'none', - groupDepth: folderWorkspaceRow.groupDepth, - lineageDepth: folderWorkspaceRow.depth - }) - return ( -
- handleWorktreeRowPointerDown(event, folderWorktree.id, folderWorktree.id) - } - > -
0 ? { paddingLeft: surfaceInset } : undefined} - > - -
- -
-
-
- ) - } - - const itemWorkspaceStatus = - groupBy === 'workspace-status' - ? getWorkspaceStatus(row.worktree, workspaceStatuses) - : null - const itemPreviewOffset = - worktreeDragState.previewOffsetsByWorktreeId.get(row.worktree.id) ?? 0 - - return ( -
handleWorkspaceStatusDragOver(event, itemWorkspaceStatus) - : undefined - } - onDragLeave={itemWorkspaceStatus ? handleWorkspaceStatusDragLeave : undefined} - onDrop={ - itemWorkspaceStatus - ? (event) => handleWorkspaceStatusDrop(event, itemWorkspaceStatus) - : undefined - } - > - {renderWorktreeRow(row, false)} -
- ) - })} -
-
- {showScrollToTop ? : null} -
- ) -}) + EMPTY_PROJECT_GROUPS, + NOOP_WORKSPACE_BOARD_DRAG_PREVIEW_CALLBACK +} from './worktree-list/virtualized-worktree-viewport-props' +import { useAgentSendTargetWorktreeId } from './worktree-list/use-agent-send-target-worktree' +import { useEffectiveCollapsedGroups } from './worktree-list/use-effective-collapsed-groups' +import { useProjectGroupDialogs } from './worktree-list/use-project-group-dialogs' +import { useSidebarExternalWorktreeCards } from './worktree-list/use-sidebar-external-worktree-cards' +import { useSidebarHostVisibleScope } from './worktree-list/use-sidebar-host-visible-scope' +import { useSidebarRevealRequests } from './worktree-list/use-sidebar-reveal-requests' +import { useSidebarSectionRows } from './worktree-list/use-sidebar-section-rows' +import { useSidebarWorktreeFilters } from './worktree-list/use-sidebar-worktree-filters' +import { useSidebarWorktreeSelection } from './worktree-list/use-sidebar-worktree-selection' +import { useSidebarWorktreeSortOrder } from './worktree-list/use-sidebar-worktree-sort-order' +import { useVisibleSidebarWorktrees } from './worktree-list/use-visible-sidebar-worktrees' +import { useWorktreeStatusMutations } from './worktree-list/use-worktree-status-mutations' type WorktreeListProps = { scrollOffsetRef: React.MutableRefObject @@ -5213,11 +47,6 @@ type WorktreeListProps = { onWorkspaceBoardDragPreviewCancel?: () => void } -export function installWorktreeVisibleRefreshVisibilityListener(onChange: () => void): () => void { - document.addEventListener('visibilitychange', onChange) - return () => document.removeEventListener('visibilitychange', onChange) -} - const WorktreeList = React.memo(function WorktreeList({ scrollOffsetRef, scrollAnchorRef, @@ -5230,9 +59,9 @@ const WorktreeList = React.memo(function WorktreeList({ const allWorktrees = useAllWorktrees() const repoMap = useRepoMap() const worktreeMap = useWorktreeMap() + const repos = useAppStore((s) => s.repos) const worktreeLineageById = useAppStore((s) => s.worktreeLineageById) const workspaceLineageByChildKey = useAppStore((s) => s.workspaceLineageByChildKey) - const worktreesByRepo = useAppStore((s) => s.worktreesByRepo) const detectedWorktreesByRepo = useAppStore((s) => s.detectedWorktreesByRepo) const activeWorktreeId = useAppStore((s) => s.activeWorktreeId) const activeWorkspaceKey = useAppStore((s) => s.activeWorkspaceKey) @@ -5241,365 +70,29 @@ const WorktreeList = React.memo(function WorktreeList({ [activeWorkspaceKey, activeWorktreeId] ) const groupBy = useAppStore((s) => s.groupBy) - const setGroupBy = useAppStore((s) => s.setGroupBy) - const workspaceHostScope = useAppStore((s) => s.workspaceHostScope) - const visibleWorkspaceHostIds = useAppStore((s) => s.visibleWorkspaceHostIds) - const workspaceHostOrder = useAppStore((s) => s.workspaceHostOrder) - const setWorkspaceHostOrder = useAppStore((s) => s.setWorkspaceHostOrder) const workspaceStatuses = useAppStore((s) => s.workspaceStatuses) const sortBy = useAppStore((s) => s.sortBy) - const setSortBy = useAppStore((s) => s.setSortBy) const projectOrderBy = useAppStore((s) => s.projectOrderBy) - const showSleepingWorkspaces = useAppStore((s) => s.showSleepingWorkspaces) - const agentStatusEpoch = useAppStore((s) => (!showSleepingWorkspaces ? s.agentStatusEpoch : 0)) - const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace) - const hideAutomationGeneratedWorkspaces = useAppStore((s) => s.hideAutomationGeneratedWorkspaces) - const hideCliCreatedWorkspaces = useAppStore((s) => s.hideCliCreatedWorkspaces) - const hideDetachedHeadWorkspaces = useAppStore((s) => s.hideDetachedHeadWorkspaces) - const hideWorkspacesFromOtherDevices = useAppStore((s) => s.hideWorkspacesFromOtherDevices) - const alwaysShowDefaultBranchWorkspace = useAppStore((s) => s.alwaysShowDefaultBranchWorkspace) - const filterRepoIds = useAppStore((s) => s.filterRepoIds) const openModal = useAppStore((s) => s.openModal) const openSettingsPage = useAppStore((s) => s.openSettingsPage) const openSettingsTarget = useAppStore((s) => s.openSettingsTarget) - const updateWorktreeMeta = useAppStore((s) => s.updateWorktreeMeta) - const updateWorktreesMeta = useAppStore((s) => s.updateWorktreesMeta) - const updateRepo = useAppStore((s) => s.updateRepo) - const fetchWorktrees = useAppStore((s) => s.fetchWorktrees) const activeView = useAppStore((s) => s.activeView) const activeModal = useAppStore((s) => s.activeModal) const pendingRevealWorktree = useAppStore((s) => s.pendingRevealWorktree) const pendingRevealSidebarRow = useAppStore((s) => s.pendingRevealSidebarRow) - const revealWorktreeInSidebar = useAppStore((s) => s.revealWorktreeInSidebar) - const revealSidebarRow = useAppStore((s) => s.revealSidebarRow) - const setWorktreesPinnedAndReveal = useAppStore((s) => s.setWorktreesPinnedAndReveal) const clearPendingRevealWorktreeId = useAppStore((s) => s.clearPendingRevealWorktreeId) const clearPendingRevealSidebarRow = useAppStore((s) => s.clearPendingRevealSidebarRow) - const agentSendPopoverTargetMode = useAppStore((s) => s.agentSendPopoverTargetMode) - // Why: eligibility only matters while the picker is open; when closed, don't subscribe to wake-time layout churn. - const agentTargetStatusByPaneKey = useAppStore((s) => - agentSendPopoverTargetMode ? s.agentStatusByPaneKey : EMPTY_AGENT_STATUS_BY_PANE_KEY - ) - const agentTargetStatusEpoch = useAppStore((s) => - agentSendPopoverTargetMode ? s.agentStatusEpoch : 0 - ) - const agentTargetTabsByWorktree = useAppStore((s) => - agentSendPopoverTargetMode ? s.tabsByWorktree : EMPTY_TABS_BY_WORKTREE - ) - const agentTargetTerminalLayoutsByTabId = useAppStore((s) => - agentSendPopoverTargetMode ? s.terminalLayoutsByTabId : EMPTY_TERMINAL_LAYOUTS_BY_TAB_ID - ) - const agentTargetPtyIdsByTabId = useAppStore((s) => - agentSendPopoverTargetMode ? s.ptyIdsByTabId : EMPTY_PTY_IDS_BY_TAB_ID - ) - const agentTargetRuntimePaneTitlesByTabId = useAppStore((s) => - agentSendPopoverTargetMode ? s.runtimePaneTitlesByTabId : EMPTY_RUNTIME_PANE_TITLES_BY_TAB_ID - ) - const agentSendTargetWorktreeId = useMemo(() => { - void agentTargetStatusEpoch - if (!agentSendPopoverTargetMode) { - return null - } - const targets = deriveRunningAgentSendTargets( - { - agentStatusByPaneKey: agentTargetStatusByPaneKey, - tabsByWorktree: agentTargetTabsByWorktree, - terminalLayoutsByTabId: agentTargetTerminalLayoutsByTabId, - ptyIdsByTabId: agentTargetPtyIdsByTabId, - runtimePaneTitlesByTabId: agentTargetRuntimePaneTitlesByTabId - }, - agentSendPopoverTargetMode.worktreeId - ) - return targets.some((target) => target.status === 'eligible') - ? agentSendPopoverTargetMode.worktreeId - : null - }, [ - // Why: eligibility can flip when the stale-boundary scheduler bumps this epoch without replacing the status map. - agentTargetStatusEpoch, - agentSendPopoverTargetMode, - agentTargetStatusByPaneKey, - agentTargetTabsByWorktree, - agentTargetTerminalLayoutsByTabId, - agentTargetPtyIdsByTabId, - agentTargetRuntimePaneTitlesByTabId - ]) - - // Read tabsByWorktree when needed for filtering or sorting - const needsActivityMaps = !showSleepingWorkspaces || sortBy === 'smart' - const tabsByWorktree = useAppStore((s) => - needsActivityMaps ? getVisibleWorktreeTerminalActivityTabs(s.tabsByWorktree) : null - ) - const ptyIdsByTabId = useAppStore((s) => (needsActivityMaps ? s.ptyIdsByTabId : null)) - const browserTabsByWorktree = useAppStore((s) => - !showSleepingWorkspaces ? getVisibleWorktreeBrowserActivityTabs(s.browserTabsByWorktree) : null - ) - + 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 settings = useAppStore((s) => s.settings) const cardProps = useAppStore((s) => s.worktreeCardProperties) - const { prCache, hostedReviewCache } = useAppStore( useShallow((s) => selectWorktreeListReviewCacheInputs(s, groupBy, cardProps)) ) - const settings = useAppStore((s) => s.settings) const pinnedDisplayPolicy = getPinnedWorktreeDisplayPolicy(settings) - const sshTargetLabels = useAppStore((s) => s.sshTargetLabels) - const sshConnectionStates = useAppStore((s) => s.sshConnectionStates) - const runtimeEnvironments = useAppStore((s) => s.runtimeEnvironments) - const runtimeStatusByEnvironmentId = useAppStore((s) => s.runtimeStatusByEnvironmentId) - const pairedDeviceIdsByEnvironment = useMemo( - () => - hideWorkspacesFromOtherDevices - ? getPairedDeviceIdsByEnvironment(runtimeEnvironments, runtimeStatusByEnvironmentId) - : EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT, - [hideWorkspacesFromOtherDevices, runtimeEnvironments, runtimeStatusByEnvironmentId] - ) - - const sortEpoch = useAppStore((s) => s.sortEpoch) - - // Non-archived count — detects structural changes (add/remove) so the debounce below can apply immediately. - const worktreeCount = useMemo(() => { - let count = 0 - for (const worktree of allWorktrees) { - if (!worktree.isArchived) { - count++ - } - } - return count - }, [allWorktrees]) - - // Why debounce: scores are time-decaying, so recomputing on every sortEpoch bump makes worktrees jump; settle to coalesce. - // Structural changes (add/remove) bypass the debounce so a new worktree appears at its sorted position immediately. - const [debouncedSortEpoch, setDebouncedSortEpoch] = useState(sortEpoch) - const prevWorktreeCountRef = useRef(worktreeCount) - useEffect(() => { - if (debouncedSortEpoch === sortEpoch) { - return - } - - const structuralChange = worktreeCount !== prevWorktreeCountRef.current - prevWorktreeCountRef.current = worktreeCount - - // Why: manual drag/drop is direct manipulation; the settle-window delay would make a successful drop look broken. - if (structuralChange || sortBy === 'manual') { - setDebouncedSortEpoch(sortEpoch) - return - } - - const timer = setTimeout(() => setDebouncedSortEpoch(sortEpoch), SORT_SETTLE_MS) - return () => clearTimeout(timer) - }, [sortEpoch, debouncedSortEpoch, worktreeCount, sortBy]) - - // Why a latching ref: a live signal makes Smart authoritative for the session, even after that activity ends. - const sessionHasHadLiveSmartSignal = useRef(false) - - // ── Stable sort order ────────────────────────────────────────── - // Why sortEpoch (not selection): selection side-effects (clearing isUnread, PR-cache refresh) must not reorder the sidebar under the user. - // Why useMemo not useEffect: order must be computed synchronously before the worktrees memo reads it. - // Why a ref alongside the memo: telemetry effects need the last attention map without re-reading store state. - const lastAttentionByWorktreeRef = useRef | null>(null) - - const recomputedSortedIds = useMemo(() => { - const state = useAppStore.getState() - const nonArchivedWorktrees = getAllWorktreesFromState(state).filter( - (worktree) => !worktree.isArchived - ) - const now = Date.now() - - // Why cold-start detection: agent-status hydrates async, so the warm comparator would collapse all to Class 4; keep the persisted order until a live signal appears. - if (sortBy === 'smart' && !sessionHasHadLiveSmartSignal.current) { - // Why tabHasLivePty over tab.ptyId: slept terminals keep tab.ptyId as a wake hint, so it'd falsely keep cold-start ordering off. - const hasAnyLivePty = Object.values(state.tabsByWorktree) - .flat() - .some((tab) => tabHasLivePty(state.ptyIdsByTabId, tab.id)) - if ( - hasAnyLivePty || - hasFreshAttributedAgentStatus(state.agentStatusByPaneKey, now, state.tabsByWorktree) - ) { - sessionHasHadLiveSmartSignal.current = true - } else { - nonArchivedWorktrees.sort( - (a, b) => b.sortOrder - a.sortOrder || compareWorktreeSortLabel(a, b) - ) - lastAttentionByWorktreeRef.current = null - return nonArchivedWorktrees.map((w) => w.id) - } - } - - const currentTabs = state.tabsByWorktree - // Why precompute: hot sort — build the attention map once so the O(N log N) comparator does O(1) lookups. - const attentionByWorktree = - sortBy === 'smart' - ? buildAttentionByWorktree( - nonArchivedWorktrees, - currentTabs, - state.agentStatusByPaneKey, - state.runtimePaneTitlesByTabId, - state.ptyIdsByTabId, - now, - state.migrationUnsupportedByPtyId, - state.terminalLayoutsByTabId - ) - : new Map() - lastAttentionByWorktreeRef.current = sortBy === 'smart' ? attentionByWorktree : null - nonArchivedWorktrees.sort(buildWorktreeComparator(sortBy, repoMap, now, attentionByWorktree)) - return nonArchivedWorktrees.map((w) => w.id) - // debouncedSortEpoch is an intentional trigger not read in the memo; its change (debounced) signals a recompute. - // oxlint-disable-next-line react-hooks/exhaustive-deps - }, [debouncedSortEpoch, repoMap, sortBy]) - // Why: stable ID order prevents rank-only refreshes from echoing an unchanged snapshot. - const sortedIds = useReusedArrayIdentity(recomputedSortedIds) - - // Why a ref of prior class: fire class_1_promotion only on transitions into Class 1, not every recompute that stays there. - const prevClassByWorktreeIdRef = useRef>(new Map()) - // Why gate the first observation: an empty prev-class map makes every existing Class-1 worktree look freshly promoted; treat the first pass as a silent baseline. - const hasObservedSmartOnceRef = useRef(false) - - useEffect(() => { - const attention = lastAttentionByWorktreeRef.current - if (sortBy !== 'smart' || !attention) { - // Why reset: leaving Smart drops the prior-class map (and first-observation gate) so re-entry doesn't fire stale promotions. - prevClassByWorktreeIdRef.current = new Map() - hasObservedSmartOnceRef.current = false - return - } - const next = new Map() - const isFirstObservation = !hasObservedSmartOnceRef.current - for (const [worktreeId, info] of attention) { - const prev = prevClassByWorktreeIdRef.current.get(worktreeId) - if (!isFirstObservation && info.cls === 1 && prev !== 1 && info.cause) { - track('smart_sort_class_1_promotion', { cause: info.cause }) - } - next.set(worktreeId, info.cls) - } - prevClassByWorktreeIdRef.current = next - hasObservedSmartOnceRef.current = true - }, [sortBy, recomputedSortedIds]) - - // Why retry on recomputation: Smart may activate before attention hydrates; fire once, then stay quiet until the user leaves Smart. - const hasTrackedSmartDistributionRef = useRef(false) - useEffect(() => { - if (sortBy !== 'smart') { - hasTrackedSmartDistributionRef.current = false - return - } - if (hasTrackedSmartDistributionRef.current) { - return - } - const attention = lastAttentionByWorktreeRef.current - if (!attention || attention.size === 0) { - return - } - let class1 = 0 - let class2 = 0 - let class3 = 0 - let class4 = 0 - for (const info of attention.values()) { - if (info.cls === 1) { - class1++ - } else if (info.cls === 2) { - class2++ - } else if (info.cls === 3) { - class3++ - } else { - class4++ - } - } - track('smart_sort_class_distribution', { - class_1: class1, - class_2: class2, - class_3: class3, - class_4: class4, - total_worktrees: attention.size - }) - hasTrackedSmartDistributionRef.current = true - }, [sortBy, recomputedSortedIds]) - - // Why fire on the transition: switching away from Smart is the signal; compare via ref so a round-trip doesn't double-fire. - const prevSortByRef = useRef(sortBy) - useEffect(() => { - const prev = prevSortByRef.current - prevSortByRef.current = sortBy - if (prev === 'smart' && sortBy === 'recent') { - track('smart_to_recent_switch', {}) - } - }, [sortBy]) - - // Why: only persist during live sessions so cold start reads the persisted order instead of overwriting it. - useEffect(() => { - if (sortBy !== 'smart' || sortedIds.length === 0 || !sessionHasHadLiveSmartSignal.current) { - return - } - // Why: sortOrder lives in each host's worktreeMeta, so persist each host's ids on that host. - const state = useAppStore.getState() - persistWorktreeSortOrderByHost(state, sortedIds) - }, [sortedIds, sortBy]) - - // Flatten/filter/sort via the shared utility so card order matches Cmd+1–9 numbering. - const recomputedVisibleWorktrees = useMemo(() => { - void agentStatusEpoch - const ids = computeVisibleWorktreeIds(worktreesByRepo, sortedIds, { - filterRepoIds, - showSleepingWorkspaces, - tabsByWorktree, - ptyIdsByTabId, - browserTabsByWorktree, - // Why snapshot on agentStatusEpoch: update membership immediately without repainting on every hook ping. - worktreeIdsWithLiveAgent: showSleepingWorkspaces - ? EMPTY_WORKTREE_ID_SET - : getWorktreeIdsWithLiveAgent( - useAppStore.getState().agentStatusByPaneKey, - tabsByWorktree, - Date.now() - ), - hideDefaultBranchWorkspace, - hideAutomationGeneratedWorkspaces, - hideCliCreatedWorkspaces, - hideDetachedHeadWorkspaces, - hideWorkspacesFromOtherDevices, - pairedDeviceIdsByEnvironment, - alwaysShowDefaultBranchWorkspace, - repoMap, - workspaceHostScope, - visibleWorkspaceHostIds, - defaultHostId: getSettingsFocusedExecutionHostId(settings), - worktreeLineageById, - forcedVisibleWorktreeIds: agentSendTargetWorktreeId ? [agentSendTargetWorktreeId] : undefined - }) - return ids.map((id) => worktreeMap.get(id)).filter((w): w is Worktree => w != null) - }, [ - agentSendTargetWorktreeId, - agentStatusEpoch, - filterRepoIds, - showSleepingWorkspaces, - hideDefaultBranchWorkspace, - hideAutomationGeneratedWorkspaces, - hideCliCreatedWorkspaces, - hideDetachedHeadWorkspaces, - hideWorkspacesFromOtherDevices, - alwaysShowDefaultBranchWorkspace, - workspaceHostScope, - visibleWorkspaceHostIds, - settings, - repoMap, - tabsByWorktree, - ptyIdsByTabId, - browserTabsByWorktree, - sortedIds, - worktreeMap, - worktreeLineageById, - worktreesByRepo, - pairedDeviceIdsByEnvironment - ]) - // Why: agentStatusEpoch bumps recompute this memo even when membership and - // order are unchanged; keeping the previous identity stops the whole - // rows/sectionRows/renderedWorktrees chain from churning per epoch. - const visibleWorktrees = useReusedArrayIdentity(recomputedVisibleWorktrees) - - const worktrees = visibleWorktrees - const collapsedGroups = useAppStore((s) => s.collapsedGroups) - const toggleGroup = useAppStore((s) => s.toggleCollapsedGroup) - - // Why: manual header order is bound to state.repos; Recent/Smart derive order from the sorted worktree stream. - const repos = useAppStore((s) => s.repos) + const defaultHostId = getSettingsFocusedExecutionHostId(settings) const projectHostSetupProjection = useProjectHostSetupProjection() const projectGrouping = useMemo( () => ({ @@ -5608,427 +101,89 @@ const WorktreeList = React.memo(function WorktreeList({ }), [projectHostSetupProjection] ) - const projectGroups = useAppStore((s) => s.projectGroups ?? EMPTY_PROJECT_GROUPS) - const folderWorkspaces = useAppStore((s) => s.folderWorkspaces) - const effectiveCollapsedGroups = useMemo(() => { - if (!agentSendTargetWorktreeId) { - return collapsedGroups - } - const targetWorktree = worktreeMap.get(agentSendTargetWorktreeId) - if (!targetWorktree) { - return collapsedGroups - } - const next = new Set(collapsedGroups) - if (targetWorktree.isPinned) { - next.delete(PINNED_GROUP_KEY) - } else { - for (const groupKey of getGroupKeysForWorktree( - groupBy, - targetWorktree, - repoMap, - prCache, - workspaceStatuses, - settings, - projectGroups, - projectGrouping - )) { - next.delete(groupKey) - } - } - for (const parent of getWorktreeLineageAncestors( - targetWorktree, - worktreeLineageById, - worktreeMap - )) { - next.delete(getLineageGroupKey(parent.id)) - } - return next - }, [ - agentSendTargetWorktreeId, - collapsedGroups, - groupBy, - prCache, - projectGroups, - projectGrouping, + const agentSendTargetWorktreeId = useAgentSendTargetWorktreeId() + const { filterState, hasFilters, clearFilters } = useSidebarWorktreeFilters() + const sortedIds = useSidebarWorktreeSortOrder({ allWorktrees, repoMap, sortBy }) + const { visibleWorktrees, pairedDeviceIdsByEnvironment } = useVisibleSidebarWorktrees({ + filterState, + sortBy, + sortedIds, repoMap, + worktreeMap, + worktreeLineageById, + settings, + agentSendTargetWorktreeId + }) + const effectiveCollapsedGroups = useEffectiveCollapsedGroups({ + collapsedGroups, + agentSendTargetWorktreeId, + groupBy, + repoMap, + worktreeMap, + worktreeLineageById, + prCache, + workspaceStatuses, + settings, + projectGroups, + projectGrouping + }) + const visibleScope = useSidebarHostVisibleScope({ + filterState, + defaultHostId, + repos, + projectGroups, + folderWorkspaces, + pairedDeviceIdsByEnvironment + }) + const externalWorktreeCards = useSidebarExternalWorktreeCards({ + repos, + visibleReposForRows: visibleScope.visibleReposForRows, + detectedWorktreesByRepo, + filterRepoIds: filterState.filterRepoIds + }) + const rowModel = useSidebarSectionRows({ + groupBy, + projectOrderBy, + pinnedDisplayPolicy, + defaultHostId, + worktrees: visibleWorktrees, + repos, + repoMap, + worktreeMap, + worktreeLineageById, + prCache, settings, workspaceStatuses, - worktreeLineageById, - worktreeMap - ]) - const defaultHostId = getSettingsFocusedExecutionHostId(settings) - const visibleHostIdSet = useMemo( - () => getVisibleSidebarHostIdSet(visibleWorkspaceHostIds, workspaceHostScope), - [visibleWorkspaceHostIds, workspaceHostScope] - ) - const visibleReposForRows = useMemo(() => { - if (!visibleHostIdSet) { - return repos - } - return repos.filter((repo) => { - const hostId = - repo.connectionId || repo.executionHostId ? getRepoExecutionHostId(repo) : defaultHostId - return visibleHostIdSet.has(hostId) - }) - }, [defaultHostId, repos, visibleHostIdSet]) - const visibleProjectGroupsForRows = useMemo( - () => filterProjectGroupsForVisibleHosts(projectGroups, visibleHostIdSet, defaultHostId), - [defaultHostId, projectGroups, visibleHostIdSet] - ) - const visibleFolderWorkspacesForRows = useMemo(() => { - const hostVisibleWorkspaces = filterFolderWorkspacesForVisibleHosts( - folderWorkspaces, - projectGroups, - visibleHostIdSet, - defaultHostId - ) - if (!hideWorkspacesFromOtherDevices) { - return hostVisibleWorkspaces - } - return filterFolderWorkspacesFromOtherDevices( - hostVisibleWorkspaces, - pairedDeviceIdsByEnvironment - ) - }, [ - defaultHostId, - folderWorkspaces, - hideWorkspacesFromOtherDevices, - pairedDeviceIdsByEnvironment, - projectGroups, - visibleHostIdSet - ]) - const repoOrder = useMemo(() => { - return getLogicalRepoOrderRankById(repos.map((repo) => repo.id)) - }, [repos]) - const [importedWorktreeCardActionState, setImportedWorktreeCardActionState] = useState< - Map - >(new Map()) - const [newExternalWorktreeInboxActionState, setNewExternalWorktreeInboxActionState] = useState< - Map - >(new Map()) - const [suppressExternalWorktreeInboxRepoId, setSuppressExternalWorktreeInboxRepoId] = useState< - string | null - >(null) - const importedWorktreesByRepo = useMemo(() => { - const forceVisibleRepoIds = new Set( - [...importedWorktreeCardActionState.entries()] - .filter(([, state]) => state.forceVisible) - .map(([repoId]) => repoId) - ) - return buildImportedWorktreesCardCandidates({ - repos: visibleReposForRows, - detectedWorktreesByRepo, - filterRepoIds, - forceVisibleRepoIds - }) - }, [detectedWorktreesByRepo, filterRepoIds, importedWorktreeCardActionState, visibleReposForRows]) - const newExternalWorktreesInboxByRepo = useMemo( - () => - buildNewExternalWorktreesInboxCandidates({ - repos: visibleReposForRows, - detectedWorktreesByRepo, - filterRepoIds - }), - [detectedWorktreesByRepo, filterRepoIds, visibleReposForRows] - ) - const placeholderRepoIds = useMemo(() => { - return getEmptyProjectPlaceholderRepoIds({ - groupBy, - repos: visibleReposForRows, - worktreesByRepo, - visibleWorktrees, - filterRepoIds - }) - }, [filterRepoIds, groupBy, visibleReposForRows, visibleWorktrees, worktreesByRepo]) - const allRepoIds = useMemo(() => repos.map((r) => r.id), [repos]) - - // Why: subscribe on a flat key array (useShallow) so progress ticks don't rebuild the whole row model. - // Split on first space — creationId is a UUID (no space) so a space-containing repoId stays intact. - const pendingCreationKeys = useAppStore( - useShallow((s) => - Object.values(s.pendingWorktreeCreations ?? {}).map( - (creation) => `${creation.creationId} ${creation.request.repoId}` - ) - ) - ) - const pendingCreations = useMemo( - () => - pendingCreationKeys.map((key) => { - const separator = key.indexOf(' ') - return { creationId: key.slice(0, separator), repoId: key.slice(separator + 1) } - }), - [pendingCreationKeys] - ) - const hostLabelOverrides = useMemo(() => getHostDisplayLabelOverrides(settings), [settings]) - const hostOptions = useMemo( - () => - buildSidebarHostOptions({ - repos, - sshTargetLabels, - sshConnectionStates, - settings, - runtimeEnvironments, - runtimeStatusByEnvironmentId, - hostLabelOverrides - }), - [ - repos, - sshTargetLabels, - sshConnectionStates, - settings, - runtimeEnvironments, - runtimeStatusByEnvironmentId, - hostLabelOverrides - ] - ) - const hostLabelById = useMemo( - () => new Map(hostOptions.map((host) => [host.id, host.label])), - [hostOptions] - ) - - const rows: Row[] = useMemo( - () => - buildRows( - groupBy, - worktrees, - repoMap, - prCache, - effectiveCollapsedGroups, - repoOrder, - workspaceStatuses, - projectOrderBy, - worktreeLineageById, - worktreeMap, - true, - settings, - visibleProjectGroupsForRows, - placeholderRepoIds, - importedWorktreesByRepo, - newExternalWorktreesInboxByRepo, - pendingCreations, - projectGrouping, - visibleFolderWorkspacesForRows, - hostLabelById, - defaultHostId, - pinnedDisplayPolicy - ), - [ - groupBy, - worktrees, - repoMap, - prCache, - effectiveCollapsedGroups, - defaultHostId, - repoOrder, - workspaceStatuses, - projectOrderBy, - worktreeLineageById, - worktreeMap, - settings, - projectGrouping, - visibleProjectGroupsForRows, - visibleFolderWorkspacesForRows, - placeholderRepoIds, - importedWorktreesByRepo, - newExternalWorktreesInboxByRepo, - pendingCreations, - hostLabelById, - pinnedDisplayPolicy - ] - ) - const orderedHostOptions = useMemo( - () => orderHostSectionOptions(hostOptions, workspaceHostOrder), - [hostOptions, workspaceHostOrder] - ) - const [hostDragActive, setHostDragActive] = useState(false) - const handleReorderHostSections = useCallback( - (orderedVisibleHostIds: ExecutionHostId[]) => { - const visibleHostIds = new Set(orderedVisibleHostIds) - const hostOptionIds = orderedHostOptions.map((host) => host.id) - const knownHostIds = new Set(hostOptionIds) - const nextOrder: ExecutionHostId[] = [...orderedVisibleHostIds] - const seen = new Set(nextOrder) - // Why: dragging only covers rendered hosts; keep non-rendered SSH/runtime hosts in the saved order so they return in place. - for (const hostId of [...workspaceHostOrder, ...hostOptionIds]) { - if (!knownHostIds.has(hostId) || visibleHostIds.has(hostId) || seen.has(hostId)) { - continue - } - nextOrder.push(hostId) - seen.add(hostId) - } - setWorkspaceHostOrder(nextOrder) - }, - [orderedHostOptions, setWorkspaceHostOrder, workspaceHostOrder] - ) - const sectionRows = useMemo( - () => - addHostSectionRows({ - rows, - hostOptions: orderedHostOptions, - workspaceHostScope, - visibleWorkspaceHostIds, - defaultHostId, - collapsedHostKeys: effectiveCollapsedGroups, - forceCollapseHosts: hostDragActive, - // Why: projects/workspaces are the primary sidebar object; host sections are only an explicit host-filter view. - preferProjectGrouping: true - }), - [ - defaultHostId, - effectiveCollapsedGroups, - hostDragActive, - orderedHostOptions, - rows, - visibleWorkspaceHostIds, - workspaceHostScope - ] - ) - const renderedSidebarRowKeys = useMemo(() => { - const keys = new Set() - for (const row of sectionRows) { - if (row.type === 'header') { - keys.add(row.key) - } else if (row.type === 'item') { - keys.add(row.rowKey) - } else if (row.type === 'folder-workspace') { - keys.add(folderWorkspaceKey(row.folderWorkspace.id)) - } else if (row.type === 'pending-creation') { - keys.add(`pending:${row.creationId}`) - } else if (row.type === 'imported-worktrees-card') { - keys.add(row.key) - } else if (row.type === 'new-external-worktrees-inbox') { - keys.add(row.key) - } - } - return keys - }, [sectionRows]) - // Why: status headers move during wake (inactive -> active); key only on grouping mode so row identity survives. - const visibleHostResetKey = visibleWorkspaceHostIds?.join(',') ?? 'all' - const viewportResetKey = `group:${groupBy}:host:${visibleHostResetKey}:lineage` - - // Why: derive order from the built rows, not the flat worktrees array, so Cmd+1–9 match visual positions when grouping reorders cards. - const renderedWorktrees = useMemo( - () => getRenderedWorktreesInSidebarOrder(sectionRows, pinnedDisplayPolicy), - [pinnedDisplayPolicy, sectionRows] - ) - // Why: order-preserving sectionRows rebuilds must not give this array a new - // identity — updateSelectionForGesture depends on it, and a fresh identity - // there defeats React.memo bail-out for every WorktreeCard on epoch bumps. - const renderedWorktreeIds = useReusedArrayIdentity( - useMemo( - () => uniqueWorktreeIds(renderedWorktrees.map((worktree) => worktree.id)), - [renderedWorktrees] - ) - ) - const [selectedWorktreeIds, setSelectedWorktreeIds] = useState>(new Set()) - const [selectionAnchorId, setSelectionAnchorId] = useState(null) - - const prunedSelection = pruneWorktreeSelection( - selectedWorktreeIds, - selectionAnchorId, - renderedWorktreeIds - ) - // Why: filters/grouping can hide selected cards; prune during render so nothing sees stale ids for unrendered worktrees. - if (!areWorktreeSelectionsEqual(selectedWorktreeIds, prunedSelection.selectedIds)) { - setSelectedWorktreeIds(prunedSelection.selectedIds) - } - if (selectionAnchorId !== prunedSelection.anchorId) { - setSelectionAnchorId(prunedSelection.anchorId) - } - - // Why identity reuse: the empty/unchanged-selection case must keep one array - // identity — selectForContextMenu and both drag-start handlers depend on - // this array, and card memo bail-out depends on those staying stable. - const selectedWorktrees = useReusedArrayIdentity( - useMemo(() => { - if (selectedWorktreeIds.size === 0) { - return [] - } - const selected = new Map() - for (const worktree of renderedWorktrees) { - if (selectedWorktreeIds.has(worktree.id) && !selected.has(worktree.id)) { - selected.set(worktree.id, worktree) - } - } - return Array.from(selected.values()) - }, [renderedWorktrees, selectedWorktreeIds]) - ) - - useEffect(() => { - if (selectedWorktreeIds.size === 0) { - return - } - - const clearSelectionOutsideSidebar = (event: PointerEvent): void => { - const target = event.target - const sidebarContainer = document.querySelector('[data-worktree-sidebar-container]') - if (target instanceof Node && sidebarContainer?.contains(target)) { - return - } - setSelectedWorktreeIds(new Set()) - setSelectionAnchorId(null) - } - - document.addEventListener('pointerdown', clearSelectionOutsideSidebar, { capture: true }) - return () => { - document.removeEventListener('pointerdown', clearSelectionOutsideSidebar, { capture: true }) - } - }, [selectedWorktreeIds.size]) - - const updateSelectionForGesture = useCallback( - (event: React.MouseEvent, worktreeId: string): boolean => { - const intent = getWorktreeSelectionIntent(event, navigator.userAgent.includes('Mac')) - const result = updateWorktreeSelection({ - visibleIds: renderedWorktreeIds, - previousSelectedIds: selectedWorktreeIds, - previousAnchorId: selectionAnchorId, - targetId: worktreeId, - intent - }) - setSelectedWorktreeIds(result.selectedIds) - setSelectionAnchorId(result.anchorId) - // Plain click navigates; modifier gestures are selection-only so a batch can build without switching away. - return intent !== 'replace' - }, - [renderedWorktreeIds, selectedWorktreeIds, selectionAnchorId] - ) - - const selectForContextMenu = useCallback( - (_event: React.MouseEvent, worktree: Worktree): readonly Worktree[] => { - if (selectedWorktreeIds.has(worktree.id) && selectedWorktreeIds.size > 1) { - return selectedWorktrees - } - setSelectedWorktreeIds(new Set([worktree.id])) - setSelectionAnchorId(worktree.id) - return [worktree] - }, - [selectedWorktreeIds, selectedWorktrees] - ) + effectiveCollapsedGroups, + projectGrouping, + visibleReposForRows: visibleScope.visibleReposForRows, + visibleProjectGroupsForRows: visibleScope.visibleProjectGroupsForRows, + visibleFolderWorkspacesForRows: visibleScope.visibleFolderWorkspacesForRows, + importedWorktreesByRepo: externalWorktreeCards.importedWorktreesByRepo, + newExternalWorktreesInboxByRepo: externalWorktreeCards.newExternalWorktreesInboxByRepo, + filterRepoIds: filterState.filterRepoIds, + visibleWorkspaceHostIds: filterState.visibleWorkspaceHostIds, + workspaceHostScope: filterState.workspaceHostScope + }) + const selection = useSidebarWorktreeSelection({ + sectionRows: rowModel.sectionRows, + pinnedDisplayPolicy + }) + const statusMutations = useWorktreeStatusMutations({ worktreeMap, workspaceStatuses, sortBy }) + const projectGroupDialogs = useProjectGroupDialogs({ repos, repoMap, projectGroups }) const handleImmediateWorktreeActivate = useCallback((worktreeId: string, rowKey?: string) => { // Why: re-rendering the virtualized sidebar on the pointer path adds visible latency; mutate the row directly and let store state reconcile after. markSidebarWorktreeActiveImmediately(worktreeId, rowKey) }, []) - // Why: full-page nav views aren't scoped to a worktree, so no sidebar card should look selected. - const selectedSidebarWorktreeId = - activeView === 'tasks' || activeView === 'activity' ? null : currentSidebarWorktreeId - - // Why layout effect: the Cmd/Ctrl+1–9 handler can fire right after commit; publishing after paint would leave the shortcut cache stale. - useLayoutEffect(() => { - setVisibleWorktreeIds(renderedWorktreeIds) - // Why null, not []: [] is a real rendered order (all collapsed/filtered); null tells shortcuts the list is unmounted. - return () => setVisibleWorktreeIds(null) - }, [renderedWorktreeIds]) - const handleCreateForRepo = useCallback( (projectId: string) => { openModal('new-workspace-composer', { initialRepoId: projectId, telemetrySource: 'sidebar' }) }, [openModal] ) - const handleOpenRepoSettings = useCallback( (projectId: string, sectionId?: string) => { openSettingsTarget({ pane: 'repo', repoId: projectId, ...(sectionId ? { sectionId } : {}) }) @@ -6036,141 +191,12 @@ const WorktreeList = React.memo(function WorktreeList({ }, [openSettingsPage, openSettingsTarget] ) - const handleOpenWorktreeVisibility = useCallback( (repo: Repo) => { - openModal('worktree-visibility', { - repoId: repo.id, - hostId: getRepoExecutionHostId(repo) - }) + openModal('worktree-visibility', { repoId: repo.id, hostId: getRepoExecutionHostId(repo) }) }, [openModal] ) - - const setImportedWorktreeCardState = useCallback( - (projectId: string, state: ImportedWorktreeCardActionState | null) => { - setImportedWorktreeCardActionState((previous) => { - const next = new Map(previous) - if (state) { - next.set(projectId, state) - } else { - next.delete(projectId) - } - return next - }) - }, - [] - ) - - const handleShowImportedWorktrees = useCallback( - async (projectId: string) => { - await showImportedWorktreesCard({ - projectId, - forceVisible: importedWorktreeCardActionState.get(projectId)?.forceVisible === true, - updateRepo, - fetchWorktrees, - setCardState: setImportedWorktreeCardState - }) - }, - [fetchWorktrees, importedWorktreeCardActionState, setImportedWorktreeCardState, updateRepo] - ) - - const handleKeepImportedWorktreesHidden = useCallback( - async (projectId: string) => { - const repo = repos.find((candidate) => candidate.id === projectId) - let detected = detectedWorktreesByRepo[projectId] - // Why: baseline seeding needs authoritative hidden paths, so don't dismiss on a stale snapshot. - if (detected?.authoritative !== true) { - const refreshed = await fetchWorktrees(projectId, { requireAuthoritative: true }) - if (!refreshed) { - setImportedWorktreeCardState(projectId, { - pending: false, - error: IMPORTED_WORKTREES_KEEP_HIDDEN_ERROR - }) - return - } - detected = useAppStore.getState().detectedWorktreesByRepo[projectId] - } - if (detected?.authoritative !== true) { - setImportedWorktreeCardState(projectId, { - pending: false, - error: IMPORTED_WORKTREES_KEEP_HIDDEN_ERROR - }) - return - } - const hiddenWorktrees = getHiddenImportedWorktrees(detected) - await keepImportedWorktreesHiddenCard({ - projectId, - updateRepo, - setCardState: setImportedWorktreeCardState, - hiddenWorktreePaths: hiddenWorktrees.map((worktree) => worktree.path), - existingBaselinePaths: repo?.externalWorktreeInboxBaselinePaths - }) - }, - [detectedWorktreesByRepo, fetchWorktrees, repos, setImportedWorktreeCardState, updateRepo] - ) - - const setNewExternalWorktreeInboxState = useCallback( - (projectId: string, state: NewExternalWorktreesInboxActionState | null) => { - setNewExternalWorktreeInboxActionState((previous) => { - const next = new Map(previous) - if (state) { - next.set(projectId, state) - } else { - next.delete(projectId) - } - return next - }) - }, - [] - ) - - const getNewExternalWorktreeInboxActionArgs = useCallback( - (projectId: string, worktreePaths: readonly string[]) => { - const repo = repos.find((candidate) => candidate.id === projectId) - if (!repo) { - return null - } - return { - projectId, - repo, - worktreePaths, - updateRepo, - fetchWorktrees, - setInboxState: setNewExternalWorktreeInboxState - } - }, - [fetchWorktrees, repos, setNewExternalWorktreeInboxState, updateRepo] - ) - - const handleOpenSuppressExternalWorktreeInbox = useCallback((projectId: string) => { - setSuppressExternalWorktreeInboxRepoId(projectId) - }, []) - - const handleConfirmSuppressExternalWorktreeInbox = useCallback(async () => { - if (!suppressExternalWorktreeInboxRepoId) { - return - } - const projectId = suppressExternalWorktreeInboxRepoId - const inboxWorktrees = newExternalWorktreesInboxByRepo.get(projectId)?.inboxWorktrees ?? [] - const args = getNewExternalWorktreeInboxActionArgs( - projectId, - inboxWorktrees.map((worktree) => worktree.path) - ) - if (!args) { - setSuppressExternalWorktreeInboxRepoId(null) - return - } - const suppressed = await suppressNewExternalWorktreeInbox(args) - if (suppressed) { - setSuppressExternalWorktreeInboxRepoId(null) - } - }, [ - getNewExternalWorktreeInboxActionArgs, - newExternalWorktreesInboxByRepo, - suppressExternalWorktreeInboxRepoId - ]) - const handleRemoveProject = useCallback( (repo: Repo) => { openModal('confirm-remove-folder', { @@ -6181,139 +207,6 @@ const WorktreeList = React.memo(function WorktreeList({ }, [openModal] ) - - const moveProjectToGroup = useAppStore((s) => s.moveProjectToGroup) - const createProjectGroup = useAppStore((s) => s.createProjectGroup) - const updateProjectGroup = useAppStore((s) => s.updateProjectGroup) - const deleteProjectGroupWithContainedProjects = useAppStore( - (s) => s.deleteProjectGroupWithContainedProjects - ) - const [projectGroupNameDialog, setProjectGroupNameDialog] = - useState(null) - const [projectGroupDeleteDialog, setProjectGroupDeleteDialog] = - useState(null) - - const handleCreateGroupFromRepo = useCallback((repo: Repo) => { - setProjectGroupNameDialog({ type: 'create-from-repo', repo }) - }, []) - - const handleMoveProjectToGroup = useCallback( - (repo: Repo, groupId: string) => { - if (repo.projectGroupId === groupId) { - return - } - void moveProjectToGroup(repo.id, groupId) - }, - [moveProjectToGroup] - ) - - const handleRemoveProjectFromGroup = useCallback( - (repo: Repo) => { - void moveProjectToGroup(repo.id, null) - }, - [moveProjectToGroup] - ) - - const handleRenameProjectGroup = useCallback((groupId: string, currentName: string) => { - setProjectGroupNameDialog({ type: 'rename', groupId, currentName }) - }, []) - - const handleSubmitProjectGroupName = useCallback( - async (name: string) => { - if (!projectGroupNameDialog) { - return - } - if (projectGroupNameDialog.type === 'create-from-repo') { - const group = await createProjectGroup(name) - if (group) { - await moveProjectToGroup(projectGroupNameDialog.repo.id, group.id) - } - return - } - await updateProjectGroup(projectGroupNameDialog.groupId, { name }) - }, - [createProjectGroup, moveProjectToGroup, projectGroupNameDialog, updateProjectGroup] - ) - - const projectGroupDeleteTargets = useMemo(() => { - if (!projectGroupDeleteDialog) { - return null - } - return selectProjectGroupRemovalTargets(projectGroups, repos, projectGroupDeleteDialog.groupId) - }, [projectGroupDeleteDialog, projectGroups, repos]) - const projectGroupDeleteProjectCount = projectGroupDeleteTargets?.projectIds.length ?? 0 - const projectGroupDeleteProjectNames = useMemo( - () => - (projectGroupDeleteTargets?.projectIds ?? []).map( - (projectId) => repoMap.get(projectId)?.displayName ?? projectId - ), - [projectGroupDeleteTargets, repoMap] - ) - const projectGroupRemoveContainedProjects = - projectGroupDeleteProjectCount > 0 && projectGroupDeleteDialog?.removeContainedProjects === true - - const handleDeleteProjectGroup = useCallback((groupId: string, groupName: string) => { - setProjectGroupDeleteDialog({ groupId, groupName, removeContainedProjects: false }) - }, []) - - const handleConfirmDeleteProjectGroup = useCallback(async () => { - if (!projectGroupDeleteDialog) { - return - } - try { - const result = await deleteProjectGroupWithContainedProjects( - projectGroupDeleteDialog.groupId, - { - removeContainedProjects: projectGroupRemoveContainedProjects - } - ) - // Why: a missing group is already the desired end state, so only a real delete failure warrants a toast. - if (result.status === 'group-delete-failed') { - toast.error( - translate( - 'auto.components.sidebar.WorktreeList.groupDeleteFailed', - 'Failed to delete group' - ), - { - description: translate( - 'auto.components.sidebar.WorktreeList.groupDeleteFailedDesc', - 'Something went wrong while deleting the group. No projects were removed.' - ) - } - ) - return - } - if (result.status === 'deleted-group' && result.failedProjectRemovals.length > 0) { - const failedCount = result.failedProjectRemovals.length - const requestedCount = result.requestedProjectIds.length - toast.error( - translate( - 'auto.components.sidebar.WorktreeList.b667b59632', - 'Some projects could not be removed from Orca' - ), - { - description: translate( - 'auto.components.sidebar.WorktreeList.f94466bc39', - '{{value0}} of {{value1}} contained project{{value2}} remained after deleting the group.', - { - value0: failedCount, - value1: requestedCount, - value2: requestedCount === 1 ? '' : 's' - } - ) - } - ) - } - } finally { - // Why: deleting contained projects can unmount this dialog before its close handler runs, so the parent owns cleanup. - setProjectGroupDeleteDialog(null) - } - }, [ - deleteProjectGroupWithContainedProjects, - projectGroupRemoveContainedProjects, - projectGroupDeleteDialog - ]) - const handleCreateFolderWorkspace = useCallback( (projectGroup: ProjectGroup) => { if (!projectGroup.parentPath) { @@ -6327,472 +220,55 @@ const WorktreeList = React.memo(function WorktreeList({ [openModal] ) - const moveWorktreeToStatus = useCallback( - (worktreeId: string, status: WorkspaceStatus) => { - const current = worktreeMap.get(worktreeId) - if (!current || getWorkspaceStatus(current, workspaceStatuses) === status) { - return - } - void updateWorktreeMeta(worktreeId, { workspaceStatus: status }) - }, - [updateWorktreeMeta, worktreeMap, workspaceStatuses] - ) - - const moveWorktreesToStatus = useCallback( - (worktreeIds: readonly string[], status: WorkspaceStatus) => { - const updates = new Map() - for (const worktreeId of worktreeIds) { - const current = worktreeMap.get(worktreeId) - if (!current || getWorkspaceStatus(current, workspaceStatuses) === status) { - continue - } - updates.set(worktreeId, { workspaceStatus: status }) - } - if (updates.size > 0) { - void updateWorktreesMeta(updates) - } - }, - [updateWorktreesMeta, worktreeMap, workspaceStatuses] - ) - - const moveWorktreesToStatusAtIndex = useCallback( - (args: { - worktreeIds: readonly string[] - status: WorkspaceStatus - dropIndex: number - groups: readonly WorktreeDragGroup[] - }) => { - const targetGroupKey = getWorkspaceStatusGroupKey(args.status) - const rankByWorktreeId = new Map() - for (const group of args.groups) { - for (const worktreeId of group.worktreeIds) { - const worktree = worktreeMap.get(worktreeId) - if (worktree) { - rankByWorktreeId.set(worktreeId, worktree.manualOrder ?? worktree.sortOrder) - } - } - } - const order = buildManualOrderUpdatesForGroupDrop({ - groups: args.groups, - targetGroupKey, - draggedIds: args.worktreeIds, - dropIndex: args.dropIndex, - now: Date.now(), - rankByWorktreeId - }) - const updates = new Map>() - for (const worktreeId of args.worktreeIds) { - const current = worktreeMap.get(worktreeId) - if (!current) { - continue - } - const next: Partial = {} - if (getWorkspaceStatus(current, workspaceStatuses) !== args.status) { - next.workspaceStatus = args.status - } - updates.set(worktreeId, next) - } - for (const [worktreeId, manualOrder] of order.updates) { - updates.set(worktreeId, { ...updates.get(worktreeId), ...manualOrder }) - } - for (const [worktreeId, update] of Array.from(updates)) { - if (Object.keys(update).length === 0) { - updates.delete(worktreeId) - } - } - if (updates.size === 0) { - return - } - // Why: the insertion line promises exact placement, so persist manual order on a cross-status drop. - if (order.changed) { - setSortBy('manual') - } - void updateWorktreesMeta(updates) - }, - [setSortBy, updateWorktreesMeta, worktreeMap, workspaceStatuses] - ) - - const pinWorktree = useCallback( - (worktreeId: string) => { - setWorktreesPinnedAndReveal([worktreeId], true) - }, - [setWorktreesPinnedAndReveal] - ) - - const pinWorktrees = useCallback( - (worktreeIds: readonly string[]) => { - setWorktreesPinnedAndReveal(worktreeIds, true) - }, - [setWorktreesPinnedAndReveal] - ) - - const reorderWorktrees = useCallback( - (args: { - groups: readonly WorktreeDragGroup[] - sourceGroupKey: string - draggedIds: readonly string[] - dropIndex: number - }) => { - const rankByWorktreeId = new Map() - for (const group of args.groups) { - for (const worktreeId of group.worktreeIds) { - const worktree = worktreeMap.get(worktreeId) - if (worktree) { - rankByWorktreeId.set(worktreeId, worktree.manualOrder ?? worktree.sortOrder) - } - } - } - const result = buildManualOrderUpdatesForVisibleGroups({ - ...args, - now: Date.now(), - rankByWorktreeId - }) - if (!result.changed) { - return - } - // Why: only switch to Manual after a real move so accidental click-drags don't change the sort. - setSortBy('manual') - void updateWorktreesMeta(result.updates) - }, - [setSortBy, updateWorktreesMeta, worktreeMap] - ) - - const shouldShowWorkspaceBoardDropIndicator = useCallback( - (worktreeIds: readonly string[], status: WorkspaceStatus) => { - const sourceGroupKeys = worktreeIds.flatMap((worktreeId) => { - const worktree = worktreeMap.get(worktreeId) - return worktree ? [getWorkspaceStatus(worktree, workspaceStatuses)] : [] - }) - return shouldWriteManualOrderForGroupDrop({ - sortBy, - sourceGroupKeys, - targetGroupKey: status - }) - }, - [sortBy, worktreeMap, workspaceStatuses] - ) - - const dropWorktreesOnWorkspaceBoard = useCallback( - (args: { - worktreeIds: readonly string[] - status: WorkspaceStatus - dropIndex: number - groups: readonly WorktreeDragGroup[] - }) => { - const result = buildWorkspaceKanbanSidebarDropUpdates({ - ...args, - worktreeById: worktreeMap, - workspaceStatuses, - sortBy, - now: Date.now() - }) - if (result.updates.size === 0) { - return - } - // Why: switch to Manual when the drop changes order so the placement stays visible. - if (result.shouldSwitchToManual) { - setSortBy('manual') - } - useAppStore.getState().recordFeatureInteraction('workspace-board-actions') - void updateWorktreesMeta(result.updates) - }, - [setSortBy, sortBy, updateWorktreesMeta, worktreeMap, workspaceStatuses] - ) - - // Why: count hideDefaultBranchWorkspace as a filter so the Clear Filters escape hatch stays reachable when it alone empties the list. - const filterState = useMemo( - () => ({ - showSleepingWorkspaces, - filterRepoIds, - hideDefaultBranchWorkspace, - hideAutomationGeneratedWorkspaces, - hideCliCreatedWorkspaces, - hideDetachedHeadWorkspaces, - hideWorkspacesFromOtherDevices, - alwaysShowDefaultBranchWorkspace, - visibleWorkspaceHostIds, - workspaceHostScope - }), - [ - showSleepingWorkspaces, - filterRepoIds, - hideDefaultBranchWorkspace, - hideAutomationGeneratedWorkspaces, - hideCliCreatedWorkspaces, - hideDetachedHeadWorkspaces, - hideWorkspacesFromOtherDevices, - alwaysShowDefaultBranchWorkspace, - visibleWorkspaceHostIds, - workspaceHostScope - ] - ) - const hasFilters = sidebarHasActiveFilters(filterState) - const setShowSleepingWorkspaces = useAppStore((s) => s.setShowSleepingWorkspaces) - const setHideDefaultBranchWorkspace = useAppStore((s) => s.setHideDefaultBranchWorkspace) - const setHideAutomationGeneratedWorkspaces = useAppStore( - (s) => s.setHideAutomationGeneratedWorkspaces - ) - const setHideCliCreatedWorkspaces = useAppStore((s) => s.setHideCliCreatedWorkspaces) - const setHideDetachedHeadWorkspaces = useAppStore((s) => s.setHideDetachedHeadWorkspaces) - const setHideWorkspacesFromOtherDevices = useAppStore((s) => s.setHideWorkspacesFromOtherDevices) - const setAlwaysShowDefaultBranchWorkspace = useAppStore( - (s) => s.setAlwaysShowDefaultBranchWorkspace - ) - const setFilterRepoIds = useAppStore((s) => s.setFilterRepoIds) - const setVisibleWorkspaceHostIds = useAppStore((s) => s.setVisibleWorkspaceHostIds) - - const clearFilters = useCallback(() => { - const actions = computeClearFilterActions(filterState) - if (actions.resetShowSleepingWorkspaces) { - setShowSleepingWorkspaces(DEFAULT_SHOW_SLEEPING_WORKSPACES) - } - if (actions.resetFilterRepoIds) { - setFilterRepoIds([]) - } - if (actions.resetHideDefaultBranchWorkspace) { - setHideDefaultBranchWorkspace(false) - } - if (actions.resetHideAutomationGeneratedWorkspaces) { - setHideAutomationGeneratedWorkspaces(false) - } - if (actions.resetHideCliCreatedWorkspaces) { - setHideCliCreatedWorkspaces(false) - } - if (actions.resetHideDetachedHeadWorkspaces) { - setHideDetachedHeadWorkspaces(false) - } - if (actions.resetHideWorkspacesFromOtherDevices) { - setHideWorkspacesFromOtherDevices(false) - } - if (actions.resetAlwaysShowDefaultBranchWorkspace) { - setAlwaysShowDefaultBranchWorkspace(true) - } - if (actions.resetVisibleWorkspaceHostIds) { - setVisibleWorkspaceHostIds(null) - } - }, [ - setShowSleepingWorkspaces, - setFilterRepoIds, - setHideDefaultBranchWorkspace, - setHideAutomationGeneratedWorkspaces, - setHideCliCreatedWorkspaces, - setHideDetachedHeadWorkspaces, - setHideWorkspacesFromOtherDevices, - setAlwaysShowDefaultBranchWorkspace, - setVisibleWorkspaceHostIds, - filterState - ]) - - useEffect(() => { - if (!pendingRevealSidebarRow) { - return - } - const rowKey = pendingRevealSidebarRow.rowKey - const isProjectHeaderTarget = - rowKey.startsWith('project-group:') || - rowKey.startsWith('project:') || - rowKey.startsWith('repo:') - if (isProjectHeaderTarget && groupBy !== 'repo') { - setGroupBy('repo') - return - } - if (!renderedSidebarRowKeys.has(rowKey) && hasFilters) { - clearFilters() - } - }, [ - clearFilters, + useSidebarRevealRequests({ groupBy, + renderedSidebarRowKeys: rowModel.renderedSidebarRowKeys, + renderedWorktreeIds: selection.renderedWorktreeIds, + currentSidebarWorktreeId, + worktreeMap, + folderWorkspaces, hasFilters, - pendingRevealSidebarRow, - renderedSidebarRowKeys, - setGroupBy - ]) - - const handleRevealCurrentWorkspaceRequest = useCallback( - (event: Event) => { - const detail = - event instanceof CustomEvent - ? (event.detail as ScrollToCurrentWorkspaceRevealRequestDetail | undefined) - : undefined - if (detail?.target?.type === 'sidebar-row') { - const sidebarDetail = detail as Extract< - ScrollToCurrentWorkspaceRevealRequestDetail, - { target: { type: 'sidebar-row' } } - > - revealSidebarRow(detail.target.rowKey, { - behavior: 'smooth', - highlight: sidebarDetail.highlight !== false - }) - return - } - if (!currentSidebarWorktreeId) { - return - } - const activeWorktree = getKnownSidebarWorktreeById( - currentSidebarWorktreeId, - worktreeMap, - folderWorkspaces - ) - if (!activeWorktree || activeWorktree.isArchived) { - return - } - if (!renderedWorktreeIds.includes(currentSidebarWorktreeId)) { - // Why: the reveal action must show the current workspace, so relax filters that hide it first. - clearFilters() - } - revealWorktreeInSidebar(currentSidebarWorktreeId, { - behavior: 'smooth', - highlight: true, - beginRename: (detail as { beginRename?: boolean } | undefined)?.beginRename === true - }) - }, - [ - clearFilters, - currentSidebarWorktreeId, - folderWorkspaces, - revealSidebarRow, - renderedWorktreeIds, - revealWorktreeInSidebar, - worktreeMap - ] - ) - - useEffect(() => { - window.addEventListener( - SCROLL_TO_CURRENT_WORKSPACE_REVEAL_REQUEST_EVENT, - handleRevealCurrentWorkspaceRequest - ) - return () => { - window.removeEventListener( - SCROLL_TO_CURRENT_WORKSPACE_REVEAL_REQUEST_EVENT, - handleRevealCurrentWorkspaceRequest - ) - } - }, [handleRevealCurrentWorkspaceRequest]) + clearFilters + }) const filtersHideAllRows = hasFilters && - worktrees.length === 0 && - placeholderRepoIds.size === 0 && - importedWorktreesByRepo.size === 0 + visibleWorktrees.length === 0 && + rowModel.placeholderRepoIds.size === 0 && + externalWorktreeCards.importedWorktreesByRepo.size === 0 // Why: when active filters hide every row, the Clear Filters empty state must win over Project Group headers. - if (rows.length === 0 || filtersHideAllRows) { - return ( -
-
-
- - {translate('auto.components.sidebar.WorktreeList.b7acbf038b', 'No workspaces found')} - - {hasFilters && ( - - )} -
-
-
- ) + if (rowModel.rows.length === 0 || filtersHideAllRows) { + return } return ( <> - { - if (!open) { - setProjectGroupNameDialog(null) - } + onConfirmSuppressExternalWorktreeInbox={() => { + void externalWorktreeCards.handleConfirmSuppressExternalWorktreeInbox() }} - onSubmit={handleSubmitProjectGroupName} - /> - repo.id === suppressExternalWorktreeInboxRepoId)?.displayName ?? - '') - : '' - } - pending={ - suppressExternalWorktreeInboxRepoId - ? (newExternalWorktreeInboxActionState.get(suppressExternalWorktreeInboxRepoId) - ?.pending ?? false) - : false - } - onOpenChange={(open) => { - if (!open) { - setSuppressExternalWorktreeInboxRepoId(null) - } - }} - onConfirm={() => { - void handleConfirmSuppressExternalWorktreeInbox() - }} - onOpenRecovery={() => { - if (!suppressExternalWorktreeInboxRepoId) { - return - } - const repo = findRepoForHost(repos, suppressExternalWorktreeInboxRepoId, { settings }) - setSuppressExternalWorktreeInboxRepoId(null) - if (repo) { - handleOpenWorktreeVisibility(repo) - } - }} - /> - { - setProjectGroupDeleteDialog((current) => - current ? { ...current, removeContainedProjects } : current - ) - }} - onOpenChange={(open) => { - if (!open) { - setProjectGroupDeleteDialog(null) - } - }} - onConfirm={handleConfirmDeleteProjectGroup} + onOpenWorktreeVisibility={handleOpenWorktreeVisibility} /> active); key only on grouping mode so row identity survives. + key={`group:${groupBy}:host:${filterState.visibleWorkspaceHostIds?.join(',') ?? 'all'}:lineage`} + rows={rowModel.sectionRows} + // Why: full-page nav views aren't scoped to a worktree, so no sidebar card should look selected. + activeWorktreeId={ + activeView === 'tasks' || activeView === 'activity' ? null : currentSidebarWorktreeId + } currentWorktreeId={currentSidebarWorktreeId} groupBy={groupBy} pinnedDisplayPolicy={pinnedDisplayPolicy} @@ -6802,17 +278,21 @@ const WorktreeList = React.memo(function WorktreeList({ handleCreateForRepo={handleCreateForRepo} handleOpenRepoSettings={handleOpenRepoSettings} handleOpenWorktreeVisibility={handleOpenWorktreeVisibility} - handleShowImportedWorktrees={handleShowImportedWorktrees} - handleKeepImportedWorktreesHidden={handleKeepImportedWorktreesHidden} - importedWorktreeCardActionState={importedWorktreeCardActionState} - handleOpenSuppressExternalWorktreeInbox={handleOpenSuppressExternalWorktreeInbox} - newExternalWorktreeInboxActionState={newExternalWorktreeInboxActionState} + handleShowImportedWorktrees={externalWorktreeCards.handleShowImportedWorktrees} + handleKeepImportedWorktreesHidden={externalWorktreeCards.handleKeepImportedWorktreesHidden} + importedWorktreeCardActionState={externalWorktreeCards.importedWorktreeCardActionState} + handleOpenSuppressExternalWorktreeInbox={ + externalWorktreeCards.handleOpenSuppressExternalWorktreeInbox + } + newExternalWorktreeInboxActionState={ + externalWorktreeCards.newExternalWorktreeInboxActionState + } handleRemoveProject={handleRemoveProject} - handleCreateGroupFromRepo={handleCreateGroupFromRepo} - handleMoveProjectToGroup={handleMoveProjectToGroup} - handleRemoveProjectFromGroup={handleRemoveProjectFromGroup} - handleRenameProjectGroup={handleRenameProjectGroup} - handleDeleteProjectGroup={handleDeleteProjectGroup} + handleCreateGroupFromRepo={projectGroupDialogs.handleCreateGroupFromRepo} + handleMoveProjectToGroup={projectGroupDialogs.handleMoveProjectToGroup} + handleRemoveProjectFromGroup={projectGroupDialogs.handleRemoveProjectFromGroup} + handleRenameProjectGroup={projectGroupDialogs.handleRenameProjectGroup} + handleDeleteProjectGroup={projectGroupDialogs.handleDeleteProjectGroup} handleCreateFolderWorkspace={handleCreateFolderWorkspace} activeModal={activeModal} pendingRevealWorktree={pendingRevealWorktree} @@ -6820,38 +300,40 @@ const WorktreeList = React.memo(function WorktreeList({ clearPendingRevealWorktreeId={clearPendingRevealWorktreeId} clearPendingRevealSidebarRow={clearPendingRevealSidebarRow} agentSendTargetWorktreeId={agentSendTargetWorktreeId} - worktrees={worktrees} + worktrees={visibleWorktrees} folderWorkspaces={folderWorkspaces} - selectedWorktreeIds={selectedWorktreeIds} - selectedWorktrees={selectedWorktrees} - onSelectionGesture={updateSelectionForGesture} + selectedWorktreeIds={selection.selectedWorktreeIds} + selectedWorktrees={selection.selectedWorktrees} + onSelectionGesture={selection.updateSelectionForGesture} onImmediateWorktreeActivate={handleImmediateWorktreeActivate} - onContextMenuSelect={selectForContextMenu} + onContextMenuSelect={selection.selectForContextMenu} repoMap={repoMap} defaultHostId={defaultHostId} worktreeMap={worktreeMap} worktreeLineageById={worktreeLineageById} workspaceLineageByChildKey={workspaceLineageByChildKey} - allRepoIds={allRepoIds} - onReorderHostSections={handleReorderHostSections} - onHostDragActiveChange={setHostDragActive} + allRepoIds={rowModel.allRepoIds} + onReorderHostSections={rowModel.handleReorderHostSections} + onHostDragActiveChange={rowModel.setHostDragActive} prCache={prCache} hostedReviewCache={hostedReviewCache} workspaceStatuses={workspaceStatuses} projectGrouping={projectGrouping} projectGroups={projectGroups} - onMoveWorktreeToStatus={moveWorktreeToStatus} - onMoveWorktreesToStatus={moveWorktreesToStatus} - onMoveWorktreesToStatusAtIndex={moveWorktreesToStatusAtIndex} - onPinWorktree={pinWorktree} - onPinWorktrees={pinWorktrees} - onDropWorktreesOnWorkspaceBoard={dropWorktreesOnWorkspaceBoard} + onMoveWorktreeToStatus={statusMutations.moveWorktreeToStatus} + onMoveWorktreesToStatus={statusMutations.moveWorktreesToStatus} + onMoveWorktreesToStatusAtIndex={statusMutations.moveWorktreesToStatusAtIndex} + onPinWorktree={statusMutations.pinWorktree} + onPinWorktrees={statusMutations.pinWorktrees} + onDropWorktreesOnWorkspaceBoard={statusMutations.dropWorktreesOnWorkspaceBoard} workspaceBoardOpen={workspaceBoardOpen} onWorkspaceBoardDragPreviewStart={onWorkspaceBoardDragPreviewStart} onWorkspaceBoardDragPreviewCommit={onWorkspaceBoardDragPreviewCommit} onWorkspaceBoardDragPreviewCancel={onWorkspaceBoardDragPreviewCancel} - shouldShowWorkspaceBoardDropIndicator={shouldShowWorkspaceBoardDropIndicator} - onReorderWorktrees={reorderWorktrees} + shouldShowWorkspaceBoardDropIndicator={ + statusMutations.shouldShowWorkspaceBoardDropIndicator + } + onReorderWorktrees={statusMutations.reorderWorktrees} scrollOffsetRef={scrollOffsetRef} scrollAnchorRef={scrollAnchorRef} /> diff --git a/src/renderer/src/components/sidebar/project-group-header-dom.test.ts b/src/renderer/src/components/sidebar/project-group-header-dom.test.ts index 5f7d294ec61..58da2de4511 100644 --- a/src/renderer/src/components/sidebar/project-group-header-dom.test.ts +++ b/src/renderer/src/components/sidebar/project-group-header-dom.test.ts @@ -3,7 +3,17 @@ import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' function readWorktreeListSource(): string { - return readFileSync(fileURLToPath(new URL('./WorktreeList.tsx', import.meta.url)), 'utf8') + return readFileSync( + fileURLToPath(new URL('./worktree-list/worktree-section-header-row.tsx', import.meta.url)), + 'utf8' + ) +} + +function readHeaderDragSource(): string { + return readFileSync( + fileURLToPath(new URL('./worktree-list/use-worktree-sidebar-header-drag.ts', import.meta.url)), + 'utf8' + ) } describe('Project Group header drag DOM source', () => { @@ -17,7 +27,7 @@ describe('Project Group header drag DOM source', () => { }) it('commits Project Group manual sorting through updateProjectGroup tabOrder', () => { - const source = readWorktreeListSource() + const source = readHeaderDragSource() expect(source).toContain('const updateProjectGroup = useAppStore((s) => s.updateProjectGroup)') expect(source).toContain('void updateProjectGroup(groupId, { tabOrder })') diff --git a/src/renderer/src/components/sidebar/worktree-keyboard-cycle.test.ts b/src/renderer/src/components/sidebar/worktree-keyboard-cycle.test.ts index 2b8bc7d68cd..edc4f41f47e 100644 --- a/src/renderer/src/components/sidebar/worktree-keyboard-cycle.test.ts +++ b/src/renderer/src/components/sidebar/worktree-keyboard-cycle.test.ts @@ -132,7 +132,9 @@ describe('getCyclableWorktreeIds', () => { describe('WorktreeList keyboard cycling', () => { it('cycles over the rendered rows instead of rebuilding a parallel layout', () => { const source = readFileSync( - fileURLToPath(new URL('./WorktreeList.tsx', import.meta.url)), + fileURLToPath( + new URL('./worktree-list/use-worktree-list-keyboard-navigation.ts', import.meta.url) + ), 'utf8' ) const navigateWorktree = source.slice( diff --git a/src/renderer/src/components/sidebar/worktree-list-groups.test.ts b/src/renderer/src/components/sidebar/worktree-list-groups.test.ts index dacfcb3a139..7e6fe4b3b58 100644 --- a/src/renderer/src/components/sidebar/worktree-list-groups.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-groups.test.ts @@ -63,6 +63,13 @@ function readWorktreeListSource(): string { return readFileSync(fileURLToPath(new URL('./WorktreeList.tsx', import.meta.url)), 'utf8') } +function readSectionHeaderRowSource(): string { + return readFileSync( + fileURLToPath(new URL('./worktree-list/worktree-section-header-row.tsx', import.meta.url)), + 'utf8' + ) +} + const remoteRepo: Repo = { id: 'repo-remote', path: '/home/alice/orca', @@ -3985,7 +3992,7 @@ describe('buildRows workspace lineage nesting', () => { describe('WorktreeList header styles', () => { it('does not title-case workspace group labels', () => { - const source = readWorktreeListSource() + const source = readSectionHeaderRowSource() expect(source).not.toContain('leading-none capitalize') }) @@ -4005,7 +4012,7 @@ describe('WorktreeList header styles', () => { }) it('resolves repo header color from project group headers only', () => { - const source = readWorktreeListSource() + const source = readSectionHeaderRowSource() expect(source).toContain('resolveProjectGroupHeaderColor({') expect(source).toContain('headerKey: row.key') diff --git a/src/renderer/src/components/sidebar/worktree-list-imported-rows.test.ts b/src/renderer/src/components/sidebar/worktree-list-imported-rows.test.ts index 9cda62a93b4..552515ff936 100644 --- a/src/renderer/src/components/sidebar/worktree-list-imported-rows.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-imported-rows.test.ts @@ -1,11 +1,8 @@ import { describe, expect, it } from 'vitest' -import { - canKeepImportedWorktreesHidden, - getRenderRowKey, - getWorktreeDragGroups, - getWorktreeDragIndexes, - renderRowContainsWorktree -} from './WorktreeList' +import { canKeepImportedWorktreesHidden } from './worktree-list/imported-worktrees-card-visibility' +import { getRenderRowKey } from './worktree-list-virtual-rows' +import { getWorktreeDragGroups, getWorktreeDragIndexes } from './worktree-list/worktree-drag-groups' +import { renderRowContainsWorktree } from './worktree-list/render-row-worktree-lookup' import type { Repo } from '../../../../shared/repo-types' import type { Worktree } from '../../../../shared/worktree/types' import type { Row } from './worktree-list-groups' diff --git a/src/renderer/src/components/sidebar/worktree-list-scroll-adjustment.test.ts b/src/renderer/src/components/sidebar/worktree-list-scroll-adjustment.test.ts index f5b9d586f63..dd6f7c0689a 100644 --- a/src/renderer/src/components/sidebar/worktree-list-scroll-adjustment.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-scroll-adjustment.test.ts @@ -1,11 +1,13 @@ import { describe, expect, it, vi } from 'vitest' import { countRecordKeysByReference, - getScrollTopToRevealBounds, resolvePendingSidebarReveal, - WORKTREE_SIDEBAR_REVEAL_TOP_INSET, shouldAdjustWorktreeSidebarMeasuredRowScroll -} from './WorktreeList' +} from './worktree-list/worktree-sidebar-scroll-tuning' +import { + getScrollTopToRevealBounds, + WORKTREE_SIDEBAR_REVEAL_TOP_INSET +} from './worktree-sidebar-reveal' import { extractWorktreeVirtualRowIndexes, estimateRenderRowSize, diff --git a/src/renderer/src/components/sidebar/worktree-list-visible-refresh.test.ts b/src/renderer/src/components/sidebar/worktree-list-visible-refresh.test.ts index 1e519ac6d36..b99c0957d52 100644 --- a/src/renderer/src/components/sidebar/worktree-list-visible-refresh.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-visible-refresh.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { installWorktreeVisibleRefreshVisibilityListener } from './WorktreeList' +import { installWorktreeVisibleRefreshVisibilityListener } from './worktree-list/worktree-visible-refresh-listener' describe('installWorktreeVisibleRefreshVisibilityListener', () => { beforeEach(() => { diff --git a/src/renderer/src/components/sidebar/worktree-list/FolderPathStatusIndicator.tsx b/src/renderer/src/components/sidebar/worktree-list/FolderPathStatusIndicator.tsx new file mode 100644 index 00000000000..9b699c1086c --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/FolderPathStatusIndicator.tsx @@ -0,0 +1,47 @@ +import React from 'react' +import { FolderX } from 'lucide-react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { cn } from '@/lib/utils' +import { + getFolderWorkspacePathStatusDescription, + getFolderWorkspacePathStatusTitle +} from '@/lib/folder-workspace-path-status' +import { + isConfirmedStaleFolderPathStatus, + type FolderWorkspacePathStatus +} from '../../../../../shared/folder-workspace-path-status' + +export function FolderPathStatusIndicator({ + status +}: { + status: FolderWorkspacePathStatus | null | undefined +}): React.JSX.Element | null { + const title = getFolderWorkspacePathStatusTitle(status) + if (!status || status.exists || !title) { + return null + } + const destructive = isConfirmedStaleFolderPathStatus(status) + return ( + + + + + + + +
+
{title}
+
+ {getFolderWorkspacePathStatusDescription(status)} +
+
+
+
+ ) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/HostSectionHeader.tsx b/src/renderer/src/components/sidebar/worktree-list/HostSectionHeader.tsx new file mode 100644 index 00000000000..898719956f2 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/HostSectionHeader.tsx @@ -0,0 +1,161 @@ +import React from 'react' +import { AlertTriangle, ChevronDown, Loader2, Server, ServerOff } from 'lucide-react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { cn } from '@/lib/utils' +import { translate } from '@/i18n/i18n' +import type { HostHeaderRow } from '../host-section-rows' +import { HostSectionHeaderMenu } from '../HostSectionHeaderMenu' + +function formatSectionActivityLabel(count: number, label: string): string { + return `${count} ${label}${count === 1 ? '' : 's'}` +} + +function SectionMetricsBadge({ count }: { count: number }): React.JSX.Element { + const totalLabel = formatSectionActivityLabel(count, 'workspace') + + return ( + + + + + {count} + + + + {totalLabel} + + + + ) +} + +function HostHeaderHealthIcon({ + health +}: { + health: HostHeaderRow['health'] +}): React.JSX.Element | null { + // Why: only surface states needing attention; healthy is the silent default. + if (health === 'connecting') { + return + } + if (health === 'blocked' || health === 'error') { + return + } + return null +} + +function getHostHeaderDetail(row: HostHeaderRow): { text: string; isWarning: boolean } | null { + // Why: a blocked compatibility verdict earns a compact warning so the host stands out. + if (row.health === 'blocked') { + return { + text: translate('auto.components.sidebar.WorktreeList.7a8b9c0d1e', 'Update required'), + isWarning: true + } + } + // Why: auth-failed needs a worded status; the health icon alone doesn't tell the user to re-auth. + if (row.connectionStatus === 'auth-failed') { + return { + text: translate( + 'auto.components.sidebar.WorktreeList.hostAuthNeeded', + 'Authentication needed' + ), + isWarning: true + } + } + if (row.health === 'disconnected') { + return { + text: translate('auto.components.sidebar.WorktreeList.hostDisconnected', 'Disconnected'), + isWarning: false + } + } + // Why: show the transport detail only for remote hosts; it's noise under the local label. + if (row.kind !== 'local') { + return { text: row.detail, isWarning: false } + } + return null +} + +export function HostSectionHeader({ + row, + onToggle, + onDragPointerDown, + dragging +}: { + row: HostHeaderRow + onToggle: () => void + onDragPointerDown?: (event: React.PointerEvent) => void + dragging?: boolean +}): React.JSX.Element { + const isBlocked = row.health === 'blocked' + const isDisconnected = row.health === 'disconnected' + const detail = getHostHeaderDetail(row) + return ( +
+ {/* Why: outlined card + server glyph marks hosts as machines, not mere groups. */} +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onToggle() + } + }} + > + {isDisconnected ? ( + + ) : ( + + )} + + {/* Why: badge hugs the label (like repo headers) instead of floating by the hover controls. */} +
+ + {row.label} + + {detail ? ( + + {detail.text} + + ) : null} + +
+
+ +
+ + + +
+
+ ) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/SidebarWorktreeListDialogs.tsx b/src/renderer/src/components/sidebar/worktree-list/SidebarWorktreeListDialogs.tsx new file mode 100644 index 00000000000..cd46a80c3c7 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/SidebarWorktreeListDialogs.tsx @@ -0,0 +1,118 @@ +import React from 'react' +import { translate } from '@/i18n/i18n' +import { findRepoForHost } from '@/store/slices/repo-host-identity' +import type { AppState } from '@/store/types' +import type { Repo } from '../../../../../shared/repo-types' +import { ProjectGroupNameDialog } from '../ProjectGroupNameDialog' +import { ProjectGroupDeleteDialog } from '../ProjectGroupDeleteDialog' +import SuppressExternalWorktreeInboxDialog from '../SuppressExternalWorktreeInboxDialog' +import type { NewExternalWorktreesInboxActionState } from '../new-external-worktrees-inbox-actions' +import type { ProjectGroupDialogs } from './use-project-group-dialogs' + +export function SidebarWorktreeListDialogs({ + dialogs, + repos, + settings, + suppressExternalWorktreeInboxRepoId, + setSuppressExternalWorktreeInboxRepoId, + newExternalWorktreeInboxActionState, + onConfirmSuppressExternalWorktreeInbox, + onOpenWorktreeVisibility +}: { + dialogs: ProjectGroupDialogs + repos: readonly Repo[] + settings: AppState['settings'] + suppressExternalWorktreeInboxRepoId: string | null + setSuppressExternalWorktreeInboxRepoId: (repoId: string | null) => void + newExternalWorktreeInboxActionState: ReadonlyMap + onConfirmSuppressExternalWorktreeInbox: () => void + onOpenWorktreeVisibility: (repo: Repo) => void +}): React.JSX.Element { + const { nameDialog, setNameDialog, deleteDialog, setDeleteDialog } = dialogs + return ( + <> + { + if (!open) { + setNameDialog(null) + } + }} + onSubmit={dialogs.handleSubmitProjectGroupName} + /> + repo.id === suppressExternalWorktreeInboxRepoId)?.displayName ?? + '') + : '' + } + pending={ + suppressExternalWorktreeInboxRepoId + ? (newExternalWorktreeInboxActionState.get(suppressExternalWorktreeInboxRepoId) + ?.pending ?? false) + : false + } + onOpenChange={(open) => { + if (!open) { + setSuppressExternalWorktreeInboxRepoId(null) + } + }} + onConfirm={onConfirmSuppressExternalWorktreeInbox} + onOpenRecovery={() => { + if (!suppressExternalWorktreeInboxRepoId) { + return + } + const repo = findRepoForHost(repos, suppressExternalWorktreeInboxRepoId, { settings }) + setSuppressExternalWorktreeInboxRepoId(null) + if (repo) { + onOpenWorktreeVisibility(repo) + } + }} + /> + { + setDeleteDialog((current) => + current ? { ...current, removeContainedProjects } : current + ) + }} + onOpenChange={(open) => { + if (!open) { + setDeleteDialog(null) + } + }} + onConfirm={dialogs.handleConfirmDeleteProjectGroup} + /> + + ) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/SidebarWorktreeListEmptyState.tsx b/src/renderer/src/components/sidebar/worktree-list/SidebarWorktreeListEmptyState.tsx new file mode 100644 index 00000000000..05f98037c60 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/SidebarWorktreeListEmptyState.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { CircleX } from 'lucide-react' +import { translate } from '@/i18n/i18n' + +export function SidebarWorktreeListEmptyState({ + hasFilters, + onClearFilters +}: { + hasFilters: boolean + onClearFilters: () => void +}): React.JSX.Element { + return ( +
+
+
+ + {translate('auto.components.sidebar.WorktreeList.b7acbf038b', 'No workspaces found')} + + {hasFilters && ( + + )} +
+
+
+ ) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/VirtualizedWorktreeViewport.tsx b/src/renderer/src/components/sidebar/worktree-list/VirtualizedWorktreeViewport.tsx new file mode 100644 index 00000000000..94455e16037 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/VirtualizedWorktreeViewport.tsx @@ -0,0 +1,411 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react' +import { useAppStore } from '@/store' +import { translate } from '@/i18n/i18n' +import { WorktreeSidebarDropIndicator } from '../WorktreeSidebarDropIndicator' +import { WorktreeListScrollToTopButton } from '../WorktreeListScrollToTopButton' +import { useWorktreeListScrollToTop } from '../use-worktree-list-scroll-to-top' +import { getActiveDescendantOptionId } from './active-descendant-option' +import { buildRenderableRows } from './renderable-rows' +import { useFolderWorkspacePathStatusRows } from './use-folder-workspace-path-status-rows' +import { useGroupToggleWithScrollAnchor } from './use-group-toggle-with-scroll-anchor' +import { usePendingSidebarReveal } from './use-pending-sidebar-reveal' +import { usePrimaryActiveWorktreeRow } from './use-primary-active-worktree-row' +import { useSidebarRevealHighlight } from './use-sidebar-reveal-highlight' +import { useVirtualRowMeasurementSync } from './use-virtual-row-measurement-sync' +import { useVisiblePrRefreshReporting } from './use-visible-pr-refresh-reporting' +import { useWorkspaceStatusRowDrag } from './use-workspace-status-row-drag' +import { useWorktreeDragRuntime } from './use-worktree-drag-runtime' +import { useWorktreeDragSession } from './use-worktree-drag-session' +import { useWorktreeDocumentDrop } from './use-worktree-document-drop' +import { useWorktreeLineageDropCommit } from './use-worktree-lineage-drop-commit' +import { useWorktreeListKeyboardNavigation } from './use-worktree-list-keyboard-navigation' +import { useWorktreeListVirtualizer } from './use-worktree-list-virtualizer' +import { useWorktreeNativeDrag } from './use-worktree-native-drag' +import { useWorktreePointerDrag } from './use-worktree-pointer-drag' +import { useWorktreeSidebarHeaderDrag } from './use-worktree-sidebar-header-drag' +import { useWorktreeSidebarScrollSuppression } from './use-worktree-sidebar-scroll-suppression' +import { + EMPTY_PROJECT_GROUPS, + type VirtualizedWorktreeViewportProps +} from './virtualized-worktree-viewport-props' +import { WORKTREE_SIDEBAR_SCROLL_STYLE } from './worktree-sidebar-scroll-tuning' +import type { WorktreeDropCommitContext } from './worktree-drop-commit-context' +import { buildWorktreeVirtualRowContext } from './worktree-virtual-row-context' +import { renderWorktreeVirtualRow } from './worktree-virtual-row-dispatch' + +export const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewport( + props: VirtualizedWorktreeViewportProps +) { + const { + rows, + groupBy, + pinnedDisplayPolicy, + activeWorktreeId, + collapsedGroups, + toggleGroup, + repoMap, + worktreeMap, + worktreeLineageById, + workspaceStatuses, + projectGroups = EMPTY_PROJECT_GROUPS, + scrollOffsetRef, + scrollAnchorRef + } = props + const scrollRef = useRef(null) + // Why: callback-ref only mutates scrollRef; state re-runs the scroll-to-top listener attach. + const [scrollElement, setScrollElement] = useState(null) + const settings = useAppStore((s) => s.settings) + const sshConnectionStates = useAppStore((s) => s.sshConnectionStates) + const newCardStyle = settings?.experimentalNewWorktreeCardStyle === true + + const reveal = useSidebarRevealHighlight() + const scrollSuppression = useWorktreeSidebarScrollSuppression(scrollRef) + const { markDirectScrollInput, markScrollMovement } = scrollSuppression + + const renderRows = useMemo(() => buildRenderableRows(rows), [rows]) + const firstHeaderIndex = useMemo( + () => renderRows.findIndex((row) => row.type === 'header' || row.type === 'host-header'), + [renderRows] + ) + const folderBackedProjectGroupIds = useMemo( + () => + new Set( + projectGroups + .filter((group) => group.createdFrom === 'folder-scan') + .map((group) => group.id) + ), + [projectGroups] + ) + + const headerDrag = useWorktreeSidebarHeaderDrag({ + rows, + renderRows, + firstHeaderIndex, + allRepoIds: props.allRepoIds, + repoMap, + projectGroups, + groupBy, + projectOrderBy: props.projectOrderBy, + scrollRef, + onReorderHostSections: props.onReorderHostSections, + onHostDragActiveChange: props.onHostDragActiveChange, + suppressMeasurementAdjustmentUntilRef: scrollSuppression.suppressMeasurementAdjustmentUntilRef, + directScrollInputUntilRef: scrollSuppression.directScrollInputUntilRef + }) + + const session = useWorktreeDragSession({ rows, scrollRef }) + const lineageDrop = useWorktreeLineageDropCommit({ + repoMap, + worktreeMap, + worktreeLineageById, + worktreeDragGroups: session.worktreeDragGroups + }) + const runtime = useWorktreeDragRuntime({ + worktreeDragSessionRef: session.worktreeDragSessionRef, + statusDropAnchorsRef: session.statusDropAnchorsRef, + onWorkspaceBoardDragPreviewCancel: props.onWorkspaceBoardDragPreviewCancel + }) + + const primaryActive = usePrimaryActiveWorktreeRow({ + rows, + activeWorktreeId, + pinnedDisplayPolicy, + onImmediateWorktreeActivate: props.onImmediateWorktreeActivate + }) + + const getCachedFolderWorkspacePathStatus = useFolderWorkspacePathStatusRows({ + allRepoIds: props.allRepoIds, + repoMap, + projectGroups, + folderWorkspaces: props.folderWorkspaces, + sshConnectionStates + }) + + const { showScrollToTop, scrollToTop } = useWorktreeListScrollToTop({ + scrollElement, + onUserScrollIntent: markDirectScrollInput + }) + + const virtualization = useWorktreeListVirtualizer({ + renderRows, + firstHeaderIndex, + scrollRef, + scrollOffsetRef, + suppressMeasurementAdjustmentUntilRef: scrollSuppression.suppressMeasurementAdjustmentUntilRef + }) + + usePendingSidebarReveal({ + pendingRevealWorktree: props.pendingRevealWorktree, + pendingRevealSidebarRow: props.pendingRevealSidebarRow, + clearPendingRevealWorktreeId: props.clearPendingRevealWorktreeId, + clearPendingRevealSidebarRow: props.clearPendingRevealSidebarRow, + agentSendTargetWorktreeId: props.agentSendTargetWorktreeId, + renderRows, + virtualizer: virtualization.virtualizer, + scrollRef, + worktrees: props.worktrees, + folderWorkspaces: props.folderWorkspaces, + repoMap, + worktreeMap, + worktreeLineageById, + collapsedGroups, + toggleGroup, + groupBy, + pinnedDisplayPolicy, + defaultHostId: props.defaultHostId, + prCache: props.prCache, + workspaceStatuses, + settings, + projectGroups, + projectGrouping: props.projectGrouping, + flashRevealedRow: reveal.flashRevealedRow, + markRevealScroll: scrollSuppression.markRevealScroll, + schedulePendingRevealFrame: reveal.schedulePendingRevealFrame, + cancelPendingRevealFrames: reveal.cancelPendingRevealFrames + }) + + const { virtualItems, measureVirtualRowElement } = useVirtualRowMeasurementSync({ + renderRows, + virtualization, + scrollRef, + scrollOffsetRef, + scrollAnchorRef, + hasDirectScrollInput: scrollSuppression.hasDirectScrollInput, + shouldSkipScrollAnchorRestore: scrollSuppression.shouldSkipScrollAnchorRestore + }) + + const { toggleGroupWithScrollAnchor, getLineageToggleHandler } = useGroupToggleWithScrollAnchor({ + scrollRef, + toggleGroup + }) + + const { handleContainerKeyDown } = useWorktreeListKeyboardNavigation({ + rows, + renderRows, + activeWorktreeId, + pinnedDisplayPolicy, + virtualizer: virtualization.virtualizer, + scrollRef, + activeModal: props.activeModal, + markDirectScrollInput + }) + + const dropCtx = useMemo( + () => ({ + scrollRef, + workspaceStatuses, + worktreeDragGroups: session.worktreeDragGroups, + worktreeDragUnitGroups: session.worktreeDragUnitGroups, + computeWorktreeDrop: session.computeWorktreeDrop, + computeWorktreeStatusDrop: session.computeWorktreeStatusDrop, + refreshWorktreeDragSession: session.refreshWorktreeDragSession, + getEligibleLineageDropTarget: lineageDrop.getEligibleLineageDropTarget, + commitWorktreeLineageParentDrop: lineageDrop.commitWorktreeLineageParentDrop, + clearReorderedWorktreeParents: lineageDrop.clearReorderedWorktreeParents, + clearWorktreeDrag: runtime.clearWorktreeDrag, + onMoveWorktreesToStatus: props.onMoveWorktreesToStatus, + onMoveWorktreesToStatusAtIndex: props.onMoveWorktreesToStatusAtIndex, + onReorderWorktrees: props.onReorderWorktrees, + onPinWorktrees: props.onPinWorktrees + }), + [ + lineageDrop.clearReorderedWorktreeParents, + lineageDrop.commitWorktreeLineageParentDrop, + lineageDrop.getEligibleLineageDropTarget, + props.onMoveWorktreesToStatus, + props.onMoveWorktreesToStatusAtIndex, + props.onPinWorktrees, + props.onReorderWorktrees, + runtime.clearWorktreeDrag, + session.computeWorktreeDrop, + session.computeWorktreeStatusDrop, + session.refreshWorktreeDragSession, + session.worktreeDragGroups, + session.worktreeDragUnitGroups, + workspaceStatuses + ] + ) + + const { handleWorktreeRowPointerDown, handleWorktreeRowClickCapture } = useWorktreePointerDrag({ + ctx: dropCtx, + session, + runtime, + scrollRef, + markScrollMovement, + selectedWorktreeIds: props.selectedWorktreeIds, + selectedWorktrees: props.selectedWorktrees, + workspaceBoardOpen: props.workspaceBoardOpen, + onWorkspaceBoardDragPreviewStart: props.onWorkspaceBoardDragPreviewStart, + onWorkspaceBoardDragPreviewCommit: props.onWorkspaceBoardDragPreviewCommit, + onDropWorktreesOnWorkspaceBoard: props.onDropWorktreesOnWorkspaceBoard, + shouldShowWorkspaceBoardDropIndicator: props.shouldShowWorkspaceBoardDropIndicator + }) + const nativeDrag = useWorktreeNativeDrag({ + ctx: dropCtx, + session, + runtime, + scrollRef, + markScrollMovement + }) + useWorktreeDocumentDrop({ + ctx: dropCtx, + worktreeDragSessionRef: session.worktreeDragSessionRef + }) + const statusDrag = useWorkspaceStatusRowDrag({ + ctx: dropCtx, + session, + runtime, + scrollRef, + rows, + groupBy, + onMoveWorktreeToStatus: props.onMoveWorktreeToStatus, + onPinWorktree: props.onPinWorktree + }) + + useVisiblePrRefreshReporting({ + currentWorktreeId: props.currentWorktreeId, + worktreeMap, + groupBy, + newCardStyle, + renderRows, + virtualItems, + scrollRef + }) + + // Why: a callback ref that changes identity is re-invoked with null on every render, which + // would run the teardown below mid-drag; depend only on the stable teardown callbacks. + const { cancelPendingRevealFrames, clearRevealHighlightFrame, clearRevealHighlightTimeout } = + reveal + const { clearWorktreeDrag } = runtime + const setScrollRootRef = useCallback( + (node: HTMLDivElement | null) => { + if (node === null && scrollRef.current !== null) { + // Why: drag previews, autoscroll frames, and reveal snapshots are tied to the scroll root; clear them before it unmounts. + cancelPendingRevealFrames() + clearRevealHighlightFrame() + clearRevealHighlightTimeout() + clearWorktreeDrag() + } + scrollRef.current = node + setScrollElement(node) + }, + [ + cancelPendingRevealFrames, + clearRevealHighlightFrame, + clearRevealHighlightTimeout, + clearWorktreeDrag + ] + ) + const handleScrollPointerDown = useCallback( + (event: React.PointerEvent) => { + const scrollbarWidth = event.currentTarget.offsetWidth - event.currentTarget.clientWidth + if (scrollbarWidth <= 0) { + return + } + const rect = event.currentTarget.getBoundingClientRect() + if (event.clientX >= rect.right - scrollbarWidth) { + markDirectScrollInput() + } + }, + [markDirectScrollInput] + ) + const handleScroll = useCallback(() => { + markScrollMovement() + }, [markScrollMovement]) + + const rowContext = buildWorktreeVirtualRowContext({ + props, + renderRows, + firstHeaderIndex, + virtualization, + measureVirtualRowElement, + settings, + sshConnectionStates, + newCardStyle, + folderBackedProjectGroupIds, + projectGroups, + session, + runtime, + primaryActive, + reveal, + statusDrag, + nativeDrag, + headerDrag, + getCachedFolderWorkspacePathStatus, + getLineageToggleHandler, + toggleGroupWithScrollAnchor, + onRowClickCapture: handleWorktreeRowClickCapture, + onRowPointerDown: handleWorktreeRowPointerDown + }) + + return ( +
+
+
+ {headerDrag.canReorderRepoHeaders && + headerDrag.repoDrag.state.draggingRepoId !== null && + headerDrag.repoDrag.state.dropIndicatorY !== null ? ( + + ) : null} + {headerDrag.canReorderProjectGroupHeaders && + headerDrag.projectGroupDrag.state.draggingGroupId !== null && + headerDrag.projectGroupDrag.state.dropIndicatorY !== null ? ( + + ) : null} + {headerDrag.hostDrag.state.draggingHostId !== null && + headerDrag.hostDrag.state.dropIndicatorY !== null ? ( + + ) : null} + {runtime.worktreeDragState.draggingWorktreeId !== null && + runtime.worktreeDragState.dropIndicatorY !== null ? ( + + ) : null} + {virtualItems.map((vItem) => { + const row = renderRows[vItem.index] + return row ? renderWorktreeVirtualRow(rowContext, row, vItem) : null + })} +
+
+ {showScrollToTop ? : null} +
+ ) +}) diff --git a/src/renderer/src/components/sidebar/worktree-list/active-descendant-option.ts b/src/renderer/src/components/sidebar/worktree-list/active-descendant-option.ts new file mode 100644 index 00000000000..b52614cfe4c --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/active-descendant-option.ts @@ -0,0 +1,67 @@ +import { folderWorkspaceKey } from '../../../../../shared/workspace-scope' +import type { RenderRow } from '../worktree-list-virtual-rows' +import type { PinnedWorktreeDisplayPolicy } from '../worktree-list-groups' +import { isPinnedWorktreeRow } from './render-row-item-rows' +import { getRenderRowWorktreeItem, renderRowContainsWorktree } from './render-row-worktree-lookup' +import { getWorktreeOptionId } from './worktree-option-dom' + +export function getRenderRowOptionId( + row: RenderRow | undefined, + worktreeId?: string | null +): string | undefined { + if (!row) { + return undefined + } + if (row.type === 'lineage-group') { + const targetRow = worktreeId ? row.rows.find((item) => item.worktree.id === worktreeId) : null + return getWorktreeOptionId((targetRow ?? row.rows[0])?.rowKey ?? row.key) + } + if (row.type === 'item') { + return getWorktreeOptionId(row.rowKey) + } + if (row.type === 'folder-workspace') { + return getWorktreeOptionId(folderWorkspaceKey(row.folderWorkspace.id)) + } + return undefined +} + +export function getActiveDescendantOptionId(args: { + activeWorktreeId: string | null + primaryActiveRowKey?: string + pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy + renderRows: readonly RenderRow[] + virtualItems: readonly { index: number }[] +}): string | undefined { + if (args.activeWorktreeId === null) { + return undefined + } + if (args.primaryActiveRowKey) { + const primaryOptionId = getWorktreeOptionId(args.primaryActiveRowKey) + for (const item of args.virtualItems) { + const row = args.renderRows[item.index] + if (row && getRenderRowOptionId(row, args.activeWorktreeId) === primaryOptionId) { + return primaryOptionId + } + } + } + let fallbackOptionId: string | undefined + for (const item of args.virtualItems) { + const row = args.renderRows[item.index] + if (row && renderRowContainsWorktree(row, args.activeWorktreeId)) { + const optionId = getRenderRowOptionId(row, args.activeWorktreeId) + if (!optionId) { + continue + } + const itemRow = getRenderRowWorktreeItem(row, args.activeWorktreeId) + if ( + args.pinnedDisplayPolicy === 'duplicate-in-groups' && + itemRow && + !isPinnedWorktreeRow(itemRow) + ) { + return optionId + } + fallbackOptionId ??= optionId + } + } + return fallbackOptionId +} diff --git a/src/renderer/src/components/sidebar/worktree-list/folder-workspace-virtual-row.tsx b/src/renderer/src/components/sidebar/worktree-list/folder-workspace-virtual-row.tsx new file mode 100644 index 00000000000..eb3dc2f3556 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/folder-workspace-virtual-row.tsx @@ -0,0 +1,129 @@ +import React from 'react' +import type { VirtualItem } from '@tanstack/react-virtual' +import type { AppState } from '@/store/types' +import type { Repo } from '../../../../../shared/repo-types' +import type { + WorkspaceLineage, + WorktreeLineage +} from '../../../../../shared/worktree/lineage-types' +import type { Worktree } from '../../../../../shared/worktree/types' +import type { FolderWorkspacePathStatus } from '../../../../../shared/folder-workspace-path-status' +import { isConfirmedStaleFolderPathStatus } from '../../../../../shared/folder-workspace-path-status' +import { folderWorkspaceToWorktree } from '../../../../../shared/folder-workspace-worktree' +import WorktreeCard from '../WorktreeCard' +import type { WorktreeGroupBy } from '../worktree-list-groups' +import { getVirtualRowTransform } from '../worktree-list-virtual-rows' +import { getFolderWorkspaceRowGeometry } from '../worktree-list-indentation' +import { getFolderWorkspaceCardPrDisplay } from '../folder-workspace-card-pr-display' +import { FolderPathStatusIndicator } from './FolderPathStatusIndicator' +import type { FolderWorkspaceItemRow } from './render-row-item-rows' +import { getWorktreeOptionId } from './worktree-option-dom' + +export type FolderWorkspaceRowContext = { + groupBy: WorktreeGroupBy + newCardStyle: boolean + settings: AppState['settings'] + activeWorktreeId: string | null + currentWorktreeId: string | null + selectedWorktreeIds: ReadonlySet + repoMap: Map + worktreeMap: Map + worktreeLineageById: Record + workspaceLineageByChildKey: Record + prCache: AppState['prCache'] | null + hostedReviewCache: AppState['hostedReviewCache'] | null + getCachedFolderWorkspacePathStatus: (request: { + scope: 'folder-workspace' + folderWorkspaceId: string + }) => FolderWorkspacePathStatus | null + onSelectionGesture: (event: React.MouseEvent, worktreeId: string) => boolean + onContextMenuSelect: ( + event: React.MouseEvent, + worktree: Worktree + ) => readonly Worktree[] + onImmediateActivate: (worktreeId: string, rowKey: string | undefined) => void + onRowClickCapture: (event: React.MouseEvent) => void + onRowPointerDown: ( + event: React.PointerEvent, + worktreeId: string, + rowKey: string + ) => void +} + +export function renderFolderWorkspaceVirtualRow(args: { + ctx: FolderWorkspaceRowContext + row: FolderWorkspaceItemRow + vItem: VirtualItem + measureVirtualRowElement: (element: HTMLDivElement | null) => void +}): React.JSX.Element { + const { ctx, row, vItem } = args + const folderWorktree = folderWorkspaceToWorktree(row.folderWorkspace) + const pathStatus = ctx.getCachedFolderWorkspacePathStatus({ + scope: 'folder-workspace', + folderWorkspaceId: row.folderWorkspace.id + }) + const activationDisabled = + pathStatus?.exists === false && + (isConfirmedStaleFolderPathStatus(pathStatus) || pathStatus.reason === 'ambiguous-connection') + const folderPrDisplay = getFolderWorkspaceCardPrDisplay({ + folderWorkspaceId: row.folderWorkspace.id, + workspaceLineageByChildKey: ctx.workspaceLineageByChildKey, + worktreeLineageById: ctx.worktreeLineageById, + worktreeMap: ctx.worktreeMap, + repoMap: ctx.repoMap, + hostedReviewCache: ctx.hostedReviewCache, + prCache: ctx.prCache, + settings: ctx.settings + }) + const { surfaceInset, cardContentIndent } = getFolderWorkspaceRowGeometry({ + experimentalNewWorktreeCardStyle: ctx.newCardStyle, + isFolderBackedWorkspaceChild: + ctx.groupBy === 'repo' && row.projectGroup.createdFrom === 'folder-scan', + isGrouped: ctx.groupBy !== 'none', + groupDepth: row.groupDepth, + lineageDepth: row.depth + }) + return ( +
ctx.onRowPointerDown(event, folderWorktree.id, folderWorktree.id)} + > +
0 ? { paddingLeft: surfaceInset } : undefined} + > + +
+ +
+
+
+ ) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/imported-worktrees-card-visibility.ts b/src/renderer/src/components/sidebar/worktree-list/imported-worktrees-card-visibility.ts new file mode 100644 index 00000000000..bfb7b38b039 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/imported-worktrees-card-visibility.ts @@ -0,0 +1,9 @@ +import type { Row } from '../worktree-list-groups' +import type { ImportedWorktreeCardActionState } from '../imported-worktrees-card-actions' + +export function canKeepImportedWorktreesHidden( + row: Extract, + actionState: ImportedWorktreeCardActionState | undefined +): boolean { + return row.placement === 'repo-group' && actionState?.forceVisible !== true +} diff --git a/src/renderer/src/components/sidebar/worktree-list/mounted-row-reveal.ts b/src/renderer/src/components/sidebar/worktree-list/mounted-row-reveal.ts new file mode 100644 index 00000000000..b3b1436211d --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/mounted-row-reveal.ts @@ -0,0 +1,35 @@ +import { revealElementInScrollContainer } from '../worktree-sidebar-reveal' +import { getMountedWorktreeOptions, getWorktreeOptionId } from './worktree-option-dom' + +export function revealMountedWorktreeElement( + container: HTMLElement, + worktreeId: string, + behavior: ScrollBehavior, + optionId?: string, + onScrollIssued?: (targetTop: number) => void +): HTMLElement | null { + const element = optionId + ? document.getElementById(optionId) + : getMountedWorktreeOptions(worktreeId, container)[0] + if (!element || !container.contains(element)) { + return null + } + return revealElementInScrollContainer(container, element, behavior, onScrollIssued) + ? element + : null +} + +export function revealMountedSidebarRowElement( + container: HTMLElement, + rowKey: string, + behavior: ScrollBehavior, + onScrollIssued?: (targetTop: number) => void +): HTMLElement | null { + const element = document.getElementById(getWorktreeOptionId(rowKey)) + if (!element || !container.contains(element)) { + return null + } + return revealElementInScrollContainer(container, element, behavior, onScrollIssued) + ? element + : null +} diff --git a/src/renderer/src/components/sidebar/worktree-list/pending-sidebar-reveal-inputs.ts b/src/renderer/src/components/sidebar/worktree-list/pending-sidebar-reveal-inputs.ts new file mode 100644 index 00000000000..c29b08bd17f --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/pending-sidebar-reveal-inputs.ts @@ -0,0 +1,116 @@ +import type React from 'react' +import type { Virtualizer } from '@tanstack/react-virtual' +import type { AppState } from '@/store/types' +import type { PendingSidebarRowReveal, PendingSidebarWorktreeReveal } from '@/store/slices/ui' +import type { FolderWorkspace } from '../../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../../shared/project-group-types' +import type { Repo } from '../../../../../shared/repo-types' +import type { WorkspaceStatusDefinition, Worktree } from '../../../../../shared/worktree/types' +import type { WorktreeLineage } from '../../../../../shared/worktree/lineage-types' +import type { ExecutionHostId } from '../../../../../shared/execution-host' +import { getWorktreeExecutionHostId } from '../../../../../shared/execution-host' +import type { RenderRow } from '../worktree-list-virtual-rows' +import { + getGroupKeysForWorktree, + getLineageGroupKey, + type PinnedWorktreeDisplayPolicy, + type ProjectGroupingModel, + type WorktreeGroupBy +} from '../worktree-list-groups' +import { getWorktreeLineageAncestors } from '../worktree-lineage-projection' +import { getFolderWorkspaceRevealGroupKeys } from '../worktree-list-folder-reveal' +import { getPinnedWorktreeRevealCollapsedGroupKeys } from './sidebar-row-reveal-ancestors' + +export const MAX_REVEAL_RETRIES = 8 + +export type PendingSidebarRevealArgs = { + pendingRevealWorktree: PendingSidebarWorktreeReveal | null + pendingRevealSidebarRow: PendingSidebarRowReveal | null + clearPendingRevealWorktreeId: () => void + clearPendingRevealSidebarRow: () => void + agentSendTargetWorktreeId: string | null + renderRows: RenderRow[] + virtualizer: Virtualizer + scrollRef: React.RefObject + worktrees: Worktree[] + folderWorkspaces: readonly FolderWorkspace[] + repoMap: Map + worktreeMap: Map + worktreeLineageById: Record + collapsedGroups: Set + toggleGroup: (key: string) => void + groupBy: WorktreeGroupBy + pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy + defaultHostId: ExecutionHostId + prCache: AppState['prCache'] | null + workspaceStatuses: readonly WorkspaceStatusDefinition[] + settings: AppState['settings'] + projectGroups: readonly ProjectGroup[] + projectGrouping?: ProjectGroupingModel + flashRevealedRow: (rowKey: string) => void + markRevealScroll: (targetTop: number) => void + schedulePendingRevealFrame: (callback: FrameRequestCallback) => void + cancelPendingRevealFrames: () => void +} + +// Expand whatever collapsed section hides the reveal target, then scroll to it. +export function expandGroupsForWorktreeReveal( + args: PendingSidebarRevealArgs, + worktreeId: string +): void { + const folderGroupKeys = getFolderWorkspaceRevealGroupKeys( + worktreeId, + args.folderWorkspaces, + args.projectGroups + ) + if (folderGroupKeys.length > 0) { + for (const groupKey of folderGroupKeys) { + if (args.collapsedGroups.has(groupKey)) { + args.toggleGroup(groupKey) + } + } + return + } + const targetWorktree = args.worktrees.find((w) => w.id === worktreeId) + if (!targetWorktree) { + return + } + const targetRepo = args.repoMap.get(targetWorktree.repoId) + const hostGroupKey = `host:${getWorktreeExecutionHostId(targetWorktree, targetRepo, args.defaultHostId)}` + if (args.collapsedGroups.has(hostGroupKey)) { + args.toggleGroup(hostGroupKey) + } + + for (const parent of getWorktreeLineageAncestors( + targetWorktree, + args.worktreeLineageById, + args.worktreeMap + )) { + const lineageGroupKey = getLineageGroupKey(parent.id) + if (args.collapsedGroups.has(lineageGroupKey)) { + args.toggleGroup(lineageGroupKey) + } + } + + const groupKeys = + targetWorktree.isPinned && args.pinnedDisplayPolicy === 'single-location' + ? getPinnedWorktreeRevealCollapsedGroupKeys({ + worktree: targetWorktree, + collapsedGroups: args.collapsedGroups + }) + : getGroupKeysForWorktree( + args.groupBy, + targetWorktree, + args.repoMap, + args.prCache, + args.workspaceStatuses, + args.settings, + args.projectGroups, + args.projectGrouping + ) + for (const groupKey of groupKeys) { + if (args.collapsedGroups.has(groupKey)) { + args.toggleGroup(groupKey) + } + } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/pointer-drop-status-target.ts b/src/renderer/src/components/sidebar/worktree-list/pointer-drop-status-target.ts new file mode 100644 index 00000000000..46e20c56234 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/pointer-drop-status-target.ts @@ -0,0 +1,56 @@ +import type { + WorkspaceStatus, + WorkspaceStatusDefinition +} from '../../../../../shared/worktree/types' +import { getWorkspaceStatusFromGroupKey } from '../workspace-status' +import { getWorktreeLineageDropTargetId } from '../worktree-lineage-drag-drop' +import type { WorktreeSidebarStatusDropTarget } from '../worktree-sidebar-drop-preview' +import { + NO_WORKTREE_SIDEBAR_DROP_TARGET, + type WorktreeSidebarLineageDropTarget +} from './worktree-row-drag-state' + +export function getPointerDropStatusTarget(args: { + container: HTMLElement + x: number + y: number +}): WorktreeSidebarLineageDropTarget { + const target = document.elementFromPoint(args.x, args.y) + if (!(target instanceof Element) || !args.container.contains(target)) { + return NO_WORKTREE_SIDEBAR_DROP_TARGET + } + const pinTarget = target.closest('[data-workspace-pin-drop-target]') + if (pinTarget && args.container.contains(pinTarget)) { + return { status: null, isPinDrop: true, lineageParentId: null } + } + const lineageParentId = getWorktreeLineageDropTargetId({ + container: args.container, + target, + pointerY: args.y + }) + const statusTarget = target.closest('[data-workspace-status-drop-target]') + return { + status: + statusTarget && args.container.contains(statusTarget) + ? ((statusTarget.dataset.workspaceStatus as WorkspaceStatus | undefined) ?? null) + : null, + isPinDrop: false, + lineageParentId + } +} + +export function shouldPreferSidebarStatusDropTarget(args: { + sourceGroupKey: string + target: WorktreeSidebarStatusDropTarget + workspaceStatuses: readonly WorkspaceStatusDefinition[] +}): boolean { + if (args.target.isPinDrop) { + return true + } + if (!args.target.status) { + return false + } + const sourceStatus = getWorkspaceStatusFromGroupKey(args.sourceGroupKey, args.workspaceStatuses) + // Why: overlapping edge zones — the section under the pointer must win so guide and drop agree. + return sourceStatus !== null && args.target.status !== sourceStatus +} diff --git a/src/renderer/src/components/sidebar/worktree-list/project-group-header-actions.tsx b/src/renderer/src/components/sidebar/worktree-list/project-group-header-actions.tsx new file mode 100644 index 00000000000..fc9edf19114 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/project-group-header-actions.tsx @@ -0,0 +1,132 @@ +import React from 'react' +import { Ellipsis, Plus } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { cn } from '@/lib/utils' +import { translate } from '@/i18n/i18n' +import { getFolderWorkspacePathStatusDescription } from '@/lib/folder-workspace-path-status' +import type { ProjectGroup } from '../../../../../shared/project-group-types' +import type { FolderWorkspacePathStatus } from '../../../../../shared/folder-workspace-path-status' +import { REPO_HEADER_ACTION_BUTTON_CLASS } from '../repo-header-action-button-class' +import { + handleRepoHeaderActionPointerDown, + stopRepoHeaderKeyboardToggle, + stopRepoHeaderMenuEvent +} from './repo-header-event-guards' + +export function ProjectGroupHeaderMenu({ + groupId, + label, + onRename, + onDelete +}: { + groupId: string + label: string + onRename: (groupId: string, currentName: string) => void + onDelete: (groupId: string, groupName: string) => void +}): React.JSX.Element { + return ( + + + + + + onRename(groupId, label)}> + {translate('auto.components.sidebar.WorktreeList.4d7b73658c', 'Rename group')} + + onDelete(groupId, label)}> + {translate('auto.components.sidebar.WorktreeList.902115cdbe', 'Delete group')} + + + + ) +} + +export function ProjectGroupCreateWorkspaceButton({ + projectGroup, + label, + pathStatus, + disabled, + onCreate +}: { + projectGroup: ProjectGroup + label: string + pathStatus: FolderWorkspacePathStatus | null + disabled: boolean + onCreate: (projectGroup: ProjectGroup) => void +}): React.JSX.Element { + const createLabel = translate( + 'auto.components.sidebar.WorktreeList.bd37a57ac8', + 'Create workspace for {{value0}}', + { value0: label } + ) + return ( + + + + + + {pathStatus?.exists === false + ? getFolderWorkspacePathStatusDescription(pathStatus) + : createLabel} + + + ) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/render-row-item-rows.ts b/src/renderer/src/components/sidebar/worktree-list/render-row-item-rows.ts new file mode 100644 index 00000000000..b0895ad6cd4 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/render-row-item-rows.ts @@ -0,0 +1,13 @@ +import { PINNED_GROUP_KEY } from '../worktree-list-groups' +import type { HostSectionRow } from '../host-section-rows' + +export type WorktreeItemRow = Extract +export type FolderWorkspaceItemRow = Extract + +export function isWorktreeItemRow(row: HostSectionRow): row is WorktreeItemRow { + return row.type === 'item' +} + +export function isPinnedWorktreeRow(row: WorktreeItemRow): boolean { + return row.sectionKey === PINNED_GROUP_KEY +} diff --git a/src/renderer/src/components/sidebar/worktree-list/render-row-worktree-lookup.ts b/src/renderer/src/components/sidebar/worktree-list/render-row-worktree-lookup.ts new file mode 100644 index 00000000000..4605058d231 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/render-row-worktree-lookup.ts @@ -0,0 +1,79 @@ +import { folderWorkspaceKey } from '../../../../../shared/workspace-scope' +import type { RenderRow } from '../worktree-list-virtual-rows' +import type { PinnedWorktreeDisplayPolicy } from '../worktree-list-groups' +import { isPinnedWorktreeRow, type WorktreeItemRow } from './render-row-item-rows' + +export function getRenderRowSidebarKey(row: RenderRow): string | null { + if (row.type === 'header') { + return row.key + } + if (row.type === 'item') { + return row.rowKey + } + if (row.type === 'folder-workspace') { + return folderWorkspaceKey(row.folderWorkspace.id) + } + if (row.type === 'pending-creation') { + return `pending:${row.creationId}` + } + if (row.type === 'imported-worktrees-card') { + return row.key + } + if (row.type === 'new-external-worktrees-inbox') { + return row.key + } + return null +} + +export function rowKeyMatchesRenderRow(row: RenderRow, rowKey: string): boolean { + if (row.type === 'lineage-group') { + return row.rows.some((item) => item.rowKey === rowKey) + } + return getRenderRowSidebarKey(row) === rowKey +} + +export function renderRowContainsWorktree(row: RenderRow, worktreeId: string | null): boolean { + if (worktreeId === null) { + return false + } + if (row.type === 'folder-workspace') { + return folderWorkspaceKey(row.folderWorkspace.id) === worktreeId + } + if (row.type === 'lineage-group') { + return row.rows.some((item) => item.worktree.id === worktreeId) + } + return row.type === 'item' && row.worktree.id === worktreeId +} + +export function getRenderRowWorktreeItem( + row: RenderRow, + worktreeId: string +): WorktreeItemRow | null { + if (row.type === 'lineage-group') { + return row.rows.find((item) => item.worktree.id === worktreeId) ?? null + } + return row.type === 'item' && row.worktree.id === worktreeId ? row : null +} + +// Prefer the worktree's natural group row over its pinned duplicate when both are rendered. +export function findPreferredRenderRowIndexForWorktree( + renderRows: readonly RenderRow[], + worktreeId: string, + pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy +): number { + let fallbackIndex = -1 + for (let index = 0; index < renderRows.length; index++) { + const row = renderRows[index] + if (!renderRowContainsWorktree(row, worktreeId)) { + continue + } + if (fallbackIndex === -1) { + fallbackIndex = index + } + const itemRow = getRenderRowWorktreeItem(row, worktreeId) + if (pinnedDisplayPolicy === 'duplicate-in-groups' && itemRow && !isPinnedWorktreeRow(itemRow)) { + return index + } + } + return fallbackIndex +} diff --git a/src/renderer/src/components/sidebar/worktree-list/renderable-rows.ts b/src/renderer/src/components/sidebar/worktree-list/renderable-rows.ts new file mode 100644 index 00000000000..cc7c5e7870b --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/renderable-rows.ts @@ -0,0 +1,40 @@ +import { getLineageGroupKey } from '../worktree-list-groups' +import type { HostSectionRow } from '../host-section-rows' +import type { RenderRow } from '../worktree-list-virtual-rows' +import { isWorktreeItemRow, type WorktreeItemRow } from './render-row-item-rows' + +// Collapse a parent and its visible lineage descendants into one virtual row so the card renders them inline. +export function buildRenderableRows(rows: HostSectionRow[]): RenderRow[] { + const renderRows: RenderRow[] = [] + for (let index = 0; index < rows.length; index++) { + const row = rows[index] + if ( + !isWorktreeItemRow(row) || + row.lineageChildCount === 0 || + row.lineageCollapsed || + rows[index + 1]?.type !== 'item' || + (rows[index + 1] as WorktreeItemRow).depth <= row.depth + ) { + renderRows.push(row) + continue + } + + const groupRows: WorktreeItemRow[] = [row] + let cursor = index + 1 + while (cursor < rows.length) { + const child = rows[cursor] + if (!isWorktreeItemRow(child) || child.depth <= row.depth) { + break + } + groupRows.push(child) + cursor++ + } + renderRows.push({ + type: 'lineage-group', + key: `${row.sectionKey}:${getLineageGroupKey(row.worktree.id)}`, + rows: groupRows + }) + index = cursor - 1 + } + return renderRows +} diff --git a/src/renderer/src/components/sidebar/worktree-list/repo-header-event-guards.ts b/src/renderer/src/components/sidebar/worktree-list/repo-header-event-guards.ts new file mode 100644 index 00000000000..c4ba87c3f50 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/repo-header-event-guards.ts @@ -0,0 +1,31 @@ +import type React from 'react' +import { isRepoHeaderActionTarget } from '../project-header-drag' + +export function stopRepoHeaderKeyboardToggle(event: React.KeyboardEvent): void { + if (event.key === 'Enter' || event.key === ' ') { + event.stopPropagation() + } +} + +export function stopNestedWorktreeCardBubble(event: React.SyntheticEvent): void { + event.stopPropagation() +} + +export function handleRepoHeaderActionPointerDown(event: React.PointerEvent): void { + event.stopPropagation() +} + +export function handleRepoHeaderCollapseAffordancePointerDown( + event: React.PointerEvent +): void { + // Why: keep collapse-chevron clicks from arming the repo-header row drag. + event.stopPropagation() +} + +export function stopRepoHeaderMenuEvent(event: React.SyntheticEvent): void { + event.stopPropagation() +} + +export function shouldIgnoreRepoHeaderToggle(event: React.SyntheticEvent): boolean { + return isRepoHeaderActionTarget(event.target, event.currentTarget) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/repo-header-project-actions.tsx b/src/renderer/src/components/sidebar/worktree-list/repo-header-project-actions.tsx new file mode 100644 index 00000000000..cd0d09c0733 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/repo-header-project-actions.tsx @@ -0,0 +1,227 @@ +import React from 'react' +import { + CircleX, + Ellipsis, + Eye, + FolderInput, + FolderPlus, + Plus, + Shapes, + SlidersHorizontal, + Trash2 +} from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { cn } from '@/lib/utils' +import { translate } from '@/i18n/i18n' +import { getRepositoryIconSectionId } from '@/components/settings/repository-settings-targets' +import type { ProjectGroup } from '../../../../../shared/project-group-types' +import type { Repo } from '../../../../../shared/repo-types' +import { isGitRepoKind } from '../../../../../shared/repo-kind' +import { + REPO_HEADER_ACTION_BUTTON_CLASS, + REPO_HEADER_ACTION_REVEAL_CLASS +} from '../repo-header-action-button-class' +import type { getRepoHeaderCreateState } from '../repo-header-create-state' +import { + handleRepoHeaderActionPointerDown, + stopRepoHeaderKeyboardToggle, + stopRepoHeaderMenuEvent +} from './repo-header-event-guards' +import { getWorktreeVisibilityMenuLabel } from './worktree-visibility-menu-label' + +export type RepoHeaderProjectActions = { + onOpenRepoSettings: (projectId: string, sectionId?: string) => void + onOpenWorktreeVisibility: (repo: Repo) => void + onCreateGroupFromRepo: (repo: Repo) => void + onMoveProjectToGroup: (repo: Repo, groupId: string) => void + onRemoveProjectFromGroup: (repo: Repo) => void + onRemoveProject: (repo: Repo) => void + onCreateForRepo: (projectId: string) => void +} + +export function RepoHeaderProjectActionsMenu({ + repo, + label, + projectGroups, + actions +}: { + repo: Repo + label: string + projectGroups: readonly ProjectGroup[] + actions: RepoHeaderProjectActions +}): React.JSX.Element { + return ( + + + + + + + + + {translate('auto.components.sidebar.WorktreeList.2ef41bf9a7', 'Project actions')} + + + + actions.onOpenRepoSettings(repo.id)}> + + {translate('auto.components.sidebar.WorktreeList.2cdffbc728', 'Project Settings')} + + actions.onOpenRepoSettings(repo.id, getRepositoryIconSectionId(repo.id))} + > + + {translate('auto.components.sidebar.WorktreeList.e82d3589a1', 'Change Project Icon')} + + {isGitRepoKind(repo) ? ( + actions.onOpenWorktreeVisibility(repo)}> + + {getWorktreeVisibilityMenuLabel(repo)} + + ) : null} + actions.onCreateGroupFromRepo(repo)}> + + {translate('auto.components.sidebar.WorktreeList.cbfd565f83', 'New group from project')} + + {projectGroups.length > 0 ? ( + + + + {translate('auto.components.sidebar.WorktreeList.4a08fb55f2', 'Move to group')} + + + {projectGroups.map((group) => ( + actions.onMoveProjectToGroup(repo, group.id)} + > + {group.name} + + ))} + + + ) : null} + {repo.projectGroupId ? ( + actions.onRemoveProjectFromGroup(repo)}> + + {translate('auto.components.sidebar.WorktreeList.64e55f7f01', 'Remove from group')} + + ) : null} + + actions.onRemoveProject(repo)}> + + {translate('auto.components.sidebar.WorktreeList.c83968f87f', 'Remove Project')} + + + + ) +} + +export function RepoHeaderCreateWorkspaceButton({ + repo, + label, + createState, + onCreateForRepo +}: { + repo: Repo + label: string + createState: ReturnType | null + onCreateForRepo: (projectId: string) => void +}): React.JSX.Element { + const fallbackLabel = translate( + 'auto.components.sidebar.WorktreeList.bb85cd86ba', + 'Create workspace for {{value0}}', + { value0: label } + ) + return ( + + + {createState?.disabled ? ( + event.stopPropagation()} + onPointerDown={handleRepoHeaderActionPointerDown} + > + + + ) : ( + + )} + + + {createState?.tooltip ?? fallbackLabel} + + + ) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/sidebar-editable-target.ts b/src/renderer/src/components/sidebar/worktree-list/sidebar-editable-target.ts new file mode 100644 index 00000000000..390df9dae2f --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/sidebar-editable-target.ts @@ -0,0 +1,19 @@ +export function isEditableTarget(target: EventTarget | null): boolean { + if (!(target instanceof HTMLElement)) { + return false + } + + // xterm's hidden input textarea isn't a real text field; treating it as one would block sidebar shortcuts. + if (target.classList.contains('xterm-helper-textarea')) { + return false + } + + if (target.isContentEditable) { + return true + } + + return ( + target.closest('input, textarea, select, [contenteditable=""], [contenteditable="true"]') !== + null + ) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/sidebar-row-reveal-ancestors.ts b/src/renderer/src/components/sidebar/worktree-list/sidebar-row-reveal-ancestors.ts new file mode 100644 index 00000000000..cc387daba32 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/sidebar-row-reveal-ancestors.ts @@ -0,0 +1,112 @@ +import type { ProjectGroup } from '../../../../../shared/project-group-types' +import type { Repo } from '../../../../../shared/repo-types' +import type { Worktree } from '../../../../../shared/worktree/types' +import { + getProjectGroupHeaderKey, + PINNED_GROUP_KEY, + type ProjectGroupingModel +} from '../worktree-list-groups' + +function getProjectIdFromHeaderRowKey(rowKey: string): string | null { + if (!rowKey.startsWith('project:')) { + return null + } + const withoutPrefix = rowKey.slice('project:'.length) + const setupSeparator = withoutPrefix.indexOf('::setup:') + return setupSeparator === -1 ? withoutPrefix : withoutPrefix.slice(0, setupSeparator) +} + +function getRepoIdsFromHeaderRowKey( + rowKey: string, + repoMap: Map, + projectGrouping?: ProjectGroupingModel +): string[] { + if (rowKey.startsWith('repo:')) { + return [rowKey.slice('repo:'.length)] + } + const setupMarker = '::setup:' + const setupIndex = rowKey.indexOf(setupMarker) + if (rowKey.startsWith('project:') && setupIndex !== -1) { + return [rowKey.slice(setupIndex + setupMarker.length)] + } + const projectId = getProjectIdFromHeaderRowKey(rowKey) + if (!projectId) { + return [] + } + const repoIds = new Set() + for (const setup of projectGrouping?.projectHostSetups ?? []) { + if (setup.projectId === projectId && repoMap.has(setup.repoId)) { + repoIds.add(setup.repoId) + } + } + const project = projectGrouping?.projects.find((candidate) => candidate.id === projectId) + for (const repoId of project?.sourceRepoIds ?? []) { + if (repoMap.has(repoId)) { + repoIds.add(repoId) + } + } + return [...repoIds] +} + +function getProjectGroupAncestorKeys( + projectGroupId: string | null | undefined, + projectGroups: readonly ProjectGroup[] +): string[] { + const groupsById = new Map(projectGroups.map((group) => [group.id, group])) + const keys: string[] = [] + const seen = new Set() + let currentGroupId = projectGroupId ?? null + while (currentGroupId && !seen.has(currentGroupId)) { + const group = groupsById.get(currentGroupId) + if (!group) { + break + } + seen.add(currentGroupId) + keys.unshift(getProjectGroupHeaderKey(group.id)) + currentGroupId = group.parentGroupId + } + return keys +} + +export function getSidebarRowRevealAncestorKeys(args: { + rowKey: string + repoMap: Map + projectGroups: readonly ProjectGroup[] + projectGrouping?: ProjectGroupingModel +}): string[] { + if (args.rowKey.startsWith('project-group:')) { + const groupId = args.rowKey.slice('project-group:'.length) + const group = args.projectGroups.find((candidate) => candidate.id === groupId) + return getProjectGroupAncestorKeys(group?.parentGroupId, args.projectGroups) + } + const keys = new Set() + for (const repoId of getRepoIdsFromHeaderRowKey( + args.rowKey, + args.repoMap, + args.projectGrouping + )) { + const repo = args.repoMap.get(repoId) + for (const key of getProjectGroupAncestorKeys(repo?.projectGroupId, args.projectGroups)) { + keys.add(key) + } + } + return [...keys] +} + +export function getPinnedWorktreeRevealCollapsedGroupKeys({ + worktree, + collapsedGroups +}: { + worktree: Worktree + collapsedGroups: ReadonlySet +}): string[] { + if (!worktree.isPinned) { + return [] + } + const keys: string[] = [] + // Why: the reveal effect already opens this host; re-returning it would toggle it back closed. + if (collapsedGroups.has(PINNED_GROUP_KEY)) { + keys.push(PINNED_GROUP_KEY) + } + return keys +} diff --git a/src/renderer/src/components/sidebar/worktree-list/stable-empty-store-slices.ts b/src/renderer/src/components/sidebar/worktree-list/stable-empty-store-slices.ts new file mode 100644 index 00000000000..49cae96a88b --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/stable-empty-store-slices.ts @@ -0,0 +1,9 @@ +import type { AppState } from '@/store/types' + +// Why: selectors that opt out of a slice must return one stable reference, or every store tick looks like a change. +export const EMPTY_AGENT_STATUS_BY_PANE_KEY: AppState['agentStatusByPaneKey'] = {} +export const EMPTY_WORKTREE_ID_SET: ReadonlySet = new Set() +export const EMPTY_TABS_BY_WORKTREE: AppState['tabsByWorktree'] = {} +export const EMPTY_TERMINAL_LAYOUTS_BY_TAB_ID: AppState['terminalLayoutsByTabId'] = {} +export const EMPTY_PTY_IDS_BY_TAB_ID: AppState['ptyIdsByTabId'] = {} +export const EMPTY_RUNTIME_PANE_TITLES_BY_TAB_ID: AppState['runtimePaneTitlesByTabId'] = {} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-agent-send-target-worktree.ts b/src/renderer/src/components/sidebar/worktree-list/use-agent-send-target-worktree.ts new file mode 100644 index 00000000000..61269d78225 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-agent-send-target-worktree.ts @@ -0,0 +1,63 @@ +import { useMemo } from 'react' +import { useAppStore } from '@/store' +import { deriveRunningAgentSendTargets } from '@/lib/running-agent-targets' +import { + EMPTY_AGENT_STATUS_BY_PANE_KEY, + EMPTY_PTY_IDS_BY_TAB_ID, + EMPTY_RUNTIME_PANE_TITLES_BY_TAB_ID, + EMPTY_TABS_BY_WORKTREE, + EMPTY_TERMINAL_LAYOUTS_BY_TAB_ID +} from './stable-empty-store-slices' + +// The agent send picker forces its target workspace visible, but only while a running +// agent there can actually receive the message. +export function useAgentSendTargetWorktreeId(): string | null { + const agentSendPopoverTargetMode = useAppStore((s) => s.agentSendPopoverTargetMode) + // Why: eligibility only matters while the picker is open; when closed, don't subscribe to wake-time layout churn. + const agentTargetStatusByPaneKey = useAppStore((s) => + agentSendPopoverTargetMode ? s.agentStatusByPaneKey : EMPTY_AGENT_STATUS_BY_PANE_KEY + ) + const agentTargetStatusEpoch = useAppStore((s) => + agentSendPopoverTargetMode ? s.agentStatusEpoch : 0 + ) + const agentTargetTabsByWorktree = useAppStore((s) => + agentSendPopoverTargetMode ? s.tabsByWorktree : EMPTY_TABS_BY_WORKTREE + ) + const agentTargetTerminalLayoutsByTabId = useAppStore((s) => + agentSendPopoverTargetMode ? s.terminalLayoutsByTabId : EMPTY_TERMINAL_LAYOUTS_BY_TAB_ID + ) + const agentTargetPtyIdsByTabId = useAppStore((s) => + agentSendPopoverTargetMode ? s.ptyIdsByTabId : EMPTY_PTY_IDS_BY_TAB_ID + ) + const agentTargetRuntimePaneTitlesByTabId = useAppStore((s) => + agentSendPopoverTargetMode ? s.runtimePaneTitlesByTabId : EMPTY_RUNTIME_PANE_TITLES_BY_TAB_ID + ) + return useMemo(() => { + void agentTargetStatusEpoch + if (!agentSendPopoverTargetMode) { + return null + } + const targets = deriveRunningAgentSendTargets( + { + agentStatusByPaneKey: agentTargetStatusByPaneKey, + tabsByWorktree: agentTargetTabsByWorktree, + terminalLayoutsByTabId: agentTargetTerminalLayoutsByTabId, + ptyIdsByTabId: agentTargetPtyIdsByTabId, + runtimePaneTitlesByTabId: agentTargetRuntimePaneTitlesByTabId + }, + agentSendPopoverTargetMode.worktreeId + ) + return targets.some((target) => target.status === 'eligible') + ? agentSendPopoverTargetMode.worktreeId + : null + }, [ + // Why: eligibility can flip when the stale-boundary scheduler bumps this epoch without replacing the status map. + agentTargetStatusEpoch, + agentSendPopoverTargetMode, + agentTargetStatusByPaneKey, + agentTargetTabsByWorktree, + agentTargetTerminalLayoutsByTabId, + agentTargetPtyIdsByTabId, + agentTargetRuntimePaneTitlesByTabId + ]) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-effective-collapsed-groups.ts b/src/renderer/src/components/sidebar/worktree-list/use-effective-collapsed-groups.ts new file mode 100644 index 00000000000..681056b5fe6 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-effective-collapsed-groups.ts @@ -0,0 +1,90 @@ +import { useMemo } from 'react' +import type { AppState } from '@/store/types' +import type { ProjectGroup } from '../../../../../shared/project-group-types' +import type { Repo } from '../../../../../shared/repo-types' +import type { WorkspaceStatusDefinition, Worktree } from '../../../../../shared/worktree/types' +import type { WorktreeLineage } from '../../../../../shared/worktree/lineage-types' +import { + getGroupKeysForWorktree, + getLineageGroupKey, + PINNED_GROUP_KEY, + type ProjectGroupingModel, + type WorktreeGroupBy +} from '../worktree-list-groups' +import { getWorktreeLineageAncestors } from '../worktree-lineage-projection' + +// While the agent send picker targets a workspace, force open every section that hides it. +export function useEffectiveCollapsedGroups(args: { + collapsedGroups: Set + agentSendTargetWorktreeId: string | null + groupBy: WorktreeGroupBy + repoMap: Map + worktreeMap: Map + worktreeLineageById: Record + prCache: AppState['prCache'] | null + workspaceStatuses: readonly WorkspaceStatusDefinition[] + settings: AppState['settings'] + projectGroups: readonly ProjectGroup[] + projectGrouping: ProjectGroupingModel +}): Set { + const { + collapsedGroups, + agentSendTargetWorktreeId, + groupBy, + repoMap, + worktreeMap, + worktreeLineageById, + prCache, + workspaceStatuses, + settings, + projectGroups, + projectGrouping + } = args + return useMemo(() => { + if (!agentSendTargetWorktreeId) { + return collapsedGroups + } + const targetWorktree = worktreeMap.get(agentSendTargetWorktreeId) + if (!targetWorktree) { + return collapsedGroups + } + const next = new Set(collapsedGroups) + if (targetWorktree.isPinned) { + next.delete(PINNED_GROUP_KEY) + } else { + for (const groupKey of getGroupKeysForWorktree( + groupBy, + targetWorktree, + repoMap, + prCache, + workspaceStatuses, + settings, + projectGroups, + projectGrouping + )) { + next.delete(groupKey) + } + } + + for (const parent of getWorktreeLineageAncestors( + targetWorktree, + worktreeLineageById, + worktreeMap + )) { + next.delete(getLineageGroupKey(parent.id)) + } + return next + }, [ + agentSendTargetWorktreeId, + collapsedGroups, + groupBy, + prCache, + projectGroups, + projectGrouping, + repoMap, + settings, + workspaceStatuses, + worktreeLineageById, + worktreeMap + ]) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-folder-workspace-path-status-rows.ts b/src/renderer/src/components/sidebar/worktree-list/use-folder-workspace-path-status-rows.ts new file mode 100644 index 00000000000..8e5bd17b81d --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-folder-workspace-path-status-rows.ts @@ -0,0 +1,128 @@ +import { useCallback, useEffect, useMemo } from 'react' +import { useShallow } from 'zustand/react/shallow' +import { useAppStore } from '@/store' +import { useFolderWorkspacePathStatusCacheExpiryTick } from '@/lib/folder-workspace-path-status-cache-expiry' +import type { FolderWorkspace } from '../../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../../shared/project-group-types' +import type { Repo } from '../../../../../shared/repo-types' +import type { AppState } from '@/store/types' +import { getFolderPathStatusRouteOptionsForRows } from '../worktree-list-host-filtering' + +type FolderPathStatusRequest = Parameters[0] + +// Keeps the sidebar's folder-path probes fresh for every project group and folder workspace +// it renders, and hands rows a cache reader that ignores expired negative results. +export function useFolderWorkspacePathStatusRows(args: { + allRepoIds: string[] + repoMap: Map + projectGroups: readonly ProjectGroup[] + folderWorkspaces: readonly FolderWorkspace[] + sshConnectionStates: AppState['sshConnectionStates'] +}) { + const { allRepoIds, repoMap, projectGroups, folderWorkspaces, sshConnectionStates } = args + const { + folderWorkspacePathStatuses, + fetchFolderWorkspacePathStatus, + getFolderWorkspacePathStatusCacheKey, + getFreshFolderWorkspacePathStatus, + activeRuntimeEnvironmentId + } = useAppStore( + useShallow((s) => ({ + folderWorkspacePathStatuses: s.folderWorkspacePathStatuses, + fetchFolderWorkspacePathStatus: s.fetchFolderWorkspacePathStatus, + getFolderWorkspacePathStatusCacheKey: s.getFolderWorkspacePathStatusCacheKey, + getFreshFolderWorkspacePathStatus: s.getFreshFolderWorkspacePathStatus, + activeRuntimeEnvironmentId: s.settings?.activeRuntimeEnvironmentId ?? null + })) + ) + const folderPathStatusRepoMembershipKey = useMemo( + () => + allRepoIds + .map((repoId) => { + const repo = repoMap.get(repoId) + return `${repoId}:${repo?.path ?? ''}:${repo?.projectGroupId ?? ''}:${repo?.connectionId ?? ''}` + }) + .join('\0'), + [allRepoIds, repoMap] + ) + const folderPathStatusSshConnectionKey = useMemo( + () => + [...sshConnectionStates.entries()] + .map(([connectionId, state]) => `${connectionId}:${state.status}`) + .sort() + .join('\0'), + [sshConnectionStates] + ) + const folderPathStatusCacheExpiryTick = useFolderWorkspacePathStatusCacheExpiryTick( + folderWorkspacePathStatuses + ) + const projectGroupByIdForFolderPathStatus = useMemo( + () => new Map(projectGroups.map((group) => [group.id, group])), + [projectGroups] + ) + const folderWorkspaceByIdForFolderPathStatus = useMemo( + () => new Map(folderWorkspaces.map((workspace) => [workspace.id, workspace])), + [folderWorkspaces] + ) + const getFolderPathStatusRouteOptions = useCallback( + (request: FolderPathStatusRequest) => + getFolderPathStatusRouteOptionsForRows({ + request, + projectGroupsById: projectGroupByIdForFolderPathStatus, + folderWorkspacesById: folderWorkspaceByIdForFolderPathStatus + }), + [folderWorkspaceByIdForFolderPathStatus, projectGroupByIdForFolderPathStatus] + ) + useEffect(() => { + const requests = new Map< + string, + { + request: FolderPathStatusRequest + options?: { runtimeEnvironmentId: string | null } + } + >() + for (const group of projectGroups) { + if (group.parentPath) { + const request = { scope: 'project-group' as const, projectGroupId: group.id } + const options = getFolderPathStatusRouteOptions(request) + requests.set(getFolderWorkspacePathStatusCacheKey(request, options), { request, options }) + } + } + for (const workspace of folderWorkspaces) { + const request = { scope: 'folder-workspace' as const, folderWorkspaceId: workspace.id } + const options = getFolderPathStatusRouteOptions(request) + requests.set(getFolderWorkspacePathStatusCacheKey(request, options), { request, options }) + } + for (const { request, options } of requests.values()) { + void fetchFolderWorkspacePathStatus(request, { force: true, ...options }) + } + }, [ + activeRuntimeEnvironmentId, + fetchFolderWorkspacePathStatus, + folderPathStatusRepoMembershipKey, + folderPathStatusSshConnectionKey, + folderWorkspaces, + getFolderPathStatusRouteOptions, + getFolderWorkspacePathStatusCacheKey, + projectGroups + ]) + const getCachedFolderWorkspacePathStatus = useCallback( + (request: FolderPathStatusRequest) => { + const options = getFolderPathStatusRouteOptions(request) + const cacheKey = getFolderWorkspacePathStatusCacheKey(request, options) + // Why: don't let an expired negative status keep folder workspaces disabled while a refresh is in flight. + void folderWorkspacePathStatuses[cacheKey] + void folderPathStatusCacheExpiryTick + return getFreshFolderWorkspacePathStatus(request, options) + }, + [ + folderWorkspacePathStatuses, + folderPathStatusCacheExpiryTick, + getFolderPathStatusRouteOptions, + getFolderWorkspacePathStatusCacheKey, + getFreshFolderWorkspacePathStatus + ] + ) + + return getCachedFolderWorkspacePathStatus +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-group-toggle-with-scroll-anchor.ts b/src/renderer/src/components/sidebar/worktree-list/use-group-toggle-with-scroll-anchor.ts new file mode 100644 index 00000000000..044c2a2651f --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-group-toggle-with-scroll-anchor.ts @@ -0,0 +1,30 @@ +import { useCallback, useMemo } from 'react' +import type React from 'react' +import { VIRTUALIZED_SCROLL_ANCHOR_RECORD_EVENT } from '@/hooks/useVirtualizedScrollAnchor' +import { createLineageToggleHandlerCache } from '../worktree-lineage-toggle-handler-cache' + +// Collapsing a section changes total height, so snapshot the anchor first or the viewport jumps. +export function useGroupToggleWithScrollAnchor(args: { + scrollRef: React.RefObject + toggleGroup: (key: string) => void +}) { + const { scrollRef, toggleGroup } = args + const recordCurrentScrollAnchor = useCallback(() => { + scrollRef.current?.dispatchEvent(new Event(VIRTUALIZED_SCROLL_ANCHOR_RECORD_EVENT)) + }, [scrollRef]) + const toggleGroupWithScrollAnchor = useCallback( + (groupKey: string) => { + recordCurrentScrollAnchor() + toggleGroup(groupKey) + }, + [recordCurrentScrollAnchor, toggleGroup] + ) + // Why: memo'd WorktreeCard needs a per-group-key stable onLineageToggle + // identity to bail out of re-renders; see worktree-lineage-toggle-handler-cache. + const getLineageToggleHandler = useMemo( + () => createLineageToggleHandlerCache(toggleGroupWithScrollAnchor), + [toggleGroupWithScrollAnchor] + ) + + return { toggleGroupWithScrollAnchor, getLineageToggleHandler } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-pending-sidebar-reveal.ts b/src/renderer/src/components/sidebar/worktree-list/use-pending-sidebar-reveal.ts new file mode 100644 index 00000000000..7ba04fb5149 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-pending-sidebar-reveal.ts @@ -0,0 +1,288 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { toast } from 'sonner' +import { useAppStore } from '@/store' +import { translate } from '@/i18n/i18n' +import { getRenderRowOptionId } from './active-descendant-option' +import { + findPreferredRenderRowIndexForWorktree, + getRenderRowSidebarKey, + rowKeyMatchesRenderRow +} from './render-row-worktree-lookup' +import { revealMountedSidebarRowElement, revealMountedWorktreeElement } from './mounted-row-reveal' +import { getSidebarRowRevealAncestorKeys } from './sidebar-row-reveal-ancestors' +import { sidebarWorkspaceStillExists } from '../worktree-list-folder-reveal' +import { resolvePendingSidebarReveal } from './worktree-sidebar-scroll-tuning' +import { + expandGroupsForWorktreeReveal, + MAX_REVEAL_RETRIES, + type PendingSidebarRevealArgs +} from './pending-sidebar-reveal-inputs' + +// Drives the store's two pending reveal requests (a worktree card, or any sidebar row) +// from "expand the ancestors" through "scroll the mounted element into view". +export function usePendingSidebarReveal(args: PendingSidebarRevealArgs): void { + const setRenamingWorktreeId = useAppStore((s) => s.setRenamingWorktreeId) + const [pendingRevealRetryTick, setPendingRevealRetryTick] = useState(0) + const pendingRevealRetryRef = useRef<{ worktreeId: string; count: number } | null>(null) + const pendingRowRevealRetryRef = useRef<{ rowKey: string; count: number } | null>(null) + const argsRef = useRef(args) + argsRef.current = args + + const { + pendingRevealWorktree, + pendingRevealSidebarRow, + clearPendingRevealWorktreeId, + clearPendingRevealSidebarRow, + renderRows, + virtualizer, + schedulePendingRevealFrame, + cancelPendingRevealFrames, + flashRevealedRow, + markRevealScroll, + pinnedDisplayPolicy + } = args + + const scheduleRetryTick = useCallback( + (cancelled: () => boolean) => { + schedulePendingRevealFrame(() => { + if (!cancelled()) { + setPendingRevealRetryTick((tick) => tick + 1) + } + }) + }, + [schedulePendingRevealFrame] + ) + + useEffect(() => { + if (!pendingRevealWorktree) { + return + } + const current = argsRef.current + if (current.agentSendTargetWorktreeId !== pendingRevealWorktree.worktreeId) { + expandGroupsForWorktreeReveal(current, pendingRevealWorktree.worktreeId) + } + + let cancelled = false + schedulePendingRevealFrame(() => { + if (cancelled) { + return + } + const targetWorktreeStillExists = sidebarWorkspaceStillExists( + pendingRevealWorktree.worktreeId, + argsRef.current.worktrees, + argsRef.current.folderWorkspaces + ) + const targetIndex = findPreferredRenderRowIndexForWorktree( + renderRows, + pendingRevealWorktree.worktreeId, + pinnedDisplayPolicy + ) + const outcome = resolvePendingSidebarReveal({ targetIndex, targetWorktreeStillExists }) + if (outcome === 'clear') { + pendingRevealRetryRef.current = null + clearPendingRevealWorktreeId() + return + } + if (outcome !== 'scroll-and-clear') { + return + } + const targetRow = renderRows[targetIndex] + const container = argsRef.current.scrollRef.current + const revealedOption = container + ? revealMountedWorktreeElement( + container, + pendingRevealWorktree.worktreeId, + pendingRevealWorktree.behavior, + getRenderRowOptionId(targetRow, pendingRevealWorktree.worktreeId), + markRevealScroll + ) + : null + if (revealedOption) { + if (pendingRevealWorktree.highlight) { + const revealedRowKey = + revealedOption.dataset.worktreeRowKey ?? getRenderRowSidebarKey(targetRow) + if (revealedRowKey) { + flashRevealedRow(revealedRowKey) + } + } + if (pendingRevealWorktree.beginRename) { + setRenamingWorktreeId({ + worktreeId: pendingRevealWorktree.worktreeId, + rowKey: revealedOption.dataset.worktreeRowKey + }) + } + pendingRevealRetryRef.current = null + clearPendingRevealWorktreeId() + return + } + + // Why: virtual indexing can leave the card edge clipped; stage it into the window, then retry the exact DOM reveal. + virtualizer.scrollToIndex(targetIndex, { align: 'auto', behavior: 'auto' }) + const previousRetry = pendingRevealRetryRef.current + const nextRetryCount = + previousRetry?.worktreeId === pendingRevealWorktree.worktreeId ? previousRetry.count + 1 : 1 + pendingRevealRetryRef.current = { + worktreeId: pendingRevealWorktree.worktreeId, + count: nextRetryCount + } + if (nextRetryCount <= MAX_REVEAL_RETRIES) { + scheduleRetryTick(() => cancelled) + return + } + pendingRevealRetryRef.current = null + clearPendingRevealWorktreeId() + }) + return () => { + cancelled = true + cancelPendingRevealFrames() + } + // Why: the effect re-reads live inputs through argsRef; only reveal-relevant identities should retrigger it. + // oxlint-disable-next-line react-hooks/exhaustive-deps + }, [ + pendingRevealWorktree, + args.agentSendTargetWorktreeId, + args.groupBy, + args.worktrees, + args.folderWorkspaces, + args.repoMap, + args.prCache, + args.worktreeLineageById, + args.worktreeMap, + renderRows, + virtualizer, + clearPendingRevealWorktreeId, + args.toggleGroup, + args.collapsedGroups, + args.defaultHostId, + args.workspaceStatuses, + args.settings, + pinnedDisplayPolicy, + args.projectGrouping, + args.projectGroups, + pendingRevealRetryTick, + flashRevealedRow, + markRevealScroll, + setRenamingWorktreeId, + schedulePendingRevealFrame, + cancelPendingRevealFrames, + scheduleRetryTick + ]) + + useEffect(() => { + if (!pendingRevealSidebarRow) { + return + } + const current = argsRef.current + + const isProjectHeaderTarget = + pendingRevealSidebarRow.rowKey.startsWith('project-group:') || + pendingRevealSidebarRow.rowKey.startsWith('project:') || + pendingRevealSidebarRow.rowKey.startsWith('repo:') + if (isProjectHeaderTarget && current.groupBy !== 'repo') { + return + } + + let toggledAncestor = false + for (const groupKey of getSidebarRowRevealAncestorKeys({ + rowKey: pendingRevealSidebarRow.rowKey, + repoMap: current.repoMap, + projectGroups: current.projectGroups, + projectGrouping: current.projectGrouping + })) { + if (current.collapsedGroups.has(groupKey)) { + current.toggleGroup(groupKey) + toggledAncestor = true + } + } + if (toggledAncestor) { + return + } + + let cancelled = false + const retryPendingReveal = (): boolean => { + const previousRetry = pendingRowRevealRetryRef.current + const nextRetryCount = + previousRetry?.rowKey === pendingRevealSidebarRow.rowKey ? previousRetry.count + 1 : 1 + pendingRowRevealRetryRef.current = { + rowKey: pendingRevealSidebarRow.rowKey, + count: nextRetryCount + } + if (nextRetryCount <= MAX_REVEAL_RETRIES) { + scheduleRetryTick(() => cancelled) + return true + } + return false + } + schedulePendingRevealFrame(() => { + if (cancelled) { + return + } + const targetIndex = renderRows.findIndex((row) => + rowKeyMatchesRenderRow(row, pendingRevealSidebarRow.rowKey) + ) + if (targetIndex === -1) { + if (retryPendingReveal()) { + return + } + pendingRowRevealRetryRef.current = null + clearPendingRevealSidebarRow() + toast.error( + translate( + 'auto.components.sidebar.WorktreeList.sidebarRowMissing', + 'Target no longer exists' + ) + ) + return + } + + const container = argsRef.current.scrollRef.current + const revealedElement = container + ? revealMountedSidebarRowElement( + container, + pendingRevealSidebarRow.rowKey, + pendingRevealSidebarRow.behavior, + markRevealScroll + ) + : null + if (revealedElement) { + if (pendingRevealSidebarRow.highlight) { + flashRevealedRow(pendingRevealSidebarRow.rowKey) + } + pendingRowRevealRetryRef.current = null + clearPendingRevealSidebarRow() + return + } + + virtualizer.scrollToIndex(targetIndex, { align: 'auto', behavior: 'auto' }) + if (retryPendingReveal()) { + return + } + pendingRowRevealRetryRef.current = null + clearPendingRevealSidebarRow() + }) + + return () => { + cancelled = true + cancelPendingRevealFrames() + } + // Why: the effect re-reads live inputs through argsRef; only reveal-relevant identities should retrigger it. + // oxlint-disable-next-line react-hooks/exhaustive-deps + }, [ + pendingRevealSidebarRow, + args.repoMap, + args.projectGroups, + args.projectGrouping, + args.collapsedGroups, + args.groupBy, + args.toggleGroup, + renderRows, + virtualizer, + pendingRevealRetryTick, + flashRevealedRow, + markRevealScroll, + clearPendingRevealSidebarRow, + schedulePendingRevealFrame, + cancelPendingRevealFrames, + scheduleRetryTick + ]) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-primary-active-worktree-row.ts b/src/renderer/src/components/sidebar/worktree-list/use-primary-active-worktree-row.ts new file mode 100644 index 00000000000..698d0b6d7c8 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-primary-active-worktree-row.ts @@ -0,0 +1,70 @@ +import { useCallback, useEffect, useState } from 'react' +import type { ActiveSurfaceVariant } from '../WorktreeCard' +import type { HostSectionRow } from '../host-section-rows' +import type { PinnedWorktreeDisplayPolicy } from '../worktree-list-groups' +import { isPinnedWorktreeRow, type WorktreeItemRow } from './render-row-item-rows' + +// A worktree can render in more than one section; the row the user actually clicked owns +// the primary active surface so its duplicates stay visually secondary. +export function usePrimaryActiveWorktreeRow(args: { + rows: HostSectionRow[] + activeWorktreeId: string | null + pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy + onImmediateWorktreeActivate: (worktreeId: string, rowKey: string | undefined) => void +}) { + const { rows, activeWorktreeId, pinnedDisplayPolicy, onImmediateWorktreeActivate } = args + const [primaryActiveWorktreeRow, setPrimaryActiveWorktreeRow] = useState<{ + worktreeId: string + rowKey: string + } | null>(null) + + useEffect(() => { + if (activeWorktreeId === null) { + setPrimaryActiveWorktreeRow(null) + return + } + setPrimaryActiveWorktreeRow((current) => { + if (current === null || current.worktreeId !== activeWorktreeId) { + return null + } + const rowStillVisible = rows.some( + (row) => + row.type === 'item' && + row.worktree.id === current.worktreeId && + row.rowKey === current.rowKey + ) + return rowStillVisible ? current : null + }) + }, [activeWorktreeId, rows]) + + const getActiveSurfaceVariant = useCallback( + (row: WorktreeItemRow): ActiveSurfaceVariant => { + if (primaryActiveWorktreeRow?.worktreeId === row.worktree.id) { + return primaryActiveWorktreeRow.rowKey === row.rowKey ? 'primary' : 'secondary' + } + if ( + pinnedDisplayPolicy === 'duplicate-in-groups' && + activeWorktreeId === row.worktree.id && + isPinnedWorktreeRow(row) + ) { + return 'secondary' + } + return 'primary' + }, + [activeWorktreeId, pinnedDisplayPolicy, primaryActiveWorktreeRow] + ) + + const handleImmediateWorktreeRowActivate = useCallback( + (worktreeId: string, rowKey: string | undefined): void => { + setPrimaryActiveWorktreeRow(rowKey ? { worktreeId, rowKey } : null) + onImmediateWorktreeActivate(worktreeId, rowKey) + }, + [onImmediateWorktreeActivate] + ) + + return { + primaryActiveWorktreeRow, + getActiveSurfaceVariant, + handleImmediateWorktreeRowActivate + } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-project-group-dialogs.ts b/src/renderer/src/components/sidebar/worktree-list/use-project-group-dialogs.ts new file mode 100644 index 00000000000..800852f9570 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-project-group-dialogs.ts @@ -0,0 +1,172 @@ +import { useCallback, useMemo, useState } from 'react' +import { toast } from 'sonner' +import { useAppStore } from '@/store' +import { translate } from '@/i18n/i18n' +import { selectProjectGroupRemovalTargets } from '@/store/slices/project-group-removal-targets' +import type { ProjectGroup } from '../../../../../shared/project-group-types' +import type { Repo } from '../../../../../shared/repo-types' + +export type ProjectGroupNameDialogState = + | { type: 'create-from-repo'; repo: Repo } + | { type: 'rename'; groupId: string; currentName: string } + +export type ProjectGroupDeleteDialogState = { + groupId: string + groupName: string + removeContainedProjects: boolean +} + +export type ProjectGroupDialogs = ReturnType + +function reportProjectGroupDeleteFailures(result: { + status: string + failedProjectRemovals: readonly unknown[] + requestedProjectIds: readonly unknown[] +}): void { + // Why: a missing group is already the desired end state, so only a real delete failure warrants a toast. + if (result.status === 'group-delete-failed') { + toast.error( + translate('auto.components.sidebar.WorktreeList.groupDeleteFailed', 'Failed to delete group'), + { + description: translate( + 'auto.components.sidebar.WorktreeList.groupDeleteFailedDesc', + 'Something went wrong while deleting the group. No projects were removed.' + ) + } + ) + return + } + if (result.status === 'deleted-group' && result.failedProjectRemovals.length > 0) { + const requestedCount = result.requestedProjectIds.length + toast.error( + translate( + 'auto.components.sidebar.WorktreeList.b667b59632', + 'Some projects could not be removed from Orca' + ), + { + description: translate( + 'auto.components.sidebar.WorktreeList.f94466bc39', + '{{value0}} of {{value1}} contained project{{value2}} remained after deleting the group.', + { + value0: result.failedProjectRemovals.length, + value1: requestedCount, + value2: requestedCount === 1 ? '' : 's' + } + ) + } + ) + } +} + +// Create/rename/delete flows for project groups, including the contained-project fan-out. +export function useProjectGroupDialogs(args: { + repos: readonly Repo[] + repoMap: Map + projectGroups: readonly ProjectGroup[] +}) { + const { repos, repoMap, projectGroups } = args + const moveProjectToGroup = useAppStore((s) => s.moveProjectToGroup) + const createProjectGroup = useAppStore((s) => s.createProjectGroup) + const updateProjectGroup = useAppStore((s) => s.updateProjectGroup) + const deleteProjectGroupWithContainedProjects = useAppStore( + (s) => s.deleteProjectGroupWithContainedProjects + ) + const [nameDialog, setNameDialog] = useState(null) + const [deleteDialog, setDeleteDialog] = useState(null) + + const handleCreateGroupFromRepo = useCallback((repo: Repo) => { + setNameDialog({ type: 'create-from-repo', repo }) + }, []) + + const handleMoveProjectToGroup = useCallback( + (repo: Repo, groupId: string) => { + if (repo.projectGroupId === groupId) { + return + } + void moveProjectToGroup(repo.id, groupId) + }, + [moveProjectToGroup] + ) + + const handleRemoveProjectFromGroup = useCallback( + (repo: Repo) => { + void moveProjectToGroup(repo.id, null) + }, + [moveProjectToGroup] + ) + + const handleRenameProjectGroup = useCallback((groupId: string, currentName: string) => { + setNameDialog({ type: 'rename', groupId, currentName }) + }, []) + + const handleSubmitProjectGroupName = useCallback( + async (name: string) => { + if (!nameDialog) { + return + } + if (nameDialog.type === 'create-from-repo') { + const group = await createProjectGroup(name) + if (group) { + await moveProjectToGroup(nameDialog.repo.id, group.id) + } + return + } + await updateProjectGroup(nameDialog.groupId, { name }) + }, + [createProjectGroup, moveProjectToGroup, nameDialog, updateProjectGroup] + ) + + const deleteTargets = useMemo(() => { + if (!deleteDialog) { + return null + } + return selectProjectGroupRemovalTargets(projectGroups, repos, deleteDialog.groupId) + }, [deleteDialog, projectGroups, repos]) + const deleteProjectCount = deleteTargets?.projectIds.length ?? 0 + const deleteProjectNames = useMemo( + () => + (deleteTargets?.projectIds ?? []).map( + (projectId) => repoMap.get(projectId)?.displayName ?? projectId + ), + [deleteTargets, repoMap] + ) + const removeContainedProjects = + deleteProjectCount > 0 && deleteDialog?.removeContainedProjects === true + + const handleDeleteProjectGroup = useCallback((groupId: string, groupName: string) => { + setDeleteDialog({ groupId, groupName, removeContainedProjects: false }) + }, []) + + const handleConfirmDeleteProjectGroup = useCallback(async () => { + if (!deleteDialog) { + return + } + try { + reportProjectGroupDeleteFailures( + await deleteProjectGroupWithContainedProjects(deleteDialog.groupId, { + removeContainedProjects + }) + ) + } finally { + // Why: deleting contained projects can unmount this dialog before its close handler runs, so the parent owns cleanup. + setDeleteDialog(null) + } + }, [deleteProjectGroupWithContainedProjects, removeContainedProjects, deleteDialog]) + + return { + nameDialog, + setNameDialog, + deleteDialog, + setDeleteDialog, + deleteProjectCount, + deleteProjectNames, + removeContainedProjects, + handleCreateGroupFromRepo, + handleMoveProjectToGroup, + handleRemoveProjectFromGroup, + handleRenameProjectGroup, + handleSubmitProjectGroupName, + handleDeleteProjectGroup, + handleConfirmDeleteProjectGroup + } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-reused-array-identity.ts b/src/renderer/src/components/sidebar/worktree-list/use-reused-array-identity.ts new file mode 100644 index 00000000000..892f2d7694a --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-reused-array-identity.ts @@ -0,0 +1,10 @@ +import { useRef } from 'react' +import { reuseArrayIfEqual } from '../worktree-agent-row-selectors' + +// Why: epoch-driven recomputes often produce arrays whose contents and order are unchanged; reusing the previous identity when element-wise equal keeps downstream memos and React.memo'd cards bailing out. Safe only because elements (Worktree objects / id strings) are immutably REPLACED on change — never wrap arrays of mutated-in-place objects. +export function useReusedArrayIdentity(next: T[]): T[] { + const previousRef = useRef(next) + const result = reuseArrayIfEqual(previousRef.current, next) + previousRef.current = result + return result +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-sidebar-external-worktree-cards.ts b/src/renderer/src/components/sidebar/worktree-list/use-sidebar-external-worktree-cards.ts new file mode 100644 index 00000000000..a4ab21143da --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-sidebar-external-worktree-cards.ts @@ -0,0 +1,187 @@ +import { useCallback, useMemo, useState } from 'react' +import { useAppStore } from '@/store' +import type { AppState } from '@/store/types' +import type { Repo } from '../../../../../shared/repo-types' +import { + IMPORTED_WORKTREES_KEEP_HIDDEN_ERROR, + keepImportedWorktreesHiddenCard, + showImportedWorktreesCard, + type ImportedWorktreeCardActionState +} from '../imported-worktrees-card-actions' +import { + buildImportedWorktreesCardCandidates, + getHiddenImportedWorktrees +} from '../imported-worktrees-card-candidates' +import { + suppressNewExternalWorktreeInbox, + type NewExternalWorktreesInboxActionState +} from '../new-external-worktrees-inbox-actions' +import { buildNewExternalWorktreesInboxCandidates } from '../new-external-worktrees-inbox-candidates' + +// The two sidebar notice rows for worktrees Orca detected but does not yet show, plus the +// pending/error state their inline actions surface. +export function useSidebarExternalWorktreeCards(args: { + repos: readonly Repo[] + visibleReposForRows: readonly Repo[] + detectedWorktreesByRepo: AppState['detectedWorktreesByRepo'] + filterRepoIds: readonly string[] +}) { + const { repos, visibleReposForRows, detectedWorktreesByRepo, filterRepoIds } = args + const updateRepo = useAppStore((s) => s.updateRepo) + const fetchWorktrees = useAppStore((s) => s.fetchWorktrees) + const [importedWorktreeCardActionState, setImportedWorktreeCardActionState] = useState< + Map + >(new Map()) + const [newExternalWorktreeInboxActionState, setNewExternalWorktreeInboxActionState] = useState< + Map + >(new Map()) + const [suppressExternalWorktreeInboxRepoId, setSuppressExternalWorktreeInboxRepoId] = useState< + string | null + >(null) + + const importedWorktreesByRepo = useMemo(() => { + const forceVisibleRepoIds = new Set( + [...importedWorktreeCardActionState.entries()] + .filter(([, state]) => state.forceVisible) + .map(([repoId]) => repoId) + ) + return buildImportedWorktreesCardCandidates({ + repos: visibleReposForRows, + detectedWorktreesByRepo, + filterRepoIds, + forceVisibleRepoIds + }) + }, [detectedWorktreesByRepo, filterRepoIds, importedWorktreeCardActionState, visibleReposForRows]) + const newExternalWorktreesInboxByRepo = useMemo( + () => + buildNewExternalWorktreesInboxCandidates({ + repos: visibleReposForRows, + detectedWorktreesByRepo, + filterRepoIds + }), + [detectedWorktreesByRepo, filterRepoIds, visibleReposForRows] + ) + + const setImportedWorktreeCardState = useCallback( + (projectId: string, state: ImportedWorktreeCardActionState | null) => { + setImportedWorktreeCardActionState((previous) => { + const next = new Map(previous) + if (state) { + next.set(projectId, state) + } else { + next.delete(projectId) + } + return next + }) + }, + [] + ) + + const handleShowImportedWorktrees = useCallback( + async (projectId: string) => { + await showImportedWorktreesCard({ + projectId, + forceVisible: importedWorktreeCardActionState.get(projectId)?.forceVisible === true, + updateRepo, + fetchWorktrees, + setCardState: setImportedWorktreeCardState + }) + }, + [fetchWorktrees, importedWorktreeCardActionState, setImportedWorktreeCardState, updateRepo] + ) + + const handleKeepImportedWorktreesHidden = useCallback( + async (projectId: string) => { + const repo = repos.find((candidate) => candidate.id === projectId) + let detected = detectedWorktreesByRepo[projectId] + // Why: baseline seeding needs authoritative hidden paths, so don't dismiss on a stale snapshot. + if (detected?.authoritative !== true) { + const refreshed = await fetchWorktrees(projectId, { requireAuthoritative: true }) + if (!refreshed) { + setImportedWorktreeCardState(projectId, { + pending: false, + error: IMPORTED_WORKTREES_KEEP_HIDDEN_ERROR + }) + return + } + detected = useAppStore.getState().detectedWorktreesByRepo[projectId] + } + if (detected?.authoritative !== true) { + setImportedWorktreeCardState(projectId, { + pending: false, + error: IMPORTED_WORKTREES_KEEP_HIDDEN_ERROR + }) + return + } + await keepImportedWorktreesHiddenCard({ + projectId, + updateRepo, + setCardState: setImportedWorktreeCardState, + hiddenWorktreePaths: getHiddenImportedWorktrees(detected).map((worktree) => worktree.path), + existingBaselinePaths: repo?.externalWorktreeInboxBaselinePaths + }) + }, + [detectedWorktreesByRepo, fetchWorktrees, repos, setImportedWorktreeCardState, updateRepo] + ) + + const setNewExternalWorktreeInboxState = useCallback( + (projectId: string, state: NewExternalWorktreesInboxActionState | null) => { + setNewExternalWorktreeInboxActionState((previous) => { + const next = new Map(previous) + if (state) { + next.set(projectId, state) + } else { + next.delete(projectId) + } + return next + }) + }, + [] + ) + + const handleOpenSuppressExternalWorktreeInbox = useCallback((projectId: string) => { + setSuppressExternalWorktreeInboxRepoId(projectId) + }, []) + + const handleConfirmSuppressExternalWorktreeInbox = useCallback(async () => { + if (!suppressExternalWorktreeInboxRepoId) { + return + } + const projectId = suppressExternalWorktreeInboxRepoId + const repo = repos.find((candidate) => candidate.id === projectId) + if (!repo) { + setSuppressExternalWorktreeInboxRepoId(null) + return + } + const inboxWorktrees = newExternalWorktreesInboxByRepo.get(projectId)?.inboxWorktrees ?? [] + const suppressed = await suppressNewExternalWorktreeInbox({ + projectId, + repo, + worktreePaths: inboxWorktrees.map((worktree) => worktree.path), + updateRepo, + setInboxState: setNewExternalWorktreeInboxState + }) + if (suppressed) { + setSuppressExternalWorktreeInboxRepoId(null) + } + }, [ + newExternalWorktreesInboxByRepo, + repos, + setNewExternalWorktreeInboxState, + suppressExternalWorktreeInboxRepoId, + updateRepo + ]) + + return { + importedWorktreeCardActionState, + newExternalWorktreeInboxActionState, + importedWorktreesByRepo, + newExternalWorktreesInboxByRepo, + suppressExternalWorktreeInboxRepoId, + setSuppressExternalWorktreeInboxRepoId, + handleShowImportedWorktrees, + handleKeepImportedWorktreesHidden, + handleOpenSuppressExternalWorktreeInbox, + handleConfirmSuppressExternalWorktreeInbox + } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-sidebar-host-visible-scope.ts b/src/renderer/src/components/sidebar/worktree-list/use-sidebar-host-visible-scope.ts new file mode 100644 index 00000000000..0a0b4cea44a --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-sidebar-host-visible-scope.ts @@ -0,0 +1,69 @@ +import { useMemo } from 'react' +import type { FolderWorkspace } from '../../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../../shared/project-group-types' +import type { Repo } from '../../../../../shared/repo-types' +import { getRepoExecutionHostId, type ExecutionHostId } from '../../../../../shared/execution-host' +import type { SidebarWorktreeFilters } from './use-sidebar-worktree-filters' +import { filterFolderWorkspacesFromOtherDevices } from '../workspace-creator-visibility' +import { + filterFolderWorkspacesForVisibleHosts, + filterProjectGroupsForVisibleHosts, + getVisibleSidebarHostIdSet +} from '../worktree-list-host-filtering' + +// Narrows repos, project groups, and folder workspaces to the hosts (and devices) the +// current host filter admits. +export function useSidebarHostVisibleScope(args: { + filterState: SidebarWorktreeFilters['filterState'] + defaultHostId: ExecutionHostId + repos: readonly Repo[] + projectGroups: readonly ProjectGroup[] + folderWorkspaces: readonly FolderWorkspace[] + pairedDeviceIdsByEnvironment: Parameters[1] +}) { + const { filterState, defaultHostId, repos, projectGroups, folderWorkspaces } = args + const { visibleWorkspaceHostIds, workspaceHostScope, hideWorkspacesFromOtherDevices } = + filterState + const visibleHostIdSet = useMemo( + () => getVisibleSidebarHostIdSet(visibleWorkspaceHostIds, workspaceHostScope), + [visibleWorkspaceHostIds, workspaceHostScope] + ) + const visibleReposForRows = useMemo(() => { + if (!visibleHostIdSet) { + return repos + } + return repos.filter((repo) => { + const hostId = + repo.connectionId || repo.executionHostId ? getRepoExecutionHostId(repo) : defaultHostId + return visibleHostIdSet.has(hostId) + }) + }, [defaultHostId, repos, visibleHostIdSet]) + const visibleProjectGroupsForRows = useMemo( + () => filterProjectGroupsForVisibleHosts(projectGroups, visibleHostIdSet, defaultHostId), + [defaultHostId, projectGroups, visibleHostIdSet] + ) + const visibleFolderWorkspacesForRows = useMemo(() => { + const hostVisibleWorkspaces = filterFolderWorkspacesForVisibleHosts( + folderWorkspaces, + projectGroups, + visibleHostIdSet, + defaultHostId + ) + if (!hideWorkspacesFromOtherDevices) { + return hostVisibleWorkspaces + } + return filterFolderWorkspacesFromOtherDevices( + hostVisibleWorkspaces, + args.pairedDeviceIdsByEnvironment + ) + }, [ + args.pairedDeviceIdsByEnvironment, + defaultHostId, + folderWorkspaces, + hideWorkspacesFromOtherDevices, + projectGroups, + visibleHostIdSet + ]) + + return { visibleReposForRows, visibleProjectGroupsForRows, visibleFolderWorkspacesForRows } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-sidebar-reveal-highlight.ts b/src/renderer/src/components/sidebar/worktree-list/use-sidebar-reveal-highlight.ts new file mode 100644 index 00000000000..82811c7cd4a --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-sidebar-reveal-highlight.ts @@ -0,0 +1,63 @@ +import { useCallback, useRef, useState } from 'react' + +export type SidebarRevealHighlight = ReturnType + +// Owns the frames a reveal schedules and the short glow it leaves on the revealed row. +export function useSidebarRevealHighlight() { + const [highlightedRevealRowKey, setHighlightedRevealRowKey] = useState(null) + const pendingRevealFrameIdsRef = useRef>(new Set()) + const revealHighlightFrameIdRef = useRef(null) + const revealHighlightTimeoutRef = useRef(null) + + const cancelPendingRevealFrames = useCallback(() => { + for (const frameId of pendingRevealFrameIdsRef.current) { + window.cancelAnimationFrame(frameId) + } + pendingRevealFrameIdsRef.current.clear() + }, []) + const schedulePendingRevealFrame = useCallback((callback: FrameRequestCallback) => { + const frameId = window.requestAnimationFrame((time) => { + pendingRevealFrameIdsRef.current.delete(frameId) + callback(time) + }) + pendingRevealFrameIdsRef.current.add(frameId) + }, []) + const clearRevealHighlightFrame = useCallback(() => { + if (revealHighlightFrameIdRef.current !== null) { + window.cancelAnimationFrame(revealHighlightFrameIdRef.current) + revealHighlightFrameIdRef.current = null + } + }, []) + const clearRevealHighlightTimeout = useCallback(() => { + if (revealHighlightTimeoutRef.current !== null) { + window.clearTimeout(revealHighlightTimeoutRef.current) + revealHighlightTimeoutRef.current = null + } + }, []) + const flashRevealedRow = useCallback( + (rowKey: string) => { + clearRevealHighlightTimeout() + clearRevealHighlightFrame() + // Why: clear before set restarts the CSS glow when revealing the same row repeatedly. + setHighlightedRevealRowKey(null) + revealHighlightFrameIdRef.current = window.requestAnimationFrame(() => { + revealHighlightFrameIdRef.current = null + setHighlightedRevealRowKey(rowKey) + revealHighlightTimeoutRef.current = window.setTimeout(() => { + revealHighlightTimeoutRef.current = null + setHighlightedRevealRowKey(null) + }, 1500) + }) + }, + [clearRevealHighlightFrame, clearRevealHighlightTimeout] + ) + + return { + highlightedRevealRowKey, + cancelPendingRevealFrames, + schedulePendingRevealFrame, + clearRevealHighlightFrame, + clearRevealHighlightTimeout, + flashRevealedRow + } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-sidebar-reveal-requests.ts b/src/renderer/src/components/sidebar/worktree-list/use-sidebar-reveal-requests.ts new file mode 100644 index 00000000000..4737a7fc60a --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-sidebar-reveal-requests.ts @@ -0,0 +1,125 @@ +import { useCallback, useEffect } from 'react' +import { useAppStore } from '@/store' +import { + SCROLL_TO_CURRENT_WORKSPACE_REVEAL_REQUEST_EVENT, + type ScrollToCurrentWorkspaceRevealRequestDetail +} from '@/lib/scroll-to-current-workspace-status' +import type { FolderWorkspace } from '../../../../../shared/folder-workspace-types' +import type { Worktree } from '../../../../../shared/worktree/types' +import type { WorktreeGroupBy } from '../worktree-list-groups' +import { getKnownSidebarWorktreeById } from '../worktree-list-folder-reveal' + +// Turns a "show me the current workspace" request into whatever the sidebar must change +// first — grouping mode, active filters — before the viewport can scroll to it. +export function useSidebarRevealRequests(args: { + groupBy: WorktreeGroupBy + renderedSidebarRowKeys: ReadonlySet + renderedWorktreeIds: readonly string[] + currentSidebarWorktreeId: string | null + worktreeMap: Map + folderWorkspaces: readonly FolderWorkspace[] + hasFilters: boolean + clearFilters: () => void +}): void { + const { + groupBy, + renderedSidebarRowKeys, + renderedWorktreeIds, + currentSidebarWorktreeId, + worktreeMap, + folderWorkspaces, + hasFilters, + clearFilters + } = args + const setGroupBy = useAppStore((s) => s.setGroupBy) + const pendingRevealSidebarRow = useAppStore((s) => s.pendingRevealSidebarRow) + const revealSidebarRow = useAppStore((s) => s.revealSidebarRow) + const revealWorktreeInSidebar = useAppStore((s) => s.revealWorktreeInSidebar) + + useEffect(() => { + if (!pendingRevealSidebarRow) { + return + } + const rowKey = pendingRevealSidebarRow.rowKey + const isProjectHeaderTarget = + rowKey.startsWith('project-group:') || + rowKey.startsWith('project:') || + rowKey.startsWith('repo:') + if (isProjectHeaderTarget && groupBy !== 'repo') { + setGroupBy('repo') + return + } + if (!renderedSidebarRowKeys.has(rowKey) && hasFilters) { + clearFilters() + } + }, [ + clearFilters, + groupBy, + hasFilters, + pendingRevealSidebarRow, + renderedSidebarRowKeys, + setGroupBy + ]) + + const handleRevealCurrentWorkspaceRequest = useCallback( + (event: Event) => { + const detail = + event instanceof CustomEvent + ? (event.detail as ScrollToCurrentWorkspaceRevealRequestDetail | undefined) + : undefined + if (detail?.target?.type === 'sidebar-row') { + const sidebarDetail = detail as Extract< + ScrollToCurrentWorkspaceRevealRequestDetail, + { target: { type: 'sidebar-row' } } + > + revealSidebarRow(detail.target.rowKey, { + behavior: 'smooth', + highlight: sidebarDetail.highlight !== false + }) + return + } + if (!currentSidebarWorktreeId) { + return + } + const activeWorktree = getKnownSidebarWorktreeById( + currentSidebarWorktreeId, + worktreeMap, + folderWorkspaces + ) + if (!activeWorktree || activeWorktree.isArchived) { + return + } + if (!renderedWorktreeIds.includes(currentSidebarWorktreeId)) { + // Why: the reveal action must show the current workspace, so relax filters that hide it first. + clearFilters() + } + revealWorktreeInSidebar(currentSidebarWorktreeId, { + behavior: 'smooth', + highlight: true, + beginRename: (detail as { beginRename?: boolean } | undefined)?.beginRename === true + }) + }, + [ + clearFilters, + currentSidebarWorktreeId, + folderWorkspaces, + revealSidebarRow, + renderedWorktreeIds, + revealWorktreeInSidebar, + worktreeMap + ] + ) + + useEffect(() => { + window.addEventListener( + SCROLL_TO_CURRENT_WORKSPACE_REVEAL_REQUEST_EVENT, + handleRevealCurrentWorkspaceRequest + ) + return () => { + window.removeEventListener( + SCROLL_TO_CURRENT_WORKSPACE_REVEAL_REQUEST_EVENT, + handleRevealCurrentWorkspaceRequest + ) + } + }, [handleRevealCurrentWorkspaceRequest]) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-sidebar-section-rows.ts b/src/renderer/src/components/sidebar/worktree-list/use-sidebar-section-rows.ts new file mode 100644 index 00000000000..9b139e88719 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-sidebar-section-rows.ts @@ -0,0 +1,260 @@ +import { useCallback, useMemo, useState } from 'react' +import { useShallow } from 'zustand/react/shallow' +import { useAppStore } from '@/store' +import type { AppState } from '@/store/types' +import type { FolderWorkspace } from '../../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../../shared/project-group-types' +import type { ProjectOrderBy } from '../../../../../shared/ui-chrome-types' +import type { Repo } from '../../../../../shared/repo-types' +import type { WorkspaceStatusDefinition, Worktree } from '../../../../../shared/worktree/types' +import type { WorktreeLineage } from '../../../../../shared/worktree/lineage-types' +import type { ExecutionHostId } from '../../../../../shared/execution-host' +import { folderWorkspaceKey } from '../../../../../shared/workspace-scope' +import { getHostDisplayLabelOverrides } from '../../../../../shared/host-setting-overrides' +import { + buildRows, + type PinnedWorktreeDisplayPolicy, + type ProjectGroupingModel, + type Row, + type WorktreeGroupBy +} from '../worktree-list-groups' +import { getLogicalRepoOrderRankById } from '../project-header-drop' +import { getEmptyProjectPlaceholderRepoIds } from '../empty-project-placeholder-repos' +import { addHostSectionRows } from '../host-section-rows' +import { orderHostSectionOptions } from '../host-section-order' +import { buildSidebarHostOptions } from '../sidebar-host-options' + +type SectionRowsArgs = { + groupBy: WorktreeGroupBy + projectOrderBy: ProjectOrderBy + pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy + defaultHostId: ExecutionHostId + worktrees: Worktree[] + repos: readonly Repo[] + repoMap: Map + worktreeMap: Map + worktreeLineageById: Record + prCache: AppState['prCache'] | null + settings: AppState['settings'] + workspaceStatuses: readonly WorkspaceStatusDefinition[] + effectiveCollapsedGroups: Set + projectGrouping: ProjectGroupingModel + visibleReposForRows: readonly Repo[] + visibleProjectGroupsForRows: readonly ProjectGroup[] + visibleFolderWorkspacesForRows: readonly FolderWorkspace[] + importedWorktreesByRepo: Parameters[14] + newExternalWorktreesInboxByRepo: Parameters[15] + filterRepoIds: readonly string[] + visibleWorkspaceHostIds: readonly ExecutionHostId[] | null + workspaceHostScope: AppState['workspaceHostScope'] +} + +function collectRenderedSidebarRowKeys(sectionRows: ReturnType) { + const keys = new Set() + for (const row of sectionRows) { + if (row.type === 'header') { + keys.add(row.key) + } else if (row.type === 'item') { + keys.add(row.rowKey) + } else if (row.type === 'folder-workspace') { + keys.add(folderWorkspaceKey(row.folderWorkspace.id)) + } else if (row.type === 'pending-creation') { + keys.add(`pending:${row.creationId}`) + } else if (row.type === 'imported-worktrees-card') { + keys.add(row.key) + } else if (row.type === 'new-external-worktrees-inbox') { + keys.add(row.key) + } + } + return keys +} + +// Builds the full sidebar row model: grouped worktree rows first, then the host-section +// tier wrapped around them. +export function useSidebarSectionRows(args: SectionRowsArgs) { + const { repos, worktrees, repoMap, effectiveCollapsedGroups, defaultHostId } = args + const worktreesByRepo = useAppStore((s) => s.worktreesByRepo) + const sshTargetLabels = useAppStore((s) => s.sshTargetLabels) + const sshConnectionStates = useAppStore((s) => s.sshConnectionStates) + const runtimeEnvironments = useAppStore((s) => s.runtimeEnvironments) + const runtimeStatusByEnvironmentId = useAppStore((s) => s.runtimeStatusByEnvironmentId) + const workspaceHostOrder = useAppStore((s) => s.workspaceHostOrder) + const setWorkspaceHostOrder = useAppStore((s) => s.setWorkspaceHostOrder) + + // Why: manual header order is bound to state.repos; Recent/Smart derive order from the sorted worktree stream. + const repoOrder = useMemo( + () => getLogicalRepoOrderRankById(repos.map((repo) => repo.id)), + [repos] + ) + const allRepoIds = useMemo(() => repos.map((r) => r.id), [repos]) + const placeholderRepoIds = useMemo( + () => + getEmptyProjectPlaceholderRepoIds({ + groupBy: args.groupBy, + repos: args.visibleReposForRows, + worktreesByRepo, + visibleWorktrees: worktrees, + filterRepoIds: args.filterRepoIds + }), + [args.filterRepoIds, args.groupBy, args.visibleReposForRows, worktrees, worktreesByRepo] + ) + + // Why: subscribe on a flat key array (useShallow) so progress ticks don't rebuild the whole row model. + // Split on first space — creationId is a UUID (no space) so a space-containing repoId stays intact. + const pendingCreationKeys = useAppStore( + useShallow((s) => + Object.values(s.pendingWorktreeCreations ?? {}).map( + (creation) => `${creation.creationId} ${creation.request.repoId}` + ) + ) + ) + const pendingCreations = useMemo( + () => + pendingCreationKeys.map((key) => { + const separator = key.indexOf(' ') + return { creationId: key.slice(0, separator), repoId: key.slice(separator + 1) } + }), + [pendingCreationKeys] + ) + const hostLabelOverrides = useMemo( + () => getHostDisplayLabelOverrides(args.settings), + [args.settings] + ) + const hostOptions = useMemo( + () => + buildSidebarHostOptions({ + repos, + sshTargetLabels, + sshConnectionStates, + settings: args.settings, + runtimeEnvironments, + runtimeStatusByEnvironmentId, + hostLabelOverrides + }), + [ + repos, + sshTargetLabels, + sshConnectionStates, + args.settings, + runtimeEnvironments, + runtimeStatusByEnvironmentId, + hostLabelOverrides + ] + ) + const hostLabelById = useMemo( + () => new Map(hostOptions.map((host) => [host.id, host.label])), + [hostOptions] + ) + + const rows: Row[] = useMemo( + () => + buildRows( + args.groupBy, + worktrees, + repoMap, + args.prCache, + effectiveCollapsedGroups, + repoOrder, + args.workspaceStatuses, + args.projectOrderBy, + args.worktreeLineageById, + args.worktreeMap, + true, + args.settings, + args.visibleProjectGroupsForRows, + placeholderRepoIds, + args.importedWorktreesByRepo, + args.newExternalWorktreesInboxByRepo, + pendingCreations, + args.projectGrouping, + args.visibleFolderWorkspacesForRows, + hostLabelById, + defaultHostId, + args.pinnedDisplayPolicy + ), + [ + args.groupBy, + worktrees, + repoMap, + args.prCache, + effectiveCollapsedGroups, + defaultHostId, + repoOrder, + args.workspaceStatuses, + args.projectOrderBy, + args.worktreeLineageById, + args.worktreeMap, + args.settings, + args.projectGrouping, + args.visibleProjectGroupsForRows, + args.visibleFolderWorkspacesForRows, + placeholderRepoIds, + args.importedWorktreesByRepo, + args.newExternalWorktreesInboxByRepo, + pendingCreations, + hostLabelById, + args.pinnedDisplayPolicy + ] + ) + const orderedHostOptions = useMemo( + () => orderHostSectionOptions(hostOptions, workspaceHostOrder), + [hostOptions, workspaceHostOrder] + ) + const [hostDragActive, setHostDragActive] = useState(false) + const handleReorderHostSections = useCallback( + (orderedVisibleHostIds: ExecutionHostId[]) => { + const visibleHostIds = new Set(orderedVisibleHostIds) + const hostOptionIds = orderedHostOptions.map((host) => host.id) + const knownHostIds = new Set(hostOptionIds) + const nextOrder: ExecutionHostId[] = [...orderedVisibleHostIds] + const seen = new Set(nextOrder) + // Why: dragging only covers rendered hosts; keep non-rendered SSH/runtime hosts in the saved order so they return in place. + for (const hostId of [...workspaceHostOrder, ...hostOptionIds]) { + if (!knownHostIds.has(hostId) || visibleHostIds.has(hostId) || seen.has(hostId)) { + continue + } + nextOrder.push(hostId) + seen.add(hostId) + } + setWorkspaceHostOrder(nextOrder) + }, + [orderedHostOptions, setWorkspaceHostOrder, workspaceHostOrder] + ) + const sectionRows = useMemo( + () => + addHostSectionRows({ + rows, + hostOptions: orderedHostOptions, + workspaceHostScope: args.workspaceHostScope, + visibleWorkspaceHostIds: args.visibleWorkspaceHostIds, + defaultHostId, + collapsedHostKeys: effectiveCollapsedGroups, + forceCollapseHosts: hostDragActive, + // Why: projects/workspaces are the primary sidebar object; host sections are only an explicit host-filter view. + preferProjectGrouping: true + }), + [ + args.visibleWorkspaceHostIds, + args.workspaceHostScope, + defaultHostId, + effectiveCollapsedGroups, + hostDragActive, + orderedHostOptions, + rows + ] + ) + const renderedSidebarRowKeys = useMemo( + () => collectRenderedSidebarRowKeys(sectionRows), + [sectionRows] + ) + + return { + rows, + sectionRows, + renderedSidebarRowKeys, + allRepoIds, + placeholderRepoIds, + handleReorderHostSections, + setHostDragActive + } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-sidebar-worktree-filters.ts b/src/renderer/src/components/sidebar/worktree-list/use-sidebar-worktree-filters.ts new file mode 100644 index 00000000000..7f59d344e6d --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-sidebar-worktree-filters.ts @@ -0,0 +1,106 @@ +import { useCallback, useMemo } from 'react' +import { useAppStore } from '@/store' +import { DEFAULT_SHOW_SLEEPING_WORKSPACES } from '../../../../../shared/constants' +import { computeClearFilterActions, sidebarHasActiveFilters } from '../visible-worktrees' + +export type SidebarWorktreeFilters = ReturnType + +// Every sidebar filter, plus the single escape hatch that resets all of them. +export function useSidebarWorktreeFilters() { + const showSleepingWorkspaces = useAppStore((s) => s.showSleepingWorkspaces) + const filterRepoIds = useAppStore((s) => s.filterRepoIds) + const hideDefaultBranchWorkspace = useAppStore((s) => s.hideDefaultBranchWorkspace) + const hideAutomationGeneratedWorkspaces = useAppStore((s) => s.hideAutomationGeneratedWorkspaces) + const hideCliCreatedWorkspaces = useAppStore((s) => s.hideCliCreatedWorkspaces) + const hideDetachedHeadWorkspaces = useAppStore((s) => s.hideDetachedHeadWorkspaces) + const hideWorkspacesFromOtherDevices = useAppStore((s) => s.hideWorkspacesFromOtherDevices) + const alwaysShowDefaultBranchWorkspace = useAppStore((s) => s.alwaysShowDefaultBranchWorkspace) + const visibleWorkspaceHostIds = useAppStore((s) => s.visibleWorkspaceHostIds) + const workspaceHostScope = useAppStore((s) => s.workspaceHostScope) + + const setShowSleepingWorkspaces = useAppStore((s) => s.setShowSleepingWorkspaces) + const setHideDefaultBranchWorkspace = useAppStore((s) => s.setHideDefaultBranchWorkspace) + const setHideAutomationGeneratedWorkspaces = useAppStore( + (s) => s.setHideAutomationGeneratedWorkspaces + ) + const setHideCliCreatedWorkspaces = useAppStore((s) => s.setHideCliCreatedWorkspaces) + const setHideDetachedHeadWorkspaces = useAppStore((s) => s.setHideDetachedHeadWorkspaces) + const setHideWorkspacesFromOtherDevices = useAppStore((s) => s.setHideWorkspacesFromOtherDevices) + const setAlwaysShowDefaultBranchWorkspace = useAppStore( + (s) => s.setAlwaysShowDefaultBranchWorkspace + ) + const setFilterRepoIds = useAppStore((s) => s.setFilterRepoIds) + const setVisibleWorkspaceHostIds = useAppStore((s) => s.setVisibleWorkspaceHostIds) + + // Why: count hideDefaultBranchWorkspace as a filter so the Clear Filters escape hatch stays reachable when it alone empties the list. + const filterState = useMemo( + () => ({ + showSleepingWorkspaces, + filterRepoIds, + hideDefaultBranchWorkspace, + hideAutomationGeneratedWorkspaces, + hideCliCreatedWorkspaces, + hideDetachedHeadWorkspaces, + hideWorkspacesFromOtherDevices, + alwaysShowDefaultBranchWorkspace, + visibleWorkspaceHostIds, + workspaceHostScope + }), + [ + showSleepingWorkspaces, + filterRepoIds, + hideDefaultBranchWorkspace, + hideAutomationGeneratedWorkspaces, + hideCliCreatedWorkspaces, + hideDetachedHeadWorkspaces, + hideWorkspacesFromOtherDevices, + alwaysShowDefaultBranchWorkspace, + visibleWorkspaceHostIds, + workspaceHostScope + ] + ) + + const clearFilters = useCallback(() => { + const actions = computeClearFilterActions(filterState) + if (actions.resetShowSleepingWorkspaces) { + setShowSleepingWorkspaces(DEFAULT_SHOW_SLEEPING_WORKSPACES) + } + if (actions.resetFilterRepoIds) { + setFilterRepoIds([]) + } + if (actions.resetHideDefaultBranchWorkspace) { + setHideDefaultBranchWorkspace(false) + } + if (actions.resetHideAutomationGeneratedWorkspaces) { + setHideAutomationGeneratedWorkspaces(false) + } + if (actions.resetHideCliCreatedWorkspaces) { + setHideCliCreatedWorkspaces(false) + } + if (actions.resetHideDetachedHeadWorkspaces) { + setHideDetachedHeadWorkspaces(false) + } + if (actions.resetHideWorkspacesFromOtherDevices) { + setHideWorkspacesFromOtherDevices(false) + } + if (actions.resetAlwaysShowDefaultBranchWorkspace) { + setAlwaysShowDefaultBranchWorkspace(true) + } + if (actions.resetVisibleWorkspaceHostIds) { + setVisibleWorkspaceHostIds(null) + } + }, [ + setShowSleepingWorkspaces, + setFilterRepoIds, + setHideDefaultBranchWorkspace, + setHideAutomationGeneratedWorkspaces, + setHideCliCreatedWorkspaces, + setHideDetachedHeadWorkspaces, + setHideWorkspacesFromOtherDevices, + setAlwaysShowDefaultBranchWorkspace, + setVisibleWorkspaceHostIds, + filterState + ]) + + return { filterState, hasFilters: sidebarHasActiveFilters(filterState), clearFilters } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-sidebar-worktree-selection.ts b/src/renderer/src/components/sidebar/worktree-list/use-sidebar-worktree-selection.ts new file mode 100644 index 00000000000..426385c6063 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-sidebar-worktree-selection.ts @@ -0,0 +1,140 @@ +import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react' +import type React from 'react' +import type { Worktree } from '../../../../../shared/worktree/types' +import type { HostSectionRow } from '../host-section-rows' +import type { PinnedWorktreeDisplayPolicy } from '../worktree-list-groups' +import { getRenderedWorktreesInSidebarOrder } from '../worktree-sidebar-row-preference' +import { setVisibleWorktreeIds } from '../visible-worktrees' +import { + areWorktreeSelectionsEqual, + getWorktreeSelectionIntent, + pruneWorktreeSelection, + updateWorktreeSelection +} from '../worktree-multi-selection' +import { useReusedArrayIdentity } from './use-reused-array-identity' + +function uniqueWorktreeIds(ids: readonly string[]): string[] { + return Array.from(new Set(ids)) +} + +// Multi-select over the rows the sidebar actually rendered, so gestures, context menus, and +// the Cmd+1–9 shortcut cache all agree on one order. +export function useSidebarWorktreeSelection(args: { + sectionRows: HostSectionRow[] + pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy +}) { + const { sectionRows, pinnedDisplayPolicy } = args + // Why: derive order from the built rows, not the flat worktrees array, so Cmd+1–9 match visual positions when grouping reorders cards. + const renderedWorktrees = useMemo( + () => getRenderedWorktreesInSidebarOrder(sectionRows, pinnedDisplayPolicy), + [pinnedDisplayPolicy, sectionRows] + ) + // Why: order-preserving sectionRows rebuilds must not give this array a new + // identity — updateSelectionForGesture depends on it, and a fresh identity + // there defeats React.memo bail-out for every WorktreeCard on epoch bumps. + const renderedWorktreeIds = useReusedArrayIdentity( + useMemo( + () => uniqueWorktreeIds(renderedWorktrees.map((worktree) => worktree.id)), + [renderedWorktrees] + ) + ) + const [selectedWorktreeIds, setSelectedWorktreeIds] = useState>(new Set()) + const [selectionAnchorId, setSelectionAnchorId] = useState(null) + + const prunedSelection = pruneWorktreeSelection( + selectedWorktreeIds, + selectionAnchorId, + renderedWorktreeIds + ) + // Why: filters/grouping can hide selected cards; prune during render so nothing sees stale ids for unrendered worktrees. + if (!areWorktreeSelectionsEqual(selectedWorktreeIds, prunedSelection.selectedIds)) { + setSelectedWorktreeIds(prunedSelection.selectedIds) + } + if (selectionAnchorId !== prunedSelection.anchorId) { + setSelectionAnchorId(prunedSelection.anchorId) + } + + // Why identity reuse: the empty/unchanged-selection case must keep one array + // identity — selectForContextMenu and both drag-start handlers depend on + // this array, and card memo bail-out depends on those staying stable. + const selectedWorktrees = useReusedArrayIdentity( + useMemo(() => { + if (selectedWorktreeIds.size === 0) { + return [] + } + const selected = new Map() + for (const worktree of renderedWorktrees) { + if (selectedWorktreeIds.has(worktree.id) && !selected.has(worktree.id)) { + selected.set(worktree.id, worktree) + } + } + return Array.from(selected.values()) + }, [renderedWorktrees, selectedWorktreeIds]) + ) + + useEffect(() => { + if (selectedWorktreeIds.size === 0) { + return + } + + const clearSelectionOutsideSidebar = (event: PointerEvent): void => { + const target = event.target + const sidebarContainer = document.querySelector('[data-worktree-sidebar-container]') + if (target instanceof Node && sidebarContainer?.contains(target)) { + return + } + setSelectedWorktreeIds(new Set()) + setSelectionAnchorId(null) + } + + document.addEventListener('pointerdown', clearSelectionOutsideSidebar, { capture: true }) + return () => { + document.removeEventListener('pointerdown', clearSelectionOutsideSidebar, { capture: true }) + } + }, [selectedWorktreeIds.size]) + + const updateSelectionForGesture = useCallback( + (event: React.MouseEvent, worktreeId: string): boolean => { + const intent = getWorktreeSelectionIntent(event, navigator.userAgent.includes('Mac')) + const result = updateWorktreeSelection({ + visibleIds: renderedWorktreeIds, + previousSelectedIds: selectedWorktreeIds, + previousAnchorId: selectionAnchorId, + targetId: worktreeId, + intent + }) + setSelectedWorktreeIds(result.selectedIds) + setSelectionAnchorId(result.anchorId) + // Plain click navigates; modifier gestures are selection-only so a batch can build without switching away. + return intent !== 'replace' + }, + [renderedWorktreeIds, selectedWorktreeIds, selectionAnchorId] + ) + + const selectForContextMenu = useCallback( + (_event: React.MouseEvent, worktree: Worktree): readonly Worktree[] => { + if (selectedWorktreeIds.has(worktree.id) && selectedWorktreeIds.size > 1) { + return selectedWorktrees + } + setSelectedWorktreeIds(new Set([worktree.id])) + setSelectionAnchorId(worktree.id) + return [worktree] + }, + [selectedWorktreeIds, selectedWorktrees] + ) + + // Why layout effect: the Cmd/Ctrl+1–9 handler can fire right after commit; publishing after paint would leave the shortcut cache stale. + useLayoutEffect(() => { + setVisibleWorktreeIds(renderedWorktreeIds) + // Why null, not []: [] is a real rendered order (all collapsed/filtered); null tells shortcuts the list is unmounted. + return () => setVisibleWorktreeIds(null) + }, [renderedWorktreeIds]) + + return { + renderedWorktreeIds, + selectedWorktreeIds, + selectedWorktrees, + updateSelectionForGesture, + selectForContextMenu + } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-sidebar-worktree-sort-order.ts b/src/renderer/src/components/sidebar/worktree-list/use-sidebar-worktree-sort-order.ts new file mode 100644 index 00000000000..64b2e426abf --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-sidebar-worktree-sort-order.ts @@ -0,0 +1,207 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { useAppStore } from '@/store' +import { getAllWorktreesFromState } from '@/store/selectors' +import { track } from '@/lib/telemetry' +import { tabHasLivePty } from '@/lib/tab-has-live-pty' +import { persistWorktreeSortOrderByHost } from '@/lib/worktree-sort-order-persistence' +import type { Repo } from '../../../../../shared/repo-types' +import type { Worktree } from '../../../../../shared/worktree/types' +import { buildWorktreeComparator, compareWorktreeSortLabel, type SortBy } from '../smart-sort' +import { + buildAttentionByWorktree, + hasFreshAttributedAgentStatus, + type SmartClass, + type WorktreeAttention +} from '../smart-attention' +import { useReusedArrayIdentity } from './use-reused-array-identity' +import { SORT_SETTLE_MS } from './worktree-sidebar-scroll-tuning' + +function trackSmartClassDistribution(attention: ReadonlyMap): void { + let class1 = 0 + let class2 = 0 + let class3 = 0 + let class4 = 0 + for (const info of attention.values()) { + if (info.cls === 1) { + class1++ + } else if (info.cls === 2) { + class2++ + } else if (info.cls === 3) { + class3++ + } else { + class4++ + } + } + track('smart_sort_class_distribution', { + class_1: class1, + class_2: class2, + class_3: class3, + class_4: class4, + total_worktrees: attention.size + }) +} + +// Why debounce: scores are time-decaying, so recomputing on every sortEpoch bump makes worktrees jump; settle to coalesce. +// Structural changes (add/remove) bypass the debounce so a new worktree appears at its sorted position immediately. +function useDebouncedSortEpoch(worktreeCount: number, sortBy: SortBy): number { + const sortEpoch = useAppStore((s) => s.sortEpoch) + const [debouncedSortEpoch, setDebouncedSortEpoch] = useState(sortEpoch) + const prevWorktreeCountRef = useRef(worktreeCount) + useEffect(() => { + if (debouncedSortEpoch === sortEpoch) { + return + } + + const structuralChange = worktreeCount !== prevWorktreeCountRef.current + prevWorktreeCountRef.current = worktreeCount + + // Why: manual drag/drop is direct manipulation; the settle-window delay would make a successful drop look broken. + if (structuralChange || sortBy === 'manual') { + setDebouncedSortEpoch(sortEpoch) + return + } + + const timer = setTimeout(() => setDebouncedSortEpoch(sortEpoch), SORT_SETTLE_MS) + return () => clearTimeout(timer) + }, [sortEpoch, debouncedSortEpoch, worktreeCount, sortBy]) + return debouncedSortEpoch +} + +// ── Stable sort order ────────────────────────────────────────── +// Why sortEpoch (not selection): selection side-effects (clearing isUnread, PR-cache refresh) must not reorder the sidebar under the user. +// Why useMemo not useEffect: order must be computed synchronously before the worktrees memo reads it. +export function useSidebarWorktreeSortOrder(args: { + allWorktrees: readonly Worktree[] + repoMap: Map + sortBy: SortBy +}): string[] { + const { allWorktrees, repoMap, sortBy } = args + // Non-archived count — detects structural changes (add/remove) so the debounce below can apply immediately. + const worktreeCount = useMemo(() => { + let count = 0 + for (const worktree of allWorktrees) { + if (!worktree.isArchived) { + count++ + } + } + return count + }, [allWorktrees]) + const debouncedSortEpoch = useDebouncedSortEpoch(worktreeCount, sortBy) + + // Why a latching ref: a live signal makes Smart authoritative for the session, even after that activity ends. + const sessionHasHadLiveSmartSignal = useRef(false) + // Why a ref alongside the memo: telemetry effects need the last attention map without re-reading store state. + const lastAttentionByWorktreeRef = useRef | null>(null) + + const recomputedSortedIds = useMemo(() => { + const state = useAppStore.getState() + const nonArchivedWorktrees = getAllWorktreesFromState(state).filter( + (worktree) => !worktree.isArchived + ) + const now = Date.now() + + // Why cold-start detection: agent-status hydrates async, so the warm comparator would collapse all to Class 4; keep the persisted order until a live signal appears. + if (sortBy === 'smart' && !sessionHasHadLiveSmartSignal.current) { + // Why tabHasLivePty over tab.ptyId: slept terminals keep tab.ptyId as a wake hint, so it'd falsely keep cold-start ordering off. + const hasAnyLivePty = Object.values(state.tabsByWorktree) + .flat() + .some((tab) => tabHasLivePty(state.ptyIdsByTabId, tab.id)) + if ( + hasAnyLivePty || + hasFreshAttributedAgentStatus(state.agentStatusByPaneKey, now, state.tabsByWorktree) + ) { + sessionHasHadLiveSmartSignal.current = true + } else { + nonArchivedWorktrees.sort( + (a, b) => b.sortOrder - a.sortOrder || compareWorktreeSortLabel(a, b) + ) + lastAttentionByWorktreeRef.current = null + return nonArchivedWorktrees.map((w) => w.id) + } + } + + // Why precompute: hot sort — build the attention map once so the O(N log N) comparator does O(1) lookups. + const attentionByWorktree = + sortBy === 'smart' + ? buildAttentionByWorktree( + nonArchivedWorktrees, + state.tabsByWorktree, + state.agentStatusByPaneKey, + state.runtimePaneTitlesByTabId, + state.ptyIdsByTabId, + now, + state.migrationUnsupportedByPtyId, + state.terminalLayoutsByTabId + ) + : new Map() + lastAttentionByWorktreeRef.current = sortBy === 'smart' ? attentionByWorktree : null + nonArchivedWorktrees.sort(buildWorktreeComparator(sortBy, repoMap, now, attentionByWorktree)) + return nonArchivedWorktrees.map((w) => w.id) + // debouncedSortEpoch is an intentional trigger not read in the memo; its change (debounced) signals a recompute. + // oxlint-disable-next-line react-hooks/exhaustive-deps + }, [debouncedSortEpoch, repoMap, sortBy]) + // Why: stable ID order prevents rank-only refreshes from echoing an unchanged snapshot. + const sortedIds = useReusedArrayIdentity(recomputedSortedIds) + + // Why a ref of prior class: fire class_1_promotion only on transitions into Class 1, not every recompute that stays there. + const prevClassByWorktreeIdRef = useRef>(new Map()) + // Why gate the first observation: an empty prev-class map makes every existing Class-1 worktree look freshly promoted; treat the first pass as a silent baseline. + const hasObservedSmartOnceRef = useRef(false) + + useEffect(() => { + const attention = lastAttentionByWorktreeRef.current + if (sortBy !== 'smart' || !attention) { + // Why reset: leaving Smart drops the prior-class map (and first-observation gate) so re-entry doesn't fire stale promotions. + prevClassByWorktreeIdRef.current = new Map() + hasObservedSmartOnceRef.current = false + return + } + const next = new Map() + const isFirstObservation = !hasObservedSmartOnceRef.current + for (const [worktreeId, info] of attention) { + const prev = prevClassByWorktreeIdRef.current.get(worktreeId) + if (!isFirstObservation && info.cls === 1 && prev !== 1 && info.cause) { + track('smart_sort_class_1_promotion', { cause: info.cause }) + } + next.set(worktreeId, info.cls) + } + prevClassByWorktreeIdRef.current = next + hasObservedSmartOnceRef.current = true + }, [sortBy, recomputedSortedIds]) + + // Why retry on recomputation: Smart may activate before attention hydrates; fire once, then stay quiet until the user leaves Smart. + const hasTrackedSmartDistributionRef = useRef(false) + useEffect(() => { + if (sortBy !== 'smart') { + hasTrackedSmartDistributionRef.current = false + return + } + const attention = lastAttentionByWorktreeRef.current + if (hasTrackedSmartDistributionRef.current || !attention || attention.size === 0) { + return + } + trackSmartClassDistribution(attention) + hasTrackedSmartDistributionRef.current = true + }, [sortBy, recomputedSortedIds]) + + // Why fire on the transition: switching away from Smart is the signal; compare via ref so a round-trip doesn't double-fire. + const prevSortByRef = useRef(sortBy) + useEffect(() => { + const prev = prevSortByRef.current + prevSortByRef.current = sortBy + if (prev === 'smart' && sortBy === 'recent') { + track('smart_to_recent_switch', {}) + } + }, [sortBy]) + + // Why: only persist during live sessions so cold start reads the persisted order instead of overwriting it. + useEffect(() => { + if (sortBy !== 'smart' || sortedIds.length === 0 || !sessionHasHadLiveSmartSignal.current) { + return + } + // Why: sortOrder lives in each host's worktreeMeta, so persist each host's ids on that host. + persistWorktreeSortOrderByHost(useAppStore.getState(), sortedIds) + }, [sortedIds, sortBy]) + + return sortedIds +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-virtual-row-measurement-sync.ts b/src/renderer/src/components/sidebar/worktree-list/use-virtual-row-measurement-sync.ts new file mode 100644 index 00000000000..79f0d53b429 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-virtual-row-measurement-sync.ts @@ -0,0 +1,116 @@ +import { useCallback, useLayoutEffect, useMemo } from 'react' +import type React from 'react' +import { useAppStore } from '@/store' +import { + useVirtualizedScrollAnchor, + type VirtualizedScrollAnchor +} from '@/hooks/useVirtualizedScrollAnchor' +import { + buildLineageRowRekeyMap, + getActiveStickyIndexesForScroll, + getRenderRowKey, + pruneStaleVirtualRowElementCache, + type RenderRow +} from '../worktree-list-virtual-rows' +import { getVirtualRowKey } from './virtual-row-dom-attributes' +import { countRecordKeysByReference } from './worktree-sidebar-scroll-tuning' +import type { WorktreeListVirtualizer } from './use-worktree-list-virtualizer' + +// Re-measures only rows whose DOM node still matches its virtual key, and publishes the +// sticky-header slots the render pass reads out of refs. +export function useVirtualRowMeasurementSync(args: { + renderRows: RenderRow[] + virtualization: WorktreeListVirtualizer + scrollRef: React.RefObject + scrollOffsetRef: React.MutableRefObject + scrollAnchorRef: React.MutableRefObject + hasDirectScrollInput: () => boolean + shouldSkipScrollAnchorRestore: () => boolean +}) { + const { + renderRows, + virtualization, + scrollRef, + scrollOffsetRef, + scrollAnchorRef, + hasDirectScrollInput, + shouldSkipScrollAnchorRestore + } = args + const { virtualizer, isCurrentVirtualRowElement } = virtualization + const prCacheLen = useAppStore((s) => countRecordKeysByReference(s.prCache)) + const issueCacheLen = useAppStore((s) => countRecordKeysByReference(s.issueCache)) + const renderRowKeySignature = useMemo( + () => renderRows.map(getRenderRowKey).join('\n'), + [renderRows] + ) + const activeRenderRowKeys = useMemo(() => new Set(renderRows.map(getRenderRowKey)), [renderRows]) + const lineageRowRekeys = useMemo(() => buildLineageRowRekeyMap(renderRows), [renderRows]) + const totalSize = virtualizer.getTotalSize() + const virtualItems = virtualizer.getVirtualItems() + const activeStickyIndexes = getActiveStickyIndexesForScroll({ + rows: renderRows, + rangeStartIndex: virtualization.stickyRangeStartIndexRef.current, + scrollOffset: virtualizer.scrollOffset ?? scrollOffsetRef.current, + stickyHeaderIndexes: virtualization.stickyHeaderIndexes, + virtualItems + }) + virtualization.activeStickyHeaderIndexRef.current = activeStickyIndexes.groupIndex + virtualization.activeStickyHostIndexRef.current = activeStickyIndexes.hostIndex + + const measureMountedRows = useCallback(() => { + virtualizer.elementsCache.forEach((element) => { + if (!isCurrentVirtualRowElement(element)) { + return + } + virtualizer.measureElement(element) + }) + }, [isCurrentVirtualRowElement, virtualizer]) + const measureVirtualRowElement = useCallback( + (element: HTMLDivElement | null) => { + if (!element) { + virtualizer.measureElement(null) + return + } + if (!isCurrentVirtualRowElement(element)) { + return + } + virtualizer.measureElement(element) + }, + [isCurrentVirtualRowElement, virtualizer] + ) + + useLayoutEffect(() => { + pruneStaleVirtualRowElementCache({ + activeRowKeys: activeRenderRowKeys, + virtualizer + }) + // Why: a stale retained element after delete/collapse measures 0px and corrupts the next slot; measure only key-matched rows. + measureMountedRows() + const frameId = window.requestAnimationFrame(measureMountedRows) + return () => window.cancelAnimationFrame(frameId) + }, [ + activeRenderRowKeys, + prCacheLen, + issueCacheLen, + measureMountedRows, + renderRowKeySignature, + virtualizer + ]) + + useVirtualizedScrollAnchor({ + anchorRef: scrollAnchorRef, + getItemElementKey: getVirtualRowKey, + getRowKey: getRenderRowKey, + itemElementSelector: '[data-worktree-virtual-row]', + rekeyedRowKeys: lineageRowRekeys, + rows: renderRows, + scrollElementRef: scrollRef, + scrollOffsetRef, + hasDirectScrollInput, + shouldSkipRestore: shouldSkipScrollAnchorRestore, + totalSize, + virtualizer + }) + + return { virtualItems, measureVirtualRowElement } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-visible-pr-refresh-reporting.ts b/src/renderer/src/components/sidebar/worktree-list/use-visible-pr-refresh-reporting.ts new file mode 100644 index 00000000000..ba6bafc0b5a --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-visible-pr-refresh-reporting.ts @@ -0,0 +1,130 @@ +import { useEffect, useRef, useState } from 'react' +import type React from 'react' +import type { VirtualItem } from '@tanstack/react-virtual' +import { useAppStore } from '@/store' +import { rightSidebarShowsPullRequestData } from '@/lib/right-sidebar-visibility' +import type { Worktree } from '../../../../../shared/worktree/types' +import type { WorktreeGroupBy } from '../worktree-list-groups' +import type { RenderRow } from '../worktree-list-virtual-rows' +import type { WorktreeItemRow } from './render-row-item-rows' +import { installWorktreeVisibleRefreshVisibilityListener } from './worktree-visible-refresh-listener' + +const DOCUMENT_HIDDEN_KEY = '__document_hidden__' +const NOTHING_TO_TRACK_KEY = '__hidden__' + +// Reports which sidebar rows are on screen so the GitHub PR/CI coordinator can refresh +// exactly those, and no more. +export function useVisiblePrRefreshReporting(args: { + currentWorktreeId: string | null + worktreeMap: Map + groupBy: WorktreeGroupBy + newCardStyle: boolean + renderRows: RenderRow[] + virtualItems: readonly VirtualItem[] + scrollRef: React.RefObject +}): void { + const { + currentWorktreeId, + worktreeMap, + groupBy, + newCardStyle, + renderRows, + virtualItems, + scrollRef + } = args + const [documentVisibilityRevision, setDocumentVisibilityRevision] = useState(0) + const lastVisibleRefreshKeyRef = useRef('') + const reportVisibleGitHubPRRefreshCandidates = useAppStore( + (s) => s.reportVisibleGitHubPRRefreshCandidates + ) + const cardProps = useAppStore((s) => s.worktreeCardProperties) + const rightSidebarShowsPR = useAppStore((s) => rightSidebarShowsPullRequestData(s)) + const sshConnectedGeneration = useAppStore((s) => s.sshConnectedGeneration) + const prVisibleRefreshGeneration = useAppStore((s) => s.prVisibleRefreshGeneration) + + useEffect( + () => + installWorktreeVisibleRefreshVisibilityListener(() => { + if (document.visibilityState !== 'visible') { + // Why: row identity may be unchanged after a hidden window; reset the key so PR/CI rows refresh. + lastVisibleRefreshKeyRef.current = DOCUMENT_HIDDEN_KEY + return + } + setDocumentVisibilityRevision((revision) => revision + 1) + }), + [] + ) + + useEffect(() => { + if (document.visibilityState !== 'visible') { + lastVisibleRefreshKeyRef.current = DOCUMENT_HIDDEN_KEY + return + } + const currentWorktree = currentWorktreeId ? (worktreeMap.get(currentWorktreeId) ?? null) : null + // Why: this reporter feeds the GitHub coordinator; GitLab-only MR panels refresh via hosted-review paths. + const sidebarWorktreeHasGitHubReview = + currentWorktree !== null && + ((currentWorktree.linkedGitLabMR ?? null) === null || + (currentWorktree.linkedPR ?? null) !== null) + const shouldTrackSidebarWorktree = rightSidebarShowsPR && sidebarWorktreeHasGitHubReview + const shouldTrackVisibleRows = + groupBy === 'pr-status' || + (newCardStyle + ? cardProps.includes('status') + : cardProps.includes('pr') || cardProps.includes('ci')) + if (!shouldTrackVisibleRows && !shouldTrackSidebarWorktree) { + if (lastVisibleRefreshKeyRef.current !== NOTHING_TO_TRACK_KEY) { + lastVisibleRefreshKeyRef.current = NOTHING_TO_TRACK_KEY + reportVisibleGitHubPRRefreshCandidates([], Date.now()) + } + return + } + const scrollEl = scrollRef.current + if (!scrollEl) { + return + } + const viewportTop = scrollEl.scrollTop + const viewportBottom = viewportTop + scrollEl.clientHeight + const visibleRows = virtualItems + .filter((item) => item.start < viewportBottom && item.end > viewportTop) + .map((item) => renderRows[item.index]) + .filter((row): row is WorktreeItemRow => row?.type === 'item') + .filter((row) => row.repo?.kind === 'git' && !row.worktree.isBare && row.worktree.branch) + const visibleWorktreeIds = new Set(visibleRows.map((row) => row.worktree.id)) + if ( + shouldTrackSidebarWorktree && + currentWorktree && + !currentWorktree.isBare && + currentWorktree.branch + ) { + visibleWorktreeIds.add(currentWorktree.id) + } + const visibleIdentity = visibleRows + .map((row) => `${row.worktree.id}:${row.worktree.branch}:${row.worktree.linkedPR ?? ''}`) + .join('|') + const sidebarIdentity = + shouldTrackSidebarWorktree && currentWorktree + ? `${currentWorktree.id}:${currentWorktree.branch}:${currentWorktree.linkedPR ?? ''}` + : '' + const key = `${visibleIdentity}:${sidebarIdentity}:${sshConnectedGeneration}:${prVisibleRefreshGeneration}:${cardProps.join(',')}` + if (!key || key === lastVisibleRefreshKeyRef.current) { + return + } + lastVisibleRefreshKeyRef.current = key + reportVisibleGitHubPRRefreshCandidates(Array.from(visibleWorktreeIds), Date.now()) + }, [ + cardProps, + currentWorktreeId, + documentVisibilityRevision, + groupBy, + renderRows, + reportVisibleGitHubPRRefreshCandidates, + prVisibleRefreshGeneration, + rightSidebarShowsPR, + scrollRef, + sshConnectedGeneration, + newCardStyle, + virtualItems, + worktreeMap + ]) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-visible-sidebar-worktrees.ts b/src/renderer/src/components/sidebar/worktree-list/use-visible-sidebar-worktrees.ts new file mode 100644 index 00000000000..bd1f37fc708 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-visible-sidebar-worktrees.ts @@ -0,0 +1,134 @@ +import { useMemo } from 'react' +import { useAppStore } from '@/store' +import { getWorktreeIdsWithLiveAgent } from '@/lib/worktree-activity-state' +import type { AppState } from '@/store/types' +import type { Repo } from '../../../../../shared/repo-types' +import type { Worktree } from '../../../../../shared/worktree/types' +import type { WorktreeLineage } from '../../../../../shared/worktree/lineage-types' +import { getSettingsFocusedExecutionHostId } from '../../../../../shared/execution-host' +import { computeVisibleWorktreeIds } from '../visible-worktrees' +import { + EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT, + getPairedDeviceIdsByEnvironment +} from '../workspace-creator-visibility' +import { + getVisibleWorktreeBrowserActivityTabs, + getVisibleWorktreeTerminalActivityTabs +} from '../visible-worktree-activity-inputs' +import type { SortBy } from '../smart-sort' +import type { SidebarWorktreeFilters } from './use-sidebar-worktree-filters' +import { EMPTY_WORKTREE_ID_SET } from './stable-empty-store-slices' +import { useReusedArrayIdentity } from './use-reused-array-identity' + +// Applies every sidebar filter to the sorted id stream. Flatten/filter/sort goes through the +// shared utility so card order matches Cmd+1–9 numbering. +export function useVisibleSidebarWorktrees(args: { + filterState: SidebarWorktreeFilters['filterState'] + sortBy: SortBy + sortedIds: string[] + repoMap: Map + worktreeMap: Map + worktreeLineageById: Record + settings: AppState['settings'] + agentSendTargetWorktreeId: string | null +}) { + const { filterState, sortBy, sortedIds, repoMap, worktreeMap, worktreeLineageById, settings } = + args + const { + showSleepingWorkspaces, + filterRepoIds, + hideDefaultBranchWorkspace, + hideAutomationGeneratedWorkspaces, + hideCliCreatedWorkspaces, + hideDetachedHeadWorkspaces, + hideWorkspacesFromOtherDevices, + alwaysShowDefaultBranchWorkspace, + visibleWorkspaceHostIds, + workspaceHostScope + } = filterState + const worktreesByRepo = useAppStore((s) => s.worktreesByRepo) + const agentStatusEpoch = useAppStore((s) => (!showSleepingWorkspaces ? s.agentStatusEpoch : 0)) + const runtimeEnvironments = useAppStore((s) => s.runtimeEnvironments) + const runtimeStatusByEnvironmentId = useAppStore((s) => s.runtimeStatusByEnvironmentId) + const pairedDeviceIdsByEnvironment = useMemo( + () => + hideWorkspacesFromOtherDevices + ? getPairedDeviceIdsByEnvironment(runtimeEnvironments, runtimeStatusByEnvironmentId) + : EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT, + [hideWorkspacesFromOtherDevices, runtimeEnvironments, runtimeStatusByEnvironmentId] + ) + + // Read tabsByWorktree when needed for filtering or sorting + const needsActivityMaps = !showSleepingWorkspaces || sortBy === 'smart' + const tabsByWorktree = useAppStore((s) => + needsActivityMaps ? getVisibleWorktreeTerminalActivityTabs(s.tabsByWorktree) : null + ) + const ptyIdsByTabId = useAppStore((s) => (needsActivityMaps ? s.ptyIdsByTabId : null)) + const browserTabsByWorktree = useAppStore((s) => + !showSleepingWorkspaces ? getVisibleWorktreeBrowserActivityTabs(s.browserTabsByWorktree) : null + ) + + const recomputedVisibleWorktrees = useMemo(() => { + void agentStatusEpoch + const ids = computeVisibleWorktreeIds(worktreesByRepo, sortedIds, { + filterRepoIds, + showSleepingWorkspaces, + tabsByWorktree, + ptyIdsByTabId, + browserTabsByWorktree, + // Why snapshot on agentStatusEpoch: update membership immediately without repainting on every hook ping. + worktreeIdsWithLiveAgent: showSleepingWorkspaces + ? EMPTY_WORKTREE_ID_SET + : getWorktreeIdsWithLiveAgent( + useAppStore.getState().agentStatusByPaneKey, + tabsByWorktree, + Date.now() + ), + hideDefaultBranchWorkspace, + hideAutomationGeneratedWorkspaces, + hideCliCreatedWorkspaces, + hideDetachedHeadWorkspaces, + hideWorkspacesFromOtherDevices, + pairedDeviceIdsByEnvironment, + alwaysShowDefaultBranchWorkspace, + repoMap, + workspaceHostScope, + visibleWorkspaceHostIds, + defaultHostId: getSettingsFocusedExecutionHostId(settings), + worktreeLineageById, + forcedVisibleWorktreeIds: args.agentSendTargetWorktreeId + ? [args.agentSendTargetWorktreeId] + : undefined + }) + return ids.map((id) => worktreeMap.get(id)).filter((w): w is Worktree => w != null) + }, [ + args.agentSendTargetWorktreeId, + agentStatusEpoch, + filterRepoIds, + showSleepingWorkspaces, + hideDefaultBranchWorkspace, + hideAutomationGeneratedWorkspaces, + hideCliCreatedWorkspaces, + hideDetachedHeadWorkspaces, + hideWorkspacesFromOtherDevices, + alwaysShowDefaultBranchWorkspace, + workspaceHostScope, + visibleWorkspaceHostIds, + settings, + repoMap, + tabsByWorktree, + ptyIdsByTabId, + browserTabsByWorktree, + sortedIds, + worktreeMap, + worktreeLineageById, + worktreesByRepo, + pairedDeviceIdsByEnvironment + ]) + // Why: agentStatusEpoch bumps recompute this memo even when membership and + // order are unchanged; keeping the previous identity stops the whole + // rows/sectionRows/renderedWorktrees chain from churning per epoch. + const visibleWorktrees = useReusedArrayIdentity(recomputedVisibleWorktrees) + + return { visibleWorktrees, pairedDeviceIdsByEnvironment } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-workspace-status-row-drag.ts b/src/renderer/src/components/sidebar/worktree-list/use-workspace-status-row-drag.ts new file mode 100644 index 00000000000..cb45d99a93f --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-workspace-status-row-drag.ts @@ -0,0 +1,147 @@ +import { useCallback, useMemo } from 'react' +import type React from 'react' +import type { WorkspaceStatus } from '../../../../../shared/worktree/types' +import type { HostSectionRow } from '../host-section-rows' +import { PINNED_GROUP_KEY, type WorktreeGroupBy } from '../worktree-list-groups' +import { hasWorkspaceDragData, readWorkspaceDragDataIds } from '../workspace-status' +import { useWorkspaceStatusDocumentDrop } from '../use-workspace-status-drop' +import type { WorktreeDropCommitContext } from './worktree-drop-commit-context' +import type { WorktreeDragSession } from './use-worktree-drag-session' +import type { WorktreeDragRuntime } from './use-worktree-drag-runtime' + +// Drag-and-drop onto a status section header or a status-grouped row, including the +// document-level fallback used when the pointer leaves the sidebar mid-drag. +export function useWorkspaceStatusRowDrag(args: { + ctx: WorktreeDropCommitContext + session: WorktreeDragSession + runtime: WorktreeDragRuntime + scrollRef: React.RefObject + rows: HostSectionRow[] + groupBy: WorktreeGroupBy + onMoveWorktreeToStatus: (worktreeId: string, status: WorkspaceStatus) => void + onPinWorktree: (worktreeId: string) => void +}) { + const { ctx, session, runtime, scrollRef, rows, groupBy } = args + const { setDragOverStatus, setPinDragOver, clearWorktreeDrag } = runtime + + const hasWorkspaceDropTargets = useMemo( + () => + groupBy === 'workspace-status' || + rows.some((row) => row.type === 'header' && row.key === PINNED_GROUP_KEY), + [groupBy, rows] + ) + + const handleWorkspaceStatusDragOver = useCallback( + (event: React.DragEvent, status: WorkspaceStatus) => { + if (!hasWorkspaceDragData(event.dataTransfer)) { + return + } + event.preventDefault() + event.dataTransfer.dropEffect = 'move' + setDragOverStatus(status) + }, + [setDragOverStatus] + ) + + const handleWorkspaceStatusDragLeave = useCallback( + (event: React.DragEvent) => { + const relatedTarget = event.relatedTarget + if (relatedTarget instanceof Node && event.currentTarget.contains(relatedTarget)) { + return + } + setDragOverStatus(null) + }, + [setDragOverStatus] + ) + + const handleWorkspacePinDragOver = useCallback( + (event: React.DragEvent) => { + if (!hasWorkspaceDragData(event.dataTransfer)) { + return + } + event.preventDefault() + event.dataTransfer.dropEffect = 'move' + setPinDragOver(true) + }, + [setPinDragOver] + ) + + const handleWorkspacePinDragLeave = useCallback( + (event: React.DragEvent) => { + const relatedTarget = event.relatedTarget + if (relatedTarget instanceof Node && event.currentTarget.contains(relatedTarget)) { + return + } + setPinDragOver(false) + }, + [setPinDragOver] + ) + + const handleWorkspaceStatusDragFinish = useCallback(() => { + setDragOverStatus(null) + setPinDragOver(false) + }, [setDragOverStatus, setPinDragOver]) + + const handleWorkspaceStatusDrop = useCallback( + (event: React.DragEvent, status: WorkspaceStatus) => { + const worktreeIds = readWorkspaceDragDataIds(event.dataTransfer) + if (worktreeIds.length === 0) { + return + } + event.preventDefault() + const dragSession = session.worktreeDragSessionRef.current + const statusDrop = dragSession + ? ctx.computeWorktreeStatusDrop({ + pointerY: event.clientY, + status, + draggedIds: dragSession.reorderDraggedIds + }) + : null + setDragOverStatus(null) + if (dragSession && statusDrop) { + event.stopPropagation() + ctx.onMoveWorktreesToStatusAtIndex({ + worktreeIds: dragSession.reorderDraggedIds, + status, + dropIndex: statusDrop.dropIndex, + groups: ctx.worktreeDragGroups + }) + clearWorktreeDrag() + return + } + // Match status-drop scope to drag-preview scope (#9083): session uses its expanded set, else expand dataTransfer ids live. + ctx.onMoveWorktreesToStatus( + dragSession ? dragSession.reorderDraggedIds : session.getReorderDraggedIds(worktreeIds), + status + ) + }, + [clearWorktreeDrag, ctx, session, setDragOverStatus] + ) + + // Why: expand here (not the shared hook, used by the flat board) so a dropped parent carries its lineage children (#9083). + const moveWorktreesToStatusForDocumentDrop = useCallback( + (ids: readonly string[], status: WorkspaceStatus) => + ctx.onMoveWorktreesToStatus(session.getReorderDraggedIds(ids), status), + [ctx, session] + ) + + useWorkspaceStatusDocumentDrop( + scrollRef, + args.onMoveWorktreeToStatus, + args.onPinWorktree, + handleWorkspaceStatusDragFinish, + hasWorkspaceDropTargets, + { + onMoveWorktreesToStatus: moveWorktreesToStatusForDocumentDrop, + onPinWorktrees: ctx.onPinWorktrees + } + ) + + return { + handleWorkspaceStatusDragOver, + handleWorkspaceStatusDragLeave, + handleWorkspacePinDragOver, + handleWorkspacePinDragLeave, + handleWorkspaceStatusDrop + } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-worktree-document-drop.ts b/src/renderer/src/components/sidebar/worktree-list/use-worktree-document-drop.ts new file mode 100644 index 00000000000..7902f0647cb --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-worktree-document-drop.ts @@ -0,0 +1,115 @@ +import { useEffect } from 'react' +import type React from 'react' +import { getFullDropIndexForWorktreeDragUnit } from '../worktree-drag-units' +import type { WorktreeSidebarDragSession } from '../worktree-sidebar-drag-autoscroll' +import { getPointerDropStatusTarget } from './pointer-drop-status-target' +import type { WorktreeDropCommitContext } from './worktree-drop-commit-context' +import { NO_WORKTREE_SIDEBAR_DROP_TARGET } from './worktree-row-drag-state' + +// Why: a drop that lands outside the sidebar still belongs to the in-flight session, so the +// capture-phase document listeners commit or abandon it before anything else sees the event. +export function useWorktreeDocumentDrop(args: { + ctx: WorktreeDropCommitContext + worktreeDragSessionRef: React.MutableRefObject +}): void { + const { ctx, worktreeDragSessionRef } = args + const { clearWorktreeDrag } = ctx + + useEffect(() => { + const handleDocumentDrop = (event: DragEvent): void => { + const session = worktreeDragSessionRef.current + if (!session) { + return + } + if (!ctx.refreshWorktreeDragSession()) { + clearWorktreeDrag() + return + } + const drop = ctx.computeWorktreeDrop(event.clientY) + if (!drop) { + const container = ctx.scrollRef.current + const target = ctx.getEligibleLineageDropTarget( + container + ? getPointerDropStatusTarget({ + container, + x: event.clientX, + y: event.clientY + }) + : NO_WORKTREE_SIDEBAR_DROP_TARGET, + session.draggedIds + ) + if (target.lineageParentId) { + event.preventDefault() + event.stopPropagation() + ctx.commitWorktreeLineageParentDrop(session.draggedIds, target.lineageParentId) + clearWorktreeDrag() + return + } + const statusDrop = target.status + ? ctx.computeWorktreeStatusDrop({ + pointerY: event.clientY, + status: target.status, + draggedIds: session.reorderDraggedIds + }) + : null + if (target.status && statusDrop) { + event.preventDefault() + event.stopPropagation() + ctx.onMoveWorktreesToStatusAtIndex({ + worktreeIds: session.reorderDraggedIds, + status: target.status, + dropIndex: statusDrop.dropIndex, + groups: ctx.worktreeDragGroups + }) + clearWorktreeDrag() + return + } + clearWorktreeDrag() + return + } + // Why: pointer still inside the source group means reorder, not status move; commit here and stop the capture handler. + event.preventDefault() + event.stopPropagation() + ctx.onReorderWorktrees({ + groups: ctx.worktreeDragGroups, + sourceGroupKey: session.sourceGroupKey, + draggedIds: session.reorderDraggedIds, + dropIndex: getFullDropIndexForWorktreeDragUnit({ + groups: ctx.worktreeDragUnitGroups, + sourceGroupKey: session.sourceGroupKey, + dropIndex: drop.dropIndex + }) + }) + ctx.clearReorderedWorktreeParents({ + draggedIds: session.draggedIds, + sourceGroupKey: session.sourceGroupKey + }) + clearWorktreeDrag() + } + + document.addEventListener('drop', handleDocumentDrop, true) + return () => document.removeEventListener('drop', handleDocumentDrop, true) + }, [clearWorktreeDrag, ctx, worktreeDragSessionRef]) + + useEffect(() => { + const handleDocumentDragEnd = (): void => { + if (worktreeDragSessionRef.current) { + clearWorktreeDrag() + } + } + + document.addEventListener('dragend', handleDocumentDragEnd, true) + return () => document.removeEventListener('dragend', handleDocumentDragEnd, true) + }, [clearWorktreeDrag, worktreeDragSessionRef]) + + useEffect(() => { + const handleVisibilityChange = (): void => { + if (document.visibilityState !== 'visible' && worktreeDragSessionRef.current) { + clearWorktreeDrag() + } + } + + document.addEventListener('visibilitychange', handleVisibilityChange) + return () => document.removeEventListener('visibilitychange', handleVisibilityChange) + }, [clearWorktreeDrag, worktreeDragSessionRef]) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-worktree-drag-runtime.ts b/src/renderer/src/components/sidebar/worktree-list/use-worktree-drag-runtime.ts new file mode 100644 index 00000000000..978c31c5cc3 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-worktree-drag-runtime.ts @@ -0,0 +1,123 @@ +import { useCallback, useMemo, useRef, useState } from 'react' +import type React from 'react' +import type { WorkspaceStatus } from '../../../../../shared/worktree/types' +import { clearWorkspaceKanbanSidebarDropTargetVisual } from '../workspace-kanban-sidebar-drop' +import { setSidebarPointerDragDocumentStyles } from '../worktree-sidebar-pointer-drag-dom' +import type { + WorktreeSidebarDragPoint, + WorktreeSidebarDragSession +} from '../worktree-sidebar-drag-autoscroll' +import type { WorktreeSidebarDropAnchor } from '../worktree-sidebar-drag-geometry' +import { + WORKTREE_ROW_DRAG_INITIAL_STATE, + type WorktreePointerDrag, + type WorktreeRowDragState +} from './worktree-row-drag-state' + +export type WorktreeDragRuntime = ReturnType + +// Owns everything a drag leaves behind: the visual state the rows read, the RAF handles, +// and the single teardown both the pointer and native drag paths call. +export function useWorktreeDragRuntime(args: { + worktreeDragSessionRef: React.MutableRefObject + statusDropAnchorsRef: React.MutableRefObject> + onWorkspaceBoardDragPreviewCancel: () => void +}) { + const { worktreeDragSessionRef, statusDropAnchorsRef, onWorkspaceBoardDragPreviewCancel } = args + const [dragOverStatus, setDragOverStatus] = useState(null) + const [pinDragOver, setPinDragOver] = useState(false) + const [nativeLineageDropTargetId, setNativeLineageDropTargetId] = useState(null) + const [worktreeDragState, setWorktreeDragState] = useState( + WORKTREE_ROW_DRAG_INITIAL_STATE + ) + const worktreePointerDragRef = useRef(null) + const pointerAutoscrollFrameIdRef = useRef(null) + const pointerAutoscrollLastFrameTimeRef = useRef(null) + const nativeAutoscrollFrameIdRef = useRef(null) + const nativeAutoscrollLastFrameTimeRef = useRef(null) + const nativeLatestPointRef = useRef(null) + const suppressWorktreeClickUntilRef = useRef(0) + + const cancelWorktreePointerAutoscroll = useCallback(() => { + if (pointerAutoscrollFrameIdRef.current !== null) { + window.cancelAnimationFrame(pointerAutoscrollFrameIdRef.current) + pointerAutoscrollFrameIdRef.current = null + } + pointerAutoscrollLastFrameTimeRef.current = null + }, []) + + const cancelWorktreeNativeAutoscroll = useCallback(() => { + if (nativeAutoscrollFrameIdRef.current !== null) { + window.cancelAnimationFrame(nativeAutoscrollFrameIdRef.current) + nativeAutoscrollFrameIdRef.current = null + } + nativeAutoscrollLastFrameTimeRef.current = null + nativeLatestPointRef.current = null + }, []) + + const cleanupWorktreePointerDrag = useCallback(() => { + const drag = worktreePointerDragRef.current + cancelWorktreePointerAutoscroll() + setNativeLineageDropTargetId(null) + if (!drag) { + return + } + if (drag.frameId !== null) { + window.cancelAnimationFrame(drag.frameId) + } + drag.preview?.remove() + worktreePointerDragRef.current = null + setSidebarPointerDragDocumentStyles(false) + setDragOverStatus(null) + setPinDragOver(false) + clearWorkspaceKanbanSidebarDropTargetVisual() + onWorkspaceBoardDragPreviewCancel() + }, [cancelWorktreePointerAutoscroll, onWorkspaceBoardDragPreviewCancel]) + + const clearWorktreeDrag = useCallback(() => { + cleanupWorktreePointerDrag() + cancelWorktreeNativeAutoscroll() + worktreeDragSessionRef.current = null + statusDropAnchorsRef.current.clear() + setWorktreeDragState(WORKTREE_ROW_DRAG_INITIAL_STATE) + }, [ + cancelWorktreeNativeAutoscroll, + cleanupWorktreePointerDrag, + statusDropAnchorsRef, + worktreeDragSessionRef + ]) + + // Why: drag handlers built from this object end up on memoised cards; keep one identity + // per meaningful change instead of one per render. + return useMemo( + () => ({ + dragOverStatus, + setDragOverStatus, + pinDragOver, + setPinDragOver, + nativeLineageDropTargetId, + setNativeLineageDropTargetId, + worktreeDragState, + setWorktreeDragState, + worktreePointerDragRef, + pointerAutoscrollFrameIdRef, + pointerAutoscrollLastFrameTimeRef, + nativeAutoscrollFrameIdRef, + nativeAutoscrollLastFrameTimeRef, + nativeLatestPointRef, + suppressWorktreeClickUntilRef, + cancelWorktreePointerAutoscroll, + cancelWorktreeNativeAutoscroll, + clearWorktreeDrag + }), + [ + cancelWorktreeNativeAutoscroll, + cancelWorktreePointerAutoscroll, + clearWorktreeDrag, + dragOverStatus, + nativeLineageDropTargetId, + pinDragOver, + worktreeDragState + ] + ) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-worktree-drag-session.ts b/src/renderer/src/components/sidebar/worktree-list/use-worktree-drag-session.ts new file mode 100644 index 00000000000..f1077c71ff0 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-worktree-drag-session.ts @@ -0,0 +1,244 @@ +import { useCallback, useMemo, useRef } from 'react' +import type React from 'react' +import type { WorkspaceStatus } from '../../../../../shared/worktree/types' +import type { HostSectionRow } from '../host-section-rows' +import { PINNED_GROUP_KEY } from '../worktree-list-groups' +import { WORKTREE_SIDEBAR_VIRTUAL_ROW_GAP } from '../worktree-list-virtual-rows' +import { getWorkspaceStatusGroupKey } from '../workspace-status' +import { expandDraggedWorktreeIdsForVisibleLineage } from '../worktree-manual-order' +import { getWorktreeDragUnitGroups } from '../worktree-drag-units' +import { + getWorktreeSidebarDragRectsForGroup, + refreshWorktreeSidebarDragSession, + type WorktreeSidebarDragRect, + type WorktreeSidebarDragSession +} from '../worktree-sidebar-drag-autoscroll' +import { + shouldReevaluateWorktreeSidebarDropAnchor, + type WorktreeSidebarDragGrab, + type WorktreeSidebarDropAnchor +} from '../worktree-sidebar-drag-geometry' +import { + computeWorktreeSidebarDropPreview, + type WorktreeSidebarDropPreview +} from '../worktree-sidebar-drop-preview' +import { getWorktreeDragGroups, getWorktreeDragIndexes } from './worktree-drag-groups' +import type { WorktreeItemRow } from './render-row-item-rows' + +export type WorktreeStatusDropRequest = { + pointerY: number + status: WorkspaceStatus + draggedIds: readonly string[] +} + +export type WorktreeDragSession = ReturnType + +// Owns the geometry side of a sidebar row drag: which groups exist, which ids travel +// together, and where the insertion line lands for a given pointer position. +export function useWorktreeDragSession(args: { + rows: HostSectionRow[] + scrollRef: React.RefObject +}) { + const { rows, scrollRef } = args + const worktreeDragSessionRef = useRef(null) + // Why: cross-group hovers hit-test a group the session never captured, so hold + // that group's drop decision separately or a card expanding in the target group + // moves the insertion line under a still pointer. + const statusDropAnchorsRef = useRef>(new Map()) + + const worktreeDragGroups = useMemo(() => getWorktreeDragGroups(rows), [rows]) + const worktreeDragUnitGroups = useMemo(() => getWorktreeDragUnitGroups(rows), [rows]) + const naturalDragWorktreeIds = useMemo( + () => + new Set( + rows.flatMap((row) => + row.type === 'item' && row.sectionKey !== PINNED_GROUP_KEY ? [row.worktree.id] : [] + ) + ), + [rows] + ) + const worktreeLineageDragRows = useMemo( + () => + rows + .filter((row): row is WorktreeItemRow => row.type === 'item') + .filter( + (row) => + row.sectionKey !== PINNED_GROUP_KEY || !naturalDragWorktreeIds.has(row.worktree.id) + ) + .map((row) => ({ worktreeId: row.worktree.id, depth: row.depth })), + [naturalDragWorktreeIds, rows] + ) + const getReorderDraggedIds = useCallback( + (draggedIds: readonly string[]) => + expandDraggedWorktreeIdsForVisibleLineage(worktreeLineageDragRows, draggedIds), + [worktreeLineageDragRows] + ) + const getReorderUnitDraggedIds = useCallback( + (sourceGroupKey: string, reorderDraggedIds: readonly string[]) => { + const group = worktreeDragUnitGroups.find((candidate) => candidate.key === sourceGroupKey) + if (!group) { + return reorderDraggedIds + } + const unitIds = new Set(group.worktreeIds) + const filtered = reorderDraggedIds.filter((worktreeId) => unitIds.has(worktreeId)) + return filtered.length > 0 ? filtered : reorderDraggedIds + }, + [worktreeDragUnitGroups] + ) + const { groupKeyByRowKey, groupIndexByRowKey } = useMemo( + () => getWorktreeDragIndexes(rows), + [rows] + ) + const refreshWorktreeDragSession = useCallback((): boolean => { + const session = worktreeDragSessionRef.current + const container = scrollRef.current + if (!session || !container) { + return false + } + + const refreshedSession = refreshWorktreeSidebarDragSession({ + session, + groups: worktreeDragGroups, + unitGroups: worktreeDragUnitGroups, + rects: getWorktreeSidebarDragRectsForGroup(container, session.sourceGroupKey) + }) + worktreeDragSessionRef.current = refreshedSession + return refreshedSession !== null + }, [scrollRef, worktreeDragGroups, worktreeDragUnitGroups]) + const computeWorktreeDropForGroup = useCallback( + (dropArgs: { + pointerY: number + groupKey: string + rects: readonly WorktreeSidebarDragRect[] + draggedIds: readonly string[] + draggingWorktreeId?: string | null + grab?: WorktreeSidebarDragGrab | null + anchor?: WorktreeSidebarDropAnchor | null + }): WorktreeSidebarDropPreview | null => { + const container = scrollRef.current + if (!container) { + return null + } + const group = worktreeDragUnitGroups.find((candidate) => candidate.key === dropArgs.groupKey) + if (!group) { + return null + } + const containerRect = container.getBoundingClientRect() + return computeWorktreeSidebarDropPreview({ + pointerY: dropArgs.pointerY, + containerTop: containerRect.top, + scrollTop: container.scrollTop, + rects: dropArgs.rects, + groupIds: group.worktreeIds, + draggedIds: dropArgs.draggedIds, + draggingWorktreeId: dropArgs.draggingWorktreeId, + fallbackGap: WORKTREE_SIDEBAR_VIRTUAL_ROW_GAP, + grab: dropArgs.grab, + anchor: dropArgs.anchor + }) + }, + [scrollRef, worktreeDragUnitGroups] + ) + const computeWorktreeDrop = useCallback( + (pointerY: number): WorktreeSidebarDropPreview | null => { + const session = worktreeDragSessionRef.current + const container = scrollRef.current + if (!session || !container) { + return null + } + const scrollTop = container.scrollTop + // Why: only real pointer or scroll movement should re-decide the slot; a + // card growing under a still pointer must not move it. + const anchor = shouldReevaluateWorktreeSidebarDropAnchor({ + anchor: session.anchor, + pointerY, + scrollTop + }) + ? null + : session.anchor + const preview = computeWorktreeDropForGroup({ + pointerY, + groupKey: session.sourceGroupKey, + rects: session.rects, + draggedIds: session.reorderUnitDraggedIds, + draggingWorktreeId: session.draggingWorktreeId, + grab: session.grab, + anchor + }) + worktreeDragSessionRef.current = { + ...session, + anchor: preview ? { beforeWorktreeId: preview.dropAnchorId, pointerY, scrollTop } : null + } + return preview + }, + [computeWorktreeDropForGroup, scrollRef] + ) + const computeWorktreeStatusDrop = useCallback( + (request: WorktreeStatusDropRequest): WorktreeSidebarDropPreview | null => { + const container = scrollRef.current + if (!container) { + return null + } + const groupKey = getWorkspaceStatusGroupKey(request.status) + const session = worktreeDragSessionRef.current + const scrollTop = container.scrollTop + const heldAnchor = statusDropAnchorsRef.current.get(groupKey) ?? null + const anchor = shouldReevaluateWorktreeSidebarDropAnchor({ + anchor: heldAnchor, + pointerY: request.pointerY, + scrollTop + }) + ? null + : heldAnchor + const preview = computeWorktreeDropForGroup({ + pointerY: request.pointerY, + groupKey, + rects: getWorktreeSidebarDragRectsForGroup(container, groupKey), + draggedIds: request.draggedIds, + draggingWorktreeId: session?.draggingWorktreeId ?? null, + grab: session?.grab ?? null, + anchor + }) + if (preview) { + statusDropAnchorsRef.current.set(groupKey, { + beforeWorktreeId: preview.dropAnchorId, + pointerY: request.pointerY, + scrollTop + }) + } else { + statusDropAnchorsRef.current.delete(groupKey) + } + return preview + }, + [computeWorktreeDropForGroup, scrollRef] + ) + + // Why: consumers pass session-derived callbacks into memoised cards; a fresh object every + // render would defeat those bail-outs. + return useMemo( + () => ({ + worktreeDragSessionRef, + statusDropAnchorsRef, + worktreeDragGroups, + worktreeDragUnitGroups, + groupKeyByRowKey, + groupIndexByRowKey, + getReorderDraggedIds, + getReorderUnitDraggedIds, + refreshWorktreeDragSession, + computeWorktreeDrop, + computeWorktreeStatusDrop + }), + [ + computeWorktreeDrop, + computeWorktreeStatusDrop, + getReorderDraggedIds, + getReorderUnitDraggedIds, + groupIndexByRowKey, + groupKeyByRowKey, + refreshWorktreeDragSession, + worktreeDragGroups, + worktreeDragUnitGroups + ] + ) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-worktree-lineage-drop-commit.ts b/src/renderer/src/components/sidebar/worktree-list/use-worktree-lineage-drop-commit.ts new file mode 100644 index 00000000000..6629fe526e1 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-worktree-lineage-drop-commit.ts @@ -0,0 +1,127 @@ +import { useCallback, useMemo } from 'react' +import { toast } from 'sonner' +import { useAppStore } from '@/store' +import { translate } from '@/i18n/i18n' +import type { Repo } from '../../../../../shared/repo-types' +import type { Worktree } from '../../../../../shared/worktree/types' +import type { WorktreeLineage } from '../../../../../shared/worktree/lineage-types' +import { isEligibleWorktreeParent } from '../worktree-parent-candidates' +import { getCyclicProjectedWorktreeLineageIds } from '../worktree-lineage-projection' +import { getReorderedWorktreeIdsToUnnest } from '../worktree-lineage-drag-drop' +import type { WorktreeDragGroup } from '../worktree-manual-order' +import type { WorktreeSidebarLineageDropTarget } from './worktree-row-drag-state' + +export type WorktreeLineageDropCommit = ReturnType + +// Nesting and un-nesting are the two lineage mutations a sidebar drag can commit. +export function useWorktreeLineageDropCommit(args: { + repoMap: Map + worktreeMap: Map + worktreeLineageById: Record + worktreeDragGroups: readonly WorktreeDragGroup[] +}) { + const { repoMap, worktreeMap, worktreeLineageById, worktreeDragGroups } = args + const assignWorktreeParent = useAppStore((s) => s.assignWorktreeParent) + const updateWorktreeLineage = useAppStore((s) => s.updateWorktreeLineage) + const cyclicLineageIds = useMemo( + () => getCyclicProjectedWorktreeLineageIds(worktreeLineageById, worktreeMap), + [worktreeLineageById, worktreeMap] + ) + + const getEligibleLineageDropTarget = useCallback( + ( + target: WorktreeSidebarLineageDropTarget, + draggedIds: readonly string[] + ): WorktreeSidebarLineageDropTarget => { + const parentId = target.lineageParentId + if (!parentId) { + return target + } + const canAssignAll = draggedIds.every((draggedId) => { + const child = worktreeMap.get(draggedId) + if (!child) { + return false + } + const candidateParent = worktreeMap.get(parentId) + return Boolean( + candidateParent && + isEligibleWorktreeParent({ + child, + candidateParent, + lineageById: worktreeLineageById, + worktreeMap, + repoMap, + cyclicLineageIds + }) + ) + }) + return canAssignAll ? target : { ...target, lineageParentId: null } + }, + [cyclicLineageIds, repoMap, worktreeLineageById, worktreeMap] + ) + + const commitWorktreeLineageParentDrop = useCallback( + (draggedIds: readonly string[], parentId: string): boolean => { + const target = getEligibleLineageDropTarget( + { status: null, isPinDrop: false, lineageParentId: parentId }, + draggedIds + ) + if (!target.lineageParentId) { + return false + } + void Promise.all( + draggedIds.map((id) => assignWorktreeParent(id, { parentWorktreeId: parentId })) + ).catch((err) => { + console.error('Failed to nest workspace:', err) + toast.error( + translate( + 'auto.components.sidebar.WorktreeList.failedNestWorkspace', + 'Failed to nest workspace' + ) + ) + }) + return true + }, + [assignWorktreeParent, getEligibleLineageDropTarget] + ) + + const clearReorderedWorktreeParents = useCallback( + (unnestArgs: { draggedIds: readonly string[]; sourceGroupKey: string }) => { + const sourceGroup = worktreeDragGroups.find( + (group) => group.key === unnestArgs.sourceGroupKey + ) + if (!sourceGroup) { + return + } + const ids = getReorderedWorktreeIdsToUnnest({ + draggedIds: unnestArgs.draggedIds, + sourceGroupIds: sourceGroup.worktreeIds, + lineageById: worktreeLineageById, + worktreeMap, + cyclicLineageIds + }) + if (ids.length === 0) { + return + } + // Why: dropping a nested card on a reorder line is the un-nest escape hatch; clear only the dragged children. + void Promise.all(ids.map((id) => updateWorktreeLineage(id, { noParent: true }))).catch( + (err) => { + console.error('Failed to unnest workspace:', err) + toast.error( + translate( + 'auto.components.sidebar.WorktreeList.failedUnnestWorkspace', + 'Failed to unnest workspace' + ) + ) + } + ) + }, + [cyclicLineageIds, updateWorktreeLineage, worktreeDragGroups, worktreeLineageById, worktreeMap] + ) + + return { + getEligibleLineageDropTarget, + commitWorktreeLineageParentDrop, + clearReorderedWorktreeParents + } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-worktree-list-keyboard-navigation.ts b/src/renderer/src/components/sidebar/worktree-list/use-worktree-list-keyboard-navigation.ts new file mode 100644 index 00000000000..4a9fc419703 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-worktree-list-keyboard-navigation.ts @@ -0,0 +1,120 @@ +import { useCallback, useEffect } from 'react' +import type React from 'react' +import type { Virtualizer } from '@tanstack/react-virtual' +import { useAppStore } from '@/store' +import { activateAndRevealWorktree } from '@/lib/worktree-activation' +import { getShortcutPlatform } from '@/lib/shortcut-platform' +import { keybindingMatchesAction } from '../../../../../shared/keybindings' +import type { HostSectionRow } from '../host-section-rows' +import type { PinnedWorktreeDisplayPolicy } from '../worktree-list-groups' +import type { RenderRow } from '../worktree-list-virtual-rows' +import { getCyclableWorktreeIds, resolveCycledWorktreeId } from '../worktree-keyboard-cycle' +import { findPreferredRenderRowIndexForWorktree } from './render-row-worktree-lookup' +import { isEditableTarget } from './sidebar-editable-target' + +export function useWorktreeListKeyboardNavigation(args: { + rows: HostSectionRow[] + renderRows: RenderRow[] + activeWorktreeId: string | null + pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy + virtualizer: Virtualizer + scrollRef: React.RefObject + activeModal: string + markDirectScrollInput: () => void +}) { + const { + rows, + renderRows, + activeWorktreeId, + pinnedDisplayPolicy, + virtualizer, + scrollRef, + activeModal, + markDirectScrollInput + } = args + const keybindings = useAppStore((s) => s.keybindings) + + const navigateWorktree = useCallback( + (direction: 'up' | 'down') => { + // Why: cycle over the rows the sidebar actually rendered — collapsing a group + // means "not now", and a rebuilt near-copy would drift from what is on screen + // (host sections, pinned placement, folder workspaces). + const nextWorktreeId = resolveCycledWorktreeId({ + worktreeIds: getCyclableWorktreeIds(rows, pinnedDisplayPolicy), + activeWorktreeId, + direction + }) + if (nextWorktreeId === null) { + return + } + + // Why: keyboard cycling is real navigation; route through the activation helper that records history. + activateAndRevealWorktree(nextWorktreeId) + + const rowIndex = findPreferredRenderRowIndexForWorktree( + renderRows, + nextWorktreeId, + pinnedDisplayPolicy + ) + if (rowIndex !== -1) { + virtualizer.scrollToIndex(rowIndex, { align: 'auto' }) + } + }, + [rows, renderRows, activeWorktreeId, virtualizer, pinnedDisplayPolicy] + ) + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (activeModal !== 'none' || isEditableTarget(e.target)) { + return + } + + const platform = getShortcutPlatform() + if (keybindingMatchesAction('sidebar.focusWorktreeList', e, platform, keybindings)) { + scrollRef.current?.focus() + e.preventDefault() + return + } + + const direction = keybindingMatchesAction('worktree.navigateUp', e, platform, keybindings) + ? 'up' + : keybindingMatchesAction('worktree.navigateDown', e, platform, keybindings) + ? 'down' + : null + if (direction) { + markDirectScrollInput() + navigateWorktree(direction) + e.preventDefault() + } + } + + window.addEventListener('keydown', handleKeyDown, { capture: true }) + return () => window.removeEventListener('keydown', handleKeyDown, { capture: true }) + }, [activeModal, keybindings, markDirectScrollInput, navigateWorktree, scrollRef]) + + const handleContainerKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { + if (e.target !== e.currentTarget) { + return + } + markDirectScrollInput() + navigateWorktree(e.key === 'ArrowUp' ? 'up' : 'down') + e.preventDefault() + } else if (e.key === 'Enter') { + const helper = document.querySelector( + '.xterm-helper-textarea' + ) as HTMLTextAreaElement | null + if (helper) { + helper.focus() + } + e.preventDefault() + } else if (['PageUp', 'PageDown', 'Home', 'End', ' '].includes(e.key)) { + markDirectScrollInput() + } + }, + [markDirectScrollInput, navigateWorktree] + ) + + return { handleContainerKeyDown } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-worktree-list-virtualizer.ts b/src/renderer/src/components/sidebar/worktree-list/use-worktree-list-virtualizer.ts new file mode 100644 index 00000000000..52bd92358ce --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-worktree-list-virtualizer.ts @@ -0,0 +1,159 @@ +import { useCallback, useMemo, useRef } from 'react' +import type React from 'react' +import { + measureElement as measureVirtualElementSize, + useVirtualizer +} from '@tanstack/react-virtual' +import type { Range } from '@tanstack/react-virtual' +import { + estimateRenderRowSize, + extractWorktreeVirtualRowIndexes, + getRenderRowKey, + getStickyHeaderIndexes, + WORKTREE_SIDEBAR_VIRTUAL_ROW_GAP, + type RenderRow +} from '../worktree-list-virtual-rows' +import { WORKTREE_SIDEBAR_REVEAL_TOP_INSET } from '../worktree-sidebar-reveal' +import { getVirtualRowIndex } from './virtual-row-dom-attributes' +import { + shouldAdjustWorktreeSidebarMeasuredRowScroll, + USER_SCROLL_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS +} from './worktree-sidebar-scroll-tuning' + +export type WorktreeListVirtualizer = ReturnType + +// The TanStack virtualizer plus the row-identity guards that stop a recycled DOM node +// from writing a stale height into the wrong slot. +export function useWorktreeListVirtualizer(args: { + renderRows: RenderRow[] + firstHeaderIndex: number + scrollRef: React.RefObject + scrollOffsetRef: React.MutableRefObject + suppressMeasurementAdjustmentUntilRef: React.MutableRefObject +}) { + const { renderRows, firstHeaderIndex, scrollRef, scrollOffsetRef } = args + const renderRowsRef = useRef(renderRows) + renderRowsRef.current = renderRows + const firstHeaderIndexRef = useRef(firstHeaderIndex) + firstHeaderIndexRef.current = firstHeaderIndex + const stickyHeaderIndexes = useMemo(() => getStickyHeaderIndexes(renderRows), [renderRows]) + const activeStickyHeaderIndexRef = useRef(null) + const activeStickyHostIndexRef = useRef(null) + const stickyRangeStartIndexRef = useRef(0) + + const getVirtualItemKey = useCallback( + (index: number) => { + const row = renderRows[index] + if (!row) { + return `__stale_${index}` + } + return getRenderRowKey(row) + }, + [renderRows] + ) + const getExpectedVirtualRowKey = useCallback((element: Element) => { + const index = getVirtualRowIndex(element) + const row = index === null ? undefined : renderRowsRef.current[index] + return row ? getRenderRowKey(row) : null + }, []) + const isCurrentVirtualRowElement = useCallback( + (element: Element) => { + const expectedKey = getExpectedVirtualRowKey(element) + return ( + element.isConnected && + expectedKey !== null && + element.getAttribute('data-worktree-virtual-row-key') === expectedKey + ) + }, + [getExpectedVirtualRowKey] + ) + const measureCurrentVirtualRowElement = useCallback( + ( + element: HTMLDivElement, + entry: ResizeObserverEntry | undefined, + instance: Parameters>[2] + ) => { + if (!isCurrentVirtualRowElement(element)) { + const index = getVirtualRowIndex(element) + const measured = instance.getVirtualItems().find((item) => item.index === index) + // Why: a stale ResizeObserver row after remount would write a wrong height; return current size to no-op it. + return ( + measured?.size ?? + estimateRenderRowSize( + renderRowsRef.current, + index ?? -1, + firstHeaderIndexRef.current, + activeStickyHeaderIndexRef.current + ) + ) + } + const index = getVirtualRowIndex(element) + if ( + index !== null && + (renderRowsRef.current[index]?.type === 'header' || + renderRowsRef.current[index]?.type === 'host-header') + ) { + return estimateRenderRowSize( + renderRowsRef.current, + index, + firstHeaderIndexRef.current, + activeStickyHeaderIndexRef.current + ) + } + return measureVirtualElementSize(element, entry, instance) + }, + [isCurrentVirtualRowElement] + ) + + const virtualizer = useVirtualizer({ + count: renderRows.length, + getScrollElement: () => scrollRef.current, + estimateSize: (index) => + estimateRenderRowSize( + renderRows, + index, + firstHeaderIndex, + activeStickyHeaderIndexRef.current + ), + measureElement: measureCurrentVirtualRowElement, + // Why: TanStack memoizes rangeExtractor by identity; header indexes must be deps or sticky slots go stale. + rangeExtractor: useCallback( + (range: Range) => { + stickyRangeStartIndexRef.current = range.startIndex + return extractWorktreeVirtualRowIndexes({ + range, + stickyHeaderIndexes, + rows: renderRowsRef.current + }) + }, + [stickyHeaderIndexes] + ), + overscan: 10, + gap: WORKTREE_SIDEBAR_VIRTUAL_ROW_GAP, + // Why: the sticky group header lives inside the virtual list, so scroll math needs the same top inset as the DOM reveal. + scrollPaddingStart: WORKTREE_SIDEBAR_REVEAL_TOP_INSET, + isScrollingResetDelay: USER_SCROLL_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS, + // Why: sync-flushing rich card renders in the scroll listener stalls wheel input; async + overscan keeps rows filled. + useFlushSync: false, + // Why: seed scrollOffset from the ref (not 0) so the first getVirtualItems() after remount picks the right rows. + initialOffset: () => scrollOffsetRef.current, + getItemKey: getVirtualItemKey + }) + // Why: TanStack's default correction writes scrollTop while cards remeasure mid-wheel, which feels like rubber-banding. + // TODO(scroll-origin-migration): wall-clock suppression misclassifies under jank; migrate to programmaticScrollMarks + restoreSignal (see CombinedDiffViewer). + virtualizer.shouldAdjustScrollPositionOnItemSizeChange = (_item, _delta, instance) => + shouldAdjustWorktreeSidebarMeasuredRowScroll({ + isScrolling: instance.isScrolling, + now: window.performance.now(), + suppressUntil: args.suppressMeasurementAdjustmentUntilRef.current + }) + + return { + virtualizer, + isCurrentVirtualRowElement, + stickyHeaderIndexes, + activeStickyHeaderIndexRef, + activeStickyHostIndexRef, + stickyRangeStartIndexRef + } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-worktree-native-drag-autoscroll.ts b/src/renderer/src/components/sidebar/worktree-list/use-worktree-native-drag-autoscroll.ts new file mode 100644 index 00000000000..339f1f8b768 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-worktree-native-drag-autoscroll.ts @@ -0,0 +1,108 @@ +import { useCallback } from 'react' +import type React from 'react' +import { getWorktreeSidebarDragAutoscroll } from '../worktree-sidebar-drag-autoscroll' +import { getPointerDropStatusTarget } from './pointer-drop-status-target' +import type { WorktreeDropCommitContext } from './worktree-drop-commit-context' +import type { WorktreeDragRuntime } from './use-worktree-drag-runtime' +import type { WorktreeDragSession } from './use-worktree-drag-session' +import { applyWorktreeDropPreview, clearWorktreeDropPreview } from './worktree-row-drag-state' + +// The HTML5 drag equivalent of pointer autoscroll: the browser gives no move events while +// hovering the edge, so re-derive the drop preview from the last known point each frame. +export function useWorktreeNativeDragAutoscroll(args: { + ctx: WorktreeDropCommitContext + session: WorktreeDragSession + runtime: WorktreeDragRuntime + scrollRef: React.RefObject + markScrollMovement: () => void +}): () => void { + const { ctx, session, runtime, scrollRef, markScrollMovement } = args + const { + nativeAutoscrollFrameIdRef, + nativeAutoscrollLastFrameTimeRef, + nativeLatestPointRef, + cancelWorktreeNativeAutoscroll, + clearWorktreeDrag, + setWorktreeDragState + } = runtime + + const runFrame = useCallback( + (frameTime: number) => { + nativeAutoscrollFrameIdRef.current = null + const point = nativeLatestPointRef.current + const container = scrollRef.current + const dragSession = session.worktreeDragSessionRef.current + if (!point || !container || !dragSession) { + cancelWorktreeNativeAutoscroll() + return + } + + const previousFrameTime = nativeAutoscrollLastFrameTimeRef.current ?? frameTime + nativeAutoscrollLastFrameTimeRef.current = frameTime + const autoscroll = getWorktreeSidebarDragAutoscroll({ + point, + containerRect: container.getBoundingClientRect(), + scrollTop: container.scrollTop, + scrollHeight: container.scrollHeight, + clientHeight: container.clientHeight, + elapsedMs: frameTime - previousFrameTime + }) + if (autoscroll) { + markScrollMovement() + container.scrollTop = autoscroll.scrollTop + if (!session.refreshWorktreeDragSession()) { + clearWorktreeDrag() + return + } + const drop = ctx.computeWorktreeDrop(point.clientY) + if (!drop) { + const target = getPointerDropStatusTarget({ + container, + x: point.clientX, + y: point.clientY + }) + const statusDrop = target.status + ? ctx.computeWorktreeStatusDrop({ + pointerY: point.clientY, + status: target.status, + draggedIds: dragSession.reorderDraggedIds + }) + : null + if (statusDrop) { + setWorktreeDragState((prev) => + applyWorktreeDropPreview(prev, statusDrop, { pointerY: point.clientY }) + ) + return + } + setWorktreeDragState((prev) => clearWorktreeDropPreview(prev, { pointerY: null })) + } else { + setWorktreeDragState((prev) => + applyWorktreeDropPreview(prev, drop, { pointerY: point.clientY }) + ) + } + } + + nativeAutoscrollFrameIdRef.current = window.requestAnimationFrame(runFrame) + }, + [ + cancelWorktreeNativeAutoscroll, + clearWorktreeDrag, + ctx, + markScrollMovement, + nativeAutoscrollFrameIdRef, + nativeAutoscrollLastFrameTimeRef, + nativeLatestPointRef, + scrollRef, + session, + setWorktreeDragState + ] + ) + + return useCallback(() => { + if (nativeAutoscrollFrameIdRef.current !== null) { + return + } + nativeAutoscrollLastFrameTimeRef.current = null + nativeAutoscrollFrameIdRef.current = window.requestAnimationFrame(runFrame) + }, [nativeAutoscrollFrameIdRef, nativeAutoscrollLastFrameTimeRef, runFrame]) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-worktree-native-drag.ts b/src/renderer/src/components/sidebar/worktree-list/use-worktree-native-drag.ts new file mode 100644 index 00000000000..b7fc693326e --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-worktree-native-drag.ts @@ -0,0 +1,231 @@ +import { useCallback } from 'react' +import type React from 'react' +import { getWorkspaceKanbanSidebarDropTarget } from '../workspace-kanban-sidebar-drop' +import { getFullDropIndexForWorktreeDragUnit } from '../worktree-drag-units' +import { getWorktreeSidebarDragRectsForGroup } from '../worktree-sidebar-drag-autoscroll' +import { getWorktreeSidebarDragGrab } from '../worktree-sidebar-drag-geometry' +import { getPointerDropStatusTarget } from './pointer-drop-status-target' +import type { WorktreeDropCommitContext } from './worktree-drop-commit-context' +import type { WorktreeDragRuntime } from './use-worktree-drag-runtime' +import type { WorktreeDragSession } from './use-worktree-drag-session' +import { useWorktreeNativeDragAutoscroll } from './use-worktree-native-drag-autoscroll' +import { + applyWorktreeDropPreview, + clearWorktreeDropPreview, + EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS, + NO_WORKTREE_SIDEBAR_DROP_TARGET +} from './worktree-row-drag-state' + +// The HTML5 drag path used by cards that start a native dragstart (board <-> sidebar transfers). +export function useWorktreeNativeDrag(args: { + ctx: WorktreeDropCommitContext + session: WorktreeDragSession + runtime: WorktreeDragRuntime + scrollRef: React.RefObject + markScrollMovement: () => void +}) { + const { ctx, session, runtime, scrollRef, markScrollMovement } = args + const { + nativeLatestPointRef, + clearWorktreeDrag, + setWorktreeDragState, + setNativeLineageDropTargetId + } = runtime + + const startWorktreeNativeAutoscroll = useWorktreeNativeDragAutoscroll({ + ctx, + session, + runtime, + scrollRef, + markScrollMovement + }) + + const handleWorktreeCardDragStart = useCallback( + (event: React.DragEvent, worktreeId: string, draggedIds: readonly string[]) => { + const sourceGroupKey = + ctx.worktreeDragGroups.find((group) => group.worktreeIds.includes(worktreeId))?.key ?? null + if (!sourceGroupKey) { + return + } + const reorderDraggedIds = session.getReorderDraggedIds(draggedIds) + const reorderUnitDraggedIds = session.getReorderUnitDraggedIds( + sourceGroupKey, + reorderDraggedIds + ) + const rects = scrollRef.current + ? getWorktreeSidebarDragRectsForGroup(scrollRef.current, sourceGroupKey) + : [] + const sourceRect = event.currentTarget.getBoundingClientRect() + session.worktreeDragSessionRef.current = { + draggingWorktreeId: worktreeId, + sourceGroupKey, + draggedIds, + reorderDraggedIds, + reorderUnitDraggedIds, + rects, + grab: getWorktreeSidebarDragGrab({ + offsetY: event.clientY - sourceRect.top, + height: sourceRect.height + }), + anchor: null + } + setWorktreeDragState({ + draggingWorktreeId: worktreeId, + sourceGroupKey, + dropIndex: null, + dropIndicatorY: null, + previewOffsetsByWorktreeId: EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS, + pointerY: null + }) + }, + [ctx.worktreeDragGroups, scrollRef, session, setWorktreeDragState] + ) + + const handleWorktreeDragOver = useCallback( + (event: React.DragEvent) => { + const dragSession = session.worktreeDragSessionRef.current + if (!dragSession) { + return + } + nativeLatestPointRef.current = { clientX: event.clientX, clientY: event.clientY } + startWorktreeNativeAutoscroll() + if (!session.refreshWorktreeDragSession()) { + clearWorktreeDrag() + return + } + const target = ctx.getEligibleLineageDropTarget( + getPointerDropStatusTarget({ + container: event.currentTarget, + x: event.clientX, + y: event.clientY + }), + dragSession.draggedIds + ) + if (target.lineageParentId) { + event.preventDefault() + event.dataTransfer.dropEffect = 'move' + setNativeLineageDropTargetId(target.lineageParentId) + setWorktreeDragState((prev) => clearWorktreeDropPreview(prev, { pointerY: event.clientY })) + return + } + setNativeLineageDropTargetId(null) + + const drop = ctx.computeWorktreeDrop(event.clientY) + if (!drop) { + const statusDrop = target.status + ? ctx.computeWorktreeStatusDrop({ + pointerY: event.clientY, + status: target.status, + draggedIds: dragSession.reorderDraggedIds + }) + : null + if (statusDrop) { + event.preventDefault() + event.dataTransfer.dropEffect = 'move' + setWorktreeDragState((prev) => + applyWorktreeDropPreview(prev, statusDrop, { pointerY: event.clientY }) + ) + return + } + setWorktreeDragState((prev) => clearWorktreeDropPreview(prev, { pointerY: null })) + return + } + event.preventDefault() + event.dataTransfer.dropEffect = 'move' + setWorktreeDragState((prev) => + applyWorktreeDropPreview(prev, drop, { pointerY: event.clientY }) + ) + }, + [ + clearWorktreeDrag, + ctx, + nativeLatestPointRef, + session, + setNativeLineageDropTargetId, + setWorktreeDragState, + startWorktreeNativeAutoscroll + ] + ) + + const handleWorktreeDrop = useCallback( + (event: React.DragEvent) => { + const dragSession = session.worktreeDragSessionRef.current + if (!dragSession) { + return + } + if (!session.refreshWorktreeDragSession()) { + clearWorktreeDrag() + return + } + const boardDropTarget = getWorkspaceKanbanSidebarDropTarget(event.clientX, event.clientY) + if (boardDropTarget.status || boardDropTarget.isPinDrop) { + clearWorktreeDrag() + return + } + + const container = scrollRef.current + const target = ctx.getEligibleLineageDropTarget( + container + ? getPointerDropStatusTarget({ + container, + x: event.clientX, + y: event.clientY + }) + : NO_WORKTREE_SIDEBAR_DROP_TARGET, + dragSession.draggedIds + ) + + if (target.lineageParentId) { + event.preventDefault() + event.stopPropagation() + ctx.commitWorktreeLineageParentDrop(dragSession.draggedIds, target.lineageParentId) + clearWorktreeDrag() + return + } + + const drop = ctx.computeWorktreeDrop(event.clientY) + if (!drop) { + const statusDrop = target.status + ? ctx.computeWorktreeStatusDrop({ + pointerY: event.clientY, + status: target.status, + draggedIds: dragSession.reorderDraggedIds + }) + : null + if (target.status && statusDrop) { + event.preventDefault() + event.stopPropagation() + ctx.onMoveWorktreesToStatusAtIndex({ + worktreeIds: dragSession.reorderDraggedIds, + status: target.status, + dropIndex: statusDrop.dropIndex, + groups: ctx.worktreeDragGroups + }) + clearWorktreeDrag() + return + } + clearWorktreeDrag() + return + } + event.preventDefault() + ctx.onReorderWorktrees({ + groups: ctx.worktreeDragGroups, + sourceGroupKey: dragSession.sourceGroupKey, + draggedIds: dragSession.reorderDraggedIds, + dropIndex: getFullDropIndexForWorktreeDragUnit({ + groups: ctx.worktreeDragUnitGroups, + sourceGroupKey: dragSession.sourceGroupKey, + dropIndex: drop.dropIndex + }) + }) + ctx.clearReorderedWorktreeParents({ + draggedIds: dragSession.draggedIds, + sourceGroupKey: dragSession.sourceGroupKey + }) + clearWorktreeDrag() + }, + [clearWorktreeDrag, ctx, scrollRef, session] + ) + + return { handleWorktreeCardDragStart, handleWorktreeDragOver, handleWorktreeDrop } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-worktree-pointer-drag-autoscroll.ts b/src/renderer/src/components/sidebar/worktree-list/use-worktree-pointer-drag-autoscroll.ts new file mode 100644 index 00000000000..a2af2bd422d --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-worktree-pointer-drag-autoscroll.ts @@ -0,0 +1,79 @@ +import { useCallback } from 'react' +import type React from 'react' +import { getWorktreeSidebarDragAutoscroll } from '../worktree-sidebar-drag-autoscroll' +import type { WorktreeDragRuntime } from './use-worktree-drag-runtime' +import type { WorktreeDragSession } from './use-worktree-drag-session' +import type { WorktreePointerDrag } from './worktree-row-drag-state' + +// Scrolls the sidebar while a pointer drag hovers near its top or bottom edge, refreshing +// the drag session on every scrolled frame so the insertion line stays on the right row. +export function useWorktreePointerDragAutoscroll(args: { + session: WorktreeDragSession + runtime: WorktreeDragRuntime + scrollRef: React.RefObject + markScrollMovement: () => void + scheduleWorktreePointerDragFrame: (drag: WorktreePointerDrag) => void +}): () => void { + const { session, runtime, scrollRef, markScrollMovement, scheduleWorktreePointerDragFrame } = args + const { + worktreePointerDragRef, + pointerAutoscrollFrameIdRef, + pointerAutoscrollLastFrameTimeRef, + cancelWorktreePointerAutoscroll, + clearWorktreeDrag + } = runtime + + const runFrame = useCallback( + (frameTime: number) => { + pointerAutoscrollFrameIdRef.current = null + const drag = worktreePointerDragRef.current + const container = scrollRef.current + const dragSession = session.worktreeDragSessionRef.current + if (!drag?.active || !container || !dragSession) { + cancelWorktreePointerAutoscroll() + return + } + + const previousFrameTime = pointerAutoscrollLastFrameTimeRef.current ?? frameTime + pointerAutoscrollLastFrameTimeRef.current = frameTime + const autoscroll = getWorktreeSidebarDragAutoscroll({ + point: { clientX: drag.currentX, clientY: drag.currentY }, + containerRect: container.getBoundingClientRect(), + scrollTop: container.scrollTop, + scrollHeight: container.scrollHeight, + clientHeight: container.clientHeight, + elapsedMs: frameTime - previousFrameTime + }) + if (autoscroll) { + markScrollMovement() + container.scrollTop = autoscroll.scrollTop + if (!session.refreshWorktreeDragSession()) { + clearWorktreeDrag() + return + } + scheduleWorktreePointerDragFrame(drag) + } + + pointerAutoscrollFrameIdRef.current = window.requestAnimationFrame(runFrame) + }, + [ + cancelWorktreePointerAutoscroll, + clearWorktreeDrag, + markScrollMovement, + pointerAutoscrollFrameIdRef, + pointerAutoscrollLastFrameTimeRef, + scheduleWorktreePointerDragFrame, + scrollRef, + session, + worktreePointerDragRef + ] + ) + + return useCallback(() => { + if (pointerAutoscrollFrameIdRef.current !== null) { + return + } + pointerAutoscrollLastFrameTimeRef.current = null + pointerAutoscrollFrameIdRef.current = window.requestAnimationFrame(runFrame) + }, [pointerAutoscrollFrameIdRef, pointerAutoscrollLastFrameTimeRef, runFrame]) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-worktree-pointer-drag-window-events.ts b/src/renderer/src/components/sidebar/worktree-list/use-worktree-pointer-drag-window-events.ts new file mode 100644 index 00000000000..745670dde4d --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-worktree-pointer-drag-window-events.ts @@ -0,0 +1,95 @@ +import { useEffect } from 'react' +import type { WorktreeDragRuntime } from './use-worktree-drag-runtime' +import type { WorktreeDropCommitContext } from './worktree-drop-commit-context' +import type { WorktreeStatusDropAtIndexArgs } from './virtualized-worktree-viewport-props' +import { commitWorktreePointerDrop } from './worktree-pointer-drag-commit' +import { SIDEBAR_POINTER_DRAG_THRESHOLD_PX } from './worktree-sidebar-scroll-tuning' +import type { WorktreePointerDrag } from './worktree-row-drag-state' + +// Pointer drags escape the row that started them, so move/up/cancel are tracked on the +// window in capture phase. +export function useWorktreePointerDragWindowEvents(args: { + ctx: WorktreeDropCommitContext + runtime: WorktreeDragRuntime + beginWorktreePointerDrag: (drag: WorktreePointerDrag) => void + scheduleWorktreePointerDragFrame: (drag: WorktreePointerDrag) => void + onWorkspaceBoardDragPreviewCommit: () => void + onDropWorktreesOnWorkspaceBoard: (dropArgs: WorktreeStatusDropAtIndexArgs) => void +}): void { + const { + ctx, + beginWorktreePointerDrag, + scheduleWorktreePointerDragFrame, + onWorkspaceBoardDragPreviewCommit, + onDropWorktreesOnWorkspaceBoard + } = args + const { worktreePointerDragRef, clearWorktreeDrag } = args.runtime + + useEffect(() => { + const handlePointerMove = (event: PointerEvent): void => { + const drag = worktreePointerDragRef.current + if (!drag || event.pointerId !== drag.pointerId) { + return + } + drag.currentX = event.clientX + drag.currentY = event.clientY + if (!drag.active) { + const distance = Math.hypot(drag.currentX - drag.startX, drag.currentY - drag.startY) + if (distance < SIDEBAR_POINTER_DRAG_THRESHOLD_PX) { + return + } + beginWorktreePointerDrag(drag) + } + event.preventDefault() + event.stopPropagation() + scheduleWorktreePointerDragFrame(drag) + } + + const handlePointerUp = (event: PointerEvent): void => { + const drag = worktreePointerDragRef.current + if (!drag || event.pointerId !== drag.pointerId) { + return + } + drag.currentX = event.clientX + drag.currentY = event.clientY + if (!drag.active) { + worktreePointerDragRef.current = null + return + } + event.preventDefault() + event.stopPropagation() + commitWorktreePointerDrop({ + event, + drag, + ctx, + onWorkspaceBoardDragPreviewCommit, + onDropWorktreesOnWorkspaceBoard + }) + } + + const handlePointerCancel = (event: PointerEvent): void => { + const drag = worktreePointerDragRef.current + if (!drag || event.pointerId !== drag.pointerId) { + return + } + clearWorktreeDrag() + } + + window.addEventListener('pointermove', handlePointerMove, { capture: true }) + window.addEventListener('pointerup', handlePointerUp, { capture: true }) + window.addEventListener('pointercancel', handlePointerCancel, { capture: true }) + return () => { + window.removeEventListener('pointermove', handlePointerMove, { capture: true }) + window.removeEventListener('pointerup', handlePointerUp, { capture: true }) + window.removeEventListener('pointercancel', handlePointerCancel, { capture: true }) + } + }, [ + beginWorktreePointerDrag, + clearWorktreeDrag, + ctx, + onDropWorktreesOnWorkspaceBoard, + onWorkspaceBoardDragPreviewCommit, + scheduleWorktreePointerDragFrame, + worktreePointerDragRef + ]) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-worktree-pointer-drag.ts b/src/renderer/src/components/sidebar/worktree-list/use-worktree-pointer-drag.ts new file mode 100644 index 00000000000..b61694d9bd4 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-worktree-pointer-drag.ts @@ -0,0 +1,259 @@ +import { useCallback, useEffect } from 'react' +import type React from 'react' +import type { WorkspaceStatus, Worktree } from '../../../../../shared/worktree/types' +import { hasWorkspaceKanbanSidebarDropBoard } from '../workspace-kanban-sidebar-drop' +import { + createSidebarDragPreview, + isSidebarPointerDragBlocked, + setSidebarPointerDragDocumentStyles +} from '../worktree-sidebar-pointer-drag-dom' +import { getWorktreeSidebarDragRectsForGroup } from '../worktree-sidebar-drag-autoscroll' +import { getWorktreeSidebarDragGrab } from '../worktree-sidebar-drag-geometry' +import { NOOP_WORKSPACE_BOARD_DRAG_PREVIEW_CALLBACK } from './virtualized-worktree-viewport-props' +import type { WorktreeStatusDropAtIndexArgs } from './virtualized-worktree-viewport-props' +import type { WorktreeDropCommitContext } from './worktree-drop-commit-context' +import type { WorktreeDragRuntime } from './use-worktree-drag-runtime' +import type { WorktreeDragSession } from './use-worktree-drag-session' +import { useWorktreePointerDragAutoscroll } from './use-worktree-pointer-drag-autoscroll' +import { useWorktreePointerDragWindowEvents } from './use-worktree-pointer-drag-window-events' +import { flushWorktreePointerDragFrame } from './worktree-pointer-drag-flush' +import { + EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS, + type WorktreePointerDrag +} from './worktree-row-drag-state' + +export function useWorktreePointerDrag(args: { + ctx: WorktreeDropCommitContext + session: WorktreeDragSession + runtime: WorktreeDragRuntime + scrollRef: React.RefObject + markScrollMovement: () => void + selectedWorktreeIds: ReadonlySet + selectedWorktrees: readonly Worktree[] + workspaceBoardOpen: boolean + onWorkspaceBoardDragPreviewStart: () => void + onWorkspaceBoardDragPreviewCommit: () => void + onDropWorktreesOnWorkspaceBoard: (dropArgs: WorktreeStatusDropAtIndexArgs) => void + shouldShowWorkspaceBoardDropIndicator: ( + worktreeIds: readonly string[], + status: WorkspaceStatus + ) => boolean +}) { + const { + ctx, + session, + runtime, + scrollRef, + markScrollMovement, + selectedWorktreeIds, + selectedWorktrees, + workspaceBoardOpen, + onWorkspaceBoardDragPreviewStart, + onWorkspaceBoardDragPreviewCommit, + onDropWorktreesOnWorkspaceBoard, + shouldShowWorkspaceBoardDropIndicator + } = args + const { + worktreePointerDragRef, + suppressWorktreeClickUntilRef, + setWorktreeDragState, + setDragOverStatus, + setPinDragOver + } = runtime + + const flushWorktreePointerDrag = useCallback(() => { + const drag = worktreePointerDragRef.current + if (!drag) { + return + } + flushWorktreePointerDragFrame({ + drag, + ctx, + workspaceBoardOpen, + onWorkspaceBoardDragPreviewStart, + onWorkspaceBoardDragPreviewCommit, + shouldShowWorkspaceBoardDropIndicator, + setWorktreeDragState, + setDragOverStatus, + setPinDragOver + }) + }, [ + ctx, + onWorkspaceBoardDragPreviewCommit, + onWorkspaceBoardDragPreviewStart, + setDragOverStatus, + setPinDragOver, + setWorktreeDragState, + shouldShowWorkspaceBoardDropIndicator, + workspaceBoardOpen, + worktreePointerDragRef + ]) + + const scheduleWorktreePointerDragFrame = useCallback( + (drag: WorktreePointerDrag) => { + if (drag.frameId !== null) { + return + } + drag.frameId = window.requestAnimationFrame(flushWorktreePointerDrag) + }, + [flushWorktreePointerDrag] + ) + + const startWorktreePointerAutoscroll = useWorktreePointerDragAutoscroll({ + session, + runtime, + scrollRef, + markScrollMovement, + scheduleWorktreePointerDragFrame + }) + + const beginWorktreePointerDrag = useCallback( + (drag: WorktreePointerDrag) => { + const { preview, offsetX, offsetY, height } = createSidebarDragPreview({ + sourceRow: drag.sourceRow, + pointerX: drag.currentX, + pointerY: drag.currentY, + draggedCount: drag.draggedIds.length + }) + drag.active = true + drag.preview = preview + drag.previewOffsetX = offsetX + drag.previewOffsetY = offsetY + suppressWorktreeClickUntilRef.current = window.performance.now() + 500 + setSidebarPointerDragDocumentStyles(true) + session.worktreeDragSessionRef.current = { + draggingWorktreeId: drag.worktreeId, + sourceGroupKey: drag.sourceGroupKey, + draggedIds: drag.draggedIds, + reorderDraggedIds: drag.reorderDraggedIds, + reorderUnitDraggedIds: drag.reorderUnitDraggedIds, + rects: drag.rects, + // Why: reuse the floating preview's own offset so the hit test tracks the + // card the user sees, not the raw pointer. + grab: getWorktreeSidebarDragGrab({ offsetY, height }), + anchor: null + } + setWorktreeDragState({ + draggingWorktreeId: drag.worktreeId, + sourceGroupKey: drag.sourceGroupKey, + dropIndex: null, + dropIndicatorY: null, + previewOffsetsByWorktreeId: EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS, + pointerY: drag.currentY + }) + startWorktreePointerAutoscroll() + scheduleWorktreePointerDragFrame(drag) + }, + [ + scheduleWorktreePointerDragFrame, + session, + setWorktreeDragState, + startWorktreePointerAutoscroll, + suppressWorktreeClickUntilRef + ] + ) + + const handleWorktreeRowPointerDown = useCallback( + (event: React.PointerEvent, worktreeId: string, rowKey: string) => { + if (event.button !== 0 || event.pointerType === 'touch') { + return + } + const sourceRow = event.currentTarget + if (isSidebarPointerDragBlocked(event.target, sourceRow)) { + return + } + const sourceGroupKey = session.groupKeyByRowKey.get(rowKey) + const container = scrollRef.current + if (!sourceGroupKey || !container) { + return + } + const rects = getWorktreeSidebarDragRectsForGroup(container, sourceGroupKey) + const canPreviewWorkspaceBoardOnDrag = + !workspaceBoardOpen && + onWorkspaceBoardDragPreviewStart !== NOOP_WORKSPACE_BOARD_DRAG_PREVIEW_CALLBACK + if ( + rects.length <= 1 && + !hasWorkspaceKanbanSidebarDropBoard() && + !canPreviewWorkspaceBoardOnDrag + ) { + return + } + const draggedIds = + selectedWorktreeIds.has(worktreeId) && selectedWorktrees.length > 1 + ? selectedWorktrees.map((worktree) => worktree.id) + : [worktreeId] + const reorderDraggedIds = session.getReorderDraggedIds(draggedIds) + const reorderUnitDraggedIds = session.getReorderUnitDraggedIds( + sourceGroupKey, + reorderDraggedIds + ) + worktreePointerDragRef.current = { + pointerId: event.pointerId, + sourceRow, + startX: event.clientX, + startY: event.clientY, + currentX: event.clientX, + currentY: event.clientY, + worktreeId, + draggedIds, + reorderDraggedIds, + reorderUnitDraggedIds, + sourceGroupKey, + rects, + active: false, + preview: null, + previewOffsetX: 0, + previewOffsetY: 0, + workspaceBoardDragPreviewRequested: false, + frameId: null, + latestBoardDropTarget: null, + latestStatusDropTarget: null + } + }, + [ + onWorkspaceBoardDragPreviewStart, + scrollRef, + selectedWorktreeIds, + selectedWorktrees, + session, + workspaceBoardOpen, + worktreePointerDragRef + ] + ) + + const handleWorktreeRowClickCapture = useCallback( + (event: React.MouseEvent) => { + if (window.performance.now() >= suppressWorktreeClickUntilRef.current) { + return + } + event.preventDefault() + event.stopPropagation() + }, + [suppressWorktreeClickUntilRef] + ) + + useWorktreePointerDragWindowEvents({ + ctx, + runtime, + beginWorktreePointerDrag, + scheduleWorktreePointerDragFrame, + onWorkspaceBoardDragPreviewCommit, + onDropWorktreesOnWorkspaceBoard + }) + + useEffect(() => { + const handleClick = (event: MouseEvent): void => { + if (window.performance.now() >= suppressWorktreeClickUntilRef.current) { + return + } + event.preventDefault() + event.stopPropagation() + event.stopImmediatePropagation() + } + + document.addEventListener('click', handleClick, true) + return () => document.removeEventListener('click', handleClick, true) + }, [suppressWorktreeClickUntilRef]) + + return { handleWorktreeRowPointerDown, handleWorktreeRowClickCapture } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-worktree-sidebar-header-drag.ts b/src/renderer/src/components/sidebar/worktree-list/use-worktree-sidebar-header-drag.ts new file mode 100644 index 00000000000..93be311bc8e --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-worktree-sidebar-header-drag.ts @@ -0,0 +1,222 @@ +import { useCallback, useEffect, useMemo } from 'react' +import type React from 'react' +import { useAppStore } from '@/store' +import type { ProjectGroup } from '../../../../../shared/project-group-types' +import type { ProjectOrderBy } from '../../../../../shared/ui-chrome-types' +import type { Repo } from '../../../../../shared/repo-types' +import type { ExecutionHostId } from '../../../../../shared/execution-host' +import type { HostHeaderRow, HostSectionRow } from '../host-section-rows' +import type { Row, WorktreeGroupBy } from '../worktree-list-groups' +import type { RenderRow } from '../worktree-list-virtual-rows' +import { + getProjectGroupHeaderSectionEndByGroupId, + getRepoHeaderSectionEndByRepoId +} from '../worktree-header-section-boundaries' +import { useHostHeaderDrag } from '../host-header-drag' +import { useRepoHeaderDrag } from '../project-header-drag' +import { getSidebarOrderedRepoHeaderIdsByBucket } from '../project-header-drop' +import { useProjectGroupHeaderDrag } from '../project-group-header-drag' +import { getSidebarOrderedProjectGroupHeaderIdsByBucket } from '../project-group-header-drop' +import { USER_SCROLL_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS } from './worktree-sidebar-scroll-tuning' + +export type WorktreeSidebarHeaderDrag = ReturnType + +function indexById(byBucket: ReadonlyMap): Map { + const map = new Map() + for (const ids of byBucket.values()) { + ids.forEach((id, index) => { + map.set(id, index) + }) + } + return map +} + +function bucketById(byBucket: ReadonlyMap): Map { + const map = new Map() + for (const [bucketKey, ids] of byBucket) { + for (const id of ids) { + map.set(id, bucketKey) + } + } + return map +} + +// Reordering the three header tiers (host card, project group, project) plus the bucket +// bookkeeping their drag controllers and DOM attributes need. +export function useWorktreeSidebarHeaderDrag(args: { + rows: HostSectionRow[] + renderRows: readonly RenderRow[] + firstHeaderIndex: number + allRepoIds: string[] + repoMap: Map + projectGroups: readonly ProjectGroup[] + groupBy: WorktreeGroupBy + projectOrderBy: ProjectOrderBy + scrollRef: React.RefObject + onReorderHostSections: (orderedHostIds: ExecutionHostId[]) => void + onHostDragActiveChange: (active: boolean) => void + suppressMeasurementAdjustmentUntilRef: React.MutableRefObject + directScrollInputUntilRef: React.MutableRefObject +}) { + const { + rows, + renderRows, + firstHeaderIndex, + allRepoIds, + repoMap, + projectGroups, + groupBy, + projectOrderBy, + scrollRef, + onReorderHostSections, + onHostDragActiveChange, + suppressMeasurementAdjustmentUntilRef, + directScrollInputUntilRef + } = args + const reorderRepos = useAppStore((s) => s.reorderRepos) + const moveProjectToGroup = useAppStore((s) => s.moveProjectToGroup) + const updateProjectGroup = useAppStore((s) => s.updateProjectGroup) + const hasProjectGroups = projectGroups.length > 0 + const canReorderRepoHeaders = groupBy === 'repo' && projectOrderBy === 'manual' + const canReorderProjectGroupHeaders = groupBy === 'repo' && hasProjectGroups + const projectGroupByIdForHeaderDrag = useMemo( + () => new Map(projectGroups.map((group) => [group.id, group])), + [projectGroups] + ) + + // Why: reorder keeps scrollTop stable; flag direct scroll input so anchor-restore won't chase the moved row (jumpy drop). + const suppressScrollCorrectionForHeaderCommit = useCallback(() => { + const suppressUntil = window.performance.now() + USER_SCROLL_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS + suppressMeasurementAdjustmentUntilRef.current = suppressUntil + directScrollInputUntilRef.current = suppressUntil + }, [directScrollInputUntilRef, suppressMeasurementAdjustmentUntilRef]) + const commitRepoReorder = useCallback( + (orderedIds: string[]) => { + suppressScrollCorrectionForHeaderCommit() + reorderRepos(orderedIds) + }, + [reorderRepos, suppressScrollCorrectionForHeaderCommit] + ) + const orderedHostIds = useMemo( + () => + rows + .filter((row): row is HostHeaderRow => row.type === 'host-header') + .map((row) => row.hostId), + [rows] + ) + const hostDrag = useHostHeaderDrag({ + orderedHostIds, + onCommit: onReorderHostSections, + getScrollContainer: () => scrollRef.current + }) + useEffect(() => { + onHostDragActiveChange(hostDrag.state.draggingHostId !== null) + }, [hostDrag.state.draggingHostId, onHostDragActiveChange]) + useEffect(() => () => onHostDragActiveChange(false), [onHostDragActiveChange]) + + const sidebarRepoHeaderIdsByBucket = useMemo( + () => + getSidebarOrderedRepoHeaderIdsByBucket( + rows.filter((row): row is Row => row.type !== 'host-header') + ), + [rows] + ) + const sidebarProjectGroupHeaderIdsByBucket = useMemo( + () => + getSidebarOrderedProjectGroupHeaderIdsByBucket( + rows.filter((row): row is Row => row.type !== 'host-header'), + projectGroupByIdForHeaderDrag + ), + [projectGroupByIdForHeaderDrag, rows] + ) + const repoHeaderIndexByRepoId = useMemo( + () => indexById(sidebarRepoHeaderIdsByBucket), + [sidebarRepoHeaderIdsByBucket] + ) + const repoHeaderBucketByRepoId = useMemo( + () => bucketById(sidebarRepoHeaderIdsByBucket), + [sidebarRepoHeaderIdsByBucket] + ) + const projectGroupHeaderIndexByGroupId = useMemo( + () => indexById(sidebarProjectGroupHeaderIdsByBucket), + [sidebarProjectGroupHeaderIdsByBucket] + ) + const projectGroupHeaderBucketByGroupId = useMemo( + () => bucketById(sidebarProjectGroupHeaderIdsByBucket), + [sidebarProjectGroupHeaderIdsByBucket] + ) + const commitProjectGroupOrder = useCallback( + (repoId: string, projectGroupId: string | null, order: number) => { + void moveProjectToGroup(repoId, projectGroupId, order) + }, + [moveProjectToGroup] + ) + const commitProjectGroupHeaderOrder = useCallback( + (groupId: string, tabOrder: number) => { + if (!Number.isFinite(tabOrder)) { + return + } + suppressScrollCorrectionForHeaderCommit() + void updateProjectGroup(groupId, { tabOrder }) + }, + [suppressScrollCorrectionForHeaderCommit, updateProjectGroup] + ) + // Drag applies only in manual order; still construct the controller inert for stable hook order. + const repoDrag = useRepoHeaderDrag({ + orderedRepoIds: allRepoIds, + sidebarRepoHeaderIdsByBucket, + repoById: repoMap, + usesProjectGroupOrdering: hasProjectGroups, + onCommitRepoOrder: commitRepoReorder, + onCommitProjectGroupOrder: commitProjectGroupOrder, + getScrollContainer: () => scrollRef.current + }) + const projectGroupDrag = useProjectGroupHeaderDrag({ + sidebarProjectGroupHeaderIdsByBucket, + projectGroupById: projectGroupByIdForHeaderDrag, + onCommitProjectGroupTabOrder: commitProjectGroupHeaderOrder, + getScrollContainer: () => scrollRef.current + }) + const repoHeaderSectionEndByRepoId = useMemo( + () => + getRepoHeaderSectionEndByRepoId({ + rows: renderRows, + firstHeaderIndex, + sidebarRepoHeaderIdsByBucket, + repoHeaderBucketByRepoId + }), + [firstHeaderIndex, renderRows, repoHeaderBucketByRepoId, sidebarRepoHeaderIdsByBucket] + ) + const projectGroupHeaderSectionEndByGroupId = useMemo( + () => + getProjectGroupHeaderSectionEndByGroupId({ + rows: renderRows, + firstHeaderIndex, + sidebarProjectGroupHeaderIdsByBucket, + projectGroupHeaderBucketByGroupId + }), + [ + firstHeaderIndex, + projectGroupHeaderBucketByGroupId, + renderRows, + sidebarProjectGroupHeaderIdsByBucket + ] + ) + + return { + canReorderRepoHeaders, + canReorderProjectGroupHeaders, + orderedHostIds, + hostDrag, + repoDrag, + projectGroupDrag, + sidebarRepoHeaderIdsByBucket, + sidebarProjectGroupHeaderIdsByBucket, + repoHeaderIndexByRepoId, + repoHeaderBucketByRepoId, + projectGroupHeaderIndexByGroupId, + projectGroupHeaderBucketByGroupId, + repoHeaderSectionEndByRepoId, + projectGroupHeaderSectionEndByGroupId + } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-worktree-sidebar-scroll-suppression.ts b/src/renderer/src/components/sidebar/worktree-list/use-worktree-sidebar-scroll-suppression.ts new file mode 100644 index 00000000000..e606c270a70 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-worktree-sidebar-scroll-suppression.ts @@ -0,0 +1,83 @@ +import { useCallback, useEffect, useRef } from 'react' +import type React from 'react' +import { SUPPRESS_WORKTREE_LIST_SCROLL_ADJUSTMENT_EVENT } from '../WorktreeCardAgents' +import { + createPendingRevealScroll, + isRevealScrollSettling, + type PendingRevealScroll +} from '../worktree-sidebar-reveal-scroll-settle' +import { + EXPANDING_CARD_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS, + USER_SCROLL_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS +} from './worktree-sidebar-scroll-tuning' + +export type WorktreeSidebarScrollSuppression = ReturnType< + typeof useWorktreeSidebarScrollSuppression +> + +// Tracks the two suppression windows the virtualizer honours: measurement correction +// (any scroll movement) and anchor restore (direct user input or a settling reveal). +export function useWorktreeSidebarScrollSuppression( + scrollRef: React.RefObject +) { + const suppressMeasurementAdjustmentUntilRef = useRef(0) + const directScrollInputUntilRef = useRef(0) + const pendingRevealScrollRef = useRef(null) + + const markScrollMovement = useCallback(() => { + suppressMeasurementAdjustmentUntilRef.current = + window.performance.now() + USER_SCROLL_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS + }, []) + const markDirectScrollInput = useCallback(() => { + const suppressUntil = window.performance.now() + USER_SCROLL_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS + suppressMeasurementAdjustmentUntilRef.current = suppressUntil + directScrollInputUntilRef.current = suppressUntil + }, []) + const hasDirectScrollInput = useCallback( + () => window.performance.now() < directScrollInputUntilRef.current, + [] + ) + const markRevealScroll = useCallback((targetTop: number) => { + pendingRevealScrollRef.current = createPendingRevealScroll(targetTop, window.performance.now()) + }, []) + const isRevealScrollSettlingNow = useCallback(() => { + const settling = isRevealScrollSettling({ + now: window.performance.now(), + pending: pendingRevealScrollRef.current, + scrollTop: scrollRef.current?.scrollTop ?? 0 + }) + if (!settling) { + pendingRevealScrollRef.current = null + } + return settling + }, [scrollRef]) + // Why: programmatic scrolls keep measurement correction quiet, but only direct input blocks anchor-restore retries. + // A reveal's smooth scroll is the exception: restoring the anchor mid-animation cancels it a few pixels in. + const shouldSkipScrollAnchorRestore = useCallback( + () => + window.performance.now() < directScrollInputUntilRef.current || isRevealScrollSettlingNow(), + [isRevealScrollSettlingNow] + ) + + useEffect(() => { + const handleSuppress = () => { + // Why: let an expanding agent row grow in place instead of TanStack compensating scrollTop. + suppressMeasurementAdjustmentUntilRef.current = + window.performance.now() + EXPANDING_CARD_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS + } + window.addEventListener(SUPPRESS_WORKTREE_LIST_SCROLL_ADJUSTMENT_EVENT, handleSuppress) + return () => { + window.removeEventListener(SUPPRESS_WORKTREE_LIST_SCROLL_ADJUSTMENT_EVENT, handleSuppress) + } + }, []) + + return { + suppressMeasurementAdjustmentUntilRef, + directScrollInputUntilRef, + markScrollMovement, + markDirectScrollInput, + hasDirectScrollInput, + markRevealScroll, + shouldSkipScrollAnchorRestore + } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/use-worktree-status-mutations.ts b/src/renderer/src/components/sidebar/worktree-list/use-worktree-status-mutations.ts new file mode 100644 index 00000000000..77f9cc38b3f --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/use-worktree-status-mutations.ts @@ -0,0 +1,201 @@ +import { useCallback } from 'react' +import { useAppStore } from '@/store' +import type { + WorkspaceStatus, + WorkspaceStatusDefinition, + Worktree +} from '../../../../../shared/worktree/types' +import type { WorktreeMeta } from '../../../../../shared/worktree/meta-types' +import { getWorkspaceStatus, getWorkspaceStatusGroupKey } from '../workspace-status' +import { + buildManualOrderUpdatesForGroupDrop, + buildManualOrderUpdatesForVisibleGroups, + shouldWriteManualOrderForGroupDrop, + type WorktreeDragGroup +} from '../worktree-manual-order' +import { buildWorkspaceKanbanSidebarDropUpdates } from '../workspace-kanban-sidebar-drop' +import type { SortBy } from '../smart-sort' +import type { WorktreeStatusDropAtIndexArgs } from './virtualized-worktree-viewport-props' + +function buildRankByWorktreeId( + groups: readonly WorktreeDragGroup[], + worktreeMap: Map +): Map { + const rankByWorktreeId = new Map() + for (const group of groups) { + for (const worktreeId of group.worktreeIds) { + const worktree = worktreeMap.get(worktreeId) + if (worktree) { + rankByWorktreeId.set(worktreeId, worktree.manualOrder ?? worktree.sortOrder) + } + } + } + return rankByWorktreeId +} + +// Every write a sidebar drop can make: status changes, pin, manual order, and the board lane drop. +export function useWorktreeStatusMutations(args: { + worktreeMap: Map + workspaceStatuses: readonly WorkspaceStatusDefinition[] + sortBy: SortBy +}) { + const { worktreeMap, workspaceStatuses, sortBy } = args + const updateWorktreeMeta = useAppStore((s) => s.updateWorktreeMeta) + const updateWorktreesMeta = useAppStore((s) => s.updateWorktreesMeta) + const setSortBy = useAppStore((s) => s.setSortBy) + const setWorktreesPinnedAndReveal = useAppStore((s) => s.setWorktreesPinnedAndReveal) + + const moveWorktreeToStatus = useCallback( + (worktreeId: string, status: WorkspaceStatus) => { + const current = worktreeMap.get(worktreeId) + if (!current || getWorkspaceStatus(current, workspaceStatuses) === status) { + return + } + void updateWorktreeMeta(worktreeId, { workspaceStatus: status }) + }, + [updateWorktreeMeta, worktreeMap, workspaceStatuses] + ) + + const moveWorktreesToStatus = useCallback( + (worktreeIds: readonly string[], status: WorkspaceStatus) => { + const updates = new Map() + for (const worktreeId of worktreeIds) { + const current = worktreeMap.get(worktreeId) + if (!current || getWorkspaceStatus(current, workspaceStatuses) === status) { + continue + } + updates.set(worktreeId, { workspaceStatus: status }) + } + if (updates.size > 0) { + void updateWorktreesMeta(updates) + } + }, + [updateWorktreesMeta, worktreeMap, workspaceStatuses] + ) + + const moveWorktreesToStatusAtIndex = useCallback( + (dropArgs: WorktreeStatusDropAtIndexArgs) => { + const order = buildManualOrderUpdatesForGroupDrop({ + groups: dropArgs.groups, + targetGroupKey: getWorkspaceStatusGroupKey(dropArgs.status), + draggedIds: dropArgs.worktreeIds, + dropIndex: dropArgs.dropIndex, + now: Date.now(), + rankByWorktreeId: buildRankByWorktreeId(dropArgs.groups, worktreeMap) + }) + const updates = new Map>() + for (const worktreeId of dropArgs.worktreeIds) { + const current = worktreeMap.get(worktreeId) + if (!current) { + continue + } + const next: Partial = {} + if (getWorkspaceStatus(current, workspaceStatuses) !== dropArgs.status) { + next.workspaceStatus = dropArgs.status + } + updates.set(worktreeId, next) + } + for (const [worktreeId, manualOrder] of order.updates) { + updates.set(worktreeId, { ...updates.get(worktreeId), ...manualOrder }) + } + for (const [worktreeId, update] of Array.from(updates)) { + if (Object.keys(update).length === 0) { + updates.delete(worktreeId) + } + } + if (updates.size === 0) { + return + } + // Why: the insertion line promises exact placement, so persist manual order on a cross-status drop. + if (order.changed) { + setSortBy('manual') + } + void updateWorktreesMeta(updates) + }, + [setSortBy, updateWorktreesMeta, worktreeMap, workspaceStatuses] + ) + + const pinWorktree = useCallback( + (worktreeId: string) => { + setWorktreesPinnedAndReveal([worktreeId], true) + }, + [setWorktreesPinnedAndReveal] + ) + + const pinWorktrees = useCallback( + (worktreeIds: readonly string[]) => { + setWorktreesPinnedAndReveal(worktreeIds, true) + }, + [setWorktreesPinnedAndReveal] + ) + + const reorderWorktrees = useCallback( + (reorderArgs: { + groups: readonly WorktreeDragGroup[] + sourceGroupKey: string + draggedIds: readonly string[] + dropIndex: number + }) => { + const result = buildManualOrderUpdatesForVisibleGroups({ + ...reorderArgs, + now: Date.now(), + rankByWorktreeId: buildRankByWorktreeId(reorderArgs.groups, worktreeMap) + }) + if (!result.changed) { + return + } + // Why: only switch to Manual after a real move so accidental click-drags don't change the sort. + setSortBy('manual') + void updateWorktreesMeta(result.updates) + }, + [setSortBy, updateWorktreesMeta, worktreeMap] + ) + + const shouldShowWorkspaceBoardDropIndicator = useCallback( + (worktreeIds: readonly string[], status: WorkspaceStatus) => { + const sourceGroupKeys = worktreeIds.flatMap((worktreeId) => { + const worktree = worktreeMap.get(worktreeId) + return worktree ? [getWorkspaceStatus(worktree, workspaceStatuses)] : [] + }) + return shouldWriteManualOrderForGroupDrop({ + sortBy, + sourceGroupKeys, + targetGroupKey: status + }) + }, + [sortBy, worktreeMap, workspaceStatuses] + ) + + const dropWorktreesOnWorkspaceBoard = useCallback( + (dropArgs: WorktreeStatusDropAtIndexArgs) => { + const result = buildWorkspaceKanbanSidebarDropUpdates({ + ...dropArgs, + worktreeById: worktreeMap, + workspaceStatuses, + sortBy, + now: Date.now() + }) + if (result.updates.size === 0) { + return + } + // Why: switch to Manual when the drop changes order so the placement stays visible. + if (result.shouldSwitchToManual) { + setSortBy('manual') + } + useAppStore.getState().recordFeatureInteraction('workspace-board-actions') + void updateWorktreesMeta(result.updates) + }, + [setSortBy, sortBy, updateWorktreesMeta, worktreeMap, workspaceStatuses] + ) + + return { + moveWorktreeToStatus, + moveWorktreesToStatus, + moveWorktreesToStatusAtIndex, + pinWorktree, + pinWorktrees, + reorderWorktrees, + shouldShowWorkspaceBoardDropIndicator, + dropWorktreesOnWorkspaceBoard + } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/virtual-row-dom-attributes.ts b/src/renderer/src/components/sidebar/worktree-list/virtual-row-dom-attributes.ts new file mode 100644 index 00000000000..7523418e7c1 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/virtual-row-dom-attributes.ts @@ -0,0 +1,15 @@ +import { getVirtualRowTransform } from '../worktree-list-virtual-rows' + +export function getVirtualRowIndex(element: Element): number | null { + const index = Number.parseInt(element.getAttribute('data-index') ?? '', 10) + return Number.isNaN(index) ? null : index +} + +export function getVirtualRowKey(element: Element): string | null { + return element.getAttribute('data-worktree-virtual-row-key') +} + +export function getWorktreeVirtualRowTransform(start: number, previewOffset: number): string { + const base = getVirtualRowTransform(start) + return previewOffset === 0 ? base : `${base} translateY(${previewOffset}px)` +} diff --git a/src/renderer/src/components/sidebar/worktree-list/virtualized-worktree-viewport-props.ts b/src/renderer/src/components/sidebar/worktree-list/virtualized-worktree-viewport-props.ts new file mode 100644 index 00000000000..2aaacf85795 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/virtualized-worktree-viewport-props.ts @@ -0,0 +1,116 @@ +import type React from 'react' +import type { AppState } from '@/store/types' +import type { VirtualizedScrollAnchor } from '@/hooks/useVirtualizedScrollAnchor' +import type { PendingSidebarRowReveal, PendingSidebarWorktreeReveal } from '@/store/slices/ui' +import type { FolderWorkspace } from '../../../../../shared/folder-workspace-types' +import type { ProjectGroup } from '../../../../../shared/project-group-types' +import type { ProjectOrderBy } from '../../../../../shared/ui-chrome-types' +import type { Repo } from '../../../../../shared/repo-types' +import type { + WorkspaceLineage, + WorktreeLineage +} from '../../../../../shared/worktree/lineage-types' +import type { + WorkspaceStatus, + WorkspaceStatusDefinition, + Worktree +} from '../../../../../shared/worktree/types' +import type { ExecutionHostId } from '../../../../../shared/execution-host' +import type { HostSectionRow } from '../host-section-rows' +import type { ImportedWorktreeCardActionState } from '../imported-worktrees-card-actions' +import type { NewExternalWorktreesInboxActionState } from '../new-external-worktrees-inbox-actions' +import type { WorktreeDragGroup } from '../worktree-manual-order' +import type { + PinnedWorktreeDisplayPolicy, + ProjectGroupingModel, + WorktreeGroupBy +} from '../worktree-list-groups' + +export const EMPTY_PROJECT_GROUPS: readonly ProjectGroup[] = [] +export const NOOP_WORKSPACE_BOARD_DRAG_PREVIEW_CALLBACK = (): void => {} + +export type WorktreeStatusDropAtIndexArgs = { + worktreeIds: readonly string[] + status: WorkspaceStatus + dropIndex: number + groups: readonly WorktreeDragGroup[] +} + +export type VirtualizedWorktreeViewportProps = { + rows: HostSectionRow[] + activeWorktreeId: string | null + currentWorktreeId: string | null + groupBy: WorktreeGroupBy + pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy + projectOrderBy: ProjectOrderBy + toggleGroup: (key: string) => void + collapsedGroups: Set + handleCreateForRepo: (projectId: string) => void + handleOpenRepoSettings: (projectId: string, sectionId?: string) => void + handleOpenWorktreeVisibility: (repo: Repo) => void + handleShowImportedWorktrees: (projectId: string) => void + handleKeepImportedWorktreesHidden: (projectId: string) => void + importedWorktreeCardActionState: ReadonlyMap + handleOpenSuppressExternalWorktreeInbox: (projectId: string) => void + newExternalWorktreeInboxActionState: ReadonlyMap + handleRemoveProject: (repo: Repo) => void + handleCreateGroupFromRepo: (repo: Repo) => void + handleMoveProjectToGroup: (repo: Repo, groupId: string) => void + handleRemoveProjectFromGroup: (repo: Repo) => void + handleRenameProjectGroup: (groupId: string, currentName: string) => void + handleDeleteProjectGroup: (groupId: string, groupName: string) => void + handleCreateFolderWorkspace: (projectGroup: ProjectGroup) => void + activeModal: string + pendingRevealWorktree: PendingSidebarWorktreeReveal | null + pendingRevealSidebarRow: PendingSidebarRowReveal | null + clearPendingRevealWorktreeId: () => void + clearPendingRevealSidebarRow: () => void + agentSendTargetWorktreeId: string | null + worktrees: Worktree[] + folderWorkspaces: readonly FolderWorkspace[] + selectedWorktreeIds: ReadonlySet + selectedWorktrees: readonly Worktree[] + onSelectionGesture: (event: React.MouseEvent, worktreeId: string) => boolean + onImmediateWorktreeActivate: (worktreeId: string, rowKey: string | undefined) => void + onContextMenuSelect: ( + event: React.MouseEvent, + worktree: Worktree + ) => readonly Worktree[] + repoMap: Map + defaultHostId: ExecutionHostId + worktreeMap: Map + worktreeLineageById: Record + workspaceLineageByChildKey: Record + // Full canonical repo-id order; must include hidden repos or a reorder silently drops them. + allRepoIds: string[] + onReorderHostSections: (orderedHostIds: ExecutionHostId[]) => void + onHostDragActiveChange: (active: boolean) => void + prCache: AppState['prCache'] | null + hostedReviewCache: AppState['hostedReviewCache'] | null + workspaceStatuses: readonly WorkspaceStatusDefinition[] + projectGrouping?: ProjectGroupingModel + projectGroups?: readonly ProjectGroup[] + onMoveWorktreeToStatus: (worktreeId: string, status: WorkspaceStatus) => void + onMoveWorktreesToStatus: (worktreeIds: readonly string[], status: WorkspaceStatus) => void + onMoveWorktreesToStatusAtIndex: (args: WorktreeStatusDropAtIndexArgs) => void + onPinWorktree: (worktreeId: string) => void + onPinWorktrees: (worktreeIds: readonly string[]) => void + onDropWorktreesOnWorkspaceBoard: (args: WorktreeStatusDropAtIndexArgs) => void + workspaceBoardOpen: boolean + onWorkspaceBoardDragPreviewStart: () => void + onWorkspaceBoardDragPreviewCommit: () => void + onWorkspaceBoardDragPreviewCancel: () => void + shouldShowWorkspaceBoardDropIndicator: ( + worktreeIds: readonly string[], + status: WorkspaceStatus + ) => boolean + onReorderWorktrees: (args: { + groups: readonly WorktreeDragGroup[] + sourceGroupKey: string + draggedIds: readonly string[] + dropIndex: number + }) => void + // Why: grouping remounts the viewport, add/delete stays mounted; bridge both so the virtualizer never resets to scrollTop 0. + scrollOffsetRef: React.MutableRefObject + scrollAnchorRef: React.MutableRefObject +} diff --git a/src/renderer/src/components/sidebar/worktree-list/worktree-drag-groups.ts b/src/renderer/src/components/sidebar/worktree-list/worktree-drag-groups.ts new file mode 100644 index 00000000000..eb93ecf617a --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/worktree-drag-groups.ts @@ -0,0 +1,72 @@ +import { ALL_GROUP_KEY, PINNED_GROUP_KEY } from '../worktree-list-groups' +import type { HostSectionRow } from '../host-section-rows' +import type { WorktreeDragGroup } from '../worktree-manual-order' + +// A pinned duplicate of a worktree that also renders in its natural group is not its own drag slot. +function getNaturalWorktreeIds(rows: readonly HostSectionRow[]): Set { + return new Set( + rows.flatMap((row) => + row.type === 'item' && row.sectionKey !== PINNED_GROUP_KEY ? [row.worktree.id] : [] + ) + ) +} + +export function getWorktreeDragGroups(rows: HostSectionRow[]): WorktreeDragGroup[] { + const groups: WorktreeDragGroup[] = [] + let current: { key: string; ids: string[] } | null = null + const naturalWorktreeIds = getNaturalWorktreeIds(rows) + + for (const row of rows) { + if (row.type === 'header') { + current = { key: row.key, ids: [] } + groups.push({ key: current.key, worktreeIds: current.ids }) + continue + } + if ( + row.type === 'host-header' || + row.type === 'imported-worktrees-card' || + row.type === 'new-external-worktrees-inbox' || + row.type === 'pending-creation' || + row.type === 'folder-workspace' + ) { + continue + } + if (row.sectionKey === PINNED_GROUP_KEY && naturalWorktreeIds.has(row.worktree.id)) { + continue + } + if (!current) { + current = { key: ALL_GROUP_KEY, ids: [] } + groups.push({ key: current.key, worktreeIds: current.ids }) + } + current.ids.push(row.worktree.id) + } + + return groups.filter((group) => group.worktreeIds.length > 0) +} + +export function getWorktreeDragIndexes(rows: readonly HostSectionRow[]): { + groupKeyByRowKey: Map + groupIndexByRowKey: Map +} { + const groupKeyByRowKey = new Map() + const groupIndexByRowKey = new Map() + const groupIndexes = new Map() + const naturalWorktreeIds = getNaturalWorktreeIds(rows) + for (const row of rows) { + if (row.type === 'header') { + groupIndexes.set(row.key, 0) + continue + } + if (row.type !== 'item') { + continue + } + if (row.sectionKey === PINNED_GROUP_KEY && naturalWorktreeIds.has(row.worktree.id)) { + continue + } + const index = groupIndexes.get(row.sectionKey) ?? 0 + groupKeyByRowKey.set(row.rowKey, row.sectionKey) + groupIndexByRowKey.set(row.rowKey, index) + groupIndexes.set(row.sectionKey, index + 1) + } + return { groupKeyByRowKey, groupIndexByRowKey } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/worktree-drop-commit-context.ts b/src/renderer/src/components/sidebar/worktree-list/worktree-drop-commit-context.ts new file mode 100644 index 00000000000..2fee58ba4a4 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/worktree-drop-commit-context.ts @@ -0,0 +1,43 @@ +import type React from 'react' +import type { + WorkspaceStatus, + WorkspaceStatusDefinition +} from '../../../../../shared/worktree/types' +import type { WorktreeDragGroup } from '../worktree-manual-order' +import type { WorktreeDragUnitGroup } from '../worktree-drag-units' +import type { WorktreeSidebarDropPreview } from '../worktree-sidebar-drop-preview' +import type { WorktreeStatusDropAtIndexArgs } from './virtualized-worktree-viewport-props' +import type { WorktreeStatusDropRequest } from './use-worktree-drag-session' +import type { WorktreeSidebarLineageDropTarget } from './worktree-row-drag-state' + +// The shared surface every drop path (pointer, native drag, document capture) commits through. +export type WorktreeDropCommitContext = { + scrollRef: React.RefObject + workspaceStatuses: readonly WorkspaceStatusDefinition[] + worktreeDragGroups: readonly WorktreeDragGroup[] + worktreeDragUnitGroups: readonly WorktreeDragUnitGroup[] + computeWorktreeDrop: (pointerY: number) => WorktreeSidebarDropPreview | null + computeWorktreeStatusDrop: ( + request: WorktreeStatusDropRequest + ) => WorktreeSidebarDropPreview | null + refreshWorktreeDragSession: () => boolean + getEligibleLineageDropTarget: ( + target: WorktreeSidebarLineageDropTarget, + draggedIds: readonly string[] + ) => WorktreeSidebarLineageDropTarget + commitWorktreeLineageParentDrop: (draggedIds: readonly string[], parentId: string) => boolean + clearReorderedWorktreeParents: (args: { + draggedIds: readonly string[] + sourceGroupKey: string + }) => void + clearWorktreeDrag: () => void + onMoveWorktreesToStatus: (worktreeIds: readonly string[], status: WorkspaceStatus) => void + onMoveWorktreesToStatusAtIndex: (args: WorktreeStatusDropAtIndexArgs) => void + onReorderWorktrees: (args: { + groups: readonly WorktreeDragGroup[] + sourceGroupKey: string + draggedIds: readonly string[] + dropIndex: number + }) => void + onPinWorktrees: (worktreeIds: readonly string[]) => void +} diff --git a/src/renderer/src/components/sidebar/worktree-list/worktree-list-item-row.tsx b/src/renderer/src/components/sidebar/worktree-list/worktree-list-item-row.tsx new file mode 100644 index 00000000000..8e0a5c070c5 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/worktree-list-item-row.tsx @@ -0,0 +1,251 @@ +import React from 'react' +import { cn } from '@/lib/utils' +import type { AppState } from '@/store/types' +import type { Worktree } from '../../../../../shared/worktree/types' +import WorktreeCard, { type ActiveSurfaceVariant } from '../WorktreeCard' +import { PINNED_GROUP_KEY, type WorktreeGroupBy } from '../worktree-list-groups' +import { + getFolderBackedRepoWorktreeCardContentIndent, + getFolderBackedRepoWorktreeCardSurfaceInset, + getLineageChildrenInlineStyle, + getLineageNestedRowGeometry, + getWorktreeCardContentIndent, + getWorktreeCardSurfaceInset, + LINEAGE_CHILDREN_INLINE_OFFSET +} from '../worktree-list-indentation' +import type { LineageToggleHandler } from '../worktree-lineage-toggle-handler-cache' +import { stopNestedWorktreeCardBubble } from './repo-header-event-guards' +import type { WorktreeItemRow } from './render-row-item-rows' +import { getWorktreeOptionId } from './worktree-option-dom' +import type { WorktreePointerDrag, WorktreeRowDragState } from './worktree-row-drag-state' + +export type WorktreeItemRowContext = { + settings: AppState['settings'] + groupBy: WorktreeGroupBy + folderBackedProjectGroupIds: ReadonlySet + groupKeyByRowKey: ReadonlyMap + groupIndexByRowKey: ReadonlyMap + agentSendTargetWorktreeId: string | null + worktreeDragState: WorktreeRowDragState + worktreePointerDragRef: React.MutableRefObject + nativeLineageDropTargetId: string | null + activeWorktreeId: string | null + currentWorktreeId: string | null + highlightedRevealRowKey: string | null + selectedWorktreeIds: ReadonlySet + selectedWorktrees: readonly Worktree[] + getActiveSurfaceVariant: (row: WorktreeItemRow) => ActiveSurfaceVariant + getLineageToggleHandler: (groupKey: string) => LineageToggleHandler + onSelectionGesture: (event: React.MouseEvent, worktreeId: string) => boolean + onContextMenuSelect: ( + event: React.MouseEvent, + worktree: Worktree + ) => readonly Worktree[] + onImmediateActivate: (worktreeId: string, rowKey: string | undefined) => void + onRowClickCapture: (event: React.MouseEvent) => void + onRowPointerDown: ( + event: React.PointerEvent, + worktreeId: string, + rowKey: string + ) => void + onCardDragStart: ( + event: React.DragEvent, + worktreeId: string, + draggedIds: readonly string[] + ) => void + onCardDragEnd: () => void +} + +// Geometry differs three ways: a plain grouped row, a lineage child inheriting its parent's +// surface, and a row inside a folder-backed project group. +function getWorktreeItemRowGeometry( + ctx: WorktreeItemRowContext, + itemRow: WorktreeItemRow, + nested: boolean +): { surfaceInset: number; cardContentIndent: number; lineageChildrenInlineOffset?: number } { + const projectGroupId = itemRow.repo?.projectGroupId + const isFolderBackedRepoChild = + ctx.groupBy === 'repo' && + Boolean(projectGroupId && ctx.folderBackedProjectGroupIds.has(projectGroupId)) + // Why: experimental in-card lineage inherits the parent surface; legacy cards keep depth-based nested geometry. + const paddingDepth = nested ? Math.max(0, itemRow.depth - 1) : itemRow.depth + const getCardContentIndent = (lineageDepth: number): number => + isFolderBackedRepoChild + ? getFolderBackedRepoWorktreeCardContentIndent({ + groupDepth: itemRow.groupDepth, + lineageDepth + }) + : getWorktreeCardContentIndent({ + isGrouped: ctx.groupBy !== 'none', + groupDepth: itemRow.groupDepth, + lineageDepth + }) + const nestedLineageGeometry = nested + ? getLineageNestedRowGeometry({ + experimentalNewWorktreeCardStyle: ctx.settings?.experimentalNewWorktreeCardStyle === true, + inheritedCardContentIndent: getCardContentIndent(0), + lineageDepth: itemRow.depth + }) + : null + // Why: grouped rows inherit their header depth, but the card surface still spans the full row. + const paddingLeft = + nested && ctx.groupBy !== 'none' + ? getWorktreeCardContentIndent({ + isGrouped: false, + groupDepth: itemRow.groupDepth, + lineageDepth: paddingDepth + }) + : getCardContentIndent(paddingDepth) + const surfaceInset = nestedLineageGeometry + ? nestedLineageGeometry.surfaceInset + : isFolderBackedRepoChild + ? getFolderBackedRepoWorktreeCardSurfaceInset({ + groupDepth: itemRow.groupDepth, + lineageDepth: paddingDepth + }) + : getWorktreeCardSurfaceInset({ + isGrouped: ctx.groupBy !== 'none', + groupDepth: itemRow.groupDepth + }) + return { + surfaceInset, + cardContentIndent: nestedLineageGeometry + ? nestedLineageGeometry.cardContentIndent + : Math.max(0, paddingLeft - surfaceInset), + lineageChildrenInlineOffset: nestedLineageGeometry?.lineageChildrenInlineOffset + } +} + +export function renderWorktreeItemRow( + ctx: WorktreeItemRowContext, + itemRow: WorktreeItemRow, + nested: boolean, + lineageChildren?: React.ReactNode, + forceActiveSurface = false +): React.JSX.Element { + const { surfaceInset, cardContentIndent, lineageChildrenInlineOffset } = + getWorktreeItemRowGeometry(ctx, itemRow, nested) + const lineageChildrenStyle = lineageChildren + ? getLineageChildrenInlineStyle(lineageChildrenInlineOffset ?? LINEAGE_CHILDREN_INLINE_OFFSET) + : undefined + const worktreeDragGroupKey = ctx.groupKeyByRowKey.get(itemRow.rowKey) + const isLineageDropTarget = + ctx.worktreeDragState.draggingWorktreeId && + (ctx.worktreePointerDragRef.current?.latestStatusDropTarget?.target.lineageParentId === + itemRow.worktree.id || + ctx.nativeLineageDropTargetId === itemRow.worktree.id) + const isActiveWorktree = ctx.activeWorktreeId === itemRow.worktree.id + return ( +
{ + if (nested) { + event.stopPropagation() + } + ctx.onRowPointerDown(event, itemRow.worktree.id, itemRow.rowKey) + }} + style={{ + paddingLeft: surfaceInset > 0 ? `${surfaceInset}px` : undefined + }} + > + +
+ ) +} + +// Rebuild the visible lineage subtree so each card renders its own children inline. +export function renderWorktreeLineageDescendants( + ctx: WorktreeItemRowContext, + parent: WorktreeItemRow, + descendants: readonly WorktreeItemRow[] +): React.ReactNode | undefined { + const childNodes: React.ReactNode[] = [] + let cursor = 0 + while (cursor < descendants.length) { + const child = descendants[cursor] + if (!child || child.depth !== parent.depth + 1) { + cursor++ + continue + } + + let nextSiblingIndex = cursor + 1 + while ( + nextSiblingIndex < descendants.length && + descendants[nextSiblingIndex]!.depth > child.depth + ) { + nextSiblingIndex++ + } + + const childLineageChildren = renderWorktreeLineageDescendants( + ctx, + child, + descendants.slice(cursor + 1, nextSiblingIndex) + ) + childNodes.push(renderWorktreeItemRow(ctx, child, true, childLineageChildren)) + cursor = nextSiblingIndex + } + return childNodes.length > 0 ? childNodes : undefined +} diff --git a/src/renderer/src/components/sidebar/worktree-list/worktree-notice-virtual-rows.tsx b/src/renderer/src/components/sidebar/worktree-list/worktree-notice-virtual-rows.tsx new file mode 100644 index 00000000000..d997548f4bd --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/worktree-notice-virtual-rows.tsx @@ -0,0 +1,116 @@ +import React from 'react' +import type { VirtualItem } from '@tanstack/react-virtual' +import type { Row } from '../worktree-list-groups' +import { getVirtualRowTransform } from '../worktree-list-virtual-rows' +import { PendingWorktreeRow } from '../PendingWorktreeRow' +import ImportedWorktreesVisibilityLine from '../ImportedWorktreesVisibilityLine' +import NewExternalWorktreesInboxLine from '../NewExternalWorktreesInboxLine' +import type { ImportedWorktreeCardActionState } from '../imported-worktrees-card-actions' +import type { NewExternalWorktreesInboxActionState } from '../new-external-worktrees-inbox-actions' +import { canKeepImportedWorktreesHidden } from './imported-worktrees-card-visibility' + +type NoticeRowFrameProps = { + vItem: VirtualItem + measureVirtualRowElement: (element: HTMLDivElement | null) => void + className?: string + children: React.ReactNode +} + +// Every non-card sidebar row shares the same absolutely-positioned virtual frame. +function NoticeRowFrame({ + vItem, + measureVirtualRowElement, + className = 'absolute left-0 right-0 top-0', + children +}: NoticeRowFrameProps): React.JSX.Element { + return ( +
+ {children} +
+ ) +} + +export function renderImportedWorktreesVirtualRow(args: { + row: Extract + vItem: VirtualItem + measureVirtualRowElement: (element: HTMLDivElement | null) => void + actionState: ImportedWorktreeCardActionState | undefined + onShow: (projectId: string) => void + onKeepHidden: (projectId: string) => void +}): React.JSX.Element { + const { row, actionState } = args + return ( + + args.onShow(row.repo.id)} + onKeepHidden={ + canKeepImportedWorktreesHidden(row, actionState) + ? () => args.onKeepHidden(row.repo.id) + : undefined + } + /> + + ) +} + +export function renderNewExternalWorktreesInboxVirtualRow(args: { + row: Extract + vItem: VirtualItem + measureVirtualRowElement: (element: HTMLDivElement | null) => void + actionState: NewExternalWorktreesInboxActionState | undefined + onReview: (repo: Extract['repo']) => void + onSuppress: (projectId: string) => void +}): React.JSX.Element { + const { row, actionState } = args + return ( + + args.onReview(row.repo)} + onSuppress={() => args.onSuppress(row.repo.id)} + /> + + ) +} + +export function renderPendingCreationVirtualRow(args: { + row: Extract + vItem: VirtualItem + measureVirtualRowElement: (element: HTMLDivElement | null) => void +}): React.JSX.Element { + return ( + + + + ) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/worktree-option-dom.ts b/src/renderer/src/components/sidebar/worktree-list/worktree-option-dom.ts new file mode 100644 index 00000000000..ec52da40a12 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/worktree-option-dom.ts @@ -0,0 +1,58 @@ +export function getWorktreeOptionId(rowKey: string): string { + return `worktree-list-option-${encodeURIComponent(rowKey)}` +} + +export function getMountedWorktreeOptions( + worktreeId: string, + root?: ParentNode | null +): HTMLElement[] { + const scope = root ?? document + const result: HTMLElement[] = [] + scope.querySelectorAll('[data-worktree-id]').forEach((element) => { + if (element.dataset.worktreeId === worktreeId) { + result.push(element) + } + }) + return result +} + +export function markSidebarWorktreeActiveImmediately( + worktreeId: string, + primaryRowKey?: string +): void { + const sidebar = document.querySelector('[data-worktree-sidebar]') + const nextOptions = getMountedWorktreeOptions(worktreeId, sidebar) + const nextOption = nextOptions[0] + if (!nextOption) { + return + } + + sidebar + ?.querySelectorAll('[role="option"][aria-current="page"]') + .forEach((option) => option.removeAttribute('aria-current')) + + for (const option of nextOptions) { + option.setAttribute('aria-current', 'page') + } + sidebar + ?.querySelectorAll('[data-worktree-card-surface][data-worktree-card-active]') + .forEach((surface) => { + if (!nextOptions.some((option) => option.contains(surface))) { + surface.removeAttribute('data-worktree-card-active') + } + }) + for (const option of nextOptions) { + const activeSurfaceVariant = + primaryRowKey !== undefined + ? option.dataset.worktreeRowKey === primaryRowKey + ? 'primary' + : 'secondary' + : option === nextOption + ? 'primary' + : 'secondary' + const surface = option.matches('[data-worktree-card-surface]') + ? option + : option.querySelector('[data-worktree-card-surface]') + surface?.setAttribute('data-worktree-card-active', activeSurfaceVariant) + } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/worktree-pointer-drag-commit.ts b/src/renderer/src/components/sidebar/worktree-list/worktree-pointer-drag-commit.ts new file mode 100644 index 00000000000..1e09a403885 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/worktree-pointer-drag-commit.ts @@ -0,0 +1,159 @@ +import { + getWorkspaceKanbanSidebarDropGroups, + getWorkspaceKanbanSidebarDropTarget, + isWorkspaceKanbanSidebarDropPointInBoard, + resolveWorkspaceKanbanSidebarFullLaneDropIndex +} from '../workspace-kanban-sidebar-drop' +import { resolveWorkspaceKanbanCardDropCommitTarget } from '../workspace-kanban-card-pointer-drag-dom' +import { getFullDropIndexForWorktreeDragUnit } from '../worktree-drag-units' +import { resolveWorktreeSidebarStatusDropCommitTarget } from '../worktree-sidebar-drop-preview' +import { + getPointerDropStatusTarget, + shouldPreferSidebarStatusDropTarget +} from './pointer-drop-status-target' +import type { WorktreeDropCommitContext } from './worktree-drop-commit-context' +import type { WorktreeStatusDropAtIndexArgs } from './virtualized-worktree-viewport-props' +import type { WorktreeSidebarStatusDropTarget } from '../worktree-sidebar-drop-preview' +import { + NO_WORKTREE_SIDEBAR_DROP_TARGET, + type WorktreePointerDrag +} from './worktree-row-drag-state' + +type PointerDropCommitArgs = { + event: PointerEvent + drag: WorktreePointerDrag + ctx: WorktreeDropCommitContext + onWorkspaceBoardDragPreviewCommit: () => void + onDropWorktreesOnWorkspaceBoard: (args: WorktreeStatusDropAtIndexArgs) => void +} + +function commitStatusOrPinDrop( + args: PointerDropCommitArgs, + target: WorktreeSidebarStatusDropTarget & { lineageParentId?: string | null }, + dropIndex: number | null +): void { + const { drag, ctx } = args + if (target.isPinDrop) { + ctx.onPinWorktrees(drag.draggedIds) + return + } + if (!target.status) { + return + } + if (dropIndex === null) { + ctx.onMoveWorktreesToStatus(drag.reorderDraggedIds, target.status) + return + } + ctx.onMoveWorktreesToStatusAtIndex({ + worktreeIds: drag.reorderDraggedIds, + status: target.status, + dropIndex, + groups: ctx.worktreeDragGroups + }) +} + +// Resolve where a released pointer drag lands: workspace board lane, lineage parent, +// status/pin section, or a reorder slot inside the source group. +export function commitWorktreePointerDrop(args: PointerDropCommitArgs): void { + const { event, drag, ctx } = args + if (!ctx.refreshWorktreeDragSession()) { + ctx.clearWorktreeDrag() + return + } + const boardDropTarget = resolveWorkspaceKanbanCardDropCommitTarget({ + currentTarget: getWorkspaceKanbanSidebarDropTarget(event.clientX, event.clientY), + latestTrackedTarget: drag.latestBoardDropTarget, + x: event.clientX, + y: event.clientY + }) + if (isWorkspaceKanbanSidebarDropPointInBoard(event.clientX, event.clientY)) { + args.onWorkspaceBoardDragPreviewCommit() + } + if (boardDropTarget.isPinDrop) { + ctx.onPinWorktrees(drag.draggedIds) + } else if (boardDropTarget.status) { + args.onDropWorktreesOnWorkspaceBoard({ + worktreeIds: drag.reorderDraggedIds, + status: boardDropTarget.status, + // Why: the target counts rendered cards, but the groups are the full + // lane. Board search can make those two differ. + dropIndex: resolveWorkspaceKanbanSidebarFullLaneDropIndex( + boardDropTarget.status, + boardDropTarget.dropIndex + ), + groups: getWorkspaceKanbanSidebarDropGroups() + }) + } else { + const preferredStatusTarget = ctx.getEligibleLineageDropTarget( + ctx.scrollRef.current + ? getPointerDropStatusTarget({ + container: ctx.scrollRef.current, + x: event.clientX, + y: event.clientY + }) + : NO_WORKTREE_SIDEBAR_DROP_TARGET, + drag.draggedIds + ) + if (preferredStatusTarget.lineageParentId) { + ctx.commitWorktreeLineageParentDrop(drag.draggedIds, preferredStatusTarget.lineageParentId) + ctx.clearWorktreeDrag() + return + } + if ( + shouldPreferSidebarStatusDropTarget({ + sourceGroupKey: drag.sourceGroupKey, + target: preferredStatusTarget, + workspaceStatuses: ctx.workspaceStatuses + }) + ) { + const statusDrop = preferredStatusTarget.status + ? ctx.computeWorktreeStatusDrop({ + pointerY: event.clientY, + status: preferredStatusTarget.status, + draggedIds: drag.reorderDraggedIds + }) + : null + commitStatusOrPinDrop(args, preferredStatusTarget, statusDrop?.dropIndex ?? null) + ctx.clearWorktreeDrag() + return + } + const drop = ctx.computeWorktreeDrop(event.clientY) + if (drop) { + ctx.onReorderWorktrees({ + groups: ctx.worktreeDragGroups, + sourceGroupKey: drag.sourceGroupKey, + draggedIds: drag.reorderDraggedIds, + dropIndex: getFullDropIndexForWorktreeDragUnit({ + groups: ctx.worktreeDragUnitGroups, + sourceGroupKey: drag.sourceGroupKey, + dropIndex: drop.dropIndex + }) + }) + ctx.clearReorderedWorktreeParents({ + draggedIds: drag.draggedIds, + sourceGroupKey: drag.sourceGroupKey + }) + } else if (ctx.scrollRef.current) { + const currentPreview = preferredStatusTarget.status + ? ctx.computeWorktreeStatusDrop({ + pointerY: event.clientY, + status: preferredStatusTarget.status, + draggedIds: drag.reorderDraggedIds + }) + : null + const { target, preview: statusDrop } = resolveWorktreeSidebarStatusDropCommitTarget({ + currentTarget: preferredStatusTarget, + currentPreview, + latestTrackedTarget: drag.latestStatusDropTarget, + x: event.clientX, + y: event.clientY + }) + if (target.lineageParentId) { + ctx.commitWorktreeLineageParentDrop(drag.draggedIds, target.lineageParentId) + } else { + commitStatusOrPinDrop(args, target, statusDrop?.dropIndex ?? null) + } + } + } + ctx.clearWorktreeDrag() +} diff --git a/src/renderer/src/components/sidebar/worktree-list/worktree-pointer-drag-flush.ts b/src/renderer/src/components/sidebar/worktree-list/worktree-pointer-drag-flush.ts new file mode 100644 index 00000000000..90d70d4534b --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/worktree-pointer-drag-flush.ts @@ -0,0 +1,172 @@ +import type React from 'react' +import type { WorkspaceStatus } from '../../../../../shared/worktree/types' +import { + clearWorkspaceKanbanSidebarDropTargetVisual, + hasWorkspaceKanbanSidebarDropBoard, + isWorkspaceKanbanSidebarDropPointInBoard, + updateWorkspaceKanbanSidebarDropTargetVisual +} from '../workspace-kanban-sidebar-drop' +import { updateSidebarDragPreviewPosition } from '../worktree-sidebar-pointer-drag-dom' +import { + getPointerDropStatusTarget, + shouldPreferSidebarStatusDropTarget +} from './pointer-drop-status-target' +import type { WorktreeDropCommitContext } from './worktree-drop-commit-context' +import { + applyWorktreeDropPreview, + clearWorktreeDropPreview, + NO_WORKTREE_SIDEBAR_DROP_TARGET, + updateLatestWorktreeStatusDropTarget, + type WorktreePointerDrag, + type WorktreeRowDragState, + type WorktreeSidebarLineageDropTarget +} from './worktree-row-drag-state' + +export type WorktreePointerDragFrameArgs = { + drag: WorktreePointerDrag + ctx: WorktreeDropCommitContext + workspaceBoardOpen: boolean + onWorkspaceBoardDragPreviewStart: () => void + onWorkspaceBoardDragPreviewCommit: () => void + shouldShowWorkspaceBoardDropIndicator: ( + worktreeIds: readonly string[], + status: WorkspaceStatus + ) => boolean + setWorktreeDragState: React.Dispatch> + setDragOverStatus: (status: WorkspaceStatus | null) => void + setPinDragOver: (pinDragOver: boolean) => void +} + +// Reflect a status/pin hover that has no insertion line of its own. +function showStatusHoverWithoutInsertionLine( + args: WorktreePointerDragFrameArgs, + target: WorktreeSidebarLineageDropTarget +): void { + const { drag, ctx } = args + const statusDrop = target.status + ? ctx.computeWorktreeStatusDrop({ + pointerY: drag.currentY, + status: target.status, + draggedIds: drag.reorderDraggedIds + }) + : null + updateLatestWorktreeStatusDropTarget(drag, target, statusDrop) + if (statusDrop) { + clearWorkspaceKanbanSidebarDropTargetVisual() + args.setDragOverStatus(null) + args.setPinDragOver(false) + args.setWorktreeDragState((prev) => + applyWorktreeDropPreview(prev, statusDrop, { + pointerY: drag.currentY, + matchPointerY: true + }) + ) + return + } + args.setDragOverStatus(target.status) + args.setPinDragOver(target.isPinDrop) + args.setWorktreeDragState((prev) => + clearWorktreeDropPreview(prev, { pointerY: drag.currentY, matchPointerY: true }) + ) +} + +function clearInsertionLine(args: WorktreePointerDragFrameArgs): void { + args.setDragOverStatus(null) + args.setPinDragOver(false) + args.setWorktreeDragState((prev) => + clearWorktreeDropPreview(prev, { pointerY: args.drag.currentY, matchPointerY: true }) + ) +} + +// One animation frame of an in-flight pointer drag: move the floating preview, then decide +// whether the pointer is over the workspace board, a status/pin section, or a reorder slot. +export function flushWorktreePointerDragFrame(args: WorktreePointerDragFrameArgs): void { + const { drag, ctx } = args + drag.frameId = null + if (!drag.active || !drag.preview) { + return + } + updateSidebarDragPreviewPosition({ + preview: drag.preview, + pointerX: drag.currentX, + pointerY: drag.currentY, + offsetX: drag.previewOffsetX, + offsetY: drag.previewOffsetY + }) + if (!ctx.refreshWorktreeDragSession()) { + ctx.clearWorktreeDrag() + return + } + // Why: show the board preview as soon as a card drag begins so the drop target is visible up front, not only at the sidebar edge. + if ( + !drag.workspaceBoardDragPreviewRequested && + !args.workspaceBoardOpen && + !hasWorkspaceKanbanSidebarDropBoard() + ) { + drag.workspaceBoardDragPreviewRequested = true + args.onWorkspaceBoardDragPreviewStart() + } + const boardTarget = updateWorkspaceKanbanSidebarDropTargetVisual({ + x: drag.currentX, + y: drag.currentY, + shouldShowDropIndicator: (target) => + Boolean( + target.status && + args.shouldShowWorkspaceBoardDropIndicator(drag.reorderDraggedIds, target.status) + ) + }) + drag.latestBoardDropTarget = { + target: boardTarget, + x: drag.currentX, + y: drag.currentY + } + if (isWorkspaceKanbanSidebarDropPointInBoard(drag.currentX, drag.currentY)) { + args.onWorkspaceBoardDragPreviewCommit() + } + if (boardTarget.status || boardTarget.isPinDrop) { + drag.latestStatusDropTarget = null + clearInsertionLine(args) + return + } + + const sidebarContainer = ctx.scrollRef.current + const preferredStatusTarget = ctx.getEligibleLineageDropTarget( + sidebarContainer + ? getPointerDropStatusTarget({ + container: sidebarContainer, + x: drag.currentX, + y: drag.currentY + }) + : NO_WORKTREE_SIDEBAR_DROP_TARGET, + drag.draggedIds + ) + if (preferredStatusTarget.lineageParentId) { + updateLatestWorktreeStatusDropTarget(drag, preferredStatusTarget, null) + clearWorkspaceKanbanSidebarDropTargetVisual() + clearInsertionLine(args) + return + } + if ( + shouldPreferSidebarStatusDropTarget({ + sourceGroupKey: drag.sourceGroupKey, + target: preferredStatusTarget, + workspaceStatuses: ctx.workspaceStatuses + }) + ) { + showStatusHoverWithoutInsertionLine(args, preferredStatusTarget) + return + } + + const drop = ctx.computeWorktreeDrop(drag.currentY) + if (!drop) { + showStatusHoverWithoutInsertionLine(args, preferredStatusTarget) + return + } + drag.latestStatusDropTarget = null + clearWorkspaceKanbanSidebarDropTargetVisual() + args.setDragOverStatus(null) + args.setPinDragOver(false) + args.setWorktreeDragState((prev) => + applyWorktreeDropPreview(prev, drop, { pointerY: drag.currentY, matchPointerY: true }) + ) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/worktree-row-drag-state.ts b/src/renderer/src/components/sidebar/worktree-list/worktree-row-drag-state.ts new file mode 100644 index 00000000000..2bb47089e22 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/worktree-row-drag-state.ts @@ -0,0 +1,132 @@ +import type { WorkspaceKanbanCardTrackedDropTarget } from '../workspace-kanban-card-pointer-drag-dom' +import type { WorktreeSidebarDragRect } from '../worktree-sidebar-drag-autoscroll' +import type { + WorktreeSidebarDropPreview, + WorktreeSidebarStatusDropTarget, + WorktreeSidebarTrackedStatusDropTarget +} from '../worktree-sidebar-drop-preview' + +export type WorktreeRowDragState = { + draggingWorktreeId: string | null + sourceGroupKey: string | null + dropIndex: number | null + dropIndicatorY: number | null + previewOffsetsByWorktreeId: ReadonlyMap + pointerY: number | null +} + +export const EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS: ReadonlyMap = new Map() + +export const WORKTREE_ROW_DRAG_INITIAL_STATE: WorktreeRowDragState = { + draggingWorktreeId: null, + sourceGroupKey: null, + dropIndex: null, + dropIndicatorY: null, + previewOffsetsByWorktreeId: EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS, + pointerY: null +} + +export type WorktreePointerDrag = { + pointerId: number + sourceRow: HTMLElement + startX: number + startY: number + currentX: number + currentY: number + worktreeId: string + draggedIds: readonly string[] + reorderDraggedIds: readonly string[] + reorderUnitDraggedIds: readonly string[] + sourceGroupKey: string + rects: readonly WorktreeSidebarDragRect[] + active: boolean + preview: HTMLElement | null + previewOffsetX: number + previewOffsetY: number + workspaceBoardDragPreviewRequested: boolean + frameId: number | null + latestBoardDropTarget: WorkspaceKanbanCardTrackedDropTarget | null + latestStatusDropTarget: WorktreeSidebarTrackedStatusDropTarget | null +} + +export type WorktreeSidebarLineageDropTarget = WorktreeSidebarStatusDropTarget & { + lineageParentId: string | null +} + +export const NO_WORKTREE_SIDEBAR_DROP_TARGET: WorktreeSidebarLineageDropTarget = { + status: null, + isPinDrop: false, + lineageParentId: null +} + +export function areWorktreeDragPreviewOffsetsEqual( + a: ReadonlyMap, + b: ReadonlyMap +): boolean { + if (a === b) { + return true + } + if (a.size !== b.size) { + return false + } + for (const [key, value] of a) { + if (b.get(key) !== value) { + return false + } + } + return true +} + +export function updateLatestWorktreeStatusDropTarget( + drag: WorktreePointerDrag, + target: WorktreeSidebarLineageDropTarget, + preview: WorktreeSidebarDropPreview | null +): void { + drag.latestStatusDropTarget = + target.status || target.isPinDrop || target.lineageParentId + ? { + target, + preview, + x: drag.currentX, + y: drag.currentY + } + : null +} + +// Why matchPointerY: pointer-driven updates must repaint when only the pointer moved; native-drag +// updates deliberately keep the previous state identity in that case. +export function clearWorktreeDropPreview( + previous: WorktreeRowDragState, + args: { pointerY: number | null; matchPointerY?: boolean } +): WorktreeRowDragState { + const unchanged = + previous.dropIndex === null && + previous.dropIndicatorY === null && + previous.previewOffsetsByWorktreeId.size === 0 && + (args.matchPointerY !== true || previous.pointerY === args.pointerY) + return unchanged + ? previous + : { + ...previous, + dropIndex: null, + dropIndicatorY: null, + previewOffsetsByWorktreeId: EMPTY_WORKTREE_DRAG_PREVIEW_OFFSETS, + pointerY: args.pointerY + } +} + +export function applyWorktreeDropPreview( + previous: WorktreeRowDragState, + drop: WorktreeSidebarDropPreview, + args: { pointerY: number; matchPointerY?: boolean } +): WorktreeRowDragState { + const unchanged = + previous.dropIndex === drop.dropIndex && + previous.dropIndicatorY === drop.dropIndicatorY && + (args.matchPointerY !== true || previous.pointerY === args.pointerY) && + areWorktreeDragPreviewOffsetsEqual( + previous.previewOffsetsByWorktreeId, + drop.previewOffsetsByWorktreeId + ) + return unchanged ? previous : { ...previous, ...drop, pointerY: args.pointerY } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/worktree-section-header-row.tsx b/src/renderer/src/components/sidebar/worktree-list/worktree-section-header-row.tsx new file mode 100644 index 00000000000..dd77e3dd0da --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/worktree-section-header-row.tsx @@ -0,0 +1,393 @@ +import React from 'react' +import { ChevronDown } from 'lucide-react' +import type { VirtualItem } from '@tanstack/react-virtual' +import { cn } from '@/lib/utils' +import type { AppState } from '@/store/types' +import { RepoIconGlyph } from '@/components/repo/repo-icon' +import { RepoForkIndicator } from '@/components/repo/repo-fork-indicator' +import type { FolderWorkspacePathStatus } from '../../../../../shared/folder-workspace-path-status' +import { isConfirmedStaleFolderPathStatus } from '../../../../../shared/folder-workspace-path-status' +import type { ProjectGroup } from '../../../../../shared/project-group-types' +import type { + WorkspaceStatus, + WorkspaceStatusDefinition +} from '../../../../../shared/worktree/types' +import type { GroupHeaderRow, WorktreeGroupBy } from '../worktree-list-groups' +import { PINNED_GROUP_KEY } from '../worktree-list-groups' +import { getWorkspaceStatusFromGroupKey } from '../workspace-status' +import { getVirtualRowTransform } from '../worktree-list-virtual-rows' +import { resolveProjectGroupHeaderColor } from '../project-header-color' +import { getRepoHeaderCreateState } from '../repo-header-create-state' +import { ProjectHeaderActions } from '../ProjectHeaderActions' +import { + getProjectGroupHeaderPaddingLeft, + WORKTREE_SECTION_HEADER_PADDING_LEFT +} from '../worktree-list-indentation' +import { FolderPathStatusIndicator } from './FolderPathStatusIndicator' +import { + ProjectGroupCreateWorkspaceButton, + ProjectGroupHeaderMenu +} from './project-group-header-actions' +import { + RepoHeaderCreateWorkspaceButton, + RepoHeaderProjectActionsMenu, + type RepoHeaderProjectActions +} from './repo-header-project-actions' +import { + handleRepoHeaderCollapseAffordancePointerDown, + shouldIgnoreRepoHeaderToggle +} from './repo-header-event-guards' +import type { WorktreeSidebarHeaderDrag } from './use-worktree-sidebar-header-drag' +import { getWorktreeOptionId } from './worktree-option-dom' + +export type SectionHeaderRowContext = { + groupBy: WorktreeGroupBy + collapsedGroups: Set + workspaceStatuses: readonly WorkspaceStatusDefinition[] + projectGroups: readonly ProjectGroup[] + sshConnectionStates: AppState['sshConnectionStates'] + highlightedRevealRowKey: string | null + dragOverStatus: WorkspaceStatus | null + pinDragOver: boolean + headerDrag: WorktreeSidebarHeaderDrag + getCachedFolderWorkspacePathStatus: (request: { + scope: 'project-group' + projectGroupId: string + }) => FolderWorkspacePathStatus | null + toggleGroupWithScrollAnchor: (groupKey: string) => void + projectActions: RepoHeaderProjectActions + onRenameProjectGroup: (groupId: string, currentName: string) => void + onDeleteProjectGroup: (groupId: string, groupName: string) => void + onCreateFolderWorkspace: (projectGroup: ProjectGroup) => void + onWorkspaceStatusDragOver: (event: React.DragEvent, status: WorkspaceStatus) => void + onWorkspaceStatusDragLeave: (event: React.DragEvent) => void + onWorkspacePinDragOver: (event: React.DragEvent) => void + onWorkspacePinDragLeave: (event: React.DragEvent) => void + onWorkspaceStatusDrop: (event: React.DragEvent, status: WorkspaceStatus) => void +} + +// The folder-scan project group whose parent path is gone can't create new workspaces. +function isFolderWorkspaceCreateDisabled(status: FolderWorkspacePathStatus | null): boolean { + return ( + status?.exists === false && + (isConfirmedStaleFolderPathStatus(status) || status.reason === 'ambiguous-connection') + ) +} + +export function renderWorktreeSectionHeaderRow(args: { + ctx: SectionHeaderRowContext + row: GroupHeaderRow + vItem: VirtualItem + isActiveStickyHeader: boolean + hasStickyHost: boolean + hasHeaderTopSpacing: boolean + measureVirtualRowElement: (element: HTMLDivElement | null) => void +}): React.JSX.Element { + const { ctx, row, vItem, isActiveStickyHeader } = args + const { headerDrag } = ctx + const isRepoHeader = ctx.groupBy === 'repo' && row.repo !== undefined + const isProjectGroupHeader = ctx.groupBy === 'repo' && row.projectGroup !== undefined + const projectIdForHeader = isRepoHeader ? row.repo!.id : undefined + const projectGroupIdForHeader = + isProjectGroupHeader && !row.repo && typeof row.projectGroup?.id === 'string' + ? row.projectGroup.id + : undefined + const repoHeaderIndex = + projectIdForHeader !== undefined + ? headerDrag.repoHeaderIndexByRepoId.get(projectIdForHeader) + : undefined + const repoHeaderBucketKey = + projectIdForHeader !== undefined + ? headerDrag.repoHeaderBucketByRepoId.get(projectIdForHeader) + : undefined + const projectGroupHeaderIndex = + projectGroupIdForHeader !== undefined + ? headerDrag.projectGroupHeaderIndexByGroupId.get(projectGroupIdForHeader) + : undefined + const projectGroupHeaderBucketKey = + projectGroupIdForHeader !== undefined + ? headerDrag.projectGroupHeaderBucketByGroupId.get(projectGroupIdForHeader) + : undefined + const isDraggableRepoHeader = Boolean( + headerDrag.canReorderRepoHeaders && + isRepoHeader && + projectIdForHeader && + repoHeaderBucketKey && + (headerDrag.sidebarRepoHeaderIdsByBucket.get(repoHeaderBucketKey)?.length ?? 0) > 1 + ) + const isDraggableProjectGroupHeader = Boolean( + headerDrag.canReorderProjectGroupHeaders && + projectGroupIdForHeader && + projectGroupHeaderBucketKey && + (headerDrag.sidebarProjectGroupHeaderIdsByBucket.get(projectGroupHeaderBucketKey)?.length ?? + 0) > 1 + ) + const isDraggingThis = + headerDrag.canReorderRepoHeaders && + headerDrag.repoDrag.state.draggingRepoId !== null && + headerDrag.repoDrag.state.draggingRepoId === projectIdForHeader + const isDraggingThisProjectGroup = + headerDrag.canReorderProjectGroupHeaders && + headerDrag.projectGroupDrag.state.draggingGroupId !== null && + headerDrag.projectGroupDrag.state.draggingGroupId === projectGroupIdForHeader + const headerWorkspaceStatus = + ctx.groupBy === 'workspace-status' + ? getWorkspaceStatusFromGroupKey(row.key, ctx.workspaceStatuses) + : null + const isPinnedHeader = row.key === PINNED_GROUP_KEY + const repoHeaderColor = resolveProjectGroupHeaderColor({ + groupBy: ctx.groupBy, + headerKey: row.key, + badgeColor: row.repo?.badgeColor + }) + const createState = row.repo + ? getRepoHeaderCreateState({ + repo: row.repo, + label: row.label, + sshStatus: row.repo.connectionId + ? (ctx.sshConnectionStates.get(row.repo.connectionId)?.status ?? null) + : null + }) + : null + const folderBackedProjectGroup = + isProjectGroupHeader && + !row.repo && + row.projectGroup && + 'parentPath' in row.projectGroup && + row.projectGroup.parentPath + ? row.projectGroup + : null + const projectGroupPathStatus = folderBackedProjectGroup + ? ctx.getCachedFolderWorkspacePathStatus({ + scope: 'project-group', + projectGroupId: folderBackedProjectGroup.id + }) + : null + const isHeaderCollapsed = ctx.collapsedGroups.has(row.key) + // Why: repo/project/status/pinned share compact section chrome; flat "All" stays a simple label. + const showHeaderCollapseAffordance = + row.count > 0 && + (isRepoHeader || isProjectGroupHeader || headerWorkspaceStatus !== null || isPinnedHeader) + return ( +
+
ctx.onWorkspaceStatusDragOver(event, headerWorkspaceStatus) + : undefined + } + onDragLeave={ + isPinnedHeader + ? ctx.onWorkspacePinDragLeave + : headerWorkspaceStatus + ? ctx.onWorkspaceStatusDragLeave + : undefined + } + onDrop={ + headerWorkspaceStatus + ? (event) => ctx.onWorkspaceStatusDrop(event, headerWorkspaceStatus) + : undefined + } + onPointerDown={ + isDraggableRepoHeader && projectIdForHeader + ? (event) => headerDrag.repoDrag.onHandlePointerDown(event, projectIdForHeader) + : isDraggableProjectGroupHeader && projectGroupIdForHeader + ? (event) => + headerDrag.projectGroupDrag.onHandlePointerDown(event, projectGroupIdForHeader) + : undefined + } + onClick={(event) => { + if (shouldIgnoreRepoHeaderToggle(event)) { + return + } + ctx.toggleGroupWithScrollAnchor(row.key) + }} + onKeyDown={(e) => { + if (shouldIgnoreRepoHeaderToggle(e)) { + return + } + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + ctx.toggleGroupWithScrollAnchor(row.key) + } + }} + > + {/* Why: grab cursor on icon+title only. Row still has handle attrs so + indent/padding can arm drag; actions are excluded via data-repo-header-actions. + self-stretch fills h-7 so grab matches the full title column height. */} +
+ {row.icon ? ( +
+ {row.repo ? ( + + ) : ( + + )} +
+ ) : null} + +
+
+
+ {row.label} +
+ + +
+
+
+ + + {showHeaderCollapseAffordance ? ( +
{ + event.preventDefault() + event.stopPropagation() + ctx.toggleGroupWithScrollAnchor(row.key) + }} + > + +
+ ) : null} + + {isProjectGroupHeader && !row.repo && projectGroupIdForHeader ? ( + + ) : null} + + {folderBackedProjectGroup ? ( + + ) : null} + + {row.repo && ctx.groupBy === 'repo' ? ( + + ) : null} + + {row.repo && ctx.groupBy === 'repo' ? ( + + ) : null} +
+
+
+ ) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/worktree-sidebar-scroll-tuning.ts b/src/renderer/src/components/sidebar/worktree-list/worktree-sidebar-scroll-tuning.ts new file mode 100644 index 00000000000..bb404507e26 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/worktree-sidebar-scroll-tuning.ts @@ -0,0 +1,42 @@ +import type React from 'react' + +// Debounce re-sort after a sortEpoch bump so background score changes don't jar row positions. +export const SORT_SETTLE_MS = 3_000 +export const USER_SCROLL_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS = 500 +export const EXPANDING_CARD_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS = 300 +export const SIDEBAR_POINTER_DRAG_THRESHOLD_PX = 4 + +export const WORKTREE_SIDEBAR_SCROLL_STYLE: React.CSSProperties = { + // Why: TanStack Virtual owns scroll correction; native overflow anchoring fights it and causes jumps. + overflowAnchor: 'none' +} + +const recordKeyCountCache = new WeakMap, number>() + +export function countRecordKeysByReference(record: Record): number { + const cached = recordKeyCountCache.get(record) + if (cached !== undefined) { + return cached + } + const count = Object.keys(record).length + recordKeyCountCache.set(record, count) + return count +} + +export function shouldAdjustWorktreeSidebarMeasuredRowScroll(args: { + isScrolling: boolean + now: number + suppressUntil: number +}): boolean { + return !args.isScrolling && args.now >= args.suppressUntil +} + +export function resolvePendingSidebarReveal(args: { + targetIndex: number + targetWorktreeStillExists: boolean +}): 'scroll-and-clear' | 'clear' | 'keep-pending' { + if (args.targetIndex !== -1) { + return 'scroll-and-clear' + } + return args.targetWorktreeStillExists ? 'keep-pending' : 'clear' +} diff --git a/src/renderer/src/components/sidebar/worktree-list/worktree-virtual-row-context.ts b/src/renderer/src/components/sidebar/worktree-list/worktree-virtual-row-context.ts new file mode 100644 index 00000000000..122bf81fc35 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/worktree-virtual-row-context.ts @@ -0,0 +1,149 @@ +import type React from 'react' +import type { AppState } from '@/store/types' +import type { ProjectGroup } from '../../../../../shared/project-group-types' +import type { RenderRow } from '../worktree-list-virtual-rows' +import type { LineageToggleHandler } from '../worktree-lineage-toggle-handler-cache' +import type { SidebarRevealHighlight } from './use-sidebar-reveal-highlight' +import type { WorktreeDragRuntime } from './use-worktree-drag-runtime' +import type { WorktreeDragSession } from './use-worktree-drag-session' +import type { useFolderWorkspacePathStatusRows } from './use-folder-workspace-path-status-rows' +import type { usePrimaryActiveWorktreeRow } from './use-primary-active-worktree-row' +import type { useWorkspaceStatusRowDrag } from './use-workspace-status-row-drag' +import type { useWorktreeNativeDrag } from './use-worktree-native-drag' +import type { WorktreeSidebarHeaderDrag } from './use-worktree-sidebar-header-drag' +import type { WorktreeListVirtualizer } from './use-worktree-list-virtualizer' +import type { VirtualizedWorktreeViewportProps } from './virtualized-worktree-viewport-props' +import type { WorktreeVirtualRowContext } from './worktree-virtual-row-dispatch' + +type BuildArgs = { + props: VirtualizedWorktreeViewportProps + projectGroups: readonly ProjectGroup[] + renderRows: RenderRow[] + firstHeaderIndex: number + virtualization: WorktreeListVirtualizer + measureVirtualRowElement: (element: HTMLDivElement | null) => void + settings: AppState['settings'] + sshConnectionStates: AppState['sshConnectionStates'] + newCardStyle: boolean + folderBackedProjectGroupIds: ReadonlySet + session: WorktreeDragSession + runtime: WorktreeDragRuntime + primaryActive: ReturnType + reveal: SidebarRevealHighlight + statusDrag: ReturnType + nativeDrag: ReturnType + headerDrag: WorktreeSidebarHeaderDrag + getCachedFolderWorkspacePathStatus: ReturnType + getLineageToggleHandler: (groupKey: string) => LineageToggleHandler + toggleGroupWithScrollAnchor: (groupKey: string) => void + onRowClickCapture: (event: React.MouseEvent) => void + onRowPointerDown: ( + event: React.PointerEvent, + worktreeId: string, + rowKey: string + ) => void +} + +// Assembles the per-render context the row dispatcher reads. Plain data — everything in it +// is either a prop, a memoised hook result, or a stable callback. +export function buildWorktreeVirtualRowContext(args: BuildArgs): WorktreeVirtualRowContext { + const { props, runtime, session, statusDrag, headerDrag, primaryActive, reveal } = args + return { + renderRows: args.renderRows, + firstHeaderIndex: args.firstHeaderIndex, + activeStickyHeaderIndexRef: args.virtualization.activeStickyHeaderIndexRef, + activeStickyHostIndexRef: args.virtualization.activeStickyHostIndexRef, + measureVirtualRowElement: args.measureVirtualRowElement, + groupBy: props.groupBy, + workspaceStatuses: props.workspaceStatuses, + activeWorktreeId: props.activeWorktreeId, + worktreeDragState: runtime.worktreeDragState, + orderedHostIds: headerDrag.orderedHostIds, + hostDrag: headerDrag.hostDrag, + toggleGroupWithScrollAnchor: args.toggleGroupWithScrollAnchor, + importedWorktreeCardActionState: props.importedWorktreeCardActionState, + newExternalWorktreeInboxActionState: props.newExternalWorktreeInboxActionState, + onShowImportedWorktrees: props.handleShowImportedWorktrees, + onKeepImportedWorktreesHidden: props.handleKeepImportedWorktreesHidden, + onOpenWorktreeVisibility: props.handleOpenWorktreeVisibility, + onOpenSuppressExternalWorktreeInbox: props.handleOpenSuppressExternalWorktreeInbox, + onWorkspaceStatusDragOver: statusDrag.handleWorkspaceStatusDragOver, + onWorkspaceStatusDragLeave: statusDrag.handleWorkspaceStatusDragLeave, + onWorkspaceStatusDrop: statusDrag.handleWorkspaceStatusDrop, + header: { + groupBy: props.groupBy, + collapsedGroups: props.collapsedGroups, + workspaceStatuses: props.workspaceStatuses, + projectGroups: args.projectGroups, + sshConnectionStates: args.sshConnectionStates, + highlightedRevealRowKey: reveal.highlightedRevealRowKey, + dragOverStatus: runtime.dragOverStatus, + pinDragOver: runtime.pinDragOver, + headerDrag, + getCachedFolderWorkspacePathStatus: args.getCachedFolderWorkspacePathStatus, + toggleGroupWithScrollAnchor: args.toggleGroupWithScrollAnchor, + projectActions: { + onOpenRepoSettings: props.handleOpenRepoSettings, + onOpenWorktreeVisibility: props.handleOpenWorktreeVisibility, + onCreateGroupFromRepo: props.handleCreateGroupFromRepo, + onMoveProjectToGroup: props.handleMoveProjectToGroup, + onRemoveProjectFromGroup: props.handleRemoveProjectFromGroup, + onRemoveProject: props.handleRemoveProject, + onCreateForRepo: props.handleCreateForRepo + }, + onRenameProjectGroup: props.handleRenameProjectGroup, + onDeleteProjectGroup: props.handleDeleteProjectGroup, + onCreateFolderWorkspace: props.handleCreateFolderWorkspace, + onWorkspaceStatusDragOver: statusDrag.handleWorkspaceStatusDragOver, + onWorkspaceStatusDragLeave: statusDrag.handleWorkspaceStatusDragLeave, + onWorkspacePinDragOver: statusDrag.handleWorkspacePinDragOver, + onWorkspacePinDragLeave: statusDrag.handleWorkspacePinDragLeave, + onWorkspaceStatusDrop: statusDrag.handleWorkspaceStatusDrop + }, + item: { + settings: args.settings, + groupBy: props.groupBy, + folderBackedProjectGroupIds: args.folderBackedProjectGroupIds, + groupKeyByRowKey: session.groupKeyByRowKey, + groupIndexByRowKey: session.groupIndexByRowKey, + agentSendTargetWorktreeId: props.agentSendTargetWorktreeId, + worktreeDragState: runtime.worktreeDragState, + worktreePointerDragRef: runtime.worktreePointerDragRef, + nativeLineageDropTargetId: runtime.nativeLineageDropTargetId, + activeWorktreeId: props.activeWorktreeId, + currentWorktreeId: props.currentWorktreeId, + highlightedRevealRowKey: reveal.highlightedRevealRowKey, + selectedWorktreeIds: props.selectedWorktreeIds, + selectedWorktrees: props.selectedWorktrees, + getActiveSurfaceVariant: primaryActive.getActiveSurfaceVariant, + getLineageToggleHandler: args.getLineageToggleHandler, + onSelectionGesture: props.onSelectionGesture, + onContextMenuSelect: props.onContextMenuSelect, + onImmediateActivate: primaryActive.handleImmediateWorktreeRowActivate, + onRowClickCapture: args.onRowClickCapture, + onRowPointerDown: args.onRowPointerDown, + onCardDragStart: args.nativeDrag.handleWorktreeCardDragStart, + onCardDragEnd: runtime.clearWorktreeDrag + }, + folderWorkspace: { + groupBy: props.groupBy, + newCardStyle: args.newCardStyle, + settings: args.settings, + activeWorktreeId: props.activeWorktreeId, + currentWorktreeId: props.currentWorktreeId, + selectedWorktreeIds: props.selectedWorktreeIds, + repoMap: props.repoMap, + worktreeMap: props.worktreeMap, + worktreeLineageById: props.worktreeLineageById, + workspaceLineageByChildKey: props.workspaceLineageByChildKey, + prCache: props.prCache, + hostedReviewCache: props.hostedReviewCache, + getCachedFolderWorkspacePathStatus: args.getCachedFolderWorkspacePathStatus, + onSelectionGesture: props.onSelectionGesture, + onContextMenuSelect: props.onContextMenuSelect, + onImmediateActivate: primaryActive.handleImmediateWorktreeRowActivate, + onRowClickCapture: args.onRowClickCapture, + onRowPointerDown: args.onRowPointerDown + } + } +} diff --git a/src/renderer/src/components/sidebar/worktree-list/worktree-virtual-row-dispatch.tsx b/src/renderer/src/components/sidebar/worktree-list/worktree-virtual-row-dispatch.tsx new file mode 100644 index 00000000000..b17c81439d9 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/worktree-virtual-row-dispatch.tsx @@ -0,0 +1,262 @@ +import React from 'react' +import type { VirtualItem } from '@tanstack/react-virtual' +import { cn } from '@/lib/utils' +import type { + WorkspaceStatus, + WorkspaceStatusDefinition +} from '../../../../../shared/worktree/types' +import { getWorkspaceStatus } from '../workspace-status' +import type { WorktreeGroupBy } from '../worktree-list-groups' +import { + getVirtualRowTransform, + shouldUseHeaderTopSpacing, + type RenderRow +} from '../worktree-list-virtual-rows' +import type { ImportedWorktreeCardActionState } from '../imported-worktrees-card-actions' +import type { NewExternalWorktreesInboxActionState } from '../new-external-worktrees-inbox-actions' +import type { useHostHeaderDrag } from '../host-header-drag' +import { HostSectionHeader } from './HostSectionHeader' +import { + renderFolderWorkspaceVirtualRow, + type FolderWorkspaceRowContext +} from './folder-workspace-virtual-row' +import { + renderImportedWorktreesVirtualRow, + renderNewExternalWorktreesInboxVirtualRow, + renderPendingCreationVirtualRow +} from './worktree-notice-virtual-rows' +import { + renderWorktreeItemRow, + renderWorktreeLineageDescendants, + type WorktreeItemRowContext +} from './worktree-list-item-row' +import { + renderWorktreeSectionHeaderRow, + type SectionHeaderRowContext +} from './worktree-section-header-row' +import { getWorktreeVirtualRowTransform } from './virtual-row-dom-attributes' +import type { WorktreeRowDragState } from './worktree-row-drag-state' + +export type WorktreeVirtualRowContext = { + renderRows: RenderRow[] + firstHeaderIndex: number + activeStickyHeaderIndexRef: React.MutableRefObject + activeStickyHostIndexRef: React.MutableRefObject + measureVirtualRowElement: (element: HTMLDivElement | null) => void + groupBy: WorktreeGroupBy + workspaceStatuses: readonly WorkspaceStatusDefinition[] + activeWorktreeId: string | null + worktreeDragState: WorktreeRowDragState + orderedHostIds: readonly string[] + hostDrag: ReturnType + toggleGroupWithScrollAnchor: (groupKey: string) => void + header: SectionHeaderRowContext + item: WorktreeItemRowContext + folderWorkspace: FolderWorkspaceRowContext + importedWorktreeCardActionState: ReadonlyMap + newExternalWorktreeInboxActionState: ReadonlyMap + onShowImportedWorktrees: (projectId: string) => void + onKeepImportedWorktreesHidden: (projectId: string) => void + onOpenWorktreeVisibility: ( + repo: Parameters[0] + ) => void + onOpenSuppressExternalWorktreeInbox: (projectId: string) => void + onWorkspaceStatusDragOver: (event: React.DragEvent, status: WorkspaceStatus) => void + onWorkspaceStatusDragLeave: (event: React.DragEvent) => void + onWorkspaceStatusDrop: (event: React.DragEvent, status: WorkspaceStatus) => void +} + +function renderHostHeaderVirtualRow( + ctx: WorktreeVirtualRowContext, + row: Extract, + vItem: VirtualItem +): React.JSX.Element { + // Why: the host card is the outer tier; it pins above group headers (z-30 vs z-20) and stays put as they hand off. + const isActiveStickyHost = ctx.activeStickyHostIndexRef.current === vItem.index + const hasHeaderTopSpacing = shouldUseHeaderTopSpacing({ + rows: ctx.renderRows, + index: vItem.index, + firstHeaderIndex: ctx.firstHeaderIndex + }) + return ( +
+ ctx.toggleGroupWithScrollAnchor(row.key)} + onDragPointerDown={ + ctx.orderedHostIds.length > 1 + ? (e) => ctx.hostDrag.onHandlePointerDown(e, row.hostId) + : undefined + } + dragging={ctx.hostDrag.state.draggingHostId === row.hostId} + /> +
+ ) +} + +function renderLineageGroupVirtualRow( + ctx: WorktreeVirtualRowContext, + row: Extract, + vItem: VirtualItem +): React.JSX.Element { + const [parent, ...children] = row.rows + const childIsActive = children.some((child) => child.worktree.id === ctx.activeWorktreeId) + const parentPreviewOffset = parent + ? (ctx.worktreeDragState.previewOffsetsByWorktreeId.get(parent.worktree.id) ?? 0) + : 0 + return ( +
+
+ {parent + ? renderWorktreeItemRow( + ctx.item, + parent, + false, + renderWorktreeLineageDescendants(ctx.item, parent, children), + childIsActive + ) + : null} +
+
+ ) +} + +// One virtual slot -> one row renderer. Every branch owns its own absolute/sticky frame +// because sticky headers and lineage groups position differently from plain cards. +export function renderWorktreeVirtualRow( + ctx: WorktreeVirtualRowContext, + row: RenderRow, + vItem: VirtualItem +): React.JSX.Element | null { + if (row.type === 'host-header') { + return renderHostHeaderVirtualRow(ctx, row, vItem) + } + + if (row.type === 'header') { + return renderWorktreeSectionHeaderRow({ + ctx: ctx.header, + row, + vItem, + isActiveStickyHeader: ctx.activeStickyHeaderIndexRef.current === vItem.index, + hasStickyHost: ctx.activeStickyHostIndexRef.current !== null, + hasHeaderTopSpacing: shouldUseHeaderTopSpacing({ + rows: ctx.renderRows, + index: vItem.index, + firstHeaderIndex: ctx.firstHeaderIndex + }), + measureVirtualRowElement: ctx.measureVirtualRowElement + }) + } + + if (row.type === 'lineage-group') { + return renderLineageGroupVirtualRow(ctx, row, vItem) + } + + if (row.type === 'imported-worktrees-card') { + return renderImportedWorktreesVirtualRow({ + row, + vItem, + measureVirtualRowElement: ctx.measureVirtualRowElement, + actionState: ctx.importedWorktreeCardActionState.get(row.repo.id), + onShow: ctx.onShowImportedWorktrees, + onKeepHidden: ctx.onKeepImportedWorktreesHidden + }) + } + + if (row.type === 'new-external-worktrees-inbox') { + return renderNewExternalWorktreesInboxVirtualRow({ + row, + vItem, + measureVirtualRowElement: ctx.measureVirtualRowElement, + actionState: ctx.newExternalWorktreeInboxActionState.get(row.repo.id), + onReview: ctx.onOpenWorktreeVisibility, + onSuppress: ctx.onOpenSuppressExternalWorktreeInbox + }) + } + + if (row.type === 'pending-creation') { + return renderPendingCreationVirtualRow({ + row, + vItem, + measureVirtualRowElement: ctx.measureVirtualRowElement + }) + } + + if (row.type === 'folder-workspace') { + return renderFolderWorkspaceVirtualRow({ + ctx: ctx.folderWorkspace, + row, + vItem, + measureVirtualRowElement: ctx.measureVirtualRowElement + }) + } + + const itemWorkspaceStatus = + ctx.groupBy === 'workspace-status' + ? getWorkspaceStatus(row.worktree, ctx.workspaceStatuses) + : null + const itemPreviewOffset = + ctx.worktreeDragState.previewOffsetsByWorktreeId.get(row.worktree.id) ?? 0 + + return ( +
ctx.onWorkspaceStatusDragOver(event, itemWorkspaceStatus) + : undefined + } + onDragLeave={itemWorkspaceStatus ? ctx.onWorkspaceStatusDragLeave : undefined} + onDrop={ + itemWorkspaceStatus + ? (event) => ctx.onWorkspaceStatusDrop(event, itemWorkspaceStatus) + : undefined + } + > + {renderWorktreeItemRow(ctx.item, row, false)} +
+ ) +} diff --git a/src/renderer/src/components/sidebar/worktree-list/worktree-visibility-menu-label.ts b/src/renderer/src/components/sidebar/worktree-list/worktree-visibility-menu-label.ts new file mode 100644 index 00000000000..666cd5121d8 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/worktree-visibility-menu-label.ts @@ -0,0 +1,13 @@ +import type { Repo } from '../../../../../shared/repo-types' +import { + effectiveExternalWorktreeVisibility, + isLegacyRepoForExternalWorktreeVisibility +} from '../../../../../shared/worktree/ownership' + +export function getWorktreeVisibilityMenuLabel(repo: Repo): string { + const visibility = effectiveExternalWorktreeVisibility( + repo, + isLegacyRepoForExternalWorktreeVisibility(repo) + ) + return visibility === 'show' ? 'Hide non-Orca worktrees' : 'Show hidden worktrees' +} diff --git a/src/renderer/src/components/sidebar/worktree-list/worktree-visible-refresh-listener.ts b/src/renderer/src/components/sidebar/worktree-list/worktree-visible-refresh-listener.ts new file mode 100644 index 00000000000..b9e77fefb83 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/worktree-visible-refresh-listener.ts @@ -0,0 +1,4 @@ +export function installWorktreeVisibleRefreshVisibilityListener(onChange: () => void): () => void { + document.addEventListener('visibilitychange', onChange) + return () => document.removeEventListener('visibilitychange', onChange) +} diff --git a/src/renderer/src/components/sidebar/worktree-scroll-to-current-button.test.ts b/src/renderer/src/components/sidebar/worktree-scroll-to-current-button.test.ts index 933261f20a4..fb984cb2d54 100644 --- a/src/renderer/src/components/sidebar/worktree-scroll-to-current-button.test.ts +++ b/src/renderer/src/components/sidebar/worktree-scroll-to-current-button.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { getScrollTopToRevealBounds } from './WorktreeList' +import { getScrollTopToRevealBounds } from './worktree-sidebar-reveal' describe('getScrollTopToRevealBounds', () => { const makeContainer = (scrollTop: number, clientHeight: number) => diff --git a/src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts b/src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts index 328ffc95739..bf7e6c75bee 100644 --- a/src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts +++ b/src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts @@ -148,10 +148,13 @@ describe('shouldHandleTerminalInterruptKeyboardEvent', () => { // And a non-Latin key on some other physical key must not become an interrupt. it('does not handle a non-Latin logical key on a physical key other than KeyC', () => { expect( - shouldHandleTerminalInterruptKeyboardEvent(event({ key: 'ㅁ', code: 'KeyA', ctrlKey: true }), { - isMac: true, - hasSelection: false - }) + shouldHandleTerminalInterruptKeyboardEvent( + event({ key: 'ㅁ', code: 'KeyA', ctrlKey: true }), + { + isMac: true, + hasSelection: false + } + ) ).toBe(false) }) @@ -160,10 +163,13 @@ describe('shouldHandleTerminalInterruptKeyboardEvent', () => { it('uses the layout map when an IME is layered over a Latin layout', () => { _setLayoutMapForTests(new Map([['KeyC', 'c']])) expect( - shouldHandleTerminalInterruptKeyboardEvent(event({ key: 'ㅊ', code: 'KeyC', ctrlKey: true }), { - isMac: true, - hasSelection: false - }) + shouldHandleTerminalInterruptKeyboardEvent( + event({ key: 'ㅊ', code: 'KeyC', ctrlKey: true }), + { + isMac: true, + hasSelection: false + } + ) ).toBe(true) }) @@ -172,10 +178,13 @@ describe('shouldHandleTerminalInterruptKeyboardEvent', () => { it('declines when the layout map shows the physical key is not C', () => { _setLayoutMapForTests(new Map([['KeyC', 'j']])) expect( - shouldHandleTerminalInterruptKeyboardEvent(event({ key: 'ㅊ', code: 'KeyC', ctrlKey: true }), { - isMac: true, - hasSelection: false - }) + shouldHandleTerminalInterruptKeyboardEvent( + event({ key: 'ㅊ', code: 'KeyC', ctrlKey: true }), + { + isMac: true, + hasSelection: false + } + ) ).toBe(false) }) From fcd99fc032187b7627c656ee587dbab15d00fa5c Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:40:04 -0700 Subject: [PATCH 14/42] fix(sidebar): satisfy extracted hook quality gates (#14467) --- .../sidebar/worktree-list/use-pending-sidebar-reveal.ts | 4 ---- .../sidebar/worktree-list/use-primary-active-worktree-row.ts | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/components/sidebar/worktree-list/use-pending-sidebar-reveal.ts b/src/renderer/src/components/sidebar/worktree-list/use-pending-sidebar-reveal.ts index 7ba04fb5149..26cd2b352db 100644 --- a/src/renderer/src/components/sidebar/worktree-list/use-pending-sidebar-reveal.ts +++ b/src/renderer/src/components/sidebar/worktree-list/use-pending-sidebar-reveal.ts @@ -136,8 +136,6 @@ export function usePendingSidebarReveal(args: PendingSidebarRevealArgs): void { cancelled = true cancelPendingRevealFrames() } - // Why: the effect re-reads live inputs through argsRef; only reveal-relevant identities should retrigger it. - // oxlint-disable-next-line react-hooks/exhaustive-deps }, [ pendingRevealWorktree, args.agentSendTargetWorktreeId, @@ -265,8 +263,6 @@ export function usePendingSidebarReveal(args: PendingSidebarRevealArgs): void { cancelled = true cancelPendingRevealFrames() } - // Why: the effect re-reads live inputs through argsRef; only reveal-relevant identities should retrigger it. - // oxlint-disable-next-line react-hooks/exhaustive-deps }, [ pendingRevealSidebarRow, args.repoMap, diff --git a/src/renderer/src/components/sidebar/worktree-list/use-primary-active-worktree-row.ts b/src/renderer/src/components/sidebar/worktree-list/use-primary-active-worktree-row.ts index 698d0b6d7c8..e3e30642c8e 100644 --- a/src/renderer/src/components/sidebar/worktree-list/use-primary-active-worktree-row.ts +++ b/src/renderer/src/components/sidebar/worktree-list/use-primary-active-worktree-row.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useLayoutEffect, useState } from 'react' import type { ActiveSurfaceVariant } from '../WorktreeCard' import type { HostSectionRow } from '../host-section-rows' import type { PinnedWorktreeDisplayPolicy } from '../worktree-list-groups' @@ -18,7 +18,7 @@ export function usePrimaryActiveWorktreeRow(args: { rowKey: string } | null>(null) - useEffect(() => { + useLayoutEffect(() => { if (activeWorktreeId === null) { setPrimaryActiveWorktreeRow(null) return From 2e91a74debba4ffbb942b9d8a4d728f9aa001234 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:44:43 -0700 Subject: [PATCH 15/42] fix(renderer): stop git history hover from showing two tooltips (#14468) Remove native title attributes from commit rows, subjects, and ref badges so only the managed tooltip appears. --- .../right-sidebar/GitHistoryPanel.test.tsx | 29 +++++++++++++++++++ .../right-sidebar/GitHistoryRow.tsx | 12 ++------ tests/e2e/git-history-tooltip-wrap.spec.ts | 2 ++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/components/right-sidebar/GitHistoryPanel.test.tsx b/src/renderer/src/components/right-sidebar/GitHistoryPanel.test.tsx index 07129f53077..33fc4565527 100644 --- a/src/renderer/src/components/right-sidebar/GitHistoryPanel.test.tsx +++ b/src/renderer/src/components/right-sidebar/GitHistoryPanel.test.tsx @@ -76,4 +76,33 @@ describe('GitHistoryPanel', () => { expect(markup).toContain('Fix tab overflow') expect(markup).toContain('52ad492') }) + + it('does not add native title tooltips alongside managed history tooltips', () => { + const result = makeHistoryResult() + result.items[0].references = [ + { id: 'refs/heads/main', name: 'main', category: 'branches' }, + { id: 'refs/heads/feature', name: 'feature', category: 'branches' }, + { id: 'refs/tags/v1.0.0', name: 'v1.0.0', category: 'tags' } + ] + + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('Fix tab overflow') + expect(markup).toContain('main') + expect(markup).toContain('feature') + expect(markup).toContain('+1') + expect(markup).not.toContain('title="Fix tab overflow"') + expect(markup).not.toContain('title="main"') + expect(markup).not.toContain('title="feature"') + expect(markup).not.toContain('title="v1.0.0"') + expect(markup).not.toMatch(/\stitle=/) + }) }) diff --git a/src/renderer/src/components/right-sidebar/GitHistoryRow.tsx b/src/renderer/src/components/right-sidebar/GitHistoryRow.tsx index f41ea2f92b4..f2c3f020178 100644 --- a/src/renderer/src/components/right-sidebar/GitHistoryRow.tsx +++ b/src/renderer/src/components/right-sidebar/GitHistoryRow.tsx @@ -20,7 +20,6 @@ function GitHistoryRefBadge({ itemRef }: { itemRef: GitHistoryItemRef }): React. borderColor: itemRef.color ? graphColor(itemRef.color) : 'var(--border)', color: itemRef.color ? graphColor(itemRef.color) : 'var(--muted-foreground)' }} - title={itemRef.name} > {itemRef.name} @@ -90,9 +89,7 @@ export const GitHistoryRow = React.forwardRef( )} - - {item.subject} - + {item.subject} ( {hiddenRefs.length > 0 && ( - ref.name).join(', ')} - > + +{hiddenRefs.length} @@ -135,7 +129,6 @@ export const GitHistoryRow = React.forwardRef( {...rootProps} ref={ref as React.Ref} className={rowClassName} - title={rowTooltip} data-testid="git-history-row" > {rowContent} @@ -157,7 +150,6 @@ export const GitHistoryRow = React.forwardRef( ref={ref as React.Ref} type="button" className={rowClassName} - title={rowTooltip} aria-expanded={canExpand ? expanded : undefined} aria-label={ canExpand diff --git a/tests/e2e/git-history-tooltip-wrap.spec.ts b/tests/e2e/git-history-tooltip-wrap.spec.ts index 6a8c4f5895b..f2f0fa50b36 100644 --- a/tests/e2e/git-history-tooltip-wrap.spec.ts +++ b/tests/e2e/git-history-tooltip-wrap.spec.ts @@ -73,7 +73,9 @@ test('keeps conventional commit-message lines intact in the history tooltip', as const row = orcaPage.getByTestId('git-history-row').filter({ hasText: subject }) await expect(row).toBeVisible({ timeout: 10_000 }) + await expect(row).not.toHaveAttribute('title') const trigger = row.locator('[data-slot="tooltip-trigger"]').filter({ hasText: subject }) + await expect(trigger).not.toHaveAttribute('title') await trigger.hover({ position: { x: 20, y: 10 } }) await trigger.hover({ position: { x: 40, y: 10 } }) From f4e4b5aded982031aaf4d64b7e4fa0e153315073 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 06:46:22 +0000 Subject: [PATCH 16/42] Update README downloads badge --- docs/assets/readme-downloads.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/assets/readme-downloads.svg b/docs/assets/readme-downloads.svg index 610bdb8b92e..7a9c2cb494e 100644 --- a/docs/assets/readme-downloads.svg +++ b/docs/assets/readme-downloads.svg @@ -1,5 +1,5 @@ - - downloads: 19m + + downloads: 20m @@ -15,7 +15,7 @@ downloads downloads - 19m - 19m + 20m + 20m From 9e5ee5ef8eb2e6a70d8c9a2b13d11d05bea399ea Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:47:20 -0700 Subject: [PATCH 17/42] feat(workspaces): rework cleanup discovery and dialog (#13413) * fix(workspaces): support full cleanup scans * feat(workspaces): persist cleanup snapshots * feat(workspaces): add cleanup filter model * refactor(workspaces): remove cleanup presets * feat(workspaces): rework cleanup dialog * fix(workspaces): keep cleanup row ordering render-pure * refactor(workspaces): simplify cleanup browsing * refactor(workspaces): show cleanup facts * refactor(workspaces): surface cleanup row facts * fix(workspaces): remove misleading cleanup count * fix(workspaces): preserve full scan semantics * fix(workspaces): scope snapshot persistence * fix(workspaces): preserve cleanup browse compatibility * fix(workspaces): reconcile cleanup dialog state * test(workspaces): update snapshot store fixtures * test(workspaces): preserve cleanup scan modes * perf(workspace-cleanup): stream scan progress and size results * fix(workspace-cleanup): address review feedback * fix(workspace-cleanup): preserve host-scoped cleanup metadata * fix(workspace-cleanup): declare review source dependencies * fix(workspace-cleanup): align size scan banner * fix(workspace-cleanup): shorten scan action * perf(workspace-cleanup): avoid redundant scan IO * perf(workspace-cleanup): bound restarted evidence scans * fix(workspace-cleanup): satisfy scan queue lint * perf(workspace-cleanup): bound scan and snapshot work * perf(workspace-cleanup): serialize final enrichment * test(workspace-cleanup): assert final enrichment drain * fix(workspace-cleanup): stop progress after renderer teardown * perf: batch workspace cleanup git evidence scans * perf(workspace-cleanup): stop redundant snapshot and scan work * fix(workspace-cleanup): resolve review findings across scan, store, and dialog Correctness: - Chunk git-evidence dispatches at the shared 500-target limit and exclude queued/in-flight ids from target selection, so fleets past the limit can no longer strand rows permanently mislabeled as checked-but-unknown. - Key destructive selection pruning on the user's filter state instead of the per-tick matched-set identity; streaming reclassification no longer silently deselects rows. - Clamp the facet clock to max(scannedAt, open time): a stale hydrated snapshot no longer misbuckets idle thresholds or keeps dead agents fresh; row labels use the same clock. - Supersede and cancel the previous broad scan when a new one starts (renderer registry and same-sender guard in main) instead of racing two fleet scans. - Gate snapshot persistence on hasTargetedWorkspaceCleanupScan so worktreeIds: [] can never persist an empty fleet snapshot. - Re-apply dismissals at set-time in progress application so a dismissal landing mid-enrichment is not clobbered. - Record a one-off local snapshot prune for single (unbatched) remote deletes so removed workspaces cannot resurrect from cache. - Strip .exe when normalizing foreground process names so Windows agent processes match. Performance: - Cache per-candidate facet and review-info objects on candidate identity; no-op streaming ticks reuse the previous rows array and skip every downstream pass; matched-set identity is stable under equal membership. - Compute facet counts/options only while the filter popover is open. - Equality-bail git-evidence publishes; structural (non-stringify) facet-group comparison memoized in the toolbar. - Identity-token fast path for the enrichment cache (cache hits skip both JSON.stringify signatures); prune viewed/dismissal records on removal and expiry; bound the superseded-scan-id set. - Restore the no-op bail in removeWorkspaceSpaceWorktrees (regression). - Abort main-side scans when the renderer is destroyed; module-scope controller maps survive handler re-registration. - Batch removal preflight into one targeted scan (with refreshActivity) per 500 ids instead of one scan per row. - Scan repos at concurrency 2, report discovered counts upfront for honest progress, share fs-activity probes per path (folder workspaces), read only the reflog tail, and skip the snapshot read-before-write via a remembered scannedAt. Split workspace-cleanup-worktree-listing, workspace-cleanup-facet-row-caches, and workspace-cleanup-selection-model out of files that crossed max-lines. * fix(workspace-cleanup): address verifier findings - Fall back to a full reflog read when the newest record exceeds the 8KB tail window, so an oversized subject cannot hide recent ref activity. - Bound the single-removal snapshot prune batch id with a UUID; embedding the unbounded worktreeId silently failed main's 128-char validation and skipped the prune for long remote ids. - Key the main-side broad-scan supersession by sender AND scan mode so legacy suggestion-only and full-workspace scans stay isolated, matching the renderer registry. * fix(workspace-cleanup): own facet caches with useMemo instead of render-time ref writes React Doctor (CI changed-lines gate) correctly flagged the three cache refs written during render. Each per-candidate cache now lives in one memo with the derived context it is keyed on, so the memo deps are the invalidation and interior fills stay content-addressed; the matched-set identity stabilization is dropped since its only consumer reads through a useEffectEvent and never keys on identity. --- config/max-lines-baseline.txt | 1 - config/scripts/locale-ko-value-overrides.mjs | 2 +- .../ipc/workspace-cleanup-activity.test.ts | 34 +- src/main/ipc/workspace-cleanup-activity.ts | 91 +- .../ipc/workspace-cleanup-broad-scan.test.ts | 453 +++++ src/main/ipc/workspace-cleanup-candidate.ts | 8 +- .../ipc/workspace-cleanup-disconnected-ssh.ts | 31 +- .../workspace-cleanup-folder-workspaces.ts | 23 + .../ipc/workspace-cleanup-git-evidence.ts | 25 +- ...orkspace-cleanup-process-preflight.test.ts | 112 ++ .../ipc/workspace-cleanup-progress-emitter.ts | 83 + .../ipc/workspace-cleanup-scan-eligibility.ts | 35 + .../ipc/workspace-cleanup-scan-primitives.ts | 28 +- .../ipc/workspace-cleanup-scan-targets.ts | 31 + src/main/ipc/workspace-cleanup-scan.ts | 342 ++-- .../workspace-cleanup-snapshot-ipc.test.ts | 281 +++ .../ipc/workspace-cleanup-worktree-listing.ts | 76 + src/main/ipc/workspace-cleanup.test.ts | 74 +- src/main/ipc/workspace-cleanup.ts | 155 +- src/main/ipc/workspace-space.test.ts | 104 +- src/main/ipc/workspace-space.ts | 34 +- src/main/ipc/worktrees-windows.test.ts | 1 + src/main/ipc/worktrees.test.ts | 100 +- src/main/ipc/worktrees.ts | 99 +- src/main/persistence.test.ts | 24 + src/main/persistence.ts | 9 + .../runtime/rpc/methods/client-ui-schemas.ts | 14 +- .../methods/ui-state-schema-parity-checks.ts | 17 + .../workspace-cleanup-ui-schema.test.ts | 66 + .../methods/workspace-cleanup-ui-schema.ts | 29 + src/main/sidecar-snapshot-file.ts | 51 + .../attach-main-window-services.test.ts | 3 +- ...ace-cleanup-removal-snapshot-prune.test.ts | 126 ++ ...orkspace-cleanup-removal-snapshot-prune.ts | 138 ++ .../workspace-cleanup-scan-snapshot.test.ts | 450 +++++ src/main/workspace-cleanup-scan-snapshot.ts | 313 ++++ src/main/workspace-snapshot-prune-index.ts | 57 + .../workspace-snapshot-pruning.bench.test.ts | 153 ++ .../workspace-space-analysis-snapshot.test.ts | 438 +++++ src/main/workspace-space-analysis-snapshot.ts | 328 ++++ src/main/workspace-space-analysis.test.ts | 20 +- src/main/workspace-space-analysis.ts | 23 +- src/preload/api/workspace-cleanup-api.ts | 13 +- src/preload/api/worktree-api.ts | 2 + src/preload/index.ts | 11 +- .../delete-worktree-parallel-flow.test.ts | 40 +- .../sidebar/worktree-delete-execution.ts | 91 +- .../sidebar/worktree-delete-request.ts | 1 + .../sidebar/worktree-snapshot-prune-batch.ts | 24 + .../status-bar/ResourceUsageStatusSegment.tsx | 10 +- .../status-bar/WorkspaceSpaceManagerPanel.tsx | 21 +- ...rkspaceCleanupDialog.mount-gating.test.tsx | 376 ++-- ...anupDialog.stale-while-revalidate.test.tsx | 399 +++++ .../WorkspaceCleanupDialog.tsx | 1574 ++++------------- .../inactive-workspace-estimate.test.ts | 70 - .../inactive-workspace-estimate.ts | 38 - .../use-workspace-cleanup-browse-state.ts | 68 + .../use-workspace-cleanup-dialog-lifecycle.ts | 83 + .../use-workspace-cleanup-facet-rows.test.tsx | 298 ++++ .../use-workspace-cleanup-facet-rows.ts | 289 +++ ...se-workspace-cleanup-git-evidence.test.tsx | 192 ++ .../use-workspace-cleanup-git-evidence.ts | 183 ++ .../use-workspace-cleanup-removal.ts | 242 +++ .../use-workspace-cleanup-row-order.test.ts | 89 + .../use-workspace-cleanup-row-order.ts | 123 ++ .../use-workspace-cleanup-scan-lifecycle.ts | 135 ++ .../workspace-cleanup-active-facets.test.ts | 38 + .../workspace-cleanup-active-facets.ts | 70 + ...-background-removal-snapshot-batch.test.ts | 59 + .../workspace-cleanup-background-removal.ts | 152 +- .../workspace-cleanup-browse-toolbar.tsx | 96 + .../workspace-cleanup-candidate-list.tsx | 14 +- ...rkspace-cleanup-candidate-row-data.test.ts | 26 +- .../workspace-cleanup-candidate-row-data.ts | 34 +- .../workspace-cleanup-candidate-row.test.tsx | 97 + .../workspace-cleanup-candidate-row.tsx | 135 +- .../workspace-cleanup-confirm-remove.test.tsx | 76 + .../workspace-cleanup-confirm-remove.tsx | 253 +++ .../workspace-cleanup-dialog-header.tsx | 97 + .../workspace-cleanup-dialog-notices.test.tsx | 58 + .../workspace-cleanup-dialog-notices.tsx | 148 ++ .../workspace-cleanup-dialog-session.test.tsx | 175 -- .../workspace-cleanup-dialog-session.ts | 205 --- .../workspace-cleanup-facet-controls.test.tsx | 161 ++ .../workspace-cleanup-facet-controls.tsx | 225 +++ .../workspace-cleanup-facet-filter.test.ts | 518 ++++++ .../workspace-cleanup-facet-labels.ts | 175 ++ .../workspace-cleanup-facet-matchers.ts | 214 +++ .../workspace-cleanup-facet-panel-model.ts | 46 + .../workspace-cleanup-facet-row-caches.ts | 143 ++ .../workspace-cleanup-facet-sort.test.ts | 140 ++ .../workspace-cleanup-facet-sort.ts | 190 ++ .../workspace-cleanup-facet.test.fixture.ts | 126 ++ .../workspace-cleanup-facets.ts | 239 +++ .../workspace-cleanup-filter-bar.test.tsx | 44 + .../workspace-cleanup-filter-bar.tsx | 111 ++ .../workspace-cleanup-filter-sort.ts | 30 +- .../workspace-cleanup-flat-list.test.tsx | 139 ++ .../workspace-cleanup-freshness.tsx | 55 + .../workspace-cleanup-git-evidence.test.ts | 148 ++ .../workspace-cleanup-git-evidence.ts | 101 ++ .../workspace-cleanup-git-review-facets.tsx | 209 +++ .../workspace-cleanup-host-identity.ts | 137 ++ .../workspace-cleanup-lifecycle-facets.tsx | 198 +++ .../workspace-cleanup-metadata-chip.tsx | 41 + .../workspace-cleanup-presentation.test.ts | 71 + .../workspace-cleanup-presentation.ts | 70 +- .../workspace-cleanup-query.ts | 142 ++ .../workspace-cleanup-relative-time.ts | 27 + .../workspace-cleanup-removal-session.ts | 265 --- .../workspace-cleanup-row-list.tsx | 98 + .../workspace-cleanup-scan-notice.ts | 121 ++ .../workspace-cleanup-scan-session.ts | 88 - .../workspace-cleanup-selection-model.ts | 38 + .../workspace-cleanup-sort-header.test.tsx | 95 + .../workspace-cleanup-sort-header.tsx | 101 ++ .../workspace-cleanup-view-selection.test.ts | 94 - .../workspace-cleanup-view-selection.ts | 34 - src/renderer/src/i18n/locales/en.json | 183 +- src/renderer/src/i18n/locales/es.json | 2 +- src/renderer/src/i18n/locales/ja.json | 2 +- src/renderer/src/i18n/locales/ko.json | 2 +- src/renderer/src/i18n/locales/zh.json | 2 +- src/renderer/src/store/index.ts | 2 + .../src/store/slices/diffComments.test.ts | 94 +- .../src/store/slices/store-test-helpers.ts | 2 + src/renderer/src/store/slices/ui.ts | 5 + .../workspace-cleanup-broad-scan-registry.ts | 87 + .../slices/workspace-cleanup-browse.test.ts | 97 + .../store/slices/workspace-cleanup-browse.ts | 54 + .../workspace-cleanup-cache-hydration.test.ts | 196 ++ .../workspace-cleanup-cache-hydration.ts | 41 + ...ace-cleanup-enrichment-performance.test.ts | 485 +++++ ...orkspace-cleanup-removal-preflight.test.ts | 40 +- .../workspace-cleanup-scan-progress.test.ts | 79 +- .../workspace-cleanup-slice-test-harness.ts | 6 +- .../src/store/slices/workspace-cleanup.ts | 480 +++-- .../src/store/slices/workspace-space.test.ts | 43 + .../src/store/slices/workspace-space.ts | 79 +- .../src/store/slices/worktree-helpers.ts | 1 + .../src/store/slices/worktrees.test.ts | 17 +- src/renderer/src/store/slices/worktrees.ts | 40 +- src/renderer/src/store/types.ts | 2 + src/renderer/src/web/web-preload-api.ts | 5 + src/shared/structural-value-equality.ts | 4 +- ...e-cleanup-browse-state-persistence.test.ts | 82 + src/shared/workspace-cleanup-browse-state.ts | 49 + .../workspace-cleanup-facet-rankings.ts | 135 ++ src/shared/workspace-cleanup-filter-model.ts | 180 ++ .../workspace-cleanup-filter-state-codec.ts | 224 +++ src/shared/workspace-cleanup-ui-state.test.ts | 50 + src/shared/workspace-cleanup-ui-state.ts | 15 + src/shared/workspace-cleanup.ts | 27 + src/shared/workspace-space-types.ts | 11 + 154 files changed, 14719 insertions(+), 3325 deletions(-) create mode 100644 src/main/ipc/workspace-cleanup-broad-scan.test.ts create mode 100644 src/main/ipc/workspace-cleanup-folder-workspaces.ts create mode 100644 src/main/ipc/workspace-cleanup-process-preflight.test.ts create mode 100644 src/main/ipc/workspace-cleanup-progress-emitter.ts create mode 100644 src/main/ipc/workspace-cleanup-scan-eligibility.ts create mode 100644 src/main/ipc/workspace-cleanup-scan-targets.ts create mode 100644 src/main/ipc/workspace-cleanup-snapshot-ipc.test.ts create mode 100644 src/main/ipc/workspace-cleanup-worktree-listing.ts create mode 100644 src/main/runtime/rpc/methods/workspace-cleanup-ui-schema.test.ts create mode 100644 src/main/runtime/rpc/methods/workspace-cleanup-ui-schema.ts create mode 100644 src/main/sidecar-snapshot-file.ts create mode 100644 src/main/workspace-cleanup-removal-snapshot-prune.test.ts create mode 100644 src/main/workspace-cleanup-removal-snapshot-prune.ts create mode 100644 src/main/workspace-cleanup-scan-snapshot.test.ts create mode 100644 src/main/workspace-cleanup-scan-snapshot.ts create mode 100644 src/main/workspace-snapshot-prune-index.ts create mode 100644 src/main/workspace-snapshot-pruning.bench.test.ts create mode 100644 src/main/workspace-space-analysis-snapshot.test.ts create mode 100644 src/main/workspace-space-analysis-snapshot.ts create mode 100644 src/renderer/src/components/sidebar/worktree-snapshot-prune-batch.ts create mode 100644 src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.stale-while-revalidate.test.tsx delete mode 100644 src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.test.ts delete mode 100644 src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.ts create mode 100644 src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-browse-state.ts create mode 100644 src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-dialog-lifecycle.ts create mode 100644 src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-facet-rows.test.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-facet-rows.ts create mode 100644 src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.test.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.ts create mode 100644 src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-removal.ts create mode 100644 src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-row-order.test.ts create mode 100644 src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-row-order.ts create mode 100644 src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-scan-lifecycle.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-active-facets.test.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-active-facets.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal-snapshot-batch.test.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-browse-toolbar.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-confirm-remove.test.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-confirm-remove.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-header.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-notices.test.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-notices.tsx delete mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-session.test.tsx delete mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-session.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-controls.test.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-controls.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-filter.test.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-labels.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-matchers.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-panel-model.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-row-caches.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-sort.test.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-sort.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet.test.fixture.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-facets.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-filter-bar.test.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-filter-bar.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-flat-list.test.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-freshness.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-git-evidence.test.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-git-evidence.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-git-review-facets.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-host-identity.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-lifecycle-facets.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-metadata-chip.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-query.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-relative-time.ts delete mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-removal-session.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-row-list.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-scan-notice.ts delete mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-scan-session.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-selection-model.ts create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.test.tsx create mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.tsx delete mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-view-selection.test.ts delete mode 100644 src/renderer/src/components/workspace-cleanup/workspace-cleanup-view-selection.ts create mode 100644 src/renderer/src/store/slices/workspace-cleanup-broad-scan-registry.ts create mode 100644 src/renderer/src/store/slices/workspace-cleanup-browse.test.ts create mode 100644 src/renderer/src/store/slices/workspace-cleanup-browse.ts create mode 100644 src/renderer/src/store/slices/workspace-cleanup-cache-hydration.test.ts create mode 100644 src/renderer/src/store/slices/workspace-cleanup-cache-hydration.ts create mode 100644 src/renderer/src/store/slices/workspace-cleanup-enrichment-performance.test.ts create mode 100644 src/shared/workspace-cleanup-browse-state-persistence.test.ts create mode 100644 src/shared/workspace-cleanup-browse-state.ts create mode 100644 src/shared/workspace-cleanup-facet-rankings.ts create mode 100644 src/shared/workspace-cleanup-filter-model.ts create mode 100644 src/shared/workspace-cleanup-filter-state-codec.ts create mode 100644 src/shared/workspace-cleanup-ui-state.test.ts create mode 100644 src/shared/workspace-cleanup-ui-state.ts diff --git a/config/max-lines-baseline.txt b/config/max-lines-baseline.txt index 839595375eb..c1421ba2499 100644 --- a/config/max-lines-baseline.txt +++ b/config/max-lines-baseline.txt @@ -258,7 +258,6 @@ inline src/renderer/src/components/terminal-pane/terminal-link-handlers.test.ts inline src/renderer/src/components/terminal-pane/use-terminal-pane-context-menu.ts inline src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.test.ts inline src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts -inline src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.tsx inline src/renderer/src/hooks/useAutomationDispatchEvents.ts inline src/renderer/src/hooks/useComposerState.ts inline src/renderer/src/hooks/useEditorExternalWatch.ts diff --git a/config/scripts/locale-ko-value-overrides.mjs b/config/scripts/locale-ko-value-overrides.mjs index 5dac8b29a0d..bcc5b6e9e8e 100644 --- a/config/scripts/locale-ko-value-overrides.mjs +++ b/config/scripts/locale-ko-value-overrides.mjs @@ -173,7 +173,7 @@ export const KO_VALUE_OVERRIDES = { '아래 워크스페이스 오류를 확인한 후 새로고침하세요.', 'Review inactive workspaces before deleting their local files and Orca state.': '로컬 파일과 Orca 상태를 삭제하기 전에 비활성 워크스페이스를 확인하세요.', - 'Review inactive workspaces (': '비활성 워크스페이스 확인(', + 'Clean up workspaces': '워크스페이스 정리', 'Review the settings that will be imported from your Ghostty config.': 'Ghostty 구성에서 가져올 설정을 확인하세요.', 'GitHub requires review approval before this pull request can merge': diff --git a/src/main/ipc/workspace-cleanup-activity.test.ts b/src/main/ipc/workspace-cleanup-activity.test.ts index a7d1e980266..a0b68ccb683 100644 --- a/src/main/ipc/workspace-cleanup-activity.test.ts +++ b/src/main/ipc/workspace-cleanup-activity.test.ts @@ -1,3 +1,5 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import os from 'node:os' import path from 'node:path' import { describe, expect, it, vi } from 'vitest' import type { Repo } from '../../shared/repo-types' @@ -135,10 +137,40 @@ describe('resolveWorkspaceCleanupActivityWorktree', () => { readTextFile ) - expect(readTextFile).toHaveBeenCalledWith(reflogPath) + // Why: only the tail of the reflog is read — the newest entry is enough. + expect(readTextFile).toHaveBeenCalledWith(reflogPath, { tailBytes: 8192 }) expect(worktree.lastActivityAt).toBe(1_700_000_900_000) }) + it('falls back to a full reflog read when the newest entry exceeds the tail window', async () => { + const dir = await mkdtemp(path.join(os.tmpdir(), 'cleanup-activity-')) + try { + const gitDir = path.join(dir, 'gitdir') + await mkdir(path.join(gitDir, 'logs'), { recursive: true }) + const worktreePath = path.join(dir, 'wt') + await mkdir(worktreePath, { recursive: true }) + await writeFile(path.join(worktreePath, '.git'), `gitdir: ${gitDir}\n`) + // Why: a single record longer than the 8192-byte tail window keeps its + // timestamp before the window; the reader must fall back to a full read. + await writeFile( + path.join(gitDir, 'logs', 'HEAD'), + '0000 1111 Dev 1700000000 -0700\tbranch: Created from HEAD\n' + + `1111 2222 Dev 1700000900 -0700\tcommit: ${'x'.repeat(9000)}\n` + ) + const statPath = vi.fn(async () => ({ mtimeMs: 10_000 })) + + const worktree = await resolveWorkspaceCleanupActivityWorktree( + REPO, + makeWorktree({ path: worktreePath }), + statPath + ) + + expect(worktree.lastActivityAt).toBe(1_700_000_900_000) + } finally { + await rm(dir, { recursive: true, force: true }) + } + }) + it('degrades to other probes when the reflog was expired to an empty file', async () => { const gitDirPath = path.join('/repo', '.git', 'worktrees', 'repo-feature') const statPath = vi.fn(async () => ({ mtimeMs: 10_000 })) diff --git a/src/main/ipc/workspace-cleanup-activity.ts b/src/main/ipc/workspace-cleanup-activity.ts index 93a6b5673cd..0d7c44b602c 100644 --- a/src/main/ipc/workspace-cleanup-activity.ts +++ b/src/main/ipc/workspace-cleanup-activity.ts @@ -1,4 +1,4 @@ -import { lstat, readFile } from 'node:fs/promises' +import { lstat, open, readFile } from 'node:fs/promises' import path from 'node:path' import type { Repo } from '../../shared/repo-types' import type { Worktree } from '../../shared/worktree/types' @@ -7,7 +7,11 @@ import { parseWslUncPath } from '../../shared/wsl-paths' import { toWindowsWslPath } from '../wsl' type StatPath = (targetPath: string) => Promise<{ mtimeMs: number }> -type ReadTextFile = (targetPath: string) => Promise +type ReadTextFile = (targetPath: string, options?: { tailBytes?: number }) => Promise + +// Why: only the newest reflog entry matters; a long-lived main worktree's full +// reflog can be megabytes, and it was read and split whole per broad scan. +const REFLOG_TAIL_BYTES = 8192 export function resolvePersistedWorkspaceCleanupActivityWorktree(worktree: Worktree): Worktree { const persistedActivityAt = getPersistedWorkspaceCleanupActivityAt(worktree) @@ -17,13 +21,24 @@ export function resolvePersistedWorkspaceCleanupActivityWorktree(worktree: Workt return { ...worktree, lastActivityAt: persistedActivityAt } } +export type WorkspaceCleanupFsActivityCache = Map> + export async function resolveWorkspaceCleanupActivityWorktree( repo: Repo, worktree: Worktree, statPath: StatPath = statLocalPath, - readTextFile: ReadTextFile = readLocalTextFile + readTextFile: ReadTextFile = readLocalTextFile, + // Why: folder workspaces surface one row per instance of the same directory; + // the filesystem probes are identical per path and must run once per scan. + fsActivityCache?: WorkspaceCleanupFsActivityCache ): Promise { - const activityAt = await resolveWorkspaceCleanupActivityAt(repo, worktree, statPath, readTextFile) + const activityAt = await resolveWorkspaceCleanupActivityAt( + repo, + worktree, + statPath, + readTextFile, + fsActivityCache + ) if (activityAt <= worktree.lastActivityAt) { return worktree } @@ -35,27 +50,48 @@ async function statLocalPath(targetPath: string): Promise<{ mtimeMs: number }> { return { mtimeMs: Number(stats.mtimeMs) } } -async function readLocalTextFile(targetPath: string): Promise { - return readFile(targetPath, 'utf8') +async function readLocalTextFile( + targetPath: string, + options?: { tailBytes?: number } +): Promise { + if (options?.tailBytes === undefined) { + return readFile(targetPath, 'utf8') + } + const handle = await open(targetPath, 'r') + try { + const { size } = await handle.stat() + const length = Math.min(options.tailBytes, size) + if (length === 0) { + return '' + } + const { buffer, bytesRead } = await handle.read({ + buffer: Buffer.alloc(length), + position: size - length + }) + return buffer.toString('utf8', 0, bytesRead) + } finally { + await handle.close() + } } async function resolveWorkspaceCleanupActivityAt( repo: Repo, worktree: Worktree, statPath: StatPath, - readTextFile: ReadTextFile + readTextFile: ReadTextFile, + fsActivityCache?: WorkspaceCleanupFsActivityCache ): Promise { const persistedActivityAt = getPersistedWorkspaceCleanupActivityAt(worktree) if (repo.connectionId) { return persistedActivityAt } - const filesystemActivityAt = await getNewestLocalWorktreeActivityAt( - worktree.path, - statPath, - readTextFile - ) - return Math.max(persistedActivityAt, filesystemActivityAt) + let filesystemActivity = fsActivityCache?.get(worktree.path) + if (!filesystemActivity) { + filesystemActivity = getNewestLocalWorktreeActivityAt(worktree.path, statPath, readTextFile) + fsActivityCache?.set(worktree.path, filesystemActivity) + } + return Math.max(persistedActivityAt, await filesystemActivity) } // Why: best-effort only. Win32 stat over \\wsl.localhost (9P) can falsely @@ -97,20 +133,33 @@ async function readNewestReflogEntryAt( readTextFile: ReadTextFile ): Promise { try { - const lines = (await readTextFile(reflogPath)).split('\n') - for (let index = lines.length - 1; index >= 0; index -= 1) { - // The trailing timezone + tab anchors the capture, so no digit-count floor is needed. - const seconds = /\s(\d{1,11})\s[-+]\d{4}\t/.exec(lines[index] ?? '')?.[1] - if (seconds) { - return Number(seconds) * 1000 - } + // A tail cut can start mid-line; scanning backward for the last parseable + // entry tolerates that. + const tail = await readTextFile(reflogPath, { tailBytes: REFLOG_TAIL_BYTES }) + const newestFromTail = parseNewestReflogEntryAt(tail) + if (newestFromTail !== 0 || tail.length === 0) { + return newestFromTail } - return 0 + // Why: a single record longer than the tail window keeps its timestamp + // before the window starts; fall back to the full read in that rare case. + return parseNewestReflogEntryAt(await readTextFile(reflogPath)) } catch { return 0 } } +function parseNewestReflogEntryAt(reflog: string): number { + const lines = reflog.split('\n') + for (let index = lines.length - 1; index >= 0; index -= 1) { + // The trailing timezone + tab anchors the capture, so no digit-count floor is needed. + const seconds = /\s(\d{1,11})\s[-+]\d{4}\t/.exec(lines[index] ?? '')?.[1] + if (seconds) { + return Number(seconds) * 1000 + } + } + return 0 +} + async function readLocalWorktreeGitDir( worktreePath: string, gitPath: string, diff --git a/src/main/ipc/workspace-cleanup-broad-scan.test.ts b/src/main/ipc/workspace-cleanup-broad-scan.test.ts new file mode 100644 index 00000000000..122f4bdd014 --- /dev/null +++ b/src/main/ipc/workspace-cleanup-broad-scan.test.ts @@ -0,0 +1,453 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import path from 'node:path' +import type { Store } from '../persistence' +import type * as RepoWorktreesModule from '../repo-worktrees' +import type { GitStatusResult } from '../../shared/git-status-types' +import type { Repo } from '../../shared/repo-types' +import type { GitWorktreeInfo } from '../../shared/worktree/types' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' +import { WORKSPACE_CLEANUP_TARGET_BATCH_LIMIT } from '../../shared/workspace-cleanup' + +const { + lstatMock, + readFileMock, + listRepoWorktreesMock, + getStatusMock, + gitExecFileAsyncMock, + getLocalProjectWorktreeGitOptionsMock, + getSshGitProviderMock +} = vi.hoisted(() => ({ + lstatMock: vi.fn(), + readFileMock: vi.fn(), + listRepoWorktreesMock: vi.fn(), + getStatusMock: vi.fn(), + gitExecFileAsyncMock: vi.fn(), + getLocalProjectWorktreeGitOptionsMock: vi.fn(), + getSshGitProviderMock: vi.fn() +})) + +vi.mock('node:fs/promises', () => ({ + lstat: lstatMock, + readFile: readFileMock +})) + +vi.mock('../repo-worktrees', async () => { + const actual = await vi.importActual('../repo-worktrees') + return { + listRepoWorktrees: listRepoWorktreesMock, + createFolderWorktree: actual.createFolderWorktree + } +}) + +vi.mock('../git/status', () => ({ + getStatus: getStatusMock +})) + +vi.mock('../git/runner', () => ({ + gitExecFileAsync: gitExecFileAsyncMock +})) + +vi.mock('../providers/ssh-git-dispatch', () => ({ + getSshGitProvider: getSshGitProviderMock +})) + +vi.mock('../project-runtime-git-options', () => ({ + getLocalProjectWorktreeGitOptions: getLocalProjectWorktreeGitOptionsMock +})) + +import { scanWorkspaceCleanup } from './workspace-cleanup-scan' + +const NOW = 1_700_000_000_000 +const DAY_MS = 24 * 60 * 60 * 1000 +const REPO: Repo = { + id: 'repo-1', + path: '/repo', + displayName: 'Repo', + badgeColor: '#000', + addedAt: NOW, + symlinkPaths: ['node_modules'] +} +const FOLDER_REPO: Repo = { + ...REPO, + id: 'repo-folder', + path: '/folder-workspace', + displayName: 'Folder', + kind: 'folder' +} + +const GIT_WORKTREES: GitWorktreeInfo[] = [ + { + path: '/repo', + head: 'main123', + branch: 'refs/heads/main', + isBare: false, + isMainWorktree: true + }, + { + path: '/repo-old', + head: 'old123', + branch: 'refs/heads/old', + isBare: false, + isMainWorktree: false + }, + { + path: '/repo-recent', + head: 'recent123', + branch: 'refs/heads/recent', + isBare: false, + isMainWorktree: false + } +] + +function makeWorktreeMeta(overrides: Partial = {}): WorktreeMeta { + return { + displayName: '', + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: NOW, + baseRef: 'origin/main', + ...overrides + } as WorktreeMeta +} + +const META_BY_WORKTREE_ID: Record = { + 'repo-1::/repo': makeWorktreeMeta({ lastActivityAt: NOW - 40 * DAY_MS }), + 'repo-1::/repo-old': makeWorktreeMeta({ lastActivityAt: NOW - 40 * DAY_MS }), + 'repo-1::/repo-recent': makeWorktreeMeta({ lastActivityAt: NOW - 2 * DAY_MS }), + 'repo-folder::/folder-workspace': makeWorktreeMeta({ lastActivityAt: NOW - 40 * DAY_MS }) +} + +function makeStore(repos: Repo[] = [REPO], allMeta: Record = {}): Store { + return { + getRepos: () => repos, + getWorktreeMeta: (worktreeId: string) => META_BY_WORKTREE_ID[worktreeId] ?? allMeta[worktreeId], + getAllWorktreeMeta: () => allMeta, + getGitHubCache: () => ({ pr: {}, issue: {} }) + } as unknown as Store +} + +describe('workspace cleanup broad scan opt-in', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(NOW) + lstatMock.mockReset().mockResolvedValue({ mtimeMs: 0 }) + readFileMock.mockReset().mockRejectedValue(new Error('not a gitdir pointer')) + listRepoWorktreesMock.mockReset().mockResolvedValue(GIT_WORKTREES) + getStatusMock.mockReset().mockResolvedValue({ + entries: [], + conflictOperation: 'unknown', + upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 } + } satisfies GitStatusResult) + gitExecFileAsyncMock.mockReset().mockResolvedValue({ stdout: '0\n', stderr: '' }) + getLocalProjectWorktreeGitOptionsMock.mockReset().mockReturnValue({}) + getSshGitProviderMock.mockReset().mockReturnValue(undefined) + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('emits every worktree when the client opts into the full workspace list', async () => { + const result = await scanWorkspaceCleanup(makeStore(), { includeAllWorkspaces: true }) + + expect(result.errors).toEqual([]) + expect(result.candidates.map((candidate) => candidate.worktreeId)).toEqual([ + 'repo-1::/repo', + 'repo-1::/repo-old', + 'repo-1::/repo-recent' + ]) + }) + + it('preserves a worktree runtime host on cleanup candidates', async () => { + const runtimeMeta = makeWorktreeMeta({ + lastActivityAt: NOW - 40 * DAY_MS, + hostId: 'runtime:env-1' + }) + const store = { + ...makeStore(), + getWorktreeMeta: (worktreeId: string) => + worktreeId === 'repo-1::/repo-old' ? runtimeMeta : META_BY_WORKTREE_ID[worktreeId] + } as Store + + const result = await scanWorkspaceCleanup(store, { includeAllWorkspaces: true }) + + expect( + result.candidates.find((candidate) => candidate.worktreeId === 'repo-1::/repo-old') + ?.executionHostId + ).toBe('runtime:env-1') + }) + + it('keeps the legacy suggestion-only projection when the flag is absent', async () => { + const result = await scanWorkspaceCleanup(makeStore()) + + expect(result.candidates.map((candidate) => candidate.worktreeId)).toEqual([ + 'repo-1::/repo-old' + ]) + }) + + it('reports recent workspaces with no cleanup reasons', async () => { + const result = await scanWorkspaceCleanup(makeStore(), { includeAllWorkspaces: true }) + + const recent = result.candidates.find( + (candidate) => candidate.worktreeId === 'repo-1::/repo-recent' + ) + expect(recent).toMatchObject({ + reasons: [], + tier: 'review', + selectedByDefault: false, + lastActivityAt: NOW - 2 * DAY_MS + }) + }) + + it('reports the main worktree with a main-worktree blocker', async () => { + const result = await scanWorkspaceCleanup(makeStore(), { includeAllWorkspaces: true }) + + const main = result.candidates.find((candidate) => candidate.worktreeId === 'repo-1::/repo') + expect(main).toMatchObject({ + tier: 'protected', + selectedByDefault: false, + blockers: ['main-worktree'] + }) + }) + + it('reports folder workspaces with a folder-repo blocker', async () => { + const instanceId = `${FOLDER_REPO.id}::${FOLDER_REPO.path}::workspace:11111111-2222-4333-8444-555555555555` + const instanceMeta = makeWorktreeMeta({ + displayName: 'Folder session', + lastActivityAt: NOW - 2 * DAY_MS + }) + const result = await scanWorkspaceCleanup( + makeStore([FOLDER_REPO], { [instanceId]: instanceMeta }), + { includeAllWorkspaces: true } + ) + + expect(result.candidates).toEqual([ + expect.objectContaining({ + worktreeId: 'repo-folder::/folder-workspace', + tier: 'protected', + selectedByDefault: false, + blockers: expect.arrayContaining(['folder-repo', 'main-worktree']) + }), + expect.objectContaining({ + worktreeId: instanceId, + displayName: 'Folder session', + path: FOLDER_REPO.path, + tier: 'protected', + selectedByDefault: false, + blockers: ['folder-repo'] + }) + ]) + }) + + it('does not mark connected SSH folder workspaces as disconnected', async () => { + const sshFolderRepo: Repo = { ...FOLDER_REPO, connectionId: 'ssh-1' } + getSshGitProviderMock.mockReturnValue({}) + + const result = await scanWorkspaceCleanup(makeStore([sshFolderRepo]), { + includeAllWorkspaces: true + }) + + expect(result.candidates).toHaveLength(1) + expect(result.candidates[0]).toMatchObject({ + worktreeId: 'repo-folder::/folder-workspace', + connectionId: 'ssh-1', + blockers: ['main-worktree', 'folder-repo'] + }) + }) + + it('defers git evidence for rows that no cleanup decision can use', async () => { + const result = await scanWorkspaceCleanup(makeStore(), { includeAllWorkspaces: true }) + + // Why: only the inactive row can ever be queued, so it is the only row worth + // a git read; the rest stream immediately with empty evidence. + expect(getStatusMock).toHaveBeenCalledTimes(1) + expect(getStatusMock).toHaveBeenCalledWith('/repo-old', expect.anything()) + expect( + result.candidates.find((candidate) => candidate.worktreeId === 'repo-1::/repo-recent')?.git + ).toEqual({ clean: null, upstreamAhead: null, upstreamBehind: null, checkedAt: null }) + expect( + result.candidates.find((candidate) => candidate.worktreeId === 'repo-1::/repo-old')?.git + ).toMatchObject({ clean: true, checkedAt: expect.any(Number) }) + }) + + it('does not read activity files for rows already known to be recent', async () => { + const recentWorktrees = ['/repo-recent-a', '/repo-recent-b'].map( + (worktreePath, index): GitWorktreeInfo => ({ + path: worktreePath, + head: `recent-${index}`, + branch: `refs/heads/recent-${index}`, + isBare: false, + isMainWorktree: false + }) + ) + listRepoWorktreesMock.mockResolvedValue(recentWorktrees) + const recentMeta = makeWorktreeMeta({ lastActivityAt: NOW - 2 * DAY_MS }) + const store = { + ...makeStore(), + getWorktreeMeta: () => recentMeta + } as unknown as Store + + const result = await scanWorkspaceCleanup(store, { includeAllWorkspaces: true }) + + expect(result.candidates).toHaveLength(recentWorktrees.length) + expect(readFileMock).not.toHaveBeenCalled() + expect(lstatMock).not.toHaveBeenCalled() + expect(getStatusMock).not.toHaveBeenCalled() + }) + + it('batches fleet progress instead of emitting one IPC payload per row', async () => { + const worktrees = [ + GIT_WORKTREES[0], + ...Array.from( + { length: 200 }, + (_, index): GitWorktreeInfo => ({ + path: `/repo-batch-${index}`, + head: `head-${index}`, + branch: `refs/heads/batch-${index}`, + isBare: false, + isMainWorktree: false + }) + ) + ] + listRepoWorktreesMock.mockResolvedValue(worktrees) + const onProgress = vi.fn() + + const result = await scanWorkspaceCleanup( + makeStore(), + { + scanId: 'fleet-scan', + includeAllWorkspaces: true, + skipGitWorktreeIds: worktrees.map((worktree) => `${REPO.id}::${worktree.path}`) + }, + { onProgress } + ) + + expect(onProgress.mock.calls.length).toBeLessThanOrEqual(2) + const progress = onProgress.mock.lastCall?.[0] + expect(progress).toMatchObject({ + scannedWorktreeCount: result.candidates.length, + totalWorktreeCount: result.candidates.length, + candidateMode: 'append' + }) + const streamedCandidates = onProgress.mock.calls.flatMap(([update]) => update.candidates) + expect(streamedCandidates.map((candidate) => candidate.worktreeId)).toEqual( + result.candidates.map((candidate) => candidate.worktreeId) + ) + }) + + it('still forces a git read for a focused scan of a recent workspace', async () => { + const result = await scanWorkspaceCleanup(makeStore(), { + worktreeId: 'repo-1::/repo-recent', + includeAllWorkspaces: true + }) + + expect(getStatusMock).toHaveBeenCalledTimes(1) + expect(lstatMock).toHaveBeenCalledWith('/repo-recent') + expect(lstatMock).toHaveBeenCalledWith(path.join('/repo-recent', '.git')) + expect(result.candidates).toHaveLength(1) + expect(result.candidates[0]).toMatchObject({ + worktreeId: 'repo-1::/repo-recent', + reasons: [], + git: { clean: true, checkedAt: expect.any(Number) } + }) + }) + + it('lists a repo once for a targeted Git-evidence batch', async () => { + const targets = ['repo-1::/repo-recent', 'repo-1::/repo-old'] + + const result = await scanWorkspaceCleanup(makeStore(), { worktreeIds: targets }) + + expect(listRepoWorktreesMock).toHaveBeenCalledTimes(1) + expect(getStatusMock).toHaveBeenCalledTimes(2) + expect(lstatMock).not.toHaveBeenCalled() + expect(new Set(result.candidates.map((candidate) => candidate.worktreeId))).toEqual( + new Set(targets) + ) + }) + + it('keeps the maximum targeted batch to one repo listing', async () => { + const worktrees = Array.from( + { length: WORKSPACE_CLEANUP_TARGET_BATCH_LIMIT + 1 }, + (_, index): GitWorktreeInfo => ({ + path: `/repo-evidence-${index}`, + head: `head-${index}`, + branch: `refs/heads/evidence-${index}`, + isBare: false, + isMainWorktree: false + }) + ) + listRepoWorktreesMock.mockResolvedValue(worktrees) + + const result = await scanWorkspaceCleanup(makeStore(), { + worktreeIds: worktrees.map((worktree) => `${REPO.id}::${worktree.path}`) + }) + + expect(result.candidates).toHaveLength(WORKSPACE_CLEANUP_TARGET_BATCH_LIMIT) + expect(listRepoWorktreesMock).toHaveBeenCalledTimes(1) + expect(getStatusMock).toHaveBeenCalledTimes(WORKSPACE_CLEANUP_TARGET_BATCH_LIMIT) + expect(lstatMock).not.toHaveBeenCalled() + }) + + it('lists disconnected SSH workspaces only on an opt-in scan', async () => { + const sshRepo: Repo = { ...REPO, id: 'repo-ssh', connectionId: 'ssh-1' } + const allMeta: Record = { + 'repo-ssh::/remote/recent': makeWorktreeMeta({ lastActivityAt: NOW - 2 * DAY_MS }) + } + + const legacy = await scanWorkspaceCleanup(makeStore([sshRepo], allMeta)) + const optIn = await scanWorkspaceCleanup(makeStore([sshRepo], allMeta), { + includeAllWorkspaces: true + }) + + expect(legacy.candidates).toEqual([]) + expect(optIn.candidates).toHaveLength(1) + expect(optIn.candidates[0]).toMatchObject({ + worktreeId: 'repo-ssh::/remote/recent', + blockers: ['ssh-disconnected'], + reasons: [] + }) + }) + + it('uses the backing path for disconnected SSH folder instances', async () => { + const sshFolderRepo: Repo = { ...FOLDER_REPO, connectionId: 'ssh-1' } + const instanceId = `${sshFolderRepo.id}::${sshFolderRepo.path}::workspace:11111111-2222-4333-8444-555555555555` + const allMeta = { + [instanceId]: makeWorktreeMeta({ displayName: 'Remote folder session' }) + } + + const result = await scanWorkspaceCleanup(makeStore([sshFolderRepo], allMeta), { + includeAllWorkspaces: true + }) + + expect(result.candidates).toEqual([ + expect.objectContaining({ + worktreeId: instanceId, + path: sshFolderRepo.path, + branch: 'folder-workspace', + blockers: ['ssh-disconnected'] + }) + ]) + }) + + it('streams every row through scan progress on an opt-in scan', async () => { + const progress: { scannedWorktreeCount: number; totalWorktreeCount: number }[] = [] + + await scanWorkspaceCleanup( + makeStore(), + { includeAllWorkspaces: true, scanId: 'scan-all' }, + { onProgress: (event) => progress.push(event) } + ) + + expect(progress.at(-1)).toMatchObject({ + scanId: 'scan-all', + scannedWorktreeCount: 3, + totalWorktreeCount: 3 + }) + }) +}) diff --git a/src/main/ipc/workspace-cleanup-candidate.ts b/src/main/ipc/workspace-cleanup-candidate.ts index d6e93429b8a..58c56316acc 100644 --- a/src/main/ipc/workspace-cleanup-candidate.ts +++ b/src/main/ipc/workspace-cleanup-candidate.ts @@ -2,6 +2,7 @@ import type { IGitProvider } from '../providers/types' import { isFolderRepo } from '../../shared/repo-kind' import type { Repo } from '../../shared/repo-types' import type { Worktree } from '../../shared/worktree/types' +import { getWorktreeExecutionHostId } from '../../shared/execution-host' import { applyWorkspaceCleanupPolicy, createWorkspaceCleanupFingerprint, @@ -24,8 +25,9 @@ export async function buildWorkspaceCleanupCandidate(args: { provider: IGitProvider | null skipGit: boolean forceGitCheck: boolean + signal?: AbortSignal }): Promise { - const { repo, worktree, scannedAt, provider, skipGit, forceGitCheck } = args + const { repo, worktree, scannedAt, provider, skipGit, forceGitCheck, signal } = args const blockers: WorkspaceCleanupBlocker[] = [] const reasons = getWorkspaceCleanupInactivityReasonsForWorkspace(worktree, scannedAt) const repoIsFolder = isFolderRepo(repo) @@ -51,7 +53,7 @@ export async function buildWorkspaceCleanupCandidate(args: { const gitEvidence = !shouldReadGit ? createEmptyWorkspaceCleanupGitEvidence() - : await readWorkspaceCleanupGitEvidence(worktree, repo, provider) + : await readWorkspaceCleanupGitEvidence(worktree, repo, provider, signal) appendWorkspaceCleanupItems(blockers, gitEvidence.blockers) const candidateWithoutFingerprint: WorkspaceCleanupCandidate = { @@ -59,6 +61,7 @@ export async function buildWorkspaceCleanupCandidate(args: { repoId: repo.id, repoName: repo.displayName, connectionId: repo.connectionId ?? null, + executionHostId: getWorktreeExecutionHostId(worktree, repo), displayName: worktree.displayName, branch: shortWorkspaceCleanupBranchName(worktree.branch), path: worktree.path, @@ -103,6 +106,7 @@ export function buildWorkspaceCleanupCandidateFromError( repoId: repo.id, repoName: repo.displayName, connectionId: repo.connectionId ?? null, + executionHostId: getWorktreeExecutionHostId(worktree, repo), displayName: worktree.displayName, branch: shortWorkspaceCleanupBranchName(worktree.branch), path: worktree.path, diff --git a/src/main/ipc/workspace-cleanup-disconnected-ssh.ts b/src/main/ipc/workspace-cleanup-disconnected-ssh.ts index 264e1eac9e2..e01484cb24a 100644 --- a/src/main/ipc/workspace-cleanup-disconnected-ssh.ts +++ b/src/main/ipc/workspace-cleanup-disconnected-ssh.ts @@ -1,4 +1,5 @@ import { basename } from 'node:path' +import { getRepoExecutionHostId, normalizeExecutionHostId } from '../../shared/execution-host' import type { Store } from '../persistence' import type { Repo } from '../../shared/repo-types' import type { WorktreeMeta } from '../../shared/worktree/meta-types' @@ -7,7 +8,7 @@ import { createWorkspaceCleanupFingerprint, type WorkspaceCleanupCandidate } from '../../shared/workspace-cleanup' -import { splitWorktreeId } from '../../shared/worktree/id' +import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id' import { getNewestWorkspaceCleanupDiffCommentAt, getWorkspaceCleanupInactivityReasonsForWorkspace, @@ -18,17 +19,24 @@ export function synthesizeDisconnectedSshCleanupCandidates( store: Store, repo: Repo, scannedAt: number, - targetWorktreeId?: string + targetWorktreeIds?: ReadonlySet, + includeAllWorkspaces = false ): WorkspaceCleanupCandidate[] { const repoWorktreePrefix = `${repo.id}::` - if (targetWorktreeId) { - if (!targetWorktreeId.startsWith(repoWorktreePrefix)) { - return [] - } - // Why: focused delete preflight names one workspace already; walking all + if (targetWorktreeIds) { + const candidates: WorkspaceCleanupCandidate[] = [] + // Why: targeted refreshes name their workspaces already; walking all // persisted metadata is unnecessary for disconnected SSH repos. - const meta = store.getWorktreeMeta(targetWorktreeId) - return meta ? [createDisconnectedSshCandidate(repo, scannedAt, targetWorktreeId, meta)] : [] + for (const worktreeId of targetWorktreeIds) { + if (!worktreeId.startsWith(repoWorktreePrefix)) { + continue + } + const meta = store.getWorktreeMeta(worktreeId) + if (meta) { + candidates.push(createDisconnectedSshCandidate(repo, scannedAt, worktreeId, meta)) + } + } + return candidates } const candidates: WorkspaceCleanupCandidate[] = [] @@ -38,7 +46,7 @@ export function synthesizeDisconnectedSshCleanupCandidates( continue } const meta = allMeta[worktreeId] - if (!meta || !isWorkspaceInactiveForCleanup(meta, scannedAt)) { + if (!meta || (!includeAllWorkspaces && !isWorkspaceInactiveForCleanup(meta, scannedAt))) { continue } candidates.push(createDisconnectedSshCandidate(repo, scannedAt, worktreeId, meta)) @@ -52,7 +60,7 @@ function createDisconnectedSshCandidate( worktreeId: string, meta: WorktreeMeta ): WorkspaceCleanupCandidate { - const parsed = splitWorktreeId(worktreeId) + const parsed = splitWorktreeIdForFilesystem(worktreeId) const path = parsed?.worktreePath ?? worktreeId const reasons = getWorkspaceCleanupInactivityReasonsForWorkspace(meta, scannedAt) return applyWorkspaceCleanupPolicy({ @@ -60,6 +68,7 @@ function createDisconnectedSshCandidate( repoId: repo.id, repoName: repo.displayName, connectionId: repo.connectionId ?? null, + executionHostId: normalizeExecutionHostId(meta.hostId) ?? getRepoExecutionHostId(repo), displayName: meta.displayName || basename(path), branch: basename(path), path, diff --git a/src/main/ipc/workspace-cleanup-folder-workspaces.ts b/src/main/ipc/workspace-cleanup-folder-workspaces.ts new file mode 100644 index 00000000000..fc6112460cc --- /dev/null +++ b/src/main/ipc/workspace-cleanup-folder-workspaces.ts @@ -0,0 +1,23 @@ +import type { Store } from '../persistence' +import { createFolderWorktree } from '../repo-worktrees' +import type { Repo } from '../../shared/repo-types' +import type { Worktree } from '../../shared/worktree/types' +import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR } from '../../shared/worktree/id' +import { mergeWorktree } from './worktree-logic' + +export function listWorkspaceCleanupFolderWorkspaces(store: Store, repo: Repo): Worktree[] { + const rootId = `${repo.id}::${repo.path}` + const instancePrefix = `${rootId}${FOLDER_WORKSPACE_INSTANCE_SEPARATOR}` + const allMeta = store.getAllWorktreeMeta() + const worktreeIds = [ + rootId, + ...Object.keys(allMeta).filter((worktreeId) => worktreeId.startsWith(instancePrefix)) + ] + const folderWorktree = createFolderWorktree(repo) + + return worktreeIds.map((worktreeId) => ({ + ...mergeWorktree(repo.id, folderWorktree, store.getWorktreeMeta(worktreeId), repo.displayName), + id: worktreeId, + isMainWorktree: worktreeId === rootId + })) +} diff --git a/src/main/ipc/workspace-cleanup-git-evidence.ts b/src/main/ipc/workspace-cleanup-git-evidence.ts index 91aca462c7a..a54829c7c25 100644 --- a/src/main/ipc/workspace-cleanup-git-evidence.ts +++ b/src/main/ipc/workspace-cleanup-git-evidence.ts @@ -7,6 +7,7 @@ import type { Worktree } from '../../shared/worktree/types' import type { WorkspaceCleanupBlocker } from '../../shared/workspace-cleanup' import { WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS, + WorkspaceCleanupScanCancelledError, withWorkspaceCleanupTimeout } from './workspace-cleanup-scan-primitives' import { getWorktreeSharedLinkPaths } from '../git/worktree-shared-directories' @@ -32,7 +33,8 @@ export function createEmptyWorkspaceCleanupGitEvidence(): WorkspaceCleanupGitEvi export async function readWorkspaceCleanupGitEvidence( worktree: Worktree, repo: Repo, - provider: IGitProvider | null + provider: IGitProvider | null, + signal?: AbortSignal ): Promise { const blockers: WorkspaceCleanupBlocker[] = [] let status: GitStatusResult @@ -49,9 +51,13 @@ export async function readWorkspaceCleanupGitEvidence( ...(sharedLinkPaths.length > 0 ? { sharedLinkPaths } : {}) }), WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS, - 'Timed out reading git status.' + 'Timed out reading git status.', + signal ) - } catch { + } catch (error) { + if (error instanceof WorkspaceCleanupScanCancelledError) { + throw error + } return { ...createEmptyWorkspaceCleanupGitEvidence(), blockers: ['git-status-error'] @@ -76,7 +82,7 @@ export async function readWorkspaceCleanupGitEvidence( blockers.push('unpushed-commits') } if (clean && upstreamAhead === null) { - const unpushedCommitCount = await readUnpushedCommitCount(worktree, repo, provider) + const unpushedCommitCount = await readUnpushedCommitCount(worktree, repo, provider, signal) if (unpushedCommitCount === null) { blockers.push('unknown-base') } else if (unpushedCommitCount > 0) { @@ -96,7 +102,8 @@ export async function readWorkspaceCleanupGitEvidence( async function readUnpushedCommitCount( worktree: Worktree, repo: Repo, - provider: IGitProvider | null + provider: IGitProvider | null, + signal?: AbortSignal ): Promise { try { const result = await withWorkspaceCleanupTimeout( @@ -110,11 +117,15 @@ async function readUnpushedCommitCount( signal }), WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS, - 'Timed out checking unpushed commits.' + 'Timed out checking unpushed commits.', + signal ) const count = Number.parseInt(result.stdout.trim(), 10) return Number.isFinite(count) ? count : null - } catch { + } catch (error) { + if (error instanceof WorkspaceCleanupScanCancelledError) { + throw error + } return null } } diff --git a/src/main/ipc/workspace-cleanup-process-preflight.test.ts b/src/main/ipc/workspace-cleanup-process-preflight.test.ts new file mode 100644 index 00000000000..1c6b8e78c30 --- /dev/null +++ b/src/main/ipc/workspace-cleanup-process-preflight.test.ts @@ -0,0 +1,112 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { ipcMain } from 'electron' +import type { Store } from '../persistence' + +const { getSshPtyProviderMock } = vi.hoisted(() => ({ + getSshPtyProviderMock: vi.fn() +})) + +vi.mock('electron', () => ({ + ipcMain: { + handle: vi.fn(), + removeHandler: vi.fn() + } +})) + +vi.mock('./pty', () => ({ + getSshPtyProvider: getSshPtyProviderMock +})) + +vi.mock('../memory/pty-registry', () => ({ + listRegisteredPtys: vi.fn(() => []) +})) + +vi.mock('../workspace-cleanup-scan-snapshot', () => ({ + persistWorkspaceCleanupScanResult: vi.fn(async () => undefined), + readWorkspaceCleanupScanSnapshot: vi.fn(async () => null) +})) + +vi.mock('../workspace-cleanup-removal-snapshot-prune', () => ({ + beginWorkspaceCleanupRemovalSnapshotPruneBatch: vi.fn(), + finishWorkspaceCleanupRemovalSnapshotPruneBatch: vi.fn(async () => undefined), + recordWorkspaceCleanupRemovalSnapshotPrune: vi.fn() +})) + +import { registerWorkspaceCleanupHandlers } from './workspace-cleanup' + +function makeEmptyStore(): Store { + return { + getProfileStorageDirectory: () => '/profile-a', + getRepos: () => [], + getWorktreeMeta: () => ({}), + getAllWorktreeMeta: () => ({}), + getGitHubCache: () => ({ pr: {}, issue: {} }) + } as unknown as Store +} + +function getPreflightHandler(): ((...args: never[]) => unknown) | undefined { + return vi + .mocked(ipcMain.handle) + .mock.calls.find(([channel]) => channel === 'workspaceCleanup:hasKillableLocalProcesses')?.[1] +} + +describe('workspace cleanup process preflight', () => { + beforeEach(() => { + vi.mocked(ipcMain.handle).mockReset() + getSshPtyProviderMock.mockReset() + }) + + it('reports local processes that workspace deletion would kill', async () => { + const localProvider = { + listProcesses: vi.fn().mockResolvedValue([ + { + id: 'repo-1::/repo-feature@@session-1', + cwd: '/repo-feature', + title: 'zsh' + } + ]) + } + registerWorkspaceCleanupHandlers(makeEmptyStore(), { + runtime: { + hasTerminalsForWorktree: vi.fn().mockResolvedValue(false) + } as never, + getLocalPtyProvider: () => localProvider as never + }) + + await expect( + getPreflightHandler()?.({} as never, { worktreeId: 'repo-1::/repo-feature' } as never) + ).resolves.toEqual({ + hasKillableProcesses: true + }) + }) + + it('reports SSH processes inside the remote workspace path', async () => { + getSshPtyProviderMock.mockReturnValue({ + listProcesses: vi.fn().mockResolvedValue([ + { + id: 'remote-session-1', + cwd: '/remote/repo-feature/subdir', + title: 'codex' + } + ]) + }) + registerWorkspaceCleanupHandlers(makeEmptyStore(), { + runtime: { + hasTerminalsForWorktree: vi.fn().mockResolvedValue(false) + } as never + }) + + await expect( + getPreflightHandler()?.( + {} as never, + { + worktreeId: 'repo-ssh::/remote/repo-feature', + connectionId: 'ssh-1', + worktreePath: '/remote/repo-feature' + } as never + ) + ).resolves.toEqual({ + hasKillableProcesses: true + }) + }) +}) diff --git a/src/main/ipc/workspace-cleanup-progress-emitter.ts b/src/main/ipc/workspace-cleanup-progress-emitter.ts new file mode 100644 index 00000000000..7a2a5394963 --- /dev/null +++ b/src/main/ipc/workspace-cleanup-progress-emitter.ts @@ -0,0 +1,83 @@ +import type { + WorkspaceCleanupCandidate, + WorkspaceCleanupScanError, + WorkspaceCleanupScanProgress +} from '../../shared/workspace-cleanup' +import { appendWorkspaceCleanupItems } from './workspace-cleanup-scan-primitives' + +const WORKSPACE_CLEANUP_PROGRESS_EMIT_INTERVAL_MS = 100 + +export type WorkspaceCleanupScanOptions = { + onProgress?: (progress: WorkspaceCleanupScanProgress) => void + signal?: AbortSignal +} + +export type WorkspaceCleanupProgressEmitter = { + addDiscovered: (count: number) => void + addCandidate: (candidate: WorkspaceCleanupCandidate) => void + addErrors: (errors: WorkspaceCleanupScanError[]) => void + flush: () => void +} + +/** Caps renderer projection churn while preserving an immediate final progress state. */ +export function createWorkspaceCleanupProgressEmitter( + scanId: string | undefined, + scannedAt: number, + options: WorkspaceCleanupScanOptions +): WorkspaceCleanupProgressEmitter { + const errors: WorkspaceCleanupScanError[] = [] + let pendingCandidates: WorkspaceCleanupCandidate[] = [] + let totalWorktreeCount = 0 + let scannedWorktreeCount = 0 + let dirty = false + let emittedInitialDiscovery = false + let timer: NodeJS.Timeout | null = null + const flush = (): void => { + if (!scanId || !dirty) { + return + } + if (timer) { + clearTimeout(timer) + timer = null + } + dirty = false + const candidates = pendingCandidates + pendingCandidates = [] + options.onProgress?.({ + scanId, + scannedAt, + totalWorktreeCount, + scannedWorktreeCount, + candidates, + errors: [...errors], + candidateMode: 'append' + }) + } + const schedule = (): void => { + if (!scanId) { + return + } + dirty = true + timer ??= setTimeout(flush, WORKSPACE_CLEANUP_PROGRESS_EMIT_INTERVAL_MS) + } + return { + addDiscovered: (count) => { + totalWorktreeCount += count + schedule() + if (!emittedInitialDiscovery) { + emittedInitialDiscovery = true + flush() + } + }, + addCandidate: (candidate) => { + pendingCandidates.push(candidate) + scannedWorktreeCount += 1 + schedule() + }, + addErrors: (newErrors) => { + appendWorkspaceCleanupItems(errors, newErrors) + schedule() + }, + flush + } +} diff --git a/src/main/ipc/workspace-cleanup-scan-eligibility.ts b/src/main/ipc/workspace-cleanup-scan-eligibility.ts new file mode 100644 index 00000000000..d6b2c94b218 --- /dev/null +++ b/src/main/ipc/workspace-cleanup-scan-eligibility.ts @@ -0,0 +1,35 @@ +import type { Worktree } from '../../shared/worktree/types' +import { getPersistedWorkspaceCleanupActivityAt } from '../../shared/workspace-cleanup' +import { isWorkspaceInactiveForCleanup } from './workspace-cleanup-candidate' + +/** + * Which worktrees a broad (non-targeted) cleanup scan is allowed to emit. + * + * Why: a client that has not opted into the full workspace list still renders + * the suggestion-only views, so the host must keep publishing exactly the rows + * it always did — inactive, non-main, non-folder worktrees — or an older client + * is flooded with rows it has no view for. + */ +export function shouldScanBroadWorkspaceCleanupWorktree(args: { + includeAllWorkspaces: boolean + repoIsFolder: boolean + worktree: Worktree + scannedAt: number +}): boolean { + const { includeAllWorkspaces, repoIsFolder, worktree, scannedAt } = args + if (includeAllWorkspaces) { + return true + } + if (repoIsFolder || worktree.isMainWorktree) { + return false + } + // Why: persisted stamps only; the filesystem read that refines them is the + // expensive part this pre-filter exists to avoid. + return isWorkspaceInactiveForCleanup( + { + isArchived: worktree.isArchived, + lastActivityAt: getPersistedWorkspaceCleanupActivityAt(worktree) + }, + scannedAt + ) +} diff --git a/src/main/ipc/workspace-cleanup-scan-primitives.ts b/src/main/ipc/workspace-cleanup-scan-primitives.ts index 8b4c5c839b4..93af63562c2 100644 --- a/src/main/ipc/workspace-cleanup-scan-primitives.ts +++ b/src/main/ipc/workspace-cleanup-scan-primitives.ts @@ -4,6 +4,19 @@ import type { WorkspaceCleanupScanError } from '../../shared/workspace-cleanup' export const WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS = 8_000 +export class WorkspaceCleanupScanCancelledError extends Error { + constructor() { + super('Workspace cleanup scan cancelled') + this.name = 'WorkspaceCleanupScanCancelledError' + } +} + +export function throwIfWorkspaceCleanupScanAborted(signal: AbortSignal | undefined): void { + if (signal?.aborted) { + throw new WorkspaceCleanupScanCancelledError() + } +} + export function appendWorkspaceCleanupItems(target: T[], entries: readonly T[]): void { // Why: cleanup can aggregate generated-size worktree batches; spreading // those batches into push can exceed JavaScript's argument limit. @@ -35,15 +48,25 @@ export async function mapWorkspaceCleanupWithConcurrency( export async function withWorkspaceCleanupTimeout( run: (signal: AbortSignal) => Promise, timeoutMs: number, - message: string + message: string, + parentSignal?: AbortSignal ): Promise { const controller = new AbortController() let timeoutId: NodeJS.Timeout | undefined + let onParentAbort: (() => void) | undefined const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { controller.abort() reject(new Error(message)) }, timeoutMs) + onParentAbort = () => { + controller.abort() + reject(new WorkspaceCleanupScanCancelledError()) + } + parentSignal?.addEventListener('abort', onParentAbort, { once: true }) + if (parentSignal?.aborted) { + onParentAbort() + } }) try { return await Promise.race([run(controller.signal), timeoutPromise]) @@ -51,6 +74,9 @@ export async function withWorkspaceCleanupTimeout( if (timeoutId) { clearTimeout(timeoutId) } + if (onParentAbort) { + parentSignal?.removeEventListener('abort', onParentAbort) + } } } diff --git a/src/main/ipc/workspace-cleanup-scan-targets.ts b/src/main/ipc/workspace-cleanup-scan-targets.ts new file mode 100644 index 00000000000..b437c13736e --- /dev/null +++ b/src/main/ipc/workspace-cleanup-scan-targets.ts @@ -0,0 +1,31 @@ +import { splitWorktreeId } from '../../shared/worktree/id' +import { + WORKSPACE_CLEANUP_TARGET_BATCH_LIMIT, + type WorkspaceCleanupScanArgs +} from '../../shared/workspace-cleanup' + +export function hasTargetedWorkspaceCleanupScan(args: WorkspaceCleanupScanArgs): boolean { + return typeof args.worktreeId === 'string' || Array.isArray(args.worktreeIds) +} + +export function getTargetWorktreeIdsByRepo( + args: WorkspaceCleanupScanArgs +): Map> { + const requestedIds = args.worktreeId + ? [args.worktreeId] + : (args.worktreeIds ?? []).slice(0, WORKSPACE_CLEANUP_TARGET_BATCH_LIMIT) + const idsByRepo = new Map>() + for (const worktreeId of requestedIds) { + if (typeof worktreeId !== 'string') { + continue + } + const parsed = splitWorktreeId(worktreeId) + if (!parsed) { + continue + } + const repoIds = idsByRepo.get(parsed.repoId) ?? new Set() + repoIds.add(worktreeId) + idsByRepo.set(parsed.repoId, repoIds) + } + return idsByRepo +} diff --git a/src/main/ipc/workspace-cleanup-scan.ts b/src/main/ipc/workspace-cleanup-scan.ts index 7072199ffcd..da6f9821356 100644 --- a/src/main/ipc/workspace-cleanup-scan.ts +++ b/src/main/ipc/workspace-cleanup-scan.ts @@ -1,23 +1,24 @@ import type { Store } from '../persistence' -import { listRepoWorktrees, createFolderWorktree } from '../repo-worktrees' -import { getSshGitProvider } from '../providers/ssh-git-dispatch' import type { IGitProvider } from '../providers/types' import { isFolderRepo } from '../../shared/repo-kind' import type { Repo } from '../../shared/repo-types' import type { GitWorktreeInfo, Worktree } from '../../shared/worktree/types' import { mergeWorktree } from './worktree-logic' -import { splitWorktreeId } from '../../shared/worktree/id' import type { WorkspaceCleanupCandidate, - WorkspaceCleanupScanError, WorkspaceCleanupScanArgs, - WorkspaceCleanupScanProgress, + WorkspaceCleanupScanError, WorkspaceCleanupScanResult } from '../../shared/workspace-cleanup' -import { getPersistedWorkspaceCleanupActivityAt } from '../../shared/workspace-cleanup' +import { + handleRepoWorktreeListError, + listCleanupGitWorktrees +} from './workspace-cleanup-worktree-listing' +import { shouldScanBroadWorkspaceCleanupWorktree } from './workspace-cleanup-scan-eligibility' import { resolvePersistedWorkspaceCleanupActivityWorktree, - resolveWorkspaceCleanupActivityWorktree + resolveWorkspaceCleanupActivityWorktree, + type WorkspaceCleanupFsActivityCache } from './workspace-cleanup-activity' import { buildWorkspaceCleanupCandidate, @@ -27,19 +28,26 @@ import { import { synthesizeDisconnectedSshCleanupCandidates } from './workspace-cleanup-disconnected-ssh' import { WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS, + WorkspaceCleanupScanCancelledError, appendWorkspaceCleanupItems, - createWorkspaceCleanupScanError, mapWorkspaceCleanupWithConcurrency, - toSafeWorkspaceCleanupRepoScanError, + throwIfWorkspaceCleanupScanAborted, withWorkspaceCleanupTimeout } from './workspace-cleanup-scan-primitives' -import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-options' +import { listWorkspaceCleanupFolderWorkspaces } from './workspace-cleanup-folder-workspaces' +import { + createWorkspaceCleanupProgressEmitter, + type WorkspaceCleanupScanOptions +} from './workspace-cleanup-progress-emitter' +import { + getTargetWorktreeIdsByRepo, + hasTargetedWorkspaceCleanupScan +} from './workspace-cleanup-scan-targets' const WORKTREE_SCAN_CONCURRENCY = 3 - -type WorkspaceCleanupScanOptions = { - onProgress?: (progress: WorkspaceCleanupScanProgress) => void -} +// Why: SSH repos pay a worktree-list round trip each; strictly serial repos +// made scan wall-clock the sum of per-repo network latencies. +const REPO_SCAN_CONCURRENCY = 2 type WorkspaceCleanupScanRepoProgress = { onWorktreesDiscovered?: (count: number) => void @@ -47,45 +55,54 @@ type WorkspaceCleanupScanRepoProgress = { onErrors?: (errors: WorkspaceCleanupScanError[]) => void } -type WorkspaceCleanupProgressEmitter = { - addDiscovered: (count: number) => void - addCandidate: (candidate: WorkspaceCleanupCandidate) => void - addErrors: (errors: WorkspaceCleanupScanError[]) => void -} - export async function scanWorkspaceCleanup( store: Store, args: WorkspaceCleanupScanArgs = {}, options: WorkspaceCleanupScanOptions = {} ): Promise { + throwIfWorkspaceCleanupScanAborted(options.signal) const scannedAt = Date.now() - const parsedTarget = args.worktreeId ? splitWorktreeId(args.worktreeId) : null - if (args.worktreeId && !parsedTarget) { + const targetWorktreeIdsByRepo = getTargetWorktreeIdsByRepo(args) + if (hasTargetedWorkspaceCleanupScan(args) && targetWorktreeIdsByRepo.size === 0) { return { scannedAt, candidates: [], errors: [] } } - const repos = parsedTarget - ? store.getRepos().filter((repo) => repo.id === parsedTarget.repoId) - : store.getRepos() - const progress = createProgressEmitter(args.scanId, scannedAt, options) + const repos = + targetWorktreeIdsByRepo.size > 0 + ? store.getRepos().filter((repo) => targetWorktreeIdsByRepo.has(repo.id)) + : store.getRepos() + const progress = createWorkspaceCleanupProgressEmitter(args.scanId, scannedAt, options) const errors: WorkspaceCleanupScanResult['errors'] = [] const candidates: WorkspaceCleanupCandidate[] = [] - for (const repo of repos) { - const result = await scanRepoWorkspaces({ - store, - repo, - scannedAt, - targetWorktreeId: args.worktreeId, - skipGitWorktreeIds: new Set(args.skipGitWorktreeIds ?? []), - onWorktreesDiscovered: progress.addDiscovered, - onCandidateScanned: progress.addCandidate, - onErrors: progress.addErrors - }) - appendWorkspaceCleanupItems(candidates, result.candidates) - appendWorkspaceCleanupItems(errors, result.errors) + try { + const repoResults = await mapWorkspaceCleanupWithConcurrency( + repos, + REPO_SCAN_CONCURRENCY, + async (repo) => { + throwIfWorkspaceCleanupScanAborted(options.signal) + return scanRepoWorkspaces({ + store, + repo, + scannedAt, + targetWorktreeIds: targetWorktreeIdsByRepo.get(repo.id), + refreshTargetActivity: args.worktreeId !== undefined || args.refreshActivity === true, + includeAllWorkspaces: args.includeAllWorkspaces === true, + skipGitWorktreeIds: new Set(args.skipGitWorktreeIds ?? []), + signal: options.signal, + onWorktreesDiscovered: progress.addDiscovered, + onCandidateScanned: progress.addCandidate, + onErrors: progress.addErrors + }) + } + ) + for (const result of repoResults) { + appendWorkspaceCleanupItems(candidates, result.candidates) + appendWorkspaceCleanupItems(errors, result.errors) + } + return { scannedAt, candidates, errors } + } finally { + progress.flush() } - - return { scannedAt, candidates, errors } } async function scanRepoWorkspaces( @@ -93,16 +110,22 @@ async function scanRepoWorkspaces( store: Store repo: Repo scannedAt: number - targetWorktreeId?: string + targetWorktreeIds?: ReadonlySet + refreshTargetActivity: boolean + includeAllWorkspaces: boolean skipGitWorktreeIds: Set + signal?: AbortSignal } & WorkspaceCleanupScanRepoProgress ): Promise { const { store, repo, scannedAt, - targetWorktreeId, + targetWorktreeIds, + refreshTargetActivity, + includeAllWorkspaces, skipGitWorktreeIds, + signal, onWorktreesDiscovered, onCandidateScanned, onErrors @@ -113,17 +136,35 @@ async function scanRepoWorkspaces( let gitWorktrees: GitWorktreeInfo[] = [] try { - const discovered = await listCleanupGitWorktrees(store, repo, repoIsFolder) + const discovered = await listCleanupGitWorktrees(store, repo, repoIsFolder, signal) provider = discovered.provider gitWorktrees = discovered.gitWorktrees } catch (error) { - return handleRepoWorktreeListError({ repo, targetWorktreeId, scannedAt, error, onErrors }) + if (error instanceof WorkspaceCleanupScanCancelledError) { + throw error + } + return handleRepoWorktreeListError({ + repo, + targeted: targetWorktreeIds !== undefined, + scannedAt, + error, + onErrors + }) } if (repo.connectionId && !provider) { - const candidates = targetWorktreeId - ? synthesizeDisconnectedSshCleanupCandidates(store, repo, scannedAt, targetWorktreeId) - : [] + // Why: a disconnected host still owns real workspaces; the full list shows + // them (blocked), while legacy scans keep omitting what they cannot inspect. + const candidates = + targetWorktreeIds || includeAllWorkspaces + ? synthesizeDisconnectedSshCleanupCandidates( + store, + repo, + scannedAt, + targetWorktreeIds, + includeAllWorkspaces + ) + : [] onWorktreesDiscovered?.(candidates.length) for (const candidate of candidates) { onCandidateScanned?.(candidate) @@ -131,43 +172,84 @@ async function scanRepoWorkspaces( return { scannedAt, candidates, errors: [] } } - const mergedWorktrees = gitWorktrees.map((gitWorktree) => { - const worktreeId = `${repo.id}::${gitWorktree.path}` - const meta = store.getWorktreeMeta(worktreeId) - return mergeWorktree(repo.id, gitWorktree, meta, repo.displayName) - }) - const candidateWorktrees = targetWorktreeId - ? mergedWorktrees.filter((worktree) => worktree.id === targetWorktreeId) + const mergedWorktrees = + repoIsFolder && includeAllWorkspaces + ? listWorkspaceCleanupFolderWorkspaces(store, repo) + : gitWorktrees.map((gitWorktree) => { + const worktreeId = `${repo.id}::${gitWorktree.path}` + const meta = store.getWorktreeMeta(worktreeId) + return mergeWorktree(repo.id, gitWorktree, meta, repo.displayName) + }) + // Why: with includeAllWorkspaces the browser shows every workspace and lets + // filters narrow it; an age threshold here would hide rows from all views. + const candidateWorktrees = targetWorktreeIds + ? mergedWorktrees.filter((worktree) => targetWorktreeIds.has(worktree.id)) : mergedWorktrees.filter((worktree) => - shouldResolveBroadWorkspaceCleanupActivity(repoIsFolder, worktree, scannedAt) + shouldScanBroadWorkspaceCleanupWorktree({ + includeAllWorkspaces, + repoIsFolder, + worktree, + scannedAt + }) ) + // Why: with a target list or the full-list browser, every filtered row will + // be reported; counting them upfront keeps the progress bar honest instead + // of advancing discovered/scanned in lockstep at ~100%. + const reportDiscoveredUpfront = targetWorktreeIds !== undefined || includeAllWorkspaces + if (reportDiscoveredUpfront && candidateWorktrees.length > 0) { + onWorktreesDiscovered?.(candidateWorktrees.length) + } // Why: fs stat has no cancellation, so on a hung network/WSL mount every // timed-out row would abandon more threadpool work. After the first timeout, // stop statting this repo and use persisted activity only. let activityStatsUnavailable = false + const fsActivityCache: WorkspaceCleanupFsActivityCache = new Map() const candidatesWithSkipped = await mapWorkspaceCleanupWithConcurrency( candidateWorktrees, WORKTREE_SCAN_CONCURRENCY, async (worktree) => { + throwIfWorkspaceCleanupScanAborted(signal) // Why: externally-created worktrees can miss Orca activity stamps; local // filesystem metadata is a conservative guard before suggesting deletion. - const worktreeWithActivity = activityStatsUnavailable - ? resolvePersistedWorkspaceCleanupActivityWorktree(worktree) - : await resolveCleanupActivityWithTimeout(repo, worktree, () => { - activityStatsUnavailable = true - }) - if (!targetWorktreeId && !isWorkspaceInactiveForCleanup(worktreeWithActivity, scannedAt)) { + const persistedActivityWorktree = resolvePersistedWorkspaceCleanupActivityWorktree(worktree) + const persistedActivityIsRecent = !isWorkspaceInactiveForCleanup( + persistedActivityWorktree, + scannedAt + ) + const worktreeWithActivity = + activityStatsUnavailable || + (targetWorktreeIds ? !refreshTargetActivity : persistedActivityIsRecent) + ? persistedActivityWorktree + : await resolveCleanupActivityWithTimeout( + repo, + worktree, + () => { + activityStatsUnavailable = true + }, + signal, + fsActivityCache + ) + const isInactive = isWorkspaceInactiveForCleanup(worktreeWithActivity, scannedAt) + if (!targetWorktreeIds && !includeAllWorkspaces && !isInactive) { return null } - onWorktreesDiscovered?.(1) + if (!reportDiscoveredUpfront) { + onWorktreesDiscovered?.(1) + } const candidate = await buildWorkspaceCleanupCandidate({ repo, worktree: worktreeWithActivity, scannedAt, provider, - skipGit: skipGitWorktreeIds.has(worktreeWithActivity.id), - forceGitCheck: Boolean(targetWorktreeId) + // Why: a row with no inactivity reason can never be queued or selected, + // so full-fleet scans stream it now and let a focused scan read git later. + skipGit: skipGitWorktreeIds.has(worktreeWithActivity.id) || !isInactive, + forceGitCheck: Boolean(targetWorktreeIds), + signal }).catch((error) => { + if (error instanceof WorkspaceCleanupScanCancelledError) { + throw error + } console.error('Workspace cleanup candidate scan failed', error) return buildWorkspaceCleanupCandidateFromError(repo, worktreeWithActivity, scannedAt) }) @@ -185,128 +267,30 @@ async function scanRepoWorkspaces( async function resolveCleanupActivityWithTimeout( repo: Repo, worktree: Worktree, - onActivityStatsUnavailable: () => void + onActivityStatsUnavailable: () => void, + signal?: AbortSignal, + fsActivityCache?: WorkspaceCleanupFsActivityCache ): Promise { try { return await withWorkspaceCleanupTimeout( - () => resolveWorkspaceCleanupActivityWorktree(repo, worktree), + () => + resolveWorkspaceCleanupActivityWorktree( + repo, + worktree, + undefined, + undefined, + fsActivityCache + ), WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS, - 'Timed out reading worktree activity.' + 'Timed out reading worktree activity.', + signal ) } catch (error) { + if (error instanceof WorkspaceCleanupScanCancelledError) { + throw error + } onActivityStatsUnavailable() console.warn('Workspace cleanup activity scan failed', error) return resolvePersistedWorkspaceCleanupActivityWorktree(worktree) } } - -function shouldResolveBroadWorkspaceCleanupActivity( - repoIsFolder: boolean, - worktree: Worktree, - scannedAt: number -): boolean { - if (repoIsFolder || worktree.isMainWorktree) { - return false - } - return isWorkspaceInactiveForCleanup( - { - isArchived: worktree.isArchived, - lastActivityAt: getPersistedWorkspaceCleanupActivityAt(worktree) - }, - scannedAt - ) -} - -async function listCleanupGitWorktrees( - store: Store, - repo: Repo, - repoIsFolder: boolean -): Promise<{ provider: IGitProvider | null; gitWorktrees: GitWorktreeInfo[] }> { - if (repoIsFolder) { - return { provider: null, gitWorktrees: [createFolderWorktree(repo)] } - } - if (repo.connectionId) { - const provider = getSshGitProvider(repo.connectionId) ?? null - if (!provider) { - // Why: cleanup should reflect only workspaces Orca can currently inspect. - return { provider: null, gitWorktrees: [] } - } - return { - provider, - gitWorktrees: await withWorkspaceCleanupTimeout( - (signal) => provider.listWorktrees(repo.path, { signal }), - WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS, - 'Timed out listing SSH worktrees.' - ) - } - } - const localGitOptions = getLocalProjectWorktreeGitOptions(store, repo) - return { - provider: null, - gitWorktrees: await withWorkspaceCleanupTimeout( - (signal) => listRepoWorktrees(repo, { ...localGitOptions, signal }), - WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS, - 'Timed out listing worktrees.' - ) - } -} - -function handleRepoWorktreeListError(args: { - repo: Repo - targetWorktreeId?: string - scannedAt: number - error: unknown - onErrors?: (errors: WorkspaceCleanupScanError[]) => void -}): WorkspaceCleanupScanResult { - const { repo, targetWorktreeId, scannedAt, error, onErrors } = args - console.error('Workspace cleanup repo scan failed', error) - if (repo.connectionId && !targetWorktreeId) { - // Why: broad cleanup only shows remote workspaces Orca can inspect now. - // A connected SSH repo that fails mid-scan is omitted, not bannered. - return { scannedAt, candidates: [], errors: [] } - } - const errors = [createWorkspaceCleanupScanError(repo, toSafeWorkspaceCleanupRepoScanError(error))] - onErrors?.(errors) - return { scannedAt, candidates: [], errors } -} - -function createProgressEmitter( - scanId: string | undefined, - scannedAt: number, - options: WorkspaceCleanupScanOptions -): WorkspaceCleanupProgressEmitter { - const errors: WorkspaceCleanupScanError[] = [] - let totalWorktreeCount = 0 - let scannedWorktreeCount = 0 - const emit = ( - candidates: WorkspaceCleanupCandidate[], - candidateMode: WorkspaceCleanupScanProgress['candidateMode'] = 'snapshot' - ): void => { - if (!scanId) { - return - } - options.onProgress?.({ - scanId, - scannedAt, - totalWorktreeCount, - scannedWorktreeCount, - candidates, - errors: [...errors], - candidateMode - }) - } - return { - addDiscovered: (count) => { - totalWorktreeCount += count - emit([], 'append') - }, - addCandidate: (candidate) => { - scannedWorktreeCount += 1 - emit([candidate], 'append') - }, - addErrors: (newErrors) => { - appendWorkspaceCleanupItems(errors, newErrors) - emit([], 'append') - } - } -} diff --git a/src/main/ipc/workspace-cleanup-snapshot-ipc.test.ts b/src/main/ipc/workspace-cleanup-snapshot-ipc.test.ts new file mode 100644 index 00000000000..8bfbc586959 --- /dev/null +++ b/src/main/ipc/workspace-cleanup-snapshot-ipc.test.ts @@ -0,0 +1,281 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { ipcMain } from 'electron' +import type { Store } from '../persistence' + +const { persistScanResultMock, readScanSnapshotMock, scanWorkspaceCleanupMock } = vi.hoisted( + () => ({ + persistScanResultMock: vi.fn(), + readScanSnapshotMock: vi.fn(), + scanWorkspaceCleanupMock: vi.fn() + }) +) + +const { beginPruneBatchMock, finishPruneBatchMock, recordPruneMock } = vi.hoisted(() => ({ + beginPruneBatchMock: vi.fn(), + finishPruneBatchMock: vi.fn().mockResolvedValue(undefined), + recordPruneMock: vi.fn() +})) + +vi.mock('electron', () => ({ + ipcMain: { + handle: vi.fn(), + removeHandler: vi.fn() + } +})) + +vi.mock('../workspace-cleanup-scan-snapshot', () => ({ + persistWorkspaceCleanupScanResult: persistScanResultMock, + readWorkspaceCleanupScanSnapshot: readScanSnapshotMock +})) + +vi.mock('./workspace-cleanup-scan', () => ({ + scanWorkspaceCleanup: scanWorkspaceCleanupMock +})) + +vi.mock('../workspace-cleanup-removal-snapshot-prune', () => ({ + beginWorkspaceCleanupRemovalSnapshotPruneBatch: beginPruneBatchMock, + finishWorkspaceCleanupRemovalSnapshotPruneBatch: finishPruneBatchMock, + recordWorkspaceCleanupRemovalSnapshotPrune: recordPruneMock +})) + +import { registerWorkspaceCleanupHandlers } from './workspace-cleanup' + +const NOW = 1_700_000_000_000 + +function makeScanEvent(senderOverrides: Record = {}): never { + return { + sender: { + id: 1, + isDestroyed: () => false, + send: vi.fn(), + once: vi.fn(), + removeListener: vi.fn(), + ...senderOverrides + } + } as never +} + +function makeEmptyStore(): Store { + return { + getProfileStorageDirectory: () => '/profile-a', + getRepos: () => [], + getWorktreeMeta: () => ({}), + getAllWorktreeMeta: () => ({}), + getGitHubCache: () => ({ pr: {}, issue: {} }) + } as unknown as Store +} + +describe('workspace cleanup snapshot IPC', () => { + beforeEach(() => { + vi.mocked(ipcMain.handle).mockClear() + persistScanResultMock.mockReset().mockResolvedValue(undefined) + readScanSnapshotMock.mockReset() + scanWorkspaceCleanupMock.mockReset().mockImplementation(async (_store, args, options) => { + const result = { scannedAt: NOW, candidates: [], errors: [] } + options.onProgress?.({ + scanId: args.scanId, + scannedAt: NOW, + scannedWorktreeCount: 0, + totalWorktreeCount: 0, + candidates: [], + errors: [] + }) + return result + }) + beginPruneBatchMock.mockReset() + finishPruneBatchMock.mockReset().mockResolvedValue(undefined) + recordPruneMock.mockReset() + }) + + it('persists the completed scan result after replying', async () => { + registerWorkspaceCleanupHandlers(makeEmptyStore()) + const handler = vi + .mocked(ipcMain.handle) + .mock.calls.find(([channel]) => channel === 'workspaceCleanup:scan')?.[1] + + const args = { includeAllWorkspaces: true } + const result = await handler?.(makeScanEvent(), args) + + expect(persistScanResultMock).toHaveBeenCalledWith('/profile-a', args, result) + }) + + it('does not rewrite the fleet snapshot for a focused scan', async () => { + registerWorkspaceCleanupHandlers(makeEmptyStore()) + const handler = vi + .mocked(ipcMain.handle) + .mock.calls.find(([channel]) => channel === 'workspaceCleanup:scan')?.[1] + + await handler?.(makeScanEvent(), { worktreeId: 'repo-1::/repo-feature' }) + + expect(persistScanResultMock).not.toHaveBeenCalled() + }) + + it('does not rewrite the fleet snapshot for a targeted evidence batch', async () => { + registerWorkspaceCleanupHandlers(makeEmptyStore()) + const handler = vi + .mocked(ipcMain.handle) + .mock.calls.find(([channel]) => channel === 'workspaceCleanup:scan')?.[1] + + await handler?.(makeScanEvent(), { + worktreeIds: ['repo-1::/repo-a', 'repo-1::/repo-b'] + }) + + expect(persistScanResultMock).not.toHaveBeenCalled() + }) + + it('does not persist an empty-target scan over the fleet snapshot', async () => { + registerWorkspaceCleanupHandlers(makeEmptyStore()) + const handler = vi + .mocked(ipcMain.handle) + .mock.calls.find(([channel]) => channel === 'workspaceCleanup:scan')?.[1] + + // Why: worktreeIds: [] is a targeted scan that returns nothing; persisting + // it as broad would wipe the fleet cache. + await handler?.(makeScanEvent(), { worktreeIds: [], includeAllWorkspaces: true }) + + expect(persistScanResultMock).not.toHaveBeenCalled() + }) + + it('stops streaming progress after the invoking renderer is destroyed', async () => { + registerWorkspaceCleanupHandlers(makeEmptyStore()) + const handler = vi + .mocked(ipcMain.handle) + .mock.calls.find(([channel]) => channel === 'workspaceCleanup:scan')?.[1] + const send = vi.fn(() => { + throw new Error('Object has been destroyed') + }) + + await expect( + handler?.(makeScanEvent({ isDestroyed: () => true, send }), { scanId: 'scan-1' }) + ).resolves.toEqual({ scannedAt: NOW, candidates: [], errors: [] }) + expect(send).not.toHaveBeenCalled() + }) + + it('aborts the scan when the invoking renderer is destroyed mid-flight', async () => { + let abortListener: (() => void) | undefined + scanWorkspaceCleanupMock.mockImplementation((_store, _args, options) => { + return new Promise((_resolve, reject) => { + options.signal?.addEventListener('abort', () => reject(new Error('cancelled')), { + once: true + }) + }) + }) + registerWorkspaceCleanupHandlers(makeEmptyStore()) + const handler = vi + .mocked(ipcMain.handle) + .mock.calls.find(([channel]) => channel === 'workspaceCleanup:scan')?.[1] + const event = makeScanEvent({ + once: vi.fn((eventName: string, listener: () => void) => { + if (eventName === 'destroyed') { + abortListener = listener + } + }) + }) + + const scan = handler?.(event, { includeAllWorkspaces: true }) + abortListener?.() + await expect(scan).rejects.toThrow('cancelled') + }) + + it('keeps legacy suggestion-only and full broad scans isolated across modes', async () => { + scanWorkspaceCleanupMock.mockImplementation((_store, _args, options) => { + return new Promise((resolve, reject) => { + options.signal?.addEventListener('abort', () => reject(new Error('cancelled')), { + once: true + }) + setTimeout(() => resolve({ scannedAt: NOW, candidates: [], errors: [] }), 5) + }) + }) + registerWorkspaceCleanupHandlers(makeEmptyStore()) + const handler = vi + .mocked(ipcMain.handle) + .mock.calls.find(([channel]) => channel === 'workspaceCleanup:scan')?.[1] + + // Why: different includeAllWorkspaces modes are separate lanes; one must + // not abort the other even from the same renderer. + const [legacy, full] = await Promise.allSettled([ + handler?.(makeScanEvent({ id: 7 }), { includeAllWorkspaces: false }) as Promise, + handler?.(makeScanEvent({ id: 7 }), { includeAllWorkspaces: true }) as Promise + ]) + + expect(legacy?.status).toBe('fulfilled') + expect(full?.status).toBe('fulfilled') + }) + + it('supersedes a concurrent broad scan from the same renderer', async () => { + const settlements: Promise[] = [] + scanWorkspaceCleanupMock.mockImplementation((_store, _args, options) => { + return new Promise((resolve, reject) => { + options.signal?.addEventListener('abort', () => reject(new Error('cancelled')), { + once: true + }) + setTimeout(() => resolve({ scannedAt: NOW, candidates: [], errors: [] }), 5) + }) + }) + registerWorkspaceCleanupHandlers(makeEmptyStore()) + const handler = vi + .mocked(ipcMain.handle) + .mock.calls.find(([channel]) => channel === 'workspaceCleanup:scan')?.[1] + + settlements.push( + handler?.(makeScanEvent({ id: 7 }), { includeAllWorkspaces: true }) as Promise, + handler?.(makeScanEvent({ id: 7 }), { includeAllWorkspaces: true }) as Promise + ) + const [first, second] = await Promise.allSettled(settlements) + + expect(first?.status).toBe('rejected') + expect(second?.status).toBe('fulfilled') + }) + + it('cancels only the invoking renderer scan id', async () => { + scanWorkspaceCleanupMock.mockImplementation((_store, _args, options) => { + return new Promise((_resolve, reject) => { + options.signal?.addEventListener('abort', () => reject(new Error('cancelled')), { + once: true + }) + }) + }) + registerWorkspaceCleanupHandlers(makeEmptyStore()) + const handlers = Object.fromEntries(vi.mocked(ipcMain.handle).mock.calls) + const event = makeScanEvent({ id: 7 }) + + const scan = handlers['workspaceCleanup:scan']?.(event, { scanId: 'scan-1' }) + expect(handlers['workspaceCleanup:cancelScan']?.(event, 'scan-1')).toBe(true) + await expect(scan).rejects.toThrow('cancelled') + expect( + handlers['workspaceCleanup:cancelScan']?.({ sender: { id: 8 } } as never, 'scan-1') + ).toBe(false) + }) + + it('serves the cached scan snapshot through getCachedScan', async () => { + const snapshot = { scannedAt: NOW, candidates: [], errors: [] } + readScanSnapshotMock.mockResolvedValue(snapshot) + registerWorkspaceCleanupHandlers(makeEmptyStore()) + const handler = vi + .mocked(ipcMain.handle) + .mock.calls.find(([channel]) => channel === 'workspaceCleanup:getCachedScan')?.[1] + + await expect(handler?.({} as never)).resolves.toBe(snapshot) + expect(readScanSnapshotMock).toHaveBeenCalledWith('/profile-a') + expect(vi.mocked(ipcMain.removeHandler)).toHaveBeenCalledWith('workspaceCleanup:getCachedScan') + }) + + it('routes a validated removal snapshot prune batch through its explicit boundary', async () => { + registerWorkspaceCleanupHandlers(makeEmptyStore()) + const handlers = Object.fromEntries(vi.mocked(ipcMain.handle).mock.calls) + const batch = { batchId: 'batch-1' } + const target = { + ...batch, + worktreeId: 'repo-ssh::/remote/feature', + executionHostId: 'ssh:ssh-1' + } + + await handlers['workspaceCleanup:beginRemovalSnapshotPruneBatch']?.({} as never, batch) + await handlers['workspaceCleanup:recordRemovalSnapshotPrune']?.({} as never, target) + await handlers['workspaceCleanup:finishRemovalSnapshotPruneBatch']?.({} as never, batch) + + expect(beginPruneBatchMock).toHaveBeenCalledExactlyOnceWith('/profile-a', batch) + expect(recordPruneMock).toHaveBeenCalledExactlyOnceWith('/profile-a', target) + expect(finishPruneBatchMock).toHaveBeenCalledExactlyOnceWith('/profile-a', batch) + }) +}) diff --git a/src/main/ipc/workspace-cleanup-worktree-listing.ts b/src/main/ipc/workspace-cleanup-worktree-listing.ts new file mode 100644 index 00000000000..dab37caf3f9 --- /dev/null +++ b/src/main/ipc/workspace-cleanup-worktree-listing.ts @@ -0,0 +1,76 @@ +import type { Store } from '../persistence' +import { listRepoWorktrees, createFolderWorktree } from '../repo-worktrees' +import { getSshGitProvider } from '../providers/ssh-git-dispatch' +import type { IGitProvider } from '../providers/types' +import type { Repo } from '../../shared/repo-types' +import type { GitWorktreeInfo } from '../../shared/worktree/types' +import type { + WorkspaceCleanupScanError, + WorkspaceCleanupScanResult +} from '../../shared/workspace-cleanup' +import { + WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS, + createWorkspaceCleanupScanError, + toSafeWorkspaceCleanupRepoScanError, + withWorkspaceCleanupTimeout +} from './workspace-cleanup-scan-primitives' +import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-options' + +export async function listCleanupGitWorktrees( + store: Store, + repo: Repo, + repoIsFolder: boolean, + signal?: AbortSignal +): Promise<{ provider: IGitProvider | null; gitWorktrees: GitWorktreeInfo[] }> { + if (repoIsFolder) { + return { + provider: repo.connectionId ? (getSshGitProvider(repo.connectionId) ?? null) : null, + gitWorktrees: [createFolderWorktree(repo)] + } + } + if (repo.connectionId) { + const provider = getSshGitProvider(repo.connectionId) ?? null + if (!provider) { + // Why: cleanup should reflect only workspaces Orca can currently inspect. + return { provider: null, gitWorktrees: [] } + } + return { + provider, + gitWorktrees: await withWorkspaceCleanupTimeout( + (signal) => provider.listWorktrees(repo.path, { signal }), + WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS, + 'Timed out listing SSH worktrees.', + signal + ) + } + } + const localGitOptions = getLocalProjectWorktreeGitOptions(store, repo) + return { + provider: null, + gitWorktrees: await withWorkspaceCleanupTimeout( + (signal) => listRepoWorktrees(repo, { ...localGitOptions, signal }), + WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS, + 'Timed out listing worktrees.', + signal + ) + } +} + +export function handleRepoWorktreeListError(args: { + repo: Repo + targeted: boolean + scannedAt: number + error: unknown + onErrors?: (errors: WorkspaceCleanupScanError[]) => void +}): WorkspaceCleanupScanResult { + const { repo, targeted, scannedAt, error, onErrors } = args + console.error('Workspace cleanup repo scan failed', error) + if (repo.connectionId && !targeted) { + // Why: broad cleanup only shows remote workspaces Orca can inspect now. + // A connected SSH repo that fails mid-scan is omitted, not bannered. + return { scannedAt, candidates: [], errors: [] } + } + const errors = [createWorkspaceCleanupScanError(repo, toSafeWorkspaceCleanupRepoScanError(error))] + onErrors?.(errors) + return { scannedAt, candidates: [], errors } +} diff --git a/src/main/ipc/workspace-cleanup.test.ts b/src/main/ipc/workspace-cleanup.test.ts index 96f0ce821f8..b35167906c8 100644 --- a/src/main/ipc/workspace-cleanup.test.ts +++ b/src/main/ipc/workspace-cleanup.test.ts @@ -17,7 +17,6 @@ const { gitExecFileAsyncMock, getLocalProjectWorktreeGitOptionsMock, getSshGitProviderMock, - getSshPtyProviderMock, listRegisteredPtysMock } = vi.hoisted(() => ({ lstatMock: vi.fn(), @@ -27,7 +26,6 @@ const { gitExecFileAsyncMock: vi.fn(), getLocalProjectWorktreeGitOptionsMock: vi.fn(), getSshGitProviderMock: vi.fn(), - getSshPtyProviderMock: vi.fn(), listRegisteredPtysMock: vi.fn() })) @@ -69,10 +67,16 @@ vi.mock('../memory/pty-registry', () => ({ })) vi.mock('./pty', () => ({ - getSshPtyProvider: getSshPtyProviderMock + getSshPtyProvider: vi.fn() })) -import { registerWorkspaceCleanupHandlers, scanWorkspaceCleanup } from './workspace-cleanup' +// Why: snapshot persistence does real file I/O; covered in workspace-cleanup-snapshot-ipc.test.ts. +vi.mock('../workspace-cleanup-scan-snapshot', () => ({ + persistWorkspaceCleanupScanResult: vi.fn(async () => undefined), + readWorkspaceCleanupScanSnapshot: vi.fn(async () => null) +})) + +import { scanWorkspaceCleanup } from './workspace-cleanup' const NOW = 1_700_000_000_000 const REPO: Repo = { @@ -161,7 +165,6 @@ describe('workspace cleanup scan', () => { gitExecFileAsyncMock.mockReset() getLocalProjectWorktreeGitOptionsMock.mockReset().mockReturnValue({}) getSshGitProviderMock.mockReset() - getSshPtyProviderMock.mockReset() listRegisteredPtysMock.mockReset() listRegisteredPtysMock.mockReturnValue([]) lstatMock.mockResolvedValue({ mtimeMs: 0 }) @@ -246,11 +249,11 @@ describe('workspace cleanup scan', () => { (event as WorkspaceCleanupScanProgress).candidateMode === 'append' && (event as WorkspaceCleanupScanProgress).candidates.length > 0 ) - expect(candidateProgress).toHaveLength(2) - expect(candidateProgress.every((event) => event.candidates.length === 1)).toBe(true) + expect(candidateProgress.length).toBeLessThanOrEqual(2) const progressWorktreeIds = candidateProgress.flatMap((event) => event.candidates.map((candidate) => candidate.worktreeId) ) + expect(progressWorktreeIds).toHaveLength(2) expect(progressWorktreeIds).toEqual( expect.arrayContaining(['repo-1::/repo-feature-a', 'repo-1::/repo-feature-b']) ) @@ -829,61 +832,4 @@ describe('workspace cleanup scan', () => { }) expect(result.candidates[0]).not.toHaveProperty('linkedPR') }) - - it('reports local processes that workspace deletion would kill', async () => { - const localProvider = { - listProcesses: vi.fn().mockResolvedValue([ - { - id: 'repo-1::/repo-feature@@session-1', - cwd: '/repo-feature', - title: 'zsh' - } - ]) - } - registerWorkspaceCleanupHandlers(makeStore(), { - runtime: { - hasTerminalsForWorktree: vi.fn().mockResolvedValue(false) - } as never, - getLocalPtyProvider: () => localProvider as never - }) - - const handler = vi - .mocked(ipcMain.handle) - .mock.calls.find(([channel]) => channel === 'workspaceCleanup:hasKillableLocalProcesses')?.[1] - - await expect(handler?.({} as never, { worktreeId: 'repo-1::/repo-feature' })).resolves.toEqual({ - hasKillableProcesses: true - }) - }) - - it('reports SSH processes inside the remote workspace path', async () => { - getSshPtyProviderMock.mockReturnValue({ - listProcesses: vi.fn().mockResolvedValue([ - { - id: 'remote-session-1', - cwd: '/remote/repo-feature/subdir', - title: 'codex' - } - ]) - }) - registerWorkspaceCleanupHandlers(makeStore(), { - runtime: { - hasTerminalsForWorktree: vi.fn().mockResolvedValue(false) - } as never - }) - - const handler = vi - .mocked(ipcMain.handle) - .mock.calls.find(([channel]) => channel === 'workspaceCleanup:hasKillableLocalProcesses')?.[1] - - await expect( - handler?.({} as never, { - worktreeId: 'repo-ssh::/remote/repo-feature', - connectionId: 'ssh-1', - worktreePath: '/remote/repo-feature' - }) - ).resolves.toEqual({ - hasKillableProcesses: true - }) - }) }) diff --git a/src/main/ipc/workspace-cleanup.ts b/src/main/ipc/workspace-cleanup.ts index 229f4c4177f..e73dd27cc5e 100644 --- a/src/main/ipc/workspace-cleanup.ts +++ b/src/main/ipc/workspace-cleanup.ts @@ -10,9 +10,22 @@ import { type WorkspaceCleanupLocalProcessArgs, type WorkspaceCleanupLocalProcessResult, type WorkspaceCleanupScanArgs, - type WorkspaceCleanupScanResult + type WorkspaceCleanupScanResult, + type WorkspaceCleanupSnapshotPruneBatchArgs, + type WorkspaceCleanupSnapshotPruneRecordArgs } from '../../shared/workspace-cleanup' +import { parseExecutionHostId } from '../../shared/execution-host' import { scanWorkspaceCleanup } from './workspace-cleanup-scan' +import { hasTargetedWorkspaceCleanupScan } from './workspace-cleanup-scan-targets' +import { + persistWorkspaceCleanupScanResult, + readWorkspaceCleanupScanSnapshot +} from '../workspace-cleanup-scan-snapshot' +import { + beginWorkspaceCleanupRemovalSnapshotPruneBatch, + finishWorkspaceCleanupRemovalSnapshotPruneBatch, + recordWorkspaceCleanupRemovalSnapshotPrune +} from '../workspace-cleanup-removal-snapshot-prune' export { scanWorkspaceCleanup } @@ -21,28 +34,109 @@ type WorkspaceCleanupHandlerDeps = { getLocalPtyProvider?: () => IPtyProvider } +// Why: module scope — handler re-registration on a new main window must not +// orphan the previous window's controllers in a discarded map. +const activeScans = new Map() +// Keyed by sender AND scan mode: legacy suggestion-only and full-workspace +// broad scans are separate lanes and must not supersede each other, matching +// the renderer's broad-scan registry. +const broadScanControllersBySenderMode = new Map() + +function getBroadScanModeKey(senderId: number, args: WorkspaceCleanupScanArgs): string { + return `${senderId}\0${args.includeAllWorkspaces === true}` +} + export function registerWorkspaceCleanupHandlers( store: Store, deps: WorkspaceCleanupHandlerDeps = {} ): void { + const snapshotDirectory = store.getProfileStorageDirectory() ipcMain.removeHandler('workspaceCleanup:scan') + ipcMain.removeHandler('workspaceCleanup:cancelScan') + ipcMain.removeHandler('workspaceCleanup:getCachedScan') ipcMain.removeHandler('workspaceCleanup:dismiss') ipcMain.removeHandler('workspaceCleanup:clearDismissals') ipcMain.removeHandler('workspaceCleanup:hasKillableLocalProcesses') + ipcMain.removeHandler('workspaceCleanup:beginRemovalSnapshotPruneBatch') + ipcMain.removeHandler('workspaceCleanup:recordRemovalSnapshotPrune') + ipcMain.removeHandler('workspaceCleanup:finishRemovalSnapshotPruneBatch') ipcMain.handle( 'workspaceCleanup:scan', - (event, args?: WorkspaceCleanupScanArgs): Promise => - scanWorkspaceCleanup(store, args ?? {}, { - onProgress: args?.scanId - ? (progress) => event.sender.send('workspaceCleanup:scanProgress', progress) - : undefined - }) + async (event, args?: WorkspaceCleanupScanArgs): Promise => { + const scanArgs = args ?? {} + const sender = event.sender + const scanKey = getWorkspaceCleanupScanKey(sender.id, scanArgs.scanId) + const controller = new AbortController() + if (scanKey) { + activeScans.set(scanKey, controller) + } + const targeted = hasTargetedWorkspaceCleanupScan(scanArgs) + const broadScanKey = getBroadScanModeKey(sender.id, scanArgs) + if (!targeted) { + // Why: two same-mode broad fleet scans from one renderer can only be a + // refresh race; running both doubles git subprocess and fs load for a + // result the renderer will discard. + broadScanControllersBySenderMode.get(broadScanKey)?.abort() + broadScanControllersBySenderMode.set(broadScanKey, controller) + } + // Why: a window close or reload must stop the fleet scan's git and fs + // work, not merely mute its progress events. + const onSenderDestroyed = (): void => controller.abort() + sender.once('destroyed', onSenderDestroyed) + try { + const result = await scanWorkspaceCleanup(store, scanArgs, { + signal: controller.signal, + onProgress: scanArgs.scanId + ? (progress) => { + if (!sender.isDestroyed()) { + sender.send('workspaceCleanup:scanProgress', progress) + } + } + : undefined + }) + // Focused scans are live-only; persisting each rewrites and fsyncs the + // fleet snapshot. worktreeIds: [] is still targeted — persisting its + // empty result would wipe the fleet cache. + if (!targeted) { + void persistWorkspaceCleanupScanResult(snapshotDirectory, scanArgs, result) + } + return result + } finally { + if (!sender.isDestroyed()) { + sender.removeListener('destroyed', onSenderDestroyed) + } + if (scanKey && activeScans.get(scanKey) === controller) { + activeScans.delete(scanKey) + } + if (!targeted && broadScanControllersBySenderMode.get(broadScanKey) === controller) { + broadScanControllersBySenderMode.delete(broadScanKey) + } + } + } + ) + + ipcMain.handle('workspaceCleanup:cancelScan', (event, scanId: string): boolean => { + const scanKey = getWorkspaceCleanupScanKey(event.sender.id, scanId) + const controller = scanKey ? activeScans.get(scanKey) : undefined + if (!controller || controller.signal.aborted) { + return false + } + controller.abort() + return true + }) + + ipcMain.handle( + 'workspaceCleanup:getCachedScan', + (): Promise => + readWorkspaceCleanupScanSnapshot(snapshotDirectory) ) ipcMain.handle('workspaceCleanup:dismiss', (_event, args: WorkspaceCleanupDismissArgs) => { - const current = store.getUI().workspaceCleanup?.dismissals ?? {} - const next = { ...current } + const next = { ...store.getUI().workspaceCleanup?.dismissals } + for (const worktreeId of args.removedWorktreeIds ?? []) { + delete next[worktreeId] + } for (const dismissal of args.dismissals ?? []) { if ( dismissal && @@ -69,6 +163,49 @@ export function registerWorkspaceCleanupHandlers( hasKillableProcesses: await hasKillableProcesses(args, deps) }) ) + + ipcMain.handle( + 'workspaceCleanup:beginRemovalSnapshotPruneBatch', + (_event, args: WorkspaceCleanupSnapshotPruneBatchArgs) => { + if (isSnapshotPruneBatchId(args?.batchId)) { + beginWorkspaceCleanupRemovalSnapshotPruneBatch(snapshotDirectory, args) + } + } + ) + + ipcMain.handle( + 'workspaceCleanup:recordRemovalSnapshotPrune', + (_event, args: WorkspaceCleanupSnapshotPruneRecordArgs) => { + if ( + isSnapshotPruneBatchId(args?.batchId) && + typeof args?.worktreeId === 'string' && + args.worktreeId.length > 0 && + (args.executionHostId === undefined || parseExecutionHostId(args.executionHostId)) + ) { + recordWorkspaceCleanupRemovalSnapshotPrune(snapshotDirectory, args) + } + } + ) + + ipcMain.handle( + 'workspaceCleanup:finishRemovalSnapshotPruneBatch', + (_event, args: WorkspaceCleanupSnapshotPruneBatchArgs) => { + if (isSnapshotPruneBatchId(args?.batchId)) { + return finishWorkspaceCleanupRemovalSnapshotPruneBatch(snapshotDirectory, args) + } + return undefined + } + ) +} + +function isSnapshotPruneBatchId(value: unknown): value is string { + return typeof value === 'string' && value.length > 0 && value.length <= 128 +} + +function getWorkspaceCleanupScanKey(senderId: number, scanId: unknown): string | null { + return typeof scanId === 'string' && scanId.length > 0 && scanId.length <= 128 + ? `${senderId}\0${scanId}` + : null } async function hasKillableProcesses( diff --git a/src/main/ipc/workspace-space.test.ts b/src/main/ipc/workspace-space.test.ts index c6605c73cce..63b118eff19 100644 --- a/src/main/ipc/workspace-space.test.ts +++ b/src/main/ipc/workspace-space.test.ts @@ -11,6 +11,8 @@ const { analyzeWorkspaceSpaceMock, removeHandlerMock, handleMock, + persistAnalysisSnapshotMock, + readAnalysisSnapshotMock, WorkspaceSpaceScanCancelledErrorMock } = vi.hoisted(() => { const handlers = new Map Promise>() @@ -21,6 +23,8 @@ const { handleMock: vi.fn((channel: string, handler: (...args: unknown[]) => Promise) => { handlers.set(channel, handler) }), + persistAnalysisSnapshotMock: vi.fn(), + readAnalysisSnapshotMock: vi.fn(), WorkspaceSpaceScanCancelledErrorMock: class WorkspaceSpaceScanCancelledError extends Error {} } }) @@ -37,6 +41,11 @@ vi.mock('../workspace-space-analysis', () => ({ analyzeWorkspaceSpace: analyzeWorkspaceSpaceMock })) +vi.mock('../workspace-space-analysis-snapshot', () => ({ + persistWorkspaceSpaceAnalysisSnapshot: persistAnalysisSnapshotMock, + readWorkspaceSpaceAnalysisSnapshot: readAnalysisSnapshotMock +})) + import { registerWorkspaceSpaceHandlers } from './workspace-space' function createAnalysis(scannedAt: number): WorkspaceSpaceAnalysis { @@ -65,13 +74,19 @@ function createEvent() { } } +function createStore(): Store { + return { getProfileStorageDirectory: () => '/profile-a' } as Store +} + describe('registerWorkspaceSpaceHandlers', () => { beforeEach(() => { analyzeWorkspaceSpaceMock.mockReset() + persistAnalysisSnapshotMock.mockReset().mockResolvedValue(undefined) + readAnalysisSnapshotMock.mockReset().mockResolvedValue(null) }) it('shares an in-flight analysis request', async () => { - const store = {} as Store + const store = createStore() let resolveFirstScan: (analysis: WorkspaceSpaceAnalysis) => void = () => {} const firstScan = new Promise((resolve) => { resolveFirstScan = resolve @@ -109,7 +124,7 @@ describe('registerWorkspaceSpaceHandlers', () => { }) it('forwards scan progress to the requesting renderer', async () => { - const store = {} as Store + const store = createStore() let onProgress: ((progress: WorkspaceSpaceScanProgress) => void) | undefined analyzeWorkspaceSpaceMock.mockImplementationOnce((_store, options) => { onProgress = options.onProgress @@ -138,8 +153,65 @@ describe('registerWorkspaceSpaceHandlers', () => { expect(event.sender.send).toHaveBeenCalledWith('workspaceSpace:progress', progress) }) + it('batches completed measurements without dropping throttled rows', async () => { + const store = createStore() + let onProgress: ((progress: WorkspaceSpaceScanProgress) => void) | undefined + let resolveScan!: (analysis: WorkspaceSpaceAnalysis) => void + analyzeWorkspaceSpaceMock.mockImplementationOnce((_store, options) => { + onProgress = options.onProgress + return new Promise((resolve) => { + resolveScan = resolve + }) + }) + const now = vi.spyOn(Date, 'now').mockReturnValue(1_000) + + try { + registerWorkspaceSpaceHandlers(store) + const event = createEvent() + const promise = handlers.get('workspaceSpace:analyze')!(event) + const base = { + scanId: 'scan-1', + state: 'running' as const, + startedAt: 1, + updatedAt: 1, + totalRepoCount: 1, + scannedRepoCount: 0, + totalWorktreeCount: 2, + currentRepoDisplayName: 'orca', + currentWorktreeDisplayName: 'feature' + } + onProgress?.({ ...base, scannedWorktreeCount: 0 }) + onProgress?.({ + ...base, + scannedWorktreeCount: 1, + completedMeasurements: [{ worktreeId: 'a', status: 'ok', sizeBytes: 10 }] + }) + onProgress?.({ + ...base, + scannedWorktreeCount: 2, + completedMeasurements: [{ worktreeId: 'b', status: 'ok', sizeBytes: 20 }] + }) + resolveScan(createAnalysis(1)) + await promise + + expect(event.sender.send).toHaveBeenCalledTimes(2) + expect(event.sender.send).toHaveBeenLastCalledWith( + 'workspaceSpace:progress', + expect.objectContaining({ + scannedWorktreeCount: 2, + completedMeasurements: [ + { worktreeId: 'a', status: 'ok', sizeBytes: 10 }, + { worktreeId: 'b', status: 'ok', sizeBytes: 20 } + ] + }) + ) + } finally { + now.mockRestore() + } + }) + it('cancels the in-flight scan', async () => { - const store = {} as Store + const store = createStore() let signal: AbortSignal | undefined analyzeWorkspaceSpaceMock.mockImplementationOnce((_store, options) => { signal = options.signal @@ -157,12 +229,36 @@ describe('registerWorkspaceSpaceHandlers', () => { }) it('returns a normal cancelled result instead of rejecting expected cancellation', async () => { - const store = {} as Store + const store = createStore() analyzeWorkspaceSpaceMock.mockRejectedValueOnce(new WorkspaceSpaceScanCancelledErrorMock()) registerWorkspaceSpaceHandlers(store) const analyzeHandler = handlers.get('workspaceSpace:analyze') await expect(analyzeHandler!(createEvent())).resolves.toEqual({ ok: false, cancelled: true }) + expect(persistAnalysisSnapshotMock).not.toHaveBeenCalled() + }) + + it('persists the completed analysis snapshot', async () => { + const analysis = createAnalysis(7) + analyzeWorkspaceSpaceMock.mockResolvedValueOnce(analysis) + + registerWorkspaceSpaceHandlers(createStore()) + const analyzeHandler = handlers.get('workspaceSpace:analyze') + + await expect(analyzeHandler!(createEvent())).resolves.toEqual({ ok: true, analysis }) + expect(persistAnalysisSnapshotMock).toHaveBeenCalledWith('/profile-a', analysis) + }) + + it('serves the cached analysis through getCachedAnalysis', async () => { + const cached = createAnalysis(3) + readAnalysisSnapshotMock.mockResolvedValue(cached) + + registerWorkspaceSpaceHandlers(createStore()) + expect(removeHandlerMock).toHaveBeenCalledWith('workspaceSpace:getCachedAnalysis') + const cachedHandler = handlers.get('workspaceSpace:getCachedAnalysis') + + await expect(cachedHandler!()).resolves.toBe(cached) + expect(readAnalysisSnapshotMock).toHaveBeenCalledWith('/profile-a') }) }) diff --git a/src/main/ipc/workspace-space.ts b/src/main/ipc/workspace-space.ts index d4449f3db52..5b10b039b31 100644 --- a/src/main/ipc/workspace-space.ts +++ b/src/main/ipc/workspace-space.ts @@ -1,13 +1,19 @@ import { ipcMain } from 'electron' import type { Store } from '../persistence' import type { + WorkspaceSpaceAnalysis, WorkspaceSpaceAnalyzeResult, - WorkspaceSpaceScanProgress + WorkspaceSpaceScanProgress, + WorkspaceSpaceWorktreeMeasurement } from '../../shared/workspace-space-types' import { analyzeWorkspaceSpace, WorkspaceSpaceScanCancelledError } from '../workspace-space-analysis' +import { + persistWorkspaceSpaceAnalysisSnapshot, + readWorkspaceSpaceAnalysisSnapshot +} from '../workspace-space-analysis-snapshot' const PROGRESS_EMIT_INTERVAL_MS = 100 @@ -19,9 +25,11 @@ type InFlightWorkspaceSpaceScan = { } export function registerWorkspaceSpaceHandlers(store: Store): void { + const snapshotDirectory = store.getProfileStorageDirectory() let inFlightScan: InFlightWorkspaceSpaceScan | null = null ipcMain.removeHandler('workspaceSpace:cancel') ipcMain.removeHandler('workspaceSpace:analyze') + ipcMain.removeHandler('workspaceSpace:getCachedAnalysis') ipcMain.handle('workspaceSpace:analyze', async (event): Promise => { if (!inFlightScan) { const controller = new AbortController() @@ -39,7 +47,11 @@ export function registerWorkspaceSpaceHandlers(store: Store): void { currentWorktreeDisplayName: null } let lastProgressSentAt = 0 + let pendingMeasurements: WorkspaceSpaceWorktreeMeasurement[] = [] const sendProgress = (progress: WorkspaceSpaceScanProgress): void => { + if (progress.completedMeasurements?.length) { + pendingMeasurements.push(...progress.completedMeasurements) + } // Why: large fleets can report one progress event per worktree; keep // the UI responsive without repainting the full Space page for each row. const now = Date.now() @@ -57,7 +69,12 @@ export function registerWorkspaceSpaceHandlers(store: Store): void { } lastProgressSentAt = now if (!event.sender.isDestroyed()) { - event.sender.send('workspaceSpace:progress', progress) + const completedMeasurements = pendingMeasurements + pendingMeasurements = [] + event.sender.send('workspaceSpace:progress', { + ...progress, + ...(completedMeasurements.length > 0 ? { completedMeasurements } : {}) + }) } } // Why: large worktree fleets require real disk traversal; duplicate @@ -78,7 +95,12 @@ export function registerWorkspaceSpaceHandlers(store: Store): void { sendProgress(progress) } }) - .then((analysis): WorkspaceSpaceAnalyzeResult => ({ ok: true, analysis })) + .then((analysis): WorkspaceSpaceAnalyzeResult => { + // Fire-and-forget: a ~6-minute cold analysis must survive reload/restart, but + // persistence must never delay or fail the reply. + void persistWorkspaceSpaceAnalysisSnapshot(snapshotDirectory, analysis) + return { ok: true, analysis } + }) .catch((error: unknown): WorkspaceSpaceAnalyzeResult => { if (error instanceof WorkspaceSpaceScanCancelledError) { return { ok: false, cancelled: true } @@ -92,6 +114,12 @@ export function registerWorkspaceSpaceHandlers(store: Store): void { return inFlightScan.promise }) + ipcMain.handle( + 'workspaceSpace:getCachedAnalysis', + (): Promise => + readWorkspaceSpaceAnalysisSnapshot(snapshotDirectory) + ) + ipcMain.handle('workspaceSpace:cancel', async (): Promise => { if (!inFlightScan || inFlightScan.controller.signal.aborted) { return false diff --git a/src/main/ipc/worktrees-windows.test.ts b/src/main/ipc/worktrees-windows.test.ts index ee9540048cd..692c2ea6d51 100644 --- a/src/main/ipc/worktrees-windows.test.ts +++ b/src/main/ipc/worktrees-windows.test.ts @@ -151,6 +151,7 @@ describe('registerWorktreeHandlers – Windows path handling', () => { } } const store = { + getProfileStorageDirectory: vi.fn(() => '/profile-a'), getRepos: vi.fn(), getRepo: vi.fn(), getProjects: vi.fn(), diff --git a/src/main/ipc/worktrees.test.ts b/src/main/ipc/worktrees.test.ts index 9e2ae8ea8aa..b9ea11b85c8 100644 --- a/src/main/ipc/worktrees.test.ts +++ b/src/main/ipc/worktrees.test.ts @@ -236,6 +236,36 @@ vi.mock('../ports/advertised-url-watcher', () => ({ } })) +const { + pruneCleanupScanSnapshotMock, + pruneCleanupScanSnapshotsMock, + pruneSpaceAnalysisSnapshotMock, + pruneSpaceAnalysisSnapshotsMock +} = vi.hoisted(() => ({ + pruneCleanupScanSnapshotMock: vi.fn().mockResolvedValue(undefined), + pruneCleanupScanSnapshotsMock: vi.fn().mockResolvedValue(undefined), + pruneSpaceAnalysisSnapshotMock: vi.fn().mockResolvedValue(undefined), + pruneSpaceAnalysisSnapshotsMock: vi.fn().mockResolvedValue(undefined) +})) + +vi.mock('../workspace-cleanup-scan-snapshot', () => ({ + pruneWorkspaceCleanupScanSnapshot: pruneCleanupScanSnapshotMock, + pruneWorkspaceCleanupScanSnapshots: pruneCleanupScanSnapshotsMock +})) + +vi.mock('../workspace-space-analysis-snapshot', () => ({ + pruneWorkspaceSpaceAnalysisSnapshot: pruneSpaceAnalysisSnapshotMock, + pruneWorkspaceSpaceAnalysisSnapshots: pruneSpaceAnalysisSnapshotsMock +})) + +const { recordRemovalSnapshotPruneMock } = vi.hoisted(() => ({ + recordRemovalSnapshotPruneMock: vi.fn() +})) + +vi.mock('../workspace-cleanup-removal-snapshot-prune', () => ({ + recordWorkspaceCleanupRemovalSnapshotPrune: recordRemovalSnapshotPruneMock +})) + const { killAllProcessesForWorktreeMock, clearProviderPtyStateMock, @@ -303,6 +333,7 @@ describe('registerWorktreeHandlers', () => { } const ipcEvent = { sender: { id: 1 } } const store = { + getProfileStorageDirectory: vi.fn(() => '/profile-a'), getRepos: vi.fn(), getRepo: vi.fn(), getProjects: vi.fn(), @@ -408,6 +439,11 @@ describe('registerWorktreeHandlers', () => { getSshPtyProviderMock, deleteWorktreeHistoryDirMock, advertisedUrlWatcherForgetWorktreeMock, + pruneCleanupScanSnapshotMock, + pruneCleanupScanSnapshotsMock, + pruneSpaceAnalysisSnapshotMock, + pruneSpaceAnalysisSnapshotsMock, + recordRemovalSnapshotPruneMock, findExistingWorktreeSymlinkPathsMock, removeWorktreeLinkedPathsMock ]) { @@ -2709,6 +2745,10 @@ describe('registerWorktreeHandlers', () => { const metaById: Record> = { 'repo-1::/remote/repo': makeWorktreeMeta({ displayName: 'main', hostId: sshHostId }), 'repo-1::/remote/deleted': makeWorktreeMeta({ displayName: 'deleted', hostId: sshHostId }), + 'repo-1::/remote/deleted-too': makeWorktreeMeta({ + displayName: 'deleted too', + hostId: sshHostId + }), 'repo-1::/remote/other-host': makeWorktreeMeta({ displayName: 'other host', hostId: toSshExecutionHostId('target-b') @@ -2726,18 +2766,33 @@ describe('registerWorktreeHandlers', () => { const forgotten = await handlers['worktrees:forgetRemovedForExecutionHost'](null, { repoId: sshRepo.id, executionHostId: sshHostId, - // Only the first is this host's row; the rest must survive an over-broad request. + // Only the first two are this host's rows; the rest must survive an over-broad request. worktreeIds: [ 'repo-1::/remote/deleted', + 'repo-1::/remote/deleted-too', 'repo-1::/remote/other-host', 'repo-2::/remote/other-repo', 'repo-1::/remote/never-persisted' ] }) - expect(forgotten).toEqual({ forgottenWorktreeIds: ['repo-1::/remote/deleted'] }) - expect(store.removeWorktreeMeta).toHaveBeenCalledTimes(1) + expect(forgotten).toEqual({ + forgottenWorktreeIds: ['repo-1::/remote/deleted', 'repo-1::/remote/deleted-too'] + }) + expect(store.removeWorktreeMeta).toHaveBeenCalledTimes(2) expect(store.removeWorktreeMeta).toHaveBeenCalledWith('repo-1::/remote/deleted', sshHostId) + const pruneTargets = [ + { worktreeId: 'repo-1::/remote/deleted', executionHostId: sshHostId }, + { worktreeId: 'repo-1::/remote/deleted-too', executionHostId: sshHostId } + ] + expect(pruneCleanupScanSnapshotsMock).toHaveBeenCalledExactlyOnceWith( + '/profile-a', + pruneTargets + ) + expect(pruneSpaceAnalysisSnapshotsMock).toHaveBeenCalledExactlyOnceWith( + '/profile-a', + pruneTargets + ) // Why: the renderer's suppression memory is session-scoped, so the next launch must not re-list the row. const relisted = (await handlers['worktrees:listKnownForExecutionHost'](null, { @@ -8295,6 +8350,45 @@ describe('registerWorktreeHandlers', () => { }) }) + it('prunes the persisted cleanup and space snapshots on removal', async () => { + mockKnownFeatureWorktree() + getEffectiveHooksMock.mockReturnValue(null) + removeWorktreeMock.mockResolvedValue({}) + + await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) + + // A removed workspace must never resurrect from the cached scan snapshots. + expect(pruneCleanupScanSnapshotMock).toHaveBeenCalledWith( + '/profile-a', + 'repo-1::/workspace/feature-wt', + 'local' + ) + expect(pruneSpaceAnalysisSnapshotMock).toHaveBeenCalledWith( + '/profile-a', + 'repo-1::/workspace/feature-wt', + 'local' + ) + }) + + it('tombstones a cleanup-batch removal without scheduling singular sidecar writes', async () => { + mockKnownFeatureWorktree() + getEffectiveHooksMock.mockReturnValue(null) + removeWorktreeMock.mockResolvedValue({}) + + await handlers['worktrees:remove'](null, { + worktreeId: 'repo-1::/workspace/feature-wt', + snapshotPruneBatchId: 'cleanup-batch-1' + }) + + expect(recordRemovalSnapshotPruneMock).toHaveBeenCalledExactlyOnceWith('/profile-a', { + batchId: 'cleanup-batch-1', + worktreeId: 'repo-1::/workspace/feature-wt', + executionHostId: 'local' + }) + expect(pruneCleanupScanSnapshotMock).not.toHaveBeenCalled() + expect(pruneSpaceAnalysisSnapshotMock).not.toHaveBeenCalled() + }) + it('traces the removal as worktree.remove with a stage sub-span tree', async () => { const records: RedactableSpan[] = [] setActiveSink({ diff --git a/src/main/ipc/worktrees.ts b/src/main/ipc/worktrees.ts index 32c85af85a0..0ece74f7bb6 100644 --- a/src/main/ipc/worktrees.ts +++ b/src/main/ipc/worktrees.ts @@ -17,6 +17,15 @@ import { getProjectGroupSubtreeIds } from '../../shared/project-groups' import { projectResolvedWorktreeLineage } from '../../shared/resolved-worktree-lineage' import { isPathInsideOrEqual, isWindowsAbsolutePathLike } from '../../shared/cross-platform-path' import { deleteWorktreeHistoryDir } from '../terminal-history-deletion' +import { + pruneWorkspaceCleanupScanSnapshot, + pruneWorkspaceCleanupScanSnapshots +} from '../workspace-cleanup-scan-snapshot' +import { + pruneWorkspaceSpaceAnalysisSnapshot, + pruneWorkspaceSpaceAnalysisSnapshots +} from '../workspace-space-analysis-snapshot' +import { recordWorkspaceCleanupRemovalSnapshotPrune } from '../workspace-cleanup-removal-snapshot-prune' import type { OrcaHooks } from '../../shared/orca-yaml-hook-types' import type { Repo } from '../../shared/repo-types' import type { @@ -162,6 +171,7 @@ type RemoveWorktreeArgs = { /** Explicit Force Delete only — `force` alone is set by the ordinary confirmation (#11960). */ allowUnverifiedPtyStop?: boolean skipArchive?: boolean + snapshotPruneBatchId?: string } type DetectedWorktreeRequestArgs = { repoId: string } | ListDetectedWorktreesArgs @@ -326,7 +336,8 @@ function resolveWorktreeRemovalOwnerHostId( function removeWorktreeMetadataAndTransientState( store: Store, worktreeId: string, - hostId?: ExecutionHostId + hostId?: ExecutionHostId, + snapshotPruneBatchId?: string ): void { // Why: worktree IDs are path-derived and reusable; drop process-local caches before the same ID can map to a new workspace. if (hostId) { @@ -341,6 +352,18 @@ function removeWorktreeMetadataAndTransientState( deleteWorktreeHistoryDir(worktreeId) // Why: release the removed worktree's PR-refresh aliases so coalesced queue entries don't retain it all session (memory creep). pruneWorktreePRRefreshAliases(worktreeId) + // Why: removed workspaces must never resurrect from the persisted cleanup/space scan snapshots. + const snapshotDirectory = store.getProfileStorageDirectory() + if (snapshotPruneBatchId) { + recordWorkspaceCleanupRemovalSnapshotPrune(snapshotDirectory, { + batchId: snapshotPruneBatchId, + worktreeId, + executionHostId: hostId + }) + return + } + void pruneWorkspaceCleanupScanSnapshot(snapshotDirectory, worktreeId, hostId) + void pruneWorkspaceSpaceAnalysisSnapshot(snapshotDirectory, worktreeId, hostId) } function getProjectHostSetupMetaUpdates( @@ -2072,6 +2095,15 @@ export function registerWorktreeHandlers( store.removeWorktreeMeta(worktreeId, requestedExecutionHostId) forgottenWorktreeIds.push(worktreeId) } + if (forgottenWorktreeIds.length > 0) { + const snapshotDirectory = store.getProfileStorageDirectory() + const targets = forgottenWorktreeIds.map((worktreeId) => ({ + worktreeId, + executionHostId: requestedExecutionHostId + })) + void pruneWorkspaceCleanupScanSnapshots(snapshotDirectory, targets) + void pruneWorkspaceSpaceAnalysisSnapshots(snapshotDirectory, targets) + } return { forgottenWorktreeIds } } ) @@ -2483,7 +2515,12 @@ export function registerWorktreeHandlers( }) }) await withWorktreeRemoveStageSpan('metadata_purge', 'folder', async () => { - removeWorktreeMetadataAndTransientState(store, args.worktreeId, removalHostId) + removeWorktreeMetadataAndTransientState( + store, + args.worktreeId, + removalHostId, + args.snapshotPruneBatchId + ) }) preservedBranchCleanupByScope.delete( preservedBranchCleanupScopeKey({ worktreeId: args.worktreeId, hostId: removalHostId }) @@ -2595,7 +2632,12 @@ export function registerWorktreeHandlers( invalidateAuthorizedRootsCache() } runtime.clearOptimisticReconcileToken(args.worktreeId) - removeWorktreeMetadataAndTransientState(store, args.worktreeId, removalHostId) + removeWorktreeMetadataAndTransientState( + store, + args.worktreeId, + removalHostId, + args.snapshotPruneBatchId + ) preservedBranchCleanupByScope.delete( preservedBranchCleanupScopeKey({ worktreeId: args.worktreeId, @@ -2645,7 +2687,12 @@ export function registerWorktreeHandlers( localWorktreeGitOptions ) runtime.clearOptimisticReconcileToken(args.worktreeId) - removeWorktreeMetadataAndTransientState(store, args.worktreeId, removalHostId) + removeWorktreeMetadataAndTransientState( + store, + args.worktreeId, + removalHostId, + args.snapshotPruneBatchId + ) preservedBranchCleanupByScope.delete( preservedBranchCleanupScopeKey({ worktreeId: args.worktreeId, @@ -2682,7 +2729,12 @@ export function registerWorktreeHandlers( invalidateAuthorizedRootsCache() } runtime.clearOptimisticReconcileToken(args.worktreeId) - removeWorktreeMetadataAndTransientState(store, args.worktreeId, removalHostId) + removeWorktreeMetadataAndTransientState( + store, + args.worktreeId, + removalHostId, + args.snapshotPruneBatchId + ) preservedBranchCleanupByScope.delete( preservedBranchCleanupScopeKey({ worktreeId: args.worktreeId, @@ -2742,7 +2794,12 @@ export function registerWorktreeHandlers( removedPushTarget ) runtime.clearOptimisticReconcileToken(args.worktreeId) - removeWorktreeMetadataAndTransientState(store, args.worktreeId, removalHostId) + removeWorktreeMetadataAndTransientState( + store, + args.worktreeId, + removalHostId, + args.snapshotPruneBatchId + ) invalidateAuthorizedRootsCache() notifyWorktreesChanged(mainWindow, repoId) return removalResult ?? {} @@ -2841,7 +2898,12 @@ export function registerWorktreeHandlers( ) runtime.clearOptimisticReconcileToken(args.worktreeId) await withWorktreeRemoveStageSpan('metadata_purge', 'remote', async () => { - removeWorktreeMetadataAndTransientState(store, args.worktreeId, removalHostId) + removeWorktreeMetadataAndTransientState( + store, + args.worktreeId, + removalHostId, + args.snapshotPruneBatchId + ) }) notifyWorktreesChanged(mainWindow, repoId) return removalResult ?? {} @@ -2987,7 +3049,12 @@ export function registerWorktreeHandlers( localWorktreeGitOptions ) runtime.clearOptimisticReconcileToken(args.worktreeId) - removeWorktreeMetadataAndTransientState(store, args.worktreeId, removalHostId) + removeWorktreeMetadataAndTransientState( + store, + args.worktreeId, + removalHostId, + args.snapshotPruneBatchId + ) preservedBranchCleanupByScope.delete( preservedBranchCleanupScopeKey({ worktreeId: args.worktreeId, @@ -3024,7 +3091,12 @@ export function registerWorktreeHandlers( ) runtime.clearOptimisticReconcileToken(args.worktreeId) await withWorktreeRemoveStageSpan('metadata_purge', 'local', async () => { - removeWorktreeMetadataAndTransientState(store, args.worktreeId, removalHostId) + removeWorktreeMetadataAndTransientState( + store, + args.worktreeId, + removalHostId, + args.snapshotPruneBatchId + ) }) await withWorktreeRemoveStageSpan('cache_invalidation', 'local', async () => { invalidateAuthorizedRootsCache() @@ -3056,7 +3128,7 @@ export function registerWorktreeHandlers( 'worktrees:forgetLocal', async ( _event, - args: Pick + args: Pick ): Promise => { const { repoId } = parseWorktreeId(args.worktreeId) const repo = getRepoForWorktreeRemoval(store, repoId, args.hostId) @@ -3118,7 +3190,12 @@ export function registerWorktreeHandlers( runtime.clearOptimisticReconcileToken(args.worktreeId) // The resolved owner, not args.hostId: an orphan forget with no hostId still has to purge its SSH/runtime partition. - removeWorktreeMetadataAndTransientState(store, args.worktreeId, ownerHost?.id) + removeWorktreeMetadataAndTransientState( + store, + args.worktreeId, + ownerHost?.id, + args.snapshotPruneBatchId + ) // Why: cached roots outlive the forgotten workspace, so an ownerless path stays filesystem-authorized until a rebuild. invalidateAuthorizedRootsCache() if (ownerHost?.id) { diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts index b92ed0c86c0..4bdcbce262c 100644 --- a/src/main/persistence.test.ts +++ b/src/main/persistence.test.ts @@ -37,6 +37,7 @@ import { SshConnectionStore } from './ssh/ssh-connection-store' import { setSourceControlActionDefault } from '../shared/source-control-ai-actions' import { LEGACY_DEFAULT_SSH_RELAY_GRACE_PERIOD_SECONDS } from '../shared/ssh-types' import { closeTerminalTabInWorkspaceSession } from '../shared/workspace-session-terminal-tab-close' +import { createDefaultWorkspaceCleanupBrowseState } from '../shared/workspace-cleanup-browse-state' // Shared mutable state so the electron mock can reference a per-test directory const testState = { dir: '' } @@ -7129,6 +7130,29 @@ describe('Store', () => { }) }) + it('updateUI preserves browse state when a legacy peer publishes dismissals only', async () => { + const store = await createStore() + const browse = createDefaultWorkspaceCleanupBrowseState() + browse.filters.query = 'stale' + + store.updateUI({ workspaceCleanup: { dismissals: {}, browse } }) + store.updateUI({ + workspaceCleanup: { + dismissals: { + 'wt-1': { + worktreeId: 'wt-1', + dismissedAt: 1700000000000, + fingerprint: 'fp-1', + classifierVersion: 2 + } + } + } + }) + + expect(store.getUI().workspaceCleanup?.browse).toEqual(browse) + expect(store.getUI().workspaceCleanup?.dismissals).toHaveProperty('wt-1') + }) + it('updateUI merges contextual tour seen ids instead of replacing stale snapshots', async () => { const store = await createStore() diff --git a/src/main/persistence.ts b/src/main/persistence.ts index b01a298d62f..c113c0f8d1c 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -186,6 +186,7 @@ import { } from '../shared/cross-platform-path' import { normalizeTerminalQuickCommands } from '../shared/terminal-quick-commands' import { normalizeTaskProviderSettings } from '../shared/task-providers' +import { mergeWorkspaceCleanupUIState } from '../shared/workspace-cleanup-ui-state' import { normalizeAutoRenameBranchFromWorkDefaultOn } from '../shared/auto-rename-branch-from-work-settings' import { addMobilePairingCustomAddress, @@ -4313,6 +4314,10 @@ export class Store { // ── Repos ────────────────────────────────────────────────────────── + getProfileStorageDirectory(): string { + return dirname(this.dataFile) + } + getRepos(): Repo[] { return this.state.repos.map((repo) => this.hydrateRepo(repo)) } @@ -6219,6 +6224,10 @@ export class Store { const nextUI = { ...currentUI, ...durableUpdates, + workspaceCleanup: mergeWorkspaceCleanupUIState( + currentUI.workspaceCleanup, + durableUpdates.workspaceCleanup + ), groupBy: durableUpdates.groupBy ? normalizeGroupBy(durableUpdates.groupBy) : normalizeGroupBy(this.state.ui?.groupBy), diff --git a/src/main/runtime/rpc/methods/client-ui-schemas.ts b/src/main/runtime/rpc/methods/client-ui-schemas.ts index 2e2555038ed..8a333894520 100644 --- a/src/main/runtime/rpc/methods/client-ui-schemas.ts +++ b/src/main/runtime/rpc/methods/client-ui-schemas.ts @@ -21,6 +21,7 @@ import { isPluginPanelTabKey } from '../../../../shared/plugins/plugin-manifest' import type { TaskProvider } from '../../../../shared/task-providers' import { ClientUiWorkspaceFilterFields } from './client-ui-workspace-filter-fields' import { TaskResumeState } from './task-resume-state-schema' +import { WorkspaceCleanup } from './workspace-cleanup-ui-schema' import { omitUndefinedValues, tolerateUnknownValues } from './ui-update-value-tolerance' const NullableString = z.string().nullable() @@ -77,19 +78,6 @@ const WorkspaceStatusDefinition = z.object({ color: z.string().optional(), icon: z.string().optional() }) -const WorkspaceCleanupDismissal = z - .object({ - worktreeId: z.string(), - dismissedAt: z.number().finite(), - fingerprint: z.string(), - classifierVersion: z.number().finite() - }) - .strict() -const WorkspaceCleanup = z - .object({ - dismissals: z.record(z.string(), WorkspaceCleanupDismissal) - }) - .strict() const FeatureInteractionRecord = z .object({ firstInteractedAt: z.number().finite().nonnegative(), diff --git a/src/main/runtime/rpc/methods/ui-state-schema-parity-checks.ts b/src/main/runtime/rpc/methods/ui-state-schema-parity-checks.ts index 2af2ffe0ed8..a8fe8ca00c7 100644 --- a/src/main/runtime/rpc/methods/ui-state-schema-parity-checks.ts +++ b/src/main/runtime/rpc/methods/ui-state-schema-parity-checks.ts @@ -1,5 +1,6 @@ import type { z } from 'zod' import type { PersistedUIState } from '../../../../shared/persisted-ui-state-types' +import type { WorkspaceCleanupUIState } from '../../../../shared/workspace-cleanup' import type { UiUpdateFieldsSchema } from './client-ui-schemas' import type { AssertNoMissingKeys, AssertNoMissingValues } from './ui-state-schema-parity' @@ -34,3 +35,19 @@ const _uiUpdateValueParity: AssertNoMissingValues< z.input > = true void _uiUpdateValueParity + +// Why a nested pair: the guards above only walk PersistedUIState's OWN keys, and +// a structural `extends` is satisfied by extra properties — so a sub-key added +// to workspaceCleanup (e.g. `browse`) and never added to its `.strict()` schema +// would keep both green while every paired client's ui.set payload is rejected. +type WorkspaceCleanupSchema = NonNullable['workspaceCleanup']> +const _workspaceCleanupParity: AssertNoMissingKeys< + WorkspaceCleanupUIState, + WorkspaceCleanupSchema +> = true +void _workspaceCleanupParity +const _workspaceCleanupValueParity: AssertNoMissingValues< + WorkspaceCleanupUIState, + WorkspaceCleanupSchema +> = true +void _workspaceCleanupValueParity diff --git a/src/main/runtime/rpc/methods/workspace-cleanup-ui-schema.test.ts b/src/main/runtime/rpc/methods/workspace-cleanup-ui-schema.test.ts new file mode 100644 index 00000000000..090a7981147 --- /dev/null +++ b/src/main/runtime/rpc/methods/workspace-cleanup-ui-schema.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from 'vitest' +import { UiUpdate } from './client-ui-schemas' +import { WorkspaceCleanup } from './workspace-cleanup-ui-schema' +import { createDefaultWorkspaceCleanupBrowseState } from '../../../../shared/workspace-cleanup-browse-state' + +const DISMISSAL = { + worktreeId: 'wt-1', + dismissedAt: 1700000000000, + fingerprint: '2|main|abc|clean|19675', + classifierVersion: 2 +} + +describe('workspaceCleanup client-ui schema', () => { + it('accepts a default browse state', () => { + const browse = createDefaultWorkspaceCleanupBrowseState() + const parsed = WorkspaceCleanup.parse({ dismissals: { 'wt-1': DISMISSAL }, browse }) + + expect(parsed.browse).toEqual(browse) + expect(parsed.dismissals['wt-1']).toEqual(DISMISSAL) + }) + + it('still accepts a dismissals-only payload from a client that predates browse', () => { + expect(WorkspaceCleanup.parse({ dismissals: {} })).toEqual({ dismissals: {} }) + }) + + // The wire contract: a NEWER client's filter groups must never fail an older + // host's schema, or the whole ui.set payload is refused (dismissals included). + it('accepts a browse state carrying unknown future fields and narrows it', () => { + const parsed = WorkspaceCleanup.parse({ + dismissals: {}, + browse: { + version: 7, + activePresetId: 'suggested', + futureTopLevelField: ['whatever'], + filters: { + query: 'kept', + futureGroup: { enabled: true }, + activity: { idleSignal: 'invented-signal', idleMinDays: 14 } + }, + sort: { field: 'invented-column', direction: 'desc' }, + customPresets: [] + } + }) + + expect(parsed.browse?.filters.query).toBe('kept') + expect(parsed.browse?.filters.activity.idleMinDays).toBe(14) + expect(parsed.browse?.filters.activity.idleSignal).toBe('last-visited') + expect(parsed.browse?.sort).toEqual({ field: 'last-activity', direction: 'desc' }) + expect(parsed.browse).not.toHaveProperty('activePresetId') + expect(parsed.browse).not.toHaveProperty('customPresets') + expect(parsed.browse).not.toHaveProperty('futureTopLevelField') + }) + + it('carries browse through the full UiUpdate payload without dropping siblings', () => { + const browse = createDefaultWorkspaceCleanupBrowseState() + browse.filters.query = 'stale' + + const parsed = UiUpdate.parse({ + sidebarWidth: 320, + workspaceCleanup: { dismissals: { 'wt-1': DISMISSAL }, browse } + }) + + expect(parsed.sidebarWidth).toBe(320) + expect(parsed.workspaceCleanup?.browse?.filters.query).toBe('stale') + }) +}) diff --git a/src/main/runtime/rpc/methods/workspace-cleanup-ui-schema.ts b/src/main/runtime/rpc/methods/workspace-cleanup-ui-schema.ts new file mode 100644 index 00000000000..79984b55721 --- /dev/null +++ b/src/main/runtime/rpc/methods/workspace-cleanup-ui-schema.ts @@ -0,0 +1,29 @@ +import { z } from 'zod' +import { + normalizeWorkspaceCleanupBrowseState, + type WorkspaceCleanupBrowseState +} from '../../../../shared/workspace-cleanup-browse-state' + +const WorkspaceCleanupDismissal = z.object({ + worktreeId: z.string(), + dismissedAt: z.number().finite(), + fingerprint: z.string(), + classifierVersion: z.number().finite() +}) + +/** + * Deliberately unvalidated shape, then normalized: the filter groups must NOT be + * strict or enumerated here. A newer client sends filters this build has never + * heard of, and a per-field zod shape would reject the whole `ui.set` payload + * instead of persisting the parts the host does understand. The shared + * normalizer never throws and degrades field by field, so an older host narrows + * the state rather than refusing it. + */ +const WorkspaceCleanupBrowse = z + .custom() + .transform((value) => normalizeWorkspaceCleanupBrowseState(value)) + +export const WorkspaceCleanup = z.object({ + dismissals: z.record(z.string(), WorkspaceCleanupDismissal), + browse: WorkspaceCleanupBrowse.optional() +}) diff --git a/src/main/sidecar-snapshot-file.ts b/src/main/sidecar-snapshot-file.ts new file mode 100644 index 00000000000..f7c0f23c0e4 --- /dev/null +++ b/src/main/sidecar-snapshot-file.ts @@ -0,0 +1,51 @@ +// Why a sidecar next to orca-data.json: scan snapshots rewrite wholesale and can reach hundreds +// of KB; folding them into orca-data.json would rewrite the whole state file per scan (the +// githubCache sidecar precedent). Best-effort by design: a lost snapshot only costs a rescan. +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import { + durableWriteTempPath, + removeStaleDurableWriteTempFiles, + writeFileDurable +} from './durable-file-write' + +const queues = new Map>() +const staleTempCleanups = new Map>() +const STALE_TEMP_AGE_MS = 24 * 60 * 60 * 1000 + +export function sidecarSnapshotFile(snapshotDirectory: string, fileName: string): string { + return join(snapshotDirectory, fileName) +} + +/** Serialize tasks per sidecar so read-modify-write patch/prune cycles cannot interleave. */ +export function withSidecarSnapshotQueue(file: string, task: () => Promise): Promise { + const previous = queues.get(file) ?? Promise.resolve() + const run = previous.then(task, task) + queues.set( + file, + run.then( + () => undefined, + () => undefined + ) + ) + return run +} + +/** Parsed sidecar contents, or null when the file is missing or not valid JSON. */ +export async function readSidecarSnapshot(file: string): Promise { + try { + return JSON.parse(await readFile(file, 'utf-8')) as unknown + } catch { + return null + } +} + +export async function writeSidecarSnapshot(file: string, payload: unknown): Promise { + let cleanup = staleTempCleanups.get(file) + if (!cleanup) { + cleanup = removeStaleDurableWriteTempFiles(file, { minimumAgeMs: STALE_TEMP_AGE_MS }) + staleTempCleanups.set(file, cleanup) + } + await cleanup + await writeFileDurable(durableWriteTempPath(file), file, JSON.stringify(payload)) +} diff --git a/src/main/window/attach-main-window-services.test.ts b/src/main/window/attach-main-window-services.test.ts index 28fbeb4cd3d..ac473733753 100644 --- a/src/main/window/attach-main-window-services.test.ts +++ b/src/main/window/attach-main-window-services.test.ts @@ -165,8 +165,9 @@ function createMainWindow( function createStore(): Store & { flushPendingAsync: MockFn } { return { + getProfileStorageDirectory: vi.fn(() => '/profile-a'), flushPendingAsync: vi.fn(() => Promise.resolve()) - } as Store & { flushPendingAsync: MockFn } + } as unknown as Store & { flushPendingAsync: MockFn } } function createRuntime(): RuntimeStub { diff --git a/src/main/workspace-cleanup-removal-snapshot-prune.test.ts b/src/main/workspace-cleanup-removal-snapshot-prune.test.ts new file mode 100644 index 00000000000..466476b10c3 --- /dev/null +++ b/src/main/workspace-cleanup-removal-snapshot-prune.test.ts @@ -0,0 +1,126 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { + finalizeCleanupSnapshotsMock, + finalizeSpaceSnapshotsMock, + pruneCleanupSnapshotsMock, + pruneSpaceSnapshotsMock, + registerCleanupTombstonesMock, + registerSpaceTombstonesMock +} = vi.hoisted(() => ({ + finalizeCleanupSnapshotsMock: vi.fn().mockResolvedValue(undefined), + finalizeSpaceSnapshotsMock: vi.fn().mockResolvedValue(undefined), + pruneCleanupSnapshotsMock: vi.fn().mockResolvedValue(undefined), + pruneSpaceSnapshotsMock: vi.fn().mockResolvedValue(undefined), + registerCleanupTombstonesMock: vi.fn(), + registerSpaceTombstonesMock: vi.fn() +})) + +vi.mock('./workspace-cleanup-scan-snapshot', () => ({ + finalizeWorkspaceCleanupScanSnapshotPrunes: finalizeCleanupSnapshotsMock, + pruneWorkspaceCleanupScanSnapshots: pruneCleanupSnapshotsMock, + registerWorkspaceCleanupScanSnapshotPruneTombstones: registerCleanupTombstonesMock +})) + +vi.mock('./workspace-space-analysis-snapshot', () => ({ + finalizeWorkspaceSpaceAnalysisSnapshotPrunes: finalizeSpaceSnapshotsMock, + pruneWorkspaceSpaceAnalysisSnapshots: pruneSpaceSnapshotsMock, + registerWorkspaceSpaceAnalysisSnapshotPruneTombstones: registerSpaceTombstonesMock +})) + +import { + beginWorkspaceCleanupRemovalSnapshotPruneBatch, + finishWorkspaceCleanupRemovalSnapshotPruneBatch, + recordWorkspaceCleanupRemovalSnapshotPrune, + resetWorkspaceCleanupRemovalSnapshotPruneBatchesForTests +} from './workspace-cleanup-removal-snapshot-prune' + +describe('workspace cleanup removal snapshot prune batch', () => { + beforeEach(() => { + vi.clearAllMocks() + resetWorkspaceCleanupRemovalSnapshotPruneBatchesForTests() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('registers every sequential tombstone immediately and flushes once per sidecar', async () => { + const snapshotDirectory = '/profile-a' + const batch = { batchId: 'batch-1' } + const targets = Array.from({ length: 100 }, (_, index) => ({ + worktreeId: `repo-${index % 10}::/workspace-${index}`, + executionHostId: index % 2 === 0 ? ('local' as const) : (`ssh:ssh-${index % 3}` as const) + })) + beginWorkspaceCleanupRemovalSnapshotPruneBatch(snapshotDirectory, batch) + + for (const target of targets) { + recordWorkspaceCleanupRemovalSnapshotPrune(snapshotDirectory, { ...batch, ...target }) + } + recordWorkspaceCleanupRemovalSnapshotPrune(snapshotDirectory, { ...batch, ...targets[0] }) + + expect(registerCleanupTombstonesMock).toHaveBeenCalledTimes(100) + expect(registerSpaceTombstonesMock).toHaveBeenCalledTimes(100) + expect(finalizeCleanupSnapshotsMock).not.toHaveBeenCalled() + expect(finalizeSpaceSnapshotsMock).not.toHaveBeenCalled() + + await finishWorkspaceCleanupRemovalSnapshotPruneBatch(snapshotDirectory, batch) + + expect(finalizeCleanupSnapshotsMock).toHaveBeenCalledExactlyOnceWith(snapshotDirectory, targets) + expect(finalizeSpaceSnapshotsMock).toHaveBeenCalledExactlyOnceWith(snapshotDirectory, targets) + }) + + it('falls back to immediate best-effort pruning when no batch is active', () => { + const target = { worktreeId: 'repo-1::/workspace', executionHostId: 'runtime:vm-1' as const } + + recordWorkspaceCleanupRemovalSnapshotPrune('/profile-a', { + batchId: 'missing-batch', + ...target + }) + + expect(pruneCleanupSnapshotsMock).toHaveBeenCalledExactlyOnceWith('/profile-a', [target]) + expect(pruneSpaceSnapshotsMock).toHaveBeenCalledExactlyOnceWith('/profile-a', [target]) + }) + + it('finalizes only the matching profile and batch scope', async () => { + const batch = { batchId: 'shared-id' } + const profileATarget = { worktreeId: 'repo-a::/workspace', executionHostId: 'local' as const } + const profileBTarget = { + worktreeId: 'repo-b::/workspace', + executionHostId: 'ssh:ssh-1' as const + } + beginWorkspaceCleanupRemovalSnapshotPruneBatch('/profile-a', batch) + beginWorkspaceCleanupRemovalSnapshotPruneBatch('/profile-b', batch) + recordWorkspaceCleanupRemovalSnapshotPrune('/profile-a', { ...batch, ...profileATarget }) + recordWorkspaceCleanupRemovalSnapshotPrune('/profile-b', { ...batch, ...profileBTarget }) + + await finishWorkspaceCleanupRemovalSnapshotPruneBatch('/profile-a', batch) + + expect(finalizeCleanupSnapshotsMock).toHaveBeenCalledExactlyOnceWith('/profile-a', [ + profileATarget + ]) + expect(finalizeSpaceSnapshotsMock).toHaveBeenCalledExactlyOnceWith('/profile-a', [ + profileATarget + ]) + + await finishWorkspaceCleanupRemovalSnapshotPruneBatch('/profile-b', batch) + expect(finalizeCleanupSnapshotsMock).toHaveBeenNthCalledWith(2, '/profile-b', [profileBTarget]) + expect(finalizeSpaceSnapshotsMock).toHaveBeenNthCalledWith(2, '/profile-b', [profileBTarget]) + }) + + it('expires and finalizes an unfinished idle batch', async () => { + vi.useFakeTimers() + const batch = { batchId: 'abandoned-batch' } + const target = { worktreeId: 'repo-1::/workspace', executionHostId: 'local' as const } + beginWorkspaceCleanupRemovalSnapshotPruneBatch('/profile-a', batch) + recordWorkspaceCleanupRemovalSnapshotPrune('/profile-a', { ...batch, ...target }) + + await vi.advanceTimersByTimeAsync(5 * 60_000) + + expect(finalizeCleanupSnapshotsMock).toHaveBeenCalledExactlyOnceWith('/profile-a', [target]) + expect(finalizeSpaceSnapshotsMock).toHaveBeenCalledExactlyOnceWith('/profile-a', [target]) + await finishWorkspaceCleanupRemovalSnapshotPruneBatch('/profile-a', batch) + expect(finalizeCleanupSnapshotsMock).toHaveBeenCalledTimes(1) + expect(finalizeSpaceSnapshotsMock).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/main/workspace-cleanup-removal-snapshot-prune.ts b/src/main/workspace-cleanup-removal-snapshot-prune.ts new file mode 100644 index 00000000000..9b4244400c6 --- /dev/null +++ b/src/main/workspace-cleanup-removal-snapshot-prune.ts @@ -0,0 +1,138 @@ +import type { + WorkspaceCleanupSnapshotPruneBatchArgs, + WorkspaceCleanupSnapshotPruneRecordArgs +} from '../shared/workspace-cleanup' +import { + finalizeWorkspaceCleanupScanSnapshotPrunes, + pruneWorkspaceCleanupScanSnapshots, + registerWorkspaceCleanupScanSnapshotPruneTombstones +} from './workspace-cleanup-scan-snapshot' +import { + finalizeWorkspaceSpaceAnalysisSnapshotPrunes, + pruneWorkspaceSpaceAnalysisSnapshots, + registerWorkspaceSpaceAnalysisSnapshotPruneTombstones +} from './workspace-space-analysis-snapshot' + +type SnapshotPruneTarget = Omit + +type SnapshotPruneBatch = { + targets: Map + expirationTimer?: ReturnType +} + +const batches = new Map() +const SNAPSHOT_PRUNE_BATCH_IDLE_TIMEOUT_MS = 5 * 60_000 + +function batchKey(snapshotDirectory: string, batchId: string): string { + return `${snapshotDirectory}\0${batchId}` +} + +function targetKey(target: SnapshotPruneTarget): string { + return `${target.executionHostId ?? '*'}\0${target.worktreeId}` +} + +function scheduleBatchExpiration( + snapshotDirectory: string, + batchId: string, + batch: SnapshotPruneBatch +): ReturnType { + const timer = setTimeout(() => { + const expired = takeBatch(snapshotDirectory, batchId, batch) + if (expired) { + void finalizeBatch(snapshotDirectory, expired) + } + }, SNAPSHOT_PRUNE_BATCH_IDLE_TIMEOUT_MS) + timer.unref() + return timer +} + +function takeBatch( + snapshotDirectory: string, + batchId: string, + expected?: SnapshotPruneBatch +): SnapshotPruneBatch | undefined { + const key = batchKey(snapshotDirectory, batchId) + const batch = batches.get(key) + if (!batch || (expected && batch !== expected)) { + return undefined + } + batches.delete(key) + if (batch.expirationTimer) { + clearTimeout(batch.expirationTimer) + } + return batch +} + +async function finalizeBatch(snapshotDirectory: string, batch: SnapshotPruneBatch): Promise { + const targets = [...batch.targets.values()] + await Promise.all([ + finalizeWorkspaceCleanupScanSnapshotPrunes(snapshotDirectory, targets), + finalizeWorkspaceSpaceAnalysisSnapshotPrunes(snapshotDirectory, targets) + ]) +} + +export function beginWorkspaceCleanupRemovalSnapshotPruneBatch( + snapshotDirectory: string, + args: WorkspaceCleanupSnapshotPruneBatchArgs +): void { + const key = batchKey(snapshotDirectory, args.batchId) + const existing = batches.get(key) + if (existing) { + if (existing.expirationTimer) { + clearTimeout(existing.expirationTimer) + } + existing.expirationTimer = scheduleBatchExpiration(snapshotDirectory, args.batchId, existing) + return + } + const batch: SnapshotPruneBatch = { targets: new Map() } + batch.expirationTimer = scheduleBatchExpiration(snapshotDirectory, args.batchId, batch) + batches.set(key, batch) +} + +/** Tombstone now; defer the two durable sidecar rewrites until the batch closes. */ +export function recordWorkspaceCleanupRemovalSnapshotPrune( + snapshotDirectory: string, + args: WorkspaceCleanupSnapshotPruneRecordArgs +): void { + const target = { + worktreeId: args.worktreeId, + ...(args.executionHostId ? { executionHostId: args.executionHostId } : {}) + } + const batch = batches.get(batchKey(snapshotDirectory, args.batchId)) + if (!batch) { + void pruneWorkspaceCleanupScanSnapshots(snapshotDirectory, [target]) + void pruneWorkspaceSpaceAnalysisSnapshots(snapshotDirectory, [target]) + return + } + + const key = targetKey(target) + if (!batch.targets.has(key)) { + registerWorkspaceCleanupScanSnapshotPruneTombstones(snapshotDirectory, [target]) + registerWorkspaceSpaceAnalysisSnapshotPruneTombstones(snapshotDirectory, [target]) + batch.targets.set(key, target) + } + if (batch.expirationTimer) { + clearTimeout(batch.expirationTimer) + } + batch.expirationTimer = scheduleBatchExpiration(snapshotDirectory, args.batchId, batch) +} + +export async function finishWorkspaceCleanupRemovalSnapshotPruneBatch( + snapshotDirectory: string, + args: WorkspaceCleanupSnapshotPruneBatchArgs +): Promise { + const batch = takeBatch(snapshotDirectory, args.batchId) + if (!batch) { + return + } + await finalizeBatch(snapshotDirectory, batch) +} + +export function resetWorkspaceCleanupRemovalSnapshotPruneBatchesForTests(): void { + for (const batch of batches.values()) { + if (batch.expirationTimer) { + clearTimeout(batch.expirationTimer) + } + } + batches.clear() +} diff --git a/src/main/workspace-cleanup-scan-snapshot.test.ts b/src/main/workspace-cleanup-scan-snapshot.test.ts new file mode 100644 index 00000000000..ca5fa7edffe --- /dev/null +++ b/src/main/workspace-cleanup-scan-snapshot.test.ts @@ -0,0 +1,450 @@ +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { + WorkspaceCleanupCandidate, + WorkspaceCleanupScanResult +} from '../shared/workspace-cleanup' + +const { snapshotWriteSpy, userDataDirHolder } = vi.hoisted(() => ({ + snapshotWriteSpy: vi.fn(), + userDataDirHolder: { dir: '' } +})) + +vi.mock('./sidecar-snapshot-file', async (importOriginal) => { + const actual = await importOriginal>() + const writeSidecarSnapshot = actual.writeSidecarSnapshot as ( + file: string, + payload: unknown + ) => Promise + return { + ...actual, + writeSidecarSnapshot: async (file: string, payload: unknown) => { + snapshotWriteSpy(file, payload) + await writeSidecarSnapshot(file, payload) + } + } +}) + +import { + finalizeWorkspaceCleanupScanSnapshotPrunes, + persistWorkspaceCleanupScanResult, + pruneWorkspaceCleanupScanSnapshot, + pruneWorkspaceCleanupScanSnapshots, + readWorkspaceCleanupScanSnapshot, + registerWorkspaceCleanupScanSnapshotPruneTombstones, + workspaceCleanupScanSnapshotFingerprint +} from './workspace-cleanup-scan-snapshot' + +const SNAPSHOT_FILE = 'orca-workspace-cleanup-scan.json' +const NOW = 1_700_000_000_000 + +function makeCandidate( + overrides: Partial = {} +): WorkspaceCleanupCandidate { + return { + worktreeId: 'repo-1::/repo-feature', + repoId: 'repo-1', + repoName: 'Repo', + connectionId: null, + executionHostId: 'local', + displayName: 'Feature', + branch: 'feature', + path: '/repo-feature', + tier: 'ready', + selectedByDefault: true, + reasons: ['idle-clean'], + blockers: [], + lastActivityAt: NOW - 40 * 24 * 60 * 60 * 1000, + localContext: { + terminalTabCount: 0, + cleanEditorTabCount: 0, + browserTabCount: 0, + diffCommentCount: 0, + newestDiffCommentAt: null, + retainedDoneAgentCount: 0 + }, + git: { clean: true, upstreamAhead: 0, upstreamBehind: 0, checkedAt: NOW }, + fingerprint: '2|feature|abc123|clean|19675', + ...overrides + } +} + +function makeBroadResult(candidates: WorkspaceCleanupCandidate[]): WorkspaceCleanupScanResult { + return { scannedAt: NOW, candidates, errors: [] } +} + +describe('workspace cleanup scan snapshot', () => { + beforeEach(async () => { + snapshotWriteSpy.mockClear() + userDataDirHolder.dir = await mkdtemp(join(tmpdir(), 'orca-cleanup-snapshot-')) + }) + + afterEach(async () => { + await rm(userDataDirHolder.dir, { recursive: true, force: true }) + }) + + it('round-trips a broad scan snapshot, including SSH connectionId rows', async () => { + const sshCandidate = makeCandidate({ + worktreeId: 'repo-ssh::/remote/repo-feature', + repoId: 'repo-ssh', + connectionId: 'ssh-1', + executionHostId: 'ssh:ssh-1', + path: '/remote/repo-feature', + blockers: ['ssh-disconnected'], + git: { clean: null, upstreamAhead: null, upstreamBehind: null, checkedAt: null } + }) + const result = makeBroadResult([makeCandidate(), sshCandidate]) + + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { includeAllWorkspaces: true }, + result + ) + + await expect(readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir)).resolves.toEqual(result) + }) + + it('does not let an older broad scan overwrite a newer snapshot', async () => { + const newer = { + scannedAt: NOW + 1, + candidates: [makeCandidate({ displayName: 'Newer' })], + errors: [] + } + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { includeAllWorkspaces: true }, + newer + ) + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { includeAllWorkspaces: true }, + makeBroadResult([makeCandidate({ displayName: 'Older' })]) + ) + + await expect(readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir)).resolves.toEqual(newer) + }) + + it('returns null when no snapshot has been persisted', async () => { + await expect(readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir)).resolves.toBeNull() + }) + + it('degrades corrupt persisted blobs to null instead of throwing', async () => { + const file = join(userDataDirHolder.dir, SNAPSHOT_FILE) + + await writeFile(file, 'not json{', 'utf-8') + await expect(readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir)).resolves.toBeNull() + + await writeFile( + file, + JSON.stringify({ + version: 2, + argsFingerprint: workspaceCleanupScanSnapshotFingerprint(), + result: { scannedAt: NOW, candidates: 'nope', errors: [] } + }), + 'utf-8' + ) + await expect(readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir)).resolves.toBeNull() + + await writeFile( + file, + JSON.stringify({ + version: 2, + argsFingerprint: workspaceCleanupScanSnapshotFingerprint(), + result: { scannedAt: NOW, candidates: [{ worktreeId: 42 }], errors: [] } + }), + 'utf-8' + ) + await expect(readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir)).resolves.toBeNull() + }) + + it('treats a snapshot from another classifier version as absent', async () => { + await writeFile( + join(userDataDirHolder.dir, SNAPSHOT_FILE), + JSON.stringify({ + version: 2, + argsFingerprint: 'classifier:1|includeAllWorkspaces', + result: makeBroadResult([makeCandidate()]) + }), + 'utf-8' + ) + + await expect(readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir)).resolves.toBeNull() + }) + + it('patches targeted rescans into the snapshot without touching other rows', async () => { + const stale = makeCandidate({ git: { ...makeCandidate().git, clean: null, checkedAt: null } }) + const other = makeCandidate({ + worktreeId: 'repo-1::/repo-other', + path: '/repo-other', + branch: 'other' + }) + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { includeAllWorkspaces: true }, + makeBroadResult([stale, other]) + ) + + const rescanned = makeCandidate({ tier: 'protected', blockers: ['dirty-files'] }) + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { worktreeId: stale.worktreeId }, + { scannedAt: NOW + 60_000, candidates: [rescanned], errors: [] } + ) + + const snapshot = await readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir) + expect(snapshot?.candidates).toEqual([rescanned, other]) + // Snapshot freshness stays anchored to the last FULL scan. + expect(snapshot?.scannedAt).toBe(NOW) + }) + + it('appends targeted rows the snapshot has not seen yet', async () => { + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { includeAllWorkspaces: true }, + makeBroadResult([makeCandidate()]) + ) + + const created = makeCandidate({ worktreeId: 'repo-1::/repo-new', path: '/repo-new' }) + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { worktreeId: created.worktreeId }, + { scannedAt: NOW + 1, candidates: [created], errors: [] } + ) + + const snapshot = await readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir) + expect(snapshot?.candidates.map((candidate) => candidate.worktreeId)).toEqual([ + 'repo-1::/repo-feature', + 'repo-1::/repo-new' + ]) + }) + + it('does not create a snapshot from a targeted scan alone', async () => { + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { worktreeId: 'repo-1::/repo-feature' }, + makeBroadResult([makeCandidate()]) + ) + + await expect(readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir)).resolves.toBeNull() + await expect(readFile(join(userDataDirHolder.dir, SNAPSHOT_FILE))).rejects.toMatchObject({ + code: 'ENOENT' + }) + }) + + it('keeps the broad snapshot when a legacy suggestion-only scan completes', async () => { + const broad = makeBroadResult([ + makeCandidate(), + makeCandidate({ worktreeId: 'repo-1::/b', path: '/b' }) + ]) + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { includeAllWorkspaces: true }, + broad + ) + + const legacyRow = makeCandidate({ tier: 'review', selectedByDefault: false }) + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + {}, + { scannedAt: NOW + 1, candidates: [legacyRow], errors: [] } + ) + + const snapshot = await readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir) + expect(snapshot?.candidates).toHaveLength(2) + expect(snapshot?.candidates[0]).toEqual(legacyRow) + }) + + it('prunes a removed worktree so it cannot resurrect from cache', async () => { + const kept = makeCandidate({ worktreeId: 'repo-1::/repo-kept', path: '/repo-kept' }) + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { includeAllWorkspaces: true }, + makeBroadResult([makeCandidate(), kept]) + ) + + snapshotWriteSpy.mockClear() + await pruneWorkspaceCleanupScanSnapshot(userDataDirHolder.dir, 'repo-1::/repo-feature', 'local') + + const snapshot = await readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir) + expect(snapshot?.candidates).toEqual([kept]) + expect(snapshotWriteSpy).toHaveBeenCalledTimes(1) + + // Unknown ids are a no-op. + snapshotWriteSpy.mockClear() + await pruneWorkspaceCleanupScanSnapshot(userDataDirHolder.dir, 'repo-1::/never-existed') + await expect(readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir)).resolves.toEqual(snapshot) + expect(snapshotWriteSpy).not.toHaveBeenCalled() + }) + + it('coalesces host-scoped local and remote prunes into one write', async () => { + const localCollision = makeCandidate() + const remoteCollision = makeCandidate({ + connectionId: 'ssh-1', + executionHostId: 'ssh:ssh-1' + }) + const localRemoved = makeCandidate({ + worktreeId: 'repo-1::/local-removed', + path: '/local-removed' + }) + const remoteRemoved = makeCandidate({ + worktreeId: 'repo-1::/remote-removed', + connectionId: 'ssh-1', + executionHostId: 'ssh:ssh-1', + path: '/remote-removed' + }) + const kept = makeCandidate({ worktreeId: 'repo-1::/kept', path: '/kept' }) + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { includeAllWorkspaces: true }, + makeBroadResult([localCollision, remoteCollision, localRemoved, remoteRemoved, kept]) + ) + + snapshotWriteSpy.mockClear() + await pruneWorkspaceCleanupScanSnapshots(userDataDirHolder.dir, [ + { worktreeId: localCollision.worktreeId, executionHostId: 'local' }, + { worktreeId: localRemoved.worktreeId, executionHostId: 'local' }, + { worktreeId: remoteRemoved.worktreeId, executionHostId: 'ssh:ssh-1' } + ]) + + expect(snapshotWriteSpy).toHaveBeenCalledTimes(1) + expect(snapshotWriteSpy).toHaveBeenCalledWith( + join(userDataDirHolder.dir, SNAPSHOT_FILE), + expect.objectContaining({ + result: expect.objectContaining({ candidates: [remoteCollision, kept] }) + }) + ) + await expect(readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir)).resolves.toMatchObject({ + candidates: [remoteCollision, kept] + }) + + snapshotWriteSpy.mockClear() + await pruneWorkspaceCleanupScanSnapshots(userDataDirHolder.dir, [ + { worktreeId: 'repo-1::/missing-local', executionHostId: 'local' }, + { worktreeId: 'repo-1::/missing-remote', executionHostId: 'ssh:ssh-1' } + ]) + expect(snapshotWriteSpy).not.toHaveBeenCalled() + }) + + it('keeps profile snapshots isolated', async () => { + const otherProfile = await mkdtemp(join(tmpdir(), 'orca-cleanup-snapshot-other-')) + try { + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { includeAllWorkspaces: true }, + makeBroadResult([makeCandidate()]) + ) + + await expect(readWorkspaceCleanupScanSnapshot(otherProfile)).resolves.toBeNull() + } finally { + await rm(otherProfile, { recursive: true, force: true }) + } + }) + + it('does not let a scan started before a bulk removal restore pruned rows', async () => { + const local = makeCandidate() + const remote = makeCandidate({ + worktreeId: 'repo-1::/remote-feature', + connectionId: 'ssh-1', + executionHostId: 'ssh:ssh-1', + path: '/remote-feature' + }) + const staleResult = { + ...makeBroadResult([local, remote]), + scannedAt: Date.now() - 1 + } + await pruneWorkspaceCleanupScanSnapshots(userDataDirHolder.dir, [ + { worktreeId: local.worktreeId, executionHostId: 'local' }, + { worktreeId: remote.worktreeId, executionHostId: 'ssh:ssh-1' } + ]) + + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { includeAllWorkspaces: true }, + staleResult + ) + expect((await readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir))?.candidates).toEqual([]) + + const recreated = { ...staleResult, scannedAt: Date.now() + 1 } + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { includeAllWorkspaces: true }, + recreated + ) + expect((await readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir))?.candidates).toEqual([ + local, + remote + ]) + }) + + it('registers a tombstone immediately without rewriting the sidecar', async () => { + const staleResult = { ...makeBroadResult([makeCandidate()]), scannedAt: Date.now() - 1 } + + registerWorkspaceCleanupScanSnapshotPruneTombstones(userDataDirHolder.dir, [ + { worktreeId: 'repo-1::/repo-feature', executionHostId: 'local' } + ]) + + expect(snapshotWriteSpy).not.toHaveBeenCalled() + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { includeAllWorkspaces: true }, + staleResult + ) + expect((await readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir))?.candidates).toEqual([]) + }) + + it('keeps the original tombstone time when a removal batch flushes', async () => { + const candidate = makeCandidate() + const target = { worktreeId: candidate.worktreeId, executionHostId: 'local' as const } + const now = vi.spyOn(Date, 'now').mockReturnValue(100) + registerWorkspaceCleanupScanSnapshotPruneTombstones(userDataDirHolder.dir, [target]) + now.mockReturnValue(200) + + await finalizeWorkspaceCleanupScanSnapshotPrunes(userDataDirHolder.dir, [target]) + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { includeAllWorkspaces: true }, + { ...makeBroadResult([candidate]), scannedAt: 150 } + ) + + expect((await readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir))?.candidates).toEqual([ + candidate + ]) + now.mockRestore() + }) + + it('patches and prunes host-colliding workspace ids independently', async () => { + const local = makeCandidate() + const remote = makeCandidate({ + connectionId: 'ssh-1', + executionHostId: 'ssh:ssh-1', + repoName: 'Remote repo' + }) + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { includeAllWorkspaces: true }, + makeBroadResult([local, remote]) + ) + + const rescannedRemote = makeCandidate({ + ...remote, + tier: 'protected', + blockers: ['dirty-files'] + }) + await persistWorkspaceCleanupScanResult( + userDataDirHolder.dir, + { worktreeId: remote.worktreeId }, + { scannedAt: NOW + 1, candidates: [rescannedRemote], errors: [] } + ) + expect((await readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir))?.candidates).toEqual([ + local, + rescannedRemote + ]) + + await pruneWorkspaceCleanupScanSnapshot(userDataDirHolder.dir, remote.worktreeId, 'ssh:ssh-1') + expect((await readWorkspaceCleanupScanSnapshot(userDataDirHolder.dir))?.candidates).toEqual([ + local + ]) + }) +}) diff --git a/src/main/workspace-cleanup-scan-snapshot.ts b/src/main/workspace-cleanup-scan-snapshot.ts new file mode 100644 index 00000000000..b87020e1ffe --- /dev/null +++ b/src/main/workspace-cleanup-scan-snapshot.ts @@ -0,0 +1,313 @@ +import { + WORKSPACE_CLEANUP_CLASSIFIER_VERSION, + type WorkspaceCleanupCandidate, + type WorkspaceCleanupScanArgs, + type WorkspaceCleanupScanResult +} from '../shared/workspace-cleanup' +import { + readSidecarSnapshot, + sidecarSnapshotFile, + withSidecarSnapshotQueue, + writeSidecarSnapshot +} from './sidecar-snapshot-file' +import type { ExecutionHostId } from '../shared/execution-host' +import { + activeWorkspaceSnapshotPruneKeys, + registerWorkspaceSnapshotPrunesForFile, + workspaceSnapshotPruneKey, + workspaceSnapshotPruneTargetKeys, + type WorkspaceSnapshotPruneTarget, + type WorkspaceSnapshotPruneTombstone +} from './workspace-snapshot-prune-index' + +const SNAPSHOT_FILE_NAME = 'orca-workspace-cleanup-scan.json' +const SNAPSHOT_VERSION = 2 + +export type WorkspaceCleanupScanSnapshotPruneTarget = WorkspaceSnapshotPruneTarget + +const prunedWorkspacesByFile = new Map>() + +type PersistedWorkspaceCleanupScanSnapshot = { + version: number + argsFingerprint: string + result: WorkspaceCleanupScanResult +} + +/** Why a fingerprint: a classifier bump reshuffles tiers/blockers wholesale, so an older snapshot must read as absent, not stale-but-plausible. */ +export function workspaceCleanupScanSnapshotFingerprint(): string { + return `classifier:${WORKSPACE_CLEANUP_CLASSIFIER_VERSION}|includeAllWorkspaces` +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function isPersistableCandidate(value: unknown): value is WorkspaceCleanupCandidate { + if (!isRecord(value)) { + return false + } + return ( + typeof value.worktreeId === 'string' && + typeof value.repoId === 'string' && + typeof value.fingerprint === 'string' && + (value.connectionId === null || typeof value.connectionId === 'string') && + typeof value.executionHostId === 'string' && + Array.isArray(value.reasons) && + Array.isArray(value.blockers) && + isRecord(value.git) && + isRecord(value.localContext) + ) +} + +/** Shape guard so a corrupt persisted blob degrades to null instead of throwing at startup. */ +function parseSnapshot(parsed: unknown): WorkspaceCleanupScanResult | null { + if (!isRecord(parsed)) { + return null + } + if (parsed.version !== SNAPSHOT_VERSION) { + return null + } + if (parsed.argsFingerprint !== workspaceCleanupScanSnapshotFingerprint()) { + return null + } + const result = parsed.result + if (!isRecord(result)) { + return null + } + if ( + typeof result.scannedAt !== 'number' || + !Array.isArray(result.candidates) || + !Array.isArray(result.errors) || + !result.candidates.every(isPersistableCandidate) + ) { + return null + } + return result as unknown as WorkspaceCleanupScanResult +} + +export async function readWorkspaceCleanupScanSnapshot( + snapshotDirectory: string +): Promise { + try { + return parseSnapshot( + await readSidecarSnapshot(sidecarSnapshotFile(snapshotDirectory, SNAPSHOT_FILE_NAME)) + ) + } catch { + return null + } +} + +async function writeSnapshot(file: string, result: WorkspaceCleanupScanResult): Promise { + await writeSidecarSnapshot(file, { + version: SNAPSHOT_VERSION, + argsFingerprint: workspaceCleanupScanSnapshotFingerprint(), + result + } satisfies PersistedWorkspaceCleanupScanSnapshot) +} + +function patchCandidates( + existing: WorkspaceCleanupScanResult, + fresh: WorkspaceCleanupCandidate[] +): WorkspaceCleanupScanResult { + const freshById = new Map(fresh.map((candidate) => [candidateSnapshotKey(candidate), candidate])) + const candidates = existing.candidates.map((candidate) => { + const key = candidateSnapshotKey(candidate) + const replacement = freshById.get(key) + freshById.delete(key) + return replacement ?? candidate + }) + candidates.push(...freshById.values()) + // Why keep scannedAt: it marks the last FULL scan; a focused rescan must not advertise fleet-wide freshness. + return { ...existing, candidates } +} + +function candidateSnapshotKey( + candidate: Pick +): string { + return `${candidate.executionHostId ?? 'local'}\0${candidate.worktreeId}` +} + +/** Register anti-resurrection tombstones without scheduling a sidecar rewrite. */ +export function registerWorkspaceCleanupScanSnapshotPruneTombstones( + snapshotDirectory: string, + targets: readonly WorkspaceCleanupScanSnapshotPruneTarget[] +): void { + if (targets.length === 0) { + return + } + registerWorkspaceSnapshotPrunesForFile( + prunedWorkspacesByFile, + sidecarSnapshotFile(snapshotDirectory, SNAPSHOT_FILE_NAME), + targets + ) +} + +function excludeRowsPrunedDuringScan( + file: string, + result: WorkspaceCleanupScanResult +): WorkspaceCleanupScanResult { + const prunedKeys = activeWorkspaceSnapshotPruneKeys( + prunedWorkspacesByFile.get(file), + result.scannedAt + ) + if (prunedKeys.size === 0) { + return result + } + const candidates = result.candidates.filter( + (candidate) => + !prunedKeys.has(workspaceSnapshotPruneKey(candidate.worktreeId, candidate.executionHostId)) && + !prunedKeys.has(workspaceSnapshotPruneKey(candidate.worktreeId)) + ) + return candidates.length === result.candidates.length ? result : { ...result, candidates } +} + +function clearSupersededPrunes( + file: string, + result: WorkspaceCleanupScanResult, + broad: boolean +): void { + const pruned = prunedWorkspacesByFile.get(file) + if (!pruned) { + return + } + const candidateKeys = broad + ? undefined + : new Set( + result.candidates.flatMap((candidate) => [ + workspaceSnapshotPruneKey(candidate.worktreeId, candidate.executionHostId), + workspaceSnapshotPruneKey(candidate.worktreeId) + ]) + ) + for (const [key, entry] of pruned) { + if (entry.prunedAt < result.scannedAt && (broad || candidateKeys?.has(key))) { + pruned.delete(key) + } + } + if (pruned.size === 0) { + prunedWorkspacesByFile.delete(file) + } +} + +/** + * Persist a completed scan: a broad (includeAllWorkspaces) scan replaces the snapshot, anything + * narrower patches matching rows into it. Never throws — the snapshot is a refetchable cache. + */ +// Why: skip the full snapshot read whose only purpose is the scannedAt +// comparison — on a large fleet that read is a multi-hundred-KB synchronous +// JSON.parse per scan. +const lastPersistedScannedAtByFile = new Map() + +export async function persistWorkspaceCleanupScanResult( + snapshotDirectory: string, + args: WorkspaceCleanupScanArgs, + result: WorkspaceCleanupScanResult +): Promise { + const file = sidecarSnapshotFile(snapshotDirectory, SNAPSHOT_FILE_NAME) + try { + await withSidecarSnapshotQueue(file, async () => { + const filteredResult = excludeRowsPrunedDuringScan(file, result) + // worktreeIds (even empty) is a targeted scan; persisting it as broad + // would replace the fleet snapshot with a subset. + const broad = + !args.worktreeId && !Array.isArray(args.worktreeIds) && args.includeAllWorkspaces === true + if (broad) { + let knownScannedAt = lastPersistedScannedAtByFile.get(file) + if (knownScannedAt === undefined) { + const existing = await readWorkspaceCleanupScanSnapshot(snapshotDirectory) + knownScannedAt = existing?.scannedAt + } + if (knownScannedAt !== undefined && knownScannedAt > filteredResult.scannedAt) { + lastPersistedScannedAtByFile.set(file, knownScannedAt) + return + } + await writeSnapshot(file, filteredResult) + lastPersistedScannedAtByFile.set(file, filteredResult.scannedAt) + clearSupersededPrunes(file, result, true) + return + } + if (filteredResult.candidates.length === 0) { + return + } + const existing = await readWorkspaceCleanupScanSnapshot(snapshotDirectory) + // Why: a focused/legacy scan is a subset; without a broad baseline it is not a fleet snapshot. + if (!existing) { + return + } + await writeSnapshot(file, patchCandidates(existing, filteredResult.candidates)) + clearSupersededPrunes(file, result, false) + }) + } catch (error) { + console.warn('[workspace-cleanup] failed to persist scan snapshot:', error) + } +} + +async function pruneWorkspaceCleanupScanSnapshotsWithRegisteredTombstones( + snapshotDirectory: string, + targets: readonly WorkspaceCleanupScanSnapshotPruneTarget[], + registerTombstones: boolean +): Promise { + if (targets.length === 0) { + return + } + const file = sidecarSnapshotFile(snapshotDirectory, SNAPSHOT_FILE_NAME) + const targetKeys = workspaceSnapshotPruneTargetKeys(targets) + if (registerTombstones) { + registerWorkspaceSnapshotPrunesForFile(prunedWorkspacesByFile, file, targets) + } + try { + await withSidecarSnapshotQueue(file, async () => { + const registered = prunedWorkspacesByFile.get(file) + const coalescedTargetKeys = registerTombstones + ? targetKeys + : new Set([...targetKeys].filter((key) => registered?.has(key))) + if (coalescedTargetKeys.size === 0) { + return + } + const existing = await readWorkspaceCleanupScanSnapshot(snapshotDirectory) + if (!existing) { + return + } + const candidates = existing.candidates.filter( + (candidate) => + !coalescedTargetKeys.has( + workspaceSnapshotPruneKey(candidate.worktreeId, candidate.executionHostId) + ) && !coalescedTargetKeys.has(workspaceSnapshotPruneKey(candidate.worktreeId)) + ) + if (candidates.length === existing.candidates.length) { + return + } + await writeSnapshot(file, { ...existing, candidates }) + }) + } catch (error) { + console.warn('[workspace-cleanup] failed to prune scan snapshot:', error) + } +} + +/** Drop removed workspaces in one sidecar transaction. Never throws. */ +export async function pruneWorkspaceCleanupScanSnapshots( + snapshotDirectory: string, + targets: readonly WorkspaceCleanupScanSnapshotPruneTarget[] +): Promise { + await pruneWorkspaceCleanupScanSnapshotsWithRegisteredTombstones(snapshotDirectory, targets, true) +} + +/** Flush only tombstones still active for this batch, preserving their original prune time. */ +export async function finalizeWorkspaceCleanupScanSnapshotPrunes( + snapshotDirectory: string, + targets: readonly WorkspaceCleanupScanSnapshotPruneTarget[] +): Promise { + await pruneWorkspaceCleanupScanSnapshotsWithRegisteredTombstones( + snapshotDirectory, + targets, + false + ) +} + +/** Drop a removed workspace so it never resurrects from cache. Never throws. */ +export async function pruneWorkspaceCleanupScanSnapshot( + snapshotDirectory: string, + worktreeId: string, + executionHostId?: ExecutionHostId +): Promise { + await pruneWorkspaceCleanupScanSnapshots(snapshotDirectory, [{ worktreeId, executionHostId }]) +} diff --git a/src/main/workspace-snapshot-prune-index.ts b/src/main/workspace-snapshot-prune-index.ts new file mode 100644 index 00000000000..56b4289335b --- /dev/null +++ b/src/main/workspace-snapshot-prune-index.ts @@ -0,0 +1,57 @@ +import type { ExecutionHostId } from '../shared/execution-host' + +export type WorkspaceSnapshotPruneTarget = { + worktreeId: string + executionHostId?: ExecutionHostId +} + +export type WorkspaceSnapshotPruneTombstone = WorkspaceSnapshotPruneTarget & { + prunedAt: number +} + +export function workspaceSnapshotPruneKey( + worktreeId: string, + executionHostId?: ExecutionHostId +): string { + return `${executionHostId ?? '*'}\0${worktreeId}` +} + +export function workspaceSnapshotPruneTargetKeys( + targets: readonly WorkspaceSnapshotPruneTarget[] +): Set { + return new Set( + targets.map(({ worktreeId, executionHostId }) => + workspaceSnapshotPruneKey(worktreeId, executionHostId) + ) + ) +} + +export function activeWorkspaceSnapshotPruneKeys( + tombstones: ReadonlyMap | undefined, + scannedAt: number +): Set { + const keys = new Set() + for (const [key, entry] of tombstones ?? []) { + if (entry.prunedAt >= scannedAt) { + keys.add(key) + } + } + return keys +} + +export function registerWorkspaceSnapshotPrunesForFile( + tombstonesByFile: Map>, + file: string, + targets: readonly WorkspaceSnapshotPruneTarget[] +): void { + const tombstones = tombstonesByFile.get(file) ?? new Map() + const prunedAt = Date.now() + for (const { worktreeId, executionHostId } of targets) { + tombstones.set(workspaceSnapshotPruneKey(worktreeId, executionHostId), { + worktreeId, + ...(executionHostId ? { executionHostId } : {}), + prunedAt + }) + } + tombstonesByFile.set(file, tombstones) +} diff --git a/src/main/workspace-snapshot-pruning.bench.test.ts b/src/main/workspace-snapshot-pruning.bench.test.ts new file mode 100644 index 00000000000..0523894265b --- /dev/null +++ b/src/main/workspace-snapshot-pruning.bench.test.ts @@ -0,0 +1,153 @@ +// Opt in: ORCA_WORKSPACE_SNAPSHOT_PRUNE_BENCH=1 pnpm exec vitest run --config config/vitest.config.ts src/main/workspace-snapshot-pruning.bench.test.ts +import { performance } from 'node:perf_hooks' +import { mkdtemp, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import type { WorkspaceCleanupCandidate } from '../shared/workspace-cleanup' +import type { WorkspaceSpaceWorktree } from '../shared/workspace-space-types' +import { + persistWorkspaceCleanupScanResult, + pruneWorkspaceCleanupScanSnapshots +} from './workspace-cleanup-scan-snapshot' +import { + persistWorkspaceSpaceAnalysisSnapshot, + pruneWorkspaceSpaceAnalysisSnapshots +} from './workspace-space-analysis-snapshot' + +const describeBench = process.env.ORCA_WORKSPACE_SNAPSHOT_PRUNE_BENCH ? describe : describe.skip +const ROW_COUNT = 610 +const DELETE_COUNT = 100 + +describeBench('workspace snapshot bulk pruning', () => { + let snapshotDirectory = '' + + beforeAll(async () => { + snapshotDirectory = await mkdtemp(join(tmpdir(), 'orca-snapshot-prune-bench-')) + }) + + afterAll(async () => { + await rm(snapshotDirectory, { recursive: true, force: true }) + }) + + it('prunes the measured 610-row/100-delete shape with one write per sidecar', async () => { + const candidates = Array.from({ length: ROW_COUNT }, (_, index) => makeCandidate(index)) + const worktrees = Array.from({ length: ROW_COUNT }, (_, index) => makeWorktree(index)) + const targets = candidates.slice(0, DELETE_COUNT).map((candidate) => ({ + worktreeId: candidate.worktreeId, + executionHostId: candidate.executionHostId + })) + await persistWorkspaceCleanupScanResult( + snapshotDirectory, + { includeAllWorkspaces: true }, + { scannedAt: 1, candidates, errors: [] } + ) + await persistWorkspaceSpaceAnalysisSnapshot(snapshotDirectory, { + scannedAt: 1, + totalSizeBytes: ROW_COUNT * 1_000, + reclaimableBytes: ROW_COUNT * 1_000, + worktreeCount: ROW_COUNT, + scannedWorktreeCount: ROW_COUNT, + unavailableWorktreeCount: 0, + repos: makeRepoSummaries(worktrees), + worktrees + }) + + const startedAt = performance.now() + await Promise.all([ + pruneWorkspaceCleanupScanSnapshots(snapshotDirectory, targets), + pruneWorkspaceSpaceAnalysisSnapshots(snapshotDirectory, targets) + ]) + const pruneMs = performance.now() - startedAt + const cleanupPayload = JSON.parse( + await readFile(join(snapshotDirectory, 'orca-workspace-cleanup-scan.json'), 'utf-8') + ) as { result: { candidates: unknown[] } } + const spacePayload = JSON.parse( + await readFile(join(snapshotDirectory, 'orca-workspace-space-analysis.json'), 'utf-8') + ) as { analysis: { worktrees: unknown[] } } + + console.log( + `[bench] rows=${ROW_COUNT} deleted=${DELETE_COUNT} bulkPrune=${pruneMs.toFixed(2)}ms` + ) + expect(cleanupPayload.result.candidates).toHaveLength(ROW_COUNT - DELETE_COUNT) + expect(spacePayload.analysis.worktrees).toHaveLength(ROW_COUNT - DELETE_COUNT) + }) +}) + +function executionHostId(index: number): 'local' | `ssh:${string}` { + return index % 3 === 0 ? `ssh:ssh-${index % 7}` : 'local' +} + +function makeCandidate(index: number): WorkspaceCleanupCandidate { + const hostId = executionHostId(index) + return { + worktreeId: `repo-${index % 20}::/workspace-${index}`, + repoId: `repo-${index % 20}`, + repoName: `Repo ${index % 20}`, + connectionId: hostId === 'local' ? null : hostId.slice('ssh:'.length), + executionHostId: hostId, + displayName: `Workspace ${index}`, + branch: `branch-${index}`, + path: `/workspace-${index}`, + tier: 'ready', + selectedByDefault: true, + reasons: ['idle-clean'], + blockers: [], + lastActivityAt: 0, + localContext: { + terminalTabCount: 0, + cleanEditorTabCount: 0, + browserTabCount: 0, + diffCommentCount: 0, + newestDiffCommentAt: null, + retainedDoneAgentCount: 0 + }, + git: { clean: true, upstreamAhead: 0, upstreamBehind: 0, checkedAt: 0 }, + fingerprint: `fingerprint-${index}` + } +} + +function makeWorktree(index: number): WorkspaceSpaceWorktree { + const candidate = makeCandidate(index) + return { + worktreeId: candidate.worktreeId, + repoId: candidate.repoId, + executionHostId: candidate.executionHostId, + repoDisplayName: candidate.repoName, + repoPath: `/repo-${index % 20}`, + displayName: candidate.displayName, + path: candidate.path, + branch: candidate.branch, + isMainWorktree: false, + isRemote: candidate.executionHostId !== 'local', + isSparse: false, + canDelete: true, + lastActivityAt: 0, + status: 'ok', + error: null, + scannedAt: 1, + sizeBytes: 1_000, + reclaimableBytes: 1_000, + skippedEntryCount: 0, + topLevelItems: [], + omittedTopLevelItemCount: 0, + omittedTopLevelSizeBytes: 0 + } +} + +function makeRepoSummaries(worktrees: WorkspaceSpaceWorktree[]) { + const groups = Map.groupBy(worktrees, (row) => `${row.executionHostId ?? 'local'}\0${row.repoId}`) + return [...groups.values()].map((rows) => ({ + repoId: rows[0].repoId, + executionHostId: rows[0].executionHostId, + displayName: rows[0].repoDisplayName, + path: rows[0].repoPath, + isRemote: rows[0].isRemote, + worktreeCount: rows.length, + scannedWorktreeCount: rows.length, + unavailableWorktreeCount: 0, + totalSizeBytes: rows.length * 1_000, + reclaimableBytes: rows.length * 1_000, + error: null + })) +} diff --git a/src/main/workspace-space-analysis-snapshot.test.ts b/src/main/workspace-space-analysis-snapshot.test.ts new file mode 100644 index 00000000000..a6d870674e3 --- /dev/null +++ b/src/main/workspace-space-analysis-snapshot.test.ts @@ -0,0 +1,438 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { + WorkspaceSpaceAnalysis, + WorkspaceSpaceWorktree +} from '../shared/workspace-space-types' + +const { snapshotWriteSpy, userDataDirHolder } = vi.hoisted(() => ({ + snapshotWriteSpy: vi.fn(), + userDataDirHolder: { dir: '' } +})) + +vi.mock('./sidecar-snapshot-file', async (importOriginal) => { + const actual = await importOriginal>() + const writeSidecarSnapshot = actual.writeSidecarSnapshot as ( + file: string, + payload: unknown + ) => Promise + return { + ...actual, + writeSidecarSnapshot: async (file: string, payload: unknown) => { + snapshotWriteSpy(file, payload) + await writeSidecarSnapshot(file, payload) + } + } +}) + +import { + finalizeWorkspaceSpaceAnalysisSnapshotPrunes, + persistWorkspaceSpaceAnalysisSnapshot, + pruneWorkspaceSpaceAnalysisSnapshot, + pruneWorkspaceSpaceAnalysisSnapshots, + readWorkspaceSpaceAnalysisSnapshot, + registerWorkspaceSpaceAnalysisSnapshotPruneTombstones +} from './workspace-space-analysis-snapshot' + +const SNAPSHOT_FILE = 'orca-workspace-space-analysis.json' +const NOW = 1_700_000_000_000 + +function makeWorktreeRow(overrides: Partial = {}): WorkspaceSpaceWorktree { + return { + worktreeId: 'repo-1::/repo-feature', + repoId: 'repo-1', + executionHostId: 'local', + repoDisplayName: 'Repo', + repoPath: '/repo', + displayName: 'Feature', + path: '/repo-feature', + branch: 'feature', + isMainWorktree: false, + isRemote: false, + isSparse: false, + canDelete: true, + lastActivityAt: NOW - 1000, + status: 'ok', + error: null, + scannedAt: NOW, + sizeBytes: 1000, + reclaimableBytes: 1000, + skippedEntryCount: 0, + topLevelItems: [], + omittedTopLevelItemCount: 0, + omittedTopLevelSizeBytes: 0, + ...overrides + } +} + +function makeAnalysis(worktrees: WorkspaceSpaceWorktree[]): WorkspaceSpaceAnalysis { + const okRows = worktrees.filter((row) => row.status === 'ok') + const rowsByHost = Map.groupBy(worktrees, (row) => row.executionHostId ?? 'local') + return { + scannedAt: NOW, + totalSizeBytes: worktrees.reduce((sum, row) => sum + row.sizeBytes, 0), + reclaimableBytes: worktrees.reduce((sum, row) => sum + row.reclaimableBytes, 0), + worktreeCount: worktrees.length, + scannedWorktreeCount: okRows.length, + unavailableWorktreeCount: worktrees.length - okRows.length, + repos: [...rowsByHost.entries()].map(([executionHostId, rows]) => { + const scanned = rows.filter((row) => row.status === 'ok') + return { + repoId: 'repo-1', + executionHostId, + displayName: 'Repo', + path: '/repo', + isRemote: executionHostId !== 'local', + worktreeCount: rows.length, + scannedWorktreeCount: scanned.length, + unavailableWorktreeCount: rows.length - scanned.length, + totalSizeBytes: rows.reduce((sum, row) => sum + row.sizeBytes, 0), + reclaimableBytes: rows.reduce((sum, row) => sum + row.reclaimableBytes, 0), + error: null + } + }), + worktrees + } +} + +describe('workspace space analysis snapshot', () => { + beforeEach(async () => { + snapshotWriteSpy.mockClear() + userDataDirHolder.dir = await mkdtemp(join(tmpdir(), 'orca-space-snapshot-')) + }) + + afterEach(async () => { + await rm(userDataDirHolder.dir, { recursive: true, force: true }) + }) + + it('round-trips a completed analysis', async () => { + const analysis = makeAnalysis([makeWorktreeRow()]) + + await persistWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir, analysis) + + await expect(readWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir)).resolves.toEqual( + analysis + ) + }) + + it('prunes topLevelItems into the omitted counters to bound the payload', async () => { + const row = makeWorktreeRow({ + sizeBytes: 5000, + topLevelItems: [ + { + name: 'node_modules', + path: '/repo-feature/node_modules', + kind: 'directory', + sizeBytes: 3000 + }, + { name: 'src', path: '/repo-feature/src', kind: 'directory', sizeBytes: 1000 } + ], + omittedTopLevelItemCount: 2, + omittedTopLevelSizeBytes: 500 + }) + + await persistWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir, makeAnalysis([row])) + + const cached = await readWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir) + expect(cached?.worktrees[0]).toMatchObject({ + sizeBytes: 5000, + topLevelItems: [], + omittedTopLevelItemCount: 4, + omittedTopLevelSizeBytes: 4500 + }) + }) + + it('returns null when missing or corrupt instead of throwing', async () => { + await expect(readWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir)).resolves.toBeNull() + + const file = join(userDataDirHolder.dir, SNAPSHOT_FILE) + await writeFile(file, '{"version":1,"analysis":', 'utf-8') + await expect(readWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir)).resolves.toBeNull() + + await writeFile( + file, + JSON.stringify({ + version: 2, + analysis: { scannedAt: 'yesterday', repos: [], worktrees: [] } + }), + 'utf-8' + ) + await expect(readWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir)).resolves.toBeNull() + + await writeFile( + file, + JSON.stringify({ version: 99, analysis: makeAnalysis([makeWorktreeRow()]) }), + 'utf-8' + ) + await expect(readWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir)).resolves.toBeNull() + }) + + it('loads legacy rows without an execution host id', async () => { + const { executionHostId: _executionHostId, ...legacyRow } = makeWorktreeRow() + const analysis = makeAnalysis([makeWorktreeRow()]) + await writeFile( + join(userDataDirHolder.dir, SNAPSHOT_FILE), + JSON.stringify({ + version: 2, + analysis: { ...analysis, worktrees: [legacyRow] } + }), + 'utf-8' + ) + + await expect(readWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir)).resolves.toMatchObject({ + worktrees: [expect.objectContaining({ worktreeId: legacyRow.worktreeId })] + }) + }) + + it('prunes a removed worktree row and rebalances totals', async () => { + const removed = makeWorktreeRow({ sizeBytes: 3000, reclaimableBytes: 3000 }) + const kept = makeWorktreeRow({ + worktreeId: 'repo-1::/repo-kept', + path: '/repo-kept', + status: 'missing', + sizeBytes: 1000, + reclaimableBytes: 0 + }) + await persistWorkspaceSpaceAnalysisSnapshot( + userDataDirHolder.dir, + makeAnalysis([removed, kept]) + ) + + snapshotWriteSpy.mockClear() + await pruneWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir, removed.worktreeId, 'local') + + const cached = await readWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir) + expect(cached?.worktrees.map((row) => row.worktreeId)).toEqual(['repo-1::/repo-kept']) + expect(cached).toMatchObject({ + worktreeCount: 1, + scannedWorktreeCount: 0, + unavailableWorktreeCount: 1, + totalSizeBytes: 1000, + reclaimableBytes: 0 + }) + expect(cached?.repos[0]).toMatchObject({ + worktreeCount: 1, + scannedWorktreeCount: 0, + unavailableWorktreeCount: 1, + totalSizeBytes: 1000, + reclaimableBytes: 0 + }) + expect(snapshotWriteSpy).toHaveBeenCalledTimes(1) + + // Unknown ids are a no-op. + snapshotWriteSpy.mockClear() + await pruneWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir, 'repo-1::/never-existed') + await expect(readWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir)).resolves.toEqual(cached) + expect(snapshotWriteSpy).not.toHaveBeenCalled() + }) + + it('coalesces local and remote row rebalancing into one write', async () => { + const localCollision = makeWorktreeRow({ sizeBytes: 1000, reclaimableBytes: 1000 }) + const remoteCollision = makeWorktreeRow({ + executionHostId: 'ssh:ssh-1', + isRemote: true, + sizeBytes: 2000, + reclaimableBytes: 2000 + }) + const localRemoved = makeWorktreeRow({ + worktreeId: 'repo-1::/local-removed', + path: '/local-removed', + status: 'missing', + sizeBytes: 3000, + reclaimableBytes: 0 + }) + const remoteRemoved = makeWorktreeRow({ + worktreeId: 'repo-1::/remote-removed', + executionHostId: 'ssh:ssh-1', + isRemote: true, + path: '/remote-removed', + sizeBytes: 4000, + reclaimableBytes: 4000 + }) + const kept = makeWorktreeRow({ + worktreeId: 'repo-1::/kept', + path: '/kept', + sizeBytes: 500, + reclaimableBytes: 500 + }) + await persistWorkspaceSpaceAnalysisSnapshot( + userDataDirHolder.dir, + makeAnalysis([localCollision, remoteCollision, localRemoved, remoteRemoved, kept]) + ) + + snapshotWriteSpy.mockClear() + await pruneWorkspaceSpaceAnalysisSnapshots(userDataDirHolder.dir, [ + { worktreeId: localCollision.worktreeId, executionHostId: 'local' }, + { worktreeId: localRemoved.worktreeId, executionHostId: 'local' }, + { worktreeId: remoteRemoved.worktreeId, executionHostId: 'ssh:ssh-1' } + ]) + + expect(snapshotWriteSpy).toHaveBeenCalledTimes(1) + expect(snapshotWriteSpy).toHaveBeenCalledWith( + join(userDataDirHolder.dir, SNAPSHOT_FILE), + expect.objectContaining({ + analysis: expect.objectContaining({ + worktrees: [remoteCollision, kept], + worktreeCount: 2, + scannedWorktreeCount: 2, + unavailableWorktreeCount: 0, + totalSizeBytes: 2500, + reclaimableBytes: 2500 + }) + }) + ) + const cached = await readWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir) + expect(cached?.repos).toEqual([ + expect.objectContaining({ executionHostId: 'local', worktreeCount: 1, totalSizeBytes: 500 }), + expect.objectContaining({ + executionHostId: 'ssh:ssh-1', + worktreeCount: 1, + totalSizeBytes: 2000 + }) + ]) + + snapshotWriteSpy.mockClear() + await pruneWorkspaceSpaceAnalysisSnapshots(userDataDirHolder.dir, [ + { worktreeId: 'repo-1::/missing-local', executionHostId: 'local' }, + { worktreeId: 'repo-1::/missing-remote', executionHostId: 'ssh:ssh-1' } + ]) + expect(snapshotWriteSpy).not.toHaveBeenCalled() + }) + + it('keeps profile snapshots isolated', async () => { + const otherProfile = await mkdtemp(join(tmpdir(), 'orca-space-snapshot-other-')) + try { + await persistWorkspaceSpaceAnalysisSnapshot( + userDataDirHolder.dir, + makeAnalysis([makeWorktreeRow()]) + ) + + await expect(readWorkspaceSpaceAnalysisSnapshot(otherProfile)).resolves.toBeNull() + } finally { + await rm(otherProfile, { recursive: true, force: true }) + } + }) + + it('does not let an analysis started before bulk removal restore pruned rows', async () => { + const local = makeWorktreeRow() + const remote = makeWorktreeRow({ + worktreeId: 'repo-1::/remote-feature', + executionHostId: 'ssh:ssh-1', + isRemote: true, + path: '/remote-feature' + }) + const staleAnalysis = { + ...makeAnalysis([local, remote]), + scannedAt: Date.now() - 1 + } + await pruneWorkspaceSpaceAnalysisSnapshots(userDataDirHolder.dir, [ + { worktreeId: local.worktreeId, executionHostId: 'local' }, + { worktreeId: remote.worktreeId, executionHostId: 'ssh:ssh-1' } + ]) + + await persistWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir, staleAnalysis) + expect((await readWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir))?.worktrees).toEqual([]) + + const recreated = { ...staleAnalysis, scannedAt: Date.now() + 1 } + await persistWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir, recreated) + expect((await readWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir))?.worktrees).toEqual([ + local, + remote + ]) + }) + + it('registers a tombstone immediately without rewriting the sidecar', async () => { + const staleAnalysis = { ...makeAnalysis([makeWorktreeRow()]), scannedAt: Date.now() - 1 } + + registerWorkspaceSpaceAnalysisSnapshotPruneTombstones(userDataDirHolder.dir, [ + { worktreeId: 'repo-1::/repo-feature', executionHostId: 'local' } + ]) + + expect(snapshotWriteSpy).not.toHaveBeenCalled() + await persistWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir, staleAnalysis) + expect((await readWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir))?.worktrees).toEqual([]) + }) + + it('keeps the original tombstone time when a removal batch flushes', async () => { + const row = makeWorktreeRow() + const target = { worktreeId: row.worktreeId, executionHostId: 'local' as const } + const now = vi.spyOn(Date, 'now').mockReturnValue(100) + registerWorkspaceSpaceAnalysisSnapshotPruneTombstones(userDataDirHolder.dir, [target]) + now.mockReturnValue(200) + + await finalizeWorkspaceSpaceAnalysisSnapshotPrunes(userDataDirHolder.dir, [target]) + await persistWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir, { + ...makeAnalysis([row]), + scannedAt: 150 + }) + + expect((await readWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir))?.worktrees).toEqual([ + { ...row, topLevelItems: [] } + ]) + now.mockRestore() + }) + + it('prunes host-colliding workspace ids without corrupting surviving totals', async () => { + const local = makeWorktreeRow({ sizeBytes: 1000, reclaimableBytes: 1000 }) + const remote = makeWorktreeRow({ + executionHostId: 'ssh:ssh-1', + isRemote: true, + sizeBytes: 3000, + reclaimableBytes: 3000 + }) + await persistWorkspaceSpaceAnalysisSnapshot( + userDataDirHolder.dir, + makeAnalysis([local, remote]) + ) + + await pruneWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir, remote.worktreeId, 'ssh:ssh-1') + + const cached = await readWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir) + expect(cached?.worktrees).toEqual([local]) + expect(cached).toMatchObject({ + worktreeCount: 1, + scannedWorktreeCount: 1, + totalSizeBytes: 1000, + reclaimableBytes: 1000 + }) + expect(cached?.repos).toEqual([ + expect.objectContaining({ + executionHostId: 'local', + worktreeCount: 1, + totalSizeBytes: 1000 + }), + expect.objectContaining({ + executionHostId: 'ssh:ssh-1', + worktreeCount: 0, + totalSizeBytes: 0 + }) + ]) + }) + + it('prunes every host-colliding row when the host is unknown', async () => { + const local = makeWorktreeRow({ sizeBytes: 1000, reclaimableBytes: 1000 }) + const remote = makeWorktreeRow({ + executionHostId: 'ssh:ssh-1', + isRemote: true, + sizeBytes: 3000, + reclaimableBytes: 3000 + }) + await persistWorkspaceSpaceAnalysisSnapshot( + userDataDirHolder.dir, + makeAnalysis([local, remote]) + ) + + await pruneWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir, local.worktreeId) + + const cached = await readWorkspaceSpaceAnalysisSnapshot(userDataDirHolder.dir) + expect(cached?.worktrees).toEqual([]) + expect(cached).toMatchObject({ + worktreeCount: 0, + scannedWorktreeCount: 0, + totalSizeBytes: 0, + reclaimableBytes: 0 + }) + }) +}) diff --git a/src/main/workspace-space-analysis-snapshot.ts b/src/main/workspace-space-analysis-snapshot.ts new file mode 100644 index 00000000000..8118095d396 --- /dev/null +++ b/src/main/workspace-space-analysis-snapshot.ts @@ -0,0 +1,328 @@ +import type { + WorkspaceSpaceAnalysis, + WorkspaceSpaceWorktree +} from '../shared/workspace-space-types' +import { + readSidecarSnapshot, + sidecarSnapshotFile, + withSidecarSnapshotQueue, + writeSidecarSnapshot +} from './sidecar-snapshot-file' +import type { ExecutionHostId } from '../shared/execution-host' +import { + activeWorkspaceSnapshotPruneKeys, + registerWorkspaceSnapshotPrunesForFile, + workspaceSnapshotPruneKey, + workspaceSnapshotPruneTargetKeys, + type WorkspaceSnapshotPruneTarget, + type WorkspaceSnapshotPruneTombstone +} from './workspace-snapshot-prune-index' + +const SNAPSHOT_FILE_NAME = 'orca-workspace-space-analysis.json' +const SNAPSHOT_VERSION = 2 + +export type WorkspaceSpaceAnalysisSnapshotPruneTarget = WorkspaceSnapshotPruneTarget + +const prunedWorkspacesByFile = new Map>() + +type PersistedWorkspaceSpaceAnalysisSnapshot = { + version: number + analysis: WorkspaceSpaceAnalysis +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function isPersistableWorktreeRow(value: unknown): value is WorkspaceSpaceWorktree { + if (!isRecord(value)) { + return false + } + return ( + typeof value.worktreeId === 'string' && + typeof value.repoId === 'string' && + (value.executionHostId === undefined || typeof value.executionHostId === 'string') && + typeof value.status === 'string' && + typeof value.sizeBytes === 'number' && + typeof value.reclaimableBytes === 'number' && + Array.isArray(value.topLevelItems) + ) +} + +/** Shape guard so a corrupt persisted blob degrades to null instead of throwing at startup. */ +function parseSnapshot(parsed: unknown): WorkspaceSpaceAnalysis | null { + if (!isRecord(parsed) || parsed.version !== SNAPSHOT_VERSION) { + return null + } + const analysis = parsed.analysis + if (!isRecord(analysis)) { + return null + } + if ( + typeof analysis.scannedAt !== 'number' || + typeof analysis.totalSizeBytes !== 'number' || + !Array.isArray(analysis.repos) || + !Array.isArray(analysis.worktrees) || + !analysis.worktrees.every(isPersistableWorktreeRow) + ) { + return null + } + return analysis as unknown as WorkspaceSpaceAnalysis +} + +// Why strip topLevelItems: 500+ worktrees x 48 items is a multi-MB blob, and the cached view only +// needs per-worktree totals. Fold the items into the omitted counters so each row stays consistent. +function stripTopLevelItems(analysis: WorkspaceSpaceAnalysis): WorkspaceSpaceAnalysis { + return { + ...analysis, + worktrees: analysis.worktrees.map((row) => ({ + ...row, + topLevelItems: [], + omittedTopLevelItemCount: row.omittedTopLevelItemCount + row.topLevelItems.length, + omittedTopLevelSizeBytes: + row.omittedTopLevelSizeBytes + + row.topLevelItems.reduce((sum, item) => sum + item.sizeBytes, 0) + })) + } +} + +export async function readWorkspaceSpaceAnalysisSnapshot( + snapshotDirectory: string +): Promise { + try { + return parseSnapshot( + await readSidecarSnapshot(sidecarSnapshotFile(snapshotDirectory, SNAPSHOT_FILE_NAME)) + ) + } catch { + return null + } +} + +async function writeSnapshot(file: string, analysis: WorkspaceSpaceAnalysis): Promise { + await writeSidecarSnapshot(file, { + version: SNAPSHOT_VERSION, + analysis + } satisfies PersistedWorkspaceSpaceAnalysisSnapshot) +} + +/** Persist a completed analysis. Never throws — the snapshot is a refetchable cache. */ +export async function persistWorkspaceSpaceAnalysisSnapshot( + snapshotDirectory: string, + analysis: WorkspaceSpaceAnalysis +): Promise { + const file = sidecarSnapshotFile(snapshotDirectory, SNAPSHOT_FILE_NAME) + try { + await withSidecarSnapshotQueue(file, async () => { + await writeSnapshot(file, stripTopLevelItems(excludeRowsPrunedDuringScan(file, analysis))) + clearSupersededPrunes(file, analysis) + }) + } catch (error) { + console.warn('[workspace-space] failed to persist analysis snapshot:', error) + } +} + +function withoutWorktreeRows( + analysis: WorkspaceSpaceAnalysis, + shouldRemove: (row: WorkspaceSpaceWorktree) => boolean +): WorkspaceSpaceAnalysis { + const worktrees: WorkspaceSpaceWorktree[] = [] + const removedByRepo = new Map< + string, + { + worktreeCount: number + scannedWorktreeCount: number + unavailableWorktreeCount: number + totalSizeBytes: number + reclaimableBytes: number + } + >() + let removedCount = 0 + let scannedDelta = 0 + let unavailableDelta = 0 + let totalSizeDelta = 0 + let reclaimableDelta = 0 + + for (const row of analysis.worktrees) { + if (!shouldRemove(row)) { + worktrees.push(row) + continue + } + const scanned = row.status === 'ok' ? 1 : 0 + const unavailable = row.status === 'ok' ? 0 : 1 + removedCount += 1 + scannedDelta += scanned + unavailableDelta += unavailable + totalSizeDelta += row.sizeBytes + reclaimableDelta += row.reclaimableBytes + const key = analysisRepoKey(row) + const delta = removedByRepo.get(key) ?? { + worktreeCount: 0, + scannedWorktreeCount: 0, + unavailableWorktreeCount: 0, + totalSizeBytes: 0, + reclaimableBytes: 0 + } + delta.worktreeCount += 1 + delta.scannedWorktreeCount += scanned + delta.unavailableWorktreeCount += unavailable + delta.totalSizeBytes += row.sizeBytes + delta.reclaimableBytes += row.reclaimableBytes + removedByRepo.set(key, delta) + } + if (removedCount === 0) { + return analysis + } + return { + ...analysis, + worktrees, + worktreeCount: Math.max(0, analysis.worktreeCount - removedCount), + scannedWorktreeCount: Math.max(0, analysis.scannedWorktreeCount - scannedDelta), + unavailableWorktreeCount: Math.max(0, analysis.unavailableWorktreeCount - unavailableDelta), + totalSizeBytes: Math.max(0, analysis.totalSizeBytes - totalSizeDelta), + reclaimableBytes: Math.max(0, analysis.reclaimableBytes - reclaimableDelta), + repos: analysis.repos.map((repo) => { + const delta = removedByRepo.get(analysisRepoKey(repo)) + return delta + ? { + ...repo, + worktreeCount: Math.max(0, repo.worktreeCount - delta.worktreeCount), + scannedWorktreeCount: Math.max( + 0, + repo.scannedWorktreeCount - delta.scannedWorktreeCount + ), + unavailableWorktreeCount: Math.max( + 0, + repo.unavailableWorktreeCount - delta.unavailableWorktreeCount + ), + totalSizeBytes: Math.max(0, repo.totalSizeBytes - delta.totalSizeBytes), + reclaimableBytes: Math.max(0, repo.reclaimableBytes - delta.reclaimableBytes) + } + : repo + }) + } +} + +function analysisRepoKey(entry: { repoId: string; executionHostId?: ExecutionHostId }): string { + return JSON.stringify([entry.executionHostId, entry.repoId]) +} + +/** Register anti-resurrection tombstones without scheduling a sidecar rewrite. */ +export function registerWorkspaceSpaceAnalysisSnapshotPruneTombstones( + snapshotDirectory: string, + targets: readonly WorkspaceSpaceAnalysisSnapshotPruneTarget[] +): void { + if (targets.length === 0) { + return + } + registerWorkspaceSnapshotPrunesForFile( + prunedWorkspacesByFile, + sidecarSnapshotFile(snapshotDirectory, SNAPSHOT_FILE_NAME), + targets + ) +} + +function excludeRowsPrunedDuringScan( + file: string, + analysis: WorkspaceSpaceAnalysis +): WorkspaceSpaceAnalysis { + const prunedKeys = activeWorkspaceSnapshotPruneKeys( + prunedWorkspacesByFile.get(file), + analysis.scannedAt + ) + return withoutWorktreeRows( + analysis, + (row) => + prunedKeys.has(workspaceSnapshotPruneKey(row.worktreeId, row.executionHostId)) || + prunedKeys.has(workspaceSnapshotPruneKey(row.worktreeId)) + ) +} + +function clearSupersededPrunes(file: string, analysis: WorkspaceSpaceAnalysis): void { + const pruned = prunedWorkspacesByFile.get(file) + if (!pruned) { + return + } + for (const [key, entry] of pruned) { + if (entry.prunedAt < analysis.scannedAt) { + pruned.delete(key) + } + } + if (pruned.size === 0) { + prunedWorkspacesByFile.delete(file) + } +} + +async function pruneWorkspaceSpaceAnalysisSnapshotsWithRegisteredTombstones( + snapshotDirectory: string, + targets: readonly WorkspaceSpaceAnalysisSnapshotPruneTarget[], + registerTombstones: boolean +): Promise { + if (targets.length === 0) { + return + } + const file = sidecarSnapshotFile(snapshotDirectory, SNAPSHOT_FILE_NAME) + const targetKeys = workspaceSnapshotPruneTargetKeys(targets) + if (registerTombstones) { + registerWorkspaceSnapshotPrunesForFile(prunedWorkspacesByFile, file, targets) + } + try { + await withSidecarSnapshotQueue(file, async () => { + const registered = prunedWorkspacesByFile.get(file) + const coalescedTargetKeys = registerTombstones + ? targetKeys + : new Set([...targetKeys].filter((key) => registered?.has(key))) + if (coalescedTargetKeys.size === 0) { + return + } + const existing = await readWorkspaceSpaceAnalysisSnapshot(snapshotDirectory) + if (!existing) { + return + } + const next = withoutWorktreeRows( + existing, + (row) => + coalescedTargetKeys.has(workspaceSnapshotPruneKey(row.worktreeId, row.executionHostId)) || + coalescedTargetKeys.has(workspaceSnapshotPruneKey(row.worktreeId)) + ) + if (next === existing) { + return + } + await writeSnapshot(file, next) + }) + } catch (error) { + console.warn('[workspace-space] failed to prune analysis snapshot:', error) + } +} + +/** Drop removed workspace rows and rebalance their totals in one sidecar transaction. Never throws. */ +export async function pruneWorkspaceSpaceAnalysisSnapshots( + snapshotDirectory: string, + targets: readonly WorkspaceSpaceAnalysisSnapshotPruneTarget[] +): Promise { + await pruneWorkspaceSpaceAnalysisSnapshotsWithRegisteredTombstones( + snapshotDirectory, + targets, + true + ) +} + +/** Flush only tombstones still active for this batch, preserving their original prune time. */ +export async function finalizeWorkspaceSpaceAnalysisSnapshotPrunes( + snapshotDirectory: string, + targets: readonly WorkspaceSpaceAnalysisSnapshotPruneTarget[] +): Promise { + await pruneWorkspaceSpaceAnalysisSnapshotsWithRegisteredTombstones( + snapshotDirectory, + targets, + false + ) +} + +/** Drop one removed workspace row and rebalance the totals it contributed. Never throws. */ +export async function pruneWorkspaceSpaceAnalysisSnapshot( + snapshotDirectory: string, + worktreeId: string, + executionHostId?: ExecutionHostId +): Promise { + await pruneWorkspaceSpaceAnalysisSnapshots(snapshotDirectory, [{ worktreeId, executionHostId }]) +} diff --git a/src/main/workspace-space-analysis.test.ts b/src/main/workspace-space-analysis.test.ts index 53082b73c7f..d6fbc4bb2d7 100644 --- a/src/main/workspace-space-analysis.test.ts +++ b/src/main/workspace-space-analysis.test.ts @@ -42,14 +42,14 @@ vi.mock('./project-runtime-git-options', () => ({ import { analyzeWorkspaceSpace, WorkspaceSpaceScanCancelledError } from './workspace-space-analysis' -function createStore(repos: Repo[]): Store { +function createStore(repos: Repo[], worktreeHostId?: `runtime:${string}`): Store { return { getRepos: () => repos, getWorktreeMeta: (worktreeId: string) => { if (worktreeId.endsWith('feature')) { - return { displayName: 'Feature Workspace', lastActivityAt: 200 } + return { displayName: 'Feature Workspace', lastActivityAt: 200, hostId: worktreeHostId } } - return undefined + return worktreeHostId ? { hostId: worktreeHostId } : undefined } } as Store } @@ -199,7 +199,7 @@ describe('analyzeWorkspaceSpace', () => { ]) const progress: unknown[] = [] - await analyzeWorkspaceSpace(createStore([repo]), { + await analyzeWorkspaceSpace(createStore([repo], 'runtime:env-1'), { scanId: 'scan-1', onProgress: (event) => progress.push(event) }) @@ -221,6 +221,18 @@ describe('analyzeWorkspaceSpace', () => { scannedRepoCount: 1, scannedWorktreeCount: 1 }) + expect(progress).toContainEqual( + expect.objectContaining({ + completedMeasurements: [ + expect.objectContaining({ + worktreeId: `repo-1::${repoPath}`, + executionHostId: 'runtime:env-1', + status: 'ok', + sizeBytes: expect.any(Number) + }) + ] + }) + ) }) it('rejects when a scan is cancelled before it starts', async () => { diff --git a/src/main/workspace-space-analysis.ts b/src/main/workspace-space-analysis.ts index 56c9a7525cd..9b36c2d3afa 100644 --- a/src/main/workspace-space-analysis.ts +++ b/src/main/workspace-space-analysis.ts @@ -36,6 +36,7 @@ import { getSshGitProvider } from './providers/ssh-git-dispatch' import { createFolderWorktree, listRepoWorktrees } from './repo-worktrees' import { mergeWorktree } from './ipc/worktree-logic' import { getLocalProjectWorktreeGitOptions } from './project-runtime-git-options' +import { getRepoExecutionHostId, getWorktreeExecutionHostId } from '../shared/execution-host' const REPO_SCAN_CONCURRENCY = 2 const WORKTREE_SCAN_CONCURRENCY = 3 @@ -311,6 +312,7 @@ function createBaseWorktreeRow( return { worktreeId: worktree.id, repoId: repo.id, + executionHostId: getWorktreeExecutionHostId(worktree, repo), repoDisplayName: repo.displayName, repoPath: repo.path, displayName: worktree.displayName, @@ -703,8 +705,13 @@ function reportProgress( updates: Partial, onProgress: WorkspaceSpaceAnalyzeOptions['onProgress'] ): void { + const completedMeasurements = updates.completedMeasurements Object.assign(progress, updates, { updatedAt: Date.now() }) - onProgress?.({ ...progress }) + delete progress.completedMeasurements + onProgress?.({ + ...progress, + ...(completedMeasurements?.length ? { completedMeasurements } : {}) + }) } async function scanRepo( @@ -735,6 +742,7 @@ async function scanRepo( worktrees: [], summary: { repoId: repo.id, + executionHostId: getRepoExecutionHostId(repo), displayName: repo.displayName, path: repo.path, isRemote: Boolean(repo.connectionId), @@ -792,7 +800,17 @@ async function scanRepo( ) reportProgress( progress, - { scannedWorktreeCount: progress.scannedWorktreeCount + 1 }, + { + scannedWorktreeCount: progress.scannedWorktreeCount + 1, + completedMeasurements: [ + { + worktreeId: row.worktreeId, + executionHostId: row.executionHostId, + status: row.status, + sizeBytes: row.sizeBytes + } + ] + }, options.onProgress ) return row @@ -811,6 +829,7 @@ async function scanRepo( worktrees: rows, summary: { repoId: repo.id, + executionHostId: getRepoExecutionHostId(repo), displayName: repo.displayName, path: repo.path, isRemote: Boolean(repo.connectionId), diff --git a/src/preload/api/workspace-cleanup-api.ts b/src/preload/api/workspace-cleanup-api.ts index 748d8216540..134dc2ee519 100644 --- a/src/preload/api/workspace-cleanup-api.ts +++ b/src/preload/api/workspace-cleanup-api.ts @@ -4,9 +4,12 @@ import type { WorkspaceCleanupLocalProcessResult, WorkspaceCleanupScanArgs, WorkspaceCleanupScanProgress, - WorkspaceCleanupScanResult + WorkspaceCleanupScanResult, + WorkspaceCleanupSnapshotPruneBatchArgs, + WorkspaceCleanupSnapshotPruneRecordArgs } from '../../shared/workspace-cleanup' import type { + WorkspaceSpaceAnalysis, WorkspaceSpaceAnalyzeResult, WorkspaceSpaceScanProgress } from '../../shared/workspace-space-types' @@ -16,15 +19,23 @@ export type WorkspaceCleanupApi = { args?: WorkspaceCleanupScanArgs, onProgress?: (progress: WorkspaceCleanupScanProgress) => void ) => Promise + cancelScan?: (scanId: string) => Promise + /** Last persisted broad-scan snapshot; null until one completes or when the cache is stale/corrupt. */ + getCachedScan: () => Promise dismiss: (args: WorkspaceCleanupDismissArgs) => Promise clearDismissals: () => Promise hasKillableLocalProcesses: ( args: WorkspaceCleanupLocalProcessArgs ) => Promise + beginRemovalSnapshotPruneBatch?: (args: WorkspaceCleanupSnapshotPruneBatchArgs) => Promise + recordRemovalSnapshotPrune?: (args: WorkspaceCleanupSnapshotPruneRecordArgs) => Promise + finishRemovalSnapshotPruneBatch?: (args: WorkspaceCleanupSnapshotPruneBatchArgs) => Promise } export type WorkspaceSpaceApi = { analyze: () => Promise + /** Last persisted analysis (topLevelItems pruned to bound the payload); null until one completes. */ + getCachedAnalysis: () => Promise cancel: () => Promise onProgress: (callback: (progress: WorkspaceSpaceScanProgress) => void) => () => void } diff --git a/src/preload/api/worktree-api.ts b/src/preload/api/worktree-api.ts index 20c352af9d3..b6564d3f5db 100644 --- a/src/preload/api/worktree-api.ts +++ b/src/preload/api/worktree-api.ts @@ -93,11 +93,13 @@ export type WorktreeApi = { // may waive the proof that every PTY stopped. allowUnverifiedPtyStop?: boolean skipArchive?: boolean + snapshotPruneBatchId?: string }) => Promise // Forget a workspace from Orca only (no remote Git/FS work) — for workspaces pinned to a removed/disconnected SSH host. forgetLocal: (args: { worktreeId: string hostId?: ExecutionHostId + snapshotPruneBatchId?: string }) => Promise forceDeletePreservedBranch: (args: { worktreeId: string diff --git a/src/preload/index.ts b/src/preload/index.ts index 17db7429632..46c5d5362d8 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -908,14 +908,23 @@ const api = { .invoke('workspaceCleanup:scan', { ...args, scanId }) .finally(() => ipcRenderer.removeListener('workspaceCleanup:scanProgress', listener)) }, + cancelScan: (scanId) => ipcRenderer.invoke('workspaceCleanup:cancelScan', scanId), + getCachedScan: () => ipcRenderer.invoke('workspaceCleanup:getCachedScan'), dismiss: (args) => ipcRenderer.invoke('workspaceCleanup:dismiss', args), clearDismissals: () => ipcRenderer.invoke('workspaceCleanup:clearDismissals'), hasKillableLocalProcesses: (args) => - ipcRenderer.invoke('workspaceCleanup:hasKillableLocalProcesses', args) + ipcRenderer.invoke('workspaceCleanup:hasKillableLocalProcesses', args), + beginRemovalSnapshotPruneBatch: (args) => + ipcRenderer.invoke('workspaceCleanup:beginRemovalSnapshotPruneBatch', args), + recordRemovalSnapshotPrune: (args) => + ipcRenderer.invoke('workspaceCleanup:recordRemovalSnapshotPrune', args), + finishRemovalSnapshotPruneBatch: (args) => + ipcRenderer.invoke('workspaceCleanup:finishRemovalSnapshotPruneBatch', args) } satisfies PreloadApi['workspaceCleanup'], workspaceSpace: { analyze: () => ipcRenderer.invoke('workspaceSpace:analyze'), + getCachedAnalysis: () => ipcRenderer.invoke('workspaceSpace:getCachedAnalysis'), cancel: () => ipcRenderer.invoke('workspaceSpace:cancel'), onProgress: (callback) => { const listener = ( diff --git a/src/renderer/src/components/sidebar/delete-worktree-parallel-flow.test.ts b/src/renderer/src/components/sidebar/delete-worktree-parallel-flow.test.ts index 42411ff6fce..b4930d74742 100644 --- a/src/renderer/src/components/sidebar/delete-worktree-parallel-flow.test.ts +++ b/src/renderer/src/components/sidebar/delete-worktree-parallel-flow.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => { const state = { @@ -85,6 +85,44 @@ describe('runWorktreeDeletesInParallel', () => { vi.mocked(showPreservedBranchBatchToast).mockClear() }) + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('uses one snapshot prune batch for a 100-workspace delete', async () => { + const begin = vi.fn(async (_args: { batchId: string }) => undefined) + const record = vi.fn(async (_args: { batchId: string; worktreeId: string }) => undefined) + const finish = vi.fn(async (_args: { batchId: string }) => undefined) + vi.stubGlobal('window', { + api: { + workspaceCleanup: { + beginRemovalSnapshotPruneBatch: begin, + recordRemovalSnapshotPrune: record, + finishRemovalSnapshotPruneBatch: finish + } + } + }) + const targets = Array.from({ length: 100 }, (_, index) => ({ + id: `wt-${index}`, + displayName: `workspace ${index}`, + repoId: `repo-${index % 10}`, + path: `/workspaces/${index}` + })) + + await expect(runDeletesForCurrentWorktrees(targets)).resolves.toHaveLength(100) + + expect(begin).toHaveBeenCalledOnce() + const batchId = begin.mock.calls[0]?.[0].batchId + expect(batchId).toEqual(expect.any(String)) + expect(mocks.state.removeWorktree).toHaveBeenCalledTimes(100) + for (const call of mocks.state.removeWorktree.mock.calls) { + expect(call[2]).toMatchObject({ snapshotPruneBatchId: batchId }) + } + expect(finish).toHaveBeenCalledOnce() + expect(finish).toHaveBeenCalledWith({ batchId }) + expect(record).not.toHaveBeenCalled() + }) + it('starts every selected delete before waiting for earlier deletes to finish', async () => { const first = deferredDeleteResult() const second = deferredDeleteResult() diff --git a/src/renderer/src/components/sidebar/worktree-delete-execution.ts b/src/renderer/src/components/sidebar/worktree-delete-execution.ts index f105acedcdf..7d0ca496a1a 100644 --- a/src/renderer/src/components/sidebar/worktree-delete-execution.ts +++ b/src/renderer/src/components/sidebar/worktree-delete-execution.ts @@ -14,6 +14,7 @@ import { showWorkspaceListChangedToast } from './stale-workspace-list-toast' import { showPreservedBranchBatchToast } from './preserved-branch-batch-toast' import type { PreservedBranchCleanup } from '@/lib/preserved-branch-cleanup' import type { WorktreeDeleteWithToastOptions } from './worktree-delete-request' +import { beginWorktreeSnapshotPruneBatch } from './worktree-snapshot-prune-batch' // A failed delete usually means unresolved changes, so land on the diff panel. function viewWorktreeDiff(worktreeId: string): void { @@ -60,41 +61,56 @@ export async function runWorktreeDeletesInParallel( const preservedBranches: PreservedBranchCleanup[] = [] const aggregatePreservedBranches = uniqueTargets.length > 1 let listChanged = false - const groupResults = await Promise.all( - Array.from(groups.values()).map(async (group) => { - const deletedInGroup: string[] = [] - const failedInGroup: (typeof group)[number][] = [] - for (const target of group) { - // A queued target may be recreated while an earlier repo sibling is deleting. - const currentTarget = getWorktreeMapFromState(useAppStore.getState()).get(target.id) - if (!currentTarget || currentTarget.instanceId !== target.instanceId) { - useAppStore.getState().clearWorktreeDeleteState(target.id) - listChanged = true - continue - } - if (failedInGroup.some((failed) => isStrictDescendantPath(target.path, failed.path))) { - useAppStore.getState().clearWorktreeDeleteState(target.id) - continue - } - const deleted = await runWorktreeDeleteWithToast(target.id, target.displayName, { - ...options, - focusSuccessorOnDelete: false, - suppressPreservedBranchToast: aggregatePreservedBranches, - onPreservedBranch: (branch) => { - preservedBranches.push(branch) - options.onPreservedBranch?.(branch) + const pendingSnapshotPruneBatch = + uniqueTargets.length > 1 ? beginWorktreeSnapshotPruneBatch() : null + const snapshotPruneBatch = pendingSnapshotPruneBatch ? await pendingSnapshotPruneBatch : null + let groupResults: string[][] + try { + groupResults = await Promise.all( + Array.from(groups.values()).map(async (group) => { + const deletedInGroup: string[] = [] + const failedInGroup: (typeof group)[number][] = [] + for (const target of group) { + // A queued target may be recreated while an earlier repo sibling is deleting. + const currentTarget = getWorktreeMapFromState(useAppStore.getState()).get(target.id) + if (!currentTarget || currentTarget.instanceId !== target.instanceId) { + useAppStore.getState().clearWorktreeDeleteState(target.id) + listChanged = true + continue + } + if (failedInGroup.some((failed) => isStrictDescendantPath(target.path, failed.path))) { + useAppStore.getState().clearWorktreeDeleteState(target.id) + continue + } + const deleted = await runWorktreeDeleteWithToast(target.id, target.displayName, { + ...options, + focusSuccessorOnDelete: false, + suppressPreservedBranchToast: aggregatePreservedBranches, + ...(snapshotPruneBatch ? { snapshotPruneBatchId: snapshotPruneBatch.batchId } : {}), + onPreservedBranch: (branch) => { + preservedBranches.push(branch) + options.onPreservedBranch?.(branch) + } + }) + if (deleted) { + deletedInGroup.push(target.id) + } else { + // A failed child makes deleting its ancestor unsafe because the child lives below it. + failedInGroup.push(target) } - }) - if (deleted) { - deletedInGroup.push(target.id) - } else { - // A failed child makes deleting its ancestor unsafe because the child lives below it. - failedInGroup.push(target) } + return deletedInGroup + }) + ) + } finally { + if (snapshotPruneBatch) { + try { + await snapshotPruneBatch.finish() + } catch (error) { + console.warn('Failed to finish workspace snapshot prune batch:', error) } - return deletedInGroup - }) - ) + } + } if (listChanged) { showWorkspaceListChangedToast() } @@ -125,9 +141,14 @@ export function runWorktreeDeleteWithToast( const commitFocus = prepareActiveWorktreeFocusAfterDelete(worktreeId) const focusSuccessor = options.focusSuccessorOnDelete !== false - const removal = options.suppressPreservedBranchToast - ? removeWorktree(worktreeId, options.force === true, { suppressPreservedBranchToast: true }) - : removeWorktree(worktreeId, options.force === true) + const removeOptions = { + ...(options.suppressPreservedBranchToast ? { suppressPreservedBranchToast: true } : {}), + ...(options.snapshotPruneBatchId ? { snapshotPruneBatchId: options.snapshotPruneBatchId } : {}) + } + const removal = + Object.keys(removeOptions).length > 0 + ? removeWorktree(worktreeId, options.force === true, removeOptions) + : removeWorktree(worktreeId, options.force === true) return removal .then((result) => { if (result.ok) { diff --git a/src/renderer/src/components/sidebar/worktree-delete-request.ts b/src/renderer/src/components/sidebar/worktree-delete-request.ts index df357d8c55e..f57a83a6b65 100644 --- a/src/renderer/src/components/sidebar/worktree-delete-request.ts +++ b/src/renderer/src/components/sidebar/worktree-delete-request.ts @@ -17,6 +17,7 @@ export type WorktreeDeleteWithToastOptions = { onForceDeleted?: (worktreeId: string) => void onPreservedBranch?: (branch: PreservedBranchCleanup) => void suppressPreservedBranchToast?: boolean + snapshotPruneBatchId?: string // Batch deletion commits one focus handoff after all targets settle. focusSuccessorOnDelete?: boolean } diff --git a/src/renderer/src/components/sidebar/worktree-snapshot-prune-batch.ts b/src/renderer/src/components/sidebar/worktree-snapshot-prune-batch.ts new file mode 100644 index 00000000000..91e970291fd --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-snapshot-prune-batch.ts @@ -0,0 +1,24 @@ +export type WorktreeSnapshotPruneBatch = { + batchId: string + finish: () => Promise +} + +export function beginWorktreeSnapshotPruneBatch(): Promise | null { + if (typeof window === 'undefined') { + return null + } + const api = window.api.workspaceCleanup + const begin = api.beginRemovalSnapshotPruneBatch + const record = api.recordRemovalSnapshotPrune + const finish = api.finishRemovalSnapshotPruneBatch + if (typeof begin !== 'function' || typeof record !== 'function' || typeof finish !== 'function') { + return null + } + const batchId = crypto.randomUUID() + return begin({ batchId }) + .then(() => ({ batchId, finish: () => finish({ batchId }) })) + .catch((error: unknown) => { + console.warn('Failed to begin workspace snapshot prune batch:', error) + return null + }) +} diff --git a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx index 688d59c9a14..6ddfef2251c 100644 --- a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx +++ b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.tsx @@ -36,7 +36,6 @@ import type { AppMemory, UsageValues } from '../../../../shared/process-stats-ty import type { Worktree } from '../../../../shared/worktree/types' import { ORPHAN_WORKTREE_ID } from '../../../../shared/constants' import { getRepoExecutionHostId, parseExecutionHostId } from '../../../../shared/execution-host' -import { countEstimatedInactiveWorkspaces } from '../workspace-cleanup/inactive-workspace-estimate' import { mergeSnapshotAndSessions, UNATTRIBUTED_REPO_ID } from './mergeSnapshotAndSessions' import type { Metric, @@ -909,17 +908,11 @@ export function ResourceUsageStatusSegment({ return map }, [repos]) - const repoById = useMemo(() => new Map(repos.map((repo) => [repo.id, repo])), [repos]) const worktreeById = useMemo( () => new Map(allWorktrees.map((worktree) => [worktree.id, worktree])), [allWorktrees] ) - const oldWorkspaceCount = useMemo( - () => countEstimatedInactiveWorkspaces(allWorktrees, repoById, Date.now()), - [allWorktrees, repoById] - ) - // Why: skip the merge when closed; the always-mounted segment recomputing on every keystroke-driven store mutation made the app laggy. const unifiedRepos = useMemo( () => @@ -1482,8 +1475,7 @@ export function ResourceUsageStatusSegment({ {translate( 'auto.components.status.bar.ResourceUsageStatusSegment.92924a14e3', - 'Review inactive workspaces ({{value0}})', - { value0: oldWorkspaceCount } + 'Clean up workspaces' )} 0 + ? { + name: translate( + 'components.status.bar.workspaceSpace.otherTopLevelItems', + 'Other top-level items ({{value0}})', + { value0: worktree.omittedTopLevelItemCount } + ), + path: '', + kind: 'other', + sizeBytes: worktree.omittedTopLevelSizeBytes + } + : null return (
@@ -1010,7 +1026,7 @@ function BreakdownList({ )}
- ) : worktree.topLevelItems.length === 0 ? ( + ) : topLevelItemCount === 0 ? (
{translate( 'auto.components.status.bar.WorkspaceSpaceManagerPanel.16988df079', @@ -1023,6 +1039,7 @@ function BreakdownList({ {worktree.topLevelItems.slice(0, 12).map((item) => ( ))} + {omittedItem ? : null}
)} diff --git a/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.mount-gating.test.tsx b/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.mount-gating.test.tsx index 7887b7978fc..49656d86e12 100644 --- a/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.mount-gating.test.tsx +++ b/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.mount-gating.test.tsx @@ -1,210 +1,84 @@ // @vitest-environment happy-dom - -import { act } from 'react' +import { act, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useAppStore } from '@/store' import type { AppState } from '@/store/types' -import { readStoreListenerCount } from '@/store/store-listener-census' -import type { - WorkspaceCleanupCandidate, - WorkspaceCleanupScanResult -} from '../../../../shared/workspace-cleanup' -import type * as WorkspaceCleanupPresentation from './workspace-cleanup-presentation' +import type { WorkspaceCleanupScanResult } from '../../../../shared/workspace-cleanup' import WorkspaceCleanupDialog from './WorkspaceCleanupDialog' -import { NOW, makeCandidate, makeState } from './workspace-cleanup-presentation-fixtures' +import { WorkspaceCleanupScanSupersededError } from '@/store/slices/workspace-cleanup-broad-scan-registry' -const contentProbe = vi.hoisted(() => ({ - mounts: vi.fn(), - projections: vi.fn(), - renders: vi.fn(), - storeNotifications: vi.fn(), - unmounts: vi.fn() -})) +const probes = vi.hoisted(() => ({ facets: vi.fn() })) +const toast = vi.hoisted(() => ({ error: vi.fn(), success: vi.fn() })) -const toastProbe = vi.hoisted(() => ({ - error: vi.fn(), - info: vi.fn(), - message: vi.fn(), - success: vi.fn(), - warning: vi.fn() -})) - -vi.mock('sonner', () => ({ toast: toastProbe })) - -vi.mock('./workspace-cleanup-presentation', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - filterWorkspaceCleanupCandidates: ( - ...args: Parameters - ) => { - contentProbe.projections() - return actual.filterWorkspaceCleanupCandidates(...args) - }, - getWorkspaceCleanupReviewInfo: ( - ...args: Parameters - ) => { - contentProbe.projections() - return actual.getWorkspaceCleanupReviewInfo(...args) - }, - sortWorkspaceCleanupCandidates: ( - ...args: Parameters - ) => { - contentProbe.projections() - return actual.sortWorkspaceCleanupCandidates(...args) +vi.mock('sonner', () => ({ toast })) +vi.mock('./use-workspace-cleanup-facet-rows', () => ({ + useWorkspaceCleanupFacetRows: () => { + probes.facets() + return { + rows: [], + selectableWorktreeIds: [], + facetMatchedWorktreeIds: new Set(), + matchedCount: 0, + totalCount: 0, + facetCounts: { + activity: 0, + size: 0, + status: 0, + agent: 0, + git: 0, + review: 0, + ticket: 0, + context: 0, + location: 0, + safety: 0 + }, + options: { workspaceStatuses: [], hostIds: [], repos: [], reviewProviders: [] }, + reviewInfoByWorktreeId: new Map(), + sizeByWorktreeId: new Map(), + measuredSizeCount: 0, + unmeasuredSizeCount: 0 } } -}) - +})) vi.mock('@/components/ui/dialog', async () => { - const React = await import('react') - const Passthrough = ({ children }: { children: React.ReactNode }) => <>{children} - const DialogContent = () => { - contentProbe.renders() - React.useEffect(() => { - contentProbe.mounts() - const unsubscribe = useAppStore.subscribe(() => contentProbe.storeNotifications()) - return () => { - contentProbe.unmounts() - unsubscribe() - } - }, []) - // Why: render while closed so the marker measures Orca's content gate, not Radix's portal gate. - return
- } + const Passthrough = ({ children }: { children: ReactNode }) => <>{children} return { Dialog: Passthrough, - DialogContent, - DialogDescription: Passthrough, - DialogFooter: Passthrough, - DialogHeader: Passthrough, - DialogTitle: Passthrough + DialogContent: () =>
} }) -type Deferred = { - promise: Promise - resolve: (value: T) => void +const initialState = useAppStore.getInitialState() +let container: HTMLDivElement +let root: Root + +function emptyScan(scannedAt: number): WorkspaceCleanupScanResult { + return { scannedAt, candidates: [], errors: [] } } -const initialAppState = useAppStore.getInitialState() -const fixtureState = makeState() -const initialCandidate = makeCandidate() -const initialScan = makeScan(1, initialCandidate) -let testContainer: HTMLDivElement -let testRoot: Root - -function deferred(): Deferred { - let resolve!: (value: T) => void - const promise = new Promise((resolvePromise) => { - resolve = resolvePromise - }) - return { promise, resolve } -} - -function makeScan(index: number, candidate: WorkspaceCleanupCandidate): WorkspaceCleanupScanResult { - return { - scannedAt: NOW + index, - candidates: [candidate], - errors: [] - } -} - -function makeChurnCandidate(index: number): WorkspaceCleanupCandidate { - return makeCandidate({ - worktreeId: `repo-1::/repo/background-${index}`, - displayName: `background-${index}`, - path: `/repo/background-${index}`, - fingerprint: `background-${index}` - }) -} - -function activeContentSubscriptions(): number { - return contentProbe.mounts.mock.calls.length - contentProbe.unmounts.mock.calls.length -} - -function requireStoreListenerCount(): number { - const count = readStoreListenerCount() - expect(count).not.toBeNull() - return count ?? 0 -} - -async function flushEffects(): Promise { +async function flush(): Promise { await act(async () => { await Promise.resolve() await Promise.resolve() }) } -async function renderDialog(): Promise { - await act(async () => testRoot.render()) - await flushEffects() -} - async function openDialog(): Promise { await act(async () => useAppStore.getState().openModal('workspace-cleanup')) - await flushEffects() + await flush() } async function closeDialog(): Promise { await act(async () => useAppStore.getState().closeModal()) - await flushEffects() + await flush() } -async function publishScanProgress(index: number): Promise { - const scan = makeScan(index, makeChurnCandidate(index)) - await act(async () => { - useAppStore.setState({ - workspaceCleanupScan: scan, - workspaceCleanupProgress: { - ...scan, - scanId: `scan-${index}`, - scannedWorktreeCount: 1, - totalWorktreeCount: 1 - } - }) - }) -} - -async function publishReviewChurn(index: number): Promise { - await act(async () => { - useAppStore.setState({ - hostedReviewCache: { - [`background-${index}`]: { data: null, fetchedAt: NOW + index } - } - }) - }) -} - -async function publishDeleteChurn(index: number): Promise { - const candidate = makeChurnCandidate(index) - await act(async () => { - useAppStore.setState({ - deleteStateByWorktreeId: { - [candidate.worktreeId]: { - isDeleting: true, - phase: 'queued', - error: null, - canForceDelete: false, - forceDeleteReason: null - } - } - }) - }) -} - -function seedStore( - scanWorkspaceCleanup: AppState['scanWorkspaceCleanup'] = vi.fn(async () => initialScan) -): void { - useAppStore.setState(initialAppState, true) +function seedStore(scanWorkspaceCleanup: AppState['scanWorkspaceCleanup']): void { + useAppStore.setState(initialState, true) useAppStore.setState({ activeModal: 'none', - repos: fixtureState.repos, - worktreesByRepo: fixtureState.worktreesByRepo, - hostedReviewCache: {}, - deleteStateByWorktreeId: {}, - workspaceCleanupScan: initialScan, + workspaceCleanupScan: emptyScan(1), workspaceCleanupProgress: null, workspaceCleanupLoading: false, workspaceCleanupError: null, @@ -216,117 +90,81 @@ describe('WorkspaceCleanupDialog mount gating', () => { beforeEach(() => { globalThis.IS_REACT_ACT_ENVIRONMENT = true vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) - Object.values(contentProbe).forEach((probe) => probe.mockClear()) - Object.values(toastProbe).forEach((probe) => probe.mockClear()) - seedStore() - testContainer = document.createElement('div') - document.body.appendChild(testContainer) - testRoot = createRoot(testContainer) + probes.facets.mockClear() + toast.error.mockClear() + toast.success.mockClear() + seedStore(vi.fn(async () => emptyScan(2))) + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) }) afterEach(async () => { - await act(async () => testRoot.unmount()) + await act(async () => root.unmount()) document.body.replaceChildren() - useAppStore.setState(initialAppState, true) + useAppStore.setState(initialState, true) vi.clearAllTimers() vi.useRealTimers() }) - it('keeps heavy content through 299 ms, then drops its projections and subscriptions', async () => { - const listenerBaseline = requireStoreListenerCount() - await renderDialog() - const shellListenerCount = requireStoreListenerCount() - - expect(shellListenerCount).toBe(listenerBaseline + 1) - expect(testContainer.querySelector('[data-workspace-cleanup-heavy-content]')).toBeNull() - expect(activeContentSubscriptions()).toBe(0) + it('drops heavy projections after the close animation', async () => { + await act(async () => root.render()) + await flush() + expect(probes.facets).not.toHaveBeenCalled() await openDialog() - const openListenerCount = requireStoreListenerCount() - - expect(testContainer.querySelector('[data-workspace-cleanup-heavy-content]')).not.toBeNull() - expect(activeContentSubscriptions()).toBe(1) - expect(openListenerCount).toBeGreaterThan(shellListenerCount) - - await closeDialog() - await act(async () => vi.advanceTimersByTimeAsync(299)) - - expect(testContainer.querySelector('[data-workspace-cleanup-heavy-content]')).not.toBeNull() - expect(activeContentSubscriptions()).toBe(1) - - const lingeringProjectionCount = contentProbe.projections.mock.calls.length - const lingeringNotificationCount = contentProbe.storeNotifications.mock.calls.length - await publishScanProgress(2) - await publishReviewChurn(2) - await publishDeleteChurn(2) - - expect(contentProbe.projections.mock.calls.length).toBeGreaterThan(lingeringProjectionCount) - expect(contentProbe.storeNotifications.mock.calls.length).toBeGreaterThan( - lingeringNotificationCount - ) - - await act(async () => vi.advanceTimersByTimeAsync(1)) - - expect(testContainer.querySelector('[data-workspace-cleanup-heavy-content]')).toBeNull() - expect(activeContentSubscriptions()).toBe(0) - expect(requireStoreListenerCount()).toBe(shellListenerCount) - - const hiddenProjectionCount = contentProbe.projections.mock.calls.length - const hiddenRenderCount = contentProbe.renders.mock.calls.length - const hiddenNotificationCount = contentProbe.storeNotifications.mock.calls.length - for (let index = 3; index < 103; index += 1) { - await publishScanProgress(index) - await publishReviewChurn(index) - await publishDeleteChurn(index) - } - - expect(contentProbe.projections).toHaveBeenCalledTimes(hiddenProjectionCount) - expect(contentProbe.renders).toHaveBeenCalledTimes(hiddenRenderCount) - expect(contentProbe.storeNotifications).toHaveBeenCalledTimes(hiddenNotificationCount) - }) - - it('cancels the pending content unmount when reopened during the linger', async () => { - await renderDialog() - await openDialog() - await closeDialog() - await act(async () => vi.advanceTimersByTimeAsync(299)) - await openDialog() - await act(async () => vi.advanceTimersByTimeAsync(1_000)) - - expect(testContainer.querySelector('[data-workspace-cleanup-heavy-content]')).not.toBeNull() - expect(activeContentSubscriptions()).toBe(1) - - const projectionCount = contentProbe.projections.mock.calls.length - await publishScanProgress(4) - expect(contentProbe.projections.mock.calls.length).toBeGreaterThan(projectionCount) - }) - - it('toasts when an open-time scan settles after the closed content unmounts', async () => { - const pendingScan = deferred() - const scanWorkspaceCleanup = vi.fn(() => pendingScan.promise) - seedStore(scanWorkspaceCleanup) - await renderDialog() - await openDialog() - - expect(scanWorkspaceCleanup).toHaveBeenCalledTimes(1) + expect(probes.facets).toHaveBeenCalled() await closeDialog() await act(async () => vi.advanceTimersByTimeAsync(300)) - expect(testContainer.querySelector('[data-workspace-cleanup-heavy-content]')).toBeNull() + const hiddenCalls = probes.facets.mock.calls.length - const completedScan = makeScan(5, makeChurnCandidate(5)) - await act(async () => { - pendingScan.resolve(completedScan) - await pendingScan.promise - await Promise.resolve() - }) - await flushEffects() - - expect(toastProbe.success).toHaveBeenCalledWith( - 'Inactive workspace scan ready', - expect.objectContaining({ - action: expect.objectContaining({ label: 'Review' }) + for (let index = 0; index < 100; index += 1) { + await act(async () => { + useAppStore.setState({ + workspaceCleanupScan: emptyScan(index + 10), + hostedReviewCache: { [`review-${index}`]: { data: null, fetchedAt: index } }, + deleteStateByWorktreeId: {} + }) }) + } + + expect(probes.facets).toHaveBeenCalledTimes(hiddenCalls) + }) + + it('keeps scan completion ownership after heavy content unmounts', async () => { + let resolveScan!: (result: WorkspaceCleanupScanResult) => void + const scanPromise = new Promise((resolve) => { + resolveScan = resolve + }) + const scanWorkspaceCleanup = vi.fn(() => scanPromise) + seedStore(scanWorkspaceCleanup) + await act(async () => root.render()) + await openDialog() + + await closeDialog() + await act(async () => vi.advanceTimersByTimeAsync(300)) + expect(container.querySelector('[data-workspace-cleanup-content]')).toBeNull() + + await act(async () => resolveScan(emptyScan(20))) + await flush() + + expect(toast.success).toHaveBeenCalledWith( + 'Workspace scan ready', + expect.objectContaining({ action: expect.objectContaining({ label: 'Review' }) }) ) }) + + it('does not report a superseded scan as a failure', async () => { + seedStore( + vi.fn(async () => { + throw new WorkspaceCleanupScanSupersededError() + }) + ) + await act(async () => root.render()) + + await openDialog() + + expect(toast.error).not.toHaveBeenCalled() + }) }) diff --git a/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.stale-while-revalidate.test.tsx b/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.stale-while-revalidate.test.tsx new file mode 100644 index 00000000000..204b28317f1 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.stale-while-revalidate.test.tsx @@ -0,0 +1,399 @@ +// @vitest-environment happy-dom +import type { ReactNode, Ref } from 'react' +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { + WorkspaceCleanupScanProgress, + WorkspaceCleanupScanResult +} from '../../../../shared/workspace-cleanup' +import { createTestStore, seedStore } from '../../store/slices/store-test-helpers' +import { resetWorkspaceCleanupBrowsePersistTimer } from '../../store/slices/workspace-cleanup-browse' +import { NOW, makeCandidate } from '../../store/slices/workspace-cleanup-slice-test-harness' +import WorkspaceCleanupDialog from './WorkspaceCleanupDialog' + +const holders = vi.hoisted(() => ({ + store: null as unknown as { (selector: (s: unknown) => unknown): unknown } & { + getState: () => Record + setState: (partial: unknown) => void + subscribe: (listener: (state: unknown, previous: unknown) => void) => () => void + }, + infoToasts: [] as string[], + rig: null as null | { + resolvers: ((result: WorkspaceCleanupScanResult) => void)[] + } +})) + +vi.mock('@/store', () => { + const useAppStore = (selector: (s: unknown) => unknown): unknown => holders.store(selector) + useAppStore.getState = () => holders.store.getState() + useAppStore.setState = (partial: unknown) => holders.store.setState(partial) + useAppStore.subscribe = (listener: (state: unknown, previous: unknown) => void) => + holders.store.subscribe(listener) + return { useAppStore } +}) + +vi.mock('sonner', () => ({ + toast: Object.assign(vi.fn(), { + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + info: (message: unknown) => holders.infoToasts.push(String(message)) + }) +})) + +vi.mock('@/lib/worktree-activation', () => ({ activateAndRevealWorktree: vi.fn() })) + +vi.mock('@/components/ui/dialog', () => ({ + Dialog: ({ open, children }: { open: boolean; children: ReactNode }) => + open ?
{children}
: null, + DialogContent: ({ children }: { children: ReactNode }) =>
{children}
, + DialogHeader: ({ children }: { children: ReactNode }) =>
{children}
, + DialogTitle: ({ children }: { children: ReactNode }) =>
{children}
, + DialogDescription: ({ children }: { children: ReactNode }) =>
{children}
, + DialogFooter: ({ children }: { children: ReactNode }) =>
{children}
+})) + +vi.mock('@/components/ui/scroll-area', () => ({ + ScrollArea: ({ children, viewportRef }: { children: ReactNode; viewportRef?: Ref }) => ( +
}>{children}
+ ), + ScrollBar: () => null +})) + +vi.mock('@/components/ui/tooltip', () => ({ + Tooltip: ({ children }: { children: ReactNode }) => <>{children}, + TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}, + TooltipContent: () => null +})) + +vi.mock('@/components/ui/popover', () => ({ + Popover: ({ children }: { children: ReactNode }) => <>{children}, + PopoverTrigger: ({ children }: { children: ReactNode }) => <>{children}, + PopoverContent: () => null +})) + +vi.mock('@/components/ui/dropdown-menu', () => ({ + DropdownMenu: ({ children }: { children: ReactNode }) => <>{children}, + DropdownMenuTrigger: ({ children }: { children: ReactNode }) => <>{children}, + DropdownMenuContent: () => null, + DropdownMenuItem: ({ children }: { children: ReactNode }) =>
{children}
, + DropdownMenuLabel: ({ children }: { children: ReactNode }) =>
{children}
+})) + +const CACHED_AT = NOW - 2 * 60 * 60 * 1000 + +type ScanRig = { + scan: ReturnType + progressCallbacks: ((progress: WorkspaceCleanupScanProgress) => void)[] + resolvers: ((result: WorkspaceCleanupScanResult) => void)[] +} + +function installApi(cachedScan: WorkspaceCleanupScanResult | null): ScanRig { + const rig: ScanRig = { scan: vi.fn(), progressCallbacks: [], resolvers: [] } + rig.scan.mockImplementation( + ( + args?: { worktreeId?: string }, + onProgress?: (progress: WorkspaceCleanupScanProgress) => void + ) => { + if (args?.worktreeId) { + return Promise.resolve({ + scannedAt: NOW, + candidates: [makeCandidate({ worktreeId: args.worktreeId })], + errors: [] + }) + } + if (onProgress) { + rig.progressCallbacks.push(onProgress) + } + return new Promise((resolve) => { + rig.resolvers.push(resolve) + }) + } + ) + ;(window as unknown as { api: unknown }).api = { + workspaceCleanup: { + scan: rig.scan, + getCachedScan: vi.fn().mockResolvedValue(cachedScan), + dismiss: vi.fn().mockResolvedValue(undefined), + clearDismissals: vi.fn().mockResolvedValue(undefined), + hasKillableLocalProcesses: vi.fn().mockResolvedValue({ hasKillableProcesses: false }) + }, + workspaceSpace: { + getCachedAnalysis: vi.fn().mockResolvedValue(null), + analyze: vi.fn(), + cancel: vi.fn().mockResolvedValue(false), + onProgress: vi.fn().mockReturnValue(() => undefined) + }, + ui: { set: vi.fn().mockResolvedValue(undefined) } + } + holders.rig = rig + return rig +} + +let root: Root | null = null +let container: HTMLDivElement | null = null + +async function flush(): Promise { + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)) + await new Promise((resolve) => setTimeout(resolve, 0)) + }) +} + +async function renderDialog(): Promise { + await act(async () => { + root?.render() + }) +} + +async function openDialog(): Promise { + await act(async () => { + holders.store.setState({ activeModal: 'workspace-cleanup' }) + }) + await flush() +} + +function rowNames(): string[] { + return [...(container?.querySelectorAll('[data-workspace-cleanup-row-name]') ?? [])].map( + (node) => node.textContent ?? '' + ) +} + +function rowCheckbox(name: string): HTMLElement | null { + return container?.querySelector(`[aria-label="Select ${name}"]`) ?? null +} + +function cachedFleet(): WorkspaceCleanupScanResult { + return { + scannedAt: CACHED_AT, + candidates: [ + makeCandidate({ worktreeId: 'repo1::/tmp/alpha', displayName: 'alpha' }), + makeCandidate({ worktreeId: 'repo1::/tmp/beta', displayName: 'beta' }) + ], + errors: [] + } +} + +function progressTick( + rig: ScanRig, + progress: Partial & Pick +): void { + rig.progressCallbacks.at(-1)?.({ + scanId: 'scan-1', + scannedAt: NOW, + scannedWorktreeCount: progress.candidates.length, + totalWorktreeCount: 3, + errors: [], + candidateMode: 'append', + ...progress + }) +} + +describe('WorkspaceCleanupDialog stale-while-revalidate', () => { + beforeEach(() => { + globalThis.IS_REACT_ACT_ENVIRONMENT = true + // Why Date-only fake: relative "Updated Xh ago" labels need a pinned clock, + // but flush() depends on real setTimeout. + vi.useFakeTimers({ toFake: ['Date'], now: NOW }) + const store = createTestStore() + seedStore(store, {}) + holders.store = store as unknown as typeof holders.store + holders.infoToasts = [] + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + }) + + afterEach(async () => { + if (root) { + act(() => root?.unmount()) + } + // Why: broad-scan dedupe lives in slice module state; an unresolved scan + // left by one case would silently join into the next case's scan call. + for (const resolve of holders.rig?.resolvers ?? []) { + resolve({ scannedAt: NOW, candidates: [], errors: [] }) + } + holders.rig = null + await new Promise((resolve) => setTimeout(resolve, 0)) + container?.remove() + root = null + container = null + resetWorkspaceCleanupBrowsePersistTimer() + vi.useRealTimers() + }) + + it('renders the cached snapshot immediately and kicks one background rescan', async () => { + const rig = installApi(cachedFleet()) + await renderDialog() + await openDialog() + + // Full list from cache while the rescan is still streaming (unresolved). + expect(rowNames()).toEqual(expect.arrayContaining(['alpha', 'beta'])) + expect(rig.scan).toHaveBeenCalledTimes(1) + expect(container?.textContent).toContain('Updated 2h ago') + expect(container?.textContent).toContain('Refreshing') + }) + + it('reconciles streamed rows into the cached list without clearing it', async () => { + const rig = installApi(cachedFleet()) + await renderDialog() + await openDialog() + + progressTick(rig, { + candidates: [ + makeCandidate({ + worktreeId: 'repo1::/tmp/alpha', + displayName: 'alpha', + fingerprint: 'fingerprint-alpha-2' + }), + makeCandidate({ worktreeId: 'repo1::/tmp/gamma', displayName: 'gamma' }) + ], + scannedWorktreeCount: 2 + }) + await flush() + + // beta was not re-reported yet; it must still be on screen mid-refresh. + expect(rowNames()).toEqual(expect.arrayContaining(['alpha', 'beta', 'gamma'])) + }) + + it('keeps search and selection interactive while rows stream in', async () => { + const rig = installApi(cachedFleet()) + await renderDialog() + await openDialog() + + const search = container?.querySelector('[aria-label="Search workspaces"]') + expect(search?.disabled).toBe(false) + const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set + await act(async () => { + setter?.call(search, 'beta') + search?.dispatchEvent(new Event('input', { bubbles: true })) + }) + progressTick(rig, { + candidates: [makeCandidate({ worktreeId: 'repo1::/tmp/gamma', displayName: 'gamma' })], + scannedWorktreeCount: 1 + }) + await flush() + + // Filter applied mid-stream: only beta visible even as new rows arrive. + expect(rowNames()).toEqual(['beta']) + + await act(async () => { + setter?.call(search, '') + search?.dispatchEvent(new Event('input', { bubbles: true })) + }) + await flush() + expect(rowNames()).toEqual(expect.arrayContaining(['alpha', 'beta', 'gamma'])) + + // Rows stay selectable during the stream. + const checkbox = rowCheckbox('alpha') + expect(checkbox).not.toBeNull() + const before = checkbox?.getAttribute('aria-checked') + await act(async () => { + checkbox?.click() + }) + expect(rowCheckbox('alpha')?.getAttribute('aria-checked')).not.toBe(before) + }) + + it('does not clear selection while free-text search hides the row', async () => { + installApi(cachedFleet()) + await renderDialog() + await openDialog() + + if (rowCheckbox('alpha')?.getAttribute('aria-checked') !== 'true') { + await act(async () => rowCheckbox('alpha')?.click()) + } + expect(rowCheckbox('alpha')?.getAttribute('aria-checked')).toBe('true') + const search = container?.querySelector('[aria-label="Search workspaces"]') + const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set + await act(async () => { + setter?.call(search, 'beta') + search?.dispatchEvent(new Event('input', { bubbles: true })) + }) + await flush() + expect(rowNames()).toEqual(['beta']) + + await act(async () => { + setter?.call(search, '') + search?.dispatchEvent(new Event('input', { bubbles: true })) + }) + await flush() + expect(rowCheckbox('alpha')?.getAttribute('aria-checked')).toBe('true') + }) + + it('adopts the in-flight scan on reopen instead of starting a duplicate', async () => { + const rig = installApi(cachedFleet()) + await renderDialog() + await openDialog() + expect(rig.scan).toHaveBeenCalledTimes(1) + + progressTick(rig, { + candidates: [makeCandidate({ worktreeId: 'repo1::/tmp/gamma', displayName: 'gamma' })], + scannedWorktreeCount: 1 + }) + await flush() + + // Close at 40% and reopen: the same scan keeps streaming into the store. + await act(async () => { + holders.store.setState({ activeModal: null }) + }) + await openDialog() + expect(rig.scan).toHaveBeenCalledTimes(1) + + progressTick(rig, { + candidates: [makeCandidate({ worktreeId: 'repo1::/tmp/delta', displayName: 'delta' })], + scannedWorktreeCount: 2 + }) + await flush() + await act(async () => { + rig.resolvers.at(-1)?.({ + scannedAt: NOW, + candidates: [ + ...cachedFleet().candidates, + makeCandidate({ worktreeId: 'repo1::/tmp/gamma', displayName: 'gamma' }), + makeCandidate({ worktreeId: 'repo1::/tmp/delta', displayName: 'delta' }) + ], + errors: [] + }) + }) + await flush() + + expect(rig.scan).toHaveBeenCalledTimes(1) + expect(rowNames()).toEqual(expect.arrayContaining(['alpha', 'beta', 'gamma', 'delta'])) + }) + + it('keeps selections across a refresh and surfaces vanished selections', async () => { + const rig = installApi(cachedFleet()) + await renderDialog() + await openDialog() + await act(async () => { + rig.resolvers.at(-1)?.(cachedFleet()) + }) + await flush() + + // Both rows are default-selected suggestions after the first settle. + expect(rowCheckbox('alpha')?.getAttribute('aria-checked')).toBe('true') + expect(rowCheckbox('beta')?.getAttribute('aria-checked')).toBe('true') + + await act(async () => { + container?.querySelector('[aria-label="Refresh"]')?.click() + }) + await flush() + expect(rig.scan).toHaveBeenCalledTimes(2) + // Selection survives while the refresh streams. + expect(rowCheckbox('alpha')?.getAttribute('aria-checked')).toBe('true') + + await act(async () => { + rig.resolvers.at(-1)?.({ + scannedAt: NOW + 1, + candidates: [makeCandidate({ worktreeId: 'repo1::/tmp/alpha', displayName: 'alpha' })], + errors: [] + }) + }) + await flush() + + expect(rowCheckbox('alpha')?.getAttribute('aria-checked')).toBe('true') + expect(rowNames()).not.toContain('beta') + expect(holders.infoToasts).toContain('1 selected workspace no longer exists.') + }) +}) diff --git a/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.tsx b/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.tsx index 586618d521b..e732584f266 100644 --- a/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.tsx +++ b/src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.tsx @@ -1,245 +1,73 @@ -/* eslint-disable max-lines -- Why: the cleanup dialog keeps scan status, - filters, row actions, localized review copy, and force-aware confirmation - in one modal flow. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react' -import { - AlertTriangle, - Info, - Loader2, - RefreshCcw, - Search, - SlidersHorizontal, - Trash2, - X -} from 'lucide-react' +import React, { useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } from 'react' +import { toast } from 'sonner' import { useShallow } from 'zustand/react/shallow' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle -} from '@/components/ui/dialog' -import { Button } from '@/components/ui/button' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuTrigger -} from '@/components/ui/dropdown-menu' -import { Input } from '@/components/ui/input' -import { Progress } from '@/components/ui/progress' -import RepoMultiCombobox from '@/components/ui/repo-multi-combobox' +import { Dialog, DialogContent } from '@/components/ui/dialog' import { ScrollArea } from '@/components/ui/scroll-area' -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { useMountedRef } from '@/hooks/useMountedRef' import { useAppStore } from '@/store' -import { cn } from '@/lib/utils' import { activateAndRevealWorktree } from '@/lib/worktree-activation' -import { isGitRepoKind } from '../../../../shared/repo-kind' +import { translate } from '@/i18n/i18n' import { canQueueWorkspaceCleanupCandidate, - type WorkspaceCleanupCandidate, - type WorkspaceCleanupScanError, - type WorkspaceCleanupScanProgress + type WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' +import type { WorkspaceCleanupDeletionPhase } from './workspace-cleanup-candidate-row' +import { WorkspaceCleanupBrowseToolbar } from './workspace-cleanup-browse-toolbar' +import { WorkspaceCleanupConfirmRemove } from './workspace-cleanup-confirm-remove' +import { WorkspaceCleanupDialogHeader } from './workspace-cleanup-dialog-header' import { - filterWorkspaceCleanupCandidates, - getWorkspaceCleanupReviewInfo, - sortWorkspaceCleanupCandidates, - type WorkspaceCleanupContextFilter, - type WorkspaceCleanupFilters, - type WorkspaceCleanupGitFilter, - type WorkspaceCleanupReviewFilter, - type WorkspaceCleanupReviewInfo, - type WorkspaceCleanupSortDirection, - type WorkspaceCleanupSortKey, - type WorkspaceCleanupTimeFilter -} from './workspace-cleanup-presentation' -import type { WorkspaceCleanupRemovalProgress } from './workspace-cleanup-background-removal' -import { countEstimatedInactiveWorkspaces } from './inactive-workspace-estimate' -import { CandidateRow, type WorkspaceCleanupDeletionPhase } from './workspace-cleanup-candidate-row' -import { WorkspaceCleanupCandidateList } from './workspace-cleanup-candidate-list' + WorkspaceCleanupInitialScanBanner, + WorkspaceCleanupNotice, + WorkspaceCleanupSkeletonRows +} from './workspace-cleanup-dialog-notices' +import { WorkspaceCleanupRowList } from './workspace-cleanup-row-list' +import { formatWorkspaceCleanupScanNotice } from './workspace-cleanup-scan-notice' import { - getCandidateStatus, - getContextPillLabel, - getDirtyGitLabel, - getReviewPillTone, - shouldShowGitMetadataChip -} from './workspace-cleanup-candidate-row-data' -import { StatusPill } from './workspace-cleanup-status-pill' + applyWorkspaceCleanupGitEvidence, + needsWorkspaceCleanupGitEvidence +} from './workspace-cleanup-git-evidence' +import { useWorkspaceCleanupBrowseState } from './use-workspace-cleanup-browse-state' import { - resolveWorkspaceCleanupActiveView, - type WorkspaceCleanupView, - type WorkspaceCleanupViewCounts -} from './workspace-cleanup-view-selection' + useWorkspaceCleanupDialogLifecycle, + type WorkspaceCleanupDialogLifecycle +} from './use-workspace-cleanup-dialog-lifecycle' +import { useWorkspaceCleanupFacetRows } from './use-workspace-cleanup-facet-rows' +import { useWorkspaceCleanupGitEvidence } from './use-workspace-cleanup-git-evidence' +import { useWorkspaceCleanupRowOrder } from './use-workspace-cleanup-row-order' import { - DEFAULT_WORKSPACE_CLEANUP_FILTERS, - useWorkspaceCleanupDialogSession, - type WorkspaceCleanupDialogSession -} from './workspace-cleanup-dialog-session' -import { translate } from '@/i18n/i18n' - -const WORKSPACE_CLEANUP_CLOSE_LINGER_MS = 300 - -const EMPTY_REVIEW_INFO: WorkspaceCleanupReviewInfo = { - hasReview: false, - label: null, - state: null, - provider: null, - title: null -} - -function formatRelativeTime(timestamp: number): string { - if (!timestamp) { - return 'Never' - } - const deltaMs = Date.now() - timestamp - if (deltaMs < 60_000) { - return 'Just now' - } - const minutes = Math.floor(deltaMs / 60_000) - if (minutes < 60) { - return `${minutes}m ago` - } - const hours = Math.floor(minutes / 60) - if (hours < 48) { - return `${hours}h ago` - } - return `${Math.floor(hours / 24)}d ago` -} - -function isDisconnectedRemoteScanError(message: string): boolean { - return ( - message === 'SSH provider is unavailable.' || - message === 'Remote workspaces are not connected. Reconnect and refresh to check them.' - ) -} - -function formatScanNoticeMessage( - errors: WorkspaceCleanupScanError[], - repoNameById: Map -): string | null { - const visibleErrors = errors.filter( - (error) => !isDisconnectedRemoteScanError(error.message ?? '') - ) - if (visibleErrors.length === 0) { - return null - } - if (visibleErrors.length === 1) { - const error = visibleErrors[0] - const repoName = formatScanErrorRepoName(error, repoNameById) - return `Could not check ${repoName}: ${formatScanErrorReason(error.message)}. Some inactive workspaces may be missing. Refresh to try again.` - } - const repoNames = visibleErrors - .slice(0, 3) - .map((error) => formatScanErrorRepoName(error, repoNameById)) - .join(', ') - const moreCount = visibleErrors.length - 3 - const suffix = moreCount > 0 ? `, +${moreCount} more` : '' - return `Could not check ${visibleErrors.length} repositories (${repoNames}${suffix}). Some inactive workspaces may be missing. Refresh to try again.` -} - -function formatScanErrorRepoName( - error: Partial, - repoNameById: Map -): string { - const repoName = error.repoName?.trim() - if (repoName) { - return repoName - } - const fallback = error.repoId ? repoNameById.get(error.repoId)?.trim() : '' - return fallback || 'a repository' -} - -function formatScanErrorReason(message: string | undefined): string { - if (!message) { - return 'Git could not list worktrees' - } - if (message === 'Could not scan workspace cleanup for this repository.') { - return 'Git could not list worktrees' - } - return message.replace(/\.$/, '') -} + formatVanishedSelectionNotice, + getDefaultSelectedWorkspaceCleanupIds, + toggleSetMember +} from './workspace-cleanup-selection-model' +/** One filterable list of every workspace Orca knows about. */ export default function WorkspaceCleanupDialog(): React.JSX.Element | null { - // Why: scans and removals outlive the modal; only the subscribed projection tree may unmount. - const session = useWorkspaceCleanupDialogSession() - const [lingering, setLingering] = useState(session.open) - useEffect(() => { - if (session.open) { - setLingering(true) - return - } - const timer = window.setTimeout(() => setLingering(false), WORKSPACE_CLEANUP_CLOSE_LINGER_MS) - return () => window.clearTimeout(timer) - }, [session.open]) - - if (!session.open && !lingering) { + const lifecycle = useWorkspaceCleanupDialogLifecycle() + if (!lifecycle.mountedContent) { return null } - return + return } function WorkspaceCleanupDialogContent({ - session -}: { - session: WorkspaceCleanupDialogSession -}): React.JSX.Element { - const { - open, - loading, - selectedIds, - setSelectedIds, - expandedRowIds, - setExpandedRowIds, - activeView, - setActiveView, - confirming, - confirmCandidates, - removalProgress, - removalInFlight, - rowFailures, - repoSelection, - setRepoSelection, - filters, - setFilters, - sortKey, - setSortKey, - sortDirection, - setSortDirection, - selectedDefaultsScanAtRef, - removalInFlightRef, - close: closeModal, - markCandidateViewed, - restoreDismissals: resetDismissals, - startScan: startWorkspaceCleanupScan, - ignoreCandidate, - applyScanDefaults, - openConfirmRemove, - cancelConfirmRemove, - backToWorkspaceCleanupList, - confirmRemove - } = session + open, + loading, + closeModal, + selectedIds, + setSelectedIds, + removal, + startWorkspaceCleanupScan +}: WorkspaceCleanupDialogLifecycle): React.JSX.Element { const scan = useAppStore((s) => s.workspaceCleanupScan) const scanProgress = useAppStore((s) => s.workspaceCleanupProgress) const error = useAppStore((s) => s.workspaceCleanupError) + const spaceError = useAppStore((s) => s.workspaceSpaceScanError) const repos = useAppStore((s) => s.repos) - const reviewStateInputs = useAppStore( - useShallow((s) => ({ - worktreesByRepo: s.worktreesByRepo, - hostedReviewCache: s.hostedReviewCache, - repos: s.repos, - settings: s.settings - })) - ) + const markCandidateViewed = useAppStore((s) => s.markWorkspaceCleanupCandidateViewed) + const dismissCandidates = useAppStore((s) => s.dismissWorkspaceCleanupCandidates) + const refreshWorkspaceSpace = useAppStore((s) => s.refreshWorkspaceSpace) + const workspaceSpaceScanning = useAppStore((s) => s.workspaceSpaceScanning) + const workspaceSpaceProgress = useAppStore((s) => s.workspaceSpaceScanProgress) const deletionPhaseByWorktreeId = useAppStore( useShallow((s) => { const phases: Record = {} @@ -256,154 +84,94 @@ function WorkspaceCleanupDialogContent({ [deletionPhaseByWorktreeId] ) + const browse = useWorkspaceCleanupBrowseState() + // Why: facet counts/options are only rendered inside the filter popover; + // tracking its open state lets their O(N) passes skip while it is closed. + const [facetPanelOpen, setFacetPanelOpen] = useState(false) + const [expandedRowIds, setExpandedRowIds] = useState>(() => new Set()) const [rowsScrollElement, setRowsScrollElement] = useState(null) - const eligibleRepos = useMemo(() => repos.filter((repo) => isGitRepoKind(repo)), [repos]) - const eligibleRepoIds = useMemo(() => eligibleRepos.map((repo) => repo.id), [eligibleRepos]) + const selectedDefaultsScanAtRef = useRef(null) + const wasOpenRef = useRef(open) + // Why: a refresh completing mid-open must not clobber a selection the user + // already made (or was given) for this open — defaults apply at most once. + const defaultsAppliedForOpenRef = useRef(false) + const selectionTouchedRef = useRef(false) + const mountedRef = useMountedRef() + + // Why: the facet clock must be stable across renders but never older than + // this open — a hydrated snapshot's scannedAt can be days stale, which would + // misbucket idle thresholds and keep dead agent statuses "fresh". + const [openedAt, setOpenedAt] = useState(() => Date.now()) useEffect(() => { - if (!open) { - return + if (open && !wasOpenRef.current) { + defaultsAppliedForOpenRef.current = false + selectionTouchedRef.current = false + setOpenedAt(Date.now()) } - setRepoSelection(new Set(eligibleRepoIds)) - }, [eligibleRepoIds, open, setRepoSelection]) + wasOpenRef.current = open + }, [open]) + + const { removalInFlightRef } = removal const candidates = useMemo(() => scan?.candidates ?? [], [scan?.candidates]) - const reviewInfoByWorktreeId = useMemo(() => { - const infos = new Map() - for (const candidate of candidates) { - infos.set(candidate.worktreeId, getWorkspaceCleanupReviewInfo(candidate, reviewStateInputs)) - } - return infos - }, [candidates, reviewStateInputs]) - const effectiveRepoSelection = useMemo>(() => { - if (repoSelection.size > 0 || eligibleRepoIds.length === 0) { - return repoSelection - } - return new Set(eligibleRepoIds) - }, [eligibleRepoIds, repoSelection]) - const selectedScanErrors = useMemo( - () => (scan?.errors ?? []).filter((error) => effectiveRepoSelection.has(error.repoId)), - [effectiveRepoSelection, scan?.errors] - ) - const filteredCandidates = useMemo(() => { - if ( - effectiveRepoSelection.size === 0 || - effectiveRepoSelection.size === eligibleRepoIds.length - ) { - return candidates - } - return candidates.filter((candidate) => effectiveRepoSelection.has(candidate.repoId)) - }, [candidates, effectiveRepoSelection, eligibleRepoIds.length]) - // Why: the Resource Manager button counts from the renderer's activity record only, - // so it routinely disagrees with the scanned list. Reconcile it rather than leaving - // the user to wonder which number is real. - const estimatedInactiveCount = useMemo(() => { - if (!open) { - return null - } - return countEstimatedInactiveWorkspaces( - Object.values(reviewStateInputs.worktreesByRepo).flat(), - new Map(reviewStateInputs.repos.map((repo) => [repo.id, repo])), - Date.now() - ) - }, [open, reviewStateInputs]) - // Why: the reconciliation only holds for a complete scan; a failed or partial - // one would blame the difference on git-history checks instead of the error. - const estimateMismatchNotice = - !loading && - !error && - selectedScanErrors.length === 0 && - scan && - estimatedInactiveCount !== null && - estimatedInactiveCount !== candidates.length && - filteredCandidates.length === candidates.length - ? translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.f637f63882', - "Resource Manager counts {{value0}}; this list found {{value1}}. That counter reads Orca's activity record alone, while this scan also checks each workspace's git history and skips disconnected remotes.", - { value0: estimatedInactiveCount, value1: candidates.length } - ) - : null - - useEffect(() => { - if (loading || !scan || selectedDefaultsScanAtRef.current === scan.scannedAt) { - return - } - selectedDefaultsScanAtRef.current = scan.scannedAt - applyScanDefaults(scan.candidates, deletingWorktreeIds) - }, [applyScanDefaults, deletingWorktreeIds, loading, scan, selectedDefaultsScanAtRef]) - - const visibleCandidates = useMemo(() => { - const rows = filteredCandidates.filter((candidate) => !candidate.blockers.includes('dismissed')) - return sortWorkspaceCleanupCandidates(rows, 'activity', 'asc', reviewInfoByWorktreeId) - }, [filteredCandidates, reviewInfoByWorktreeId]) - const hiddenCandidates = useMemo( - () => - sortWorkspaceCleanupCandidates( - filteredCandidates.filter((candidate) => candidate.blockers.includes('dismissed')), - 'activity', - 'asc', - reviewInfoByWorktreeId - ), - [filteredCandidates, reviewInfoByWorktreeId] - ) - const groups = useMemo( - () => ({ - ready: visibleCandidates.filter((candidate) => candidate.tier === 'ready'), - review: visibleCandidates.filter((candidate) => candidate.tier === 'review'), - protected: visibleCandidates.filter((candidate) => candidate.tier === 'protected') - }), - [visibleCandidates] - ) - const cleanupViewCounts = useMemo( - () => ({ - ready: groups.ready.length, - review: groups.review.length, - protected: groups.protected.length, - hidden: hiddenCandidates.length - }), - [groups.protected.length, groups.ready.length, groups.review.length, hiddenCandidates.length] - ) - const resolvedActiveView = resolveWorkspaceCleanupActiveView({ - requestedView: activeView, - counts: cleanupViewCounts, - open, - loading, - hasScan: scan != null + const gitEvidenceNeeded = open && needsWorkspaceCleanupGitEvidence(browse.filters, browse.sort) + const gitEvidence = useWorkspaceCleanupGitEvidence({ + enabled: gitEvidenceNeeded, + candidates, + scannedAt: scan?.scannedAt ?? null }) - const repoNameById = useMemo( - () => new Map(repos.map((repo) => [repo.id, repo.displayName || repo.path])), - [repos] + const evidencedCandidates = useMemo( + () => applyWorkspaceCleanupGitEvidence(candidates, gitEvidence.evidenceByWorktreeId), + [candidates, gitEvidence.evidenceByWorktreeId] ) + // Why: a live clock would re-run every facet on each render; the newer of + // scan time and open time is the honest stable "as of" moment — a fresh scan + // keeps its timestamp, a stale hydrated snapshot is judged from this open. + const facetNow = Math.max(scan?.scannedAt ?? 0, openedAt) + const facetRows = useWorkspaceCleanupFacetRows({ + candidates: evidencedCandidates, + filters: browse.filters, + sort: browse.sort, + now: facetNow, + facetPanelOpen + }) + const rows = useWorkspaceCleanupRowOrder({ + rows: facetRows.rows, + streaming: loading, + sort: browse.sort + }) + const candidateIds = useMemo( + () => new Set(candidates.map((candidate) => candidate.worktreeId)), + [candidates] + ) + const scanNoticeMessage = useMemo( - () => formatScanNoticeMessage(selectedScanErrors, repoNameById), - [repoNameById, selectedScanErrors] - ) - const hasAnyCandidates = candidates.length > 0 - const initialLoading = loading && !hasAnyCandidates - const activeBaseRows = - resolvedActiveView === 'hidden' ? hiddenCandidates : groups[resolvedActiveView] - const activeRows = useMemo( () => - sortWorkspaceCleanupCandidates( - filterWorkspaceCleanupCandidates( - activeBaseRows, - filters, - reviewInfoByWorktreeId, - scan?.scannedAt ?? Date.now() - ), - sortKey, - sortDirection, - reviewInfoByWorktreeId + formatWorkspaceCleanupScanNotice( + scan?.errors ?? [], + new Map(repos.map((repo) => [repo.id, repo.displayName || repo.path])) ), - [activeBaseRows, filters, reviewInfoByWorktreeId, scan?.scannedAt, sortDirection, sortKey] + [repos, scan?.errors] ) - const activeRowIds = useMemo( - () => new Set(activeRows.map((candidate) => candidate.worktreeId)), - [activeRows] - ) - const activeFilters = hasActiveWorkspaceCleanupFilters(filters) + const initialLoading = loading && candidates.length === 0 + const runSpaceScan = useCallback(() => { + void refreshWorkspaceSpace().catch((scanError: unknown) => { + if (!mountedRef.current) { + return + } + toast.error( + translate( + 'components.workspace.cleanup.browse.measureSizesFailed', + 'Could not scan workspace sizes' + ), + { description: scanError instanceof Error ? scanError.message : String(scanError) } + ) + }) + }, [mountedRef, refreshWorkspaceSpace]) + const selectedCandidates = useMemo(() => { - const byId = new Map(activeRows.map((candidate) => [candidate.worktreeId, candidate])) + const byId = new Map(rows.map((row) => [row.worktreeId, row.candidate])) return [...selectedIds] .map((id) => byId.get(id)) .filter( @@ -412,59 +180,140 @@ function WorkspaceCleanupDialogContent({ canQueueWorkspaceCleanupCandidate(candidate) && !deletingWorktreeIds.has(candidate.worktreeId) ) - }, [activeRows, deletingWorktreeIds, selectedIds]) + }, [deletingWorktreeIds, rows, selectedIds]) + const selectedCount = selectedCandidates.length + useEffect(() => { - if (!open || confirming) { + if (loading || !scan || selectedDefaultsScanAtRef.current === scan.scannedAt) { return } - // Why: destructive selection must stay scoped to the rows the user can - // currently review after tier/filter changes. + selectedDefaultsScanAtRef.current = scan.scannedAt + if (removalInFlightRef.current) { + return + } + if (defaultsAppliedForOpenRef.current || selectionTouchedRef.current) { + return + } + defaultsAppliedForOpenRef.current = true + setSelectedIds(getDefaultSelectedWorkspaceCleanupIds(scan.candidates, deletingWorktreeIds)) + }, [deletingWorktreeIds, loading, removalInFlightRef, scan, setSelectedIds]) + + const pruneSelectionToVisibleRows = useEffectEvent(() => { setSelectedIds((current) => { const next = new Set( - [...current].filter((id) => activeRowIds.has(id) && !deletingWorktreeIds.has(id)) + [...current].filter( + (id) => facetRows.facetMatchedWorktreeIds.has(id) && !deletingWorktreeIds.has(id) + ) ) return next.size === current.size ? current : next }) - }, [activeRowIds, confirming, deletingWorktreeIds, open, setSelectedIds]) + }) - const handleOpenChange = useCallback( - (nextOpen: boolean) => { - if (!nextOpen) { - closeModal() - } + useEffect(() => { + if (!open || removal.confirming) { + return + } + // Why: destructive selection must stay scoped to the rows the user can + // currently review after a filter change — and only then; keying on the + // user's filter state (not the matched set, whose identity changes every + // streaming tick) keeps a re-classified row from being silently deselected. + pruneSelectionToVisibleRows() + }, [browse.filters, open, removal.confirming]) + + const pruneVanishedSelections = useEffectEvent(() => { + const kept = [...selectedIds].filter( + (id) => candidateIds.has(id) && !deletingWorktreeIds.has(id) + ) + if (kept.length === selectedIds.size) { + return + } + const vanishedCount = [...selectedIds].filter( + (id) => !candidateIds.has(id) && !deletingWorktreeIds.has(id) + ).length + setSelectedIds(new Set(kept)) + if (vanishedCount > 0) { + toast.info(formatVanishedSelectionNotice(vanishedCount)) + } + }) + + useEffect(() => { + if (loading) { + return + } + // Why: a settled refresh reconciles by worktreeId — selections survive it, + // and only rows that truly no longer exist drop (with a visible reason). + pruneVanishedSelections() + }, [candidateIds, deletingWorktreeIds, loading]) + + const ignoreCandidate = useCallback( + (candidate: WorkspaceCleanupCandidate) => { + void dismissCandidates([candidate]) + .then(() => { + if (mountedRef.current) { + setSelectedIds((current) => { + const next = new Set(current) + next.delete(candidate.worktreeId) + return next + }) + } + }) + .catch((err: unknown) => { + if (mountedRef.current) { + toast.error( + translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.7f451a3e2c', + 'Could not ignore cleanup suggestion' + ), + { description: err instanceof Error ? err.message : String(err) } + ) + } + }) }, - [closeModal] + [dismissCandidates, mountedRef, setSelectedIds] ) - const refresh = useCallback(() => { - startWorkspaceCleanupScan({ notifyWhenReady: true }) - }, [startWorkspaceCleanupScan]) - - const toggleExpandedRow = useCallback( - (worktreeId: string) => { - setExpandedRowIds((current) => toggleSetMember(current, worktreeId)) - }, - [setExpandedRowIds] - ) + const toggleExpandedRow = useCallback((worktreeId: string) => { + setExpandedRowIds((current) => toggleSetMember(current, worktreeId)) + }, []) const toggleSelectedRow = useCallback( (worktreeId: string) => { + selectionTouchedRef.current = true setSelectedIds((current) => toggleSetMember(current, worktreeId)) }, [setSelectedIds] ) + const selectableWorktreeIds = useMemo( + () => + removal.removalInFlight + ? [] + : facetRows.selectableWorktreeIds.filter((id) => !deletingWorktreeIds.has(id)), + [deletingWorktreeIds, facetRows.selectableWorktreeIds, removal.removalInFlight] + ) + const toggleSelectAll = useCallback( + (selectAll: boolean) => { + selectionTouchedRef.current = true + setSelectedIds(selectAll ? new Set(selectableWorktreeIds) : new Set()) + }, + [selectableWorktreeIds, setSelectedIds] + ) + // Why: stable per-row handlers so React.memo keeps unchanged CandidateRow // instances from re-rendering on scan stream-in and selection changes. + const openConfirmRemove = removal.openConfirmRemove const handleRemoveRow = useCallback( (candidate: WorkspaceCleanupCandidate) => { - if (loading || removalInFlightRef.current) { + // Why: rows stay actionable while a rescan streams; the confirm-time + // preflight is the safety gate, not the scan state. + if (removalInFlightRef.current) { return } + selectionTouchedRef.current = true setSelectedIds(new Set([candidate.worktreeId])) openConfirmRemove([candidate]) }, - [loading, openConfirmRemove, removalInFlightRef, setSelectedIds] + [openConfirmRemove, removalInFlightRef, setSelectedIds] ) const handleViewCandidate = useCallback( @@ -476,911 +325,102 @@ function WorkspaceCleanupDialogContent({ [closeModal, markCandidateViewed] ) - const selectedCount = selectedCandidates.length + const handleOpenChange = useCallback( + (nextOpen: boolean) => { + if (!nextOpen) { + closeModal() + } + }, + [closeModal] + ) return ( - {!confirming ? ( + {!removal.confirming ? ( <> - -
-
- - {translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.b2c1331844', - 'Delete Inactive Workspaces' - )} - -
-
- - - - - - {translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.7ae2ad30f4', - 'Refresh' - )} - - - -
-
-
+ openConfirmRemove(selectedCandidates)} + onRefresh={() => startWorkspaceCleanupScan({ notifyWhenReady: true })} + onClose={closeModal} + /> - {initialLoading ? ( -
- -
-
- {translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.7eee951968', - 'Checking inactive workspaces' - )} -
-
- {translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.47123d0108', - 'Scanning inactive workspaces. You can close this and come back.' - )} -
-
- {formatWorkspaceCleanupProgress(scanProgress)} -
-
-
- ) : hasAnyCandidates ? ( -
-
-
- {selectedCount}{' '} - {translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.ac5ba84cc1', - 'selected' - )} -
-
-
- {eligibleRepos.length > 1 ? ( -
- setRepoSelection(new Set(next))} - onSelectAll={() => setRepoSelection(new Set(eligibleRepoIds))} - triggerClassName="h-8 w-full rounded-md border border-border/60 bg-background px-2 text-xs font-medium shadow-xs hover:bg-accent/60" - /> -
- ) : null} - -
-
+ {initialLoading ? : null} + {error ? : null} + {!error && spaceError ? ( + + ) : null} + {!error && scanNoticeMessage ? ( + ) : null} - {loading && scan && hasAnyCandidates ? ( -
-
- - - {translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.9a3be9f2df', - 'Scanning inactive workspaces. New rows appear here as they finish. You can close this and come back.' - )} - - - {formatWorkspaceCleanupProgress(scanProgress)} - -
-
- ) : null} + - {error ? ( -
- {error} + +
+ {initialLoading ? : null} +
- ) : scanNoticeMessage ? ( -
- - {scanNoticeMessage} -
- ) : null} - - {estimateMismatchNotice ? ( -
- - {estimateMismatchNotice} -
- ) : null} - -
- -
- {filteredCandidates.length > 0 ? ( - 0 - } - sortKey={sortKey} - sortDirection={sortDirection} - onFiltersChange={setFilters} - onSortKeyChange={setSortKey} - onSortDirectionChange={setSortDirection} - onRestoreIgnored={() => void resetDismissals()} - /> - ) : null} - -
- {initialLoading ? : null} - {!loading && scan && candidates.length === 0 && !scanNoticeMessage ? ( - - ) : null} - {!loading && scan && candidates.length === 0 && scanNoticeMessage ? ( - - ) : null} - {!loading && - scan && - candidates.length > 0 && - filteredCandidates.length === 0 ? ( - setRepoSelection(new Set(eligibleRepoIds))} - /> - ) : null} - {!loading && - scan && - filteredCandidates.length > 0 && - visibleCandidates.length === 0 ? ( - setActiveView('hidden')} - /> - ) : null} - {!loading && - scan && - activeRows.length === 0 && - activeBaseRows.length > 0 && - activeFilters ? ( - setFilters(DEFAULT_WORKSPACE_CLEANUP_FILTERS)} - /> - ) : null} - {!loading && - scan && - activeRows.length === 0 && - visibleCandidates.length > 0 && - !activeFilters ? ( - - ) : null} - ( - 1 && index === activeRows.length - 1} - expanded={expandedRowIds.has(candidate.worktreeId)} - lastActivityLabel={formatRelativeTime(candidate.lastActivityAt)} - deletionPhase={deletionPhaseByWorktreeId[candidate.worktreeId]} - removing={ - loading || - removalInFlight || - deletingWorktreeIds.has(candidate.worktreeId) - } - selected={ - selectedIds.has(candidate.worktreeId) && - !loading && - !deletingWorktreeIds.has(candidate.worktreeId) - } - failure={rowFailures[candidate.worktreeId]} - onToggleExpanded={toggleExpandedRow} - onToggleSelected={toggleSelectedRow} - onView={handleViewCandidate} - onIgnore={ignoreCandidate} - onRemove={handleRemoveRow} - /> - )} - /> -
-
-
-
+
) : ( - )}
) } - -function WorkspaceCleanupFilterToolbar({ - filters, - showRestoreIgnored, - sortKey, - sortDirection, - onFiltersChange, - onSortKeyChange, - onSortDirectionChange, - onRestoreIgnored -}: { - filters: WorkspaceCleanupFilters - showRestoreIgnored: boolean - sortKey: WorkspaceCleanupSortKey - sortDirection: WorkspaceCleanupSortDirection - onFiltersChange: (filters: WorkspaceCleanupFilters) => void - onSortKeyChange: (sortKey: WorkspaceCleanupSortKey) => void - onSortDirectionChange: (direction: WorkspaceCleanupSortDirection) => void - onRestoreIgnored: () => void -}): React.JSX.Element { - const updateFilter = ( - key: K, - value: WorkspaceCleanupFilters[K] - ): void => { - onFiltersChange({ ...filters, [key]: value }) - } - const hasHiddenControls = hasActiveWorkspaceCleanupPanelControls(filters, sortKey, sortDirection) - const resetPanelControls = (): void => { - onFiltersChange({ - ...filters, - time: 'all', - review: 'all', - git: 'all', - context: 'all' - }) - onSortKeyChange('activity') - onSortDirectionChange('asc') - } - - return ( -
-
- - updateFilter('query', event.target.value)} - placeholder={translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.searchPlaceholder', - 'Search workspaces' - )} - className="h-8 pl-8 text-xs" - /> -
- - - - - - - - - {translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.efb3843e75', - 'Filter and sort workspaces' - )} - - - - - {translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.93b7381d50', - 'Filters' - )} - - - label={translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.ageFilter', - 'Age' - )} - value={filters.time} - options={[ - ['all', 'Any age'], - ['30d', '30d+'], - ['90d', '90d+'], - ['archived', 'Archived'] - ]} - onChange={(value) => updateFilter('time', value)} - /> - - label={translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.reviewFilter', - 'Review' - )} - value={filters.review} - options={[ - ['all', 'Any review'], - ['no-review', 'No PR/MR'], - ['has-review', 'Has PR/MR'], - ['open-review', 'Open'], - ['closed-review', 'Closed'] - ]} - onChange={(value) => updateFilter('review', value)} - /> - - label={translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.gitFilter', - 'Git' - )} - value={filters.git} - options={[ - ['all', 'Any git'], - ['clean', 'Clean'], - ['dirty', 'Dirty'], - ['unpushed', 'Unpushed'], - ['unknown', 'Unknown'] - ]} - onChange={(value) => updateFilter('git', value)} - /> - - label={translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.contextFilter', - 'Context' - )} - value={filters.context} - options={[ - ['all', 'Any context'], - ['has-context', 'Has context'], - ['no-context', 'No context'] - ]} - onChange={(value) => updateFilter('context', value)} - /> - - - {translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.a615e24679', - 'Sort' - )} - - - label={translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.sortBy', - 'Sort by' - )} - value={sortKey} - options={[ - ['activity', 'Activity'], - ['name', 'Name'], - ['repo', 'Repo'], - ['review', 'Review'], - ['git', 'Git'] - ]} - onChange={onSortKeyChange} - /> - - label={translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.sortDirection', - 'Direction' - )} - value={sortDirection} - options={[ - ['asc', 'Ascending'], - ['desc', 'Descending'] - ]} - onChange={onSortDirectionChange} - /> - {showRestoreIgnored ? ( - <> - - - {translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.aaee139eab', - 'Restore ignored suggestions' - )} - - - ) : null} - {hasHiddenControls ? ( - <> - - - {translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.e94b1f8bb4', - 'Clear filters' - )} - - - ) : null} - - -
- ) -} - -function WorkspaceCleanupMenuSub({ - label, - value, - options, - onChange -}: { - label: string - value: T - options: readonly (readonly [T, string])[] - onChange: (value: T) => void -}): React.JSX.Element { - const valueLabel = options.find(([optionValue]) => optionValue === value)?.[1] ?? value - return ( - - - - {label} - - {valueLabel} - - - - - onChange(next as T)}> - {options.map(([optionValue, optionLabel]) => ( - event.preventDefault()} - > - {optionLabel} - - ))} - - - - ) -} - -function CleanupViewNav({ - activeView, - counts, - onViewChange -}: { - activeView: WorkspaceCleanupView - counts: WorkspaceCleanupViewCounts - onViewChange: (view: WorkspaceCleanupView) => void -}): React.JSX.Element { - const items: { view: WorkspaceCleanupView; label: string }[] = [ - { - view: 'ready', - label: translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.4b93a235d8', - 'Suggested' - ) - }, - { - view: 'review', - label: translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.d1094dd529', - 'Needs review' - ) - }, - { - view: 'protected', - label: translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.c4f4782c02', - 'Not suggested' - ) - }, - { - view: 'hidden', - label: translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.e8b3741ff7', - 'Ignored' - ) - } - ] - - return ( - - ) -} - -function ConfirmRemove({ - candidates, - reviewInfoByWorktreeId, - progress, - onBack, - onCancel, - onConfirm -}: { - candidates: WorkspaceCleanupCandidate[] - reviewInfoByWorktreeId: ReadonlyMap - progress: WorkspaceCleanupRemovalProgress | null - onBack: () => void - onCancel: () => void - onConfirm: () => void -}): React.JSX.Element { - const count = candidates.length - const deleting = progress !== null - const progressValue = progress - ? Math.min(100, Math.max(0, (progress.processedCount / progress.totalCount) * 100)) - : 0 - return ( - <> - -
-
-
- {deleting ? ( - - ) : ( - - )} -
-
- - {deleting - ? translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.deletingCount', - 'Deleting workspaces: {{value0}}', - { value0: count } - ) - : translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.deleteCount', - 'Delete workspaces: {{value0}}?', - { value0: count } - )} - - - {deleting - ? translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.1d3503357d', - 'You can close this and come back while deletion continues.' - ) - : translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.38ca0b1400', - "This permanently deletes their local files. You can't undo this." - )} - -
-
- -
-
-
- {progress ? ( -
-
- - - {formatWorkspaceCleanupRemovalProgress(progress)} - -
- -
- ) : null} -
-
- {translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.selectedForDeletionCount', - 'Selected for deletion: {{value0}}', - { value0: count } - )} -
-
- {translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.592fbab446', - 'Sorted by oldest activity' - )} -
-
- - {candidates.map((candidate, index) => ( - - ))} - -
- - - {!deleting ? ( - - ) : null} - - - ) -} - -function ConfirmRemoveRow({ - candidate, - reviewInfo, - last -}: { - candidate: WorkspaceCleanupCandidate - reviewInfo: WorkspaceCleanupReviewInfo - last: boolean -}): React.JSX.Element { - const dirtyLabel = getDirtyGitLabel(candidate) - const branchDiffersFromName = candidate.branch !== candidate.displayName - const contextPillLabel = getContextPillLabel(candidate) - const showGitMetadataChip = shouldShowGitMetadataChip(candidate) - const status = getCandidateStatus(candidate) - return ( -
-
- {candidate.displayName} - - {translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.352f15d6fc', - 'Last active' - )}{' '} - {formatRelativeTime(candidate.lastActivityAt)} - - {status.label} - {reviewInfo.label ? ( - {reviewInfo.label} - ) : null} - {contextPillLabel ? {contextPillLabel} : null} - {dirtyLabel && showGitMetadataChip ? ( - {dirtyLabel} - ) : null} -
-
- {candidate.repoName} - {branchDiffersFromName ? ( - <> - - {candidate.branch} - - ) : null} -
-
- {candidate.path} -
-
- ) -} - -function hasActiveWorkspaceCleanupFilters(filters: WorkspaceCleanupFilters): boolean { - return ( - filters.query.trim() !== '' || - filters.time !== 'all' || - filters.review !== 'all' || - filters.git !== 'all' || - filters.context !== 'all' - ) -} - -function hasActiveWorkspaceCleanupPanelControls( - filters: WorkspaceCleanupFilters, - sortKey: WorkspaceCleanupSortKey, - sortDirection: WorkspaceCleanupSortDirection -): boolean { - return ( - filters.time !== 'all' || - filters.review !== 'all' || - filters.git !== 'all' || - filters.context !== 'all' || - sortKey !== 'activity' || - sortDirection !== 'asc' - ) -} - -function formatWorkspaceCleanupRemovalProgress(progress: WorkspaceCleanupRemovalProgress): string { - const deletedText = translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.4c2990886e', - '{{value0}}/{{value1}} deleted', - { - value0: progress.removedCount, - value1: progress.totalCount - } - ) - if (progress.failedCount === 0) { - return deletedText - } - return translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.86ba852118', - '{{value0}}, {{value1}} failed', - { - value0: deletedText, - value1: progress.failedCount - } - ) -} - -function formatWorkspaceCleanupProgress(progress: WorkspaceCleanupScanProgress | null): string { - if (!progress || progress.scannedWorktreeCount === 0) { - return translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.4cc5b73efe', - 'Finding inactive workspaces...' - ) - } - return translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.7b7bde5181', - 'Checked workspaces so far: {{value0}}', - { - value0: progress.scannedWorktreeCount - } - ) -} - -function SkeletonRows(): React.JSX.Element { - return ( -
- {[0, 1, 2].map((index) => ( -
- ))} -
- ) -} - -function EmptyState({ - title, - actionLabel, - onAction -}: { - title: string - actionLabel?: string - onAction?: () => void -}): React.JSX.Element { - return ( -
- {title} - {actionLabel && onAction ? ( - - ) : null} -
- ) -} - -function toggleSetMember(current: Set, value: string): Set { - const next = new Set(current) - if (next.has(value)) { - next.delete(value) - } else { - next.add(value) - } - return next -} diff --git a/src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.test.ts b/src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.test.ts deleted file mode 100644 index 0cb2b0329d0..00000000000 --- a/src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { Repo } from '../../../../shared/repo-types' -import type { Worktree } from '../../../../shared/worktree/types' -import { countEstimatedInactiveWorkspaces } from './inactive-workspace-estimate' - -const NOW = 1_800_000_000_000 -const DAY_MS = 24 * 60 * 60 * 1000 - -const GIT_REPO = { id: 'repo-1', path: '/repo', displayName: 'Repo' } as Repo -const FOLDER_REPO = { - id: 'folder-1', - path: '/folder', - displayName: 'Folder', - kind: 'folder' -} as Repo - -const REPOS = new Map([ - [GIT_REPO.id, GIT_REPO], - [FOLDER_REPO.id, FOLDER_REPO] -]) - -function makeWorktree(overrides: Partial = {}): Worktree { - return { - id: 'repo-1::/repo-feature', - repoId: 'repo-1', - path: '/repo-feature', - isMainWorktree: false, - isArchived: false, - lastActivityAt: NOW - 90 * DAY_MS, - ...overrides - } as Worktree -} - -describe('countEstimatedInactiveWorkspaces', () => { - it('counts workspaces idle past the cleanup threshold', () => { - expect(countEstimatedInactiveWorkspaces([makeWorktree()], REPOS, NOW)).toBe(1) - }) - - it('does not count a missing activity stamp as ancient', () => { - const worktrees = [ - makeWorktree({ lastActivityAt: 0 }), - makeWorktree({ lastActivityAt: Number.NaN }) - ] - - expect(countEstimatedInactiveWorkspaces(worktrees, REPOS, NOW)).toBe(0) - }) - - it('falls back to the creation stamp when activity was never recorded', () => { - const recent = makeWorktree({ lastActivityAt: 0, createdAt: NOW - DAY_MS }) - const old = makeWorktree({ lastActivityAt: 0, createdAt: NOW - 90 * DAY_MS }) - - expect(countEstimatedInactiveWorkspaces([recent, old], REPOS, NOW)).toBe(1) - }) - - it('skips main worktrees, folder workspaces, and unknown repos', () => { - const worktrees = [ - makeWorktree({ isMainWorktree: true }), - makeWorktree({ repoId: FOLDER_REPO.id }), - makeWorktree({ repoId: 'gone' }) - ] - - expect(countEstimatedInactiveWorkspaces(worktrees, REPOS, NOW)).toBe(0) - }) - - it('uses the shorter archived threshold', () => { - const worktree = makeWorktree({ isArchived: true, lastActivityAt: NOW - 10 * DAY_MS }) - - expect(countEstimatedInactiveWorkspaces([worktree], REPOS, NOW)).toBe(1) - }) -}) diff --git a/src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.ts b/src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.ts deleted file mode 100644 index 23524b5e904..00000000000 --- a/src/renderer/src/components/workspace-cleanup/inactive-workspace-estimate.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Repo } from '../../../../shared/repo-types' -import type { Worktree } from '../../../../shared/worktree/types' -import { isFolderRepo } from '../../../../shared/repo-kind' -import { - getPersistedWorkspaceCleanupActivityAt, - isWorkspaceOldForCleanup -} from '../../../../shared/workspace-cleanup' - -/** - * Renderer-side estimate of how many workspaces the cleanup scan would list. It - * sees only Orca's persisted activity record, so it disagrees with the scan by - * design — the scan additionally reads each worktree's git history and cannot - * inspect disconnected remotes. - */ -export function countEstimatedInactiveWorkspaces( - worktrees: readonly Worktree[], - repoById: ReadonlyMap, - now: number -): number { - let count = 0 - for (const worktree of worktrees) { - const repo = repoById.get(worktree.repoId) - if (!repo || isFolderRepo(repo) || worktree.isMainWorktree) { - continue - } - // Why: an unstamped workspace (externally created, or never opened here) reads as - // epoch 0, which counted every one of them as idle and inflated the estimate far - // past what the scan lists. Unknown is not old. - const lastActivityAt = getPersistedWorkspaceCleanupActivityAt(worktree) - if ( - lastActivityAt > 0 && - isWorkspaceOldForCleanup({ isArchived: worktree.isArchived, lastActivityAt }, now) - ) { - count += 1 - } - } - return count -} diff --git a/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-browse-state.ts b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-browse-state.ts new file mode 100644 index 00000000000..948928b9a93 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-browse-state.ts @@ -0,0 +1,68 @@ +import { useCallback } from 'react' +import { useAppStore } from '@/store' +import { + createDefaultWorkspaceCleanupFilterState, + type WorkspaceCleanupFilterState, + type WorkspaceCleanupSortField, + type WorkspaceCleanupSortState +} from '../../../../shared/workspace-cleanup-filter-model' + +export type WorkspaceCleanupBrowseController = { + filters: WorkspaceCleanupFilterState + sort: WorkspaceCleanupSortState + patchFilters: ( + key: K, + value: Partial | WorkspaceCleanupFilterState[K] + ) => void + toggleSortField: (field: WorkspaceCleanupSortField) => void + clearFilters: () => void +} + +/** + * Single seam over the persisted browse slice so reopening the dialog keeps the + * user's filters and sort. + */ +export function useWorkspaceCleanupBrowseState(): WorkspaceCleanupBrowseController { + const browse = useAppStore((s) => s.workspaceCleanupBrowse) + const updateBrowse = useAppStore((s) => s.updateWorkspaceCleanupBrowseState) + + const patchFilters = useCallback( + (key, value) => { + const current = browse.filters[key] + const next = + typeof current === 'object' && current !== null + ? { ...current, ...(value as object) } + : value + // Cast: a computed key over a union widens the spread result past + // WorkspaceCleanupFilterState even though `key` is constrained to it. + const filters = { ...browse.filters, [key]: next } as WorkspaceCleanupFilterState + updateBrowse({ ...browse, filters }) + }, + [browse, updateBrowse] + ) + + // Why: re-picking the active sort flips its direction. + const toggleSortField = useCallback( + (field: WorkspaceCleanupSortField) => { + const direction = + browse.sort.field === field && browse.sort.direction === 'asc' ? 'desc' : 'asc' + updateBrowse({ ...browse, sort: { field, direction } }) + }, + [browse, updateBrowse] + ) + + const clearFilters = useCallback(() => { + updateBrowse({ + ...browse, + filters: createDefaultWorkspaceCleanupFilterState() + }) + }, [browse, updateBrowse]) + + return { + filters: browse.filters, + sort: browse.sort, + patchFilters, + toggleSortField, + clearFilters + } +} diff --git a/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-dialog-lifecycle.ts b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-dialog-lifecycle.ts new file mode 100644 index 00000000000..194ed95240b --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-dialog-lifecycle.ts @@ -0,0 +1,83 @@ +import { useCallback, useEffect, useState, type Dispatch, type SetStateAction } from 'react' +import { useAppStore } from '@/store' +import { + useWorkspaceCleanupRemoval, + type WorkspaceCleanupRemovalController +} from './use-workspace-cleanup-removal' +import { useWorkspaceCleanupScanLifecycle } from './use-workspace-cleanup-scan-lifecycle' + +const WORKSPACE_CLEANUP_CLOSE_LINGER_MS = 300 + +export type WorkspaceCleanupDialogLifecycle = { + open: boolean + mountedContent: boolean + loading: boolean + closeModal: () => void + selectedIds: Set + setSelectedIds: Dispatch>> + removal: WorkspaceCleanupRemovalController + startWorkspaceCleanupScan: (options?: { notifyWhenReady?: boolean }) => void +} + +/** Keeps scan/removal ownership mounted while the closed dialog drops its projection tree. */ +export function useWorkspaceCleanupDialogLifecycle(): WorkspaceCleanupDialogLifecycle { + const activeModal = useAppStore((s) => s.activeModal) + const closeModal = useAppStore((s) => s.closeModal) + const loading = useAppStore((s) => s.workspaceCleanupLoading) + const open = activeModal === 'workspace-cleanup' + const [selectedIds, setSelectedIds] = useState>(() => new Set()) + + const deselectRemovedIds = useCallback((removedIds: readonly string[]) => { + if (removedIds.length === 0) { + return + } + setSelectedIds((current) => { + const next = new Set(current) + for (const id of removedIds) { + next.delete(id) + } + return next + }) + }, []) + const removal = useWorkspaceCleanupRemoval({ onDeselect: deselectRemovedIds, closeModal }) + const { removalInFlightRef, resetForReopen, resetRowFailures } = removal + + const onFreshOpen = useCallback(() => { + if (!removalInFlightRef.current) { + resetForReopen() + setSelectedIds(new Set()) + } + }, [removalInFlightRef, resetForReopen]) + + const { startWorkspaceCleanupScan } = useWorkspaceCleanupScanLifecycle({ + open, + loading, + removalInFlight: removal.removalInFlight, + removalInFlightRef, + resetRowFailures, + onFreshOpen + }) + const [mountedContent, setMountedContent] = useState(open) + useEffect(() => { + if (open) { + setMountedContent(true) + return + } + const timer = window.setTimeout( + () => setMountedContent(false), + WORKSPACE_CLEANUP_CLOSE_LINGER_MS + ) + return () => window.clearTimeout(timer) + }, [open]) + + return { + open, + mountedContent, + loading, + closeModal, + selectedIds, + setSelectedIds, + removal, + startWorkspaceCleanupScan + } +} diff --git a/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-facet-rows.test.tsx b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-facet-rows.test.tsx new file mode 100644 index 00000000000..413493216ad --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-facet-rows.test.tsx @@ -0,0 +1,298 @@ +// @vitest-environment happy-dom +import { renderHook } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { AppState } from '@/store/types' +import { + createDefaultWorkspaceCleanupFilterState, + DEFAULT_WORKSPACE_CLEANUP_SORT +} from '../../../../shared/workspace-cleanup-filter-model' +import { cloneDefaultWorkspaceStatuses } from '../../../../shared/workspace-statuses' +import type * as FacetModule from './workspace-cleanup-facets' +import type * as QueryModule from './workspace-cleanup-query' +import { makeFacetCandidate } from './workspace-cleanup-facet.test.fixture' + +const holders = vi.hoisted(() => ({ state: null as AppState | null })) +const counts = vi.hoisted(() => ({ facetCounts: 0, measured: 0, queries: 0 })) + +vi.mock('@/store', () => ({ + useAppStore: (selector: (state: AppState) => T): T => { + if (!holders.state) { + throw new Error('Missing test state') + } + return selector(holders.state) + } +})) + +vi.mock('./workspace-cleanup-facets', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + countWorkspaceCleanupMeasuredRows: ( + ...args: Parameters + ) => { + counts.measured += 1 + return actual.countWorkspaceCleanupMeasuredRows(...args) + } + } +}) + +vi.mock('./workspace-cleanup-query', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + countWorkspaceCleanupFacetMatches: ( + ...args: Parameters + ) => { + counts.facetCounts += 1 + return actual.countWorkspaceCleanupFacetMatches(...args) + }, + runWorkspaceCleanupQuery: (...args: Parameters) => { + counts.queries += 1 + return actual.runWorkspaceCleanupQuery(...args) + } + } +}) + +import { useWorkspaceCleanupFacetRows } from './use-workspace-cleanup-facet-rows' + +function makeState(): AppState { + return { + worktreesByRepo: {}, + hostedReviewCache: {}, + repos: [], + settings: {}, + workspaceStatuses: cloneDefaultWorkspaceStatuses(), + lastVisitedAtByWorktreeId: {}, + agentStatusByPaneKey: {}, + tabsByWorktree: {}, + workspaceCleanupDismissals: {}, + workspaceSpaceAnalysis: null, + workspaceSpaceMeasurements: [] + } as unknown as AppState +} + +describe('useWorkspaceCleanupFacetRows hot paths', () => { + beforeEach(() => { + holders.state = makeState() + counts.facetCounts = 0 + counts.measured = 0 + counts.queries = 0 + }) + + it('does only the query work when the user types', () => { + const candidates = [makeFacetCandidate()] + const filters = createDefaultWorkspaceCleanupFilterState() + const view = renderHook( + ({ currentFilters }) => + useWorkspaceCleanupFacetRows({ + candidates, + filters: currentFilters, + sort: DEFAULT_WORKSPACE_CLEANUP_SORT, + now: 1_700_000_000_000 + }), + { initialProps: { currentFilters: filters } } + ) + const initialCounts = { ...counts } + const reviewIndex = view.result.current.reviewInfoByWorktreeId + + view.rerender({ currentFilters: { ...filters, query: 'alpha' } }) + + expect(counts.queries).toBeGreaterThan(initialCounts.queries) + expect(counts.facetCounts).toBe(initialCounts.facetCounts) + expect(counts.measured).toBe(initialCounts.measured) + expect(view.result.current.reviewInfoByWorktreeId).toBe(reviewIndex) + }) + + it('keeps review joins stable during unrelated agent-status churn', () => { + const candidates = [makeFacetCandidate()] + const filters = createDefaultWorkspaceCleanupFilterState() + const view = renderHook(() => + useWorkspaceCleanupFacetRows({ + candidates, + filters, + sort: DEFAULT_WORKSPACE_CLEANUP_SORT, + now: 1_700_000_000_000 + }) + ) + const reviewIndex = view.result.current.reviewInfoByWorktreeId + + holders.state = { ...holders.state!, agentStatusByPaneKey: {} } + view.rerender() + + expect(view.result.current.reviewInfoByWorktreeId).toBe(reviewIndex) + }) + + it('does not rebuild facets for count-only size progress', () => { + const candidates = [makeFacetCandidate()] + const filters = createDefaultWorkspaceCleanupFilterState() + const view = renderHook(() => + useWorkspaceCleanupFacetRows({ + candidates, + filters, + sort: DEFAULT_WORKSPACE_CLEANUP_SORT, + now: 1_700_000_000_000 + }) + ) + const initialCounts = { ...counts } + + holders.state = { + ...holders.state!, + workspaceSpaceScanProgress: { + scanId: 'scan-1', + state: 'running', + startedAt: 1, + updatedAt: 2, + totalRepoCount: 1, + scannedRepoCount: 0, + totalWorktreeCount: 100, + scannedWorktreeCount: 20, + currentRepoDisplayName: 'Repo', + currentWorktreeDisplayName: 'alpha' + } + } + view.rerender() + + expect(counts).toEqual(initialCounts) + }) + + it('projects streamed size measurements before the full scan completes', () => { + const candidates = [makeFacetCandidate()] + const filters = createDefaultWorkspaceCleanupFilterState() + const view = renderHook(() => + useWorkspaceCleanupFacetRows({ + candidates, + filters, + sort: DEFAULT_WORKSPACE_CLEANUP_SORT, + now: 1_700_000_000_000 + }) + ) + expect(view.result.current.rows[0]?.sizeBytes).toBeNull() + + holders.state = { + ...holders.state!, + workspaceSpaceMeasurements: [ + { worktreeId: candidates[0]!.worktreeId, status: 'ok', sizeBytes: 4_096 } + ] + } + view.rerender() + + expect(view.result.current.rows[0]?.sizeBytes).toBe(4_096) + expect(view.result.current.measuredSizeCount).toBe(1) + }) + + it('includes folder repositories discovered from cleanup candidates', () => { + const candidate = makeFacetCandidate({ repoId: 'folder-1', repoName: 'Loose files' }) + const view = renderHook(() => + useWorkspaceCleanupFacetRows({ + candidates: [candidate], + filters: createDefaultWorkspaceCleanupFilterState(), + sort: DEFAULT_WORKSPACE_CLEANUP_SORT, + now: 1_700_000_000_000 + }) + ) + + expect(view.result.current.options.repos).toEqual([{ id: 'folder-1', label: 'Loose files' }]) + }) + + it('skips every downstream pass when a streaming tick changes no candidate', () => { + const candidates = [makeFacetCandidate(), makeFacetCandidate({ worktreeId: 'repo-1::/b' })] + const filters = createDefaultWorkspaceCleanupFilterState() + const view = renderHook( + ({ current }) => + useWorkspaceCleanupFacetRows({ + candidates: current, + filters, + sort: DEFAULT_WORKSPACE_CLEANUP_SORT, + now: 1_700_000_000_000 + }), + { initialProps: { current: candidates } } + ) + const initialCounts = { ...counts } + const matched = view.result.current.facetMatchedWorktreeIds + const rows = view.result.current.rows + + // A no-op progress tick delivers a fresh array of the same candidate objects. + view.rerender({ current: [...candidates] }) + + expect(counts).toEqual(initialCounts) + expect(view.result.current.rows).toBe(rows) + expect(view.result.current.facetMatchedWorktreeIds).toBe(matched) + }) + + it('keeps facet identity for untouched rows when a tick replaces one candidate', () => { + const stable = makeFacetCandidate() + const replaced = makeFacetCandidate({ worktreeId: 'repo-1::/b' }) + const filters = createDefaultWorkspaceCleanupFilterState() + const view = renderHook( + ({ current }) => + useWorkspaceCleanupFacetRows({ + candidates: current, + filters, + sort: DEFAULT_WORKSPACE_CLEANUP_SORT, + now: 1_700_000_000_000 + }), + { initialProps: { current: [stable, replaced] } } + ) + const stableFacet = view.result.current.rows.find((row) => row.worktreeId === stable.worktreeId) + + view.rerender({ + current: [stable, makeFacetCandidate({ worktreeId: 'repo-1::/b', displayName: 'renamed' })] + }) + + expect(view.result.current.rows.find((row) => row.worktreeId === stable.worktreeId)).toBe( + stableFacet + ) + expect( + view.result.current.rows.find((row) => row.worktreeId === 'repo-1::/b')?.displayName + ).toBe('renamed') + }) + + it('skips facet count and option passes while the filter panel is closed', () => { + const view = renderHook(() => + useWorkspaceCleanupFacetRows({ + candidates: [makeFacetCandidate()], + filters: createDefaultWorkspaceCleanupFilterState(), + sort: DEFAULT_WORKSPACE_CLEANUP_SORT, + now: 1_700_000_000_000, + facetPanelOpen: false + }) + ) + + expect(counts.facetCounts).toBe(0) + expect(view.result.current.options.repos).toEqual([]) + }) + + it('projects same-id streamed sizes only onto their owning hosts', () => { + const worktreeId = 'repo-1::/repo/alpha' + const candidates = [ + makeFacetCandidate({ worktreeId, displayName: 'local', executionHostId: 'local' }), + makeFacetCandidate({ + worktreeId, + displayName: 'remote', + connectionId: 'builder', + executionHostId: 'ssh:builder' + }) + ] + holders.state = { + ...holders.state!, + workspaceSpaceMeasurements: [ + { worktreeId, executionHostId: 'ssh:builder', status: 'ok', sizeBytes: 4_096 } + ] + } + const view = renderHook(() => + useWorkspaceCleanupFacetRows({ + candidates, + filters: createDefaultWorkspaceCleanupFilterState(), + sort: DEFAULT_WORKSPACE_CLEANUP_SORT, + now: 1_700_000_000_000 + }) + ) + + expect( + view.result.current.rows.find((row) => row.displayName === 'local')?.sizeBytes + ).toBeNull() + expect(view.result.current.rows.find((row) => row.displayName === 'remote')?.sizeBytes).toBe( + 4_096 + ) + }) +}) diff --git a/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-facet-rows.ts b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-facet-rows.ts new file mode 100644 index 00000000000..b957f715eaa --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-facet-rows.ts @@ -0,0 +1,289 @@ +import { useMemo } from 'react' +import { useShallow } from 'zustand/react/shallow' +import { useAppStore } from '@/store' +import { getLiveAgentStatusByWorktreeId } from '@/lib/worktree-activity-state' +import type { HostedReviewProvider } from '../../../../shared/hosted-review' +import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' +import type { + WorkspaceCleanupFilterState, + WorkspaceCleanupSortState +} from '../../../../shared/workspace-cleanup-filter-model' +import { + countWorkspaceCleanupMeasuredRows, + type WorkspaceCleanupFacets +} from './workspace-cleanup-facets' +import { + countWorkspaceCleanupFacetMatches, + filterWorkspaceCleanupFacets, + runWorkspaceCleanupQuery +} from './workspace-cleanup-query' +import { + buildWorkspaceCleanupReviewLookup, + type WorkspaceCleanupReviewInfo +} from './workspace-cleanup-presentation' +import type { + WorkspaceCleanupFacetCounts, + WorkspaceCleanupFacetOptions +} from './workspace-cleanup-facet-panel-model' +import { + buildWorkspaceCleanupSizeIndex, + buildWorkspaceCleanupWorktreeIndex, + countWorkspaceCleanupCandidateIds +} from './workspace-cleanup-host-identity' +import { + computeWorkspaceCleanupFacetList, + computeWorkspaceCleanupReviewInfoIndex, + createWorkspaceCleanupFacetListCache, + createWorkspaceCleanupReviewInfoCache +} from './workspace-cleanup-facet-row-caches' + +export type WorkspaceCleanupFacetRows = { + rows: WorkspaceCleanupFacets[] + selectableWorktreeIds: string[] + facetMatchedWorktreeIds: ReadonlySet + matchedCount: number + totalCount: number + facetCounts: WorkspaceCleanupFacetCounts + options: WorkspaceCleanupFacetOptions + reviewInfoByWorktreeId: ReadonlyMap + sizeByWorktreeId: ReadonlyMap + /** 0 means the workspace-space scan has never produced a usable size. */ + measuredSizeCount: number + unmeasuredSizeCount: number +} + +const EMPTY_FACET_COUNTS: WorkspaceCleanupFacetCounts = Object.freeze({ + activity: 0, + size: 0, + status: 0, + agent: 0, + git: 0, + review: 0, + ticket: 0, + context: 0, + location: 0, + safety: 0 +}) + +const EMPTY_FACET_OPTIONS: WorkspaceCleanupFacetOptions = Object.freeze({ + workspaceStatuses: Object.freeze([]), + hostIds: Object.freeze([]), + repos: Object.freeze([]), + reviewProviders: Object.freeze([]) +}) + +/** + * Joins the scan rows with everything the flat list filters and sorts on: + * renderer-only signals (visits, live agents, review cache, dismissals) plus + * sizes from the EXISTING workspace-space scan — no second scanner. + * + * Per-candidate work is cached on candidate object identity (see + * workspace-cleanup-facet-row-caches) so streaming ticks touch only changed + * rows and no-op ticks skip every downstream pass. + */ +export function useWorkspaceCleanupFacetRows({ + candidates, + filters, + sort, + now, + facetPanelOpen = true +}: { + candidates: readonly WorkspaceCleanupCandidate[] + filters: WorkspaceCleanupFilterState + sort: WorkspaceCleanupSortState + now: number + /** Facet counts/options are only rendered inside the filter popover; pass + * false while it is closed to skip their O(N) passes entirely. */ + facetPanelOpen?: boolean +}): WorkspaceCleanupFacetRows { + const sources = useAppStore( + useShallow((s) => ({ + worktreesByRepo: s.worktreesByRepo, + hostedReviewCache: s.hostedReviewCache, + repos: s.repos, + settings: s.settings, + // Statuses are a top-level UI-slice field, not part of GlobalSettings. + workspaceStatuses: s.workspaceStatuses, + lastVisitedAtByWorktreeId: s.lastVisitedAtByWorktreeId, + agentStatusByPaneKey: s.agentStatusByPaneKey, + tabsByWorktree: s.tabsByWorktree, + dismissals: s.workspaceCleanupDismissals, + spaceWorktrees: s.workspaceSpaceAnalysis?.worktrees ?? null, + spaceMeasurements: s.workspaceSpaceMeasurements + })) + ) + const { hostedReviewCache, repos, settings, worktreesByRepo } = sources + const candidateIdCounts = useMemo( + () => countWorkspaceCleanupCandidateIds(candidates), + [candidates] + ) + // Why: each per-candidate cache lives in one memo together with the derived + // context it is keyed on — the memo deps ARE the cache invalidation, no refs + // (ref writes during render are unsafe under concurrent rendering), and + // interior fills are content-addressed and idempotent. + const reviewContext = useMemo( + () => ({ + sources: { hostedReviewCache, repos, settings, worktreesByRepo }, + lookup: buildWorkspaceCleanupReviewLookup({ repos, worktreesByRepo }), + cache: createWorkspaceCleanupReviewInfoCache() + }), + [hostedReviewCache, repos, settings, worktreesByRepo] + ) + const facetContext = useMemo( + () => ({ + worktreeById: buildWorkspaceCleanupWorktreeIndex(worktreesByRepo, repos), + workspaceStatuses: sources.workspaceStatuses, + cache: createWorkspaceCleanupFacetListCache() + }), + [repos, sources.workspaceStatuses, worktreesByRepo] + ) + const liveAgentStatusByWorktreeId = useMemo( + () => getLiveAgentStatusByWorktreeId(sources.agentStatusByPaneKey, sources.tabsByWorktree, now), + [now, sources.agentStatusByPaneKey, sources.tabsByWorktree] + ) + const dismissedWorktreeIds = useMemo( + () => new Set(Object.keys(sources.dismissals)), + [sources.dismissals] + ) + + const reviewInfoByWorktreeId = useMemo( + () => + computeWorkspaceCleanupReviewInfoIndex({ + candidates, + candidateIdCounts, + reviewSources: reviewContext.sources, + reviewLookup: reviewContext.lookup, + cache: reviewContext.cache + }), + [candidateIdCounts, candidates, reviewContext] + ) + + const completedSizeByWorktreeId = useMemo( + () => buildWorkspaceCleanupSizeIndex(sources.spaceWorktrees, candidates), + [candidates, sources.spaceWorktrees] + ) + const sizeByWorktreeId = useMemo(() => { + if (sources.spaceMeasurements.length === 0) { + return completedSizeByWorktreeId + } + return new Map([ + ...completedSizeByWorktreeId, + ...buildWorkspaceCleanupSizeIndex(sources.spaceMeasurements, candidates) + ]) + }, [candidates, completedSizeByWorktreeId, sources.spaceMeasurements]) + + const facets = useMemo( + () => + computeWorkspaceCleanupFacetList({ + candidates, + sources: { + worktreeById: facetContext.worktreeById, + workspaceStatuses: facetContext.workspaceStatuses, + sizeBytesByWorktreeId: sizeByWorktreeId, + lastVisitedAtByWorktreeId: sources.lastVisitedAtByWorktreeId, + liveAgentStatusByWorktreeId, + reviewInfoByWorktreeId, + dismissedWorktreeIds + }, + cache: facetContext.cache + }), + [ + candidates, + dismissedWorktreeIds, + facetContext, + liveAgentStatusByWorktreeId, + reviewInfoByWorktreeId, + sizeByWorktreeId, + sources.lastVisitedAtByWorktreeId + ] + ) + + const result = useMemo( + () => runWorkspaceCleanupQuery(facets, { filters, sort }, now), + [facets, filters, now, sort] + ) + const facetFilters = useMemo( + () => ({ + query: '', + activity: filters.activity, + size: filters.size, + status: filters.status, + agent: filters.agent, + git: filters.git, + review: filters.review, + ticket: filters.ticket, + context: filters.context, + location: filters.location, + safety: filters.safety + }), + [ + filters.activity, + filters.agent, + filters.context, + filters.git, + filters.location, + filters.review, + filters.safety, + filters.size, + filters.status, + filters.ticket + ] + ) + const facetCounts = useMemo( + () => + facetPanelOpen + ? countWorkspaceCleanupFacetMatches(facets, facetFilters, now) + : EMPTY_FACET_COUNTS, + [facetFilters, facetPanelOpen, facets, now] + ) + // Identity churn here is harmless: the only consumer reads the latest set + // inside a useEffectEvent body and never keys an effect on it. + const facetMatchedWorktreeIds = useMemo>( + () => + new Set(filterWorkspaceCleanupFacets(facets, facetFilters, now).map((row) => row.worktreeId)), + [facetFilters, facets, now] + ) + const measuredSizeCount = useMemo(() => countWorkspaceCleanupMeasuredRows(facets), [facets]) + const unmeasuredSizeCount = facets.length - measuredSizeCount + + const options = useMemo(() => { + if (!facetPanelOpen) { + return EMPTY_FACET_OPTIONS + } + const repoLabels = new Map() + for (const row of facets) { + if (!repoLabels.has(row.repoId)) { + repoLabels.set(row.repoId, row.repoName) + } + } + return { + workspaceStatuses: sources.workspaceStatuses.map((status) => ({ + id: status.id, + label: status.label + })), + hostIds: [...new Set(facets.map((row) => row.hostId))].sort(), + repos: [...repoLabels].map(([id, label]) => ({ id, label })), + reviewProviders: [ + ...new Set( + facets + .map((row) => row.review.provider) + .filter((provider): provider is HostedReviewProvider => provider !== null) + ) + ].sort() + } + }, [facetPanelOpen, facets, sources.workspaceStatuses]) + + return { + rows: result.rows, + selectableWorktreeIds: result.selectableWorktreeIds, + facetMatchedWorktreeIds, + matchedCount: result.matchedCount, + totalCount: result.totalCount, + facetCounts, + options, + reviewInfoByWorktreeId, + sizeByWorktreeId, + measuredSizeCount, + unmeasuredSizeCount + } +} diff --git a/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.test.tsx b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.test.tsx new file mode 100644 index 00000000000..c4cf958bbcf --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.test.tsx @@ -0,0 +1,192 @@ +// @vitest-environment happy-dom + +import { act, renderHook, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { + WorkspaceCleanupScanProgress, + WorkspaceCleanupScanResult +} from '../../../../shared/workspace-cleanup' +import { makeFacetCandidate } from './workspace-cleanup-facet.test.fixture' +import { useWorkspaceCleanupGitEvidence } from './use-workspace-cleanup-git-evidence' + +const holders = vi.hoisted(() => ({ cancelScan: vi.fn(), scan: vi.fn() })) + +type PendingScan = { + worktreeIds: string[] + onProgress?: (progress: WorkspaceCleanupScanProgress) => void + resolve: (result: WorkspaceCleanupScanResult) => void +} + +function deferredCandidate(worktreeId: string) { + return makeFacetCandidate({ + worktreeId, + git: { clean: null, upstreamAhead: null, upstreamBehind: null, checkedAt: null } + }) +} + +describe('useWorkspaceCleanupGitEvidence', () => { + let pending: PendingScan[] + + beforeEach(() => { + pending = [] + holders.scan.mockReset() + holders.cancelScan.mockReset().mockResolvedValue(true) + holders.scan.mockImplementation( + ( + { worktreeIds }: { worktreeIds: string[] }, + onProgress?: (progress: WorkspaceCleanupScanProgress) => void + ) => { + return new Promise((resolve) => { + pending.push({ worktreeIds, onProgress, resolve }) + }) + } + ) + ;(window as unknown as { api: unknown }).api = { + workspaceCleanup: { + cancelScan: holders.cancelScan, + scan: holders.scan + } + } + }) + + it('bounds restarted passes and isolates them from stale settlements', async () => { + const candidates = ['a', 'b', 'c', 'd', 'e', 'f'].map(deferredCandidate) + const view = renderHook( + ({ enabled }: { enabled: boolean }) => + useWorkspaceCleanupGitEvidence({ enabled, candidates, scannedAt: 1 }), + { initialProps: { enabled: true } } + ) + + await waitFor(() => expect(view.result.current.totalCount).toBe(6)) + expect(view.result.current.pendingWorktreeIds.size).toBe(6) + expect(holders.scan).toHaveBeenCalledTimes(1) + expect(holders.scan).toHaveBeenCalledWith( + { + worktreeIds: ['a', 'b', 'c', 'd', 'e', 'f'], + scanId: expect.any(String) + }, + expect.any(Function) + ) + + view.rerender({ enabled: false }) + await waitFor(() => expect(view.result.current.pendingWorktreeIds.size).toBe(0)) + expect(holders.cancelScan).toHaveBeenCalledWith(expect.any(String)) + view.rerender({ enabled: true }) + await waitFor(() => expect(view.result.current.pendingWorktreeIds.size).toBe(6)) + expect(holders.scan).toHaveBeenCalledTimes(1) + expect(view.result.current.pendingWorktreeIds.size).toBe(6) + + await act(async () => { + pending[0]?.resolve({ + scannedAt: 2, + candidates: [makeFacetCandidate({ worktreeId: 'a' })], + errors: [] + }) + }) + await waitFor(() => expect(holders.scan).toHaveBeenCalledTimes(2)) + expect(view.result.current.pendingWorktreeIds.size).toBe(6) + expect(view.result.current.evidenceByWorktreeId.size).toBe(0) + + await act(async () => { + pending[1]?.onProgress?.({ + scanId: 'batch-2', + scannedAt: 3, + scannedWorktreeCount: 1, + totalWorktreeCount: 6, + candidates: [makeFacetCandidate({ worktreeId: 'a' })], + errors: [], + candidateMode: 'append' + }) + }) + await waitFor(() => expect(view.result.current.evidenceByWorktreeId.size).toBe(1)) + expect(view.result.current.pendingWorktreeIds.size).toBe(5) + }) + + it('chunks dispatches at the shared target batch limit so no queued id is dropped', async () => { + const first = Array.from({ length: 300 }, (_, i) => deferredCandidate(`a${i}`)) + const grown = [...first, ...Array.from({ length: 600 }, (_, i) => deferredCandidate(`b${i}`))] + const view = renderHook( + ({ candidates }: { candidates: ReturnType[] }) => + useWorkspaceCleanupGitEvidence({ enabled: true, candidates, scannedAt: 1 }), + { initialProps: { candidates: first } } + ) + + await waitFor(() => expect(holders.scan).toHaveBeenCalledTimes(1)) + expect(pending[0]?.worktreeIds).toHaveLength(300) + + // 600 more ids queue while the first request is in flight. + view.rerender({ candidates: grown }) + await waitFor(() => expect(view.result.current.totalCount).toBe(900)) + expect(holders.scan).toHaveBeenCalledTimes(1) + + await act(async () => { + pending[0]?.resolve({ scannedAt: 1, candidates: [], errors: [] }) + }) + await waitFor(() => expect(holders.scan).toHaveBeenCalledTimes(2)) + expect(pending[1]?.worktreeIds).toHaveLength(500) + + await act(async () => { + pending[1]?.resolve({ scannedAt: 1, candidates: [], errors: [] }) + }) + await waitFor(() => expect(holders.scan).toHaveBeenCalledTimes(3)) + expect(pending[2]?.worktreeIds).toHaveLength(100) + + const dispatched = pending.flatMap((request) => request.worktreeIds) + expect(new Set(dispatched).size).toBe(900) + }) + + it('keeps state identity stable when a progress frame changes nothing', async () => { + const candidates = [deferredCandidate('a')] + const view = renderHook(() => + useWorkspaceCleanupGitEvidence({ enabled: true, candidates, scannedAt: 1 }) + ) + await waitFor(() => expect(holders.scan).toHaveBeenCalledTimes(1)) + const before = view.result.current + + await act(async () => { + pending[0]?.onProgress?.({ + scanId: 'noop', + scannedAt: 1, + scannedWorktreeCount: 0, + totalWorktreeCount: 1, + candidates: [makeFacetCandidate({ worktreeId: 'unrelated' })], + errors: [], + candidateMode: 'append' + }) + }) + expect(view.result.current).toBe(before) + }) + + it('restarts evidence collection when the settled scan snapshot changes', async () => { + const candidates = [deferredCandidate('a')] + const view = renderHook( + ({ scannedAt }: { scannedAt: number }) => + useWorkspaceCleanupGitEvidence({ enabled: true, candidates, scannedAt }), + { initialProps: { scannedAt: 1 } } + ) + + await waitFor(() => expect(holders.scan).toHaveBeenCalledTimes(1)) + view.rerender({ scannedAt: 2 }) + expect(holders.scan).toHaveBeenCalledTimes(1) + + await act(async () => { + pending[0]?.resolve({ + scannedAt: 1, + candidates: [makeFacetCandidate({ worktreeId: 'a' })], + errors: [] + }) + }) + await waitFor(() => expect(holders.scan).toHaveBeenCalledTimes(2)) + expect(view.result.current.evidenceByWorktreeId.size).toBe(0) + expect(view.result.current.pendingWorktreeIds).toEqual(new Set(['a'])) + + await act(async () => { + pending[1]?.resolve({ + scannedAt: 2, + candidates: [makeFacetCandidate({ worktreeId: 'a' })], + errors: [] + }) + }) + await waitFor(() => expect(view.result.current.evidenceByWorktreeId.size).toBe(1)) + }) +}) diff --git a/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.ts b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.ts new file mode 100644 index 00000000000..95377c8a5de --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.ts @@ -0,0 +1,183 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { useMountedRef } from '@/hooks/useMountedRef' +import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' +import { + selectWorkspaceCleanupGitEvidenceTargets, + WORKSPACE_CLEANUP_GIT_EVIDENCE_MAX_TARGETS +} from './workspace-cleanup-git-evidence' + +export type WorkspaceCleanupGitEvidenceState = { + /** Focused re-scan results, keyed by worktree id. */ + evidenceByWorktreeId: ReadonlyMap + pendingWorktreeIds: ReadonlySet + checkedCount: number + totalCount: number +} + +const EMPTY_EVIDENCE: WorkspaceCleanupGitEvidenceState = { + evidenceByWorktreeId: new Map(), + pendingWorktreeIds: new Set(), + checkedCount: 0, + totalCount: 0 +} + +/** + * Fills in git evidence the broad scan deferred with one targeted batch scan. + * Only runs while a git-dependent filter or sort is active — the initial list + * render never waits on it, so an unfiltered browse stays instant. + */ +export function useWorkspaceCleanupGitEvidence({ + enabled, + candidates, + scannedAt +}: { + enabled: boolean + candidates: readonly WorkspaceCleanupCandidate[] + scannedAt: number | null +}): WorkspaceCleanupGitEvidenceState { + const mountedRef = useMountedRef() + const [state, setState] = useState(EMPTY_EVIDENCE) + const evidenceRef = useRef(new Map()) + const attemptedRef = useRef(new Set()) + const queueRef = useRef([]) + const queuedRef = useRef(new Set()) + const inFlightRef = useRef(new Set()) + // Why: restarted filters must share one physical RPC/subprocess cap. + const activeRequestWorktreeIdsRef = useRef(new Set()) + const activeScanIdRef = useRef(null) + const totalRef = useRef(0) + const enabledRef = useRef(enabled) + const scannedAtRef = useRef(scannedAt) + // Why: results that land after the user cleared the git filter belong to a + // stale pass and must not resurrect its pending banner. + const generationRef = useRef(0) + + // Why: streaming ticks call publish repeatedly; unchanged evidence must not + // mint new Map/Set identities or every subscriber re-renders per tick. + const publishDirtyRef = useRef(false) + const publish = useCallback(() => { + if (!mountedRef.current || !publishDirtyRef.current) { + return + } + publishDirtyRef.current = false + setState({ + evidenceByWorktreeId: new Map(evidenceRef.current), + pendingWorktreeIds: new Set([...queuedRef.current, ...inFlightRef.current]), + checkedCount: totalRef.current - queuedRef.current.size - inFlightRef.current.size, + totalCount: totalRef.current + }) + }, [mountedRef]) + + const pump = useCallback(() => { + if (activeRequestWorktreeIdsRef.current.size > 0 || queueRef.current.length === 0) { + publish() + return + } + const generation = generationRef.current + // Why: main truncates targeted scans at this shared limit; a larger request + // would silently drop the overflow ids while marking them attempted. + const worktreeIds = queueRef.current.slice(0, WORKSPACE_CLEANUP_GIT_EVIDENCE_MAX_TARGETS) + queueRef.current = queueRef.current.slice(worktreeIds.length) + const scanId = crypto.randomUUID() + activeScanIdRef.current = scanId + for (const worktreeId of worktreeIds) { + queuedRef.current.delete(worktreeId) + inFlightRef.current.add(worktreeId) + activeRequestWorktreeIdsRef.current.add(worktreeId) + attemptedRef.current.add(worktreeId) + } + publishDirtyRef.current = true + const acceptCandidates = (nextCandidates: readonly WorkspaceCleanupCandidate[]): void => { + if (generation !== generationRef.current) { + return + } + for (const candidate of nextCandidates) { + if (!activeRequestWorktreeIdsRef.current.has(candidate.worktreeId)) { + continue + } + evidenceRef.current.set(candidate.worktreeId, candidate) + inFlightRef.current.delete(candidate.worktreeId) + publishDirtyRef.current = true + } + publish() + } + void window.api.workspaceCleanup + .scan({ worktreeIds, scanId }, (progress) => acceptCandidates(progress.candidates)) + .then((result) => acceptCandidates(result.candidates)) + .catch(() => { + // Why: a failed targeted scan leaves rows honestly "Not checked"; + // attemptedRef keeps it from retrying in a loop. + }) + .finally(() => { + if (activeScanIdRef.current === scanId) { + activeScanIdRef.current = null + } + for (const worktreeId of worktreeIds) { + activeRequestWorktreeIdsRef.current.delete(worktreeId) + if (generation === generationRef.current && inFlightRef.current.delete(worktreeId)) { + publishDirtyRef.current = true + } + } + pump() + }) + publish() + }, [publish]) + + useEffect(() => { + const wasEnabled = enabledRef.current + const snapshotChanged = scannedAtRef.current !== scannedAt + enabledRef.current = enabled + scannedAtRef.current = scannedAt + + if ((wasEnabled && !enabled) || (enabled && snapshotChanged)) { + generationRef.current += 1 + const activeScanId = activeScanIdRef.current + if (activeScanId) { + void window.api.workspaceCleanup.cancelScan?.(activeScanId) + } + queueRef.current = [] + queuedRef.current.clear() + inFlightRef.current.clear() + attemptedRef.current.clear() + evidenceRef.current.clear() + totalRef.current = 0 + publishDirtyRef.current = true + publish() + } + if (!enabled) { + return + } + // Why: the exclusion set must cover queued/in-flight ids too — a cap that + // counts them would permanently skip everything past it on a settled scan. + const targets = selectWorkspaceCleanupGitEvidenceTargets(candidates, { + resolvedWorktreeIds: new Set([ + ...attemptedRef.current, + ...queuedRef.current, + ...inFlightRef.current + ]), + maxTargets: Number.POSITIVE_INFINITY + }) + if (targets.length === 0) { + return + } + queueRef.current.push(...targets) + for (const target of targets) { + queuedRef.current.add(target) + } + totalRef.current += targets.length + publishDirtyRef.current = true + pump() + }, [candidates, enabled, pump, publish, scannedAt]) + + useEffect( + () => () => { + const activeScanId = activeScanIdRef.current + if (activeScanId) { + void window.api.workspaceCleanup.cancelScan?.(activeScanId) + } + }, + [] + ) + + return state +} diff --git a/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-removal.ts b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-removal.ts new file mode 100644 index 00000000000..2379be86965 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-removal.ts @@ -0,0 +1,242 @@ +import { useCallback, useRef, useState } from 'react' +import { useAppStore } from '@/store' +import { useMountedRef } from '@/hooks/useMountedRef' +import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' +import { + startWorkspaceCleanupBackgroundRemoval, + type WorkspaceCleanupRemovalProgress +} from './workspace-cleanup-background-removal' +import { filterWorkspaceCleanupRemovalCandidates } from './workspace-cleanup-removal-candidates' + +export type WorkspaceCleanupRemovalController = { + confirming: boolean + confirmCandidates: WorkspaceCleanupCandidate[] + removalProgress: WorkspaceCleanupRemovalProgress | null + removalInFlight: boolean + /** Synchronous guard; state alone lags a second confirm click. */ + removalInFlightRef: { current: boolean } + rowFailures: Record + resetRowFailures: () => void + resetForReopen: () => void + openConfirmRemove: (candidates: readonly WorkspaceCleanupCandidate[]) => void + confirmRemove: () => void + cancelConfirmRemove: () => void + backToList: () => void +} + +/** + * Owns the destructive path: confirm gate, background batch, late settlement, + * and the queued-delete overlay each row shows in the sidebar. + */ +export function useWorkspaceCleanupRemoval({ + onDeselect, + closeModal +}: { + onDeselect: (removedIds: readonly string[]) => void + closeModal: () => void +}): WorkspaceCleanupRemovalController { + const removeCandidates = useAppStore((s) => s.removeWorkspaceCleanupCandidates) + const markWorktreesQueuedForDeletion = useAppStore((s) => s.markWorktreesQueuedForDeletion) + const clearWorktreeDeleteState = useAppStore((s) => s.clearWorktreeDeleteState) + const mountedRef = useMountedRef() + + const [confirming, setConfirming] = useState(false) + const [confirmCandidates, setConfirmCandidates] = useState([]) + const [removalProgress, setRemovalProgress] = useState( + null + ) + // Why: `removalProgress` only arrives once the batch reports, so rendering + // needs its own in-flight flag; removalInFlightRef stays the synchronous guard. + const [removalInFlight, setRemovalInFlight] = useState(false) + const [rowFailures, setRowFailures] = useState>({}) + const removalInFlightRef = useRef(false) + // Why: the dialog stays mounted across cleanup runs, so late settlements from + // an earlier batch must not mutate a newer batch's row state. + const removalBatchIdRef = useRef(0) + + const resetRowFailures = useCallback(() => setRowFailures({}), []) + + const resetForReopen = useCallback(() => { + if (removalInFlightRef.current) { + return + } + setConfirming(false) + setRowFailures({}) + }, []) + + const clearQueuedDeleteState = useCallback( + (worktreeId: string) => { + const deleteState = useAppStore.getState().deleteStateByWorktreeId[worktreeId] + // Why: candidates that fail before removal starts would otherwise stay + // marked "Queued for deletion" in the sidebar; rows already in the + // 'deleting' phase or failed with an error keep their own state. + if (deleteState?.isDeleting && deleteState.error === null && deleteState.phase === 'queued') { + clearWorktreeDeleteState(worktreeId) + } + }, + [clearWorktreeDeleteState] + ) + + const openConfirmRemove = useCallback((candidates: readonly WorkspaceCleanupCandidate[]) => { + const nextCandidates = filterWorkspaceCleanupRemovalCandidates( + candidates, + useAppStore.getState().deleteStateByWorktreeId + ) + if (nextCandidates.length === 0) { + return + } + setConfirmCandidates(nextCandidates) + setConfirming(true) + }, []) + + const cancelConfirmRemove = useCallback(() => { + if (removalProgress) { + closeModal() + return + } + setConfirming(false) + setConfirmCandidates([]) + }, [closeModal, removalProgress]) + + // Why: the header X reads as "leave this screen", not "abandon the dialog". The + // batch keeps running either way and the list shows each row's progress. + // Diverges from cancelConfirmRemove, which closes the dialog mid-batch: here + // removalProgress stays set until the batch settles, so re-entry stays blocked. + const backToList = useCallback(() => { + setConfirming(false) + setConfirmCandidates([]) + }, []) + + const settle = useCallback(() => { + setRemovalProgress(null) + setRemovalInFlight(false) + setConfirming(false) + setConfirmCandidates([]) + }, []) + + const confirmRemove = useCallback(() => { + if (confirmCandidates.length === 0 || removalInFlightRef.current) { + return + } + const removableCandidates = filterWorkspaceCleanupRemovalCandidates( + confirmCandidates, + useAppStore.getState().deleteStateByWorktreeId + ) + if (removableCandidates.length === 0) { + setConfirming(false) + setConfirmCandidates([]) + return + } + removalInFlightRef.current = true + setRemovalInFlight(true) + removalBatchIdRef.current += 1 + const removalBatchId = removalBatchIdRef.current + // Why: a hung late settlement retains these callbacks for the renderer's + // lifetime; capture only ids so it cannot pin the candidate objects. + const removableWorktreeIds = removableCandidates.map((candidate) => candidate.worktreeId) + setRowFailures({}) + markWorktreesQueuedForDeletion(removableWorktreeIds) + const handleRemovalError = (): void => { + for (const worktreeId of removableWorktreeIds) { + clearWorktreeDeleteState(worktreeId) + } + if (mountedRef.current) { + settle() + } + removalInFlightRef.current = false + } + try { + const beginSnapshotPruneBatch = window.api.workspaceCleanup.beginRemovalSnapshotPruneBatch + const recordSnapshotPrune = window.api.workspaceCleanup.recordRemovalSnapshotPrune + const finishSnapshotPruneBatch = window.api.workspaceCleanup.finishRemovalSnapshotPruneBatch + const snapshotPruneBatch = + typeof beginSnapshotPruneBatch === 'function' && + typeof recordSnapshotPrune === 'function' && + typeof finishSnapshotPruneBatch === 'function' + ? (() => { + const batchId = crypto.randomUUID() + return { + batchId, + begin: () => beginSnapshotPruneBatch({ batchId }), + finish: () => finishSnapshotPruneBatch({ batchId }) + } + })() + : undefined + startWorkspaceCleanupBackgroundRemoval({ + candidates: removableCandidates, + removeCandidates, + snapshotPruneBatch, + onProgress: (progress) => { + if (mountedRef.current) { + setRemovalProgress(progress) + } + }, + onRowFailed: (failure) => { + clearQueuedDeleteState(failure.worktreeId) + }, + onResult: (result) => { + const nextFailures: Record = {} + for (const failure of result.failures) { + nextFailures[failure.worktreeId] = failure.message + // Why: defensively covers failures that never reached onRowFailed. + clearQueuedDeleteState(failure.worktreeId) + } + if (mountedRef.current) { + setRowFailures(nextFailures) + onDeselect(result.removedIds) + settle() + } + removalInFlightRef.current = false + }, + onLateResult: (result) => { + for (const failure of result.failures) { + // Why: a late failure can come from a hung preflight whose row never + // reached 'deleting'; clear its queued overlay like every other path. + clearQueuedDeleteState(failure.worktreeId) + } + if (!mountedRef.current || removalBatchIdRef.current !== removalBatchId) { + return + } + setRowFailures((current) => { + const next = { ...current } + for (const id of result.removedIds) { + delete next[id] + } + for (const failure of result.failures) { + next[failure.worktreeId] = failure.message + } + return next + }) + onDeselect(result.removedIds) + }, + onError: handleRemovalError + }) + } catch { + handleRemovalError() + } + }, [ + clearQueuedDeleteState, + clearWorktreeDeleteState, + confirmCandidates, + markWorktreesQueuedForDeletion, + mountedRef, + onDeselect, + removeCandidates, + settle + ]) + + return { + confirming, + confirmCandidates, + removalProgress, + removalInFlight, + removalInFlightRef, + rowFailures, + resetRowFailures, + resetForReopen, + openConfirmRemove, + confirmRemove, + cancelConfirmRemove, + backToList + } +} diff --git a/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-row-order.test.ts b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-row-order.test.ts new file mode 100644 index 00000000000..f2f04547bbc --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-row-order.test.ts @@ -0,0 +1,89 @@ +// @vitest-environment happy-dom + +import { renderHook } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { + arrangeWorkspaceCleanupRowsByFrozenOrder, + createWorkspaceCleanupFrozenRowOrder, + extendWorkspaceCleanupFrozenRowOrder, + useWorkspaceCleanupRowOrder +} from './use-workspace-cleanup-row-order' +import type { WorkspaceCleanupSortState } from '../../../../shared/workspace-cleanup-filter-model' + +type Row = { worktreeId: string } + +const row = (worktreeId: string): Row => ({ worktreeId }) +const ids = (rows: readonly Row[]): string[] => rows.map((entry) => entry.worktreeId) +const ascending: WorkspaceCleanupSortState = { field: 'name', direction: 'asc' } + +describe('workspace cleanup streaming row order', () => { + it('keeps existing rows in their frozen slots when streamed values would resort them', () => { + const order = createWorkspaceCleanupFrozenRowOrder([row('a'), row('b'), row('c')], 'name:asc') + + // The live sort now says c < a < b (values changed mid-stream). + const arranged = arrangeWorkspaceCleanupRowsByFrozenOrder([row('c'), row('a'), row('b')], order) + + expect(ids(arranged)).toEqual(['a', 'b', 'c']) + }) + + it('appends unseen rows after the frozen ones and keeps them stable on later ticks', () => { + const order = createWorkspaceCleanupFrozenRowOrder([row('a'), row('b')], 'name:asc') + + const firstRows = [row('a'), row('d'), row('b'), row('c')] + const firstTick = arrangeWorkspaceCleanupRowsByFrozenOrder(firstRows, order) + expect(ids(firstTick)).toEqual(['a', 'b', 'd', 'c']) + const extendedOrder = extendWorkspaceCleanupFrozenRowOrder(firstRows, order) + + // A later tick sorting d and c differently must not swap them anymore. + const secondTick = arrangeWorkspaceCleanupRowsByFrozenOrder( + [row('c'), row('d'), row('b'), row('a')], + extendedOrder + ) + expect(ids(secondTick)).toEqual(['a', 'b', 'd', 'c']) + expect(order.positions.has('d')).toBe(false) + }) + + it('holds slots for rows a filter temporarily hides', () => { + const order = createWorkspaceCleanupFrozenRowOrder([row('a'), row('b'), row('c')], 'name:asc') + + expect(ids(arrangeWorkspaceCleanupRowsByFrozenOrder([row('c'), row('a')], order))).toEqual([ + 'a', + 'c' + ]) + // Un-hiding b restores its original slot between a and c. + expect( + ids(arrangeWorkspaceCleanupRowsByFrozenOrder([row('c'), row('b'), row('a')], order)) + ).toEqual(['a', 'b', 'c']) + }) + + it('grows the frozen order from empty during a cold-start stream', () => { + const order = createWorkspaceCleanupFrozenRowOrder([], 'name:asc') + const firstRows = [row('b'), row('d')] + + expect(ids(arrangeWorkspaceCleanupRowsByFrozenOrder(firstRows, order))).toEqual(['b', 'd']) + const extendedOrder = extendWorkspaceCleanupFrozenRowOrder(firstRows, order) + // New rows slot after the already-presented ones even when they sort earlier. + expect( + ids( + arrangeWorkspaceCleanupRowsByFrozenOrder( + [row('a'), row('b'), row('c'), row('d')], + extendedOrder + ) + ) + ).toEqual(['b', 'd', 'a', 'c']) + }) + + it('commits appended positions after each rendered stream tick', () => { + const view = renderHook( + ({ rows, streaming }: { rows: readonly Row[]; streaming: boolean }) => + useWorkspaceCleanupRowOrder({ rows, streaming, sort: ascending }), + { initialProps: { rows: [row('a'), row('b')], streaming: false } } + ) + + view.rerender({ rows: [row('a'), row('d'), row('b'), row('c')], streaming: true }) + expect(ids(view.result.current)).toEqual(['a', 'b', 'd', 'c']) + + view.rerender({ rows: [row('c'), row('d'), row('b'), row('a')], streaming: true }) + expect(ids(view.result.current)).toEqual(['a', 'b', 'd', 'c']) + }) +}) diff --git a/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-row-order.ts b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-row-order.ts new file mode 100644 index 00000000000..ad530fa05ad --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-row-order.ts @@ -0,0 +1,123 @@ +import { useEffect, useEffectEvent, useMemo, useRef, useState } from 'react' +import type { WorkspaceCleanupSortState } from '../../../../shared/workspace-cleanup-filter-model' + +type OrderedRow = { worktreeId: string } + +export type WorkspaceCleanupFrozenRowOrder = { + sortSignature: string + positions: Map +} + +const UNCOMMITTED_ROW_ORDER: WorkspaceCleanupFrozenRowOrder = { + sortSignature: '', + positions: new Map() +} + +export function createWorkspaceCleanupFrozenRowOrder( + rows: readonly OrderedRow[], + sortSignature: string +): WorkspaceCleanupFrozenRowOrder { + return { + sortSignature, + positions: new Map(rows.map((row, index) => [row.worktreeId, index])) + } +} + +/** + * Rows already on screen keep their slot while a refresh streams updated + * values in; rows the order has not seen yet append after them (in the live + * sort among themselves) and claim the next slots, so later ticks cannot + * reshuffle them either. + */ +export function arrangeWorkspaceCleanupRowsByFrozenOrder( + sortedRows: readonly Row[], + order: WorkspaceCleanupFrozenRowOrder +): Row[] { + const known: Row[] = [] + const fresh: Row[] = [] + for (const row of sortedRows) { + if (order.positions.has(row.worktreeId)) { + known.push(row) + } else { + fresh.push(row) + } + } + known.sort( + (left, right) => + (order.positions.get(left.worktreeId) ?? 0) - (order.positions.get(right.worktreeId) ?? 0) + ) + return [...known, ...fresh] +} + +export function extendWorkspaceCleanupFrozenRowOrder( + rows: readonly OrderedRow[], + order: WorkspaceCleanupFrozenRowOrder +): WorkspaceCleanupFrozenRowOrder { + let positions: Map | null = null + for (const row of rows) { + if (order.positions.has(row.worktreeId)) { + continue + } + positions ??= new Map(order.positions) + if (!positions.has(row.worktreeId)) { + positions.set(row.worktreeId, positions.size) + } + } + return positions === null ? order : { ...order, positions } +} + +/** + * Streaming-order stability for the flat list: while a scan streams, existing + * rows must not reshuffle on every progress tick. The user's sort applies to + * the snapshot immediately; streamed value changes land in place; one calm + * re-sort happens when the scan settles — or at once if the user explicitly + * changes the sort mid-scan. + */ +export function useWorkspaceCleanupRowOrder({ + rows, + streaming, + sort +}: { + rows: readonly Row[] + streaming: boolean + sort: WorkspaceCleanupSortState +}): readonly Row[] { + // Why: the frozen order must be React-owned state — a ref read during render + // is not a valid memo input under concurrent rendering and could arrange the + // list from a different render pass's order. The idle capture stays in a ref + // because it is only read and written inside the commit effect. + const [frozenOrder, setFrozenOrder] = + useState(UNCOMMITTED_ROW_ORDER) + const idleOrderRef = useRef(UNCOMMITTED_ROW_ORDER) + const sortSignature = `${sort.field}:${sort.direction}` + const orderedRows = useMemo( + () => + !streaming || frozenOrder.sortSignature !== sortSignature + ? rows + : arrangeWorkspaceCleanupRowsByFrozenOrder(rows, frozenOrder), + [frozenOrder, rows, sortSignature, streaming] + ) + const commitFrozenOrder = useEffectEvent(() => { + // Why: only committed renders may advance the order a later stream holds. + if (!streaming) { + idleOrderRef.current = createWorkspaceCleanupFrozenRowOrder(rows, sortSignature) + setFrozenOrder((current) => + current === UNCOMMITTED_ROW_ORDER ? current : UNCOMMITTED_ROW_ORDER + ) + return + } + setFrozenOrder((current) => { + const base = + current !== UNCOMMITTED_ROW_ORDER && current.sortSignature === sortSignature + ? current + : idleOrderRef.current.sortSignature === sortSignature + ? idleOrderRef.current + : createWorkspaceCleanupFrozenRowOrder(rows, sortSignature) + return extendWorkspaceCleanupFrozenRowOrder(rows, base) + }) + }) + useEffect(() => { + commitFrozenOrder() + }, [rows, sortSignature, streaming]) + return orderedRows +} diff --git a/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-scan-lifecycle.ts b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-scan-lifecycle.ts new file mode 100644 index 00000000000..8537f2136ff --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-scan-lifecycle.ts @@ -0,0 +1,135 @@ +import { useCallback, useEffect, useRef } from 'react' +import { toast } from 'sonner' +import { useAppStore } from '@/store' +import { useMountedRef } from '@/hooks/useMountedRef' +import { translate } from '@/i18n/i18n' +import { formatWorkspaceCleanupReadyToast } from './workspace-cleanup-scan-notice' +import { isWorkspaceCleanupScanSupersededError } from '@/store/slices/workspace-cleanup-broad-scan-registry' + +export type WorkspaceCleanupScanLifecycle = { + startWorkspaceCleanupScan: (options?: { notifyWhenReady?: boolean }) => void +} + +/** + * Owns the dialog's scan cadence: seed the list from the persisted snapshot, + * kick one background rescan per open, and adopt — never duplicate — a scan + * already streaming into the store when the dialog reopens mid-flight. + */ +export function useWorkspaceCleanupScanLifecycle({ + open, + loading, + removalInFlight, + removalInFlightRef, + resetRowFailures, + onFreshOpen +}: { + open: boolean + loading: boolean + removalInFlight: boolean + removalInFlightRef: { current: boolean } + resetRowFailures: () => void + onFreshOpen: () => void +}): WorkspaceCleanupScanLifecycle { + const scanWorkspaceCleanup = useAppStore((s) => s.scanWorkspaceCleanup) + const hydrateCleanupFromCache = useAppStore((s) => s.hydrateWorkspaceCleanupFromCache) + const hydrateSpaceFromCache = useAppStore((s) => s.hydrateWorkspaceSpaceFromCache) + const openModal = useAppStore((s) => s.openModal) + const mountedRef = useMountedRef() + const openRef = useRef(open) + const wasOpenRef = useRef(false) + const autoScanAttemptedForOpenRef = useRef(false) + const latestReadyToastScanAtRef = useRef(null) + + useEffect(() => { + openRef.current = open + }, [open]) + + const startWorkspaceCleanupScan = useCallback( + (options: { notifyWhenReady?: boolean } = {}) => { + resetRowFailures() + void scanWorkspaceCleanup() + .then((result) => { + if (!mountedRef.current || !options.notifyWhenReady || openRef.current) { + return + } + if (latestReadyToastScanAtRef.current === result.scannedAt) { + return + } + latestReadyToastScanAtRef.current = result.scannedAt + const suggestedCount = result.candidates.filter( + (candidate) => candidate.selectedByDefault + ).length + toast.success( + translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.0e2d235c63', + 'Workspace scan ready' + ), + { + description: formatWorkspaceCleanupReadyToast( + result.candidates.length, + suggestedCount + ), + action: { + label: translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.4a35c08764', + 'Review' + ), + onClick: () => openModal('workspace-cleanup') + } + } + ) + }) + .catch((err: unknown) => { + if (mountedRef.current && !isWorkspaceCleanupScanSupersededError(err)) { + toast.error( + translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.662b8ec3f8', + 'Workspace cleanup scan failed' + ), + { description: err instanceof Error ? err.message : String(err) } + ) + } + }) + }, + [mountedRef, openModal, resetRowFailures, scanWorkspaceCleanup] + ) + + const hydrateThenScan = useCallback(() => { + // Why: the cached snapshot must seed the list before the rescan starts so + // progress reconciles into full rows instead of a cold rebuild. + void Promise.allSettled([hydrateCleanupFromCache(), hydrateSpaceFromCache()]).then(() => { + startWorkspaceCleanupScan({ notifyWhenReady: true }) + }) + }, [hydrateCleanupFromCache, hydrateSpaceFromCache, startWorkspaceCleanupScan]) + + useEffect(() => { + if (!open) { + wasOpenRef.current = false + autoScanAttemptedForOpenRef.current = false + return + } + if (!wasOpenRef.current) { + wasOpenRef.current = true + autoScanAttemptedForOpenRef.current = false + onFreshOpen() + } + // Why: reopening mid-batch keeps the deletion progress view; a broad scan + // started here would be discarded by the removal's scan invalidation, so + // skip it while a removal batch is running. + if (removalInFlight || removalInFlightRef.current) { + return + } + if (autoScanAttemptedForOpenRef.current) { + return + } + autoScanAttemptedForOpenRef.current = true + if (loading) { + // Why: reopening mid-scan adopts the stream already feeding the store; + // starting another broad scan here would duplicate it once it settles. + return + } + hydrateThenScan() + }, [hydrateThenScan, loading, onFreshOpen, open, removalInFlight, removalInFlightRef]) + + return { startWorkspaceCleanupScan } +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-active-facets.test.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-active-facets.test.ts new file mode 100644 index 00000000000..1c45ddbc7ad --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-active-facets.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest' +import { createDefaultWorkspaceCleanupFilterState } from '../../../../shared/workspace-cleanup-filter-model' +import { + hasActiveWorkspaceCleanupFilters, + listActiveWorkspaceCleanupFacetGroups +} from './workspace-cleanup-active-facets' + +describe('listActiveWorkspaceCleanupFacetGroups', () => { + it('reports nothing for the default state', () => { + const filters = createDefaultWorkspaceCleanupFilterState() + expect(listActiveWorkspaceCleanupFacetGroups(filters)).toEqual([]) + expect(hasActiveWorkspaceCleanupFilters(filters)).toBe(false) + }) + + it('names each edited group', () => { + const filters = createDefaultWorkspaceCleanupFilterState() + filters.activity.idleMinDays = 45 + filters.git.states = ['dirty'] + expect(listActiveWorkspaceCleanupFacetGroups(filters)).toEqual(['activity', 'git']) + }) + + it('treats multi-select order as meaningless', () => { + const filters = createDefaultWorkspaceCleanupFilterState() + filters.review.states = ['merged', 'closed'] + const reordered = createDefaultWorkspaceCleanupFilterState() + reordered.review.states = ['closed', 'merged'] + expect(listActiveWorkspaceCleanupFacetGroups(filters)).toEqual( + listActiveWorkspaceCleanupFacetGroups(reordered) + ) + }) + + it('counts a text query as an active filter without naming a group', () => { + const filters = createDefaultWorkspaceCleanupFilterState() + filters.query = ' alpha ' + expect(listActiveWorkspaceCleanupFacetGroups(filters)).toEqual([]) + expect(hasActiveWorkspaceCleanupFilters(filters)).toBe(true) + }) +}) diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-active-facets.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-active-facets.ts new file mode 100644 index 00000000000..d9b359cff69 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-active-facets.ts @@ -0,0 +1,70 @@ +import { + createDefaultWorkspaceCleanupFilterState, + type WorkspaceCleanupFilterState +} from '../../../../shared/workspace-cleanup-filter-model' + +export type WorkspaceCleanupFacetGroupKey = keyof Omit + +const FACET_GROUP_KEYS: readonly WorkspaceCleanupFacetGroupKey[] = [ + 'activity', + 'size', + 'status', + 'agent', + 'git', + 'review', + 'ticket', + 'context', + 'location', + 'safety' +] + +/** + * Facet groups the user has moved off the default state. Drives the "N filters + * on" badge. + */ +export function listActiveWorkspaceCleanupFacetGroups( + filters: WorkspaceCleanupFilterState +): WorkspaceCleanupFacetGroupKey[] { + const defaults = createDefaultWorkspaceCleanupFilterState() + return FACET_GROUP_KEYS.filter((key) => !isGroupEqual(filters[key], defaults[key])) +} + +export function hasActiveWorkspaceCleanupFilters(filters: WorkspaceCleanupFilterState): boolean { + return ( + filters.query.trim().length > 0 || listActiveWorkspaceCleanupFacetGroups(filters).length > 0 + ) +} + +/** Selection order is not meaning, so array members compare as sets. */ +function isGroupEqual(left: unknown, right: unknown): boolean { + if (Array.isArray(left) || Array.isArray(right)) { + if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) { + return false + } + const leftMembers = left.map(String).sort() + const rightMembers = right.map(String).sort() + return leftMembers.every((member, index) => member === rightMembers[index]) + } + if (isRecord(left) || isRecord(right)) { + if (!isRecord(left) || !isRecord(right)) { + return false + } + const leftKeys = Object.keys(left).sort() + const rightKeys = Object.keys(right).sort() + return ( + leftKeys.length === rightKeys.length && + leftKeys.every( + (key, index) => key === rightKeys[index] && isGroupEqual(trim(left[key]), trim(right[key])) + ) + ) + } + return left === right +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === 'object' +} + +function trim(value: unknown): unknown { + return typeof value === 'string' ? value.trim() : value +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal-snapshot-batch.test.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal-snapshot-batch.test.ts new file mode 100644 index 00000000000..04b088e43d9 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal-snapshot-batch.test.ts @@ -0,0 +1,59 @@ +import { expect, it, vi } from 'vitest' +import { startWorkspaceCleanupBackgroundRemoval } from './workspace-cleanup-background-removal' +import { makeCandidate } from './workspace-cleanup-presentation-fixtures' + +vi.mock('sonner', () => ({ + toast: { error: vi.fn(), info: vi.fn(), success: vi.fn() } +})) + +async function settleBackgroundRemoval(): Promise { + for (let index = 0; index < 10; index += 1) { + await Promise.resolve() + } +} + +it('holds one snapshot prune batch across the sequential cleanup removals', async () => { + const first = makeCandidate() + const second = makeCandidate({ + worktreeId: 'repo-1::/repo/beta', + displayName: 'beta', + branch: 'beta', + path: '/repo/beta' + }) + const events: string[] = [] + const removeCandidates = vi.fn(async (worktreeIds: readonly string[]) => { + events.push(`remove:${worktreeIds[0]}`) + return { removedIds: [...worktreeIds], failures: [] } + }) + + startWorkspaceCleanupBackgroundRemoval({ + candidates: [first, second], + removeCandidates, + onProgress: vi.fn(), + snapshotPruneBatch: { + batchId: 'batch-1', + begin: async () => { + events.push('begin') + }, + finish: async () => { + events.push('finish') + } + } + }) + await settleBackgroundRemoval() + + expect(removeCandidates).toHaveBeenNthCalledWith(1, [first.worktreeId], { + approvedCandidates: [first], + snapshotPruneBatchId: 'batch-1' + }) + expect(removeCandidates).toHaveBeenNthCalledWith(2, [second.worktreeId], { + approvedCandidates: [second], + snapshotPruneBatchId: 'batch-1' + }) + expect(events).toEqual([ + 'begin', + `remove:${first.worktreeId}`, + `remove:${second.worktreeId}`, + 'finish' + ]) +}) diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal.ts index 2384e4749b7..9e98435faac 100644 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal.ts +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal.ts @@ -50,6 +50,11 @@ export type WorkspaceCleanupBackgroundRemovalArgs = { onRowFailed?: (failure: WorkspaceCleanupFailure) => void removalTimeoutMs?: number removalSettlementGraceMs?: number + snapshotPruneBatch?: { + batchId: string + begin: () => Promise + finish: () => Promise + } } export function startWorkspaceCleanupBackgroundRemoval({ @@ -61,7 +66,8 @@ export function startWorkspaceCleanupBackgroundRemoval({ onError, onRowFailed, removalTimeoutMs = DEFAULT_WORKSPACE_CLEANUP_REMOVAL_TIMEOUT_MS, - removalSettlementGraceMs = DEFAULT_WORKSPACE_CLEANUP_SETTLEMENT_GRACE_MS + removalSettlementGraceMs = DEFAULT_WORKSPACE_CLEANUP_SETTLEMENT_GRACE_MS, + snapshotPruneBatch }: WorkspaceCleanupBackgroundRemovalArgs): void { if (candidates.length === 0) { try { @@ -166,71 +172,95 @@ export function startWorkspaceCleanupBackgroundRemoval({ } void (async () => { - while (queue.length > 0) { - const candidate = queue.shift() - if (!candidate) { - break - } - const blockers = findBlockingDescendants(candidate) - if (blockers.length > 0) { - skipBlockedAncestor(candidate, blockers) - continue - } + let snapshotPruneBatchActive = false + if (snapshotPruneBatch) { try { - const outcome = await waitForWorkspaceCleanupRemovalWithTimeout( - removeCandidates([candidate.worktreeId], { approvedCandidates: [candidate] }), - removalTimeoutMs, - removalSettlementGraceMs - ) - if (outcome.status === 'rejected') { - throw outcome.error + await snapshotPruneBatch.begin() + snapshotPruneBatchActive = true + } catch (error) { + console.warn('Failed to begin workspace cleanup snapshot prune batch:', error) + } + } + try { + while (queue.length > 0) { + const candidate = queue.shift() + if (!candidate) { + break } - if (outcome.status === 'unresolved') { - const timeoutFailure = getWorkspaceCleanupTimeoutFailure(candidate) - failedCandidates.push(candidate) - provisionallyBlocked.add(candidate) - pendingSettlementFailures.add(timeoutFailure) - reportFailures([timeoutFailure]) - // Why: renderer IPC cannot be cancelled at the timeout boundary; keep - // its authoritative settlement without unblocking ancestors early. - // After the batch ends, detach switches to the post-batch path which - // still holds skip state so ancestors can reclassify. - lateSettlementTrackers.push( - trackWorkspaceCleanupLateSettlement(outcome.settlement, candidate, (lateResult) => { - removeArrayEntry(failures, timeoutFailure) - pendingSettlementFailures.delete(timeoutFailure) - provisionallyBlocked.delete(candidate) - removedIds.push(...lateResult.removedIds) - preservedBranches.push(...(lateResult.preservedBranches ?? [])) - reportFailures(lateResult.failures) - if (lateResult.failures.length === 0) { - removeArrayEntry(failedCandidates, candidate) - } - resettleSkippedAncestors() - emitProgress() - }) - ) + const blockers = findBlockingDescendants(candidate) + if (blockers.length > 0) { + skipBlockedAncestor(candidate, blockers) continue } - const result = outcome.result - removedIds.push(...result.removedIds) - preservedBranches.push(...(result.preservedBranches ?? [])) - reportFailures(result.failures) - if (result.failures.length > 0) { - failedCandidates.push(candidate) - } - } catch (error: unknown) { - failedCandidates.push(candidate) - reportFailures([ - { - worktreeId: candidate.worktreeId, - displayName: candidate.displayName, - message: error instanceof Error ? error.message : String(error) + try { + const outcome = await waitForWorkspaceCleanupRemovalWithTimeout( + removeCandidates([candidate.worktreeId], { + approvedCandidates: [candidate], + ...(snapshotPruneBatchActive && snapshotPruneBatch + ? { snapshotPruneBatchId: snapshotPruneBatch.batchId } + : {}) + }), + removalTimeoutMs, + removalSettlementGraceMs + ) + if (outcome.status === 'rejected') { + throw outcome.error } - ]) - } finally { - processedCount += 1 - emitProgress() + if (outcome.status === 'unresolved') { + const timeoutFailure = getWorkspaceCleanupTimeoutFailure(candidate) + failedCandidates.push(candidate) + provisionallyBlocked.add(candidate) + pendingSettlementFailures.add(timeoutFailure) + reportFailures([timeoutFailure]) + // Why: renderer IPC cannot be cancelled at the timeout boundary; keep + // its authoritative settlement without unblocking ancestors early. + // After the batch ends, detach switches to the post-batch path which + // still holds skip state so ancestors can reclassify. + lateSettlementTrackers.push( + trackWorkspaceCleanupLateSettlement(outcome.settlement, candidate, (lateResult) => { + removeArrayEntry(failures, timeoutFailure) + pendingSettlementFailures.delete(timeoutFailure) + provisionallyBlocked.delete(candidate) + removedIds.push(...lateResult.removedIds) + preservedBranches.push(...(lateResult.preservedBranches ?? [])) + reportFailures(lateResult.failures) + if (lateResult.failures.length === 0) { + removeArrayEntry(failedCandidates, candidate) + } + resettleSkippedAncestors() + emitProgress() + }) + ) + continue + } + const result = outcome.result + removedIds.push(...result.removedIds) + preservedBranches.push(...(result.preservedBranches ?? [])) + reportFailures(result.failures) + if (result.failures.length > 0) { + failedCandidates.push(candidate) + } + } catch (error: unknown) { + failedCandidates.push(candidate) + reportFailures([ + { + worktreeId: candidate.worktreeId, + displayName: candidate.displayName, + message: error instanceof Error ? error.message : String(error) + } + ]) + } finally { + processedCount += 1 + emitProgress() + } + } + } finally { + if (snapshotPruneBatchActive && snapshotPruneBatch) { + try { + await snapshotPruneBatch.finish() + } catch (error) { + console.warn('Failed to finish workspace cleanup snapshot prune batch:', error) + } } } diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-browse-toolbar.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-browse-toolbar.tsx new file mode 100644 index 00000000000..c70209dd0eb --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-browse-toolbar.tsx @@ -0,0 +1,96 @@ +import React from 'react' +import { translate } from '@/i18n/i18n' +import type { WorkspaceCleanupBrowseController } from './use-workspace-cleanup-browse-state' +import type { WorkspaceCleanupFacetRows } from './use-workspace-cleanup-facet-rows' +import { listActiveWorkspaceCleanupFacetGroups } from './workspace-cleanup-active-facets' +import { + WorkspaceCleanupEmptyState, + WorkspaceCleanupSizeScanBanner +} from './workspace-cleanup-dialog-notices' +import { WorkspaceCleanupFilterBar } from './workspace-cleanup-filter-bar' +import { WorkspaceCleanupSortHeader } from './workspace-cleanup-sort-header' +import type { WorkspaceSpaceScanProgress } from '../../../../shared/workspace-space-types' + +export function WorkspaceCleanupBrowseToolbar({ + browse, + facetRows, + facetPanelOpen, + onFacetPanelOpenChange, + selectableCount, + selectedCount, + spaceScanning, + spaceProgress, + gitPendingCount, + gitCheckedTotal, + onRunSpaceScan, + onToggleSelectAll +}: { + browse: WorkspaceCleanupBrowseController + facetRows: WorkspaceCleanupFacetRows + facetPanelOpen: boolean + onFacetPanelOpenChange: (open: boolean) => void + selectableCount: number + selectedCount: number + spaceScanning: boolean + spaceProgress: WorkspaceSpaceScanProgress | null + gitPendingCount: number + gitCheckedTotal: number + onRunSpaceScan: () => void + onToggleSelectAll: (selectAll: boolean) => void +}): React.JSX.Element { + // Why: this toolbar re-renders per keystroke and per streaming tick; the + // group comparisons must not rerun unless the filter state itself changed. + const activeFacetGroupCount = React.useMemo( + () => listActiveWorkspaceCleanupFacetGroups(browse.filters).length, + [browse.filters] + ) + const activeFilters = browse.filters.query.trim().length > 0 || activeFacetGroupCount > 0 + return ( + <> + {spaceScanning || facetRows.unmeasuredSizeCount > 0 ? ( + + ) : null} + browse.patchFilters('query', query)} + onClearFilters={browse.clearFilters} + /> + + {browse.sort.field === 'size' && facetRows.measuredSizeCount === 0 && !spaceScanning ? ( + + ) : null} + + ) +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-list.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-list.tsx index 68569c7aba9..7cfd365bc60 100644 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-list.tsx +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-list.tsx @@ -1,6 +1,8 @@ import React from 'react' import { useVirtualizer } from '@tanstack/react-virtual' -import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' + +/** Any row shape the flat list renders — raw candidates or enriched facet rows. */ +type WorkspaceCleanupListRow = { worktreeId: string } // Why: below this count plain rows keep the pre-virtualization DOM (natural // flow, no absolute positioning), so the common few-worktrees case is @@ -19,16 +21,18 @@ const WORKSPACE_CLEANUP_ROW_OVERSCAN = 8 * no scroll-margin bookkeeping is needed. Lists shorter than * WORKSPACE_CLEANUP_VIRTUALIZE_MIN_ROWS render plainly. */ -export function WorkspaceCleanupCandidateList({ +export function WorkspaceCleanupCandidateList({ rows, renderRow, + estimatedRowHeight = WORKSPACE_CLEANUP_ROW_ESTIMATE_PX, // Why: a state-held element, not a ref — the ScrollArea viewport is not // attached when this component first mounts, so a ref would leave the // virtualizer unobserved until some unrelated re-render. scrollElement }: { - rows: readonly WorkspaceCleanupCandidate[] - renderRow: (candidate: WorkspaceCleanupCandidate, index: number) => React.ReactNode + rows: readonly Row[] + renderRow: (row: Row, index: number) => React.ReactNode + estimatedRowHeight?: number scrollElement: HTMLDivElement | null }): React.JSX.Element { const virtualize = rows.length >= WORKSPACE_CLEANUP_VIRTUALIZE_MIN_ROWS @@ -37,7 +41,7 @@ export function WorkspaceCleanupCandidateList({ count: rows.length, enabled: virtualize && scrollElement !== null, getScrollElement: () => scrollElement, - estimateSize: () => WORKSPACE_CLEANUP_ROW_ESTIMATE_PX, + estimateSize: () => estimatedRowHeight, overscan: WORKSPACE_CLEANUP_ROW_OVERSCAN, // Why: stable worktree keys let the virtualizer carry row identity across // scan refreshes instead of remounting the window on every streamed row. diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row-data.test.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row-data.test.ts index 3cd5309ac54..7ed376b10b6 100644 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row-data.test.ts +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row-data.test.ts @@ -1,8 +1,20 @@ import { describe, expect, it } from 'vitest' -import { getDirtyGitLabel, shouldShowGitMetadataChip } from './workspace-cleanup-candidate-row-data' +import { + getCandidateFactStatus, + getDirtyGitLabel, + shouldShowGitMetadataChip +} from './workspace-cleanup-candidate-row-data' import { makeCandidate } from './workspace-cleanup-presentation-fixtures' describe('workspace cleanup candidate row data', () => { + it('shows facts instead of cleanup policy tiers', () => { + expect(getCandidateFactStatus(makeCandidate({ tier: 'ready' }))).toBeNull() + expect(getCandidateFactStatus(makeCandidate({ tier: 'review' }))).toBeNull() + expect( + getCandidateFactStatus(makeCandidate({ tier: 'ready', reasons: ['archived'] })) + ).toMatchObject({ label: 'Archived' }) + }) + it('does not duplicate git status blockers as a separate git icon label', () => { const gitStatusError = makeCandidate({ blockers: ['git-status-error'], @@ -29,6 +41,18 @@ describe('workspace cleanup candidate row data', () => { ).toBe(true) }) + it('keeps unpushed risk visible for archived rows', () => { + expect( + getCandidateFactStatus( + makeCandidate({ + tier: 'review', + reasons: ['archived'], + git: { clean: true, upstreamAhead: 2, upstreamBehind: 0, checkedAt: 1 } + }) + ) + ).toMatchObject({ label: 'Unpushed commits' }) + }) + it('suppresses the git metadata chip when the status pill already names git risk', () => { expect( shouldShowGitMetadataChip( diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row-data.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row-data.ts index 9e819d90017..42a7d0c58a5 100644 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row-data.ts +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row-data.ts @@ -23,10 +23,10 @@ export function getWorkspaceCleanupBlockerLabels(candidate: WorkspaceCleanupCand return candidate.blockers.map((blocker) => getWorkspaceCleanupBlockerLabel(blocker)) } -export function getCandidateStatus(candidate: WorkspaceCleanupCandidate): { +export function getCandidateFactStatus(candidate: WorkspaceCleanupCandidate): { label: string tone: StatusPillTone -} { +} | null { if (candidate.blockers.includes('dismissed')) { return { label: translate( @@ -36,20 +36,6 @@ export function getCandidateStatus(candidate: WorkspaceCleanupCandidate): { tone: 'neutral' } } - if (candidate.tier === 'ready') { - return { - label: candidate.reasons.includes('archived') - ? translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.archivedStatus', - 'Archived' - ) - : translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.readyStatus', - 'Ready' - ), - tone: 'ready' - } - } if (candidate.blockers.length > 0) { return { label: getWorkspaceCleanupBlockerLabel(candidate.blockers[0]), tone: 'neutral' } } @@ -71,22 +57,16 @@ export function getCandidateStatus(candidate: WorkspaceCleanupCandidate): { tone: 'review' } } - if (candidate.tier === 'review') { + if (candidate.reasons.includes('archived')) { return { label: translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.0a2e3c7cba', - 'Review' + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.archivedStatus', + 'Archived' ), - tone: 'review' + tone: 'neutral' } } - return { - label: translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.c4f4782c02', - 'Not suggested' - ), - tone: 'neutral' - } + return null } export function formatGitStatus(candidate: WorkspaceCleanupCandidate): string { diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row.test.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row.test.tsx index 7de51d662cb..2f8ecc9fb07 100644 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row.test.tsx +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row.test.tsx @@ -31,6 +31,103 @@ describe('CandidateRow', () => { container = null }) + it('does not present cleanup policy tiers as workspace facts', () => { + const candidate = makeCandidate({ tier: 'ready' }) + + act(() => { + root?.render( + + ) + }) + + expect(container?.textContent).not.toContain('Ready') + expect(container?.textContent).not.toContain('Status unavailable') + }) + + it('shows factual status, disk size, activity, and git state before expansion', () => { + const candidate = makeCandidate({ tier: 'ready' }) + + act(() => { + root?.render( + + ) + }) + + expect(container?.textContent).toContain('In progress') + expect(container?.textContent).toContain('4.00 MB') + expect(container?.textContent).toContain('1d') + expect(container?.textContent).toContain('Clean') + }) + + it('uses an external-link action to open the workspace', () => { + const candidate = makeCandidate() + + act(() => { + root?.render( + + ) + }) + + const openButton = container?.querySelector(`[aria-label="Open ${candidate.displayName}"]`) + expect(openButton?.querySelector('.lucide-external-link')).not.toBeNull() + }) + it('hides selection and remove controls while the workspace is already deleting', () => { const candidate = makeCandidate() diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row.tsx index 2953f46fa5a..f8f68b03a9c 100644 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row.tsx +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row.tsx @@ -3,16 +3,17 @@ import { AlertTriangle, Check, ChevronDown, + CircleDot, Clock3, EyeOff, + ExternalLink, FileWarning, GitBranch, GitPullRequest, + HardDrive, Loader2, - Search, SquareTerminal, - Trash2, - type LucideIcon + Trash2 } from 'lucide-react' import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' @@ -31,15 +32,15 @@ import { formatBranchSafetyDetails, formatContextDetails, formatGitStatus, - getCandidateStatus, + getCandidateFactStatus, getContextCount, getDirtyGitLabel, getReviewPillTone, getWorkspaceCleanupBlockerLabels, - shouldShowGitMetadataChip, - type StatusPillTone + shouldShowGitMetadataChip } from './workspace-cleanup-candidate-row-data' import { StatusPill } from './workspace-cleanup-status-pill' +import { WorkspaceCleanupMetadataChip } from './workspace-cleanup-metadata-chip' export type WorkspaceCleanupDeletionPhase = 'deleting' | 'queued' @@ -48,9 +49,15 @@ type CandidateRowProps = { deletionPhase?: WorkspaceCleanupDeletionPhase expanded: boolean failure?: string + /** A focused git re-scan is in flight, so "Not checked" is provisional. */ + gitEvidencePending?: boolean last: boolean lastActivityLabel: string removing?: boolean + /** Joined from the workspace-space scan; null when that scan has not run. */ + sizeLabel?: string | null + /** User-configured status label; null when this broad-scan row is not in renderer state. */ + workspaceStatusLabel?: string | null reviewInfo: WorkspaceCleanupReviewInfo selected: boolean onIgnore: (candidate: WorkspaceCleanupCandidate) => void @@ -60,42 +67,6 @@ type CandidateRowProps = { onView: (candidate: WorkspaceCleanupCandidate) => void } -function MetadataIconChip({ - icon: Icon, - label, - value, - tone = 'neutral' -}: { - icon: LucideIcon - label: string - value?: string - tone?: StatusPillTone -}): React.JSX.Element { - return ( - - - - - - - {label} - - - ) -} - // Why: the cleanup list re-renders on every checkbox/expand/search keystroke; // memo keeps each unchanged row from re-rendering. Effective only while the // parent passes stable (useCallback) handlers — see WorkspaceCleanupDialog. @@ -106,9 +77,12 @@ export const CandidateRow = React.memo(function CandidateRow({ deletionPhase, expanded, failure, + gitEvidencePending = false, last, lastActivityLabel, removing = false, + sizeLabel = null, + workspaceStatusLabel = null, reviewInfo, selected, onIgnore, @@ -125,10 +99,13 @@ export const CandidateRow = React.memo(function CandidateRow({ const blockers = getWorkspaceCleanupBlockerLabels(candidate) const contextDetails = formatContextDetails(candidate) const branchSafetyDetails = formatBranchSafetyDetails(candidate) - const status = getCandidateStatus(candidate) + const factStatus = getCandidateFactStatus(candidate) const dirtyLabel = getDirtyGitLabel(candidate) + const gitLabel = getWorkspaceCleanupGitLabel(candidate) const showGitMetadataChip = shouldShowGitMetadataChip(candidate) const contextCount = getContextCount(candidate) + const sizeValue = + sizeLabel ?? translate('components.workspace.cleanup.browse.notMeasured', 'Not measured') const hasExpandableDetails = blockers.length > 0 || candidate.path.length > 0 || @@ -169,7 +146,9 @@ export const CandidateRow = React.memo(function CandidateRow({
- {candidate.displayName} + + {candidate.displayName} + {deletionPhase ? ( {deletionPhase === 'queued' @@ -182,10 +161,21 @@ export const CandidateRow = React.memo(function CandidateRow({ 'Deleting…' )} - ) : ( - {status.label} - )} - {factStatus.label} + ) : null} + {workspaceStatusLabel ? ( + + ) : null} + - {dirtyLabel && showGitMetadataChip ? ( - + + {gitEvidencePending ? ( + + + ) : dirtyLabel && showGitMetadataChip ? ( + ) : showGitMetadataChip ? ( - ) : null} {contextDetails ? ( - ) : null} {reviewInfo.label ? ( - onView(candidate)} > - + - {translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.ee81adfcef', - 'View' - )} + {translate('components.workspace.cleanup.browse.openWorkspace', 'Open workspace')} {!ignored ? ( diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-confirm-remove.test.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-confirm-remove.test.tsx new file mode 100644 index 00000000000..f93672103c8 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-confirm-remove.test.tsx @@ -0,0 +1,76 @@ +// @vitest-environment happy-dom +import { act, type ReactNode } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { makeFacetCandidate } from './workspace-cleanup-facet.test.fixture' +import { WorkspaceCleanupConfirmRemove } from './workspace-cleanup-confirm-remove' + +const listProbe = vi.hoisted(() => vi.fn()) + +vi.mock('./workspace-cleanup-candidate-list', () => ({ + WorkspaceCleanupCandidateList: ({ + rows, + renderRow + }: { + rows: readonly Row[] + renderRow: (row: Row, index: number) => ReactNode + }) => { + listProbe(rows.length) + return <>{rows.slice(0, 5).map(renderRow)} + } +})) + +vi.mock('@/components/ui/dialog', () => ({ + DialogDescription: ({ children }: { children: ReactNode }) =>
{children}
, + DialogFooter: ({ children }: { children: ReactNode }) =>
{children}
, + DialogHeader: ({ children }: { children: ReactNode }) =>
{children}
, + DialogTitle: ({ children }: { children: ReactNode }) =>
{children}
+})) + +vi.mock('@/components/ui/scroll-area', () => ({ + ScrollArea: ({ children }: { children: ReactNode }) =>
{children}
+})) + +let container: HTMLDivElement +let root: Root + +describe('WorkspaceCleanupConfirmRemove', () => { + beforeEach(() => { + listProbe.mockClear() + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + }) + + afterEach(() => { + act(() => root.unmount()) + document.body.replaceChildren() + }) + + it('routes fleet-sized confirmations through the bounded row list', () => { + const candidates = Array.from({ length: 2_727 }, (_, index) => + makeFacetCandidate({ + worktreeId: `repo-1::/repo/worktree-${index}`, + displayName: `Workspace ${index}`, + path: `/repo/worktree-${index}` + }) + ) + + act(() => { + root.render( + + ) + }) + + expect(listProbe).toHaveBeenCalledWith(candidates.length) + expect(container.querySelectorAll('span.truncate.text-sm.font-medium')).toHaveLength(5) + }) +}) diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-confirm-remove.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-confirm-remove.tsx new file mode 100644 index 00000000000..0d946cc14e0 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-confirm-remove.tsx @@ -0,0 +1,253 @@ +import React from 'react' +import { AlertTriangle, Loader2, Trash2, X } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Progress } from '@/components/ui/progress' +import { ScrollArea } from '@/components/ui/scroll-area' +import { cn } from '@/lib/utils' +import { translate } from '@/i18n/i18n' +import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' +import type { WorkspaceCleanupRemovalProgress } from './workspace-cleanup-background-removal' +import { + getCandidateFactStatus, + getContextPillLabel, + getDirtyGitLabel, + getReviewPillTone, + shouldShowGitMetadataChip +} from './workspace-cleanup-candidate-row-data' +import type { WorkspaceCleanupReviewInfo } from './workspace-cleanup-presentation' +import { formatWorkspaceCleanupRelativeTime } from './workspace-cleanup-relative-time' +import { StatusPill } from './workspace-cleanup-status-pill' +import { WorkspaceCleanupCandidateList } from './workspace-cleanup-candidate-list' +import { + getWorkspaceCleanupCandidateHostId, + getWorkspaceCleanupHostIdentity +} from './workspace-cleanup-host-identity' + +const CONFIRM_REMOVE_ROW_ESTIMATE_PX = 76 + +const EMPTY_REVIEW_INFO: WorkspaceCleanupReviewInfo = { + hasReview: false, + label: null, + state: null, + provider: null, + title: null +} + +export function WorkspaceCleanupConfirmRemove({ + candidates, + now, + reviewInfoByWorktreeId, + progress, + onBack, + onCancel, + onConfirm +}: { + candidates: WorkspaceCleanupCandidate[] + /** The dialog's "as of" clock; keeps labels consistent with the list view. */ + now: number + reviewInfoByWorktreeId: ReadonlyMap + progress: WorkspaceCleanupRemovalProgress | null + onBack: () => void + onCancel: () => void + onConfirm: () => void +}): React.JSX.Element { + const [scrollElement, setScrollElement] = React.useState(null) + const count = candidates.length + const deleting = progress !== null + const progressValue = progress + ? Math.min(100, Math.max(0, (progress.processedCount / progress.totalCount) * 100)) + : 0 + return ( + <> + +
+
+
+ {deleting ? ( + + ) : ( + + )} +
+
+ + {deleting + ? translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.deletingCount', + 'Deleting workspaces: {{value0}}', + { value0: count } + ) + : translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.deleteCount', + 'Delete workspaces: {{value0}}?', + { value0: count } + )} + + + {deleting + ? translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.1d3503357d', + 'You can close this and come back while deletion continues.' + ) + : translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.38ca0b1400', + "This permanently deletes their local files. You can't undo this." + )} + +
+
+ +
+
+
+ {progress ? ( +
+
+ + + {formatWorkspaceCleanupRemovalProgress(progress)} + +
+ +
+ ) : null} +
+
+ {translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.selectedForDeletionCount', + 'Selected for deletion: {{value0}}', + { value0: count } + )} +
+
+ + ( + + )} + /> + +
+ + + {!deleting ? ( + + ) : null} + + + ) +} + +function ConfirmRemoveRow({ + candidate, + now, + reviewInfo, + last +}: { + candidate: WorkspaceCleanupCandidate + now: number + reviewInfo: WorkspaceCleanupReviewInfo + last: boolean +}): React.JSX.Element { + const dirtyLabel = getDirtyGitLabel(candidate) + const branchDiffersFromName = candidate.branch !== candidate.displayName + const contextPillLabel = getContextPillLabel(candidate) + const showGitMetadataChip = shouldShowGitMetadataChip(candidate) + const factStatus = getCandidateFactStatus(candidate) + return ( +
+
+ {candidate.displayName} + + {translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.352f15d6fc', + 'Last active' + )}{' '} + {formatWorkspaceCleanupRelativeTime(candidate.lastActivityAt, now)} + + {factStatus ? {factStatus.label} : null} + {reviewInfo.label ? ( + {reviewInfo.label} + ) : null} + {contextPillLabel ? {contextPillLabel} : null} + {dirtyLabel && showGitMetadataChip ? ( + {dirtyLabel} + ) : null} +
+
+ {candidate.repoName} + {branchDiffersFromName ? ( + <> + + {candidate.branch} + + ) : null} +
+
+ {candidate.path} +
+
+ ) +} + +function formatWorkspaceCleanupRemovalProgress(progress: WorkspaceCleanupRemovalProgress): string { + const deletedText = translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.4c2990886e', + '{{value0}}/{{value1}} deleted', + { value0: progress.removedCount, value1: progress.totalCount } + ) + if (progress.failedCount === 0) { + return deletedText + } + return translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.86ba852118', + '{{value0}}, {{value1}} failed', + { value0: deletedText, value1: progress.failedCount } + ) +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-header.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-header.tsx new file mode 100644 index 00000000000..a632903a8cc --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-header.tsx @@ -0,0 +1,97 @@ +import React from 'react' +import { RefreshCcw, Trash2, X } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { cn } from '@/lib/utils' +import { translate } from '@/i18n/i18n' +import type { WorkspaceCleanupScanProgress } from '../../../../shared/workspace-cleanup' +import { WorkspaceCleanupFreshness } from './workspace-cleanup-freshness' + +export function WorkspaceCleanupDialogHeader({ + selectedCount, + deleteDisabled, + loading, + scannedAt, + scanProgress, + onDeleteSelected, + onRefresh, + onClose +}: { + selectedCount: number + deleteDisabled: boolean + loading: boolean + scannedAt: number | null + scanProgress: WorkspaceCleanupScanProgress | null + onDeleteSelected: () => void + onRefresh: () => void + onClose: () => void +}): React.JSX.Element { + const refreshLabel = translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.7ae2ad30f4', + 'Refresh' + ) + return ( + +
+ + {translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.b2c1331844', + 'Delete Inactive Workspaces' + )} + +
+ + + {translate('components.workspace.cleanup.browse.selectedCount', '{{value0}} selected', { + value0: selectedCount + })} + + + + + + + + {refreshLabel} + + + +
+
+
+ ) +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-notices.test.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-notices.test.tsx new file mode 100644 index 00000000000..8544d8eb415 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-notices.test.tsx @@ -0,0 +1,58 @@ +// @vitest-environment happy-dom +import { cleanup, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + WorkspaceCleanupInitialScanBanner, + WorkspaceCleanupSizeScanBanner +} from './workspace-cleanup-dialog-notices' + +afterEach(cleanup) + +describe('WorkspaceCleanupInitialScanBanner', () => { + it('puts determinate progress in the title with matching typography', () => { + render( + + ) + + expect(screen.getByText('Scanning workspaces (23/3048)')).toBeTruthy() + expect(screen.queryByText('Checked workspaces so far: 23')).toBeNull() + }) +}) + +describe('WorkspaceCleanupSizeScanBanner', () => { + it('explains why scanning is useful and exposes the action', () => { + render( + + ) + + expect(screen.getByText('Scan workspace sizes')).toBeTruthy() + expect( + screen.getByText('Scan disk usage to compare, sort, and filter workspaces by size.') + ).toBeTruthy() + expect(screen.getByRole('button', { name: 'Scan' })).toBeTruthy() + }) + + it('shows determinate progress and disables duplicate runs', () => { + render( + + ) + + expect( + (screen.getByRole('button', { name: 'Scanning 23/100' }) as HTMLButtonElement).disabled + ).toBe(true) + }) +}) diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-notices.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-notices.tsx new file mode 100644 index 00000000000..40592de7f5b --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-notices.tsx @@ -0,0 +1,148 @@ +import React from 'react' +import { AlertTriangle, HardDrive, Loader2 } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { translate } from '@/i18n/i18n' +import type { WorkspaceCleanupScanProgress } from '../../../../shared/workspace-cleanup' + +export function WorkspaceCleanupInitialScanBanner({ + progress +}: { + progress: WorkspaceCleanupScanProgress | null +}): React.JSX.Element { + const title = + progress && progress.totalWorktreeCount > 0 + ? translate( + 'components.workspace.cleanup.scan.progress', + 'Scanning workspaces ({{value0}}/{{value1}})', + { + value0: progress.scannedWorktreeCount, + value1: progress.totalWorktreeCount + } + ) + : translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.7eee951968', + 'Scanning workspaces' + ) + + return ( +
+ +
+
{title}
+
+ {translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.47123d0108', + 'Collecting workspace information. You can close this and come back.' + )} +
+
+
+ ) +} + +export function WorkspaceCleanupSizeScanBanner({ + scanning, + scannedCount, + totalCount, + onRun +}: { + scanning: boolean + scannedCount: number + totalCount: number + onRun: () => void +}): React.JSX.Element { + const actionLabel = scanning + ? totalCount > 0 + ? translate( + 'components.workspace.cleanup.browse.measuringSizesProgress', + 'Scanning {{value0}}/{{value1}}', + { value0: scannedCount, value1: totalCount } + ) + : translate('components.workspace.cleanup.browse.measuringSizes', 'Scanning sizes') + : translate('components.workspace.cleanup.browse.measureSizes', 'Scan') + + return ( +
+
+ +
+
+ {translate( + 'components.workspace.cleanup.browse.measureSizesTitle', + 'Scan workspace sizes' + )} +
+
+ {translate( + 'components.workspace.cleanup.browse.measureSizesDescription', + 'Scan disk usage to compare, sort, and filter workspaces by size.' + )} +
+
+
+ +
+ ) +} + +export function WorkspaceCleanupNotice({ + tone = 'muted', + message +}: { + tone?: 'muted' | 'destructive' + message: string +}): React.JSX.Element { + if (tone === 'destructive') { + return ( +
+ {message} +
+ ) + } + return ( +
+ + {message} +
+ ) +} + +export function WorkspaceCleanupEmptyState({ + title, + description, + actionLabel, + onAction +}: { + title: string + description?: string + actionLabel?: string + onAction?: () => void +}): React.JSX.Element { + return ( +
+ {title} + {description ? {description} : null} + {actionLabel && onAction ? ( + + ) : null} +
+ ) +} + +export function WorkspaceCleanupSkeletonRows(): React.JSX.Element { + return ( +
+ {[0, 1, 2].map((index) => ( +
+ ))} +
+ ) +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-session.test.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-session.test.tsx deleted file mode 100644 index ae35cbc9b6d..00000000000 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-session.test.tsx +++ /dev/null @@ -1,175 +0,0 @@ -// @vitest-environment happy-dom - -import { act } from 'react' -import { createRoot, type Root } from 'react-dom/client' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { WorkspaceCleanupScanResult } from '../../../../shared/workspace-cleanup' -import { useAppStore } from '@/store' -import type { WorkspaceCleanupRemoveResult } from '@/store/slices/workspace-cleanup' -import { - useWorkspaceCleanupDialogSession, - type WorkspaceCleanupDialogSession -} from './workspace-cleanup-dialog-session' -import { makeCandidate } from './workspace-cleanup-presentation-fixtures' - -const toastMocks = vi.hoisted(() => ({ - error: vi.fn(), - info: vi.fn(), - success: vi.fn() -})) - -vi.mock('sonner', () => ({ toast: toastMocks })) - -const initialAppState = useAppStore.getInitialState() -let container: HTMLDivElement -let root: Root -let currentSession: WorkspaceCleanupDialogSession | null - -function deferred(): { - promise: Promise - resolve: (value: T) => void -} { - let resolve!: (value: T) => void - const promise = new Promise((innerResolve) => { - resolve = innerResolve - }) - return { promise, resolve } -} - -function Probe(): null { - currentSession = useWorkspaceCleanupDialogSession() - return null -} - -function session(): WorkspaceCleanupDialogSession { - if (!currentSession) { - throw new Error('Workspace cleanup session probe is not mounted') - } - return currentSession -} - -async function renderProbe(): Promise { - await act(async () => root.render()) -} - -describe('useWorkspaceCleanupDialogSession', () => { - beforeEach(() => { - globalThis.IS_REACT_ACT_ENVIRONMENT = true - vi.clearAllMocks() - useAppStore.setState(initialAppState, true) - useAppStore.setState({ - activeModal: 'workspace-cleanup', - workspaceCleanupLoading: false, - deleteStateByWorktreeId: {} - }) - currentSession = null - container = document.createElement('div') - document.body.appendChild(container) - root = createRoot(container) - }) - - afterEach(async () => { - await act(async () => root.unmount()) - container.remove() - currentSession = null - useAppStore.setState(initialAppState, true) - }) - - it('offers to reopen when a scan finishes after the dialog closes', async () => { - const candidate = makeCandidate() - const scan = deferred() - const scanWorkspaceCleanup = vi.fn(() => scan.promise) - useAppStore.setState({ scanWorkspaceCleanup }) - - await renderProbe() - expect(scanWorkspaceCleanup).toHaveBeenCalledOnce() - - await act(async () => useAppStore.getState().closeModal()) - expect(session().open).toBe(false) - - await act(async () => { - scan.resolve({ scannedAt: 42, candidates: [candidate], errors: [] }) - await scan.promise - }) - - expect(toastMocks.success).toHaveBeenCalledWith( - 'Inactive workspace scan ready', - expect.objectContaining({ - description: '1 inactive workspace found, with 1 cleanup suggestion.', - action: expect.objectContaining({ label: 'Review' }) - }) - ) - const options = toastMocks.success.mock.lastCall?.[1] as { - action?: { onClick: () => void } - } - - await act(async () => options.action?.onClick()) - - expect(useAppStore.getState().activeModal).toBe('workspace-cleanup') - expect(session().open).toBe(true) - }) - - it('keeps a deferred removal alive and visible across close and reopen', async () => { - const candidate = makeCandidate() - const removal = deferred() - const removeWorkspaceCleanupCandidates = vi.fn(() => removal.promise) - const scanWorkspaceCleanup = vi.fn().mockResolvedValue({ - scannedAt: 41, - candidates: [candidate], - errors: [] - } satisfies WorkspaceCleanupScanResult) - useAppStore.setState({ removeWorkspaceCleanupCandidates, scanWorkspaceCleanup }) - await renderProbe() - - await act(async () => session().openConfirmRemove([candidate])) - await act(async () => session().confirmRemove()) - - expect(removeWorkspaceCleanupCandidates).toHaveBeenCalledWith([candidate.worktreeId], { - approvedCandidates: [candidate] - }) - expect(session()).toMatchObject({ - open: true, - confirming: true, - removalInFlight: true, - removalProgress: { - totalCount: 1, - processedCount: 0, - removedCount: 0, - failedCount: 0 - } - }) - - await act(async () => session().close()) - - expect(session()).toMatchObject({ - open: false, - confirming: true, - removalInFlight: true - }) - expect(session().removalProgress?.processedCount).toBe(0) - - await act(async () => useAppStore.getState().openModal('workspace-cleanup')) - - expect(session()).toMatchObject({ - open: true, - confirming: true, - removalInFlight: true - }) - expect(session().removalProgress?.processedCount).toBe(0) - expect(scanWorkspaceCleanup).toHaveBeenCalledOnce() - - await act(async () => { - removal.resolve({ removedIds: [candidate.worktreeId], failures: [] }) - await removal.promise - await Promise.resolve() - }) - - expect(session()).toMatchObject({ - open: true, - confirming: false, - confirmCandidates: [], - removalInFlight: false, - removalProgress: null - }) - }) -}) diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-session.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-session.ts deleted file mode 100644 index 670e73fa4dc..00000000000 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-session.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { - useCallback, - useEffect, - useRef, - useState, - type Dispatch, - type RefObject, - type SetStateAction -} from 'react' -import { toast } from 'sonner' -import { useShallow } from 'zustand/react/shallow' -import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' -import { useMountedRef } from '@/hooks/useMountedRef' -import { translate } from '@/i18n/i18n' -import { useAppStore } from '@/store' -import { - useWorkspaceCleanupRemovalSession, - type WorkspaceCleanupRemovalSession -} from './workspace-cleanup-removal-session' -import { useWorkspaceCleanupScanSession } from './workspace-cleanup-scan-session' -import type { - WorkspaceCleanupFilters, - WorkspaceCleanupSortDirection, - WorkspaceCleanupSortKey -} from './workspace-cleanup-presentation' -import type { WorkspaceCleanupView } from './workspace-cleanup-view-selection' - -export const DEFAULT_WORKSPACE_CLEANUP_FILTERS: WorkspaceCleanupFilters = { - query: '', - time: 'all', - review: 'all', - git: 'all', - context: 'all' -} - -type WorkspaceCleanupStateSetter = Dispatch> -type PublicRemovalSession = Omit< - WorkspaceCleanupRemovalSession, - 'clearRowFailures' | 'resetForOpen' -> - -export type WorkspaceCleanupDialogSession = PublicRemovalSession & { - open: boolean - loading: boolean - selectedIds: Set - setSelectedIds: WorkspaceCleanupStateSetter> - expandedRowIds: Set - setExpandedRowIds: WorkspaceCleanupStateSetter> - activeView: WorkspaceCleanupView - setActiveView: WorkspaceCleanupStateSetter - repoSelection: ReadonlySet - setRepoSelection: WorkspaceCleanupStateSetter> - filters: WorkspaceCleanupFilters - setFilters: WorkspaceCleanupStateSetter - sortKey: WorkspaceCleanupSortKey - setSortKey: WorkspaceCleanupStateSetter - sortDirection: WorkspaceCleanupSortDirection - setSortDirection: WorkspaceCleanupStateSetter - selectedDefaultsScanAtRef: RefObject - close: () => void - markCandidateViewed: (candidate: WorkspaceCleanupCandidate) => void - restoreDismissals: () => void - startScan: (options?: { notifyWhenReady?: boolean }) => void - ignoreCandidate: (candidate: WorkspaceCleanupCandidate) => void -} - -export function useWorkspaceCleanupDialogSession(): WorkspaceCleanupDialogSession { - const { - open, - loading, - openModal, - closeModal, - scanWorkspaceCleanup, - markCandidateViewed, - dismissCandidates, - resetDismissals, - removeCandidates, - markWorktreesQueuedForDeletion, - clearWorktreeDeleteState - } = useAppStore( - useShallow((state) => ({ - open: state.activeModal === 'workspace-cleanup', - loading: state.workspaceCleanupLoading, - openModal: state.openModal, - closeModal: state.closeModal, - scanWorkspaceCleanup: state.scanWorkspaceCleanup, - markCandidateViewed: state.markWorkspaceCleanupCandidateViewed, - dismissCandidates: state.dismissWorkspaceCleanupCandidates, - resetDismissals: state.resetWorkspaceCleanupDismissals, - removeCandidates: state.removeWorkspaceCleanupCandidates, - markWorktreesQueuedForDeletion: state.markWorktreesQueuedForDeletion, - clearWorktreeDeleteState: state.clearWorktreeDeleteState - })) - ) - const [selectedIds, setSelectedIds] = useState>(() => new Set()) - const [expandedRowIds, setExpandedRowIds] = useState>(() => new Set()) - const [activeView, setActiveView] = useState('ready') - const [repoSelection, setRepoSelection] = useState>(() => new Set()) - const [filters, setFilters] = useState(DEFAULT_WORKSPACE_CLEANUP_FILTERS) - const [sortKey, setSortKey] = useState('activity') - const [sortDirection, setSortDirection] = useState('asc') - const selectedDefaultsScanAtRef = useRef(null) - const autoScanAttemptedForOpenRef = useRef(false) - const wasOpenRef = useRef(false) - const mountedRef = useMountedRef() - const removal = useWorkspaceCleanupRemovalSession({ - mountedRef, - setSelectedIds, - closeModal, - removeCandidates, - markWorktreesQueuedForDeletion, - clearWorktreeDeleteState - }) - const { clearRowFailures, resetForOpen, ...publicRemoval } = removal - const startScan = useWorkspaceCleanupScanSession({ - open, - mountedRef, - openModal, - scanWorkspaceCleanup, - clearRowFailures - }) - - useEffect(() => { - if (!open) { - wasOpenRef.current = false - autoScanAttemptedForOpenRef.current = false - return - } - if (!wasOpenRef.current) { - wasOpenRef.current = true - autoScanAttemptedForOpenRef.current = false - if (!removal.removalInFlightRef.current) { - setActiveView('ready') - setFilters(DEFAULT_WORKSPACE_CLEANUP_FILTERS) - setSortKey('activity') - setSortDirection('asc') - resetForOpen() - } - } - // Why: reopening mid-batch must preserve progress and avoid a scan the removal would invalidate. - if (!loading && !autoScanAttemptedForOpenRef.current && !removal.removalInFlightRef.current) { - autoScanAttemptedForOpenRef.current = true - startScan({ notifyWhenReady: true }) - } - }, [loading, open, removal.removalInFlightRef, resetForOpen, startScan]) - - const ignoreCandidate = useCallback( - (candidate: WorkspaceCleanupCandidate) => { - void dismissCandidates([candidate]) - .then(() => { - if (!mountedRef.current) { - return - } - setSelectedIds((current) => { - const next = new Set(current) - next.delete(candidate.worktreeId) - return next - }) - }) - .catch((error: unknown) => { - if (!mountedRef.current) { - return - } - toast.error( - translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.7f451a3e2c', - 'Could not ignore cleanup suggestion' - ), - { description: error instanceof Error ? error.message : String(error) } - ) - }) - }, - [dismissCandidates, mountedRef] - ) - - const restoreDismissals = useCallback(() => { - void resetDismissals() - }, [resetDismissals]) - - return { - open, - loading, - selectedIds, - setSelectedIds, - expandedRowIds, - setExpandedRowIds, - activeView, - setActiveView, - repoSelection, - setRepoSelection, - filters, - setFilters, - sortKey, - setSortKey, - sortDirection, - setSortDirection, - selectedDefaultsScanAtRef, - close: closeModal, - markCandidateViewed, - restoreDismissals, - startScan, - ignoreCandidate, - ...publicRemoval - } -} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-controls.test.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-controls.test.tsx new file mode 100644 index 00000000000..10049ceeb4e --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-controls.test.tsx @@ -0,0 +1,161 @@ +// @vitest-environment happy-dom +import type { ReactNode } from 'react' +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest' +import { + createDefaultWorkspaceCleanupFilterState, + type WorkspaceCleanupFilterState +} from '../../../../shared/workspace-cleanup-filter-model' +import { WorkspaceCleanupGitReviewFacets } from './workspace-cleanup-git-review-facets' +import { WorkspaceCleanupLifecycleFacets } from './workspace-cleanup-lifecycle-facets' +import type { + WorkspaceCleanupFacetCounts, + WorkspaceCleanupFacetGroupProps, + WorkspaceCleanupFacetOptions +} from './workspace-cleanup-facet-panel-model' + +/** `Mock` erases T's type parameter, so keep the generic call signature and add the spy API. */ +type PatchMock = WorkspaceCleanupFacetGroupProps['onPatch'] & MockInstance + +function createPatchMock(): PatchMock { + return vi.fn() +} + +let root: Root | null = null +let container: HTMLDivElement | null = null + +const COUNTS: WorkspaceCleanupFacetCounts = { + activity: 3, + size: 4, + status: 8, + agent: 8, + git: 2, + review: 5, + ticket: 8, + context: 6, + location: 8, + safety: 7 +} + +const OPTIONS: WorkspaceCleanupFacetOptions = { + workspaceStatuses: [{ id: 'in-progress', label: 'In progress' }], + hostIds: ['local', 'ssh:box'], + repos: [ + { id: 'repo-1', label: 'Alpha' }, + { id: 'repo-2', label: 'Beta' } + ], + reviewProviders: ['github', 'gitlab'] +} + +function render(node: ReactNode): void { + act(() => root?.render(node)) +} + +function control(label: string): HTMLElement | null { + return container?.querySelector(`[aria-label="${label}"]`) ?? null +} + +/** React tracks the controlled value, so a bare `input.value =` is swallowed. */ +function typeInto(input: HTMLInputElement | null, value: string): void { + if (!input) { + return + } + const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set + setter?.call(input, value) + input.dispatchEvent(new Event('input', { bubbles: true })) +} + +function renderFacets(filters: WorkspaceCleanupFilterState, onPatch: PatchMock): void { + const props = { filters, counts: COUNTS, totalCount: 8, options: OPTIONS, onPatch } + render( + <> + + + + ) +} + +describe('workspace cleanup facet controls', () => { + beforeEach(() => { + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + }) + + afterEach(() => { + if (root) { + act(() => root?.unmount()) + } + container?.remove() + root = null + container = null + }) + + it('shows a per-facet match count for every group', () => { + renderFacets(createDefaultWorkspaceCleanupFilterState(), createPatchMock()) + + const counts = [...(container?.querySelectorAll('[data-facet-count]') ?? [])].map( + (node) => node.textContent + ) + expect(counts).toHaveLength(10) + expect(counts).toContain('2/8') + expect(counts).toContain('3/8') + }) + + it('takes a user-chosen idle threshold in days rather than a fixed enum', () => { + const onPatch = createPatchMock() + renderFacets(createDefaultWorkspaceCleanupFilterState(), onPatch) + + const input = control('Idle for at least') as HTMLInputElement | null + expect(input?.type).toBe('number') + act(() => typeInto(input, '17')) + expect(onPatch).toHaveBeenCalledWith('activity', { idleMinDays: 17 }) + }) + + it('multi-selects git states', () => { + const onPatch = createPatchMock() + const filters = createDefaultWorkspaceCleanupFilterState() + filters.git.states = ['dirty'] + renderFacets(filters, onPatch) + + expect(control('Uncommitted changes')?.getAttribute('aria-checked')).toBe('true') + act(() => control('Unpushed commits')?.click()) + expect(onPatch).toHaveBeenCalledWith('git', { states: ['dirty', 'unpushed'] }) + }) + + it('offers the hosts and providers discovered in the fleet, not a hardcoded list', () => { + renderFacets(createDefaultWorkspaceCleanupFilterState(), createPatchMock()) + + expect(control('GitLab')).not.toBeNull() + expect(container?.textContent).toContain('Alpha') + expect(container?.textContent).toContain('In progress') + }) + + it('cycles a tri-state safety facet', () => { + const onPatch = createPatchMock() + renderFacets(createDefaultWorkspaceCleanupFilterState(), onPatch) + + act(() => container?.querySelector('[aria-label="Ignored: Only"]')?.click()) + expect(onPatch).toHaveBeenCalledWith('safety', { dismissed: 'only' }) + }) + + it('labels blocker matching without exposing model tokens', () => { + renderFacets(createDefaultWorkspaceCleanupFilterState(), createPatchMock()) + + expect(control('Blocker match: Has any')).not.toBeNull() + expect(control('Blocker match: Has none')).not.toBeNull() + expect(container?.textContent).not.toContain('any-of') + expect(container?.textContent).not.toContain('none-of') + }) + + it('toggles the unsized escape hatch for the opt-in space scan', () => { + const onPatch = createPatchMock() + renderFacets(createDefaultWorkspaceCleanupFilterState(), onPatch) + + const checkbox = control('Include unmeasured workspaces') + expect(checkbox?.getAttribute('data-state')).toBe('checked') + act(() => checkbox?.click()) + expect(onPatch).toHaveBeenCalledWith('size', { includeUnsized: false }) + }) +}) diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-controls.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-controls.tsx new file mode 100644 index 00000000000..7dc1c0d92e7 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-controls.tsx @@ -0,0 +1,225 @@ +import React from 'react' +import { Checkbox } from '@/components/ui/checkbox' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { cn } from '@/lib/utils' +import { parseWorkspaceCleanupFacetNumber } from './workspace-cleanup-facet-panel-model' + +/** One facet group inside the filter panel, with its own match count. */ +export function FacetSection({ + title, + matchCount, + totalCount, + children +}: { + title: string + matchCount: number + totalCount: number + children: React.ReactNode +}): React.JSX.Element { + return ( +
+
+

+ {title} +

+ + {matchCount}/{totalCount} + +
+
{children}
+
+ ) +} + +/** Multi-select: empty selection leaves the facet unconstrained. */ +export function FacetToggleList({ + label, + values, + selected, + getLabel, + onChange +}: { + label?: string + values: readonly T[] + selected: readonly T[] + getLabel: (value: T) => string + onChange: (next: T[]) => void +}): React.JSX.Element { + const selectedSet = new Set(selected) + return ( + +
+ {values.map((value) => { + const active = selectedSet.has(value) + return ( + + ) + })} +
+
+ ) +} + +/** Single-choice segmented control for tri-state / presence / enum facets. */ +export function FacetChoice({ + label, + value, + options, + getLabel, + onChange +}: { + label: string + value: T + options: readonly T[] + getLabel?: (value: T) => string + onChange: (next: T) => void +}): React.JSX.Element { + return ( + +
+ {options.map((option) => ( + + ))} +
+
+ ) +} + +/** Free-form numeric threshold — deliberately not a fixed 30/90 enum. */ +export function FacetNumberField({ + label, + value, + placeholder, + suffix, + onChange +}: { + label: string + value: number | null + placeholder?: string + suffix?: string + onChange: (next: number | null) => void +}): React.JSX.Element { + return ( + +
+ onChange(parseWorkspaceCleanupFacetNumber(event.target.value))} + className="h-7 w-24 text-xs" + /> + {suffix ? {suffix} : null} +
+
+ ) +} + +export function FacetTextField({ + label, + value, + placeholder, + onChange +}: { + label: string + value: string + placeholder?: string + onChange: (next: string) => void +}): React.JSX.Element { + return ( + + onChange(event.target.value)} + className="h-7 text-xs" + /> + + ) +} + +export function FacetCheckbox({ + id, + label, + checked, + onChange +}: { + id: string + label: string + checked: boolean + onChange: (next: boolean) => void +}): React.JSX.Element { + const checkboxId = `workspace-cleanup-facet-${id}` + return ( +
+ onChange(next === true)} + /> + +
+ ) +} + +function FacetField({ + label, + children +}: { + label?: string + children: React.ReactNode +}): React.JSX.Element { + if (!label) { + return <>{children} + } + return ( +
+
{label}
+ {children} +
+ ) +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-filter.test.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-filter.test.ts new file mode 100644 index 00000000000..8a5dd1a0ed9 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-filter.test.ts @@ -0,0 +1,518 @@ +import { describe, expect, it } from 'vitest' +import { + buildWorkspaceCleanupFacetList, + countWorkspaceCleanupMeasuredRows +} from './workspace-cleanup-facets' +import { + buildWorkspaceCleanupSizeIndex, + buildWorkspaceCleanupWorktreeIndex, + getWorkspaceCleanupHostIdentity +} from './workspace-cleanup-host-identity' +import { + DAY, + FACET_NOW, + makeFacetCandidate, + makeFacets, + makeNamedFacets +} from './workspace-cleanup-facet.test.fixture' +import { + createDefaultWorkspaceCleanupFilterState, + type WorkspaceCleanupFilterState +} from '../../../../shared/workspace-cleanup-filter-model' +import { + countWorkspaceCleanupFacetMatches, + filterWorkspaceCleanupFacets, + matchesWorkspaceCleanupFilters, + queryWorkspaceCleanupCandidates +} from './workspace-cleanup-query' +import type { WorkspaceSpaceWorktree } from '../../../../shared/workspace-space-types' + +function filters(patch: (state: WorkspaceCleanupFilterState) => void): WorkspaceCleanupFilterState { + const state = createDefaultWorkspaceCleanupFilterState() + patch(state) + return state +} + +const ANY = filters((state) => { + state.safety.dismissed = 'any' +}) + +describe('facet building', () => { + it('strips the ref prefix and falls back to local host', () => { + const facets = makeFacets({ worktree: { branch: 'refs/heads/feature/x' } }) + expect(facets.branch).toBe('feature/x') + expect(facets.hostId).toBe('local') + }) + + it('preserves a disconnected candidate execution host', () => { + const facets = makeFacets({ + candidate: { connectionId: 'builder', executionHostId: 'ssh:builder' }, + worktree: null + }) + expect(facets.hostId).toBe('ssh:builder') + }) + + it('treats a missing size entry as unsized rather than zero', () => { + expect(makeFacets().sizeBytes).toBeNull() + expect(makeFacets({ sizeBytes: 0 }).sizeBytes).toBe(0) + }) + + it('treats a missing visit stamp as never opened', () => { + expect(makeFacets().lastVisitedAt).toBeNull() + expect(makeFacets({ lastVisitedAt: FACET_NOW }).lastVisitedAt).toBe(FACET_NOW) + }) + + it('resolves the visible workspace status without changing statusless filter semantics', () => { + expect(makeFacets().workspaceStatus).toBeNull() + expect(makeFacets().workspaceStatusLabel).toBe('In progress') + expect(makeFacets({ worktree: { workspaceStatus: 'in-review' } }).workspaceStatusLabel).toBe( + 'In review' + ) + expect(makeFacets({ worktree: null }).workspaceStatusLabel).toBeNull() + }) + + it('only trusts sizes from an ok space scan', () => { + const scanned = [ + { worktreeId: 'a', status: 'ok', sizeBytes: 10 }, + { worktreeId: 'b', status: 'permission-denied', sizeBytes: 99 } + ] as WorkspaceSpaceWorktree[] + const index = buildWorkspaceCleanupSizeIndex(scanned) + expect(index.get('a')).toBe(10) + expect(index.has('b')).toBe(false) + }) + + it('counts measurements only for rows in the current fleet', () => { + const rows = [makeFacets({ sizeBytes: 10 }), makeNamedFacets('unsized')] + expect(countWorkspaceCleanupMeasuredRows(rows)).toBe(1) + }) + + it('indexes worktrees across repos by host and id', () => { + const index = buildWorkspaceCleanupWorktreeIndex({ + 'repo-1': [{ id: 'w1', hostId: 'local' }], + 'repo-2': [{ id: 'w2', hostId: 'ssh:builder' }] + }) + expect([...index.keys()]).toEqual([ + getWorkspaceCleanupHostIdentity('local', 'w1'), + getWorkspaceCleanupHostIdentity('ssh:builder', 'w2') + ]) + }) + + it('keeps same-id worktrees and sizes isolated by host', () => { + const candidates = [ + makeFacetCandidate({ executionHostId: 'local' }), + makeFacetCandidate({ executionHostId: 'ssh:builder', connectionId: 'builder' }) + ] + const sizes = buildWorkspaceCleanupSizeIndex( + [ + { + worktreeId: candidates[0]!.worktreeId, + executionHostId: 'local', + status: 'ok', + sizeBytes: 10 + }, + { + worktreeId: candidates[1]!.worktreeId, + executionHostId: 'ssh:builder', + status: 'ok', + sizeBytes: 20 + } + ], + candidates + ) + + expect(sizes.get(getWorkspaceCleanupHostIdentity('local', candidates[0]!.worktreeId))).toBe(10) + expect( + sizes.get(getWorkspaceCleanupHostIdentity('ssh:builder', candidates[1]!.worktreeId)) + ).toBe(20) + expect(sizes.has(candidates[0]!.worktreeId)).toBe(false) + }) + + it('does not apply a legacy size to duplicate candidate ids', () => { + const candidates = [ + makeFacetCandidate({ executionHostId: 'local' }), + makeFacetCandidate({ executionHostId: 'ssh:builder', connectionId: 'builder' }) + ] + const sizes = buildWorkspaceCleanupSizeIndex( + [{ worktreeId: candidates[0]!.worktreeId, status: 'ok', sizeBytes: 10 }], + candidates + ) + + expect(sizes.size).toBe(0) + }) + + it('does not apply duplicate legacy size rows to one current candidate', () => { + const candidate = makeFacetCandidate({ executionHostId: 'local' }) + const sizes = buildWorkspaceCleanupSizeIndex( + [ + { worktreeId: candidate.worktreeId, status: 'ok', sizeBytes: 10 }, + { worktreeId: candidate.worktreeId, status: 'ok', sizeBytes: 20 } + ], + [candidate] + ) + + expect(sizes.size).toBe(0) + }) + + it('does not assign legacy worktree metadata with multi-host repo ownership', () => { + const index = buildWorkspaceCleanupWorktreeIndex( + { 'repo-1': [{ id: 'w1', repoId: 'repo-1' }] }, + [ + { id: 'repo-1', executionHostId: 'local' }, + { id: 'repo-1', executionHostId: 'ssh:builder' } + ] + ) + + expect(index.size).toBe(0) + }) + + it('counts a workspace with nothing attached as completely empty', () => { + expect(makeFacets().isCompletelyEmpty).toBe(true) + expect(makeFacets({ worktree: { comment: 'keep' } }).isCompletelyEmpty).toBe(false) + expect(makeFacets({ review: {} }).isCompletelyEmpty).toBe(false) + expect(makeFacets({ worktree: { linkedIssue: 12 } }).isCompletelyEmpty).toBe(false) + }) +}) + +describe('query search', () => { + it('matches branch, host, status, provider, and blockers', () => { + const facets = makeFacets({ + candidate: { blockers: ['live-agent'] }, + worktree: { + branch: 'refs/heads/fix/login', + hostId: 'ssh:builder', + workspaceStatus: 'in-review' + }, + review: { provider: 'gitlab', label: 'MR #7' } + }) + for (const term of ['fix/login', 'ssh:builder', 'in-review', 'gitlab', 'live-agent']) { + expect(matchesWorkspaceCleanupFilters(facets, { ...ANY, query: term }, FACET_NOW)).toBe(true) + } + expect(matchesWorkspaceCleanupFilters(facets, { ...ANY, query: 'nope' }, FACET_NOW)).toBe(false) + }) +}) + +describe('activity filter', () => { + it('uses the user-chosen day threshold against the chosen signal', () => { + const recentlyVisited = makeFacets({ lastVisitedAt: FACET_NOW - 5 * DAY }) + const staleVisit = makeFacets({ lastVisitedAt: FACET_NOW - 45 * DAY }) + const state = filters((s) => { + s.safety.dismissed = 'any' + s.activity.idleSignal = 'last-visited' + s.activity.idleMinDays = 30 + }) + expect(matchesWorkspaceCleanupFilters(recentlyVisited, state, FACET_NOW)).toBe(false) + expect(matchesWorkspaceCleanupFilters(staleVisit, state, FACET_NOW)).toBe(true) + }) + + it('reads a missing signal as maximally idle', () => { + const state = filters((s) => { + s.safety.dismissed = 'any' + s.activity.idleMinDays = 365 + }) + expect(matchesWorkspaceCleanupFilters(makeFacets(), state, FACET_NOW)).toBe(true) + }) + + it('separates background activity from a real visit', () => { + const busyButUnopened = makeFacets({ candidate: { lastActivityAt: FACET_NOW } }) + const state = filters((s) => { + s.safety.dismissed = 'any' + s.activity.neverVisited = true + }) + expect(matchesWorkspaceCleanupFilters(busyButUnopened, state, FACET_NOW)).toBe(true) + expect( + matchesWorkspaceCleanupFilters(makeFacets({ lastVisitedAt: FACET_NOW }), state, FACET_NOW) + ).toBe(false) + }) +}) + +describe('size filter', () => { + it('keeps unsized rows unless the user opts out', () => { + const unsized = makeFacets() + expect(matchesWorkspaceCleanupFilters(unsized, ANY, FACET_NOW)).toBe(true) + const state = filters((s) => { + s.safety.dismissed = 'any' + s.size.includeUnsized = false + }) + expect(matchesWorkspaceCleanupFilters(unsized, state, FACET_NOW)).toBe(false) + }) + + it('applies min and max byte bounds', () => { + const state = filters((s) => { + s.safety.dismissed = 'any' + s.size.minBytes = 100 + s.size.maxBytes = 200 + }) + expect(matchesWorkspaceCleanupFilters(makeFacets({ sizeBytes: 150 }), state, FACET_NOW)).toBe( + true + ) + expect(matchesWorkspaceCleanupFilters(makeFacets({ sizeBytes: 99 }), state, FACET_NOW)).toBe( + false + ) + expect(matchesWorkspaceCleanupFilters(makeFacets({ sizeBytes: 201 }), state, FACET_NOW)).toBe( + false + ) + }) +}) + +describe('status filter', () => { + it('matches open workspace-status strings and statusless rows separately', () => { + const inReview = makeFacets({ worktree: { workspaceStatus: 'in-review' } }) + const statusless = makeFacets() + const state = filters((s) => { + s.safety.dismissed = 'any' + s.status.workspaceStatuses = ['in-review'] + s.status.matchStatusless = false + }) + expect(matchesWorkspaceCleanupFilters(inReview, state, FACET_NOW)).toBe(true) + expect(matchesWorkspaceCleanupFilters(statusless, state, FACET_NOW)).toBe(false) + state.status.matchStatusless = true + expect(matchesWorkspaceCleanupFilters(statusless, state, FACET_NOW)).toBe(true) + }) + + it('excludes statusless rows even when no named status is selected', () => { + const state = filters((s) => { + s.safety.dismissed = 'any' + s.status.workspaceStatuses = [] + s.status.matchStatusless = false + }) + expect(matchesWorkspaceCleanupFilters(makeFacets(), state, FACET_NOW)).toBe(false) + expect( + matchesWorkspaceCleanupFilters( + makeFacets({ worktree: { workspaceStatus: 'in-review' } }), + state, + FACET_NOW + ) + ).toBe(true) + }) + + it('applies tri-state flags', () => { + const archived = makeFacets({ worktree: { isArchived: true } }) + const only = filters((s) => { + s.safety.dismissed = 'any' + s.status.archived = 'only' + }) + const exclude = filters((s) => { + s.safety.dismissed = 'any' + s.status.archived = 'exclude' + }) + expect(matchesWorkspaceCleanupFilters(archived, only, FACET_NOW)).toBe(true) + expect(matchesWorkspaceCleanupFilters(archived, exclude, FACET_NOW)).toBe(false) + expect(matchesWorkspaceCleanupFilters(makeFacets(), exclude, FACET_NOW)).toBe(true) + }) +}) + +describe('agent, git, review, ticket, context, and location filters', () => { + it('filters by live agent rollup and retained done agents', () => { + const state = filters((s) => { + s.safety.dismissed = 'any' + s.agent.states = ['permission'] + }) + expect( + matchesWorkspaceCleanupFilters(makeFacets({ agentStatus: 'permission' }), state, FACET_NOW) + ).toBe(true) + expect( + matchesWorkspaceCleanupFilters(makeFacets({ agentStatus: 'working' }), state, FACET_NOW) + ).toBe(false) + expect(makeFacets().agentState).toBe('idle') + + const retained = filters((s) => { + s.safety.dismissed = 'any' + s.agent.retainedDoneAgents = 'only' + }) + const withRetained = makeFacets({ + candidate: { + localContext: { + ...makeFacetCandidate().localContext, + retainedDoneAgentCount: 2 + } + } + }) + expect(matchesWorkspaceCleanupFilters(withRetained, retained, FACET_NOW)).toBe(true) + expect(matchesWorkspaceCleanupFilters(makeFacets(), retained, FACET_NOW)).toBe(false) + }) + + it('filters by git state, ahead counts, branch text, prunable and locked', () => { + const unpushed = makeFacets({ + candidate: { git: { clean: true, upstreamAhead: 3, upstreamBehind: 1, checkedAt: 1 } } + }) + expect(unpushed.gitState).toBe('unpushed') + const state = filters((s) => { + s.safety.dismissed = 'any' + s.git.states = ['unpushed'] + s.git.minAhead = 2 + }) + expect(matchesWorkspaceCleanupFilters(unpushed, state, FACET_NOW)).toBe(true) + state.git.minAhead = 4 + expect(matchesWorkspaceCleanupFilters(unpushed, state, FACET_NOW)).toBe(false) + + const branchState = filters((s) => { + s.safety.dismissed = 'any' + s.git.branchQuery = 'ALPH' + }) + expect(matchesWorkspaceCleanupFilters(makeFacets(), branchState, FACET_NOW)).toBe(true) + + const prunable = filters((s) => { + s.safety.dismissed = 'any' + s.git.prunable = 'only' + }) + expect( + matchesWorkspaceCleanupFilters( + makeFacets({ worktree: { prunable: true } }), + prunable, + FACET_NOW + ) + ).toBe(true) + expect(matchesWorkspaceCleanupFilters(makeFacets(), prunable, FACET_NOW)).toBe(false) + }) + + it('keeps review filtering provider-general and treats draft as a state', () => { + const draftMr = makeFacets({ review: { provider: 'gitlab', state: 'draft', label: 'MR #7' } }) + const state = filters((s) => { + s.safety.dismissed = 'any' + s.review.presence = 'some' + s.review.states = ['draft'] + s.review.providers = ['gitlab', 'bitbucket'] + }) + expect(matchesWorkspaceCleanupFilters(draftMr, state, FACET_NOW)).toBe(true) + state.review.providers = ['github'] + expect(matchesWorkspaceCleanupFilters(draftMr, state, FACET_NOW)).toBe(false) + + const none = filters((s) => { + s.safety.dismissed = 'any' + s.review.presence = 'none' + }) + expect(matchesWorkspaceCleanupFilters(makeFacets(), none, FACET_NOW)).toBe(true) + expect(matchesWorkspaceCleanupFilters(draftMr, none, FACET_NOW)).toBe(false) + }) + + it('filters by ticket source', () => { + const linear = makeFacets({ worktree: { linkedLinearIssue: 'STA-1' } }) + expect(linear.ticketSources).toEqual(['linear']) + const state = filters((s) => { + s.safety.dismissed = 'any' + s.ticket.presence = 'some' + s.ticket.sources = ['linear'] + }) + expect(matchesWorkspaceCleanupFilters(linear, state, FACET_NOW)).toBe(true) + expect( + matchesWorkspaceCleanupFilters(makeFacets({ worktree: { linkedIssue: 9 } }), state, FACET_NOW) + ).toBe(false) + }) + + it('filters by local context and completely-empty', () => { + const busy = makeFacets({ + candidate: { + localContext: { ...makeFacetCandidate().localContext, terminalTabCount: 2 } + } + }) + const hasContext = filters((s) => { + s.safety.dismissed = 'any' + s.context.presence = 'some' + }) + expect(matchesWorkspaceCleanupFilters(busy, hasContext, FACET_NOW)).toBe(true) + const empty = filters((s) => { + s.safety.dismissed = 'any' + s.context.completelyEmpty = true + }) + expect(matchesWorkspaceCleanupFilters(busy, empty, FACET_NOW)).toBe(false) + expect(matchesWorkspaceCleanupFilters(makeFacets(), empty, FACET_NOW)).toBe(true) + }) + + it('filters by execution host, repo, and path prefix', () => { + const remote = makeFacets({ worktree: { hostId: 'ssh:builder' } }) + const state = filters((s) => { + s.safety.dismissed = 'any' + s.location.hostIds = ['ssh:builder'] + s.location.repoIds = ['repo-1'] + s.location.pathPrefix = '/repo/' + }) + expect(matchesWorkspaceCleanupFilters(remote, state, FACET_NOW)).toBe(true) + state.location.pathPrefix = '/other/' + expect(matchesWorkspaceCleanupFilters(remote, state, FACET_NOW)).toBe(false) + expect(matchesWorkspaceCleanupFilters(makeFacets(), state, FACET_NOW)).toBe(false) + }) +}) + +describe('safety filter', () => { + it('supports any-of and none-of blocker selection', () => { + const live = makeFacets({ + candidate: { blockers: ['live-agent', 'pinned'], tier: 'protected' } + }) + const anyOf = filters((s) => { + s.safety.dismissed = 'any' + s.safety.blockers = ['live-agent'] + s.safety.blockerMode = 'any-of' + }) + const noneOf = filters((s) => { + s.safety.dismissed = 'any' + s.safety.blockers = ['live-agent'] + s.safety.blockerMode = 'none-of' + }) + expect(matchesWorkspaceCleanupFilters(live, anyOf, FACET_NOW)).toBe(true) + expect(matchesWorkspaceCleanupFilters(live, noneOf, FACET_NOW)).toBe(false) + expect(matchesWorkspaceCleanupFilters(makeFacets(), noneOf, FACET_NOW)).toBe(true) + }) + + it('includes dismissed rows by default and narrows them on demand', () => { + const dismissed = makeFacets({ dismissed: true }) + expect( + matchesWorkspaceCleanupFilters( + dismissed, + createDefaultWorkspaceCleanupFilterState(), + FACET_NOW + ) + ).toBe(true) + const exclude = filters((s) => { + s.safety.dismissed = 'exclude' + }) + expect(matchesWorkspaceCleanupFilters(dismissed, exclude, FACET_NOW)).toBe(false) + expect(matchesWorkspaceCleanupFilters(makeFacets(), exclude, FACET_NOW)).toBe(true) + const only = filters((s) => { + s.safety.dismissed = 'only' + }) + expect(matchesWorkspaceCleanupFilters(dismissed, only, FACET_NOW)).toBe(true) + expect(matchesWorkspaceCleanupFilters(makeFacets(), only, FACET_NOW)).toBe(false) + }) + + it('restricts to rows the policy would actually let the user delete', () => { + const blocked = makeFacets({ candidate: { blockers: ['dirty-files'], tier: 'review' } }) + const state = filters((s) => { + s.safety.dismissed = 'any' + s.safety.selectableOnly = true + }) + expect(matchesWorkspaceCleanupFilters(blocked, state, FACET_NOW)).toBe(false) + expect(matchesWorkspaceCleanupFilters(makeFacets(), state, FACET_NOW)).toBe(true) + }) +}) + +describe('query pipeline', () => { + it('reports counts and selectable ids', () => { + const rows = [ + makeNamedFacets('alpha'), + makeNamedFacets('beta', { candidate: { blockers: ['dirty-files'], tier: 'review' } }) + ] + expect(filterWorkspaceCleanupFacets(rows, ANY, FACET_NOW)).toHaveLength(2) + const result = queryWorkspaceCleanupCandidates( + buildWorkspaceCleanupFacetList([makeFacetCandidate()]).map((f) => f.candidate), + { filters: ANY, sort: { field: 'name', direction: 'asc' } }, + {}, + FACET_NOW + ) + expect(result.totalCount).toBe(1) + expect(result.matchedCount).toBe(1) + expect(result.selectableWorktreeIds).toEqual(['repo-1::/repo/alpha']) + }) + + it('counts per-facet matches independently of the other groups', () => { + const rows = [makeNamedFacets('alpha', { sizeBytes: 10 }), makeNamedFacets('beta')] + const state = filters((s) => { + s.safety.dismissed = 'any' + s.size.includeUnsized = false + s.git.states = ['dirty'] + }) + const counts = countWorkspaceCleanupFacetMatches(rows, state, FACET_NOW) + expect(counts.size).toBe(1) + expect(counts.git).toBe(0) + expect(counts.safety).toBe(2) + }) +}) diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-labels.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-labels.ts new file mode 100644 index 00000000000..9ea2cd09adf --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-labels.ts @@ -0,0 +1,175 @@ +import { translate } from '@/i18n/i18n' +import type { HostedReviewProvider } from '../../../../shared/hosted-review' +import type { WorkspaceCleanupTier } from '../../../../shared/workspace-cleanup' +import type { + WorkspaceCleanupAgentState, + WorkspaceCleanupBlockerMode, + WorkspaceCleanupGitState, + WorkspaceCleanupIdleSignal, + WorkspaceCleanupPresence, + WorkspaceCleanupReviewState, + WorkspaceCleanupSortField, + WorkspaceCleanupTicketSource, + WorkspaceCleanupTriState +} from '../../../../shared/workspace-cleanup-filter-model' + +export function getWorkspaceCleanupSortFieldLabel(field: WorkspaceCleanupSortField): string { + switch (field) { + case 'last-activity': + return translate('components.workspace.cleanup.browse.sort.lastActivity', 'Last activity') + case 'last-visited': + return translate('components.workspace.cleanup.browse.sort.lastVisited', 'Last opened') + case 'created': + return translate('components.workspace.cleanup.browse.sort.created', 'Created') + case 'size': + return translate('components.workspace.cleanup.browse.sort.size', 'Size') + case 'name': + return translate('components.workspace.cleanup.browse.sort.name', 'Workspace') + case 'repo': + return translate('components.workspace.cleanup.browse.sort.repo', 'Repository') + case 'path': + return translate('components.workspace.cleanup.browse.sort.path', 'Path') + case 'host': + return translate('components.workspace.cleanup.browse.sort.host', 'Host') + case 'workspace-status': + return translate('components.workspace.cleanup.browse.sort.workspaceStatus', 'Status') + case 'agent': + return translate('components.workspace.cleanup.browse.sort.agent', 'Agent') + case 'git': + return translate('components.workspace.cleanup.browse.sort.git', 'Git') + case 'ahead': + return translate('components.workspace.cleanup.browse.sort.ahead', 'Ahead') + case 'behind': + return translate('components.workspace.cleanup.browse.sort.behind', 'Behind') + case 'branch': + return translate('components.workspace.cleanup.browse.sort.branch', 'Branch') + case 'review': + return translate('components.workspace.cleanup.browse.sort.review', 'Review') + case 'ticket': + return translate('components.workspace.cleanup.browse.sort.ticket', 'Ticket') + case 'local-context': + return translate('components.workspace.cleanup.browse.sort.localContext', 'Open context') + case 'tier': + return translate('components.workspace.cleanup.browse.sort.tier', 'Safety') + case 'blocker-count': + return translate('components.workspace.cleanup.browse.sort.blockerCount', 'Blockers') + } +} + +export function getWorkspaceCleanupGitStateLabel(state: WorkspaceCleanupGitState): string { + switch (state) { + case 'clean': + return translate('components.workspace.cleanup.browse.git.clean', 'Clean') + case 'dirty': + return translate('components.workspace.cleanup.browse.git.dirty', 'Uncommitted changes') + case 'unpushed': + return translate('components.workspace.cleanup.browse.git.unpushed', 'Unpushed commits') + case 'unknown': + return translate('components.workspace.cleanup.browse.git.unknown', 'Not checked') + } +} + +export function getWorkspaceCleanupAgentStateLabel(state: WorkspaceCleanupAgentState): string { + switch (state) { + case 'working': + return translate('components.workspace.cleanup.browse.agent.working', 'Working') + case 'permission': + return translate('components.workspace.cleanup.browse.agent.permission', 'Waiting on you') + case 'idle': + return translate('components.workspace.cleanup.browse.agent.idle', 'Idle') + } +} + +export function getWorkspaceCleanupReviewStateLabel(state: WorkspaceCleanupReviewState): string { + switch (state) { + case 'open': + return translate('components.workspace.cleanup.browse.review.open', 'Open') + case 'draft': + return translate('components.workspace.cleanup.browse.review.draft', 'Draft') + case 'merged': + return translate('components.workspace.cleanup.browse.review.merged', 'Merged') + case 'closed': + return translate('components.workspace.cleanup.browse.review.closed', 'Closed') + case 'unknown': + return translate('components.workspace.cleanup.browse.review.unknown', 'Unknown') + } +} + +/** Provider-general: GitLab MRs and Azure DevOps PRs are first-class here. */ +export function getWorkspaceCleanupReviewProviderLabel(provider: HostedReviewProvider): string { + switch (provider) { + case 'github': + return 'GitHub' + case 'gitlab': + return 'GitLab' + case 'bitbucket': + return 'Bitbucket' + case 'azure-devops': + return 'Azure DevOps' + case 'gitea': + return 'Gitea' + case 'unsupported': + return translate('components.workspace.cleanup.browse.review.otherProvider', 'Other') + } +} + +export function getWorkspaceCleanupTicketSourceLabel(source: WorkspaceCleanupTicketSource): string { + switch (source) { + case 'work-item': + return translate('components.workspace.cleanup.browse.ticket.workItem', 'Work item') + case 'linear': + return translate('components.workspace.cleanup.browse.ticket.linear', 'Linear') + case 'issue': + return translate('components.workspace.cleanup.browse.ticket.issue', 'Issue') + } +} + +export function getWorkspaceCleanupTierLabel(tier: WorkspaceCleanupTier): string { + switch (tier) { + case 'ready': + return translate('components.workspace.cleanup.browse.tier.ready', 'Ready') + case 'review': + return translate('components.workspace.cleanup.browse.tier.review', 'Needs review') + case 'protected': + return translate('components.workspace.cleanup.browse.tier.protected', 'Protected') + } +} + +export function getWorkspaceCleanupIdleSignalLabel(signal: WorkspaceCleanupIdleSignal): string { + switch (signal) { + case 'last-visited': + return translate('components.workspace.cleanup.browse.idleSignal.lastVisited', 'Not opened') + case 'last-activity': + return translate('components.workspace.cleanup.browse.idleSignal.lastActivity', 'No activity') + case 'created': + return translate('components.workspace.cleanup.browse.idleSignal.created', 'Created before') + } +} + +export function getWorkspaceCleanupTriStateLabel(value: WorkspaceCleanupTriState): string { + switch (value) { + case 'any': + return translate('components.workspace.cleanup.browse.triState.any', 'Any') + case 'only': + return translate('components.workspace.cleanup.browse.triState.only', 'Only') + case 'exclude': + return translate('components.workspace.cleanup.browse.triState.exclude', 'Exclude') + } +} + +export function getWorkspaceCleanupPresenceLabel(value: WorkspaceCleanupPresence): string { + switch (value) { + case 'any': + return translate('components.workspace.cleanup.browse.presence.any', 'Any') + case 'some': + return translate('components.workspace.cleanup.browse.presence.some', 'Has one') + case 'none': + return translate('components.workspace.cleanup.browse.presence.none', 'Has none') + } +} + +export function getWorkspaceCleanupBlockerModeLabel(value: WorkspaceCleanupBlockerMode): string { + return value === 'any-of' + ? translate('components.workspace.cleanup.browse.blockerModeAny', 'Has any') + : translate('components.workspace.cleanup.browse.blockerModeNone', 'Has none') +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-matchers.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-matchers.ts new file mode 100644 index 00000000000..988a31c1489 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-matchers.ts @@ -0,0 +1,214 @@ +import type { WorkspaceCleanupFacets } from './workspace-cleanup-facets' +import type { + WorkspaceCleanupActivityFilter, + WorkspaceCleanupAgentFilter, + WorkspaceCleanupContextFilterState, + WorkspaceCleanupGitFilterState, + WorkspaceCleanupLocationFilter, + WorkspaceCleanupPresence, + WorkspaceCleanupReviewFilterState, + WorkspaceCleanupSafetyFilter, + WorkspaceCleanupSizeFilter, + WorkspaceCleanupStatusFilter, + WorkspaceCleanupTicketFilter, + WorkspaceCleanupTriState +} from '../../../../shared/workspace-cleanup-filter-model' + +export const WORKSPACE_CLEANUP_DAY_MS = 24 * 60 * 60 * 1000 + +export function matchesWorkspaceCleanupTriState( + filter: WorkspaceCleanupTriState, + value: boolean +): boolean { + return filter === 'any' || (filter === 'only' ? value : !value) +} + +export function matchesWorkspaceCleanupPresence( + filter: WorkspaceCleanupPresence, + value: boolean +): boolean { + return filter === 'any' || (filter === 'some' ? value : !value) +} + +export function matchesWorkspaceCleanupActivity( + facets: WorkspaceCleanupFacets, + filter: WorkspaceCleanupActivityFilter, + now: number +): boolean { + if (filter.neverVisited && facets.lastVisitedAt !== null) { + return false + } + if (filter.idleMinDays === null) { + return true + } + const signalAt = getIdleSignalAt(facets, filter) + // Why: a missing signal means Orca has no evidence of recent use, so it + // reads as maximally idle rather than being filtered out. + return signalAt === null || now - signalAt >= filter.idleMinDays * WORKSPACE_CLEANUP_DAY_MS +} + +export function matchesWorkspaceCleanupSize( + facets: WorkspaceCleanupFacets, + filter: WorkspaceCleanupSizeFilter +): boolean { + if (facets.sizeBytes === null) { + return filter.includeUnsized + } + if (filter.minBytes !== null && facets.sizeBytes < filter.minBytes) { + return false + } + return filter.maxBytes === null || facets.sizeBytes <= filter.maxBytes +} + +export function matchesWorkspaceCleanupStatus( + facets: WorkspaceCleanupFacets, + filter: WorkspaceCleanupStatusFilter +): boolean { + if (!matchesWorkspaceStatusList(facets, filter)) { + return false + } + return ( + matchesWorkspaceCleanupTriState(filter.archived, facets.isArchived) && + matchesWorkspaceCleanupTriState(filter.pinned, facets.isPinned) && + matchesWorkspaceCleanupTriState(filter.unread, facets.isUnread) && + matchesWorkspaceCleanupTriState(filter.comment, facets.hasComment) + ) +} + +export function matchesWorkspaceCleanupAgent( + facets: WorkspaceCleanupFacets, + filter: WorkspaceCleanupAgentFilter +): boolean { + if (filter.states.length > 0 && !filter.states.includes(facets.agentState)) { + return false + } + return matchesWorkspaceCleanupTriState( + filter.retainedDoneAgents, + facets.retainedDoneAgentCount > 0 + ) +} + +export function matchesWorkspaceCleanupGit( + facets: WorkspaceCleanupFacets, + filter: WorkspaceCleanupGitFilterState +): boolean { + if (filter.states.length > 0 && !filter.states.includes(facets.gitState)) { + return false + } + if (filter.minAhead !== null && (facets.upstreamAhead ?? 0) < filter.minAhead) { + return false + } + if (filter.minBehind !== null && (facets.upstreamBehind ?? 0) < filter.minBehind) { + return false + } + const branchQuery = filter.branchQuery.trim().toLowerCase() + if (branchQuery && !facets.branch.toLowerCase().includes(branchQuery)) { + return false + } + return ( + matchesWorkspaceCleanupTriState(filter.prunable, facets.isPrunable) && + matchesWorkspaceCleanupTriState(filter.locked, facets.isLocked) + ) +} + +export function matchesWorkspaceCleanupReview( + facets: WorkspaceCleanupFacets, + filter: WorkspaceCleanupReviewFilterState +): boolean { + if (!matchesWorkspaceCleanupPresence(filter.presence, facets.review.hasReview)) { + return false + } + if (filter.states.length > 0) { + if (facets.reviewState === null || !filter.states.includes(facets.reviewState)) { + return false + } + } + if (filter.providers.length === 0) { + return true + } + return facets.review.provider !== null && filter.providers.includes(facets.review.provider) +} + +export function matchesWorkspaceCleanupTicket( + facets: WorkspaceCleanupFacets, + filter: WorkspaceCleanupTicketFilter +): boolean { + if (!matchesWorkspaceCleanupPresence(filter.presence, facets.ticketSources.length > 0)) { + return false + } + return ( + filter.sources.length === 0 || + filter.sources.some((source) => facets.ticketSources.includes(source)) + ) +} + +export function matchesWorkspaceCleanupContext( + facets: WorkspaceCleanupFacets, + filter: WorkspaceCleanupContextFilterState +): boolean { + if (!matchesWorkspaceCleanupPresence(filter.presence, facets.hasLocalContext)) { + return false + } + return !filter.completelyEmpty || facets.isCompletelyEmpty +} + +export function matchesWorkspaceCleanupLocation( + facets: WorkspaceCleanupFacets, + filter: WorkspaceCleanupLocationFilter +): boolean { + if (filter.hostIds.length > 0 && !filter.hostIds.includes(facets.hostId)) { + return false + } + if (filter.repoIds.length > 0 && !filter.repoIds.includes(facets.repoId)) { + return false + } + const prefix = filter.pathPrefix.trim() + return prefix.length === 0 || facets.path.startsWith(prefix) +} + +export function matchesWorkspaceCleanupSafety( + facets: WorkspaceCleanupFacets, + filter: WorkspaceCleanupSafetyFilter +): boolean { + if (filter.tiers.length > 0 && !filter.tiers.includes(facets.tier)) { + return false + } + if (!matchesWorkspaceCleanupTriState(filter.dismissed, facets.isDismissed)) { + return false + } + if (filter.selectableOnly && !facets.isSelectable) { + return false + } + if (filter.blockers.length === 0) { + return true + } + const hit = filter.blockers.some((blocker) => facets.blockers.includes(blocker)) + return filter.blockerMode === 'any-of' ? hit : !hit +} + +function matchesWorkspaceStatusList( + facets: WorkspaceCleanupFacets, + filter: WorkspaceCleanupStatusFilter +): boolean { + if (facets.workspaceStatus === null) { + return filter.matchStatusless + } + return ( + filter.workspaceStatuses.length === 0 || + filter.workspaceStatuses.includes(facets.workspaceStatus) + ) +} + +function getIdleSignalAt( + facets: WorkspaceCleanupFacets, + filter: WorkspaceCleanupActivityFilter +): number | null { + switch (filter.idleSignal) { + case 'last-visited': + return facets.lastVisitedAt + case 'last-activity': + return facets.lastActivityAt + case 'created': + return facets.createdAt + } +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-panel-model.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-panel-model.ts new file mode 100644 index 00000000000..60a097a00e1 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-panel-model.ts @@ -0,0 +1,46 @@ +import type { ExecutionHostId } from '../../../../shared/execution-host' +import type { HostedReviewProvider } from '../../../../shared/hosted-review' +import type { WorkspaceCleanupFilterState } from '../../../../shared/workspace-cleanup-filter-model' + +export type WorkspaceCleanupFacetCounts = Record< + keyof Omit, + number +> + +/** Facet values discovered from the loaded fleet, not a hardcoded enum. */ +export type WorkspaceCleanupFacetOptions = { + workspaceStatuses: readonly { id: string; label: string }[] + hostIds: readonly ExecutionHostId[] + repos: readonly { id: string; label: string }[] + reviewProviders: readonly HostedReviewProvider[] +} + +export type WorkspaceCleanupFacetGroupProps = { + filters: WorkspaceCleanupFilterState + counts: WorkspaceCleanupFacetCounts + totalCount: number + options: WorkspaceCleanupFacetOptions + onPatch: ( + key: K, + value: Partial | WorkspaceCleanupFilterState[K] + ) => void +} + +export const WORKSPACE_CLEANUP_MEGABYTE = 1024 * 1024 + +export function toMegabytes(bytes: number | null): number | null { + return bytes === null ? null : Math.round(bytes / WORKSPACE_CLEANUP_MEGABYTE) +} + +export function fromMegabytes(megabytes: number | null): number | null { + return megabytes === null ? null : Math.round(megabytes * WORKSPACE_CLEANUP_MEGABYTE) +} + +export function parseWorkspaceCleanupFacetNumber(raw: string): number | null { + const trimmed = raw.trim() + if (trimmed === '') { + return null + } + const parsed = Number(trimmed) + return Number.isFinite(parsed) && parsed >= 0 ? parsed : null +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-row-caches.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-row-caches.ts new file mode 100644 index 00000000000..e6949e35e57 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-row-caches.ts @@ -0,0 +1,143 @@ +import type { LiveAgentWorktreeStatus } from '@/lib/worktree-activity-state' +import type { WorkspaceStatusDefinition } from '../../../../shared/worktree/types' +import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' +import { + buildWorkspaceCleanupFacets, + type WorkspaceCleanupFacetSources, + type WorkspaceCleanupFacets +} from './workspace-cleanup-facets' +import { + getWorkspaceCleanupReviewInfo, + type WorkspaceCleanupRendererStateInputs, + type WorkspaceCleanupReviewInfo, + type WorkspaceCleanupReviewLookup +} from './workspace-cleanup-presentation' +import { + getWorkspaceCleanupCandidateHostId, + getWorkspaceCleanupHostIdentity +} from './workspace-cleanup-host-identity' + +/** + * Streaming ticks replace only the candidate objects they touched, so all + * per-candidate work here is cached on candidate object identity; unchanged + * rows reuse their previous facet/review objects and the row array itself + * keeps identity when nothing changed, which is what lets React.memo and + * downstream memos hold. + * + * The caches are owned by useMemo keyed on their invalidation inputs (never + * refs — ref writes during render are unsafe under concurrent rendering). + * Filling them during render is safe because they are content-addressed and + * idempotent: a replayed or discarded render stores the same value for the + * same key. + */ +export type WorkspaceCleanupReviewInfoCache = WeakMap< + WorkspaceCleanupCandidate, + WorkspaceCleanupReviewInfo +> + +export function createWorkspaceCleanupReviewInfoCache(): WorkspaceCleanupReviewInfoCache { + return new WeakMap() +} + +export function computeWorkspaceCleanupReviewInfoIndex(args: { + candidates: readonly WorkspaceCleanupCandidate[] + candidateIdCounts: ReadonlyMap + reviewSources: WorkspaceCleanupRendererStateInputs + reviewLookup: WorkspaceCleanupReviewLookup + cache: WorkspaceCleanupReviewInfoCache +}): Map { + const { candidates, candidateIdCounts, reviewSources, reviewLookup, cache } = args + const infos = new Map() + for (const candidate of candidates) { + let info = cache.get(candidate) + if (info === undefined) { + info = getWorkspaceCleanupReviewInfo(candidate, reviewSources, reviewLookup) + cache.set(candidate, info) + } + infos.set( + getWorkspaceCleanupHostIdentity( + getWorkspaceCleanupCandidateHostId(candidate), + candidate.worktreeId + ), + info + ) + if (candidateIdCounts.get(candidate.worktreeId) === 1) { + infos.set(candidate.worktreeId, info) + } + } + return infos +} + +type FacetCacheEntry = { + facet: WorkspaceCleanupFacets + sizeBytes: number | null | undefined + lastVisitedAt: number | undefined + agentState: LiveAgentWorktreeStatus | undefined + review: WorkspaceCleanupReviewInfo | undefined + isDismissed: boolean +} + +export type WorkspaceCleanupFacetListCache = { + byCandidate: WeakMap + lastList: WorkspaceCleanupFacets[] +} + +export function createWorkspaceCleanupFacetListCache(): WorkspaceCleanupFacetListCache { + return { byCandidate: new WeakMap(), lastList: [] } +} + +export function computeWorkspaceCleanupFacetList(args: { + candidates: readonly WorkspaceCleanupCandidate[] + sources: Required> & { + workspaceStatuses: readonly WorkspaceStatusDefinition[] + } + cache: WorkspaceCleanupFacetListCache +}): WorkspaceCleanupFacets[] { + const { candidates, sources, cache } = args + const list = candidates.map((candidate) => { + const hostIdentity = getWorkspaceCleanupHostIdentity( + getWorkspaceCleanupCandidateHostId(candidate), + candidate.worktreeId + ) + // Per-id projections mirror buildWorkspaceCleanupFacets' lookups; the + // cached facet is valid only while every projected input is unchanged. + const sizeBytes = + sources.sizeBytesByWorktreeId.get(hostIdentity) ?? + sources.sizeBytesByWorktreeId.get(candidate.worktreeId) + const lastVisitedAt = sources.lastVisitedAtByWorktreeId[candidate.worktreeId] + const agentState = sources.liveAgentStatusByWorktreeId.get(candidate.worktreeId) + const review = + sources.reviewInfoByWorktreeId.get(hostIdentity) ?? + sources.reviewInfoByWorktreeId.get(candidate.worktreeId) + const isDismissed = sources.dismissedWorktreeIds.has(candidate.worktreeId) + const cached = cache.byCandidate.get(candidate) + if ( + cached && + cached.sizeBytes === sizeBytes && + cached.lastVisitedAt === lastVisitedAt && + cached.agentState === agentState && + cached.review === review && + cached.isDismissed === isDismissed + ) { + return cached.facet + } + const facet = buildWorkspaceCleanupFacets(candidate, sources) + cache.byCandidate.set(candidate, { + facet, + sizeBytes, + lastVisitedAt, + agentState, + review, + isDismissed + }) + return facet + }) + // Why: reusing the previous array identity when no row changed lets every + // downstream memo skip its O(N) pass on no-op streaming ticks. + const previous = cache.lastList + if (previous.length === list.length && list.every((facet, index) => facet === previous[index])) { + return previous + } + cache.lastList = list + return list +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-sort.test.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-sort.test.ts new file mode 100644 index 00000000000..4da525a1352 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-sort.test.ts @@ -0,0 +1,140 @@ +import { describe, expect, it } from 'vitest' +import { DAY, FACET_NOW, makeNamedFacets } from './workspace-cleanup-facet.test.fixture' +import { + isWorkspaceCleanupAbsentLastSortField, + sortWorkspaceCleanupFacets +} from './workspace-cleanup-facet-sort' +import type { WorkspaceCleanupSortField } from '../../../../shared/workspace-cleanup-filter-model' + +function names( + rows: ReturnType[], + field: WorkspaceCleanupSortField, + direction: 'asc' | 'desc' +): string[] { + return sortWorkspaceCleanupFacets(rows, { field, direction }).map((row) => row.displayName) +} + +describe('absent values', () => { + it('sinks unsized workspaces to the bottom in both directions', () => { + const rows = [ + makeNamedFacets('unsized'), + makeNamedFacets('small', { sizeBytes: 10 }), + makeNamedFacets('big', { sizeBytes: 900 }) + ] + expect(names(rows, 'size', 'desc')).toEqual(['big', 'small', 'unsized']) + expect(names(rows, 'size', 'asc')).toEqual(['small', 'big', 'unsized']) + }) + + it('sinks never-visited workspaces to the bottom in both directions', () => { + const rows = [ + makeNamedFacets('never'), + makeNamedFacets('old', { lastVisitedAt: FACET_NOW - 50 * DAY }), + makeNamedFacets('recent', { lastVisitedAt: FACET_NOW }) + ] + expect(names(rows, 'last-visited', 'asc')).toEqual(['old', 'recent', 'never']) + expect(names(rows, 'last-visited', 'desc')).toEqual(['recent', 'old', 'never']) + }) + + it('sinks review-less and ticket-less workspaces to the bottom', () => { + const rows = [ + makeNamedFacets('none'), + makeNamedFacets('merged', { review: { state: 'merged' } }), + makeNamedFacets('open', { review: { state: 'open' } }) + ] + expect(names(rows, 'review', 'desc')).toEqual(['open', 'merged', 'none']) + + const ticketRows = [ + makeNamedFacets('untracked'), + makeNamedFacets('linear', { worktree: { linkedLinearIssue: 'STA-1' } }) + ] + expect(names(ticketRows, 'ticket', 'asc')).toEqual(['linear', 'untracked']) + }) + + it('marks exactly the optional-value fields as absent-last', () => { + expect(isWorkspaceCleanupAbsentLastSortField('size')).toBe(true) + expect(isWorkspaceCleanupAbsentLastSortField('name')).toBe(false) + }) +}) + +describe('ranked fields', () => { + it('orders git states from safest to riskiest', () => { + const rows = [ + makeNamedFacets('unpushed', { + candidate: { git: { clean: true, upstreamAhead: 2, upstreamBehind: 0, checkedAt: 1 } } + }), + makeNamedFacets('clean'), + makeNamedFacets('dirty', { + candidate: { git: { clean: false, upstreamAhead: 0, upstreamBehind: 0, checkedAt: 1 } } + }), + makeNamedFacets('unknown', { + candidate: { git: { clean: null, upstreamAhead: 0, upstreamBehind: 0, checkedAt: null } } + }) + ] + expect(names(rows, 'git', 'asc')).toEqual(['clean', 'unknown', 'dirty', 'unpushed']) + }) + + it('orders tiers ready to protected and agents idle to permission', () => { + const tierRows = [ + makeNamedFacets('protectedRow', { candidate: { tier: 'protected' } }), + makeNamedFacets('readyRow', { candidate: { tier: 'ready' } }), + makeNamedFacets('reviewRow', { candidate: { tier: 'review' } }) + ] + expect(names(tierRows, 'tier', 'asc')).toEqual(['readyRow', 'reviewRow', 'protectedRow']) + + const agentRows = [ + makeNamedFacets('permission', { agentStatus: 'permission' }), + makeNamedFacets('idle'), + makeNamedFacets('working', { agentStatus: 'working' }) + ] + expect(names(agentRows, 'agent', 'desc')).toEqual(['permission', 'working', 'idle']) + }) + + it('breaks equal blocker counts by the worst blocker', () => { + const rows = [ + makeNamedFacets('softest', { candidate: { blockers: ['git-status-error'] } }), + makeNamedFacets('hardest', { candidate: { blockers: ['main-worktree'] } }) + ] + expect(names(rows, 'blocker-count', 'desc')).toEqual(['hardest', 'softest']) + }) +}) + +describe('tie-breaks', () => { + it('falls back to activity, repo, name, then worktree id', () => { + const rows = [ + makeNamedFacets('zeta', { candidate: { lastActivityAt: FACET_NOW - DAY } }), + makeNamedFacets('alpha', { candidate: { lastActivityAt: FACET_NOW - 2 * DAY } }) + ] + // Same size on both sides, so only the tie-break chain can order them. + expect(names(rows, 'size', 'desc')).toEqual(['alpha', 'zeta']) + }) + + it('keeps tie-broken rows in the same order when direction flips', () => { + const rows = [ + makeNamedFacets('b', { candidate: { lastActivityAt: FACET_NOW - DAY } }), + makeNamedFacets('a', { candidate: { lastActivityAt: FACET_NOW - 2 * DAY } }) + ] + expect(names(rows, 'git', 'asc')).toEqual(names(rows, 'git', 'desc')) + }) + + it('does not mutate the input array', () => { + const rows = [makeNamedFacets('b'), makeNamedFacets('a')] + const before = rows.map((row) => row.displayName) + sortWorkspaceCleanupFacets(rows, { field: 'name', direction: 'asc' }) + expect(rows.map((row) => row.displayName)).toEqual(before) + }) +}) + +describe('text fields', () => { + it('sorts by name, repo, path, branch, and host', () => { + const rows = [makeNamedFacets('charlie'), makeNamedFacets('alpha'), makeNamedFacets('bravo')] + expect(names(rows, 'name', 'asc')).toEqual(['alpha', 'bravo', 'charlie']) + expect(names(rows, 'path', 'desc')).toEqual(['charlie', 'bravo', 'alpha']) + expect(names(rows, 'branch', 'asc')).toEqual(['alpha', 'bravo', 'charlie']) + + const hostRows = [ + makeNamedFacets('remote', { worktree: { hostId: 'ssh:builder' } }), + makeNamedFacets('here', { worktree: { hostId: 'local' } }) + ] + expect(names(hostRows, 'host', 'asc')).toEqual(['here', 'remote']) + }) +}) diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-sort.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-sort.ts new file mode 100644 index 00000000000..50787aa6bd9 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-sort.ts @@ -0,0 +1,190 @@ +import type { WorkspaceCleanupFacets } from './workspace-cleanup-facets' +import { + getWorkspaceCleanupAgentRank, + getWorkspaceCleanupBlockerSeverity, + getWorkspaceCleanupGitRank, + getWorkspaceCleanupReviewRank, + getWorkspaceCleanupTierRank +} from '../../../../shared/workspace-cleanup-facet-rankings' +import type { + WorkspaceCleanupSortDirectionState, + WorkspaceCleanupSortField, + WorkspaceCleanupSortState +} from '../../../../shared/workspace-cleanup-filter-model' + +/** + * Fields whose value can be absent. Absent rows always sink to the bottom in + * BOTH directions — flipping direction should reorder the rows that carry the + * signal, not promote the ones that carry nothing. + */ +const ABSENT_LAST_FIELDS: ReadonlySet = new Set([ + 'size', + 'last-visited', + 'created', + 'ahead', + 'behind', + 'workspace-status', + 'review', + 'ticket' +]) + +export function isWorkspaceCleanupAbsentLastSortField(field: WorkspaceCleanupSortField): boolean { + return ABSENT_LAST_FIELDS.has(field) +} + +export function sortWorkspaceCleanupFacets( + facets: readonly WorkspaceCleanupFacets[], + sort: WorkspaceCleanupSortState +): WorkspaceCleanupFacets[] { + const multiplier = sort.direction === 'asc' ? 1 : -1 + return facets.toSorted( + (left, right) => + comparePrimary(left, right, sort.field, multiplier) || compareTieBreaks(left, right) + ) +} + +export function compareWorkspaceCleanupFacets( + left: WorkspaceCleanupFacets, + right: WorkspaceCleanupFacets, + field: WorkspaceCleanupSortField, + direction: WorkspaceCleanupSortDirectionState +): number { + const multiplier = direction === 'asc' ? 1 : -1 + return comparePrimary(left, right, field, multiplier) || compareTieBreaks(left, right) +} + +function comparePrimary( + left: WorkspaceCleanupFacets, + right: WorkspaceCleanupFacets, + field: WorkspaceCleanupSortField, + multiplier: number +): number { + const absence = compareAbsence(left, right, field) + if (absence !== 0) { + return absence + } + return compareNatural(left, right, field) * multiplier +} + +/** Non-zero only when exactly one side is absent for an absent-last field. */ +function compareAbsence( + left: WorkspaceCleanupFacets, + right: WorkspaceCleanupFacets, + field: WorkspaceCleanupSortField +): number { + if (!ABSENT_LAST_FIELDS.has(field)) { + return 0 + } + const leftAbsent = isAbsent(left, field) + const rightAbsent = isAbsent(right, field) + if (leftAbsent === rightAbsent) { + return 0 + } + return leftAbsent ? 1 : -1 +} + +function isAbsent(facets: WorkspaceCleanupFacets, field: WorkspaceCleanupSortField): boolean { + if (field === 'size') { + return facets.sizeBytes === null + } + if (field === 'last-visited') { + return facets.lastVisitedAt === null + } + if (field === 'created') { + return facets.createdAt === null + } + if (field === 'ahead') { + return facets.upstreamAhead === null + } + if (field === 'behind') { + return facets.upstreamBehind === null + } + if (field === 'workspace-status') { + return facets.workspaceStatus === null + } + if (field === 'review') { + return !facets.review.hasReview + } + if (field === 'ticket') { + return facets.ticketSources.length === 0 + } + return false +} + +function compareNatural( + left: WorkspaceCleanupFacets, + right: WorkspaceCleanupFacets, + field: WorkspaceCleanupSortField +): number { + switch (field) { + case 'last-activity': + return left.lastActivityAt - right.lastActivityAt + case 'last-visited': + return (left.lastVisitedAt ?? 0) - (right.lastVisitedAt ?? 0) + case 'created': + return (left.createdAt ?? 0) - (right.createdAt ?? 0) + case 'size': + return (left.sizeBytes ?? 0) - (right.sizeBytes ?? 0) + case 'name': + return left.displayName.localeCompare(right.displayName) + case 'repo': + return left.repoName.localeCompare(right.repoName) + case 'path': + return left.path.localeCompare(right.path) + case 'host': + return left.hostId.localeCompare(right.hostId) + case 'workspace-status': + return (left.workspaceStatus ?? '').localeCompare(right.workspaceStatus ?? '') + case 'agent': + return ( + getWorkspaceCleanupAgentRank(left.agentState) - + getWorkspaceCleanupAgentRank(right.agentState) + ) + case 'git': + return getWorkspaceCleanupGitRank(left.gitState) - getWorkspaceCleanupGitRank(right.gitState) + case 'ahead': + return (left.upstreamAhead ?? 0) - (right.upstreamAhead ?? 0) + case 'behind': + return (left.upstreamBehind ?? 0) - (right.upstreamBehind ?? 0) + case 'branch': + return left.branch.localeCompare(right.branch) + case 'review': + return ( + getWorkspaceCleanupReviewRank(left.reviewState) - + getWorkspaceCleanupReviewRank(right.reviewState) || + (left.review.label ?? '').localeCompare(right.review.label ?? '') + ) + case 'ticket': + return ( + left.ticketSources.length - right.ticketSources.length || + (left.ticketSources[0] ?? '').localeCompare(right.ticketSources[0] ?? '') + ) + case 'local-context': + return left.localContextCount - right.localContextCount + case 'tier': + return getWorkspaceCleanupTierRank(left.tier) - getWorkspaceCleanupTierRank(right.tier) + case 'blocker-count': + return ( + left.blockerCount - right.blockerCount || + getMaxBlockerSeverity(left) - getMaxBlockerSeverity(right) + ) + } +} + +/** Direction-independent so a flipped sort never reshuffles equal rows. */ +function compareTieBreaks(left: WorkspaceCleanupFacets, right: WorkspaceCleanupFacets): number { + return ( + left.lastActivityAt - right.lastActivityAt || + left.repoName.localeCompare(right.repoName) || + left.displayName.localeCompare(right.displayName) || + left.worktreeId.localeCompare(right.worktreeId) + ) +} + +function getMaxBlockerSeverity(facets: WorkspaceCleanupFacets): number { + let max = 0 + for (const blocker of facets.blockers) { + max = Math.max(max, getWorkspaceCleanupBlockerSeverity(blocker)) + } + return max +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet.test.fixture.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet.test.fixture.ts new file mode 100644 index 00000000000..07e38b48141 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet.test.fixture.ts @@ -0,0 +1,126 @@ +import type { LiveAgentWorktreeStatus } from '@/lib/worktree-activity-state' +import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' +import { cloneDefaultWorkspaceStatuses } from '../../../../shared/workspace-statuses' +import { + buildWorkspaceCleanupFacets, + type WorkspaceCleanupFacetSources, + type WorkspaceCleanupFacets, + type WorkspaceCleanupWorktreeFacts +} from './workspace-cleanup-facets' +import type { WorkspaceCleanupReviewInfo } from './workspace-cleanup-presentation' + +export const FACET_NOW = 1_700_000_000_000 +export const DAY = 24 * 60 * 60 * 1000 + +export function makeFacetCandidate( + overrides: Partial = {} +): WorkspaceCleanupCandidate { + return { + worktreeId: 'repo-1::/repo/alpha', + repoId: 'repo-1', + repoName: 'Repo', + connectionId: null, + displayName: 'alpha', + branch: 'refs/heads/alpha', + path: '/repo/alpha', + tier: 'ready', + selectedByDefault: true, + reasons: ['idle-clean'], + blockers: [], + lastActivityAt: FACET_NOW - 40 * DAY, + localContext: { + terminalTabCount: 0, + cleanEditorTabCount: 0, + browserTabCount: 0, + diffCommentCount: 0, + newestDiffCommentAt: null, + retainedDoneAgentCount: 0 + }, + git: { clean: true, upstreamAhead: 0, upstreamBehind: 0, checkedAt: FACET_NOW }, + fingerprint: 'fingerprint', + ...overrides + } +} + +export function makeWorktreeFacts( + overrides: Partial = {} +): WorkspaceCleanupWorktreeFacts { + return { + id: 'repo-1::/repo/alpha', + branch: 'refs/heads/alpha', + comment: '', + isArchived: false, + isPinned: false, + isUnread: false, + linkedIssue: null, + linkedLinearIssue: null, + ...overrides + } +} + +export function makeReviewInfo( + overrides: Partial = {} +): WorkspaceCleanupReviewInfo { + return { + hasReview: true, + label: 'PR #42', + state: 'open', + provider: 'github', + title: 'Alpha cleanup', + ...overrides + } +} + +export type FacetFixtureInput = { + candidate?: Partial + worktree?: Partial | null + sizeBytes?: number + lastVisitedAt?: number + agentStatus?: LiveAgentWorktreeStatus + review?: Partial + dismissed?: boolean +} + +/** Builds through the real facet builder so tests exercise the production join. */ +export function makeFacets(input: FacetFixtureInput = {}): WorkspaceCleanupFacets { + const candidate = makeFacetCandidate(input.candidate) + const id = candidate.worktreeId + const sources: WorkspaceCleanupFacetSources = { + workspaceStatuses: cloneDefaultWorkspaceStatuses(), + worktreeById: + input.worktree === null + ? new Map() + : new Map([[id, makeWorktreeFacts({ id, ...input.worktree })]]), + sizeBytesByWorktreeId: + input.sizeBytes === undefined ? new Map() : new Map([[id, input.sizeBytes]]), + lastVisitedAtByWorktreeId: + input.lastVisitedAt === undefined ? {} : { [id]: input.lastVisitedAt }, + liveAgentStatusByWorktreeId: + input.agentStatus === undefined ? new Map() : new Map([[id, input.agentStatus]]), + reviewInfoByWorktreeId: + input.review === undefined ? new Map() : new Map([[id, makeReviewInfo(input.review)]]), + dismissedWorktreeIds: new Set(input.dismissed ? [id] : []) + } + return buildWorkspaceCleanupFacets(candidate, sources) +} + +/** Distinct worktree/display identity so tie-break ordering is observable. */ +export function makeNamedFacets( + name: string, + input: FacetFixtureInput = {} +): WorkspaceCleanupFacets { + return makeFacets({ + ...input, + candidate: { + worktreeId: `repo-1::/repo/${name}`, + displayName: name, + path: `/repo/${name}`, + branch: `refs/heads/${name}`, + ...input.candidate + }, + worktree: + input.worktree === null + ? null + : { id: `repo-1::/repo/${name}`, branch: `refs/heads/${name}`, ...input.worktree } + }) +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facets.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facets.ts new file mode 100644 index 00000000000..7207931377f --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-facets.ts @@ -0,0 +1,239 @@ +import type { LiveAgentWorktreeStatus } from '@/lib/worktree-activity-state' +import type { ExecutionHostId } from '../../../../shared/execution-host' +import type { WorkspaceStatusDefinition } from '../../../../shared/worktree/types' +import { getWorkspaceStatus } from '../../../../shared/workspace-statuses' +import { + canSelectWorkspaceCleanupCandidate, + type WorkspaceCleanupBlocker, + type WorkspaceCleanupCandidate, + type WorkspaceCleanupTier +} from '../../../../shared/workspace-cleanup' +import { getWorkspaceCleanupGitState } from './workspace-cleanup-filter-sort' +import type { + WorkspaceCleanupAgentState, + WorkspaceCleanupGitState, + WorkspaceCleanupReviewState, + WorkspaceCleanupTicketSource +} from '../../../../shared/workspace-cleanup-filter-model' +import type { WorkspaceCleanupReviewInfo } from './workspace-cleanup-presentation' +import { + getWorkspaceCleanupCandidateHostId, + getWorkspaceCleanupHostIdentity, + type WorkspaceCleanupWorktreeFacts +} from './workspace-cleanup-host-identity' +export type { WorkspaceCleanupWorktreeFacts } from './workspace-cleanup-host-identity' + +export type WorkspaceCleanupFacetSources = { + worktreeById?: ReadonlyMap + workspaceStatuses?: readonly WorkspaceStatusDefinition[] + /** Absent entry = the space scan never ran for that worktree. */ + sizeBytesByWorktreeId?: ReadonlyMap + lastVisitedAtByWorktreeId?: Readonly> + liveAgentStatusByWorktreeId?: ReadonlyMap + reviewInfoByWorktreeId?: ReadonlyMap + dismissedWorktreeIds?: ReadonlySet +} + +export type WorkspaceCleanupFacets = { + candidate: WorkspaceCleanupCandidate + worktreeId: string + repoId: string + repoName: string + displayName: string + path: string + branch: string + hostId: ExecutionHostId + tier: WorkspaceCleanupTier + blockers: readonly WorkspaceCleanupBlocker[] + blockerCount: number + isDismissed: boolean + isSelectable: boolean + /** Background signal: ambient PTY/agent churn bumps this without a human. */ + lastActivityAt: number + createdAt: number | null + /** Honest "user opened it" signal; null when Orca never recorded a visit. */ + lastVisitedAt: number | null + sizeBytes: number | null + workspaceStatus: string | null + workspaceStatusLabel: string | null + isArchived: boolean + isPinned: boolean + isUnread: boolean + hasComment: boolean + agentState: WorkspaceCleanupAgentState + retainedDoneAgentCount: number + gitState: WorkspaceCleanupGitState + upstreamAhead: number | null + upstreamBehind: number | null + isPrunable: boolean + isLocked: boolean + review: WorkspaceCleanupReviewInfo + reviewState: WorkspaceCleanupReviewState | null + ticketSources: readonly WorkspaceCleanupTicketSource[] + localContextCount: number + hasLocalContext: boolean + isCompletelyEmpty: boolean + searchText: string +} + +const EMPTY_REVIEW_INFO: WorkspaceCleanupReviewInfo = { + hasReview: false, + label: null, + state: null, + provider: null, + title: null +} + +export function buildWorkspaceCleanupFacets( + candidate: WorkspaceCleanupCandidate, + sources: WorkspaceCleanupFacetSources = {} +): WorkspaceCleanupFacets { + const hostIdentity = getWorkspaceCleanupHostIdentity( + getWorkspaceCleanupCandidateHostId(candidate), + candidate.worktreeId + ) + const worktree = + sources.worktreeById?.get(hostIdentity) ?? + sources.worktreeById?.get(candidate.worktreeId) ?? + null + const review = + sources.reviewInfoByWorktreeId?.get(hostIdentity) ?? + sources.reviewInfoByWorktreeId?.get(candidate.worktreeId) ?? + EMPTY_REVIEW_INFO + const ticketSources = getTicketSources(worktree) + const localContextCount = getLocalContextCount(candidate) + const hasComment = (worktree?.comment ?? '').trim().length > 0 + const branch = getBranchDisplayName(worktree?.branch ?? candidate.branch) + const facets: Omit = { + candidate, + worktreeId: candidate.worktreeId, + repoId: candidate.repoId, + repoName: candidate.repoName, + displayName: candidate.displayName, + path: candidate.path, + branch, + hostId: worktree?.hostId ?? getWorkspaceCleanupCandidateHostId(candidate), + tier: candidate.tier, + blockers: candidate.blockers, + blockerCount: candidate.blockers.length, + isDismissed: + (sources.dismissedWorktreeIds?.has(candidate.worktreeId) ?? false) || + candidate.blockers.includes('dismissed'), + isSelectable: canSelectWorkspaceCleanupCandidate(candidate), + lastActivityAt: candidate.lastActivityAt, + createdAt: toFiniteOrNull(worktree?.createdAt ?? candidate.createdAt), + lastVisitedAt: toFiniteOrNull(sources.lastVisitedAtByWorktreeId?.[candidate.worktreeId]), + sizeBytes: toFiniteOrNull( + sources.sizeBytesByWorktreeId?.get(hostIdentity) ?? + sources.sizeBytesByWorktreeId?.get(candidate.worktreeId) + ), + workspaceStatus: normalizeStatus(worktree?.workspaceStatus), + workspaceStatusLabel: getWorkspaceStatusLabel(worktree, sources.workspaceStatuses), + isArchived: worktree?.isArchived ?? candidate.reasons.includes('archived'), + isPinned: worktree?.isPinned ?? candidate.blockers.includes('pinned'), + isUnread: worktree?.isUnread ?? false, + hasComment, + agentState: sources.liveAgentStatusByWorktreeId?.get(candidate.worktreeId) ?? 'idle', + retainedDoneAgentCount: candidate.localContext.retainedDoneAgentCount, + gitState: getWorkspaceCleanupGitState(candidate), + upstreamAhead: toFiniteOrNull(candidate.git.upstreamAhead), + upstreamBehind: toFiniteOrNull(candidate.git.upstreamBehind), + isPrunable: worktree?.prunable ?? false, + isLocked: worktree?.locked ?? false, + review, + reviewState: review.hasReview ? (review.state ?? 'unknown') : null, + ticketSources, + localContextCount, + hasLocalContext: localContextCount > 0, + isCompletelyEmpty: + localContextCount === 0 && !review.hasReview && ticketSources.length === 0 && !hasComment + } + return { ...facets, searchText: buildSearchText(facets) } +} + +export function buildWorkspaceCleanupFacetList( + candidates: readonly WorkspaceCleanupCandidate[], + sources: WorkspaceCleanupFacetSources = {} +): WorkspaceCleanupFacets[] { + return candidates.map((candidate) => buildWorkspaceCleanupFacets(candidate, sources)) +} + +export function countWorkspaceCleanupMeasuredRows(rows: readonly WorkspaceCleanupFacets[]): number { + return rows.reduce((count, row) => count + (row.sizeBytes === null ? 0 : 1), 0) +} + +function getLocalContextCount(candidate: WorkspaceCleanupCandidate): number { + const context = candidate.localContext + return ( + context.terminalTabCount + + context.cleanEditorTabCount + + context.browserTabCount + + context.diffCommentCount + + context.retainedDoneAgentCount + ) +} + +function getTicketSources( + worktree: WorkspaceCleanupWorktreeFacts | null +): WorkspaceCleanupTicketSource[] { + if (!worktree) { + return [] + } + const sources: WorkspaceCleanupTicketSource[] = [] + if (worktree.linkedWorkItem != null) { + sources.push('work-item') + } + if ((worktree.linkedLinearIssue ?? '').length > 0) { + sources.push('linear') + } + if (worktree.linkedIssue != null) { + sources.push('issue') + } + return sources +} + +function buildSearchText(facets: Omit): string { + return [ + facets.displayName, + facets.repoName, + facets.branch, + facets.path, + facets.hostId, + facets.workspaceStatus, + facets.workspaceStatusLabel, + facets.review.label, + facets.review.title, + facets.review.provider, + facets.gitState, + facets.tier, + ...facets.ticketSources, + ...facets.blockers + ] + .filter((value): value is string => Boolean(value)) + .join(' ') + .toLowerCase() +} + +function normalizeStatus(status: string | undefined): string | null { + const trimmed = (status ?? '').trim() + return trimmed.length > 0 ? trimmed : null +} + +function getWorkspaceStatusLabel( + worktree: WorkspaceCleanupWorktreeFacts | null, + statuses: readonly WorkspaceStatusDefinition[] | undefined +): string | null { + if (!worktree || !statuses?.length) { + return null + } + const statusId = getWorkspaceStatus({ workspaceStatus: worktree.workspaceStatus }, statuses) + return statuses.find((status) => status.id === statusId)?.label ?? statusId +} + +function toFiniteOrNull(value: number | null | undefined): number | null { + return typeof value === 'number' && Number.isFinite(value) ? value : null +} + +function getBranchDisplayName(branch: string): string { + return branch.replace(/^refs\/heads\//, '') || 'HEAD' +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-filter-bar.test.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-filter-bar.test.tsx new file mode 100644 index 00000000000..8575c3f710c --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-filter-bar.test.tsx @@ -0,0 +1,44 @@ +// @vitest-environment happy-dom +import { cleanup, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { createDefaultWorkspaceCleanupFilterState } from '../../../../shared/workspace-cleanup-filter-model' +import { WorkspaceCleanupFilterBar } from './workspace-cleanup-filter-bar' + +afterEach(cleanup) + +describe('WorkspaceCleanupFilterBar', () => { + it('keeps size measurement out of the browse controls', () => { + render( + + ) + + expect(screen.queryByRole('button', { name: 'Scan' })).toBeNull() + }) +}) diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-filter-bar.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-filter-bar.tsx new file mode 100644 index 00000000000..5cb4e389af5 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-filter-bar.tsx @@ -0,0 +1,111 @@ +import React from 'react' +import { Loader2, RotateCcw, Search, SlidersHorizontal } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { ScrollArea } from '@/components/ui/scroll-area' +import { translate } from '@/i18n/i18n' +import { WorkspaceCleanupGitReviewFacets } from './workspace-cleanup-git-review-facets' +import { WorkspaceCleanupLifecycleFacets } from './workspace-cleanup-lifecycle-facets' +import type { WorkspaceCleanupFacetGroupProps } from './workspace-cleanup-facet-panel-model' + +export type WorkspaceCleanupGitEvidenceProgress = { + pendingCount: number + totalCount: number +} + +export function WorkspaceCleanupFilterBar({ + facetProps, + facetPanelOpen, + onFacetPanelOpenChange, + activeFacetGroupCount, + matchedCount, + hasActiveFilters, + gitEvidence, + onQueryChange, + onClearFilters +}: { + facetProps: WorkspaceCleanupFacetGroupProps + facetPanelOpen: boolean + onFacetPanelOpenChange: (open: boolean) => void + activeFacetGroupCount: number + matchedCount: number + hasActiveFilters: boolean + gitEvidence: WorkspaceCleanupGitEvidenceProgress + onQueryChange: (query: string) => void + onClearFilters: () => void +}): React.JSX.Element { + const { filters, totalCount } = facetProps + return ( +
+
+ + onQueryChange(event.target.value)} + placeholder={translate( + 'components.workspace.cleanup.browse.searchPlaceholder', + 'Search name, repo, branch, path, host' + )} + aria-label={translate( + 'components.workspace.cleanup.browse.searchLabel', + 'Search workspaces' + )} + className="h-8 pl-8 text-xs" + /> +
+ + + + + + + + + + +
+ + {translate( + 'components.workspace.cleanup.browse.showingCount', + 'Showing {{value0}} of {{value1}}', + { value0: matchedCount, value1: totalCount } + )} + + +
+
+
+ + + {translate( + 'components.workspace.cleanup.browse.showingCount', + 'Showing {{value0}} of {{value1}}', + { value0: matchedCount, value1: totalCount } + )} + + + {gitEvidence.pendingCount > 0 ? ( + + + {translate( + 'components.workspace.cleanup.browse.checkingGit', + 'Checking git status: {{value0}} left', + { value0: gitEvidence.pendingCount } + )} + + ) : null} +
+ ) +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-filter-sort.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-filter-sort.ts index ff597290bfd..e839de0c288 100644 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-filter-sort.ts +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-filter-sort.ts @@ -1,4 +1,5 @@ import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' +import type { WorkspaceCleanupGitState } from '../../../../shared/workspace-cleanup-filter-model' import type { WorkspaceCleanupContextFilter, WorkspaceCleanupFilters, @@ -94,20 +95,35 @@ export function sortWorkspaceCleanupCandidates( }) } +const GIT_STATE_LABELS: Record = { + unpushed: 'Unpushed', + unknown: 'Unknown', + clean: 'Clean', + dirty: 'Dirty' +} + export function getWorkspaceCleanupGitLabel(candidate: WorkspaceCleanupCandidate): string { + return GIT_STATE_LABELS[getWorkspaceCleanupGitState(candidate)] +} + +/** Unpushed outranks unknown: a lost commit is worse than an unreadable status. */ +export function getWorkspaceCleanupGitState( + candidate: WorkspaceCleanupCandidate +): WorkspaceCleanupGitState { if (hasUnpushedCommits(candidate)) { - return 'Unpushed' + return 'unpushed' } if (isGitStatusUnknown(candidate)) { - return 'Unknown' + return 'unknown' } if (candidate.git.clean === true) { - return 'Clean' + return 'clean' } - if (candidate.git.clean === false) { - return 'Dirty' - } - return 'Unknown' + return candidate.git.clean === false ? 'dirty' : 'unknown' +} + +export function hasWorkspaceCleanupUnpushedCommits(candidate: WorkspaceCleanupCandidate): boolean { + return hasUnpushedCommits(candidate) } function compareWorkspaceCleanupCandidates( diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-flat-list.test.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-flat-list.test.tsx new file mode 100644 index 00000000000..bbbeaac5e73 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-flat-list.test.tsx @@ -0,0 +1,139 @@ +// @vitest-environment happy-dom +import type { ReactNode } from 'react' +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + createDefaultWorkspaceCleanupFilterState, + DEFAULT_WORKSPACE_CLEANUP_SORT, + type WorkspaceCleanupFilterState +} from '../../../../shared/workspace-cleanup-filter-model' +import { CandidateRow } from './workspace-cleanup-candidate-row' +import { WorkspaceCleanupCandidateList } from './workspace-cleanup-candidate-list' +import { FACET_NOW, makeNamedFacets } from './workspace-cleanup-facet.test.fixture' +import { runWorkspaceCleanupQuery } from './workspace-cleanup-query' +import type { WorkspaceCleanupFacets } from './workspace-cleanup-facets' + +vi.mock('@/components/ui/tooltip', () => ({ + Tooltip: ({ children }: { children: ReactNode }) => <>{children}, + TooltipContent: () => null, + TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children} +})) + +let root: Root | null = null +let container: HTMLDivElement | null = null + +// One row per bucket the retired tab partition used to hide behind a tab. +const FLEET: WorkspaceCleanupFacets[] = [ + makeNamedFacets('ready-one', { candidate: { tier: 'ready' }, sizeBytes: 4 * 1024 * 1024 }), + makeNamedFacets('needs-review', { candidate: { tier: 'review' } }), + makeNamedFacets('protected-one', { + candidate: { tier: 'protected', blockers: ['pinned'], selectedByDefault: false } + }), + makeNamedFacets('ignored-one', { dismissed: true }) +] + +function renderRows(rows: readonly WorkspaceCleanupFacets[]): void { + act(() => + root?.render( + ( + + )} + /> + ) + ) +} + +function renderedNames(): string[] { + // Buttons share `text-sm font-medium`, so scope to the row's name span. + return [...(container?.querySelectorAll('span.truncate.text-sm.font-medium') ?? [])].map( + (node) => node.textContent ?? '' + ) +} + +function query(filters: WorkspaceCleanupFilterState = createDefaultWorkspaceCleanupFilterState()) { + return runWorkspaceCleanupQuery( + FLEET, + { filters, sort: DEFAULT_WORKSPACE_CLEANUP_SORT }, + FACET_NOW + ) +} + +describe('workspace cleanup flat list', () => { + beforeEach(() => { + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + }) + + afterEach(() => { + if (root) { + act(() => root?.unmount()) + } + container?.remove() + root = null + container = null + }) + + it('renders every tier in one list rather than per-tab groups', () => { + const result = query() + + renderRows(result.rows) + + expect(result.totalCount).toBe(FLEET.length) + expect(result.matchedCount).toBe(FLEET.length) + expect(renderedNames()).toEqual( + expect.arrayContaining(['ready-one', 'needs-review', 'protected-one', 'ignored-one']) + ) + }) + + it('switches the visible rows when filters are applied', () => { + const filters = createDefaultWorkspaceCleanupFilterState() + filters.safety.tiers = ['ready'] + filters.safety.dismissed = 'exclude' + renderRows(query(filters).rows) + expect(renderedNames()).toEqual(['ready-one']) + + filters.safety.tiers = [] + filters.safety.dismissed = 'only' + renderRows(query(filters).rows) + expect(renderedNames()).toEqual(['ignored-one']) + + filters.safety.dismissed = 'any' + filters.safety.tiers = ['protected'] + renderRows(query(filters).rows) + expect(renderedNames()).toEqual(['protected-one']) + }) + + it('reports only the rows the user could actually queue for deletion', () => { + expect(query().selectableWorktreeIds).not.toContain('repo-1::/repo/protected-one') + }) + + it('shows status and disk-size facts without expanding a row', () => { + renderRows(query().rows) + + expect(container?.textContent).toContain('In progress') + expect(container?.textContent).toContain('4.00 MB') + expect(container?.textContent).toContain('Not measured') + const sizedRows = [...(container?.querySelectorAll('[aria-label^="Size on disk"]') ?? [])] + expect(sizedRows).toHaveLength(FLEET.length) + }) +}) diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-freshness.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-freshness.tsx new file mode 100644 index 00000000000..7a78c9fd597 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-freshness.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import { Loader2 } from 'lucide-react' +import { useNow } from '@/components/dashboard/useNow' +import { translate } from '@/i18n/i18n' +import type { WorkspaceCleanupScanProgress } from '../../../../shared/workspace-cleanup' +import { formatWorkspaceCleanupRelativeTime } from './workspace-cleanup-relative-time' + +/** + * Fixed-slot freshness line: "Updated 2h ago" for the data on screen plus a + * non-blocking "Refreshing" affordance while a background rescan streams. + */ +export function WorkspaceCleanupFreshness({ + scannedAt, + refreshing, + progress +}: { + scannedAt: number | null + refreshing: boolean + progress: WorkspaceCleanupScanProgress | null +}): React.JSX.Element | null { + const now = useNow(30_000) + if (scannedAt === null && !refreshing) { + return null + } + return ( + + {scannedAt !== null ? ( + + {translate('components.workspace.cleanup.browse.updatedAgo', 'Updated {{value0}}', { + value0: formatWorkspaceCleanupRelativeTime(scannedAt, now).toLowerCase() + })} + + ) : null} + {refreshing ? ( + + + ) : null} + + ) +} + +function formatWorkspaceCleanupRefreshingLabel( + progress: WorkspaceCleanupScanProgress | null +): string { + if (!progress || progress.totalWorktreeCount === 0) { + return translate('components.workspace.cleanup.browse.refreshing', 'Refreshing…') + } + return translate( + 'components.workspace.cleanup.browse.refreshingProgress', + 'Refreshing {{value0}}/{{value1}}', + { value0: progress.scannedWorktreeCount, value1: progress.totalWorktreeCount } + ) +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-git-evidence.test.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-git-evidence.test.ts new file mode 100644 index 00000000000..bfcc1a087a4 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-git-evidence.test.ts @@ -0,0 +1,148 @@ +import { describe, expect, it } from 'vitest' +import { createDefaultWorkspaceCleanupFilterState } from '../../../../shared/workspace-cleanup-filter-model' +import { makeFacetCandidate } from './workspace-cleanup-facet.test.fixture' +import { + applyWorkspaceCleanupGitEvidence, + hasWorkspaceCleanupGitEvidence, + needsWorkspaceCleanupGitEvidence, + selectWorkspaceCleanupGitEvidenceTargets +} from './workspace-cleanup-git-evidence' + +const CLEAN_SORT = { field: 'name', direction: 'asc' } as const + +function deferredCandidate(worktreeId: string) { + return makeFacetCandidate({ + worktreeId, + git: { clean: null, upstreamAhead: null, upstreamBehind: null, checkedAt: null } + }) +} + +describe('needsWorkspaceCleanupGitEvidence', () => { + it('stays off for a filter and sort that never read git', () => { + expect( + needsWorkspaceCleanupGitEvidence(createDefaultWorkspaceCleanupFilterState(), CLEAN_SORT) + ).toBe(false) + }) + + it.each([ + [ + 'git state', + (f: ReturnType) => { + f.git.states = ['dirty'] + } + ], + [ + 'ahead threshold', + (f: ReturnType) => { + f.git.minAhead = 1 + } + ], + [ + 'behind threshold', + (f: ReturnType) => { + f.git.minBehind = 1 + } + ], + [ + 'git-derived blocker', + (f: ReturnType) => { + f.safety.blockers = ['unpushed-commits'] + } + ] + ])('turns on for a %s filter', (_label, mutate) => { + const filters = createDefaultWorkspaceCleanupFilterState() + mutate(filters) + expect(needsWorkspaceCleanupGitEvidence(filters, CLEAN_SORT)).toBe(true) + }) + + it.each(['git', 'ahead', 'behind'] as const)('turns on for the %s sort', (field) => { + expect( + needsWorkspaceCleanupGitEvidence(createDefaultWorkspaceCleanupFilterState(), { + field, + direction: 'asc' + }) + ).toBe(true) + }) + + it('ignores a blocker filter that does not depend on git', () => { + const filters = createDefaultWorkspaceCleanupFilterState() + filters.safety.blockers = ['pinned'] + expect(needsWorkspaceCleanupGitEvidence(filters, CLEAN_SORT)).toBe(false) + }) +}) + +describe('selectWorkspaceCleanupGitEvidenceTargets', () => { + it('only targets rows the host deferred', () => { + const targets = selectWorkspaceCleanupGitEvidenceTargets([ + deferredCandidate('a'), + makeFacetCandidate({ worktreeId: 'b' }) + ]) + expect(targets).toEqual(['a']) + expect(hasWorkspaceCleanupGitEvidence(makeFacetCandidate({ worktreeId: 'b' }))).toBe(true) + }) + + it('skips rows already attempted and caps the batch', () => { + const candidates = ['a', 'b', 'c'].map(deferredCandidate) + expect( + selectWorkspaceCleanupGitEvidenceTargets(candidates, { + resolvedWorktreeIds: new Set(['a']) + }) + ).toEqual(['b', 'c']) + expect(selectWorkspaceCleanupGitEvidenceTargets(candidates, { maxTargets: 2 })).toEqual([ + 'a', + 'b' + ]) + }) +}) + +describe('applyWorkspaceCleanupGitEvidence', () => { + it('replaces the deferred row with the focused re-scan result', () => { + const deferred = deferredCandidate('a') + const refreshed = makeFacetCandidate({ + worktreeId: 'a', + git: { clean: false, upstreamAhead: 2, upstreamBehind: 0, checkedAt: 5 } + }) + const [row] = applyWorkspaceCleanupGitEvidence([deferred], new Map([['a', refreshed]])) + expect(row.git.clean).toBe(false) + expect(row.git.checkedAt).toBe(5) + }) + + it('returns the input untouched when nothing was re-scanned', () => { + const candidates = [deferredCandidate('a')] + expect(applyWorkspaceCleanupGitEvidence(candidates, new Map())).toBe(candidates) + }) + + it('merges only git facts into the current broad-scan row', () => { + const current = deferredCandidate('a') + current.displayName = 'current name' + current.blockers = ['dismissed'] + current.fingerprint = 'current-fingerprint' + const refreshed = makeFacetCandidate({ + worktreeId: 'a', + displayName: 'stale name', + blockers: ['dirty-files'], + fingerprint: 'focused-fingerprint', + git: { clean: false, upstreamAhead: 0, upstreamBehind: 0, checkedAt: 5 } + }) + + const [row] = applyWorkspaceCleanupGitEvidence([current], new Map([['a', refreshed]])) + + expect(row.displayName).toBe('current name') + expect(row.fingerprint).toBe('current-fingerprint') + expect(row.blockers).toEqual(expect.arrayContaining(['dismissed', 'dirty-files'])) + expect(row.git).toEqual(refreshed.git) + }) + + it('keeps newer broad-scan git evidence', () => { + const current = makeFacetCandidate({ + worktreeId: 'a', + git: { clean: true, upstreamAhead: 0, upstreamBehind: 0, checkedAt: 10 } + }) + const older = makeFacetCandidate({ + worktreeId: 'a', + git: { clean: false, upstreamAhead: 2, upstreamBehind: 0, checkedAt: 5 } + }) + + expect(applyWorkspaceCleanupGitEvidence([current], new Map([['a', older]]))[0]).toBe(current) + }) +}) diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-git-evidence.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-git-evidence.ts new file mode 100644 index 00000000000..24fb5de87ff --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-git-evidence.ts @@ -0,0 +1,101 @@ +import type { + WorkspaceCleanupBlocker, + WorkspaceCleanupCandidate +} from '../../../../shared/workspace-cleanup' +import { + applyWorkspaceCleanupPolicy, + WORKSPACE_CLEANUP_TARGET_BATCH_LIMIT +} from '../../../../shared/workspace-cleanup' +import type { + WorkspaceCleanupFilterState, + WorkspaceCleanupSortField, + WorkspaceCleanupSortState +} from '../../../../shared/workspace-cleanup-filter-model' + +/** + * The host defers git evidence for rows with no inactivity reason, so their + * `git.checkedAt` is null and the facet layer reads them as `unknown`. A + * git-dependent filter or sort would then quietly mislabel most of the list, + * so the dialog re-scans those rows on demand before trusting the answer. + */ +export function hasWorkspaceCleanupGitEvidence(candidate: WorkspaceCleanupCandidate): boolean { + return candidate.git.checkedAt !== null +} + +const GIT_SORT_FIELDS: ReadonlySet = new Set(['git', 'ahead', 'behind']) + +const GIT_DERIVED_BLOCKERS: ReadonlySet = new Set([ + 'dirty-files', + 'unpushed-commits', + 'git-status-error', + 'unknown-base' +]) + +/** Bounds how many worktrees one Git-evidence batch can inspect across a huge fleet. */ +export const WORKSPACE_CLEANUP_GIT_EVIDENCE_MAX_TARGETS = WORKSPACE_CLEANUP_TARGET_BATCH_LIMIT + +export function needsWorkspaceCleanupGitEvidence( + filters: WorkspaceCleanupFilterState, + sort: WorkspaceCleanupSortState +): boolean { + return ( + filters.git.states.length > 0 || + filters.git.minAhead !== null || + filters.git.minBehind !== null || + filters.safety.blockers.some((blocker) => GIT_DERIVED_BLOCKERS.has(blocker)) || + GIT_SORT_FIELDS.has(sort.field) + ) +} + +/** + * Ids still missing git evidence, capped and in list order so the rows the user + * is looking at resolve first. + */ +export function selectWorkspaceCleanupGitEvidenceTargets( + candidates: readonly WorkspaceCleanupCandidate[], + options: { + resolvedWorktreeIds?: ReadonlySet + maxTargets?: number + } = {} +): string[] { + const resolved = options.resolvedWorktreeIds ?? new Set() + const max = options.maxTargets ?? WORKSPACE_CLEANUP_GIT_EVIDENCE_MAX_TARGETS + const targets: string[] = [] + for (const candidate of candidates) { + if (targets.length >= max) { + break + } + if (!hasWorkspaceCleanupGitEvidence(candidate) && !resolved.has(candidate.worktreeId)) { + targets.push(candidate.worktreeId) + } + } + return targets +} + +/** Focused re-scan results win over the deferred broad-scan row. */ +export function applyWorkspaceCleanupGitEvidence( + candidates: readonly WorkspaceCleanupCandidate[], + evidenceByWorktreeId: ReadonlyMap +): readonly WorkspaceCleanupCandidate[] { + if (evidenceByWorktreeId.size === 0) { + return candidates + } + return candidates.map((candidate) => { + const evidence = evidenceByWorktreeId.get(candidate.worktreeId) + if ( + evidence === undefined || + (candidate.git.checkedAt !== null && + candidate.git.checkedAt >= (evidence.git.checkedAt ?? Number.NEGATIVE_INFINITY)) + ) { + return candidate + } + return applyWorkspaceCleanupPolicy({ + ...candidate, + blockers: [ + ...candidate.blockers.filter((blocker) => !GIT_DERIVED_BLOCKERS.has(blocker)), + ...evidence.blockers.filter((blocker) => GIT_DERIVED_BLOCKERS.has(blocker)) + ], + git: evidence.git + }) + }) +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-git-review-facets.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-git-review-facets.tsx new file mode 100644 index 00000000000..3db2300c4c5 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-git-review-facets.tsx @@ -0,0 +1,209 @@ +import React from 'react' +import { translate } from '@/i18n/i18n' +import { getExecutionHostLabel } from '../../../../shared/execution-host' +import { + WORKSPACE_CLEANUP_BLOCKER_MODE_VALUES, + WORKSPACE_CLEANUP_BLOCKER_VALUES, + WORKSPACE_CLEANUP_GIT_STATE_VALUES, + WORKSPACE_CLEANUP_PRESENCE_VALUES, + WORKSPACE_CLEANUP_REVIEW_STATE_VALUES, + WORKSPACE_CLEANUP_TICKET_SOURCE_VALUES, + WORKSPACE_CLEANUP_TIER_VALUES, + WORKSPACE_CLEANUP_TRI_STATE_VALUES +} from '../../../../shared/workspace-cleanup-facet-rankings' +import { getWorkspaceCleanupBlockerLabel } from './workspace-cleanup-candidate-labels' +import { + FacetCheckbox, + FacetChoice, + FacetNumberField, + FacetSection, + FacetTextField, + FacetToggleList +} from './workspace-cleanup-facet-controls' +import { + getWorkspaceCleanupBlockerModeLabel, + getWorkspaceCleanupGitStateLabel, + getWorkspaceCleanupPresenceLabel, + getWorkspaceCleanupReviewProviderLabel, + getWorkspaceCleanupReviewStateLabel, + getWorkspaceCleanupTicketSourceLabel, + getWorkspaceCleanupTierLabel, + getWorkspaceCleanupTriStateLabel +} from './workspace-cleanup-facet-labels' +import type { WorkspaceCleanupFacetGroupProps } from './workspace-cleanup-facet-panel-model' + +/** Git, review/PR, tickets, location/host, and safety blockers. */ +export function WorkspaceCleanupGitReviewFacets({ + filters, + counts, + totalCount, + options, + onPatch +}: WorkspaceCleanupFacetGroupProps): React.JSX.Element { + return ( + <> + + onPatch('git', { states })} + /> + onPatch('git', { minAhead })} + /> + onPatch('git', { minBehind })} + /> + onPatch('git', { branchQuery })} + /> + onPatch('git', { prunable })} + /> + onPatch('git', { locked })} + /> + + + + onPatch('review', { presence })} + /> + onPatch('review', { states })} + /> + {options.reviewProviders.length > 0 ? ( + onPatch('review', { providers })} + /> + ) : null} + + + + onPatch('ticket', { presence })} + /> + onPatch('ticket', { sources })} + /> + + + + onPatch('location', { hostIds })} + /> + {options.repos.length > 1 ? ( + repo.id)} + selected={filters.location.repoIds} + getLabel={(id) => options.repos.find((repo) => repo.id === id)?.label ?? id} + onChange={(repoIds) => onPatch('location', { repoIds })} + /> + ) : null} + onPatch('location', { pathPrefix })} + /> + + + + onPatch('safety', { tiers })} + /> + onPatch('safety', { blockerMode })} + /> + onPatch('safety', { blockers })} + /> + onPatch('safety', { dismissed })} + /> + onPatch('safety', { selectableOnly })} + /> + + + ) +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-host-identity.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-host-identity.ts new file mode 100644 index 00000000000..5f01b485678 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-host-identity.ts @@ -0,0 +1,137 @@ +import { + getRepoExecutionHostId, + LOCAL_EXECUTION_HOST_ID, + normalizeExecutionHostId, + toSshExecutionHostId, + type ExecutionHostId +} from '../../../../shared/execution-host' +import type { Repo } from '../../../../shared/repo-types' +import type { Worktree } from '../../../../shared/worktree/types' +import type { + WorkspaceSpaceWorktree, + WorkspaceSpaceWorktreeMeasurement +} from '../../../../shared/workspace-space-types' +import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' + +/** Structural subset of `Worktree` kept in sync with the persisted source. */ +export type WorkspaceCleanupWorktreeFacts = Pick & + Partial< + Pick< + Worktree, + | 'repoId' + | 'workspaceStatus' + | 'isArchived' + | 'isPinned' + | 'isUnread' + | 'comment' + | 'hostId' + | 'branch' + | 'createdAt' + | 'linkedWorkItem' + | 'linkedLinearIssue' + | 'linkedIssue' + | 'locked' + | 'prunable' + > + > + +export function getWorkspaceCleanupCandidateHostId( + candidate: Pick +): ExecutionHostId { + return ( + normalizeExecutionHostId(candidate.executionHostId) ?? + (candidate.connectionId + ? toSshExecutionHostId(candidate.connectionId) + : LOCAL_EXECUTION_HOST_ID) + ) +} + +export function getWorkspaceCleanupHostIdentity(hostId: string, id: string): string { + return `${hostId}\0${id}` +} + +function countIds( + rows: readonly Row[], + include: (row: Row) => boolean = () => true +): Map { + const counts = new Map() + for (const row of rows) { + if (include(row)) { + counts.set(row.worktreeId, (counts.get(row.worktreeId) ?? 0) + 1) + } + } + return counts +} + +export function countWorkspaceCleanupCandidateIds( + candidates: readonly Pick[] +): Map { + return countIds(candidates) +} + +/** Only `ok` scans carry a trustworthy byte count; everything else stays unsized. */ +export function buildWorkspaceCleanupSizeIndex( + worktrees: + | readonly (WorkspaceSpaceWorktree | WorkspaceSpaceWorktreeMeasurement)[] + | null + | undefined, + candidates?: readonly Pick< + WorkspaceCleanupCandidate, + 'connectionId' | 'executionHostId' | 'worktreeId' + >[] +): Map { + const index = new Map() + const candidateCounts = candidates ? countWorkspaceCleanupCandidateIds(candidates) : null + const legacyCounts = countIds( + worktrees ?? [], + (entry) => !normalizeExecutionHostId(entry.executionHostId) + ) + for (const entry of worktrees ?? []) { + if (entry.status === 'ok' && Number.isFinite(entry.sizeBytes)) { + const hostId = normalizeExecutionHostId(entry.executionHostId) + if (hostId) { + index.set(getWorkspaceCleanupHostIdentity(hostId, entry.worktreeId), entry.sizeBytes) + } else if ( + legacyCounts.get(entry.worktreeId) === 1 && + (!candidateCounts || candidateCounts.get(entry.worktreeId) === 1) + ) { + index.set(entry.worktreeId, entry.sizeBytes) + } + } + } + return index +} + +export function buildWorkspaceCleanupWorktreeIndex( + worktreesByRepo: Readonly>, + repos: readonly Pick[] = [] +): Map { + const index = new Map() + const repoHostsById = new Map>() + for (const repo of repos) { + const hosts = repoHostsById.get(repo.id) ?? new Set() + hosts.add(getRepoExecutionHostId(repo)) + repoHostsById.set(repo.id, hosts) + } + const legacyCounts = new Map() + for (const worktrees of Object.values(worktreesByRepo)) { + for (const worktree of worktrees) { + if (!normalizeExecutionHostId(worktree.hostId)) { + legacyCounts.set(worktree.id, (legacyCounts.get(worktree.id) ?? 0) + 1) + } + } + } + for (const worktrees of Object.values(worktreesByRepo)) { + for (const worktree of worktrees) { + const explicitHostId = normalizeExecutionHostId(worktree.hostId) + const repoHosts = worktree.repoId ? repoHostsById.get(worktree.repoId) : undefined + const derivedHostId = repoHosts?.size === 1 ? [...repoHosts][0] : null + const hostId = + explicitHostId ?? (legacyCounts.get(worktree.id) === 1 ? derivedHostId : undefined) + if (hostId) { + index.set(getWorkspaceCleanupHostIdentity(hostId, worktree.id), worktree) + } + } + } + return index +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-lifecycle-facets.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-lifecycle-facets.tsx new file mode 100644 index 00000000000..a65332eabb6 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-lifecycle-facets.tsx @@ -0,0 +1,198 @@ +import React from 'react' +import { translate } from '@/i18n/i18n' +import { + WORKSPACE_CLEANUP_AGENT_STATE_VALUES, + WORKSPACE_CLEANUP_IDLE_SIGNAL_VALUES, + WORKSPACE_CLEANUP_PRESENCE_VALUES, + WORKSPACE_CLEANUP_TRI_STATE_VALUES +} from '../../../../shared/workspace-cleanup-facet-rankings' +import { + FacetCheckbox, + FacetChoice, + FacetNumberField, + FacetSection, + FacetToggleList +} from './workspace-cleanup-facet-controls' +import { + getWorkspaceCleanupAgentStateLabel, + getWorkspaceCleanupIdleSignalLabel, + getWorkspaceCleanupPresenceLabel, + getWorkspaceCleanupTriStateLabel +} from './workspace-cleanup-facet-labels' +import { + fromMegabytes, + toMegabytes, + type WorkspaceCleanupFacetGroupProps +} from './workspace-cleanup-facet-panel-model' + +/** Activity, size, workspace status, agent state, and local context. */ +export function WorkspaceCleanupLifecycleFacets({ + filters, + counts, + totalCount, + options, + onPatch +}: WorkspaceCleanupFacetGroupProps): React.JSX.Element { + return ( + <> + + onPatch('activity', { idleSignal })} + /> + onPatch('activity', { idleMinDays })} + /> + onPatch('activity', { neverVisited })} + /> + + + + onPatch('size', { minBytes: fromMegabytes(megabytes) })} + /> + onPatch('size', { maxBytes: fromMegabytes(megabytes) })} + /> + onPatch('size', { includeUnsized })} + /> + + + + {options.workspaceStatuses.length > 0 ? ( + status.id)} + selected={filters.status.workspaceStatuses} + getLabel={(id) => + options.workspaceStatuses.find((status) => status.id === id)?.label ?? id + } + onChange={(workspaceStatuses) => onPatch('status', { workspaceStatuses })} + /> + ) : null} + onPatch('status', { matchStatusless })} + /> + onPatch('status', { archived })} + /> + onPatch('status', { pinned })} + /> + onPatch('status', { unread })} + /> + onPatch('status', { comment })} + /> + + + + onPatch('agent', { states })} + /> + onPatch('agent', { retainedDoneAgents })} + /> + + + + onPatch('context', { presence })} + /> + onPatch('context', { completelyEmpty })} + /> + + + ) +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-metadata-chip.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-metadata-chip.tsx new file mode 100644 index 00000000000..5fea9257680 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-metadata-chip.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import type { LucideIcon } from 'lucide-react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { cn } from '@/lib/utils' +import type { StatusPillTone } from './workspace-cleanup-candidate-row-data' + +export function WorkspaceCleanupMetadataChip({ + icon: Icon, + label, + value, + tone = 'neutral' +}: { + icon: LucideIcon + label: string + value?: string + tone?: StatusPillTone +}): React.JSX.Element { + return ( + + + + + + + {label} + + + ) +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation.test.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation.test.ts index f6e1aab4488..2f24f47fb0c 100644 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation.test.ts +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation.test.ts @@ -16,6 +16,7 @@ import { makeReview, makeState } from './workspace-cleanup-presentation-fixtures' +import type { Repo } from '../../../../shared/repo-types' describe('workspace cleanup presentation', () => { it('finds hosted review details from renderer state', () => { @@ -34,6 +35,76 @@ describe('workspace cleanup presentation', () => { }) }) + it('host-qualifies same-id repo, worktree, and review cache joins', () => { + const base = makeState() + const localRepo = { ...base.repos[0]!, executionHostId: 'local' as const } + const remoteRepo: Repo = { + ...localRepo, + connectionId: 'builder', + executionHostId: 'ssh:builder' + } + const worktreeId = 'repo-1::/repo/alpha' + const localWorktree = { + ...base.worktreesByRepo['repo-1']![0]!, + hostId: 'local' as const, + linkedPR: 11 + } + const remoteWorktree = { + ...localWorktree, + hostId: 'ssh:builder' as const, + linkedPR: 22 + } + const localKey = getHostedReviewCacheKey('/repo', 'alpha', {}, 'repo-1', null, 'local', true) + const remoteKey = getHostedReviewCacheKey( + '/repo', + 'alpha', + {}, + 'repo-1', + 'builder', + 'ssh:builder', + true + ) + const state = makeState({ + repos: [localRepo, remoteRepo], + worktreesByRepo: { 'repo-1': [localWorktree, remoteWorktree] }, + hostedReviewCache: { + [localKey]: { data: makeReview({ number: 11, title: 'Local review' }), fetchedAt: NOW }, + [remoteKey]: { data: makeReview({ number: 22, title: 'Remote review' }), fetchedAt: NOW } + } + }) + + expect( + getWorkspaceCleanupReviewInfo( + makeCandidate({ + worktreeId, + connectionId: 'builder', + executionHostId: 'ssh:builder' + }), + state + ) + ).toMatchObject({ label: 'PR #22', title: 'Remote review' }) + }) + + it('does not guess a host for legacy same-id review joins', () => { + const base = makeState() + const duplicateRepo = { + ...base.repos[0]!, + connectionId: 'builder', + executionHostId: 'ssh:builder' as const + } + const state = makeState({ + repos: [base.repos[0]!, duplicateRepo], + worktreesByRepo: { + 'repo-1': [ + { ...base.worktreesByRepo['repo-1']![0]!, linkedPR: 11 }, + { ...base.worktreesByRepo['repo-1']![0]!, linkedPR: 22 } + ] + } + }) + + expect(getWorkspaceCleanupReviewInfo(makeCandidate(), state).hasReview).toBe(false) + }) + it('filters by time, review, git, and context', () => { const openReview: WorkspaceCleanupReviewInfo = { hasReview: true, diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation.ts index 1accef346ca..f3506b07e20 100644 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation.ts +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation.ts @@ -1,11 +1,12 @@ -import { getWorktreeMapFromState } from '@/store/selectors' import { getHostedReviewCacheKey } from '@/store/slices/hosted-review' import type { AppState } from '@/store/types' import { translate } from '@/i18n/i18n' +import { getRepoExecutionHostId } from '../../../../shared/execution-host' import type { HostedReviewInfo, HostedReviewProvider } from '../../../../shared/hosted-review' import type { Repo } from '../../../../shared/repo-types' import type { Worktree } from '../../../../shared/worktree/types' import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' +import { getWorkspaceCleanupCandidateHostId } from './workspace-cleanup-host-identity' export type WorkspaceCleanupSortKey = 'activity' | 'name' | 'repo' | 'review' | 'git' export type WorkspaceCleanupSortDirection = 'asc' | 'desc' @@ -40,6 +41,11 @@ export type WorkspaceCleanupRendererStateInputs = Pick< 'worktreesByRepo' | 'hostedReviewCache' | 'repos' | 'settings' > +export type WorkspaceCleanupReviewLookup = { + reposById: ReadonlyMap + worktreesByRepoAndId: ReadonlyMap +} + export { filterWorkspaceCleanupCandidates, getWorkspaceCleanupGitLabel, @@ -50,10 +56,11 @@ export { export function getWorkspaceCleanupReviewInfo( candidate: WorkspaceCleanupCandidate, - state: WorkspaceCleanupRendererStateInputs + state: WorkspaceCleanupRendererStateInputs, + lookup: WorkspaceCleanupReviewLookup = buildWorkspaceCleanupReviewLookup(state) ): WorkspaceCleanupReviewInfo { - const worktree = getWorktreeMapFromState(state).get(candidate.worktreeId) ?? null - const repo = state.repos.find((entry) => entry.id === candidate.repoId) ?? null + const repo = findCandidateRepo(candidate, lookup) + const worktree = findCandidateWorktree(candidate, repo, lookup) const hostedReview = getCachedHostedReview(candidate, worktree, repo, state) if (hostedReview) { return { @@ -85,6 +92,27 @@ export function getWorkspaceCleanupReviewInfo( } } +export function buildWorkspaceCleanupReviewLookup( + state: Pick +): WorkspaceCleanupReviewLookup { + const reposById = new Map() + for (const repo of state.repos) { + const matches = reposById.get(repo.id) ?? [] + matches.push(repo) + reposById.set(repo.id, matches) + } + const worktreesByRepoAndId = new Map() + for (const [repoId, worktrees] of Object.entries(state.worktreesByRepo)) { + for (const worktree of worktrees) { + const key = `${repoId}\0${worktree.id}` + const matches = worktreesByRepoAndId.get(key) ?? [] + matches.push(worktree) + worktreesByRepoAndId.set(key, matches) + } + } + return { reposById, worktreesByRepoAndId } +} + function getCachedHostedReview( candidate: WorkspaceCleanupCandidate, worktree: Worktree | null, @@ -99,11 +127,43 @@ function getCachedHostedReview( getBranchDisplayName(worktree?.branch ?? candidate.branch), state.settings, repo.id, - repo.connectionId + repo.connectionId, + repo.executionHostId, + true ) return state.hostedReviewCache[cacheKey]?.data ?? null } +function findCandidateRepo( + candidate: WorkspaceCleanupCandidate, + lookup: WorkspaceCleanupReviewLookup +): Repo | null { + const matches = lookup.reposById.get(candidate.repoId) ?? [] + if (!candidate.executionHostId && !candidate.connectionId) { + return matches.length === 1 ? matches[0] : null + } + const hostId = getWorkspaceCleanupCandidateHostId(candidate) + const hostMatches = matches.filter((repo) => getRepoExecutionHostId(repo) === hostId) + return hostMatches.length === 1 ? hostMatches[0] : null +} + +function findCandidateWorktree( + candidate: WorkspaceCleanupCandidate, + repo: Repo | null, + lookup: WorkspaceCleanupReviewLookup +): Worktree | null { + const matches = + lookup.worktreesByRepoAndId.get(`${candidate.repoId}\0${candidate.worktreeId}`) ?? [] + const hostId = getWorkspaceCleanupCandidateHostId(candidate) + const hostMatches = matches.filter((worktree) => worktree.hostId === hostId) + if (hostMatches.length === 1) { + return hostMatches[0] + } + const legacyMatches = matches.filter((worktree) => !worktree.hostId) + const repoMatches = lookup.reposById.get(candidate.repoId) ?? [] + return legacyMatches.length === 1 && repo && repoMatches.length === 1 ? legacyMatches[0] : null +} + function getLinkedReviewFallback(worktree: Worktree | null): { label: string provider: HostedReviewProvider diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-query.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-query.ts new file mode 100644 index 00000000000..6a722d15ab0 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-query.ts @@ -0,0 +1,142 @@ +import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' +import { + buildWorkspaceCleanupFacetList, + type WorkspaceCleanupFacetSources, + type WorkspaceCleanupFacets +} from './workspace-cleanup-facets' +import { + matchesWorkspaceCleanupActivity, + matchesWorkspaceCleanupAgent, + matchesWorkspaceCleanupContext, + matchesWorkspaceCleanupGit, + matchesWorkspaceCleanupLocation, + matchesWorkspaceCleanupReview, + matchesWorkspaceCleanupSafety, + matchesWorkspaceCleanupSize, + matchesWorkspaceCleanupStatus, + matchesWorkspaceCleanupTicket +} from './workspace-cleanup-facet-matchers' +import { sortWorkspaceCleanupFacets } from './workspace-cleanup-facet-sort' +import type { + WorkspaceCleanupFilterState, + WorkspaceCleanupSortState +} from '../../../../shared/workspace-cleanup-filter-model' + +export type WorkspaceCleanupQuery = { + filters: WorkspaceCleanupFilterState + sort: WorkspaceCleanupSortState +} + +export type WorkspaceCleanupQueryResult = { + /** Sorted rows that passed every facet. */ + rows: WorkspaceCleanupFacets[] + /** Rows the user could actually queue for deletion, for the select-all affordance. */ + selectableWorktreeIds: string[] + totalCount: number + matchedCount: number +} + +export function matchesWorkspaceCleanupFilters( + facets: WorkspaceCleanupFacets, + filters: WorkspaceCleanupFilterState, + now: number = Date.now() +): boolean { + const query = filters.query.trim().toLowerCase() + return matchesWorkspaceCleanupFiltersWithQuery(facets, filters, now, query) +} + +function matchesWorkspaceCleanupFiltersWithQuery( + facets: WorkspaceCleanupFacets, + filters: WorkspaceCleanupFilterState, + now: number, + query: string +): boolean { + if (query && !facets.searchText.includes(query)) { + return false + } + return ( + matchesWorkspaceCleanupSafety(facets, filters.safety) && + matchesWorkspaceCleanupActivity(facets, filters.activity, now) && + matchesWorkspaceCleanupSize(facets, filters.size) && + matchesWorkspaceCleanupStatus(facets, filters.status) && + matchesWorkspaceCleanupAgent(facets, filters.agent) && + matchesWorkspaceCleanupGit(facets, filters.git) && + matchesWorkspaceCleanupReview(facets, filters.review) && + matchesWorkspaceCleanupTicket(facets, filters.ticket) && + matchesWorkspaceCleanupContext(facets, filters.context) && + matchesWorkspaceCleanupLocation(facets, filters.location) + ) +} + +export function filterWorkspaceCleanupFacets( + facets: readonly WorkspaceCleanupFacets[], + filters: WorkspaceCleanupFilterState, + now: number = Date.now() +): WorkspaceCleanupFacets[] { + const query = filters.query.trim().toLowerCase() + return facets.filter((entry) => + matchesWorkspaceCleanupFiltersWithQuery(entry, filters, now, query) + ) +} + +export function runWorkspaceCleanupQuery( + facets: readonly WorkspaceCleanupFacets[], + query: WorkspaceCleanupQuery, + now: number = Date.now() +): WorkspaceCleanupQueryResult { + const matched = filterWorkspaceCleanupFacets(facets, query.filters, now) + const rows = sortWorkspaceCleanupFacets(matched, query.sort) + return { + rows, + selectableWorktreeIds: rows.filter((row) => row.isSelectable).map((row) => row.worktreeId), + totalCount: facets.length, + matchedCount: rows.length + } +} + +/** One-shot convenience for callers holding raw scan candidates. */ +export function queryWorkspaceCleanupCandidates( + candidates: readonly WorkspaceCleanupCandidate[], + query: WorkspaceCleanupQuery, + sources: WorkspaceCleanupFacetSources = {}, + now: number = Date.now() +): WorkspaceCleanupQueryResult { + return runWorkspaceCleanupQuery(buildWorkspaceCleanupFacetList(candidates, sources), query, now) +} + +/** + * Per-facet-group match counts for badge/count rendering. Each entry counts the + * rows that would survive if only that group were applied, so a UI can show how + * much each facet is costing without re-running the whole pipeline per group. + */ +export function countWorkspaceCleanupFacetMatches( + facets: readonly WorkspaceCleanupFacets[], + filters: WorkspaceCleanupFilterState, + now: number = Date.now() +): Record, number> { + return { + activity: count(facets, (row) => matchesWorkspaceCleanupActivity(row, filters.activity, now)), + size: count(facets, (row) => matchesWorkspaceCleanupSize(row, filters.size)), + status: count(facets, (row) => matchesWorkspaceCleanupStatus(row, filters.status)), + agent: count(facets, (row) => matchesWorkspaceCleanupAgent(row, filters.agent)), + git: count(facets, (row) => matchesWorkspaceCleanupGit(row, filters.git)), + review: count(facets, (row) => matchesWorkspaceCleanupReview(row, filters.review)), + ticket: count(facets, (row) => matchesWorkspaceCleanupTicket(row, filters.ticket)), + context: count(facets, (row) => matchesWorkspaceCleanupContext(row, filters.context)), + location: count(facets, (row) => matchesWorkspaceCleanupLocation(row, filters.location)), + safety: count(facets, (row) => matchesWorkspaceCleanupSafety(row, filters.safety)) + } +} + +function count( + facets: readonly WorkspaceCleanupFacets[], + predicate: (facets: WorkspaceCleanupFacets) => boolean +): number { + let total = 0 + for (const entry of facets) { + if (predicate(entry)) { + total += 1 + } + } + return total +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-relative-time.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-relative-time.ts new file mode 100644 index 00000000000..1391ec7327b --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-relative-time.ts @@ -0,0 +1,27 @@ +import { translate } from '@/i18n/i18n' + +/** Compact "how long ago" label for cleanup rows; 0 means Orca never recorded it. */ +export function formatWorkspaceCleanupRelativeTime(timestamp: number, now = Date.now()): string { + if (!timestamp) { + return translate('components.workspace.cleanup.relativeTime.never', 'Never') + } + const deltaMs = now - timestamp + if (deltaMs < 60_000) { + return translate('components.workspace.cleanup.relativeTime.justNow', 'Just now') + } + const minutes = Math.floor(deltaMs / 60_000) + if (minutes < 60) { + return translate('components.workspace.cleanup.relativeTime.minutesAgo', '{{value0}}m ago', { + value0: minutes + }) + } + const hours = Math.floor(minutes / 60) + if (hours < 48) { + return translate('components.workspace.cleanup.relativeTime.hoursAgo', '{{value0}}h ago', { + value0: hours + }) + } + return translate('components.workspace.cleanup.relativeTime.daysAgo', '{{value0}}d ago', { + value0: Math.floor(hours / 24) + }) +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-removal-session.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-removal-session.ts deleted file mode 100644 index e202ac8bfe5..00000000000 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-removal-session.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { - useCallback, - useRef, - useState, - type Dispatch, - type RefObject, - type SetStateAction -} from 'react' -import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' -import type { AppState } from '@/store/types' -import { useAppStore } from '@/store' -import { - startWorkspaceCleanupBackgroundRemoval, - type WorkspaceCleanupRemovalProgress -} from './workspace-cleanup-background-removal' -import { filterWorkspaceCleanupRemovalCandidates } from './workspace-cleanup-removal-candidates' - -type WorkspaceCleanupSelectedIdsSetter = Dispatch>> - -export type WorkspaceCleanupRemovalSession = { - confirming: boolean - confirmCandidates: WorkspaceCleanupCandidate[] - removalProgress: WorkspaceCleanupRemovalProgress | null - removalInFlight: boolean - rowFailures: Record - removalInFlightRef: RefObject - clearRowFailures: () => void - resetForOpen: () => void - applyScanDefaults: ( - candidates: readonly WorkspaceCleanupCandidate[], - deletingWorktreeIds: ReadonlySet - ) => void - openConfirmRemove: (candidates: readonly WorkspaceCleanupCandidate[]) => void - cancelConfirmRemove: () => void - backToWorkspaceCleanupList: () => void - confirmRemove: () => void -} - -export function useWorkspaceCleanupRemovalSession({ - mountedRef, - setSelectedIds, - closeModal, - removeCandidates, - markWorktreesQueuedForDeletion, - clearWorktreeDeleteState -}: { - mountedRef: RefObject - setSelectedIds: WorkspaceCleanupSelectedIdsSetter - closeModal: AppState['closeModal'] - removeCandidates: AppState['removeWorkspaceCleanupCandidates'] - markWorktreesQueuedForDeletion: AppState['markWorktreesQueuedForDeletion'] - clearWorktreeDeleteState: AppState['clearWorktreeDeleteState'] -}): WorkspaceCleanupRemovalSession { - const [confirming, setConfirming] = useState(false) - const [confirmCandidates, setConfirmCandidates] = useState([]) - const [removalProgress, setRemovalProgress] = useState( - null - ) - // Why: progress arrives after the click; the ref synchronously rejects a second batch. - const [removalInFlight, setRemovalInFlight] = useState(false) - const [rowFailures, setRowFailures] = useState>({}) - const removalInFlightRef = useRef(false) - // Why: an older batch's late settlement must not mutate a newer batch's UI. - const removalBatchIdRef = useRef(0) - - const clearRowFailures = useCallback(() => setRowFailures({}), []) - - const resetForOpen = useCallback(() => { - setConfirming(false) - setRowFailures({}) - setSelectedIds(new Set()) - }, [setSelectedIds]) - - const applyScanDefaults = useCallback( - ( - candidates: readonly WorkspaceCleanupCandidate[], - deletingWorktreeIds: ReadonlySet - ) => { - if (removalInFlightRef.current) { - return - } - setSelectedIds(getDefaultSelectedWorkspaceCleanupIds(candidates, deletingWorktreeIds)) - setConfirming(false) - setRowFailures({}) - }, - [setSelectedIds] - ) - - const openConfirmRemove = useCallback((candidates: readonly WorkspaceCleanupCandidate[]) => { - const nextCandidates = filterWorkspaceCleanupRemovalCandidates( - candidates, - useAppStore.getState().deleteStateByWorktreeId - ) - if (nextCandidates.length === 0) { - return - } - setConfirmCandidates(nextCandidates) - setConfirming(true) - }, []) - - const cancelConfirmRemove = useCallback(() => { - if (removalProgress) { - closeModal() - return - } - setConfirming(false) - setConfirmCandidates([]) - }, [closeModal, removalProgress]) - - // Why: Back exposes per-row progress without cancelling the batch; Close dismisses the modal. - const backToWorkspaceCleanupList = useCallback(() => { - setConfirming(false) - setConfirmCandidates([]) - }, []) - - const clearQueuedDeleteState = useCallback( - (worktreeId: string) => { - const deleteState = useAppStore.getState().deleteStateByWorktreeId[worktreeId] - // Why: rows that fail before removal starts must not stay queued in the sidebar. - if (deleteState?.isDeleting && deleteState.error === null && deleteState.phase === 'queued') { - clearWorktreeDeleteState(worktreeId) - } - }, - [clearWorktreeDeleteState] - ) - - const deselectRemovedIds = useCallback( - (removedIds: readonly string[]) => { - if (removedIds.length === 0) { - return - } - setSelectedIds((current) => { - const next = new Set(current) - for (const id of removedIds) { - next.delete(id) - } - return next - }) - }, - [setSelectedIds] - ) - - const confirmRemove = useCallback(() => { - if (confirmCandidates.length === 0 || removalInFlightRef.current) { - return - } - const removableCandidates = filterWorkspaceCleanupRemovalCandidates( - confirmCandidates, - useAppStore.getState().deleteStateByWorktreeId - ) - if (removableCandidates.length === 0) { - setConfirming(false) - setConfirmCandidates([]) - return - } - removalInFlightRef.current = true - setRemovalInFlight(true) - removalBatchIdRef.current += 1 - const removalBatchId = removalBatchIdRef.current - // Why: hung late settlements retain callbacks, so capture ids instead of candidate objects. - const removableWorktreeIds = removableCandidates.map((candidate) => candidate.worktreeId) - setRowFailures({}) - markWorktreesQueuedForDeletion(removableWorktreeIds) - startWorkspaceCleanupBackgroundRemoval({ - candidates: removableCandidates, - removeCandidates, - onProgress: (progress) => { - if (mountedRef.current) { - setRemovalProgress(progress) - } - }, - onRowFailed: (failure) => { - clearQueuedDeleteState(failure.worktreeId) - }, - onResult: (result) => { - const nextFailures: Record = {} - for (const failure of result.failures) { - nextFailures[failure.worktreeId] = failure.message - clearQueuedDeleteState(failure.worktreeId) - } - if (mountedRef.current) { - setRowFailures(nextFailures) - deselectRemovedIds(result.removedIds) - setRemovalProgress(null) - setRemovalInFlight(false) - setConfirming(false) - setConfirmCandidates([]) - } - removalInFlightRef.current = false - }, - onLateResult: (result) => { - for (const failure of result.failures) { - // Why: a late preflight failure can leave a row that never started stuck as queued. - clearQueuedDeleteState(failure.worktreeId) - } - if (!mountedRef.current || removalBatchIdRef.current !== removalBatchId) { - return - } - setRowFailures((current) => { - const next = { ...current } - for (const id of result.removedIds) { - delete next[id] - } - for (const failure of result.failures) { - next[failure.worktreeId] = failure.message - } - return next - }) - deselectRemovedIds(result.removedIds) - }, - onError: () => { - // Why: the batch driver is gone, so nothing will settle these rows later; clear - // unconditionally (not just queued ones) or a hung 'deleting' row stays stuck in - // the sidebar and filterWorkspaceCleanupRemovalCandidates blocks every retry. - for (const worktreeId of removableWorktreeIds) { - clearWorktreeDeleteState(worktreeId) - } - if (mountedRef.current) { - setRemovalProgress(null) - setRemovalInFlight(false) - setConfirming(false) - setConfirmCandidates([]) - } - removalInFlightRef.current = false - } - }) - }, [ - clearQueuedDeleteState, - clearWorktreeDeleteState, - confirmCandidates, - deselectRemovedIds, - markWorktreesQueuedForDeletion, - mountedRef, - removeCandidates - ]) - - return { - confirming, - confirmCandidates, - removalProgress, - removalInFlight, - rowFailures, - removalInFlightRef, - clearRowFailures, - resetForOpen, - applyScanDefaults, - openConfirmRemove, - cancelConfirmRemove, - backToWorkspaceCleanupList, - confirmRemove - } -} - -function getDefaultSelectedWorkspaceCleanupIds( - candidates: readonly WorkspaceCleanupCandidate[], - deletingWorktreeIds: ReadonlySet -): Set { - return new Set( - candidates - .filter( - (candidate) => candidate.selectedByDefault && !deletingWorktreeIds.has(candidate.worktreeId) - ) - .map((candidate) => candidate.worktreeId) - ) -} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-row-list.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-row-list.tsx new file mode 100644 index 00000000000..e706c60b486 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-row-list.tsx @@ -0,0 +1,98 @@ +import React from 'react' +import { translate } from '@/i18n/i18n' +import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' +import { formatBytes } from '../status-bar/workspace-space-format' +import { CandidateRow, type WorkspaceCleanupDeletionPhase } from './workspace-cleanup-candidate-row' +import { WorkspaceCleanupCandidateList } from './workspace-cleanup-candidate-list' +import { WorkspaceCleanupEmptyState } from './workspace-cleanup-dialog-notices' +import type { WorkspaceCleanupFacets } from './workspace-cleanup-facets' +import { formatWorkspaceCleanupRelativeTime } from './workspace-cleanup-relative-time' + +export type WorkspaceCleanupRowListState = { + rows: readonly WorkspaceCleanupFacets[] + /** The dialog's "as of" clock; keeps row labels consistent with filter bucketing. */ + now: number + /** Rows the scan produced before filtering; separates "empty fleet" from "no matches". */ + scannedCount: number + hasScanned: boolean + loading: boolean + deletionPhaseByWorktreeId: Record + deletingWorktreeIds: ReadonlySet + expandedRowIds: ReadonlySet + selectedIds: ReadonlySet + gitPendingWorktreeIds: ReadonlySet + rowFailures: Record + removalInFlight: boolean + scrollElement: HTMLDivElement | null + onClearFilters: () => void + onToggleExpanded: (worktreeId: string) => void + onToggleSelected: (worktreeId: string) => void + onView: (candidate: WorkspaceCleanupCandidate) => void + onIgnore: (candidate: WorkspaceCleanupCandidate) => void + onRemove: (candidate: WorkspaceCleanupCandidate) => void +} + +export function WorkspaceCleanupRowList(props: WorkspaceCleanupRowListState): React.JSX.Element { + const { rows, loading, hasScanned, scannedCount } = props + const settled = !loading && hasScanned + return ( + <> + {settled && scannedCount === 0 ? ( + + ) : null} + {hasScanned && scannedCount > 0 && rows.length === 0 ? ( + + ) : null} + ( + 1 && index === rows.length - 1} + expanded={props.expandedRowIds.has(row.worktreeId)} + lastActivityLabel={formatWorkspaceCleanupRelativeTime(row.lastActivityAt, props.now)} + sizeLabel={row.sizeBytes === null ? null : formatBytes(row.sizeBytes)} + workspaceStatusLabel={row.workspaceStatusLabel} + gitEvidencePending={props.gitPendingWorktreeIds.has(row.worktreeId)} + deletionPhase={props.deletionPhaseByWorktreeId[row.worktreeId]} + // Why: a background rescan must not lock rows; only an actual + // removal batch or this row's own deletion disables it. + removing={props.removalInFlight || props.deletingWorktreeIds.has(row.worktreeId)} + selected={ + props.selectedIds.has(row.worktreeId) && + !props.deletingWorktreeIds.has(row.worktreeId) + } + failure={props.rowFailures[row.worktreeId]} + onToggleExpanded={props.onToggleExpanded} + onToggleSelected={props.onToggleSelected} + onView={props.onView} + onIgnore={props.onIgnore} + onRemove={props.onRemove} + /> + )} + /> + + ) +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-scan-notice.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-scan-notice.ts new file mode 100644 index 00000000000..e05d4b2caa7 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-scan-notice.ts @@ -0,0 +1,121 @@ +import { translate } from '@/i18n/i18n' +import type { + WorkspaceCleanupScanError, + WorkspaceCleanupScanProgress +} from '../../../../shared/workspace-cleanup' + +function isDisconnectedRemoteScanError(message: string): boolean { + return ( + message === 'SSH provider is unavailable.' || + message === 'Remote workspaces are not connected. Reconnect and refresh to check them.' + ) +} + +export function formatWorkspaceCleanupScanNotice( + errors: readonly WorkspaceCleanupScanError[], + repoNameById: ReadonlyMap +): string | null { + const visibleErrors = errors.filter( + (error) => !isDisconnectedRemoteScanError(error.message ?? '') + ) + if (visibleErrors.length === 0) { + return null + } + if (visibleErrors.length === 1) { + const error = visibleErrors[0] + const repoName = formatScanErrorRepoName(error, repoNameById) + return translate( + 'components.workspace.cleanup.scan.singleError', + 'Could not check {{value0}}: {{value1}}. Some workspaces may be missing. Refresh to try again.', + { value0: repoName, value1: formatScanErrorReason(error.message) } + ) + } + const repoNames = visibleErrors + .slice(0, 3) + .map((error) => formatScanErrorRepoName(error, repoNameById)) + .join(', ') + const moreCount = visibleErrors.length - 3 + const suffix = + moreCount > 0 + ? translate('components.workspace.cleanup.scan.moreErrors', ', +{{value0}} more', { + value0: moreCount + }) + : '' + return translate( + 'components.workspace.cleanup.scan.multipleErrors', + 'Could not check {{value0}} repositories ({{value1}}{{value2}}). Some workspaces may be missing. Refresh to try again.', + { value0: visibleErrors.length, value1: repoNames, value2: suffix } + ) +} + +export function formatWorkspaceCleanupScanProgress( + progress: WorkspaceCleanupScanProgress | null +): string { + if (!progress || progress.scannedWorktreeCount === 0) { + return translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.4cc5b73efe', + 'Finding workspaces...' + ) + } + return translate( + 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.7b7bde5181', + 'Checked workspaces so far: {{value0}}', + { value0: progress.scannedWorktreeCount } + ) +} + +export function formatWorkspaceCleanupReadyToast( + workspaceCount: number, + suggestedCount: number +): string { + if (workspaceCount === 0) { + return translate('components.workspace.cleanup.scan.noWorkspaces', 'No workspaces found.') + } + if (workspaceCount === 1) { + return suggestedCount === 1 + ? translate( + 'components.workspace.cleanup.scan.readyOneOne', + '1 workspace found, with 1 cleanup suggestion.' + ) + : translate( + 'components.workspace.cleanup.scan.readyOneMany', + '1 workspace found, with {{value0}} cleanup suggestions.', + { value0: suggestedCount } + ) + } + return suggestedCount === 1 + ? translate( + 'components.workspace.cleanup.scan.readyManyOne', + '{{value0}} workspaces found, with 1 cleanup suggestion.', + { value0: workspaceCount } + ) + : translate( + 'components.workspace.cleanup.scan.readyManyMany', + '{{value0}} workspaces found, with {{value1}} cleanup suggestions.', + { value0: workspaceCount, value1: suggestedCount } + ) +} + +function formatScanErrorRepoName( + error: Partial, + repoNameById: ReadonlyMap +): string { + const repoName = error.repoName?.trim() + if (repoName) { + return repoName + } + const fallback = error.repoId ? repoNameById.get(error.repoId)?.trim() : '' + return ( + fallback || translate('components.workspace.cleanup.scan.fallbackRepository', 'a repository') + ) +} + +function formatScanErrorReason(message: string | undefined): string { + if (!message || message === 'Could not scan workspace cleanup for this repository.') { + return translate( + 'components.workspace.cleanup.scan.gitListFailed', + 'Git could not list worktrees' + ) + } + return message.replace(/\.$/, '') +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-scan-session.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-scan-session.ts deleted file mode 100644 index fc4cd59b1fd..00000000000 --- a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-scan-session.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { useCallback, useEffect, useRef, type RefObject } from 'react' -import { toast } from 'sonner' -import type { AppState } from '@/store/types' -import { translate } from '@/i18n/i18n' - -export function useWorkspaceCleanupScanSession({ - open, - mountedRef, - openModal, - scanWorkspaceCleanup, - clearRowFailures -}: { - open: boolean - mountedRef: RefObject - openModal: AppState['openModal'] - scanWorkspaceCleanup: AppState['scanWorkspaceCleanup'] - clearRowFailures: () => void -}): (options?: { notifyWhenReady?: boolean }) => void { - const openRef = useRef(open) - const latestReadyToastScanAtRef = useRef(null) - - useEffect(() => { - openRef.current = open - }, [open]) - - return useCallback( - (options: { notifyWhenReady?: boolean } = {}) => { - clearRowFailures() - void scanWorkspaceCleanup() - .then((result) => { - if (!mountedRef.current || !options.notifyWhenReady || openRef.current) { - return - } - if (latestReadyToastScanAtRef.current === result.scannedAt) { - return - } - latestReadyToastScanAtRef.current = result.scannedAt - const suggestedCount = result.candidates.filter( - (candidate) => candidate.selectedByDefault - ).length - toast.success( - translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.0e2d235c63', - 'Inactive workspace scan ready' - ), - { - description: formatWorkspaceCleanupReadyToastDescription( - result.candidates.length, - suggestedCount - ), - action: { - label: translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.4a35c08764', - 'Review' - ), - onClick: () => openModal('workspace-cleanup') - } - } - ) - }) - .catch((error: unknown) => { - if (!mountedRef.current) { - return - } - toast.error( - translate( - 'auto.components.workspace.cleanup.WorkspaceCleanupDialog.662b8ec3f8', - 'Workspace cleanup scan failed' - ), - { description: error instanceof Error ? error.message : String(error) } - ) - }) - }, - [clearRowFailures, mountedRef, openModal, scanWorkspaceCleanup] - ) -} - -function formatWorkspaceCleanupReadyToastDescription( - inactiveCount: number, - suggestedCount: number -): string { - if (inactiveCount === 0) { - return 'No inactive workspaces found.' - } - const inactiveNoun = inactiveCount === 1 ? 'workspace' : 'workspaces' - const suggestedNoun = suggestedCount === 1 ? 'suggestion' : 'suggestions' - return `${inactiveCount} inactive ${inactiveNoun} found, with ${suggestedCount} cleanup ${suggestedNoun}.` -} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-selection-model.ts b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-selection-model.ts new file mode 100644 index 00000000000..b32e789a5b7 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-selection-model.ts @@ -0,0 +1,38 @@ +import { translate } from '@/i18n/i18n' +import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup' + +export function formatVanishedSelectionNotice(count: number): string { + return count === 1 + ? translate( + 'components.workspace.cleanup.browse.selectionVanishedOne', + '1 selected workspace no longer exists.' + ) + : translate( + 'components.workspace.cleanup.browse.selectionVanished', + '{{value0}} selected workspaces no longer exist.', + { value0: count } + ) +} + +export function getDefaultSelectedWorkspaceCleanupIds( + candidates: readonly WorkspaceCleanupCandidate[], + deletingWorktreeIds: ReadonlySet = new Set() +): Set { + return new Set( + candidates + .filter( + (candidate) => candidate.selectedByDefault && !deletingWorktreeIds.has(candidate.worktreeId) + ) + .map((candidate) => candidate.worktreeId) + ) +} + +export function toggleSetMember(current: Set, value: string): Set { + const next = new Set(current) + if (next.has(value)) { + next.delete(value) + } else { + next.add(value) + } + return next +} diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.test.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.test.tsx new file mode 100644 index 00000000000..0300865b531 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.test.tsx @@ -0,0 +1,95 @@ +// @vitest-environment happy-dom +import { act, type ReactNode } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { WorkspaceCleanupSortState } from '../../../../shared/workspace-cleanup-filter-model' +import { WorkspaceCleanupSortHeader } from './workspace-cleanup-sort-header' + +vi.mock('@/components/ui/dropdown-menu', () => ({ + DropdownMenu: ({ children }: { children: ReactNode }) => <>{children}, + DropdownMenuContent: ({ children }: { children: ReactNode }) => <>{children}, + DropdownMenuItem: ({ children, onSelect }: { children: ReactNode; onSelect: () => void }) => ( + + ), + DropdownMenuLabel: ({ children }: { children: ReactNode }) => <>{children}, + DropdownMenuTrigger: ({ children }: { children: ReactNode }) => <>{children} +})) + +let root: Root | null = null +let container: HTMLDivElement | null = null + +function render(sort: WorkspaceCleanupSortState, handlers: Record = {}): void { + act(() => + root?.render( + + ) + ) +} + +function sortOption(label: string): HTMLButtonElement | undefined { + return [...(container?.querySelectorAll('[data-sort-option]') ?? [])].find( + (button) => button.textContent?.trim() === label + ) +} + +describe('WorkspaceCleanupSortHeader', () => { + beforeEach(() => { + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + }) + + afterEach(() => { + if (root) { + act(() => root?.unmount()) + } + container?.remove() + root = null + container = null + }) + + it('shows the active sort in one menu trigger', () => { + render({ field: 'size', direction: 'desc' }) + + expect(container?.querySelector('[aria-label="Sort by Size"]')?.textContent).toContain( + 'Sort by Size' + ) + }) + + it('routes a menu choice through the sort toggle', () => { + const onToggleSortField = vi.fn() + render({ field: 'last-activity', direction: 'asc' }, { onToggleSortField }) + + act(() => sortOption('Repository')?.click()) + + expect(onToggleSortField).toHaveBeenCalledWith('repo') + }) + + it('select-all reads the query result rather than the rendered page', () => { + const onToggleSelectAll = vi.fn() + render({ field: 'name', direction: 'asc' }, { onToggleSelectAll }) + + const checkbox = container?.querySelector('[role="checkbox"]') + expect(checkbox?.getAttribute('aria-checked')).toBe('false') + act(() => checkbox?.click()) + + expect(onToggleSelectAll).toHaveBeenCalledWith(true) + }) + + it('exposes a partial selection as mixed', () => { + render({ field: 'name', direction: 'asc' }, { selectableCount: 3, selectedCount: 1 }) + + expect( + container?.querySelector('[role="checkbox"]')?.getAttribute('aria-checked') + ).toBe('mixed') + }) +}) diff --git a/src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.tsx b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.tsx new file mode 100644 index 00000000000..2e8de45c1a8 --- /dev/null +++ b/src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.tsx @@ -0,0 +1,101 @@ +import React from 'react' +import { ArrowDown, ArrowUp, Check, ChevronsUpDown, Minus } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { translate } from '@/i18n/i18n' +import { WORKSPACE_CLEANUP_SORT_FIELD_VALUES } from '../../../../shared/workspace-cleanup-facet-rankings' +import type { + WorkspaceCleanupSortField, + WorkspaceCleanupSortState +} from '../../../../shared/workspace-cleanup-filter-model' +import { getWorkspaceCleanupSortFieldLabel } from './workspace-cleanup-facet-labels' + +export function WorkspaceCleanupSortHeader({ + sort, + selectableCount, + selectedCount, + onToggleSortField, + onToggleSelectAll +}: { + sort: WorkspaceCleanupSortState + selectableCount: number + selectedCount: number + onToggleSortField: (field: WorkspaceCleanupSortField) => void + onToggleSelectAll: (selectAll: boolean) => void +}): React.JSX.Element { + const allSelected = selectableCount > 0 && selectedCount >= selectableCount + const someSelected = selectableCount > 0 && selectedCount > 0 && !allSelected + return ( +
+ + + {translate( + 'components.workspace.cleanup.browse.selectAll', + 'Select all matching workspaces' + )} + + + + + + + + {translate('components.workspace.cleanup.browse.sortBy', 'Sort by')} + + {WORKSPACE_CLEANUP_SORT_FIELD_VALUES.map((field) => ( + onToggleSortField(field)}> + {getWorkspaceCleanupSortFieldLabel(field)} + {sort.field === field ? : null} + + ))} + + +
+ ) +} + +function SortDirectionIcon({ direction }: { direction: 'asc' | 'desc' }): React.JSX.Element { + return direction === 'asc' ? ( +