Skip to content

Commit aebd78e

Browse files
committed
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
1 parent 0e1b23d commit aebd78e

File tree

5 files changed

+70
-0
lines changed

5 files changed

+70
-0
lines changed

packages/types/src/global-settings.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ export const globalSettingsSchema = z.object({
4343
taskHistory: z.array(historyItemSchema).optional(),
4444
dismissedUpsells: z.array(z.string()).optional(),
4545

46+
// Chat draft persistence
47+
chatDraft: z
48+
.object({
49+
text: z.string(),
50+
images: z.array(z.string()),
51+
})
52+
.optional(),
53+
4654
// Image generation settings (experimental) - flattened for simplicity
4755
openRouterImageApiKey: z.string().optional(),
4856
openRouterImageGenerationSelectedModel: z.string().optional(),

src/core/webview/webviewMessageHandler.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3058,5 +3058,27 @@ export const webviewMessageHandler = async (
30583058
})
30593059
break
30603060
}
3061+
case "persistDraft": {
3062+
// Save the draft to global state
3063+
await updateGlobalState("chatDraft", { text: message.text || "", images: message.images || [] })
3064+
break
3065+
}
3066+
case "clearPersistedDraft": {
3067+
// Clear the persisted draft
3068+
await updateGlobalState("chatDraft", undefined)
3069+
break
3070+
}
3071+
case "requestPersistedDraft": {
3072+
// Send the persisted draft to the webview
3073+
const draft = getGlobalState("chatDraft")
3074+
if (draft) {
3075+
await provider.postMessageToWebview({
3076+
type: "persistedDraft",
3077+
text: draft.text,
3078+
images: draft.images,
3079+
})
3080+
}
3081+
break
3082+
}
30613083
}
30623084
}

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ export interface ExtensionMessage {
124124
| "commands"
125125
| "insertTextIntoTextarea"
126126
| "dismissedUpsells"
127+
| "persistedDraft"
127128
text?: string
128129
payload?: any // Add a generic payload for now, can refine later
129130
action?:

src/shared/WebviewMessage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,9 @@ export interface WebviewMessage {
227227
| "editQueuedMessage"
228228
| "dismissUpsell"
229229
| "getDismissedUpsells"
230+
| "persistDraft"
231+
| "clearPersistedDraft"
232+
| "requestPersistedDraft"
230233
text?: string
231234
editedMessageContent?: string
232235
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
171171
// Has to be after api_req_finished are all reduced into api_req_started messages.
172172
const apiMetrics = useMemo(() => getApiMetrics(modifiedMessages), [modifiedMessages])
173173

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

228+
// Load persisted draft on mount
229+
useEffect(() => {
230+
// Request the persisted draft from the extension
231+
vscode.postMessage({ type: "requestPersistedDraft" })
232+
}, [])
233+
234+
// Save draft when input changes (debounced)
235+
useEffect(() => {
236+
// Don't save empty drafts or when there's an active task
237+
if (!task && (inputValue.trim() || selectedImages.length > 0)) {
238+
const timer = setTimeout(() => {
239+
vscode.postMessage({
240+
type: "persistDraft",
241+
text: inputValue,
242+
images: selectedImages,
243+
})
244+
}, 500) // Debounce for 500ms
245+
246+
return () => clearTimeout(timer)
247+
}
248+
}, [inputValue, selectedImages, task])
249+
227250
useEffect(() => {
228251
isMountedRef.current = true
229252
return () => {
@@ -582,6 +605,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
582605
// setPrimaryButtonText(undefined)
583606
// setSecondaryButtonText(undefined)
584607
disableAutoScrollRef.current = false
608+
609+
// Clear the persisted draft when chat is reset (task started)
610+
vscode.postMessage({ type: "clearPersistedDraft" })
585611
}, [])
586612

587613
/**
@@ -793,6 +819,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
793819
break
794820
}
795821
break
822+
case "persistedDraft":
823+
// Restore the persisted draft if we don't have an active task
824+
if (!task && message.text !== undefined) {
825+
setInputValue(message.text)
826+
if (message.images && message.images.length > 0) {
827+
setSelectedImages(message.images)
828+
}
829+
}
830+
break
796831
case "selectedImages":
797832
// Only handle selectedImages if it's not for editing context
798833
// When context is "edit", ChatRow will handle the images
@@ -845,6 +880,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
845880
handleSetChatBoxMessage,
846881
handlePrimaryButtonClick,
847882
handleSecondaryButtonClick,
883+
task,
848884
],
849885
)
850886

0 commit comments

Comments
 (0)