From 28c726855e1d226d4d8e5960c4a9ff2df3d40ff4 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Thu, 22 Oct 2026 23:59:41 -0400 Subject: [PATCH] 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. --- .../renderer-owned-tab-properties.test.ts | 1 - .../session-tabs/headless-tab-properties.ts | 72 +++++++++++++++++ .../headless-terminal-tab-close.ts | 81 ------------------- 3 files changed, 72 insertions(+), 82 deletions(-) diff --git a/src/main/runtime/renderer-owned-tab-properties.test.ts b/src/main/runtime/renderer-owned-tab-properties.test.ts index a49a157af91..b2b84ed4cab 100644 --- a/src/main/runtime/renderer-owned-tab-properties.test.ts +++ b/src/main/runtime/renderer-owned-tab-properties.test.ts @@ -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 diff --git a/src/main/runtime/session-tabs/headless-tab-properties.ts b/src/main/runtime/session-tabs/headless-tab-properties.ts index a157bca97a5..f646c6ccb38 100644 --- a/src/main/runtime/session-tabs/headless-tab-properties.ts +++ b/src/main/runtime/session-tabs/headless-tab-properties.ts @@ -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( diff --git a/src/main/runtime/session-tabs/headless-terminal-tab-close.ts b/src/main/runtime/session-tabs/headless-terminal-tab-close.ts index 26968f82176..f6797ba9aea 100644 --- a/src/main/runtime/session-tabs/headless-terminal-tab-close.ts +++ b/src/main/runtime/session-tabs/headless-terminal-tab-close.ts @@ -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 - ): ReturnType - protected abstract applyHeadlessTerminalPaneLayoutToSnapshot( ...args: Parameters ): ReturnType @@ -35,10 +31,6 @@ export abstract class OrcaRuntimeWithCloseHeadlessMobileTerminalTab extends Orca ...args: Parameters ): ReturnType - protected abstract persistHeadlessSessionTabProps( - ...args: Parameters - ): ReturnType - protected abstract persistHeadlessTerminalPaneLayout( ...args: Parameters ): ReturnType @@ -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 } - } }