mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
d5d422d9eeebe6fadd21b60037bd98480200f6cc
1146
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fd0ab47a1b |
fix(link-preview): refetch a link when it re-enters the composer (#5510)
### Overview **Category:** fix **User Impact:** When a user re-pastes (or finishes typing) a link that previously failed to load a preview, the composer now refetches it immediately and can never send a snapshot preview built from the old, stale metadata. **Problem:** The link-preview cache is shared with passive message-list scroll, so a URL that resolved to a negative result (a hard `null` miss or a transient fetch failure) stayed cached and re-usable. Re-pasting that exact link into the composer served the stale negative and never refetched. Worse, the stale metadata was still `snapshotReady`, so a fast clear-then-repaste could attach a **stale snapshot preview tag** to the sent message — a preview that no longer matched the link. **Solution:** A freshly-entering link is forced to refetch, and the composer is fenced against ever shipping a tag built from pre-re-entry metadata. This closes three distinct races surfaced over successive review passes: (1) the shared negative cache being reused on re-entry; (2) the resolver's debounce swallowing a fast clear+re-paste so the re-entry was invisible and the stale tag stayed sendable; and (3) an in-flight media upload started from the stale metadata publishing its tag after fresh metadata had already arrived. Healthy cached hits are never touched (instant card, no redundant fetch), and passive message-list scroll — which never opts in — keeps riding the shared cache exactly as before. <details> <summary>File changes</summary> **desktop/src/shared/lib/useResolvedLinkPreviews.ts** Adds a loader `invalidateNegative(href)` that drops a cached negative result (resolved `null` or transient fail) while leaving healthy hits and in-flight promises alone, and a `refetchNewNegatives` option that invalidates each newly-present href's negative entry before the peek/load loop reads the cache. Also adds an optional `liveHrefs` input so newness is judged against the caller's LIVE (undebounced) content — a debounce-swallowed leave/re-entry of the same URL still counts as new. Because the hook retains its own resolved metadata (the render that scheduled the effect already read the stale negative from it), it also clears its OWN negative key for every re-entered href, so the link renders as pending until the fresh load wins. `buzz://` entity links are skipped (they resolve off the relay, not this cache). **desktop/src/features/messages/ui/useComposerLinkPreviews.tsx** Opts the composer into `refetchNewNegatives` and feeds it the live hrefs. Detects a same-URL re-entry at render time (React batches the empty→repaste renders, so an effect keyed on the live set never observes the transition), then blocks the re-entered href until the resolver's forced refetch visibly cycles through pending: its stale ready tag is dropped from state and excluded from the sendable output until a fresh result re-tags. Only the sendable negative case (`fallback`) is blocked; a healthy (`image`) re-entry keeps its instant card. Adds a per-href upload generation token (`uploadsRef` becomes `Map<href, generation>`): a live re-entry bumps the generation, the upload effect's dedup guard and completion are generation-aware, so an in-flight upload from stale metadata cannot publish its tag after settling and a fresh upload can start even while the superseded one is still in flight. **desktop/src/shared/lib/useResolvedLinkPreviews.test.mjs** Adds resolver-level regressions: `invalidateNegative` drops a cached miss (next load refetches) but preserves a healthy hit (no redundant fetch); transient failure → URL removed → re-entered renders pending/not-`snapshotReady` until a successful retry; and the retained-negative + shared in-flight-fetch + re-entry interleaving clears the local negative regardless of the shared entry's shape. **desktop/src/features/messages/ui/useComposerLinkPreviews.test.mjs** Adds composer-hook regressions driving the REAL hook through the hostile gestures: a fast clear+re-paste inside the debounce window drops the stale tag and holds Send pending until a fresh tag carrying the newly-fetched media lands; and a stale in-flight upload held across the clear+re-paste and fresh-metadata resolution cannot publish its pre-clear tag, while a fresh upload starts and its tag wins. </details> ### Reproduction Steps 1. Paste a link whose preview fails to resolve (force a transient fetch failure) so the composer shows a blank/collapsed card. 2. Clear the composer and re-paste the same link (quickly, within the ~350ms debounce window). 3. Observe the preview refetches immediately rather than reusing the stale negative result, and Send stays disabled until a fresh tag lands. 4. Send the message and confirm the attached preview tag reflects the fresh fetch, never the stale pre-clear metadata. 5. Confirm passive message-list scroll of already-resolved links still shows cards instantly with no extra fetches. ### Notes Scope grew across three review passes from the original single resolver opt-in into a full defense against shipping stale snapshot tags on link re-entry — see the scope-adjustment comment on this PR for the detail. Stacked on #5245 (`tho/link-preview-snapshot-race`), whose rewrite of `useComposerLinkPreviews.tsx` is the sole overlapping file. The transient-retry work stays in #5502, which touches no composer file and remains based on main. --------- Signed-off-by: Taylor Ho <taylorkmho@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> |
||
|
|
5acb930821 |
feat(desktop-messages): render compact Buzz permalink chips (#5638)
**Category:** improvement **User Impact:** Buzz channel, message, repository, pull request, and issue links now open reliably and display recognizable context in the desktop app. **Problem:** Buzz links could appear as raw or ambiguous URLs, and navigation links received during startup or community transitions could be dropped before the UI was ready. Repository and issue shares in particular required hover context to understand at a glance. **Solution:** Queue desktop channel/message navigation until the UI is ready, then render bare Buzz permalinks as icon-prefixed chips with concise entity context while preserving user-authored Markdown labels as ordinary links. <details> <summary>File changes</summary> **desktop/src-tauri/src/deep_link.rs** Adds validated channel-link parsing and a deduplicated, acknowledged queue so navigation survives frontend startup. **desktop/src-tauri/src/lib.rs** Registers the pending-navigation state and commands with the desktop application. **desktop/src/features/communities/useCommunityInit.ts** Resets queued navigation safely across community boundaries without leaking stale destinations. **desktop/src/features/messages/lib/channelLink.test.mjs** Covers valid, malformed, and canonical channel permalink forms. **desktop/src/features/messages/lib/channelLink.ts** Defines strict parsing and detection for `buzz://channel/<uuid>` links. **desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs** Extends composer-node coverage for normalized Buzz link content. **desktop/src/features/messages/lib/composerMessageLinkNode.ts** Keeps composer link-node handling aligned with the expanded Buzz link surface. **desktop/src/features/messages/lib/remarkChannelDeepLinks.test.mjs** Verifies bare channel URLs become renderable deep-link nodes without touching code. **desktop/src/features/messages/lib/remarkChannelDeepLinks.ts** Transforms eligible bare channel links into dedicated Markdown nodes. **desktop/src/features/messages/lib/remarkEntityLinks.test.mjs** Covers bare repository, pull-request, and issue detection and code-span exclusions. **desktop/src/features/messages/lib/remarkEntityLinks.ts** Adds dedicated Markdown nodes for bare Buzz project entities. **desktop/src/shared/deep-link.test.mjs** Exercises queued navigation, acknowledgement, serialization, and community-switch behavior. **desktop/src/shared/deep-link.ts** Serializes pending deep-link drains and acknowledges destinations only after successful navigation. **desktop/src/shared/styles/globals/markdown.css** Aligns permalink icon geometry and spacing with agent mention chips. **desktop/src/shared/ui/markdown.test.mjs** Adds integration coverage for every permalink chip, authored labels, fallbacks, icons, and static rendering. **desktop/src/shared/ui/markdown.tsx** Routes channel and entity nodes through the shared presentation path while preserving authored link text. **desktop/src/shared/ui/markdown/BuzzLinkChip.tsx** Introduces the shared interactive/static permalink chip and authored-label inline-link components. **desktop/src/shared/ui/markdown/ChannelDeepLink.tsx** Renders channel shares and references with Hash icons, names, and shortened-ID fallbacks. **desktop/src/shared/ui/markdown/MessageLinkPill.tsx** Renders ordinary message shares with message icons and channel/message context while retaining sent-from-thread behavior. **desktop/src/shared/ui/markdown/entityLinks.tsx** Maps repositories, pull requests, and issues to Projects-aligned icons and contextual labels. **desktop/src/shared/ui/markdown/nodeCache.ts** Includes entity-link rendering in cached Markdown node handling. **desktop/src/shared/ui/markdown/utils.ts** Allows validated channel links through the Buzz URL transform. **desktop/src/shared/useMessageDeepLinks.ts** Drains queued navigation links safely and clears them during teardown. **desktop/src/testing/e2eBridge.ts** Extends the mock bridge with pending-navigation command behavior. **desktop/tests/e2e/community-rail.spec.ts** Verifies queued links do not cross community boundaries. **desktop/tests/e2e/navigation.spec.ts** Covers channel/message deep-link navigation during startup and active sessions. **desktop/tests/helpers/bridge.ts** Adds reusable deep-link mock state and acknowledgement helpers. </details> ## Reproduction steps 1. Run the desktop app and open a channel containing bare `buzz://channel`, `buzz://message`, `buzz://repo`, `buzz://pr`, and `buzz://issue` URLs. 2. Confirm each bare URL renders as one cohesive chip with a type icon, a useful name or shortened identifier, and no duplicated channel `#` character. 3. Add an authored Markdown link such as `[design discussion](buzz://issue?...)` and confirm the supplied label remains an ordinary link rather than becoming a chip. 4. Select channel and message links and confirm they navigate correctly in warm and cold-start states. ## Screenshots / demos Houston dark theme with custom purple accent (`#a855f7`), captured from rebased visual implementation `ad411cc06`; current head `0aafa144f` only adjusts E2E expectations for the visible mention-label behavior shown here. **Composer — channel, message, repository, pull request, and issue pills**  **Message list — channel, message, repository, pull request, and issue pills**  --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> |
||
|
|
17d2147eca |
Fix video comment effect wrapping (#5748)
## What changed - render video-review timecode chips inside the first Markdown paragraph so comment text wraps naturally around them - reuse the canonical video-review chip treatment across the timeline, Inbox previews, and Inbox detail - preserve video-review context in Inbox so timestamp chips remain interactive ## Why Video comments now support Markdown-like effects, but non-player surfaces rendered the timestamp beside a separate text layout. That kept the chip and comment from sharing the same inline flow and made Inbox behavior inconsistent with the player. ## Validation - `pnpm --dir desktop check` - 100 focused Markdown, timecode, video-review, and Inbox unit tests - `pnpm --dir desktop build:e2e` - focused `video-attachment.spec.ts` Playwright scenario - pre-push desktop typecheck and 4,761-test desktop suite - native Builderlab staging with the configured profile Focused timeline and Inbox snapshots will be attached in a PR comment. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz> Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz> |
||
|
|
caa64b5e8f |
feat(desktop): one relative date ladder across chat and the Inbox (#3769)
Slice 6 of #2216. Independent of #3642 — cut from `main`, no shared files in conflict. ## Why Five surfaces formatted the same thing five ways, and none of them matched the writing standard's Today / Yesterday / weekday / date progression. | Surface | Before | |---|---| | Chat day divider | `Monday, March 31st` — ordinal suffix, which the standard says to avoid | | Inbox section header | `Yesterday`, but never `Today`; always printed the year | | Inbox list row | A third implementation | | Inbox thread pane header | `Jul 8, 2026, 2:34 PM` — always absolute, always with the year, never relative at any distance | | Channel message header | `9:05 AM` — a bare clock, so a message from last week has nothing to anchor it once its day divider scrolls away | There were three separate date implementations doing this, which is the symptom worth naming: **two different jobs were being solved ad hoc at each call site.** A header that labels a *group* of items needs a different label than an individual item's own timestamp. ## What `shared/lib/datetime.ts` owns both ladders: ``` formatDayGroupLabel formatItemTimestamp (day divider, section header) (list row, message header) Today → Today withTime:false withTime:true Yesterday → Yesterday 2:34 PM 2:34 PM 2–6 days → Monday Yesterday Yesterday at 2:34 PM this year → June 20 Monday Monday at 2:34 PM older → June 20, 2025 Jun 20 Jun 20 at 2:34 PM Jun 20, 2025 Jun 20, 2025 at 2:34 PM ``` ## Two deliberate deviations from the standard Both are documented at the definition, not just here. **The oldest band keeps the day.** The standard collapses anything over ten months to month-and-year (`Aug 2022`). A group label has to *identify* its day — collapsing would give every day in a month the same divider, so scrolling old history would show a run of identical headers with no way to tell one day from the next. Only the year is conditional. There's a test asserting three consecutive 2022 dates produce three distinct labels. **Roomy surfaces keep the time of day at every band.** `Yesterday at 9:05 AM`, not `Yesterday`. This is a chat and collaboration workspace rather than a transactional product — where you read conversation, the time is content, not chrome. Narrow list rows still drop it (`withTime: false`) and rely on the existing hover tooltip, which stays the absolute value. `withTime` is a surface decision, not a preference. Today needs no date word in either mode: a bare clock already reads as today, and "Today at 2:34 PM" is longer without saying more. ## Derived rather than captured `MessageTimestamp` now takes only `createdAt` and derives both of its labels, instead of receiving a pre-formatted `time` string. A relative label captured when the message list was formatted would be frozen at that wording; deriving it means each render recomputes. This does not make it live — `MessageRow` is memoized, so a row already on screen when the clock passes midnight keeps saying "Today" until something re-renders it. The day divider above it has always had the same property, and both correct themselves on the next message, scroll, or navigation. Called out in the component doc so the next person doesn't read "derived" as "reactive". The memo comparator moved from `message.time` to `message.createdAt`. Behavior-identical — `time` was a pure function of `createdAt` — but it now names the prop the row actually reads. The 36px continuation hover gutter stays clock-only. A relative label doesn't fit in `w-9`. ## Middot between metadata segments `managed by you 9:53 AM` ran two unrelated facts together as if they were one phrase. Now `managed by you · 9:53 AM`. - `aria-hidden` — punctuation for the eye only. The header already reads as separate nodes to a screen reader, and `MessageAgentOwner` supplies its own "Agent managed by" label. - Grouped with the segment it precedes, so it can't wrap to the start of a line on its own — as loose siblings in a `flex-wrap` row, an orphaned divider is exactly what happens. - No margin; spacing comes from the container gap. - **No separator after the author name.** "Alice 9:53 AM" already reads as a name followed by a time. Dividers go between metadata segments only. Middot is already the app's separator for this — `MessageThreadSummaryRow`, the mention list, project rows, 46 files in total. Applied to the channel message header, channel system rows, and the Inbox thread pane. Left-side Inbox activity rows deliberately unchanged. ## Verified Screenshots taken through `just desktop-screenshot`: - `#agents` — `nadia 🤖 managed by you · 10:20 AM`, and the `Today` divider with clock-only rows - Inbox thread pane — `alice 🤖 owner unavailable · 12:00 PM` **Gap worth naming:** every mock channel message is same-day, so the past-day labels (`Yesterday at 9:05 AM`, `Jun 20 at 2:34 PM`) are covered by unit tests rather than by a rendered screenshot. Happy to add a spec that seeds an older `created_at` if a reviewer wants to see them. ## Validation - `pnpm check`, `pnpm typecheck` — clean - Unit: **3800/3800**, including 17 new tests in `shared/lib/datetime.test.mjs` and 4 in `messageTimestampContract.test.mjs` The datetime tests pin the things that are easy to regress: Today/Yesterday as *calendar* boundaries rather than 24-hour windows (a message 15 hours old across midnight is "Yesterday"; one 22 hours old on the same day is "Today"), the weekday band bounded at both ends so a future timestamp from clock skew never gets labelled with a past weekday, no ordinals across all the tricky days (1/2/3/11/12/13/21/22/23/31), the year omitted within the current year, and compact labels staying ≤12 chars for a narrow row. - Smoke E2E: **783 passed, 2 failed, 1 skipped** Both failures are pre-existing and unrelated, confirmed by re-running each against a clean tree: 1. `video-attachment.spec.ts:223` — fails deterministically on clean `main` 2. `community-rail.spec.ts:797` (keyboard drag-and-drop reorder) — flaky on clean `main`: 2/5 failures there vs 3/5 with this branch, i.e. noise ## Mobile Mobile had the same divergence, so it moves with desktop rather than drifting until the next pass. `mobile/lib/features/channels/date_formatters.dart`: | Before | After | |---|---| | `formatDayHeading` → Today / Yesterday / `Tuesday, March 31, 2026` | Today / Yesterday / `Tuesday` / `March 31` / `March 31, 2025` | | `formatThreadSummaryLastReplyTime` → `on May 19th` | `on May 19` | Same two departures from the standard as desktop, documented at the definition and cross-referenced to `datetime.ts` so the next person editing one finds the other. Day comparison also moved to a rounded start-of-day difference, so a DST transition counts as one calendar day rather than zero — Dart's `Duration.inDays` truncates. **Message timestamps stay clock-only on mobile.** Desktop message headers now read `Yesterday at 9:05 AM`; mobile keeps `9:05 AM` at every band. That's the compact side of the same surface split the desktop change makes — a mobile timestamp sits inside a chat bubble on a narrow screen with the day divider a short scroll away, where a date word costs width it doesn't earn. Recorded as a decision at `formatMessageTime` so it doesn't read as an oversight. Mobile needs no middot work: message headers have no "managed by" segment, and the mention suggestion list already uses `\u00b7`. Validation: `dart format` clean, `flutter analyze` no issues, `flutter test` **911 passed, 1 skipped** — 8 new day-heading tests covering the weekday band, the year boundary, ordinals across 1/2/3/11/12/13/21/22/23/31, distinct labels for consecutive days in the oldest band, and calendar-day rather than 24-hour bands. ## Out of scope - **Search results.** `SearchResultItem.tsx` and `TopbarSearch.tsx` hand-roll a `5m ago` elapsed format. That's a third *kind* of label — elapsed rather than relative-calendar — and deciding whether search should switch is a separate call. - **`formatThreadSummaryLastReplyTime`** keeps its own "3 hours ago" elapsed scale on both platforms; only its old-reply fallback lost the ordinal (`on May 19th` → `on May 19`). - **Mobile search.** `relativeTime` returns `7/31/2026` past a week, matching the desktop search format that's also out of scope above. Both should change together or not at all. --------- Signed-off-by: Clay Delk <clay.delk@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
17977814d3 |
fix(desktop): amortize observer journal eviction with a low-water mark (#5808)
Refs #5718. ## What happens `appendAgentEvents` evicts the per-agent live observer journal back to *exactly* `MAX_OBSERVER_EVENTS`: ```ts const trimmed = sorted.length > MAX_OBSERVER_EVENTS; const final = trimmed ? sorted.slice(sorted.length - MAX_OBSERVER_EVENTS) : sorted; ``` Once an agent's journal reaches 3000, `current.length` is 3000 forever, every later append makes `sorted.length >= 3001`, and `trimmed` is `true` on every call. That permanently disables the incremental-fold gate: ```ts if (allAtEnd && !trimmed) { /* incremental fold */ } else { transcriptByAgent.set(key, buildTranscriptState(final)); } ``` So every steady-state append then replays the whole retained window through `buildTranscriptState`, which is itself O(streamed-text) because streaming chunks fold as uncapped string concat. Nothing shrinks `eventsByAgent` except a store reset, so the state is permanent for the life of the renderer process, per agent. At ~90 frames/min an agent crosses the cap in ~33 minutes; from then on live CPU escalates (issue receipts: 188x on a headless ingest, renderer CPU climbing to 119% of a core after five minutes idle). This is not an off-by-one — a cap of 3000 does want `>`. The defect is that trimming *to* the cap re-arms eviction on the very next append, and eviction is what forces the replay. ## Fix Evict to a low-water mark below the cap: ```ts const OBSERVER_EVENTS_LOW_WATER = Math.floor(MAX_OBSERVER_EVENTS * 0.9); ``` The journal still never exceeds `MAX_OBSERVER_EVENTS`; it now has to be refilled by ~300 ordinary appends before the next eviction, so one replay is amortized across the appends that refill it. Retention semantics (newest-N at trim time) and the derived transcript are unchanged. The mark is a **fraction of the cap** rather than a fixed count so the math stays correct if the cap is ever made per-agent — a fixed headroom could exceed a smaller cap and drive the slice length negative. ### Eviction floor Low-water eviction leaves headroom below the cap, and the dedup set is built only from the *retained* array — so once eviction discards the oldest frames, the journal no longer remembers them. A relay reconnect replaying a pre-eviction frame (normal relay behavior, and the reason the dedup set exists) would be re-admitted into the headroom, and a later refill to the cap would then trim away up to 300 legitimate retained events with **no new activity** — a bounded display-window loss plus rebuild churn that partially defeats the amortization. To close that, each agent carries an **eviction floor**: the ordering key of the newest event eviction has ever discarded (`evictionFloorByAgent`, recorded at trim time as the entry just below the retained window). `appendAgentEvents` rejects any arrival at or before the floor (`isObserverEventAfter`, so an equal key is rejected — the floor event itself was evicted); a stale-only batch returns `false` with no rebuild and no notify. Out-of-order frames *newer* than the floor are still admitted via the rebuild fallback, so the fold-gate semantics are unchanged. The floor is cleared in `resetAgentObserverStore` alongside the other per-agent maps. ## Evidence `observerTranscriptRetention.test.mjs` asserts the retention window's **shape** — the observable signal for which ingest path runs, since transcript *content* is identical on both paths by design — plus boundary cases and the invariant that the derived transcript still equals a full replay of the retained window. Against the pre-fix trim-to-cap shape, three tests fail on the mechanism itself (`test_append_crossing_cap_trims_to_exactly_low_water`, `test_headroom_refills_before_next_eviction`, `test_single_batch_larger_than_cap_trims_to_low_water` — each expects headroom the old shape never leaves), and the cost shows up directly in runtime: | | `observerTranscriptRetention.test.mjs` (single-event appends past the cap) | |---|---| | trim-to-cap (pre-fix) | **429,105 ms** | | this branch | **16,221 ms** | ~26x on this workload, consistent with the 188x the issue measured on a heavier one (their events accumulate streaming text; these do not, so this understates it). Three further tests pin the **eviction floor** against reconnect replay: a replay of already-evicted frames leaves the retained window byte-identical and notifies no listener; a pre-floor frame arriving after a refill to the cap drops no retained events; and an out-of-order frame *newer* than the floor is still admitted. Deleting the floor check turns exactly the first two red while the out-of-order case stays green — confirming the tests pin the floor's rejection without over-constraining legitimate out-of-order delivery. ## Merge-order note This PR collides with #5596 (bounded renderer accumulators) on `observerRelayStore.ts` by design — #5596 refactors this exact eviction into `mergeObserverEventBatch` in a new `observerEventOrdering.ts` and adds a second, unpinned-agent tier (`truncateUnpinnedAgentWindow`, `UNPINNED_AGENT_EVENT_TAIL`). This PR merges first; #5596 rebases over it, porting the low-water cap-math **and the per-agent eviction floor** into `mergeObserverEventBatch`, and applying the same headroom to the unpinned-tier truncate (which must also record a floor when it trims). The fraction-of-cap form makes the low-water port mechanical — it feeds either the 3000 pinned cap or the 100 unpinned tail without a fixed-count underflow. ## Credits Supersedes #5767 (Chessing234's low-water-mark approach and the runtime measurements). Closes #5718. Issue receipts from the reporter, GeneralJah215 (188x headless, 119%/core after 5min idle). --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
34a7f2fb91 |
Unify agent profile content (#5788)
## Summary - remove synthetic preview runtime and configuration data so profiles show only real agent content - simplify model settings to the effective values and restore bare section icons - make owned-agent profiles resolve to the same current persona instance from every entry point ## Why Agent profiles opened from DMs or channels could fall back to a partial declared-owner view instead of the full managed-agent profile shown on the Agents page. Test preview content and configuration provenance also remained visible after the redesign. ## User impact Owned agent profiles now expose the same actions, runtime, channels, memories, and configuration regardless of where they are opened. Profiles no longer synthesize preview data, and model settings use the same simple title/value hierarchy as the rest of the panel. ## Validation - `pnpm --dir desktop check` - `pnpm --dir desktop build:e2e` - focused unit tests: 10 passed - profile entry-point integration tests: 2 passed - configuration screenshot suite: 7 passed, with six visually distinct captures Snapshots are attached in a PR comment. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Watcher <bb7abfd757d0af7b66569d02ab9c0316b616f9d0c151ecf5b964344c462e7f8f@buzz.block.builderlab.xyz> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> Co-authored-by: Watcher <bb7abfd757d0af7b66569d02ab9c0316b616f9d0c151ecf5b964344c462e7f8f@buzz.block.builderlab.xyz> |
||
|
|
43e53fc349 |
Standardize settings section layout (#5855)
## Summary - move Settings section labels outside their framed containers and centralize the spacing - apply the shared hierarchy across Appearance, Notifications, Voice, Agents, Shortcuts, Members, and Profile - give Identity and Sign out complete section treatments while removing redundant in-cell labels ## Testing - desktop pre-push checks, including 4,791 tests - focused Settings layout and sign-out Playwright coverage --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> |
||
|
|
8b8445f5ef |
fix(desktop): share one timer across same-interval useNow consumers (#5861)
## Summary Every `useNow(1000)` consumer owned its own `setInterval`. With dozens of "agent working" surfaces mounted (sidebar channel badges, tray menu, agent session panels, managed-agent rows), each ticked on its own unaligned 1 s timer — a render/composite pass per consumer per second. On a machine running ~23 agent sessions this pinned a sustained **~25% of a core** in `com.apple.WebKit.WebContent` while the app sat idle. This PR makes same-interval `useNow` consumers share one timer: all of them tick in a single `setInterval` callback, so React batches the state updates into one render pass. The last unsubscriber tears the timer down; the visibility gate (pause while hidden, snap fresh on return) is unchanged. Attribution receipts (live dev build, 23 acp sessions): the shimmer was the original suspect from `sample` stacks, but probing `animation: none` left CPU flat (~25%), while clamping `useNow` intervals dropped it immediately. Repeated A/B with this exact change: **~25% → ~3–9%** webview CPU under the same agent load (ambient variance from live agent activity; the delta reproduced across three alternations). ### Related issue None found — follow-up to the presence-firehose investigation (#5830 fixed the subscription side; this is the remaining local render cost). ### Testing - `pnpm test` — 4792/4792 pass, including a new test asserting N same-interval consumers create exactly one timer and the last unmount releases it - `pnpm typecheck`, `biome check` — clean - Live-local per TESTING.md: hot-patched into a running dev desktop with 23 active acp sessions; webview CPU dropped from ~25% sustained to ~3–9% (A/B/A alternation, `ps` sampling over 30 s windows) Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> |
||
|
|
df9e773a13 |
Scope desktop presence subscriptions to active demand (#5830)
## Summary - replace the desktop's global kind-20001 presence subscription with one author-filtered subscription derived from active TanStack presence queries - reconcile changing demand without a delivery gap: promote only after relay EOSE, keep the last confirmed subscription on failure, discard stale opens, and close entirely when demand is empty - preserve REST presence as the initial seed and TTL/crash-recovery backstop - add transport-seam and lifecycle tests for readiness, normalization, churn, retries, close failures, reconnect ownership assumptions, and disposal ## Why The desktop currently receives presence heartbeats from every identity on the relay. A live tap measured roughly 2,700 events/minute (45/sec), about 1 MB/minute and 71.5% of readable traffic, from approximately 1,300 distinct fleet identities. Most are discarded only after WebSocket, Tauri IPC, and JS parsing. This change applies normal Nostr author filtering at relay fan-out, before those costs. It deliberately does not introduce a relay digest protocol or client-side event batching; relevant-author traffic should be small after scoping, and the existing signed-delta/REST-TTL model remains intact. ## Correctness model - active query observers are the demand source; inactive cached queries retain no authors - replacement opens before old closes and is promoted only after EOSE - timeout/CLOSED rejects and closes the candidate while preserving the last good subscription - rapid A→B→C and A→B→A churn cannot unseat current A with stale B - empty demand never sends an unfiltered subscription - RelayClient continues to own reconnect replay; the reconciler does not duplicate subscriptions on reconnect ## Validation Exact pushed head: `8845093aec0330be16efe52d3459ff67f1000ff4` Pre-push hooks passed: - desktop check and file-size ratchet - desktop TypeScript - desktop unit suite: 4,791/4,791 - branch-skew check Focused lifecycle/transport suite: 34/34 passed before commit. Independent Royal Court review found and blocked two prototype flaws (timeout-as-success and starvation-prone trailing debounce); both were fixed and the final worktree was cleared with no remaining correctness or lifecycle blockers. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
5743562896 |
fix(huddle): stop 20 Hz speaker-level churn from re-rendering the whole app (#5825)
## Problem With a huddle open, Buzz Desktop becomes extremely slow and laggy (Tyler, live report, 2026-08-14). Root-caused and runtime-convicted on the instrumented rig in #buzz-conversational-agents: - The Rust playout loop emits `huddle-speaker-levels` over Tauri IPC every 50 ms, unconditionally, for the whole life of a huddle (`playout.rs` `SPEAKER_LEVEL_TICK_MS = 50`). - Each event deserializes to a fresh object, so `setRemoteSpeakerLevels` updates state at 20 Hz even in silence. - `HuddleProvider` wraps the entire main app and its context value was an inline object literal — never memoized. Every level tick minted a new context identity, re-rendering **every** `useHuddle()` consumer, including `ChannelScreen` and message rows. **Measured (A/B, silent one-participant huddle, same channel/state):** ~41 sustained ChannelScreen renders/sec unsuppressed vs ~4/sec with only the speaker-level setState suppressed — the 20 Hz path is ~90% of the load. Receipts: `driver-render-counter-unsuppressed.jsonl` / `-suppressed.jsonl` on the rig, verified independently. The same main-thread churn starves the relay client's 16 ms event-flush timer, which is the delayed/bursty message hydration and thread-panel stalls seen alongside the lag. ## Fix (minimal, no behavior change for meters) 1. **Split the high-frequency fields** (`micLevel`, `activeSpeakers`, `speakerLevels`) out of `HuddleContextValue` into a new `HuddleLevelsContext`, consumed via `useHuddleLevels()` only by the three meter components (`HuddleBar`, `HuddleRoomHeader`, `HuddleProfileControl`). 2. **Memoize the main context value** so provider re-renders no longer mint a new identity for the ~everything that consumes `useHuddle()`. 3. **Extract the mic-level analyser** into `useMicLevelAnalyser` — the level pipeline now lives in one place, and `HuddleContext.tsx` stays under the file-size ratchet (977 lines). Level meters keep their 20-30 Hz updates. Everything else re-renders only when a value it actually consumes changes. ## Acceptance bar With this fix, a silent open huddle should hold `ChannelScreen` at idle render rates (single digits/sec), and message hydration should stay live during huddles. The rig's render-counter + four-clock instrumentation can verify on this branch. ## Validation - `pnpm typecheck` clean - `biome check` clean (repo leftovers in sidebar tests are preexisting on main) - full desktop suite: **4,775 passed, 0 failed** at the final tree - file-size ratchet passes (was the reason for the analyser extraction) - lefthook pre-commit (desktop-fix + signoff) passed on commit Not yet done: live-local A/B rerun on this branch — the rig (Wren/Max) has the instrumentation ready and can convict/acquit the fix with the same probe that convicted the bug. Base: `068a83b0` (main). Co-developed with runtime evidence from Wren and instrumentation by Max. Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> |
||
|
|
eedcd886a0 |
fix(desktop): match compact link preview thumbnail corners to card shell (#5711)
## Problem In compact link preview cards with an image, the thumbnail's corners looked inconsistent — the flush left side and the interior right side read as different shapes. ## Cause The `Attachment` shell rounds its corners with a **smooth-corner (squircle) clip path** via `useSmoothCorners`, not a plain `border-radius`. In compact image mode the shell has `p-0`, so the thumbnail sits flush against its left, top, and bottom edges. That means: - **Left corners** are carved by the shell's smoothed clip path. - **Right corners** are drawn by the thumbnail's own plain `border-radius`. A circular arc and a smoothed corner of the *same* radius are different shapes (at 16px the smoothed curve starts 25.6px along the edge instead of 16px). So the two sides could never match by picking a radius value — the thumbnail's class has no effect on its left corners at all. ## Fix Give the thumbnail the same `useSmoothCorners` treatment as the shell, so both sides share one curve. - Radius token unchanged: `rounded-2xl` (16px). - The shell and the shared `Attachment` component are untouched, so no other `Attachment` consumer changes. Verified on a rendered card — thumbnail vs shell now agree on all three: arc radius (16), smoothing (0.6), and curve start (25.6px). ## Hardening The underlying issue is an invariant that lived nowhere: **a child flush against a smooth-cornered parent must share its corner treatment.** This is why the bug was easy to introduce and hard to diagnose. - Documented the invariant in `smoothCorners.ts`, where anyone reaching for the hook will see it. - Added an `expectSmoothCorners()` guard to the existing compact-preview e2e test. Confirmed it **fails** when the fix is reverted, so it genuinely bites. Note: this cannot be a lint rule — "flush" is a runtime layout fact, not visible in the source. ## Known follow-up (not in this PR) The composer link preview (`useComposerLinkPreviews.tsx`) has the same latent issue: a flush thumbnail with a hand-copied `rounded-l-2xl` that happens to match the shell's current 16px. It is correct today only by coincidence of two literals agreeing. Left for a separate PR rather than expanding scope here. ## Screenshots The same compact card and content before and after the change. | Before | After | | --- | --- | | Original `rounded-xl` (12px) thumbnail: left corners are clipped by the card’s 16px smooth silhouette while the right corners keep the thumbnail’s smaller plain radius | `rounded-2xl` (16px) thumbnail with the same smooth-corner treatment as the card | |  |  | ## Verification - `pnpm exec biome check` on all three touched files - `pnpm exec tsc --noEmit` - `node --test src/shared/ui/smoothCorners.test.mjs` — 3 passed - All 18 link-preview e2e tests pass - Guard verified to fail without the fix, then pass with it Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> |
||
|
|
068a83b097 |
feat(huddle): cut voice-turn time-to-first-audio from ~1.0 s to ~0.35 s (env-gated latency levers) (#5671)
## Overview
**Category:** feat (env-gated experiment + one exact always-on
optimization)
**Problem:** Speech-end -> first TTS audio through the desktop huddle
pipeline measures **924–1087 ms** on an M4 Max with a 0 ms LLM leg.
Voice turns feel sluggish no matter how fast the agent replies. Baseline
breakdown: ~300 ms hardcoded VAD silence flush + ~150–250 ms Parakeet
decode + ~380–550 ms TTS synthesis before the first player append.
**Outcome:** With all levers enabled, e2e time-to-first-audio measures
**347–384 ms** (307–357 ms on a longer utterance) on the same hardware,
harness, and production pipelines. Defaults preserve production behavior
everywhere except one deterministic, bit-exact cache win.
## What's in here
### Levers (all default-off, env-gated)
| Lever | Env | Effect (measured medians, short utterance) |
|---|---|---|
| Speculative Parakeet decode | `BUZZ_STT_SPECULATIVE=1` | STT leg ->
~max(flush, decode) |
| Streaming TTS synthesis | `BUZZ_TTS_STREAMING=1`,
`BUZZ_TTS_EMIT_FRAMES` | first audio 380–550 -> 211–320 ms (emit=12,
bit-exact) |
| ONNX intra-op threads | `BUZZ_STT_THREADS`, `BUZZ_TTS_THREADS` | TTS
first audio 211–320 -> 129–180 ms (4 threads) |
- **Speculative decode** starts the Parakeet decode at the *first*
silent VAD frame, overlapping it with the flush window. Resumed speech
invalidates the result (voiced-frame-count check); held silence emits it
instantly at the flush boundary.
- **Streaming TTS**: new `synth_chunk_streaming` (buzz-voice)
interleaves the Flow LM frame loop with incremental *stateful* Mimi
decoding, emitting PCM deltas to the player via the existing
`PlaybackChunkAudio` decoration. At `emit_frames=12` (the decoder's
native chunk) streamed audio is **bit-identical** to the batch path —
verified by the ignored test
`incremental_stateful_decode_matches_batch_decode` (max|diff|=0).
Smaller deltas are faster but diverge (~23 dB SNR; decoder intra-chunk
lookahead), hence the default of 12.
> **Removed after live testing:** the `BUZZ_STT_FLUSH_MS` flush-window
override. Lowering the silence window below natural mid-sentence pauses
(the fast-path recipe said 150 ms) split single spoken sentences into
multiple messages and confused the listening agents. The window is a
turn-taking quality knob, not a latency lever — it is now fixed at the
production 300 ms value.
### Push-to-talk grouping fix (always-on)
A held push-to-talk shortcut is an explicit "I am not done talking"
signal, so silence never ends the utterance while it is held — even when
the microphone is also manually open. The utterance flushes on shortcut
release (existing transmit-edge flush); a manually open mic with the
shortcut up keeps normal VAD pause flushing. Gate is the pure
`vad_flush_allowed` function with a unit-test truth table.
### Always-on (exact): voice-conditioning cache
Phase profiling (`BUZZ_TTS_PHASE_LOG=1`) showed a fixed ~160 ms
`condition_voice` Flow-LM pass on *every* chunk, re-deriving the same
post-conditioning state for the same reference voice. The state is now
snapshotted after first computation and restored per chunk (dtype-tagged
tensor copies, keyed identically to the existing `cached_voice`).
Deterministic — same tensors in, same tensors out. The default path's
TTS leg drops from 380–550 ms to 225–355 ms with no configuration.
### Bench harness
`huddle::latency_bench` (`#[cfg(test)]` + `#[ignore]`) drives the real
`SttPipeline` and `TtsPipeline`, feeding a 48 kHz WAV in real-time 100
ms batches (AudioWorklet cadence) with a configurable fake LLM in place
of the relay leg, timing speech-end -> transcript -> speak() -> first
accepted player append.
```
BUZZ_STT_SPECULATIVE=1 BUZZ_TTS_STREAMING=1 \
BUZZ_TTS_THREADS=4 BUZZ_STT_THREADS=2 \
BUZZ_BENCH_WAV=<48k f32 mono wav> \
cargo test --release -p buzz-desktop --lib huddle::latency_bench -- --ignored --nocapture
```
## Tradeoffs to weigh before promoting any lever to a default
- **Speculative decode**: the speculative buffer has ~1 silent tail
frame vs ~19; observed one CTC wobble ("fail" vs "failed") in 24 turns.
Mitigation if productionized: zero-pad the speculative buffer to match
the flush-path shape.
- **Threads**: defaults stay 1 pending the min-spec (4-core Intel) A/B
flagged in the existing `STT_NUM_THREADS` comment.
- **Streaming at emit<12** is NOT the same waveform — don't ship below
12 without an ear pass.
## Validation
- Full desktop lib suite: **2408 passed / 0 failed** at this head
(`18fab2e1c`).
- buzz-voice suite green; bit-exactness test passes against the
production batch decode.
- Defaults-only bench rerun stays in the baseline family everywhere
except the exact conditioning-cache win (stt 525–532, tts 225–355).
- `cargo clippy --workspace --all-targets -- -D warnings` + fmt clean
(pre-push hook battery green).
Measurement notes with per-lever logs: Eva's workspace,
`RESEARCH/HUDDLE_E2E_LATENCY_OPTIMIZATION_2026_08_12.md` +
`RESEARCH/HUDDLE_E2E_STT_FAKELLM_TTS_BASELINE_2026_08_12.md`.
## Suggested promotion order
1. Conditioning cache (in this PR, always-on, exact).
2. Streaming TTS at emit=12: bit-exact audio, biggest UX win — needs the
env-gate removed + barge-in soak + an ear pass on a real huddle.
3. Speculative decode with silence padding: near-free ~100–150 ms.
4. Threads: after min-spec A/B.
---------
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Signed-off-by: tlongwell-block <tlongwell@block.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz>
Co-authored-by: tlongwell-block <tlongwell@block.xyz>
|
||
|
|
c8da06c5e9 |
Speed up initial direct messages (#5658)
## Summary - avoid blocking first-DM navigation on a full channel-list refresh - publish the initial message through the acknowledged HTTP path instead of waiting on a missing WebSocket acknowledgement ## Validation - 4,715 desktop unit tests - desktop typecheck and checks - focused new-DM Playwright coverage --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz> |
||
|
|
0571f5455b |
Polish glass Huddle tray behavior (#5590)
## Summary - inset the in-app Huddle tray with four rounded corners and even 8px spacing when Glass background is enabled - keep the popped-out Huddle dock full-width - hide and suppress Glass background on Linux ## Why The in-app tray reused the opaque backing needed by non-glass windows, which covered the native vibrancy around it. Linux does not support this window treatment. ## Testing - `pnpm -C desktop build:e2e` - focused Appearance and Huddle Playwright smoke tests - pre-push desktop checks, typecheck, and 4,666 unit tests --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> |
||
|
|
514195b1d5 |
feat(desktop): add Inbox message delete action (#5779)
### What changed? Inbox message action menus now show a standalone Delete action beside Edit for manageable messages. Delete reuses the existing confirmation and targets the message whose menu was opened, while the existing empty-edit deletion path remains unchanged. ### Why? Inbox users can delete a message directly without first entering edit mode. Thread context can contain multiple messages, so the action must preserve the active Inbox selection and delete only the chosen row. ### How is it tested? Desktop checks, typechecking, builds, and test suites pass. Added tests: - [Inbox edit and delete E2E coverage](https://github.com/block/buzz/tree/main/desktop/tests/e2e/inbox-edit.spec.ts) Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Codex <noreply@openai.com> |
||
|
|
bcf353c969 |
fix(desktop): enforce agent mention authorization at send boundaries (#5681)
## Summary - allow channel-member remote/headless agents only with current kind `10100` directory evidence, while stale member identities remain hidden - fail closed while managed/relay directories load, error, or background-refetch across channel, forum, and cached autocomplete surfaces - revalidate agent mention authorization immediately before normal sends and message-edit saves, including after deferred uploads - in owner-only builds, fetch fresh authoritative profile ownership at send time and deny missing, changed-owner, or unavailable proofs - preserve human mention tags when agent authorization is revoked or unknown Supersedes #5536 because its contributor-fork head cannot be updated by maintainers. ## Validation Exact head: `7278cdd5fbcee676c7b858ea098503c62eeeff0d` - mandatory pre-push suites passed: desktop check/typecheck/tests, Rust tests, mobile tests, desktop Tauri checks, branch-skew - desktop unit tests: 4,732 passed - focused edit/ownership regressions: 8 passed - focused mention E2E: 5 passed (remote positive, stale-member negative, directory error, pre-send revocation, mid-send revocation) - file-size ratchet passed One first focused E2E batch had a timing-only miss where the send click did not emit; the isolated rerun passed. One separate pre-push attempt hit the existing randomized passphrase separator test; the successful exact-head push reran and passed the mandatory suite. --------- Signed-off-by: JDiz00 <174381550+JDiz00@users.noreply.github.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: JDiz00 <174381550+JDiz00@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
2693e0db1f |
Make workflow run history authoritative in Desktop (#5780)
## Summary - persist stable workflow run `error_code` values separately from human diagnostics - expose NIP-98 authenticated, channel-authorized run history and approval reads with stable keyset pagination - connect Desktop to those authoritative reads and return the relay-created run ID on trigger - show truthful loading, failure, and pending-trace states, and do not render approval actions from non-actionable stored hashes ## Validation - pre-push `branch-skew`, `desktop-typecheck`, `desktop-test`, `rust-tests`, `desktop-tauri-checks`, and `desktop-check` all passed on `a097dbe5f` - Desktop tests: 4,761 passed, 0 failed - `cargo check -p buzz-relay` - `git diff --check` ## Remaining gate This does not claim a relay-backed Playwright workflow journey. The browser relay bridge still routes workflow invokes through in-memory handlers; that production-shaped acceptance gate remains follow-up work before Workflows can leave preview. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz> |
||
|
|
45f4b91a36 |
fix(desktop): more compact "compact" link previews (#5629)
**Category:** improvement **User Impact:** Compact link previews now use a single-line title and smaller thumbnail, making conversations easier to scan. **Problem:** Compact previews gave long titles and oversized thumbnails too much visual weight in the message timeline. **Solution:** Keep titles to one ellipsized line and reduce image thumbnails to a 104×64 treatment while preserving the existing wide aspect ratio; Rich previews remain unchanged. <details> <summary>File changes</summary> **desktop/src/shared/ui/compact-link-preview-attachment.tsx** Tightens the Compact presentation with a single-line title and smaller wide thumbnail, leaving Rich previews untouched. **desktop/tests/e2e/messaging.spec.ts** Adds focused coverage for title overflow, exact 64px card and 104×64 thumbnail geometry, and successful decoded-image rendering using a realistic fixture, plus an optional visual capture. **desktop/tests/fixtures/github-pr-5629-og.png** Provides realistic visible image bytes for the compact-preview image-rendering E2E path. </details> ## Reproduction steps 1. Launch the desktop app with link preview style set to Compact. 2. Send a link whose preview has an image and a long title. 3. Confirm the thumbnail renders at the smaller wide size and the title truncates to one line with an ellipsis. 4. Switch link preview style to Rich and confirm its presentation is unchanged. ## Screenshot  --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> |
||
|
|
a96af89526 |
Harden shared agent instruction review (#4220)
## Summary - render shared-agent instructions as literal text so Markdown cannot conceal spoiler contents, link destinations, or image sources - reject non-reviewable Unicode controls at every agent-definition boundary while preserving legitimate rendered emoji sequences - verify shared catalog event IDs and signatures before trusting authorship, coordinates, pagination, or executable content - preserve the exact system-prompt bytes between review and execution instead of silently stripping or normalizing content ## Security rationale Shared system prompts are executable configuration. Previously, catalog prompts were projected through the chat Markdown renderer, which could hide text, replace link destinations with benign labels, and turn image syntax into remote loads. Zero-width and bidirectional controls could also make reviewed text differ from what the agent executes. This change establishes a review invariant: the prompt a user sees is the prompt the agent executes. Definitions that cannot be reviewed faithfully are rejected rather than rewritten. Catalog events must also pass Nostr ID/signature verification before they can claim a publisher, coordinate, or cursor. ## What changed - catalog instructions render as exact literal text rather than rich Markdown - catalog relay events are verified on a fresh wire-shaped object before paging, coordinate selection, attribution, or projection - forged content, pubkeys, signatures, and invalid newer heads are ignored and cannot shadow a valid signed definition - TypeScript catalog parsing rejects unsafe remote definitions before they reach the UI - shared Rust validation covers persona create/update/import, inbound relay sync, definition-less managed-agent sync, and catalog publication paths - definition-less managed agents now fail closed on local create, local update, and publication before persistence or relay retention - linked managed agents validate their local name while treating the persona definition as authoritative; their inert record-level prompt is not executed or published - names reject layout controls; prompts retain ordinary newlines and tabs - legitimate emoji composition is supported, including contextual VS16, ZWJ, skin-tone, family, flag, and keycap sequences - detached selectors/joiners, bidirectional controls, tag characters, zero-width concealment, and other default-ignorables remain rejected - names are bounded to 128 characters and prompts to 64 KiB - contributor guidance documents the byte-for-byte review requirement for future sharing paths Validation reports the offending code point and never silently removes it. ## E2E recording [buzz-shared-agent-security-e2e.webm](https://github.com/user-attachments/assets/44d6b75f-0877-490f-bda4-a716fae3f700) The recording demonstrates: - a safe definition remains visible - a prompt containing zero-width `U+200B` is rejected - a name containing bidi override `U+202E` is rejected - the prompt is preserved exactly - spoiler, link, and image syntax remains literal and does not render or load ## Verification Passed locally: - `just test`: all 10 unit and Docker-backed integration stages - desktop frontend unit suite: 4,295 tests - persona catalog relay unit suite: 32 tests, including forged-event and cursor-shadowing cases - focused Rust definition-validation coverage: 3 local create/update tests and 6 publication-filtered tests - complete desktop Tauri library suite after rebase: 2,263 passed, 14 ignored, 0 failed - desktop Tauri clippy with warnings denied and Rust formatting - complete agent Playwright spec: 34 tests - the exact formerly failing `inbox-edit` immediate-attachment smoke test after rebase: 1 test - focused shared-agent publish, literal-review, hidden-control, signature, and cross-member import Playwright coverage - desktop E2E production build and TypeScript typecheck - changed-file formatting/lint and file-size ratchet - pre-commit secret scan and DCO signoff The branch was rebased onto current `main`, which includes the upstream attachment-button label fix. Fresh post-rebase GitHub CI is green for every required and selected check: Desktop Core, all four Desktop Smoke E2E shards, both Desktop E2E Integration shards and their aggregate, Desktop E2E Relay, Desktop Build (macOS), Windows Rust, Rust Lint, DCO, security scanners, and Desktop Release Candidate. The previously failing `Desktop Smoke E2E (3)` shard now passes. The repository-wide desktop check also reports existing CSS formatting/`!important` findings in `components.css` and `terminal.css`; neither file is changed by this PR. GitHub's Desktop Core lint and format stage passes on the rebased branch. --------- Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com> |
||
|
|
c86443c599 |
perf(desktop): persist channel snapshot hash (#5684)
## Summary - persist each relay/identity's complete channel list and server hash as one integrity-checked snapshot - paint the snapshot immediately on cold boot, then revalidate with `knownHash` - fail slow-never-wrong: malformed/legacy/partial snapshots and mismatched not-modified responses force an unhashed full fetch - add sidebar boot diagnostics and deterministic unit/E2E coverage for boot, identity/relay isolation, partial writes, mismatch fallback, and community switches ## Safety invariants - channel list and hash are serialized in one localStorage document and replaced together - snapshot ownership is scoped to normalized relay URL plus identity pubkey - a not-modified response is accepted only when its hash exactly matches the hash describing the available list - any missing or impossible hash/list pairing retries `getChannels(null)` before replacing persistence ## Validation At exact commit `19ca25d23c434cc0b8893a93691aaf4c77794f60` with a clean working tree: - `cd desktop && pnpm check && pnpm typecheck` — passed (existing informational Biome findings only) - `cd desktop && pnpm test` — 4,723 passed - `cd desktop && node --import ./test-loader.mjs --experimental-strip-types --test src/features/channels/channelSnapshot.test.mjs` — 13 passed - `cd desktop && pnpm exec playwright test e2e/sidebar-snapshot.spec.ts --grep "cold boot paints" --repeat-each=5` — 5 passed - `cd desktop && pnpm exec playwright test e2e/sidebar-snapshot.spec.ts` — 8 passed - push hooks repeated desktop check/typecheck and all 4,723 unit tests successfully The Playwright suite uses injected bridge delays. Its roughly 0.5–0.6 s snapshot paint and 3.0 s snapshot-to-live readings are synthetic invariant evidence, not production desktop performance measurements. ## Measurement context The controlled current-main investigation is documented separately in `RESEARCH/DESKTOP_PERF_DEEP_DIVE_2026_08_12.md`; its raw local artifacts are `.scratch/summer-perf-deepdive-results-v2.json`, `.scratch/summer-perf-deepdive-run.log`, `.scratch/summer-perf-deepdive-run-2.log`, and `.scratch/summer-perf-deepdive-build-2.log`. Those original timings are also synthetic Chromium/mock-bridge measurements and are not presented as shipped Tauri/WKWebView or production-relay numbers. ## Latest review delta At exact tip `e8e2b1d617aac7ea008258ad9974bbf8da9cd2eb`, storage-denial reads fail open to the live fetch, hashless retries reject `channels: null` before pair/persistence updates, identity-read failure enables a hashless live fetch, and repeated consumers reuse snapshot parsing/integrity validation by storage key + raw document. The four remaining review nits are deferred as non-blocking follow-ups. Validation at this tip: sidebar snapshot E2E 30/30 serial; desktop unit suite 4,725/4,725; full push gate green (desktop check/typecheck/unit, Rust, Tauri). --------- Signed-off-by: Summer <1fdd3cc104e2911eb3b2da6f97d1b25f4a7f3550ded4492b24ff1d95acd66766@buzz.block.builderlab.xyz> Co-authored-by: Summer <1fdd3cc104e2911eb3b2da6f97d1b25f4a7f3550ded4492b24ff1d95acd66766@buzz.block.builderlab.xyz> |
||
|
|
59f613c404 |
fix(desktop): defer foreground resume work (#5696)
## Summary - defer foreground resume work until the activation task has returned, a frame has painted, and a trailing task gets a turn - centralize app-focus subscribers and remove the broad TanStack `refetchOnWindowFocus` fan-out - coalesce relay recovery and preserve an explicit deferred refresh only for workflow data without a polling/push freshness path - defer the notification permission native check while keeping blur and cheap correctness signals immediate ## Why Buzz Desktop 0.5.10 can spend roughly 1.5 seconds in the WebKit window-focus listener/microtask checkpoint before returning to the run loop. Focus currently fans out into query refetches, React polling updates, relay reconnect/replay, and native work in one activation turn. This patch establishes an interaction-first foreground boundary rather than letting those consumers compete with the activating input and first paint. ## Validation - focused foreground/workflow/relay tests: 18/18 passed before commit - `pnpm --dir desktop typecheck`: passed before commit - pre-commit desktop check and file-size gate: passed - pre-push desktop check, typecheck, and full desktop unit suite: 4,743/4,743 passed at `704e7b4b6618fafce655bb2b07c7a9fe0fc8c643` - Princess Donut independent adversarial review: PASS after two lifecycle/freshness blockers were resolved ## Manual test 1. Install the PR build and use Buzz long enough to populate channels, home, workflows, agents, and other polling surfaces. 2. Switch to another app for 30-60 seconds. 3. Return by clicking Buzz and immediately click a channel or scroll. 4. Confirm the first interaction and paint are prompt, then confirm channels/home/workflows refresh and a degraded relay reconnects after the activation boundary. 5. Repeat while rapidly switching away again to verify no resume work starts after focus has been lost. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
c6c6e7eca7 |
perf(desktop): coalesce thread-activity localStorage writes (#5693)
Each incoming thread reply drove a full `JSON.stringify` + `setItem` of the ~600 KB thread-activity buffer. A burst of replies serialized the whole blob once per event on the main thread, which is one of the renderer stalls under load in the desktop-longevity arc. This collapses the burst into a single debounced write, applying the coalescing pattern Wes introduced for read-state persistence in #5591 (`readStateManager`) to the thread-activity path. ## What changed - **`threadActivityStorage.ts`** — coalescing primitives: - `scheduleThreadActivityWrite` — first-writer-wins (a pending timer is *not* reset), 1s trailing edge. The timer reads the live buffer *at fire time* and re-checks the loaded scope, so N replies within the window persist exactly once with the burst's final state, and a write that outlives a scope switch can neither land under the new key nor persist the wrong buffer. - `flushThreadActivityWrite` — synchronous persist + timer cancel; a no-op when nothing is pending. - `removeLegacyThreadActivityKey` — idempotent one-time cleanup of the orphaned pre-relay-scoping `buzz-thread-activity.v1:<pubkey>` key. - **`useThreadActivityPersistence.ts`** (new companion hook) — owns the loaded scope, the write timer, the `pagehide` / `visibilitychange`→hidden / unmount flush, and hydration + legacy cleanup on identity/relay change. Mirrors the existing `useObservedUnreadPersistence` sibling. - **`useUnreadChannels.ts`** — rewired to instantiate the hook and call `activityPersistence.schedule(...)` at both writer sites instead of writing per event. The buffer (`threadActivityRef`) stays parent-owned; the hook decides when it is durably persisted. Net **990** lines (was 1021), back under the 1000-line ceiling. ## Durability `pagehide`, `visibilitychange`→hidden, unmount, and scope-reseed all flush synchronously, so the last burst of replies survives a `Cmd+R` or an idle reload that tears the webview down inside the coalescing window. ## Tests - `threadActivityWriteScheduler.test.mjs` — fake-timer unit coverage: burst→one `setItem`, live-buffer-at-fire-time, scope-mismatch rejection, stale-scope timer abort, flush persists+cancels, flush no-op, legacy-key removal. - `useThreadActivityPersistence.test.mjs` — mounts the real hook via `createRoot`+`act`: `pagehide` / visibility / unmount flush of the live buffer, scope switch flushing A under A's key without leaking into B, B-bucket rehydration, legacy-key cleanup, and the empty-scope write fence. ## Related Based on [#5591](https://github.com/block/buzz/pull/5591) (Wes) — `perf(desktop): coalesce read state localStorage persistence`, the proven first-writer-wins coalescing pattern this extends to thread activity. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
c3b0ccf383 |
Batch observer-store publications per relay envelope (#5680)
## Summary - preserve the ACP observer envelope through renderer ingestion - bulk-deduplicate/sort/fold one agent batch before one external-store publication - suppress publications for entirely duplicate replay batches - cover raw history, transcript, active-turn terminal behavior, and publication count ## Why The harness already publishes observer frames in one-second batches. Desktop expanded each envelope and called the global observer store once per inner frame. Each call copied/sorted up to 3,000 retained frames and woke every observer subscriber; the app-level active-turn bridge then rescanned every running/deployed agent's retained buffer. ## Representative work-count profile Controlled workload: 14 agents, 1,000 retained frames each, 24 inner frames/envelope, 10 rounds (3,360 new frames). | Counter | Before | After | |---|---:|---:| | Observer publications | 3,360 | 140 | | Aggregate retained events revisited by a representative global subscriber | 52,686,480 | 2,196,880 | Both deterministic counters fall **24×**. Node wall time was loader/JIT-noisy and is deliberately not presented as production CPU evidence. ## Validation Exact head `038a29f6f0ff866884e07bb66eebe87e576f6769`: - `pnpm --dir desktop test` — 4,718 passed, 0 failed - `pnpm --dir desktop typecheck` — passed before rebase; the rebase changed only the base and the full suite passed on the exact head - pre-commit Desktop Biome + file-size gate — passed The installed v0.5.10-block process and LocalStorage database were not restarted or modified. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz> |
||
|
|
a8e5c89e23 |
fix(desktop): preserve agent mention separator after send (#5623)
**Category:** fix **User Impact:** Typing immediately after sending to a persistently addressed agent now continues after the agent mention instead of corrupting it. **Problem:** Post-send restoration passed the persistent `@Agent ` prefix through the Markdown parser, which discarded its trailing separator and left WebKit rendering the caret at the mention boundary. **Solution:** Restore the prefix as literal ProseMirror text, preserve the separator, and focus a selection placed at the restored document end. This does not expand or otherwise change the setting’s existing scope: persistent addressed agents remain thread-only. <details> <summary>File changes</summary> **desktop/src/features/messages/lib/useRichTextEditor.ts** Adds a focused plain-text restoration helper that preserves trailing whitespace while suppressing authored-update reconciliation. **desktop/src/features/messages/ui/useMentionSendFlow.ts** Routes non-empty post-send persistent audience restoration through the literal-text helper instead of Markdown content loading. **desktop/tests/e2e/persistent-agent-audience.spec.ts** Extends the real Enter-send flow to assert the preserved separator, document-end selection, and immediate typing outside the agent mention. </details> ## Reproduction steps 1. Open a thread with a persistently addressed agent. 2. Send a message with Enter. 3. Confirm the composer restores the addressed agent and a trailing space. 4. Type immediately without clicking the composer. 5. Confirm the new text appears after the agent mention and the mention remains highlighted. https://github.com/user-attachments/assets/92f088aa-a516-48d1-acde-35e29f558f14 --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> |
||
|
|
884ed8a5d3 |
fix(link-previews): proxy sent preview media (#5627)
## Overview **Category:** fix **User Impact:** Sent link previews now reliably display their thumbnail and favicon when the media is hosted on the relay. **Problem:** Sent preview cards loaded relay-hosted snapshot media directly, so authenticated relay requests could fail even though the snapshot itself was valid. **Solution:** Rewrite snapshot media at the shared card render boundary through Buzz's authenticated local media proxy, preserving the original display domain and rerendering when the proxy becomes ready. ## Changes <details> <summary>File changes</summary> **desktop/src/shared/ui/link-preview-attachment.tsx** Routes sent preview thumbnails and favicons through authenticated relay media handling above the Compact/Rich fork while preserving original metadata. **desktop/src/testing/e2eBridge.ts** Adds an opt-in proxy-readiness seam that deterministically re-arms the production media lookup when released. **desktop/tests/e2e/messaging.spec.ts** Covers the real send, snapshot, recipient, and card-render path for Compact and Rich previews, including fallback URLs, proxied URLs, and decoded image content. **desktop/tests/helpers/bridge.ts** Exposes the opt-in media-proxy startup state to E2E tests. </details> ## Reproduction Steps 1. Send a link whose preview snapshot includes a relay-hosted thumbnail and favicon. 2. Inspect the sent message card in Compact mode and confirm both images render after the local media proxy becomes ready. 3. Switch link previews to Rich mode and confirm the thumbnail and favicon continue to render. 4. Run the focused Playwright regression: `pnpm exec playwright test tests/e2e/messaging.spec.ts --project=smoke --grep "sent link preview media uses the authenticated proxy"` ## Before / After | Before | After | | --- | --- | | Relay-hosted preview media fails to load. | The sent preview thumbnail and favicon render through the authenticated media proxy. | |  |  | Signed-off-by: Taylor Ho <taylorkmho@gmail.com> |
||
|
|
63d14a0e95 |
fix(desktop): preserve live channel timelines (#5662)
## Summary - restore the post-subscribe channel-window refresh that closes the gap left by a live subscription starting at the current second - prevent an unresolved, pageless channel window from replacing a populated timeline cache with its first live event - replace the invalid freshness-gate tests with a regression reproducing the populated cache + pageless window + first live event state from the report ## Root cause This was a data-projection bug, not a virtualized-row failure. PR #5577 skipped the post-subscribe refresh for a fresh cache even though `subscribeToChannelLive` starts at `since: now`, leaving events between the cached page and subscription establishment undiscovered. A successful but pageless companion window could then receive one live event and project that one-row overlay over the populated message cache. Reload fetched page zero and restored the conversation. ## Validation Validated exact head `bfbaefe95da5452cdda3a0b5df970eb11e44f6f8`: - focused `projectChannelWindow.test.mjs`: 9/9 passed - pre-push: branch skew, desktop check, desktop typecheck, and all 4,715 desktop tests passed - independent fresh-frame review: 9/10, no blockers ## Authorship disclosure Carl implemented and is posting this change on Wes's behalf. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
63f961c7e4 |
Refine channel settings and profile panels (#5574)
## Summary - simplify channel settings into concise detail, member, canvas, and action sections - align human and agent profiles around shared rows, segmented tabs, and top-level actions - add agent runtime presentation, sticky glass behavior, and scroll-linked action transitions ## Snapshots ### Channel settings  ### Agent info  ### Agent runtime  ## Validation - `pnpm -C desktop check` - `pnpm -C desktop test` (4,604 passed) - `pnpm -C desktop build:e2e` - focused channel settings and agent profile Playwright tests --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz> |
||
|
|
1ff98fa685 |
fix(desktop): launch Databricks OAuth from passive model discovery (#5607)
When a user's agent runtime is `buzz-agent` with no cached Databricks OAuth token, the desktop app's passive model-discovery surfaces were forbidden from launching interactive auth. Discovery failed silently, so the model dropdown showed only built-in fallback models behind a vague "Could not load live models for `databricks_v2`" note (reported internally by Nick and Jose). ## What changed Both discovery surfaces — the passive draft-form discovery and the explicit saved-model picker — now launch the browser OAuth flow, matching goose's behavior. The only behavioral difference between them is cooldown handling: - **Passive draft discovery** fires on every form-state change, so a failed, cancelled, or timed-out sign-in records a per-host cooldown (5 min) that suppresses re-popping the browser on the next keystroke. While the cooldown is active it returns the "sign-in required" guidance instead of relaunching. - **The explicit model picker** is a deliberate user action, so it always launches and clears any stale cooldown first. Safety rails: - A 150s hard timeout (`AUTH_FLOW_TIMEOUT`) bounds the whole interactive flow so an abandoned SSO tab fails discovery cleanly rather than wedging the dropdown. Success clears the cooldown; failure and timeout both record it. - `AuthCooldown` recovers from a poisoned lock rather than wedging every future sign-in on one panic. The frontend maps the terminal Databricks sign-in states to typed, actionable copy in `formatModelDiscoveryErrorStatus`: "sign-in required" is a muted note pointing at the picker and `buzz-agent auth databricks`; a failed or timed-out sign-in is a warning pointing at the explicit retry. Other Databricks failures fall through to the existing generic notice. ## Scope Changes are confined to Databricks discovery and its frontend status formatter — no `agent_models.rs` call sites are touched. The interactive-auth helper takes an injected timeout so the timeout/cooldown policy is unit-testable without a live browser. ## Deferred Cooldown keys use the raw trimmed `DATABRICKS_HOST`, while the catalog and OAuth cache normalize trailing slashes (`crates/buzz-agent/src/catalog.rs:96`, `crates/buzz-agent/src/llm.rs:2046`). So `https://workspace/` and `https://workspace` share credentials but get separate cooldown entries — an equivalent-spelling change to the host field mid-cooldown can re-pop passive OAuth once within the 5-minute window. Self-limiting (one extra browser launch, never auth corruption). Follow-up: a `trim_end_matches('/')` on the cooldown key plus an equivalent-host test, picked up with the coordinator migration if [#5545](https://github.com/block/buzz/pull/5545) ever merges. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
f35930104b |
fix(desktop): remove 0.5.9+ perf regressions, speed up get_channels (#5599)
Desktop input latency regressed sharply for users on v0.5.9 and worsened on latest main: multi-second stalls when clicking back into the app, slow fresh boots, intermittent lockups, and scroll/mouse degradation. Reverting to `119a84897` (pre-0.5.9) was confirmed to resolve it, isolating the regression to that range. Profiling a live production renderer plus a commit-level audit of the range found three independent, additive causes — fixed here — plus a long-standing `get_channels` cost that made every remaining refetch expensive, also addressed here. ## 1. Focus-return refetch storm (`refetchOnWindowFocus`) #5490 wired TanStack's `focusManager` to app focus and flipped ~20 query sites to `refetchOnWindowFocus: true`. A focus return after >60s away fires them all within milliseconds — and a click into an unfocused window *is* a focus return, so the burst runs before the click is processed. That is the "click into the composer, wait 5 seconds" symptom, and it also explains why mouse input feels worse than keyboard (clicks arrive with focus transitions; typing happens while already focused). A 5-second `sample` of a live production renderer caught a single window activity-state transition consuming ~1.25s of main-thread time, dominated by `JSON.parse` in the focus listener's microtask drain. #5535 already established the fix pattern but applied it to only two families (channels, home-feed). This PR extends the same 5-minute `staleTime` discipline to the remaining families: pulse (×5), workflows (×4), agents (×4), forum (×2), presence, user-status, custom-emoji, channel-templates, and the persona catalog. Polling cadences and push-invalidation paths are untouched — interval refetches and `invalidateQueries` both bypass `staleTime`, so live-update behavior is unchanged. Each gated family exports its focus-refetch policy as an options object that the production hook spreads into `useQuery`, and a `focusRefetchPolicy.test.mjs` drives a `QueryObserver` with that same production object — locking the policy behaviorally (fresh focus return → 0 fetches; stale → refetch) and failing if a hook's `staleTime`/`refetchOnWindowFocus` wiring drifts. Four families deliberately keep tighter freshness, all surfaces where the 5-minute gate would suppress the only refresh path and none of which feed the app-wide storm: `repo-sync-status` keeps its fresh focus refetch (its inline comment documents the "committed in a terminal, switched back to the app" flow as intended); the workflow-runs list stale-gates at 10s because a remotely-started run has no push invalidation and its conditional 1s poll is off while the cache shows no active runs; the workflow list queries (`useChannelWorkflowsQuery` and the all-channels aggregate) stale-gate at 10s because they have no poll and no relay subscription, and mutation-driven invalidation only covers this renderer — remote workflow creates/edits/deletes surface only via focus refetch; and the managed-agent log stale-gates at one poll tick (30s) so returning to a live agent log refreshes immediately. Run approvals keep the 5-minute gate under `RUN_APPROVALS_FOCUS_STALE_TIME_MS` — their focused 10s poll already covers freshness. ## 2. Synchronous localStorage sweep on the boot/focus path #5453's stale-cache sweep synchronously `getItem` + `JSON.parse`s every whitelisted localStorage entry on the main thread (multi-MB on seasoned profiles), scheduled with a `requestIdleCallback` timeout of 1.5s that guaranteed it landed mid-boot, and re-armed on every hidden→visible transition — stacking it onto the exact moment the focus storm fires. #5454's `trimSelfProfileCaches()` additionally scanned every localStorage key on every `writeSelfProfileCache()` call (which fires per relay self-profile delivery at boot). Now: the first sweep waits `BOOT_SWEEP_FLOOR_MS` (30s) after startup, the scan is time-sliced across idle callbacks, and the visibility trigger is removed — boot-delayed plus hourly still covers the 14-day TTL contract. The sliced sweep re-checks staleness immediately before each removal (a key rewritten fresh mid-sweep survives), isolates per-key storage errors so one bad entry can't strand the rest of the snapshot, defers oversized values once rather than parsing them on a zero-budget slice, guarantees forward progress on timeout-fired callbacks, and cancels its scheduled slice when stopped. The profile trim keeps a lazily-initialized memoized key count so the common under-cap write is O(1); the full parse scan runs only when the count exceeds a cap, resyncs if external deletions made it stale, and a failed scan skips the trim instead of aborting the write. Sweep semantics (rules, TTLs, eviction) are unchanged, and tests cover the scheduling, slice-progress, error-isolation, defer-once, and trim short-circuit behaviors. ## 3. The macOS window was never opaque #5478's glass appearance is correctly opt-in at the CSS layer, but the compositor cost was baked in deeper than its native `on_webview_ready` transparency call: the main window is declared `"transparent": true` in `tauri.conf.json` (added for the original glass work in #1671), which makes tao call `NSWindow.setOpaque(false)` at creation and resolve every later `set_background_color(None)` to `clearColor` — and no runtime `setOpaque(true)` path exists through tauri, while wry's runtime background setter can only force the WKWebView's `drawsBackground` off, never back on. So "restore the platform default" was unreachable: every launch, glass or not, ran with a non-opaque NSWindow, defeating WindowServer's opaque-window compositing fast path and forcing full window compositing every frame — compounded by the existing `backdrop-blur` chrome overlapping the scrolling timeline. This matches the compositor-shaped symptoms (scroll and pointer input degrading first). The window is now created opaque (`"transparent": false`) and the NSWindow layer is never made transparent at runtime. Glass never needed a transparent window: behind-window `NSVisualEffectView` vibrancy renders inside opaque windows (this is how Finder and Notes draw vibrant sidebars); it only requires a transparent WKWebView canvas, which the `set_window_vibrancy` enable path already establishes at runtime (`macos-private-api` compiles that in independent of the window flag). Enabling glass installs the vibrancy layer and then makes only the webview canvas see-through; disabling clears the vibrancy layer — the canvas may stay non-drawing afterwards (wry's flag is one-way at runtime), which is harmless because glass-off CSS paints fully opaque above an always-opaque NSWindow. The boot-path first-frame backing writes touch only the NSWindow backing color and are therefore inert to glass state regardless of how they order against the `ThemeProvider`'s vibrancy call on a persisted-glass-on cold boot. Glass-off users (the default) get an end-to-end opaque window from boot for the first time. ## 4. `get_channels`: serial round-trips and a multi-MB payload on every refetch The stale gates in (1) cut refetch frequency; this cuts the cost of the refetches that legitimately remain (boot, and focus returns after more than 5 minutes away — previously still a multi-second stall). `get_channels` made ~8 fully serial relay round-trips (~3.2–3.6s at 1,100+ channels), then shipped the full `ChannelInfo` list — including every channel's member pubkeys — across IPC, where the renderer's `JSON.parse` of the multi-MB payload froze the main thread (the ~1.25s stall captured in the live sample). - **Concurrent stages**: the membership chain, the open-channel directory scan, and the hidden-DM snapshot run concurrently, as do the member-count and last-message queries that follow. The critical path drops from ~8 sequential round-trips to 2 phases. Filters, limits, pagination, and merge semantics are unchanged. - **Not-modified short-circuit**: the command now takes a client-supplied content hash (FNV-1a 64 over the channel list, canonicalized by id and excluding `last_message_at`) and omits the channel list from the response when nothing else changed. Last-message timestamps — which change on nearly every message anywhere — ship as a small separate map that the client overlays onto its cached list with reference preservation, so React Query's structural sharing also skips downstream re-renders. On a typical refocus the renderer parses kilobytes instead of megabytes. The hash is stored in the query cache itself, tying its lifecycle to the data it describes so a community switch can never leak a stale hash. The E2E mock bridge speaks the new payload shape — including the complete `last_messages` map the client treats as authoritative — and hash canonicalization plus overlay reference-preservation are unit-tested on both sides. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
9203bf60ee |
perf(desktop): coalesce read state localStorage persistence (#5591)
Follow-on to #5453/#5454's localStorage work — found while investigating app-slowness reports on a real profile. ## Problem `ReadStateManager.persistLocalState()` serialized and rewrote **all three** read-state localStorage blobs (`buzz.channel-read-state.v2`, `.publishable.v1`, `.source-created-at.v1`) synchronously on every context advance. On a real profile (1,643 contexts, ~450K chars across the three blobs) this produced ~880KB of localStorage sqlite WAL growth per 30 seconds at idle, with writes every ~5s — steady main-thread serialization + sync IPC for no user-visible benefit. Observed WAL size on the affected profile: 94–114MB. ## Fix - Local persistence coalesced behind a **1s trailing-edge timer**: a burst of N advances produces one `writeStoredReadState` (one write per blob). - Pending dirty state **flushes synchronously** on `pagehide`, hidden `visibilitychange`, `destroy()`, and before each relay publish — disk is current before any relay event goes out. - Hydration still persists immediately. Publish debounce (5s), merge logic, and blob formats unchanged (`DEBOUNCE_MS` renamed to `PUBLISH_DEBOUNCE_MS` only). ## Accepted residual A hard kill (SIGKILL/power loss — not webview teardown) inside the 1s window loses ≤1s of local read-state advances; relay max-merge bounds the effect to a message flickering back unread. On the record per review. ## Validation - `readStateManager.test.mjs`: fake-timer/mock-storage coverage — exactly one 3-blob write per burst (zero before the timer fires), hidden-flush cancels the timer and persists, hydrate persists immediately, pre-publish flush. Suite 26/26. - Push gate at the pushed commit: desktop check, typecheck, full desktop unit suite 4,670/4,670. - Independent adversarial FULL REVIEW: **APPROVE** at tree `371a02cf` (commit metadata rewritten afterward for attribution; tree identical) — all six `persistLocalState` call sites traced, lifecycle/leak checks (StrictMode remount, pubkey change), no external readers of the blob keys. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
d9dc76c0aa |
fix(desktop): bound initial timeline retention (#5603)
## Summary - let Virtua own the initial visible timeline range instead of passing every loaded row to `keepMounted` - populate the existing bounded retention window after the virtualizer reports its first settled viewport - cover a 10,000-row timeline to prevent an all-history initial mount regression ## Why `useTimelineRetention` initialized its retained-key set with every loaded timeline key. Those indices were passed to Virtua's `keepMounted`, effectively defeating virtualization during initial channel positioning until `onScrollEnd` pruned the set. On a large real channel this grew WebContent into multiple gigabytes and blocked the renderer main thread for 20+ seconds while WebKit laid out and painted the retained rows. Starting with no retained rows restores Virtua's visible-range mount; the existing reader-neighborhood and visual-tail retention is populated once the viewport is measured. ## Validation - `node --import ./test-loader.mjs --experimental-strip-types --test src/features/messages/ui/useTimelineRetention.test.mjs` - pre-push hook at `8e86a189de7e9a8f2cb119396c8f912ed9dacd6e`: branch-skew, desktop-check, desktop-typecheck, and all 4,671 desktop tests passed - manual ablation against PR #5599 on the affected profile: catastrophic channel-switch stalls disappeared ## Authorship disclosure Carl implemented and is posting this change on Wes's behalf. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
cf03bd7c37 |
Improve desktop search scoping (#5306)
## What changed - unify Cmd+K and channel Cmd+F around a removable channel or conversation scope - add conservative fuzzy matching for people and channels while preserving exact-match ordering - make scoped message search complete for one-character queries and expose up to 40 scrollable results - keep the pre-scope channel or DM action in the normal results flow so it scrolls away with the list ## Validation - desktop TypeScript typecheck - desktop text-size and file-size guards - focused fuzzy-search unit tests (24 passed) - focused search Playwright coverage (7 passed), including channel and DM copy, one-character results/no-results, 40-result scrolling, and the non-sticky scope action - desktop E2E build - visual review of channel, scoped, expanded-results, and DM states --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> |
||
|
|
cd2aa5c12d |
Add glass appearance and cohesive settings (#5478)
## Summary - add an opt-in native glass sidebar with opacity controls and live theme previews - refine sidebar spacing and Buzz-only active rows while preserving production defaults - unify settings section cards, subtitles, and agent runtime rows ## Validation - repository format, lint, type, and file-size checks - 4,538 desktop tests and 2,270 native desktop tests - desktop and web production builds - 1,261 mobile tests in the completed full gate - focused Playwright appearance, sidebar, settings, pairing, and runtime coverage --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Signed-off-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz> |
||
|
|
b0795a10ea |
Add Send to channel for thread messages (#5305)
## Summary - Share eligible self-authored or owned-agent thread messages into the parent channel as new top-level messages. - Link the shared message back to the exact root thread with a semantic channel label and excerpt. - Add a dedicated channel-arrow icon plus ownership and navigation coverage. ## Validation - Desktop lint, size, and text guards - Desktop TypeScript build and all 4,543 unit tests - Focused Playwright send-to-channel and thread-link navigation tests --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: Fast Fizz <2df81cb51f05a9d5387ef24d7b9ecb8fcdfcd1c70ffabc67061c9596e1b5b1c4@buzz.block.builderlab.xyz> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
bba3e06386 |
Fix macOS attachment picker lifecycle and allow inert HTML downloads (#5569)
## Problem Canceling the native macOS file chooser leaves the composer's temporary, detached `<input type="file">` without a `change` event or an explicit cleanup path. Opening Finder again immediately creates a second detached input while WebKit may still be unwinding the first picker. The newly selected files can therefore fail to reach the upload pipeline. Drag and drop is unaffected because it bypasses this picker lifecycle. This does **not** add an automatic retry mechanism. “Retry” means the user's next attachment attempt after canceling or after a prior selection. ## Fix - give each composer hook one hidden, body-mounted file input for its lifetime instead of creating a detached one per click - reset and reconfigure that input before every open, replace its handler rather than stacking handlers, and remove it cleanly on unmount - preserve normal selection, cancel then reopen, selecting the same file again, and multi-select behavior - accept canonical `text/html` attachments while continuing to serve and render them strictly as inert downloads - keep XHTML, SVG, JavaScript, and executable MIME types blocked The picker change fixes the ownership/lifecycle bug at its source; it does not retry failed uploads, add delays, or mask errors. ## Testing - mandatory pre-push gate: branch-skew, desktop typecheck/tests/check, Rust tests, and desktop Tauri checks passed on `ea5a97adf957803935b28d63d32f9f332cf65287` - `cargo test -p buzz-media --lib` (110 passed) - `pnpm --dir desktop typecheck` - focused Biome check for the three picker files - picker Playwright regression: cancel/no selection then reopen, select the same file again, and multiple selection (run on the source commit before integration) - HTML live-relay response regression added as ignored E2E because it requires the S3-backed relay harness ## Manual verification Playwright models cancellation with Chromium's `FileChooser.setFiles([])`; it cannot exercise the native macOS Finder panel/WebKit presentation lifecycle. Before merge, manually verify in the built macOS app: 1. select a PNG normally 2. cancel, then immediately reopen and select a PNG 3. select the same PNG on a subsequent attempt 4. multi-select two PNGs --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Princess Donut <68157ebd23b3897c1991015c3038658ea916200c67d3a54620b0754d1b92f6e0@buzz.block.builderlab.xyz> Co-authored-by: Mongo <5c25403eab7271f9f94ddd4f2b270e8cac2c92e2c830c51877cca6ec974ffb3f@buzz.block.builderlab.xyz> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
d3ec831e0c |
fix(desktop): preserve fresh channel timelines (#5577)
## Summary - skip the channel subscription catch-up request when the authoritative channel window was fetched successfully within the existing five-minute freshness period - keep the responsive deferred skeleton for populated channel switches instead of briefly rendering empty-channel actions - preserve a real empty-channel intro across the first appended message only after React has committed that empty state This is intentionally narrow. It does not claim to solve the separate sidebar startup cost or general main-thread stalls found during the investigation. ### Related issue N/A — no matching open issue or PR found. ### Testing - pre-push desktop gate on `f1be6beea90b9715e04e5fc65cc5cfbe8210e0d9`: - desktop tests: 4,621 passed - desktop check: passed - desktop typecheck: passed - branch-skew: passed - focused cache/surface/lifecycle tests: 62 passed - manual diagnostic trace after rollback: - 16/16 channel revisits skipped catch-up refresh - 0 revisit refresh starts - 0 populated-channel empty/intro flashes - cached switches retained the deferred skeleton-to-list path No screenshot: the regression is a transient channel-switch state and request behavior, covered by lifecycle tests and the diagnostic trace rather than a stable visual diff. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
4935724494 |
fix(desktop): suppress fresh focus-return refetches for channels and home-feed (#5535)
Fixes the 0.5.9 sluggishness Wes reported in app-slowness-mac (channel list slow, content slow). ## Problem #5490 (shipped in 0.5.9) flipped ~20 query sites to `refetchOnWindowFocus: true` and wired TanStack focusManager to app focus. Instrumented at that exact commit: regaining focus after >60s away fires **7 query fetches within 2ms**, including `get_channels`, which settles at **~3.6s** (production probe: median 3.2s at 1,133 channels — 8 serial round-trips, 1,133-filter last-message batch). In 0.5.8 this burst was zero by configuration. Net: a burst of fetch/parse contention exactly when the user returns to the app. Relay ruled out: v0.2.1 small reads are 2–4ms upstream; nothing in v0.2.0..v0.2.1 degrades the query path. The O(N) `get_channels` design is a pre-existing issue (June analysis) — this PR fixes the new stampede that made it user-visible. ## Fix Raise `staleTime` to 5 minutes on the two expensive focus-refetch families — `channels` and `home-feed` — so a focus return inside that window serves cache instead of refetching. `refetchOnWindowFocus: true` only refetches stale queries, so genuinely old data still refreshes on return. Unchanged: focused polling cadence (60s channels / 30s home-feed; interval refetches ignore staleTime), #5490 blur quiescence (no changes to `useDocumentVisible.ts`/`queryClient.ts`), all push-style invalidation paths (`invalidateQueries` bypasses staleTime), and channels cold-start revalidate (`initialDataUpdatedAt: 0`). ## Validation - New regression test `desktop/src/features/home/focusRefetchPolicy.test.mjs` (4/4): fresh focus return → 0 fetches; stale → 1; polling constants locked. - Pre-push gate at the reviewed tree: desktop-check, desktop-typecheck, full desktop-test **4588/4588**. - Independent adversarial review (Beth): APPROVE at tree `4e2546ec` — verified fresh-skip/stale-refetch against query-core 5.100.14 source, polling-cadence via browser-simulated probe, side-effect sweep of all invalidation paths clean. Sole CHANGE was commit trailers, fixed by amend (tree unchanged). ## Known residual Focus returns after >5min still fire the full burst including the ~3.2–3.6s `get_channels`. This cuts stampede frequency, not magnitude — the O(N) `get_channels` relay path (RESEARCH/GET_CHANNELS_SLOWNESS.md) is the follow-up that fixes magnitude. Diagnosis: Summer (focus profiling) + Morty (relay probe); implemented by Meeseeks; reviewed by Beth; integrated by Rick. Thread: app-slowness-mac e78fad29380d9a0974c9d673910450994a228781ddce133a8cedbd90504d95be. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz> |
||
|
|
83ca595ada |
fix(desktop): preserve theme when opening communities (#5266)
**Category:** fix **User Impact:** Previously selected community themes are preserved when opening a relay through onboarding, while first-time theme migration still completes for communities with no saved theme. **Problem:** During community initialization, desktop queried theme history before establishing live delivery. If a replacement event arrived while an empty history query was in flight, the client could incorrectly treat the theme as absent and publish the default over the user's saved selection. **Solution:** Subscribe before fetching history, expose whether live readiness reached EOSE, flush buffered live events before resolving EOSE, and retain the newest delivered replacement through hydration. Seed the inherited/default theme only when both live and history snapshots reach EOSE with no valid or unreadable event; subscription failures, CLOSED, readiness timeout, relay failure, and unreadable events fail closed without publishing. <details> <summary>File changes</summary> **desktop/src/shared/api/relayClientSession.ts / relayClientShared.ts / relayClosedRecovery.ts** Distinguish EOSE from CLOSED/timeout readiness and flush buffered events before resolving an EOSE fence. **desktop/src/shared/theme/CommunityThemeController.tsx** Seed and complete first-community migration only for confirmed absence; uncertain hydration remains non-publishing. **desktop/src/shared/theme/communityThemePreference.ts** Keep the inherited appearance for the first migrated community and the stable default for later empty communities. **desktop/src/shared/theme/communityThemeSync.ts** Arbitrate live and history results into valid, confirmed-absent, invalid, or unavailable hydration outcomes. **Tests** Cover EOSE/CLOSED readiness, subscription failure, timeout, unreadable and live-racing events, no-op initialization, and first-to-later community fallback isolation. </details> ## Reproduction steps 1. Save a non-default appearance for a community relay. 2. Remove the community locally, then open the same relay again through onboarding. 3. Arrange for the saved replacement event to arrive live while the initial history query returns empty. 4. Confirm the saved appearance remains selected and the client does not publish the default theme over it. 5. On an account with no theme records, open a first empty community and confirm its inherited appearance is migrated; open a later empty community and confirm it starts from the stable default. ## Validation - Pre-push desktop checks, typecheck, and full desktop tests: passed at `f79556b0e` - Focused theme/relay readiness tests: 38 passed - Desktop file-size ratchet and diff check: passed --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> |
||
|
|
be48ce98bd |
fix(link-preview): reliably render previews sent right after they resolve (#5245)
## Overview **Category:** fix **User impact:** Link previews no longer disappear when a message is sent while preview metadata or media is still settling. Fast Enter, rapid Enter, and confirmed-draft auto-send now preserve the preview without duplicate sends or stale tags. **Problem:** The composer could look ready before its sender-authored snapshot tag existed. Send paths could then race preview resolution/upload, while debounced preview state could attach a tag for a URL that had already been removed. The same timing also caused confirmed-draft auto-send to be consumed without sending. **Solution:** - Debounce preview resolution to avoid card flicker while typing, then disable every submit path while a supported external preview settles. A 2-second escape cap still permits a bare-link send if resolution stalls. - Keep submit synchronous: acquire a composer-local lock before asynchronous send work, read ready tags from the live URL set, and reject Enter/form submits while a snapshot is pending. - Retry confirmed-draft auto-submit until preview settling clears, then submit exactly once. - Upload thumbnail and favicon independently. A failed upload shows a toast and degrades to the surviving media (or text-only) rather than leaving the card spinning. - Exclude message-edit mode from preview resolution, upload, and Save gating. Edit-time preview snapshots remain follow-up #5273. - Canonicalize fragment-bearing URLs for preview lookup/snapshot identity while preserving the original fragment links in message text. ## Link preview state walkthrough Captured using PR #5245's actual public Open Graph metadata and artwork. The deterministic E2E bridge controls only upload timing so the transient disabled state can be captured reliably. | State | Expected behavior | Screenshot | | --- | --- | --- | | **1. Snapshot upload pending** | The real PR preview is visible, but Submit remains disabled until its sendable snapshot tag is ready. Click and Enter cannot send a bare link during the settling window. |  | | **2. Snapshot ready** | Once snapshot upload settles and the tag is ready, the same preview remains and Submit becomes active. |  | | **3. Message sent** | The sent event carries the snapshot tag and renders the PR title, description, and artwork inline instead of degrading to a bare URL. |  | ## Regression coverage - Enter during metadata resolution or snapshot upload cannot send early. - Paste-and-immediate-Enter sends after settling; rapid Enter submits exactly once. - Confirmed-draft auto-send waits for settling and fires exactly once. - Removed/replaced URLs cannot leak stale snapshot tags or media refs. - Thumbnail upload failure toasts and sends with the surviving favicon. - Edit mode does not resolve/upload previews or gate Save. - Fragment variants share a canonical preview while original fragment links remain clickable. - Existing ready-preview, suppression, bare-link fallback, and multi-preview behavior remains covered. ## Reproduction steps 1. Open a channel and paste a supported external URL into the composer. 2. Press Enter immediately, before preview metadata/media finishes settling. 3. Before this fix, the event could be sent without its preview snapshot (or confirmed-draft auto-send could be lost). With this fix, submit waits behind the disabled state and fires once with the matching snapshot tag. 4. Remove or replace the URL and press Enter inside the debounce window. The sent event contains tags only for URLs still present in the submitted content. ## Validation All required PR checks are green, including Desktop Core, Desktop Smoke E2E shards, Desktop E2E Integration shards, macOS build, security checks, and DCO. --------- Signed-off-by: Taylor Ho <taylorkmho@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> |
||
|
|
7e6e9c547f |
fix(link-preview): restore Buzz entity link cards (#5494)
**Category:** fix **User Impact:** Buzz pull request, issue, and repository links now show compact, useful metadata cards in received messages, including messages sent by agents and the CLI. **Problem:** Sender-authored snapshots protect recipients from external preview fetches, but that change also removed recipient-side cards for trusted Buzz entity links when the sender did not attach snapshots. **Solution:** Resolve recognized Buzz entities only against the active relay and show signed repository identity, title, and compact builder context with the current inline Buzz mark in the favicon slot, but without avatars, thumbnails, or external image fetches. Entity metadata wins over conflicting sender snapshots, while unsupported or unavailable metadata retains a safe text fallback. <details> <summary>File changes</summary> **desktop/playwright.config.ts** Adds the entity-link regression spec to the smoke test project. **desktop/src/features/messages/ui/useComposerLinkPreviews.tsx** Treats recognized Buzz entity cards as complete without generating snapshot tags and retains fallback cards when relay metadata is absent. **desktop/src/shared/lib/useResolvedLinkPreviews.test.mjs** Covers kind-scoped entity detection, trusted relay metadata, root-scoped lifecycle queries, exact single-repository root binding, image-less pending state, and fallback behavior. **desktop/src/shared/lib/useResolvedLinkPreviews.ts** Resolves signed repository, pull request, and issue metadata from the active relay. Entity roots fail closed unless they carry exactly one matching repository tag; lifecycle queries are root-scoped before limits; successful metadata remains stable until relay/community reset, and PR commit context uses the immutable root event rather than an unindexed update query. **desktop/src/shared/ui/compact-link-preview-attachment.tsx** Uses Buzz repository identity as the compact card provider and avoids reserving thumbnail space for image-less entity cards. **desktop/src/shared/ui/markdown.tsx** Routes message cards through the combined entity/snapshot preview hook. **desktop/src/shared/ui/markdown/useMessageLinkPreviews.test.mjs** Proves relay-authenticated entity metadata beats a forged sender snapshot while preserving mixed-link content order. **desktop/src/shared/ui/markdown/useMessageLinkPreviews.ts** Combines recipient-resolved Buzz entities with sender-authored external snapshots using explicit trust precedence and first-seen ordering. **desktop/tests/e2e/entity-link-recipient-cards.spec.ts** Exercises repository identity, PR workflow context, repository metadata, image-less rendering, and composer send behavior for agent/CLI-style entity links. </details> ## Reproduction steps 1. Open a channel containing a message sent without `link-preview` tags whose content includes valid `buzz://pr`, `buzz://issue`, or `buzz://repo` links. 2. Confirm each card shows its repository identity and signed title; PRs/issues also show compact lifecycle context, and repositories show description/status/default branch. 3. Confirm the cards use the Buzz mark in the favicon slot with no avatar, thumbnail, or reserved image area. 4. Compose and send a message containing a Buzz entity link; confirm sending is not blocked waiting for a snapshot. 5. Send a message containing both a Buzz entity link and a snapshot-backed HTTPS link; confirm cards follow content order and the HTTPS link remains sender-snapshot-only. ## Screenshots ### Recipient view — Buzz-branded metadata cards Repository identity, title, and compact builder context render with the current inline Buzz mark in the favicon slot and no avatar, thumbnail, or reserved image space.  ## Validation At commit `7bc70b0a9f70392bd062ed25b1d2362cc4021a40` with a clean working tree: - Pre-push hooks passed: branch skew, desktop check, desktop typecheck, and full desktop unit suite - Full desktop unit suite: 4,560 passed - Purpose-built Playwright regression after a fresh E2E build: 2 passed - Screenshot regenerated from the same commit and visually inspected Originating conversation: Buzz channel `c2859932-b679-4091-9c7e-f5a65deddd64`, thread `93c3e7be59a8d1ec10b4992efd783a2a79f253a10f10d39746c6ad41b0d5bb42`. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> |
||
|
|
3f2f32641f |
Polish desktop onboarding flow (#5310)
## Summary - standardize onboarding navigation and horizontal step transitions - refine the avatar editor with live preview, segmented modes, search, skin tones, and reduced-motion-safe feedback - simplify harness/default-model actions and supporting copy ## Testing - desktop typecheck and static guards - desktop E2E build - 9 focused onboarding smoke tests - 4 focused onboarding/profile integration walkthroughs - 4,535 desktop unit tests --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz> |
||
|
|
07a3c768d6 |
fix(desktop): quiesce renderer polling while hidden (#3677) (#5490)
Fixes #3677. ## Problem The renderer never quiesces: recurring timers, query polling, and re-render tickers run at full rate whether the window is visible, hidden, or minimized. Measured on a live installed app: **27.5% mean renderer CPU visible vs 28.7% hidden** (60×1s `ps` samples of the WebContent process; `sample(1)` dominated by `WebCore::timerFired`/ThreadTimers, microtask checkpoints, JSON parsing, style matching). Matches all three reproductions in #3677 (macOS prerelease, Linux/WebKitGTK A/B/A minimize test, stable macOS). Per-timer instrumentation (dev build, wrapped `setInterval`/`setTimeout`/rAF) attributed the recurring work: `useNow` 60 fires/min, 40 active TanStack refetch intervals, agent-turn pruning 12/min, auto-restart ticks, huddle/reminder polls — none visibility-gated. ## Fix (two-tier gating, standard mechanisms only) Two separate signals in `desktop/src/shared/lib/useDocumentVisible.ts`, because they mean different things and (see residuals) are delivered differently on macOS: - **`useDocumentVisible`** — true Page Visibility only (`document.visibilityState`). Gates local UI work that must keep running on a visible-but-unfocused window: `useNow` relative clocks, agent-turn pruning, huddle bar state/model-status polling, auto-restart tick. Hidden ⇒ paused; `useNow` snaps to fresh `Date.now()` on return. - **`useAppFocused`** — visible AND `document.hasFocus()`. Gates network refetch polling only (`useFocusedRefetchInterval`, ~15 query families: forum/home/agents/channels/templates/emoji/user-status/projects/workflows/persona-catalog/pulse/presence-list). TanStack's `focusManager` is wired to this signal (idempotent, single install) with `refetchOnWindowFocus: true`, so stale queries refresh promptly on return. Deliberate side effect, documented in code: query retries pause on blur; mutations and the presence heartbeat (`retry: 0`) are unaffected. - **Never gated:** reminder due-notification poll (fires while hidden/unfocused — extracted to `reminderNotificationPoll.ts` with regression test), huddle pipeline hot-start (`check_pipeline_hotstart` survives backgrounding for the duration of a huddle), relay stall watchdog, presence heartbeat. Live WebSocket delivery untouched throughout. - Huddle model-status indicator now clears only on huddle phase end, not on visibility/focus changes. ## Validation - Instrumented dev build, populated channel, fires/min: **visible+focused** unchanged (`useNow 60 / prune 12 / watchdog 6 / query 4 / auto-restart 4 / low-rate huddle/reminder/presence`); **visible+blurred**: query polls 0, UI clocks continue (`useNow 60 / prune 12`), reminders 2, presence live; **truly hidden**: only watchdog 6, reminders 2, presence ~2 — everything else 0. Return restored visible+focused, selection preserved, queries refreshed. - Hide-vs-blur decomposition (instrumented probe instance, AppleScript-driven): on macOS WKWebView, Cmd-H / minimize / full occlusion did **not** reliably produce `visibilityState === "hidden"` — they reliably produced focus loss. The CPU-dominant quiescence path on macOS is therefore the focus gate; the visibility gate is exercised fully on platforms that report hidden (e.g. WebKitGTK minimize per the Linux repro). - Gate-regression tests: signal separation, `useNow` hidden-pause + fresh-snap on return, focus-gated interval pause/resume-with-refresh, reminder delivery while hidden+unfocused (5 new, plus primitive wiring tests). - Push gate: desktop check, typecheck, full desktop suite **4549/4549** at `1237548d1`. ## Known residuals - **macOS hidden-signal limitation:** because WKWebView rarely reports `hidden` on app-hide/minimize, hidden-only consumers (`useNow`, prune, huddle UI polls) may keep ticking on macOS when the app is hidden. These are cheap local timers; the expensive network polling still quiesces via focus loss, which is what the measured 28% CPU was attributed to. If the residual local-timer cost proves measurable, the follow-up is bridging Tauri window hidden/minimized events into the visibility signal. - End-to-end CPU confirmation on a packaged build is the post-merge follow-up (against the 28% idle baseline). - Visible-state costs (skeleton animation pileups on stuck loading views, per-poll JSON payload churn) are intentionally out of scope — separate follow-up issue. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz> |
||
|
|
2777189d96 |
fix(channels): restore member invitations to private channels (#5493)
## Summary - restore private-channel invitations for every active member - keep owner/admin-only enforcement for elevated role grants, active role changes, and removals - preserve #4612's unrelated Desktop/mobile failure handling and hardening - add relay coverage for the ordinary actor/target role matrix (`member`, `guest`, `bot`) ## Validation - pre-push hook passed on `7de700e17642ad7e10155f9537033168d9249268`: branch skew, Desktop checks/typecheck/tests/Tauri checks, mobile tests, and Rust tests - `cargo test -p buzz-test-client --test e2e_relay --no-run` - `cargo fmt --all -- --check` - `git diff --check` - Donut and Mongo independently reviewed the cross-layer authorization behavior; Donut's role-matrix coverage finding is addressed in this revision --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
9c074bb89b |
fix(desktop): bound nine unbounded localStorage stores (#5454)
Part of #5418 (Phase 1, lane A). Companion to #5453 (TTL sweep). ## What Nine localStorage stores grew without bound (full 58-call-site audit in the tracking issue). Each now has an explicit leak-guard cap, applied wherever the store is parsed, merged, or written, preserving each file's merge/versioning semantics: - **Community icons:** 32 entries, 96 KiB/value (aligned with the relay's `MAX_WORKSPACE_ICON_DATA_URL_LEN`); touched relay becomes newest. - **Channel mutes/stars:** newest-500 cap each, bounded by recency (`updatedAt`, channel-ID lexical tie-breaker), with the just-written channel unconditionally preserved for that write (cap−1 recency slots + the mutated key). A bounded LWW store cannot guarantee permanent deletion history; the guarantee here is that **the just-written mutation survives its own bounding** and, as the newest entry, defeats an older remote `true` through the pre-publish `mergeStores`. Known residual (accepted): `updatedAt` is whole-second, so two distinct mutations inside the same second at exact capacity can still evict the earlier one before the debounced publish — same root cause as the merge-path same-second tie, tracked for the follow-up precision fix rather than more preservation machinery. Enforced at parse, post-merge, local state, and persistence. - **Forced unread:** newest 500 insertion-ordered, touched channels refreshed. - **Persistent agent audiences:** 200-scope LRU. An unchanged-audience touch (including re-initializing an existing scope) refreshes LRU order and persists without advancing the scope's revision or emitting; an already-most-recent touch is a pure no-op (no clone, no write), so render-path re-initialization causes zero storage traffic. - **Self profiles:** newest 8 per relay / 32 globally by `updatedAt`, just-written key always preserved; trim count-gates before parsing payloads so under-cap writes skip the scan entirely. - **Sections:** newest 100 + newest 1,000 assignments, orphans removed; `assignChannel` delete/reinserts the touched channel so a reassignment becomes newest in insertion order and cannot be evicted by the next assignment. **Sort prefs:** 104 groups (100 sections + 4 fixed). - **Feature overrides:** `getOverrides()` filters to current-manifest boolean ids on read only — no write-back from the render-path getter. ## Review-driven revisions - `237f25e4` — three narrow changes from the first adversarial review (no render-path storage write, icon cap aligned to relay constant, count-gated profile trim). - `d864ffb0` — fixes for the two GitHub review findings on `237f25e4`: (P1) mute/star bounding switched from false-tombstone-first eviction to pure recency, with regressions proving an at-capacity unmute/unstar survives bounding and the pre-publish LWW merge; (P2) unchanged agent-audience touches now refresh LRU order (no revision advance, no emit), with a subscriber-mounted regression. - `3ddbb26d` — MRU guard from the second adversarial VERIFY: the P2 touch path skips clone/persist entirely when the scope is already most-recently-inserted, eliminating repeat synchronous localStorage writes from render-path effects. Test proves a non-MRU identical touch writes exactly once (scope persisted last) and an already-MRU touch writes zero times. - `e220ccd9` — fixes for the second GitHub review round (Carl, on Wes's behalf): (1) mute/star bounders preserve the just-mutated key so a same-second mutation at capacity survives its own bounding; merge/sync call sites unchanged; (2) `assignChannel` delete/reinserts the touched key so an at-capacity reassignment isn't evicted by the next new assignment. Regressions at storage and hook level for both; negative-control run of the 7 new tests against the old sources: 7 fail. ## Validation - Full desktop suite 4555/4555 at both `d864ffb0` and `3ddbb26d`, plus desktop-check/typecheck via the push gate; focused storage/audience tests 62/62 at `d864ffb0`, 14/14 audience suite at `3ddbb26d`. - Independent adversarial review: APPROVE at `88a55aee` (including 100 smoke E2E specs covering every seeded store, run manually since push hooks exclude Playwright), then a second VERIFY pass: **VERIFIED at `d864ffb0`** — P1/P2 confirmed closed via negative-control runs of the new suites against the old sources, plus smoke Playwright on the mute/star/audience specs (17 passed). That VERIFY requested one pre-merge change (no localStorage writes from the render path), landed as the narrow MRU guard in `3ddbb26d` within the reviewer's stated no-re-review boundary. A third VERIFY pass: **VERIFIED at `e220ccd9`** — both findings from the second GitHub review confirmed closed by sensitivity testing (new tests fail on old sources), hostile same-call section-trim case constructed and passed, full suite 4562/4562 re-run independently. Authored by Meeseeks (agent), reviewed by Beth (agent), integrated by Rick (agent). Discussion: Buzz channel time-based-localstorage-eviction, thread 0d85a73ca43e54748128f89c3512a4726131bf5473253395d46bf8f3a7b58bd4. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz> |
||
|
|
bb9aae1065 |
feat(desktop): time-based sweep for stale localStorage caches (#5453)
Part of #5418 (Phase 1, lane B). ## What Adds a periodic, whitelist-driven TTL sweep for disposable localStorage caches so a desktop session left open for days converges to the same storage state as one restarted nightly. - New `desktop/src/shared/lib/localStorageSweep.ts`: declarative `LOCAL_STORAGE_SWEEP_RULES` table — six repaintable pure-cache prefixes (matching `PURE_CACHE_KEY_PREFIXES` in `localStorageQuota.ts`), all 14-day TTL, keyed on each payload's `updatedAt` (user-label buckets use their newest nested per-profile timestamp). - Entries with no trustworthy timestamp are retained, never guessed stale. `buzz-self-profile.v1:` is deliberately excluded — it is the load-bearing offline identity fallback (guard comment in the table). - Scheduler: first sweep deferred off the boot critical path via `requestIdleCallback` (1.5s timeout) with a 250ms timer fallback, then hourly and on return-to-visible, debounced to 5 minutes. Throw-safe throughout (failures `console.warn`, never crash — per `safeStorage.ts` conventions / #5078). - Wired in `desktop/src/main.tsx` beside `recoverLocalStorageQuotaOnStartup()`. ## Validation - Focused node test 7/7 at HEAD; pre-push gate green (desktop-check, desktop-typecheck, full desktop-test 4542/4542). - Manual Playwright (not covered by push hooks): `relay-connectivity.spec.ts -g "04"` (offline cached identity) passes 1/1 at HEAD — this spec caught and now guards the v1 regression. - Independent adversarial review: FULL REVIEW (REQUEST CHANGES) then VERIFIED — PASS at exactly this commit, including whitelist containment against the 58-site inventory, scheduler tracing, and smoke E2E. Authored by Summer (agent), reviewed by Beth (agent), integrated by Rick (agent). Discussion: Buzz channel time-based-localstorage-eviction, thread 0d85a73ca43e54748128f89c3512a4726131bf5473253395d46bf8f3a7b58bd4. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Summer <1fdd3cc104e2911eb3b2da6f97d1b25f4a7f3550ded4492b24ff1d95acd66766@buzz.block.builderlab.xyz> |
||
|
|
5e4c05f90b |
feat(desktop): NIP-AM agent-usage backend — P2 emission/transport/archive + P4a aggregation/D6 (#4000)
## What Implements Phases 2 and 4a of the Usage v2 plan (plan events `d0268cd0`/`0e95b035`), extending the archive backend to emit, transport, archive, and aggregate both cache categories and billing identity fail-closed. ### P2 — emission, transport, archive **Tri-state accumulators** (`Unseen`/`Exact`/`Unknown`) for cache-read and cache-write in `buzz-agent` turn and session state. Absent field = Unknown (never zero) through the full pipeline. No `unwrap_or(0)` on the cache path. Both cache folds are gated on usage-bearing responses (same gate as the total-state and identity folds) — a response with no usage at all must not poison either accumulator. **Overflow-aware input token parsing and accumulation** — closed end-to-end from parse through wire to ACP: - `sum_usage()` returns `SumUsageResult` (`Exact(u64)` | `Overflow`) — checked arithmetic, never clamps. `anthropic_input_tokens()` returns `Option<SumUsageResult>` since it sums three fields (`input_tokens + cache_read_input_tokens + cache_creation_input_tokens`) that can collectively overflow. Single-field callers (`prompt_tokens`, `completion_tokens`, etc.) convert via `.into_exact()` — their single-field sums cannot overflow. - `LlmResponse.input_tokens_overflowed: bool` propagates the parse-layer signal into the run loop. When set, `input_tokens` is `None` (clamped value discarded), the context-gate baseline (`last_request_input_tokens`) is frozen at its prior reading, and `turn_input_tokens` is poisoned to `TurnIOState::Poisoned` before any emission — including mid-turn `emit_usage_update` calls. A dedicated enum on `LlmResponse.input_tokens` would ripple into ~20 existing test assertions on `r.input_tokens == Some(...)`; the bool flag confines the change to the two call sites that check it. - `TurnIOState` (`Unseen`/`Exact`/`Poisoned`) for input and output: per-round fold uses `checked_add`; overflow poisons permanently at turn and session level, no healing. Absence does not poison (pass-2-cleared contract unchanged). Wire emission omits `accumulatedInputTokens`/`accumulatedOutputTokens` when poisoned — never null, never `u64::MAX`. ACP treats absent = publisher-poisoned: `delta_reliable: false`, null turn fields, null cumulative for that category; session cumulative stays unknown for all subsequent turns once poisoned. **Conditional wire emission** for `accumulatedCachedInputTokens` and new `accumulatedCacheWriteTokens`: fields are omitted when the cumulative is Unseen or Unknown. ACP `_goose/unstable/session/update` contract documented next to the payload with tests for all absence/zero variants. **`PricingIdentity` stamping (publisher-side)**: - `pricing_authority()`: canonical parsed-URL endpoint comparison against the official allowlist — HTTPS only, exact allowlisted host (lookalike-safe), default port (omitted or explicit :443), required API base path, rejects userinfo/query/fragment/path-prefix lookalikes. - Model: the actually-requested `request_model` after mesh/auto resolution (not `effective_model_str`). - Turn discipline: identity retained only while ALL usage in the current turn carries one identical proven identity; any mismatch, unproven-usage-bearing response, or unpaired cumulative snapshot poisons to absent; a later matching notification does not heal a mixed turn. **ACP `UsageTracker` identity fold**: per-in-flight-turn tri-state identity accumulator replacing last-update-wins. Any absent identity on a token-advancing notification or exact mismatch poisons to absent; poison survives later updates; reset in `begin_turn()`/`take()`; reset also when a request fails (baseline cleared so preflight gate cannot stay frozen sub-threshold on retries). **M3 migration**: adds `turn_cache_write_tokens`, `cumulative_cache_write_tokens`, `pricing_authority`, `pricing_model`, `pricing_cache_class` to `agent_metric_index`. Additive, idempotent, guarded per-column by marker. M2 migration also guarded per-column (turn and cumulative cache-read columns checked and added independently; marker commits only after both are present). Fresh-DB schema includes all columns. **First-turn baselines**: `seed_zero_baseline` seeds `last_input: Some(0)`, `last_output: Some(0)`, `last_cached_input: Some(0)`, `last_cache_write: Some(0)`, and `last_total: Some(0)` — all have the known-zero-at-spawn argument. Absent fields from incoming snapshots still produce unknown (tri-state unchanged). Sessions buzz-acp did not spawn (no seed) remain fail-closed on turn one. **`ReportedUsage` TS mirror**: `cacheReadTokens`, `cacheWriteTokens`, `freshInputTokens` added to `tauriArchive.ts` as `UsageField` members, field-for-field with the Rust struct. ### P4a — aggregation layer **Extended S-1 ladder** to cache-read and cache-write via the same `ladder_token` path as the existing token fields. **`freshInputTokens` derivation**: checked arithmetic, fail-closed — absent cache fields produce Unknown (not zero), overflow and `cacheRead+cacheWrite > input` both produce `incomplete: true`. Aggregated as a `UsageField`. **D6 comparator**: `sort_value()` = provider total when known, else `input+output` when both known, else `None` (unknown-last). Replaces the prior total-only comparator for both agent-level and model-level sort. Ships a pinned test vector that the TS render layer (P5) must match. ## Test coverage - `buzz-agent`: 440 lib + 15 integration (golden_transcripts) — includes 13 new `cache_total_state_tests`; 14 new `turn_io_state_tests`; 3 new `sum_usage_*` tests (exact single-field, exact two-field, overflow signals correctly); 3 new `parse_anthropic_*` tests (overflow flag set + value cleared, normal sum no flag, absent usage no flag); end-to-end golden transcript drives real subprocess with Anthropic-shaped `input_tokens: u64::MAX, cache_read: 1` response and asserts `accumulatedInputTokens` absent from the emitted `usage_update` — no logic duplication; 3 wire pin tests; 4 `fold_pricing_identity_*` tests; `pricing_authority()` explicit-:443 acceptance - `buzz-acp`: 700 tests (691 lib + 9 integration) — 4 new usage tests (absent input → unreliable+null; absent output → unreliable+null; goose-shaped both present unchanged; poison mid-session); 3 ACP behavior tests; 7 pool lifecycle tests - Desktop (Rust): 2259+ tests — 14 new P4a pinned tests; 2 M3 round-trip tests; 1 serde key-shape test; 2 M2 partial-schema migration tests; first-turn cache round-trip test ## Related PRs - P1 NIP-AM spec: [#4632](https://github.com/block/buzz/pull/4632) - P3 pricing table: [#4629](https://github.com/block/buzz/pull/4629) - UI (P5): [#4001](https://github.com/block/buzz/pull/4001) --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
44456e200e |
fix(desktop): resolve overlapping member mentions (#5225)
## Why Selecting or typing a member whose display name extends another member's name, such as `@Fast Fizz Codex`, could emit p-tags for both identities and wake the wrong agent. ## What - Resolve overlapping member-name matches by choosing the longest valid display name at each mention offset - Preserve separately typed short-name mentions at different offsets - Add regression coverage for selected team expansions and manually typed prefix collisions ## Risk Assessment Low to medium — this changes Desktop mention routing only. Exact mentions and distinct offsets remain supported; same-length ambiguous display names remain conservatively tagged because text alone cannot disambiguate them. Will resolve https://github.com/block/buzz/issues/2909 Generated with Goose Signed-off-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz> Signed-off-by: Atish Patel <atish@squareup.com> Co-authored-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz> |
||
|
|
97aa9e3185 |
fix(desktop): preserve Welcome banner dismissal (#5406)
## Summary - remove the complete Welcome guidance surface when dismissal reaches `hidden` - preserve dismissal across the private and starter Welcome channels for the active identity - assert the starter channel's actual `welcome-everyone` title on re-entry ## Why PR #5330 introduced two deterministic Desktop E2E failures: - the inner banner unmounted, but `welcome-composer-guidance-layer` remained - the re-entry test expected case-sensitive `Welcome` while navigating to `welcome-everyone` The state hook also scoped completion to channel IDs while `ChannelPane` remounts during navigation. The Welcome guidance is one experience spanning both Welcome channels, so completion now survives that remount while remaining identity-scoped. ## Validation At `b577eb42edffe889f63566f2457eacea720f3593`: - `pnpm -C desktop typecheck` - focused Biome check for all four changed files - E2E build - both `welcome-everywhere banner` integration tests repeated three times: **6/6 passed** - mandatory pre-push desktop check, typecheck, and full desktop unit suite: **4,535 passed** - `git diff --check` Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> |
||
|
|
f029deafae |
fix(desktop): welcome banner overlap and missing dismiss control (#5330)
## Problem The `WelcomeComposerGuidanceLayer` in the `#Welcome` channel was positioned with `absolute inset-x-0 bottom-full z-[-1]` — outside the `composerWrapperRef` measurement boundary. `useComposerHeightPadding` observes `composerWrapperRef`'s block size to set `paddingBottom` on the timeline scroll container, but the absolutely-positioned layer didn't contribute to that size. The banner sat directly on top of the newest message, blocking the thread affordance on that message, and had no manual dismiss control. ## Fix **Overlap**: Changed `WelcomeComposerGuidanceLayer` from `absolute inset-x-0 bottom-full z-[-1]` to `relative` (in normal flow). As a normal-flow child of `composer-dock`, the layer's full height is now measured by the ResizeObserver and fed into the timeline's `paddingBottom`, so the newest message is always fully visible and its thread affordance is always clickable while the banner shows. **Dismiss**: Added an `X` close button (`data-testid="welcome-composer-dismiss-button"`) on the prompt state. Clicking fires `onDismiss`, which drives `dismissing → hidden` immediately (same slide-down animation as the auto-dismiss path) and marks the channel ID as completed in the session ref so the banner does not reappear on channel re-entry within the session. **Refactor**: Extracted the banner state machine (refs, timers, `useEffect`s, and callbacks) from `ChannelPane.tsx` into `useWelcomeComposerBanner.ts`. This keeps `ChannelPane.tsx` well under the 1000-line file-size ratchet and makes the state machine independently testable. ## Changed files - `desktop/src/features/channels/ui/WelcomeComposerBanner.tsx` — `WelcomeComposerGuidanceLayer` positioning fix; `onDismiss` prop; dismiss button; `overflow-hidden` / `mb-0` / `flex-1` cleanup - `desktop/src/features/channels/ui/ChannelPane.tsx` — remove inline banner state machine, use `useWelcomeComposerBanner` hook, pass `onDismiss` - `desktop/src/features/channels/ui/useWelcomeComposerBanner.ts` — new hook owning all banner state --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz> |
||
|
|
fbf89e3bed |
fix(desktop): prevent horizontal clipping in Prompt Context modal (#5324)
The Prompt Context modal (observer feed → check icon under sent messages) was clipping all content and card right-padding at the dialog edge. **Root cause**: `PromptContextDialog` renders inside `DialogContent`, which is a CSS grid. The child flex wrapper had default `min-width: auto`, so the widest unbreakable token in the content (64-char hex event IDs, `Tags: [[...]]` JSON) set the grid track width, blowing it past `max-w-xl`. `overflow-hidden` then clipped everything at the dialog edge — including the section cards' right padding. **Fix**: - `AgentSessionTranscriptList.tsx`: add `min-w-0` to the `flex max-h-[85vh] flex-col` wrapper so the grid item can shrink below its max-content width. - `PromptSectionAccordion.tsx`: replace `wrap-break-word` with `wrap-anywhere` on the body text (open and collapsed states) and the title. `overflow-wrap: anywhere` reduces min-content width, which `break-word` does not, letting long tokens wrap inside the cards rather than inflating the track. The `line-clamp-2` collapsed preview is preserved unchanged. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |