- 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>
62 lines
1.3 KiB
TypeScript
62 lines
1.3 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import type { WorkspacePaneKind } from '@/api/types';
|
|
|
|
export interface ActivePaneSnapshot {
|
|
sessionId: string | null;
|
|
paneId: string | null;
|
|
kind: WorkspacePaneKind | null;
|
|
activeFile: string | null;
|
|
}
|
|
|
|
const EMPTY: ActivePaneSnapshot = {
|
|
sessionId: null,
|
|
paneId: null,
|
|
kind: null,
|
|
activeFile: null,
|
|
};
|
|
|
|
let current: ActivePaneSnapshot = EMPTY;
|
|
const subs = new Set<() => void>();
|
|
|
|
function notify(): void {
|
|
for (const sub of subs) {
|
|
try {
|
|
sub();
|
|
} catch {
|
|
// swallow — one bad listener shouldn't break others
|
|
}
|
|
}
|
|
}
|
|
|
|
function isSame(a: ActivePaneSnapshot, b: ActivePaneSnapshot): boolean {
|
|
return (
|
|
a.sessionId === b.sessionId &&
|
|
a.paneId === b.paneId &&
|
|
a.kind === b.kind &&
|
|
a.activeFile === b.activeFile
|
|
);
|
|
}
|
|
|
|
export function setActivePaneInfo(next: ActivePaneSnapshot): void {
|
|
if (isSame(current, next)) return;
|
|
current = next;
|
|
notify();
|
|
}
|
|
|
|
export function clearActivePane(): void {
|
|
setActivePaneInfo(EMPTY);
|
|
}
|
|
|
|
export function useActivePane(): ActivePaneSnapshot {
|
|
const [snap, setSnap] = useState<ActivePaneSnapshot>(current);
|
|
useEffect(() => {
|
|
const sub = () => setSnap(current);
|
|
subs.add(sub);
|
|
sub();
|
|
return () => {
|
|
subs.delete(sub);
|
|
};
|
|
}, []);
|
|
return snap;
|
|
}
|