Repository navigation
feat(wizard): surface the agent's pending question in the sync widget - #74324
Conversation
While a local wizard run is blocked on a wizard_ask question, a user who navigated away to the web app sees a widget that just says the run is in progress. If they don't return to the terminal, the ask times out and the run degrades. The wizard CLI now publishes the open question on the session upsert as a nullable pending_input field (PostHog/wizard PR pending). Since the wire is a full-state upsert, dismissal needs no event of its own: the next push without the field clears the state. Backend: nullable pending_input JSONField on WizardSession, carried through the upsert path, the DTO, and the SSE payload unchanged. Frontend: InstallationProgress gains pendingInput, gated on a live non-stale session so a dead wizard's prompt cannot outlive it. The sync card and dialog render the question in the warning tone, with a generic terminal pointer for sensitive asks (their prompt text never leaves the CLI). An open question also overrides the widget's minimized state, and the server clearing the field restores the user's choice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🤖 CI report
|
| File | Size | Δ vs base |
|---|---|---|
render-query/src/render-query/render-query.js |
21.25 MiB | 🟢 -36.3 KiB (-0.2%) |
posthog-app/src/lib/components/Shortcuts/utils/DebugCHQueriesImpl.js |
33.4 KiB | 🔺 +18.2 KiB (+119.1%) |
posthog-app/_parent/products/data_catalog/frontend/DataCatalogMetricScene.js |
16.1 KiB | 🔺 +16.1 KiB (new) |
posthog-app/_parent/products/mcp_analytics/frontend/MCPAnalyticsScene.js |
156.6 KiB | 🔺 +16.1 KiB (+11.4%) |
posthog-app/_parent/products/data_catalog/frontend/DataCatalogScene.js |
11.7 KiB | 🟢 -5.1 KiB (-30.5%) |
exporter/src/exporter/scenes/ExporterQueryScene.js |
3.7 KiB | 🔺 +3.7 KiB (new) |
posthog-app/_parent/products/managed_migrations/frontend/ManagedMigration.js |
27.4 KiB | 🔺 +3.3 KiB (+13.6%) |
posthog-app/src/scenes/insights/InsightScene.js |
45.5 KiB | 🔺 +3.1 KiB (+7.3%) |
posthog-app/_parent/products/workflows/frontend/WorkflowsScene.js |
77.1 KiB | 🔺 +1.2 KiB (+1.6%) |
exporter/src/exporter/scenes/ExporterNotebookScene.js |
3.11 MiB | 🔺 +1.2 KiB (+0.0%) |
Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report
✅ Eager graph — within budget
How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.
| Root | Eager (shipped) | Δ vs base | Budget |
|---|---|---|---|
entry (logged-out pages, app bootstrap)src/index.tsx |
1.24 MiB · 22 files | no change | ███░░░░░░░ 27.6% of 4.51 MiB |
authenticated shell (every logged-in page)src/scenes/AuthenticatedShell.tsx |
8.10 MiB · 3,017 files | 🔺 +9.6 KiB (+0.1%) | ████████░░ 83.4% of 9.71 MiB |
🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
Largest files eagerly shipped from src/index.tsx
| Size | File |
|---|---|
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 24.6 KiB | ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js |
| 6.3 KiB | ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js |
| 4.5 KiB | ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js |
| 3.9 KiB | ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js |
| 1.4 KiB | ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js |
| 1.3 KiB | src/RootErrorBoundary.tsx |
| 912 B | ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js |
| 789 B | src/scenes/ChunkLoadErrorBoundary.tsx |
| 762 B | src/index.tsx |
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
| Size | File |
|---|---|
| 281.6 KiB | ../node_modules/.pnpm/posthog-js@1.407.6/node_modules/posthog-js/dist/rrweb.js |
| 267.7 KiB | ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
| 235.5 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 226.5 KiB | ../node_modules/.pnpm/posthog-js@1.407.6/node_modules/posthog-js/dist/module.js |
| 154.3 KiB | ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js |
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 105.2 KiB | src/lib/api.ts |
| 94.0 KiB | ../packages/quill/packages/quill/dist/index.js |
| 93.3 KiB | ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js |
| 90.6 KiB | ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js |
Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479
✅ Toolbar bundle — eager 2.18 MiB within budget
What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.
| Metric | Size | Δ vs base | Budget |
|---|---|---|---|
| Eager (shipped) entry + static imports |
2.18 MiB · 17 files | 🔺 +55 B (+0.0%) | ████░░░░░░ 38.2% of 5.72 MiB |
| Deferred (lazy) | 2.07 MiB · 33 files | no change | n/a — loads on demand |
Loader dist/toolbar.js |
1.1 KiB | no change | █░░░░░░░░░ 5.8% of 19.5 KiB |
Largest eagerly-shipped chunks
| Size | File |
|---|---|
| 717.2 KiB | dist/toolbar/toolbar-app-JFACJRNE.css |
| 546.0 KiB | dist/toolbar/chunk-chunk-CDYMM3XO.js |
| 484.3 KiB | dist/toolbar/chunk-chunk-CQF6YMXB.js |
| 133.6 KiB | dist/toolbar/chunk-chunk-XSXSV5RW.js |
| 131.8 KiB | dist/toolbar/chunk-chunk-T5KY5WYR.js |
| 71.0 KiB | dist/toolbar/toolbar-app-UEUIIM6N.js |
| 69.0 KiB | dist/toolbar/chunk-chunk-27JL52RE.js |
| 35.6 KiB | dist/toolbar/chunk-chunk-IXOMC3I3.js |
| 20.9 KiB | dist/toolbar/chunk-chunk-ZBTP6T3F.js |
| 12.2 KiB | dist/toolbar/chunk-chunk-PIK3PADE.js |
Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile
✅ Dist folder size — 🔺 +4.72 MiB (+0.3%)
Total size of the built frontend/dist folder (all assets), compared against the base branch.
Total: 1367.87 MiB · 🔺 +4.72 MiB (+0.3%)
⚠️ Django migration SQL — 1 new migration to review
We've detected new migrations on this PR. Review the SQL output for each migration:
products/wizard/backend/migrations/0003_wizardsession_pending_input.py
BEGIN;
--
-- Add field pending_input to wizardsession
--
ALTER TABLE "wizard_wizardsession" ADD COLUMN "pending_input" jsonb NULL;
COMMIT;Last updated: 2026-07-29 14:19 UTC (a3a8070)
✅ Django migration risk — migration analysis complete
We've analyzed your migrations for potential risks.
Summary: 1 Safe | 0 Needs Review | 0 Blocked
✅ Safe
Brief or no lock, backwards compatible
wizard.0003_wizardsession_pending_input
└─ #1 ✅ AddField
Adding nullable field requires brief lock
model: wizardsession, field: pending_input
📚 How to Deploy These Changes Safely
AddField:
This operation acquires a brief lock but doesn't rewrite the table.
Deployment uses lock timeouts with automatic retries, so lock contention will cause retries rather than connection pile-up.
Last updated: 2026-07-29 14:20 UTC (a3a8070)
🦔 Hogbox preview · ❌ build failedThe preview didn't come up for commit Previews are optional and never block merging. A failure here is often a hogland or tailnet hiccup rather than anything in your PR, so the check stays green and this comment is the status. |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
|
|
✅ Visual changes approved by @fercgomes — baseline updated in 2 changed, 2 new. |
|
😎 Merged successfully - details. |
| tasks = models.JSONField(default=list) | ||
| event_plan = models.JSONField(null=True, blank=True) | ||
| error = models.JSONField(null=True, blank=True) | ||
| # An in-flight wizard_ask prompt ({id, asked_at, question_count, sensitive, prompts?}). | ||
| # Null means no input is pending — each push replaces it, so the CLI clearing the | ||
| # question is just the next upsert without the field. | ||
| pending_input = models.JSONField(null=True, blank=True) |
There was a problem hiding this comment.
nit: I wonder if we should enforce some kind of schema to these eventually?
There was a problem hiding this comment.
Worth noting this PR ended up doing exactly that for pending_input, after the security bots flagged the free-form dict. It now goes through a typed PendingInputSerializer that bounds the id, timestamp, question count, and prompt list, and drops prompts server-side when sensitive is set, so a malformed or oversized payload is a 400 rather than something the app-wide widget has to survive rendering. That also means the generated frontend type is a real PendingInputApi instead of an unchecked cast.
tasks, event_plan, and error are still free-form. Leaving them as they are here to keep this PR to the one field, but the pattern is in place if we want to extend it, and error looks like the next obvious candidate since it already has an implied {type, message} shape. Happy to follow up separately if you think it is worth doing.
2 updated Run: a51c28d8-6bf3-439d-88c9-461ea0f08e48 Co-authored-by: fercgomes <28491014+fercgomes@users.noreply.github.com>
pending_input was accepted as a free-form dict, so two things followed from trusting the producer: the sensitive contract was enforced only in the browser, after the prompt text had already been persisted, streamed over SSE, and logged; and a non-string prompt would reach the app-wide widget and throw on render. Give the field a typed serializer that bounds the id, timestamp, count, and prompt list, and drop prompts server-side when sensitive is set, before the row is written. The frontend keeps a runtime string filter, since the generated type is the serializer's promise rather than a guarantee about rows already stored. Also fixes the test_stream helper, which still omitted the now-required pending_input argument.
pending_input is a JSONField, so mypy types it as Any | None and rejected both the membership check and the subscript.
asked_at is validated as a datetime but stored as a string, since the column is JSON. Pin that every read serializes it back out cleanly, so a later change to the field type cannot quietly break latest, retrieve, and list.
The card led with the question text, which reads as information rather than as something to act on. What unblocks the run is knowing the terminal wants you, not what it asked. The prominent line is now that call to action, and the question moves below it in tertiary text. Sensitive asks publish no prompt, so the call to action stands alone. The prompt is marked ph-no-capture, since it is whatever the wizard chose to ask.
4 updated Run: 7d0e6c1e-c2a5-45d0-b655-874956f9e910 Co-authored-by: fercgomes <28491014+fercgomes@users.noreply.github.com>
|
/trunk merge |
1 similar comment
|
/trunk merge |
Problem
While a local wizard run is blocked on a
wizard_askquestion, the sync widget in the web app just says the run is in progress. A user who tabbed away from the terminal has no way to know the agent is waiting on them, and after 5 minutes the ask times out and the run degrades.Closes #74309 (companion CLI PR: PostHog/wizard#1008).
Changes
The wizard CLI now publishes the open question on the session upsert as a nullable
pending_inputfield. Because the wire is a full-state upsert, dismissal needs no event of its own. The next push without the field clears the state, so "attention required" and "attention dismissed" are one mechanism.Backend
WizardSession.pending_inputnullable JSONField (additive migration), carried through the upsert path, DTO, serializer, and SSE payload unchanged.Frontend
InstallationProgressgainspendingInput, extracted bypendingInputFromSessionand gated on a live, non-stale session. A dead wizard never sends the clearing push, so the server'sis_staleverdict is what retires an orphaned prompt.IconQuestion,text-warning): "Your terminal needs your attention". What unblocks the run is knowing the terminal wants you, not what it asked, so the question itself sits below it in tertiary text. Sensitive asks (secrets) publish no prompt text by design, so the call to action stands alone. The prompt is markedph-no-capture, since it is whatever the wizard chose to ask.LocalWaitingForInputandLocalWaitingForSensitiveInputfor the attention state, with and without prompt text.Screenshots
The card blocked on a question. The call to action leads; the question is tertiary. A sensitive ask publishes no prompt, so the call to action stands alone.
How did you test this code?
Automated only, no manual run of the full loop:
products/wizard/backend/tests/test_logic.pyincluding a new round-trip test: pending_input stored on upsert, cleared by the next push without it (24 passed).frontend/src/scenes/onboarding/shared/wizard-sync/Jest suites, with new cases: surfacing on a live session, gating off for stale/terminal/absent rows, prompt withholding for sensitive asks, clear-on-next-push (208 passed).helpers.test.tspins the copy hierarchy: the prominent line is the call to action and never the question text, the question is carried separately so it can render below it, and a sensitive ask has no question to show.test_presentation.pycovers the serializer: sensitive prompts are dropped before the row is written, malformedpending_inputis rejected (non-string prompt, over-long prompt, too many prompts, missing id, out-of-range count), andasked_atsurvives thelatest,retrieve, andlistread paths (77 passed).The new tests catch regressions no existing test did: the staleness gate (a dead wizard's prompt outliving it) and sensitive-prompt redaction.
Automatic notifications
Docs update
Not needed, feature is behind the existing wizard sync flags.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Authored with Claude Code. Skills consulted: django-migrations, improving-drf-endpoints, writing-user-facing-copy, writing-tests conventions. Design decision worth noting: a fifth
run_phasevalue (awaiting_input) was rejected because that enum is load-bearing across the model choices, generated API clients, and every phase branch in the frontend, and a session awaiting input is still running. A nullable side-field only touches consumers that care.🤖 Generated with Claude Code