Compare commits

...

1 Commits

Author SHA1 Message Date
Jiang Bohan
f25eb19722 fix(agents): invalidate runtimes cache on daemon events
The Agents page never received runtime cache updates when daemons
registered or deregistered, causing the Create Agent dialog to show
"No runtime available" even when runtimes existed. This happened because
daemon events were only handled by the Runtimes page component, not
globally.

- Add daemon:register to the centralized realtime sync refresh map
- Skip daemon:heartbeat in the generic handler to avoid excessive refetches
- Invalidate runtimes on WS reconnect alongside other workspace data
- Show a loading indicator in the Create Agent dialog while runtimes load
2026-04-10 14:43:27 +08:00
3 changed files with 19 additions and 5 deletions

View File

@@ -9,6 +9,7 @@ import type { WorkspaceStore } from "../workspace/store";
import { createLogger } from "../logger";
import { issueKeys } from "../issues/queries";
import { projectKeys } from "../projects/queries";
import { runtimeKeys } from "../runtimes/queries";
import {
onIssueCreated,
onIssueUpdated,
@@ -54,7 +55,8 @@ export interface RealtimeSyncStores {
*
* Per-issue events (comments, activity, reactions, subscribers) are handled
* both here (invalidation fallback) and by per-page useWSEvent hooks (granular
* updates). Daemon events are handled by individual components only.
* updates). Daemon register events invalidate runtimes globally; heartbeats
* are skipped to avoid excessive refetches.
*
* @param ws - WebSocket client instance (null when not yet connected)
* @param stores - Platform-created Zustand store instances for auth and workspace
@@ -95,6 +97,10 @@ export function useRealtimeSync(
const wsId = workspaceStore.getState().workspace?.id;
if (wsId) qc.invalidateQueries({ queryKey: projectKeys.all(wsId) });
},
daemon: () => {
const wsId = workspaceStore.getState().workspace?.id;
if (wsId) qc.invalidateQueries({ queryKey: runtimeKeys.all(wsId) });
},
};
const timers = new Map<string, ReturnType<typeof setTimeout>>();
@@ -118,6 +124,7 @@ export function useRealtimeSync(
"reaction:added", "reaction:removed",
"issue_reaction:added", "issue_reaction:removed",
"subscriber:added", "subscriber:removed",
"daemon:heartbeat",
]);
const unsubAny = ws.onAny((msg) => {
@@ -300,6 +307,7 @@ export function useRealtimeSync(
qc.invalidateQueries({ queryKey: workspaceKeys.members(wsId) });
qc.invalidateQueries({ queryKey: workspaceKeys.skills(wsId) });
qc.invalidateQueries({ queryKey: projectKeys.all(wsId) });
qc.invalidateQueries({ queryKey: runtimeKeys.all(wsId) });
}
qc.invalidateQueries({ queryKey: workspaceKeys.list() });
} catch (e) {

View File

@@ -30,7 +30,7 @@ export function AgentsPage() {
const [selectedId, setSelectedId] = useState<string>("");
const [showArchived, setShowArchived] = useState(false);
const [showCreate, setShowCreate] = useState(false);
const { data: runtimes = [] } = useQuery(runtimeListOptions(wsId));
const { data: runtimes = [], isLoading: runtimesLoading } = useQuery(runtimeListOptions(wsId));
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
id: "multica_agents_layout",
});
@@ -224,6 +224,7 @@ export function AgentsPage() {
{showCreate && (
<CreateAgentDialog
runtimes={runtimes}
runtimesLoading={runtimesLoading}
onClose={() => setShowCreate(false)}
onCreate={handleCreate}
/>

View File

@@ -7,6 +7,7 @@ import {
ChevronDown,
Globe,
Lock,
Loader2,
} from "lucide-react";
import type {
AgentVisibility,
@@ -33,10 +34,12 @@ import { toast } from "sonner";
export function CreateAgentDialog({
runtimes,
runtimesLoading,
onClose,
onCreate,
}: {
runtimes: RuntimeDevice[];
runtimesLoading?: boolean;
onClose: () => void;
onCreate: (data: CreateAgentRequest) => Promise<void>;
}) {
@@ -147,10 +150,12 @@ export function CreateAgentDialog({
<Label className="text-xs text-muted-foreground">Runtime</Label>
<Popover open={runtimeOpen} onOpenChange={setRuntimeOpen}>
<PopoverTrigger
disabled={runtimes.length === 0}
disabled={runtimes.length === 0 && !runtimesLoading}
className="flex w-full items-center gap-3 rounded-lg border border-border bg-background px-3 py-2.5 mt-1.5 text-left text-sm transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50"
>
{selectedRuntime?.runtime_mode === "cloud" ? (
{runtimesLoading ? (
<Loader2 className="h-4 w-4 shrink-0 animate-spin text-muted-foreground" />
) : selectedRuntime?.runtime_mode === "cloud" ? (
<Cloud className="h-4 w-4 shrink-0 text-muted-foreground" />
) : (
<Monitor className="h-4 w-4 shrink-0 text-muted-foreground" />
@@ -158,7 +163,7 @@ export function CreateAgentDialog({
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="truncate font-medium">
{selectedRuntime?.name ?? "No runtime available"}
{runtimesLoading ? "Loading runtimes..." : (selectedRuntime?.name ?? "No runtime available")}
</span>
{selectedRuntime?.runtime_mode === "cloud" && (
<span className="shrink-0 rounded bg-info/10 px-1.5 py-0.5 text-xs font-medium text-info">