Skip to content

Persist in-progress question answers per question#3126

Draft
jonathanlab wants to merge 2 commits into
mainfrom
posthog-code/persist-question-answer-draft
Draft

Persist in-progress question answers per question#3126
jonathanlab wants to merge 2 commits into
mainfrom
posthog-code/persist-question-answer-draft

Conversation

@jonathanlab

Copy link
Copy Markdown
Contributor

Problem

If you start typing an answer to a question the agent asked and then switch to another session (e.g. to copy something), the typed text is erased. The answer box kept its state only in local React state, so unmounting the view lost it.

Changes

  • Add a per-question draft store keyed by the question's tool-call id (not per session): active step, selected options, and free-text input are saved on every edit and restored on remount.
  • The draft is cleared when the question is submitted or cancelled.
  • The question selector is now keyed per question, so a new question no longer inherits the previous one's state.

How did you test this?

  • Added questionDraftStore.test.ts (store isolation, clearing, persistence) and QuestionPermission.test.tsx (type → unmount → remount restores the text; no leak across question ids; draft cleared on answer).
  • pnpm --filter @posthog/ui test for the permissions + action-selector suites, pnpm --filter @posthog/ui typecheck, and Biome lint/format — all pass.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Created with PostHog from a Slack thread

When the agent asks a question, the answer box kept its state only in local component state, so switching to another session (unmounting the view) erased anything half-typed.

Add a per-question draft store keyed by the question's tool-call id: the active step, selected options, and free-text input are saved on every edit and restored on remount. The draft is cleared when the question is submitted or cancelled, and the selector is now keyed per question so a new question no longer inherits the previous one's state.

Generated-By: PostHog Code
Task-Id: 624a18e6-9eac-4d18-8bd4-62491e117547
@trunk-io

trunk-io Bot commented Jul 3, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

React Doctor found 1 issue in 1 file · 1 warning.

1 warning

src/features/permissions/QuestionPermission.tsx

Reviewed by React Doctor for commit 7ff232c.

@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "Persist in-progress question answers per..." | Re-trigger Greptile

Comment on lines +214 to +225
const draftReportMountedRef = useRef(false);
useEffect(() => {
if (!draftReportMountedRef.current) {
draftReportMountedRef.current = true;
return;
}
onDraftChangeRef.current?.(
internalStep,
Array.from(checkedOptions),
customInput,
);
}, [checkedOptions, customInput, internalStep]);

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.

P2 Stale step data written to draft during step navigation

When internalStep changes (step navigation), this effect fires with the updated internalStep but still-current checkedOptions and customInput from the previous step, because React effects share the state snapshot of the render in which they run. The restoreStepAnswer effect that resets those values runs in the same commit phase, so the parent receives onDraftChange(newStep, oldOptions, oldInput) before the correction fires. handleDraftChange in the parent temporarily overwrites the saved answer for newStep with the previous step's selections, and the setDraft effect persists that to storage. A second render corrects it, but there is a window where the persisted draft has incorrect step data.

Comment on lines +127 to +132
useEffect(() => {
setDraft(questionId, {
activeStep,
stepAnswers: Object.fromEntries(stepAnswers),
});
}, [questionId, activeStep, stepAnswers, setDraft]);

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.

P2 Draft entries accumulate for questions that are never resolved

This useEffect runs on mount and writes { activeStep: 0, stepAnswers: {} } even when the user has not interacted at all. clearDraft is only called from resolveSelect and handleCancel, so if the agent removes the question (component unmounts without either firing), the entry lingers in Electron storage permanently. Over many conversations each with multiple questions, this grows without bound. A TTL or a cleanup pass keyed to session/conversation lifecycle would prevent unbounded accumulation.

Rule Used: When using global state, consider the trade-offs i... (source)

Learned From
PostHog/posthog#32692

Comment on lines +9 to +23

function questionToolCall(toolCallId: string): PermissionToolCall {
return {
toolCallId,
title: "Question",
_meta: {
codeToolKind: "question",
questions: [
{
question: "What should the button say?",
header: "Button label",
options: [{ label: "Submit" }, { label: "Continue" }],
},
],
},

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.

P2 Test mock omits required interface fields, relying on a wide cast

as unknown as PermissionToolCall sidesteps TypeScript's type-checking. If PermissionToolCall gains new required fields in the future, the test will silently compile while potentially failing at runtime in unexpected ways. Including all required properties in the literal (or in a shared makePermissionToolCall factory that the existing test suite already has, if one exists) makes the mock self-documenting and catches interface drift at compile time.

Rule Used: When creating mock objects for tests, include all ... (source)

Learned From
PostHog/posthog#32521

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

- Report the current-step draft imperatively from the edit handlers (typing/toggling) instead of a state effect. The effect keyed on internalStep could fire with the previous step's selections before restoreStepAnswer corrected them, briefly persisting wrong step data.
- Only persist a draft while it holds real content, and clear it otherwise, so untouched questions (or ones the agent removes without a submit/cancel) don't leave empty entries accumulating in storage.
- Tighten the test tool-call mock to `satisfies PermissionToolCall` and add a test that an untouched question leaves no draft.

Generated-By: PostHog Code
Task-Id: 624a18e6-9eac-4d18-8bd4-62491e117547
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant