Skip to content

fix(tui): detect missing TTY and show helpful error instead of Bubbletea crash#905

Open
decode2 wants to merge 1 commit into
Gentleman-Programming:mainfrom
decode2:fix/tty-guard-non-interactive
Open

fix(tui): detect missing TTY and show helpful error instead of Bubbletea crash#905
decode2 wants to merge 1 commit into
Gentleman-Programming:mainfrom
decode2:fix/tty-guard-non-interactive

Conversation

@decode2

@decode2 decode2 commented Jun 16, 2026

Copy link
Copy Markdown

Summary

Detects missing TTY before launching the interactive TUI and shows a helpful error listing available non-interactive commands, instead of crashing with a confusing Bubbletea panic.

Problem

When gentle-ai is launched without a terminal attached (scripts, CI/CD, non-interactive SSH), Bubbletea panics with:

Error: could not open a new TTY: open /dev/tty: device not configured

This gives no guidance on what to do instead.

Fix

  1. Added stdinIsTerminal() helper that reuses the existing isattyFn seam from selfupdate.go for testability.

  2. Added ErrNoTTYForTUI error with a clear message listing non-interactive commands:

    • gentle-ai --version
    • gentle-ai --help
    • gentle-ai update
    • gentle-ai install
    • gentle-ai sync
    • gentle-ai doctor
  3. Inserted the TTY guard in RunArgs immediately before the TUI launch block, after info commands (version, help) are dispatched — so those continue to work without a TTY.

Tests

  • TestRunArgs_NoTTY_ReturnsErrNoTTYForTUI — verifies the error is returned with the expected message
  • TestRunArgs_NoTTY_StillAllowsVersionAndHelp — verifies info commands work without TTY
  • Updated 6 existing TUI-launching tests to mock stdinIsTerminal = func() bool { return true } so the guard doesn't block them

Closes #95

Summary by CodeRabbit

  • Bug Fixes

    • TUI no longer attempts to start in non-terminal environments; displays a helpful message listing available non-TUI commands instead.
  • Tests

    • Added tests to verify proper behavior in non-interactive contexts and ensure version/help commands remain accessible in all environments.

@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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f2c9292e-8064-45dd-b1f3-67d491491363

📥 Commits

Reviewing files that changed from the base of the PR and between 9219528 and 1fb584f.

📒 Files selected for processing (3)
  • internal/app/app.go
  • internal/app/app_test.go
  • internal/app/parity_test.go

📝 Walkthrough

Walkthrough

Adds a stdinIsTerminal helper and exported ErrNoTTYForTUI error to RunArgs in internal/app/app.go. When no args are provided and stdin is not a terminal, RunArgs returns the error instead of launching the TUI. Existing TUI-path tests and the parity test are updated to mock stdinIsTerminal as true; two new tests validate the no-TTY guard and confirm that version/help commands still work without a TTY.

Changes

TTY Guard for TUI Launch

Layer / File(s) Summary
ErrNoTTYForTUI error and stdinIsTerminal guard in RunArgs
internal/app/app.go
Exports ErrNoTTYForTUI with a message listing non-TUI commands, introduces stdinIsTerminal wrapping isattyFn, and inserts a TTY check in RunArgs that returns ErrNoTTYForTUI when stdin is not a terminal and the TUI path would be taken.
New no-TTY tests and existing TUI test fixups
internal/app/app_test.go, internal/app/parity_test.go
Adds TestRunArgs_NoTTY_ReturnsErrNoTTYForTUI and TestRunArgs_NoTTY_StillAllowsVersionAndHelp; updates TestRunArgs_TUISkipsSelfUpdate, TestRunArgs_TUIRestartsAfterGentleAIUpgradeResult, all four deferred-sync tests, and TestRunArgsNoCommandLaunchesTUI to save/restore stdinIsTerminal and force it to true.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

  • #95 — Detect missing TTY and show helpful error instead of Bubbletea crash: This PR directly implements the described scope: detecting !isatty(stdin) before TUI launch, returning a clear error message listing --version and --help as alternatives, and exiting cleanly via the returned ErrNoTTYForTUI.
  • #254: The PR introduces the stdinIsTerminal guard in RunArgs and updates TestRunArgsNoCommandLaunchesTUI to mock the TTY check, which directly addresses preventing hangs in headless/non-TTY test and runtime environments.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: detecting missing TTY and showing a helpful error instead of Bubbletea crash, which is the primary focus of the PR.
Linked Issues check ✅ Passed The PR directly implements all three coding objectives from issue #95: TTY detection using isatty(stdin), an informative error message listing available non-TUI commands, and clean exit behavior.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #95 objectives: TTY detection, error messaging, and clean exit. No unrelated changes to implement --no-interactive mode, separate CLI binary, or environment toggles.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@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: 1

🤖 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/update/upgrade/executor.go`:
- Around line 521-524: The effectiveMethod function is being called multiple
times during a single tool execution—at line 521 for the spinner message, at
line 576 for result metadata, and again in runStrategy within
internal/update/upgrade/strategy.go at line 72—which causes unnecessary process
overhead and can result in inconsistent routing if one probe fails while another
succeeds. Resolve effectiveMethod once at the start of the tool execution
(before line 521), store the result in a variable, and then pass this resolved
method value through the execution chain instead of calling effectiveMethod
again at line 576. Additionally, update the runStrategy function and any related
execution functions to accept the pre-resolved method as a parameter rather than
computing it internally, eliminating the redundant call at strategy.go line 72.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 044909ec-e20b-449f-9ebd-589a3bb637fc

📥 Commits

Reviewing files that changed from the base of the PR and between 8b2e2cf and 9219528.

📒 Files selected for processing (8)
  • internal/app/app.go
  • internal/app/app_test.go
  • internal/app/parity_test.go
  • internal/cli/run.go
  • internal/cli/run_component_paths_test.go
  • internal/update/upgrade/executor.go
  • internal/update/upgrade/strategy.go
  • internal/update/upgrade/strategy_test.go

Comment thread internal/update/upgrade/executor.go Outdated
Comment on lines 521 to 524
method := effectiveMethod(ctx, r.Tool, profile)
msg := fmt.Sprintf("Upgrading %s via %s (%s → %s)", r.Tool.Name, method, r.InstalledVersion, r.LatestVersion)
sp := NewSpinner(pw, msg)
toolResult := executeOne(ctx, r, profile, dryRun)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid recomputing effectiveMethod multiple times per tool execution.

Line 521 resolves method for spinner text, Line 576 resolves it again for result metadata, and runStrategy resolves it a third time (internal/update/upgrade/strategy.go Line 72). Since effectiveMethod now performs an external brew probe, this adds avoidable process overhead and can yield inconsistent routing if one probe fails/cancels while another succeeds. Resolve once per tool and pass the resolved method through execution.

Suggested refactor sketch
--- a/internal/update/upgrade/executor.go
+++ b/internal/update/upgrade/executor.go
@@
-        method := effectiveMethod(ctx, r.Tool, profile)
+        method := effectiveMethod(ctx, r.Tool, profile)
         msg := fmt.Sprintf("Upgrading %s via %s (%s → %s)", r.Tool.Name, method, r.InstalledVersion, r.LatestVersion)
         sp := NewSpinner(pw, msg)
-        toolResult := executeOne(ctx, r, profile, dryRun)
+        toolResult := executeOne(ctx, r, profile, dryRun, method)
@@
-func executeOne(ctx context.Context, r update.UpdateResult, profile system.PlatformProfile, dryRun bool) ToolUpgradeResult {
+func executeOne(ctx context.Context, r update.UpdateResult, profile system.PlatformProfile, dryRun bool, method update.InstallMethod) ToolUpgradeResult {
@@
-        Method:     effectiveMethod(ctx, r.Tool, profile),
+        Method:     method,
@@
-    exitReq, err := runStrategy(ctx, r, profile)
+    exitReq, err := runStrategy(ctx, r, profile, method)
--- a/internal/update/upgrade/strategy.go
+++ b/internal/update/upgrade/strategy.go
@@
-func runStrategy(ctx context.Context, r update.UpdateResult, profile system.PlatformProfile) (bool, error) {
+func runStrategy(ctx context.Context, r update.UpdateResult, profile system.PlatformProfile, method update.InstallMethod) (bool, error) {
@@
-    method := effectiveMethod(ctx, r.Tool, profile)
-
     switch method {

Also applies to: 576-577

🤖 Prompt for 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.

In `@internal/update/upgrade/executor.go` around lines 521 - 524, The
effectiveMethod function is being called multiple times during a single tool
execution—at line 521 for the spinner message, at line 576 for result metadata,
and again in runStrategy within internal/update/upgrade/strategy.go at line
72—which causes unnecessary process overhead and can result in inconsistent
routing if one probe fails while another succeeds. Resolve effectiveMethod once
at the start of the tool execution (before line 521), store the result in a
variable, and then pass this resolved method value through the execution chain
instead of calling effectiveMethod again at line 576. Additionally, update the
runStrategy function and any related execution functions to accept the
pre-resolved method as a parameter rather than computing it internally,
eliminating the redundant call at strategy.go line 72.

…tea crash

When gentle-ai launches the interactive TUI without a terminal attached
(scripts, CI/CD, non-interactive SSH), Bubbletea panics with a confusing
'could not open a new TTY' error.

Added a TTY guard that checks stdin before launching the TUI. When no
TTY is available, returns a clear error listing non-interactive commands
(--version, --help, update, install, sync, doctor) instead of crashing.

The guard uses the existing isattyFn seam from selfupdate.go for
testability. Info commands (version, help) continue to work without a
TTY since they are dispatched before the guard.

Closes Gentleman-Programming#95
@decode2 decode2 force-pushed the fix/tty-guard-non-interactive branch from 9219528 to 1fb584f Compare June 16, 2026 05:41
@Alan-TheGentleman Alan-TheGentleman added the type:bug Bug fix label Jun 17, 2026

@Alan-TheGentleman Alan-TheGentleman 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.

Scoped, linked to the approved issue, and the guard runs before Bubbletea starts while keeping help/version usable without a TTY. The no-TTY message duplicates a bit of help copy, but that is not blocking for this bugfix. Approving.

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

Labels

type:bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Detect missing TTY and show helpful error instead of Bubbletea crash

2 participants