Skip to content

fix(search): show slow lookups beside their section headings - #2442

Merged
tulsi-builder merged 6 commits into
mainfrom
eugene/search-channel-status
Nov 4, 2026
Merged

tulsi-builder merged 6 commits into
mainfrom
eugene/search-channel-status

Conversation

@tulsi-builder

@tulsi-builder tulsi-builder commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Context

In ⌘K search, "Searching open channels…" sat below every result group. When a channel you wanted hadn't arrived yet, nothing near the top said that results were still coming, and the status line was cramped under the last row. People and message lookups had the same problem: the community people lookup showed no progress at all, and "Searching messages…" appeared only when nothing else matched, with no delay, so it could flicker.

Description

Each section that waits on the network now shows its own status beside its heading: Channels · Searching…, People · Searching…, and Most relevant · Searching… (or Most recent · Searching…). Local sections (agents, chats, DMs, pages, plugins) are unchanged.

  • The status appears only after a lookup has run for one second, so fast searches don't flicker. It clears when that lookup settles.
  • A section with no rows yet appears only while its lookup is still running, as a heading with no empty-state row. If the lookup finds nothing, Channels and People disappear as before. Messages returns to its existing settled hint.
  • Result ranking, Enter behavior, "Show more messages", the "type at least N characters" hint, and error and retry states are unchanged.
  • The heading suffix is hidden from assistive technology, and group names stay plain. A single polite status outside the list names the sections still searching ("Searching channels and people…"). Its text is fixed once announced and cleared when none remain, so it doesn't re-announce as sources start or finish.
  • Each heading row keeps its height, and the suffix inherits the heading label's type and color. There are no CSS or shared design-system changes.

Changes

  • SearchResults.tsx: a local delayed flag per network source, a latched announcement, and headingStatus on Channels, People, and both message branches. The bottom LoadingRegion is removed.
  • SearchChoices.tsx: an optional headingStatus on SearchGroup. It admits a status-only group without an empty row and renders after the label inside the existing hideLabel heading. Most of this file's diff is biome re-wrapping the .map callback, so review it with whitespace hidden.
  • docs/buzz-help.md: one line on the new behavior.

Testing

  • Vitest component tests mount the real hooks with held service promises. They cover each source at the 999 and 1000 ms boundary, status-only groups and their removal, results arriving under the status, messages searching alongside other groups, scoped messages restoring their settled hint, staggered and simultaneous sources with the frozen announcement, and the highlighted People row still opening on Enter after Channels re-ranks above it.
  • No browser journeys were added or removed. A temporary local Playwright capture with the three lookups held open checked rendering in Chromium: light and dark themes, a 560px width with 125% text, the suffix only after the delay, and no wrapping or extra row height. The capture is not committed.
  • Review follow-up (f14a1ec20): the live status names only lookups behind rendered groups, so the author and date pickers announce nothing. A positive test covers the pending in: resolution. Both new tests fail without their production line. All 20 src/app/shell test files pass (316 tests), as do pnpm check and the pre-push hooks.
  • On 0332b36b4 (based on 1e85347de): pnpm check passes. The full Vitest run had 16,454 passing tests and 20 failing in unrelated files: MessageReactions, ProfileAgentIdentity, NotificationSettings, store-system-discovery, and browser-host/archive. Other test runs were loading the machine at the same time. In that state, running those five files alone fails similarly on unchanged origin/main (17–18 failures, a first-test 5s timeout then cascading failures). MessageReactions.test.tsx passes 22/22 on this head when run alone. Hosted CI is the authority here.
  • Before the rebase onto 1e85347de, the same change passed the full suite (16,422 tests) on 8ec2ea938. The only rebase conflict was SearchChoices.tsx against feat(search): browse open channels from a Channels tab in search #2164's hideLabel, which keeps its existing behavior.
  • The design owner tried an earlier channels-only revision in the desktop dev app. A desktop try of this final revision is still pending, so this stays a draft.

🤖 Generated with Claude Code
1-results-arriving-light
2-still-searching-dark
3-narrow-large-text

Channel, people, and message lookups that take more than a second now show
"· Searching…" next to the Channels, People, and message section headings,
instead of a single status pinned below all results. Each section waits
one second before showing it, so fast searches don't flicker, and a
section with no results yet appears only while its lookup is still
running. A single screen-reader status names the sections still searching
and doesn't re-announce while they finish.

Co-authored-by: Eugene <11b65c7022d0662a7e3c969ca511980925a21d138ec0d25214a4997e7b598240@buzz.block.builderlab.xyz>
Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>
@tulsi-builder
tulsi-builder marked this pull request as ready for review November 4, 2026 16:46
@tulsi-builder
tulsi-builder requested a review from a team as a code owner November 4, 2026 16:46

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Two small follow-ups: align the live announcement with rendered groups and add a positive assertion for pending channel resolution.

slowChannels,
slowPeople,
slowMessages,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P3] Limit announcements to the rendered result groups. With crew after: and held channel/message promises, only Dates is rendered, but after 1.5 seconds this status says ‘Searching channels and messages…’. The previous channel-only announcement already existed; announcing messages in this picker branch is new. Gate channel/message announcement sources when the date or author picker replaces those groups so assistive output matches the available choices.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Fixed in f14a1ec. The announcement now counts the channel and message lookups only when the result groups are rendered (not behind the author or date picker). New test announces no hidden result lookups behind the %s picker covers crew after: and crew from: with channel and message reads held. It fails without the gate.

Comment thread src/app/shell/SearchResults.test.tsx Outdated
);
await started;
expect(screen.getByText("Searching messages…")).toBeVisible();
expect(screen.queryByText("Searching messages…")).toBeNull();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P3] Assert the replacement pending indicator positively. This assertion checks absence of ‘Searching messages…’, copy removed from production by this PR, so it no longer protects the pending in: resolution behavior. The added message tests exercise search.loading, while messagesPending also includes operatorLookupPending. Advance 1000 ms after the lookup starts and assert the Most relevant heading status and live status text in this test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Fixed in f14a1ec. I removed the stale assertion. Doing the 1000 ms advance inside that test leaves the operator lookup and the public channel search in flight together, so the failure path shows two identical notices (existing behavior, outside this PR), which would change what that test proves. I added a sibling test instead, shows a pending in: channel lookup beside Most relevant. It holds only the in: resolution, advances 1000 ms after it starts, then asserts Most relevant · Searching…, the "Searching messages…" live status, and no message read, and that both clear on release. It fails if operatorLookupPending is dropped from messagesPending.

tulsi-builder and others added 4 commits November 4, 2026 12:20
An author or date picker replaces the result groups, so the live status
no longer names channel or message lookups that are hidden behind it.
Cover the pending in: channel lookup with a positive heading and live
status assertion instead of checking for removed copy.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>
Resolve docs/buzz-help.md by keeping both the search status and View only join entries.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>
Picks up #2444, which fixes the harness kit test failing on main.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>
Picks up #2333's channel member counts alongside the search heading status.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>

@loganj loganj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Larry and Auto: an independent adversarial review of db0c942. Source review only. Auto also ran vitest run src/app/shell at this head: 20 files, 322 tests passed. kalvinnchau's two earlier P3 threads (announce only rendered groups, positive in: assertion) are already addressed in f14a1ec, so we do not repeat them.

Verdict: approve. No must-fix. Timers reset and cancel correctly. A lookup that settles and starts again waits a fresh second. Status-only groups add no rows, so highlight and Enter are unchanged. Every network-backed source is covered: channels, people, both message branches, and in: resolution through operatorLookupPending. Apart from re-wrapping, the real SearchChoices change is small.

Not-inline note:

  • question (UX intent): messageEmpty is now undefined while a lookup is pending (SearchResults.tsx:1166-1168). With no local matches, the list is blank for up to one second. This also happens in scoped search, where "Searching messages…" used to show at once. That is the intended no-flicker trade. It is worth a line in the design review.

),
)}
{(notice ||
(!destinations.length && !headingStatus)) && (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

advice (a11y/ARIA): a status-only group renders role="rowgroup" (grid mode, when inline settings give hasControls) or role="group" with no row/option inside. In grid mode, the old empty-state row satisfied that, but !destinations.length && !headingStatus now drops it. Suggestion: set aria-busy="true" on a status-only group. The required-children rule exempts busy containers, and it also states the loading meaning.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Done in 6c03d1f. A status-only group (heading status, no destinations) now sets aria-busy="true". A group with rows and a status doesn't, so its existing rows stay readable. The People status-only test asserts both cases.

Comment thread src/app/shell/SearchChoices.tsx Outdated
className="navigation-section-heading"
aria-hidden={hasControls || undefined}
>
<h2 className="navigation-section-label text-body text-secondary truncate">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

advice (convention/scope): truncate now applies to every SearchChoices heading, not only headings with a status. That includes PageSearch, SearchSettings, ChannelDirectoryResults and inline settings. Long labels that used to wrap now get an ellipsis, and the description does not mention it. Either apply it only when headingStatus is set, or call it out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Done in 6c03d1f. truncate now applies only when headingStatus is set, so PageSearch, SearchSettings, ChannelDirectoryResults and inline settings headings wrap as they did before.

sources.length === 3
? `${sources.slice(0, 2).join(", ")}, and ${sources[2]}`
: sources.join(" and ");
return previous || `Searching ${names}…`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (a11y): the announcement keeps its first text until no section is searching. Example: channels become slow, then messages become slow before channels finish. The status still says "Searching channels…" after channels are done, while only messages are still searching. A screen-reader user who reads the status with the virtual cursor gets the wrong source. Options: keep neutral text ("Still searching…"), or accept this on purpose.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Accepted on purpose. A polite live region re-announces whenever its text changes, so when sources start and finish at different times, updating the text caused repeated announcements. The design review chose a fixed message per searching episode. The stale-source case you describe is real but brief: the status clears when the last source settles, and the visual headings always show which sections are still searching. If a neutral "Still searching…" for the second and later sources reads better to you, it's a one-line follow-up, and I'll take it to the design owner.

Comment thread src/app/shell/SearchResults.tsx Outdated
scopedChannelId ? "" : parsed.text,
list.status === "ready",
);
const channelsPending =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (completeness) + advice (simplicity):

  • Nothing shows while the channel roster itself is still loading. usePublicChannelSearch is inactive until list.status === "ready", so on a cold start Channels never shows "Searching…", even though channels and chats can be incomplete. The Enter pending gate (~line 1424) already treats that state as pending, and Messages counts it through operatorLookupPending. The old bottom LoadingRegion had the same gap, so this is not a regression. Is a cold-roster status wanted here?
  • !!query.trim() && !scopedChannelId repeats checks the hook already makes: when scoped, its query is "", and an empty parsed.text is not searchable. publicChannels.loading alone is enough.
  • Note only: useDelayedSearch (line 112) is the fourth local copy of the 1000 ms delay, after LoadingRegion, ChannelLifecycleDialog and UsagePage. A shared hook would keep the delay in one place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Thanks. Simplified in 6c03d1f: the channel status now reads publicChannels.loading alone. The hook is idle for scoped search (empty query) and for unsearchable text, and the shell and channel tests still pass. The cold-roster status is left out on purpose because, as you note, it isn't a regression from the old bottom indicator, and it would be a new source for this PR. It's a reasonable follow-up. I'm also leaving the shared 1000 ms hook for a follow-up rather than widening this PR into LoadingRegion, ChannelLifecycleDialog and UsagePage.

A group showing only "Searching…" has no rows yet, so it is aria-busy.
Only headings with a status truncate; other search headings wrap as
before. The channel status reads the lookup's own loading flag.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>
@tulsi-builder

Copy link
Copy Markdown
Contributor Author

🤖 Thanks for the review. On the messageEmpty question: yes, the brief blank list (up to one second) is the intended no-flicker trade, including in scoped search. The old immediate "Searching messages…" flashed on every fast lookup, which was the reason for this change. After one second, the Most relevant heading shows "· Searching…". The advice items are addressed in 6c03d1f.

@tulsi-builder
tulsi-builder merged commit 0db2588 into main Nov 4, 2026
41 checks passed
@tulsi-builder
tulsi-builder deleted the eugene/search-channel-status branch November 4, 2026 20:25
wpfleger96 added a commit that referenced this pull request Nov 4, 2026
…-nip-fi

* origin/main: (23 commits)
  fix(inbox): show Solo responses under All, read live replies in the open conversation, describe toolbar badges (#2275)
  AIDA-2434: Show saved teams in mentions (#2339)
  AIDA-2455: Restore channel scrollbar dragging beside threads (#2436)
  feat(agent-protection): show the owner why a protection provider denied input (#2105)
  fix(search): show slow lookups beside their section headings (#2442)
  Align Bestie DM layout and unread state; speed up thinking shimmer (#2418)
  Send Claude, Pi and Goose history as exact records (#2088)
  fix(agent-activity): align the stopped note and show it after the agent's reply (#2441)
  fix(agents): stop orphaned Attention commands and prevent duplicate runs (#2448)
  feat(mentions): quieter mention menu that marks agents and their owners (#2328)
  feat: add a Canvas View tab, History diffs and a 256 KiB limit (#2318)
  feat(plugins): let local agents write and reload plugins (#2435)
  feat(onboarding): offer Create a community after public Builderlab sign-in (#2397)
  Open any Inbox filter view by route (#2394)
  fix(bestie): deleting an agent saves no Bestie record where there was none (#2420)
  fix(unread): never treat agents-audience messages as unread (#1552)
  feat(focus): add an optional Focus plugin that quiets everything except Solo chats (#2277)
  [5/5] Add protection to the Goose harness plugin (#839)
  feat(agents): identify plugins in native hook output (#2299)
  [4/5] Add protection to the Codex harness plugin (#1879)
  ...

# Conflicts:
#	src/features/identity/builderlab-hosted.test.ts
#	src/features/identity/builderlab-hosted.ts
gilgtm added a commit that referenced this pull request Nov 4, 2026
* origin/main:
  fix: exclude muted channels from sidebar unread scroll indicators (#2335)
  fix(claude-code): open long Claude transcripts a page at a time (#2428)
  feat(solo): share chats as view only (#2375)
  fix(inbox): show Solo responses under All, read live replies in the open conversation, describe toolbar badges (#2275)
  AIDA-2434: Show saved teams in mentions (#2339)
  AIDA-2455: Restore channel scrollbar dragging beside threads (#2436)
  feat(agent-protection): show the owner why a protection provider denied input (#2105)
  fix(search): show slow lookups beside their section headings (#2442)
  Align Bestie DM layout and unread state; speed up thinking shimmer (#2418)
  Send Claude, Pi and Goose history as exact records (#2088)
  fix(agent-activity): align the stopped note and show it after the agent's reply (#2441)
  fix(agents): stop orphaned Attention commands and prevent duplicate runs (#2448)

Signed-off-by: Gil Birman <[email protected]>
Co-authored-by: Claude <[email protected]>

# Conflicts:
#	src/app/shell/PageNavigation.tsx
damienrj added a commit to damienrj/buzz-app that referenced this pull request Nov 4, 2026
…-clone-auth-ab8b97

* origin/main: (38 commits)
  feat(identity): decide a missing backup choice from community membership (block#2378)
  Offer Builderlab decisions in FI builds (block#2227)
  feat(inbox): preview recent Inbox activity when hovering the sidebar row (block#2347)
  Add collapsible Solo and Team sidebar sections (block#2296)
  fix(codex): open long Codex transcripts a page of items at a time (block#2259)
  Keep Solo model and effort choices per chat (block#2342)
  feat(sidebar): smooth channel reordering with bounded motion (block#2108)
  Connect legacy agents from DMs and channel mentions (block#2423)
  fix: exclude muted channels from sidebar unread scroll indicators (block#2335)
  fix(claude-code): open long Claude transcripts a page at a time (block#2428)
  feat(solo): share chats as view only (block#2375)
  fix(inbox): show Solo responses under All, read live replies in the open conversation, describe toolbar badges (block#2275)
  AIDA-2434: Show saved teams in mentions (block#2339)
  AIDA-2455: Restore channel scrollbar dragging beside threads (block#2436)
  feat(agent-protection): show the owner why a protection provider denied input (block#2105)
  fix(search): show slow lookups beside their section headings (block#2442)
  Align Bestie DM layout and unread state; speed up thinking shimmer (block#2418)
  Send Claude, Pi and Goose history as exact records (block#2088)
  fix(agent-activity): align the stopped note and show it after the agent's reply (block#2441)
  fix(agents): stop orphaned Attention commands and prevent duplicate runs (block#2448)
  ...

Signed-off-by: damienrj <[email protected]>
kalvinnchau added a commit that referenced this pull request Nov 5, 2026
## Problem

#2442 added `useDelayedSearch`, which shows "Searching…" once a lookup
has been pending for a second. Its effect calls `setSlow(false)` on
every `pending` change, and `pending` flips on every keystroke while the
relay looks up the new text. The state is almost always already `false`,
but the set still re-rendered `SearchResults`, and each render ranks
every channel and person name against the text again. In jsdom that is 4
renders per keystroke instead of the 3 before #2442, plus one more for
each additional lookup round under real relay latency.

## Change

A ref records whether the slow notice was actually set. The effect
resets the state only then. The timer that shows the notice is
unchanged.

## No behavior changes

The hook still returns `pending && slow`: the notice appears after 1 s
of pending and clears as soon as the lookup settles or the text changes.
The search palette suites pass unchanged, including every "Searching…"
test. There is no new test: the DOM is identical, so only a render count
would show the change.

## Evidence

Browser benchmark with 128 DMs, 1,001 participants and read state, at 4×
CPU throttling in production Chromium:
- **Switch, quiet:** open Cmd+K, type a channel name and press Enter,
with no other traffic.
- **Switch:** the same while another channel receives a message every
200 ms.

Stock and this PR were interleaved on 8 Linux and 8 macOS workstations.
Medians are per run.

| scenario, metric | Linux: stock | Linux: this PR | Linux Δ % | macOS:
stock | macOS: this PR | macOS Δ % |
|---|---|---|---|---|---|---|
| quiet, script | 379.5 ms | 372.1 ms | −1.9% | 253.1 ms | 251.6 ms |
−0.6% |
| quiet, task | 636.3 ms | 630.4 ms | −0.9% | 418.7 ms | 417.4 ms |
−0.3% |
| switch, script | 653.2 ms | 648.2 ms | −0.8% | 373.2 ms | 368.1 ms |
−1.4% |
| switch, blocking | 308.6 ms | 292.2 ms | −5.3% | 121.6 ms | 120.1 ms |
−1.3% |

Lower is better. Blocking is long-animation-frame blocking time. There
are 96 samples per cell. The benchmark's relay answers lookups almost
immediately; with real latency each keystroke has more lookup rounds,
and each one cost an extra render before this change.

Checks: the `SearchResults`, `PageSearch` and `SearchChoices` suites
pass (174 tests), along with `tsc` and `biome`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Kalvin C <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
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.

3 participants