From 6242fa22e117ba0febc1a862359a4f3558f023e0 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Fri, 23 Jan 2026 11:13:12 +0530 Subject: [PATCH 1/7] feat: save input drafts to local storage --- .../TipTapEditor/utils/editorConfig.ts | 23 ++++++++++++++++++- gui/src/util/localStorage.ts | 1 + 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts b/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts index b05109c9bca..2e453ee15be 100644 --- a/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts +++ b/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts @@ -22,6 +22,10 @@ import { selectSelectedChatModel } from "../../../../redux/slices/configSlice"; import { AppDispatch } from "../../../../redux/store"; import { exitEdit } from "../../../../redux/thunks/edit"; import { getFontSize, isJetBrains } from "../../../../util"; +import { + getLocalStorage, + setLocalStorage, +} from "../../../../util/localStorage"; import { CodeBlock, Mention, PromptBlock, SlashCommand } from "../extensions"; import { TipTapEditorProps } from "../TipTapEditor"; import { @@ -379,8 +383,23 @@ export function createEditorConfig(options: { style: `font-size: ${getFontSize()}px;`, }, }, - content: props.editorState, + content: + props.editorState ?? + (props.isMainInput + ? getLocalStorage(`inputDraft_${props.historyKey}`) + : undefined), editable: !isStreaming || props.isMainInput, + onUpdate: ({ editor }) => { + if (props.isMainInput) { + const content = editor.getJSON(); + if (hasValidEditorContent(content)) { + setLocalStorage(`inputDraft_${props.historyKey}`, content); + } else { + // clear draft if content is empty + localStorage.removeItem(`inputDraft_${props.historyKey}`); + } + } + }, }); const onEnter = (modifiers: InputModifiers) => { @@ -400,6 +419,8 @@ export function createEditorConfig(options: { if (props.isMainInput) { addRef.current(json); + // clear draft from localStorage after successful submission + localStorage.removeItem(`inputDraft_${props.historyKey}`); } props.onEnter(json, modifiers, editor); diff --git a/gui/src/util/localStorage.ts b/gui/src/util/localStorage.ts index 15221159f74..e97210706d2 100644 --- a/gui/src/util/localStorage.ts +++ b/gui/src/util/localStorage.ts @@ -11,6 +11,7 @@ type LocalStorageTypes = { vsCodeUriScheme: string; fontSize: number; [key: `inputHistory_${string}`]: JSONContent[]; + [key: `inputDraft_${string}`]: JSONContent; extensionVersion: string; showTutorialCard: boolean; shownProfilesIntroduction: boolean; From e2a38e37a91a0a2710a9be802b96cff26a878822 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Fri, 23 Jan 2026 11:13:40 +0530 Subject: [PATCH 2/7] remove unnecessary clearContent --- gui/src/components/mainInput/TipTapEditor/TipTapEditor.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/gui/src/components/mainInput/TipTapEditor/TipTapEditor.tsx b/gui/src/components/mainInput/TipTapEditor/TipTapEditor.tsx index c19ae0af242..43278094cab 100644 --- a/gui/src/components/mainInput/TipTapEditor/TipTapEditor.tsx +++ b/gui/src/components/mainInput/TipTapEditor/TipTapEditor.tsx @@ -95,12 +95,6 @@ function TipTapEditorInner(props: TipTapEditorProps) { } }, [editor, props.placeholder, historyLength]); - useEffect(() => { - if (props.isMainInput) { - editor?.commands.clearContent(true); - } - }, [editor, props.isMainInput]); - useEffect(() => { if (isInEdit) { setShouldHideToolbar(false); From 4a8b98ab5abebf0a26aa1cb17d39069394183f02 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:56:52 +0530 Subject: [PATCH 3/7] save editing draft of the message and scroll to position when going back --- .../TipTapEditor/utils/editorConfig.ts | 22 ++++++++- gui/src/pages/gui/Chat.tsx | 45 +++++++++++++++++-- gui/src/util/localStorage.ts | 7 +++ 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts b/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts index e412ee6b8bc..821bd52a267 100644 --- a/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts +++ b/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts @@ -401,14 +401,30 @@ export function createEditorConfig(options: { : undefined), editable: !isStreaming || props.isMainInput, onUpdate: ({ editor }) => { + const content = editor.getJSON(); if (props.isMainInput) { - const content = editor.getJSON(); if (hasValidEditorContent(content)) { setLocalStorage(`inputDraft_${props.historyKey}`, content); + localStorage.removeItem(`editingDraft_${props.historyKey}`); } else { // clear draft if content is empty localStorage.removeItem(`inputDraft_${props.historyKey}`); } + } else { + if (hasValidEditorContent(content)) { + const scrollContainer = document.querySelector( + '[class*="overflow-y-scroll"]', + ); + const scrollTop = scrollContainer?.scrollTop ?? 0; + setLocalStorage(`editingDraft_${props.historyKey}`, { + content, + messageId: props.inputId, + scrollTop, + }); + localStorage.removeItem(`inputDraft_${props.historyKey}`); + } else { + localStorage.removeItem(`editingDraft_${props.historyKey}`); + } } }, }); @@ -428,10 +444,12 @@ export function createEditorConfig(options: { return; } + // clear draft from localStorage after successful submission if (props.isMainInput) { addRef.current(json); - // clear draft from localStorage after successful submission localStorage.removeItem(`inputDraft_${props.historyKey}`); + } else { + localStorage.removeItem(`editingDraft_${props.historyKey}`); } props.onEnter(json, modifiers, editor); diff --git a/gui/src/pages/gui/Chat.tsx b/gui/src/pages/gui/Chat.tsx index 45929ec12e2..4647d1983cf 100644 --- a/gui/src/pages/gui/Chat.tsx +++ b/gui/src/pages/gui/Chat.tsx @@ -55,7 +55,11 @@ import { resolveEditorContent } from "../../components/mainInput/TipTapEditor/ut import { setDialogMessage, setShowDialog } from "../../redux/slices/uiSlice"; import { RootState } from "../../redux/store"; import { cancelStream } from "../../redux/thunks/cancelStream"; -import { getLocalStorage, setLocalStorage } from "../../util/localStorage"; +import { + getLocalStorage, + InputDraftWithPosition, + setLocalStorage, +} from "../../util/localStorage"; import { EmptyChatBody } from "./EmptyChatBody"; import { ExploreDialogWatcher } from "./ExploreDialogWatcher"; import { useAutoScroll } from "./useAutoScroll"; @@ -119,12 +123,13 @@ export function Chat() { const mainTextInputRef = useRef(null); const stepsDivRef = useRef(null); const tabsRef = useRef(null); + const timerRef = useRef(undefined); const history = useAppSelector((state) => state.session.history); const showChatScrollbar = useAppSelector( (state) => state.config.config.ui?.showChatScrollbar, ); - const codeToEdit = useAppSelector((state) => state.editModeState.codeToEdit); const isInEdit = useAppSelector((store) => store.session.isInEdit); + const sessionId = useAppSelector((state) => state.session.id); const lastSessionId = useAppSelector((state) => state.session.lastSessionId); const allSessionMetadata = useAppSelector( @@ -134,13 +139,38 @@ export function Chat() { (state) => state.ui.hasDismissedExploreDialog, ); const mode = useAppSelector((state) => state.session.mode); - const currentOrg = useAppSelector(selectCurrentOrg); const jetbrains = useMemo(() => { return isJetBrains(); }, []); useAutoScroll(stepsDivRef, history); + useEffect(() => { + const historyKey = isInEdit ? "edit" : "chat"; + const savedDraft = getLocalStorage(`editingDraft_${historyKey}`) as + | InputDraftWithPosition + | undefined; + if (savedDraft && savedDraft.messageId && stepsDivRef.current) { + timerRef.current = setTimeout(() => { + // scroll to and focus on the message being edited + requestAnimationFrame(() => { + if (stepsDivRef.current && savedDraft.scrollTop !== undefined) { + stepsDivRef.current.scrollTop = savedDraft.scrollTop; + } + const editorElement = document.querySelector( + `[data-testid="editor-input-${savedDraft.messageId}"]`, + ) as HTMLElement; + if (editorElement) { + editorElement.focus(); + } + }); + }, 100); + } + return () => { + clearTimeout(timerRef.current); + }; + }, [sessionId, isInEdit]); + useEffect(() => { // Cmd + Backspace to delete current step const listener = (e: KeyboardEvent) => { @@ -337,6 +367,13 @@ export function Chat() { latestSummaryIndex !== -1 && index < latestSummaryIndex; if (message.role === "user") { + const historyKey = isInEdit ? "edit" : "chat"; + const savedDraft = getLocalStorage(`editingDraft_${historyKey}`) as + | InputDraftWithPosition + | undefined; + const draftContent = + savedDraft?.messageId === message.id ? savedDraft.content : undefined; + return ( @@ -344,7 +381,7 @@ export function Chat() { } isLastUserInput={isLastUserInput(index)} isMainInput={false} - editorState={editorState ?? item.message.content} + editorState={draftContent ?? editorState ?? item.message.content} contextItems={contextItems} appliedRules={appliedRules} inputId={message.id} diff --git a/gui/src/util/localStorage.ts b/gui/src/util/localStorage.ts index e97210706d2..03d6ac3f6f1 100644 --- a/gui/src/util/localStorage.ts +++ b/gui/src/util/localStorage.ts @@ -1,6 +1,12 @@ import { JSONContent } from "@tiptap/react"; import { OnboardingStatus } from "../components/OnboardingCard"; +export type InputDraftWithPosition = { + content: JSONContent; + messageId: string; + scrollTop: number; +}; + type LocalStorageTypes = { isExploreDialogOpen: boolean; hasDismissedExploreDialog: boolean; @@ -12,6 +18,7 @@ type LocalStorageTypes = { fontSize: number; [key: `inputHistory_${string}`]: JSONContent[]; [key: `inputDraft_${string}`]: JSONContent; + [key: `editingDraft_${string}`]: InputDraftWithPosition; extensionVersion: string; showTutorialCard: boolean; shownProfilesIntroduction: boolean; From d090ce50fb676f21701a6672799194fcfd261c81 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:02:33 +0530 Subject: [PATCH 4/7] get inputdraft from chat.tsx --- .../mainInput/TipTapEditor/utils/editorConfig.ts | 11 ++--------- gui/src/pages/gui/Chat.tsx | 3 +++ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts b/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts index 821bd52a267..b193145f81f 100644 --- a/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts +++ b/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts @@ -22,10 +22,7 @@ import { selectSelectedChatModel } from "../../../../redux/slices/configSlice"; import { AppDispatch } from "../../../../redux/store"; import { exitEdit } from "../../../../redux/thunks/edit"; import { getFontSize, isJetBrains } from "../../../../util"; -import { - getLocalStorage, - setLocalStorage, -} from "../../../../util/localStorage"; +import { setLocalStorage } from "../../../../util/localStorage"; import { CodeBlock, Mention, PromptBlock, SlashCommand } from "../extensions"; import { TipTapEditorProps } from "../TipTapEditor"; import { @@ -394,11 +391,7 @@ export function createEditorConfig(options: { style: `font-size: ${getFontSize()}px;`, }, }, - content: - props.editorState ?? - (props.isMainInput - ? getLocalStorage(`inputDraft_${props.historyKey}`) - : undefined), + content: props.editorState, editable: !isStreaming || props.isMainInput, onUpdate: ({ editor }) => { const content = editor.getJSON(); diff --git a/gui/src/pages/gui/Chat.tsx b/gui/src/pages/gui/Chat.tsx index 4647d1983cf..9bdee5ca0a3 100644 --- a/gui/src/pages/gui/Chat.tsx +++ b/gui/src/pages/gui/Chat.tsx @@ -508,6 +508,9 @@ export function Chat() { onEnter={(editorState, modifiers, editor) => sendInput(editorState, modifiers, undefined, editor) } + editorState={getLocalStorage( + `inputDraft_${isInEdit ? "edit" : "chat"}`, + )} inputId={MAIN_EDITOR_INPUT_ID} /> From 74b737d148e63950bd29272271dd3eeec24350f1 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:17:41 +0530 Subject: [PATCH 5/7] remove generic class for getting scroll container --- .../mainInput/TipTapEditor/utils/editorConfig.ts | 9 ++++----- gui/src/pages/gui/Chat.tsx | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts b/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts index b193145f81f..cad969f44d0 100644 --- a/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts +++ b/gui/src/components/mainInput/TipTapEditor/utils/editorConfig.ts @@ -405,8 +405,8 @@ export function createEditorConfig(options: { } } else { if (hasValidEditorContent(content)) { - const scrollContainer = document.querySelector( - '[class*="overflow-y-scroll"]', + const scrollContainer = document.getElementById( + "chat-scroll-container", ); const scrollTop = scrollContainer?.scrollTop ?? 0; setLocalStorage(`editingDraft_${props.historyKey}`, { @@ -440,10 +440,9 @@ export function createEditorConfig(options: { // clear draft from localStorage after successful submission if (props.isMainInput) { addRef.current(json); - localStorage.removeItem(`inputDraft_${props.historyKey}`); - } else { - localStorage.removeItem(`editingDraft_${props.historyKey}`); } + localStorage.removeItem(`inputDraft_${props.historyKey}`); + localStorage.removeItem(`editingDraft_${props.historyKey}`); props.onEnter(json, modifiers, editor); }; diff --git a/gui/src/pages/gui/Chat.tsx b/gui/src/pages/gui/Chat.tsx index 9bdee5ca0a3..85ebd9aa541 100644 --- a/gui/src/pages/gui/Chat.tsx +++ b/gui/src/pages/gui/Chat.tsx @@ -476,6 +476,7 @@ export function Chat() { {widget} 0 ? "flex-1" : ""}`} > From c58c705f9ced53eeabe082b88b0a1d9d8c922c7c Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:32:10 +0530 Subject: [PATCH 6/7] remove editing draft on unmount --- gui/src/hooks/ParallelListeners.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gui/src/hooks/ParallelListeners.tsx b/gui/src/hooks/ParallelListeners.tsx index bccb7d25bb5..d2543b8de96 100644 --- a/gui/src/hooks/ParallelListeners.tsx +++ b/gui/src/hooks/ParallelListeners.tsx @@ -267,6 +267,13 @@ function ParallelListeners() { migrateLocalStorage(dispatch); }, []); + useEffect(() => { + return () => { + localStorage.removeItem("editingDraft_edit"); + localStorage.removeItem("editingDraft_chat"); + }; + }); + return <>; } From dde7a975084318b2cc19aa28a104d02f422a793b Mon Sep 17 00:00:00 2001 From: Aditya Mitra <61635505+uinstinct@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:23:13 +0530 Subject: [PATCH 7/7] Update gui/src/hooks/ParallelListeners.tsx Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- gui/src/hooks/ParallelListeners.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/src/hooks/ParallelListeners.tsx b/gui/src/hooks/ParallelListeners.tsx index d2543b8de96..c2e4bb11267 100644 --- a/gui/src/hooks/ParallelListeners.tsx +++ b/gui/src/hooks/ParallelListeners.tsx @@ -272,7 +272,7 @@ function ParallelListeners() { localStorage.removeItem("editingDraft_edit"); localStorage.removeItem("editingDraft_chat"); }; - }); + }, []); return <>; }