From 178b4437b116e84be2065992d7698e7d541adce3 Mon Sep 17 00:00:00 2001 From: Harry Bayliss Date: Mon, 25 May 2026 13:00:54 +0100 Subject: [PATCH] Give injected agent submit Enter a longer settle delay The trailing CR that submits orchestrator-injected input was written only 15ms after the body, inside TUI agents' paste-coalescing window, so codex (and other paste-detecting agents) intermittently swallowed it as a newline and left the message composed but unsent. Centralize the per-piece timing in a pure pieceWriteDelay helper: keep 15ms between body lines but give the final lone Enter a 100ms settle gap so the agent closes the preceding burst and registers the CR as submit. Covers send_input, send_message, timers, and the spawn initial prompt (all go through writeInput). Resolves the codex composer-submit TODO item. --- CHANGELOG.md | 3 ++ TODO.md | 5 --- internal/app/child.go | 23 ++++++++++-- internal/app/child_input_test.go | 61 ++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae66ac0..711034c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ loosely follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). over MCP. ### Fixed +- Injected agent input now sends the submit Enter as a separated, + settled keystroke so messages reliably submit instead of sometimes + sitting unsent in the composer. - Codex agents are no longer reported idle while a turn is still running. - Slow MCP tool calls such as `wait_for_pattern` no longer block later diff --git a/TODO.md b/TODO.md index e409838..b5dae71 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1 @@ -- [ ] When opening a codex sub agent, the message gets input to the field, but the message is never submitted. - - This appears to be inconsistent. Sometimes it works, sometimes it doesn't. Might be because of popups on codex sub agents? - - Question: when it fails, is a Codex startup popup visible (trust/workspace, auth/model selection, permissions), or is the normal composer focused? - - Question: if the message is sitting in the composer, does pressing Enter once manually submit it, or does something else need to be dismissed first? - - Question: does this happen with short one-line prompts as well as long/multiline sub-agent instructions? - [ ] The per-tab agent summary text should display below the tab always, not just when the tab is focused. diff --git a/internal/app/child.go b/internal/app/child.go index 92df21b..91f96f4 100644 --- a/internal/app/child.go +++ b/internal/app/child.go @@ -26,6 +26,11 @@ import ( // false positives (timestamps, exit codes, etc.). var portRegex = regexp.MustCompile(`https?://[^\s:/]+:(\d{2,5})(?:/[^\s]*)?`) +const ( + agentInterPieceDelay = 15 * time.Millisecond + agentSubmitSettleDelay = 100 * time.Millisecond +) + type ChildStatus string const ( @@ -642,8 +647,8 @@ func (c *Child) writeInput(b []byte) error { return err } for i, piece := range pieces { - if i > 0 { - time.Sleep(15 * time.Millisecond) + if delay := pieceWriteDelay(i, len(pieces), piece); delay > 0 { + time.Sleep(delay) } if _, err := pty.Write(piece); err != nil { return err @@ -659,6 +664,20 @@ func inputWritePieces(kind ChildKind, b []byte) [][]byte { return splitOnEnter(b) } +func pieceWriteDelay(index, total int, piece []byte) time.Duration { + if index == 0 { + return 0 + } + if index == total-1 && isLoneEnter(piece) { + return agentSubmitSettleDelay + } + return agentInterPieceDelay +} + +func isLoneEnter(piece []byte) bool { + return len(piece) == 1 && (piece[0] == '\r' || piece[0] == '\n') +} + func mintIdentity() string { var buf [12]byte _, _ = rand.Read(buf[:]) diff --git a/internal/app/child_input_test.go b/internal/app/child_input_test.go index f834457..1e472ca 100644 --- a/internal/app/child_input_test.go +++ b/internal/app/child_input_test.go @@ -3,6 +3,7 @@ package app import ( "bytes" "testing" + "time" ) func TestInputWritePiecesOnlySplitAgentEnters(t *testing.T) { @@ -27,3 +28,63 @@ func TestInputWritePiecesOnlySplitAgentEnters(t *testing.T) { } } } + +func TestPieceWriteDelay(t *testing.T) { + cases := []struct { + name string + index int + total int + piece []byte + want time.Duration + }{ + { + name: "first piece", + index: 0, + total: 3, + piece: []byte("body"), + want: 0, + }, + { + name: "middle body piece", + index: 1, + total: 3, + piece: []byte("body"), + want: agentInterPieceDelay, + }, + { + name: "final carriage return submit", + index: 1, + total: 2, + piece: []byte("\r"), + want: agentSubmitSettleDelay, + }, + { + name: "final newline submit", + index: 1, + total: 2, + piece: []byte("\n"), + want: agentSubmitSettleDelay, + }, + { + name: "final non-enter piece", + index: 2, + total: 3, + piece: []byte("tail"), + want: agentInterPieceDelay, + }, + { + name: "standalone enter fast path", + index: 0, + total: 1, + piece: []byte("\r"), + want: 0, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := pieceWriteDelay(tc.index, tc.total, tc.piece); got != tc.want { + t.Fatalf("pieceWriteDelay(%d, %d, %q) = %s, want %s", tc.index, tc.total, tc.piece, got, tc.want) + } + }) + } +}