mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
* Allow worktree parentage across projects on one host * Keep sidebar placement and project ownership consistent * Keep project labels on phone rows outside their own section * Derive sidebar placement and reveal from shared occurrences * Preserve project text across workspace card styles * Resolve saved lineage peers without rebuilding host projects * fix: preserve independent project deletion progress * fix: satisfy mobile formatting and test lint checks * test: declare project fixture arrays without assertions * Align reveal fixtures with rendered workspace occurrences * fix: reuse project name for cross-repo child cards * fix: satisfy mobile section lint after main merge * fix: retain project label for nested mobile rows * test: keep nested card owner assertion resilient * test: assert project badge visibility across card styles * test: drop lineage tests already covered by the whole-flow suites The host-peer projection is exercised by the runtime and SSH listing integration tests, and the simple phone nesting case is covered by the deep-chain, Pinned and hidden-project flow test.
188 lines
6.2 KiB
TypeScript
188 lines
6.2 KiB
TypeScript
import { getTopVisibleLineageRoots, getVisibleLineagePlacement } from './worktree-lineage-placement'
|
|
|
|
export type OccurrenceSectionPath = {
|
|
key: string
|
|
parent?: OccurrenceSectionPath
|
|
}
|
|
|
|
export type WorktreeOccurrence<Row> = {
|
|
key: string
|
|
row: Row
|
|
owningSectionKey?: string
|
|
sectionKey: string
|
|
placement: 'pinned' | 'natural'
|
|
sectionPath: OccurrenceSectionPath
|
|
parent?: WorktreeOccurrence<Row>
|
|
depth: number
|
|
childCount: number
|
|
isLastChild: boolean
|
|
showProjectName: boolean
|
|
}
|
|
|
|
export type WorktreeOccurrenceSection<Row> = {
|
|
key: string
|
|
items: readonly Row[]
|
|
revealPath?: OccurrenceSectionPath
|
|
placement?: 'pinned' | 'natural'
|
|
}
|
|
|
|
export type WorktreeOccurrencePlan<Row> = {
|
|
bySection: ReadonlyMap<string, readonly WorktreeOccurrence<Row>[]>
|
|
preferredByIdentity: ReadonlyMap<string, WorktreeOccurrence<Row>>
|
|
}
|
|
|
|
type LineagePlacementRows<Row> = {
|
|
pinned: Row[]
|
|
natural: readonly Row[]
|
|
displayRootByIdentity: ReadonlyMap<string, Row>
|
|
}
|
|
|
|
export function getLineagePlacementRows<Row>(args: {
|
|
rows: readonly Row[]
|
|
childrenByParentIdentity: ReadonlyMap<string, readonly Row[]>
|
|
identityOf: (row: Row) => string
|
|
isPinned: (row: Row) => boolean
|
|
duplicatePinned: boolean
|
|
}): LineagePlacementRows<Row> {
|
|
const { rows, childrenByParentIdentity, identityOf } = args
|
|
const pinnedPlacement = getVisibleLineagePlacement(
|
|
rows,
|
|
rows.filter(args.isPinned),
|
|
childrenByParentIdentity,
|
|
identityOf,
|
|
true
|
|
)
|
|
const pinned = rows.filter((row) => pinnedPlacement.has(identityOf(row)))
|
|
const natural = args.duplicatePinned
|
|
? rows
|
|
: rows.filter((row) => !pinnedPlacement.has(identityOf(row)))
|
|
return {
|
|
pinned,
|
|
natural,
|
|
displayRootByIdentity: getTopVisibleLineageRoots(natural, childrenByParentIdentity, identityOf)
|
|
}
|
|
}
|
|
|
|
/** Placement and disclosure are facts of an uncollapsed section occurrence. */
|
|
export function buildWorktreeOccurrencePlan<
|
|
Row,
|
|
Section extends WorktreeOccurrenceSection<Row>
|
|
>(args: {
|
|
rows: readonly Row[]
|
|
childrenByParentIdentity: ReadonlyMap<string, readonly Row[]>
|
|
identityOf: (row: Row) => string
|
|
isPinned: (row: Row) => boolean
|
|
duplicatePinned: boolean
|
|
sectionsOf: (placement: LineagePlacementRows<Row>) => readonly Section[]
|
|
owningSectionOf?: (row: Row) => { key: string; label: string }
|
|
projectIdOf?: (row: Row) => string
|
|
}): WorktreeOccurrencePlan<Row> & { sections: readonly Section[] } {
|
|
const { identityOf, childrenByParentIdentity } = args
|
|
const ownerByIdentity = new Map(
|
|
args.rows.map((row) => [identityOf(row), args.owningSectionOf?.(row)])
|
|
)
|
|
const sections = args.sectionsOf(getLineagePlacementRows(args))
|
|
const parentByIdentity = new Map<string, string>()
|
|
for (const [parent, children] of childrenByParentIdentity) {
|
|
for (const child of children) {
|
|
parentByIdentity.set(identityOf(child), parent)
|
|
}
|
|
}
|
|
const bySection = new Map<string, WorktreeOccurrence<Row>[]>()
|
|
const preferredByIdentity = new Map<string, WorktreeOccurrence<Row>>()
|
|
for (const section of sections) {
|
|
const sectionPath = section.revealPath ?? { key: section.key }
|
|
const members = new Set(section.items.map(identityOf))
|
|
const children = new Map<string, Row[]>()
|
|
const roots: Row[] = []
|
|
for (const row of section.items) {
|
|
const parent = parentByIdentity.get(identityOf(row))
|
|
if (parent !== undefined && members.has(parent)) {
|
|
const siblings = children.get(parent) ?? []
|
|
siblings.push(row)
|
|
children.set(parent, siblings)
|
|
} else {
|
|
roots.push(row)
|
|
}
|
|
}
|
|
const occurrences: WorktreeOccurrence<Row>[] = []
|
|
const emitted = new Set<string>()
|
|
const pending: { row: Row; parent?: WorktreeOccurrence<Row>; isLastChild: boolean }[] = []
|
|
const emit = (): void => {
|
|
while (pending.length > 0) {
|
|
const next = pending.pop()
|
|
if (!next) {
|
|
continue
|
|
}
|
|
const identity = identityOf(next.row)
|
|
if (emitted.has(identity)) {
|
|
continue
|
|
}
|
|
emitted.add(identity)
|
|
const descendants = children.get(identity) ?? []
|
|
const owner = ownerByIdentity.get(identity)
|
|
const occurrence: WorktreeOccurrence<Row> = {
|
|
key: `${section.key}:${identity}`,
|
|
row: next.row,
|
|
owningSectionKey: owner?.key,
|
|
sectionKey: section.key,
|
|
placement: section.placement ?? 'natural',
|
|
sectionPath,
|
|
parent: next.parent,
|
|
depth: next.parent ? next.parent.depth + 1 : 0,
|
|
childCount: descendants.length,
|
|
isLastChild: next.isLastChild,
|
|
showProjectName:
|
|
next.parent !== undefined &&
|
|
args.projectIdOf !== undefined &&
|
|
args.projectIdOf(next.row) !== args.projectIdOf(next.parent.row)
|
|
}
|
|
occurrences.push(occurrence)
|
|
if (section.placement !== 'pinned' || !preferredByIdentity.has(identity)) {
|
|
preferredByIdentity.set(identity, occurrence)
|
|
}
|
|
for (let i = descendants.length - 1; i >= 0; i -= 1) {
|
|
pending.push({
|
|
row: descendants[i],
|
|
parent: occurrence,
|
|
isLastChild: i === descendants.length - 1
|
|
})
|
|
}
|
|
}
|
|
}
|
|
roots.forEach((row, i) => {
|
|
pending.push({ row, isLastChild: i === roots.length - 1 })
|
|
emit()
|
|
})
|
|
// Why: malformed older-peer cycles must not erase the entire list.
|
|
for (const row of section.items) {
|
|
if (!emitted.has(identityOf(row))) {
|
|
pending.push({ row, isLastChild: true })
|
|
emit()
|
|
}
|
|
}
|
|
bySection.set(section.key, occurrences)
|
|
}
|
|
return { sections, bySection, preferredByIdentity }
|
|
}
|
|
|
|
/** Collapse only removes occurrences; it never changes their parent or section. */
|
|
export function getExpandedWorktreeOccurrences<Row>(
|
|
occurrences: readonly WorktreeOccurrence<Row>[],
|
|
isCollapsed: (row: Row) => boolean
|
|
): WorktreeOccurrence<Row>[] {
|
|
const result: WorktreeOccurrence<Row>[] = []
|
|
let hiddenBelowDepth = Infinity
|
|
for (const occurrence of occurrences) {
|
|
if (occurrence.depth > hiddenBelowDepth) {
|
|
continue
|
|
}
|
|
hiddenBelowDepth = Infinity
|
|
result.push(occurrence)
|
|
if (isCollapsed(occurrence.row)) {
|
|
hiddenBelowDepth = occurrence.depth
|
|
}
|
|
}
|
|
return result
|
|
}
|