mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 11:07:42 +00:00
* Record chat tab opens, closes and host-listed chats in main (dark, layout-owner build only) A chat tab the window opens, closes, or adds and removes from a host listing of this machine's chats now reaches main as openAgentSessionTab, closeTabs or setTabProps under the window's ids, so the window's save no longer teaches main a chat tab. openAgentSessionTab takes the window's place, creation time and new split group, and the same open again changes nothing. Switch-off builds are unchanged. * test(layout): a paired server's chat tabs send nothing to main (dark) * fix(layout): keep a windowless host's chat-tab saves as they were; send synced chat opens in tab order (dark) Only a window's openAgentSessionTab (main marks it drawn on layout.apply) takes the new rules: a repeat changes nothing, a taken id is refused, and a workspace with no group takes the window's. A windowless host's own saves mint a fresh group and keep a chat whose id another tab holds, as before. A host listing's new chats are recorded in tab order, so each open's anchor already reached main. * refactor(layout): one rule set for a chat tab open; the windowless caller names only a group it holds (dark) Drops the per-sender drawn flag. The windowless save passes its remembered active group only when the workspace still holds it, so it mints a fresh group as before. Refusing a taken chat tab id is gated on the build switch: shipped builds still save the duplicate id.
71 lines
3.2 KiB
TypeScript
71 lines
3.2 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 activeGroupId = loaded.desktopView.activeGroupIdByWorktree?.[worktreeId]
|
|
let layout = loaded.layout
|
|
for (const change of changes) {
|
|
const workspace = layout.workspaces[worktreeId]
|
|
// A remembered group the workspace no longer holds names nothing: the open mints its own.
|
|
const groupId = workspace?.groups.some((group) => group.id === activeGroupId)
|
|
? activeGroupId
|
|
: undefined
|
|
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)
|
|
)
|
|
}
|