Phase 1 — crash recovery: SSE auto-reconnect + backoff, polling gate during disconnect, connection banner with retry button, empty-response retry 3x, non-terminal resume on empty response, persistent error cards. Phase 2/4 — visibility + continuation: custom ExecutionStatus renderer, approvals extracted on every tool_result (not just done), activity bar with status/goal, SessionDigest live polling, Continue button. Phase 3 — cleanup: complete_task auto-cancels orphaned approvals, deletes assent/destructive window keys, propose_plan marks pending steps as replaced, plan step seq-order enforcement. Phase 5 — knowledge loop: list_lxcs state filter (active/destroyed), SOUL.md unmissable writeback section, propose_plan validation nudge, complete_task writeback check, upsert_knowledge about array support, plan generation grouping in frontend, session approval count badge. Retire request_execution — all mutations now route through run. Updated SOUL.md, AGENTS.md, CLIENTS.md, skills, and agent system notes. Migration 020: plan step generation column, audit_log session_id index, nomos_plan_executions pending-approval index.
81 lines
3.1 KiB
Svelte
81 lines
3.1 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte'
|
|
import { sessions, currentSession, loadSessions, loadSessionMessages, newChat, deleteSession } from '$lib/stores/chat'
|
|
import { relativeTime } from '$lib/utils'
|
|
import { Button } from '$lib/components/ui/button'
|
|
import { ScrollArea } from '$lib/components/ui/scroll-area'
|
|
import PlusIcon from '@lucide/svelte/icons/plus'
|
|
import Trash2Icon from '@lucide/svelte/icons/trash-2'
|
|
|
|
onMount(() => {
|
|
loadSessions()
|
|
})
|
|
|
|
$effect(() => {
|
|
void $currentSession
|
|
loadSessions()
|
|
})
|
|
|
|
let confirmDelete = $state<string | null>(null)
|
|
|
|
function handleDelete(e: MouseEvent, id: string) {
|
|
e.stopPropagation()
|
|
if (confirmDelete === id) {
|
|
deleteSession(id)
|
|
confirmDelete = null
|
|
} else {
|
|
confirmDelete = id
|
|
// Hide confirmation after 3s
|
|
setTimeout(() => { if (confirmDelete === id) confirmDelete = null }, 3000)
|
|
}
|
|
}
|
|
|
|
function handleClick(sessionId: string) {
|
|
confirmDelete = null
|
|
loadSessionMessages(sessionId)
|
|
}
|
|
</script>
|
|
|
|
<aside class="flex h-full w-56 shrink-0 flex-col gap-2 overflow-y-auto border-r bg-card/50 p-2">
|
|
<Button variant="outline" size="sm" class="justify-start gap-2" onclick={() => newChat()}>
|
|
<PlusIcon class="size-3.5" />
|
|
New chat
|
|
</Button>
|
|
<ScrollArea class="min-h-0 flex-1">
|
|
<div class="flex flex-col gap-1 pr-2">
|
|
{#each $sessions as session (session.id)}
|
|
<div class="group relative">
|
|
<button
|
|
type="button"
|
|
class="flex w-full flex-col items-start gap-0.5 rounded-md border py-1.5 pl-2 pr-7 text-left text-xs transition-colors hover:bg-muted/60 {$currentSession === session.id ? 'border-primary bg-muted/50' : 'border-transparent'}"
|
|
onclick={() => handleClick(session.id)}
|
|
>
|
|
<span class="min-w-0 max-w-full truncate font-medium">{session.title || 'Untitled'}</span>
|
|
<span class="flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
{relativeTime(session.last_active_at)}
|
|
{#if session.pending_approvals}
|
|
<span class="rounded bg-warning/20 px-1 text-[10px] font-medium text-warning">{session.pending_approvals}</span>
|
|
{/if}
|
|
</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="absolute right-1 top-1.5 shrink-0 rounded p-0.5 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 hover:bg-destructive/20 hover:text-destructive"
|
|
onclick={(e) => handleDelete(e, session.id)}
|
|
aria-label={confirmDelete === session.id ? 'Click again to confirm delete' : 'Delete session'}
|
|
title={confirmDelete === session.id ? 'Click again to confirm delete' : 'Delete session'}
|
|
>
|
|
{#if confirmDelete === session.id}
|
|
<span class="text-[10px] font-semibold text-destructive">Sure?</span>
|
|
{:else}
|
|
<Trash2Icon class="size-3" />
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
{:else}
|
|
<p class="px-2 py-4 text-center text-xs text-muted-foreground">No sessions yet.</p>
|
|
{/each}
|
|
</div>
|
|
</ScrollArea>
|
|
</aside>
|