- 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>
122 lines
3.7 KiB
TypeScript
122 lines
3.7 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { api } from '@/api/client';
|
|
import type { AvailableProject } from '@/api/types';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onAdded: () => void;
|
|
}
|
|
|
|
export function AddProjectModal({ open, onOpenChange, onAdded }: Props) {
|
|
const [available, setAvailable] = useState<AvailableProject[] | null>(null);
|
|
const [customPath, setCustomPath] = useState('');
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
setError(null);
|
|
setCustomPath('');
|
|
setAvailable(null);
|
|
api.projects
|
|
.available()
|
|
.then(setAvailable)
|
|
.catch((err) =>
|
|
setError(err instanceof Error ? err.message : 'failed to list available projects')
|
|
);
|
|
}, [open]);
|
|
|
|
async function add(path: string) {
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
await api.projects.add({ path });
|
|
// Server publishes project_created via WS; let useUserEvents deliver it.
|
|
onAdded();
|
|
onOpenChange(false);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'failed to add');
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Add project</DialogTitle>
|
|
<DialogDescription>
|
|
Pick from detected repos in /opt or type a path.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-3">
|
|
<div className="rounded-md border max-h-64 overflow-y-auto">
|
|
{available === null && (
|
|
<div className="px-3 py-2 text-sm text-muted-foreground">Loading…</div>
|
|
)}
|
|
{available && available.length === 0 && (
|
|
<div className="px-3 py-2 text-sm text-muted-foreground">
|
|
No undiscovered repos in /opt.
|
|
</div>
|
|
)}
|
|
{available?.map((p) => (
|
|
<button
|
|
key={p.path}
|
|
disabled={busy}
|
|
onClick={() => void add(p.path)}
|
|
className="w-full text-left px-3 py-2 hover:bg-muted disabled:opacity-50 border-b last:border-b-0"
|
|
>
|
|
<div className="text-sm font-medium">{p.name}</div>
|
|
<div className="text-xs text-muted-foreground font-mono">{p.path}</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="custom-path">Custom path</Label>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
id="custom-path"
|
|
placeholder="/opt/some-repo"
|
|
value={customPath}
|
|
onChange={(e) => setCustomPath(e.target.value)}
|
|
disabled={busy}
|
|
/>
|
|
<Button
|
|
onClick={() => void add(customPath.trim())}
|
|
disabled={busy || !customPath.trim()}
|
|
>
|
|
Add
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="text-sm text-destructive">{error}</div>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={busy}>
|
|
Cancel
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|