Skip to content

tighten the MCP server, fix the UX blockers, add a notification center and site analytics - #39

Merged
OrellBuehler merged 5 commits into
mainfrom
chore/site-analytics-and-ux
Aug 30, 2026
Merged

tighten the MCP server, fix the UX blockers, add a notification center and site analytics#39
OrellBuehler merged 5 commits into
mainfrom
chore/site-analytics-and-ux

Conversation

@OrellBuehler

Copy link
Copy Markdown
Owner

Five commits that had piled up locally on main, pushed together.

  • d5389bb tighten the MCP server
  • 53ad658 close the gaps the MCP review left open
  • 4367c93 fix the seven blockers from the UX review
  • d4997a2 add a notification center so a toast can be read after it is gone
  • 029eb81 add umami analytics to the site

The last one is the only change to site/: a [params.analytics] block in
hugo.toml and a deferred script tag in baseof.html, guarded by
hugo.IsProduction so hugo server does not log local page views. Umami is
cookieless and self-hosted, so there is no consent banner to add.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qi7Fe5GC1JgRsok7HLVP7o

Fixes, from a read of the whole tool surface:

- snap_to_beats returned `timeline()` where every other read uses
  `working_timeline()`, so inside a task it handed back the user's
  untouched cut next to a `cuts_aligned` count of moves that were not
  in it.
- timeline_summary promised "any per-track gaps" and returned none.
  It now reports them, head gap included — a hole is black picture,
  which an agent never sees for itself.
- download_speech_model filled the cache but never selected the model,
  so naming one and then analyzing still transcribed with the old one.
  Add set_speech_model, the write side of transcription_status.
- core_err mapped everything to internal_error; a stale id or an
  out-of-range value is the caller's mistake, and invalid_params is
  what tells a model to fix its arguments instead of giving up.
- Clamp the sizes a model picks out of a schema description
  (waveform buckets, frame widths), the way skim_asset already does.

Optimizations:

- Build the tool router once. #[tool_handler]'s default router
  expression is re-evaluated by call_tool, list_tools and get_tool, so
  every request rebuilt all 85 routes — ~250us of release-build work
  per tool call for an identical result.
- platform_check resolved the cut summary twice (two timeline
  deserializes, two asset queries) for one answer.
- Release the project lock before emitting `project-changed`. The event
  makes the GUI re-fetch and that re-fetch takes the same lock, so the
  notify belonged outside. An `edit()` helper does the whole shape once
  and takes the lock/mutate/notify boilerplate out of 61 tools.

- export now reports progress and can be cancelled. A render runs for
  minutes; an agent could neither tell a slow export from a hung one
  nor abandon settings it already knew were wrong. Both ride the
  protocol — the client's `progressToken` and the cancellation token
  rmcp trips on `notifications/cancelled` — and a cancelled render
  deletes its half-written file from inside the blocking job, since
  cancelling the request can drop the handler future.

- Add import_asset, so an agent can load media rather than only
  rearranging what it was handed. It deliberately does not stage: a
  file on disk is not an edit to the user's cut.

- set_mask no longer swallows a failed timeline read. `.ok()` made a
  read that failed indistinguishable from "this clip has no mask yet",
  and silently reset every field the caller did not name.

- set_speech_model emits `speech-model-changed`, which the webview
  listens for. The GUI reads transcription status only at launch, so a
  model an agent picked stayed invisible in the picker until restart.
Each one was the UI saying or doing something that wasn't true.

- The Inspector was gated on a clip being selected, so its Text overlays
  section — which belongs to the timeline, not to any clip — was
  unreachable until you clicked one. It is now always mounted (it already
  had an empty state) and toggled from the toolbar, so closing it is also
  how you get the width back.
- The timeline's transition badge read `crossfade ? 'Crossfade' : 'Dip to
  black'`, mislabelling nine of eleven kinds. `transitionLabel()` already
  existed, unused.
- "Drop media to start" now does: `+page.svelte` listens for Tauri's
  drag-drop event, filters by the same extension list the picker uses and
  runs the same import.
- The export dialog opened on Web 1080p, so a 9:16 project opened its
  export already landscape and the readiness panel warned about the frame
  the user had just chosen. It now opens on the project frame.
- The agent panel showed a green "live" dot whether or not anything was
  connected. A streamable-HTTP client holds no connection between calls,
  so `agent_status` reports when an agent last spoke to the endpoint
  (stamped in `lock_agent` and in `get_info`) and the panel judges from
  the age: connected / away / nothing yet.
- Import ran a full analysis per asset with no way out — a model download
  and then minutes of inference each. Analysis is now cancellable end to
  end: `CancelFn` beside the `ProgressFn`, checked between steps, polled
  about once a second inside the ffmpeg whisper run, and per chunk during
  the model download (keeping the `.part` file so a retry resumes). The
  status bar names the step, says what is queued behind it, and stops it.
- `editor.error` was recorded and never rendered, so a `.kerf` that would
  not open opened as silence. It is a dismissible banner now, and
  `editor.loading` shows in the status bar.
Toasts vanish in four seconds. That is fine for "Clip copied" and useless
for "the speech model could not be downloaded: <reason>" — the notice you
actually needed to read, and there was nowhere to read it afterwards.

Every toast now also writes to a notification log
(src/lib/notifications.svelte.ts). Components import `toast` from there
instead of from svelte-sonner — a drop-in wrapper, so no call site
changed; a toast that isn't recorded is one that can be lost, so the swap
is whole-app. The title bar's bell opens the log (All / Unread /
Problems, per-row read toggle, mark all read, clear) and badges the
unread count, red when something unread actually failed. Errors and
warnings also linger longer on screen than sonner's default.

The log is deliberately not replayable: a toast's "Undo" action is
dropped rather than kept, since an hour later it would undo whatever the
newest revision is, not the edit the notice was about.

And a log only helps if the notice is raised at all, so the failure paths
that used to reject into nothing now report: a speech model download that
fails, an asset the import queue could not analyze, and the media bin's
two `void ui.runAnalysis(...)` calls.
Cookieless, self-hosted at a.orellbuehler.ch, so no consent banner. Gated on
hugo.IsProduction — `hugo server` runs the development environment, and local
page views are not traffic.
@OrellBuehler
OrellBuehler merged commit bec8523 into main Aug 30, 2026
23 of 24 checks passed
@OrellBuehler
OrellBuehler deleted the chore/site-analytics-and-ux branch August 30, 2026 20:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant