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:
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { api } from '@/api/client';
|
||||
import type { Session as SessionType } from '@/api/types';
|
||||
import type { Project, Session as SessionType } from '@/api/types';
|
||||
import { sessionEvents } from '@/hooks/sessionEvents';
|
||||
import { useActivePane } from '@/hooks/useActivePane';
|
||||
import { Workspace } from '@/components/Workspace';
|
||||
import { ModelPicker } from '@/components/ModelPicker';
|
||||
|
||||
@@ -11,12 +12,15 @@ export function Session() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [session, setSession] = useState<SessionType | null>(null);
|
||||
const [project, setProject] = useState<Project | null>(null);
|
||||
const [name, setName] = useState('');
|
||||
const [editingName, setEditingName] = useState(false);
|
||||
const active = useActivePane();
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
setSession(null);
|
||||
setProject(null);
|
||||
let cancelled = false;
|
||||
api.sessions
|
||||
.get(id)
|
||||
@@ -24,16 +28,17 @@ export function Session() {
|
||||
if (cancelled) return;
|
||||
setSession(s);
|
||||
setName(s.name);
|
||||
// Emit unconditionally — the sidebar's session_loaded handler
|
||||
// updates activeSession; redundant when the session is already in
|
||||
// the recent_sessions cache but harmless. This lets the sidebar
|
||||
// highlight the parent project for deep-linked sessions that
|
||||
// aren't in the cache.
|
||||
sessionEvents.emit({
|
||||
type: 'session_loaded',
|
||||
session_id: id,
|
||||
project_id: s.project_id,
|
||||
});
|
||||
// Load project for breadcrumb. Listing is fine — small N, cached by client.
|
||||
api.projects.list().then((projects) => {
|
||||
if (cancelled) return;
|
||||
const p = projects.find((x) => x.id === s.project_id);
|
||||
if (p) setProject(p);
|
||||
}).catch(() => {});
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
@@ -68,26 +73,33 @@ export function Session() {
|
||||
}
|
||||
const updated = await api.sessions.update(id, { name: trimmed });
|
||||
setSession(updated);
|
||||
sessionEvents.emit({
|
||||
type: 'session_renamed',
|
||||
session_id: id,
|
||||
name: trimmed,
|
||||
});
|
||||
setEditingName(false);
|
||||
// Server publishes session_renamed via broker.publishUser; no local emit needed.
|
||||
}
|
||||
|
||||
// Workspace only sets activeFile for file-browser panes; checking it alone
|
||||
// suffices and is forward-compatible with future pane kinds.
|
||||
const showActiveFile = active.sessionId === id && !!active.activeFile;
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<header className="border-b px-4 py-2 flex items-center gap-2 shrink-0">
|
||||
{session && (
|
||||
<header className="border-b px-4 py-2 flex items-center gap-1.5 shrink-0 text-sm">
|
||||
<Link to="/" className="text-muted-foreground hover:text-foreground">
|
||||
Projects
|
||||
</Link>
|
||||
<ChevronRight className="size-3 text-muted-foreground/60" />
|
||||
{project ? (
|
||||
<Link
|
||||
to={`/project/${session.project_id}`}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
aria-label="Back to project"
|
||||
to={`/project/${project.id}`}
|
||||
className="text-muted-foreground hover:text-foreground truncate max-w-[200px]"
|
||||
title={project.name}
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
{project.name}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-muted-foreground/60">…</span>
|
||||
)}
|
||||
<ChevronRight className="size-3 text-muted-foreground/60" />
|
||||
{editingName ? (
|
||||
<input
|
||||
autoFocus
|
||||
@@ -106,21 +118,35 @@ export function Session() {
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm font-medium hover:underline"
|
||||
className="text-sm font-medium hover:underline truncate max-w-[280px]"
|
||||
onClick={() => setEditingName(true)}
|
||||
title={session?.name ?? ''}
|
||||
>
|
||||
{session?.name ?? '…'}
|
||||
</button>
|
||||
)}
|
||||
{showActiveFile && active.activeFile && (
|
||||
<>
|
||||
<span className="text-muted-foreground/40 mx-1">·</span>
|
||||
<span
|
||||
className="text-xs font-mono text-muted-foreground truncate max-w-[320px]"
|
||||
title={active.activeFile}
|
||||
>
|
||||
{active.activeFile}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<div className="ml-auto">
|
||||
{session && (
|
||||
<ModelPicker
|
||||
value={session.model}
|
||||
onChange={async (model) => {
|
||||
const updated = await api.sessions.update(session.id, { model });
|
||||
setSession(updated);
|
||||
}}
|
||||
/>
|
||||
<div className="inline-flex items-center rounded-full bg-muted/40 hover:bg-muted/70 px-1">
|
||||
<ModelPicker
|
||||
value={session.model}
|
||||
onChange={async (model) => {
|
||||
const updated = await api.sessions.update(session.id, { model });
|
||||
setSession(updated);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user