Files
orca/src/shared/terminal-pane-placement.ts
T
Jinwoo Hong a0026588e4 terminal: window says where each new terminal goes (no behavior change) (#26078)
* refactor(terminal): renderer senders attach placement to pty spawn, report-only

The window now says where each fresh terminal goes: the first pane of a tab
sends new-tab with the tab's creation fields, a split sends the parent leaf,
direction, ratio and the tree after the split, background launches send the
same, and a Codex restart that rebuilds a rootless layout sends root. A
reattach sends nothing. Main still only records whether placement names the
tab its binding write picks; its 'absent' value now counts only new leaves
without placement, so it reads the fallback mint and graft directly.

Extends the shared placement type with optional row, ratio and proposedRoot,
each dropped alone if malformed, so older and newer peers keep working.

* fix(terminal): background agent placement row claims no launchAgent the tab lacks

The adopted tab is created without launchAgent, so a row naming one would
diverge from the renderer's tab once main applies placement.

* fix(terminal): a before-split publishes its pane with the final tree order

wrapInSplit now places the new pane first itself, so the pane-created
handler's proposedRoot sees the real post-split tree instead of the order
before the subtree split moved it.

* test(terminal): type the background-terminal tab patch precisely
2026-10-07 14:45:26 -04:00

58 lines
1.9 KiB
TypeScript

import { z } from 'zod'
import { isTerminalLeafId } from './stable-pane-id'
import { terminalPaneLayoutNodeSchema, terminalTabSchema } from './workspace-session-schema'
// The creation fields of the tab the renderer made, so main need not wait for its save.
const NewTabRow = terminalTabSchema
.pick({
title: true,
defaultTitle: true,
customTitle: true,
color: true,
createdAt: true,
startupCwd: true,
quickCommandLabel: true,
launchAgent: true,
viewMode: true
})
.extend({ shellOverride: z.string().optional() })
.partial()
// Optional fields degrade alone: a malformed one is dropped, not the whole placement.
const NewTabPlacement = z.object({
kind: z.literal('new-tab'),
row: NewTabRow.optional().catch(undefined)
})
const SplitPlacement = z.object({
kind: z.literal('split'),
parentLeafId: z
.string()
.max(128)
.refine((value): boolean => isTerminalLeafId(value)),
direction: z.enum(['horizontal', 'vertical']),
ratio: z.number().optional().catch(undefined),
/** The tab's tree after the split, which `parentLeafId` alone can't express (subtrees, before/after). */
proposedRoot: terminalPaneLayoutNodeSchema.optional().catch(undefined)
})
// An existing tab whose layout is still empty.
const RootPlacement = z.object({ kind: z.literal('root') })
const TerminalPanePlacementSchema = z.discriminatedUnion('kind', [
NewTabPlacement,
SplitPlacement,
RootPlacement
])
export type TerminalPaneNewTabRow = z.infer<typeof NewTabRow>
/** Which tab and leaf a new PTY joins; a binding reads it only for a leaf it does not know yet. */
export type TerminalPanePlacement = z.infer<typeof TerminalPanePlacementSchema>
/** Null for anything malformed, including a kind this build does not know. */
export function parseTerminalPanePlacement(value: unknown): TerminalPanePlacement | null {
const parsed = TerminalPanePlacementSchema.safeParse(value)
return parsed.success ? parsed.data : null
}