- Move voice status into settings store (voiceSttReady, voiceTtsReady),
checked once at login and refreshed after admin model reload
- ChatView, BriefingView, DashboardChatInput now use computed refs from
the store — mic buttons appear reactively without needing a page reload
- BriefingView: separate STT-only guard for mic PTT vs TTS-only guard for
listen mode / speak buttons
- Add ▶ Preview button to Voice & Speed section in Settings for single-
voice testing without enabling blend mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ChatView: add PTT mic button in input bar (hold to speak → transcribe → send)
- BriefingView: restyle input bar to match ChatView (floating pill, circle send)
- BriefingView: move listen/mic controls into input bar, remove from header
- BriefingView: consistent icon-button style for speaker/mic matching ChatView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the empty-conversation cleanup only fired when switching
between chats (convId watcher). Navigating to /notes, /tasks, etc.
left orphaned untitled empty conversations. onUnmounted now runs the
same check, deleting the current conversation if message_count === 0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Design direction: "Illuminated Transcript" — the AI response is the
primary artifact; user prompts are subordinate margin notes.
ChatMessage.vue:
- User bubble: ghosted background (rgba 3-4% opacity), muted text,
thin near-invisible border. De-emphasized, recessed.
- Assistant bubble: left indigo accent border (2px), elevated shadow
via --color-bubble-asst-shadow. Commands attention.
- Assistant role label: indigo (var(--color-primary)); user label muted.
- Removed old white-override content rules (no longer on dark bg).
- Thinking block: full border → left-only indigo accent bar (35% opacity),
removed inner top border.
ChatView.vue:
- Streaming bubble: same left accent + shadow as assistant bubbles.
- Streaming role label: indigo to match ChatMessage.
- Thinking block in streaming: same treatment as ChatMessage.
theme.css:
- Added --color-bubble-user-bg/border/text for light + dark.
- Added --color-bubble-asst-shadow for light + dark.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The role-user justify-content and message-bubble shape styles are scoped
to ChatMessage.vue and don't reach the inline queued bubbles. Add them
directly in ChatView and WorkspaceView: right-aligned row, 16px border
radius with 4px bottom-right corner, same padding and font as user bubbles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Queued message bubbles now match user bubble style (primary colour,
right-aligned, opacity 0.45) in both ChatView and WorkspaceView
- Queue persisted to localStorage (fa_conv_queue_<id>); survives page
refresh, restored on fetchConversation, cleared on delete
- ToolCallCard confirm/deny: buttons now inline in header row; "Create
anyway" calls POST /api/notes or /api/tasks directly (no LLM round-trip);
shows "✓ Created" / "Skipped" state; removed chatStore dependency
- POST /api/notes and POST /api/tasks now accept project (name string) and
resolve to project_id server-side, matching tools.py behaviour
- Remove semantic similarity duplicate check from create_note and
create_task — 0.87 threshold was too aggressive for topically-related
notes; title-based exact/fuzzy checks are sufficient
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dashboard chat: allow typing/sending during streaming (queued in store)
placeholder updates to "Type to queue…" during generation
- ChatView/WorkspaceView: replace queued chip with actual pending message
bubbles — light grey, dashed border, "Queued" badge above content; clear
button below the stack
- ToolCallCard: when tool returns requires_confirmation=True, show
"Similar content found" label (not "Error"), link to the similar note,
and "Create Anyway" / "Don't Create" buttons that auto-send the reply
- tools.py: fuzzy title match now returns requires_confirmation=True with
similar_note data instead of a hard error, so numbered-series notes
(Lore: X 0, Lore: X 1) can be created with one button click; semantic
match responses also include similar_note for the link
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Chat store: when sendMessage() is called while streaming, push to a
per-conversation queue and return. When the stream ends the next queued
message fires automatically via setTimeout(0) to keep the call stack
clean. clearQueue() and queuedCount exposed for UI consumption.
Queue is cleaned up on conversation delete.
ChatView/WorkspaceView: remove the streaming guard from the local
sendMessage() functions and the :disabled on the textarea so users can
type and submit freely while streaming. Input placeholder changes to
"Type to queue next message…" during streaming. A small "⏳ N queued ×"
chip appears below the streaming bubble showing queue depth with a
cancel button.
DashboardChatInput: disable input, attach button, and send button
during streaming. The dashboard creates a fresh conversation per
message so in-conversation queuing doesn't apply — locking the input
is the correct UX here. Placeholder updates to "Generating response…"
during streaming.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Conversations are bucketed into: Today, Yesterday, This week, This month,
and older entries sub-grouped by calendar month (e.g. "February 2026").
Group labels are sticky so they stay visible while scrolling. Older
buckets use month+year sub-grouping rather than a single "Older" catch-all.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
stores/chat.ts:
- Replace 7 global stream refs with convStreams: Record<number, ConvStreamState>
- Each conversation tracks its own stream state; 7 names become computed getters
- Add isStreamingConv(id) public helper
- Increment message_count +2 immediately after POST 202 (not at done) so the
cleanup watcher never deletes a mid-generation conversation
- SSE reconnect retries now run for background conversations regardless of
which conv is currently viewed
- error toasts only shown when the erroring conv is currently viewed
- deleteConversation cleans up orphaned stream state
ChatView.vue:
- Remove invalid write to store.lastContextMeta (now a read-only computed)
- Add !store.isStreamingConv(newId) guard to watch(convId) fetch so navigating
back to a generating conversation shows accumulated content without stale refetch
- Add startNewConversation() helper; opening /chat with no ID auto-creates a
new conversation and redirects to it (on mount and on navigation)
- Deleting a conversation also auto-creates a new one
Also committing previous-session changes (keyboard nav + task routing):
- App.vue: e shortcut only on note-view (not task-view, which is removed)
- TaskCard.vue: route directly to /tasks/:id (viewer), remove View buttons
- router/index.ts: remove task-view route; /tasks/:id now maps to TaskEditorView
- TasksListView.vue: Enter key pushes to /tasks/:id
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Raise similarity threshold 0.30 → 0.45: only genuinely relevant notes
shown; loosely-related notes no longer pad the sidebar
- Increase max suggested notes 3 → 8 (zero added compute — threshold is
the real gate; the embedding call is fixed regardless of limit)
- semantic_search_notes now returns list[tuple[float, Note]] instead of
list[Note] so scores propagate through context_meta to the frontend
- Keyword fallback notes carry score=null (no cosine similarity available)
- ChatView sidebar shows % badge on each suggested note:
green ≥75%, amber 60–74%, muted <60%
Hovering reveals the raw score in a tooltip
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the paperclip button created a one-shot attachment that was
only included for the single message it was sent with, then discarded.
Now selecting a note via the picker calls includeNote() directly, so it
appears in the "In Context" sidebar and stays for the entire conversation
— consistent with clicking "+" on a suggested note.
Removed attachedNote ref, removeAttachedNote(), the pinned-note sidebar
block, and the contextNoteId/contextNoteTitle sendMessage arguments that
are no longer needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ollama streams message.thinking tokens alongside message.content when
think=True — previously silently dropped. Now forwarded end-to-end.
Backend:
- llm.py: ChatChunk type gains "thinking" variant; stream_chat_with_tools
yields ChatChunk(type="thinking") for msg.thinking chunks before content
- generation_task.py: thinking chunks emit "thinking_chunk" SSE events
(not added to content_so_far — not persisted to DB)
Frontend:
- types/chat.ts: Message.thinking?: string (session-only, not from DB)
- stores/chat.ts: streamingThinking ref; thinking_chunk handler accumulates
chunks; on done, thinking carried into committed Message object then cleared
- ChatMessage.vue: collapsible <details class="thinking-block"> shown for
messages that have .thinking content (collapsed by default)
- ChatView.vue + ChatPanel.vue: live thinking block in streaming bubble —
open while only thinking is flowing, auto-collapses when content arrives;
typing indicator hidden while thinking is active
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
stream_chat_with_tools now accepts a think parameter. run_generation
forwards it to Ollama. The message POST route reads think from the
request body. ChatView passes think=true so qwen3 uses chain-of-thought
reasoning for full conversations; the dashboard widget and ChatPanel
omit it, staying fast. Dashboard button updated to "Think it through
in Chat →" to signal the deeper capability.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Before executing any write tool (create/update/delete), the backend now
pauses with an asyncio.Future and emits a tool_pending SSE event. The
frontend displays a ToolConfirmCard with Accept and Decline buttons.
Clicking Accept resolves the Future and proceeds; Decline records a
declined tool_call chip and falls through to regular streaming. Typing
single-word yes/no responses (e.g. "yes", "cancel") also works as
confirmation. 120s timeout auto-declines if no response.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug fix:
- ChatView.vue onMounted now skips fetchConversation when the conversation
is already loaded in the store (same guard that the convId watcher uses).
This prevents duplicate assistant messages when navigating from the
dashboard inline chat to /chat/:id after streaming completes.
Generation timing:
- logging.py: add log_generation() — persists per-generation timing
breakdown to app_logs (category=usage, action=generation) including
model, total_ms, intent_ms, ttft_ms, generation_ms, and per-tool timings.
Queryable via existing admin log viewer.
- generation_task.py: collect wall-clock timestamps at every pipeline stage:
intent classification, per-tool execution (both intent-routed and native),
time-to-first-token (measured from generation start to first content chunk),
LLM streaming round duration. Logs via log_generation() and includes timing
in the SSE 'done' event payload.
- types/chat.ts: add GenerationTiming interface; add optional timing field
to Message.
- chat.ts: capture timing from done event and attach to assistant message.
- ChatMessage.vue: show timing footer on assistant messages with breakdown:
"⏱ 4.2s total · first token 0.8s · analyzed 0.3s · created event 0.4s
· generated 3.5s". Visible this session; persisted to app_logs for
cross-session benchmarking.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Streaming status transparency:
- generation_task.py emits 'status' SSE events at each pipeline stage:
"Analyzing your request..." before intent classification, tool label
before each tool execution, "Generating/Composing response..." before
each LLM streaming round
- chat.ts adds streamingStatus ref; cleared on first chunk or done/error;
includes fast 5s poll loop after warmModel() until model shows as loaded
- ChatView.vue shows pulsing dot + italic status label above content area;
falls back to blinking cursor once content arrives
- HomeView.vue shows status label in dashboard panel instead of '...'
Model load state indicator:
- /api/chat/status now queries /api/tags and /api/ps in parallel to
distinguish installed-but-cold vs loaded-in-VRAM model states
- New model status values: 'not_found' | 'cold' | 'loaded' (was 'ready')
- chatReady true for both 'cold' and 'loaded' (cold models still work)
- AppHeader shows 5 states: gray pulse (checking), red (Ollama down),
orange (not installed), yellow pulse (cold), green (loaded)
- Inline short label ("Cold", "Ready", "Offline", etc.) visible without
hovering; detailed tooltip on hover
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CalDAV integration: per-user calendar config, create/list/search events
via caldav library, LLM tools for calendar operations from chat
- LLM-suggested tags: new tag_suggestions service prompts LLM with existing
tags and note content to suggest 3-5 relevant tags; exposed via API
endpoints (suggest-tags, append-tag); integrated into editor views
(suggest button + clickable pills) and chat tool calls (pills in
ToolCallCard with one-click apply)
- Settings/model UI refinements, generation task improvements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ollama tool/function calling integration allows the LLM to create tasks,
create notes, and search existing notes on behalf of the user during chat.
Multi-round tool loop (max 5 rounds) lets the model execute tools then
produce a natural language response. Tool results are persisted in a new
JSONB column on messages and rendered as compact cards with linked titles.
- Migration 0013: add tool_calls JSONB column to messages
- New services/tools.py: tool definitions + execute_tool dispatcher
- llm.py: ChatChunk dataclass, stream_chat_with_tools(), date in system prompt
- generation_task.py: multi-round tool call loop with SSE tool_call events
- Frontend: ToolCallRecord type, streamingToolCalls in store, ToolCallCard
component, rendering in ChatMessage and ChatView streaming bubble
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add GET /api/chat/ps and POST /api/chat/warm endpoints for hot model
visibility and pre-loading
- Extend PATCH /api/chat/conversations/:id to accept model in addition
to title
- Add ModelSelector component with hot/cold indicators from Ollama /api/ps
- Add DashboardChatInput component (model selector + note picker + textarea)
replacing the simple "New Chat" button on the dashboard
- Add model selector dropdown to ChatView header, persisted per-conversation
- Warm default model on dashboard mount via fire-and-forget background task
- Configure Ollama with OLLAMA_MAX_LOADED_MODELS=2 and OLLAMA_KEEP_ALIVE=30m
- Always-visible edit buttons on NoteCard/TaskCard (remove hover-only behavior)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add min-width to circular buttons in ChatView and ChatPanel to prevent
flex compression on narrow viewports.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace plain textarea with Tiptap (ProseMirror) WYSIWYG editor for notes
and tasks. Headings, bold, lists, and code blocks now render inline while
editing. Tags and wikilinks get visual highlighting via decoration plugins,
and autocomplete uses @tiptap/suggestion with heading disambiguation.
Layout: pin navbar with app-shell flex column (100dvh), sticky editor
toolbar above scrolling content, AI Assist panel fixed at bottom 1/3 with
full-height section selector and prompt. Increase max-width to 960px
across all views. Hide assist controls during streaming/review.
Other fixes: markdown paste handling, auto-syncing assist sections via
body watcher, trailing newline on AI accept, ChatView height fix.
Add README.md describing the app, usage guide, and technical overview.
Update summary.md with Phase 5.2 roadmap and current status.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Improve chat context: build_context() now returns metadata about auto-found
notes, emitted as an SSE event so the frontend can display context pills
showing which notes influenced the response. Users can promote notes for
deeper context (+) or exclude irrelevant ones (x). A note picker lets users
manually attach notes. Multi-word search uses per-term AND matching, and
auto-search iterates keywords individually for broader OR-style coverage.
Standardize styling: introduce CSS design tokens (--radius-sm/md/lg/pill,
--color-success/warning/overlay, --focus-ring) and migrate all components
to use them. Fix header alignment to full-width, add active nav link state,
replace hardcoded colors with CSS variables, and normalize button padding.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Settings infrastructure: key-value settings table, GET/PUT API, Pinia store
- Configurable assistant name (default "Fable") in settings and LLM system prompt
- Model catalog with 18 models in 3 categories (General Purpose, Coding,
Uncensored / Creative Writing) with download/select/remove functionality
- Move Ollama status indicator from chat views to global nav bar
- Chat bubble layout: user messages right-aligned, assistant left-aligned
- Floating dark input bar with auto-focus and circular send button
- Fix HTML entity rendering (' apostrophe issue in marked/DOMPurify pipeline)
- Fix new chat button navigation (fetchConversation before router.push)
- Recent chats section on home page with "New Chat" button
- Update summary.md with Phase 4.5 changes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds visibility into whether Ollama is reachable and the configured model
is ready before allowing chat. Prevents silent failures when the model is
still downloading or Ollama is unavailable.
- Backend: GET /api/chat/status endpoint checks Ollama /api/tags (5s timeout)
Returns ollama availability + model readiness + default model name
- Frontend: OllamaStatus type, chat store polling (30s interval)
- ChatView: status dot + label in header (green/yellow/red), disabled send
- ChatPanel: compact status indicator in panel header, disabled send
- summary.md: updated with new endpoint, store changes, and component descriptions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 4: Full chat system with SSE streaming, note-aware context, and
conversation persistence.
Backend:
- Migration 0005: conversations + messages tables with FKs and indexes
- Conversation/Message SQLAlchemy models with relationships
- LLM service: ensure_model (auto-pull on startup), stream_chat (NDJSON),
generate_completion, fetch_url_content (HTML stripping), build_context
(keyword extraction, related note search, URL content injection)
- Chat service: conversation CRUD, save_response_as_note,
summarize_conversation_as_note
- Chat routes blueprint: 9 endpoints including SSE streaming for messages,
save/summarize as note, Ollama model listing
- Auto-pull llama3.1 model on app startup (non-blocking)
Frontend:
- apiStreamPost: SSE client using fetch + ReadableStream
- Chat Pinia store with streaming state management
- ChatView: dedicated /chat page with conversation sidebar + message thread
- ChatPanel: slide-out panel with contextNoteId from current route
- ChatMessage: markdown-rendered message bubble with "Save as Note" action
- Updated AppHeader with Chat nav link + panel toggle button
- Updated App.vue to mount ChatPanel with route-derived context
- Added /chat and /chat/:id routes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>