Add persistent context sidebar, note title fix, and expanded tool suite
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 <noreply@anthropic.com>
This commit is contained in:
@@ -81,7 +81,7 @@ const timingParts = computed((): string[] => {
|
||||
class="context-badge"
|
||||
>
|
||||
<router-link :to="`/notes/${message.context_note_id}`">
|
||||
Note #{{ message.context_note_id }}
|
||||
{{ message.context_note_title || `Note #${message.context_note_id}` }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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) {
|
||||
<template v-else-if="toolCall.status === 'error'">
|
||||
<span class="tool-error">{{ toolCall.result.error }}</span>
|
||||
</template>
|
||||
<template v-else-if="deletedNote">
|
||||
<span class="tool-deleted">{{ deletedNote.title || "Untitled" }}</span>
|
||||
</template>
|
||||
<template v-else-if="noteContent">
|
||||
<router-link :to="`/notes/${noteContent.id}`" class="tool-link">{{ noteContent.title || "Untitled" }}</router-link>
|
||||
<span v-if="noteContent.tags.length" class="tool-note-tags">
|
||||
<span v-for="tag in noteContent.tags" :key="tag" class="tool-note-tag">#{{ tag }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="noteList !== null">
|
||||
<span class="tool-search-info">{{ noteListCount }} found</span>
|
||||
<div v-if="noteList.length > 0" class="tool-search-results">
|
||||
<router-link
|
||||
v-for="n in noteList.slice(0, 8)"
|
||||
:key="n.id"
|
||||
:to="`/notes/${n.id}`"
|
||||
class="tool-search-item"
|
||||
>
|
||||
{{ n.title || "Untitled" }}
|
||||
</router-link>
|
||||
<span v-if="noteList.length > 8" class="tool-event-more">+{{ noteList.length - 8 }} more</span>
|
||||
</div>
|
||||
<span v-else class="tool-search-info">No notes found</span>
|
||||
</template>
|
||||
<template v-else-if="searchResults">
|
||||
<span class="tool-search-info">{{ (toolCall.result.data?.total as number) ?? 0 }} found</span>
|
||||
<div class="tool-search-results">
|
||||
@@ -380,6 +438,16 @@ async function applyTag(tag: string) {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.tool-note-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.2rem;
|
||||
width: 100%;
|
||||
}
|
||||
.tool-note-tag {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.tool-task-priority {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
|
||||
Reference in New Issue
Block a user