mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 10:37:35 +00:00
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
This commit is contained in:
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { parseTerminalPanePlacement } from './terminal-pane-placement'
|
||||
|
||||
const LEAF = '11111111-1111-4111-8111-111111111111'
|
||||
const OTHER_LEAF = '22222222-2222-4222-8222-222222222222'
|
||||
|
||||
describe('parseTerminalPanePlacement', () => {
|
||||
it('accepts each kind', () => {
|
||||
@@ -11,6 +12,49 @@ describe('parseTerminalPanePlacement', () => {
|
||||
expect(parseTerminalPanePlacement({ kind: 'root' })).toEqual({ kind: 'root' })
|
||||
})
|
||||
|
||||
it('keeps a new tab row and a split ratio and proposed tree', () => {
|
||||
const newTab = {
|
||||
kind: 'new-tab',
|
||||
row: {
|
||||
title: 'Terminal 2',
|
||||
customTitle: null,
|
||||
color: '#f97316',
|
||||
createdAt: 1,
|
||||
viewMode: 'chat'
|
||||
}
|
||||
}
|
||||
const split = {
|
||||
kind: 'split',
|
||||
parentLeafId: LEAF,
|
||||
direction: 'horizontal',
|
||||
ratio: 0.3,
|
||||
proposedRoot: {
|
||||
type: 'split',
|
||||
direction: 'horizontal',
|
||||
first: { type: 'leaf', leafId: OTHER_LEAF },
|
||||
second: { type: 'leaf', leafId: LEAF },
|
||||
ratio: 0.3
|
||||
}
|
||||
}
|
||||
expect(parseTerminalPanePlacement(newTab)).toEqual(newTab)
|
||||
expect(parseTerminalPanePlacement(split)).toEqual(split)
|
||||
})
|
||||
|
||||
it('drops only a malformed optional field', () => {
|
||||
expect(parseTerminalPanePlacement({ kind: 'new-tab', row: { createdAt: 'now' } })).toEqual({
|
||||
kind: 'new-tab'
|
||||
})
|
||||
expect(
|
||||
parseTerminalPanePlacement({
|
||||
kind: 'split',
|
||||
parentLeafId: LEAF,
|
||||
direction: 'vertical',
|
||||
ratio: 'half',
|
||||
proposedRoot: { type: 'tree' }
|
||||
})
|
||||
).toEqual({ kind: 'split', parentLeafId: LEAF, direction: 'vertical' })
|
||||
})
|
||||
|
||||
it('strips fields a newer sender adds', () => {
|
||||
expect(parseTerminalPanePlacement({ kind: 'root', future: 1 })).toEqual({ kind: 'root' })
|
||||
expect(parseTerminalPanePlacement({ kind: 'new-tab', size: { cols: 120, rows: 40 } })).toEqual({
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
import { z } from 'zod'
|
||||
import { isTerminalLeafId } from './stable-pane-id'
|
||||
import { terminalPaneLayoutNodeSchema, terminalTabSchema } from './workspace-session-schema'
|
||||
|
||||
const NewTabPlacement = z.object({ kind: z.literal('new-tab') })
|
||||
// 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'),
|
||||
@@ -9,7 +30,10 @@ const SplitPlacement = z.object({
|
||||
.string()
|
||||
.max(128)
|
||||
.refine((value): boolean => isTerminalLeafId(value)),
|
||||
direction: z.enum(['horizontal', 'vertical'])
|
||||
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.
|
||||
@@ -21,6 +45,8 @@ const TerminalPanePlacementSchema = z.discriminatedUnion('kind', [
|
||||
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>
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const workspaceKeySchema = z.custom<WorkspaceKey>(
|
||||
// Why: z.lazy + type annotation keeps the recursive inference working without
|
||||
// forcing zod to resolve the whole tree at definition time. Discriminated on `type` because a
|
||||
// plain union re-tries the leaf branch for every split node of every restored terminal layout.
|
||||
const terminalPaneLayoutNodeSchema: z.ZodType<TerminalPaneLayoutNode> = z.lazy(() =>
|
||||
export const terminalPaneLayoutNodeSchema: z.ZodType<TerminalPaneLayoutNode> = z.lazy(() =>
|
||||
z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('leaf'),
|
||||
@@ -82,7 +82,7 @@ const terminalLayoutSnapshotSchema = z.object({
|
||||
|
||||
// ─── Terminal tab (legacy) ──────────────────────────────────────────
|
||||
|
||||
const terminalTabSchema = z.object({
|
||||
export const terminalTabSchema = z.object({
|
||||
id: terminalTabIdSchema,
|
||||
ptyId: z.string().nullable(),
|
||||
worktreeId: z.string(),
|
||||
|
||||
Reference in New Issue
Block a user