Feat:Migration of tiny robot version to v0.4.0#1804
Conversation
WalkthroughThis PR refactors the robot plugin's AI client architecture from ChangesProvider Migration & Conversation Refactor
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
There was a problem hiding this comment.
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 winGuard against
lastMessagebeing undefined.
messages.at(-1)returnsChatMessage | undefined, but Line 98 (lastMessage.content) and Line 120 (lastMessage.content ?? '') both dereference it directly. Ifmessagesis 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 winSame
messages.at(-1)undefined risk on Line 127.
messages.at(-1).contentcan throw when the array is empty (e.g. error fired before any message was appended). Mirror the optional-chaining fix fromhandleFinishRequest.🛡️ 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 winRun
pnpm build:pluginto validate the package build.The robot plugin package has no local test script; run the repository-level
pnpm build:plugincommand 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 winFix incorrect prop structure and unsafe default in MarkdownRenderer.
The
messageprop is misnamed and incorrectly typed. This renderer should receive acontentprop (a markdown string to render), not amessageobject, consistent with other renderers likeImgRendererandLoadingRenderer. The current implementation accessesprops.message.content(line 69), which:
- Uses the wrong prop name and type (
Optionsis markdown-it's config type, not a message type)- Has an unsafe default: with
default: () => ({}),props.message.contentisundefined, andmarkdownIt.render(undefined)throws at runtimeChange the
messageprop tocontent(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
📒 Files selected for processing (14)
packages/plugins/robot/package.jsonpackages/plugins/robot/src/Main.vuepackages/plugins/robot/src/components/chat/RobotChat.vuepackages/plugins/robot/src/components/header-extension/History.vuepackages/plugins/robot/src/components/renderers/MarkdownRenderer.vuepackages/plugins/robot/src/composables/core/useConversation.tspackages/plugins/robot/src/composables/core/useMessageStream.tspackages/plugins/robot/src/composables/features/useToolCalls.tspackages/plugins/robot/src/composables/modes/useAgentMode.tspackages/plugins/robot/src/composables/modes/useChatMode.tspackages/plugins/robot/src/composables/useChat.tspackages/plugins/robot/src/constants/index.tspackages/plugins/robot/src/constants/status.tspackages/plugins/robot/src/services/aiClient.ts
| 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 | ||
| })) | ||
| ]) |
There was a problem hiding this comment.
🧩 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 -20Repository: opentiny/tiny-engine
Length of output: 1077
🏁 Script executed:
find . -name "RobotChat.vue" -o -name "RobotChat.ts" | head -5Repository: opentiny/tiny-engine
Length of output: 123
🏁 Script executed:
wc -l packages/plugins/robot/src/components/chat/RobotChat.vueRepository: 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 -20Repository: opentiny/tiny-engine
Length of output: 252
🏁 Script executed:
cat -n packages/plugins/robot/src/types/chat.types.tsRepository: 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:
-
Markdown matcher ignores per-item content type. At line 172,
find: (message: any) => !message.loading && message.contentchecks the rawmessage.contentproperty, while all other matchers (Tools at line 161, Reasoning at line 166, custom renderers at line 182) check the per-itemcontentparameter. Since the library invokesfind()per resolved content item, the markdown matcher should also check the per-itemcontenttype to avoid matching when an image or tool content item is passed. -
Image matcher reads from the wrong message property. At line 177,
find: (message: any) => message?.content?.[0]?.type === 'img'inspectsmessage.content[0], but file messages created inhandleSendMessage(lines 300–309) havecontent: ''with the actual image stored underrenderContent: [{ type: 'img', content: file.url }]. When the library invokesfind()with the resolved content item fromrenderContent, the matcher ignores that per-itemcontentparameter and instead checksmessage.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.
| 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.
| 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) |
There was a problem hiding this comment.
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.
| 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.
| 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() | ||
| } |
There was a problem hiding this comment.
onRequestEnd is fire-and-forget; potential race with conversation switch.
A few concerns in abortRequest:
onRequestEndis async (and pushes intomessages.at(-1)!.renderContentinuseChatMode), but is invoked withoutawait. WheninterruptActiveRequest()is called fromcreateConversation/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.as stringon Line 205 masks the fact thatat(-1)?.contentcan beundefined; inonRequestEndmessages.at(-1)!.renderContent.push(...)will then NPE ifmessagesis empty.- There’s no
try/catcharound 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.
| 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.
English | 简体中文
PR
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
Background and solution
What is the current behavior?
Issue Number: N/A
What is the new behavior?
Does this PR introduce a breaking change?
Other information
Summary by CodeRabbit
New Features
Improvements
Updates