Phase 21: Intent-first pipeline, visible ack, KV-stable system prompt
Pipeline changes (generation_task.py, intent.py): - Remove optimistic streaming queue/race (_drain_queue deleted) - Remove _generate_acknowledgment — ack now embedded in intent JSON - Round 0: await intent (~400ms), stream ack immediately as TTFT, then execute tool sequentially; chat-only streams directly - IntentResult.ack: one-sentence acknowledgment, intent max_tokens 200→350 - _parse_intent extracts and trims ack field KV cache stability (llm.py, generation_buffer.py, generation_task.py): - build_context: replace cached_note_ids with include_note_ids - Auto-found notes populate context_meta["auto_notes"] for sidebar but are NOT injected into system prompt (--- Related Notes --- removed) - Explicitly included notes injected as --- Included Notes --- - _conv_note_cache dict + get/set/clear functions removed from generation_buffer.py - All clear_conv_note_cache() calls removed Cold model retry (llm.py): - generate_completion (used by classify_intent) retries on HTTP 500: 3 attempts with 3s/6s delays — prevents intent failure during cold load API + frontend (routes/chat.py, stores/chat.ts, views/ChatView.vue, components/ChatPanel.vue): - exclude_note_ids → include_note_ids throughout - ChatView sidebar: Suggested (auto-found, + to include) + In Context (× to remove) - ChatPanel: remove exclude button from context pills; no IDs passed to sendMessage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,8 +29,6 @@ const noteSearchResults = ref<{ id: number; title: string }[]>([]);
|
||||
const noteSearchLoading = ref(false);
|
||||
let noteSearchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Exclude tracking
|
||||
const excludedNoteIds = ref<Set<number>>(new Set());
|
||||
|
||||
const streamingRendered = computed(() => {
|
||||
if (!store.streamingContent) return "";
|
||||
@@ -65,11 +63,7 @@ async function sendMessage() {
|
||||
messageInput.value = "";
|
||||
resetTextareaHeight();
|
||||
scrollToBottom();
|
||||
await store.sendMessage(
|
||||
content,
|
||||
contextNoteId,
|
||||
excludedNoteIds.value.size ? [...excludedNoteIds.value] : undefined,
|
||||
);
|
||||
await store.sendMessage(content, contextNoteId);
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
@@ -151,10 +145,6 @@ function removeAttachedNote() {
|
||||
function promoteAutoNote(note: { id: number; title: string }) {
|
||||
attachedNote.value = note;
|
||||
}
|
||||
|
||||
function excludeAutoNote(noteId: number) {
|
||||
excludedNoteIds.value = new Set([...excludedNoteIds.value, noteId]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -198,11 +188,6 @@ function excludeAutoNote(noteId: number) {
|
||||
@click="promoteAutoNote(note)"
|
||||
title="Attach for next message"
|
||||
>+</button>
|
||||
<button
|
||||
class="context-pill-btn exclude"
|
||||
@click="excludeAutoNote(note.id)"
|
||||
title="Exclude from auto-search"
|
||||
>×</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -463,9 +448,6 @@ function excludeAutoNote(noteId: number) {
|
||||
.context-pill-btn.promote:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.context-pill-btn.exclude:hover {
|
||||
color: var(--color-danger, #e74c3c);
|
||||
}
|
||||
|
||||
/* Input wrapper */
|
||||
.panel-input-wrapper {
|
||||
|
||||
@@ -119,7 +119,7 @@ export const useChatStore = defineStore("chat", () => {
|
||||
async function sendMessage(
|
||||
content: string,
|
||||
contextNoteId?: number | null,
|
||||
excludeNoteIds?: number[],
|
||||
includeNoteIds?: number[],
|
||||
think = false,
|
||||
contextNoteTitle?: string,
|
||||
) {
|
||||
@@ -167,7 +167,7 @@ export const useChatStore = defineStore("chat", () => {
|
||||
{
|
||||
content,
|
||||
context_note_id: contextNoteId,
|
||||
exclude_note_ids: excludeNoteIds?.length ? excludeNoteIds : undefined,
|
||||
include_note_ids: includeNoteIds?.length ? includeNoteIds : undefined,
|
||||
think,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -30,11 +30,12 @@ const noteSearchResults = ref<{ id: number; title: string }[]>([]);
|
||||
const noteSearchLoading = ref(false);
|
||||
let noteSearchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Exclude tracking (session-scoped per conversation)
|
||||
const excludedNoteIds = ref<Set<number>>(new Set());
|
||||
// Explicitly included notes (user clicked "+ include" in sidebar)
|
||||
const includedNoteIds = ref<Set<number>>(new Set());
|
||||
const includedNotes = ref<{ id: number; title: string }[]>([]);
|
||||
|
||||
// Persistent context notes — populated as assistant responds, cleared on conv change
|
||||
const contextNotes = ref<{ id: number; title: string }[]>([]);
|
||||
// Suggested notes — auto-found by search, not yet included
|
||||
const suggestedNotes = ref<{ id: number; title: string }[]>([]);
|
||||
|
||||
let prevConvId: number | null = null;
|
||||
|
||||
@@ -91,8 +92,9 @@ watch(convId, async (newId) => {
|
||||
}
|
||||
prevConvId = newId ?? null;
|
||||
|
||||
excludedNoteIds.value = new Set();
|
||||
contextNotes.value = [];
|
||||
includedNoteIds.value = new Set();
|
||||
includedNotes.value = [];
|
||||
suggestedNotes.value = [];
|
||||
attachedNote.value = null;
|
||||
store.lastContextMeta = null;
|
||||
if (newId) {
|
||||
@@ -117,12 +119,11 @@ watch(
|
||||
() => store.lastContextMeta?.auto_notes,
|
||||
(newNotes) => {
|
||||
if (!newNotes) return;
|
||||
const excluded = excludedNoteIds.value;
|
||||
const existing = new Set(contextNotes.value.map((n) => n.id));
|
||||
const alreadyIncluded = includedNoteIds.value;
|
||||
const alreadySuggested = new Set(suggestedNotes.value.map((n) => n.id));
|
||||
for (const note of newNotes) {
|
||||
if (!excluded.has(note.id) && !existing.has(note.id)) {
|
||||
contextNotes.value.push(note);
|
||||
existing.add(note.id);
|
||||
if (!alreadyIncluded.has(note.id) && !alreadySuggested.has(note.id)) {
|
||||
suggestedNotes.value.push(note);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,7 +181,7 @@ async function sendMessage() {
|
||||
await store.sendMessage(
|
||||
content,
|
||||
contextNoteId,
|
||||
excludedNoteIds.value.size ? [...excludedNoteIds.value] : undefined,
|
||||
includedNoteIds.value.size ? [...includedNoteIds.value] : undefined,
|
||||
true, // enable thinking in the full chat view
|
||||
contextNoteTitle,
|
||||
);
|
||||
@@ -282,9 +283,20 @@ function removeAttachedNote() {
|
||||
attachedNote.value = null;
|
||||
}
|
||||
|
||||
function excludeAutoNote(noteId: number) {
|
||||
excludedNoteIds.value = new Set([...excludedNoteIds.value, noteId]);
|
||||
contextNotes.value = contextNotes.value.filter((n) => n.id !== noteId);
|
||||
function includeNote(note: { id: number; title: string }) {
|
||||
if (includedNoteIds.value.has(note.id)) return;
|
||||
includedNoteIds.value = new Set([...includedNoteIds.value, note.id]);
|
||||
includedNotes.value.push(note);
|
||||
suggestedNotes.value = suggestedNotes.value.filter((n) => n.id !== note.id);
|
||||
}
|
||||
|
||||
function removeIncludedNote(noteId: number) {
|
||||
includedNoteIds.value = new Set([...includedNoteIds.value].filter((id) => id !== noteId));
|
||||
const removed = includedNotes.value.find((n) => n.id === noteId);
|
||||
includedNotes.value = includedNotes.value.filter((n) => n.id !== noteId);
|
||||
if (removed && !suggestedNotes.value.some((n) => n.id === noteId)) {
|
||||
suggestedNotes.value.push(removed);
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
@@ -420,26 +432,40 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Persistent context sidebar -->
|
||||
<aside v-if="contextNotes.length || attachedNote" class="context-sidebar">
|
||||
<div class="context-sidebar-header">In Context</div>
|
||||
<!-- Context sidebar -->
|
||||
<aside v-if="includedNotes.length || suggestedNotes.length || attachedNote" class="context-sidebar">
|
||||
<!-- IN CONTEXT section -->
|
||||
<template v-if="attachedNote || includedNotes.length">
|
||||
<div class="context-sidebar-header">In Context</div>
|
||||
|
||||
<!-- Manually attached note — pinned until sent -->
|
||||
<div v-if="attachedNote" class="context-note context-note-pinned">
|
||||
<span class="context-note-icon" title="Attached for this message">📌</span>
|
||||
<router-link :to="`/notes/${attachedNote.id}`" class="context-note-name">
|
||||
{{ attachedNote.title }}
|
||||
</router-link>
|
||||
<button class="context-note-remove" @click="removeAttachedNote" title="Remove">×</button>
|
||||
</div>
|
||||
<!-- Manually attached note — pinned until sent -->
|
||||
<div v-if="attachedNote" class="context-note context-note-pinned">
|
||||
<span class="context-note-icon" title="Attached for this message">📌</span>
|
||||
<router-link :to="`/notes/${attachedNote.id}`" class="context-note-name">
|
||||
{{ attachedNote.title }}
|
||||
</router-link>
|
||||
<button class="context-note-remove" @click="removeAttachedNote" title="Remove">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Auto-found notes — persist across turns -->
|
||||
<div v-for="note in contextNotes" :key="note.id" class="context-note">
|
||||
<router-link :to="`/notes/${note.id}`" class="context-note-name">
|
||||
{{ note.title }}
|
||||
</router-link>
|
||||
<button class="context-note-remove" @click="excludeAutoNote(note.id)" title="Remove from context">×</button>
|
||||
</div>
|
||||
<!-- Explicitly included notes -->
|
||||
<div v-for="note in includedNotes" :key="note.id" class="context-note context-note-included">
|
||||
<router-link :to="`/notes/${note.id}`" class="context-note-name">
|
||||
{{ note.title }}
|
||||
</router-link>
|
||||
<button class="context-note-remove" @click="removeIncludedNote(note.id)" title="Remove from context">×</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- SUGGESTED section -->
|
||||
<template v-if="suggestedNotes.length">
|
||||
<div class="context-sidebar-header" :class="{ 'context-sidebar-header-gap': attachedNote || includedNotes.length }">Suggested</div>
|
||||
<div v-for="note in suggestedNotes" :key="note.id" class="context-note context-note-suggested">
|
||||
<router-link :to="`/notes/${note.id}`" class="context-note-name">
|
||||
{{ note.title }}
|
||||
</router-link>
|
||||
<button class="context-note-add" @click="includeNote(note)" title="Add to context">+</button>
|
||||
</div>
|
||||
</template>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -729,6 +755,15 @@ onUnmounted(() => {
|
||||
.context-note-name:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.context-sidebar-header-gap {
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
.context-note-included {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
.context-note-suggested {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.context-note-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
@@ -742,6 +777,20 @@ onUnmounted(() => {
|
||||
.context-note-remove:hover {
|
||||
color: var(--color-danger, #e74c3c);
|
||||
}
|
||||
.context-note-add {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
padding: 0 0.1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.context-note-add:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
.messages-inner {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user