Headless mode is intentionally just another front-end for the same worker. * With -p/--prompt it is a one-shot execution. Without -p it becomes a small * stdin protocol: announce readiness on stderr, collect bytes until stdin has * been quiet for 200 ms, submit that buffer as one prompt, and keep reading so * later input can be queued while the model is still working. */
| 9636 | /* Classify a possible terminal cursor-position reply (ESC[row;colR). User |
| 9637 | * keystrokes can arrive interleaved with these replies, so we only swallow bytes |
| 9638 | * when they are definitely part of a complete CPR sequence. */ |
| 9639 | static cpr_state cpr_candidate_state(const char *buf, size_t len) { |
| 9640 | if (len == 0) return CPR_PARTIAL; |
| 9641 | if ((unsigned char)buf[0] != 0x1b) return CPR_INVALID; |
| 9642 | if (len == 1) return CPR_PARTIAL; |
| 9643 | if (buf[1] != '[') return CPR_INVALID; |
| 9644 | if (len == 2) return CPR_PARTIAL; |
| 9645 | |
| 9646 | size_t p = 2; |
| 9647 | if (buf[p] < '0' || buf[p] > '9') return CPR_INVALID; |
| 9648 | while (p < len && buf[p] >= '0' && buf[p] <= '9') p++; |
| 9649 | if (p == len) return CPR_PARTIAL; |
| 9650 | if (buf[p++] != ';') return CPR_INVALID; |
| 9651 | if (p == len) return CPR_PARTIAL; |
| 9652 | if (buf[p] < '0' || buf[p] > '9') return CPR_INVALID; |
| 9653 | while (p < len && buf[p] >= '0' && buf[p] <= '9') p++; |
| 9654 | if (p == len) return CPR_PARTIAL; |
| 9655 | return p + 1 == len && buf[p] == 'R' ? CPR_COMPLETE : CPR_INVALID; |
| 9656 | } |
| 9657 | |
| 9658 | static void editor_flush_cpr_candidate(agent_editor *ed) { |
| 9659 | if (!ed->cpr_len) return; |
| 9660 | linenoiseEditQueueInput(&ed->edit, ed->cpr_buf, ed->cpr_len); |
| 9661 | ed->cpr_len = 0; |
| 9662 | } |
| 9663 | |
| 9664 | static bool agent_tail_ends_with(const char *tail, size_t tail_len, |
| 9665 | const char *seq, size_t seq_len) { |
| 9666 | return tail_len >= seq_len && |
| 9667 | memcmp(tail + tail_len - seq_len, seq, seq_len) == 0; |
| 9668 | } |
| 9669 | |
| 9670 | static bool agent_tail_has_seq_prefix(const char *tail, size_t tail_len, |
| 9671 | const char *seq, size_t seq_len) { |
| 9672 | size_t max = tail_len < seq_len - 1 ? tail_len : seq_len - 1; |
| 9673 | for (size_t n = max; n > 0; n--) { |
| 9674 | if (memcmp(tail + tail_len - n, seq, n) == 0) return true; |
| 9675 | } |
| 9676 | return false; |
| 9677 | } |
| 9678 | |
| 9679 | /* Track bracketed paste markers outside linenoise. The nonblocking event loop |
| 9680 | * may receive a paste in chunks; pausing linenoiseEditFeed() until ESC[201~ |
| 9681 | * arrives prevents pasted newlines from being interpreted as Enter. */ |
| 9682 | static void editor_track_bracketed_paste(agent_editor *ed, char c) { |
| 9683 | static const char start[] = "\x1b[200~"; |
| 9684 | static const char end[] = "\x1b[201~"; |
| 9685 | |
| 9686 | if (ed->paste_tail_len == sizeof(ed->paste_tail)) { |
| 9687 | memmove(ed->paste_tail, ed->paste_tail + 1, sizeof(ed->paste_tail) - 1); |
| 9688 | ed->paste_tail_len--; |
| 9689 | } |
| 9690 | ed->paste_tail[ed->paste_tail_len++] = c; |
| 9691 | |
| 9692 | /* The blocking linenoise() path waits inside linenoiseEditPaste() until it |
| 9693 | * sees ESC[201~. In the agent the outer event loop reads stdin in |
| 9694 | * non-blocking chunks; if we let linenoise start parsing ESC[200~ before |
| 9695 | * the closing marker has arrived, pasted newlines can be interpreted as |
no test coverage detected