Files
orca/src/main/runtime/agent-session-tab-layout.ts
T
Jinwoo Hong d3b17ca9f4 fix(runtime): save a windowless host's chat tabs in its layout (#28354)
* fix(runtime): save a windowless host's chat tabs in its layout

Claude-Session: p2-headless

* fix(runtime): remove a chat's saved tab on the index's hidden notice, which every close reaches

Claude-Session: p2-headless

* fix(runtime): a chat whose record is gone loses its saved tab at startup

Claude-Session: p2-headless

* test: run the windowless chat-layout host test in the Node runtime project

Claude-Session: p2-headless

* fix(runtime): remove a chat's saved tab from whichever partition still holds it

Claude-Session: p2-headless

* refactor(runtime): list saved chat tabs across partitions in the layout module

Claude-Session: p2-headless

* refactor(runtime): read saved chat tabs straight from the store's partitions

Claude-Session: p2-headless

* fix(chat-tabs): save a windowless chat tab under its own tab id

Review pass: the saved tab reuses the id status and attention address, the close
checks for a window before scanning partitions, and the host tests seed readable
records with unreadable, other-host and no-index cases.

Claude-Session: p2-headless

* test(chat-tabs): a window opening a windowless-saved chat reuses its tab

Claude-Session: p2-headless

* test(chat-tabs): check the saved tab with the lookup a window reuses

The window's existing-tab lookup moves to the shared projection so the
windowless-layout test exercises it without importing the renderer.

Claude-Session: p2-headless

* test(chat-tabs): assert the reused tab's identity directly

Drops the shared lookup added for the test; two functions already carry that name.

Claude-Session: p2-headless

* fix(chat-tabs): leave an unstamped chat tab in an SSH partition to that host

Claude-Session: p2-headless

* fix(chat-tabs): count an unstamped chat tab as this host's only in the local partition

A desktop mirrors a remote server's chats into that server's runtime partition
without a host stamp; the startup sweep must not delete them.

Claude-Session: p2-headless

* refactor(chat-tabs): one chat tab writer, one startup match, local partition only

Chats run only on local hosts, so their saved tabs live in the local partition:
the cross-partition listing and its writer go. Open and close share one layout
load, the startup fill and sweep become one function beside the index seed, and
the tab filter is the existing savedSessionId.

Claude-Session: p2-headless

* fix(chat-tabs): restore and sweep saved Claude chat tabs like any structured chat

The Claude skip predates Claude structured chats (#18519, #18560); an id the host
has no record of is already dropped by the readable restorer.

Claude-Session: p2-headless
2026-10-14 04:15:36 -04:00

67 lines
3.0 KiB
TypeScript

// A chat tab a host with no window shows, saved in its layout as a window saves its own, so the
// layout is a complete record of the host's tabs.
import type { AgentType } from '../../shared/agent-status-types'
import type { ExecutionHostId } from '../../shared/execution-host'
import { applyLayoutCommand } from '../../shared/workspace-layout/workspace-layout-commands'
import { loadWorkspaceLayout } from '../../shared/workspace-layout/workspace-layout-load'
import { nameBasedLoadContext } from '../../shared/workspace-layout/workspace-layout-minted-ids'
import type { LayoutCommand } from '../../shared/workspace-layout/workspace-layout-command-types'
import { saveWorkspaceLayout } from '../../shared/workspace-layout/workspace-layout-save'
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'
import { randomUUID } from 'node:crypto'
import { getStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry'
import { structuredAgentSessionTabId } from '../../shared/structured-agent-session-projection'
export type ChatTabChange =
| { open: { sessionId: string; agent: AgentType; tabId: string } }
| { close: string }
/**
* The partition with the worktree's chat tabs changed in one layout load: a chat opens in the active
* group unless a saved tab shows it, and a close drops the chat's tab. Null when nothing changed.
*/
export function withChatTabChanges(
hostId: ExecutionHostId,
session: WorkspaceSessionState,
worktreeId: string,
changes: readonly ChatTabChange[]
): WorkspaceSessionState | null {
const loaded = loadWorkspaceLayout(hostId, session, nameBasedLoadContext())
const groupId = loaded.desktopView.activeGroupIdByWorktree?.[worktreeId]
let layout = loaded.layout
for (const change of changes) {
const workspace = layout.workspaces[worktreeId]
const sessionId = 'close' in change ? change.close : change.open.sessionId
const tabId =
workspace?.tabs.find((tab) => tab.kind === 'agent-session' && tab.entityId === sessionId)
?.id ?? null
let command: LayoutCommand | null = null
if ('close' in change && tabId) {
command = { type: 'closeTabs', workspace: worktreeId, tabIds: [tabId], force: true }
} else if ('open' in change && !tabId) {
const open = { type: 'openAgentSessionTab', workspace: worktreeId, ...change.open } as const
command = groupId ? { ...open, groupId } : open
}
const applied = command
? applyLayoutCommand(layout, command, {
mintId: randomUUID,
mintLeafId: randomUUID,
now: Date.now
})
: null
if (applied?.ok) {
layout = applied.model
}
}
return layout === loaded.layout ? null : saveWorkspaceLayout({ ...loaded, layout })
}
/** The id the window, the index and status updates all address the chat's tab by. */
export function chatTabId(sessionId: string): string {
return (
getStructuredAgentSessionHost()?.getSessionTabId?.(sessionId) ??
structuredAgentSessionTabId(sessionId)
)
}