feat(web): delete a session from the sidebar
A session could be created from the sidebar but never removed, so the chat surface accumulated every experiment anyone ever named. Each row now carries a small delete affordance that only names the key: the confirmation is the shell's, and it says the turn history goes with the session before calling the new route. On confirm the chat moves to the first remaining session — the stream's watcher follows the key — so nothing keeps streaming a session that no longer exists. The row had to stop being a single button, since a nested button is invalid HTML: the select button and the delete button now sit in a container that owns the hover and active surface.
This commit is contained in:
1 parent
8db1e2aa11
commit
994df09c8f
5 files changed
+160
-22
No files matched your search
@@ -319,6 +319,54 @@ async function createSession(name: string): Promise<void> {
|
||||
void refreshSessions()
|
||||
}
|
||||
|
||||
// --- deleting a session ----------------------------------------------
|
||||
/** The key the confirmation names; null while no dialog is open. */
|
||||
const deletingKey = ref<string | null>(null)
|
||||
const deleting = ref(false)
|
||||
|
||||
function askDeleteSession(key: string): void {
|
||||
deletingKey.value = key
|
||||
}
|
||||
|
||||
function closeDeleteSession(): void {
|
||||
// A delete in flight is not cancellable: the claw is already writing.
|
||||
if (deleting.value) return
|
||||
deletingKey.value = null
|
||||
}
|
||||
|
||||
/** The session to show once `gone` is deleted: the first interactive one
|
||||
* the inventory still has, else the first of any kind, else none — an
|
||||
* empty key opens no stream (see `resubscribe`). */
|
||||
function nextActiveKey(gone: string): string {
|
||||
const remaining = sessions.value.filter((session) => session.key !== gone)
|
||||
const next = remaining.find((session) => isInteractive(session.key)) ?? remaining[0]
|
||||
return next?.key ?? ''
|
||||
}
|
||||
|
||||
async function confirmDeleteSession(): Promise<void> {
|
||||
const key = deletingKey.value
|
||||
if (key === null || deleting.value) return
|
||||
deleting.value = true
|
||||
try {
|
||||
await claw.deleteSession(key)
|
||||
apiError.value = null
|
||||
pushNotice(`session ${key} deleted`)
|
||||
// The inventory first: the replacement key is whatever is left.
|
||||
await refreshSessions()
|
||||
// Moving activeKey resubscribes the stream (the watcher below), so
|
||||
// the chat follows the delete instead of streaming a key that is
|
||||
// gone.
|
||||
if (activeKey.value === key) activeKey.value = nextActiveKey(key)
|
||||
} catch (err) {
|
||||
noteApiError(err)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
// The dialog closes either way: a refusal lands in the banner, where
|
||||
// the rest of the claw's errors do.
|
||||
deletingKey.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshProviders(): Promise<void> {
|
||||
try {
|
||||
providersState.value = await claw.providers()
|
||||
@@ -540,11 +588,15 @@ let stream: { close: () => void } | null = null
|
||||
|
||||
function resubscribe(): void {
|
||||
stream?.close()
|
||||
stream = null
|
||||
transcript.reset()
|
||||
olderAvailable.value = true
|
||||
attemptedAttachments.clear()
|
||||
notices.value = []
|
||||
streamStatus.value = 'connecting'
|
||||
// Deleting the last session clears the key, and an empty key is not a
|
||||
// stream: subscribing to one would retry a 404 forever.
|
||||
if (activeKey.value === '') return
|
||||
stream = claw.subscribeEvents(activeKey.value, {
|
||||
onEvent: (event, seq) => transcript.applyEvent(event, seq),
|
||||
// The record lost what this mirror holds (a wiped or reset
|
||||
@@ -672,6 +724,7 @@ onUnmounted(() => {
|
||||
@select="activeKey = $event"
|
||||
@refresh="refreshSessions"
|
||||
@create="createSession"
|
||||
@delete="askDeleteSession"
|
||||
/>
|
||||
<ChatPanel
|
||||
:items="transcriptItems"
|
||||
@@ -740,6 +793,26 @@ onUnmounted(() => {
|
||||
</template>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<AppModal
|
||||
:open="deletingKey !== null"
|
||||
title="Delete session"
|
||||
@close="closeDeleteSession"
|
||||
>
|
||||
<p>
|
||||
Delete <span class="mono">{{ deletingKey }}</span>? Its dispatch record and its
|
||||
turn history both go: the transcript cannot be recovered, and a session
|
||||
created under the same name starts empty.
|
||||
</p>
|
||||
<template #actions>
|
||||
<button class="ghost" :disabled="deleting" @click="closeDeleteSession">
|
||||
cancel
|
||||
</button>
|
||||
<button :disabled="deleting" @click="confirmDeleteSession">
|
||||
{{ deleting ? 'deleting…' : 'delete' }}
|
||||
</button>
|
||||
</template>
|
||||
</AppModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in new issue
Block a user