From 32e4ee12f25c3b65b2bb0db3d2867d94ecaa5d6d Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 19 Feb 2026 14:40:34 -0500 Subject: [PATCH] Add persistent context sidebar, note title fix, and expanded tool suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context sidebar + note title: - ChatView: replace ephemeral context pills with a persistent right-panel sidebar; auto-found notes accumulate across turns; attached note shows with pin icon; × button excludes a note from future auto-search; hidden on mobile - routes/chat.py: batch-fetch note titles via get_notes_by_ids() and inject context_note_title into each message dict at conversation load time - notes.py: add get_notes_by_ids() batch fetch helper - types/chat.ts: add context_note_title field to Message interface - stores/chat.ts: sendMessage accepts optional 5th arg contextNoteTitle, included in optimistic user message - ChatMessage.vue: context badge shows note title instead of 'Note #N' Expanded LLM tool suite (all with intent router rules + ToolCallCard display): - delete_note / delete_task: permanent delete with user confirmation (write tool), type-safe (refuse to delete wrong type), clears note context cache on success - get_note: fetch full note body by query (search_notes returns only 200-char preview) - list_notes: browse notes by recency/keyword/tags with limit; notes only - update_note: add tags + tag_mode (replace/add/remove) parameters - search_notes: add optional type filter ("note" | "task") - search_todos (CalDAV): keyword-filter todos, companion to list_todos - caldav.py: add search_todos() built on top of list_todos() - generation_task.py: register new tools in _WRITE_TOOLS, _TOOL_LABELS, _TOOL_ACTIONS - llm.py: update available actions list and guidance in system prompt - intent.py: routing rules for all new tools Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/ChatMessage.vue | 2 +- frontend/src/components/ToolCallCard.vue | 68 ++++ frontend/src/stores/chat.ts | 2 + frontend/src/types/chat.ts | 1 + frontend/src/views/ChatView.vue | 321 +++++++++--------- src/fabledassistant/routes/chat.py | 10 +- src/fabledassistant/services/caldav.py | 15 + .../services/generation_task.py | 16 +- src/fabledassistant/services/intent.py | 6 + src/fabledassistant/services/llm.py | 12 +- src/fabledassistant/services/notes.py | 11 + src/fabledassistant/services/tools.py | 266 ++++++++++++++- summary.md | 35 +- 13 files changed, 578 insertions(+), 187 deletions(-) diff --git a/frontend/src/components/ChatMessage.vue b/frontend/src/components/ChatMessage.vue index 63de22d..a859d53 100644 --- a/frontend/src/components/ChatMessage.vue +++ b/frontend/src/components/ChatMessage.vue @@ -81,7 +81,7 @@ const timingParts = computed((): string[] => { class="context-badge" > - Note #{{ message.context_note_id }} + {{ message.context_note_title || `Note #${message.context_note_id}` }} diff --git a/frontend/src/components/ToolCallCard.vue b/frontend/src/components/ToolCallCard.vue index 742ae5e..298494a 100644 --- a/frontend/src/components/ToolCallCard.vue +++ b/frontend/src/components/ToolCallCard.vue @@ -20,6 +20,14 @@ const label = computed(() => { return "Created note"; case "note_updated": return "Updated note"; + case "note_deleted": + return "Deleted note"; + case "task_deleted": + return "Deleted task"; + case "note_content": + return "Note"; + case "notes_list": + return "Notes"; case "tasks": return "Tasks"; case "todo_updated": @@ -166,6 +174,32 @@ const searchResults = computed(() => { return results && results.length > 0 ? results : null; }); +const deletedNote = computed(() => { + const data = props.toolCall.result.data; + const type = props.toolCall.result.type; + if (!data || (type !== "note_deleted" && type !== "task_deleted")) return null; + return data as { id: number; title: string }; +}); + +const noteContent = computed(() => { + const data = props.toolCall.result.data; + if (!data || props.toolCall.result.type !== "note_content") return null; + return data as { id: number; title: string; body: string; tags: string[] }; +}); + +const noteList = computed(() => { + const data = props.toolCall.result.data; + if (!data || props.toolCall.result.type !== "notes_list") return null; + const results = data.results as Array<{ id: number; title: string; tags: string[]; preview: string }> | undefined; + return results ?? []; +}); + +const noteListCount = computed(() => { + const data = props.toolCall.result.data; + if (!data || props.toolCall.result.type !== "notes_list") return 0; + return (data.total as number) ?? 0; +}); + async function applyTag(tag: string) { if (!noteId.value || appliedTags.value.has(tag) || applyingTag.value) return; applyingTag.value = tag; @@ -189,6 +223,30 @@ async function applyTag(tag: string) { + + +