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.
This commit is contained in:
2026-05-25 13:00:54 +01:00
parent 0725375755
commit 178b4437b1
4 changed files with 85 additions and 7 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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[:])

View File

@@ -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)
}
})
}
}