mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-05 16:04:43 +00:00
fix(plugins): a kept command honors the composer lock, and useAgent metadata is frozen
A plugin action that awaits an RPC holds the composer it was created with. The lock was captured at that render, so a submit that started meanwhile did not stop its insertText. Commands now ask the composer whether it is locked when they run. The agent snapshot a plugin gets from useAgent froze only the outer metadata record. It now uses the same deep frozen copy as the composer.
This commit is contained in:
@@ -115,6 +115,9 @@ describe("plugin context snapshots", () => {
|
||||
expect(Object.isFrozen(snapshot.labels)).toBe(true);
|
||||
expect(Reflect.set(snapshot, "title", "Hostile rename")).toBe(false);
|
||||
expect(Reflect.set(snapshot.labels, "phase", "hostile")).toBe(false);
|
||||
const issue = snapshot.metadata["linear.issue"] as { id: string };
|
||||
expect(Reflect.set(issue, "id", "hostile")).toBe(false);
|
||||
expect(agent.metadata?.["linear.issue"]).toEqual({ id: "ENG-1" });
|
||||
expect(snapshot.title).toBe("Implement plugin panels");
|
||||
expect(snapshot.labels).toEqual({ phase: "implementation" });
|
||||
expect(agent.title).toBe("Implement plugin panels");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { PluginAgentSnapshot, PluginWorkspaceSnapshot } from "@getpaseo/plugin";
|
||||
import type { Agent, WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { cloneAndFreeze } from "../freeze";
|
||||
|
||||
const workspaceSnapshots = new WeakMap<WorkspaceDescriptor, PluginWorkspaceSnapshot>();
|
||||
const agentSnapshots = new WeakMap<Agent, Map<string, PluginAgentSnapshot>>();
|
||||
@@ -49,7 +50,7 @@ export function createPluginAgentSnapshot(agent: Agent, workspaceId: string): Pl
|
||||
attentionReason: agent.attentionReason ?? null,
|
||||
parentAgentId: agent.parentAgentId,
|
||||
labels: Object.freeze({ ...agent.labels }),
|
||||
metadata: Object.freeze({ ...agent.metadata }),
|
||||
metadata: cloneAndFreeze(agent.metadata ?? {}),
|
||||
});
|
||||
const byWorkspace = agentSnapshots.get(agent) ?? new Map<string, PluginAgentSnapshot>();
|
||||
byWorkspace.set(workspaceId, snapshot);
|
||||
|
||||
@@ -64,6 +64,7 @@ function fakeAgent(context: ComposerAgent["context"], locked = false) {
|
||||
const agent: ComposerAgent = {
|
||||
context,
|
||||
locked,
|
||||
isLocked: () => locked,
|
||||
provider: "claude",
|
||||
model: "sonnet",
|
||||
mode: "default",
|
||||
@@ -89,6 +90,7 @@ function fakeInput(initial: UserComposerAttachment[] = [], locked = false) {
|
||||
const state = { text: "", attachments: initial };
|
||||
const input: ComposerInput = {
|
||||
locked,
|
||||
isLocked: () => locked,
|
||||
textSource: { getSnapshot: () => state.text, subscribe: () => () => {} },
|
||||
setText: (text) => {
|
||||
state.text = text;
|
||||
@@ -213,6 +215,25 @@ describe("toPluginComposer", () => {
|
||||
expect(calls).toEqual([]);
|
||||
});
|
||||
|
||||
it("refuses a command a plugin kept from before the composer locked", async () => {
|
||||
const { agent } = fakeAgent("new-workspace");
|
||||
const { input } = fakeInput();
|
||||
const composer = toPluginComposer({
|
||||
plugin: installed(),
|
||||
agent,
|
||||
composerInput: input,
|
||||
prompt: "",
|
||||
});
|
||||
|
||||
// The plugin's action was awaiting an RPC when the user submitted.
|
||||
const lock = { locked: true };
|
||||
agent.isLocked = () => lock.locked;
|
||||
input.isLocked = () => lock.locked;
|
||||
|
||||
expect(() => composer.insertText("late")).toThrow("The composer is locked");
|
||||
await expect(composer.setOptions({ effort: "low" })).rejects.toThrow("The composer is locked");
|
||||
});
|
||||
|
||||
it("hands out options and metadata a plugin cannot change", () => {
|
||||
const { agent } = fakeAgent("draft");
|
||||
agent.metadata = { nested: { list: [1] } };
|
||||
|
||||
@@ -22,12 +22,12 @@ function isPluginAttachment(
|
||||
|
||||
const LOCKED_MESSAGE = "The composer is locked while the agent is being created.";
|
||||
|
||||
function assertUnlocked(locked: boolean): void {
|
||||
if (locked) throw new Error(LOCKED_MESSAGE);
|
||||
function assertUnlocked(isLocked: () => boolean): void {
|
||||
if (isLocked()) throw new Error(LOCKED_MESSAGE);
|
||||
}
|
||||
|
||||
async function whenUnlocked(agent: ComposerAgent, command: () => Promise<void>): Promise<void> {
|
||||
assertUnlocked(agent.locked);
|
||||
assertUnlocked(agent.isLocked);
|
||||
await command();
|
||||
}
|
||||
|
||||
@@ -60,11 +60,11 @@ export function toPluginComposer(input: {
|
||||
metadata: cloneAndFreeze(agent.metadata),
|
||||
},
|
||||
setPrompt: (text) => {
|
||||
assertUnlocked(composerInput.locked);
|
||||
assertUnlocked(composerInput.isLocked);
|
||||
composerInput.setText(text);
|
||||
},
|
||||
insertText: (text) => {
|
||||
assertUnlocked(composerInput.locked);
|
||||
assertUnlocked(composerInput.isLocked);
|
||||
composerInput.insertText(text);
|
||||
},
|
||||
addAttachment({ sourceId, item }) {
|
||||
|
||||
Reference in New Issue
Block a user