feat: shared PaneHeaderActions + chat-resolve WorkspaceState fix (v2.7.7)
In-flight workspace UX work. - Extract a shared PaneHeaderActions cluster (+/Split/Reopen/History/Close) used by ChatTabBar + the Workspace coder/terminal pane headers, replacing the divergent per-header copies; SessionLandingPage history + useWorkspacePanes tweaks. - Fix coder-side correctness bug: resolveChatId read sessions.workspace_panes as a bare WorkspacePane[] but v2.6.5 widened it to a WorkspaceState envelope, so it mis-read panes and clobbered tabNumbers/nextTabNumber/closedPaneStack on every pane-chat write. New normalizeWorkspaceState handles either shape and preserves the envelope (+ regression test). - CLAUDE.md doc-sync (coder vitest suite, deploy-by-surface, dual-remote push, in-flight-web-WIP staging, release-branch naming). Web tsc + coder build + coder tests green. Builds on v2.7.6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,36 @@ interface WorkspacePaneRow {
|
||||
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<string, number>;
|
||||
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<WorkspaceStateRow>;
|
||||
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';
|
||||
@@ -28,12 +58,13 @@ export async function resolveChatId(
|
||||
paneId: string,
|
||||
): Promise<string | null> {
|
||||
return sql.begin(async (tx) => {
|
||||
const sessionRows = await tx<{ workspace_panes: WorkspacePaneRow[] }[]>`
|
||||
const sessionRows = await tx<{ workspace_panes: unknown }[]>`
|
||||
SELECT workspace_panes FROM sessions WHERE id = ${sessionId} FOR UPDATE
|
||||
`;
|
||||
if (sessionRows.length === 0) return null;
|
||||
|
||||
const panes = sessionRows[0]!.workspace_panes ?? [];
|
||||
const state = normalizeWorkspaceState(sessionRows[0]!.workspace_panes);
|
||||
const panes = state.panes;
|
||||
const paneIdx = panes.findIndex((p) => p.id === paneId);
|
||||
if (paneIdx < 0) return null;
|
||||
|
||||
@@ -69,9 +100,10 @@ export async function resolveChatId(
|
||||
: p,
|
||||
);
|
||||
|
||||
const nextState: WorkspaceStateRow = { ...state, panes: nextPanes };
|
||||
await tx`
|
||||
UPDATE sessions
|
||||
SET workspace_panes = ${tx.json(nextPanes as never)},
|
||||
SET workspace_panes = ${tx.json(nextState as never)},
|
||||
updated_at = clock_timestamp()
|
||||
WHERE id = ${sessionId}
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user