Skip to content

agent: restore native TUI with terminal regression harness - #39

Merged
gary149 merged 5 commits into
masterfrom
tui-rendering-harness
Sep 10, 2026
Merged

gary149 merged 5 commits into
masterfrom
tui-rendering-harness

Conversation

@youngsemicolon

@youngsemicolon youngsemicolon commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

Overview

Reworks the native inline TUI from #37 and adds a repeatable terminal rendering harness. Streaming output, editor input, completion and permission overlays now share the renderer, with fixes for resize, cursor placement, cancellation, terminal cleanup, and image attachment ownership.

The harness drives the production renderer through a PTY, checks interpreted screens and cursors, and replays captured output in xterm.js with screenshots. Adds Linux/macOS/Windows CI and a native Windows console test.

Normal session video

Watch the normal session (9:48, MP4)

Normal session preview

The actual agent runs directly in a fixed 100x28 xterm window at normal playback speed, with the real Qwen server. It uses all six built-in tools to fix a small Python project, then demonstrates menus, skills, permissions, shell commands, paste, an actual clipboard image, cancellation, resume, and conversation clearing. Full coverage and observations.

The recording retains rough edges: Alt+Enter does not insert a newline in this xterm setup, and the resumed model picks up the earlier cancelled counting request until interrupted and given a clearer instruction. Multiline clipboard paste works. The clipboard image is correctly read as Tuesday with 25 units; /compact reports that the conversation is too short to compact. No MCP server is configured for this demo.

Terminal test video

Watch the captioned Linux xterm recording (4:06, MP4)

Terminal video preview

Shows all 36 terminal scenarios with color off/on, a fresh real Qwen session and tool run, and fresh CTest, sanitizer, and browser-replay results. The production renderer's PTY output is displayed live in xterm; fixture events and clipboard bytes are injected. Recording scope and limitations.

The initial fork CI run passed Linux terminal tests, failed the Windows build, and timed out in the macOS PTY tests. The video does not resolve or demonstrate those platform checks.

Menu spacing regression

Opening the command menu previously scrolled the transcript up permanently, leaving six blank rows after dismissal and adding scrollback on repeated openings. Layout height changes now repaint the transcript above the editor without scrolling temporary menu rows. Regression tests check exact transcript rows, dismissal paths, resize, permission completion, repeated cycles, and the live Qwen command menu. An xterm screenshot comparison also confirms identical transcript pixels before opening and after closing. The video above records the earlier commit and predates this fix.

Validation

  • All 10 agent CTest suites pass locally, including 40 terminal scenarios with color enabled and disabled.
  • 254 browser screen/cursor checkpoints match.
  • AddressSanitizer and UndefinedBehaviorSanitizer checks pass.
  • Live Qwen smoke test passes session resume, response, resize, permission approval, file creation, and cancellation.
  • Windows/macOS cross-compilation checked. Native Windows/macOS success, actual clipboard/image protocols, and joined emoji rendering remain unverified; see the CI failures above.

See tools/agent/tests/README.md for reproduction commands and verification limits. This PR targets this fork only.

Requirements

  • Human review of the changes and contributing guidelines remains with the contributor.
  • AI usage disclosure: YES. Codex implemented and tested the changes and prepared this fork PR at the owner's request.

@github-actions github-actions Bot added documentation Improvements or additions to documentation examples devops labels Sep 10, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f93ca6ca8f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

} else {
ss << " ";
}
ss << "\xE2\x86\x91 " << state.stats.total_input << " \xE2\x86\x93 " << state.stats.total_output;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace TUI Unicode glyphs with ASCII

The footer emits Unicode arrows here, while the same new TUI also emits a Unicode bullet and prompt chevron. Replace these glyphs and their test expectations with ASCII equivalents as required by the repository's contributor rules.

AGENTS.md reference: AGENTS.md:L73-L73

Useful? React with 👍 / 👎.

Comment thread tools/agent/agent-tool-parser.cpp Outdated
Comment on lines +27 to +30
// Scan forward from `pos` (which must point to a '{') and return the index one
// past the matching closing '}', or std::string::npos if the braces are never
// balanced. Handles nested objects and arrays, and skips over string literals
// (including escaped characters) so interior braces inside strings are ignored.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the parser comment concise

This new comment hard-wraps a single explanation across four lines, contrary to the repository requirement that comments are usually one or two lines and are not hard-wrapped. Condense it or remove details already expressed by the implementation.

AGENTS.md reference: AGENTS.md:L75-L77

Useful? React with 👍 / 👎.

Comment thread tools/agent/agent.cpp Outdated
Comment on lines +1235 to +1238
auto cmd_result = run_user_command(cmd, working_dir, g_is_interrupted,
tui ? [&](std::string_view chunk) {
tui->post_transcript(std::string(chunk), tui_transcript_style::NORMAL);
} : std::function<void(std::string_view)>());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route Ctrl+C to the running shell command

When an interactive !cmd is still running, this TUI path never marks the footer as generating. Because raw mode disables terminal signals, Ctrl+C is consumed by the renderer, takes its idle-editor branch, and queues EOF instead of setting g_is_interrupted; a long-running shell command therefore continues, and the agent exits only after it eventually finishes. Mark shell execution as interruptible or route Ctrl+C directly to its interruption flag.

Useful? React with 👍 / 👎.

Comment on lines +665 to +667
case tui_event_type::TRANSCRIPT:
flush_text_buffer(false);
append_transcript_line(event.text, event.transcript_style);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve shell output across read chunk boundaries

For TUI !cmd output, each pipe-read chunk is posted as a separate TRANSCRIPT event, and this call treats every event as a complete line. Since the transcript path appends a newline when the chunk lacks one, output exceeding the 4095-byte read buffer or arriving through multiple delayed writes gains spurious line breaks and can corrupt split UTF-8 or ANSI sequences. Buffer these chunks until actual newlines or EOF instead of rendering each callback as a line.

Useful? React with 👍 / 👎.

Comment thread tools/agent/tui-renderer.cpp Outdated
Comment on lines +1102 to +1103
std::lock_guard<std::mutex> lock(autocomplete_threads_mu_);
autocomplete_threads_.push_back(std::move(worker));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reap superseded completion workers

While editing any @ completion, every changed query starts another thread, but completed workers remain as joinable entries in autocomplete_threads_ until renderer shutdown. Repeated completion use therefore retains thread resources for the entire session and can also leave many directory scans running concurrently in large directories; join or reuse superseded workers during the session rather than accumulating them.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8383268f5c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread tools/agent/agent.cpp Outdated
if (!params.prompt.empty()) {
initial_prompt = params.prompt;
params.prompt.clear(); // Only use once
params.single_turn = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve interactive mode when using -p

When stdin is a terminal, -p/--prompt previously supplied the first turn and then left the agent interactive; this assignment now forces every such invocation to exit after that response. The shared CLI exposes -st/--single-turn as the separate opt-in for this behavior, so users can no longer seed an interactive session with -p unless this assignment is removed or restricted to an explicitly non-interactive mode.

Useful? React with 👍 / 👎.

Comment thread tools/agent/agent.cpp Outdated
console::log("\n› ");
if (tui) {
tui_command command;
if (!tui->wait_for_command(command)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wake the idle TUI when a termination signal arrives

On POSIX, if SIGTERM or an externally delivered SIGINT arrives while the interactive TUI is waiting for input, signal_handler only sets g_is_interrupted, while this indefinite queue wait never polls that flag and no command is enqueued. The process therefore remains running after the first termination signal; the signal path needs to wake this wait or the wait must periodically check the interruption flag.

Useful? React with 👍 / 👎.

Comment thread tools/agent/tui-renderer.cpp Outdated
Comment on lines +781 to +786
std::string display = truncate_display(event.tool_output, 500);
if (!display.empty()) {
if (display.back() != '\n') {
display.push_back('\n');
}
append_transcript_line(display, event.tool_success ? tui_transcript_style::NORMAL : tui_transcript_style::FAILURE);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Display the error field for failed TUI tools

When a tool fails with an empty output and a populated error - for example, write cannot open its target or receives no file_path - the streaming path at agent-loop.cpp:464 puts only tool_res.output in this event. Consequently this renderer displays only the timing footer and hides the failure reason from the interactive user; carry the error through the event and render it for unsuccessful results.

Useful? React with 👍 / 👎.

Comment thread tools/agent/agent-tool-parser.cpp Outdated
Comment on lines +203 to +208
size_t brace_start = visible_content.find('{', body_start);
if (brace_start == std::string::npos) {
// No JSON object at all after the opening tag; nothing to do.
break;
}
size_t brace_end = find_balanced_brace_end(visible_content, brace_start);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recover the complete unterminated tool-call array

For a complete JSON array whose closing XML tag is missing, such as <tool_calls>[{...},{...}], searching for the first { starts balancing inside the first element rather than at the surrounding [. The fallback therefore parses and executes only the first call, removes that prefix, and leaves the remaining calls as visible assistant text; start at the first JSON object or array delimiter so the entire payload is recovered.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff12bdf172

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread tools/agent/agent.cpp Outdated
console::log("\nCompacting...\n");
if (command_buffer == "/compact") {
emit_tui_or_console("\nCompacting...\n", tui_transcript_style::INFO);
if (agent.compact()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make manual compaction interruptible

When /compact performs summary generation, this synchronous call can take as long as a model turn, but the TUI is never marked as generating. handle_input_event only invokes the interruption callback while footer_.generating is true, so ESC does nothing and Ctrl+C queues EOF instead; the compaction continues, and the agent exits only after it finishes. Set the generating state around this call or route cancellation directly to the interruption flag.

Useful? React with 👍 / 👎.

Comment thread tools/agent/tui-renderer.cpp Outdated
}

void tui_renderer::render_tool_result(const tui_event & event) {
std::string display = truncate_display(event.tool_output, 500);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve rich tool-result metadata in the TUI

For successful TUI tools, this unconditionally truncates the output after the event projection has discarded the rest of tool_result. The read tool stores image bytes and MIME type specifically for terminal preview, while update_plan sets no_truncate_display; the legacy CLI honors both. Consequently the TUI no longer previews images read by vision models and clips long plans, so carry and honor these fields in the TUI event.

Useful? React with 👍 / 👎.

Comment thread tools/agent/agent.cpp
console::log("\n");
}

// Build user content — multimodal if images were pasted, plain string otherwise

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace the em dash with ASCII

This newly added comment contains an em dash despite the repository's explicit ASCII-only requirement; replace it with an ASCII hyphen.

AGENTS.md reference: AGENTS.md:L73-L73

Useful? React with 👍 / 👎.

- read the fixture control pipe with stdio and retry on EINTR: libc++ getline
  blocked until the pipe buffer filled and SIGWINCH interrupted reads, which
  is why the macOS PTY tests timed out
- install the SIGWINCH handler with SA_RESTART so a resize cannot fail a
  blocking read on another thread
- ignore the transient PENDIN termios bit and resolve the executable path in
  test_terminal.py; raise the PTY test timeout to 600s
- default-initialize permission_request::type (GCC -Werror=uninitialized)
- undef the windows.h ERROR macro in http-inference-backend.cpp (MSVC)
- buffer shell command output by line in the TUI; mark shell commands and
  /compact as generating so ESC/Ctrl+C interrupt them; discard a partial
  compaction summary when interrupted
- keep -p interactive (use -st for single turn); exit the idle TUI on SIGTERM
- show tool errors and honor no_truncate_display in the TUI
- reap finished file completion workers instead of keeping them until exit
- recover unterminated <tool_calls> arrays in the tool protocol parser
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-10T13:43:52.444674Z 48342bf New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 48342bfb13

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/agent/agent.cpp
Comment on lines +728 to +730
if (backend_mode != "local") {
params.server_base = server_url;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate the requested model to HTTP backends

When --server-url targets an OpenAI-compatible endpoint without llama-server's /props, this new server_base assignment allows parsing without -m, while the same change removes the only fallback that populated http_cfg.model. The backend consequently sends model: "default", and even a supplied -m is ignored, so generic routers can reject the request or route it to the wrong model. Propagate params.model.get_name() or add an explicit HTTP model option.

Useful? React with 👍 / 👎.

return lines;
}

width = std::max(width, 8);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor the terminal width when drawing completion menus

When a slash or file-completion menu is open while the terminal is narrowed below eight columns, this forces each menu row to use an eight-column layout even though current_region_lines() budgets it as one row at term_cols_. The terminal then wraps that row into multiple physical rows, scrolling or overwriting the managed editor/footer and invalidating cursor placement; retain the actual width and use a shorter prefix or truncation marker for very narrow terminals.

Useful? React with 👍 / 👎.

@gary149
gary149 merged commit f5b7596 into master Sep 10, 2026
8 of 34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

devops documentation Improvements or additions to documentation examples

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants