Skip to content

Search in the pane: a live field, its scopes, and the Find keys. - #55

Merged
itz4blitz merged 6 commits into
mainfrom
feat/search-pane-scopes
Oct 24, 2026
Merged

itz4blitz merged 6 commits into
mainfrom
feat/search-pane-scopes

Conversation

@unRekable

Copy link
Copy Markdown
Collaborator

What

The message search pane gets its own field and its scopes, per docs/product/05-navigation.md §5 and docs/design/polish/07-panes.md §2:

  • The pane's head is a live field now (it was the switcher's words as text). Every keystroke asks the cache.
  • Command-F opens it on the conversation open ("Search in Conversation"), Shift-Command-F everywhere ("Search Everywhere"); both come back to the Edit menu as the Find submenu, with their rows in Settings' shortcut list. Chips switch the scope of a standing pane: This conversation · This space · Everywhere. A scope about nothing open falls back to everywhere, as the switcher's row does.
  • The keyboard moves over the hits (up/down), Enter opens the picked one, a clicked hit stays lit. Esc closes the pane and returns the keys to the composer.
  • SearchMessages grows an optional space filter, serde-default, so an older frame still reads; the store search narrows on conversation.space.

Two fixes found while testing this on Windows

  • The picker sweep could take its directory out from under a download. A preview asked again while its picker was closing raced the removal of the directory it streams into: the download failed with a path not found and the pane's ask went unanswered. The close is one decision under the lock now (only while the picker is still idle), the write makes the part's directory again, and a rename that lost its directory fetches once more. The test's idle rises from 2 s to 5 s so a slow machine cannot overtake it.
  • The Discord tests spawned the Unix true as their placeholder child and failed before any test ran on a Windows machine without Git's usr/bin on PATH. Windows takes cmd /C exit 0.

Tests

cargo test --workspace green on Windows x64 (after protoc 29.3 and Git's openssl on PATH, which the mediad voice tests use), cargo test -p tinyclient --features gui 350 passed, the picker test repeated stable.

Known gaps

  • Result rows show title · age · snippet · cached; the spec's "network icon · space › conversation · date" context line is the next slice.
  • The network layer on Enter ("Searched Slack · acme") stays out, as the pane's empty state says.

Side observation, not part of this PR: calls::a_ring_we_placed… and polish's twelve parallel daemons against a 10 s socket deadline are timing-flaky on slow or loaded Windows machines; both pass in isolation and in CI.

@unRekable

Copy link
Copy Markdown
Collaborator Author

Rendered the demo on Windows x64 (--features gui-shot, scratch daemon with --seed-demo) while reviewing this stack — six surfaces, all clean: inbox with badges and the scam tag, a conversation with image attachment / edited marker / call summaries / bot embed, the thread pane, People with call states, the Discord home with voice-channel stacks, and the account menu. No rendering defects found (fonts, avatars, keycaps, brand marks all draw correctly).

Screenshots:

Happy to take more surfaces on request — this machine makes them cheap.

@itz4blitz itz4blitz left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for this. The live field, scopes and Find keys read well, and the Windows true → cmd /C exit 0 change is right. A few things before it can land.

Must fix

  1. The switcher's "Search messages" row no longer searches (regression from main). open_message_search (gui/conversation/side.rs:403-407) calls new_search, which asks with empty words, then fills the field with input.set_text(&words). set_text emits nothing (gui/input.rs:366-368: "Not an edit: nothing is emitted"), so search_typed never runs and page.words stays empty. The field shows the words, the body says "Search the cache.", and nothing is asked until the user edits the field. Call search_typed(cx) after set_text, or seed page.words before reask_search, and please add a test for the switcher path. This is still present in #57 and #58.
  2. README now conflicts with main. #59 merged, so please rebase. Keep your Search bullet, and keep #59's removal of "voice messages" from the not-built list.

Picker sweep race (chatd/src/blobs.rs:507-528, blobs/fetch.rs:260-266)

  1. Taking the idle decision under the queue lock is an improvement, but the store eviction and remove_dir_all still run after the lock is released. An ask, or a preview, that lands in that gap and reads the store before eviction gets a path that is then deleted. So the race is narrowed, not closed. Either do the eviction and removal while holding the decision, or give each account's picker a generation that asks and workers check.
  2. create_dir_all on every write re-creates media/<net>/<acct> or pick/<acct> after forget(), a cache clear, or a picker that really closed, and the bytes are downloaded and then thrown away. Re-creating only on the retry path, or under the same generation check, would avoid that.
  3. No test exercises the race; the only test change is raising the idle from 2 s to 5 s. The retry loop is bounded (for last in [false, true]), so that part is fine.

Smaller

  1. Command-F with no conversation open sets the scope to Conversation. That chip is hidden, so no chip is lit while the search runs everywhere. Resolve it to Everywhere, as the switcher's row does.
  2. Closing the pane with ✕ (close_right then take_in) leaves the page and field alive and doesn't return the keys to the composer. Esc does both.
  3. An older chatd silently ignores the new space field (no deny_unknown_fields). It answers from the whole cache while "This space" is lit. That's probably acceptable since the two ship together, but worth knowing.
  4. docs/architecture.md:40's protocol table doesn't mention space yet.

The stale-answer counter restarting per page, and "This conversation" not following a switch, are both fixed in #57, so it's fine for those to land as a stack.

@unRekable
unRekable force-pushed the feat/search-pane-scopes branch from 81ef9a9 to 89f3e40 Compare October 20, 2026 14:53

@unRekable unRekable left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thank you for the thorough review — every point is addressed on the pushed branch (rebased on main, keeping #59's README state).

Must fix

  1. The switcher's row asks again. open_message_search now goes through new_search_with(scope, &words, …), which seeds page.words before the ask — the ask carries the words from the start, and set_text (which fills the field to match) is no longer load-bearing, since it emits nothing. The seeding is its own function with a test (the_switcher_s_words_start_asked_for); a Shell-level test of the row itself would need gpui's test-support, which cargo test --workspace does not build — the pure test pins the seeding the ask reads.
  2. Rebased on main. The Search bullet kept, #59's removal of "voice messages" kept.

Picker sweep race

  1. Generations, as your second option. Each picker session carries a generation (Picker { asked, generation }); the close records the generation it ends; an ask after the close opens the next one (next_generation(closed, open), always above the highest closed). A job stamps its session's generation and is dropped before its bytes are kept when job.picker_generation <= picker_closed(account) — so a fresh ask's file lands and a closed session's never does, whichever order the sweep's removals and the landing meet in. The store eviction stays a snapshot taken at close time, which a fresh session's landing postdates.
  2. create_dir_all is scoped to picks now (Want.pick): only a picker's directory can be removed under a live download, and only a picker's is re-made — the non-pick re-creation after forget or a cache clear is gone.
  3. Tests: a_close_ends_one_picker_generation_and_an_ask_opens_the_next and the_sweep_closes_only_what_stood_idle_and_a_reopened_picker_is_never_stale pin the generation bookkeeping and the staleness decision at the unit level, and the existing picker test still pins the file-level lifecycle (closed → re-asked → lands).

Smaller

  1. Command-F with nothing open now asks everywhere, so a chip is lit.
  2. The ✕ close drops the page and field in take_in (same as Esc's branch) and close_right takes the window, returning the keys to the composer.
  3. Known and accepted, as you say — the two ship together.
  4. docs/architecture.md's table names space on SearchMessages and the new SearchNetwork row.

#57 and #58 are rebased on this branch and carry no copy of the old behaviour.

unRekable and others added 6 commits October 24, 2026 12:46
The pane opened only from the switcher's last row, its words were text, and it asked the whole cache. The head is the field now, Command-F asks what the conversation open holds, Shift-Command-F the whole cache, and the chips switch the ask between the two and the conversation's Space, falling back to everywhere with nothing open, as the switcher's row does. SearchMessages names a Space, so an older frame still reads. Find's two items come back to the Edit menu, with their keys in the shortcut list.
…ened picker's download.

A preview asked again while its picker was closing raced the removal of the directory it streams into: the download failed with a path not found, and the pane's ask went unanswered. The close now leaves a picker that re-registered, the write makes the part's directory again, and a rename that lost its directory fetches once more. The test's idle rises from 2 s to 5, so a slow or cold machine cannot overtake the last section.
… no `true`.

Every Discord test spawns a no-op child before the real chatd, and asked the Unix `true` for it: on Windows that is program not found, and the whole binary failed before any test ran. Windows takes `cmd /C exit 0`.
… and the close returns the keys.

The switcher's row seeds its words into the page's first ask - set_text is not an edit and emits nothing, so nothing depended on it anymore, and the seeding has its test. A picker's close ends its session's generation: a job of the closed session is dropped before its bytes are kept, an ask after the close opens the next generation, and only a picker's directory is re-made under a live download. The sweep and the generations carry their own tests. Command-F with nothing open asks everywhere, where a lit chip can name what was asked; the pane's close by its X goes with the keys to the composer, as Esc does; and the protocol table in docs/architecture.md knows the space filter.
@itz4blitz
itz4blitz force-pushed the feat/search-pane-scopes branch from afc49e6 to 345c650 Compare October 24, 2026 16:50
@itz4blitz
itz4blitz merged commit dabf834 into main Oct 24, 2026
@itz4blitz
itz4blitz deleted the feat/search-pane-scopes branch October 24, 2026 17:12
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