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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user