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:
2026-06-01 14:28:49 +00:00
parent b7fb254e5d
commit c56d169ef9
9 changed files with 469 additions and 223 deletions

View File

@@ -640,13 +640,23 @@ export function useWorkspacePanes(sessionId: string): UseWorkspacePanesResult {
const showLandingPage = useCallback((paneIdx: number) => {
setPanes((prev) => {
const pane = prev[paneIdx];
// Coder/terminal panes are not chat hosts — history button is chat-only.
if (!pane || pane.kind === 'coder' || pane.kind === 'terminal') return prev;
if (!pane) return prev;
const next = [...prev];
next[paneIdx] = { ...pane, kind: 'empty', chatId: undefined };
if (pane.kind === 'coder' || pane.kind === 'terminal') {
// Scoped panes don't host chat tabs. Leaving one for the session
// history closes it: drop the pane→chat binding, and for terminals
// kill the tmux session (terminals are ephemeral — closing = killing,
// mirroring removePane).
if (pane.kind === 'terminal') {
api.terminals.kill(sessionId, pane.id).catch(() => { /* non-fatal */ });
}
next[paneIdx] = { ...pane, kind: 'empty', chatId: undefined, chatIds: [], activeChatIdx: -1 };
} else {
next[paneIdx] = { ...pane, kind: 'empty', chatId: undefined };
}
return next;
});
}, []);
}, [sessionId]);
const addSplitPane = useCallback((kind: 'chat' | 'terminal' | 'coder'): string | null => {
// Generate the id outside the updater so we can return it deterministically.