05a55dc566795158964529510eb9534f91e190f6
62
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7038e41ec7 |
feat(rules): preferences are writable, and their drift arrives (#3895)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / integration (push) Successful in 52s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m32s
CI & Build / Build & push image (push) Successful in 34s
Milestone 399 step 5. Steps 1-4 put preferences into the backend: a kind column, an inverted write path, a third register in the injected block, a delivery slot. Nothing the operator could touch. Rule 27 forbids leaving it there, and here it matters more than usual, because the UI is the only guard against the risk the milestone named up front — an agent misreads one session, rewrites a preference, and follows the rewritten version forever while the operator never sees the moment it changed. Four things ship. A preference is DISTINGUISHABLE. `kind` reaches the client (the server has always sent it in rule_brief) and a preference carries a chip. Force is the one thing a list of instructions must not leave the reader to infer, and a row that renders identically to a rule teaches the opposite of both facts about a preference: it does not bind, and a session may rewrite it. A preference is WRITABLE. The editor gains the kind as a first-class choice with the test beside it — what happens when someone does not do this — and says plainly, when preference is chosen, that sessions rewrite these without asking and every rewrite is kept. DRIFT ARRIVES. `GET /api/rules/drift` returns one row per rewritten preference carrying its latest rewrite: what it said, what it says now, and the record named by `arose_from_id` that taught the change. Both texts ride along so the list shows the diff without a call per row. The new pane sits beside the staleness sweep, because drift belongs to no one rulebook, and it answers a question the operator would not have thought to ask. REVERSION IS ONE ACTION, and this is the carve-out worth arguing with. Milestone 323 refused a one-click restore for rules — "a binding instruction should not be revertible in one click", because a silent revert erases the only record of why the rewrite happened. That reasoning turns on the rewrite being the operator's own decision. A preference's is not: the agent makes it mid-work without asking, so reverting is a veto over someone else's edit rather than an undo of your own, and a veto costing more than a shrug is not supervision. The route refuses anything but a preference (409), and nothing is erased: the restore goes through update_rule, so it snapshots too and the history GAINS the revert. An integration test pins that, because it is the whole basis for the exception. Tested against real Postgres — every claim is about which rows come back and in what order, which a stand-in session cannot judge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy |
||
|
|
4e4020c040 |
feat(rules): move a rule between global and project scope, keeping its id, history, areas and edges (#4063)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 54s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / Python tests (push) Successful in 1m41s
CI & Build / Build & push image (push) Successful in 33s
A rule's home is its reach: a rulebook topic makes it global, a project makes it that project's. There was no way to change one, so a project rule decided to be global could only be recreated and the original trashed — losing the id every record cites, its edit history, its area tags and its relations. - services.rulebooks.move_rule(rule_id, user_id, topic_id= | project_id=): exactly one destination (the model's CHECK), owned by the caller, not the rule's current home. A topic already holding a live rule with the same title is refused with a message naming that rule, instead of uq_rule_per_topic failing the commit. Someone else's rule reads as not found. - Deliberately NOT done, and said in the docstring: no version (a version is what a rule said, milestone 323 decision 4), no duplicate gate (nothing new enters the corpus), no re-embed (retrieval reads the home at query time). - Both doors: MCP move_rule, REST POST /api/rules/<id>/move (rule 33). - UI: RuleHomePicker, one component in the rule editor (a global rule) and a project's rules tab (a project rule), so the two cannot drift on what a destination is. - using-scribe names move_rule under "Where a new rule goes". Plugin 2026.09.15.1626. Milestone 414 step 3. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy |
||
|
|
0e10f6bb8a |
feat(rules)!: retire the always-on tier — every rule arrives by retrieval (#394)
CI & Build / Python lint (push) Failing after 3s
CI & Build / Plugin hooks (push) Failing after 12s
CI & Build / integration (push) Failing after 27s
CI & Build / TypeScript typecheck (push) Failing after 35s
CI & Build / Python tests (push) Failing after 37s
CI & Build / Build & push image (push) Skipped
Milestone 394, steps 5-8. Operator: "remove the always on rule functionality as the goal was to not have it at all since it didn't seem to work as expected." Unconditional preload had three failures the retrieval arms do not. It could not be MEASURED — a resident rule is in the context whether or not it mattered, so nothing distinguished "this governed the act" from "this was scenery", and it was the one surface structurally exempt from the scoreboard judging every other. It was SUMMARISED AWAY by compaction while the session went on believing it held the rules. And it CROWDED OUT the few rules that applied with the thirty that did not. WHAT GOES Schema (0100): rules.tier + ck_rules_tier, rule_versions.tier, rulebooks.always_on, and project_rulebook_exclusions — a table recording a project's opt-out of something that no longer binds it unasked. Tools: list_always_on_rules, exclude_always_on_rulebook, include_always_on_rulebook. Service: the same three plus rules_etag_for, _valid_tier and the whole etag family. The SessionStart preload and the write-path staleness arm go with them: nothing is resident, so nothing can have drifted since a session loaded it. THREE CALLS WORTH REVIEWING enter_project got NARROWER, not wider. Its filter was `always_on OR area-tagged`; dropping the tier arm leaves the deterministic half, so a project with no canonical-tagged Systems gets no bulk rules and reaches them by retrieval instead. Dropping the whole clause would have made that payload bigger than the preload this milestone deletes. Backups import tolerantly. A pre-394 archive carries tier, always_on and the retired inception choice; none is read, and the exclusion key is DROPPED rather than remapped, because restoring it would write data that validate_inception now rejects as unknown. The migration is irreversible in the way that matters and says so: downgrade recreates the columns at their defaults and cannot restore which rules were always-on. A value invented to fill a hole is not a measurement. THE INSTRUCTION SURFACES SAY THE HARDER THING Deleting "call list_always_on_rules()" is easy; replacing it is not, because the new model asks a session to trust something it cannot see. All three surfaces now say a session holds nothing, that rules arrive when work matches them, and — the half that got dangerous — that "no rule arrived" means "nothing matched", never "there is no rule". Under residency an empty session was rare and suspicious; it is now the ordinary state of most turns, so reading it as permission is wrong on nearly every turn rather than occasionally. That is #3720's defect at session scale. test_instruction_surfaces_agree is repointed rather than retired: its two halves collapsed into one instruction, and it gains a guard that every surface states what absence means. _INSTRUCTIONS is back at 1999/2000 — the inception clause paid for the longer HOW line. UI (rule 27, and the opportunity step 8 named) The tier selector is gone, and what replaces it is the point: `when_to_apply` is now the field that decides whether a rule is ever seen, so the editor marks it required, warns while it is empty, and both rule lists badge a trigger-less rule "never surfaces". A rule without one is not quiet, it is unreachable. TESTS Two files deleted outright — test_rules_etag.py and test_inception_rules.py tested subsystems that no longer exist. Elsewhere obsolete cases were removed and the rest repointed. One deserves naming: the wiring test asserted the act arms pass no `tier`, which had become an assertion that could not fail. It is repointed onto `kind`, which does still exist and where the same claim is live — a preference must reach a write exactly as a rule does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ |
||
|
|
1ec44071d2 |
feat(ui): a note's check is editable, dated and sweepable (#3167, milestone 317 step 4)
CI & Build / TypeScript typecheck (push) Successful in 39s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Python tests (push) Successful in 1m12s
CI & Build / integration (push) Successful in 1m41s
CI & Build / Build & push image (push) Successful in 1m1s
Rule 27: no UI, no ship. Three surfaces. THE EDITOR ASKS, but only where the answer can be saved: the fields appear for a plain note and not for a task or a snippet, matching the service gate from step 2 so the form never offers a write the save would reject. The labels are phrased as the QUESTION rather than the field name — "how would someone check this is still true?" and, underneath, "could this become false without anyone editing it?". "Verify with" gets filled in on every note; the question gets filled in on the few that can go stale. `expires_when` appears only once a check exists, and asks for a state rather than a date in the placeholder itself. THE NOTE SHOWS ITS AGE beside the field — "checked 2026-08-28" or "never checked", italic, and nothing at all when no check exists. No red/amber ramp, matching RuleSweepPane: a colour scale would restate the sweep's ordering and force an invented staleness threshold. "Never" is marked because it is categorically different from a date, not a worse one. THE SWEEP is a pane in the Knowledge view, not beside the rules sweep — operator's call, taken over a unified "everything due" surface and over a second pane under /rules. Notes stay where notes live. The cost, accepted knowingly: no single screen shows every unconfirmed record. It REPLACES the feed rather than filtering it, because a facet answers "show me this kind" and this answers "show me what nobody has confirmed" — a question the type chips cannot narrow without under-reporting. Two REST routes for it, since step 3 built only the service and the MCP door. Along the way: NoteEditorView spelled its write payload out at three call sites (save, create, auto-save), so every new field had to be added three times — which is how one of them ends up not carrying it. Now one `payload()` and one `snapshot()`. Known and filed, not fixed: NoteSweepPane copies ~12 scoped CSS rules from RuleSweepPane (#3207). The clean extraction needs prefixed names, because `.age`, `.row-title`, `.lede` and `.actions` all exist scoped in other components and an unscoped global would leak into them — which means editing the shipped rules sweep, blind, inside a step whose acceptance is the operator looking at a different surface. |
||
|
|
69d93898d9 |
fix(tasks): a task's kind is correctable — the Kind select stops lying (#3129)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 29s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Failing after 48s
CI & Build / Build & push image (push) Skipped
`kind` was accepted at CREATE on both doors and dropped at UPDATE on both: update_task had no such parameter, and the REST PATCH allow-list never read the field. So a task filed under the wrong kind could never be corrected. The frontend made it worse by looking like it worked. TaskEditorView binds a Kind select, marks the form dirty, and HAS ALWAYS SENT `kind` in the update payload — the store even types it. The route ignored it, returned 200, the view optimistically updated, the toast said "Task saved", and the old value came back on reload. Silent success, same class as #2709. Found by trying to re-file #3126 as a spike after deploying 0091. It could not be done; the task had to be recreated as #3128 and the original cancelled. One seam, not two doors. `minted_kind()` lives in services/notes.py because the REST route cannot import an MCP tool module and a second spelling of the list is how the doors would come to disagree. Both create and update route through it, so a bogus kind is now a readable error rather than a CheckViolationError surfacing as a 500. TaskKind joins TaskStatus and TaskPriority as a real enum, and update_note validates task_kind exactly as it already validated those two — the field had been reaching setattr through the hasattr guard with no validation at all, unnoticed only because no door ever offered it. The `-> plan` question #3129 raised is answered in code rather than left implicit: MINTABLE_KINDS is work/issue/spike, deliberately NARROWER than the column's CHECK. `plan` stays a valid stored value because historical plan-tasks carry it and must stay writable; it is simply not a value any door hands out, and the refusal names start_planning because a caller reaching for it wants a plan. The whitelist and the policy answer different questions and are not the same list. Every new test reads the value BACK. One that only asserted the call succeeded would have passed against the broken code — the route returned 200 while dropping the field, which is how this survived long enough to be found by hand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c83bedf3be |
feat(rules): the check is editable, visible, and sweepable in the UI (#3098, milestone 312 step 4)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 29s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 1m13s
CI & Build / Build & push image (push) Successful in 37s
Rule 27 — the milestone was backend-only until this. Four surfaces:
RULE EDITOR — verify_with and expires_when under a legend that asks the
actual question ("Can this rule go stale?") and says empty is the normal
answer, because most rules are decisions and a form that implies a missing
field would get them filled in out of tidiness. When the SAVED rule carries
a check, the stamp shows with Still true / No longer true beside it. The
stamp reads the stored value, not the draft: an unsaved edit to the textarea
has not been run against anything.
SWEEP PANE — its own surface, not a filter on the rule list. That list can
only ever show one topic of one rulebook, and a rule that has gone false
belongs to no one rulebook; filtering it would under-report, which is the
failure this whole surface exists to catch. Reached from the rulebook list,
below the rulebooks, because that is where you go to look at rules.
RULE ROWS — a chip only on rules carrying a check, so its presence is the
signal. PROJECT RULES TAB — the check shows beside `why` when a rule has
one, read-only: that tab is the project's view of what binds it.
NO AGE-GRADED COLOUR anywhere, deliberately. The sweep is already ordered by
urgency, so a red/amber ramp would restate the ordering AND require an
invented "stale after N days" threshold — a magic number nobody could defend
and the first thing to go out of date. --fs-overdue is error red and reserved
for a broken promise like a missed due date; a verification age is not one,
and colouring it that way makes a rule someone just wrote look broken. Only
"never" is marked, because it is categorically different from a date rather
than a worse one — and it is marked by weight, not hue.
An empty sweep says "Nothing to check", not nothing: good news must not read
as a broken page.
Two chips (tier, then verification) turned out byte-identical, so .rule-chip
moves to rules-shared.css and snippet #2906 is updated to match rather than
left describing a file that has moved on. Its header comment counted the
panes it served; that count went stale the moment a fourth arrived, so it no
longer counts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
682bea5257 |
fix(rules): the third row literal — fetchRules builds a list row too (#3029)
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 11s
CI & Build / integration (push) Successful in 20s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Python tests (push) Successful in 1m3s
CI & Build / Build & push image (push) Successful in 40s
vue-tsc caught what I missed: there were THREE places hand-building a rule list row, not two. fetchRules mapped full rules down to the same four fields in a spot far from the other two, so consolidating the pair I could see left this one behind — which is precisely how the server side ended up with three divergent trim dicts in the first place. All three now go through toHeader. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8b60d552d2 |
feat(rules): the rule editor asks when it applies, and the list shows its age (#3029, milestone 307 step 3, UI)
CI & Build / Python lint (push) Successful in 6s
CI & Build / integration (push) Successful in 38s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Failing after 27s
CI & Build / Python tests (push) Successful in 1m27s
CI & Build / Build & push image (push) Skipped
Rule 27 — the schema and both doors shipped with no human surface, so step 3 was not shippable until this. RuleEditorSlideOver gains the trigger, the tier, the areas, and a read-only view of the rule's edges. The tier is a radio pair carrying the test itself rather than a bare toggle: can you name the trigger WITHOUT naming a system, an artifact type or a moment? If the honest answer is "whenever you are working", it is always on. It also says why conditional is not a demotion — it costs nothing when irrelevant, which is what lets a rule be as long as it needs to be. The relations block states the rule the whole milestone turns on: rules that FAIL TOGETHER are linked, never merged. RuleListPane shows the trigger and the LAST-CHANGED DATE on every row, and marks conditional only — always_on is the default and badging every row would say nothing. The date is the cheap triage the FabledCurator case wanted: a rule whose age predates the capability it duplicates is visible at a glance instead of needing a get_rule to find out. ProjectRulesTab's inline create form gains the same two fields, because a project rule bloats exactly the way a family one does — FabledCurator has 23 of them. Two type fixes the new shapes forced, both worth keeping: - toHeader() in the store: a list row is the server's rule_brief, so patching a list locally has to mirror every field it carries or the two disagree. There were two hand-built four-field literals doing that job. - ApplicableRules.rules / .project_rules are now described AS RuleHeader rather than as two more hand-written shapes — the same builder produces them, so the same type should describe them. groupByRulebookAndTopic skips a null-topic rule rather than widening TopicGroup to accept one: a rule carries topic_id XOR project_id, so a null topic in that list means something is wrong upstream, and a widened type would hide it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c58529718b |
feat(systems): the catalog reaches the moment a name is minted, and gets a face (#3028, milestone 307 step 2)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 48s
CI & Build / integration (push) Successful in 38s
CI & Build / Python tests (push) Failing after 57s
CI & Build / Build & push image (push) Skipped
Step 1 found the reason the standard names never held, and it is sharper than "prose doesn't fire": the list WAS real and it WAS seeded — but only on the inception path, for a project with zero Systems. Ad-hoc create_system never consulted it, which is how Forge minted "CI and Release" and Portal minted "CI & release" after the constant already existed. This wires the vocabulary to the moment that mints a name. - services/systems.assess_system_name: the local duplicate gate AND the catalog lookup, in ONE service function both doors call. The gate lived only in the MCP tool, which is exactly how the web UI shipped without a check the agent surface enforced (#2482). REST now answers 409 with the System that already covers the area. - An `exact` catalog hit is APPLIED (mechanical — the names differ only in spelling). An `overlap` is only OFFERED, on both doors: applying a judgment call silently is how a cross-project rule surfaces in the wrong project. - canonical_systems.best_overlap is the ONE scorer behind the create-time offer and the review sweep, so the two surfaces can never name different areas for one System. It also takes the catalog the caller already holds, so the review is not an N+1. UI (folded in from step 1 — rule 27, that step shipped with no human surface): - SystemsSection: a Shared area picker on create and edit, the area on each card, and a collapsed review of proposals that appears only when there is something to decide. `exact` and `overlap` never share a style — one is mechanical, the other is the reviewer's judgment, and presenting them alike is how a wrong mapping gets waved through. - Settings → Admin → Areas: the catalog itself, showing each entry's slug, because the slug is what decides whether two names are the same area and a rename moves it. - A picker rather than a live matcher: reproducing the slug rule in TypeScript would give this feature two matchers to keep in step — the exact drift the catalog exists to end. The server stays authoritative. tests/helpers.fake_system gains canonical_id=None: an unnamed attribute is an auto-MagicMock and therefore truthy, which is the trap that helper exists for (note 2109) and a nullable FK walks straight into it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
12f71fabdf |
refactor(scribe): remove calendar + entity surfaces from web UI (frontend)
CI & Build / integration (push) Successful in 28s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 46s
CI & Build / Build & push image (push) Successful in 44s
Frontend half of the narrowing (milestone #194); matches backend
|
||
|
|
94d32c524a |
feat(issues): S4b frontend — open-issues lists + issue plumbing
- types/note.ts: Note gains systems? + arose_from_id?; TaskKind includes 'issue'. - stores/tasks.ts: create/update accept IssueFields (kind/system_ids/arose_from_id). - api/systems.ts: getProjectIssues + TaskLike. - DashboardView.vue: 'Open issues' rail section from dashboard.open_issues (links to /tasks/<id>, project + status). - SystemsSection.vue (project Systems tab): 'Open issues' list via getProjectIssues, with system chips, links to /tasks/<id>. Both reuse existing CSS tokens. Issue-editor controls (kind selector / system multi-select / arose-from picker in WorkspaceTaskPanel) are the remaining S4b piece. NEEDS operator browser verification (CI typechecks only). Refs plan 825 (S4b frontend — lists). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4da29562bd |
feat(issues): S4a UI — Systems management section in project view
Frontend foundation for Issues + Systems (spec #825, S4a). - frontend/src/api/systems.ts: typed client (System + list/create/update/delete) over /api/projects/<id>/systems, matching the rulebooks api style. - frontend/src/stores/systems.ts: Pinia store keyed by project (fetch/create/ update/archive/unarchive/delete), toast-on-error. - frontend/src/components/SystemsSection.vue: a Systems management section — cards (color swatch, name, description, 'N open' issue-count badge) with inline create/edit, archive (hidden behind a 'show archived' toggle), and a delete-confirm modal. v1 quality: loading skeleton, empty state, error toasts, keyboard a11y, focus rings; reuses existing CSS tokens (no new colors). - ProjectView.vue: new 'Systems' tab (between Notes and Rules), rendering <SystemsSection :project-id>, wired like the existing rules tab. S4b (next) adds issue-editor controls (kind=issue/system multi-select/arose-from), open-issues lists, and the dashboard surface. NEEDS operator browser verification (CI typechecks but can't render). Refs plan 825 (S4a). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
64bc50c788 |
chore(frontend): drop dead settings toggle, types, and store list-surface
Drift-audit Group 7 frontend cleanup (no behavioral change): - SettingsView: remove the 'auto-consolidate task bodies' toggle and its saveAutoConsolidate handler. The auto_consolidate_tasks setting has zero backend readers (curator removed in Phase 8); the control did nothing. - AppSettings type: drop the dead assistant_name / default_model hints (kept the open string index signature the store actually uses). Delete the fully orphaned types/chat.ts (zero importers). - notes/tasks Pinia stores: remove the list/filter/sort/pagination surface that backed the removed /notes and /tasks list views (verified no consumer uses the tasks/notes arrays, refresh, or any filter/sort/pagination method). Kept currentNote/currentTask, loading, fetch/create/update/delete, convert, patchStatus, startPlanning, backlinks, tags. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
658348f208 |
feat(rules): always_on rulebook flag + Scribe-first prompt
Adds rulebooks.always_on (migration 0058) and a new list_always_on_rules MCP tool so a session-start eager pull can fetch standing rules without needing an active-project notion. Updates _INSTRUCTIONS so Claude calls the new tool at session start and codifies engineering rules in Scribe rather than CLAUDE.md / auto-memory. Seeds FabledSword family rulebook to always_on=true on migrate, matching its design role as the cross-project standards rulebook. Frontend: badge in RulebookListPane for always-on rulebooks; toggle in RulebookDetailPane header bound to a new toggleAlwaysOn store action. This is S1+S2 of the rules-consolidation plan (Scribe task #508). S3 (project-scoped rules) and S4 (enter_project handshake) follow. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
e5796b6f5c | feat(trash): frontend trash API client + Pinia store | ||
|
|
311322fdc8 | fix(plan): export startPlanning from tasks store (type-check) | ||
|
|
1d5f49fe3b | feat(plan): frontend startPlanning API + store action + task_kind type | ||
|
|
605dd0a13a | feat(rulebook): frontend API client + Pinia store | ||
|
|
91bafb641f |
refactor: Phase 8 — backend deletion (chat / voice / push / journal / curator)
Mega-commit. Strips all server-side LLM machinery now that Phase 7 has
removed the corresponding UI surfaces and the MCP HTTP endpoint is the
sole assistant interface.
Deleted (services/):
chat, generation_buffer, generation_log, generation_task, llm, tools/
(entire package), stt, tts, voice_config, voice_library, push,
journal_closeout, journal_pipeline, journal_prep, journal_scheduler,
journal_search, curator, curator_scheduler, consolidation,
tag_suggestions, research, weather, article_fetcher, pending_actions,
moments, assist, wikipedia.
Deleted (routes/):
chat, voice, push, journal, quick_capture, fable_mcp_dist.
Deleted (models/):
conversation, generation_tool_log, push_subscription,
pending_curator_action, moment, weather_cache.
Deleted (tests/):
test_generation_log, test_journal_*, test_consolidation, test_lookup_tool,
test_notes_consolidation_trigger, test_record_moment_guards,
test_research_pipeline, test_tools_*, test_tool_use_fixes,
test_voice_library, test_weather_service, test_calendar_tool_tz,
test_wikipedia.
Deleted (top-level):
fable-mcp/ (legacy standalone stdio package — wheel-build pipeline
also removed from Dockerfile).
app.py:
- blueprint registrations for the 6 deleted routes
- startup hook trimmed: no more Ollama warmup, KV-cache priming,
journal/curator schedulers, voice model loading
- shutdown hook simplified
- httpx import dropped (was for Ollama calls)
pyproject.toml:
- removed deps: pywebpush, feedparser, html2text, trafilatura
- removed [voice] extras entirely
- description updated for the MCP-first architecture
Dockerfile:
- removed faster-whisper / piper-tts install steps
- removed bundled piper voice download stage
- removed fable-mcp wheel build stage
Surviving-file edits:
- services/auth.py: drop Conversation table claim on first-user setup
- services/backup.py: drop conversation / push-subscription export+restore;
v1/v2 restore now silently skip pre-pivot conversation data
- services/notes.py: drop maybe_consolidate trigger on task done/cancelled;
drop _maybe_trigger_project_summary (LLM auto-summary)
- services/projects.py: drop generate_project_summary + backfill_project_summaries
(both LLM-driven)
- services/user_profile.py: drop append_observations / consolidate /
clear_learned_data (curator-tied) and build_profile_context
(was LLM system-prompt builder)
- services/notifications.py: stub out _fire_push_notif (was send_push_notification)
- services/event_scheduler.py: drop event-reminder push + chat-retention
cleanup job; keep CalDAV pull-sync + reminders job (in-app)
- services/diagnostics.py: _curator_busy() always False
- routes/notes.py: drop /assist, /assist/stream, /suggest-tags endpoints
- routes/tasks.py: drop /<id>/consolidate endpoint
- routes/settings.py: drop /models, KV-cache-prime-on-save, journal-schedule
timezone hook, and the SearXNG search-test endpoint; inline _is_private_url
(was in services/llm.py)
- routes/admin.py: drop /voice, /voice/reload endpoints
- routes/profile.py: drop /consolidate, /observations (GET, DELETE)
- models/__init__.py: drop the 6 dead model imports
Frontend cascade:
- stores/push.ts: deleted entirely (no callers after Phase 7)
- stores/settings.ts: drop checkVoiceStatus + voice-status state
- views/SettingsView.vue: drop Locations section + journalConfig state
(was tied to /api/journal/config); drop JournalConfig + journal/voice
api/client imports
- frontend/api/client.ts: orphaned voice/journal/profile-observation/
fable-mcp-dist exports are left as dead but harmless (call them and
they 404; type-check is clean).
Pre-existing v1 backups that contained conversations/messages still
restore — those tables are silently dropped from the import path.
Anyone pulling the new image with a populated database will need the
Phase 9 migration to drop the dead tables (coming next).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
18eb1e7ab2 |
refactor(ui): Phase 7 — strip chat/voice/journal/workspace/home surfaces
Frontend deletion phase of the MCP-first pivot. All in-app
conversational surfaces are gone — Claude/MCP is the assistant now.
Deleted views:
ChatView, JournalView, WorkspaceView, HomeView
Deleted components:
ChatPanel, ChatInputBar, ChatMessage, ChatStreamingBubble,
ToolCallCard, ToolConfirmCard, WorkspaceChatWidget
Deleted composables + store:
useVoiceRecorder, useVoiceAudio, useStreamingTts, stores/chat
Router changes:
- / now redirects to /knowledge (was /journal)
- dropped /chat, /chat/:id, /journal, /workspace/:projectId
- /tasks still redirects to / (→ /knowledge)
- /notes still redirects to /knowledge
KnowledgeView:
- removed ChatPanel + ChatInputBar embeds
- removed minichat floating widget + state + handlers
- removed Chat link from today bar
- removed `chatStore` driven auto-refresh-on-tool-call watch
App.vue:
- removed useChatStore + startStatusPolling/stopStatusPolling
- removed VAD ONNX preloader (voice subsystem dead)
- removed visibilitychange listener (only did voice status re-check)
- removed `c` single-key shortcut (focus chat / goto chat)
- removed `g+c` two-key sequence (goto chat)
- removed Chat section from shortcuts overlay
- removed `.chat-page` / `.workspace-root` CSS overflow rule
AppHeader.vue:
- removed useChatStore + status indicator (Ollama model status)
- removed Chat / Journal nav links (desktop + mobile)
SettingsView.vue (4598 → 4079 lines):
- removed Voice tab entirely
- Notifications tab: dropped Push Notifications + Chat History
+ About sections (kept Email Notifications)
- General tab: dropped Assistant (name + model pickers) +
Model Management sections (kept Tasks + Timezone)
- Profile tab: dropped Journal + Observations sections
- VALID_TABS + tab list array no longer include "voice"
- removed `loadVoiceTab()` activation trigger
Service worker (frontend/public/sw.js):
- dropped push and notificationclick handlers (push subsystem
only fired on internal generation completion, which is gone)
- kept empty fetch handler as PWA installability shell
Script-level dead code (state refs, helper functions referencing
removed APIs) remains in SettingsView and stores/push.ts and
stores/settings.ts for now — Phase 8 backend deletion will clean
those up alongside the matching backend route removals.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
dbd9f00061 |
refactor: hard-cut RSS infrastructure (scope C)
Removes the entire RSS feature surface — feeds, items, embeddings, reactions, discussion-note flow, briefing news context, settings, env-vars, and DB tables. Keeps the URL-generic article-reader (the read_article LLM tool) under a clean module so the LLM can still fetch arbitrary article content from URLs the user provides. Backend: - New services/article_fetcher.py — single source of trafilatura URL→text - New services/tools/article.py — read_article tool (was nested under tools/rss) - Delete services/rss.py, rss_classifier.py, rss_filtering.py, article_context.py - Delete services/tools/rss.py - Delete models/rss_feed.py (RssFeed, RssItem), models/rss_item_embedding.py - services/embeddings.py: drop upsert/semantic_search/backfill RSS helpers - services/llm.py: remove _build_briefing_article_context, briefing-conv branch, ARTICLE_DISCUSS_SEED skip-RAG branch; drop get_rss_items / add_rss_feed from the actions list - services/generation_task.py: drop _maybe_save_article_discussion_note + caller - routes/chat.py: drop /api/chat/from-article/<id> endpoint - routes/journal.py: re-import via web.py refactor (article_fetcher path) - services/tools/__init__.py: register `article`, drop `rss` - services/tools/_registry.py: drop the requires=='rss' check - app.py: drop backfill_rss_item_embeddings + backfill_rss_article_content tasks - config.py: prose-only edit (no env var change — RSS env vars were never first-class) Frontend: - stores/settings.ts: drop rssEnabled - SettingsView.vue: drop the RSS-classification mention - api/client.ts: drop openArticleInChat (the from-article endpoint is gone) Tests: - Delete tests/test_rss_service.py, test_news_api.py, test_article_reading.py Migration: - 0042_drop_rss: DROP TABLE rss_item_embeddings, rss_item_reactions, rss_items, rss_feeds; DELETE settings rows for rss_enabled / briefing_*_topics Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
3ac32dc3bc |
feat: add rss_enabled user setting to toggle RSS functionality
RSS is now off by default. When disabled: - Scheduler skips RSS feed sync during compilation slot - Briefing pipeline skips RSS item gathering - RSS LLM tools (get_rss_items, add_rss_feed) are hidden - API routes return empty results for feeds/news - Frontend hides News nav link, RSS Feeds and News Preferences in settings - Briefing view hides news sidebar section Toggle in Settings > Briefing > RSS / News. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
ed715dcc23 |
feat(knowledge): note types, counts, new-note button, audit fixes
- Add note_type (note/person/place/list) selector + entity metadata fields (relationship, email, phone / address, hours) to NoteEditorView - Pre-select type via ?type= query param from KnowledgeView new-note dropdown - KnowledgeView: add split "New note / ▾" button with type dropdown - KnowledgeView: show per-type counts on sidebar filter buttons (when > 1) - Fix: filter-btn now flex layout so count badge aligns to right edge - Fix: list_notes count_query was missing parent_id filter (inflated totals) - Fix: PATCH /api/notes/:id now fires upsert_note_embedding (workspace autosave) - Fix: get_knowledge_counts endpoint for per-type counts - Fix: get_knowledge_tags was silently discarding note_type filter (double-stmt bug) - Fix: NoteEditorView onMounted stray brace from edit session Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
71ca0ecb5c |
fix: voice status global store, per-view mic reactivity, single-voice preview
- 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> |
||
|
|
ac90548823 |
feat: task management enhancements (cancelled status, recurrence, timestamps)
- Add 'cancelled' status to TaskStatus type, StatusBadge, TaskCard, TaskEditorView, TaskViewerView, TasksListView - Add RecurrenceEditor component (none / interval / calendar rules) - TaskEditorView: wire RecurrenceEditor, show started_at/completed_at timestamps read-only, include recurrence_rule in save payload - TaskViewerView: show recurrence summary, timestamps in meta row - tasks.ts: statusFilter/priorityFilter as arrays, add recurrence_rule to updateTask and createTask payloads Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
fd05c65018 |
fix(calendar): correct event timezone handling
- Frontend sends user_timezone (IANA, from Intl.DateTimeFormat) with every message POST; threaded through route → generation_task → build_context - System prompt now tells the LLM the user's timezone so it creates events with the correct UTC offset (e.g. 15:00+01:00 not 15:00Z) - Calendar tool guidance updated to require UTC offset in all event datetimes - EventSlideOver: dateFromIso/timeFromIso now use JS Date to convert stored UTC times to local time for display; toIso includes local timezone offset when saving so the correct UTC time is stored Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
ebc79b34f9 |
feat(rag): RAG scoping and context isolation controls
- Migration 0030: add conversations.rag_project_id (NULL=orphan-only, -1=all notes, positive=project), projects.auto_summary and projects.summary_updated_at - Three-value scope semantics thread from build_context() → semantic search and keyword fallback via orphan_only + effective_project_id - Project summarization background job (generate_project_summary, backfill_project_summaries) called via Ollama; triggered on project update and note saves (debounced 1h); runs at startup - New LLM tools: search_projects (SequenceMatcher scoring on title+description+auto_summary) and set_rag_scope (persists to DB, workspace-guarded, emits new_rag_scope in SSE done event) - execute_tool() accepts conv_id + workspace_project_id; generation_task passes both and captures scope changes for SSE done enrichment - Frontend: Conversation type gets rag_project_id; chat store adds ragProjectId computed + updateRagScope(); SSE done handler syncs scope - ChatView: replace sidebar ProjectSelector with a scope chip pill above the input bar, animated dropdown, pulse on model-driven scope change Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
b44d8496bc |
fix: queue drain survives navigation away from a streaming conversation
Previously the queue drain guard (currentConversation.id === convId) meant that if the user navigated to ChatView while a workspace stream was running, the workspace's queued messages were silently abandoned — they sat in localStorage indefinitely with no code path to resume them. - Extract _tryDrainQueue(convId) with the same guard logic - Call it at stream-end (replaces the inline block) - Call it in fetchConversation after _loadQueue, so returning to a conversation with orphaned queue messages drains them automatically - Order is now preserved: messages drain in the order they were queued, even across navigation events Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
c7ef709633 |
Multi-user sharing, groups, and in-app notifications
Backend: - Alembic migration 0025: groups, group_memberships, project_shares, note_shares, notifications tables - models: Group, GroupMembership, ProjectShare, NoteShare, Notification - services/access.py: permission resolution (viewer/editor/admin/owner) - services/groups.py + routes/groups.py: full group CRUD + membership - services/sharing.py + routes/shares.py: project/note sharing API - services/notifications.py: in-app notification create/list/mark-read - routes/in_app_notifications.py: GET/POST notification endpoints - routes/users.py: user search endpoint - services/projects.py + services/notes.py: *_for_user variants - routes updated to use *_for_user on get/list; adds permission field - app.py: register all new blueprints Frontend: - api/client.ts: ShareEntry, GroupEntry, NotificationEntry types + helpers - stores/notifications.ts: Pinia store with polling - NotificationBell.vue: bell icon + badge + dropdown toggle + 60s poll - NotificationsPanel.vue: unread notification list with mark-all-read - ShareDialog.vue: teleport modal for sharing projects/notes with users/groups - SharedWithMeView.vue: /shared route listing shared projects and notes - AppHeader: NotificationBell, Shared nav link - ProjectView, NoteViewerView, TaskViewerView: Share button + ShareDialog - SettingsView: Groups admin tab with create/delete groups + member mgmt - router: /shared route Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
4dd3c1fe81 |
Fix queued bubbles, queue persistence, duplicate confirm UX, semantic check
- 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> |
||
|
|
d7e1fe6aab |
UX: queue bubbles, dashboard queuing, duplicate confirm buttons, short-note fix
- 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> |
||
|
|
059a2e06d5 |
Queue concurrent prompts instead of dropping or conflicting
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> |
||
|
|
c65aad6639 |
Upgrade all major frontend dependencies
- TipTap 2 → 3: Extension from @tiptap/core, Placeholder from @tiptap/extensions, TaskList/TaskItem from @tiptap/extension-list, link: false in StarterKit (now bundles Link), @tiptap/core added - marked 15 → 17: heading renderer updated to tokens/parseInline API - Pinia 2 → 3, Vue Router 4 → 5 (no code changes required) - Vite 6 → 7, @vitejs/plugin-vue 5 → 6, vue-tsc 2 → 3 - TypeScript 5.6 → 5.9: fixed Uint8Array<ArrayBuffer> strictness in push.ts, removed unused bodyEl ref in NoteViewerView.vue - .npmrc: legacy-peer-deps=true for TipTap v3 peer dep resolution TipTap 3 new capabilities now available: static renderer (createStaticRenderer), MarkViews, @tiptap/extensions package. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
432e0bd2a0 |
Show Qwen3 thinking output in chat as collapsible Reasoning block
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> |
||
|
|
e119331645 |
Phase 21: Intent-first pipeline, visible ack, KV-stable system prompt
Pipeline changes (generation_task.py, intent.py): - Remove optimistic streaming queue/race (_drain_queue deleted) - Remove _generate_acknowledgment — ack now embedded in intent JSON - Round 0: await intent (~400ms), stream ack immediately as TTFT, then execute tool sequentially; chat-only streams directly - IntentResult.ack: one-sentence acknowledgment, intent max_tokens 200→350 - _parse_intent extracts and trims ack field KV cache stability (llm.py, generation_buffer.py, generation_task.py): - build_context: replace cached_note_ids with include_note_ids - Auto-found notes populate context_meta["auto_notes"] for sidebar but are NOT injected into system prompt (--- Related Notes --- removed) - Explicitly included notes injected as --- Included Notes --- - _conv_note_cache dict + get/set/clear functions removed from generation_buffer.py - All clear_conv_note_cache() calls removed Cold model retry (llm.py): - generate_completion (used by classify_intent) retries on HTTP 500: 3 attempts with 3s/6s delays — prevents intent failure during cold load API + frontend (routes/chat.py, stores/chat.ts, views/ChatView.vue, components/ChatPanel.vue): - exclude_note_ids → include_note_ids throughout - ChatView sidebar: Suggested (auto-found, + to include) + In Context (× to remove) - ChatPanel: remove exclude button from context pills; no IDs passed to sendMessage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
316a85e13b |
Phase 20: Dedicated tag field — chip input, explicit tags array
Tags are now a first-class field rather than being auto-extracted from the note body. A new TagInput.vue chip component handles tag entry in both editor views with autocomplete, Enter/comma/backspace UX, and space-to-hyphen sanitization. Backend: - routes/notes.py: create reads tags from JSON; update accepts explicit tags (omit = keep existing); append_tag writes to tags array with dedup; suggest-tags accepts current_tags filter; remove extract_tags - routes/tasks.py: same — explicit tags on create/update; remove extract_tags - services/tag_suggestions.py: current_tags param replaces body extraction - services/tools.py: create_note tool schema adds tags param; executor passes it - services/llm.py: system prompt tells LLM to use tags param, not embed #tag in body Frontend: - components/TagInput.vue: new chip-based tag input (autocomplete, keyboard UX) - NoteEditorView.vue / TaskEditorView.vue: tags ref loaded from note.tags; TagInput placed between title and body; save/autosave include tags; suggest now adds chips; fetchTagSuggestions passes current_tags; dirty tracks tags - TiptapEditor.vue: remove fetchTags prop and TagSuggestion extension; keep TagDecoration for legacy inline #tag highlighting No DB migration needed — tags column already correct. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
b37e15d59a |
Add Authentik OAuth/OIDC SSO, email change, and setup docs
Phase 18 changes: OAuth/OIDC SSO (Authorization Code + PKCE): - alembic/versions/0015_add_oauth_fields.py: add oauth_sub UNIQUE column, drop NOT NULL on password_hash - src/fabledassistant/services/oauth.py: OIDC discovery (cached), build_auth_url, exchange_code, get_userinfo, find_or_create_oauth_user (sub→email auto-link→create) - src/fabledassistant/routes/auth.py: GET /api/auth/oauth/login and GET /api/auth/oauth/callback; LOCAL_AUTH_ENABLED guards on login/register; /api/auth/status now returns oauth_enabled + local_auth_enabled - src/fabledassistant/config.py: OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_SCOPES, LOCAL_AUTH_ENABLED, oidc_enabled() classmethod - src/fabledassistant/models/user.py: password_hash nullable, oauth_sub field, has_password bool in to_dict() - src/fabledassistant/services/auth.py: create_user accepts password=None + oauth_sub kwarg; authenticate returns None for OAuth-only users; add get_user_by_oauth_sub, link_oauth_sub, update_user_email - frontend: AuthStatus + User types updated; auth store exposes oauthEnabled + localAuthEnabled; LoginView shows SSO button / hides password form accordingly Email change: - PUT /api/auth/email: requires password confirmation for local-auth users, skips check for OAuth-only users; enforces email uniqueness - SettingsView.vue: new Email Address section pre-filled with current email, updates authStore.user in-place on success Docs: - docs/oauth-setup.md: step-by-step Authentik provider setup, example docker-compose env vars, account linking explanation, per-provider issuer URL table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
667ccd70cd |
Security audit, bug fixes, auto-save, and writing assistant improvements
Backend security & correctness: - Add rate_limit.py: sliding-window rate limiter (asyncio) applied to login, register, forgot/reset password endpoints (10/60s or 5/300s per IP) - app.py: add security headers in after_request (X-Frame-Options, CSP, X-Content-Type-Options, Referrer-Policy) using setdefault to preserve SSE headers - auth.py: refactor duplicate login_required/admin_required into shared _check_auth() - config.py: add TRUST_PROXY_HEADERS for proxy-aware client IP resolution - routes/auth.py: rate limiting, _client_ip() helper, cleaned-up reset_password route - routes/chat.py, notes.py, tasks.py: int() DoS fix on last_event_id; limit capped at 500; date.fromisoformat() wrapped in try/except → 400 on invalid dates - services/auth.py: fix Setting.user_id update bug (filter on NULL not user.id); reset_password_with_token returns int|None (user_id) instead of bool - services/backup.py: add _security_notice to full backup JSON export - services/assist.py: system prompt explicitly preserves markdown list structure and nested indented sub-items Infrastructure: - docker-compose.yml: add healthcheck on app service (/api/health, 10s interval) - .dockerignore: prevent secrets/node_modules/__pycache__/.env.* leaking into build Frontend bug fixes: - TaskCard.vue, TaskViewerView.vue: fix isOverdue() timezone bug (ISO string compare) - useAssist.ts: accept() now resets state to idle when document changed since proposal - stores/chat.ts: fix memory leak in _pollUntilLoaded() (try/catch around fetchStatus) - TiptapEditor.vue: selection offset uses closest-match strategy (not first-match) - utils/markdown.ts: explicit DOMPurify config with FORCE_BODY; remove as const (DOMPurify expects mutable string[]) New features: - Auto-save (5-minute interval) in NoteEditorView and TaskEditorView — only when editing an existing dirty record; silent on error, shows "Auto-saved" toast - sectionParser.ts: top-level bullet/numbered list items are now individual sections in the AI Assist panel (previously treated as one undifferentiated block) - editor-shared.css: extracted ~500 lines of CSS duplicated between both editors; includes .inline-assist-btn at global scope (required for teleported elements) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
32e4ee12f2 |
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>
|
||
|
|
24d3c5bc68 |
Enable thinking mode in full chat view, keep disabled in widget/panel
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> |
||
|
|
815eed2574 |
Add tool confirmation UI with Accept/Decline for write operations
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> |
||
|
|
765e99bb24 |
Fix duplicate message bug and add generation timing instrumentation
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> |
||
|
|
fbce540638 |
Add streaming status UX and model load state indicator
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>
|
||
|
|
d7bc3f3222 |
Add CalDAV calendar integration, LLM-suggested tags, and settings refinements
- 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> |