Commit graph

2095 commits

Author SHA1 Message Date
Till JS
74bbfda212 feat(ai): Mission Grant consent UI + Workbench audit tab
Phase 3 — user-facing side of the Mission Key-Grant rollout. Users
can now opt into server-side execution, revoke it, and inspect every
decrypt the runner has performed.

Webapp:
- MissionGrantDialog explains the scope (record count, tables, TTL,
  audit visibility, revocation) and calls requestMissionGrant. Error
  paths render distinctly for ZK, not-configured, missing vault.
- Mission detail shows a Server-Zugriff box with status pill
  (aktiv/abgelaufen/nicht erteilt), Neu-erteilen + Zurückziehen
  buttons. Only renders for missions with at least one encrypted-
  table input.
- store.ts: setMissionGrant / revokeMissionGrant helpers, Proxy-
  stripped like the rest of the store's writes.
- Workbench adds a Timeline/Datenzugriff tab switch. Audit tab queries
  the new GET /api/v1/me/ai-audit endpoint, renders decrypt events
  with color-coded status pills (ok/failed/scope-violation) and
  stable reason strings.
- getManaAiUrl() added to api/config for the audit fetch.

mana-ai:
- GET /api/v1/me/ai-audit (JWT-gated via shared-hono authMiddleware)
  backed by readDecryptAudit() — withUser + RLS double-gate so a user
  can only read their own rows.
- Limit capped at 1000, newest-first.

Missions without a grant continue to work exactly as before; the
grant UI is purely additive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:53:11 +02:00
Till JS
4b29f6d293 fix(ai-missions): swap structuredClone for JSON-roundtrip deepClone
Browser \`structuredClone\` itself fails on Svelte 5 \$state Proxies
("Failed to execute 'structuredClone' on 'Window': [object Array]
could not be cloned") — it doesn't transparently unwrap the Proxy the
way I'd hoped. The structured-clone algorithm refuses any non-cloneable
host object, including Svelte's reactive wrappers.

JSON.parse(JSON.stringify(...)) traverses through the Proxy by reading
each property normally, producing a plain-data copy that Dexie can
serialise without complaint.

Mission payloads are pure JSON values (strings/numbers/arrays/objects)
so JSON-roundtrip is lossless. The new \`deepClone\` helper is local to
this file with a comment pointing at the structured-clone failure.

77/77 webapp tests still green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:50:05 +02:00
Till JS
394931e3b3 fix(ai-missions): strip Svelte \$state Proxies before Dexie writes
`createMission`, `updateMission`, and `{start,finish}Iteration` all
received caller-supplied objects that can be Svelte 5 \$state Proxies
(MissionInputPicker binds the inputs array with \$state). IndexedDB's
structured-clone algorithm doesn't accept proxied arrays and throws
`DataCloneError: [object Array] could not be cloned` — visible to
users as "Mission anlegen" failing silently after clicking Create.

Wrap each proxy-carrying payload in `structuredClone()` at the store
boundary:
  - createMission: `inputs` + `cadence`
  - updateMission: whole `patch` (anything can be proxy)
  - startIteration: `plan`
  - finishIteration: `plan` (conditional)

`structuredClone` is the native browser / Bun helper; strips Proxies
while preserving Dates / Maps / Sets / nested plain data. Store stays
robust to any future caller that forgets to snapshot before passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:44:05 +02:00
Till JS
9809b06adf feat(app-registry): new 'AI' category at top of the app picker
Groups the six AI-Workbench apps plus the Companion chat under a
dedicated category, moved to order 0 so they're the first thing a user
sees when adding a page. Brain icon.

- `categories.ts`
  - New `ai` type added to AppCategory union at order 0
  - Old 'companion' category stays for derived projections (myday,
    activity, goals) that aren't themselves AI-driven — renamed doc
    only, behavior unchanged
  - APP_CATEGORY_MAP reassigns: companion, ai-missions, ai-workbench,
    ai-rituals, ai-policy, ai-insights, ai-health → 'ai'
- `AppPagePicker.svelte` collapsed-state map gains `ai: false` so the
  new category is expanded by default (it's the user's new front door)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:34:07 +02:00
Till JS
37e39a5ddb feat(ai): AI features as top-level workbench apps (not sub-routes)
Reverts the previous /companion-carousel misstep. The user's model is
that Missions / Workbench / Rituals / Policy / Insights / Health each
live as their OWN app in the root `/` workbench scene, alongside todo /
calendar / notes / etc. — openable from the normal app picker, freely
combinable with any other module card.

- New modules, each with a ListView.svelte usable inside AppPage's
  PageShell (no self-wrapping, no shell-control props):
    `lib/modules/ai-missions/ListView.svelte`
    `lib/modules/ai-workbench/ListView.svelte`
    `lib/modules/ai-rituals/ListView.svelte`
    `lib/modules/ai-policy/ListView.svelte`
    `lib/modules/ai-insights/ListView.svelte`
    `lib/modules/ai-health/ListView.svelte`
- Registered in `app-registry/apps.ts`: `ai-missions`, `ai-workbench`,
  `ai-rituals`, `ai-policy`, `ai-insights`, `ai-health`. Each picks a
  distinct color + icon.
- `/companion/+page.svelte` restored to the simple chat it was before.
  The companion app (chat) remains as its own registered app —
  unchanged.
- Removed: `lib/modules/companion/pages/` + the
  `workbench-settings.svelte.ts` store (both were the dead-end
  PageCarousel approach).

User model now:
  ` / ` (workbench root) → [+ App] → any of the 6 AI apps + any module
  /companion                         → full-screen chat (unchanged)
  /todo, /calendar, …                → module-inline ghost inbox stays

svelte-check clean; webapp AI tests 71/71 green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:23:20 +02:00
Till JS
9686198a16 feat(companion): refactor into PageCarousel — every AI feature is a page
Drops the split /companion/missions + /companion/workbench +
/companion/rituals sub-routes and rebuilds /companion around the
shared PageCarousel pattern (same one /todo uses). Every feature is
now a self-contained page the user opens/closes/reorders/resizes in
one unified surface.

New pages:
- **Home** (default) — compact stats + one-click shortcuts to every
  other page, with "offen" badge when already open
- **Chat** — conversation sidebar + active chat inline
- **Missions** — list ↔ create ↔ detail master-detail inside one pane
- **Workbench** — timeline grouped by iteration + Revert per bucket,
  Mission filter dropdown (replaces the old ?mission=… query-param)
- **Rituals** — migrated from /companion/rituals
- **Policy** — NEW: 3-way per-tool toggle (auto/propose/deny) with
  localStorage-backed overrides merged into DEFAULT_AI_POLICY live
- **Insights** — NEW: approval rate, 14-day bar chart of AI events,
  per-mission stats, top-5 recurring user-feedback strings. All from
  local Dexie liveQueries, no server calls.
- **Health** — NEW: foreground runner status, manual tick trigger,
  link out to status.mana.how for server-runner uptime

Plumbing:
- `stores/workbench-settings.svelte.ts` — persistent open-pages list
  in localStorage (id + widthPx + heightPx + maximized); open/close/
  resize/moveLeft/moveRight helpers
- `pages/page-meta.ts` — central registry (title, color, icon,
  description) consumed by home shortcuts + PagePicker
- `pages/PagePicker.svelte` — lists available (not-yet-open) pages
  with icon + description
- Old sub-routes deleted: /companion/{missions,workbench,rituals}/

Webapp tests still 71/71 green; svelte-check clean on the new pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 13:11:16 +02:00
Till JS
ce944ef14f docs(ai): document observability + Revert + full scope in webapp CLAUDE.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:53:37 +02:00
Till JS
ad5f670ec2 feat(ai): revert-per-iteration button in the Workbench timeline
Each iteration bucket now carries a Revert button that undoes every AI
write attributed to that iteration. Closes the last open Workbench
feature in the roadmap.

- `data/ai/revert/inverse-operations.ts` — pluggable registry mapping
  event types to their undo actions. Ships with inverses for the five
  most common proposable outcomes:
  * TaskCreated        → tasksStore.deleteTask
  * TaskCompleted      → tasksStore.toggleComplete (back to incomplete)
  * CalendarEventCreated → eventsStore.deleteEvent
  * PlaceCreated       → placesStore.deletePlace
  * DrinkLogged        → drinkStore.deleteEntry
  Events with no registered inverse are tallied as `skippedUnsupported`
  — user knows to handle those manually rather than the service
  silently doing nothing.
- `data/ai/revert/revert-iteration.ts` — orchestrator. Filters
  `_events` by `actor.iterationId + actor.missionId`, sorts
  newest-first (so a completion unwinds before the underlying task
  deletion), applies each inverse, returns `RevertStats` summary.
- Workbench UI: Revert button with confirm dialog on every bucket.
  Shows "X zurückgenommen · Y nicht unterstützt · Z fehlgeschlagen"
  result alert.
- 5 unit tests cover: happy path, unsupported types, failure isolation,
  user-event skipping, newest-first ordering.

With this, the AI Workbench has full audit + undo semantics: user sees
everything the AI did, can approve/reject at stage time, and can roll
back approved actions after the fact.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:18:14 +02:00
Till JS
24eb8b3b7f refactor(shared-ui): extract search-core for highlight + debounce
Both QuickInputBar (InputBar.svelte), CommandBar.svelte, and GlobalSpotlight
were duplicating syntax highlighting and the 150ms search debounce. Pull
these into a new `packages/shared-ui/src/search-core/` module so the two
input surfaces stay in sync on feel and matching rules.

- search-core/highlight.ts — HighlightPattern type, locale-aware
  getHighlightPatterns(), and the shared highlightText() (HTML-escape +
  span wrap). Patterns were previously in quick-input/highlightPatterns.ts
  + inline in CommandBar.svelte.
- search-core/config.ts — SEARCH_DEBOUNCE_MS = 150. Used from InputBar,
  CommandBar, GlobalSpotlight, and apps/mana web SearchEngine.
- quick-input/highlightPatterns.ts + types.ts become thin back-compat
  re-exports.
- Public surface: @mana/shared-ui now exports getHighlightPatterns,
  highlightText, SEARCH_DEBOUNCE_MS, and the HighlightPattern type.

No UX change. UIs still live in their own files (per earlier split
recommendation: shared backend, separate surfaces).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:06:37 +02:00
Till JS
b03bbe132e feat(shared-ui): unify bottom-stack bars with shared Pill component
- Extract Pill.svelte as the single visual primitive (44px, icon+label,
  active/primary/danger variants) used by PillDropdownBar and TagStrip.
  PillNav keeps its own internal .pill class (36px, icon-only-oriented).
- Extract phosphor-icon-map.ts to deduplicate the icon lookup tables
  that previously lived inline in PillDropdownBar.
- Unify bar slot heights in (app)/+layout.svelte: 56px PillNav,
  64px for tags / quickinput / tabbar / dropdown-bar. Remove debug
  outlines. Collapse bottom-stack gap so bars sit flush below PillNav.
- SceneAppBar wrapped in 64px slot, scene-pill/app-tab 40px to match.
- Enforce single-bar policy: opening one bar closes the others.
- QuickInputBar strip-down: remove leading CheckSquare icon and trailing
  nav-toggle snippet; bar is pure search input now.
- Move user-menu (last PillNav pill) to bar-mode with short content:
  Einstellungen, Light/Dark/System segmented, Theme, Logout.
- Swap tabs nav icon from Columns to Cards for better readability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:58:46 +02:00
Till JS
ce646550cd fix(ai): bubble proposal-reject feedback into Mission iteration
The Planner reads `mission.iterations[].userFeedback` — not
`pendingProposals.userFeedback` — so feedback stored only on the
proposal row never reached the next plan. rejectProposal now also
appends to the matching iteration's `userFeedback` (merging with any
existing reasons via "\n· " bullets) when the proposal carries
missionId + iterationId.

Lazy imports the mission store to avoid a proposal↔mission cycle.
Best-effort: proposal rejection still succeeds even if the bubble fails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:57:25 +02:00
Till JS
98547b9e4e feat(ai): freitext feedback on proposal rejection
Rejecting a ghost-card proposal now reveals an inline textarea. Trimmed
feedback lands in `proposal.userFeedback` where the next Planner
iteration already reads it via iteration history — the AI learns from
concrete rejection reasons without needing a settings UI.

- Click "Ablehnen" → textarea + Cancel/Reject buttons replace the
  approve/reject row
- Empty submission still rejects (userFeedback stays undefined so the
  Planner sees "no reason given" rather than an empty string)
- `rejectingId` + `rejectDraft` are per-inbox local state; only one
  reject form is open at a time

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:56:39 +02:00
Till JS
9bc44c075a feat(ai): roll out AiProposalInbox to calendar / places / drink / food
One-line drop-in per module page. Each module's proposals now render as
ghost cards inline wherever the user already expects to see that
module's content — no separate approvals inbox to hunt for.

Covers all four remaining modules with proposable tools in
AI_PROPOSABLE_TOOL_NAMES:
- calendar → create_event
- places → create_place, visit_place
- drink → undo_drink
- food → (auto-tools only today; the inbox is ready for future
  propose-class food tools)

Now the only modules with proposable tools NOT showing the inbox are
those that don't have their own /route (all covered by todo, calendar,
places, drink, food).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:54:59 +02:00
Till JS
4d6e6e61b4 feat(mana-web): keyboard shortcuts for workbench + nav bars
- 1–9 scroll to the Nth open app on the workbench homepage; 0 opens the
  app picker.
- q/w/e toggle the bottom bars (workbench tabs / search / tags); r opens
  the user-menu PillDropdownBar (expanding the PillNav first if needed);
  t toggles the PillNav visibility.

Adds a `data-user-menu-trigger` hook on the user pill so the layout can
drive the menu bar programmatically without duplicating its config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:53:42 +02:00
Till JS
4be5e29bd3 feat(shared-ai): canonical proposable-tool list + drift guard on mana-ai
Makes the webapp's AI policy and the server's tool allow-list physically
impossible to drift. Adds the missing entries the guard caught on first
run: `complete_tasks_by_title`, `visit_place`, `undo_drink` now have
parameter schemas server-side too.

- `packages/shared-ai/src/policy/proposable-tools.ts`
  - `AI_PROPOSABLE_TOOL_NAMES` as `const` array + literal union type
  - `AI_PROPOSABLE_TOOL_SET` for set-membership checks
- Webapp `DEFAULT_AI_POLICY` derives its `propose` entries from the
  shared list via `Object.fromEntries(...)` — adding a tool there is now
  a one-line change in `@mana/shared-ai`
- mana-ai `AI_AVAILABLE_TOOLS`: module-load assertion compares its
  hardcoded names against `AI_PROPOSABLE_TOOL_SET` and throws with a
  pointed error on drift (extras in one direction, missing in the
  other). Service refuses to start on mismatch — better than silent
  degradation.
- Bun test (`tools.test.ts`) runs the same contract plus sanity checks
  (non-empty description, required params carry docs). Vitest policy
  test adds the symmetric check on the webapp side.

All three runtimes now green: webapp 66/66, shared-ai 2/2,
mana-ai 9/9 Bun tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:52:38 +02:00
Till JS
4b67316343 feat(ai): add tasks + calendar events as Mission inputs (webapp side)
Mission-Input-Picker now surfaces open tasks + upcoming calendar events
alongside notes / kontext / goals. When the foreground runner runs, the
corresponding resolvers decrypt the records client-side and hand real
content (title + due date / event time + description) into the Planner
prompt.

- `tasksResolver` + `tasksIndexer` — reads unencrypted subset + decrypts
  via `decryptRecords('tasks', …)`. Picker shows only OPEN tasks (not
  completed ones) to keep the list relevant. Resolver output is
  `[status]{ · fällig date}{ \n description}` — terse by design.
- `calendarResolver` + `calendarIndexer` — similarly decrypts events;
  picker prioritizes upcoming events (sorted by startIso), shows
  near-term times as "bald: YYYY-MM-DDTHH:MM" to make recency obvious
- Both tables are encrypted client-side — server-side mana-ai resolvers
  remain intentionally absent (per the privacy contract in
  `services/mana-ai/src/db/resolvers/types.ts`)

With this, a user can create a Mission like "plan my week" and link a
few tasks + calendar events as context; the in-browser planner sees the
full picture (decrypted), while the off-tab runner still plans from
objective + concept only for those missions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:44:59 +02:00
Till JS
5e01763caa feat(ai): close the loop — server write-back + webapp staging effect
Completes the off-tab AI pipeline. mana-ai now writes produced plans
back to `sync_changes` as a server-sourced Mission iteration; the webapp
picks it up on next sync and translates each PlanStep into a local
Proposal via the existing createProposal flow. User sees the resulting
ghost cards in the matching module's AiProposalInbox with full mission
attribution.

Server (mana-ai v0.3):
- `db/connection.ts` — `withUser(sql, userId, fn)` RLS-scoped tx helper
  mirroring the Go `withUser` pattern (SET LOCAL app.current_user_id)
- `db/iteration-writer.ts`
  - `planToIteration(plan, id, now)` — shared-ai AiPlanOutput → inline
    MissionIteration with `source: 'server'` + status='awaiting-review'
  - `appendServerIteration(sql, input)` — INSERT sync_changes row with
    op=update, data={iterations: [...]} + field_timestamps + actor
    JSONB={kind:'system', source:'mission-runner'}
- `cron/tick.ts` — after parse success: build iteration, append to
  mission.iterations, persist via appendServerIteration. Stats now
  include `plansWrittenBack`.

Actor union:
- `packages/shared-ai/src/actor.ts` + webapp actor: `system.source` gains
  `'mission-runner'` so the server's own writes are attributed correctly
  and distinguishable from projection/rule writes

Webapp:
- `data/ai/missions/server-iteration-staging.ts`
  - `startServerIterationStaging()` subscribes to aiMissions via Dexie
    liveQuery; on each Mission update, walks iterations looking for
    `source='server'` entries that haven't been staged yet
  - For each such iteration: creates a Proposal per PlanStep under
    `{kind:'ai', missionId, iterationId, rationale}` so policy + hooks
    fire correctly
  - Writes proposalIds back into plan[].proposalId + status='staged' so
    other tabs and app restarts skip re-staging
  - Idempotent: in-memory `processedIterations` Set + durable
    proposalId marker
- Wired into (app)/+layout.svelte alongside startMissionTick
- 3 unit tests: translate server iteration → proposal, skip
  already-staged, ignore browser iterations

Full pipeline now: user creates Mission in /companion/missions →
mana-ai tick picks it up → calls mana-llm → parses plan →
writes iteration → synced to webapp → staging effect creates
proposals → user approves in /todo (or any module) → task lands with
`{actor: ai, missionId, iterationId, rationale}` attribution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:29:30 +02:00
Till JS
0d90b12d1c feat(shared-ai): extract planner + mission types to @mana/shared-ai
Single source of truth for AI Workbench types shared between the webapp
(Vite/SvelteKit) and the server-side mana-ai Bun service. Prevents the
two runtimes from drifting on prompt shape or mission structure.

- `@mana/shared-ai` package:
  - `actor.ts` — Actor union (user | ai | system) + helpers, mirrors the
    webapp's runtime type so server-side consumers parse incoming actors
    without re-declaring
  - `missions/types.ts` — Mission, MissionCadence, MissionInputRef,
    MissionIteration, PlanStep, MissionState. Adds optional
    `iteration.source: 'browser' | 'server'` to distinguish foreground
    vs server-produced iterations (groundwork for proposal write-back)
  - `planner/prompt.ts` — `buildPlannerPrompt` pure function
  - `planner/parser.ts` — `parsePlannerResponse` strict JSON validator
  - Vitest smoke tests (2) cover prompt → parse round-trip + unknown-
    tool rejection
- Webapp:
  - `missions/types.ts` re-exports from shared-ai, keeps webapp-local
    `MISSIONS_TABLE` constant + `planStepStatusFromProposal` bridge
  - `missions/planner/{types,prompt,parser}.ts` become re-export stubs
    so existing imports keep working unchanged
  - Existing webapp tests (60) continue to pass — the wire code didn't
    move, just its home

Next: mana-ai service imports buildPlannerPrompt/parsePlannerResponse
from shared-ai + wires mana-llm + writes iteration back as a
'source=server' row (tracked in services/mana-ai/CLAUDE.md).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:01:57 +02:00
Till JS
5922abbbd8 feat(sync): stamp __lastActor + __fieldActors on incoming server changes
Closes the cross-device attribution loop. When another device pushes a
change with `actor: { kind: 'ai', missionId, … }`, the receiving device
now persists that attribution onto the record so the Workbench timeline
and per-module ghost badges render the same way they would on the
originating device.

- `readFieldActors()` sibling helper next to `readFieldTimestamps` for
  reading the per-field actor map off a record
- `applyServerChanges`:
  - Insert-new-record: stamp every field with `change.actor`, set
    `__lastActor` on the whole record
  - Insert-as-upsert: stamp only the winning fields (same LWW condition
    as the timestamp merge), update `__lastActor` to the change actor
  - Field-level update: same per-field + whole-record stamping
  - Pre-actor clients (change.actor undefined) fall back to USER_ACTOR so
    legacy rows still have a valid stamp
- All three paths also add the new hidden keys to their "skip" lists so
  incoming payloads can't smuggle old bookkeeping fields through

With this, the full pipeline is cross-device:
  Device A (AI writes)  → meta.actor + __lastActor + pendingChange.actor
  mana-sync (Go)        → persists actor JSONB on sync_changes row
  Device B (sync pull)  → applyServerChanges re-stamps __lastActor +
                          __fieldActors from the incoming change
  Device B (Workbench)  → renders the AI's activity from `_events` with
                          correct rationale + mission context

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:37:19 +02:00
Till JS
615b1c23c3 feat(sync): thread actor through webapp sync client
Matches the wire contract the Go server just learned to persist. Every
PendingChange now carries the actor through the Dexie row into the POST
payload; SyncChange on the receiving side accepts an opaque actor blob.

- `sync.ts`
  - `SyncChange.actor?: Actor` on the wire type; documented as opaque +
    back-compat with pre-actor clients
  - `PendingChange.actor?: Actor` — the pending-changes row already gets
    an actor stamped by the Dexie hook, the type now reflects it
  - `isValidSyncChange` accepts actor as an object or undefined, never
    asserts internal shape (the payload is opaque by design)
  - Push payload includes `actor: p.actor` alongside the other fields

- `module-registry.test.ts` — `pendingProposals` added to INTERNAL_TABLES.
  It's a local-only staging table that intentionally does NOT sync
  (approved writes run the underlying tool, which syncs normally).

Follow-up still open: when applyServerChanges writes a record from an
incoming change, stamp `__lastActor` + `__fieldActors` from the incoming
actor so the Workbench timeline attributes cross-device writes
correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:35:08 +02:00
Till JS
90e6d4dcc6 refactor(projections): wrap streak-tracker writes in system actor
Derived-state writes should be attributed to the projection subsystem,
not to whoever triggered the upstream event. `_streakState` is local-
only today so no cross-device user-visible effect, but once any derived
table joins sync this is the only correct model.

- `markActive` and `ensureSeeded` now run under
  `runAsAsync({ kind: 'system', source: 'projection' }, …)`
- Sets the pattern for future projections (DaySnapshot, correlations, …)
  to follow verbatim when they start writing persistently

Closes one of the Step-1 follow-ups tracked in
COMPANION_BRAIN_ARCHITECTURE §20. Remaining:
- mana-sync Go + Postgres migration for the `actor` field
- rule-engine to wrap its future writes the same way (no writes today)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:23:16 +02:00
Till JS
2fe9522953 feat(ai): Workbench timeline — cross-module AI activity lens
Single-page view of everything the AI has done, grouped by mission
iteration. Closes the "what did the assistant actually touch today?"
question that used to require raw event-log spelunking.

- `data/ai/timeline/queries.ts`
  - `useAiTimeline({ missionId?, module?, limit? })` — reactive live
    query over `_events`, filtered to `actor.kind === 'ai'`. Over-fetches
    by 3x and client-filters because `actor.kind` isn't indexed; cap at
    500 entries keeps it cheap.
  - `bucketByIteration(events)` — groups events sharing
    `actor.missionId + actor.iterationId` into a single visual unit so
    the rationale reads once per iteration rather than once per event.
    Pure function, fully unit-tested.
- `routes/(app)/companion/workbench/+page.svelte`
  - Buckets rendered chronologically with mission-link header + rationale
  - Per-event row shows module + event type + payload title + deep-link
    back into the module
  - Module dropdown filter + `?mission=…` query-string for mission-scoped
    views (linked from /companion/missions detail header)
- `/companion` sidebar + missions detail header now link to the Workbench

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:21:04 +02:00
Till JS
bf6b9cdd4b docs(ai): mark Missions UI + input picker (Step 6) done; document resolver/indexer symmetry
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 22:30:20 +02:00
Till JS
41052f769e feat(ai): input picker — link notes / kontext / goals to a Mission
Closes the "blind Planner" gap: users can now attach context records to
a Mission, and the Runner resolves them through the existing resolver
registry before calling the Planner. The LLM sees the actual linked
note content, not just the mission objective.

- `data/ai/missions/input-index.ts` — sibling registry to input-resolvers.
  Resolvers turn a ref into prompt text (Runner path); indexers list
  candidates for the picker UI (create-form path). Same shape, different
  direction; keeps modules decoupled from the AI layer on both ends.
- `data/ai/missions/default-resolvers.ts` — registers indexers for
  notes (title + content preview, capped at 200), kontext (the singleton
  doc), goals (title + progress). Co-located with the resolvers so the
  two halves stay in sync.
- `components/ai/MissionInputPicker.svelte` — drop-in picker: module
  selector → candidate list → chip-style selected display. Binds
  directly to `MissionInputRef[]` so forms use it as a single control.
- `/companion/missions` — picker wired into the create form between
  Konzept and Cadence; detail view's meta block now lists the linked
  inputs so users can see what context the Planner will see.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 22:25:07 +02:00
Till JS
3a8c019ab0 feat(ai): Missions UI under /companion/missions
Create, review, and control AI Missions from the app. Closes the last
UX gap in the end-to-end pipeline — users no longer need the Dexie
console to drive the Runner.

- `data/ai/missions/queries.ts` — `useMissions({ state? })` live query
  + single-mission `useMission(id)`. Decryption-ready via `decryptRecords`
  wrapper (no-op today, future-proof when/if Missions get added to the
  crypto registry).
- `routes/(app)/companion/missions/+page.svelte`
  - Inline create form: title + objective + markdown concept + cadence
    picker (manual / interval-minutes / daily-hour). Weekly + cron are
    wired in state but not exposed until a richer picker is worth it.
  - List / detail layout, responsive to narrow viewports.
  - Detail view: run-now button (invokes runMission with productionDeps),
    pause/resume/complete/delete lifecycle actions, iteration history
    with per-iteration feedback form.
  - Uses shared-icons + scoped CSS with theme-token fallbacks.
- `routes/(app)/companion/+page.svelte` — footer nav links to
  /companion/missions and /companion/rituals from the chat sidebar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:34:11 +02:00
Till JS
7535480007 feat(ai): production wiring for Mission Runner + default input resolvers
Connects the dependency-injected Runner to the real LlmOrchestrator and
drives it on a foreground tick in the app shell. Registers sensible
default input resolvers so Missions linked to notes / kontext / goals
work without per-module opt-in.

- `data/ai/missions/setup.ts`
  - `productionDeps` wires `aiPlanTask` through `llmOrchestrator.run`
  - `startMissionTick(intervalMs = 60_000)` kicks an immediate run then
    schedules `runDueMissions` on interval. Idempotent + overlap-guarded
    so a slow LLM run can't pile up ticks.
  - `stopMissionTick` clears the interval for teardown / HMR.
- `data/ai/missions/default-resolvers.ts` — resolvers for notes (title +
  decrypted content), kontext (singleton markdown), goals (progress
  projection). Registered once when the tick starts.
- `(app)/+layout.svelte` wires startMissionTick into the idle-phase init
  block alongside startEventStore / startStreakTracker / etc., and
  stopMissionTick into the teardown path.

System is now end-to-end runnable in the browser: create a Mission with
cadence 'interval', wait for the tick, see proposals appear in
/todo's AiProposalInbox. Missions UI (create/edit form) still open.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:24:13 +02:00
Till JS
1c6201be50 feat(ai): MissionRunner — orchestrates Planner + proposal staging
Executes one iteration end-to-end: resolve Mission inputs → build the
policy-filtered tool allowlist → invoke the Planner → stage each
PlannedStep as a Proposal (or auto-run if policy says so) → finalize
the iteration with summary + status.

- `data/ai/missions/runner.ts`
  - `runMission(id, deps)` runs a single iteration. Planner + stageStep
    are injected so the Runner is unit-testable without a live LLM.
  - `runDueMissions(now, deps)` scans for active missions past their
    nextRunAt and runs each once. Safe to call on a foreground tick.
  - Reuses the iteration id returned by `startIteration` so
    `finishIteration` updates the same row (fixed a dup-id bug the
    tests caught).
- `data/ai/missions/input-resolvers.ts` — registry: modules register a
  resolver at init, Runner looks up by module name. Missing resolvers
  degrade gracefully to "fewer inputs", never crash a run.
- `data/ai/missions/available-tools.ts` — exposes only tools the AI
  policy rates non-`deny`. Defence-in-depth with the executor + parser.

overallStatus derivation:
  0 steps                         → 'approved'  (no-op run is valid)
  all steps failed                → 'failed'
  any step staged (proposal id)   → 'awaiting-review'
  all steps ran auto              → 'approved'

Planner throw is caught and recorded as a failed iteration — one bad
mission can't stall the queue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:20:29 +02:00
Till JS
72d5c708c4 feat(ai): Mission Planner LLM task + prompt/parser
Turns a Mission (concept + objective + linked inputs + iteration history)
into a structured plan of tool-call proposals via the shared
LlmOrchestrator.

- `data/ai/missions/planner/types.ts` — AiPlanInput, AiPlanOutput,
  PlannedStep, ResolvedInput, AvailableTool
- `data/ai/missions/planner/prompt.ts` — pure builder producing system +
  user messages. System prompt enforces a strict fenced-JSON contract and
  lists available tools with parameter schema. User prompt injects the
  mission content, resolved input records, and the last 3 iterations
  (especially any userFeedback so the planner can course-correct).
- `data/ai/missions/planner/parser.ts` — strict parser with a
  discriminated ParseResult union. Rejects unknown tools, missing
  rationale, malformed shape. Tolerates missing optional fields.
- `llm-tasks/ai-plan.ts` — aiPlanTask LlmTask, minTier 'browser',
  contentClass 'personal'. On parse failure returns an empty plan with
  an explanatory summary rather than throwing, so the Runner can record
  a failed iteration without killing the queue.

No Runner yet — the planner is pure (input in, plan out). Runner (next
commit) will resolve inputs from modules, invoke the task, stage each
PlannedStep as a Proposal under the AI actor, and update the Mission
iteration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:16:10 +02:00
Till JS
d7bf8a2fd4 feat(ai): Mission datamodel — long-lived autonomous work items
Foundational entity for the AI Workbench Runner. A Mission carries the
user's standing instruction (concept + objective), references to the
modules it should draw context from, a cadence, and an append-only
history of iterations. Each iteration records the plan the AI generated
for that run plus the resulting proposal statuses and user feedback.

- `data/ai/missions/types.ts` — Mission, PlanStep, MissionIteration,
  MissionCadence union (manual / interval / daily / weekly / cron)
- `data/ai/missions/cadence.ts` — pure `nextRunForCadence(cadence, from)`
  used by the store on create / update / finishIteration
- `data/ai/missions/store.ts` — CRUD + lifecycle
  (pause / resume / complete / archive / delete) + iteration helpers
  (start / finish / addFeedback)
- `data/ai/module.config.ts` — new `ai` sync app; Missions sync
  cross-device (unlike the local-only `pendingProposals`)
- `db.version(18)` adds the `aiMissions` Dexie store with indexes on
  state, createdAt, nextRunAt, and [state+nextRunAt] for the Runner's
  "due now" query

Iterations live inline on the Mission record — append-only, small N,
always loaded together by the Planner. No separate child table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:10:19 +02:00
Till JS
0f3fd4eebd docs(ai): document Actor attribution + AI Workbench pilot
- COMPANION_BRAIN_ARCHITECTURE §20: Actor model, policy layer,
  pendingProposals lifecycle, ghost-UI pilot, roadmap, open follow-ups,
  manual test snippet
- DATA_LAYER_AUDIT §9: new Actor columns on records
  (`__lastActor`, `__fieldActors`), `pendingProposals` table, write-path
  diagrams for user / AI / approval, open mana-sync Go + Postgres work
- apps/mana/CLAUDE.md: short AI Workbench section with pointers + Dexie
  hook now lists actor stamping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:05:30 +02:00
Till JS
851a281e5a refactor: rename zitare -> quotes (Zitate)
Zitare was opaque Latin/Italian-flavored branding. Renamed to clear
English "quotes" (DE: Zitate) matching short-concrete-noun cluster.

- Module, routes, API, i18n, standalone landing app, plans dirs
- Dexie tables: quotesFavorites, quotesLists, quotesListTags,
  customQuotes (dropped redundant "quotes" prefix on the last)
- Logo QuotesLogo, theme quotes.css, search provider, dashboard
  widget QuoteWidget
- German user-facing label "Zitate" (English brand stays Quotes)

Pre-launch, no data migration needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:59:16 +02:00
Till JS
7a1f11c971 feat(ai): inline proposal inbox in the todo module
First pilot of the AI Workbench ghost-state pattern. A reusable
`<AiProposalInbox module="todo" />` component renders pending proposals
for a given module as dashed-outline ghost cards above the real content —
zero UI when the AI is idle, approve / reject inline when it's not.

- `data/ai/proposals/queries.ts` — reactive `useAiProposals` live query
  with module / status / missionId filters. Module filter resolves via
  the tool registry so each proposal auto-routes to the right page.
- `components/ai/AiProposalInbox.svelte` — the drop-in inbox component.
  Shows tool description + params + AI rationale; approve runs the
  original intent under the AI actor context (preserving attribution),
  reject stores the row with status=rejected for the next planner pass.
- Wired into /todo for the pilot. Other modules opt in by adding one
  line once their tools land in DEFAULT_AI_POLICY.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:58:46 +02:00
Till JS
513e3c7496 fix(types): enable allowImportingTsExtensions, restore .ts on shared-types
The prior dance (93bb94a12 drop .ts, bb278fb3c switch to .js) kept
breaking one consumer or the other:

- bare specifiers (no extension) satisfied svelte-check but broke the
  Node ESM loader invoked via @tailwindcss/node during SSR — SSR of
  every (app) route 500'd with ERR_MODULE_NOT_FOUND on 'theme'.

- .js extensions satisfied svelte-check and Vite but still broke the
  Tailwind loader, because the files on disk are .ts — Node ESM walks
  the actual filesystem and can't rewrite .js → .ts the way tsc does
  at type-check time.

Flip the web app's tsconfig to "allowImportingTsExtensions": true and
put the .ts extensions back. tsc now accepts the imports, and Node's
loader finds the real file on disk. No build step, no emit, and the
shared-types package stays a pure source-only TS workspace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:54:35 +02:00
Till JS
e38257b93d feat(ai): policy-gated tool executor with pendingProposals lifecycle
Adds the staging layer that turns AI-attributed tool calls into user-reviewed
proposals instead of direct writes.

- `data/ai/policy.ts` — per-tool AiPolicy (`auto` | `propose` | `deny`) with
  module-level defaults and a global fallback. `user` and `system` actors
  always bypass (they ARE the decision / are trusted subsystems).
- `data/ai/proposals/` — Proposal + Intent types, store with
  create/list/approve/reject/expire. Proposals are local-only (do NOT
  sync); the approved write syncs through the normal module path.
- `tools/executor.ts` routes by actor+policy: `auto` runs directly under
  `runAsAsync(actor, ...)`, `propose` stages a Proposal carrying rationale
  + mission metadata, `deny` refuses. `executeToolRaw` bypasses the policy
  gate — used only on the approval path where consent already exists.

Default policy is conservative: read-only and append-only self-state
(log_drink, log_meal) auto-execute; everything that mutates user-visible
records defaults to propose.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:48:53 +02:00
Till JS
d1a0d09692 feat(data): stamp actor on records and pending changes via Dexie hooks
Extends the creating/updating hooks to capture the ambient actor
synchronously and freeze it onto every write:

- `__lastActor` on each record (whole-record attribution for Workbench badges)
- `__fieldActors` parallel to `__fieldTimestamps` (field-level attribution
  for inline diff rendering — e.g. "AI changed due date, user changed title")
- `actor` on `_pendingChanges` rows so mana-sync + cross-device views can
  distinguish AI- vs user-initiated writes

Also adds `kontextDoc` to v17 (missing from schema while module was live)
alongside the new `pendingProposals` table for staged AI intents.

Actor is captured in-hook rather than at emit time because
`trackPendingChange` is deferred via setTimeout and would otherwise lose
ambient context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:48:30 +02:00
Till JS
a18506caf6 feat(events): Actor attribution on every DomainEvent
Introduces a discriminated Actor union (user | ai | system) threaded through
the event pipeline so downstream consumers can distinguish human writes from
AI-initiated ones and derived subsystem writes.

- `EventMeta.actor: Actor` is required (no legacy fallback — pre-launch)
- `emitDomainEvent` takes an options bag `{ actor?, causedBy? }`; falls back
  to the ambient actor set by `runAs` / `runAsAsync`
- `runAs` / `runAsAsync` pin the actor at defined boundaries (tool executor,
  mission runner, projection dispatcher) — primitives capture synchronously
  so ambient context is never SoT past the write moment

Foundation for the AI Workbench. Follow-up: mana-sync server must accept
and persist `actor` in pending-change payloads.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:48:03 +02:00
Till JS
2fb2bb60fb feat(kontext): singleton markdown doc with inline editor
New Mana module "Kontext" — one editable markdown document per user,
displayed as a workbench page. Toggle between rendered preview and a
raw textarea editor (Cmd/Ctrl+E); debounced autosave 500 ms.

Content is encrypted at rest (new `kontextDoc` table, singleton row).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:46:04 +02:00
Till JS
66f8e86d59 fix(web): add missing pako dep for backup import
The .mana backup parser in src/lib/data/backup/format.ts imports
inflateRaw from pako but the package was never declared. The
production Vite build fails to resolve it — pnpm let it through
locally only because some other workspace dep hoists pako into
node_modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:29:31 +02:00
Till JS
9a6ccf5076 fix(a11y): clear pre-push svelte-check warnings
- mail/ListView: add a11y_click_events_have_key_events ignore to match
  the existing a11y_no_static_element_interactions suppression
- sleep/MorningLog + companion/CompanionChat: mark intentional
  initial-value state reads with state_referenced_locally ignore
- goals/GoalEditor: add tabindex="-1" to the dialog role element

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:12:14 +02:00
Till JS
93bb94a121 fix(types): drop .ts extensions + narrow Uint8Array buffer type
- shared-types/index.ts: drop .ts import extensions (web-app tsconfig
  has no allowImportingTsExtensions; bundler resolution handles it)
- backup/format.test.ts: narrow buildZip return to Uint8Array<ArrayBuffer>
  so the Blob() constructor accepts it under strict lib.dom

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:07:22 +02:00
Till JS
73e3fdbbed feat(meditate): add meditation module with presets, sessions, breathing UI
Local-first module with meditatePresets/Sessions/Settings tables, hub
ListView with stats + recent sessions, and SessionPlayer with
BreathingCircle + MoodPicker. Route at /meditate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:50:13 +02:00
Till JS
d11f6aebf7 chore: ignore vite-plugin-pwa dev-dist output
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:50:03 +02:00
Till JS
7c1c6cd54c chore(audit): module complexity reports + workbench map
Adds four audit scripts (module health, inter-module coupling, per-function
cognitive complexity, D3 treemap) with generated reports under docs/ and
an iframe-embedded workbench app at /admin/complexity. Reports regenerate
weekly via the module-health GitHub Action.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:47:42 +02:00
Till JS
b857063120 refactor: rename eventstream -> activity, cycles -> period
eventstream was confusingly branded "Events" in the app registry,
colliding with the real events calendar module. Renamed to activity
(DE: Aktivität) since it's a live activity feed across all modules.

cycles -> period (DE: Periode) makes the menstrual-tracking module
self-describing. Tables cycles/cycleDayLogs/cycleSymptoms renamed to
periods/periodDayLogs/periodSymptoms; field cycleId -> periodId;
TimeBlockType 'cycle' -> 'period'; domain event CycleDayLogged ->
PeriodDayLogged. Generic "cycle" usages (billing, lifecycle, breath,
bicycle, import cycles) left untouched.

Constant disambiguation: prior DEFAULT_PERIOD_LENGTH (bleeding days)
renamed to DEFAULT_BLEEDING_DAYS; prior DEFAULT_CYCLE_LENGTH (28d full
cycle) is now DEFAULT_PERIOD_LENGTH.

Pre-launch, no data migration needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:45:43 +02:00
Till JS
66cda80620 feat(settings): inline BYOK key manager under AI tier card
Moves the BYOK key CRUD from the standalone /settings/ai-keys subpage
directly under the new BYOK tier card in the main AI settings section.
Users now manage keys in-context where they toggle the tier.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:42:40 +02:00
Till JS
4f33435607 docs(sync): document backup/restore pipeline + stability contract
- DATA_LAYER_AUDIT.md: new section 8 covering the export/import flow
  end-to-end — architecture diagram, .mana format, protocol-stability
  commitments we locked in pre-launch (eventId + schemaVersion + op
  vocab + tombstones-forever), encryption-boundary argument, file
  map, and the remaining backup backlog (M4b, M5, signature,
  resumable download, dedup table).
- services/mana-sync/CLAUDE.md: /backup/export row in API table with
  explicit note that it sits outside the billing gate, new Backup /
  Restore section with format sketch + split between writer.go (pure)
  and handler.go (shim), test-coverage line mentions the backup cases,
  project-structure tree lists backup/*.go, Security section mentions
  RLS still applies to the export path.

No code changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 17:48:47 +02:00
Till JS
e219d38436 test(sync): 8 vitest cases for .mana zip parser
Builds synthetic PKZIP archives in-memory (same deflateRaw the runtime
uses on the inflate side) and asserts:

- round-trip through parseBackup surfaces manifest + events + matching
  sha256
- events.jsonl iteration yields both records with fieldTimestamps intact
- wrong formatVersion is rejected with a clear error
- missing manifest.json or events.jsonl is rejected by name
- non-zip input is rejected at EOCD scan
- sha mismatch surfaces as differing manifest vs computed hash fields
- iterateEvents skips blanks + throws on malformed JSON

This is the only untrusted-input frontier in the backup flow, so it
earns a real test harness rather than relying on integration smoke.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:51:47 +02:00
Till JS
7aee552ab4 feat(sync): .mana backup import — zip parser + replay (M4a)
Client-side restore for the same-account case:

- lib/data/backup/format.ts: hand-rolled .mana (zip) parser. Walks the
  central directory, inflates DEFLATE entries via pako (already in the
  repo), exposes manifest + events.jsonl + recomputed sha256. No new
  dependency; the archive shape is narrow enough that 200 lines cover it.
- lib/data/backup/import.ts: validates manifest (userId match is hard-
  refused, eventsSha256 must match, schemaVersionMax ≤ client support),
  streams events through iterateEvents(), batches 300 per appId and
  replays via the existing applyServerChanges() path. LWW makes the
  operation idempotent.
- settings/my-data: file picker, progress bar, per-phase labels, success
  summary with event count + source timestamp.

Scope is intentionally same-account only: events originate from the
server for this user, so re-pushing them is unnecessary. Cross-account
migration needs the MK transfer path in M5.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:49:08 +02:00
Till JS
cf9f4ecd52 fix(llm): per-task tier override bypasses global allowedTiers gate
Bug: setting taskOverrides['companion.chat'] = 'byok' didn't work
when the user's allowedTiers was empty/['none']. The tier-too-low
check in run() compared task.minTier ('browser') against userMaxTier
('none') and threw TierTooLowError before the override was even read.

Same issue in canRun() and candidateTiers().

Fix: when a per-task override exists, treat it as opt-in to that tier
even if not in the global allowedTiers. The override is the user's
explicit per-task signal — overriding the global default is exactly
what an override is for.

- run(): effectiveMaxTier = max(override, userMaxTier)
- candidateTiers(task, override): adds override to baseTiers
- canRun(): now passes the override to candidateTiers

The Companion chat now correctly uses BYOK when selected from the
toolbar, even if the user hasn't enabled BYOK in their global LLM
settings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:19:50 +02:00
Till JS
e95d0487b9 perf(workbench): split SceneAppBar registration from prop updates
The single $effect that wired SceneAppBar into bottomBarStore was
re-writing barComponent on every reactive tick — every change to
openApps, locale or activeSceneId redirected through .set() and
re-assigned the component reference identically.

Add a setProps() method to bottomBarStore that mutates only barProps,
and split the workbench effect in two: a registration effect that
fires on the scenes-empty/non-empty transition, and a props effect
that pushes fresh data without touching barComponent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:13:32 +02:00