From 1144b6023ca6f164cb1efe952e39336d896037c2 Mon Sep 17 00:00:00 2001 From: MiniMax M3 Date: Mon, 1 Jun 2026 09:52:37 +0800 Subject: [PATCH] fix(agents): address code review on runtime machine filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Plumb localDaemonId / localMachineName / hasLocalMachine / currentUserId through AgentsPage → buildRuntimeMachines so the Local section and device-name consolidation match the Runtimes page on both web and Desktop. Adds a DesktopAgentsPage wrapper that bridges daemonAPI the same way DesktopRuntimesPage does. - Make the 'All runtimes' badge use the in-scope total instead of summing per-machine counts, so an agent bound to a GC'd runtime doesn't silently vanish from the count. - Move Date.now() out of the machines useMemo into a useState lazy init so the snapshot stays stable per mount. - Drop unused i18n keys (all_description / this_machine / reset) from runtime_filter in en / zh-Hans / ko. - Add a regression test for the All-runtimes badge divergence. Co-authored-by: multica-agent --- .../src/components/desktop-agents-page.tsx | 54 ++++++++++++++++ apps/desktop/src/renderer/src/routes.tsx | 4 +- .../(dashboard)/agents/page.tsx | 13 +++- .../views/agents/components/agents-page.tsx | 63 ++++++++++++++++--- .../runtime-machine-filter-dropdown.test.tsx | 29 +++++++++ .../runtime-machine-filter-dropdown.tsx | 14 ++--- packages/views/locales/en/agents.json | 5 +- packages/views/locales/ko/agents.json | 5 +- packages/views/locales/zh-Hans/agents.json | 5 +- 9 files changed, 161 insertions(+), 31 deletions(-) create mode 100644 apps/desktop/src/renderer/src/components/desktop-agents-page.tsx diff --git a/apps/desktop/src/renderer/src/components/desktop-agents-page.tsx b/apps/desktop/src/renderer/src/components/desktop-agents-page.tsx new file mode 100644 index 000000000..72513ff76 --- /dev/null +++ b/apps/desktop/src/renderer/src/components/desktop-agents-page.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from "react"; +import { AgentsPage } from "@multica/views/agents"; +import type { DaemonStatus } from "../../../shared/daemon-types"; + +/** + * Desktop wrapper around the shared `AgentsPage`. Bridges the Electron + * `daemonAPI` (main-process daemon state) into the page so the runtime + * machine filter can render the Local section the same way the Runtimes + * page does — without these props the page falls back to grouping + * every local-mode runtime under "Remote" with a generic title, which + * breaks the "drill from a machine into its agents" promise of the + * filter. + * + * Mirrors `DesktopRuntimesPage`: we cache the last seen daemon + * identity so the Local row doesn't get reclassified as Remote when + * the daemon is stopped (which would null out `status.daemonId`), and + * we fall back to the OS hostname so the section label stays useful + * even when the app doesn't manage the running daemon (WSL2 etc.). + */ +export function DesktopAgentsPage() { + const [status, setStatus] = useState({ state: "stopped" }); + const [lastIdentity, setLastIdentity] = useState<{ + daemonId: string | null; + deviceName: string | null; + }>({ daemonId: null, deviceName: null }); + const [hostName, setHostName] = useState(null); + + useEffect(() => { + const apply = (s: DaemonStatus) => { + setStatus(s); + if (s.daemonId) { + setLastIdentity({ + daemonId: s.daemonId, + deviceName: s.deviceName ?? null, + }); + } + }; + window.daemonAPI.getStatus().then(apply); + window.daemonAPI.getHostName().then((name) => setHostName(name || null)); + return window.daemonAPI.onStatusChange(apply); + }, []); + + return ( + + ); +} diff --git a/apps/desktop/src/renderer/src/routes.tsx b/apps/desktop/src/renderer/src/routes.tsx index 46cef3af9..e8130b6b5 100644 --- a/apps/desktop/src/renderer/src/routes.tsx +++ b/apps/desktop/src/renderer/src/routes.tsx @@ -21,7 +21,7 @@ import { AutopilotsPage } from "@multica/views/autopilots/components"; import { MyIssuesPage } from "@multica/views/my-issues"; import { SkillsPage } from "@multica/views/skills"; import { DesktopRuntimesPage } from "./components/desktop-runtimes-page"; -import { AgentsPage } from "@multica/views/agents"; +import { DesktopAgentsPage } from "./components/desktop-agents-page"; import { SquadsPage, SquadDetailPage as SquadDetailPageView } from "@multica/views/squads/components"; import { InboxPage } from "@multica/views/inbox"; import { SettingsPage } from "@multica/views/settings"; @@ -171,7 +171,7 @@ export const appRoutes: RouteObject[] = [ element: , handle: { title: "Skill" }, }, - { path: "agents", element: , handle: { title: "Agents" } }, + { path: "agents", element: , handle: { title: "Agents" } }, { path: "agents/:id", element: , diff --git a/apps/web/app/[workspaceSlug]/(dashboard)/agents/page.tsx b/apps/web/app/[workspaceSlug]/(dashboard)/agents/page.tsx index 336d8549d..243efc5e8 100644 --- a/apps/web/app/[workspaceSlug]/(dashboard)/agents/page.tsx +++ b/apps/web/app/[workspaceSlug]/(dashboard)/agents/page.tsx @@ -1 +1,12 @@ -export { AgentsPage as default } from "@multica/views/agents"; +import { AgentsPage } from "@multica/views/agents"; + +// Web has no bundled daemon, so the runtime filter always groups +// local-mode runtimes under "Remote" (buildRuntimeMachines has no +// localDaemonId / localMachineName / ensureLocalMachine context +// here) — that's the expected web behavior, not a bug. The Desktop +// app wires those props through `DesktopAgentsPage` so the local +// section appears in the dropdown the same way it does on the +// Runtimes page. +export default function AgentsRoute() { + return ; +} diff --git a/packages/views/agents/components/agents-page.tsx b/packages/views/agents/components/agents-page.tsx index 2548d1b0b..c77554a84 100644 --- a/packages/views/agents/components/agents-page.tsx +++ b/packages/views/agents/components/agents-page.tsx @@ -83,7 +83,36 @@ const SORT_LABEL_KEY: Record Date.now()); const machines = useMemo( - () => buildRuntimeMachines(runtimes, { now: Date.now() }), - [runtimes], + () => + buildRuntimeMachines(runtimes, { + now: machinesNow, + localDaemonId, + localMachineName, + currentUserId: currentUser?.id ?? null, + ensureLocalMachine: hasLocalMachine, + }), + [runtimes, machinesNow, localDaemonId, localMachineName, currentUser?.id, hasLocalMachine], ); // Reverse map: runtime_id → machine id. Lets the filter step look up @@ -227,8 +268,13 @@ export function AgentsPage() { // Per-machine agent counts in `inScope` — used both for the chip // badges in the dropdown AND to make the runtime filter respect the // current scope (e.g. "Mine" only shows machines that have one of - // my agents). Computed against `inScope` (not `visibleInView`) so the - // number next to "All" is exactly `inScope.length`. + // my agents). Computed against `inScope` (not `visibleInView`). + // Agents whose runtime doesn't map to a current machine + // (e.g. bound to a GC'd runtime) are intentionally skipped here + // — they still appear in the list when the filter is "All + // runtimes", just not bucketed under any per-machine chip. The + // "All runtimes" badge uses `inScope.length` directly so it stays + // consistent with the unfiltered list. const agentCountByMachine = useMemo(() => { const counts = new Map(); for (const a of inScope) { @@ -711,6 +757,7 @@ function ActiveToolbarRow({ value={runtimeMachineId} onChange={onRuntimeMachineChange} agentCountByMachine={agentCountByMachine} + totalAgentCount={totalCount} /> {archivedCount > 0 && (