Files
paseo/packages/app/src/plugins/command-center/registration.tsx
T
Mohamed Boudra 58bb1c5667 test(plugins): a Command Center agent item reaches the composer through the real client state
The command-path test now reads workspace and agent through
createPluginClientStateSource over seeded session stores, so an empty
lookup, which makes the Command Center return before calling the item,
would fail it. Also records at the focused-agent publisher that it
publishes in an effect.
2026-11-05 23:25:42 +01:00

82 lines
3.7 KiB
TypeScript

import { usePathname } from "expo-router";
import { useEffect, useMemo } from "react";
import { useCommandCenterActions } from "@/command-center/provider";
import { useToast } from "@/contexts/toast-context";
import { useHostRuntimeClient } from "@/runtime/host-runtime";
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
import { useSessionStore } from "@/stores/session-store";
import { useWorkspaceExists } from "@/stores/session-store-hooks";
import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
import { normalizeWorkspaceOpaqueId } from "@/utils/workspace-identity";
import { createPluginClientStateSource } from "../client-state/source";
import { publishFocusedAgent } from "../focused-agent";
import { hostIdFromPathname } from "../routes";
import { useInstalledPlugins } from "../registry";
import { buildPluginCommandCenterContributions } from "./contributions";
import { getFocusedAgentId } from "./context";
import { createPluginNavigation } from "../navigation";
export function PluginCommandCenterActions() {
const pathname = usePathname();
const selection = useActiveWorkspaceSelection();
const serverId = selection?.serverId ?? hostIdFromPathname(pathname);
const workspaceId = selection?.workspaceId ?? null;
const workspaceExists = useWorkspaceExists(serverId, workspaceId);
const workspaceKey = serverId && workspaceId ? `${serverId}:${workspaceId}` : null;
const layout = useWorkspaceLayoutStore((state) =>
workspaceKey ? (state.layoutByWorkspace[workspaceKey] ?? null) : null,
);
const focusedAgentId = getFocusedAgentId(layout);
const agentExists = useSessionStore((state) => {
if (!serverId || !focusedAgentId) return null;
const session = state.sessions[serverId];
const agent =
session?.agents.get(focusedAgentId) ?? session?.agentDetails.get(focusedAgentId) ?? null;
return (
Boolean(agent) &&
Boolean(workspaceId) &&
normalizeWorkspaceOpaqueId(agent?.workspaceId) === normalizeWorkspaceOpaqueId(workspaceId)
);
});
const handedAgentId = agentExists ? focusedAgentId : null;
// The agent a plugin's `{ focused: true }` composer names is the one handed to agent items.
// Known limit: this publishes in an effect, so a command that runs on the first commit after a
// route change can still see the previous route's agent. Publishing from a shared
// navigation/focus context would remove the gap.
useEffect(() => {
publishFocusedAgent(serverId && handedAgentId ? { serverId, agentId: handedAgentId } : null);
return () => publishFocusedAgent(null);
}, [handedAgentId, serverId]);
const client = useHostRuntimeClient(serverId ?? "");
const installed = useInstalledPlugins();
const plugins = useMemo(
() => installed.filter((plugin) => plugin.serverId === serverId),
[installed, serverId],
);
const stateSource = useMemo(
() => (serverId ? createPluginClientStateSource(serverId) : null),
[serverId],
);
const toast = useToast();
const actions = useMemo(() => {
if (!client || !serverId || !stateSource) return [];
return buildPluginCommandCenterContributions({
plugins,
state: stateSource,
workspaceId: workspaceExists ? workspaceId : null,
agentId: handedAgentId,
navigation: createPluginNavigation({ serverId, workspaceId }),
reportError(error) {
toast.error(error instanceof Error ? error.message : String(error));
},
});
}, [client, handedAgentId, plugins, serverId, stateSource, toast, workspaceExists, workspaceId]);
useCommandCenterActions({
sourceId: serverId ? `plugins:${serverId}` : "plugins",
enabled: Boolean(client && plugins.length > 0),
actions,
});
return null;
}