Files
orca/src/shared/runtime-pane-title-leaf-id.ts
T
Jinwoo Hong a039e9064b feat(sidebar): main keeps the list's time rules on one clock, checked against the window (dark) (#30104)
* 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
2026-10-17 15:57:54 -07:00

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
}