mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 08:03:37 +00:00
fix(mobile): scope terminal identity to leaves
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user