mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(runtime): tab property edits live with the other tab properties, and main typechecks again (#32168)
#32018 pushed headless-terminal-tab-close.ts over the 300-line lint limit, and its new test passed sleepWorktree to RuntimeNotifier after #31564 removed it. setMobileSessionTabProps moves unchanged into RuntimeHeadlessTabProperties, which already owns the helpers it calls and directly extends the close class.
This commit is contained in:
@@ -23,7 +23,6 @@ function fixture() {
|
||||
renameTerminal: vi.fn(),
|
||||
focusTerminal: vi.fn(),
|
||||
closeTerminal: vi.fn(),
|
||||
sleepWorktree: vi.fn(),
|
||||
terminalFitOverrideChanged: vi.fn(),
|
||||
terminalDriverChanged: vi.fn()
|
||||
} satisfies RuntimeNotifier
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
TerminalLayoutSnapshot,
|
||||
TerminalPaneLayoutNode
|
||||
} from '../../../shared/terminal-tab-types'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch'
|
||||
import { cloneTerminalLayoutSnapshot } from '../mobile-session-layout-projection'
|
||||
import { sameStringRecord, terminalLayoutNodeEqual } from '../../../shared/terminal-layout-equality'
|
||||
|
||||
@@ -32,6 +33,77 @@ function paneLayoutEditIsNoOp(
|
||||
}
|
||||
|
||||
export abstract class RuntimeHeadlessTabProperties extends OrcaRuntimeWithCloseHeadlessMobileTerminalTab {
|
||||
// Why: tab color/pin are host-authoritative for remote-server tabs but had no
|
||||
// push path, so pinning or coloring a tab reverted on the next snapshot and
|
||||
// was never persisted. Persist to the workspace session + live snapshot.
|
||||
async setMobileSessionTabProps(
|
||||
worktreeSelector: string,
|
||||
args: {
|
||||
tabId: string
|
||||
color?: string | null
|
||||
isPinned?: boolean
|
||||
viewMode?: 'terminal' | 'chat'
|
||||
customTitle?: string | null
|
||||
}
|
||||
): Promise<{ updated: true }> {
|
||||
const explicitWorktreeId = this.getExplicitSessionWorktreeIdSelector(worktreeSelector)
|
||||
const worktreeId =
|
||||
explicitWorktreeId ?? (await this.resolveWorktreeSelector(worktreeSelector)).id
|
||||
// Why: a renderer-authoritative host owns + republishes tab props, so a
|
||||
// headless write would be overwritten. Persist only when headless.
|
||||
const snapshot = this.mobileSessionTabsByWorktree.get(worktreeId)
|
||||
const hostTabId = snapshot
|
||||
? (this.resolveMobileSessionHostTabId(snapshot, args.tabId) ?? args.tabId)
|
||||
: args.tabId
|
||||
// Why by tab: a sleeping or starting pane has no handle for terminal.rename to name (#29975).
|
||||
const customTitle =
|
||||
args.customTitle === undefined ? undefined : args.customTitle?.trim() || null
|
||||
// Switch point: a terminal tab main owns takes its props in main's layout, window or not.
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN && (await this.setMainTabProps(worktreeId, hostTabId, args))) {
|
||||
return { updated: true }
|
||||
}
|
||||
if (this.getAvailableAuthoritativeWindow()) {
|
||||
if (args.color !== undefined || args.isPinned !== undefined || args.viewMode !== undefined) {
|
||||
throw Object.assign(
|
||||
new Error(
|
||||
'Tab color, pin, and view mode edits are unavailable while a desktop window owns the layout'
|
||||
),
|
||||
{ code: 'capability_unsupported' }
|
||||
)
|
||||
}
|
||||
if (customTitle !== undefined) {
|
||||
this.notifier?.renameTerminal(hostTabId, customTitle)
|
||||
}
|
||||
return { updated: true }
|
||||
}
|
||||
this.persistHeadlessSessionTabProps(worktreeId, hostTabId, args)
|
||||
// Why: a live pane's own title outranks the snapshot's, so name it the way terminal.rename does.
|
||||
const liveHandles =
|
||||
customTitle === undefined
|
||||
? []
|
||||
: this.getPtyIdsForExplicitTabClose(worktreeId, hostTabId).flatMap((ptyId) => {
|
||||
const handle = this.handleByPtyId.get(ptyId)
|
||||
return handle ? [handle] : []
|
||||
})
|
||||
for (const handle of liveHandles) {
|
||||
await this.renameTerminal(handle, customTitle ?? null)
|
||||
}
|
||||
if (customTitle !== undefined && liveHandles.length === 0) {
|
||||
this.persistHeadlessTerminalTitle(worktreeId, hostTabId, customTitle)
|
||||
}
|
||||
// A live pane's rename above already set its title; only a paneless tab needs `title` here.
|
||||
this.applyHeadlessSessionTabPropsToSnapshot(worktreeId, hostTabId, {
|
||||
...args,
|
||||
...(customTitle !== undefined && liveHandles.length === 0
|
||||
? {
|
||||
customTitle,
|
||||
title: customTitle ?? this.headlessTerminalFallbackTitle(worktreeId, hostTabId)
|
||||
}
|
||||
: {})
|
||||
})
|
||||
return { updated: true }
|
||||
}
|
||||
|
||||
/** What `title` reads once a manual name is cleared; readers that predate `customTitle` show it. */
|
||||
protected headlessTerminalFallbackTitle(worktreeId: string, tabId: string): string {
|
||||
const tab = this.getWorkspaceSessionForWorktree(worktreeId)?.tabsByWorktree[worktreeId]?.find(
|
||||
|
||||
@@ -19,10 +19,6 @@ import type { RuntimeTerminalRename } from '../../../shared/runtime-terminal-con
|
||||
import type { RuntimeSessionTabCloseReason } from '../../../shared/runtime-session-contracts'
|
||||
|
||||
export abstract class OrcaRuntimeWithCloseHeadlessMobileTerminalTab extends OrcaRuntimeWithMainLayoutTabMoves {
|
||||
protected abstract applyHeadlessSessionTabPropsToSnapshot(
|
||||
...args: Parameters<HeadlessTabPropertiesMethods['applyHeadlessSessionTabPropsToSnapshot']>
|
||||
): ReturnType<HeadlessTabPropertiesMethods['applyHeadlessSessionTabPropsToSnapshot']>
|
||||
|
||||
protected abstract applyHeadlessTerminalPaneLayoutToSnapshot(
|
||||
...args: Parameters<HeadlessTabPropertiesMethods['applyHeadlessTerminalPaneLayoutToSnapshot']>
|
||||
): ReturnType<HeadlessTabPropertiesMethods['applyHeadlessTerminalPaneLayoutToSnapshot']>
|
||||
@@ -35,10 +31,6 @@ export abstract class OrcaRuntimeWithCloseHeadlessMobileTerminalTab extends Orca
|
||||
...args: Parameters<PersistHeadlessTerminalTitleMethods['normalizeMobileSessionTabOrder']>
|
||||
): ReturnType<PersistHeadlessTerminalTitleMethods['normalizeMobileSessionTabOrder']>
|
||||
|
||||
protected abstract persistHeadlessSessionTabProps(
|
||||
...args: Parameters<HeadlessTabPropertiesMethods['persistHeadlessSessionTabProps']>
|
||||
): ReturnType<HeadlessTabPropertiesMethods['persistHeadlessSessionTabProps']>
|
||||
|
||||
protected abstract persistHeadlessTerminalPaneLayout(
|
||||
...args: Parameters<HeadlessTabPropertiesMethods['persistHeadlessTerminalPaneLayout']>
|
||||
): ReturnType<HeadlessTabPropertiesMethods['persistHeadlessTerminalPaneLayout']>
|
||||
@@ -275,77 +267,4 @@ export abstract class OrcaRuntimeWithCloseHeadlessMobileTerminalTab extends Orca
|
||||
}
|
||||
return { updated: true }
|
||||
}
|
||||
|
||||
protected abstract headlessTerminalFallbackTitle(worktreeId: string, tabId: string): string
|
||||
|
||||
// Why: tab color/pin are host-authoritative for remote-server tabs but had no
|
||||
// push path, so pinning or coloring a tab reverted on the next snapshot and
|
||||
// was never persisted. Persist to the workspace session + live snapshot.
|
||||
async setMobileSessionTabProps(
|
||||
worktreeSelector: string,
|
||||
args: {
|
||||
tabId: string
|
||||
color?: string | null
|
||||
isPinned?: boolean
|
||||
viewMode?: 'terminal' | 'chat'
|
||||
customTitle?: string | null
|
||||
}
|
||||
): Promise<{ updated: true }> {
|
||||
const explicitWorktreeId = this.getExplicitSessionWorktreeIdSelector(worktreeSelector)
|
||||
const worktreeId =
|
||||
explicitWorktreeId ?? (await this.resolveWorktreeSelector(worktreeSelector)).id
|
||||
// Why: a renderer-authoritative host owns + republishes tab props, so a
|
||||
// headless write would be overwritten. Persist only when headless.
|
||||
const snapshot = this.mobileSessionTabsByWorktree.get(worktreeId)
|
||||
const hostTabId = snapshot
|
||||
? (this.resolveMobileSessionHostTabId(snapshot, args.tabId) ?? args.tabId)
|
||||
: args.tabId
|
||||
// Why by tab: a sleeping or starting pane has no handle for terminal.rename to name (#29975).
|
||||
const customTitle =
|
||||
args.customTitle === undefined ? undefined : args.customTitle?.trim() || null
|
||||
// Switch point: a terminal tab main owns takes its props in main's layout, window or not.
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN && (await this.setMainTabProps(worktreeId, hostTabId, args))) {
|
||||
return { updated: true }
|
||||
}
|
||||
if (this.getAvailableAuthoritativeWindow()) {
|
||||
if (args.color !== undefined || args.isPinned !== undefined || args.viewMode !== undefined) {
|
||||
throw Object.assign(
|
||||
new Error(
|
||||
'Tab color, pin, and view mode edits are unavailable while a desktop window owns the layout'
|
||||
),
|
||||
{ code: 'capability_unsupported' }
|
||||
)
|
||||
}
|
||||
if (customTitle !== undefined) {
|
||||
this.notifier?.renameTerminal(hostTabId, customTitle)
|
||||
}
|
||||
return { updated: true }
|
||||
}
|
||||
this.persistHeadlessSessionTabProps(worktreeId, hostTabId, args)
|
||||
// Why: a live pane's own title outranks the snapshot's, so name it the way terminal.rename does.
|
||||
const liveHandles =
|
||||
customTitle === undefined
|
||||
? []
|
||||
: this.getPtyIdsForExplicitTabClose(worktreeId, hostTabId).flatMap((ptyId) => {
|
||||
const handle = this.handleByPtyId.get(ptyId)
|
||||
return handle ? [handle] : []
|
||||
})
|
||||
for (const handle of liveHandles) {
|
||||
await this.renameTerminal(handle, customTitle ?? null)
|
||||
}
|
||||
if (customTitle !== undefined && liveHandles.length === 0) {
|
||||
this.persistHeadlessTerminalTitle(worktreeId, hostTabId, customTitle)
|
||||
}
|
||||
// A live pane's rename above already set its title; only a paneless tab needs `title` here.
|
||||
this.applyHeadlessSessionTabPropsToSnapshot(worktreeId, hostTabId, {
|
||||
...args,
|
||||
...(customTitle !== undefined && liveHandles.length === 0
|
||||
? {
|
||||
customTitle,
|
||||
title: customTitle ?? this.headlessTerminalFallbackTitle(worktreeId, hostTabId)
|
||||
}
|
||||
: {})
|
||||
})
|
||||
return { updated: true }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user