diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx index 323a3dc497e..3e8a4a08796 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx @@ -548,6 +548,8 @@ function TerminalPane( const leafIds = getNativeChatLeafIds() return leafIds.length === 1 ? leafIds[0] : null }, [getNativeChatLeafIds, tabWideAgentHintLeafId]) + const getTabWideAgentHintLeafIdRef = useRef(getTabWideAgentHintLeafId) + getTabWideAgentHintLeafIdRef.current = getTabWideAgentHintLeafId useEffect(() => { if (tabWideAgentHintLeafId !== undefined) { return @@ -1349,6 +1351,7 @@ function TerminalPane( effectiveMacOptionAsAltRef: macOptionAsAltRef, initialLayoutRef, managerRef, + getTabWideAgentHintLeafId: () => getTabWideAgentHintLeafIdRef.current(), containerRef, expandedStyleSnapshotRef, paneFontSizesRef, diff --git a/src/renderer/src/components/terminal-pane/codex-detached-pane-restart.test.ts b/src/renderer/src/components/terminal-pane/codex-detached-pane-restart.test.ts index 78d944229c5..70682d481fb 100644 --- a/src/renderer/src/components/terminal-pane/codex-detached-pane-restart.test.ts +++ b/src/renderer/src/components/terminal-pane/codex-detached-pane-restart.test.ts @@ -297,7 +297,8 @@ describe('codex detached pane restart executor', () => { worktreeId: 'wt1', getManager: () => null, getContainer: () => null, - getPtyIdForPane: () => null + getPtyIdForPane: () => null, + getTabWideAgentHintLeafId: () => null }) try { await sweepUnclaimedCodexPaneRestarts() @@ -324,7 +325,8 @@ describe('codex detached pane restart executor', () => { worktreeId: 'wt1', getManager: () => null, getContainer: () => null, - getPtyIdForPane: () => OLD_PTY + getPtyIdForPane: () => OLD_PTY, + getTabWideAgentHintLeafId: () => null }) try { pendingSpawn.resolve({ id: NEW_PTY }) diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts index 31928913df5..cef1faf0675 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts @@ -272,6 +272,7 @@ type UseTerminalPaneLifecycleDeps = { effectiveMacOptionAsAltRef: React.RefObject initialLayoutRef: React.RefObject managerRef: React.RefObject + getTabWideAgentHintLeafId: () => string | null containerRef: React.RefObject expandedStyleSnapshotRef: React.MutableRefObject< Map @@ -624,6 +625,7 @@ export function useTerminalPaneLifecycle({ effectiveMacOptionAsAltRef, initialLayoutRef, managerRef, + getTabWideAgentHintLeafId, containerRef, expandedStyleSnapshotRef, paneFontSizesRef, @@ -921,7 +923,8 @@ export function useTerminalPaneLifecycle({ worktreeId, getManager: () => managerRef.current, getContainer: () => containerRef.current, - getPtyIdForPane: (paneId) => paneTransportsRef.current.get(paneId)?.getPtyId() ?? null + getPtyIdForPane: (paneId) => paneTransportsRef.current.get(paneId)?.getPtyId() ?? null, + getTabWideAgentHintLeafId }) const fileOpenLinkHint = getTerminalFileOpenHint() diff --git a/src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts b/src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts index 50b6d28c190..f23a0f04a4a 100644 --- a/src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts @@ -78,14 +78,16 @@ describe('runtime terminal registration ownership', () => { worktreeId: 'wt-1', getManager: () => null, getContainer: () => null, - getPtyIdForPane: () => null + getPtyIdForPane: () => null, + getTabWideAgentHintLeafId: () => null }) const second = registerRuntimeTerminalTab({ tabId: 'term-replaced', worktreeId: 'wt-1', getManager: () => null, getContainer: () => null, - getPtyIdForPane: () => null + getPtyIdForPane: () => null, + getTabWideAgentHintLeafId: () => null }) first() @@ -115,7 +117,8 @@ describe('scheduleRuntimeGraphSync', () => { worktreeId: 'wt-1', getManager: () => null, getContainer: () => null, - getPtyIdForPane: () => null + getPtyIdForPane: () => null, + getTabWideAgentHintLeafId: () => null }) setRuntimeGraphStoreStateGetter(() => makeState({ @@ -151,7 +154,8 @@ describe('scheduleRuntimeGraphSync', () => { worktreeId: 'wt-1', getManager: () => null, getContainer: () => null, - getPtyIdForPane: () => null + getPtyIdForPane: () => null, + getTabWideAgentHintLeafId: () => null }) setRuntimeGraphStoreStateGetter(() => makeState({ diff --git a/src/renderer/src/runtime/sync-runtime-graph.test.ts b/src/renderer/src/runtime/sync-runtime-graph.test.ts index 2365b25c3b7..8f19058beab 100644 --- a/src/renderer/src/runtime/sync-runtime-graph.test.ts +++ b/src/renderer/src/runtime/sync-runtime-graph.test.ts @@ -4,6 +4,7 @@ import { buildMobileSessionTabSnapshots, canSkipRuntimeMobileSessionSyncKeyBuild, getRuntimeMobileSessionSyncKey, + registerRuntimeTerminalTab, runtimeMobileSessionSyncKeysEqual } from './sync-runtime-graph' import type { AgentStatusEntry } from '../../../shared/agent-status-types' @@ -83,6 +84,47 @@ function makeAgentStatusEntry(overrides: Partial = {}): AgentS } } +function registerMobileTestSurface(args: { + tabId: string + leafIds: string[] + activeLeafId: string + launchAgentLeafId: string | null +}): { + unregister: () => void + setTopology: (leafIds: string[], activeLeafId: string) => void +} { + let leafIds = [...args.leafIds] + let activeLeafId = args.activeLeafId + const paneIdByLeafId = new Map(args.leafIds.map((leafId, index) => [leafId, index + 1])) + const manager = { + getPanes: () => leafIds.map((leafId) => ({ id: paneIdByLeafId.get(leafId)!, leafId })), + getActivePane: () => { + const paneId = paneIdByLeafId.get(activeLeafId) + return !paneId || !leafIds.includes(activeLeafId) + ? null + : { id: paneId, leafId: activeLeafId } + }, + getLeafId: (paneId: number) => + leafIds.find((leafId) => paneIdByLeafId.get(leafId) === paneId) ?? null, + getNumericIdForLeaf: (leafId: string) => + leafIds.includes(leafId) ? (paneIdByLeafId.get(leafId) ?? null) : null + } + return { + unregister: registerRuntimeTerminalTab({ + tabId: args.tabId, + worktreeId: 'wt-1', + getManager: () => manager as never, + getContainer: () => null, + getPtyIdForPane: (paneId) => `pty-${paneId}`, + getTabWideAgentHintLeafId: () => args.launchAgentLeafId + }), + setTopology: (nextLeafIds, nextActiveLeafId) => { + leafIds = [...nextLeafIds] + activeLeafId = nextActiveLeafId + } + } +} + describe('getRuntimeMobileSessionSyncKey', () => { it('changes when mobile markdown tab state changes', () => { const base = makeState({ @@ -878,6 +920,232 @@ describe('buildMobileSessionTabSnapshots', () => { expect(snapshot?.tabs[0]).not.toHaveProperty('launchDraft') }) + it('keeps agent identity and launch context off a plain sibling leaf', () => { + const agentLeafId = '11111111-1111-4111-8111-111111111111' + const shellLeafId = '22222222-2222-4222-8222-222222222222' + const surface = registerMobileTestSurface({ + tabId: 'term-1', + leafIds: [agentLeafId, shellLeafId], + activeLeafId: shellLeafId, + launchAgentLeafId: agentLeafId + }) + try { + const state = makeState({ + tabsByWorktree: { + 'wt-1': [ + { + id: 'term-1', + title: 'Codex working', + customTitle: null, + launchAgent: 'codex', + generatedTitle: 'Inspect the image', + aiVaultTitle: { + agent: 'codex', + sessionId: 'session-1', + title: '[Image #1] Inspect the image' + } + } + ] + } as unknown as AppState['tabsByWorktree'], + terminalLayoutsByTabId: { + 'term-1': { + root: { + type: 'split', + direction: 'horizontal', + first: { type: 'leaf', leafId: agentLeafId }, + second: { type: 'leaf', leafId: shellLeafId } + }, + activeLeafId: shellLeafId, + expandedLeafId: null + } + } as AppState['terminalLayoutsByTabId'], + runtimePaneTitlesByTabId: { + 'term-1': { 1: 'Codex working', 2: 'zsh' } + }, + agentStatusByPaneKey: { + [`term-1:${agentLeafId}`]: makeAgentStatusEntry({ + paneKey: `term-1:${agentLeafId}`, + agentType: 'codex', + terminalTitle: 'Codex working' + }) + }, + nativeChatLaunchDraftByTabId: { + 'term-1': { + tabId: 'term-1', + agent: 'codex', + text: 'inspect this image', + createdAt: 1 + } + } + }) + + const tabs = buildMobileSessionTabSnapshots(state)[0]?.tabs ?? [] + const agentLeaf = tabs.find((tab) => tab.type === 'terminal' && tab.leafId === agentLeafId) + const shellLeaf = tabs.find((tab) => tab.type === 'terminal' && tab.leafId === shellLeafId) + + expect(agentLeaf).toMatchObject({ + title: 'Codex working', + agentStatus: { agentType: 'codex' } + }) + expect(shellLeaf).toMatchObject({ title: 'zsh' }) + expect(shellLeaf).not.toHaveProperty('agentStatus') + expect(shellLeaf).not.toHaveProperty('launchAgent') + expect(shellLeaf).not.toHaveProperty('launchDraft') + expect(shellLeaf).not.toHaveProperty('quickCommandLabel') + } finally { + surface.unregister() + } + }) + + it('re-engages tab-wide launch context after the owning leaf becomes the sole leaf', () => { + const agentLeafId = '33333333-3333-4333-8333-333333333333' + const shellLeafId = '44444444-4444-4444-8444-444444444444' + const surface = registerMobileTestSurface({ + tabId: 'term-collapse', + leafIds: [agentLeafId, shellLeafId], + activeLeafId: shellLeafId, + launchAgentLeafId: agentLeafId + }) + try { + surface.setTopology([agentLeafId], agentLeafId) + const state = makeState({ + tabsByWorktree: { + 'wt-1': [ + { + id: 'term-collapse', + title: 'Codex ready', + customTitle: null, + launchAgent: 'codex', + aiVaultTitle: { + agent: 'codex', + sessionId: 'session-collapse', + title: 'Inspect mobile routing' + } + } + ] + } as unknown as AppState['tabsByWorktree'], + terminalLayoutsByTabId: { + 'term-collapse': { + root: { type: 'leaf', leafId: agentLeafId }, + activeLeafId: agentLeafId, + expandedLeafId: null + } + } as AppState['terminalLayoutsByTabId'], + nativeChatLaunchDraftByTabId: { + 'term-collapse': { + tabId: 'term-collapse', + agent: 'codex', + text: 'inspect mobile routing', + createdAt: 2 + } + } + }) + + expect(buildMobileSessionTabSnapshots(state)[0]?.tabs).toEqual([ + expect.objectContaining({ + leafId: agentLeafId, + title: 'Inspect mobile routing', + launchAgent: 'codex', + launchDraft: 'inspect mobile routing' + }) + ]) + } finally { + surface.unregister() + } + }) + + it('keeps split-leaf ownership stable across reorder and active-leaf changes', () => { + const agentLeafId = '55555555-5555-4555-8555-555555555555' + const shellLeafId = '66666666-6666-4666-8666-666666666666' + const surface = registerMobileTestSurface({ + tabId: 'term-reorder', + leafIds: [agentLeafId, shellLeafId], + activeLeafId: agentLeafId, + launchAgentLeafId: agentLeafId + }) + try { + surface.setTopology([shellLeafId, agentLeafId], shellLeafId) + const state = makeState({ + activeTabId: 'term-reorder', + tabsByWorktree: { + 'wt-1': [ + { + id: 'term-reorder', + title: '[Image #1] Wrong scope', + customTitle: null, + launchAgent: 'codex' + } + ] + } as unknown as AppState['tabsByWorktree'], + terminalLayoutsByTabId: { + 'term-reorder': { + root: { + type: 'split', + direction: 'horizontal', + first: { type: 'leaf', leafId: shellLeafId }, + second: { type: 'leaf', leafId: agentLeafId } + }, + activeLeafId: shellLeafId, + expandedLeafId: null + } + } as AppState['terminalLayoutsByTabId'], + runtimePaneTitlesByTabId: { + 'term-reorder': { 1: 'Codex working', 2: 'fish' } + }, + agentStatusByPaneKey: { + [`term-reorder:${agentLeafId}`]: makeAgentStatusEntry({ + paneKey: `term-reorder:${agentLeafId}`, + terminalTitle: 'Codex working' + }) + } + }) + + const tabs = buildMobileSessionTabSnapshots(state)[0]?.tabs ?? [] + expect(tabs).toMatchObject([ + { leafId: shellLeafId, title: 'fish', isActive: true }, + { leafId: agentLeafId, title: 'Codex working', isActive: false } + ]) + expect(tabs[0]).not.toHaveProperty('agentStatus') + expect(tabs[1]).toHaveProperty('agentStatus.agentType', 'codex') + } finally { + surface.unregister() + } + }) + + it('preserves single-leaf tab-wide identity while provider status is arriving', () => { + const leafId = '77777777-7777-4777-8777-777777777777' + const state = makeState({ + tabsByWorktree: { + 'wt-1': [ + { + id: 'term-pending-status', + title: 'Codex ready', + customTitle: null, + launchAgent: 'codex', + generatedTitle: 'Prepare the release' + } + ] + } as unknown as AppState['tabsByWorktree'], + terminalLayoutsByTabId: { + 'term-pending-status': { + root: { type: 'leaf', leafId }, + activeLeafId: leafId, + expandedLeafId: null + } + } as AppState['terminalLayoutsByTabId'], + settings: { ...getDefaultSettings('/tmp'), tabAutoGenerateTitle: true }, + agentStatusByPaneKey: {} + }) + + expect(buildMobileSessionTabSnapshots(state)[0]?.tabs).toEqual([ + expect.objectContaining({ + leafId, + title: 'Prepare the release', + launchAgent: 'codex' + }) + ]) + }) + it('preserves source-control diff metadata for mobile file tabs', () => { const diffId = 'wt-1::diff::unstaged::src/app.ts' const state = makeState({ diff --git a/src/renderer/src/runtime/sync-runtime-graph.ts b/src/renderer/src/runtime/sync-runtime-graph.ts index ebd29bc20b7..4c717053a7f 100644 --- a/src/renderer/src/runtime/sync-runtime-graph.ts +++ b/src/renderer/src/runtime/sync-runtime-graph.ts @@ -35,6 +35,11 @@ import type { TerminalTab } from '../../../shared/types' import { resolveTerminalTabTitle } from '../../../shared/tab-title-resolution' +import { + isNativeChatTabWideFallbackSafe, + nativeChatLaunchAgentForLeaf, + resolveNativeChatActiveLayoutLeafId +} from '../components/native-chat/native-chat-leaf-routing' import { getActiveTabNavOrder, getGroupVisibleTabOrder, @@ -50,6 +55,7 @@ type RegisteredTerminalTab = { getManager: () => PaneManager | null getContainer: () => HTMLDivElement | null getPtyIdForPane: (paneId: number) => string | null + getTabWideAgentHintLeafId: () => string | null } type OpenFileByWorktreeAndId = Map> @@ -108,6 +114,7 @@ type MountedTerminalSurfaceCapture = { liveLayoutRoot: TerminalPaneLayoutNode | null numericPaneIdByLeafId: ReadonlyMap ptyIdByNumericPaneId: ReadonlyMap + tabWideAgentHintLeafId: string | null } /** * One worktree's complete mobile-snapshot input set. @@ -128,7 +135,7 @@ type MobileSessionWorktreeInputs = { openFileIds: readonly string[] terminalLayoutByTabId: ReadonlyMap paneTitlesByTabId: ReadonlyMap - launchDraftByTabId: ReadonlyMap< + launchDraftByPaneKey: ReadonlyMap< string, NonNullable[string] > @@ -992,6 +999,11 @@ function buildMobileSessionWorktreeInputs( ): MobileSessionWorktreeInputs { const terminalTabs = state.tabsByWorktree[worktreeId] ?? EMPTY_WORKTREE_TERMINAL_TABS const terminalTabIds = terminalTabs.map((tab) => tab.id) + const terminalLayoutByTabId = narrowRecordByKeys(state.terminalLayoutsByTabId, terminalTabIds) + const mountedSurfaceCaptureByTabId = captureMountedTerminalSurfaces( + terminalTabs, + state.terminalLayoutsByTabId + ) const browserWorkspaces = publication.browserTabsByWorktree[worktreeId] ?? EMPTY_WORKTREE_BROWSER_WORKSPACES const pagesByBrowserWorkspaceId = narrowRecordByKeys( @@ -1023,9 +1035,14 @@ function buildMobileSessionWorktreeInputs( tabGroupLayout: (state.layoutByWorktree ?? EMPTY_LAYOUT_BY_WORKTREE)[worktreeId], openFilesById, openFileIds, - terminalLayoutByTabId: narrowRecordByKeys(state.terminalLayoutsByTabId, terminalTabIds), + terminalLayoutByTabId, paneTitlesByTabId: narrowRecordByKeys(state.runtimePaneTitlesByTabId, terminalTabIds), - launchDraftByTabId: narrowRecordByKeys(state.nativeChatLaunchDraftByTabId, terminalTabIds), + launchDraftByPaneKey: buildMobileLaunchDraftsByPaneKey({ + terminalTabs, + terminalLayoutByTabId, + mountedSurfaceCaptureByTabId, + launchDraftByTabId: narrowRecordByKeys(state.nativeChatLaunchDraftByTabId, terminalTabIds) + }), agentStatusByPaneKey: publication.agentStatusByWorktreeId.get(worktreeId) ?? EMPTY_NARROWED_BY_KEY, editorDraftVersionByFileId: narrowMapByKeys( @@ -1046,10 +1063,7 @@ function buildMobileSessionWorktreeInputs( activeBrowserWorkspaceId: state.activeBrowserTabIdByWorktree?.[worktreeId] ?? null, generatedTitlesEnabled: publication.generatedTitlesEnabled, terminalTheme: publication.terminalTheme, - mountedSurfaceCaptureByTabId: captureMountedTerminalSurfaces( - terminalTabs, - state.terminalLayoutsByTabId - ) + mountedSurfaceCaptureByTabId } } @@ -1099,7 +1113,8 @@ function captureMountedTerminalSurface( typeof HTMLElement !== 'undefined' && firstChild instanceof HTMLElement ? firstChild : null ), numericPaneIdByLeafId, - ptyIdByNumericPaneId + ptyIdByNumericPaneId, + tabWideAgentHintLeafId: registered.getTabWideAgentHintLeafId() } } @@ -1114,6 +1129,7 @@ function mountedTerminalSurfaceCaptureEquals( a.paneLeafIds.every((leafId, index) => b.paneLeafIds[index] === leafId) && narrowedEntriesEqual(a.numericPaneIdByLeafId, b.numericPaneIdByLeafId) && narrowedEntriesEqual(a.ptyIdByNumericPaneId, b.ptyIdByNumericPaneId) && + a.tabWideAgentHintLeafId === b.tabWideAgentHintLeafId && // Why: serialization allocates a fresh tree per capture, so it compares by content. jsonContentEquals(a.liveLayoutRoot, b.liveLayoutRoot) ) @@ -1182,7 +1198,7 @@ function canReuseMobileSessionSnapshot( previous.terminalTheme === next.terminalTheme && narrowedEntriesEqual(previous.terminalLayoutByTabId, next.terminalLayoutByTabId) && narrowedEntriesEqual(previous.paneTitlesByTabId, next.paneTitlesByTabId) && - narrowedEntriesEqual(previous.launchDraftByTabId, next.launchDraftByTabId) && + narrowedEntriesEqual(previous.launchDraftByPaneKey, next.launchDraftByPaneKey) && narrowedEntriesEqual(previous.agentStatusByPaneKey, next.agentStatusByPaneKey) && narrowedEntriesEqual(previous.editorDraftVersionByFileId, next.editorDraftVersionByFileId) && narrowedEntriesEqual(previous.pagesByBrowserWorkspaceId, next.pagesByBrowserWorkspaceId) && @@ -1811,6 +1827,54 @@ function getRuntimeLeafIdsForTerminal( return [] } +function resolveMobileTabWideAgentHintLeafId( + capture: MountedTerminalSurfaceCapture | undefined, + savedLayout: AppState['terminalLayoutsByTabId'][string] | undefined +): string | null { + if (capture) { + return capture.tabWideAgentHintLeafId + } + return isNativeChatTabWideFallbackSafe(savedLayout) + ? resolveNativeChatActiveLayoutLeafId(savedLayout) + : null +} + +function buildMobileLaunchDraftsByPaneKey(args: { + terminalTabs: AppState['tabsByWorktree'][string] + terminalLayoutByTabId: MobileSessionWorktreeInputs['terminalLayoutByTabId'] + mountedSurfaceCaptureByTabId: MobileSessionWorktreeInputs['mountedSurfaceCaptureByTabId'] + launchDraftByTabId: ReadonlyMap< + string, + NonNullable[string] + > +}): MobileSessionWorktreeInputs['launchDraftByPaneKey'] { + const draftsByPaneKey = new Map< + string, + NonNullable[string] + >() + for (const terminal of args.terminalTabs) { + const draft = args.launchDraftByTabId.get(terminal.id) + if (!draft || draft.resolved) { + continue + } + const capture = args.mountedSurfaceCaptureByTabId.get(terminal.id) + const savedLayout = args.terminalLayoutByTabId.get(terminal.id) + const leafIds = getRuntimeLeafIdsForTerminal(capture, savedLayout) + const ownerLeafId = resolveMobileTabWideAgentHintLeafId(capture, savedLayout) + const launchAgent = nativeChatLaunchAgentForLeaf({ + launchAgent: terminal.launchAgent, + launchAgentLeafId: ownerLeafId, + leafId: ownerLeafId, + leafIds + }) + if (!launchAgent || launchAgent !== draft.agent || !ownerLeafId) { + continue + } + draftsByPaneKey.set(makePaneKey(terminal.id, ownerLeafId), draft) + } + return draftsByPaneKey +} + function buildMobileTerminalSurfaceTabs( inputs: MobileSessionWorktreeInputs, terminal: NonNullable[number], @@ -1822,6 +1886,7 @@ function buildMobileTerminalSurfaceTabs( : inputs.activeTerminalTabId === terminal.id const savedLayout = inputs.terminalLayoutByTabId.get(terminal.id) const leafIds = getRuntimeLeafIdsForTerminal(capture, savedLayout) + const launchAgentLeafId = resolveMobileTabWideAgentHintLeafId(capture, savedLayout) const activeLeafId = capture?.hasLiveActivePane ? capture.liveActiveLeafId : (savedLayout?.activeLeafId ?? leafIds[0] ?? null) @@ -1832,16 +1897,6 @@ function buildMobileTerminalSurfaceTabs( : undefined const savedPtyIdsByLeafId = sanitizedSavedLayout?.ptyIdsByLeafId ?? {} const terminalTheme = inputs.terminalTheme - // Agent-matched like the desktop consumer: a pane whose agent changed keeps its - // tab id, so an unmatched seed would prefill the new agent's chat with stale text. - const seededLaunchDraft = inputs.launchDraftByTabId.get(terminal.id) - const launchDraftEntry = - seededLaunchDraft && - !seededLaunchDraft.resolved && - seededLaunchDraft.agent === terminal.launchAgent - ? seededLaunchDraft - : null - const publishedLaunchDraft = launchDraftEntry?.text.trim() ? launchDraftEntry : null const liveLayoutRoot = capture?.liveLayoutRoot ?? null const parentLayout = normalizeTerminalLayoutSnapshot({ // Why: live DOM tree is authoritative when mounted, else the saved tree; synthesize only as a last resort, never re-guess. @@ -1875,21 +1930,38 @@ function buildMobileTerminalSurfaceTabs( ? paneTitles[Number(legacyPaneId)] : undefined const paneKey = isTerminalLeafId(leafId) ? makePaneKey(terminal.id, leafId) : null - const title = resolveRuntimeTerminalTitle( - terminal, - generatedTitlesEnabled, - paneTitle ?? terminal.title ?? 'Terminal' - ) - const agentStatusTitle = paneTitle ?? terminal.title ?? '' + const tabWideFallbackSafe = + isNativeChatTabWideFallbackSafe(parentLayout) && launchAgentLeafId === leafId + const title = + paneTitle?.trim() || + (tabWideFallbackSafe + ? resolveRuntimeTerminalTitle( + terminal, + generatedTitlesEnabled, + terminal.title ?? 'Terminal' + ) + : 'Terminal') + const agentStatusTitle = paneTitle ?? (tabWideFallbackSafe ? terminal.title : '') ?? '' const agentStatus = paneKey && !isClaudeManagementTitle(agentStatusTitle) ? inputs.agentStatusByPaneKey.get(paneKey) : undefined + const launchAgent = nativeChatLaunchAgentForLeaf({ + launchAgent: terminal.launchAgent, + launchAgentLeafId, + leafId, + leafIds + }) + const launchDraft = paneKey ? inputs.launchDraftByPaneKey.get(paneKey) : undefined + const publishedLaunchDraft = + launchDraft && launchDraft.agent === launchAgent && launchDraft.text.trim() + ? launchDraft + : null return { type: 'terminal' as const, id: mobileTerminalSurfaceId(terminal.id, leafId), title, - ...(terminal.quickCommandLabel?.trim() + ...(tabWideFallbackSafe && terminal.quickCommandLabel?.trim() ? { quickCommandLabel: terminal.quickCommandLabel.trim() } : {}), parentTabId: terminal.id, @@ -1897,7 +1969,7 @@ function buildMobileTerminalSurfaceTabs( ptyId, ...(terminalTheme ? { terminalTheme } : {}), ...(agentStatus ? { agentStatus } : {}), - ...(terminal.launchAgent ? { launchAgent: terminal.launchAgent } : {}), + ...(launchAgent ? { launchAgent } : {}), // Launch context that exists only as an unsent TUI-input draft; mobile // prefills its chat composer from it (desktop keeps its own seed store). ...(publishedLaunchDraft