Project-aware assist, link suggestions, project-scoped RAG, semantic search tool, SSE race fix

- Writing assistant: inject project notes as context (definition-tagged first), wikilink suggestions
- Link suggestions: server-side endpoint finds unlinked term occurrences, NoteEditorView sidebar panel
- Project-scoped RAG: ChatView ProjectSelector filters semantic+keyword search to selected project
- Semantic search tool: LLM search_notes upgraded to hybrid semantic (0.40 threshold) + keyword merge
- SSE race condition fix: drain remaining events after stream loop exits in chat.py and notes.py
- RAG_AUTO_SNIPPET raised 800→4000; sidebar include uses full note body; MAX_BODY_CHARS 8000→24000
- Enter-to-submit on writing assistant instruction textareas (note and task editors)
- DiffView: equal-line collapsing with 3-line context around changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 14:02:54 -05:00
parent 9036dfd931
commit 48f070f773
22 changed files with 767 additions and 113 deletions
+39
View File
@@ -9,6 +9,7 @@ import ChatMessage from "@/components/ChatMessage.vue";
import ToolCallCard from "@/components/ToolCallCard.vue";
import ToolConfirmCard from "@/components/ToolConfirmCard.vue";
import type { Note } from "@/types/note";
import ProjectSelector from "@/components/ProjectSelector.vue";
const route = useRoute();
const router = useRouter();
@@ -46,6 +47,9 @@ const autoInjectedNotes = ref<{ id: number; title: string; score?: number | null
// Note IDs excluded from auto-injection on next message
const excludedNoteIds = ref<number[]>([]);
// Project scope for RAG — when set, semantic & keyword search is restricted to this project
const ragProjectId = ref<number | null>(null);
let prevConvId: number | null = null;
const convId = computed(() => {
@@ -89,6 +93,8 @@ onMounted(async () => {
if (store.currentConversation?.id !== convId.value) {
await store.fetchConversation(convId.value);
}
// Reconnect if the last assistant message is still generating (e.g. after page refresh)
store.reconnectIfGenerating(convId.value);
} else {
await startNewConversation();
}
@@ -116,6 +122,8 @@ watch(convId, async (newId) => {
if (store.currentConversation?.id !== newId && !store.isStreamingConv(newId)) {
await store.fetchConversation(newId);
}
// Reconnect if a prior stream died while the backend was still generating
store.reconnectIfGenerating(newId);
scrollToBottom();
} else {
await startNewConversation();
@@ -220,6 +228,7 @@ async function sendMessage() {
true, // enable thinking in the full chat view
undefined,
excludedNoteIds.value.length ? excludedNoteIds.value : undefined,
ragProjectId.value,
);
sending.value = false;
@@ -404,6 +413,14 @@ onUnmounted(() => {
<button class="btn-new-conv" @click="newConversation">
+ New Chat
</button>
<!-- RAG project scope -->
<div class="rag-scope-section">
<label class="rag-scope-label">Scope notes to project</label>
<ProjectSelector v-model="ragProjectId" />
<p v-if="ragProjectId" class="rag-scope-hint">RAG search restricted to this project</p>
</div>
<div class="conv-list">
<div
v-for="conv in store.conversations"
@@ -713,6 +730,28 @@ onUnmounted(() => {
opacity: 0.9;
}
.rag-scope-section {
padding: 0.5rem 0.75rem 0.25rem;
border-bottom: 1px solid var(--color-border);
}
.rag-scope-label {
display: block;
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
margin-bottom: 0.3rem;
}
.rag-scope-hint {
margin: 0.3rem 0 0;
font-size: 0.7rem;
color: var(--color-primary);
font-style: italic;
}
.conv-list {
flex: 1;
overflow-y: auto;