mirror of
https://github.com/block/buzz.git
synced 2026-11-12 12:22:06 +00:00
dc32f27975ed01409712ffcbe6cb3843eb1886d0
2520 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dc32f27975 |
chore(sidebar): remove unused imports and dead vars from consolidated test wrappers
Remove unused isSubsumedBy/idsFromStore imports from stars/mutes storage wrappers (not consumed by runMergeLaneStorageSuite), drop RELAY_KEY from sections/sort sync wrappers (not consumed by the suite), remove unused makeStore function from sections sync wrapper (superseded by makeSectionsStore), and remove the makeStore parameter from runWholeBlobSyncSuite (declared but never accessed in the body). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
444dda87b7 |
refactor(sidebar): complete merge-lane storage + legacy-claim + test consolidation
Extract merge-lane storage helpers to mergeLaneStorage.shared.ts: - channelStarsStorage.ts + channelMutesStorage.ts now delegate readStore, writeOutbox, readOutbox, clearOutbox, reclaimSubsumedOutbox, makeStorageKey, and claimLegacy to the shared module. - channelSectionsStorage.ts imports claimLegacy from the shared module, removing the inline duplicate (sections' readChannelSectionsStore and outbox keep whole-blob watermark wiring). Parameterize remaining test mirrors: - mergeLaneStorage.shared.test.mjs: 47-invariant storage suite covering all stars/mutes storage behaviors. channelStarsStorage.test.mjs and channelMutesStorage.test.mjs become 32-line wrappers. - mergeLaneHook.shared.test.mjs: shared hook suite. useChannelStars and useChannelMutes tests become 22-line wrappers. - wholeBlobSync.shared.test.mjs: 17-invariant shared whole-blob sync suite. channelSectionsSync.test.mjs and channelSortSync.test.mjs call runWholeBlobSyncSuite + retain their lane-specific tests (serialized generations, ambiguous ACK, malformed payload, unsupported-version, reconnect adopts, durable outbox, failed publish). Every named regression from all prior review rounds survives identically, relocated to the shared suite or kept in the lane file. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
02a510cc05 |
test(sections-sync): parameterize stars/mutes test suites into shared helper
Extract mergeLaneSync.shared.test.mjs as a runMergeLaneSyncSuite() parameterized function covering all 14 merge-lane invariants (observe high-water, destroy cancel, generation CAS A-succeeds/A-fails, bounded backoff, retention-confirmation loser, boot-seed guard suite, failed fetch retain, unreadable head retain, unsupported schema retain, and timestamp clamp). Replace the 668-line channelStarsSync.test.mjs and channelMutesSync.test.mjs with thin 14-line wrappers that call the shared suite with lane-specific config (Manager, outbox reader, watermark key, entry factory, publish/getPending/fetchRemote adapters). Every named regression survives identically — test titles are prefixed with the lane label so each pass/fail maps to its originating invariant. Total: 316 pass (was 315; one test was added by the shared teardown loop). Net test-file delta: -1300 lines. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
553aab4a02 |
refactor(sections-sync): extract shared WholeBlobSyncManager and MergeLaneSyncManager
The four sidebar sync engines (sections, sort, stars, mutes) shared nearly all implementation. The two whole-blob lanes (sections + sort) carried the same generation-CAS, outbox, watermark, retained-head confirmation, prior-gen fold, and bounded-retry machinery; the two merge lanes (stars + mutes) differed only in store type and the merge/subsumption functions. Extract: - wholeBlobSyncManager.ts: parameterized by WholeBlobLaneConfig<S> (kind, dTag, parse, serializePayload, writeOutbox, clearOutbox, storesEqual, isLocalNonEmpty). Preserves all pass-2/pass-3/r7 invariants. ChannelSectionSyncManager and ChannelSortSyncManager are now thin wrappers (~75 lines each) that pass lane config and expose their named API methods. - mergeLaneSyncManager.ts: parameterized by MergeLaneConfig<S> (kind, dTag, parse, serializePayload, mergeWithRemote, isSubsumedBy, storesEqual, observe, writeOutbox, clearOutbox, isLocalNonEmpty). ChannelStarSyncManager and ChannelMuteSyncManager are thin wrappers (~90 lines each). All 315 sidebar lib tests pass at this commit with no behavior change. Each lane wrapper still exposes its named public API (publishSections, publishSortPrefs, publishStars, publishMutes, fetchRemoteSections, subscribeToSections, etc.) so no hook changes are required. Co-authored-by: Will Pfleger <wpfleger96@gmail.com> Signed-off-by: Will Pfleger <wpfleger96@gmail.com> |
||
|
|
7c195d8fbd |
fix(sections-sync): guard storage handler against peer cache writes while pending, require version in sections parser
P1.1: Both whole-blob storage event handlers (sections + sort) replaced React
state unconditionally from the shared scoped cache, bypassing the
hasPendingEdit() guard that applyRemote uses for relay arrivals. A peer window
writing B1 while A has a pending edit (A1) caused the updater for A2 to derive
from B1, stranding A1's outbox record (Carl r8).
Fix: check hasPendingEdit() in both storage handlers; defer the peer cache
write until the pending edit's debounced publish-or-adopt cycle completes.
When nothing is pending the peer write applies immediately, preserving
cross-window state mirroring for the idle case.
P1.2: parseChannelSectionPayload did not check version; relay decryptAndParse
called it bare, so a future {version: 2} relay head parsed as v1 state instead
of triggering the retain/retry path. Sort, stars, and mutes all reject non-v1
in their parsers; sections was the odd one out.
Fix: add version !== 1 guard at the top of parseChannelSectionPayload. The
local-storage parse path (parseRaw) already checked version before calling the
parser, so no local-storage read is affected.
Tests: two new hook regression tests (sections + sort) for P1.1 using
StorageEvent dispatch; one new manager regression test and one new storage unit
test for P1.2; existing parser tests updated to include version: 1 in
previously version-less test fixtures.
Co-authored-by: Will Pfleger <wpfleger96@gmail.com>
Signed-off-by: Will Pfleger <wpfleger96@gmail.com>
|
||
|
|
af90b8a901 |
fix(sections-sync): gate baseline fold on prior-generation attempt, add regressions
Track attempt id → generation in ambiguousAttemptIds (Map<string,number>) instead of a plain Set. foldSupersedingAttemptWinner now requires that the mapped generation is strictly less than pendingGeneration, making it explicit that the fold only applies when a newer edit's baseline was poisoned by an older generation's unconfirmed attempt — not when the current generation's own in-flight attempt is the baseline. The generation invariant is structurally guaranteed (publishBaseline is frozen before the attempt id is known), but the explicit guard provides defense in depth against future code changes and makes the constraint self-documenting. Add two regressions per lane (sections + sort): - Single-edit confirmation-retain path: ACK resolves, confirmation returns retain, retry sees peer winner → adopt, do not republish. Companion test proving correct adopt behavior in the no-second-edit path. - Queued-during-confirmation: second edit queued after ACK but before confirmRetainedHead resolves; stale-gen adoptRemote folds winner into baseline; second edit's pre-publish check publishes above true head. Mutation: reverting adoptRemote fold fails this test. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
5021d34873 |
fix(sections-sync): repair poisoned baseline on same-second collision mid-confirmation
When a second edit is queued after a publish ACK but before confirmRetainedHead resolves, pendingGeneration bumps while the first edit's confirmation fetch is in flight. The second edit's publishBaseline is frozen against the attempted (non-retained) event id. When confirmation returns the peer's same-second lower-id winner via adoptRemote with stale gen, the phantom baseline causes the second edit's pre-publish check to see the winner as having advanced past our attempt and adopt it away — the user's second edit is silently lost (Carl r6 P1). Extract foldSupersedingAttemptWinner (shared helper, both managers): when publishBaseline.eventId is one of our own attempted ids and the remote is the same-second lower-id peer winner, fold the winner into the baseline. The pre-publish check then sees equality and publishes above the true retained head. Scoped to same-second lower-id only — a strictly-later remote is a genuine advance that the pending edit must still adopt (pass-2 invariant). Invoke from adoptRemote stale-gen path (confirmation variant) and from fetchOwnBlobBeforePublish (confirmation-retain/retry variant). Both managers (sections, sort) mirrored. Add one regression per manager: pauses the confirmation fetch, queues the second edit during the pause, then releases with the peer winner. Asserts the second edit publishes above the winner and is confirmed retained rather than adopted away. Mutation: reverting the adoptRemote fold fails the test. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
e0a16fe12f |
fix(sync): roll back migration scoped copy only when legacy is provably present
The pass-3 catch rolled back the just-written scoped copy on any failure. If the legacy delete succeeded but the confirmation read then threw, legacy was already gone, so the rollback destroyed the only surviving copy — permanent preference loss. Roll back only when the legacy key is provably still importable (keeping the scoped copy then would let a second relay scope double-import it). If legacy is gone, the scoped copy is the sole copy and is kept; if the probe itself throws, keep it too, favoring no-data-loss. The read-time ownership gate keeps a kept-but-unproven copy hidden while legacy remains, so it never seeds early. Residual: legacy-delete and catch-probe both throw, then a relay switch after partial recovery can let both scopes carry the value — bounded and deliberately preferred over data loss. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
950b8f6247 |
fix(sync): prove legacy migration ownership at read time
The migration wrote the scoped key before deleting the legacy key. If the legacy delete threw and the rollback delete also threw, the scoped copy survived and the next read gave it precedence, exposing legacy prefs to seed-publish while the legacy key was still importable by another relay. Move the ownership check into the reader: a scoped copy is exposed only while no importable legacy key remains, so a half-migrated copy can never be read as owned. The claim completes inline once storage recovers. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
0d9b8d7933 |
fix(sidebar): expose migrated legacy prefs only once the legacy key is gone
Thufir's r6 review found the legacy→scoped migration exposed legacy data to bootstrap even when the scoped write threw or the legacy delete did not take, wrapped in a swallowed try/catch. On any storage failure the legacy key survived while the relay still received the legacy preferences and could seed-publish them; a second relay later read the same legacy key and seeded it too — so "globally one-time" held only on the happy path. migrateLegacyStore now exposes migrated data only when ownership is proven: write the scoped key, delete the legacy key, and confirm the delete took. On a scoped-write throw, a legacy-delete throw, or a delete that silently does not take, roll back the partial scoped copy and return DEFAULT_STORE, leaving the migration retryable. An empty store cannot seed-publish (bootstrap's non-empty-local gate), so DEFAULT is safe. The identical flaw existed in all three lanes with a legacy primary-cache migration — sections (pre-existing on main), stars, and mutes. Sort has no such migration and is untouched. Concurrent claimants are safe: every window resolves the same app-wide active community, so at any instant they scope to the same relay and any race migrates the identical value to the identical key; a second relay only enters play after a community switch, which is strictly after the first scoped read consumed the legacy key. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
a55b032674 |
fix(sidebar): confirm retained head after ACK and relay-scope stars/mutes
Carl r6 raised two P1 correctness gaps. Whole-blob lanes (sections, sort) treated a resolved publishEvent as proof of retention, but the relay OKs a superseded NIP-33 write as a no-op (Duplicate). Two windows stamping the same created_at both get OK while only the lower event id is retained, so the loser recorded a nonexistent head as its baseline and its next edit adopted the true winner away. After ACK the lanes now fetch the authoritative head and only clear/fold/discard on an exact event-id match; a different readable head is adopted, an unprovable/unreadable/own-prior head is retained and retried. The stars/mutes primary cache was pubkey-only while its outbox was relay-scoped, so a first visit to relay B seed-published relay A's preferences. The cache key is now relay-scoped like sections/sort, with a one-time legacy migration that deletes the pubkey-only key so no other relay can import it. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
8bbc11b340 |
fix(sidebar): reject rev at MAX_SAFE_INTEGER boundary to keep mint headroom
The rev parse bound used Number.isSafeInteger, which accepts Number.MAX_SAFE_INTEGER itself. The click path mints rev = maxRev + 1, so an accepted boundary rev overflows to an unsafe value that never advances again — recreating the same-second toggle wedge the bound exists to prevent. Reject rev >= Number.MAX_SAFE_INTEGER (exclusive, normalize to 0) so maxRev + 1 always stays safe. updatedAt keeps its inclusive bound: the click path takes Math.max(now, observed), never increments, so it cannot overflow. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
baf3e4083a |
fix(sidebar): retain over failed/unreadable heads and bound rev parse
Extends the retain-on-unreadable pattern to the two remaining overwrite paths and hardens the rev/timestamp parse boundary. - All four lanes tri-state the pre-publish read: a THROWN fetch (timeout / auth / socket) now retains and retries instead of falling through to publish, so an edit during a transient outage can no longer sign above a stale watermark and erase an unseen newer head. - The merge lanes (stars/mutes) retain when the existing head fails decryption/JSON/schema parsing rather than publishing the local store over it — a max-merge is only safe once both operands were read, matching the whole-blob lanes. - The rev/timestamp parsers require Number.isSafeInteger: an unsafe rev is normalized to 0 and an unsafe timestamp entry is dropped, so a malformed huge-rev true entry can no longer wedge later false toggles forever. Retain reuses the existing bounded-backoff retry and generation CAS, so a later readable/successful head resumes normal resolution. Regressions cover all four lanes for the failed-fetch and unreadable-head paths and the rev-wedge P2 case in both storage suites. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
6078943f3b |
fix(sidebar): harden sidebar-sync durability, LWW, and mixed-fleet residual
Close the pass-3 review blocker and Carl's four P1s on the channel sections/sort/stars/mutes relay sync, all in the sidebar lib: - Gate the legacy-consumed marker on a proven v2 transfer. writeOwnOutbox now returns whether the fresh key's setItem succeeded; the whole-blob hooks write the marker only when durable, so a quota failure leaves the legacy record replayable instead of silently suppressing the only copy. - Reconnect wakes the existing pending edit (retryPendingPublish) rather than re-queueing via publish*(), which reset the frozen baseline and published a stale edit over an advanced remote. - Merge lanes clear an own outbox only after an authoritative retained-head fetch proves the head subsumes the attempted store; a bare EVENT OK on a superseded NIP-33 write no longer drops the loser's durable click. - An unreadable/unsupported pre-publish head retains the pending edit and retries rather than overwriting state the client could not inspect. - Document the accepted mixed-fleet residual (a same-second old-build click reads rev 0 and loses to an earlier new-build rev, healing on the next later-second click) and pin it with a test; no protocol change. Regressions added for each item, mutation-verified where the fix lives in the hook. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
3712e6fb2c |
fix(sidebar): make retained legacy whole-blob replay one-shot per value
The legacy v1 shared outbox key is never deleted (it is mutable and a concurrently-live old build may still rewrite it), so the whole-blob resume path re-read it on every boot and republished the stale blob above the current relay head forever. A found relay head never stopped it: a fresh manager has no lastPublishedStore and queueing the replay freezes publishBaseline to the just-fetched head. Distinguish compatibility replay from permanently pending intent with a durable per-value consumption marker, whole-blob lanes only. resumeWholeBlobOutbox excludes the legacy record when its exact raw matches the stored marker; a live old build rewriting the key stores a different raw and is replayed again. The hook transfers the intent into its own v2 key (synchronous publish) BEFORE writing the marker, so a crash in that gap replays the blob once more rather than losing it. Merge lanes (stars/mutes) need no marker but gained a head-subsumed gate so a lingering legacy key does not re-drive an identical boot-time publish. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
323bf68107 |
fix(sidebar): make outbox records write-once so foreign reclaim is race-free
The per-window token/recheck reclamation still performed a non-atomic compare-then-delete on a mutable foreign key: a live owner could rewrite its key between the reclaim decision-read and the removeItem, and the whole-blob `queuedAt <= head.created_at` gate dropped same-second and legacy `queuedAt=0` records that had not provably lost LWW. Records are now write-once: a key is `<prefix>:<pubkey>:<relay>:<nonce>:<seq>` and is never rewritten. A new edit writes a new key (next zero-padded seq) then deletes its own older keys (write-before-delete, so a crash leaves at least one record). Foreign reclamation reads an immutable record, proves it reclaimable against durable relay evidence, and deletes it with no recheck. Whole-blob supersession is strict (`queuedAt < head.created_at`); replay runs before reclamation in every hook so a same-second record is consumed into pending first; the legacy v1 shared key is only ever replayed, never deleted. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
adb3d17c19 |
fix(sync): key the sidebar durable outbox per window
localStorage has no atomic compare-and-delete or transactional read-modify-write, so a single outbox key shared across every window could not be mutated safely: one window's read-then-write or read-then-remove races a peer's write in the gap and drops its still-unpublished edit. A per-write ownership token narrowed that window but could not close it — the token proves what was read, not what is still stored at the destructive op. Key the outbox per window instead: <prefix>:<pubkey>:<relay>:<nonce>, where the nonce is minted once and parked in sessionStorage. Each window is the sole writer of its own key, so a hot-path write is one unconditional setItem — the write race is designed out, not guarded. Resume enumerates every window's key: merge lanes (stars/mutes) fold all records order-independently; whole-blob lanes (sort/sections) replay the max-queuedAt record with a nonce tiebreak. Redundant foreign keys are reclaimed at boot, gated on durable relay evidence (merge: head subsumes; whole-blob: head created_at supersedes) and re-read immediately before removal so a live peer's fresh write in the recheck gap survives. Reclamation runs only on a successful head fetch, never on a failed one. The token contract is deleted entirely — ownership is the key. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
9141fe2922 |
fix(sync): make the sidebar durable outbox multi-window safe
Sections, stars, mutes, and sort each persist an unpublished edit under one
localStorage outbox key per identity+relay, but generation ownership is only
in-memory per window. Without cross-window ownership, one window's completing
publish could clear a peer window's still-unpublished edit, and (on the merge
lanes) a peer's write could overwrite an edit before it published.
Every outbox write now mints an ownership token and stores a {store, token}
envelope; a completing publish compare-and-clears only when the stored token
still matches its own, so a peer's newer write survives an older window's ACK.
Stars and mutes additionally read-merge-write both the durable outbox and the
main store via their per-entry mergeStores, so two windows editing different
channels both survive; sort and sections replace whole-blob with LWW resolution
matching the relay. Sort also gains the durable outbox + bounded retry it
previously lacked, and stars/mutes/sort now clamp publish created_at inside the
relay's future-drift window like sections. The envelope reader tolerates a
legacy token-less entry so an outbox written by a prior build still resumes.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
|
||
|
|
c7933fd9da |
fix(sync): clamp publish timestamps and give sort prefs the durable lane
Carl's re-review found three edit-loss paths remaining after the stars/mutes rev-merge landed. [P2] Stars, mutes, and sort stamped createdAt = lastRemoteCreatedAt + 1 uncapped, so a self-authored head accepted near the relay's +900s drift boundary wedged every later publish until wall time caught up. Extract the sections clamp into a shared clampPublishCreatedAt in sidebarSyncWatermark.ts (all four surfaces already import that module) and wire sections/stars/mutes/sort to it. [P1] Sort preferences never got the durable lane: doPublish only logged on failure, destroy() discarded the pending edit, and a live remote cancelled the debounce with the edit stranded. Port the sections lane — durable outbox + bootstrap resume, generation/CAS ownership, single- flight + completion re-drive, 2s->30s backoff, 60s reconcile loop, and pending-aware remote application. Sort stays whole-blob LWW; a lost head is adopted at pre-publish rather than republished. Tests: a clamp test per surface, and sort lifecycle coverage — outbox resume after destroy-inside-debounce, retry without a later edit, overlapping-generation safety, live-remote-during-debounce adopt, and a hook-level pending-defer test. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
16aeab68b0 |
test(sidebar): make the two accepted-residual tests faithful to their contract
The hard-eviction-branch test asserted only the rev tuple outcome on two one-entry stores while its comment claimed the >500-entry eviction/remount setup. Build the real fixture: >500 equal-updatedAt entries so the bound evicts the target by the id tiebreak, then a remounted rev-1 click merged against the retained rev-100 remote, asserting the deterministic rev-100 outcome in both merge orders. Stars and mutes. The unobserved-future mixed-fleet residual was stated but never exercised directly: the fast-clock suites cover only the observed-future fix. Add a click with an empty high-water at t, then a genuinely unobserved opposite-value head at t+300 that wins on the primary updatedAt key. Both hook suites. Test-only; no production source changes. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
439df98240 |
refactor(sidebar): fold channel stars/mutes onto Lamport-rev max-merge
Replace the LWW register plus ownership/dirty-set/canonical-supersession machinery with a per-entry Lamport `rev` and a single max-merge on every path, mirroring the read-state data model. Each entry carries an additive optional `rev` (missing implies 0; payload stays `version: 1` so older builds keep parsing our blobs). One `mergeStores` orders by updatedAt then rev then the starred/muted-true leaf, and ends in the 500-entry bound. Clicks stamp `updatedAt = max(now, localEntry?.updatedAt ?? 0, maxUpdatedAtSeen(id))` and mint `rev = max(localEntry.rev, maxRevSeen(id)) + 1`, so a click strictly dominates every state its replica has observed and cannot lose to a same-second remote. The sync managers hold a per-channel two-field high-water map fed by a single `observe()` on every ingest path. Stars sync keeps the generation-CAS + single-flight lane and bounded-backoff retry plus a durable outbox so an in-flight publish can never clear a newer pending edit; mutes sync mirrors it. Remote ingestion never touches the pending lane. Deleted: mergeApplyingRemote, mergeStoresWithTie, mergeCanonicalSupersession, dirtyChannelIds, lastAppliedRemoteTs/lastAppliedEventId, and the event-clock branch. Sections and sort are untouched. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
51af859759 |
fix(sidebar): overlay local edits on canonical correction and fold ambiguous-ACK heads
Round-4 client-side convergence fixes for channel-sections/stars/mutes sync, closing two silent edit-loss variants that survived publish serialization. Ambiguous-ACK fold: a publish whose ACK is lost may still have been accepted by the relay. Retain each attempt's signed id; when a later cycle's pre-publish fetch returns a head whose id matches a prior attempt, fold it forward as our own accepted predecessor and publish above it instead of adopting it away and erasing the queued edit. A head the relay never accepted can never surface by id, so the fold is proof-gated on an exact id match. Canonical-supersession dirty overlay (stars + mutes): a lower-id canonical correction that arrives after a same-second local click must not clobber the click. Apply the correction to the prior remote layer, then overlay entries changed locally since that layer; never cancel a pending publish merely because a correction arrived. Client-only: loss discovery relies on the existing pre-publish fetch, live subscription, and reconcile loop rather than a relay conflict signal. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
d94b8bb22b |
fix(sidebar): serialize section publishes and scope remote-tie merge
Each prior round patched one cross-generation interleaving and opened another a layer deeper. Kill the race class structurally instead. Sections: serialize publish cycles (one in-flight at a time; a newer edit queued mid-cycle defers and the completion re-drives it). The per-edit pre-publish baseline is frozen at queue time, so a genuine remote observed during the debounce window still adopts, while our own accepted head is folded forward via canonicalMax so a stale generation's own write is never mistaken for a competing remote and adopted away. Dual generation guards in doPublish (post-fetch and pre-publish) stop a stale generation signing or publishing after a newer edit exists. Stars/mutes: scope mergeApplyingRemote (remote-wins on entry-tie) and the pending-publish cancel to fire only on a canonical supersession of an already-applied same-timestamp larger-id head. Every other application (bootstrap/live/newer-timestamp) keeps local-wins mergeStores and does not cancel the pending publish, so a later same-second local click is no longer clobbered by an older remote entry that decrypts late. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
53ba5ab4b1 |
fix(sidebar): close pre-publish and per-entry merge convergence holes
Two convergence holes one layer under the pass-1 fixes: Sections: the pre-publish head check compared the fetched head against the mutable lastRemoteCreatedAt, which a live event observed during the debounce window already advanced to that same head — equality fell through to publish and the local blob overwrote a remote that became head after the edit was queued. Freeze a canonical head baseline (created_at, id) at publishSections and compare the fetched head against that generation baseline instead, adopting when the head advanced. Stars/mutes: applyRemote admits the canonical lower-id winner but then mergeStores resolved equal per-entry updatedAt as local/prev-wins, so a stale larger-id value delivered first survived and undid the winner. Add mergeApplyingRemote which resolves an entry-timestamp tie toward the canonical incoming blob while keeping strictly-newer local entries. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
d5d83e0909 |
fix(sidebar): correct equal-timestamp tie-break in stars, mutes, sort
useChannelStars, useChannelMutes, and useChannelSortPreference carried the same inverted equal-timestamp comparator as channel sections: applyRemote kept the largest event id, opposite the relay/database canonical order (created_at DESC, id ASC -> lowest id wins). Two devices writing the same second could leave the UI showing an event the relay did not store. Apply a strictly-lower id and ignore ids >= the last applied, matching the sections fix and the relay winner across all four 30078 sidebar surfaces. Each hook gains a regression test: larger-then-lower id delivery at equal timestamp, lower-id store wins (mutation-checked - reverting >= to <= fails each). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
1046e88904 |
fix(sidebar): close pending-edit convergence races in channel sections
Three cross-layer races defeated the one-convergence-mechanism design: - An older in-flight publish unconditionally cleared pending state on completion, erasing a newer edit queued mid-flight. Each pending edit now carries a monotonic generation; a completion clears pending/outbox/retry only via compare-and-swap on the generation it published. - Hook-level remote application (bootstrap/live/periodic) cancelled the pending publish's timers without deciding supersession, stranding the durable outbox and clobbering the optimistic edit. applyRemote now defers entirely to a pending edit, whose own debounced publish converges via publish-or-adopt; the manager's adopt path clears pending before write-through so the winning remote still applies. - The equal-timestamp tie-break kept the largest event id, opposite the relay/database canonical order (created_at DESC, id ASC → lowest id wins). applyRemote now applies a strictly-lower id and ignores ids >= the last applied, so the UI converges on the event the relay actually stored. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
a05280f164 |
fix(desktop): converge channel sections on conflict and never drop edits
Channel-section sidebar state diverged between a user's devices and sometimes never self-healed. Four client-side gaps fed the divergence: - A local edit that lost whole-blob LWW was silently republished as remote content while the UI kept showing the edit. Now the manager adopts the winning remote head (writes it through to state + storage, advances the watermark) and skips publishing, unifying with the relay's OK-false conflict path as one convergence mechanism. - Edits made inside the 2s publish debounce were dropped on quit or community switch. A durable localStorage outbox persists every edit synchronously and resumes it on next mount; adopt clears the outbox so a superseded edit can never be replayed. - A skewed remote head could push the published createdAt past the relay's future-drift window and wedge all later publishes. createdAt is now clamped inside that window. - Stale-at-open state waited for a reconnect that a healthy socket never fires. A reconciliation loop periodically refetches the head (steady 60s, backoff on failure) and refreshes on window visibility. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
ef0d202568 |
refactor(db): finish replaceable event store extraction (#6777)
## Why Finish the replaceable-event slice of [tracker #2](https://github.com/TheSentinel454/buzz/issues/2) and [domain issue #6](https://github.com/TheSentinel454/buzz/issues/6) without disturbing the runtime/store boundary established by #6660 and #6668. PR #6700 has merged; this PR now targets current main containing its replacement lock and transaction observability. ## What - Move `Db::replace_addressable_event`, its SQL, lock/transaction instrumentation, and focused addressable/parameterized replacement tests from `lib.rs` to `replaceable.rs` - Preserve the transaction-required parameterized API, replacement ordering, rollback semantics, mention indexing, and exactly one datastore span per public operation - Keep implementation and focused PostgreSQL tests co-located; follow-up review removed the dedicated replaceable ownership source guard as low value ## Stack - Exact base: main at |
||
|
|
dcee330b5c |
Fix Admin feedback filter overflow (#6825)
## Why Long community hostnames can make the native Community select exceed the Admin feedback filter grid, pushing the Received and Status controls outside the filter bar and creating horizontal overflow. ## What - Allow filter labels and selects to shrink within their grid tracks - Add a browser regression test covering an unusually long community hostname ## Risk Assessment Low — this is limited to Admin feedback filter sizing and preserves the existing desktop and mobile grid layouts. ## Before The Community select expands past the filter bar and pushes the Received and Status controls outside it.  ## After Long community names remain contained, keeping all four controls inside the filter bar.  ## References - Verified on Blox at the local checkout revision: Admin checks passed and all 9 Playwright tests passed Generated with Codex |
||
|
|
db514b153a |
fix(desktop): stop pulsing addressed agents on send (#6873)
## Summary - Stop replaying the selected-agent pulse when a remembered agent remains addressed across a send. - Preserve the existing pulse for genuine selections and shake feedback for failed sends. ### Related issue Reported in Buzz: buzz://message?channel=2745947d-f905-4f31-8a3f-fc4485d3c36c&id=0cffab376186be9cc4e7f29027352cc1c7941aead48b3fed948f36181f4a59af ### Testing - `cd desktop && pnpm test` (5,556 passed) - `cd desktop && pnpm typecheck` - `cd desktop && pnpm check` - `cd desktop && pnpm build:e2e` - `cd desktop && pnpm exec playwright test tests/e2e/persistent-agent-audience.spec.ts --grep 'always-mentioned agents remain selected|failed always-mentioned send'` (2 passed) This changes animation timing only; the regression is covered by Playwright assertions on the pulse and shake versions, while a static screenshot is unchanged. Signed-off-by: Matt Toohey <contact@matttoohey.com> |
||
|
|
4f529a45f2 |
fix(desktop): prioritize sidebar channel status (#6861)
## Summary - Treat the sidebar temp-channel icon as a fallback behind working, mute, numeric unread, and thread-unread indicators. - Keep the header affordance and the sidebar temp icon when it is the only row status. - Extend the ephemeral-channel browser scenario across working, idle, collapsed-sidebar, and unread states. ## Before / After | Before: five temp icons compete with duration and unread states | After: all five duration and unread states remain clear | | --- | --- | |  |  | ## Testing - `node --import ./test-loader.mjs --experimental-strip-types --test "src/**/*.test.mjs"` (5,556 passed) - `tsc --noEmit -p desktop/tsconfig.json` - `biome check desktop/src/features/sidebar/ui/SidebarSection.tsx desktop/tests/e2e/channels.spec.ts` - `vite build --mode e2e` - Isolated Playwright run of `create ephemeral stream shows sidebar and header affordances` (1 passed) ## Duplicate check No matching open PR or issue found for `ephemeral sidebar icon`. Signed-off-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz> Co-authored-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz> |
||
|
|
e8172b5bac |
feat(desktop): hyperlink selected composer text on link paste (#6684)
## Summary Pasting a link while composer text is selected now turns that text into a hyperlink instead of replacing it — the behavior people expect from every other editor. - `resolveExactLinkPaste` recognizes a clipboard payload that is exactly one supported link (bare or `<…>`-wrapped `http(s)`, or a Buzz deep link, canonicalized through the existing composer-link attribute resolver) and returns the href to apply. - The paste handler applies that href as a `link` mark over the current selection, collapses the caret to the end of the newly linked range, clears stored marks so subsequent typing isn't linked, and refocuses the editor. Edge cases handled: - **No link mark available / selection can't carry marks** — falls through to the existing replace-with-node path rather than silently dropping the paste. - **Selection contains a composer message-link chip** — keeps the old replacement behavior instead of nesting a link over a chip. - **Whitespace in the clipboard payload** — `unwrapExactHttpLink` now rejects any text containing whitespace, so multi-line or "text plus URL" pastes are not treated as an exact link. - **Empty selection (caret paste)** — unchanged. - **Idempotent re-paste** of an already-wrapped link is consumed rather than doubling up. ## Testing `desktop/src/features/messages/lib/composerMessageLinkNode.test.mjs` gains coverage for exact-link resolution (per-shape accept/reject cases, Buzz canonicalization) and for the paste handler: linking selected text, canonicalizing Buzz links over a selection, idempotent wrapped-link paste, fall-through when the selection can't carry marks, all-selection collapse, chip-containing selections, and preserved caret-paste behavior. Full pre-push gate ran green on this branch: `file-size-check`, `desktop-check`, `desktop-typecheck`, and `desktop-test` (5425 tests, 0 failures). ## Demo Pasting an `https://` URL over selected composer text turns the selected words into a hyperlink (instead of replacing them); text typed immediately after is not linked: https://github.com/user-attachments/assets/bbc93574-dd9d-4b1b-bad0-53307b6e887c --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> |
||
|
|
52621c09be |
chore(release): release Buzz Desktop version 0.5.20 (#6839)
## Buzz Desktop release v0.5.20 - **Frozen main:** `8471049c430073474939336dfc6aa98272bc8762` - **Reviewed candidate:** `95154bee4034ca7a40b33095c2ddbde8c9aa1614` - **Previous desktop release:** `desktop-v0.5.19` - **Proposed immutable tag:** `desktop-v0.5.20` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
8471049c43 |
feat(desktop): add KLIPY GIF search to composers (#5554)
## Summary - Supersedes #1913 with a KLIPY-hosted URL implementation. - Adds KLIPY GIF search and trending results to desktop message and forum composers. - Keeps selected GIFs hosted by KLIPY; Buzz stores only the external URL and media metadata (no imeta tag, since relays only accept hash-backed local `/media/` entries). - Aligns the Emoji/GIF picker with Buzz's standard segmented control, theme surfaces, and motion behavior. ## Relay-to-provider boundary - The relay proxies KLIPY search/share so the `BUZZ_KLIPY_API_KEY` never reaches the desktop; the key stays server-side behind a redacted `Debug` impl. - The dedicated GIF `reqwest` client sets `redirect::Policy::none()`. Because the API key rides in the request path, following a provider `3xx` could replay a key-bearing URL to an attacker-chosen host (an SSRF/key-disclosure primitive). With redirects disabled, a `3xx` returns as a non-success status that the handlers map to a generic `502`; the `Location` target is never read or forwarded. - Admission reuses the established NIP-98, membership, replay, and per-pubkey rate-limit gates, with an upstream response-size cap and allowlisting so KLIPY error bodies never cross the relay boundary. ## Accessibility - Under `prefers-reduced-motion: reduce`, the picker grid renders a static provider poster (a normalized `jpg` asset) instead of the animated preview, or a named static placeholder when no poster is available. It reacts to preference changes while mounted. `no-preference` keeps the animated preview. - Selected GIFs carry their title through `ImetaMedia`'s `displayLabel`, so the composer thumbnail, preview dialog, editor, lightbox, and remove control all derive one non-empty accessible name instead of an empty `Attachment ` label. Ordinary hashed uploads keep their existing hash-derived names. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz> |
||
|
|
e8cd7516e6 |
fix(desktop): respect automatic mention preference after send (#6837)
**Category:** fix **User Impact:** Disabling automatic agent mentions now keeps one-time agent mentions out of the next message draft. **Problem:** A successfully sent inline agent mention was treated as eligible for post-send restoration even when **Automatically mention agents** was disabled, so the agent immediately reappeared in the composer. **Solution:** Gate the send-success restoration path on the live preference while leaving explicitly pinned agents and enabled automatic mentions unchanged. <details> <summary>File changes</summary> **desktop/src/features/messages/ui/MessageComposer.tsx** Skips the automatic post-send mention restoration path when the preference is disabled. **desktop/tests/e2e/persistent-agent-audience.spec.ts** Reproduces disabling the toggle before sending an inline agent mention and verifies the next composer is empty while the outgoing mention still reaches the agent. </details> ## Reproduction steps 1. Enable **Automatically mention agents** in the composer mention options. 2. Disable it again. 3. Compose and send `@Agent test` using the inline mention picker. 4. Confirm the message still mentions the agent, but the cleared composer does not repopulate `@Agent`. 5. Re-enable the preference and repeat; confirm the mention is restored for the next message. ## Testing At `a179a2d00a6ac5cf0bc4a1c7c1ef3d5dfa9d0875`: - Full desktop unit suite: 5,501 passed. - Desktop check and typecheck passed in the pre-push hooks. - Desktop file-size ratchet passed in the pre-push hooks. - Full `persistent-agent-audience.spec.ts` Playwright file: 17 passed, including preference off (empty composer), preference on (restored mention), and the existing address-undo journey. ## Visual validation | Automatic mentions off | Automatic mentions on | | --- | --- | | After sending a one-time mention, the outgoing message is preserved and the next composer stays empty. | After sending, the agent remains addressed and is restored in the next composer. | |  |  | <details> <summary>Original regression</summary> With automatic mentions disabled, sending `@Alia test` still repopulated `@Alia` in the cleared composer.  </details> Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> |
||
|
|
820a858997 |
fix(release): attribute desktop candidates to the operator (#6831)
The desktop release tooling hardcoded one contributor's personal identity into every release candidate commit. `scripts/prepare-desktop-release.sh` committed the candidate with a `git -c user.name='Wes' -c user.email='wesbillman@users.noreply.github.com'` override, and `scripts/desktop_release.py` `validate` required the candidate author to be exactly `Wes <wesbillman@users.noreply.github.com>` plus a matching `Signed-off-by` trailer. That leaked from Wes's working setup into the validation contract in #3568, so a release cut by any other operator was falsely attributed to and signed off by Wes (as happened on #6828). ## Change - `prepare-desktop-release.sh`: drop the `-c` identity overrides so `git commit -s` uses the operator's own configured identity to author and sign off the candidate. The automation `Co-authored-by` trailer is unchanged. - `desktop_release.py` `validate`: replace the exact-Wes checks with structural ones — the commit author must be non-empty, the body must contain a `Signed-off-by` trailer whose name and email match the commit author (honest DCO), and the existing automation `Co-authored-by` regex check stays. Failure messages remain specific. - `test-desktop-release-candidate.sh`: the fixture candidate now commits under the harness's own identity, and a new negative case rewrites the author to a mismatched identity and asserts the validator rejects it. Release authorization is bound to the merged PR via the GitHub API in `scripts/verify-desktop-release-merge.sh`, never the commit author field, so this does not weaken the trust model. `RELEASING.md` and `.github/workflows/desktop-release-candidate.yml` reference no author identity and need no change. Verified locally: `scripts/test-desktop-release-candidate.sh` passes, including the new sign-off/author-mismatch rejection. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
cae7f826b3 |
fix(ci): check out source in docker.yml merge job (#6833)
## Problem Every `docker.yml` run on `main` has failed since [#6781](https://github.com/block/buzz/pull/6781) merged. That PR added a "Create deployment eligibility predicate" step to the `merge` job, which runs `jq` against `$GITHUB_WORKSPACE/scripts/create-deployment-eligibility-predicate.jq`. But the `merge` job has no `actions/checkout` step — the workspace is empty, so `jq` can't open the file and exits `2` (`jq: Could not open ... No such file or directory`). The `build` and `qualify` jobs each check out the source; `merge` never needed one until this step was added. ## Fix - `.github/workflows/docker.yml`: add `actions/checkout` (same pinned SHA as the other jobs, `df4cb1c` / v6.0.3) as the first step of the `merge` job. - `scripts/test-relay-image-eligibility-workflow.sh`: guard the regression by asserting the `merge` job checks out the source before building the predicate. ## Verification CI cannot exercise the `merge` job on a pull request — the job is gated `if: github.event_name != 'pull_request'`, so it only runs on push to `main`. The proof is the root cause (missing checkout for a step that reads a repo file) plus the eligibility workflow test: `scripts/test-relay-image-eligibility-workflow.sh` passes with the fix and fails with the new guard's specific message (`merge job must check out the source before building the eligibility predicate`) when the checkout is removed. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> |
||
|
|
7a1b7d8e09 |
chore(release): release Buzz Desktop version 0.5.19 (#6828)
## Buzz Desktop release v0.5.19 - **Frozen main:** `ee6ca5fa28bce04dfecb6717de65b08a57f2ac47` - **Reviewed candidate:** `ab691bcdaeccaa6698a2199beaba3f6e93daae81` - **Previous desktop release:** `desktop-v0.5.18` - **Proposed immutable tag:** `desktop-v0.5.19` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
ee6ca5fa28 |
Remove public relay signing key fallback (#6729)
Require `BUZZ_RELAY_PRIVATE_KEY` on every relay startup and remove the shared public fallback key completely. For local development, `just bootstrap` now generates a random key once in the gitignored `.env` file. Re-running bootstrap preserves that key, so relay-authored events retain the same identity across restarts. The local relay recipes reload `.env`, while isolated CI and E2E launchers pass explicit per-run keys. Startup fails immediately when the key is missing or invalid, before connecting to Postgres or Redis. Deployed relays continue to receive their stable key from the existing chart-managed secret. Validated with: - `./scripts/test-ensure-local-relay-key.sh` - `cargo test -p buzz-relay --bin buzz-relay` - `cargo clippy -p buzz-relay --bin buzz-relay -- -D warnings` - `cargo fmt --all -- --check` - `shellcheck scripts/ensure-local-relay-key.sh scripts/test-ensure-local-relay-key.sh` Signed-off-by: Jordan Mecom <jm@squareup.com> |
||
|
|
22f32c99e2 |
docs(nest): make commit attribution policy-neutral (#6707)
## Summary - replace the generated Nest's unconditional human author/sign-off rules with portable guidance that separates authorship, material co-authorship, DCO certification, and cryptographic signing - defer attribution to repository-local policy, forbid inferred or guessed identities, and require inspection of every outgoing commit - bump the Nest template version so existing installations refresh, with regression coverage for fresh generation and upgrade preservation ### Related issue None found. Related runtime identity work exists in #6177, but this PR is intentionally limited to the generated Nest guidance and its refresh behavior. ### Testing - `bin/just desktop-tauri-clippy` - `bin/just desktop-tauri-test` - `bin/just file-size-check` - `git diff --check` - pre-push hooks: `push-head-scope`, `branch-skew`, `file-size-check`, and `desktop-tauri-checks` Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> |
||
|
|
b58de7cfa7 |
fix(desktop-messages): preserve inline agent mentions with persistent addressing (#6793)
**Category:** fix **User Impact:** Users can place `@Agent` mentions inline anywhere in a message—even when that agent is also persistently addressed—so separate agents can receive separate instructions in the same message. **Problem:** Selecting an agent from the composer mention picker could turn the selection into persistent addressing instead of leaving an inline `@Agent` at the cursor. That made persistent addressing and inline composition mutually exclusive: once the persistent behavior took over, users lost the clear `@Agent A do X, @Agent B do Y` message structure they previously had. **Solution:** Make inline mentioning and persistent addressing independent behaviors: - Ordinary mention selections always insert `@Agent` inline at the current cursor position. - This remains true when the agent is already persistently addressed; the persistent audience never blocks or replaces an inline mention. - If **Automatically mention agents** is enabled, a successfully sent inline mention may additionally make that agent persistent for later messages. Existing saved preferences remain respected. - The dedicated automatic-mention control and primary+Shift+Enter shortcut continue to add or remove persistent addressing directly. - For users without a saved preference, **Automatically mention agents** now defaults off. <details> <summary>File changes</summary> **desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.test.mjs** Updates preference coverage for the default-off behavior while preserving explicit saved choices. **desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.ts** Defaults automatic post-send persistence off when no valid preference exists. **desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs** Verifies ordinary picker selections insert inline mentions without changing or pulsing the persistent audience, including when the selected agent is already persistently addressed. **desktop/src/features/messages/ui/useAgentAddressLockPicker.ts** Keeps ordinary mention selection on the existing inline insertion path while preserving the separate persistent-address controls and shortcut. **desktop/tests/e2e/persistent-agent-audience.spec.ts** Covers separately targeted inline instructions to two agents, signed outgoing recipients, default-off persistence, and explicit opt-in persistence scenarios. </details> ## Reproduction steps ### Inline mentions without persistence 1. Open a channel with at least two available agents and leave **Automatically mention agents** disabled. 2. Use the composer mention picker to insert the first agent, type an instruction, then insert a second agent and type a different instruction. 3. Confirm the draft reads like `@Agent A review this, @Agent B test that` and neither agent appears in the persistent addressed-agent controls. 4. Send the message and confirm both agents are recipients while neither remains persistently addressed for the next draft. ### Inline mentions with persistence 1. Enable **Automatically mention agents**, then mention an agent inline and send successfully. 2. Confirm that agent becomes persistently addressed for later messages. 3. In a new draft, select the same agent from the mention picker again. 4. Confirm a new inline `@Agent` is inserted at the cursor while the agent remains persistently addressed. 5. Confirm the dedicated automatic-mention control and primary+Shift+Enter shortcut can still add or remove persistent addressing directly. ## Demo - **Before:** Persistent addressing could consume an ordinary picker selection, preventing users from placing that agent inline in the message. - **After:** Ordinary selection always produces an inline `@Agent`; persistence is a separate optional behavior that can coexist with inline mentions. ### Related issue N/A — reported through the Buzz feature room. ### Testing At `a4579c0a666f3644abe44271614186d7b9356bf5`: - Post-push hooks passed desktop check, desktop typecheck, and the full desktop unit suite. - The two persistence-focused Playwright smoke scenarios passed after explicitly opting into **Automatically mention agents**. - The one-time multi-agent targeting and persistent-address shortcut Playwright scenarios passed on the feature changes before the final rebase; CI is validating the rebased branch. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> |
||
|
|
f249710331 |
Qualify canonical relay images for staged delivery (#6781)
## Why Phase 1 of progressive delivery needs an explicit eligibility boundary before deployment automation can safely consume relay artifacts. Today the canonical Docker workflow can publish independently of the same-SHA CI result, the relay does not expose its source/build identity, and the Helm chart only renders mutable tags. The existing `ghcr.io/block/buzz-staging-dev` workflow in #6709 remains a manual pre-merge preview lane. It is deliberately not a canonical promotion input. ## What changed - Compile the full source SHA and GitHub Actions build identity into canonical relay binaries and expose it at `/_status`. - Gate creation of tagged multi-architecture manifests on the latest exact-SHA `CI` push run succeeding on `main` or `release`. - Preserve the signed SLSA provenance and add a signed, release-only deployment-eligibility predicate with source, build run, qualifying CI run, and exact compatible Buzz chart version. - Keep failed or pending builds untagged and without an eligibility predicate; preview-package artifacts are excluded by package and signer contract. - Add optional `image.digest` support to Buzz chart `0.1.8`, retaining the existing tag fallback. - Document verification and the intentional boundary between image/chart compatibility and backwards-compatible database migrations. ## Risk and rollout Canonical tag publication now waits for same-SHA CI and fails closed when the newest matching run fails. Per-architecture blobs may remain untagged after a failed qualification; they are not promotion inputs. Debug manifests are also CI-gated but do not receive the release eligibility predicate. There are no migration or schema-orchestration changes. The companion BuilderBot infrastructure PR will pin a separately verified canonical main digest and expose a read-only running-deployment inspection command. Signed eligibility is available only for artifacts published after this workflow lands. ## Verification - Blox: workflow contract fixtures, including stale-green rejection and predicate structure - Blox: `actionlint` for `ci.yml` and `docker.yml` - Blox: 46 Helm unit tests, chart lint, and production/quickstart render fixtures - Blox: relay status unit test, workspace Rust formatting, and workspace all-target clippy - Live evidence: exact-SHA CI selector chose successful run `32857345153` for source `9d1e4b257657f382d3111ce748f3da8d063b7671` - Existing canonical digest provenance verified with `gh attestation verify` ## References - #6709 — developer-preview precursor; intentionally separate from canonical eligibility - `docs/deployment-identity.md` *Generated with Codex* --------- Signed-off-by: Luke Tornquist <tornquist@squareup.com>chart-v0.1.8 |
||
|
|
7ba1197aa6 |
feat(desktop): persist agent addressing across composer messages (#6714)
**Category:** improvement **User Impact:** Agent addressing now stays visible, editable, and consistent across messages, so users can keep talking to the same agents without rebuilding the audience for every send. **Problem:** Agent addressing was transient and easy to lose track of. Inline mention text, persistent audience state, restored drafts, and sent-message rendering could drift apart, making it unclear who would receive the next message. **Solution:** The composer now treats addressed agents as a persistent audience represented by ordinary editable mention chips and a synchronized compact avatar tray. Eligible agent mentions can become persistent immediately, addressing survives sends without flashing empty, and removing or explicitly unpinning an agent updates that state without disturbing unrelated draft text. `Cmd+Shift+M` / `Ctrl+Shift+M` provides a fast way to toggle the current recipient or select the best eligible agent. - Promotes eligible inline agent mentions into persistent addressing when the preference is enabled; human mentions remain unchanged. - Keeps inline chips, restored drafts, persistent recipient state, and the avatar tray synchronized. - Clears an agent's persistent address when its final chip is removed, while explicit unpinning preserves unrelated draft text. - Restores persistent mentions optimistically during send and restores the full draft on failure, avoiding an empty-composer flash. - Deduplicates inline mentions and tag-backed address prefixes in sent messages. - Toggles the current addressed agent with `Cmd+Shift+M` / `Ctrl+Shift+M`, or selects the best eligible agent when none is addressed. - Ranks shortcut selection by recent explicit stream mentions, then active/member/runnable/stable fallbacks; DM participant fan-out does not affect recency. - Adds focused avatar feedback for entry/exit, send bounce, and failure shake, with reduced-motion and narrow-layout support. - Applies the same behavior to main and thread composers while preserving draft restoration, channel switching, and send-race correctness. ### Screenshots #### Light theme The compact tray keeps persistent recipients visible alongside editable mention chips.  #### Dark theme Persistent addressing remains clear and legible in the dark composer treatment.  ### Reproduction steps 1. Open a channel with an eligible agent and enable persistent agent addressing. 2. Insert an agent mention and confirm it appears as a normal editable chip while the compact avatar tray reflects the same persistent recipient. 3. Send several messages and confirm the agent remains addressed, the mention restores without an empty-composer flash, and sent messages do not duplicate the addressed agent. 4. Remove the agent's final mention chip and confirm its persistent address clears. Add unrelated draft text, explicitly unpin the agent, and confirm that text remains intact. 5. Press `Cmd+Shift+M` on macOS or `Ctrl+Shift+M` on Windows/Linux. Confirm it toggles the current addressed agent or, when none is selected, chooses the most relevant eligible agent. 6. Verify recent explicit channel/thread mentions influence shortcut selection, while DM participant fan-out does not. 7. Force a failed send and confirm the addressed avatars shake and the full draft is restored; confirm successful sends use the subtler bounce feedback. 8. Repeat in the thread composer, after switching channels, in dark theme, with reduced motion enabled, and at a narrow composer width. <details> <summary>File changes</summary> **desktop/src/features/channels/ui/ChannelPane.tsx** Feeds recent explicit agent mentions into the composer for contextual shortcut selection. **desktop/src/features/messages/lib/getVisibleAgentAddressPubkeys.ts** and tests Show tag-backed addressed agents only when they are not already represented by inline mention text. **desktop/src/features/messages/lib/mentionCandidates.ts**, **mentionRanking.ts**, **mentionSuggestionMapping.ts**, **recentMentionPubkeys.ts**, and tests Model agent eligibility and activity, derive explicit mention recency, and rank the best shortcut recipient. **desktop/src/features/messages/lib/useActiveAgentPubkeys.ts**, **useDefaultAgentSuggestion.ts**, and **useMentions.ts** Expose active-agent and recent-recipient context to mention consumers. **desktop/src/features/messages/ui/ComposerAddressControls.tsx** and tests Render the compact persistent-recipient tray with enter, exit, send, failure, layout, and reduced-motion states. **desktop/src/features/messages/ui/MessageComposer.tsx** and **MessageComposer.types.ts** Connect editable mention text, persistent addressing, shortcut selection, draft restoration, and send feedback. **desktop/src/features/messages/ui/MessageRow.tsx** Avoids duplicating addressed agents already rendered as inline mentions. **desktop/src/features/messages/ui/MessageThreadPanel.tsx** Brings the same recency and persistent-addressing behavior to thread composers. **desktop/src/features/messages/ui/useAgentAddressLockPicker.ts**, **useAutoPinMentionedAgents.ts**, **useAlwaysAddressShortcut.ts**, **useMentionSendFlow.ts**, and tests Synchronize mention chips with persistent audience state, promote eligible mentions, handle shortcut toggling/default selection, and preserve recipients through send success or failure. **desktop/src/features/messages/ui/useComposerPasteHandler.ts** and **useMentionSendFlow.types.ts** Keep the composer implementation focused by extracting paste and send-flow boundaries. **desktop/src/shared/lib/keyboard-shortcuts.ts** Documents `Cmd+Shift+M` / `Ctrl+Shift+M` as the agent-address shortcut. **desktop/tests/e2e/persistent-agent-audience.spec.ts** Exercises persistence, selection, removal, sends, failures, restored drafts, themes, reduced motion, and narrow layouts. </details> --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> |
||
|
|
a526dca9bc |
feat: navigate images across message threads (#6705)
## Summary - navigate across image occurrences that are currently rendered inside the nearest explicit open-thread DOM scope - preserve rendered DOM order, duplicate occurrence identity, hidden-spoiler exclusion, preview-image entry parity, and the exact thumbnail return target - keep adjacent timeline messages and unrelated surfaces as separate galleries - make Copy and Download follow the current gallery item, including when a link-preview image opens the gallery first and navigation reaches Markdown media ## Scope contract This is intentionally a **rendered-media gallery only**. Gallery membership comes from mounted image triggers under the nearest explicit thread scope. Collapsed or unloaded descendants are excluded until the thread UI renders them. There is no canonical thread fetch, descendant projection, depth override, or synchronous message reparse in this change. `ChannelPane.tsx` remains unchanged from `main`; `useSearchHighlightProps` and both timeline/thread prop spreads are preserved. ## Verification - rebased onto `origin/main` at `113a33b7e49b7173ee1767c49ef2f49c63803034` - `git diff --check` - differential file-size check - full Desktop check (Biome plus text/pubkey policy checks; only pre-existing informational diagnostics) - Desktop TypeScript typecheck - Desktop production build and rebuilt E2E build - full Desktop JS units: `5,476/5,476` passed - full Desktop Tauri workspace tests passed (one documented native performance test ignored) - rebuilt image gallery Playwright spec: `14/14` passed - search-highlight smoke cases: `5/5` passed - reduced diff: 6 files, `+417/-41`; production `+78/-41`, E2E `+339/-0` The four added E2E cases cover cross-message thread navigation and return targeting, rendered-only exclusion of collapsed descendants, preview-first current-item actions, and adjacent-message isolation. --------- Signed-off-by: Kalvin Chau <kalvin@block.xyz> Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> |
||
|
|
113a33b7e4 |
Add database pressure observability (#6700)
## Why Database pressure currently collapses several distinct delays into one symptom. This adds the evidence layer needed to distinguish pool acquisition wait, logical database operation time, advisory-lock wait, and selected transaction duration before changing timeout or retry policy. This is the phase 2 Lane A observability bundle for [#26](https://github.com/TheSentinel454/buzz/issues/26), [#28](https://github.com/TheSentinel454/buzz/issues/28), and [#33](https://github.com/TheSentinel454/buzz/issues/33). It is stacked on #6668. ## What - Record explicit reader/writer checkout wait and acquisition outcomes with `buzz_db_pool_acquire_wait_seconds` and `buzz_db_pool_acquisitions_total`. - Extend the compile-time `#[datastore_span(name = "...")]` seam with `buzz_db_operation_duration_seconds`, so operation labels remain static source literals instead of request data. - Route correctness-critical replacement, membership, push-gate, deletion, and migration/schema-safety advisory locks through one observer without changing their SQL, order, scope, or blocking behavior. - Measure six internally owned transaction lifetimes with `buzz_db_transaction_duration_seconds`, starting after `BEGIN` succeeds and ending after explicit commit/rollback or scope exit. - Emit root slow-operation warnings at 500 ms, logging the first slow completion and then 1/100 per call site with only `operation`, `outcome`, and `elapsed_ms`. - Document names, units, fixed label vocabularies, measurement boundaries, and blind spots in this PR description. Fixed labels are deliberately small: - `pool_role`: `writer`, `reader` - `lock_type`: `replacement`, `membership`, `push_gate`, `deletion`, `migration_schema_safety` - `outcome`: `success`, `error`, `timeout` where SQLx/PostgreSQL can distinguish it accurately - `operation`: compile-time datastore names plus the six closed transaction operation names documented in the runbook No metric or slow warning contains community IDs, event IDs, event kinds, coordinates, d-tags, SQL/query text, query IDs, returned errors, or event content. ## Coverage boundaries - Operation duration is the complete annotated logical function body, not pure SQL execution; it may include implicit checkout, lock wait, nested operations, and application work. Cancelled futures do not reach its completion hook. - Pool timing covers explicit helper checkouts, including proved-reader routing and selected writer-owned transactions. Implicit SQLx checkout through `&PgPool` remains folded into operation duration. - Lock timing covers application-side blocking locks in the five named families. Trigger/stored-procedure locks, channel-TTL locking, the usage try-lock, and the audit service session lock remain outside this slice. - Transaction timing covers only the six wholly owned boundaries documented in the runbook. It excludes pool wait, `BEGIN`, asynchronous rollback cleanup after an early return, and caller-owned `Db::begin_transaction` lifetime. ## Relationship to #6229 #6229 is the incident-driven timeout precursor. This PR does not add or change `statement_timeout`, `lock_timeout`, `idle_in_transaction_session_timeout`, retries, audit durability, or client-visible conflicts. It provides the missing distributions needed to evaluate those policies later and intentionally leaves #6229's open audit retry/durability finding untouched. The branches overlap in `crates/buzz-db/src/lib.rs` and `crates/buzz-db/src/migration.rs`, so a later rebase may need textual conflict resolution, but the behavior is complementary rather than duplicated. ## Risk assessment Moderate-low. The primary risk is instrumentation overhead and added static series. Cardinality is source-bounded, slow logs are sampled/redacted root events, and the lock/transaction changes wrap existing awaits without changing policy or ordering. ## Verification Author workstation: `buzz-tornquist-db-pressure-observability` (`2010927`), exact head `d7cf833e26c528adfcde3917ded80daf6f4ddac9`, parent `6f50e6b2b2a996349149af61d35bdd6a355f77fd`. - `cargo fmt --all --check` — passed - `cargo clippy -p buzz-datastore-tracing -p buzz-db -p buzz-audit -p buzz-search -p buzz-relay --all-targets -- -D warnings` — passed - `cargo test -p buzz-datastore-tracing --quiet` — 4 passed - `cargo test -p buzz-db --quiet` — 109 passed, 200 ignored - `cargo test -p buzz-audit -p buzz-search --quiet` — 16 passed, 25 ignored - `cargo test -p buzz-relay --lib --quiet -- --test-threads=1` — 906 passed, 48 ignored - Native PostgreSQL focused tests for pool success/timeout/error, lock success/contention/timeout/error, replacement, membership serialization, push ordering, deletion fencing, migration/schema exclusion, and reader fallback — 8 passed The default-parallel relay run passed once; subsequent runs exposed the existing load-sensitive `api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo` 504 at the end of the suite. That test passes in isolation and the full relay suite passes serially. Independent exact-head review workstation: `buzz-tornquist-db-pressure-observability-review` (`2013067`). Formatting, the same all-target clippy command, datastore instrumentation tests, DB unit tests, source privacy guards, and diff/non-goal audits passed; no review findings. Generated with Codex --------- Signed-off-by: tornquist <tornquist@squareup.com> |
||
|
|
12f3fea26e |
revert fixed mention highlight (#6716)
**Category:** fix **User Impact:** Mention chips now follow the active theme without a fixed yellow fill and remain readable across every supported theme. **Problem:** PR #6696 added an opaque yellow treatment to every human and agent mention. Removing that fixed color exposed another issue: some theme accent colors do not provide WCAG AA contrast when also used as normal-size mention text. **Solution:** Remove the fixed yellow treatment and keep the active theme accent as a subtle translucent chip background. Mention labels use a light/dark semantic foreground selected for readable contrast, including hover states. <details> <summary>File changes</summary> **desktop/src/shared/styles/globals/markdown.css** Removes the fixed-yellow treatment, retains theme-derived accent backgrounds, and applies an AA-safe semantic foreground to human and agent mention labels in resting and hover states. **desktop/src/shared/styles/globals/theme.css** Removes the obsolete fixed-yellow tokens and defines light/dark mention foreground values. **desktop/tests/e2e/mentions.spec.ts** Adds rendered contrast coverage for resting and hover states across every supported syntax theme, compositing translucent chip and ancestor surfaces before checking the 4.5:1 threshold. </details> ## Reproduction steps 1. Open a message containing a human or agent mention. 2. Confirm its subtle background follows the active theme accent rather than using yellow. 3. Hover the mention and confirm the accent background strengthens while the label remains readable. 4. Switch between default, light, dark, and custom syntax themes and confirm mention labels remain legible. This is a focused visual rollback of the mention treatment introduced by #6696. The channel notification badge changes from that PR remain intact. --------- Signed-off-by: tulsi <tulsi@block.xyz> |
||
|
|
29f2054c69 |
highlight search terms in results and messages (#6702)
**Category:** fix **User Impact:** Search terms are now highlighted in yellow in Cmd+F results and in the message opened from a result. **Problem:** Search returned relevant messages, but users still had to reread each preview and destination message to discover where the query appeared. Search navigation could also lose or misapply highlighting during rapid typing, repeated navigation, thread opening, and forum navigation. **Solution:** Render match-focused previews with a shared yellow treatment, carry the result query through navigation, and apply it only to the clicked destination. The implementation binds highlights to the debounced result set, supports token and prefix matching, and handles channels, threads, forums, diffs, and wave messages consistently. <details> <summary>File changes</summary> **desktop/src/app/AppShell.tsx** Carries the active result query into search-hit navigation so the destination can preserve the user's context. **desktop/src/app/navigation/searchHitEventCache.ts** Adds a bounded, one-shot cache for result queries and navigation IDs alongside cached search-hit events. **desktop/src/app/navigation/searchHitNavigation.test.mjs** Covers query retention, one-shot consumption, repeated same-route activations, forum posts, forced routing, and cancellation. **desktop/src/app/navigation/searchHitNavigation.ts** Associates each search-result activation with a unique navigation ID and forwards it to channel or forum destinations. **desktop/src/app/navigation/useAppNavigation.ts** Extends channel and forum navigation to carry search navigation state without changing normal navigation behavior. **desktop/src/app/routes/ChannelRouteScreen.tsx** Consumes search highlight state after navigation and retains it for the selected message while clearing it on community context changes. **desktop/src/app/routes/channels.$channelId.posts.$postId.tsx** Validates and forwards forum search-navigation IDs. **desktop/src/app/routes/channels.$channelId.tsx** Validates and forwards channel search-navigation IDs. **desktop/src/features/channels/ui/ChannelPane.tsx** Passes the selected result ID and query into the main timeline and open thread panel. **desktop/src/features/channels/ui/ChannelPane.types.ts** Defines the destination-highlight contract for channel panes. **desktop/src/features/channels/ui/ChannelScreen.tsx** Routes destination highlighting to either forum content or the channel timeline. **desktop/src/features/channels/ui/ChannelScreen.types.ts** Defines route-level search-highlight inputs. **desktop/src/features/channels/ui/ForumChannelContent.tsx** Forwards search context into expanded forum threads. **desktop/src/features/forum/ui/ForumThreadPanel.tsx** Highlights the matching text in the selected forum post or reply. **desktop/src/features/forum/ui/ForumView.tsx** Carries selected-result context from forum routing into the thread panel. **desktop/src/features/messages/ui/DiffMessage.tsx** Forwards the query into diff descriptions and rendered diff content. **desktop/src/features/messages/ui/DiffViewer.tsx** Provides a highlighted match excerpt for structured diffs and highlights fallback raw diff text. **desktop/src/features/messages/ui/MessageRow.tsx** Passes destination queries into normal Markdown, diff, and wave message renderers. **desktop/src/features/messages/ui/MessageThreadPanel.tsx** Applies highlighting to the selected thread head or reply without tinting unrelated messages. **desktop/src/features/messages/ui/WaveMessageAttachment.tsx** Highlights matches in wave-message fallback text. **desktop/src/features/search/lib/searchMatch.test.mjs** Covers case-insensitive, literal, multi-term, prefix, one-character, and late-preview matches. **desktop/src/features/search/lib/searchMatch.ts** Centralizes token extraction, case-insensitive match splitting, and match-focused preview generation. **desktop/src/features/search/ui/HighlightedSearchText.tsx** Adds the reusable accessible mark renderer used by result and specialized message surfaces. **desktop/src/features/search/ui/TopbarSearch.tsx** Highlights result previews, centers excerpts around matches, strips search operators, and hides stale results during debounce transitions. **desktop/src/features/sidebar/ui/AppSidebar.types.ts** Updates the sidebar search callback contract to include the result query. **desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx** Passes query-aware result selection through the pinned search entry point. **desktop/src/shared/lib/rehypeSearchHighlight.ts** Uses the shared matcher to mark every matching Markdown text segment while leaving code blocks untouched. **desktop/src/shared/lib/searchHighlightStyle.ts** Defines one yellow highlight treatment for light and dark themes. **desktop/src/shared/ui/markdown/nodeCache.test.mjs** Verifies all Markdown matches, one-character scoped searches, code exclusion, and transient cache behavior. **desktop/src/shared/ui/markdown/nodeCache.ts** Enables transient highlighting for one-character scoped searches without polluting the Markdown cache. **desktop/tests/e2e/smoke.spec.ts** Exercises result and destination highlighting, same-route forum activation, and stale-query suppression end to end. </details> ## Reproduction steps 1. Open a channel containing a longer message and press Cmd+F. 2. Search for a word that appears later in the message, such as `mentions`. 3. Confirm each matching result shows the word in yellow and keeps the match visible in its preview. 4. Open a result and confirm the same term is highlighted in yellow in the destination message. 5. Repeat with a thread reply or forum post, and quickly change the query to confirm stale results are not selectable. ## Demo - **Before:** Search results returned the correct message but offered no visual indication of where the query matched; opening the message also showed no match treatment. - **After:** Every matching term is marked in yellow in the result preview and in the exact channel, thread, or forum message opened from that result. --------- Signed-off-by: tulsi <tulsi@block.xyz> |
||
|
|
9b6a637d01 |
fix(desktop): make lightbox zoom controls interactive (#6710)
## Summary - Replace the decorative lightbox zoom icons with accessible Zoom out and Zoom in buttons. - Route button clicks through the existing 1x–3x, 5% stepped zoom state without dismissing the viewer, including clicks on SVG icon descendants. - Extract the zoom toolbar so `markdown.tsx` remains below the repository file-size threshold. ### Testing - `pnpm build:e2e` - `pnpm exec playwright test image-attachment-gallery.spec.ts --project=smoke` — 10 passed - `pnpm typecheck` - Full desktop unit suite — 5,397 passed - Pre-push checks — file-size gate, desktop checks, typecheck, and desktop tests passed - `git diff --check` ### UI evidence The rendered lightbox flow is covered by the gallery smoke test: Zoom out is disabled at 100%, Zoom in changes the value to 105%, Zoom out returns it to 100%, and the dialog remains open throughout. Existing gallery, keyboard, spoiler, context-menu, and close-path assertions also pass. --------- Signed-off-by: sanic <c57bf9b4275088b2b33db7f746975407210f159fbd8bd733c0e532375f69aa80@buzz.block.builderlab.xyz> Signed-off-by: Kalvin Chau <kalvin@block.xyz> Co-authored-by: sanic <c57bf9b4275088b2b33db7f746975407210f159fbd8bd733c0e532375f69aa80@buzz.block.builderlab.xyz> Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz> |
||
|
|
d12dea4e67 | Support community deletion in versioned media buckets (#6738) | ||
|
|
bb5b9357a7 |
Fix TipTap editor mount race (#6779)
## Summary - wait for TipTap's mount lifecycle before reading the selection formatting tray's editor DOM - detach and reattach DOM listeners across editor view unmounts and remounts - cover mounted and pre-mount editor view access ## Testing - `just ci` - `pnpm build:e2e` - `pnpm exec playwright test tests/e2e/composer-selection-formatting.spec.ts --project=smoke --grep "right-clicking selected composer text"` - Native Builderlab staging: reproduced the pre-mount crash on main, relaunched with the fix, and verified the composer mounts without the TipTap error Signed-off-by: kenny lopez <klopez4212@gmail.com> |