mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 00:03:09 +00:00
Harden project host compatibility projection
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -22,7 +22,7 @@ The target model is:
|
||||
- [x] Add a shared run-target type or selector for RunHostContext / WorkspaceRunContext so workspace creation, agent launch, automations, and remote execution use the same vocabulary.
|
||||
- [~] Audit user-facing copy so the product says **Host** for Local Mac, SSH hosts, remote servers, and future cloud VMs, while preserving technical labels only where useful.
|
||||
- [x] Document the invariant that task source context and run host are separate choices.
|
||||
- [~] Add compatibility adapters for existing repo-id/path-based callers so old data can be read while new code moves to explicit contexts.
|
||||
- [x] Add compatibility adapters for existing repo-id/path-based callers so old data can be read while new code moves to explicit contexts.
|
||||
- [x] Add schema migration notes for old automations, cached task selections, pending worktree requests, and any persisted task drawer state.
|
||||
|
||||
Done means: a new agent can read the shared types and understand exactly where work item data comes from versus where execution happens.
|
||||
@@ -256,3 +256,4 @@ Done means: reviewers and future agents can understand the vision, implementatio
|
||||
- [x] 2026-06-12: Marked task drawer source-context persistence complete based on UI slice coverage that records GitHub, GitLab, Linear, and Jira task-detail history entries with their exact `TaskSourceContext`, plus direct-open prefetch coverage for source-scoped GitHub and Linear reads.
|
||||
- [x] 2026-06-12: Marked PR/MR base resolution complete based on `useComposerState-host-context-boundaries.test.ts`, which asserts GitHub PR and GitLab MR base lookup uses the selected run repo instead of the provider item's source repo while preserving source item metadata separately.
|
||||
- [x] 2026-06-12: Extracted and tested external automation source availability copy so local missing tooling, disconnected SSH sources, connecting SSH sources, and connected SSH hosts with missing remote tooling get distinct states; verified with focused automation availability tests, targeted oxlint, and full `pnpm run typecheck`.
|
||||
- [x] 2026-06-12: Marked repo-id/path compatibility adapters complete after hardening the renderer project-host selector to merge repo-derived compatibility rows when hydrated project/setup arrays are empty or partial, alongside existing shared projection, persistence backfill/sync, automation context migration, and pending worktree context coverage. Verified with selector/projection tests, persistence tests, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import type { Repo, Worktree } from '../../../shared/types'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
|
||||
import { toRuntimeExecutionHostId } from '../../../shared/execution-host'
|
||||
import type { AppState } from './types'
|
||||
import {
|
||||
getAllWorktreesFromState,
|
||||
@@ -227,4 +228,86 @@ describe('store selectors', () => {
|
||||
setups: projectHostSetups
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to repo compatibility projection when hydrated setup state is empty', () => {
|
||||
const repos = [
|
||||
makeRepo({
|
||||
id: 'repo-1',
|
||||
path: '/Users/alice/orca',
|
||||
displayName: 'orca',
|
||||
upstream: { owner: 'stablyai', repo: 'orca' }
|
||||
})
|
||||
]
|
||||
|
||||
const projection = getProjectHostSetupProjectionFromState({
|
||||
repos,
|
||||
projects: [],
|
||||
projectHostSetups: []
|
||||
})
|
||||
|
||||
expect(projection.projects).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'github:stablyai/orca',
|
||||
sourceRepoIds: ['repo-1']
|
||||
})
|
||||
])
|
||||
expect(projection.setups).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'repo-1',
|
||||
projectId: 'github:stablyai/orca',
|
||||
repoId: 'repo-1',
|
||||
hostId: 'local',
|
||||
path: '/Users/alice/orca'
|
||||
})
|
||||
])
|
||||
})
|
||||
|
||||
it('merges missing repo compatibility rows with independent hydrated setups', () => {
|
||||
const repos = [
|
||||
makeRepo({
|
||||
id: 'repo-1',
|
||||
path: '/Users/alice/orca',
|
||||
displayName: 'orca'
|
||||
})
|
||||
]
|
||||
const projects = [
|
||||
{
|
||||
id: 'cloud-project',
|
||||
displayName: 'Cloud Project',
|
||||
badgeColor: '#737373',
|
||||
sourceRepoIds: [],
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
]
|
||||
const projectHostSetups = [
|
||||
{
|
||||
id: 'cloud-project::gpu-vm',
|
||||
projectId: 'cloud-project',
|
||||
hostId: toRuntimeExecutionHostId('gpu-vm'),
|
||||
repoId: '',
|
||||
path: '/srv/cloud-project',
|
||||
displayName: 'GPU VM',
|
||||
setupState: 'ready' as const,
|
||||
setupMethod: 'provisioned' as const,
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
]
|
||||
|
||||
const projection = getProjectHostSetupProjectionFromState({
|
||||
repos,
|
||||
projects,
|
||||
projectHostSetups
|
||||
})
|
||||
|
||||
expect(projection.projects.map((project) => project.id)).toEqual([
|
||||
'repo:repo-1',
|
||||
'cloud-project'
|
||||
])
|
||||
expect(projection.setups.map((setup) => setup.id)).toEqual(['repo-1', 'cloud-project::gpu-vm'])
|
||||
expect(getProjectHostSetupProjectionFromState({ repos, projects, projectHostSetups })).toBe(
|
||||
projection
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -40,6 +40,10 @@ const providedProjectHostSetupProjectionCache = new WeakMap<
|
||||
Project[],
|
||||
WeakMap<ProjectHostSetup[], ProjectHostSetupProjection>
|
||||
>()
|
||||
const mergedProjectHostSetupProjectionCache = new WeakMap<
|
||||
AppState['repos'],
|
||||
WeakMap<Project[], WeakMap<ProjectHostSetup[], ProjectHostSetupProjection>>
|
||||
>()
|
||||
let floatingVisibleTabCountCache: FloatingVisibleTabCountCache | null = null
|
||||
|
||||
function getWorktreeSnapshot(worktreesByRepo: AppState['worktreesByRepo']): WorktreeSnapshot {
|
||||
@@ -134,6 +138,54 @@ function getCachedProvidedProjectHostSetupProjection(
|
||||
return projection
|
||||
}
|
||||
|
||||
function mergeById<T extends { id: string }>(base: readonly T[], overlay: readonly T[]): T[] {
|
||||
const merged = [...base]
|
||||
const indexById = new Map(merged.map((entry, index) => [entry.id, index]))
|
||||
for (const entry of overlay) {
|
||||
const index = indexById.get(entry.id)
|
||||
if (index === undefined) {
|
||||
indexById.set(entry.id, merged.length)
|
||||
merged.push(entry)
|
||||
} else {
|
||||
merged[index] = entry
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
function mergeProjectHostSetupProjection(
|
||||
repos: AppState['repos'],
|
||||
projects: Project[],
|
||||
setups: ProjectHostSetup[]
|
||||
): ProjectHostSetupProjection {
|
||||
const cachedByProjects = mergedProjectHostSetupProjectionCache.get(repos)
|
||||
const cachedBySetups = cachedByProjects?.get(projects)
|
||||
const cachedProjection = cachedBySetups?.get(setups)
|
||||
if (cachedProjection) {
|
||||
return cachedProjection
|
||||
}
|
||||
const derived = getCachedProjectHostSetupProjection(repos)
|
||||
// Why: older runtimes/profiles may hydrate empty or partial project/setup arrays
|
||||
// beside legacy repos. Keep repo-backed compatibility rows visible in that case.
|
||||
const projection = {
|
||||
projects: mergeById(derived.projects, projects),
|
||||
setups: mergeById(derived.setups, setups)
|
||||
}
|
||||
const nextCachedByProjects =
|
||||
cachedByProjects ??
|
||||
new WeakMap<Project[], WeakMap<ProjectHostSetup[], ProjectHostSetupProjection>>()
|
||||
const nextCachedBySetups =
|
||||
cachedBySetups ?? new WeakMap<ProjectHostSetup[], ProjectHostSetupProjection>()
|
||||
nextCachedBySetups.set(setups, projection)
|
||||
if (!cachedBySetups) {
|
||||
nextCachedByProjects.set(projects, nextCachedBySetups)
|
||||
}
|
||||
if (!cachedByProjects) {
|
||||
mergedProjectHostSetupProjectionCache.set(repos, nextCachedByProjects)
|
||||
}
|
||||
return projection
|
||||
}
|
||||
|
||||
export function selectFloatingVisibleTabCount(state: FloatingVisibleTabCountState): number {
|
||||
const terminalTabs = state.tabsByWorktree[FLOATING_TERMINAL_WORKTREE_ID] ?? EMPTY_TABS
|
||||
const browserTabs =
|
||||
@@ -213,6 +265,24 @@ export function getProjectHostSetupProjectionFromState(
|
||||
state: Pick<AppState, 'repos'> & Partial<Pick<AppState, 'projects' | 'projectHostSetups'>>
|
||||
): ProjectHostSetupProjection {
|
||||
if (state.projects && state.projectHostSetups) {
|
||||
const repoIds = new Set(state.repos.map((repo) => repo.id))
|
||||
const coveredRepoIds = new Set<string>()
|
||||
for (const setup of state.projectHostSetups) {
|
||||
const repoId = typeof setup.repoId === 'string' ? setup.repoId : ''
|
||||
if (repoIds.has(repoId)) {
|
||||
coveredRepoIds.add(repoId)
|
||||
}
|
||||
if (repoIds.has(setup.id)) {
|
||||
coveredRepoIds.add(setup.id)
|
||||
}
|
||||
}
|
||||
if (state.repos.length > 0 && coveredRepoIds.size < repoIds.size) {
|
||||
return mergeProjectHostSetupProjection(
|
||||
state.repos,
|
||||
state.projects as Project[],
|
||||
state.projectHostSetups as ProjectHostSetup[]
|
||||
)
|
||||
}
|
||||
return getCachedProvidedProjectHostSetupProjection(
|
||||
state.projects as Project[],
|
||||
state.projectHostSetups as ProjectHostSetup[]
|
||||
|
||||
Reference in New Issue
Block a user