From b04642061d183dff8504127dfbee1c1a8352682c Mon Sep 17 00:00:00 2001 From: Pascal Date: Wed, 30 Sep 2026 16:24:46 +0200 Subject: [PATCH] cli: exit on stdin EOF and drop the console wide Ctrl+C broadcast (#29722) * cli: exit on stdin EOF and drop the console wide Ctrl+C broadcast On Windows the simple input reader sends CTRL_C_EVENT to every process attached to the console when stdin reaches EOF, killing unrelated processes such as a supervising agent. The CLI only stopped on EOF because of that self inflicted SIGINT; on POSIX, and with the advanced reader, it spins forever printing prompts. Drop the broadcast so both platforms just return an empty read, and treat an empty read as EOF in the chat loop and the model selection, since a submitted line always ends with a newline. * cli: keep the newline of a trailing "/" and stop mtmd-cli on EOF A lone "/" came back as an empty read and was taken for EOF, and mtmd-cli only stopped on EOF through the removed broadcast. --- common/console.cpp | 4 ++-- tools/cli/cli-context.cpp | 9 +++++++-- tools/mtmd/mtmd-cli.cpp | 4 ++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/common/console.cpp b/common/console.cpp index 9020eb5131..54fea813e6 100644 --- a/common/console.cpp +++ b/common/console.cpp @@ -1019,6 +1019,7 @@ namespace console { line.clear(); pop_cursor(); } + line += '\n'; has_more = false; } } else { @@ -1050,7 +1051,6 @@ namespace console { if (!std::getline(std::wcin, wline)) { // Input stream is bad or EOF received line.clear(); - GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); return false; } @@ -1065,7 +1065,7 @@ namespace console { if (!line.empty()) { char last = line.back(); if (last == '/') { // Always return control on '/' symbol - line.pop_back(); + line.back() = '\n'; return false; } if (last == '\\') { // '\\' changes the default action diff --git a/tools/cli/cli-context.cpp b/tools/cli/cli-context.cpp index aa4eb76796..d8787c13d2 100644 --- a/tools/cli/cli-context.cpp +++ b/tools/cli/cli-context.cpp @@ -255,7 +255,7 @@ bool cli_context::list_and_ask_models() { ui::user_turn user_turn; selection = user_turn.read_input(false, "Select model by number: "); if (selection.empty()) { - continue; + return false; } try { size_t idx = std::stoul(selection); @@ -502,8 +502,13 @@ int cli_context::run() { break; } + // a submitted line always ends with a newline, an empty read is EOF + if (buffer.empty()) { + break; + } + // remove trailing newline - if (!buffer.empty() && buffer.back() == '\n') { + if (buffer.back() == '\n') { buffer.pop_back(); } diff --git a/tools/mtmd/mtmd-cli.cpp b/tools/mtmd/mtmd-cli.cpp index 6fe058fd8c..4156602e4a 100644 --- a/tools/mtmd/mtmd-cli.cpp +++ b/tools/mtmd/mtmd-cli.cpp @@ -529,6 +529,10 @@ int main(int argc, char ** argv) { console::readline(line, false); if (g_is_interrupted) break; console::set_display(DISPLAY_TYPE_RESET); + // a submitted line always ends with a newline, an empty read is EOF + if (line.empty()) { + break; + } line = string_strip(line); if (line.empty()) { continue;