Commit Graph

6 Commits

Author SHA1 Message Date
Matt Toohey
d77611dd08 feat(desktop): let users resize Project Canvas widgets
Widgets could be dragged and nudged but their footprint stayed fixed by
the package, so a dense reviews list or cramped tasks card could not be
adjusted without editing data/*.json. Resize is the same kind of direct
manipulation as drag, so it follows the same host-persisted,
revision-independent layout contract.

- `canvas.layout` gains an optional `sizes` map alongside `widgets`,
  validated like positions: bounded dimensions (16 to 100 000), the
  widget-id charset as keys, at most 256 entries, inside the existing
  64 KiB port cap. Optional keeps packages that predate size persistence
  valid — malformed layout messages count toward frame teardown.
- Size overrides persist and replay independently of positions:
  resizing a widget must not pin its position and vice versa, so a later
  package revision can still move a resized widget (and resize a moved
  one). `projectCanvasLayout.ts` sanitizes stored sizes like points,
  pruning malformed entries and folding them into the record caps.
- The template gets a per-widget resize handle: a focusable button
  (aria-label "Resize {title} widget", the label's single owner) that
  drags with the pointer and resizes with arrow keys (24px steps, 48px
  with Shift), snapping to the 24px grid and clamping at 192x144 so a
  widget cannot shrink out of reach. Saves go through the SDK's
  debounced `layout.save`, which now clamps dimensions into the accepted
  range so a save can never emit an invalid port message.
- Reset restores package sizes and clears stored overrides. The e2e mock
  package grew a resize control and had to slim its shell comment: the
  descriptor `url` data-URL cap is 8192 characters and the first version
  exceeded it by 12, failing every canvas frame activation.
- Also updates the template-harness avatar assertion broken by
  3ed98a804: with both a pubkey and a data URL the SDK now prefers the
  `./__buzz/avatar/` route, per projectCanvasSdkAvatar.test.mjs.

The base-prompt/nest-skill documentation for `layout.save({..., sizes})`
landed with 3ed98a804's doc edits; this change binds those strings in
the buzz-acp doc test.

Verified: canvas unit suites 75 passing (size sanitization, corrupt
records, `__proto__` safety, override independence, keyboard resize and
clamp, reset — bound to the real template + SDK in jsdom), buzz-acp
doc-binding test, tsc --noEmit, biome, and all 9 project-canvas
Playwright e2e tests including the extended resize/reload/reset flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-02 15:08:28 +10:00
Matt Toohey
5bad6f3b1a feat(desktop): persist Project Canvas widget positions across reloads
Widget placement was session-only: the template seeded `state.positions`
from the package defaults on every `host.init` and nothing wrote back, so
an arrangement was lost on navigating away, restarting, reloading the
canvas, an agent presentation update, answering the consent banner, and
community switching. Pan reset the same way.

Persist the arrangement host-side, keyed by (community, project,
dashboard, widget) and deliberately *not* by revision — an agent editing
the package must not throw away where the user put things, while consent
stays per-revision. Only overrides are stored, so an untouched widget
keeps following the package and a later revision can still move it.

- `canvas.layout` is a new child-to-host port message, not a
  `canvas.command`: it is direct user manipulation of host-rendered
  chrome, so it needs no capability and no consent, but it is strictly
  validated — bounded pan and per-widget coordinates (±100 000), the
  existing widget-id charset as keys, at most 256 widgets, and the
  existing 64 KiB port cap. `projectCanvasRpc` explicitly excludes it, so
  layout can never reach the broker.
- `projectCanvasLayout.ts` mirrors `projectCanvasConsent.ts`: one
  localStorage record per community+project holding a per-dashboard map.
  A write replaces its dashboard wholesale (stale widget ids self-prune),
  an empty layout deletes the entry and the last one clears the record,
  and stored dashboards (32) and record bytes (128 KiB) are capped by
  dropping the least recently written. Corrupt or oversized records read
  as no layouts; write failures degrade to package defaults next load.
  This is refreshable preference state, not a durable record.
- The host reads layouts at frame mount into `host.init` and persists
  `canvas.layout` beside the `canvas.rendered` branch. Both callbacks
  capture the binding they were built for, so a message landing while the
  host switches community or project can never read or write under the
  next binding's key.
- `sdk.layout.save({dashboard, pan, widgets})` sanitizes and trailing-
  debounces (300ms, last write wins) through the queued-until-init send
  path. The debounce is load-bearing: a held arrow key auto-repeats far
  past the host's 60-per-10s port limit, which kills the frame rather
  than failing one message.
- The template seeds positions and pan from `layouts[dashboard.id]`,
  saves after a drag end, keyboard nudge, and pan end, and its reset
  button becomes a true layout reset — package pan *and* positions, then
  a save that clears the record. Widgets move in place rather than
  re-rendering, so live subscriptions survive it. Combined with
  coordinate clamping, a stranded widget is always recoverable.
- Extract `ProjectCanvasFrame` and the Tauri command wrappers out of
  `ProjectCanvasHost.tsx`, which the new wiring pushed to 1035 lines,
  past the 1000-line file-size gate.

Tests: protocol coverage for the widget cap, non-finite and out-of-range
coordinates, malformed widget-id keys, and unknown fields; new storage
tests for round-trip, wholesale replace, deletion, corrupt/oversized
records, both caps, and a widget named `__proto__`; three template
harness tests driving the real template and real SDK — a persisted
override reaching `data-world-x/y` and the transform, a nudge burst
producing exactly one debounced message carrying only the moved widget's
delta, and reset clearing to empty overrides and default pan; and a
Playwright spec binding the host seam end to end (arrange, reload into a
new load id and revision, reset, reload). Each was checked falsifiable:
dropping the init merge, the reset's position restore, or the host's
persisted `layouts` fails the matching test.

Verified: desktop unit suite 5923 passing, Tauri lib suite 3077 passing,
buzz-acp prompt tests passing, canvas e2e specs 9 passing, and clippy -D
warnings, rustfmt, tsc, biome, px-text, and the file-size gate clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-02 12:32:18 +10:00
Matt Toohey
380cc506e1 feat(desktop): let Project Canvas widgets send direct messages
Add a dm.send command to the canvas widget SDK so a widget can deliver a
message to a user's DM as the viewing user, behind a new consent-gated
app.dm.send capability.

- Protocol: app.dm.send joins the capability schema and the consequential
  (consent-required) set in projectCanvasProtocol.ts, mirrored in the Rust
  manifest validator's ALLOWED_CAPABILITIES. The consent banner names it
  "send direct messages as you" and now joins three-plus capability
  phrases as a serial list instead of chaining " and ".
- Broker: dm.send takes strict {pubkey, message} params — 64-hex
  recipient, message capped at 2,000 chars and rejected when blank after
  trimming — and dispatches to a new sendDirectMessage delegate with the
  recipient lowercased. Like the existing `user` open target, the
  recipient is any valid pubkey rather than a host-source row, because
  legitimate recipients come from people.lookup/people.search results,
  which are not host sources; containment is the per-revision consent
  gate, the 10-per-minute command budget, and a host toast on every send.
- Delegate: useProjectCanvasBroker captures tenant scope (normalized
  relay URL + signer pubkey) before the first await so the backend fails
  closed if the community or identity changes mid-send, opens the DM via
  useOpenDmMutation (which also upserts the conversation into the
  sidebar), and sends over the HTTP path because a freshly opened DM has
  no live subscription yet. ProjectChannelHome feeds the community relay
  URL through a new relayUrl parameter.
- Host: success toasts "Canvas sent a direct message"; failures title as
  "Canvas direct message failed" instead of borrowing the task-update
  title.
- Template: the support dashboard's bug reporter now delivers the report
  as a DM to the project owner (resolved via project.metadata) when
  app.dm.send and project.metadata.read are granted, showing "Report
  sent"; without the capability it stages the report exactly as before.
  The bundled manifest requests app.dm.send.
- Docs: base_prompt.md and nest_skill.md document dm.send, its params,
  and the third consent capability; the buzz-acp doc-binding test now
  asserts both `dm.send` and `app.dm.send` so the prompt cannot drift.

Tests: new broker coverage for the capability gate, param bounds, blank
and oversized messages, unknown keys, and recipient normalization; the
external-template harness drives the real bug reporter through the SDK
(metadata query -> dm.send command -> success copy) and pins the staged
fallback when the capability is missing; protocol tests cover the
granted/consent/effective sets with the new capability; the Rust
manifest test accepts the full eight-capability set.

Verified: desktop unit suite 5898 passing, Tauri lib suite 3077 passing,
buzz-acp prompt tests passing, canvas e2e spec 7 passing, and clippy -D
warnings, rustfmt, biome, tsc, and the file-size gate all clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-01 16:30:27 +10:00
Matt Toohey
61226c68be feat(desktop): add a live data SDK to Project Canvas widgets
Implement the Project Canvas plan: live queries, task actions, navigation,
and standard components for sandboxed widget packages.

- RPC plumbing: child-to-host canvas.query/subscribe/unsubscribe/command/
  open messages with strict schemas, tiered budgets answered as error
  results instead of frame kills (16 concurrent subscriptions, 10 commands
  per minute, 3 opens per 10s, 64 KiB port messages), and a disposal-fenced
  dispatcher (projectCanvasRpc) so late completions never post to a closed
  port.
- Data broker (projectCanvasBroker): project.metadata,
  project.channels.list, project.reviews.list, project.tasks.list/get,
  people.lookup (max 32 pubkeys, avatars inlined as data URLs up to 48 KiB
  through a 32-entry LRU), and people.search, all scoped by page-fed
  sources; live subscriptions re-push on source changes with serialized
  dedupe, and commands/opens resolve against those sources before any
  delegate runs so widgets can never widen scope.
- New capabilities project.tasks.read, project.people.read,
  project.tasks.write, and app.open in the Rust manifest validator and TS
  protocol, with per-(binding, revision) consent for the consequential
  pair: an approve/deny banner gates them, denial degrades the canvas to
  read-only, and widget-initiated writes surface toasts.
- Task actions: NIP-34 status events (kinds 1630-1633) via the previously
  unused buildGitStatusTags, tasks.setStatus/assign/unassign mutations, and
  useLiveProjectWorkItems keeping issues and reviews fresh over a relay
  subscription with backoff, reconnect overlap, and debounced invalidation.
- Navigation: canvas.open targets channel/user/task/review behind app.open;
  ProjectChannelHome routes them and remounts the workspace sheet with the
  selected item.
- SDK runtime served at /__buzz/sdk.js and /__buzz/sdk.css before every
  package script: data.query/liveQuery/command, app.open, capabilities(),
  and ui.avatar/reviewRow/channelRow themed by --buzz-* variables. The
  bundled template's activeChannels and reviews widgets now render from
  liveQuery instead of fixture snapshots, a tasks widget adds Mark done/
  Reopen/Assign to me, and the SDK surface is documented in base_prompt.md
  and nest_skill.md.

Tested: desktop unit suite 5895 passing (new broker, rpc, protocol,
template, and issue-template tests), Tauri lib 3076 passing, buzz-acp
prompt tests, canvas e2e spec 7 passing with new capability-badge and
consent assertions, and clippy, fmt, biome, tsc, px-text, and file-size
gates all clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-01 14:45:32 +10:00
Matt Toohey
3eb9a239bb feat(desktop): add channel project features and project canvas
Move projects onto the channel-first stack: a channel can opt into
project features and surface them as tabs alongside chat, instead of
projects living as a separate top-level surface.

Desktop:
- Channel project features are opt-in per channel, with a feature bar,
  settings pane, and tabs for the canvas, resources, repos, reviews, and
  meetings; project home redirects to its channel
- Sidebar nests project channels under a project disclosure group, adds
  move destinations, and keeps section storage in sync with moves
- Extract the join-channel banner and the channel pane main column, and
  pull sidebar scroll lock and unread DM previews into their own modules
  to stay under the file-size ratchet
- Survive unmounted editor views and listener teardown races via
  mountedEditorView and safeUnlisten

Project Canvas:
- Sandboxed local canvas packages served over a Tauri custom protocol,
  with manifest validation, path security, and snapshot storage that
  supports pending/committed widget updates and revision pruning
- React canvas host and surface plus a shared protocol module
- Bundle a project-canvas-template resource with demo dashboards,
  widgets, and styles

CLI and agent surface:
- Add `buzz canvas notify` to tell the local desktop that a widget's
  presentation or data changed; it runs locally with no relay connection
- Update the ACP base prompt and nest skill docs for canvas authoring

Also promotes Projects out of preview-features.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-09-01 10:19:37 +10:00
Tyler
61ba9dfaa0 refactor(voice): extract reusable Pocket primitives + Pocket voice settings (relands #2467 + #3208) (#3910)
Relands **#2467** (extract `buzz-voice` crate) and **#3208** (Pocket
voice settings) onto main, after #3266 and #3180 merged.

## Why a fresh PR
The repo is squash-only with delete-branch-on-merge. Squashing #3266
deleted `jtennant/pocket-tts-2026-04`, which was #2467's base — GitHub
auto-closed #2467 and it cannot be reopened. Squash merges also sever
ancestry, so GitHub's natural merge-base reports phantom conflicts for
the whole remaining stack.

## Content provenance
- Byte-identical to the blessed `jt/buzz-voice-refactor` branch
(`93029c577`, tree `6729e0eff` — reviewed by Dawn (#2467) and Max
(#3208) at exact heads) **except** the three files where #3180 and #3208
genuinely interact.
- Three-file resolution (union of both sides):
- `huddle/mod.rs` — #3180's pipeline re-exports + #3208's
`agent_tts_routing` imports.
- `huddle/state.rs` — `reset_preserving_generation` preserves both
`huddle_generation` (#3180) and `tts_enabled` (#3208); test sets merged
into one `tests` module.
- `desktop/src/testing/e2eBridge.ts` — both switch arms kept; no
duplicate case labels.

## Verification at cf32dacd7
- `cargo test` (desktop/src-tauri, pinned 1.95.0): **2047 + 3 pass / 0
fail** (14 ignored: 8 keychain, 4 real_relay, 2 flag-gated)
- `cargo clippy --all-targets -- -D warnings`: clean; `cargo fmt
--check`: clean
- `cargo check --workspace` (root, includes new `buzz-voice` member):
clean; `cargo test -p buzz-voice`: 5/0
- `pnpm test`: **3885 / 0**; `tsc --noEmit`: clean; lint: clean

The 3180×3208 interaction resolution is getting an independent team
re-review before merge.

Buzz channel: buzz-desktop-voice `fd5fb402-b651-4238-89b1-bb3e2fa4dc96`,
thread `b4798ecc`.

Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
2026-07-31 09:41:09 -04:00