From e27209645964325fb33f4f1c9aace6684e3e1503 Mon Sep 17 00:00:00 2001 From: sarah <129242944+sarah-inkeep@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:18:18 -0700 Subject: [PATCH] =?UTF-8?q?feat(open-knowledge):=20ask=20why=20on=20uninst?= =?UTF-8?q?all=20=E2=80=94=20churn=20feedback=20survey=20(desktop=20+=20CL?= =?UTF-8?q?I)=20(#2849)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [US-001] add shared uninstall feedback reasons taxonomy to core The desktop uninstall window and the `ok uninstall` CLI prompt both need the same "why are you leaving?" reason list. Declaring it once in core keeps the two surfaces from drifting. The slugs are the contract: they travel to /api/feedback inside the opaque `reasons` array and are what churn tickets get grouped by, so the test pins the exact set and order. Labels are display-only plain English (neither consumer is Lingui-wired) and stay free to reword. * [US-002] add plain-Node uninstall feedback submit helper to core * [US-003] add filterable uninstall Linear label to feedback intake Uninstall feedback tickets now carry LINEAR_UNINSTALL_FEEDBACK_LABEL_IDS on top of the shared LINEAR_FEEDBACK_LABEL_IDS, so churn feedback is filterable in Linear on its own rather than by title search. General feedback is unchanged, and the new var is optional so the code can ship ahead of creating the label and setting its id in Vercel env. The union is a Set so an operator who lists the shared id in both vars cannot make issueCreate receive the same label id twice, and parseLabelIds already trims and drops empties so no empty-string id is ever sent. The /api/feedback request schema is untouched. Covers the three label branches in route.test.ts against the real issueCreate GraphQL input, plus the both-unset case (no labelIds key at all). * [US-004] add desktop uninstall feedback window builder and result parser Adds buildDesktopUninstallFeedbackHtml() and parseDesktopUninstallFeedbackUrl() to desktop-uninstall.ts: the optional churn survey shown between the confirm surface and cleanup, in the same inline-HTML native-dialog vocabulary as the sibling picker and notice windows. The screen renders the shared 7-reason taxonomy as a single-select radio group plus an optional note and an opt-in email field. It has no Escape handler and no backdrop, so the only exits are the two buttons or a deliberate window close. Answers return to main on the existing ok-desktop-uninstall: scheme under feedback-skip / feedback-send hosts. Core gains isUninstallFeedbackReason() so the parser can re-narrow the slug that arrives back as an arbitrary string; an off-taxonomy slug is dropped while the note and email survive, and both are clamped to the intake's field limits so an oversized answer cannot reject the whole ticket. The send button submits the form rather than handling a click, letting the browser's own email validation run first; the email input stays disabled until the opt-in is checked, since a hidden-but-validatable field makes the browser refuse the submit with nothing visible to fix. * [US-005] show the uninstall feedback screen between confirm and cleanup Extracts the pre-cleanup flow decisions into the Electron-free desktop-uninstall module so they are unit-testable: confirmDesktopUninstall runs the project picker or the plain confirm notice, then awaits the feedback step after either branch before reporting proceed. runDesktopUninstallFeedbackStep skips the POST when the user left nothing, awaits the bounded submit so it flushes before app.quit tears the process down, and reports failures as an outcome instead of throwing. Main wires a feedback window shaped like the project picker, except closing it or failing to load resolves an empty answer set and continues the uninstall. Reusing the picker's close-means-cancel mapping would have turned an optional question into a second, hidden cancel gate for a decision the user already made. * [US-006] add the interactive uninstall feedback prompt to the CLI After the existing destructive confirm and before removal, `ok uninstall` now offers the shared 7-reason churn survey plus an optional note and an optional follow-up email, then POSTs it best-effort with source 'cli_uninstall'. The prompt fires only when stdin and stdout are both terminals and neither --yes nor --json is set. Gating on stdout alone would still prompt a caller that piped only its input, and the desktop cleanup script shells out `ok uninstall --yes`, where a prompt would hang a detached process. Nothing in the survey can abort the removal: the send is awaited so it flushes before the process exits, but an interrupted prompt, a closed stdin, or a failed transport all fall through to the removal. @inquirer/input is not a dependency here, so the note and email prompts use node:readline/promises the way confirmDestructive already does. All survey prompts render on stderr so stdout stays the removal report. * [ship] add changeset for uninstall feedback form * docs(open-knowledge): uninstall feedback form spec + evidence * feat(open-knowledge): ask uninstall feedback after removal, only on success Reorder desktop + CLI so feedback is asked after a successful removal (not before): only surveys users whose uninstall completed, and removes the answer-before-doing friction. Desktop decision logic in the new runDesktopUninstallOutcomeStep; CLI gated on outcome.failed.length === 0. D13/D14/FR3 unchanged. (spec D15) * feat(open-knowledge): add email example placeholder to uninstall feedback Format-hint parity with the in-app feedback form's example, while keeping the uninstall form's real