perf(desktop): stop beachballs on agents menu and thread open#1402
Merged
Conversation
…thread These Tauri commands were synchronous (`pub fn`), so in Tauri v2 they ran on the main UI thread. Their blocking work — disk reads/writes of agent, persona, and team records plus per-process liveness syscalls — froze the UI (beachball) on the agents-menu mount and after every start/stop/edit, which invalidates and refetches `list_managed_agents`. Convert them to `async fn` and move each body into `tokio::task::spawn_blocking` so the blocking work runs on Tauri's worker pool, matching the pattern already used by `deploy_to_provider` and `probe_backend_provider`. State is re-derived from the owned `AppHandle` inside the closure because `State<'_, _>` is borrowed and `std::sync::MutexGuard` is not `Send`; the existing `managed_agents_store_lock` still serializes the on-disk store, so consistency is unchanged. Converted: list_managed_agents, stop_managed_agent, delete_managed_agent; list_personas, create_persona, update_persona, delete_persona, set_persona_active; list_teams, create_team, update_team, delete_team, install_team_from_directory, sync_team_directory. Left synchronous on purpose: reconcile_inbound_persona_event and put_agent_session_config (background per-relay-event paths), parse_team_file / parse_persona_files (pure CPU, rare import), and the trivial log/settings reads. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Move thread panel open/close state changes out of the discrete click task and into a transitioned timer. This removes the explicit flushSync path that forced the channel timeline width reflow to happen synchronously while opening a thread. Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
The async conversions pushed agents.rs to 1464 lines, over its file-size guard ceiling (1437). Rather than bump the limit, extract the two self-contained backend-provider commands (`discover_backend_providers`, `probe_backend_provider`) into `commands/agent_providers.rs`, matching the existing `agent_discovery`/`agent_models`/`agent_config` split pattern. This brings agents.rs to 1420 lines (under the ceiling) with no logic change — the commands are glob-re-exported through `commands::*`, so the Tauri invoke-handler registration is unchanged. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Users still hit macOS beachballs after the #1338 scroll/perf work. #1338 fixed the cold-mount cost of building hundreds of message rows (a synchronous markdown-parse longtask). But the remaining reports — a 2-message thread ("size doesn't matter") and the agents menu / start-stop / edit — are a different, count-independent class of main-thread block. This PR fixes both.
Investigation writeup:
RESEARCH/BEACHBALL_SECOND_PASS_2026_06_30.md(in the Buzz workspace).Two root causes, two fixes
A) Agents menu / start / stop / edit — blocking Tauri commands on the main thread
In Tauri v2, a synchronous
#[tauri::command] pub fnruns on the main UI thread; onlyasync fncommands get a worker thread. The agent/persona/team commands were sync and did blocking disk I/O + per-process liveness syscalls there.list_managed_agents(agents-menu mount, and refetched after every start/stop/edit) reads agent + persona records from disk, syncs every child process, and builds a per-agent summary — a multi-hundred-ms freeze.Fix: convert 14 of them to
async fn+tokio::task::spawn_blocking, matching the existing pattern (deploy_to_provider,probe_backend_provider). State is re-derived from the ownedAppHandleinside the closure (State<'_,_>is borrowed andstd::sync::MutexGuardis!Send). Themanaged_agents_store_lockstill serializes the on-disk store, so consistency is unchanged. IPC-transparent: the frontend alreadyawaits these; only the Tauri-injectedstateparam was dropped (never sent from JS).Converted:
list_managed_agents,stop_managed_agent,delete_managed_agent;list_personas,create_persona,update_persona,delete_persona,set_persona_active;list_teams,create_team,update_team,delete_team,install_team_from_directory,sync_team_directory.Left sync on purpose:
reconcile_inbound_persona_event+put_agent_session_config(background per-relay-event paths — async-per-event adds overhead/ordering risk for no UX win),parse_team_file/parse_persona_files(pure CPU, rare import), and the trivial log/settings reads.B) Opening a thread forced the whole channel reflow into the click task
Opening a thread doesn't unmount the channel timeline; the timeline (up to 2000 rows) and the thread panel are siblings in one flex-row, so opening the panel shrinks the timeline's width and reflows it — a cost that scales with the channel behind the panel, not the 2-message thread.
handleOpenThreadwrapped the optimistic open influshSync, which forced that whole reflow to run synchronously inside the discrete click task, blocking paint.Fix: remove the
flushSyncfrom the thread open/close path and move the panel-state batch behind a yieldedsetTimeout(0)+React.startTransition, so the expensive mount + width reflow no longer blocks the click. (desktop/src/features/channels/useChannelPaneHandlers.ts)Validation
just desktop-tauri-check✅, rustfmt ✅, 850 tauri unit tests pass ✅. (Localdesktop-tauri-clippyshows 6 errors that all pre-exist on cleanmainin untouched files; this diff adds none. Tauri clippy is not a CI gate.)pnpm typecheck✅,pnpm build✅,pnpm check✅, plus smoke e2e:messaging.spec.ts"opens a single-level thread panel" ✅ andnavigation.spec.ts"thread panels" ✅.Authorship
Two commits, both DCO-clean (authored Wes, signed-off Wes): cause A co-authored Brain, cause B co-authored Pinky.