mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Refactor: Optimize React component rendering and improve robustness
This commit introduces several changes aimed at improving UI rendering performance and overall robustness, potentially addressing "grey screen" or unresponsiveness issues.
1. **Optimized Memoization for Chat Rows:**
* Replaced generic `deepEqual` with custom comparison functions for `React.memo` in `ChatRow.tsx` and `BrowserSessionRow.tsx`. These custom functions perform more targeted comparisons of props, focusing only on fields relevant to rendering, which should reduce the overhead of memoization and prevent unnecessary re-renders.
* The internal `ChatRowContentComponent` in `ChatRow.tsx` was also wrapped with `React.memo`.
2. **Increased Robustness:**
* Added `try-catch` blocks around `JSON.parse` calls within `BrowserSessionRow.tsx` to prevent runtime errors from malformed JSON in message text.
3. **Code Analysis Confirmations:**
* Analysis of `ChatView.tsx` indicated that its `useEffect` dependency arrays were already in a reasonably optimized state.
* Review of `ClineProvider.ts` confirmed that its `dispose` method is comprehensive and correctly wired to the `onDidDispose` event of webview panels, ensuring cleanup of tab-specific provider instances.
* Review of `ShadowCheckpointService.ts` confirmed that the `renameNestedGitRepos` method and its usage in `stageAll` include appropriate `try...catch` and `try...finally` blocks for robust handling of file system operations.
These changes collectively aim to make the UI more efficient and the extension more stable.
This commit is contained in:
parent
ea93cea9ac
commit
f12302b526
3 changed files with 261 additions and 51 deletions
|
|
@ -1,6 +1,5 @@
|
|||
import React, { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useSize } from "react-use"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
|
|
@ -37,13 +36,18 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
|||
|
||||
const isLastApiReqInterrupted = useMemo(() => {
|
||||
// Check if last api_req_started is cancelled
|
||||
const lastApiReqStarted = [...messages].reverse().find((m) => m.say === "api_req_started")
|
||||
if (lastApiReqStarted?.text) {
|
||||
const info = JSON.parse(lastApiReqStarted.text) as { cancelReason: string | null }
|
||||
if (info && info.cancelReason !== null) {
|
||||
return true
|
||||
const lastApiReqStartedInGroup = [...messages].reverse().find((m) => m.say === "api_req_started")
|
||||
if (lastApiReqStartedInGroup?.text) {
|
||||
try {
|
||||
const info = JSON.parse(lastApiReqStartedInGroup.text) as { cancelReason: string | null }
|
||||
if (info && info.cancelReason !== null) {
|
||||
return true
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore parse error if text is not json
|
||||
}
|
||||
}
|
||||
// also check the global lastModifiedMessage which might be outside this group
|
||||
const lastApiReqFailed = isLast && lastModifiedMessage?.ask === "api_req_failed"
|
||||
if (lastApiReqFailed) {
|
||||
return true
|
||||
|
|
@ -216,11 +220,15 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
|||
// Look through current page's next actions for the latest browser_action
|
||||
const actions = currentPage?.nextAction?.messages || []
|
||||
for (let i = actions.length - 1; i >= 0; i--) {
|
||||
const message = actions[i]
|
||||
if (message.say === "browser_action") {
|
||||
const browserAction = JSON.parse(message.text || "{}") as ClineSayBrowserAction
|
||||
if (browserAction.action === "click" && browserAction.coordinate) {
|
||||
return browserAction.coordinate
|
||||
const currentMessage = actions[i]
|
||||
if (currentMessage.say === "browser_action") {
|
||||
try {
|
||||
const browserAction = JSON.parse(currentMessage.text || "{}") as ClineSayBrowserAction
|
||||
if (browserAction.action === "click" && browserAction.coordinate) {
|
||||
return browserAction.coordinate
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore parse error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -408,8 +416,68 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
|||
}, [rowHeight, isLast, onHeightChange])
|
||||
|
||||
return browserSessionRow
|
||||
}, deepEqual)
|
||||
}, browserSessionRowPropsAreEqual)
|
||||
|
||||
function browserSessionRowPropsAreEqual(
|
||||
prevProps: BrowserSessionRowProps,
|
||||
nextProps: BrowserSessionRowProps,
|
||||
): boolean {
|
||||
if (
|
||||
prevProps.isLast !== nextProps.isLast ||
|
||||
prevProps.isStreaming !== nextProps.isStreaming ||
|
||||
prevProps.isExpanded !== nextProps.isExpanded || // function ref
|
||||
prevProps.onToggleExpand !== nextProps.onToggleExpand || // function ref
|
||||
prevProps.onHeightChange !== nextProps.onHeightChange // function ref
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare lastModifiedMessage by relevant fields
|
||||
const prevLMM = prevProps.lastModifiedMessage
|
||||
const nextLMM = nextProps.lastModifiedMessage
|
||||
if (prevLMM && nextLMM) {
|
||||
if (prevLMM.ask !== nextLMM.ask || prevLMM.text !== nextLMM.text || prevLMM.say !== nextLMM.say) {
|
||||
return false
|
||||
}
|
||||
} else if (prevLMM || nextLMM) {
|
||||
// one is null/undefined, the other is not
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare messages array
|
||||
if (prevProps.messages.length !== nextProps.messages.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (let i = 0; i < prevProps.messages.length; i++) {
|
||||
const prevMsg = prevProps.messages[i]
|
||||
const nextMsg = nextProps.messages[i]
|
||||
|
||||
if (
|
||||
prevMsg.type !== nextMsg.type ||
|
||||
prevMsg.ask !== nextMsg.ask ||
|
||||
prevMsg.say !== nextMsg.say ||
|
||||
prevMsg.text !== nextMsg.text || // This text can be JSON, careful if it has unstable formatting
|
||||
prevMsg.partial !== nextMsg.partial ||
|
||||
prevMsg.progressStatus !== nextMsg.progressStatus ||
|
||||
prevMsg.ts !== nextMsg.ts
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// For fields used by ChatRowContent (images, checkpoint, contextCondense)
|
||||
// Assuming ChatRowContent itself has its own memoization or these fields are less critical for BrowserSessionRow's direct rendering
|
||||
// If ChatRowContent's rendering based on these is critical for BrowserSessionRow's own layout,
|
||||
// then these checks would be needed here too.
|
||||
// For now, focusing on fields directly used by BrowserSessionRow logic or passed to ChatRowContent for its core identity.
|
||||
// A more thorough comparison would include shallow or deep checks for:
|
||||
// prevMsg.images vs nextMsg.images
|
||||
// prevMsg.checkpoint vs nextMsg.checkpoint
|
||||
// prevMsg.contextCondense vs nextMsg.contextCondense
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
interface BrowserSessionRowContentProps extends Omit<BrowserSessionRowProps, "messages"> {
|
||||
message: ClineMessage
|
||||
setMaxActionHeight: (height: number) => void
|
||||
|
|
@ -459,13 +527,30 @@ const BrowserSessionRowContent = ({
|
|||
|
||||
case "browser_action":
|
||||
const browserAction = JSON.parse(message.text || "{}") as ClineSayBrowserAction
|
||||
return (
|
||||
<BrowserActionBox
|
||||
action={browserAction.action}
|
||||
coordinate={browserAction.coordinate}
|
||||
text={browserAction.text}
|
||||
/>
|
||||
)
|
||||
try {
|
||||
const browserAction = JSON.parse(message.text || "{}") as ClineSayBrowserAction
|
||||
return (
|
||||
<BrowserActionBox
|
||||
action={browserAction.action}
|
||||
coordinate={browserAction.coordinate}
|
||||
text={browserAction.text}
|
||||
/>
|
||||
)
|
||||
} catch (e) {
|
||||
// If JSON parsing fails, render as simple text or error
|
||||
return (
|
||||
<div style={{ padding: "10px 0 10px 0" }}>
|
||||
<ChatRowContent
|
||||
message={{ ...message, say: "error", text: "Invalid browser action format" }}
|
||||
isExpanded={isExpanded(message.ts)}
|
||||
onToggleExpand={() => onToggleExpand(message.ts)}
|
||||
lastModifiedMessage={lastModifiedMessage}
|
||||
isLast={isLast}
|
||||
isStreaming={isStreaming}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { memo, useEffect, useMemo, useRef, useState } from "react"
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useSize } from "react-use"
|
||||
import { useTranslation, Trans } from "react-i18next"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import { VSCodeBadge, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { ClineApiReqInfo, ClineAskUseMcpServer, ClineMessage, ClineSayTool } from "@roo/shared/ExtensionMessage"
|
||||
|
|
@ -77,13 +76,96 @@ const ChatRow = memo(
|
|||
// we cannot return null as virtuoso does not support it, so we use a separate visibleMessages array to filter out messages that should not be rendered
|
||||
return chatrow
|
||||
},
|
||||
// memo does shallow comparison of props, so we need to do deep comparison of arrays/objects whose properties might change
|
||||
deepEqual,
|
||||
// Custom comparison function for React.memo
|
||||
chatRowPropsAreEqual,
|
||||
)
|
||||
|
||||
export default ChatRow
|
||||
function chatRowPropsAreEqual(prevProps: ChatRowProps, nextProps: ChatRowProps): boolean {
|
||||
// Compare primitive props directly
|
||||
if (
|
||||
prevProps.isExpanded !== nextProps.isExpanded ||
|
||||
prevProps.isLast !== nextProps.isLast ||
|
||||
prevProps.isStreaming !== nextProps.isStreaming
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
export const ChatRowContent = ({
|
||||
// Compare callback functions by reference
|
||||
if (
|
||||
prevProps.onToggleExpand !== nextProps.onToggleExpand ||
|
||||
prevProps.onHeightChange !== nextProps.onHeightChange ||
|
||||
prevProps.onSuggestionClick !== nextProps.onSuggestionClick
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare 'message' object by specific fields
|
||||
const prevMsg = prevProps.message
|
||||
const nextMsg = nextProps.message
|
||||
|
||||
if (
|
||||
prevMsg.type !== nextMsg.type ||
|
||||
prevMsg.ask !== nextMsg.ask ||
|
||||
prevMsg.say !== nextMsg.say ||
|
||||
prevMsg.text !== nextMsg.text ||
|
||||
prevMsg.partial !== nextMsg.partial ||
|
||||
prevMsg.progressStatus !== nextMsg.progressStatus ||
|
||||
prevMsg.ts !== nextMsg.ts
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare 'message.images' array (shallow comparison of array and its string elements)
|
||||
if (prevMsg.images?.length !== nextMsg.images?.length) return false
|
||||
if (prevMsg.images && nextMsg.images) {
|
||||
for (let i = 0; i < prevMsg.images.length; i++) {
|
||||
if (prevMsg.images[i] !== nextMsg.images[i]) return false
|
||||
}
|
||||
} else if (prevMsg.images || nextMsg.images) {
|
||||
// one is null/undefined and the other is not
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare 'message.checkpoint' object
|
||||
if (prevMsg.checkpoint?.id !== nextMsg.checkpoint?.id) return false
|
||||
if (prevMsg.checkpoint?.description !== nextMsg.checkpoint?.description) return false
|
||||
|
||||
// Compare 'message.contextCondense' object
|
||||
const prevCondense = prevMsg.contextCondense
|
||||
const nextCondense = nextMsg.contextCondense
|
||||
if (
|
||||
prevCondense?.keptCharacters !== nextCondense?.keptCharacters ||
|
||||
prevCondense?.totalCharacters !== nextCondense?.totalCharacters ||
|
||||
prevCondense?.removedCharacters !== nextCondense?.removedCharacters ||
|
||||
prevCondense?.removedMessages !== nextCondense?.removedMessages ||
|
||||
prevCondense?.removedTokens !== nextCondense?.removedTokens
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare 'lastModifiedMessage' object (if it exists)
|
||||
const prevLastModMsg = prevProps.lastModifiedMessage
|
||||
const nextLastModMsg = nextProps.lastModifiedMessage
|
||||
|
||||
if (prevLastModMsg && nextLastModMsg) {
|
||||
if (
|
||||
prevLastModMsg.ask !== nextLastModMsg.ask ||
|
||||
prevLastModMsg.text !== nextLastModMsg.text ||
|
||||
prevLastModMsg.say !== nextLastModMsg.say
|
||||
) {
|
||||
return false
|
||||
}
|
||||
} else if (prevLastModMsg || nextLastModMsg) {
|
||||
// One exists and the other doesn't
|
||||
return false
|
||||
}
|
||||
|
||||
// All relevant props are equal
|
||||
return true
|
||||
}
|
||||
|
||||
export default ChatRow
|
||||
const ChatRowContentComponent = ({
|
||||
message,
|
||||
lastModifiedMessage,
|
||||
isExpanded,
|
||||
|
|
@ -1103,3 +1185,10 @@ export const ChatRowContent = ({
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Memoize ChatRowContent to prevent re-renders if its specific props haven't changed.
|
||||
// This is particularly useful because ChatRow itself is memoized with a custom function,
|
||||
// but ChatRowContent is the one doing the bulk of the rendering logic.
|
||||
// We can use a simple shallow comparison (React.memo's default) or a more specific one if needed.
|
||||
// For now, default shallow comparison should be a good starting point if its props are mostly primitive or stable.
|
||||
export const ChatRowContent = React.memo(ChatRowContentComponent)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"
|
||||
import { useDeepCompareEffect, useEvent, useMount } from "react-use"
|
||||
import { useEvent, useMount } from "react-use"
|
||||
import debounce from "debounce"
|
||||
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
|
||||
import removeMd from "remove-markdown"
|
||||
|
|
@ -146,6 +146,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
const lastMessage = useMemo(() => messages.at(-1), [messages])
|
||||
const secondLastMessage = useMemo(() => messages.at(-2), [messages])
|
||||
|
||||
// Memoized values for useEffect dependencies
|
||||
const lastMessageType = lastMessage?.type
|
||||
const lastMessageAsk = lastMessage?.ask
|
||||
const lastMessagePartial = lastMessage?.partial
|
||||
const lastMessageText = lastMessage?.text
|
||||
const secondLastMessageAsk = secondLastMessage?.ask
|
||||
const secondLastMessageSay = secondLastMessage?.say
|
||||
const lastMessageSay = lastMessage?.say
|
||||
|
||||
// Setup sound hooks with use-sound
|
||||
const volume = typeof soundVolume === "number" ? soundVolume : 0.5
|
||||
const soundConfig = {
|
||||
|
|
@ -185,15 +194,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
vscode.postMessage({ type: "playTts", text })
|
||||
}
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
useEffect(() => {
|
||||
// if last message is an ask, show user ask UI
|
||||
// if user finished a task, then start a new task with a new conversation history since in this moment that the extension is waiting for user response, the user could close the extension and the conversation history would be lost.
|
||||
// basically as long as a task is active, the conversation history will be persisted
|
||||
if (lastMessage) {
|
||||
switch (lastMessage.type) {
|
||||
switch (lastMessageType) {
|
||||
case "ask":
|
||||
const isPartial = lastMessage.partial === true
|
||||
switch (lastMessage.ask) {
|
||||
const isPartial = lastMessagePartial === true
|
||||
switch (lastMessageAsk) {
|
||||
case "api_req_failed":
|
||||
playSound("progress_loop")
|
||||
setSendingDisabled(true)
|
||||
|
|
@ -231,7 +240,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
setSendingDisabled(isPartial)
|
||||
setClineAsk("tool")
|
||||
setEnableButtons(!isPartial)
|
||||
const tool = JSON.parse(lastMessage.text || "{}") as ClineSayTool
|
||||
const tool = JSON.parse(lastMessageText || "{}") as ClineSayTool
|
||||
switch (tool.tool) {
|
||||
case "editedExistingFile":
|
||||
case "appliedDiff":
|
||||
|
|
@ -325,12 +334,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
case "say":
|
||||
// Don't want to reset since there could be a "say" after
|
||||
// an "ask" while ask is waiting for response.
|
||||
switch (lastMessage.say) {
|
||||
switch (lastMessageSay) {
|
||||
case "api_req_retry_delayed":
|
||||
setSendingDisabled(true)
|
||||
break
|
||||
case "api_req_started":
|
||||
if (secondLastMessage?.ask === "command_output") {
|
||||
if (secondLastMessageAsk === "command_output") {
|
||||
// If the last ask is a command_output, and we
|
||||
// receive an api_req_started, then that means
|
||||
// the command has finished and we don't need
|
||||
|
|
@ -359,7 +368,18 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
break
|
||||
}
|
||||
}
|
||||
}, [lastMessage, secondLastMessage])
|
||||
}, [
|
||||
lastMessage, // Keep lastMessage itself for isAutoApproved and direct parsing
|
||||
lastMessageType,
|
||||
lastMessageAsk,
|
||||
lastMessagePartial,
|
||||
lastMessageText,
|
||||
secondLastMessageAsk,
|
||||
secondLastMessageSay,
|
||||
lastMessageSay,
|
||||
isAutoApproved, // isAutoApproved is memoized and depends on lastMessage internally
|
||||
t,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length === 0) {
|
||||
|
|
@ -872,12 +892,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
// labeled as `user_feedback`.
|
||||
if (lastMessage && messages.length > 1) {
|
||||
if (
|
||||
lastMessage.text && // has text
|
||||
(lastMessage.say === "text" || lastMessage.say === "completion_result") && // is a text message
|
||||
!lastMessage.partial && // not a partial message
|
||||
!lastMessage.text.startsWith("{") // not a json object
|
||||
lastMessageText && // has text
|
||||
(lastMessageSay === "text" || lastMessageSay === "completion_result") && // is a text message
|
||||
!lastMessagePartial && // not a partial message
|
||||
!lastMessageText.startsWith("{") // not a json object
|
||||
) {
|
||||
let text = lastMessage?.text || ""
|
||||
let text = lastMessageText || ""
|
||||
const mermaidRegex = /```mermaid[\s\S]*?```/g
|
||||
// remove mermaid diagrams from text
|
||||
text = text.replace(mermaidRegex, "")
|
||||
|
|
@ -897,8 +917,21 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
}
|
||||
|
||||
// Update previous value.
|
||||
setWasStreaming(isStreaming)
|
||||
}, [isStreaming, lastMessage, wasStreaming, isAutoApproved, messages.length])
|
||||
// The logic for wasStreaming is to prevent re-playing TTS when isStreaming flips from true to false
|
||||
// if other dependencies haven't changed. If isStreaming is the only thing that changes,
|
||||
// and it becomes false, we don't want to re-evaluate TTS for the same last message.
|
||||
if (wasStreaming !== isStreaming) {
|
||||
setWasStreaming(isStreaming)
|
||||
}
|
||||
}, [
|
||||
isStreaming,
|
||||
wasStreaming, // Keep wasStreaming to compare its previous state
|
||||
lastMessageText,
|
||||
lastMessageSay,
|
||||
lastMessagePartial,
|
||||
messages.length, // messages.length is a simple primitive
|
||||
lastMessage, // Required for the messages.length > 1 check and ensuring lastMessage exists
|
||||
])
|
||||
|
||||
const isBrowserSessionMessage = (message: ClineMessage): boolean => {
|
||||
// Which of visible messages are browser session messages, see above.
|
||||
|
|
@ -1179,13 +1212,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
}
|
||||
|
||||
const autoApprove = async () => {
|
||||
if (lastMessage?.ask && isAutoApproved(lastMessage)) {
|
||||
if (lastMessageAsk && isAutoApproved(lastMessage)) {
|
||||
// Note that `isAutoApproved` can only return true if
|
||||
// lastMessage is an ask of type "browser_action_launch",
|
||||
// "use_mcp_server", "command", or "tool".
|
||||
|
||||
// Add delay for write operations.
|
||||
if (lastMessage.ask === "tool" && isWriteToolAction(lastMessage)) {
|
||||
if (lastMessageAsk === "tool" && isWriteToolAction(lastMessage)) {
|
||||
await new Promise((resolve) => setTimeout(resolve, writeDelayMs))
|
||||
}
|
||||
|
||||
|
|
@ -1205,7 +1238,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
}, [
|
||||
clineAsk,
|
||||
enableButtons,
|
||||
handlePrimaryButtonClick,
|
||||
// handlePrimaryButtonClick, // Not used directly in this effect's logic
|
||||
alwaysAllowBrowser,
|
||||
alwaysAllowReadOnly,
|
||||
alwaysAllowReadOnlyOutsideWorkspace,
|
||||
|
|
@ -1213,13 +1246,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
alwaysAllowWriteOutsideWorkspace,
|
||||
alwaysAllowExecute,
|
||||
alwaysAllowMcp,
|
||||
messages,
|
||||
allowedCommands,
|
||||
mcpServers,
|
||||
isAutoApproved,
|
||||
lastMessage,
|
||||
// messages, // Removed as per optimization
|
||||
// allowedCommands, // Part of isAutoApproved's dependencies
|
||||
// mcpServers, // Part of isAutoApproved's dependencies
|
||||
isAutoApproved, // Memoized: depends on allowedCommands, mcpServers, lastMessage etc.
|
||||
lastMessage, // Keep lastMessage for isAutoApproved and isWriteToolAction
|
||||
lastMessageAsk, // Specific primitive from lastMessage
|
||||
lastMessageType, // Specific primitive from lastMessage
|
||||
lastMessageText, // Specific primitive from lastMessage
|
||||
writeDelayMs,
|
||||
isWriteToolAction,
|
||||
isWriteToolAction, // Memoized: depends on lastMessage
|
||||
])
|
||||
|
||||
// Function to handle mode switching
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue