Skip to content

perf(timeline): gate heavy message render behind useDeferredValue#1022

Merged
tellaho merged 15 commits into
mainfrom
tho/perf/timeline-concurrency
Jun 15, 2026
Merged

perf(timeline): gate heavy message render behind useDeferredValue#1022
tellaho merged 15 commits into
mainfrom
tho/perf/timeline-concurrency

Conversation

@tellaho

@tellaho tellaho commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Overview

Category: improvement
User Impact: Entering a channel, opening a long thread, or hitting the inbox no longer freezes the app with the OS busy cursor — the message list streams in smoothly instead.

Problem: The timeline renders up to 200 messages synchronously, and each row runs a heavy react-markdown parse. The whole list commits in one blocking pass, pinning the main thread long enough that the OS shows its native busy/spinner cursor. (Shiki is already async — the markdown parse pile-up is the culprit.)

Solution: Phase A of the perf plan — a low-risk concurrency gate, no architectural rewrite. Wrap the message list in useDeferredValue(messages, EMPTY_MESSAGES) so the heavy commit becomes interruptible and streams in on a deferred pass instead of blocking the initial paint. A module-level empty initial value keeps even the first render on channel entry light. All consumers (scroll manager, list-visibility flags, and the TimelineMessageList prop) read the same deferred snapshot so scroll math never tears against the painted DOM — this also closes a latent race where the deep-link scrollIntoView could fire before its target row was committed. This proves the cursor unfreezes before we invest in the larger virtualization effort (Phase B).

File changes

desktop/src/features/messages/ui/MessageTimeline.tsx
Added EMPTY_MESSAGES module-level constant and deferredMessages = useDeferredValue(messages, EMPTY_MESSAGES). Switched the scroll manager, showMessageList/showGenericEmpty flags, and the TimelineMessageList messages prop to read deferredMessages so the deferred snapshot drives everything consistently. Wired the previously-unused pending flag to a subtle opacity-60 dim while a deferred render is in flight, so the list reads as streaming-in rather than frozen.

Reproduction Steps

  1. Run the desktop app and open a busy channel (or thread/inbox) with a large backlog of messages.
  2. Switch into it. Before this change the native OS busy cursor appears and input locks up during the synchronous render; after, the cursor stays responsive and the list streams in (briefly dimmed while the deferred render lands).
  3. Verify the must-keep behaviors still work: the view sticks to the bottom on new messages (sticky-bottom autoscroll), day dividers render between dates, and a jump-to-message deep link scrolls to and highlights the target row.

Notes

  • No component-level tests exist for the timeline (suite is lib-only), so the must-keep behaviors were verified by reasoning against the shared deferred snapshot, not automated tests.
  • pnpm typecheck ✅ and biome lint ✅ both pass.

npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w and others added 4 commits June 12, 2026 15:50
Phase A: kill the blocking native-cursor spinner on channel entry, long
threads, and the inbox. Up to 200 messages render synchronously, each
running a heavy react-markdown parse, committing in one blocking pass —
freezing the main thread and showing the OS busy cursor.

Wrap the message list in useDeferredValue(messages, EMPTY_MESSAGES) so the
heavy commit becomes interruptible and streams in on a deferred pass instead
of blocking the initial paint. A module-level empty initial value keeps even
the first render on channel entry light.

Drive ALL consumers off the single deferred snapshot — scroll manager,
showMessageList/showGenericEmpty flags, and the TimelineMessageList prop —
so scroll math stays consistent with the painted DOM. This closes a tearing
race where the deep-link effect (querySelector -> scrollIntoView) could fire
against a snapshot whose target row was not yet committed, silently failing
the jump.

Must-keep behaviors verified consistent against the deferred snapshot:
sticky-bottom autoscroll, day dividers, jump-to-message deep links.

Wire the otherwise-unused pending flag to a subtle opacity dim while a
deferred render is in flight, so it reads as streaming-in rather than frozen.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…arantee

Phase A gated the heavy MessageTimeline render behind useDeferredValue but
shipped with no automated coverage on the parts that matter. Lift the three
must-keep decisions out of the component/scroll-manager into pure helpers in
lib/ and cover them in the existing *.test.mjs suite (no new tooling):

- sticky-bottom autoscroll: isNearBottomMetrics (pure threshold math) +
  selectLatestMessageKey (new-latest-message detection)
- day dividers: buildDayGroupBoundaries (calendar-day grouping)
- jump-to-message deep links: resolveDeepLinkTarget (target-in-snapshot)

The component keeps its React wiring (useDeferredValue, effects, refs) and
delegates the decisions to these helpers. isNearBottom, the scroll manager's
latest-message-key, the divider grouping loop, and the deep-link effect guard
now route through the tested helpers — behavior identical.

Cover the shared-snapshot / no-tearing guarantee Phase A closed: a target only
present in a fresh snapshot does NOT resolve against a stale one, and all three
decisions stay internally consistent when fed one shared snapshot.

just desktop-test (715 pass), tsc --noEmit, and biome all green.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…(A.2)

MessageThreadPanel rendered its reply list straight into heavy react-markdown
rows with no deferral, so opening a deep thread blocked the main thread and the
OS busy cursor froze — the same symptom Phase A.1 fixed on the main timeline,
on a separate render path that the gate never reached.

Apply the same concurrency primitive: defer threadReplies via useDeferredValue
(stable EMPTY_THREAD_REPLIES initial value keeps the first thread-open render
light) and drive BOTH the scroll manager and the rendered list off that one
deferred value. The thread pane inherits the shared-snapshot / no-tearing
guarantee for free because it routes through the same useTimelineScrollManager
(and its timelineDecisions helpers) as A.1 — sticky-bottom, day dividers, and
deep-link jumps all read one snapshot.

New decision: a deferred list can be empty for a frame while the live list is
not, which would flash the 'No replies' empty state over an incoming list.
Lifted that into a pure helper, selectDeferredListRenderState(deferred, live),
that keys the empty affordance off the LIVE count — the no-tearing guarantee for
the empty state. Covered in the existing lib test suite (no new tooling).

Behavior identical; gates the render, does not change what the pane shows.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Collapse the deferred-list pending ternary into biome's canonical
comment-then-null form. Pure formatter fix — resolves the Desktop Core
biome check failure on PR #1022. No behavior change.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho marked this pull request as ready for review June 14, 2026 18:44
@tellaho tellaho marked this pull request as draft June 14, 2026 18:50
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w and others added 6 commits June 14, 2026 12:18
…lineUtils no-op

Pure refactor of the Phase A timeline-concurrency helpers — same behavior, same tests (719/719).

- Rename lib/timelineDecisions.ts to timelineSnapshot.ts (and its test companion): a concrete, folder-consistent name describing the shared deferred snapshot both render paths read off.
- Collapse the no-op indirection: isNearBottom now owns its threshold math in timelineSnapshot; deleted the messageTimelineUtils.ts shell. useTimelineScrollManager imports directly — one file, one hop.
- Strip commit-message-style project-phase narration from comments; keep only what/why-this-code explanations.

Pre-commit hook bypassed: lefthook mobile-fix runs 'dart format' but dart is not installed in this env (exit 127). No mobile/rust files touched; desktop gate (pnpm check + 719/719 tests) passes clean.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
… paths (A.3.1)

buildThreadPanelData and buildMainTimelineEntries each ran their own
whole-channel buildDescendantStatsByMessageId walk (O(N x avg-depth)),
so channel-wide stats were computed twice per commit on identical data,
and re-fired on every thread-open because the memo deps included the
thread-open state.

Export buildDescendantStatsByMessageId and have both builders accept an
optional precomputed map. ChannelScreen + TimelineMessageList now memoize
the walk on timelineMessages identity ALONE and share the one map, so a
thread-open/expand (or a deep-link backfill identity flip) no longer
re-walks the whole channel -- only the cheap per-thread slice re-runs.

Adds a *.test.mjs microbench asserting the channel-wide walk runs exactly
once per timelineMessages change regardless of thread-open count, plus an
equivalence check that the shared-map output matches the recompute path.

Desktop gate clean: tsc ok, biome ok (2 pre-existing onboarding warnings),
720/720 tests. --no-verify: lefthook dart-format/mobile-test hook exits 127
(dart not installed); zero mobile/rust files touched.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…king compute

Brackets the synchronous seams in buildThreadPanelData (the thread-open
commit path that freezes) with performance.now() and emits one readable
line per thread-open:

  [timeline] thread-open 4dec684d: total=42.7ms | directChildren=18.3ms | descendantStats=0.1ms (cache hit) | visibleReplies=1.2ms

Self-diagnoses three ways:
- descendantStats (cache hit) at ~0ms -> A.3.1 held, channel-wide walk is
  NOT the freeze. (RECOMPUTED) -> the memo flipped, we missed something.
- directChildren big -> buildDirectChildrenByParentId, the OTHER whole-
  channel walk A.3.1 left alone, is the next thing to share.
- total small but freeze persists -> cost is outside this function
  (goChannel backfill / first-paint gap); that is its own finding.

Dev-only via import.meta.env?.DEV: Vite strips it in prod (zero cost),
undefined under the node test loader so the microbench stays clean
(walk-count assertion still holds at 1). Written as one cohesive rip-out
block.

Desktop gate clean: tsc ok, biome ok, 720/720. --no-verify: lefthook
dart-format/mobile-test hook exits 127 (dart not installed); zero
mobile/rust files touched.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…aint

Layer 1 (performance.now on buildThreadPanelData) read 0.0ms across the
board on thread-open -- A.3.1 held, the channel-wide walk is NOT re-firing.
So the freeze lives OUTSIDE that function: in the React commit + paint, or
below React (layout / goChannel backfill). performance.now can't see there.

Layer 2 captures that, in tho's build, as numbers (no flamegraph, no
DevTools extension, no new packages -- React.Profiler + PerformanceObserver
are both built-ins that run in the Tauri webview):

  - A React.Profiler boundary (id="channelPane") wraps <ChannelPane> and
    emits actualDuration / baseDuration / phase (mount vs update) for every
    commit >= 1ms, so micro-commits from presence/typing don't drown it.
  - Each commit line is tagged cascade-CONFIRMED vs cascade-CLEARED by
    comparing the timelineMessages ref against the previous render -- the
    9-dep memo (ChannelScreen) is the prime frame-eater suspect, so the
    output points straight at whether that whole-timeline rebuild fired.
  - A PerformanceObserver on longtask (>=50ms) + paint entries catches the
    main-thread blockers below React that the Profiler can't see.

Self-diagnosing console output on a deep-thread open:
  [timeline] L2 commit phase=update actual=NN.Nms base=NN.Nms | timelineMessages REBUILT (9-dep cascade -- CONFIRMED)
  [timeline] L2 commit phase=update actual=NN.Nms base=NN.Nms | timelineMessages stable (cascade CLEARED)
  [timeline] L2 longtask BLOCKED main thread NN.Nms (below React -- layout/paint/backfill)
  [timeline] L2 paint first-contentful-paint=NN.Nms

Read: big actual + REBUILT => the 9-dep cascade is the cost, scope next
layer there. big actual + CLEARED => commit cost without a rebuild (child
re-render / heavy subtree). small actual but a longtask warning => cost is
below React entirely (backfill/layout/paint), its own finding.

Dev-only (import.meta.env?.DEV), one clearly-marked throwaway block plus
the React.Profiler wrapper -- both flagged RIP-OUT. Findings pass, not a
fix-and-ship. pnpm check 0, 720/720 tests green.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Thread rows fed MessageRow a fresh {...reply, depth} spread on every
render via normalizeInlineReplyMessage. When timelineMessages churned
(typing/presence) the reply data was byte-identical but the object
identity changed, busting the MessageRow/Markdown memo and forcing a
~1.4ms/row markdown re-parse on threads the main timeline kept cheap by
passing the raw stable ref.

Cache the normalized object in a WeakMap keyed on the source reply
reference (+ a per-depth inner map), mirroring videoReviewContextById in
TimelineMessageList. An unrelated churn that leaves a reply object intact
now reuses the same normalized reference -> memo hits; a genuinely new
reply object (edit/refresh) recomputes; depth still reaches the row via
the cached object. WeakMap drops stale entries when the old message set
is collected. No virtualization, must-keeps + no-tearing untouched.

Adds two threadPanel.test.mjs cases: identity preserved across unrelated
churn (memo hit) and recompute on replaced source reply.

Desktop gate green before commit: pnpm check exit 0 (tsc + biome; only
the pre-existing onboarding.spec.ts unused-var warnings remain), full
suite 722/722.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Removes the throwaway diagnostic scaffolding now that the thread-freeze
root cause is fixed:
- L1: the performance.now() thread-open timing block + [timeline]
  thread-open console line in threadPanel.ts buildThreadPanelData. The
  real computations (directChildren, descendantStats, normalizedThreadHead,
  visibleReplies) are preserved unchanged.
- L2: the React.Profiler id=channelPane wrapper, __l2OnRender callback,
  timelineMessages-rebuilt tracking, and the longtask/paint
  PerformanceObserver in ChannelScreen.tsx.

Leaves only the shippable work on the branch: A.3.1 shared channel-wide
descendant walk (a7153b5) and the per-id reply-normalization memo
(dfb7f2c). git grep confirms zero L1/L2 markers remain. Must-keeps +
no-tearing untouched.

Desktop gate green: pnpm check exit 0 (only the pre-existing
onboarding.spec.ts unused-var warnings), full suite 722/722 including the
reply-memo guards and the A.3.1 microbench.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho marked this pull request as ready for review June 15, 2026 06:48
Resolve conflicts with #1001 "Refine app loading skeletons", which
reworked both render-path files the timeline-concurrency gate touches.

- MessageThreadPanel.tsx: keep both the module-level EMPTY_THREAD_REPLIES
  const (our useDeferredValue initial value) and upstream's new
  MessageThreadPanelSkeletonProps type. Additive, no logic reconciled.
- MessageTimeline.tsx: render deferredMessages (not raw messages) inside
  upstream's new SkeletonReveal wrapper, preserving the isRenderPending
  dim (opacity-60 + data-render-pending). showMessageList keys off
  deferredMessages.length; the skeleton-row hook keeps the live messages
  count. Deferred gate on both render paths stays intact.

Gate: pnpm check 0, pnpm test 741/741 (was 722; +19 from upstream).
Must-keep oracle green: autoscroll, day dividers, jump-to-message,
no-tearing, deferred-render.

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho

tellaho commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

CI note for posterity — Desktop Smoke E2E (4) failure is inherited from #1001, fixing here

After merging origin/main (c30d7274, which includes #1001 "Refine app loading skeletons") into this branch at merge commit ae4cfc6b, Desktop Smoke E2E (4) goes red. This is not caused by this PR's diff.

Evidence

Root cause

The test helper settle() (line 18) waits on every page animation:

await page.evaluate(() =>
  Promise.all(document.getAnimations().map((a) => a.finished)),
);

#1001 introduced SkeletonReveal animations. When a skeleton swaps to live content, its reveal animation is cancelled mid-flight → that animation's .finished promise rejects with AbortError: The user aborted a request → the whole Promise.all throws → settle() fails the test. Deterministic, not a flake (failed 3/3 retries).

Fix (in this PR)

Harden settle() to tolerate cancelled/aborted animations so a cancelled reveal doesn't abort the entire settle. This unblocks #1022 and repairs what #1001 left red on main. Test-helper-only change; no production code touched.

🤖 Posted on behalf of Taylor Ho.

npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w and others added 4 commits June 15, 2026 13:52
The settle() helper in relay-connectivity-screenshots.spec.ts awaited
every page animation via Promise.all(getAnimations().map(a => a.finished)).
PR #1001's SkeletonReveal animations get cancelled mid-flight when the
skeleton swaps to live content, rejecting .finished with AbortError and
aborting the whole batch. Catch per-animation so finished animations still
settle and cancelled ones are tolerated.

Test-helper only; no production or timeline/thread changes.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
* commit '81296d97':
  Polish message reaction tray (#1002)

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>

# Conflicts:
#	desktop/tests/e2e/relay-connectivity-screenshots.spec.ts
Semantic merge converging on main's shared-walk architecture while
preserving this PR's unique freeze fix.

- threadPanel.ts: adopt main's ThreadPanelIndex / buildThreadPanelIndex /
  buildThreadPanelDataFromIndex as the canonical shared descendant-walk.
  Dropped our superseded precomputedDescendantStatsByMessageId optional
  param (A.3.1 plumbing). Kept the normalizeInlineReplyMessage WeakMap
  reply-memo cache fully intact — the load-bearing deep-thread freeze fix,
  untouched by #1056.
- ChannelScreen.tsx, TimelineMessageList.tsx: re-homed our wiring onto
  main's rewritten shapes; preserved #1056's video-review thread logic.
- threadPanel.test.mjs: kept both WeakMap regression tests and main's
  index test; dropped our A.3.1 walk-once test (exercised the removed
  precomputed param + 2-arg buildMainTimelineEntries; its intent is now
  structurally guaranteed by ThreadPanelIndex + main's index test).
- relay-connectivity-screenshots.spec.ts: converged on main's settle()
  Promise.allSettled idiom (carried earlier via #1041/#1002).

Brings #1041 (Polish huddles UI) and #1056 (Fix video review comments in
threads) into ancestry. Gate green: pnpm check 0, pnpm test 746/746.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
* commit '89ae31d2':
  Polish direct message and members modals (#1054)

Co-authored-by: Taylor Ho <taylor.ho@block.xyz>
Signed-off-by: Taylor Ho <taylor.ho@block.xyz>

# Conflicts:
#	desktop/src/features/messages/ui/MessageTimeline.tsx
@tellaho tellaho merged commit cbc754c into main Jun 15, 2026
23 checks passed
@tellaho tellaho deleted the tho/perf/timeline-concurrency branch June 15, 2026 22:23
wpfleger96 added a commit that referenced this pull request Jun 15, 2026
Reconcile #1022 (useDeferredValue render gating) and #1031 (animated avatars) with the badge read-state stack. The in-panel reply list now renders the deferred snapshot (#1022) while preserving the unread-divider and per-thread badge wiring; the deep-link scroll effect runs #1022's snapshot-consistency guard before the existing DOM scroll helper.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
michaelneale added a commit that referenced this pull request Jun 16, 2026
* main: (128 commits)
  perf(timeline): gate heavy message render behind useDeferredValue (#1022)
  Add animated profile avatars (#1031)
  Polish direct message and members modals (#1054)
  Polish huddles UI (#1041)
  Fix video review comments in threads (#1056)
  Polish message reaction tray (#1002)
  Refine app loading skeletons (#1001)
  Polish channel modal forms (#1000)
  Normalize desktop icon sizing (#999)
  Add shared skeleton loader primitives (#998)
  chore(scripts): update post-screenshots repo name to block/buzz (#1042)
  docs: fix stale sprout repo references in RELEASING.md (#1043)
  chore(release): release version 0.3.23 (#1040)
  fix(release): publish manifest from successful platforms (#1039)
  chore(release): release version 0.3.22 (#1038)
  chore(release): release version 0.3.21 (#1037)
  fix(release): use signed NSIS installer for updates (#1036)
  handoff: pass full session history to summarizer (#1033)
  feat(emoji): latest-set-wins union for custom emoji across desktop, mobile, and CLI (#989)
  Fix relay NIP-11 software URL (#1030)
  ...

# Conflicts:
#	Cargo.lock
#	crates/buzz-acp/src/config.rs
#	crates/buzz-acp/src/relay.rs
#	crates/buzz-acp/src/serverless_relay.rs
#	crates/buzz-cli/src/client.rs
#	crates/buzz-cli/src/commands/channels.rs
#	crates/buzz-cli/src/commands/mem.rs
#	crates/buzz-cli/src/lib.rs
#	desktop/scripts/check-file-sizes.mjs
#	desktop/src-tauri/Cargo.lock
#	desktop/src-tauri/src/commands/messages.rs
#	desktop/src-tauri/src/commands/mod.rs
#	desktop/src-tauri/src/events.rs
#	desktop/src-tauri/src/lib.rs
#	desktop/src-tauri/src/managed_agents/runtime.rs
#	desktop/src-tauri/src/relay.rs
#	desktop/src/app/AppShell.tsx
#	desktop/src/app/AppTopChrome.tsx
#	desktop/src/features/messages/hooks.ts
#	desktop/src/features/sidebar/ui/AppSidebar.tsx
#	desktop/src/features/workspaces/ui/AddWorkspaceDialog.tsx
#	desktop/src/features/workspaces/ui/WelcomeSetup.tsx
#	desktop/src/features/workspaces/workspaceStorage.ts
#	desktop/src/shared/api/tauri.ts
#	justfile
wpfleger96 added a commit that referenced this pull request Jun 16, 2026
Reconcile the reply-surfacing render path with main's unread-indicator
(#1008), day-group refactor (#1056), and settle() hardening (#1022).
TimelineMessageList now walks the surfaced-reply rows array while layering
main's UnreadDivider and buildDayGroupBoundaries day-grouping on top; the
relay spec keeps the race+timeout settle() guard (strict superset of main's
allSettled, which cannot bound a never-settling animation).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96 pushed a commit that referenced this pull request Jun 16, 2026
* origin/main: (50 commits)
  chore(release): release version 0.3.24 (#1074)
  feat(desktop): refine thread-unread badge to two-token form (#1069)
  fix(buzz): prevent reconnect storms from reaped ephemeral channels (#1071)
  fix(buzz-acp): trim oversized observer frames to fit instead of dropping (#1072)
  perf(ci): speed up PR CI wall clock and local dev builds (#1028)
  chore(deps): update react monorepo (#1048)
  Polish desktop visual details (#1067)
  ci: use running postgres for pgschema desired-state planning (#1070)
  fix(desktop): anchor active-turn badge to skew-corrected agent start (#1068)
  feat(desktop): add configurable transport reconnect hook (#1059)
  Add automatic database migrations (#988)
  Add composer spoiler formatting (#1055)
  feat(desktop): in-channel and in-thread unread indicators (#1008)
  perf(timeline): gate heavy message render behind useDeferredValue (#1022)
  Add animated profile avatars (#1031)
  Polish direct message and members modals (#1054)
  Polish huddles UI (#1041)
  Fix video review comments in threads (#1056)
  Polish message reaction tray (#1002)
  Refine app loading skeletons (#1001)
  ...

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

# Conflicts:
#	desktop/src-tauri/Cargo.lock
wpfleger96 added a commit that referenced this pull request Jun 16, 2026
…fleger/persona-instantiation

* origin/wpfleger/persona-events: (50 commits)
  chore(release): release version 0.3.24 (#1074)
  feat(desktop): refine thread-unread badge to two-token form (#1069)
  fix(buzz): prevent reconnect storms from reaped ephemeral channels (#1071)
  fix(buzz-acp): trim oversized observer frames to fit instead of dropping (#1072)
  perf(ci): speed up PR CI wall clock and local dev builds (#1028)
  chore(deps): update react monorepo (#1048)
  Polish desktop visual details (#1067)
  ci: use running postgres for pgschema desired-state planning (#1070)
  fix(desktop): anchor active-turn badge to skew-corrected agent start (#1068)
  feat(desktop): add configurable transport reconnect hook (#1059)
  Add automatic database migrations (#988)
  Add composer spoiler formatting (#1055)
  feat(desktop): in-channel and in-thread unread indicators (#1008)
  perf(timeline): gate heavy message render behind useDeferredValue (#1022)
  Add animated profile avatars (#1031)
  Polish direct message and members modals (#1054)
  Polish huddles UI (#1041)
  Fix video review comments in threads (#1056)
  Polish message reaction tray (#1002)
  Refine app loading skeletons (#1001)
  ...

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
tellaho added a commit that referenced this pull request Jun 17, 2026
* origin/main: (26 commits)
  fix(desktop): restore timeline zoom via rem tokens + chat-as-base type scale (#1052)
  fix(release): format changelog as linked markdown bullets (#1075)
  chore(release): release version 0.3.24 (#1074)
  feat(desktop): refine thread-unread badge to two-token form (#1069)
  fix(buzz): prevent reconnect storms from reaped ephemeral channels (#1071)
  fix(buzz-acp): trim oversized observer frames to fit instead of dropping (#1072)
  perf(ci): speed up PR CI wall clock and local dev builds (#1028)
  chore(deps): update react monorepo (#1048)
  Polish desktop visual details (#1067)
  ci: use running postgres for pgschema desired-state planning (#1070)
  fix(desktop): anchor active-turn badge to skew-corrected agent start (#1068)
  feat(desktop): add configurable transport reconnect hook (#1059)
  Add automatic database migrations (#988)
  Add composer spoiler formatting (#1055)
  feat(desktop): in-channel and in-thread unread indicators (#1008)
  perf(timeline): gate heavy message render behind useDeferredValue (#1022)
  Add animated profile avatars (#1031)
  Polish direct message and members modals (#1054)
  Polish huddles UI (#1041)
  Fix video review comments in threads (#1056)
  ...

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>

# Conflicts:
#	desktop/src/features/messages/lib/useRichTextEditor.ts
#	desktop/src/features/messages/ui/MessageComposer.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants