diff --git a/apps/desktop/src/renderer/src/App.tsx b/apps/desktop/src/renderer/src/App.tsx
index 4e159ed..940caeb 100644
--- a/apps/desktop/src/renderer/src/App.tsx
+++ b/apps/desktop/src/renderer/src/App.tsx
@@ -135,11 +135,6 @@ export default function App(): JSX.Element {
// 主进程侧 openView(插件 main 工厂 / 深链按钮)→ 渲染层切视图
useEffect(() => window.api.onPluginsView((change) => handleOpenView(change.view, change.params)), [handleOpenView])
- // TEMP-DEBUG: 移除(改为 App.tsx 模块级触发)
-
- // 主进程侧 openView(插件 main 工厂 / 深链按钮)→ 渲染层切视图
- useEffect(() => window.api.onPluginsView((change) => handleOpenView(change.view, change.params)), [handleOpenView])
-
// 插件列表变化(扫描/重载/启停)→ 刷新外置插件页(iframe 换 src 整页重载)
useEffect(
() =>
diff --git a/apps/desktop/src/renderer/src/components/MessageItem.tsx b/apps/desktop/src/renderer/src/components/MessageItem.tsx
index 70af401..d1321c1 100644
--- a/apps/desktop/src/renderer/src/components/MessageItem.tsx
+++ b/apps/desktop/src/renderer/src/components/MessageItem.tsx
@@ -7,7 +7,7 @@ import { StreamingText } from './StreamingText'
import { Thinking, thinkingRows } from './Thinking'
import { ToolChips, type ToolCallView } from './ToolChips'
import { useEntrance } from '../hooks/useEntrance'
-import { LuFoldVertical, LuGitFork, LuHistory, LuLoaderCircle, LuUndo2 } from 'react-icons/lu'
+import { LuFoldVertical, LuGitFork, LuHistory, LuLoaderCircle, LuTriangleAlert, LuUndo2 } from 'react-icons/lu'
import { formatTokens } from '../lib/utils'
interface ContentBlock {
@@ -41,6 +41,29 @@ function lastThinkingLine(text: string): string {
return ''
}
+/** 消息级错误(stopReason:'error',如 API 402 余额耗尽/限流):无此错误返回空串 */
+function readMessageError(raw: unknown): string {
+ if (!raw || typeof raw !== 'object') return ''
+ const m = raw as { stopReason?: unknown; errorMessage?: unknown }
+ if (m.stopReason !== 'error' || typeof m.errorMessage !== 'string' || !m.errorMessage.trim()) return ''
+ return m.errorMessage
+}
+
+/** 提炼错误文案的可读部分:'402: {"message":"Insufficient Balance",…}' → '402: Insufficient Balance' */
+function readableMessageError(err: string): string {
+ const match = err.match(/^(\d{3}):\s*([\s\S]*)$/)
+ const body = match ? match[2] : err
+ try {
+ const parsed = JSON.parse(body) as { message?: unknown }
+ if (parsed && typeof parsed.message === 'string' && parsed.message) {
+ return match ? `${match[1]}: ${parsed.message}` : parsed.message
+ }
+ } catch {
+ // 非 JSON 体,保持原文
+ }
+ return err
+}
+
/** 工作计时:working=true 起表,结束后冻结读数。
* working 中途抖动(pi 轮次间隙 streaming/turnActive 抖动)不锁存:
* 恢复 true 时解除冻结,继续从起点计时。 */
@@ -166,6 +189,8 @@ function AssistantTurnBase({
let thinking = 0
let stream: UiMessage['stream'] | null = null
let answerText = ''
+ // 消息级错误(API 失败等):回合内最后一条错误保留展示,否则错误轮会渲染成空白
+ let messageError = ''
// 流式中的消息 = 答案雏形(最后一条);其余消息全部是过程。
// 只有回合结束(settled)且没有流式占位时,最后一条已完成消息的正文才是答案。
@@ -177,6 +202,8 @@ function AssistantTurnBase({
stream = message.stream
continue
}
+ const msgErr = readMessageError(message.raw)
+ if (msgErr) messageError = msgErr
const asAnswer = settled && streamPos === -1 && idx === messages.length - 1
for (const block of readBlocks(message.raw)) {
const thinkingText = block.type === 'thinking' ? (block.text ?? block.thinking) : undefined
@@ -436,6 +463,19 @@ function AssistantTurnBase({
{/* 回合进行中但没有流式占位:工具执行或自动压缩的静默间隙,给一个存活指示 */}
{turnActive && !stream &&