batch4: chats-in-sessions, force-send, /compact, right-rail file browser
Session 1:N Chat data model with backfill. Workspace switches to client-side multi-tab pane management. Right-rail file browser with float-over viewer and click-drag line selection replaces FileBrowserPane. Adds /compact streaming summarizer (respects compact markers in context builder), force-send (cancels in-flight, persists partial as 'cancelled', awaits cancellation completion via deferred Promise + 5s timeout), message queue, stop generation, chat auto-rename, session archive/unarchive with Closed Sessions section on repo landing page. CHECK constraints on sessions.status, messages.role, messages.status with KEEP IN SYNC comments tying to MESSAGE_ROLES / MESSAGE_STATUSES const arrays. Deletes dead pane routes/hook and the api.panes.* client block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,13 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuTrigger,
|
||||
} from '@/components/ui/context-menu';
|
||||
import { AddProjectModal } from './AddProjectModal';
|
||||
import { api } from '@/api/client';
|
||||
import { sessionEvents } from '@/hooks/sessionEvents';
|
||||
@@ -91,6 +98,8 @@ export function ProjectSidebar() {
|
||||
useSidebar();
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
const [expanded, setExpanded] = useState<Set<string>>(() => readExpanded());
|
||||
const [renamingSession, setRenamingSession] = useState<string | null>(null);
|
||||
const [renameValue, setRenameValue] = useState('');
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const lastToastedError = useRef<string | null>(null);
|
||||
@@ -133,6 +142,28 @@ 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 });
|
||||
if (activeSession === sessionId) navigate(`/project/${projectId}`);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'failed to archive session');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRenameSession(sessionId: string) {
|
||||
const trimmed = renameValue.trim();
|
||||
setRenamingSession(null);
|
||||
if (!trimmed) return;
|
||||
try {
|
||||
await api.sessions.update(sessionId, { name: trimmed });
|
||||
sessionEvents.emit({ type: 'session_renamed', session_id: sessionId, name: trimmed });
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'failed to rename session');
|
||||
}
|
||||
}
|
||||
|
||||
const rowCls = (active: boolean) =>
|
||||
active ? 'bg-sidebar-accent text-sidebar-accent-foreground' : 'hover:bg-sidebar-accent/60';
|
||||
|
||||
@@ -225,17 +256,49 @@ export function ProjectSidebar() {
|
||||
{isExpanded && (
|
||||
<div className="ml-5 mt-0.5 space-y-0.5">
|
||||
{visible.map((s) => (
|
||||
<NavLink
|
||||
key={s.id}
|
||||
to={`/session/${s.id}`}
|
||||
className={`flex items-center gap-2 rounded-md px-2 py-1 text-sm min-w-0 ${rowCls(activeSession === s.id)}`}
|
||||
>
|
||||
<MessageSquare className="size-3.5 shrink-0 opacity-70" />
|
||||
<span className="truncate flex-1" title={s.name}>{s.name}</span>
|
||||
<span className="text-xs text-muted-foreground shrink-0 tabular-nums">
|
||||
{relTime(s.updated_at)}
|
||||
</span>
|
||||
</NavLink>
|
||||
<ContextMenu key={s.id}>
|
||||
<ContextMenuTrigger asChild>
|
||||
{renamingSession === s.id ? (
|
||||
<div className={`flex items-center gap-2 rounded-md px-2 py-1 text-sm min-w-0 ${rowCls(activeSession === s.id)}`}>
|
||||
<MessageSquare className="size-3.5 shrink-0 opacity-70" />
|
||||
<input
|
||||
autoFocus
|
||||
value={renameValue}
|
||||
onChange={(e) => setRenameValue(e.target.value)}
|
||||
onBlur={() => void handleRenameSession(s.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') void handleRenameSession(s.id);
|
||||
if (e.key === 'Escape') setRenamingSession(null);
|
||||
}}
|
||||
className="bg-transparent border-b border-border text-sm outline-none flex-1 min-w-0"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<NavLink
|
||||
to={`/session/${s.id}`}
|
||||
className={`flex items-center gap-2 rounded-md px-2 py-1 text-sm min-w-0 ${rowCls(activeSession === s.id)}`}
|
||||
>
|
||||
<MessageSquare className="size-3.5 shrink-0 opacity-70" />
|
||||
<span className="truncate flex-1" title={s.name}>{s.name}</span>
|
||||
<span className="text-xs text-muted-foreground shrink-0 tabular-nums">
|
||||
{relTime(s.updated_at)}
|
||||
</span>
|
||||
</NavLink>
|
||||
)}
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem onSelect={() => {
|
||||
setRenamingSession(s.id);
|
||||
setRenameValue(s.name);
|
||||
}}>
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onSelect={() => void handleArchiveSession(s.id, p.id)}>
|
||||
Archive
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
))}
|
||||
{p.total_sessions > MAX_VISIBLE_SESSIONS && (
|
||||
<NavLink
|
||||
|
||||
Reference in New Issue
Block a user