diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx
index 0c1439f39..d5e82e3d0 100644
--- a/components/canvas/CanvasChatPanel.tsx
+++ b/components/canvas/CanvasChatPanel.tsx
@@ -144,15 +144,16 @@ import {
type CanvasToolbarItemKey,
} from "@/lib/canvas-toolbar-config";
import {
- AGENT_PROGRESS_STEPS,
type AgentProgressStreamPayload,
type AgentTargetLockRefPreview,
type AgentTargetLockStreamPayload,
+ buildAgentProgressNodeSteps,
type CanvasOperationStreamPayload,
type CanvasViewOperationStreamPayload,
type CanvasMediaStreamPayload,
filterCanvasMediaPayloadForUnhandledItems,
getAgentProgressFallbackTitle,
+ getAgentProgressStepLabel,
getAgentTargetLockTitle,
getAgentTargetResolutionLabel,
getCanvasMediaPayloadHandledItemIds,
@@ -1623,6 +1624,130 @@ function CanvasAssistantTypingPlaceholder({ hint }: { hint?: string | null }) {
);
}
+function CanvasAgentProgressTimeline({
+ progress,
+}: {
+ progress?: AgentProgressStreamPayload | null;
+}) {
+ const steps = buildAgentProgressNodeSteps(progress);
+ const title = progress?.title?.trim() || getAgentProgressFallbackTitle(progress);
+ const isFailed = progress?.stage === "failed";
+ const isCompleted = progress?.stage === "completed";
+ const isClarify = progress?.stage === "clarify";
+
+ return (
+
+
+
+
+ {isFailed ? (
+
+ ) : isCompleted ? (
+
+ ) : (
+
+ )}
+
+
+ {title}
+
+
+
+
+
+ {steps.map((step, index) => {
+ const isStepCompleted = step.state === "completed";
+ const isStepCurrent = step.state === "current";
+ const isStepFailed = step.state === "failed";
+ const lineIsActive =
+ isStepCompleted || isStepCurrent || isStepFailed;
+
+ return (
+
+
+
+ {isStepCompleted ? (
+
+ ) : isStepFailed ? (
+
+ ) : isStepCurrent ? (
+
+ ) : (
+
+ )}
+ {isStepCurrent ? (
+
+ ) : null}
+
+
+ {step.label}
+
+
+ {index < steps.length - 1 ? (
+
+ ) : null}
+
+ );
+ })}
+
+
+
+
+ );
+}
+
function CanvasComposerUploadProgressDialog({
progress,
}: {
@@ -12318,26 +12443,10 @@ export function CanvasChatPanel({
activeAgentProgress?.title?.trim() || getAgentProgressFallbackTitle(activeAgentProgress);
const agentProgressMessage =
activeAgentProgress?.message?.trim() || "我正在根据当前上下文持续推进这一步。";
- const agentProgressSteps = useMemo(() => {
- const currentStepIndex = Math.max(
- 0,
- AGENT_PROGRESS_STEPS.findIndex((step) => step.key === activeAgentProgress?.currentStep)
- );
- return AGENT_PROGRESS_STEPS.map((step, index) => {
- let state: "upcoming" | "current" | "completed" | "failed" = "upcoming";
- if (activeAgentProgress?.stage === "failed") {
- state = index < currentStepIndex ? "completed" : index === currentStepIndex ? "failed" : "upcoming";
- } else if (activeAgentProgress?.stage === "completed") {
- state = "completed";
- } else {
- state = index < currentStepIndex ? "completed" : index === currentStepIndex ? "current" : "upcoming";
- }
- return {
- ...step,
- state,
- };
- });
- }, [activeAgentProgress]);
+ const agentProgressSteps = useMemo(
+ () => buildAgentProgressNodeSteps(activeAgentProgress),
+ [activeAgentProgress]
+ );
const renderModelSelectContent = () => (
-
-
- 分析中
-
-
+
)}
@@ -14660,9 +14764,10 @@ export function CanvasChatPanel({
{agentProgressTitle}
- {AGENT_PROGRESS_STEPS.find(
- (step) => step.key === activeAgentProgress.currentStep
- )?.label || "处理中"}
+ {getAgentProgressStepLabel(
+ activeAgentProgress.currentStep || "lock_target",
+ activeAgentProgress
+ )}
diff --git a/components/canvas/chat-panel/canvas-agent-stream.ts b/components/canvas/chat-panel/canvas-agent-stream.ts
index 3e2480894..e7a6246a2 100644
--- a/components/canvas/chat-panel/canvas-agent-stream.ts
+++ b/components/canvas/chat-panel/canvas-agent-stream.ts
@@ -206,14 +206,26 @@ export type AgentProgressStreamPayload = {
capability?: string;
};
+export type AgentProgressStepState =
+ | "upcoming"
+ | "current"
+ | "completed"
+ | "failed";
+
+export type AgentProgressNodeStep = {
+ key: NonNullable
;
+ label: string;
+ state: AgentProgressStepState;
+};
+
export const AGENT_PROGRESS_STEPS: Array<{
key: NonNullable;
label: string;
}> = [
- { key: "lock_target", label: "整理素材" },
- { key: "analyze", label: "理解需求" },
- { key: "select_tool", label: "优化提示" },
- { key: "execute", label: "提交生成" },
+ { key: "lock_target", label: "理解用户需求" },
+ { key: "analyze", label: "分析使用工具" },
+ { key: "select_tool", label: "确认执行工具" },
+ { key: "execute", label: "执行中" },
{ key: "finalize", label: "整理结果" },
];
@@ -260,3 +272,94 @@ export function getAgentProgressFallbackTitle(payload?: AgentProgressStreamPaylo
return "正在理解需求";
}
}
+
+export function getAgentProgressActionLabel(
+ payload?: AgentProgressStreamPayload | null
+) {
+ switch (payload?.capability) {
+ case "analyze":
+ return "图片分析";
+ case "generic_edit":
+ return "修图";
+ case "remove_background":
+ return "抠图";
+ case "remove_watermark":
+ return "去水印";
+ case "enhance":
+ return "图像增强";
+ case "resize":
+ return "尺寸调整";
+ case "extend":
+ return "扩图";
+ case "angle_variant":
+ return "多视角";
+ case "local_edit":
+ return "局部编辑";
+ case "box_cutout":
+ return "框选抠图";
+ case "generate_image":
+ return "生图";
+ case "generate_video":
+ return "视频生成";
+ default:
+ return "工具";
+ }
+}
+
+export function getAgentProgressStepLabel(
+ stepKey: NonNullable,
+ payload?: AgentProgressStreamPayload | null
+) {
+ const actionLabel = getAgentProgressActionLabel(payload);
+ switch (stepKey) {
+ case "lock_target":
+ return "理解用户需求";
+ case "analyze":
+ return payload?.capability === "analyze" ? "分析图片" : "分析使用工具";
+ case "select_tool":
+ return actionLabel === "工具" ? "确认执行工具" : `确认${actionLabel}工具`;
+ case "execute":
+ return actionLabel === "工具" ? "执行中" : `${actionLabel}中`;
+ case "finalize":
+ return "整理结果";
+ default:
+ return "处理中";
+ }
+}
+
+export function buildAgentProgressNodeSteps(
+ payload?: AgentProgressStreamPayload | null
+): AgentProgressNodeStep[] {
+ const currentStep = payload?.currentStep || "lock_target";
+ const currentStepIndex = Math.max(
+ 0,
+ AGENT_PROGRESS_STEPS.findIndex((step) => step.key === currentStep)
+ );
+
+ return AGENT_PROGRESS_STEPS.map((step, index) => {
+ let state: AgentProgressStepState = "upcoming";
+ if (payload?.stage === "failed") {
+ state =
+ index < currentStepIndex
+ ? "completed"
+ : index === currentStepIndex
+ ? "failed"
+ : "upcoming";
+ } else if (payload?.stage === "completed") {
+ state = "completed";
+ } else {
+ state =
+ index < currentStepIndex
+ ? "completed"
+ : index === currentStepIndex
+ ? "current"
+ : "upcoming";
+ }
+
+ return {
+ key: step.key,
+ label: getAgentProgressStepLabel(step.key, payload),
+ state,
+ };
+ });
+}
diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts
index 1ed8f6a5a..2504c581c 100644
--- a/tests/routes/canvas-chat-helpers.test.ts
+++ b/tests/routes/canvas-chat-helpers.test.ts
@@ -9,8 +9,10 @@ import {
} from "@/components/canvas/chat-panel/canvas-chat-media";
import {
filterCanvasMediaPayloadForUnhandledItems,
+ buildAgentProgressNodeSteps,
getCanvasMediaPayloadHandledItemIds,
getCanvasOperationHandledMediaItemIds,
+ getAgentProgressStepLabel,
isCanvasAgentViewOperationClientEnabled,
normalizeCanvasAgentClientBoolean,
shouldRetryCanvasViewOperationAfterRefresh,
@@ -1210,6 +1212,44 @@ test.describe("canvas chat pure helpers", () => {
expect(editText).toContain("替换背景");
});
+ test("builds capability-aware agent progress node labels", () => {
+ const selectingSteps = buildAgentProgressNodeSteps({
+ stage: "planning",
+ currentStep: "select_tool",
+ capability: "remove_background",
+ });
+
+ expect(selectingSteps.map((step) => step.label)).toEqual([
+ "理解用户需求",
+ "分析使用工具",
+ "确认抠图工具",
+ "抠图中",
+ "整理结果",
+ ]);
+ expect(selectingSteps.map((step) => step.state)).toEqual([
+ "completed",
+ "completed",
+ "current",
+ "upcoming",
+ "upcoming",
+ ]);
+
+ const executingSteps = buildAgentProgressNodeSteps({
+ stage: "executing",
+ currentStep: "execute",
+ capability: "remove_background",
+ });
+
+ expect(executingSteps[2]?.state).toBe("completed");
+ expect(executingSteps[3]).toMatchObject({
+ label: "抠图中",
+ state: "current",
+ });
+ expect(getAgentProgressStepLabel("execute", { capability: "generate_video" })).toBe(
+ "视频生成中"
+ );
+ });
+
test("resolves deictic canvas edit requests to latest results and direct tools", () => {
const latestResultUrl = "https://cdn.example.com/latest-cat-result.png";
const requestPolicy = resolveCanvasAgentRequestPolicy({