fix: chat session reliability, cost, and hygiene (empty-response guard, tool truncation, delete, titles)
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

- Empty/refusal responses retried once, then surfaced as errors instead of silent blanks
- Chinese refusal boilerplate detected via denylist + non-ASCII heuristic
- Bulk-tool preference added to SOUL.md (list_lxcs over per-entity get_lxc_state)
- Tool results truncated to 4KB on persist; get_state_snapshot filters null-state entities
- Session delete (DELETE /sessions/{id} + confirm-on-second-click UI)
- Session titles auto-generated from assistant answer instead of raw user message
This commit is contained in:
2026-07-09 10:18:06 +02:00
parent 614c38ea7c
commit 49c37fe8b1
9 changed files with 335 additions and 60 deletions

View File

@@ -1,29 +1,59 @@
<script lang="ts">
import { sessions, loadSessions, loadSessionMessages, messages, currentSession } from '$lib/stores/chat'
import { sessions, loadSessions, loadSessionMessages, deleteSession, currentSession } from '$lib/stores/chat'
import { onMount } from 'svelte'
import Trash2Icon from '@lucide/svelte/icons/trash2'
onMount(() => {
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
setTimeout(() => { if (confirmDelete === id) confirmDelete = null }, 3000)
}
}
</script>
<div class="sessions-page">
<h2>Sessions</h2>
<div class="session-list">
{#each $sessions as session (session.id)}
<button
class="session-card"
<div
class="session-card group"
class:active={$currentSession === session.id}
onclick={() => {
loadSessionMessages(session.id)
location.hash = '#/chat'
}}
>
<div class="session-title">{session.title || 'Untitled'}</div>
<div class="session-meta">
{new Date(session.last_active_at).toLocaleString()}
</div>
</button>
<button
class="session-content"
onclick={() => {
loadSessionMessages(session.id)
location.hash = '#/chat'
}}
>
<div class="session-title">{session.title || 'Untitled'}</div>
<div class="session-meta">
{new Date(session.last_active_at).toLocaleString()}
</div>
</button>
<button
class="session-delete"
onclick={(e) => handleDelete(e, session.id)}
title={confirmDelete === session.id ? 'Click again to confirm delete' : 'Delete session'}
aria-label="Delete session"
>
{#if confirmDelete === session.id}
<span class="confirm-text">Delete?</span>
{:else}
<Trash2Icon class="icon" />
{/if}
</button>
</div>
{:else}
<div class="empty">No sessions yet. Start chatting with Nomos.</div>
{/each}
@@ -54,15 +84,12 @@
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 8px;
cursor: pointer;
color: var(--text);
font-family: inherit;
font-size: 0.875rem;
text-align: left;
transition: border-color 0.15s;
}
@@ -75,6 +102,58 @@
background: var(--bg-hover);
}
.session-content {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: none;
border: none;
cursor: pointer;
color: inherit;
font-family: inherit;
font-size: inherit;
text-align: left;
}
.session-delete {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
margin-right: 4px;
padding: 0;
background: none;
border: none;
border-radius: 6px;
cursor: pointer;
color: var(--text-muted);
opacity: 0;
transition: opacity 0.15s, background 0.15s;
}
.group:hover .session-delete {
opacity: 1;
}
.session-delete:hover {
background: var(--destructive-bg-subtle, rgba(239, 68, 68, 0.1));
color: var(--destructive, #ef4444);
}
.confirm-text {
font-size: 0.65rem;
font-weight: 700;
color: var(--destructive, #ef4444);
}
.icon {
width: 16px;
height: 16px;
}
.session-title {
font-weight: 500;
}