Commit Graph

2151 Commits

Author SHA1 Message Date
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
db07f35525 chore(desktop): satisfy composer file size ratchet
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:14 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
7c705123ea test(composer): assert asynchronous preview paste
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:14 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
975323e2e8 fix(composer): preserve completed preview sends
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:13 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
8c2e0b6c92 fix(composer): place preview suppression outside card
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:13 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
b602548a96 feat(composer): add draft-wide preview suppression
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:13 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
9b1f09b36e test(desktop): assert preview media container
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:12 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
9578e9c0ad test(desktop): cover authored preview snapshots
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:12 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
0871f36398 feat(desktop): send authored link preview snapshots
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:12 -07:00
Taylor Ho
f192cf3b56 fix(desktop): scope preview retry constant to tests
- Import the retry-after bound only in the link preview test module
- Keep production Tauri builds free of the unused-import warning enforced by pre-push clippy

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:11 -07:00
Taylor Ho
3a9dbe2c83 fix(desktop): stabilize link preview thumbnails
- Classify native preview image outcomes and honor bounded Retry-After cooldowns for rate-limited hosts
- Extract host cooldown bookkeeping to keep the preview command within the desktop file-size guard
- Fetch preview images and favicons independently so secondary failures preserve resolved metadata
- Add bounded frontend concurrency, fragment deduplication, expiring negative cache entries, and cooldown-aware retries
- Render deterministic Compact and Rich fallbacks for transient fetch and browser decode failures
- Expand Rust, metadata-loader, mock bridge, and Playwright coverage for throttling and recovery

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:11 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
010b533f9c test(desktop): expect square compact link previews
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:10 -07:00
Taylor Ho
e204c9e16e feat(desktop): unify compact preview hover treatment
- Underline compact preview hostnames and titles together whenever the card is hovered.
- Remove independent hostname hover styling so the interaction consistently represents the full destination URL.
- Restore stronger description colors for standard rich previews and tweet content while keeping hostnames secondary.
- Add E2E coverage confirming both compact identity elements receive the shared hover affordance.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:10 -07:00
Taylor Ho
d6699cc4dc refactor(desktop): unify link preview identity styling
- Remove legacy provider-specific compact preview logos and layouts in favor of hostname-driven metadata.
- Present imageless compact previews with the same 84px height, description support, and left-rule treatment as image-backed cards.
- Preserve fetched favicons and make favicon/hostname identity rows link directly to their source URL.
- Align rich and compact hostname and description colors on the same secondary foreground treatment.
- Expand messaging E2E coverage for equal-height cards, generic favicons, linked hostnames, and imageless descriptions.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:10 -07:00
Taylor Ho
62996409fe feat(desktop): refine compact preview content hierarchy
- Expand compact preview thumbnails to an 88px height with slightly wider responsive dimensions.
- Reduce the thumbnail-to-content inset so preview text sits closer to its image.
- Allow compact titles to wrap across two lines for better readability.
- De-emphasize hostnames and descriptions with a softer secondary foreground color.
- Update desktop geometry coverage for the revised dimensions, spacing, and title treatment.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:09 -07:00
Taylor Ho
fc454603e8 feat(desktop): refine compact link preview cards
- Remove compact card borders and background surfaces while preserving transparent hover behavior.
- Round thumbnail images on every corner and expand cards to the 24rem image-upload width.
- Shorten the preview settings submenu label from Link display to Display.
- Update compact preview geometry coverage for the wider borderless layout and await screenshot animations.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:09 -07:00
Taylor Ho
2cb0d3ddc9 feat(desktop): add inline link preview display controls
- Add a hover-only vertical ellipsis beside the first compact or rich preview.
- Provide a Link display submenu for switching the global Rich or Compact appearance preference.
- Show an actionable confirmation-backed Remove preview option for message authors while keeping display controls available to everyone.
- Link preference-change toasts to Appearance settings and expand rich preview images to match uploaded image width.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:08 -07:00
Taylor Ho
699ee829f9 feat(desktop): integrate rich previews with the markdown lightbox
- Route rich link preview images through the markdown gallery lightbox so preview images support consistent zoom transitions and gallery navigation.
- Extract link preview lightbox behavior and focusable-element discovery into focused markdown modules to keep the main renderer within its size limit.
- Infer image dimensions from loaded thumbnails when explicit lightbox metadata is unavailable.
- Pass the shared lightbox component through the link preview list and attachment layers.
- Render compact preview cards inline with wrapping and place the remove action on the final compact card.
- Preserve rich preview stacking and existing remove behavior for rich layouts.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:08 -07:00
Taylor Ho
2f431c23e4 feat(desktop): preserve rich preview description structure
- Render OG description line breaks instead of collapsing them into spaces
- Group blank-line-separated paragraphs with compact spacing to avoid oversized gaps
- Reuse the rich preview image component for consistent skeleton and lightbox behavior across link types

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:08 -07:00
Taylor Ho
c8da74e07b feat(desktop): refine rich link preview presentation
- Update rich preview cards with wider layouts, compact image sizing, and unified content expansion controls
- Allow link titles to span two lines and preserve full expanded descriptions
- Raise fetched description metadata to the standard 280-character X post limit
- Preserve encoded description line breaks while normalizing intra-line whitespace
- Add metadata tests covering line breaks and description length limits

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:07 -07:00
Taylor Ho
df14ba2c3a feat(desktop): simplify rich preview expansion
- Replace separate description and image controls with one Show more/Show less toggle that collapses both elements.
- Expand rich preview text to the full available metadata and widen preview containers for readable 280-character tweets.
- Add vertical padding beside the accent border and cap preview images at 300px while preserving responsive sizing.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:07 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
f5e8728217 test(desktop): await compact preview geometry
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:06 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
222c187377 test(desktop): isolate link preview style coverage
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:06 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
a9d6931abc feat(desktop): add rich link preview preference
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:06 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
9e1343ff9c feat(desktop): show favicon beside preview hostname
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:05 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
348acd3476 fix(desktop): restore approved compact preview layout
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:05 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
9a70e4f143 feat(desktop): restore compact link preview cards
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:04 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
8d9530ca6a fix(desktop): simplify preview removal copy
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:04 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
d6e284b28f refactor(desktop): group edit message input
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:04 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
82c722b659 refactor(desktop): extract link preview helpers
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:03 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
634d9cf169 feat(previews): unfurl safe link images
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:03 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
14356443e0 fix(desktop): label image previews by domain
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:02 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
c1eba41556 feat(desktop): enrich link preview image cards
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:02 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
c7f3d6e19c fix(desktop): remove link preview image padding
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:02 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
04da27f2ba fix(desktop): render compact link preview images
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:01 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
1a3a809cde fix(previews): parse bounded html prefixes
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:01 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
30574760c8 refactor(desktop): use shared button for preview removal
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:00 -07:00
Taylor Ho
c2b2145c46 refactor(previews): remove preview-image caption and type label from cards
- Remove the "Automatic preview from {domain}" gradient overlay on preview
  images; simplify the img alt text to "Preview from {domain}"
- Drop the "· {typeLabel}" suffix (PR/issue/repo/file/link/etc.) from the
  card metadata row — the provider name and logo already convey the source,
  so the row now shows just the provider
- typeLabel stays in the SupportedLinkPreview model since it still powers
  the accessible aria-label ("Open GitHub PR: ...")

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:00 -07:00
Taylor Ho
68f75f3610 feat(previews): replace remove-previews text link with inline X on preview card
- link-preview-attachment.tsx: add optional onRemove prop that renders a
  small X button floating just outside the card's top-right corner
  (left-full top-0, z-20 above the link trigger), fading in on message
  row hover via group-hover/message and reachable by keyboard via
  focus-visible
- link-preview-attachment.tsx: wrap the card in a relative container
  carrying the width classes (w-80 max-w-full shrink-0) since the card's
  overflow-hidden would clip a button positioned outside its bounds
- link-preview-attachment.tsx: remove the AttachmentActions block with
  the hover-revealed ExternalLink icon, eliminating the reserved flex
  gap on card hover
- markdown.tsx: drop the "Remove previews for everyone" text button
  below the preview list and its spacing wrapper; pass onRemove to each
  LinkPreviewAttachment instead, opening the same confirmation dialog
- removal flow and AlertDialog confirmation are unchanged; X only
  renders when onRemoveLinkPreviewsForEveryone is available (own
  messages)

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:37:00 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
927946431c fix(messages): make link preview removal global only
Co-authored-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
Signed-off-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
2026-08-04 11:36:59 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
c0f591b1bb feat(messages): add link preview controls
Co-authored-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
Signed-off-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
2026-08-04 11:36:59 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
482daa3b0f feat(previews): add safe automatic preview images
Co-authored-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
Signed-off-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
2026-08-04 11:36:59 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
5258f65004 test(desktop): target exact project guide link
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-04 11:36:58 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
450e782902 fix(desktop): use globe for generic link previews
Co-authored-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
Signed-off-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
2026-08-04 11:36:58 -07:00
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
72a87bfecd feat(desktop): enrich link previews with page metadata
Co-authored-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
Signed-off-by: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f <ab176f059d100602ea25073d9a69ee9817f7b691c76a897180d48498d959faa2@buzz.block.builderlab.xyz>
2026-08-04 11:36:57 -07:00
Wes
e5efd04705 fix(desktop): close reconnect gaps that previously required CMD+R (#4737)
> Opened by Brain (agent) on behalf of @wesbillman.

## Problem

Users report the desktop app doesn't reliably reconnect and can wedge in
states where only CMD+R (or a full restart) restores connectivity
(thread `c2205e2b` in #desktop-reconnecting).

Pinky's empirical light-switch matrix (real `buzz-relay`, SIGTERM/1012 +
SIGKILL × 1s/45s/3min, at `f18a9cb10`) passed 4/4 — the backoff state
machine recovers cleanly from ordinary relay loss. That isolates the
user-stuck states to four special cases a reload resets but the auto
flow never did.

## Fixes

| Gap | Change |
|---|---|
| **G1** — recovery rode solely on the backoff timer (max 30s),
throttled by WKWebView in occluded/background windows; nothing fired on
network return or wake | New `useRelayResumeTriggers`: `online`, window
focus, and visibility→visible call `preconnect()` when the session is
`reconnecting`/`stalled`, rate-limited to one attempt per 5s
(`relayResumeTriggerPolicy.ts`). Deliberately inert for the terminal
`disconnected` state. |
| **G2** — any AUTH `OK false` latched the session terminal forever,
though the relay also rejects for transient causes (duplicate-AUTH
"already authenticated" race, ±60s clock skew, fail-closed allowlist DB
errors) | New `AuthOkTracker` (`relayAuthPolicy.ts`): "already
authenticated" resolves as success; transient rejections retry with
normal backoff; latch only on `restricted:` or after 3 consecutive
rejections. |
| **G3** — an `auth-required:` CLOSED (REQ racing AUTH after reconnect)
permanently deleted the live subscription with no UI signal — frozen
channel while state reads "connected" | Reclassified `auth-required:` as
retryable in `relayClosedPolicy.ts`. Genuinely terminal classes
(`restricted:`, `invalid:`, …) still delete. Can't loop: a truly
unauthenticated session latches terminal at the connection level. |
| **G4** — `useRelayAutoHeal` observed the 2s-debounced connection hook,
so sub-2s flaps never triggered the heal even though `resetConnection`
had already rejected every in-flight query | Auto-heal now observes the
raw connection-state emitter. The existing 15s heal rate-limit still
guards against flap storms. |

Each fix is a colocated pure-policy module + unit tests, matching the
existing `relayReconnectPolicy`/`relayClosedPolicy` pattern.

## Validation

- Full desktop unit suite: **4151 pass, 0 fail** (at branch tip, `pnpm
-C desktop test`)
- `pnpm -C desktop typecheck` and `pnpm -C desktop check` clean
(file-size ratchet respected — `relayClientSession.ts` net −2 lines
despite the tracker wiring)
- Evidence trail: `RESEARCH/DESKTOP_RECONNECT_CMDR_GAP_AUDIT.md`
(audit), `RESEARCH/DESKTOP_RECONNECT_LIGHT_SWITCH_RESULTS.md` (Pinky's
matrix)

## Not covered / follow-ups

- Native macOS sleep-wake was not automated (would kill the harness
session); G1's focus trigger is the mechanism that covers wake in
practice, but a manual sleep-wake verification on a real build is
worthwhile.
- G3 terminal-CLOSED classes (`restricted:` etc.) still silently delete
subs with no UI signal — surfacing that is a separate UX decision.
- Stall-watchdog latency (60s idle + 10s check) left unchanged; G1
triggers largely mask it.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
Co-authored-by: npub1yxv5wk0u0fh6dwt925wntn7h397jvteyj4r87ttcd9xae7n2t3lqqj9jmm <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz>
Co-authored-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
2026-08-04 18:33:42 +00:00
Wes
cb4a73e17d Dock Buzz Term within channel workspace (#4724)
## Summary

- replace Buzz Term's full-app takeover with a resizable bottom dock
inside the channel content surface
- add a discoverable channel-header button plus hide and
maximize/restore controls
- create PTYs lazily and keep separate, persistent terminal workspaces
per channel
- capture immutable channel/thread context on every terminal session

## Multiple-channel behavior

The dock is a single surface, but its tabs are partitioned by channel.
Switching channels swaps to that channel's sessions without terminating
background PTYs; returning restores them. New tabs capture the currently
visible channel/thread context.

## Verification

At commit `7ca087f8e08c80528387684364a65bf4ccd6315f`:

- `pnpm --dir desktop typecheck`
- `pnpm --dir desktop test` — 4,129 passed
- pre-push repository hooks — desktop check/test, Tauri checks, terminal
Rust suites all passed

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Co-authored-by: kenny lopez <klopez4212@gmail.com>
2026-08-04 11:10:47 -07:00
Jemiah Westerman
bc9e6528a7 perf(relay): index channel-id lookups and skip trace-only reads (#4647)
## Problem

`SELECT id, community_id FROM channels WHERE id = ANY($1) AND deleted_at
IS NULL` is the top **Load by waits (AAS)** on the Buzz Postgres writer.
Two independent causes compound, and both are fixed here.

### 1. No index can serve it

`channels` is `PRIMARY KEY (community_id, id)`, and every secondary
index leads with `community_id`:

| Index | Columns |
|---|---|
| *(primary key)* | `(community_id, id)` |
| `idx_channels_nip29_group` | `(community_id, nip29_group_id)` |
| `idx_channels_dm_hash` | `(community_id, participant_hash)` |
| `idx_channels_community_type` | `(community_id, channel_type)` |
| `idx_channels_community_visibility` | `(community_id, visibility)` |
| `idx_channels_created_by` | `(community_id, created_by)` |
| `idx_channels_ttl_expiry` | `(ttl_deadline)` *(partial)* |

The two tenant-independent lookups carry **no `community_id` predicate**
— deliberately:

- `Db::communities_of_channels` — `WHERE id = ANY($1) AND deleted_at IS
NULL`
- `Db::community_of_channel` — `WHERE id = $1 AND deleted_at IS NULL`

That independence is load-bearing, not an oversight: projecting a row's
*true* owning community regardless of the fetch query's `WHERE` clause
is what makes `Inv_NonInterference` non-vacuous. If the fetch ever
dropped its tenant scoping, this lookup would still report the real
label and the checker would catch the mismatch.

But a composite btree is only usable when its leading column is
constrained, so neither query can use the primary key, and nothing else
leads with `id`. **Both sequentially scan `channels` on every call.**

### 2. In production the result is discarded

Both call sites feed `record_read_message_rows` /
`record_read_by_id_rows`, which call `tracer.record(...)`. Production
binds `NoopTracer` (`crates/buzz-relay/src/state.rs`), whose `record`
body is empty.

The existing guard tests `trace_state`, which is `Some` for every
well-formed request — it only goes `None` on malformed pubkey bytes. So
the scan ran on the hot read path and its output was dropped. This is
the classic eager-argument bug: `log.debug("..." + expensiveCall())`
with no `isDebugEnabled()` check.

### 3. Multiplied per filter

The non-search call site sits **inside the phase-3 per-filter loop**, so
a `REQ` carrying N filters performed N sequential scans of `channels`
before responding.

## Changes

**`Tracer::enabled()`** — a capability check on the trait (the
`isDebugEnabled()` of this seam), defaulting to `true`. `NoopTracer`
overrides it to `false`, and both emitters in `req.rs` now gate on it,
skipping the trace-only DB read entirely in production.

**`migrations/0027_channels_id_lookup_index.sql`**

```sql
CREATE INDEX IF NOT EXISTS idx_channels_id_live
    ON channels (id) INCLUDE (community_id)
    WHERE deleted_at IS NULL;
```

- `INCLUDE (community_id)` — both queries select exactly `(id,
community_id)`, so this is covering and can be served index-only.
- Partial on `deleted_at IS NULL` — matches both predicates exactly,
excludes soft-deleted history, and lets Postgres skip the recheck.
- **Not `UNIQUE`.** `id` alone is *not* unique in this table —
`command_executor.rs` documents that `community_of_channel(channel_id)`
is ambiguous because the same channel id can appear under more than one
community. A unique index would encode a false constraint and fail to
build on any database already holding such a pair.

Worth keeping the index even though fix #1 removes the production
caller: it still runs under conformance, and `community_of_channel` has
the same problem on its own paths.

**`schema/schema.sql`** — mirrored, since a test asserts desired-state
parity.

## Conformance is unchanged

This is the part worth reviewing closely. Under a real tracer
`enabled()` returns `true` and **every emit happens exactly as before**
— the gate only skips *building* emit inputs when nothing observes them,
never an emit that would otherwise have been made. The coverage-breach
guard stays non-vacuous.

`CountingTracer` forwards `enabled()` to its inner tracer rather than
inheriting the `true` default. Both directions matter and both fail
silently:

- inheriting `true` over a `NoopTracer` would keep the overhead this PR
removes;
- hardcoding `false` over a live tracer would suppress the emits whose
absence `EmitGuard` reports as `ImplBug` — masking real breaches behind
expected ones.

Covered by a new regression test,
`counting_tracer_delegates_enabled_to_inner`, which asserts delegation
in both directions.

## Verification

- `cargo check -p buzz-conformance -p buzz-relay` — clean
- `cargo clippy --all-targets` — clean, zero warnings
- `cargo test -p buzz-conformance` — 6/6
- `cargo test -p buzz-relay --lib conformance` — 11/11
- `cargo test -p buzz-db --lib migration` — 7/7
- `just test-unit` (pre-push) — green

Migration-count assertions in `crates/buzz-db/src/migration.rs` were
bumped 26 → 27, with content assertions for 0027 following the existing
per-migration pattern (including a guard that it never becomes
`UNIQUE`).

## Open questions for reviewers

1. **Lock strategy.** Built *without* `CONCURRENTLY`, following
migration 0004's precedent, because sqlx runs each migration inside a
transaction and `CREATE INDEX CONCURRENTLY` cannot run in one. This
takes a brief `SHARE` lock on `channels` (blocks writes, not reads) —
small relative to `events`, but an operator preferring zero
write-blocking can pre-build it by hand and `IF NOT EXISTS` makes the
migration a no-op. I could not confirm whether sqlx 0.9 supports a `--
no-transaction` directive; if it does, that may be preferable.

2. **Diagnosis is static.** This comes from reading the source, not from
`EXPLAIN` against the live database. Worth confirming with `EXPLAIN
(ANALYZE, BUFFERS)` on the writer before/after — that also sizes the win
by revealing the real table size and row counts.

3. **Expected impact** scales with average filters-per-`REQ`, which I
did not measure. `pg_stat_statements` ordered by `total_exec_time` would
confirm this query drops off the top and show whether anything else is
scanning the same way.

Signed-off-by: Jemiah Westerman <jemiah@squareup.com>
2026-08-04 13:59:54 -04:00
Will Pfleger
0c33a8a55f fix(agents): canonicalize stale persona harness pins (#4631)
Replace the stale `agent_command_override` drop logic in
`apply_persona_snapshot` with a three-tier canonical command resolver.

## What this fixes

The old code dropped a create-time harness pin when the persona switched
to a different runtime, but it had two failure modes:

1. **Preset harnesses invisible.** `known_acp_runtime_exact()` only
searches `KNOWN_ACP_RUNTIMES` (builtins). Preset harnesses such as
OpenClaw live in `PRESET_HARNESSES`, so the destination lookup returned
`None` and the outer `if let` branch never executed — a Goose→OpenClaw
persona switch left the stale Goose override in place, keeping the agent
running Goose instead of OpenClaw.

2. **Pin-side canonical resolution incomplete.** The pin was resolved by
`known_acp_runtime()`, which searches by id/command/alias and returns a
`&KnownAcpRuntime` entry correctly. However, if the *pin* named an alias
(e.g. `claude-code-acp`) and the *destination* was a preset harness
absent from builtins, the outer guard still failed for the same reason
as (1). The alias regression test pins the requirement that the
canonical resolver must handle both sides: alias pins must be recognised
and drops must fire when the destination is a known preset.

## How it works now

`canonical_harness_command(input)` accepts any form a stored override
can take — bare command, alias, path prefix, or runtime id — and
resolves it to the harness primary command through three tiers:

1. **Builtins** — `KNOWN_ACP_RUNTIMES`, matched by id/command/alias.
2. **Static presets** — `PRESET_HARNESSES`, matched by id or normalised
command.
3. **Loaded registry** — custom/preset definitions loaded at runtime.

`command_for_runtime_id` (id-only input, same three tiers) replaces the
two-step `known_acp_runtime_exact`/`lookup_loaded_harness_by_id` pattern
in `record_agent_command`, `effective_agent_command`, and
`try_record_agent_command`, adding the static preset tier so preset
harnesses resolve correctly even without a warm registry.

## Changed files

- `discovery/presets.rs` — `preset_command_for_id`,
`command_for_runtime_id`, `canonical_harness_command`
- `discovery.rs` — re-export new functions; make
`normalize_command_identity` `pub(crate)`; refactor three
command-resolution functions to use `command_for_runtime_id`
- `custom_harnesses.rs` — `loaded_harness_registry` visibility `fn` →
`pub(super)` (needed by `canonical_harness_command`)
- `persona_events.rs` — replace two-step
`known_acp_runtime_exact`/`known_acp_runtime` + pointer comparison with
canonical-command comparison
- `persona_events/stale_pin_tests.rs` (new) — four regression tests:
Goose→OpenClaw drop, OpenClaw→Goose drop, claude-code-acp alias→OpenClaw
drop, same-harness path keep
- `persona_events/tests.rs` — `sample_record`/`sample_persona` exposed
as `pub(super)` for the new test module

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
2026-08-04 13:52:35 -04:00
klopez4212
e1287c92cc Refine community invite links (#4734)
## Summary

- Separate direct invites from link sharing with a labeled divider.
- Show the generated invite URL inline with truncation and a copy
control.
- Use shared loading feedback and a restrained copy-status resize.

## Validation

- `pnpm -C desktop exec playwright test
tests/e2e/invite-link-copy.spec.ts
tests/e2e/invites-settings-screenshots.spec.ts` (7 passed)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-08-04 10:39:09 -07:00
Will Pfleger
0afeac8a7c feat(desktop): persist sidebar observed-unread across webview reload (#3976)
## Problem

`Command+R` (webview reload) wipes the two in-memory refs driving
sidebar channel unread badges: `observedUnreadEventsByChannelRef` and
`latestByChannelRef`. The boot catch-up REQ can only fetch events newer
than each channel's NIP-RS frontier, so thread replies that arrived
before the frontier was passively advanced (the common case) are never
re-discovered.

Inbox is unaffected because it rebuilds candidates from a relay feed
query and checks fine-grained `thread:`/`msg:` markers. The sidebar
badge path lacks an equivalent recovery mechanism.

## Solution

Persist the sidebar's per-event candidate set to localStorage as a
disposable, versioned projection cache
(`buzz-observed-unread.v1:<relay>:<pubkey>`) and hydrate it on boot
before the catch-up REQ runs.

### New files

**`observedUnreadStorage.ts`** — storage module for the cache:
- Keyed
`buzz-observed-unread.v1:<normalizedRelayUrl>:<normalizedPubkey>`
(relay-scoped to prevent cross-community leakage, matching
`threadActivityStorage`)
- Stores validated per-event `ObservedUnreadEvent` rows;
`latestByChannel` is derived at hydration — no divergent dual aggregate
- Age pruning (7d = `READ_STATE_HORIZON_SECONDS`), per-channel cap
(1000), global cap (5000) across all channels in a scope bucket
- Payload `updatedAt` for LRU ordering; registered in
`PURE_CACHE_KEY_PREFIXES` for 2 MiB eviction budget
- Field-level validation on decode; write failure is non-fatal
(session-only degradation)
- Snapshot-owning timers: `scheduleObservedUnreadWrite` deep-clones the
events map at schedule time — a late A-scope timer can never read B's
mutable refs or write under B's key

**`useObservedUnreadPersistence.ts`** — hook that owns all persistence
lifecycle:
- Scope fence: `normalized pubkey + normalized relay` identity;
`isScopeLoaded()` callback guards both projection (`rawUnread`) and
every **observed-cache mutation** (`recordUnreadEvent`, `removeChannel`,
`clearAll`) before touching refs or storage. Note: stale-scope calls to
`markChannelRead`/`markAllChannelsRead` can still affect
`forcedUnreadRef` and NIP-RS markers, which are pre-existing on `main`
and deferred to the NIP-RS arc (see Deferred below).
- Synchronous `pagehide` flush closes the Cmd+R timing gap
(`useReloadShortcut.ts` reloads within 500ms of teardown, before the
1-second debounce fires)
- Identity-reset effect: flushes old scope, resets refs, hydrates from
storage, stamps loaded scope — all atomic; cleanup flushes on unmount
- `clearAll` cancels the pending timer, resets both in-memory refs, and
clears storage in a single transactional operation; `removeChannel`
deletes the channel from both refs and replaces any pending snapshot
with the current full map — never cancel-without-replacement, preserving
sibling-channel events on reload
- Marker-prune effect on `readStateVersion`: evaluates each retained
event with `observedUnreadEventReadAt()` (the same evaluator used by the
projection memo) and removes covered events, rederiving per-channel
latest — never clears a whole channel for a single thread/msg marker
- Returns a stable `useMemo`-wrapped API object keyed on actual deps so
unrelated re-renders do not restart the catch-up REQ
- `isScopeLoaded` is a `useCallback` (not a memoized boolean) — always
reads the ref at call time, never stale

### Modified files

**`useUnreadChannels.ts`** — hook integration:
- Calls `useObservedUnreadPersistence` with all persistence wired
through the returned API
- `rawUnread`: `isScopeLoaded()` guard suppresses A-scope refs from
projecting under B
- `recordUnreadEvent`: `isScopeLoaded()` fence before touching refs;
schedules a debounced write on each successful record
- `markChannelRead` clearObserved path: calls `removeChannel` so the
cleared state survives reload
- `markAllChannelsRead`: delegates to the owner's fenced `clearAll` —
the parent does not reset the observed refs directly; `clearAll` owns
the transactional clear of both refs and storage, preventing a stale
scope-A callback from corrupting scope B

**`localStorageQuota.ts`** — registers `buzz-observed-unread.v1:` in
`PURE_CACHE_KEY_PREFIXES`

## Design constraints

The cache is a **disposable projection**: versioned key, read-through
only, safe to delete wholesale. It does not touch `ReadStateManager`,
marker semantics, or `forcedUnreadStore`. Zero overlap with the NIP-RS
manual mark-read/unread protocol work in progress in another channel;
migration path when that lands is "stop reading the key."

## Test coverage

**`observedUnreadStorage.test.mjs`** covers storage primitives:
- Key normalization, relay-scoped isolation, round-trip correctness
- Age-prune and per-channel cap on read and write; global cap across
channels
- `deriveLatestByChannel` correctness
- Thread-marker prune leaves sibling thread events persisted and lit
- Scope-isolation state machine: A rows visible in A, absent in B,
restored on A again; late A-scope write does not overwrite B's bucket
- Malformed structures/fields, relay/pubkey isolation, quota failure
degradation

**`useObservedUnreadPersistence.test.mjs`** exercises the real hook via
`createRoot` + `act`:
- pagehide flush: event recorded within debounce window survives reload
(headline regression)
- Unmount with pending write flushes before teardown
- `clearAll` cancels pending debounce so no resurrection after reload
- `removeChannel` replaces pending snapshot so sibling channel B
survives reload (two-channel repro)
- Marker prune: thread and channel markers prune covered events; sibling
channels survive
- `isScopeLoaded` returns false before identity-reset effect commits,
true after
- A→B scope switch: pending A-timer is cancelled by flush, A data
persisted synchronously (hydration round-trip)
- Stale `clearAll` from scope A rejects after scope B loads
(observed-cache scope fence)
- Stale `removeChannel` from scope A rejects after scope B loads
(observed-cache scope fence)
- API object identity stable across unrelated re-renders (catch-up
stability)

**`useUnreadChannels.test.mjs`** exercises the full parent-to-owner seam
with real hook mounts:
- Stale `markChannelRead` from scope A does not corrupt B's observed
bucket after flush
- Stale `markAllChannelsRead` from scope A does not overwrite B's bucket
after flush

## Deferred

Issues deferred to the NIP-RS arc (`#unread-messages-ux`) or future
hardening — not regressions introduced by this PR:

- **Stale-scope `forcedUnreadRef` / `markContextRead` exposure**: a
stale scope-A `markChannelRead` or `markAllChannelsRead` still deletes
B's `forcedUnreadRef` entries and advances B's NIP-RS markers via
`markContextRead` before the observed-cache fence rejects. This is
pre-existing on `origin/main` (identical shape at lines 316/330). Fix
requires touching `forcedUnreadStore` and marker paths — out of scope
for Fix A. Deferred to the NIP-RS work.
- **`isScopeLoaded` empty-scope hardening**: `isScopeLoaded()` returns
`true` when `pubkey` and `relay` are empty strings (no active session).
A guard could assert non-empty identity before stamping scope-loaded.
Low risk in practice since the hook is only mounted after auth, but
could be tightened.
- **Catch-up batch scheduling**: `handleChannelMessage` and the catch-up
loop each clone the full events map per event via
`scheduleObservedUnreadWrite`. For channels with large backlogs this
produces O(n) snapshot clones per catch-up batch. A batch-schedule API
(single snapshot at end of batch) would reduce allocations. Not
observable in normal use; deferred as a performance optimization.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-08-04 12:44:24 -04:00