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
+34
View File
@@ -155,6 +155,7 @@ export const useChatStore = defineStore("chat", () => {
think = false,
contextNoteTitle?: string,
excludeNoteIds?: number[],
ragProjectId?: number | null,
) {
if (!currentConversation.value) return;
@@ -204,6 +205,7 @@ export const useChatStore = defineStore("chat", () => {
include_note_ids: includeNoteIds?.length ? includeNoteIds : undefined,
excluded_note_ids: excludeNoteIds?.length ? excludeNoteIds : undefined,
think,
rag_project_id: ragProjectId ?? undefined,
},
);
assistantMessageId = resp.assistant_message_id;
@@ -348,6 +350,37 @@ export const useChatStore = defineStore("chat", () => {
}
}
async function reconnectIfGenerating(convId: number): Promise<void> {
// Skip if a stream is already active for this conversation
if (isStreamingConv(convId)) return;
const conv = currentConversation.value;
if (!conv || conv.id !== convId) return;
// Find the last assistant message still in generating state
const lastMsg = [...conv.messages].reverse().find(
(m) => m.role === "assistant" && m.status === "generating"
);
if (!lastMsg) return;
// Remove the partial message — the done event will re-add the final version
conv.messages = conv.messages.filter((m) => m.id !== lastMsg.id);
const s = _getOrInitStream(convId);
s.streaming = true;
s.content = "";
s.thinking = "";
s.toolCalls = [];
s.status = "Reconnecting...";
s.pendingTool = null;
s.contextMeta = null;
// Reconnect from the beginning — the buffer replays all buffered events.
// If the buffer is gone (>60s stale), _streamGeneration will exhaust retries
// then fall back to fetchConversation to show the final saved content.
await _streamGeneration(convId, lastMsg.id);
}
async function confirmTool(confirmed: boolean) {
const convId = currentConversation.value?.id;
if (!convId) return;
@@ -461,6 +494,7 @@ export const useChatStore = defineStore("chat", () => {
deleteConversation,
updateTitle,
sendMessage,
reconnectIfGenerating,
confirmTool,
cancelGeneration,
saveMessageAsNote,