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 && (