Skip to content

Conversation

@gene9831
Copy link
Collaborator

@gene9831 gene9831 commented Jan 16, 2026

PR 描述:v0.4.0 重大组件重构与增强

📊 概览

本次 PR 对 Bubble 组件系统和核心消息/会话钩子进行了重大增强,代表从 v0.3.x 到 v0.4.x 的重大升级。变更包括架构改进、新功能和全面的迁移指南。

🎯 主要变更

1. Bubble 组件 v0.4 重大升级

核心架构变更

  • 数据模型迁移BubbleListitems prop 改为 messages prop,支持消息分组、状态、推理和工具调用
  • 渲染器系统重构
    • 引入 Box 渲染器(控制外层容器样式/布局)
    • 引入 Content 渲染器(控制内容渲染:文本/图片/markdown/工具/推理)
    • 基于匹配的渲染器系统,使用 priority + find() 模式
    • 为未匹配情况提供回退渲染器

新功能

  • 消息分组:支持分组策略(consecutivedivider、自定义函数)
  • 状态管理:新增 state prop 和 state-change 事件,用于 UI 状态管理而不污染原始消息
  • 内容解析
    • contentResolver 函数用于灵活的内容字段提取
    • contentRenderMode (single | split) 用于数组内容渲染
  • 内置渲染器
    • Image - 图片内容渲染
    • Markdown - 增强的 markdown 渲染
    • Loading - 加载状态显示
    • Reasoning - 推理内容显示,带增强样式和动画
    • Tool / Tools / ToolRole - 工具调用渲染
  • 增强的自动滚动:改进的自动滚动行为,监控内容/推理变化
  • 复制清理:自动清理复制的文本(移除多余换行)

API 变更

  • BubbleListitemsmessagesrolesroleConfigs
  • 新增 props:groupStrategydividerRolefallbackRolecontentResolvercontentRenderMode
  • Bubble:新增 shape: 'none' 选项,增强 avatar 支持 Component 类型

2. useMessage Hook 重构

架构改进

  • 插件系统:引入可扩展的插件架构
    • 内置插件:fallbackRolePluginthinkingPluginlengthPlugintoolPlugin
    • 插件去重和执行管道
  • 响应提供者模式:用 responseProvider 函数替代直接使用 client
    • 支持 Promise<T>(非流式)和 AsyncGenerator<T>(流式)
  • 增强的状态管理
    • requestState'idle' | 'processing' | 'completed' | 'error'
    • processingState:详细的处理信息
  • 字段包含/排除
    • requestMessageFields:指定请求中包含哪些字段
    • requestMessageFieldsExclude:指定请求中排除哪些字段(默认:['state', 'metadata', 'loading']

3. useConversation Hook 增强

主要改进

  • 懒加载:会话在切换时按需加载
  • 独立引擎:每个会话拥有自己的 useMessage 引擎实例
  • 存储策略重构
    • 分离存储操作:loadConversationsloadMessagessaveConversationsaveMessages
    • 增强的 IndexedDBStrategyLocalStorageStrategy 实现
    • 通过 ConversationStorageStrategy 接口支持自定义存储策略
  • 带节流的自动保存
    • 可配置的自动保存,使用 autoSaveMessagesautoSaveThrottle 选项
    • 使用 useThrottleFn 进行高效节流(leading + trailing)
  • 增强的加载逻辑:改进的会话加载和合并策略
  • 运行时引擎缓存:仅在内存中保留活跃和后台运行的会话

新工具函数

  • useThrottleFn:带 leading/trailing 选项的节流函数实现
  • 存储工具:createStorageStrategyvalidateStorageStrategy

4. 存储系统增强

新存储工具(packages/kit/src/storage/utils.ts

  • 策略工厂函数
  • 存储验证工具
  • 存储操作辅助函数

增强的存储策略

  • IndexedDB 策略:改进实现,更好的错误处理
  • LocalStorage 策略:增强序列化
  • 自定义存储支持:易于实现自定义存储后端

5. 新组件和组合式函数

Bubble 组合式函数

  • useContentResolver:从消息中解析内容
  • useCopyCleanup:清理复制的文本
  • useToolCall:增强的工具调用管理
  • useBubbleBoxRenderer:Box 渲染器解析
  • useBubbleContentRenderer:Content 渲染器解析

新渲染器组件

  • Image.vue:图片内容渲染器
  • Markdown.vue:增强的 markdown 渲染器
  • Loading.vue:加载状态渲染器
  • Reasoning.vue:带动画的推理内容渲染器
  • 工具渲染器:Tool.vueTools.vueToolRole.vue

6. 文档和迁移指南

新迁移指南

  • Bubble 迁移指南docs/src/migration/bubble-migration.md):从 v0.3.x 迁移到 v0.4.x 的全面指南
  • useMessage 迁移指南docs/src/migration/use-message-migration.md):迁移 useMessage hook 的指南
  • useConversation 迁移指南docs/src/migration/use-conversation-migration.md):迁移 useConversation hook 的指南

更新的文档

  • 增强的 Bubble 组件文档,包含新功能
  • 更新的会话和消息工具文档
  • 新存储文档(docs/src/tools/storage.md
  • 新工具函数文档(docs/src/tools/utils.md

新示例

  • 内容渲染模式示例
  • 内容解析器示例
  • 自定义渲染器示例
  • 图片渲染示例
  • 列表示例(数组内容、自动滚动、连续、自定义分组、隐藏)
  • 提供者渲染器示例
  • 推理示例
  • 状态变更示例
  • 工具示例
  • 存储示例(自定义、IndexedDB、LocalStorage)
  • SSE Stream 工具示例

7. Bug 修复和改进

  • 修复基于用户角色的消息分组逻辑
  • 强制要求 state-change 事件中的 contentIndex
  • 改进 useBubbleBoxRenderer 中的内容提取逻辑
  • 增强会话加载逻辑和合并策略
  • 改进内容处理和清理逻辑
  • 增强自动滚动行为

🔄 破坏性变更

Bubble 组件

  • BubbleList.itemsBubbleList.messages(必需变更)
  • BubbleList.rolesBubbleList.roleConfigs(命名变更)
  • 渲染器系统完全重新设计(提供迁移指南)

useMessage Hook

  • 移除 client prop,替换为 responseProvider
  • 引入插件系统(默认插件向后兼容)
  • 状态管理 API 变更

useConversation Hook

  • 移除 client prop,替换为 useMessageOptions
  • 存储 API 变更(现在使用策略模式)
  • 每个会话现在拥有独立引擎

🧪 测试

  • 所有现有示例已更新以使用新 API
  • 为所有新功能添加了新示例
  • 文档中提供了迁移示例

📦 文件变更摘要

核心组件

  • packages/components/src/bubble/* - 重大重构
  • packages/kit/src/vue/message/* - 插件系统和重构
  • packages/kit/src/vue/conversation/* - 增强懒加载和存储策略
  • packages/kit/src/storage/* - 新工具和增强策略

🎉 优势

  1. 更好的架构:更灵活和可扩展的组件系统
  2. 改进的性能:懒加载、节流和优化的渲染
  3. 增强的开发者体验:更好的 API、全面的文档和迁移指南
  4. 更多功能:工具调用、推理显示、图片渲染等
  5. 更好的可维护性:插件系统、清晰的关注点分离和改进的代码组织

Summary by CodeRabbit

  • New Features

    • Major Bubble overhaul: two-tier renderers, content-resolver, split/single content modes, new built-in renderers (Image, Loading, Markdown, Reasoning, Tool/Tools).
    • Conversation & messaging revamp: per-conversation engines, plugin-driven message flow, abort/cancel actions, IndexedDB/LocalStorage storage strategies.
  • Documentation

    • New migration guides for Bubble, useMessage, useConversation and expanded utilities/docs (SSE, helpers).
  • Enhancements

    • Many interactive demo updates and new examples showcasing auto-scroll, grouping, tooling, streaming, reasoning, and copy cleanup.

✏️ Tip: You can customize this high-level summary in your review settings.

…ndering and integrate jsonrepair for argument handling
…or renderer matching and enhance loading state handling
… new wrapper components and remove deprecated renderers
…t and improved prop handling for polymorphic messages
…ne list structure for improved message rendering
… ToolCall and BubbleChatMessageItem interfaces
…s components for consistency in message handling
…essageFields composable and updating event emission in Bubble components
…w renderers and improving message handling with updated props and types
…nd enhance message handling with new props and plugins
…-change' across components and introduce BubbleRenderers for improved renderer management
…dex access with 'at()' method for better readability and safety
- Renamed `createSSEStreamGenerator` to `sseStreamToGenerator` for clarity.
- Added comprehensive documentation in Chinese for the new function, detailing its purpose and usage.
- Updated the export statement in `index.ts` to reflect the new function name.
- Updated demo content in `avatar-and-placement.vue` to reflect user and AI messages.
- Added new demos for content rendering modes, including `content-render-mode.vue` and `content-resolver.vue`, showcasing single and split rendering.
- Introduced custom renderer examples in `custom-renderer.vue` and `provider-renderer.vue` for enhanced flexibility in message display.
- Implemented new grouping strategies in `list-consecutive.vue`, `list-custom-group.vue`, and `list-array-content.vue` to improve message organization.
- Enhanced `loading.vue` and `streaming.vue` demos for better user interaction and visual feedback.
- Updated documentation in `bubble.md` to include new features and usage examples.
- Added new sidebar items for '存储策略' and '工具函数' in the theme configuration.
- Refactored demo components to utilize `activeConversationId` and `historyData` for improved state management.
- Updated message handling in various demo files to reflect changes in the `useMessage` and `useConversation` composables.
- Introduced new storage strategy demos for LocalStorage and IndexedDB, showcasing persistent conversation management.
- Enhanced documentation for AI client and conversation management, marking deprecated features and providing clearer usage examples.
@coderabbitai
Copy link

coderabbitai bot commented Jan 16, 2026

Warning

Rate limit exceeded

@gene9831 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 29 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

Comprehensive v0.4.0 upgrade: introduces a two‑tier Bubble renderer architecture, plugin-based useMessage, per-conversation engines with storage strategies, streaming/tool-call support, many new renderers/composables, storage refactors, updated demos, and migration documentation.

Changes

Cohort / File(s) Summary
Bubble Rendering Architecture
packages/components/src/bubble/Bubble.vue, packages/components/src/bubble/BubbleList.vue, packages/components/src/bubble/BubbleItem.vue, packages/components/src/bubble/BubbleContentWrapper.vue, packages/components/src/bubble/composables/useBubbleBoxRenderer.ts, packages/components/src/bubble/composables/useBubbleContentRenderer.ts, packages/components/src/bubble/index.type.ts
Added contentResolver prop, split content rendering mode, box/content renderer matching with priority, required numeric contentIndex in state-change payloads, and updated renderer match signatures.
New Bubble Composables
packages/components/src/bubble/composables/useContentResolver.ts, packages/components/src/bubble/composables/useCopyCleanup.ts, packages/components/src/bubble/composables/useToolCall.ts, packages/components/src/bubble/composables/useMessageContent.ts, packages/components/src/bubble/composables/index.ts
Added useContentResolver, useCopyCleanup, useToolCall; changed useMessageContent API to return { content, contentText }; re-exported new composables.
Bubble Renderers (New Components)
packages/components/src/bubble/renderers/Image.vue, packages/components/src/bubble/renderers/Loading.vue, packages/components/src/bubble/renderers/Markdown.vue, packages/components/src/bubble/renderers/Reasoning.vue, packages/components/src/bubble/renderers/Tool.vue, packages/components/src/bubble/renderers/ToolRole.vue, packages/components/src/bubble/renderers/Tools.vue, packages/components/src/bubble/renderers/Text.vue, packages/components/src/bubble/renderers/allRenderers.ts, packages/components/src/bubble/renderers/defaultRenderers.ts
Added image/loading/markdown/reasoning/tool-related renderer components and updated default renderer match arrays to include image_url, loading, reasoning, and tools.
Bubble Constants & Exports
packages/components/src/bubble/constants.ts, packages/components/src/bubble/index.ts, packages/components/src/index.ts
Added BUBBLE\_LIST\_CONTEXT\_KEY injection key and exported useToolCall. Updated component export surface.
Message Hook Plugin System
packages/kit/src/vue/message/types.ts, packages/kit/src/vue/message/useMessage.ts, packages/kit/src/vue/message/utils.ts, packages/kit/src/vue/message/plugins/fallbackRolePlugin.ts, packages/kit/src/vue/message/plugins/lengthPlugin.ts, packages/kit/src/vue/message/plugins/thinkingPlugin.ts, packages/kit/src/vue/message/plugins/toolPlugin.ts, packages/kit/src/vue/message/plugins/index.ts
Reworked useMessage into a plugin-driven API: requestState/processingState, responseProvider, plugin lifecycle hooks, built-in plugins (fallbackRole, length, thinking, tool), deduplication and streaming chunk handling utilities.
Conversation Management Refactor
packages/kit/src/vue/conversation/types.ts, packages/kit/src/vue/conversation/useConversation.ts, packages/kit/src/vue/conversation/useThrottleFn.ts
Introduced ConversationInfo/Conversation types, per-conversation engines, activeConversationId, lazy engine lifecycle, throttled auto-save, and new useConversation return shape.
Storage Strategy System
packages/kit/src/storage/types.ts, packages/kit/src/storage/localStorageStrategy.ts, packages/kit/src/storage/indexedDBStrategy.ts, packages/kit/src/storage/utils.ts
Reworked ConversationStorageStrategy to per-conversation load/save messages API; added saveMessages/loadMessages/deleteConversation; implemented IndexedDB/localStorage strategy updates and unwrap/transform utilities.
Core Types & Utilities
packages/kit/src/types.ts, packages/kit/src/utils.ts, packages/kit/src/client.ts, packages/kit/src/index.ts, packages/kit/src/vue/index.ts
Added MaybePromise, ToolCall, MessageMetadata, expanded ChatMessage fields (reasoning_content, tool_calls), added sseStreamToGenerator, reorganized exports, and marked AIClient deprecated.
Bubble Demo Components
docs/demos/bubble/... (many files)
Updated demos to new API: provider-based renderers, contentRenderMode, contentResolver, new renderer demos; removed several legacy demos.
Message Hook Demos
docs/demos/tools/message/*.ts, docs/demos/tools/message/*.vue
Added/updated examples for streaming/non-streaming, mock streams, custom completion chunk handling, error handling, requestState/processingState, and tool-call flows using the new useMessage APIs.
Conversation Tool Demos
docs/demos/tools/conversation/..., docs/demos/tools/storage/...
Updated conversation demos to use useConversation and storage strategies; added mock and memory storage demos and IndexedDB/localStorage examples.
Documentation & Migration Guides
docs/src/bubble.md, docs/src/tools/message.md, docs/src/tools/conversation.md, docs/src/tools/utils.md, docs/src/tools/ai-client.md, docs/src/migration/bubble-migration.md, docs/src/migration/use-message-migration.md, docs/src/migration/use-conversation-migration.md
Added comprehensive v0.4 docs and migration guides covering Bubble, useMessage, useConversation, utilities, and migration recipes.
Navigation & Styling
docs/.vitepress/themeConfig.ts, docs/src/components/bubble.less, packages/components/src/styles/components/bubble.less, packages/playground/src/utils/default-files.ts
Added "迁移指南" nav entry and utils sidebar link; added CSS variables for tool-call styling and updated playground style variable usage.
Storage Implementation Support
docs/demos/tools/conversation/mockStorageStrategy.ts, docs/demos/tools/conversation/mockResponseProvider.ts, docs/demos/tools/message/Basic.ts, docs/demos/tools/message/...
Added mock response providers and mock storage strategies to support demos and local testing.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Component as Bubble/BubbleList
    participant Resolver as ContentResolver
    participant Provider as BubbleProvider
    participant Renderer as ContentRenderer
    participant Store as BubbleStore

    User->>Component: add/update message
    Component->>Resolver: resolver(message)
    Resolver-->>Component: resolved content
    Component->>Provider: find matching renderer
    Provider-->>Component: matched Renderer
    Component->>Renderer: render(props)
    Renderer->>Store: read/write state (open/liked/tool results)
    Renderer-->>Component: emit state-change
    Component-->>User: UI updated
Loading
sequenceDiagram
    participant App
    participant useMessage as useMessage Hook
    participant Plugins as Plugin System
    participant Provider as ResponseProvider
    participant Engine as Message Engine
    participant UI as App Component

    App->>useMessage: initialize(options)
    useMessage->>Plugins: load & deduplicate plugins
    App->>useMessage: sendMessage(content)
    useMessage->>Plugins: onTurnStart
    useMessage->>Plugins: onBeforeRequest
    useMessage->>Provider: POST request (with abortSignal)
    Provider-->>useMessage: AsyncGenerator<ChatCompletion>
    loop for each chunk
      useMessage->>Plugins: onCompletionChunk(chunk)
      useMessage->>Engine: merge delta into current message
      useMessage->>UI: update messages
    end
    useMessage->>Plugins: onAfterRequest
    useMessage->>Plugins: onTurnEnd
Loading
sequenceDiagram
    participant App
    participant useConversation as useConversation Hook
    participant Storage as StorageStrategy
    participant Engine as PerConversation Engine
    participant AutoSave as ThrottleSaver

    App->>useConversation: init(options)
    useConversation->>Storage: loadConversations()
    Storage-->>useConversation: ConversationInfo[]
    App->>useConversation: switchConversation(id)
    useConversation->>useConversation: ensureEngine(id)
    useConversation->>Storage: loadMessages(id)
    Storage-->>Engine: ChatMessage[]
    useConversation->>AutoSave: setup throttle save(engine)
    App->>Engine: sendMessage(content)
    Engine-->>App: messages updated
    AutoSave->>Storage: saveMessages(id, messages)
    Storage-->>AutoSave: persisted
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 Whiskers twitch, I hop with cheer,
New renderers, plugins, engines near.
Streams and tools now hum in song,
Conversations tidy, schemas strong.
A rabbit hops — migration's here! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'refactor: components/kit 0.4.0' accurately summarizes the main change as a major version upgrade refactoring of components and kit packages.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 16, 2026

✅ Preview build completed successfully!

Click the image above to preview.
Preview will be automatically removed when this PR is closed.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 16, 2026

- Introduced migration guides for Bubble and Kit components, detailing changes from v0.3.x to v0.4.x.
- Updated theme configuration to include a new sidebar item for the migration guide.
- Enhanced demo examples with improved styling and updated properties for better user experience.
- Refactored demo components to align with the latest API changes and best practices.
- Added new CSS variables for consistent styling across components.
- Introduced `getContentItems` function to streamline content extraction from messages, enhancing the rendering process.
- Updated `useBubbleBoxRenderer` and `useBubbleContentRenderer` to utilize the new content handling approach, ensuring consistent content structure.
- Refactored rendering logic in `Bubble.vue` and related components to support unified content item format, improving compatibility with various content types.
- Enhanced type definitions in `index.type.ts` to reflect changes in content handling, ensuring better type safety and clarity.
- Updated the `state-change` event payload in `Bubble.vue`, `BubbleContentWrapper.vue`, `BubbleItem.vue`, and `BubbleList.vue` to make `contentIndex` a required field.
- Adjusted type definitions in `index.type.ts` to reflect the change in `contentIndex` from optional to required.
- Modified the `useBubbleContentRenderer` and related logic to ensure consistent handling of `contentIndex` across components.
…nderer

- Introduced a new helper function `getContentAndIndex` to streamline the retrieval of content and index from messages.
- Updated the main computed property to utilize the new function, enhancing readability and maintainability of the code.
- Ensured consistent handling of content and index across the bubble rendering logic.
- Updated the grouping logic in `BubbleList.vue` to ensure that messages with an array content are only treated as independent groups if the message role is 'user'.
- Adjusted the documentation in `index.type.ts` to reflect this new condition for message grouping, enhancing clarity on how messages are processed.
@gene9831 gene9831 force-pushed the v0.4.0-bubble-part2 branch from 59486a0 to 4fb32c3 Compare January 26, 2026 03:35
…ssage and useConversation

- Added migration guides for `useMessage` and `useConversation`, detailing changes from v0.3.x to v0.4.x.
- Updated theme configuration to include new sidebar items for the migration guides.
- Enhanced documentation for AI client and conversation management, marking deprecated features and providing clearer usage examples.
- Removed the obsolete `kit-migration.md` file to streamline documentation.
@gene9831 gene9831 marked this pull request as ready for review January 26, 2026 09:27
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/components/src/bubble/index.type.ts (1)

99-105: Update custom renderers: contentIndex is now required in BubbleContentRendererProps.

The type definition now requires contentIndex: number (previously optional). All custom renderers implementing this type must pass contentIndex explicitly. If you were previously omitting this parameter or treating it as optional, update your renderer implementations to include it. Built-in renderers and wrapper components already reflect this change.

Note: Documentation currently describes contentIndex as optional, but the type definition enforces it as required. Align your custom renderers with the type definition.

🤖 Fix all issues with AI agents
In `@docs/demos/tools/conversation/Basic.vue`:
- Around line 128-130: The module-scope access to window when computing apiUrl
(the const apiUrl line referencing window.parent?.location.origin) will break
SSR/static builds; change the computation to first check for browser runtime
(e.g., typeof window !== 'undefined') before accessing window or window.parent,
and fall back to using import.meta/env BASE_URL or a safe origin when window is
not available; update the apiUrl definition (and related consts meta/baseUrl if
needed) so all window or location access is guarded by that runtime check.

In `@docs/demos/tools/conversation/LocalStorage.vue`:
- Line 49: The module-scope access to window.parent?.location.origin in the
apiUrl constant will throw during SSR; update the apiUrl initialization in
LocalStorage.vue so it only reads browser globals when available by guarding
with typeof window !== 'undefined' and using window.location (not
window.parent). Specifically, replace the top-level const apiUrl = ... with a
guarded expression (or compute apiUrl inside a setup/mounted hook) that returns
typeof window !== 'undefined' ? window.location.origin : '' (or another safe
server-side fallback) so server-side builds no longer reference window/location
at import time.

In `@docs/demos/tools/utils/SSEStream.vue`:
- Around line 45-55: The code currently reads parent origin directly and never
appends BASE_URL; change the apiUrl resolution to safely determine origin and
always append a normalized BASE_URL: use the existing meta and baseUrl variables
(ImportMetaEnv, ImportMetaWithEnv, meta, baseUrl) but compute origin with a
guarded check (if window.parent !== window) and a try/catch around accessing
window.parent.location.origin to avoid cross-origin throws, fall back to
window.location.origin on error, then normalize baseUrl to ensure it begins with
'/' (or empty string if not provided) and set apiUrl = origin +
normalizedBaseUrl so BASE_URL is always applied and parent access is safe.

In `@packages/components/src/bubble/composables/useBubbleBoxRenderer.ts`:
- Around line 63-65: The current assignment for content in
useBubbleBoxRenderer.ts unsafely casts resolvedContent.at(contentIndex ?? 0) to
ChatMessageContentItem; instead, check bounds and return undefined when out of
range: if resolvedContent is an array, compute idx = contentIndex ?? 0, then if
idx < 0 || idx >= resolvedContent.length set content = undefined, else set
content = resolvedContent[idx]; ensure the declared type for content remains
ChatMessageContentItem | undefined (no non-null cast) so downstream code (e.g.,
defaultRenderers.ts checks like content.type) must handle undefined safely.

In `@packages/components/src/bubble/composables/useCopyCleanup.ts`:
- Around line 7-35: The watcher in useCopyCleanup wrongly calls stopWatch()
inside its own callback causing it to stop on first run and also never removes
prior copy listeners; instead, keep the watch handle assigned to stopWatch (do
not call stopWatch() inside the callback), create a named handler (e.g.
handleCopy) that you add via elem.addEventListener('copy', handleCopy), and use
onCleanup within the watch callback to call elem.removeEventListener('copy',
handleCopy) so the previous listener is removed when elementRef changes or when
the watcher is stopped; ensure you still return/maintain stopWatch from watch
and let Vue's onCleanup manage per-run teardown.

In `@packages/components/src/bubble/renderers/Markdown.vue`:
- Around line 27-33: The code builds HTML with markdown(mdConfig ||
{}).render(content.value) and then calls dompurify.sanitize(...) but discards
the return value, leaving markdownContent.value set to unsanitized HTML; update
the watchEffect in the Markdown.vue block so you capture and assign the
sanitizer output to markdownContent.value (use the markdown and dompurify
variables from markdownItAndDompurify.value, call markdown.render(content.value)
into a temp var, pass that to dompurify.sanitize(..., dompurifyConfig), and
assign the sanitized result to markdownContent.value before rendering via
v-html).

In `@packages/components/src/bubble/renderers/Tool.vue`:
- Around line 45-67: The highlightJSON function injects raw JSON into HTML spans
without escaping HTML entities, allowing tool output to inject HTML/JS when
rendered via v-html; fix by HTML-escaping the input string first (convert & < >
" ' to entities) before running the regex/highlighting logic in highlightJSON
(or call a small escapeHtml helper at the start of highlightJSON), then continue
to apply the existing regex and class lookup (classes['number'], classes['key'],
classes['string'], etc.) so all injected content is safe when used with v-html.
- Around line 128-130: The expand-control (IconArrowDown with class
"expand-icon") is not keyboard-accessible; update the template in Tool.vue to
use a semantic, focusable control (preferably replace the IconArrowDown wrapper
div with a <button> or add tabindex="0", role="button", and keyboard handlers)
so keyboard users can toggle via Enter/Space and see state via ARIA. Ensure the
control calls the existing handleClick method on click and on keydown for
Enter/Space, and add aria-expanded bound to the open prop (and aria-controls if
applicable) so assistive tech can read the expanded state.

In `@packages/kit/src/storage/indexedDBStrategy.ts`:
- Around line 51-63: The upgrade handler currently only creates missing stores
and doesn't perform migrations, so existing 'conversations' entries using the
old full Conversation shape may become incompatible with the new
ConversationInfo type; update the upgrade(db, oldVersion, newVersion) logic to
be version-aware (use the passed oldVersion/newVersion), and when migrating from
versions that stored full Conversation objects, open the 'conversations'
objectStore, iterate all records, transform each Conversation into the new
ConversationInfo shape (selecting/renaming fields like id, title, updatedAt,
etc.), put the transformed records back into the 'conversations' store, and
likewise handle any schema changes for 'messages' if needed; ensure you only run
the migration steps when oldVersion is less than the target version to avoid
reprocessing.

In `@packages/kit/src/storage/utils.ts`:
- Around line 100-119: transformMessages can drop non-text renderContent because
when renderContent is an array but contains no 'collapsible-text' or
'markdown'/'text' items the function returns restMessage (which omits
renderContent) and loses data; update transformMessages to only return
restMessage when a transformation actually occurred (i.e.,
collapsibleTextItems.length > 0 or textItems.length > 0) and otherwise return
the original message object, keeping renderContent intact; modify the branch
that handles Array.isArray(renderContent) to detect if no items matched and
return message, otherwise set restMessage.reasoning_content/restMessage.content
as currently done and return restMessage.
- Around line 13-96: The unwrapProxy function currently uses a WeakSet and
returns empty placeholders for already-seen objects which loses shared data;
change the visited parameter to a WeakMap<object, any> that maps original
objects to their cloned result, so repeated encounters return the same clone
instead of an empty object/array. In unwrapProxy, after obtaining rawValue (via
toRaw), check visited.has(rawValue) and return visited.get(rawValue) if present;
before recursing into properties, create the clone placeholder (Array or
Object), store it in visited.set(rawValue, clone), then populate its entries
recursively (using unwrapProxy for prop values). Ensure arrays, Dates, RegExps,
ArrayBuffer/Blob still return directly and functions/getters/symbols are skipped
as before.

In `@packages/kit/src/vue/conversation/useConversation.ts`:
- Around line 116-123: The async loadConversations() call assigns directly to
conversations.value which can clobber any conversations created via
createConversation or switched via switchConversation before the promise
resolves; change the logic in the storage.loadConversations handling to merge
the loaded list into the existing conversations.value (e.g., upsert by unique
id) instead of replacing it, and ensure activeConversation is preserved or
remapped to the corresponding loaded item if needed; reference
storage.loadConversations, conversations.value, createConversation,
switchConversation, and activeConversation when implementing the merge/guard to
prevent overwriting in-memory state.

In `@packages/kit/src/vue/message/plugins/fallbackRolePlugin.ts`:
- Around line 9-17: The onBeforeRequest handler currently rebuilds
requestBody.messages from context.messages which overwrites prior plugin
mutations; change it to map over context.requestBody.messages
(requestBody.messages) instead so you preserve earlier edits, applying
fallbackRole only where message.role is falsy while keeping the rest of each
message's properties; update the mapping in onBeforeRequest to operate on
requestBody.messages and assign the result back to requestBody.messages.

In `@packages/kit/src/vue/message/useMessage.ts`:
- Around line 275-286: The loop incorrectly sets hasOnError = true for every
non-disabled plugin, causing errors to be swallowed even when no plugin
implements onError; update the loop in useMessage.ts so that hasOnError is set
to true only when plugin.onError exists (e.g., check plugin.onError before
setting the flag), then call plugin.onError with the context returned by
getBaseContext(ac.signal) and err; keep using isPluginDisabled(plugin, context)
to filter plugins and ensure the final if (!hasOnError) throw err remains
unchanged.
🟡 Minor comments (15)
packages/components/src/shared/composables/useAutoScroll.ts-134-137 (1)

134-137: Update the JSDoc return description to include arrivedState.

The hook now returns arrivedState but the doc comment still says it only returns scrollToBottom. Please align the documentation with the new API.

packages/components/src/bubble/composables/useMessageContent.ts-10-19 (1)

10-19: Add fallback handling for array indexing.

The cast as ChatMessageContentItem on line 13 masks the possibility that Array.prototype.at() returns undefined. While the current usage pattern guarantees contentIndex is always valid (sourced from iterating the content array), this lacks defensive protection against future modifications or edge cases. Add an explicit fallback to align with defensive programming practices and match similar patterns elsewhere in the codebase.

🛠️ Suggested fix
  const content = computed(() => {
    const resolvedContent = contentResolver(props.message)
-   return Array.isArray(resolvedContent)
-     ? (resolvedContent.at(props.contentIndex) as ChatMessageContentItem)
-     : { type: 'text', text: resolvedContent || '' }
+   if (Array.isArray(resolvedContent)) {
+     const item = resolvedContent.at(props.contentIndex) as ChatMessageContentItem | undefined
+     return item ?? { type: 'text', text: '' }
+   }
+   return { type: 'text', text: resolvedContent || '' }
  })
docs/demos/bubble/list-custom-group.vue-44-60 (1)

44-60: Use nullish timestamp checks to avoid mis-grouping on timestamp: 0.

Lines 55 and 57 use truthy checks on timestamps, which treats a valid timestamp: 0 as missing and collapses groups incorrectly. Replace with nullish checks (!= null) and cache the last message to eliminate repeated casting.

🛠️ Suggested fix
+    const lastMsg = lastGroup?.messages[lastGroup.messages.length - 1] as
+      | (typeof messages)[0]
+      | undefined
+    const currTs = msgWithTimestamp.timestamp
+    const lastTs = lastMsg?.timestamp
+
-    if (
-      !lastGroup ||
-      (msgWithTimestamp.timestamp &&
-        lastGroup.messages.length > 0 &&
-        (lastGroup.messages[lastGroup.messages.length - 1] as (typeof messages)[0]).timestamp &&
-        msgWithTimestamp.timestamp -
-          ((lastGroup.messages[lastGroup.messages.length - 1] as (typeof messages)[0]).timestamp || 0) >
-          TIME_THRESHOLD)
-    ) {
+    if (!lastGroup || (currTs != null && lastTs != null && currTs - lastTs > TIME_THRESHOLD)) {
packages/components/src/bubble/composables/useToolCall.ts-69-84 (1)

69-84: Prevent stale async jsonrepair updates.

The watchEffect tracks toolCall.value and toolCallResults.value as dependencies. When either changes while a getJsonrepair() promise is pending, a new promise launches. If the earlier promise resolves after the newer one, it overwrites toolCallWithResult.value with stale data. Add onInvalidate to cancel stale promise results.

Suggested fix
-  watchEffect(() => {
+  watchEffect((onInvalidate) => {
     const args = toolCall.value?.function.arguments
     const result = toolCallResults.value
 
+    let cancelled = false
+    onInvalidate(() => {
+      cancelled = true
+    })
+
     getJsonrepair()
       .then(({ jsonrepair }) => {
+        if (cancelled) return
         const repairedArgs = jsonrepair(typeof args === 'string' ? args || '{}' : JSON.stringify(args))
         toolCallWithResult.value = {
           arguments: JSON.parse(repairedArgs),
           result: result ? JSON.parse(jsonrepair(result || '{}')) : undefined,
         }
       })
       .catch((error) => {
-        console.warn(error)
+        if (!cancelled) console.warn(error)
       })
   })
packages/kit/src/vue/message/plugins/lengthPlugin.ts-9-19 (1)

9-19: Consider the execution order and potential infinite loop risk.

Two observations:

  1. Execution order: requestNext() is called before restOptions.onAfterRequest, which may not allow downstream hooks to prevent or modify the continuation behavior.

  2. Infinite loop risk: If the model consistently returns finish_reason: 'length', this plugin will repeatedly append messages and trigger new requests. Consider adding a safeguard (e.g., max continuation count).

💡 Suggested safeguard
+const MAX_CONTINUATIONS = 5
+
 export const lengthPlugin = (options: UseMessagePlugin & { continueContent?: string } = {}): UseMessagePlugin => {
-  const { continueContent = 'Please continue with your previous answer.', ...restOptions } = options
+  const { continueContent = 'Please continue with your previous answer.', maxContinuations = MAX_CONTINUATIONS, ...restOptions } = options
+  let continuationCount = 0

   return {
     name: 'length',
     ...restOptions,
+    onTurnStart: async (context) => {
+      continuationCount = 0
+      return restOptions.onTurnStart?.(context)
+    },
     onAfterRequest: async (context) => {
       const { lastChoice, appendMessage, requestNext } = context

-      if (lastChoice?.finish_reason === 'length') {
+      if (lastChoice?.finish_reason === 'length' && continuationCount < maxContinuations) {
+        continuationCount++
         appendMessage({ role: 'user', content: continueContent })
         requestNext()
       }

       return restOptions.onAfterRequest?.(context)
     },
   }
 }
docs/demos/bubble/list-auto-scroll.vue-11-14 (1)

11-14: Unused ref attribute on container div.

The ref="containerRef" attribute is declared in the template but containerRef is never defined or used in the script. Either remove the ref or add the corresponding const containerRef = ref() if it's needed for future functionality.

Suggested fix (remove unused ref)
     <div
-      ref="containerRef"
       style="height: 300px; border: 1px solid `#ddd`; border-radius: 4px; overflow-y: auto; padding: 8px"
     >
packages/kit/src/utils.ts-170-172 (1)

170-172: Unhandled promise rejection from reader.cancel().

Unlike handleSSEStream (Line 30) which catches errors from reader.cancel(), this abort handler ignores the returned promise. If cancellation fails, the error will be silently swallowed.

Suggested fix
   // Set up abort signal listener
   const abortHandler = () => {
-    reader.cancel()
+    reader.cancel().catch((err) => console.error('Error cancelling reader:', err))
   }
packages/kit/src/vue/message/plugins/thinkingPlugin.ts-3-6 (1)

3-6: Plugin name can be unintentionally overwritten.

Spreading options after defining name allows callers to override the plugin's internal name, which could cause issues if other code depends on the plugin being named 'thinking'.

Proposed fix
 export const thinkingPlugin = (options: UseMessagePlugin = {}): UseMessagePlugin => {
+  const { name: _, ...restOptions } = options
   return {
     name: 'thinking',
-    ...options,
+    ...restOptions,
     onCompletionChunk(context) {
packages/kit/src/storage/localStorageStrategy.ts-78-85 (1)

78-85: Missing error handling in deleteConversation.

Unlike other methods in this class, deleteConversation lacks try-catch error handling. If localStorage.setItem throws (e.g., quota exceeded), the error will propagate unexpectedly.

🐛 Suggested fix
   deleteConversation(conversationId: string) {
+    try {
       const conversations = getConversations(this.storageKey)
       const index = conversations.findIndex((item) => item.id === conversationId)
       if (index !== -1) {
         conversations.splice(index, 1)
       }
       localStorage.setItem(this.storageKey, JSON.stringify(conversations))
+    } catch (error) {
+      console.error('删除会话失败:', error)
+    }
   }
packages/kit/src/storage/localStorageStrategy.ts-53-64 (1)

53-64: Silent failure when saving messages for non-existent conversation.

saveMessages silently does nothing if the conversation doesn't exist (when index === -1). This could hide bugs where messages are being saved to a deleted or non-existent conversation.

Consider logging a warning or throwing an error when the conversation is not found.

🐛 Suggested improvement
   saveMessages(conversationId: string, messages: ChatMessage[]) {
     try {
       const conversations = getConversations(this.storageKey)
       const index = conversations.findIndex((item) => item.id === conversationId)
       if (index !== -1) {
         conversations[index].messages = messages
+        localStorage.setItem(this.storageKey, JSON.stringify(conversations))
+      } else {
+        console.warn(`Cannot save messages: conversation ${conversationId} not found`)
       }
-      localStorage.setItem(this.storageKey, JSON.stringify(conversations))
     } catch (error) {
       console.error('保存会话消息失败:', error)
     }
   }
packages/kit/src/vue/message/utils.ts-137-143 (1)

137-143: Truthy check may skip valid falsy values.

The condition if (targetValue) on line 137 will treat 0, '', and false as non-existent, causing direct assignment instead of merge. If these are valid values that should trigger merge behavior, consider using explicit undefined check.

Suggested fix if falsy values should be preserved
-    if (targetValue) {
+    if (targetValue !== undefined) {
packages/kit/src/vue/message/plugins/toolPlugin.ts-264-326 (1)

264-326: Consider guarding requestNext() when all tool calls are aborted.

After Promise.all(toolCallPromises) completes, requestNext() is called unconditionally on line 329. If the request was aborted and all tool calls returned early (line 314), this could trigger an unnecessary next request cycle.

Suggested fix
       await Promise.all(toolCallPromises)
+
+      // Skip requestNext if aborted
+      if (abortSignal.aborted) {
+        return restOptions.onAfterRequest?.(context)
+      }
+
       requestNext()
packages/kit/src/storage/indexedDBStrategy.ts-149-162 (1)

149-162: Non-atomic deletion of conversation and messages.

The delete operations for conversations and messages stores are executed sequentially without a transaction wrapper. If an error occurs between the two deletes, data could become inconsistent (orphaned messages).

Suggested: Use single transaction
   async deleteConversation(conversationId: string): Promise<void> {
     try {
       const db = await this.getDB()
-
-      // 删除会话
-      await db.delete('conversations', conversationId)
-
-      // 删除该会话的消息记录(通过 conversationId 直接删除)
-      await db.delete('messages', conversationId)
+      const tx = db.transaction(['conversations', 'messages'], 'readwrite')
+      await Promise.all([
+        tx.objectStore('conversations').delete(conversationId),
+        tx.objectStore('messages').delete(conversationId),
+        tx.done,
+      ])
     } catch (error) {
packages/kit/src/vue/conversation/useConversation.ts-164-181 (1)

164-181: Guard against duplicate conversation IDs

createConversation allows a caller-supplied id but doesn’t prevent collisions. If the id already exists, you’ll get duplicate entries and possibly overwrite workingEngines, breaking find/switch logic. Consider guarding or regenerating on collision.

🛠️ Suggested guard
-    const { id = generateId(), title, metadata, useMessageOptions } = params || {}
+    const { id: providedId = generateId(), title, metadata, useMessageOptions } = params || {}
+    let id = providedId
+    if (conversations.value.some((c) => c.id === id)) {
+      console.warn('[useConversation] duplicate conversation id, generating a new one:', id)
+      id = generateId()
+    }
docs/src/components/bubble.md-255-256 (1)

255-256: Align contentIndex optionality with the type block

The text says contentIndex is optional, but the type snippet below shows it as required. Consider updating the wording to avoid confusion (e.g., “contentIndex is always provided; in single mode it’s 0”).

🛠️ Suggested wording tweak
-Content 渲染器接收 `BubbleContentRendererProps` 作为 props,包含 `message` 和可选的 `contentIndex`。
+Content 渲染器接收 `BubbleContentRendererProps` 作为 props,包含 `message` 和 `contentIndex`(单内容模式下为 0)。
🧹 Nitpick comments (36)
docs/demos/examples/Assistant.vue (2)

447-454: Add Content-Type header and consider response status check.

The fetch request is missing the Content-Type: application/json header, which may cause the server to misinterpret the request body. Additionally, there's no check for response.ok before processing.

♻️ Suggested improvement
 responseProvider: async (requestBody, abortSignal) => {
   const response = await fetch('/api/chat/completions', {
     method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
     body: JSON.stringify({ ...requestBody, stream: true }),
     signal: abortSignal,
   })
+  if (!response.ok) {
+    throw new Error(`Request failed: ${response.status}`)
+  }
   return sseStreamToGenerator(response, { signal: abortSignal })
 },

465-467: Consider adding a fallback for isProcessing.

For consistency with messages (which has || []), consider adding || false to isProcessing to avoid passing undefined to the :loading prop.

-const isProcessing = computed(() => messageEngine.value?.isProcessing.value)
+const isProcessing = computed(() => messageEngine.value?.isProcessing.value ?? false)
docs/demos/tools/utils/SSEStream.vue (1)

56-73: Clear the input after a successful submit.
Keeps the sender field in sync with typical chat UX.

✨ UX tweak
   messages.value.push({
     role: 'user',
     content: content.trim(),
   })
+  inputMessage.value = ''
packages/kit/src/client.ts (1)

10-13: Consider adding migration guidance to the deprecation notice.

The @deprecated annotation is appropriate, but it would be more helpful to include a reference to the replacement API or migration guide. This helps users understand what to use instead.

💡 Suggested enhancement
 /**
- * `@deprecated`
+ * `@deprecated` Use `useConversation` and `useMessage` composables instead.
+ * See migration guide: docs/src/migration/use-conversation-migration.md
  * AI客户端类
  */
docs/demos/bubble/custom-renderer.vue (1)

36-53: Type safety could be improved in the custom renderer example.

The props definition uses generic Object type and the setup function relies on a double type assertion (as unknown as CodeMessage), which reduces type safety. Since this is a demo that users may copy, consider improving the typing:

  1. The message prop could use as PropType<BubbleMessage> for proper Vue type inference.
  2. The double cast on line 49 suggests a type mismatch between what useMessageContent returns and CodeMessage.
💡 Suggested improvement
+import type { PropType } from 'vue'
+import type { BubbleMessage } from '@opentiny/tiny-robot'

 const CodeBlockRenderer = defineComponent({
   props: {
     message: {
-      type: Object,
+      type: Object as PropType<BubbleMessage>,
       required: true,
     },
     contentIndex: Number,
   },
packages/components/src/bubble/renderers/ToolRole.vue (1)

10-20: Consider cleanup on unmount for tool call results.

The watchEffect writes to shared store state but doesn't clean up when the component unmounts. If tool call results should be scoped to the component's lifecycle, consider using onUnmounted to remove the entry. However, if the results are intentionally persisted across component lifecycles (e.g., for caching tool responses), the current approach is appropriate.

💡 Optional cleanup pattern if results should be scoped
 <script setup lang="ts">
-import { watchEffect } from 'vue'
+import { watchEffect, onUnmounted } from 'vue'
 import { useBubbleStore } from '../composables'
 import { BubbleContentRendererProps } from '../index.type'
 
 const props = defineProps<BubbleContentRendererProps<string>>()
 
 const store = useBubbleStore<{ toolCallResults?: Record<string, string> }>()
 
 watchEffect(() => {
   if (!props.message.tool_call_id) {
     return
   }
 
   if (!store.toolCallResults) {
     store.toolCallResults = {}
   }
 
   store.toolCallResults[props.message.tool_call_id] = props.message.content ?? ''
 })
+
+onUnmounted(() => {
+  if (props.message.tool_call_id && store.toolCallResults) {
+    delete store.toolCallResults[props.message.tool_call_id]
+  }
+})
 </script>
docs/demos/bubble/state-change.vue (1)

43-46: Redundant state update in toggleLike.

The liked state is already toggled on line 44, so calling handleStateChange on line 45 with the same value is redundant for local state management. If the intent is to demonstrate emitting state changes to a parent, consider adding a comment clarifying this pattern.

Additionally, note that the checkbox on line 5 directly mutates messageState.expanded via v-model, bypassing handleStateChange, while the like button uses handleStateChange. This inconsistency may confuse users learning the state-change pattern.

♻️ Suggested clarification
 const toggleLike = () => {
   messageState.value.liked = !messageState.value.liked
+  // Emit state change event (useful when parent needs to sync state)
   handleStateChange({ key: 'liked', value: messageState.value.liked })
 }
docs/src/tools/storage.md (1)

190-221: Consider adding error handling note in custom strategy example.

The RemoteStorageStrategy example omits error handling for brevity. Consider adding a brief comment or note that production implementations should handle network errors and non-2xx responses to avoid silent failures.

📝 Optional: Add error handling note
   async loadConversations(): Promise<ConversationInfo[]> {
     const response = await fetch(`${this.apiUrl}/conversations`)
+    // Production code should check response.ok and handle errors
     return response.json()
   }

Or add a note after the example:

> **Note**: This example omits error handling for brevity. Production implementations should handle network failures and non-2xx responses.
packages/components/src/bubble/renderers/Image.vue (1)

13-23: Consider adding defensive handling for unexpected image_url formats.

If content.value.image_url is neither a string nor an object with a url property (e.g., undefined, null, or malformed), Line 22 will throw. Consider adding a fallback:

♻️ Suggested defensive handling
 const imageUrl = computed(() => {
   if (!content.value) {
     return null
   }

   if (typeof content.value.image_url === 'string') {
     return content.value.image_url
   }

-  return content.value.image_url.url
+  return content.value.image_url?.url ?? null
 })
docs/demos/bubble/provider-renderer.vue (1)

32-55: Consider aligning prop types with defineComponent's props definition.

The setup function explicitly types props as BubbleBoxRendererProps, but the props object only defines placement and shape as String. This mismatch may cause TypeScript inconsistencies. For a demo, this is acceptable, but in production code consider defining props more precisely.

packages/kit/src/vue/message/plugins/thinkingPlugin.ts (1)

19-24: Consider using .at(-1) for cleaner array access.

Using .at(-1) is more idiomatic for accessing the last element of an array.

Suggested change
     onTurnEnd(context) {
       // 如果不是流式数据或者请求被中断,thinking 状态可能不会被更新,在 onTurnEnd 中手动更新
-      const lastMessage = context.currentTurn.slice(-1)[0]
+      const lastMessage = context.currentTurn.at(-1)
       if (lastMessage?.state) {
         lastMessage.state.thinking = undefined
       }
docs/demos/bubble/tools.vue (1)

37-42: Consider wrapping JSON.parse in try-catch for robustness.

While this is demo code, JSON.parse can throw if arguments contains malformed JSON, which could cause an unhandled exception.

Suggested defensive approach
 const handleChangeToolCallArguments = () => {
   const args = toolCalls.value[0]!.function.arguments
-  const parsedArgs = JSON.parse(args)
-  parsedArgs.a = parsedArgs.a + 1
-  toolCalls.value[0]!.function.arguments = JSON.stringify(parsedArgs)
+  try {
+    const parsedArgs = JSON.parse(args)
+    parsedArgs.a = parsedArgs.a + 1
+    toolCalls.value[0]!.function.arguments = JSON.stringify(parsedArgs)
+  } catch (e) {
+    console.warn('Failed to parse tool call arguments:', e)
+  }
 }
packages/components/src/bubble/composables/useBubbleContentRenderer.ts (1)

49-53: Redundant nullish coalescing for required parameter.

Since contentIndex is now a required number parameter (Line 40), the ?? 0 fallback on Line 51 is unnecessary and could be misleading.

Suggested cleanup
     const resolvedContent = contentResolver(msg)
     const content = Array.isArray(resolvedContent)
-      ? (resolvedContent.at(contentIndex ?? 0) as ChatMessageContentItem)
+      ? (resolvedContent.at(contentIndex) as ChatMessageContentItem)
       : { type: 'text', text: resolvedContent || '' }
docs/demos/bubble/streaming.vue (1)

15-23: Prevent overlapping stream loops on repeated clicks.
If the button is clicked multiple times, concurrent loops can interleave. A simple token/cancel guard keeps output consistent.

♻️ Proposed refactor
 const fullText = '这是一段流式输出的文本内容。'
 const streamContent = ref('点击上方按钮开始流式输出文本')
+let streamToken = 0
 
 const resetStreamContent = async () => {
+  const token = ++streamToken
   streamContent.value = ''
   for (const char of fullText) {
+    if (token !== streamToken) break
     streamContent.value += char
     await new Promise((resolve) => setTimeout(resolve, 100))
   }
 }
packages/components/src/bubble/renderers/Tools.vue (1)

8-15: Consider normalizing tool_calls for defensive consistency.

While the Tools renderer is only instantiated when tool_calls is guaranteed to be a non-empty array (via the find condition in defaultRenderers.ts), normalizing to an empty array aligns with the defensive patterns used elsewhere in the codebase and makes the component more resilient to future changes.

♻️ Proposed refactor
 const { restMessage, restProps } = useOmitMessageFields(props, ['tool_calls'])
 
 const renderer = useBubbleContentRenderer(restMessage, props.contentIndex)
+const toolCalls = props.message.tool_calls ?? []
-  <Tool v-for="(tool, index) in props.message.tool_calls" :key="tool.id" v-bind="props" :tool-call-index="index" />
+  <Tool v-for="(tool, index) in toolCalls" :key="tool.id" v-bind="props" :tool-call-index="index" />
packages/components/src/bubble/composables/useBubbleBoxRenderer.ts (1)

50-55: Validation runs only once at setup time, not on reactive updates.

The contentIndex validation checks toValue(messages).length during function execution, but if messages is reactive and changes later, this validation won't re-run. If the invariant must hold throughout the component's lifetime, consider moving this check inside the computed callback or into getContentAndIndex.

♻️ Potential refactor to validate reactively
-  // 如果 contentIndex 为数字,说明是 split 模式,当前 messages 数组长度为 1
-  if (typeof contentIndex === 'number') {
-    if (toValue(messages).length !== 1) {
-      throw new Error('[BubbleBoxRenderer] When contentIndex is a number, messages array length must be 1')
-    }
-  }
-
   const getContentAndIndex = (msgs: BubbleMessage[]) => {
+    // Validate split mode invariant
+    if (typeof contentIndex === 'number' && msgs.length !== 1) {
+      throw new Error('[BubbleBoxRenderer] When contentIndex is a number, messages array length must be 1')
+    }
     if (msgs.length !== 1) {
       return { content: undefined, index: undefined }
     }
packages/components/src/bubble/renderers/Reasoning.vue (2)

36-50: Auto-scroll watch lacks cleanup and may trigger on unmounted component.

The watch on props.message.reasoning_content with nextTick could potentially execute after component unmount if the message updates rapidly. Consider adding a guard or using watchEffect with cleanup.

♻️ Safer approach with mounted guard
+import { nextTick, onBeforeUnmount, ref, watch, watchEffect } from 'vue'
+
+let isMounted = true
+onBeforeUnmount(() => {
+  isMounted = false
+})

 watch(
   () => props.message.reasoning_content,
   () => {
     nextTick(() => {
-      if (!detailRef.value) {
+      if (!isMounted || !detailRef.value) {
         return
       }

56-59: Hardcoded Chinese strings may affect internationalization.

The text "正在思考" and "已思考" are hardcoded. If the component library supports i18n, consider using translation keys or making these configurable via props/slots.

docs/demos/tools/storage/Custom.vue (1)

110-117: Consider adding error handling for fetch failures.

The responseProvider doesn't handle network errors or non-2xx responses. While this is a demo, adding basic error handling would make it more robust and educational.

♻️ Add basic error handling
     responseProvider: async (requestBody, abortSignal) => {
       const response = await fetch(`${apiUrl}/api/chat/completions`, {
         method: 'POST',
         body: JSON.stringify({ ...requestBody, stream: true }),
         signal: abortSignal,
       })
+      if (!response.ok) {
+        throw new Error(`Request failed: ${response.status}`)
+      }
       return sseStreamToGenerator(response, { signal: abortSignal })
     },
docs/demos/tools/storage/IndexedDB.vue (1)

60-67: Same error handling suggestion applies here.

Similar to the Custom.vue demo, the responseProvider lacks error handling for failed requests. Consider adding a guard for non-2xx responses for a more complete example.

docs/demos/bubble/list.vue (1)

2-2: Consider renaming roles to roleConfigs for consistency.

The variable is named roles but is passed to the :role-configs prop. For consistency with the new API naming (as documented in the migration guide), consider renaming the variable to roleConfigs.

♻️ Suggested change
-  <tr-bubble-list :messages="messages" :role-configs="roles"></tr-bubble-list>
+  <tr-bubble-list :messages="messages" :role-configs="roleConfigs"></tr-bubble-list>
-const roles: Record<string, BubbleRoleConfig> = {
+const roleConfigs: Record<string, BubbleRoleConfig> = {
   ai: {
     placement: 'start',
     avatar: aiAvatar,
   },
   user: {
     placement: 'end',
     avatar: userAvatar,
   },
 }

Also applies to: 20-29

packages/kit/src/types.ts (1)

1-1: Consider scoping the eslint-disable more narrowly.

The file-wide eslint-disable for @typescript-eslint/no-explicit-any could be scoped to specific lines where any is intentionally used (lines 28, 41-42) rather than disabling for the entire file.

packages/components/src/bubble/renderers/Markdown.vue (1)

14-14: Type annotation could be clearer.

The type Awaited<ReturnType<typeof getMarkdownItAndDompurify>> with initial value null works because the function likely returns a nullable type, but consider making this explicit for readability.

♻️ Suggested improvement
-const markdownItAndDompurify = ref<Awaited<ReturnType<typeof getMarkdownItAndDompurify>>>(null)
+const markdownItAndDompurify = ref<Awaited<ReturnType<typeof getMarkdownItAndDompurify>> | null>(null)
docs/demos/tools/storage/LocalStorage.vue (1)

48-48: Inconsistent API URL construction compared to Basic.vue.

This demo uses only window.parent?.location.origin || location.origin, while Basic.vue includes baseUrl from import.meta.env.BASE_URL. If the API is served from a subpath, requests may fail in this demo.

♻️ Suggested fix for consistency
+// Get BASE_URL from import.meta if available
+const baseUrl = (import.meta as any)?.env?.BASE_URL || ''
-const apiUrl = window.parent?.location.origin || location.origin
+const apiUrl = window.parent?.location.origin || location.origin + baseUrl
docs/demos/tools/message/Basic.vue (1)

23-32: Consider simplifying the import.meta type augmentation.

The custom interface declarations work but are verbose. Vue/Vite projects typically have these types available via vite/client types.

♻️ Simplified alternative
-// Get BASE_URL from import.meta if available, otherwise use empty string
-interface ImportMetaEnv {
-  BASE_URL?: string
-}
-interface ImportMetaWithEnv extends ImportMeta {
-  env?: ImportMetaEnv
-}
-const meta = typeof import.meta !== 'undefined' ? (import.meta as ImportMetaWithEnv) : null
-const baseUrl = meta?.env?.BASE_URL || ''
+const baseUrl = import.meta.env?.BASE_URL || ''

If Vite types are not available, consider a simpler cast:

const baseUrl = (import.meta as any).env?.BASE_URL || ''
docs/demos/bubble/reasoning.vue (1)

48-67: Consider adding cleanup for the animation on unmount.

If the component unmounts during the replay animation, the async function will continue executing and attempt to update refs that may no longer exist.

♻️ Add cancellation support
+import { h, ref, onUnmounted } from 'vue'
+
+let animationAborted = false
+onUnmounted(() => {
+  animationAborted = true
+})

 const replayThinking = async () => {
   if (reasoningState.value.thinking) {
     return
   }
+  animationAborted = false
   reasoningState.value.thinking = true
   reasoningContent.value = ''
   content.value = ''

   for (const char of rawReasoningContent) {
+    if (animationAborted) return
     await new Promise((resolve) => setTimeout(resolve, 10))
     reasoningContent.value += char
   }

   reasoningState.value.thinking = false

   for (const char of rawContent) {
+    if (animationAborted) return
     await new Promise((resolve) => setTimeout(resolve, 10))
     content.value += char
   }
 }
docs/src/migration/use-message-migration.md (2)

7-7: Minor: Inconsistent version format throughout the document.

The document mixes v0.3.x (with 'v' prefix) and 0.4.x (without prefix). Consider using a consistent format throughout for clarity.


125-128: Consider adding error handling note for JSON.parse in the example.

The JSON.parse(toolCall.function.arguments || '{}') could throw if arguments contains malformed JSON. While this is a simplified demo, a brief comment about production error handling would be helpful.

📝 Suggested improvement
       callTool: async (toolCall) => {
-        const args = JSON.parse(toolCall.function.arguments || '{}')
+        // In production, wrap in try-catch for malformed JSON
+        const args = JSON.parse(toolCall.function.arguments || '{}')
         return `Weather of ${args.city}: Sunny`
       },
packages/components/src/bubble/Bubble.vue (1)

78-85: Conditional composable call may cause issues.

Calling useCopyCleanup conditionally at the component's top level breaks Vue's composition API rules. Although inject returns a static value here, if BUBBLE_LIST_CONTEXT_KEY were ever provided dynamically or if the component is reused in different contexts, this pattern could lead to inconsistent hook invocation.

Consider always calling the composable but conditionally activating its behavior:

♻️ Suggested refactor
 // 检查 Bubble 是否在 BubbleList 下
 const isInBubbleList = inject(BUBBLE_LIST_CONTEXT_KEY, false)
 const bubbleRef = ref<HTMLDivElement | null>(null)

-// 只有当 Bubble 不在 BubbleList 下时才使用 useCopyCleanup
-if (!isInBubbleList) {
-  useCopyCleanup(bubbleRef)
-}
+// useCopyCleanup should handle the isInBubbleList check internally
+useCopyCleanup(bubbleRef, { enabled: !isInBubbleList })

Alternatively, if useCopyCleanup cannot be modified, move the conditional logic inside the composable or ensure the composable gracefully handles being disabled.

packages/kit/src/vue/message/useMessage.ts (1)

298-301: Minor: Redundant existence check.

The check if (currentTurn.slice(-1)[0]) is unnecessary since currentTurn.slice(-1)[0]?.loading = undefined would safely handle the undefined case with optional chaining.

♻️ Suggested simplification
-      if (currentTurn.slice(-1)[0]) {
-        currentTurn.slice(-1)[0].loading = undefined
-      }
+      const lastMessage = currentTurn.at(-1)
+      if (lastMessage) {
+        lastMessage.loading = undefined
+      }
packages/kit/src/storage/localStorageStrategy.ts (1)

6-10: Consider defensive parsing for legacy data.

The getConversations helper assumes stored data always has a messages property. If there's legacy data without this field, conversation.messages could be undefined, which may cause issues downstream.

♻️ Suggested improvement
 const getConversations = (storageKey: string) => {
   const conversationsStr = localStorage.getItem(storageKey)
   const conversations = conversationsStr ? JSON.parse(conversationsStr) : []
-  return conversations as (ConversationInfo & { messages: ChatMessage[] })[]
+  return (conversations as (ConversationInfo & { messages?: ChatMessage[] })[]).map(c => ({
+    ...c,
+    messages: c.messages ?? []
+  }))
 }
packages/kit/src/storage/types.ts (1)

1-2: Move ConversationInfo to shared types for proper layer separation.

The storage/ layer imports ConversationInfo from ../vue/conversation/types, creating an unwanted dependency from the storage layer to the Vue-specific layer. ConversationInfo is a plain TypeScript interface (containing only id, title, createdAt, updatedAt, and metadata) with no Vue-specific types, making it suitable for the shared types location.

Move ConversationInfo to src/types.ts alongside other framework-agnostic types like ChatMessage and MaybePromise to maintain proper architectural separation. This also affects localStorageStrategy.ts and indexedDBStrategy.ts, which currently depend on the same import.

Note: storage/utils.ts also imports toRaw from Vue; consider if this dependency should be revisited for consistency.

packages/kit/src/vue/message/plugins/toolPlugin.ts (1)

293-300: Consider more specific error handling for JSON parse failures.

When JSON.parse fails on existing toolMessage.content, the code logs a warning but continues with an empty object. This silently discards previously accumulated content. Consider preserving the original string content or logging a more descriptive message.

Suggested improvement
              let parsedContent: Record<string, any> = {}
              try {
                parsedContent = JSON.parse(toolMessage.content || '{}')
              } catch (error) {
-               console.warn(error)
+               console.warn('Failed to parse tool message content as JSON, starting fresh:', error)
              }
packages/kit/src/vue/message/utils.ts (1)

165-168: Potential performance concern with large index Maps.

Using Math.max(...Array.from(targetMap.keys())) with spread operator could cause stack overflow for very large arrays (browser-dependent, typically ~100k+ elements). For typical chat scenarios this is likely fine, but consider a safer approach for robustness.

Safer max calculation
-          const arrLen = Math.max(...Array.from(targetMap.keys()), -1) + 1
+          const arrLen = Array.from(targetMap.keys()).reduce((max, k) => Math.max(max, k), -1) + 1
docs/demos/tools/conversation/IndexedDB.vue (1)

92-102: Consider extracting database name to a constant.

The database name 'demo-chat-db' is duplicated on lines 70 and 96. Extracting to a constant would prevent sync issues.

Suggested refactor
+const DB_NAME = 'demo-chat-db'
+
 const {
   // ...
 } = useConversation({
   // ...
   storage: indexedDBStorageStrategyFactory({
-    dbName: 'demo-chat-db',
+    dbName: DB_NAME,
     dbVersion: 1,
   }),
 })

 const clearStorage = async () => {
   if (confirm('确定要清空所有会话数据吗?')) {
     try {
-      indexedDB.deleteDatabase('demo-chat-db')
+      indexedDB.deleteDatabase(DB_NAME)
       location.reload()
packages/kit/src/storage/indexedDBStrategy.ts (1)

77-81: Consider cursor-based iteration for large datasets.

Using getAllFromIndex followed by reverse() loads all conversations into memory. For users with many conversations, consider using a cursor with 'prev' direction for direct reverse iteration.

Suggested optimization for large datasets
   async loadConversations(): Promise<ConversationInfo[]> {
     try {
       const db = await this.getDB()
-
-      // 按更新时间倒序获取所有会话
-      const conversations = await db.getAllFromIndex('conversations', 'by-updated')
-
-      // 最新的在前
-      return conversations.reverse()
+      const conversations: ConversationInfo[] = []
+      const tx = db.transaction('conversations', 'readonly')
+      const index = tx.store.index('by-updated')
+      
+      // Iterate in reverse (newest first) using 'prev' direction
+      let cursor = await index.openCursor(null, 'prev')
+      while (cursor) {
+        conversations.push(cursor.value)
+        cursor = await cursor.continue()
+      }
+      
+      return conversations
     } catch (error) {

- Simplified the content extraction in `useBubbleBoxRenderer` by removing unnecessary type imports and ensuring consistent content resolution.
- Enhanced `useCopyCleanup` to utilize `onCleanup` for better event listener management, improving performance and preventing memory leaks.
- Updated `Markdown.vue` to sanitize rendered content after processing, ensuring security against XSS attacks.
- Added HTML escaping in `Tool.vue` to prevent XSS vulnerabilities when rendering JSON content.
- Refined `unwrapProxy` function in `utils.ts` to use a `WeakMap` for better handling of circular references and shared objects.
- Improved error handling in `useMessage` by ensuring `onError` is only called if defined, enhancing plugin robustness.
- Adjusted `fallbackRolePlugin` to correctly map messages, ensuring fallback roles are applied consistently.
@gene9831 gene9831 changed the title robot/kit 0.4.0 refactor: robot/kit 0.4.0 Jan 27, 2026
@gene9831 gene9831 changed the title refactor: robot/kit 0.4.0 refactor: components/kit 0.4.0 Jan 27, 2026
…rategy

- Improved the loading mechanism in `useConversation` to handle empty conversation lists gracefully.
- Implemented a merging strategy that prioritizes in-memory conversations over stored ones, ensuring that newly created conversations are not overwritten.
- Added checks to maintain the integrity of the active conversation after merging, preventing accidental loss of context.
SonyLeo
SonyLeo previously approved these changes Jan 28, 2026
@hexqi hexqi added this to the v0.4.x milestone Jan 29, 2026
…e demo examples

- Removed the '存储策略' sidebar item from the theme configuration to simplify navigation.
- Added mock response and storage strategies to the conversation demo, allowing for a complete offline experience.
- Updated the Basic.vue demo to include a message sender and delete conversation functionality, improving user interaction.
- Introduced new utility files for mock response and storage strategies, enhancing the flexibility of the conversation management system.
- Enhanced documentation for conversation management, providing clearer examples and usage instructions.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In `@docs/demos/tools/message/Basic.ts`:
- Around line 19-25: In responseProvider (the async function that calls fetch
and returns sseStreamToGenerator), add the Content-Type: application/json header
to the fetch request and immediately check response.ok after the fetch; if not
ok, read response.text() (or json()) and throw or return a rejected Error with
that body so you don't call sseStreamToGenerator on an error response; update
references to apiUrl/responseProvider and keep sseStreamToGenerator usage only
for successful responses.

In `@docs/demos/tools/message/CustomChunk.ts`:
- Around line 20-27: In the responseProvider passed to useMessage, add a
"Content-Type: application/json" header to the fetch and check the HTTP status
before streaming: after await fetch(`${apiUrl}/api/chat/completions`, ...)
inspect response.ok (or response.status) and throw or handle a non-2xx response
(including reading response.text() for an error message) instead of calling
sseStreamToGenerator on an error response; ensure you still pass abortSignal and
the stream to sseStreamToGenerator when response.ok is true.

In `@docs/demos/tools/message/ErrorHandling.ts`:
- Around line 11-13: The apiUrl construction uses wrong operator precedence so
baseUrl is only appended when window.parent is missing; update the expression
for apiUrl (the apiUrl variable) to parenthesize the origin selection: compute
origin as (window.parent?.location.origin || location.origin) and then append
baseUrl, i.e. ensure you use parentheses so baseUrl is always concatenated to
the chosen origin; check the variables meta and baseUrl remain unchanged.

In `@docs/demos/tools/message/MockStream.vue`:
- Around line 30-33: The handleSubmit function currently calls sendMessage with
whatever content is provided; add input validation to prevent sending empty or
whitespace-only messages and avoid sending while processing: in handleSubmit
(and related UI binding using inputMessage.value) check if (!content?.trim() ||
isProcessing.value) return before calling sendMessage, and only clear
inputMessage.value after a successful send.

In `@docs/demos/tools/message/NonStreaming.ts`:
- Around line 19-26: In responseProvider (the async function assigned to
responseProvider), add the Content-Type: application/json header to the fetch
request and validate the HTTP response before calling response.json();
specifically, after await fetch(...) check response.ok and if false throw or
return a rejected Error including response.status and await response.text() for
diagnostics, otherwise return response.json(); keep stream: false in the JSON
body and preserve the abortSignal.

In `@docs/demos/tools/message/OnBeforeRequest.ts`:
- Around line 31-37: The responseProvider implementation is missing the
Content-Type header and lacks response status validation; update the fetch call
in responseProvider to include headers: { 'Content-Type': 'application/json' }
and after awaiting fetch check response.ok (e.g., throw an Error including
response.status and await response.text() for details) before calling
sseStreamToGenerator so sseStreamToGenerator only receives a successful
response; reference the responseProvider function and sseStreamToGenerator and
keep using abortSignal for the fetch signal.

In `@docs/demos/tools/message/RequestState.ts`:
- Around line 18-26: responseProvider currently uses a fixed 1.5s delay that
ignores abortSignal and proceeds to stream even for non-OK HTTP responses;
change the artificial delay to be abort-aware (reject immediately when
abortSignal.aborted or hook signal into a cancellable Promise) so the function
aborts without waiting, and after fetch check response.ok and throw an Error
(include status/statusText) before calling sseStreamToGenerator; reference
responseProvider, abortSignal, fetch, response.ok, and sseStreamToGenerator when
making these changes.
- Around line 9-11: The top-level apiUrl computation uses
window.parent.location.origin which can throw in SSR or cross-origin iframes;
change it to compute apiUrl lazily and guard access by checking typeof window
!== 'undefined' before touching window, wrap any access to
window.parent.location.origin in a try/catch or conditionally compare
window.parent === window to avoid cross-origin reads, and fall back to
location.origin + baseUrl or an empty string; update the symbols meta, baseUrl
and apiUrl in RequestState.ts so meta/baseUrl remain top-level but apiUrl is
derived inside a safe function or getter that performs these runtime guards.

In `@docs/demos/tools/message/RequestState.vue`:
- Around line 39-41: The handleSubmit function currently calls
sendMessage(content) without awaiting it and clears inputMessage.value
immediately; change it to await sendMessage(content) and only set
inputMessage.value = '' after the awaited call succeeds (wrap in try/catch
around sendMessage to handle errors and avoid unhandled promise rejections,
e.g., call await sendMessage(content) inside try and clear inputMessage.value
there, and handle or rethrow the error in catch). Ensure you modify the
handleSubmit function reference so the promise is awaited and errors are caught.

In `@docs/src/tools/message.md`:
- Line 164: Clarify plugin activation in the `useMessage` docs: state that
`thinkingPlugin` is automatically active by default (no need to add to
`plugins`), while `fallbackRolePlugin`, `lengthPlugin`, and `toolPlugin` are
built-in but opt-in and only take effect if explicitly included in the `plugins`
array; also note that supplying `thinkingPlugin(...)` in `plugins` will
override/customize the default behavior if users want to change or disable it.
🧹 Nitpick comments (9)
docs/src/tools/message.md (3)

23-23: Document or link to sseStreamToGenerator utility.

The text mentions sseStreamToGenerator as a utility function to convert SSE responses to async generators, but doesn't show where to import it from or link to its documentation. Users encountering this for the first time won't know how to use it.

📝 Suggested addition to clarify usage

Consider adding a brief note after line 23:

使用 `responseProvider` 发起流式请求,配合 `initialMessages` 展示欢迎语。当后端返回 SSE(Server-Sent Events)流时,可使用 `sseStreamToGenerator` 工具函数(从 `@opentiny/tiny-robot-kit` 导入)将 `fetch``Response` 转为异步生成器(`AsyncGenerator`),供 `useMessage` 逐块消费并合并到消息内容中。

或者在 API 部分添加一个"工具函数"小节专门说明 sseStreamToGenerator 的签名和用法。

83-92: Clarify the interaction between requestMessageFields and requestMessageFieldsExclude.

The current explanation describes the filtering logic twice (lines 85-86 and 90-91) with similar wording, which may confuse readers. The interaction between whitelist and blacklist could be explained more clearly and concisely.

📝 Proposed clearer explanation

Consider consolidating and clarifying the logic:

  /**
   * 请求消息时,要包含的字段(白名单)。默认包含所有字段。
-  * 如果 `requestMessageFieldsExclude` 存在,会先取 `requestMessageFields` 中的字段,再排除 `requestMessageFieldsExclude` 中的字段
+  * 若同时指定黑名单,则先应用白名单筛选,再从结果中排除黑名单字段。
   */
  requestMessageFields?: string[]
  /**
-  * 请求消息时,要排除的字段(黑名单)。默认会排除 `state`、`metadata`、`loading` 字段(这几个字段是给UI展示用的)。
-  * 如果 `requestMessageFields` 存在,会先取 `requestMessageFields` 中的字段,再排除 `requestMessageFieldsExclude` 中的字段
+  * 请求消息时,要排除的字段(黑名单)。
+  * 默认排除: `state`、`metadata`、`loading`(仅用于 UI 展示)。
+  * 若同时指定白名单,则先应用白名单筛选,再从结果中排除黑名单字段。
   */
  requestMessageFieldsExclude?: string[]

Or add a note after both fields explaining the precedence:

> **字段筛选逻辑**: 若同时指定 `requestMessageFields`(白名单)和 `requestMessageFieldsExclude`(黑名单),则先应用白名单筛选出指定字段,再从结果中排除黑名单字段。若仅指定白名单,则只保留白名单字段;若仅指定黑名单,则从所有字段中排除黑名单字段。

119-120: Consider reformatting for improved readability.

The explanation of responseProvider return values is comprehensive but presented as a single dense paragraph. Breaking it into structured points would make it easier to understand.

📖 Proposed structured format
**responseProvider 返回值**:

`responseProvider` 的返回值决定响应模式:

- **非流式**(`Promise<T>`): 一次性得到完整结果,适用于不支持 SSE 的后端(`stream: false`)。`useMessage` 会将解析出的内容整体写入消息。

- **流式**(`AsyncGenerator<T>``Promise<AsyncGenerator<T>>`): 逐块产出数据,适用于流式接口(如 SSE)。`useMessage` 会按块消费并增量合并到消息内容中。

- **SSE 转换**: 若后端返回 SSE 流,可使用 `sseStreamToGenerator``fetch``Response` 转为异步生成器。
docs/demos/tools/message/CustomChunk.ts (1)

4-12: Consider extracting the shared apiUrl computation to a utility.

This ImportMetaEnv/ImportMetaWithEnv interface pattern and apiUrl computation is duplicated across multiple demo files (Basic.ts, OnBeforeRequest.ts, NonStreaming.ts). Extracting this to a shared utility (e.g., docs/demos/tools/utils/apiUrl.ts) would reduce duplication and simplify future maintenance.

docs/demos/tools/message/ToolCall.ts (1)

96-98: Consider wrapping JSON.parse in try-catch for robustness.

If toolCall.function?.arguments contains malformed JSON, JSON.parse will throw an unhandled exception. While this is demo code, adding error handling improves reliability and serves as a better example.

🛡️ Suggested defensive parsing
       callTool: async (toolCall) => {
-        const args = JSON.parse(toolCall.function?.arguments || '{}')
+        let args: Record<string, any> = {}
+        try {
+          args = JSON.parse(toolCall.function?.arguments || '{}')
+        } catch {
+          return '工具调用参数解析失败。'
+        }
         return `${args.city} 天气:晴,25°C。`
       },
docs/demos/tools/conversation/mockStorageStrategy.ts (1)

61-63: Minor: Redundant fallback in loadConversations.

The || [] fallback is unnecessary since this.conversations is always initialized as an array at declaration time.

♻️ Suggested simplification
   async loadConversations(): Promise<ConversationInfo[]> {
-    return this.conversations || []
+    return this.conversations
   }
docs/demos/tools/message/MockStream.ts (1)

5-28: Consider extracting shared mock stream logic.

This mockStream implementation is nearly identical to mockResponseProvider in docs/demos/tools/conversation/mockResponseProvider.ts, differing only in delay (30ms vs 150ms) and reply text. For demo files, self-contained examples are acceptable, but if more mock providers are added, consider extracting a shared utility with configurable delay and message.

docs/demos/tools/message/ErrorHandling.ts (1)

18-27: Consider defensive check for empty currentTurn.

The non-null assertion currentTurn.at(-1)! assumes currentTurn always has at least one message. While this is typically true in the normal message flow, adding a guard would prevent potential runtime errors in edge cases.

Proposed defensive check
   onError({ currentTurn, error }) {
     const message = error instanceof Error ? error.message : String(error)
     const lastMessage = currentTurn.at(-1)!
+    if (!lastMessage) return
     if (error instanceof Error && error.name === 'ErrorRenderer') {
docs/src/tools/conversation.md (1)

108-109: Clarify saveMessages behavior when id is omitted.

The comment says "保存指定会话的消息" (save messages of specified conversation), but id is optional. Consider documenting that when id is omitted, it saves the active conversation's messages (if that's the intended behavior).

Suggested documentation update
-  /** 保存指定会话的消息 */
+  /** 保存指定会话的消息,不传 id 时保存当前活跃会话 */
   saveMessages: (id?: string) => void

- Added 'Content-Type' header to fetch requests in various demo components to ensure proper JSON handling.
- Implemented error handling for API responses, throwing an error with status information for better debugging.
- Updated multiple demo files including Assistant.vue, IndexedDB.vue, LocalStorage.vue, and others to improve robustness in message processing.
- Clarified the default activation status of built-in plugins: `fallbackRolePlugin`, `thinkingPlugin`, and `lengthPlugin`.
- Specified that `toolPlugin` is an optional plugin that requires explicit addition to the `plugins` array.
- Updated descriptions for each plugin to include information on customization and default behavior, improving clarity for users.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@docs/demos/tools/conversation/IndexedDB.vue`:
- Line 49: The current apiUrl assignment reads window.parent.location.origin
which can throw a SecurityError in cross-origin embeds; change the logic around
the apiUrl variable to safely detect/access parent origin: either first check if
window.parent === window (same window) or wrap access to
window.parent.location.origin in a try/catch and fall back to location.origin on
any exception; update the apiUrl initialization (the const named apiUrl) so it
uses the guarded check/try-catch and always resolves to a safe origin string.

In `@docs/demos/tools/message/OnBeforeRequest.ts`:
- Around line 10-12: The code reads window.parent.location.origin directly when
computing apiUrl (using meta and baseUrl) which can throw a SecurityError for
cross‑origin embeds; change the apiUrl assignment to attempt reading
window.parent.location.origin inside a try/catch and on any error or if
undefined fall back to location.origin + baseUrl so the hook never throws—keep
the existing meta and baseUrl variables and only wrap the parent origin access
for apiUrl.

In `@docs/demos/tools/storage/Custom.vue`:
- Line 95: The assignment to the apiUrl constant can throw a DOMException when
accessing window.parent.location.origin in cross-origin iframes; update the
apiUrl initialization (where apiUrl is defined in Custom.vue) to safely resolve
origin by wrapping the window.parent access in a try/catch (or checking
window.parent === window) and falling back to location.origin, and ensure any
caught DOMException is ignored or logged so the component can still mount.
🧹 Nitpick comments (2)
docs/demos/tools/conversation/LocalStorage.vue (1)

74-76: Avoid duplicating the storage key string.
Centralize it to prevent mismatches if it changes.

♻️ Proposed refactor
-// 使用 LocalStorage 策略
+const STORAGE_KEY = 'demo-conversations-localstorage'
+
+// 使用 LocalStorage 策略
 const {
   activeConversation,
   activeConversationId,
   conversations,
   createConversation,
   switchConversation,
   abortActiveRequest,
 } = useConversation({
   useMessageOptions: {
@@
   },
   storage: localStorageStrategyFactory({
-    key: 'demo-conversations-localstorage', // 自定义存储键名
+    key: STORAGE_KEY, // 自定义存储键名
   }),
 })
@@
 const clearStorage = () => {
   if (confirm('确定要清空所有会话数据吗?')) {
-    localStorage.removeItem('demo-conversations-localstorage')
+    localStorage.removeItem(STORAGE_KEY)
     location.reload()
   }
 }

Also applies to: 96-99

docs/demos/tools/storage/Custom.vue (1)

46-79: Consider cloning message objects to avoid shared-reference mutations.

saveMessages/loadMessages return shallow array copies, but the message objects are still shared. In-memory storage can then “persist” in-place mutations without explicit saves, which differs from persistent strategies. A small clone helper keeps behavior consistent.

♻️ Suggested change (clone on save/load)
 class MemoryStorageStrategy implements ConversationStorageStrategy {
   private conversations: ConversationInfo[] = []
   private messagesMap: Map<string, ChatMessage[]> = new Map()
+  private cloneMessages(messages: ChatMessage[]): ChatMessage[] {
+    return JSON.parse(JSON.stringify(messages))
+  }
 
   loadMessages(conversationId: string): ChatMessage[] {
-    return [...(this.messagesMap.get(conversationId) || [])]
+    return this.cloneMessages(this.messagesMap.get(conversationId) || [])
   }
 
   saveMessages(conversationId: string, messages: ChatMessage[]): void {
-    this.messagesMap.set(conversationId, [...messages])
+    this.messagesMap.set(conversationId, this.cloneMessages(messages))
   }
 }

@hexqi hexqi merged commit b599aa5 into opentiny:develop Jan 30, 2026
4 checks passed
@github-actions
Copy link
Contributor

🧹 Preview Cleaned Up

The preview deployment has been removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants