fix(mobile): scope terminal identity to leaves

This commit is contained in:
Brennan Benson
2026-08-13 01:12:01 -07:00
parent 501337454c
commit 322d79e6e2
6 changed files with 386 additions and 34 deletions
@@ -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,
@@ -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 })
@@ -272,6 +272,7 @@ type UseTerminalPaneLifecycleDeps = {
effectiveMacOptionAsAltRef: React.RefObject<EffectiveMacOptionAsAlt>
initialLayoutRef: React.RefObject<TerminalLayoutSnapshot>
managerRef: React.RefObject<PaneManager | null>
getTabWideAgentHintLeafId: () => string | null
containerRef: React.RefObject<HTMLDivElement | null>
expandedStyleSnapshotRef: React.MutableRefObject<
Map<HTMLElement, { display: string; flex: string }>
@@ -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()
@@ -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({
@@ -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<AgentStatusEntry> = {}): 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({
+99 -27
View File
@@ -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<string, Map<string, AppState['openFiles'][number]>>
@@ -108,6 +114,7 @@ type MountedTerminalSurfaceCapture = {
liveLayoutRoot: TerminalPaneLayoutNode | null
numericPaneIdByLeafId: ReadonlyMap<string, number | null>
ptyIdByNumericPaneId: ReadonlyMap<number, string | null>
tabWideAgentHintLeafId: string | null
}
/**
* One worktree's complete mobile-snapshot input set.
@@ -128,7 +135,7 @@ type MobileSessionWorktreeInputs = {
openFileIds: readonly string[]
terminalLayoutByTabId: ReadonlyMap<string, AppState['terminalLayoutsByTabId'][string]>
paneTitlesByTabId: ReadonlyMap<string, AppState['runtimePaneTitlesByTabId'][string]>
launchDraftByTabId: ReadonlyMap<
launchDraftByPaneKey: ReadonlyMap<
string,
NonNullable<AppState['nativeChatLaunchDraftByTabId']>[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<AppState['nativeChatLaunchDraftByTabId']>[string]
>
}): MobileSessionWorktreeInputs['launchDraftByPaneKey'] {
const draftsByPaneKey = new Map<
string,
NonNullable<AppState['nativeChatLaunchDraftByTabId']>[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<AppState['tabsByWorktree'][string]>[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