diff --git a/src/components/AIChat/chat/chatMessageProcessSegments.test.ts b/src/components/AIChat/chat/chatMessageProcessSegments.test.ts
index c79ef3e8..29e1cd6d 100644
--- a/src/components/AIChat/chat/chatMessageProcessSegments.test.ts
+++ b/src/components/AIChat/chat/chatMessageProcessSegments.test.ts
@@ -10,10 +10,13 @@ type TestBlock =
| { type: 'text'; text: string }
| { type: 'think'; text: string; durationMs?: number }
| { type: 'tool'; name: string; durationMs?: number }
+ | { type: 'skill'; name: string }
+ | { type: 'error'; message: string }
| { type: 'chart'; title: string }
| { type: 'evidence'; title: string }
-const isFoldableProcessBlock = (block: TestBlock): boolean => block.type === 'think' || block.type === 'tool'
+const isFoldableProcessBlock = (block: TestBlock): boolean =>
+ block.type === 'think' || block.type === 'tool' || block.type === 'skill' || block.type === 'error'
const isTextBlock = (block: TestBlock): boolean => block.type === 'text'
@@ -41,12 +44,14 @@ describe('chat message process segments', () => {
assert.deepEqual(getVisibleSegmentBlocks(segments), [blocks[4]])
})
- it('keeps result blocks visible and starts a new process segment after them', () => {
+ it('keeps one process segment while result blocks remain visible', () => {
const blocks: TestBlock[] = [
{ type: 'tool', name: 'get_time_stats' },
{ type: 'chart', title: '发言趋势' },
{ type: 'text', text: '图表之后继续核对。' },
+ { type: 'skill', name: 'relationship-analysis' },
{ type: 'tool', name: 'retrieve_chat_evidence' },
+ { type: 'error', message: '部分证据读取失败' },
{ type: 'evidence', title: '证据' },
{ type: 'text', text: '最终解释' },
]
@@ -54,13 +59,12 @@ describe('chat message process segments', () => {
const segments = buildProcessSegments(blocks, { isFoldableProcessBlock, isTextBlock })
assert.deepEqual(segments, [
- { type: 'process', blocks: [blocks[0]] },
+ { type: 'process', blocks: [blocks[0], blocks[2], blocks[3], blocks[4], blocks[5]] },
{ type: 'visible', block: blocks[1] },
- { type: 'process', blocks: blocks.slice(2, 4) },
- { type: 'visible', block: blocks[4] },
- { type: 'visible', block: blocks[5] },
+ { type: 'visible', block: blocks[6] },
+ { type: 'visible', block: blocks[7] },
])
- assert.deepEqual(getVisibleSegmentBlocks(segments), [blocks[1], blocks[4], blocks[5]])
+ assert.deepEqual(getVisibleSegmentBlocks(segments), [blocks[1], blocks[6], blocks[7]])
})
it('keeps text visible when no later process block exists', () => {
diff --git a/src/components/AIChat/chat/chatMessageProcessSegments.ts b/src/components/AIChat/chat/chatMessageProcessSegments.ts
index bc03539c..705ba768 100644
--- a/src/components/AIChat/chat/chatMessageProcessSegments.ts
+++ b/src/components/AIChat/chat/chatMessageProcessSegments.ts
@@ -27,24 +27,23 @@ function hasLaterFoldableProcessBlock
(
}
export function buildProcessSegments(blocks: T[], options: ProcessSegmentOptions): ProcessSegment[] {
+ const isProcessBlock = blocks.map(
+ (block, index) =>
+ options.isFoldableProcessBlock(block) ||
+ (options.isTextBlock(block) && hasLaterFoldableProcessBlock(blocks, index, options.isFoldableProcessBlock))
+ )
+ const processBlocks = blocks.filter((_block, index) => isProcessBlock[index])
+ const firstProcessIndex = isProcessBlock.findIndex(Boolean)
const segments: ProcessSegment[] = []
blocks.forEach((block, index) => {
- const isProcess =
- options.isFoldableProcessBlock(block) ||
- (options.isTextBlock(block) && hasLaterFoldableProcessBlock(blocks, index, options.isFoldableProcessBlock))
-
- if (!isProcess) {
- segments.push({ type: 'visible', block })
+ if (index === firstProcessIndex) {
+ segments.push({ type: 'process', blocks: processBlocks })
return
}
- const lastSegment = segments[segments.length - 1]
- if (lastSegment?.type === 'process') {
- lastSegment.blocks.push(block)
- } else {
- segments.push({ type: 'process', blocks: [block] })
- }
+ if (isProcessBlock[index]) return
+ segments.push({ type: 'visible', block })
})
return segments
diff --git a/src/components/AIChat/input/AIChatComposer.vue b/src/components/AIChat/input/AIChatComposer.vue
index d4751964..8fc7361a 100644
--- a/src/components/AIChat/input/AIChatComposer.vue
+++ b/src/components/AIChat/input/AIChatComposer.vue
@@ -9,11 +9,13 @@ const props = withDefaults(
placeholder: string
sendButtonTitle: string
activeSkillName?: string | null
+ embedded?: boolean
}>(),
{
disabled: false,
status: 'ready',
activeSkillName: null,
+ embedded: false,
}
)
@@ -75,12 +77,17 @@ defineExpose({ focus, getSelection, setSelectionRange, syncHeight })
diff --git a/src/components/AIChat/input/AIChatInput.vue b/src/components/AIChat/input/AIChatInput.vue
index 10a0eac6..a8082014 100644
--- a/src/components/AIChat/input/AIChatInput.vue
+++ b/src/components/AIChat/input/AIChatInput.vue
@@ -17,6 +17,7 @@ const props = defineProps<{
disabled?: boolean
status?: 'ready' | 'submitted' | 'streaming' | 'error'
chatType: 'group' | 'private'
+ embedded?: boolean
}>()
const emit = defineEmits<{
@@ -650,7 +651,7 @@ defineExpose({
-
+
{
)
})
- it('converts render-only chart tool failures to visible error blocks', () => {
+ it('extracts render-only chart tool failure details', () => {
const errorBlock = toRenderOnlyToolErrorBlock('render_chart', {
content: [{ type: 'text', text: 'Error: Field "missing_count" does not exist in SQL result' }],
details: null,
@@ -234,6 +234,35 @@ describe('aiChat chart block helpers', () => {
)
})
+ it('keeps render-only chart failures on the matching tool step', () => {
+ const blocks = [
+ { type: 'think', tag: 'thinking', text: '准备生成图表' },
+ createRenderOnlyToolPendingBlock('render_chart', { spec: { title: '趋势图' } }, 'call_chart_error'),
+ ].filter((block): block is NonNullable => block !== null)
+ const errorBlock = toRenderOnlyToolErrorBlock('render_chart', {
+ content: [{ type: 'text', text: 'Error: encoding.x must be a non-empty field name' }],
+ })
+
+ const nextBlocks = finishRenderOnlyToolResultBlocks(blocks, 'render_chart', 'call_chart_error', [], errorBlock)
+
+ assert.deepEqual(nextBlocks, [
+ blocks[0],
+ {
+ type: 'tool',
+ tool: {
+ name: 'render_chart',
+ displayName: 'render_chart',
+ status: 'error',
+ params: { spec: { title: '趋势图' } },
+ toolCallId: 'call_chart_error',
+ result: 'encoding.x must be a non-empty field name',
+ displayResult: 'encoding.x must be a non-empty field name',
+ isError: true,
+ },
+ },
+ ])
+ })
+
it('hides recovered chart render errors when a later native chart exists', () => {
const blocks = [
{ type: 'error', error: { name: 'ChartRenderError', message: 'Field "metric" does not exist in SQL result' } },
diff --git a/src/stores/aiChatChartBlocks.ts b/src/stores/aiChatChartBlocks.ts
index bfc83fd9..dffdc407 100644
--- a/src/stores/aiChatChartBlocks.ts
+++ b/src/stores/aiChatChartBlocks.ts
@@ -9,7 +9,10 @@ export type RenderOnlyToolPendingBlock = {
status: 'running' | 'done' | 'error'
params?: Record
toolCallId?: string
- transient: true
+ transient?: true
+ result?: string
+ displayResult?: string
+ isError?: boolean
}
}
export type RenderOnlyToolErrorBlock = {
@@ -210,13 +213,33 @@ export function finishRenderOnlyToolResultBlocks(
charts: ChartPayload[],
errorBlock: RenderOnlyToolErrorBlock | null
): Array {
- // render-only 工具的 pending 行只用于流式占位;如果没有生成图表,也没有需要展示的错误,就应直接移除。
+ // A pending render-only row is only a streaming placeholder. Remove it when the tool produces no visible result.
if (charts.length > 0) {
return replaceRenderOnlyToolPendingBlockWithCharts(blocks, toolName, toolCallId, charts)
}
+ if (errorBlock) {
+ let replaced = false
+ const withToolError = blocks.map((block) => {
+ if (!isMatchingPendingRenderOnlyToolBlock(block, toolName, toolCallId)) return block
+ replaced = true
+ const settledTool = { ...block.tool }
+ delete settledTool.transient
+ return {
+ ...block,
+ tool: {
+ ...settledTool,
+ status: 'error' as const,
+ result: errorBlock.error.message,
+ displayResult: errorBlock.error.message,
+ isError: true,
+ },
+ }
+ })
+ return replaced ? withToolError : [...blocks, errorBlock]
+ }
+
const withoutPending = removeRenderOnlyToolPendingBlock(blocks, toolName, toolCallId)
- if (errorBlock) return [...withoutPending, errorBlock]
return withoutPending
}