+ {visibleActions.map((action) => (
+
))}
-
+ {isNodeRead && (collapsedReadNodeCount > 0 || readExpanded) ? (
+
+ ) : null}
+
+ ) : null}
+ {state.isError && text && text !== summary ? (
+
+
{state.icon}
-
-
- {categoryIcon}
- {categoryLabel}
-
- {summary}
- {state.label !== "已完成" ? (
-
- {state.label}
-
- ) : null}
-
- {actions.length ? (
-
- {visibleActions.map((action) => (
-
- ))}
- {isNodeRead && (collapsedReadNodeCount > 0 || readExpanded) ? (
-
- ) : null}
-
- ) : null}
- {state.isError && text && text !== summary ? (
-
- {conciseError}
-
- ) : null}
- {state.isError && text.length > 180 ? (
-
- 查看完整错误详情
- {text}
-
- ) : null}
- {state.isError && objectField(objectField(detail, "result"), "taskId") ? (
-
- 任务 ID:{String(objectField(objectField(detail, "result"), "taskId"))}
-
- ) : null}
+ {header}
{state.label === "已完成" ?
已完成 : null}
);
+
}
export function AgentWorkingMessage({ theme, label = WORKING_TEXT }: { theme: (typeof canvasThemes)[keyof typeof canvasThemes]; label?: string }) {
@@ -512,7 +522,7 @@ export function AgentWorkingMessage({ theme, label = WORKING_TEXT }: { theme: (t
);
}
-export function AgentPlanBar({ items, theme, minimized, onToggle }: { items: CloudAgentPlanItem[]; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; minimized: boolean; onToggle: () => void }) {
+export function AgentPlanBar({ items, theme, minimized, onToggle, terminal = false }: { items: CloudAgentPlanItem[]; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; minimized: boolean; onToggle: () => void; terminal?: boolean }) {
const doneCount = items.filter((entry) => entry.status === "done").length;
const allDone = doneCount === items.length;
return (
@@ -523,6 +533,7 @@ export function AgentPlanBar({ items, theme, minimized, onToggle }: { items: Clo
{doneCount}/{items.length}
+ {terminal && !allDone ?
本轮已结束,未完成项已停止 : null}
{minimized ? "展开" : "收起"}
{minimized ?
:
}
@@ -532,11 +543,11 @@ export function AgentPlanBar({ items, theme, minimized, onToggle }: { items: Clo
{items.map((entry) => {
const done = entry.status === "done";
const doing = entry.status === "doing";
- const Icon = done ? CheckCircle2 : doing ? LoaderCircle : CircleDot;
+ const Icon = done ? CheckCircle2 : terminal ? CircleAlert : doing ? LoaderCircle : CircleDot;
return (
-
- {entry.title}
+
+ {entry.title}
);
})}
diff --git a/web/src/components/canvas/canvas-cloud-agent-panel.tsx b/web/src/components/canvas/canvas-cloud-agent-panel.tsx
index 9d7470930..876fd37c8 100644
--- a/web/src/components/canvas/canvas-cloud-agent-panel.tsx
+++ b/web/src/components/canvas/canvas-cloud-agent-panel.tsx
@@ -6,7 +6,7 @@ import { saveAs } from "file-saver";
import { buildAgentDebugExport } from "@/lib/canvas/agent-debug-export";
import { markdownPlainText } from "@/lib/markdown-plain-text";
import { agentToolRetry, mergeAgentToolRetry } from "@/lib/canvas/agent-tool-retry";
-import { agentPlanVisible, latestAgentPlanItems, pendingAgentQuestion } from "@/lib/canvas/cloud-agent-plan";
+import { agentPlanVisible, latestAgentPlanItems, latestAgentPlanTerminal, pendingAgentQuestion } from "@/lib/canvas/cloud-agent-plan";
import { emptyAgentContextUsage, presentAgentContextUsage, reduceAgentContextUsage, type AgentContextPhase, type AgentContextUsage, type AgentContextUsageView } from "@/lib/canvas/agent-context-usage";
import { nanoid } from "nanoid";
@@ -43,11 +43,11 @@ import { live2DModelURL } from "@/services/api/appearance";
import { Live2DAvatar } from "./live2d-avatar";
import "./canvas-cloud-agent.css";
-type CloudAgentPanelProps = { canvasId: string; domainProjectId?: string; nodeCount: number; references: CanvasResourceReference[]; open: boolean; prefillPrompt?: string; onOpen: () => void; onCollapse: () => void; onFocusNode?: (nodeId: string) => void };
+type CloudAgentPanelProps = { canvasId: string; domainProjectId?: string; nodeCount: number; selectedNodeIds: string[]; references: CanvasResourceReference[]; open: boolean; prefillPrompt?: string; onOpen: () => void; onCollapse: () => void; onFocusNode?: (nodeId: string) => void };
type ApprovalState = { approvalId: string; detail: Record
; reason: string };
type AgentPanelView = "chat" | "history" | "settings";
-export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, references, open, prefillPrompt, onOpen, onCollapse, onFocusNode }: CloudAgentPanelProps) {
+export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, selectedNodeIds, references, open, prefillPrompt, onOpen, onCollapse, onFocusNode }: CloudAgentPanelProps) {
const userId = useUserStore((state) => state.user?.id);
const appearance = useAppearanceStore((state) => state.appearance.canvas) || DEFAULT_CANVAS_APPEARANCE;
const theme = canvasThemes[useActiveTheme()];
@@ -95,6 +95,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
const [pendingHydrated, setPendingHydrated] = useState(false);
const [planMinimized, setPlanMinimized] = useState(false);
const planItems = useMemo(() => latestAgentPlanItems(messages), [messages]);
+ const planTerminal = useMemo(() => latestAgentPlanTerminal(messages), [messages]);
const planVisible = agentPlanVisible(planItems);
const pendingQuestion = useMemo(() => pendingAgentQuestion(messages), [messages]);
const [scenePresets, setScenePresets] = useState([]);
@@ -543,6 +544,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
model: modelOptionName(selectedModel) || undefined,
...(logicalModelId ? { logicalModelId } : requestConfig.channelId ? { channelId: requestConfig.channelId, channelModelKey: modelOptionName(selectedModel) || undefined } : {}),
skillIds: [...new Set([...selectedSkillIds, ...resolveSkillMentions(value, installedSkills).map((skill) => skill.skillId)])],
+ focusNodeIds: selectedNodeIds.length <= 8 ? selectedNodeIds : [],
permissionMode, contextScope,
budget: { maxCredits: positiveNumber(maxCredits), maxGenerationTasks: permissionMode === "read_only" ? 0 : Number(maxGenerationTasks), maxVideoSeconds: permissionMode === "read_only" ? 0 : Number(maxVideoSeconds) },
};
@@ -871,7 +873,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
onApprove={(settings) => void submitApproval("approve", settings)}
onReject={() => void submitApproval("reject")}
/>
- {planVisible ? setPlanMinimized((value) => !value)} /> : null}
+ {planVisible ? setPlanMinimized((value) => !value)} /> : null}
{historyHydrated && !messages.some((message) => message.role === "user" || message.role === "assistant") && !run ? (
(current ? { ...current, status: String(payload.status || current.status) as AgentRun["status"], updatedAt: event.createdAt, revision: Number(payload.revision || 0), cleanupPending: Boolean(payload.cleanupPending), failureMessage: String(payload.failureMessage || ""), skills: payload.skills as AgentRun["skills"], spentCredits: Number(payload.spentCredits || 0), step: Number(payload.step || 0), approval: snapshotApproval } : current));
+ const nextStatus = String(payload.status || "") as AgentRun["status"];
+ const terminal = ["completed", "failed", "cancelled", "rejected"].includes(nextStatus);
+ setRun((current) => (current ? { ...current, status: nextStatus || current.status, updatedAt: event.createdAt, revision: Number(payload.revision || 0), cleanupPending: Boolean(payload.cleanupPending), failureMessage: String(payload.failureMessage || ""), skills: payload.skills as AgentRun["skills"], spentCredits: Number(payload.spentCredits || 0), step: Number(payload.step || 0), approval: snapshotApproval } : current));
+ if (terminal) {
+ setMessages((current) => current.map((message) => message.id === `plan-${event.runId}` && message.planItems?.length ? { ...message, planTerminal: true, streaming: false } : message));
+ }
if (payload.failureMessage) setMessages((current) => appendAgentError(current, `terminal-${event.runId}`, String(payload.failureMessage)));
if (snapshotApproval && !snapshotApproval.decision && snapshotApproval.approvalId) {
setApproval((current) => ({ approvalId: snapshotApproval.approvalId, detail: snapshotApproval, reason: current?.approvalId === snapshotApproval.approvalId ? current.reason : snapshotApproval.reason || "" }));
diff --git a/web/src/components/canvas/canvas-cloud-agent.css b/web/src/components/canvas/canvas-cloud-agent.css
index 272e5acbe..f9eb4f25a 100644
--- a/web/src/components/canvas/canvas-cloud-agent.css
+++ b/web/src/components/canvas/canvas-cloud-agent.css
@@ -787,6 +787,36 @@
transition: transform 160ms ease;
}
+/* Tool calls are secondary activity, not chat content. Keep the receipt row
+ scannable and reveal node links / diagnostics only on demand. */
+.agent-tool-details {
+ min-width: 0;
+}
+
+.agent-tool-summary-toggle {
+ display: flex;
+ align-items: flex-start;
+}
+
+.agent-tool-summary-toggle::-webkit-details-marker {
+ display: none;
+}
+
+.agent-tool-chevron {
+ flex: 0 0 auto;
+ color: color-mix(in srgb, currentColor 48%, transparent);
+ transition: transform 160ms ease;
+}
+
+.agent-tool-details[open] .agent-tool-chevron {
+ transform: rotate(180deg);
+}
+
+.agent-tool-detail-body {
+ margin: -1px 0 4px 31px;
+ padding: 0 2px 4px;
+}
+
/* 没有节点明细的“读取信息 / 操作画布”只保留时间线语义,不再撑成一张空卡片。 */
.agent-tool-row--plain {
min-height: 28px;
@@ -899,6 +929,7 @@
@media (prefers-reduced-motion: reduce) {
.agent-tool-row,
+ .agent-tool-chevron,
.agent-tool-action-link { transition: none; }
}
diff --git a/web/src/lib/canvas/cloud-agent-plan.ts b/web/src/lib/canvas/cloud-agent-plan.ts
index acc7fe424..1a6f0cab1 100644
--- a/web/src/lib/canvas/cloud-agent-plan.ts
+++ b/web/src/lib/canvas/cloud-agent-plan.ts
@@ -1,6 +1,6 @@
import type { CloudAgentPlanItem, CloudAgentUserQuestion } from "@/components/canvas/canvas-cloud-agent-chat-ui";
-type PlanCarrier = { planItems?: CloudAgentPlanItem[] };
+type PlanCarrier = { planItems?: CloudAgentPlanItem[]; planTerminal?: boolean };
/**
* 从对话消息流里挑出**当前该展示的那份**待办清单。
@@ -17,6 +17,18 @@ export function latestAgentPlanItems(messages: readonly PlanCarrier[]): CloudAge
return [];
}
+/**
+ * 计划清单可以在运行结束后仍保留未完成项。这个标记必须和清单一起
+ * 持久化,否则从本地历史恢复时,未完成项会被误显示为仍在执行。
+ */
+export function latestAgentPlanTerminal(messages: readonly PlanCarrier[]): boolean {
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
+ const items = messages[index].planItems;
+ if (items?.length) return messages[index].planTerminal === true;
+ }
+ return false;
+}
+
/** 全部完成才收起清单;只要还有未开始/进行中,就一直占着输入框上方那一行。 */
export function agentPlanVisible(items: readonly CloudAgentPlanItem[]): boolean {
return items.length > 0 && items.some((entry) => entry.status !== "done");
diff --git a/web/src/pages/canvas/project.tsx b/web/src/pages/canvas/project.tsx
index 6eaf88555..f3e37c513 100644
--- a/web/src/pages/canvas/project.tsx
+++ b/web/src/pages/canvas/project.tsx
@@ -468,6 +468,11 @@ function InfiniteCanvasPage() {
const ids = nodeId ? [nodeId] : Array.from(selectedNodeIdsRef.current);
const references = ids.map((id) => agentMentionReferences.find((reference) => reference.nodeId === id)).filter((reference): reference is CanvasResourceReference => Boolean(reference));
if (!references.length) return;
+ if (nodeId && !selectedNodeIdsRef.current.has(nodeId)) {
+ const selection = new Set([nodeId]);
+ selectedNodeIdsRef.current = selection;
+ setSelectedNodeIds(selection);
+ }
setAgentPrefillPrompt(`${references.map(canvasResourceMentionToken).join(" ")} `);
openAgent();
setContextMenu(null);
@@ -2822,7 +2827,7 @@ function InfiniteCanvasPage() {