Skip to content

fix(app): restore free-tier usage limit dialog, timeline line, and prompt recall - #57466

Merged
kitlangton merged 1 commit into
devfrom
free-limit-desktop
Oct 20, 2026
Merged

kitlangton merged 1 commit into
devfrom
free-limit-desktop

Conversation

@kitlangton

@kitlangton kitlangton commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Stack (merge in order):

  1. feat(core): typed free-limit error with reset time #57146 — feat(core): typed free-limit error with reset time (base dev)
  2. fix(app): restore free-tier usage limit dialog, timeline line, and prompt recall #57466 — fix(app): restore free-tier usage limit dialog, timeline line, and prompt recall (this PR, base dev)
  3. feat(tui): show free-limit notice, recall prompt, and skip failure toast #57467 — feat(tui): show free-limit notice, recall prompt, and skip failure toast (base feat(core): typed free-limit error with reset time #57146)
  4. feat(tui): restore animated Go upsell modal on free usage limit #57471 — feat(tui): restore animated Go upsell modal on free usage limit (base feat(tui): show free-limit notice, recall prompt, and skip failure toast #57467)

▶ Pitch (phone, 69 s):

pr-57146-phone.mp4

Desktop: https://github.com/user-attachments/assets/8ddef02d-df27-4829-a6bb-743c475ad23d

**Size: +445/−46 (10 files)

Closes #50996
Related: #52894

Today

Desktop's existing DialogUsageExceeded (#27677) still listens for v1's session.status with reason: "free_tier_limit", which v2 never emits. When a free user hits the daily limit, Desktop shows a red "Rate limit exceeded. Please try again later." error card in the timeline, never opens the "Free limit reached" dialog, and leaves the composer empty.

After

  • Dialog: useUsageExceededDialogs listens for session.step.failed with error.type === "provider.free-limit" once persisted() preferences have hydrated, opening DialogUsageExceeded at most once per 24 hours when "Don't show again" isn't set and the composer draft is empty. Includes the local reset time anchored to the failure's timestamp and passes the active composer's model selection to DialogConnectProvider so Subscribe → Go sign-in → Continue switches the model in the current session instead of opening a new draft tab.
  • Timeline row: renders "You've used today's free messages. They reset at 5:00 PM." in muted text instead of a red error card.
  • Prompt restore: recalls the delivered user message for the failed step once, only when the entire composer draft (text, attachments/images, and context items) is empty.
Today (dev) After (#57466)
Desktop dark desktop-before-dark desktop-after-dark
Desktop light desktop-before-light desktop-after-light

How

  • packages/session-ui/src/timeline/projection.ts, timeline-row.ts, & session-timeline-row.tsx: project provider.free-limit into an error row with kind: "free-limit" and validated resetAt timestamp, rendered as muted inline copy.
  • packages/app/src/session/usage-exceeded-dialogs.tsx & packages/app/src/session/screen.tsx: gate DialogUsageExceeded on goUpsellReady(), format the reset sentence anchored to evt.created, and thread the composer's selection and onDone focus callback into DialogConnectProvider.
  • packages/app/src/session/composer/region.tsx & packages/app/src/session/revert.ts: recall the failed assistant turn's delivered user input once when the full composer draft is empty and data.session.input.ready(sessionID) is true.

Proof

  • packages/session-ui/src/timeline/rows-current.test.ts: verifies provider.free-limit projects kind: "free-limit" and resetAt, omitting out-of-range delays.
  • packages/app/e2e/regression/composer.spec.ts: with a frozen clock, verifies Free limit reached opens with the reset time, renders the muted timeline line, restores the prompt, switches the model in the current session after Subscribe → Go sign-in → Continue without opening a new draft tab, respects Don't show again after >24h and reload, and preserves an attachment-only draft.

Risk

Low. Scoped to provider.free-limit events on Desktop; paid quota and rate-limit errors keep their existing error cards and dialogs.


Added by the opencode bot (AI-generated).

Related: #52894

@kitlangton
kitlangton marked this pull request as ready for review October 20, 2026 23:23
@kitlangton
kitlangton merged commit 5c3891e into dev Oct 20, 2026
22 checks passed
@kitlangton
kitlangton deleted the free-limit-desktop branch October 20, 2026 23:24

@opencode-agent opencode-agent 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.

I couldn't run the new e2e test: Playwright's Chromium won't start in this sandbox, and a real free-limit response needs Zen. So I reviewed the code by reading it. The session-ui timeline test passes, and typecheck is clean for session-ui and app. The prompt recall matches the TUI version from #57467. Paid quota and rate-limit errors still take their existing paths, so the change looks sound.

Public API: unchanged. The only additions are internal UI props (kind/resetAt on the timeline Error row, selection/onDone on useUsageExceededDialogs) and new i18n keys.

Main concern: the new e2e test (inline comment). It adds a typecheck:e2e error. It's also one long scenario that's hard to follow, and its last "no dialog" check passes straight away.

Related: #52894


test.use({ permissions: ["clipboard-read", "clipboard-write"] })

test("the free usage limit opens the Go dialog once, leaves a readable notice, and restores the prompt", async ({

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.

This adds a typecheck:e2e error. default: { build: "opencode/big-pickle" } (line 44) doesn't match MockProviderCatalog's default, which expects { providerID, modelID } (TS2322).

The test is also one ~170-line scenario that checks seven behaviours: the dialog, the timeline line, recall, the Go sign-in model switch, the 24h window, "Don't show again" across a reload, and the attachment-only draft. It uses frozen clocks and a reload along the way, so when it fails it will be hard to tell which part broke.

Also, the final expect(page.getByRole("dialog")).toHaveCount(0) passes as soon as the event is pushed, so it doesn't really show that "Don't show again" suppressed anything.

Could you cut this down to the core case (failure → muted line, dialog with reset time, prompt restored)? The Go-switch and "Don't show again" flows could go in small separate tests, or be dropped.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rate limit exceeded. Please try again later.

1 participant