Files
paseo/packages/app/src/plugins/composer-items/source.test.ts
T
Mohamed Boudra b597dfc023 fix(plugins): composers by reference follow the focused agent and reach retained composers
- `{ focused: true }` is the agent Command Center hands to agent items, published by its
  registration through plugins/focused-agent.ts and empty outside a workspace. The composer source
  reads that module instead of the remembered workspace selection, the navigation store and the
  layout store, which also removes the require cycles those imports added.
- A composer the app retains in a workspace the user left has no live input. `insertText` and
  `setPrompt` now store the text with `replaceDraftText`, which asks the mounted input to show it;
  before, the text reached the store but the retained input never displayed it.
- `model` and `thinking` read exactly what the picker resolves before the catalog loads (no
  models), then re-resolve when it lands, instead of reading null.
- The daemon validates a thinking option against the model the agent runs: the runtime model when
  the provider lists it, else the configured one.
- The composer sources subscribe to query-cache updates only. A query another component creates
  during its render is announced as `added`, and a subscriber that updates then made React warn
  about updating a component while rendering a different one. A catalog read no longer starts its
  load synchronously either.
2026-11-04 08:10:29 +01:00

265 lines
8.2 KiB
TypeScript

import { defineRpc } from "@getpaseo/plugin";
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import type { PluginAgentSnapshot, PluginWorkspaceSnapshot } from "@getpaseo/plugin";
import { PluginTranslationRuntime } from "@getpaseo/plugin/client/host";
import { QueryClient } from "@tanstack/react-query";
import { z } from "zod";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { queryClient } from "@/data/query-client";
import { useDraftStore } from "@/stores/draft-store";
import { useSessionStore, type Agent } from "@/stores/session-store";
import { buildPluginCommandCenterContributions } from "../command-center/contributions";
import { publishFocusedAgent } from "../focused-agent";
import type { InstalledPlugin } from "../types";
import { pluginComposersSource } from "./source";
const serverId = "host-1";
function plugin(): InstalledPlugin {
return {
id: "review",
serverId,
clientBundle: "bundle",
lifetime: new AbortController(),
locale: "en",
i18n: new PluginTranslationRuntime({ getLocale: () => "en", subscribe: () => () => {} }),
queryClient: new QueryClient(),
paseo: {} as InstalledPlugin["paseo"],
invoke: async () => undefined,
subscribe: () => {
throw new Error("Subscriptions are not used here");
},
cleanup: () => {},
settingsScreens: [],
surfaces: [],
sidebarItems: { header: [], footer: [] },
legacySidebarItems: [],
filePanels: [],
composerItems: [],
newWorkspaceItems: [],
workspacePanels: [],
commandCenterItems: [],
clientSlashCommands: [],
attachmentSources: [
{
id: "issues",
title: "Issues",
pickerTitle: "Issues",
searchPlaceholder: "Search",
icon: "Bug",
search: defineRpc({
name: "issues.search",
input: z.object({}),
output: z.array(z.object({})),
}),
},
],
themes: [],
timelineTransformers: [],
timelineRenderers: [],
};
}
function agent(id: string, archivedAt: Date | null = null): Agent {
return {
serverId,
id,
provider: "claude",
status: "idle",
turn: { phase: "idle", cancellationRequestId: null },
createdAt: new Date(),
updatedAt: new Date(),
lastUserMessageAt: null,
lastActivityAt: new Date(),
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
currentModeId: "default",
availableModes: [],
pendingPermissions: [],
persistence: null,
title: null,
cwd: "/repo",
workspaceId: "workspace-1",
model: null,
parentAgentId: null,
labels: {},
archivedAt,
};
}
beforeEach(() => {
useDraftStore.setState({ drafts: {}, textReplacementRequestByDraftKey: {} });
const store = useSessionStore.getState();
store.initializeSession(serverId, {} as never);
store.setAgents(
serverId,
new Map([
["agent-1", agent("agent-1")],
["agent-2", agent("agent-2", new Date())],
]),
);
});
afterEach(() => {
publishFocusedAgent(null);
useSessionStore.getState().clearSession(serverId);
});
describe("a plugin's composers by reference", () => {
it("names a running agent by id, and nothing for an unknown or archived one", () => {
const source = pluginComposersSource(plugin());
expect(source.resolve({ agentId: "agent-1" })).toHaveLength(1);
expect(source.resolve({ agentId: "nope" })).toEqual([]);
expect(source.resolve({ agentId: "agent-2" })).toEqual([]);
});
it("follows the agent the Command Center hands out, and is empty when there is none", () => {
const source = pluginComposersSource(plugin());
let notified = 0;
const stop = source.subscribe(() => {
notified += 1;
});
expect(source.resolve({ focused: true })).toEqual([]);
publishFocusedAgent({ serverId, agentId: "agent-1" });
const [focused] = source.resolve({ focused: true });
expect(focused?.context).toBe("agent");
expect(notified).toBeGreaterThan(0);
// Leaving the workspace for a plugin screen or New workspace publishes no agent.
publishFocusedAgent(null);
expect(source.resolve({ focused: true })).toEqual([]);
// Another host's focused agent is not this plugin's.
publishFocusedAgent({ serverId: "host-2", agentId: "agent-1" });
expect(source.resolve({ focused: true })).toEqual([]);
stop();
});
it("is not told when another component creates a query while it renders", () => {
const source = pluginComposersSource(plugin());
let notified = 0;
const stop = source.subscribe(() => {
notified += 1;
});
// `useQuery` does this during a render, and a subscriber that updates then makes React warn
// that one component updated another while rendering.
queryClient.getQueryCache().build(queryClient, { queryKey: ["created-during-a-render"] });
expect(notified).toBe(0);
stop();
});
it("returns the same array until a composer changes", () => {
const source = pluginComposersSource(plugin());
const first = source.resolve({ agentId: "agent-1" });
expect(source.resolve({ agentId: "agent-1" })).toBe(first);
first[0]?.insertText("hello");
const next = source.resolve({ agentId: "agent-1" });
expect(next).not.toBe(first);
expect(next[0]?.prompt).toBe("hello");
});
});
describe("a Command Center item reaching an agent's composer by id", () => {
const snapshot: PluginAgentSnapshot = {
id: "agent-1",
workspaceId: "workspace-1",
provider: "claude",
status: "idle",
createdAt: "2026-08-16T10:00:00.000Z",
updatedAt: "2026-08-16T10:00:00.000Z",
lastActivityAt: "2026-08-16T10:00:00.000Z",
title: null,
cwd: "/repo",
model: null,
currentModeId: null,
thinkingOptionId: null,
requiresAttention: false,
attentionReason: null,
parentAgentId: null,
labels: {},
metadata: {},
};
const workspace: PluginWorkspaceSnapshot = {
id: "workspace-1",
projectId: "project-1",
projectDisplayName: "Repo",
projectRootPath: "/repo",
directory: "/repo",
projectKind: "git",
kind: "checkout",
name: "Repo",
title: null,
status: "running",
statusEnteredAt: null,
archivingAt: null,
diffStat: null,
};
function markForReview(rejection?: Error) {
const patches: unknown[] = [];
const client = {
async setAgentMetadata(_agentId: string, patch: unknown) {
patches.push(patch);
if (rejection) throw rejection;
},
} as unknown as DaemonClient;
useSessionStore.getState().clearSession(serverId);
useSessionStore.getState().initializeSession(serverId, client);
useSessionStore.getState().setAgents(serverId, new Map([["agent-1", agent("agent-1")]]));
const installed = plugin();
installed.commandCenterItems = [
{
id: "mark",
title: "Mark for review",
icon: "Flag",
context: "agent",
async onSelect({ agent: handed, composers }) {
for (const composer of composers({ agentId: handed.id })) {
await composer.setMetadata({ needsReview: true });
}
},
},
];
const reported: unknown[] = [];
const [item] = buildPluginCommandCenterContributions({
plugins: [installed],
state: {
subscribe: () => () => {},
getWorkspace: () => workspace,
getAgent: () => snapshot,
},
workspaceId: "workspace-1",
agentId: "agent-1",
navigation: {
openSettings() {},
openSurface() {},
openWorkspacePanel() {},
openAgentPanel() {},
},
reportError: (error) => reported.push(error),
});
return { run: item?.run, patches, reported };
}
it("changes the agent's metadata through the daemon", async () => {
const { run, patches, reported } = markForReview();
await run?.();
expect(patches).toEqual([{ needsReview: true }]);
expect(reported).toEqual([]);
});
it("reports what the daemon rejected", async () => {
const { run, reported } = markForReview(new Error("metadata refused"));
await run?.();
expect(reported).toEqual([new Error("metadata refused")]);
});
});