mirror of
https://github.com/stablyai/orca.git
synced 2026-11-05 19:47:27 +00:00
* refactor(sidebar): move the Smart comparator and attention scoring to src/shared/sidebar-list Main needs them to own the frozen order. Renderer helpers they depend on move with them; the window imports the shared copies. No behaviour change. * refactor(sidebar): one shared ranking for the frozen sort order The window's sort memo and the palette's Smart sort now call rankWorktreesForSort, so main can produce the same order from its own records. The pure state-history step moves to shared for the same reason. No behaviour change. * feat(sidebar): main keeps the list's time rules on one clock, checked against the window in dev and test * test(sidebar): fake-clock parity of main's temporal owner against the window * fix(sidebar): main restarts the settle window only where the window's sortEpoch bumps * test(sidebar): type the owner harness without an assertion * fix(sidebar): main's temporal owner stays idle until a dev or test window begins a session * fix(sidebar): main reads Hide sleeping from the field the window persists * refactor(sidebar): keep the begin params in their own contract module so the snapshot schema compiles out * chore: restore files the merge's formatter rewrote to their origin/main content
157 lines
4.9 KiB
TypeScript
157 lines
4.9 KiB
TypeScript
import { FIRST_PANE_ID } from './pane-key'
|
|
import { isTerminalLeafId } from './stable-pane-id'
|
|
import type { TerminalLayoutSnapshot, TerminalPaneLayoutNode } from './terminal-tab-types'
|
|
|
|
export type RuntimePaneTitleLeafResolution = {
|
|
title: string | null
|
|
hasAnyPaneTitle: boolean
|
|
}
|
|
|
|
function getLeftmostLeafId(node: TerminalPaneLayoutNode): string {
|
|
return node.type === 'leaf' ? node.leafId : getLeftmostLeafId(node.first)
|
|
}
|
|
|
|
function collectReplayCreatedPaneLeafIds(
|
|
node: TerminalPaneLayoutNode,
|
|
leafIdsInReplayCreationOrder: string[]
|
|
): void {
|
|
if (node.type === 'leaf') {
|
|
return
|
|
}
|
|
|
|
leafIdsInReplayCreationOrder.push(getLeftmostLeafId(node.second))
|
|
|
|
if (node.first.type === 'split') {
|
|
collectReplayCreatedPaneLeafIds(node.first, leafIdsInReplayCreationOrder)
|
|
}
|
|
if (node.second.type === 'split') {
|
|
collectReplayCreatedPaneLeafIds(node.second, leafIdsInReplayCreationOrder)
|
|
}
|
|
}
|
|
|
|
function collectLeafIdsInReplayCreationOrder(
|
|
node: TerminalPaneLayoutNode | null | undefined
|
|
): string[] {
|
|
if (!node) {
|
|
return []
|
|
}
|
|
const leafIdsInReplayCreationOrder = [getLeftmostLeafId(node)]
|
|
if (node.type === 'split') {
|
|
collectReplayCreatedPaneLeafIds(node, leafIdsInReplayCreationOrder)
|
|
}
|
|
return leafIdsInReplayCreationOrder
|
|
}
|
|
|
|
export function collectRuntimePaneLeafIds(
|
|
node: TerminalPaneLayoutNode | null | undefined
|
|
): string[] {
|
|
if (!node) {
|
|
return []
|
|
}
|
|
if (node.type === 'leaf') {
|
|
return [node.leafId]
|
|
}
|
|
return [...collectRuntimePaneLeafIds(node.first), ...collectRuntimePaneLeafIds(node.second)]
|
|
}
|
|
|
|
export function resolveRuntimePaneTitleLeafId(
|
|
tabLayout: { root?: TerminalLayoutSnapshot['root'] } | undefined,
|
|
runtimePaneId: string
|
|
): string | null {
|
|
return resolveRuntimePaneTitleLeafIdFromRoot(tabLayout?.root, runtimePaneId)
|
|
}
|
|
|
|
/**
|
|
* Resolve the runtime-reported pane title for a specific layout leaf. Pane
|
|
* title maps are keyed by runtime pane id, which only lines up with the leaf id
|
|
* after replay-order resolution — split tabs can carry a sparse title map, so a
|
|
* lone background title must not be attributed to an unrelated leaf.
|
|
*/
|
|
export function resolveRuntimePaneTitleForLeaf(
|
|
tabLayout: { root?: TerminalLayoutSnapshot['root'] } | undefined,
|
|
paneTitles: Record<number, string> | undefined,
|
|
leafId: string
|
|
): string | null {
|
|
return resolveRuntimePaneTitleLeafResolution(tabLayout, paneTitles, leafId).title
|
|
}
|
|
|
|
export function resolveRuntimePaneTitleLeafResolution(
|
|
tabLayout: { root?: TerminalLayoutSnapshot['root'] } | undefined,
|
|
paneTitles: Record<number, string> | undefined,
|
|
leafId: string
|
|
): RuntimePaneTitleLeafResolution {
|
|
if (!paneTitles) {
|
|
return { title: null, hasAnyPaneTitle: false }
|
|
}
|
|
|
|
const titlesByPaneId = paneTitles as Record<string, string>
|
|
let firstTitle: string | null = null
|
|
let hasOnePaneTitle = false
|
|
let hasMultiplePaneTitles = false
|
|
|
|
for (const runtimePaneId in titlesByPaneId) {
|
|
if (!Object.hasOwn(titlesByPaneId, runtimePaneId)) {
|
|
continue
|
|
}
|
|
|
|
const title = titlesByPaneId[runtimePaneId]
|
|
if (hasOnePaneTitle) {
|
|
hasMultiplePaneTitles = true
|
|
} else {
|
|
firstTitle = title
|
|
hasOnePaneTitle = true
|
|
}
|
|
|
|
if (resolveRuntimePaneTitleLeafId(tabLayout, runtimePaneId) === leafId) {
|
|
return { title, hasAnyPaneTitle: true }
|
|
}
|
|
}
|
|
|
|
// Why: without a layout root, only a single reported pane title can be
|
|
// attributed; any pane title still suppresses stale tab-title fallback.
|
|
if (!tabLayout?.root && hasOnePaneTitle && !hasMultiplePaneTitles) {
|
|
return { title: firstTitle, hasAnyPaneTitle: true }
|
|
}
|
|
|
|
return { title: null, hasAnyPaneTitle: hasOnePaneTitle }
|
|
}
|
|
|
|
export function resolveRuntimePaneTitleLeafIdFromRoot(
|
|
root: TerminalPaneLayoutNode | null | undefined,
|
|
runtimePaneId: string
|
|
): string | null {
|
|
if (isTerminalLeafId(runtimePaneId)) {
|
|
return runtimePaneId
|
|
}
|
|
const numericPaneId = Number(runtimePaneId)
|
|
if (!Number.isInteger(numericPaneId) || numericPaneId < FIRST_PANE_ID) {
|
|
return null
|
|
}
|
|
const leafIds = collectLeafIdsInReplayCreationOrder(root)
|
|
return leafIds[numericPaneId - FIRST_PANE_ID] ?? null
|
|
}
|
|
|
|
/**
|
|
* Resolve a sparse live runtime pane slot through the tab's current PTY bindings.
|
|
* PaneManager ids survive closes with gaps, while the live PTY list retains order.
|
|
*/
|
|
export function resolveRuntimePaneTitleLeafIdFromSparseSlots(args: {
|
|
layout: Pick<TerminalLayoutSnapshot, 'ptyIdsByLeafId'> | undefined
|
|
paneId: number
|
|
liveSlotIds: number[]
|
|
ptyIds: string[]
|
|
}): string | null {
|
|
if (args.ptyIds.length !== args.liveSlotIds.length) {
|
|
return null
|
|
}
|
|
const paneIndex = args.liveSlotIds.indexOf(args.paneId)
|
|
const ptyId = paneIndex === -1 ? undefined : args.ptyIds[paneIndex]
|
|
if (!ptyId) {
|
|
return null
|
|
}
|
|
const boundLeafIds = Object.entries(args.layout?.ptyIdsByLeafId ?? {})
|
|
.filter(([, boundPtyId]) => boundPtyId === ptyId)
|
|
.map(([leafId]) => leafId)
|
|
return boundLeafIds.length === 1 ? boundLeafIds[0] : null
|
|
}
|