- Update rich preview cards with wider layouts, compact image sizing, and unified content expansion controls
- Allow link titles to span two lines and preserve full expanded descriptions
- Raise fetched description metadata to the standard 280-character X post limit
- Preserve encoded description line breaks while normalizing intra-line whitespace
- Add metadata tests covering line breaks and description length limits
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
- Replace separate description and image controls with one Show more/Show less toggle that collapses both elements.
- Expand rich preview text to the full available metadata and widen preview containers for readable 280-character tweets.
- Add vertical padding beside the accent border and cap preview images at 300px while preserving responsive sizing.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
- Remove the "Automatic preview from {domain}" gradient overlay on preview
images; simplify the img alt text to "Preview from {domain}"
- Drop the "· {typeLabel}" suffix (PR/issue/repo/file/link/etc.) from the
card metadata row — the provider name and logo already convey the source,
so the row now shows just the provider
- typeLabel stays in the SupportedLinkPreview model since it still powers
the accessible aria-label ("Open GitHub PR: ...")
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
- link-preview-attachment.tsx: add optional onRemove prop that renders a
small X button floating just outside the card's top-right corner
(left-full top-0, z-20 above the link trigger), fading in on message
row hover via group-hover/message and reachable by keyboard via
focus-visible
- link-preview-attachment.tsx: wrap the card in a relative container
carrying the width classes (w-80 max-w-full shrink-0) since the card's
overflow-hidden would clip a button positioned outside its bounds
- link-preview-attachment.tsx: remove the AttachmentActions block with
the hover-revealed ExternalLink icon, eliminating the reserved flex
gap on card hover
- markdown.tsx: drop the "Remove previews for everyone" text button
below the preview list and its spacing wrapper; pass onRemove to each
LinkPreviewAttachment instead, opening the same confirmation dialog
- removal flow and AlertDialog confirmation are unchanged; X only
renders when onRemoveLinkPreviewsForEveryone is available (own
messages)
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
> Opened by Brain (agent) on behalf of @wesbillman.
## Problem
Users report the desktop app doesn't reliably reconnect and can wedge in
states where only CMD+R (or a full restart) restores connectivity
(thread `c2205e2b` in #desktop-reconnecting).
Pinky's empirical light-switch matrix (real `buzz-relay`, SIGTERM/1012 +
SIGKILL × 1s/45s/3min, at `f18a9cb10`) passed 4/4 — the backoff state
machine recovers cleanly from ordinary relay loss. That isolates the
user-stuck states to four special cases a reload resets but the auto
flow never did.
## Fixes
| Gap | Change |
|---|---|
| **G1** — recovery rode solely on the backoff timer (max 30s),
throttled by WKWebView in occluded/background windows; nothing fired on
network return or wake | New `useRelayResumeTriggers`: `online`, window
focus, and visibility→visible call `preconnect()` when the session is
`reconnecting`/`stalled`, rate-limited to one attempt per 5s
(`relayResumeTriggerPolicy.ts`). Deliberately inert for the terminal
`disconnected` state. |
| **G2** — any AUTH `OK false` latched the session terminal forever,
though the relay also rejects for transient causes (duplicate-AUTH
"already authenticated" race, ±60s clock skew, fail-closed allowlist DB
errors) | New `AuthOkTracker` (`relayAuthPolicy.ts`): "already
authenticated" resolves as success; transient rejections retry with
normal backoff; latch only on `restricted:` or after 3 consecutive
rejections. |
| **G3** — an `auth-required:` CLOSED (REQ racing AUTH after reconnect)
permanently deleted the live subscription with no UI signal — frozen
channel while state reads "connected" | Reclassified `auth-required:` as
retryable in `relayClosedPolicy.ts`. Genuinely terminal classes
(`restricted:`, `invalid:`, …) still delete. Can't loop: a truly
unauthenticated session latches terminal at the connection level. |
| **G4** — `useRelayAutoHeal` observed the 2s-debounced connection hook,
so sub-2s flaps never triggered the heal even though `resetConnection`
had already rejected every in-flight query | Auto-heal now observes the
raw connection-state emitter. The existing 15s heal rate-limit still
guards against flap storms. |
Each fix is a colocated pure-policy module + unit tests, matching the
existing `relayReconnectPolicy`/`relayClosedPolicy` pattern.
## Validation
- Full desktop unit suite: **4151 pass, 0 fail** (at branch tip, `pnpm
-C desktop test`)
- `pnpm -C desktop typecheck` and `pnpm -C desktop check` clean
(file-size ratchet respected — `relayClientSession.ts` net −2 lines
despite the tracker wiring)
- Evidence trail: `RESEARCH/DESKTOP_RECONNECT_CMDR_GAP_AUDIT.md`
(audit), `RESEARCH/DESKTOP_RECONNECT_LIGHT_SWITCH_RESULTS.md` (Pinky's
matrix)
## Not covered / follow-ups
- Native macOS sleep-wake was not automated (would kill the harness
session); G1's focus trigger is the mechanism that covers wake in
practice, but a manual sleep-wake verification on a real build is
worthwhile.
- G3 terminal-CLOSED classes (`restricted:` etc.) still silently delete
subs with no UI signal — surfacing that is a separate UX decision.
- Stall-watchdog latency (60s idle + 10s check) left unchanged; G1
triggers largely mask it.
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
Co-authored-by: npub1yxv5wk0u0fh6dwt925wntn7h397jvteyj4r87ttcd9xae7n2t3lqqj9jmm <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
Co-authored-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
## Summary
- replace Buzz Term's full-app takeover with a resizable bottom dock
inside the channel content surface
- add a discoverable channel-header button plus hide and
maximize/restore controls
- create PTYs lazily and keep separate, persistent terminal workspaces
per channel
- capture immutable channel/thread context on every terminal session
## Multiple-channel behavior
The dock is a single surface, but its tabs are partitioned by channel.
Switching channels swaps to that channel's sessions without terminating
background PTYs; returning restores them. New tabs capture the currently
visible channel/thread context.
## Verification
At commit `7ca087f8e08c80528387684364a65bf4ccd6315f`:
- `pnpm --dir desktop typecheck`
- `pnpm --dir desktop test` — 4,129 passed
- pre-push repository hooks — desktop check/test, Tauri checks, terminal
Rust suites all passed
---------
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: kenny lopez <klopez4212@gmail.com>
## Problem
`SELECT id, community_id FROM channels WHERE id = ANY($1) AND deleted_at
IS NULL` is the top **Load by waits (AAS)** on the Buzz Postgres writer.
Two independent causes compound, and both are fixed here.
### 1. No index can serve it
`channels` is `PRIMARY KEY (community_id, id)`, and every secondary
index leads with `community_id`:
| Index | Columns |
|---|---|
| *(primary key)* | `(community_id, id)` |
| `idx_channels_nip29_group` | `(community_id, nip29_group_id)` |
| `idx_channels_dm_hash` | `(community_id, participant_hash)` |
| `idx_channels_community_type` | `(community_id, channel_type)` |
| `idx_channels_community_visibility` | `(community_id, visibility)` |
| `idx_channels_created_by` | `(community_id, created_by)` |
| `idx_channels_ttl_expiry` | `(ttl_deadline)` *(partial)* |
The two tenant-independent lookups carry **no `community_id` predicate**
— deliberately:
- `Db::communities_of_channels` — `WHERE id = ANY($1) AND deleted_at IS
NULL`
- `Db::community_of_channel` — `WHERE id = $1 AND deleted_at IS NULL`
That independence is load-bearing, not an oversight: projecting a row's
*true* owning community regardless of the fetch query's `WHERE` clause
is what makes `Inv_NonInterference` non-vacuous. If the fetch ever
dropped its tenant scoping, this lookup would still report the real
label and the checker would catch the mismatch.
But a composite btree is only usable when its leading column is
constrained, so neither query can use the primary key, and nothing else
leads with `id`. **Both sequentially scan `channels` on every call.**
### 2. In production the result is discarded
Both call sites feed `record_read_message_rows` /
`record_read_by_id_rows`, which call `tracer.record(...)`. Production
binds `NoopTracer` (`crates/buzz-relay/src/state.rs`), whose `record`
body is empty.
The existing guard tests `trace_state`, which is `Some` for every
well-formed request — it only goes `None` on malformed pubkey bytes. So
the scan ran on the hot read path and its output was dropped. This is
the classic eager-argument bug: `log.debug("..." + expensiveCall())`
with no `isDebugEnabled()` check.
### 3. Multiplied per filter
The non-search call site sits **inside the phase-3 per-filter loop**, so
a `REQ` carrying N filters performed N sequential scans of `channels`
before responding.
## Changes
**`Tracer::enabled()`** — a capability check on the trait (the
`isDebugEnabled()` of this seam), defaulting to `true`. `NoopTracer`
overrides it to `false`, and both emitters in `req.rs` now gate on it,
skipping the trace-only DB read entirely in production.
**`migrations/0027_channels_id_lookup_index.sql`**
```sql
CREATE INDEX IF NOT EXISTS idx_channels_id_live
ON channels (id) INCLUDE (community_id)
WHERE deleted_at IS NULL;
```
- `INCLUDE (community_id)` — both queries select exactly `(id,
community_id)`, so this is covering and can be served index-only.
- Partial on `deleted_at IS NULL` — matches both predicates exactly,
excludes soft-deleted history, and lets Postgres skip the recheck.
- **Not `UNIQUE`.** `id` alone is *not* unique in this table —
`command_executor.rs` documents that `community_of_channel(channel_id)`
is ambiguous because the same channel id can appear under more than one
community. A unique index would encode a false constraint and fail to
build on any database already holding such a pair.
Worth keeping the index even though fix#1 removes the production
caller: it still runs under conformance, and `community_of_channel` has
the same problem on its own paths.
**`schema/schema.sql`** — mirrored, since a test asserts desired-state
parity.
## Conformance is unchanged
This is the part worth reviewing closely. Under a real tracer
`enabled()` returns `true` and **every emit happens exactly as before**
— the gate only skips *building* emit inputs when nothing observes them,
never an emit that would otherwise have been made. The coverage-breach
guard stays non-vacuous.
`CountingTracer` forwards `enabled()` to its inner tracer rather than
inheriting the `true` default. Both directions matter and both fail
silently:
- inheriting `true` over a `NoopTracer` would keep the overhead this PR
removes;
- hardcoding `false` over a live tracer would suppress the emits whose
absence `EmitGuard` reports as `ImplBug` — masking real breaches behind
expected ones.
Covered by a new regression test,
`counting_tracer_delegates_enabled_to_inner`, which asserts delegation
in both directions.
## Verification
- `cargo check -p buzz-conformance -p buzz-relay` — clean
- `cargo clippy --all-targets` — clean, zero warnings
- `cargo test -p buzz-conformance` — 6/6
- `cargo test -p buzz-relay --lib conformance` — 11/11
- `cargo test -p buzz-db --lib migration` — 7/7
- `just test-unit` (pre-push) — green
Migration-count assertions in `crates/buzz-db/src/migration.rs` were
bumped 26 → 27, with content assertions for 0027 following the existing
per-migration pattern (including a guard that it never becomes
`UNIQUE`).
## Open questions for reviewers
1. **Lock strategy.** Built *without* `CONCURRENTLY`, following
migration 0004's precedent, because sqlx runs each migration inside a
transaction and `CREATE INDEX CONCURRENTLY` cannot run in one. This
takes a brief `SHARE` lock on `channels` (blocks writes, not reads) —
small relative to `events`, but an operator preferring zero
write-blocking can pre-build it by hand and `IF NOT EXISTS` makes the
migration a no-op. I could not confirm whether sqlx 0.9 supports a `--
no-transaction` directive; if it does, that may be preferable.
2. **Diagnosis is static.** This comes from reading the source, not from
`EXPLAIN` against the live database. Worth confirming with `EXPLAIN
(ANALYZE, BUFFERS)` on the writer before/after — that also sizes the win
by revealing the real table size and row counts.
3. **Expected impact** scales with average filters-per-`REQ`, which I
did not measure. `pg_stat_statements` ordered by `total_exec_time` would
confirm this query drops off the top and show whether anything else is
scanning the same way.
Signed-off-by: Jemiah Westerman <jemiah@squareup.com>
Replace the stale `agent_command_override` drop logic in
`apply_persona_snapshot` with a three-tier canonical command resolver.
## What this fixes
The old code dropped a create-time harness pin when the persona switched
to a different runtime, but it had two failure modes:
1. **Preset harnesses invisible.** `known_acp_runtime_exact()` only
searches `KNOWN_ACP_RUNTIMES` (builtins). Preset harnesses such as
OpenClaw live in `PRESET_HARNESSES`, so the destination lookup returned
`None` and the outer `if let` branch never executed — a Goose→OpenClaw
persona switch left the stale Goose override in place, keeping the agent
running Goose instead of OpenClaw.
2. **Pin-side canonical resolution incomplete.** The pin was resolved by
`known_acp_runtime()`, which searches by id/command/alias and returns a
`&KnownAcpRuntime` entry correctly. However, if the *pin* named an alias
(e.g. `claude-code-acp`) and the *destination* was a preset harness
absent from builtins, the outer guard still failed for the same reason
as (1). The alias regression test pins the requirement that the
canonical resolver must handle both sides: alias pins must be recognised
and drops must fire when the destination is a known preset.
## How it works now
`canonical_harness_command(input)` accepts any form a stored override
can take — bare command, alias, path prefix, or runtime id — and
resolves it to the harness primary command through three tiers:
1. **Builtins** — `KNOWN_ACP_RUNTIMES`, matched by id/command/alias.
2. **Static presets** — `PRESET_HARNESSES`, matched by id or normalised
command.
3. **Loaded registry** — custom/preset definitions loaded at runtime.
`command_for_runtime_id` (id-only input, same three tiers) replaces the
two-step `known_acp_runtime_exact`/`lookup_loaded_harness_by_id` pattern
in `record_agent_command`, `effective_agent_command`, and
`try_record_agent_command`, adding the static preset tier so preset
harnesses resolve correctly even without a warm registry.
## Changed files
- `discovery/presets.rs` — `preset_command_for_id`,
`command_for_runtime_id`, `canonical_harness_command`
- `discovery.rs` — re-export new functions; make
`normalize_command_identity` `pub(crate)`; refactor three
command-resolution functions to use `command_for_runtime_id`
- `custom_harnesses.rs` — `loaded_harness_registry` visibility `fn` →
`pub(super)` (needed by `canonical_harness_command`)
- `persona_events.rs` — replace two-step
`known_acp_runtime_exact`/`known_acp_runtime` + pointer comparison with
canonical-command comparison
- `persona_events/stale_pin_tests.rs` (new) — four regression tests:
Goose→OpenClaw drop, OpenClaw→Goose drop, claude-code-acp alias→OpenClaw
drop, same-harness path keep
- `persona_events/tests.rs` — `sample_record`/`sample_persona` exposed
as `pub(super)` for the new test module
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
## Summary
- Separate direct invites from link sharing with a labeled divider.
- Show the generated invite URL inline with truncation and a copy
control.
- Use shared loading feedback and a restrained copy-status resize.
## Validation
- `pnpm -C desktop exec playwright test
tests/e2e/invite-link-copy.spec.ts
tests/e2e/invites-settings-screenshots.spec.ts` (7 passed)
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
## Problem
`Command+R` (webview reload) wipes the two in-memory refs driving
sidebar channel unread badges: `observedUnreadEventsByChannelRef` and
`latestByChannelRef`. The boot catch-up REQ can only fetch events newer
than each channel's NIP-RS frontier, so thread replies that arrived
before the frontier was passively advanced (the common case) are never
re-discovered.
Inbox is unaffected because it rebuilds candidates from a relay feed
query and checks fine-grained `thread:`/`msg:` markers. The sidebar
badge path lacks an equivalent recovery mechanism.
## Solution
Persist the sidebar's per-event candidate set to localStorage as a
disposable, versioned projection cache
(`buzz-observed-unread.v1:<relay>:<pubkey>`) and hydrate it on boot
before the catch-up REQ runs.
### New files
**`observedUnreadStorage.ts`** — storage module for the cache:
- Keyed
`buzz-observed-unread.v1:<normalizedRelayUrl>:<normalizedPubkey>`
(relay-scoped to prevent cross-community leakage, matching
`threadActivityStorage`)
- Stores validated per-event `ObservedUnreadEvent` rows;
`latestByChannel` is derived at hydration — no divergent dual aggregate
- Age pruning (7d = `READ_STATE_HORIZON_SECONDS`), per-channel cap
(1000), global cap (5000) across all channels in a scope bucket
- Payload `updatedAt` for LRU ordering; registered in
`PURE_CACHE_KEY_PREFIXES` for 2 MiB eviction budget
- Field-level validation on decode; write failure is non-fatal
(session-only degradation)
- Snapshot-owning timers: `scheduleObservedUnreadWrite` deep-clones the
events map at schedule time — a late A-scope timer can never read B's
mutable refs or write under B's key
**`useObservedUnreadPersistence.ts`** — hook that owns all persistence
lifecycle:
- Scope fence: `normalized pubkey + normalized relay` identity;
`isScopeLoaded()` callback guards both projection (`rawUnread`) and
every **observed-cache mutation** (`recordUnreadEvent`, `removeChannel`,
`clearAll`) before touching refs or storage. Note: stale-scope calls to
`markChannelRead`/`markAllChannelsRead` can still affect
`forcedUnreadRef` and NIP-RS markers, which are pre-existing on `main`
and deferred to the NIP-RS arc (see Deferred below).
- Synchronous `pagehide` flush closes the Cmd+R timing gap
(`useReloadShortcut.ts` reloads within 500ms of teardown, before the
1-second debounce fires)
- Identity-reset effect: flushes old scope, resets refs, hydrates from
storage, stamps loaded scope — all atomic; cleanup flushes on unmount
- `clearAll` cancels the pending timer, resets both in-memory refs, and
clears storage in a single transactional operation; `removeChannel`
deletes the channel from both refs and replaces any pending snapshot
with the current full map — never cancel-without-replacement, preserving
sibling-channel events on reload
- Marker-prune effect on `readStateVersion`: evaluates each retained
event with `observedUnreadEventReadAt()` (the same evaluator used by the
projection memo) and removes covered events, rederiving per-channel
latest — never clears a whole channel for a single thread/msg marker
- Returns a stable `useMemo`-wrapped API object keyed on actual deps so
unrelated re-renders do not restart the catch-up REQ
- `isScopeLoaded` is a `useCallback` (not a memoized boolean) — always
reads the ref at call time, never stale
### Modified files
**`useUnreadChannels.ts`** — hook integration:
- Calls `useObservedUnreadPersistence` with all persistence wired
through the returned API
- `rawUnread`: `isScopeLoaded()` guard suppresses A-scope refs from
projecting under B
- `recordUnreadEvent`: `isScopeLoaded()` fence before touching refs;
schedules a debounced write on each successful record
- `markChannelRead` clearObserved path: calls `removeChannel` so the
cleared state survives reload
- `markAllChannelsRead`: delegates to the owner's fenced `clearAll` —
the parent does not reset the observed refs directly; `clearAll` owns
the transactional clear of both refs and storage, preventing a stale
scope-A callback from corrupting scope B
**`localStorageQuota.ts`** — registers `buzz-observed-unread.v1:` in
`PURE_CACHE_KEY_PREFIXES`
## Design constraints
The cache is a **disposable projection**: versioned key, read-through
only, safe to delete wholesale. It does not touch `ReadStateManager`,
marker semantics, or `forcedUnreadStore`. Zero overlap with the NIP-RS
manual mark-read/unread protocol work in progress in another channel;
migration path when that lands is "stop reading the key."
## Test coverage
**`observedUnreadStorage.test.mjs`** covers storage primitives:
- Key normalization, relay-scoped isolation, round-trip correctness
- Age-prune and per-channel cap on read and write; global cap across
channels
- `deriveLatestByChannel` correctness
- Thread-marker prune leaves sibling thread events persisted and lit
- Scope-isolation state machine: A rows visible in A, absent in B,
restored on A again; late A-scope write does not overwrite B's bucket
- Malformed structures/fields, relay/pubkey isolation, quota failure
degradation
**`useObservedUnreadPersistence.test.mjs`** exercises the real hook via
`createRoot` + `act`:
- pagehide flush: event recorded within debounce window survives reload
(headline regression)
- Unmount with pending write flushes before teardown
- `clearAll` cancels pending debounce so no resurrection after reload
- `removeChannel` replaces pending snapshot so sibling channel B
survives reload (two-channel repro)
- Marker prune: thread and channel markers prune covered events; sibling
channels survive
- `isScopeLoaded` returns false before identity-reset effect commits,
true after
- A→B scope switch: pending A-timer is cancelled by flush, A data
persisted synchronously (hydration round-trip)
- Stale `clearAll` from scope A rejects after scope B loads
(observed-cache scope fence)
- Stale `removeChannel` from scope A rejects after scope B loads
(observed-cache scope fence)
- API object identity stable across unrelated re-renders (catch-up
stability)
**`useUnreadChannels.test.mjs`** exercises the full parent-to-owner seam
with real hook mounts:
- Stale `markChannelRead` from scope A does not corrupt B's observed
bucket after flush
- Stale `markAllChannelsRead` from scope A does not overwrite B's bucket
after flush
## Deferred
Issues deferred to the NIP-RS arc (`#unread-messages-ux`) or future
hardening — not regressions introduced by this PR:
- **Stale-scope `forcedUnreadRef` / `markContextRead` exposure**: a
stale scope-A `markChannelRead` or `markAllChannelsRead` still deletes
B's `forcedUnreadRef` entries and advances B's NIP-RS markers via
`markContextRead` before the observed-cache fence rejects. This is
pre-existing on `origin/main` (identical shape at lines 316/330). Fix
requires touching `forcedUnreadStore` and marker paths — out of scope
for Fix A. Deferred to the NIP-RS work.
- **`isScopeLoaded` empty-scope hardening**: `isScopeLoaded()` returns
`true` when `pubkey` and `relay` are empty strings (no active session).
A guard could assert non-empty identity before stamping scope-loaded.
Low risk in practice since the hook is only mounted after auth, but
could be tightened.
- **Catch-up batch scheduling**: `handleChannelMessage` and the catch-up
loop each clone the full events map per event via
`scheduleObservedUnreadWrite`. For channels with large backlogs this
produces O(n) snapshot clones per catch-up batch. A batch-schedule API
(single snapshot at end of batch) would reduce allocations. Not
observable in normal use; deferred as a performance optimization.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
The "Restart required" badge reports that an agent's running config has
drifted from its spawn-time config, but never says what changed. This
ships the full feature: a typed Rust diff engine and a TS/UI layer that
renders it at every badge site.
## Rust core (spawn-snapshot diff engine)
Replaces the lossy `u64` `spawn_config_hash` with a typed
`SpawnConfigSnapshot`. The snapshot is stamped from the already-resolved
command/env/config values immediately before `spawn()`, closing the race
window where a mid-spawn config edit would suppress the badge.
`SpawnConfigSnapshot::canonical()` is the single JSON projection shared
by the badge and the diff. Drift is `to_value(stamped) !=
to_value(current)`; the diff is a generic leaf walk over those same two
values, so badge-on and diff-non-empty are structurally guaranteed.
Adding a snapshot field reaches the UI with no code change to the diff
engine — `mutation_table_covers_every_serialized_field` fails CI if a
new field arrives without a mutation row.
`eligible_restart_diff(persona_orphaned, Option<TrackedSpawnState>)`
returns the final vector — snapshot walk entries plus a synthetic
`adapter_availability` entry. It returns empty for an orphaned instance
(spawning one would fail) and for agents with no tracked spawn state
(never stamped, can never have drifted). `needs_restart =
!restart_diff.is_empty()` derives from that vector and nothing else.
Redaction policy (`policy_for(path)`) is shared by the wire diff and the
snapshot's manual `Debug` via `is_safe_to_reveal()` from
`managed_agents::env_vars` as the single authority for env-key masking:
| Policy | Paths | Rendering |
|---|---|---|
| `Text` | `system_prompt`, `team_instructions` | character counts only
|
| `MaskedBare` | `args`, `relay_url` | `••••`, no suffix |
| `MaskedSuffix` | non-allowlisted `env.*` | `••••` + last 4 chars when
longer than 8 |
| `Plain` | allowlisted `env.*` (`BUZZ_AGENT_THINKING_EFFORT`,
`BUZZ_AGENT_PROVIDER`, `BUZZ_AGENT_MODEL`, `DATABRICKS_HOST/MODEL`) and
everything else | verbatim |
Default-deny: every env key not in the explicit allowlist stays masked.
`is_safe_to_reveal()` is the single allowlist authority for both the
baked-env display and the diff.
`restart_diff` is omitted from the wire when empty
(`skip_serializing_if`).
## TypeScript / UI layer
New `restartDiff.ts` module defines `RestartDiffEntry`, `RestartChange`,
`JsonValue`; `tauri.ts` and `types.ts` re-export and add `restart_diff`
/ `restartDiff` fields (Rust omission → `restartDiff: []`).
**`RestartDiffBadge`** — hover tooltip capped at 6 entries + "and N
more", `asChild` span trigger (never inside a `<button>`), auto-restart
blurb below the diff list (on/off variant from `autoRestartEnabled`
prop; same `AUTO_RESTART_ON_BLURB` / `AUTO_RESTART_OFF_BLURB` constants
shared with the Runtime-tab banner). **`RestartDiffList`** renders the
full uncapped list for the Runtime-tab banner with `tooltip`/`inline`
presentation variants for correct foreground in both surfaces.
**`ManagedAgentRow` B4 fix** — badge moved to a sibling `div` of the row
expansion button; tooltip trigger has no `button` ancestor.
**`UnifiedAgentsSection`** — both badge sites render
`<RestartDiffBadge>` instead of a raw `<Badge>`, with
`autoRestartEnabled` threaded from `agent.autoRestartOnConfigChange`.
**Side-panel fix** — `RestartDiffBadge` rendered tab-independently in
the `ProfileSummaryView` hero area (was Runtime-tab only — root cause of
the ~50% inconsistency Will reported). Hero badge is `self-center` in
the flex column. `ProfileRuntimeTabContent` early-return checks
`needsRestart` so the banner is never dropped when all other content is
empty. Auto-restart blurb in the Runtime-tab banner uses the shared
constants.
## Wire shape
```jsonc
"restart_diff": [
{ "field": "model", "change": { "kind": "value", "before": "gpt-5", "after": "claude-4" } },
{ "field": "system_prompt", "change": { "kind": "text", "before_chars": 1234, "after_chars": 1410 } },
{ "field": "env.OPENAI_API_KEY", "change": { "kind": "masked", "before": "••••bc12", "after": "••••xyz9" } },
{ "field": "env.BUZZ_AGENT_THINKING_EFFORT", "change": { "kind": "value", "before": "medium", "after": "high" } }
]
```
`added`/`removed` occur only for dynamic-map keys; nullable struct
fields always serialize as `null`; arrays are atomic leaves (`args`,
never `args.0`).
## Tests
**Rust** — 1902 passing: snapshot mutation coverage, diff entry
serialization, allowlist-aware env masking
(`allowlisted_env_key_shows_plain_value`,
`allowlisted_env_key_is_case_insensitive`,
`non_allowlisted_env_key_stays_masked`),
`unstamped_agent_yields_no_badge_and_no_entries` (both orphan values),
`summary_without_drift_omits_restart_diff_from_the_wire`,
`unstamped_availability_is_not_drift`. Clippy clean, fmt clean.
**TypeScript** — `needs-restart-screenshots.spec.ts`: 11 E2E cases
registered in the smoke project — all three badge sites, tooltip +
keyboard focus, DOM no-button-ancestor assertion, 6+1 truncation,
uncapped Runtime list, unknown field humanisation, side-panel badge on
default Info tab, inactive/friendly-error Runtime opening path.
Consolidates [#3652](https://github.com/block/buzz/pull/3652)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary
- Keep selected sidebar rows regular by default; manually unread rows
become bold immediately.
- Apply a clearer dark-mode hierarchy: standard inactive rows at 75%,
muted rows at 45%, and unread rows at full emphasis.
- Keep hover text color stable while retaining the selected-row and
unread cues.
## Validation
- `pnpm typecheck`
- `pnpm build:e2e`
- Playwright: sidebar badge and channel-mute coverage
## Screenshots
Posted in the PR comments.
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
## Why
Buzz restores cached channels and messages before profile lookups
complete. On launch, that briefly exposes pubkey-derived labels in place
of familiar display names.
## What
- Persist a bounded, relay-scoped cache of last-known display names,
NIP-01 names, and NIP-05 handles
- Seed batch profile queries from those labels immediately, while
keeping them stale so the existing relay request revalidates them
- Keep cached data presentation-only: avatars and ownership metadata are
not persisted or used to seed profile-detail caches
- Remove cleared or missing profiles, purge a relay's labels when its
community is removed, and include the cache in local-storage quota
recovery
- Add unit coverage for parsing, bounds, eviction, malformed data, and
cleared profiles
- Add an E2E regression that delays the relay profile response and
verifies the cached name is rendered first
## Risk Assessment
Low. The cache is disposable, capped at 1,000 entries per relay, scoped
by normalized relay URL, and always revalidated. It contains only public
label fields and does not restore avatars, agent ownership, or
authorization state.
## Verification
- `just ci`
- `pnpm typecheck`
- `pnpm test` — 3,727 passed
- `pnpm exec playwright test tests/e2e/channels.spec.ts --grep "cached
profile labels"` — passed
Generated with Codex
## Overview
Agents running in Buzz have no built-in awareness that each channel is
an isolated conversation context. When a human mentions work "you" are
doing in another channel, the current session can misread this as its
own active context and try to coordinate, re-plan, or take ownership of
it — causing confusion and wasted turns.
## What changed
Added a `## Session Model` section to
`crates/buzz-acp/src/base_prompt.md`, inserted immediately after the
opening paragraph and before `## Buzz CLI`. The section explains:
- Each channel is a separate session; multiple sessions of the same
agent identity may be active simultaneously.
- Sessions share core memory, workspace, and relay — but not
conversation context or in-flight reasoning.
- Cross-channel work belongs to the owning session by default; the
current session may take it over only when the human explicitly requests
it.
No runtime code changes. Base prompt only.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Amends `docs/nips/NIP-AM.md` with three normative publisher-behavior
changes per the cleared Usage v2 plan (plan v3, D4 + D2').
## Changes
### 1. Cache emission semantics (D4)
Replaces the unconditional `MAY` with qualified obligations:
- Publishers SHOULD emit `cacheReadTokens` / `cacheWriteTokens` when the
provider exposes a cache component.
- Publishers MUST preserve an explicit zero when the provider reports
zero.
- Publishers MUST omit the field (never null or fabricated zero) when
that component is unavailable to the publisher — including when the
provider supports it but the harness does not surface it.
An explicit carve-out in both the JSON comment block and the
Numeric-validity prose exempts these fields from the payload-wide null
guidance. Omission is the only valid representation for an unavailable
cache component.
### 2. Optional `pricingIdentity` field (D2')
Adds an optional, non-nullable `pricingIdentity` object (`authority`,
`model`, `cacheClass`), defined as billing authority — distinct from the
transport `Provider` enum.
- `authority` is a registered billing-namespace identifier: exact
lowercase hostname, no scheme, no path, no trailing slash. Registered
values: `api.anthropic.com`, `api.openai.com`, `openrouter.ai`. The set
extends only by NIP amendment. Pricing lookup is an exact string match
on `(authority, model)`.
- Present only when the publisher can prove applicability: direct
official-endpoint connections prove via the actually-requested resolved
model; other routes MUST receive response-supplied authoritative billing
identity.
- MUST omit for custom/overridden base URLs, gateways (unless the
gateway is the named billing authority), unresolved aliases, and turns
where usage contributions carry more than one billing identity
(including identity-bearing mixed with unresolved).
- `cacheClass` is omitted (not null) when not applicable.
- `pricingIdentity` is optional but not nullable — omission is the only
absence representation.
- The existing `model` field retains its non-billing semantics
(configured/session model) and is never overloaded.
- Consumers MUST treat omission as "price unknown" and MUST NOT infer a
price from the session `model` field.
### 3. Consumer cost guidance (D4)
- Consumers MAY recompute cost estimates using the billing identity and
a pricing manifest.
- Consumers MUST retain the provenance of any cost value (e.g.
`manifest-estimated`, `wire-reported`).
- Consumers MUST NOT merge manifest-estimated and wire-reported costs
into an unlabeled total.
Manifest-vs-wire display preference is application policy and
deliberately excluded from this NIP.
## Scope
Doc-only. Single file: `docs/nips/NIP-AM.md`.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary
- Keep the Welcome composer prompt above the dock blur so it stays
readable.
- Remove blur from the prompt and persona-motion paths.
- Cover the crisp, correctly layered banner in the onboarding browser
test.
## Validation
- `pnpm -C desktop exec biome check
src/features/channels/ui/WelcomeComposerBanner.tsx
tests/e2e/onboarding.spec.ts`
- `pnpm -C desktop build:e2e`
- `pnpm -C desktop exec playwright test tests/e2e/onboarding.spec.ts
--grep "finishing onboarding creates starter channels and focuses
welcome-everyone for a new member" --project=integration`
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
## Summary
- Make channel join/leave activity use the selected inline avatar-stack
treatment.
- Group related membership activity for one hour and preserve
profile/overflow-name interactions.
- Restore the virtualized day-divider handoff and align the sticky date
behavior with the message timeline.
## Validation
- `pnpm check`
- `pnpm test`
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml`
- Visual desktop screenshot captured with seeded membership activity
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
## Summary
- send desktop messages immediately while media uploads continue in
background state across channel navigation
- show immediate progress above the composer and keep Jump to latest
above it
- report the real media stages as Preparing, Processing, Converting,
Uploading, and Finishing
- use Buzz's shared spinner during local media work, then switch to the
real percentage when byte transfer begins
- animate phase-label and status-suffix changes without overlap or
layout jumps
- keep cancel, progress fill, message publication, and community-reset
behavior coordinated with the background task
- use raw Tauri IPC for large browser files so renderer-side byte
serialization does not block initial feedback
## Why
Desktop previously blocked sending while attachments uploaded in the
composer. Large videos could also pause the renderer before progress
appeared, and the progress pill said Uploading while native media
processing was still underway. This makes the initial response immediate
and describes the work actually happening.
## Validation
- `cd desktop && pnpm check`
- `cd desktop && pnpm typecheck`
- `cd desktop && pnpm test` (3,931 passed)
- `cd desktop && pnpm exec vite build --mode e2e`
- `cd desktop && pnpm exec playwright test
tests/e2e/file-attachment.spec.ts --project=smoke` (11 passed)
- focused native media tests (80 passed)
- native Clippy with all targets and features
- pre-push native suite (2,107 passed, 14 ignored; 3 diagnostics passed)
Updated phase snapshots are included in the PR comments.
Split from #4512 so the desktop and mobile changes can be reviewed
independently.
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## What
Fixes#2392 — the action cards in the empty-channel intro ("Create
agent", "Add people") had their `focus-visible` ring clipped by the
surrounding scroll container.
## Root cause
The cards sit in a `flex ... overflow-x-auto pb-1` row. Setting
`overflow-x` (without `overflow-y`) makes the browser compute
`overflow-y: auto` as well, so the container clips anything painted
outside its padding box — including the cards' `focus-visible:ring-2`
box-shadow. With only `pb-1` padding, the top/left/right of the ring
were cut off when Tabbing to a card.
## Change
`desktop/src/features/messages/ui/ChannelIntroBlock.tsx` — `pb-1` →
`p-1` on the action-cards scroll container, reserving 4px on all four
sides so the focus ring renders fully inside the scroll container's
padding box.
- 1 file, 1 line. No behavior change for mouse users or layout.
## Verification
- `pnpm typecheck` — clean
- `pnpm exec biome check src/features/messages/ui/ChannelIntroBlock.tsx`
— clean
- `pnpm check:file-sizes` — clean
- Desktop unit suite — **3906/3906 pass**
Signed-off-by: Sarthak Singh <sarthak.singh@juspay.in>
Signed-off-by: Sarthak Singh <sarthak.singh@juspay.in>
## Summary
- make mobile unread state visible with bold channel names, an animated
Inbox badge, and swipe-to-toggle Inbox rows
- add directional transitions for top-level mobile navigation
- let mobile send while media uploads, with cancellable progress UI
- normalize iOS and Android video uploads, attach poster frames, and
improve native video playback
## Validation
- `just mobile-check`
- `just mobile-test`
- `cargo test -p buzz-media`
- Pixel smoke test
- iPhone smoke test
Desktop background uploads moved to #4522 so the two platforms can be
reviewed independently.
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: leader <71e9f2c44a6932b6772caaaccda1911d010463c3e2c6c40410b8329956046801@buzz.block.builderlab.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
## Summary
- ship **Buzz Term** end to end: the terminal engine/runtime, mounted
desktop substrate, and user-visible naming
- add Quinn's tape-deck-inspired banner: a beveled chassis filled by the
`buzz term` wordmark, surrounded by a complete-hex field
- derive the wordmark's three-stop sweep from each theme's terminal
palette so primary, secondary, and accent roles remain visibly distinct
across all 62 shipped themes, including light themes
- paint the banner once on its own pointer-transparent canvas; PTY
rendering beneath it remains unchanged
## Banner behavior
- uses the renderer's shared `8.4 × 17` cell metrics and production
aspect ratio `2.0238`
- regenerates only for viewport/theme changes; palette switches repaint
correctly while the banner is visible
- dismisses on non-empty output from the active terminal session; empty
output and inactive sessions do not dismiss it
- fails closed below **70 columns** rather than squeezing or clipping
the wordmark
- adds **8 lines** to `terminalRenderer.ts` for shared cell metrics and
**zero lines inside `paint()`**
## Screenshots
| Buzz (light) | Buzz Dark |
|---|---|
| 
| 
|
| Kanagawa Lotus (light) | Red |
|---|---|
| 
| 
|
Additional production-aspect finals:
[Vesper](https://buzz.block.builderlab.xyz/media/9ca6514b63f8cfb2107a85ca46f16a940c0883848e6fbc718e411af94aa13100.png),
[Min
Dark](https://buzz.block.builderlab.xyz/media/f67bd2970e5d64ffb07b1ae78ab58c847e6ebc23e7e7a48e067eb024dba64ec8.png),
and [Dark
Plus](https://buzz.block.builderlab.xyz/media/290fee08924f37d064abc687ecf3e9526ab05b87e8e56d610f23048949793dbe.png).
The screenshot harness was checked against the shipped painter at this
exact head: all **2,541 draw calls** matched on color, glyph, x, and y;
four deliberate divergence controls fired.
## Verification at `98ebc8f9048bd5f0ceb7e843b67874d642f0b7fd`
- desktop tests: **3,946 / 3,946**
- TypeScript: clean
- checks: pass (two pre-existing informational `useTemplate` notices
only)
- integration/e2e: PASS (independent exact-SHA lane; artifacts recorded
in the originating Buzz thread)
- artifact/dead-path sweep: clean
- redteam G1–G7: PASS
- all six named banner emitter-deletion mutants die
- independent handwritten five-row full-wordmark fixture kills Quinn's
seven-mutant battery, including a one-pixel glyph change
- real `112 × 46` canvas-rect dismissal tests separately cover active
non-empty, active empty, and inactive non-empty output
- layer-drop and zero-draw painter mutants die; z-order and
pointer-events verified
- CI's `tsc && vite build` includes all three banner modules
- performance at DPR 2 (worst-case measured envelope):
- one-time content paint: **~0.7–0.8 ms**, paid only when the banner is
built or its palette changes
- busy compositor, CSS `1277 × 697`, backing `2554 × 1394`: **470–497
µs/frame** for the full banner (**2.82–2.98%** of a 60 Hz frame)
- busy compositor, CSS `1920 × 1080`, backing `3840 × 2160`:
**1,139–1,212 µs/frame** (**6.83–7.27%**)
- empty, one-glyph, and full-banner controls converge: compositor cost
follows backing-layer area and DPR rather than painted-cell count
- in the actual idle welcome state, cost is below both vsync-clamped
rigs' resolution; it is not claimed as zero
- **Pane cross-rig spread: resolved at matched loop rate.** Two
independent rigs initially differed 2.3× (58–68 vs 136 µs/Mpx of backing
store; pane, CSS 1277×697 / backing 2554×1394, DPR 2). The cause of
*that* spread is rAF loop rate: the higher figure came from a
free-running loop at ~1600fps. Throttled to ~200–236fps, both rigs read
58–68 µs/Mpx (1.25–1.44% of a 60Hz frame). The busy-composite figures
quoted above remain the **unthrottled worst case** and are conservative
by ~2.3× at the pane. Not established: the mechanism and sign of
free-running distortion (one rig under-charges ~15%, the other
over-charges 2.3×), and the 1080p figure has not been re-measured
throttled.
- the layer paints only on generation/theme/resize and dismisses on
first non-empty active-session output, so the measurable busy cost is a
short-lived worst case rather than a persistent PTY paint-path tax
## Follow-ups in this PR
These are intentionally subsequent commits after the certified
static-banner head, not claims about `98ebc8f90`:
1. close the compositor metrology: remeasure the 1080p point throttled
and characterize the opposite-sign free-running rAF distortion, with
each measurement regime stated
2. add Tyler's animated honeycomb color waves, gated by
`prefers-reduced-motion`, a full 62-theme phase-sweep contrast check,
and DPR-2 per-tick performance certification
3. land the already-proven mounted theme-switch regression probe from
`RESEARCH/BUZZ_TERM_G3A_PROBE/`
4. bound the slow/hang-shaped G1-c mutant `waitFor`
5. optionally trim the generator to its ink bounding box, reducing the
minimum viewport from 70 to 62 columns
---------
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
Co-authored-by: npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6 <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz>
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
**Category:** improvement
**User Impact:** Mobile users can sort each channel group by recent
activity or A–Z, with their choices synchronized with desktop.
**Problem:** Desktop supports persistent per-group channel sorting, but
mobile shows the same groups without equivalent controls or shared
preferences. The earlier mobile attempt coupled sorting to unsafe
dirty-state behavior that could overwrite newer cross-client changes.
**Solution:** Add mobile sorting controls and encrypted NIP-78
synchronization using the existing desktop `channel-sort` contract,
while retaining ordinary whole-blob last-write-wins behavior. Local
state is scoped by identity and normalized relay, startup closes
fetch/subscription gaps, and both clients use the same deterministic
ordering rules.
<details>
<summary>File changes</summary>
**desktop/src/features/sidebar/lib/channelSortPreference.test.mjs**
Updates ordering coverage for the deterministic, cross-client A–Z
comparison rule.
**desktop/src/features/sidebar/lib/channelSortPreference.ts**
Aligns desktop channel-name collation with mobile so synchronized
preferences produce the same visible order.
**mobile/lib/features/channels/channel_sort/channel_sort_manager.dart**
Adds encrypted relay synchronization with safe startup gap handling,
clock checks, and ordinary last-write-wins conflicts.
**mobile/lib/features/channels/channel_sort/channel_sort_provider.dart**
Scopes sort state to the active identity and community lifecycle.
**mobile/lib/features/channels/channel_sort/channel_sort_storage.dart**
Defines the desktop-compatible payload, relay-scoped cache and
migration, cleanup, and shared ordering behavior.
**mobile/lib/features/channels/channels_page.dart**
Connects sort state to the channel page.
**mobile/lib/features/channels/channels_page/body.dart**
Applies each selected order to Starred, custom groups, Channels, and
DMs.
**mobile/lib/features/channels/channels_page/sections.dart**
Adds checked Recent and A–Z actions using the existing anchored-popover
UI.
**mobile/test/features/channels/channel_sort/channel_sort_manager_test.dart**
Covers payload adoption, encrypted publication, conflicts, timestamps,
retries, and cleanup.
**mobile/test/features/channels/channel_sort/channel_sort_storage_test.dart**
Covers parsing, relay isolation, migration, cleanup, and ordering modes.
**mobile/test/features/channels/channels_page_test.dart**
Verifies the group controls expose both choices.
</details>
### Reproduction steps
1. Open the mobile channel list with populated built-in and custom
groups.
2. Open a group menu and choose **Sort: Recent**; confirm active
channels move to the top.
3. Choose **Sort: A–Z**; confirm deterministic alphabetical ordering
returns.
4. Repeat for Starred, a custom group, Channels, and DMs.
5. Open desktop with the same identity and community and confirm each
synchronized preference.
6. Switch communities and confirm cached preferences do not bleed across
relays.
### Screenshots
Approved `live` custom-section flow with `research` kept offscreen.
| Recent selected | A–Z result | A–Z selected |
|---|---|---|
| 
| 
| 
|
### Validation
- Mobile `flutter analyze` — clean
- Focused mobile sort and channel-page suites — 37/37 passed
- Desktop full suite — 3906/3906 passed
- Mobile full suite — 1034 passed, 1 skipped, 1 unrelated baseline
failure reproduced at `ac4fa13b8`
<!-- Originating Buzz channel: 2a16a2bb-6fd3-4d69-8182-2afcb21b2d14 -->
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
**Category:** improvement
**User Impact:** Mobile readers can jump directly to their oldest unread
message and return to the latest message with compact directional
controls.
**Problem:** Opening an active channel at its newest message makes it
easy to miss where unread conversation began, while moving back through
history lacks a lightweight route to the live edge.
**Solution:** Capture the channel's unread boundary when it opens, offer
an accessible up-chevron beneath the app bar to reach that stable
target, then reveal the inverse down-chevron at the bottom whenever the
reader is away from latest. Deep links retain precedence, and
live-follow, pagination, composer resizing, and explicit scroll
ownership continue to use the existing timeline behavior.
<details>
<summary>File changes</summary>
**mobile/lib/features/channels/channel_detail_page.dart**
Captures the channel's read state at open time and passes a stable
unread snapshot into the timeline before the normal deferred read update
advances it.
**mobile/lib/features/channels/channel_detail_page/message_list.dart**
Adds mutually exclusive oldest-unread and latest navigation, with
accessible icon controls positioned at opposite edges of the message
surface while preserving existing follow and deep-link behavior.
**mobile/test/features/channels/channel_detail_page_test.dart**
Covers the unread target, compact inverse controls, accessible tooltips,
and placement beneath the frosted app bar.
</details>
## Reproduction steps
1. Open a Flutter mobile channel that has unread messages without
entering through a message or thread deep link.
2. Confirm an up-chevron appears directly below the channel app bar
while the timeline remains at latest.
3. Tap the up-chevron and confirm the timeline scrolls to the oldest
message that was unread when the channel opened.
4. Confirm the unread control is replaced by a down-chevron at the
bottom of the timeline.
5. Tap the down-chevron and confirm the timeline returns to latest and
resumes following new messages.
## Screenshots
| At latest — up-chevron to oldest unread | Away from latest —
down-chevron to latest |
|---|---|
| 
| 
|
_Real iPhone 17 Pro Simulator captures from the neutral
`buzz-mobile-scroll-to` channel._
Originating Buzz thread:
`buzz://message?channel=5b16c478-22d8-4ddd-951a-6036e19b81ff&id=6a78af32d7ac6f531b182c4e70dd5a04c503a2dab2ce2c0c74b2c6baa5921741&thread=6a78af32d7ac6f531b182c4e70dd5a04c503a2dab2ce2c0c74b2c6baa5921741`
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
## Summary
- open Huddles in a focused companion window with a clean handoff back
to the in-app drawer and backing channel
- redesign the participant film strip, sidebar control, transcript
surface, and themed shell treatment
- preserve microphone and device control across windows, start agent
voice on the first reply, and show agent speaking activity in the film
strip
- give each agent a distinct session voice, beginning with the
configured default, plus compact per-agent text-to-speech and voice
controls
- enroll only agents explicitly mentioned or deliberately added through
an agent panel into the live Huddle roster
- keep temporary Huddle channels out of the sidebar unless the user
explicitly brings one into the main app
- remove Huddle-only avatar policy badges and filter short silence or
noise segments before speech-to-text posts
## Why
The previous flow exposed the temporary channel as product UI, obscured
who was present or speaking, and split transcript and audio state
between the main and companion windows. This keeps backing channels as
implementation details unless a user explicitly brings a Huddle into the
app, while sharing the live conversation and audio lifecycle across both
surfaces. Agent participants now join only after an explicit invitation,
distinct voices make multi-agent Huddles easier to follow, and short
microphone noise no longer becomes stray transcript messages.
## Validation
- `pnpm check`
- `pnpm build:e2e`
- `pnpm exec playwright test tests/e2e/huddle-transcription.spec.ts
--project=smoke` (13 passed)
- Huddle sidebar visibility unit coverage (4 passed)
- focused managed-agent and persona-mention E2E coverage (2 passed)
- `pnpm test` (3,910 passed)
- `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml
--all-targets -- -D warnings`
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml` (2,093
passed, 14 ignored; 3 diagnostics passed)
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
## Overview
**Category:** improvement
**User Impact:** Mobile users can now access consistent channel and DM
actions from both the channel list and conversation header.
**Problem:** Mobile channel menus exposed a narrower, inconsistent set
of actions than desktop, and the available actions differed by entry
point.
**Solution:** This change introduces one reusable action sheet with a
clear quick-action hierarchy, role-aware lifecycle controls,
confirmations for consequential actions, and a deliberately narrower DM
menu.
## Changes
<details>
<summary>File changes</summary>
**mobile/lib/features/channels/channel_actions_sheet.dart**
Adds the shared channel and DM action-sheet experience used by both
entry points, including Star/Unstar and Read/Unread quick actions for
channels, section movement, mute, management, inline copy actions,
guarded lifecycle actions, confirmations, and a compact DM menu without
quick actions.
**mobile/lib/features/channels/channel_detail_page.dart**
Routes the header ellipsis through the shared action sheet so the
in-channel menu matches the channel-list experience, including for DMs.
**mobile/lib/features/channels/channel_management_provider.dart**
Adds archive and delete operations using the desktop-compatible relay
event kinds and refreshes channel state after completion.
**mobile/lib/features/channels/channels_page.dart**
Makes the shared channel action-sheet entry point available to the
channel-list implementation.
**mobile/lib/features/channels/channels_page/channel_tile.dart**
Replaces the tile-specific long-press menu with the reusable action
sheet while preserving read state and section context.
**mobile/test/features/channels/channel_actions_sheet_test.dart**
Covers action hierarchy, owner/admin/member capability guards, loading
and failure states, DM narrowing with no quick-action row, and inline
copy actions.
**mobile/test/features/channels/channel_detail_page_test.dart**
Updates channel-header flows to exercise management through the new
shared action sheet.
**mobile/test/features/channels/channel_management_provider_test.dart**
Verifies archive and delete event tags stay compatible with desktop
behavior.
</details>
## Reproduction Steps
1. Run the mobile app and open a populated channel list.
2. Long-press a regular channel and verify the Star/Unstar and
Read/Unread quick actions appear above Move to section…, Mute, Manage,
Copy channel name, and Copy channel ID.
3. Choose either copy action and verify it copies the expected value.
4. Open a channel, tap the header ellipsis, and verify the same action
sheet appears.
5. As an admin or owner, verify Archive appears; as an owner, verify
Delete also appears. Confirm that lifecycle actions require
confirmation.
6. Long-press or open the header menu for a DM and verify it has no
quick-action row and starts with Mute, followed by Copy channel name and
Copy channel ID.
## Screenshots
### Channel menu
| Regular channel — Mark Unread | DM — no quick actions | Archive
confirmation |
|---|---|---|
| 
| 
| 
|
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
## Overview
The global Agent Defaults surface (Settings card, defaults modal,
onboarding) exposed structured controls for Effort but left Max Output
Tokens, Context Limit, and Max Rounds as raw env vars. Per-agent dialogs
had structured numeric fields but only for `isBuzzAgentRuntime` —
incorrectly excluding Goose. This PR unifies numeric-tuning capability
across all surfaces, fixes a pre-existing dual-editor defect, and adds
full test coverage.
## What changed
### Phase 1 — Catalog projection
- Add `max_rounds_env_var` to `KnownAcpRuntime` in `runtime_metadata.rs`
(`Some("BUZZ_AGENT_MAX_ROUNDS")` for buzz-agent, `None` elsewhere).
- Project all three numeric env-var fields (`max_tokens_env_var`,
`context_limit_env_var`, `max_rounds_env_var`) end-to-end:
`AcpRuntimeCatalogEntry` Rust struct, TS `types.ts`,
`RawAcpRuntimeCatalogEntry` + `fromRawAcpRuntimeCatalogEntry` in
`tauri.ts`, and the e2e mock bridge (`withMockRuntimeConfigMetadata`).
### Phase 2 — Field model
- `deriveAgentConfigFieldModel` now derives `maxOutputTokens` /
`contextLimit` / `maxRounds` descriptors from catalog-projected fields.
- `structuredEnvKeys(descriptors)` — exported helper that takes the
**rendered** descriptor set (not the whole model). Hidden keys follow
what is actually rendered per surface: global hides effort + all three
numeric keys for buzz-agent / two for Goose; per-agent buzz-agent hides
effort + three numeric keys; per-agent Goose hides only its two numeric
keys. `BUZZ_AGENT_THINKING_EFFORT` stays a visible generic env row
per-agent because no effort control renders there.
### Phase 3 — UI
- Extract `NumericTuningFields` from `buzzAgentModelTuningFields.tsx` as
a shared descriptor-driven component (`descriptors`, `envVars`,
`inheritedEnvVars`, `onEnvVarChange`). Kind-specific minima:
`NUMERIC_KIND_MIN` map (`maxOutputTokens`/`contextLimit`: 1,
`maxRounds`: 0) applied to `<input min>`.
- **Global surface** (`AgentConfigFields.tsx`): deduplicate the
previously duplicated Advanced env-editor block; render
`NumericTuningFields` below the env editor when descriptors exist;
`hiddenKeys` and `bakedGenericRows` exclusions use `structuredEnvKeys`
so structured keys are never double-rendered. Under 1000 lines.
- **Per-agent surfaces** (`EditAgentAdvancedFields`,
`PersonaAdvancedFields`): replace `isBuzzAgentRuntime` as the
numeric-field gate with `deriveNumericDescriptors(selectedRuntime)` from
`agentConfigCore`; hidden keys come from
`structuredEnvKeys(numericDescriptors)` — the same rendered descriptor
set, no local rebuilding (fixes pre-existing dual-editor defect).
Catalog status carried as `RuntimeCatalogStatus` (`loading | ready |
error`); both error and loading withhold structured controls and leave
saved values visible as generic rows, making error distinguishable from
"runtime not capable" (`ready` + no runtime).
- **Dialogs** (`AgentDefinitionDialog`, `AgentInstanceEditDialog`,
callers): `AgentDefinitionDialog` accepts `runtimeCatalogStatus?:
"loading" | "ready" | "error"` (replaces separate
`runtimesLoading`/`runtimesError` booleans); all call sites —
`AgentManagementDialogs`, `AgentsView`, `RequestedAgentCreateDialogs`,
`UserProfilePersonaDialogs` — compute and pass the status.
### Phase 4 — Tests
- `buildRecord` exported from `EnvVarsEditor.tsx` as a pure `(nextRows,
value, requiredKeys, hiddenKeys) => Record<string, string>` helper for
isolation testing.
- **17 new node tests** in `agentConfigCore.test.mjs`:
`deriveNumericDescriptors` (all three fields, partial, undefined
runtime, matches field-model subset); `structuredEnvKeys` per surface
including discriminating Goose per-agent effort-key invariant;
`NUMERIC_KIND_MIN` values.
- **4 new node tests** in `EnvVarsEditor.test.mjs`: hidden tuning key
preserved through generic row edits; runtime-switch then generic edit
(derives both descriptor sets, asserts new-runtime hidden key survives
`buildRecord` via `hiddenKeys` and old-runtime key survives via generic
rows); baked numeric key excluded via `filterBakedGenericRows` with
`numericTuningPlaceholder` assertion; clearing a structured override —
`numericTuningPlaceholder` verifies placeholder text.
- **5 new Playwright tests** in `agent-numeric-tuning.spec.ts` (added to
smoke project `testMatch`): global numeric fields visible for
buzz-agent; global: non-capable runtime hides numeric controls; Goose
per-agent shows `Inherit (16384)` after saving global value through the
UI; delayed catalog: saved values visible as generic rows while loading
then structured controls appear after settle; failed catalog: saved
values remain visible as generic rows (never the "unsupported" empty
state).
## Result
- buzz-agent global defaults: Max output tokens, Context limit, Max
rounds as structured inputs with `Inherit (N)` placeholders from baked
env.
- Goose global defaults: Max output tokens, Context limit as structured
inputs.
- A Goose global value surfaces as `Inherit (<value>)` in the per-agent
Goose edit dialog.
- No structured key is editable in two places on any surface; no
persisted key has zero editors.
- No `runtime.id === "buzz-agent"` comparison decides numeric-field
visibility anywhere — capability flows catalog →
`AcpRuntimeCatalogEntry` → field model → UI.
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Summary
- Refresh Share Compute with the shared agent-style model controls.
- Reveal sharing details and advanced options only while sharing.
- Remove the preview-only mesh API path.
## Validation
- `pnpm check`
- `pnpm test`
- `pnpm exec playwright test tests/e2e/mesh-compute.spec.ts`
Snapshots are attached in a follow-up comment.
---------
Signed-off-by: kenny lopez <klopez4212@gmail.com>
**Category:** fix
**User Impact:** Long custom emoji names now stay contained inside
reaction popovers and remain fully readable.
**Problem:** An unbroken custom emoji name could force a reaction
popover beyond its intended maximum width and overflow the message view.
**Solution:** Give the reaction popover a definite 288px width and allow
the complete emoji name to wrap within it without truncation or
ellipsis. Short names retain the same content and interaction behavior.
<details>
<summary>File changes</summary>
**desktop/src/features/messages/ui/MessageReactions.tsx**
Bounds the reaction popover width and allows long names to break across
lines while preserving the full shortcode.
**desktop/tests/e2e/reaction-names.spec.ts**
Covers fixed width, full text preservation, and wrapping for the maximum
supported colon-wrapped reaction name, with deterministic seeded Picsum
visual fixtures and explicit image-load waits.
</details>
## Reproduction Steps
1. Open a message with a custom emoji reaction whose name is 64
characters.
2. Hover or focus the reaction pill to open its details popover.
3. Confirm the popover remains 288px wide and the complete name wraps
within it without ellipsis.
4. Open a short-name reaction and confirm its popover remains readable
and unchanged in behavior.
## Screenshots
| Before | After |
| --- | --- |
| 
| 
|
**Short-name regression check**

## Verification
- `pnpm test` in `desktop`: 3,858 passed
- Focused reaction-name E2E with seeded Picsum captures: 2 passed
- Desktop checks and commit hooks passed
Originating Buzz channel: `f2ec9671-d78e-4cde-894c-9f4c458c7f1f`
---------
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>