-
-
Notifications
You must be signed in to change notification settings - Fork 213
feat: persist editor state on page refresh #619
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bf25c77
2d72804
bc6c38d
35c7d55
87803c5
0e36daf
431fc1c
35254e0
24a870e
49b0608
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,6 +11,8 @@ import * as playground from "../samples/playground"; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { compress, decompress } from "../utils/compression/compression"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { AIConfig, ChatState } from '../types/components/AIAssistant.types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const EDITOR_STATE_KEY = 'editor-state'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface AppState { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| templateMarkdown: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| editorValue: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -61,6 +63,7 @@ interface AppState { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toggleModelCollapse: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toggleTemplateCollapse: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toggleDataCollapse: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resetToDefault: () => Promise<void>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| showLineNumbers: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setShowLineNumbers: (value: boolean) => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isSettingsOpen: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -137,6 +140,54 @@ const getInitialPanelState = () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return defaults; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* --- Helper to safely load editor state --- */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const getInitialEditorState = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if(typeof window !== 'undefined'){ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const saved = localStorage.getItem(EDITOR_STATE_KEY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if(saved){ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return JSON.parse(saved); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch(e){ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* ignore */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+143
to
+152
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* --- Helper to safely load editor state --- */ | |
| const getInitialEditorState = () => { | |
| if(typeof window !== 'undefined'){ | |
| try{ | |
| const saved = localStorage.getItem(EDITOR_STATE_KEY); | |
| if(saved){ | |
| return JSON.parse(saved); | |
| } | |
| } catch(e){ | |
| /* ignore */ | |
| interface PersistedEditorState { | |
| readonly editorValue?: string; | |
| readonly templateMarkdown?: string; | |
| readonly editorModelCto?: string; | |
| readonly modelCto?: string; | |
| readonly data?: string; | |
| readonly editorAgreementData?: string; | |
| } | |
| const isPersistedEditorState = (value: unknown): value is PersistedEditorState => { | |
| if (value === null || typeof value !== "object") { | |
| return false; | |
| } | |
| const candidate = value as Record<string, unknown>; | |
| const isStringOrUndefined = (v: unknown): v is string | undefined => | |
| typeof v === "string" || typeof v === "undefined"; | |
| return ( | |
| (!("editorValue" in candidate) || isStringOrUndefined(candidate.editorValue)) && | |
| (!("templateMarkdown" in candidate) || isStringOrUndefined(candidate.templateMarkdown)) && | |
| (!("editorModelCto" in candidate) || isStringOrUndefined(candidate.editorModelCto)) && | |
| (!("modelCto" in candidate) || isStringOrUndefined(candidate.modelCto)) && | |
| (!("data" in candidate) || isStringOrUndefined(candidate.data)) && | |
| (!("editorAgreementData" in candidate) || isStringOrUndefined(candidate.editorAgreementData)) | |
| ); | |
| }; | |
| /* --- Helper to safely load editor state --- */ | |
| const getInitialEditorState = (): PersistedEditorState | null => { | |
| if (typeof window !== "undefined") { | |
| try { | |
| const saved = localStorage.getItem(EDITOR_STATE_KEY); | |
| if (saved) { | |
| const parsed: unknown = JSON.parse(saved); | |
| if (isPersistedEditorState(parsed)) { | |
| return parsed; | |
| } | |
| } | |
| } catch (e) { | |
| /* ignore */ |
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The quota handling is currently gated on e.name === 'QuotaExceededError'. In practice, quota-exceeded failures use different names in some browsers (e.g., Firefox's NS_ERROR_DOM_QUOTA_REACHED) and sometimes only provide a numeric code. Consider broadening the detection (e.g., check DOMException code/name variants) so the graceful fallback runs consistently cross-browser.
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR adds new persistent editor behavior + a new reset action, but there are existing unit tests for the store and localStorage-backed settings. Please add/extend store tests to cover: (1) debounced writes to editor-state, (2) init restoring state, and (3) resetToDefault clearing storage (including the case where a debounced save is pending).
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Persistence is only triggered from the individual setter actions. Store-driven content changes like loadSample (and potentially other bulk loads) won't update editor-state unless the user edits afterward, so a refresh immediately after loading a sample can restore stale persisted content. Consider invoking the debounced save after bulk state updates that change the editors (e.g., at the end of loadSample).
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resetToDefault removes the localStorage key, but any in-flight saveEditorStateDeBounced call scheduled from recent edits can still fire after the reset and repopulate editor-state with the pre-reset content. To make reset reliable, cancel/flush the pending debounced save before clearing storage (and/or temporarily disable saving during the reset flow).
| // Clear saved editor state from localStorage | |
| if (typeof window !== 'undefined') { | |
| // Cancel any in-flight debounced save to avoid restoring stale editor state | |
| try { | |
| // saveEditorStateDeBounced is created via ts-debounce and exposes a cancel method | |
| saveEditorStateDeBounced.cancel(); | |
| } catch { | |
| // If for some reason cancellation fails, continue with reset | |
| } | |
| // Clear saved editor state from localStorage | |
| if (typeof window !== "undefined") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new "Reset" sidebar action is user-facing and destructive; there are existing component tests for
PlaygroundSidebar, but they don't cover rendering/clicking this new item or asserting the confirmation flow. Please extend the sidebar test to include the Reset button and verify it callsModal.confirmand triggersresetToDefaulton confirmation.