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

@@ -37,11 +37,17 @@ export function Project() {
if (event.type === 'session_archived' && event.project_id === id) {
setArchivedSessions((prev) => {
if (!prev) return prev;
if (prev.some((s) => s.id === event.session_id)) return prev;
const session = sessions?.find((s) => s.id === event.session_id);
if (!session) return prev;
return [{ ...session, status: 'archived' as const }, ...prev];
});
}
if (event.type === 'session_deleted' && event.project_id === id) {
setArchivedSessions((prev) =>
prev ? prev.filter((s) => s.id !== event.session_id) : prev
);
}
});
}, [id, sessions]);
@@ -50,7 +56,7 @@ export function Project() {
setCreating(true);
try {
const s = await create({});
sessionEvents.emit({ type: 'session_created', session: s, project_id: id });
// Server publishes session_created via WS; let useUserEvents deliver it.
navigate(`/session/${s.id}`);
} finally {
setCreating(false);
@@ -112,11 +118,7 @@ export function Project() {
onClick={async () => {
try {
await remove(s.id);
sessionEvents.emit({
type: 'session_deleted',
session_id: s.id,
project_id: id!,
});
// Server publishes session_deleted via WS.
} catch (err) {
toast.error(
err instanceof Error ? err.message : 'failed to delete session'

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { ChevronLeft } from 'lucide-react';
import { api } from '@/api/client';
import type { Session as SessionType } from '@/api/types';
@@ -9,6 +9,7 @@ import { ModelPicker } from '@/components/ModelPicker';
export function Session() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [session, setSession] = useState<SessionType | null>(null);
const [name, setName] = useState('');
const [editingName, setEditingName] = useState(false);
@@ -43,12 +44,19 @@ export function Session() {
useEffect(() => {
if (!id) return;
return sessionEvents.subscribe((event) => {
if (event.type !== 'session_renamed') return;
if (event.session_id !== id) return;
setSession((prev) => (prev ? { ...prev, name: event.name } : prev));
setName((prev) => (editingName ? prev : event.name));
if (event.type === 'session_renamed' && event.session_id === id) {
setSession((prev) => (prev ? { ...prev, name: event.name } : prev));
setName((prev) => (editingName ? prev : event.name));
return;
}
if (
(event.type === 'session_deleted' || event.type === 'session_archived') &&
event.session_id === id
) {
navigate(`/project/${event.project_id}`);
}
});
}, [id, editingName]);
}, [id, editingName, navigate]);
async function saveName() {
if (!id || !session) return;