Summary
Opening the embedded shell pane in the TUI causes the app to freeze or crash, making shell mode completely unusable. Even on systems where the freeze does not manifest, the consumer's CLI binary is not available inside the shell.
Steps to reproduce
- Run
gotit tui in a project that has specs.
- Select any spec and press r to run it.
- Once the spec finishes (pass or fail), press Enter to open the step-review panel.
- Press Enter again to open the embedded shell.
Expected: an interactive shell prompt appears inside the TUI.
Actual: the TUI freezes (hangs indefinitely) — or, if it does open, the consumer binary is not on PATH.
Environment
- Especially reproducible when
$SHELL is fish or a plugin-heavy zsh.
- Does not require any particular OS; confirmed on Linux (CachyOS / Arch).
Root causes
1 — Unread vt.Emulator response pipe deadlocks the bubbletea Update loop
vt.NewEmulator creates an unbuffered io.Pipe internally (e.pr / e.pw). When the shell queries the terminal on startup — cursor position (\e[6n), device attributes (\e[c, \e[>c), mode reports, OSC colour queries — the emulator writes responses to e.pw. Because nothing ever reads from e.pr, every such write blocks indefinitely inside emulator.Write().
emulator.Write() is called from shellModel.handleOutput, which is dispatched from the bubbletea Update loop. The entire TUI therefore deadlocks.
Fish and zsh with TERM=xterm-256color (set explicitly by shell.go) send these queries on every prompt redraw; bash with TERM=dumb does not — which is why the freeze is shell-dependent.
2 — Duplicate PTY reader on re-focus
enterShell() called waitForOutput() even when a shell for the same spec was already running. This spawned a second goroutine reading from the same PTY file descriptor, splitting the byte stream between the two readers and garbling the emulator display.
3 — Consumer binary deleted before the shell opens
testdriver.Run registers an unconditional t.Cleanup that removes binDir at test teardown. GOTIT_KEEP_HOMEDIR=1 (already used to preserve homeDir) was not applied to binDir, so the consumer CLI binary was already deleted by the time the user typed commands in the shell.
4 — Wrong shell spawned (fish / plugin-heavy zsh)
shell.go derived the shell binary from $SHELL (the user's login shell). Fish ignores PS1 and queries terminal capabilities aggressively; a heavily configured zsh does the same. For an embedded debug pane, bash is the right default.
Affected files
| File |
Issue |
tui/shell.go |
Unread pipe → deadlock; $SHELL used instead of bash |
tui/tui.go |
Duplicate waitForOutput() call in enterShell() |
runner/testdriver/run.go |
Unconditional binDir cleanup deletes the binary too early |
Fix
PR #2 addresses all four root causes.
Summary
Opening the embedded shell pane in the TUI causes the app to freeze or crash, making shell mode completely unusable. Even on systems where the freeze does not manifest, the consumer's CLI binary is not available inside the shell.
Steps to reproduce
gotit tuiin a project that has specs.Expected: an interactive shell prompt appears inside the TUI.
Actual: the TUI freezes (hangs indefinitely) — or, if it does open, the consumer binary is not on
PATH.Environment
$SHELLisfishor a plugin-heavyzsh.Root causes
1 — Unread
vt.Emulatorresponse pipe deadlocks the bubbletea Update loopvt.NewEmulatorcreates an unbufferedio.Pipeinternally (e.pr/e.pw). When the shell queries the terminal on startup — cursor position (\e[6n), device attributes (\e[c,\e[>c), mode reports, OSC colour queries — the emulator writes responses toe.pw. Because nothing ever reads frome.pr, every such write blocks indefinitely insideemulator.Write().emulator.Write()is called fromshellModel.handleOutput, which is dispatched from the bubbletea Update loop. The entire TUI therefore deadlocks.Fish and zsh with
TERM=xterm-256color(set explicitly byshell.go) send these queries on every prompt redraw; bash withTERM=dumbdoes not — which is why the freeze is shell-dependent.2 — Duplicate PTY reader on re-focus
enterShell()calledwaitForOutput()even when a shell for the same spec was already running. This spawned a second goroutine reading from the same PTY file descriptor, splitting the byte stream between the two readers and garbling the emulator display.3 — Consumer binary deleted before the shell opens
testdriver.Runregisters an unconditionalt.Cleanupthat removesbinDirat test teardown.GOTIT_KEEP_HOMEDIR=1(already used to preservehomeDir) was not applied tobinDir, so the consumer CLI binary was already deleted by the time the user typed commands in the shell.4 — Wrong shell spawned (
fish/ plugin-heavyzsh)shell.goderived the shell binary from$SHELL(the user's login shell). Fish ignoresPS1and queries terminal capabilities aggressively; a heavily configured zsh does the same. For an embedded debug pane, bash is the right default.Affected files
tui/shell.go$SHELLused instead of bashtui/tui.gowaitForOutput()call inenterShell()runner/testdriver/run.gobinDircleanup deletes the binary too earlyFix
PR #2 addresses all four root causes.