feat: implement chat draft persistence

- Add chatDraft field to GlobalState type in global-settings.ts
- Implement draft saving/loading in ChatView component with 500ms debounce
- Add message handlers for draft persistence in webviewMessageHandler
- Add new WebviewMessage and ExtensionMessage types for draft operations
- Clear draft when task starts to avoid interference
- Persist both text and images in draft

Fixes #8236
This commit is contained in:
Roo Code 2025-09-22 23:37:05 +00:00
parent 0e1b23d09c
commit aebd78e079
5 changed files with 70 additions and 0 deletions

View file

@ -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(),

View file

@ -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 || [] })
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
}
}
}

View file

@ -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?:

View file

@ -227,6 +227,9 @@ export interface WebviewMessage {
| "editQueuedMessage"
| "dismissUpsell"
| "getDismissedUpsells"
| "persistDraft"
| "clearPersistedDraft"
| "requestPersistedDraft"
text?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"

View file

@ -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)
@ -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" })
}, [])
// 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
return () => clearTimeout(timer)
}
}, [inputValue, selectedImages, task])
useEffect(() => {
isMountedRef.current = true
return () => {
@ -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" })
}, [])
/**
@ -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
@ -845,6 +880,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
handleSetChatBoxMessage,
handlePrimaryButtonClick,
handleSecondaryButtonClick,
task,
],
)