Skip to content

fix(web): preserve unsent user-input answers across thread switches#4592

Open
ipanasenko wants to merge 2 commits into
pingdotgg:mainfrom
ipanasenko:fix/preserve-pending-user-input-draft
Open

fix(web): preserve unsent user-input answers across thread switches#4592
ipanasenko wants to merge 2 commits into
pingdotgg:mainfrom
ipanasenko:fix/preserve-pending-user-input-draft

Conversation

@ipanasenko

@ipanasenko ipanasenko commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

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 ChatView component state — pendingUserInputAnswersByRequestId / pendingUserInputQuestionIndexByRequestId. The router creates a new match per $threadId, so a thread switch unmounts ChatView and throws that state away. Nothing was persisted.

Change

  • New apps/web/src/pendingUserInputDraftStore.ts: a zustand store persisted to localStorage (t3code:pending-user-input-drafts:v1, same pattern as terminalUiStateStore) holding draft answers and the active question index keyed by requestId. Has a 50-request retention cap so abandoned drafts don't accumulate.
  • ChatView reads/writes that store instead of local useState. Answer logic (togglePendingUserInputOptionSelection, setPendingUserInputCustomAnswer) is unchanged — the store just takes an updater.
  • The draft is cleared once the answer submits successfully; a failed submit keeps it so the user can retry.

On returning to the thread, ChatComposer's existing hydration effect restores the editor text from activePendingProgress.customAnswer, so typed text, selected options, and multi-question position all come back. Drafts now also survive a reload.

Testing

  • tsgo --noEmit on apps/web — clean
  • vp lint on the touched files — clean
  • vp test run --project unit — 8 new store tests plus existing pendingUserInput, ChatView.logic, AppRoot suites (52 tests) pass

Also verified manually live in the app against a real pending question.

Note

Preserve unsent user-input draft answers across thread switches in ChatView

  • Replaces component-local state in ChatViewContent with a new persisted Zustand store (pendingUserInputDraftStore) that survives component unmounts such as thread switches.
  • Draft answers and question index are keyed by requestId, so each pending input request retains its own independent state.
  • Drafts are cleared from the store on successful respondToUserInput submission and persist to localStorage across sessions.
  • The store caps retained drafts at 50 entries, evicting the oldest when exceeded, and safely handles special keys like __proto__.
  • Behavioral Change: draft answers now persist to localStorage and 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 ChatView into a new persisted Zustand store (pendingUserInputDraftStore), keyed by requestId in localStorage.

ChatView now 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.

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.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8631f599-99d8-471e-b242-d475a4eade96

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 26, 2026
Comment thread apps/web/src/pendingUserInputDraftStore.ts Outdated
`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.
@ipanasenko
ipanasenko marked this pull request as ready for review July 26, 2026 22:40

@cursor cursor Bot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ 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,
),
};

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.

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.

Fix in Cursor Fix in Web

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);

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.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 510f62d. Configure here.

@macroscopeapp

macroscopeapp Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant