Files
orca/src/shared/mobile-project-sections-contract.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

87 lines
3.5 KiB
TypeScript

import type { ProjectGroup } from './project-group-types'
import type { ProjectHostSetupProjection } from './project-host-setup-projection'
import type { ProjectSectionProject, ProjectSectionSetup } from './project-section-grouping'
import type { RepoIcon } from './repo-icon'
import type { Repo } from './repo-types'
import type { ProjectOrderBy } from './ui-chrome-types'
// Desktop owns the sidebar grouping inputs the phone must match.
export const MOBILE_PROJECT_SECTIONS_METHOD = 'mobileRelay.projectSections'
/** A sidebar repo as Project sections read it; never its hooks, credentials or AI settings. */
export type MobileProjectSectionRepo = Pick<Repo, 'id' | 'displayName' | 'path' | 'badgeColor'> &
Partial<
Pick<
Repo,
| 'addedAt'
| 'connectionId'
| 'executionHostId'
| 'projectGroupId'
| 'projectGroupOrder'
| 'hiddenAt'
>
> & { repoIcon?: RepoIcon }
export type MobileProjectSectionCatalog = {
/** Every repo the sidebar holds, across hosts, in its header order. */
repos: MobileProjectSectionRepo[]
projects: ProjectSectionProject[]
setups: ProjectSectionSetup[]
projectOrderBy: ProjectOrderBy
/** Every host's project groups, which a feed reader lists folder workspaces under; older desktops omit it. */
projectGroups?: ProjectGroup[]
}
/** Null until a desktop window has published its sidebar catalog. */
export type MobileProjectSectionsResult = { catalog: MobileProjectSectionCatalog | null }
function sectionRepo(repo: Repo): MobileProjectSectionRepo {
return {
id: repo.id,
displayName: repo.displayName,
path: repo.path,
badgeColor: repo.badgeColor,
addedAt: repo.addedAt,
...(repo.connectionId !== undefined ? { connectionId: repo.connectionId } : {}),
...(repo.executionHostId ? { executionHostId: repo.executionHostId } : {}),
// Why: the phone's list files a project under its group, in the group's order.
...(repo.projectGroupId ? { projectGroupId: repo.projectGroupId } : {}),
...(typeof repo.projectGroupOrder === 'number'
? { projectGroupOrder: repo.projectGroupOrder }
: {}),
// Why: a reader must drop a hidden project's header and rows, as the sidebar does.
...(typeof repo.hiddenAt === 'number' ? { hiddenAt: repo.hiddenAt } : {}),
// Exclude uploaded data URLs: each repo icon can add hundreds of KB to every read.
...(repo.repoIcon && !(repo.repoIcon.type === 'image' && repo.repoIcon.src.startsWith('data:'))
? { repoIcon: repo.repoIcon }
: {})
}
}
/** The sidebar's repos, in its order, and the projection its project-header selector produced. */
export function buildMobileProjectSectionCatalog(
repos: readonly Repo[],
projection: ProjectHostSetupProjection,
projectOrderBy: ProjectOrderBy,
projectGroups?: readonly ProjectGroup[]
): MobileProjectSectionCatalog {
return {
repos: repos.map(sectionRepo),
projects: projection.projects.map(({ id, displayName }) => ({ id, displayName })),
setups: projection.setups.map((setup) => ({
id: setup.id,
projectId: setup.projectId,
hostId: setup.hostId,
repoId: setup.repoId,
path: setup.path,
displayName: setup.displayName,
setupMethod: setup.setupMethod,
...(setup.kind ? { kind: setup.kind } : {}),
...(setup.connectionId !== undefined ? { connectionId: setup.connectionId } : {}),
...(setup.executionHostId ? { executionHostId: setup.executionHostId } : {})
})),
projectOrderBy,
...(projectGroups ? { projectGroups: [...projectGroups] } : {})
}
}