Skip to content

feat(tty): Ctrl-Z job-control suspend + clean Ctrl-C exit#8

Merged
amix merged 1 commit into
mainfrom
amix/job-control
May 16, 2026
Merged

feat(tty): Ctrl-Z job-control suspend + clean Ctrl-C exit#8
amix merged 1 commit into
mainfrom
amix/job-control

Conversation

@amix
Copy link
Copy Markdown
Owner

@amix amix commented May 16, 2026

Context

Backport of modem-dev/hunk#269 + #292. PR 4 of an expert-gated backport series.

OpenTUI ran with exitOnCtrlC: true: Ctrl-C hard-exited and bypassed dunk's ordered shutdown() (renderer torn down out of order), and Ctrl-Z was swallowed in raw mode so dunk could not be suspended as a shell job. No SIGINT/SIGTERM handling.

What was changed

  • New src/core/jobControl.ts:
    • installJobControlSuspendSupport — Ctrl-Z → renderer.suspend(), register once("SIGCONT"), send SIGTSTP to the foreground process group (pid 0); resumes the renderer on SIGCONT; restores it if SIGTSTP throws; win32 no-op.
    • installJobControlInterruptSupport — Ctrl-C → dunk's ordered shutdown().
  • main.tsx: exitOnCtrlC: false; SIGINT/SIGTERM → shutdown() (registered via process.once, removed in shutdown()); both supports disposed in shutdown() (idempotent via the existing shuttingDown guard); jobControl lazy-imported on the app branch so cold-path startup perf is preserved.
  • Upstream's daemon hostClient.stop() dropped — dunk has no daemon.

Expert-reviewed (terminal-lifecycle PR): lifecycle/race ordering, pid 0 target, and the hostClient.stop() drop validated. Incorporated: confirmed CliRenderer.suspend()/resume() flush synchronously (native restore before SIGTSTP), confirmed SIGTERM still exits (shutdownSessionprocess.exit(0)), added the "ignore Ctrl-Z after renderer destroyed" unit test.

Verification

  • bun run typecheck, bun run lint: clean.
  • jobControl.test.ts: 10 pass — both functions, dispose cleanup, SIGTSTP-throw restore, win32 no-op, ignore-after-destroy for Ctrl-C and Ctrl-Z.
  • Full src/core: 179 pass. PTY suite: 21 pass (real launches unaffected by exitOnCtrlC: false).
  • Real-PTY Ctrl-C smoke: clean exit 0 with terminal-restore sequences emitted (alt-screen exit, cursor/mouse restore) — the #292 path verified end-to-end on a real TTY.

Out of scope

The interactive Ctrl-Z → shell fg → resume cycle requires a controlling-TTY foreground process group and can't be faithfully reproduced in CI (a synthetic self-SIGTSTP outside a foreground job doesn't stop under Bun — an orphaned-process-group artifact, not a defect). The dunk-owned logic is unit-tested; the OS path is manual-smoke, matching upstream which also unit-tests jobControl only.

🤖 Generated with Claude Code

@amix amix marked this pull request as ready for review May 16, 2026 19:37
OpenTUI ran with `exitOnCtrlC: true`, so Ctrl-C hard-exited and bypassed
dunk's ordered shutdown, and Ctrl-Z was swallowed in raw mode — dunk
could not be suspended as a shell job.

Add `src/core/jobControl.ts`: `installJobControlSuspendSupport`
(Ctrl-Z → renderer.suspend(), SIGTSTP to the foreground group, resume on
SIGCONT; restores the renderer if SIGTSTP throws; win32 no-op) and
`installJobControlInterruptSupport` (Ctrl-C → ordered shutdown). Wire
both into `main.tsx`, flip `exitOnCtrlC: false`, and shut down cleanly
on SIGINT/SIGTERM. Lazy-imported on the app branch so cold paths keep
their startup perf. Upstream's daemon `hostClient.stop()` is dropped —
dunk has no daemon.

Backports modem-dev/hunk#269 + #292.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@amix amix force-pushed the amix/job-control branch from 8c651a2 to f8fc6c2 Compare May 16, 2026 19:40
@amix amix merged commit f586b2a into main May 16, 2026
2 checks passed
@amix amix deleted the amix/job-control branch May 16, 2026 19:44
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.

1 participant