Repository navigation
Paywall: let an out-of-runs user write their prompt first - #127
Merged
Merged
Conversation
…keys land Autocomplete is out until it is re-scoped in its own milestone. Typing no longer produces a ghost completion: the 13 local patterns, the suggestion session, the debounce, Tab-to-accept, Escape-to-dismiss, the Tab hint button and the polite live region are all removed. Tab and Escape keep their native behaviour. The ghost layer stays because it positions the demo caret, and the input handler now mirrors into it directly (the suggestion helpers used to do that). The orange demo caret now behaves like a real text cursor: is-demo-keying holds it solid on every character the hero demo types or erases, and on every character a chip types. It blinks only while the field is idle: the hold before typing and the rest on the full example. Before, it blinked through the backspace, so it was invisible in about half the frames. Tests: suggestion unit and e2e specs replaced by "typing makes no suggestion" and "Tab moves focus and never changes the prompt". The hero spec now asserts the caret's computed state on every sampled frame (blinking at the hold and the rest; animation none and opacity 1 on each typed or erased character). New spec: a chip fills behind a solid caret.
The "Free generations used" screen used to replace the composer the moment the page loaded for anyone already at their limit: updateUsageDisplay() surfaced it on every usage update (startup identity, subscription refresh), so the user never got to write a prompt. Now the paywall only answers a generation attempt. updateUsageDisplay() only clears it once the limit no longer applies (upgrade, sign-in, new month); canRunPipeline() shows it when a prompt is submitted at the limit, as before. That submitted prompt now fills the "Your prompt" recap beside the upgrade path, and becomes pipelineState.submittedPrompt, so Edit prompt returns to it instead of an earlier run's prompt. hasActiveModal() only existed to defer the auto-surfaced paywall while a dialog was open, so it is removed. Tests: stu391's startup spec now asserts that the exhausted startup leaves the user on the composer (it previously asserted the paywall appeared on reload), and that a run attempt shows the prompt in the recap. New stu382 spec covers the full flow: land on the composer, the paywall appears on submit with the prompt and pricing open, no pipeline request is sent, and Edit prompt returns to the prompt.
…ection Two Devin Review findings on #127: - A usage update that dropped the count below the limit while the paywall was up hid the paywall but left the generation stage owning Home, so the user faced an empty panel instead of the composer. updateUsageDisplay() now also restores the composer when the paywall it hides was the live panel — covering both the count correction and the unresolved- subscription paths, and leaving progress/results panels alone. - Edit prompt hid the stage but left the paywall live inside it and the main-stage latch set, so a Plans trip and back reopened the paywall with no new submit. Editing now dismisses the paywall too, and switchView only restores the stage while a panel inside it is still live (progress, results, paywall or ready state) via generationStageHasLivePanel(). Two new stu382 specs cover both: a held identity response correcting the count mid-paywall hands the composer back with the draft, and a Plans round-trip after Edit prompt keeps Home on the composer. Both fail on the pre-change app.js. Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
Applies the animation-skill timing model (GSAP timelines, Remotion's typewriter): every frame is derived from elapsed time, built by string slicing, rather than advanced one step per timer callback. Chained timers stretch the animation when the main thread is busy. That is what made the chip-fill spec time out under parallel test load, and it would drag the demo on a slow phone. Now a late tick lands everything already due: frames drop, the timing holds. - heroDemoFrame(elapsed, length) is the demo as a pure timeline: hold, then type at 73ms/char, rest 1.6s, then erase at 35ms/char. For any moment it returns the characters showing, whether the caret is keying (solid) or idle (blinking), whether the demo is done, and the ms until the next change. The runtime renders the frame due now and sleeps until the next one. - The chip fill reads its character count off the clock the same way. Timing is unchanged when nothing runs late. The timeline is now unit tested without a browser: exact phase boundaries, a late tick catching up, and a 1ms sweep showing the count rises and falls by exactly one character at a time.
Under parallel local runs these failed intermittently. Baseline on #126's tree, before this change: 5/20 in one --repeat-each batch. - stu445 morph: a fixed 1250ms sleep read the reveal before a slow morph had handed off. It now polls for the reveal, and waits for the 1400ms motion sample to exist before reading it. Same expected values. - stu389 field: counted frames over 500ms against a >5 bar, and read 5 at about 10fps. It now counts over 1500ms with the same bar. - stu390 logo loop: two re-entry checks compared counts from separate windows (framesB < framesA * c). The file's own comment says this trips when a slow runner stalls the baseline. They now use the file's existing framesPerDisplayFrame same-window ratio (< 1.6x), which still catches a doubled loop. After: 60/60 and 135/135 across repeat batches, and 3 consecutive clean full runs (265/265).
…after-prompt # Conflicts: # dist/assets/index-DGpKq1Hs.js # dist/index.html
…Plans Submitting a prompt at the limit used to open the pricing dialog on top of the "Free generations used" screen. Now it shows only the screen, and its View Plans button opens pricing when the user chooses. The openModal option had no other caller, so it is removed. Specs: stu382 asserts pricing stays closed on submit and opens from View Plans. stu391 asserts the run attempt leaves pricing closed. Mutation-checked: re-opening pricing on submit fails both.
Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
Co-Authored-By: Stuart Gardoll <sgardoll@gmail.com>
…ix/paywall-after-prompt
The three flaky specs asserted absolute frame counts in fixed wall-clock windows. A count is frames per second, so those floors measured how busy the runner was, not whether the animation worked: at 12 workers they failed 5 times per 128 runs with the loops running perfectly. Measured first. A running logo loop paints exactly one frame per display frame (3-11 frames under load, always equal); a stopped one paints none while display frames run 3-36. So the honest claim is expressed against display frames from the same window, in one shared helper: - stu390: replace every `frames > 5` floor with expectRunning(), which asserts the loop painted (logo > 0), the window contained real display frames, and painted at most once per display frame. steadyFramesIn and its fixed 250ms warm-up go with it. 256/256 at 12 workers, from 5 failures per 128. - stu389: the field is the only rAF driver on that page (framesOff is asserted to be exactly 0), so the count is the field's own. The >5 floor only added "the runner does 4fps"; rendering at all, then stopping to 0, is the claim. - stu445: six geometry tests run in parallel and each drives a whole gated pipeline, ~5.3s of authored-pacing sleeps plus ten geometry reads, so they exceeded the 30s default on the sleeps rather than on a mismatch. Give them a 60s budget. Also rebuilt the morph spec to record transient state from inside the page instead of racing the 1120ms morph from outside. open() now captures the ghost, the hidden destination and the reveal as they happen, and the handoff is recorded the frame the ghost leaves rather than at a fixed 1400ms. Checked from outside, a loaded machine finishes the morph before the check runs. Verified by mutation: deleting the data-morphing CSS rule fails the "destination hidden during the morph" assertion, and removing both clears of data-morphing fails the handoff assertion. Restoring the never-painting loop mutation fails expectRunning. Full suite 268/268 twice, 413 unit.
…ix/paywall-after-prompt # Conflicts: # dist/assets/index-FNrAL4lj.js # dist/index.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
For a user who had already used all their generations, the page opened straight onto the "Free generations used" screen with an empty Your prompt panel. They never got to the composer.
Cause:
updateUsageDisplay()surfaced the paywall on every usage update (the startup identity check, subscription refreshes). Load → identity resolves withusage_count ≥ limit→ paywall.Fix
updateUsageDisplay()no longer shows it. It only clears it once the limit no longer applies: an upgrade, a sign-in, or a new month.canRunPipeline()still shows it when a prompt is submitted at the limit, unchanged.pipelineState.submittedPrompt. Before, "Edit prompt" could restore an earlier run's prompt over the one just written.hasActiveModal()is removed. It only existed to delay the auto-shown paywall while a dialog was open.New flow for an out-of-runs user:
Tests
npm test: 409/409New
stu382spec covers the full flow:stu391's startup spec previously asserted that the paywall appears on reload. It now asserts the opposite, plus the prompt recap after a run attempt. The reload step clears the stored usage count first, so the test checks the reloaded page's own result rather than the value left from before.Mutation checks, each reverted after the run:
Full
npx playwright test: 264–265 pass per run. 1–2 failures per run move between three load-sensitive timing specs:stu376chip-fill valuestu389frame countstu445morph revealThese specs never reach the paywall path: they use the default guest with usage 0, where the old and new code do the same thing. They also flake on Remove composer autocomplete; hold the demo caret solid while keys land #126's tree without this change: 5/20 in a
--repeat-each 10batch. In isolation the chip spec passes 40/40. CI runs one worker with retries, which is why Remove composer autocomplete; hold the demo caret solid while keys land #126's CI stayed green.