From 4496a835a3c710d09eef0a757cbe0d164ad91c8f Mon Sep 17 00:00:00 2001 From: Wang Jason Date: Tue, 19 May 2026 21:34:15 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix(widget):=20=E6=8F=90=E5=89=8D=E8=AF=86?= =?UTF-8?q?=E5=88=AB=20partial=20fence=20=E5=89=8D=E7=BC=80=EF=BC=8C?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E6=B5=81=E5=BC=8F=E5=88=86=E5=9D=97=E6=97=B6?= =?UTF-8?q?=E9=A2=91=E7=B9=81=20unmount/mount?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 流式 markdown 块解析在 chunk 起始位置遇到 ``` / ``` a 等 fence 头时,会先被 判定为非 widget 再被改判,触发 Vue 组件 mount → unmount → mount。在 widget 延迟加载场景下,第二次 mount 会因 pending instanceId 已被回收而 register 失败, 表现为 assistant-widget 偶发 "no pending"。 修复:在 isWidgetBlock 中将明显的 partial widget fence 前缀(裸 ``` 起头但行内 还未补完 widget 标识、或刚写到 ``` a 等中间态)识别为 WIDGET-PENDING, 块类型保持稳定,组件只 mount 一次直到 fence 完整。 Co-authored-by: Cursor --- .../text-content/utils/block-parser.ts | 3 +- .../src/composition/use-widget-content.ts | 41 ++++++++++++++++--- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/x-ui/components/bubble/src/components/text-content/utils/block-parser.ts b/packages/x-ui/components/bubble/src/components/text-content/utils/block-parser.ts index 62cd9b68a..8ba0c795c 100644 --- a/packages/x-ui/components/bubble/src/components/text-content/utils/block-parser.ts +++ b/packages/x-ui/components/bubble/src/components/text-content/utils/block-parser.ts @@ -1,6 +1,7 @@ import { formatDisplayText, isWidgetBlock, + isWidgetBlockComplete, parseAssistantWidget, getWidgetImplOptions, } from '../../../composition/use-widget-content'; @@ -31,7 +32,7 @@ export function parseBlocks(text: string): ParsedBlock[] { for (const part of assistantParts) { if (isWidgetBlock(part)) { - if (part.endsWith('```')) { + if (isWidgetBlockComplete(part)) { // assistant-widget 块 const widgetData = parseAssistantWidget(part); blocks.push({ diff --git a/packages/x-ui/components/bubble/src/composition/use-widget-content.ts b/packages/x-ui/components/bubble/src/composition/use-widget-content.ts index d557332c4..74685a663 100644 --- a/packages/x-ui/components/bubble/src/composition/use-widget-content.ts +++ b/packages/x-ui/components/bubble/src/composition/use-widget-content.ts @@ -41,17 +41,46 @@ function normalizeWidgetBlock(text: string): string { return text.replace(/```\s*\n+\s*assistant-widget/g, '``` assistant-widget'); } +/** 完整 widget 代码块:`` ``` assistant-widget ... ``` `` 且结尾闭围栏不与开围栏重叠 */ +const WIDGET_COMPLETE_REGEX = /^```\s*assistant-widget[\s\S]*?```$/; +/** 不完整 widget 代码块:以 `` ``` assistant-widget`` 开头但未闭合 */ +const WIDGET_INCOMPLETE_REGEX = /^```\s*assistant-widget/; +/** + * 「正在变成 widget 的前缀」:流式渲染过程中,当前累计文本是 `` ``` ``、`` ``` a``、`` ``` assistant`` 这类 + * `assistant-widget` 的前缀子串,后续 chunk 可能补齐成 widget。 + * 不识别这种前缀会让 block 类型经历 `TextBlock → AssistantWidgetPending` 的硬切换, + * 给 Vue 渲染层制造卸载/重挂载副作用,进而丢失 widget JS 的动态加载。 + */ +const WIDGET_PARTIAL_PREFIX_REGEX = /^```\s*([a-zA-Z0-9-]*)$/; + +function matchesWidgetPartialPrefix(text: string): boolean { + const m = text.match(WIDGET_PARTIAL_PREFIX_REGEX); + if (!m) return false; + const lang = (m[1] ?? '').toLowerCase(); + return lang === '' || 'assistant-widget'.startsWith(lang); +} + +/** + * 判断文本是否为完整的 widget 代码块 + * 用于区分流式过程中的 pending / complete 状态:单纯 `endsWith('```')` 会把 + * 仅有一个开围栏 `` ``` `` 的中间态误判为完整 widget。 + */ +export function isWidgetBlockComplete(text: string): boolean { + if (!text) return false; + return WIDGET_COMPLETE_REGEX.test(text); +} + /** * 判断文本是否为 widget 代码块 - * 支持标准格式和不完整格式(流式渲染中途被截断的情况) + * 支持标准格式、不完整格式(流式渲染中途被截断),以及流式起步时仅有围栏前缀的情况 */ export function isWidgetBlock(text: string): boolean { if (!text) return false; - // 匹配完整的 widget 代码块(assistant-widget) - const completeRegex = /^```\s*assistant-widget[\s\S]*?```$/; - // 匹配不完整的 widget 代码块(截断情况) - const incompleteRegex = /^```\s*assistant-widget/; - return completeRegex.test(text) || incompleteRegex.test(text); + return ( + WIDGET_COMPLETE_REGEX.test(text) || + WIDGET_INCOMPLETE_REGEX.test(text) || + matchesWidgetPartialPrefix(text) + ); } /** -- Gitee From 2f6c672e041869a637a9a6803cb773af95d15eb8 Mon Sep 17 00:00:00 2001 From: Wang Jason Date: Wed, 20 May 2026 16:13:16 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(started-todo):=20listStatus=3Dcomplete?= =?UTF-8?q?d=20=E6=95=B4=E4=BD=93=E5=AE=8C=E6=88=90=E6=80=81=E8=A7=86?= =?UTF-8?q?=E8=A7=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ThoughtChainContent / MessageContentTodo 新增 listStatus (active|completed) + completedCount + totalCount 字段,未显式传入时 组件内部按 todoList.filter(status='Done') 自动兜底。 - FXStartedTodo: listStatus=completed 时根节点加 is-completed 类、 header 圆形 icon 强制走 StatusIcon(Done)(不走单项 haiyueLogoUrl), 并在标题后追加「已完成 (N/N)」chip;不影响展开/收起,进行中态行为 保持不变。 - BubbleThoughtChain: 透传 listStatus / completedCount / totalCount 给 FXStartedTodo。 - style.scss: .f-chat-started-todo-completed-chip 主流任务卡片绿色 小圆角 chip(参考 Linear/Cursor),.is-completed 仅做轻量点缀, 避免整卡变色抢眼。 Co-authored-by: Cursor --- .../bubble-thought-chain.component.tsx | 3 ++ .../bubble/src/composition/types.ts | 10 +++++ .../src/started-todo.component.tsx | 39 ++++++++++++++++++- .../started-todo/src/started-todo.props.ts | 19 ++++++++- packages/x-ui/components/style.scss | 27 +++++++++++++ .../components/todo/src/composition/type.ts | 9 +++++ 6 files changed, 105 insertions(+), 2 deletions(-) diff --git a/packages/x-ui/components/bubble/src/components/bubble-thought-chain/bubble-thought-chain.component.tsx b/packages/x-ui/components/bubble/src/components/bubble-thought-chain/bubble-thought-chain.component.tsx index 0d81f50dc..09defa09b 100644 --- a/packages/x-ui/components/bubble/src/components/bubble-thought-chain/bubble-thought-chain.component.tsx +++ b/packages/x-ui/components/bubble/src/components/bubble-thought-chain/bubble-thought-chain.component.tsx @@ -54,6 +54,9 @@ export default defineComponent({ showLabel={false} showLoading={true} initExpanded={true} + listStatus={props.content?.listStatus} + completedCount={props.content?.completedCount} + totalCount={props.content?.totalCount} /> ); }; diff --git a/packages/x-ui/components/bubble/src/composition/types.ts b/packages/x-ui/components/bubble/src/composition/types.ts index 1fb27f2e0..aa671706c 100644 --- a/packages/x-ui/components/bubble/src/composition/types.ts +++ b/packages/x-ui/components/bubble/src/composition/types.ts @@ -121,6 +121,16 @@ export interface ThoughtChainContent { * 用于 `Todo` 规划标题:`message` 仅为文案,与 `taskList` 根任务状态无关,避免 `NotStart` 被画成「未完成任务」。 */ showHeaderStatusIcon?: boolean; + /** + * 待办列表整体状态:由网关 `agent/todo-list.content.listStatus` 透传。 + * `completed` 时 `FXStartedTodo` 走「整体完成」分支:header 圆形 icon 换为对勾、 + * 标题后追加「已完成 (N/N)」chip,根节点加 `is-completed` 类(不自动收起)。 + */ + listStatus?: 'active' | 'completed'; + /** 已完成项数(用于完成 chip 文案,可由 todoList 自动计算时不必显式传) */ + completedCount?: number; + /** 总项数(同上,未显式传时回退到 `todoList.length`) */ + totalCount?: number; } /** 思考过程 */ diff --git a/packages/x-ui/components/started-todo/src/started-todo.component.tsx b/packages/x-ui/components/started-todo/src/started-todo.component.tsx index d078066e6..a71c6472a 100644 --- a/packages/x-ui/components/started-todo/src/started-todo.component.tsx +++ b/packages/x-ui/components/started-todo/src/started-todo.component.tsx @@ -125,6 +125,21 @@ export default defineComponent({ ) ); } + const isListCompleted = computed(() => props.listStatus === 'completed'); + + /** + * 完成数/总数:优先用业务方显式传入;否则按 `todoList` 中 `status === 'Done'` 自动统计。 + * 仅在 `listStatus === 'completed'` 渲染 chip 时使用,进行中态不展示数字。 + */ + const completedTotal = computed(() => { + const list = props.todoList ?? []; + const total = props.totalCount ?? list.length; + const done = + props.completedCount ?? + list.filter((it: TodoWorkItem) => it?.status === 'Done').length; + return { done, total }; + }); + function renderLoadingIcon() { if (!props.showLoading) { return null; @@ -133,19 +148,40 @@ export default defineComponent({ if (props.loading) { return props.loading; } + // 整体已完成:header 圆形 icon 直接显示勾选(不走 haiyueLogoUrl 单项分支) + if (isListCompleted.value) { + return ; + } if (props.status === 'Done') { return } return ; } + function renderCompletedChip() { + if (!isListCompleted.value) return null; + const { done, total } = completedTotal.value; + const label = total > 0 ? `已完成 (${done}/${total})` : '已完成'; + return ( + + {label} + + ); + } + return () => { return (
} {renderLoadingIcon()} + {renderCompletedChip()} {renderExpand()}
{props.detailViewMode === "expand" && ( diff --git a/packages/x-ui/components/started-todo/src/started-todo.props.ts b/packages/x-ui/components/started-todo/src/started-todo.props.ts index a2b046b0f..58ad40f27 100644 --- a/packages/x-ui/components/started-todo/src/started-todo.props.ts +++ b/packages/x-ui/components/started-todo/src/started-todo.props.ts @@ -51,8 +51,25 @@ export const startedTodoProps = { outputMode: { type: String as PropType, default: 'streaming' as OutputMode + }, + /** + * 待办列表整体状态:`completed` 时 header 圆形 icon 换为对勾、追加「已完成 (N/N)」chip, + * 根节点添加 `is-completed` 类,便于业务方在 SCSS 做轻量点缀;不影响展开/收起。 + */ + listStatus: { + type: String as PropType<'active' | 'completed'>, + default: undefined + }, + /** 已完成项数:若未显式传入,组件内部按 `todoList.filter(status='Done').length` 自动计算 */ + completedCount: { + type: Number, + default: undefined + }, + /** 总项数:若未显式传入,组件内部回退到 `todoList.length` */ + totalCount: { + type: Number, + default: undefined } - }; diff --git a/packages/x-ui/components/style.scss b/packages/x-ui/components/style.scss index 2c7d8df40..9e4401460 100644 --- a/packages/x-ui/components/style.scss +++ b/packages/x-ui/components/style.scss @@ -4052,6 +4052,33 @@ color: $text-caption;; } } +/* 整体待办完成态(由 ThoughtChainContent.listStatus === 'completed' 触发) */ +.f-chat-message-started-todo.is-completed { + .f-chat-message-started-todo--header { + .f-chat-started-todo-message { + color: $text-primary; + } + } +} + +/* 整体完成 chip:紧跟标题后,浅色背景,不抢眼 */ +.f-chat-started-todo-completed-chip { + display: inline-flex; + align-items: center; + flex-shrink: 0; + margin-left: 6px; + padding: 1px 8px; + font-size: 12px; + font-weight: 500; + line-height: 18px; + color: #16a34a; /* 主流任务卡片完成色(参考 Linear / Cursor) */ + background-color: rgba(22, 163, 74, 0.1); + border: 1px solid rgba(22, 163, 74, 0.2); + border-radius: 10px; + white-space: nowrap; + user-select: none; +} + .f-chat-todo-item{ &>.f-chat-message-started-todo{ box-shadow: none; diff --git a/packages/x-ui/components/todo/src/composition/type.ts b/packages/x-ui/components/todo/src/composition/type.ts index a8edddde7..ef56974cc 100644 --- a/packages/x-ui/components/todo/src/composition/type.ts +++ b/packages/x-ui/components/todo/src/composition/type.ts @@ -26,10 +26,19 @@ export interface TodoWorkItem { title?: string; } +/** 待办列表整体状态:`active` 进行中(含创建/更新);`completed` 整个列表全部完成 */ +export type TodoListStatus = 'active' | 'completed'; + /** 待办任务类型消息内容 */ export interface MessageContentTodo { type: 'Todo'; /** 消息标题,显示在列表外部 */ message: string; items: TodoWorkItem[]; + /** + * 整体待办状态:由网关 `agent/todo-list` content.listStatus 透传。 + * `completed` 时上层(如 BubbleThoughtChain / FXStartedTodo)应展示「全部完成」视觉 + * (header icon 换勾、追加 chip 等),不再自动收起。 + */ + listStatus?: TodoListStatus; } -- Gitee From e637597ad405b3602b7183cc1400fabb4acb82fb Mon Sep 17 00:00:00 2001 From: Wang Jason Date: Wed, 20 May 2026 17:10:25 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix(todo):=20=E6=B8=B2=E6=9F=93=20descripti?= =?UTF-8?q?on=20=E5=89=AF=E6=96=87=E6=9C=AC=EF=BC=8C=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E7=BB=9F=E4=B8=80=E7=BB=BF=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - todo-list-item-view 注册并 watch props.message 为 detail, 让流式模式也能逐字输出 description;render 在两种分支下都 把 displayDetail 作为额外的描述行渲染(之前的 displayDetail computed 已存在但未接到 DOM)。 - style.scss 把 NotStart / Working / Done 三种状态图标的基色 从 $color-accent(蓝)切到 #16a34a 绿色,与主流任务卡片视觉一致; 保留 .is-done / .is-completed 的语义级覆盖作为兜底。 Co-authored-by: Cursor --- packages/x-ui/components/style.scss | 18 +++++++++++++++--- .../todo-list-item-view.component.tsx | 16 +++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/x-ui/components/style.scss b/packages/x-ui/components/style.scss index 9e4401460..25893c177 100644 --- a/packages/x-ui/components/style.scss +++ b/packages/x-ui/components/style.scss @@ -1913,6 +1913,11 @@ $color-accent-red: #f46160; color: $text-muted-dark; } + /* 完成项的对勾图标统一走绿色,与主流任务卡片(Linear / Cursor)一致 */ + &.is-done .f-chat-todo-status-done { + color: #16a34a; + } + .f-chat-todo-task-wrapper { flex: 1; min-width: 0; @@ -1992,9 +1997,12 @@ $color-accent-red: #f46160; color: rgba(0, 0, 0, 0.55); } +/* Todo 状态图标统一走绿色(NotStart / Working / Done),与主流任务卡片视觉一致 */ +$todo-status-color: #16a34a; + /* NotStart: 空心圆 - farrisicon 未找到时 CSS 占位 */ .f-chat-todo-status-notstart { - border: 1px solid $color-accent; + border: 1px solid $todo-status-color; border-radius: 50%; background: transparent; } @@ -2002,7 +2010,7 @@ $color-accent-red: #f46160; /* Working: 转圈 - farrisicon 未找到时 CSS 占位 */ .f-chat-todo-status-working { font-size: 16px; - color: $color-accent; + color: $todo-status-color; font-family: FarrisXIcons; &::after { content: "\e6da"; @@ -2020,7 +2028,7 @@ $color-accent-red: #f46160; /* Done: 对号圆 - farrisicon 未找到时 CSS 占位 */ .f-chat-todo-status-done { font-size: 16px; - color: $color-accent; + color: $todo-status-color; font-family: FarrisXIcons; &::after { content: "\e6d9"; @@ -4058,6 +4066,10 @@ color: $text-caption;; .f-chat-started-todo-message { color: $text-primary; } + /* header 的对勾图标走绿色,与列表项 done 状态保持一致 */ + > .f-chat-todo-status-done { + color: #16a34a; + } } } diff --git a/packages/x-ui/components/todo/src/components/todo-list-item-view.component.tsx b/packages/x-ui/components/todo/src/components/todo-list-item-view.component.tsx index 377d545e8..f9d0cc17c 100644 --- a/packages/x-ui/components/todo/src/components/todo-list-item-view.component.tsx +++ b/packages/x-ui/components/todo/src/components/todo-list-item-view.component.tsx @@ -15,13 +15,15 @@ export default defineComponent({ registerProperty('title', props.title ?? ''); registerProperty('task', props.task ?? ''); + registerProperty('detail', props.message ?? ''); // 监听属性变化 watch( - [() => props.title, () => props.task], - ([title, task]) => { + [() => props.title, () => props.task, () => props.message], + ([title, task, message]) => { updatePropertyValue('title', title ?? ''); updatePropertyValue('task', task ?? ''); + updatePropertyValue('detail', message ?? ''); } ); @@ -94,9 +96,17 @@ export default defineComponent({
{displayTitle.value}
{displayTask.value}
+ {displayDetail.value ? ( +
{displayDetail.value}
+ ) : null}
) : ( -
{displayTask.value}
+
+ {displayTask.value} + {displayDetail.value ? ( +
{displayDetail.value}
+ ) : null} +
)} ))} -- Gitee From 0c97ed6eaa309bc9cd34f124e91bac5d1990cff0 Mon Sep 17 00:00:00 2001 From: Wang Jason Date: Wed, 20 May 2026 17:40:26 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat(chat):=20=E7=8B=AC=E7=AB=8B=E6=80=9D?= =?UTF-8?q?=E8=80=83=E6=B6=88=E6=81=AF=E4=B8=8A=E6=96=B9=E5=8A=A0=2030px?= =?UTF-8?q?=20=E9=97=B4=E8=B7=9D=EF=BC=8C=E7=B4=A7=E9=82=BB=20user=20?= =?UTF-8?q?=E5=90=8E=E4=BE=8B=E5=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为区隔「前一段助手输出」与「新一轮思考」,对 `.f-chat-message.is-thinking-message`(newvenus 侧打标)默认上方 留 30px 间距;以下两种情况保持原有紧凑间距: 1. 列表首条(:first-child) 2. 紧邻用户消息之后的第一条思考 实现细节:覆盖规则的右侧改为显式 `.f-chat-message.is-thinking-message` (不再使用 `&`),编译后选择器 specificity 高于默认 30px 规则, 稳定覆盖;同时避免 dart-sass 双 `&` 邻接组合符的语义不确定性。 Co-authored-by: Cursor --- packages/x-ui/components/style.scss | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/x-ui/components/style.scss b/packages/x-ui/components/style.scss index 25893c177..6edf27fe5 100644 --- a/packages/x-ui/components/style.scss +++ b/packages/x-ui/components/style.scss @@ -844,6 +844,27 @@ $color-accent-red: #f46160; // } } + /** + * 独立思考气泡(content.type === 'AgentThinking')上方默认留 30px 间距, + * 用于区隔"前一段助手输出"与"新一轮思考"。 + */ + &.is-thinking-message { + margin-top: 30px; + } + + /** + * 两种例外不加上方间距: + * 1. 列表首条 + * 2. 紧邻用户消息之后的第一条思考 + * 右侧用 `.f-chat-message.is-thinking-message` 而不是 `&.is-thinking-message` + * 避免 dart-sass 双 `&` 邻接组合符语义不确定;同时选择器 specificity 高于 + * 上面的 30px 规则,可稳定覆盖。 + */ + &.is-thinking-message:first-child, + &.user + .f-chat-message.is-thinking-message { + margin-top: 0; + } + .f-chat-message-bubble { padding: 10px 12px; border-radius: 12px; -- Gitee