- {item.title || (errorTone === "warning" ? "Agent 正在重试" : "Agent 暂时无法继续")}
+
+
+
+
+
+
+
{title}
+ {description ?
{description}
: null}
-
- {item.text}
-
- {item.meta ? (
-
- {item.meta}
-
+ {item.meta ?
{item.meta}
: null}
+ {diagnosticId ? (
+
+
+
+ 诊断信息
+
+
+
如反复出现,请将诊断号反馈给管理员。
+
{diagnosticId}
+
+
) : null}
{onRetry ? (
-
:
} disabled={retrying} onClick={onRetry}>
+
:
} disabled={retrying} onClick={onRetry}>
{retrying ? "重试中" : "重试本轮"}
) : null}
diff --git a/web/src/components/canvas/canvas-cloud-agent-composer.tsx b/web/src/components/canvas/canvas-cloud-agent-composer.tsx
index 251d242ac..18a733243 100644
--- a/web/src/components/canvas/canvas-cloud-agent-composer.tsx
+++ b/web/src/components/canvas/canvas-cloud-agent-composer.tsx
@@ -187,6 +187,7 @@ export function AgentChatComposer({
onRemoveAttachment,
left,
submitAccessory,
+ footer,
onStop,
stopping,
references = [],
@@ -209,6 +210,8 @@ export function AgentChatComposer({
left?: ReactNode;
/** 发送按钮左侧的附属控件,例如上下文用量环。 */
submitAccessory?: ReactNode;
+ /** 输入区底部的附加内容(例如连接器条)。 */
+ footer?: ReactNode;
/** 供「@」插入的画布节点/素材/技能引用候选(可选,默认空,缺省时退化为普通输入框) */
references?: CanvasResourceReference[];
/** 供「/」弹出的技能候选(可选) */
@@ -339,13 +342,17 @@ export function AgentChatComposer({
return (
event.stopPropagation()}>
+ {/* 有底部附加内容时,外壳托住输入卡片与底部条,形成一张整卡。 */}
+
+ {/* 整卡模式下发送光环包住整张卡,避免画在输入区与连接器之间形成一条线。 */}
+ {footer && sending && !reducedMotion ?
: null}
- {sending && !reducedMotion ?
: null}
+ {!footer && sending && !reducedMotion ?
: null}
{attachments.length ? (
{attachments.map((item, index) => (
@@ -522,6 +529,8 @@ export function AgentChatComposer({
+ {footer}
+
{previewAttachment ?
setPreviewAttachment(null)} /> : null}
);
diff --git a/web/src/components/canvas/canvas-cloud-agent-connectors.css b/web/src/components/canvas/canvas-cloud-agent-connectors.css
new file mode 100644
index 000000000..ee80b5c5a
--- /dev/null
+++ b/web/src/components/canvas/canvas-cloud-agent-connectors.css
@@ -0,0 +1,96 @@
+/* Agent 连接器样式 - 极简紧凑版:高度压缩,图标更小,左右紧密排列 */
+
+.agent-connectors {
+ display: flex;
+ align-items: center;
+ gap: var(--space-1-half);
+ padding: var(--space-1-half) var(--space-3) var(--space-2-half);
+ color: var(--agent-caption);
+}
+
+.agent-connectors-label {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 10.5px;
+ font-weight: 500;
+ flex-shrink: 0;
+ opacity: 0.7;
+}
+
+.agent-connectors-label > svg {
+ width: 13px;
+ height: 13px;
+}
+
+.agent-connectors-list {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex: 1;
+ min-width: 0;
+}
+
+.agent-connector-item {
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ display: grid;
+ place-items: center;
+ cursor: pointer;
+ flex-shrink: 0;
+ transition: transform 0.16s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.agent-connector-item > svg {
+ width: 20px;
+ height: 20px;
+}
+
+.agent-connector-item:hover {
+ transform: translateY(-1px) scale(1.08);
+}
+
+.agent-connector-item:active {
+ transform: scale(0.96);
+}
+
+.agent-connectors-close {
+ width: 22px;
+ height: 22px;
+ padding: 0;
+ border: 0;
+ border-radius: 999px;
+ background: transparent;
+ color: var(--agent-caption);
+ display: grid;
+ place-items: center;
+ cursor: pointer;
+ flex-shrink: 0;
+ transition: all 0.14s ease;
+ opacity: 0.6;
+}
+
+.agent-connectors-close > svg {
+ width: 12px;
+ height: 12px;
+}
+
+.agent-connectors-close:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--agent-ink) 6%, transparent);
+}
+
+.agent-connectors-close:active {
+ transform: scale(0.92);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .agent-connector-item,
+ .agent-connector-item:hover,
+ .agent-connector-item:active {
+ transform: none;
+ }
+}
diff --git a/web/src/components/canvas/canvas-cloud-agent-connectors.tsx b/web/src/components/canvas/canvas-cloud-agent-connectors.tsx
new file mode 100644
index 000000000..21cba6a86
--- /dev/null
+++ b/web/src/components/canvas/canvas-cloud-agent-connectors.tsx
@@ -0,0 +1,130 @@
+// Agent 连接器:输入框底部的第三方协作工具入口条。
+//
+// 流光溢彩渐变背景 + 紧凑排列的品牌图标,无独立按钮背景。
+// 品牌图标为内联 SVG 占位,避免外链 favicon 依赖。
+
+import { App } from "antd";
+import { Cable, X } from "lucide-react";
+import type { ReactNode } from "react";
+import { Tooltip } from "@/components/ui/base/tooltip";
+import "./canvas-cloud-agent-connectors.css";
+
+type AgentConnector = {
+ id: string;
+ name: string;
+ icon: ReactNode;
+};
+
+function NotionIcon() {
+ return (
+
+ );
+}
+
+function FeishuIcon() {
+ return (
+
+ );
+}
+
+function BasecampIcon() {
+ return (
+
+ );
+}
+
+function ObsidianIcon() {
+ return (
+
+ );
+}
+
+function GitHubIcon() {
+ return (
+
+ );
+}
+
+function TencentDocsIcon() {
+ return (
+
+ );
+}
+
+function SlackIcon() {
+ return (
+
+ );
+}
+
+const AGENT_CONNECTORS: AgentConnector[] = [
+ { id: "notion", name: "Notion", icon:
},
+ { id: "feishu", name: "飞书", icon:
},
+ { id: "basecamp", name: "Basecamp", icon:
},
+ { id: "obsidian", name: "Obsidian", icon:
},
+ { id: "github", name: "GitHub", icon:
},
+ { id: "tencent-docs", name: "腾讯文档", icon:
},
+ { id: "slack", name: "Slack", icon:
},
+];
+
+/** 输入框底部的连接器条,流光溢彩背景,紧凑图标阵列。 */
+export function AgentConnectorsBar({ onClose }: { onClose: () => void }) {
+ const { message } = App.useApp();
+ return (
+
+
+
+ 连接器
+
+
+ {AGENT_CONNECTORS.map((connector) => (
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/web/src/components/canvas/canvas-cloud-agent-events.ts b/web/src/components/canvas/canvas-cloud-agent-events.ts
index 08a6e1e83..a57bd9a44 100644
--- a/web/src/components/canvas/canvas-cloud-agent-events.ts
+++ b/web/src/components/canvas/canvas-cloud-agent-events.ts
@@ -19,6 +19,25 @@ export function positiveNumber(value: string) {
return Number.isFinite(number) && number > 0 ? number : undefined;
}
+function compactionMessageID(event: AgentEvent, payload: Record
) {
+ const compactionID = typeof payload.compactionId === "string" ? payload.compactionId.trim() : "";
+ const eventKey = event.eventId || (Number.isFinite(event.seq) ? `seq-${event.seq}` : "event");
+ return compactionID ? `compaction-${event.runId}-${compactionID}` : `compaction-${event.runId}-${eventKey}`;
+}
+
+function compactTokenLabel(value: unknown) {
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return "";
+ if (value >= 1_000_000) return `${Math.round(value / 100_000) / 10}M`;
+ if (value >= 1_000) return `${Math.round(value / 100) / 10}K`;
+ return Math.round(value).toLocaleString("zh-CN");
+}
+
+function compactionCompletedText(payload: Record) {
+ const before = compactTokenLabel(payload.tokensBefore);
+ const after = compactTokenLabel(payload.estimatedTokensAfter);
+ return before && after ? `上下文压缩完成(${before} → ${after} Token)` : "上下文压缩完成";
+}
+
export function applyAgentEvent(
event: AgentEvent,
setMessages: Dispatch>,
@@ -51,8 +70,8 @@ export function applyAgentEvent(
if (terminal) {
setMessages((current) => current.map((message) => (message.id === `plan-${event.runId}` && message.planItems?.length ? { ...message, planTerminal: true, streaming: false } : message)));
}
- if (terminal && nextStatus === "failed" && payload.failureMessage) {
- setMessages((current) => appendAgentError(current, `terminal-${event.runId}`, String(payload.failureMessage), "Agent 执行失败", { severity: "error" }));
+ if (nextStatus === "failed") {
+ setMessages((current) => upsertRunFailure(current, event, 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 || "" }));
@@ -125,6 +144,65 @@ export function applyAgentEvent(
setPrompt?.((current) => (current.trim() ? current : text));
return;
}
+ if (event.type === "context_compaction_requested") {
+ const id = compactionMessageID(event, payload);
+ const compactionId = typeof payload.compactionId === "string" ? payload.compactionId : undefined;
+ setMessages((current) =>
+ appendUniqueMessage(current, {
+ id,
+ role: "system",
+ text: "正在整理上下文",
+ compaction: { status: "pending", compactionId, reason: String(payload.reason || "") },
+ }),
+ );
+ return;
+ }
+ if (event.type === "context_compacted") {
+ const explicitId = typeof payload.compactionId === "string" && payload.compactionId.trim() ? `compaction-${event.runId}-${payload.compactionId}` : undefined;
+ const fallbackId = compactionMessageID(event, payload);
+ const tokensBefore = typeof payload.tokensBefore === "number" ? payload.tokensBefore : undefined;
+ const estimatedTokensAfter = typeof payload.estimatedTokensAfter === "number" ? payload.estimatedTokensAfter : undefined;
+ const compaction = { status: "completed" as const, compactionId: typeof payload.compactionId === "string" ? payload.compactionId : undefined, tokensBefore, estimatedTokensAfter, reason: String(payload.reason || "") };
+ setMessages((current) => {
+ const existing = explicitId
+ ? current.findIndex((item) => item.id === explicitId)
+ : ([...current]
+ .map((item, index) => ({ item, index }))
+ .reverse()
+ .find(({ item }) => item.compaction?.status === "pending")?.index ?? -1);
+ const id = existing >= 0 ? current[existing]?.id || fallbackId : fallbackId;
+ if (existing >= 0) {
+ const next = [...current];
+ next[existing] = { ...next[existing], text: compactionCompletedText(payload), compaction };
+ return next;
+ }
+ return appendUniqueMessage(current, { id, role: "system", text: compactionCompletedText(payload), compaction });
+ });
+ return;
+ }
+ if (event.type === "context_compaction_failed") {
+ const explicitId = typeof payload.compactionId === "string" && payload.compactionId.trim() ? `compaction-${event.runId}-${payload.compactionId}` : undefined;
+ const fallbackId = compactionMessageID(event, payload);
+ const aborted = payload.cancelled === true || payload.aborted === true;
+ const errorText = String(payload.errorMessage || payload.text || "上下文压缩失败");
+ setMessages((current) => {
+ const existing = explicitId
+ ? current.findIndex((item) => item.id === explicitId)
+ : ([...current]
+ .map((item, index) => ({ item, index }))
+ .reverse()
+ .find(({ item }) => item.compaction?.status === "pending")?.index ?? -1);
+ const id = existing >= 0 ? current[existing]?.id || fallbackId : fallbackId;
+ const compaction = { status: aborted ? ("aborted" as const) : ("failed" as const), compactionId: typeof payload.compactionId === "string" ? payload.compactionId : undefined, reason: String(payload.reason || ""), errorMessage: errorText };
+ if (existing >= 0) {
+ const next = [...current];
+ next[existing] = { ...next[existing], text: errorText, errorSeverity: "warning", compaction };
+ return next;
+ }
+ return appendUniqueMessage(current, { id, role: "system", text: errorText, errorSeverity: "warning", compaction });
+ });
+ return;
+ }
if (event.type === "user_question") {
const options = Array.isArray(payload.options)
? (payload.options as Array<{ label?: unknown; detail?: unknown }>).map((option) => ({ label: String(option?.label || "").trim(), detail: option?.detail === undefined ? undefined : String(option.detail) })).filter((option) => option.label)
@@ -192,7 +270,7 @@ export function applyAgentEvent(
if (payload.operation === "generate_media_submit" || payload.operation === "generate_media_complete") return;
const { canvasPatch: _patch, ...detail } = payload;
const id = payload.callId ? `canvas-${event.runId}-${payload.callId}` : event.eventId;
- setMessages((current) => appendUniqueMessage(current, { id, role: "tool", title: "canvas_apply_ops", text: "画布操作已完成", detail: { ...detail, eventType: event.type } }));
+ setMessages((current) => appendUniqueMessage(current, { id, role: "tool", title: "canvas_apply_ops", text: String(payload.text || "画布操作已完成"), detail: { ...detail, eventType: event.type } }));
return;
}
if (event.type.startsWith("tool_") && agentToolRetry(payload)) {
@@ -243,7 +321,23 @@ export function applyAgentEvent(
}
return;
}
- if (event.type === "run_failed" || event.type === "error") setMessages((current) => appendAgentError(current, event.eventId, text || "Agent 执行失败"));
+ if (event.type === "run_failed" || event.type === "error") setMessages((current) => upsertRunFailure(current, event, text));
+}
+
+function upsertRunFailure(current: CloudAgentChatMessage[], event: AgentEvent, text: string) {
+ const id = `terminal-${event.runId}`;
+ const message: CloudAgentChatMessage = { id, role: "error", ...agentErrorPresentation(text), errorSeverity: "error" };
+ // 失败事件和终态快照描述的是同一次失败;重放时同时收敛已缓存的事件行。
+ const next = current.filter((item) => item.role !== "error" || item.id !== event.eventId || item.id === id);
+ const index = next.findIndex((item) => item.id === id);
+ if (index < 0) return [...next, message];
+ const previous = next[index];
+ const hasDetails = message.text !== message.title;
+ const hadDetails = previous.text !== previous.title;
+ // 快照的公开原因优先;乱序到达的通用通知不能覆盖更完整的说明。
+ if (!hasDetails || (event.type !== "run_status" && hadDetails)) return next;
+ next[index] = message;
+ return next;
}
export function toolDetailRecord(value: unknown): Record {
diff --git a/web/src/components/canvas/canvas-cloud-agent-panel-parts.tsx b/web/src/components/canvas/canvas-cloud-agent-panel-parts.tsx
index 3739c1821..fbc10d5eb 100644
--- a/web/src/components/canvas/canvas-cloud-agent-panel-parts.tsx
+++ b/web/src/components/canvas/canvas-cloud-agent-panel-parts.tsx
@@ -165,7 +165,7 @@ export function AgentContextRing({ view }: { view: AgentContextUsageView }) {
const protocolBytes = formatContextBytes(view.protocolBytes);
const usedRatio = view.ratio === undefined ? 0 : Math.max(0, Math.min(1, view.ratio));
const phaseLabel = CONTEXT_PHASE_LABEL[view.phase];
- const sourceLabel = view.tokenSource === "provider" ? "模型实测校准" : view.estimate ? "本地估算" : "未测量";
+ const sourceLabel = view.tokenSource === "pi" ? "Pi 会话估算" : "未测量";
const usageHeading = view.ratio !== undefined ? `上下文已用 ${percent}` : view.phase === "idle" ? "上下文用量" : view.phase === "unknown" ? "上下文窗口未知" : `上下文${view.label}`;
return (
@@ -235,11 +235,11 @@ export function AgentContextRing({ view }: { view: AgentContextUsageView }) {
{sourceLabel}
- {view.estimate ? " · 不是计费 Token" : " · 预计下次请求"}
+ {view.tokenSource === "pi" ? " · 不是计费 Token" : ""}
{view.compactAtTokens ? 压缩线 {formatContextCount(view.compactAtTokens)} : null}
- {view.lastCompaction ? 本轮已完成一次上下文压缩,下一次读数会刷新。
: null}
+ {view.compactionError ? {view.compactionError}
: view.lastCompaction ? 本轮已完成一次上下文压缩,下一次读数会刷新。
: null}
}
>
diff --git a/web/src/components/canvas/canvas-cloud-agent-panel.tsx b/web/src/components/canvas/canvas-cloud-agent-panel.tsx
index b9bb7ba1c..8043b999e 100644
--- a/web/src/components/canvas/canvas-cloud-agent-panel.tsx
+++ b/web/src/components/canvas/canvas-cloud-agent-panel.tsx
@@ -29,6 +29,7 @@ import {
type AgentRun,
} from "@/services/api/agent";
import { agentApprovalMatchesSettings, agentApprovalTargetGenerating } from "@/lib/canvas/agent-media-approval";
+import { appendAgentPromptPrefill } from "@/lib/canvas/agent-prompt-prefill";
import type { CanvasNodeData } from "@/types/canvas";
import type { AgentMediaSettings } from "@/services/api/agent";
import { addSkill, listAddedSkills, listSkillLibraryCategories, listSkills, listSkillPresets, type Skill, type SkillCategory, type SkillLibraryCategory, type SkillPreset } from "@/services/api/skills";
@@ -56,6 +57,7 @@ import { useAgentPanelLayout } from "./use-agent-panel-layout";
import "./canvas-cloud-agent.css";
import { appendAgentError, appendUniqueMessage, applyAgentEvent, positiveNumber, type ApprovalState } from "./canvas-cloud-agent-events";
import { AgentContextRing, AgentConversation, AgentHeader, AgentHistory, AgentLauncher, ComposerControls } from "./canvas-cloud-agent-panel-parts";
+import { AgentConnectorsBar } from "./canvas-cloud-agent-connectors";
type CloudAgentPanelProps = {
canvasId: string;
@@ -64,7 +66,8 @@ type CloudAgentPanelProps = {
selectedNodeIds: string[];
references: CanvasResourceReference[];
open: boolean;
- prefillPrompt?: string;
+ /** 外部预填请求(如右键“发送到 Agent”);id 变化即追加一次 text。 */
+ prefillRequest?: { id: number; text: string } | null;
onOpen: () => void;
onCollapse: () => void;
onFocusNode?: (nodeId: string) => void;
@@ -74,7 +77,7 @@ type CloudAgentPanelProps = {
};
type AgentPanelView = "chat" | "history" | "settings";
-export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, selectedNodeIds, references, open, prefillPrompt, onOpen, onCollapse, onFocusNode, canvasNodes, runningNodeId }: CloudAgentPanelProps) {
+export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, selectedNodeIds, references, open, prefillRequest, onOpen, onCollapse, onFocusNode, canvasNodes, runningNodeId }: CloudAgentPanelProps) {
const userId = useUserStore((state) => state.user?.id);
const theme = canvasThemes[useActiveTheme()];
const config = useEffectiveConfig();
@@ -87,7 +90,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, se
const [connectionEpoch, setConnectionEpoch] = useState(0);
const [messages, setMessages] = useState
([]);
const [prompt, setPrompt] = useState("");
- const lastPrefillPromptRef = useRef("");
+ const lastPrefillIdRef = useRef(0);
const [reasoningMode, setReasoningMode] = useState("off");
const [profileView, setProfileView] = useState(null);
const [profileLoading, setProfileLoading] = useState(false);
@@ -128,6 +131,8 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, se
const [scenePresets, setScenePresets] = useState([]);
const [presetApplyingId, setPresetApplyingId] = useState("");
const presetApplyingRef = useRef(null);
+ // 连接器条默认展示,关闭后本次会话内不再显示。
+ const [connectorsVisible, setConnectorsVisible] = useState(true);
const panelLayout = useAgentPanelLayout();
const lastSeqRef = useRef(0);
const canvasSyncRef = useRef | null>(null);
@@ -304,12 +309,12 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, se
const statusColor = status === "failed" ? "#e66b6b" : status === "rejected" || status === "cancelled" ? theme.node.muted : status === "waiting_approval" ? "#d6a24a" : status === "running" || status === "queued" ? "#69c29b" : theme.node.muted;
useEffect(() => {
- const value = prefillPrompt?.trim();
- if (!value || value === lastPrefillPromptRef.current) return;
- lastPrefillPromptRef.current = value;
- setPrompt(value);
+ if (!prefillRequest || prefillRequest.id === lastPrefillIdRef.current) return;
+ lastPrefillIdRef.current = prefillRequest.id;
+ if (!prefillRequest.text.trim()) return;
+ setPrompt((current) => appendAgentPromptPrefill(current, prefillRequest.text));
setView("chat");
- }, [prefillPrompt]);
+ }, [prefillRequest]);
useEffect(() => {
if (!open || view !== "chat") setSkillsOpen(false);
@@ -1032,6 +1037,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, se
slashSkills={installedSkills}
includeAssetLibrary={false}
submitAccessory={}
+ footer={connectorsVisible ? setConnectorsVisible(false)} /> : null}
left={
summary {
+ display: flex;
+ width: fit-content;
+ align-items: center;
+ gap: var(--space-1);
+ border-radius: var(--r-xs);
+ cursor: pointer;
+ list-style: none;
+}
+
+.agent-error-diagnostics > summary::-webkit-details-marker {
+ display: none;
+}
+
+.agent-error-diagnostics > summary:hover {
+ color: var(--agent-ink);
+}
+
+.agent-error-diagnostics > summary:focus-visible {
+ outline: 2px solid var(--agent-focus-ring);
+ outline-offset: 3px;
+}
+
+.agent-error-diagnostics[open] > summary svg {
+ transform: rotate(180deg);
+}
+
+.agent-error-diagnostics-body {
+ margin-top: var(--space-2);
+ padding: var(--space-2) var(--space-3);
+ border-radius: var(--r-md);
+ background: var(--agent-surface-soft);
+ overflow-wrap: anywhere;
+}
+
+.agent-error-diagnostics-body p {
+ margin: 0 0 var(--space-1);
+}
+
+.agent-error-diagnostics-body code {
+ user-select: all;
+}
+
+.agent-error-retry.ant-btn {
+ height: auto;
+ margin-top: var(--space-2);
+ padding: var(--space-1) var(--space-2);
+ font-size: var(--fs-label);
}
.agent-message-user {
@@ -660,6 +730,17 @@
box-shadow: 0 0 0 2px color-mix(in srgb, var(--agent-accent, var(--foreground)) 35%, transparent);
}
+/* 底部托盘:外壳与输入卡片同为一张卡,输入卡片嵌在上方,底部条直接落在托盘里;无分隔线、无间距。 */
+.agent-composer-shell[data-has-footer] {
+ border-radius: var(--agent-radius-surface, var(--radius-xl));
+ background: color-mix(in srgb, var(--agent-ink) 3%, var(--agent-material-solid, var(--background)));
+}
+
+/* 整卡模式下输入区焦点环会落在输入区与连接器之间形成一条线,不画焦点环。 */
+.agent-composer-shell[data-has-footer] > .agent-composer-surface:focus-within {
+ box-shadow: none;
+}
+
.agent-plan-bar {
border: 0;
background: var(--agent-surface-soft, color-mix(in srgb, var(--foreground) 4%, transparent));
diff --git a/web/src/components/canvas/canvas-mention-menu.tsx b/web/src/components/canvas/canvas-mention-menu.tsx
index 0c9a946c8..e44caac7b 100644
--- a/web/src/components/canvas/canvas-mention-menu.tsx
+++ b/web/src/components/canvas/canvas-mention-menu.tsx
@@ -174,10 +174,17 @@ export function MentionMenu({
}
export function MentionReferenceList({ references, activeReferenceId, onSelect }: { references: CanvasResourceReference[]; activeReferenceId?: string; onSelect: (reference: CanvasResourceReference) => void }) {
+ const activeRef = useRef(null);
+
+ useLayoutEffect(() => {
+ activeRef.current?.scrollIntoView({ block: "nearest", behavior: "smooth" });
+ }, [activeReferenceId]);
+
if (!references.length) return 没有匹配的引用
;
return references.map((reference) => (