mirror of
https://github.com/block/buzz.git
synced 2026-11-07 01:41:06 +00:00
fix(desktop): keep member runtime status off the UI thread (#6445)
## Summary - skip managed-agent runtime discovery when the members sidebar has no local managed bots - run runtime listing disk, process, and mutex work on Tauri’s blocking pool - preserve local managed-bot status and Start/Stop behavior with positive and negative E2E coverage Opening Add people in a human-only channel could invoke synchronous native runtime discovery before the sidebar painted, leaving the macOS app beachballed. Human invites do not depend on that data. ## Why I have seen slowness opening this dialog in the UI https://github.com/user-attachments/assets/1955eb5e-ee47-4edf-8e5c-606d11ffbc25 ### Related issue Related overlap: #4851 is a broader managed-agent lifecycle change that includes a similar native offload. This draft is intentionally limited to the sidebar critical path and adds the human-only query gate. ### Testing I have verified the pause in the video goes away after this change. - `just ci` - `just desktop-check` - `just desktop-test` (5,241 passed) - `just desktop-tauri-fmt-check` - `just desktop-tauri-clippy` - `just desktop-tauri-test` (2,702 passed; 18 ignored) - focused Playwright: human-only sidebar skips runtime discovery - focused Playwright: local managed bot retains status and Stop/Start controls No visual styling changed, so screenshots are not applicable. Signed-off-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
@@ -137,86 +137,91 @@ pub fn put_managed_agent_runtime_lifecycle(
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
// Keep disk, process, and mutex work off the main thread so opening members cannot stall the UI.
|
||||
#[tauri::command]
|
||||
pub fn list_managed_agent_runtimes(
|
||||
pub async fn list_managed_agent_runtimes(
|
||||
app: AppHandle,
|
||||
) -> Result<Vec<ManagedAgentRuntimeStatus>, String> {
|
||||
// This command is polled whenever the members sidebar opens and refetched
|
||||
// on every status event — load the per-row status inputs once, outside
|
||||
// the locks, instead of hitting disk per row while holding them.
|
||||
let personas = load_personas(&app).unwrap_or_default();
|
||||
let global = load_global_agent_config(&app).unwrap_or_default();
|
||||
let state = app.state::<AppState>();
|
||||
let _transition = state
|
||||
.managed_agent_runtime_transition
|
||||
.lock()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let _store = state
|
||||
.managed_agents_store_lock
|
||||
.lock()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut records = load_managed_agents(&app)?;
|
||||
let mut runtimes = state
|
||||
.managed_agent_processes
|
||||
.lock()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let exited_keys: Vec<_> = runtimes
|
||||
.iter_mut()
|
||||
.filter_map(|(key, runtime)| match runtime.child.try_wait() {
|
||||
Ok(Some(_)) | Err(_) => Some(key.clone()),
|
||||
Ok(None) => None,
|
||||
})
|
||||
.collect();
|
||||
let records_changed = !exited_keys.is_empty();
|
||||
let mut statuses = Vec::new();
|
||||
for key in exited_keys {
|
||||
runtimes.remove(&key);
|
||||
super::remove_agent_runtime_receipt(&app, &key);
|
||||
state.clear_agent_session_cache(&key);
|
||||
if let Some(record) = records
|
||||
tokio::task::spawn_blocking(move || {
|
||||
// This command is polled whenever the members sidebar opens and refetched
|
||||
// on every status event — load the per-row status inputs once, outside
|
||||
// the locks, instead of hitting disk per row while holding them.
|
||||
let personas = load_personas(&app).unwrap_or_default();
|
||||
let global = load_global_agent_config(&app).unwrap_or_default();
|
||||
let state = app.state::<AppState>();
|
||||
let _transition = state
|
||||
.managed_agent_runtime_transition
|
||||
.lock()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let _store = state
|
||||
.managed_agents_store_lock
|
||||
.lock()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut records = load_managed_agents(&app)?;
|
||||
let mut runtimes = state
|
||||
.managed_agent_processes
|
||||
.lock()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let exited_keys: Vec<_> = runtimes
|
||||
.iter_mut()
|
||||
.find(|record| record.pubkey.eq_ignore_ascii_case(&key.pubkey))
|
||||
{
|
||||
record.updated_at = crate::util::now_iso();
|
||||
record.last_stopped_at = Some(record.updated_at.clone());
|
||||
let status = status_for_with(
|
||||
.filter_map(|(key, runtime)| match runtime.child.try_wait() {
|
||||
Ok(Some(_)) | Err(_) => Some(key.clone()),
|
||||
Ok(None) => None,
|
||||
})
|
||||
.collect();
|
||||
let records_changed = !exited_keys.is_empty();
|
||||
let mut statuses = Vec::new();
|
||||
for key in exited_keys {
|
||||
runtimes.remove(&key);
|
||||
super::remove_agent_runtime_receipt(&app, &key);
|
||||
state.clear_agent_session_cache(&key);
|
||||
if let Some(record) = records
|
||||
.iter_mut()
|
||||
.find(|record| record.pubkey.eq_ignore_ascii_case(&key.pubkey))
|
||||
{
|
||||
record.updated_at = crate::util::now_iso();
|
||||
record.last_stopped_at = Some(record.updated_at.clone());
|
||||
let status = status_for_with(
|
||||
&app,
|
||||
record,
|
||||
&key,
|
||||
None,
|
||||
None,
|
||||
StatusInputs {
|
||||
personas: &personas,
|
||||
global: &global,
|
||||
},
|
||||
);
|
||||
emit_status(&app, &status);
|
||||
statuses.push(status);
|
||||
}
|
||||
}
|
||||
statuses.extend(runtimes.iter().filter_map(|(key, runtime)| {
|
||||
let record = records
|
||||
.iter()
|
||||
.find(|record| record.pubkey.eq_ignore_ascii_case(&key.pubkey))?;
|
||||
Some(status_for_with(
|
||||
&app,
|
||||
record,
|
||||
&key,
|
||||
None,
|
||||
key,
|
||||
Some(runtime),
|
||||
None,
|
||||
StatusInputs {
|
||||
personas: &personas,
|
||||
global: &global,
|
||||
},
|
||||
);
|
||||
emit_status(&app, &status);
|
||||
statuses.push(status);
|
||||
))
|
||||
}));
|
||||
drop(runtimes);
|
||||
// Records are only mutated above when a runtime exited — skip the store
|
||||
// rewrite on the common nothing-changed poll.
|
||||
if records_changed {
|
||||
save_managed_agents(&app, &records)?;
|
||||
}
|
||||
}
|
||||
statuses.extend(runtimes.iter().filter_map(|(key, runtime)| {
|
||||
let record = records
|
||||
.iter()
|
||||
.find(|record| record.pubkey.eq_ignore_ascii_case(&key.pubkey))?;
|
||||
Some(status_for_with(
|
||||
&app,
|
||||
record,
|
||||
key,
|
||||
Some(runtime),
|
||||
None,
|
||||
StatusInputs {
|
||||
personas: &personas,
|
||||
global: &global,
|
||||
},
|
||||
))
|
||||
}));
|
||||
drop(runtimes);
|
||||
// Records are only mutated above when a runtime exited — skip the store
|
||||
// rewrite on the common nothing-changed poll.
|
||||
if records_changed {
|
||||
save_managed_agents(&app, &records)?;
|
||||
}
|
||||
Ok(statuses)
|
||||
Ok(statuses)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("spawn_blocking failed: {e}"))?
|
||||
}
|
||||
|
||||
pub(crate) fn start_managed_agent_runtime_pair_lazy(
|
||||
@@ -572,6 +577,18 @@ pub async fn reconcile_managed_agent_runtimes(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn list_managed_agent_runtimes_returns_a_future() {
|
||||
fn assert_async_command<F, Fut>(_command: F)
|
||||
where
|
||||
F: Fn(AppHandle) -> Fut,
|
||||
Fut: std::future::Future<Output = Result<Vec<ManagedAgentRuntimeStatus>, String>>,
|
||||
{
|
||||
}
|
||||
|
||||
assert_async_command(list_managed_agent_runtimes);
|
||||
}
|
||||
|
||||
fn payload(
|
||||
relay_url: &str,
|
||||
lifecycle: ManagedAgentRuntimeLifecycle,
|
||||
|
||||
@@ -141,9 +141,6 @@ export function MembersSidebar({
|
||||
relayUrl,
|
||||
}: MembersSidebarProps) {
|
||||
const channelId = channel?.id ?? null;
|
||||
const managedAgentRuntimesQuery = useManagedAgentRuntimesQuery({
|
||||
enabled: open,
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
||||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
@@ -470,6 +467,18 @@ export function MembersSidebar({
|
||||
),
|
||||
[managedAgentsQuery.data],
|
||||
);
|
||||
const hasLocalManagedMember = React.useMemo(
|
||||
() =>
|
||||
[...bots, ...archived].some(
|
||||
(member) =>
|
||||
managedAgentByPubkey.get(normalizePubkey(member.pubkey))?.backend
|
||||
.type === "local",
|
||||
),
|
||||
[archived, bots, managedAgentByPubkey],
|
||||
);
|
||||
const managedAgentRuntimesQuery = useManagedAgentRuntimesQuery({
|
||||
enabled: open && Boolean(relayUrl) && hasLocalManagedMember,
|
||||
});
|
||||
const controllableManagedBots = React.useMemo(
|
||||
() =>
|
||||
bots.flatMap((member) => {
|
||||
|
||||
@@ -4017,6 +4017,25 @@ test("members sidebar virtualizes large channel rosters", async ({ page }) => {
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("opening a human-only members sidebar skips managed runtime discovery", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
const baselineCommands = await readCommandLog(page);
|
||||
const baselineRuntimeListCount = commandCount(
|
||||
baselineCommands,
|
||||
"list_managed_agent_runtimes",
|
||||
);
|
||||
|
||||
await openMembersSidebar(page, "random");
|
||||
await expect(page.getByTestId("members-sidebar-people")).toBeVisible();
|
||||
|
||||
const commands = await readCommandLog(page);
|
||||
expect(commandCount(commands, "list_managed_agent_runtimes")).toBe(
|
||||
baselineRuntimeListCount,
|
||||
);
|
||||
});
|
||||
|
||||
test("members sidebar can invite relay-authorized agents", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
relayAgents: [
|
||||
@@ -4528,8 +4547,17 @@ test("members sidebar can stop and start a managed bot in this community", async
|
||||
baselineCommands,
|
||||
"stop_managed_agent",
|
||||
);
|
||||
const baselineRuntimeListCount = commandCount(
|
||||
baselineCommands,
|
||||
"list_managed_agent_runtimes",
|
||||
);
|
||||
|
||||
await openMembersSidebar(page, "general");
|
||||
await expect
|
||||
.poll(async () =>
|
||||
commandCount(await readCommandLog(page), "list_managed_agent_runtimes"),
|
||||
)
|
||||
.toBe(baselineRuntimeListCount + 1);
|
||||
|
||||
const agentStatus = page.getByTestId(
|
||||
`sidebar-managed-agent-status-${agentPubkey}`,
|
||||
|
||||
Reference in New Issue
Block a user