Skip to content

Commit 79d945a

Browse files
skulidropekclaude
andcommitted
fix(shell): guard TUI render against non-TTY Docker environments
Ink's render() calls setRawMode(true) on mount. When docker-git runs without a pseudo-TTY (docker exec without -t, CI, piped stdin), this throws "Raw mode is not supported" and waitUntilExit() never resolves, causing an infinite hang. - Wrap render() in Effect.suspend and check process.stdin.isTTY and typeof process.stdin.setRawMode before calling render() - Fail immediately with a descriptive InputReadError guiding the user to attach a terminal (ssh or docker run -it) Invariant: ∀ env: ¬isTTY(env) → fail(InputReadError) ∧ ¬hang Closes #100 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5940a62 commit 79d945a

1 file changed

Lines changed: 23 additions & 3 deletions

File tree

packages/app/src/docker-git/menu.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,14 +282,34 @@ const TuiApp = () => {
282282
// EFFECT: Effect<void, AppError, FileSystem | Path | CommandExecutor>
283283
// INVARIANT: app exits only on Quit or ctrl+c
284284
// COMPLEXITY: O(1) per input
285+
//
286+
// CHANGE: guard against non-TTY environments (Docker without -t)
287+
// WHY: Ink calls setRawMode(true) on mount — without a TTY stdin does not support
288+
// raw mode, causing an unhandled error and a hang in waitUntilExit().
289+
// Fail fast with a descriptive error instead.
290+
// QUOTE(ТЗ): "вечный цикл зависания на TUI из за ошибки Raw mode is not supported"
291+
// REF: issue-100
292+
// SOURCE: https://github.com/vadimdemedes/ink/#israwmodesupported
293+
// FORMAT THEOREM: ∀ env: ¬isTTY(env) → fail(InputReadError) ∧ ¬hang
294+
// INVARIANT: render() is only called when stdin.isTTY ∧ setRawMode ∈ stdin
285295
export const runMenu = pipe(
286296
Effect.sync(() => {
287297
resumeTui()
288298
}),
289299
Effect.zipRight(
290-
Effect.tryPromise({
291-
try: () => render(React.createElement(TuiApp)).waitUntilExit(),
292-
catch: (error) => new InputReadError({ message: error instanceof Error ? error.message : String(error) })
300+
Effect.suspend(() => {
301+
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== "function") {
302+
return Effect.fail(
303+
new InputReadError({
304+
message:
305+
"TUI requires a TTY. Attach a terminal: ssh into the container or use `docker run -it`."
306+
})
307+
)
308+
}
309+
return Effect.tryPromise({
310+
try: () => render(React.createElement(TuiApp)).waitUntilExit(),
311+
catch: (error) => new InputReadError({ message: error instanceof Error ? error.message : String(error) })
312+
})
293313
})
294314
),
295315
Effect.ensuring(

0 commit comments

Comments
 (0)