Skip to content

ref(seer): Persist Seer Explorer input draft per run#115919

Merged
aliu39 merged 6 commits into
masterfrom
aliu/seer-explorer-persist-input
May 21, 2026
Merged

ref(seer): Persist Seer Explorer input draft per run#115919
aliu39 merged 6 commits into
masterfrom
aliu/seer-explorer-persist-input

Conversation

@aliu39
Copy link
Copy Markdown
Member

@aliu39 aliu39 commented May 20, 2026

Summary

  • Add useDeferredSessionStorage<T>(key, initialValue) in static/app/utils/, a generic sessionStorage hook with deferred writes — storage is only touched on key change, unmount, or explicit reset(). No per-keystroke main-thread writes.
  • Extract readStorageValue / writeStorageValue / removeStorageValue helpers in useSessionStorage.tsx and share them with the new hook. useSessionStorage itself now accepts key: string | null, with null disabling persistence (matches the new hook's behavior).
  • Wire useDeferredSessionStorage into the Seer Explorer drawer keyed by runId, so the textarea draft survives drawer close/reopen and run switches.

Behavior notes

  • handleSend calls reset(), which wipes both state and storage for the current runId.
  • Closing the drawer (component unmount) flushes the current draft to its runId's key.
  • Switching runs auto-saves the current run's draft to its key and loads the new run's draft.
  • When runId is null (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.
  • The hook's auto-flush on key change replaces the previous onSuccess: clearInput plumbing in startNewSession / switchToRun.

Tests

Added an Input Persistence block to explorerDrawerContent.spec.tsx:

  • Restores the persisted draft when the drawer remounts.
  • Persists the draft per runId across run switches.
  • Never writes to sessionStorage when runId is null.
  • Clears the persisted draft when a message is sent.

Test plan

  • Type a partial message in the drawer with an active session, close it, reopen — draft is restored.
  • Type in run A, switch to run B, switch back — each run shows its own draft.
  • Open the drawer with no active session, type, close — nothing in sessionStorage.
  • Send a message — input clears and sessionStorage for that runId is removed.
  • Refresh the page (same tab) — drafts persist; open a new tab — drafts not shared (sessionStorage scope).

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]>
@github-actions github-actions Bot added the Scope: Frontend Automatically applied to PRs that change frontend components label May 20, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

📊 Type Coverage Diff

✅ No new type safety issues introduced. Coverage: 93.59%

aliu39 and others added 2 commits May 20, 2026 11:13
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]>
@aliu39 aliu39 marked this pull request as ready for review May 20, 2026 18:34
@aliu39 aliu39 requested a review from a team as a code owner May 20, 2026 18:34
@aliu39 aliu39 changed the title feat(seer): Persist Seer Explorer input draft per run ref(seer): Persist Seer Explorer input draft per run May 20, 2026
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks similar to useLocalStorageState. you could use that and pass in the same kind of key

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd aim to layer a throttle or something, instead of baking things in

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ryan953 do you mean to throttle the number of updates to local/session storage?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

natemoo-re
natemoo-re previously approved these changes May 20, 2026
Copy link
Copy Markdown
Member

@natemoo-re natemoo-re left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@natemoo-re natemoo-re dismissed their stale review May 20, 2026 19:01

Waiting until we resolve hook duplication discussion

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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);
};
}, []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 158e036. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wont affect functionality, just an extra storage write on this edge case

@aliu39 aliu39 merged commit b06bf2d into master May 21, 2026
72 checks passed
@aliu39 aliu39 deleted the aliu/seer-explorer-persist-input branch May 21, 2026 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants