Skip to content

Feat:Migration of tiny robot version to v0.4.0#1804

Open
lichunn wants to merge 3 commits into
opentiny:developfrom
lichunn:feat/robot-330
Open

Feat:Migration of tiny robot version to v0.4.0#1804
lichunn wants to merge 3 commits into
opentiny:developfrom
lichunn:feat/robot-330

Conversation

@lichunn
Copy link
Copy Markdown
Collaborator

@lichunn lichunn commented May 14, 2026

English | 简体中文

PR

PR Checklist

Please check if your PR fulfills the following requirements:

  • The commit message follows our Commit Message Guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)
  • Built its own designer, fully self-validated

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

Background and solution

What is the current behavior?

Issue Number: N/A

What is the new behavior?

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Summary by CodeRabbit

  • New Features

    • Added voice input with speech-to-text recognition.
    • Added image upload capability in chat.
    • Aborted conversation messages now display an indicator.
  • Improvements

    • Enhanced chat message rendering and streaming.
    • Refined tool-call execution handling.
    • Improved active request management.
  • Updates

    • Robot plugin dependencies updated to v0.4.0.

Review Change Stack

@github-actions github-actions Bot added the enhancement New feature or request label May 14, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

Walkthrough

This PR refactors the robot plugin's AI client architecture from AIClient to OpenAICompatibleProvider, rebuilds conversation state management with async streaming and plugin-based completion handling, updates stream delta processing to support content merging, and enhances UI components for speech input and renderer configuration.

Changes

Provider Migration & Conversation Refactor

Layer / File(s) Summary
Status Constants and Dependencies Foundation
packages/plugins/robot/src/constants/status.ts, packages/plugins/robot/src/constants/index.ts, packages/plugins/robot/package.json
New STATUS enum (PENDING, STREAMING, FINISHED, ERROR, ABORTED), GeneratingStatus subset, and MessageState interface are introduced; constants are re-exported from index; dependency versions bumped to 0.4.0.
AI Provider Module Refactoring
packages/plugins/robot/src/services/aiClient.ts
AIClient instantiation removed; module now exports OpenAICompatibleProvider instance with retained getClientConfig and updateClientConfig helpers.
Conversation Adapter Architecture Rewrite
packages/plugins/robot/src/composables/core/useConversation.ts
useConversationAdapter refactored with new createResponseProvider async generator wrapping provider.chatStream with queuing and abort handling; updateMessageMetadata helper processes completion chunks; adapterPlugin handles finish/error callbacks; persistence uses localStorageStrategyFactory; methods assembled via apis object and spread in return.
Stream Delta and Tool-Call Processing
packages/plugins/robot/src/composables/core/useMessageStream.ts, packages/plugins/robot/src/composables/features/useToolCalls.ts
useMessageStream.ts adds "already merged" flags to handleDeltaReasoning and handleDeltaContent to prevent duplicate content updates; useToolCalls.ts switches to provider.chatStream, marks tools as handled, inserts placeholder assistant message, and continues streaming.
Chat Mode and Agent Mode Behaviors
packages/plugins/robot/src/composables/modes/useChatMode.ts, packages/plugins/robot/src/composables/modes/useAgentMode.ts
useChatMode.ts introduces updateToolCallState and updateToolCallRenderContent helpers for coordinated state/UI updates and removes explicit save calls; useAgentMode.ts uses provider and sources STATUS/MessageState from local constants.
Main Chat Composable Integration
packages/plugins/robot/src/composables/useChat.ts
Passes provider to composables; adds hasActiveRequest, abortRequest, interruptActiveRequest for request lifecycle; refines tool-call completion condition; removes placeholder loading message from sendUserMessage; exposes mappedStatus computed property mapping CHAT_STATUS to STATUS.
UI Components: Chat, Header, Markdown
packages/plugins/robot/src/Main.vue, packages/plugins/robot/src/components/chat/RobotChat.vue, packages/plugins/robot/src/components/header-extension/History.vue, packages/plugins/robot/src/components/renderers/MarkdownRenderer.vue
Main.vue binds mappedStatus instead of chatStatus; RobotChat.vue refactors renderer selection to content-renderer-matches, adds speech-input handlers, conditional upload button, and aborted message footer; History.vue updates to ConversationInfo type; MarkdownRenderer.vue accepts message object prop with nested content field.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A rabbit hops through the provider's door,
From AIClient to streaming core,
State merges gently, messages align,
Speech buttons whisper when you're online,
And conversations persist with care—
The refactor's magic fills the air! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: upgrading the tiny-robot plugin from v0.3.1 to v0.4.0 and migrating related components and composables to work with the new version.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

Copy link
Copy Markdown
Contributor

@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

Caution

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

⚠️ Outside diff range comments (4)
packages/plugins/robot/src/composables/useChat.ts (3)

95-122: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against lastMessage being undefined.

messages.at(-1) returns ChatMessage | undefined, but Line 98 (lastMessage.content) and Line 120 (lastMessage.content ?? '') both dereference it directly. If messages is ever empty when finish handling runs (e.g. an aborted request before any assistant message lands), this throws a TypeError. Add optional chaining or an early return.

🛡️ Proposed fix
 const lastMessage = messages.at(-1)
+ if (!lastMessage) return
 
 delete abortControllerMap.main
 await onRequestEnd(finishReason, lastMessage.content, messages) // 本次请求结束
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/plugins/robot/src/composables/useChat.ts` around lines 95 - 122, The
code assumes lastMessage = messages.at(-1) is always defined but it can be
undefined; update the endRequest/finalization logic to guard against that by
early-returning when lastMessage is falsy or using optional chaining when
accessing lastMessage.content and lastMessage.tool_calls; specifically adjust
the usages in this block (references: lastMessage, messages.at, onRequestEnd,
handleToolCall, onMessageProcessed, messageState, chatStatus) so you call
onRequestEnd with a safe string (or return early), skip tool handling and state
transitions when lastMessage is undefined, and avoid direct dereferences like
lastMessage.content or lastMessage.tool_calls without checks.

124-129: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Same messages.at(-1) undefined risk on Line 127.

messages.at(-1).content can throw when the array is empty (e.g. error fired before any message was appended). Mirror the optional-chaining fix from handleFinishRequest.

🛡️ Proposed fix
-  await onRequestEnd('error', messages.at(-1).content, messages, { error })
+  await onRequestEnd('error', messages.at(-1)?.content ?? '', messages, { error })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/plugins/robot/src/composables/useChat.ts` around lines 124 - 129, In
handleRequestError, avoid directly calling messages.at(-1).content which can
throw when messages is empty; mirror the fix from handleFinishRequest by using
optional chaining and a safe fallback (e.g., messages.at(-1)?.content ?? '')
when calling onRequestEnd so the function won’t throw if there’s no last
message; update the call site in handleRequestError accordingly.

1-317: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Run pnpm build:plugin to validate the package build.

The robot plugin package has no local test script; run the repository-level pnpm build:plugin command to ensure the changes compile correctly and published package behavior is unaffected. This is required by coding guidelines when build output or published behavior may change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/plugins/robot/src/composables/useChat.ts` around lines 1 - 317, Run
the repository-level build command `pnpm build:plugin` to reproduce the
compilation errors for the robot plugin and fix whatever TypeScript/build
problems surface in the package (packages/plugins/robot). Focus on correcting
imports/exports and types referenced in useChat.ts—check symbols like
initChatClient, beforeRequest, createStreamDataHandler, createToolCallHandler,
useConversationAdapter, createConversationWithMode and sendUserMessage for
missing/incorrect types or broken imports; iterate until `pnpm build:plugin`
completes successfully, then commit the fixes.
packages/plugins/robot/src/components/renderers/MarkdownRenderer.vue (1)

30-43: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix incorrect prop structure and unsafe default in MarkdownRenderer.

The message prop is misnamed and incorrectly typed. This renderer should receive a content prop (a markdown string to render), not a message object, consistent with other renderers like ImgRenderer and LoadingRenderer. The current implementation accesses props.message.content (line 69), which:

  1. Uses the wrong prop name and type (Options is markdown-it's config type, not a message type)
  2. Has an unsafe default: with default: () => ({}), props.message.content is undefined, and markdownIt.render(undefined) throws at runtime

Change the message prop to content (string), make it required or default to an empty string, and access it directly:

🛡️ Proposed fix
 const props = defineProps({
-  message: {
-    type: Object as () => Options,
-    default: () => ({})
+  content: {
+    type: String,
+    required: true
   },
   theme: {
     type: String as () => 'light' | 'dark',
     default: 'light'
   },
   options: {
     type: Object as () => Options,
     default: () => ({})
   }
 })

 const renderContent = computed(() => {
-  return DOMPurify.sanitize(markdownIt.render(props.message.content))
+  return DOMPurify.sanitize(markdownIt.render(props.content))
 })

Also applies to: line 69

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/plugins/robot/src/components/renderers/MarkdownRenderer.vue` around
lines 30 - 43, The prop definition in MarkdownRenderer (defineProps) is wrong:
replace the current message prop (typed as Options with default {}) with a
content prop of type String that is either required or has a safe default (e.g.,
empty string), keep theme and options as-is, and update all usages that read
props.message.content (e.g., where markdownIt.render is called) to use
props.content directly so markdownIt.render never receives undefined.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/plugins/robot/src/components/chat/RobotChat.vue`:
- Around line 153-185: The issue is that the markdown and image matchers in
contentRendererMatches inspect message.content instead of the per-item content
parameter, causing wrong matches; update the Markdown matcher (currently using
MarkdownRenderer) to use the signature find: (message: any, content: any) =>
!message.loading && content && content.type !== 'img' && content.type !==
'image' && content.type !== 'tool' (or similar check that content exists and is
not an image/tool), and update the Img matcher (currently using ImgRenderer) to
use find: (message: any, content: any) => content?.type === 'img' ||
content?.type === 'image' so both follow the same per-item content pattern as
Tools, Reasoning, and custom renderers.

In `@packages/plugins/robot/src/composables/modes/useChatMode.ts`:
- Around line 41-47: The updateToolCallRenderContent function lacks the same
guard as updateToolCallState and may match or create entries with an undefined
tool.id; add an early return at the top of updateToolCallRenderContent when
tool.id is falsy (same behavior as in updateToolCallState) so you never call
renderContent.find(... toolCallId === tool.id) or push a new entry with
toolCallId: undefined; reference the function name updateToolCallRenderContent
and the symbol tool.id/toolCallId when making the change.

In `@packages/plugins/robot/src/composables/useChat.ts`:
- Around line 198-214: abortRequest currently calls the async onRequestEnd
fire-and-forget and coerces message content with "as string", causing race
conditions, possible NPEs, and unhandled rejections; make abortRequest async,
synchronously capture a safe snapshot of the last message content and the
messages array (or await onRequestEnd before allowing conversation switch),
replace the unsafe "as string" cast with a guarded nullable value (e.g., check
existence before passing), and wrap the onRequestEnd invocation in try/catch (or
await it and propagate errors) so interruptActiveRequest can await abortRequest
and avoid appending the "aborted" block to the wrong conversation; reference
abortRequest, interruptActiveRequest, onRequestEnd, and messageManager.messages
when making these changes.

---

Outside diff comments:
In `@packages/plugins/robot/src/components/renderers/MarkdownRenderer.vue`:
- Around line 30-43: The prop definition in MarkdownRenderer (defineProps) is
wrong: replace the current message prop (typed as Options with default {}) with
a content prop of type String that is either required or has a safe default
(e.g., empty string), keep theme and options as-is, and update all usages that
read props.message.content (e.g., where markdownIt.render is called) to use
props.content directly so markdownIt.render never receives undefined.

In `@packages/plugins/robot/src/composables/useChat.ts`:
- Around line 95-122: The code assumes lastMessage = messages.at(-1) is always
defined but it can be undefined; update the endRequest/finalization logic to
guard against that by early-returning when lastMessage is falsy or using
optional chaining when accessing lastMessage.content and lastMessage.tool_calls;
specifically adjust the usages in this block (references: lastMessage,
messages.at, onRequestEnd, handleToolCall, onMessageProcessed, messageState,
chatStatus) so you call onRequestEnd with a safe string (or return early), skip
tool handling and state transitions when lastMessage is undefined, and avoid
direct dereferences like lastMessage.content or lastMessage.tool_calls without
checks.
- Around line 124-129: In handleRequestError, avoid directly calling
messages.at(-1).content which can throw when messages is empty; mirror the fix
from handleFinishRequest by using optional chaining and a safe fallback (e.g.,
messages.at(-1)?.content ?? '') when calling onRequestEnd so the function won’t
throw if there’s no last message; update the call site in handleRequestError
accordingly.
- Around line 1-317: Run the repository-level build command `pnpm build:plugin`
to reproduce the compilation errors for the robot plugin and fix whatever
TypeScript/build problems surface in the package (packages/plugins/robot). Focus
on correcting imports/exports and types referenced in useChat.ts—check symbols
like initChatClient, beforeRequest, createStreamDataHandler,
createToolCallHandler, useConversationAdapter, createConversationWithMode and
sendUserMessage for missing/incorrect types or broken imports; iterate until
`pnpm build:plugin` completes successfully, then commit the fixes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 714a8ea4-34b4-405d-8683-fbea959acc47

📥 Commits

Reviewing files that changed from the base of the PR and between 59e8548 and e3a7d22.

📒 Files selected for processing (14)
  • packages/plugins/robot/package.json
  • packages/plugins/robot/src/Main.vue
  • packages/plugins/robot/src/components/chat/RobotChat.vue
  • packages/plugins/robot/src/components/header-extension/History.vue
  • packages/plugins/robot/src/components/renderers/MarkdownRenderer.vue
  • packages/plugins/robot/src/composables/core/useConversation.ts
  • packages/plugins/robot/src/composables/core/useMessageStream.ts
  • packages/plugins/robot/src/composables/features/useToolCalls.ts
  • packages/plugins/robot/src/composables/modes/useAgentMode.ts
  • packages/plugins/robot/src/composables/modes/useChatMode.ts
  • packages/plugins/robot/src/composables/useChat.ts
  • packages/plugins/robot/src/constants/index.ts
  • packages/plugins/robot/src/constants/status.ts
  • packages/plugins/robot/src/services/aiClient.ts

Comment on lines +153 to +185
const contentRendererMatches = computed<BubbleContentRendererMatch[]>(() => [
{
priority: BubbleRendererMatchPriority.LOADING,
find: (message) => Boolean(message.loading),
renderer: LoadingRenderer
},
{
priority: BubbleRendererMatchPriority.NORMAL,
find: (message: any, content: any) => content?.type === 'tool' && message.tool_calls?.length,
renderer: BubbleRenderers.Tools
},
{
priority: BubbleRendererMatchPriority.NORMAL,
find: (message: any, content: any) =>
content?.type !== 'tool' && typeof message.reasoning_content === 'string' && message.reasoning_content,
renderer: BubbleRenderers.Reasoning
},
{
priority: BubbleRendererMatchPriority.NORMAL,
find: (message: any) => !message.loading && message.content,
renderer: MarkdownRenderer
},
{
priority: BubbleRendererMatchPriority.NORMAL,
find: (message: any) => message?.content?.[0]?.type === 'img' || message?.content?.[0]?.type === 'image',
renderer: ImgRenderer
},
...Object.entries(props.bubbleRenderers).map(([type, renderer]) => ({
priority: BubbleRendererMatchPriority.NORMAL,
find: (_message: any, content: any) => content?.type === type,
renderer
}))
])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

@opentiny/tiny-robot BubbleContentRendererMatch find signature content argument

💡 Result:

In @opentiny/tiny-robot, BubbleContentRendererMatch.find takes (message, content, contentIndex) and the match function’s signature is: find: (message: BubbleMessage, content: ChatMessageContentItem, contentIndex: number) => boolean [1] So the “content argument” (the second parameter) is the resolved/unified content item, and when content is an array the system uses contentIndex to select the specific item; the docs state: “content … 若为数组则取 contentIndex 对应项,若为字符串则转为 { type: 'text', text: string }” [1].

Citations:


🏁 Script executed:

fd -e vue -e ts -e js | grep -i robot | head -20

Repository: opentiny/tiny-engine

Length of output: 1077


🏁 Script executed:

find . -name "RobotChat.vue" -o -name "RobotChat.ts" | head -5

Repository: opentiny/tiny-engine

Length of output: 123


🏁 Script executed:

wc -l packages/plugins/robot/src/components/chat/RobotChat.vue

Repository: opentiny/tiny-engine

Length of output: 125


🏁 Script executed:

cat -n packages/plugins/robot/src/components/chat/RobotChat.vue | sed -n '140,200p'

Repository: opentiny/tiny-engine

Length of output: 2380


🏁 Script executed:

cat -n packages/plugins/robot/src/components/chat/RobotChat.vue | sed -n '280,330p'

Repository: opentiny/tiny-engine

Length of output: 1658


🏁 Script executed:

grep -n "renderContent" packages/plugins/robot/src/components/chat/RobotChat.vue | head -20

Repository: opentiny/tiny-engine

Length of output: 252


🏁 Script executed:

cat -n packages/plugins/robot/src/types/chat.types.ts

Repository: opentiny/tiny-engine

Length of output: 2222


Renderer matchers don't consistently use the per-item content parameter, causing some messages to render incorrectly.

The contentRendererMatches have two interrelated problems:

  1. Markdown matcher ignores per-item content type. At line 172, find: (message: any) => !message.loading && message.content checks the raw message.content property, while all other matchers (Tools at line 161, Reasoning at line 166, custom renderers at line 182) check the per-item content parameter. Since the library invokes find() per resolved content item, the markdown matcher should also check the per-item content type to avoid matching when an image or tool content item is passed.

  2. Image matcher reads from the wrong message property. At line 177, find: (message: any) => message?.content?.[0]?.type === 'img' inspects message.content[0], but file messages created in handleSendMessage (lines 300–309) have content: '' with the actual image stored under renderContent: [{ type: 'img', content: file.url }]. When the library invokes find() with the resolved content item from renderContent, the matcher ignores that per-item content parameter and instead checks message.content[0].type, which is always undefined. These file messages fall through to the markdown matcher and render as empty strings.

Both matchers should use the second parameter (content) like the Tools/Reasoning/custom matchers do:

Proposed fix
   {
     priority: BubbleRendererMatchPriority.NORMAL,
-    find: (message: any) => !message.loading && message.content,
+    find: (message: any, content: any) =>
+      !message.loading && (typeof content === 'string' || content?.type === 'text' || content?.type === 'markdown'),
     renderer: MarkdownRenderer
   },
   {
     priority: BubbleRendererMatchPriority.NORMAL,
-    find: (message: any) => message?.content?.[0]?.type === 'img' || message?.content?.[0]?.type === 'image',
+    find: (_message: any, content: any) => content?.type === 'img' || content?.type === 'image',
     renderer: ImgRenderer
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const contentRendererMatches = computed<BubbleContentRendererMatch[]>(() => [
{
priority: BubbleRendererMatchPriority.LOADING,
find: (message) => Boolean(message.loading),
renderer: LoadingRenderer
},
{
priority: BubbleRendererMatchPriority.NORMAL,
find: (message: any, content: any) => content?.type === 'tool' && message.tool_calls?.length,
renderer: BubbleRenderers.Tools
},
{
priority: BubbleRendererMatchPriority.NORMAL,
find: (message: any, content: any) =>
content?.type !== 'tool' && typeof message.reasoning_content === 'string' && message.reasoning_content,
renderer: BubbleRenderers.Reasoning
},
{
priority: BubbleRendererMatchPriority.NORMAL,
find: (message: any) => !message.loading && message.content,
renderer: MarkdownRenderer
},
{
priority: BubbleRendererMatchPriority.NORMAL,
find: (message: any) => message?.content?.[0]?.type === 'img' || message?.content?.[0]?.type === 'image',
renderer: ImgRenderer
},
...Object.entries(props.bubbleRenderers).map(([type, renderer]) => ({
priority: BubbleRendererMatchPriority.NORMAL,
find: (_message: any, content: any) => content?.type === type,
renderer
}))
])
const contentRendererMatches = computed<BubbleContentRendererMatch[]>(() => [
{
priority: BubbleRendererMatchPriority.LOADING,
find: (message) => Boolean(message.loading),
renderer: LoadingRenderer
},
{
priority: BubbleRendererMatchPriority.NORMAL,
find: (message: any, content: any) => content?.type === 'tool' && message.tool_calls?.length,
renderer: BubbleRenderers.Tools
},
{
priority: BubbleRendererMatchPriority.NORMAL,
find: (message: any, content: any) =>
content?.type !== 'tool' && typeof message.reasoning_content === 'string' && message.reasoning_content,
renderer: BubbleRenderers.Reasoning
},
{
priority: BubbleRendererMatchPriority.NORMAL,
find: (message: any, content: any) =>
!message.loading && (typeof content === 'string' || content?.type === 'text' || content?.type === 'markdown'),
renderer: MarkdownRenderer
},
{
priority: BubbleRendererMatchPriority.NORMAL,
find: (_message: any, content: any) => content?.type === 'img' || content?.type === 'image',
renderer: ImgRenderer
},
...Object.entries(props.bubbleRenderers).map(([type, renderer]) => ({
priority: BubbleRendererMatchPriority.NORMAL,
find: (_message: any, content: any) => content?.type === type,
renderer
}))
])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/plugins/robot/src/components/chat/RobotChat.vue` around lines 153 -
185, The issue is that the markdown and image matchers in contentRendererMatches
inspect message.content instead of the per-item content parameter, causing wrong
matches; update the Markdown matcher (currently using MarkdownRenderer) to use
the signature find: (message: any, content: any) => !message.loading && content
&& content.type !== 'img' && content.type !== 'image' && content.type !== 'tool'
(or similar check that content exists and is not an image/tool), and update the
Img matcher (currently using ImgRenderer) to use find: (message: any, content:
any) => content?.type === 'img' || content?.type === 'image' so both follow the
same per-item content pattern as Tools, Reasoning, and custom renderers.

Comment on lines +41 to +47
const updateToolCallRenderContent = (
tool: Record<string, unknown>,
currentMessage: any,
{ status, result }: { status?: string; result?: object | string } = {}
) => {
const renderContent = currentMessage.renderContent || (currentMessage.renderContent = [])
const currentToolCallContent = renderContent.find((item: any) => item.type === 'tool' && item.toolCallId === tool.id)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard tool.id consistently with updateToolCallState.

updateToolCallState early-returns when tool.id is falsy (line 24), but updateToolCallRenderContent does not. If a streamed tool delta arrives without an id, the find on Line 47 will match the first existing entry whose toolCallId is also undefined and silently overwrite it, or push a new entry with toolCallId: undefined, causing multiple distinct tool calls to collide on the same render block. Apply the same guard for consistency.

🛡️ Proposed guard
 const updateToolCallRenderContent = (
   tool: Record<string, unknown>,
   currentMessage: any,
   { status, result }: { status?: string; result?: object | string } = {}
 ) => {
+  if (!tool.id) {
+    return
+  }
+
   const renderContent = currentMessage.renderContent || (currentMessage.renderContent = [])
   const currentToolCallContent = renderContent.find((item: any) => item.type === 'tool' && item.toolCallId === tool.id)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const updateToolCallRenderContent = (
tool: Record<string, unknown>,
currentMessage: any,
{ status, result }: { status?: string; result?: object | string } = {}
) => {
const renderContent = currentMessage.renderContent || (currentMessage.renderContent = [])
const currentToolCallContent = renderContent.find((item: any) => item.type === 'tool' && item.toolCallId === tool.id)
const updateToolCallRenderContent = (
tool: Record<string, unknown>,
currentMessage: any,
{ status, result }: { status?: string; result?: object | string } = {}
) => {
if (!tool.id) {
return
}
const renderContent = currentMessage.renderContent || (currentMessage.renderContent = [])
const currentToolCallContent = renderContent.find((item: any) => item.type === 'tool' && item.toolCallId === tool.id)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/plugins/robot/src/composables/modes/useChatMode.ts` around lines 41
- 47, The updateToolCallRenderContent function lacks the same guard as
updateToolCallState and may match or create entries with an undefined tool.id;
add an early return at the top of updateToolCallRenderContent when tool.id is
falsy (same behavior as in updateToolCallState) so you never call
renderContent.find(... toolCallId === tool.id) or push a new entry with
toolCallId: undefined; reference the function name updateToolCallRenderContent
and the symbol tool.id/toolCallId when making the change.

Comment on lines +198 to +214
const abortRequest = () => {
Object.values(abortControllerMap).forEach((controller) => controller?.abort())
for (const key of Object.keys(abortControllerMap)) {
delete abortControllerMap[key]
}
chatStatus.value = CHAT_STATUS.FINISHED

onRequestEnd('aborted', messageManager.messages.value.at(-1)?.content as string, messageManager.messages.value)
}

const interruptActiveRequest = () => {
if (!hasActiveRequest()) {
return
}

abortRequest()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

onRequestEnd is fire-and-forget; potential race with conversation switch.

A few concerns in abortRequest:

  1. onRequestEnd is async (and pushes into messages.at(-1)!.renderContent in useChatMode), but is invoked without await. When interruptActiveRequest() is called from createConversation/switchConversation, the async callback may run after the new conversation’s messages are swapped in, causing the "aborted" error block to be appended to the wrong conversation's last message. Capture the messages snapshot synchronously or await before swapping.
  2. as string on Line 205 masks the fact that at(-1)?.content can be undefined; in onRequestEnd messages.at(-1)!.renderContent.push(...) will then NPE if messages is empty.
  3. There’s no try/catch around the fire-and-forget call, so any error inside hooks is unhandled.
🛡️ Proposed refinement
 const abortRequest = () => {
   Object.values(abortControllerMap).forEach((controller) => controller?.abort())
   for (const key of Object.keys(abortControllerMap)) {
     delete abortControllerMap[key]
   }
   chatStatus.value = CHAT_STATUS.FINISHED
 
-  onRequestEnd('aborted', messageManager.messages.value.at(-1)?.content as string, messageManager.messages.value)
+  const messages = messageManager.messages.value
+  if (messages.length === 0) return
+  Promise.resolve(onRequestEnd('aborted', messages.at(-1)?.content ?? '', messages)).catch((err) =>
+    console.error('onRequestEnd(aborted) failed', err)
+  )
 }

If callers can tolerate it, prefer making abortRequest async and await-ing it in interruptActiveRequest so conversation switches see the cleanup complete first.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const abortRequest = () => {
Object.values(abortControllerMap).forEach((controller) => controller?.abort())
for (const key of Object.keys(abortControllerMap)) {
delete abortControllerMap[key]
}
chatStatus.value = CHAT_STATUS.FINISHED
onRequestEnd('aborted', messageManager.messages.value.at(-1)?.content as string, messageManager.messages.value)
}
const interruptActiveRequest = () => {
if (!hasActiveRequest()) {
return
}
abortRequest()
}
const abortRequest = () => {
Object.values(abortControllerMap).forEach((controller) => controller?.abort())
for (const key of Object.keys(abortControllerMap)) {
delete abortControllerMap[key]
}
chatStatus.value = CHAT_STATUS.FINISHED
const messages = messageManager.messages.value
if (messages.length === 0) return
Promise.resolve(onRequestEnd('aborted', messages.at(-1)?.content ?? '', messages)).catch((err) =>
console.error('onRequestEnd(aborted) failed', err)
)
}
const interruptActiveRequest = () => {
if (!hasActiveRequest()) {
return
}
abortRequest()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/plugins/robot/src/composables/useChat.ts` around lines 198 - 214,
abortRequest currently calls the async onRequestEnd fire-and-forget and coerces
message content with "as string", causing race conditions, possible NPEs, and
unhandled rejections; make abortRequest async, synchronously capture a safe
snapshot of the last message content and the messages array (or await
onRequestEnd before allowing conversation switch), replace the unsafe "as
string" cast with a guarded nullable value (e.g., check existence before
passing), and wrap the onRequestEnd invocation in try/catch (or await it and
propagate errors) so interruptActiveRequest can await abortRequest and avoid
appending the "aborted" block to the wrong conversation; reference abortRequest,
interruptActiveRequest, onRequestEnd, and messageManager.messages when making
these changes.

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant