Skip to content

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

Open
decode2 wants to merge 3 commits 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 3 commits 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

    • Prevented the TUI from starting when standard input isn’t attached to a terminal.
    • Added a clear error message explaining that the TUI isn’t available in non-interactive contexts, including available non-TUI commands and how to use version/help.
  • Tests

    • Added/updated coverage for no-TTY scenarios to verify the TUI-specific error is returned for default runs.
    • Confirmed --version/--help (including short forms) still succeed and produce output without a terminal.

@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: 53e366e1-d4c0-43d4-95b5-647cc446de7a

📥 Commits

Reviewing files that changed from the base of the PR and between 7e4c7c5 and db72087.

📒 Files selected for processing (4)
  • internal/app/app_test.go
  • internal/update/upgrade/executor.go
  • internal/update/upgrade/strategy.go
  • internal/update/upgrade/strategy_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. Additionally, the upgrade executor is refactored to pre-compute installation method once per tool and pass it downstream to executeOne and runStrategy, with all strategy tests updated to match the new signatures.

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
internal/app/app_test.go
Adds TestRunArgs_NoTTY_ReturnsErrNoTTYForTUI and TestRunArgs_NoTTY_StillAllowsVersionAndHelp that stub stdinIsTerminal to false, validating that empty args return an error mentioning --version and --help as alternatives, while those commands still succeed with non-empty output.
Existing TUI test fixups
internal/app/app_test.go, internal/app/parity_test.go
Updates TestRunArgs_TUISkipsSelfUpdate, TestRunArgs_TUIRestartsAfterGentleAIUpgradeResult, all four deferred-sync tests, and TestRunArgsNoCommandLaunchesTUI to save/restore stdinIsTerminal and force it to true during test execution.

Upgrade Strategy Method Resolution

Layer / File(s) Summary
ExecuteWithOptions pre-computes method once per tool
internal/update/upgrade/executor.go
In the per-tool upgrade loop, ExecuteWithOptions now resolves the effective installation method before creating the spinner and passes the pre-resolved method to executeOne, removing deferred resolution.
executeOne and runStrategy accept pre-resolved method
internal/update/upgrade/executor.go, internal/update/upgrade/strategy.go
executeOne now requires a pre-resolved method parameter, removes the internal effectiveMethod call, and passes the method to runStrategy. runStrategy signature updated to accept the method parameter instead of deriving it.
strategy_test.go tests updated for signature changes
internal/update/upgrade/strategy_test.go
All TestRunStrategy_* and OpenCode plugin tests updated to compute method := effectiveMethod(...) and pass it to runStrategy, matching the new function signatures across multiple test cases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Gentleman-Programming/gentle-ai#826: Both PRs modify the TUI execution path in RunArgs (len(args)==0); this PR adds the no-TTY guard, while that PR adjusts the same TUI flow for gentle-ai upgrade restart handling.
  • Gentleman-Programming/gentle-ai#877: Both PRs modify the TUI/no-args startup path in RunArgs; this PR adds the ErrNoTTYForTUI guard, while that PR adds PendingSync deferred-sync logic before TUI launch.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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 PR title accurately and concisely describes the main change: detecting missing TTY and returning a helpful error instead of a Bubbletea crash.
Linked Issues check ✅ Passed The PR fully addresses all coding objectives from issue #95: TTY detection before TUI launch, helpful error message listing non-TUI commands, and clean exit with proper error handling.
Out of Scope Changes check ✅ Passed All changes remain within scope. Core TTY detection and error handling are directly aligned with issue #95 requirements. Refactoring of upgrade method resolution is supporting infrastructure for test reliability.

✏️ 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.

@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
…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.

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

Good direction. The TTY guard is the right behavior, and keeping version/help available without a TTY is exactly the contract we want.

Please tighten the tests before merge. The new no-TTY path stubs stdin, but RunArgs still goes through OS support and system detection before reaching the TUI launch path. That leaves the test dependent on the host runner instead of fully controlling the contract.

Concrete ask: stub the OS/support and system-detection seams in the no-TTY tests, the same way stdin is controlled, so the test only fails when the TTY behavior regresses.

Stub ensureCurrentOSSupported and detectSystem in no-TTY tests to
isolate TTY behavior and remove dependency on host runner.

Addresses review feedback from Alan-TheGentleman on PR Gentleman-Programming#905
@decode2

decode2 commented Jun 23, 2026

Copy link
Copy Markdown
Author

@Alan-TheGentleman Stubeen los seams de OS/support y system-detection en los tests no-TTY para aislar el comportamiento TTY y eliminar la dependencia del host runner.

Cambios realizados:

  • TestRunArgs_NoTTY_ReturnsErrNoTTYForTUI: Ahora stubea �nsureCurrentOSSupported y detectSystem
  • TestRunArgs_NoTTY_StillAllowsVersionAndHelp: Ahora stubea �nsureCurrentOSSupported y detectSystem

Ambos tests ahora controlan completamente el contrato y solo fallarán cuando el comportamiento TTY regrese.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/app/app_test.go (1)

1202-1214: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert sentinel error identity, not only message text.

At Line 1202 onward, please also validate errors.Is(err, ErrNoTTYForTUI) before message-content checks. This prevents false positives if another error happens to include similar text.

Suggested test tightening
+import "errors"
...
  err := RunArgs([]string{}, &buf)
  if err == nil {
      t.Fatal("RunArgs(empty args) with no TTY: expected error, got nil")
  }
+ if !errors.Is(err, ErrNoTTYForTUI) {
+     t.Fatalf("RunArgs(empty args) with no TTY: error = %v; want ErrNoTTYForTUI", err)
+ }
  if !strings.Contains(err.Error(), "no TTY available") {
      t.Fatalf("RunArgs(empty args) with no TTY: error = %v; want error containing 'no TTY available'", err)
  }
🤖 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/app/app_test.go` around lines 1202 - 1214, The test is only
validating error message content using strings.Contains checks, which can
produce false positives if another error happens to contain similar text. Add a
validation using errors.Is(err, ErrNoTTYForTUI) immediately after the initial
nil check on the err returned from RunArgs to confirm the actual error identity
before proceeding with the message content checks for "no TTY available",
"--version", and "--help". This ensures you are testing for the specific
sentinel error, not just relying on string matching.
🤖 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.

Outside diff comments:
In `@internal/app/app_test.go`:
- Around line 1202-1214: The test is only validating error message content using
strings.Contains checks, which can produce false positives if another error
happens to contain similar text. Add a validation using errors.Is(err,
ErrNoTTYForTUI) immediately after the initial nil check on the err returned from
RunArgs to confirm the actual error identity before proceeding with the message
content checks for "no TTY available", "--version", and "--help". This ensures
you are testing for the specific sentinel error, not just relying on string
matching.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3a53bbcb-415b-46f9-a732-428acaca2325

📥 Commits

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

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

…entinel error identity

- Resolve effectiveMethod once per tool in the executable loop and thread
  the pre-resolved method through executeOne and runStrategy, eliminating
  two redundant calls (and a potential external brew probe on each).
- Update runStrategy signature to accept method as a parameter; remove
  the internal effectiveMethod call from strategy.go.
- Update all strategy_test.go call sites to pass the pre-resolved method.
- Add errors.Is(err, ErrNoTTYForTUI) assertion before strings.Contains
  checks in TestRunArgs_NoTTY_ReturnsErrNoTTYForTUI to guard against
  false positives from coincidental message text matches.
- Add errors import to app_test.go.

Addresses CodeRabbit review feedback on PR Gentleman-Programming#905.
@decode2

decode2 commented Jun 23, 2026

Copy link
Copy Markdown
Author

Hi @Alan-TheGentleman, I have addressed your comments:

  • Stubbed the OS/support and system-detection seams (\�nsureCurrentOSSupported\ and \detectSystem) in the no-TTY tests to fully isolate TTY behavior from the host runner.
  • Added an explicit \�rrors.Is(err, ErrNoTTYForTUI)\ assertion to verify the sentinel error identity in tests as recommended.

Ready for re-review!

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