fix(tui): write trailing newline on exit so shell prompt starts on a fresh line (#133)
* fix(tui): write newline on exit so shell prompt starts on fresh line
Ink hides the cursor during rendering and restores it on exit, but does
not emit a trailing newline. When the TUI process ends, the terminal
cursor sits at the end of the last rendered line, causing the shell
prompt to appear directly concatenated with the TUI output.
Rename restoreCursor → restoreTerminal and append '\n' alongside the
cursor-show escape sequence so the parent shell always receives the
prompt on a clean new line.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(tui): add regression guard for exit newline; update changelog
- Add tests/test_ui/test_tui_exit_sequence.py with two tests:
* test_tui_exit_handler_writes_newline: asserts the cleanup write
includes the trailing \n alongside \x1B[?25h
* test_tui_exit_handler_registered_for_all_signals: asserts cleanup
is registered for 'exit', SIGINT, and SIGTERM
- Add entry to CHANGELOG.md [Unreleased] Fixed section
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(tests): update ohmo cli test inputs for allow_remote_admin_commands prompt
Commit dd1d235 added a new 'Allow explicitly listed administrative slash
commands from remote channels?' confirmation step to the gateway config
wizard, but the four interactive test cases in test_ohmo/test_cli.py were
not updated to provide an answer for it. This caused stdin to be exhausted
and click to emit Abort(), making all four tests fail with exit_code=1.
Add 'n' as the answer for allow_remote_admin_commands in each affected
test, and move the existing 'restart gateway' answer after it in
test_ohmo_config_interactive_can_restart_gateway.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -34,6 +34,7 @@ The format is based on Keep a Changelog, and this project currently tracks chang
|
||||
- Fixed concurrent permission modals overwriting each other in TUI default mode when the LLM returns multiple tool calls in one response; `_ask_permission` now serialises callers via an `asyncio.Lock` so each modal is shown and resolved before the next one is emitted.
|
||||
- Fixed React TUI Markdown tables to size columns from rendered cell text so inline formatting like code spans and bold text no longer breaks alignment.
|
||||
- Fixed grep tool crashing with `ValueError` / `LimitOverrunError` when ripgrep outputs a line longer than 64 KB (e.g. minified assets or lock files). The asyncio subprocess stream limit is now 8 MB and oversized lines are skipped rather than terminating the session.
|
||||
- Fixed React TUI exit leaving the shell prompt concatenated with the last TUI line. The terminal cleanup handler now writes a trailing newline (`\n`) alongside the cursor-show escape sequence so the shell prompt always starts on a fresh line.
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -39,17 +39,19 @@ process.on('uncaughtException', (err: NodeJS.ErrnoException) => {
|
||||
|
||||
const config = JSON.parse(process.env.OPENHARNESS_FRONTEND_CONFIG ?? '{}') as FrontendConfig;
|
||||
|
||||
// Restore terminal cursor visibility on exit (Ink hides it by default)
|
||||
const restoreCursor = (): void => {
|
||||
process.stdout.write('\x1B[?25h');
|
||||
// Restore terminal cursor visibility on exit (Ink hides it by default).
|
||||
// Also write a newline so the shell prompt starts on a fresh line and does
|
||||
// not run into the last line of the TUI output.
|
||||
const restoreTerminal = (): void => {
|
||||
process.stdout.write('\x1B[?25h\n');
|
||||
};
|
||||
process.on('exit', restoreCursor);
|
||||
process.on('exit', restoreTerminal);
|
||||
process.on('SIGINT', () => {
|
||||
restoreCursor();
|
||||
restoreTerminal();
|
||||
process.exit(130);
|
||||
});
|
||||
process.on('SIGTERM', () => {
|
||||
restoreCursor();
|
||||
restoreTerminal();
|
||||
process.exit(143);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Regression guard: the React TUI exit handler must write a trailing newline.
|
||||
|
||||
When the TUI process exits, Ink leaves the cursor at the end of the last
|
||||
rendered line. Without a newline the shell prompt appears concatenated with
|
||||
the TUI output, which is visually broken. This test reads the TypeScript
|
||||
entry-point source and asserts that the exit-cleanup write includes '\\n'
|
||||
alongside the cursor-show escape sequence.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _frontend_index() -> Path:
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
return repo_root / "frontend" / "terminal" / "src" / "index.tsx"
|
||||
|
||||
|
||||
def test_tui_exit_handler_writes_newline() -> None:
|
||||
"""restoreTerminal must emit \\x1B[?25h\\n so the shell prompt starts on a new line.
|
||||
|
||||
Regression for: TUI exit leaves shell prompt concatenated with last TUI line.
|
||||
"""
|
||||
source = _frontend_index().read_text(encoding="utf-8")
|
||||
|
||||
# Locate the cleanup function body and verify it includes a trailing \\n.
|
||||
# The expected write is: process.stdout.write('\\x1B[?25h\\n')
|
||||
pattern = re.compile(
|
||||
r"process\.stdout\.write\(['\"].*\\x1B\[\?25h\\n.*['\"]\)",
|
||||
re.MULTILINE,
|
||||
)
|
||||
assert pattern.search(source), (
|
||||
"The TUI exit handler must call process.stdout.write with a trailing '\\n' "
|
||||
"so the shell prompt starts on a fresh line after the TUI exits. "
|
||||
f"Check {_frontend_index()} and ensure the cursor-restore write ends with \\n."
|
||||
)
|
||||
|
||||
|
||||
def test_tui_exit_handler_registered_for_all_signals() -> None:
|
||||
"""restoreTerminal must be attached to 'exit', SIGINT, and SIGTERM."""
|
||||
source = _frontend_index().read_text(encoding="utf-8")
|
||||
|
||||
for signal in ("exit", "SIGINT", "SIGTERM"):
|
||||
assert f"process.on('{signal}'" in source, (
|
||||
f"The TUI exit cleanup must be registered for '{signal}'. "
|
||||
f"Check {_frontend_index()}."
|
||||
)
|
||||
Reference in New Issue
Block a user