v1.4-fork-header: fork from message + delete message + header polish + housekeeping

- Fork: POST /api/chats/:id/fork creates a new chat in the same session,
  copies messages up to target (status=complete) with row-offset
  clock_timestamp() for stable ordering. Client emits open_chat_in_active_pane
  event; Workspace opens it in the active pane. No maybeAutoNameChat on forks.
- Delete: DELETE /api/chats/:id/messages/:message_id with 409 if the chat is
  currently streaming. Cascading-forward delete (created_at >= target).
  MessageBubble Trash button + confirm Dialog.
- Header: Projects -> Project -> Session breadcrumb, model badge pill,
  inline session rename, active file path via new useActivePane() hook.
  Server now publishes session_renamed on PATCH /api/sessions/:id;
  client-side dup emit removed from Session.tsx.
- Housekeeping: NOW() -> clock_timestamp() in schema.sql defaults, dead
  PaneTab.tsx and panes/PaneShell.tsx removed, session_panes backfill
  INSERT removed (CREATE TABLE retained), Tailnet trust comment near
  app.listen().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 04:12:01 +00:00
parent eabef7671e
commit 59fe6f0522
16 changed files with 426 additions and 206 deletions

View File

@@ -4,6 +4,7 @@ import { PanelRight, MessageSquare, Terminal, Bot } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/api/client';
import { sessionEvents } from '@/hooks/sessionEvents';
import { setActivePaneInfo, clearActivePane } from '@/hooks/useActivePane';
import type { Chat, WorkspacePane } from '@/api/types';
import { ChatPane } from '@/components/panes/ChatPane';
import { ChatTabBar } from '@/components/ChatTabBar';
@@ -87,6 +88,29 @@ export function Workspace({ sessionId, projectId }: Props) {
savePanes(sessionId, panes);
}, [sessionId, panes]);
useEffect(() => {
const active = panes[activePaneIdx];
if (!active) {
clearActivePane();
return;
}
setActivePaneInfo({
sessionId,
paneId: active.id,
kind: active.kind,
activeFile: null,
});
}, [sessionId, panes, activePaneIdx]);
useEffect(() => {
return () => {
clearActivePane();
};
}, []);
const activePaneIdxRef = useRef(activePaneIdx);
activePaneIdxRef.current = activePaneIdx;
useEffect(() => {
return sessionEvents.subscribe((event) => {
if (event.type === 'chat_created' && event.session_id === sessionId) {
@@ -118,6 +142,9 @@ export function Workspace({ sessionId, projectId }: Props) {
setChats((prev) => prev.filter((c) => c.id !== event.chat_id));
removeChatFromPanes(event.chat_id);
}
if (event.type === 'open_chat_in_active_pane') {
openChatInPane(activePaneIdxRef.current, event.chat_id);
}
});
}, [sessionId]);