Skip to content

Migrate TUI to Bubble Tea v2 - #222

Merged
anandh8x merged 3 commits into
mainfrom
chore/bubbletea-v2-migration
Jun 16, 2026
Merged

Migrate TUI to Bubble Tea v2#222
anandh8x merged 3 commits into
mainfrom
chore/bubbletea-v2-migration

Conversation

@anandh8x

@anandh8x anandh8x commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • migrate the TUI stack to Bubble Tea/Bubbles/Lip Gloss v2
  • adapt key, paste, mouse, and view handling to the v2 APIs
  • refresh Go module versions and tidy the dependency graph
  • make the PDF truncation fixture deterministic across poppler and pure-Go extraction

Tests

  • GOCACHE=/tmp/zero-go-cache GOMODCACHE=/tmp/zero-go-mod-cache go test ./internal/tui
  • GOCACHE=/tmp/zero-go-cache GOMODCACHE=/tmp/zero-go-mod-cache go test ./internal/imageinput -count=1 -v
  • GOCACHE=/tmp/zero-go-cache GOMODCACHE=/tmp/zero-go-mod-cache go test ./...

Summary by CodeRabbit

Release Notes

  • Dependencies

    • Updated Go to 1.25.0.
    • Updated TUI libraries to newer v2 module paths.
  • Bug Fixes

    • Hardened OAuth/device-login so stale results no longer affect the active attempt.
    • Improved mouse/overlay hit-testing and chat scrolling behavior (including scroll clamping/pinning).
    • Throttled high-frequency mouse input to keep interactions responsive.
  • Refactor & Tests

    • Standardized TUI input handling via shared key/mouse helpers and updated/strengthened related tests.

Move the TUI stack to charm.land Bubble Tea, Bubbles, and Lip Gloss v2, including key, paste, mouse, and view handling changes for the v2 APIs.

Add compatibility helpers for the new input event shapes and update TUI tests accordingly.

Refresh related Go dependencies and make the PDF truncation fixture deterministic across poppler and pure-Go extraction.

Tested: GOCACHE=/tmp/zero-go-cache GOMODCACHE=/tmp/zero-go-mod-cache go test ./internal/tui

Tested: GOCACHE=/tmp/zero-go-cache GOMODCACHE=/tmp/zero-go-mod-cache go test ./internal/imageinput -count=1 -v

Tested: GOCACHE=/tmp/zero-go-cache GOMODCACHE=/tmp/zero-go-mod-cache go test ./...
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cd6245f5-e846-4d9d-95ed-e6effa3cea77

📥 Commits

Reviewing files that changed from the base of the PR and between 7bd975f and 3967216.

📒 Files selected for processing (6)
  • internal/tui/model.go
  • internal/tui/mouse_filter.go
  • internal/tui/mouse_filter_test.go
  • internal/tui/run.go
  • internal/tui/scroll_pin_test.go
  • internal/tui/scroll_test.go
✅ Files skipped from review due to trivial changes (2)
  • internal/tui/mouse_filter.go
  • internal/tui/scroll_pin_test.go

Walkthrough

Migrates the TUI package to charm.land/*/v2 modules, adds shared input-compatibility helpers, refactors production key/mouse handling and view construction, rewires TUI tests to helper-built events, and updates the minimal PDF test fixture to generate safer, variable-height content.

Changes

TUI v2 Migration and Input Refactor

Layer / File(s) Summary
Dependencies and input helpers
go.mod, internal/tui/input_compat.go, internal/tui/input_compat_test.go, internal/tui/mouse_filter.go, internal/tui/mouse_filter_test.go, internal/tui/run.go
go.mod switches to Go 1.25.0 and charm.land/*/v2 dependencies; shared key/mouse helpers and mouse throttling helpers are added; Run installs the mouse-event filter.
Runtime model, mouse, and rendering
internal/tui/model.go, internal/tui/mouse.go, internal/tui/rendering.go, internal/tui/rendering_lime_test.go, internal/tui/startup.go, internal/tui/theme.go, internal/tui/view.go, internal/tui/selectable_list.go, internal/tui/pr_status.go, internal/tui/assistant_markdown.go, internal/tui/options.go, internal/tui/command_bash.go, internal/tui/command_center.go, internal/tui/command_views.go, internal/tui/flush.go, internal/tui/spec_mode.go, internal/tui/session_controls.go, internal/tui/picker.go
The main TUI model now uses v2 components, View() returns tea.View, mouse capture is initialized through the model, mouse hit-testing uses helper coordinates, and rendering/input sizing paths use the newer view and width APIs.
Composer, onboarding, provider, MCP, and transcript flows
internal/tui/composer.go, internal/tui/onboarding.go, internal/tui/provider_wizard.go, internal/tui/provider_wizard_discovery.go, internal/tui/provider_wizard_oauth_test.go, internal/tui/mcp_add_wizard.go, internal/tui/mcp_manager.go, internal/tui/transcript_selection.go
Composer, onboarding, provider wizard, MCP wizard/manager, and transcript selection handling switch from raw msg.Type checks to helper predicates, and provider OAuth/device flows gain attempt-aware message matching.
TUI test rewiring and behavior coverage
internal/tui/*_test.go
The TUI tests switch to charm.land imports, helper-built key/mouse messages, viewString(...)/plainRender(...) assertions, and updated onboarding/session/picker/composer/transcript coverage.
Minimal PDF generator
internal/imageinput/pdf_test.go
The PDF fixture now chunks and escapes text safely, computes page height from content, and expands the truncation test payload with more context in the failure message.

Sequence Diagram(s)

sequenceDiagram
  participant ProviderWizard
  participant providerWizardDiscovery
  participant providerWizardOAuthCmdFor
  participant providerWizardDevicePrepareCmd
  participant providerWizardDevicePollCmd
  participant applyProviderWizardOAuth
  ProviderWizard->>providerWizardDiscovery: beginOAuthAttempt(false)
  providerWizardDiscovery->>providerWizardOAuthCmdFor: pass attemptID
  providerWizardOAuthCmdFor->>providerWizardDevicePrepareCmd: start device flow with attemptID
  providerWizardDevicePrepareCmd->>providerWizardDevicePollCmd: poll with attemptID
  providerWizardDevicePollCmd->>applyProviderWizardOAuth: deliver providerID and attemptID
  applyProviderWizardOAuth->>ProviderWizard: accept only matching attempt
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Gitlawb/zero#163: Updates the same first-run onboarding flow in internal/tui/onboarding.go with helper-based key handling and rendering changes.
  • Gitlawb/zero#161: Touches the main internal/tui/model.go input/rendering path and shared TUI helper plumbing that this PR also changes.
  • Gitlawb/zero#168: Refactors internal/tui/composer.go key handling in the same area that this PR rewires to helper predicates.

Suggested reviewers

  • Vasanthdev2004
  • gnanam1990
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/bubbletea-v2-migration

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/tui/input_compat.go`:
- Around line 61-63: The mouseButton function is unused and triggers a
golangci-lint error. Either remove the mouseButton function entirely if it is
not needed, or identify where it should be called and wire it into the codebase
to use it. If the function is intended for future use or public API purposes,
consider adding a comment explaining its purpose to suppress the lint warning,
or ensure it is actually being utilized by callers.

In `@internal/tui/provider_wizard.go`:
- Around line 586-591: The OAuth async login initiated at line 590 lacks an
attempt identifier, allowing stale results from previous wizard attempts to be
incorrectly applied to new ones. Generate and store a unique attempt ID in the
wizard state when starting the OAuth flow (when providerWizardSupportsOAuth is
true), then pass this attempt ID to providerWizardOAuthCmdFor. Update the
applyProviderWizardOAuth and applyProviderWizardDeviceCode functions to validate
three conditions before applying results: wizard.oauthPending is true, the
providerID matches the currentProvider, and the attemptID matches the stored
attempt ID. This ensures async results are only applied to the correct wizard
attempt they originated from.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f821cd23-f20c-4e3d-bb95-5f186836cfee

📥 Commits

Reviewing files that changed from the base of the PR and between a0b9876 and 22793bc.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (55)
  • go.mod
  • internal/imageinput/pdf_test.go
  • internal/tui/ask_user_test.go
  • internal/tui/assistant_markdown.go
  • internal/tui/autocomplete_test.go
  • internal/tui/command_bash.go
  • internal/tui/command_center.go
  • internal/tui/command_polish_test.go
  • internal/tui/command_views.go
  • internal/tui/commands_test.go
  • internal/tui/composer.go
  • internal/tui/composer_test.go
  • internal/tui/doctor_command_test.go
  • internal/tui/flush.go
  • internal/tui/flush_test.go
  • internal/tui/input_compat.go
  • internal/tui/input_compat_test.go
  • internal/tui/mcp_add_wizard.go
  • internal/tui/mcp_add_wizard_view.go
  • internal/tui/mcp_manager.go
  • internal/tui/mcp_view_test.go
  • internal/tui/model.go
  • internal/tui/model_test.go
  • internal/tui/mouse.go
  • internal/tui/mouse_test.go
  • internal/tui/onboarding.go
  • internal/tui/onboarding_test.go
  • internal/tui/options.go
  • internal/tui/picker.go
  • internal/tui/picker_test.go
  • internal/tui/pr_status.go
  • internal/tui/provider_onboarding_test.go
  • internal/tui/provider_wizard.go
  • internal/tui/provider_wizard_discovery.go
  • internal/tui/provider_wizard_oauth_test.go
  • internal/tui/provider_wizard_test.go
  • internal/tui/queued_message_test.go
  • internal/tui/rendering.go
  • internal/tui/rendering_lime_test.go
  • internal/tui/run.go
  • internal/tui/scroll_test.go
  • internal/tui/selectable_list.go
  • internal/tui/selectable_list_test.go
  • internal/tui/session_controls.go
  • internal/tui/session_controls_test.go
  • internal/tui/session_test.go
  • internal/tui/spec_mode.go
  • internal/tui/spec_mode_test.go
  • internal/tui/startup.go
  • internal/tui/startup_test.go
  • internal/tui/theme.go
  • internal/tui/transcript_selection.go
  • internal/tui/transcript_view_test.go
  • internal/tui/view.go
  • internal/tui/width_tiers_test.go

Comment thread internal/tui/input_compat.go Outdated
Comment thread internal/tui/provider_wizard.go
Remove the unused mouseButton compatibility helper flagged by lint.

Correlate provider wizard OAuth and device-code async results with the active provider and attempt id so stale results from abandoned attempts are ignored.

Add regression coverage for stale browser OAuth and stale device-code results.

Tested: GOCACHE=/tmp/zero-go-cache GOMODCACHE=/tmp/zero-go-mod-cache go test ./internal/tui -run 'ProviderWizard.*OAuth|ProviderWizard.*Device|ApplyProviderWizardOAuth|RenderCredentialStepShowsOAuth' -count=1 -v

Tested: GOCACHE=/tmp/zero-go-cache GOMODCACHE=/tmp/zero-go-mod-cache go test ./internal/tui

Tested: GOCACHE=/tmp/zero-go-cache GOMODCACHE=/tmp/zero-go-mod-cache go test ./...

Tested: git diff --check
@anandh8x
anandh8x marked this pull request as ready for review June 16, 2026 13:05
@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: 3967216ab33e
Changed files (59): go.mod, go.sum, internal/imageinput/pdf_test.go, internal/tui/ask_user_test.go, internal/tui/assistant_markdown.go, internal/tui/autocomplete_test.go, internal/tui/command_bash.go, internal/tui/command_center.go, internal/tui/command_polish_test.go, internal/tui/command_views.go, internal/tui/commands_test.go, internal/tui/composer.go, and 47 more

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@gnanam1990 gnanam1990 mentioned this pull request Jun 16, 2026
@gnanam1990

Copy link
Copy Markdown
Collaborator

Review — Migrate TUI to Bubble Tea v2

Verdict: approach is sound, and both of CodeRabbit's blockers already appear fixed on the branch HEAD — the CHANGES_REQUESTED looks like a stale verdict. Residual risk is inherent to a framework swap, not the diff.

Strengths

  • input_compat.go is the right design — it centralizes the entire v2 input surface (keyCode/keyText/keyAlt/keyCtrl/keyIs, mouseLeftPress/mouseMotion/mouseWheelUp, type-switches on MouseClickMsg/MouseMotionMsg) into helpers, so the rest of the TUI stays readable and the v2 API lives in one file. Exactly how to do a migration of this size.

Both flagged findings look resolved

  • input_compat.go:63 "unused mouseButton" → gone (no mouseButton in the branch anymore).
  • provider_wizard.go:609 "correlate OAuth/device results to the active attempt" → already implemented: the msgs now carry providerID + attemptID, beginOAuthAttempt() issues the token, and applyProviderWizardOAuth/applyProviderWizardDeviceCode gate on oauthResultMatches(providerID, attemptID). (Same stale-result class handled for the onboarding flow in OAuth / account login for providers (OpenRouter, xAI) + subscription-proxy support #217 — good the wizard path has it now too.)

Risks to verify before merge (not blockers)

  • A stop-the-world TUI swap — automated tests can't catch rendering/input regressions. Needs real manual driving (key/paste/mouse, alt-screen scroll, overlays, resize) on macOS + Linux + Windows terminals.
  • charmbracelet/ultraviolet is pinned to a 2026-06-15 pseudo-version (brand new, indirect) — confirm it's intentional and go.sum is clean.
  • Confirm lint + full CI is green (the original mouseButton finding was a lint gate).

Merge order

#217, #220, and this PR all rewrite the same TUI files, and this one changes the v1→v2 input API itself. Recommend landing this migration first (or last with everyone rebasing onto it) — otherwise #220/#217 have to be re-expressed in the v2 helper API after the fact.

@anandh8x
anandh8x merged commit 9c1cc69 into main Jun 16, 2026
7 checks passed

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE

Read at ae17cf0. Walked through the 28 files (885 +, 197 -), focused on the new behavior:

  • in-memory session grants + sessionMu in internal/sandbox/engine.go
  • the new PermissionDecisionAllowForSession path in internal/agent/loop.go:579-587
  • workspaceWriteAutoAllowed + the new apply_patch short-circuit in internal/sandbox/engine.go
  • the TUI fallback in internal/tui/permission_prompt.go:21-53
  • the new TestRunAllowsWorkspaceWriteWithoutPromptWhenSandboxPolicyPermits and the session-grant test in internal/agent/loop_test.go
  • the transcript-row label switch in internal/tui/session_controls.go

The shape of this is good. Session grants have a real concurrency primitive, AvailableDecisions flows from the engine to the TUI cleanly, and the agent loop's "honor the allow for THIS call regardless" comment on lines 591-598 is the right call. persistSessionPermissionGrant returning a sandbox.Grant instead of an opaque bool keeps the event payload honest.

Three things I want to flag for follow-up, none blocking:

1. apply_patch workspace auto-allow bypasses the engine's path-list validation, not just the user prompt.

workspaceWriteAutoAllowed returns true for apply_patch unconditionally, and the engine's path validation iterates requestPaths(request) (in internal/sandbox/risk.go:223) which does not include the patch key. So with EnforceWorkspace: true and an in-workspace patch header, the engine approves the call without checking DenyWrite/AllowWrite. The apply_patch tool's own validatePatchPaths in internal/tools/apply_patch.go only does workspace confinement (no .., no absolute paths, resolveWorkspaceTargetPath). It does not consult the policy.

The OS-level sandbox (bwrap, sandbox-exec, landlock, Windows ACL) still enforces DenyWrite at the kernel level, so this is defense-in-depth, not a hole. But users running in pure permissions mode (no OS sandbox) get a behavior change: a patch that targets a DenyWrite path used to fail when the user reviewed it in the prompt, now passes silently. Worth either threading the policy into the apply_patch tool or calling the trade-off out in the PR body so it isn't a surprise.

2. No test for apply_patch + DenyWrite.

TestRunAllowsWorkspaceWriteWithoutPromptWhenSandboxPolicyPermits only covers an in-workspace write_file against DefaultPolicy() (which has no DenyWrite). A test that an apply_patch targeting a DenyWrite path is denied would close the loop. If the answer is "the apply_patch tool catches it", a test at that layer would be even more useful.

3. TUI permissionOptions fallback offers "always" even with no persistent store.

In internal/tui/permission_prompt.go:21-30, when AvailableDecisions is empty the fallback hardcodes all four decisions. The TUI is only invoked for prompts that always populate AvailableDecisions, so the fallback is unreachable today. But if a future call site passes a non-prompt event to the TUI, the user can hit "y" with no store and silently get only-this-call behavior. Easy fix: derive the fallback from the presence of a store, same as the engine does in availablePermissionDecisions.

Minor: the persistSessionPermissionGrant error path is swallowed with if ... err == nil (loop.go:583), and that's the right call for "don't deny what the user explicitly allowed", but the asymmetry with the AlwaysAllow branch (which builds the permission event from the persisted grant on success) means the session-grant event's GrantMatched flips to false if persistence fails. Worth a code comment so a future reader doesn't think the event is broken.

Non-blocking follow-ups; happy to see this land.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

Heads up — wrong review posted here.

I just posted a CHANGES_REQUESTED-style review on this PR in error. The body talks about session approvals, the in-memory sessionMu, workspaceWriteAutoAllowed, and apply_patch policy bypass — that is the content of PR #244 (permission-session-approvals), not this one (TUI v2 migration). The commit SHA ae17cf0 in the review is on the permission-session-approvals branch, which is not part of #222's history.

PR #222 is already MERGED, so the orphan review doesn't change anything in the codebase. I tried to dismiss it but the dismiss endpoint didn't take (likely because #222 is merged). Treating the review as a no-op; the real review will land on #244.

Sorry for the noise.

@Vasanthdev2004
Vasanthdev2004 deleted the chore/bubbletea-v2-migration branch June 28, 2026 08:27
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.

3 participants