Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ export const globalSettingsSchema = z.object({
taskHistory: z.array(historyItemSchema).optional(),
dismissedUpsells: z.array(z.string()).optional(),

// Chat draft persistence
chatDraft: z
.object({
text: z.string(),
images: z.array(z.string()),
})
.optional(),

// Image generation settings (experimental) - flattened for simplicity
openRouterImageApiKey: z.string().optional(),
openRouterImageGenerationSelectedModel: z.string().optional(),
Expand Down
22 changes: 22 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3058,5 +3058,27 @@ export const webviewMessageHandler = async (
})
break
}
case "persistDraft": {
// Save the draft to global state
await updateGlobalState("chatDraft", { text: message.text || "", images: message.images || [] })
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Missing error handling: These draft persistence operations should have try-catch blocks to handle potential failures gracefully.

break
}
case "clearPersistedDraft": {
// Clear the persisted draft
await updateGlobalState("chatDraft", undefined)
break
}
case "requestPersistedDraft": {
// Send the persisted draft to the webview
const draft = getGlobalState("chatDraft")
if (draft) {
await provider.postMessageToWebview({
type: "persistedDraft",
text: draft.text,
images: draft.images,
})
}
break
}
}
}
1 change: 1 addition & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export interface ExtensionMessage {
| "commands"
| "insertTextIntoTextarea"
| "dismissedUpsells"
| "persistedDraft"
text?: string
payload?: any // Add a generic payload for now, can refine later
action?:
Expand Down
3 changes: 3 additions & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ export interface WebviewMessage {
| "editQueuedMessage"
| "dismissUpsell"
| "getDismissedUpsells"
| "persistDraft"
Copy link
Contributor

Choose a reason for hiding this comment

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

There's an inconsistency in the naming of these message types: the new key is persistDraft (without the 'ed'), whereas the subsequent keys use Persisted (i.e. clearPersistedDraft and requestPersistedDraft). Consider aligning these so that the naming is consistent (e.g., either use persistDraft consistently or update it to persistedDraft).

| "clearPersistedDraft"
| "requestPersistedDraft"
text?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
Expand Down
36 changes: 36 additions & 0 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
// Has to be after api_req_finished are all reduced into api_req_started messages.
const apiMetrics = useMemo(() => getApiMetrics(modifiedMessages), [modifiedMessages])

// Initialize input value from persisted draft if available
const [inputValue, setInputValue] = useState("")
const inputValueRef = useRef(inputValue)
const textAreaRef = useRef<HTMLTextAreaElement>(null)
Expand Down Expand Up @@ -224,6 +225,28 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
inputValueRef.current = inputValue
}, [inputValue])

// Load persisted draft on mount
useEffect(() => {
// Request the persisted draft from the extension
vscode.postMessage({ type: "requestPersistedDraft" })
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Race condition risk: There's no guarantee the extension will respond before the user starts typing, potentially overwriting their input with an older draft.

Consider adding a flag to prevent overwriting if the user has already started typing.

}, [])

// Save draft when input changes (debounced)
useEffect(() => {
// Don't save empty drafts or when there's an active task
if (!task && (inputValue.trim() || selectedImages.length > 0)) {
const timer = setTimeout(() => {
vscode.postMessage({
type: "persistDraft",
text: inputValue,
images: selectedImages,
})
}, 500) // Debounce for 500ms
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Potential memory leak: This useEffect creates a new timeout on every inputValue or selectedImages change but only cleans up the last one. If the user types quickly, multiple timeouts could accumulate.

Consider using a ref to track the timeout or a proper debounce hook.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hardcoded magic number: Consider extracting this debounce delay as a named constant for better maintainability: const DRAFT_SAVE_DEBOUNCE_MS = 500;


return () => clearTimeout(timer)
}
}, [inputValue, selectedImages, task])

useEffect(() => {
isMountedRef.current = true
return () => {
Expand Down Expand Up @@ -582,6 +605,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
// setPrimaryButtonText(undefined)
// setSecondaryButtonText(undefined)
disableAutoScrollRef.current = false

// Clear the persisted draft when chat is reset (task started)
vscode.postMessage({ type: "clearPersistedDraft" })
}, [])

/**
Expand Down Expand Up @@ -793,6 +819,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
break
}
break
case "persistedDraft":
// Restore the persisted draft if we don't have an active task
if (!task && message.text !== undefined) {
setInputValue(message.text)
if (message.images && message.images.length > 0) {
setSelectedImages(message.images)
}
}
break
case "selectedImages":
// Only handle selectedImages if it's not for editing context
// When context is "edit", ChatRow will handle the images
Expand Down Expand Up @@ -845,6 +880,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
handleSetChatBoxMessage,
handlePrimaryButtonClick,
handleSecondaryButtonClick,
task,
],
)

Expand Down
Loading