feat(agent-ui): add inline skill and canvas references

This commit is contained in:
yu
2026-08-06 19:11:40 +08:00
parent 10dc65d2d6
commit 89a4e192e5
15 changed files with 758 additions and 232 deletions
+35 -7
View File
@@ -18,7 +18,7 @@ type AgentSkillStore = {
loaded: boolean;
errors: string[];
loadSkills: (endpoint: string, token: string, forceReload?: boolean) => Promise<void>;
selectSkill: (skill: AgentSkillSummary | null) => void;
selectSkill: (skill: AgentSkillSummary | null, prompt?: string) => void;
clearSelection: (expectedRevision?: number) => void;
setDraft: (draft: AgentSkillDraft | null) => void;
setGeneratingSource: (source: "conversation" | "canvas" | null) => void;
@@ -47,24 +47,41 @@ export const useAgentSkillStore = create<AgentSkillStore>((set, get) => ({
const selected = current.selectedSkill;
const selectedSkill = selected ? skills.find((item) => item.name === selected.name && item.path === selected.path && item.enabled) || null : null;
const selectionChanged = Boolean(selected && !selectedSkill);
if (selectionChanged) clearSelectedSkillPrompt(current);
const autoPrompt = selectedSkill
? replaceAutoPrompt(current.autoPrompt, selectedSkill.interface?.defaultPrompt?.trim() || "", false)
: replaceAutoPrompt(current.autoPrompt, "", false);
? replaceAutoPrompt(current.autoPrompt, selectedSkill.interface?.defaultPrompt?.trim() || `$${selectedSkill.name}`, false)
: "";
set({ skills, selectedSkill, autoPrompt, selectionRevision: current.selectionRevision + (selectionChanged ? 1 : 0), loading: false, loaded: true, errors: (response.errors || []).map(skillErrorText) });
} catch (error) {
if (sequence !== loadSequence) return;
set({ loading: false, loaded: true, errors: [error instanceof Error ? error.message : i18n.t("agent.state.skillReadFailed")] });
}
},
selectSkill: (selectedSkill) => {
selectSkill: (selectedSkill, prompt) => {
const current = get();
const autoPrompt = replaceAutoPrompt(current.autoPrompt, selectedSkill?.interface?.defaultPrompt?.trim() || "", true);
const agent = useAgentStore.getState();
const marker = selectedSkill ? `$${selectedSkill.name}` : "";
const previousMarker = current.selectedSkill ? `$${current.selectedSkill.name}` : "";
let nextPrompt = prompt;
let autoPrompt = "";
if (nextPrompt === undefined && selectedSkill) {
const defaultPrompt = selectedSkill.interface?.defaultPrompt?.trim() || marker;
if (!agent.prompt.trim() || Boolean(current.autoPrompt && agent.prompt === current.autoPrompt)) {
nextPrompt = defaultPrompt;
autoPrompt = defaultPrompt;
} else if (previousMarker && agent.prompt.includes(previousMarker)) {
nextPrompt = agent.prompt.replace(previousMarker, marker);
} else {
nextPrompt = agent.prompt.includes(marker) ? agent.prompt : `${agent.prompt}${/\s$/.test(agent.prompt) ? "" : " "}${marker}`;
}
}
if (nextPrompt !== undefined && agent.prompt !== nextPrompt) agent.setAgentState({ prompt: nextPrompt });
set({ selectedSkill, autoPrompt, selectionRevision: current.selectionRevision + 1 });
},
clearSelection: (expectedRevision) => {
const current = get();
if (expectedRevision !== undefined && expectedRevision !== current.selectionRevision) return;
replaceAutoPrompt(current.autoPrompt, "", false);
clearSelectedSkillPrompt(current);
set({ selectedSkill: null, autoPrompt: "", selectionRevision: current.selectionRevision + 1 });
},
setDraft: (draft) => set({ draft }),
@@ -72,7 +89,7 @@ export const useAgentSkillStore = create<AgentSkillStore>((set, get) => ({
reset: () => {
loadSequence += 1;
const current = get();
replaceAutoPrompt(current.autoPrompt, "", false);
clearSelectedSkillPrompt(current);
set({ skills: [], selectedSkill: null, selectionRevision: current.selectionRevision + 1, connectionRevision: current.connectionRevision + 1, autoPrompt: "", draft: null, generatingSource: null, loading: false, loaded: false, errors: [] });
},
}));
@@ -85,6 +102,17 @@ function replaceAutoPrompt(previous: string, next: string, fillEmpty: boolean) {
return next;
}
function clearSelectedSkillPrompt(selection: Pick<AgentSkillStore, "selectedSkill" | "autoPrompt">) {
const agent = useAgentStore.getState();
if (selection.autoPrompt && agent.prompt === selection.autoPrompt) {
agent.setAgentState({ prompt: "" });
return;
}
if (!selection.selectedSkill) return;
const marker = `$${selection.selectedSkill.name}`;
if (agent.prompt.includes(marker)) agent.setAgentState({ prompt: agent.prompt.replace(marker, "") });
}
function skillErrorText(value: unknown) {
if (typeof value === "string") return value;
if (!value || typeof value !== "object") return i18n.t("agent.state.skillParseFailed");
+7 -1
View File
@@ -2,10 +2,14 @@ import { create } from "zustand";
import i18n from "@/i18n";
import type { CanvasAgentOp, CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
export type AgentChatRole = "user" | "assistant" | "system" | "tool" | "error";
export type AgentAttachment = { id: string; name: string; type: string; size: number; width: number; height: number; url: string; dataUrl: string };
export type AgentChatItem = { id: string; itemId?: string; clientMessageId?: string; threadId?: string; turnId?: string; role: AgentChatRole; title?: string; text: string; historyText?: string; meta?: string; detail?: unknown; attachments?: AgentAttachment[]; streamId?: string; activityItems?: Record<string, string> };
export type AgentMessageAttachment = Pick<AgentAttachment, "id" | "name" | "url"> & Partial<Pick<AgentAttachment, "type" | "size" | "width" | "height" | "dataUrl">>;
export type AgentCanvasReference = Pick<CanvasResourceReference, "nodeId" | "label" | "title" | "kind" | "previewUrl" | "text">;
export type AgentSkillReference = { name: string; path: string; displayName?: string };
export type AgentChatItem = { id: string; itemId?: string; clientMessageId?: string; threadId?: string; turnId?: string; role: AgentChatRole; title?: string; text: string; meta?: string; detail?: unknown; attachments?: AgentMessageAttachment[]; canvasReferences?: AgentCanvasReference[]; skill?: AgentSkillReference; streamId?: string; activityItems?: Record<string, string> };
export type AgentEventLog = { id: string; time: string; title: string; text: string; raw?: unknown };
export type AgentPendingToolCall = { requestId: string; name: string; input?: { ops?: CanvasAgentOp[]; path?: string } & Record<string, unknown> };
export type AgentPermissionMode = "request" | "automatic" | "full";
@@ -52,6 +56,7 @@ type AgentStore = {
silentConnect: boolean;
prompt: string;
attachments: AgentAttachment[];
canvasReferences: CanvasResourceReference[];
sending: boolean;
waiting: boolean;
messages: AgentChatItem[];
@@ -102,6 +107,7 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
silentConnect: false,
prompt: "",
attachments: [],
canvasReferences: [],
sending: false,
waiting: false,
messages: [],