mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
Draft metadata was component state while the prompt lived in the draft store, so a composer that remounted across the compact breakpoint kept its text and lost the plugin's choices. It now lives in the draft store beside the prompt, is cleared when the draft is sent, and is not cleared when the prompt is emptied. It is not persisted across app restarts. While the New workspace composer is creating the agent, the built-in controls are disabled but plugin controls and commands were not. They read the composer's own lock: Paseo controls render disabled, and setPrompt, insertText and the agent commands refuse. The options and metadata a plugin reads are frozen copies, through the same helper the timeline projection uses. The composer control test mounted components in JSDOM, which the testing rules ban; it now runs in the browser project.
136 lines
4.5 KiB
TypeScript
136 lines
4.5 KiB
TypeScript
import type { AgentTimelineItem, ToolCallTimelineItem } from "@getpaseo/protocol/agent-types";
|
|
import type { AgentToolCallData, PluginTimelineStreamItem, StreamItem } from "@/types/stream";
|
|
import { cloneAndFreeze } from "../freeze";
|
|
import type { TimelineItemTransform } from "./model";
|
|
|
|
type AssistantPhase = "streaming" | "complete";
|
|
|
|
const projectionCache = new WeakMap<
|
|
TimelineItemTransform,
|
|
WeakMap<StreamItem, Partial<Record<AssistantPhase, StreamItem[]>>>
|
|
>();
|
|
|
|
function sourceToolCallTimelineItem(data: AgentToolCallData): ToolCallTimelineItem {
|
|
const { callId, name, status, error, detail, metadata, agentMessage } = data;
|
|
const base = {
|
|
type: "tool_call" as const,
|
|
callId,
|
|
name,
|
|
detail,
|
|
...(metadata ? { metadata } : {}),
|
|
...(agentMessage ? { agentMessage } : {}),
|
|
};
|
|
switch (status) {
|
|
case "running":
|
|
case "completed":
|
|
case "canceled":
|
|
return { ...base, status, error: null };
|
|
case "failed":
|
|
return { ...base, status, error };
|
|
}
|
|
}
|
|
|
|
function sourceTimelineItem(item: StreamItem): AgentTimelineItem | null {
|
|
switch (item.kind) {
|
|
case "user_message":
|
|
return {
|
|
type: "user_message",
|
|
text: item.text,
|
|
...(item.messageId ? { messageId: item.messageId } : {}),
|
|
...(item.clientMessageId ? { clientMessageId: item.clientMessageId } : {}),
|
|
};
|
|
case "assistant_message":
|
|
return {
|
|
type: "assistant_message",
|
|
text: item.text,
|
|
...(item.messageId ? { messageId: item.messageId } : {}),
|
|
};
|
|
case "thought":
|
|
return { type: "reasoning", text: item.text };
|
|
case "tool_call": {
|
|
if (item.payload.source !== "agent") return null;
|
|
return sourceToolCallTimelineItem(item.payload.data);
|
|
}
|
|
case "todo_list":
|
|
return { type: "todo", items: item.items };
|
|
case "notification":
|
|
return item.sourceType === "error"
|
|
? { type: "error", message: item.message }
|
|
: { type: "notification", level: item.level, message: item.message };
|
|
case "compaction":
|
|
return {
|
|
type: "compaction",
|
|
status: item.status,
|
|
...(item.trigger ? { trigger: item.trigger } : {}),
|
|
...(item.preTokens !== undefined ? { preTokens: item.preTokens } : {}),
|
|
};
|
|
case "plugin":
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function transformSourceItem(
|
|
item: StreamItem,
|
|
transformTimelineItem: TimelineItemTransform,
|
|
assistantPhase: AssistantPhase,
|
|
): StreamItem[] {
|
|
const source = sourceTimelineItem(item);
|
|
if (!source) return [item];
|
|
const isStreamingThought = item.kind === "thought" && item.status === "loading";
|
|
const isStreamingToolCall =
|
|
item.kind === "tool_call" &&
|
|
item.payload.source === "agent" &&
|
|
item.payload.data.status === "running";
|
|
const isStreamingAssistant = item.kind === "assistant_message" && assistantPhase === "streaming";
|
|
const phase =
|
|
isStreamingAssistant || isStreamingThought || isStreamingToolCall ? "streaming" : "complete";
|
|
const transformed = transformTimelineItem({
|
|
item: cloneAndFreeze(source),
|
|
phase,
|
|
sourceId: item.id,
|
|
});
|
|
if (transformed === undefined) return [item];
|
|
return transformed.map((pluginItem) => {
|
|
const projected: PluginTimelineStreamItem = {
|
|
kind: "plugin",
|
|
id: `${pluginItem.pluginId}/${pluginItem.id}`,
|
|
sourceId: item.id,
|
|
timestamp: item.timestamp,
|
|
pluginId: pluginItem.pluginId,
|
|
pluginItemId: pluginItem.id,
|
|
itemKind: pluginItem.kind,
|
|
version: pluginItem.version,
|
|
data: pluginItem.data,
|
|
};
|
|
if (item.timelineCursor) projected.timelineCursor = item.timelineCursor;
|
|
if (item.turnId) projected.turnId = item.turnId;
|
|
return projected;
|
|
});
|
|
}
|
|
|
|
export function projectPluginTimelineItems(
|
|
items: StreamItem[],
|
|
transformTimelineItem: TimelineItemTransform | undefined,
|
|
assistantPhase: AssistantPhase = "complete",
|
|
): StreamItem[] {
|
|
if (!transformTimelineItem) return items;
|
|
let bySource = projectionCache.get(transformTimelineItem);
|
|
if (!bySource) {
|
|
bySource = new WeakMap();
|
|
projectionCache.set(transformTimelineItem, bySource);
|
|
}
|
|
let changed = false;
|
|
const projected = items.flatMap((item) => {
|
|
const cached = bySource.get(item)?.[assistantPhase];
|
|
if (cached) {
|
|
changed = changed || cached.length !== 1 || cached[0] !== item;
|
|
return cached;
|
|
}
|
|
const output = transformSourceItem(item, transformTimelineItem, assistantPhase);
|
|
bySource.set(item, { ...bySource.get(item), [assistantPhase]: output });
|
|
changed = changed || output.length !== 1 || output[0] !== item;
|
|
return output;
|
|
});
|
|
return changed ? projected : items;
|
|
}
|