ref(seer): Persist Seer Explorer input draft per run#115919
Conversation
Add `usePersistedValue` to keep a textarea draft in sessionStorage, scoped by runId. Writes are deferred — storage is only touched on scope change, unmount, or explicit clear — so per-keystroke writes never hit the main thread. Wire it into the explorer drawer so closing or switching runs no longer drops in-progress text. Co-Authored-By: Claude <[email protected]>
📊 Type Coverage Diff✅ No new type safety issues introduced. Coverage: 93.59% |
Add four cases under "Input Persistence": - Restores the persisted draft when the drawer remounts. - Defers sessionStorage writes (no per-keystroke write). - Scopes the draft per runId across run switches. - Clears the persisted draft when a message is sent. Co-Authored-By: Claude <[email protected]>
Treat a null `scopeId` as "in-memory only" — the hook no longer reads or writes sessionStorage for the no-session state, so drafts typed before a run exists never hit storage. Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
this looks similar to useLocalStorageState. you could use that and pass in the same kind of key
There was a problem hiding this comment.
None of our existing use*Storage hooks support deferred writes like this hook, which avoids hammering storage on every keystroke in a textarea.
There's certainly an argument to be made that we should add an option for one of those hooks to support it, though.
There was a problem hiding this comment.
i'd aim to layer a throttle or something, instead of baking things in
There was a problem hiding this comment.
@ryan953 do you mean to throttle the number of updates to local/session storage?
There was a problem hiding this comment.
I don't have strong enough opinions to block this PR. But I'll think it all through (rant) since we're here...
I was sorta alerted because i know the llms love to make new files instead of finding existing ones, and i think that kind of duplication was already a problem and it'll get much worse over time.
So we've got the base useLocalStorageState, which is the place that does serialization as well as the setState call (2 jobs). I'd argue that those two jobs could be split up a little better; the benefit could be that we don't need to have readValue() and writeValue() re-implemented.
I also found NoteInputWithStorage which is another implementation of the persist-unsaved-content pattern. It's got debounce() to wrap the save call, evidence that its a great call to have here too 👍 . It would be ideal if that component were composable, so we could just use it directly. Instead its coupled to NoteInput.
This kind of circles back to the idea that there are multiple similar implementations all over and they're hard to find. So if we keep this file i'd move it into something like static/app/utils/storage. The NoteInput isn't obvious to find, and who wants to share by importing from components/activity/note/
So yeah, not trying to block this. brain dump for y'all.
There was a problem hiding this comment.
Makes sense, thanks for the thoughts! I can move to app/utils so its easier to find, and see if we can reuse read/write
Waiting until we resolve hook duplication discussion
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 158e036. Configure here.
| return () => { | ||
| writeValue(keyRef.current, valueRef.current); | ||
| }; | ||
| }, []); |
There was a problem hiding this comment.
Unmount cleanup re-writes value after explicit reset
Low Severity
The unmount cleanup effect unconditionally writes valueRef.current to storage, which re-creates a storage entry that reset() explicitly removed via sessionStorageWrapper.removeItem. After handleSend calls clearInput() (i.e., reset()), closing the drawer triggers the unmount cleanup and writes the initialValue back under the same key, creating a zombie entry in sessionStorage. The test for "clears the persisted draft when a message is sent" only passes because it asserts before unmount occurs.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 158e036. Configure here.
There was a problem hiding this comment.
wont affect functionality, just an extra storage write on this edge case


Summary
useDeferredSessionStorage<T>(key, initialValue)instatic/app/utils/, a generic sessionStorage hook with deferred writes — storage is only touched on key change, unmount, or explicitreset(). No per-keystroke main-thread writes.readStorageValue/writeStorageValue/removeStorageValuehelpers inuseSessionStorage.tsxand share them with the new hook.useSessionStorageitself now acceptskey: string | null, withnulldisabling persistence (matches the new hook's behavior).useDeferredSessionStorageinto the Seer Explorer drawer keyed byrunId, so the textarea draft survives drawer close/reopen and run switches.Behavior notes
handleSendcallsreset(), which wipes both state and storage for the currentrunId.runId's key.runIdisnull(no session yet), the draft lives in React state only — nothing is read from or written to sessionStorage. Drafts only start persisting once a run exists.onSuccess: clearInputplumbing instartNewSession/switchToRun.Tests
Added an Input Persistence block to explorerDrawerContent.spec.tsx:
Test plan