Commit Graph

137 Commits

Author SHA1 Message Date
bvandeusen e128406790 Add Forgejo Actions CI/CD, ruff linting, and test foundation
- .forgejo/workflows/ci.yml: fast checks on every push (TS typecheck,
  Python lint via ruff, pytest) — no Docker build, ~30s with cache
- .forgejo/workflows/build.yml: Docker build with registry-side layer
  caching + SSH deploy on main branch pushes only
- pyproject.toml: add ruff to dev deps, configure pytest and ruff rules
- tests/: conftest.py + first unit tests for _safe_filename (no DB needed)
- Makefile: add lint, fmt, typecheck, test, check targets for local use

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 18:46:54 -04:00
bvandeusen ef141f07f8 UI polish pass: word count, slash commands, task lists, graph peek, bulk delete, export
- WordCount component (toggle words/chars vs read time, persisted mode)
- TipTap: TaskList/TaskItem extensions, slash command menu (H1-H3, lists, code, quote, task)
- Markdown serializer: task list → `- [ ]` / `- [x]` roundtrip
- GraphView: slide-in peek panel for note/task nodes (body, tags, linked nodes); tag nodes still navigate
- ChatView: bulk-select conversations with two-click confirm delete + chat retention policy (default 90d)
- NoteEditorView: pill tab bar with animated edit/preview toggle + WordCount in toolbar
- WorkspaceNoteEditor: inline search with tag matching, inline new-note creation, WordCount
- WorkspaceTaskPanel: task body rendered in slide-over + Edit link
- Settings: data export (Markdown ZIP / JSON) via GET /api/export
- Accessibility: skip-to-content link, aria-labels on all icon-only buttons
- Fix: export route used non-existent fabledassistant.database — corrected to async_session()
- Fix: ToolCallRecord.status type now includes "running" (was causing TS build error)
- Dockerfile: upgrade npm to latest before install to suppress major-version notice
- npm audit fix: patched minimatch (ReDoS) and rollup (path traversal) — 0 vulnerabilities

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 23:15:01 -05:00
bvandeusen de7709039a Persist workspace panel open/closed state per project
Panel state is saved to localStorage as workspace_panels_{projectId} on
every toggle and restored on mount. Each project remembers its own layout
independently. Fallback guard ensures at least one panel is always open
after restore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 20:47:35 -05:00
bvandeusen fe63a732df Group conversations by date in chat sidebar
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>
2026-03-07 20:45:57 -05:00
bvandeusen e107da911a Collapse tool call cards by default; expand on click
- Completed (success) tool calls render collapsed: label + inline summary + ▶ chevron
- Clicking the header expands the detail section (result list, links, tags)
- Error cards start expanded so failures are immediately visible
- Running (in-progress) cards start expanded for live progress visibility
- Auto-collapses when a running call completes during streaming
- Suggested-tag pills remain accessible in both collapsed (via separate row) and expanded states

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 20:42:40 -05:00
bvandeusen 659c08def5 Keyboard navigation improvements across all editors
TiptapEditor.vue (central — applies to all three editors):
- Escape inside editor blurs it and emits 'escape' event to parent

TagInput.vue (central — applies everywhere tags are used):
- ArrowUp/ArrowDown navigate autocomplete suggestion list with visual highlight
- Enter confirms the keyboard-selected suggestion instead of typed text

NoteEditorView, TaskEditorView, WorkspaceNoteEditor (per-editor wiring):
- @escape on TiptapEditor returns focus to the title input (ref="titleRef")
- Ctrl+E from title input or editor main column jumps focus into editor body
- Ctrl+S on title input saves (was already on editor area; now consistent)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 18:46:08 -05:00
bvandeusen 0e9ab84afb Workspace note editor: toolbar, link suggestions, wikilink button, Ctrl+S
- Add MarkdownToolbar to the workspace note editor (was missing entirely)
- Add [[ button to MarkdownToolbar so wikilinks are discoverable in all editors;
  clicking inserts [[ which immediately triggers the WikilinkSuggestion dropdown
- Add link suggestions strip: polls /api/notes/link-suggestions 2.5s after edit,
  shows unlinked note-title mentions as clickable chips to wrap in [[...]], plus
  "All" button to apply everything at once — directly addresses the heading→wikilink
  workflow (note title appearing as heading text gets detected and offered for linking)
- Add Ctrl+S keyboard shortcut on title input and editor area to save
- Replace ✕ delete icon on note list items with trash can SVG (consistency with task panel)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 18:26:30 -05:00
bvandeusen 320e879788 Workspace polish: milestone selector, streaming fix, push fix, log quieting
- WorkspaceTaskPanel: add milestone <select> in task detail (PATCH /api/notes/:id),
  replace delete ✕ with trash can SVG icon
- WorkspaceView: scroll to bottom when streaming ends so final message is visible
  without a page refresh
- ToolCallCard: fix search_notes result count (was reading data.total; tool returns
  data.count), so results no longer show "0 found"
- push.py: switch from deprecated WebPusher().send(vapid_private_key=...) to
  webpush() function (pywebpush 2.x API compatibility)
- app.py: downgrade /api/health, /api/chat/status, and static asset requests from
  INFO to DEBUG in after_request logger to reduce log noise at default log level

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 12:58:09 -05:00
bvandeusen 74ebb8a87f Project Workspace view, abort button, session invalidation, workspace fixes
Workspace (/workspace/:projectId):
- Three-panel layout (tasks / chat / notes) with CSS grid collapse toggles
- WorkspaceTaskPanel: tasks grouped by milestone, collapsible groups, task
  detail slide-over with status cycling, TaskLogSection work log, and
  inline-confirm delete
- WorkspaceNoteEditor: list view (sorted by updated_at, inline tag pills,
  inline-confirm delete) with editor view (TipTap, TagInput, Suggest tags,
  60s autosave)
- Persistent workspace conversation stored in localStorage per project;
  reused on return visits
- Thinking enabled (think: true) with Reasoning block in streaming bubble
- workspace_project_id backend pipeline: chat.py → generation_task.py →
  llm.py; system prompt uses project title so agent passes project="Title"
  to tools (fixes create_note failing with numeric project string)
- SSE tool-call watcher bridges agent actions to panel updates
- Height fix: workspace-root uses height 100%; app-content switches to
  overflow hidden via :has() selector
- Entry point: "Open Workspace" button on ProjectView

Abort button:
- Stop button in ChatView header and WorkspaceView input bar
- Calls existing cancelGeneration() / POST .../generation/cancel

Session invalidation:
- POST /api/auth/invalidate-sessions bumps session_version, keeps current
  session alive; useful after SSO/OAuth password rotation
- Button in Settings → Active Sessions section

Other:
- Dashboard recent notes limit increased from 8 to 16
- Workspace chat abort replaces Send button while streaming

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 11:34:06 -05:00
bvandeusen a8bb687349 Force-directed graph view with tag nodes, project clustering, physics tuning
- New /graph route with D3 force simulation (GraphView.vue)
- Tag nodes as first-class graph nodes (string IDs "tag:name") — clicking
  navigates to /notes?tags=name; tags shown by default
- Invisible project hub nodes attract project members into clusters
- Physics panel with live sliders: repulsion, link distance, link strength,
  project pull, gravity (forceX/forceY, not forceCenter)
- Wikilink edges retain directed arrowheads; tag edges are thin, no arrowhead
- Graph nav link in AppHeader; `g` shortcut in App.vue
- Backend: build_note_graph() emits tag nodes + note→tag edges instead of
  O(n²) note→note shared-tag mesh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 20:50:43 -05:00
bvandeusen b33aa25fb7 Session invalidation on credential change
Add session_version to users table. Sessions now carry session_version
alongside user_id; @login_required rejects any session where the version
doesn't match the DB value.

- migration 0024: session_version INTEGER NOT NULL DEFAULT 1
- models/user.py: session_version Mapped[int] column
- auth.py: version mismatch → session.clear() + 401
- services/auth.py: change_password and reset_password_with_token both
  increment session_version, evicting all other live sessions
- routes/auth.py: login/register/oauth_callback store session_version;
  update_password rehydrates current session with new version so the
  user who changed their password stays logged in

Existing sessions will require one re-login after the migration runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 16:30:14 -05:00
bvandeusen 16ecd6bbeb DRY refactoring pass: shared mixins, route helpers, composables, CSS
Backend:
- models/base.py: TimestampMixin + CreatedAtMixin; applied to all 10+ models
- routes/utils.py: not_found() + parse_iso_date() helpers; used across all route files
- routes/milestones.py: _milestone_dict() helper replaces 5 repeated to_dict + progress blocks

Frontend:
- 5 new composables: useAutoSave, useEditorGuards, useTagSuggestions,
  useFloatingAssist, useListKeyboardNavigation
- ConfirmDialog.vue: reusable confirm modal replacing inline <teleport> blocks
- editor-shared.css: sidebar CSS consolidated from both editor views
- viewer-shared.css: context-bar/breadcrumb CSS consolidated from both viewer views
- NoteEditorView + TaskEditorView: ~120 lines each replaced with composable calls;
  duplicate scoped sidebar CSS removed
- NotesListView + TasksListView: inline keyboard-nav replaced with composable
- NoteViewerView + TaskViewerView: duplicate context-bar CSS removed

No behaviour changes. Net: -634 lines, +237 lines across 31 files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 15:26:34 -05:00
bvandeusen 48f070f773 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>
2026-03-06 14:02:54 -05:00
bvandeusen 9036dfd931 Note editor sidebar, full-doc assist, persistent drafts, version history
NoteEditorView: two-column sidebar layout (project/milestone/tags/assist
always visible), removed assist toggle button, InlineAssistPanel removed.

Writing assist: whole_doc mode rewrites entire document; DiffView.vue
replaces editor during review showing full-document diff. Scope dropdown
in sidebar switches between whole-document and section modes.

Persistent drafts: migration 0022 adds note_drafts (UNIQUE per note+user)
and note_versions (max 20, auto-pruned) tables. Draft saved after generation
completes, restored on editor mount, cleared on accept/reject. Version
snapshot created automatically whenever note body changes on save.

HistoryPanel.vue: version list + DiffView modal, restore button writes
body back to editor.

Config: OLLAMA_NUM_CTX default raised to 65536; assist num_predict now
tracks Config.OLLAMA_NUM_CTX instead of a hardcoded 4096.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 17:10:55 -05:00
bvandeusen b11a92f32d Fix writing assist: disable thinking mode, drop stuck-buffer 409
- stream_chat: add think=False parameter passed through to Ollama payload.
  qwen3 models have thinking enabled by default; without this flag the model
  spends minutes generating internal thinking tokens that stream_chat silently
  discards, leaving the frontend spinner blank until the SSE connection times
  out and the widget disappears.

- create_assist_buffer: orphan (overwrite) a still-running buffer instead of
  raising. The old asyncio task holds a direct reference and completes
  harmlessly against the stale buffer. New requests always win.

- assist_route: remove the 409 guard that blocked new requests when a previous
  generation got stuck. create_assist_buffer now handles this transparently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 13:28:59 -05:00
bvandeusen 9bf047ec45 Task work log, inline writing assistant, task editor sidebar layout
Backend:
- Migration 0021: task_logs table (FK → notes + users, CASCADE, indexed)
- models/task_log.py: SQLAlchemy model with to_dict()
- services/task_logs.py: CRUD with ownership checks, _UNSET sentinel for optional duration clear
- routes/task_logs.py: GET/POST/PATCH/DELETE /api/tasks/<id>/logs
- services/tools.py: log_work LLM tool (resolves task by title, creates log entry)
- services/generation_task.py: retry assist generation up to 3× on HTTP 500

Frontend:
- types/task.ts: TaskLog interface
- TaskLogSection.vue: chronological work log with date+time timestamps, duration badge, inline edit, autofocus
- InlineAssistPanel.vue: streaming preview + diff review rendered inline in editor column
- useAssist.ts: removed chatStore.chatReady gate; toast notifications for errors
- NoteEditorView.vue + TaskEditorView.vue: inline assist panel, aside restricted to idle state
- TaskEditorView.vue: two-column layout (editor+log left, metadata sidebar right), body defaults to Preview, sidebarOpen accordion for mobile
- editor-shared.css: .assist-active-hint style

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 13:05:26 -05:00
bvandeusen dc39a56293 Per-conv streaming state, immediate message_count commit, auto-new-chat on open
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>
2026-03-04 20:32:56 -05:00
bvandeusen 404d58d037 Update summary.md with keyboard navigation changes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 08:41:42 -05:00
bvandeusen 969ef0efa3 Add keyboard navigation: e/c/slash shortcuts, j/k list nav, focus visibility
- App.vue: e → edit on viewer pages; / → focus search (custom event); c → focus
  chat on home or navigate to /chat; update shortcuts panel with Lists + Chat sections
- theme.css: add a:focus-visible to focus-ring rules (router-link cards now show
  visible keyboard focus outline)
- SearchBar.vue: expose focus() via defineExpose for cross-component focus dispatch
- NotesListView / TasksListView: j/k vim-style navigation with kb-active-item
  highlight; listen for shortcut:focus-search; cleanup on unmount
- HomeView.vue: listen for shortcut:focus-chat, call chatInputRef.focus()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 08:39:49 -05:00
bvandeusen 264c3664ba Escape progressively unfocuses then navigates home
First Escape blurs the active input (reaching neutral state where
shortcuts work), second Escape navigates home. Allows full keyboard
navigation without touching the mouse to leave a focused field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 08:20:26 -05:00
bvandeusen 3836f08007 Add overflow-x: clip and min-width: 0 to all list view containers
Prevents content from escaping max-width boundaries across notes,
tasks, projects list, and project detail views.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 22:13:58 -05:00
bvandeusen c8b3d0f4e1 Fix dashboard overflow: clip at container boundary, min-width: 0 on grid children
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 22:06:22 -05:00
bvandeusen 7fed77318d Fix notes grid columns: dashboard 2-per-row, notes list 3-per-row
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 21:58:31 -05:00
bvandeusen 5b07a3349b Dashboard: equal 50/50 columns, 2 project cards/row, 3 note cards/row
- grid-template-columns: 3fr 2fr → 1fr 1fr (equal split)
- projects-strip-inline: 3 columns → 2 columns
- notes-mini-grid: auto-fill minmax(220px) → repeat(3, 1fr)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 21:46:07 -05:00
bvandeusen e3873d7483 Route task card clicks to edit view; repurpose button as View
Clicking a task card now goes directly to the edit view since that's
the primary use case. The hover button (compact) and button (full card)
are now labeled "View" and link to the detail/viewer view instead.
Sub-task links in TaskViewerView also route to edit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 19:51:58 -05:00
bvandeusen ab0002f342 Move Active Projects widget into tasks column, 3 cards across
Projects now appear at the top of the tasks column directly above
the task sections, laid out in a fixed 3-column grid. The previous
full-width widget below the main grid has been removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 19:48:32 -05:00
bvandeusen eac4c6d86a Implement keyboard shortcuts and enrich note/task viewer views
Keyboard shortcuts (App.vue):
- g+h/n/t/p/c: navigate to home/notes/tasks/projects/chat
- n / t: new note / new task (when not in an input field)
- Escape: go home (when not typing)
- Shortcuts panel updated to document all working shortcuts

NoteViewerView:
- Context breadcrumb: parent note link (↑), project badge, milestone badge
- Fetches project and parent note titles in parallel on load
- Back button label improved to "← Notes"

TaskViewerView:
- Context breadcrumb: parent task link (uses parent_title from API), project badge, milestone badge
- Sub-tasks section with inline progress bar and status dots (clickable to advance)
- Sub-tasks loaded from /api/notes?parent_id=X&type=task
- Note type extended with optional parent_title field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 19:32:19 -05:00
bvandeusen 3bc6443161 Fix notes column overflow on wide viewports
Add min-width: 0 to .col-tasks and .col-notes so CSS grid items
shrink to their track size. Change notes-mini-grid from repeat(2, 1fr)
to auto-fill minmax(220px, 1fr) so cards wrap instead of overflowing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 19:15:39 -05:00
bvandeusen 56d5268fad Overhaul dashboard, notes, and tasks views for density and usability
NoteCard: add compact prop — single-row title/tags/timestamp, no body preview
TaskCard: add compact prop — single-row with status dot, priority badge,
  title, optional project breadcrumb, due date; edit button appears on hover.
  Add projectTitle prop for cross-view context.

NotesListView: auto-fill CSS grid layout (2–4 columns); compact list toggle;
  view mode persisted to localStorage.

TasksListView: compact rows throughout; group-by-project toggle with
  collapsible sections, task counts, and "Open project" links; fetches
  project list for group labels and task breadcrumbs; limit raised to 100
  in grouped mode; view mode persisted to localStorage.

HomeView dashboard: task sections use compact rows (project breadcrumb shown);
  notes column uses 2-per-row mini-grid; new "Active Projects" widget below
  main grid shows milestone progress bars for up to 6 active projects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 18:57:57 -05:00
Bryan Van Deusen eed6aedc9e Add duplicate entry prevention to LLM tool pipeline
Three-layer guard on create_note and create_task:
1. Exact title match (get_note_by_title, case-insensitive) → hard block,
   redirects model to use update_note instead
2. Fuzzy title match (SequenceMatcher ≥82%, punct-stripped word search for
   candidates) → hard block; catches "Game Premise" vs "Game Premise Notes"
3. Semantic content similarity (semantic_search_notes threshold=0.87) → soft
   block with requires_confirmation:true; model asks user then retries with
   confirmed:true; graceful no-op if embedding model is unavailable

create_project and create_milestone now always require confirmed:true before
creation (structural entities — must be intentional). Both are also guarded
by exact + fuzzy title checks post-confirmation.

create_note and create_task gain an optional confirmed parameter (not
required by default; only used to bypass a content-similarity soft-block
after the user has been asked).

Helpers added to tools.py:
- _fuzzy_title_match(title, candidates, threshold=0.82)
- _PUNCT_RE for stripping punctuation before word-search candidate fetch
- difflib.SequenceMatcher and re imported at module level

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 14:19:58 -05:00
Bryan Van Deusen 87d3f3ea16 Fix project/milestone association and redesign AppHeader navigation
Backend:
- routes/tasks.py: POST + PUT were silently dropping project_id,
  milestone_id, parent_id from request body — root cause of association
  not saving from the task editor
- routes/tasks.py: GET /api/tasks/:id now includes parent_title in
  response (secondary lookup when parent_id is set)
- routes/notes.py: add PATCH /api/notes/:id for partial updates (used
  by sub-task status toggle; PUT already existed but PATCH was missing)
- routes/projects.py: GET /api/projects/:id/notes now fetches milestone
  IDs and passes them via milestone_ids so tasks assigned to a milestone
  (but lacking project_id) are included in the project view
- services/notes.py: create_note() auto-sets project_id from milestone
  when milestone_id is provided and project_id is omitted; list_notes()
  gains milestone_ids param — when combined with project_id uses OR
  condition (project_id=X OR milestone_id IN (...))

Frontend:
- NoteEditorView: add MilestoneSelector; milestone resets when project
  changes; all save paths (save/create/auto-save) include milestone_id
- stores/notes.ts: add milestone_id to createNote + updateNote types
- TaskEditorView: sub-tasks now inherit milestone_id from parent task
- AppHeader: three-zone layout — brand left, Notes/Projects/Tasks/Chat
  centered (absolute positioning), right rail with status/theme/? and
  gear dropdown containing Settings/Users/Logs; mobile dropdown unchanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 13:34:01 -05:00
bvandeusen 47c9cb39a2 Auto-generate VAPID keys at startup; no manual configuration needed
On first boot, ensure_vapid_keys() generates a fresh EC P-256 key pair
and saves it to /data/vapid_keys.json (inside the app_data volume) so
it survives container restarts. Subsequent boots load from that file.

VAPID_PRIVATE_KEY / VAPID_PUBLIC_KEY env vars still take precedence for
deployments that prefer to manage keys externally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 23:07:30 -05:00
bvandeusen 7d532b82a6 Fix startup warm-up to use user model settings instead of env var
Startup no longer auto-warms Config.OLLAMA_MODEL. Instead it queries
all distinct default_model values from user settings, cross-references
with Ollama's installed models, and warms only the intersection.

Models that users have selected but not yet installed are skipped with
an info log — they are never auto-pulled. The embedding model pull
behaviour is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:58:20 -05:00
bvandeusen 226ee5b22f Change default OLLAMA_MODEL from llama3.1 to qwen3:14b
Updates the startup warm-up and fallback default to match the
currently preferred model. Instances without a user setting will
also default to qwen3:14b.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:51:42 -05:00
bvandeusen 76c384ea53 Fix chat model always using conv.model instead of user setting
conv.model was stored at conversation creation time and short-circuited
the get_setting() call, so changing default_model in Settings had no
effect on existing conversations. Now the user's default_model setting
is always the source of truth for generation and summarization.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:44:14 -05:00
bvandeusen 4fd2c915b6 Improve project/task/sub-task workflows across backend and web
Backend:
- notes.py: add parent_id filter to list_notes()
- routes/notes.py: expose project_id, milestone_id, parent_id, type
  query params on GET /api/notes
- milestones.py: add find_milestone_by_title() (cross-project search)
- tools.py: list_notes project filter + updated_at; list_tasks milestone
  without project; tag_mode default add; fail-fast project resolution

Web frontend:
- TaskEditorView: add sub-tasks section (fetch, toggle, create inline);
  pre-fill projectId/milestoneId/parentId from URL query params on new task
- ProjectView: add + button in Todo kanban column header linking to
  /tasks/new?projectId=X&milestoneId=Y for quick task creation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:00:44 -05:00
bvandeusen 6580d16942 Improve tools reliability and add milestone management to ProjectView
tools.py:
- Default tag_mode changed from 'replace' to 'add' — existing tags are
  preserved unless the user explicitly requests replacement
- create_task, create_note, update_note, create_milestone: project and
  milestone lookup now uses get_by_title with fuzzy fallback; returns a
  clear error (with a hint to use list_projects/list_milestones) instead
  of silently creating duplicate projects or milestones via get_or_create
- create_task: project/milestone resolved before note creation so a bad
  project name fails fast without leaving an orphaned task behind
- update_note return value now includes item_type, tags, project_id, and
  an 'updated' summary string so the LLM can confirm what was modified
- research_topic stub now logs an error and returns failure instead of
  silently returning success when reached via wrong code path

ProjectView.vue:
- Milestone headers now show edit (✎) and delete (✕) action buttons on hover
- Edit triggers inline rename input; Enter/blur commits, Escape cancels
- Delete opens a confirmation modal clarifying tasks are unlinked not deleted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 21:33:13 -05:00
bvandeusen 012eb1d46b Add Projects, Milestones, RAG auto-inject, push notifications, PWA, tag normalisation
## Projects & Milestones (Phases A + G)
- New models: Project, Milestone (Project → Milestone → Task hierarchy)
- notes table: project_id + milestone_id FKs; parent_id FK constraint activated
- Migrations: 0017 (projects), 0018 (push_subscriptions), 0019 (events), 0020 (milestones)
- Services: projects.py, milestones.py (CRUD + progress tracking)
- Routes: /api/projects + /api/projects/<id>/milestones
- LLM tools: create/list/get/update project; create/list milestone; project + milestone + parent_task params on note/task tools
- Frontend: ProjectListView (stacked milestone bars), ProjectView (milestone-grouped kanban), ProjectSelector, MilestoneSelector, NoteEditorView + TaskEditorView updated

## RAG Auto-injection (Phase B)
- Notes ≥0.60 cosine similarity auto-injected into system prompt (max 3, 800 chars each)
- excluded_note_ids param; ChatView "Auto-included" sidebar section

## Summarisation improvements (Phase C)
- Threshold 20→30, keep-recent 6→8, max_tokens 200→400
- Two-pass summarisation for histories >50 messages

## Browser push notifications (Phase E)
- PushSubscription model + migration; pywebpush dependency
- /api/push routes; VAPID config; fire-and-forget on generation complete
- Frontend: sw.js, push store, Settings toggle

## PWA manifest (Phase F)
- manifest.json, Apple meta tags, service worker registration in main.ts

## Tag normalisation
- All tags lowercased + deduplicated at backend (create_note/update_note) and frontend (TagInput sanitize)
- Note/Task types gain project_id + milestone_id fields; store signatures updated

## CalDAV
- Radicale embedded server reverted; back to user-configured external CalDAV

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 20:52:21 -05:00
bvandeusen 3d7be5888e Remove intent model entirely; quick-capture uses primary model
The separate intent model (OLLAMA_INTENT_MODEL / qwen2.5:7b) is removed
from every part of the system. All classification now uses the primary model.

Changes:
- config.py: remove OLLAMA_INTENT_MODEL
- intent.py: remove classify_intent() and all supporting infrastructure
  (_SYSTEM_PROMPT_TEMPLATE, _RESEARCH_PREFIX, _PRIOR_WORK_REFS); file now
  only contains the quick-capture classifier
- quick_capture.py: classify_capture_intent() now called with Config.OLLAMA_MODEL
- generation_task.py: remove intent_model_setting DB lookup and get_setting import;
  history summarization and research pipeline use the primary model directly
- research.py: remove intent_model parameter from run_research_pipeline() and
  _generate_sub_queries(); both use the model param throughout
- routes/settings.py: remove intent_model from model-key validation and response
- app.py: remove intent model pre-warming at startup
- SettingsView.vue: remove Intent Model selector and related refs/state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 18:41:49 -05:00
bvandeusen 53e54ea761 Remove intent router from chat pipeline; raise OLLAMA_NUM_CTX to 16384
The intent classifier (Phase 21) is removed from the main chat generation
path. The main model now handles all tool routing natively via Ollama's
structured tool-calling API, eliminating misidentification issues caused
by the small intent model.

Changes:
- generation_task.py: remove classify_intent call, intent_task, _WRITE_TOOLS,
  _TOOL_ACTIONS, _INTENT_TRIGGER_WORDS, _should_skip_intent(), and the entire
  round-0 intent-first + write-tool confirmation block (~315 lines removed)
- research_topic tool calls are now handled inline in the streaming loop:
  runs run_research_pipeline, streams synthesis to buf, then breaks the round
  loop (research is still the full response, no model follow-up)
- config.py: raise OLLAMA_NUM_CTX default from 8192 to 16384

The quick-capture dedicated classifier (classify_capture_intent) is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 18:30:21 -05:00
bvandeusen 1106399883 Quick-capture notes: enrich content via main model
Add _process_note() — a second LLM pass using the main model that
transforms raw capture text into a well-formed note with a genuine
summary title and formatted body. Replaces the previous behaviour of
using the captured text verbatim as both title and body.

The processing prompt instructs the model to:
- Generate a 3-8 word summary title (never a verbatim copy)
- Format the body appropriately: bullet lists for items, clean prose
  for stream-of-thought, organised paragraphs for raw notes/fragments
- Preserve all original information without inventing new facts

The enrichment pass runs for both the intent-classified create_note
path and the fallback path. On LLM/parse failure it degrades safely
to the old verbatim behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 17:57:29 -05:00
bvandeusen 2c2874a1cc Fix streaming timeout and false-offline status indicator
- stream_chat and stream_chat_with_tools: remove read=300s per-chunk
  timeout, replace with read=None. In httpx streaming mode, the read
  timeout applies per-chunk — if Ollama pauses >300s while processing
  a large input context before the first token, it raises ReadTimeout,
  killing generation and leaving the assistant message as an empty stub.
  With read=None the stream is unbounded; connect=30s still guards the
  initial connection.

- chat_status_route: increase Ollama status check timeout 5s → 10s.
  When Ollama is busy processing a large prompt it can be slow to
  respond to /api/tags, causing the status indicator to briefly flip to
  "offline" even though generation is running normally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 17:50:42 -05:00
bvandeusen 6f5170854b Remove CalDAV todo tools; overhaul quick-capture
- Remove all 6 CalDAV todo tools (create/list/update/complete/delete/search_todos)
  from tools.py definitions, imports, execute_tool branches, intent routing rules,
  generation_task labels/actions, and llm.py system prompt hints. CalDAV event
  tools remain. Todo functions still exist in caldav.py but are no longer exposed.

- Quick-capture now uses a dedicated classify_capture_intent() with a focused
  _CAPTURE_SYSTEM_PROMPT that always routes to a tool (never null). Tool set
  expanded: create_note/task/event + update_note + research_topic.

- research_topic in quick-capture calls run_research_pipeline() directly (no SSE
  buffer). run_research_pipeline() now accepts buf=None; all buf.append_event
  calls are guarded so status events are skipped when no buffer is provided.

- Fallback note now always sets body=text (was empty for texts ≤80 chars).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 19:13:07 -05:00
bvandeusen 4c77eab84b Fix image serving: use cache_timeout not max_age (Quart API)
Quart's send_file uses cache_timeout= not max_age=. The TypeError on
every /api/images/<id> request caused a 500, which the browser rendered
as alt text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 14:38:54 -05:00
bvandeusen 1890f0c7f1 Fix image search: embed markdown instead of describing URLs
- tools.py: search_images result now includes 'embed' (ready-to-use
  markdown image syntax) and 'citation' fields instead of raw 'local_url';
  adds 'instructions' field so the model knows to render them verbatim
- llm.py: system prompt now explicitly tells the model to embed images
  using the 'embed' field rather than describing or listing URLs
- markdown.ts: explicitly allow src/alt in PURIFY_OPTS_FULL so img tags
  are never stripped by DOMPurify

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 13:19:59 -05:00
bvandeusen de1831f689 Update summary.md for session: image cache, suggested notes improvements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 13:09:01 -05:00
bvandeusen c962e86039 Broaden app volume mount from /data/images to /data
Single named volume app_data covering the entire /data directory so
future persistent storage (uploads, exports, etc.) doesn't need
additional volume entries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 12:58:33 -05:00
bvandeusen efe0a15b8c Add image search with local cache (Phase 23)
Images found via SearXNG are fetched server-side, stored on disk, and
served from /api/images/<id> — the user's browser never contacts the
original image host.  Original URLs are preserved for citation.

New files:
- alembic/versions/0016_add_image_cache.py  — image_cache table
- src/fabledassistant/models/image_cache.py — SQLAlchemy model
- src/fabledassistant/services/images.py    — fetch/store/serve logic
- src/fabledassistant/routes/images.py      — GET /api/images/<id>

Modified:
- config.py: IMAGE_CACHE_DIR (/data/images), IMAGE_MAX_BYTES (5 MB)
- research.py: _search_searxng_images() — SearXNG categories=images
- tools.py: _IMAGE_TOOLS def + search_images branch in execute_tool
- intent.py: search_images routing rule (explicit visual language only)
- app.py: register images_bp
- docker-compose.yml: image_cache named volume mounted at /data/images
- ToolCallCard.vue: "image_search" label

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 12:55:10 -05:00
bvandeusen 90afd3f131 Improve suggested notes: limit 8, threshold 0.45, show relevance scores
- 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>
2026-03-01 12:10:39 -05:00
bvandeusen 7728e38318 Update summary.md for session: quick-capture, bug fixes, note picker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 23:15:54 -05:00