mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* fix(sidebar): the phone reads projects and project groups from main's sidebar feed, installed with the rows The phone built its project sections from a catalog the desktop window published through a side channel (mobileRelay.projectSections). That copy changed on a timer after the event that told the phone to re-read it, and a re-read asked for during a read in flight was dropped, so the phone could keep an old grouping (a project under the wrong group, a group missing) indefinitely. Main now publishes the catalog (repos with their group and order, project groups, projects and host setups, every host's including paired servers') as a projectCatalog domain of the sidebar feed, from its own store and its own reads of each paired server. The phone reads it from the same install as the rows. mobileRelay.projectSections stays, answered from the same builder, for pages that predate the domain; a page on a desktop without the domain still reads it, now re-reading when an invalidation lands mid-read. The phone e2e list helper runs the page's real refresh lifecycle instead of re-reading on every assertion, and covers a group move while a read is held. * test(mobile): stand AppState in for the screen hooks' react-native mocks, and tolerate a world with no project rows in the parity catalog * fix(sidebar): main files a server's copy of a grouped project under its group, and reads a server's catalog only on catalog events - The window gives an ungrouped paired-server repo the group of an existing copy of the same project (applyInheritedProjectGroups). That function now lives in src/shared and main's catalog builder folds each server's repos through it, so the phone files the copy where the desktop does. - A paired server's change watch now says whether the event can carry a catalog change. Only ready, a replay and reposChanged mark the catalog stale; worktreesChanged (pins, unread, sort stamps) reads rows only. - The parity fixture now builds the server side from raw server answers, folded through the window's mergeFetchedReposForHost and through main's own server read and builder, with a server copy of a grouped project and a partial saved manual order. It fails without the inheritance. * fix(sidebar): parse a paired server's catalog rows with a schema instead of reading keys reflectively * fix(mobile): sync the feed ref after render and re-read queued project sections without an await loop * fix(mobile): spell the AppState shape the refresh reads, so the e2e helper pulls in no React Native types * test(sidebar): give the merged builder test's feed domains a project catalog --------- Co-authored-by: E2E Test <e2e@test.local>
93 lines
3.8 KiB
TypeScript
93 lines
3.8 KiB
TypeScript
import { z } from 'zod'
|
|
import { normalizeExecutionHostId } from './execution-host'
|
|
import type { ProjectHostSetupMethod } from './project-types'
|
|
import type { ProjectGroup, ProjectGroupCreatedFrom } from './project-group-types'
|
|
import type { RepoKind } from './repo-types'
|
|
import type { ProjectOrderBy } from './ui-chrome-types'
|
|
import { hostUnionArms, salvagedOptional, salvagingArray } from './zod-salvage'
|
|
|
|
// The sidebar's project catalog as a reader parses it, from mobileRelay.projectSections or the
|
|
// sidebar feed's projectCatalog domain (MobileProjectSectionCatalog is what a host sends).
|
|
|
|
const SETUP_METHOD_ARMS = hostUnionArms<ProjectHostSetupMethod>({
|
|
'legacy-repo': true,
|
|
'imported-existing-folder': true,
|
|
cloned: true,
|
|
provisioned: true
|
|
})
|
|
const REPO_KIND_ARMS = hostUnionArms<RepoKind>({ git: true, folder: true })
|
|
const PROJECT_ORDER_BY_ARMS = hostUnionArms<ProjectOrderBy>({ manual: true, recent: true })
|
|
|
|
// A host id the desktop spells; normalized, so a spelling this build cannot read drops.
|
|
const hostIdSchema = z.string().transform((value, ctx) => {
|
|
const hostId = normalizeExecutionHostId(value)
|
|
if (!hostId) {
|
|
ctx.addIssue({ code: 'custom', message: 'unknown host id' })
|
|
return z.NEVER
|
|
}
|
|
return hostId
|
|
})
|
|
|
|
const repoSchema = z.object({
|
|
id: z.string(),
|
|
displayName: z.string(),
|
|
path: z.string(),
|
|
badgeColor: salvagedOptional('badgeColor', z.string()),
|
|
addedAt: salvagedOptional('addedAt', z.number()),
|
|
connectionId: salvagedOptional('connectionId', z.string().nullable()),
|
|
executionHostId: salvagedOptional('executionHostId', hostIdSchema),
|
|
// Older desktops omit both; the project then lists ungrouped, as it did.
|
|
projectGroupId: salvagedOptional('projectGroupId', z.string().nullable()),
|
|
projectGroupOrder: salvagedOptional('projectGroupOrder', z.number()),
|
|
// Decoded where drawn (readMobileRepoIcon), so an arm this build lacks still keeps the row.
|
|
repoIcon: z.unknown().optional()
|
|
})
|
|
|
|
const setupSchema = z.object({
|
|
id: z.string(),
|
|
projectId: z.string(),
|
|
hostId: hostIdSchema,
|
|
repoId: z.string(),
|
|
path: z.string(),
|
|
displayName: z.string(),
|
|
// Why the fallback: only 'provisioned' changes grouping, so a newer arm groups as a checkout.
|
|
setupMethod: z.enum(SETUP_METHOD_ARMS).catch('legacy-repo'),
|
|
kind: salvagedOptional('kind', z.enum(REPO_KIND_ARMS)),
|
|
connectionId: salvagedOptional('connectionId', z.string().nullable()),
|
|
executionHostId: salvagedOptional('executionHostId', hostIdSchema)
|
|
})
|
|
|
|
const PROJECT_GROUP_CREATED_FROM_ARMS = hostUnionArms<ProjectGroupCreatedFrom>({
|
|
manual: true,
|
|
'folder-scan': true,
|
|
migration: true
|
|
})
|
|
|
|
export const ProjectGroupSchema = z.object({
|
|
id: z.string(),
|
|
name: z.string(),
|
|
parentPath: z.string().nullable(),
|
|
connectionId: salvagedOptional('connectionId', z.string().nullable()),
|
|
executionHostId: salvagedOptional('executionHostId', z.string().nullable()),
|
|
parentGroupId: z.string().nullable(),
|
|
createdFrom: z.enum(PROJECT_GROUP_CREATED_FROM_ARMS).catch('manual'),
|
|
tabOrder: z.number(),
|
|
isCollapsed: z.boolean(),
|
|
color: z.string().nullable(),
|
|
createdAt: z.number(),
|
|
updatedAt: z.number()
|
|
}) satisfies z.ZodType<ProjectGroup, unknown>
|
|
|
|
export const ProjectSectionCatalogSchema = z.object({
|
|
repos: salvagingArray(repoSchema),
|
|
projects: salvagingArray(z.object({ id: z.string(), displayName: z.string() })),
|
|
setups: salvagingArray(setupSchema),
|
|
// Until the phone has read the shared UI value (projectOrderBy, ui.get), it orders by this.
|
|
projectOrderBy: z.enum(PROJECT_ORDER_BY_ARMS).catch('manual'),
|
|
// Every host's project groups, for a list built from the sidebar feed; older desktops omit it.
|
|
projectGroups: salvagedOptional('projectGroups', salvagingArray(ProjectGroupSchema))
|
|
})
|
|
|
|
/** The desktop sidebar's Project grouping inputs, as a reader holds them. */
|
|
export type ServedProjectSections = z.output<typeof ProjectSectionCatalogSchema>
|