From d3381e550c9eb0fbb72b69cb1b0bcf4af564b137 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:28:25 -0700 Subject: [PATCH 01/11] fix(terminal): stop leaking raw PTY lifecycle tokens into the error toast (#14415) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(terminal): stop raw PTY-not-found and session-expired tokens reaching the error toast A reattach the host answers "no such session" for surfaced its wire token verbatim — `SSH_SESSION_EXPIRED: orca:@@pty-N` or the relay's raw `PTY "..." not found` — including the internal PTY id, and invited the user to file an issue for an ordinary lifecycle event. Humanize both in the toast's existing daemon-boundary seam and mark them explained so the issue link is suppressed. The copy stays silent on whether the remote shell died: absence from the host is not proof of exit. Refs STA-4238 * style(terminal): apply oxfmt to the toast humanization tests * fix(terminal): treat the humanized session copy as literal replacement text --- .../terminal-pane/TerminalErrorToast.test.ts | 54 +++++++++++++++++++ .../terminal-pane/TerminalErrorToast.tsx | 30 ++++++++++- src/renderer/src/i18n/locales/en.json | 3 +- 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/TerminalErrorToast.test.ts b/src/renderer/src/components/terminal-pane/TerminalErrorToast.test.ts index 67d2cadd3b9..069ed371ec0 100644 --- a/src/renderer/src/components/terminal-pane/TerminalErrorToast.test.ts +++ b/src/renderer/src/components/terminal-pane/TerminalErrorToast.test.ts @@ -109,6 +109,51 @@ describe('humanizeTerminalError', () => { expect(humanized).toContain('\nterminal_host_gone_extra') }) + it('replaces an expired SSH session token and its internal relay id', () => { + const wrapped = + "Error invoking remote method 'pty:spawn': Error: SSH_SESSION_EXPIRED: orca:2f1c@@pty-7" + const humanized = humanizeTerminalError(wrapped) + expect(humanized).not.toContain('SSH_SESSION_EXPIRED') + expect(humanized).not.toContain('orca:2f1c@@pty-7') + expect(humanized).toContain('Open a new terminal to continue') + }) + + it('replaces an expired SSH session token carrying the identity-mismatch marker', () => { + const humanized = humanizeTerminalError( + 'SSH_SESSION_EXPIRED: orca:2f1c@@pty-7 SSH_PTY_IDENTITY_MISMATCH' + ) + expect(humanized).not.toContain('SSH_SESSION_EXPIRED') + expect(humanized).not.toContain('SSH_PTY_IDENTITY_MISMATCH') + }) + + it('replaces a raw relay PTY-not-found string and its quoted id', () => { + const humanized = humanizeTerminalError( + 'Error invoking remote method \'pty:spawn\': Error: PTY "orca:2f1c@@pty-7" not found' + ) + expect(humanized).not.toContain('not found') + expect(humanized).not.toContain('orca:2f1c@@pty-7') + expect(humanized).toContain('Open a new terminal to continue') + }) + + it('replaces the identity-mismatch form of PTY-not-found', () => { + const humanized = humanizeTerminalError('PTY "orca:2f1c@@pty-7" not found (identity mismatch)') + expect(humanized).not.toContain('identity mismatch') + expect(humanized).not.toContain('orca:2f1c@@pty-7') + }) + + // Why: "no such session" from the relay is not proof the remote shell died, so the copy must not claim either. + it('does not claim the remote shell is still running or dead', () => { + const humanized = humanizeTerminalError('SSH_SESSION_EXPIRED: orca:2f1c@@pty-7') + expect(humanized).not.toContain('may still be running') + expect(humanized).not.toContain('exited') + }) + + it('replaces only the unreattachable line in an aggregated error', () => { + const humanized = humanizeTerminalError('Paste failed.\nSSH_SESSION_EXPIRED: orca:2f1c@@pty-7') + expect(humanized.startsWith('Paste failed.\n')).toBe(true) + expect(humanized).not.toContain('SSH_SESSION_EXPIRED') + }) + it.each(['ENOENT', 'ECONNREFUSED'])( 'does not combine a %s connection failure with a host endpoint on another line', (code) => { @@ -134,6 +179,15 @@ describe('isExplainedTerminalError', () => { ).toBe(true) }) + it('suppresses the issue link for a session the host cannot reattach', () => { + expect(isExplainedTerminalError('SSH_SESSION_EXPIRED: orca:2f1c@@pty-7')).toBe(true) + expect( + isExplainedTerminalError( + 'Error invoking remote method \'pty:spawn\': Error: PTY "orca:2f1c@@pty-7" not found' + ) + ).toBe(true) + }) + it('keeps the issue link for errors Orca cannot explain', () => { expect(isExplainedTerminalError('Paste failed.')).toBe(false) expect(isExplainedTerminalError('node-pty: open_slave failed: EMFILE')).toBe(false) diff --git a/src/renderer/src/components/terminal-pane/TerminalErrorToast.tsx b/src/renderer/src/components/terminal-pane/TerminalErrorToast.tsx index 3c65a1b56f5..6aa2bba6252 100644 --- a/src/renderer/src/components/terminal-pane/TerminalErrorToast.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalErrorToast.tsx @@ -25,6 +25,19 @@ const TERMINAL_HOST_GONE_PATTERN = new RegExp(TERMINAL_HOST_GONE_SOURCE) const TERMINAL_HOST_GONE_REPLACE_PATTERN = new RegExp(TERMINAL_HOST_GONE_SOURCE, 'g') const LEGACY_TERMINAL_HOST_GONE_PATTERN = /(^|[^a-z])connect (?:ENOENT|ECONNREFUSED) [^\r\n]*orca-terminal-host-v[^\r\n]*/i +// A reattach the host answered "no such session" for: the SSH provider's expiry token, or the relay's +// raw not-found string when nothing mapped it. Both carry an internal PTY id, and neither is proof the +// remote shell died — the copy says only that this pane lost its session. Same lastIndex hazard as above. +const UNREATTACHABLE_SESSION_SOURCES = [ + 'SSH_SESSION_EXPIRED:[ \\t]*\\S*(?:[ \\t]+SSH_PTY_IDENTITY_MISMATCH)?', + 'PTY "[^"\\r\\n]*" not found(?: \\(identity mismatch\\))?' +] +const UNREATTACHABLE_SESSION_PATTERNS = UNREATTACHABLE_SESSION_SOURCES.map( + (source) => new RegExp(source) +) +const UNREATTACHABLE_SESSION_REPLACE_PATTERNS = UNREATTACHABLE_SESSION_SOURCES.map( + (source) => new RegExp(source, 'g') +) function isSshError(error: string): boolean { return isSshReconnectOwnedTerminalError(error) @@ -60,10 +73,24 @@ export function isExplainedTerminalError(error: string): boolean { .split('\n') .some( (line) => - TERMINAL_HOST_GONE_PATTERN.test(line) || LEGACY_TERMINAL_HOST_GONE_PATTERN.test(line) + TERMINAL_HOST_GONE_PATTERN.test(line) || + LEGACY_TERMINAL_HOST_GONE_PATTERN.test(line) || + UNREATTACHABLE_SESSION_PATTERNS.some((pattern) => pattern.test(line)) ) } +function humanizeUnreattachableSession(error: string): string { + const explanation = translate( + 'auto.components.terminal.pane.TerminalErrorToast.sessionUnavailable', + "Orca couldn't reattach to this pane's terminal session on the host. Open a new terminal to continue." + ) + // Why a replacer: a translation containing `$&` or `$1` would otherwise be read as a substitution. + return UNREATTACHABLE_SESSION_REPLACE_PATTERNS.reduce( + (message, pattern) => message.replace(pattern, () => explanation), + error + ) +} + /** Swaps raw daemon-boundary codes for copy a user can act on. */ export function humanizeTerminalError(error: string): string { let humanized = error @@ -76,6 +103,7 @@ export function humanizeTerminalError(error: string): string { ) ) } + humanized = humanizeUnreattachableSession(humanized) if (!isExplainedTerminalError(humanized)) { return humanized } diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 03a2830bb43..07ab7693ec8 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -2875,7 +2875,8 @@ "5c8ce20be6": "If this persists, please", "cc6d997c65": "Restart the terminal daemon from here to clear stale daemon state.", "7ee11bc0db": "Orca couldn't confirm whether this terminal's previous session is still running, so it left the session untouched. Reopen this pane to retry.", - "e16012e31e": "The terminal daemon that owned this session exited, so the session and its scrollback could not be recovered. Open a new terminal to continue." + "e16012e31e": "The terminal daemon that owned this session exited, so the session and its scrollback could not be recovered. Open a new terminal to continue.", + "sessionUnavailable": "Orca couldn't reattach to this pane's terminal session on the host. Open a new terminal to continue." }, "TerminalPane": { "ac112e9036": "Remove title", From 17690d49eab6728556ef34a2db340644fce61661 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:28:29 -0700 Subject: [PATCH 02/11] fix(repos): re-probe git remote identity so a stale snapshot stops misjudging identity gates (#14414) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(repo-identity): re-probe resolved git remote identities on a long TTL A resolved gitRemoteIdentity was written once and frozen for the life of the repo record, so adding an `upstream` remote later — or a project rename or transfer — left identity gates judging against the path the repo had when it was added. Re-probe resolved repos on a 6h TTL, seeded 5 minutes after a repo is first seen in a process and capped at 4 refreshes per sweep so a restart cannot fan out a subprocess per repo. Only a successful probe that yields a different canonicalKey overwrites; failures and no-remote answers leave the existing identity alone. Also explain why the worktree-scan admin fingerprint timeout deliberately exceeds its caller budget, and log when that probe expires — expiry was silent and indistinguishable from "fingerprint unavailable". Refs STA-4247 * fix(projects): carry project state across derived project id changes A project id is derived from repo identity, so a remote re-probe (or a repo:->git:->github: promotion) rewrites it. The compatibility merge matched prior rows by id only, dropping the user's localWindowsRuntimePreference and leaving a ghost project row that independent host setups still pointed at. Both merge sites now fall back to the prior row whose sourceRepoIds overlap and re-point independent setups at the surviving project. --- .../profile-project-state-file.test.ts | 153 ++++++++++++++++ .../profile-project-state-file.ts | 33 ++-- src/main/persistence.test.ts | 118 +++++++++++++ src/main/persistence.ts | 36 ++-- ...epo-git-remote-identity-enrichment.test.ts | 165 +++++++++++++++++- .../repo-git-remote-identity-enrichment.ts | 93 +++++++--- src/main/runtime/orca-runtime.ts | 13 +- .../project-identity-succession.test.ts | 138 +++++++++++++++ src/shared/project-identity-succession.ts | 91 ++++++++++ 9 files changed, 780 insertions(+), 60 deletions(-) create mode 100644 src/main/orca-profiles/profile-project-state-file.test.ts create mode 100644 src/shared/project-identity-succession.test.ts create mode 100644 src/shared/project-identity-succession.ts diff --git a/src/main/orca-profiles/profile-project-state-file.test.ts b/src/main/orca-profiles/profile-project-state-file.test.ts new file mode 100644 index 00000000000..223d1229555 --- /dev/null +++ b/src/main/orca-profiles/profile-project-state-file.test.ts @@ -0,0 +1,153 @@ +import { describe, expect, it } from 'vitest' +import { getDefaultPersistedState } from '../../shared/constants' +import type { PersistedState, Project, ProjectHostSetup, Repo } from '../../shared/types' +import { rebuildRepoBackedProjectState } from './profile-project-state-file' + +const upstreamIdentity = { + canonicalKey: 'git.example.com/acme/app-upstream', + remoteName: 'upstream', + remoteUrl: 'git@git.example.com:acme/app-upstream.git' +} + +const makeRepo = (overrides: Partial = {}): Repo => ({ + id: 'r1', + path: '/repo', + displayName: 'App', + badgeColor: '#fff', + addedAt: 1, + ...overrides +}) + +const makeProject = (overrides: Partial = {}): Project => ({ + id: 'project-1', + displayName: 'App', + badgeColor: '#737373', + sourceRepoIds: [], + createdAt: 1, + updatedAt: 1, + ...overrides +}) + +const makeSetup = (overrides: Partial = {}): ProjectHostSetup => ({ + id: 'setup-1', + projectId: 'project-1', + hostId: 'local', + repoId: '', + path: '/repo', + displayName: 'App', + setupState: 'ready', + setupMethod: 'imported-existing-folder', + createdAt: 1, + updatedAt: 1, + ...overrides +}) + +function makeState(overrides: Partial): PersistedState { + return { ...getDefaultPersistedState('/home/test'), ...overrides } +} + +describe('rebuildRepoBackedProjectState', () => { + it('carries project state and independent setups across a repo remote identity change', () => { + const originProjectId = 'git:git.example.com/acme/app' + const rebuilt = rebuildRepoBackedProjectState( + makeState({ + repos: [makeRepo({ gitRemoteIdentity: upstreamIdentity })], + projects: [ + makeProject({ + id: originProjectId, + sourceRepoIds: ['r1'], + localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' } + }) + ], + projectHostSetups: [ + makeSetup({ id: 'r1', projectId: originProjectId, repoId: 'r1' }), + makeSetup({ + id: 'app::gpu-vm', + projectId: originProjectId, + hostId: 'runtime:gpu-vm', + path: '/srv/app' + }) + ] + }) + ) + + expect(rebuilt.projects).toEqual([ + expect.objectContaining({ + id: 'git:git.example.com/acme/app-upstream', + localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' } + }) + ]) + expect(rebuilt.projectHostSetups.find((setup) => setup.id === 'app::gpu-vm')?.projectId).toBe( + 'git:git.example.com/acme/app-upstream' + ) + }) + + it('picks one predecessor project when several prior rows overlap the same repos', () => { + const sharedIdentity = { + canonicalKey: 'git.example.com/acme/shared', + remoteName: 'origin', + remoteUrl: 'git@git.example.com:acme/shared.git' + } + const rebuilt = rebuildRepoBackedProjectState( + makeState({ + repos: [ + makeRepo({ id: 'r1', path: '/left', gitRemoteIdentity: sharedIdentity }), + makeRepo({ id: 'r2', path: '/right', gitRemoteIdentity: sharedIdentity }) + ], + projects: [ + makeProject({ + id: 'git:git.example.com/acme/left', + sourceRepoIds: ['r1'], + updatedAt: 200, + localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' } + }), + makeProject({ + id: 'git:git.example.com/acme/right', + sourceRepoIds: ['r2'], + updatedAt: 100, + localWindowsRuntimePreference: { kind: 'windows-host' } + }) + ], + projectHostSetups: [ + makeSetup({ id: 'r1', projectId: 'git:git.example.com/acme/left', repoId: 'r1' }), + makeSetup({ id: 'r2', projectId: 'git:git.example.com/acme/right', repoId: 'r2' }) + ] + }) + ) + + // Equal repo overlap resolves by newest updatedAt; the loser's preference is never merged in. + expect(rebuilt.projects).toEqual([ + expect.objectContaining({ + id: 'git:git.example.com/acme/shared', + sourceRepoIds: ['r1', 'r2'], + localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' } + }) + ]) + }) + + it('leaves an unclaimed prior project standing with its own independent setups', () => { + const rebuilt = rebuildRepoBackedProjectState( + makeState({ + repos: [makeRepo({ gitRemoteIdentity: upstreamIdentity })], + projects: [ + makeProject({ + id: 'cloud-project', + localWindowsRuntimePreference: { kind: 'windows-host' } + }) + ], + projectHostSetups: [ + makeSetup({ id: 'cloud-project::gpu-vm', projectId: 'cloud-project', path: '/srv/cloud' }) + ] + }) + ) + + expect(rebuilt.projects.map((project) => project.id)).toEqual([ + 'git:git.example.com/acme/app-upstream', + 'cloud-project' + ]) + expect(rebuilt.projectHostSetups.map((setup) => setup.projectId)).toEqual([ + 'git:git.example.com/acme/app-upstream', + 'cloud-project' + ]) + }) +}) diff --git a/src/main/orca-profiles/profile-project-state-file.ts b/src/main/orca-profiles/profile-project-state-file.ts index 4169fea6ffc..7e00533350f 100644 --- a/src/main/orca-profiles/profile-project-state-file.ts +++ b/src/main/orca-profiles/profile-project-state-file.ts @@ -5,6 +5,7 @@ import { dirname } from 'node:path' import { getDefaultPersistedState, getDefaultWorkspaceSession } from '../../shared/constants' import type { ExecutionHostId } from '../../shared/execution-host' import { projectHostSetupProjectionFromRepos } from '../../shared/project-host-setup-projection' +import { carryProjectStateThroughIdentityChange } from '../../shared/project-identity-succession' import type { PersistedState, Project, @@ -102,16 +103,22 @@ function isRepoBackedProjectHostSetup( export function rebuildRepoBackedProjectState(state: TransferProfileState): TransferProfileState { const projection = projectHostSetupProjectionFromRepos(state.repos) - const existingProjectsById = new Map(state.projects.map((project) => [project.id, project])) + const succession = carryProjectStateThroughIdentityChange(projection.projects, state.projects) const currentRepoIds = new Set(state.repos.map((repo) => repo.id)) const projectedProjectIds = new Set(projection.projects.map((project) => project.id)) const projectedSetupIds = new Set(projection.setups.map((setup) => setup.id)) - const independentSetups = state.projectHostSetups.filter((setup) => { - if (projectedSetupIds.has(setup.id)) { - return false - } - return !isRepoBackedProjectHostSetup(setup, currentRepoIds) - }) + const independentSetups = state.projectHostSetups + .filter((setup) => { + if (projectedSetupIds.has(setup.id)) { + return false + } + return !isRepoBackedProjectHostSetup(setup, currentRepoIds) + }) + // Why: follow the repo's project through a derived-id change so no ghost project row survives. + .map((setup) => { + const remappedProjectId = succession.remappedProjectIds.get(setup.projectId) + return remappedProjectId ? { ...setup, projectId: remappedProjectId } : setup + }) const independentProjectIds = new Set(independentSetups.map((setup) => setup.projectId)) const independentProjects = state.projects .filter( @@ -121,19 +128,9 @@ export function rebuildRepoBackedProjectState(state: TransferProfileState): Tran ...project, sourceRepoIds: project.sourceRepoIds.filter((repoId) => currentRepoIds.has(repoId)) })) - const projectedProjects = projection.projects.map((project) => { - const existingProject = existingProjectsById.get(project.id) - return existingProject?.localWindowsRuntimePreference - ? { - ...project, - localWindowsRuntimePreference: existingProject.localWindowsRuntimePreference, - updatedAt: Math.max(project.updatedAt, existingProject.updatedAt) - } - : project - }) return { ...state, - projects: [...projectedProjects, ...independentProjects], + projects: [...succession.projects, ...independentProjects], projectHostSetups: [...projection.setups, ...independentSetups] } } diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts index f8f211e9ced..b2c56cf1aca 100644 --- a/src/main/persistence.test.ts +++ b/src/main/persistence.test.ts @@ -595,6 +595,124 @@ describe('Store', () => { }) }) + it('carries project state and independent setups across a repo remote identity change', async () => { + const originProjectId = 'git:git.example.com/acme/app' + writeDataFile({ + ...getDefaultPersistedState(testState.dir), + repos: [ + makeRepo({ + id: 'r1', + path: '/repo', + displayName: 'App', + gitRemoteIdentity: { + canonicalKey: 'git.example.com/acme/app', + remoteName: 'origin', + remoteUrl: 'git@git.example.com:acme/app.git' + } + }) + ], + projects: [ + makeProject({ + id: originProjectId, + sourceRepoIds: ['r1'], + localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' } + }) + ], + projectHostSetups: [ + makeProjectHostSetup({ id: 'r1', projectId: originProjectId, repoId: 'r1' }), + makeProjectHostSetup({ + id: 'app::gpu-vm', + projectId: originProjectId, + hostId: 'runtime:gpu-vm', + repoId: '', + path: '/srv/app' + }) + ] + }) + const store = await createStore() + + // A re-probe that now prefers the `upstream` remote rewrites the derived project id. + store.updateRepo('r1', { + gitRemoteIdentity: { + canonicalKey: 'git.example.com/acme/app-upstream', + remoteName: 'upstream', + remoteUrl: 'git@git.example.com:acme/app-upstream.git' + } + }) + + const upstreamProjectId = 'git:git.example.com/acme/app-upstream' + expect(store.getProjects().map((project) => project.id)).toEqual([upstreamProjectId]) + expect(store.getProjects()[0]?.localWindowsRuntimePreference).toEqual({ + kind: 'wsl', + distro: 'Ubuntu' + }) + expect( + store.getProjectHostSetups().find((setup) => setup.id === 'app::gpu-vm')?.projectId + ).toBe(upstreamProjectId) + }) + + it('picks one predecessor project when several prior rows overlap the same repos', async () => { + const sharedIdentity = { + canonicalKey: 'git.example.com/acme/shared', + remoteName: 'origin', + remoteUrl: 'git@git.example.com:acme/shared.git' + } + writeDataFile({ + ...getDefaultPersistedState(testState.dir), + repos: [ + makeRepo({ + id: 'r1', + path: '/left', + displayName: 'Left', + gitRemoteIdentity: sharedIdentity + }), + makeRepo({ + id: 'r2', + path: '/right', + displayName: 'Right', + gitRemoteIdentity: sharedIdentity + }) + ], + projects: [ + makeProject({ + id: 'git:git.example.com/acme/left', + sourceRepoIds: ['r1'], + updatedAt: 200, + localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' } + }), + makeProject({ + id: 'git:git.example.com/acme/right', + sourceRepoIds: ['r2'], + updatedAt: 100, + localWindowsRuntimePreference: { kind: 'windows-host' } + }) + ], + projectHostSetups: [ + makeProjectHostSetup({ + id: 'r1', + projectId: 'git:git.example.com/acme/left', + repoId: 'r1' + }), + makeProjectHostSetup({ + id: 'r2', + projectId: 'git:git.example.com/acme/right', + repoId: 'r2' + }) + ] + }) + + const store = await createStore() + + // Equal repo overlap resolves by newest updatedAt; the loser's preference is never merged in. + expect(store.getProjects()).toEqual([ + expect.objectContaining({ + id: 'git:git.example.com/acme/shared', + sourceRepoIds: ['r1', 'r2'], + localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' } + }) + ]) + }) + it('migrates legacy WSL agent settings into the global Windows runtime default', async () => { writeDataFile({ schemaVersion: 1, diff --git a/src/main/persistence.ts b/src/main/persistence.ts index b7f165bafcb..d58b621c94b 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -79,6 +79,7 @@ import { normalizeProjectRuntimePreference } from '../shared/project-execution-runtime' import { projectHostSetupProjectionFromRepos } from '../shared/project-host-setup-projection' +import { carryProjectStateThroughIdentityChange } from '../shared/project-identity-succession' import { isPluginPanelTabKey } from '../shared/plugins/plugin-manifest' import type { GitRemoteIdentity } from '../shared/git-remote-identity' import { @@ -2466,19 +2467,26 @@ function mergeProjectHostSetupCompatibilityState( repos: readonly Repo[] ): Pick { const projection = projectHostSetupProjectionFromRepos(repos) - const existingProjectsById = new Map( - (state.projects ?? []).map((project) => [project.id, project]) + const succession = carryProjectStateThroughIdentityChange( + projection.projects, + state.projects ?? [] ) const currentRepoIds = new Set(repos.map((repo) => repo.id)) const projectedProjectIds = new Set(projection.projects.map((project) => project.id)) const projectedSetupIds = new Set(projection.setups.map((setup) => setup.id)) // Why: legacy/repo-backed setup rows reuse the repo id; keep only independent rows so repo deletion leaves no ghosts. - const independentSetups = (state.projectHostSetups ?? []).filter((setup) => { - if (projectedSetupIds.has(setup.id)) { - return false - } - return !isRepoBackedProjectHostSetup(setup, currentRepoIds) - }) + const independentSetups = (state.projectHostSetups ?? []) + .filter((setup) => { + if (projectedSetupIds.has(setup.id)) { + return false + } + return !isRepoBackedProjectHostSetup(setup, currentRepoIds) + }) + // Why: follow the repo's project through a derived-id change so no ghost project row survives. + .map((setup) => { + const remappedProjectId = succession.remappedProjectIds.get(setup.projectId) + return remappedProjectId ? { ...setup, projectId: remappedProjectId } : setup + }) const independentProjectIds = new Set(independentSetups.map((setup) => setup.projectId)) const independentProjects = (state.projects ?? []) .filter( @@ -2488,18 +2496,8 @@ function mergeProjectHostSetupCompatibilityState( ...project, sourceRepoIds: project.sourceRepoIds.filter((repoId) => currentRepoIds.has(repoId)) })) - const projectedProjects = projection.projects.map((project) => { - const existingProject = existingProjectsById.get(project.id) - return existingProject?.localWindowsRuntimePreference - ? { - ...project, - localWindowsRuntimePreference: existingProject.localWindowsRuntimePreference, - updatedAt: Math.max(project.updatedAt, existingProject.updatedAt) - } - : project - }) return { - projects: [...projectedProjects, ...independentProjects], + projects: [...succession.projects, ...independentProjects], projectHostSetups: [...projection.setups, ...independentSetups] } } diff --git a/src/main/repo-git-remote-identity-enrichment.test.ts b/src/main/repo-git-remote-identity-enrichment.test.ts index 8d33eb8fffe..24f498b03fc 100644 --- a/src/main/repo-git-remote-identity-enrichment.test.ts +++ b/src/main/repo-git-remote-identity-enrichment.test.ts @@ -38,8 +38,7 @@ function makeRepo(overrides: Partial = {}): Repo { } } -function makeStore(repo: Repo): RepoIdentityStore & { updateRepo: ReturnType } { - const repos = [repo] +function makeStore(...repos: Repo[]): RepoIdentityStore & { updateRepo: ReturnType } { return { getRepos: () => repos, getRepo: (id) => repos.find((candidate) => candidate.id === id), @@ -65,6 +64,27 @@ function deferred(): { return { promise, resolve } } +const REFRESH_STARTUP_DELAY_MS = 5 * 60 * 1000 +const REFRESH_TTL_MS = 6 * 60 * 60 * 1000 + +const movedIdentity: GitRemoteIdentity = { + canonicalKey: 'git.company.test/platform/sample-app', + remoteName: 'upstream', + remoteUrl: 'git@git.company.test:platform/sample-app.git' +} + +/** Drains the sequential sweep: each flush only awaits the probe in flight at that moment. */ +async function drainEnrichmentSweep(): Promise { + for (let attempt = 0; attempt < 8; attempt += 1) { + await flushRepoGitRemoteIdentityEnrichmentForTests() + } +} + +async function sweep(store: RepoIdentityStore): Promise { + enrichMissingRepoGitRemoteIdentities(store) + await drainEnrichmentSweep() +} + afterEach(() => { vi.useRealTimers() vi.clearAllMocks() @@ -168,6 +188,147 @@ describe('enrichMissingRepoGitRemoteIdentities', () => { expect(store.updateRepo).toHaveBeenCalledWith('repo-1', { gitRemoteIdentity: remoteIdentity }) }) + it('does not re-probe a resolved identity before the refresh window elapses', async () => { + vi.useFakeTimers() + vi.setSystemTime(1_000) + vi.mocked(probeGitRemoteIdentity).mockResolvedValue(resolvedProbe) + const store = makeStore(makeRepo({ gitRemoteIdentity: remoteIdentity })) + + // Why two sweeps: the first only schedules the re-probe, so a restart cannot fan out at launch. + await sweep(store) + expect(probeGitRemoteIdentity).not.toHaveBeenCalled() + + vi.setSystemTime(1_000 + REFRESH_STARTUP_DELAY_MS + 1) + await sweep(store) + expect(probeGitRemoteIdentity).toHaveBeenCalledTimes(1) + + vi.setSystemTime(1_000 + REFRESH_STARTUP_DELAY_MS + REFRESH_TTL_MS - 1) + await sweep(store) + expect(probeGitRemoteIdentity).toHaveBeenCalledTimes(1) + }) + + it('re-probes a resolved identity after the refresh window elapses', async () => { + vi.useFakeTimers() + vi.setSystemTime(1_000) + vi.mocked(probeGitRemoteIdentity).mockResolvedValue(resolvedProbe) + const store = makeStore(makeRepo({ gitRemoteIdentity: remoteIdentity })) + + await sweep(store) + vi.setSystemTime(1_000 + REFRESH_STARTUP_DELAY_MS + 1) + await sweep(store) + vi.setSystemTime(1_000 + REFRESH_STARTUP_DELAY_MS + REFRESH_TTL_MS + 2) + await sweep(store) + + expect(probeGitRemoteIdentity).toHaveBeenCalledTimes(2) + }) + + it('overwrites a resolved identity when the re-probe finds a different canonical key', async () => { + vi.useFakeTimers() + vi.setSystemTime(1_000) + vi.mocked(probeGitRemoteIdentity).mockResolvedValue({ + status: 'resolved', + identity: movedIdentity + }) + const repo = makeRepo({ gitRemoteIdentity: remoteIdentity }) + const store = makeStore(repo) + const onChanged = vi.fn() + + await sweep(store) + vi.setSystemTime(1_000 + REFRESH_STARTUP_DELAY_MS + 1) + enrichMissingRepoGitRemoteIdentities(store, { onChanged }) + await drainEnrichmentSweep() + + expect(store.updateRepo).toHaveBeenCalledWith('repo-1', { gitRemoteIdentity: movedIdentity }) + expect(repo.gitRemoteIdentity).toEqual(movedIdentity) + expect(onChanged).toHaveBeenCalledTimes(1) + }) + + it('writes nothing when the re-probe returns the same canonical key', async () => { + vi.useFakeTimers() + vi.setSystemTime(1_000) + vi.mocked(probeGitRemoteIdentity).mockResolvedValue({ + status: 'resolved', + identity: { ...remoteIdentity, remoteUrl: 'https://git.company.test/team/sample-app.git' } + }) + const store = makeStore(makeRepo({ gitRemoteIdentity: remoteIdentity })) + const onChanged = vi.fn() + + await sweep(store) + vi.setSystemTime(1_000 + REFRESH_STARTUP_DELAY_MS + 1) + enrichMissingRepoGitRemoteIdentities(store, { onChanged }) + await drainEnrichmentSweep() + + expect(probeGitRemoteIdentity).toHaveBeenCalledTimes(1) + expect(store.updateRepo).not.toHaveBeenCalled() + expect(onChanged).not.toHaveBeenCalled() + }) + + it('keeps the existing identity when a re-probe cannot reach the host', async () => { + vi.useFakeTimers() + vi.setSystemTime(1_000) + vi.mocked(probeGitRemoteIdentity).mockResolvedValue({ status: 'unavailable' }) + const repo = makeRepo({ connectionId: 'builder', gitRemoteIdentity: remoteIdentity }) + const store = makeStore(repo) + + await sweep(store) + vi.setSystemTime(1_000 + REFRESH_STARTUP_DELAY_MS + 1) + await sweep(store) + + expect(probeGitRemoteIdentity).toHaveBeenCalledTimes(1) + expect(store.updateRepo).not.toHaveBeenCalled() + expect(repo.gitRemoteIdentity).toEqual(remoteIdentity) + }) + + it('keeps the existing identity when a re-probe reports no usable remote', async () => { + vi.useFakeTimers() + vi.setSystemTime(1_000) + vi.mocked(probeGitRemoteIdentity).mockResolvedValue({ status: 'no-remote' }) + const repo = makeRepo({ gitRemoteIdentity: remoteIdentity }) + const store = makeStore(repo) + + await sweep(store) + vi.setSystemTime(1_000 + REFRESH_STARTUP_DELAY_MS + 1) + await sweep(store) + + expect(store.updateRepo).not.toHaveBeenCalled() + expect(repo.gitRemoteIdentity).toEqual(remoteIdentity) + }) + + it('bounds re-probes per sweep so a large repo list drains gradually', async () => { + vi.useFakeTimers() + vi.setSystemTime(1_000) + vi.mocked(probeGitRemoteIdentity).mockResolvedValue(resolvedProbe) + const repos = Array.from({ length: 6 }, (_unused, index) => + makeRepo({ + id: `repo-${index}`, + path: `/workspace/sample-app-${index}`, + gitRemoteIdentity: remoteIdentity + }) + ) + const store = makeStore(...repos) + + await sweep(store) + vi.setSystemTime(1_000 + REFRESH_STARTUP_DELAY_MS + 1) + await sweep(store) + expect(probeGitRemoteIdentity).toHaveBeenCalledTimes(4) + + await sweep(store) + expect(probeGitRemoteIdentity).toHaveBeenCalledTimes(6) + }) + + it('does not re-probe folder workspaces', async () => { + vi.useFakeTimers() + vi.setSystemTime(1_000) + vi.mocked(probeGitRemoteIdentity).mockResolvedValue(resolvedProbe) + const store = makeStore(makeRepo({ kind: 'folder', gitRemoteIdentity: remoteIdentity })) + + await sweep(store) + vi.setSystemTime(1_000 + REFRESH_STARTUP_DELAY_MS + REFRESH_TTL_MS + 1) + await sweep(store) + + expect(probeGitRemoteIdentity).not.toHaveBeenCalled() + }) + it('does not write stale identity data after the repo path changes', async () => { const probe = deferred() vi.mocked(probeGitRemoteIdentity).mockReturnValue(probe.promise) diff --git a/src/main/repo-git-remote-identity-enrichment.ts b/src/main/repo-git-remote-identity-enrichment.ts index 9da00e68953..e48177f2e62 100644 --- a/src/main/repo-git-remote-identity-enrichment.ts +++ b/src/main/repo-git-remote-identity-enrichment.ts @@ -2,6 +2,16 @@ import type { Repo } from '../shared/types' import { probeGitRemoteIdentity } from './repo-git-remote-identity' const NO_IDENTITY_RETRY_TTL_MS = 5 * 60 * 1000 +// Why 6h: a resolved identity only changes when a remote is added or the project is renamed or +// transferred, so this trades a `git remote -v` per repo per quarter-day for identity gates that +// stop being frozen for the life of the repo record. +const RESOLVED_IDENTITY_REFRESH_TTL_MS = 6 * 60 * 60 * 1000 +// Why: the deadline map is per-process, so a restart would otherwise make every resolved repo due +// at once, on the first list call — the worst moment to spawn a subprocess per repo. +const RESOLVED_IDENTITY_REFRESH_STARTUP_DELAY_MS = 5 * 60 * 1000 +// Why: refreshes are speculative, so a large repo list drains across sweeps instead of stalling one +// list call behind N sequential probes (each an SSH round trip for off-host repos). +const MAX_IDENTITY_REFRESHES_PER_SWEEP = 4 type RepoIdentityStore = { getRepos(): Repo[] @@ -14,7 +24,7 @@ type EnrichmentOptions = { } const inFlightProbesByLocation = new Map>() -const noIdentityRetryAfterByLocation = new Map() +const probeRetryAfterByLocation = new Map() function getRepoLocationKey(repo: Pick): string { return `${repo.connectionId ?? 'local'}\0${repo.path}` @@ -24,28 +34,38 @@ function getCurrentRepo(store: RepoIdentityStore, id: string): Repo | undefined return store.getRepo?.(id) ?? store.getRepos().find((repo) => repo.id === id) } -function isSameUnenrichedRepo(snapshot: Repo, current: Repo | undefined): boolean { +function isSameProbedRepo(snapshot: Repo, current: Repo | undefined): current is Repo { return ( !!current && current.kind !== 'folder' && - !current.gitRemoteIdentity && current.path === snapshot.path && (current.connectionId ?? null) === (snapshot.connectionId ?? null) ) } +function shouldWriteProbedIdentity(current: Repo, probed: Repo['gitRemoteIdentity']): boolean { + const existing = current.gitRemoteIdentity + if (!existing) { + // Why: the no-remote marker is re-derived on every retry; skip the redundant + // write so repo-list consumers do not churn. + return !(probed === null && existing === null) + } + // Why: a resolved identity is replaced only by a probe that found a genuinely different repo. + // Failures and no-remote answers must never clear it — consumers read an absent identity as + // "unknown" and gate permissively, so losing one is worse than carrying a stale one. + return !!probed && probed.canonicalKey !== existing.canonicalKey +} + function writeIdentity( store: RepoIdentityStore, snapshot: Repo, gitRemoteIdentity: Repo['gitRemoteIdentity'] ): boolean { const current = getCurrentRepo(store, snapshot.id) - if (!isSameUnenrichedRepo(snapshot, current)) { - return false - } - // Why: the no-remote marker is re-derived on every retry; skip the redundant - // write so repo-list consumers do not churn. - if (gitRemoteIdentity === null && current?.gitRemoteIdentity === null) { + if ( + !isSameProbedRepo(snapshot, current) || + !shouldWriteProbedIdentity(current, gitRemoteIdentity) + ) { return false } return !!store.updateRepo(snapshot.id, { gitRemoteIdentity }) @@ -53,7 +73,7 @@ function writeIdentity( async function enrichRepoGitRemoteIdentity(store: RepoIdentityStore, repo: Repo): Promise { const locationKey = getRepoLocationKey(repo) - const retryAfter = noIdentityRetryAfterByLocation.get(locationKey) ?? 0 + const retryAfter = probeRetryAfterByLocation.get(locationKey) ?? 0 if (retryAfter > Date.now()) { return false } @@ -61,19 +81,24 @@ async function enrichRepoGitRemoteIdentity(store: RepoIdentityStore, repo: Repo) if (inFlight) { return inFlight } + // Why: a failed refresh of an already-resolved repo backs off on the long TTL — an SSH host that + // is down for the day must not re-probe every five minutes for an identity we already hold. + const missTtlMs = repo.gitRemoteIdentity + ? RESOLVED_IDENTITY_REFRESH_TTL_MS + : NO_IDENTITY_RETRY_TTL_MS const probe = (async () => { const result = await probeGitRemoteIdentity(repo.path, repo.connectionId) if (result.status !== 'resolved') { // Why: repos without a parseable remote are common; cache misses briefly so // list calls stay cheap while still allowing recent remote changes to land. - noIdentityRetryAfterByLocation.set(locationKey, Date.now() + NO_IDENTITY_RETRY_TTL_MS) + probeRetryAfterByLocation.set(locationKey, Date.now() + missTtlMs) // Why: only a probe that actually reached git settles "no usable remote". // An unreachable host leaves the identity unknown so consumers can keep // treating the repo as pending instead of ineligible. return result.status === 'no-remote' ? writeIdentity(store, repo, null) : false } - noIdentityRetryAfterByLocation.delete(locationKey) + probeRetryAfterByLocation.set(locationKey, Date.now() + RESOLVED_IDENTITY_REFRESH_TTL_MS) return writeIdentity(store, repo, result.identity) })().finally(() => { if (inFlightProbesByLocation.get(locationKey) === probe) { @@ -84,17 +109,45 @@ async function enrichRepoGitRemoteIdentity(store: RepoIdentityStore, repo: Repo) return probe } -async function enrichMissingRepoGitRemoteIdentitiesInBackground( - store: RepoIdentityStore, - options: EnrichmentOptions -): Promise { +function isIdentityRefreshDue(repo: Repo, now: number): boolean { + const locationKey = getRepoLocationKey(repo) + const dueAt = probeRetryAfterByLocation.get(locationKey) + if (dueAt === undefined) { + probeRetryAfterByLocation.set(locationKey, now + RESOLVED_IDENTITY_REFRESH_STARTUP_DELAY_MS) + return false + } + return dueAt <= now +} + +function selectEnrichmentCandidates(store: RepoIdentityStore): Repo[] { + const now = Date.now() + const repos = store.getRepos().filter((repo) => repo.kind !== 'folder') // Why: the settled `null` marker stays a candidate on purpose — a repo that // gains a remote later must still resolve. Do not tighten this to // `=== undefined`; the retry TTL already bounds the cost and `writeIdentity` // skips the redundant rewrite. - const candidates = store - .getRepos() - .filter((repo) => repo.kind !== 'folder' && !repo.gitRemoteIdentity) + const candidates = repos.filter((repo) => !repo.gitRemoteIdentity) + // Why re-probe at all: an identity resolved once is otherwise frozen for the life of the repo + // record, so a later `git remote add upstream` or a project rename/transfer keeps misjudging + // identity gates against the path the repo had when it was added. + let refreshes = 0 + for (const repo of repos) { + if (refreshes >= MAX_IDENTITY_REFRESHES_PER_SWEEP) { + break + } + if (repo.gitRemoteIdentity && isIdentityRefreshDue(repo, now)) { + candidates.push(repo) + refreshes += 1 + } + } + return candidates +} + +async function enrichMissingRepoGitRemoteIdentitiesInBackground( + store: RepoIdentityStore, + options: EnrichmentOptions +): Promise { + const candidates = selectEnrichmentCandidates(store) let changed = false for (const repo of candidates) { // Why: enrichment runs later; capture the location we probed so a mutable @@ -123,5 +176,5 @@ export async function flushRepoGitRemoteIdentityEnrichmentForTests(): Promise = {}): Project => ({ + id: 'project-1', + displayName: 'Project', + badgeColor: '#737373', + sourceRepoIds: [], + createdAt: 1, + updatedAt: 1, + ...overrides +}) + +describe('carryProjectStateThroughIdentityChange', () => { + it('keeps the exact-id match and reports no remap', () => { + const projected = makeProject({ id: 'git:host/acme/app', sourceRepoIds: ['r1'] }) + const previous = makeProject({ + id: 'git:host/acme/app', + sourceRepoIds: ['r1'], + updatedAt: 50, + localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' } + }) + + const result = carryProjectStateThroughIdentityChange([projected], [previous]) + + expect(result.projects[0]).toMatchObject({ + id: 'git:host/acme/app', + localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, + updatedAt: 50 + }) + expect(result.remappedProjectIds.size).toBe(0) + }) + + it('adopts a prior row through a changed identity key by repo overlap', () => { + const projected = makeProject({ id: 'github:acme/app', sourceRepoIds: ['r1'] }) + const previous = makeProject({ + id: 'repo:r1', + sourceRepoIds: ['r1'], + localWindowsRuntimePreference: { kind: 'windows-host' } + }) + + const result = carryProjectStateThroughIdentityChange([projected], [previous]) + + expect(result.projects[0]?.localWindowsRuntimePreference).toEqual({ kind: 'windows-host' }) + expect([...result.remappedProjectIds]).toEqual([['repo:r1', 'github:acme/app']]) + }) + + it('ignores prior rows that still exist under their own id', () => { + const kept = makeProject({ id: 'git:host/acme/app', sourceRepoIds: ['r1'] }) + const renamed = makeProject({ id: 'git:host/acme/tool', sourceRepoIds: ['r2'] }) + const previousKept = makeProject({ + id: 'git:host/acme/app', + sourceRepoIds: ['r1', 'r2'], + localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' } + }) + + const result = carryProjectStateThroughIdentityChange([kept, renamed], [previousKept]) + + expect(result.projects[1]?.localWindowsRuntimePreference).toBeUndefined() + expect(result.remappedProjectIds.size).toBe(0) + }) + + it('prefers the prior row sharing the most repos over the more recently updated one', () => { + const projected = makeProject({ id: 'git:host/acme/merged', sourceRepoIds: ['r1', 'r2'] }) + const wide = makeProject({ + id: 'git:host/acme/wide', + sourceRepoIds: ['r1', 'r2'], + updatedAt: 10, + localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' } + }) + const narrow = makeProject({ + id: 'git:host/acme/narrow', + sourceRepoIds: ['r2'], + updatedAt: 900, + localWindowsRuntimePreference: { kind: 'windows-host' } + }) + + const result = carryProjectStateThroughIdentityChange([projected], [wide, narrow]) + + expect(result.projects[0]?.localWindowsRuntimePreference).toEqual({ + kind: 'wsl', + distro: 'Ubuntu' + }) + expect([...result.remappedProjectIds.keys()]).toEqual(['git:host/acme/wide']) + }) + + it('breaks an equal-overlap tie by newest updatedAt, then lowest prior id', () => { + const projected = makeProject({ id: 'git:host/acme/merged', sourceRepoIds: ['r1', 'r2'] }) + const older = makeProject({ + id: 'git:host/acme/a', + sourceRepoIds: ['r1'], + updatedAt: 100, + localWindowsRuntimePreference: { kind: 'windows-host' } + }) + const newer = makeProject({ + id: 'git:host/acme/b', + sourceRepoIds: ['r2'], + updatedAt: 200, + localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' } + }) + + expect( + carryProjectStateThroughIdentityChange([projected], [older, newer]).projects[0] + ?.localWindowsRuntimePreference + ).toEqual({ kind: 'wsl', distro: 'Ubuntu' }) + + const sameStamp = { ...newer, updatedAt: 100 } + // Same overlap and same updatedAt: the lexicographically lowest prior id wins, both orders. + for (const previous of [ + [older, sameStamp], + [sameStamp, older] + ]) { + const result = carryProjectStateThroughIdentityChange([projected], previous) + expect(result.projects[0]?.localWindowsRuntimePreference).toEqual({ kind: 'windows-host' }) + expect([...result.remappedProjectIds.keys()]).toEqual(['git:host/acme/a']) + } + }) + + it('lets one prior row be claimed by only one surviving project', () => { + const left = makeProject({ id: 'git:host/acme/left', sourceRepoIds: ['r1'] }) + const right = makeProject({ id: 'git:host/acme/right', sourceRepoIds: ['r2'] }) + const previous = makeProject({ + id: 'repo:r1', + sourceRepoIds: ['r1', 'r2'], + localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' } + }) + + const result = carryProjectStateThroughIdentityChange([left, right], [previous]) + + expect(result.projects[0]?.localWindowsRuntimePreference).toEqual({ + kind: 'wsl', + distro: 'Ubuntu' + }) + expect(result.projects[1]?.localWindowsRuntimePreference).toBeUndefined() + expect([...result.remappedProjectIds]).toEqual([['repo:r1', 'git:host/acme/left']]) + }) +}) diff --git a/src/shared/project-identity-succession.ts b/src/shared/project-identity-succession.ts new file mode 100644 index 00000000000..815d1b58f6d --- /dev/null +++ b/src/shared/project-identity-succession.ts @@ -0,0 +1,91 @@ +import type { Project } from './types' + +export type ProjectIdentitySuccession = { + /** Projected projects with user-set state carried forward from their predecessor row. */ + projects: readonly Project[] + /** Predecessor project id -> surviving project id, for rows whose identity key changed. */ + remappedProjectIds: ReadonlyMap +} + +function carryUserState(projected: Project, previous: Project): Project { + return previous.localWindowsRuntimePreference + ? { + ...projected, + localWindowsRuntimePreference: previous.localWindowsRuntimePreference, + updatedAt: Math.max(projected.updatedAt, previous.updatedAt) + } + : projected +} + +function countSharedRepoIds(sourceRepoIds: readonly string[], other: ReadonlySet): number { + return sourceRepoIds.reduce((count, repoId) => (other.has(repoId) ? count + 1 : count), 0) +} + +function compareStrings(left: string, right: string): number { + return left < right ? -1 : left > right ? 1 : 0 +} + +/** + * Carries persisted per-project user state (and the rows that point at it) across a + * project id change. Project ids are derived from repo identity, so `repo:` -> `git:` -> + * `github:` promotion and remote re-probes rewrite them mid-session; matching only on id + * would drop the user's runtime preference and leave a ghost row behind. + * + * Predecessor rule when several prior rows overlap a surviving project: most shared + * `sourceRepoIds` wins, then newest `updatedAt`, then lowest prior id (then lowest new id). + * Each prior row is claimed by at most one surviving project, so two distinct user + * preferences are never merged into one — the unclaimed row keeps its own identity. + */ +export function carryProjectStateThroughIdentityChange( + projectedProjects: readonly Project[], + previousProjects: readonly Project[] +): ProjectIdentitySuccession { + const previousById = new Map(previousProjects.map((project) => [project.id, project])) + const projectedIds = new Set(projectedProjects.map((project) => project.id)) + // Why: a prior row that still exists under its own id is live, not a predecessor. + const orphanedPrevious = previousProjects.filter((project) => !projectedIds.has(project.id)) + const unmatched = projectedProjects.filter((project) => !previousById.has(project.id)) + const candidates = unmatched.flatMap((project) => { + const repoIds = new Set(project.sourceRepoIds) + return orphanedPrevious + .map((previous) => ({ + project, + previous, + shared: countSharedRepoIds(previous.sourceRepoIds, repoIds) + })) + .filter((candidate) => candidate.shared > 0) + }) + candidates.sort( + (left, right) => + right.shared - left.shared || + right.previous.updatedAt - left.previous.updatedAt || + compareStrings(left.previous.id, right.previous.id) || + compareStrings(left.project.id, right.project.id) + ) + const claimedPreviousIds = new Set() + const predecessorByProjectId = new Map() + for (const candidate of candidates) { + if ( + claimedPreviousIds.has(candidate.previous.id) || + predecessorByProjectId.has(candidate.project.id) + ) { + continue + } + claimedPreviousIds.add(candidate.previous.id) + predecessorByProjectId.set(candidate.project.id, candidate.previous) + } + const remappedProjectIds = new Map() + const projects = projectedProjects.map((project) => { + const exact = previousById.get(project.id) + if (exact) { + return carryUserState(project, exact) + } + const predecessor = predecessorByProjectId.get(project.id) + if (!predecessor) { + return project + } + remappedProjectIds.set(predecessor.id, project.id) + return carryUserState(project, predecessor) + }) + return { projects, remappedProjectIds } +} From 400321edcd54f98a217299cc94355e0ffee4c324 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:28:32 -0700 Subject: [PATCH 03/11] fix(workspaces): gate the GitHub palette number match on repo remote identity (#14413) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(workspaces): gate the GitHub palette number match on repo identity `repoMatchesGitHubSlug` returned the permissive `'unknown'` whenever the repo displayName was not in `owner/repo` form and no upstream metadata existed — the common basename-named non-fork case. The caller only rejects on `false`, so a pasted issue/PR URL could activate a workspace in a different repo that happened to share the number, since issue/PR numbers are per-repo. Mirror the GitLab gate from #14381: fall back to the probed `gitRemoteIdentity.canonicalKey` before giving up, comparing host and owner/repo after normalizing port, `www.`, and case. An `upstream`-derived identity stays `'unknown'` because `deriveGitRemoteIdentity` ranks `upstream` above `origin`, so a fork's own origin is invisible and rejecting would drop URLs from the fork the user actually checked out. The canonicalKey compare runs after the displayName branch: displayName is compared host-agnostically, so mirrors and host aliases of the same owner/repo keep matching as they do today, and the probed remote only fills in where no name evidence exists. Refs STA-4237 * fix(workspaces): keep SSH host aliases matching in the palette identity gate `git remote -v` reports ssh.github.com, www., and ~/.ssh/config `Host` aliases verbatim, so comparing a probed canonicalKey against a pasted URL host rejected legitimate GitHub/GitLab remotes. Normalize the alias hosts both sides can fold offline, and downgrade a host-only mismatch to 'unknown' when the probed host is dotless (an unexpandable OpenSSH alias); dotted hosts like ghe.example.com still lose. Lifts the GitHub host normalizer into shared instead of a third copy. * fix(repos): keep the www host fold out of the derived project identity getProjectIdentityKey feeds the persisted Project id, so folding www. there re-keyed existing projects on upgrade and dropped localWindowsRuntimePreference. Restrict the fold to the palette's URL-vs-remote comparison, and pin the derived id for a www. remote so it cannot drift silently again. --- .../github/github-remote-identity-parsing.ts | 7 +- .../lib/worktree-palette-gitlab-url-match.ts | 38 ++- .../worktree-palette-task-url-match.test.ts | 240 ++++++++++++++++++ .../lib/worktree-palette-task-url-match.ts | 37 ++- src/shared/git-remote-host-alias.ts | 44 ++++ src/shared/git-remote-identity.test.ts | 37 ++- src/shared/git-remote-identity.ts | 40 +++ .../project-host-setup-projection.test.ts | 36 +++ src/shared/project-host-setup-projection.ts | 6 +- 9 files changed, 460 insertions(+), 25 deletions(-) create mode 100644 src/shared/git-remote-host-alias.ts diff --git a/src/main/github/github-remote-identity-parsing.ts b/src/main/github/github-remote-identity-parsing.ts index cc8e283c11c..02c21fb8983 100644 --- a/src/main/github/github-remote-identity-parsing.ts +++ b/src/main/github/github-remote-identity-parsing.ts @@ -1,13 +1,8 @@ +import { normalizeGitHubRemoteHost } from '../../shared/git-remote-host-alias' import type { GitHubOwnerRepo } from '../../shared/types' export type GitHubRemoteIdentity = GitHubOwnerRepo & { host: string } -export function normalizeGitHubRemoteHost(host: string): string { - const normalizedHost = host.toLowerCase() - // Why: GitHub documents ssh.github.com as SSH-over-HTTPS for github.com repos. - return normalizedHost === 'ssh.github.com' ? 'github.com' : normalizedHost -} - // Why: HTTP ports identify the GHES web/API endpoint; SSH and git ports are // transport-only and must not leak into gh's host identity. function hostFromRemoteUrl(url: URL): string { 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 4ec322c5ae4..a40c330208f 100644 --- a/src/renderer/src/lib/worktree-palette-gitlab-url-match.ts +++ b/src/renderer/src/lib/worktree-palette-gitlab-url-match.ts @@ -1,3 +1,9 @@ +import { foldComparableGitLabHost } from '../../../shared/git-remote-host-alias' +import { + matchGitRemoteKeyParts, + splitGitRemoteKey, + type GitRemoteKeyParts +} from '../../../shared/git-remote-identity' import type { HostedReviewInfo } from '../../../shared/hosted-review' import { parseGitLabIssueOrMRLink, @@ -7,15 +13,21 @@ import type { Repo, Worktree } from '../../../shared/types' export type GitLabIssueOrMRLink = NonNullable> -/** Same shape as `GitRemoteIdentity.canonicalKey`: lowercased host (no port) + normalized project path. */ +/** Host + project path, matching how `GitRemoteIdentity.canonicalKey` is built. */ +function gitLabProjectKeyParts(slug: ProjectSlug): GitRemoteKeyParts { + return { + host: foldComparableGitLabHost(slug.host.replace(/:\d+$/, '')), + tail: slug.path + .replace(/^\/+/, '') + .replace(/\/+$/, '') + .replace(/\.git$/i, '') + .toLowerCase() + } +} + function gitLabProjectKey(slug: ProjectSlug): string { - const host = slug.host.replace(/:\d+$/, '').toLowerCase() - const path = slug.path - .replace(/^\/+/, '') - .replace(/\/+$/, '') - .replace(/\.git$/i, '') - .toLowerCase() - return `${host}/${path}` + const { host, tail } = gitLabProjectKeyParts(slug) + return `${host}/${tail}` } function gitLabLinksEqual(left: GitLabIssueOrMRLink, right: GitLabIssueOrMRLink): boolean { @@ -29,16 +41,18 @@ function gitLabLinksEqual(left: GitLabIssueOrMRLink, right: GitLabIssueOrMRLink) /** Tri-state like `repoMatchesGitHubSlug`: `'unknown'` stays permissive for forks and host aliases. */ function repoMatchesGitLabSlug(repo: Repo | undefined, slug: ProjectSlug): boolean | 'unknown' { const identity = repo?.gitRemoteIdentity - if (!identity?.canonicalKey) { + const identityParts = splitGitRemoteKey(identity?.canonicalKey, foldComparableGitLabHost) + if (!identityParts) { return 'unknown' } - if (identity.canonicalKey.replace(/\/+$/, '').toLowerCase() === gitLabProjectKey(slug)) { - return true + const verdict = matchGitRemoteKeyParts(identityParts, gitLabProjectKeyParts(slug)) + if (verdict !== false) { + return verdict } // Why not false: identity keeps one remote, chosen when the repo was added and never re-probed. // An `upstream` pick means a fork's `origin` existed and is invisible here, so rejecting would // drop MR URLs from the fork itself. A stale snapshot can still misjudge a renamed project. - return identity.remoteName === 'upstream' ? 'unknown' : false + return identity?.remoteName === 'upstream' ? 'unknown' : false } export function worktreeMatchesGitLabUrl( 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 b75902fd726..88691ac8bb6 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 @@ -50,6 +50,19 @@ function gitLabRepo(canonicalKey: string): Repo { } } +/** Basename displayName: the common non-fork case, where only the remote identifies the repo. */ +function gitHubRepo(canonicalKey: string): Repo { + return { + ...orcaRepo, + displayName: 'orca', + gitRemoteIdentity: { + canonicalKey, + remoteName: 'origin', + remoteUrl: `git@${canonicalKey.replace('/', ':')}.git` + } + } +} + describe('parseCmdJTaskSourceUrl', () => { it('parses GitHub issue and pull URLs', () => { expect(parseCmdJTaskSourceUrl('https://github.com/stablyai/orca/issues/14198')).toEqual({ @@ -165,6 +178,208 @@ describe('matchWorktreePaletteTaskUrl', () => { ).toMatchObject({ matchedField: 'pr', supportingText: { text: 'PR #12789' } }) }) + it('gates a stored GitHub number on the repo remote identity', () => { + const intent = parseCmdJTaskSourceUrl('https://github.com/stablyai/orca/pull/12789') + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ linkedPR: 12789 }), + intent: intent!, + repo: gitHubRepo('github.com/other/project') + }) + ).toBeNull() + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ linkedPR: 12789 }), + intent: intent!, + repo: gitHubRepo('github.com/stablyai/orca') + }) + ).toMatchObject({ matchedField: 'pr', supportingText: { text: 'PR #12789' } }) + }) + + it('gates a stored GitHub work item with no URL on the repo remote identity', () => { + const intent = parseCmdJTaskSourceUrl('https://github.com/stablyai/orca/issues/14198') + const worktree = makeWorktree({ + linkedWorkItem: { provider: 'github', type: 'issue', number: 14198, title: 'Bug', url: '' } + }) + expect( + matchWorktreePaletteTaskUrl({ + worktree, + intent: intent!, + repo: gitHubRepo('github.com/other/project') + }) + ).toBeNull() + expect( + matchWorktreePaletteTaskUrl({ + worktree, + intent: intent!, + repo: gitHubRepo('github.com/stablyai/orca') + }) + ).toMatchObject({ matchedField: 'issue' }) + }) + + it('does not match a GitHub URL on a different host for the same owner/repo', () => { + const intent = parseCmdJTaskSourceUrl('https://ghe.example.com/stablyai/orca/pull/12789') + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ linkedPR: 12789 }), + intent: intent!, + repo: gitHubRepo('github.com/stablyai/orca') + }) + ).toBeNull() + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ linkedPR: 12789 }), + intent: intent!, + repo: gitHubRepo('ghe.example.com/stablyai/orca') + }) + ).toMatchObject({ matchedField: 'pr' }) + }) + + it('matches GitHub remotes whose host is an SSH alias or www form of github.com', () => { + const intent = parseCmdJTaskSourceUrl('https://github.com/stablyai/orca/pull/12789') + for (const canonicalKey of [ + // ssh://git@ssh.github.com:443/... — GitHub's port-443 workaround. + 'ssh.github.com/stablyai/orca', + // git@github-work:... — an OpenSSH `Host` alias `git remote -v` cannot expand. + 'github-work/stablyai/orca', + 'www.github.com/stablyai/orca' + ]) { + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ linkedPR: 12789 }), + intent: intent!, + repo: gitHubRepo(canonicalKey) + }) + ).toMatchObject({ matchedField: 'pr' }) + } + // A real, resolvable host is evidence of a different forge, not an alias. + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ linkedPR: 12789 }), + intent: intent!, + repo: gitHubRepo('ghe.example.com/stablyai/orca') + }) + ).toBeNull() + }) + + it('normalizes host case, port, and owner case before comparing GitHub identities', () => { + const intent = parseCmdJTaskSourceUrl('https://GHE.Example.com:8443/StablyAI/Orca/pull/12789') + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ linkedPR: 12789 }), + intent: intent!, + repo: gitHubRepo('ghe.example.com/stablyai/orca') + }) + ).toMatchObject({ matchedField: 'pr' }) + }) + + it('stays permissive for GitHub numbers when the repo remote identity is unknown', () => { + const intent = parseCmdJTaskSourceUrl('https://github.com/stablyai/orca/pull/12789') + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ linkedPR: 12789 }), + intent: intent!, + repo: { ...orcaRepo, displayName: 'orca' } + }) + ).toMatchObject({ matchedField: 'pr' }) + expect( + matchWorktreePaletteTaskUrl({ worktree: makeWorktree({ linkedPR: 12789 }), intent: intent! }) + ).toMatchObject({ matchedField: 'pr' }) + }) + + it('stays permissive for a GitHub fork whose identity resolved to the upstream remote', () => { + // `deriveGitRemoteIdentity` prefers `upstream`, so the fork's own `origin` is not visible here. + const forkRepo: Repo = { + ...gitHubRepo('github.com/stablyai/orca'), + gitRemoteIdentity: { + canonicalKey: 'github.com/stablyai/orca', + remoteName: 'upstream', + remoteUrl: 'git@github.com:stablyai/orca.git' + } + } + const intent = parseCmdJTaskSourceUrl('https://github.com/me/orca/pull/12789') + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ linkedPR: 12789 }), + intent: intent!, + repo: forkRepo + }) + ).toMatchObject({ matchedField: 'pr' }) + // An `origin`-derived identity is authoritative, so a different repo still loses. + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ linkedPR: 12789 }), + intent: intent!, + repo: gitHubRepo('github.com/stablyai/orca') + }) + ).toBeNull() + }) + + it('keeps the GitHub number gate type-aware across repos', () => { + const prIntent = parseCmdJTaskSourceUrl('https://github.com/stablyai/orca/pull/12789') + const issueIntent = parseCmdJTaskSourceUrl('https://github.com/stablyai/orca/issues/12789') + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ linkedIssue: 12789 }), + intent: prIntent!, + repo: gitHubRepo('github.com/stablyai/orca') + }) + ).toBeNull() + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ linkedIssue: 12789 }), + intent: issueIntent!, + repo: gitHubRepo('github.com/other/project') + }) + ).toBeNull() + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ linkedIssue: 12789 }), + intent: issueIntent!, + repo: gitHubRepo('github.com/stablyai/orca') + }) + ).toMatchObject({ matchedField: 'issue', supportingText: { text: 'Issue #12789' } }) + }) + + it('keeps an owner/repo displayName authoritative over a host-alias remote', () => { + const intent = parseCmdJTaskSourceUrl('https://github.com/stablyai/orca/pull/12789') + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ linkedPR: 12789 }), + intent: intent!, + repo: { + ...orcaRepo, + gitRemoteIdentity: { + canonicalKey: 'git-mirror.example.com/stablyai/orca', + remoteName: 'origin', + remoteUrl: 'git@git-mirror.example.com:stablyai/orca.git' + } + } + }) + ).toMatchObject({ matchedField: 'pr' }) + }) + + it('matches a GitHub PR URL via the linked review URL regardless of remote identity', () => { + const intent = parseCmdJTaskSourceUrl('https://github.com/stablyai/orca/pull/12789') + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree(), + intent: intent!, + repo: gitHubRepo('github.com/other/project'), + review: { + provider: 'github', + number: 12789, + title: 'Fork PR', + state: 'open', + url: 'https://github.com/stablyai/orca/pull/12789', + status: 'pending', + updatedAt: '2026-01-01T00:00:00Z', + mergeable: 'UNKNOWN' + } + }) + ).toMatchObject({ matchedField: 'pr' }) + }) + it('rejects a GitLab MR URL from a different project than the stored URL', () => { const intent = parseCmdJTaskSourceUrl('https://gitlab.com/acme/orca/-/merge_requests/17') expect( @@ -265,6 +480,31 @@ describe('matchWorktreePaletteTaskUrl', () => { ).toMatchObject({ matchedField: 'pr' }) }) + it('matches GitLab remotes whose host is an SSH alias or www form of gitlab.com', () => { + const intent = parseCmdJTaskSourceUrl('https://gitlab.com/acme/orca/-/merge_requests/17') + for (const canonicalKey of [ + // altssh.gitlab.com is GitLab's port-443 SSH endpoint; `gitlab-work` is an ssh-config alias. + 'altssh.gitlab.com/acme/orca', + 'gitlab-work/acme/orca', + 'www.gitlab.com/acme/orca' + ]) { + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ linkedGitLabMR: 17 }), + intent: intent!, + repo: gitLabRepo(canonicalKey) + }) + ).toMatchObject({ matchedField: 'pr' }) + } + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ linkedGitLabMR: 17 }), + intent: intent!, + repo: gitLabRepo('gitlab.example.com/acme/orca') + }) + ).toBeNull() + }) + it('stays permissive for GitLab numbers when the repo remote identity is unknown', () => { const intent = parseCmdJTaskSourceUrl('https://gitlab.com/acme/orca/-/merge_requests/17') expect( 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 2cc310fa35c..3676b7edacb 100644 --- a/src/renderer/src/lib/worktree-palette-task-url-match.ts +++ b/src/renderer/src/lib/worktree-palette-task-url-match.ts @@ -1,3 +1,9 @@ +import { foldComparableGitHubHost } from '../../../shared/git-remote-host-alias' +import { + matchGitRemoteKeyParts, + splitGitRemoteKey, + type GitRemoteKeyParts +} from '../../../shared/git-remote-identity' import type { HostedReviewInfo } from '../../../shared/hosted-review' import { parseGitHubIssueOrPRLink, @@ -75,10 +81,37 @@ function parseOwnerRepoDisplayName(value: string | null | undefined): RepoSlug | return { owner: match[1], repo: match[2] } } +/** Host + `owner/repo` tail, matching how `GitRemoteIdentity.canonicalKey` is built. */ +function githubRemoteKeyParts(slug: RepoSlug): GitRemoteKeyParts { + return { + host: foldComparableGitHubHost((slug.host || 'github.com').replace(/:\d+$/, '')), + tail: `${slug.owner.toLowerCase()}/${slug.repo.replace(/\.git$/i, '').toLowerCase()}` + } +} + +function remoteIdentityMatchesGitHubSlug(repo: Repo, slug: RepoSlug): boolean | 'unknown' { + const identity = repo.gitRemoteIdentity + const identityParts = splitGitRemoteKey(identity?.canonicalKey, foldComparableGitHubHost) + if (!identityParts) { + return 'unknown' + } + const verdict = matchGitRemoteKeyParts(identityParts, githubRemoteKeyParts(slug)) + if (verdict !== false) { + return verdict + } + // Why not false: identity keeps one remote, chosen when the repo was added and never re-probed. + // An `upstream` pick means a fork's `origin` existed and is invisible here, so rejecting would + // drop URLs from the fork itself. A stale snapshot can still misjudge a renamed repo. + return identity?.remoteName === 'upstream' ? 'unknown' : false +} + +/** Tri-state: `'unknown'` stays permissive for forks and host aliases. */ function repoMatchesGitHubSlug(repo: Repo | undefined, slug: RepoSlug): boolean | 'unknown' { if (!repo) { return 'unknown' } + // Why displayName first: it is compared host-agnostically, so mirrors and host aliases of the + // same owner/repo keep matching; the probed remote only fills in where no name evidence exists. const fromName = parseOwnerRepoDisplayName(repo.displayName) if (fromName) { return githubIdentityKey({ ...fromName, host: slug.host }) === githubIdentityKey(slug) @@ -86,7 +119,9 @@ function repoMatchesGitHubSlug(repo: Repo | undefined, slug: RepoSlug): boolean if (repo.upstream?.owner && repo.upstream.repo) { return githubIdentityKey(repo.upstream) === githubIdentityKey(slug) } - return 'unknown' + // Why: a basename-only displayName is the common non-fork case, and issue/PR numbers are + // per-repo, so a bare number must still clear the remote the repo actually points at. + return remoteIdentityMatchesGitHubSlug(repo, slug) } export function parseCmdJTaskSourceUrl(query: string): CmdJTaskSourceUrl | null { diff --git a/src/shared/git-remote-host-alias.ts b/src/shared/git-remote-host-alias.ts new file mode 100644 index 00000000000..341f9807f21 --- /dev/null +++ b/src/shared/git-remote-host-alias.ts @@ -0,0 +1,44 @@ +// Why: a remote URL can name a forge through an alias host (SSH-over-443, `www.`), and every +// comparison of a probed remote against a pasted URL must fold those to the same identity. + +function normalizeRemoteHost(host: string): string { + return host.trim().toLowerCase() +} + +/** + * Fold a `www.` prefix. Deliberately NOT part of the forge normalizers: those feed + * `getProjectIdentityKey`, so folding there would re-key already-persisted projects on upgrade. + * Only URL-vs-remote comparison may use it. + */ +export function foldWwwHostAlias(host: string): string { + return normalizeRemoteHost(host).replace(/^www\./, '') +} + +export function normalizeGitHubRemoteHost(host: string): string { + const normalizedHost = normalizeRemoteHost(host) + // Why: GitHub documents ssh.github.com as SSH-over-HTTPS for github.com repos. + return normalizedHost === 'ssh.github.com' ? 'github.com' : normalizedHost +} + +export function normalizeGitLabRemoteHost(host: string): string { + const normalizedHost = normalizeRemoteHost(host) + // Why: GitLab documents altssh.gitlab.com as SSH-over-443 for gitlab.com projects. + return normalizedHost === 'altssh.gitlab.com' ? 'gitlab.com' : normalizedHost +} + +/** Comparison-only host fold: the forge alias plus `www.`, for matching a remote to a pasted URL. */ +export function foldComparableGitHubHost(host: string): string { + return normalizeGitHubRemoteHost(foldWwwHostAlias(host)) +} + +export function foldComparableGitLabHost(host: string): string { + return normalizeGitLabRemoteHost(foldWwwHostAlias(host)) +} + +/** + * A host with no dot is an OpenSSH `Host` alias (`git@github-work:owner/repo`) that only + * ~/.ssh/config can expand; `git remote -v` never resolves it, so it is unknown, not wrong. + */ +export function isUnresolvedSshHostAlias(host: string): boolean { + return !normalizeRemoteHost(host).includes('.') +} diff --git a/src/shared/git-remote-identity.test.ts b/src/shared/git-remote-identity.test.ts index c145526e253..cc4fa4dff57 100644 --- a/src/shared/git-remote-identity.test.ts +++ b/src/shared/git-remote-identity.test.ts @@ -1,5 +1,11 @@ import { describe, expect, it } from 'vitest' -import { deriveGitRemoteIdentity, normalizeGitRemoteUrl } from './git-remote-identity' +import { normalizeGitHubRemoteHost } from './git-remote-host-alias' +import { + deriveGitRemoteIdentity, + matchGitRemoteKeyParts, + normalizeGitRemoteUrl, + splitGitRemoteKey +} from './git-remote-identity' describe('normalizeGitRemoteUrl', () => { it('normalizes HTTPS and SSH GitHub remotes to the same canonical key', () => { @@ -76,3 +82,32 @@ describe('deriveGitRemoteIdentity', () => { }) }) }) + +describe('splitGitRemoteKey / matchGitRemoteKeyParts', () => { + const github = (key: string | undefined) => splitGitRemoteKey(key, normalizeGitHubRemoteHost) + + it('rejects keys without both a host and a path', () => { + expect(github('github.com')).toBeNull() + expect(github('github.com/')).toBeNull() + expect(github('/example/sample-app')).toBeNull() + expect(github(undefined)).toBeNull() + }) + + it('normalizes the host and lowercases the path tail', () => { + expect(github('SSH.GitHub.com:443/Example/Sample-App')).toEqual({ + host: 'github.com', + tail: 'example/sample-app' + }) + }) + + it('treats a dotless probed host as unknown but a real host as a mismatch', () => { + const target = { host: 'github.com', tail: 'example/sample-app' } + expect(matchGitRemoteKeyParts({ host: 'github-work', tail: target.tail }, target)).toBe( + 'unknown' + ) + expect(matchGitRemoteKeyParts({ host: 'ghe.example.com', tail: target.tail }, target)).toBe( + false + ) + expect(matchGitRemoteKeyParts({ host: 'github-work', tail: 'other/repo' }, target)).toBe(false) + }) +}) diff --git a/src/shared/git-remote-identity.ts b/src/shared/git-remote-identity.ts index 7b457f68b0b..bc221157a2e 100644 --- a/src/shared/git-remote-identity.ts +++ b/src/shared/git-remote-identity.ts @@ -1,9 +1,49 @@ +import { isUnresolvedSshHostAlias } from './git-remote-host-alias' + export type GitRemoteIdentity = { canonicalKey: string remoteName: string remoteUrl: string } +export type GitRemoteKeyParts = { + host: string + tail: string +} + +/** Split a `canonicalKey` into `host` + path tail so the host can be alias-normalized. */ +export function splitGitRemoteKey( + canonicalKey: string | null | undefined, + normalizeHost: (host: string) => string +): GitRemoteKeyParts | null { + const key = canonicalKey?.trim().replace(/\/+$/, '').toLowerCase() ?? '' + const separator = key.indexOf('/') + if (separator <= 0 || separator === key.length - 1) { + return null + } + return { + host: normalizeHost(key.slice(0, separator).replace(/:\d+$/, '')), + tail: key.slice(separator + 1) + } +} + +/** + * Compare a probed remote against a pasted URL. A host-only mismatch is `'unknown'` when the + * probed host is an SSH alias, since `git remote -v` reports it unexpanded. + */ +export function matchGitRemoteKeyParts( + identity: GitRemoteKeyParts, + target: GitRemoteKeyParts +): boolean | 'unknown' { + if (identity.tail !== target.tail) { + return false + } + if (identity.host === target.host) { + return true + } + return isUnresolvedSshHostAlias(identity.host) ? 'unknown' : false +} + type GitRemoteEntry = { name: string url: string diff --git a/src/shared/project-host-setup-projection.test.ts b/src/shared/project-host-setup-projection.test.ts index c7573c50784..d0456d392de 100644 --- a/src/shared/project-host-setup-projection.test.ts +++ b/src/shared/project-host-setup-projection.test.ts @@ -628,3 +628,39 @@ describe('isProjectRemoteIdentityPending', () => { ).toBe(false) }) }) + +describe('derived project identity stability', () => { + const base = { id: 'r', path: '/r', displayName: 'r' } as const + + // Why pinned: this id is the persisted Project primary key, so folding a host alias here would + // silently re-key existing projects on upgrade and drop their runtime preference. + it('keeps a www. remote provider-neutral instead of folding it onto github.com', () => { + const projection = projectHostSetupProjectionFromRepos([ + repo({ + ...base, + gitRemoteIdentity: { + canonicalKey: 'www.github.com/acme/app', + remoteName: 'origin', + remoteUrl: 'https://www.github.com/acme/app.git' + } + }) + ]) + expect(projection.projects.map((project) => project.id)).toEqual([ + 'git:www.github.com/acme/app' + ]) + }) + + it('still folds the documented ssh.github.com alias onto github.com', () => { + const projection = projectHostSetupProjectionFromRepos([ + repo({ + ...base, + gitRemoteIdentity: { + canonicalKey: 'ssh.github.com/acme/app', + remoteName: 'origin', + remoteUrl: 'ssh://git@ssh.github.com:443/acme/app.git' + } + }) + ]) + expect(projection.projects.map((project) => project.id)).toEqual(['github:acme/app']) + }) +}) diff --git a/src/shared/project-host-setup-projection.ts b/src/shared/project-host-setup-projection.ts index 00719497740..83ba100f30d 100644 --- a/src/shared/project-host-setup-projection.ts +++ b/src/shared/project-host-setup-projection.ts @@ -1,4 +1,5 @@ import { getRepoExecutionHostId } from './execution-host' +import { normalizeGitHubRemoteHost } from './git-remote-host-alias' import { githubRepoIdentityKey, isDefaultGitHubHost } from './github-repository-identity-key' import type { Project, @@ -120,11 +121,6 @@ function getProjectId( return getProjectIdentityKey(repo) } -function normalizeGitHubRemoteHost(host: string): string { - const normalizedHost = host.toLowerCase() - return normalizedHost === 'ssh.github.com' ? 'github.com' : normalizedHost -} - function isGitHubRemoteHost(host: string): boolean { const hostname = host.toLowerCase().replace(/:\d+$/, '') // A generic git remote is provider-neutral. Only infer GHES when the host From 070572bfd77560148a010db33a99258621d91bc5 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:34:31 -0700 Subject: [PATCH 04/11] refactor(shared): split shared/types.ts into per-domain type modules (#14397) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `src/shared/types.ts` was 3,981 raw lines (2,825 counted, 9.4x the 300-line budget) behind an `eslint-disable max-lines`, and is imported by 2,092 files — the single widest contract surface in the repo. Move all 320 top-level declarations into 46 per-domain modules (`repo-types.ts`, `worktree-types.ts`, `github-pr-types.ts`, ...) and reduce `types.ts` to an explicit re-export barrel, so the 2,092 import sites are untouched. `HostSettingOverrides` moves into the pre-existing `host-setting-overrides.ts` alongside the accessors that operate on it, which also removes that module's circular import back into `types.ts`. Named re-exports only, never `export type *`: with star re-exports a name exported by two modules is silently dropped, which would surface as a confusing "has no exported member" at a random call site. Verified lossless mechanically, not by inspection: - export parity — the module's resolved export set through the TS checker is identical before and after (396 names, no additions, no removals) - declaration parity — all 320 declarations compare character-identical modulo comments and whitespace, so no optionality, union order, or generic parameter drifted - `tsc --noEmit` green on the node, cli, and web projects - `oxfmt --write` is byte-identical, so the barrel is format-stable Drops the `max-lines` bypass and its baseline entry (ratchet 346 -> 345). --- config/max-lines-baseline.txt | 1 - src/shared/browser-workspace-types.ts | 155 + src/shared/classified-error.ts | 11 + src/shared/code-search-types.ts | 33 + src/shared/commit-message-ai-types.ts | 30 + src/shared/diff-comment-types.ts | 48 + src/shared/filesystem-entry-types.ts | 28 + src/shared/folder-workspace-types.ts | 47 + src/shared/git-diff-compare-types.ts | 71 + src/shared/github-check-types.ts | 70 + src/shared/github-comment-types.ts | 91 + src/shared/github-pr-refresh-types.ts | 121 + src/shared/github-pr-types.ts | 157 + src/shared/github-rate-limit-types.ts | 26 + src/shared/github-work-item-types.ts | 129 + src/shared/global-settings-types.ts | 454 ++ src/shared/host-setting-overrides.ts | 14 +- src/shared/issue-mutation-types.ts | 42 + src/shared/linear-issue-types.ts | 52 + src/shared/linear-project-types.ts | 107 + src/shared/linear-workspace-types.ts | 73 + src/shared/managed-account-types.ts | 95 + src/shared/notification-settings-types.ts | 106 + src/shared/onboarding-state-types.ts | 30 + src/shared/orca-yaml-hook-types.ts | 78 + src/shared/persisted-state-types.ts | 100 + src/shared/persisted-ui-state-types.ts | 204 + src/shared/pet-types.ts | 36 + src/shared/process-stats-types.ts | 74 + src/shared/project-group-types.ts | 60 + src/shared/project-types.ts | 142 + src/shared/repo-types.ts | 131 + src/shared/shell-path-hydration-types.ts | 14 + src/shared/tab-types.ts | 66 + src/shared/terminal-color-overrides.ts | 28 + src/shared/terminal-quick-command-types.ts | 32 + src/shared/terminal-tab-types.ts | 88 + src/shared/tui-agent.ts | 39 + src/shared/types.ts | 4088 +------------------ src/shared/ui-chrome-types.ts | 117 + src/shared/update-status-types.ts | 88 + src/shared/workspace-session-state-types.ts | 113 + src/shared/worktree-base-ref-drift-types.ts | 33 + src/shared/worktree-create-types.ts | 179 + src/shared/worktree-launch-types.ts | 39 + src/shared/worktree-lineage-types.ts | 56 + src/shared/worktree-meta-types.ts | 91 + src/shared/worktree-types.ts | 217 + 48 files changed, 4239 insertions(+), 3865 deletions(-) create mode 100644 src/shared/browser-workspace-types.ts create mode 100644 src/shared/classified-error.ts create mode 100644 src/shared/code-search-types.ts create mode 100644 src/shared/commit-message-ai-types.ts create mode 100644 src/shared/diff-comment-types.ts create mode 100644 src/shared/filesystem-entry-types.ts create mode 100644 src/shared/folder-workspace-types.ts create mode 100644 src/shared/git-diff-compare-types.ts create mode 100644 src/shared/github-check-types.ts create mode 100644 src/shared/github-comment-types.ts create mode 100644 src/shared/github-pr-refresh-types.ts create mode 100644 src/shared/github-pr-types.ts create mode 100644 src/shared/github-rate-limit-types.ts create mode 100644 src/shared/github-work-item-types.ts create mode 100644 src/shared/global-settings-types.ts create mode 100644 src/shared/issue-mutation-types.ts create mode 100644 src/shared/linear-issue-types.ts create mode 100644 src/shared/linear-project-types.ts create mode 100644 src/shared/linear-workspace-types.ts create mode 100644 src/shared/managed-account-types.ts create mode 100644 src/shared/notification-settings-types.ts create mode 100644 src/shared/onboarding-state-types.ts create mode 100644 src/shared/orca-yaml-hook-types.ts create mode 100644 src/shared/persisted-state-types.ts create mode 100644 src/shared/persisted-ui-state-types.ts create mode 100644 src/shared/pet-types.ts create mode 100644 src/shared/process-stats-types.ts create mode 100644 src/shared/project-group-types.ts create mode 100644 src/shared/project-types.ts create mode 100644 src/shared/repo-types.ts create mode 100644 src/shared/shell-path-hydration-types.ts create mode 100644 src/shared/tab-types.ts create mode 100644 src/shared/terminal-color-overrides.ts create mode 100644 src/shared/terminal-quick-command-types.ts create mode 100644 src/shared/terminal-tab-types.ts create mode 100644 src/shared/tui-agent.ts create mode 100644 src/shared/ui-chrome-types.ts create mode 100644 src/shared/update-status-types.ts create mode 100644 src/shared/workspace-session-state-types.ts create mode 100644 src/shared/worktree-base-ref-drift-types.ts create mode 100644 src/shared/worktree-create-types.ts create mode 100644 src/shared/worktree-launch-types.ts create mode 100644 src/shared/worktree-lineage-types.ts create mode 100644 src/shared/worktree-meta-types.ts create mode 100644 src/shared/worktree-types.ts diff --git a/config/max-lines-baseline.txt b/config/max-lines-baseline.txt index a9c03d1b9d6..bece70eff7e 100644 --- a/config/max-lines-baseline.txt +++ b/config/max-lines-baseline.txt @@ -327,7 +327,6 @@ inline src/shared/runtime-types.ts inline src/shared/source-control-ai.ts inline src/shared/telemetry-events.ts inline src/shared/text-search.ts -inline src/shared/types.ts inline tests/e2e/helpers/terminal.ts inline tests/e2e/terminal-panes.spec.ts mobile-config app/h/*/files/*.tsx diff --git a/src/shared/browser-workspace-types.ts b/src/shared/browser-workspace-types.ts new file mode 100644 index 00000000000..67619decd58 --- /dev/null +++ b/src/shared/browser-workspace-types.ts @@ -0,0 +1,155 @@ +export type BrowserHistoryEntry = { + url: string + normalizedUrl: string + title: string + lastVisitedAt: number + visitCount: number +} + +export type BrowserLoadError = { + code: number + description: string + validatedUrl: string +} + +export type BrowserCertificateFailure = { + challengeId: string + browserPageId: string + errorCode: number | null + error: string + origin: string + displayHost: string + canProceed: boolean + observedAt: number +} + +export type BrowserCertificateProceedFailureReason = + | 'expired' + | 'changed' + | 'ineligible' + | 'missing' + | 'navigated' + +export type BrowserCertificateProceedResult = + | { ok: true } + | { ok: false; reason: BrowserCertificateProceedFailureReason } + +// Why: BrowserPage persists the active viewport preset so CDP emulation can be +// reapplied on reload/navigation without the user re-picking from the toolbar. +export type BrowserViewportPresetId = + | 'mobile-s' + | 'mobile-m' + | 'mobile-l' + | 'tablet' + | 'laptop' + | 'laptop-l' + | 'desktop' + +export type BrowserViewportOverride = { + width: number + height: number + deviceScaleFactor: number + mobile: boolean +} + +export type BrowserPage = { + id: string + workspaceId: string + worktreeId: string + url: string + title: string + loading: boolean + faviconUrl: string | null + canGoBack: boolean + canGoForward: boolean + loadError: BrowserLoadError | null + createdAt: number + // Why: remote-owned worktrees can still host client-local fallback browser + // pages until headless remote runtimes support real browser panes. + browserRuntimeEnvironmentId?: string | null + /** Active CDP viewport emulation preset. null = default (fill pane, no CDP override) */ + viewportPresetId?: BrowserViewportPresetId | null +} + +export type BrowserWorkspace = { + id: string + worktreeId: string + /** Stable display label for the outer Orca tab ("Browser 1", "Browser 2", …). + * Optional so sessions persisted before this field was added fall back + * gracefully to the URL-derived label in getBrowserTabLabel. */ + label?: string + // Why: each browser workspace binds to exactly one session profile at creation + // time. The profile determines which Electron partition (and thus which + // cookies/storage) the guest webview uses. Absent means the legacy shared + // partition, which keeps backward compat with workspaces persisted before + // session profiles existed. + sessionProfileId?: string | null + // Why: runtime-created tabs resolve profile partition in main. Persisting it + // keeps isolated storage stable when the renderer profile mirror is stale. + sessionPartition?: string | null + activePageId?: string | null + pageIds?: string[] + // Why: the active page owns real browser chrome state now, but the top-level + // Orca tab strip still renders one workspace entry. Mirror the active page's + // title/url/loading metadata here so existing workspace-level UI can stay + // stable while Phase 2 introduces nested browser pages. + url: string + title: string + loading: boolean + faviconUrl: string | null + canGoBack: boolean + canGoForward: boolean + loadError: BrowserLoadError | null + createdAt: number +} + +export type BrowserTab = BrowserWorkspace + +export type BrowserSessionProfileScope = 'default' | 'isolated' | 'imported' + +export type BrowserSessionUserAgentMode = 'clean' | 'native' + +export type BrowserSessionProfileCreateOptions = { + userAgentMode?: BrowserSessionUserAgentMode +} + +export type BrowserSessionProfileSource = { + browserFamily: + | 'chrome' + | 'chromium' + | 'arc' + | 'edge' + | 'firefox' + | 'safari' + | 'comet' + | 'helium' + | 'manual' + profileName?: string + importedAt: number +} + +export type BrowserSessionProfile = { + id: string + scope: BrowserSessionProfileScope + partition: string + label: string + source: BrowserSessionProfileSource | null + userAgentMode?: BrowserSessionUserAgentMode +} + +export type BrowserCookieImportSummary = { + totalCookies: number + importedCookies: number + skippedCookies: number + googleCookiesSkipped?: number + domains: string[] + warning?: { + code: 'restart-fallback-unavailable' + loadedCookies: number + failedCookies: number + } +} + +export type BrowserCookieImportResult = + | { ok: true; profileId: string; summary: BrowserCookieImportSummary } + | { ok: false; reason: string } diff --git a/src/shared/classified-error.ts b/src/shared/classified-error.ts new file mode 100644 index 00000000000..68e7bcf96a9 --- /dev/null +++ b/src/shared/classified-error.ts @@ -0,0 +1,11 @@ +export type ClassifiedError = { + type: + | 'permission_denied' + | 'not_found' + | 'issues_disabled' + | 'validation_error' + | 'rate_limited' + | 'network_error' + | 'unknown' + message: string +} diff --git a/src/shared/code-search-types.ts b/src/shared/code-search-types.ts new file mode 100644 index 00000000000..6d8a8f1ccd1 --- /dev/null +++ b/src/shared/code-search-types.ts @@ -0,0 +1,33 @@ +// ─── Search ───────────────────────────────────────────── +export type SearchMatch = { + line: number + column: number + matchLength: number + lineContent: string + displayColumn?: number + displayMatchLength?: number +} + +export type SearchFileResult = { + filePath: string + relativePath: string + matches: SearchMatch[] + matchCount?: number +} + +export type SearchResult = { + files: SearchFileResult[] + totalMatches: number + truncated: boolean +} + +export type SearchOptions = { + query: string + rootPath: string + caseSensitive?: boolean + wholeWord?: boolean + useRegex?: boolean + includePattern?: string + excludePattern?: string + maxResults?: number +} diff --git a/src/shared/commit-message-ai-types.ts b/src/shared/commit-message-ai-types.ts new file mode 100644 index 00000000000..4e7e57a165b --- /dev/null +++ b/src/shared/commit-message-ai-types.ts @@ -0,0 +1,30 @@ +import type { TuiAgent } from './tui-agent' + +export type CommitMessageAiModelCapability = { + id: string + label: string + thinkingLevels?: { id: string; label: string }[] + defaultThinkingLevel?: string +} + +export type CommitMessageAiSettings = { + enabled: boolean + /** A TuiAgent id, the literal `'custom'` for a user-supplied command, or null. */ + agentId: TuiAgent | 'custom' | null + /** Per-agent: switching agents preserves the previously-picked model. */ + selectedModelByAgent: Partial> + /** Host-scoped model selections; dynamic agents can expose different models per SSH target. */ + selectedModelByAgentByHost?: Partial>>> + /** Per-agent dynamic models last discovered from the CLI, persisted so main can validate selections. */ + discoveredModelsByAgent?: Partial> + /** Host-scoped dynamic model discovery cache. */ + discoveredModelsByAgentByHost?: Partial< + Record>> + > + /** Per-model: thinking effort depends on the model, not the agent. Keyed by model id. */ + selectedThinkingByModel: Record + /** Optional user-provided suffix appended to the base prompt (style overrides, etc.). */ + customPrompt: string + /** Command template for agentId === 'custom'; {prompt} substitutes the diff prompt via argv, else the prompt is piped via stdin. */ + customAgentCommand: string +} diff --git a/src/shared/diff-comment-types.ts b/src/shared/diff-comment-types.ts new file mode 100644 index 00000000000..11e3694dffb --- /dev/null +++ b/src/shared/diff-comment-types.ts @@ -0,0 +1,48 @@ +// ─── Diff line comments ────────────────────────────────────────────── +// Why: users leave review notes on specific lines of the modified side of +// a diff so they can be handed back to an AI agent (pasted into a terminal +// or used to bootstrap a new agent session). Stored on WorktreeMeta so the +// existing persistence layer writes them to orca-data.json automatically. +export type DiffCommentSource = 'diff' | 'markdown' +export type DiffReviewScope = 'unstaged' | 'staged' | 'branch' + +export type MobileDiffReviewFileState = { + key: string + filePath: string + oldPath?: string + scope: DiffReviewScope + lastOpenedAt?: number + lastSeenDiffIdentity?: string + reviewedAt?: number + reviewDiffIdentity?: string +} + +export type MobileDiffReviewState = { + version: 1 + updatedAt?: number + completedAt?: number + files: Record +} + +export type DiffComment = { + id: string + worktreeId: string + filePath: string + /** Undefined means a legacy diff note. */ + source?: DiffCommentSource + /** Exact text selected when creating a markdown note, when available. */ + selectedText?: string + /** Inclusive range start. Must be <= lineNumber when present. */ + startLine?: number + lineNumber: number + body: string + createdAt: number + updatedAt?: number + /** Set after the note has been handed to an agent. Edits clear it. */ + sentAt?: number + scope?: DiffReviewScope + oldPath?: string + diffIdentity?: string + // Reserved for future "comments on the original side" — always 'modified' in v1. + side: 'modified' +} diff --git a/src/shared/filesystem-entry-types.ts b/src/shared/filesystem-entry-types.ts new file mode 100644 index 00000000000..b81a68f4c2a --- /dev/null +++ b/src/shared/filesystem-entry-types.ts @@ -0,0 +1,28 @@ +// ─── Filesystem ───────────────────────────────────────────── +export type FilesystemPathFlavor = 'posix' | 'win32' + +export type DirEntry = { + name: string + isDirectory: boolean + isSymlink: boolean +} + +export type MarkdownDocument = { + filePath: string + relativePath: string + basename: string + name: string +} + +// ─── Filesystem watcher ───────────────────────────────────── +export type FsChangeEvent = { + kind: 'create' | 'update' | 'delete' | 'rename' | 'overflow' + absolutePath: string + oldAbsolutePath?: string + isDirectory?: boolean +} + +export type FsChangedPayload = { + worktreePath: string + events: FsChangeEvent[] +} diff --git a/src/shared/folder-workspace-types.ts b/src/shared/folder-workspace-types.ts new file mode 100644 index 00000000000..0730afa2140 --- /dev/null +++ b/src/shared/folder-workspace-types.ts @@ -0,0 +1,47 @@ +import type { ExecutionHostId } from './execution-host' +import type { TaskSourceContext } from './task-source-context' +import type { TuiAgent } from './tui-agent' +import type { DiffComment } from './diff-comment-types' +import type { + WorkspaceCreatorProvenance, + WorkspaceLinkedItem, + WorkspaceStatus +} from './worktree-types' + +export type WorkspaceScope = + | { type: 'worktree'; worktreeId: string } + | { type: 'folder'; folderWorkspaceId: string } + +export type WorkspaceKey = `worktree:${string}` | `folder:${string}` + +export type FolderWorkspace = { + id: string + projectGroupId: string + name: string + folderPath: string + /** SSH target ID for folder workspaces whose folder path lives remotely. */ + connectionId?: string | null + /** Renderer-owned host stamp for host-qualified folder catalogs. */ + executionHostId?: ExecutionHostId | null + /** Authenticated client that created this workspace. Missing means unknown legacy origin. */ + creatorProvenance?: WorkspaceCreatorProvenance + linkedTask: WorkspaceLinkedItem | null + linkedTaskSourceContext?: TaskSourceContext | null + comment: string + isArchived: boolean + isUnread: boolean + isPinned: boolean + sortOrder: number + /** User-authored sidebar ordering. Higher values render earlier in Manual sort. */ + manualOrder?: number + workspaceStatus?: WorkspaceStatus + createdWithAgent?: TuiAgent + pendingFirstAgentMessageRename?: boolean + firstAgentMessageRenameError?: string | null + lastActivityAt: number + createdAt: number + updatedAt: number + diffComments?: DiffComment[] +} + +export type FolderWorkspaceLinkedTask = WorkspaceLinkedItem diff --git a/src/shared/git-diff-compare-types.ts b/src/shared/git-diff-compare-types.ts new file mode 100644 index 00000000000..ba1ee97015f --- /dev/null +++ b/src/shared/git-diff-compare-types.ts @@ -0,0 +1,71 @@ +import type { GitBranchChangeStatus } from './git-status-types' +import type { LargeDiffRenderLimit } from './large-diff-render-limit' + +// ─── Git Status ───────────────────────────────────────────── +// Re-exported from git-status-types.ts so mobile shares the wire contract without this desktop aggregate. + +export type GitBranchChangeEntry = { + path: string + status: GitBranchChangeStatus + oldPath?: string + added?: number + removed?: number +} + +export type GitBranchCompareSummary = { + baseRef: string + baseOid: string | null + compareRef: string + headOid: string | null + mergeBase: string | null + changedFiles: number + commitsAhead?: number + status: 'ready' | 'invalid-base' | 'unborn-head' | 'no-merge-base' | 'loading' | 'error' + errorMessage?: string +} + +export type GitBranchCompareResult = { + summary: GitBranchCompareSummary + entries: GitBranchChangeEntry[] +} + +export type GitCommitCompareSummary = { + commitOid: string + parentOid: string | null + compareRef: string + baseRef: string + changedFiles: number + status: 'ready' | 'invalid-commit' | 'error' + errorMessage?: string +} + +export type GitCommitCompareResult = { + summary: GitCommitCompareSummary + entries: GitBranchChangeEntry[] +} + +export type GitDiffTextResult = { + kind: 'text' + originalContent: string + modifiedContent: string + originalIsBinary: false + modifiedIsBinary: false + largeDiffRenderLimit?: LargeDiffRenderLimit +} + +export type GitDiffBinaryResult = { + kind: 'binary' + originalContent: string + modifiedContent: string + /** Legacy flag used by the renderer for any binary format it can preview, including PDFs. */ + isImage?: boolean + /** MIME type for binary preview rendering, e.g. "image/png" or "application/pdf" */ + mimeType?: string + /** True only for a proven deletion — distinct from an empty modified side caused by a read failure or size cap. */ + modifiedDeleted?: boolean +} & ( + | { originalIsBinary: true; modifiedIsBinary: boolean } + | { originalIsBinary: boolean; modifiedIsBinary: true } +) + +export type GitDiffResult = GitDiffTextResult | GitDiffBinaryResult diff --git a/src/shared/github-check-types.ts b/src/shared/github-check-types.ts new file mode 100644 index 00000000000..6a81094723c --- /dev/null +++ b/src/shared/github-check-types.ts @@ -0,0 +1,70 @@ +export type PRCheckDetail = { + name: string + status: 'queued' | 'in_progress' | 'completed' + conclusion: + | 'success' + | 'failure' + | 'cancelled' + | 'timed_out' + | 'neutral' + | 'skipped' + | 'pending' + // Why: a check suite needing manual action (e.g. a workflow awaiting "Approve + // and run") has no check run and is absent from statusCheckRollup, yet blocks + // auto-merge (GitHub returns "unstable status"). Surface it as its own state. + | 'action_required' + | null + url: string | null + checkRunId?: number + workflowRunId?: number + // Why: the GitLab job trace API is addressed by numeric job id only, so the + // Checks panel cannot load a job log without carrying it on the row. + gitlabJobId?: number +} + +export type PRCheckAnnotation = { + path: string | null + startLine: number | null + endLine: number | null + annotationLevel: string | null + title: string | null + message: string + rawDetails: string | null +} + +export type PRCheckStep = { + name: string + status: string | null + conclusion: string | null + startedAt: string | null + completedAt: string | null +} + +export type PRCheckJob = { + id: number | null + name: string + status: string | null + conclusion: string | null + startedAt: string | null + completedAt: string | null + url: string | null + logTail: string | null + steps: PRCheckStep[] +} + +export type PRCheckRunDetails = { + name: string + status: PRCheckDetail['status'] | (string & {}) | null + conclusion: PRCheckDetail['conclusion'] | (string & {}) | null + url: string | null + detailsUrl: string | null + startedAt: string | null + completedAt: string | null + title: string | null + summary: string | null + text: string | null + annotations: PRCheckAnnotation[] + jobs: PRCheckJob[] +} + +export type GitHubRerunPRChecksResult = { ok: true; count: number } | { ok: false; error: string } diff --git a/src/shared/github-comment-types.ts b/src/shared/github-comment-types.ts new file mode 100644 index 00000000000..0fc48af62c2 --- /dev/null +++ b/src/shared/github-comment-types.ts @@ -0,0 +1,91 @@ +import type { GitHubRepositoryIdentity } from './github-pr-types' + +export type GitHubReactionContent = + | '+1' + | '-1' + | 'laugh' + | 'confused' + | 'heart' + | 'hooray' + | 'rocket' + | 'eyes' + +export type GitHubReaction = { + content: GitHubReactionContent + count: number + viewerHasReacted?: boolean +} + +export type PRComment = { + id: number + author: string + authorAvatarUrl: string + body: string + createdAt: string + url: string + reactions?: GitHubReaction[] + /** GraphQL node ID for GitHub comments that support reaction mutations. */ + reactionSubjectId?: string + /** File path for inline review comments (absent for top-level conversation comments). */ + path?: string + /** GraphQL node ID of the review thread — present only for inline review comments. + * Used to resolve/unresolve the thread via GitHub's GraphQL API. */ + threadId?: string + /** Whether the review thread has been resolved. Only meaningful when threadId is set. */ + isResolved?: boolean + /** True when GitHub no longer maps the thread to the current diff. */ + isOutdated?: boolean + /** End line of the review annotation (1-based). */ + line?: number + /** Start line of the review annotation range (1-based). Absent for single-line comments. */ + startLine?: number + /** True when GitHub identifies the author as a bot (REST `user.type === 'Bot'` or + * GraphQL `__typename === 'Bot'`). Preferred over login-string heuristics because + * third-party review bots (e.g. qodo-ai-reviewer, coderabbitai) don't follow a + * predictable naming convention. Absent when the data source can't report it + * (non-GitHub fallbacks via `gh pr view`). */ + isBot?: boolean +} + +export type GitHubIssueTimelineTarget = { + type: 'issue' | 'pr' + number: number + title: string + url: string + repository?: string +} + +export type GitHubIssueTimelineItem = { + id: string + event: + | 'assigned' + | 'unassigned' + | 'mentioned' + | 'cross-referenced' + | 'closed' + | 'reopened' + | 'moved_columns_in_project' + actor: string + actorAvatarUrl: string + createdAt: string + assignee?: string + source?: GitHubIssueTimelineTarget + closer?: GitHubIssueTimelineTarget + stateReason?: string | null + previousColumnName?: string | null + columnName?: string | null + projectName?: string | null +} + +export type GitHubCommentResult = { ok: true; comment: PRComment } | { ok: false; error: string } + +export type GitHubPRReviewCommentInput = { + repoPath: string + prRepo?: GitHubRepositoryIdentity | null + prNumber: number + commitId: string + path: string + line: number + startLine?: number + body: string +} diff --git a/src/shared/github-pr-refresh-types.ts b/src/shared/github-pr-refresh-types.ts new file mode 100644 index 00000000000..35fdd0f27d7 --- /dev/null +++ b/src/shared/github-pr-refresh-types.ts @@ -0,0 +1,121 @@ +import type { RepoKind } from './repo-types' +import type { CheckStatus, PRInfo, PRMergeableState, PRState } from './github-pr-types' + +/** + * Discriminates a classified GitHub PR-refresh failure. The renderer maps these + * to stable, non-destructive empty-state copy; a `hard` subset (auth, permission, + * repo_unavailable, gh_unavailable) means the existing-review lookup is currently + * impossible and must hide the Create composer. + */ +export type PRRefreshErrorType = + | 'rate_limited' + | 'auth' + | 'network' + | 'permission' + | 'repo_unavailable' + | 'gh_unavailable' + | 'server_error' + | 'unknown' + +// Backward-compatible name used by outage-copy consumers added on main. +export type PRRefreshUpstreamErrorType = PRRefreshErrorType + +export type PRRefreshOutcome = + | { kind: 'found'; pr: PRInfo; fetchedAt: number } + | { kind: 'no-pr'; fetchedAt: number } + | { + kind: 'upstream-error' + errorType: PRRefreshErrorType + message: string + fetchedAt: number + // Unified retry schedule (see docs/reference/pr-panel-refresh-guidance.md). + // `nextAutoRetryAt`: earliest time main expects to auto-retry this key. + // `retryDisabledUntil`: earliest time a manual Retry / refreshPRNow is + // accepted (rate-limit gates only, never ordinary network/auth backoff). + nextAutoRetryAt?: number + retryDisabledUntil?: number + } + +export type GitHubPRRefreshReason = 'visible' | 'active' | 'post-push' | 'manual' | 'swr' + +export type GitHubPRRefreshEnqueueResult = + | { kind: 'queued' } + | { kind: 'skipped'; skippedReason: 'validation-denied' | 'validation-backoff' } + | { kind: 'fallback' } + +export type GitHubPRRefreshAlias = { + cacheKey: string + repoId?: string + repoPath: string + branch: string + worktreeId?: string + connectionId?: string | null + executionHostId?: string | null + linkedPRNumber?: number | null + fallbackPRNumber?: number | null + fallbackPRSource?: 'explicit' | 'pr-cache' | 'hosted-review' | null + // Why: request-time worktree HEAD. Merged branch-matched PRs are only visible + // for heads that belong to the PR, and refresh consumers need this snapshot to + // clear a durable linked PR once main confirms the head diverged. + currentHeadOid?: string | null +} + +export type GitHubPRRefreshCandidate = GitHubPRRefreshAlias & { + repoKind: RepoKind + repoId: string + isBare?: boolean + isArchived?: boolean + connectionId?: string | null + executionHostId?: string | null + connectionState?: 'connected' | 'disconnected' | 'unknown' + cachedFetchedAt?: number | null + cachedHasPR?: boolean | null + cachedPRState?: PRState | null + cachedChecksStatus?: CheckStatus | null + cachedMergeable?: PRMergeableState | null + cachedMergeStateStatus?: string | null + localGitOptions?: { wslDistro?: string } +} + +export type GitHubPRRefreshSkippedReason = + | 'fresh' + | 'not-git' + | 'bare' + | 'archived' + | 'disconnected' + | 'remote' + | 'rate-limit' + | 'capacity' + +type GitHubPRRefreshEventBase = { + sequence: number + reason: GitHubPRRefreshReason + aliases: GitHubPRRefreshAlias[] + requestStartedAt?: number +} + +export type GitHubPRRefreshEvent = + | (GitHubPRRefreshEventBase & { + outcome: PRRefreshOutcome + status?: never + pausedUntil?: never + skippedReason?: never + }) + | (GitHubPRRefreshEventBase & { + status: 'queued' | 'in-flight' + outcome?: never + pausedUntil?: never + skippedReason?: never + }) + | (GitHubPRRefreshEventBase & { + status: 'paused' + pausedUntil: number + skippedReason: 'rate-limit' + outcome?: never + }) + | (GitHubPRRefreshEventBase & { + status: 'skipped' + skippedReason: GitHubPRRefreshSkippedReason + outcome?: never + pausedUntil?: never + }) diff --git a/src/shared/github-pr-types.ts b/src/shared/github-pr-types.ts new file mode 100644 index 00000000000..6024d702bf3 --- /dev/null +++ b/src/shared/github-pr-types.ts @@ -0,0 +1,157 @@ +export type PRState = 'open' | 'closed' | 'merged' | 'draft' +export type IssueState = 'open' | 'closed' +export type CheckStatus = 'pending' | 'success' | 'failure' | 'neutral' + +export type PRMergeableState = 'MERGEABLE' | 'CONFLICTING' | 'UNKNOWN' +export type PRReviewDecision = 'APPROVED' | 'CHANGES_REQUESTED' | 'REVIEW_REQUIRED' + +export type PRConflictSummary = { + baseRef: string + baseCommit: string + commitsBehind: number + files: string[] + localMergeState?: 'clean' +} + +// Why: host must survive renderer/RPC boundaries so Enterprise review actions +// cannot silently fall back to a same-named repository on github.com. +export type GitHubRepositoryIdentity = { owner: string; repo: string; host?: string } + +export type GitHubPRMergeMethod = 'merge' | 'squash' | 'rebase' + +export type GitHubPRMergeMethodSettings = { + defaultMethod: GitHubPRMergeMethod + allowedMethods: Record +} + +export type GitHubPRStackEntry = { + position: number + number: number + title: string + url: string + updatedAt?: string + state: PRState + checksStatus: CheckStatus + mergeable: PRMergeableState + reviewDecision?: PRReviewDecision | null + mergeStateStatus?: string | null + headRefName?: string + headSha?: string +} + +export type GitHubPRStack = { + number: number + position: number + size: number + baseRefName: string + baseSha?: string + entries?: GitHubPRStackEntry[] +} + +export type PRInfo = { + number: number + title: string + state: PRState + url: string + checksStatus: CheckStatus + updatedAt: string + mergeable: PRMergeableState + reviewDecision?: PRReviewDecision | null + autoMergeEnabled?: boolean + autoMergeAllowed?: boolean | null + mergeQueueRequired?: boolean | null + mergeMethodSettings?: GitHubPRMergeMethodSettings + mergeStateStatus?: string | null + /** GitHub-registered stack metadata. Absent for ordinary dependent PR chains. */ + stack?: GitHubPRStack + // Why: check-runs are keyed by the PR head commit, not the mutable branch name. + // Keeping the head SHA in cached PR metadata lets the checks panel poll the + // correct commit without re-querying GitHub or guessing from local branch refs. + headSha?: string + // Why: a merged branch-matched PR stays visible when the worktree head is one + // of the PR's own commits (behind update-branch/web commits). Cache staleness + // checks must honor that confirmation without re-querying GitHub. + confirmedContainedHeadOid?: string + // Why: the worktree HEAD OID this merged linked PR was confirmed to have + // diverged from (a definite not-contained probe). Head-scoped, not a bare + // boolean, so a PR-number-coalesced refresh broadcast cannot clear a sibling + // worktree whose own head is still on the PR's line of work. Clearing a + // durable linked PR requires this positive signal for that exact head, never + // the mere absence of a containment confirmation after a rate-limit/error. + headDivergedFromMergedPRAtOid?: string + /** Target branch name for PR-created worktree compare-base repair. */ + baseRefName?: string + /** PR head branch name. Lets linked-PR consumers detect that the worktree + * has switched to a different branch and the durable link is stale. */ + headRefName?: string + prRepo?: GitHubRepositoryIdentity + headRepo?: GitHubRepositoryIdentity + conflictSummary?: PRConflictSummary +} + +export type IssueInfo = { + number: number + title: string + state: IssueState + url: string + labels: string[] + /** Full markdown body when fetched through the single-issue endpoint. */ + description?: string +} + +export type GitHubViewer = { + login: string + email: string | null +} + +export type GitHubAssignableUser = { + login: string + name: string | null + avatarUrl: string +} + +export type ProviderCheckSummary = { + state: 'success' | 'failure' | 'pending' | 'neutral' | 'none' + total: number + passed: number + failed: number + pending: number + neutral: number +} + +export type GitHubPRReviewSummary = { + login: string + state?: string | null + avatarUrl?: string | null +} + +export type GitHubPRFileViewedState = 'DISMISSED' | 'VIEWED' | 'UNVIEWED' + +export type GitHubPRFile = { + path: string + oldPath?: string + status: 'added' | 'modified' | 'removed' | 'renamed' | 'copied' | 'changed' | 'unchanged' + additions: number + deletions: number + /** GitHub marks files above its diff size limit as binary-like; we skip content fetches for these. */ + isBinary: boolean + /** Modified-side line numbers that GitHub accepts for inline review comments. */ + reviewCommentLineNumbers?: number[] + /** GitHub's per-viewer review state. DISMISSED means new changes arrived after the file was viewed. */ + viewerViewedState?: GitHubPRFileViewedState +} + +export type GitHubPRFileContents = { + original: string + modified: string + originalIsBinary: boolean + modifiedIsBinary: boolean + originalTooLarge?: boolean + modifiedTooLarge?: boolean +} + +// Why: declared here as a shared shape so IPC return envelopes and renderer +// slices can reference the same structural type without importing from main. +// Aliased as `OwnerRepo` in `src/main/github/gh-utils.ts` so main call sites +// can continue using the short local name. +export type GitHubOwnerRepo = GitHubRepositoryIdentity diff --git a/src/shared/github-rate-limit-types.ts b/src/shared/github-rate-limit-types.ts new file mode 100644 index 00000000000..45ae8335769 --- /dev/null +++ b/src/shared/github-rate-limit-types.ts @@ -0,0 +1,26 @@ +/** + * GitHub API rate-limit buckets surfaced in the TaskPage header so users can + * see remaining budget before they hit the wall. `core` = REST (5000/hr), + * `search` = Search API (30/min — hit by countWorkItems), `graphql` = + * GraphQL (5000 points/hr — hit by project-view + discovery). All three are + * the buckets this app actually stresses; other buckets (e.g. code_search) + * are not surfaced because we don't touch them. + */ +export type GitHubRateLimitBucket = { + remaining: number + limit: number + /** Unix epoch seconds when the window resets. */ + resetAt: number +} + +export type GitHubRateLimitSnapshot = { + core: GitHubRateLimitBucket + search: GitHubRateLimitBucket + graphql: GitHubRateLimitBucket + /** Unix epoch ms the snapshot was produced (for "fetched Xs ago" copy). */ + fetchedAt: number +} + +export type GetRateLimitResult = + | { ok: true; snapshot: GitHubRateLimitSnapshot } + | { ok: false; error: string } diff --git a/src/shared/github-work-item-types.ts b/src/shared/github-work-item-types.ts new file mode 100644 index 00000000000..6b9cec763e0 --- /dev/null +++ b/src/shared/github-work-item-types.ts @@ -0,0 +1,129 @@ +import type { ClassifiedError } from './classified-error' +import type { PRCheckDetail } from './github-check-types' +import type { GitHubIssueTimelineItem, PRComment } from './github-comment-types' +import type { + GitHubAssignableUser, + GitHubOwnerRepo, + GitHubPRFile, + GitHubPRMergeMethodSettings, + GitHubPRReviewSummary, + GitHubRepositoryIdentity, + PRMergeableState, + PRReviewDecision, + ProviderCheckSummary +} from './github-pr-types' + +export type GitHubWorkItem = { + id: string + type: 'issue' | 'pr' + number: number + title: string + state: 'open' | 'closed' | 'merged' | 'draft' + url: string + labels: string[] + updatedAt: string + author: string | null + // Why: GHE user logins don't exist on github.com, so the github.com/{login}.png + // fallback 404s. Carry the API-provided avatar_url so github.com + Enterprise + // both render; absent on the gh-pr-view path (gh omits avatar), then the UI + // falls back to the login URL and finally an initials placeholder. See #8784. + authorAvatarUrl?: string + branchName?: string + baseRefName?: string + // Why: PR checks are keyed by head commit; carrying this lets task rows use + // the cached check-runs endpoint instead of one `gh pr checks` call per row. + headSha?: string + prRepo?: GitHubRepositoryIdentity + additions?: number + deletions?: number + changedFiles?: number + reviewDecision?: PRReviewDecision | null + reviewRequests?: GitHubAssignableUser[] + latestReviews?: GitHubPRReviewSummary[] + assignees?: GitHubAssignableUser[] + checksSummary?: ProviderCheckSummary + mergeable?: PRMergeableState + autoMergeEnabled?: boolean + autoMergeAllowed?: boolean | null + mergeQueueRequired?: boolean | null + mergeMethodSettings?: GitHubPRMergeMethodSettings + mergeStateStatus?: string | null + maintainerCanModify?: boolean + // Why: true when a PR's head lives on a fork (headRepositoryOwner !== selected repo owner). + // The Start-from picker passes this to resolvePrBase so fork heads use + // refs/pull//head for creation and a separate PR-head push target. + isCrossRepository?: boolean + /** Why: required because the cross-repo view merges items from every selected + * repo — the table row's repo pill and the "open in browser" fallback need + * to know which repo an item came from. Stamped by the renderer fetcher + * (`fetchWorkItems`) and by optimistic stubs on the new-issue path. */ + repoId: string +} + +export type GitHubWorkItemDetails = { + // Why: main-process doesn't know Orca's Repo.id, so this inner item omits + // repoId. The renderer stamps it when routing the details through the store. + item: Omit + body: string + comments: PRComment[] + /** Issue-only provider activity such as assignment, references, project moves, and state changes. */ + timelineItems?: GitHubIssueTimelineItem[] + /** Only set for PRs. Head/base SHAs used by the Files tab to fetch per-file content. */ + headSha?: string + baseSha?: string + /** GraphQL node ID required by GitHub's file-viewed mutations. Only set for PRs. */ + pullRequestId?: string + checks?: PRCheckDetail[] + files?: GitHubPRFile[] + /** Only set for PRs. True when the file fetch failed (rate limit, auth, + * unresolved remote) rather than the PR genuinely having no changed files. */ + filesUnavailable?: boolean + participants?: GitHubAssignableUser[] + /** Logins of current assignees. Only set for issues. */ + assignees?: string[] +} + +/** + * Envelope for `gh:listWorkItems`. Carries resolved issue/PR sources so the + * renderer can render the "Issues from owner/repo" indicator without an + * extra IPC round-trip, and per-source classified errors so the UI can show + * a retryable banner when (e.g.) a private upstream 403s. + * + * Why piggyback instead of adding `gh:resolveWorkItemSources`: the renderer + * already round-trips this endpoint on every Tasks refresh, and the source + * data is a 2-field-per-side metadata add — cheaper than another IPC call. + * + * Invariant: `items` always contains whatever succeeded; `errors.issues` indicates + * the issues-side fetch failed, but any PR-side items that succeeded are still + * present in `items`. Consumers should render `items` alongside the error banner. + */ +export type ListWorkItemsResult = { + items: T[] + sources: { + issues: GitHubOwnerRepo | null + prs: GitHubOwnerRepo | null + /** Raw `origin` remote resolved for this repo, independent of the + * user's preference. Required-nullable so the renderer can compare raw + * remote candidates without inferring origin from the effective PR + * source. */ + originCandidate: GitHubOwnerRepo | null + /** Raw `upstream` remote resolved for this repo, independent of the + * user's preference. Present so the renderer's issue-source selector + * can always decide whether to render (upstream exists & differs from + * origin) and show both slugs in its tooltips, even when the user has + * picked 'origin' and `sources.issues` has collapsed onto origin. */ + upstreamCandidate: GitHubOwnerRepo | null + } + errors?: { + issues?: ClassifiedError + prs?: ClassifiedError + } + /** True when the user's per-repo preference was `'upstream'` but no upstream + * remote is configured, so the resolver fell back to origin. Renderer uses + * this to surface a one-time-per-session toast. Omitted when absent so + * existing consumers and test fixtures don't care about it. + * Typed as `?: true` (not `?: boolean`) to encode the invariant "present + * iff fell-back" — an explicit `false` write would be a bug, so make it a + * compile error. */ + issueSourceFellBack?: true +} diff --git a/src/shared/global-settings-types.ts b/src/shared/global-settings-types.ts new file mode 100644 index 00000000000..71abe86bb46 --- /dev/null +++ b/src/shared/global-settings-types.ts @@ -0,0 +1,454 @@ +import type { ExecutionHostId } from './execution-host' +import type { GitHubProjectSettings } from './github-project-types' +import type { VoiceSettings } from './speech-types' +import type { GitLabProjectSettings } from './gitlab-types' +import type { TaskProvider } from './task-providers' +import type { KeybindingOverrides, TerminalShortcutPolicy } from './keybindings' +import type { AppIconId } from './app-icon' +import type { SourceControlAiSettings } from './source-control-ai-types' +import type { ClaudeAgentTeamsMode } from './claude-agent-teams-tmux-compat' +import type { TerminalCustomTheme } from './terminal-custom-themes' +import type { UiLanguage } from './ui-language' +import type { GlobalWindowsRuntimeDefault } from './project-execution-runtime' +import type { PersistedNativeChatSessionOptions } from './native-chat-session-options' +import type { ComputerAwakeMode } from './computer-awake-mode' +import type { CommitMessageAiSettings } from './commit-message-ai-types' +import type { HostSettingOverrides } from './host-setting-overrides' +import type { + ClaudeManagedAccount, + ClaudeManagedAccountRuntimeSelection, + CodexManagedAccount, + CodexManagedAccountRuntimeSelection +} from './managed-account-types' +import type { NotificationSettings } from './notification-settings-types' +import type { CtrlTabOrderMode } from './tab-types' +import type { TerminalColorOverrides } from './terminal-color-overrides' +import type { TerminalQuickCommand } from './terminal-quick-command-types' +import type { TuiAgent } from './tui-agent' +import type { + AgentDashboardMode, + BranchPrefixStrategy, + FloatingTerminalTriggerLocation, + LeftSidebarAppearanceMode, + OpenInApplication, + SourceControlGroupOrder, + SourceControlViewMode, + TaskViewPresetId +} from './ui-chrome-types' +import type { SetupScriptLaunchMode } from './worktree-launch-types' + +export type GlobalSettings = { + workspaceDir: string + /** Per-host overrides keyed by ExecutionHostId. Effective value for a + * host-varying setting is `host override ?? client default`. */ + hostSettingOverrides?: Partial> + nestWorkspaces: boolean + workspaceDirHistory?: OrcaWorkspaceLayout[] + refreshLocalBaseRefOnWorktreeCreate: boolean + /** Set once the user dismisses the "local main is behind" suggestion toast, so + * the nudge to enable refreshLocalBaseRefOnWorktreeCreate never shows again. */ + localBaseRefSuggestionDismissed: boolean + /** When enabled, Orca renames a workspace's auto-generated creature branch to + * a short name derived from the first prompt once work begins. Users can + * still turn this off from global Git settings. */ + autoRenameBranchFromWork: boolean + /** One-shot migration guard for the default-on rollout. Existing profiles + * without the guard are flipped on once; later explicit opt-outs stick. */ + autoRenameBranchFromWorkDefaultedOn?: boolean + branchPrefix: BranchPrefixStrategy + branchPrefixCustom: string + theme: 'system' | 'dark' | 'light' + /** Controls the left sidebar surface without changing terminal brightness. */ + leftSidebarAppearanceMode: LeftSidebarAppearanceMode + leftSidebarTintColor?: string + leftSidebarTintOpacity?: number + uiLanguage: UiLanguage + appIcon: AppIconId + appFontFamily: string + editorAutoSave: boolean + editorAutoSaveDelayMs: number + editorMinimapEnabled: boolean + /** Opt-in code-editor font; empty (the default) keeps following `terminalFontFamily`. */ + editorFontFamily?: string + /** Defaults on for profiles saved before file-editor wrapping became configurable. */ + editorWordWrap?: boolean + /** Persisted opt-out for browser spellcheck noise in rich Markdown editing surfaces. */ + richMarkdownSpellcheckEnabled?: boolean + /** Whether local markdown review note controls and the review panel are shown. */ + markdownReviewToolsEnabled: boolean + /** Why: mirrors terminal selection-paste muscle memory without mutating the + * normal system clipboard; Linux and macOS enable it by default, Windows + * leaves middle-click semantics unchanged unless the user opts in. */ + primarySelectionMiddleClickPaste?: boolean + /** One-shot migration guard for turning the Linux default on for profiles + * that persisted the earlier off-by-default value. */ + primarySelectionMiddleClickPasteDefaultedForLinux?: boolean + /** One-shot migration guard for widening the terminal-style default to + * Linux/macOS while preserving later explicit opt-outs. */ + primarySelectionMiddleClickPasteDefaultedForTerminalDefaults?: boolean + terminalFontSize: number + terminalFontFamily: string + terminalFontWeight: number + terminalFontWeightBold: number + terminalLineHeight: number + terminalScrollSensitivity: number + terminalFastScrollSensitivity: number + terminalTuiScrollSensitivity: number + /** One-shot migration guard for moving inherited TUI wheel reports from 3 to 1. */ + terminalTuiScrollSensitivityDefaultedToOne?: boolean + /** Terminal renderer policy. + * - 'auto': try xterm WebGL and fall back to DOM when unsupported or risky. + * - 'on': always try xterm WebGL. + * - 'off': keep terminal rendering on xterm's DOM renderer. */ + terminalGpuAcceleration: 'auto' | 'on' | 'off' + /** Whether to enable programming-ligatures rendering via + * `@xterm/addon-ligatures`. + * - `'auto'` (default): enabled only when the configured font is known to + * ship ligatures (Fira Code, JetBrains Mono, Cascadia Code, etc.). This + * keeps the out-of-the-box experience right for users who install a + * ligature font without touching settings. + * - `'on'` / `'off'`: explicit override. Never changes when the user + * switches fonts, so "off" always stays off. */ + terminalLigatures: 'auto' | 'on' | 'off' + terminalCursorStyle: 'bar' | 'block' | 'underline' + /** One-shot migration guard for moving inherited cursor defaults to block. */ + terminalCursorStyleDefaultedToBlock?: boolean + terminalCursorBlink: boolean + terminalThemeDark: string + terminalCustomThemes?: TerminalCustomTheme[] + terminalDividerColorDark: string + terminalUseSeparateLightTheme: boolean + terminalThemeLight: string + terminalDividerColorLight: string + terminalInactivePaneOpacity: number + terminalActivePaneOpacity: number + terminalPaneOpacityTransitionMs: number + terminalDividerThicknessPx: number + terminalBackgroundOpacity?: number + terminalColorOverrides?: TerminalColorOverrides + terminalPaddingX?: number + terminalPaddingY?: number + terminalMouseHideWhileTyping?: boolean + terminalWordSeparator?: string + terminalCursorOpacity?: number + terminalQuickCommands?: TerminalQuickCommand[] + windowBackgroundBlur?: boolean + /** Windows-only: close (X) hides to tray instead of quitting; the tray icon is always present regardless. */ + minimizeToTrayOnClose?: boolean + /** macOS: toggles the additive menu-bar entry (Orca survives last-window close); doesn't change Dock behavior. */ + showMenuBarIcon?: boolean + /** Windows convention: right-click pastes; macOS/Linux keep the context menu. */ + terminalRightClickToPaste: boolean + /** One-shot guard distinguishing the old global true default from a per-platform choice. */ + terminalRightClickToPasteDefaultedForPlatform?: boolean + /** Windows-only: COMSPEC always points to cmd.exe, so this explicit shell (default 'powershell.exe') overrides it. */ + terminalWindowsShell: string + /** Pins the WSL distro for terminals/agent scans instead of WSL's current global default. */ + terminalWindowsWslDistro?: string | null + /** Account/auth location; auto follows the global Windows runtime while host/wsl pin it. */ + localAccountRuntime: 'auto' | 'host' | 'wsl' + localAccountWslDistro?: string | null + /** One-shot guard for migrating the legacy host default to auto. */ + localAccountRuntimeDefaultedToAutoForAllUsers?: boolean + /** Independent from the terminal shell so users can inspect Windows vs WSL agent PATH state without changing it. */ + localAgentRuntime?: 'host' | 'wsl' + localAgentWslDistro?: string | null + /** Why: global is only the default policy; project-level runtime preference wins. */ + localWindowsRuntimeDefault: GlobalWindowsRuntimeDefault + /** 'auto' resolves to PowerShell 7+ when present, else falls back to inbox Windows PowerShell. */ + terminalWindowsPowerShellImplementation: 'auto' | 'powershell.exe' | 'pwsh.exe' + terminalFocusFollowsMouse: boolean + /** X11/gnome-terminal "copy on select": selecting text auto-copies to the clipboard; default off. */ + terminalClipboardOnSelect: boolean + /** Enables OSC 52 clipboard writes for TUIs (tmux/Zellij/nvim, incl. over SSH); default on. Clipboard *queries* stay blocked and payload size is capped, so this is write-only exposure. */ + terminalAllowOsc52Clipboard: boolean + /** One-shot stamp: profiles saved under the old off default get flipped on once, after which an explicit opt-out sticks. */ + terminalAllowOsc52ClipboardDefaultedOnForAllUsers?: boolean + /** Experimental Claude Agent Teams; native panes use a tmux-compatible shim so teammate output stays on the normal PTY path. */ + claudeAgentTeamsMode?: ClaudeAgentTeamsMode + /** Where the repo setup script runs on workspace create; defaults to a background "Setup" tab to keep the main terminal usable. */ + setupScriptLaunchMode: SetupScriptLaunchMode + terminalScrollbackRows: number + /** Optional app-level proxy for Electron networking and local PTYs; empty preserves system/inherited proxy env. */ + httpProxyUrl?: string + /** Optional semicolon/comma/newline-separated bypass rules for httpProxyUrl. */ + httpProxyBypassRules?: string + /** Why: corporate TLS-intercepting proxies can break HTTP/2 downloads; opt-in Chromium process-wide HTTP/1.1 switch. */ + electronHttp1CompatibilityMode?: boolean + /** Opt-in in-app browsing (isolated guest surface); default keeps links opening in the system browser. */ + openLinksInApp: boolean + /** Worktree-scoped localhost hostnames to distinguish tabs; opt-in since a non-localhost host can break apps binding cookies/sessions to localhost. */ + localhostWorktreeLabelsEnabled?: boolean + /** Tracks the one-time first-use prompt for terminal link routing (avoid silently changing where links open). */ + openLinksInAppPreferencePrompted: boolean + /** Opt-in: Shift+modifier click inverts openLinksInApp instead of always forcing the system browser. Off keeps the historical one-way escape hatch. */ + openLinksInAppModifierInverts?: boolean + /** Show terminal link actions on plain click; off restores modifier-click-only terminal links. */ + terminalLinkActionPopoverEnabled?: boolean + /** Opt-in: open new coding-agent tabs in native chat instead of the raw terminal; optional for legacy settings. */ + openAgentTabsInChatByDefault?: boolean + /** Experimental native chat surface for Claude/Codex sessions; off by default. */ + experimentalNativeChat?: boolean + /** Last explicit native-chat model + option selections; live panes need an applied/dispatched record before showing a value. */ + nativeChatSessionOptions?: PersistedNativeChatSessionOptions + /** Extra launcher rows for the worktree "Open in" submenu. VS Code is always shown first. */ + openInApplications?: OpenInApplication[] + /** Deprecated: migration/backward-compat only. Use PersistedUIState.rightSidebarOpen. */ + rightSidebarOpenByDefault: boolean + showGitIgnoredFiles?: boolean + /** Preferred Source Control changes layout. Per-user, not per-workspace. */ + sourceControlViewMode: SourceControlViewMode + /** Preferred Source Control group order. Per-user, not per-workspace. */ + sourceControlGroupOrder: SourceControlGroupOrder + /** Compare base defaults to the branch upstream instead of the repo default; affects only the compare/diff view, not the PR/rebase target. Per-user. */ + sourceControlCompareAgainstUpstream: boolean + /** Whether to show the Orca app name in the titlebar. */ + showTitlebarAppName: boolean + /** Hides the Tasks sidebar button (also removes it from keyboard navigation). */ + showTasksButton: boolean + /** Only toggles the sidebar shortcut; Automations stay reachable from Settings/View menu. */ + showAutomationsButton?: boolean + /** Deprecated: Artifacts are always available. Use showArtifactsButton for sidebar visibility. */ + artifactsEnabled?: boolean + /** Capability gate for agent-driven publishing; off until granted, enforced in main, not just the UI. */ + artifactSharingEnabled?: boolean + /** Only toggles the sidebar shortcut; Artifacts stay reachable from Settings. */ + showArtifactsButton?: boolean + /** Only toggles the sidebar shortcut; Orca Mobile stays reachable from Settings. */ + showMobileButton?: boolean + /** Pinned workspaces show in one sidebar location by default; opt in to also show them in their natural groups. */ + showPinnedWorktreesInGroups?: boolean + /** How Ctrl+Tab picks the next visible tab; optional (older profiles), readers default to MRU. */ + ctrlTabOrderMode?: CtrlTabOrderMode + /** Orca-first keeps app shortcuts from TUIs; terminal-first is opt-in to let shell/TUI bindings win. */ + terminalShortcutPolicy?: TerminalShortcutPolicy + /** Floating Workspace: global surface for terminal/browser/markdown tabs outside repo/worktree context. */ + floatingTerminalEnabled: boolean + /** One-shot migration flag for the floating-workspace default-on rollout; after migration an explicit off sticks. */ + floatingTerminalDefaultedForAllUsers?: boolean + /** Start dir for new floating-workspace terminal tabs; empty or '~' = home dir. */ + floatingTerminalCwd: string + /** Picker-approved floating-workspace dirs reauthorized across restarts; renderer text alone must not populate this. */ + floatingTerminalTrustedCwds?: string[] + /** One-shot migration marker for legacy floating workspace cwd trust grants. */ + floatingTerminalCwdMigratedToAppWorkspace?: boolean + /** Where the Floating Workspace toggle is shown; defaults to the floating button for discoverability. */ + floatingTerminalTriggerLocation: FloatingTerminalTriggerLocation + /** Legacy keyboard-shortcut overrides; new writes go to ~/.orca/keybindings.json, migrated once when present. */ + keybindings?: KeybindingOverrides + diffDefaultView: 'inline' | 'side-by-side' + diffWordWrap: boolean + combinedDiffFileTreeVisibleByDefault: boolean + /** Bot-marked comment-author logins (stored lowercased); escape hatch for review bots on regular accounts that defeat provider metadata/heuristics. */ + prBotAuthorOverrides: string[] + notifications: NotificationSettings + /** Countdown after a Claude agent goes idle showing time left before the prompt cache expires. */ + promptCacheTimerEnabled: boolean + /** Prompt-cache TTL (ms); only 300000 (5 min standard) or 3600000 (1 hr, extended-TTL plans). */ + promptCacheTtlMs: number + /** Why: durable main-owned pref so Orca can prepare shared ~/.codex before the renderer hydrates. */ + codexManagedAccounts: CodexManagedAccount[] + activeCodexManagedAccountId: string | null + activeCodexManagedAccountIdsByRuntime?: CodexManagedAccountRuntimeSelection + /** Why: persist only per-account auth (not a CLAUDE_CONFIG_DIR swap) so switching accounts doesn't fork Claude's shared chat/session context. */ + claudeManagedAccounts: ClaudeManagedAccount[] + activeClaudeManagedAccountId: string | null + activeClaudeManagedAccountIdsByRuntime?: ClaudeManagedAccountRuntimeSelection + /** Per-worktree shell history file so ArrowUp doesn't surface other worktrees' commands. Defaults to true. */ + terminalScopeHistoryByWorktree: boolean + /** Kill switch for hidden terminal view parking: unmount long-hidden panes while a pane-less watcher keeps PTY side effects alive. */ + terminalHiddenViewParking?: boolean + /** Kill switch for SSH terminal parking (C1): SSH panes park like local ones; reveal restores from main's headless model, falling back to relay replay. */ + terminalSshViewParking?: boolean + /** Kill switch for the hidden-worktree retention budget (C1): force-parks the least-recently-hidden un-parkable worktrees beyond a count budget or TTL. */ + terminalHiddenWorktreeRetentionBudget?: boolean + /** Kill switch for the browser-guest worktree retention budget: destroys the least-recently-activated hidden worktrees' webview guests beyond an LRU count budget. */ + browserGuestWorktreeRetentionBudget?: boolean + /** Kill switch for main-process PTY side-effect authority; on (default) = title/bell/agent facts via pty:sideEffect channel, not renderer byte parsing. */ + terminalMainSideEffectAuthority?: boolean + /** Kill switch for main's hidden-delivery gate (Phase 4): drops PTY bytes to hidden views after model ingestion; requires terminalMainSideEffectAuthority. */ + terminalHiddenDeliveryGate?: boolean + /** Kill switch for main's model query responder (Phase 5); active only when both Phase-4 gates are also on. */ + terminalModelQueryAuthority?: boolean + /** Which agent to pre-select in the new-workspace composer. + * - null: auto (first detected agent) + * - 'blank': blank terminal (no agent launched) + * - TuiAgent: a specific agent id */ + defaultTuiAgent: TuiAgent | 'blank' | null + /** Agents hidden from picker/auto-launch; detection stays a raw PATH snapshot. */ + disabledTuiAgents: TuiAgent[] + /** Master switch for the experimental plugin system. Off by default: no + * discovery, no panels, no plugin code paths run at all. */ + pluginSystemEnabled: boolean + /** Qualified plugin keys (`publisher.id`) the user disabled. Discovered + * plugins stay listed but are not activated. */ + disabledPlugins: string[] + /** Consent records: qualified plugin key → capability/worker-trust fingerprint. + * A plugin whose current fingerprint differs is pending again, so an update + * crossing either trust boundary re-prompts before code runs. Absent key = + * never consented. */ + pluginConsents: Record + /** Local directories loaded as dev-mode plugins (manifest hot-reload). */ + devPluginPaths: string[] + /** One-shot guard: start Claude Agent Teams hidden for existing profiles without overriding later opt-ins. */ + claudeAgentTeamsDefaultDisabledMigrated?: boolean + /** Why: worktree deletion is destructive (rm -rf of the working dir), so confirm by default. */ + skipDeleteWorktreeConfirm: boolean + /** Why: closing a terminal with child processes kills foreground work; keep this skip separate from other confirmations. */ + skipCloseTerminalWithRunningProcessConfirm: boolean + /** Why: deleting an automation also deletes its run history; keep this skip separate from worktree deletion. */ + skipDeleteAutomationConfirm: boolean + /** Why: deleting an artifact breaks a public link others may already hold; keep this skip separate from local deletions. */ + skipDeleteArtifactConfirm: boolean + /** Why: a Codex rate-limit reset spends a scarce credit on the live account; keep this skip separate from local confirmations. */ + skipCodexRateLimitResetConfirm: boolean + /** Default preset in the new-workspace GitHub task view. */ + defaultTaskViewPreset: TaskViewPresetId + /** Persisted last-used task source so Tasks reopens to the same provider instead of defaulting to GitHub. */ + defaultTaskSource: TaskProvider + /** Persisted visible task providers; hides unused providers from Tasks chrome and sidebar shortcuts. */ + visibleTaskProviders: TaskProvider[] + /** Why: one-shot guard to make Jira visible for existing profiles once, without re-adding after a later opt-out. */ + visibleTaskProvidersDefaultedForJira: boolean + /** Persisted repo selection (cross-repo tasks view). null = sticky-all (includes future-added repos); + * string[] = frozen curated subset (ineligible ids dropped on load; empty after drop is treated as null). */ + defaultRepoSelection: string[] | null + /** Persisted Linear team selection (tasks view). Same nullable-array pattern as + * defaultRepoSelection: null = sticky-all, string[] = frozen subset of team IDs. */ + defaultLinearTeamSelection: string[] | null + /** Session cookie for OpenCode Go rate-limit fetching. Stored encrypted. */ + opencodeSessionCookie: string + /** Optional OpenCode Go workspace ID override; when set, skips the workspaces lookup and fetches usage directly. */ + opencodeWorkspaceId: string + /** Optional MiniMax group id. When empty, the usage fetcher extracts minimax_group_id_v2 from the cookie. */ + minimaxGroupId: string + /** Comma-separated MiniMax model names to show in the status bar usage window. */ + minimaxUsageModels: string + /** Extract OAuth credentials from the local Gemini CLI for rate-limit fetching. Off by default (explicit opt-in). */ + geminiCliOAuthEnabled: boolean + /** Per-agent CLI command overrides. A missing key means use the catalog default binary name. */ + agentCmdOverrides: Partial> + /** Custom CODEX_HOME for Codex session-history discovery (defaults to ~/.codex). + * History-only: does not change which account/config/hooks Orca uses. */ + codexSessionSourceHome?: { + /** Absolute host path; empty/undefined falls back to ~/.codex. */ + host?: string + /** Per-WSL-distro absolute Linux path; missing distro falls back to /.codex. */ + wsl?: Record + } + /** Per-agent default CLI arguments appended after the binary/path and before prompts. */ + agentDefaultArgs?: Partial> + /** Per-agent launch environment defaults used when yolo mode is exposed as env. */ + agentDefaultEnv?: Partial>> + /** One-shot guard for adding yolo-mode default args to untouched agent launch profiles. */ + agentYoloDefaultsMigrated?: boolean + /** Why: disabling must persist so startup doesn't reinstall global agent hook entries the user just removed. */ + agentStatusHooksEnabled: boolean + /** Dismissed freshness tuples: no write authority, just suppress re-nudging the same official placement/revision. */ + dismissedSkillFreshnessNudges?: string[] + /** Why: generated tab titles are subjective, so they stay opt-in and manual renames win. */ + tabAutoGenerateTitle: boolean + /** Why: pinned tabs can still be closed via keyboard/native-menu; this gates that behind a confirmation. Defaults on. */ + confirmClosePinnedTab: boolean + /** When true, Orca requests local awake assertions while hook-reported agents are working. */ + keepComputerAwakeWhileAgentsRun: boolean + /** Optional for mixed-version compatibility; the legacy boolean maps true to Auto. */ + computerAwakeMode?: ComputerAwakeMode + /** macOS Option key: compose layout chars (@ German, € French) vs act as Meta/Esc for readline. + * 'auto' (default) = layout-aware via navigator.keyboard.getLayoutMap() (US → Meta, else compose); + * 'false' = compose; 'true' = Meta on both Option keys; 'left'/'right' = only that key is Meta. + * See docs/terminal-option-key-layout-aware-default.md. */ + terminalMacOptionAsAlt: 'auto' | 'true' | 'false' | 'left' | 'right' + /** One-shot migration guard for the 'auto' rollout. Old default 'true' was ambiguous (explicit vs default); + * on first upgrade launch, reset a persisted 'true' to 'auto' so non-US keyboards aren't broken by the stale default. */ + terminalMacOptionAsAltMigrated: boolean + /** Whether macOS terminal input maps the physical JIS Yen (¥) key to backslash, per common terminal expectation. */ + terminalJISYenToBackslash: boolean + experimentalMobile: boolean + /** Why: iOS Simulator is default-on for capable macOS hosts; this is the durable off switch (hides UI, blocks CLI attach). */ + mobileEmulatorEnabled?: boolean + /** Preferred iOS Simulator UDID for UI auto-attach and agent CLI attach. */ + mobileEmulatorDefaultDeviceUdid?: string | null + /** Explicit Android SDK root for when auto-discovery (ANDROID_HOME / default path) fails; null (default) auto-discovers. */ + androidSdkPath?: string | null + /** Auto-restore window (ms) for a phone-fit PTY after the last mobile subscriber leaves. + * `null` (default) holds phone size indefinitely; a finite value schedules restore. + * Clamped on read to [5_000ms, 60min]. See docs/mobile-fit-hold.md. */ + mobileAutoRestoreFitMs: number | null + /** Preferred mobile pairing path for new QR codes. Missing/'automatic' = Anywhere (Relay + local); + * explicit 'local-only' = same-network only. */ + mobilePairingConnectionMode?: 'automatic' | 'local-only' + /** Explicit custom address restored when generating future mobile pairing codes. */ + mobilePairingCustomAddress?: string | null + /** Saved custom addresses available in both mobile pairing pickers. */ + mobilePairingCustomAddresses?: string[] + /** Experimental: floating animated pet in the bottom-right corner. Opt-in cosmetic; + * off never mounts the overlay, and toggling takes effect instantly (renderer-side). */ + experimentalPet: boolean + /** Legacy persisted key from before the sidekick -> pet rename; read only during migration, new writes use experimentalPet. */ + experimentalSidekick?: boolean + /** Experimental: left-sidebar Agents view — threaded feed of agent completions, blocking/unread state, worktree creation. */ + experimentalActivity: boolean + /** Experimental: pop-out Kanban dashboard for monitoring and opening agent terminals across worktrees. */ + experimentalAgentDashboardPopout?: boolean + /** How the Agent Dashboard opens: an in-window companion board or a separate pop-out window. Defaults to in-window. */ + experimentalAgentDashboardMode?: AgentDashboardMode + /** Includes stale quiet agents as a fourth Agent Dashboard column. */ + experimentalAgentDashboardShowIdle?: boolean + /** One-shot migration guard for defaulting the Agents view off; later explicit opt-ins persist normally. */ + experimentalActivityDefaultedOffForAllUsers?: boolean + /** Experimental: persistent terminal-pane attention ring for bell + agent-completion events. Opt-in while tuning signal/noise. */ + experimentalTerminalAttention: boolean + /** Experimental: automatically sleep completed, resumable background agent terminals. */ + experimentalAgentHibernation?: boolean + /** Milliseconds a completed agent must stay idle before hibernation can be considered. */ + agentHibernationIdleMs?: number + /** Experimental: opt-in preview of the updated worktree-card layout and metadata behavior. */ + experimentalNewWorktreeCardStyle?: boolean + /** Experimental: per-workspace on-demand environment recipes and setup surface. */ + experimentalEphemeralVms?: boolean + /** Compact worktree cards: hide the metadata row when title and branch say the same thing. */ + compactWorktreeCards: boolean + /** Legacy persisted key from the Experimental rollout; new writes use compactWorktreeCards. */ + experimentalCompactWorktreeCards?: boolean + /** Active non-local runtime environment for client-routed RPC; null keeps local desktop behavior. */ + activeRuntimeEnvironmentId?: string | null + /** GitHub Project mode state (pinned/recent/active project, last view per project). + * Optional for pre-feature profiles; the persistence merge hydrates the default. */ + githubProjects?: GitHubProjectSettings + /** AI commit-message config (agent, model, per-model thinking, prompt suffix). Optional to avoid migrating existing profiles. */ + commitMessageAi?: CommitMessageAiSettings + /** Source-control AI generation settings for commit messages and hosted-review drafts. */ + sourceControlAi?: SourceControlAiSettings + /** GitLab project preferences (pinned + recent paths). Optional for pre-GitLab profiles; persistence merge fills the default. */ + gitlabProjects?: GitLabProjectSettings + /** Anonymous product-telemetry state; optional until the one-shot Store.load() migration populates it. + * Holds only consent + identity, not volatile counters — those would amplify the debounced settings write. */ + telemetry?: { + /** New users: true at install. Existing users: null until they resolve the first-launch banner. */ + optedIn: boolean | null + /** Anonymous UUID v4. Generated on first run. Stable across launches; not surfaced in the UI. */ + installId: string + /** Cohort marker: true for pre-existing profiles (gates the opt-in banner), false for fresh installs. */ + existedBeforeTelemetryRelease: boolean + } + /** One-shot cohort marker for the tab-switch keybinding swap. 'pending' = + * pre-existing install (seed pins old chords, then flips to 'done'); 'done' = fresh install. */ + tabSwitchKeybindingSeed?: 'pending' | 'done' + /** Local voice/dictation config. Optional for pre-voice profiles; getDefaultSettings() hydrates defaults via the persistence merge. */ + voice?: VoiceSettings +} + +export type OrcaWorkspaceLayout = { + path: string + nestWorkspaces: boolean +} + +export type GhosttyImportPreview = { + found: boolean + configPath?: string + configPaths?: string[] + diff: Partial + unsupportedKeys: string[] + error?: string +} diff --git a/src/shared/host-setting-overrides.ts b/src/shared/host-setting-overrides.ts index 908da41fd0e..5d7f0693d3a 100644 --- a/src/shared/host-setting-overrides.ts +++ b/src/shared/host-setting-overrides.ts @@ -1,5 +1,17 @@ import type { ExecutionHostId } from './execution-host' -import type { GlobalSettings, HostSettingOverrides } from './types' +import type { GlobalSettings } from './types' + +/** Per-host overrides for client preferences that genuinely vary by execution + * host. NARROW by design: only settings whose value is meaningless to share + * across hosts belong here. + * - `displayLabel`: a client-side rename for the host shown in sidebar/pickers. + * - `defaultWorktreeLocation`: the host's root worktree directory; a remote + * SSH/runtime host has a different filesystem layout than the local Mac, so + * the client `workspaceDir` default cannot apply unchanged. */ +export type HostSettingOverrides = { + displayLabel?: string + defaultWorktreeLocation?: string +} // Why: per-host preferences follow `effective = host override ?? client default`. // These pure helpers centralize that rule so the UI, registry, and tests share a diff --git a/src/shared/issue-mutation-types.ts b/src/shared/issue-mutation-types.ts new file mode 100644 index 00000000000..421befcdb90 --- /dev/null +++ b/src/shared/issue-mutation-types.ts @@ -0,0 +1,42 @@ +export type GitHubCreateIssueFields = { + labels?: string[] + assignees?: string[] +} + +export type GitHubCreateIssueResult = + | { ok: true; number: number; url: string; bodySaveWarning?: string } + | { ok: false; error: string } + +export type GitHubIssueCloseReason = 'completed' | 'not_planned' | 'duplicate' + +export type GitHubIssueUpdate = { + state?: 'open' | 'closed' + stateReason?: GitHubIssueCloseReason + duplicateOf?: number + title?: string + // Why: body writes use the REST issue endpoint instead of `gh issue edit` + // because that command does not consistently cover every body-edit case the + // dialog needs. + body?: string + addLabels?: string[] + removeLabels?: string[] + addAssignees?: string[] + removeAssignees?: string[] +} + +export type GitHubPullRequestStateUpdate = { + state: 'open' | 'closed' +} + +export type LinearIssueUpdate = { + stateId?: string + title?: string + description?: string + assigneeId?: string | null + estimate?: number | null + priority?: number + dueDate?: string | null + labelIds?: string[] + projectId?: string | null + parentId?: string | null +} diff --git a/src/shared/linear-issue-types.ts b/src/shared/linear-issue-types.ts new file mode 100644 index 00000000000..9f82d3b88e6 --- /dev/null +++ b/src/shared/linear-issue-types.ts @@ -0,0 +1,52 @@ +import type { LinearProjectSummary } from './linear-project-types' + +export type LinearIssue = { + id: string + workspaceId?: string + workspaceName?: string + identifier: string + title: string + branchName?: string + description?: string + url: string + state: { + name: string + type: string + color: string + } + team: { + id: string + name: string + key: string + } + project?: LinearProjectSummary + subIssues?: LinearIssueChildSummary[] + labels: string[] + labelIds: string[] + assignee?: { + id: string + displayName: string + avatarUrl?: string + } + estimate?: number | null + priority: number + dueDate?: string | null + updatedAt: string +} + +export type LinearIssueChildSummary = { + id: string + identifier: string + title: string + url: string +} + +export type LinearComment = { + id: string + body: string + createdAt: string + user?: { + displayName: string + avatarUrl?: string + } +} diff --git a/src/shared/linear-project-types.ts b/src/shared/linear-project-types.ts new file mode 100644 index 00000000000..a1f914ddefd --- /dev/null +++ b/src/shared/linear-project-types.ts @@ -0,0 +1,107 @@ +export type LinearProjectSummary = { + id: string + slugId?: string + workspaceId?: string + workspaceName?: string + name: string + url?: string + color?: string + icon?: string + description?: string + content?: string + status?: LinearProjectStatusSummary + health?: string | null + priority?: number | null + priorityLabel?: string | null + lead?: LinearProjectMemberSummary + members?: LinearProjectMemberSummary[] + teams?: { + id: string + name: string + key?: string + }[] + labels?: { + id: string + name: string + color?: string + }[] + startDate?: string | null + targetDate?: string | null + createdAt?: string + updatedAt?: string + completedAt?: string | null + canceledAt?: string | null + startedAt?: string | null + progress?: number | null + scope?: number | null + issueCount?: number + completedIssueCount?: number +} + +export type LinearProjectStatusSummary = { + id: string + name: string + type?: string + color?: string +} + +export type LinearProjectMemberSummary = { + id: string + displayName: string + avatarUrl?: string +} + +export type LinearProjectMilestoneSummary = { + id: string + name: string + status?: string + targetDate?: string | null + progress?: number | null +} + +export type LinearProjectResourceSummary = { + id: string + title: string + url: string + type?: string +} + +export type LinearProjectUpdateSummary = { + id: string + body?: string + health?: string | null + url?: string + createdAt?: string + updatedAt?: string + user?: LinearProjectMemberSummary +} + +export type LinearProjectDetail = LinearProjectSummary & { + milestones?: LinearProjectMilestoneSummary[] + resources?: LinearProjectResourceSummary[] + latestUpdate?: LinearProjectUpdateSummary +} + +export type LinearCustomViewModel = 'issue' | 'project' + +export type LinearCustomViewSummary = { + id: string + workspaceId?: string + workspaceName?: string + name: string + description?: string + model: LinearCustomViewModel + url?: string + color?: string + icon?: string + shared?: boolean + team?: { + id: string + name?: string + key?: string + } + owner?: LinearProjectMemberSummary + creator?: LinearProjectMemberSummary + createdAt?: string + updatedAt?: string +} diff --git a/src/shared/linear-workspace-types.ts b/src/shared/linear-workspace-types.ts new file mode 100644 index 00000000000..802842bb72f --- /dev/null +++ b/src/shared/linear-workspace-types.ts @@ -0,0 +1,73 @@ +export type LinearViewer = { + displayName: string + email: string | null + organizationId?: string + organizationName: string + organizationUrlKey?: string +} + +export type LinearWorkspace = LinearViewer & { + id: string + organizationId: string + isLegacy?: true + credentialRevision?: number +} + +export type LinearWorkspaceSelection = (string & {}) | 'all' +export type LinearWorkspaceSelector = LinearWorkspaceSelection | undefined +export type LinearConcreteWorkspaceId = string + +export type LinearWorkspaceError = { + workspaceId: string + workspaceName?: string + type: 'auth' | 'rate_limited' | 'network' | 'unknown' + message: string +} + +export type LinearCollectionResult = { + items: T[] + errors?: LinearWorkspaceError[] + hasMore?: boolean +} + +export type LinearConnectionStatus = { + connected: boolean + viewer: LinearViewer | null + workspaces?: LinearWorkspace[] + activeWorkspaceId?: string | null + selectedWorkspaceId?: LinearWorkspaceSelection | null + // Set when a stored token file exists but could not be decrypted, so the + // UI can explain reads failing while the connection still looks saved. + credentialError?: string +} + +export type LinearWorkflowState = { + id: string + name: string + type: string + color: string + position: number +} + +export type LinearLabel = { + id: string + name: string + color: string +} + +export type LinearMember = { + id: string + displayName: string + name?: string + email?: string + avatarUrl?: string +} + +export type LinearTeam = { + id: string + workspaceId?: string + workspaceName?: string + name: string + key: string + url?: string +} diff --git a/src/shared/managed-account-types.ts b/src/shared/managed-account-types.ts new file mode 100644 index 00000000000..7239c62e6ad --- /dev/null +++ b/src/shared/managed-account-types.ts @@ -0,0 +1,95 @@ +export type CodexManagedAccount = { + id: string + email: string + managedHomePath: string + managedHomeRuntime?: 'host' | 'wsl' + wslDistro?: string | null + wslLinuxHomePath?: string | null + providerAccountId?: string | null + workspaceLabel?: string | null + workspaceAccountId?: string | null + createdAt: number + updatedAt: number + lastAuthenticatedAt: number +} + +export type CodexManagedAccountSummary = { + id: string + email: string + managedHomeRuntime?: 'host' | 'wsl' + wslDistro?: string | null + providerAccountId?: string | null + workspaceLabel?: string | null + workspaceAccountId?: string | null + createdAt: number + updatedAt: number + lastAuthenticatedAt: number +} + +/** Live, read-only identity of the user's real ~/.codex used by the + * system-default (activeAccountId:null) Codex account. Orca reads this to + * display and attribute the system default; it never writes ~/.codex. */ +export type CodexSystemDefaultIdentity = { + /** True when ~/.codex/auth.json exists (signed in via a token file). */ + hasAuth: boolean + /** 'oauth' = ChatGPT sign-in with an id token (has ChatGPT usage); + * 'api-key' = env-key/custom provider (no ChatGPT usage); + * 'none' = signed out or identity could not be resolved. */ + authKind: 'oauth' | 'api-key' | 'none' + email: string | null + providerAccountId: string | null + workspaceLabel: string | null +} + +export type CodexRateLimitAccountsState = { + accounts: CodexManagedAccountSummary[] + activeAccountId: string | null + activeAccountIdsByRuntime?: CodexManagedAccountRuntimeSelection + /** Resolved identity of the host system-default (real ~/.codex) account. + * Omitted for runtimes where it is not resolved (e.g. per-distro WSL). */ + systemDefault?: CodexSystemDefaultIdentity +} + +export type CodexManagedAccountRuntimeSelection = { + host: string | null + wsl: Record +} + +export type ClaudeManagedAccount = { + id: string + email: string + managedAuthPath: string + managedAuthRuntime?: 'host' | 'wsl' + wslDistro?: string | null + wslLinuxAuthPath?: string | null + authMethod: 'subscription-oauth' | 'unknown' + organizationUuid?: string | null + organizationName?: string | null + createdAt: number + updatedAt: number + lastAuthenticatedAt: number +} + +export type ClaudeManagedAccountSummary = { + id: string + email: string + managedAuthRuntime?: 'host' | 'wsl' + wslDistro?: string | null + authMethod: 'subscription-oauth' | 'unknown' + organizationUuid?: string | null + organizationName?: string | null + createdAt: number + updatedAt: number + lastAuthenticatedAt: number +} + +export type ClaudeRateLimitAccountsState = { + accounts: ClaudeManagedAccountSummary[] + activeAccountId: string | null + activeAccountIdsByRuntime?: ClaudeManagedAccountRuntimeSelection +} + +export type ClaudeManagedAccountRuntimeSelection = { + host: string | null + wsl: Record +} diff --git a/src/shared/notification-settings-types.ts b/src/shared/notification-settings-types.ts new file mode 100644 index 00000000000..fe4ef74015c --- /dev/null +++ b/src/shared/notification-settings-types.ts @@ -0,0 +1,106 @@ +import type { AgentStatusState, AgentType } from './agent-status-types' + +export type NotificationSettings = { + enabled: boolean + agentTaskComplete: boolean + terminalBell: boolean + suppressWhenFocused: boolean + customSoundId: + | 'system' + | 'two-tone' + | 'bong' + | 'thump' + | 'blip' + | 'sonar' + | 'blop' + | 'ding' + | 'clack' + | 'beep' + | 'custom' + customSoundPath: string | null + customSoundVolume: number +} + +export type NotificationEventSource = 'agent-task-complete' | 'terminal-bell' | 'test' + +export type NotificationDispatchRequest = { + source: NotificationEventSource + notificationId?: string + /** Why: useful for fast native failures, but macOS can still drop notifications after 'show'. */ + requireDisplayConfirmation?: boolean + worktreeId?: string + /** Stable `${tabId}:${leafId}` terminal pane key for click-to-focus routing. */ + paneKey?: string + repoLabel?: string + worktreeLabel?: string + hasMultipleActiveRepos?: boolean + terminalTitle?: string + isActiveWorktree?: boolean + agentType?: AgentType + agentState?: AgentStatusState + agentPrompt?: string + agentToolName?: string + agentToolInput?: string + agentLastAssistantMessage?: string + agentInterrupted?: boolean +} + +export type NotificationDispatchResult = { + delivered: boolean + /** Why delivery was skipped (set when delivered is false); 'blocked-by-system' = macOS would silently swallow it. */ + reason?: + | 'disabled' + | 'source-disabled' + | 'suppressed-focus' + | 'cooldown' + | 'not-supported' + | 'not-displayed' + | 'blocked-by-system' + | 'invalid-request' +} + +export type NotificationDismissResult = { + dismissed: number +} + +export type NotificationSoundResult = { + played: boolean + reason?: + | 'missing-path' + | 'invalid-path' + | 'unsupported-type' + | 'too-large' + | 'read-failed' + | 'playback-failed' + | 'deduped' +} + +export type NotificationSoundDataResult = + | { + ok: true + data: Uint8Array + mimeType: string + path: string + } + | { + ok: false + reason: Exclude + } + +export type NotificationSoundPathResult = + | { ok: true; path: string } + | { ok: false; reason: 'missing-path' | 'invalid-path' | 'unsupported-type' } + +export type NotificationPermissionStatusResult = { + supported: boolean + platform: NodeJS.Platform + requested: boolean +} + +/** macOS notification permission outcome: authoritative native UNUserNotificationCenter readout, else a weaker + * delivery-probe fallback; 'awaiting-decision' = permission dialog unanswered. */ +export type NotificationDeliveryProbeResult = { + state: 'delivered' | 'blocked' | 'awaiting-decision' | 'unsupported' + /** True when the state comes from the native authorization readout (vs. the delivery-probe fallback). */ + authoritative: boolean +} diff --git a/src/shared/onboarding-state-types.ts b/src/shared/onboarding-state-types.ts new file mode 100644 index 00000000000..669c70b94f3 --- /dev/null +++ b/src/shared/onboarding-state-types.ts @@ -0,0 +1,30 @@ +// Subset of onboarding Ghostty DiscoveryState statuses that emit telemetry; UI-only 'idle'/'detecting' don't. +export type DiscoveryStatusEmitted = 'found' | 'absent' | 'imported' + +export type OnboardingOutcome = 'completed' | 'dismissed' + +export type OnboardingChecklistState = { + addedRepo: boolean + choseAgent: boolean + ranFirstAgent: boolean + ranSecondAgentOnSameTask: boolean + triedCmdJ: boolean + shapedSidebar: boolean + reviewedDiff: boolean + openedPr: boolean + addedFolder: boolean + openedFile: boolean + ranAgentOnFile: boolean + // Why: UI state flag (panel visibility), not an activation event; telemetry checklist enum omits it. + dismissed: boolean +} + +export type OnboardingState = { + // Why: step meanings change when pages are removed; version marker prevents migration re-running on new progress. + flowVersion: number + closedAt: number | null + outcome: OnboardingOutcome | null + // Sentinel -1 = not started; 1..5 = highest finished wizard step. number (not union) because callers clamp via Math.max/min. + lastCompletedStep: number + checklist: OnboardingChecklistState +} diff --git a/src/shared/orca-yaml-hook-types.ts b/src/shared/orca-yaml-hook-types.ts new file mode 100644 index 00000000000..42122784490 --- /dev/null +++ b/src/shared/orca-yaml-hook-types.ts @@ -0,0 +1,78 @@ +export type SetupRunPolicy = 'ask' | 'run-by-default' | 'skip-by-default' +export type SetupAgentStartupPolicy = 'start-immediately' | 'wait-for-setup' +export type HookCommandSourcePolicy = 'shared-only' | 'local-only' | 'run-both' + +// ─── Hooks (orca.yaml) ────────────────────────────────────────────── +export type OrcaHooks = { + scripts: { + setup?: string // Runs after worktree is created + archive?: string // Runs before worktree is archived + } + issueCommand?: string // Shared default command for linked GitHub issues + defaultTabs?: OrcaDefaultTabTemplate[] // Terminal tabs to create once for a new worktree + environmentRecipes?: OrcaVmRecipe[] // Project-scoped per-workspace environment recipes + environmentRecipeDiagnostics?: OrcaVmRecipeDiagnostic[] // Non-fatal validation issues from environmentRecipes + worktree?: OrcaWorktreeDefaults // Project-scoped defaults applied when a worktree is created +} + +export type OrcaWorktreeDefaults = { + // Why: shared (symlinked) rather than copied — large rebuildable dirs like + // node_modules should be one install serving every worktree. + sharedDirectories?: string[] +} + +export type OrcaDefaultTabTemplate = { + title?: string + color?: string + command?: string +} + +export type EphemeralVmCheckoutMode = 'orca-worktree' | 'provisioned-root' + +export type OrcaVmRecipe = { + id: string + name: string + create: string + checkoutMode?: EphemeralVmCheckoutMode + description?: string + suspend?: string + resume?: string + destroy?: string + destroyDisabled?: boolean +} + +export type OrcaVmRecipeDiagnostic = { + index: number + field?: string + message: string +} + +export type RepoHookSettings = { + // Why: persisted data may still include the old mode field from the earlier + // hook UI. Keep it in the shape so existing local state reads without a migration. + mode: 'auto' | 'override' + setupRunPolicy?: SetupRunPolicy + setupAgentStartupPolicy?: SetupAgentStartupPolicy + commandSourcePolicy?: HookCommandSourcePolicy + scripts: { + setup: string + archive: string + } +} + +export type PersistedTrustedOrcaHookEntry = { + contentHash: string + approvedAt: number +} + +export type PersistedTrustedOrcaHookRepo = { + all?: { + approvedAt: number + } + setup?: PersistedTrustedOrcaHookEntry + archive?: PersistedTrustedOrcaHookEntry + issueCommand?: PersistedTrustedOrcaHookEntry + vmRecipe?: PersistedTrustedOrcaHookEntry +} + +export type PersistedTrustedOrcaHooks = Record diff --git a/src/shared/persisted-state-types.ts b/src/shared/persisted-state-types.ts new file mode 100644 index 00000000000..76ec8824798 --- /dev/null +++ b/src/shared/persisted-state-types.ts @@ -0,0 +1,100 @@ +import type { ExecutionHostId } from './execution-host' +import type { + RemovedSshTargetTombstone, + SshPtyConsumerRecovery, + SshRemotePtyLease, + SshTarget +} from './ssh-types' +import type { Automation, AutomationRun } from './automations-types' +import type { MigrationUnsupportedPtyEntry } from './agent-status-types' +import type { FeatureInteractionTelemetryBucketState } from './feature-interactions' +import type { CodexResetCreditAttemptLedger } from './codex-reset-credit-attempt-ledger' +import type { DiffComment } from './diff-comment-types' +import type { FolderWorkspace, WorkspaceKey } from './folder-workspace-types' +import type { GlobalSettings } from './global-settings-types' +import type { IssueInfo, PRInfo } from './github-pr-types' +import type { OnboardingState } from './onboarding-state-types' +import type { PersistedUIState } from './persisted-ui-state-types' +import type { ProjectGroup } from './project-group-types' +import type { Project, ProjectHostSetup } from './project-types' +import type { Repo } from './repo-types' +import type { SparsePreset } from './worktree-create-types' +import type { WorkspaceLineage, WorktreeLineage } from './worktree-lineage-types' +import type { WorktreeMeta } from './worktree-meta-types' +import type { WorkspaceSessionState } from './workspace-session-state-types' + +export type LegacyPaneKeyAliasEntry = { + ptyId: string + /** Physical pane key retained by the live process; name is persisted for compatibility (UUID keys after detach). */ + legacyPaneKey: string + /** Current logical owner pane key. May belong to another tab after detach. */ + stablePaneKey: string + updatedAt: number +} + +/** Last tab selection a paired client made in a worktree; restores phone navigation across host restarts. */ +export type PersistedMobileClientTabSelection = { + activeTabId: string | null + activeGroupId: string | null + activeTabIdByGroupId: Readonly> +} + +/** deviceId → worktreeId → selection. */ +export type PersistedMobileClientTabSelections = Record< + string, + Record +> + +// ─── Persistence shape ────────────────────────────────────────────── +export type PersistedState = { + schemaVersion: number + repos: Repo[] + projects: Project[] + projectHostSetups: ProjectHostSetup[] + projectGroups: ProjectGroup[] + folderWorkspaces: FolderWorkspace[] + /** Folder-workspace review notes, keyed by FolderWorkspace.id. Top-level, NOT nested in + * folderWorkspaces[]: normalizeFolderWorkspaces rebuilds each record field-by-field, so an + * older build drops nested fields, while unknown top-level keys round-trip untouched. + * + * WRITE-ONLY PROJECTION. FolderWorkspace.diffComments is the single in-memory home; load() + * hydrates from this key and then deletes it from Store state, and buildStateToSave() is the + * only producer of it. Never read Store.state.folderWorkspaceDiffComments outside load(). */ + folderWorkspaceDiffComments?: Record + /** Sparse-checkout presets keyed by repoId. */ + sparsePresetsByRepo: Record + /** Per paired device last tab selection by worktree; keeps mobile navigation across host restarts. */ + mobileClientTabSelectionsByDeviceId?: PersistedMobileClientTabSelections + worktreeMeta: Record + worktreeLineageById: Record + workspaceLineageByChildKey: Record + settings: GlobalSettings + ui: PersistedUIState + githubCache: { + pr: Record + issue: Record + } + /** Legacy single-blob session, kept as the canonical 'local' host partition so an app downgrade still reads its workspace. */ + workspaceSession: WorkspaceSessionState + /** Per-execution-host session partitions for non-'local' hosts (ssh:/runtime:); 'local' stays in workspaceSession so pre-partition builds keep working. */ + workspaceSessionsByHostId?: Partial> + sshTargets: SshTarget[] + /** SSH config aliases the user deleted; suppresses re-import from ~/.ssh/config so a deleted host doesn't reappear. */ + deletedSshConfigAliases: string[] + /** Identity records for removed SSH targets so a re-added host can re-adopt workspaces orphaned on the old target id. */ + removedSshTargetTombstones?: RemovedSshTargetTombstone[] + sshRemotePtyLeases: SshRemotePtyLease[] + /** Main-owned authenticated relay recovery records; never expose through renderer settings APIs. */ + sshPtyConsumerRecoveries?: SshPtyConsumerRecovery[] + /** Live local Claude daemon session ids; seeds the live-PTY gate so early OAuth refresh can't rotate the single-use refresh token out from under a running daemon. */ + claudeLivePtySessionIds?: string[] + migrationUnsupportedPtyEntries: MigrationUnsupportedPtyEntry[] + legacyPaneKeyAliasEntries: LegacyPaneKeyAliasEntry[] + automations: Automation[] + automationRuns: AutomationRun[] + onboarding: OnboardingState + /** Main-owned telemetry de-dupe marker; never exposed through PersistedUIState. */ + featureInteractionTelemetryBuckets?: FeatureInteractionTelemetryBucketState + /** Main-owned reset mutation journal. Never expose this through renderer settings APIs. */ + codexResetCreditAttemptLedger?: CodexResetCreditAttemptLedger +} diff --git a/src/shared/persisted-ui-state-types.ts b/src/shared/persisted-ui-state-types.ts new file mode 100644 index 00000000000..c8650c6df83 --- /dev/null +++ b/src/shared/persisted-ui-state-types.ts @@ -0,0 +1,204 @@ +import type { ReleaseChannel } from './release-channel' +import type { WorkspaceCleanupUIState } from './workspace-cleanup' +import type { FeatureTipId } from './feature-tips' +import type { ContextualTourId } from './contextual-tours' +import type { FeatureInteractionState } from './feature-interactions' +import type { UsagePercentageDisplay } from './usage-percentage-display' +import type { StatusBarUsageMode } from './status-bar-usage-mode' +import type { PersistedTrustedOrcaHooks } from './orca-yaml-hook-types' +import type { CustomPet } from './pet-types' +import type { + AgentActivityDisplayMode, + ManualRepoOrderEntry, + ProjectOrderBy, + RightSidebarExplorerView, + RightSidebarTab, + StatusBarItem, + TaskResumeState, + TopLevelView, + VisibleWorkspaceHostIds, + WorkspaceHostOrder, + WorkspaceHostScope, + WorktreeCardProperty +} from './ui-chrome-types' +import type { WorkspaceStatusDefinition } from './worktree-types' + +export type PersistedUIState = { + lastActiveRepoId: string | null + lastActiveWorktreeId: string | null + /** Active top-level view at save time, restored on relaunch; sanitized to 'terminal' if unknown or now-gated. */ + activeView: TopLevelView + sidebarWidth: number + rightSidebarOpen: boolean + rightSidebarTab: RightSidebarTab + rightSidebarExplorerView: RightSidebarExplorerView + rightSidebarWidth: number + markdownTocPanelWidth?: number + combinedDiffFileTreeWidth?: number + groupBy: 'none' | 'workspace-status' | 'repo' | 'pr-status' + sortBy: 'name' | 'smart' | 'recent' | 'repo' | 'manual' + /** Project header ordering in `groupBy: 'repo'`, independent of `sortBy`: 'manual' uses persisted order + header drag, 'recent' by latest visible activity. */ + projectOrderBy: ProjectOrderBy + /** Deprecated; the Active only filter is retired and ignored on hydration. */ + showActiveOnly: boolean + /** Hide sleeping/inactive workspaces from workspace navigation. Off by default. */ + hideSleepingWorkspaces?: boolean + /** Which execution hosts the sidebar shows; `all` = mixed view, specific IDs focus without tearing down other hosts' sessions. */ + workspaceHostScope?: WorkspaceHostScope + /** Which execution hosts the sidebar shows; `null` = sticky all-hosts so new hosts appear automatically. */ + visibleWorkspaceHostIds?: VisibleWorkspaceHostIds + /** User-defined sidebar order for host sections; missing/new hosts append in discovered order. */ + workspaceHostOrder?: WorkspaceHostOrder + /** Desktop-owned all-host repo order; host-qualified identities keep a manual cross-host interleaving while each host owns its local permutation. */ + manualRepoOrder?: ManualRepoOrderEntry[] + /** Deprecated legacy positive-form setting. Ignored on hydration. */ + showSleepingWorkspaces?: boolean + /** Deprecated legacy name used by a short-lived build. Ignored on hydration. */ + showInactiveWorkspaces?: boolean + /** Hide the repo's checked-out branch from workspace nav (sidebar, Cmd+J); folder-mode repos are unaffected (empty-branch worktrees excluded). */ + hideDefaultBranchWorkspace: boolean + /** Hide workspaces created by automation new-per-run dispatches. */ + hideAutomationGeneratedWorkspaces?: boolean + /** Hide workspaces created through `orca worktree create`. */ + hideCliCreatedWorkspaces?: boolean + /** Hide workspaces sitting on a detached HEAD; folder workspaces (no head at all) are unaffected. */ + hideDetachedHeadWorkspaces?: boolean + /** Hide workspaces with known provenance from another paired device or the host UI. */ + hideWorkspacesFromOtherDevices?: boolean + /** Keep each project's main workspace out of the "Hide sleeping" sweep. Absent means on (#8873). */ + alwaysShowDefaultBranchWorkspace?: boolean + /** Per-worktree Explorer dotfile visibility. Missing entries inherit the default: show. */ + showDotfilesByWorktree?: Record + filterRepoIds: string[] + collapsedGroups: string[] + uiZoomLevel: number + editorFontZoomLevel: number + worktreeCardProperties: WorktreeCardProperty[] + /** One-shot migration flag for deriving card properties from the two worktree card modes. */ + _worktreeCardModeDefaulted?: boolean + agentActivityDisplayMode?: AgentActivityDisplayMode + workspaceStatuses?: WorkspaceStatusDefinition[] + workspaceBoardOpacity?: number + workspaceBoardColumnWidth?: number + syncTaskStatusFromWorkspaceBoard?: boolean + /** One-shot migration flag for a short-lived build that persisted default statuses in reverse order; once stamped, ordering is never re-inferred from IDs/labels. */ + _workspaceStatusesDefaultOrderMigrated?: boolean + /** One-shot repair flag for the exact default payload a short-lived build persisted in reverse workflow order. */ + _workspaceStatusesReorderedDefaultRepaired?: boolean + /** One-shot migration flag for default status workflow labels/visuals; only exact legacy defaults migrate, customized statuses preserved. */ + _workspaceStatusesDefaultWorkflowMigrated?: boolean + /** One-shot migration flag for the old default status visuals; once stamped, user-authored colors/icons are preserved. */ + _workspaceStatusesDefaultVisualsMigrated?: boolean + /** One-shot migration flag for adding the default-on Ports status item. */ + _portsStatusBarDefaultAdded?: boolean + /** One-shot migration flag for adding the default-on Kimi status item. */ + _kimiStatusBarDefaultAdded?: boolean + /** One-shot migration flag for adding the default-on MiniMax status item. */ + _minimaxStatusBarDefaultAdded?: boolean + /** One-shot migration flag for adding the default-on Antigravity status item. */ + _antigravityStatusBarDefaultAdded?: boolean + /** One-shot migration flag for adding the default-on Grok status item. */ + _grokStatusBarDefaultAdded?: boolean + statusBarItems: StatusBarItem[] + statusBarVisible: boolean + /** Why: this is client-side presentation, not a provider/account or execution-host setting. */ + usagePercentageDisplay?: UsagePercentageDisplay + /** Client-side footer presentation; verbose preserves the pre-roster all-window default. */ + statusBarUsageMode?: StatusBarUsageMode + dismissedUpdateVersion: string | null + lastUpdateCheckAt: number | null + /** Dev-only update channel override; absent means the build's own channel. */ + releaseChannelOverride?: ReleaseChannel | null + pendingUpdateNudgeId?: string | null + dismissedUpdateNudgeId?: string | null + /** Whether Orca already tried triggering the macOS notification permission dialog; prevents re-firing every launch. */ + notificationPermissionRequested?: boolean + /** Once the "your sessions won't be interrupted" reassurance card is seen, never show it again. */ + updateReassuranceSeen?: boolean + /** Per-paneKey "row visited" timestamps that mute seen inline-agent rows; persisted because rows survive restart, else acked rows return bold. Renderer-owned via ui:set. */ + acknowledgedAgentsByPaneKey?: Record + /** User-hidden setup-guide sidebar entry; a reversible declutter pref (Help menu stays available), not completion. */ + setupGuideSidebarDismissed?: boolean + /** One-shot marker for the browser setup-guide milestone; profiles missing it are evaluated once in the renderer (completion needs runtime probes). */ + setupGuideBrowserMilestoneMigrated?: boolean + /** Existing users who completed/dismissed the pre-browser checklist stay complete after the browser milestone is added. */ + setupGuideBrowserMilestoneLegacyComplete?: boolean + /** User-dismissed browser import toolbar hint; import stays available from Settings > Browser and the overflow menu. */ + browserImportHintHidden?: boolean + /** Why: Windows-only. Set once on first hide to tray so the "Orca is still running" notice shows only once. */ + trayMinimizeNoticeShown?: boolean + /** Set by the OSC 52 default-on migration when it overrode a persisted `false`; the renderer shows one notice and clears it. */ + osc52ClipboardDefaultOnNoticePending?: boolean + /** User dismissed the first-run Mobile Emulator intro; reversible only by re-enabling the feature in Settings. */ + mobileEmulatorTabIntroDismissed?: boolean + /** User deferred the in-pane Mobile Emulator CLI + skill setup guide. */ + mobileEmulatorAgentSetupDismissed?: boolean + /** One-shot rollout notice for manual project ordering default; absent or true keeps the sidebar callout hidden. */ + projectOrderManualDefaultNoticeDismissed?: boolean + /** One-shot notice that usage meters show percent used, not remaining; absent resolves on load (new profiles dismissed, upgraded see it once). */ + usagePercentageDisplayChangeNoticeDismissed?: boolean + /** User-hidden empty-state usage CTA; permanently hides the "Connect AI accounts" prompt even if providers are later disconnected. */ + usageEmptyStateDismissed?: boolean + /** URL for new browser tabs; null = blank tab. */ + browserDefaultUrl?: string | null + browserDefaultSearchEngine?: 'google' | 'duckduckgo' | 'bing' | 'kagi' | null + /** Electron browser zoom level applied when a new local browser tab is created. */ + browserDefaultZoomLevel?: number + /** Optional Kagi private-session link used only when Kagi is the search engine. */ + browserKagiSessionLink?: string | null + /** Saved window bounds so the app restores last position/size instead of maximizing each launch. */ + windowBounds?: { x: number; y: number; width: number; height: number } | null + /** Whether the window was maximized when it was last closed. */ + windowMaximized?: boolean + /** Saved bounds for the pop-out dashboard window so it restores to its last + * position/size. Independent of the main window's bounds. */ + dashboardPopoutBounds?: { x: number; y: number; width: number; height: number } | null + /** One-shot flag: 'recent' once meant the smart sort (v1→v2 rename), migrated to 'smart' once so the new last-activity 'recent' isn't re-clobbered. */ + _sortBySmartMigrated?: boolean + /** LEGACY inline-agents flag, stamped unconditionally every load so it can't gate migration; kept only for rollback forward-compat (real gate: _inlineAgentsDefaultedForAllUsers). */ + _inlineAgentsDefaultedForExperiment?: boolean + /** One-shot flag for the inline-agents default-on rollout; distinct from _inlineAgentsDefaultedForExperiment, which was stamped every load and is permanently dirty. */ + _inlineAgentsDefaultedForAllUsers?: boolean + /** One-shot migration flag for split-out card properties, set once so later deliberate unchecks of Linear issue/Ports stick across restarts. */ + _expandedWorktreeCardPropertiesDefaulted?: boolean + /** One-shot backfill flag for 'jira-issue', which joined the defaults after the expansion migration had already stamped upgraded profiles. */ + _jiraIssueWorktreeCardPropertyDefaulted?: boolean + /** totalAgentsSpawned snapshot at first sighting of the current app version, so the nag counts agents since last update (not from zero). */ + starNagBaselineAgents?: number | null + /** App version that set the current baseline; a version change re-captures the baseline on next spawn, restarting the nag countdown. */ + starNagAppVersion?: string | null + /** Next agents-since-baseline threshold that fires the star-nag; starts at 35, doubles per dismissal without starring. */ + starNagNextThreshold?: number + /** Once the user has starred Orca (any entry point), permanently suppress the nag. */ + starNagCompleted?: boolean + /** Timestamp until which nonterminal dismissals suppress threshold prompts (force-show bypasses for dev/testing). */ + starNagDeferredUntil?: number | null + /** App version that consumed the first value-moment ask; main-owned so remote/web clients can't spoof the once-per-version cap. */ + starNagAgentValueMomentAppVersion?: string | null + trustedOrcaHooks?: PersistedTrustedOrcaHooks + setupScriptPromptDismissedRepoIds?: string[] + /** Pet overlay visibility, separate from the experimentalPet settings flag so "Hide pet" is a reversible dismiss; absent = true. */ + petVisible?: boolean + /** Active pet id (bundled id or custom UUID); unknown ids fall back to the default on read so a removed custom pet doesn't blank the overlay. */ + petId?: string + /** Metadata index for user-uploaded pet images; bytes live under legacy userData/sidekicks/custom/. */ + customPets?: CustomPet[] + /** Pet overlay size in CSS pixels (square); clamped to [PET_SIZE_MIN, PET_SIZE_MAX] on read. */ + petSize?: number + /** Legacy keys from before the sidekick -> pet rename; read only during migration, new writes use pet* above. */ + sidekickVisible?: boolean + sidekickId?: string + customSidekicks?: CustomPet[] + sidekickSize?: number + /** Page-position state for Tasks: only transient tabs/searches (source/repo/team/project selections use their own settings paths). */ + taskResumeState?: TaskResumeState + workspaceCleanup?: WorkspaceCleanupUIState + /** Feature tips already surfaced; startup opens the tips modal only when a current tip id is missing here. */ + featureTipsSeenIds?: FeatureTipId[] + /** Feature ids the user has actually used; education surfaces skip teaching already-discovered features. */ + featureInteractions?: FeatureInteractionState + /** Contextual tours already surfaced; unknown ids ignored on hydration for downgrade/upgrade forward-compat. */ + contextualToursSeenIds?: ContextualTourId[] + /** Whether this profile may receive automatic contextual tours; missing = renderer hasn't classified the profile yet. */ + contextualToursAutoEligible?: boolean +} diff --git a/src/shared/pet-types.ts b/src/shared/pet-types.ts new file mode 100644 index 00000000000..c036d845673 --- /dev/null +++ b/src/shared/pet-types.ts @@ -0,0 +1,36 @@ +export const PET_SIZE_MIN = 60 +export const PET_SIZE_MAX = 360 +export const PET_SIZE_DEFAULT = 180 + +/** User-uploaded pet image metadata; renderer fetches bytes from main via pet:read (id, fileName), never learning the on-disk path. */ +export type CustomPet = { + id: string + label: string + fileName: string + /** MIME type for the renderer's Blob Content-Type — esp. image/svg+xml, which browsers won't render from a misdeclared blob URL. */ + mimeType: string + /** Storage layout: `image` = legacy flat file `custom/.`; `bundle` = `.codex-pet` expanded into `custom//`; absent = legacy `image`. */ + kind?: 'image' | 'bundle' + /** Sprite-sheet metadata; present iff from a `.codex-pet` bundle with a manifest frame layout. Dims derived in main so the renderer needn't probe the image. */ + sprite?: { + frameWidth: number + frameHeight: number + columns: number + rows: number + sheetWidth: number + sheetHeight: number + fps: number + defaultAnimation?: string + animations?: Record + } + /** Manifest-declared fps kept even when frames are auto-detected, so playback honors the bundle's speed instead of a hardcoded 8 fps. */ + spriteFps?: number +} + +/** One animation strip in a sprite sheet: `row` = 0-based y-index, `frames` = consecutive cells played left-to-right. */ +export type SpriteAnimation = { + row: number + frames: number + /** Per-frame holds in ms (length === frames). Absent means uniform sheet fps. */ + frameDurationsMs?: number[] +} diff --git a/src/shared/process-stats-types.ts b/src/shared/process-stats-types.ts new file mode 100644 index 00000000000..ec0187864ab --- /dev/null +++ b/src/shared/process-stats-types.ts @@ -0,0 +1,74 @@ +// ─── Stats ────────────────────────────────────────────────────────── + +export type StatsSummary = { + totalAgentsSpawned: number + totalPRsCreated: number + totalAgentTimeMs: number + // Sourced from aggregates, not the event log, so it survives event trimming. + firstEventAt: number | null // timestamp of first-ever event, for "tracking since..." +} + +// ─── Memory dashboard ────────────────────────────────────────────── + +/** cpu is percent of a single core — can exceed 100 on multi-core. memory is in bytes. */ +export type UsageValues = { + cpu: number + memory: number +} + +export type ProcessMemoryMetric = 'rss' | 'working-set' + +export type HostAvailableMemorySource = 'memory-pressure' | 'proc-meminfo' | 'free-memory' + +/** The top-level cpu/memory are the sum of main + renderer + other. */ +export type AppMemory = UsageValues & { + main: UsageValues + renderer: UsageValues + other: UsageValues + /** Oldest-first memory samples (bytes) for the whole Orca app; empty before the first snapshot. */ + history: number[] +} + +export type SessionMemory = UsageValues & { + sessionId: string + paneKey: string | null + pid: number +} + +/** The top-level cpu/memory are the sum of sessions. */ +export type WorktreeMemory = UsageValues & { + worktreeId: string + worktreeName: string + repoId: string + repoName: string + sessions: SessionMemory[] + /** Oldest-first memory samples (bytes) for this worktree's tracked subtrees. */ + history: number[] +} + +export type HostMemory = { + totalMemory: number + /** Immediately free memory reported by Node's host API. */ + freeMemory: number + /** Memory available without material pressure, or freeMemory when unavailable. */ + availableMemory: number + availableMemorySource: HostAvailableMemorySource + /** totalMemory - availableMemory. */ + usedMemory: number + memoryUsagePercent: number + cpuCoreCount: number + loadAverage1m: number +} + +export type MemorySnapshot = { + app: AppMemory + worktrees: WorktreeMemory[] + host: HostMemory + /** Per-process byte metric used by app, session, worktree, history, and totalMemory values. */ + processMemoryMetric: ProcessMemoryMetric + /** Sum of app + all tracked worktree sessions. Percent of a single core, so may exceed 100 on multi-core machines. */ + totalCpu: number + /** Sum of per-process samples. Shared pages may repeat, so this can exceed host.totalMemory. */ + totalMemory: number + collectedAt: number +} diff --git a/src/shared/project-group-types.ts b/src/shared/project-group-types.ts new file mode 100644 index 00000000000..43bd889a67d --- /dev/null +++ b/src/shared/project-group-types.ts @@ -0,0 +1,60 @@ +export type ProjectGroupCreatedFrom = 'manual' | 'folder-scan' | 'migration' + +export type ProjectGroup = { + id: string + name: string + parentPath: string | null + /** SSH target ID for folder-backed groups imported from a remote root. */ + connectionId?: string | null + /** Renderer-owned host stamp for groups fetched from a runtime environment. */ + executionHostId?: string | null + parentGroupId: string | null + createdFrom: ProjectGroupCreatedFrom + tabOrder: number + isCollapsed: boolean + color: string | null + createdAt: number + updatedAt: number +} + +export type NestedRepoScanOptions = { + maxDepth?: number + maxRepos?: number + timeoutMs?: number | null +} + +export type NestedRepoCandidate = { + path: string + displayName: string + depth: number +} + +export type NestedRepoScanResult = { + selectedPath: string + selectedPathKind: 'git_repo' | 'non_git_folder' + repos: NestedRepoCandidate[] + truncated: boolean + timedOut: boolean + stopped: boolean + durationMs: number + maxDepth: number + maxRepos: number + timeoutMs: number | null +} + +export type ProjectGroupImportMode = 'group' | 'separate' + +export type ProjectGroupImportProjectResult = { + path: string + projectId?: string + status: 'imported' | 'already-known' | 'failed' + error?: string +} + +export type ProjectGroupImportResult = { + group?: ProjectGroup + projects: ProjectGroupImportProjectResult[] + importedCount: number + alreadyKnownCount: number + failedCount: number +} diff --git a/src/shared/project-types.ts b/src/shared/project-types.ts new file mode 100644 index 00000000000..f5704c504ef --- /dev/null +++ b/src/shared/project-types.ts @@ -0,0 +1,142 @@ +import type { ExecutionHostId } from './execution-host' +import type { RepoIcon } from './repo-icon' +import type { GitRemoteIdentity } from './git-remote-identity' +import type { LocalWindowsRuntimePreference } from './project-execution-runtime' +import type { RepoHookSettings } from './orca-yaml-hook-types' +import type { RepoSourceControlAiOverrides } from './source-control-ai-types' +import type { Repo, RepoKind } from './repo-types' + +export type ProjectProviderIdentity = { + provider: 'github' + owner: string + repo: string + host?: string +} + +export type Project = { + id: string + displayName: string + badgeColor: string + repoIcon?: RepoIcon | null + kind?: RepoKind + providerIdentity?: ProjectProviderIdentity + gitRemoteIdentity?: GitRemoteIdentity + /** Local Windows projects inherit the global runtime default unless this override is set. */ + localWindowsRuntimePreference?: LocalWindowsRuntimePreference + sourceRepoIds: string[] + createdAt: number + updatedAt: number +} + +export type ProjectUpdateArgs = { + projectId: string + updates: Partial> +} + +export type ProjectHostSetupState = 'ready' | 'not-set-up' | 'setting-up' | 'error' | 'unsupported' +export type ProjectHostSetupMethod = + | 'legacy-repo' + | 'imported-existing-folder' + | 'cloned' + | 'provisioned' +export type RepoProjectHostSetupMethod = Extract< + ProjectHostSetupMethod, + 'imported-existing-folder' | 'cloned' +> + +export type ProjectHostSetup = { + id: string + projectId: string + hostId: ExecutionHostId + repoId: string + path: string + displayName: string + kind?: RepoKind + connectionId?: string | null + executionHostId?: ExecutionHostId | null + /** Renderer projection of the paired runtime that owns this setup's transport. */ + runtimeOwnerEnvironmentId?: string + worktreeBasePath?: string + hookSettings?: RepoHookSettings + gitUsername?: string + setupState: ProjectHostSetupState + setupMethod: ProjectHostSetupMethod + sourceControlAi?: RepoSourceControlAiOverrides + createdAt: number + updatedAt: number +} + +export type ProjectHostSetupExistingFolderArgs = { + projectId: string + projectProviderIdentity?: ProjectProviderIdentity + hostId: ExecutionHostId + path: string + kind?: RepoKind + displayName?: string + setupMethod?: RepoProjectHostSetupMethod +} + +export type ProjectHostSetupCreateArgs = { + projectId: string + hostId: ExecutionHostId + setupId?: string + path?: string + kind?: RepoKind + displayName?: string + worktreeBasePath?: string + gitUsername?: string + setupState?: ProjectHostSetupState + setupMethod?: Exclude +} + +export type ProjectHostSetupCloneArgs = { + projectId: string + projectProviderIdentity?: ProjectProviderIdentity + hostId: ExecutionHostId + url: string + destination: string + displayName?: string +} + +export type ProjectHostSetupUpdateArgs = { + setupId: string + updates: Partial< + Pick< + ProjectHostSetup, + | 'displayName' + | 'path' + | 'worktreeBasePath' + | 'setupState' + | 'setupMethod' + | 'gitUsername' + | 'kind' + > + > +} + +export type ProjectHostSetupDeleteArgs = { + setupId: string +} + +export type ProjectHostSetupResult = { + project: Project + setup: ProjectHostSetup + repo: Repo +} + +export type ProjectHostSetupCreateResult = { + project: Project + setup: ProjectHostSetup +} + +export type ProjectHostSetupUpdateResult = { + project: Project + setup: ProjectHostSetup + repo?: Repo +} + +export type ProjectHostSetupDeleteResult = { + project: Project + setup: ProjectHostSetup + repo?: Repo +} diff --git a/src/shared/repo-types.ts b/src/shared/repo-types.ts new file mode 100644 index 00000000000..99cdd957ad0 --- /dev/null +++ b/src/shared/repo-types.ts @@ -0,0 +1,131 @@ +import type { RepoIcon } from './repo-icon' +import type { GitHubRepositoryIdentity } from './github-pr-types' +import type { RepoHookSettings } from './orca-yaml-hook-types' +import type { ForkSyncMode } from './git-fork-sync' +import type { GitRemoteIdentity } from './git-remote-identity' +import type { RepoSourceControlAiOverrides } from './source-control-ai-types' +import type { RepoProjectHostSetupMethod } from './project-types' + +// ─── Repo ──────────────────────────────────────────────────────────── +export type RepoKind = 'git' | 'folder' + +/** + * Per-repo user choice for where issues are fetched and filed. + * + * Why three states, not two: storage must distinguish "user explicitly chose + * upstream" from "heuristic happens to resolve to upstream right now." Collapsing + * the two would let a remote-topology change (someone removes `upstream`, or + * adds one later) silently move the effective source — the exact silent-source- + * switch class the upstream-issue-source design rejects. + * + * - `'auto'` (or undefined): honor the heuristic in `getIssueOwnerRepo` + * (upstream-if-exists, else origin). Initial state for every repo. + * - `'upstream'`: explicit upstream. Wins over heuristic and future topology + * changes. Falls back to origin if `upstream` remote vanishes, with a toast. + * - `'origin'`: explicit origin. Same precedence. + */ +export type IssueSourcePreference = 'upstream' | 'origin' | 'auto' +export type ExternalWorktreeVisibility = 'hide' | 'show' + +export type BuiltInWorktreeVisibilitySourceId = 'claude' | 'gsd' + +export type CustomWorktreeVisibilitySource = { + id: string + rootPath: string +} + +export type WorktreeVisibilitySourcePreferences = { + builtIn?: Partial> + custom?: Record +} + +export type Repo = { + id: string + path: string + displayName: string + badgeColor: string + repoIcon?: RepoIcon | null + /** Set when the repo is a fork: the upstream/parent owner/repo. Drives the + * fork indicator and the default avatar of same-name forks (renamed forks + * keep their own owner). Absent = not a fork, or fork status not yet + * resolved. */ + upstream?: GitHubRepositoryIdentity | null + addedAt: number + kind?: RepoKind + gitUsername?: string + worktreeBaseRef?: string + /** Optional repo-scoped workspace root override. Relative paths resolve from `path`. */ + worktreeBasePath?: string + hookSettings?: RepoHookSettings + /** SSH target ID for remote repos. null/undefined = local. */ + connectionId?: string | null + /** + * Explicit execution owner for this repo. Runtime-host repos need this + * because they otherwise look identical to local repos (`connectionId: null`). + */ + executionHostId?: 'local' | `ssh:${string}` | `runtime:${string}` | null + /** Per-repo override for issue-source resolution. `undefined` is treated + * identically to `'auto'`; writers leave it undefined on creation so + * existing persisted records stay forward-compatible. */ + issueSourcePreference?: IssueSourcePreference + /** Controls Orca's fork-default-branch sync offer for repos with upstream metadata. */ + forkSyncMode?: ForkSyncMode + /** Canonical identity for the repo remote Orca should use for provider-level grouping. */ + gitRemoteIdentity?: GitRemoteIdentity | null + /** Controls whether worktrees Orca did not create appear in the sidebar. */ + externalWorktreeVisibility?: ExternalWorktreeVisibility + /** True when the repo predates hidden-by-default external worktrees. */ + externalWorktreeVisibilityLegacy?: boolean + /** One-shot guard for the optional existing-user visibility prompt. */ + externalWorktreeVisibilityPromptDismissedAt?: number + /** Hidden external worktree paths acknowledged by Keep hidden on the inbox. */ + externalWorktreeInboxBaselinePaths?: string[] + /** External worktree paths explicitly imported while global visibility stays hide. */ + importedExternalWorktreePaths?: string[] + /** Opt-in repo policy for coding-agent scratch worktrees; absent means hide. */ + agentWorktreeVisibility?: ExternalWorktreeVisibility + /** User-defined roots classified independently from ordinary external worktrees. */ + customWorktreeVisibilitySources?: CustomWorktreeVisibilitySource[] + /** Per-source visibility; absent built-ins inherit the legacy agent policy. */ + worktreeVisibilitySourcePreferences?: WorktreeVisibilitySourcePreferences + /** User permanently opted out of the new-external-worktree inbox for this repo. */ + externalWorktreeDiscoverySuppressedAt?: number + /** Paths (relative to the primary checkout) that should be APFS clone-copied + * on macOS when possible, otherwise symlinked, into newly created worktrees. + * Undefined/empty means no shared paths are created for this repo. */ + symlinkPaths?: string[] + /** Durable sidebar-only repo organization. Execution remains repo-scoped. */ + projectGroupId?: string | null + /** User-authored ordering inside the project group or ungrouped bucket. */ + projectGroupOrder?: number + /** Repo-specific source-control AI overrides. Missing fields inherit global settings. */ + sourceControlAi?: RepoSourceControlAiOverrides + /** Transitional source for ProjectHostSetup.setupMethod while Repo remains compatibility storage. */ + projectHostSetupMethod?: RepoProjectHostSetupMethod +} + +/** + * Envelope returned by the `repos:getBaseRefDefault` IPC handler. + * + * Why: declared in `shared/` rather than colocated with the handler so the + * preload bridge and renderer can import the same named type. Before this + * lived in `src/main/git/repo.ts` — the preload layer cannot import from + * `src/main/`, which forced three sites to inline the same structural shape + * and risk silent drift. + * + * Why `remoteCount`: BaseRefPicker renders a multi-remote hint when the repo + * has more than one configured remote; piggybacking the count on this IPC + * avoids a second round-trip. + * + * Why `defaultBaseRef` (not `default`): `default` is a reserved word and is + * awkward to destructure. + */ +export type BaseRefDefaultResult = { + defaultBaseRef: string | null + remoteCount: number +} + +export type BaseRefSearchResult = { + refName: string + localBranchName: string +} diff --git a/src/shared/shell-path-hydration-types.ts b/src/shared/shell-path-hydration-types.ts new file mode 100644 index 00000000000..bae5efad41f --- /dev/null +++ b/src/shared/shell-path-hydration-types.ts @@ -0,0 +1,14 @@ +// ─── Shell PATH hydration ──────────────────────────────────────────── +// Why: shared so the main-side `HydrationResult` discriminator and the +// telemetry schema in `telemetry-events.ts` stay in lockstep without +// `src/shared/` taking a forbidden import from `src/main/`. A compile-time +// guard in telemetry-events.ts asserts the schema enum matches this alias — +// adding a new failure mode without updating both places fails the build. +export type ShellHydrationFailureReason = + | 'none' + | 'no_shell' + | 'timeout' + | 'spawn_error' + | 'empty_path' + +export type PathSource = 'shell_hydrate' | 'sync_seed_only' diff --git a/src/shared/tab-types.ts b/src/shared/tab-types.ts new file mode 100644 index 00000000000..e321aaaee6f --- /dev/null +++ b/src/shared/tab-types.ts @@ -0,0 +1,66 @@ +import type { AiVaultSessionTitle } from './ai-vault-session-title' + +// ─── Tab Group Layout ─────────────────────────────────────────────── +export type TabGroupSplitDirection = 'horizontal' | 'vertical' + +export type TabGroupLayoutNode = + | { type: 'leaf'; groupId: string } + | { + type: 'split' + direction: TabGroupSplitDirection + first: TabGroupLayoutNode + second: TabGroupLayoutNode + /** Flex ratio of the first child (0–1). Defaults to 0.5 if absent. */ + ratio?: number + } + +// ─── Unified Tab ──────────────────────────────────────────────────── +export type TabContentType = + | 'terminal' + | 'editor' + | 'diff' + | 'conflict-review' + | 'check-details' + | 'browser' + | 'simulator' + +export type WorkspaceVisibleTabType = 'terminal' | 'editor' | 'browser' | 'simulator' +export type CtrlTabOrderMode = 'mru' | 'sequential' + +export type Tab = { + id: string // UUID for terminals, filePath for editors (preserves current convention) + entityId: string // ID of the backing content (terminal tab ID, file path, browser workspace ID) + groupId: string + worktreeId: string + contentType: TabContentType + label: string // display title (auto-derived from PTY or filename) + generatedLabel?: string | null + /** Stable AI Vault conversation name, bound to its provider session identity. */ + aiVaultTitle?: AiVaultSessionTitle | null + quickCommandLabel?: string | null + customLabel: string | null + color: string | null + sortOrder: number + createdAt: number + isPreview?: boolean // preview tabs get replaced by next single-click open + isPinned?: boolean // pinned tabs survive "close others" + /** Why: per-tab rendering mode for coding-agent terminals. `'chat'` shows the + * native chat view as an overlay while the live terminal stays mounted + * underneath; `'terminal'` (the default for legacy/missing) shows the raw + * xterm. Optional so sessions persisted before this field hydrate cleanly. */ + viewMode?: 'terminal' | 'chat' +} + +export type TabGroup = { + id: string + worktreeId: string + activeTabId: string | null + tabOrder: string[] // canonical visual order of tab IDs + /** Per-group MRU stack (oldest → most-recent at the tail). Drives which tab + * becomes active when the current active tab closes: we pop back to the + * previously-active tab instead of jumping to a visual neighbor. Scoped to + * the group so split panes keep independent histories. Optional because + * sessions persisted before this field was added still hydrate cleanly — + * hydration seeds from activeTabId. */ + recentTabIds?: string[] +} diff --git a/src/shared/terminal-color-overrides.ts b/src/shared/terminal-color-overrides.ts new file mode 100644 index 00000000000..073ad9de2f8 --- /dev/null +++ b/src/shared/terminal-color-overrides.ts @@ -0,0 +1,28 @@ +export type TerminalColorOverrides = { + foreground?: string + background?: string + cursor?: string + cursorAccent?: string + selectionBackground?: string + selectionForeground?: string + black?: string + red?: string + green?: string + yellow?: string + blue?: string + magenta?: string + cyan?: string + white?: string + brightBlack?: string + brightRed?: string + brightGreen?: string + brightYellow?: string + brightBlue?: string + brightMagenta?: string + brightCyan?: string + brightWhite?: string + // Why: xterm.js ITheme does not expose a `bold` key, but Ghostty users + // expect the setting to be preserved so a future renderer CSS override + // or xterm upgrade can honour it without a migration. + bold?: string +} diff --git a/src/shared/terminal-quick-command-types.ts b/src/shared/terminal-quick-command-types.ts new file mode 100644 index 00000000000..a887f47b84d --- /dev/null +++ b/src/shared/terminal-quick-command-types.ts @@ -0,0 +1,32 @@ +import type { TuiAgent } from './tui-agent' + +export type TerminalQuickCommandScope = + | { + type: 'global' + } + | { + type: 'repo' + repoId: string + } + +export type TerminalQuickCommandAction = 'terminal-command' | 'agent-prompt' + +export type TerminalQuickCommandBase = { + id: string + label: string + scope?: TerminalQuickCommandScope +} + +export type TerminalCommandQuickCommand = TerminalQuickCommandBase & { + action?: 'terminal-command' + command: string + appendEnter: boolean +} + +export type TerminalAgentQuickCommand = TerminalQuickCommandBase & { + action: 'agent-prompt' + agent: TuiAgent + prompt: string +} + +export type TerminalQuickCommand = TerminalCommandQuickCommand | TerminalAgentQuickCommand diff --git a/src/shared/terminal-tab-types.ts b/src/shared/terminal-tab-types.ts new file mode 100644 index 00000000000..1c455333ea9 --- /dev/null +++ b/src/shared/terminal-tab-types.ts @@ -0,0 +1,88 @@ +import type { AiVaultSessionTitle } from './ai-vault-session-title' +import type { TuiAgent } from './tui-agent' + +// ─── Terminal Tab (legacy — used by persistence and TerminalContentSlice) ─ +export type TerminalTab = { + id: string + ptyId: string | null + worktreeId: string + title: string + /** Stable fallback label for default-named terminals ("Terminal 1", etc.). + * Why: agent CLIs overwrite the live title via OSC updates, but Orca still + * needs the original terminal label for numbering and reset behavior. */ + defaultTitle?: string + /** Stable opt-in label derived from the first known agent prompt. */ + generatedTitle?: string | null + /** Stable AI Vault conversation name, bound to its provider session identity. */ + aiVaultTitle?: AiVaultSessionTitle | null + /** Stable label from the tab-bar Quick Command that created this terminal. */ + quickCommandLabel?: string | null + customTitle: string | null + color: string | null + /** Pinned tabs survive "close others"; host-persisted for remote servers. */ + isPinned?: boolean + /** Per-tab view preference (terminal xterm vs native chat); host-persisted so + * paired clients converge. Optional: older persisted tabs default to 'terminal'. */ + viewMode?: 'terminal' | 'chat' + sortOrder: number + createdAt: number + /** Bumped on shutdown so TerminalPane remounts with a fresh PTY. */ + generation?: number + /** Why: records the shell this tab was opened with (e.g. 'wsl.exe') so the + * PTY and tab icon stay stable even if the default shell setting changes + * later. Older persisted tabs may omit this field. */ + shellOverride?: string + /** Keeps an ephemeral host fallback out of the active project's runtime. */ + forceHostRuntime?: boolean + /** Why: explorer-created terminals can start below the workspace root while + * still belonging to that workspace for tab/session ownership. */ + startupCwd?: string + /** Why: the coding-harness agent Orca launched in this tab. Lets the tab bar + * show the provider icon immediately, before the agent emits its first hook + * event (a freshly-launched, idle agent reports no live status yet). Live + * hook status overrides this once the agent does anything. Plain terminals + * and manually-started agents omit it. */ + launchAgent?: TuiAgent + /** Why: when `setActiveWorktree` bumps generation on all-dead tabs to drive a + * TerminalPane remount, the fresh PTY that results is caused by navigation, + * not by the user doing work. Without this flag the resulting + * `updateTabPtyId` call would call `bumpWorktreeActivity` and flip the + * sidebar's recency sort on every click — the reorder-on-click bug. The + * flag is set by `setActiveWorktree` and consumed by the activation-driven + * PTY lifecycle calls that follow, which then suppress activity bumps and + * `sortEpoch` increments. Split layouts use a numeric count because one tab + * can remount several panes. Never persisted — it is a transient handoff. */ + pendingActivationSpawn?: boolean | number +} + +export type TerminalPaneSplitDirection = 'vertical' | 'horizontal' + +export type TerminalPaneLayoutNode = + | { + type: 'leaf' + leafId: string + } + | { + type: 'split' + direction: TerminalPaneSplitDirection + first: TerminalPaneLayoutNode + second: TerminalPaneLayoutNode + /** Flex ratio of the first child (0–1). Defaults to 0.5 if absent. */ + ratio?: number + } + +export type TerminalLayoutSnapshot = { + root: TerminalPaneLayoutNode | null + activeLeafId: string | null + expandedLeafId: string | null + /** Live PTY IDs per leaf for in-session remounts such as tab-group moves. + * Not used for app restart because PTYs are transient processes. */ + ptyIdsByLeafId?: Record + /** Serialized terminal buffers per leaf for scrollback restoration on restart. */ + buffersByLeafId?: Record + /** Durable scrollback snapshot refs per leaf; raw bytes live outside session JSON. */ + scrollbackRefsByLeafId?: Record + /** User-assigned pane titles, keyed by stable layout leaf UUID. + * Persisted alongside buffers via the existing session:set flow. */ + titlesByLeafId?: Record +} diff --git a/src/shared/tui-agent.ts b/src/shared/tui-agent.ts new file mode 100644 index 00000000000..a885e3dd24c --- /dev/null +++ b/src/shared/tui-agent.ts @@ -0,0 +1,39 @@ +/** All AI coding agents Orca knows how to launch. Used for the agent picker in the new-workspace + * flow and for the default-agent setting. Extend this union as new agents are added. */ +export type TuiAgent = + | 'claude' // Claude Code + | 'claude-agent-teams' // Claude Code Agent Teams via Orca native panes + | 'openclaude' // OpenClaude + | 'codex' // OpenAI Codex + | 'autohand' // Autohand Code CLI + | 'opencode' // OpenCode + | 'mimo-code' + | 'pi' // Pi (pi.dev) + | 'omp' // OMP (omp.sh) + | 'gemini' // Gemini CLI + | 'antigravity' // Google Antigravity CLI + | 'aider' // Aider + | 'goose' // Goose + | 'amp' // Amp + | 'kilo' // Kilocode + | 'kiro' // Kiro + | 'crush' // Charm/Crush + | 'aug' // Augment/Auggie + | 'cline' // Cline + | 'codebuff' // Codebuff + | 'command-code' // Command Code + | 'continue' // Continue + | 'cursor' // Cursor + | 'droid' // Factory Droid + | 'kimi' // Kimi + | 'mistral-vibe' // Mistral Vibe + | 'qwen-code' // Qwen Code + | 'rovo' // Rovo Dev + | 'hermes' // Hermes Agent + | 'openclaw' // OpenClaw + | 'copilot' // GitHub Copilot CLI + | 'grok' // xAI Grok CLI + | 'devin' // Devin CLI + | 'ante' // Ante (Antigma Labs) + | 'trae' // Trae CLI + | 'prime-agent' // Prime Agent (Prime Intellect) diff --git a/src/shared/types.ts b/src/shared/types.ts index b2ebefa57a5..c3ef81392d2 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -1,64 +1,111 @@ -/* eslint-disable max-lines */ -import type { ExecutionHostId } from './execution-host' -import type { - RemovedSshTargetTombstone, - SshPtyConsumerRecovery, - SshRemotePtyLease, - SshTarget -} from './ssh-types' -import type { Automation, AutomationExecutionTargetType, AutomationRun } from './automations-types' -import type { WorkspaceSource } from './workspace-source' -import type { DedicatedRepoChannel, ReleaseBuild, ReleaseChannel } from './release-channel' -import type { GitHubProjectSettings } from './github-project-types' -import type { - AgentStatusState, - AgentType, - MigrationUnsupportedPtyEntry -} from './agent-status-types' -import type { VoiceSettings } from './speech-types' -import type { WorkspaceCleanupUIState } from './workspace-cleanup' -import type { LargeDiffRenderLimit } from './large-diff-render-limit' -import type { GitLabProjectSettings } from './gitlab-types' -import type { TaskProvider } from './task-providers' -import type { FeatureTipId } from './feature-tips' -import type { ContextualTourId } from './contextual-tours' -import type { - FeatureInteractionState, - FeatureInteractionTelemetryBucketState -} from './feature-interactions' -import type { GitBranchChangeStatus } from './git-status-types' -import type { KeybindingOverrides, TerminalShortcutPolicy } from './keybindings' -import type { RepoIcon } from './repo-icon' -import type { AppIconId } from './app-icon' -import type { - RepoSourceControlAiOverrides, - SourceControlAiSettings -} from './source-control-ai-types' -import type { StartupCommandDelivery } from './codex-startup-delivery' -import type { AgentKind, LaunchSource, RequestKind } from './telemetry-events' -import type { SleepingAgentLaunchConfig, SleepingAgentSessionRecord } from './agent-session-resume' -import type { ClaudeAgentTeamsMode } from './claude-agent-teams-tmux-compat' -import type { TerminalCustomTheme } from './terminal-custom-themes' -import type { UiLanguage } from './ui-language' -import type { ForkSyncMode } from './git-fork-sync' -import type { GitRemoteIdentity } from './git-remote-identity' -import type { - GlobalWindowsRuntimeDefault, - LocalWindowsRuntimePreference -} from './project-execution-runtime' -import type { UsagePercentageDisplay } from './usage-percentage-display' -import type { StatusBarUsageMode } from './status-bar-usage-mode' -import type { PersistedNativeChatSessionOptions } from './native-chat-session-options' -import type { CodexResetCreditAttemptLedger } from './codex-reset-credit-attempt-ledger' -import type { TaskSourceContext } from './task-source-context' -import type { SetupRunnerShell } from './setup-runner-command' -import type { AiVaultSessionTitle } from './ai-vault-session-title' -import type { ComputerAwakeMode } from './computer-awake-mode' +/** + * 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' -export type { TaskProvider } from './task-providers' + +// 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, @@ -74,1963 +121,47 @@ export type { GitUpstreamStatus } from './git-status-types' -// ─── Shell PATH hydration ──────────────────────────────────────────── -// Why: shared so the main-side `HydrationResult` discriminator and the -// telemetry schema in `telemetry-events.ts` stay in lockstep without -// `src/shared/` taking a forbidden import from `src/main/`. A compile-time -// guard in telemetry-events.ts asserts the schema enum matches this alias — -// adding a new failure mode without updating both places fails the build. -export type ShellHydrationFailureReason = - | 'none' - | 'no_shell' - | 'timeout' - | 'spawn_error' - | 'empty_path' +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 PathSource = 'shell_hydrate' | 'sync_seed_only' +export type { DiffCommentSource, DiffReviewScope } from './diff-comment-types' +export type { MobileDiffReviewFileState, MobileDiffReviewState } from './diff-comment-types' +export type { DiffComment } from './diff-comment-types' -// ─── Repo ──────────────────────────────────────────────────────────── -export type RepoKind = 'git' | 'folder' +// GitHub -/** - * Per-repo user choice for where issues are fetched and filed. - * - * Why three states, not two: storage must distinguish "user explicitly chose - * upstream" from "heuristic happens to resolve to upstream right now." Collapsing - * the two would let a remote-topology change (someone removes `upstream`, or - * adds one later) silently move the effective source — the exact silent-source- - * switch class the upstream-issue-source design rejects. - * - * - `'auto'` (or undefined): honor the heuristic in `getIssueOwnerRepo` - * (upstream-if-exists, else origin). Initial state for every repo. - * - `'upstream'`: explicit upstream. Wins over heuristic and future topology - * changes. Falls back to origin if `upstream` remote vanishes, with a toast. - * - `'origin'`: explicit origin. Same precedence. - */ -export type IssueSourcePreference = 'upstream' | 'origin' | 'auto' -export type { ForkSyncMode, GitForkSyncExpectedUpstream, GitForkSyncResult } from './git-fork-sync' -export type ExternalWorktreeVisibility = 'hide' | 'show' +export type { PRState, IssueState, CheckStatus, PRMergeableState } from './github-pr-types' +export type { PRReviewDecision, PRConflictSummary } from './github-pr-types' +export type { GitHubRepositoryIdentity, GitHubOwnerRepo } from './github-pr-types' +export type { GitHubPRMergeMethod, GitHubPRMergeMethodSettings } from './github-pr-types' +export type { GitHubPRStackEntry, GitHubPRStack, PRInfo, IssueInfo } from './github-pr-types' +export type { GitHubViewer, GitHubAssignableUser, ProviderCheckSummary } from './github-pr-types' +export type { GitHubPRReviewSummary, GitHubPRFileViewedState } from './github-pr-types' +export type { GitHubPRFile, GitHubPRFileContents } from './github-pr-types' -export type BuiltInWorktreeVisibilitySourceId = 'claude' | 'gsd' +export type { PRRefreshErrorType, PRRefreshUpstreamErrorType } from './github-pr-refresh-types' +export type { PRRefreshOutcome, GitHubPRRefreshReason } from './github-pr-refresh-types' +export type { GitHubPRRefreshEnqueueResult, GitHubPRRefreshAlias } from './github-pr-refresh-types' +export type { GitHubPRRefreshCandidate } from './github-pr-refresh-types' +export type { GitHubPRRefreshSkippedReason, GitHubPRRefreshEvent } from './github-pr-refresh-types' -export type CustomWorktreeVisibilitySource = { - id: string - rootPath: string -} +export type { PRCheckDetail, PRCheckAnnotation, PRCheckStep } from './github-check-types' +export type { PRCheckJob, PRCheckRunDetails, GitHubRerunPRChecksResult } from './github-check-types' -export type WorktreeVisibilitySourcePreferences = { - builtIn?: Partial> - custom?: Record -} +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 ProjectProviderIdentity = { - provider: 'github' - owner: string - repo: string - host?: string -} +export type { GitHubWorkItem, GitHubWorkItemDetails } from './github-work-item-types' +export type { ListWorkItemsResult } from './github-work-item-types' -export type Project = { - id: string - displayName: string - badgeColor: string - repoIcon?: RepoIcon | null - kind?: RepoKind - providerIdentity?: ProjectProviderIdentity - gitRemoteIdentity?: GitRemoteIdentity - /** Local Windows projects inherit the global runtime default unless this override is set. */ - localWindowsRuntimePreference?: LocalWindowsRuntimePreference - sourceRepoIds: string[] - createdAt: number - updatedAt: number -} +export type { GitHubRateLimitBucket, GitHubRateLimitSnapshot } from './github-rate-limit-types' +export type { GetRateLimitResult } from './github-rate-limit-types' -export type ProjectUpdateArgs = { - projectId: string - updates: Partial> -} +// GitLab -export type ProjectHostSetupState = 'ready' | 'not-set-up' | 'setting-up' | 'error' | 'unsupported' -export type ProjectHostSetupMethod = - | 'legacy-repo' - | 'imported-existing-folder' - | 'cloned' - | 'provisioned' -export type RepoProjectHostSetupMethod = Extract< - ProjectHostSetupMethod, - 'imported-existing-folder' | 'cloned' -> - -export type ProjectHostSetup = { - id: string - projectId: string - hostId: ExecutionHostId - repoId: string - path: string - displayName: string - kind?: RepoKind - connectionId?: string | null - executionHostId?: ExecutionHostId | null - /** Renderer projection of the paired runtime that owns this setup's transport. */ - runtimeOwnerEnvironmentId?: string - worktreeBasePath?: string - hookSettings?: RepoHookSettings - gitUsername?: string - setupState: ProjectHostSetupState - setupMethod: ProjectHostSetupMethod - sourceControlAi?: RepoSourceControlAiOverrides - createdAt: number - updatedAt: number -} - -export type ProjectHostSetupExistingFolderArgs = { - projectId: string - projectProviderIdentity?: ProjectProviderIdentity - hostId: ExecutionHostId - path: string - kind?: RepoKind - displayName?: string - setupMethod?: RepoProjectHostSetupMethod -} - -export type ProjectHostSetupCreateArgs = { - projectId: string - hostId: ExecutionHostId - setupId?: string - path?: string - kind?: RepoKind - displayName?: string - worktreeBasePath?: string - gitUsername?: string - setupState?: ProjectHostSetupState - setupMethod?: Exclude -} - -export type ProjectHostSetupCloneArgs = { - projectId: string - projectProviderIdentity?: ProjectProviderIdentity - hostId: ExecutionHostId - url: string - destination: string - displayName?: string -} - -export type ProjectHostSetupUpdateArgs = { - setupId: string - updates: Partial< - Pick< - ProjectHostSetup, - | 'displayName' - | 'path' - | 'worktreeBasePath' - | 'setupState' - | 'setupMethod' - | 'gitUsername' - | 'kind' - > - > -} - -export type ProjectHostSetupDeleteArgs = { - setupId: string -} - -export type ProjectHostSetupResult = { - project: Project - setup: ProjectHostSetup - repo: Repo -} - -export type ProjectHostSetupCreateResult = { - project: Project - setup: ProjectHostSetup -} - -export type ProjectHostSetupUpdateResult = { - project: Project - setup: ProjectHostSetup - repo?: Repo -} - -export type ProjectHostSetupDeleteResult = { - project: Project - setup: ProjectHostSetup - repo?: Repo -} - -export type Repo = { - id: string - path: string - displayName: string - badgeColor: string - repoIcon?: RepoIcon | null - /** Set when the repo is a fork: the upstream/parent owner/repo. Drives the - * fork indicator and the default avatar of same-name forks (renamed forks - * keep their own owner). Absent = not a fork, or fork status not yet - * resolved. */ - upstream?: GitHubRepositoryIdentity | null - addedAt: number - kind?: RepoKind - gitUsername?: string - worktreeBaseRef?: string - /** Optional repo-scoped workspace root override. Relative paths resolve from `path`. */ - worktreeBasePath?: string - hookSettings?: RepoHookSettings - /** SSH target ID for remote repos. null/undefined = local. */ - connectionId?: string | null - /** - * Explicit execution owner for this repo. Runtime-host repos need this - * because they otherwise look identical to local repos (`connectionId: null`). - */ - executionHostId?: 'local' | `ssh:${string}` | `runtime:${string}` | null - /** Per-repo override for issue-source resolution. `undefined` is treated - * identically to `'auto'`; writers leave it undefined on creation so - * existing persisted records stay forward-compatible. */ - issueSourcePreference?: IssueSourcePreference - /** Controls Orca's fork-default-branch sync offer for repos with upstream metadata. */ - forkSyncMode?: ForkSyncMode - /** Canonical identity for the repo remote Orca should use for provider-level grouping. */ - gitRemoteIdentity?: GitRemoteIdentity | null - /** Controls whether worktrees Orca did not create appear in the sidebar. */ - externalWorktreeVisibility?: ExternalWorktreeVisibility - /** True when the repo predates hidden-by-default external worktrees. */ - externalWorktreeVisibilityLegacy?: boolean - /** One-shot guard for the optional existing-user visibility prompt. */ - externalWorktreeVisibilityPromptDismissedAt?: number - /** Hidden external worktree paths acknowledged by Keep hidden on the inbox. */ - externalWorktreeInboxBaselinePaths?: string[] - /** External worktree paths explicitly imported while global visibility stays hide. */ - importedExternalWorktreePaths?: string[] - /** Opt-in repo policy for coding-agent scratch worktrees; absent means hide. */ - agentWorktreeVisibility?: ExternalWorktreeVisibility - /** User-defined roots classified independently from ordinary external worktrees. */ - customWorktreeVisibilitySources?: CustomWorktreeVisibilitySource[] - /** Per-source visibility; absent built-ins inherit the legacy agent policy. */ - worktreeVisibilitySourcePreferences?: WorktreeVisibilitySourcePreferences - /** User permanently opted out of the new-external-worktree inbox for this repo. */ - externalWorktreeDiscoverySuppressedAt?: number - /** Paths (relative to the primary checkout) that should be APFS clone-copied - * on macOS when possible, otherwise symlinked, into newly created worktrees. - * Undefined/empty means no shared paths are created for this repo. */ - symlinkPaths?: string[] - /** Durable sidebar-only repo organization. Execution remains repo-scoped. */ - projectGroupId?: string | null - /** User-authored ordering inside the project group or ungrouped bucket. */ - projectGroupOrder?: number - /** Repo-specific source-control AI overrides. Missing fields inherit global settings. */ - sourceControlAi?: RepoSourceControlAiOverrides - /** Transitional source for ProjectHostSetup.setupMethod while Repo remains compatibility storage. */ - projectHostSetupMethod?: RepoProjectHostSetupMethod -} - -export type ProjectGroupCreatedFrom = 'manual' | 'folder-scan' | 'migration' - -export type ProjectGroup = { - id: string - name: string - parentPath: string | null - /** SSH target ID for folder-backed groups imported from a remote root. */ - connectionId?: string | null - /** Renderer-owned host stamp for groups fetched from a runtime environment. */ - executionHostId?: string | null - parentGroupId: string | null - createdFrom: ProjectGroupCreatedFrom - tabOrder: number - isCollapsed: boolean - color: string | null - createdAt: number - updatedAt: number -} - -export type WorkspaceScope = - | { type: 'worktree'; worktreeId: string } - | { type: 'folder'; folderWorkspaceId: string } - -export type WorkspaceKey = `worktree:${string}` | `folder:${string}` - -export type FolderWorkspace = { - id: string - projectGroupId: string - name: string - folderPath: string - /** SSH target ID for folder workspaces whose folder path lives remotely. */ - connectionId?: string | null - /** Renderer-owned host stamp for host-qualified folder catalogs. */ - executionHostId?: ExecutionHostId | null - /** Authenticated client that created this workspace. Missing means unknown legacy origin. */ - creatorProvenance?: WorkspaceCreatorProvenance - linkedTask: WorkspaceLinkedItem | null - linkedTaskSourceContext?: TaskSourceContext | null - comment: string - isArchived: boolean - isUnread: boolean - isPinned: boolean - sortOrder: number - /** User-authored sidebar ordering. Higher values render earlier in Manual sort. */ - manualOrder?: number - workspaceStatus?: WorkspaceStatus - createdWithAgent?: TuiAgent - pendingFirstAgentMessageRename?: boolean - firstAgentMessageRenameError?: string | null - lastActivityAt: number - createdAt: number - updatedAt: number - diffComments?: DiffComment[] -} - -export type WorkspaceLinkedItem = { - provider: 'github' | 'gitlab' | 'linear' | 'jira' - type: 'issue' | 'pr' | 'mr' - number: number - title: string - url: string - linearIdentifier?: string - jiraIdentifier?: string - repoId?: string -} - -export type FolderWorkspaceLinkedTask = WorkspaceLinkedItem - -export type NestedRepoScanOptions = { - maxDepth?: number - maxRepos?: number - timeoutMs?: number | null -} - -export type NestedRepoCandidate = { - path: string - displayName: string - depth: number -} - -export type NestedRepoScanResult = { - selectedPath: string - selectedPathKind: 'git_repo' | 'non_git_folder' - repos: NestedRepoCandidate[] - truncated: boolean - timedOut: boolean - stopped: boolean - durationMs: number - maxDepth: number - maxRepos: number - timeoutMs: number | null -} - -export type ProjectGroupImportMode = 'group' | 'separate' - -export type ProjectGroupImportProjectResult = { - path: string - projectId?: string - status: 'imported' | 'already-known' | 'failed' - error?: string -} - -export type ProjectGroupImportResult = { - group?: ProjectGroup - projects: ProjectGroupImportProjectResult[] - importedCount: number - alreadyKnownCount: number - failedCount: number -} - -export type SetupRunPolicy = 'ask' | 'run-by-default' | 'skip-by-default' -export type SetupAgentStartupPolicy = 'start-immediately' | 'wait-for-setup' -export type SetupDecision = 'inherit' | 'run' | 'skip' -export type HookCommandSourcePolicy = 'shared-only' | 'local-only' | 'run-both' - -/** - * Envelope returned by the `repos:getBaseRefDefault` IPC handler. - * - * Why: declared in `shared/` rather than colocated with the handler so the - * preload bridge and renderer can import the same named type. Before this - * lived in `src/main/git/repo.ts` — the preload layer cannot import from - * `src/main/`, which forced three sites to inline the same structural shape - * and risk silent drift. - * - * Why `remoteCount`: BaseRefPicker renders a multi-remote hint when the repo - * has more than one configured remote; piggybacking the count on this IPC - * avoids a second round-trip. - * - * Why `defaultBaseRef` (not `default`): `default` is a reserved word and is - * awkward to destructure. - */ -export type BaseRefDefaultResult = { - defaultBaseRef: string | null - remoteCount: number -} - -export type BaseRefSearchResult = { - refName: string - localBranchName: string -} - -// ─── Worktree (git-level) ──────────────────────────────────────────── -export type GitWorktreeInfo = { - path: string - head: string - branch: string - isBare: boolean - isSparse?: boolean - locked?: boolean - lockReason?: string - /** True when Git reports the worktree as prunable (its directory is gone but - * the registration remains). Detected via the `prunable` porcelain field - * (Git ≥ 2.36) or a path-existence probe on older Git. */ - prunable?: boolean - prunableReason?: string - /** True for the repo's main working tree (the first entry from `git worktree list`). - * Linked worktrees created via `git worktree add` have this set to false. */ - isMainWorktree: boolean -} - -/** Head/branch snapshot read from Git metadata files without spawning Git. - * Carries background-worktree freshness when status-only churn includes a - * real head move (external commit/amend/reset) that must not re-enter the - * structural `worktrees:changed` fanout. */ -export type WorktreeHeadIdentity = { - worktreePath: string - head: string - /** Full ref (e.g. `refs/heads/main`), or null for a detached HEAD. */ - branch: string | null -} - -// ─── Worktree (app-level, enriched) ────────────────────────────────── -export type WorkspaceStatus = string - -export type WorkspaceStatusDefinition = { - id: WorkspaceStatus - label: string - color?: string - icon?: string -} - -export type Worktree = { - id: string // `${repoId}::${path}` - instanceId?: string - repoId: string - /** Durable project identity. Optional while legacy repo-only workspaces migrate. */ - projectId?: string - /** Execution host that owns the workspace. Optional for pre-project-host metadata. */ - hostId?: ExecutionHostId - /** Renderer projection of the paired runtime that transports operations to `hostId`. */ - runtimeOwnerEnvironmentId?: string - /** Authenticated client that created this workspace. Missing means unknown legacy origin. */ - creatorProvenance?: WorkspaceCreatorProvenance - /** Host-specific setup used to create/run this workspace. */ - projectHostSetupId?: string - /** Checkout ownership for a recipe-provisioned main workspace. */ - ephemeralVmCheckoutMode?: EphemeralVmCheckoutMode - displayName: string - comment: string - linkedIssue: number | null - linkedPR: number | null - linkedLinearIssue: string | null - linkedLinearIssueWorkspaceId?: string | null - linkedLinearIssueOrganizationUrlKey?: string | null - // Why: parallel slots for non-GitHub work-item references. Kept as separate - // fields (rather than reusing linkedIssue / linkedPR with a provider - // discriminator) so the persistence layer is unambiguous when a user - // has remotes from several providers on the same repo, and so the - // existing GitHub renderer code keeps reading linkedPR / linkedIssue - // unchanged. Optional on the type so existing test fixtures and - // persisted older worktrees that never carried these fields continue - // to typecheck and load without migration. - linkedGitLabMR?: number | null - linkedGitLabIssue?: number | null - linkedBitbucketPR?: number | null - linkedAzureDevOpsPR?: number | null - linkedGiteaPR?: number | null - linkedWorkItem?: WorkspaceLinkedItem | null - linkedTaskSourceContext?: TaskSourceContext | null - isArchived: boolean - isUnread: boolean - isPinned: boolean - sortOrder: number - /** User-authored sidebar ordering. Higher values render earlier in Manual sort. */ - manualOrder?: number - lastActivityAt: number - /** Set once when Orca creates the worktree. Absent for worktrees discovered - * on disk or persisted before this field existed. Used by the sidebar to - * grant newly-created worktrees a short grace window at the top of Recent, - * immune to ambient PTY-bump reordering in other worktrees. */ - createdAt?: number - /** Agent selected when Orca originally created the worktree. Used only to - * seed a replacement terminal if the user later reopens the worktree after - * closing every visible surface. */ - createdWithAgent?: TuiAgent - /** True while an auto-named workspace is waiting for the first agent message - * to drive the branch/title rename. */ - pendingFirstAgentMessageRename?: boolean - /** Holds the last auto-rename generation failure message so the sidebar can - * show a "rename failed" badge. null/undefined when there is no failure - * (never attempted, succeeded, or only a benign skip). */ - firstAgentMessageRenameError?: string | null - sparseDirectories?: string[] - sparseBaseRef?: string - /** ID of the saved preset this worktree was created from, if any. Cleared - * when the worktree is no longer sparse on refresh. */ - sparsePresetId?: string - /** Intended create base for stale-base probes. Persisted metadata, not UI drift state. */ - baseRef?: string - /** Remote/branch Orca should publish review commits to when it created this worktree. */ - pushTarget?: GitPushTarget - /** Path-derived worktree ids this worktree had before folder renames. */ - priorWorktreeIds?: string[] - workspaceStatus?: WorkspaceStatus - diffComments?: DiffComment[] - mobileDiffReview?: MobileDiffReviewState - automationProvenance?: AutomationWorkspaceProvenance - cliProvenance?: CliWorkspaceProvenance -} & GitWorktreeInfo - -/** Provenance for workspaces created through `orca worktree create`. Absent on - * workspaces created before this field existed and on every non-CLI create, so - * consumers must read "missing" as "not CLI-created". */ -export type CliWorkspaceProvenance = { - kind: 'created-by-cli' - createdAt: number - /** Orca terminal the CLI ran inside, when the caller had one — distinguishes - * an agent-issued create from one hand-typed in an external shell. */ - callerTerminalHandle?: string - /** Agent requested via `--agent`, when one was passed. */ - startupAgent?: TuiAgent -} - -export type WorkspaceCreatorProvenance = - | { kind: 'host' } - | { kind: 'paired-device'; deviceId: string } - -export type AutomationWorkspaceProvenance = { - kind: 'created-by-automation' - automationId: string - automationNameSnapshot: string - automationRunId: string - automationRunTitleSnapshot: string - createdAt: number - executionTargetType: AutomationExecutionTargetType - executionTargetId: string - projectId: string - repoId?: string - hostId?: ExecutionHostId -} - -export type AutomationWorkspaceProvenanceRequest = { - automationId: string - automationRunId: string - dispatchToken: string - createRequestId: string -} - -export type GitPushTarget = { - remoteName: string - branchName: string - remoteUrl?: string - /** True when Orca added this remote while preparing a fork-PR worktree. */ - remoteCreated?: boolean -} - -export type GitHubPrStartPoint = { - baseBranch: string - /** Review target branch to use for Source Control compare after creating from a PR head SHA. */ - compareBaseRef?: string - pushTarget?: GitPushTarget - /** Verified PR head commit. Present when checkout can be tied to a stable SHA. */ - headSha?: string - /** Exact local branch name to create/reuse when the PR head is a safe same-repo branch. */ - branchNameOverride?: string - /** Fork PRs: false when "Allow edits from maintainers" is off; a push to the fork may be rejected. */ - maintainerCanModify?: boolean -} - -// ─── Worktree metadata (persisted user-authored fields only) ───────── -export type WorktreeMeta = { - /** Immutable per-workspace-instance ID used to reject stale lineage after path reuse. */ - instanceId?: string - /** See Worktree.projectId. Persisted for project-first workspace ownership. */ - projectId?: string - /** See Worktree.hostId. Persisted for project-first workspace ownership. */ - hostId?: ExecutionHostId - /** See Worktree.projectHostSetupId. Persisted for project-first workspace ownership. */ - projectHostSetupId?: string - /** See Worktree.ephemeralVmCheckoutMode. */ - ephemeralVmCheckoutMode?: EphemeralVmCheckoutMode - /** See Worktree.creatorProvenance. */ - creatorProvenance?: WorkspaceCreatorProvenance - displayName: string - comment: string - linkedIssue: number | null - linkedPR: number | null - linkedLinearIssue: string | null - linkedLinearIssueWorkspaceId?: string | null - linkedLinearIssueOrganizationUrlKey?: string | null - /** Optional for backward compatibility — see Worktree.linkedGitLabMR. */ - linkedGitLabMR?: number | null - /** Optional for backward compatibility — see Worktree.linkedGitLabIssue. */ - linkedGitLabIssue?: number | null - /** Optional for backward compatibility — see Worktree.linkedBitbucketPR. */ - linkedBitbucketPR?: number | null - /** Optional for backward compatibility — see Worktree.linkedAzureDevOpsPR. */ - linkedAzureDevOpsPR?: number | null - /** Optional for backward compatibility — see Worktree.linkedGiteaPR. */ - linkedGiteaPR?: number | null - linkedWorkItem?: WorkspaceLinkedItem | null - linkedTaskSourceContext?: TaskSourceContext | null - isArchived: boolean - isUnread: boolean - isPinned: boolean - sortOrder: number - /** User-authored sidebar ordering. Higher values render earlier in Manual sort. */ - manualOrder?: number - lastActivityAt: number - /** See {@link Worktree.createdAt}. Persisted to orca-data.json. */ - createdAt?: number - /** See {@link Worktree.createdWithAgent}. Persisted to orca-data.json. */ - createdWithAgent?: TuiAgent - /** See {@link Worktree.pendingFirstAgentMessageRename}. */ - pendingFirstAgentMessageRename?: boolean - /** See {@link Worktree.firstAgentMessageRenameError}. */ - firstAgentMessageRenameError?: string | null - sparseDirectories?: string[] - sparseBaseRef?: string - sparsePresetId?: string - /** Intended create base for stale-base probes. Persisted metadata, not UI drift state. */ - baseRef?: string - /** True when Orca checked out a pre-existing local branch that delete must not prune. */ - preserveBranchOnDelete?: boolean - /** See {@link Worktree.pushTarget}. Persisted so refreshed worktree lists keep the target. */ - pushTarget?: GitPushTarget - /** Explicit marker stamped when Orca creates the worktree. */ - orcaCreatedAt?: number - orcaCreationSource?: 'desktop' | 'runtime' | 'cli' | 'ssh' - /** Workspace layout active when Orca created the worktree. */ - orcaCreationWorkspaceLayout?: OrcaWorkspaceLayout - /** User-assigned workspace board status for manual sidebar organization. */ - workspaceStatus?: WorkspaceStatus - diffComments?: DiffComment[] - /** Path-derived worktree ids this worktree had before its folder was renamed - * on disk (the id embeds the path). Lets the daemon's session GC and registry - * hydration recognize sessions minted under an old id instead of reaping - * them. Self-prunes when the worktree is deleted. */ - priorWorktreeIds?: string[] - mobileDiffReview?: MobileDiffReviewState - /** System-owned provenance for workspaces created by automation new-per-run dispatches. */ - automationProvenance?: AutomationWorkspaceProvenance - /** System-owned provenance for workspaces created via `orca worktree create`. */ - cliProvenance?: CliWorkspaceProvenance -} - -export type WorktreeOwnership = 'orca-managed' | 'external' | 'unknown-legacy' | 'agent-scratch' - -export type DetectedWorktreeListSource = 'git' | 'metadata-fallback' | 'session-fallback' - -export type DetectedWorktree = Worktree & { - ownership: WorktreeOwnership - selectedCheckout: boolean - visible: boolean - /** Optional additive source identity; older hosts omit it. */ - visibilitySource?: - | { kind: 'built-in'; id: BuiltInWorktreeVisibilitySourceId } - | { kind: 'custom'; id: string } -} - -export type DetectedWorktreeListResult = { - repoId: string - authoritative: boolean - source: DetectedWorktreeListSource - worktrees: DetectedWorktree[] -} - -export type WorktreeLineageOrigin = 'orchestration' | 'cli' | 'manual' -export type WorktreeLineageCaptureConfidence = 'explicit' | 'inferred' -export type WorktreeLineageCaptureSource = - | 'explicit-cli-flag' - | 'env-workspace' - | 'cwd-context' - | 'terminal-context' - | 'orchestration-context' - | 'active-workspace' - | 'manual-action' - -export type WorktreeLineageCapture = { - source: WorktreeLineageCaptureSource - confidence: WorktreeLineageCaptureConfidence -} - -export type WorktreeLineage = { - worktreeId: string - worktreeInstanceId: string - parentWorktreeId: string - parentWorktreeInstanceId: string - origin: WorktreeLineageOrigin - capture: WorktreeLineageCapture - orchestrationRunId?: string - taskId?: string - coordinatorHandle?: string - createdByTerminalHandle?: string - createdAt: number -} - -export type WorkspaceLineage = { - childWorkspaceKey: WorkspaceKey - childInstanceId?: string | null - parentWorkspaceKey: WorkspaceKey - parentInstanceId?: string | null - origin: WorktreeLineageOrigin - capture: WorktreeLineageCapture - taskId?: string - orchestrationRunId?: string - coordinatorHandle?: string - createdByTerminalHandle?: string - createdAt: number -} - -export type WorktreeLineageWarningCode = - | 'LINEAGE_PARENT_CONTEXT_MISSING' - | 'LINEAGE_PARENT_CONTEXT_CONFLICT' - | 'LINEAGE_PARENT_INSTANCE_STALE' - -export type WorktreeLineageWarning = { - code: WorktreeLineageWarningCode - message: string - details?: Record -} - -// ─── Diff line comments ────────────────────────────────────────────── -// Why: users leave review notes on specific lines of the modified side of -// a diff so they can be handed back to an AI agent (pasted into a terminal -// or used to bootstrap a new agent session). Stored on WorktreeMeta so the -// existing persistence layer writes them to orca-data.json automatically. -export type DiffCommentSource = 'diff' | 'markdown' -export type DiffReviewScope = 'unstaged' | 'staged' | 'branch' - -export type MobileDiffReviewFileState = { - key: string - filePath: string - oldPath?: string - scope: DiffReviewScope - lastOpenedAt?: number - lastSeenDiffIdentity?: string - reviewedAt?: number - reviewDiffIdentity?: string -} - -export type MobileDiffReviewState = { - version: 1 - updatedAt?: number - completedAt?: number - files: Record -} - -export type DiffComment = { - id: string - worktreeId: string - filePath: string - /** Undefined means a legacy diff note. */ - source?: DiffCommentSource - /** Exact text selected when creating a markdown note, when available. */ - selectedText?: string - /** Inclusive range start. Must be <= lineNumber when present. */ - startLine?: number - lineNumber: number - body: string - createdAt: number - updatedAt?: number - /** Set after the note has been handed to an agent. Edits clear it. */ - sentAt?: number - scope?: DiffReviewScope - oldPath?: string - diffIdentity?: string - // Reserved for future "comments on the original side" — always 'modified' in v1. - side: 'modified' -} - -// ─── Tab Group Layout ─────────────────────────────────────────────── -export type TabGroupSplitDirection = 'horizontal' | 'vertical' - -export type TabGroupLayoutNode = - | { type: 'leaf'; groupId: string } - | { - type: 'split' - direction: TabGroupSplitDirection - first: TabGroupLayoutNode - second: TabGroupLayoutNode - /** Flex ratio of the first child (0–1). Defaults to 0.5 if absent. */ - ratio?: number - } - -// ─── Unified Tab ──────────────────────────────────────────────────── -export type TabContentType = - | 'terminal' - | 'editor' - | 'diff' - | 'conflict-review' - | 'check-details' - | 'browser' - | 'simulator' - -export type WorkspaceVisibleTabType = 'terminal' | 'editor' | 'browser' | 'simulator' -export type CtrlTabOrderMode = 'mru' | 'sequential' - -export type Tab = { - id: string // UUID for terminals, filePath for editors (preserves current convention) - entityId: string // ID of the backing content (terminal tab ID, file path, browser workspace ID) - groupId: string - worktreeId: string - contentType: TabContentType - label: string // display title (auto-derived from PTY or filename) - generatedLabel?: string | null - /** Stable AI Vault conversation name, bound to its provider session identity. */ - aiVaultTitle?: AiVaultSessionTitle | null - quickCommandLabel?: string | null - customLabel: string | null - color: string | null - sortOrder: number - createdAt: number - isPreview?: boolean // preview tabs get replaced by next single-click open - isPinned?: boolean // pinned tabs survive "close others" - /** Why: per-tab rendering mode for coding-agent terminals. `'chat'` shows the - * native chat view as an overlay while the live terminal stays mounted - * underneath; `'terminal'` (the default for legacy/missing) shows the raw - * xterm. Optional so sessions persisted before this field hydrate cleanly. */ - viewMode?: 'terminal' | 'chat' -} - -export type TabGroup = { - id: string - worktreeId: string - activeTabId: string | null - tabOrder: string[] // canonical visual order of tab IDs - /** Per-group MRU stack (oldest → most-recent at the tail). Drives which tab - * becomes active when the current active tab closes: we pop back to the - * previously-active tab instead of jumping to a visual neighbor. Scoped to - * the group so split panes keep independent histories. Optional because - * sessions persisted before this field was added still hydrate cleanly — - * hydration seeds from activeTabId. */ - recentTabIds?: string[] -} - -// ─── Terminal Tab (legacy — used by persistence and TerminalContentSlice) ─ -export type TerminalTab = { - id: string - ptyId: string | null - worktreeId: string - title: string - /** Stable fallback label for default-named terminals ("Terminal 1", etc.). - * Why: agent CLIs overwrite the live title via OSC updates, but Orca still - * needs the original terminal label for numbering and reset behavior. */ - defaultTitle?: string - /** Stable opt-in label derived from the first known agent prompt. */ - generatedTitle?: string | null - /** Stable AI Vault conversation name, bound to its provider session identity. */ - aiVaultTitle?: AiVaultSessionTitle | null - /** Stable label from the tab-bar Quick Command that created this terminal. */ - quickCommandLabel?: string | null - customTitle: string | null - color: string | null - /** Pinned tabs survive "close others"; host-persisted for remote servers. */ - isPinned?: boolean - /** Per-tab view preference (terminal xterm vs native chat); host-persisted so - * paired clients converge. Optional: older persisted tabs default to 'terminal'. */ - viewMode?: 'terminal' | 'chat' - sortOrder: number - createdAt: number - /** Bumped on shutdown so TerminalPane remounts with a fresh PTY. */ - generation?: number - /** Why: records the shell this tab was opened with (e.g. 'wsl.exe') so the - * PTY and tab icon stay stable even if the default shell setting changes - * later. Older persisted tabs may omit this field. */ - shellOverride?: string - /** Keeps an ephemeral host fallback out of the active project's runtime. */ - forceHostRuntime?: boolean - /** Why: explorer-created terminals can start below the workspace root while - * still belonging to that workspace for tab/session ownership. */ - startupCwd?: string - /** Why: the coding-harness agent Orca launched in this tab. Lets the tab bar - * show the provider icon immediately, before the agent emits its first hook - * event (a freshly-launched, idle agent reports no live status yet). Live - * hook status overrides this once the agent does anything. Plain terminals - * and manually-started agents omit it. */ - launchAgent?: TuiAgent - /** Why: when `setActiveWorktree` bumps generation on all-dead tabs to drive a - * TerminalPane remount, the fresh PTY that results is caused by navigation, - * not by the user doing work. Without this flag the resulting - * `updateTabPtyId` call would call `bumpWorktreeActivity` and flip the - * sidebar's recency sort on every click — the reorder-on-click bug. The - * flag is set by `setActiveWorktree` and consumed by the activation-driven - * PTY lifecycle calls that follow, which then suppress activity bumps and - * `sortEpoch` increments. Split layouts use a numeric count because one tab - * can remount several panes. Never persisted — it is a transient handoff. */ - pendingActivationSpawn?: boolean | number -} - -export type BrowserHistoryEntry = { - url: string - normalizedUrl: string - title: string - lastVisitedAt: number - visitCount: number -} - -export type BrowserLoadError = { - code: number - description: string - validatedUrl: string -} - -export type BrowserCertificateFailure = { - challengeId: string - browserPageId: string - errorCode: number | null - error: string - origin: string - displayHost: string - canProceed: boolean - observedAt: number -} - -export type BrowserCertificateProceedFailureReason = - | 'expired' - | 'changed' - | 'ineligible' - | 'missing' - | 'navigated' - -export type BrowserCertificateProceedResult = - | { ok: true } - | { ok: false; reason: BrowserCertificateProceedFailureReason } - -// Why: BrowserPage persists the active viewport preset so CDP emulation can be -// reapplied on reload/navigation without the user re-picking from the toolbar. -export type BrowserViewportPresetId = - | 'mobile-s' - | 'mobile-m' - | 'mobile-l' - | 'tablet' - | 'laptop' - | 'laptop-l' - | 'desktop' - -export type BrowserViewportOverride = { - width: number - height: number - deviceScaleFactor: number - mobile: boolean -} - -export type BrowserPage = { - id: string - workspaceId: string - worktreeId: string - url: string - title: string - loading: boolean - faviconUrl: string | null - canGoBack: boolean - canGoForward: boolean - loadError: BrowserLoadError | null - createdAt: number - // Why: remote-owned worktrees can still host client-local fallback browser - // pages until headless remote runtimes support real browser panes. - browserRuntimeEnvironmentId?: string | null - /** Active CDP viewport emulation preset. null = default (fill pane, no CDP override) */ - viewportPresetId?: BrowserViewportPresetId | null -} - -export type BrowserWorkspace = { - id: string - worktreeId: string - /** Stable display label for the outer Orca tab ("Browser 1", "Browser 2", …). - * Optional so sessions persisted before this field was added fall back - * gracefully to the URL-derived label in getBrowserTabLabel. */ - label?: string - // Why: each browser workspace binds to exactly one session profile at creation - // time. The profile determines which Electron partition (and thus which - // cookies/storage) the guest webview uses. Absent means the legacy shared - // partition, which keeps backward compat with workspaces persisted before - // session profiles existed. - sessionProfileId?: string | null - // Why: runtime-created tabs resolve profile partition in main. Persisting it - // keeps isolated storage stable when the renderer profile mirror is stale. - sessionPartition?: string | null - activePageId?: string | null - pageIds?: string[] - // Why: the active page owns real browser chrome state now, but the top-level - // Orca tab strip still renders one workspace entry. Mirror the active page's - // title/url/loading metadata here so existing workspace-level UI can stay - // stable while Phase 2 introduces nested browser pages. - url: string - title: string - loading: boolean - faviconUrl: string | null - canGoBack: boolean - canGoForward: boolean - loadError: BrowserLoadError | null - createdAt: number -} - -export type BrowserTab = BrowserWorkspace - -export type BrowserSessionProfileScope = 'default' | 'isolated' | 'imported' - -export type BrowserSessionUserAgentMode = 'clean' | 'native' - -export type BrowserSessionProfileCreateOptions = { - userAgentMode?: BrowserSessionUserAgentMode -} - -export type BrowserSessionProfileSource = { - browserFamily: - | 'chrome' - | 'chromium' - | 'arc' - | 'edge' - | 'firefox' - | 'safari' - | 'comet' - | 'helium' - | 'manual' - profileName?: string - importedAt: number -} - -export type BrowserSessionProfile = { - id: string - scope: BrowserSessionProfileScope - partition: string - label: string - source: BrowserSessionProfileSource | null - userAgentMode?: BrowserSessionUserAgentMode -} - -export type BrowserCookieImportSummary = { - totalCookies: number - importedCookies: number - skippedCookies: number - googleCookiesSkipped?: number - domains: string[] - warning?: { - code: 'restart-fallback-unavailable' - loadedCookies: number - failedCookies: number - } -} - -export type BrowserCookieImportResult = - | { ok: true; profileId: string; summary: BrowserCookieImportSummary } - | { ok: false; reason: string } - -export type TerminalPaneSplitDirection = 'vertical' | 'horizontal' - -export type TerminalPaneLayoutNode = - | { - type: 'leaf' - leafId: string - } - | { - type: 'split' - direction: TerminalPaneSplitDirection - first: TerminalPaneLayoutNode - second: TerminalPaneLayoutNode - /** Flex ratio of the first child (0–1). Defaults to 0.5 if absent. */ - ratio?: number - } - -export type TerminalLayoutSnapshot = { - root: TerminalPaneLayoutNode | null - activeLeafId: string | null - expandedLeafId: string | null - /** Live PTY IDs per leaf for in-session remounts such as tab-group moves. - * Not used for app restart because PTYs are transient processes. */ - ptyIdsByLeafId?: Record - /** Serialized terminal buffers per leaf for scrollback restoration on restart. */ - buffersByLeafId?: Record - /** Durable scrollback snapshot refs per leaf; raw bytes live outside session JSON. */ - scrollbackRefsByLeafId?: Record - /** User-assigned pane titles, keyed by stable layout leaf UUID. - * Persisted alongside buffers via the existing session:set flow. */ - titlesByLeafId?: Record -} - -/** Minimal subset of OpenFile persisted across restarts. - * Only edit-mode files are saved — diffs, conflict reviews, and other - * transient views are reconstructed on demand from git state. */ -export type PersistedOpenFile = { - filePath: string - relativePath: string - worktreeId: string - language: string - isPreview?: boolean - runtimeEnvironmentId?: string | null - /** SSH target that owns an absolute path outside the worktree. */ - externalSshTargetId?: string - /** Unsaved editor buffer captured for hot exit; presence restores the tab dirty. */ - dirtyDraftContent?: string - /** Signature of the disk content the dirty draft is based on; lets restore - * re-derive a changed-on-disk conflict from ground truth. */ - lastKnownDiskSignature?: string - /** Why: a read-only tab (AI Vault View Log) must survive restart still - * read-only; persisted only when true so old sessions stay writable. */ - readOnly?: boolean - /** Opt-in streaming append for a read-only local log tab. */ - liveTail?: boolean -} - -export type WorkspaceSessionState = { - activeRepoId: string | null - /** Scope-aware active owner for folder workspaces. Legacy worktree UI still reads activeWorktreeId. */ - activeWorkspaceKey?: WorkspaceKey | null - activeWorkspaceExecutionHostId?: ExecutionHostId | null - activeWorktreeId: string | null - activeTabId: string | null - /** Keys may be legacy raw worktree IDs or canonical WorkspaceKey values. */ - tabsByWorktree: Record - terminalLayoutsByTabId: Record - /** Worktree IDs that had at least one tab with a live PTY at shutdown. - * Used on startup to eagerly re-spawn PTY processes so the Active filter - * works immediately after restart. */ - activeWorktreeIdsOnShutdown?: string[] - /** Editor files that were open at shutdown, keyed by worktree ID. - * Only edit-mode files are persisted — diffs and conflict views are - * transient and not restored. */ - openFilesByWorktree?: Record - /** Per-worktree active editor file ID (filePath) at shutdown. */ - activeFileIdByWorktree?: Record - /** Per-file markdown preview front-matter visibility. Absent entry means hidden. */ - markdownFrontmatterVisible?: Record - /** Persisted browser workspaces, keyed by worktree ID. */ - browserTabsByWorktree?: Record - /** Persisted browser pages, keyed by workspace ID. */ - browserPagesByWorkspace?: Record - /** Per-worktree active browser workspace ID at shutdown. */ - activeBrowserTabIdByWorktree?: Record - /** Per-worktree active tab type (terminal vs editor vs browser) at shutdown. */ - activeTabTypeByWorktree?: Record - /** Global browser URL history for address bar autocomplete. */ - browserUrlHistory?: BrowserHistoryEntry[] - /** Per-worktree last-active terminal tab ID at shutdown. */ - activeTabIdByWorktree?: Record - /** Unified tab model — present when saved by a build that includes TabsSlice. - * Read-path checks for this first; falls back to legacy fields if absent. */ - unifiedTabs?: Record - /** Tab group model — present alongside unifiedTabs. */ - tabGroups?: Record - /** Persisted split layout tree per worktree. */ - tabGroupLayouts?: Record - /** Per-worktree focused group at shutdown. */ - activeGroupIdByWorktree?: Record - /** SSH target IDs that were connected at shutdown. Used on startup to - * auto-reconnect before attempting remote PTY reattach. */ - activeConnectionIdsAtShutdown?: string[] - /** Maps tab IDs to their remote relay PTY session IDs. Populated at - * shutdown from renderer state so remote PTYs can be reattached via - * the relay's pty.attach RPC on startup. */ - remoteSessionIdsByTabId?: Record - /** Per-worktree focus-recency timestamps used by the Cmd+J empty-query - * ordering. Separate from worktree.lastActivityAt (background signal) - * and worktreeNavHistory (Back/Forward stack). See - * docs/cmd-j-empty-query-ordering.md. Absent in sessions written by - * older builds — hydration tolerates missing/partial maps and the - * active worktree is seeded on first restore. */ - lastVisitedAtByWorktreeId?: Record - /** Worktrees whose repo-defined default terminal tabs have already been - * considered. Persisted so closing all tabs and re-opening the workspace - * does not recreate the template. */ - defaultTerminalTabsAppliedByWorktreeId?: Record - /** Provider-session resume records captured when workspaces sleep. */ - sleepingAgentSessionsByPaneKey?: Record - /** Host-issued process incarnation for each durable terminal surface. */ - terminalPtyIncarnationsByPaneKey?: Record - /** Monotonic host authority watermark for terminal membership in each repo. */ - terminalTopologyRevisionByRepoId?: Record - /** Legacy per-surface fences migrated into terminalTopologyRevisionByRepoId on load. */ - terminalSurfaceTombstonesByPaneKey?: Record< - string, - { - worktreeId: string - parentTabId: string - leafId: string - ptyId: string - incarnationId: string - retiredAt: number - } - > -} - -export type WorkspaceSessionPatch = Partial - -// ─── GitHub ────────────────────────────────────────────────────────── -export type PRState = 'open' | 'closed' | 'merged' | 'draft' -export type IssueState = 'open' | 'closed' -export type CheckStatus = 'pending' | 'success' | 'failure' | 'neutral' - -export type PRMergeableState = 'MERGEABLE' | 'CONFLICTING' | 'UNKNOWN' -export type PRReviewDecision = 'APPROVED' | 'CHANGES_REQUESTED' | 'REVIEW_REQUIRED' - -export type PRConflictSummary = { - baseRef: string - baseCommit: string - commitsBehind: number - files: string[] - localMergeState?: 'clean' -} - -// Why: host must survive renderer/RPC boundaries so Enterprise review actions -// cannot silently fall back to a same-named repository on github.com. -export type GitHubRepositoryIdentity = { owner: string; repo: string; host?: string } - -export type GitHubPRMergeMethod = 'merge' | 'squash' | 'rebase' - -export type GitHubPRMergeMethodSettings = { - defaultMethod: GitHubPRMergeMethod - allowedMethods: Record -} - -export type GitHubPRStackEntry = { - position: number - number: number - title: string - url: string - updatedAt?: string - state: PRState - checksStatus: CheckStatus - mergeable: PRMergeableState - reviewDecision?: PRReviewDecision | null - mergeStateStatus?: string | null - headRefName?: string - headSha?: string -} - -export type GitHubPRStack = { - number: number - position: number - size: number - baseRefName: string - baseSha?: string - entries?: GitHubPRStackEntry[] -} - -export type PRInfo = { - number: number - title: string - state: PRState - url: string - checksStatus: CheckStatus - updatedAt: string - mergeable: PRMergeableState - reviewDecision?: PRReviewDecision | null - autoMergeEnabled?: boolean - autoMergeAllowed?: boolean | null - mergeQueueRequired?: boolean | null - mergeMethodSettings?: GitHubPRMergeMethodSettings - mergeStateStatus?: string | null - /** GitHub-registered stack metadata. Absent for ordinary dependent PR chains. */ - stack?: GitHubPRStack - // Why: check-runs are keyed by the PR head commit, not the mutable branch name. - // Keeping the head SHA in cached PR metadata lets the checks panel poll the - // correct commit without re-querying GitHub or guessing from local branch refs. - headSha?: string - // Why: a merged branch-matched PR stays visible when the worktree head is one - // of the PR's own commits (behind update-branch/web commits). Cache staleness - // checks must honor that confirmation without re-querying GitHub. - confirmedContainedHeadOid?: string - // Why: the worktree HEAD OID this merged linked PR was confirmed to have - // diverged from (a definite not-contained probe). Head-scoped, not a bare - // boolean, so a PR-number-coalesced refresh broadcast cannot clear a sibling - // worktree whose own head is still on the PR's line of work. Clearing a - // durable linked PR requires this positive signal for that exact head, never - // the mere absence of a containment confirmation after a rate-limit/error. - headDivergedFromMergedPRAtOid?: string - /** Target branch name for PR-created worktree compare-base repair. */ - baseRefName?: string - /** PR head branch name. Lets linked-PR consumers detect that the worktree - * has switched to a different branch and the durable link is stale. */ - headRefName?: string - prRepo?: GitHubRepositoryIdentity - headRepo?: GitHubRepositoryIdentity - conflictSummary?: PRConflictSummary -} - -/** - * Discriminates a classified GitHub PR-refresh failure. The renderer maps these - * to stable, non-destructive empty-state copy; a `hard` subset (auth, permission, - * repo_unavailable, gh_unavailable) means the existing-review lookup is currently - * impossible and must hide the Create composer. - */ -export type PRRefreshErrorType = - | 'rate_limited' - | 'auth' - | 'network' - | 'permission' - | 'repo_unavailable' - | 'gh_unavailable' - | 'server_error' - | 'unknown' - -// Backward-compatible name used by outage-copy consumers added on main. -export type PRRefreshUpstreamErrorType = PRRefreshErrorType - -export type PRRefreshOutcome = - | { kind: 'found'; pr: PRInfo; fetchedAt: number } - | { kind: 'no-pr'; fetchedAt: number } - | { - kind: 'upstream-error' - errorType: PRRefreshErrorType - message: string - fetchedAt: number - // Unified retry schedule (see docs/reference/pr-panel-refresh-guidance.md). - // `nextAutoRetryAt`: earliest time main expects to auto-retry this key. - // `retryDisabledUntil`: earliest time a manual Retry / refreshPRNow is - // accepted (rate-limit gates only, never ordinary network/auth backoff). - nextAutoRetryAt?: number - retryDisabledUntil?: number - } - -export type GitHubPRRefreshReason = 'visible' | 'active' | 'post-push' | 'manual' | 'swr' - -export type GitHubPRRefreshEnqueueResult = - | { kind: 'queued' } - | { kind: 'skipped'; skippedReason: 'validation-denied' | 'validation-backoff' } - | { kind: 'fallback' } - -export type GitHubPRRefreshAlias = { - cacheKey: string - repoId?: string - repoPath: string - branch: string - worktreeId?: string - connectionId?: string | null - executionHostId?: string | null - linkedPRNumber?: number | null - fallbackPRNumber?: number | null - fallbackPRSource?: 'explicit' | 'pr-cache' | 'hosted-review' | null - // Why: request-time worktree HEAD. Merged branch-matched PRs are only visible - // for heads that belong to the PR, and refresh consumers need this snapshot to - // clear a durable linked PR once main confirms the head diverged. - currentHeadOid?: string | null -} - -export type GitHubPRRefreshCandidate = GitHubPRRefreshAlias & { - repoKind: RepoKind - repoId: string - isBare?: boolean - isArchived?: boolean - connectionId?: string | null - executionHostId?: string | null - connectionState?: 'connected' | 'disconnected' | 'unknown' - cachedFetchedAt?: number | null - cachedHasPR?: boolean | null - cachedPRState?: PRState | null - cachedChecksStatus?: CheckStatus | null - cachedMergeable?: PRMergeableState | null - cachedMergeStateStatus?: string | null - localGitOptions?: { wslDistro?: string } -} - -export type GitHubPRRefreshSkippedReason = - | 'fresh' - | 'not-git' - | 'bare' - | 'archived' - | 'disconnected' - | 'remote' - | 'rate-limit' - | 'capacity' - -type GitHubPRRefreshEventBase = { - sequence: number - reason: GitHubPRRefreshReason - aliases: GitHubPRRefreshAlias[] - requestStartedAt?: number -} - -export type GitHubPRRefreshEvent = - | (GitHubPRRefreshEventBase & { - outcome: PRRefreshOutcome - status?: never - pausedUntil?: never - skippedReason?: never - }) - | (GitHubPRRefreshEventBase & { - status: 'queued' | 'in-flight' - outcome?: never - pausedUntil?: never - skippedReason?: never - }) - | (GitHubPRRefreshEventBase & { - status: 'paused' - pausedUntil: number - skippedReason: 'rate-limit' - outcome?: never - }) - | (GitHubPRRefreshEventBase & { - status: 'skipped' - skippedReason: GitHubPRRefreshSkippedReason - outcome?: never - pausedUntil?: never - }) - -export type PRCheckDetail = { - name: string - status: 'queued' | 'in_progress' | 'completed' - conclusion: - | 'success' - | 'failure' - | 'cancelled' - | 'timed_out' - | 'neutral' - | 'skipped' - | 'pending' - // Why: a check suite needing manual action (e.g. a workflow awaiting "Approve - // and run") has no check run and is absent from statusCheckRollup, yet blocks - // auto-merge (GitHub returns "unstable status"). Surface it as its own state. - | 'action_required' - | null - url: string | null - checkRunId?: number - workflowRunId?: number - // Why: the GitLab job trace API is addressed by numeric job id only, so the - // Checks panel cannot load a job log without carrying it on the row. - gitlabJobId?: number -} - -export type PRCheckAnnotation = { - path: string | null - startLine: number | null - endLine: number | null - annotationLevel: string | null - title: string | null - message: string - rawDetails: string | null -} - -export type PRCheckStep = { - name: string - status: string | null - conclusion: string | null - startedAt: string | null - completedAt: string | null -} - -export type PRCheckJob = { - id: number | null - name: string - status: string | null - conclusion: string | null - startedAt: string | null - completedAt: string | null - url: string | null - logTail: string | null - steps: PRCheckStep[] -} - -export type PRCheckRunDetails = { - name: string - status: PRCheckDetail['status'] | (string & {}) | null - conclusion: PRCheckDetail['conclusion'] | (string & {}) | null - url: string | null - detailsUrl: string | null - startedAt: string | null - completedAt: string | null - title: string | null - summary: string | null - text: string | null - annotations: PRCheckAnnotation[] - jobs: PRCheckJob[] -} - -export type GitHubRerunPRChecksResult = { ok: true; count: number } | { ok: false; error: string } - -export type GitHubReactionContent = - | '+1' - | '-1' - | 'laugh' - | 'confused' - | 'heart' - | 'hooray' - | 'rocket' - | 'eyes' - -export type GitHubReaction = { - content: GitHubReactionContent - count: number - viewerHasReacted?: boolean -} - -export type PRComment = { - id: number - author: string - authorAvatarUrl: string - body: string - createdAt: string - url: string - reactions?: GitHubReaction[] - /** GraphQL node ID for GitHub comments that support reaction mutations. */ - reactionSubjectId?: string - /** File path for inline review comments (absent for top-level conversation comments). */ - path?: string - /** GraphQL node ID of the review thread — present only for inline review comments. - * Used to resolve/unresolve the thread via GitHub's GraphQL API. */ - threadId?: string - /** Whether the review thread has been resolved. Only meaningful when threadId is set. */ - isResolved?: boolean - /** True when GitHub no longer maps the thread to the current diff. */ - isOutdated?: boolean - /** End line of the review annotation (1-based). */ - line?: number - /** Start line of the review annotation range (1-based). Absent for single-line comments. */ - startLine?: number - /** True when GitHub identifies the author as a bot (REST `user.type === 'Bot'` or - * GraphQL `__typename === 'Bot'`). Preferred over login-string heuristics because - * third-party review bots (e.g. qodo-ai-reviewer, coderabbitai) don't follow a - * predictable naming convention. Absent when the data source can't report it - * (non-GitHub fallbacks via `gh pr view`). */ - isBot?: boolean -} - -export type GitHubIssueTimelineTarget = { - type: 'issue' | 'pr' - number: number - title: string - url: string - repository?: string -} - -export type GitHubIssueTimelineItem = { - id: string - event: - | 'assigned' - | 'unassigned' - | 'mentioned' - | 'cross-referenced' - | 'closed' - | 'reopened' - | 'moved_columns_in_project' - actor: string - actorAvatarUrl: string - createdAt: string - assignee?: string - source?: GitHubIssueTimelineTarget - closer?: GitHubIssueTimelineTarget - stateReason?: string | null - previousColumnName?: string | null - columnName?: string | null - projectName?: string | null -} - -export type GitHubCommentResult = { ok: true; comment: PRComment } | { ok: false; error: string } - -export type IssueInfo = { - number: number - title: string - state: IssueState - url: string - labels: string[] - /** Full markdown body when fetched through the single-issue endpoint. */ - description?: string -} - -export type GitHubViewer = { - login: string - email: string | null -} - -export type GitHubAssignableUser = { - login: string - name: string | null - avatarUrl: string -} - -export type ProviderCheckSummary = { - state: 'success' | 'failure' | 'pending' | 'neutral' | 'none' - total: number - passed: number - failed: number - pending: number - neutral: number -} - -export type GitHubPRReviewSummary = { - login: string - state?: string | null - avatarUrl?: string | null -} - -export type GitHubPRFileViewedState = 'DISMISSED' | 'VIEWED' | 'UNVIEWED' - -export type GitHubWorkItem = { - id: string - type: 'issue' | 'pr' - number: number - title: string - state: 'open' | 'closed' | 'merged' | 'draft' - url: string - labels: string[] - updatedAt: string - author: string | null - // Why: GHE user logins don't exist on github.com, so the github.com/{login}.png - // fallback 404s. Carry the API-provided avatar_url so github.com + Enterprise - // both render; absent on the gh-pr-view path (gh omits avatar), then the UI - // falls back to the login URL and finally an initials placeholder. See #8784. - authorAvatarUrl?: string - branchName?: string - baseRefName?: string - // Why: PR checks are keyed by head commit; carrying this lets task rows use - // the cached check-runs endpoint instead of one `gh pr checks` call per row. - headSha?: string - prRepo?: GitHubRepositoryIdentity - additions?: number - deletions?: number - changedFiles?: number - reviewDecision?: PRReviewDecision | null - reviewRequests?: GitHubAssignableUser[] - latestReviews?: GitHubPRReviewSummary[] - assignees?: GitHubAssignableUser[] - checksSummary?: ProviderCheckSummary - mergeable?: PRMergeableState - autoMergeEnabled?: boolean - autoMergeAllowed?: boolean | null - mergeQueueRequired?: boolean | null - mergeMethodSettings?: GitHubPRMergeMethodSettings - mergeStateStatus?: string | null - maintainerCanModify?: boolean - // Why: true when a PR's head lives on a fork (headRepositoryOwner !== selected repo owner). - // The Start-from picker passes this to resolvePrBase so fork heads use - // refs/pull//head for creation and a separate PR-head push target. - isCrossRepository?: boolean - /** Why: required because the cross-repo view merges items from every selected - * repo — the table row's repo pill and the "open in browser" fallback need - * to know which repo an item came from. Stamped by the renderer fetcher - * (`fetchWorkItems`) and by optimistic stubs on the new-issue path. */ - repoId: string -} - -export type GitHubPRFile = { - path: string - oldPath?: string - status: 'added' | 'modified' | 'removed' | 'renamed' | 'copied' | 'changed' | 'unchanged' - additions: number - deletions: number - /** GitHub marks files above its diff size limit as binary-like; we skip content fetches for these. */ - isBinary: boolean - /** Modified-side line numbers that GitHub accepts for inline review comments. */ - reviewCommentLineNumbers?: number[] - /** GitHub's per-viewer review state. DISMISSED means new changes arrived after the file was viewed. */ - viewerViewedState?: GitHubPRFileViewedState -} - -export type GitHubPRFileContents = { - original: string - modified: string - originalIsBinary: boolean - modifiedIsBinary: boolean - originalTooLarge?: boolean - modifiedTooLarge?: boolean -} - -export type GitHubPRReviewCommentInput = { - repoPath: string - prRepo?: GitHubRepositoryIdentity | null - prNumber: number - commitId: string - path: string - line: number - startLine?: number - body: string -} - -export type GitHubWorkItemDetails = { - // Why: main-process doesn't know Orca's Repo.id, so this inner item omits - // repoId. The renderer stamps it when routing the details through the store. - item: Omit - body: string - comments: PRComment[] - /** Issue-only provider activity such as assignment, references, project moves, and state changes. */ - timelineItems?: GitHubIssueTimelineItem[] - /** Only set for PRs. Head/base SHAs used by the Files tab to fetch per-file content. */ - headSha?: string - baseSha?: string - /** GraphQL node ID required by GitHub's file-viewed mutations. Only set for PRs. */ - pullRequestId?: string - checks?: PRCheckDetail[] - files?: GitHubPRFile[] - /** Only set for PRs. True when the file fetch failed (rate limit, auth, - * unresolved remote) rather than the PR genuinely having no changed files. */ - filesUnavailable?: boolean - participants?: GitHubAssignableUser[] - /** Logins of current assignees. Only set for issues. */ - assignees?: string[] -} - -// ─── Linear ───────────────────────────────────────────────────────── -export type LinearViewer = { - displayName: string - email: string | null - organizationId?: string - organizationName: string - organizationUrlKey?: string -} - -export type LinearWorkspace = LinearViewer & { - id: string - organizationId: string - isLegacy?: true - credentialRevision?: number -} - -export type LinearWorkspaceSelection = (string & {}) | 'all' -export type LinearWorkspaceSelector = LinearWorkspaceSelection | undefined -export type LinearConcreteWorkspaceId = string - -export type LinearWorkspaceError = { - workspaceId: string - workspaceName?: string - type: 'auth' | 'rate_limited' | 'network' | 'unknown' - message: string -} - -export type LinearCollectionResult = { - items: T[] - errors?: LinearWorkspaceError[] - hasMore?: boolean -} - -export type LinearConnectionStatus = { - connected: boolean - viewer: LinearViewer | null - workspaces?: LinearWorkspace[] - activeWorkspaceId?: string | null - selectedWorkspaceId?: LinearWorkspaceSelection | null - // Set when a stored token file exists but could not be decrypted, so the - // UI can explain reads failing while the connection still looks saved. - credentialError?: string -} - -export type LinearIssue = { - id: string - workspaceId?: string - workspaceName?: string - identifier: string - title: string - branchName?: string - description?: string - url: string - state: { - name: string - type: string - color: string - } - team: { - id: string - name: string - key: string - } - project?: LinearProjectSummary - subIssues?: LinearIssueChildSummary[] - labels: string[] - labelIds: string[] - assignee?: { - id: string - displayName: string - avatarUrl?: string - } - estimate?: number | null - priority: number - dueDate?: string | null - updatedAt: string -} - -export type LinearProjectSummary = { - id: string - slugId?: string - workspaceId?: string - workspaceName?: string - name: string - url?: string - color?: string - icon?: string - description?: string - content?: string - status?: LinearProjectStatusSummary - health?: string | null - priority?: number | null - priorityLabel?: string | null - lead?: LinearProjectMemberSummary - members?: LinearProjectMemberSummary[] - teams?: { - id: string - name: string - key?: string - }[] - labels?: { - id: string - name: string - color?: string - }[] - startDate?: string | null - targetDate?: string | null - createdAt?: string - updatedAt?: string - completedAt?: string | null - canceledAt?: string | null - startedAt?: string | null - progress?: number | null - scope?: number | null - issueCount?: number - completedIssueCount?: number -} - -export type LinearProjectStatusSummary = { - id: string - name: string - type?: string - color?: string -} - -export type LinearProjectMemberSummary = { - id: string - displayName: string - avatarUrl?: string -} - -export type LinearProjectMilestoneSummary = { - id: string - name: string - status?: string - targetDate?: string | null - progress?: number | null -} - -export type LinearProjectResourceSummary = { - id: string - title: string - url: string - type?: string -} - -export type LinearProjectUpdateSummary = { - id: string - body?: string - health?: string | null - url?: string - createdAt?: string - updatedAt?: string - user?: LinearProjectMemberSummary -} - -export type LinearProjectDetail = LinearProjectSummary & { - milestones?: LinearProjectMilestoneSummary[] - resources?: LinearProjectResourceSummary[] - latestUpdate?: LinearProjectUpdateSummary -} - -export type LinearCustomViewModel = 'issue' | 'project' - -export type LinearCustomViewSummary = { - id: string - workspaceId?: string - workspaceName?: string - name: string - description?: string - model: LinearCustomViewModel - url?: string - color?: string - icon?: string - shared?: boolean - team?: { - id: string - name?: string - key?: string - } - owner?: LinearProjectMemberSummary - creator?: LinearProjectMemberSummary - createdAt?: string - updatedAt?: string -} - -export type LinearIssueChildSummary = { - id: string - identifier: string - title: string - url: string -} - -export type LinearComment = { - id: string - body: string - createdAt: string - user?: { - displayName: string - avatarUrl?: string - } -} - -// ─── Issue Mutations ──────────────────────────────────────────────── - -export type GitHubCreateIssueFields = { - labels?: string[] - assignees?: string[] -} - -export type GitHubCreateIssueResult = - | { ok: true; number: number; url: string; bodySaveWarning?: string } - | { ok: false; error: string } - -export type GitHubIssueCloseReason = 'completed' | 'not_planned' | 'duplicate' - -export type GitHubIssueUpdate = { - state?: 'open' | 'closed' - stateReason?: GitHubIssueCloseReason - duplicateOf?: number - title?: string - // Why: body writes use the REST issue endpoint instead of `gh issue edit` - // because that command does not consistently cover every body-edit case the - // dialog needs. - body?: string - addLabels?: string[] - removeLabels?: string[] - addAssignees?: string[] - removeAssignees?: string[] -} - -export type GitHubPullRequestStateUpdate = { - state: 'open' | 'closed' -} - -export type LinearIssueUpdate = { - stateId?: string - title?: string - description?: string - assigneeId?: string | null - estimate?: number | null - priority?: number - dueDate?: string | null - labelIds?: string[] - projectId?: string | null - parentId?: string | null -} - -export type ClassifiedError = { - type: - | 'permission_denied' - | 'not_found' - | 'issues_disabled' - | 'validation_error' - | 'rate_limited' - | 'network_error' - | 'unknown' - message: string -} - -// Why: declared here as a shared shape so IPC return envelopes and renderer -// slices can reference the same structural type without importing from main. -// Aliased as `OwnerRepo` in `src/main/github/gh-utils.ts` so main call sites -// can continue using the short local name. -export type GitHubOwnerRepo = GitHubRepositoryIdentity - -// Why: GitLab-specific types live in `./gitlab-types` so they can grow -// independently from the central types file (which is touched by every -// upstream feature). Re-exported here so existing call sites -// (`from '../shared/types'`) keep working without changes. export type { GitLabAssignableUser, GitLabAuthDiagnostic, @@ -2069,6 +200,8 @@ export type { MRState } from './gitlab-types' +// Jira + export type { JiraAuthType, JiraComment, @@ -2094,1888 +227,117 @@ export type { JiraViewer } from './jira-types' -/** - * GitHub API rate-limit buckets surfaced in the TaskPage header so users can - * see remaining budget before they hit the wall. `core` = REST (5000/hr), - * `search` = Search API (30/min — hit by countWorkItems), `graphql` = - * GraphQL (5000 points/hr — hit by project-view + discovery). All three are - * the buckets this app actually stresses; other buckets (e.g. code_search) - * are not surfaced because we don't touch them. - */ -export type GitHubRateLimitBucket = { - remaining: number - limit: number - /** Unix epoch seconds when the window resets. */ - resetAt: number -} +// 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 GitHubRateLimitSnapshot = { - core: GitHubRateLimitBucket - search: GitHubRateLimitBucket - graphql: GitHubRateLimitBucket - /** Unix epoch ms the snapshot was produced (for "fetched Xs ago" copy). */ - fetchedAt: number -} +export type { LinearIssue, LinearIssueChildSummary, LinearComment } from './linear-issue-types' -export type GetRateLimitResult = - | { ok: true; snapshot: GitHubRateLimitSnapshot } - | { ok: false; error: string } +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' -/** - * Envelope for `gh:listWorkItems`. Carries resolved issue/PR sources so the - * renderer can render the "Issues from owner/repo" indicator without an - * extra IPC round-trip, and per-source classified errors so the UI can show - * a retryable banner when (e.g.) a private upstream 403s. - * - * Why piggyback instead of adding `gh:resolveWorkItemSources`: the renderer - * already round-trips this endpoint on every Tasks refresh, and the source - * data is a 2-field-per-side metadata add — cheaper than another IPC call. - * - * Invariant: `items` always contains whatever succeeded; `errors.issues` indicates - * the issues-side fetch failed, but any PR-side items that succeeded are still - * present in `items`. Consumers should render `items` alongside the error banner. - */ -export type ListWorkItemsResult = { - items: T[] - sources: { - issues: GitHubOwnerRepo | null - prs: GitHubOwnerRepo | null - /** Raw `origin` remote resolved for this repo, independent of the - * user's preference. Required-nullable so the renderer can compare raw - * remote candidates without inferring origin from the effective PR - * source. */ - originCandidate: GitHubOwnerRepo | null - /** Raw `upstream` remote resolved for this repo, independent of the - * user's preference. Present so the renderer's issue-source selector - * can always decide whether to render (upstream exists & differs from - * origin) and show both slugs in its tooltips, even when the user has - * picked 'origin' and `sources.issues` has collapsed onto origin. */ - upstreamCandidate: GitHubOwnerRepo | null - } - errors?: { - issues?: ClassifiedError - prs?: ClassifiedError - } - /** True when the user's per-repo preference was `'upstream'` but no upstream - * remote is configured, so the resolver fell back to origin. Renderer uses - * this to surface a one-time-per-session toast. Omitted when absent so - * existing consumers and test fixtures don't care about it. - * Typed as `?: true` (not `?: boolean`) to encode the invariant "present - * iff fell-back" — an explicit `false` write would be a bug, so make it a - * compile error. */ - issueSourceFellBack?: true -} +// Cross-provider issue mutations and task sources -export type LinearWorkflowState = { - id: string - name: string - type: string - color: string - position: number -} +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 LinearLabel = { - id: string - name: string - color: string -} +export type { TaskProvider } from './task-providers' -export type LinearMember = { - id: string - displayName: string - name?: string - email?: string - avatarUrl?: string -} +// orca.yaml hooks, setup policy, and VM recipes -export type LinearTeam = { - id: string - workspaceId?: string - workspaceName?: string - name: string - key: string - url?: string -} +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' -// ─── Hooks (orca.yaml) ────────────────────────────────────────────── -export type OrcaHooks = { - scripts: { - setup?: string // Runs after worktree is created - archive?: string // Runs before worktree is archived - } - issueCommand?: string // Shared default command for linked GitHub issues - defaultTabs?: OrcaDefaultTabTemplate[] // Terminal tabs to create once for a new worktree - environmentRecipes?: OrcaVmRecipe[] // Project-scoped per-workspace environment recipes - environmentRecipeDiagnostics?: OrcaVmRecipeDiagnostic[] // Non-fatal validation issues from environmentRecipes - worktree?: OrcaWorktreeDefaults // Project-scoped defaults applied when a worktree is created -} +// Settings, persisted UI state, and onboarding -export type OrcaWorktreeDefaults = { - // Why: shared (symlinked) rather than copied — large rebuildable dirs like - // node_modules should be one install serving every worktree. - sharedDirectories?: string[] -} +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 OrcaDefaultTabTemplate = { - title?: string - color?: string - command?: string -} +export type { GlobalSettings, OrcaWorkspaceLayout } from './global-settings-types' +export type { GhosttyImportPreview } from './global-settings-types' -export type EphemeralVmCheckoutMode = 'orca-worktree' | 'provisioned-root' +export type { HostSettingOverrides } from './host-setting-overrides' -export type OrcaVmRecipe = { - id: string - name: string - create: string - checkoutMode?: EphemeralVmCheckoutMode - description?: string - suspend?: string - resume?: string - destroy?: string - destroyDisabled?: boolean -} +export type { CommitMessageAiModelCapability } from './commit-message-ai-types' +export type { CommitMessageAiSettings } from './commit-message-ai-types' -export type OrcaVmRecipeDiagnostic = { - index: number - field?: string - message: string -} +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 RepoHookSettings = { - // Why: persisted data may still include the old mode field from the earlier - // hook UI. Keep it in the shape so existing local state reads without a migration. - mode: 'auto' | 'override' - setupRunPolicy?: SetupRunPolicy - setupAgentStartupPolicy?: SetupAgentStartupPolicy - commandSourcePolicy?: HookCommandSourcePolicy - scripts: { - setup: string - archive: string - } -} +export type { DiscoveryStatusEmitted, OnboardingOutcome } from './onboarding-state-types' +export type { OnboardingChecklistState, OnboardingState } from './onboarding-state-types' -export type WorktreeSetupLaunch = { - runnerScriptPath: string - envVars: Record - shell?: SetupRunnerShell - command?: string - waitForAgentStartup?: boolean -} +export type { PersistedUIState } from './persisted-ui-state-types' -export type WorktreeStartupLaunch = { - command: string - env?: Record - launchConfig?: SleepingAgentLaunchConfig - launchToken?: string - launchAgent?: TuiAgent - viewMode?: 'terminal' | 'chat' - startupCommandDelivery?: StartupCommandDelivery - telemetry?: { agent_kind: AgentKind; launch_source: LaunchSource; request_kind: RequestKind } -} +export type { LegacyPaneKeyAliasEntry } from './persisted-state-types' +export type { PersistedMobileClientTabSelection } from './persisted-state-types' +export type { PersistedMobileClientTabSelections, PersistedState } from './persisted-state-types' -export type WorktreeDefaultTabsLaunch = { - tabs: OrcaDefaultTabTemplate[] - runCommands: boolean -} +export { PET_SIZE_MIN, PET_SIZE_MAX, PET_SIZE_DEFAULT } from './pet-types' +export type { CustomPet, SpriteAnimation } from './pet-types' -export type WorktreeCreateTimingPhase = { - phase: string - startedAtMs: number - durationMs: number -} +// Agents, managed accounts, and updates -export type WorktreeCreateTiming = { - totalDurationMs: number - phases: WorktreeCreateTimingPhase[] -} +export type { TuiAgent } from './tui-agent' -export type CreateSparseCheckoutRequest = { - directories: string[] - /** Set when the directories came from a saved preset and the user did not - * modify them — recorded on WorktreeMeta so the worktree can show "from - * preset X" later. Cleared if the user edited the textarea. */ - presetId?: string -} +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' -/** A reusable per-repo sparse directory list. Saved by the user from the - * composer; surfaced again the next time they create a worktree in the same - * repo. The MVP scope (no preset) is `presetId === undefined`. */ -export type SparsePreset = { - id: string - repoId: string - name: string - directories: string[] - createdAt: number - updatedAt: number -} +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' -export type CreateWorktreeArgs = { - repoId: string - name: string - /** Optional user-facing label to persist separately from the git-safe - * branch/path seed. Used when a workspace is created from a GitHub or - * Linear artifact whose title should remain readable in the sidebar. */ - displayName?: string - baseBranch?: string - /** Source Control compare target when it differs from the checkout start point. */ - compareBaseRef?: string - /** Optional git branch to create, separate from the filesystem-safe worktree - * name. Used when creating from an existing branch whose local branch name - * legitimately contains `/` while the worktree directory must not. */ - branchNameOverride?: string - setupDecision?: SetupDecision - sparseCheckout?: CreateSparseCheckoutRequest - linkedIssue?: number - linkedPR?: number - linkedLinearIssue?: string - linkedLinearIssueWorkspaceId?: string | null - linkedLinearIssueOrganizationUrlKey?: string | null - linkedGitLabIssue?: number - linkedGitLabMR?: number - linkedBitbucketPR?: number | null - linkedAzureDevOpsPR?: number | null - linkedGiteaPR?: number | null - linkedWorkItem?: WorkspaceLinkedItem | null - linkedTaskSourceContext?: TaskSourceContext | null - pushTarget?: GitPushTarget - workspaceStatus?: WorkspaceStatus - manualOrder?: number - /** Parent workspace for in-app creates launched from a folder workspace. */ - parentWorkspace?: WorkspaceKey - /** Agent selected in the create surface. Omitted for blank-shell creates. */ - createdWithAgent?: TuiAgent - /** Set when the renderer knows this auto-generated branch should be renamed - * from the first agent message. */ - pendingFirstAgentMessageRename?: boolean - /** Telemetry-only: which UI surface initiated this create. Threaded from - * the renderer entry point so main can emit `workspace_created` with the - * correct `source`. `unknown` is a valid wire value — an unrecognized - * surface emits `source: 'unknown'` rather than dropping the event, so - * dashboards surface enum-coverage gaps as a slice rather than as - * missing data. Optional on the type so older renderer code paths that - * pre-date this prop default to `unknown` at the IPC boundary instead - * of failing typecheck. */ - telemetrySource?: WorkspaceSource - /** Optional startup command for callers that want the backend to spawn the - * first terminal as soon as the worktree is registered. */ - startup?: WorktreeStartupLaunch - /** Correlates `createWorktree:progress` events back to a specific pending - * creation in the renderer, so concurrent background creates each drive - * their own status surface. Omitted by synchronous callers. */ - creationId?: string - /** Authorizes the host to mint system-owned automation provenance. */ - automationProvenanceRequest?: AutomationWorkspaceProvenanceRequest -} +// Filesystem, search, process stats, and shared error shape -export type AdoptProvisionedRootArgs = CreateWorktreeArgs & { - runtimeId: string - executionHostId: ExecutionHostId - expectedPath: string -} +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 CreateWorktreeResult = { - worktree: Worktree & { - parentWorktreeId?: string | null - childWorktreeIds?: string[] - lineage?: WorktreeLineage | null - workspaceLineage?: WorkspaceLineage | null - git?: GitWorktreeInfo - } - lineage?: WorktreeLineage | null - workspaceLineage?: WorkspaceLineage | null - warnings?: WorktreeLineageWarning[] - setup?: WorktreeSetupLaunch - setupReceipt?: { - requested: 'run' | 'skip' | 'inherit' - hookFound: boolean - startupPolicy: 'start-immediately' | 'wait-for-setup' - state: 'running' | 'skipped' | 'not_configured' | 'spawn_failed' - terminalHandle?: string - } - defaultTabs?: WorktreeDefaultTabsLaunch - warning?: string - baseFallback?: WorktreeCreateBaseFallback - initialBaseStatus?: WorktreeBaseStatusEvent - localBaseRefRefresh?: LocalBaseRefRefreshResult - localBaseRefUpdateSuggestion?: LocalBaseRefUpdateSuggestion - startupTerminal?: { - spawned: boolean - handle?: string - tabId?: string - paneKey?: string | null - ptyId?: string | null - surface?: 'visible' | 'background' - } - timing?: WorktreeCreateTiming -} +export type { SearchMatch, SearchFileResult, SearchResult } from './code-search-types' +export type { SearchOptions } from './code-search-types' -export type WorktreeCreateBaseFallback = { - requestedRef: string - localRef: string -} +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 PreservedWorktreeBranch = { - branchName: string - head?: string -} - -export type RemoveWorktreeResult = { - preservedBranch?: PreservedWorktreeBranch -} - -export type ForceDeleteWorktreeBranchResult = { - deleted: true -} - -export type LocalBaseRefRefreshResult = { - status: 'updated' | 'skipped_dirty_worktree' | 'skipped_not_fast_forward' | 'skipped_error' - baseRef: string - localBranch: string - ownerWorktreePath?: string -} - -export type LocalBaseRefUpdateSuggestion = { - baseRef: string - localBranch: string - behind: number -} - -export type WorktreeBaseStatusKind = 'checking' | 'current' | 'drift' | 'base_changed' | 'unknown' - -export type WorktreeBaseStatusEvent = { - repoId: string - worktreeId: string - status: WorktreeBaseStatusKind - base: string - /** Configured remote name parsed from `base` (longest-prefix match). Absent - * when classification skipped optimistic reconcile (e.g. legacy fallback). */ - remote?: string - behind?: number - recentSubjects?: string[] -} - -export type WorktreeRemoteBranchConflictEvent = { - repoId: string - worktreeId: string - remote: string - branchName: string -} - -// ─── Updater ───────────────────────────────────────────────────────── - -// Why: the release object sent to the renderer omits `version` (redundant -// with the top-level UpdateStatus.version) to keep one source of truth. -export type ChangelogRelease = { - title: string - description: string - mediaUrl?: string - releaseNotesUrl: string -} - -export type ChangelogData = { - release: ChangelogRelease - releasesBehind: number | null -} - -export type UpdateCheckOptions = { - includePrerelease?: boolean - includePerfPrerelease?: boolean - localBuild?: boolean - /** Dev channel switching; `targetTag` pins an exact build, including older ones. */ - channel?: ReleaseChannel - targetTag?: string -} - -/** Non-release origins for an update. Derived from the dev-channel list so a new - * channel with its own repo cannot be reported as an ordinary release. */ -export type UpdateSource = 'local' | DedicatedRepoChannel - -/** Root-package Linux install formats whose update installs need privilege escalation. */ -export type LinuxRootPackageType = 'deb' | 'rpm' - -export type LinuxPackageInstallFailureReason = - | 'authentication-agent-unavailable' - | 'authentication-denied' - | 'package-install-failed' - -// Why: the renderer must not infer "no polkit agent" from copy alone — main classifies and the card branches on this discriminant. -export type LinuxPackageInstallRecovery = { - kind: 'linux-package-install' - packageType: LinuxRootPackageType - reason: LinuxPackageInstallFailureReason - version: string -} - -/** Why: only these two mean no safe command exists here; every other failure clears recovery entirely. */ -export type LinuxPackageCommandUnavailableReason = 'no-sudo' | 'no-package-manager' - -export type LinuxPackageInstallInstructions = - | { ok: true; command: string; packageFileName: string } - | { ok: false; reason: LinuxPackageCommandUnavailableReason; message: string } - -export type UpdateStatus = ( - | { state: 'idle' } - | { state: 'checking'; userInitiated?: boolean } - | { - state: 'available' - version: string - activeNudgeId?: string - // Why: releaseUrl is not currently populated by the update-available handler - // (it always sends undefined). Kept on the type for the Settings page's - // release-notes link fallback and for potential future use if the main - // process starts extracting release URLs from electron-updater metadata. - releaseUrl?: string - // Why: changelog is always explicitly set by the main process — null means - // the fetch failed or the version wasn't in the JSON (simple mode), and a - // populated object means rich mode. Using `| null` (not `?`) avoids a - // three-state ambiguity (undefined vs null vs present) and makes exhaustive - // checks straightforward. - changelog: ChangelogData | null - } - | { state: 'not-available'; userInitiated?: boolean } - | { state: 'downloading'; percent: number; version: string; activeNudgeId?: string } - | { state: 'downloaded'; version: string; releaseUrl?: string; activeNudgeId?: string } - | { - state: 'error' - message: string - userInitiated?: boolean - activeNudgeId?: string - recovery?: LinuxPackageInstallRecovery - } -) & { source?: UpdateSource } - -export type ReleaseBuildListResult = - | { ok: true; channel: ReleaseChannel; builds: ReleaseBuild[] } - | { ok: false; channel: ReleaseChannel; message: string } - -// ─── Settings ──────────────────────────────────────────────────────── -export type NotificationSettings = { - enabled: boolean - agentTaskComplete: boolean - terminalBell: boolean - suppressWhenFocused: boolean - customSoundId: - | 'system' - | 'two-tone' - | 'bong' - | 'thump' - | 'blip' - | 'sonar' - | 'blop' - | 'ding' - | 'clack' - | 'beep' - | 'custom' - customSoundPath: string | null - customSoundVolume: number -} - -export type CodexManagedAccount = { - id: string - email: string - managedHomePath: string - managedHomeRuntime?: 'host' | 'wsl' - wslDistro?: string | null - wslLinuxHomePath?: string | null - providerAccountId?: string | null - workspaceLabel?: string | null - workspaceAccountId?: string | null - createdAt: number - updatedAt: number - lastAuthenticatedAt: number -} - -export type CodexManagedAccountSummary = { - id: string - email: string - managedHomeRuntime?: 'host' | 'wsl' - wslDistro?: string | null - providerAccountId?: string | null - workspaceLabel?: string | null - workspaceAccountId?: string | null - createdAt: number - updatedAt: number - lastAuthenticatedAt: number -} - -/** Live, read-only identity of the user's real ~/.codex used by the - * system-default (activeAccountId:null) Codex account. Orca reads this to - * display and attribute the system default; it never writes ~/.codex. */ -export type CodexSystemDefaultIdentity = { - /** True when ~/.codex/auth.json exists (signed in via a token file). */ - hasAuth: boolean - /** 'oauth' = ChatGPT sign-in with an id token (has ChatGPT usage); - * 'api-key' = env-key/custom provider (no ChatGPT usage); - * 'none' = signed out or identity could not be resolved. */ - authKind: 'oauth' | 'api-key' | 'none' - email: string | null - providerAccountId: string | null - workspaceLabel: string | null -} - -export type CodexRateLimitAccountsState = { - accounts: CodexManagedAccountSummary[] - activeAccountId: string | null - activeAccountIdsByRuntime?: CodexManagedAccountRuntimeSelection - /** Resolved identity of the host system-default (real ~/.codex) account. - * Omitted for runtimes where it is not resolved (e.g. per-distro WSL). */ - systemDefault?: CodexSystemDefaultIdentity -} - -export type CodexManagedAccountRuntimeSelection = { - host: string | null - wsl: Record -} - -export type ClaudeManagedAccount = { - id: string - email: string - managedAuthPath: string - managedAuthRuntime?: 'host' | 'wsl' - wslDistro?: string | null - wslLinuxAuthPath?: string | null - authMethod: 'subscription-oauth' | 'unknown' - organizationUuid?: string | null - organizationName?: string | null - createdAt: number - updatedAt: number - lastAuthenticatedAt: number -} - -export type ClaudeManagedAccountSummary = { - id: string - email: string - managedAuthRuntime?: 'host' | 'wsl' - wslDistro?: string | null - authMethod: 'subscription-oauth' | 'unknown' - organizationUuid?: string | null - organizationName?: string | null - createdAt: number - updatedAt: number - lastAuthenticatedAt: number -} - -export type ClaudeRateLimitAccountsState = { - accounts: ClaudeManagedAccountSummary[] - activeAccountId: string | null - activeAccountIdsByRuntime?: ClaudeManagedAccountRuntimeSelection -} - -export type ClaudeManagedAccountRuntimeSelection = { - host: string | null - wsl: Record -} - -/** All AI coding agents Orca knows how to launch. Used for the agent picker in the new-workspace - * flow and for the default-agent setting. Extend this union as new agents are added. */ -export type TuiAgent = - | 'claude' // Claude Code - | 'claude-agent-teams' // Claude Code Agent Teams via Orca native panes - | 'openclaude' // OpenClaude - | 'codex' // OpenAI Codex - | 'autohand' // Autohand Code CLI - | 'opencode' // OpenCode - | 'mimo-code' - | 'pi' // Pi (pi.dev) - | 'omp' // OMP (omp.sh) - | 'gemini' // Gemini CLI - | 'antigravity' // Google Antigravity CLI - | 'aider' // Aider - | 'goose' // Goose - | 'amp' // Amp - | 'kilo' // Kilocode - | 'kiro' // Kiro - | 'crush' // Charm/Crush - | 'aug' // Augment/Auggie - | 'cline' // Cline - | 'codebuff' // Codebuff - | 'command-code' // Command Code - | 'continue' // Continue - | 'cursor' // Cursor - | 'droid' // Factory Droid - | 'kimi' // Kimi - | 'mistral-vibe' // Mistral Vibe - | 'qwen-code' // Qwen Code - | 'rovo' // Rovo Dev - | 'hermes' // Hermes Agent - | 'openclaw' // OpenClaw - | 'copilot' // GitHub Copilot CLI - | 'grok' // xAI Grok CLI - | 'devin' // Devin CLI - | 'ante' // Ante (Antigma Labs) - | 'trae' // Trae CLI - | 'prime-agent' // Prime Agent (Prime Intellect) - -export type TaskViewPresetId = 'all' | 'issues' | 'review' | 'my-issues' | 'my-prs' | 'prs' - -/** Where the repo setup script runs when a worktree is created. - * - 'new-tab': open a background tab titled "Setup" and leave focus on the first tab (default). - * - 'split-vertical': split the initial terminal pane with a vertical divider. - * - 'split-horizontal': split the initial terminal pane with a horizontal divider. */ -export type SetupScriptLaunchMode = 'split-vertical' | 'split-horizontal' | 'new-tab' - -/** Direction used when the setup script launch mode is a split. */ -export type SetupSplitDirection = 'vertical' | 'horizontal' - -export type TerminalColorOverrides = { - foreground?: string - background?: string - cursor?: string - cursorAccent?: string - selectionBackground?: string - selectionForeground?: string - black?: string - red?: string - green?: string - yellow?: string - blue?: string - magenta?: string - cyan?: string - white?: string - brightBlack?: string - brightRed?: string - brightGreen?: string - brightYellow?: string - brightBlue?: string - brightMagenta?: string - brightCyan?: string - brightWhite?: string - // Why: xterm.js ITheme does not expose a `bold` key, but Ghostty users - // expect the setting to be preserved so a future renderer CSS override - // or xterm upgrade can honour it without a migration. - bold?: string -} - -export type TerminalQuickCommandScope = - | { - type: 'global' - } - | { - type: 'repo' - repoId: string - } - -export type TerminalQuickCommandAction = 'terminal-command' | 'agent-prompt' - -export type TerminalQuickCommandBase = { - id: string - label: string - scope?: TerminalQuickCommandScope -} - -export type TerminalCommandQuickCommand = TerminalQuickCommandBase & { - action?: 'terminal-command' - command: string - appendEnter: boolean -} - -export type TerminalAgentQuickCommand = TerminalQuickCommandBase & { - action: 'agent-prompt' - agent: TuiAgent - prompt: string -} - -export type TerminalQuickCommand = TerminalCommandQuickCommand | TerminalAgentQuickCommand - -export type OpenInApplication = { - id: string - label: string - command: string -} - -export type SourceControlViewMode = 'list' | 'tree' -export type SourceControlGroupOrder = 'changes-first' | 'staged-first' | 'untracked-first' - -export type LeftSidebarAppearanceMode = 'default' | 'match-terminal' | 'tinted' - -/** Strategy for the prefix prepended to worktree branch names. */ -export type BranchPrefixStrategy = 'git-username' | 'custom' | 'none' - -export type FloatingTerminalCwdRequest = { - path?: string - requireTrusted?: boolean -} - -/** Per-host overrides for client preferences that genuinely vary by execution - * host. NARROW by design: only settings whose value is meaningless to share - * across hosts belong here. - * - `displayLabel`: a client-side rename for the host shown in sidebar/pickers. - * - `defaultWorktreeLocation`: the host's root worktree directory; a remote - * SSH/runtime host has a different filesystem layout than the local Mac, so - * the client `workspaceDir` default cannot apply unchanged. */ -export type HostSettingOverrides = { - displayLabel?: string - defaultWorktreeLocation?: string -} - -/** Presentation mode for the experimental Agent Dashboard. */ -export type AgentDashboardMode = 'in-window' | 'popout' - -export type GlobalSettings = { - workspaceDir: string - /** Per-host overrides keyed by ExecutionHostId. Effective value for a - * host-varying setting is `host override ?? client default`. */ - hostSettingOverrides?: Partial> - nestWorkspaces: boolean - workspaceDirHistory?: OrcaWorkspaceLayout[] - refreshLocalBaseRefOnWorktreeCreate: boolean - /** Set once the user dismisses the "local main is behind" suggestion toast, so - * the nudge to enable refreshLocalBaseRefOnWorktreeCreate never shows again. */ - localBaseRefSuggestionDismissed: boolean - /** When enabled, Orca renames a workspace's auto-generated creature branch to - * a short name derived from the first prompt once work begins. Users can - * still turn this off from global Git settings. */ - autoRenameBranchFromWork: boolean - /** One-shot migration guard for the default-on rollout. Existing profiles - * without the guard are flipped on once; later explicit opt-outs stick. */ - autoRenameBranchFromWorkDefaultedOn?: boolean - branchPrefix: BranchPrefixStrategy - branchPrefixCustom: string - theme: 'system' | 'dark' | 'light' - /** Controls the left sidebar surface without changing terminal brightness. */ - leftSidebarAppearanceMode: LeftSidebarAppearanceMode - leftSidebarTintColor?: string - leftSidebarTintOpacity?: number - uiLanguage: UiLanguage - appIcon: AppIconId - appFontFamily: string - editorAutoSave: boolean - editorAutoSaveDelayMs: number - editorMinimapEnabled: boolean - /** Opt-in code-editor font; empty (the default) keeps following `terminalFontFamily`. */ - editorFontFamily?: string - /** Defaults on for profiles saved before file-editor wrapping became configurable. */ - editorWordWrap?: boolean - /** Persisted opt-out for browser spellcheck noise in rich Markdown editing surfaces. */ - richMarkdownSpellcheckEnabled?: boolean - /** Whether local markdown review note controls and the review panel are shown. */ - markdownReviewToolsEnabled: boolean - /** Why: mirrors terminal selection-paste muscle memory without mutating the - * normal system clipboard; Linux and macOS enable it by default, Windows - * leaves middle-click semantics unchanged unless the user opts in. */ - primarySelectionMiddleClickPaste?: boolean - /** One-shot migration guard for turning the Linux default on for profiles - * that persisted the earlier off-by-default value. */ - primarySelectionMiddleClickPasteDefaultedForLinux?: boolean - /** One-shot migration guard for widening the terminal-style default to - * Linux/macOS while preserving later explicit opt-outs. */ - primarySelectionMiddleClickPasteDefaultedForTerminalDefaults?: boolean - terminalFontSize: number - terminalFontFamily: string - terminalFontWeight: number - terminalFontWeightBold: number - terminalLineHeight: number - terminalScrollSensitivity: number - terminalFastScrollSensitivity: number - terminalTuiScrollSensitivity: number - /** One-shot migration guard for moving inherited TUI wheel reports from 3 to 1. */ - terminalTuiScrollSensitivityDefaultedToOne?: boolean - /** Terminal renderer policy. - * - 'auto': try xterm WebGL and fall back to DOM when unsupported or risky. - * - 'on': always try xterm WebGL. - * - 'off': keep terminal rendering on xterm's DOM renderer. */ - terminalGpuAcceleration: 'auto' | 'on' | 'off' - /** Whether to enable programming-ligatures rendering via - * `@xterm/addon-ligatures`. - * - `'auto'` (default): enabled only when the configured font is known to - * ship ligatures (Fira Code, JetBrains Mono, Cascadia Code, etc.). This - * keeps the out-of-the-box experience right for users who install a - * ligature font without touching settings. - * - `'on'` / `'off'`: explicit override. Never changes when the user - * switches fonts, so "off" always stays off. */ - terminalLigatures: 'auto' | 'on' | 'off' - terminalCursorStyle: 'bar' | 'block' | 'underline' - /** One-shot migration guard for moving inherited cursor defaults to block. */ - terminalCursorStyleDefaultedToBlock?: boolean - terminalCursorBlink: boolean - terminalThemeDark: string - terminalCustomThemes?: TerminalCustomTheme[] - terminalDividerColorDark: string - terminalUseSeparateLightTheme: boolean - terminalThemeLight: string - terminalDividerColorLight: string - terminalInactivePaneOpacity: number - terminalActivePaneOpacity: number - terminalPaneOpacityTransitionMs: number - terminalDividerThicknessPx: number - terminalBackgroundOpacity?: number - terminalColorOverrides?: TerminalColorOverrides - terminalPaddingX?: number - terminalPaddingY?: number - terminalMouseHideWhileTyping?: boolean - terminalWordSeparator?: string - terminalCursorOpacity?: number - terminalQuickCommands?: TerminalQuickCommand[] - windowBackgroundBlur?: boolean - /** Windows-only: close (X) hides to tray instead of quitting; the tray icon is always present regardless. */ - minimizeToTrayOnClose?: boolean - /** macOS: toggles the additive menu-bar entry (Orca survives last-window close); doesn't change Dock behavior. */ - showMenuBarIcon?: boolean - /** Windows convention: right-click pastes; macOS/Linux keep the context menu. */ - terminalRightClickToPaste: boolean - /** One-shot guard distinguishing the old global true default from a per-platform choice. */ - terminalRightClickToPasteDefaultedForPlatform?: boolean - /** Windows-only: COMSPEC always points to cmd.exe, so this explicit shell (default 'powershell.exe') overrides it. */ - terminalWindowsShell: string - /** Pins the WSL distro for terminals/agent scans instead of WSL's current global default. */ - terminalWindowsWslDistro?: string | null - /** Account/auth location; auto follows the global Windows runtime while host/wsl pin it. */ - localAccountRuntime: 'auto' | 'host' | 'wsl' - localAccountWslDistro?: string | null - /** One-shot guard for migrating the legacy host default to auto. */ - localAccountRuntimeDefaultedToAutoForAllUsers?: boolean - /** Independent from the terminal shell so users can inspect Windows vs WSL agent PATH state without changing it. */ - localAgentRuntime?: 'host' | 'wsl' - localAgentWslDistro?: string | null - /** Why: global is only the default policy; project-level runtime preference wins. */ - localWindowsRuntimeDefault: GlobalWindowsRuntimeDefault - /** 'auto' resolves to PowerShell 7+ when present, else falls back to inbox Windows PowerShell. */ - terminalWindowsPowerShellImplementation: 'auto' | 'powershell.exe' | 'pwsh.exe' - terminalFocusFollowsMouse: boolean - /** X11/gnome-terminal "copy on select": selecting text auto-copies to the clipboard; default off. */ - terminalClipboardOnSelect: boolean - /** Enables OSC 52 clipboard writes for TUIs (tmux/Zellij/nvim, incl. over SSH); default on. Clipboard *queries* stay blocked and payload size is capped, so this is write-only exposure. */ - terminalAllowOsc52Clipboard: boolean - /** One-shot stamp: profiles saved under the old off default get flipped on once, after which an explicit opt-out sticks. */ - terminalAllowOsc52ClipboardDefaultedOnForAllUsers?: boolean - /** Experimental Claude Agent Teams; native panes use a tmux-compatible shim so teammate output stays on the normal PTY path. */ - claudeAgentTeamsMode?: ClaudeAgentTeamsMode - /** Where the repo setup script runs on workspace create; defaults to a background "Setup" tab to keep the main terminal usable. */ - setupScriptLaunchMode: SetupScriptLaunchMode - terminalScrollbackRows: number - /** Optional app-level proxy for Electron networking and local PTYs; empty preserves system/inherited proxy env. */ - httpProxyUrl?: string - /** Optional semicolon/comma/newline-separated bypass rules for httpProxyUrl. */ - httpProxyBypassRules?: string - /** Why: corporate TLS-intercepting proxies can break HTTP/2 downloads; opt-in Chromium process-wide HTTP/1.1 switch. */ - electronHttp1CompatibilityMode?: boolean - /** Opt-in in-app browsing (isolated guest surface); default keeps links opening in the system browser. */ - openLinksInApp: boolean - /** Worktree-scoped localhost hostnames to distinguish tabs; opt-in since a non-localhost host can break apps binding cookies/sessions to localhost. */ - localhostWorktreeLabelsEnabled?: boolean - /** Tracks the one-time first-use prompt for terminal link routing (avoid silently changing where links open). */ - openLinksInAppPreferencePrompted: boolean - /** Opt-in: Shift+modifier click inverts openLinksInApp instead of always forcing the system browser. Off keeps the historical one-way escape hatch. */ - openLinksInAppModifierInverts?: boolean - /** Show terminal link actions on plain click; off restores modifier-click-only terminal links. */ - terminalLinkActionPopoverEnabled?: boolean - /** Opt-in: open new coding-agent tabs in native chat instead of the raw terminal; optional for legacy settings. */ - openAgentTabsInChatByDefault?: boolean - /** Experimental native chat surface for Claude/Codex sessions; off by default. */ - experimentalNativeChat?: boolean - /** Last explicit native-chat model + option selections; live panes need an applied/dispatched record before showing a value. */ - nativeChatSessionOptions?: PersistedNativeChatSessionOptions - /** Extra launcher rows for the worktree "Open in" submenu. VS Code is always shown first. */ - openInApplications?: OpenInApplication[] - /** Deprecated: migration/backward-compat only. Use PersistedUIState.rightSidebarOpen. */ - rightSidebarOpenByDefault: boolean - showGitIgnoredFiles?: boolean - /** Preferred Source Control changes layout. Per-user, not per-workspace. */ - sourceControlViewMode: SourceControlViewMode - /** Preferred Source Control group order. Per-user, not per-workspace. */ - sourceControlGroupOrder: SourceControlGroupOrder - /** Compare base defaults to the branch upstream instead of the repo default; affects only the compare/diff view, not the PR/rebase target. Per-user. */ - sourceControlCompareAgainstUpstream: boolean - /** Whether to show the Orca app name in the titlebar. */ - showTitlebarAppName: boolean - /** Hides the Tasks sidebar button (also removes it from keyboard navigation). */ - showTasksButton: boolean - /** Only toggles the sidebar shortcut; Automations stay reachable from Settings/View menu. */ - showAutomationsButton?: boolean - /** Deprecated: Artifacts are always available. Use showArtifactsButton for sidebar visibility. */ - artifactsEnabled?: boolean - /** Capability gate for agent-driven publishing; off until granted, enforced in main, not just the UI. */ - artifactSharingEnabled?: boolean - /** Only toggles the sidebar shortcut; Artifacts stay reachable from Settings. */ - showArtifactsButton?: boolean - /** Only toggles the sidebar shortcut; Orca Mobile stays reachable from Settings. */ - showMobileButton?: boolean - /** Pinned workspaces show in one sidebar location by default; opt in to also show them in their natural groups. */ - showPinnedWorktreesInGroups?: boolean - /** How Ctrl+Tab picks the next visible tab; optional (older profiles), readers default to MRU. */ - ctrlTabOrderMode?: CtrlTabOrderMode - /** Orca-first keeps app shortcuts from TUIs; terminal-first is opt-in to let shell/TUI bindings win. */ - terminalShortcutPolicy?: TerminalShortcutPolicy - /** Floating Workspace: global surface for terminal/browser/markdown tabs outside repo/worktree context. */ - floatingTerminalEnabled: boolean - /** One-shot migration flag for the floating-workspace default-on rollout; after migration an explicit off sticks. */ - floatingTerminalDefaultedForAllUsers?: boolean - /** Start dir for new floating-workspace terminal tabs; empty or '~' = home dir. */ - floatingTerminalCwd: string - /** Picker-approved floating-workspace dirs reauthorized across restarts; renderer text alone must not populate this. */ - floatingTerminalTrustedCwds?: string[] - /** One-shot migration marker for legacy floating workspace cwd trust grants. */ - floatingTerminalCwdMigratedToAppWorkspace?: boolean - /** Where the Floating Workspace toggle is shown; defaults to the floating button for discoverability. */ - floatingTerminalTriggerLocation: FloatingTerminalTriggerLocation - /** Legacy keyboard-shortcut overrides; new writes go to ~/.orca/keybindings.json, migrated once when present. */ - keybindings?: KeybindingOverrides - diffDefaultView: 'inline' | 'side-by-side' - diffWordWrap: boolean - combinedDiffFileTreeVisibleByDefault: boolean - /** Bot-marked comment-author logins (stored lowercased); escape hatch for review bots on regular accounts that defeat provider metadata/heuristics. */ - prBotAuthorOverrides: string[] - notifications: NotificationSettings - /** Countdown after a Claude agent goes idle showing time left before the prompt cache expires. */ - promptCacheTimerEnabled: boolean - /** Prompt-cache TTL (ms); only 300000 (5 min standard) or 3600000 (1 hr, extended-TTL plans). */ - promptCacheTtlMs: number - /** Why: durable main-owned pref so Orca can prepare shared ~/.codex before the renderer hydrates. */ - codexManagedAccounts: CodexManagedAccount[] - activeCodexManagedAccountId: string | null - activeCodexManagedAccountIdsByRuntime?: CodexManagedAccountRuntimeSelection - /** Why: persist only per-account auth (not a CLAUDE_CONFIG_DIR swap) so switching accounts doesn't fork Claude's shared chat/session context. */ - claudeManagedAccounts: ClaudeManagedAccount[] - activeClaudeManagedAccountId: string | null - activeClaudeManagedAccountIdsByRuntime?: ClaudeManagedAccountRuntimeSelection - /** Per-worktree shell history file so ArrowUp doesn't surface other worktrees' commands. Defaults to true. */ - terminalScopeHistoryByWorktree: boolean - /** Kill switch for hidden terminal view parking: unmount long-hidden panes while a pane-less watcher keeps PTY side effects alive. */ - terminalHiddenViewParking?: boolean - /** Kill switch for SSH terminal parking (C1): SSH panes park like local ones; reveal restores from main's headless model, falling back to relay replay. */ - terminalSshViewParking?: boolean - /** Kill switch for the hidden-worktree retention budget (C1): force-parks the least-recently-hidden un-parkable worktrees beyond a count budget or TTL. */ - terminalHiddenWorktreeRetentionBudget?: boolean - /** Kill switch for the browser-guest worktree retention budget: destroys the least-recently-activated hidden worktrees' webview guests beyond an LRU count budget. */ - browserGuestWorktreeRetentionBudget?: boolean - /** Kill switch for main-process PTY side-effect authority; on (default) = title/bell/agent facts via pty:sideEffect channel, not renderer byte parsing. */ - terminalMainSideEffectAuthority?: boolean - /** Kill switch for main's hidden-delivery gate (Phase 4): drops PTY bytes to hidden views after model ingestion; requires terminalMainSideEffectAuthority. */ - terminalHiddenDeliveryGate?: boolean - /** Kill switch for main's model query responder (Phase 5); active only when both Phase-4 gates are also on. */ - terminalModelQueryAuthority?: boolean - /** Which agent to pre-select in the new-workspace composer. - * - null: auto (first detected agent) - * - 'blank': blank terminal (no agent launched) - * - TuiAgent: a specific agent id */ - defaultTuiAgent: TuiAgent | 'blank' | null - /** Agents hidden from picker/auto-launch; detection stays a raw PATH snapshot. */ - disabledTuiAgents: TuiAgent[] - /** Master switch for the experimental plugin system. Off by default: no - * discovery, no panels, no plugin code paths run at all. */ - pluginSystemEnabled: boolean - /** Qualified plugin keys (`publisher.id`) the user disabled. Discovered - * plugins stay listed but are not activated. */ - disabledPlugins: string[] - /** Consent records: qualified plugin key → capability/worker-trust fingerprint. - * A plugin whose current fingerprint differs is pending again, so an update - * crossing either trust boundary re-prompts before code runs. Absent key = - * never consented. */ - pluginConsents: Record - /** Local directories loaded as dev-mode plugins (manifest hot-reload). */ - devPluginPaths: string[] - /** One-shot guard: start Claude Agent Teams hidden for existing profiles without overriding later opt-ins. */ - claudeAgentTeamsDefaultDisabledMigrated?: boolean - /** Why: worktree deletion is destructive (rm -rf of the working dir), so confirm by default. */ - skipDeleteWorktreeConfirm: boolean - /** Why: closing a terminal with child processes kills foreground work; keep this skip separate from other confirmations. */ - skipCloseTerminalWithRunningProcessConfirm: boolean - /** Why: deleting an automation also deletes its run history; keep this skip separate from worktree deletion. */ - skipDeleteAutomationConfirm: boolean - /** Why: deleting an artifact breaks a public link others may already hold; keep this skip separate from local deletions. */ - skipDeleteArtifactConfirm: boolean - /** Why: a Codex rate-limit reset spends a scarce credit on the live account; keep this skip separate from local confirmations. */ - skipCodexRateLimitResetConfirm: boolean - /** Default preset in the new-workspace GitHub task view. */ - defaultTaskViewPreset: TaskViewPresetId - /** Persisted last-used task source so Tasks reopens to the same provider instead of defaulting to GitHub. */ - defaultTaskSource: TaskProvider - /** Persisted visible task providers; hides unused providers from Tasks chrome and sidebar shortcuts. */ - visibleTaskProviders: TaskProvider[] - /** Why: one-shot guard to make Jira visible for existing profiles once, without re-adding after a later opt-out. */ - visibleTaskProvidersDefaultedForJira: boolean - /** Persisted repo selection (cross-repo tasks view). null = sticky-all (includes future-added repos); - * string[] = frozen curated subset (ineligible ids dropped on load; empty after drop is treated as null). */ - defaultRepoSelection: string[] | null - /** Persisted Linear team selection (tasks view). Same nullable-array pattern as - * defaultRepoSelection: null = sticky-all, string[] = frozen subset of team IDs. */ - defaultLinearTeamSelection: string[] | null - /** Session cookie for OpenCode Go rate-limit fetching. Stored encrypted. */ - opencodeSessionCookie: string - /** Optional OpenCode Go workspace ID override; when set, skips the workspaces lookup and fetches usage directly. */ - opencodeWorkspaceId: string - /** Optional MiniMax group id. When empty, the usage fetcher extracts minimax_group_id_v2 from the cookie. */ - minimaxGroupId: string - /** Comma-separated MiniMax model names to show in the status bar usage window. */ - minimaxUsageModels: string - /** Extract OAuth credentials from the local Gemini CLI for rate-limit fetching. Off by default (explicit opt-in). */ - geminiCliOAuthEnabled: boolean - /** Per-agent CLI command overrides. A missing key means use the catalog default binary name. */ - agentCmdOverrides: Partial> - /** Custom CODEX_HOME for Codex session-history discovery (defaults to ~/.codex). - * History-only: does not change which account/config/hooks Orca uses. */ - codexSessionSourceHome?: { - /** Absolute host path; empty/undefined falls back to ~/.codex. */ - host?: string - /** Per-WSL-distro absolute Linux path; missing distro falls back to /.codex. */ - wsl?: Record - } - /** Per-agent default CLI arguments appended after the binary/path and before prompts. */ - agentDefaultArgs?: Partial> - /** Per-agent launch environment defaults used when yolo mode is exposed as env. */ - agentDefaultEnv?: Partial>> - /** One-shot guard for adding yolo-mode default args to untouched agent launch profiles. */ - agentYoloDefaultsMigrated?: boolean - /** Why: disabling must persist so startup doesn't reinstall global agent hook entries the user just removed. */ - agentStatusHooksEnabled: boolean - /** Dismissed freshness tuples: no write authority, just suppress re-nudging the same official placement/revision. */ - dismissedSkillFreshnessNudges?: string[] - /** Why: generated tab titles are subjective, so they stay opt-in and manual renames win. */ - tabAutoGenerateTitle: boolean - /** Why: pinned tabs can still be closed via keyboard/native-menu; this gates that behind a confirmation. Defaults on. */ - confirmClosePinnedTab: boolean - /** When true, Orca requests local awake assertions while hook-reported agents are working. */ - keepComputerAwakeWhileAgentsRun: boolean - /** Optional for mixed-version compatibility; the legacy boolean maps true to Auto. */ - computerAwakeMode?: ComputerAwakeMode - /** macOS Option key: compose layout chars (@ German, € French) vs act as Meta/Esc for readline. - * 'auto' (default) = layout-aware via navigator.keyboard.getLayoutMap() (US → Meta, else compose); - * 'false' = compose; 'true' = Meta on both Option keys; 'left'/'right' = only that key is Meta. - * See docs/terminal-option-key-layout-aware-default.md. */ - terminalMacOptionAsAlt: 'auto' | 'true' | 'false' | 'left' | 'right' - /** One-shot migration guard for the 'auto' rollout. Old default 'true' was ambiguous (explicit vs default); - * on first upgrade launch, reset a persisted 'true' to 'auto' so non-US keyboards aren't broken by the stale default. */ - terminalMacOptionAsAltMigrated: boolean - /** Whether macOS terminal input maps the physical JIS Yen (¥) key to backslash, per common terminal expectation. */ - terminalJISYenToBackslash: boolean - experimentalMobile: boolean - /** Why: iOS Simulator is default-on for capable macOS hosts; this is the durable off switch (hides UI, blocks CLI attach). */ - mobileEmulatorEnabled?: boolean - /** Preferred iOS Simulator UDID for UI auto-attach and agent CLI attach. */ - mobileEmulatorDefaultDeviceUdid?: string | null - /** Explicit Android SDK root for when auto-discovery (ANDROID_HOME / default path) fails; null (default) auto-discovers. */ - androidSdkPath?: string | null - /** Auto-restore window (ms) for a phone-fit PTY after the last mobile subscriber leaves. - * `null` (default) holds phone size indefinitely; a finite value schedules restore. - * Clamped on read to [5_000ms, 60min]. See docs/mobile-fit-hold.md. */ - mobileAutoRestoreFitMs: number | null - /** Preferred mobile pairing path for new QR codes. Missing/'automatic' = Anywhere (Relay + local); - * explicit 'local-only' = same-network only. */ - mobilePairingConnectionMode?: 'automatic' | 'local-only' - /** Explicit custom address restored when generating future mobile pairing codes. */ - mobilePairingCustomAddress?: string | null - /** Saved custom addresses available in both mobile pairing pickers. */ - mobilePairingCustomAddresses?: string[] - /** Experimental: floating animated pet in the bottom-right corner. Opt-in cosmetic; - * off never mounts the overlay, and toggling takes effect instantly (renderer-side). */ - experimentalPet: boolean - /** Legacy persisted key from before the sidekick -> pet rename; read only during migration, new writes use experimentalPet. */ - experimentalSidekick?: boolean - /** Experimental: left-sidebar Agents view — threaded feed of agent completions, blocking/unread state, worktree creation. */ - experimentalActivity: boolean - /** Experimental: pop-out Kanban dashboard for monitoring and opening agent terminals across worktrees. */ - experimentalAgentDashboardPopout?: boolean - /** How the Agent Dashboard opens: an in-window companion board or a separate pop-out window. Defaults to in-window. */ - experimentalAgentDashboardMode?: AgentDashboardMode - /** Includes stale quiet agents as a fourth Agent Dashboard column. */ - experimentalAgentDashboardShowIdle?: boolean - /** One-shot migration guard for defaulting the Agents view off; later explicit opt-ins persist normally. */ - experimentalActivityDefaultedOffForAllUsers?: boolean - /** Experimental: persistent terminal-pane attention ring for bell + agent-completion events. Opt-in while tuning signal/noise. */ - experimentalTerminalAttention: boolean - /** Experimental: automatically sleep completed, resumable background agent terminals. */ - experimentalAgentHibernation?: boolean - /** Milliseconds a completed agent must stay idle before hibernation can be considered. */ - agentHibernationIdleMs?: number - /** Experimental: opt-in preview of the updated worktree-card layout and metadata behavior. */ - experimentalNewWorktreeCardStyle?: boolean - /** Experimental: per-workspace on-demand environment recipes and setup surface. */ - experimentalEphemeralVms?: boolean - /** Compact worktree cards: hide the metadata row when title and branch say the same thing. */ - compactWorktreeCards: boolean - /** Legacy persisted key from the Experimental rollout; new writes use compactWorktreeCards. */ - experimentalCompactWorktreeCards?: boolean - /** Active non-local runtime environment for client-routed RPC; null keeps local desktop behavior. */ - activeRuntimeEnvironmentId?: string | null - /** GitHub Project mode state (pinned/recent/active project, last view per project). - * Optional for pre-feature profiles; the persistence merge hydrates the default. */ - githubProjects?: GitHubProjectSettings - /** AI commit-message config (agent, model, per-model thinking, prompt suffix). Optional to avoid migrating existing profiles. */ - commitMessageAi?: CommitMessageAiSettings - /** Source-control AI generation settings for commit messages and hosted-review drafts. */ - sourceControlAi?: SourceControlAiSettings - /** GitLab project preferences (pinned + recent paths). Optional for pre-GitLab profiles; persistence merge fills the default. */ - gitlabProjects?: GitLabProjectSettings - /** Anonymous product-telemetry state; optional until the one-shot Store.load() migration populates it. - * Holds only consent + identity, not volatile counters — those would amplify the debounced settings write. */ - telemetry?: { - /** New users: true at install. Existing users: null until they resolve the first-launch banner. */ - optedIn: boolean | null - /** Anonymous UUID v4. Generated on first run. Stable across launches; not surfaced in the UI. */ - installId: string - /** Cohort marker: true for pre-existing profiles (gates the opt-in banner), false for fresh installs. */ - existedBeforeTelemetryRelease: boolean - } - /** One-shot cohort marker for the tab-switch keybinding swap. 'pending' = - * pre-existing install (seed pins old chords, then flips to 'done'); 'done' = fresh install. */ - tabSwitchKeybindingSeed?: 'pending' | 'done' - /** Local voice/dictation config. Optional for pre-voice profiles; getDefaultSettings() hydrates defaults via the persistence merge. */ - voice?: VoiceSettings -} - -export type OrcaWorkspaceLayout = { - path: string - nestWorkspaces: boolean -} - -export type CommitMessageAiModelCapability = { - id: string - label: string - thinkingLevels?: { id: string; label: string }[] - defaultThinkingLevel?: string -} - -export type CommitMessageAiSettings = { - enabled: boolean - /** A TuiAgent id, the literal `'custom'` for a user-supplied command, or null. */ - agentId: TuiAgent | 'custom' | null - /** Per-agent: switching agents preserves the previously-picked model. */ - selectedModelByAgent: Partial> - /** Host-scoped model selections; dynamic agents can expose different models per SSH target. */ - selectedModelByAgentByHost?: Partial>>> - /** Per-agent dynamic models last discovered from the CLI, persisted so main can validate selections. */ - discoveredModelsByAgent?: Partial> - /** Host-scoped dynamic model discovery cache. */ - discoveredModelsByAgentByHost?: Partial< - Record>> - > - /** Per-model: thinking effort depends on the model, not the agent. Keyed by model id. */ - selectedThinkingByModel: Record - /** Optional user-provided suffix appended to the base prompt (style overrides, etc.). */ - customPrompt: string - /** Command template for agentId === 'custom'; {prompt} substitutes the diff prompt via argv, else the prompt is piped via stdin. */ - customAgentCommand: string -} - -export type GhosttyImportPreview = { - found: boolean - configPath?: string - configPaths?: string[] - diff: Partial - unsupportedKeys: string[] - error?: string -} - -// Subset of onboarding Ghostty DiscoveryState statuses that emit telemetry; UI-only 'idle'/'detecting' don't. -export type DiscoveryStatusEmitted = 'found' | 'absent' | 'imported' - -export type NotificationEventSource = 'agent-task-complete' | 'terminal-bell' | 'test' - -export type NotificationDispatchRequest = { - source: NotificationEventSource - notificationId?: string - /** Why: useful for fast native failures, but macOS can still drop notifications after 'show'. */ - requireDisplayConfirmation?: boolean - worktreeId?: string - /** Stable `${tabId}:${leafId}` terminal pane key for click-to-focus routing. */ - paneKey?: string - repoLabel?: string - worktreeLabel?: string - hasMultipleActiveRepos?: boolean - terminalTitle?: string - isActiveWorktree?: boolean - agentType?: AgentType - agentState?: AgentStatusState - agentPrompt?: string - agentToolName?: string - agentToolInput?: string - agentLastAssistantMessage?: string - agentInterrupted?: boolean -} - -export type NotificationDispatchResult = { - delivered: boolean - /** Why delivery was skipped (set when delivered is false); 'blocked-by-system' = macOS would silently swallow it. */ - reason?: - | 'disabled' - | 'source-disabled' - | 'suppressed-focus' - | 'cooldown' - | 'not-supported' - | 'not-displayed' - | 'blocked-by-system' - | 'invalid-request' -} - -export type NotificationDismissResult = { - dismissed: number -} - -export type NotificationSoundResult = { - played: boolean - reason?: - | 'missing-path' - | 'invalid-path' - | 'unsupported-type' - | 'too-large' - | 'read-failed' - | 'playback-failed' - | 'deduped' -} - -export type NotificationSoundDataResult = - | { - ok: true - data: Uint8Array - mimeType: string - path: string - } - | { - ok: false - reason: Exclude - } - -export type NotificationSoundPathResult = - | { ok: true; path: string } - | { ok: false; reason: 'missing-path' | 'invalid-path' | 'unsupported-type' } - -export type OnboardingOutcome = 'completed' | 'dismissed' - -export type OnboardingChecklistState = { - addedRepo: boolean - choseAgent: boolean - ranFirstAgent: boolean - ranSecondAgentOnSameTask: boolean - triedCmdJ: boolean - shapedSidebar: boolean - reviewedDiff: boolean - openedPr: boolean - addedFolder: boolean - openedFile: boolean - ranAgentOnFile: boolean - // Why: UI state flag (panel visibility), not an activation event; telemetry checklist enum omits it. - dismissed: boolean -} - -export type OnboardingState = { - // Why: step meanings change when pages are removed; version marker prevents migration re-running on new progress. - flowVersion: number - closedAt: number | null - outcome: OnboardingOutcome | null - // Sentinel -1 = not started; 1..5 = highest finished wizard step. number (not union) because callers clamp via Math.max/min. - lastCompletedStep: number - checklist: OnboardingChecklistState -} - -export type NotificationPermissionStatusResult = { - supported: boolean - platform: NodeJS.Platform - requested: boolean -} - -/** macOS notification permission outcome: authoritative native UNUserNotificationCenter readout, else a weaker - * delivery-probe fallback; 'awaiting-decision' = permission dialog unanswered. */ -export type NotificationDeliveryProbeResult = { - state: 'delivered' | 'blocked' | 'awaiting-decision' | 'unsupported' - /** True when the state comes from the native authorization readout (vs. the delivery-probe fallback). */ - authoritative: boolean -} - -export type WorktreeCardProperty = - | 'status' - | 'unread' - // Legacy persisted preference. CI status is now represented by linked PR metadata. - | 'ci' - // Migration-only: legacy detailed cards showed branch identity as a visible row. - | 'branch' - // Task metadata on workspace cards; provider-specific persisted values kept for older profiles. - | 'issue' - | 'linear-issue' - | 'jira-issue' - | 'pr' - | 'automation' - // Badge marking workspaces created through `orca worktree create`. - | 'cli' - | 'comment' - | 'ports' - // Inline agent-activity list rendered in each workspace card; on by default (see DEFAULT_WORKTREE_CARD_PROPERTIES in shared/constants.ts). - | 'inline-agents' - -export type WorktreeCardMode = 'Default' | 'Compact' - -export type AgentActivityDisplayMode = 'compact' | 'full' - -export type StatusBarItem = - | 'claude' - | 'codex' - | 'gemini' - | 'antigravity' - | 'opencode-go' - | 'kimi' - | 'minimax' - | 'grok' - | 'ssh' - | 'resource-usage' - | 'ports' -export type FloatingTerminalTriggerLocation = 'floating-button' | 'status-bar' - -export type TaskResumeState = { - githubMode?: 'items' | 'project' - githubItemsPreset?: TaskViewPresetId | null - githubItemsQuery?: string - githubProjectHiddenFieldIdsByView?: Record - linearMode?: 'issues' | 'projects' | 'views' | 'in-orca' - linearPreset?: 'assigned' | 'created' | 'all' | 'completed' - linearQuery?: string - linearContext?: { - kind: 'project' | 'view' - id: string - workspaceId: LinearConcreteWorkspaceId - model?: LinearCustomViewModel - } - jiraPreset?: 'assigned' | 'reported' | 'all' | 'done' - jiraQuery?: string -} - -export type RightSidebarTab = - | 'explorer' - | 'search' - | 'vault' - | 'workspaces' - | 'pr-checks' - | 'source-control' - | 'checks' - | 'ports' - // Plugin-contributed panels are keyed `plugin:/` so the - // static union stays closed while plugin tabs remain type-representable. - | `plugin:${string}` -export type ActiveRightSidebarTab = Exclude -export type RightSidebarExplorerView = 'files' | 'search' - -export type ProjectOrderBy = 'manual' | 'recent' -export type WorkspaceHostScope = 'all' | 'local' | `ssh:${string}` | `runtime:${string}` -export type VisibleWorkspaceHostIds = Exclude[] | null -export type WorkspaceHostOrder = Exclude[] -export type ManualRepoOrderEntry = { - hostId: WorkspaceHostOrder[number] - repoId: string -} - -/** The active top-level section shown in the main content area. */ -export type TopLevelView = - | 'terminal' - | 'settings' - | 'tasks' - | 'activity' - | 'automations' - | 'space' - | 'artifacts' - | 'mobile' - -export type PersistedUIState = { - lastActiveRepoId: string | null - lastActiveWorktreeId: string | null - /** Active top-level view at save time, restored on relaunch; sanitized to 'terminal' if unknown or now-gated. */ - activeView: TopLevelView - sidebarWidth: number - rightSidebarOpen: boolean - rightSidebarTab: RightSidebarTab - rightSidebarExplorerView: RightSidebarExplorerView - rightSidebarWidth: number - markdownTocPanelWidth?: number - combinedDiffFileTreeWidth?: number - groupBy: 'none' | 'workspace-status' | 'repo' | 'pr-status' - sortBy: 'name' | 'smart' | 'recent' | 'repo' | 'manual' - /** Project header ordering in `groupBy: 'repo'`, independent of `sortBy`: 'manual' uses persisted order + header drag, 'recent' by latest visible activity. */ - projectOrderBy: ProjectOrderBy - /** Deprecated; the Active only filter is retired and ignored on hydration. */ - showActiveOnly: boolean - /** Hide sleeping/inactive workspaces from workspace navigation. Off by default. */ - hideSleepingWorkspaces?: boolean - /** Which execution hosts the sidebar shows; `all` = mixed view, specific IDs focus without tearing down other hosts' sessions. */ - workspaceHostScope?: WorkspaceHostScope - /** Which execution hosts the sidebar shows; `null` = sticky all-hosts so new hosts appear automatically. */ - visibleWorkspaceHostIds?: VisibleWorkspaceHostIds - /** User-defined sidebar order for host sections; missing/new hosts append in discovered order. */ - workspaceHostOrder?: WorkspaceHostOrder - /** Desktop-owned all-host repo order; host-qualified identities keep a manual cross-host interleaving while each host owns its local permutation. */ - manualRepoOrder?: ManualRepoOrderEntry[] - /** Deprecated legacy positive-form setting. Ignored on hydration. */ - showSleepingWorkspaces?: boolean - /** Deprecated legacy name used by a short-lived build. Ignored on hydration. */ - showInactiveWorkspaces?: boolean - /** Hide the repo's checked-out branch from workspace nav (sidebar, Cmd+J); folder-mode repos are unaffected (empty-branch worktrees excluded). */ - hideDefaultBranchWorkspace: boolean - /** Hide workspaces created by automation new-per-run dispatches. */ - hideAutomationGeneratedWorkspaces?: boolean - /** Hide workspaces created through `orca worktree create`. */ - hideCliCreatedWorkspaces?: boolean - /** Hide workspaces sitting on a detached HEAD; folder workspaces (no head at all) are unaffected. */ - hideDetachedHeadWorkspaces?: boolean - /** Hide workspaces with known provenance from another paired device or the host UI. */ - hideWorkspacesFromOtherDevices?: boolean - /** Keep each project's main workspace out of the "Hide sleeping" sweep. Absent means on (#8873). */ - alwaysShowDefaultBranchWorkspace?: boolean - /** Per-worktree Explorer dotfile visibility. Missing entries inherit the default: show. */ - showDotfilesByWorktree?: Record - filterRepoIds: string[] - collapsedGroups: string[] - uiZoomLevel: number - editorFontZoomLevel: number - worktreeCardProperties: WorktreeCardProperty[] - /** One-shot migration flag for deriving card properties from the two worktree card modes. */ - _worktreeCardModeDefaulted?: boolean - agentActivityDisplayMode?: AgentActivityDisplayMode - workspaceStatuses?: WorkspaceStatusDefinition[] - workspaceBoardOpacity?: number - workspaceBoardColumnWidth?: number - syncTaskStatusFromWorkspaceBoard?: boolean - /** One-shot migration flag for a short-lived build that persisted default statuses in reverse order; once stamped, ordering is never re-inferred from IDs/labels. */ - _workspaceStatusesDefaultOrderMigrated?: boolean - /** One-shot repair flag for the exact default payload a short-lived build persisted in reverse workflow order. */ - _workspaceStatusesReorderedDefaultRepaired?: boolean - /** One-shot migration flag for default status workflow labels/visuals; only exact legacy defaults migrate, customized statuses preserved. */ - _workspaceStatusesDefaultWorkflowMigrated?: boolean - /** One-shot migration flag for the old default status visuals; once stamped, user-authored colors/icons are preserved. */ - _workspaceStatusesDefaultVisualsMigrated?: boolean - /** One-shot migration flag for adding the default-on Ports status item. */ - _portsStatusBarDefaultAdded?: boolean - /** One-shot migration flag for adding the default-on Kimi status item. */ - _kimiStatusBarDefaultAdded?: boolean - /** One-shot migration flag for adding the default-on MiniMax status item. */ - _minimaxStatusBarDefaultAdded?: boolean - /** One-shot migration flag for adding the default-on Antigravity status item. */ - _antigravityStatusBarDefaultAdded?: boolean - /** One-shot migration flag for adding the default-on Grok status item. */ - _grokStatusBarDefaultAdded?: boolean - statusBarItems: StatusBarItem[] - statusBarVisible: boolean - /** Why: this is client-side presentation, not a provider/account or execution-host setting. */ - usagePercentageDisplay?: UsagePercentageDisplay - /** Client-side footer presentation; verbose preserves the pre-roster all-window default. */ - statusBarUsageMode?: StatusBarUsageMode - dismissedUpdateVersion: string | null - lastUpdateCheckAt: number | null - /** Dev-only update channel override; absent means the build's own channel. */ - releaseChannelOverride?: ReleaseChannel | null - pendingUpdateNudgeId?: string | null - dismissedUpdateNudgeId?: string | null - /** Whether Orca already tried triggering the macOS notification permission dialog; prevents re-firing every launch. */ - notificationPermissionRequested?: boolean - /** Once the "your sessions won't be interrupted" reassurance card is seen, never show it again. */ - updateReassuranceSeen?: boolean - /** Per-paneKey "row visited" timestamps that mute seen inline-agent rows; persisted because rows survive restart, else acked rows return bold. Renderer-owned via ui:set. */ - acknowledgedAgentsByPaneKey?: Record - /** User-hidden setup-guide sidebar entry; a reversible declutter pref (Help menu stays available), not completion. */ - setupGuideSidebarDismissed?: boolean - /** One-shot marker for the browser setup-guide milestone; profiles missing it are evaluated once in the renderer (completion needs runtime probes). */ - setupGuideBrowserMilestoneMigrated?: boolean - /** Existing users who completed/dismissed the pre-browser checklist stay complete after the browser milestone is added. */ - setupGuideBrowserMilestoneLegacyComplete?: boolean - /** User-dismissed browser import toolbar hint; import stays available from Settings > Browser and the overflow menu. */ - browserImportHintHidden?: boolean - /** Why: Windows-only. Set once on first hide to tray so the "Orca is still running" notice shows only once. */ - trayMinimizeNoticeShown?: boolean - /** Set by the OSC 52 default-on migration when it overrode a persisted `false`; the renderer shows one notice and clears it. */ - osc52ClipboardDefaultOnNoticePending?: boolean - /** User dismissed the first-run Mobile Emulator intro; reversible only by re-enabling the feature in Settings. */ - mobileEmulatorTabIntroDismissed?: boolean - /** User deferred the in-pane Mobile Emulator CLI + skill setup guide. */ - mobileEmulatorAgentSetupDismissed?: boolean - /** One-shot rollout notice for manual project ordering default; absent or true keeps the sidebar callout hidden. */ - projectOrderManualDefaultNoticeDismissed?: boolean - /** One-shot notice that usage meters show percent used, not remaining; absent resolves on load (new profiles dismissed, upgraded see it once). */ - usagePercentageDisplayChangeNoticeDismissed?: boolean - /** User-hidden empty-state usage CTA; permanently hides the "Connect AI accounts" prompt even if providers are later disconnected. */ - usageEmptyStateDismissed?: boolean - /** URL for new browser tabs; null = blank tab. */ - browserDefaultUrl?: string | null - browserDefaultSearchEngine?: 'google' | 'duckduckgo' | 'bing' | 'kagi' | null - /** Electron browser zoom level applied when a new local browser tab is created. */ - browserDefaultZoomLevel?: number - /** Optional Kagi private-session link used only when Kagi is the search engine. */ - browserKagiSessionLink?: string | null - /** Saved window bounds so the app restores last position/size instead of maximizing each launch. */ - windowBounds?: { x: number; y: number; width: number; height: number } | null - /** Whether the window was maximized when it was last closed. */ - windowMaximized?: boolean - /** Saved bounds for the pop-out dashboard window so it restores to its last - * position/size. Independent of the main window's bounds. */ - dashboardPopoutBounds?: { x: number; y: number; width: number; height: number } | null - /** One-shot flag: 'recent' once meant the smart sort (v1→v2 rename), migrated to 'smart' once so the new last-activity 'recent' isn't re-clobbered. */ - _sortBySmartMigrated?: boolean - /** LEGACY inline-agents flag, stamped unconditionally every load so it can't gate migration; kept only for rollback forward-compat (real gate: _inlineAgentsDefaultedForAllUsers). */ - _inlineAgentsDefaultedForExperiment?: boolean - /** One-shot flag for the inline-agents default-on rollout; distinct from _inlineAgentsDefaultedForExperiment, which was stamped every load and is permanently dirty. */ - _inlineAgentsDefaultedForAllUsers?: boolean - /** One-shot migration flag for split-out card properties, set once so later deliberate unchecks of Linear issue/Ports stick across restarts. */ - _expandedWorktreeCardPropertiesDefaulted?: boolean - /** One-shot backfill flag for 'jira-issue', which joined the defaults after the expansion migration had already stamped upgraded profiles. */ - _jiraIssueWorktreeCardPropertyDefaulted?: boolean - /** totalAgentsSpawned snapshot at first sighting of the current app version, so the nag counts agents since last update (not from zero). */ - starNagBaselineAgents?: number | null - /** App version that set the current baseline; a version change re-captures the baseline on next spawn, restarting the nag countdown. */ - starNagAppVersion?: string | null - /** Next agents-since-baseline threshold that fires the star-nag; starts at 35, doubles per dismissal without starring. */ - starNagNextThreshold?: number - /** Once the user has starred Orca (any entry point), permanently suppress the nag. */ - starNagCompleted?: boolean - /** Timestamp until which nonterminal dismissals suppress threshold prompts (force-show bypasses for dev/testing). */ - starNagDeferredUntil?: number | null - /** App version that consumed the first value-moment ask; main-owned so remote/web clients can't spoof the once-per-version cap. */ - starNagAgentValueMomentAppVersion?: string | null - trustedOrcaHooks?: PersistedTrustedOrcaHooks - setupScriptPromptDismissedRepoIds?: string[] - /** Pet overlay visibility, separate from the experimentalPet settings flag so "Hide pet" is a reversible dismiss; absent = true. */ - petVisible?: boolean - /** Active pet id (bundled id or custom UUID); unknown ids fall back to the default on read so a removed custom pet doesn't blank the overlay. */ - petId?: string - /** Metadata index for user-uploaded pet images; bytes live under legacy userData/sidekicks/custom/. */ - customPets?: CustomPet[] - /** Pet overlay size in CSS pixels (square); clamped to [PET_SIZE_MIN, PET_SIZE_MAX] on read. */ - petSize?: number - /** Legacy keys from before the sidekick -> pet rename; read only during migration, new writes use pet* above. */ - sidekickVisible?: boolean - sidekickId?: string - customSidekicks?: CustomPet[] - sidekickSize?: number - /** Page-position state for Tasks: only transient tabs/searches (source/repo/team/project selections use their own settings paths). */ - taskResumeState?: TaskResumeState - workspaceCleanup?: WorkspaceCleanupUIState - /** Feature tips already surfaced; startup opens the tips modal only when a current tip id is missing here. */ - featureTipsSeenIds?: FeatureTipId[] - /** Feature ids the user has actually used; education surfaces skip teaching already-discovered features. */ - featureInteractions?: FeatureInteractionState - /** Contextual tours already surfaced; unknown ids ignored on hydration for downgrade/upgrade forward-compat. */ - contextualToursSeenIds?: ContextualTourId[] - /** Whether this profile may receive automatic contextual tours; missing = renderer hasn't classified the profile yet. */ - contextualToursAutoEligible?: boolean -} - -export const PET_SIZE_MIN = 60 -export const PET_SIZE_MAX = 360 -export const PET_SIZE_DEFAULT = 180 - -/** User-uploaded pet image metadata; renderer fetches bytes from main via pet:read (id, fileName), never learning the on-disk path. */ -export type CustomPet = { - id: string - label: string - fileName: string - /** MIME type for the renderer's Blob Content-Type — esp. image/svg+xml, which browsers won't render from a misdeclared blob URL. */ - mimeType: string - /** Storage layout: `image` = legacy flat file `custom/.`; `bundle` = `.codex-pet` expanded into `custom//`; absent = legacy `image`. */ - kind?: 'image' | 'bundle' - /** Sprite-sheet metadata; present iff from a `.codex-pet` bundle with a manifest frame layout. Dims derived in main so the renderer needn't probe the image. */ - sprite?: { - frameWidth: number - frameHeight: number - columns: number - rows: number - sheetWidth: number - sheetHeight: number - fps: number - defaultAnimation?: string - animations?: Record - } - /** Manifest-declared fps kept even when frames are auto-detected, so playback honors the bundle's speed instead of a hardcoded 8 fps. */ - spriteFps?: number -} - -/** One animation strip in a sprite sheet: `row` = 0-based y-index, `frames` = consecutive cells played left-to-right. */ -export type SpriteAnimation = { - row: number - frames: number - /** Per-frame holds in ms (length === frames). Absent means uniform sheet fps. */ - frameDurationsMs?: number[] -} - -export type PersistedTrustedOrcaHookEntry = { - contentHash: string - approvedAt: number -} - -export type PersistedTrustedOrcaHookRepo = { - all?: { - approvedAt: number - } - setup?: PersistedTrustedOrcaHookEntry - archive?: PersistedTrustedOrcaHookEntry - issueCommand?: PersistedTrustedOrcaHookEntry - vmRecipe?: PersistedTrustedOrcaHookEntry -} - -export type PersistedTrustedOrcaHooks = Record - -export type LegacyPaneKeyAliasEntry = { - ptyId: string - /** Physical pane key retained by the live process; name is persisted for compatibility (UUID keys after detach). */ - legacyPaneKey: string - /** Current logical owner pane key. May belong to another tab after detach. */ - stablePaneKey: string - updatedAt: number -} - -/** Last tab selection a paired client made in a worktree; restores phone navigation across host restarts. */ -export type PersistedMobileClientTabSelection = { - activeTabId: string | null - activeGroupId: string | null - activeTabIdByGroupId: Readonly> -} - -/** deviceId → worktreeId → selection. */ -export type PersistedMobileClientTabSelections = Record< - string, - Record -> - -// ─── Persistence shape ────────────────────────────────────────────── -export type PersistedState = { - schemaVersion: number - repos: Repo[] - projects: Project[] - projectHostSetups: ProjectHostSetup[] - projectGroups: ProjectGroup[] - folderWorkspaces: FolderWorkspace[] - /** Folder-workspace review notes, keyed by FolderWorkspace.id. Top-level, NOT nested in - * folderWorkspaces[]: normalizeFolderWorkspaces rebuilds each record field-by-field, so an - * older build drops nested fields, while unknown top-level keys round-trip untouched. - * - * WRITE-ONLY PROJECTION. FolderWorkspace.diffComments is the single in-memory home; load() - * hydrates from this key and then deletes it from Store state, and buildStateToSave() is the - * only producer of it. Never read Store.state.folderWorkspaceDiffComments outside load(). */ - folderWorkspaceDiffComments?: Record - /** Sparse-checkout presets keyed by repoId. */ - sparsePresetsByRepo: Record - /** Per paired device last tab selection by worktree; keeps mobile navigation across host restarts. */ - mobileClientTabSelectionsByDeviceId?: PersistedMobileClientTabSelections - worktreeMeta: Record - worktreeLineageById: Record - workspaceLineageByChildKey: Record - settings: GlobalSettings - ui: PersistedUIState - githubCache: { - pr: Record - issue: Record - } - /** Legacy single-blob session, kept as the canonical 'local' host partition so an app downgrade still reads its workspace. */ - workspaceSession: WorkspaceSessionState - /** Per-execution-host session partitions for non-'local' hosts (ssh:/runtime:); 'local' stays in workspaceSession so pre-partition builds keep working. */ - workspaceSessionsByHostId?: Partial> - sshTargets: SshTarget[] - /** SSH config aliases the user deleted; suppresses re-import from ~/.ssh/config so a deleted host doesn't reappear. */ - deletedSshConfigAliases: string[] - /** Identity records for removed SSH targets so a re-added host can re-adopt workspaces orphaned on the old target id. */ - removedSshTargetTombstones?: RemovedSshTargetTombstone[] - sshRemotePtyLeases: SshRemotePtyLease[] - /** Main-owned authenticated relay recovery records; never expose through renderer settings APIs. */ - sshPtyConsumerRecoveries?: SshPtyConsumerRecovery[] - /** Live local Claude daemon session ids; seeds the live-PTY gate so early OAuth refresh can't rotate the single-use refresh token out from under a running daemon. */ - claudeLivePtySessionIds?: string[] - migrationUnsupportedPtyEntries: MigrationUnsupportedPtyEntry[] - legacyPaneKeyAliasEntries: LegacyPaneKeyAliasEntry[] - automations: Automation[] - automationRuns: AutomationRun[] - onboarding: OnboardingState - /** Main-owned telemetry de-dupe marker; never exposed through PersistedUIState. */ - featureInteractionTelemetryBuckets?: FeatureInteractionTelemetryBucketState - /** Main-owned reset mutation journal. Never expose this through renderer settings APIs. */ - codexResetCreditAttemptLedger?: CodexResetCreditAttemptLedger -} - -// ─── Filesystem ───────────────────────────────────────────── -export type FilesystemPathFlavor = 'posix' | 'win32' - -export type DirEntry = { - name: string - isDirectory: boolean - isSymlink: boolean -} - -export type MarkdownDocument = { - filePath: string - relativePath: string - basename: string - name: string -} - -// ─── Filesystem watcher ───────────────────────────────────── -export type FsChangeEvent = { - kind: 'create' | 'update' | 'delete' | 'rename' | 'overflow' - absolutePath: string - oldAbsolutePath?: string - isDirectory?: boolean -} - -export type FsChangedPayload = { - worktreePath: string - events: FsChangeEvent[] -} - -// ─── Git Status ───────────────────────────────────────────── -// Re-exported from git-status-types.ts so mobile shares the wire contract without this desktop aggregate. - -export type GitBranchChangeEntry = { - path: string - status: GitBranchChangeStatus - oldPath?: string - added?: number - removed?: number -} - -export type GitBranchCompareSummary = { - baseRef: string - baseOid: string | null - compareRef: string - headOid: string | null - mergeBase: string | null - changedFiles: number - commitsAhead?: number - status: 'ready' | 'invalid-base' | 'unborn-head' | 'no-merge-base' | 'loading' | 'error' - errorMessage?: string -} - -export type GitBranchCompareResult = { - summary: GitBranchCompareSummary - entries: GitBranchChangeEntry[] -} - -export type GitCommitCompareSummary = { - commitOid: string - parentOid: string | null - compareRef: string - baseRef: string - changedFiles: number - status: 'ready' | 'invalid-commit' | 'error' - errorMessage?: string -} - -export type GitCommitCompareResult = { - summary: GitCommitCompareSummary - entries: GitBranchChangeEntry[] -} - -export type GitDiffTextResult = { - kind: 'text' - originalContent: string - modifiedContent: string - originalIsBinary: false - modifiedIsBinary: false - largeDiffRenderLimit?: LargeDiffRenderLimit -} - -export type GitDiffBinaryResult = { - kind: 'binary' - originalContent: string - modifiedContent: string - /** Legacy flag used by the renderer for any binary format it can preview, including PDFs. */ - isImage?: boolean - /** MIME type for binary preview rendering, e.g. "image/png" or "application/pdf" */ - mimeType?: string - /** True only for a proven deletion — distinct from an empty modified side caused by a read failure or size cap. */ - modifiedDeleted?: boolean -} & ( - | { originalIsBinary: true; modifiedIsBinary: boolean } - | { originalIsBinary: boolean; modifiedIsBinary: true } -) - -export type GitDiffResult = GitDiffTextResult | GitDiffBinaryResult - -// ─── Search ───────────────────────────────────────────── -export type SearchMatch = { - line: number - column: number - matchLength: number - lineContent: string - displayColumn?: number - displayMatchLength?: number -} - -export type SearchFileResult = { - filePath: string - relativePath: string - matches: SearchMatch[] - matchCount?: number -} - -export type SearchResult = { - files: SearchFileResult[] - totalMatches: number - truncated: boolean -} - -export type SearchOptions = { - query: string - rootPath: string - caseSensitive?: boolean - wholeWord?: boolean - useRegex?: boolean - includePattern?: string - excludePattern?: string - maxResults?: number -} - -// ─── Stats ────────────────────────────────────────────────────────── - -export type StatsSummary = { - totalAgentsSpawned: number - totalPRsCreated: number - totalAgentTimeMs: number - // Sourced from aggregates, not the event log, so it survives event trimming. - firstEventAt: number | null // timestamp of first-ever event, for "tracking since..." -} - -// ─── Memory dashboard ────────────────────────────────────────────── - -/** cpu is percent of a single core — can exceed 100 on multi-core. memory is in bytes. */ -export type UsageValues = { - cpu: number - memory: number -} - -export type ProcessMemoryMetric = 'rss' | 'working-set' - -export type HostAvailableMemorySource = 'memory-pressure' | 'proc-meminfo' | 'free-memory' - -/** The top-level cpu/memory are the sum of main + renderer + other. */ -export type AppMemory = UsageValues & { - main: UsageValues - renderer: UsageValues - other: UsageValues - /** Oldest-first memory samples (bytes) for the whole Orca app; empty before the first snapshot. */ - history: number[] -} - -export type SessionMemory = UsageValues & { - sessionId: string - paneKey: string | null - pid: number -} - -/** The top-level cpu/memory are the sum of sessions. */ -export type WorktreeMemory = UsageValues & { - worktreeId: string - worktreeName: string - repoId: string - repoName: string - sessions: SessionMemory[] - /** Oldest-first memory samples (bytes) for this worktree's tracked subtrees. */ - history: number[] -} - -export type HostMemory = { - totalMemory: number - /** Immediately free memory reported by Node's host API. */ - freeMemory: number - /** Memory available without material pressure, or freeMemory when unavailable. */ - availableMemory: number - availableMemorySource: HostAvailableMemorySource - /** totalMemory - availableMemory. */ - usedMemory: number - memoryUsagePercent: number - cpuCoreCount: number - loadAverage1m: number -} - -export type MemorySnapshot = { - app: AppMemory - worktrees: WorktreeMemory[] - host: HostMemory - /** Per-process byte metric used by app, session, worktree, history, and totalMemory values. */ - processMemoryMetric: ProcessMemoryMetric - /** Sum of app + all tracked worktree sessions. Percent of a single core, so may exceed 100 on multi-core machines. */ - totalCpu: number - /** Sum of per-process samples. Shared pages may repeat, so this can exceed host.totalMemory. */ - totalMemory: number - collectedAt: number -} +export type { ClassifiedError } from './classified-error' diff --git a/src/shared/ui-chrome-types.ts b/src/shared/ui-chrome-types.ts new file mode 100644 index 00000000000..f7a4bd1efdf --- /dev/null +++ b/src/shared/ui-chrome-types.ts @@ -0,0 +1,117 @@ +import type { LinearConcreteWorkspaceId } from './linear-workspace-types' +import type { LinearCustomViewModel } from './linear-project-types' + +export type TaskViewPresetId = 'all' | 'issues' | 'review' | 'my-issues' | 'my-prs' | 'prs' + +export type OpenInApplication = { + id: string + label: string + command: string +} + +export type SourceControlViewMode = 'list' | 'tree' +export type SourceControlGroupOrder = 'changes-first' | 'staged-first' | 'untracked-first' + +export type LeftSidebarAppearanceMode = 'default' | 'match-terminal' | 'tinted' + +/** Strategy for the prefix prepended to worktree branch names. */ +export type BranchPrefixStrategy = 'git-username' | 'custom' | 'none' + +export type FloatingTerminalCwdRequest = { + path?: string + requireTrusted?: boolean +} + +/** Presentation mode for the experimental Agent Dashboard. */ +export type AgentDashboardMode = 'in-window' | 'popout' + +export type WorktreeCardProperty = + | 'status' + | 'unread' + // Legacy persisted preference. CI status is now represented by linked PR metadata. + | 'ci' + // Migration-only: legacy detailed cards showed branch identity as a visible row. + | 'branch' + // Task metadata on workspace cards; provider-specific persisted values kept for older profiles. + | 'issue' + | 'linear-issue' + | 'jira-issue' + | 'pr' + | 'automation' + // Badge marking workspaces created through `orca worktree create`. + | 'cli' + | 'comment' + | 'ports' + // Inline agent-activity list rendered in each workspace card; on by default (see DEFAULT_WORKTREE_CARD_PROPERTIES in shared/constants.ts). + | 'inline-agents' + +export type WorktreeCardMode = 'Default' | 'Compact' + +export type AgentActivityDisplayMode = 'compact' | 'full' + +export type StatusBarItem = + | 'claude' + | 'codex' + | 'gemini' + | 'antigravity' + | 'opencode-go' + | 'kimi' + | 'minimax' + | 'grok' + | 'ssh' + | 'resource-usage' + | 'ports' +export type FloatingTerminalTriggerLocation = 'floating-button' | 'status-bar' + +export type TaskResumeState = { + githubMode?: 'items' | 'project' + githubItemsPreset?: TaskViewPresetId | null + githubItemsQuery?: string + githubProjectHiddenFieldIdsByView?: Record + linearMode?: 'issues' | 'projects' | 'views' | 'in-orca' + linearPreset?: 'assigned' | 'created' | 'all' | 'completed' + linearQuery?: string + linearContext?: { + kind: 'project' | 'view' + id: string + workspaceId: LinearConcreteWorkspaceId + model?: LinearCustomViewModel + } + jiraPreset?: 'assigned' | 'reported' | 'all' | 'done' + jiraQuery?: string +} + +export type RightSidebarTab = + | 'explorer' + | 'search' + | 'vault' + | 'workspaces' + | 'pr-checks' + | 'source-control' + | 'checks' + | 'ports' + // Plugin-contributed panels are keyed `plugin:/` so the + // static union stays closed while plugin tabs remain type-representable. + | `plugin:${string}` +export type ActiveRightSidebarTab = Exclude +export type RightSidebarExplorerView = 'files' | 'search' + +export type ProjectOrderBy = 'manual' | 'recent' +export type WorkspaceHostScope = 'all' | 'local' | `ssh:${string}` | `runtime:${string}` +export type VisibleWorkspaceHostIds = Exclude[] | null +export type WorkspaceHostOrder = Exclude[] +export type ManualRepoOrderEntry = { + hostId: WorkspaceHostOrder[number] + repoId: string +} + +/** The active top-level section shown in the main content area. */ +export type TopLevelView = + | 'terminal' + | 'settings' + | 'tasks' + | 'activity' + | 'automations' + | 'space' + | 'artifacts' + | 'mobile' diff --git a/src/shared/update-status-types.ts b/src/shared/update-status-types.ts new file mode 100644 index 00000000000..8d54837c9c5 --- /dev/null +++ b/src/shared/update-status-types.ts @@ -0,0 +1,88 @@ +import type { DedicatedRepoChannel, ReleaseBuild, ReleaseChannel } from './release-channel' + +// ─── Updater ───────────────────────────────────────────────────────── + +// Why: the release object sent to the renderer omits `version` (redundant +// with the top-level UpdateStatus.version) to keep one source of truth. +export type ChangelogRelease = { + title: string + description: string + mediaUrl?: string + releaseNotesUrl: string +} + +export type ChangelogData = { + release: ChangelogRelease + releasesBehind: number | null +} + +export type UpdateCheckOptions = { + includePrerelease?: boolean + includePerfPrerelease?: boolean + localBuild?: boolean + /** Dev channel switching; `targetTag` pins an exact build, including older ones. */ + channel?: ReleaseChannel + targetTag?: string +} + +/** Non-release origins for an update. Derived from the dev-channel list so a new + * channel with its own repo cannot be reported as an ordinary release. */ +export type UpdateSource = 'local' | DedicatedRepoChannel + +/** Root-package Linux install formats whose update installs need privilege escalation. */ +export type LinuxRootPackageType = 'deb' | 'rpm' + +export type LinuxPackageInstallFailureReason = + | 'authentication-agent-unavailable' + | 'authentication-denied' + | 'package-install-failed' + +// Why: the renderer must not infer "no polkit agent" from copy alone — main classifies and the card branches on this discriminant. +export type LinuxPackageInstallRecovery = { + kind: 'linux-package-install' + packageType: LinuxRootPackageType + reason: LinuxPackageInstallFailureReason + version: string +} + +/** Why: only these two mean no safe command exists here; every other failure clears recovery entirely. */ +export type LinuxPackageCommandUnavailableReason = 'no-sudo' | 'no-package-manager' + +export type LinuxPackageInstallInstructions = + | { ok: true; command: string; packageFileName: string } + | { ok: false; reason: LinuxPackageCommandUnavailableReason; message: string } + +export type UpdateStatus = ( + | { state: 'idle' } + | { state: 'checking'; userInitiated?: boolean } + | { + state: 'available' + version: string + activeNudgeId?: string + // Why: releaseUrl is not currently populated by the update-available handler + // (it always sends undefined). Kept on the type for the Settings page's + // release-notes link fallback and for potential future use if the main + // process starts extracting release URLs from electron-updater metadata. + releaseUrl?: string + // Why: changelog is always explicitly set by the main process — null means + // the fetch failed or the version wasn't in the JSON (simple mode), and a + // populated object means rich mode. Using `| null` (not `?`) avoids a + // three-state ambiguity (undefined vs null vs present) and makes exhaustive + // checks straightforward. + changelog: ChangelogData | null + } + | { state: 'not-available'; userInitiated?: boolean } + | { state: 'downloading'; percent: number; version: string; activeNudgeId?: string } + | { state: 'downloaded'; version: string; releaseUrl?: string; activeNudgeId?: string } + | { + state: 'error' + message: string + userInitiated?: boolean + activeNudgeId?: string + recovery?: LinuxPackageInstallRecovery + } +) & { source?: UpdateSource } + +export type ReleaseBuildListResult = + | { ok: true; channel: ReleaseChannel; builds: ReleaseBuild[] } + | { ok: false; channel: ReleaseChannel; message: string } diff --git a/src/shared/workspace-session-state-types.ts b/src/shared/workspace-session-state-types.ts new file mode 100644 index 00000000000..bc20b7c5459 --- /dev/null +++ b/src/shared/workspace-session-state-types.ts @@ -0,0 +1,113 @@ +import type { ExecutionHostId } from './execution-host' +import type { SleepingAgentSessionRecord } from './agent-session-resume' +import type { WorkspaceKey } from './folder-workspace-types' +import type { Tab, TabGroup, TabGroupLayoutNode, WorkspaceVisibleTabType } from './tab-types' +import type { TerminalLayoutSnapshot, TerminalTab } from './terminal-tab-types' +import type { BrowserHistoryEntry, BrowserPage, BrowserWorkspace } from './browser-workspace-types' + +/** Minimal subset of OpenFile persisted across restarts. + * Only edit-mode files are saved — diffs, conflict reviews, and other + * transient views are reconstructed on demand from git state. */ +export type PersistedOpenFile = { + filePath: string + relativePath: string + worktreeId: string + language: string + isPreview?: boolean + runtimeEnvironmentId?: string | null + /** SSH target that owns an absolute path outside the worktree. */ + externalSshTargetId?: string + /** Unsaved editor buffer captured for hot exit; presence restores the tab dirty. */ + dirtyDraftContent?: string + /** Signature of the disk content the dirty draft is based on; lets restore + * re-derive a changed-on-disk conflict from ground truth. */ + lastKnownDiskSignature?: string + /** Why: a read-only tab (AI Vault View Log) must survive restart still + * read-only; persisted only when true so old sessions stay writable. */ + readOnly?: boolean + /** Opt-in streaming append for a read-only local log tab. */ + liveTail?: boolean +} + +export type WorkspaceSessionState = { + activeRepoId: string | null + /** Scope-aware active owner for folder workspaces. Legacy worktree UI still reads activeWorktreeId. */ + activeWorkspaceKey?: WorkspaceKey | null + activeWorkspaceExecutionHostId?: ExecutionHostId | null + activeWorktreeId: string | null + activeTabId: string | null + /** Keys may be legacy raw worktree IDs or canonical WorkspaceKey values. */ + tabsByWorktree: Record + terminalLayoutsByTabId: Record + /** Worktree IDs that had at least one tab with a live PTY at shutdown. + * Used on startup to eagerly re-spawn PTY processes so the Active filter + * works immediately after restart. */ + activeWorktreeIdsOnShutdown?: string[] + /** Editor files that were open at shutdown, keyed by worktree ID. + * Only edit-mode files are persisted — diffs and conflict views are + * transient and not restored. */ + openFilesByWorktree?: Record + /** Per-worktree active editor file ID (filePath) at shutdown. */ + activeFileIdByWorktree?: Record + /** Per-file markdown preview front-matter visibility. Absent entry means hidden. */ + markdownFrontmatterVisible?: Record + /** Persisted browser workspaces, keyed by worktree ID. */ + browserTabsByWorktree?: Record + /** Persisted browser pages, keyed by workspace ID. */ + browserPagesByWorkspace?: Record + /** Per-worktree active browser workspace ID at shutdown. */ + activeBrowserTabIdByWorktree?: Record + /** Per-worktree active tab type (terminal vs editor vs browser) at shutdown. */ + activeTabTypeByWorktree?: Record + /** Global browser URL history for address bar autocomplete. */ + browserUrlHistory?: BrowserHistoryEntry[] + /** Per-worktree last-active terminal tab ID at shutdown. */ + activeTabIdByWorktree?: Record + /** Unified tab model — present when saved by a build that includes TabsSlice. + * Read-path checks for this first; falls back to legacy fields if absent. */ + unifiedTabs?: Record + /** Tab group model — present alongside unifiedTabs. */ + tabGroups?: Record + /** Persisted split layout tree per worktree. */ + tabGroupLayouts?: Record + /** Per-worktree focused group at shutdown. */ + activeGroupIdByWorktree?: Record + /** SSH target IDs that were connected at shutdown. Used on startup to + * auto-reconnect before attempting remote PTY reattach. */ + activeConnectionIdsAtShutdown?: string[] + /** Maps tab IDs to their remote relay PTY session IDs. Populated at + * shutdown from renderer state so remote PTYs can be reattached via + * the relay's pty.attach RPC on startup. */ + remoteSessionIdsByTabId?: Record + /** Per-worktree focus-recency timestamps used by the Cmd+J empty-query + * ordering. Separate from worktree.lastActivityAt (background signal) + * and worktreeNavHistory (Back/Forward stack). See + * docs/cmd-j-empty-query-ordering.md. Absent in sessions written by + * older builds — hydration tolerates missing/partial maps and the + * active worktree is seeded on first restore. */ + lastVisitedAtByWorktreeId?: Record + /** Worktrees whose repo-defined default terminal tabs have already been + * considered. Persisted so closing all tabs and re-opening the workspace + * does not recreate the template. */ + defaultTerminalTabsAppliedByWorktreeId?: Record + /** Provider-session resume records captured when workspaces sleep. */ + sleepingAgentSessionsByPaneKey?: Record + /** Host-issued process incarnation for each durable terminal surface. */ + terminalPtyIncarnationsByPaneKey?: Record + /** Monotonic host authority watermark for terminal membership in each repo. */ + terminalTopologyRevisionByRepoId?: Record + /** Legacy per-surface fences migrated into terminalTopologyRevisionByRepoId on load. */ + terminalSurfaceTombstonesByPaneKey?: Record< + string, + { + worktreeId: string + parentTabId: string + leafId: string + ptyId: string + incarnationId: string + retiredAt: number + } + > +} + +export type WorkspaceSessionPatch = Partial diff --git a/src/shared/worktree-base-ref-drift-types.ts b/src/shared/worktree-base-ref-drift-types.ts new file mode 100644 index 00000000000..416e8a96812 --- /dev/null +++ b/src/shared/worktree-base-ref-drift-types.ts @@ -0,0 +1,33 @@ +export type LocalBaseRefRefreshResult = { + status: 'updated' | 'skipped_dirty_worktree' | 'skipped_not_fast_forward' | 'skipped_error' + baseRef: string + localBranch: string + ownerWorktreePath?: string +} + +export type LocalBaseRefUpdateSuggestion = { + baseRef: string + localBranch: string + behind: number +} + +export type WorktreeBaseStatusKind = 'checking' | 'current' | 'drift' | 'base_changed' | 'unknown' + +export type WorktreeBaseStatusEvent = { + repoId: string + worktreeId: string + status: WorktreeBaseStatusKind + base: string + /** Configured remote name parsed from `base` (longest-prefix match). Absent + * when classification skipped optimistic reconcile (e.g. legacy fallback). */ + remote?: string + behind?: number + recentSubjects?: string[] +} + +export type WorktreeRemoteBranchConflictEvent = { + repoId: string + worktreeId: string + remote: string + branchName: string +} diff --git a/src/shared/worktree-create-types.ts b/src/shared/worktree-create-types.ts new file mode 100644 index 00000000000..ff7929332ce --- /dev/null +++ b/src/shared/worktree-create-types.ts @@ -0,0 +1,179 @@ +import type { ExecutionHostId } from './execution-host' +import type { WorkspaceSource } from './workspace-source' +import type { TaskSourceContext } from './task-source-context' +import type { WorkspaceKey } from './folder-workspace-types' +import type { TuiAgent } from './tui-agent' +import type { + AutomationWorkspaceProvenanceRequest, + GitPushTarget, + GitWorktreeInfo, + WorkspaceLinkedItem, + WorkspaceStatus, + Worktree +} from './worktree-types' +import type { + WorkspaceLineage, + WorktreeLineage, + WorktreeLineageWarning +} from './worktree-lineage-types' +import type { + WorktreeDefaultTabsLaunch, + WorktreeSetupLaunch, + WorktreeStartupLaunch +} from './worktree-launch-types' +import type { + LocalBaseRefRefreshResult, + LocalBaseRefUpdateSuggestion, + WorktreeBaseStatusEvent +} from './worktree-base-ref-drift-types' + +export type SetupDecision = 'inherit' | 'run' | 'skip' + +export type WorktreeCreateTimingPhase = { + phase: string + startedAtMs: number + durationMs: number +} + +export type WorktreeCreateTiming = { + totalDurationMs: number + phases: WorktreeCreateTimingPhase[] +} + +export type CreateSparseCheckoutRequest = { + directories: string[] + /** Set when the directories came from a saved preset and the user did not + * modify them — recorded on WorktreeMeta so the worktree can show "from + * preset X" later. Cleared if the user edited the textarea. */ + presetId?: string +} + +/** A reusable per-repo sparse directory list. Saved by the user from the + * composer; surfaced again the next time they create a worktree in the same + * repo. The MVP scope (no preset) is `presetId === undefined`. */ +export type SparsePreset = { + id: string + repoId: string + name: string + directories: string[] + createdAt: number + updatedAt: number +} + +export type CreateWorktreeArgs = { + repoId: string + name: string + /** Optional user-facing label to persist separately from the git-safe + * branch/path seed. Used when a workspace is created from a GitHub or + * Linear artifact whose title should remain readable in the sidebar. */ + displayName?: string + baseBranch?: string + /** Source Control compare target when it differs from the checkout start point. */ + compareBaseRef?: string + /** Optional git branch to create, separate from the filesystem-safe worktree + * name. Used when creating from an existing branch whose local branch name + * legitimately contains `/` while the worktree directory must not. */ + branchNameOverride?: string + setupDecision?: SetupDecision + sparseCheckout?: CreateSparseCheckoutRequest + linkedIssue?: number + linkedPR?: number + linkedLinearIssue?: string + linkedLinearIssueWorkspaceId?: string | null + linkedLinearIssueOrganizationUrlKey?: string | null + linkedGitLabIssue?: number + linkedGitLabMR?: number + linkedBitbucketPR?: number | null + linkedAzureDevOpsPR?: number | null + linkedGiteaPR?: number | null + linkedWorkItem?: WorkspaceLinkedItem | null + linkedTaskSourceContext?: TaskSourceContext | null + pushTarget?: GitPushTarget + workspaceStatus?: WorkspaceStatus + manualOrder?: number + /** Parent workspace for in-app creates launched from a folder workspace. */ + parentWorkspace?: WorkspaceKey + /** Agent selected in the create surface. Omitted for blank-shell creates. */ + createdWithAgent?: TuiAgent + /** Set when the renderer knows this auto-generated branch should be renamed + * from the first agent message. */ + pendingFirstAgentMessageRename?: boolean + /** Telemetry-only: which UI surface initiated this create. Threaded from + * the renderer entry point so main can emit `workspace_created` with the + * correct `source`. `unknown` is a valid wire value — an unrecognized + * surface emits `source: 'unknown'` rather than dropping the event, so + * dashboards surface enum-coverage gaps as a slice rather than as + * missing data. Optional on the type so older renderer code paths that + * pre-date this prop default to `unknown` at the IPC boundary instead + * of failing typecheck. */ + telemetrySource?: WorkspaceSource + /** Optional startup command for callers that want the backend to spawn the + * first terminal as soon as the worktree is registered. */ + startup?: WorktreeStartupLaunch + /** Correlates `createWorktree:progress` events back to a specific pending + * creation in the renderer, so concurrent background creates each drive + * their own status surface. Omitted by synchronous callers. */ + creationId?: string + /** Authorizes the host to mint system-owned automation provenance. */ + automationProvenanceRequest?: AutomationWorkspaceProvenanceRequest +} + +export type AdoptProvisionedRootArgs = CreateWorktreeArgs & { + runtimeId: string + executionHostId: ExecutionHostId + expectedPath: string +} + +export type CreateWorktreeResult = { + worktree: Worktree & { + parentWorktreeId?: string | null + childWorktreeIds?: string[] + lineage?: WorktreeLineage | null + workspaceLineage?: WorkspaceLineage | null + git?: GitWorktreeInfo + } + lineage?: WorktreeLineage | null + workspaceLineage?: WorkspaceLineage | null + warnings?: WorktreeLineageWarning[] + setup?: WorktreeSetupLaunch + setupReceipt?: { + requested: 'run' | 'skip' | 'inherit' + hookFound: boolean + startupPolicy: 'start-immediately' | 'wait-for-setup' + state: 'running' | 'skipped' | 'not_configured' | 'spawn_failed' + terminalHandle?: string + } + defaultTabs?: WorktreeDefaultTabsLaunch + warning?: string + baseFallback?: WorktreeCreateBaseFallback + initialBaseStatus?: WorktreeBaseStatusEvent + localBaseRefRefresh?: LocalBaseRefRefreshResult + localBaseRefUpdateSuggestion?: LocalBaseRefUpdateSuggestion + startupTerminal?: { + spawned: boolean + handle?: string + tabId?: string + paneKey?: string | null + ptyId?: string | null + surface?: 'visible' | 'background' + } + timing?: WorktreeCreateTiming +} + +export type WorktreeCreateBaseFallback = { + requestedRef: string + localRef: string +} + +export type PreservedWorktreeBranch = { + branchName: string + head?: string +} + +export type RemoveWorktreeResult = { + preservedBranch?: PreservedWorktreeBranch +} + +export type ForceDeleteWorktreeBranchResult = { + deleted: true +} diff --git a/src/shared/worktree-launch-types.ts b/src/shared/worktree-launch-types.ts new file mode 100644 index 00000000000..3cdeeb7abbb --- /dev/null +++ b/src/shared/worktree-launch-types.ts @@ -0,0 +1,39 @@ +import type { StartupCommandDelivery } from './codex-startup-delivery' +import type { AgentKind, LaunchSource, RequestKind } from './telemetry-events' +import type { SleepingAgentLaunchConfig } from './agent-session-resume' +import type { SetupRunnerShell } from './setup-runner-command' +import type { OrcaDefaultTabTemplate } from './orca-yaml-hook-types' +import type { TuiAgent } from './tui-agent' + +export type WorktreeSetupLaunch = { + runnerScriptPath: string + envVars: Record + shell?: SetupRunnerShell + command?: string + waitForAgentStartup?: boolean +} + +export type WorktreeStartupLaunch = { + command: string + env?: Record + launchConfig?: SleepingAgentLaunchConfig + launchToken?: string + launchAgent?: TuiAgent + viewMode?: 'terminal' | 'chat' + startupCommandDelivery?: StartupCommandDelivery + telemetry?: { agent_kind: AgentKind; launch_source: LaunchSource; request_kind: RequestKind } +} + +export type WorktreeDefaultTabsLaunch = { + tabs: OrcaDefaultTabTemplate[] + runCommands: boolean +} + +/** Where the repo setup script runs when a worktree is created. + * - 'new-tab': open a background tab titled "Setup" and leave focus on the first tab (default). + * - 'split-vertical': split the initial terminal pane with a vertical divider. + * - 'split-horizontal': split the initial terminal pane with a horizontal divider. */ +export type SetupScriptLaunchMode = 'split-vertical' | 'split-horizontal' | 'new-tab' + +/** Direction used when the setup script launch mode is a split. */ +export type SetupSplitDirection = 'vertical' | 'horizontal' diff --git a/src/shared/worktree-lineage-types.ts b/src/shared/worktree-lineage-types.ts new file mode 100644 index 00000000000..c421b927272 --- /dev/null +++ b/src/shared/worktree-lineage-types.ts @@ -0,0 +1,56 @@ +import type { WorkspaceKey } from './folder-workspace-types' + +export type WorktreeLineageOrigin = 'orchestration' | 'cli' | 'manual' +export type WorktreeLineageCaptureConfidence = 'explicit' | 'inferred' +export type WorktreeLineageCaptureSource = + | 'explicit-cli-flag' + | 'env-workspace' + | 'cwd-context' + | 'terminal-context' + | 'orchestration-context' + | 'active-workspace' + | 'manual-action' + +export type WorktreeLineageCapture = { + source: WorktreeLineageCaptureSource + confidence: WorktreeLineageCaptureConfidence +} + +export type WorktreeLineage = { + worktreeId: string + worktreeInstanceId: string + parentWorktreeId: string + parentWorktreeInstanceId: string + origin: WorktreeLineageOrigin + capture: WorktreeLineageCapture + orchestrationRunId?: string + taskId?: string + coordinatorHandle?: string + createdByTerminalHandle?: string + createdAt: number +} + +export type WorkspaceLineage = { + childWorkspaceKey: WorkspaceKey + childInstanceId?: string | null + parentWorkspaceKey: WorkspaceKey + parentInstanceId?: string | null + origin: WorktreeLineageOrigin + capture: WorktreeLineageCapture + taskId?: string + orchestrationRunId?: string + coordinatorHandle?: string + createdByTerminalHandle?: string + createdAt: number +} + +export type WorktreeLineageWarningCode = + | 'LINEAGE_PARENT_CONTEXT_MISSING' + | 'LINEAGE_PARENT_CONTEXT_CONFLICT' + | 'LINEAGE_PARENT_INSTANCE_STALE' + +export type WorktreeLineageWarning = { + code: WorktreeLineageWarningCode + message: string + details?: Record +} diff --git a/src/shared/worktree-meta-types.ts b/src/shared/worktree-meta-types.ts new file mode 100644 index 00000000000..8d7df1a53fc --- /dev/null +++ b/src/shared/worktree-meta-types.ts @@ -0,0 +1,91 @@ +import type { ExecutionHostId } from './execution-host' +import type { TaskSourceContext } from './task-source-context' +import type { EphemeralVmCheckoutMode } from './orca-yaml-hook-types' +import type { + AutomationWorkspaceProvenance, + CliWorkspaceProvenance, + GitPushTarget, + WorkspaceCreatorProvenance, + WorkspaceLinkedItem, + WorkspaceStatus +} from './worktree-types' +import type { TuiAgent } from './tui-agent' +import type { OrcaWorkspaceLayout } from './global-settings-types' +import type { DiffComment, MobileDiffReviewState } from './diff-comment-types' + +// ─── Worktree metadata (persisted user-authored fields only) ───────── +export type WorktreeMeta = { + /** Immutable per-workspace-instance ID used to reject stale lineage after path reuse. */ + instanceId?: string + /** See Worktree.projectId. Persisted for project-first workspace ownership. */ + projectId?: string + /** See Worktree.hostId. Persisted for project-first workspace ownership. */ + hostId?: ExecutionHostId + /** See Worktree.projectHostSetupId. Persisted for project-first workspace ownership. */ + projectHostSetupId?: string + /** See Worktree.ephemeralVmCheckoutMode. */ + ephemeralVmCheckoutMode?: EphemeralVmCheckoutMode + /** See Worktree.creatorProvenance. */ + creatorProvenance?: WorkspaceCreatorProvenance + displayName: string + comment: string + linkedIssue: number | null + linkedPR: number | null + linkedLinearIssue: string | null + linkedLinearIssueWorkspaceId?: string | null + linkedLinearIssueOrganizationUrlKey?: string | null + /** Optional for backward compatibility — see Worktree.linkedGitLabMR. */ + linkedGitLabMR?: number | null + /** Optional for backward compatibility — see Worktree.linkedGitLabIssue. */ + linkedGitLabIssue?: number | null + /** Optional for backward compatibility — see Worktree.linkedBitbucketPR. */ + linkedBitbucketPR?: number | null + /** Optional for backward compatibility — see Worktree.linkedAzureDevOpsPR. */ + linkedAzureDevOpsPR?: number | null + /** Optional for backward compatibility — see Worktree.linkedGiteaPR. */ + linkedGiteaPR?: number | null + linkedWorkItem?: WorkspaceLinkedItem | null + linkedTaskSourceContext?: TaskSourceContext | null + isArchived: boolean + isUnread: boolean + isPinned: boolean + sortOrder: number + /** User-authored sidebar ordering. Higher values render earlier in Manual sort. */ + manualOrder?: number + lastActivityAt: number + /** See {@link Worktree.createdAt}. Persisted to orca-data.json. */ + createdAt?: number + /** See {@link Worktree.createdWithAgent}. Persisted to orca-data.json. */ + createdWithAgent?: TuiAgent + /** See {@link Worktree.pendingFirstAgentMessageRename}. */ + pendingFirstAgentMessageRename?: boolean + /** See {@link Worktree.firstAgentMessageRenameError}. */ + firstAgentMessageRenameError?: string | null + sparseDirectories?: string[] + sparseBaseRef?: string + sparsePresetId?: string + /** Intended create base for stale-base probes. Persisted metadata, not UI drift state. */ + baseRef?: string + /** True when Orca checked out a pre-existing local branch that delete must not prune. */ + preserveBranchOnDelete?: boolean + /** See {@link Worktree.pushTarget}. Persisted so refreshed worktree lists keep the target. */ + pushTarget?: GitPushTarget + /** Explicit marker stamped when Orca creates the worktree. */ + orcaCreatedAt?: number + orcaCreationSource?: 'desktop' | 'runtime' | 'cli' | 'ssh' + /** Workspace layout active when Orca created the worktree. */ + orcaCreationWorkspaceLayout?: OrcaWorkspaceLayout + /** User-assigned workspace board status for manual sidebar organization. */ + workspaceStatus?: WorkspaceStatus + diffComments?: DiffComment[] + /** Path-derived worktree ids this worktree had before its folder was renamed + * on disk (the id embeds the path). Lets the daemon's session GC and registry + * hydration recognize sessions minted under an old id instead of reaping + * them. Self-prunes when the worktree is deleted. */ + priorWorktreeIds?: string[] + mobileDiffReview?: MobileDiffReviewState + /** System-owned provenance for workspaces created by automation new-per-run dispatches. */ + automationProvenance?: AutomationWorkspaceProvenance + /** System-owned provenance for workspaces created via `orca worktree create`. */ + cliProvenance?: CliWorkspaceProvenance +} diff --git a/src/shared/worktree-types.ts b/src/shared/worktree-types.ts new file mode 100644 index 00000000000..25686dbf6f3 --- /dev/null +++ b/src/shared/worktree-types.ts @@ -0,0 +1,217 @@ +import type { ExecutionHostId } from './execution-host' +import type { AutomationExecutionTargetType } from './automations-types' +import type { TaskSourceContext } from './task-source-context' +import type { TuiAgent } from './tui-agent' +import type { DiffComment, MobileDiffReviewState } from './diff-comment-types' +import type { EphemeralVmCheckoutMode } from './orca-yaml-hook-types' +import type { BuiltInWorktreeVisibilitySourceId } from './repo-types' + +export type WorkspaceLinkedItem = { + provider: 'github' | 'gitlab' | 'linear' | 'jira' + type: 'issue' | 'pr' | 'mr' + number: number + title: string + url: string + linearIdentifier?: string + jiraIdentifier?: string + repoId?: string +} + +// ─── Worktree (git-level) ──────────────────────────────────────────── +export type GitWorktreeInfo = { + path: string + head: string + branch: string + isBare: boolean + isSparse?: boolean + locked?: boolean + lockReason?: string + /** True when Git reports the worktree as prunable (its directory is gone but + * the registration remains). Detected via the `prunable` porcelain field + * (Git ≥ 2.36) or a path-existence probe on older Git. */ + prunable?: boolean + prunableReason?: string + /** True for the repo's main working tree (the first entry from `git worktree list`). + * Linked worktrees created via `git worktree add` have this set to false. */ + isMainWorktree: boolean +} + +/** Head/branch snapshot read from Git metadata files without spawning Git. + * Carries background-worktree freshness when status-only churn includes a + * real head move (external commit/amend/reset) that must not re-enter the + * structural `worktrees:changed` fanout. */ +export type WorktreeHeadIdentity = { + worktreePath: string + head: string + /** Full ref (e.g. `refs/heads/main`), or null for a detached HEAD. */ + branch: string | null +} + +// ─── Worktree (app-level, enriched) ────────────────────────────────── +export type WorkspaceStatus = string + +export type WorkspaceStatusDefinition = { + id: WorkspaceStatus + label: string + color?: string + icon?: string +} + +export type Worktree = { + id: string // `${repoId}::${path}` + instanceId?: string + repoId: string + /** Durable project identity. Optional while legacy repo-only workspaces migrate. */ + projectId?: string + /** Execution host that owns the workspace. Optional for pre-project-host metadata. */ + hostId?: ExecutionHostId + /** Renderer projection of the paired runtime that transports operations to `hostId`. */ + runtimeOwnerEnvironmentId?: string + /** Authenticated client that created this workspace. Missing means unknown legacy origin. */ + creatorProvenance?: WorkspaceCreatorProvenance + /** Host-specific setup used to create/run this workspace. */ + projectHostSetupId?: string + /** Checkout ownership for a recipe-provisioned main workspace. */ + ephemeralVmCheckoutMode?: EphemeralVmCheckoutMode + displayName: string + comment: string + linkedIssue: number | null + linkedPR: number | null + linkedLinearIssue: string | null + linkedLinearIssueWorkspaceId?: string | null + linkedLinearIssueOrganizationUrlKey?: string | null + // Why: parallel slots for non-GitHub work-item references. Kept as separate + // fields (rather than reusing linkedIssue / linkedPR with a provider + // discriminator) so the persistence layer is unambiguous when a user + // has remotes from several providers on the same repo, and so the + // existing GitHub renderer code keeps reading linkedPR / linkedIssue + // unchanged. Optional on the type so existing test fixtures and + // persisted older worktrees that never carried these fields continue + // to typecheck and load without migration. + linkedGitLabMR?: number | null + linkedGitLabIssue?: number | null + linkedBitbucketPR?: number | null + linkedAzureDevOpsPR?: number | null + linkedGiteaPR?: number | null + linkedWorkItem?: WorkspaceLinkedItem | null + linkedTaskSourceContext?: TaskSourceContext | null + isArchived: boolean + isUnread: boolean + isPinned: boolean + sortOrder: number + /** User-authored sidebar ordering. Higher values render earlier in Manual sort. */ + manualOrder?: number + lastActivityAt: number + /** Set once when Orca creates the worktree. Absent for worktrees discovered + * on disk or persisted before this field existed. Used by the sidebar to + * grant newly-created worktrees a short grace window at the top of Recent, + * immune to ambient PTY-bump reordering in other worktrees. */ + createdAt?: number + /** Agent selected when Orca originally created the worktree. Used only to + * seed a replacement terminal if the user later reopens the worktree after + * closing every visible surface. */ + createdWithAgent?: TuiAgent + /** True while an auto-named workspace is waiting for the first agent message + * to drive the branch/title rename. */ + pendingFirstAgentMessageRename?: boolean + /** Holds the last auto-rename generation failure message so the sidebar can + * show a "rename failed" badge. null/undefined when there is no failure + * (never attempted, succeeded, or only a benign skip). */ + firstAgentMessageRenameError?: string | null + sparseDirectories?: string[] + sparseBaseRef?: string + /** ID of the saved preset this worktree was created from, if any. Cleared + * when the worktree is no longer sparse on refresh. */ + sparsePresetId?: string + /** Intended create base for stale-base probes. Persisted metadata, not UI drift state. */ + baseRef?: string + /** Remote/branch Orca should publish review commits to when it created this worktree. */ + pushTarget?: GitPushTarget + /** Path-derived worktree ids this worktree had before folder renames. */ + priorWorktreeIds?: string[] + workspaceStatus?: WorkspaceStatus + diffComments?: DiffComment[] + mobileDiffReview?: MobileDiffReviewState + automationProvenance?: AutomationWorkspaceProvenance + cliProvenance?: CliWorkspaceProvenance +} & GitWorktreeInfo + +/** Provenance for workspaces created through `orca worktree create`. Absent on + * workspaces created before this field existed and on every non-CLI create, so + * consumers must read "missing" as "not CLI-created". */ +export type CliWorkspaceProvenance = { + kind: 'created-by-cli' + createdAt: number + /** Orca terminal the CLI ran inside, when the caller had one — distinguishes + * an agent-issued create from one hand-typed in an external shell. */ + callerTerminalHandle?: string + /** Agent requested via `--agent`, when one was passed. */ + startupAgent?: TuiAgent +} + +export type WorkspaceCreatorProvenance = + | { kind: 'host' } + | { kind: 'paired-device'; deviceId: string } + +export type AutomationWorkspaceProvenance = { + kind: 'created-by-automation' + automationId: string + automationNameSnapshot: string + automationRunId: string + automationRunTitleSnapshot: string + createdAt: number + executionTargetType: AutomationExecutionTargetType + executionTargetId: string + projectId: string + repoId?: string + hostId?: ExecutionHostId +} + +export type AutomationWorkspaceProvenanceRequest = { + automationId: string + automationRunId: string + dispatchToken: string + createRequestId: string +} + +export type GitPushTarget = { + remoteName: string + branchName: string + remoteUrl?: string + /** True when Orca added this remote while preparing a fork-PR worktree. */ + remoteCreated?: boolean +} + +export type GitHubPrStartPoint = { + baseBranch: string + /** Review target branch to use for Source Control compare after creating from a PR head SHA. */ + compareBaseRef?: string + pushTarget?: GitPushTarget + /** Verified PR head commit. Present when checkout can be tied to a stable SHA. */ + headSha?: string + /** Exact local branch name to create/reuse when the PR head is a safe same-repo branch. */ + branchNameOverride?: string + /** Fork PRs: false when "Allow edits from maintainers" is off; a push to the fork may be rejected. */ + maintainerCanModify?: boolean +} + +export type WorktreeOwnership = 'orca-managed' | 'external' | 'unknown-legacy' | 'agent-scratch' + +export type DetectedWorktreeListSource = 'git' | 'metadata-fallback' | 'session-fallback' + +export type DetectedWorktree = Worktree & { + ownership: WorktreeOwnership + selectedCheckout: boolean + visible: boolean + /** Optional additive source identity; older hosts omit it. */ + visibilitySource?: + | { kind: 'built-in'; id: BuiltInWorktreeVisibilitySourceId } + | { kind: 'custom'; id: string } +} + +export type DetectedWorktreeListResult = { + repoId: string + authoritative: boolean + source: DetectedWorktreeListSource + worktrees: DetectedWorktree[] +} From 583ab1601bb2ff2642ce2187112aff3376c11cd7 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:44:16 -0700 Subject: [PATCH 05/11] refactor(shared): group worktree, github, and linear modules into folders (#14437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `src/shared` is a flat directory of ~1,150 entries. The worktree, github, and linear domains accounted for 71 of them, so finding the module you wanted meant scanning a wall of same-prefixed filenames. Move each domain into its own folder and drop the now-redundant prefix: src/shared/github-pr-types.ts -> src/shared/github/pull-request-types.ts src/shared/worktree-id.ts -> src/shared/worktree/id.ts src/shared/linear-links.ts -> src/shared/linear/links.ts This follows the existing `network/` and `new-workspace/` convention in the same directory, which also drop the prefix inside the folder. Whole clusters move, including tests. Foldering only part of a domain would be worse than flat: a reader would have to check both `github/` and the flat directory, and `github-auth-types.ts` / `github-project-types.ts` are type modules that belong with the rest. No files with these prefixes remain flat. Import specifiers were rewritten by resolving each one to an absolute path and recomputing it, not by string substitution, so the `@/../../shared/...` alias forms are handled correctly. 501 specifiers across 298 files. Two things `tsc` cannot catch, handled explicitly: - `github-project-types.ts` carries its own `max-lines` bypass, so its baseline entry is REPOINTED to the new path rather than pruned. Pruning would drop the bypass and then flag the new path as a fresh violation. Ratchet stays at 345. - `mobile/` is outside `pnpm typecheck` and cannot be typechecked here (`mobile/node_modules` is empty). Instead every relative specifier in the repo was resolved against the filesystem: 174 unresolved before this change and 174 after — identical, so nothing broke in mobile either. The pinned `tests/e2e/.cross-version-checkouts` fixtures are deliberately NOT rewritten; they are a snapshot of an older release and still reference the old paths. Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted first — these projects are `composite: true` and reuse stale caches). --- config/max-lines-baseline.txt | 2 +- .../worktree-refresh-churn-benchmark.mjs | 2 +- mobile/app/h/[hostId]/tasks.tsx | 6 +- .../agent-history-session-worktree.ts | 2 +- mobile/src/session/github-pr-rpc.ts | 2 +- mobile/src/session/pr-actions-engine.ts | 2 +- mobile/src/session/use-live-worktree-name.ts | 2 +- mobile/src/tasks/github-project-reference.ts | 2 +- mobile/src/tasks/github-project-repo-match.ts | 2 +- mobile/src/tasks/smart-source-paste-intent.ts | 2 +- src/cli/handlers/linear-list-issues.ts | 2 +- src/cli/handlers/linear-relation-write.ts | 2 +- src/cli/handlers/linear-save-issue.ts | 2 +- src/cli/handlers/linear.ts | 4 +- .../handlers/worktree-linear-issue-link.ts | 2 +- src/cli/linear-format.test.ts | 2 +- src/cli/linear-format.ts | 4 +- src/cli/linear-request-builders.ts | 6 +- src/cli/linear-save-issue-request.ts | 2 +- .../first-work-branch-rename-test-harness.ts | 2 +- .../first-work-branch-rename.test.ts | 2 +- .../agent-hooks/first-work-branch-rename.ts | 2 +- .../agent-hooks/first-work-folder-rename.ts | 2 +- .../restored-subagent-liveness-sweep.ts | 2 +- src/main/automations/run-target-resolution.ts | 2 +- src/main/daemon/wsl-session-context.ts | 2 +- src/main/git/status.ts | 2 +- src/main/git/worktree.ts | 6 +- src/main/github/auth-diagnose.ts | 2 +- .../client-work-items-query-paging.test.ts | 2 +- src/main/github/client-work-items.test.ts | 2 +- src/main/github/client.ts | 10 +- src/main/github/gh-utils.test.ts | 2 +- src/main/github/github-api-repository.ts | 2 +- .../github/github-owner-repo-selection.ts | 2 +- src/main/github/github-pr-stack.ts | 2 +- .../github/merged-pr-commit-membership.ts | 2 +- .../github/pr-refresh-error-classification.ts | 2 +- src/main/github/project-view.test.ts | 2 +- src/main/github/project-view.ts | 6 +- src/main/github/project-view/internals.ts | 6 +- src/main/github/project-view/mutations.ts | 4 +- .../project-error-classification.ts | 4 +- src/main/github/rate-limit.ts | 2 +- src/main/github/stacked-pr-creation.ts | 2 +- src/main/index.ts | 2 +- src/main/ipc/filesystem.ts | 2 +- src/main/ipc/github.ts | 2 +- src/main/ipc/linear.ts | 4 +- .../ipc/local-worktree-runtime-options.ts | 2 +- src/main/ipc/notifications.ts | 2 +- src/main/ipc/pty.ts | 2 +- src/main/ipc/remote-workspace.ts | 2 +- src/main/ipc/repos.ts | 2 +- .../ipc/source-control-ai-linked-issue.ts | 2 +- src/main/ipc/watcher-removal-gate.test.ts | 2 +- src/main/ipc/watcher-removal-gate.ts | 2 +- .../ipc/workspace-cleanup-disconnected-ssh.ts | 2 +- src/main/ipc/workspace-cleanup-scan.ts | 2 +- src/main/ipc/worktree-folder-rename-target.ts | 2 +- src/main/ipc/worktree-git-status-ref-watch.ts | 2 +- src/main/ipc/worktree-logic.ts | 2 +- src/main/ipc/worktree-push-target-cleanup.ts | 2 +- src/main/ipc/worktree-remote.ts | 4 +- src/main/ipc/worktrees.ts | 10 +- src/main/linear/issue-activity-raw.ts | 2 +- src/main/linear/issue-context-client.ts | 4 +- src/main/linear/issue-context-current.ts | 2 +- src/main/linear/issue-context-errors.ts | 2 +- src/main/linear/issue-context-fanout.ts | 2 +- .../linear/issue-context-includes.test.ts | 2 +- src/main/linear/issue-context-includes.ts | 6 +- .../linear/issue-context-inline-media.test.ts | 2 +- src/main/linear/issue-context-pagination.ts | 2 +- src/main/linear/issue-context-raw.ts | 2 +- src/main/linear/issue-context-relations.ts | 4 +- src/main/linear/issue-context-workspaces.ts | 2 +- src/main/linear/issue-context.ts | 6 +- src/main/linear/issue-list-filter.ts | 2 +- src/main/linear/issue-relation-write.ts | 4 +- src/main/linear/issues.ts | 4 +- src/main/linear/linear-team-pages.ts | 2 +- src/main/linear/mcp-issue-list.ts | 2 +- src/main/linear/projects.ts | 2 +- src/main/local-project-runtime-resolution.ts | 2 +- src/main/local-worktree-removal-recovery.ts | 2 +- src/main/memory/collector.ts | 2 +- src/main/memory/hydrate-local-pty-registry.ts | 2 +- .../profile-project-worktree-identity.ts | 2 +- src/main/persistence.ts | 6 +- .../ports/ssh-advertised-url-enrichment.ts | 2 +- src/main/ports/workspace-port-ownership.ts | 2 +- .../agent-foreground-context-paths.ts | 2 +- src/main/providers/local-pty-provider.ts | 2 +- .../folder-workspace-pty-identity.test.ts | 2 +- src/main/runtime/linear-save-issue.test.ts | 2 +- ...issing-worktree-terminal-reconciliation.ts | 2 +- src/main/runtime/orca-runtime.test.ts | 2 +- src/main/runtime/orca-runtime.ts | 22 +-- src/main/runtime/orchestration/cli-command.ts | 2 +- src/main/runtime/rpc/errors.ts | 2 +- .../runtime/rpc/methods/client-ui-schemas.ts | 2 +- .../runtime/rpc/methods/client-ui.test.ts | 2 +- .../rpc/methods/linear-agent-access.ts | 2 +- .../linear-issue-attribute-filter-schema.ts | 2 +- .../runtime/rpc/methods/repo-update-schema.ts | 2 +- .../runtime/runtime-folder-workspace.test.ts | 2 +- src/main/runtime/runtime-folder-workspace.ts | 2 +- .../runtime/unstopped-pty-verification.ts | 2 +- ...e-session-terminal-membership-authority.ts | 2 +- .../worktree-teardown-unstopped-pty.test.ts | 2 +- src/main/runtime/worktree-teardown.ts | 4 +- .../source-control/hosted-review-creation.ts | 2 +- .../ssh-remote-linear-activity-output.test.ts | 2 +- src/main/ssh/ssh-remote-linear-list-issues.ts | 2 +- src/main/ssh/ssh-remote-linear-output.ts | 4 +- src/main/ssh/ssh-remote-linear-read-cli.ts | 2 +- .../ssh/ssh-remote-linear-relation-write.ts | 2 +- .../ssh/ssh-remote-linear-result-guards.ts | 2 +- .../ssh/ssh-remote-linear-write-support.ts | 2 +- src/main/usage-worktree-metadata.ts | 2 +- src/main/worktree-create-base-prefetch.ts | 2 +- src/preload/api-types.ts | 6 +- src/preload/index.ts | 4 +- src/relay/git-handler-worktree-ops.ts | 2 +- src/relay/git-handler-worktree-remove.ts | 4 +- src/relay/pty-handler.ts | 2 +- .../src/components/GitHubItemDialog.tsx | 4 +- .../src/components/PullRequestPage.tsx | 4 +- src/renderer/src/components/TaskPage.tsx | 12 +- .../automations/AutomationsPage.tsx | 2 +- .../src/components/github-pr-merge-state.ts | 2 +- .../github-project/GhAuthErrorHelp.test.ts | 2 +- .../github-project/GhAuthErrorHelp.tsx | 4 +- .../components/github-project/ProjectCell.tsx | 2 +- .../github-project/ProjectGroupHeader.tsx | 2 +- .../github-project/ProjectPicker.tsx | 6 +- .../components/github-project/ProjectRow.tsx | 2 +- .../github-project/ProjectViewList.tsx | 4 +- .../github-project/ProjectViewWrapper.tsx | 4 +- .../github-project/column-widths.ts | 2 +- .../src/components/github-project/columns.ts | 2 +- .../github-project-picker-filter.test.ts | 2 +- .../github-project-picker-filter.ts | 4 +- .../github-project/group-sort.test.ts | 4 +- .../project-picker-browse-cache.test.ts | 2 +- .../project-picker-browse-cache.ts | 2 +- .../project-row-filtering.test.ts | 2 +- .../github-project/project-row-filtering.ts | 2 +- ...ew-wrapper-source-context-boundary.test.ts | 2 +- .../project-visible-table-cache.test.ts | 2 +- .../project-visible-table-cache.ts | 2 +- .../github-project/slug-dialog/Comments.tsx | 2 +- .../github/IssueSourceIndicator.tsx | 2 +- .../github/github-work-item-edit-mutations.ts | 2 +- .../github/github-work-item-identity.ts | 2 +- .../src/components/linear-api-key-dialog.tsx | 2 +- ...-issue-attribute-filter-dropdowns.test.tsx | 2 +- ...inear-issue-attribute-filter-dropdowns.tsx | 2 +- ...linear-issue-attribute-filter-sections.tsx | 2 +- .../linear-issue-view-storage.test.ts | 2 +- .../components/linear-issue-view-storage.ts | 2 +- .../new-workspace/SmartWorkspaceNameField.tsx | 2 +- .../components/right-sidebar/ChecksPanel.tsx | 4 +- .../right-sidebar/HostedReviewActions.tsx | 2 +- .../right-sidebar/ai-vault-scope-paths.ts | 2 +- .../ai-vault-session-row-display.tsx | 2 +- .../ai-vault-session-worktree.ts | 2 +- .../checks-panel-async-result-key.ts | 2 +- .../checks-panel-terminal-worktree.ts | 2 +- .../components/settings/McpConfigSection.tsx | 2 +- .../settings/manage-sessions-format.ts | 2 +- .../settings/repository-icon-github.ts | 2 +- .../repository-runtime-session-summary.ts | 2 +- .../PreservedBranchBatchReviewDialog.tsx | 2 +- .../sidebar/WorktreeCard.pr-display.test.tsx | 2 +- .../src/components/sidebar/WorktreeList.tsx | 2 +- .../sidebar/WorktreeVisibilityDialog.tsx | 2 +- .../sidebar/WorktreeVisibilitySourceList.tsx | 2 +- .../sidebar/delete-worktree-failure-toast.tsx | 2 +- .../sidebar/delete-worktree-toast.test.ts | 2 +- .../sidebar/delete-worktree-toast.ts | 2 +- .../imported-worktrees-card-candidates.ts | 2 +- .../sidebar/preserved-branch-batch-toast.tsx | 2 +- .../sidebar/use-worktree-issue-link.ts | 2 +- .../sidebar/worktree-meta-updates.ts | 2 +- .../status-bar/WorkspaceSpaceManagerPanel.tsx | 2 +- .../status-bar/mergeSnapshotAndSessions.ts | 2 +- .../tab-bar/TabBarQuickCommandsButton.tsx | 2 +- .../task-page-linear-in-orca-issues.ts | 2 +- ...task-page-linear-issue-empty-state.test.ts | 2 +- .../task-page-linear-issue-empty-state.ts | 2 +- .../task-page-linear-issue-request.test.ts | 2 +- .../task-page-linear-issue-request.ts | 2 +- .../task-page-localized-options.tsx | 4 +- .../components/terminal-pane/TerminalPane.tsx | 2 +- .../terminal-pane/pty-connection.test.ts | 2 +- .../terminal-pane/pty-connection.ts | 2 +- .../hooks/remote-workspace-session-merge.ts | 2 +- src/renderer/src/hooks/useComposerState.ts | 2 +- .../src/hooks/useGitHubSlugMetadata.ts | 6 +- .../src/lib/ai-vault-resume-target.ts | 2 +- .../src/lib/cmd-j-github-url-lookup.ts | 2 +- .../src/lib/cmd-j-linear-issue-intent.test.ts | 2 +- .../src/lib/connection-owner-resolution.ts | 2 +- .../src/lib/linear-issue-url-lookup.ts | 2 +- .../lib/linear-issue-workspace-attachment.ts | 2 +- src/renderer/src/lib/repo-slug-cache.test.ts | 2 +- src/renderer/src/lib/repo-slug-cache.ts | 2 +- src/renderer/src/lib/repo-slug-index.ts | 2 +- src/renderer/src/lib/smart-github-submit.ts | 2 +- .../lib/workspace-session-host-persistence.ts | 2 +- .../lib/workspace-session-hydration-keys.ts | 2 +- .../lib/worktree-palette-task-url-match.ts | 6 +- .../runtime/github-check-details-timeout.ts | 2 +- .../src/runtime/runtime-git-client.ts | 2 +- .../src/runtime/runtime-linear-client.ts | 4 +- .../slices/degraded-repo-worktree-validity.ts | 2 +- .../src/store/slices/github-checks.ts | 2 +- .../slices/github-work-items-query-bounds.ts | 2 +- src/renderer/src/store/slices/github.ts | 14 +- src/renderer/src/store/slices/linear.ts | 4 +- src/renderer/src/store/slices/repos.ts | 4 +- src/renderer/src/store/slices/terminals.ts | 2 +- .../src/store/slices/worktree-helpers.ts | 6 +- src/renderer/src/store/slices/worktrees.ts | 4 +- src/shared/agent-scratch-worktrees.ts | 2 +- src/shared/constants.ts | 4 +- src/shared/external-worktree-inbox.test.ts | 2 +- src/shared/folder-workspace-types.ts | 2 +- .../api-availability.test.ts} | 2 +- .../api-availability.ts} | 0 .../auth-types.ts} | 0 .../check-details-deadline.ts} | 0 .../check-types.ts} | 0 .../comment-types.ts} | 2 +- .../{github-links.ts => github/links.ts} | 0 .../project-group-sort.ts} | 2 +- .../project-identity.test.ts} | 2 +- .../project-identity.ts} | 2 +- .../project-ref-input.test.ts} | 2 +- .../project-ref-input.ts} | 2 +- .../project-types.ts} | 2 +- ...l-request-auto-merge-availability.test.ts} | 2 +- .../pull-request-auto-merge-availability.ts} | 2 +- .../pull-request-for-branch-outcome.test.ts} | 4 +- .../pull-request-for-branch-outcome.ts} | 2 +- .../pull-request-merge-methods.test.ts} | 2 +- .../pull-request-merge-methods.ts} | 2 +- .../pull-request-refresh-types.ts} | 4 +- .../pull-request-types.ts} | 0 .../rate-limit-types.ts} | 0 .../repository-identity-key.test.ts} | 2 +- .../repository-identity-key.ts} | 0 .../work-item-types.ts} | 8 +- .../work-items-query-bounds.test.ts} | 2 +- .../work-items-query-bounds.ts} | 2 +- src/shared/global-settings-types.ts | 4 +- src/shared/issue-link-input.ts | 4 +- .../agent-access.ts} | 10 +- .../agent-result-types.ts} | 6 +- .../inline-media.test.ts} | 2 +- .../inline-media.ts} | 0 .../issue-activity.ts} | 2 +- .../issue-attribute-filter.test.ts} | 2 +- .../issue-attribute-filter.ts} | 0 .../issue-read-limits.ts} | 0 .../issue-relation-write.ts} | 4 +- .../issue-types.ts} | 2 +- .../issue-view-resume-state.test.ts} | 4 +- .../issue-view-resume-state.ts} | 2 +- .../links.test.ts} | 2 +- .../{linear-links.ts => linear/links.ts} | 2 +- .../mcp-issue-list.ts} | 4 +- .../project-list-format.ts} | 2 +- .../project-types.ts} | 0 src/shared/{linear-uuid.ts => linear/uuid.ts} | 0 .../workspace-types.ts} | 0 src/shared/new-workspace/github-links.ts | 4 +- .../smart-workspace-linear-intent.ts | 2 +- src/shared/new-workspace/workspace-source.ts | 2 +- src/shared/persisted-state-types.ts | 8 +- src/shared/persisted-ui-state-types.ts | 2 +- src/shared/project-host-setup-projection.ts | 2 +- .../remote-workspace-session-projection.ts | 2 +- src/shared/repo-types.ts | 2 +- src/shared/task-provider-identity.ts | 2 +- .../terminal-github-pr-link-detector.ts | 4 +- src/shared/terminal-startup-cwd.ts | 2 +- src/shared/types.ts | 149 ++++++++++-------- src/shared/ui-chrome-types.ts | 4 +- .../workspace-session-terminal-buffers.ts | 2 +- .../base-ref-drift-types.ts} | 0 .../base-ref.test.ts} | 2 +- .../base-ref.ts} | 0 .../card-properties.test.ts} | 2 +- .../card-properties.ts} | 2 +- .../create-types.ts} | 22 ++- .../id.test.ts} | 2 +- src/shared/{worktree-id.ts => worktree/id.ts} | 4 +- .../launch-types.ts} | 12 +- .../lineage-types.ts} | 2 +- .../meta-types.ts} | 14 +- .../ownership-worktree-base-path.test.ts} | 4 +- .../ownership.test.ts} | 6 +- .../ownership.ts} | 16 +- .../removal-fence-error.test.ts} | 2 +- .../removal-fence-error.ts} | 0 .../removal-force-classification.test.ts} | 2 +- .../removal.ts} | 2 +- .../sort-order-update.test.ts} | 2 +- .../sort-order-update.ts} | 0 .../submodule-removal.test.ts} | 2 +- .../submodule-removal.ts} | 0 .../{worktree-types.ts => worktree/types.ts} | 14 +- .../visibility-sources.test.ts} | 4 +- .../visibility-sources.ts} | 4 +- ...completed-worker-retirement-resume.spec.ts | 2 +- 318 files changed, 526 insertions(+), 509 deletions(-) rename src/shared/{github-api-availability.test.ts => github/api-availability.test.ts} (98%) rename src/shared/{github-api-availability.ts => github/api-availability.ts} (100%) rename src/shared/{github-auth-types.ts => github/auth-types.ts} (100%) rename src/shared/{github-check-details-deadline.ts => github/check-details-deadline.ts} (100%) rename src/shared/{github-check-types.ts => github/check-types.ts} (100%) rename src/shared/{github-comment-types.ts => github/comment-types.ts} (97%) rename src/shared/{github-links.ts => github/links.ts} (100%) rename src/shared/{github-project-group-sort.ts => github/project-group-sort.ts} (99%) rename src/shared/{github-project-identity.test.ts => github/project-identity.test.ts} (90%) rename src/shared/{github-project-identity.ts => github/project-identity.ts} (92%) rename src/shared/{github-project-ref-input.test.ts => github/project-ref-input.test.ts} (97%) rename src/shared/{github-project-ref-input.ts => github/project-ref-input.ts} (95%) rename src/shared/{github-project-types.ts => github/project-types.ts} (99%) rename src/shared/{github-pr-auto-merge-availability.test.ts => github/pull-request-auto-merge-availability.test.ts} (98%) rename src/shared/{github-pr-auto-merge-availability.ts => github/pull-request-auto-merge-availability.ts} (99%) rename src/shared/{github-pr-for-branch-outcome.test.ts => github/pull-request-for-branch-outcome.test.ts} (88%) rename src/shared/{github-pr-for-branch-outcome.ts => github/pull-request-for-branch-outcome.ts} (89%) rename src/shared/{github-pr-merge-methods.test.ts => github/pull-request-merge-methods.test.ts} (97%) rename src/shared/{github-pr-merge-methods.ts => github/pull-request-merge-methods.ts} (99%) rename src/shared/{github-pr-refresh-types.ts => github/pull-request-refresh-types.ts} (98%) rename src/shared/{github-pr-types.ts => github/pull-request-types.ts} (100%) rename src/shared/{github-rate-limit-types.ts => github/rate-limit-types.ts} (100%) rename src/shared/{github-repository-identity-key.test.ts => github/repository-identity-key.test.ts} (84%) rename src/shared/{github-repository-identity-key.ts => github/repository-identity-key.ts} (100%) rename src/shared/{github-work-item-types.ts => github/work-item-types.ts} (96%) rename src/shared/{github-work-items-query-bounds.test.ts => github/work-items-query-bounds.test.ts} (95%) rename src/shared/{github-work-items-query-bounds.ts => github/work-items-query-bounds.ts} (87%) rename src/shared/{linear-agent-access.ts => linear/agent-access.ts} (96%) rename src/shared/{linear-agent-result-types.ts => linear/agent-result-types.ts} (97%) rename src/shared/{linear-inline-media.test.ts => linear/inline-media.test.ts} (95%) rename src/shared/{linear-inline-media.ts => linear/inline-media.ts} (100%) rename src/shared/{linear-issue-activity.ts => linear/issue-activity.ts} (89%) rename src/shared/{linear-issue-attribute-filter.test.ts => linear/issue-attribute-filter.test.ts} (99%) rename src/shared/{linear-issue-attribute-filter.ts => linear/issue-attribute-filter.ts} (100%) rename src/shared/{linear-issue-read-limits.ts => linear/issue-read-limits.ts} (100%) rename src/shared/{linear-issue-relation-write.ts => linear/issue-relation-write.ts} (86%) rename src/shared/{linear-issue-types.ts => linear/issue-types.ts} (92%) rename src/shared/{linear-issue-view-resume-state.test.ts => linear/issue-view-resume-state.test.ts} (99%) rename src/shared/{linear-issue-view-resume-state.ts => linear/issue-view-resume-state.ts} (99%) rename src/shared/{linear-links.test.ts => linear/links.test.ts} (99%) rename src/shared/{linear-links.ts => linear/links.ts} (99%) rename src/shared/{linear-mcp-issue-list.ts => linear/mcp-issue-list.ts} (90%) rename src/shared/{linear-project-list-format.ts => linear/project-list-format.ts} (94%) rename src/shared/{linear-project-types.ts => linear/project-types.ts} (100%) rename src/shared/{linear-uuid.ts => linear/uuid.ts} (100%) rename src/shared/{linear-workspace-types.ts => linear/workspace-types.ts} (100%) rename src/shared/{worktree-base-ref-drift-types.ts => worktree/base-ref-drift-types.ts} (100%) rename src/shared/{worktree-base-ref.test.ts => worktree/base-ref.test.ts} (97%) rename src/shared/{worktree-base-ref.ts => worktree/base-ref.ts} (100%) rename src/shared/{worktree-card-properties.test.ts => worktree/card-properties.test.ts} (98%) rename src/shared/{worktree-card-properties.ts => worktree/card-properties.ts} (99%) rename src/shared/{worktree-create-types.ts => worktree/create-types.ts} (92%) rename src/shared/{worktree-id.test.ts => worktree/id.test.ts} (99%) rename src/shared/{worktree-id.ts => worktree/id.ts} (93%) rename src/shared/{worktree-launch-types.ts => worktree/launch-types.ts} (74%) rename src/shared/{worktree-lineage-types.ts => worktree/lineage-types.ts} (95%) rename src/shared/{worktree-meta-types.ts => worktree/meta-types.ts} (90%) rename src/shared/{worktree-ownership-worktree-base-path.test.ts => worktree/ownership-worktree-base-path.test.ts} (96%) rename src/shared/{worktree-ownership.test.ts => worktree/ownership.test.ts} (99%) rename src/shared/{worktree-ownership.ts => worktree/ownership.ts} (96%) rename src/shared/{worktree-removal-fence-error.test.ts => worktree/removal-fence-error.test.ts} (96%) rename src/shared/{worktree-removal-fence-error.ts => worktree/removal-fence-error.ts} (100%) rename src/shared/{worktree-removal-force-classification.test.ts => worktree/removal-force-classification.test.ts} (99%) rename src/shared/{worktree-removal.ts => worktree/removal.ts} (99%) rename src/shared/{worktree-sort-order-update.test.ts => worktree/sort-order-update.test.ts} (95%) rename src/shared/{worktree-sort-order-update.ts => worktree/sort-order-update.ts} (100%) rename src/shared/{worktree-submodule-removal.test.ts => worktree/submodule-removal.test.ts} (93%) rename src/shared/{worktree-submodule-removal.ts => worktree/submodule-removal.ts} (100%) rename src/shared/{worktree-types.ts => worktree/types.ts} (94%) rename src/shared/{worktree-visibility-sources.test.ts => worktree/visibility-sources.test.ts} (98%) rename src/shared/{worktree-visibility-sources.ts => worktree/visibility-sources.ts} (99%) diff --git a/config/max-lines-baseline.txt b/config/max-lines-baseline.txt index bece70eff7e..17f573b47d1 100644 --- a/config/max-lines-baseline.txt +++ b/config/max-lines-baseline.txt @@ -318,7 +318,7 @@ inline src/shared/agent-hook-listener.ts inline src/shared/automation-schedules.ts inline src/shared/commit-message-agent-spec.ts inline src/shared/constants.ts -inline src/shared/github-project-types.ts +inline src/shared/github/project-types.ts inline src/shared/keybindings.test.ts inline src/shared/keybindings.ts inline src/shared/marine-creatures.ts diff --git a/config/scripts/worktree-refresh-churn-benchmark.mjs b/config/scripts/worktree-refresh-churn-benchmark.mjs index 67265305322..5a165801ded 100644 --- a/config/scripts/worktree-refresh-churn-benchmark.mjs +++ b/config/scripts/worktree-refresh-churn-benchmark.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node import { performance } from 'node:perf_hooks' -import { planWorktreeSortOrderUpdates } from '../../src/shared/worktree-sort-order-update.ts' +import { planWorktreeSortOrderUpdates } from '../../src/shared/worktree/sort-order-update.ts' import { reuseEqualCatalogRows } from '../../src/renderer/src/store/slices/worktree-catalog-reconciliation.ts' const WORKTREE_COUNT = 655 diff --git a/mobile/app/h/[hostId]/tasks.tsx b/mobile/app/h/[hostId]/tasks.tsx index 553490b4748..7ef451df8aa 100644 --- a/mobile/app/h/[hostId]/tasks.tsx +++ b/mobile/app/h/[hostId]/tasks.tsx @@ -119,11 +119,11 @@ import { isIterationCurrent, sortRows, type ProjectGroup -} from '../../../../src/shared/github-project-group-sort' +} from '../../../../src/shared/github/project-group-sort' import type { GitHubProjectSortDirection, GitHubProjectTable as SharedGitHubProjectTable -} from '../../../../src/shared/github-project-types' +} from '../../../../src/shared/github/project-types' import { CROSS_REPO_DISPLAY_LIMIT, isGitHubWorkItemsSshRemoteRequiredError, @@ -161,7 +161,7 @@ import type { HostedReviewDecision } from '../../../../src/shared/hosted-review' import { githubProjectHost, githubProjectIdentityKey as githubProjectKey -} from '../../../../src/shared/github-project-identity' +} from '../../../../src/shared/github/project-identity' type RepoSummary = { id: string diff --git a/mobile/src/agent-history/agent-history-session-worktree.ts b/mobile/src/agent-history/agent-history-session-worktree.ts index 0e859bbf5ee..4e2a6c7b6a6 100644 --- a/mobile/src/agent-history/agent-history-session-worktree.ts +++ b/mobile/src/agent-history/agent-history-session-worktree.ts @@ -1,6 +1,6 @@ import { isPathInsideOrEqual, isRuntimePathAbsolute } from '../../../src/shared/cross-platform-path' import { parseWslUncPath } from '../../../src/shared/wsl-paths' -import { splitWorktreeIdForFilesystem } from '../../../src/shared/worktree-id' +import { splitWorktreeIdForFilesystem } from '../../../src/shared/worktree/id' import type { AiVaultSession } from '../../../src/shared/ai-vault-types' import type { Worktree } from '../worktree/workspace-list-types' diff --git a/mobile/src/session/github-pr-rpc.ts b/mobile/src/session/github-pr-rpc.ts index a2583376662..a34ed49581c 100644 --- a/mobile/src/session/github-pr-rpc.ts +++ b/mobile/src/session/github-pr-rpc.ts @@ -9,7 +9,7 @@ import type { HostedReviewInfo } from '../../../src/shared/hosted-review' import { normalizeGitHubPRForBranchOutcome, type GitHubPRForBranchResponse -} from '../../../src/shared/github-pr-for-branch-outcome' +} from '../../../src/shared/github/pull-request-for-branch-outcome' import type { RpcClient } from '../transport/rpc-client' import type { RpcSuccess } from '../transport/types' import { mobileRepoSelectorFromWorktreeId } from '../source-control/mobile-pr-create' diff --git a/mobile/src/session/pr-actions-engine.ts b/mobile/src/session/pr-actions-engine.ts index ce19c9bbbf9..cc5bfde80a3 100644 --- a/mobile/src/session/pr-actions-engine.ts +++ b/mobile/src/session/pr-actions-engine.ts @@ -1,5 +1,5 @@ import type { GitHubPRMergeMethod, PRState } from '../../../src/shared/types' -import { githubRepoIdentityKey } from '../../../src/shared/github-repository-identity-key' +import { githubRepoIdentityKey } from '../../../src/shared/github/repository-identity-key' import { classifyPrSidebarFailure } from './mobile-pr-sidebar-state' import { createOptimisticField, type OptimisticField } from './optimistic-write-sequence' import type { GitHubPrMutationOutcome } from './github-pr-mutations' diff --git a/mobile/src/session/use-live-worktree-name.ts b/mobile/src/session/use-live-worktree-name.ts index a2bc6d1175c..f0e9fa32138 100644 --- a/mobile/src/session/use-live-worktree-name.ts +++ b/mobile/src/session/use-live-worktree-name.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { useFocusEffect } from 'expo-router' import type { RuntimeClientEventStreamMessage } from '../../../src/shared/runtime-client-events' -import { getRepoIdFromWorktreeId } from '../../../src/shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../../src/shared/worktree/id' import type { RpcClient } from '../transport/rpc-client' import type { ConnectionState, RpcSuccess } from '../transport/types' import { getLiveWorktreeDisplayName, type WorktreeDisplayNameSource } from './worktree-display-name' diff --git a/mobile/src/tasks/github-project-reference.ts b/mobile/src/tasks/github-project-reference.ts index 00047f15f53..5ed7ef22ead 100644 --- a/mobile/src/tasks/github-project-reference.ts +++ b/mobile/src/tasks/github-project-reference.ts @@ -1,4 +1,4 @@ -import type { GitHubProjectIdentity } from '../../../src/shared/github-project-identity' +import type { GitHubProjectIdentity } from '../../../src/shared/github/project-identity' export type GitHubProjectOwnerType = GitHubProjectIdentity['ownerType'] export type GitHubProjectRef = GitHubProjectIdentity diff --git a/mobile/src/tasks/github-project-repo-match.ts b/mobile/src/tasks/github-project-repo-match.ts index 06a4f94e802..7a9ba339d1a 100644 --- a/mobile/src/tasks/github-project-repo-match.ts +++ b/mobile/src/tasks/github-project-repo-match.ts @@ -1,7 +1,7 @@ import { githubRepoIdentityKey, isDefaultGitHubHost -} from '../../../src/shared/github-repository-identity-key' +} from '../../../src/shared/github/repository-identity-key' export type GitHubProjectRepoMatch = { id: string diff --git a/mobile/src/tasks/smart-source-paste-intent.ts b/mobile/src/tasks/smart-source-paste-intent.ts index 00f4c0d9ee7..534d36fd0f4 100644 --- a/mobile/src/tasks/smart-source-paste-intent.ts +++ b/mobile/src/tasks/smart-source-paste-intent.ts @@ -9,7 +9,7 @@ import { parseGitLabIssueOrMRLink } from '../../../src/shared/new-workspace/gitl import { isSmartWorkspaceSourceQueryWithinLimit } from '../../../src/shared/new-workspace/smart-workspace-source-results' import type { RpcClient } from '../transport/rpc-client' import type { RpcSuccess } from '../transport/types' -import { githubRepoIdentityKey } from '../../../src/shared/github-repository-identity-key' +import { githubRepoIdentityKey } from '../../../src/shared/github/repository-identity-key' // A repo the picker can switch to for a cross-repo GitHub paste. Slug is derived // best-effort from the repo's remote metadata. diff --git a/src/cli/handlers/linear-list-issues.ts b/src/cli/handlers/linear-list-issues.ts index 8e4801c1ec5..51efa1b711e 100644 --- a/src/cli/handlers/linear-list-issues.ts +++ b/src/cli/handlers/linear-list-issues.ts @@ -1,7 +1,7 @@ import type { LinearMcpIssueListRequest, LinearMcpIssueListResult -} from '../../shared/linear-agent-access' +} from '../../shared/linear/agent-access' import type { CommandHandler } from '../dispatch' import { getOptionalNonNegativeIntegerFlag, diff --git a/src/cli/handlers/linear-relation-write.ts b/src/cli/handlers/linear-relation-write.ts index 7257780a86f..2a718442df7 100644 --- a/src/cli/handlers/linear-relation-write.ts +++ b/src/cli/handlers/linear-relation-write.ts @@ -2,7 +2,7 @@ import type { LinearIssueRelationship, LinearIssueRelationWriteRequest, LinearIssueRelationWriteResult -} from '../../shared/linear-agent-access' +} from '../../shared/linear/agent-access' import type { CommandHandler } from '../dispatch' import { getRequiredStringFlag } from '../flags' import { printResult } from '../format' diff --git a/src/cli/handlers/linear-save-issue.ts b/src/cli/handlers/linear-save-issue.ts index 7c64c6523b7..c946e0d15c9 100644 --- a/src/cli/handlers/linear-save-issue.ts +++ b/src/cli/handlers/linear-save-issue.ts @@ -1,7 +1,7 @@ import type { LinearSaveIssueRequest, LinearSaveIssueResult -} from '../../shared/linear-agent-access' +} from '../../shared/linear/agent-access' import type { CommandHandler } from '../dispatch' import { printResult } from '../format' import { formatLinearSaveIssue } from '../linear-format' diff --git a/src/cli/handlers/linear.ts b/src/cli/handlers/linear.ts index 68204fa7a9e..8a04fa4707a 100644 --- a/src/cli/handlers/linear.ts +++ b/src/cli/handlers/linear.ts @@ -19,8 +19,8 @@ import type { LinearTeamListResult, LinearTeamMembersResult, LinearTeamStatesResult -} from '../../shared/linear-agent-access' -import { clampLinearSearchLimit } from '../../shared/linear-agent-access' +} from '../../shared/linear/agent-access' +import { clampLinearSearchLimit } from '../../shared/linear/agent-access' import type { CommandHandler } from '../dispatch' import { printResult } from '../format' import { RuntimeClientError } from '../runtime-client' diff --git a/src/cli/handlers/worktree-linear-issue-link.ts b/src/cli/handlers/worktree-linear-issue-link.ts index c8e1020d847..01c90e4d1f9 100644 --- a/src/cli/handlers/worktree-linear-issue-link.ts +++ b/src/cli/handlers/worktree-linear-issue-link.ts @@ -2,7 +2,7 @@ import { buildLinearIssueLinkUpdates, LINEAR_ISSUE_LINK_CLEARED, type LinearIssueLinkUpdates -} from '../../shared/linear-links' +} from '../../shared/linear/links' import { RuntimeClientError } from '../runtime-client' export function getOptionalLinearIssueLinkFlag( diff --git a/src/cli/linear-format.test.ts b/src/cli/linear-format.test.ts index ab6a7681133..caa6b0bb7de 100644 --- a/src/cli/linear-format.test.ts +++ b/src/cli/linear-format.test.ts @@ -5,7 +5,7 @@ import type { LinearMcpIssueListResult, LinearProjectListResult, LinearSearchResult -} from '../shared/linear-agent-access' +} from '../shared/linear/agent-access' import { formatLinearCreate, formatLinearIssue, diff --git a/src/cli/linear-format.ts b/src/cli/linear-format.ts index d6aa5df0a95..969923d46ef 100644 --- a/src/cli/linear-format.ts +++ b/src/cli/linear-format.ts @@ -16,11 +16,11 @@ import type { LinearTeamMembersResult, LinearTeamStatesResult, LinearStatusSetResult -} from '../shared/linear-agent-access' +} from '../shared/linear/agent-access' import { formatLinearProjectListRows, linearProjectListWarningLines -} from '../shared/linear-project-list-format' +} from '../shared/linear/project-list-format' export function formatLinearIssue(result: LinearIssueContextResult): string { const issue = result.issue diff --git a/src/cli/linear-request-builders.ts b/src/cli/linear-request-builders.ts index 34d1ee9d473..4af1f91dd57 100644 --- a/src/cli/linear-request-builders.ts +++ b/src/cli/linear-request-builders.ts @@ -6,13 +6,13 @@ import type { LinearIssueRequest, LinearIssueTaskUpdateRequest, LinearWriteTargetRequest -} from '../shared/linear-agent-access' +} from '../shared/linear/agent-access' import { LINEAR_CHILDREN_MAX_DEPTH, LINEAR_WRITE_BODY_CAP, clampLinearIssueDepth -} from '../shared/linear-agent-access' -import { isLinearUuid } from '../shared/linear-uuid' +} from '../shared/linear/agent-access' +import { isLinearUuid } from '../shared/linear/uuid' import { getOptionalNonNegativeIntegerFlag, getOptionalStringFlag, diff --git a/src/cli/linear-save-issue-request.ts b/src/cli/linear-save-issue-request.ts index 786f2b252be..95111463cc8 100644 --- a/src/cli/linear-save-issue-request.ts +++ b/src/cli/linear-save-issue-request.ts @@ -1,4 +1,4 @@ -import type { LinearSaveIssueRequest } from '../shared/linear-agent-access' +import type { LinearSaveIssueRequest } from '../shared/linear/agent-access' import { getOptionalNullableNumberFlag, getOptionalStringFlag, 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 1f30a6e6b2c..427e2539722 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,6 +1,6 @@ import type { vi } from 'vitest' import type { GlobalSettings, Repo } from '../../shared/types' -import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree-id' +import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree/id' import type { FirstWorkBranchRenameDeps, FirstWorkBranchRenameEvent 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 e8d8cd5f373..7690215eedb 100644 --- a/src/main/agent-hooks/first-work-branch-rename.test.ts +++ b/src/main/agent-hooks/first-work-branch-rename.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { GlobalSettings, Repo } from '../../shared/types' -import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree-id' +import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree/id' const { gitExecFileAsyncMock, diff --git a/src/main/agent-hooks/first-work-branch-rename.ts b/src/main/agent-hooks/first-work-branch-rename.ts index 02784cbef86..ea2614a344a 100644 --- a/src/main/agent-hooks/first-work-branch-rename.ts +++ b/src/main/agent-hooks/first-work-branch-rename.ts @@ -1,6 +1,6 @@ // On first agent work in a fresh workspace, replace the auto-generated creature branch (e.g. `you/Nautilus`) with a short work-derived name. import type { GlobalSettings, Repo } from '../../shared/types' -import { getRepoIdFromWorktreeId, splitWorktreeIdForFilesystem } from '../../shared/worktree-id' +import { getRepoIdFromWorktreeId, splitWorktreeIdForFilesystem } from '../../shared/worktree/id' import { parseWorkspaceKey } from '../../shared/workspace-scope' import { parsePaneKey } from '../../shared/stable-pane-id' import { diff --git a/src/main/agent-hooks/first-work-folder-rename.ts b/src/main/agent-hooks/first-work-folder-rename.ts index c7050310834..79b3f98493e 100644 --- a/src/main/agent-hooks/first-work-folder-rename.ts +++ b/src/main/agent-hooks/first-work-folder-rename.ts @@ -10,7 +10,7 @@ import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR, getRepoIdFromWorktreeId, splitWorktreeIdForFilesystem -} from '../../shared/worktree-id' +} from '../../shared/worktree/id' import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' import { planWorktreeFolderRename } from '../ipc/worktree-folder-rename-target' diff --git a/src/main/agent-hooks/restored-subagent-liveness-sweep.ts b/src/main/agent-hooks/restored-subagent-liveness-sweep.ts index 6fcea40a8cf..e44a47f11d0 100644 --- a/src/main/agent-hooks/restored-subagent-liveness-sweep.ts +++ b/src/main/agent-hooks/restored-subagent-liveness-sweep.ts @@ -5,7 +5,7 @@ import { type ExecutionHostId } from '../../shared/execution-host' import type { FolderWorkspace, ProjectGroup } from '../../shared/types' -import { getRepoIdFromWorktreeId } from '../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../shared/worktree/id' import { parseWorkspaceKey } from '../../shared/workspace-scope' export type RestoredSubagentLivenessSweepDeps = { diff --git a/src/main/automations/run-target-resolution.ts b/src/main/automations/run-target-resolution.ts index 5ff447d6537..1168bf8fb6a 100644 --- a/src/main/automations/run-target-resolution.ts +++ b/src/main/automations/run-target-resolution.ts @@ -3,7 +3,7 @@ import type { Automation } from '../../shared/automations-types' import { getAutomationLegacyRepoId } from '../../shared/automation-run-identity' import { getRepoExecutionHostId, parseExecutionHostId } from '../../shared/execution-host' import type { ProjectHostSetup, Repo } from '../../shared/types' -import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id' +import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id' export type AutomationRunTargetResult = | { ok: true; cwd: string; repo: Repo; setup?: ProjectHostSetup } diff --git a/src/main/daemon/wsl-session-context.ts b/src/main/daemon/wsl-session-context.ts index fa69089f55b..0cf419872f9 100644 --- a/src/main/daemon/wsl-session-context.ts +++ b/src/main/daemon/wsl-session-context.ts @@ -1,4 +1,4 @@ -import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id' +import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id' import { isWslShellName } from '../../shared/local-windows-terminal-runtime' import { getDefaultWslDistro, parseWslPath } from '../wsl' import { parsePtySessionId } from './pty-session-id' diff --git a/src/main/git/status.ts b/src/main/git/status.ts index a51d1784ddf..31f364789de 100644 --- a/src/main/git/status.ts +++ b/src/main/git/status.ts @@ -46,7 +46,7 @@ import { removeSafeUntrackedDiscardTargets } from '../../shared/git-discard-path-safety' import { readBranchCompareHead } from '../../shared/git-branch-compare-head' -import { resolveWorktreeAddBaseRef } from '../../shared/worktree-base-ref' +import { resolveWorktreeAddBaseRef } from '../../shared/worktree/base-ref' import { resolveWorktreeBaseCommitOid } from './worktree-base-ref-probe' import { getLargeDiffRenderLimit } from '../../shared/large-diff-render-limit' import { InFlightPromiseDedupe, stableInFlightKey } from '../../shared/in-flight-promise-dedupe' diff --git a/src/main/git/worktree.ts b/src/main/git/worktree.ts index 9ee8eb2e188..11439bff0ae 100644 --- a/src/main/git/worktree.ts +++ b/src/main/git/worktree.ts @@ -5,7 +5,7 @@ import { branchHasNoUnmergedChangesWithLazyTargetRefresh, getBranchCleanupTargetRefs } from '../../shared/git-branch-cleanup' -import { resolveWorktreeAddBaseRef } from '../../shared/worktree-base-ref' +import { resolveWorktreeAddBaseRef } from '../../shared/worktree/base-ref' import { withSpan } from '../observability/tracer' import { withWorktreeRemoveStageSpan } from '../observability/instrumentation' import { @@ -20,8 +20,8 @@ import type { LocalBaseRefUpdateSuggestion, RemoveWorktreeResult } from '../../shared/types' -import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree-removal' -import { isSubmoduleWorktreeRemovalRefusal } from '../../shared/worktree-submodule-removal' +import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree/removal' +import { isSubmoduleWorktreeRemovalRefusal } from '../../shared/worktree/submodule-removal' import { decodeGitCQuotedPath } from '../../shared/git-cquoted-path' import { parseGitRevListAheadBehindCounts } from '../../shared/git-rev-list-output' import { parseWslUncPath } from '../../shared/wsl-paths' diff --git a/src/main/github/auth-diagnose.ts b/src/main/github/auth-diagnose.ts index 8051af7f8b4..61aafdd7cf8 100644 --- a/src/main/github/auth-diagnose.ts +++ b/src/main/github/auth-diagnose.ts @@ -12,7 +12,7 @@ * uses stable field labels ("Token scopes:", "(GITHUB_TOKEN)", etc.). */ import { ghExecFileAsync } from '../git/runner' -import type { GhAuthDiagnostic, GhAuthAccount } from '../../shared/github-auth-types' +import type { GhAuthDiagnostic, GhAuthAccount } from '../../shared/github/auth-types' // Required scopes for ProjectV2 GraphQL access in Orca. `project` is the // scope that gates ProjectV2 reads/writes; the others are needed for the diff --git a/src/main/github/client-work-items-query-paging.test.ts b/src/main/github/client-work-items-query-paging.test.ts index 7af6b124c34..0e8b62bdfcf 100644 --- a/src/main/github/client-work-items-query-paging.test.ts +++ b/src/main/github/client-work-items-query-paging.test.ts @@ -118,7 +118,7 @@ import { _resetMergeQueueCacheForTests, _resetOwnerRepoCache } from './client' -import { GITHUB_WORK_ITEMS_QUERY_MAX_BYTES } from '../../shared/github-work-items-query-bounds' +import { GITHUB_WORK_ITEMS_QUERY_MAX_BYTES } from '../../shared/github/work-items-query-bounds' import { _resetOriginGitHubApiRepositoryCache } from './github-api-repository' diff --git a/src/main/github/client-work-items.test.ts b/src/main/github/client-work-items.test.ts index 84119b3b2a2..f26ccfc12d9 100644 --- a/src/main/github/client-work-items.test.ts +++ b/src/main/github/client-work-items.test.ts @@ -112,7 +112,7 @@ import { _resetMergeQueueCacheForTests, _resetOwnerRepoCache } from './client' -import { GITHUB_WORK_ITEMS_QUERY_MAX_BYTES } from '../../shared/github-work-items-query-bounds' +import { GITHUB_WORK_ITEMS_QUERY_MAX_BYTES } from '../../shared/github/work-items-query-bounds' import { _resetOriginGitHubApiRepositoryCache } from './github-api-repository' diff --git a/src/main/github/client.ts b/src/main/github/client.ts index 068d3ccc419..2457c7f21f4 100644 --- a/src/main/github/client.ts +++ b/src/main/github/client.ts @@ -28,10 +28,10 @@ import { normalizeHostedReviewBaseRef, normalizeHostedReviewHeadRef } from '../../shared/hosted-review-refs' -import { normalizeGitHubPRMergeMethodSettings } from '../../shared/github-pr-merge-methods' +import { normalizeGitHubPRMergeMethodSettings } from '../../shared/github/pull-request-merge-methods' import { summarizeProviderChecks } from '../../shared/provider-check-summary' -import { isGitHubWorkItemsQueryTooLarge } from '../../shared/github-work-items-query-bounds' -import { classifyGitHubUnavailable } from '../../shared/github-api-availability' +import { isGitHubWorkItemsQueryTooLarge } from '../../shared/github/work-items-query-bounds' +import { classifyGitHubUnavailable } from '../../shared/github/api-availability' import { parseTaskQuery, type ParsedTaskQuery } from '../../shared/task-query' import { GITHUB_WORK_ITEMS_SSH_REMOTE_REQUIRED_MESSAGE, @@ -91,7 +91,7 @@ import { type GitHubRepoExecOptions, type GitHubApiRepository } from './github-api-repository' -import { githubRepoIdentityKey } from '../../shared/github-repository-identity-key' +import { githubRepoIdentityKey } from '../../shared/github/repository-identity-key' export { _resetOwnerRepoCache } from './gh-utils' export { getIssue, @@ -127,7 +127,7 @@ import { import { GITHUB_CHECK_DETAILS_HOST_TIMEOUT_MS, GITHUB_CHECK_DETAILS_TIMEOUT_MESSAGE -} from '../../shared/github-check-details-deadline' +} from '../../shared/github/check-details-deadline' import { hydrateGitHubPRStack, mergeGitHubPRStack } from './github-pr-stack' type GhExecOptions = GitHubRepoExecOptions & { signal?: AbortSignal } diff --git a/src/main/github/gh-utils.test.ts b/src/main/github/gh-utils.test.ts index ca9098e53bf..ba19fc13fda 100644 --- a/src/main/github/gh-utils.test.ts +++ b/src/main/github/gh-utils.test.ts @@ -38,7 +38,7 @@ import { __resetLocalGitConfigSignatureCacheForTests, readLocalGitConfigSignature } from './local-git-config-signature' -import { GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN } from '../../shared/github-work-items-query-bounds' +import { GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN } from '../../shared/github/work-items-query-bounds' describe('github owner/repo resolution', () => { beforeEach(() => { diff --git a/src/main/github/github-api-repository.ts b/src/main/github/github-api-repository.ts index 3a61b9387da..24037c4f73c 100644 --- a/src/main/github/github-api-repository.ts +++ b/src/main/github/github-api-repository.ts @@ -2,7 +2,7 @@ import type { GitHubOwnerRepo, IssueSourcePreference } from '../../shared/types' import { githubRepoIdentityKey, isDefaultGitHubHost -} from '../../shared/github-repository-identity-key' +} from '../../shared/github/repository-identity-key' import { getOwnerRepoForRemote, ghRepoExecOptions, diff --git a/src/main/github/github-owner-repo-selection.ts b/src/main/github/github-owner-repo-selection.ts index 63b6d75678e..2e7c6aa0f4c 100644 --- a/src/main/github/github-owner-repo-selection.ts +++ b/src/main/github/github-owner-repo-selection.ts @@ -1,5 +1,5 @@ import type { IssueSourcePreference } from '../../shared/types' -import { githubRepoIdentityKey } from '../../shared/github-repository-identity-key' +import { githubRepoIdentityKey } from '../../shared/github/repository-identity-key' import { getOwnerRepoForRemote, type LocalGitExecOptions, diff --git a/src/main/github/github-pr-stack.ts b/src/main/github/github-pr-stack.ts index 24d0773a91c..a11cf597769 100644 --- a/src/main/github/github-pr-stack.ts +++ b/src/main/github/github-pr-stack.ts @@ -6,7 +6,7 @@ import type { PRReviewDecision, PRState } from '../../shared/types' -import { githubRepoIdentityKey } from '../../shared/github-repository-identity-key' +import { githubRepoIdentityKey } from '../../shared/github/repository-identity-key' import { ghExecFileAsync } from '../git/runner' import { githubHostExecOptions, diff --git a/src/main/github/merged-pr-commit-membership.ts b/src/main/github/merged-pr-commit-membership.ts index 7350416ca6f..dd595da69f4 100644 --- a/src/main/github/merged-pr-commit-membership.ts +++ b/src/main/github/merged-pr-commit-membership.ts @@ -1,7 +1,7 @@ import { ghExecFileAsync } from './gh-utils' import { noteRepositoryRateLimitSpend, repositoryRateLimitGuard } from './rate-limit' import { githubHostExecOptions } from './github-api-repository' -import { githubRepoIdentityKey } from '../../shared/github-repository-identity-key' +import { githubRepoIdentityKey } from '../../shared/github/repository-identity-key' import type { OwnerRepo } from './github-repository-identity' type GhExecOptions = Parameters[1] diff --git a/src/main/github/pr-refresh-error-classification.ts b/src/main/github/pr-refresh-error-classification.ts index aa9e226939d..12c42d092b2 100644 --- a/src/main/github/pr-refresh-error-classification.ts +++ b/src/main/github/pr-refresh-error-classification.ts @@ -1,5 +1,5 @@ import type { PRRefreshErrorType } from '../../shared/types' -import { classifyGitHubUnavailable } from '../../shared/github-api-availability' +import { classifyGitHubUnavailable } from '../../shared/github/api-availability' import { extractExecError } from '../git/exec-error' /** diff --git a/src/main/github/project-view.test.ts b/src/main/github/project-view.test.ts index 1339c5c0fdc..48486ae4a9e 100644 --- a/src/main/github/project-view.test.ts +++ b/src/main/github/project-view.test.ts @@ -11,7 +11,7 @@ import { beforeEach, describe, expect, it } from 'vitest' import { GITHUB_PROJECT_REF_INPUT_MAX_BYTES, GITHUB_PROJECT_REF_INPUT_TOO_LARGE_ERROR -} from '../../shared/github-project-ref-input' +} from '../../shared/github/project-ref-input' import { PROJECT_VIEW_OWNER_CACHE_MAX_ENTRIES, _getProjectViewCacheSizesForTests, diff --git a/src/main/github/project-view.ts b/src/main/github/project-view.ts index db344de7133..cd763b2542e 100644 --- a/src/main/github/project-view.ts +++ b/src/main/github/project-view.ts @@ -46,12 +46,12 @@ import type { ListProjectViewsResult, ResolveProjectRefArgs, ResolveProjectRefResult -} from '../../shared/github-project-types' +} from '../../shared/github/project-types' import { GITHUB_PROJECT_REF_INPUT_TOO_LARGE_ERROR, isGitHubProjectRefInputTooLarge -} from '../../shared/github-project-ref-input' -import { githubProjectHost } from '../../shared/github-project-identity' +} from '../../shared/github/project-ref-input' +import { githubProjectHost } from '../../shared/github/project-identity' // Re-export the public API so existing `./project-view` call sites keep working; the split is internal-only. export { isValidOwnerSlug, isValidRepoSlug } from './project-view/internals' diff --git a/src/main/github/project-view/internals.ts b/src/main/github/project-view/internals.ts index 1dd794370c7..06f0f7d69cf 100644 --- a/src/main/github/project-view/internals.ts +++ b/src/main/github/project-view/internals.ts @@ -9,9 +9,9 @@ import { noteRepositoryRateLimitSpend, type RateLimitBucketKind } from '../rate-limit' -import type { GitHubProjectViewError } from '../../../shared/github-project-types' -import { githubProjectHost } from '../../../shared/github-project-identity' -import { isDefaultGitHubHost } from '../../../shared/github-repository-identity-key' +import type { GitHubProjectViewError } from '../../../shared/github/project-types' +import { githubProjectHost } from '../../../shared/github/project-identity' +import { isDefaultGitHubHost } from '../../../shared/github/repository-identity-key' import { isGitHubHostAuthenticatedForGlobalCli } from '../github-enterprise-repository' import { classifyProjectError, diff --git a/src/main/github/project-view/mutations.ts b/src/main/github/project-view/mutations.ts index 453a695baec..5f6044debbf 100644 --- a/src/main/github/project-view/mutations.ts +++ b/src/main/github/project-view/mutations.ts @@ -17,7 +17,7 @@ import { type GraphqlVars } from './internals' import { classifyProjectError, rateLimitedError } from './project-error-classification' -import { githubProjectHost } from '../../../shared/github-project-identity' +import { githubProjectHost } from '../../../shared/github/project-identity' import type { GitHubAssignableUser, GitHubWorkItemDetails, PRComment } from '../../../shared/types' import type { AddIssueCommentBySlugArgs, @@ -39,7 +39,7 @@ import type { UpdateIssueTypeBySlugArgs, UpdatePullRequestBySlugArgs, UpdateProjectItemFieldArgs -} from '../../../shared/github-project-types' +} from '../../../shared/github/project-types' function githubHostExecOptions(args: { host?: string }): { host: string } { return { host: githubProjectHost(args.host) } diff --git a/src/main/github/project-view/project-error-classification.ts b/src/main/github/project-view/project-error-classification.ts index 1468552b345..5db90f4305d 100644 --- a/src/main/github/project-view/project-error-classification.ts +++ b/src/main/github/project-view/project-error-classification.ts @@ -1,8 +1,8 @@ // Why: turns raw gh stderr/stdout into the typed GitHubProjectViewError the // renderer can act on (auth vs scope vs rate limit vs drift), shared by the // project-view read and mutation paths. -import type { GitHubProjectViewError } from '../../../shared/github-project-types' -import { githubProjectHost } from '../../../shared/github-project-identity' +import type { GitHubProjectViewError } from '../../../shared/github/project-types' +import { githubProjectHost } from '../../../shared/github/project-identity' export type GhGraphqlErrorShape = { type?: string diff --git a/src/main/github/rate-limit.ts b/src/main/github/rate-limit.ts index 2ca3f5cad43..4a48823164d 100644 --- a/src/main/github/rate-limit.ts +++ b/src/main/github/rate-limit.ts @@ -9,7 +9,7 @@ import type { GitHubRateLimitBucket, GitHubRateLimitSnapshot } from '../../shared/types' -import { isDefaultGitHubHost } from '../../shared/github-repository-identity-key' +import { isDefaultGitHubHost } from '../../shared/github/repository-identity-key' import { isWslUncPath } from '../../shared/wsl-paths' import { acquire, release } from './gh-utils' import { ghExecFileAsync } from '../git/runner' diff --git a/src/main/github/stacked-pr-creation.ts b/src/main/github/stacked-pr-creation.ts index e34752df6d9..2d060f65a83 100644 --- a/src/main/github/stacked-pr-creation.ts +++ b/src/main/github/stacked-pr-creation.ts @@ -2,7 +2,7 @@ import type { CreateStackedHostedReviewInput, CreateStackedHostedReviewResult } from '../../shared/hosted-review' -import { isDefaultGitHubHost } from '../../shared/github-repository-identity-key' +import { isDefaultGitHubHost } from '../../shared/github/repository-identity-key' import { normalizeHostedReviewBaseRef, normalizeHostedReviewHeadRef diff --git a/src/main/index.ts b/src/main/index.ts index b4e0e1b5ae4..6b7cafa79e4 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -267,7 +267,7 @@ import { configureWindowsHostGitEnvironmentReadiness, setDefaultWslDistroOverride } from './git/runner' -import { getRepoIdFromWorktreeId } from '../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../shared/worktree/id' import { parseWorkspaceKey } from '../shared/workspace-scope' import { setMigrationUnsupportedPtyListener } from './agent-hooks/migration-unsupported-pty-state' import { AgentBrowserBridge } from './browser/agent-browser-bridge' diff --git a/src/main/ipc/filesystem.ts b/src/main/ipc/filesystem.ts index 40701203e06..44239bdac74 100644 --- a/src/main/ipc/filesystem.ts +++ b/src/main/ipc/filesystem.ts @@ -135,7 +135,7 @@ import { import { listRepoWorktrees } from '../repo-worktrees' import { recordCrashBreadcrumb } from '../crash-reporting/crash-breadcrumb-store' import { buildReadDirErrorBreadcrumb, type ReadDirThrowSite } from './readdir-error-diagnostics' -import { splitWorktreeId } from '../../shared/worktree-id' +import { splitWorktreeId } from '../../shared/worktree/id' import { getRuntimePathBasename } from '../../shared/cross-platform-path' import type { LocalProjectWorktreeGitOptions } from '../project-runtime-git-options' import { registerLocalLogTailHandlers } from './local-log-tail' diff --git a/src/main/ipc/github.ts b/src/main/ipc/github.ts index 91cfc94f061..eda32115b19 100644 --- a/src/main/ipc/github.ts +++ b/src/main/ipc/github.ts @@ -107,7 +107,7 @@ import type { UpdateIssueTypeBySlugArgs, UpdateProjectItemFieldArgs, UpdatePullRequestBySlugArgs -} from '../../shared/github-project-types' +} from '../../shared/github/project-types' import { appStarSourceSchema } from '../../shared/gh-star-source' import { track } from '../telemetry/client' import { getCohortAtEmit } from '../telemetry/cohort-classifier' diff --git a/src/main/ipc/linear.ts b/src/main/ipc/linear.ts index 2e70cd0564d..0905c691f4c 100644 --- a/src/main/ipc/linear.ts +++ b/src/main/ipc/linear.ts @@ -24,8 +24,8 @@ import { } from '../linear/projects' import { listTeams, getTeamStates, getTeamLabels, getTeamMembers } from '../linear/teams' import type { LinearListFilter } from '../linear/issues' -import { clampLinearIssueListLimit } from '../../shared/linear-issue-read-limits' -import { optionalParsedLinearIssueAttributeFilter } from '../../shared/linear-issue-attribute-filter' +import { clampLinearIssueListLimit } from '../../shared/linear/issue-read-limits' +import { optionalParsedLinearIssueAttributeFilter } from '../../shared/linear/issue-attribute-filter' import type { LinearCustomViewModel, LinearIssueUpdate, diff --git a/src/main/ipc/local-worktree-runtime-options.ts b/src/main/ipc/local-worktree-runtime-options.ts index 13a13e7f536..f9850a7a399 100644 --- a/src/main/ipc/local-worktree-runtime-options.ts +++ b/src/main/ipc/local-worktree-runtime-options.ts @@ -4,7 +4,7 @@ import { getLocalProjectWorktreeGitOptions, type LocalProjectWorktreeGitOptions } from '../project-runtime-git-options' -import { splitWorktreeId } from '../../shared/worktree-id' +import { splitWorktreeId } from '../../shared/worktree/id' import type { Repo } from '../../shared/types' function comparableLocalPath(value: string): string { diff --git a/src/main/ipc/notifications.ts b/src/main/ipc/notifications.ts index 7418396a199..031cdf63378 100644 --- a/src/main/ipc/notifications.ts +++ b/src/main/ipc/notifications.ts @@ -21,7 +21,7 @@ import type { NotificationSettings, NotificationSoundDataResult } from '../../shared/types' -import { getRepoIdFromWorktreeId } from '../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../shared/worktree/id' import type { OrcaRuntimeService } from '../runtime/orca-runtime' import { buildNotificationOptions } from './notification-options' import { readNotificationAuthorizationStatus } from './notification-authorization-status' diff --git a/src/main/ipc/pty.ts b/src/main/ipc/pty.ts index 6e28044a889..680bc78b4ec 100644 --- a/src/main/ipc/pty.ts +++ b/src/main/ipc/pty.ts @@ -148,7 +148,7 @@ import { type TerminalStartupCwdMissingDirFallback } from '../../shared/terminal-startup-cwd' import { isWslUncPath, toWindowsWslPath } from '../../shared/wsl-paths' -import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id' +import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id' import type { AgentSessionOwnerBinding } from '../../shared/agent-session-host-authority' import { agentSessionOwnerBindingsEqual, diff --git a/src/main/ipc/remote-workspace.ts b/src/main/ipc/remote-workspace.ts index 713fe1b8a17..361f66bb21f 100644 --- a/src/main/ipc/remote-workspace.ts +++ b/src/main/ipc/remote-workspace.ts @@ -15,7 +15,7 @@ import type { } from '../../shared/remote-workspace-types' import type { SshTarget } from '../../shared/ssh-types' import type { WorkspaceSessionState } from '../../shared/types' -import { getRepoIdFromWorktreeId } from '../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../shared/worktree/id' import { getRemoteWorkspaceNamespace } from './remote-workspace-namespace' import { registerRemoteWorkspaceNotificationHandler } from './remote-workspace-events' diff --git a/src/main/ipc/repos.ts b/src/main/ipc/repos.ts index a9ec6dbf4c5..97d31dc36bf 100644 --- a/src/main/ipc/repos.ts +++ b/src/main/ipc/repos.ts @@ -121,7 +121,7 @@ import { prepareLocalWorktreeRootForRepo } from '../worktree-root-preparation' import { normalizeCustomWorktreeVisibilitySources, normalizeWorktreeVisibilitySourcePreferences -} from '../../shared/worktree-visibility-sources' +} from '../../shared/worktree/visibility-sources' import { runWithGitReadCacheInvalidation } from '../git/status' import { isAdmissibleDirectSshAuthority } from '../../shared/ssh-retained-payload-admission' import { isCurrentSshProviderAuthority } from '../ssh/ssh-provider-authority' diff --git a/src/main/ipc/source-control-ai-linked-issue.ts b/src/main/ipc/source-control-ai-linked-issue.ts index fa000148fef..aa0696dd913 100644 --- a/src/main/ipc/source-control-ai-linked-issue.ts +++ b/src/main/ipc/source-control-ai-linked-issue.ts @@ -2,7 +2,7 @@ import { resolve } from 'node:path' import type { Store } from '../persistence' import { isLinkedIssueNumber } from '../../shared/source-control-ai-action-variables' import type { WorktreeMeta } from '../../shared/types' -import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id' +import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id' export type LinkedIssueLookupArgs = { worktreeId?: string diff --git a/src/main/ipc/watcher-removal-gate.test.ts b/src/main/ipc/watcher-removal-gate.test.ts index 221f5f724c9..677cb381d1f 100644 --- a/src/main/ipc/watcher-removal-gate.test.ts +++ b/src/main/ipc/watcher-removal-gate.test.ts @@ -6,7 +6,7 @@ import { TerminalRemovalInProgressError, WatcherRemovalInProgressError } from './watcher-removal-gate' -import { isWorktreeRemovalFenceError } from '../../shared/worktree-removal-fence-error' +import { isWorktreeRemovalFenceError } from '../../shared/worktree/removal-fence-error' describe('watcher removal gate', () => { it('waits for an existing install and rejects later equivalent-path installs', async () => { diff --git a/src/main/ipc/watcher-removal-gate.ts b/src/main/ipc/watcher-removal-gate.ts index de19acfdf46..ec4bef288ca 100644 --- a/src/main/ipc/watcher-removal-gate.ts +++ b/src/main/ipc/watcher-removal-gate.ts @@ -5,7 +5,7 @@ import { import { TERMINAL_REMOVAL_IN_PROGRESS_MESSAGE, WATCHER_REMOVAL_IN_PROGRESS_MESSAGE -} from '../../shared/worktree-removal-fence-error' +} from '../../shared/worktree/removal-fence-error' // Why: fence slots are identities, not a counter, so a removal that gives up waiting can drop exactly // the installs it waited on without a late finishInstall corrupting the count for a newer install. diff --git a/src/main/ipc/workspace-cleanup-disconnected-ssh.ts b/src/main/ipc/workspace-cleanup-disconnected-ssh.ts index bd549a9b6ee..f7128d26827 100644 --- a/src/main/ipc/workspace-cleanup-disconnected-ssh.ts +++ b/src/main/ipc/workspace-cleanup-disconnected-ssh.ts @@ -6,7 +6,7 @@ import { createWorkspaceCleanupFingerprint, type WorkspaceCleanupCandidate } from '../../shared/workspace-cleanup' -import { splitWorktreeId } from '../../shared/worktree-id' +import { splitWorktreeId } from '../../shared/worktree/id' import { getNewestWorkspaceCleanupDiffCommentAt, getWorkspaceCleanupInactivityReasonsForWorkspace, diff --git a/src/main/ipc/workspace-cleanup-scan.ts b/src/main/ipc/workspace-cleanup-scan.ts index 32794f61977..41f94a07814 100644 --- a/src/main/ipc/workspace-cleanup-scan.ts +++ b/src/main/ipc/workspace-cleanup-scan.ts @@ -5,7 +5,7 @@ import type { IGitProvider } from '../providers/types' import { isFolderRepo } from '../../shared/repo-kind' import type { GitWorktreeInfo, Repo, Worktree } from '../../shared/types' import { mergeWorktree } from './worktree-logic' -import { splitWorktreeId } from '../../shared/worktree-id' +import { splitWorktreeId } from '../../shared/worktree/id' import type { WorkspaceCleanupCandidate, WorkspaceCleanupScanError, diff --git a/src/main/ipc/worktree-folder-rename-target.ts b/src/main/ipc/worktree-folder-rename-target.ts index 5725b4efd75..9cdbe7188c2 100644 --- a/src/main/ipc/worktree-folder-rename-target.ts +++ b/src/main/ipc/worktree-folder-rename-target.ts @@ -1,6 +1,6 @@ import { posix } from 'node:path' import type { GlobalSettings } from '../../shared/types' -import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree-id' +import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree/id' import { computeWorktreePath } from './worktree-logic' type WorktreePathSettings = Pick diff --git a/src/main/ipc/worktree-git-status-ref-watch.ts b/src/main/ipc/worktree-git-status-ref-watch.ts index f8035b0510f..db24dd6ac3c 100644 --- a/src/main/ipc/worktree-git-status-ref-watch.ts +++ b/src/main/ipc/worktree-git-status-ref-watch.ts @@ -1,6 +1,6 @@ import { resolveRuntimePath } from '../../shared/cross-platform-path' import { isSafeGitStatusUpstreamRef } from '../../shared/git-status-upstream-ref' -import { getRepoIdFromWorktreeId } from '../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../shared/worktree/id' import { pathRelativeToWorktreeWatchRoot, type WorktreeBaseWatchTarget diff --git a/src/main/ipc/worktree-logic.ts b/src/main/ipc/worktree-logic.ts index a056d657f02..45c6cbdff03 100644 --- a/src/main/ipc/worktree-logic.ts +++ b/src/main/ipc/worktree-logic.ts @@ -2,7 +2,7 @@ import { resolve, relative, isAbsolute, posix, sep, win32 } from 'node:path' import type { GlobalSettings, OrcaWorkspaceLayout, Repo } from '../../shared/types' import { isWindowsAbsolutePathLike, resolveRuntimePath } from '../../shared/cross-platform-path' import { isWslUncPath } from '../../shared/wsl-paths' -import { splitWorktreeId } from '../../shared/worktree-id' +import { splitWorktreeId } from '../../shared/worktree/id' import { replaceKnownEmojiWithShortcodes } from '../../shared/emoji-shortcode-catalog' import { getWslHome, parseWslPath } from '../wsl' diff --git a/src/main/ipc/worktree-push-target-cleanup.ts b/src/main/ipc/worktree-push-target-cleanup.ts index d7fdc7a04be..0fd1faf10ad 100644 --- a/src/main/ipc/worktree-push-target-cleanup.ts +++ b/src/main/ipc/worktree-push-target-cleanup.ts @@ -6,7 +6,7 @@ import type { Store } from '../persistence' import type { GitPushTarget } from '../../shared/types' import { parseGitHubOwnerRepo } from '../github/gh-utils' -import { getRepoIdFromWorktreeId } from '../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../shared/worktree/id' import { iterateProcessOutputLines } from '../../shared/process-output-field-scanner' export type GitRemoteExec = ( diff --git a/src/main/ipc/worktree-remote.ts b/src/main/ipc/worktree-remote.ts index f83c5035976..8d9e70481cd 100644 --- a/src/main/ipc/worktree-remote.ts +++ b/src/main/ipc/worktree-remote.ts @@ -32,7 +32,7 @@ import { import { resolveLocalGitUsername, getSshGitUsername } from '../git/git-username' import { hasCommitObjectViaGitExec } from '../git/commit-object-ref' import { resolveWorktreeCreateBase } from '../worktree-create-base' -import { resolveWorktreeAddBaseRef } from '../../shared/worktree-base-ref' +import { resolveWorktreeAddBaseRef } from '../../shared/worktree/base-ref' import { getHostedReviewForBranch } from '../source-control/hosted-review' import type { ForgeProviderId } from '../source-control/forge-provider' import { validateGitPushTarget } from '../git/push-target-validation' @@ -88,7 +88,7 @@ import { } from './worktree-logic' import { findCreatedWorktree } from './created-worktree-reconciliation' import type { BranchPrefixSettings } from '../../shared/branch-prefix' -import { getRepoIdFromWorktreeId } from '../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../shared/worktree/id' import { parseWorkspaceKey, worktreeWorkspaceKey } from '../../shared/workspace-scope' import { cleanupUnusedWorktreePushTargetRemoteWithExec, diff --git a/src/main/ipc/worktrees.ts b/src/main/ipc/worktrees.ts index 3400247002a..e56dd3cc13d 100644 --- a/src/main/ipc/worktrees.ts +++ b/src/main/ipc/worktrees.ts @@ -8,7 +8,7 @@ import { isFolderRepo } from '../../shared/repo-kind' import { readBranchRenameFailureOutputForDisplay } from '../agent-hooks/branch-rename-failure-output' import { parseWorkspaceKey } from '../../shared/workspace-scope' import { inspectSetupScriptImportCandidates } from '../../shared/setup-script-imports' -import { planWorktreeSortOrderUpdates } from '../../shared/worktree-sort-order-update' +import { planWorktreeSortOrderUpdates } from '../../shared/worktree/sort-order-update' import { getProjectHostSetupWorktreeMeta } from '../../shared/project-host-setup-projection' import { TaskSourceContextSchema } from '../../shared/task-source-context-schema' import { WorkspaceLinkedItemSchema } from '../../shared/workspace-linked-item-schema' @@ -37,7 +37,7 @@ import type { WorkspaceLineage, WorktreeMeta } from '../../shared/types' -import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree-removal' +import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree/removal' import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID, @@ -66,11 +66,11 @@ import { buildKnownOrcaWorkspaceLayouts, isLegacyRepoForExternalWorktreeVisibility, toDetectedWorktree -} from '../../shared/worktree-ownership' +} from '../../shared/worktree/ownership' import { createWorktreeVisibilitySourceMatcher, normalizeCustomWorktreeVisibilitySources -} from '../../shared/worktree-visibility-sources' +} from '../../shared/worktree/visibility-sources' import { assertWorktreeCleanForRemoval, forceDeleteLocalBranch, @@ -239,7 +239,7 @@ import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR, getRepoIdFromWorktreeId, getWorktreePathBasenameFromId -} from '../../shared/worktree-id' +} from '../../shared/worktree/id' import { prefetchWorktreeCreateBase } from '../worktree-create-base-prefetch' import { getLocalProjectGitExecOptions, diff --git a/src/main/linear/issue-activity-raw.ts b/src/main/linear/issue-activity-raw.ts index 992a175b701..c53e713e738 100644 --- a/src/main/linear/issue-activity-raw.ts +++ b/src/main/linear/issue-activity-raw.ts @@ -1,7 +1,7 @@ import type { LinearIssueActivityEntry, LinearIssueActivityValue -} from '../../shared/linear-issue-activity' +} from '../../shared/linear/issue-activity' import type { RawNamedEntity, RawPageInfo, RawUser } from './issue-context-raw' type RawActivityEntity = RawNamedEntity & { diff --git a/src/main/linear/issue-context-client.ts b/src/main/linear/issue-context-client.ts index c9e5085f3bb..1e3a45bd301 100644 --- a/src/main/linear/issue-context-client.ts +++ b/src/main/linear/issue-context-client.ts @@ -1,5 +1,5 @@ -import type { LinearSearchIssueSummary, LinearSearchResult } from '../../shared/linear-agent-access' -import { clampLinearSearchLimit } from '../../shared/linear-agent-access' +import type { LinearSearchIssueSummary, LinearSearchResult } from '../../shared/linear/agent-access' +import { clampLinearSearchLimit } from '../../shared/linear/agent-access' import type { LinearWorkspace } from '../../shared/types' import { acquire, diff --git a/src/main/linear/issue-context-current.ts b/src/main/linear/issue-context-current.ts index dbde6c8176f..e433435b421 100644 --- a/src/main/linear/issue-context-current.ts +++ b/src/main/linear/issue-context-current.ts @@ -1,4 +1,4 @@ -import { parseLinearIssueInput } from '../../shared/linear-links' +import { parseLinearIssueInput } from '../../shared/linear/links' import { getConnectedWorkspaces } from './issue-context-client' import { linearError } from './issue-context-errors' diff --git a/src/main/linear/issue-context-errors.ts b/src/main/linear/issue-context-errors.ts index 4e76835eb6a..1dde1b58537 100644 --- a/src/main/linear/issue-context-errors.ts +++ b/src/main/linear/issue-context-errors.ts @@ -1,4 +1,4 @@ -import type { LinearErrorCode, LinearIncludeErrorCode } from '../../shared/linear-agent-access' +import type { LinearErrorCode, LinearIncludeErrorCode } from '../../shared/linear/agent-access' export class LinearAgentAccessError extends Error { readonly code: LinearErrorCode diff --git a/src/main/linear/issue-context-fanout.ts b/src/main/linear/issue-context-fanout.ts index 484eec818f7..c28f03a2c9c 100644 --- a/src/main/linear/issue-context-fanout.ts +++ b/src/main/linear/issue-context-fanout.ts @@ -1,4 +1,4 @@ -import type { LinearErrorCode, LinearWorkspaceCandidate } from '../../shared/linear-agent-access' +import type { LinearErrorCode, LinearWorkspaceCandidate } from '../../shared/linear/agent-access' import type { LinearWorkspace } from '../../shared/types' import { getClients, getStatus, type LinearClientForWorkspace } from './client' import { diff --git a/src/main/linear/issue-context-includes.test.ts b/src/main/linear/issue-context-includes.test.ts index 05e5f2b9892..0d8d432a1c5 100644 --- a/src/main/linear/issue-context-includes.test.ts +++ b/src/main/linear/issue-context-includes.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { LinearIssueContextResult, LinearIssueRequest } from '../../shared/linear-agent-access' +import type { LinearIssueContextResult, LinearIssueRequest } from '../../shared/linear/agent-access' import type { ResolvedIssue } from './issue-context-client' import { ACTIVITY_QUERY } from './issue-activity-raw' import { diff --git a/src/main/linear/issue-context-includes.ts b/src/main/linear/issue-context-includes.ts index 13a8f4ae234..785bdd82f4f 100644 --- a/src/main/linear/issue-context-includes.ts +++ b/src/main/linear/issue-context-includes.ts @@ -7,7 +7,7 @@ import type { LinearIssueContextResult, LinearIssueInclude, LinearIssueRequest -} from '../../shared/linear-agent-access' +} from '../../shared/linear/agent-access' import { LINEAR_ATTACHMENTS_CAP, LINEAR_ACTIVITY_CAP, @@ -15,8 +15,8 @@ import { LINEAR_COMMENTS_CAP, LINEAR_COMMENT_BODY_CAP, clampLinearIssueDepth -} from '../../shared/linear-agent-access' -import { extractLinearInlineMedia } from '../../shared/linear-inline-media' +} from '../../shared/linear/agent-access' +import { extractLinearInlineMedia } from '../../shared/linear/inline-media' import type { ResolvedIssue } from './issue-context-client' import { getRequiredEntry, withLinearRead } from './issue-context-client' import { getPublicFileUrlClient } from './client' diff --git a/src/main/linear/issue-context-inline-media.test.ts b/src/main/linear/issue-context-inline-media.test.ts index 3b3b97994de..e3e484a4593 100644 --- a/src/main/linear/issue-context-inline-media.test.ts +++ b/src/main/linear/issue-context-inline-media.test.ts @@ -4,7 +4,7 @@ import type { LinearIssueCommentNode, LinearIssueContextResult, LinearIssueSummary -} from '../../shared/linear-agent-access' +} from '../../shared/linear/agent-access' import { collectInlineMedia } from './issue-context' function summary(overrides: Partial): LinearIssueSummary { diff --git a/src/main/linear/issue-context-pagination.ts b/src/main/linear/issue-context-pagination.ts index d8c5f239ac9..83118c41b72 100644 --- a/src/main/linear/issue-context-pagination.ts +++ b/src/main/linear/issue-context-pagination.ts @@ -1,4 +1,4 @@ -import { LINEAR_ISSUE_API_PAGE_SIZE_MAX } from '../../shared/linear-issue-read-limits' +import { LINEAR_ISSUE_API_PAGE_SIZE_MAX } from '../../shared/linear/issue-read-limits' export type LinearPageVariables = { first: number; after?: string } diff --git a/src/main/linear/issue-context-raw.ts b/src/main/linear/issue-context-raw.ts index 6f29e9e6a01..d421a17e017 100644 --- a/src/main/linear/issue-context-raw.ts +++ b/src/main/linear/issue-context-raw.ts @@ -2,7 +2,7 @@ import type { LinearCollectionMeta, LinearIssueSummary, LinearSearchIssueSummary -} from '../../shared/linear-agent-access' +} from '../../shared/linear/agent-access' export type RawIssueResponse = { issue?: RawIssue | null diff --git a/src/main/linear/issue-context-relations.ts b/src/main/linear/issue-context-relations.ts index fd39787d720..66e2fac9d3d 100644 --- a/src/main/linear/issue-context-relations.ts +++ b/src/main/linear/issue-context-relations.ts @@ -1,5 +1,5 @@ -import type { LinearCollectionMeta, LinearIssueRelation } from '../../shared/linear-agent-access' -import { LINEAR_RELATIONS_CAP } from '../../shared/linear-agent-access' +import type { LinearCollectionMeta, LinearIssueRelation } from '../../shared/linear/agent-access' +import { LINEAR_RELATIONS_CAP } from '../../shared/linear/agent-access' import type { ResolvedIssue } from './issue-context-client' import { getRequiredEntry, withLinearRead } from './issue-context-client' import { readConnectionPages } from './issue-context-pagination' diff --git a/src/main/linear/issue-context-workspaces.ts b/src/main/linear/issue-context-workspaces.ts index debdf646c4e..47ad620a0a3 100644 --- a/src/main/linear/issue-context-workspaces.ts +++ b/src/main/linear/issue-context-workspaces.ts @@ -1,4 +1,4 @@ -import type { LinearWorkspaceCandidate } from '../../shared/linear-agent-access' +import type { LinearWorkspaceCandidate } from '../../shared/linear/agent-access' import type { LinearWorkspace } from '../../shared/types' import { linearError } from './issue-context-errors' diff --git a/src/main/linear/issue-context.ts b/src/main/linear/issue-context.ts index 9116b9a591b..0cf5ea5fd0b 100644 --- a/src/main/linear/issue-context.ts +++ b/src/main/linear/issue-context.ts @@ -3,9 +3,9 @@ import type { LinearIssueChildNode, LinearIssueContextResult, LinearIssueRequest -} from '../../shared/linear-agent-access' -import { extractLinearInlineMedia } from '../../shared/linear-inline-media' -import { parseLinearIssueInput } from '../../shared/linear-links' +} from '../../shared/linear/agent-access' +import { extractLinearInlineMedia } from '../../shared/linear/inline-media' +import { parseLinearIssueInput } from '../../shared/linear/links' import { resolveIssue, searchLinearIssuesForAgents, diff --git a/src/main/linear/issue-list-filter.ts b/src/main/linear/issue-list-filter.ts index b85b7e3ba09..6190716ea0c 100644 --- a/src/main/linear/issue-list-filter.ts +++ b/src/main/linear/issue-list-filter.ts @@ -1,7 +1,7 @@ import { isEmptyLinearIssueAttributeFilter, type LinearIssueAttributeFilter -} from '../../shared/linear-issue-attribute-filter' +} from '../../shared/linear/issue-attribute-filter' import type { LinearListFilter } from './issues' // Why: attribute facets must merge into Linear's IssueFilter before the first-N diff --git a/src/main/linear/issue-relation-write.ts b/src/main/linear/issue-relation-write.ts index 6f9ce71c634..08f2a3bc91e 100644 --- a/src/main/linear/issue-relation-write.ts +++ b/src/main/linear/issue-relation-write.ts @@ -3,8 +3,8 @@ import { loadLinearSdk } from './linear-sdk' import type { LinearIssueRelationship, LinearIssueRelationWriteResult -} from '../../shared/linear-issue-relation-write' -import { LINEAR_ISSUE_API_PAGE_SIZE_MAX } from '../../shared/linear-issue-read-limits' +} from '../../shared/linear/issue-relation-write' +import { LINEAR_ISSUE_API_PAGE_SIZE_MAX } from '../../shared/linear/issue-read-limits' import { acquire, clearToken, getClients, isAuthError, release } from './client' import { linearError } from './issue-context-errors' import { diff --git a/src/main/linear/issues.ts b/src/main/linear/issues.ts index 1c8d7d8ee74..301bc66024a 100644 --- a/src/main/linear/issues.ts +++ b/src/main/linear/issues.ts @@ -14,11 +14,11 @@ import { loadLinearSdk } from './linear-sdk' import { LINEAR_ISSUE_API_PAGE_SIZE_MAX, clampLinearIssueListLimit -} from '../../shared/linear-issue-read-limits' +} from '../../shared/linear/issue-read-limits' import { isEmptyLinearIssueAttributeFilter, type LinearIssueAttributeFilter -} from '../../shared/linear-issue-attribute-filter' +} from '../../shared/linear/issue-attribute-filter' import { acquire, release, diff --git a/src/main/linear/linear-team-pages.ts b/src/main/linear/linear-team-pages.ts index 1da7fd6242f..abc1c485a8b 100644 --- a/src/main/linear/linear-team-pages.ts +++ b/src/main/linear/linear-team-pages.ts @@ -1,5 +1,5 @@ import type { LinearLabel, LinearMember, LinearTeam, LinearWorkflowState } from '../../shared/types' -import { buildLinearTeamUrl } from '../../shared/linear-links' +import { buildLinearTeamUrl } from '../../shared/linear/links' import type { LinearClientForWorkspace } from './client' const TEAM_PAGE_SIZE = 100 diff --git a/src/main/linear/mcp-issue-list.ts b/src/main/linear/mcp-issue-list.ts index 09c5fff3336..ff98278cc7d 100644 --- a/src/main/linear/mcp-issue-list.ts +++ b/src/main/linear/mcp-issue-list.ts @@ -1,7 +1,7 @@ import type { LinearMcpIssueListRequest, LinearMcpIssueListResult -} from '../../shared/linear-agent-access' +} from '../../shared/linear/agent-access' import { getClients, getStatus, type LinearClientForWorkspace } from './client' import { withLinearRead } from './issue-context-client' import { linearError } from './issue-context-errors' diff --git a/src/main/linear/projects.ts b/src/main/linear/projects.ts index 5945c3fc95b..d55d2f6e6a6 100644 --- a/src/main/linear/projects.ts +++ b/src/main/linear/projects.ts @@ -15,7 +15,7 @@ import type { import { LINEAR_ISSUE_API_PAGE_SIZE_MAX, clampLinearIssueListLimit -} from '../../shared/linear-issue-read-limits' +} from '../../shared/linear/issue-read-limits' import { acquire, clearToken, diff --git a/src/main/local-project-runtime-resolution.ts b/src/main/local-project-runtime-resolution.ts index d7588ae51a1..e86a2f31927 100644 --- a/src/main/local-project-runtime-resolution.ts +++ b/src/main/local-project-runtime-resolution.ts @@ -10,7 +10,7 @@ import { hasCachedWslAvailability, hasCachedWslDistros } from './wsl' -import { getRepoIdFromWorktreeId } from '../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../shared/worktree/id' import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../shared/execution-host' function canResolveProjectRuntimeForRepo(store: Store): boolean { diff --git a/src/main/local-worktree-removal-recovery.ts b/src/main/local-worktree-removal-recovery.ts index 5a920a4ee6f..578743029f6 100644 --- a/src/main/local-worktree-removal-recovery.ts +++ b/src/main/local-worktree-removal-recovery.ts @@ -1,5 +1,5 @@ import type { GitWorktreeInfo, RemoveWorktreeResult } from '../shared/types' -import { assertWorktreeUnlockedForRemoval } from '../shared/worktree-removal' +import { assertWorktreeUnlockedForRemoval } from '../shared/worktree/removal' import { areWorktreePathsEqual, formatWorktreeRemovalError } from './ipc/worktree-logic' import { gitExecFileAsync } from './git/runner' import { listWorktreesStrict, type GitWorktreeExecOptions } from './git/worktree' diff --git a/src/main/memory/collector.ts b/src/main/memory/collector.ts index d2a8783a510..c1fe58989ad 100644 --- a/src/main/memory/collector.ts +++ b/src/main/memory/collector.ts @@ -21,7 +21,7 @@ import { basename } from 'node:path' import { exec } from 'node:child_process' import { promisify } from 'node:util' import os from 'node:os' -import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id' +import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id' import { getProcessOutputFields, iterateProcessOutputLines diff --git a/src/main/memory/hydrate-local-pty-registry.ts b/src/main/memory/hydrate-local-pty-registry.ts index 6608fd2641c..3388eeebed9 100644 --- a/src/main/memory/hydrate-local-pty-registry.ts +++ b/src/main/memory/hydrate-local-pty-registry.ts @@ -16,7 +16,7 @@ import type { SessionInfo } from '../daemon/types' import { listRegisteredPtys, registerPty } from './pty-registry' import { listRepoWorktrees } from '../repo-worktrees' import { parsePtySessionId } from '../../shared/pty-session-id-format' -import { splitWorktreeId } from '../../shared/worktree-id' +import { splitWorktreeId } from '../../shared/worktree/id' import type { Store } from '../persistence' // Why: macOS dock reactivation reruns setup; avoid repeating Git I/O and daemon RPC. diff --git a/src/main/orca-profiles/profile-project-worktree-identity.ts b/src/main/orca-profiles/profile-project-worktree-identity.ts index 9bb4fa9fc27..58aa55da8b3 100644 --- a/src/main/orca-profiles/profile-project-worktree-identity.ts +++ b/src/main/orca-profiles/profile-project-worktree-identity.ts @@ -2,7 +2,7 @@ import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-p import { getRepoExecutionHostId } from '../../shared/execution-host' import type { Repo, WorkspaceKey } from '../../shared/types' import { parseWorkspaceKey, worktreeWorkspaceKey } from '../../shared/workspace-scope' -import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree-id' +import { WORKTREE_ID_SEPARATOR } from '../../shared/worktree/id' export function repoPhysicalKey( repo: Pick diff --git a/src/main/persistence.ts b/src/main/persistence.ts index d58b621c94b..d0699cea658 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -141,7 +141,7 @@ import { normalizeStatusBarUsageMode } from '../shared/status-bar-usage-mode' import { normalizeCustomWorktreeVisibilitySources, normalizeWorktreeVisibilitySourcePreferences -} from '../shared/worktree-visibility-sources' +} from '../shared/worktree/visibility-sources' import { isExistingPersistedProfile } from '../shared/project-order-manual-default-notice' import { resolveUsagePercentageDisplayChangeNoticeDismissed } from '../shared/usage-percentage-display-change-notice' import { normalizePRBotAuthorOverrides } from '../shared/pr-bot-author-overrides' @@ -173,7 +173,7 @@ import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR, getRepoIdFromWorktreeId, getWorktreePathBasenameFromId -} from '../shared/worktree-id' +} from '../shared/worktree/id' import { isPathInsideOrEqual, isWindowsAbsolutePathLike, @@ -220,7 +220,7 @@ import { } from '../shared/workspace-statuses' import { clampMarkdownTocPanelWidth } from '../shared/markdown-toc-panel-width' import { clampCombinedDiffFileTreeWidth } from '../shared/combined-diff-file-tree-width' -import { isLegacyRepoForExternalWorktreeVisibility } from '../shared/worktree-ownership' +import { isLegacyRepoForExternalWorktreeVisibility } from '../shared/worktree/ownership' import { sanitizeRepoIcon } from '../shared/repo-icon' import { normalizeRepoBadgeColor } from '../shared/repo-badge-color' import { diff --git a/src/main/ports/ssh-advertised-url-enrichment.ts b/src/main/ports/ssh-advertised-url-enrichment.ts index c7c264d3eb4..4a6b17f46d8 100644 --- a/src/main/ports/ssh-advertised-url-enrichment.ts +++ b/src/main/ports/ssh-advertised-url-enrichment.ts @@ -7,7 +7,7 @@ import type { DetectedPort, EnrichedDetectedPort, PortForwardEntry } from '../../shared/ssh-types' import type { Store } from '../persistence' -import { splitWorktreeId } from '../../shared/worktree-id' +import { splitWorktreeId } from '../../shared/worktree/id' import { advertisedUrlWatcher, type AdvertisedUrl, diff --git a/src/main/ports/workspace-port-ownership.ts b/src/main/ports/workspace-port-ownership.ts index 9f7df2465cb..f9020d76682 100644 --- a/src/main/ports/workspace-port-ownership.ts +++ b/src/main/ports/workspace-port-ownership.ts @@ -1,6 +1,6 @@ import path from 'node:path' import type { Store } from '../persistence' -import { splitWorktreeId, splitWorktreeIdForFilesystem } from '../../shared/worktree-id' +import { splitWorktreeId, splitWorktreeIdForFilesystem } from '../../shared/worktree/id' import { isFolderRepo } from '../../shared/repo-kind' import type { WorkspacePortKillRequest, diff --git a/src/main/providers/agent-foreground-context-paths.ts b/src/main/providers/agent-foreground-context-paths.ts index 7ff8a6d3e62..760b2933960 100644 --- a/src/main/providers/agent-foreground-context-paths.ts +++ b/src/main/providers/agent-foreground-context-paths.ts @@ -1,4 +1,4 @@ -import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id' +import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id' export function getAgentForegroundContextPaths(options: { cwd?: string diff --git a/src/main/providers/local-pty-provider.ts b/src/main/providers/local-pty-provider.ts index b4af3c907fb..4a852017f6a 100644 --- a/src/main/providers/local-pty-provider.ts +++ b/src/main/providers/local-pty-provider.ts @@ -12,7 +12,7 @@ import { resolveProcessCwd } from './process-cwd' import { existsSync } from 'node:fs' import * as pty from 'node-pty' import { getDefaultWslDistro, parseWslPath, isWslAvailableAsync } from '../wsl' -import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id' +import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id' import { isBracketedPasteSafeShell } from '../../shared/startup-command-submission' import { injectHistoryEnv, diff --git a/src/main/runtime/folder-workspace-pty-identity.test.ts b/src/main/runtime/folder-workspace-pty-identity.test.ts index d986fa54938..db6b1c1927a 100644 --- a/src/main/runtime/folder-workspace-pty-identity.test.ts +++ b/src/main/runtime/folder-workspace-pty-identity.test.ts @@ -3,7 +3,7 @@ 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 { FOLDER_WORKSPACE_INSTANCE_SEPARATOR } from '../../shared/worktree-id' +import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR } from '../../shared/worktree/id' import { OrcaRuntimeService } from './orca-runtime' // Folder projects back several workspaces with ONE directory; only the diff --git a/src/main/runtime/linear-save-issue.test.ts b/src/main/runtime/linear-save-issue.test.ts index eb8ed4093d2..02630a7452b 100644 --- a/src/main/runtime/linear-save-issue.test.ts +++ b/src/main/runtime/linear-save-issue.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import { LINEAR_WRITE_BODY_CAP } from '../../shared/linear-agent-access' +import { LINEAR_WRITE_BODY_CAP } from '../../shared/linear/agent-access' import * as linearTeams from '../linear/teams' import { OrcaRuntimeService } from './orca-runtime' diff --git a/src/main/runtime/missing-worktree-terminal-reconciliation.ts b/src/main/runtime/missing-worktree-terminal-reconciliation.ts index 6c4f7f762a4..25cd1f2129d 100644 --- a/src/main/runtime/missing-worktree-terminal-reconciliation.ts +++ b/src/main/runtime/missing-worktree-terminal-reconciliation.ts @@ -1,6 +1,6 @@ import type { IPtyProvider } from '../providers/types' import type { Repo } from '../../shared/types' -import { splitWorktreeId } from '../../shared/worktree-id' +import { splitWorktreeId } from '../../shared/worktree/id' import { mapWithConcurrency } from '../../shared/map-with-concurrency' import type { OrcaRuntimeService } from './orca-runtime' import { killAllProcessesForWorktree } from './worktree-teardown' diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 573884c48a0..9beb782cebc 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -123,7 +123,7 @@ import type { AgentSessionExecutionClaim, AgentSessionSurfaceBinding } from '../../shared/agent-session-host-authority' -import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR } from '../../shared/worktree-id' +import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR } from '../../shared/worktree/id' import { RpcDispatcher } from './rpc/dispatcher' import type { RpcRequest } from './rpc/core' import { TERMINAL_METHODS } from './rpc/methods/terminal' diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 15e1c4c7f4a..807eef35c05 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -12,7 +12,7 @@ import { normalizeTerminalTitle } from '../../shared/agent-detection' import { extractOscTitleScanTail } from '../../shared/osc-title-scan-tail' -import { planWorktreeSortOrderUpdates } from '../../shared/worktree-sort-order-update' +import { planWorktreeSortOrderUpdates } from '../../shared/worktree/sort-order-update' import { isArtifactSharingEnabled } from '../../shared/artifact-sharing-gate' import { sortDirEntries } from '../../shared/file-name-sort' import { isServerDriveListRequest, listWindowsDrives } from './windows-drive-listing' @@ -117,7 +117,7 @@ import { homedir } from 'node:os' import { isAbsolute, join, resolve } from 'node:path' import { mkdir, readFile, readdir, rm, stat } from 'node:fs/promises' import { resolveWorktreeCreateBase } from '../worktree-create-base' -import { resolveWorktreeAddBaseRef } from '../../shared/worktree-base-ref' +import { resolveWorktreeAddBaseRef } from '../../shared/worktree/base-ref' import { OrchestrationDb } from './orchestration/db' import { reconcileRequestedWorkerTerminalReleases } from './orchestration/worker-terminal-release-reconciliation' import { @@ -260,7 +260,7 @@ import type { CodexRateLimitAccountsState } from '../../shared/types' import type { TaskSourceContext } from '../../shared/task-source-context' -import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree-removal' +import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree/removal' import { LOCAL_EXECUTION_HOST_ID, getRepoExecutionHostId, @@ -314,7 +314,7 @@ import type { LinearTeamMembersResult, LinearTeamStatesResult, LinearStatusSetResult -} from '../../shared/linear-agent-access' +} from '../../shared/linear/agent-access' import { HEADLESS_RUNTIME_WINDOW_ID, type RuntimeDesktopWindowStatus, @@ -382,8 +382,8 @@ import { LINEAR_SEARCH_MAX_LIMIT, LINEAR_WRITE_BODY_CAP, clampLinearSearchLimit -} from '../../shared/linear-agent-access' -import { isLinearUuid } from '../../shared/linear-uuid' +} from '../../shared/linear/agent-access' +import { isLinearUuid } from '../../shared/linear/uuid' import type { FeatureInteractionId } from '../../shared/feature-interactions' import type { TerminalPaneSplitSource } from '../../shared/feature-education-telemetry' import { @@ -392,14 +392,14 @@ import { getRepoIdFromWorktreeId, splitWorktreeId, splitWorktreeIdForFilesystem -} from '../../shared/worktree-id' +} from '../../shared/worktree/id' import { getProjectIdForProviderIdentity, getProjectHostSetupForRepo, getProjectHostSetupWorktreeMeta } from '../../shared/project-host-setup-projection' import { parsePtySessionId } from '../../shared/pty-session-id-format' -import { clampLinearIssueListLimit } from '../../shared/linear-issue-read-limits' +import { clampLinearIssueListLimit } from '../../shared/linear/issue-read-limits' import { isFolderRepo } from '../../shared/repo-kind' import { DEFAULT_WORKSPACE_STATUS_ID } from '../../shared/workspace-statuses' import { @@ -490,13 +490,13 @@ import { buildKnownOrcaWorkspaceLayouts, isLegacyRepoForExternalWorktreeVisibility, toDetectedWorktree -} from '../../shared/worktree-ownership' +} from '../../shared/worktree/ownership' import { isAgentScratchRepoRootPath } from '../../shared/agent-scratch-worktrees' import { createWorktreeVisibilitySourceMatcher, normalizeCustomWorktreeVisibilitySources, type WorktreeVisibilitySourceMatcher -} from '../../shared/worktree-visibility-sources' +} from '../../shared/worktree/visibility-sources' import { BROWSER_HEADLESS_RUNTIME_CAPABILITY, BROWSER_CERTIFICATE_TRUST_RUNTIME_CAPABILITY, @@ -854,7 +854,7 @@ import type { UpdateIssueTypeBySlugArgs, UpdateProjectItemFieldArgs, UpdatePullRequestBySlugArgs -} from '../../shared/github-project-types' +} from '../../shared/github/project-types' import { getBaseRefDefault, getDefaultRemote, diff --git a/src/main/runtime/orchestration/cli-command.ts b/src/main/runtime/orchestration/cli-command.ts index c0f4883f1e4..482b66efa95 100644 --- a/src/main/runtime/orchestration/cli-command.ts +++ b/src/main/runtime/orchestration/cli-command.ts @@ -1,6 +1,6 @@ import type { ProjectExecutionRuntimeResolution } from '../../../shared/project-execution-runtime' import { isWslUncPath } from '../../../shared/wsl-paths' -import { splitWorktreeIdForFilesystem } from '../../../shared/worktree-id' +import { splitWorktreeIdForFilesystem } from '../../../shared/worktree/id' export type OrchestrationCliCommand = 'orca' | 'orca-ide' diff --git a/src/main/runtime/rpc/errors.ts b/src/main/runtime/rpc/errors.ts index db6396b8e0d..ad5e5f36695 100644 --- a/src/main/runtime/rpc/errors.ts +++ b/src/main/runtime/rpc/errors.ts @@ -5,7 +5,7 @@ import type { RpcEnvelopeMeta, RpcFailure, RpcSuccess } from './core' import { computerUseErrorRecoveryData } from '../../../shared/computer-use-error-recovery' import { COMPUTER_ERROR_CODES } from '../../../shared/runtime-types' -import { LINEAR_ERROR_CODES } from '../../../shared/linear-agent-access' +import { LINEAR_ERROR_CODES } from '../../../shared/linear/agent-access' import { AGENT_SESSION_RPC_ERROR_CODES } from '../../../shared/agent-session-host-authority' import { ARTIFACT_SHARING_DISABLED_CODE } from '../../../shared/artifact-sharing-gate' diff --git a/src/main/runtime/rpc/methods/client-ui-schemas.ts b/src/main/runtime/rpc/methods/client-ui-schemas.ts index ff125290da1..3d9e0e2e69e 100644 --- a/src/main/runtime/rpc/methods/client-ui-schemas.ts +++ b/src/main/runtime/rpc/methods/client-ui-schemas.ts @@ -16,7 +16,7 @@ import { normalizePRBotAuthorOverrides } from '../../../../shared/pr-bot-author- import { normalizeWorktreeCardProperties, WORKTREE_CARD_PROPERTIES -} from '../../../../shared/worktree-card-properties' +} from '../../../../shared/worktree/card-properties' import { isPluginPanelTabKey } from '../../../../shared/plugins/plugin-manifest' import type { TaskProvider } from '../../../../shared/types' import { ClientUiWorkspaceFilterFields } from './client-ui-workspace-filter-fields' diff --git a/src/main/runtime/rpc/methods/client-ui.test.ts b/src/main/runtime/rpc/methods/client-ui.test.ts index 18c747c242c..a8323413b23 100644 --- a/src/main/runtime/rpc/methods/client-ui.test.ts +++ b/src/main/runtime/rpc/methods/client-ui.test.ts @@ -7,7 +7,7 @@ import { MAX_QUICK_COMMAND_REPO_ID_LENGTH, MAX_QUICK_COMMAND_TERMINAL_TEXT_LENGTH } from '../../../../shared/terminal-quick-commands' -import { DEFAULT_WORKTREE_CARD_PROPERTIES } from '../../../../shared/worktree-card-properties' +import { DEFAULT_WORKTREE_CARD_PROPERTIES } from '../../../../shared/worktree/card-properties' import type { PersistedUIState } from '../../../../shared/types' import type { OrcaRuntimeService } from '../../orca-runtime' import type { RpcRequest } from '../core' diff --git a/src/main/runtime/rpc/methods/linear-agent-access.ts b/src/main/runtime/rpc/methods/linear-agent-access.ts index 13f9db57e38..50e7bfef3a2 100644 --- a/src/main/runtime/rpc/methods/linear-agent-access.ts +++ b/src/main/runtime/rpc/methods/linear-agent-access.ts @@ -2,7 +2,7 @@ import { z } from 'zod' import { defineMethod, type RpcMethod } from '../core' import { OptionalFiniteNumber, OptionalString, requiredString } from '../schemas' import { linearError } from '../../../linear/issue-context-errors' -import { isLinearUuid } from '../../../../shared/linear-uuid' +import { isLinearUuid } from '../../../../shared/linear/uuid' const LINEAR_DUE_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/ const LinearDueDate = z.string().refine((value) => LINEAR_DUE_DATE_PATTERN.test(value), { diff --git a/src/main/runtime/rpc/methods/linear-issue-attribute-filter-schema.ts b/src/main/runtime/rpc/methods/linear-issue-attribute-filter-schema.ts index 8d9dde7ddc8..7b7176f48b8 100644 --- a/src/main/runtime/rpc/methods/linear-issue-attribute-filter-schema.ts +++ b/src/main/runtime/rpc/methods/linear-issue-attribute-filter-schema.ts @@ -4,7 +4,7 @@ import { LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_LABEL_IDS, LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_PRIORITIES, LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_STATE_IDS -} from '../../../../shared/linear-issue-attribute-filter' +} from '../../../../shared/linear/issue-attribute-filter' // Why: keep ListIssues param validation co-located with shared limits without // pushing linear.ts past the max-lines ratchet. diff --git a/src/main/runtime/rpc/methods/repo-update-schema.ts b/src/main/runtime/rpc/methods/repo-update-schema.ts index f178bfa8d11..4fff864abcd 100644 --- a/src/main/runtime/rpc/methods/repo-update-schema.ts +++ b/src/main/runtime/rpc/methods/repo-update-schema.ts @@ -6,7 +6,7 @@ import { normalizeRepoSourceControlAiOverrides } from '../../../../shared/source import { normalizeCustomWorktreeVisibilitySources, normalizeWorktreeVisibilitySourcePreferences -} from '../../../../shared/worktree-visibility-sources' +} from '../../../../shared/worktree/visibility-sources' export const RepoSourceControlAiOverrides = z .unknown() diff --git a/src/main/runtime/runtime-folder-workspace.test.ts b/src/main/runtime/runtime-folder-workspace.test.ts index f00da160330..06d97809c90 100644 --- a/src/main/runtime/runtime-folder-workspace.test.ts +++ b/src/main/runtime/runtime-folder-workspace.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { Repo, WorktreeMeta } from '../../shared/types' -import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR } from '../../shared/worktree-id' +import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR } from '../../shared/worktree/id' import { getRuntimeFolderWorkspaceInstanceId, getRuntimeFolderWorkspaceRootId, diff --git a/src/main/runtime/runtime-folder-workspace.ts b/src/main/runtime/runtime-folder-workspace.ts index fcbbacbd51d..63f548a1037 100644 --- a/src/main/runtime/runtime-folder-workspace.ts +++ b/src/main/runtime/runtime-folder-workspace.ts @@ -1,6 +1,6 @@ import { DEFAULT_WORKSPACE_STATUS_ID } from '../../shared/workspace-statuses' import type { Repo, Worktree, WorktreeMeta } from '../../shared/types' -import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR } from '../../shared/worktree-id' +import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR } from '../../shared/worktree/id' import { normalizeWorkspaceCreatorProvenance } from '../../shared/workspace-creator-provenance' export function getRuntimeFolderWorkspaceRootId(repo: Repo): string { diff --git a/src/main/runtime/unstopped-pty-verification.ts b/src/main/runtime/unstopped-pty-verification.ts index 068334eb98d..2dc9aac5960 100644 --- a/src/main/runtime/unstopped-pty-verification.ts +++ b/src/main/runtime/unstopped-pty-verification.ts @@ -3,7 +3,7 @@ import { UNSTOPPED_PTY_DETAIL_SEPARATOR, UNSTOPPED_PTY_LIVE_DETAIL_PREFIX, UNSTOPPED_PTY_REMOVAL_PREFIX -} from '../../shared/worktree-removal' +} from '../../shared/worktree/removal' import { settleBeforeDeadline } from './settle-before-deadline' // Floor for the verification window when the sweep ran on a very short budget. diff --git a/src/main/runtime/workspace-session-terminal-membership-authority.ts b/src/main/runtime/workspace-session-terminal-membership-authority.ts index 9dace4c8cb1..556d488edc2 100644 --- a/src/main/runtime/workspace-session-terminal-membership-authority.ts +++ b/src/main/runtime/workspace-session-terminal-membership-authority.ts @@ -5,7 +5,7 @@ import type { TerminalPaneLayoutNode, WorkspaceSessionState } from '../../shared/types' -import { getRepoIdFromWorktreeId } from '../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../shared/worktree/id' import { pruneTabGroupLayoutAfterRetirement } from './mobile-session-terminal-retirement' function collectLeafIds(node: TerminalPaneLayoutNode | null, ids: Set): void { diff --git a/src/main/runtime/worktree-teardown-unstopped-pty.test.ts b/src/main/runtime/worktree-teardown-unstopped-pty.test.ts index ca09e01b2d8..31ba390c3ec 100644 --- a/src/main/runtime/worktree-teardown-unstopped-pty.test.ts +++ b/src/main/runtime/worktree-teardown-unstopped-pty.test.ts @@ -13,7 +13,7 @@ import { ABANDONED_SWEEP_GRACE_MS } from './forced-sweep-settlement' import { classifyWorktreeForceDeleteReason, isProvenLivePtyRemovalError -} from '../../shared/worktree-removal' +} from '../../shared/worktree/removal' import type { IPtyProvider, PtyProcessInfo } from '../providers/types' // Why: these tests advance fake timers *before* awaiting the teardown, so a diff --git a/src/main/runtime/worktree-teardown.ts b/src/main/runtime/worktree-teardown.ts index 993a263d348..ed0df69e8f7 100644 --- a/src/main/runtime/worktree-teardown.ts +++ b/src/main/runtime/worktree-teardown.ts @@ -2,13 +2,13 @@ import type { IPtyProvider } from '../providers/types' import type { OrcaRuntimeService } from './orca-runtime' import { listRegisteredPtys } from '../memory/pty-registry' import { isPathInsideOrEqual } from '../../shared/cross-platform-path' -import { splitWorktreeId, splitWorktreeIdForFilesystem } from '../../shared/worktree-id' +import { splitWorktreeId, splitWorktreeIdForFilesystem } from '../../shared/worktree/id' import { mapWithConcurrency } from '../../shared/map-with-concurrency' import { isUnstoppedPtyRemovalError, WORKTREE_TEARDOWN_FORCE_HINT, WORKTREE_TEARDOWN_TIMEOUT_PREFIX -} from '../../shared/worktree-removal' +} from '../../shared/worktree/removal' import { settleBeforeDeadline } from './settle-before-deadline' import { createWorktreeSweepTracker, settleSweepsForForcedRemoval } from './forced-sweep-settlement' import { diff --git a/src/main/source-control/hosted-review-creation.ts b/src/main/source-control/hosted-review-creation.ts index 0fa50ef3a32..2149c1ab000 100644 --- a/src/main/source-control/hosted-review-creation.ts +++ b/src/main/source-control/hosted-review-creation.ts @@ -21,7 +21,7 @@ import { isGiteaReviewCreationAuthenticated } from '../gitea/pull-request-creati import { isBitbucketReviewCreationAuthenticated } from '../bitbucket/pull-request-creation' import { getEnterpriseGitHubRepoSlug } from '../github/github-enterprise-repository' import { getRepoSlug } from '../github/client' -import { isDefaultGitHubHost } from '../../shared/github-repository-identity-key' +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' diff --git a/src/main/ssh/ssh-remote-linear-activity-output.test.ts b/src/main/ssh/ssh-remote-linear-activity-output.test.ts index ffbd63a71bc..3cfb70c4686 100644 --- a/src/main/ssh/ssh-remote-linear-activity-output.test.ts +++ b/src/main/ssh/ssh-remote-linear-activity-output.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { LinearIssueContextResult } from '../../shared/linear-agent-access' +import type { LinearIssueContextResult } from '../../shared/linear/agent-access' import { formatRemoteLinearCli } from './ssh-remote-linear-output' describe('SSH Linear activity output', () => { diff --git a/src/main/ssh/ssh-remote-linear-list-issues.ts b/src/main/ssh/ssh-remote-linear-list-issues.ts index eb9213ada7c..98f10e8f36c 100644 --- a/src/main/ssh/ssh-remote-linear-list-issues.ts +++ b/src/main/ssh/ssh-remote-linear-list-issues.ts @@ -1,4 +1,4 @@ -import type { LinearMcpIssueListRequest } from '../../shared/linear-agent-access' +import type { LinearMcpIssueListRequest } from '../../shared/linear/agent-access' import type { RpcDispatcher } from '../runtime/rpc/dispatcher' import type { RpcResponse } from '../runtime/rpc/core' import { RemoteCliArgumentError, type ParsedRemoteCli } from './ssh-remote-cli-argument-error' diff --git a/src/main/ssh/ssh-remote-linear-output.ts b/src/main/ssh/ssh-remote-linear-output.ts index 4835791733c..0025248fb37 100644 --- a/src/main/ssh/ssh-remote-linear-output.ts +++ b/src/main/ssh/ssh-remote-linear-output.ts @@ -16,11 +16,11 @@ import type { LinearAttachResult, LinearCreateResult, LinearSaveIssueResult -} from '../../shared/linear-agent-access' +} from '../../shared/linear/agent-access' import { formatLinearProjectListRows, linearProjectListWarningLines -} from '../../shared/linear-project-list-format' +} from '../../shared/linear/project-list-format' import { isLinearAttachResult, isLinearCommentAddResult, diff --git a/src/main/ssh/ssh-remote-linear-read-cli.ts b/src/main/ssh/ssh-remote-linear-read-cli.ts index 123a279f0b6..d9ecfb27365 100644 --- a/src/main/ssh/ssh-remote-linear-read-cli.ts +++ b/src/main/ssh/ssh-remote-linear-read-cli.ts @@ -3,7 +3,7 @@ import { clampLinearIssueDepth, clampLinearSearchLimit, type LinearIssueInclude -} from '../../shared/linear-agent-access' +} from '../../shared/linear/agent-access' import type { RpcDispatcher } from '../runtime/rpc/dispatcher' import type { RpcResponse } from '../runtime/rpc/core' import { RemoteCliArgumentError, type ParsedRemoteCli } from './ssh-remote-cli-argument-error' diff --git a/src/main/ssh/ssh-remote-linear-relation-write.ts b/src/main/ssh/ssh-remote-linear-relation-write.ts index f89911958ff..e0ca568f8ed 100644 --- a/src/main/ssh/ssh-remote-linear-relation-write.ts +++ b/src/main/ssh/ssh-remote-linear-relation-write.ts @@ -1,4 +1,4 @@ -import type { LinearIssueRelationship } from '../../shared/linear-agent-access' +import type { LinearIssueRelationship } from '../../shared/linear/agent-access' import type { RpcResponse } from '../runtime/rpc/core' import type { RpcDispatcher } from '../runtime/rpc/dispatcher' import { diff --git a/src/main/ssh/ssh-remote-linear-result-guards.ts b/src/main/ssh/ssh-remote-linear-result-guards.ts index fd9bb2573c4..af05a6ed4da 100644 --- a/src/main/ssh/ssh-remote-linear-result-guards.ts +++ b/src/main/ssh/ssh-remote-linear-result-guards.ts @@ -15,7 +15,7 @@ import type { LinearTeamListResult, LinearTeamMembersResult, LinearTeamStatesResult -} from '../../shared/linear-agent-access' +} from '../../shared/linear/agent-access' export function isLinearIssueContextResult(result: unknown): result is LinearIssueContextResult { return ( diff --git a/src/main/ssh/ssh-remote-linear-write-support.ts b/src/main/ssh/ssh-remote-linear-write-support.ts index a341f0a93e8..b4967c90536 100644 --- a/src/main/ssh/ssh-remote-linear-write-support.ts +++ b/src/main/ssh/ssh-remote-linear-write-support.ts @@ -1,6 +1,6 @@ import type { RpcResponse } from '../runtime/rpc/core' import type { RpcDispatcher } from '../runtime/rpc/dispatcher' -import { isLinearUuid } from '../../shared/linear-uuid' +import { isLinearUuid } from '../../shared/linear/uuid' type ParsedRemoteCli = { commandPath: string[] diff --git a/src/main/usage-worktree-metadata.ts b/src/main/usage-worktree-metadata.ts index 53043124c94..8c4f3a94ba6 100644 --- a/src/main/usage-worktree-metadata.ts +++ b/src/main/usage-worktree-metadata.ts @@ -1,6 +1,6 @@ import { basename } from 'node:path' import type { Repo } from '../shared/types' -import { splitWorktreeId, splitWorktreeIdForFilesystem } from '../shared/worktree-id' +import { splitWorktreeId, splitWorktreeIdForFilesystem } from '../shared/worktree/id' import { isFolderRepo } from '../shared/repo-kind' import type { Store } from './persistence' diff --git a/src/main/worktree-create-base-prefetch.ts b/src/main/worktree-create-base-prefetch.ts index 8653c6866eb..fc43980202e 100644 --- a/src/main/worktree-create-base-prefetch.ts +++ b/src/main/worktree-create-base-prefetch.ts @@ -6,7 +6,7 @@ import { getBaseRefDefault } from './git/repo' import { getSshGitProvider } from './providers/ssh-git-dispatch' import { prefetchRemoteWorktreeCreateBase } from './ipc/worktree-remote' import { resolveWorktreeCreateBase } from './worktree-create-base' -import { resolveWorktreeAddBaseRef } from '../shared/worktree-base-ref' +import { resolveWorktreeAddBaseRef } from '../shared/worktree/base-ref' type RemoteTrackingBaseForPrefetch = { remote: string diff --git a/src/preload/api-types.ts b/src/preload/api-types.ts index 3b21663bccb..84ceb217cf7 100644 --- a/src/preload/api-types.ts +++ b/src/preload/api-types.ts @@ -108,7 +108,7 @@ import type { } from '../shared/orca-profiles' import type { TerminalPaneSplitSource } from '../shared/feature-education-telemetry' import type { TaskSourceContext } from '../shared/task-source-context' -import type { LinearIssueAttributeFilter } from '../shared/linear-issue-attribute-filter' +import type { LinearIssueAttributeFilter } from '../shared/linear/issue-attribute-filter' import type { ProjectExecutionRuntimeResolution } from '../shared/project-execution-runtime' import type { StartupCommandDelivery } from '../shared/codex-startup-delivery' import type { @@ -347,7 +347,7 @@ import type { UpdateIssueTypeBySlugArgs, UpdatePullRequestBySlugArgs, UpdateProjectItemFieldArgs -} from '../shared/github-project-types' +} from '../shared/github/project-types' import type { RichMarkdownContextMenuCommandPayload, RichMarkdownContextMenuTableTarget @@ -475,7 +475,7 @@ import type { WorkspacePortScanRequest, WorkspacePortScanResult } from '../shared/workspace-ports' -import type { GhAuthDiagnostic } from '../shared/github-auth-types' +import type { GhAuthDiagnostic } from '../shared/github/auth-types' import type { CodexUsageBreakdownKind, CodexUsageSnapshot } from '../shared/codex-usage-types' import type { OpenCodeUsageBreakdownKind, diff --git a/src/preload/index.ts b/src/preload/index.ts index 518feac7d82..24e692088e0 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -160,7 +160,7 @@ import type { import type { WorkspaceSpaceScanProgress } from '../shared/workspace-space-types' import type { WorkspaceCleanupScanProgress } from '../shared/workspace-cleanup' import type { WorkspacePortAdvertisedUrlChangedEvent } from '../shared/workspace-ports' -import type { GhAuthDiagnostic } from '../shared/github-auth-types' +import type { GhAuthDiagnostic } from '../shared/github/auth-types' import type { TaskSourceContext } from '../shared/task-source-context' import type { AddIssueCommentBySlugArgs, @@ -189,7 +189,7 @@ import type { UpdateIssueTypeBySlugArgs, UpdatePullRequestBySlugArgs, UpdateProjectItemFieldArgs -} from '../shared/github-project-types' +} from '../shared/github/project-types' import { richMarkdownContextMenuCommandChannel, richMarkdownContextMenuTargetChannel, diff --git a/src/relay/git-handler-worktree-ops.ts b/src/relay/git-handler-worktree-ops.ts index 487fabd7688..0cdb0761b47 100644 --- a/src/relay/git-handler-worktree-ops.ts +++ b/src/relay/git-handler-worktree-ops.ts @@ -1,5 +1,5 @@ import * as path from 'node:path' -import { resolveWorktreeAddBaseRef } from '../shared/worktree-base-ref' +import { resolveWorktreeAddBaseRef } from '../shared/worktree/base-ref' import type { GitExec } from './git-handler-ops' export { removeWorktreeOp } from './git-handler-worktree-remove' export { readRelayWorktreeList } from './git-handler-worktree-list' diff --git a/src/relay/git-handler-worktree-remove.ts b/src/relay/git-handler-worktree-remove.ts index 46ebaf8a355..80bf08c25fe 100644 --- a/src/relay/git-handler-worktree-remove.ts +++ b/src/relay/git-handler-worktree-remove.ts @@ -1,7 +1,7 @@ import * as path from 'node:path' import type { RemoveWorktreeResult } from '../shared/types' -import { assertWorktreeUnlockedForRemoval } from '../shared/worktree-removal' -import { isSubmoduleWorktreeRemovalRefusal } from '../shared/worktree-submodule-removal' +import { assertWorktreeUnlockedForRemoval } from '../shared/worktree/removal' +import { isSubmoduleWorktreeRemovalRefusal } from '../shared/worktree/submodule-removal' import { deleteAlreadyMergedRelayBranchAfterSafeDeleteFailure } from './git-handler-branch-cleanup' import type { GitExec } from './git-handler-ops' import type { GitCapabilityCache } from '../shared/git-capability-cache' diff --git a/src/relay/pty-handler.ts b/src/relay/pty-handler.ts index 3f8bd593e08..faa12d5093b 100644 --- a/src/relay/pty-handler.ts +++ b/src/relay/pty-handler.ts @@ -25,7 +25,7 @@ import { isPathInsideOrEqual, normalizeRuntimePathForComparison } from '../shared/cross-platform-path' -import { splitWorktreeId } from '../shared/worktree-id' +import { splitWorktreeId } from '../shared/worktree/id' import { PhysicalExitTracker } from '../shared/physical-exit-tracker' import { SHELL_READY_MARKER_PREFIX } from '../main/shell-ready-marker-scanner' import { diff --git a/src/renderer/src/components/GitHubItemDialog.tsx b/src/renderer/src/components/GitHubItemDialog.tsx index 5f73fb31bea..bba079e9f6d 100644 --- a/src/renderer/src/components/GitHubItemDialog.tsx +++ b/src/renderer/src/components/GitHubItemDialog.tsx @@ -177,8 +177,8 @@ import { presentGitHubPRMergeState } from '@/components/github-pr-merge-state' import { GITHUB_PR_MERGE_METHOD_LABELS, resolveGitHubPRMergeMethods -} from '../../../shared/github-pr-merge-methods' -import { githubRepoIdentityKey } from '../../../shared/github-repository-identity-key' +} from '../../../shared/github/pull-request-merge-methods' +import { githubRepoIdentityKey } from '../../../shared/github/repository-identity-key' import { findGithubIssueWorkspaceAttachment, getGithubWorkItemWorkspaceAttachmentLabel diff --git a/src/renderer/src/components/PullRequestPage.tsx b/src/renderer/src/components/PullRequestPage.tsx index aedc7bdd5e0..f47b452b573 100644 --- a/src/renderer/src/components/PullRequestPage.tsx +++ b/src/renderer/src/components/PullRequestPage.tsx @@ -170,8 +170,8 @@ import { presentGitHubPRMergeState } from '@/components/github-pr-merge-state' import { GITHUB_PR_MERGE_METHOD_LABELS, resolveGitHubPRMergeMethods -} from '../../../shared/github-pr-merge-methods' -import { githubRepoIdentityKey } from '../../../shared/github-repository-identity-key' +} from '../../../shared/github/pull-request-merge-methods' +import { githubRepoIdentityKey } from '../../../shared/github/repository-identity-key' import { findGithubPrWorkspaceAttachment, getGithubPrWorkspaceAttachmentLabel diff --git a/src/renderer/src/components/TaskPage.tsx b/src/renderer/src/components/TaskPage.tsx index 29bc0bc4587..d7cfc3e9e5e 100644 --- a/src/renderer/src/components/TaskPage.tsx +++ b/src/renderer/src/components/TaskPage.tsx @@ -127,11 +127,11 @@ import { getLinearStatePillStyle } from '@/components/linear-state-pill-style' import { parseTaskQuery, stripRepoQualifiers, withQualifier } from '../../../shared/task-query' -import { githubProjectHost } from '../../../shared/github-project-identity' +import { githubProjectHost } from '../../../shared/github/project-identity' import { buildLinearTeamUrl, getLinearOrganizationUrlKeyFromIssueUrl -} from '../../../shared/linear-links' +} from '../../../shared/linear/links' import PRFilterDropdowns, { type PRFilterChange } from '@/components/github/PRFilterDropdowns' import { GitHubMarkdownComposer } from '@/components/github/GitHubMarkdownComposer' import { GitHubUserAvatar } from '@/components/github/github-user-avatar' @@ -287,7 +287,7 @@ import { import { linearIssueAttributeFilterSignature, type LinearIssueAttributeFilter -} from '../../../shared/linear-issue-attribute-filter' +} from '../../../shared/linear/issue-attribute-filter' import { DEFAULT_LINEAR_GROUP_BY, DEFAULT_LINEAR_ORDER_BY, @@ -296,7 +296,7 @@ import { selectLinearWorkspaceIssueFilter, serializeLinearIssueViewResumeState, setLinearWorkspaceIssueFilter -} from '../../../shared/linear-issue-view-resume-state' +} from '../../../shared/linear/issue-view-resume-state' import { loadLinearIssueView, saveLinearIssueView } from './linear-issue-view-storage' import { isNewIssueDraftContentful, @@ -349,7 +349,7 @@ import { presentGitHubPRMergeState } from '@/components/github-pr-merge-state' import { GITHUB_PR_MERGE_METHOD_LABELS, resolveGitHubPRMergeMethods -} from '../../../shared/github-pr-merge-methods' +} from '../../../shared/github/pull-request-merge-methods' import type { GitHubOwnerRepo, GitHubAssignableUser, @@ -382,7 +382,7 @@ import type { GitLabProjectRef } from '../../../shared/gitlab-types' import { LINEAR_ISSUE_LIST_MAX, clampLinearIssueListLimit -} from '../../../shared/linear-issue-read-limits' +} from '../../../shared/linear/issue-read-limits' import { shouldSuppressEnterSubmit } from '@/lib/new-workspace-enter-guard' import { useContextualTour } from '@/components/contextual-tours/use-contextual-tour' import { getScreenSubmitShortcutLabel, isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' diff --git a/src/renderer/src/components/automations/AutomationsPage.tsx b/src/renderer/src/components/automations/AutomationsPage.tsx index b1fe9c4988b..a17d79e4197 100644 --- a/src/renderer/src/components/automations/AutomationsPage.tsx +++ b/src/renderer/src/components/automations/AutomationsPage.tsx @@ -29,7 +29,7 @@ import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-ov import type { PreflightStatus } from '../../../../preload/api-types' import type { TaskSourceContext } from '../../../../shared/task-source-context' import type { OrcaHooks, Repo } from '../../../../shared/types' -import { getWorktreePathBasenameFromId } from '../../../../shared/worktree-id' +import { getWorktreePathBasenameFromId } from '../../../../shared/worktree/id' import { buildAutomationRrule, isValidAutomationCronSchedule, diff --git a/src/renderer/src/components/github-pr-merge-state.ts b/src/renderer/src/components/github-pr-merge-state.ts index ca6c47e71b4..d67a3b8a3ef 100644 --- a/src/renderer/src/components/github-pr-merge-state.ts +++ b/src/renderer/src/components/github-pr-merge-state.ts @@ -5,7 +5,7 @@ import type { PRReviewDecision, PRState } from '../../../shared/types' -import { canEnableGitHubPRAutoMerge } from '../../../shared/github-pr-auto-merge-availability' +import { canEnableGitHubPRAutoMerge } from '../../../shared/github/pull-request-auto-merge-availability' import { translate } from '@/i18n/i18n' export type GitHubPRMergeStateInput = { diff --git a/src/renderer/src/components/github-project/GhAuthErrorHelp.test.ts b/src/renderer/src/components/github-project/GhAuthErrorHelp.test.ts index 248783de9e0..5e352e13884 100644 --- a/src/renderer/src/components/github-project/GhAuthErrorHelp.test.ts +++ b/src/renderer/src/components/github-project/GhAuthErrorHelp.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import type { GhAuthDiagnostic } from '../../../../shared/github-auth-types' +import type { GhAuthDiagnostic } from '../../../../shared/github/auth-types' vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback diff --git a/src/renderer/src/components/github-project/GhAuthErrorHelp.tsx b/src/renderer/src/components/github-project/GhAuthErrorHelp.tsx index 7bbeff291d7..6cec15c57b5 100644 --- a/src/renderer/src/components/github-project/GhAuthErrorHelp.tsx +++ b/src/renderer/src/components/github-project/GhAuthErrorHelp.tsx @@ -12,8 +12,8 @@ import { useEffect, useState } from 'react' import { Copy, ExternalLink, RotateCw } from 'lucide-react' import { toast } from 'sonner' import { Button } from '@/components/ui/button' -import type { GitHubProjectViewError } from '@/../../shared/github-project-types' -import type { GhAuthDiagnostic } from '@/../../shared/github-auth-types' +import type { GitHubProjectViewError } from '../../../../shared/github/project-types' +import type { GhAuthDiagnostic } from '../../../../shared/github/auth-types' import { translate } from '@/i18n/i18n' type AuthErrorKind = 'auth_required' | 'scope_missing' diff --git a/src/renderer/src/components/github-project/ProjectCell.tsx b/src/renderer/src/components/github-project/ProjectCell.tsx index 305fccc3873..877f426311a 100644 --- a/src/renderer/src/components/github-project/ProjectCell.tsx +++ b/src/renderer/src/components/github-project/ProjectCell.tsx @@ -24,7 +24,7 @@ import type { GitHubProjectRow, GitHubProjectUser, ListIssueTypesBySlugResult -} from '../../../../shared/github-project-types' +} from '../../../../shared/github/project-types' import type { GlobalSettings } from '../../../../shared/types' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/github-project/ProjectGroupHeader.tsx b/src/renderer/src/components/github-project/ProjectGroupHeader.tsx index c0856cb59a3..add12fb4e99 100644 --- a/src/renderer/src/components/github-project/ProjectGroupHeader.tsx +++ b/src/renderer/src/components/github-project/ProjectGroupHeader.tsx @@ -1,7 +1,7 @@ import React from 'react' import { ChevronDown, ChevronRight } from 'lucide-react' import { cn } from '@/lib/utils' -import { isIterationCurrent, type ProjectGroup } from '../../../../shared/github-project-group-sort' +import { isIterationCurrent, type ProjectGroup } from '../../../../shared/github/project-group-sort' import { translate } from '@/i18n/i18n' type Props = { diff --git a/src/renderer/src/components/github-project/ProjectPicker.tsx b/src/renderer/src/components/github-project/ProjectPicker.tsx index 2abf6273e59..cd142d5b04c 100644 --- a/src/renderer/src/components/github-project/ProjectPicker.tsx +++ b/src/renderer/src/components/github-project/ProjectPicker.tsx @@ -23,12 +23,12 @@ import type { ListAccessibleProjectsResult, ListProjectViewsResult, ResolveProjectRefResult -} from '../../../../shared/github-project-types' +} from '../../../../shared/github/project-types' import { GITHUB_PROJECT_REF_INPUT_TOO_LARGE_ERROR, hasBoundedGitHubProjectRefInputText, isGitHubProjectRefInputTooLarge -} from '../../../../shared/github-project-ref-input' +} from '../../../../shared/github/project-ref-input' import { filterGitHubProjectPickerProjects } from './github-project-picker-filter' import { getProjectPickerBrowseCacheEntry, @@ -39,7 +39,7 @@ import { translate } from '@/i18n/i18n' import { githubProjectHost, githubProjectIdentityKey -} from '../../../../shared/github-project-identity' +} from '../../../../shared/github/project-identity' export type ResolvedProjectSelection = { owner: string diff --git a/src/renderer/src/components/github-project/ProjectRow.tsx b/src/renderer/src/components/github-project/ProjectRow.tsx index e59c58903eb..d57a807702e 100644 --- a/src/renderer/src/components/github-project/ProjectRow.tsx +++ b/src/renderer/src/components/github-project/ProjectRow.tsx @@ -11,7 +11,7 @@ import type { GitHubProjectField, GitHubProjectFieldMutationValue, GitHubProjectRow as GitHubProjectRowType -} from '../../../../shared/github-project-types' +} from '../../../../shared/github/project-types' import type { GlobalSettings } from '../../../../shared/types' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/github-project/ProjectViewList.tsx b/src/renderer/src/components/github-project/ProjectViewList.tsx index fbc869b1107..8bd6963b4c7 100644 --- a/src/renderer/src/components/github-project/ProjectViewList.tsx +++ b/src/renderer/src/components/github-project/ProjectViewList.tsx @@ -5,7 +5,7 @@ import { cn } from '@/lib/utils' import ColumnResizeHandle from './ColumnResizeHandle' import ProjectGroupHeader from './ProjectGroupHeader' import ProjectRow from './ProjectRow' -import { groupRows, sortRows } from '../../../../shared/github-project-group-sort' +import { groupRows, sortRows } from '../../../../shared/github/project-group-sort' import { getAvailableColumns, loadHiddenColumns, saveHiddenColumns } from './columns' import { ACTION_COLUMN_WIDTH, @@ -21,7 +21,7 @@ import type { GitHubProjectRow, GitHubProjectSortDirection, GitHubProjectTable -} from '../../../../shared/github-project-types' +} from '../../../../shared/github/project-types' import type { GlobalSettings } from '../../../../shared/types' import { translate } from '@/i18n/i18n' diff --git a/src/renderer/src/components/github-project/ProjectViewWrapper.tsx b/src/renderer/src/components/github-project/ProjectViewWrapper.tsx index 59d02e24f31..7cac639ba8a 100644 --- a/src/renderer/src/components/github-project/ProjectViewWrapper.tsx +++ b/src/renderer/src/components/github-project/ProjectViewWrapper.tsx @@ -40,7 +40,7 @@ import type { GitHubProjectViewError, GitHubProjectViewSummary, ListProjectViewsResult -} from '../../../../shared/github-project-types' +} from '../../../../shared/github/project-types' import type { GitHubWorkItem } from '../../../../shared/types' import ProjectPicker, { type ResolvedProjectSelection } from './ProjectPicker' import ProjectViewList from './ProjectViewList' @@ -64,7 +64,7 @@ import { buildTaskSourceContextFromRepo } from '../../../../shared/task-source-c import { githubProjectHost, githubProjectIdentityKey -} from '../../../../shared/github-project-identity' +} from '../../../../shared/github/project-identity' type Props = { selectedRepoIds: ReadonlySet diff --git a/src/renderer/src/components/github-project/column-widths.ts b/src/renderer/src/components/github-project/column-widths.ts index 68106fb451b..5967e4d2b5e 100644 --- a/src/renderer/src/components/github-project/column-widths.ts +++ b/src/renderer/src/components/github-project/column-widths.ts @@ -7,7 +7,7 @@ // the grid always fit its container exactly. Resize redistributes // weights between a column pair so the total stays constant and the // table never grows beyond its container. -import type { GitHubProjectField } from '../../../../shared/github-project-types' +import type { GitHubProjectField } from '../../../../shared/github/project-types' const STORAGE_KEY = 'orca.githubProject.columnWidths' diff --git a/src/renderer/src/components/github-project/columns.ts b/src/renderer/src/components/github-project/columns.ts index 9e3684b5bfe..b2cd21b5276 100644 --- a/src/renderer/src/components/github-project/columns.ts +++ b/src/renderer/src/components/github-project/columns.ts @@ -3,7 +3,7 @@ // local visibility filter on top. Persisted in localStorage (not settings) // because it's purely cosmetic per device and would otherwise bloat the // debounced settings write on every checkbox toggle. -import type { GitHubProjectField, GitHubProjectView } from '../../../../shared/github-project-types' +import type { GitHubProjectField, GitHubProjectView } from '../../../../shared/github/project-types' export const TYPE_FIELD_ID = '__type__' export const TYPE_FIELD_DATA_TYPE = '__TYPE__' diff --git a/src/renderer/src/components/github-project/github-project-picker-filter.test.ts b/src/renderer/src/components/github-project/github-project-picker-filter.test.ts index 2f04b73f71f..b3b3aee335c 100644 --- a/src/renderer/src/components/github-project/github-project-picker-filter.test.ts +++ b/src/renderer/src/components/github-project/github-project-picker-filter.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { GitHubProjectSummary } from '../../../../shared/github-project-types' +import type { GitHubProjectSummary } from '../../../../shared/github/project-types' import { GITHUB_PROJECT_PICKER_QUERY_MAX_BYTES, filterGitHubProjectPickerProjects, diff --git a/src/renderer/src/components/github-project/github-project-picker-filter.ts b/src/renderer/src/components/github-project/github-project-picker-filter.ts index 0f8065db291..fcee171db82 100644 --- a/src/renderer/src/components/github-project/github-project-picker-filter.ts +++ b/src/renderer/src/components/github-project/github-project-picker-filter.ts @@ -2,8 +2,8 @@ import { isClipboardTextByteLengthOverLimit } from '../../../../shared/clipboard import type { GitHubProjectSettings, GitHubProjectSummary -} from '../../../../shared/github-project-types' -import { githubProjectIdentityKey } from '../../../../shared/github-project-identity' +} from '../../../../shared/github/project-types' +import { githubProjectIdentityKey } from '../../../../shared/github/project-identity' export const GITHUB_PROJECT_PICKER_QUERY_MAX_BYTES = 2 * 1024 diff --git a/src/renderer/src/components/github-project/group-sort.test.ts b/src/renderer/src/components/github-project/group-sort.test.ts index 4d36f0125da..cd5e77738bc 100644 --- a/src/renderer/src/components/github-project/group-sort.test.ts +++ b/src/renderer/src/components/github-project/group-sort.test.ts @@ -8,8 +8,8 @@ import type { GitHubProjectSort, GitHubProjectTable, GitHubProjectView -} from '../../../../shared/github-project-types' -import { sortRows, groupRows } from '../../../../shared/github-project-group-sort' +} from '../../../../shared/github/project-types' +import { sortRows, groupRows } from '../../../../shared/github/project-group-sort' const singleSelectField: GitHubProjectField = { kind: 'single-select', diff --git a/src/renderer/src/components/github-project/project-picker-browse-cache.test.ts b/src/renderer/src/components/github-project/project-picker-browse-cache.test.ts index 8f0052c4fbe..cb0e5b2f7a6 100644 --- a/src/renderer/src/components/github-project/project-picker-browse-cache.test.ts +++ b/src/renderer/src/components/github-project/project-picker-browse-cache.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest' -import type { GitHubProjectSummary } from '../../../../shared/github-project-types' +import type { GitHubProjectSummary } from '../../../../shared/github/project-types' import { PROJECT_PICKER_BROWSE_CACHE_MAX_ENTRIES, PROJECT_PICKER_BROWSE_CACHE_TTL_MS, diff --git a/src/renderer/src/components/github-project/project-picker-browse-cache.ts b/src/renderer/src/components/github-project/project-picker-browse-cache.ts index bd1726aec3a..1854d1f4ca3 100644 --- a/src/renderer/src/components/github-project/project-picker-browse-cache.ts +++ b/src/renderer/src/components/github-project/project-picker-browse-cache.ts @@ -1,4 +1,4 @@ -import type { GitHubProjectSummary } from '../../../../shared/github-project-types' +import type { GitHubProjectSummary } from '../../../../shared/github/project-types' export const PROJECT_PICKER_BROWSE_CACHE_TTL_MS = 5 * 60_000 export const PROJECT_PICKER_BROWSE_CACHE_MAX_ENTRIES = 32 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 8871c047f7a..636382aa53c 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,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import type { GitHubProjectRow, GitHubProjectTable } from '../../../../shared/github-project-types' +import type { GitHubProjectRow, GitHubProjectTable } from '../../../../shared/github/project-types' import type { Repo } from '../../../../shared/types' import { filterProjectTableRowsByOpenRepos, 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 52dd0138274..b4c1b3738a3 100644 --- a/src/renderer/src/components/github-project/project-row-filtering.ts +++ b/src/renderer/src/components/github-project/project-row-filtering.ts @@ -1,4 +1,4 @@ -import type { GitHubProjectRow, GitHubProjectTable } from '../../../../shared/github-project-types' +import type { GitHubProjectRow, GitHubProjectTable } from '../../../../shared/github/project-types' import type { Repo } from '../../../../shared/types' export type ProjectRowSlugLookup = ( 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 6fe48619960..b7273b5e306 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 @@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs' import { join } from 'node:path' import { describe, expect, it } from 'vitest' -import type { GitHubProjectRow } from '../../../../shared/github-project-types' +import type { GitHubProjectRow } from '../../../../shared/github/project-types' const COMPONENT_ROOT = __dirname diff --git a/src/renderer/src/components/github-project/project-visible-table-cache.test.ts b/src/renderer/src/components/github-project/project-visible-table-cache.test.ts index e5eeabc40fc..ddf089a49d0 100644 --- a/src/renderer/src/components/github-project/project-visible-table-cache.test.ts +++ b/src/renderer/src/components/github-project/project-visible-table-cache.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { GitHubProjectTable } from '../../../../shared/github-project-types' +import type { GitHubProjectTable } from '../../../../shared/github/project-types' import { getSelectedRepoFingerprint, getNextVisibleProjectTableCache, diff --git a/src/renderer/src/components/github-project/project-visible-table-cache.ts b/src/renderer/src/components/github-project/project-visible-table-cache.ts index 6390e897281..e3dc0772aa0 100644 --- a/src/renderer/src/components/github-project/project-visible-table-cache.ts +++ b/src/renderer/src/components/github-project/project-visible-table-cache.ts @@ -1,4 +1,4 @@ -import type { GitHubProjectTable } from '../../../../shared/github-project-types' +import type { GitHubProjectTable } from '../../../../shared/github/project-types' export type CachedVisibleProjectTable = { cacheKey: string 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 6de67e2ca54..748edc1fe74 100644 --- a/src/renderer/src/components/github-project/slug-dialog/Comments.tsx +++ b/src/renderer/src/components/github-project/slug-dialog/Comments.tsx @@ -16,7 +16,7 @@ import type { GlobalSettings, PRComment } from '../../../../../shared/types' import type { GitHubProjectCommentMutationResult, GitHubProjectMutationResult -} from '../../../../../shared/github-project-types' +} from '../../../../../shared/github/project-types' import { translate } from '@/i18n/i18n' function getRuntimeTarget(settings: Parameters[0]) { diff --git a/src/renderer/src/components/github/IssueSourceIndicator.tsx b/src/renderer/src/components/github/IssueSourceIndicator.tsx index 57e3d0a2af7..c9615562e81 100644 --- a/src/renderer/src/components/github/IssueSourceIndicator.tsx +++ b/src/renderer/src/components/github/IssueSourceIndicator.tsx @@ -5,7 +5,7 @@ import { cn } from '@/lib/utils' import { githubRepoIdentityKey, isDefaultGitHubHost -} from '../../../../shared/github-repository-identity-key' +} from '../../../../shared/github/repository-identity-key' export type IssueSourceIndicatorProps = { /** Resolved issue-source owner/repo. `null` means the source hasn't been 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 9385ef14ff2..e81adba1f0e 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 @@ -8,7 +8,7 @@ import { import { getSettingsForRepoRuntimeOwner } from '@/lib/repo-runtime-owner' import type { GitHubWorkItemProjectOrigin } from '@/components/github/github-work-item-identity' import { notifyWorkItemDetailsMutation } from '@/components/github/github-work-item-comment-mutations' -import { githubProjectHost } from '../../../../shared/github-project-identity' +import { githubProjectHost } from '../../../../shared/github/project-identity' import { getTaskSourceRuntimeSettings, type TaskSourceContext 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 b4feceed9fb..f4808e17f3f 100644 --- a/src/renderer/src/components/github/github-work-item-identity.ts +++ b/src/renderer/src/components/github/github-work-item-identity.ts @@ -1,4 +1,4 @@ -import { githubProjectHost } from '../../../../shared/github-project-identity' +import { githubProjectHost } from '../../../../shared/github/project-identity' import type { GitHubOwnerRepo, GitHubWorkItem } from '../../../../shared/types' // Why: the dialog lacks repository context, so recover its host-aware identity from the canonical item URL. diff --git a/src/renderer/src/components/linear-api-key-dialog.tsx b/src/renderer/src/components/linear-api-key-dialog.tsx index b9e94ba3bd0..0f00205d26e 100644 --- a/src/renderer/src/components/linear-api-key-dialog.tsx +++ b/src/renderer/src/components/linear-api-key-dialog.tsx @@ -4,7 +4,7 @@ import type { LinearWorkspace } from '../../../shared/types' import { buildLinearPersonalApiKeySettingsUrl, buildLinearWorkspaceApiSettingsUrl -} from '../../../shared/linear-links' +} from '../../../shared/linear/links' import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' import { useAppStore } from '@/store' import { useMountedRef } from '@/hooks/useMountedRef' 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 8fa3956959d..2352f25b876 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 @@ -9,7 +9,7 @@ import { countLinearIssueAttributeFilters, linearIssueAttributeFilterPillLabels } from './linear-issue-attribute-filter-sections' -import type { LinearIssueAttributeFilter } from '../../../shared/linear-issue-attribute-filter' +import type { LinearIssueAttributeFilter } from '../../../shared/linear/issue-attribute-filter' import LinearIssueAttributeFilterDropdowns from './linear-issue-attribute-filter-dropdowns' const metadataMocks = vi.hoisted(() => ({ 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 85e6553ba36..e914a40c064 100644 --- a/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.tsx +++ b/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.tsx @@ -11,7 +11,7 @@ import { canonicalizeLinearIssueAttributeFilter, emptyLinearIssueAttributeFilter, type LinearIssueAttributeFilter -} from '../../../shared/linear-issue-attribute-filter' +} from '../../../shared/linear/issue-attribute-filter' import type { LinearTeam } from '../../../shared/types' import { LinearIssueFilterSectionDetail, diff --git a/src/renderer/src/components/linear-issue-attribute-filter-sections.tsx b/src/renderer/src/components/linear-issue-attribute-filter-sections.tsx index 451a25a8ea9..dcf12b026e9 100644 --- a/src/renderer/src/components/linear-issue-attribute-filter-sections.tsx +++ b/src/renderer/src/components/linear-issue-attribute-filter-sections.tsx @@ -10,7 +10,7 @@ import { translate } from '@/i18n/i18n' import { canonicalizeLinearIssueAttributeFilter, type LinearIssueAttributeFilter -} from '../../../shared/linear-issue-attribute-filter' +} from '../../../shared/linear/issue-attribute-filter' import { getLinearPriorityLabel } from './task-page-localized-options' export type LinearIssueFilterSectionKey = 'status' | 'priority' | 'assignee' | 'labels' diff --git a/src/renderer/src/components/linear-issue-view-storage.test.ts b/src/renderer/src/components/linear-issue-view-storage.test.ts index b9654721ba8..c1c632ed116 100644 --- a/src/renderer/src/components/linear-issue-view-storage.test.ts +++ b/src/renderer/src/components/linear-issue-view-storage.test.ts @@ -1,6 +1,6 @@ // @vitest-environment happy-dom import { beforeEach, describe, expect, it } from 'vitest' -import { defaultLinearIssueViewResumeState } from '../../../shared/linear-issue-view-resume-state' +import { defaultLinearIssueViewResumeState } from '../../../shared/linear/issue-view-resume-state' import { loadLinearIssueView, saveLinearIssueView } from './linear-issue-view-storage' const STORAGE_KEY = 'orca.linear.issue-view.v1' diff --git a/src/renderer/src/components/linear-issue-view-storage.ts b/src/renderer/src/components/linear-issue-view-storage.ts index b34f0cc55fa..2a227644028 100644 --- a/src/renderer/src/components/linear-issue-view-storage.ts +++ b/src/renderer/src/components/linear-issue-view-storage.ts @@ -2,7 +2,7 @@ import { normalizeLinearIssueViewResumeState, resolveLinearIssueViewResumeState, type LinearIssueViewResumeState -} from '../../../shared/linear-issue-view-resume-state' +} from '../../../shared/linear/issue-view-resume-state' // Why: issue-list layout is a per-device view preference, not host state. Routing it // through `ui.set` put it inside a strict nested schema, where a host that predates diff --git a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx index b87523a7d2d..66dd756e18f 100644 --- a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx +++ b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx @@ -96,7 +96,7 @@ import { type TaskSourceContext } from '../../../../shared/task-source-context' import { parseExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host' -import { githubRepoIdentityKey } from '../../../../shared/github-repository-identity-key' +import { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key' import { callRuntimeRpc } from '@/runtime/runtime-rpc-client' import { getGitHubRuntimeRepoId, diff --git a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx index 25489863d7f..2d63be3bacf 100644 --- a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx +++ b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx @@ -40,7 +40,7 @@ import { DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { isFolderRepo } from '../../../../shared/repo-kind' -import { githubProjectHost } from '../../../../shared/github-project-identity' +import { githubProjectHost } from '../../../../shared/github/project-identity' import HostedReviewActions from './HostedReviewActions' import { GitHubPRStackMap, type GitHubPRStackMapNavigationModifiers } from './GitHubPRStackMap' import { @@ -97,7 +97,7 @@ import type { PendingPRCommentAiAck, PendingPRCommentAiAckGithubTarget } from './pr-comments-ai-launch-ack' -import { parseGitHubIssueOrPRLink } from '../../../../shared/github-links' +import { parseGitHubIssueOrPRLink } from '../../../../shared/github/links' import { startFixChecksAgent } from '@/lib/fix-checks-agent-launch' import { hostedReviewProviderSupportsDraft, diff --git a/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx b/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx index b8adeb40c4f..cace5ee7d98 100644 --- a/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx +++ b/src/renderer/src/components/right-sidebar/HostedReviewActions.tsx @@ -13,7 +13,7 @@ import { } from '@/components/ui/dropdown-menu' import { presentGitHubPRMergeState } from '@/components/github-pr-merge-state' import type { PRInfo, Repo, Worktree } from '../../../../shared/types' -import { resolveGitHubPRMergeMethods } from '../../../../shared/github-pr-merge-methods' +import { resolveGitHubPRMergeMethods } from '../../../../shared/github/pull-request-merge-methods' import { runWorktreeDelete } from '../sidebar/delete-worktree-flow' import { presentGitLabMRMergeState } from './gitlab-mr-merge-state' import { 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 c9ad8137fc9..bcd3391a4fc 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 @@ -4,7 +4,7 @@ import { } from '../../../../shared/cross-platform-path' import type { ProjectHostSetupProjection } from '../../../../shared/project-host-setup-projection' import type { ProjectHostSetup, Worktree } from '../../../../shared/types' -import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree-id' +import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree/id' export function deriveAiVaultWorkspaceScopePaths( activeWorktree: Pick | null, diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-row-display.tsx b/src/renderer/src/components/right-sidebar/ai-vault-session-row-display.tsx index f0c9d2035d4..2ad0b1cca39 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-row-display.tsx +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-row-display.tsx @@ -6,7 +6,7 @@ import { AgentIcon } from '@/lib/agent-catalog' import type { AgentStatusState } from '../../../../shared/agent-status-types' import { useRepoById } from '@/store/selectors' import { resolveRepoBadgeColor } from '../../../../shared/repo-badge-color' -import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree-id' +import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree/id' import { isAiVaultSessionRecoverableEmpty, type AiVaultScope, 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 d03aedb809e..a82a68c8f1e 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 @@ -1,6 +1,6 @@ import { useMemo } from 'react' import { parseWslUncPath } from '../../../../shared/wsl-paths' -import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree-id' +import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree/id' import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID, 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 e3cee92b293..fc95acb9a7c 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,5 +1,5 @@ import type { GitHubOwnerRepo } from '../../../../shared/types' -import { githubRepoIdentityKey } from '../../../../shared/github-repository-identity-key' +import { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key' function normalizedPRRepoIdentity(prRepo?: GitHubOwnerRepo | null): string { return prRepo ? githubRepoIdentityKey(prRepo) : 'none' 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 c8f2f3cb720..ab92bbd1469 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 @@ -1,5 +1,5 @@ import { parseWslUncPath } from '../../../../shared/wsl-paths' -import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree-id' +import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree/id' import { isPathInsideOrEqual, isRuntimePathAbsolute, diff --git a/src/renderer/src/components/settings/McpConfigSection.tsx b/src/renderer/src/components/settings/McpConfigSection.tsx index 881cd3eb8bb..ce9b3d8db4a 100644 --- a/src/renderer/src/components/settings/McpConfigSection.tsx +++ b/src/renderer/src/components/settings/McpConfigSection.tsx @@ -3,7 +3,7 @@ import { AlertCircle, FileCode2, LoaderCircle, Plus, RefreshCw } from 'lucide-re import { toast } from 'sonner' import { useMountedRef } from '@/hooks/useMountedRef' import type { Repo, Worktree } from '../../../../shared/types' -import { getRepoIdFromWorktreeId } from '../../../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id' import { canInspectLocalMcpConfigRoot, inspectMcpConfigContent, diff --git a/src/renderer/src/components/settings/manage-sessions-format.ts b/src/renderer/src/components/settings/manage-sessions-format.ts index ee1c4c7a193..f7e50dbbddc 100644 --- a/src/renderer/src/components/settings/manage-sessions-format.ts +++ b/src/renderer/src/components/settings/manage-sessions-format.ts @@ -1,5 +1,5 @@ import type { PtyManagementSession } from '../../../../preload/api-types' -import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree-id' +import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree/id' export function shortCwd(cwd: string): string { if (!cwd) { diff --git a/src/renderer/src/components/settings/repository-icon-github.ts b/src/renderer/src/components/settings/repository-icon-github.ts index b5519e36f36..0a44a88f030 100644 --- a/src/renderer/src/components/settings/repository-icon-github.ts +++ b/src/renderer/src/components/settings/repository-icon-github.ts @@ -1,6 +1,6 @@ import type { GitHubRepositoryIdentity, Repo } from '../../../../shared/types' import { githubAvatarIcon, githubAvatarSlug, type RepoIcon } from '../../../../shared/repo-icon' -import { githubRepoIdentityKey } from '../../../../shared/github-repository-identity-key' +import { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key' import { callRuntimeRpc, type getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' type RuntimeTarget = ReturnType diff --git a/src/renderer/src/components/settings/repository-runtime-session-summary.ts b/src/renderer/src/components/settings/repository-runtime-session-summary.ts index 0279683b22b..5dffcd3b1e2 100644 --- a/src/renderer/src/components/settings/repository-runtime-session-summary.ts +++ b/src/renderer/src/components/settings/repository-runtime-session-summary.ts @@ -1,5 +1,5 @@ import type { AppState } from '../../store/types' -import { getRepoIdFromWorktreeId } from '../../../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id' export type ProjectRuntimeSessionSummary = { liveTerminalCount: number diff --git a/src/renderer/src/components/sidebar/PreservedBranchBatchReviewDialog.tsx b/src/renderer/src/components/sidebar/PreservedBranchBatchReviewDialog.tsx index c1340b79327..21b2321c814 100644 --- a/src/renderer/src/components/sidebar/PreservedBranchBatchReviewDialog.tsx +++ b/src/renderer/src/components/sidebar/PreservedBranchBatchReviewDialog.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react' import { Trash2 } from 'lucide-react' -import { getRepoIdFromWorktreeId } from '../../../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id' import { translate } from '@/i18n/i18n' import type { PreservedBranchCleanup } from '@/lib/preserved-branch-cleanup' import { useAppStore } from '@/store' 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 fef27ea71cc..4c31d82e153 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx @@ -9,7 +9,7 @@ import type { Worktree, WorktreeCardProperty } from '../../../../shared/types' -import { COMPACT_WORKTREE_CARD_PROPERTIES } from '../../../../shared/worktree-card-properties' +import { COMPACT_WORKTREE_CARD_PROPERTIES } from '../../../../shared/worktree/card-properties' import type { WorkspacePortScanResult } from '../../../../shared/workspace-ports' const fetchHostedReviewForBranch = vi.fn() diff --git a/src/renderer/src/components/sidebar/WorktreeList.tsx b/src/renderer/src/components/sidebar/WorktreeList.tsx index ff5d8c13923..0202dd41d70 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.tsx @@ -257,7 +257,7 @@ import { isGitRepoKind } from '../../../../shared/repo-kind' import { effectiveExternalWorktreeVisibility, isLegacyRepoForExternalWorktreeVisibility -} from '../../../../shared/worktree-ownership' +} from '../../../../shared/worktree/ownership' import { RepoIconGlyph } from '@/components/repo/repo-icon' import { RepoForkIndicator } from '@/components/repo/repo-fork-indicator' import ImportedWorktreesVisibilityLine from './ImportedWorktreesVisibilityLine' diff --git a/src/renderer/src/components/sidebar/WorktreeVisibilityDialog.tsx b/src/renderer/src/components/sidebar/WorktreeVisibilityDialog.tsx index 835f68d317e..baa73875d79 100644 --- a/src/renderer/src/components/sidebar/WorktreeVisibilityDialog.tsx +++ b/src/renderer/src/components/sidebar/WorktreeVisibilityDialog.tsx @@ -44,7 +44,7 @@ import { normalizeCustomWorktreeVisibilitySources, normalizeWorktreeVisibilitySourcePreferences, removeCustomWorktreeSourcePreference -} from '../../../../shared/worktree-visibility-sources' +} from '../../../../shared/worktree/visibility-sources' import HiddenWorktreeRecoveryList from './HiddenWorktreeRecoveryList' import { worktreeVisibilityUpdateError } from './worktree-visibility-update-error' diff --git a/src/renderer/src/components/sidebar/WorktreeVisibilitySourceList.tsx b/src/renderer/src/components/sidebar/WorktreeVisibilitySourceList.tsx index 2ff19c4ff06..f7caa8bdee6 100644 --- a/src/renderer/src/components/sidebar/WorktreeVisibilitySourceList.tsx +++ b/src/renderer/src/components/sidebar/WorktreeVisibilitySourceList.tsx @@ -18,7 +18,7 @@ import { effectiveCustomWorktreeSourceVisibility, normalizeCustomWorktreeVisibilitySources, type WorktreeVisibilitySourceMatch -} from '../../../../shared/worktree-visibility-sources' +} from '../../../../shared/worktree/visibility-sources' import { effectiveExternalWorktreeVisibility, isLegacyRepoForExternalWorktreeVisibility diff --git a/src/renderer/src/components/sidebar/delete-worktree-failure-toast.tsx b/src/renderer/src/components/sidebar/delete-worktree-failure-toast.tsx index bb9649f28f3..549c36c97a0 100644 --- a/src/renderer/src/components/sidebar/delete-worktree-failure-toast.tsx +++ b/src/renderer/src/components/sidebar/delete-worktree-failure-toast.tsx @@ -5,7 +5,7 @@ import { translate } from '@/i18n/i18n' import { isLockedWorktreeRemovalError, type WorktreeForceDeleteReason -} from '../../../../shared/worktree-removal' +} from '../../../../shared/worktree/removal' type DeleteWorktreeFailureToastOptions = { error: string diff --git a/src/renderer/src/components/sidebar/delete-worktree-toast.test.ts b/src/renderer/src/components/sidebar/delete-worktree-toast.test.ts index acaa7d8f16f..dffedf06337 100644 --- a/src/renderer/src/components/sidebar/delete-worktree-toast.test.ts +++ b/src/renderer/src/components/sidebar/delete-worktree-toast.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { getDeleteWorktreeToastCopy } from './delete-worktree-toast' -import { classifyWorktreeForceDeleteReason } from '../../../../shared/worktree-removal' +import { classifyWorktreeForceDeleteReason } from '../../../../shared/worktree/removal' // Why: production never hands this function a literal reason — the store derives it from // classifyWorktreeForceDeleteReason (store/slices/worktrees.ts). Passing one in would let a diff --git a/src/renderer/src/components/sidebar/delete-worktree-toast.ts b/src/renderer/src/components/sidebar/delete-worktree-toast.ts index fac1c6ee06a..1c013da0da6 100644 --- a/src/renderer/src/components/sidebar/delete-worktree-toast.ts +++ b/src/renderer/src/components/sidebar/delete-worktree-toast.ts @@ -3,7 +3,7 @@ import { isLockedWorktreeRemovalError, isProvenLivePtyRemovalError, type WorktreeForceDeleteReason -} from '../../../../shared/worktree-removal' +} from '../../../../shared/worktree/removal' export type DeleteWorktreeToastCopy = { title: string description?: string 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 d701ff20a21..e827d1662ef 100644 --- a/src/renderer/src/components/sidebar/imported-worktrees-card-candidates.ts +++ b/src/renderer/src/components/sidebar/imported-worktrees-card-candidates.ts @@ -4,7 +4,7 @@ import { isGitRepoKind } from '../../../../shared/repo-kind' import { effectiveExternalWorktreeVisibility, isLegacyRepoForExternalWorktreeVisibility -} from '../../../../shared/worktree-ownership' +} from '../../../../shared/worktree/ownership' import type { ImportedWorktreesCardCandidate } from './worktree-list-groups' export function getHiddenImportedWorktrees( diff --git a/src/renderer/src/components/sidebar/preserved-branch-batch-toast.tsx b/src/renderer/src/components/sidebar/preserved-branch-batch-toast.tsx index d98cd37a438..a5d553da042 100644 --- a/src/renderer/src/components/sidebar/preserved-branch-batch-toast.tsx +++ b/src/renderer/src/components/sidebar/preserved-branch-batch-toast.tsx @@ -3,7 +3,7 @@ import { toast } from 'sonner' import { useAppStore } from '@/store' import { translate } from '@/i18n/i18n' import { mapWithConcurrency } from '../../../../shared/map-with-concurrency' -import { getRepoIdFromWorktreeId } from '../../../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id' import { Button } from '../ui/button' import type { PreservedBranchCleanup } from '@/lib/preserved-branch-cleanup' diff --git a/src/renderer/src/components/sidebar/use-worktree-issue-link.ts b/src/renderer/src/components/sidebar/use-worktree-issue-link.ts index 939d71bb10c..bf1e0a1bb31 100644 --- a/src/renderer/src/components/sidebar/use-worktree-issue-link.ts +++ b/src/renderer/src/components/sidebar/use-worktree-issue-link.ts @@ -4,7 +4,7 @@ import { parseGitHubIssueOrPRNumber } from '@/lib/github-links' import { issueCacheKey as getIssueCacheKey } from '@/store/slices/github' import { useMountedRef } from '@/hooks/useMountedRef' import { findIndexedWorktreeOwner } from '@/lib/worktree-runtime-owner-index' -import { buildLinearIssueUrl, parseLinearIssueInput } from '../../../../shared/linear-links' +import { buildLinearIssueUrl, parseLinearIssueInput } from '../../../../shared/linear/links' import type { IssueLinkProvider } from '../../../../shared/issue-link-input' import type { TaskSourceContext } from '../../../../shared/task-source-context' import { parseExplicitGitHubIssueUrl } from './worktree-meta-updates' diff --git a/src/renderer/src/components/sidebar/worktree-meta-updates.ts b/src/renderer/src/components/sidebar/worktree-meta-updates.ts index 944da175d77..20afdf8af26 100644 --- a/src/renderer/src/components/sidebar/worktree-meta-updates.ts +++ b/src/renderer/src/components/sidebar/worktree-meta-updates.ts @@ -2,7 +2,7 @@ import { parseGitHubIssueOrPRLink, parseGitHubIssueOrPRNumber } from '@/lib/gith import { buildLinearIssueLinkUpdates, LINEAR_ISSUE_LINK_CLEARED -} from '../../../../shared/linear-links' +} 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' diff --git a/src/renderer/src/components/status-bar/WorkspaceSpaceManagerPanel.tsx b/src/renderer/src/components/status-bar/WorkspaceSpaceManagerPanel.tsx index 4353a6fb81a..453e3553792 100644 --- a/src/renderer/src/components/status-bar/WorkspaceSpaceManagerPanel.tsx +++ b/src/renderer/src/components/status-bar/WorkspaceSpaceManagerPanel.tsx @@ -84,7 +84,7 @@ import { type WorkspaceSpaceSortKey } from './workspace-space-presentation' import { translate } from '@/i18n/i18n' -import type { WorktreeForceDeleteReason } from '../../../../shared/worktree-removal' +import type { WorktreeForceDeleteReason } from '../../../../shared/worktree/removal' const TREEMAP_FILLS = [ 'color-mix(in srgb, var(--chart-2) 34%, var(--card))', diff --git a/src/renderer/src/components/status-bar/mergeSnapshotAndSessions.ts b/src/renderer/src/components/status-bar/mergeSnapshotAndSessions.ts index baf9c189fde..672e7c622c2 100644 --- a/src/renderer/src/components/status-bar/mergeSnapshotAndSessions.ts +++ b/src/renderer/src/components/status-bar/mergeSnapshotAndSessions.ts @@ -23,7 +23,7 @@ import { parsePaneKey as parseStablePaneKey } from '../../../../shared/stable-pa import { getRepoIdFromWorktreeId, getWorktreePathBasenameFromId -} from '../../../../shared/worktree-id' +} from '../../../../shared/worktree/id' import type { DaemonSession, MergeContext, diff --git a/src/renderer/src/components/tab-bar/TabBarQuickCommandsButton.tsx b/src/renderer/src/components/tab-bar/TabBarQuickCommandsButton.tsx index 5e989f9e4a9..776e7939b3e 100644 --- a/src/renderer/src/components/tab-bar/TabBarQuickCommandsButton.tsx +++ b/src/renderer/src/components/tab-bar/TabBarQuickCommandsButton.tsx @@ -10,7 +10,7 @@ import { getTerminalQuickCommandScope, isTerminalQuickCommandComplete } from '../../../../shared/terminal-quick-commands' -import { getRepoIdFromWorktreeId } from '../../../../shared/worktree-id' +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' 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 38f1dbe4802..55ae9293b85 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,5 +1,5 @@ import type { LinearIssue, LinearWorkspace, Worktree } from '../../../shared/types' -import { parseLinearIssueInput } from '../../../shared/linear-links' +import { parseLinearIssueInput } from '../../../shared/linear/links' import { getTaskSourceCacheScope, type TaskSourceContext diff --git a/src/renderer/src/components/task-page-linear-issue-empty-state.test.ts b/src/renderer/src/components/task-page-linear-issue-empty-state.test.ts index 914d24db855..9c04161e722 100644 --- a/src/renderer/src/components/task-page-linear-issue-empty-state.test.ts +++ b/src/renderer/src/components/task-page-linear-issue-empty-state.test.ts @@ -3,7 +3,7 @@ import { resolveLinearIssueEmptyKind, shouldOfferLinearIssueFetchMore } from './task-page-linear-issue-empty-state' -import { emptyLinearIssueAttributeFilter } from '../../../shared/linear-issue-attribute-filter' +import { emptyLinearIssueAttributeFilter } from '../../../shared/linear/issue-attribute-filter' describe('task-page-linear-issue-empty-state', () => { it('prefers context, then search, then server filter, then client team, then unfiltered', () => { diff --git a/src/renderer/src/components/task-page-linear-issue-empty-state.ts b/src/renderer/src/components/task-page-linear-issue-empty-state.ts index a62370a1130..c4c4e4a5fb8 100644 --- a/src/renderer/src/components/task-page-linear-issue-empty-state.ts +++ b/src/renderer/src/components/task-page-linear-issue-empty-state.ts @@ -1,7 +1,7 @@ import { isEmptyLinearIssueAttributeFilter, type LinearIssueAttributeFilter -} from '../../../shared/linear-issue-attribute-filter' +} from '../../../shared/linear/issue-attribute-filter' export type LinearIssueEmptyKind = | 'context' diff --git a/src/renderer/src/components/task-page-linear-issue-request.test.ts b/src/renderer/src/components/task-page-linear-issue-request.test.ts index f2ce103f645..1df1d8bd0ff 100644 --- a/src/renderer/src/components/task-page-linear-issue-request.test.ts +++ b/src/renderer/src/components/task-page-linear-issue-request.test.ts @@ -7,7 +7,7 @@ import { shouldForceLinearIssueListRead, teamDerivedFacetsForPrimaryTeamChange } from './task-page-linear-issue-request' -import type { LinearIssueAttributeFilter } from '../../../shared/linear-issue-attribute-filter' +import type { LinearIssueAttributeFilter } from '../../../shared/linear/issue-attribute-filter' const filter: LinearIssueAttributeFilter = { stateIds: ['s1'], diff --git a/src/renderer/src/components/task-page-linear-issue-request.ts b/src/renderer/src/components/task-page-linear-issue-request.ts index 3e85be2def0..158770d26ef 100644 --- a/src/renderer/src/components/task-page-linear-issue-request.ts +++ b/src/renderer/src/components/task-page-linear-issue-request.ts @@ -2,7 +2,7 @@ import { isEmptyLinearIssueAttributeFilter, linearIssueAttributeFilterSignature, type LinearIssueAttributeFilter -} from '../../../shared/linear-issue-attribute-filter' +} from '../../../shared/linear/issue-attribute-filter' import { getTaskSourceCacheScope, type TaskSourceContext diff --git a/src/renderer/src/components/task-page-localized-options.tsx b/src/renderer/src/components/task-page-localized-options.tsx index 74ef7152e8f..506f27add17 100644 --- a/src/renderer/src/components/task-page-localized-options.tsx +++ b/src/renderer/src/components/task-page-localized-options.tsx @@ -13,7 +13,7 @@ import { type LinearGroupBy, type LinearOrderBy, type LinearViewMode -} from '../../../shared/linear-issue-view-resume-state' +} from '../../../shared/linear/issue-view-resume-state' import { getTaskPresetQuery } from '../../../shared/task-preset-query' import type { TaskProvider, TaskViewPresetId } from '../../../shared/types' @@ -46,7 +46,7 @@ export type { LinearGroupBy, LinearOrderBy, LinearViewMode -} from '../../../shared/linear-issue-view-resume-state' +} from '../../../shared/linear/issue-view-resume-state' export function LinearIcon({ className }: { className?: string }): React.JSX.Element { return ( diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx index 2c2dd44cf92..5546390b6db 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx @@ -163,7 +163,7 @@ import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../shared/execution-host' -import { getRepoIdFromWorktreeId } from '../../../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id' import { useProjectHostSetupProjection, useRepoById } from '@/store/selectors' import { refitAndRefreshAllTerminalPanes } from '@/lib/pane-manager/pane-manager-registry' import { 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 90a3341ad6c..6fbe70501f8 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.test.ts @@ -2236,7 +2236,7 @@ describe('connectPanePty', () => { it('swallows a worktree-removal fence error instead of surfacing it', async () => { const { connectPanePty } = await import('./pty-connection') const { TERMINAL_REMOVAL_IN_PROGRESS_MESSAGE } = - await import('../../../../shared/worktree-removal-fence-error') + await import('../../../../shared/worktree/removal-fence-error') const transport = createMockTransport() const capturedOnError: { current: ((message: string) => void) | null } = { current: null } transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index b4a683874ee..e2efbb0a9c4 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -20,7 +20,7 @@ import { TerminalKittyKeyboardModeTracker } from '../../../../shared/terminal-ki import { parseTerminalKittyKeyboardFlags } from '../../../../shared/terminal-kitty-keyboard-flags' import { isRuntimeOwnedSshTargetId, parseExecutionHostId } from '../../../../shared/execution-host' import { createTerminalZeroDimensionsMessage } from '../../../../shared/terminal-zero-dimensions-diagnostic' -import { isWorktreeRemovalFenceError } from '../../../../shared/worktree-removal-fence-error' +import { isWorktreeRemovalFenceError } from '../../../../shared/worktree/removal-fence-error' import { parseTerminalOscColorQuery } from '../../../../shared/terminal-osc-color-reply' import { HIDDEN_STARTUP_RENDERER_QUERY_PENDING_CHARS, diff --git a/src/renderer/src/hooks/remote-workspace-session-merge.ts b/src/renderer/src/hooks/remote-workspace-session-merge.ts index cc9d47ab6e7..0f772ff8794 100644 --- a/src/renderer/src/hooks/remote-workspace-session-merge.ts +++ b/src/renderer/src/hooks/remote-workspace-session-merge.ts @@ -1,6 +1,6 @@ import type { TerminalTab, WorkspaceSessionState } from '../../../shared/types' import { worktreeWorkspaceKey } from '../../../shared/workspace-scope' -import { splitWorktreeId } from '../../../shared/worktree-id' +import { splitWorktreeId } from '../../../shared/worktree/id' import type { AppState } from '../store/types' function preserveNewerLocalTerminalFields(remote: TerminalTab, local: TerminalTab): TerminalTab { diff --git a/src/renderer/src/hooks/useComposerState.ts b/src/renderer/src/hooks/useComposerState.ts index 4ed46768687..aa83700baf2 100644 --- a/src/renderer/src/hooks/useComposerState.ts +++ b/src/renderer/src/hooks/useComposerState.ts @@ -59,7 +59,7 @@ import type { WorkspaceCreateTelemetrySource, ProjectGroup } from '../../../shared/types' -import { githubRepoIdentityKey } from '../../../shared/github-repository-identity-key' +import { githubRepoIdentityKey } from '../../../shared/github/repository-identity-key' import { isWorkspaceStatusId } from '../../../shared/workspace-statuses' import { CLIENT_PLATFORM, diff --git a/src/renderer/src/hooks/useGitHubSlugMetadata.ts b/src/renderer/src/hooks/useGitHubSlugMetadata.ts index 3da1c66787d..a268a7bbc93 100644 --- a/src/renderer/src/hooks/useGitHubSlugMetadata.ts +++ b/src/renderer/src/hooks/useGitHubSlugMetadata.ts @@ -3,10 +3,10 @@ import type { GitHubAssignableUser, GlobalSettings } from '../../../shared/types import type { ListAssignableUsersBySlugResult, ListLabelsBySlugResult -} from '../../../shared/github-project-types' +} from '../../../shared/github/project-types' import { clearMetadataRequestStore, createMetadataRequestStore } from './metadata-request-cache' -import { githubRepoIdentityKey } from '../../../shared/github-repository-identity-key' -import { githubProjectHost } from '../../../shared/github-project-identity' +import { githubRepoIdentityKey } from '../../../shared/github/repository-identity-key' +import { githubProjectHost } from '../../../shared/github/project-identity' import { useMetadataListRequest, type MetadataListState } from './useMetadataListRequest' const slugLabelStore = createMetadataRequestStore() diff --git a/src/renderer/src/lib/ai-vault-resume-target.ts b/src/renderer/src/lib/ai-vault-resume-target.ts index fca20f25cc8..2cab097ca3d 100644 --- a/src/renderer/src/lib/ai-vault-resume-target.ts +++ b/src/renderer/src/lib/ai-vault-resume-target.ts @@ -7,7 +7,7 @@ import { type ExecutionHostId } from '../../../shared/execution-host' import type { Repo } from '../../../shared/types' -import { getRepoIdFromWorktreeId } from '../../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id' import { parseWorkspaceKey } from '../../../shared/workspace-scope' import { isWslUncPath } from '../../../shared/wsl-paths' import type { AppState } from '@/store/types' 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 bbc7b0e3f10..ba7fcc03d5a 100644 --- a/src/renderer/src/lib/cmd-j-github-url-lookup.ts +++ b/src/renderer/src/lib/cmd-j-github-url-lookup.ts @@ -1,4 +1,4 @@ -import type { GitHubIssueOrPRLink } from '../../../shared/github-links' +import type { GitHubIssueOrPRLink } from '../../../shared/github/links' import type { GitHubWorkItem, Repo } from '../../../shared/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-linear-issue-intent.test.ts b/src/renderer/src/lib/cmd-j-linear-issue-intent.test.ts index 124294f5e4f..672f058e25a 100644 --- a/src/renderer/src/lib/cmd-j-linear-issue-intent.test.ts +++ b/src/renderer/src/lib/cmd-j-linear-issue-intent.test.ts @@ -4,7 +4,7 @@ import { findLinearIssueWorkspaceLookupIds, isLinearIssueUrlResolutionMatch, parseLinearIssueUrlIntent -} from '../../../shared/linear-links' +} from '../../../shared/linear/links' describe('Cmd+J Linear issue intent', () => { it('accepts canonical Linear issue URLs with optional slugs', () => { diff --git a/src/renderer/src/lib/connection-owner-resolution.ts b/src/renderer/src/lib/connection-owner-resolution.ts index 3d365621546..91169e939c6 100644 --- a/src/renderer/src/lib/connection-owner-resolution.ts +++ b/src/renderer/src/lib/connection-owner-resolution.ts @@ -1,7 +1,7 @@ import type { AppState } from '@/store/types' import { getIndexedRepoMap, getIndexedWorktreeMap } from '@/store/worktree-repo-index' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' -import { getRepoIdFromWorktreeId } from '../../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id' import { parseWorkspaceKey } from '../../../shared/workspace-scope' import { isPathInsideOrEqual, diff --git a/src/renderer/src/lib/linear-issue-url-lookup.ts b/src/renderer/src/lib/linear-issue-url-lookup.ts index 03e2a1d56f9..55493d628f7 100644 --- a/src/renderer/src/lib/linear-issue-url-lookup.ts +++ b/src/renderer/src/lib/linear-issue-url-lookup.ts @@ -3,7 +3,7 @@ import { findLinearIssueWorkspaceLookupIds, isLinearIssueUrlResolutionMatch, type LinearIssueUrlIntent -} from '../../../shared/linear-links' +} from '../../../shared/linear/links' import type { LinearConnectionStatus, LinearIssue } from '../../../shared/types' import type { TaskSourceContext } from '../../../shared/task-source-context' diff --git a/src/renderer/src/lib/linear-issue-workspace-attachment.ts b/src/renderer/src/lib/linear-issue-workspace-attachment.ts index afea291b21f..e1904dd02d4 100644 --- a/src/renderer/src/lib/linear-issue-workspace-attachment.ts +++ b/src/renderer/src/lib/linear-issue-workspace-attachment.ts @@ -2,7 +2,7 @@ import type { LinearIssue, Worktree } from '../../../shared/types' import { getLinearOrganizationUrlKeyFromIssueUrl, parseLinearIssueInput -} from '../../../shared/linear-links' +} from '../../../shared/linear/links' import { getWorktreeAttachmentLabel } from './worktree-attachment-label' export type LinearIssueAttachmentRef = Pick & diff --git a/src/renderer/src/lib/repo-slug-cache.test.ts b/src/renderer/src/lib/repo-slug-cache.test.ts index d9de7ae3e7b..491afbc22b9 100644 --- a/src/renderer/src/lib/repo-slug-cache.test.ts +++ b/src/renderer/src/lib/repo-slug-cache.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it } from 'vitest' import type { Repo } from '../../../shared/types' -import { githubRepoIdentityKey } from '../../../shared/github-repository-identity-key' +import { githubRepoIdentityKey } from '../../../shared/github/repository-identity-key' import { REPO_SLUG_FAILURE_TTL_MS, clearRepoSlugCacheValues, diff --git a/src/renderer/src/lib/repo-slug-cache.ts b/src/renderer/src/lib/repo-slug-cache.ts index cb6552aa572..9c2a455f574 100644 --- a/src/renderer/src/lib/repo-slug-cache.ts +++ b/src/renderer/src/lib/repo-slug-cache.ts @@ -7,7 +7,7 @@ import { getSettingsForRepoRuntimeOwner } from './repo-runtime-owner' import { githubHostFromIdentityKey, githubRepoIdentityKey -} from '../../../shared/github-repository-identity-key' +} from '../../../shared/github/repository-identity-key' /** Lowercased `owner/repo` → Repo[]. */ export type SlugIndex = Map diff --git a/src/renderer/src/lib/repo-slug-index.ts b/src/renderer/src/lib/repo-slug-index.ts index 442a29d6d62..07d6896cb08 100644 --- a/src/renderer/src/lib/repo-slug-index.ts +++ b/src/renderer/src/lib/repo-slug-index.ts @@ -29,7 +29,7 @@ import { type RepoSlugMatches, type SlugIndex } from './repo-slug-cache' -import { githubRepoIdentityKey } from '../../../shared/github-repository-identity-key' +import { githubRepoIdentityKey } from '../../../shared/github/repository-identity-key' export { lookupReposBySlugFromCache } from './repo-slug-cache' diff --git a/src/renderer/src/lib/smart-github-submit.ts b/src/renderer/src/lib/smart-github-submit.ts index 131e532f543..9d500d98eb9 100644 --- a/src/renderer/src/lib/smart-github-submit.ts +++ b/src/renderer/src/lib/smart-github-submit.ts @@ -5,7 +5,7 @@ import { getLinkedWorkItemWorkspaceName } from '../../../shared/workspace-name' import type { LinkedWorkItemSummary } from './new-workspace' import { parseGitHubIssueOrPRLink } from './github-links' import { resolveGitHubWorkItemIdentity } from '@/lib/github-work-item-identity' -import { githubRepoIdentityKey } from '../../../shared/github-repository-identity-key' +import { githubRepoIdentityKey } from '../../../shared/github/repository-identity-key' export type SmartGitHubSubmitIntent = | { diff --git a/src/renderer/src/lib/workspace-session-host-persistence.ts b/src/renderer/src/lib/workspace-session-host-persistence.ts index 445ef2917e4..47a93b4d577 100644 --- a/src/renderer/src/lib/workspace-session-host-persistence.ts +++ b/src/renderer/src/lib/workspace-session-host-persistence.ts @@ -6,7 +6,7 @@ import { type ExecutionHostId } from '../../../shared/execution-host' import { parseWorkspaceKey } from '../../../shared/workspace-scope' -import { getRepoIdFromWorktreeId } from '../../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id' import { mergeWorkspaceSessionsFromHosts, splitWorkspaceSessionByHost, diff --git a/src/renderer/src/lib/workspace-session-hydration-keys.ts b/src/renderer/src/lib/workspace-session-hydration-keys.ts index 1166d8702a1..d4339e30cb6 100644 --- a/src/renderer/src/lib/workspace-session-hydration-keys.ts +++ b/src/renderer/src/lib/workspace-session-hydration-keys.ts @@ -2,7 +2,7 @@ import type { ExecutionHostId } from '../../../shared/execution-host' import { parseExecutionHostId } from '../../../shared/execution-host' import type { WorkspaceKey, WorkspaceSessionState } from '../../../shared/types' import { parseWorkspaceKey } from '../../../shared/workspace-scope' -import { getRepoIdFromWorktreeId } from '../../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id' export type WorkspaceSessionHydrationOptions = { additionalValidWorkspaceKeys?: readonly WorkspaceKey[] 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 3676b7edacb..d16d135dcb1 100644 --- a/src/renderer/src/lib/worktree-palette-task-url-match.ts +++ b/src/renderer/src/lib/worktree-palette-task-url-match.ts @@ -9,11 +9,11 @@ import { parseGitHubIssueOrPRLink, type GitHubIssueOrPRLink, type RepoSlug -} from '../../../shared/github-links' -import { githubRepoIdentityKey } from '../../../shared/github-repository-identity-key' +} from '../../../shared/github/links' +import { githubRepoIdentityKey } from '../../../shared/github/repository-identity-key' 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 { parseLinearIssueUrlIntent, type LinearIssueUrlIntent } from '../../../shared/linear/links' import type { Repo, Worktree } from '../../../shared/types' import { normalizeLinearIdentifier } from './linear-issue-workspace-attachment' import { diff --git a/src/renderer/src/runtime/github-check-details-timeout.ts b/src/renderer/src/runtime/github-check-details-timeout.ts index 1ccaca60d6f..014c0ff43fa 100644 --- a/src/renderer/src/runtime/github-check-details-timeout.ts +++ b/src/renderer/src/runtime/github-check-details-timeout.ts @@ -2,7 +2,7 @@ import { translate } from '@/i18n/i18n' import { GITHUB_CHECK_DETAILS_TIMEOUT_MESSAGE, isGitHubCheckDetailsTimeout -} from '../../../shared/github-check-details-deadline' +} from '../../../shared/github/check-details-deadline' export const GITHUB_CHECK_DETAILS_TIMEOUT_MS = 30_000 diff --git a/src/renderer/src/runtime/runtime-git-client.ts b/src/renderer/src/runtime/runtime-git-client.ts index 48e608daf4a..ae5d2d2156d 100644 --- a/src/renderer/src/runtime/runtime-git-client.ts +++ b/src/renderer/src/runtime/runtime-git-client.ts @@ -22,7 +22,7 @@ import type { HostedReviewProvider } from '../../../shared/hosted-review' import type { ResolvedSourceControlAiGenerationParams } from '../../../shared/source-control-ai' import { getCommitMessageModelDiscoveryHostKeyForScope } from '../../../shared/commit-message-host-key' import type { GitHistoryOptions, GitHistoryResult } from '../../../shared/git-history' -import { getRepoIdFromWorktreeId, splitWorktreeIdForFilesystem } from '../../../shared/worktree-id' +import { getRepoIdFromWorktreeId, splitWorktreeIdForFilesystem } from '../../../shared/worktree/id' import { callRuntimeRpc, getActiveRuntimeTarget } from './runtime-rpc-client' import { toRuntimeWorktreeSelector } from './runtime-worktree-selector' diff --git a/src/renderer/src/runtime/runtime-linear-client.ts b/src/renderer/src/runtime/runtime-linear-client.ts index 4bf5d5797e2..94d6be6197c 100644 --- a/src/renderer/src/runtime/runtime-linear-client.ts +++ b/src/renderer/src/runtime/runtime-linear-client.ts @@ -29,11 +29,11 @@ import { type TaskSourceContext } from '../../../shared/task-source-context' import { isRuntimeProviderSearchQueryWithinLimit } from './runtime-provider-search-bounds' -import type { LinearIssueAttributeFilter } from '../../../shared/linear-issue-attribute-filter' +import type { LinearIssueAttributeFilter } from '../../../shared/linear/issue-attribute-filter' import { canonicalizeLinearIssueAttributeFilter, isEmptyLinearIssueAttributeFilter -} from '../../../shared/linear-issue-attribute-filter' +} from '../../../shared/linear/issue-attribute-filter' import { LINEAR_ISSUE_ATTRIBUTE_FILTER_RUNTIME_CAPABILITY } from '../../../shared/protocol-version' export type RuntimeLinearSettings = 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 1a9693bdf5f..139312608b2 100644 --- a/src/renderer/src/store/slices/degraded-repo-worktree-validity.ts +++ b/src/renderer/src/store/slices/degraded-repo-worktree-validity.ts @@ -5,7 +5,7 @@ import type { Worktree } from '../../../../shared/types' import { parseWorkspaceKey } from '../../../../shared/workspace-scope' -import { getRepoIdFromWorktreeId } from '../../../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id' type WorktreeValidityCatalog = { repos: readonly Pick[] diff --git a/src/renderer/src/store/slices/github-checks.ts b/src/renderer/src/store/slices/github-checks.ts index 46bf368d757..da36a378ddf 100644 --- a/src/renderer/src/store/slices/github-checks.ts +++ b/src/renderer/src/store/slices/github-checks.ts @@ -1,7 +1,7 @@ import type { AppState } from '../types' import type { PRCheckDetail, GitHubOwnerRepo } from '../../../../shared/types' import { getGitHubPRCacheKey } from './github-cache-key' -import { githubRepoIdentityKey } from '../../../../shared/github-repository-identity-key' +import { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key' import { derivePRCheckStatus } from '../../../../shared/pr-check-status' export function normalizeBranchName(branch: string): string { diff --git a/src/renderer/src/store/slices/github-work-items-query-bounds.ts b/src/renderer/src/store/slices/github-work-items-query-bounds.ts index 0ac2107a8a2..4cd04801973 100644 --- a/src/renderer/src/store/slices/github-work-items-query-bounds.ts +++ b/src/renderer/src/store/slices/github-work-items-query-bounds.ts @@ -2,4 +2,4 @@ export { GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN, GITHUB_WORK_ITEMS_QUERY_MAX_BYTES, isGitHubWorkItemsQueryTooLarge -} from '../../../../shared/github-work-items-query-bounds' +} from '../../../../shared/github/work-items-query-bounds' diff --git a/src/renderer/src/store/slices/github.ts b/src/renderer/src/store/slices/github.ts index 24501bc23e9..6134198b1c1 100644 --- a/src/renderer/src/store/slices/github.ts +++ b/src/renderer/src/store/slices/github.ts @@ -2,8 +2,8 @@ import type { StateCreator } from 'zustand' 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 { githubRepoIdentityKey } from '../../../../shared/github/repository-identity-key' +import { githubProjectIdentityKey } from '../../../../shared/github/project-identity' import type { ClassifiedError, GitHubOwnerRepo, @@ -36,7 +36,7 @@ import type { GitHubProjectRow, GitHubProjectTable, GitHubProjectViewError -} from '../../../../shared/github-project-types' +} from '../../../../shared/github/project-types' import { isGitHubWorkItemsSshRemoteRequiredError, sortWorkItemsByNumber, @@ -58,7 +58,7 @@ import { GITHUB_SEARCH_RESULT_WINDOW_ERROR_PATTERN, isGitHubWorkItemsQueryTooLarge } from './github-work-items-query-bounds' -import { classifyGitHubUnavailable } from '../../../../shared/github-api-availability' +import { classifyGitHubUnavailable } from '../../../../shared/github/api-availability' import { isMacAppDataPath } from '@/lib/passive-macos-app-data-access' import { translate } from '@/i18n/i18n' import { @@ -74,7 +74,7 @@ import { getTaskSourceRuntimeSettings, type TaskSourceContext } from '../../../../shared/task-source-context' -import { normalizeGitHubPRForBranchOutcome } from '../../../../shared/github-pr-for-branch-outcome' +import { normalizeGitHubPRForBranchOutcome } from '../../../../shared/github/pull-request-for-branch-outcome' import { restoreReactionOnSubject, setReactionOnSubject } from '@/lib/pr-comment-reactions' import { withGitHubCheckDetailsTimeout } from '@/runtime/github-check-details-timeout' import { getGitHubRepoLookupIndex } from './github-repo-lookup-index' @@ -2787,9 +2787,7 @@ export const createGitHubSlice: StateCreator = (s ...(errorForCache ? { error: - errorUnchanged && previousError !== undefined - ? previousError - : errorForCache + errorUnchanged && previousError !== undefined ? previousError : errorForCache } : {}), ...(nextFellBack ? { issueSourceFellBack: true } : {}) diff --git a/src/renderer/src/store/slices/linear.ts b/src/renderer/src/store/slices/linear.ts index 63991d49148..614020debd5 100644 --- a/src/renderer/src/store/slices/linear.ts +++ b/src/renderer/src/store/slices/linear.ts @@ -18,7 +18,7 @@ import type { LinearWorkspaceSelection } from '../../../../shared/types' import type { CacheEntry } from './github' -import { clampLinearIssueListLimit } from '../../../../shared/linear-issue-read-limits' +import { clampLinearIssueListLimit } from '../../../../shared/linear/issue-read-limits' import { isIntegrationCredentialDecryptionError } from '../../../../shared/integration-credential-errors' import { clearLinearMetadataCache } from '../../hooks/useIssueMetadata' import { @@ -53,7 +53,7 @@ import { isEmptyLinearIssueAttributeFilter, linearIssueAttributeFilterSignature, type LinearIssueAttributeFilter -} from '../../../../shared/linear-issue-attribute-filter' +} from '../../../../shared/linear/issue-attribute-filter' const CACHE_TTL = 60_000 // 60s — same as GitHub work-items revalidation TTL const TEAM_CACHE_TTL = 10 * 60_000 // Teams change rarely and block visible Linear rows. diff --git a/src/renderer/src/store/slices/repos.ts b/src/renderer/src/store/slices/repos.ts index 51df18322ba..059baa6dde5 100644 --- a/src/renderer/src/store/slices/repos.ts +++ b/src/renderer/src/store/slices/repos.ts @@ -47,7 +47,7 @@ import { normalizeRepoBadgeColor } from '../../../../shared/repo-badge-color' import { applyManualRepoOrder, getManualRepoOrder } from '../../../../shared/manual-repo-order' import { getProjectGroupSubtreeIds } from '../../../../shared/project-groups' import { isPathInsideOrEqual } from '../../../../shared/cross-platform-path' -import { getRepoIdFromWorktreeId } from '../../../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id' import { selectProjectGroupRemovalTargets } from './project-group-removal-targets' import { areValuesEqual, @@ -97,7 +97,7 @@ import { isRemovedRuntimeHostId } from './stale-runtime-host-rows' import { normalizeCustomWorktreeVisibilitySources, normalizeWorktreeVisibilitySourcePreferences -} from '../../../../shared/worktree-visibility-sources' +} from '../../../../shared/worktree/visibility-sources' import { cleanupEphemeralVmRuntimesForDeleted } from '@/lib/ephemeral-vm-runtime-cleanup' import { folderWorkspaceKey, parseWorkspaceKey } from '../../../../shared/workspace-scope' import { formatFolderWorkspaceCreateError } from '../../lib/folder-workspace-path-status' diff --git a/src/renderer/src/store/slices/terminals.ts b/src/renderer/src/store/slices/terminals.ts index c644b6db241..9b65267f895 100644 --- a/src/renderer/src/store/slices/terminals.ts +++ b/src/renderer/src/store/slices/terminals.ts @@ -42,7 +42,7 @@ import { isSameCodexRestartNoticeAccount } from './codex-restart-notice-account- import { getRepoIdFromWorktreeId, splitWorktreeIdForFilesystem -} from '../../../../shared/worktree-id' +} from '../../../../shared/worktree/id' import { isWslUncPath } from '../../../../shared/wsl-paths' import type { ProjectExecutionRuntimeResolution } from '../../../../shared/project-execution-runtime' import type { StartupCommandDelivery } from '../../../../shared/codex-startup-delivery' diff --git a/src/renderer/src/store/slices/worktree-helpers.ts b/src/renderer/src/store/slices/worktree-helpers.ts index 688561865b0..45c64ed8b4c 100644 --- a/src/renderer/src/store/slices/worktree-helpers.ts +++ b/src/renderer/src/store/slices/worktree-helpers.ts @@ -22,7 +22,7 @@ import type { WorkspaceKey } from '../../../../shared/types' import type { TaskSourceContext } from '../../../../shared/task-source-context' -import type { WorktreeForceDeleteReason } from '../../../../shared/worktree-removal' +import type { WorktreeForceDeleteReason } from '../../../../shared/worktree/removal' import type { TerminalGitHubPRLink } from '../../../../shared/terminal-github-pr-link-detector' import type { ExecutionHostId } from '../../../../shared/execution-host' import type { @@ -34,9 +34,9 @@ import type { PendingWorktreeCreation, WorktreeCreationPhase } from '@/lib/pending-worktree-creation' -import { getRepoIdFromWorktreeId } from '../../../../shared/worktree-id' +import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id' import type { AppState } from '../types' -export { getRepoIdFromWorktreeId } from '../../../../shared/worktree-id' +export { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id' export type WorktreeDeleteState = { isDeleting: boolean diff --git a/src/renderer/src/store/slices/worktrees.ts b/src/renderer/src/store/slices/worktrees.ts index 85f85307b2b..497e8d3be6b 100644 --- a/src/renderer/src/store/slices/worktrees.ts +++ b/src/renderer/src/store/slices/worktrees.ts @@ -28,7 +28,7 @@ import { type WorktreeSlice } from './worktree-helpers' import { projectWorktreeTabModelReconciliation } from './tabs' -import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree-id' +import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree/id' import { remapClosedTerminalTabSnapshotCwds, type ClosedTerminalTabSnapshot @@ -113,7 +113,7 @@ import { classifyWorktreeForceDeleteReason, getLockedWorktreeRemovalReason, isLockedWorktreeRemovalError -} from '../../../../shared/worktree-removal' +} from '../../../../shared/worktree/removal' import { FolderWorkspaceActivityPersistence } from './folder-workspace-activity-persistence' import { createDetectedWorktreeRefreshLeaseRegistry, diff --git a/src/shared/agent-scratch-worktrees.ts b/src/shared/agent-scratch-worktrees.ts index c9add6692b9..d7dbf0dce99 100644 --- a/src/shared/agent-scratch-worktrees.ts +++ b/src/shared/agent-scratch-worktrees.ts @@ -2,7 +2,7 @@ import { normalizeRuntimePathForComparison } from './cross-platform-path' import { createWorktreeVisibilitySourceMatcher, type WorktreeVisibilitySourceMatcher -} from './worktree-visibility-sources' +} from './worktree/visibility-sources' /** Why: agent CLIs reserve these repo-root paths for scratch; broader matches * can hide legitimate user worktrees (#9388). */ diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 07f487b9f8a..b0cf4abffca 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -17,7 +17,7 @@ import { getDefaultTerminalQuickCommands } from './terminal-quick-commands' import type { VoiceSettings } from './speech-types' import { cloneDefaultWorkspaceStatuses } from './workspace-statuses' import { TASK_PROVIDERS } from './task-providers' -import { DEFAULT_WORKTREE_CARD_PROPERTIES } from './worktree-card-properties' +import { DEFAULT_WORKTREE_CARD_PROPERTIES } from './worktree/card-properties' import { getDefaultSourceControlAiSettings } from './source-control-ai' import { DEFAULT_APP_ICON_ID } from './app-icon' import { DEFAULT_OPEN_IN_APPLICATIONS } from './open-in-applications' @@ -44,7 +44,7 @@ export { getWorktreeCardModeUpdates, isDefaultedCompactWorktreeCardProperties, normalizeWorktreeCardProperties -} from './worktree-card-properties' +} from './worktree/card-properties' export const SCHEMA_VERSION = 1 export const DEFAULT_APP_FONT_FAMILY = 'Geist' diff --git a/src/shared/external-worktree-inbox.test.ts b/src/shared/external-worktree-inbox.test.ts index b9a72f62f8c..2c29fe4a117 100644 --- a/src/shared/external-worktree-inbox.test.ts +++ b/src/shared/external-worktree-inbox.test.ts @@ -20,7 +20,7 @@ import { buildKnownOrcaWorkspaceLayouts, EXTERNAL_WORKTREE_VISIBILITY_ROLLOUT_AT, toDetectedWorktree -} from './worktree-ownership' +} from './worktree/ownership' const repo: Repo = { id: 'repo-1', diff --git a/src/shared/folder-workspace-types.ts b/src/shared/folder-workspace-types.ts index 0730afa2140..81e8dcf9f98 100644 --- a/src/shared/folder-workspace-types.ts +++ b/src/shared/folder-workspace-types.ts @@ -6,7 +6,7 @@ import type { WorkspaceCreatorProvenance, WorkspaceLinkedItem, WorkspaceStatus -} from './worktree-types' +} from './worktree/types' export type WorkspaceScope = | { type: 'worktree'; worktreeId: string } diff --git a/src/shared/github-api-availability.test.ts b/src/shared/github/api-availability.test.ts similarity index 98% rename from src/shared/github-api-availability.test.ts rename to src/shared/github/api-availability.test.ts index 6e0ec633ef4..aec7abd4422 100644 --- a/src/shared/github-api-availability.test.ts +++ b/src/shared/github/api-availability.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { classifyGitHubUnavailable, isGitHubUnavailableError } from './github-api-availability' +import { classifyGitHubUnavailable, isGitHubUnavailableError } from './api-availability' describe('classifyGitHubUnavailable', () => { it('classifies HTTP 5xx outages as server_error', () => { diff --git a/src/shared/github-api-availability.ts b/src/shared/github/api-availability.ts similarity index 100% rename from src/shared/github-api-availability.ts rename to src/shared/github/api-availability.ts diff --git a/src/shared/github-auth-types.ts b/src/shared/github/auth-types.ts similarity index 100% rename from src/shared/github-auth-types.ts rename to src/shared/github/auth-types.ts diff --git a/src/shared/github-check-details-deadline.ts b/src/shared/github/check-details-deadline.ts similarity index 100% rename from src/shared/github-check-details-deadline.ts rename to src/shared/github/check-details-deadline.ts diff --git a/src/shared/github-check-types.ts b/src/shared/github/check-types.ts similarity index 100% rename from src/shared/github-check-types.ts rename to src/shared/github/check-types.ts diff --git a/src/shared/github-comment-types.ts b/src/shared/github/comment-types.ts similarity index 97% rename from src/shared/github-comment-types.ts rename to src/shared/github/comment-types.ts index 0fc48af62c2..6f529ded0ef 100644 --- a/src/shared/github-comment-types.ts +++ b/src/shared/github/comment-types.ts @@ -1,4 +1,4 @@ -import type { GitHubRepositoryIdentity } from './github-pr-types' +import type { GitHubRepositoryIdentity } from './pull-request-types' export type GitHubReactionContent = | '+1' diff --git a/src/shared/github-links.ts b/src/shared/github/links.ts similarity index 100% rename from src/shared/github-links.ts rename to src/shared/github/links.ts diff --git a/src/shared/github-project-group-sort.ts b/src/shared/github/project-group-sort.ts similarity index 99% rename from src/shared/github-project-group-sort.ts rename to src/shared/github/project-group-sort.ts index 923b8aacf49..707fbfb35e4 100644 --- a/src/shared/github-project-group-sort.ts +++ b/src/shared/github/project-group-sort.ts @@ -6,7 +6,7 @@ import type { GitHubProjectRow, GitHubProjectSort, GitHubProjectTable -} from './github-project-types' +} from './project-types' export type ProjectGroup = { /** Stable key used for React reconciliation. */ diff --git a/src/shared/github-project-identity.test.ts b/src/shared/github/project-identity.test.ts similarity index 90% rename from src/shared/github-project-identity.test.ts rename to src/shared/github/project-identity.test.ts index 40c96a316dd..cc340c2be65 100644 --- a/src/shared/github-project-identity.test.ts +++ b/src/shared/github/project-identity.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { githubProjectHost, githubProjectIdentityKey } from './github-project-identity' +import { githubProjectHost, githubProjectIdentityKey } from './project-identity' describe('GitHub project identity', () => { it('keeps legacy github.com keys while isolating Enterprise hosts and ports', () => { diff --git a/src/shared/github-project-identity.ts b/src/shared/github/project-identity.ts similarity index 92% rename from src/shared/github-project-identity.ts rename to src/shared/github/project-identity.ts index dbe7c478504..5de8dcf5517 100644 --- a/src/shared/github-project-identity.ts +++ b/src/shared/github/project-identity.ts @@ -1,4 +1,4 @@ -import { isDefaultGitHubHost } from './github-repository-identity-key' +import { isDefaultGitHubHost } from './repository-identity-key' export type GitHubProjectIdentity = { owner: string diff --git a/src/shared/github-project-ref-input.test.ts b/src/shared/github/project-ref-input.test.ts similarity index 97% rename from src/shared/github-project-ref-input.test.ts rename to src/shared/github/project-ref-input.test.ts index cd0ece5f5e8..08f71c3e44a 100644 --- a/src/shared/github-project-ref-input.test.ts +++ b/src/shared/github/project-ref-input.test.ts @@ -5,7 +5,7 @@ import { getGitHubProjectRefInputByteLength, hasBoundedGitHubProjectRefInputText, isGitHubProjectRefInputTooLarge -} from './github-project-ref-input' +} from './project-ref-input' describe('GitHub project reference input limits', () => { it('allows normal project references below the byte budget', () => { diff --git a/src/shared/github-project-ref-input.ts b/src/shared/github/project-ref-input.ts similarity index 95% rename from src/shared/github-project-ref-input.ts rename to src/shared/github/project-ref-input.ts index 5b0ee6c8d7e..8295617a0d1 100644 --- a/src/shared/github-project-ref-input.ts +++ b/src/shared/github/project-ref-input.ts @@ -1,4 +1,4 @@ -import { getClipboardTextByteLength, isClipboardTextByteLengthOverLimit } from './clipboard-text' +import { getClipboardTextByteLength, isClipboardTextByteLengthOverLimit } from '../clipboard-text' export const GITHUB_PROJECT_REF_INPUT_MAX_BYTES = 2 * 1024 export const GITHUB_PROJECT_REF_INPUT_TOO_LARGE_ERROR = 'Project reference is too large to resolve.' diff --git a/src/shared/github-project-types.ts b/src/shared/github/project-types.ts similarity index 99% rename from src/shared/github-project-types.ts rename to src/shared/github/project-types.ts index 9175c4b27b1..6560e410474 100644 --- a/src/shared/github-project-types.ts +++ b/src/shared/github/project-types.ts @@ -9,7 +9,7 @@ import type { GitHubIssueUpdate, GitHubWorkItemDetails, PRComment -} from './types' +} from '../types' export type GitHubProjectViewLayout = 'TABLE_LAYOUT' | 'BOARD_LAYOUT' | 'ROADMAP_LAYOUT' export type GitHubProjectOwnerType = 'organization' | 'user' diff --git a/src/shared/github-pr-auto-merge-availability.test.ts b/src/shared/github/pull-request-auto-merge-availability.test.ts similarity index 98% rename from src/shared/github-pr-auto-merge-availability.test.ts rename to src/shared/github/pull-request-auto-merge-availability.test.ts index 2e22a3bd3f2..8cfe706476e 100644 --- a/src/shared/github-pr-auto-merge-availability.test.ts +++ b/src/shared/github/pull-request-auto-merge-availability.test.ts @@ -3,7 +3,7 @@ import { canEnableGitHubPRAutoMerge, canShowGitHubPRAutoMergeControl, type GitHubPRAutoMergeAvailabilityInput -} from './github-pr-auto-merge-availability' +} from './pull-request-auto-merge-availability' function pr( overrides: Partial = {} diff --git a/src/shared/github-pr-auto-merge-availability.ts b/src/shared/github/pull-request-auto-merge-availability.ts similarity index 99% rename from src/shared/github-pr-auto-merge-availability.ts rename to src/shared/github/pull-request-auto-merge-availability.ts index 67b6e25ad72..90500d1618e 100644 --- a/src/shared/github-pr-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 '../types' export type GitHubPRAutoMergeAvailabilityInput = { state: PRState | 'open' | 'closed' | 'merged' | 'draft' diff --git a/src/shared/github-pr-for-branch-outcome.test.ts b/src/shared/github/pull-request-for-branch-outcome.test.ts similarity index 88% rename from src/shared/github-pr-for-branch-outcome.test.ts rename to src/shared/github/pull-request-for-branch-outcome.test.ts index 3eaec4a99c7..2eff1b7ae33 100644 --- a/src/shared/github-pr-for-branch-outcome.test.ts +++ b/src/shared/github/pull-request-for-branch-outcome.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { PRInfo, PRRefreshOutcome } from './types' -import { normalizeGitHubPRForBranchOutcome } from './github-pr-for-branch-outcome' +import type { PRInfo, PRRefreshOutcome } from '../types' +import { normalizeGitHubPRForBranchOutcome } from './pull-request-for-branch-outcome' const PR = { number: 42, diff --git a/src/shared/github-pr-for-branch-outcome.ts b/src/shared/github/pull-request-for-branch-outcome.ts similarity index 89% rename from src/shared/github-pr-for-branch-outcome.ts rename to src/shared/github/pull-request-for-branch-outcome.ts index 90b5661f884..b6cdc180609 100644 --- a/src/shared/github-pr-for-branch-outcome.ts +++ b/src/shared/github/pull-request-for-branch-outcome.ts @@ -1,4 +1,4 @@ -import type { PRInfo, PRRefreshOutcome } from './types' +import type { PRInfo, PRRefreshOutcome } from '../types' export type GitHubPRForBranchResponse = PRRefreshOutcome | PRInfo | null diff --git a/src/shared/github-pr-merge-methods.test.ts b/src/shared/github/pull-request-merge-methods.test.ts similarity index 97% rename from src/shared/github-pr-merge-methods.test.ts rename to src/shared/github/pull-request-merge-methods.test.ts index f4705d6b00f..550bb00b0a5 100644 --- a/src/shared/github-pr-merge-methods.test.ts +++ b/src/shared/github/pull-request-merge-methods.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { normalizeGitHubPRMergeMethodSettings, resolveGitHubPRMergeMethods -} from './github-pr-merge-methods' +} from './pull-request-merge-methods' describe('GitHub PR merge methods', () => { it('keeps the historical squash-first fallback when repository metadata is missing', () => { diff --git a/src/shared/github-pr-merge-methods.ts b/src/shared/github/pull-request-merge-methods.ts similarity index 99% rename from src/shared/github-pr-merge-methods.ts rename to src/shared/github/pull-request-merge-methods.ts index a9372b461f6..2072706a41b 100644 --- a/src/shared/github-pr-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 '../types' export const GITHUB_PR_MERGE_METHODS = ['squash', 'merge', 'rebase'] as const diff --git a/src/shared/github-pr-refresh-types.ts b/src/shared/github/pull-request-refresh-types.ts similarity index 98% rename from src/shared/github-pr-refresh-types.ts rename to src/shared/github/pull-request-refresh-types.ts index 35fdd0f27d7..261773b327f 100644 --- a/src/shared/github-pr-refresh-types.ts +++ b/src/shared/github/pull-request-refresh-types.ts @@ -1,5 +1,5 @@ -import type { RepoKind } from './repo-types' -import type { CheckStatus, PRInfo, PRMergeableState, PRState } from './github-pr-types' +import type { RepoKind } from '../repo-types' +import type { CheckStatus, PRInfo, PRMergeableState, PRState } from './pull-request-types' /** * Discriminates a classified GitHub PR-refresh failure. The renderer maps these diff --git a/src/shared/github-pr-types.ts b/src/shared/github/pull-request-types.ts similarity index 100% rename from src/shared/github-pr-types.ts rename to src/shared/github/pull-request-types.ts diff --git a/src/shared/github-rate-limit-types.ts b/src/shared/github/rate-limit-types.ts similarity index 100% rename from src/shared/github-rate-limit-types.ts rename to src/shared/github/rate-limit-types.ts diff --git a/src/shared/github-repository-identity-key.test.ts b/src/shared/github/repository-identity-key.test.ts similarity index 84% rename from src/shared/github-repository-identity-key.test.ts rename to src/shared/github/repository-identity-key.test.ts index ef51d276889..9e2726ffb89 100644 --- a/src/shared/github-repository-identity-key.test.ts +++ b/src/shared/github/repository-identity-key.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { githubRepoIdentityKey, isDefaultGitHubHost } from './github-repository-identity-key' +import { githubRepoIdentityKey, isDefaultGitHubHost } from './repository-identity-key' describe('GitHub repository identity', () => { it('normalizes case and harmless surrounding whitespace without merging GHES hosts', () => { diff --git a/src/shared/github-repository-identity-key.ts b/src/shared/github/repository-identity-key.ts similarity index 100% rename from src/shared/github-repository-identity-key.ts rename to src/shared/github/repository-identity-key.ts diff --git a/src/shared/github-work-item-types.ts b/src/shared/github/work-item-types.ts similarity index 96% rename from src/shared/github-work-item-types.ts rename to src/shared/github/work-item-types.ts index 6b9cec763e0..437a1af20f0 100644 --- a/src/shared/github-work-item-types.ts +++ b/src/shared/github/work-item-types.ts @@ -1,6 +1,6 @@ -import type { ClassifiedError } from './classified-error' -import type { PRCheckDetail } from './github-check-types' -import type { GitHubIssueTimelineItem, PRComment } from './github-comment-types' +import type { ClassifiedError } from '../classified-error' +import type { PRCheckDetail } from './check-types' +import type { GitHubIssueTimelineItem, PRComment } from './comment-types' import type { GitHubAssignableUser, GitHubOwnerRepo, @@ -11,7 +11,7 @@ import type { PRMergeableState, PRReviewDecision, ProviderCheckSummary -} from './github-pr-types' +} from './pull-request-types' export type GitHubWorkItem = { id: string diff --git a/src/shared/github-work-items-query-bounds.test.ts b/src/shared/github/work-items-query-bounds.test.ts similarity index 95% rename from src/shared/github-work-items-query-bounds.test.ts rename to src/shared/github/work-items-query-bounds.test.ts index 54839d873c7..9b8bea74b4e 100644 --- a/src/shared/github-work-items-query-bounds.test.ts +++ b/src/shared/github/work-items-query-bounds.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest' import { GITHUB_WORK_ITEMS_QUERY_MAX_BYTES, isGitHubWorkItemsQueryTooLarge -} from './github-work-items-query-bounds' +} from './work-items-query-bounds' describe('shared GitHub work item query bounds', () => { it('allows normal GitHub search syntax', () => { diff --git a/src/shared/github-work-items-query-bounds.ts b/src/shared/github/work-items-query-bounds.ts similarity index 87% rename from src/shared/github-work-items-query-bounds.ts rename to src/shared/github/work-items-query-bounds.ts index 85616688a47..be09c66e624 100644 --- a/src/shared/github-work-items-query-bounds.ts +++ b/src/shared/github/work-items-query-bounds.ts @@ -1,4 +1,4 @@ -import { isClipboardTextByteLengthOverLimit } from './clipboard-text' +import { isClipboardTextByteLengthOverLimit } from '../clipboard-text' export const GITHUB_WORK_ITEMS_QUERY_MAX_BYTES = 8 * 1024 diff --git a/src/shared/global-settings-types.ts b/src/shared/global-settings-types.ts index 71abe86bb46..eb81db14e01 100644 --- a/src/shared/global-settings-types.ts +++ b/src/shared/global-settings-types.ts @@ -1,5 +1,5 @@ import type { ExecutionHostId } from './execution-host' -import type { GitHubProjectSettings } from './github-project-types' +import type { GitHubProjectSettings } from './github/project-types' import type { VoiceSettings } from './speech-types' import type { GitLabProjectSettings } from './gitlab-types' import type { TaskProvider } from './task-providers' @@ -35,7 +35,7 @@ import type { SourceControlViewMode, TaskViewPresetId } from './ui-chrome-types' -import type { SetupScriptLaunchMode } from './worktree-launch-types' +import type { SetupScriptLaunchMode } from './worktree/launch-types' export type GlobalSettings = { workspaceDir: string diff --git a/src/shared/issue-link-input.ts b/src/shared/issue-link-input.ts index 37d98f55072..2f2890a633f 100644 --- a/src/shared/issue-link-input.ts +++ b/src/shared/issue-link-input.ts @@ -1,5 +1,5 @@ -import { parseGitHubIssueOrPRLink } from './github-links' -import { parseLinearIssueInput } from './linear-links' +import { parseGitHubIssueOrPRLink } from './github/links' +import { parseLinearIssueInput } from './linear/links' import type { WorkspaceSourceProvider } from './new-workspace/workspace-source' // Why: narrows the canonical provider union instead of minting a parallel one, diff --git a/src/shared/linear-agent-access.ts b/src/shared/linear/agent-access.ts similarity index 96% rename from src/shared/linear-agent-access.ts rename to src/shared/linear/agent-access.ts index 69169268767..5e69edc0c55 100644 --- a/src/shared/linear-agent-access.ts +++ b/src/shared/linear/agent-access.ts @@ -92,15 +92,15 @@ export type { LinearWriteIssueRef, LinearIssueTaskUpdateResult, LinearSaveIssueResult -} from './linear-agent-result-types' -export type { LinearIssueActivityEntry, LinearIssueActivityValue } from './linear-issue-activity' -export type { LinearInlineMedia } from './linear-inline-media' -export type { LinearMcpIssueListRequest, LinearMcpIssueListResult } from './linear-mcp-issue-list' +} from './agent-result-types' +export type { LinearIssueActivityEntry, LinearIssueActivityValue } from './issue-activity' +export type { LinearInlineMedia } from './inline-media' +export type { LinearMcpIssueListRequest, LinearMcpIssueListResult } from './mcp-issue-list' export type { LinearIssueRelationship, LinearIssueRelationWriteRequest, LinearIssueRelationWriteResult -} from './linear-issue-relation-write' +} from './issue-relation-write' export type LinearWriteTargetRequest = { input?: string diff --git a/src/shared/linear-agent-result-types.ts b/src/shared/linear/agent-result-types.ts similarity index 97% rename from src/shared/linear-agent-result-types.ts rename to src/shared/linear/agent-result-types.ts index ae04c842d4d..d8b0c016f12 100644 --- a/src/shared/linear-agent-result-types.ts +++ b/src/shared/linear/agent-result-types.ts @@ -4,9 +4,9 @@ import type { LinearIssueInclude, LinearIssueListFilter, LinearIssueTaskUpdateRequest -} from './linear-agent-access' -import type { LinearInlineMedia } from './linear-inline-media' -import type { LinearIssueActivityEntry } from './linear-issue-activity' +} from './agent-access' +import type { LinearInlineMedia } from './inline-media' +import type { LinearIssueActivityEntry } from './issue-activity' export type LinearIssueSummary = { id: string diff --git a/src/shared/linear-inline-media.test.ts b/src/shared/linear/inline-media.test.ts similarity index 95% rename from src/shared/linear-inline-media.test.ts rename to src/shared/linear/inline-media.test.ts index 0e59e906f3d..6e633d1e8bf 100644 --- a/src/shared/linear-inline-media.test.ts +++ b/src/shared/linear/inline-media.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { extractLinearInlineMedia } from './linear-inline-media' +import { extractLinearInlineMedia } from './inline-media' describe('Linear inline media extraction', () => { it('extracts markdown and HTML media URLs with source metadata', () => { diff --git a/src/shared/linear-inline-media.ts b/src/shared/linear/inline-media.ts similarity index 100% rename from src/shared/linear-inline-media.ts rename to src/shared/linear/inline-media.ts diff --git a/src/shared/linear-issue-activity.ts b/src/shared/linear/issue-activity.ts similarity index 89% rename from src/shared/linear-issue-activity.ts rename to src/shared/linear/issue-activity.ts index 1a9ff4d675c..13e97e52b56 100644 --- a/src/shared/linear-issue-activity.ts +++ b/src/shared/linear/issue-activity.ts @@ -1,4 +1,4 @@ -import type { LinearNamedEntity, LinearUserSummary } from './linear-agent-result-types' +import type { LinearNamedEntity, LinearUserSummary } from './agent-result-types' export type LinearIssueActivityActor = LinearUserSummary & { kind: 'user' | 'bot' | 'system' diff --git a/src/shared/linear-issue-attribute-filter.test.ts b/src/shared/linear/issue-attribute-filter.test.ts similarity index 99% rename from src/shared/linear-issue-attribute-filter.test.ts rename to src/shared/linear/issue-attribute-filter.test.ts index 57c0ecba441..f0d5b1f952b 100644 --- a/src/shared/linear-issue-attribute-filter.test.ts +++ b/src/shared/linear/issue-attribute-filter.test.ts @@ -7,7 +7,7 @@ import { linearIssueAttributeFilterSignature, parseLinearIssueAttributeFilter, type LinearIssueAttributeFilter -} from './linear-issue-attribute-filter' +} from './issue-attribute-filter' const sample: LinearIssueAttributeFilter = { stateIds: ['b', 'a', 'a', ' c '], diff --git a/src/shared/linear-issue-attribute-filter.ts b/src/shared/linear/issue-attribute-filter.ts similarity index 100% rename from src/shared/linear-issue-attribute-filter.ts rename to src/shared/linear/issue-attribute-filter.ts diff --git a/src/shared/linear-issue-read-limits.ts b/src/shared/linear/issue-read-limits.ts similarity index 100% rename from src/shared/linear-issue-read-limits.ts rename to src/shared/linear/issue-read-limits.ts diff --git a/src/shared/linear-issue-relation-write.ts b/src/shared/linear/issue-relation-write.ts similarity index 86% rename from src/shared/linear-issue-relation-write.ts rename to src/shared/linear/issue-relation-write.ts index 9bdb9d1aece..c95b66173f4 100644 --- a/src/shared/linear-issue-relation-write.ts +++ b/src/shared/linear/issue-relation-write.ts @@ -1,9 +1,9 @@ -import type { LinearCurrentIssueContextHints } from './linear-agent-access' +import type { LinearCurrentIssueContextHints } from './agent-access' import type { LinearIssueRelation, LinearIssueSummary, LinearWriteIssueRef -} from './linear-agent-result-types' +} from './agent-result-types' export type LinearIssueRelationship = 'blocks' | 'blockedBy' | 'relatedTo' | 'duplicateOf' diff --git a/src/shared/linear-issue-types.ts b/src/shared/linear/issue-types.ts similarity index 92% rename from src/shared/linear-issue-types.ts rename to src/shared/linear/issue-types.ts index 9f82d3b88e6..db1ba1a068d 100644 --- a/src/shared/linear-issue-types.ts +++ b/src/shared/linear/issue-types.ts @@ -1,4 +1,4 @@ -import type { LinearProjectSummary } from './linear-project-types' +import type { LinearProjectSummary } from './project-types' export type LinearIssue = { id: string diff --git a/src/shared/linear-issue-view-resume-state.test.ts b/src/shared/linear/issue-view-resume-state.test.ts similarity index 99% rename from src/shared/linear-issue-view-resume-state.test.ts rename to src/shared/linear/issue-view-resume-state.test.ts index 806977e28be..645d3a6ac6a 100644 --- a/src/shared/linear-issue-view-resume-state.test.ts +++ b/src/shared/linear/issue-view-resume-state.test.ts @@ -3,7 +3,7 @@ import { emptyLinearIssueAttributeFilter, LINEAR_ISSUE_ATTRIBUTE_FILTER_ID_MAX_LENGTH, type LinearIssueAttributeFilter -} from './linear-issue-attribute-filter' +} from './issue-attribute-filter' import { defaultLinearIssueViewResumeState, LINEAR_DISPLAY_PROPERTIES, @@ -13,7 +13,7 @@ import { serializeLinearIssueViewResumeState, setLinearWorkspaceIssueFilter, type LinearIssueViewResumeState -} from './linear-issue-view-resume-state' +} from './issue-view-resume-state' function filter(overrides: Partial = {}): LinearIssueAttributeFilter { return { ...emptyLinearIssueAttributeFilter(), ...overrides } diff --git a/src/shared/linear-issue-view-resume-state.ts b/src/shared/linear/issue-view-resume-state.ts similarity index 99% rename from src/shared/linear-issue-view-resume-state.ts rename to src/shared/linear/issue-view-resume-state.ts index 82c03f87c2e..5330b5546ab 100644 --- a/src/shared/linear-issue-view-resume-state.ts +++ b/src/shared/linear/issue-view-resume-state.ts @@ -9,7 +9,7 @@ import { parseLinearIssueAttributeFilter, LINEAR_ISSUE_ATTRIBUTE_FILTER_ID_MAX_LENGTH, type LinearIssueAttributeFilter -} from './linear-issue-attribute-filter' +} from './issue-attribute-filter' export const LINEAR_VIEW_MODES = ['list', 'board'] as const export const LINEAR_GROUP_BY_OPTIONS = ['none', 'status', 'assignee', 'priority', 'team'] as const diff --git a/src/shared/linear-links.test.ts b/src/shared/linear/links.test.ts similarity index 99% rename from src/shared/linear-links.test.ts rename to src/shared/linear/links.test.ts index 24ac962df6e..48005d93d06 100644 --- a/src/shared/linear-links.test.ts +++ b/src/shared/linear/links.test.ts @@ -8,7 +8,7 @@ import { buildLinearWorkspaceApiSettingsUrl, getLinearOrganizationUrlKeyFromIssueUrl, parseLinearIssueInput -} from './linear-links' +} from './links' describe('linear links', () => { it('builds team URLs from workspace and team keys', () => { diff --git a/src/shared/linear-links.ts b/src/shared/linear/links.ts similarity index 99% rename from src/shared/linear-links.ts rename to src/shared/linear/links.ts index bcbb7180e36..ce457f0a419 100644 --- a/src/shared/linear-links.ts +++ b/src/shared/linear/links.ts @@ -1,4 +1,4 @@ -import type { LinearConnectionStatus, LinearIssue, LinearWorkspace } from './types' +import type { LinearConnectionStatus, LinearIssue, LinearWorkspace } from '../types' export function buildLinearTeamUrl(args: { organizationUrlKey?: string | null diff --git a/src/shared/linear-mcp-issue-list.ts b/src/shared/linear/mcp-issue-list.ts similarity index 90% rename from src/shared/linear-mcp-issue-list.ts rename to src/shared/linear/mcp-issue-list.ts index 6955eef16ab..29a21ddb323 100644 --- a/src/shared/linear-mcp-issue-list.ts +++ b/src/shared/linear/mcp-issue-list.ts @@ -1,5 +1,5 @@ -import type { LinearErrorCode } from './linear-agent-access' -import type { LinearIssueSummary, LinearWorkspaceCandidate } from './linear-agent-result-types' +import type { LinearErrorCode } from './agent-access' +import type { LinearIssueSummary, LinearWorkspaceCandidate } from './agent-result-types' export type LinearMcpIssueListRequest = { team?: string diff --git a/src/shared/linear-project-list-format.ts b/src/shared/linear/project-list-format.ts similarity index 94% rename from src/shared/linear-project-list-format.ts rename to src/shared/linear/project-list-format.ts index f6cd3cd0903..b739c788e5c 100644 --- a/src/shared/linear-project-list-format.ts +++ b/src/shared/linear/project-list-format.ts @@ -1,4 +1,4 @@ -import type { LinearProjectListResult } from './linear-agent-result-types' +import type { LinearProjectListResult } from './agent-result-types' // Why: non-JSON project output aligns ids with the existing compact Linear tables. const LINEAR_PROJECT_NAME_COLUMN_WIDTH = 28 diff --git a/src/shared/linear-project-types.ts b/src/shared/linear/project-types.ts similarity index 100% rename from src/shared/linear-project-types.ts rename to src/shared/linear/project-types.ts diff --git a/src/shared/linear-uuid.ts b/src/shared/linear/uuid.ts similarity index 100% rename from src/shared/linear-uuid.ts rename to src/shared/linear/uuid.ts diff --git a/src/shared/linear-workspace-types.ts b/src/shared/linear/workspace-types.ts similarity index 100% rename from src/shared/linear-workspace-types.ts rename to src/shared/linear/workspace-types.ts diff --git a/src/shared/new-workspace/github-links.ts b/src/shared/new-workspace/github-links.ts index dc6cbd050ab..a838c4ec2f7 100644 --- a/src/shared/new-workspace/github-links.ts +++ b/src/shared/new-workspace/github-links.ts @@ -2,10 +2,10 @@ import { type GitHubIssueOrPRLink, parseGitHubIssueOrPRLink, parseGitHubIssueOrPRNumber -} from '../github-links' +} from '../github/links' import { isWorkItemLinkQueryTooLarge } from './work-item-link-query-bounds' -export * from '../github-links' +export * from '../github/links' const HTTP_URL_PREFIX_RE = /^https?:\/\//i diff --git a/src/shared/new-workspace/smart-workspace-linear-intent.ts b/src/shared/new-workspace/smart-workspace-linear-intent.ts index 0995cdd9cb9..ec77fcb2fbd 100644 --- a/src/shared/new-workspace/smart-workspace-linear-intent.ts +++ b/src/shared/new-workspace/smart-workspace-linear-intent.ts @@ -4,7 +4,7 @@ import { parseLinearIssueUrlIntent, type LinearIssueUrlIntent, type ParsedLinearIssueInput -} from '../linear-links' +} from '../linear/links' import { isSmartWorkspaceSourceQueryWithinLimit } from './smart-workspace-source-query' type SmartWorkspaceLinearMode = 'smart' | 'linear' | (string & {}) diff --git a/src/shared/new-workspace/workspace-source.ts b/src/shared/new-workspace/workspace-source.ts index 69b9e23a186..db15573d9ab 100644 --- a/src/shared/new-workspace/workspace-source.ts +++ b/src/shared/new-workspace/workspace-source.ts @@ -1,4 +1,4 @@ -import { getLinearOrganizationUrlKeyFromIssueUrl } from '../linear-links' +import { getLinearOrganizationUrlKeyFromIssueUrl } from '../linear/links' import type { FolderWorkspaceLinkedTask, JiraIssue, LinearIssue } from '../types' import { getLinkedWorkItemSuggestedName, diff --git a/src/shared/persisted-state-types.ts b/src/shared/persisted-state-types.ts index 76ec8824798..da8757cae28 100644 --- a/src/shared/persisted-state-types.ts +++ b/src/shared/persisted-state-types.ts @@ -12,15 +12,15 @@ import type { CodexResetCreditAttemptLedger } from './codex-reset-credit-attempt import type { DiffComment } from './diff-comment-types' import type { FolderWorkspace, WorkspaceKey } from './folder-workspace-types' import type { GlobalSettings } from './global-settings-types' -import type { IssueInfo, PRInfo } from './github-pr-types' +import type { IssueInfo, PRInfo } from './github/pull-request-types' import type { OnboardingState } from './onboarding-state-types' import type { PersistedUIState } from './persisted-ui-state-types' import type { ProjectGroup } from './project-group-types' import type { Project, ProjectHostSetup } from './project-types' import type { Repo } from './repo-types' -import type { SparsePreset } from './worktree-create-types' -import type { WorkspaceLineage, WorktreeLineage } from './worktree-lineage-types' -import type { WorktreeMeta } from './worktree-meta-types' +import type { SparsePreset } from './worktree/create-types' +import type { WorkspaceLineage, WorktreeLineage } from './worktree/lineage-types' +import type { WorktreeMeta } from './worktree/meta-types' import type { WorkspaceSessionState } from './workspace-session-state-types' export type LegacyPaneKeyAliasEntry = { diff --git a/src/shared/persisted-ui-state-types.ts b/src/shared/persisted-ui-state-types.ts index c8650c6df83..1db7f8e5dfc 100644 --- a/src/shared/persisted-ui-state-types.ts +++ b/src/shared/persisted-ui-state-types.ts @@ -21,7 +21,7 @@ import type { WorkspaceHostScope, WorktreeCardProperty } from './ui-chrome-types' -import type { WorkspaceStatusDefinition } from './worktree-types' +import type { WorkspaceStatusDefinition } from './worktree/types' export type PersistedUIState = { lastActiveRepoId: string | null diff --git a/src/shared/project-host-setup-projection.ts b/src/shared/project-host-setup-projection.ts index 83ba100f30d..1db66b2d2c9 100644 --- a/src/shared/project-host-setup-projection.ts +++ b/src/shared/project-host-setup-projection.ts @@ -1,6 +1,6 @@ import { getRepoExecutionHostId } from './execution-host' import { normalizeGitHubRemoteHost } from './git-remote-host-alias' -import { githubRepoIdentityKey, isDefaultGitHubHost } from './github-repository-identity-key' +import { githubRepoIdentityKey, isDefaultGitHubHost } from './github/repository-identity-key' import type { Project, ProjectHostSetup, diff --git a/src/shared/remote-workspace-session-projection.ts b/src/shared/remote-workspace-session-projection.ts index 647b577dba6..dc9a4019a8b 100644 --- a/src/shared/remote-workspace-session-projection.ts +++ b/src/shared/remote-workspace-session-projection.ts @@ -1,7 +1,7 @@ import { getDefaultWorkspaceSession } from './constants' import type { RemoteWorkspaceSession, RemoteWorkspaceTerminalTab } from './remote-workspace-types' import type { TerminalTab, WorkspaceSessionState } from './types' -import { splitWorktreeId } from './worktree-id' +import { splitWorktreeId } from './worktree/id' type ExportOptions = { isTargetWorktree: (worktreeId: string) => boolean diff --git a/src/shared/repo-types.ts b/src/shared/repo-types.ts index 99cdd957ad0..a6980b45d54 100644 --- a/src/shared/repo-types.ts +++ b/src/shared/repo-types.ts @@ -1,5 +1,5 @@ import type { RepoIcon } from './repo-icon' -import type { GitHubRepositoryIdentity } from './github-pr-types' +import type { GitHubRepositoryIdentity } from './github/pull-request-types' import type { RepoHookSettings } from './orca-yaml-hook-types' import type { ForkSyncMode } from './git-fork-sync' import type { GitRemoteIdentity } from './git-remote-identity' diff --git a/src/shared/task-provider-identity.ts b/src/shared/task-provider-identity.ts index cacd28f3232..5d9dbe8c390 100644 --- a/src/shared/task-provider-identity.ts +++ b/src/shared/task-provider-identity.ts @@ -1,4 +1,4 @@ -import { githubRepoIdentityKey } from './github-repository-identity-key' +import { githubRepoIdentityKey } from './github/repository-identity-key' import type { TaskProvider } from './task-providers' import type { ProjectProviderIdentity } from './types' diff --git a/src/shared/terminal-github-pr-link-detector.ts b/src/shared/terminal-github-pr-link-detector.ts index 7d6204ce393..7cc0f3d44ab 100644 --- a/src/shared/terminal-github-pr-link-detector.ts +++ b/src/shared/terminal-github-pr-link-detector.ts @@ -8,8 +8,8 @@ * path. Both paths must share the carry/dedupe semantics or links split across * chunks would resolve differently per authority mode. */ -import type { RepoSlug } from './github-links' -import { parseGitHubIssueOrPRLink } from './github-links' +import type { RepoSlug } from './github/links' +import { parseGitHubIssueOrPRLink } from './github/links' const GITHUB_PR_PATH_MARKER = '/pull/' const TERMINAL_SGR_PATTERN = /\x1b\[[0-?]*[ -/]*m/g diff --git a/src/shared/terminal-startup-cwd.ts b/src/shared/terminal-startup-cwd.ts index a6b3f935e1d..8344abf07cc 100644 --- a/src/shared/terminal-startup-cwd.ts +++ b/src/shared/terminal-startup-cwd.ts @@ -1,7 +1,7 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from './constants' import { resolveRuntimePath } from './cross-platform-path' import { parseWorkspaceKey } from './workspace-scope' -import { splitWorktreeIdForFilesystem } from './worktree-id' +import { splitWorktreeIdForFilesystem } from './worktree/id' export type TerminalStartupCwdMissingDirFallback = { // Why: only local callers can probe the filesystem — SSH/remote worktree diff --git a/src/shared/types.ts b/src/shared/types.ts index c3ef81392d2..b08e93a3be8 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -33,39 +33,39 @@ 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 { 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 { 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 { 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 { 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 { 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 { 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' @@ -132,33 +132,56 @@ export type { DiffComment } from './diff-comment-types' // GitHub -export type { PRState, IssueState, CheckStatus, PRMergeableState } from './github-pr-types' -export type { PRReviewDecision, PRConflictSummary } from './github-pr-types' -export type { GitHubRepositoryIdentity, GitHubOwnerRepo } from './github-pr-types' -export type { GitHubPRMergeMethod, GitHubPRMergeMethodSettings } from './github-pr-types' -export type { GitHubPRStackEntry, GitHubPRStack, PRInfo, IssueInfo } from './github-pr-types' -export type { GitHubViewer, GitHubAssignableUser, ProviderCheckSummary } from './github-pr-types' -export type { GitHubPRReviewSummary, GitHubPRFileViewedState } from './github-pr-types' -export type { GitHubPRFile, GitHubPRFileContents } from './github-pr-types' +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-pr-refresh-types' -export type { PRRefreshOutcome, GitHubPRRefreshReason } from './github-pr-refresh-types' -export type { GitHubPRRefreshEnqueueResult, GitHubPRRefreshAlias } from './github-pr-refresh-types' -export type { GitHubPRRefreshCandidate } from './github-pr-refresh-types' -export type { GitHubPRRefreshSkippedReason, GitHubPRRefreshEvent } from './github-pr-refresh-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 { 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 { 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 { 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' +export type { GitHubRateLimitBucket, GitHubRateLimitSnapshot } from './github/rate-limit-types' +export type { GetRateLimitResult } from './github/rate-limit-types' // GitLab @@ -229,21 +252,21 @@ export type { // 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 { 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 { 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' +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 diff --git a/src/shared/ui-chrome-types.ts b/src/shared/ui-chrome-types.ts index f7a4bd1efdf..80c78d55afa 100644 --- a/src/shared/ui-chrome-types.ts +++ b/src/shared/ui-chrome-types.ts @@ -1,5 +1,5 @@ -import type { LinearConcreteWorkspaceId } from './linear-workspace-types' -import type { LinearCustomViewModel } from './linear-project-types' +import type { LinearConcreteWorkspaceId } from './linear/workspace-types' +import type { LinearCustomViewModel } from './linear/project-types' export type TaskViewPresetId = 'all' | 'issues' | 'review' | 'my-issues' | 'my-prs' | 'prs' diff --git a/src/shared/workspace-session-terminal-buffers.ts b/src/shared/workspace-session-terminal-buffers.ts index 81ab8315c72..b2ec9df45dc 100644 --- a/src/shared/workspace-session-terminal-buffers.ts +++ b/src/shared/workspace-session-terminal-buffers.ts @@ -1,6 +1,6 @@ import type { Repo, WorkspaceSessionState } from './types' import { FLOATING_TERMINAL_WORKTREE_ID } from './constants' -import { getRepoIdFromWorktreeId } from './worktree-id' +import { getRepoIdFromWorktreeId } from './worktree/id' import { TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT } from './terminal-scrollback-limits' import { clampUtf8TextTail, measureUtf8ByteLength } from './utf8-byte-limits' import { parseExecutionHostId } from './execution-host' diff --git a/src/shared/worktree-base-ref-drift-types.ts b/src/shared/worktree/base-ref-drift-types.ts similarity index 100% rename from src/shared/worktree-base-ref-drift-types.ts rename to src/shared/worktree/base-ref-drift-types.ts diff --git a/src/shared/worktree-base-ref.test.ts b/src/shared/worktree/base-ref.test.ts similarity index 97% rename from src/shared/worktree-base-ref.test.ts rename to src/shared/worktree/base-ref.test.ts index 9776be541f5..062d840b1cd 100644 --- a/src/shared/worktree-base-ref.test.ts +++ b/src/shared/worktree/base-ref.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { resolveWorktreeAddBaseRef } from './worktree-base-ref' +import { resolveWorktreeAddBaseRef } from './base-ref' describe('resolveWorktreeAddBaseRef', () => { it('leaves fully qualified refs unchanged', async () => { diff --git a/src/shared/worktree-base-ref.ts b/src/shared/worktree/base-ref.ts similarity index 100% rename from src/shared/worktree-base-ref.ts rename to src/shared/worktree/base-ref.ts diff --git a/src/shared/worktree-card-properties.test.ts b/src/shared/worktree/card-properties.test.ts similarity index 98% rename from src/shared/worktree-card-properties.test.ts rename to src/shared/worktree/card-properties.test.ts index be9973265f3..45bc50e24a9 100644 --- a/src/shared/worktree-card-properties.test.ts +++ b/src/shared/worktree/card-properties.test.ts @@ -7,7 +7,7 @@ import { getWorktreeCardModeUpdates, isDefaultedCompactWorktreeCardProperties, normalizeWorktreeCardProperties -} from './worktree-card-properties' +} from './card-properties' describe('worktree card properties', () => { it('defines Default with inline agents and without branch', () => { diff --git a/src/shared/worktree-card-properties.ts b/src/shared/worktree/card-properties.ts similarity index 99% rename from src/shared/worktree-card-properties.ts rename to src/shared/worktree/card-properties.ts index a11bbdc3c03..240b3ba341e 100644 --- a/src/shared/worktree-card-properties.ts +++ b/src/shared/worktree/card-properties.ts @@ -3,7 +3,7 @@ import type { PersistedUIState, WorktreeCardMode, WorktreeCardProperty -} from './types' +} from '../types' const FIXED_WORKTREE_CARD_PROPERTIES: WorktreeCardProperty[] = ['status', 'unread'] diff --git a/src/shared/worktree-create-types.ts b/src/shared/worktree/create-types.ts similarity index 92% rename from src/shared/worktree-create-types.ts rename to src/shared/worktree/create-types.ts index ff7929332ce..80e865d2c85 100644 --- a/src/shared/worktree-create-types.ts +++ b/src/shared/worktree/create-types.ts @@ -1,8 +1,8 @@ -import type { ExecutionHostId } from './execution-host' -import type { WorkspaceSource } from './workspace-source' -import type { TaskSourceContext } from './task-source-context' -import type { WorkspaceKey } from './folder-workspace-types' -import type { TuiAgent } from './tui-agent' +import type { ExecutionHostId } from '../execution-host' +import type { WorkspaceSource } from '../workspace-source' +import type { TaskSourceContext } from '../task-source-context' +import type { WorkspaceKey } from '../folder-workspace-types' +import type { TuiAgent } from '../tui-agent' import type { AutomationWorkspaceProvenanceRequest, GitPushTarget, @@ -10,22 +10,18 @@ import type { WorkspaceLinkedItem, WorkspaceStatus, Worktree -} from './worktree-types' -import type { - WorkspaceLineage, - WorktreeLineage, - WorktreeLineageWarning -} from './worktree-lineage-types' +} from './types' +import type { WorkspaceLineage, WorktreeLineage, WorktreeLineageWarning } from './lineage-types' import type { WorktreeDefaultTabsLaunch, WorktreeSetupLaunch, WorktreeStartupLaunch -} from './worktree-launch-types' +} from './launch-types' import type { LocalBaseRefRefreshResult, LocalBaseRefUpdateSuggestion, WorktreeBaseStatusEvent -} from './worktree-base-ref-drift-types' +} from './base-ref-drift-types' export type SetupDecision = 'inherit' | 'run' | 'skip' diff --git a/src/shared/worktree-id.test.ts b/src/shared/worktree/id.test.ts similarity index 99% rename from src/shared/worktree-id.test.ts rename to src/shared/worktree/id.test.ts index 99e6b4e49e7..e64d27784f5 100644 --- a/src/shared/worktree-id.test.ts +++ b/src/shared/worktree/id.test.ts @@ -5,7 +5,7 @@ import { getWorktreePathBasenameFromId, splitWorktreeId, splitWorktreeIdForFilesystem -} from './worktree-id' +} from './id' describe('WORKTREE_ID_SEPARATOR', () => { it('is the literal "::" separator', () => { diff --git a/src/shared/worktree-id.ts b/src/shared/worktree/id.ts similarity index 93% rename from src/shared/worktree-id.ts rename to src/shared/worktree/id.ts index 69f01f4b378..d71cbac66eb 100644 --- a/src/shared/worktree-id.ts +++ b/src/shared/worktree/id.ts @@ -1,6 +1,6 @@ -import { WORKTREE_ID_SEPARATOR } from './pty-session-id-format' +import { WORKTREE_ID_SEPARATOR } from '../pty-session-id-format' -export { WORKTREE_ID_SEPARATOR } from './pty-session-id-format' +export { WORKTREE_ID_SEPARATOR } from '../pty-session-id-format' export type ParsedWorktreeId = { repoId: string diff --git a/src/shared/worktree-launch-types.ts b/src/shared/worktree/launch-types.ts similarity index 74% rename from src/shared/worktree-launch-types.ts rename to src/shared/worktree/launch-types.ts index 3cdeeb7abbb..5fc9f18d397 100644 --- a/src/shared/worktree-launch-types.ts +++ b/src/shared/worktree/launch-types.ts @@ -1,9 +1,9 @@ -import type { StartupCommandDelivery } from './codex-startup-delivery' -import type { AgentKind, LaunchSource, RequestKind } from './telemetry-events' -import type { SleepingAgentLaunchConfig } from './agent-session-resume' -import type { SetupRunnerShell } from './setup-runner-command' -import type { OrcaDefaultTabTemplate } from './orca-yaml-hook-types' -import type { TuiAgent } from './tui-agent' +import type { StartupCommandDelivery } from '../codex-startup-delivery' +import type { AgentKind, LaunchSource, RequestKind } from '../telemetry-events' +import type { SleepingAgentLaunchConfig } from '../agent-session-resume' +import type { SetupRunnerShell } from '../setup-runner-command' +import type { OrcaDefaultTabTemplate } from '../orca-yaml-hook-types' +import type { TuiAgent } from '../tui-agent' export type WorktreeSetupLaunch = { runnerScriptPath: string diff --git a/src/shared/worktree-lineage-types.ts b/src/shared/worktree/lineage-types.ts similarity index 95% rename from src/shared/worktree-lineage-types.ts rename to src/shared/worktree/lineage-types.ts index c421b927272..c7f0e3d791e 100644 --- a/src/shared/worktree-lineage-types.ts +++ b/src/shared/worktree/lineage-types.ts @@ -1,4 +1,4 @@ -import type { WorkspaceKey } from './folder-workspace-types' +import type { WorkspaceKey } from '../folder-workspace-types' export type WorktreeLineageOrigin = 'orchestration' | 'cli' | 'manual' export type WorktreeLineageCaptureConfidence = 'explicit' | 'inferred' diff --git a/src/shared/worktree-meta-types.ts b/src/shared/worktree/meta-types.ts similarity index 90% rename from src/shared/worktree-meta-types.ts rename to src/shared/worktree/meta-types.ts index 8d7df1a53fc..54bc96c890d 100644 --- a/src/shared/worktree-meta-types.ts +++ b/src/shared/worktree/meta-types.ts @@ -1,6 +1,6 @@ -import type { ExecutionHostId } from './execution-host' -import type { TaskSourceContext } from './task-source-context' -import type { EphemeralVmCheckoutMode } from './orca-yaml-hook-types' +import type { ExecutionHostId } from '../execution-host' +import type { TaskSourceContext } from '../task-source-context' +import type { EphemeralVmCheckoutMode } from '../orca-yaml-hook-types' import type { AutomationWorkspaceProvenance, CliWorkspaceProvenance, @@ -8,10 +8,10 @@ import type { WorkspaceCreatorProvenance, WorkspaceLinkedItem, WorkspaceStatus -} from './worktree-types' -import type { TuiAgent } from './tui-agent' -import type { OrcaWorkspaceLayout } from './global-settings-types' -import type { DiffComment, MobileDiffReviewState } from './diff-comment-types' +} from './types' +import type { TuiAgent } from '../tui-agent' +import type { OrcaWorkspaceLayout } from '../global-settings-types' +import type { DiffComment, MobileDiffReviewState } from '../diff-comment-types' // ─── Worktree metadata (persisted user-authored fields only) ───────── export type WorktreeMeta = { diff --git a/src/shared/worktree-ownership-worktree-base-path.test.ts b/src/shared/worktree/ownership-worktree-base-path.test.ts similarity index 96% rename from src/shared/worktree-ownership-worktree-base-path.test.ts rename to src/shared/worktree/ownership-worktree-base-path.test.ts index d6af4dab77d..aff3c951285 100644 --- a/src/shared/worktree-ownership-worktree-base-path.test.ts +++ b/src/shared/worktree/ownership-worktree-base-path.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { GlobalSettings, Repo, Worktree } from './types' -import { buildKnownOrcaWorkspaceLayouts, classifyWorktreeOwnership } from './worktree-ownership' +import type { GlobalSettings, Repo, Worktree } from '../types' +import { buildKnownOrcaWorkspaceLayouts, classifyWorktreeOwnership } from './ownership' function makeRepo(overrides: Partial = {}): Repo { return { diff --git a/src/shared/worktree-ownership.test.ts b/src/shared/worktree/ownership.test.ts similarity index 99% rename from src/shared/worktree-ownership.test.ts rename to src/shared/worktree/ownership.test.ts index ce56ef2c0cd..8877a415b53 100644 --- a/src/shared/worktree-ownership.test.ts +++ b/src/shared/worktree/ownership.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import type { GlobalSettings, Repo, Worktree, WorktreeMeta } from './types' -import { createAgentScratchWorktreePathMatcher } from './agent-scratch-worktrees' +import type { GlobalSettings, Repo, Worktree, WorktreeMeta } from '../types' +import { createAgentScratchWorktreePathMatcher } from '../agent-scratch-worktrees' import { applyMetadataFallbackVisibility, buildKnownOrcaWorkspaceLayouts, @@ -10,7 +10,7 @@ import { shouldShowWorktree, toDetectedWorktree, EXTERNAL_WORKTREE_VISIBILITY_ROLLOUT_AT -} from './worktree-ownership' +} from './ownership' const LARGE_WORKSPACE_HISTORY_COUNT = 150_000 diff --git a/src/shared/worktree-ownership.ts b/src/shared/worktree/ownership.ts similarity index 96% rename from src/shared/worktree-ownership.ts rename to src/shared/worktree/ownership.ts index 22f73690bda..7e224d0689f 100644 --- a/src/shared/worktree-ownership.ts +++ b/src/shared/worktree/ownership.ts @@ -5,24 +5,24 @@ import { normalizeRuntimePathSeparators, relativePathInsideRoot, resolveRuntimePath -} from './cross-platform-path' -import { parseWslUncPath } from './wsl-paths' +} from '../cross-platform-path' +import { parseWslUncPath } from '../wsl-paths' import { isAgentScratchWorktreePath, type AgentScratchWorktreePathMatcher -} from './agent-scratch-worktrees' +} from '../agent-scratch-worktrees' import { createWorktreeVisibilitySourceMatcher, effectiveWorktreeSourceVisibility, normalizeCustomWorktreeVisibilitySources, type WorktreeVisibilitySourceMatcher -} from './worktree-visibility-sources' -import { isExplicitlyImportedExternalWorktreePath } from './external-worktree-inbox' +} from './visibility-sources' +import { isExplicitlyImportedExternalWorktreePath } from '../external-worktree-inbox' import { effectiveAgentWorktreeVisibility, effectiveExternalWorktreeVisibility, isLegacyRepoForExternalWorktreeVisibility -} from './external-worktree-visibility' +} from '../external-worktree-visibility' import type { DetectedWorktree, GlobalSettings, @@ -31,14 +31,14 @@ import type { Worktree, WorktreeMeta, WorktreeOwnership -} from './types' +} from '../types' export { effectiveAgentWorktreeVisibility, effectiveExternalWorktreeVisibility, EXTERNAL_WORKTREE_VISIBILITY_ROLLOUT_AT, isLegacyRepoForExternalWorktreeVisibility -} from './external-worktree-visibility' +} from '../external-worktree-visibility' export function buildKnownOrcaWorkspaceLayouts( settings: Pick, diff --git a/src/shared/worktree-removal-fence-error.test.ts b/src/shared/worktree/removal-fence-error.test.ts similarity index 96% rename from src/shared/worktree-removal-fence-error.test.ts rename to src/shared/worktree/removal-fence-error.test.ts index ce9e3d76bb7..407f1baf1f5 100644 --- a/src/shared/worktree-removal-fence-error.test.ts +++ b/src/shared/worktree/removal-fence-error.test.ts @@ -3,7 +3,7 @@ import { TERMINAL_REMOVAL_IN_PROGRESS_MESSAGE, WATCHER_REMOVAL_IN_PROGRESS_MESSAGE, isWorktreeRemovalFenceError -} from './worktree-removal-fence-error' +} from './removal-fence-error' describe('isWorktreeRemovalFenceError', () => { it('recognizes the raw terminal and watcher fence messages', () => { diff --git a/src/shared/worktree-removal-fence-error.ts b/src/shared/worktree/removal-fence-error.ts similarity index 100% rename from src/shared/worktree-removal-fence-error.ts rename to src/shared/worktree/removal-fence-error.ts diff --git a/src/shared/worktree-removal-force-classification.test.ts b/src/shared/worktree/removal-force-classification.test.ts similarity index 99% rename from src/shared/worktree-removal-force-classification.test.ts rename to src/shared/worktree/removal-force-classification.test.ts index b4d95a5727d..73ac6ea77a6 100644 --- a/src/shared/worktree-removal-force-classification.test.ts +++ b/src/shared/worktree/removal-force-classification.test.ts @@ -5,7 +5,7 @@ import { UNSTOPPED_PTY_REMOVAL_PREFIX, WORKTREE_TEARDOWN_FORCE_HINT, WORKTREE_TEARDOWN_TIMEOUT_PREFIX -} from './worktree-removal' +} from './removal' const liveError = `${UNSTOPPED_PTY_REMOVAL_PREFIX} repo-1::/w — still live: term_a. ${WORKTREE_TEARDOWN_FORCE_HINT}` const unverifiableError = `${UNSTOPPED_PTY_REMOVAL_PREFIX} repo-1::/w — could not verify these exited: term_a (daemon socket closed). ${WORKTREE_TEARDOWN_FORCE_HINT}` diff --git a/src/shared/worktree-removal.ts b/src/shared/worktree/removal.ts similarity index 99% rename from src/shared/worktree-removal.ts rename to src/shared/worktree/removal.ts index 8788171ebe0..68d1072b247 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-sort-order-update.test.ts b/src/shared/worktree/sort-order-update.test.ts similarity index 95% rename from src/shared/worktree-sort-order-update.test.ts rename to src/shared/worktree/sort-order-update.test.ts index 4c53ddff56d..6981e851eef 100644 --- a/src/shared/worktree-sort-order-update.test.ts +++ b/src/shared/worktree/sort-order-update.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { planWorktreeSortOrderUpdates } from './worktree-sort-order-update' +import { planWorktreeSortOrderUpdates } from './sort-order-update' describe('planWorktreeSortOrderUpdates', () => { it('skips an order that is already represented by descending ranks', () => { diff --git a/src/shared/worktree-sort-order-update.ts b/src/shared/worktree/sort-order-update.ts similarity index 100% rename from src/shared/worktree-sort-order-update.ts rename to src/shared/worktree/sort-order-update.ts diff --git a/src/shared/worktree-submodule-removal.test.ts b/src/shared/worktree/submodule-removal.test.ts similarity index 93% rename from src/shared/worktree-submodule-removal.test.ts rename to src/shared/worktree/submodule-removal.test.ts index 0c8564b79d3..464fc9e7766 100644 --- a/src/shared/worktree-submodule-removal.test.ts +++ b/src/shared/worktree/submodule-removal.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { isSubmoduleWorktreeRemovalRefusal } from './worktree-submodule-removal' +import { isSubmoduleWorktreeRemovalRefusal } from './submodule-removal' describe('isSubmoduleWorktreeRemovalRefusal', () => { it('matches the English git fatal on stderr', () => { diff --git a/src/shared/worktree-submodule-removal.ts b/src/shared/worktree/submodule-removal.ts similarity index 100% rename from src/shared/worktree-submodule-removal.ts rename to src/shared/worktree/submodule-removal.ts diff --git a/src/shared/worktree-types.ts b/src/shared/worktree/types.ts similarity index 94% rename from src/shared/worktree-types.ts rename to src/shared/worktree/types.ts index 25686dbf6f3..af6252f5c3a 100644 --- a/src/shared/worktree-types.ts +++ b/src/shared/worktree/types.ts @@ -1,10 +1,10 @@ -import type { ExecutionHostId } from './execution-host' -import type { AutomationExecutionTargetType } from './automations-types' -import type { TaskSourceContext } from './task-source-context' -import type { TuiAgent } from './tui-agent' -import type { DiffComment, MobileDiffReviewState } from './diff-comment-types' -import type { EphemeralVmCheckoutMode } from './orca-yaml-hook-types' -import type { BuiltInWorktreeVisibilitySourceId } from './repo-types' +import type { ExecutionHostId } from '../execution-host' +import type { AutomationExecutionTargetType } from '../automations-types' +import type { TaskSourceContext } from '../task-source-context' +import type { TuiAgent } from '../tui-agent' +import type { DiffComment, MobileDiffReviewState } from '../diff-comment-types' +import type { EphemeralVmCheckoutMode } from '../orca-yaml-hook-types' +import type { BuiltInWorktreeVisibilitySourceId } from '../repo-types' export type WorkspaceLinkedItem = { provider: 'github' | 'gitlab' | 'linear' | 'jira' diff --git a/src/shared/worktree-visibility-sources.test.ts b/src/shared/worktree/visibility-sources.test.ts similarity index 98% rename from src/shared/worktree-visibility-sources.test.ts rename to src/shared/worktree/visibility-sources.test.ts index 875c34b4ad8..280ac0f3082 100644 --- a/src/shared/worktree-visibility-sources.test.ts +++ b/src/shared/worktree/visibility-sources.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it, vi } from 'vitest' -import type { Repo } from './types' +import type { Repo } from '../types' import { buildWorktreeSourcePreferenceUpdate, createWorktreeVisibilitySourceMatcher, effectiveBuiltInWorktreeSourceVisibility, normalizeCustomWorktreeVisibilitySources, normalizeWorktreeVisibilitySourcePreferences -} from './worktree-visibility-sources' +} from './visibility-sources' function repo(overrides: Partial = {}): Repo { return { diff --git a/src/shared/worktree-visibility-sources.ts b/src/shared/worktree/visibility-sources.ts similarity index 99% rename from src/shared/worktree-visibility-sources.ts rename to src/shared/worktree/visibility-sources.ts index e4153daa86d..0053bedf2de 100644 --- a/src/shared/worktree-visibility-sources.ts +++ b/src/shared/worktree/visibility-sources.ts @@ -2,14 +2,14 @@ import { createNormalizedPathInsideOrEqualMatcher, isRuntimePathAbsolute, normalizeRuntimePathForComparison -} from './cross-platform-path' +} from '../cross-platform-path' import type { BuiltInWorktreeVisibilitySourceId, CustomWorktreeVisibilitySource, ExternalWorktreeVisibility, Repo, WorktreeVisibilitySourcePreferences -} from './types' +} from '../types' export const MAX_CUSTOM_WORKTREE_VISIBILITY_SOURCES = 32 const MAX_SOURCE_ID_LENGTH = 128 diff --git a/tests/e2e/completed-worker-retirement-resume.spec.ts b/tests/e2e/completed-worker-retirement-resume.spec.ts index 5540c4d6896..7d505c69587 100644 --- a/tests/e2e/completed-worker-retirement-resume.spec.ts +++ b/tests/e2e/completed-worker-retirement-resume.spec.ts @@ -19,7 +19,7 @@ import { } from './helpers/completed-worker-retirement-fixture' import { RuntimeClient } from '../../src/cli/runtime-client' import type { RuntimeTerminalRead, RuntimeTerminalSummary } from '../../src/shared/runtime-types' -import { splitWorktreeIdForFilesystem } from '../../src/shared/worktree-id' +import { splitWorktreeIdForFilesystem } from '../../src/shared/worktree/id' const PROVIDER_SESSION_ID = '019feb51-2269-71c2-89c6-faa8dc65c8dc' From 4221f8d429410110e29d809feb897e0e9cd72f47 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:52:04 -0700 Subject: [PATCH 06/11] refactor(preload): split the preload contract into per-domain api modules (#14403) `src/preload/api-types.ts` was 3,752 raw lines (3,533 counted, 11.8x the 300-line budget) behind an `eslint-disable max-lines`. Almost all of it was a single `PreloadApi` object type whose ~83 namespace properties were declared inline, so any IPC surface change meant editing one 2,600-line type. Give each namespace a named type in its own module under `src/preload/api/` (`pty-api.ts`, `filesystem-api.ts`, `github-pull-request-api.ts`, ...) and recompose `PreloadApi` from those names. `api-types.ts` keeps the `declare global` Window augmentation and re-exports every moved name, so all 52 import sites are untouched. Two shapes needed care to stay type-identical rather than merely compatible: - Three keys (`gh`, `git`, `ui`) are composed from two modules each. A plain intersection is NOT identical to the original flat object literal, so those use a `Merged` mapped type; a negative control confirmed that dropping it fails the parity assertion. - Keys whose module groups several namespaces use indexed access (`fs: FilesystemApi['fs']`) to preserve exact identity and source order. `config/tsconfig.web.json` and `tsconfig.tc.web.json` enumerate files by path, so they need `src/preload/api/**/*` alongside the existing `api-types.ts` seed or the web projects fail TS6307. Verified by exact type identity, not assignability: 41 assertions of the form `Equals` against a frozen pre-split snapshot, covering every exported name, plus a per-key pass over all 83 `PreloadApi` keys. All three projects typecheck clean with those assertions active. Verification note: these tsconfigs are `composite: true`, and `tsc --noEmit` will reuse a stale `.tsbuildinfo` and report clean for a state that genuinely fails. Every result above was produced after deleting the buildinfo, including a negative control confirming the gate still fails on deliberate drift. Drops the `max-lines` bypass and its baseline entry (ratchet 346 -> 345). --- config/max-lines-baseline.txt | 1 - config/tsconfig.tc.web.json | 1 + config/tsconfig.web.json | 1 + src/preload/api-types.ts | 3903 +------------------- src/preload/api/agent-account-api.ts | 62 + src/preload/api/agent-hook-api.ts | 51 + src/preload/api/agent-skill-api.ts | 16 + src/preload/api/agent-status-api.ts | 52 + src/preload/api/agent-usage-api.ts | 59 + src/preload/api/ai-vault-api.ts | 37 + src/preload/api/app-api.ts | 82 + src/preload/api/automation-api.ts | 37 + src/preload/api/browser-api.ts | 132 + src/preload/api/cli-install-api.ts | 10 + src/preload/api/crash-report-api.ts | 38 + src/preload/api/dashboard-api.ts | 42 + src/preload/api/emulator-api.ts | 37 + src/preload/api/ephemeral-vm-api.ts | 72 + src/preload/api/filesystem-api.ts | 208 ++ src/preload/api/git-inspection-api.ts | 123 + src/preload/api/git-operation-api.ts | 128 + src/preload/api/github-pull-request-api.ts | 196 + src/preload/api/github-work-item-api.ts | 167 + src/preload/api/gitlab-api.ts | 172 + src/preload/api/hosted-review-api.ts | 31 + src/preload/api/jira-api.ts | 84 + src/preload/api/linear-api.ts | 128 + src/preload/api/mobile-api.ts | 86 + src/preload/api/native-chat-api.ts | 71 + src/preload/api/onboarding-api.ts | 28 + src/preload/api/orca-profile-api.ts | 59 + src/preload/api/os-permission-api.ts | 54 + src/preload/api/pet-api.ts | 8 + src/preload/api/plugin-host-api.ts | 191 + src/preload/api/preflight-api.ts | 55 + src/preload/api/pty-api.ts | 224 ++ src/preload/api/pty-management-api.ts | 30 + src/preload/api/repository-api.ts | 169 + src/preload/api/runtime-api.ts | 116 + src/preload/api/settings-api.ts | 35 + src/preload/api/shell-api.ts | 32 + src/preload/api/speech-api.ts | 33 + src/preload/api/ssh-api.ts | 78 + src/preload/api/telemetry-api.ts | 63 + src/preload/api/ui-command-event-api.ts | 203 + src/preload/api/ui-window-api.ts | 51 + src/preload/api/updater-api.ts | 25 + src/preload/api/workspace-cleanup-api.ts | 30 + src/preload/api/workspace-port-api.ts | 23 + src/preload/api/workspace-session-api.ts | 50 + src/preload/api/worktree-api.ts | 179 + 51 files changed, 4040 insertions(+), 3723 deletions(-) create mode 100644 src/preload/api/agent-account-api.ts create mode 100644 src/preload/api/agent-hook-api.ts create mode 100644 src/preload/api/agent-skill-api.ts create mode 100644 src/preload/api/agent-status-api.ts create mode 100644 src/preload/api/agent-usage-api.ts create mode 100644 src/preload/api/ai-vault-api.ts create mode 100644 src/preload/api/app-api.ts create mode 100644 src/preload/api/automation-api.ts create mode 100644 src/preload/api/browser-api.ts create mode 100644 src/preload/api/cli-install-api.ts create mode 100644 src/preload/api/crash-report-api.ts create mode 100644 src/preload/api/dashboard-api.ts create mode 100644 src/preload/api/emulator-api.ts create mode 100644 src/preload/api/ephemeral-vm-api.ts create mode 100644 src/preload/api/filesystem-api.ts create mode 100644 src/preload/api/git-inspection-api.ts create mode 100644 src/preload/api/git-operation-api.ts create mode 100644 src/preload/api/github-pull-request-api.ts create mode 100644 src/preload/api/github-work-item-api.ts create mode 100644 src/preload/api/gitlab-api.ts create mode 100644 src/preload/api/hosted-review-api.ts create mode 100644 src/preload/api/jira-api.ts create mode 100644 src/preload/api/linear-api.ts create mode 100644 src/preload/api/mobile-api.ts create mode 100644 src/preload/api/native-chat-api.ts create mode 100644 src/preload/api/onboarding-api.ts create mode 100644 src/preload/api/orca-profile-api.ts create mode 100644 src/preload/api/os-permission-api.ts create mode 100644 src/preload/api/pet-api.ts create mode 100644 src/preload/api/plugin-host-api.ts create mode 100644 src/preload/api/preflight-api.ts create mode 100644 src/preload/api/pty-api.ts create mode 100644 src/preload/api/pty-management-api.ts create mode 100644 src/preload/api/repository-api.ts create mode 100644 src/preload/api/runtime-api.ts create mode 100644 src/preload/api/settings-api.ts create mode 100644 src/preload/api/shell-api.ts create mode 100644 src/preload/api/speech-api.ts create mode 100644 src/preload/api/ssh-api.ts create mode 100644 src/preload/api/telemetry-api.ts create mode 100644 src/preload/api/ui-command-event-api.ts create mode 100644 src/preload/api/ui-window-api.ts create mode 100644 src/preload/api/updater-api.ts create mode 100644 src/preload/api/workspace-cleanup-api.ts create mode 100644 src/preload/api/workspace-port-api.ts create mode 100644 src/preload/api/workspace-session-api.ts create mode 100644 src/preload/api/worktree-api.ts diff --git a/config/max-lines-baseline.txt b/config/max-lines-baseline.txt index 17f573b47d1..4375803952e 100644 --- a/config/max-lines-baseline.txt +++ b/config/max-lines-baseline.txt @@ -167,7 +167,6 @@ inline src/main/window/attach-main-window-services.ts inline src/main/window/createMainWindow.test.ts inline src/main/window/createMainWindow.ts inline src/main/workspace-space-analysis.ts -inline src/preload/api-types.ts inline src/preload/index.ts inline src/relay/agent-hook-server.ts inline src/relay/dispatcher.ts diff --git a/config/tsconfig.tc.web.json b/config/tsconfig.tc.web.json index 1034cc16ced..9f83cddee95 100644 --- a/config/tsconfig.tc.web.json +++ b/config/tsconfig.tc.web.json @@ -5,6 +5,7 @@ "../src/renderer/src/**/*", "../src/renderer/src/**/*.tsx", "../src/preload/api-types.ts", + "../src/preload/api/**/*", "../src/shared/**/*", "../src/main/gitlab/mappers.ts", "../src/main/ipc/worktree-branch-name.ts", diff --git a/config/tsconfig.web.json b/config/tsconfig.web.json index 92a6b9f2831..5a43720b5aa 100644 --- a/config/tsconfig.web.json +++ b/config/tsconfig.web.json @@ -5,6 +5,7 @@ "../src/renderer/src/**/*", "../src/renderer/src/**/*.tsx", "../src/preload/api-types.ts", + "../src/preload/api/**/*", "../src/shared/**/*" ], "compilerOptions": { diff --git a/src/preload/api-types.ts b/src/preload/api-types.ts index 84ceb217cf7..d81d8c59f93 100644 --- a/src/preload/api-types.ts +++ b/src/preload/api-types.ts @@ -1,2746 +1,130 @@ -/* eslint-disable max-lines -- Why: the preload contract is intentionally centralized in one declaration file so renderer and preload stay in lockstep when IPC surfaces change. */ -import type { - CreateHostedReviewArgs, - CreateHostedReviewResult, - CreateStackedHostedReviewArgs, - CreateStackedHostedReviewResult, - HostedReviewCreationEligibility, - HostedReviewCreationEligibilityArgs, - HostedReviewForBranchArgs, - HostedReviewInfo, - HostedReviewProvider -} from '../shared/hosted-review' -import type { - BitbucketConnectArgs, - BitbucketConnectionStatus -} from '../shared/bitbucket-credentials' -import type { NativeFileDropPayload } from '../shared/native-file-drop' -import type { ComputerAwakeStatus } from '../shared/computer-awake-mode' -import type { BrowserFindSource } from '../shared/browser-find-source' -import type { - DashboardRevealAgentArgs, - DashboardSleepWorkspaceArgs, - DashboardSnapshot, - DashboardSpawnAgentArgs -} from '../shared/dashboard-snapshot' -import type { - TerminalPreviewConnectResult, - TerminalPreviewDataPayload -} from '../shared/terminal-preview' -import type { - TerminalTabCloseRequest, - TerminalTabCloseResponse -} from '../shared/terminal-tab-close' -import type { TerminalTabCreateReply } from '../shared/terminal-reveal-identity' -import type { - LocalLogTailChangedPayload, - LocalLogTailReadArgs, - LocalLogTailReadResult, - LocalLogTailWatchArgs -} from '../shared/local-log-tail-types' -import type { ReadClipboardTextOptions } from '../shared/clipboard-text' -import type { AppIdentity } from '../shared/app-identity' -import type { ReleaseChannel } from '../shared/release-channel' -import type { - ForgetRemovedWorktreesForExecutionHostArgs, - ForgetRemovedWorktreesForExecutionHostResult, - HostQualifiedKnownWorktreeResult, - HostQualifiedDetectedWorktreeResult, - LegacyDetectedWorktreeRequest, - ListKnownWorktreesForExecutionHostArgs, - ListDetectedWorktreesArgs, - ProviderRequestId -} from '../shared/detected-worktree-provider-contract' -import type { - HostRepoCatalogSnapshot, - ListReposForExecutionHostArgs -} from '../shared/host-repo-catalog-contract' -import type { - HostLineageSnapshot, - ListDesktopLineageForHostArgs -} from '../shared/host-lineage-contract' -import type { - WriteTerminalRenderDesyncEvidenceArgs, - WriteTerminalRenderDesyncEvidenceResult -} from '../shared/terminal-render-desync-evidence' -import type { MobileRelayStatus } from '../shared/mobile-relay-status' -import type { MobilePairingConnectionMode } from '../shared/mobile-pairing-connection-mode' -import type { RuntimePairingReach } from '../shared/runtime-pairing-reach' -import type { MobileRelayMintFailure } from '../shared/mobile-relay-mint-failure' -import type { VerifyAndAddRuntimeEnvironmentResult } from '../shared/remote-pairing-verification' -import type { - SshMutationExpectation, - SshConnectionState, - SshConfigHostListArgs, - SshConfigHostListResult, - SshConfigHostResolution, - SshConfigImportResult, - SshTargetAddResult, - SshTarget, - PortForwardEntry, - EnrichedDetectedPort -} from '../shared/ssh-types' -import type { - CreateLocalOrcaProfileArgs, - CreateLocalOrcaProfileResult, - CreateCloudLinkedOrcaProfileArgs, - CreateCloudLinkedOrcaProfileResult, - ConnectCurrentOrcaProfileResult, - FindOrcaProfileProjectsByPathArgs, - FindOrcaProfileProjectsByPathResult, - OrcaProfileListResult, - OrcaProfileAuthStatus, - RefreshCurrentOrcaProfileAuthResult, - SelectOrcaProfileOrgArgs, - SelectOrcaProfileOrgResult, - SignOutCurrentOrcaProfileResult, - SwitchOrcaProfileArgs, - SwitchOrcaProfileResult, - TransferOrcaProfileProjectArgs, - TransferOrcaProfileProjectResult, - OrcaProfileOrgInviteRevokeArgs, - OrcaProfileOrgMemberChangeRoleArgs, - OrcaProfileOrgMemberInviteArgs, - OrcaProfileOrgMemberMutationResult, - OrcaProfileOrgMemberRemoveArgs, - OrcaProfileOrgMembersListArgs, - OrcaProfileOrgMembersListResult -} from '../shared/orca-profiles' -import type { TerminalPaneSplitSource } from '../shared/feature-education-telemetry' -import type { TaskSourceContext } from '../shared/task-source-context' -import type { LinearIssueAttributeFilter } from '../shared/linear/issue-attribute-filter' -import type { ProjectExecutionRuntimeResolution } from '../shared/project-execution-runtime' -import type { StartupCommandDelivery } from '../shared/codex-startup-delivery' -import type { - AgentProviderSessionMetadata, - SleepingAgentLaunchConfig -} from '../shared/agent-session-resume' -import type { - PluginPanelActionOutcome, - PluginPanelEntry -} from '../shared/plugins/plugin-panel-bridge' -import type { PluginConsentRequest } from '../shared/plugins/plugin-consent-request' -import type { PluginLanguagePackRegistration } from '../shared/plugins/plugin-language-pack-artifact' -import type { PluginChangeEvent } from '../shared/plugins/plugin-change-event' -import type { PluginManifest } from '../shared/plugins/plugin-manifest' -import type { PluginMarketplaceGitSource } from '../shared/plugins/plugin-marketplace' -import type { - LocalhostWorktreeLabelResult, - LocalhostWorktreeLabelRoute -} from '../shared/localhost-worktree-labels' -import type { - FolderWorkspacePathStatus, - FolderWorkspacePathStatusRequest -} from '../shared/folder-workspace-path-status' -import type { - BaseRefDefaultResult, - BaseRefSearchResult, - BrowserCookieImportResult, - BrowserCertificateFailure, - BrowserCertificateProceedResult, - BrowserLoadError, - BrowserSessionProfile, - BrowserSessionProfileCreateOptions, - BrowserSessionProfileScope, - BrowserSessionProfileSource, - BrowserViewportOverride, - ClaudeRateLimitAccountsState, - ClassifiedError, - CodexRateLimitAccountsState, - AdoptProvisionedRootArgs, - CreateWorktreeArgs, - CreateWorktreeResult, - CustomPet, - DetectedWorktreeListResult, - DirEntry, - FilesystemPathFlavor, - ForceDeleteWorktreeBranchResult, - FsChangedPayload, - GhosttyImportPreview, - GlobalSettings, - GitBranchCompareResult, - GitCommitCompareResult, - GitConflictOperation, - GitDiffResult, - GitForkSyncExpectedUpstream, - GitForkSyncResult, - GitPushTarget, - GitStagingArea, - GitStatusResult, - GitUpstreamStatus, - GitHubAssignableUser, - GitHubCreateIssueResult, - GitHubPRFile, - GitHubPRFileContents, - GitHubPrStartPoint, - GitHubPRReviewCommentInput, - GitHubCommentResult, - GitHubOwnerRepo, - GitHubWorkItem, - GitHubWorkItemDetails, - GitHubViewer, - GitLabAssignableUser, - GitLabAuthDiagnostic, - GitLabCommentResult, - GitLabDiscussionResolveResult, - GitLabIssueInfo, - GitLabIssueUpdate, - GitLabJobTraceResult, - GitLabMRInlineCommentInput, - GitLabMRReviewersUpdateResult, - GitLabMRUpdate, - GitLabProjectRef, - GitLabRetryJobResult, - GitLabTodo, - GitLabViewer, - GitLabWorkItem, - GitLabWorkItemDetails, - GetGitLabRateLimitResult, - ListMergeRequestsResult, - MRInfo, - MRListState, - ListWorkItemsResult, - IssueInfo, - JiraComment, - JiraConnectionStatus, - JiraCreateField, - JiraCreateIssueArgs, - JiraIssue, - JiraIssueFilter, - JiraIssueType, - JiraProjectStatusOrder, - JiraIssueUpdate, - JiraPriority, - JiraProject, - JiraSiteSelection, - JiraTransition, - JiraUser, - JiraViewer, - LinearViewer, - LinearCollectionResult, - LinearConnectionStatus, - LinearCustomViewModel, - LinearCustomViewSummary, - LinearWorkspaceSelection, - LinearIssue, - LinearIssueUpdate, - LinearComment, - LinearWorkflowState, - LinearLabel, - LinearMember, - LinearProjectDetail, - LinearProjectSummary, - LinearTeam, - MarkdownDocument, - FloatingTerminalCwdRequest, - GitHubIssueUpdate, - GitHubReactionContent, - GitHubPRRefreshCandidate, - GitHubPRRefreshEnqueueResult, - GitHubPRRefreshEvent, - GitHubPRRefreshReason, - GetRateLimitResult, - NotificationDispatchRequest, - NotificationDispatchResult, - NotificationDeliveryProbeResult, - NotificationDismissResult, - NotificationPermissionStatusResult, - NotificationSoundResult, - OnboardingState, - OrcaHooks, - PathSource, - PersistedUIState, - PRCheckDetail, - PRCheckRunDetails, - PRComment, - PRInfo, - PRRefreshOutcome, - Project, - ProjectUpdateArgs, - Repo, - ProjectGroup, - ProjectHostSetup, - ProjectHostSetupCreateArgs, - ProjectHostSetupCreateResult, - ProjectHostSetupDeleteArgs, - ProjectHostSetupDeleteResult, - ProjectHostSetupExistingFolderArgs, - ProjectHostSetupResult, - ProjectHostSetupUpdateArgs, - ProjectHostSetupUpdateResult, - FolderWorkspace, - ProjectGroupImportResult, - ProjectGroupImportMode, - ShellHydrationFailureReason, - SparsePreset, - SearchOptions, - NestedRepoScanResult, - SearchResult, - StatsSummary, - MemorySnapshot, - TuiAgent, - ReleaseBuildListResult, - UpdateCheckOptions, - UpdateStatus, - Worktree, - WorktreeBaseStatusEvent, - WorktreeHeadIdentity, - WorktreeLineage, - WorkspaceLineage, - WorktreeMeta, - WorktreeRemoteBranchConflictEvent, - RemoveWorktreeResult, - WorktreeDefaultTabsLaunch, - WorktreeSetupLaunch, - WorktreeStartupLaunch, - WorkspaceSessionPatch, - WorkspaceSessionState, - LinuxPackageInstallInstructions -} from '../shared/types' -import type { PtyModelRestoreNeededEvent } from '../shared/pty-model-restore-marker' -import type { PtyListedSession } from '../shared/pty-listed-session' -import type { - PtyRendererDeliveryHealthReply, - PtyRendererDeliveryStateReport -} from '../shared/pty-renderer-delivery-health' -import type { TerminalViewAttributes } from '../shared/terminal-view-attributes' -import type { PtyMainDeliveryDiagnostics } from '../shared/pty-delivery-diagnostics' -import type { - WarpThemeImportPreview, - WarpThemeImportSource -} from '../shared/terminal-custom-themes' - -import type { SetupScriptImportCandidate } from '../shared/setup-script-imports' -import type { GitHistoryOptions, GitHistoryResult } from '../shared/git-history' -import type { PublicKnownRuntimeEnvironment } from '../shared/runtime-environments' -import type { EphemeralVmRecipeDoctorResult } from '../shared/ephemeral-vm-recipes' -import type { EphemeralVmRecipeResultWarning } from '../shared/ephemeral-vm-recipe-diagnostics' -import type { EphemeralVmRuntimeRecord } from '../shared/ephemeral-vm-runtimes' -import type { RuntimeAccessGrant } from '../shared/runtime-access-grants' -import type { RuntimeRpcResponse } from '../shared/runtime-rpc-envelope' -import type { ExecutionHostId } from '../shared/execution-host' -import type { FeatureInteractionId } from '../shared/feature-interactions' -import type { - AddIssueCommentBySlugArgs, - ClearProjectItemFieldArgs, - DeleteIssueCommentBySlugArgs, - GetProjectViewTableArgs, - GetProjectViewTableResult, - GitHubProjectCommentMutationResult, - GitHubProjectMutationResult, - ListAccessibleProjectsArgs, - ListAccessibleProjectsResult, - ListAssignableUsersBySlugArgs, - ListAssignableUsersBySlugResult, - ListIssueTypesBySlugArgs, - ListIssueTypesBySlugResult, - ListLabelsBySlugArgs, - ListLabelsBySlugResult, - ListProjectViewsArgs, - ListProjectViewsResult, - ProjectWorkItemDetailsBySlugArgs, - ProjectWorkItemDetailsBySlugResult, - ResolveProjectRefArgs, - ResolveProjectRefResult, - UpdateIssueBySlugArgs, - UpdateIssueCommentBySlugArgs, - UpdateIssueTypeBySlugArgs, - UpdatePullRequestBySlugArgs, - UpdateProjectItemFieldArgs -} from '../shared/github/project-types' -import type { - RichMarkdownContextMenuCommandPayload, - RichMarkdownContextMenuTableTarget -} from '../shared/rich-markdown-context-menu' -import type { - BrowserSetGrabModeArgs, - BrowserSetGrabModeResult, - BrowserAwaitGrabSelectionArgs, - BrowserGrabResult, - BrowserCancelGrabArgs, - BrowserCaptureSelectionScreenshotArgs, - BrowserCaptureSelectionScreenshotResult, - BrowserExtractHoverArgs, - BrowserExtractHoverResult -} from '../shared/browser-grab-types' -import type { - BrowserContextMenuDismissedEvent, - BrowserContextMenuRequestedEvent, - BrowserDownloadFinishedEvent, - BrowserDownloadProgressEvent, - BrowserDownloadRequestedEvent, - BrowserPermissionDeniedEvent, - BrowserPopupEvent -} from '../shared/browser-guest-events' import type { ElectronAPI } from '@electron-toolkit/preload' -import type { BrowserSetAnnotationViewportBridgeArgs } from '../shared/browser-annotation-viewport-bridge' -import type { CliInstallStatus } from '../shared/cli-install-types' -import type { E2EConfig } from '../shared/e2e-config' -import type { AgentHookInstallStatus } from '../shared/agent-hook-types' -import type { CodexConfigSyncStatus } from '../shared/codex-config-sync-types' import type { - AgentStatusClearIpcPayload, - AgentStatusIpcPayload, - MigrationUnsupportedPtyEntry -} from '../shared/agent-status-types' -import type { AgentInterruptInferenceRequest } from '../shared/agent-interrupt-intent' -import type { AgentQuestionAnsweredInferenceRequest } from '../shared/agent-question-answered-intent' -import type { TerminalSideEffectBatch } from '../shared/terminal-side-effect-facts' + ClaudeAccountsApi, + CodexAccountsApi, + CodexConfigSyncApi, + GrokAccountsApi, + MinimaxCredentialsApi +} from './api/agent-account-api' +import type { AgentHooksApi, HooksApi } from './api/agent-hook-api' +import type { SkillsApi } from './api/agent-skill-api' +import type { AgentAwakeApi, AgentStatusApi, AgentTrustApi } from './api/agent-status-api' import type { - RuntimeBrowserDriverState, - RuntimeMobileSessionTabMove, - RuntimeStatus, - RuntimeSyncWindowGraphResult, - RuntimeSyncWindowGraph, - RuntimeTerminalCreateRequestPayload, - RuntimeTerminalDriverState, - RuntimeTerminalPresentation -} from '../shared/runtime-types' + ClaudeUsageApi, + CodexUsageApi, + OpenCodeUsageApi, + RateLimitsApi +} from './api/agent-usage-api' +import type { AiVaultApi } from './api/ai-vault-api' +import type { AppApi, E2EApi, PlatformApi } from './api/app-api' +import type { AutomationsApi } from './api/automation-api' +import type { BrowserApi } from './api/browser-api' +import type { CliApi } from './api/cli-install-api' +import type { CrashReportsApi, FeedbackApi } from './api/crash-report-api' +import type { DashboardApi, TerminalPreviewApi } from './api/dashboard-api' +import type { EmulatorApi } from './api/emulator-api' +import type { EphemeralVmApi } from './api/ephemeral-vm-api' +import type { ExportApi, FilesystemApi } from './api/filesystem-api' +import type { GitInspectionApi } from './api/git-inspection-api' +import type { GitOperationApi } from './api/git-operation-api' +import type { GithubPullRequestApi } from './api/github-pull-request-api' +import type { GithubWorkItemApi } from './api/github-work-item-api' +import type { GitLabApi } from './api/gitlab-api' +import type { BitbucketApi, HostedReviewApi } from './api/hosted-review-api' +import type { JiraApi } from './api/jira-api' +import type { LinearApi } from './api/linear-api' +import type { MobileApi } from './api/mobile-api' +import type { NativeChatApi } from './api/native-chat-api' +import type { OnboardingApi, StarNagApi } from './api/onboarding-api' +import type { OrcaProfileApi } from './api/orca-profile-api' import type { - CommitMessageAgentCapability, - CommitMessageModelCapability -} from '../shared/commit-message-agent-spec' -import type { ResolvedSourceControlAiGenerationParams } from '../shared/source-control-ai' -import type { SourceControlAiSettings } from '../shared/source-control-ai-types' -import type { - ShellOpenExternalEditorRequest, - ShellOpenExternalEditorResult, - ShellOpenLocalPathResult -} from '../shared/shell-open-types' -import type { SkillDiscoveryResult, SkillDiscoveryTarget } from '../shared/skills' -import type { - SkillFreshnessInventory, - SkillUpdateRun, - SkillUpdateStartResult -} from '../shared/skill-freshness' -import type { - CrashReportBreadcrumbData, - CrashReportCopyDiagnosticsArgs, - CrashReportRecord, - CrashReportSubmitArgs, - CrashReportSubmitResult, - ReactErrorBoundaryReportArgs, - ReactErrorBoundaryReportResult -} from '../shared/crash-reporting' -import type { RendererHeapStatistics } from '../shared/renderer-heap-statistics' + ComputerUsePermissionsApi, + DeveloperPermissionsApi, + MacosTccPromptsApi, + NotificationsApi +} from './api/os-permission-api' +import type { PetApi } from './api/pet-api' +import type { PluginsApi } from './api/plugin-host-api' +import type { PreflightApi } from './api/preflight-api' +import type { PtyApi } from './api/pty-api' +import type { ProjectGroupsApi, ProjectsApi, RepositoryApi } from './api/repository-api' +import type { RuntimeApi } from './api/runtime-api' +import type { KeybindingsApi, SettingsApi } from './api/settings-api' +import type { ShellApi } from './api/shell-api' +import type { SpeechApi } from './api/speech-api' +import type { SshApi } from './api/ssh-api' +import type { DiagnosticsApi, MemoryApi, StatsApi, TelemetryApi } from './api/telemetry-api' +import type { UiCommandEventApi } from './api/ui-command-event-api' +import type { UiWindowApi } from './api/ui-window-api' +import type { UpdaterApi } from './api/updater-api' +import type { WorkspaceCleanupApi, WorkspaceSpaceApi } from './api/workspace-cleanup-api' +import type { LocalhostWorktreeLabelsApi, WorkspacePortsApi } from './api/workspace-port-api' +import type { WorkspaceSessionApi } from './api/workspace-session-api' +import type { FolderWorkspacesApi, SparsePresetsApi, WorktreeApi } from './api/worktree-api' -export type { - ShellOpenExternalEditorRequest, - ShellOpenExternalEditorResult, - ShellOpenLocalPathResult -} from '../shared/shell-open-types' - -type RuntimeEnvironmentSubscriptionHandle = { - unsubscribe: () => void - sendBinary: (bytes: Uint8Array) => void -} -import type { - RuntimeMobileMarkdownRequest, - RuntimeMobileMarkdownResponse -} from '../shared/mobile-markdown-document' -import type { - DeveloperPermissionId, - DeveloperPermissionRequestResult, - DeveloperPermissionState, - LocalNetworkConnectionTestResult -} from '../shared/developer-permissions-types' -import type { - ComputerUsePermissionId, - ComputerUsePermissionResetResult, - ComputerUsePermissionSetupResult, - ComputerUsePermissionStatusResult -} from '../shared/computer-use-permissions-types' -import type { ClaudeUsageBreakdownKind, ClaudeUsageSnapshot } from '../shared/claude-usage-types' -import type { - CodexRateLimitResetResult, - GrokAccountStatus, - RateLimitRuntimeTarget, - RateLimitState -} from '../shared/rate-limit-types' -import type { - SpeechErrorEvent, - SpeechLifecycleEvent, - SpeechModelManifest, - SpeechModelState, - SpeechTranscriptEvent -} from '../shared/speech-types' -import type { - WorkspaceSpaceAnalyzeResult, - WorkspaceSpaceScanProgress -} from '../shared/workspace-space-types' -import type { - WorkspacePortAdvertisedUrlChangedEvent, - WorkspacePortKillRequest, - WorkspacePortKillResult, - WorkspacePortScanRequest, - WorkspacePortScanResult -} from '../shared/workspace-ports' -import type { GhAuthDiagnostic } from '../shared/github/auth-types' -import type { CodexUsageBreakdownKind, CodexUsageSnapshot } from '../shared/codex-usage-types' -import type { - OpenCodeUsageBreakdownKind, - OpenCodeUsageSnapshot -} from '../shared/opencode-usage-types' -import type { - AiVaultDeleteSessionArgs, - AiVaultDeleteSessionResult -} from '../shared/ai-vault-session-deletion' -import type { - AiVaultFirstUserPromptArgs, - AiVaultFirstUserPromptResult, - AiVaultListArgs, - AiVaultListResult, - AiVaultSubagentListArgs, - AiVaultSubagentListResult -} from '../shared/ai-vault-types' -import type { - AiVaultSessionTitlesArgs, - AiVaultSessionTitlesResult -} from '../shared/ai-vault-session-title' -import type { - AiVaultPrepareSessionResumeArgs, - AiVaultPrepareSessionResumeResult -} from '../shared/ai-vault-resume-preparation' -import type { - AgentType, - NativeChatMessage, - NativeChatTurnLifecycle -} from '../shared/native-chat-types' -import type { TelemetryConsentState } from '../shared/telemetry-consent-types' -import type { AgentKind, LaunchSource, RequestKind } from '../shared/telemetry-events' -import type { AppStarSource } from '../shared/gh-star-source' -import type { - RemoteWorkspaceChangedEvent, - RemoteWorkspaceConnectedClient, - RemoteWorkspacePatchResult, - RemoteWorkspaceSnapshot -} from '../shared/remote-workspace-types' -import type { - Automation, - AutomationCreateInput, - AutomationDispatchRequest, - AutomationDispatchResult, - ExternalAutomationCreateInput, - ExternalAutomationActionInput, - ExternalAutomationManager, - ExternalAutomationRunsInput, - ExternalAutomationRunsPage, - ExternalAutomationUpdateInput, - AutomationRun, - AutomationPrecheckResult, - AutomationUpdateInput -} from '../shared/automations-types' -import type { - WorkspaceCleanupDismissArgs, - WorkspaceCleanupLocalProcessArgs, - WorkspaceCleanupLocalProcessResult, - WorkspaceCleanupScanArgs, - WorkspaceCleanupScanProgress, - WorkspaceCleanupScanResult -} from '../shared/workspace-cleanup' -import type { KeybindingActionId, KeybindingFileSnapshot } from '../shared/keybindings' - -type GitLabRepoSelectorArgs = { - repoPath: string - repoId?: string | null - sourceContext?: TaskSourceContext | null -} - -type GitHubRepoSelectorArgs = { - repoPath: string - repoId?: string | null - sourceContext?: TaskSourceContext | null -} - -export type BrowserApi = { - registerGuest: (args: { - browserPageId: string - workspaceId: string - worktreeId: string - sessionProfileId?: string | null - webContentsId: number - }) => Promise - isGuestRegistered: (args: { browserPageId: string; webContentsId: number }) => Promise - repairGuestRegistration: (args: { - browserPageId: string - workspaceId: string - worktreeId: string - sessionProfileId?: string | null - webContentsId: number - }) => Promise - unregisterGuest: (args: { browserPageId: string }) => Promise - openDevTools: (args: { browserPageId: string }) => Promise - setViewportOverride: (args: { - browserPageId: string - override: BrowserViewportOverride | null - }) => Promise - setAnnotationViewportBridge: (args: BrowserSetAnnotationViewportBridgeArgs) => Promise - onGuestLoadFailed: ( - callback: (args: { browserPageId: string; loadError: BrowserLoadError }) => void - ) => () => void - onCertificateFailureChanged: ( - callback: (event: { browserPageId: string; failure: BrowserCertificateFailure | null }) => void - ) => () => void - proceedCertificate: (args: { - browserPageId: string - challengeId: string - }) => Promise - onPermissionDenied: (callback: (event: BrowserPermissionDeniedEvent) => void) => () => void - onPopup: (callback: (event: BrowserPopupEvent) => void) => () => void - onDownloadRequested: (callback: (event: BrowserDownloadRequestedEvent) => void) => () => void - onDownloadProgress: (callback: (event: BrowserDownloadProgressEvent) => void) => () => void - onDownloadFinished: (callback: (event: BrowserDownloadFinishedEvent) => void) => () => void - onContextMenuRequested: ( - callback: (event: BrowserContextMenuRequestedEvent) => void - ) => () => void - onContextMenuDismissed: ( - callback: (event: BrowserContextMenuDismissedEvent) => void - ) => () => void - onNavigationUpdate: ( - callback: (event: { browserPageId: string; url: string; title: string }) => void - ) => () => void - onActivateView: ( - callback: (data: { worktreeId?: string; browserPageId?: string }) => void - ) => () => void - onPaneFocus: ( - callback: (data: { worktreeId: string | null; browserPageId: string }) => void - ) => () => void - onOpenLinkInOrcaTab: ( - callback: (event: { browserPageId: string; url: string }) => void - ) => () => void - cancelDownload: (args: { downloadId: string }) => Promise - setGrabMode: (args: BrowserSetGrabModeArgs) => Promise - awaitGrabSelection: (args: BrowserAwaitGrabSelectionArgs) => Promise - cancelGrab: (args: BrowserCancelGrabArgs) => Promise - captureSelectionScreenshot: ( - args: BrowserCaptureSelectionScreenshotArgs - ) => Promise - extractHoverPayload: (args: BrowserExtractHoverArgs) => Promise - onGrabModeToggle: (callback: (browserPageId: string) => void) => () => void - onGrabActionShortcut: ( - callback: (args: { browserPageId: string; key: 'c' | 's' }) => void - ) => () => void - sessionListProfiles: () => Promise - sessionCreateProfile: ( - args: { - scope: BrowserSessionProfileScope - label: string - } & BrowserSessionProfileCreateOptions - ) => Promise - sessionDeleteProfile: (args: { profileId: string }) => Promise - sessionImportCookies: (args: { profileId: string }) => Promise - sessionResolvePartition: (args: { profileId: string | null }) => Promise - sessionDetectBrowsers: () => Promise - sessionImportFromBrowser: (args: { - profileId: string - browserFamily: string - browserProfile?: string - }) => Promise - sessionClearDefaultCookies: () => Promise - notifyActiveTabChanged: (args: { browserPageId: string }) => Promise -} - -export type EmulatorApi = { - onPaneFocus: (callback: (data: { worktreeId: string }) => void) => () => void - onAutoAttach: ( - callback: (data: { - worktreeId: string - info: { deviceUdid: string; streamUrl: string; wsUrl: string; axUrl?: string } - }) => void - ) => () => void - startFrameStream: (args: { streamUrl: string; streamKey?: string }) => Promise<{ - streamId: string - }> - stopFrameStream: (args: { streamId: string }) => Promise - onFrameStreamFrame: ( - callback: (data: { streamId: string; bytes: ArrayBuffer }) => void - ) => () => void - onFrameStreamError: ( - callback: (data: { streamId: string; message: string }) => void - ) => () => void - startVideoStream: (args: { deviceId: string; streamId: string }) => Promise<{ streamId: string }> - stopVideoStream: (args: { streamId: string }) => Promise - onVideoStreamMeta: ( - callback: (data: { - streamId: string - deviceId: string - meta: { codecId: string; width: number; height: number } - }) => void - ) => () => void - onVideoStreamFrame: ( - callback: (data: { - streamId: string - deviceId: string - config: boolean - keyFrame: boolean - bytes: ArrayBuffer - }) => void - ) => () => void -} - -export type DetectedBrowserProfileInfo = { - name: string - directory: string -} - -export type DetectedBrowserInfo = { - family: BrowserSessionProfileSource['browserFamily'] - label: string - profiles: DetectedBrowserProfileInfo[] - selectedProfile: string -} - -export type PreflightStatus = { - git: { installed: boolean } - gh: { installed: boolean; authenticated: boolean } - /** Optional — older preload payloads predating GitLab support omit it; consumers gate on `glab?.installed`. */ - glab?: { installed: boolean; authenticated: boolean } - bitbucket?: { configured: boolean; authenticated: boolean; account: string | null } - azureDevOps?: { - configured: boolean - authenticated: boolean - account: string | null - baseUrl: string | null - tokenConfigured: boolean - } - gitea?: { - configured: boolean - authenticated: boolean - account: string | null - baseUrl: string | null - tokenConfigured: boolean - } -} - -export type RefreshAgentsResult = { - agents: string[] - addedPathSegments: string[] - shellHydrationOk: boolean - /** Drives agent_picks `on_path:false` triage (dashboard 1562016). `'shell_hydrate'` = detection saw the user's - * full shell PATH; `'sync_seed_only'` = hydration failed and detection ran against the `patchPackagedProcessPath` seed list. */ - pathSource: PathSource - /** Classified hydration outcome: `'none'` on success, else a failure mode when `shellHydrationOk` is false. */ - pathFailureReason: ShellHydrationFailureReason -} - -export type PreflightRuntimeContext = { - wslDistro?: string | null - wslDefault?: boolean - projectRuntime?: ProjectExecutionRuntimeResolution -} - -export type PreflightApi = { - check: (args?: PreflightRuntimeContext & { force?: boolean }) => Promise - detectAgents: (args?: PreflightRuntimeContext) => Promise - refreshAgents: (args?: PreflightRuntimeContext) => Promise - detectRemoteAgents: (args: { connectionId: string }) => Promise - detectRemoteWindowsTerminalCapabilities: (args: { connectionId: string }) => Promise<{ - wslAvailable: boolean - wslDistros: string[] - pwshAvailable: boolean - gitBashAvailable: boolean - hostPlatform: NodeJS.Platform | null - }> -} - -// Mirror of daemon's `DaemonSessionInfo` (src/main/daemon/types.ts); not imported — preload can't depend on main-only protocol types. -export type PtyManagementSession = { - sessionId: string - state: 'created' | 'spawning' | 'running' | 'exiting' | 'exited' - shellState: 'pending' | 'ready' | 'timed_out' | 'unsupported' - isAlive: boolean - pid: number | null - cwd: string | null - cols: number - rows: number - createdAt: number - protocolVersion: number -} - -// 'severed': macOS can no longer attribute daemon terminals to Orca, so Accessibility/ -// Automation grants silently stop applying until the daemon is restarted (STA-3491). -export type PtyManagementMacTccAttributionHealth = 'intact' | 'severed' | 'unknown' - -export type PtyManagementApi = { - // `degraded`: daemon is alive but can't spawn fresh PTYs, so new terminals run locally without daemon persistence. - listSessions: () => Promise<{ sessions: PtyManagementSession[]; degraded: boolean }> - killAll: () => Promise<{ - killedCount: number - remainingCount: number - killedSessionIds?: string[] - }> - killOne: (args: { sessionId: string }) => Promise<{ success: boolean }> - restart: () => Promise<{ success: boolean }> - macTccAttribution: () => Promise<{ health: PtyManagementMacTccAttributionHealth }> -} - -export type ExportApi = { - htmlToPdf: (args: { - html: string - title: string - }) => Promise< - { success: true; filePath: string } | { success: false; cancelled?: boolean; error?: string } - > -} - -export type StatsApi = { - getSummary: () => Promise -} - -// Diagnostics IPC payloads; mirror the runtime types in `src/main/observability/{index,bundle}.ts`. -export type DiagnosticsStatusPayload = { - readonly localFileEnabled: boolean - readonly bundleEnabled: boolean - readonly traceFilePath: string - readonly traceFamilySize: number - readonly disabledReason?: - | 'do_not_track' - | 'orca_telemetry_disabled' - | 'orca_diagnostics_disabled' - | 'ci' -} -export type DiagnosticsBundlePayload = { - readonly bundleSubmissionId: string - readonly bytes: number - readonly spanCount: number -} -export type DiagnosticsUploadPayload = - | { - readonly ticketId: string - } - | { - readonly canceled: true - } - -export type MemoryApi = { - getSnapshot: () => Promise -} - -type UsageProviderSnapshot = { - scanState: unknown - summary: { scope: string; range: string } - daily: unknown[] - modelBreakdown: unknown[] - recentSessions: unknown[] -} - -type UsageQueryArgs = Pick< - Snapshot['summary'], - 'scope' | 'range' -> - -type UsageProviderApi = { - getScanState: () => Promise - setEnabled: (args: { enabled: boolean }) => Promise - refresh: (args?: { force?: boolean }) => Promise - getSnapshot: (args: UsageQueryArgs & { limit?: number }) => Promise - getSummary: (args: UsageQueryArgs) => Promise - getDaily: (args: UsageQueryArgs) => Promise - getBreakdown: ( - args: UsageQueryArgs & { kind: BreakdownKind } - ) => Promise - getRecentSessions: ( - args: UsageQueryArgs & { limit?: number } - ) => Promise -} - -export type ClaudeUsageApi = UsageProviderApi - -export type CodexUsageApi = UsageProviderApi - -export type OpenCodeUsageApi = UsageProviderApi - -export type AiVaultApi = { - listSessions: (args?: AiVaultListArgs) => Promise - resolveSessionTitles: (args: AiVaultSessionTitlesArgs) => Promise - cancelListSessions: (args: { requestToken: string }) => Promise - prepareSessionResume: ( - args: AiVaultPrepareSessionResumeArgs - ) => Promise - /** Lists the Task subagent transcripts of one session, on demand. */ - listSubagentSessions: (args: AiVaultSubagentListArgs) => Promise - /** Full first user prompt for copy/reuse (re-parses one transcript). */ - getFirstUserPrompt: (args: AiVaultFirstUserPromptArgs) => Promise - /** Moves a deletable session's transcript to the OS trash; local sessions only. */ - deleteSession: (args: AiVaultDeleteSessionArgs) => Promise - /** Fires when any app window regains OS focus; returns an unsubscribe. */ - onWindowFocused: (callback: () => void) => () => void -} - -// notFound marks a not-yet-on-disk miss (retry-worthy) vs a real read/parse error (#8401). -export type NativeChatReadSessionResult = - | { - messages: NativeChatMessage[] - lifecycle?: NativeChatTurnLifecycle - } - | { error: string; notFound?: true } - -/** Messages appended to a live-tailed transcript since the previous emit. */ -export type NativeChatAppendedMessages = NativeChatMessage[] - -export type NativeChatSubscriptionFrame = - | { - type: 'snapshot' - messages: NativeChatMessage[] - hasMore: boolean - error?: string - lifecycle?: NativeChatTurnLifecycle - } - | { - type: 'replacement' - messages: NativeChatMessage[] - hasMore: boolean - lifecycle?: NativeChatTurnLifecycle - } - | { - type: 'appended' - messages: NativeChatMessage[] - lifecycle?: NativeChatTurnLifecycle - } - -/** Wire payload for the `nativeChat:appended` push channel. */ -export type NativeChatAppendedPayload = { - subscriptionId: string - frame: NativeChatSubscriptionFrame -} - -export type NativeChatSubscribeArgs = { - /** Unique per-caller id, echoed on every append so multiple live panes in - * one renderer don't cross-talk. */ - subscriptionId: string - agent: AgentType - sessionId: string - /** Authoritative transcript path from the agent hook (providerSession). */ - transcriptPath?: string - /** First snapshot size; later readSession calls grow this for pagination. */ - limit?: number -} - -export type NativeChatApi = { - /** Read the on-disk transcript for an agent + session id, windowed to the most recent `limit` - * turns. `transcriptPath` is the hook-reported authoritative path, preferred over the id glob. */ - readSession: ( - agent: AgentType, - sessionId: string, - limit?: number, - transcriptPath?: string - ) => Promise - /** Live-tail a transcript. The first frame is a bounded race-safe snapshot; - * later frames contain only newly appended messages. */ - subscribe: ( - args: NativeChatSubscribeArgs, - onFrame: (frame: NativeChatSubscriptionFrame) => void - ) => () => void -} - -export type AppApi = { - /** Returns the app identity currently exposed to native chrome and the titlebar. */ - getIdentity: () => Promise - /** Returns a URL base for feature-wall assets. In dev this is Vite /@fs; - * in packaged builds this is file:// resources. Renderer appends filenames. */ - getFeatureWallAssetBaseUrl: () => Promise - /** Relaunches the app (app.relaunch() + app.exit(0)) for settings that need a full restart to apply. */ - relaunch: () => Promise - /** Restarts Orca through the normal quit pipeline so daemon-backed terminal - * sessions survive and can reattach after the new process starts. */ - restart: () => Promise - /** Reloads the current app renderer through main so expected renderer - * teardown can be classified before Electron emits process-gone events. */ - reload: () => Promise - /** Stages the renderer's final state synchronously before unload. */ - stageBeforeUnloadSync: (args: { - sessions: { state: WorkspaceSessionState; hostId?: ExecutionHostId }[] - ui: Partial - }) => void - /** Resolves once the last staged checkpoint is durably written; rejects if that - * write failed, so a reload/restart can abort instead of losing the snapshot. */ - awaitBeforeUnloadCheckpoint: () => Promise - /** Resolves when the daemon PTY provider and hook receiver have either - * started or failed open for the first BrowserWindow. */ - awaitFirstWindowStartupServices: () => Promise - /** Reconciles legacy worker authority around persisted terminal reconnect. */ - recoverLegacyWorkerTerminalsForRendererStartup: () => Promise - /** Emits a startup benchmark marker when ORCA_STARTUP_DIAGNOSTICS is enabled. */ - startupDiagnostic: (event: string, details?: Record) => Promise - /** macOS active input mode, or layout ID when no IME is selected (e.g. `com.apple.keylayout.PolishPro`). - * Distinguishes CJK IMEs and Option-layer-composing layouts that look like US QWERTY (issue #1205). - * Returns null on non-Darwin or when the defaults read fails. */ - getKeyboardInputSourceId: () => Promise - /** Updates the macOS Dock unread badge. No-op on Windows/Linux. */ - setUnreadDockBadgeCount: (count: number) => Promise - /** Resolves the launch directory for global Floating Terminal tabs. */ - getFloatingTerminalCwd: (args?: FloatingTerminalCwdRequest) => Promise - /** Resolves Orca's app-owned directory for auto-created Floating Workspace - * markdown notes. */ - getFloatingMarkdownDirectory: () => Promise - /** Opens a native picker for markdown documents, rooted in the floating - * workspace, and authorizes the selected file for editor reads/writes. */ - pickFloatingMarkdownDocument: () => Promise - /** Opens a native directory picker and authorizes the selected directory - * for Floating Workspace markdown file creation. */ - pickFloatingWorkspaceDirectory: () => Promise - /** Persists flag-gated terminal render evidence under app-owned userData. */ - writeTerminalRenderDesyncEvidence: ( - args: WriteTerminalRenderDesyncEvidenceArgs - ) => Promise -} - -/** Panel contribution as surfaced by the main-process plugin service. */ -export type PluginHostPanel = { - id: string - title: string - /** Lucide icon name declared in the plugin manifest. */ - icon?: string - tabKey: `plugin:${string}` -} - -/** `pending` = awaiting (re-)consent; `idle` = enabled, worker not running - * (lazy); `restarting` = waiting for supervised backoff; `errored` = crashed past the restart budget or failed to start; - * `invalid` = unreadable manifest. */ -export type PluginHostStatus = - | 'running' - | 'restarting' - | 'idle' - | 'pending' - | 'disabled' - | 'errored' - | 'invalid' - -/** Wire shape of plugins:list — must stay assignable from the main-process - * projection in src/main/plugins/plugin-list-projection.ts. */ -export type PluginHostListEntry = { - pluginKey: string - consentFingerprint: string | null - name: string - version: string - publisher: string - description?: string - status: PluginHostStatus - needsReconsent: boolean - error?: string - isDev: boolean - official: boolean - bundled: boolean - capabilities: { kind: string; description: string }[] - panels: PluginHostPanel[] - commands: { - id: string - title: string - context: 'global' | 'worktree' - handler: { type: 'built-in'; action: string } | { type: 'worker' } - keybindings: { key: string; when: 'global' | 'worktree' }[] - }[] - hasWorker: boolean - vmRecipes?: { - id: string - name: string - description?: string - commands: { - phase: 'create' | 'suspend' | 'resume' | 'destroy' - command: string - }[] - }[] - restarts: number - blockedByKillList?: { reason: string; advisoryUrl?: string } - source?: { - kind: 'local-path' | 'git' | 'marketplace' | 'bundled' - reference: string - resolvedCommit: string | null - contentHash: string - marketplace?: { reference: string; resolvedCommit: string } - } -} - -export type PluginHostLogLine = { ts: number; level: 'info' | 'warn' | 'error'; line: string } - -export type PluginHostInstallSource = - | { kind: 'local-path'; path: string } - | { kind: 'git'; url: string; ref: string } - -export type PluginHostInstallResult = - | { - ok: true - pluginKey: string - version: string - contentHash: string - consentFingerprint: string - resolvedCommit: string | null - } - | { ok: false; error: string } - -export type PluginMarketplaceHostSourceState = { - id: string - source: PluginMarketplaceGitSource - addedAt: number - marketplace: { - name: string - owner: string - resolvedCommit: string - fetchedAt: number - } | null - stale: boolean - official: boolean - error?: string -} - -export type PluginMarketplaceHostListing = { - marketplaceSourceId: string - marketplaceName: string - marketplaceOwner: string - marketplaceCommit: string - pluginKey: string - source: PluginMarketplaceGitSource - description?: string - categories: string[] - official: boolean - bundled: boolean - blockedByKillList?: { reason: string; advisoryUrl?: string } -} - -export type PluginMarketplaceHostInstallPreview = { - marketplaceSourceId: string - marketplaceName: string - marketplaceOwner: string - marketplaceCommit: string - pluginKey: string - source: PluginMarketplaceGitSource - resolvedCommit: string - contentHash: string - consentFingerprint: string - manifest: PluginManifest - official: boolean - bundled: boolean - blockedByKillList?: { reason: string; advisoryUrl?: string } -} +// Flattens contracts that share one PreloadApi key: an intersection is not type-identical to the flat shape. +type Merged = { [K in keyof T]: T[K] } export type PreloadApi = { app: AppApi - orcaProfiles: { - list: () => Promise - authStatus: () => Promise - createLocal: (args?: CreateLocalOrcaProfileArgs) => Promise - createCloudLinked: ( - args?: CreateCloudLinkedOrcaProfileArgs - ) => Promise - switchProfile: (args: SwitchOrcaProfileArgs) => Promise - transferProject: ( - args: TransferOrcaProfileProjectArgs - ) => Promise - findProjectProfiles: ( - args: FindOrcaProfileProjectsByPathArgs - ) => Promise - connectCurrent: () => Promise - refreshAuth: () => Promise - signOutCurrent: () => Promise - selectOrg: (args: SelectOrcaProfileOrgArgs) => Promise - orgMembersList: ( - args: OrcaProfileOrgMembersListArgs - ) => Promise - orgMemberInvite: ( - args: OrcaProfileOrgMemberInviteArgs - ) => Promise - orgInviteRevoke: ( - args: OrcaProfileOrgInviteRevokeArgs - ) => Promise - orgMemberChangeRole: ( - args: OrcaProfileOrgMemberChangeRoleArgs - ) => Promise - orgMemberRemove: ( - args: OrcaProfileOrgMemberRemoveArgs - ) => Promise - } - platform: { - get: () => { - platform: NodeJS.Platform - osRelease: string - arch: string - /** Login shell or ComSpec when available. */ - shell: string - displayServer: 'wayland' | 'x11' | null - } - } - e2e: { - getConfig: () => E2EConfig - } - repos: { - list: () => Promise - listForExecutionHost?: (args: ListReposForExecutionHostArgs) => Promise - // Why: error union matches the IPC handler's return shape; renderer callers branch on `'error' in result`. - add: (args: { - path: string - kind?: 'git' | 'folder' - }) => Promise<{ repo: Repo } | { error: string }> - remove: (args: { repoId: string }) => Promise - // Forget a project on one execution host only, leaving the same repo id on other hosts intact. - removeForHost: (args: { repoId: string; hostId: string }) => Promise - reorder: (args: { orderedIds: string[] }) => Promise<{ status: 'applied' | 'rejected' }> - reorderForHost: (args: { - orderedIds: string[] - hostId: string - }) => Promise<{ status: 'applied' | 'rejected' }> - update: (args: { - repoId: string - hostId?: ExecutionHostId - updates: Partial< - Pick< - Repo, - | 'displayName' - | 'badgeColor' - | 'repoIcon' - | 'upstream' - | 'hookSettings' - | 'worktreeBaseRef' - | 'worktreeBasePath' - | 'kind' - | 'issueSourcePreference' - | 'externalWorktreeVisibility' - | 'externalWorktreeVisibilityPromptDismissedAt' - | 'externalWorktreeInboxBaselinePaths' - | 'importedExternalWorktreePaths' - | 'agentWorktreeVisibility' - | 'customWorktreeVisibilitySources' - | 'worktreeVisibilitySourcePreferences' - | 'projectGroupId' - | 'projectGroupOrder' - | 'forkSyncMode' - > - > & { - sourceControlAi?: Repo['sourceControlAi'] | null - externalWorktreeDiscoverySuppressedAt?: Repo['externalWorktreeDiscoverySuppressedAt'] | null - } - }) => Promise - pickFolder: () => Promise - pickFolders: () => Promise - pickDirectory: () => Promise - clone: (args: { url: string; destination: string }) => Promise - cloneRemote: (args: { connectionId: string; url: string; destination: string }) => Promise - createRemote: (args: { - connectionId: string - parentPath: string - name: string - kind: 'git' | 'folder' - }) => Promise<{ repo: Repo } | { error: string }> - cloneAbort: () => Promise - // Why: error union matches the IPC handler's return shape; renderer callers branch on `'error' in result`. - addRemote: (args: { - connectionId: string - remotePath: string - displayName?: string - kind?: 'git' | 'folder' - }) => Promise<{ repo: Repo } | { error: string }> - // Why: error union matches the IPC handler's return shape; renderer callers branch on `'error' in result`. - create: (args: { - parentPath: string - name: string - kind: 'git' | 'folder' - }) => Promise<{ repo: Repo } | { error: string }> - isGitAvailable: () => Promise - getDefaultCreateProjectParent: () => Promise - onCloneProgress: (callback: (data: { phase: string; percent: number }) => void) => () => void - getGitUsername: (args: { repoId: string }) => Promise - getBaseRefDefault: (args: { - repoId: string - hostId?: ExecutionHostId - }) => Promise - searchBaseRefs: (args: { - repoId: string - query: string - limit?: number - hostId?: ExecutionHostId - }) => Promise - searchBaseRefDetails: (args: { - repoId: string - query: string - limit?: number - hostId?: ExecutionHostId - }) => Promise - onChanged: (callback: () => void) => () => void - } - projects: { - list: () => Promise - update: (args: ProjectUpdateArgs) => Promise - listHostSetups: () => Promise - createHostSetup: (args: ProjectHostSetupCreateArgs) => Promise - setupExistingFolder: ( - args: ProjectHostSetupExistingFolderArgs - ) => Promise - updateHostSetup: (args: ProjectHostSetupUpdateArgs) => Promise - deleteHostSetup: (args: ProjectHostSetupDeleteArgs) => Promise - } - projectGroups: { - list: () => Promise - create: (args: { - name: string - parentPath?: string | null - connectionId?: string | null - parentGroupId?: string | null - createdFrom?: ProjectGroup['createdFrom'] - }) => Promise - update: (args: { - groupId: string - updates: Partial> - }) => Promise - delete: (args: { groupId: string }) => Promise - moveProject: (args: { - projectId: string - groupId: string | null - order?: number - }) => Promise - scanNested: (args: { - path: string - connectionId?: string - scanId?: string - options?: Record - }) => Promise - cancelNestedScan: (args: { scanId: string }) => Promise - onNestedScanProgress: ( - callback: (data: { scanId: string; scan: NestedRepoScanResult }) => void - ) => () => void - importNested: (args: { - parentPath: string - groupName: string - projectPaths: string[] - connectionId?: string - scanId?: string - mode: ProjectGroupImportMode - }) => Promise - } - folderWorkspaces: { - list: () => Promise - getPathStatus: (args: FolderWorkspacePathStatusRequest) => Promise - create: (args: { - projectGroupId: string - name?: string - folderPath?: string | null - connectionId?: string | null - linkedTask?: FolderWorkspace['linkedTask'] - createdWithAgent?: FolderWorkspace['createdWithAgent'] - pendingFirstAgentMessageRename?: boolean - }) => Promise - update: (args: { - folderWorkspaceId: string - updates: Partial< - Pick< - FolderWorkspace, - | 'name' - | 'folderPath' - | 'linkedTask' - | 'comment' - | 'isArchived' - | 'isUnread' - | 'isPinned' - | 'sortOrder' - | 'manualOrder' - | 'workspaceStatus' - | 'createdWithAgent' - | 'pendingFirstAgentMessageRename' - | 'firstAgentMessageRenameError' - | 'lastActivityAt' - | 'diffComments' - > - > - }) => Promise - delete: (args: { folderWorkspaceId: string }) => Promise - } - sparsePresets: { - list: (args: { repoId: string }) => Promise - save: (args: { - repoId: string - id?: string - name: string - directories: string[] - }) => Promise - remove: (args: { repoId: string; presetId: string }) => Promise - onChanged: (callback: (data: { repoId: string }) => void) => () => void - } - worktrees: { - list: (args: { repoId: string }) => Promise - listDetected: { - ( - args: ListDetectedWorktreesArgs - ): Promise - (args: LegacyDetectedWorktreeRequest): Promise - } - listKnownForExecutionHost?: ( - args: ListKnownWorktreesForExecutionHostArgs - ) => Promise - /** Retires the persisted metadata an authoritative scan proved gone, so it stops feeding the read above. */ - forgetRemovedForExecutionHost?: ( - args: ForgetRemovedWorktreesForExecutionHostArgs - ) => Promise - cancelListDetected?: (args: { providerRequestId: ProviderRequestId }) => Promise - listAll: () => Promise - create: (args: CreateWorktreeArgs) => Promise - adoptProvisionedRoot: (args: AdoptProvisionedRootArgs) => Promise - /** Two-phase progress for a background `create`, correlated by `creationId`. The remote/runtime - * create path emits nothing, so the surface falls back to an indeterminate spinner. */ - onCreateProgress: ( - callback: (data: { creationId?: string; phase: 'fetching' | 'creating' }) => void - ) => () => void - prefetchCreateBase: (args: { repoId: string; baseBranch?: string }) => Promise - resolvePrBase: (args: { - repoId: string - prNumber: number - headRefName?: string - baseRefName?: string - isCrossRepository?: boolean - }) => Promise - /** GitLab parallel of resolvePrBase. For same-project MRs returns - * `/`; for fork MRs fetches - * refs/merge-requests//head and returns the SHA. */ - resolveMrBase: (args: { - repoId: string - mrIid: number - sourceBranch?: string - targetBranch?: string - isCrossRepository?: boolean - }) => Promise< - | { baseBranch: string; compareBaseRef?: string; pushTarget?: GitPushTarget } - | { error: string } - > - remove: (args: { - worktreeId: string - hostId?: ExecutionHostId - force?: boolean - // Why (#11960): distinct from `force`, which the plain Delete confirmation - // already sets to skip the dirty-file prompt. Only an explicit Force Delete - // may waive the proof that every PTY stopped. - allowUnverifiedPtyStop?: boolean - skipArchive?: boolean - }) => 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 - }) => Promise - forceDeletePreservedBranch: (args: { - worktreeId: string - branchName: string - expectedHead: string - hostId?: ExecutionHostId - }) => Promise - updateMeta: (args: { worktreeId: string; updates: Partial }) => Promise - listLineage: () => Promise<{ - lineage: Record - workspaceLineage?: Record - }> - listLineageForHost?: (args: ListDesktopLineageForHostArgs) => Promise - updateLineage: (args: { - worktreeId: string - parentWorktreeId?: string - noParent?: boolean - }) => Promise - persistSortOrder: (args: { orderedIds: string[] }) => Promise - /** Full CLI output of the last branch auto-rename generation failure, held - * in main memory only — null after a restart or once the failure clears. */ - getBranchRenameFailureOutput: (args: { worktreeId: string }) => Promise - onChanged: (callback: (data: { repoId: string }) => void) => () => void - onGitStatusMetadataChanged: (callback: (data: { repoId: string }) => void) => () => void - onHeadIdentitiesChanged: ( - callback: (data: { repoId: string; identities: WorktreeHeadIdentity[] }) => void - ) => () => void - onBaseStatus: (callback: (data: WorktreeBaseStatusEvent) => void) => () => void - onRemoteBranchConflict: ( - callback: (data: WorktreeRemoteBranchConflictEvent) => void - ) => () => void - } - workspaceCleanup: { - scan: ( - args?: WorkspaceCleanupScanArgs, - onProgress?: (progress: WorkspaceCleanupScanProgress) => void - ) => Promise - dismiss: (args: WorkspaceCleanupDismissArgs) => Promise - clearDismissals: () => Promise - hasKillableLocalProcesses: ( - args: WorkspaceCleanupLocalProcessArgs - ) => Promise - } - workspaceSpace: { - analyze: () => Promise - cancel: () => Promise - onProgress: (callback: (progress: WorkspaceSpaceScanProgress) => void) => () => void - } - workspacePorts: { - scan: (args: WorkspacePortScanRequest) => Promise - kill: (args: WorkspacePortKillRequest) => Promise - onAdvertisedUrlChanged: ( - callback: (event: WorkspacePortAdvertisedUrlChangedEvent) => void - ) => () => void - } - pty: { - spawn: (opts: { - cols: number - rows: number - cwd?: string - cwdFallback?: 'worktree' - env?: Record - envToDelete?: string[] - command?: string - commandDelivery?: 'renderer' | 'provider' - launchConfig?: SleepingAgentLaunchConfig - resumeProviderSession?: AgentProviderSessionMetadata - launchToken?: string - launchAgent?: TuiAgent - startupCommandDelivery?: StartupCommandDelivery - connectionId?: string | null - worktreeId?: string - sessionId?: string - // Why: lets a single tab open in a different shell than the user's default. - shellOverride?: string - projectRuntime?: ProjectExecutionRuntimeResolution - terminalColorQueryReplies?: { foreground?: string; background?: string } - // Why: mark the PTY hidden before its first byte so the delivery gate owns spawn-time queries (terminal-query-authority.md §races). - initiallyHidden?: boolean - // Why: main sync-flushes the (worktreeId,tabId,leafId→ptyId) binding before pty:spawn returns to close a SIGKILL race (INVESTIGATION.md). - tabId?: string - leafId?: string - // Why: main fires `agent_started` only on spawn success, so launch metadata rides this field (telemetry-plan.md §Agent launch semantics). - telemetry?: { agent_kind: AgentKind; launch_source: LaunchSource; request_kind: RequestKind } - }) => Promise<{ - id: string - launchAgent?: TuiAgent - launchConfig?: SleepingAgentLaunchConfig - snapshot?: string - snapshotCols?: number - snapshotRows?: number - snapshotPrefixAnsi?: string - snapshotFrameAnsi?: string - snapshotFrameRestoreAnsi?: string - snapshotKittyKeyboardFlags?: number - snapshotSeq?: number - isReattach?: boolean - isAlternateScreen?: boolean - replay?: string - sessionExpired?: boolean - coldRestore?: { scrollback: string; cwd: string; cols?: number; rows?: number } - startupCwdFallback?: { kind: 'worktree'; cwd: string } - agentResumeUnavailable?: true - }> - write: (id: string, data: string) => void - writeAccepted: (id: string, data: string) => Promise - onWriteUnavailable?: (callback: (payload: { id: string }) => void) => () => void - resize: (id: string, cols: number, rows: number) => void - claimViewport: (id: string, cols: number, rows: number) => void - reportGeometry: (id: string, cols: number, rows: number) => void - signal: (id: string, signal: string) => void - clearBuffer: (id: string) => void - kill: (id: string, opts?: { keepHistory?: boolean }) => Promise - ackColdRestore: (id: string) => void - ackData: (id: string, charCount: number, processedChars?: number) => void - onDeliveryResyncRequest: (callback: (payload: { requestId: number }) => void) => () => void - respondDeliveryResync: (payload: { - requestId: number - processedCharsByPty: Record - }) => void - /** Renderer-initiated delivery health/heal lane over invoke — reaches main - * even when every main→renderer push channel is dead (field wedge). */ - reportRendererDeliveryState: ( - report: PtyRendererDeliveryStateReport - ) => Promise - /** Live pty:data listener count on the preload emitter (sync) — heal-time - * discriminator between a detached listener and a dead channel. */ - getPtyDataListenerCount: () => number - /** One-shot signal that this page's pty:data dispatcher is registered, so - * main can release sends held during the load/reload boot window. */ - rendererDispatcherReady: () => void - setActiveRendererPty: (id: string, active: boolean) => void - setRendererPtyVisible: (id: string, visible: boolean) => void - /** Hidden-delivery gate (Phase 4): hidden=true lets main drop renderer - * byte delivery after model ingestion; reveal restores from snapshots. */ - setHiddenRendererPty: (id: string, hidden: boolean) => void - /** Ref-counted-on-the-renderer delivery-interest signal that suppresses - * the hidden-delivery gate while any raw-byte consumer is registered. */ - setPtyDeliveryInterest: (id: string, interested: boolean) => void - /** View-attribute bridge (Phase 5 slice 2): app-global composed terminal - * appearance push backing main's hidden-PTY OSC/DSR color replies. */ - publishTerminalViewAttributes: (attributes: TerminalViewAttributes) => void - hasChildProcesses: (id: string) => Promise - getForegroundProcess: (id: string) => Promise - inspectProcess: (id: string) => Promise<{ - foregroundProcess: string | null - hasChildProcesses: boolean - unavailable?: true - }> - confirmForegroundProcess: (id: string) => Promise - getCwd: (id: string) => Promise - getSize: (id: string) => Promise<{ cols: number; rows: number } | null> - listSessions: () => Promise - getAuthoritativeBufferSnapshotCapabilities?: ( - ids: string[] - ) => Promise<{ id: string; authoritative: boolean | null }[]> - hasPty: (id: string) => Promise - getMainBufferSnapshot: ( - id: string, - opts?: { scrollbackRows?: number } - ) => Promise<{ - data: string - frameRestoreAnsi?: string - cols: number - rows: number - cwd?: string | null - seq?: number - /** Start of main's pending renderer-delivery queue at snapshot time - * (equals `seq` when empty) — bounds the renderer's post-restore - * duplicate window. */ - pendingDeliveryStartSeq?: number - source?: 'headless' | 'renderer' - alternateScreen?: boolean - /** Authoritative normal buffer paired with an alternate-screen frame. */ - scrollbackAnsi?: string - /** Trailing incomplete escape the emulator ingested; the restorer must - * write it after its post-replay resets, last before live chunks. */ - pendingEscapeTailAnsi?: string - /** Effective kitty flags the snapshot owner proved at `seq`. Absent means - * unknown; consumers must not turn that into a known `0`. */ - kittyKeyboardFlags?: number - } | null> - getRendererDeliveryDebugSnapshot: () => Promise<{ - pendingPtyCount: number - pendingChars: number - maxPendingCharsByPty: number - rendererInFlightPtyCount: number - rendererInFlightChars: number - maxRendererInFlightCharsByPty: number - activeRendererPtyCount: number - flushScheduled: boolean - peakPendingChars: number - peakMaxPendingCharsByPty: number - peakRendererInFlightChars: number - peakMaxRendererInFlightCharsByPty: number - ackGatedFlushSkipCount: number - hiddenDeliveryGatedPtyCount: number - hiddenDeliveryGatedVisiblePtyCount: number - hiddenDeliveryGatedActivePtyCount: number - deliveryInterestPtyCount: number - hiddenDeliveryDroppedChars: number - hiddenDeliveryDroppedChunks: number - pendingDroppedChars: number - diagnostics: PtyMainDeliveryDiagnostics - rendererLifecycleResetCount: number - lastLifecycleResetClearedChars: number - rendererPtyDispatcherReady: boolean - rendererDispatcherReadyForcedCount: number - }> - resetRendererDeliveryDebug: () => Promise - onData: ( - callback: (data: { - id: string - data: string - seq?: number - rawLength?: number - transformed?: boolean - background?: boolean - droppedOutput?: boolean - }) => void - ) => () => void - onReplay: (callback: (data: { id: string; data: string }) => void) => () => void - /** Out-of-band main→renderer signal that renderer-bound bytes were - * dropped (hidden-delivery gate / pending cap); the pane restores from - * the model snapshot. Never delivered in-band on pty:data. */ - onModelRestoreNeeded: (callback: (event: PtyModelRestoreNeededEvent) => void) => () => void - /** Batched derived side-effect facts for PTYs whose bytes transit local - * main. */ - onSideEffect: (callback: (batch: TerminalSideEffectBatch) => void) => () => void - /** Title-only replay snapshot for (re)attach; attention facts never replay. */ - getSideEffectSnapshot: (id: string) => Promise - onExit: ( - callback: (data: { id: string; code: number; preserveRendererBinding?: boolean }) => void - ) => () => void - onSpawned: (callback: (data: { id: string }) => void) => () => void - onSerializeBufferRequest: ( - callback: (data: { - requestId: string - ptyId: string - opts?: { scrollbackRows?: number; altScreenForcesZeroRows?: boolean } - }) => void - ) => () => void - onClearBufferRequest: (callback: (data: { ptyId: string }) => void) => () => void - sendSerializedBuffer: ( - requestId: string, - snapshot: { - data: string - cols: number - rows: number - seq?: number - lastTitle?: string - kittyKeyboardFlags?: number - } | null - ) => void - declarePendingPaneSerializer: (paneKey: string) => Promise - settlePaneSerializer: (paneKey: string, gen: number) => Promise - clearPendingPaneSerializer: (paneKey: string, gen: number) => Promise - reportRendererSerializerReady?: (ptyId: string) => Promise - management: PtyManagementApi - } - feedback: { - submit: (args: { - feedback: string - submitAnonymously?: boolean - githubLogin: string | null - githubEmail: string | null - images?: { contentType: string; data: Uint8Array }[] - }) => Promise< - { ok: true; imagesDelivered?: boolean } | { ok: false; status: number | null; error: string } - > - } - crashReports: { - getLatestPending: () => Promise - getLatestReport: () => Promise - dismiss: (args: { reportId: string }) => Promise - recordRendererError: ( - args: ReactErrorBoundaryReportArgs - ) => Promise - recordBreadcrumb: (args: { name: string; data?: CrashReportBreadcrumbData }) => void - submit: (args: CrashReportSubmitArgs) => Promise - copyLatestDiagnostics: ( - args?: CrashReportCopyDiagnosticsArgs - ) => Promise<{ ok: true } | { ok: false; error: string }> - /** Exact V8/Blink heap sizes; null when the runtime withholds them. */ - readHeapStatistics: () => RendererHeapStatistics | null - } + orcaProfiles: OrcaProfileApi + platform: PlatformApi + e2e: E2EApi + repos: RepositoryApi + projects: ProjectsApi + projectGroups: ProjectGroupsApi + folderWorkspaces: FolderWorkspacesApi + sparsePresets: SparsePresetsApi + worktrees: WorktreeApi + workspaceCleanup: WorkspaceCleanupApi + workspaceSpace: WorkspaceSpaceApi + workspacePorts: WorkspacePortsApi + pty: PtyApi + feedback: FeedbackApi + crashReports: CrashReportsApi export: ExportApi - gh: { - viewer: () => Promise - repoSlug: (args: { - repoPath: string - repoId?: string - }) => Promise<{ owner: string; repo: string; host?: string } | null> - repoUpstream: (args: { - repoPath: string - repoId?: string - }) => Promise<{ owner: string; repo: string; host?: string } | null> - prForBranch: (args: { - repoPath: string - repoId?: string - branch: string - linkedPRNumber?: number | null - fallbackPRNumber?: number | null - acceptMergedFallbackPR?: boolean - currentHeadOid?: string | null - }) => Promise - refreshPRNow: (args: { candidate: GitHubPRRefreshCandidate }) => Promise - enqueuePRRefresh: (args: { - candidate: GitHubPRRefreshCandidate - reason: GitHubPRRefreshReason - priority?: number - }) => Promise - reportVisiblePRRefreshCandidates: (args: { - candidates: GitHubPRRefreshCandidate[] - generation: number - }) => Promise - onPRRefreshEvent: (callback: (event: GitHubPRRefreshEvent) => void) => () => void - issue: (args: { - repoPath: string - repoId?: string - sourceContext?: TaskSourceContext | null - number: number - }) => Promise - workItem: (args: { - repoPath: string - repoId?: string - sourceContext?: TaskSourceContext | null - number: number - type?: 'issue' | 'pr' - }) => Promise | null> - workItemByOwnerRepo: (args: { - repoPath: string - repoId?: string - owner: string - repo: string - host?: string - number: number - type: 'issue' | 'pr' - }) => Promise | null> - workItemDetails: ( - args: GitHubRepoSelectorArgs & { - number: number - type?: 'issue' | 'pr' - } - ) => Promise - notifyWorkItemMutated: (args: { - repoPath: string - repoId?: string - type: 'issue' | 'pr' - number: number - }) => Promise - prFileContents: ( - args: GitHubRepoSelectorArgs & { - prNumber: number - prRepo?: GitHubOwnerRepo | null - path: string - oldPath?: string - status: GitHubPRFile['status'] - headSha: string - baseSha: string - } - ) => Promise - listIssues: (args: { - repoPath: string - repoId?: string - limit?: number - }) => Promise - createIssue: (args: { - repoPath: string - repoId?: string - sourceContext?: TaskSourceContext | null - title: string - body: string - labels?: string[] - assignees?: string[] - }) => Promise - countWorkItems: (args: { repoPath: string; repoId?: string; query?: string }) => Promise - listWorkItems: (args: { - repoPath: string - repoId?: string - limit?: number - query?: string - page?: number - noCache?: boolean - }) => Promise>> - prChecks: ( - args: GitHubRepoSelectorArgs & { - prNumber: number - headSha?: string - prRepo?: GitHubOwnerRepo | null - noCache?: boolean - } - ) => Promise - prCheckDetails: (args: { - repoPath: string - repoId?: string - sourceContext?: TaskSourceContext | null - checkRunId?: number - workflowRunId?: number - checkName?: string - url?: string | null - prRepo?: GitHubOwnerRepo | null - }) => Promise - rerunPRChecks: ( - args: GitHubRepoSelectorArgs & { - prNumber: number - headSha?: string - failedOnly?: boolean - prRepo?: GitHubOwnerRepo | null - } - ) => Promise<{ ok: true; count: number } | { ok: false; error: string }> - prComments: (args: { - repoPath: string - repoId?: string - sourceContext?: TaskSourceContext | null - prNumber: number - prRepo?: GitHubOwnerRepo | null - noCache?: boolean - }) => Promise - setPRCommentReaction: (args: { - repoPath: string - repoId?: string - sourceContext?: TaskSourceContext | null - reactionSubjectId: string - content: GitHubReactionContent - reacted: boolean - prRepo?: GitHubOwnerRepo | null - }) => Promise - resolveReviewThread: (args: { - repoPath: string - repoId?: string - sourceContext?: TaskSourceContext | null - threadId: string - resolve: boolean - prRepo?: GitHubOwnerRepo | null - }) => Promise - setPRFileViewed: ( - args: GitHubRepoSelectorArgs & { - prNumber: number - prRepo?: GitHubOwnerRepo | null - pullRequestId: string - path: string - viewed: boolean - } - ) => Promise - updatePRTitle: (args: { - repoPath: string - repoId?: string - prNumber: number - title: string - prRepo?: GitHubOwnerRepo | null - }) => Promise - mergePR: ( - args: GitHubRepoSelectorArgs & { - prNumber: number - method?: 'merge' | 'squash' | 'rebase' - prRepo?: GitHubOwnerRepo | null - } - ) => Promise<{ ok: true } | { ok: false; error: string }> - setPRAutoMerge: ( - args: GitHubRepoSelectorArgs & { - prNumber: number - enabled: boolean - method?: 'merge' | 'squash' | 'rebase' - prRepo?: GitHubOwnerRepo | null - } - ) => Promise<{ ok: true } | { ok: false; error: string }> - updatePRState: ( - args: GitHubRepoSelectorArgs & { - prNumber: number - updates: { state: 'open' | 'closed' } - prRepo?: GitHubOwnerRepo | null - } - ) => Promise<{ ok: true } | { ok: false; error: string }> - requestPRReviewers: ( - args: GitHubRepoSelectorArgs & { - prNumber: number - reviewers: string[] - prRepo?: GitHubOwnerRepo | null - } - ) => Promise<{ ok: true } | { ok: false; error: string }> - removePRReviewers: ( - args: GitHubRepoSelectorArgs & { - prNumber: number - reviewers: string[] - prRepo?: GitHubOwnerRepo | null - } - ) => Promise<{ ok: true } | { ok: false; error: string }> - updateIssue: ( - args: GitHubRepoSelectorArgs & { - number: number - updates: GitHubIssueUpdate - } - ) => Promise<{ ok: true } | { ok: false; error: string }> - addIssueComment: ( - args: GitHubRepoSelectorArgs & { - number: number - body: string - /** Why: scopes the cross-window cache invalidation so a PR and issue sharing the same number don't evict each other. */ - type?: 'issue' | 'pr' - prRepo?: GitHubOwnerRepo | null - } - ) => Promise - addPRReviewCommentReply: ( - args: GitHubRepoSelectorArgs & { - prNumber: number - commentId: number - body: string - threadId?: string - path?: string - line?: number - prRepo?: GitHubOwnerRepo | null - } - ) => Promise - addPRReviewComment: ( - args: GitHubPRReviewCommentInput & { - repoId?: string - sourceContext?: TaskSourceContext | null - } - ) => Promise - listLabels: (args: { - repoPath: string - repoId?: string - sourceContext?: TaskSourceContext | null - }) => Promise - listAssignableUsers: (args: { - repoPath: string - repoId?: string - sourceContext?: TaskSourceContext | null - }) => Promise - /** Subscribe to local-mutation broadcasts so the work-item-drawer cache can invalidate across windows. Returns an unsubscribe. */ - onWorkItemMutated: ( - callback: (payload: { - repoPath: string - repoId?: string - type: 'issue' | 'pr' - number: number - }) => void - ) => () => void - checkOrcaStarred: () => Promise - starOrca: (source: AppStarSource) => Promise - /** - * GitHub API rate-limit snapshot. Does NOT consume quota (the - * `rate_limit` endpoint is exempt). Cached 30s server-side — pass - * `force: true` to bust after a known-expensive op. - */ - rateLimit: (args?: { force?: boolean }) => Promise - /** Explains scope_missing ProjectV2 failures — notably a shell `GITHUB_TOKEN` shadowing the keyring credential, where `gh auth refresh` is a no-op. */ - diagnoseAuth: (args?: { host?: string }) => Promise - // ── ProjectV2 (GitHub Projects) ───────────────────────────────── - listAccessibleProjects: ( - args?: ListAccessibleProjectsArgs - ) => Promise - resolveProjectRef: (args: ResolveProjectRefArgs) => Promise - listProjectViews: (args: ListProjectViewsArgs) => Promise - getProjectViewTable: (args: GetProjectViewTableArgs) => Promise - projectWorkItemDetailsBySlug: ( - args: ProjectWorkItemDetailsBySlugArgs - ) => Promise - updateProjectItemField: ( - args: UpdateProjectItemFieldArgs - ) => Promise - clearProjectItemField: (args: ClearProjectItemFieldArgs) => Promise - updateIssueBySlug: (args: UpdateIssueBySlugArgs) => Promise - updatePullRequestBySlug: ( - args: UpdatePullRequestBySlugArgs - ) => Promise - addIssueCommentBySlug: ( - args: AddIssueCommentBySlugArgs - ) => Promise - updateIssueCommentBySlug: ( - args: UpdateIssueCommentBySlugArgs - ) => Promise - deleteIssueCommentBySlug: ( - args: DeleteIssueCommentBySlugArgs - ) => Promise - listLabelsBySlug: (args: ListLabelsBySlugArgs) => Promise - listAssignableUsersBySlug: ( - args: ListAssignableUsersBySlugArgs - ) => Promise - listIssueTypesBySlug: (args: ListIssueTypesBySlugArgs) => Promise - updateIssueTypeBySlug: (args: UpdateIssueTypeBySlugArgs) => Promise - } - hostedReview: { - forBranch: (args: HostedReviewForBranchArgs) => Promise - getCreationEligibility: ( - args: HostedReviewCreationEligibilityArgs - ) => Promise - create: (args: CreateHostedReviewArgs) => Promise - createStacked: (args: CreateStackedHostedReviewArgs) => Promise - } - // ── GitLab — parallel to gh, MR/issue surface only in v1 ──────── - // Shapes mirror gh.* except where GitLab's API differs (MR states, host-qualified project path, `glab api -i` paging). - gl: { - viewer: () => Promise - diagnoseAuth: () => Promise - rateLimit: (args?: { - force?: boolean - host?: string | null - }) => Promise - projectSlug: (args: GitLabRepoSelectorArgs) => Promise - mrForBranch: ( - args: GitLabRepoSelectorArgs & { - branch: string - linkedMRIid?: number | null - } - ) => Promise - mr: (args: GitLabRepoSelectorArgs & { iid: number }) => Promise - listMRs: ( - args: GitLabRepoSelectorArgs & { - state?: MRListState - page?: number - perPage?: number - query?: string - } - ) => Promise - /** Combined MR + issue list filtered by state. Issues are skipped - * when state is 'merged' (issues don't merge). */ - listWorkItems: ( - args: GitLabRepoSelectorArgs & { - state?: MRListState - page?: number - perPage?: number - query?: string - } - ) => Promise - issue: (args: GitLabRepoSelectorArgs & { number: number }) => Promise - listIssues: ( - args: GitLabRepoSelectorArgs & { - state?: 'opened' | 'closed' | 'all' - assignee?: string - limit?: number - } - ) => Promise<{ items: GitLabWorkItem[]; error?: ClassifiedError }> - createIssue: ( - args: GitLabRepoSelectorArgs & { - title: string - body: string - } - ) => Promise<{ ok: true; number: number; url: string } | { ok: false; error: string }> - updateIssue: ( - args: GitLabRepoSelectorArgs & { - number: number - updates: GitLabIssueUpdate - } - ) => Promise<{ ok: true } | { ok: false; error: string }> - addIssueComment: ( - args: GitLabRepoSelectorArgs & { - number: number - body: string - } - ) => Promise - listLabels: (args: GitLabRepoSelectorArgs) => Promise - listAssignableUsers: (args: GitLabRepoSelectorArgs) => Promise - /** Cross-project user-scoped todos (gitlab.com/dashboard/todos). */ - todos: (args: GitLabRepoSelectorArgs) => Promise - /** Aggregated dialog payload — body + discussions + pipeline jobs. */ - workItemDetails: ( - args: GitLabRepoSelectorArgs & { - iid: number - type: 'issue' | 'mr' - } - ) => Promise - closeMR: ( - args: GitLabRepoSelectorArgs & { - iid: number - } - ) => Promise<{ ok: true } | { ok: false; error: string }> - reopenMR: ( - args: GitLabRepoSelectorArgs & { - iid: number - } - ) => Promise<{ ok: true } | { ok: false; error: string }> - mergeMR: ( - args: GitLabRepoSelectorArgs & { - iid: number - method?: 'merge' | 'squash' | 'rebase' - } - ) => Promise<{ ok: true } | { ok: false; error: string }> - updateMR: ( - args: GitLabRepoSelectorArgs & { - iid: number - updates: GitLabMRUpdate - } - ) => Promise<{ ok: true } | { ok: false; error: string }> - updateMRReviewers: ( - args: GitLabRepoSelectorArgs & { - iid: number - reviewerIds: number[] - projectRef?: GitLabProjectRef | null - } - ) => Promise - addMRComment: ( - args: GitLabRepoSelectorArgs & { - iid: number - body: string - } - ) => Promise - addMRInlineComment: ( - args: GitLabRepoSelectorArgs & { - iid: number - input: GitLabMRInlineCommentInput - projectRef?: GitLabProjectRef | null - } - ) => Promise - resolveMRDiscussion: ( - args: GitLabRepoSelectorArgs & { - iid: number - discussionId: string - resolved: boolean - } - ) => Promise - jobTrace: ( - args: GitLabRepoSelectorArgs & { - jobId: number - projectRef?: GitLabProjectRef | null - /** Bound the trace in main to a readable excerpt (see gitLabJobTraceToLogExcerpt). */ - logExcerpt?: boolean - } - ) => Promise - retryJob: ( - args: GitLabRepoSelectorArgs & { - jobId: number - projectRef?: GitLabProjectRef | null - } - ) => Promise - workItemByPath: ( - args: GitLabRepoSelectorArgs & { - host: string - path: string - iid: number - type: 'issue' | 'mr' - } - ) => Promise | null> - } - bitbucket: { - connect: ( - args: BitbucketConnectArgs - ) => Promise<{ ok: true; account: string | null } | { ok: false; error: string }> - disconnect: () => Promise - status: () => Promise - } - linear: { - connect: (args: { - apiKey: string - }) => Promise<{ ok: true; viewer: LinearViewer } | { ok: false; error: string }> - disconnect: (args?: { workspaceId?: string }) => Promise - selectWorkspace: (args: { - workspaceId: LinearWorkspaceSelection - }) => Promise - status: () => Promise - testConnection: (args?: { - workspaceId?: string - }) => Promise<{ ok: true; viewer: LinearViewer } | { ok: false; error: string }> - searchIssues: (args: { - query: string - limit?: number - workspaceId?: LinearWorkspaceSelection - }) => Promise - listIssues: (args?: { - filter?: 'assigned' | 'created' | 'all' | 'completed' - limit?: number - workspaceId?: LinearWorkspaceSelection - attributeFilter?: LinearIssueAttributeFilter - }) => Promise> - createIssue: (args: { - teamId: string - title: string - description?: string - workspaceId?: string - parentIssueId?: string - projectId?: string | null - stateId?: string - priority?: number - assigneeId?: string | null - labelIds?: string[] - }) => Promise< - | { ok: true; id: string; identifier: string; title: string; url: string } - | { ok: false; error: string } - > - getIssue: (args: { id: string; workspaceId?: string }) => Promise - updateIssue: (args: { - id: string - updates: LinearIssueUpdate - workspaceId?: string - }) => Promise<{ ok: true } | { ok: false; error: string }> - addIssueComment: (args: { - issueId: string - body: string - workspaceId?: string - }) => Promise<{ ok: true; id: string } | { ok: false; error: string }> - issueComments: (args: { issueId: string; workspaceId?: string }) => Promise - listTeams: (args?: { workspaceId?: LinearWorkspaceSelection }) => Promise - listProjects: (args?: { - query?: string - limit?: number - workspaceId?: LinearWorkspaceSelection - force?: boolean - }) => Promise> - createProject: (args: { - name: string - description?: string - content?: string - teamIds: string[] - workspaceId?: string - leadId?: string | null - memberIds?: string[] - labelIds?: string[] - priority?: number - startDate?: string - targetDate?: string - }) => Promise<{ ok: true; project: LinearProjectDetail } | { ok: false; error: string }> - getProject: (args: { - id: string - workspaceId: string - force?: boolean - }) => Promise - listProjectIssues: (args: { - projectId: string - limit?: number - workspaceId: string - force?: boolean - }) => Promise> - listCustomViews: (args: { - model: LinearCustomViewModel - limit?: number - workspaceId?: LinearWorkspaceSelection - force?: boolean - }) => Promise> - getCustomView: (args: { - viewId: string - model: LinearCustomViewModel - workspaceId: string - force?: boolean - }) => Promise - listCustomViewIssues: (args: { - viewId: string - limit?: number - workspaceId: string - force?: boolean - }) => Promise> - listCustomViewProjects: (args: { - viewId: string - limit?: number - workspaceId: string - force?: boolean - }) => Promise> - teamStates: (args: { teamId: string; workspaceId?: string }) => Promise - teamLabels: (args: { teamId: string; workspaceId?: string }) => Promise - teamMembers: (args: { teamId: string; workspaceId?: string }) => Promise - } - jira: { - connect: (args: { - siteUrl: string - email: string - apiToken: string - authType?: 'cloud' | 'server' - }) => Promise<{ ok: true; viewer: JiraViewer } | { ok: false; error: string }> - disconnect: (args?: { siteId?: string }) => Promise - selectSite: (args: { siteId: JiraSiteSelection }) => Promise - status: () => Promise - readStatus: () => Promise - testConnection: (args?: { - siteId?: string - }) => Promise<{ ok: true; viewer: JiraViewer } | { ok: false; error: string }> - searchIssues: (args: { - jql: string - limit?: number - siteId?: JiraSiteSelection - requestId?: string - }) => Promise - cancelSearchIssues: (args: { requestId: string }) => Promise - listIssues: (args?: { - filter?: JiraIssueFilter - limit?: number - siteId?: JiraSiteSelection - }) => Promise - getIssue: (args: { key: string; siteId?: string }) => Promise - lookupIssueSummary: (args: { - key: string - siteId: string - requestId?: string - }) => Promise - cancelIssueSummary: (args: { requestId: string }) => Promise - createIssue: ( - args: JiraCreateIssueArgs - ) => Promise<{ ok: true; id: string; key: string; url: string } | { ok: false; error: string }> - updateIssue: (args: { - key: string - updates: JiraIssueUpdate - siteId?: string - }) => Promise<{ ok: true } | { ok: false; error: string }> - addIssueComment: (args: { - key: string - body: string - siteId?: string - }) => Promise<{ ok: true; id: string } | { ok: false; error: string }> - issueComments: (args: { key: string; siteId?: string }) => Promise - listProjects: (args?: { siteId?: JiraSiteSelection }) => Promise - listIssueTypes: (args: { projectIdOrKey: string; siteId?: string }) => Promise - listCreateFields: (args: { - projectIdOrKey: string - issueTypeId: string - siteId?: string - }) => Promise - listPriorities: (args?: { siteId?: string }) => Promise - listAssignableUsers: (args: { - key: string - query?: string - siteId?: string - }) => Promise - listTransitions: (args: { key: string; siteId?: string }) => Promise - getProjectStatusOrder: (args: { - projectKey: string - siteId?: string - }) => Promise - } - starNag: { - onShow: ( - callback: (payload?: { mode?: 'gh' | 'web'; surface?: 'card' | 'toast' }) => void - ) => () => void - onHide: (callback: () => void) => () => void - dismiss: () => Promise - later: () => Promise - complete: () => Promise - disable: () => Promise - openWeb: () => Promise - starOrca: () => Promise - forceShow: () => Promise - agentValueMoment: () => Promise<{ status: 'ready'; mode: 'gh' | 'web' } | { status: 'skipped' }> - showAgentValueMoment: () => Promise - onboardingCompleted: () => Promise - } - /** Fire-and-forget track. Loose IPC typing on purpose — the main-side validator enforces; - * renderer sites should import `track()` from lib/telemetry.ts, not reach here. */ - telemetryTrack: (name: string, props: Record) => Promise - /** Flip the persisted opt-in preference. Subject to a per-session - * consent-mutation rate limit on the main side (≤5/session). */ - telemetrySetOptIn: (optedIn: boolean) => Promise - /** Diagnostic file controls (telemetry-error-tracking.md §User controls). Main does the FS/network - * work and retains upload payloads so the renderer can't read or substitute arbitrary bytes. */ - diagnostics: { - getStatus: () => Promise - collectBundle: (lookbackMinutes?: number) => Promise - openBundlePreview: (bundleSubmissionId: string) => Promise - discardBundlePreview: (bundleSubmissionId: string) => Promise - uploadBundle: (bundleSubmissionId: string) => Promise - deleteBundle: (ticketId: string) => Promise - } - /** Read-only effective consent state (+ reason if disabled) — env vars are main-side state the renderer can't read directly. */ - telemetryGetConsentState: () => Promise - /** Banner ✕ — persist `optedIn = true` silently. Separate channel from `telemetrySetOptIn`, - * whose `via` derivation would wrongly fire `telemetry_opted_in`. Same per-session rate limit. */ - telemetryAcknowledgeBanner: () => Promise - settings: { - get: () => Promise - /** Synchronous persisted-settings read for startup decisions that can't wait for async hydration. Blocking IPC — call sparingly. */ - getSync: () => GlobalSettings | null - set: (args: Partial) => Promise - setActiveRuntimeEnvironmentPreference: (args: { - environmentId: string | null - }) => Promise - updatePRBotAuthorOverride: (args: { author: string; isBot: boolean }) => Promise - listFonts: () => Promise - previewGhosttyImport: () => Promise - previewWarpThemeImport: (source: WarpThemeImportSource) => Promise - /** Subscribe to out-of-band settings updates (e.g. View > Appearance toggles) to stay in sync with main. */ - onChanged: (callback: (updates: Partial) => void) => () => void - } - agentAwake: { - getStatus: () => Promise - onChanged: (callback: (status: ComputerAwakeStatus) => void) => () => void - } - localhostWorktreeLabels: { - register: (args: LocalhostWorktreeLabelRoute) => Promise - } - keybindings: { - get: () => Promise - ensureFile: () => Promise - setAction: (args: { - actionId: KeybindingActionId - bindings: string[] | null - }) => Promise - reload: () => Promise - openFile: () => Promise - revealFile: () => Promise - onChanged: (callback: (snapshot: KeybindingFileSnapshot) => void) => () => void - } - codexAccounts: { - list: () => Promise - add: (args?: { - runtime?: 'host' | 'wsl' - wslDistro?: string | null - }) => Promise - reauthenticate: (args: { accountId: string }) => Promise - remove: (args: { accountId: string }) => Promise - select: (args: { - accountId: string | null - runtime?: 'host' | 'wsl' - wslDistro?: string | null - }) => Promise - /** Live PTYs whose baked CODEX_HOME still points at a deselected account. */ - listStalePanes: (args: { ptyIds: string[] }) => Promise< - { - ptyId: string - launchAccountId: string | null - activeAccountId: string | null - /** Optional for compatibility with a pre-reason main process. */ - reason?: 'account-change' | 'home-route-change' - }[] - > - /** The selection lane each PTY launched from, keyed by pty id; unrecorded panes are absent. */ - listRecordedPaneLanes: (args: { ptyIds: string[] }) => Promise> - /** Drops launch records so a dismissed prompt stays dismissed across restarts. */ - forgetStalePanes: (args: { ptyIds: string[] }) => Promise - } - claudeAccounts: { - list: () => Promise - add: (args?: { - runtime?: 'host' | 'wsl' - wslDistro?: string | null - }) => Promise - cancelPendingLogin: () => Promise - reauthenticate: (args: { accountId: string }) => Promise - remove: (args: { accountId: string }) => Promise - select: (args: { - accountId: string | null - runtime?: 'host' | 'wsl' - wslDistro?: string | null - }) => Promise - } - cli: { - getInstallStatus: () => Promise - install: () => Promise - remove: () => Promise - getWslInstallStatus: (args?: { distro?: string | null }) => Promise - installWsl: (args?: { distro?: string | null }) => Promise - removeWsl: (args?: { distro?: string | null }) => Promise - } - codexConfigSync: { - status: () => Promise - } - agentHooks: { - claudeStatus: () => Promise - openClaudeStatus: () => Promise - codexStatus: () => Promise - geminiStatus: () => Promise - antigravityStatus: () => Promise - ampStatus: () => Promise - cursorStatus: () => Promise - droidStatus: () => Promise - commandCodeStatus: () => Promise - grokStatus: () => Promise - copilotStatus: () => Promise - hermesStatus: () => Promise - devinStatus: () => Promise - } - agentTrust: { - markTrusted: (args: { - preset: 'cursor' | 'copilot' | 'codex' - workspacePath: string - connectionId?: string - }) => Promise - } + gh: Merged + hostedReview: HostedReviewApi + gl: GitLabApi + bitbucket: BitbucketApi + linear: LinearApi + jira: JiraApi + starNag: StarNagApi + telemetryTrack: TelemetryApi['telemetryTrack'] + telemetrySetOptIn: TelemetryApi['telemetrySetOptIn'] + diagnostics: DiagnosticsApi + telemetryGetConsentState: TelemetryApi['telemetryGetConsentState'] + telemetryAcknowledgeBanner: TelemetryApi['telemetryAcknowledgeBanner'] + settings: SettingsApi + agentAwake: AgentAwakeApi + localhostWorktreeLabels: LocalhostWorktreeLabelsApi + keybindings: KeybindingsApi + codexAccounts: CodexAccountsApi + claudeAccounts: ClaudeAccountsApi + cli: CliApi + codexConfigSync: CodexConfigSyncApi + agentHooks: AgentHooksApi + agentTrust: AgentTrustApi preflight: PreflightApi - notifications: { - dispatch: (args: NotificationDispatchRequest) => Promise - dismiss: (ids: string[]) => Promise - openSystemSettings: () => Promise - getPermissionStatus: () => Promise - probeDelivery: (args?: { force?: boolean }) => Promise - playSound: (options?: { force?: boolean; volume?: number }) => Promise - } - onboarding: { - get: () => Promise - // Why: main merges the checklist field-by-field, so a partial checklist is fine. - update: ( - updates: Partial> & { - checklist?: Partial - } - ) => Promise - } - dashboard: { - openPopout: (view?: 'board' | 'map') => Promise - publishSnapshot: (snapshot: DashboardSnapshot) => Promise - getPopoutOpen: () => Promise - onPopoutOpenChanged: (callback: (open: boolean) => void) => () => void - onSnapshotRequested: (callback: () => void) => () => void - onRevealAgent: (callback: (args: DashboardRevealAgentArgs) => void) => () => void - onAckAgent: (callback: (paneKey: string) => void) => () => void - onSpawnAgent: (callback: (args: DashboardSpawnAgentArgs) => void) => () => void - onSleepWorkspace: (callback: (args: DashboardSleepWorkspaceArgs) => void) => () => void - requestSnapshot: () => Promise - onSnapshot: (callback: (snapshot: DashboardSnapshot) => void) => () => void - onViewRequested: (callback: (view: 'board' | 'map') => void) => () => void - revealAgent: (args: DashboardRevealAgentArgs) => Promise - ackAgent: (paneKey: string) => Promise - spawnAgent: (args: DashboardSpawnAgentArgs) => Promise - sleepWorkspace: (args: DashboardSleepWorkspaceArgs) => Promise - } - terminalPreview: { - connect: ( - ptyId: string, - opts?: { scrollbackRows?: number } - ) => Promise - input: (ptyId: string, data: string) => Promise - /** Claim the PTY grid for the preview dialog; resolves to the size actually in effect. */ - fit: ( - ptyId: string, - cols: number, - rows: number - ) => Promise<{ cols: number; rows: number } | null> - ack: (ptyId: string, bytes: number) => Promise - unsubscribe: (ptyId: string) => Promise - onData: (callback: (payload: TerminalPreviewDataPayload) => void) => () => void - } - macosTccPrompts: { - /** Fires once macOS has raised its Nth consent dialog naming Orca (#9756). */ - onThreshold: (callback: (payload: { promptCount: number }) => void) => () => void - consumePending: () => Promise<{ claimId: number; promptCount: number } | null> - acknowledgePending: (claimId: number) => Promise - releasePending: (claimId: number) => Promise - dismiss: () => Promise - } - developerPermissions: { - getStatus: () => Promise - request: (args: { id: DeveloperPermissionId }) => Promise - openSettings: (args: { id: DeveloperPermissionId }) => Promise - testLocalNetworkConnection: (args: { - host: string - port: number - }) => Promise - } - computerUsePermissions: { - getStatus: () => Promise - openSetup: (args?: { - id?: ComputerUsePermissionId - }) => Promise - reset: () => Promise - } - shell: { - openPath: (path: string) => Promise - openInFileManager: (path: string) => Promise - openInExternalEditor: ( - request: ShellOpenExternalEditorRequest - ) => Promise - openUrl: (url: string) => Promise - openFilePath: (path: string) => Promise - openFileUri: (uri: string) => Promise - pathExists: (path: string) => Promise - pickAttachment: () => Promise - pickImage: () => Promise - pickRepoIconImage: () => Promise<{ dataUrl: string; fileName: string } | null> - pickAudio: () => Promise - pickDirectory: (args: { defaultPath?: string }) => Promise - copyFile: (args: { srcPath: string; destPath: string }) => Promise - } - skills: { - discover: (target?: SkillDiscoveryTarget) => Promise - freshnessInventory: () => Promise - startUpdateRun: (names: string[]) => Promise - cancelUpdateRun: () => Promise - acknowledgeUpdateRun: () => Promise - getUpdateRun: () => Promise - onUpdateRun: (callback: (run: SkillUpdateRun) => void) => () => void - } - pet: { - import: () => Promise - importPetBundle: () => Promise - read: (id: string, fileName: string, kind?: 'image' | 'bundle') => Promise - delete: (id: string, fileName: string, kind?: 'image' | 'bundle') => Promise - } + notifications: NotificationsApi + onboarding: OnboardingApi + dashboard: DashboardApi + terminalPreview: TerminalPreviewApi + macosTccPrompts: MacosTccPromptsApi + developerPermissions: DeveloperPermissionsApi + computerUsePermissions: ComputerUsePermissionsApi + shell: ShellApi + skills: SkillsApi + pet: PetApi browser: BrowserApi emulator: EmulatorApi - hooks: { - check: (args: { repoId: string; hostId?: ExecutionHostId }) => Promise<{ - status?: 'ok' | 'error' - hasHooks: boolean - hooks: OrcaHooks | null - mayNeedUpdate: boolean - }> - inspectSetupScriptImports: (args: { - repoId: string - hostId?: ExecutionHostId - }) => Promise - createIssueCommandRunner: (args: { - repoId: string - worktreePath: string - command: string - }) => Promise - readIssueCommand: (args: { repoId: string; hostId?: ExecutionHostId }) => Promise<{ - status?: 'ok' | 'error' - localContent: string | null - sharedContent: string | null - effectiveContent: string | null - localFilePath: string - source: 'local' | 'shared' | 'none' - }> - writeIssueCommand: (args: { - repoId: string - content: string - hostId?: ExecutionHostId - }) => Promise - } - ephemeralVm: { - listRecipes: (args: { repoId: string }) => Promise<{ - status: 'ok' | 'error' - repoPath: string | null - recipes: OrcaHooks['environmentRecipes'] - diagnostics: NonNullable - message?: string - }> - listRecipeCatalog: () => Promise< - { - repoId: string - repoName: string - repoPath: string - recipes: NonNullable - diagnostics: NonNullable - }[] - > - doctor: (args: { repoId: string; recipeId: string }) => Promise - provision: (args: { - repoId: string - recipeId: string - workspaceName?: string - projectId?: string - workspaceId?: string - branch?: string - ref?: string - provisionId?: string - }) => Promise< - | { - ok: true - connectionType: 'orca-server' - runtime: EphemeralVmRuntimeRecord - environment: PublicKnownRuntimeEnvironment - stderr: string - warnings: EphemeralVmRecipeResultWarning[] - } - | { - ok: true - connectionType: 'ssh' - runtime: EphemeralVmRuntimeRecord - sshTargetId: string - stderr: string - warnings: EphemeralVmRecipeResultWarning[] - } - | { ok: false; error: string; stderr: string; stdout: string } - > - cancelProvision: (args: { provisionId: string }) => Promise<{ cancelled: boolean }> - onProvisionEvent: ( - callback: (event: { provisionId: string; stream: 'stdout' | 'stderr'; chunk: string }) => void - ) => () => void - listRuntimes: () => Promise - attachWorkspace: (args: { - runtimeId: string - workspaceId: string - }) => Promise - suspendWorkspace: (args: { workspaceId: string }) => Promise - resumeWorkspace: (args: { workspaceId: string }) => Promise - cleanup: (args: { runtimeId: string }) => Promise - getCleanupCommand: (args: { runtimeId: string }) => Promise<{ - runtimeId: string - command: string | null - payloadJson: string - cleanupDisabled: boolean - message?: string - }> - } - cache: { - getGitHub: () => Promise<{ - pr: Record - issue: Record - }> - setGitHub: (args: { - cache: { - pr: Record - issue: Record - } - }) => Promise - } - session: { - // hostId defaults to the 'local' partition on main, so omitting it stays backward-compatible. - get: (hostId?: ExecutionHostId) => Promise - set: (args: WorkspaceSessionState, hostId?: ExecutionHostId) => Promise - patch: (args: WorkspaceSessionPatch, hostId?: ExecutionHostId) => Promise - flush: () => Promise - readTerminalScrollback: (args: { ref: string }) => string | null - setSync: (args: WorkspaceSessionState, hostId?: ExecutionHostId) => void - } - remoteWorkspace: { - get: (args: { targetId: string }) => Promise - setForConnectedTargets: (args: { - session?: WorkspaceSessionState - hydratedTargetIds?: string[] - }) => Promise<{ targetId: string; result: RemoteWorkspacePatchResult }[]> - listEnabledConnectedTargets: () => Promise - listConnectedClients: (args?: { - targetIds?: string[] - }) => Promise<{ targetId: string; clients: RemoteWorkspaceConnectedClient[] }[]> - clientId: () => Promise - onChanged: (callback: (event: RemoteWorkspaceChangedEvent) => void) => () => void - } - updater: { - getVersion: () => Promise - getStatus: () => Promise - check: (options?: UpdateCheckOptions) => Promise - download: () => Promise - quitAndInstall: () => Promise - dismissNudge: () => Promise - dismissAvailableUpdate: () => Promise - /** Desktop-only. Rejects unless the current status carries `linux-package-install` recovery. */ - getLinuxPackageInstallInstructions: () => Promise - /** Desktop-only. Reveals the revalidated cached package in the native file manager. */ - showLinuxPackage: () => Promise - listBuilds: (channel: ReleaseChannel) => Promise - - onStatus: (callback: (status: UpdateStatus) => void) => () => void - onClearDismissal: (callback: () => void) => () => void - } - notebook: { - runPythonCell: (args: { - filePath: string - code: string - preamble?: string - connectionId?: string | null - }) => Promise<{ stdout: string; stderr: string; exitCode: number | null; error?: string }> - } + hooks: HooksApi + ephemeralVm: EphemeralVmApi + cache: WorkspaceSessionApi['cache'] + session: WorkspaceSessionApi['session'] + remoteWorkspace: WorkspaceSessionApi['remoteWorkspace'] + updater: UpdaterApi + notebook: FilesystemApi['notebook'] stats: StatsApi memory: MemoryApi claudeUsage: ClaudeUsageApi @@ -2748,999 +132,74 @@ export type PreloadApi = { openCodeUsage: OpenCodeUsageApi aiVault: AiVaultApi nativeChat: NativeChatApi - fs: { - readDir: (args: { dirPath: string; connectionId?: string }) => Promise - readFile: (args: { - filePath: string - connectionId?: string - includeLocalLogMetadata?: boolean - }) => Promise<{ - content: string - isBinary: boolean - isImage?: boolean - mimeType?: string - fileIdentity?: string - }> - readLocalLogTail: (args: LocalLogTailReadArgs) => Promise - startLocalLogTail: (args: LocalLogTailWatchArgs) => Promise - stopLocalLogTail: (args: { subscriptionId: string }) => Promise - onLocalLogTailChanged: (callback: (payload: LocalLogTailChangedPayload) => void) => () => void - downloadFile: (args: { - filePath: string - connectionId: string - }) => Promise<{ canceled: true } | { canceled: false; destinationPath: string }> - downloadFolder: (args: { - dirPath: string - connectionId: string - }) => Promise<{ canceled: true } | { canceled: false; destinationPath: string }> - saveDownloadedFile: (args: { - suggestedName: string - content: string - encoding: 'utf8' | 'base64' - }) => Promise<{ canceled: true } | { canceled: false; destinationPath: string }> - startDownloadedFile: (args: { - suggestedName: string - }) => Promise< - { canceled: true } | { canceled: false; transferId: string; destinationPath: string } - > - appendDownloadedFileChunk: (args: { - transferId: string - contentBase64: string - }) => Promise<{ ok: true }> - finishDownloadedFile: (args: { - transferId: string - }) => Promise<{ canceled: false; destinationPath: string }> - cancelDownloadedFile: (args: { transferId: string }) => Promise<{ ok: true }> - listMarkdownDocuments: (args: { - rootPath: string - connectionId?: string - }) => Promise - writeFile: ( - args: { filePath: string; content: string; connectionId?: string } & SshMutationExpectation - ) => Promise - createFile: ( - args: { filePath: string; connectionId?: string } & SshMutationExpectation - ) => Promise - createDir: ( - args: { dirPath: string; connectionId?: string } & SshMutationExpectation - ) => Promise - rename: ( - args: { oldPath: string; newPath: string; connectionId?: string } & SshMutationExpectation - ) => Promise - copy: ( - args: { - sourcePath: string - destinationPath: string - connectionId?: string - } & SshMutationExpectation - ) => Promise - deletePath: ( - args: { - targetPath: string - connectionId?: string - recursive?: boolean - } & SshMutationExpectation - ) => Promise - authorizeExternalPath: (args: { targetPath: string }) => Promise - stat: (args: { - filePath: string - connectionId?: string - }) => Promise<{ size: number; isDirectory: boolean; mtime: number }> - pathExists: (args: { filePath: string; connectionId?: string }) => Promise - listFiles: (args: { - rootPath: string - connectionId?: string - excludePaths?: string[] - requestToken?: string - }) => Promise - cancelListFiles: (args: { requestToken: string }) => Promise - search: (args: SearchOptions & { connectionId?: string }) => Promise - importExternalPaths: ( - args: { - sourcePaths: string[] - destDir: string - connectionId?: string - ensureDir?: boolean - } & SshMutationExpectation - ) => Promise<{ - results: ( - | { - sourcePath: string - status: 'imported' - destPath: string - kind: 'file' | 'directory' - renamed: boolean - } - | { - sourcePath: string - status: 'skipped' - reason: 'missing' | 'symlink' | 'permission-denied' | 'unsupported' - } - | { - sourcePath: string - status: 'failed' - reason: string - } - )[] - }> - stageExternalPathsForRuntimeUpload: (args: { sourcePaths: string[] }) => Promise<{ - sources: ( - | { - sourcePath: string - status: 'staged' - name: string - kind: 'file' | 'directory' - entries: ( - | { relativePath: string; kind: 'directory' } - | { relativePath: string; kind: 'file'; contentBase64: string } - )[] - } - | { - sourcePath: string - status: 'skipped' - reason: 'missing' | 'symlink' | 'permission-denied' | 'unsupported' - } - | { - sourcePath: string - status: 'failed' - reason: string - } - )[] - }> - resolveDroppedPathsForAgent: ( - args: { - paths: string[] - worktreePath: string - connectionId?: string - } & SshMutationExpectation - ) => Promise<{ - resolvedPaths: string[] - skipped: { - sourcePath: string - reason: 'missing' | 'symlink' | 'permission-denied' | 'unsupported' - }[] - failed: { sourcePath: string; reason: string }[] - }> - watchWorktree: (args: { worktreePath: string; connectionId?: string }) => Promise - unwatchWorktree: (args: { worktreePath: string; connectionId?: string }) => Promise - onFsChanged: (callback: (payload: FsChangedPayload) => void) => () => void - } - git: { - status: (args: { - worktreePath: string - connectionId?: string - includeIgnored?: boolean - bypassEffectiveUpstreamNegativeCache?: boolean - reuseLineStats?: boolean - /** Merge-base OID to measure the branch line total against; omit to skip the work. */ - branchLineTotalMergeBase?: string - requestToken?: string - }) => Promise - cancelStatus: (args: { requestToken: string }) => Promise - setStatusUpstreamRefWatch: (args: { - worktreeId: string - worktreePath: string - executionHostId: string - connectionId?: string - branch?: string - upstreamName?: string - }) => Promise - submoduleStatus: (args: { - worktreePath: string - submodulePath: string - connectionId?: string - area?: GitStagingArea - }) => Promise - checkIgnored: (args: { - worktreePath: string - paths: string[] - connectionId?: string - }) => Promise - findHugeFoldersToIgnore: (args: { worktreePath: string }) => Promise - appendGitignore: (args: { worktreePath: string; folderName: string }) => Promise - history: ( - args: { worktreePath: string; connectionId?: string } & GitHistoryOptions - ) => Promise - conflictOperation: (args: { - worktreePath: string - connectionId?: string - }) => Promise - abortMerge: (args: { worktreePath: string; connectionId?: string }) => Promise - abortRebase: (args: { worktreePath: string; connectionId?: string }) => Promise - diff: (args: { - worktreePath: string - filePath: string - staged: boolean - compareAgainstHead?: boolean - connectionId?: string - }) => Promise - branchCompare: (args: { - worktreePath: string - baseRef: string - connectionId?: string - }) => Promise - commitCompare: (args: { - worktreePath: string - commitId: string - connectionId?: string - }) => Promise - upstreamStatus: (args: { - worktreePath: string - connectionId?: string - pushTarget?: GitPushTarget - }) => Promise - fetch: (args: { - worktreePath: string - connectionId?: string - pushTarget?: GitPushTarget - }) => Promise - syncFork: (args: { - worktreePath: string - connectionId?: string - expectedUpstream: GitForkSyncExpectedUpstream - }) => Promise - push: (args: { - worktreePath: string - publish?: boolean - forceWithLease?: boolean - connectionId?: string - pushTarget?: GitPushTarget - }) => Promise - pull: (args: { - worktreePath: string - connectionId?: string - pushTarget?: GitPushTarget - }) => Promise - fastForward: (args: { - worktreePath: string - connectionId?: string - pushTarget?: GitPushTarget - }) => Promise - rebaseFromBase: (args: { - worktreePath: string - baseRef: string - connectionId?: string - }) => Promise - branchDiff: (args: { - worktreePath: string - compare: { - baseRef: string - baseOid: string - headOid: string - mergeBase: string - } - filePath: string - oldPath?: string - connectionId?: string - }) => Promise - commitDiff: (args: { - worktreePath: string - commitOid: string - parentOid?: string | null - filePath: string - oldPath?: string - connectionId?: string - }) => Promise - commit: (args: { - worktreePath: string - message: string - connectionId?: string - }) => Promise<{ success: boolean; error?: string }> - generateCommitMessage: (args: { - worktreePath: string - /** Raw (unstripped) worktree meta key; validated against worktreePath in main. */ - worktreeId?: string - repoId?: string - connectionId?: string - sourceControlAiResolvedParams?: ResolvedSourceControlAiGenerationParams - sourceControlAi?: SourceControlAiSettings - agentCmdOverrides?: Partial> - }) => Promise< - | { success: true; message: string; agentLabel?: string } - | { success: false; error: string; canceled?: boolean } - > - discoverCommitMessageModels: (args: { - agentId: string - worktreePath?: string - connectionId?: string - }) => Promise< - | { - success: true - capability: CommitMessageAgentCapability - models: CommitMessageModelCapability[] - defaultModelId: string - catalogOrigin: 'probe' | 'spec' - } - | { success: false; error: string } - > - cancelGenerateCommitMessage: (args: { - worktreePath: string - connectionId?: string - }) => Promise - generatePullRequestFields: (args: { - worktreePath: string - /** Raw (unstripped) worktree meta key; validated against worktreePath in main. */ - worktreeId?: string - repoId?: string - base: string - title: string - body: string - draft: boolean - provider?: HostedReviewProvider - useTemplate?: boolean - connectionId?: string - sourceControlAiResolvedParams?: ResolvedSourceControlAiGenerationParams - sourceControlAi?: SourceControlAiSettings - agentCmdOverrides?: Partial> - }) => Promise< - | { - success: true - fields: { base: string; title: string; body: string; draft: boolean } - agentLabel?: string - branchChangedByPreparation?: boolean - } - | { success: false; error: string; canceled?: boolean; branchChangedByPreparation?: boolean } - > - cancelGeneratePullRequestFields: (args: { - worktreePath: string - connectionId?: string - }) => Promise - stage: (args: { - worktreePath: string - filePath: string - connectionId?: string - }) => Promise - bulkStage: (args: { - worktreePath: string - filePaths: string[] - connectionId?: string - }) => Promise - unstage: (args: { - worktreePath: string - filePath: string - connectionId?: string - }) => Promise - bulkUnstage: (args: { - worktreePath: string - filePaths: string[] - connectionId?: string - }) => Promise - discard: (args: { - worktreePath: string - filePath: string - connectionId?: string - }) => Promise - bulkDiscard: (args: { - worktreePath: string - filePaths: string[] - connectionId?: string - }) => Promise - remoteFileUrl: (args: { - worktreePath: string - relativePath: string - line: number - connectionId?: string - }) => Promise - remoteCommitUrl: (args: { - worktreePath: string - sha: string - connectionId?: string - }) => Promise - } - ui: { - get: () => Promise - set: (args: Partial) => Promise - recordFeatureInteraction: (id: FeatureInteractionId) => Promise - onStateChanged: (callback: (ui: PersistedUIState) => void) => () => void - onOpenSettings: (callback: () => void) => () => void - /** Consumes a one-shot tray/menu-bar "open settings" intent queued before mount. */ - consumePendingOpenSettings: () => Promise - onOpenSetupGuide: (callback: () => void) => () => void - onOpenFeatureTour: (callback: () => void) => () => void - onOpenCrashReport: (callback: () => void) => () => void - onToggleLeftSidebar: (callback: () => void) => () => void - onToggleRightSidebar: (callback: () => void) => () => void - onToggleWorktreePalette: (callback: () => void) => () => void - onToggleFloatingTerminal: (callback: () => void) => () => void - onTerminalShortcutCaptured: ( - callback: (data: { actionId: KeybindingActionId }) => void - ) => () => void - onOpenQuickOpen: (callback: () => void) => () => void - onToggleQuickCommandsMenu: (callback: () => void) => () => void - onOpenNewWorkspace: (callback: () => void) => () => void - onDeleteCurrentWorkspace: (callback: () => void) => () => void - onOpenWorkspaceBoard: (callback: () => void) => () => void - onOpenTasks: (callback: () => void) => () => void - onJumpToWorktreeIndex: (callback: (index: number) => void) => () => void - onJumpToTabIndex: (callback: (index: number) => void) => () => void - onWorktreeHistoryNavigate: (callback: (direction: 'back' | 'forward') => void) => () => void - onNewBrowserTab: (callback: () => void) => () => void - onNewMarkdownTab: (callback: () => void) => () => void - onNewSimulatorTab: (callback: () => void) => () => void - onRequestTabCreate: ( - callback: (data: { - requestId: string - url: string - worktreeId?: string - sessionProfileId?: string | null - sessionPartition?: string - activate?: boolean - }) => void - ) => () => void - replyTabCreate: (reply: { requestId: string; browserPageId?: string; error?: string }) => void - onRequestTabSetProfile: ( - callback: (data: { - requestId: string - browserPageId: string - profileId: string - sessionPartition?: string - }) => void - ) => () => void - replyTabSetProfile: (reply: { requestId: string; error?: string }) => void - onRequestTabClose: ( - callback: (data: { requestId: string; tabId: string | null; worktreeId?: string }) => void - ) => () => void - replyTabClose: (reply: { requestId: string; error?: string }) => void - onNewTerminalTab: (callback: () => void) => () => void - onFocusBrowserAddressBar: (callback: () => void) => () => void - onFindInBrowserPage: (source: BrowserFindSource, callback: () => void) => () => void - onReloadBrowserPage: (callback: () => void) => () => void - onBrowserHistoryNavigate: (callback: (direction: 'back' | 'forward') => void) => () => void - onZoomBrowserPage: (callback: (direction: 'in' | 'out' | 'reset') => void) => () => void - onHardReloadBrowserPage: (callback: () => void) => () => void - onCloseActiveTab: (callback: () => void) => () => void - onCloseFloatingItem: (callback: (payload: { sourceId: string }) => void) => () => void - onSelectFloatingIndex: (callback: (payload: { index: number }) => void) => () => void - onSwitchTab: (callback: (direction: 1 | -1) => void) => () => void - onSwitchTabAcrossAllTypes: (callback: (direction: 1 | -1) => void) => () => void - onSwitchRecentTab: (callback: () => void) => () => void - onSwitchTerminalTab: (callback: (direction: 1 | -1) => void) => () => void - onCtrlTabKeyDown: (callback: (data: { shiftKey: boolean }) => void) => () => void - onCtrlTabKeyUp: (callback: () => void) => () => void - onToggleStatusBar: (callback: () => void) => () => void - onDictationKeyDown: (callback: () => void) => () => void - onExportPdfRequested: (callback: () => void) => () => void - onAppMenuPaste: (callback: () => void) => () => void - onAppMenuSelectionAction: (callback: (action: 'copy' | 'select-all') => void) => () => void - onEditableContextPaste: (callback: (data: { plainTextOnly: boolean }) => void) => () => void - onActivateWorktree: ( - callback: (data: { - repoId: string - worktreeId: string - setup?: WorktreeSetupLaunch - startup?: WorktreeStartupLaunch - defaultTabs?: WorktreeDefaultTabsLaunch - }) => void - ) => () => void - onCreateTerminal: ( - callback: (data: { - requestId?: string - worktreeId: string - command?: string - cwd?: string - env?: Record - launchConfig?: SleepingAgentLaunchConfig - resumeProviderSession?: AgentProviderSessionMetadata - launchToken?: string - launchAgent?: TuiAgent - viewMode?: 'terminal' | 'chat' - title?: string - ptyId?: string - activate?: boolean - focus?: boolean - presentation?: RuntimeTerminalPresentation - surfaceOwner?: false - tabId?: string - leafId?: string - splitFromLeafId?: string - splitDirection?: 'horizontal' | 'vertical' - splitTelemetrySource?: TerminalPaneSplitSource - }) => void - ) => () => void - onRequestTerminalCreate: ( - callback: (data: RuntimeTerminalCreateRequestPayload) => void - ) => () => void - onRequestTerminalTabMount: ( - callback: (data: { worktreeId: string; tabId?: string; ptyId?: string }) => void - ) => () => void - replyTerminalCreate: (reply: TerminalTabCreateReply) => void - onSplitTerminal: ( - callback: (data: { - tabId: string - paneRuntimeId: number - direction: 'horizontal' | 'vertical' - command?: string - telemetrySource?: TerminalPaneSplitSource - }) => void - ) => () => void - onRenameTerminal: ( - callback: (data: { tabId: string; title: string | null }) => void - ) => () => void - onFocusTerminal: ( - callback: (data: { - tabId: string - worktreeId: string - leafId?: string | null - ackPaneKeyOnSuccess?: string - flashFocusedPane?: boolean - scrollToBottomIfOutputSinceLastView?: boolean - }) => void - ) => () => void - onFocusEditorTab: ( - callback: (data: { tabId: string; worktreeId: string }) => void - ) => () => void - onCloseSessionTab: ( - callback: (data: { tabId: string; worktreeId: string }) => void - ) => () => void - onMoveSessionTab: ( - callback: (data: { worktreeId: string } & RuntimeMobileSessionTabMove) => void - ) => () => void - onOpenFileFromMobile: ( - callback: (data: { - worktreeId: string - filePath: string - relativePath: string - runtimeEnvironmentId?: string - }) => void - ) => () => void - onOpenDiffFromMobile: ( - callback: (data: { - worktreeId: string - filePath: string - relativePath: string - staged: boolean - runtimeEnvironmentId?: string - }) => void - ) => () => void - onMobileMarkdownRequest: ( - callback: (request: RuntimeMobileMarkdownRequest) => void - ) => () => void - respondMobileMarkdownRequest: (response: RuntimeMobileMarkdownResponse) => void - onCloseTerminal: ( - callback: (data: { tabId: string; paneRuntimeId?: number }) => void - ) => () => void - onTerminalTabCloseRequest: (callback: (request: TerminalTabCloseRequest) => void) => () => void - respondTerminalTabClose: (response: TerminalTabCloseResponse) => void - onSleepWorktree: (callback: (data: { worktreeId: string }) => void) => () => void - onResumeSleepingAgents: (callback: (data: { worktreeId: string }) => void) => () => void - onTerminalZoom: (callback: (direction: 'in' | 'out' | 'reset') => void) => () => void - onSystemResumed: (callback: () => void) => () => void - readClipboardText: (options?: ReadClipboardTextOptions) => Promise - readSelectionClipboardText: (options?: ReadClipboardTextOptions) => Promise - saveClipboardImageAsTempFile: (args?: { - connectionId?: string | null - runtimeEnvironmentId?: string | null - }) => Promise - writeClipboardText: (text: string) => Promise - writeTerminalClipboardText: (text: string) => Promise - writeSelectionClipboardText: (text: string) => Promise - writeClipboardImage: (dataUrl: string) => Promise - performNativePaste: (options?: { mode?: 'paste' | 'paste-and-match-style' }) => void - performNativeSelectionAction: (action: 'copy' | 'select-all') => void - writeClipboardFile: ( - args: - | { - filePath: string - connectionId?: string | null - } - | string - ) => Promise<{ ok: boolean; reason?: string }> - onFileDrop: (callback: (data: NativeFileDropPayload) => void) => () => void - getZoomLevel: () => number - setZoomLevel: (level: number) => void - syncTrafficLights: (zoomFactor: number) => void - setMarkdownEditorFocused: (focused: boolean) => void - setRichMarkdownContextMenuTarget: (target: RichMarkdownContextMenuTableTarget | null) => void - setTerminalInputFocused: (focused: boolean) => void - setFloatingFocus: (state: { panelFocused: boolean; terminalFocused: boolean }) => void - setShortcutRecorderFocused: (focused: boolean) => void - onRichMarkdownContextCommand: ( - callback: (payload: RichMarkdownContextMenuCommandPayload) => void - ) => () => void - onFullscreenChanged: (callback: (isFullScreen: boolean) => void) => () => void - minimize: () => void - maximize: () => void - isMaximized: () => Promise - onMaximizeChanged: (callback: (isMaximized: boolean) => void) => () => void - requestClose: () => void - popupMenu: () => void - onWindowCloseRequested: (callback: (data: { isQuitting: boolean }) => void) => () => void - confirmWindowClose: () => void - notifyWindowRevealed: () => void - } - runtime: { - syncWindowGraph: (graph: RuntimeSyncWindowGraph) => Promise - getStatus: () => Promise - call: (args: { method: string; params?: unknown }) => Promise> - getTerminalFitOverrides: () => Promise< - { ptyId: string; mode: 'mobile-fit' | 'remote-desktop-fit'; cols: number; rows: number }[] - > - getTerminalDrivers: () => Promise< - { - ptyId: string - driver: RuntimeTerminalDriverState - }[] - > - getBrowserDrivers: () => Promise< - { - browserPageId: string - driver: RuntimeBrowserDriverState - }[] - > - restoreTerminalFit: (ptyId: string) => Promise<{ restored: boolean }> - reclaimBrowserForDesktop: (browserPageId: string) => Promise<{ reclaimed: boolean }> - onTerminalFitOverrideChanged: ( - callback: (event: { - ptyId: string - mode: 'mobile-fit' | 'remote-desktop-fit' | 'desktop-fit' - cols: number - rows: number - }) => void - ) => () => void - onTerminalDriverChanged: ( - callback: (event: { ptyId: string; driver: RuntimeTerminalDriverState }) => void - ) => () => void - onNativeChatLaunchDraftResolved?: ( - callback: (event: { tabId: string; text: string; createdAt: number }) => void - ) => () => void - onBrowserDriverChanged: ( - callback: (event: { browserPageId: string; driver: RuntimeBrowserDriverState }) => void - ) => () => void - } - runtimeEnvironments: { - list: () => Promise - addFromPairingCode: (args: { - name: string - pairingCode: string - }) => Promise<{ environment: PublicKnownRuntimeEnvironment }> - verifyAndAddFromPairingCode: (args: { - name: string - pairingCode: string - allowLoopback?: boolean - }) => Promise - resolve: (args: { selector: string }) => Promise - remove: (args: { selector: string }) => Promise<{ removed: PublicKnownRuntimeEnvironment }> - disconnect: (args: { - selector: string - }) => Promise<{ disconnected: PublicKnownRuntimeEnvironment }> - connect: (args: { - selector: string - timeoutMs?: number - }) => Promise> - getStatus: (args: { - selector: string - timeoutMs?: number - }) => Promise> - // Why: system resume / browser online advance pending shared-control reconnect timers only. - retryConnectionsNow?: () => Promise - call: (args: { - selector: string - method: string - params?: unknown - timeoutMs?: number - expectedEnvironmentPairingRevision?: number - }) => Promise> - subscribe: ( - args: { - selector: string - method: string - params?: unknown - timeoutMs?: number - expectedEnvironmentPairingRevision?: number - }, - callbacks: { - onResponse: (response: RuntimeRpcResponse) => void - onBinary?: (bytes: Uint8Array) => void - onError?: (error: { code: string; message: string }) => void - onClose?: () => void - } - ) => Promise - } - rateLimits: { - get: () => Promise - refresh: () => Promise - refreshCodexForTarget: (target: RateLimitRuntimeTarget) => Promise - consumeCodexResetCredit: () => Promise - refreshClaudeForTarget: (target: RateLimitRuntimeTarget) => Promise - setPollingInterval: (ms: number) => Promise - fetchInactiveClaudeAccounts: () => Promise - fetchInactiveCodexAccounts: () => Promise - refreshMiniMax: () => Promise - refreshGrok: () => Promise - onUpdate: (callback: (state: RateLimitState) => void) => () => void - } - minimaxCredentials: { - getStatus: () => Promise<{ configured: boolean }> - saveCookie: (cookie: string) => Promise<{ configured: boolean }> - clearCookie: () => Promise<{ configured: boolean }> - } - grokAccounts: { - getStatus: () => Promise - } - ssh: { - listTargets: () => Promise - // Removed-target id → last known label, for a friendly host name on workspaces still pinned to a removed target. - listRemovedTargetLabels: () => Promise> - addTarget: (args: { target: Omit }) => Promise - updateTarget: (args: { - id: string - updates: Partial> - }) => Promise - removeTarget: (args: { id: string }) => Promise - importConfig: (args?: { reAdopt?: boolean }) => Promise - listConfigHosts: (args?: SshConfigHostListArgs) => Promise - resolveConfigHost: (args: { alias: string }) => Promise - connect: (args: { targetId: string }) => Promise - disconnect: (args: { targetId: string }) => Promise - terminateSessions: (args: { targetId: string }) => Promise - resetRelay: (args: { targetId: string }) => Promise - getState: (args: { targetId: string }) => Promise - needsPassphrasePrompt: (args: { targetId: string }) => Promise - testConnection: (args: { - targetId: string - }) => Promise<{ success: boolean; error?: string; state?: SshConnectionState }> - onStateChanged: ( - callback: (data: { targetId: string; state: SshConnectionState }) => void - ) => () => void - addPortForward: (args: { - targetId: string - localPort: number - remoteHost: string - remotePort: number - label?: string - }) => Promise - updatePortForward: (args: { - id: string - targetId: string - localPort: number - remoteHost: string - remotePort: number - label?: string - }) => Promise - removePortForward: (args: { id: string }) => Promise - listPortForwards: (args?: { targetId?: string }) => Promise - listDetectedPorts: (args: { targetId: string }) => Promise - onPortForwardsChanged: ( - callback: (data: { targetId: string; forwards: PortForwardEntry[] }) => void - ) => () => void - onDetectedPortsChanged: ( - callback: (data: { targetId: string; ports: EnrichedDetectedPort[] }) => void - ) => () => void - browseDir: (args: { targetId: string; dirPath: string }) => Promise<{ - entries: { name: string; isDirectory: boolean }[] - resolvedPath: string - pathFlavor: FilesystemPathFlavor - }> - onCredentialRequest: ( - callback: (data: { - requestId: string - targetId: string - kind: 'passphrase' | 'password' - detail: string - }) => void - ) => () => void - onCredentialResolved: (callback: (data: { requestId: string }) => void) => () => void - submitCredential: (args: { requestId: string; value: string | null }) => Promise - } - automations: { - list: () => Promise - listRuns: (args?: { automationId?: string }) => Promise - listExternalManagers: () => Promise - listExternalRuns: (input: ExternalAutomationRunsInput) => Promise - createExternal: (input: ExternalAutomationCreateInput) => Promise - updateExternal: (input: ExternalAutomationUpdateInput) => Promise - runExternalAction: (input: ExternalAutomationActionInput) => Promise - create: (input: AutomationCreateInput) => Promise - update: (args: { id: string; updates: AutomationUpdateInput }) => Promise - delete: (args: { id: string }) => Promise - runNow: (args: { id: string }) => Promise - runPrecheck: (args: { - automationId: string - runId: string - }) => Promise - markDispatchResult: (result: AutomationDispatchResult) => Promise - snapshotWorkspaceName: (args: { workspaceId: string; displayName: string }) => Promise - rendererReady: () => Promise - onDispatchRequested: (callback: (request: AutomationDispatchRequest) => void) => () => void - } - wsl: { - isAvailable: () => Promise - listDistros: () => Promise - } - pwsh: { - isAvailable: () => Promise - } - gitBash: { - isAvailable: () => Promise - } - plugins: { - list: () => Promise - listLanguagePacks: () => Promise - /** Records the consent-dialog answer; approval is keyed to the plugin's - * current capability and trusted-worker fingerprint. */ - consent: (args: PluginConsentRequest) => Promise - setEnabled: (args: { pluginKey: string; enabled: boolean }) => Promise - /** Returns the panel's CSP-wrapped HTML, or null when the plugin or - * panel is missing/disabled. Rendered only inside a sandboxed iframe. */ - readPanelEntry: (args: { - pluginKey: string - panelId: string - }) => Promise - invokeCommand: (args: { - pluginKey: string - commandId: string - args?: unknown - }) => Promise - /** Relays a sandboxed panel's bridge request to main, which enforces the - * plugin's consented capabilities before executing. */ - panelAction: (args: { - sessionToken: string - action: string - params?: unknown - }) => Promise - install: (source: PluginHostInstallSource) => Promise - listMarketplaces: () => Promise - addMarketplace: ( - source: PluginMarketplaceGitSource - ) => Promise - removeMarketplace: (args: { sourceId: string }) => Promise - refreshMarketplaces: (args?: { - sourceId?: string - }) => Promise - listMarketplacePlugins: () => Promise - previewMarketplacePlugin: (args: { - marketplaceSourceId: string - pluginKey: string - }) => Promise - installMarketplacePlugin: ( - preview: Pick< - PluginMarketplaceHostInstallPreview, - 'marketplaceSourceId' | 'marketplaceCommit' | 'pluginKey' | 'resolvedCommit' - > - ) => Promise - previewMarketplaceUpdate: (args: { - pluginKey: string - }) => Promise - rollbackMarketplacePlugin: (args: { pluginKey: string }) => Promise - remove: (args: { pluginKey: string }) => Promise - getLogs: (args: { pluginKey: string }) => Promise - /** Re-discovers after settings edits (feature flag, dev paths). */ - refresh: () => Promise - /** Fires whenever installed plugins, worker states, panels, or content packs change. */ - onChanged: (callback: (event: PluginChangeEvent) => void) => () => void - } - agentStatus: { - /** Listen for agent status updates forwarded from native hook receivers. */ - onSet: (callback: (data: AgentStatusIpcPayload) => void) => () => void - /** Listen for main-process cleanup that evicted cached hook status. */ - onClear: (callback: (data: AgentStatusClearIpcPayload) => void) => () => void - /** Return the current main-process hook cache after renderer hydration. */ - getSnapshot: () => Promise - inferInterrupt: (request: AgentInterruptInferenceRequest) => Promise - /** Guarded clear for an answered AskUserQuestion wait — the CLI emits no hook at answer time, so the renderer reports the submit keystroke. */ - inferQuestionAnswered: (request: AgentQuestionAnsweredInferenceRequest) => Promise - /** Listen for PTYs on a legacy numeric pane key that have registry-backed UUID pane proof. */ - onMigrationUnsupported: (callback: (entry: MigrationUnsupportedPtyEntry) => void) => () => void - onMigrationUnsupportedClear: (callback: (data: { ptyId: string }) => void) => () => void - onLegacyWorkerTerminalRecovery: ( - callback: (data: { - paneKey: string - resolution: 'adopted' | 'exited' | 'rolled_back' - ptyId?: string - }) => void - ) => () => void - getMigrationUnsupportedSnapshot: () => Promise - /** Drop a paneKey from the main-process hook cache and on-disk last-status file. Fire-and-forget. */ - drop: (paneKey: string) => void - /** Drop every cached hook status under one terminal tab prefix. Fire-and-forget. */ - dropByTabPrefix: (tabId: string) => void - /** Permanently retire one pane's hook authority while siblings stay live. */ - retirePaneAuthority: (paneKey: string) => void - /** Move hook authority when a live pane is detached into another tab. */ - transferPaneAuthority: (args: { - fromPaneKey: string - toPaneKey: string - ptyId?: string - }) => void - } - mobile: { - listNetworkInterfaces: () => Promise<{ - interfaces: { name: string; address: string; hasDefaultRoute?: boolean }[] - }> - getPairingQR: (args?: { - address?: string - connectionMode?: MobilePairingConnectionMode - rotate?: boolean - }) => Promise< - | { - available: false - reason?: string - guidance?: string - relayFailure?: MobileRelayMintFailure - } - | { - available: true - qrDataUrl: string | null - qrError?: 'encoding_failed' - pairingUrl: string - /** Null when no direct address was advertised — the QR pairs over Relay alone. */ - endpoint: string | null - deviceId: string - /** Mode the QR actually encodes. */ - connectionMode: MobilePairingConnectionMode - } - > - getWindowsFirewallStatus: (args?: { address?: string }) => Promise< - | { supported: false } - | { - supported: true - port: number - ruleAllowed: boolean - blockingRuleDetected: boolean - privateFirewallEnabled: boolean - networkCategory: 'private' | 'public' | 'domain' | 'unknown' - inspectionAvailable: boolean - } - > - repairWindowsFirewall: () => Promise< - { ok: true } | { ok: false; reason: 'cancelled' | 'failed' | 'unsupported' } - > - openWindowsNetworkSettings: () => Promise - getRuntimePairingUrl: (args?: { - address?: string - rotate?: boolean - reach?: RuntimePairingReach - }) => Promise< - | { available: false; reason?: 'network_exposure_failed'; guidance?: string } - | { - available: true - pairingUrl: string - webClientUrl: string | null - endpoint: string - deviceId: string - } - > - listDevices: () => Promise<{ - devices: { deviceId: string; name: string; pairedAt: number; lastSeenAt: number }[] - }> - revokeDevice: (args: { deviceId: string }) => Promise<{ revoked: boolean }> - listRuntimeAccessGrants: () => Promise<{ grants: RuntimeAccessGrant[] }> - revokeRuntimeAccess: (args: { deviceId: string }) => Promise<{ revoked: boolean }> - isWebSocketReady: () => Promise<{ ready: boolean; endpoint: string | null }> - getRelayStatus: () => Promise<{ status: MobileRelayStatus }> - onRelayStatusChanged: (callback: (status: MobileRelayStatus) => void) => () => void - /** Consumes an auth-failure notification that arrived before the renderer listener mounted. */ - consumePendingUnpairedDeviceAuthFailure?: () => Promise - /** Fires (throttled, once per session) when an unpaired phone repeatedly fails direct-transport auth. */ - onUnpairedDeviceAuthFailure?: (callback: () => void) => () => void - } - speech: { - getCatalog: () => Promise - getModelStates: () => Promise - getOpenAiApiKeyStatus: () => Promise<{ configured: boolean }> - saveOpenAiApiKey: (apiKey: string) => Promise<{ configured: boolean }> - clearOpenAiApiKey: () => Promise<{ configured: boolean }> - downloadModel: (modelId: string) => Promise - cancelDownload: (modelId: string) => Promise - deleteModel: (modelId: string) => Promise - startDictation: ( - modelId: string, - hotwords: string[] | undefined, - sessionId: string - ) => Promise - feedAudio: (samples: Float32Array, sampleRate: number, sessionId?: string) => Promise - stopDictation: (sessionId?: string) => Promise - onPartialTranscript: (callback: (data: SpeechTranscriptEvent) => void) => () => void - onFinalTranscript: (callback: (data: SpeechTranscriptEvent) => void) => () => void - onDownloadProgress: ( - callback: (data: { modelId: string; progress: number }) => void - ) => () => void - onReady: (callback: (data: SpeechLifecycleEvent) => void) => () => void - onStopped: (callback: (data: SpeechLifecycleEvent) => void) => () => void - onError: (callback: (data: SpeechErrorEvent) => void) => () => void - } + fs: FilesystemApi['fs'] + git: Merged + ui: Merged + runtime: RuntimeApi['runtime'] + runtimeEnvironments: RuntimeApi['runtimeEnvironments'] + rateLimits: RateLimitsApi + minimaxCredentials: MinimaxCredentialsApi + grokAccounts: GrokAccountsApi + ssh: SshApi + automations: AutomationsApi + wsl: RuntimeApi['wsl'] + pwsh: RuntimeApi['pwsh'] + gitBash: RuntimeApi['gitBash'] + plugins: PluginsApi + agentStatus: AgentStatusApi + mobile: MobileApi + speech: SpeechApi } +export type { ClaudeUsageApi, CodexUsageApi, OpenCodeUsageApi } from './api/agent-usage-api' +export type { AiVaultApi } from './api/ai-vault-api' +export type { AppApi } from './api/app-api' +export type { BrowserApi, DetectedBrowserInfo, DetectedBrowserProfileInfo } from './api/browser-api' +export type { EmulatorApi } from './api/emulator-api' +export type { ExportApi } from './api/filesystem-api' +export type { + NativeChatApi, + NativeChatAppendedMessages, + NativeChatAppendedPayload, + NativeChatReadSessionResult, + NativeChatSubscribeArgs, + NativeChatSubscriptionFrame +} from './api/native-chat-api' +export type { + PluginHostInstallResult, + PluginHostInstallSource, + PluginHostListEntry, + PluginHostLogLine, + PluginHostPanel, + PluginHostStatus, + PluginMarketplaceHostInstallPreview, + PluginMarketplaceHostListing, + PluginMarketplaceHostSourceState +} from './api/plugin-host-api' +export type { + PreflightApi, + PreflightRuntimeContext, + PreflightStatus, + RefreshAgentsResult +} from './api/preflight-api' +export type { + PtyManagementApi, + PtyManagementMacTccAttributionHealth, + PtyManagementSession +} from './api/pty-management-api' +export type { + ShellOpenExternalEditorRequest, + ShellOpenExternalEditorResult, + ShellOpenLocalPathResult +} from './api/shell-api' +export type { + DiagnosticsBundlePayload, + DiagnosticsStatusPayload, + DiagnosticsUploadPayload, + MemoryApi, + StatsApi +} from './api/telemetry-api' + declare global { // oxlint-disable-next-line typescript-eslint/consistent-type-definitions -- declaration merging requires interface interface Window { diff --git a/src/preload/api/agent-account-api.ts b/src/preload/api/agent-account-api.ts new file mode 100644 index 00000000000..43e8542cddd --- /dev/null +++ b/src/preload/api/agent-account-api.ts @@ -0,0 +1,62 @@ +import type { ClaudeRateLimitAccountsState, CodexRateLimitAccountsState } from '../../shared/types' +import type { CodexConfigSyncStatus } from '../../shared/codex-config-sync-types' +import type { GrokAccountStatus } from '../../shared/rate-limit-types' + +export type CodexAccountsApi = { + list: () => Promise + add: (args?: { + runtime?: 'host' | 'wsl' + wslDistro?: string | null + }) => Promise + reauthenticate: (args: { accountId: string }) => Promise + remove: (args: { accountId: string }) => Promise + select: (args: { + accountId: string | null + runtime?: 'host' | 'wsl' + wslDistro?: string | null + }) => Promise + /** Live PTYs whose baked CODEX_HOME still points at a deselected account. */ + listStalePanes: (args: { ptyIds: string[] }) => Promise< + { + ptyId: string + launchAccountId: string | null + activeAccountId: string | null + /** Optional for compatibility with a pre-reason main process. */ + reason?: 'account-change' | 'home-route-change' + }[] + > + /** The selection lane each PTY launched from, keyed by pty id; unrecorded panes are absent. */ + listRecordedPaneLanes: (args: { ptyIds: string[] }) => Promise> + /** Drops launch records so a dismissed prompt stays dismissed across restarts. */ + forgetStalePanes: (args: { ptyIds: string[] }) => Promise +} + +export type ClaudeAccountsApi = { + list: () => Promise + add: (args?: { + runtime?: 'host' | 'wsl' + wslDistro?: string | null + }) => Promise + cancelPendingLogin: () => Promise + reauthenticate: (args: { accountId: string }) => Promise + remove: (args: { accountId: string }) => Promise + select: (args: { + accountId: string | null + runtime?: 'host' | 'wsl' + wslDistro?: string | null + }) => Promise +} + +export type GrokAccountsApi = { + getStatus: () => Promise +} + +export type MinimaxCredentialsApi = { + getStatus: () => Promise<{ configured: boolean }> + saveCookie: (cookie: string) => Promise<{ configured: boolean }> + clearCookie: () => Promise<{ configured: boolean }> +} + +export type CodexConfigSyncApi = { + status: () => Promise +} diff --git a/src/preload/api/agent-hook-api.ts b/src/preload/api/agent-hook-api.ts new file mode 100644 index 00000000000..df782419c5c --- /dev/null +++ b/src/preload/api/agent-hook-api.ts @@ -0,0 +1,51 @@ +import type { OrcaHooks, WorktreeSetupLaunch } from '../../shared/types' +import type { ExecutionHostId } from '../../shared/execution-host' +import type { SetupScriptImportCandidate } from '../../shared/setup-script-imports' +import type { AgentHookInstallStatus } from '../../shared/agent-hook-types' + +export type HooksApi = { + check: (args: { repoId: string; hostId?: ExecutionHostId }) => Promise<{ + status?: 'ok' | 'error' + hasHooks: boolean + hooks: OrcaHooks | null + mayNeedUpdate: boolean + }> + inspectSetupScriptImports: (args: { + repoId: string + hostId?: ExecutionHostId + }) => Promise + createIssueCommandRunner: (args: { + repoId: string + worktreePath: string + command: string + }) => Promise + readIssueCommand: (args: { repoId: string; hostId?: ExecutionHostId }) => Promise<{ + status?: 'ok' | 'error' + localContent: string | null + sharedContent: string | null + effectiveContent: string | null + localFilePath: string + source: 'local' | 'shared' | 'none' + }> + writeIssueCommand: (args: { + repoId: string + content: string + hostId?: ExecutionHostId + }) => Promise +} + +export type AgentHooksApi = { + claudeStatus: () => Promise + openClaudeStatus: () => Promise + codexStatus: () => Promise + geminiStatus: () => Promise + antigravityStatus: () => Promise + ampStatus: () => Promise + cursorStatus: () => Promise + droidStatus: () => Promise + commandCodeStatus: () => Promise + grokStatus: () => Promise + copilotStatus: () => Promise + hermesStatus: () => Promise + devinStatus: () => Promise +} diff --git a/src/preload/api/agent-skill-api.ts b/src/preload/api/agent-skill-api.ts new file mode 100644 index 00000000000..489d48c3d5d --- /dev/null +++ b/src/preload/api/agent-skill-api.ts @@ -0,0 +1,16 @@ +import type { SkillDiscoveryResult, SkillDiscoveryTarget } from '../../shared/skills' +import type { + SkillFreshnessInventory, + SkillUpdateRun, + SkillUpdateStartResult +} from '../../shared/skill-freshness' + +export type SkillsApi = { + discover: (target?: SkillDiscoveryTarget) => Promise + freshnessInventory: () => Promise + startUpdateRun: (names: string[]) => Promise + cancelUpdateRun: () => Promise + acknowledgeUpdateRun: () => Promise + getUpdateRun: () => Promise + onUpdateRun: (callback: (run: SkillUpdateRun) => void) => () => void +} diff --git a/src/preload/api/agent-status-api.ts b/src/preload/api/agent-status-api.ts new file mode 100644 index 00000000000..6e61114df47 --- /dev/null +++ b/src/preload/api/agent-status-api.ts @@ -0,0 +1,52 @@ +import type { + AgentStatusClearIpcPayload, + AgentStatusIpcPayload, + MigrationUnsupportedPtyEntry +} from '../../shared/agent-status-types' +import type { AgentInterruptInferenceRequest } from '../../shared/agent-interrupt-intent' +import type { AgentQuestionAnsweredInferenceRequest } from '../../shared/agent-question-answered-intent' +import type { ComputerAwakeStatus } from '../../shared/computer-awake-mode' + +export type AgentStatusApi = { + /** Listen for agent status updates forwarded from native hook receivers. */ + onSet: (callback: (data: AgentStatusIpcPayload) => void) => () => void + /** Listen for main-process cleanup that evicted cached hook status. */ + onClear: (callback: (data: AgentStatusClearIpcPayload) => void) => () => void + /** Return the current main-process hook cache after renderer hydration. */ + getSnapshot: () => Promise + inferInterrupt: (request: AgentInterruptInferenceRequest) => Promise + /** Guarded clear for an answered AskUserQuestion wait — the CLI emits no hook at answer time, so the renderer reports the submit keystroke. */ + inferQuestionAnswered: (request: AgentQuestionAnsweredInferenceRequest) => Promise + /** Listen for PTYs on a legacy numeric pane key that have registry-backed UUID pane proof. */ + onMigrationUnsupported: (callback: (entry: MigrationUnsupportedPtyEntry) => void) => () => void + onMigrationUnsupportedClear: (callback: (data: { ptyId: string }) => void) => () => void + onLegacyWorkerTerminalRecovery: ( + callback: (data: { + paneKey: string + resolution: 'adopted' | 'exited' | 'rolled_back' + ptyId?: string + }) => void + ) => () => void + getMigrationUnsupportedSnapshot: () => Promise + /** Drop a paneKey from the main-process hook cache and on-disk last-status file. Fire-and-forget. */ + drop: (paneKey: string) => void + /** Drop every cached hook status under one terminal tab prefix. Fire-and-forget. */ + dropByTabPrefix: (tabId: string) => void + /** Permanently retire one pane's hook authority while siblings stay live. */ + retirePaneAuthority: (paneKey: string) => void + /** Move hook authority when a live pane is detached into another tab. */ + transferPaneAuthority: (args: { fromPaneKey: string; toPaneKey: string; ptyId?: string }) => void +} + +export type AgentTrustApi = { + markTrusted: (args: { + preset: 'cursor' | 'copilot' | 'codex' + workspacePath: string + connectionId?: string + }) => Promise +} + +export type AgentAwakeApi = { + getStatus: () => Promise + onChanged: (callback: (status: ComputerAwakeStatus) => void) => () => void +} diff --git a/src/preload/api/agent-usage-api.ts b/src/preload/api/agent-usage-api.ts new file mode 100644 index 00000000000..59322ad9bad --- /dev/null +++ b/src/preload/api/agent-usage-api.ts @@ -0,0 +1,59 @@ +import type { ClaudeUsageBreakdownKind, ClaudeUsageSnapshot } from '../../shared/claude-usage-types' +import type { CodexUsageBreakdownKind, CodexUsageSnapshot } from '../../shared/codex-usage-types' +import type { + OpenCodeUsageBreakdownKind, + OpenCodeUsageSnapshot +} from '../../shared/opencode-usage-types' +import type { + CodexRateLimitResetResult, + RateLimitRuntimeTarget, + RateLimitState +} from '../../shared/rate-limit-types' + +export type UsageProviderSnapshot = { + scanState: unknown + summary: { scope: string; range: string } + daily: unknown[] + modelBreakdown: unknown[] + recentSessions: unknown[] +} + +export type UsageQueryArgs = Pick< + Snapshot['summary'], + 'scope' | 'range' +> + +export type UsageProviderApi = { + getScanState: () => Promise + setEnabled: (args: { enabled: boolean }) => Promise + refresh: (args?: { force?: boolean }) => Promise + getSnapshot: (args: UsageQueryArgs & { limit?: number }) => Promise + getSummary: (args: UsageQueryArgs) => Promise + getDaily: (args: UsageQueryArgs) => Promise + getBreakdown: ( + args: UsageQueryArgs & { kind: BreakdownKind } + ) => Promise + getRecentSessions: ( + args: UsageQueryArgs & { limit?: number } + ) => Promise +} + +export type ClaudeUsageApi = UsageProviderApi + +export type CodexUsageApi = UsageProviderApi + +export type OpenCodeUsageApi = UsageProviderApi + +export type RateLimitsApi = { + get: () => Promise + refresh: () => Promise + refreshCodexForTarget: (target: RateLimitRuntimeTarget) => Promise + consumeCodexResetCredit: () => Promise + refreshClaudeForTarget: (target: RateLimitRuntimeTarget) => Promise + setPollingInterval: (ms: number) => Promise + fetchInactiveClaudeAccounts: () => Promise + fetchInactiveCodexAccounts: () => Promise + refreshMiniMax: () => Promise + refreshGrok: () => Promise + onUpdate: (callback: (state: RateLimitState) => void) => () => void +} diff --git a/src/preload/api/ai-vault-api.ts b/src/preload/api/ai-vault-api.ts new file mode 100644 index 00000000000..10cf8a4d515 --- /dev/null +++ b/src/preload/api/ai-vault-api.ts @@ -0,0 +1,37 @@ +import type { + AiVaultDeleteSessionArgs, + AiVaultDeleteSessionResult +} from '../../shared/ai-vault-session-deletion' +import type { + AiVaultFirstUserPromptArgs, + AiVaultFirstUserPromptResult, + AiVaultListArgs, + AiVaultListResult, + AiVaultSubagentListArgs, + AiVaultSubagentListResult +} from '../../shared/ai-vault-types' +import type { + AiVaultSessionTitlesArgs, + AiVaultSessionTitlesResult +} from '../../shared/ai-vault-session-title' +import type { + AiVaultPrepareSessionResumeArgs, + AiVaultPrepareSessionResumeResult +} from '../../shared/ai-vault-resume-preparation' + +export type AiVaultApi = { + listSessions: (args?: AiVaultListArgs) => Promise + resolveSessionTitles: (args: AiVaultSessionTitlesArgs) => Promise + cancelListSessions: (args: { requestToken: string }) => Promise + prepareSessionResume: ( + args: AiVaultPrepareSessionResumeArgs + ) => Promise + /** Lists the Task subagent transcripts of one session, on demand. */ + listSubagentSessions: (args: AiVaultSubagentListArgs) => Promise + /** Full first user prompt for copy/reuse (re-parses one transcript). */ + getFirstUserPrompt: (args: AiVaultFirstUserPromptArgs) => Promise + /** Moves a deletable session's transcript to the OS trash; local sessions only. */ + deleteSession: (args: AiVaultDeleteSessionArgs) => Promise + /** Fires when any app window regains OS focus; returns an unsubscribe. */ + onWindowFocused: (callback: () => void) => () => void +} diff --git a/src/preload/api/app-api.ts b/src/preload/api/app-api.ts new file mode 100644 index 00000000000..a03960fd531 --- /dev/null +++ b/src/preload/api/app-api.ts @@ -0,0 +1,82 @@ +import type { AppIdentity } from '../../shared/app-identity' +import type { E2EConfig } from '../../shared/e2e-config' +import type { ExecutionHostId } from '../../shared/execution-host' +import type { + WriteTerminalRenderDesyncEvidenceArgs, + WriteTerminalRenderDesyncEvidenceResult +} from '../../shared/terminal-render-desync-evidence' +import type { + FloatingTerminalCwdRequest, + MarkdownDocument, + PersistedUIState, + WorkspaceSessionState +} from '../../shared/types' + +export type AppApi = { + /** Returns the app identity currently exposed to native chrome and the titlebar. */ + getIdentity: () => Promise + /** Returns a URL base for feature-wall assets. In dev this is Vite /@fs; + * in packaged builds this is file:// resources. Renderer appends filenames. */ + getFeatureWallAssetBaseUrl: () => Promise + /** Relaunches the app (app.relaunch() + app.exit(0)) for settings that need a full restart to apply. */ + relaunch: () => Promise + /** Restarts Orca through the normal quit pipeline so daemon-backed terminal + * sessions survive and can reattach after the new process starts. */ + restart: () => Promise + /** Reloads the current app renderer through main so expected renderer + * teardown can be classified before Electron emits process-gone events. */ + reload: () => Promise + /** Stages the renderer's final state synchronously before unload. */ + stageBeforeUnloadSync: (args: { + sessions: { state: WorkspaceSessionState; hostId?: ExecutionHostId }[] + ui: Partial + }) => void + /** Resolves once the last staged checkpoint is durably written; rejects if that + * write failed, so a reload/restart can abort instead of losing the snapshot. */ + awaitBeforeUnloadCheckpoint: () => Promise + /** Resolves when the daemon PTY provider and hook receiver have either + * started or failed open for the first BrowserWindow. */ + awaitFirstWindowStartupServices: () => Promise + /** Reconciles legacy worker authority around persisted terminal reconnect. */ + recoverLegacyWorkerTerminalsForRendererStartup: () => Promise + /** Emits a startup benchmark marker when ORCA_STARTUP_DIAGNOSTICS is enabled. */ + startupDiagnostic: (event: string, details?: Record) => Promise + /** macOS active input mode, or layout ID when no IME is selected (e.g. `com.apple.keylayout.PolishPro`). + * Distinguishes CJK IMEs and Option-layer-composing layouts that look like US QWERTY (issue #1205). + * Returns null on non-Darwin or when the defaults read fails. */ + getKeyboardInputSourceId: () => Promise + /** Updates the macOS Dock unread badge. No-op on Windows/Linux. */ + setUnreadDockBadgeCount: (count: number) => Promise + /** Resolves the launch directory for global Floating Terminal tabs. */ + getFloatingTerminalCwd: (args?: FloatingTerminalCwdRequest) => Promise + /** Resolves Orca's app-owned directory for auto-created Floating Workspace + * markdown notes. */ + getFloatingMarkdownDirectory: () => Promise + /** Opens a native picker for markdown documents, rooted in the floating + * workspace, and authorizes the selected file for editor reads/writes. */ + pickFloatingMarkdownDocument: () => Promise + /** Opens a native directory picker and authorizes the selected directory + * for Floating Workspace markdown file creation. */ + pickFloatingWorkspaceDirectory: () => Promise + /** Persists flag-gated terminal render evidence under app-owned userData. */ + writeTerminalRenderDesyncEvidence: ( + args: WriteTerminalRenderDesyncEvidenceArgs + ) => Promise +} + +/** Panel contribution as surfaced by the main-process plugin service. */ + +export type PlatformApi = { + get: () => { + platform: NodeJS.Platform + osRelease: string + arch: string + /** Login shell or ComSpec when available. */ + shell: string + displayServer: 'wayland' | 'x11' | null + } +} + +export type E2EApi = { + getConfig: () => E2EConfig +} diff --git a/src/preload/api/automation-api.ts b/src/preload/api/automation-api.ts new file mode 100644 index 00000000000..90f13e4ce1e --- /dev/null +++ b/src/preload/api/automation-api.ts @@ -0,0 +1,37 @@ +import type { + Automation, + AutomationCreateInput, + AutomationDispatchRequest, + AutomationDispatchResult, + ExternalAutomationCreateInput, + ExternalAutomationActionInput, + ExternalAutomationManager, + ExternalAutomationRunsInput, + ExternalAutomationRunsPage, + ExternalAutomationUpdateInput, + AutomationRun, + AutomationPrecheckResult, + AutomationUpdateInput +} from '../../shared/automations-types' + +export type AutomationsApi = { + list: () => Promise + listRuns: (args?: { automationId?: string }) => Promise + listExternalManagers: () => Promise + listExternalRuns: (input: ExternalAutomationRunsInput) => Promise + createExternal: (input: ExternalAutomationCreateInput) => Promise + updateExternal: (input: ExternalAutomationUpdateInput) => Promise + runExternalAction: (input: ExternalAutomationActionInput) => Promise + create: (input: AutomationCreateInput) => Promise + update: (args: { id: string; updates: AutomationUpdateInput }) => Promise + delete: (args: { id: string }) => Promise + runNow: (args: { id: string }) => Promise + runPrecheck: (args: { + automationId: string + runId: string + }) => Promise + markDispatchResult: (result: AutomationDispatchResult) => Promise + snapshotWorkspaceName: (args: { workspaceId: string; displayName: string }) => Promise + rendererReady: () => Promise + onDispatchRequested: (callback: (request: AutomationDispatchRequest) => void) => () => void +} diff --git a/src/preload/api/browser-api.ts b/src/preload/api/browser-api.ts new file mode 100644 index 00000000000..b6591162d19 --- /dev/null +++ b/src/preload/api/browser-api.ts @@ -0,0 +1,132 @@ +import type { BrowserSetAnnotationViewportBridgeArgs } from '../../shared/browser-annotation-viewport-bridge' +import type { + BrowserSetGrabModeArgs, + BrowserSetGrabModeResult, + BrowserAwaitGrabSelectionArgs, + BrowserGrabResult, + BrowserCancelGrabArgs, + BrowserCaptureSelectionScreenshotArgs, + BrowserCaptureSelectionScreenshotResult, + BrowserExtractHoverArgs, + BrowserExtractHoverResult +} from '../../shared/browser-grab-types' +import type { + BrowserContextMenuDismissedEvent, + BrowserContextMenuRequestedEvent, + BrowserDownloadFinishedEvent, + BrowserDownloadProgressEvent, + BrowserDownloadRequestedEvent, + BrowserPermissionDeniedEvent, + BrowserPopupEvent +} from '../../shared/browser-guest-events' +import type { + BrowserCookieImportResult, + BrowserCertificateFailure, + BrowserCertificateProceedResult, + BrowserLoadError, + BrowserSessionProfile, + BrowserSessionProfileCreateOptions, + BrowserSessionProfileScope, + BrowserSessionProfileSource, + BrowserViewportOverride +} from '../../shared/types' + +export type BrowserApi = { + registerGuest: (args: { + browserPageId: string + workspaceId: string + worktreeId: string + sessionProfileId?: string | null + webContentsId: number + }) => Promise + isGuestRegistered: (args: { browserPageId: string; webContentsId: number }) => Promise + repairGuestRegistration: (args: { + browserPageId: string + workspaceId: string + worktreeId: string + sessionProfileId?: string | null + webContentsId: number + }) => Promise + unregisterGuest: (args: { browserPageId: string }) => Promise + openDevTools: (args: { browserPageId: string }) => Promise + setViewportOverride: (args: { + browserPageId: string + override: BrowserViewportOverride | null + }) => Promise + setAnnotationViewportBridge: (args: BrowserSetAnnotationViewportBridgeArgs) => Promise + onGuestLoadFailed: ( + callback: (args: { browserPageId: string; loadError: BrowserLoadError }) => void + ) => () => void + onCertificateFailureChanged: ( + callback: (event: { browserPageId: string; failure: BrowserCertificateFailure | null }) => void + ) => () => void + proceedCertificate: (args: { + browserPageId: string + challengeId: string + }) => Promise + onPermissionDenied: (callback: (event: BrowserPermissionDeniedEvent) => void) => () => void + onPopup: (callback: (event: BrowserPopupEvent) => void) => () => void + onDownloadRequested: (callback: (event: BrowserDownloadRequestedEvent) => void) => () => void + onDownloadProgress: (callback: (event: BrowserDownloadProgressEvent) => void) => () => void + onDownloadFinished: (callback: (event: BrowserDownloadFinishedEvent) => void) => () => void + onContextMenuRequested: ( + callback: (event: BrowserContextMenuRequestedEvent) => void + ) => () => void + onContextMenuDismissed: ( + callback: (event: BrowserContextMenuDismissedEvent) => void + ) => () => void + onNavigationUpdate: ( + callback: (event: { browserPageId: string; url: string; title: string }) => void + ) => () => void + onActivateView: ( + callback: (data: { worktreeId?: string; browserPageId?: string }) => void + ) => () => void + onPaneFocus: ( + callback: (data: { worktreeId: string | null; browserPageId: string }) => void + ) => () => void + onOpenLinkInOrcaTab: ( + callback: (event: { browserPageId: string; url: string }) => void + ) => () => void + cancelDownload: (args: { downloadId: string }) => Promise + setGrabMode: (args: BrowserSetGrabModeArgs) => Promise + awaitGrabSelection: (args: BrowserAwaitGrabSelectionArgs) => Promise + cancelGrab: (args: BrowserCancelGrabArgs) => Promise + captureSelectionScreenshot: ( + args: BrowserCaptureSelectionScreenshotArgs + ) => Promise + extractHoverPayload: (args: BrowserExtractHoverArgs) => Promise + onGrabModeToggle: (callback: (browserPageId: string) => void) => () => void + onGrabActionShortcut: ( + callback: (args: { browserPageId: string; key: 'c' | 's' }) => void + ) => () => void + sessionListProfiles: () => Promise + sessionCreateProfile: ( + args: { + scope: BrowserSessionProfileScope + label: string + } & BrowserSessionProfileCreateOptions + ) => Promise + sessionDeleteProfile: (args: { profileId: string }) => Promise + sessionImportCookies: (args: { profileId: string }) => Promise + sessionResolvePartition: (args: { profileId: string | null }) => Promise + sessionDetectBrowsers: () => Promise + sessionImportFromBrowser: (args: { + profileId: string + browserFamily: string + browserProfile?: string + }) => Promise + sessionClearDefaultCookies: () => Promise + notifyActiveTabChanged: (args: { browserPageId: string }) => Promise +} + +export type DetectedBrowserProfileInfo = { + name: string + directory: string +} + +export type DetectedBrowserInfo = { + family: BrowserSessionProfileSource['browserFamily'] + label: string + profiles: DetectedBrowserProfileInfo[] + selectedProfile: string +} diff --git a/src/preload/api/cli-install-api.ts b/src/preload/api/cli-install-api.ts new file mode 100644 index 00000000000..cadf7f5cfb7 --- /dev/null +++ b/src/preload/api/cli-install-api.ts @@ -0,0 +1,10 @@ +import type { CliInstallStatus } from '../../shared/cli-install-types' + +export type CliApi = { + getInstallStatus: () => Promise + install: () => Promise + remove: () => Promise + getWslInstallStatus: (args?: { distro?: string | null }) => Promise + installWsl: (args?: { distro?: string | null }) => Promise + removeWsl: (args?: { distro?: string | null }) => Promise +} diff --git a/src/preload/api/crash-report-api.ts b/src/preload/api/crash-report-api.ts new file mode 100644 index 00000000000..c99e1ccf767 --- /dev/null +++ b/src/preload/api/crash-report-api.ts @@ -0,0 +1,38 @@ +import type { + CrashReportBreadcrumbData, + CrashReportCopyDiagnosticsArgs, + CrashReportRecord, + CrashReportSubmitArgs, + CrashReportSubmitResult, + ReactErrorBoundaryReportArgs, + ReactErrorBoundaryReportResult +} from '../../shared/crash-reporting' +import type { RendererHeapStatistics } from '../../shared/renderer-heap-statistics' + +export type CrashReportsApi = { + getLatestPending: () => Promise + getLatestReport: () => Promise + dismiss: (args: { reportId: string }) => Promise + recordRendererError: ( + args: ReactErrorBoundaryReportArgs + ) => Promise + recordBreadcrumb: (args: { name: string; data?: CrashReportBreadcrumbData }) => void + submit: (args: CrashReportSubmitArgs) => Promise + copyLatestDiagnostics: ( + args?: CrashReportCopyDiagnosticsArgs + ) => Promise<{ ok: true } | { ok: false; error: string }> + /** Exact V8/Blink heap sizes; null when the runtime withholds them. */ + readHeapStatistics: () => RendererHeapStatistics | null +} + +export type FeedbackApi = { + submit: (args: { + feedback: string + submitAnonymously?: boolean + githubLogin: string | null + githubEmail: string | null + images?: { contentType: string; data: Uint8Array }[] + }) => Promise< + { ok: true; imagesDelivered?: boolean } | { ok: false; status: number | null; error: string } + > +} diff --git a/src/preload/api/dashboard-api.ts b/src/preload/api/dashboard-api.ts new file mode 100644 index 00000000000..aa8d6dabd8a --- /dev/null +++ b/src/preload/api/dashboard-api.ts @@ -0,0 +1,42 @@ +import type { + DashboardRevealAgentArgs, + DashboardSleepWorkspaceArgs, + DashboardSnapshot, + DashboardSpawnAgentArgs +} from '../../shared/dashboard-snapshot' +import type { + TerminalPreviewConnectResult, + TerminalPreviewDataPayload +} from '../../shared/terminal-preview' + +export type DashboardApi = { + openPopout: (view?: 'board' | 'map') => Promise + publishSnapshot: (snapshot: DashboardSnapshot) => Promise + getPopoutOpen: () => Promise + onPopoutOpenChanged: (callback: (open: boolean) => void) => () => void + onSnapshotRequested: (callback: () => void) => () => void + onRevealAgent: (callback: (args: DashboardRevealAgentArgs) => void) => () => void + onAckAgent: (callback: (paneKey: string) => void) => () => void + onSpawnAgent: (callback: (args: DashboardSpawnAgentArgs) => void) => () => void + onSleepWorkspace: (callback: (args: DashboardSleepWorkspaceArgs) => void) => () => void + requestSnapshot: () => Promise + onSnapshot: (callback: (snapshot: DashboardSnapshot) => void) => () => void + onViewRequested: (callback: (view: 'board' | 'map') => void) => () => void + revealAgent: (args: DashboardRevealAgentArgs) => Promise + ackAgent: (paneKey: string) => Promise + spawnAgent: (args: DashboardSpawnAgentArgs) => Promise + sleepWorkspace: (args: DashboardSleepWorkspaceArgs) => Promise +} + +export type TerminalPreviewApi = { + connect: ( + ptyId: string, + opts?: { scrollbackRows?: number } + ) => Promise + input: (ptyId: string, data: string) => Promise + /** Claim the PTY grid for the preview dialog; resolves to the size actually in effect. */ + fit: (ptyId: string, cols: number, rows: number) => Promise<{ cols: number; rows: number } | null> + ack: (ptyId: string, bytes: number) => Promise + unsubscribe: (ptyId: string) => Promise + onData: (callback: (payload: TerminalPreviewDataPayload) => void) => () => void +} diff --git a/src/preload/api/emulator-api.ts b/src/preload/api/emulator-api.ts new file mode 100644 index 00000000000..5781045bc67 --- /dev/null +++ b/src/preload/api/emulator-api.ts @@ -0,0 +1,37 @@ +export type EmulatorApi = { + onPaneFocus: (callback: (data: { worktreeId: string }) => void) => () => void + onAutoAttach: ( + callback: (data: { + worktreeId: string + info: { deviceUdid: string; streamUrl: string; wsUrl: string; axUrl?: string } + }) => void + ) => () => void + startFrameStream: (args: { streamUrl: string; streamKey?: string }) => Promise<{ + streamId: string + }> + stopFrameStream: (args: { streamId: string }) => Promise + onFrameStreamFrame: ( + callback: (data: { streamId: string; bytes: ArrayBuffer }) => void + ) => () => void + onFrameStreamError: ( + callback: (data: { streamId: string; message: string }) => void + ) => () => void + startVideoStream: (args: { deviceId: string; streamId: string }) => Promise<{ streamId: string }> + stopVideoStream: (args: { streamId: string }) => Promise + onVideoStreamMeta: ( + callback: (data: { + streamId: string + deviceId: string + meta: { codecId: string; width: number; height: number } + }) => void + ) => () => void + onVideoStreamFrame: ( + callback: (data: { + streamId: string + deviceId: string + config: boolean + keyFrame: boolean + bytes: ArrayBuffer + }) => void + ) => () => void +} diff --git a/src/preload/api/ephemeral-vm-api.ts b/src/preload/api/ephemeral-vm-api.ts new file mode 100644 index 00000000000..e3b6ea202fd --- /dev/null +++ b/src/preload/api/ephemeral-vm-api.ts @@ -0,0 +1,72 @@ +import type { OrcaHooks } from '../../shared/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' +import type { EphemeralVmRuntimeRecord } from '../../shared/ephemeral-vm-runtimes' + +export type EphemeralVmApi = { + listRecipes: (args: { repoId: string }) => Promise<{ + status: 'ok' | 'error' + repoPath: string | null + recipes: OrcaHooks['environmentRecipes'] + diagnostics: NonNullable + message?: string + }> + listRecipeCatalog: () => Promise< + { + repoId: string + repoName: string + repoPath: string + recipes: NonNullable + diagnostics: NonNullable + }[] + > + doctor: (args: { repoId: string; recipeId: string }) => Promise + provision: (args: { + repoId: string + recipeId: string + workspaceName?: string + projectId?: string + workspaceId?: string + branch?: string + ref?: string + provisionId?: string + }) => Promise< + | { + ok: true + connectionType: 'orca-server' + runtime: EphemeralVmRuntimeRecord + environment: PublicKnownRuntimeEnvironment + stderr: string + warnings: EphemeralVmRecipeResultWarning[] + } + | { + ok: true + connectionType: 'ssh' + runtime: EphemeralVmRuntimeRecord + sshTargetId: string + stderr: string + warnings: EphemeralVmRecipeResultWarning[] + } + | { ok: false; error: string; stderr: string; stdout: string } + > + cancelProvision: (args: { provisionId: string }) => Promise<{ cancelled: boolean }> + onProvisionEvent: ( + callback: (event: { provisionId: string; stream: 'stdout' | 'stderr'; chunk: string }) => void + ) => () => void + listRuntimes: () => Promise + attachWorkspace: (args: { + runtimeId: string + workspaceId: string + }) => Promise + suspendWorkspace: (args: { workspaceId: string }) => Promise + resumeWorkspace: (args: { workspaceId: string }) => Promise + cleanup: (args: { runtimeId: string }) => Promise + getCleanupCommand: (args: { runtimeId: string }) => Promise<{ + runtimeId: string + command: string | null + payloadJson: string + cleanupDisabled: boolean + message?: string + }> +} diff --git a/src/preload/api/filesystem-api.ts b/src/preload/api/filesystem-api.ts new file mode 100644 index 00000000000..fdc1ac73250 --- /dev/null +++ b/src/preload/api/filesystem-api.ts @@ -0,0 +1,208 @@ +import type { + DirEntry, + FsChangedPayload, + MarkdownDocument, + SearchOptions, + SearchResult +} from '../../shared/types' +import type { + LocalLogTailChangedPayload, + LocalLogTailReadArgs, + LocalLogTailReadResult, + LocalLogTailWatchArgs +} from '../../shared/local-log-tail-types' +import type { SshMutationExpectation } from '../../shared/ssh-types' + +export type ExportApi = { + htmlToPdf: (args: { + html: string + title: string + }) => Promise< + { success: true; filePath: string } | { success: false; cancelled?: boolean; error?: string } + > +} + +export type FilesystemApi = { + fs: { + readDir: (args: { dirPath: string; connectionId?: string }) => Promise + readFile: (args: { + filePath: string + connectionId?: string + includeLocalLogMetadata?: boolean + }) => Promise<{ + content: string + isBinary: boolean + isImage?: boolean + mimeType?: string + fileIdentity?: string + }> + readLocalLogTail: (args: LocalLogTailReadArgs) => Promise + startLocalLogTail: (args: LocalLogTailWatchArgs) => Promise + stopLocalLogTail: (args: { subscriptionId: string }) => Promise + onLocalLogTailChanged: (callback: (payload: LocalLogTailChangedPayload) => void) => () => void + downloadFile: (args: { + filePath: string + connectionId: string + }) => Promise<{ canceled: true } | { canceled: false; destinationPath: string }> + downloadFolder: (args: { + dirPath: string + connectionId: string + }) => Promise<{ canceled: true } | { canceled: false; destinationPath: string }> + saveDownloadedFile: (args: { + suggestedName: string + content: string + encoding: 'utf8' | 'base64' + }) => Promise<{ canceled: true } | { canceled: false; destinationPath: string }> + startDownloadedFile: (args: { + suggestedName: string + }) => Promise< + { canceled: true } | { canceled: false; transferId: string; destinationPath: string } + > + appendDownloadedFileChunk: (args: { + transferId: string + contentBase64: string + }) => Promise<{ ok: true }> + finishDownloadedFile: (args: { + transferId: string + }) => Promise<{ canceled: false; destinationPath: string }> + cancelDownloadedFile: (args: { transferId: string }) => Promise<{ ok: true }> + listMarkdownDocuments: (args: { + rootPath: string + connectionId?: string + }) => Promise + writeFile: ( + args: { + filePath: string + content: string + connectionId?: string + } & SshMutationExpectation + ) => Promise + createFile: ( + args: { + filePath: string + connectionId?: string + } & SshMutationExpectation + ) => Promise + createDir: ( + args: { dirPath: string; connectionId?: string } & SshMutationExpectation + ) => Promise + rename: ( + args: { + oldPath: string + newPath: string + connectionId?: string + } & SshMutationExpectation + ) => Promise + copy: ( + args: { + sourcePath: string + destinationPath: string + connectionId?: string + } & SshMutationExpectation + ) => Promise + deletePath: ( + args: { + targetPath: string + connectionId?: string + recursive?: boolean + } & SshMutationExpectation + ) => Promise + authorizeExternalPath: (args: { targetPath: string }) => Promise + stat: (args: { + filePath: string + connectionId?: string + }) => Promise<{ size: number; isDirectory: boolean; mtime: number }> + pathExists: (args: { filePath: string; connectionId?: string }) => Promise + listFiles: (args: { + rootPath: string + connectionId?: string + excludePaths?: string[] + requestToken?: string + }) => Promise + cancelListFiles: (args: { requestToken: string }) => Promise + search: (args: SearchOptions & { connectionId?: string }) => Promise + importExternalPaths: ( + args: { + sourcePaths: string[] + destDir: string + connectionId?: string + ensureDir?: boolean + } & SshMutationExpectation + ) => Promise<{ + results: ( + | { + sourcePath: string + status: 'imported' + destPath: string + kind: 'file' | 'directory' + renamed: boolean + } + | { + sourcePath: string + status: 'skipped' + reason: 'missing' | 'symlink' | 'permission-denied' | 'unsupported' + } + | { + sourcePath: string + status: 'failed' + reason: string + } + )[] + }> + stageExternalPathsForRuntimeUpload: (args: { sourcePaths: string[] }) => Promise<{ + sources: ( + | { + sourcePath: string + status: 'staged' + name: string + kind: 'file' | 'directory' + entries: ( + | { relativePath: string; kind: 'directory' } + | { relativePath: string; kind: 'file'; contentBase64: string } + )[] + } + | { + sourcePath: string + status: 'skipped' + reason: 'missing' | 'symlink' | 'permission-denied' | 'unsupported' + } + | { + sourcePath: string + status: 'failed' + reason: string + } + )[] + }> + resolveDroppedPathsForAgent: ( + args: { + paths: string[] + worktreePath: string + connectionId?: string + } & SshMutationExpectation + ) => Promise<{ + resolvedPaths: string[] + skipped: { + sourcePath: string + reason: 'missing' | 'symlink' | 'permission-denied' | 'unsupported' + }[] + failed: { sourcePath: string; reason: string }[] + }> + watchWorktree: (args: { worktreePath: string; connectionId?: string }) => Promise + unwatchWorktree: (args: { worktreePath: string; connectionId?: string }) => Promise + onFsChanged: (callback: (payload: FsChangedPayload) => void) => () => void + } + notebook: { + runPythonCell: (args: { + filePath: string + code: string + preamble?: string + connectionId?: string | null + }) => Promise<{ + stdout: string + stderr: string + exitCode: number | null + error?: string + }> + } + export: ExportApi +} diff --git a/src/preload/api/git-inspection-api.ts b/src/preload/api/git-inspection-api.ts new file mode 100644 index 00000000000..c23bcdeecaa --- /dev/null +++ b/src/preload/api/git-inspection-api.ts @@ -0,0 +1,123 @@ +import type { + GitBranchCompareResult, + GitCommitCompareResult, + GitConflictOperation, + GitDiffResult, + GitPushTarget, + GitStagingArea, + GitStatusResult, + GitUpstreamStatus +} from '../../shared/types' +import type { GitHistoryOptions, GitHistoryResult } from '../../shared/git-history' +import type { + CommitMessageAgentCapability, + CommitMessageModelCapability +} from '../../shared/commit-message-agent-spec' + +export type GitInspectionApi = { + status: (args: { + worktreePath: string + connectionId?: string + includeIgnored?: boolean + bypassEffectiveUpstreamNegativeCache?: boolean + reuseLineStats?: boolean + /** Merge-base OID to measure the branch line total against; omit to skip the work. */ + branchLineTotalMergeBase?: string + requestToken?: string + }) => Promise + cancelStatus: (args: { requestToken: string }) => Promise + setStatusUpstreamRefWatch: (args: { + worktreeId: string + worktreePath: string + executionHostId: string + connectionId?: string + branch?: string + upstreamName?: string + }) => Promise + submoduleStatus: (args: { + worktreePath: string + submodulePath: string + connectionId?: string + area?: GitStagingArea + }) => Promise + checkIgnored: (args: { + worktreePath: string + paths: string[] + connectionId?: string + }) => Promise + findHugeFoldersToIgnore: (args: { worktreePath: string }) => Promise + history: ( + args: { worktreePath: string; connectionId?: string } & GitHistoryOptions + ) => Promise + conflictOperation: (args: { + worktreePath: string + connectionId?: string + }) => Promise + diff: (args: { + worktreePath: string + filePath: string + staged: boolean + compareAgainstHead?: boolean + connectionId?: string + }) => Promise + branchCompare: (args: { + worktreePath: string + baseRef: string + connectionId?: string + }) => Promise + commitCompare: (args: { + worktreePath: string + commitId: string + connectionId?: string + }) => Promise + upstreamStatus: (args: { + worktreePath: string + connectionId?: string + pushTarget?: GitPushTarget + }) => Promise + branchDiff: (args: { + worktreePath: string + compare: { + baseRef: string + baseOid: string + headOid: string + mergeBase: string + } + filePath: string + oldPath?: string + connectionId?: string + }) => Promise + commitDiff: (args: { + worktreePath: string + commitOid: string + parentOid?: string | null + filePath: string + oldPath?: string + connectionId?: string + }) => Promise + discoverCommitMessageModels: (args: { + agentId: string + worktreePath?: string + connectionId?: string + }) => Promise< + | { + success: true + capability: CommitMessageAgentCapability + models: CommitMessageModelCapability[] + defaultModelId: string + catalogOrigin: 'probe' | 'spec' + } + | { success: false; error: string } + > + remoteFileUrl: (args: { + worktreePath: string + relativePath: string + line: number + connectionId?: string + }) => Promise + remoteCommitUrl: (args: { + worktreePath: string + sha: string + connectionId?: string + }) => Promise +} diff --git a/src/preload/api/git-operation-api.ts b/src/preload/api/git-operation-api.ts new file mode 100644 index 00000000000..fee33db4efd --- /dev/null +++ b/src/preload/api/git-operation-api.ts @@ -0,0 +1,128 @@ +import type { + GitForkSyncExpectedUpstream, + GitForkSyncResult, + GitPushTarget, + TuiAgent +} from '../../shared/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' + +export type GitOperationApi = { + appendGitignore: (args: { worktreePath: string; folderName: string }) => Promise + abortMerge: (args: { worktreePath: string; connectionId?: string }) => Promise + abortRebase: (args: { worktreePath: string; connectionId?: string }) => Promise + fetch: (args: { + worktreePath: string + connectionId?: string + pushTarget?: GitPushTarget + }) => Promise + syncFork: (args: { + worktreePath: string + connectionId?: string + expectedUpstream: GitForkSyncExpectedUpstream + }) => Promise + push: (args: { + worktreePath: string + publish?: boolean + forceWithLease?: boolean + connectionId?: string + pushTarget?: GitPushTarget + }) => Promise + pull: (args: { + worktreePath: string + connectionId?: string + pushTarget?: GitPushTarget + }) => Promise + fastForward: (args: { + worktreePath: string + connectionId?: string + pushTarget?: GitPushTarget + }) => Promise + rebaseFromBase: (args: { + worktreePath: string + baseRef: string + connectionId?: string + }) => Promise + commit: (args: { + worktreePath: string + message: string + connectionId?: string + }) => Promise<{ success: boolean; error?: string }> + generateCommitMessage: (args: { + worktreePath: string + /** Raw (unstripped) worktree meta key; validated against worktreePath in main. */ + worktreeId?: string + repoId?: string + connectionId?: string + sourceControlAiResolvedParams?: ResolvedSourceControlAiGenerationParams + sourceControlAi?: SourceControlAiSettings + agentCmdOverrides?: Partial> + }) => Promise< + | { success: true; message: string; agentLabel?: string } + | { success: false; error: string; canceled?: boolean } + > + cancelGenerateCommitMessage: (args: { + worktreePath: string + connectionId?: string + }) => Promise + generatePullRequestFields: (args: { + worktreePath: string + /** Raw (unstripped) worktree meta key; validated against worktreePath in main. */ + worktreeId?: string + repoId?: string + base: string + title: string + body: string + draft: boolean + provider?: HostedReviewProvider + useTemplate?: boolean + connectionId?: string + sourceControlAiResolvedParams?: ResolvedSourceControlAiGenerationParams + sourceControlAi?: SourceControlAiSettings + agentCmdOverrides?: Partial> + }) => Promise< + | { + success: true + fields: { base: string; title: string; body: string; draft: boolean } + agentLabel?: string + branchChangedByPreparation?: boolean + } + | { + success: false + error: string + canceled?: boolean + branchChangedByPreparation?: boolean + } + > + cancelGeneratePullRequestFields: (args: { + worktreePath: string + connectionId?: string + }) => Promise + stage: (args: { worktreePath: string; filePath: string; connectionId?: string }) => Promise + bulkStage: (args: { + worktreePath: string + filePaths: string[] + connectionId?: string + }) => Promise + unstage: (args: { + worktreePath: string + filePath: string + connectionId?: string + }) => Promise + bulkUnstage: (args: { + worktreePath: string + filePaths: string[] + connectionId?: string + }) => Promise + discard: (args: { + worktreePath: string + filePath: string + connectionId?: string + }) => Promise + bulkDiscard: (args: { + worktreePath: string + filePaths: string[] + connectionId?: string + }) => Promise +} diff --git a/src/preload/api/github-pull-request-api.ts b/src/preload/api/github-pull-request-api.ts new file mode 100644 index 00000000000..245453cb18f --- /dev/null +++ b/src/preload/api/github-pull-request-api.ts @@ -0,0 +1,196 @@ +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 { + GetRateLimitResult, + GitHubCommentResult, + GitHubOwnerRepo, + GitHubPRFile, + GitHubPRFileContents, + GitHubPRRefreshCandidate, + GitHubPRRefreshEnqueueResult, + GitHubPRRefreshEvent, + GitHubPRRefreshReason, + GitHubPRReviewCommentInput, + GitHubReactionContent, + GitHubViewer, + PRCheckDetail, + PRCheckRunDetails, + PRComment, + PRInfo, + PRRefreshOutcome +} from '../../shared/types' +import type { GitHubRepoSelectorArgs } from './github-work-item-api' + +export type GithubPullRequestApi = { + viewer: () => Promise + repoSlug: (args: { + repoPath: string + repoId?: string + }) => Promise<{ owner: string; repo: string; host?: string } | null> + repoUpstream: (args: { + repoPath: string + repoId?: string + }) => Promise<{ owner: string; repo: string; host?: string } | null> + prForBranch: (args: { + repoPath: string + repoId?: string + branch: string + linkedPRNumber?: number | null + fallbackPRNumber?: number | null + acceptMergedFallbackPR?: boolean + currentHeadOid?: string | null + }) => Promise + refreshPRNow: (args: { candidate: GitHubPRRefreshCandidate }) => Promise + enqueuePRRefresh: (args: { + candidate: GitHubPRRefreshCandidate + reason: GitHubPRRefreshReason + priority?: number + }) => Promise + reportVisiblePRRefreshCandidates: (args: { + candidates: GitHubPRRefreshCandidate[] + generation: number + }) => Promise + onPRRefreshEvent: (callback: (event: GitHubPRRefreshEvent) => void) => () => void + prFileContents: ( + args: GitHubRepoSelectorArgs & { + prNumber: number + prRepo?: GitHubOwnerRepo | null + path: string + oldPath?: string + status: GitHubPRFile['status'] + headSha: string + baseSha: string + } + ) => Promise + prChecks: ( + args: GitHubRepoSelectorArgs & { + prNumber: number + headSha?: string + prRepo?: GitHubOwnerRepo | null + noCache?: boolean + } + ) => Promise + prCheckDetails: (args: { + repoPath: string + repoId?: string + sourceContext?: TaskSourceContext | null + checkRunId?: number + workflowRunId?: number + checkName?: string + url?: string | null + prRepo?: GitHubOwnerRepo | null + }) => Promise + rerunPRChecks: ( + args: GitHubRepoSelectorArgs & { + prNumber: number + headSha?: string + failedOnly?: boolean + prRepo?: GitHubOwnerRepo | null + } + ) => Promise<{ ok: true; count: number } | { ok: false; error: string }> + prComments: (args: { + repoPath: string + repoId?: string + sourceContext?: TaskSourceContext | null + prNumber: number + prRepo?: GitHubOwnerRepo | null + noCache?: boolean + }) => Promise + setPRCommentReaction: (args: { + repoPath: string + repoId?: string + sourceContext?: TaskSourceContext | null + reactionSubjectId: string + content: GitHubReactionContent + reacted: boolean + prRepo?: GitHubOwnerRepo | null + }) => Promise + resolveReviewThread: (args: { + repoPath: string + repoId?: string + sourceContext?: TaskSourceContext | null + threadId: string + resolve: boolean + prRepo?: GitHubOwnerRepo | null + }) => Promise + setPRFileViewed: ( + args: GitHubRepoSelectorArgs & { + prNumber: number + prRepo?: GitHubOwnerRepo | null + pullRequestId: string + path: string + viewed: boolean + } + ) => Promise + updatePRTitle: (args: { + repoPath: string + repoId?: string + prNumber: number + title: string + prRepo?: GitHubOwnerRepo | null + }) => Promise + mergePR: ( + args: GitHubRepoSelectorArgs & { + prNumber: number + method?: 'merge' | 'squash' | 'rebase' + prRepo?: GitHubOwnerRepo | null + } + ) => Promise<{ ok: true } | { ok: false; error: string }> + setPRAutoMerge: ( + args: GitHubRepoSelectorArgs & { + prNumber: number + enabled: boolean + method?: 'merge' | 'squash' | 'rebase' + prRepo?: GitHubOwnerRepo | null + } + ) => Promise<{ ok: true } | { ok: false; error: string }> + updatePRState: ( + args: GitHubRepoSelectorArgs & { + prNumber: number + updates: { state: 'open' | 'closed' } + prRepo?: GitHubOwnerRepo | null + } + ) => Promise<{ ok: true } | { ok: false; error: string }> + requestPRReviewers: ( + args: GitHubRepoSelectorArgs & { + prNumber: number + reviewers: string[] + prRepo?: GitHubOwnerRepo | null + } + ) => Promise<{ ok: true } | { ok: false; error: string }> + removePRReviewers: ( + args: GitHubRepoSelectorArgs & { + prNumber: number + reviewers: string[] + prRepo?: GitHubOwnerRepo | null + } + ) => Promise<{ ok: true } | { ok: false; error: string }> + addPRReviewCommentReply: ( + args: GitHubRepoSelectorArgs & { + prNumber: number + commentId: number + body: string + threadId?: string + path?: string + line?: number + prRepo?: GitHubOwnerRepo | null + } + ) => Promise + addPRReviewComment: ( + args: GitHubPRReviewCommentInput & { + repoId?: string + sourceContext?: TaskSourceContext | null + } + ) => Promise + checkOrcaStarred: () => Promise + starOrca: (source: AppStarSource) => Promise + /** + * GitHub API rate-limit snapshot. Does NOT consume quota (the + * `rate_limit` endpoint is exempt). Cached 30s server-side — pass + * `force: true` to bust after a known-expensive op. + */ + rateLimit: (args?: { force?: boolean }) => Promise + /** Explains scope_missing ProjectV2 failures — notably a shell `GITHUB_TOKEN` shadowing the keyring credential, where `gh auth refresh` is a no-op. */ + diagnoseAuth: (args?: { host?: string }) => Promise +} diff --git a/src/preload/api/github-work-item-api.ts b/src/preload/api/github-work-item-api.ts new file mode 100644 index 00000000000..e302e4517a8 --- /dev/null +++ b/src/preload/api/github-work-item-api.ts @@ -0,0 +1,167 @@ +import type { + AddIssueCommentBySlugArgs, + ClearProjectItemFieldArgs, + DeleteIssueCommentBySlugArgs, + GetProjectViewTableArgs, + GetProjectViewTableResult, + GitHubProjectCommentMutationResult, + GitHubProjectMutationResult, + ListAccessibleProjectsArgs, + ListAccessibleProjectsResult, + ListAssignableUsersBySlugArgs, + ListAssignableUsersBySlugResult, + ListIssueTypesBySlugArgs, + ListIssueTypesBySlugResult, + ListLabelsBySlugArgs, + ListLabelsBySlugResult, + ListProjectViewsArgs, + ListProjectViewsResult, + ProjectWorkItemDetailsBySlugArgs, + ProjectWorkItemDetailsBySlugResult, + ResolveProjectRefArgs, + ResolveProjectRefResult, + UpdateIssueBySlugArgs, + UpdateIssueCommentBySlugArgs, + UpdateIssueTypeBySlugArgs, + UpdateProjectItemFieldArgs, + UpdatePullRequestBySlugArgs +} from '../../shared/github/project-types' +import type { TaskSourceContext } from '../../shared/task-source-context' +import type { + GitHubAssignableUser, + GitHubCommentResult, + GitHubCreateIssueResult, + GitHubIssueUpdate, + GitHubOwnerRepo, + GitHubWorkItem, + GitHubWorkItemDetails, + IssueInfo, + ListWorkItemsResult +} from '../../shared/types' + +export type GitHubRepoSelectorArgs = { + repoPath: string + repoId?: string | null + sourceContext?: TaskSourceContext | null +} + +export type GithubWorkItemApi = { + issue: (args: { + repoPath: string + repoId?: string + sourceContext?: TaskSourceContext | null + number: number + }) => Promise + workItem: (args: { + repoPath: string + repoId?: string + sourceContext?: TaskSourceContext | null + number: number + type?: 'issue' | 'pr' + }) => Promise | null> + workItemByOwnerRepo: (args: { + repoPath: string + repoId?: string + owner: string + repo: string + host?: string + number: number + type: 'issue' | 'pr' + }) => Promise | null> + workItemDetails: ( + args: GitHubRepoSelectorArgs & { + number: number + type?: 'issue' | 'pr' + } + ) => Promise + notifyWorkItemMutated: (args: { + repoPath: string + repoId?: string + type: 'issue' | 'pr' + number: number + }) => Promise + listIssues: (args: { repoPath: string; repoId?: string; limit?: number }) => Promise + createIssue: (args: { + repoPath: string + repoId?: string + sourceContext?: TaskSourceContext | null + title: string + body: string + labels?: string[] + assignees?: string[] + }) => Promise + countWorkItems: (args: { repoPath: string; repoId?: string; query?: string }) => Promise + listWorkItems: (args: { + repoPath: string + repoId?: string + limit?: number + query?: string + page?: number + noCache?: boolean + }) => Promise>> + updateIssue: ( + args: GitHubRepoSelectorArgs & { + number: number + updates: GitHubIssueUpdate + } + ) => Promise<{ ok: true } | { ok: false; error: string }> + addIssueComment: ( + args: GitHubRepoSelectorArgs & { + number: number + body: string + /** Why: scopes the cross-window cache invalidation so a PR and issue sharing the same number don't evict each other. */ + type?: 'issue' | 'pr' + prRepo?: GitHubOwnerRepo | null + } + ) => Promise + listLabels: (args: { + repoPath: string + repoId?: string + sourceContext?: TaskSourceContext | null + }) => Promise + listAssignableUsers: (args: { + repoPath: string + repoId?: string + sourceContext?: TaskSourceContext | null + }) => Promise + /** Subscribe to local-mutation broadcasts so the work-item-drawer cache can invalidate across windows. Returns an unsubscribe. */ + onWorkItemMutated: ( + callback: (payload: { + repoPath: string + repoId?: string + type: 'issue' | 'pr' + number: number + }) => void + ) => () => void + // ── ProjectV2 (GitHub Projects) ───────────────────────────────── + listAccessibleProjects: ( + args?: ListAccessibleProjectsArgs + ) => Promise + resolveProjectRef: (args: ResolveProjectRefArgs) => Promise + listProjectViews: (args: ListProjectViewsArgs) => Promise + getProjectViewTable: (args: GetProjectViewTableArgs) => Promise + projectWorkItemDetailsBySlug: ( + args: ProjectWorkItemDetailsBySlugArgs + ) => Promise + updateProjectItemField: (args: UpdateProjectItemFieldArgs) => Promise + clearProjectItemField: (args: ClearProjectItemFieldArgs) => Promise + updateIssueBySlug: (args: UpdateIssueBySlugArgs) => Promise + updatePullRequestBySlug: ( + args: UpdatePullRequestBySlugArgs + ) => Promise + addIssueCommentBySlug: ( + args: AddIssueCommentBySlugArgs + ) => Promise + updateIssueCommentBySlug: ( + args: UpdateIssueCommentBySlugArgs + ) => Promise + deleteIssueCommentBySlug: ( + args: DeleteIssueCommentBySlugArgs + ) => Promise + listLabelsBySlug: (args: ListLabelsBySlugArgs) => Promise + listAssignableUsersBySlug: ( + args: ListAssignableUsersBySlugArgs + ) => Promise + listIssueTypesBySlug: (args: ListIssueTypesBySlugArgs) => Promise + updateIssueTypeBySlug: (args: UpdateIssueTypeBySlugArgs) => Promise +} diff --git a/src/preload/api/gitlab-api.ts b/src/preload/api/gitlab-api.ts new file mode 100644 index 00000000000..b1467a891f4 --- /dev/null +++ b/src/preload/api/gitlab-api.ts @@ -0,0 +1,172 @@ +import type { TaskSourceContext } from '../../shared/task-source-context' +import type { + ClassifiedError, + GetGitLabRateLimitResult, + GitLabAssignableUser, + GitLabAuthDiagnostic, + GitLabCommentResult, + GitLabDiscussionResolveResult, + GitLabIssueInfo, + GitLabIssueUpdate, + GitLabJobTraceResult, + GitLabMRInlineCommentInput, + GitLabMRReviewersUpdateResult, + GitLabMRUpdate, + GitLabProjectRef, + GitLabRetryJobResult, + GitLabTodo, + GitLabViewer, + GitLabWorkItem, + GitLabWorkItemDetails, + ListMergeRequestsResult, + MRInfo, + MRListState +} from '../../shared/types' + +export type GitLabRepoSelectorArgs = { + repoPath: string + repoId?: string | null + sourceContext?: TaskSourceContext | null +} + +// ── GitLab — parallel to gh, MR/issue surface only in v1 ──────── +// Shapes mirror gh.* except where GitLab's API differs (MR states, host-qualified project path, `glab api -i` paging). +export type GitLabApi = { + viewer: () => Promise + diagnoseAuth: () => Promise + rateLimit: (args?: { force?: boolean; host?: string | null }) => Promise + projectSlug: (args: GitLabRepoSelectorArgs) => Promise + mrForBranch: ( + args: GitLabRepoSelectorArgs & { + branch: string + linkedMRIid?: number | null + } + ) => Promise + mr: (args: GitLabRepoSelectorArgs & { iid: number }) => Promise + listMRs: ( + args: GitLabRepoSelectorArgs & { + state?: MRListState + page?: number + perPage?: number + query?: string + } + ) => Promise + /** Combined MR + issue list filtered by state. Issues are skipped + * when state is 'merged' (issues don't merge). */ + listWorkItems: ( + args: GitLabRepoSelectorArgs & { + state?: MRListState + page?: number + perPage?: number + query?: string + } + ) => Promise + issue: (args: GitLabRepoSelectorArgs & { number: number }) => Promise + listIssues: ( + args: GitLabRepoSelectorArgs & { + state?: 'opened' | 'closed' | 'all' + assignee?: string + limit?: number + } + ) => Promise<{ items: GitLabWorkItem[]; error?: ClassifiedError }> + createIssue: ( + args: GitLabRepoSelectorArgs & { + title: string + body: string + } + ) => Promise<{ ok: true; number: number; url: string } | { ok: false; error: string }> + updateIssue: ( + args: GitLabRepoSelectorArgs & { + number: number + updates: GitLabIssueUpdate + } + ) => Promise<{ ok: true } | { ok: false; error: string }> + addIssueComment: ( + args: GitLabRepoSelectorArgs & { + number: number + body: string + } + ) => Promise + listLabels: (args: GitLabRepoSelectorArgs) => Promise + listAssignableUsers: (args: GitLabRepoSelectorArgs) => Promise + /** Cross-project user-scoped todos (gitlab.com/dashboard/todos). */ + todos: (args: GitLabRepoSelectorArgs) => Promise + /** Aggregated dialog payload — body + discussions + pipeline jobs. */ + workItemDetails: ( + args: GitLabRepoSelectorArgs & { + iid: number + type: 'issue' | 'mr' + } + ) => Promise + closeMR: ( + args: GitLabRepoSelectorArgs & { + iid: number + } + ) => Promise<{ ok: true } | { ok: false; error: string }> + reopenMR: ( + args: GitLabRepoSelectorArgs & { + iid: number + } + ) => Promise<{ ok: true } | { ok: false; error: string }> + mergeMR: ( + args: GitLabRepoSelectorArgs & { + iid: number + method?: 'merge' | 'squash' | 'rebase' + } + ) => Promise<{ ok: true } | { ok: false; error: string }> + updateMR: ( + args: GitLabRepoSelectorArgs & { + iid: number + updates: GitLabMRUpdate + } + ) => Promise<{ ok: true } | { ok: false; error: string }> + updateMRReviewers: ( + args: GitLabRepoSelectorArgs & { + iid: number + reviewerIds: number[] + projectRef?: GitLabProjectRef | null + } + ) => Promise + addMRComment: ( + args: GitLabRepoSelectorArgs & { + iid: number + body: string + } + ) => Promise + addMRInlineComment: ( + args: GitLabRepoSelectorArgs & { + iid: number + input: GitLabMRInlineCommentInput + projectRef?: GitLabProjectRef | null + } + ) => Promise + resolveMRDiscussion: ( + args: GitLabRepoSelectorArgs & { + iid: number + discussionId: string + resolved: boolean + } + ) => Promise + jobTrace: ( + args: GitLabRepoSelectorArgs & { + jobId: number + projectRef?: GitLabProjectRef | null + /** Bound the trace in main to a readable excerpt (see gitLabJobTraceToLogExcerpt). */ + logExcerpt?: boolean + } + ) => Promise + retryJob: ( + args: GitLabRepoSelectorArgs & { + jobId: number + projectRef?: GitLabProjectRef | null + } + ) => Promise + workItemByPath: ( + args: GitLabRepoSelectorArgs & { + host: string + path: string + iid: number + type: 'issue' | 'mr' + } + ) => Promise | null> +} diff --git a/src/preload/api/hosted-review-api.ts b/src/preload/api/hosted-review-api.ts new file mode 100644 index 00000000000..7abe2d81f61 --- /dev/null +++ b/src/preload/api/hosted-review-api.ts @@ -0,0 +1,31 @@ +import type { + BitbucketConnectArgs, + BitbucketConnectionStatus +} from '../../shared/bitbucket-credentials' +import type { + CreateHostedReviewArgs, + CreateHostedReviewResult, + CreateStackedHostedReviewArgs, + CreateStackedHostedReviewResult, + HostedReviewCreationEligibility, + HostedReviewCreationEligibilityArgs, + HostedReviewForBranchArgs, + HostedReviewInfo +} from '../../shared/hosted-review' + +export type HostedReviewApi = { + forBranch: (args: HostedReviewForBranchArgs) => Promise + getCreationEligibility: ( + args: HostedReviewCreationEligibilityArgs + ) => Promise + create: (args: CreateHostedReviewArgs) => Promise + createStacked: (args: CreateStackedHostedReviewArgs) => Promise +} + +export type BitbucketApi = { + connect: ( + args: BitbucketConnectArgs + ) => Promise<{ ok: true; account: string | null } | { ok: false; error: string }> + disconnect: () => Promise + status: () => Promise +} diff --git a/src/preload/api/jira-api.ts b/src/preload/api/jira-api.ts new file mode 100644 index 00000000000..d452bd9728d --- /dev/null +++ b/src/preload/api/jira-api.ts @@ -0,0 +1,84 @@ +import type { + JiraComment, + JiraConnectionStatus, + JiraCreateField, + JiraCreateIssueArgs, + JiraIssue, + JiraIssueFilter, + JiraIssueType, + JiraIssueUpdate, + JiraPriority, + JiraProject, + JiraProjectStatusOrder, + JiraSiteSelection, + JiraTransition, + JiraUser, + JiraViewer +} from '../../shared/types' + +export type JiraApi = { + connect: (args: { + siteUrl: string + email: string + apiToken: string + authType?: 'cloud' | 'server' + }) => Promise<{ ok: true; viewer: JiraViewer } | { ok: false; error: string }> + disconnect: (args?: { siteId?: string }) => Promise + selectSite: (args: { siteId: JiraSiteSelection }) => Promise + status: () => Promise + readStatus: () => Promise + testConnection: (args?: { + siteId?: string + }) => Promise<{ ok: true; viewer: JiraViewer } | { ok: false; error: string }> + searchIssues: (args: { + jql: string + limit?: number + siteId?: JiraSiteSelection + requestId?: string + }) => Promise + cancelSearchIssues: (args: { requestId: string }) => Promise + listIssues: (args?: { + filter?: JiraIssueFilter + limit?: number + siteId?: JiraSiteSelection + }) => Promise + getIssue: (args: { key: string; siteId?: string }) => Promise + lookupIssueSummary: (args: { + key: string + siteId: string + requestId?: string + }) => Promise + cancelIssueSummary: (args: { requestId: string }) => Promise + createIssue: ( + args: JiraCreateIssueArgs + ) => Promise<{ ok: true; id: string; key: string; url: string } | { ok: false; error: string }> + updateIssue: (args: { + key: string + updates: JiraIssueUpdate + siteId?: string + }) => Promise<{ ok: true } | { ok: false; error: string }> + addIssueComment: (args: { + key: string + body: string + siteId?: string + }) => Promise<{ ok: true; id: string } | { ok: false; error: string }> + issueComments: (args: { key: string; siteId?: string }) => Promise + listProjects: (args?: { siteId?: JiraSiteSelection }) => Promise + listIssueTypes: (args: { projectIdOrKey: string; siteId?: string }) => Promise + listCreateFields: (args: { + projectIdOrKey: string + issueTypeId: string + siteId?: string + }) => Promise + listPriorities: (args?: { siteId?: string }) => Promise + listAssignableUsers: (args: { + key: string + query?: string + siteId?: string + }) => Promise + listTransitions: (args: { key: string; siteId?: string }) => Promise + getProjectStatusOrder: (args: { + projectKey: string + siteId?: string + }) => Promise +} diff --git a/src/preload/api/linear-api.ts b/src/preload/api/linear-api.ts new file mode 100644 index 00000000000..613837c4fa4 --- /dev/null +++ b/src/preload/api/linear-api.ts @@ -0,0 +1,128 @@ +import type { LinearIssueAttributeFilter } from '../../shared/linear/issue-attribute-filter' +import type { + LinearCollectionResult, + LinearComment, + LinearConnectionStatus, + LinearCustomViewModel, + LinearCustomViewSummary, + LinearIssue, + LinearIssueUpdate, + LinearLabel, + LinearMember, + LinearProjectDetail, + LinearProjectSummary, + LinearTeam, + LinearViewer, + LinearWorkflowState, + LinearWorkspaceSelection +} from '../../shared/types' + +export type LinearApi = { + connect: (args: { + apiKey: string + }) => Promise<{ ok: true; viewer: LinearViewer } | { ok: false; error: string }> + disconnect: (args?: { workspaceId?: string }) => Promise + selectWorkspace: (args: { + workspaceId: LinearWorkspaceSelection + }) => Promise + status: () => Promise + testConnection: (args?: { + workspaceId?: string + }) => Promise<{ ok: true; viewer: LinearViewer } | { ok: false; error: string }> + searchIssues: (args: { + query: string + limit?: number + workspaceId?: LinearWorkspaceSelection + }) => Promise + listIssues: (args?: { + filter?: 'assigned' | 'created' | 'all' | 'completed' + limit?: number + workspaceId?: LinearWorkspaceSelection + attributeFilter?: LinearIssueAttributeFilter + }) => Promise> + createIssue: (args: { + teamId: string + title: string + description?: string + workspaceId?: string + parentIssueId?: string + projectId?: string | null + stateId?: string + priority?: number + assigneeId?: string | null + labelIds?: string[] + }) => Promise< + | { ok: true; id: string; identifier: string; title: string; url: string } + | { ok: false; error: string } + > + getIssue: (args: { id: string; workspaceId?: string }) => Promise + updateIssue: (args: { + id: string + updates: LinearIssueUpdate + workspaceId?: string + }) => Promise<{ ok: true } | { ok: false; error: string }> + addIssueComment: (args: { + issueId: string + body: string + workspaceId?: string + }) => Promise<{ ok: true; id: string } | { ok: false; error: string }> + issueComments: (args: { issueId: string; workspaceId?: string }) => Promise + listTeams: (args?: { workspaceId?: LinearWorkspaceSelection }) => Promise + listProjects: (args?: { + query?: string + limit?: number + workspaceId?: LinearWorkspaceSelection + force?: boolean + }) => Promise> + createProject: (args: { + name: string + description?: string + content?: string + teamIds: string[] + workspaceId?: string + leadId?: string | null + memberIds?: string[] + labelIds?: string[] + priority?: number + startDate?: string + targetDate?: string + }) => Promise<{ ok: true; project: LinearProjectDetail } | { ok: false; error: string }> + getProject: (args: { + id: string + workspaceId: string + force?: boolean + }) => Promise + listProjectIssues: (args: { + projectId: string + limit?: number + workspaceId: string + force?: boolean + }) => Promise> + listCustomViews: (args: { + model: LinearCustomViewModel + limit?: number + workspaceId?: LinearWorkspaceSelection + force?: boolean + }) => Promise> + getCustomView: (args: { + viewId: string + model: LinearCustomViewModel + workspaceId: string + force?: boolean + }) => Promise + listCustomViewIssues: (args: { + viewId: string + limit?: number + workspaceId: string + force?: boolean + }) => Promise> + listCustomViewProjects: (args: { + viewId: string + limit?: number + workspaceId: string + force?: boolean + }) => Promise> + teamStates: (args: { teamId: string; workspaceId?: string }) => Promise + teamLabels: (args: { teamId: string; workspaceId?: string }) => Promise + teamMembers: (args: { teamId: string; workspaceId?: string }) => Promise +} diff --git a/src/preload/api/mobile-api.ts b/src/preload/api/mobile-api.ts new file mode 100644 index 00000000000..02e82f34044 --- /dev/null +++ b/src/preload/api/mobile-api.ts @@ -0,0 +1,86 @@ +import type { MobileRelayStatus } from '../../shared/mobile-relay-status' +import type { MobilePairingConnectionMode } from '../../shared/mobile-pairing-connection-mode' +import type { RuntimePairingReach } from '../../shared/runtime-pairing-reach' +import type { MobileRelayMintFailure } from '../../shared/mobile-relay-mint-failure' +import type { RuntimeAccessGrant } from '../../shared/runtime-access-grants' + +export type MobileApi = { + listNetworkInterfaces: () => Promise<{ + interfaces: { name: string; address: string; hasDefaultRoute?: boolean }[] + }> + getPairingQR: (args?: { + address?: string + connectionMode?: MobilePairingConnectionMode + rotate?: boolean + }) => Promise< + | { + available: false + reason?: string + guidance?: string + relayFailure?: MobileRelayMintFailure + } + | { + available: true + qrDataUrl: string | null + qrError?: 'encoding_failed' + pairingUrl: string + /** Null when no direct address was advertised — the QR pairs over Relay alone. */ + endpoint: string | null + deviceId: string + /** Mode the QR actually encodes. */ + connectionMode: MobilePairingConnectionMode + } + > + getWindowsFirewallStatus: (args?: { address?: string }) => Promise< + | { supported: false } + | { + supported: true + port: number + ruleAllowed: boolean + blockingRuleDetected: boolean + privateFirewallEnabled: boolean + networkCategory: 'private' | 'public' | 'domain' | 'unknown' + inspectionAvailable: boolean + } + > + repairWindowsFirewall: () => Promise< + { ok: true } | { ok: false; reason: 'cancelled' | 'failed' | 'unsupported' } + > + openWindowsNetworkSettings: () => Promise + getRuntimePairingUrl: (args?: { + address?: string + rotate?: boolean + reach?: RuntimePairingReach + }) => Promise< + | { + available: false + reason?: 'network_exposure_failed' + guidance?: string + } + | { + available: true + pairingUrl: string + webClientUrl: string | null + endpoint: string + deviceId: string + } + > + listDevices: () => Promise<{ + devices: { + deviceId: string + name: string + pairedAt: number + lastSeenAt: number + }[] + }> + revokeDevice: (args: { deviceId: string }) => Promise<{ revoked: boolean }> + listRuntimeAccessGrants: () => Promise<{ grants: RuntimeAccessGrant[] }> + revokeRuntimeAccess: (args: { deviceId: string }) => Promise<{ revoked: boolean }> + isWebSocketReady: () => Promise<{ ready: boolean; endpoint: string | null }> + getRelayStatus: () => Promise<{ status: MobileRelayStatus }> + onRelayStatusChanged: (callback: (status: MobileRelayStatus) => void) => () => void + /** Consumes an auth-failure notification that arrived before the renderer listener mounted. */ + consumePendingUnpairedDeviceAuthFailure?: () => Promise + /** Fires (throttled, once per session) when an unpaired phone repeatedly fails direct-transport auth. */ + onUnpairedDeviceAuthFailure?: (callback: () => void) => () => void +} diff --git a/src/preload/api/native-chat-api.ts b/src/preload/api/native-chat-api.ts new file mode 100644 index 00000000000..01eac44bde6 --- /dev/null +++ b/src/preload/api/native-chat-api.ts @@ -0,0 +1,71 @@ +import type { + AgentType, + NativeChatMessage, + NativeChatTurnLifecycle +} from '../../shared/native-chat-types' + +// notFound marks a not-yet-on-disk miss (retry-worthy) vs a real read/parse error (#8401). +export type NativeChatReadSessionResult = + | { + messages: NativeChatMessage[] + lifecycle?: NativeChatTurnLifecycle + } + | { error: string; notFound?: true } + +/** Messages appended to a live-tailed transcript since the previous emit. */ +export type NativeChatAppendedMessages = NativeChatMessage[] + +export type NativeChatSubscriptionFrame = + | { + type: 'snapshot' + messages: NativeChatMessage[] + hasMore: boolean + error?: string + lifecycle?: NativeChatTurnLifecycle + } + | { + type: 'replacement' + messages: NativeChatMessage[] + hasMore: boolean + lifecycle?: NativeChatTurnLifecycle + } + | { + type: 'appended' + messages: NativeChatMessage[] + lifecycle?: NativeChatTurnLifecycle + } + +/** Wire payload for the `nativeChat:appended` push channel. */ +export type NativeChatAppendedPayload = { + subscriptionId: string + frame: NativeChatSubscriptionFrame +} + +export type NativeChatSubscribeArgs = { + /** Unique per-caller id, echoed on every append so multiple live panes in + * one renderer don't cross-talk. */ + subscriptionId: string + agent: AgentType + sessionId: string + /** Authoritative transcript path from the agent hook (providerSession). */ + transcriptPath?: string + /** First snapshot size; later readSession calls grow this for pagination. */ + limit?: number +} + +export type NativeChatApi = { + /** Read the on-disk transcript for an agent + session id, windowed to the most recent `limit` + * turns. `transcriptPath` is the hook-reported authoritative path, preferred over the id glob. */ + readSession: ( + agent: AgentType, + sessionId: string, + limit?: number, + transcriptPath?: string + ) => Promise + /** Live-tail a transcript. The first frame is a bounded race-safe snapshot; + * later frames contain only newly appended messages. */ + subscribe: ( + args: NativeChatSubscribeArgs, + onFrame: (frame: NativeChatSubscriptionFrame) => void + ) => () => void +} diff --git a/src/preload/api/onboarding-api.ts b/src/preload/api/onboarding-api.ts new file mode 100644 index 00000000000..678a6b38294 --- /dev/null +++ b/src/preload/api/onboarding-api.ts @@ -0,0 +1,28 @@ +import type { OnboardingState } from '../../shared/types' + +export type OnboardingApi = { + get: () => Promise + // Why: main merges the checklist field-by-field, so a partial checklist is fine. + update: ( + updates: Partial> & { + checklist?: Partial + } + ) => Promise +} + +export type StarNagApi = { + onShow: ( + callback: (payload?: { mode?: 'gh' | 'web'; surface?: 'card' | 'toast' }) => void + ) => () => void + onHide: (callback: () => void) => () => void + dismiss: () => Promise + later: () => Promise + complete: () => Promise + disable: () => Promise + openWeb: () => Promise + starOrca: () => Promise + forceShow: () => Promise + agentValueMoment: () => Promise<{ status: 'ready'; mode: 'gh' | 'web' } | { status: 'skipped' }> + showAgentValueMoment: () => Promise + onboardingCompleted: () => Promise +} diff --git a/src/preload/api/orca-profile-api.ts b/src/preload/api/orca-profile-api.ts new file mode 100644 index 00000000000..16c2a575078 --- /dev/null +++ b/src/preload/api/orca-profile-api.ts @@ -0,0 +1,59 @@ +import type { + ConnectCurrentOrcaProfileResult, + CreateCloudLinkedOrcaProfileArgs, + CreateCloudLinkedOrcaProfileResult, + CreateLocalOrcaProfileArgs, + CreateLocalOrcaProfileResult, + FindOrcaProfileProjectsByPathArgs, + FindOrcaProfileProjectsByPathResult, + OrcaProfileAuthStatus, + OrcaProfileListResult, + OrcaProfileOrgInviteRevokeArgs, + OrcaProfileOrgMemberChangeRoleArgs, + OrcaProfileOrgMemberInviteArgs, + OrcaProfileOrgMemberMutationResult, + OrcaProfileOrgMemberRemoveArgs, + OrcaProfileOrgMembersListArgs, + OrcaProfileOrgMembersListResult, + RefreshCurrentOrcaProfileAuthResult, + SelectOrcaProfileOrgArgs, + SelectOrcaProfileOrgResult, + SignOutCurrentOrcaProfileResult, + SwitchOrcaProfileArgs, + SwitchOrcaProfileResult, + TransferOrcaProfileProjectArgs, + TransferOrcaProfileProjectResult +} from '../../shared/orca-profiles' + +export type OrcaProfileApi = { + list: () => Promise + authStatus: () => Promise + createLocal: (args?: CreateLocalOrcaProfileArgs) => Promise + createCloudLinked: ( + args?: CreateCloudLinkedOrcaProfileArgs + ) => Promise + switchProfile: (args: SwitchOrcaProfileArgs) => Promise + transferProject: ( + args: TransferOrcaProfileProjectArgs + ) => Promise + findProjectProfiles: ( + args: FindOrcaProfileProjectsByPathArgs + ) => Promise + connectCurrent: () => Promise + refreshAuth: () => Promise + signOutCurrent: () => Promise + selectOrg: (args: SelectOrcaProfileOrgArgs) => Promise + orgMembersList: (args: OrcaProfileOrgMembersListArgs) => Promise + orgMemberInvite: ( + args: OrcaProfileOrgMemberInviteArgs + ) => Promise + orgInviteRevoke: ( + args: OrcaProfileOrgInviteRevokeArgs + ) => Promise + orgMemberChangeRole: ( + args: OrcaProfileOrgMemberChangeRoleArgs + ) => Promise + orgMemberRemove: ( + args: OrcaProfileOrgMemberRemoveArgs + ) => Promise +} diff --git a/src/preload/api/os-permission-api.ts b/src/preload/api/os-permission-api.ts new file mode 100644 index 00000000000..fc452df1196 --- /dev/null +++ b/src/preload/api/os-permission-api.ts @@ -0,0 +1,54 @@ +import type { + ComputerUsePermissionId, + ComputerUsePermissionResetResult, + ComputerUsePermissionSetupResult, + ComputerUsePermissionStatusResult +} from '../../shared/computer-use-permissions-types' +import type { + DeveloperPermissionId, + DeveloperPermissionRequestResult, + DeveloperPermissionState, + LocalNetworkConnectionTestResult +} from '../../shared/developer-permissions-types' +import type { + NotificationDispatchRequest, + NotificationDispatchResult, + NotificationDeliveryProbeResult, + NotificationDismissResult, + NotificationPermissionStatusResult, + NotificationSoundResult +} from '../../shared/types' + +export type NotificationsApi = { + dispatch: (args: NotificationDispatchRequest) => Promise + dismiss: (ids: string[]) => Promise + openSystemSettings: () => Promise + getPermissionStatus: () => Promise + probeDelivery: (args?: { force?: boolean }) => Promise + playSound: (options?: { force?: boolean; volume?: number }) => Promise +} + +export type MacosTccPromptsApi = { + /** Fires once macOS has raised its Nth consent dialog naming Orca (#9756). */ + onThreshold: (callback: (payload: { promptCount: number }) => void) => () => void + consumePending: () => Promise<{ claimId: number; promptCount: number } | null> + acknowledgePending: (claimId: number) => Promise + releasePending: (claimId: number) => Promise + dismiss: () => Promise +} + +export type DeveloperPermissionsApi = { + getStatus: () => Promise + request: (args: { id: DeveloperPermissionId }) => Promise + openSettings: (args: { id: DeveloperPermissionId }) => Promise + testLocalNetworkConnection: (args: { + host: string + port: number + }) => Promise +} + +export type ComputerUsePermissionsApi = { + getStatus: () => Promise + openSetup: (args?: { id?: ComputerUsePermissionId }) => Promise + reset: () => Promise +} diff --git a/src/preload/api/pet-api.ts b/src/preload/api/pet-api.ts new file mode 100644 index 00000000000..e0031cb8e36 --- /dev/null +++ b/src/preload/api/pet-api.ts @@ -0,0 +1,8 @@ +import type { CustomPet } from '../../shared/types' + +export type PetApi = { + import: () => Promise + importPetBundle: () => Promise + read: (id: string, fileName: string, kind?: 'image' | 'bundle') => Promise + delete: (id: string, fileName: string, kind?: 'image' | 'bundle') => Promise +} diff --git a/src/preload/api/plugin-host-api.ts b/src/preload/api/plugin-host-api.ts new file mode 100644 index 00000000000..49222ae7a0b --- /dev/null +++ b/src/preload/api/plugin-host-api.ts @@ -0,0 +1,191 @@ +import type { + PluginPanelActionOutcome, + PluginPanelEntry +} from '../../shared/plugins/plugin-panel-bridge' +import type { PluginConsentRequest } from '../../shared/plugins/plugin-consent-request' +import type { PluginLanguagePackRegistration } from '../../shared/plugins/plugin-language-pack-artifact' +import type { PluginChangeEvent } from '../../shared/plugins/plugin-change-event' +import type { PluginManifest } from '../../shared/plugins/plugin-manifest' +import type { PluginMarketplaceGitSource } from '../../shared/plugins/plugin-marketplace' + +/** Panel contribution as surfaced by the main-process plugin service. */ +export type PluginHostPanel = { + id: string + title: string + /** Lucide icon name declared in the plugin manifest. */ + icon?: string + tabKey: `plugin:${string}` +} + +/** `pending` = awaiting (re-)consent; `idle` = enabled, worker not running + * (lazy); `restarting` = waiting for supervised backoff; `errored` = crashed past the restart budget or failed to start; + * `invalid` = unreadable manifest. */ +export type PluginHostStatus = + | 'running' + | 'restarting' + | 'idle' + | 'pending' + | 'disabled' + | 'errored' + | 'invalid' + +/** Wire shape of plugins:list — must stay assignable from the main-process + * projection in src/main/plugins/plugin-list-projection.ts. */ +export type PluginHostListEntry = { + pluginKey: string + consentFingerprint: string | null + name: string + version: string + publisher: string + description?: string + status: PluginHostStatus + needsReconsent: boolean + error?: string + isDev: boolean + official: boolean + bundled: boolean + capabilities: { kind: string; description: string }[] + panels: PluginHostPanel[] + commands: { + id: string + title: string + context: 'global' | 'worktree' + handler: { type: 'built-in'; action: string } | { type: 'worker' } + keybindings: { key: string; when: 'global' | 'worktree' }[] + }[] + hasWorker: boolean + vmRecipes?: { + id: string + name: string + description?: string + commands: { + phase: 'create' | 'suspend' | 'resume' | 'destroy' + command: string + }[] + }[] + restarts: number + blockedByKillList?: { reason: string; advisoryUrl?: string } + source?: { + kind: 'local-path' | 'git' | 'marketplace' | 'bundled' + reference: string + resolvedCommit: string | null + contentHash: string + marketplace?: { reference: string; resolvedCommit: string } + } +} + +export type PluginHostLogLine = { + ts: number + level: 'info' | 'warn' | 'error' + line: string +} + +export type PluginHostInstallSource = + | { kind: 'local-path'; path: string } + | { kind: 'git'; url: string; ref: string } + +export type PluginHostInstallResult = + | { + ok: true + pluginKey: string + version: string + contentHash: string + consentFingerprint: string + resolvedCommit: string | null + } + | { ok: false; error: string } + +export type PluginMarketplaceHostSourceState = { + id: string + source: PluginMarketplaceGitSource + addedAt: number + marketplace: { + name: string + owner: string + resolvedCommit: string + fetchedAt: number + } | null + stale: boolean + official: boolean + error?: string +} + +export type PluginMarketplaceHostListing = { + marketplaceSourceId: string + marketplaceName: string + marketplaceOwner: string + marketplaceCommit: string + pluginKey: string + source: PluginMarketplaceGitSource + description?: string + categories: string[] + official: boolean + bundled: boolean + blockedByKillList?: { reason: string; advisoryUrl?: string } +} + +export type PluginMarketplaceHostInstallPreview = { + marketplaceSourceId: string + marketplaceName: string + marketplaceOwner: string + marketplaceCommit: string + pluginKey: string + source: PluginMarketplaceGitSource + resolvedCommit: string + contentHash: string + consentFingerprint: string + manifest: PluginManifest + official: boolean + bundled: boolean + blockedByKillList?: { reason: string; advisoryUrl?: string } +} + +export type PluginsApi = { + list: () => Promise + listLanguagePacks: () => Promise + /** Records the consent-dialog answer; approval is keyed to the plugin's + * current capability and trusted-worker fingerprint. */ + consent: (args: PluginConsentRequest) => Promise + setEnabled: (args: { pluginKey: string; enabled: boolean }) => Promise + /** Returns the panel's CSP-wrapped HTML, or null when the plugin or + * panel is missing/disabled. Rendered only inside a sandboxed iframe. */ + readPanelEntry: (args: { pluginKey: string; panelId: string }) => Promise + invokeCommand: (args: { + pluginKey: string + commandId: string + args?: unknown + }) => Promise + /** Relays a sandboxed panel's bridge request to main, which enforces the + * plugin's consented capabilities before executing. */ + panelAction: (args: { + sessionToken: string + action: string + params?: unknown + }) => Promise + install: (source: PluginHostInstallSource) => Promise + listMarketplaces: () => Promise + addMarketplace: (source: PluginMarketplaceGitSource) => Promise + removeMarketplace: (args: { sourceId: string }) => Promise + refreshMarketplaces: (args?: { sourceId?: string }) => Promise + listMarketplacePlugins: () => Promise + previewMarketplacePlugin: (args: { + marketplaceSourceId: string + pluginKey: string + }) => Promise + installMarketplacePlugin: ( + preview: Pick< + PluginMarketplaceHostInstallPreview, + 'marketplaceSourceId' | 'marketplaceCommit' | 'pluginKey' | 'resolvedCommit' + > + ) => Promise + previewMarketplaceUpdate: (args: { + pluginKey: string + }) => Promise + rollbackMarketplacePlugin: (args: { pluginKey: string }) => Promise + remove: (args: { pluginKey: string }) => Promise + getLogs: (args: { pluginKey: string }) => Promise + /** Re-discovers after settings edits (feature flag, dev paths). */ + refresh: () => Promise + /** Fires whenever installed plugins, worker states, panels, or content packs change. */ + onChanged: (callback: (event: PluginChangeEvent) => void) => () => void +} diff --git a/src/preload/api/preflight-api.ts b/src/preload/api/preflight-api.ts new file mode 100644 index 00000000000..f63099a07ec --- /dev/null +++ b/src/preload/api/preflight-api.ts @@ -0,0 +1,55 @@ +import type { ProjectExecutionRuntimeResolution } from '../../shared/project-execution-runtime' +import type { PathSource, ShellHydrationFailureReason } from '../../shared/types' + +export type PreflightStatus = { + git: { installed: boolean } + gh: { installed: boolean; authenticated: boolean } + /** Optional — older preload payloads predating GitLab support omit it; consumers gate on `glab?.installed`. */ + glab?: { installed: boolean; authenticated: boolean } + bitbucket?: { configured: boolean; authenticated: boolean; account: string | null } + azureDevOps?: { + configured: boolean + authenticated: boolean + account: string | null + baseUrl: string | null + tokenConfigured: boolean + } + gitea?: { + configured: boolean + authenticated: boolean + account: string | null + baseUrl: string | null + tokenConfigured: boolean + } +} + +export type RefreshAgentsResult = { + agents: string[] + addedPathSegments: string[] + shellHydrationOk: boolean + /** Drives agent_picks `on_path:false` triage (dashboard 1562016). `'shell_hydrate'` = detection saw the user's + * full shell PATH; `'sync_seed_only'` = hydration failed and detection ran against the `patchPackagedProcessPath` seed list. */ + pathSource: PathSource + /** Classified hydration outcome: `'none'` on success, else a failure mode when `shellHydrationOk` is false. */ + pathFailureReason: ShellHydrationFailureReason +} + +export type PreflightRuntimeContext = { + wslDistro?: string | null + wslDefault?: boolean + projectRuntime?: ProjectExecutionRuntimeResolution +} + +export type PreflightApi = { + check: (args?: PreflightRuntimeContext & { force?: boolean }) => Promise + detectAgents: (args?: PreflightRuntimeContext) => Promise + refreshAgents: (args?: PreflightRuntimeContext) => Promise + detectRemoteAgents: (args: { connectionId: string }) => Promise + detectRemoteWindowsTerminalCapabilities: (args: { connectionId: string }) => Promise<{ + wslAvailable: boolean + wslDistros: string[] + pwshAvailable: boolean + gitBashAvailable: boolean + hostPlatform: NodeJS.Platform | null + }> +} diff --git a/src/preload/api/pty-api.ts b/src/preload/api/pty-api.ts new file mode 100644 index 00000000000..774196b6a01 --- /dev/null +++ b/src/preload/api/pty-api.ts @@ -0,0 +1,224 @@ +import type { + AgentProviderSessionMetadata, + SleepingAgentLaunchConfig +} from '../../shared/agent-session-resume' +import type { StartupCommandDelivery } from '../../shared/codex-startup-delivery' +import type { ProjectExecutionRuntimeResolution } from '../../shared/project-execution-runtime' +import type { PtyListedSession } from '../../shared/pty-listed-session' +import type { PtyMainDeliveryDiagnostics } from '../../shared/pty-delivery-diagnostics' +import type { PtyModelRestoreNeededEvent } from '../../shared/pty-model-restore-marker' +import type { + PtyRendererDeliveryHealthReply, + PtyRendererDeliveryStateReport +} from '../../shared/pty-renderer-delivery-health' +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 { PtyManagementApi } from './pty-management-api' + +export type PtyApi = { + spawn: (opts: { + cols: number + rows: number + cwd?: string + cwdFallback?: 'worktree' + env?: Record + envToDelete?: string[] + command?: string + commandDelivery?: 'renderer' | 'provider' + launchConfig?: SleepingAgentLaunchConfig + resumeProviderSession?: AgentProviderSessionMetadata + launchToken?: string + launchAgent?: TuiAgent + startupCommandDelivery?: StartupCommandDelivery + connectionId?: string | null + worktreeId?: string + sessionId?: string + // Why: lets a single tab open in a different shell than the user's default. + shellOverride?: string + projectRuntime?: ProjectExecutionRuntimeResolution + terminalColorQueryReplies?: { foreground?: string; background?: string } + // Why: mark the PTY hidden before its first byte so the delivery gate owns spawn-time queries (terminal-query-authority.md §races). + initiallyHidden?: boolean + // Why: main sync-flushes the (worktreeId,tabId,leafId→ptyId) binding before pty:spawn returns to close a SIGKILL race (INVESTIGATION.md). + tabId?: string + leafId?: string + // Why: main fires `agent_started` only on spawn success, so launch metadata rides this field (telemetry-plan.md §Agent launch semantics). + telemetry?: { agent_kind: AgentKind; launch_source: LaunchSource; request_kind: RequestKind } + }) => Promise<{ + id: string + launchAgent?: TuiAgent + launchConfig?: SleepingAgentLaunchConfig + snapshot?: string + snapshotCols?: number + snapshotRows?: number + snapshotPrefixAnsi?: string + snapshotFrameAnsi?: string + snapshotFrameRestoreAnsi?: string + snapshotKittyKeyboardFlags?: number + snapshotSeq?: number + isReattach?: boolean + isAlternateScreen?: boolean + replay?: string + sessionExpired?: boolean + coldRestore?: { scrollback: string; cwd: string; cols?: number; rows?: number } + startupCwdFallback?: { kind: 'worktree'; cwd: string } + agentResumeUnavailable?: true + }> + write: (id: string, data: string) => void + writeAccepted: (id: string, data: string) => Promise + onWriteUnavailable?: (callback: (payload: { id: string }) => void) => () => void + resize: (id: string, cols: number, rows: number) => void + claimViewport: (id: string, cols: number, rows: number) => void + reportGeometry: (id: string, cols: number, rows: number) => void + signal: (id: string, signal: string) => void + clearBuffer: (id: string) => void + kill: (id: string, opts?: { keepHistory?: boolean }) => Promise + ackColdRestore: (id: string) => void + ackData: (id: string, charCount: number, processedChars?: number) => void + onDeliveryResyncRequest: (callback: (payload: { requestId: number }) => void) => () => void + respondDeliveryResync: (payload: { + requestId: number + processedCharsByPty: Record + }) => void + /** Renderer-initiated delivery health/heal lane over invoke — reaches main + * even when every main→renderer push channel is dead (field wedge). */ + reportRendererDeliveryState: ( + report: PtyRendererDeliveryStateReport + ) => Promise + /** Live pty:data listener count on the preload emitter (sync) — heal-time + * discriminator between a detached listener and a dead channel. */ + getPtyDataListenerCount: () => number + /** One-shot signal that this page's pty:data dispatcher is registered, so + * main can release sends held during the load/reload boot window. */ + rendererDispatcherReady: () => void + setActiveRendererPty: (id: string, active: boolean) => void + setRendererPtyVisible: (id: string, visible: boolean) => void + /** Hidden-delivery gate (Phase 4): hidden=true lets main drop renderer + * byte delivery after model ingestion; reveal restores from snapshots. */ + setHiddenRendererPty: (id: string, hidden: boolean) => void + /** Ref-counted-on-the-renderer delivery-interest signal that suppresses + * the hidden-delivery gate while any raw-byte consumer is registered. */ + setPtyDeliveryInterest: (id: string, interested: boolean) => void + /** View-attribute bridge (Phase 5 slice 2): app-global composed terminal + * appearance push backing main's hidden-PTY OSC/DSR color replies. */ + publishTerminalViewAttributes: (attributes: TerminalViewAttributes) => void + hasChildProcesses: (id: string) => Promise + getForegroundProcess: (id: string) => Promise + inspectProcess: (id: string) => Promise<{ + foregroundProcess: string | null + hasChildProcesses: boolean + unavailable?: true + }> + confirmForegroundProcess: (id: string) => Promise + getCwd: (id: string) => Promise + getSize: (id: string) => Promise<{ cols: number; rows: number } | null> + listSessions: () => Promise + getAuthoritativeBufferSnapshotCapabilities?: ( + ids: string[] + ) => Promise<{ id: string; authoritative: boolean | null }[]> + hasPty: (id: string) => Promise + getMainBufferSnapshot: ( + id: string, + opts?: { scrollbackRows?: number } + ) => Promise<{ + data: string + frameRestoreAnsi?: string + cols: number + rows: number + cwd?: string | null + seq?: number + /** Start of main's pending renderer-delivery queue at snapshot time + * (equals `seq` when empty) — bounds the renderer's post-restore + * duplicate window. */ + pendingDeliveryStartSeq?: number + source?: 'headless' | 'renderer' + alternateScreen?: boolean + /** Authoritative normal buffer paired with an alternate-screen frame. */ + scrollbackAnsi?: string + /** Trailing incomplete escape the emulator ingested; the restorer must + * write it after its post-replay resets, last before live chunks. */ + pendingEscapeTailAnsi?: string + /** Effective kitty flags the snapshot owner proved at `seq`. Absent means + * unknown; consumers must not turn that into a known `0`. */ + kittyKeyboardFlags?: number + } | null> + getRendererDeliveryDebugSnapshot: () => Promise<{ + pendingPtyCount: number + pendingChars: number + maxPendingCharsByPty: number + rendererInFlightPtyCount: number + rendererInFlightChars: number + maxRendererInFlightCharsByPty: number + activeRendererPtyCount: number + flushScheduled: boolean + peakPendingChars: number + peakMaxPendingCharsByPty: number + peakRendererInFlightChars: number + peakMaxRendererInFlightCharsByPty: number + ackGatedFlushSkipCount: number + hiddenDeliveryGatedPtyCount: number + hiddenDeliveryGatedVisiblePtyCount: number + hiddenDeliveryGatedActivePtyCount: number + deliveryInterestPtyCount: number + hiddenDeliveryDroppedChars: number + hiddenDeliveryDroppedChunks: number + pendingDroppedChars: number + diagnostics: PtyMainDeliveryDiagnostics + rendererLifecycleResetCount: number + lastLifecycleResetClearedChars: number + rendererPtyDispatcherReady: boolean + rendererDispatcherReadyForcedCount: number + }> + resetRendererDeliveryDebug: () => Promise + onData: ( + callback: (data: { + id: string + data: string + seq?: number + rawLength?: number + transformed?: boolean + background?: boolean + droppedOutput?: boolean + }) => void + ) => () => void + onReplay: (callback: (data: { id: string; data: string }) => void) => () => void + /** Out-of-band main→renderer signal that renderer-bound bytes were + * dropped (hidden-delivery gate / pending cap); the pane restores from + * the model snapshot. Never delivered in-band on pty:data. */ + onModelRestoreNeeded: (callback: (event: PtyModelRestoreNeededEvent) => void) => () => void + /** Batched derived side-effect facts for PTYs whose bytes transit local + * main. */ + onSideEffect: (callback: (batch: TerminalSideEffectBatch) => void) => () => void + /** Title-only replay snapshot for (re)attach; attention facts never replay. */ + getSideEffectSnapshot: (id: string) => Promise + onExit: ( + callback: (data: { id: string; code: number; preserveRendererBinding?: boolean }) => void + ) => () => void + onSpawned: (callback: (data: { id: string }) => void) => () => void + onSerializeBufferRequest: ( + callback: (data: { + requestId: string + ptyId: string + opts?: { scrollbackRows?: number; altScreenForcesZeroRows?: boolean } + }) => void + ) => () => void + onClearBufferRequest: (callback: (data: { ptyId: string }) => void) => () => void + sendSerializedBuffer: ( + requestId: string, + snapshot: { + data: string + cols: number + rows: number + seq?: number + lastTitle?: string + kittyKeyboardFlags?: number + } | null + ) => void + declarePendingPaneSerializer: (paneKey: string) => Promise + settlePaneSerializer: (paneKey: string, gen: number) => Promise + clearPendingPaneSerializer: (paneKey: string, gen: number) => Promise + reportRendererSerializerReady?: (ptyId: string) => Promise + management: PtyManagementApi +} diff --git a/src/preload/api/pty-management-api.ts b/src/preload/api/pty-management-api.ts new file mode 100644 index 00000000000..10fef2d2303 --- /dev/null +++ b/src/preload/api/pty-management-api.ts @@ -0,0 +1,30 @@ +// Mirror of daemon's `DaemonSessionInfo` (src/main/daemon/types.ts); not imported — preload can't depend on main-only protocol types. +export type PtyManagementSession = { + sessionId: string + state: 'created' | 'spawning' | 'running' | 'exiting' | 'exited' + shellState: 'pending' | 'ready' | 'timed_out' | 'unsupported' + isAlive: boolean + pid: number | null + cwd: string | null + cols: number + rows: number + createdAt: number + protocolVersion: number +} + +// 'severed': macOS can no longer attribute daemon terminals to Orca, so Accessibility/ +// Automation grants silently stop applying until the daemon is restarted (STA-3491). +export type PtyManagementMacTccAttributionHealth = 'intact' | 'severed' | 'unknown' + +export type PtyManagementApi = { + // `degraded`: daemon is alive but can't spawn fresh PTYs, so new terminals run locally without daemon persistence. + listSessions: () => Promise<{ sessions: PtyManagementSession[]; degraded: boolean }> + killAll: () => Promise<{ + killedCount: number + remainingCount: number + killedSessionIds?: string[] + }> + killOne: (args: { sessionId: string }) => Promise<{ success: boolean }> + restart: () => Promise<{ success: boolean }> + macTccAttribution: () => Promise<{ health: PtyManagementMacTccAttributionHealth }> +} diff --git a/src/preload/api/repository-api.ts b/src/preload/api/repository-api.ts new file mode 100644 index 00000000000..1734c35bdc7 --- /dev/null +++ b/src/preload/api/repository-api.ts @@ -0,0 +1,169 @@ +import type { ExecutionHostId } from '../../shared/execution-host' +import type { + HostRepoCatalogSnapshot, + ListReposForExecutionHostArgs +} from '../../shared/host-repo-catalog-contract' +import type { + BaseRefDefaultResult, + BaseRefSearchResult, + NestedRepoScanResult, + Project, + ProjectGroup, + ProjectGroupImportMode, + ProjectGroupImportResult, + ProjectHostSetup, + ProjectHostSetupCreateArgs, + ProjectHostSetupCreateResult, + ProjectHostSetupDeleteArgs, + ProjectHostSetupDeleteResult, + ProjectHostSetupExistingFolderArgs, + ProjectHostSetupResult, + ProjectHostSetupUpdateArgs, + ProjectHostSetupUpdateResult, + ProjectUpdateArgs, + Repo +} from '../../shared/types' + +export type RepositoryApi = { + list: () => Promise + listForExecutionHost?: (args: ListReposForExecutionHostArgs) => Promise + // Why: error union matches the IPC handler's return shape; renderer callers branch on `'error' in result`. + add: (args: { + path: string + kind?: 'git' | 'folder' + }) => Promise<{ repo: Repo } | { error: string }> + remove: (args: { repoId: string }) => Promise + // Forget a project on one execution host only, leaving the same repo id on other hosts intact. + removeForHost: (args: { repoId: string; hostId: string }) => Promise + reorder: (args: { orderedIds: string[] }) => Promise<{ status: 'applied' | 'rejected' }> + reorderForHost: (args: { + orderedIds: string[] + hostId: string + }) => Promise<{ status: 'applied' | 'rejected' }> + update: (args: { + repoId: string + hostId?: ExecutionHostId + updates: Partial< + Pick< + Repo, + | 'displayName' + | 'badgeColor' + | 'repoIcon' + | 'upstream' + | 'hookSettings' + | 'worktreeBaseRef' + | 'worktreeBasePath' + | 'kind' + | 'issueSourcePreference' + | 'externalWorktreeVisibility' + | 'externalWorktreeVisibilityPromptDismissedAt' + | 'externalWorktreeInboxBaselinePaths' + | 'importedExternalWorktreePaths' + | 'agentWorktreeVisibility' + | 'customWorktreeVisibilitySources' + | 'worktreeVisibilitySourcePreferences' + | 'projectGroupId' + | 'projectGroupOrder' + | 'forkSyncMode' + > + > & { + sourceControlAi?: Repo['sourceControlAi'] | null + externalWorktreeDiscoverySuppressedAt?: Repo['externalWorktreeDiscoverySuppressedAt'] | null + } + }) => Promise + pickFolder: () => Promise + pickFolders: () => Promise + pickDirectory: () => Promise + clone: (args: { url: string; destination: string }) => Promise + cloneRemote: (args: { connectionId: string; url: string; destination: string }) => Promise + createRemote: (args: { + connectionId: string + parentPath: string + name: string + kind: 'git' | 'folder' + }) => Promise<{ repo: Repo } | { error: string }> + cloneAbort: () => Promise + // Why: error union matches the IPC handler's return shape; renderer callers branch on `'error' in result`. + addRemote: (args: { + connectionId: string + remotePath: string + displayName?: string + kind?: 'git' | 'folder' + }) => Promise<{ repo: Repo } | { error: string }> + // Why: error union matches the IPC handler's return shape; renderer callers branch on `'error' in result`. + create: (args: { + parentPath: string + name: string + kind: 'git' | 'folder' + }) => Promise<{ repo: Repo } | { error: string }> + isGitAvailable: () => Promise + getDefaultCreateProjectParent: () => Promise + onCloneProgress: (callback: (data: { phase: string; percent: number }) => void) => () => void + getGitUsername: (args: { repoId: string }) => Promise + getBaseRefDefault: (args: { + repoId: string + hostId?: ExecutionHostId + }) => Promise + searchBaseRefs: (args: { + repoId: string + query: string + limit?: number + hostId?: ExecutionHostId + }) => Promise + searchBaseRefDetails: (args: { + repoId: string + query: string + limit?: number + hostId?: ExecutionHostId + }) => Promise + onChanged: (callback: () => void) => () => void +} + +export type ProjectsApi = { + list: () => Promise + update: (args: ProjectUpdateArgs) => Promise + listHostSetups: () => Promise + createHostSetup: (args: ProjectHostSetupCreateArgs) => Promise + setupExistingFolder: (args: ProjectHostSetupExistingFolderArgs) => Promise + updateHostSetup: (args: ProjectHostSetupUpdateArgs) => Promise + deleteHostSetup: (args: ProjectHostSetupDeleteArgs) => Promise +} + +export type ProjectGroupsApi = { + list: () => Promise + create: (args: { + name: string + parentPath?: string | null + connectionId?: string | null + parentGroupId?: string | null + createdFrom?: ProjectGroup['createdFrom'] + }) => Promise + update: (args: { + groupId: string + updates: Partial> + }) => Promise + delete: (args: { groupId: string }) => Promise + moveProject: (args: { + projectId: string + groupId: string | null + order?: number + }) => Promise + scanNested: (args: { + path: string + connectionId?: string + scanId?: string + options?: Record + }) => Promise + cancelNestedScan: (args: { scanId: string }) => Promise + onNestedScanProgress: ( + callback: (data: { scanId: string; scan: NestedRepoScanResult }) => void + ) => () => void + importNested: (args: { + parentPath: string + groupName: string + projectPaths: string[] + connectionId?: string + scanId?: string + mode: ProjectGroupImportMode + }) => Promise +} diff --git a/src/preload/api/runtime-api.ts b/src/preload/api/runtime-api.ts new file mode 100644 index 00000000000..1acc43f5f2c --- /dev/null +++ b/src/preload/api/runtime-api.ts @@ -0,0 +1,116 @@ +import type { + RuntimeBrowserDriverState, + RuntimeStatus, + RuntimeSyncWindowGraph, + RuntimeSyncWindowGraphResult, + RuntimeTerminalDriverState +} from '../../shared/runtime-types' +import type { RuntimeRpcResponse } from '../../shared/runtime-rpc-envelope' +import type { PublicKnownRuntimeEnvironment } from '../../shared/runtime-environments' +import type { VerifyAndAddRuntimeEnvironmentResult } from '../../shared/remote-pairing-verification' + +export type RuntimeEnvironmentSubscriptionHandle = { + unsubscribe: () => void + sendBinary: (bytes: Uint8Array) => void +} + +export type RuntimeApi = { + runtime: { + syncWindowGraph: (graph: RuntimeSyncWindowGraph) => Promise + getStatus: () => Promise + call: (args: { method: string; params?: unknown }) => Promise> + getTerminalFitOverrides: () => Promise< + { ptyId: string; mode: 'mobile-fit' | 'remote-desktop-fit'; cols: number; rows: number }[] + > + getTerminalDrivers: () => Promise< + { + ptyId: string + driver: RuntimeTerminalDriverState + }[] + > + getBrowserDrivers: () => Promise< + { + browserPageId: string + driver: RuntimeBrowserDriverState + }[] + > + restoreTerminalFit: (ptyId: string) => Promise<{ restored: boolean }> + reclaimBrowserForDesktop: (browserPageId: string) => Promise<{ reclaimed: boolean }> + onTerminalFitOverrideChanged: ( + callback: (event: { + ptyId: string + mode: 'mobile-fit' | 'remote-desktop-fit' | 'desktop-fit' + cols: number + rows: number + }) => void + ) => () => void + onTerminalDriverChanged: ( + callback: (event: { ptyId: string; driver: RuntimeTerminalDriverState }) => void + ) => () => void + onNativeChatLaunchDraftResolved?: ( + callback: (event: { tabId: string; text: string; createdAt: number }) => void + ) => () => void + onBrowserDriverChanged: ( + callback: (event: { browserPageId: string; driver: RuntimeBrowserDriverState }) => void + ) => () => void + } + runtimeEnvironments: { + list: () => Promise + addFromPairingCode: (args: { + name: string + pairingCode: string + }) => Promise<{ environment: PublicKnownRuntimeEnvironment }> + verifyAndAddFromPairingCode: (args: { + name: string + pairingCode: string + allowLoopback?: boolean + }) => Promise + resolve: (args: { selector: string }) => Promise + remove: (args: { selector: string }) => Promise<{ removed: PublicKnownRuntimeEnvironment }> + disconnect: (args: { + selector: string + }) => Promise<{ disconnected: PublicKnownRuntimeEnvironment }> + connect: (args: { + selector: string + timeoutMs?: number + }) => Promise> + getStatus: (args: { + selector: string + timeoutMs?: number + }) => Promise> + // Why: system resume / browser online advance pending shared-control reconnect timers only. + retryConnectionsNow?: () => Promise + call: (args: { + selector: string + method: string + params?: unknown + timeoutMs?: number + expectedEnvironmentPairingRevision?: number + }) => Promise> + subscribe: ( + args: { + selector: string + method: string + params?: unknown + timeoutMs?: number + expectedEnvironmentPairingRevision?: number + }, + callbacks: { + onResponse: (response: RuntimeRpcResponse) => void + onBinary?: (bytes: Uint8Array) => void + onError?: (error: { code: string; message: string }) => void + onClose?: () => void + } + ) => Promise + } + wsl: { + isAvailable: () => Promise + listDistros: () => Promise + } + pwsh: { + isAvailable: () => Promise + } + gitBash: { + isAvailable: () => Promise + } +} diff --git a/src/preload/api/settings-api.ts b/src/preload/api/settings-api.ts new file mode 100644 index 00000000000..b50843160dd --- /dev/null +++ b/src/preload/api/settings-api.ts @@ -0,0 +1,35 @@ +import type { KeybindingActionId, KeybindingFileSnapshot } from '../../shared/keybindings' +import type { + WarpThemeImportPreview, + WarpThemeImportSource +} from '../../shared/terminal-custom-themes' +import type { GhosttyImportPreview, GlobalSettings } from '../../shared/types' + +export type SettingsApi = { + get: () => Promise + /** Synchronous persisted-settings read for startup decisions that can't wait for async hydration. Blocking IPC — call sparingly. */ + getSync: () => GlobalSettings | null + set: (args: Partial) => Promise + setActiveRuntimeEnvironmentPreference: (args: { + environmentId: string | null + }) => Promise + updatePRBotAuthorOverride: (args: { author: string; isBot: boolean }) => Promise + listFonts: () => Promise + previewGhosttyImport: () => Promise + previewWarpThemeImport: (source: WarpThemeImportSource) => Promise + /** Subscribe to out-of-band settings updates (e.g. View > Appearance toggles) to stay in sync with main. */ + onChanged: (callback: (updates: Partial) => void) => () => void +} + +export type KeybindingsApi = { + get: () => Promise + ensureFile: () => Promise + setAction: (args: { + actionId: KeybindingActionId + bindings: string[] | null + }) => Promise + reload: () => Promise + openFile: () => Promise + revealFile: () => Promise + onChanged: (callback: (snapshot: KeybindingFileSnapshot) => void) => () => void +} diff --git a/src/preload/api/shell-api.ts b/src/preload/api/shell-api.ts new file mode 100644 index 00000000000..d466ac26163 --- /dev/null +++ b/src/preload/api/shell-api.ts @@ -0,0 +1,32 @@ +import type { + ShellOpenExternalEditorRequest, + ShellOpenExternalEditorResult, + ShellOpenLocalPathResult +} from '../../shared/shell-open-types' + +export type { + ShellOpenExternalEditorRequest, + ShellOpenExternalEditorResult, + ShellOpenLocalPathResult +} from '../../shared/shell-open-types' + +export type ShellApi = { + openPath: (path: string) => Promise + openInFileManager: (path: string) => Promise + openInExternalEditor: ( + request: ShellOpenExternalEditorRequest + ) => Promise + openUrl: (url: string) => Promise + openFilePath: (path: string) => Promise + openFileUri: (uri: string) => Promise + pathExists: (path: string) => Promise + pickAttachment: () => Promise + pickImage: () => Promise + pickRepoIconImage: () => Promise<{ + dataUrl: string + fileName: string + } | null> + pickAudio: () => Promise + pickDirectory: (args: { defaultPath?: string }) => Promise + copyFile: (args: { srcPath: string; destPath: string }) => Promise +} diff --git a/src/preload/api/speech-api.ts b/src/preload/api/speech-api.ts new file mode 100644 index 00000000000..fb866dace15 --- /dev/null +++ b/src/preload/api/speech-api.ts @@ -0,0 +1,33 @@ +import type { + SpeechErrorEvent, + SpeechLifecycleEvent, + SpeechModelManifest, + SpeechModelState, + SpeechTranscriptEvent +} from '../../shared/speech-types' + +export type SpeechApi = { + getCatalog: () => Promise + getModelStates: () => Promise + getOpenAiApiKeyStatus: () => Promise<{ configured: boolean }> + saveOpenAiApiKey: (apiKey: string) => Promise<{ configured: boolean }> + clearOpenAiApiKey: () => Promise<{ configured: boolean }> + downloadModel: (modelId: string) => Promise + cancelDownload: (modelId: string) => Promise + deleteModel: (modelId: string) => Promise + startDictation: ( + modelId: string, + hotwords: string[] | undefined, + sessionId: string + ) => Promise + feedAudio: (samples: Float32Array, sampleRate: number, sessionId?: string) => Promise + stopDictation: (sessionId?: string) => Promise + onPartialTranscript: (callback: (data: SpeechTranscriptEvent) => void) => () => void + onFinalTranscript: (callback: (data: SpeechTranscriptEvent) => void) => () => void + onDownloadProgress: ( + callback: (data: { modelId: string; progress: number }) => void + ) => () => void + onReady: (callback: (data: SpeechLifecycleEvent) => void) => () => void + onStopped: (callback: (data: SpeechLifecycleEvent) => void) => () => void + onError: (callback: (data: SpeechErrorEvent) => void) => () => void +} diff --git a/src/preload/api/ssh-api.ts b/src/preload/api/ssh-api.ts new file mode 100644 index 00000000000..b25f1713e16 --- /dev/null +++ b/src/preload/api/ssh-api.ts @@ -0,0 +1,78 @@ +import type { + EnrichedDetectedPort, + PortForwardEntry, + SshConfigHostListArgs, + SshConfigHostListResult, + SshConfigHostResolution, + SshConfigImportResult, + SshConnectionState, + SshTarget, + SshTargetAddResult +} from '../../shared/ssh-types' +import type { FilesystemPathFlavor } from '../../shared/types' + +export type SshApi = { + listTargets: () => Promise + // Removed-target id → last known label, for a friendly host name on workspaces still pinned to a removed target. + listRemovedTargetLabels: () => Promise> + addTarget: (args: { target: Omit }) => Promise + updateTarget: (args: { + id: string + updates: Partial> + }) => Promise + removeTarget: (args: { id: string }) => Promise + importConfig: (args?: { reAdopt?: boolean }) => Promise + listConfigHosts: (args?: SshConfigHostListArgs) => Promise + resolveConfigHost: (args: { alias: string }) => Promise + connect: (args: { targetId: string }) => Promise + disconnect: (args: { targetId: string }) => Promise + terminateSessions: (args: { targetId: string }) => Promise + resetRelay: (args: { targetId: string }) => Promise + getState: (args: { targetId: string }) => Promise + needsPassphrasePrompt: (args: { targetId: string }) => Promise + testConnection: (args: { + targetId: string + }) => Promise<{ success: boolean; error?: string; state?: SshConnectionState }> + onStateChanged: ( + callback: (data: { targetId: string; state: SshConnectionState }) => void + ) => () => void + addPortForward: (args: { + targetId: string + localPort: number + remoteHost: string + remotePort: number + label?: string + }) => Promise + updatePortForward: (args: { + id: string + targetId: string + localPort: number + remoteHost: string + remotePort: number + label?: string + }) => Promise + removePortForward: (args: { id: string }) => Promise + listPortForwards: (args?: { targetId?: string }) => Promise + listDetectedPorts: (args: { targetId: string }) => Promise + onPortForwardsChanged: ( + callback: (data: { targetId: string; forwards: PortForwardEntry[] }) => void + ) => () => void + onDetectedPortsChanged: ( + callback: (data: { targetId: string; ports: EnrichedDetectedPort[] }) => void + ) => () => void + browseDir: (args: { targetId: string; dirPath: string }) => Promise<{ + entries: { name: string; isDirectory: boolean }[] + resolvedPath: string + pathFlavor: FilesystemPathFlavor + }> + onCredentialRequest: ( + callback: (data: { + requestId: string + targetId: string + kind: 'passphrase' | 'password' + detail: string + }) => void + ) => () => void + onCredentialResolved: (callback: (data: { requestId: string }) => void) => () => void + submitCredential: (args: { requestId: string; value: string | null }) => Promise +} diff --git a/src/preload/api/telemetry-api.ts b/src/preload/api/telemetry-api.ts new file mode 100644 index 00000000000..9fc70f908b7 --- /dev/null +++ b/src/preload/api/telemetry-api.ts @@ -0,0 +1,63 @@ +import type { TelemetryConsentState } from '../../shared/telemetry-consent-types' +import type { MemorySnapshot, StatsSummary } from '../../shared/types' + +export type StatsApi = { + getSummary: () => Promise +} + +// Diagnostics IPC payloads; mirror the runtime types in `src/main/observability/{index,bundle}.ts`. +export type DiagnosticsStatusPayload = { + readonly localFileEnabled: boolean + readonly bundleEnabled: boolean + readonly traceFilePath: string + readonly traceFamilySize: number + readonly disabledReason?: + | 'do_not_track' + | 'orca_telemetry_disabled' + | 'orca_diagnostics_disabled' + | 'ci' +} +export type DiagnosticsBundlePayload = { + readonly bundleSubmissionId: string + readonly bytes: number + readonly spanCount: number +} +export type DiagnosticsUploadPayload = + | { + readonly ticketId: string + } + | { + readonly canceled: true + } + +export type MemoryApi = { + getSnapshot: () => Promise +} + +export type DiagnosticsApi = { + getStatus: () => Promise + collectBundle: (lookbackMinutes?: number) => Promise + openBundlePreview: (bundleSubmissionId: string) => Promise + discardBundlePreview: (bundleSubmissionId: string) => Promise + uploadBundle: (bundleSubmissionId: string) => Promise + deleteBundle: (ticketId: string) => Promise +} + +export type TelemetryApi = { + /** Fire-and-forget track. Loose IPC typing on purpose — the main-side validator enforces; + * renderer sites should import `track()` from lib/telemetry.ts, not reach here. */ + telemetryTrack: (name: string, props: Record) => Promise + /** Flip the persisted opt-in preference. Subject to a per-session + * consent-mutation rate limit on the main side (≤5/session). */ + telemetrySetOptIn: (optedIn: boolean) => Promise + /** Diagnostic file controls (telemetry-error-tracking.md §User controls). Main does the FS/network + * work and retains upload payloads so the renderer can't read or substitute arbitrary bytes. */ + diagnostics: DiagnosticsApi + /** Read-only effective consent state (+ reason if disabled) — env vars are main-side state the renderer can't read directly. */ + telemetryGetConsentState: () => Promise + /** Banner ✕ — persist `optedIn = true` silently. Separate channel from `telemetrySetOptIn`, + * whose `via` derivation would wrongly fire `telemetry_opted_in`. Same per-session rate limit. */ + telemetryAcknowledgeBanner: () => Promise + stats: StatsApi + memory: MemoryApi +} diff --git a/src/preload/api/ui-command-event-api.ts b/src/preload/api/ui-command-event-api.ts new file mode 100644 index 00000000000..e46c09139b7 --- /dev/null +++ b/src/preload/api/ui-command-event-api.ts @@ -0,0 +1,203 @@ +import type { + PersistedUIState, + TuiAgent, + WorktreeDefaultTabsLaunch, + WorktreeSetupLaunch, + WorktreeStartupLaunch +} from '../../shared/types' +import type { FeatureInteractionId } from '../../shared/feature-interactions' +import type { KeybindingActionId } from '../../shared/keybindings' +import type { BrowserFindSource } from '../../shared/browser-find-source' +import type { + AgentProviderSessionMetadata, + SleepingAgentLaunchConfig +} from '../../shared/agent-session-resume' +import type { TerminalPaneSplitSource } from '../../shared/feature-education-telemetry' +import type { + RuntimeMobileSessionTabMove, + RuntimeTerminalCreateRequestPayload, + RuntimeTerminalPresentation +} from '../../shared/runtime-types' +import type { + RuntimeMobileMarkdownRequest, + RuntimeMobileMarkdownResponse +} from '../../shared/mobile-markdown-document' +import type { TerminalTabCreateReply } from '../../shared/terminal-reveal-identity' +import type { + TerminalTabCloseRequest, + TerminalTabCloseResponse +} from '../../shared/terminal-tab-close' + +export type UiCommandEventApi = { + get: () => Promise + set: (args: Partial) => Promise + recordFeatureInteraction: (id: FeatureInteractionId) => Promise + onStateChanged: (callback: (ui: PersistedUIState) => void) => () => void + onOpenSettings: (callback: () => void) => () => void + /** Consumes a one-shot tray/menu-bar "open settings" intent queued before mount. */ + consumePendingOpenSettings: () => Promise + onOpenSetupGuide: (callback: () => void) => () => void + onOpenFeatureTour: (callback: () => void) => () => void + onOpenCrashReport: (callback: () => void) => () => void + onToggleLeftSidebar: (callback: () => void) => () => void + onToggleRightSidebar: (callback: () => void) => () => void + onToggleWorktreePalette: (callback: () => void) => () => void + onToggleFloatingTerminal: (callback: () => void) => () => void + onTerminalShortcutCaptured: ( + callback: (data: { actionId: KeybindingActionId }) => void + ) => () => void + onOpenQuickOpen: (callback: () => void) => () => void + onToggleQuickCommandsMenu: (callback: () => void) => () => void + onOpenNewWorkspace: (callback: () => void) => () => void + onDeleteCurrentWorkspace: (callback: () => void) => () => void + onOpenWorkspaceBoard: (callback: () => void) => () => void + onOpenTasks: (callback: () => void) => () => void + onJumpToWorktreeIndex: (callback: (index: number) => void) => () => void + onJumpToTabIndex: (callback: (index: number) => void) => () => void + onWorktreeHistoryNavigate: (callback: (direction: 'back' | 'forward') => void) => () => void + onNewBrowserTab: (callback: () => void) => () => void + onNewMarkdownTab: (callback: () => void) => () => void + onNewSimulatorTab: (callback: () => void) => () => void + onRequestTabCreate: ( + callback: (data: { + requestId: string + url: string + worktreeId?: string + sessionProfileId?: string | null + sessionPartition?: string + activate?: boolean + }) => void + ) => () => void + replyTabCreate: (reply: { requestId: string; browserPageId?: string; error?: string }) => void + onRequestTabSetProfile: ( + callback: (data: { + requestId: string + browserPageId: string + profileId: string + sessionPartition?: string + }) => void + ) => () => void + replyTabSetProfile: (reply: { requestId: string; error?: string }) => void + onRequestTabClose: ( + callback: (data: { requestId: string; tabId: string | null; worktreeId?: string }) => void + ) => () => void + replyTabClose: (reply: { requestId: string; error?: string }) => void + onNewTerminalTab: (callback: () => void) => () => void + onFocusBrowserAddressBar: (callback: () => void) => () => void + onFindInBrowserPage: (source: BrowserFindSource, callback: () => void) => () => void + onReloadBrowserPage: (callback: () => void) => () => void + onBrowserHistoryNavigate: (callback: (direction: 'back' | 'forward') => void) => () => void + onZoomBrowserPage: (callback: (direction: 'in' | 'out' | 'reset') => void) => () => void + onHardReloadBrowserPage: (callback: () => void) => () => void + onCloseActiveTab: (callback: () => void) => () => void + onCloseFloatingItem: (callback: (payload: { sourceId: string }) => void) => () => void + onSelectFloatingIndex: (callback: (payload: { index: number }) => void) => () => void + onSwitchTab: (callback: (direction: 1 | -1) => void) => () => void + onSwitchTabAcrossAllTypes: (callback: (direction: 1 | -1) => void) => () => void + onSwitchRecentTab: (callback: () => void) => () => void + onSwitchTerminalTab: (callback: (direction: 1 | -1) => void) => () => void + onCtrlTabKeyDown: (callback: (data: { shiftKey: boolean }) => void) => () => void + onCtrlTabKeyUp: (callback: () => void) => () => void + onToggleStatusBar: (callback: () => void) => () => void + onDictationKeyDown: (callback: () => void) => () => void + onExportPdfRequested: (callback: () => void) => () => void + onAppMenuPaste: (callback: () => void) => () => void + onAppMenuSelectionAction: (callback: (action: 'copy' | 'select-all') => void) => () => void + onEditableContextPaste: (callback: (data: { plainTextOnly: boolean }) => void) => () => void + onActivateWorktree: ( + callback: (data: { + repoId: string + worktreeId: string + setup?: WorktreeSetupLaunch + startup?: WorktreeStartupLaunch + defaultTabs?: WorktreeDefaultTabsLaunch + }) => void + ) => () => void + onCreateTerminal: ( + callback: (data: { + requestId?: string + worktreeId: string + command?: string + cwd?: string + env?: Record + launchConfig?: SleepingAgentLaunchConfig + resumeProviderSession?: AgentProviderSessionMetadata + launchToken?: string + launchAgent?: TuiAgent + viewMode?: 'terminal' | 'chat' + title?: string + ptyId?: string + activate?: boolean + focus?: boolean + presentation?: RuntimeTerminalPresentation + surfaceOwner?: false + tabId?: string + leafId?: string + splitFromLeafId?: string + splitDirection?: 'horizontal' | 'vertical' + splitTelemetrySource?: TerminalPaneSplitSource + }) => void + ) => () => void + onRequestTerminalCreate: ( + callback: (data: RuntimeTerminalCreateRequestPayload) => void + ) => () => void + onRequestTerminalTabMount: ( + callback: (data: { worktreeId: string; tabId?: string; ptyId?: string }) => void + ) => () => void + replyTerminalCreate: (reply: TerminalTabCreateReply) => void + onSplitTerminal: ( + callback: (data: { + tabId: string + paneRuntimeId: number + direction: 'horizontal' | 'vertical' + command?: string + telemetrySource?: TerminalPaneSplitSource + }) => void + ) => () => void + onRenameTerminal: ( + callback: (data: { tabId: string; title: string | null }) => void + ) => () => void + onFocusTerminal: ( + callback: (data: { + tabId: string + worktreeId: string + leafId?: string | null + ackPaneKeyOnSuccess?: string + flashFocusedPane?: boolean + scrollToBottomIfOutputSinceLastView?: boolean + }) => void + ) => () => void + onFocusEditorTab: (callback: (data: { tabId: string; worktreeId: string }) => void) => () => void + onCloseSessionTab: (callback: (data: { tabId: string; worktreeId: string }) => void) => () => void + onMoveSessionTab: ( + callback: (data: { worktreeId: string } & RuntimeMobileSessionTabMove) => void + ) => () => void + onOpenFileFromMobile: ( + callback: (data: { + worktreeId: string + filePath: string + relativePath: string + runtimeEnvironmentId?: string + }) => void + ) => () => void + onOpenDiffFromMobile: ( + callback: (data: { + worktreeId: string + filePath: string + relativePath: string + staged: boolean + runtimeEnvironmentId?: string + }) => void + ) => () => void + onMobileMarkdownRequest: (callback: (request: RuntimeMobileMarkdownRequest) => void) => () => void + respondMobileMarkdownRequest: (response: RuntimeMobileMarkdownResponse) => void + onCloseTerminal: ( + callback: (data: { tabId: string; paneRuntimeId?: number }) => void + ) => () => void + onTerminalTabCloseRequest: (callback: (request: TerminalTabCloseRequest) => void) => () => void + respondTerminalTabClose: (response: TerminalTabCloseResponse) => void + onSleepWorktree: (callback: (data: { worktreeId: string }) => void) => () => void + onResumeSleepingAgents: (callback: (data: { worktreeId: string }) => void) => () => void + onTerminalZoom: (callback: (direction: 'in' | 'out' | 'reset') => void) => () => void + onSystemResumed: (callback: () => void) => () => void +} diff --git a/src/preload/api/ui-window-api.ts b/src/preload/api/ui-window-api.ts new file mode 100644 index 00000000000..fc6aef6991b --- /dev/null +++ b/src/preload/api/ui-window-api.ts @@ -0,0 +1,51 @@ +import type { ReadClipboardTextOptions } from '../../shared/clipboard-text' +import type { NativeFileDropPayload } from '../../shared/native-file-drop' +import type { + RichMarkdownContextMenuCommandPayload, + RichMarkdownContextMenuTableTarget +} from '../../shared/rich-markdown-context-menu' + +export type UiWindowApi = { + readClipboardText: (options?: ReadClipboardTextOptions) => Promise + readSelectionClipboardText: (options?: ReadClipboardTextOptions) => Promise + saveClipboardImageAsTempFile: (args?: { + connectionId?: string | null + runtimeEnvironmentId?: string | null + }) => Promise + writeClipboardText: (text: string) => Promise + writeTerminalClipboardText: (text: string) => Promise + writeSelectionClipboardText: (text: string) => Promise + writeClipboardImage: (dataUrl: string) => Promise + performNativePaste: (options?: { mode?: 'paste' | 'paste-and-match-style' }) => void + performNativeSelectionAction: (action: 'copy' | 'select-all') => void + writeClipboardFile: ( + args: + | { + filePath: string + connectionId?: string | null + } + | string + ) => Promise<{ ok: boolean; reason?: string }> + onFileDrop: (callback: (data: NativeFileDropPayload) => void) => () => void + getZoomLevel: () => number + setZoomLevel: (level: number) => void + syncTrafficLights: (zoomFactor: number) => void + setMarkdownEditorFocused: (focused: boolean) => void + setRichMarkdownContextMenuTarget: (target: RichMarkdownContextMenuTableTarget | null) => void + setTerminalInputFocused: (focused: boolean) => void + setFloatingFocus: (state: { panelFocused: boolean; terminalFocused: boolean }) => void + setShortcutRecorderFocused: (focused: boolean) => void + onRichMarkdownContextCommand: ( + callback: (payload: RichMarkdownContextMenuCommandPayload) => void + ) => () => void + onFullscreenChanged: (callback: (isFullScreen: boolean) => void) => () => void + minimize: () => void + maximize: () => void + isMaximized: () => Promise + onMaximizeChanged: (callback: (isMaximized: boolean) => void) => () => void + requestClose: () => void + popupMenu: () => void + onWindowCloseRequested: (callback: (data: { isQuitting: boolean }) => void) => () => void + confirmWindowClose: () => void + notifyWindowRevealed: () => void +} diff --git a/src/preload/api/updater-api.ts b/src/preload/api/updater-api.ts new file mode 100644 index 00000000000..f6a43da9834 --- /dev/null +++ b/src/preload/api/updater-api.ts @@ -0,0 +1,25 @@ +import type { + LinuxPackageInstallInstructions, + ReleaseBuildListResult, + UpdateCheckOptions, + UpdateStatus +} from '../../shared/types' +import type { ReleaseChannel } from '../../shared/release-channel' + +export type UpdaterApi = { + getVersion: () => Promise + getStatus: () => Promise + check: (options?: UpdateCheckOptions) => Promise + download: () => Promise + quitAndInstall: () => Promise + dismissNudge: () => Promise + dismissAvailableUpdate: () => Promise + /** Desktop-only. Rejects unless the current status carries `linux-package-install` recovery. */ + getLinuxPackageInstallInstructions: () => Promise + /** Desktop-only. Reveals the revalidated cached package in the native file manager. */ + showLinuxPackage: () => Promise + listBuilds: (channel: ReleaseChannel) => Promise + + onStatus: (callback: (status: UpdateStatus) => void) => () => void + onClearDismissal: (callback: () => void) => () => void +} diff --git a/src/preload/api/workspace-cleanup-api.ts b/src/preload/api/workspace-cleanup-api.ts new file mode 100644 index 00000000000..748d8216540 --- /dev/null +++ b/src/preload/api/workspace-cleanup-api.ts @@ -0,0 +1,30 @@ +import type { + WorkspaceCleanupDismissArgs, + WorkspaceCleanupLocalProcessArgs, + WorkspaceCleanupLocalProcessResult, + WorkspaceCleanupScanArgs, + WorkspaceCleanupScanProgress, + WorkspaceCleanupScanResult +} from '../../shared/workspace-cleanup' +import type { + WorkspaceSpaceAnalyzeResult, + WorkspaceSpaceScanProgress +} from '../../shared/workspace-space-types' + +export type WorkspaceCleanupApi = { + scan: ( + args?: WorkspaceCleanupScanArgs, + onProgress?: (progress: WorkspaceCleanupScanProgress) => void + ) => Promise + dismiss: (args: WorkspaceCleanupDismissArgs) => Promise + clearDismissals: () => Promise + hasKillableLocalProcesses: ( + args: WorkspaceCleanupLocalProcessArgs + ) => Promise +} + +export type WorkspaceSpaceApi = { + analyze: () => Promise + cancel: () => Promise + onProgress: (callback: (progress: WorkspaceSpaceScanProgress) => void) => () => void +} diff --git a/src/preload/api/workspace-port-api.ts b/src/preload/api/workspace-port-api.ts new file mode 100644 index 00000000000..8c0166695ac --- /dev/null +++ b/src/preload/api/workspace-port-api.ts @@ -0,0 +1,23 @@ +import type { + LocalhostWorktreeLabelResult, + LocalhostWorktreeLabelRoute +} from '../../shared/localhost-worktree-labels' +import type { + WorkspacePortAdvertisedUrlChangedEvent, + WorkspacePortKillRequest, + WorkspacePortKillResult, + WorkspacePortScanRequest, + WorkspacePortScanResult +} from '../../shared/workspace-ports' + +export type WorkspacePortsApi = { + scan: (args: WorkspacePortScanRequest) => Promise + kill: (args: WorkspacePortKillRequest) => Promise + onAdvertisedUrlChanged: ( + callback: (event: WorkspacePortAdvertisedUrlChangedEvent) => void + ) => () => void +} + +export type LocalhostWorktreeLabelsApi = { + register: (args: LocalhostWorktreeLabelRoute) => Promise +} diff --git a/src/preload/api/workspace-session-api.ts b/src/preload/api/workspace-session-api.ts new file mode 100644 index 00000000000..35eb17486d7 --- /dev/null +++ b/src/preload/api/workspace-session-api.ts @@ -0,0 +1,50 @@ +import type { + IssueInfo, + PRInfo, + WorkspaceSessionPatch, + WorkspaceSessionState +} from '../../shared/types' +import type { ExecutionHostId } from '../../shared/execution-host' +import type { + RemoteWorkspaceChangedEvent, + RemoteWorkspaceConnectedClient, + RemoteWorkspacePatchResult, + RemoteWorkspaceSnapshot +} from '../../shared/remote-workspace-types' + +export type WorkspaceSessionApi = { + session: { + // hostId defaults to the 'local' partition on main, so omitting it stays backward-compatible. + get: (hostId?: ExecutionHostId) => Promise + set: (args: WorkspaceSessionState, hostId?: ExecutionHostId) => Promise + patch: (args: WorkspaceSessionPatch, hostId?: ExecutionHostId) => Promise + flush: () => Promise + readTerminalScrollback: (args: { ref: string }) => string | null + setSync: (args: WorkspaceSessionState, hostId?: ExecutionHostId) => void + } + cache: { + getGitHub: () => Promise<{ + pr: Record + issue: Record + }> + setGitHub: (args: { + cache: { + pr: Record + issue: Record + } + }) => Promise + } + remoteWorkspace: { + get: (args: { targetId: string }) => Promise + setForConnectedTargets: (args: { + session?: WorkspaceSessionState + hydratedTargetIds?: string[] + }) => Promise<{ targetId: string; result: RemoteWorkspacePatchResult }[]> + listEnabledConnectedTargets: () => Promise + listConnectedClients: (args?: { + targetIds?: string[] + }) => Promise<{ targetId: string; clients: RemoteWorkspaceConnectedClient[] }[]> + clientId: () => Promise + onChanged: (callback: (event: RemoteWorkspaceChangedEvent) => void) => () => void + } +} diff --git a/src/preload/api/worktree-api.ts b/src/preload/api/worktree-api.ts new file mode 100644 index 00000000000..96089d10e88 --- /dev/null +++ b/src/preload/api/worktree-api.ts @@ -0,0 +1,179 @@ +import type { + ForgetRemovedWorktreesForExecutionHostArgs, + ForgetRemovedWorktreesForExecutionHostResult, + HostQualifiedDetectedWorktreeResult, + HostQualifiedKnownWorktreeResult, + LegacyDetectedWorktreeRequest, + ListDetectedWorktreesArgs, + ListKnownWorktreesForExecutionHostArgs, + ProviderRequestId +} from '../../shared/detected-worktree-provider-contract' +import type { ExecutionHostId } from '../../shared/execution-host' +import type { + FolderWorkspacePathStatus, + FolderWorkspacePathStatusRequest +} from '../../shared/folder-workspace-path-status' +import type { + HostLineageSnapshot, + ListDesktopLineageForHostArgs +} from '../../shared/host-lineage-contract' +import type { + AdoptProvisionedRootArgs, + CreateWorktreeArgs, + CreateWorktreeResult, + DetectedWorktreeListResult, + FolderWorkspace, + ForceDeleteWorktreeBranchResult, + GitHubPrStartPoint, + GitPushTarget, + RemoveWorktreeResult, + SparsePreset, + WorkspaceLineage, + Worktree, + WorktreeBaseStatusEvent, + WorktreeHeadIdentity, + WorktreeLineage, + WorktreeMeta, + WorktreeRemoteBranchConflictEvent +} from '../../shared/types' + +export type WorktreeApi = { + list: (args: { repoId: string }) => Promise + listDetected: { + ( + args: ListDetectedWorktreesArgs + ): Promise + (args: LegacyDetectedWorktreeRequest): Promise + } + listKnownForExecutionHost?: ( + args: ListKnownWorktreesForExecutionHostArgs + ) => Promise + /** Retires the persisted metadata an authoritative scan proved gone, so it stops feeding the read above. */ + forgetRemovedForExecutionHost?: ( + args: ForgetRemovedWorktreesForExecutionHostArgs + ) => Promise + cancelListDetected?: (args: { providerRequestId: ProviderRequestId }) => Promise + listAll: () => Promise + create: (args: CreateWorktreeArgs) => Promise + adoptProvisionedRoot: (args: AdoptProvisionedRootArgs) => Promise + /** Two-phase progress for a background `create`, correlated by `creationId`. The remote/runtime + * create path emits nothing, so the surface falls back to an indeterminate spinner. */ + onCreateProgress: ( + callback: (data: { creationId?: string; phase: 'fetching' | 'creating' }) => void + ) => () => void + prefetchCreateBase: (args: { repoId: string; baseBranch?: string }) => Promise + resolvePrBase: (args: { + repoId: string + prNumber: number + headRefName?: string + baseRefName?: string + isCrossRepository?: boolean + }) => Promise + /** GitLab parallel of resolvePrBase. For same-project MRs returns + * `/`; for fork MRs fetches + * refs/merge-requests//head and returns the SHA. */ + resolveMrBase: (args: { + repoId: string + mrIid: number + sourceBranch?: string + targetBranch?: string + isCrossRepository?: boolean + }) => Promise< + { baseBranch: string; compareBaseRef?: string; pushTarget?: GitPushTarget } | { error: string } + > + remove: (args: { + worktreeId: string + hostId?: ExecutionHostId + force?: boolean + // Why (#11960): distinct from `force`, which the plain Delete confirmation + // already sets to skip the dirty-file prompt. Only an explicit Force Delete + // may waive the proof that every PTY stopped. + allowUnverifiedPtyStop?: boolean + skipArchive?: boolean + }) => 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 + }) => Promise + forceDeletePreservedBranch: (args: { + worktreeId: string + branchName: string + expectedHead: string + hostId?: ExecutionHostId + }) => Promise + updateMeta: (args: { worktreeId: string; updates: Partial }) => Promise + listLineage: () => Promise<{ + lineage: Record + workspaceLineage?: Record + }> + listLineageForHost?: (args: ListDesktopLineageForHostArgs) => Promise + updateLineage: (args: { + worktreeId: string + parentWorktreeId?: string + noParent?: boolean + }) => Promise + persistSortOrder: (args: { orderedIds: string[] }) => Promise + /** Full CLI output of the last branch auto-rename generation failure, held + * in main memory only — null after a restart or once the failure clears. */ + getBranchRenameFailureOutput: (args: { worktreeId: string }) => Promise + onChanged: (callback: (data: { repoId: string }) => void) => () => void + onGitStatusMetadataChanged: (callback: (data: { repoId: string }) => void) => () => void + onHeadIdentitiesChanged: ( + callback: (data: { repoId: string; identities: WorktreeHeadIdentity[] }) => void + ) => () => void + onBaseStatus: (callback: (data: WorktreeBaseStatusEvent) => void) => () => void + onRemoteBranchConflict: ( + callback: (data: WorktreeRemoteBranchConflictEvent) => void + ) => () => void +} + +export type FolderWorkspacesApi = { + list: () => Promise + getPathStatus: (args: FolderWorkspacePathStatusRequest) => Promise + create: (args: { + projectGroupId: string + name?: string + folderPath?: string | null + connectionId?: string | null + linkedTask?: FolderWorkspace['linkedTask'] + createdWithAgent?: FolderWorkspace['createdWithAgent'] + pendingFirstAgentMessageRename?: boolean + }) => Promise + update: (args: { + folderWorkspaceId: string + updates: Partial< + Pick< + FolderWorkspace, + | 'name' + | 'folderPath' + | 'linkedTask' + | 'comment' + | 'isArchived' + | 'isUnread' + | 'isPinned' + | 'sortOrder' + | 'manualOrder' + | 'workspaceStatus' + | 'createdWithAgent' + | 'pendingFirstAgentMessageRename' + | 'firstAgentMessageRenameError' + | 'lastActivityAt' + | 'diffComments' + > + > + }) => Promise + delete: (args: { folderWorkspaceId: string }) => Promise +} + +export type SparsePresetsApi = { + list: (args: { repoId: string }) => Promise + save: (args: { + repoId: string + id?: string + name: string + directories: string[] + }) => Promise + remove: (args: { repoId: string; presetId: string }) => Promise + onChanged: (callback: (data: { repoId: string }) => void) => () => void +} 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 07/11] 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 08/11] 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 09/11] 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 10/11] 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 11/11] 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) +}