fix(web): preserve unsent user-input answers across thread switches#4592
fix(web): preserve unsent user-input answers across thread switches#4592ipanasenko wants to merge 2 commits into
Conversation
Draft answers to a provider's user-input request lived in ChatView state, keyed by requestId. The router creates a new match per thread, so switching threads unmounted ChatView and discarded a typed but unsent answer. Move the drafts into a persisted zustand store keyed by requestId, so they outlive the component (and a reload). The draft is cleared once the answer is submitted successfully; failures keep it so the user can retry.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
`record[key] = value` invokes the prototype setter for a `"__proto__"` question id instead of creating an own property, so the draft vanished on serialize. Build the next answers map with a computed key in an object literal (and rest-strip on removal), both of which define own properties.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 510f62d. Configure here.
| }, | ||
| requestId, | ||
| ), | ||
| }; |
There was a problem hiding this comment.
Split eviction desyncs draft maps
Medium Severity
The evictOldestRequestDrafts function operates independently on answersByRequestId and questionIndexByRequestId. This can cause a requestId to be evicted from one map but not the other when the retention cap is reached, resulting in desynchronized or incomplete draft state for a pending user input request.
Reviewed by Cursor Bugbot for commit 510f62d. Configure here.
| } else if (result._tag !== "Failure") { | ||
| // The answer is on its way to the provider; the persisted draft has | ||
| // served its purpose. Failures keep it so the user can retry. | ||
| clearPendingUserInputDraft(requestId); |
There was a problem hiding this comment.
Draft cleared while still pending
Medium Severity
After a successful respondToUserInput command, clearPendingUserInputDraft runs immediately, but derivePendingUserInputs can still list that requestId until a user-input.resolved activity arrives. The pending composer then shows empty answers and selections even though the request is still open—unlike the prior in-memory state, which kept the draft visible until the pending UI went away.
Reviewed by Cursor Bugbot for commit 510f62d. Configure here.
ApprovabilityVerdict: Needs human review This PR introduces new persistence behavior for draft answers and has two unresolved medium-severity review comments identifying potential bugs in the eviction logic and draft clearing timing. You can customize Macroscope's approvability policy. Learn more. |


Problem
A typed-but-unsent answer to a provider's user-input question is lost as soon as you switch to another thread and come back.
Screen.Recording.2026-07-26.at.11.30.20.PM.mov
Cause
The draft answers lived in
ChatViewcomponent state —pendingUserInputAnswersByRequestId/pendingUserInputQuestionIndexByRequestId. The router creates a new match per$threadId, so a thread switch unmountsChatViewand throws that state away. Nothing was persisted.Change
apps/web/src/pendingUserInputDraftStore.ts: a zustand store persisted tolocalStorage(t3code:pending-user-input-drafts:v1, same pattern asterminalUiStateStore) holding draft answers and the active question index keyed byrequestId. Has a 50-request retention cap so abandoned drafts don't accumulate.ChatViewreads/writes that store instead of localuseState. Answer logic (togglePendingUserInputOptionSelection,setPendingUserInputCustomAnswer) is unchanged — the store just takes an updater.On returning to the thread,
ChatComposer's existing hydration effect restores the editor text fromactivePendingProgress.customAnswer, so typed text, selected options, and multi-question position all come back. Drafts now also survive a reload.Testing
tsgo --noEmitonapps/web— cleanvp linton the touched files — cleanvp test run --project unit— 8 new store tests plus existingpendingUserInput,ChatView.logic,AppRootsuites (52 tests) passAlso verified manually live in the app against a real pending question.
Note
Preserve unsent user-input draft answers across thread switches in ChatView
ChatViewContentwith a new persisted Zustand store (pendingUserInputDraftStore) that survives component unmounts such as thread switches.requestId, so each pending input request retains its own independent state.respondToUserInputsubmission and persist tolocalStorageacross sessions.__proto__.localStorageand survive page reloads, where previously they were lost on any component unmount.Macroscope summarized 510f62d.
Note
Low Risk
Client-side UX/state persistence only; no auth or server contract changes beyond when drafts are cleared after submit.
Overview
Fixes lost in-progress answers when switching threads by moving pending user-input draft state out of
ChatViewinto a new persisted Zustand store (pendingUserInputDraftStore), keyed byrequestIdinlocalStorage.ChatViewnow reads/writes draft answers and the active question index via store hooks and updaters; submission logic is unchanged. Drafts are cleared only after a successful respond so failed submits can be retried. The store caps retention at 50 requests, normalizes question indexes, and uses safe record updates so odd question ids (e.g.__proto__) serialize correctly.Reviewed by Cursor Bugbot for commit 510f62d. Bugbot is set up for automated code reviews on this repo. Configure here.