Files
paseo/packages/app/src/plugins/composer-items/source.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

82 lines
3.3 KiB
TypeScript

import type { PluginComposer, PluginComposerSelector } from "@getpaseo/plugin/client";
import type { PluginComposersSource } from "@getpaseo/plugin/client/host";
import { agentComposer } from "@/composer/agent-composer";
import type { ComposerState } from "@/composer/model";
import { queryClient } from "@/data/query-client";
import { useDraftStore } from "@/stores/draft-store";
import { useSessionStore } from "@/stores/session-store";
import { getFocusedAgent, subscribeFocusedAgent } from "../focused-agent";
import type { InstalledPlugin } from "../types";
import { toPluginComposer } from "./model";
const NO_COMPOSERS: readonly PluginComposer[] = Object.freeze([]);
const sources = new WeakMap<InstalledPlugin, PluginComposersSource>();
/**
* The composers a plugin reaches by reference: one per running agent, open in a tab or not. The
* same source serves `useComposers` in every surface and `composers()` in every command.
*/
export function pluginComposersSource(plugin: InstalledPlugin): PluginComposersSource {
let source = sources.get(plugin);
if (!source) {
source = createSource(plugin);
sources.set(plugin, source);
}
return source;
}
function createSource(plugin: InstalledPlugin): PluginComposersSource {
const { serverId } = plugin;
// Handles and result arrays are reused while nothing they read has changed, so a subscriber
// that re-reads on every notification renders only when its composers did change.
const handles = new WeakMap<ComposerState, PluginComposer>();
const results = new WeakMap<PluginComposer, readonly PluginComposer[]>();
function runningAgentId(agentId: string): string | null {
const session = useSessionStore.getState().sessions[serverId];
const agent = session?.agents.get(agentId) ?? session?.agentDetails.get(agentId) ?? null;
return agent && !agent.archivedAt ? agent.id : null;
}
function focusedAgentId(): string | null {
const focused = getFocusedAgent();
return focused?.serverId === serverId ? focused.agentId : null;
}
return {
subscribe(listener) {
const stops = [
useSessionStore.subscribe(listener),
useDraftStore.subscribe(listener),
subscribeFocusedAgent(listener),
// The model catalog arrives as an update. A query another component creates while it
// renders is announced too, and a subscriber that updates then makes React warn.
queryClient.getQueryCache().subscribe((event) => {
if (event.type === "updated") listener();
}),
];
return () => {
for (const stop of stops) stop();
};
},
resolve(selector: PluginComposerSelector) {
const requested = "agentId" in selector ? selector.agentId : focusedAgentId();
const agentId = requested === null ? null : runningAgentId(requested);
if (agentId === null) return NO_COMPOSERS;
const model = agentComposer(serverId, agentId);
const state = model.getState();
let composer = handles.get(state);
if (!composer) {
composer = toPluginComposer({ plugin, model, state });
handles.set(state, composer);
}
let result = results.get(composer);
if (!result) {
result = Object.freeze([composer]);
results.set(composer, result);
}
return result;
},
};
}