import type { Sql } from '../db.js'; interface WorkspacePaneRow { id: string; kind: string; chatId?: string; chatIds?: string[]; activeChatIdx?: number; } // v2.6.5: sessions.workspace_panes widened from a bare WorkspacePane[] to a // WorkspaceState envelope { panes, tabNumbers, nextTabNumber, closedPaneStack }. // (See the union validator in apps/server routes/sessions.ts + normalizeWorkspaceState // in apps/server read_tab_by_number.ts — this is the coder-side mirror.) interface WorkspaceStateRow { panes: WorkspacePaneRow[]; tabNumbers: Record; nextTabNumber: number; closedPaneStack: unknown[]; } // MIGRATION: the stored value may be the legacy bare array OR the envelope. // Normalize to a full envelope so callers always read `.panes` as an array and // write the envelope back intact (preserving tabNumbers/nextTabNumber/closedPaneStack). export function normalizeWorkspaceState(v: unknown): WorkspaceStateRow { if (Array.isArray(v)) { return { panes: v as WorkspacePaneRow[], tabNumbers: {}, nextTabNumber: 1, closedPaneStack: [] }; } if (v && typeof v === 'object' && Array.isArray((v as { panes?: unknown }).panes)) { const env = v as Partial; return { panes: env.panes ?? [], tabNumbers: env.tabNumbers ?? {}, nextTabNumber: env.nextTabNumber ?? 1, closedPaneStack: env.closedPaneStack ?? [], }; } return { panes: [], tabNumbers: {}, nextTabNumber: 1, closedPaneStack: [] }; } function chatNameForKind(kind: string): string { if (kind === 'coder' || kind === 'agent') return 'BooCoder'; if (kind === 'terminal') return 'Terminal'; return 'Chat'; } function activeChatIdForPane(pane: WorkspacePaneRow): string | undefined { const chatIds = pane.chatIds ?? []; const idx = pane.activeChatIdx ?? 0; if (idx >= 0 && idx < chatIds.length) return chatIds[idx]; return pane.chatId; } /** Resolve the active chat for a workspace pane; auto-seed when empty. */ export async function resolveChatId( sql: Sql, sessionId: string, paneId: string, ): Promise { return sql.begin(async (tx) => { const sessionRows = await tx<{ workspace_panes: unknown }[]>` SELECT workspace_panes FROM sessions WHERE id = ${sessionId} FOR UPDATE `; if (sessionRows.length === 0) return null; const state = normalizeWorkspaceState(sessionRows[0]!.workspace_panes); const panes = state.panes; const paneIdx = panes.findIndex((p) => p.id === paneId); if (paneIdx < 0) return null; const pane = panes[paneIdx]!; const existingChatId = activeChatIdForPane(pane); if (existingChatId) { const chatRows = await tx<{ id: string }[]>` SELECT id FROM chats WHERE id = ${existingChatId} AND session_id = ${sessionId} AND status = 'open' `; if (chatRows.length > 0) return existingChatId; } const [newChat] = await tx<{ id: string }[]>` INSERT INTO chats (session_id, name, status) VALUES (${sessionId}, ${chatNameForKind(pane.kind)}, 'open') RETURNING id `; if (!newChat) return null; const nextChatIds = [...(pane.chatIds ?? []), newChat.id]; const nextActiveIdx = nextChatIds.length - 1; const nextPanes = panes.map((p, i) => i === paneIdx ? { ...p, chatIds: nextChatIds, activeChatIdx: nextActiveIdx, chatId: newChat.id, } : p, ); const nextState: WorkspaceStateRow = { ...state, panes: nextPanes }; await tx` UPDATE sessions SET workspace_panes = ${tx.json(nextState as never)}, updated_at = clock_timestamp() WHERE id = ${sessionId} `; return newChat.id; }); }