Files
orca/src/shared/project-section-catalog-schema.ts
T
Neil d277381bb3 Hide projects from the sidebar without stopping them (#41529)
* Hide projects from the sidebar without stopping them

Adds Repo.hiddenAt, a per-host, view-only flag. A hidden project's
workspaces, empty header and project-filter entry leave the desktop
sidebar, the paired phone's list, the new-workspace picker and the
palette's project filter. Its terminals, agents and automations keep
running, and repo.list / worktree.ps still list it.

- Hide Project in the project menu (toast with Undo), a collapsed
  "N hidden projects" list under the workspace list with Show buttons,
  and a Hide from sidebar switch in Settings > Projects.
- repo.setHidden RPC ({repo, repoHostId?, hidden} -> {repo, hostId,
  hidden}) and `orca repo hide|unhide [--host]`; `repo list` marks
  hidden rows.
- Opening a hidden project's workspace (notification, CLI, deep link)
  focuses it without unhiding it, lifting sidebar filters, or leaving a
  pending sidebar reveal.
- repo.update echo check: a host that drops hiddenAt reports failure
  instead of reading as hidden.

* Address hidden-projects review: multi-host hide, picker scope, mute, stale filters, shared predicate, restore list styling

* Hidden projects re-review: keep saved filters, Agents filter, composer seed, per-toast Undo, shared default host

* Hidden projects: address review threads (pre-feed, palette search, notices, dialog default, seed remap, row keys, a11y)

* Hidden projects: no implicit dialog default when every project is hidden
2026-11-12 07:02:57 -08:00

95 lines
4.0 KiB
TypeScript

import * as 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'
// Read sidebar-feed/mobileRelay.projectSections catalogs; MobileProjectSectionCatalog is the sender shape.
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()),
// Older desktops omit it; the project then stays listed, as it did.
hiddenAt: salvagedOptional('hiddenAt', 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(),
rootProjectOrder: salvagedOptional('rootProjectOrder', z.number().finite()),
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>