Compare commits

...
Author SHA1 Message Date
Arafatkatze b1e4c74d65 fixing 2026-01-29 21:02:37 -08:00
Arafatkatze 78baccedb6 fix 2026-01-19 16:48:15 -08:00
Arafatkatze 84229fd094 Adding Queue message 2026-01-19 16:47:13 -08:00
Arafatkatze ee89be883f feat(chat): improve message queue reliability with optimistic retention
- Keep messages in queue until successfully sent instead of removing optimistically
- Return boolean from handleSendMessage to indicate send success/failure
- Remove messages from queue only after confirmed delivery
- Refactor message handler with early returns for cleaner flow
- Preserve user input when processing queued messages
2026-01-19 16:43:47 -08:00
Arafatkatze 474bbd9d5f Adding Queue message 2026-01-19 16:14:05 -08:00
Arafatkatze a562e56175 fix: block message queue during user decision states
Add additional conditions to prevent message queue processing during:
- command_output: wait for command to finish
- mistake_limit_reached: user must make a decision
- condense: user is reviewing conversation summary
- report_bug: user is filing a bug report

This prevents race conditions where queued messages could interfere
with states requiring explicit user interaction.
2026-01-19 13:49:02 -08:00
Arafatkatze 69f3f6daa6 fix resume tasks 2026-01-19 12:28:59 -08:00
Arafatkatze 3cbf9812d0 fix: preserve user input when processing queued messages
Don't clear input field, active quote, images, and files when
processing messages from the queue, as the user may have typed
new content while waiting. Only clear these fields for direct
user submissions.
2026-01-19 11:21:11 -08:00
Arafatkatze f579bf4b7c adding changes with debug logs 2026-01-19 11:21:11 -08:00
Arafatkatze 7e66fe2ac6 adding changes with debug logs 2026-01-19 11:21:11 -08:00
Arafatkatze 683f03bc67 adding changes with debug logs 2026-01-19 11:21:11 -08:00
Arafatkatze 4680cacfd2 feat: reset abandoned flag when resuming task
Reset the abandoned flag alongside the abort flag when a task is
resumed to ensure proper task state initialization and prevent
stale abandoned state from affecting resumed tasks.
2026-01-19 11:21:11 -08:00
Arafatkatze d11225d233 feat: add message queue for handling user input during task cancellation
- Add messageQueue state to useChatState hook to buffer messages during cancel flow
- Clear message queue automatically when a new task starts
- Remove platform-specific rollup dependencies from package-lock.json
- Update ChatView and related hooks to support queued message handling

This ensures user messages sent during task cancellation are properly
queued and processed when the new task begins.
2026-01-19 11:21:08 -08:00
16 changed files with 835 additions and 136 deletions
+49 -10
View File
@@ -39,6 +39,7 @@ import { BannerCardData } from "@/shared/cline/banner"
import { getAxiosSettings } from "@/shared/net"
import { ShowMessageType } from "@/shared/proto/host/window"
import { getLatestAnnouncementId } from "@/utils/announcements"
import { extensionLog } from "@/utils/debugLogger"
import { getCwd, getDesktopDir } from "@/utils/path"
import { PromptRegistry } from "../prompts/system-prompt"
import {
@@ -325,9 +326,9 @@ export class Controller {
})
if (historyItem) {
this.task.resumeTaskFromHistory()
await this.task.resumeTaskFromHistory()
} else if (task || images || files) {
this.task.startTask(task, images, files)
await this.task.startTask(task, images, files)
}
return this.task.taskId
@@ -409,7 +410,6 @@ export class Controller {
async cancelTask() {
// Prevent duplicate cancellations from spam clicking
if (this.cancelInProgress) {
console.log(`[Controller.cancelTask] Cancellation already in progress, ignoring duplicate request`)
return
}
@@ -445,6 +445,11 @@ export class Controller {
if (this.task) {
// 'abandoned' will prevent this cline instance from affecting future cline instance gui. this may happen if its hanging on a streaming request
this.task.taskState.abandoned = true
// NOTE: We DON'T clear messages here anymore.
// The 'abandoned' guard in ask() and say() already prevents stale messages from being added.
// Clearing messages here caused a bug where new tasks (not yet saved to disk) would lose
// their messages on cancel. The in-memory messages are the source of truth until saved.
}
// Small delay to ensure state manager has persisted the history update
@@ -455,20 +460,33 @@ export class Controller {
try {
const result = await this.getTaskWithId(this.task.taskId)
historyItem = result.historyItem
} catch (error) {
} catch {
// Task not in history yet (new task with no messages); catch the
// error to enable the agent to continue making progress.
console.log(`[Controller.cancelTask] Task not found in history: ${error}`)
}
// Only re-initialize if we found a history item, otherwise just clear
// Re-initialize from disk if history exists, otherwise keep current task state
if (historyItem) {
// Re-initialize task to keep it visible in UI with resume button
await this.initTask(undefined, undefined, undefined, historyItem, undefined)
} else {
await this.clearTask()
// Task was saved to disk - reload messages from disk and show resume button
// DON'T call initTask() here because it calls clearTask() which wipes messages!
try {
// Clear cancelInProgress BEFORE resumeTaskFromHistory() because it blocks
// waiting for user to click Resume. If we don't clear it here, user can't
// cancel again if something goes wrong with parallel tool calls.
this.cancelInProgress = false
await this.task.resumeTaskFromHistory()
// resumeTaskFromHistory() calls ask("resume_task") which posts state to webview
// So we don't need to call postStateToWebview() here
return
} catch (error) {
console.error(`[Controller.cancelTask] Failed to resume from history:`, error)
// Fall through to keep current task
}
}
// If no historyItem OR resume failed, keep the current task instance with its in-memory messages
// This handles the case where user cancels before first message is saved
await this.postStateToWebview()
} finally {
// Always clear the flag, even if cancellation fails
@@ -799,6 +817,12 @@ export class Controller {
async postStateToWebview() {
const state = await this.getStateToPostToWebview()
console.log(
`[Controller.postStateToWebview] Sending state - taskId: ${this.task?.taskId || "none"}, messages: ${state.clineMessages.length}`,
)
extensionLog.info(
`[postStateToWebview] Sending - taskId: ${this.task?.taskId || "none"}, messageCount: ${state.clineMessages.length}`,
)
await sendStateUpdate(state)
}
@@ -859,6 +883,9 @@ export class Controller {
const currentTaskItem = this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined
const clineMessages = this.task?.messageStateHandler.getClineMessages() || []
console.log(
`[Controller.getStateToPostToWebview] Got ${clineMessages.length} messages from task ${this.task?.taskId || "none"}`,
)
const checkpointManagerErrorMessage = this.task?.taskState.checkpointManagerErrorMessage
const processedTaskHistory = (taskHistory || [])
@@ -968,8 +995,20 @@ export class Controller {
async clearTask() {
if (this.task) {
// Save current state to disk BEFORE clearing
// This ensures messages are persisted if user clicks "New Task" while task is running
try {
await this.task.messageStateHandler.saveClineMessagesAndUpdateHistory()
} catch (error) {
console.error(`[Controller.clearTask] Failed to save messages before clearing:`, error)
}
// Clear task settings cache when task ends
await this.stateManager.clearTaskSettings()
// DON'T clear message queues here! We just saved them to disk above.
// Clearing them could trigger another auto-save that overwrites with [].
// Since we're setting this.task = undefined below anyway, no need to clear.
}
await this.task?.abortTask()
this.task = undefined // removes reference to it, so once promises end it will be garbage collected
+10 -7
View File
@@ -19,12 +19,15 @@ export async function showTaskWithId(controller: Controller, request: StringRequ
// We need to initialize the task before returning data
if (historyItem) {
// Always initialize the task with the history item
await controller.initTask(undefined, undefined, undefined, historyItem)
// Send UI update to show the chat view
// Send UI update to show the chat view FIRST
// This must happen BEFORE initTask() because initTask() calls resumeTaskFromHistory()
// which blocks on ask("resume_task") waiting for user to click Resume.
// If we don't navigate to chat view first, user can't see the Resume button!
await sendChatButtonClickedEvent()
// Now initialize the task with the history item
await controller.initTask(undefined, undefined, undefined, historyItem)
// Return task data for gRPC response
return TaskResponse.create({
id: historyItem.id,
@@ -43,12 +46,12 @@ export async function showTaskWithId(controller: Controller, request: StringRequ
// If not in global state, fetch from storage
const { historyItem: fetchedItem } = await controller.getTaskWithId(id)
// Send UI update to show the chat view FIRST (same reason as above)
await sendChatButtonClickedEvent()
// Initialize the task with the fetched item
await controller.initTask(undefined, undefined, undefined, fetchedItem)
// Send UI update to show the chat view
await sendChatButtonClickedEvent()
return TaskResponse.create({
id: fetchedItem.id,
task: fetchedItem.task || "",
+21 -14
View File
@@ -582,7 +582,7 @@ export class Task {
askTs?: number
}> {
// Allow resume asks even when aborted to enable resume button after cancellation
if (this.taskState.abort && type !== "resume_task" && type !== "resume_completed_task") {
if ((this.taskState.abort || this.taskState.abandoned) && type !== "resume_task" && type !== "resume_completed_task") {
throw new Error("Cline instance aborted")
}
let askTs: number
@@ -717,7 +717,7 @@ export class Task {
partial?: boolean,
): Promise<number | undefined> {
// Allow hook messages even when aborted to enable proper cleanup
if (this.taskState.abort && type !== "hook_status" && type !== "hook_output_stream") {
if ((this.taskState.abort || this.taskState.abandoned) && type !== "hook_status" && type !== "hook_output_stream") {
throw new Error("Cline instance aborted")
}
@@ -867,9 +867,6 @@ export class Task {
// Update UI
await this.postStateToWebview()
// Log for debugging/telemetry
console.log(`[Task ${this.taskId}] ${hookName} hook cancelled (userInitiated: ${wasCancelled})`)
}
/**
@@ -1062,10 +1059,15 @@ export class Task {
public async resumeTaskFromHistory() {
try {
await this.clineIgnoreController.initialize()
} catch (error) {
console.error("Failed to initialize ClineIgnoreController:", error)
// Optionally, inform the user or handle the error appropriately
try {
await this.clineIgnoreController.initialize()
} catch (error) {
console.error("Failed to initialize ClineIgnoreController:", error)
// Optionally, inform the user or handle the error appropriately
}
} catch (outerError) {
console.error(`[Task.resumeTaskFromHistory] CRITICAL ERROR at start:`, outerError)
throw outerError
}
const savedClineMessages = await getSavedClineMessages(this.taskId)
@@ -1117,10 +1119,17 @@ export class Task {
}
this.taskState.isInitialized = true
this.taskState.abort = false // Reset abort flag when resuming task
// NOTE: Do NOT reset abort/abandoned flags here!
// The streaming loop needs to see these flags as true to stop.
// We reset them AFTER user clicks Resume (after ask() returns).
const { response, text, images, files } = await this.ask(askType) // calls poststatetowebview
// NOW reset the abort/abandoned flags after user clicked Resume or provided input
// This ensures the streaming loop has already stopped before we allow new work
this.taskState.abort = false
this.taskState.abandoned = false
// Initialize newUserContent array for hook context
const newUserContent: ClineContent[] = []
@@ -1490,9 +1499,8 @@ export class Task {
// Present the resume ask - this will show the resume button in the UI
// We don't await this because we want to set the abort flag immediately
// The ask will be waiting when the user decides to resume
this.ask(askType).catch((error) => {
// If ask fails (e.g., task was cleared), that's okay - just log it
console.log("[TaskCancel] Resume ask failed (task may have been cleared):", error)
this.ask(askType).catch(() => {
// If ask fails (e.g., task was cleared), that's okay - ignore it
})
} catch (error) {
// TaskCancel hook failed - non-fatal, just log
@@ -2466,7 +2474,6 @@ export class Task {
// lastMessage.ts = Date.now() DO NOT update ts since it is used as a key for virtuoso list
lastMessage.partial = false
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
console.log("updating partial message", lastMessage)
// await this.saveClineMessagesAndUpdateHistory()
}
// update api_req_started to have cancelled and cost, so that we can display the cost of the partial stream
+11
View File
@@ -6,6 +6,7 @@ import { HostProvider } from "@/hosts/host-provider"
import { ExtensionRegistryInfo } from "@/registry"
import type { ExtensionMessage } from "@/shared/ExtensionMessage"
import { WebviewMessage } from "@/shared/WebviewMessage"
import { debugLog } from "@/utils/debugLogger"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -173,6 +174,16 @@ export class VscodeWebviewProvider extends WebviewProvider implements vscode.Web
}
break
}
case "webview_debug_log": {
if (message.webview_debug_log) {
const { level, args } = message.webview_debug_log
// Map webview log levels to debug logger levels ("log" -> "info")
const mappedLevel = level === "log" ? "info" : level
// Use unified debug logger - writes to ~/cline-debug.log
debugLog("webview", mappedLevel, ...args)
}
break
}
default: {
console.error("Received unhandled WebviewMessage type:", JSON.stringify(message))
}
+15
View File
@@ -316,3 +316,18 @@ export interface ClineApiReqInfo {
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled" | "retries_exhausted"
export const COMPLETION_RESULT_CHANGES_FLAG = "HAS_CHANGES"
/**
* QueuedMessage
* Represents a message that is queued to be sent when sending is enabled
*/
export interface QueuedMessage {
/** Unique identifier for the queued message */
id: string
/** The text content of the message */
text: string
/** Array of image data URLs attached to the message */
images: string[]
/** Array of file paths attached to the message */
files: string[]
}
+8 -1
View File
@@ -1,7 +1,14 @@
export interface WebviewMessage {
type: "grpc_request" | "grpc_request_cancel"
type: "grpc_request" | "grpc_request_cancel" | "webview_debug_log"
grpc_request?: GrpcRequest
grpc_request_cancel?: GrpcCancel
webview_debug_log?: WebviewDebugLog
}
export type WebviewDebugLog = {
level: "log" | "warn" | "error" | "debug"
args: string[]
timestamp: number
}
export type GrpcRequest = {
@@ -622,10 +622,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
if (event.key === "Enter" && !event.shiftKey && !isComposing) {
event.preventDefault()
if (!sendingDisabled) {
setIsTextAreaFocused(false)
onSend()
}
// Always call onSend - let handleSendMessage decide whether to queue or send
setIsTextAreaFocused(false)
onSend()
}
if (event.key === "Backspace" && !isComposing) {
@@ -1699,10 +1698,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
)}
data-testid="send-button"
onClick={() => {
if (!sendingDisabled) {
setIsTextAreaFocused(false)
onSend()
}
// Always call onSend - let handleSendMessage decide whether to queue or send
setIsTextAreaFocused(false)
onSend()
}}
/>
)}
+69 -1
View File
@@ -6,7 +6,7 @@ import { combineHookSequences } from "@shared/combineHookSequences"
import type { ClineApiReqInfo, ClineMessage } from "@shared/ExtensionMessage"
import { getApiMetrics } from "@shared/getApiMetrics"
import { BooleanRequest, StringRequest } from "@shared/proto/cline/common"
import { useCallback, useEffect, useMemo } from "react"
import { useCallback, useEffect, useMemo, useRef } from "react"
import { useMount } from "react-use"
import { normalizeApiConfiguration } from "@/components/settings/utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
@@ -19,6 +19,7 @@ import {
ActionButtons,
CHAT_CONSTANTS,
ChatLayout,
canProcessQueue,
convertHtmlToMarkdown,
filterVisibleMessages,
groupLowStakesTools,
@@ -31,6 +32,7 @@ import {
useScrollBehavior,
WelcomeSection,
} from "./chat-view"
import QueuedMessages from "./QueuedMessages"
interface ChatViewProps {
isHidden: boolean
@@ -103,8 +105,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
expandedRows,
setExpandedRows,
textAreaRef,
messageQueue,
setMessageQueue,
clineAsk,
} = chatState
// Processing lock to prevent race conditions in queue processing
const isProcessingQueueRef = useRef(false)
useEffect(() => {
const handleCopy = async (e: ClipboardEvent) => {
const targetElement = e.target as HTMLElement | null
@@ -312,6 +320,58 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
}
}, [isHidden, sendingDisabled, enableButtons])
// Queue processing effect - processes queued messages when sending becomes enabled
// See queueUtils.ts for detailed documentation on blocking logic
useEffect(() => {
if (
!canProcessQueue({
sendingDisabled,
messageQueueLength: messageQueue.length,
clineAsk,
isProcessing: isProcessingQueueRef.current,
messagesLength: messages.length,
})
) {
return
}
// Set processing lock immediately to prevent race conditions
isProcessingQueueRef.current = true
// Get the first message but don't remove yet - only remove on successful send
const nextMessage = messageQueue[0]
// Process the message asynchronously
const processMessage = async () => {
try {
const success = await messageHandlers.handleSendMessage(
nextMessage.text,
nextMessage.images,
nextMessage.files,
true,
)
if (success) {
// Only remove from queue if message was actually sent
setMessageQueue((current) => current.slice(1))
}
// If !success, message stays in queue and will be retried
// when state changes trigger the effect again
} catch (error) {
console.error("[ChatView] Failed to send queued message:", {
messageId: nextMessage.id,
error,
})
// On error, message stays in queue (no action needed)
} finally {
// Release the processing lock
isProcessingQueueRef.current = false
}
}
processMessage()
}, [sendingDisabled, messageQueue, clineAsk, setMessageQueue, messageHandlers, messages.length])
const visibleMessages = useMemo(() => {
return filterVisibleMessages(modifiedMessages)
}, [modifiedMessages])
@@ -379,6 +439,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
</div>
<footer className="bg-(--vscode-sidebar-background)" style={{ gridRow: "2" }}>
<AutoApproveBar />
<QueuedMessages
onClearAll={() => chatState.setMessageQueue([])}
onRemove={(index) => chatState.setMessageQueue((prev) => prev.filter((_, i) => i !== index))}
onUpdate={(index, newText) => {
chatState.setMessageQueue((prev) => prev.map((msg, i) => (i === index ? { ...msg, text: newText } : msg)))
}}
queue={chatState.messageQueue}
/>
<ActionButtons
chatState={chatState}
messageHandlers={messageHandlers}
@@ -0,0 +1,156 @@
import { QueuedMessage } from "@shared/ExtensionMessage"
import React, { useState } from "react"
const MAX_QUEUE_SIZE = 5
interface QueuedMessagesProps {
queue: QueuedMessage[]
onRemove: (index: number) => void
onUpdate: (index: number, newText: string) => void
onClearAll: () => void
}
const QueuedMessages: React.FC<QueuedMessagesProps> = ({ queue, onRemove, onUpdate, onClearAll }) => {
const [editingStates, setEditingStates] = useState<Record<string, { isEditing: boolean; value: string }>>({})
// console.log("[QueuedMessages] Component rendered:", {
// queueLength: queue.length,
// queue,
// })
if (queue.length === 0) {
// console.log("[QueuedMessages] Queue is empty, returning null")
return null
}
const getEditState = (messageId: string, currentText: string) => {
return editingStates[messageId] || { isEditing: false, value: currentText }
}
const setEditState = (messageId: string, isEditing: boolean, value?: string) => {
setEditingStates((prev) => ({
...prev,
[messageId]: { isEditing, value: value ?? prev[messageId]?.value ?? "" },
}))
}
const handleSaveEdit = (index: number, messageId: string, newValue: string) => {
console.log("[QueuedMessages] Saving edit:", {
index,
messageId,
newValue,
})
onUpdate(index, newValue)
setEditState(messageId, false)
}
const handleRemove = (index: number, messageId: string) => {
console.log("[QueuedMessages] Removing message:", {
index,
messageId,
})
onRemove(index)
}
return (
<div className="px-[15px] py-[10px] pr-[6px]" data-testid="queued-messages">
<div className="flex items-center justify-between mb-2">
<div className="text-[var(--vscode-descriptionForeground)] text-sm font-medium">
Queued Messages ({queue.length}/{MAX_QUEUE_SIZE})
</div>
<button
aria-label="Clear all queued messages"
className="flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium bg-[var(--vscode-button-secondaryBackground)] text-[var(--vscode-button-secondaryForeground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)] transition-colors"
onClick={onClearAll}
title="Clear all queued messages">
<span className="codicon codicon-clear-all text-sm" />
<span>Clear All</span>
</button>
</div>
<div className="flex flex-col gap-2 max-h-[300px] overflow-y-auto pr-2">
{queue.map((message, index) => {
const editState = getEditState(message.id, message.text)
return (
<div
className="bg-[var(--vscode-editor-background)] border border-[var(--vscode-panel-border)] rounded p-2 overflow-hidden flex-shrink-0"
key={message.id}>
<div className="flex justify-between gap-2">
<div className="flex-grow min-w-0">
{editState.isEditing ? (
<textarea
autoFocus
className="w-full bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border border-[var(--vscode-input-border)] rounded px-2 py-1 resize-none focus:outline-none focus:ring-1 focus:ring-[var(--vscode-focusBorder)] font-sans text-[13px]"
onBlur={() => handleSaveEdit(index, message.id, editState.value)}
onChange={(e) => setEditState(message.id, true, e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
handleSaveEdit(index, message.id, editState.value)
}
if (e.key === "Escape") {
setEditState(message.id, false, message.text)
}
}}
placeholder="Edit message..."
ref={(textarea) => {
if (textarea) {
// Set cursor at the end
textarea.setSelectionRange(textarea.value.length, textarea.value.length)
}
}}
rows={Math.min(editState.value.split("\n").length, 10)}
value={editState.value}
/>
) : (
<div
className="cursor-pointer hover:bg-[var(--vscode-list-hoverBackground)] px-2 py-1 -mx-2 -my-1 rounded transition-colors whitespace-pre-wrap break-words text-[var(--vscode-editor-foreground)] text-[13px]"
onClick={() => setEditState(message.id, true, message.text)}
title="Click to edit">
{message.text}
</div>
)}
</div>
<button
aria-label="Remove message"
className="flex-shrink-0 w-6 h-6 flex items-center justify-center rounded hover:bg-[var(--vscode-toolbar-hoverBackground)] text-[var(--vscode-icon-foreground)] transition-colors"
onClick={(e) => {
e.stopPropagation()
handleRemove(index, message.id)
}}
title="Remove from queue">
<span className="codicon codicon-trash text-sm" />
</button>
</div>
{message.images && message.images.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{message.images.map((image, imgIndex) => (
<img
alt={`Attachment ${imgIndex + 1}`}
className="max-w-[100px] max-h-[100px] rounded border border-[var(--vscode-panel-border)] object-cover"
key={imgIndex}
src={image}
/>
))}
</div>
)}
{message.files && message.files.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{message.files.map((file, fileIndex) => (
<div
className="px-2 py-1 bg-[var(--vscode-badge-background)] text-[var(--vscode-badge-foreground)] rounded text-xs flex items-center gap-1"
key={fileIndex}>
<span className="codicon codicon-file text-xs" />
<span className="truncate max-w-[150px]">{file.split("/").pop()}</span>
</div>
))}
</div>
)}
</div>
)
})}
</div>
</div>
)
}
export default QueuedMessages
@@ -64,16 +64,20 @@ export const ActionButtons: React.FC<ActionButtonsProps> = ({
const handleActionClick = useCallback(
(action: ButtonActionType, text?: string, images?: string[], files?: string[]) => {
console.log("[CANCEL_FLOW] [ActionButtons] Button clicked:", action)
if (isProcessing) {
console.log("[CANCEL_FLOW] [ActionButtons] Already processing, ignoring click")
return
}
setIsProcessing(true)
// Special handling for cancel action
if (action === "cancel") {
console.log("[CANCEL_FLOW] [ActionButtons] Cancel action detected, resetting processing state")
setIsProcessing(false)
}
console.log("[CANCEL_FLOW] [ActionButtons] Calling executeButtonAction:", action)
messageHandlers.executeButtonAction(action, text, images, files)
},
[messageHandlers, isProcessing],
@@ -83,6 +87,7 @@ export const ActionButtons: React.FC<ActionButtonsProps> = ({
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.key === "Escape") {
console.log("[CANCEL_FLOW] [ActionButtons] Escape key pressed, triggering cancel")
event.preventDefault()
event.stopPropagation()
messageHandlers.executeButtonAction("cancel")
@@ -1,4 +1,4 @@
import { ClineMessage } from "@shared/ExtensionMessage"
import { ClineMessage, QueuedMessage } from "@shared/ExtensionMessage"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { ChatState } from "../types/chatTypes"
@@ -21,6 +21,9 @@ export function useChatState(messages: ClineMessage[]): ChatState {
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>("Reject")
const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
// Message queue state
const [messageQueue, setMessageQueue] = useState<QueuedMessage[]>([])
// Refs
const textAreaRef = useRef<HTMLTextAreaElement>(null)
@@ -35,12 +38,16 @@ export function useChatState(messages: ClineMessage[]): ChatState {
setExpandedRows({})
}, [])
// Track previous task timestamp to detect genuinely new tasks
const prevTaskTsRef = useRef<number | undefined>(undefined)
// Reset state when starting new conversation
const resetState = useCallback(() => {
setInputValue("")
setActiveQuote(null)
setSelectedImages([])
setSelectedFiles([])
setMessageQueue([])
}, [])
// Handle focus change
@@ -48,8 +55,27 @@ export function useChatState(messages: ClineMessage[]): ChatState {
setIsTextAreaFocused(isFocused)
}, [])
// Auto-expand last message row when task or messages first changed.
// Clear message queue when task changes
// This handles: undefined → Task B, Task A → Task B, Task A → undefined
// Does NOT clear on: Task A → Task A (mode switch), undefined → undefined (no task)
useEffect(() => {
const currentTaskTs = task?.ts
const prevTaskTs = prevTaskTsRef.current
// Clear queue when:
// 1. A new task starts (undefined → Task B, Task A → Task B)
// 2. Task is explicitly cleared via "New Task" button (Task A → undefined)
// Don't clear when: undefined → undefined (no task, stays no task)
const newTaskStarted = currentTaskTs !== undefined && prevTaskTs !== currentTaskTs
const taskCleared = currentTaskTs === undefined && prevTaskTs !== undefined
if (newTaskStarted || taskCleared) {
setMessageQueue([])
}
// Always update ref to track task changes
prevTaskTsRef.current = currentTaskTs
clearExpandedRows()
}, [task?.ts, clearExpandedRows])
@@ -76,6 +102,10 @@ export function useChatState(messages: ClineMessage[]): ChatState {
expandedRows,
setExpandedRows,
// Message queue state
messageQueue,
setMessageQueue,
// Refs
textAreaRef,
@@ -21,14 +21,19 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
setSelectedFiles,
setSendingDisabled,
setEnableButtons,
sendingDisabled,
clineAsk,
lastMessage,
messageQueue,
setMessageQueue,
} = chatState
// Handle sending a message
// Returns true if message was successfully sent, false otherwise
const handleSendMessage = useCallback(
async (text: string, images: string[], files: string[]) => {
async (text: string, images: string[], files: string[], fromQueue = false): Promise<boolean> => {
let messageToSend = text.trim()
let messageSent = false
const hasContent = messageToSend || images.length > 0 || files.length > 0
// Prepend the active quote if it exists
@@ -39,107 +44,150 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
messageToSend = `${prefix} ${formattedQuote} ${suffix} ${messageToSend}`
}
if (hasContent) {
console.log("[ChatView] handleSendMessage - Sending message:", messageToSend)
let messageSent = false
if (!hasContent) {
return false
}
if (messages.length === 0) {
await TaskServiceClient.newTask(
NewTaskRequest.create({
// If sending is disabled and this is not from the queue, add to queue
if (sendingDisabled && !fromQueue) {
// Check queue size limit (max 5 messages)
if (messageQueue.length >= 5) {
// Queue is full, don't add and keep input so user doesn't lose their message
return false
}
// Generate a unique ID using timestamp + random component
const messageId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
setMessageQueue((prev) => [...prev, { id: messageId, text: messageToSend, images, files }])
setInputValue("")
setSelectedImages([])
setSelectedFiles([])
return false // Message was queued, not sent
}
if (messages.length === 0) {
// Clear input BEFORE the async call to avoid race condition
// The newTask() call triggers a state update from the backend that can
// cause a React re-render before newTask() returns, which would leave
// the input with stale data if we clear it after the await.
// BUT: Don't clear if processing from queue - user may have typed new text!
if (!fromQueue) {
setInputValue("")
setActiveQuote(null)
setSelectedImages([])
setSelectedFiles([])
}
setSendingDisabled(true)
setEnableButtons(false)
// Reset auto-scroll
if ("disableAutoScrollRef" in chatState) {
;(chatState as any).disableAutoScrollRef.current = false
}
await TaskServiceClient.newTask(
NewTaskRequest.create({
text: messageToSend,
images,
files,
}),
)
return true // New task created successfully
}
if (clineAsk) {
// For resume_task and resume_completed_task, use yesButtonClicked to match Resume button behavior
// This ensures Enter key and Resume button work identically
if (clineAsk === "resume_task" || clineAsk === "resume_completed_task") {
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "yesButtonClicked",
text: messageToSend,
images,
files,
}),
)
messageSent = true
} else if (clineAsk) {
// For resume_task and resume_completed_task, use yesButtonClicked to match Resume button behavior
// This ensures Enter key and Resume button work identically
if (clineAsk === "resume_task" || clineAsk === "resume_completed_task") {
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "yesButtonClicked",
text: messageToSend,
images,
files,
}),
)
messageSent = true
} else {
// All other ask types use messageResponse
switch (clineAsk) {
case "followup":
case "plan_mode_respond":
case "tool":
case "browser_action_launch":
case "command":
case "command_output":
case "use_mcp_server":
case "completion_result":
case "mistake_limit_reached":
case "api_req_failed":
case "new_task":
case "condense":
case "report_bug":
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "messageResponse",
text: messageToSend,
images,
files,
}),
)
messageSent = true
break
}
}
} else if (messages.length > 0) {
// No clineAsk set - check if task is actively running
// If so, allow interrupting it with feedback
const lastMessage = messages[messages.length - 1]
const isTaskRunning =
lastMessage.partial === true || (lastMessage.type === "say" && lastMessage.say === "api_req_started")
if (isTaskRunning) {
// Task is running - send message as interruption/feedback
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "messageResponse",
text: messageToSend,
images,
files,
}),
)
messageSent = true
} else {
// All other ask types use messageResponse
switch (clineAsk) {
case "followup":
case "plan_mode_respond":
case "tool":
case "browser_action_launch":
case "command":
case "command_output":
case "use_mcp_server":
case "completion_result":
case "mistake_limit_reached":
case "api_req_failed":
case "new_task":
case "condense":
case "report_bug":
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "messageResponse",
text: messageToSend,
images,
files,
}),
)
messageSent = true
break
}
}
} else if (messages.length > 0) {
// No clineAsk set - check if task is actively running
// If so, allow interrupting it with feedback
const lastMessage = messages[messages.length - 1]
const isTaskRunning =
lastMessage.partial === true || (lastMessage.type === "say" && lastMessage.say === "api_req_started")
// Only clear input and disable UI if message was actually sent
if (messageSent) {
setInputValue("")
setActiveQuote(null)
setSendingDisabled(true)
setSelectedImages([])
setSelectedFiles([])
setEnableButtons(false)
// Reset auto-scroll
if ("disableAutoScrollRef" in chatState) {
;(chatState as any).disableAutoScrollRef.current = false
}
if (isTaskRunning) {
// Task is running - send message as interruption/feedback
await TaskServiceClient.askResponse(
AskResponseRequest.create({
responseType: "messageResponse",
text: messageToSend,
images,
files,
}),
)
messageSent = true
}
}
// Only clear input and disable UI if message was actually sent
if (messageSent) {
// Don't clear input if processing from queue - user may have typed new text!
if (!fromQueue) {
setInputValue("")
setActiveQuote(null)
setSelectedImages([])
setSelectedFiles([])
}
setSendingDisabled(true)
setEnableButtons(false)
// Reset auto-scroll
if ("disableAutoScrollRef" in chatState) {
;(chatState as any).disableAutoScrollRef.current = false
}
}
return messageSent
},
[
messages.length,
clineAsk,
activeQuote,
sendingDisabled,
setInputValue,
setActiveQuote,
setSendingDisabled,
setSelectedImages,
setSelectedFiles,
setEnableButtons,
setMessageQueue,
chatState,
],
)
@@ -147,8 +195,9 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
// Start a new task
const startNewTask = useCallback(async () => {
setActiveQuote(null)
setMessageQueue([])
await TaskServiceClient.clearTask(EmptyRequest.create({}))
}, [setActiveQuote])
}, [setActiveQuote, setMessageQueue])
// Clear input state helper
const clearInputState = useCallback(() => {
@@ -249,14 +298,18 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
break
case "cancel":
// Clear state IMMEDIATELY before async call, not after
// The cancelTask() call blocks until user clicks Resume, so we must
// clear the queue before calling it, otherwise the queue stays visible
setSendingDisabled(false)
setEnableButtons(true)
setMessageQueue([]) // Clear queue on cancel
if (backgroundCommandRunning) {
await TaskServiceClient.cancelBackgroundCommand(EmptyRequest.create({}))
} else {
await TaskServiceClient.cancelTask(EmptyRequest.create({}))
}
// Clear any pending state that might interfere with resume
setSendingDisabled(false)
setEnableButtons(true)
break
case "utility":
@@ -290,6 +343,7 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
backgroundCommandRunning,
setSendingDisabled,
setEnableButtons,
setMessageQueue,
],
)
@@ -15,4 +15,5 @@ export * from "./types/chatTypes"
// Export utilities
export * from "./utils/markdownUtils"
export * from "./utils/messageUtils"
export * from "./utils/queueUtils"
export * from "./utils/scrollUtils"
@@ -2,7 +2,7 @@
* Shared types and interfaces for the chat view components
*/
import { ClineAsk, ClineMessage } from "@shared/ExtensionMessage"
import { ClineAsk, ClineMessage, QueuedMessage } from "@shared/ExtensionMessage"
import { ListRange, VirtuosoHandle } from "react-virtuoso"
import { ButtonActionType } from "../shared/buttonConfig"
@@ -42,6 +42,10 @@ export interface ChatState {
expandedRows: Record<number, boolean>
setExpandedRows: React.Dispatch<React.SetStateAction<Record<number, boolean>>>
// Message queue state
messageQueue: QueuedMessage[]
setMessageQueue: React.Dispatch<React.SetStateAction<QueuedMessage[]>>
// Refs
textAreaRef: React.RefObject<HTMLTextAreaElement>
@@ -67,7 +71,7 @@ export interface ChatState {
*/
export interface MessageHandlers {
executeButtonAction: (action: ButtonActionType, text?: string, images?: string[], files?: string[]) => Promise<void>
handleSendMessage: (text: string, images: string[], files: string[]) => Promise<void>
handleSendMessage: (text: string, images: string[], files: string[], fromQueue?: boolean) => Promise<boolean>
handleTaskCloseButtonClick: () => void
startNewTask: () => Promise<void>
}
@@ -0,0 +1,270 @@
import type { ClineAsk } from "@shared/ExtensionMessage"
/**
* ==================================================================================
* MESSAGE QUEUE BLOCKING LOGIC
* ==================================================================================
*
* This module determines when the message queue should be blocked from processing.
* The queue holds messages the user typed while Cline was busy. These messages
* should only be sent at appropriate times.
*
* GENERAL PRINCIPLE:
* - Block queue when user needs to make a decision
* - Block queue during active operations that could be disrupted
* - Allow queue when task is complete or paused for user input
*
* ==================================================================================
* ASK TYPES AND THEIR BLOCKING BEHAVIOR
* ==================================================================================
*/
/**
* CATEGORY 1: ALWAYS BLOCKING ASKS
*
* These asks ALWAYS require user attention and should ALWAYS block the queue,
* regardless of auto-approval settings.
*
* | Ask Type | When It Appears | Why Block Queue |
* |-----------------------|---------------------------|------------------------------------|
* | api_req_failed | API call failed | User must retry or abort |
* | command_output | Command running in term | Wait for command to complete |
* | mistake_limit_reached | Too many consecutive errs | User must acknowledge + guide |
* | condense | Context window full | User reviewing conversation summary|
* | report_bug | User filing bug report | User composing bug details |
*/
const ALWAYS_BLOCKING_ASKS: readonly ClineAsk[] = [
"api_req_failed",
"command_output",
"mistake_limit_reached",
"condense",
"report_bug",
] as const
/**
* CATEGORY 2: TOOL APPROVAL ASKS (Block Only When Auto-Approve is OFF)
*
* These asks ONLY appear when the corresponding auto-approval setting is OFF.
* When auto-approval is ON, the tool executes immediately and NO ask is shown.
*
* KEY INSIGHT: We don't need to check autoApprovalSettings here!
* If we see one of these asks, it means auto-approval is already OFF.
* If auto-approval were ON, the ask would never be created.
*
* | Ask Type | Auto-Approve Setting | When Ask Appears | When Ask Hidden |
* |----------------------|----------------------------|-----------------------------|---------------------------|
* | tool | readFiles / editFiles | Setting is OFF | Setting is ON (auto-exec) |
* | command | executeSafe/AllCommands | Setting is OFF | Setting is ON (auto-exec) |
* | browser_action_launch| useBrowser | Setting is OFF | Setting is ON (auto-exec) |
* | use_mcp_server | useMcp | Setting is OFF | Setting is ON (auto-exec) |
*
* FLOW WHEN AUTO-APPROVE IS ON:
* ```
* Model calls tool → Backend checks settings → Auto-approve ON →
* Execute immediately → No ask() called → clineAsk stays undefined →
* Queue sees clineAsk=undefined → NOT blocked → Works correctly ✓
* ```
*
* FLOW WHEN AUTO-APPROVE IS OFF:
* ```
* Model calls tool → Backend checks settings → Auto-approve OFF →
* Call ask("tool", ...) → User sees approval dialog → clineAsk="tool" →
* Queue sees clineAsk="tool" → BLOCKED (this is what we want) ✓
* ```
*/
const TOOL_APPROVAL_ASKS: readonly ClineAsk[] = [
"tool", // File read/write - blocked when readFiles/editFiles is OFF
"command", // Command execution - blocked when executeSafe/AllCommands is OFF
"browser_action_launch", // Browser automation - blocked when useBrowser is OFF
"use_mcp_server", // MCP tool usage - blocked when useMcp is OFF
] as const
/**
* CATEGORY 3: ALWAYS ALLOWING ASKS
*
* These asks represent natural pause points where the queue SHOULD process.
* These are moments when the user would naturally want their queued message sent.
*
* | Ask Type | When It Appears | Why ALLOW Queue |
* |-----------------------|---------------------------|------------------------------------|
* | followup | Model asked a question | User can answer + queue fires |
* | plan_mode_respond | Plan mode discussion | Conversational, queue is fine |
* | completion_result | Task finished | Perfect time for next request |
* | resume_task | Resuming paused task | Starting fresh, queue can fire |
* | resume_completed_task | Resuming completed task | Starting fresh, queue can fire |
* | new_task | New task context created | Starting fresh, queue can fire |
*
* Note: This array is for documentation purposes. The function below uses
* the blocking arrays and allows everything else by default.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const NEVER_BLOCKING_ASKS: readonly ClineAsk[] = [
"followup",
"plan_mode_respond",
"completion_result",
"resume_task",
"resume_completed_task",
"new_task",
] as const
/**
* Determines if the given ask type should block queue processing.
*
* @param clineAsk - The current ask type, or undefined if no ask is active
* @returns true if queue should be BLOCKED, false if queue can process
*
* DECISION TREE:
* ```
* clineAsk === undefined?
* └── YES → return false (queue can process, no ask blocking it)
* └── NO → Is it in ALWAYS_BLOCKING_ASKS?
* └── YES → return true (always block)
* └── NO → Is it in TOOL_APPROVAL_ASKS?
* └── YES → return true (tool needs approval)
* └── NO → return false (allow queue)
* ```
*
* EXAMPLES:
*
* Case 1: No active ask
* ```
* shouldBlockQueueForAsk(undefined) → false
* // Queue can process - nothing is asking user for input
* ```
*
* Case 2: API failed
* ```
* shouldBlockQueueForAsk("api_req_failed") → true
* // Queue blocked - user must handle error first
* ```
*
* Case 3: Tool approval (auto-approve OFF)
* ```
* shouldBlockQueueForAsk("tool") → true
* // Queue blocked - user must approve/reject tool first
* // Note: We only see "tool" ask because auto-approve is OFF
* ```
*
* Case 4: Tool execution (auto-approve ON)
* ```
* shouldBlockQueueForAsk(undefined) → false
* // Queue can process - but sendingDisabled=true blocks it anyway
* // Note: clineAsk is undefined because tool auto-executed (no ask shown)
* ```
*
* Case 5: Task completed
* ```
* shouldBlockQueueForAsk("completion_result") → false
* // Queue can process - perfect time to send user's queued message
* ```
*
* Case 6: Model asked followup
* ```
* shouldBlockQueueForAsk("followup") → false
* // Queue can process - user's queued message is their response
* ```
*/
export function shouldBlockQueueForAsk(clineAsk: ClineAsk | undefined): boolean {
// No ask active - nothing blocking the queue from the ask side
if (!clineAsk) {
return false
}
// Category 1: Always block these - user must handle them first
if ((ALWAYS_BLOCKING_ASKS as readonly string[]).includes(clineAsk)) {
return true
}
// Category 2: Block tool approvals - user must approve/reject first
// Note: If we see these asks, it means auto-approve is OFF for that tool type.
// If auto-approve were ON, the ask would never be created and clineAsk would be undefined.
if ((TOOL_APPROVAL_ASKS as readonly string[]).includes(clineAsk)) {
return true
}
// Category 3 (implicit): Everything else allows queue processing
// This includes followup, completion_result, resume_task, etc.
return false
}
/**
* Complete queue processing eligibility check.
* Combines all blocking conditions into a single function.
*
* @returns true if queue CAN process, false if queue is BLOCKED
*
* BLOCKING CONDITIONS (if ANY are true, queue is blocked):
*
* 1. sendingDisabled = true
* - Active work happening (streaming, API request, tool executing)
* - This is the PRIMARY blocker during normal operation
*
* 2. messageQueueLength = 0
* - No messages to process (obvious)
*
* 3. isProcessing = true
* - Already processing a queued message (prevents race conditions)
*
* 4. messagesLength = 0
* - No task exists (user clicked "New Task", queue should clear not fire)
*
* 5. shouldBlockQueueForAsk(clineAsk) = true
* - User needs to make a decision (see function above for details)
*
* TIMING DIAGRAM - WHEN EACH BLOCKER IS ACTIVE:
*
* ```
* ─────────────────────────────────────────────────────────────────────────
* State: | Idle | Stream | Tool(ON) | Tool(OFF) | Error | Complete |
* ─────────────────────────────────────────────────────────────────────────
* sendingDis. | ✗ | ✓ | ✓ | ✗ | ✓* | ✗ |
* clineAsk | - | - | - | "tool" | "err" | "complete"|
* shouldBlock() | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ |
* ─────────────────────────────────────────────────────────────────────────
* Queue blocked | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ |
* ─────────────────────────────────────────────────────────────────────────
*
* Legend:
* - Tool(ON) = Auto-approve is ON, tool executes immediately
* - Tool(OFF) = Auto-approve is OFF, user sees approval dialog
* - Error = api_req_failed or similar error state
* - * = sendingDisabled may be true or false depending on error type
* ```
*/
export function canProcessQueue(params: {
sendingDisabled: boolean
messageQueueLength: number
clineAsk: ClineAsk | undefined
isProcessing: boolean
messagesLength: number
}): boolean {
const { sendingDisabled, messageQueueLength, clineAsk, isProcessing, messagesLength } = params
// Blocker 1: Active work happening
if (sendingDisabled) {
return false
}
// Blocker 2: No messages to send
if (messageQueueLength === 0) {
return false
}
// Blocker 3: Already processing (prevents duplicate sends)
if (isProcessing) {
return false
}
// Blocker 4: No task (shouldn't auto-start with queue after "New Task")
if (messagesLength === 0) {
return false
}
// Blocker 5: Ask requires user decision
if (shouldBlockQueueForAsk(clineAsk)) {
return false
}
// All checks passed - queue can process!
return true
}
@@ -346,15 +346,46 @@ export const ExtensionStateContextProvider: React.FC<{
try {
const stateData = JSON.parse(response.stateJson) as ExtensionState
setState((prevState) => {
const incomingTaskId = stateData.currentTaskItem?.id
const prevTaskId = prevState.currentTaskItem?.id
// Versioning logic for autoApprovalSettings
const incomingVersion = stateData.autoApprovalSettings?.version ?? 1
const currentVersion = prevState.autoApprovalSettings?.version ?? 1
const shouldUpdateAutoApproval = incomingVersion > currentVersion
// HACK: Preserve clineMessages if currentTaskItem is the same
if (stateData.currentTaskItem?.id === prevState.currentTaskItem?.id) {
stateData.clineMessages = stateData.clineMessages?.length
? stateData.clineMessages
: prevState.clineMessages
// HACK: Preserve clineMessages to avoid losing conversation history
// Cases where we preserve prev messages:
// 1. Same task ID (both defined) and prev has more messages
// 2. Incoming task is undefined but prev task exists (transient cancel state)
// 3. Different task ID but incoming has 0 messages (transient state)
const incomingCount = stateData.clineMessages?.length ?? 0
const prevCount = prevState.clineMessages?.length ?? 0
// Reuse incomingTaskId and prevTaskId from debug section above
const sameTaskId = incomingTaskId === prevTaskId
const bothTasksUndefined = incomingTaskId === undefined && prevTaskId === undefined
const incomingTaskUndefined = incomingTaskId === undefined
const prevTaskExists = prevTaskId !== undefined
// Evaluate each condition separately for detailed logging
// Condition 1: Same task, prev has more messages, but incoming > 0 (stale update during streaming)
const condition1 = sameTaskId && !bothTasksUndefined && prevCount > incomingCount && incomingCount > 0
// Condition 2 REMOVED: Was causing "double click" bug on New Task button.
// const condition2 = incomingTaskUndefined && prevTaskExists && prevCount > 0
// Condition 3: Different task ID (both defined), incoming has 0 messages (loading new task)
// NOTE: Only trigger if BOTH task IDs are defined - if incoming is undefined, it's an intentional clear
const condition3 =
!sameTaskId &&
incomingTaskId !== undefined &&
prevTaskId !== undefined &&
incomingCount === 0 &&
prevCount > 0
const shouldPreservePrev = condition1 || condition3
if (shouldPreservePrev) {
stateData.clineMessages = prevState.clineMessages
// NOTE: We do NOT preserve currentTaskItem here to allow cancel to work
// When user cancels, backend sends currentTaskItem: undefined intentionally
// Preserving it would make cancel appear stuck
}
const newState = {
@@ -414,8 +445,8 @@ export const ExtensionStateContextProvider: React.FC<{
{},
{
onResponse: () => {
// When history button is clicked, navigate to history view
console.log("[DEBUG] Received history button clicked event from gRPC stream")
// When history button is clicked, navigate to history view
navigateToHistory()
},
onError: (error) => {
@@ -432,8 +463,8 @@ export const ExtensionStateContextProvider: React.FC<{
{},
{
onResponse: () => {
// When chat button is clicked, navigate to chat
console.log("[DEBUG] Received chat button clicked event from gRPC stream")
// When chat button is clicked, navigate to chat
navigateToChat()
},
onError: (error) => {
@@ -580,8 +611,8 @@ export const ExtensionStateContextProvider: React.FC<{
// Set up account button clicked subscription
accountButtonClickedSubscriptionRef.current = UiServiceClient.subscribeToAccountButtonClicked(EmptyRequest.create(), {
onResponse: () => {
// When account button is clicked, navigate to account view
console.log("[DEBUG] Received account button clicked event from gRPC stream")
// When account button is clicked, navigate to account view
navigateToAccount()
},
onError: (error) => {