batch4.1-5.1: dedup audit, archive 400 fix, sidebar Delete, landing-page enrichment, auto-name tool-call fix

- Fastify global empty-JSON-body parser fixes archive/unarchive/stop 400s
- Removed redundant local sessionEvents.emit at all 5+2 sites with server-side WS publishers; added dedupe guards in useSidebar/Workspace/Project handlers
- Sidebar session right-click adds Delete (destructive) with confirm Dialog
- Session.tsx navigates away on session_deleted/session_archived for the active session
- SessionLandingPage chat rows show message_count, effective_context_tokens, last_message_preview via LATERAL joins on GET /api/sessions/:id/chats
- Workspace.tsx pane drag-to-reorder using native HTML5 events (no new deps)
- CompactCard: Copy toast, Send-to-chat with target chat name, empty-state in share popover, Re-run button
- auto_name.ts: filter count gate and assistant-fetch by content <> '' so tool-call assistant rows don't trip the once-and-only-once guard
- Adds CLAUDE.md and apps/web/src/lib/format.ts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 23:36:01 +00:00
parent c35ec65fc4
commit 051f3b96ae
15 changed files with 451 additions and 90 deletions

View File

@@ -16,6 +16,13 @@ import {
ContextMenuSeparator,
ContextMenuTrigger,
} from '@/components/ui/context-menu';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from '@/components/ui/dialog';
import { AddProjectModal } from './AddProjectModal';
import { api } from '@/api/client';
import { sessionEvents } from '@/hooks/sessionEvents';
@@ -100,6 +107,7 @@ export function ProjectSidebar() {
const [expanded, setExpanded] = useState<Set<string>>(() => readExpanded());
const [renamingSession, setRenamingSession] = useState<string | null>(null);
const [renameValue, setRenameValue] = useState('');
const [deleteConfirm, setDeleteConfirm] = useState<{ id: string; name: string } | null>(null);
const navigate = useNavigate();
const location = useLocation();
const lastToastedError = useRef<string | null>(null);
@@ -135,7 +143,7 @@ export function ProjectSidebar() {
async function handleRemove(id: string) {
try {
await api.projects.remove(id);
sessionEvents.emit({ type: 'project_deleted', project_id: id });
// Server publishes project_deleted via WS; useUserEvents delivers it.
navigate('/');
} catch (err) {
toast.error(err instanceof Error ? err.message : 'failed to remove project');
@@ -145,13 +153,23 @@ export function ProjectSidebar() {
async function handleArchiveSession(sessionId: string, projectId: string) {
try {
await api.sessions.archive(sessionId);
sessionEvents.emit({ type: 'session_archived', session_id: sessionId, project_id: projectId });
// Server publishes session_archived via WS; useUserEvents delivers it.
if (activeSession === sessionId) navigate(`/project/${projectId}`);
} catch (err) {
toast.error(err instanceof Error ? err.message : 'failed to archive session');
}
}
async function handleDeleteSession(sessionId: string, projectId: string) {
try {
await api.sessions.remove(sessionId);
// Server publishes session_deleted via WS; useUserEvents delivers it.
if (activeSession === sessionId) navigate(`/project/${projectId}`);
} catch (err) {
toast.error(err instanceof Error ? err.message : 'failed to delete session');
}
}
async function handleRenameSession(sessionId: string) {
const trimmed = renameValue.trim();
setRenamingSession(null);
@@ -293,10 +311,16 @@ export function ProjectSidebar() {
}}>
Rename
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onSelect={() => void handleArchiveSession(s.id, p.id)}>
Archive
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem
variant="destructive"
onSelect={() => setDeleteConfirm({ id: s.id, name: s.name })}
>
Delete
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
))}
@@ -316,6 +340,36 @@ export function ProjectSidebar() {
</nav>
<AddProjectModal open={addOpen} onOpenChange={setAddOpen} onAdded={() => {}} />
<Dialog open={deleteConfirm !== null} onOpenChange={(open) => { if (!open) setDeleteConfirm(null); }}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete session?</DialogTitle>
<DialogDescription>
This will permanently delete {deleteConfirm ? `"${deleteConfirm.name}"` : 'this session'} and all its chats and messages. This cannot be undone.
</DialogDescription>
</DialogHeader>
<div className="flex gap-2 justify-end pt-2">
<Button variant="outline" onClick={() => setDeleteConfirm(null)}>
Cancel
</Button>
<Button
variant="destructive"
onClick={() => {
if (deleteConfirm) {
const projectId = projects.find((p) =>
p.recent_sessions.some((s) => s.id === deleteConfirm.id)
)?.id;
if (projectId) void handleDeleteSession(deleteConfirm.id, projectId);
}
setDeleteConfirm(null);
}}
>
Delete
</Button>
</div>
</DialogContent>
</Dialog>
</aside>
);
}