Skip to content

Commit 5692dc2

Browse files
authored
Merge pull request #2 from stefanc-ai2/fix-sending-message
fix(wezterm): improve enter injection reliability
2 parents 5e1864c + be7b0ec commit 5692dc2

3 files changed

Lines changed: 194 additions & 8 deletions

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,16 @@ cq-mounted --json
125125
python -m compileall -q lib bin cq test
126126
python -m pytest test/ -v --tb=short
127127
```
128+
129+
---
130+
131+
## WezTerm notes
132+
133+
`ask` sends text to a pane and then injects **Enter** so the target TUI processes it. If you see text get pasted but not submitted, tune these env vars:
134+
135+
- `CQ_WEZTERM_ENTER_METHOD=auto|key|text` (default: `auto`)
136+
- `auto`: try `wezterm cli send-key Enter`, fall back to a raw CR byte
137+
- `key`: force `send-key` only (no fallback)
138+
- `text`: legacy mode (CR byte only)
139+
- `CQ_WEZTERM_ENTER_DELAY` (seconds): delay before injecting Enter
140+
- `CQ_WEZTERM_PASTE_DELAY` (seconds): delay between paste-mode send and Enter injection (multiline)

lib/terminal.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import shlex
77
import shutil
88
import subprocess
9+
import sys
910
import time
1011
from abc import ABC, abstractmethod
1112
from dataclasses import dataclass
@@ -641,24 +642,24 @@ def _send_enter(self, pane_id: str) -> None:
641642
time.sleep(enter_delay)
642643

643644
env_method_raw = os.environ.get("CQ_WEZTERM_ENTER_METHOD")
644-
# Default behavior is intentionally unchanged on non-Windows platforms:
645-
# previously we used `send-text` with a CR byte; keep that unless the user overrides.
646-
default_method = "auto" if os.name == "nt" else "text"
645+
# Default to "auto": try `wezterm cli send-key` first (real key event), then fall back to a CR byte.
646+
# Users can force the legacy behavior via `CQ_WEZTERM_ENTER_METHOD=text`.
647+
default_method = "auto"
647648
method = (env_method_raw or default_method).strip().lower()
648649
if method not in {"auto", "key", "text"}:
649650
method = default_method
650651

651652
# Retry mechanism for reliability (Windows native occasionally drops Enter)
652653
max_retries = 3
653654
for attempt in range(max_retries):
654-
# Only enable "auto key" behavior by default on native Windows.
655-
# Users can force key injection everywhere via CQ_WEZTERM_ENTER_METHOD=key.
656-
if method == "key" or (method == "auto" and os.name == "nt"):
655+
# Prefer a real key event. Fall back to a CR byte for older WezTerm CLIs or raw-mode TUIs.
656+
if method in {"auto", "key"}:
657657
if self._send_key_cli(pane_id, "Enter"):
658658
return
659659

660660
# Fallback: send CR byte; works for shells/readline, but not for all raw-mode TUIs.
661-
if method in {"auto", "text", "key"}:
661+
# NOTE: `key` mode is strict: if `send-key` fails, do not fall back.
662+
if method in {"auto", "text"}:
662663
result = _run(
663664
[*self._cli_base_args(), "send-text", "--pane-id", pane_id, "--no-paste"],
664665
input=b"\r",
@@ -668,7 +669,21 @@ def _send_enter(self, pane_id: str) -> None:
668669
return
669670

670671
if attempt < max_retries - 1:
671-
time.sleep(0.05)
672+
time.sleep(0.05 * (attempt + 1))
673+
674+
# If we got here, all attempts failed. The message text may be waiting in the input buffer.
675+
debug = (os.environ.get("CQ_DEBUG") or "").strip().lower() in ("1", "true", "yes")
676+
if debug or method == "key":
677+
hint = (
678+
"Press Enter manually in the target pane. "
679+
"If this happens often, try `CQ_WEZTERM_ENTER_METHOD=auto` and/or increase "
680+
"`CQ_WEZTERM_ENTER_DELAY` / `CQ_WEZTERM_PASTE_DELAY`."
681+
)
682+
level = "DEBUG" if debug else "WARN"
683+
print(
684+
f"[{level}] WezTerm Enter injection failed (method={method}, pane_id={pane_id}). {hint}",
685+
file=sys.stderr,
686+
)
672687

673688
def send_text(self, pane_id: str, text: str) -> None:
674689
sanitized = text.replace("\r", "").strip()

test/test_wezterm_enter.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
from __future__ import annotations
2+
3+
import terminal
4+
5+
6+
def test_wezterm_send_enter_defaults_to_auto_tries_key_first(monkeypatch) -> None:
7+
"""Default should try send-key first (real key event), then return on success."""
8+
monkeypatch.delenv("CQ_WEZTERM_ENTER_METHOD", raising=False)
9+
monkeypatch.setattr(terminal, "_get_wezterm_bin", lambda: "/usr/bin/wezterm")
10+
monkeypatch.setattr(terminal.time, "sleep", lambda _: None)
11+
12+
calls: list[list[str]] = []
13+
14+
def fake_run(*args, **kwargs):
15+
cmd = args[0]
16+
calls.append(cmd)
17+
if "send-key" in cmd:
18+
return terminal.subprocess.CompletedProcess(args=cmd, returncode=0, stdout="", stderr="")
19+
return terminal.subprocess.CompletedProcess(args=cmd, returncode=1, stdout="", stderr="unexpected")
20+
21+
monkeypatch.setattr(terminal, "_run", fake_run)
22+
23+
backend = terminal.WeztermBackend()
24+
backend._send_enter("123")
25+
26+
assert any("send-key" in cmd for cmd in calls)
27+
assert not any("send-text" in cmd for cmd in calls)
28+
29+
30+
def test_wezterm_send_enter_text_mode_does_not_use_send_key(monkeypatch) -> None:
31+
"""Legacy mode should only use send-text CR injection."""
32+
monkeypatch.setenv("CQ_WEZTERM_ENTER_METHOD", "text")
33+
monkeypatch.setattr(terminal, "_get_wezterm_bin", lambda: "/usr/bin/wezterm")
34+
monkeypatch.setattr(terminal.time, "sleep", lambda _: None)
35+
36+
calls: list[list[str]] = []
37+
38+
def fake_run(*args, **kwargs):
39+
cmd = args[0]
40+
calls.append(cmd)
41+
if "send-text" in cmd:
42+
assert kwargs.get("input") == b"\r"
43+
return terminal.subprocess.CompletedProcess(args=cmd, returncode=0, stdout="", stderr="")
44+
return terminal.subprocess.CompletedProcess(args=cmd, returncode=1, stdout="", stderr="unexpected")
45+
46+
monkeypatch.setattr(terminal, "_run", fake_run)
47+
48+
backend = terminal.WeztermBackend()
49+
backend._send_enter("123")
50+
51+
assert any("send-text" in cmd for cmd in calls)
52+
assert not any("send-key" in cmd for cmd in calls)
53+
54+
55+
def test_wezterm_send_enter_auto_falls_back_to_cr(monkeypatch) -> None:
56+
"""Auto should fall back to CR when send-key isn't supported."""
57+
monkeypatch.delenv("CQ_WEZTERM_ENTER_METHOD", raising=False)
58+
monkeypatch.setattr(terminal, "_get_wezterm_bin", lambda: "/usr/bin/wezterm")
59+
monkeypatch.setattr(terminal.time, "sleep", lambda _: None)
60+
61+
calls: list[list[str]] = []
62+
63+
def fake_run(*args, **kwargs):
64+
cmd = args[0]
65+
calls.append(cmd)
66+
if "send-key" in cmd:
67+
return terminal.subprocess.CompletedProcess(args=cmd, returncode=1, stdout="", stderr="nope")
68+
if "send-text" in cmd:
69+
assert kwargs.get("input") == b"\r"
70+
return terminal.subprocess.CompletedProcess(args=cmd, returncode=0, stdout="", stderr="")
71+
return terminal.subprocess.CompletedProcess(args=cmd, returncode=1, stdout="", stderr="unexpected")
72+
73+
monkeypatch.setattr(terminal, "_run", fake_run)
74+
75+
backend = terminal.WeztermBackend()
76+
backend._send_enter("123")
77+
78+
assert any("send-key" in cmd for cmd in calls)
79+
assert any("send-text" in cmd for cmd in calls)
80+
81+
82+
def test_wezterm_send_enter_key_mode_is_strict(monkeypatch) -> None:
83+
"""Key mode should not fall back to CR injection if send-key fails."""
84+
monkeypatch.setenv("CQ_WEZTERM_ENTER_METHOD", "key")
85+
monkeypatch.setattr(terminal, "_get_wezterm_bin", lambda: "/usr/bin/wezterm")
86+
monkeypatch.setattr(terminal.time, "sleep", lambda _: None)
87+
88+
calls: list[list[str]] = []
89+
90+
def fake_run(*args, **kwargs):
91+
cmd = args[0]
92+
calls.append(cmd)
93+
if "send-key" in cmd:
94+
return terminal.subprocess.CompletedProcess(args=cmd, returncode=1, stdout="", stderr="nope")
95+
if "send-text" in cmd:
96+
return terminal.subprocess.CompletedProcess(args=cmd, returncode=0, stdout="", stderr="")
97+
return terminal.subprocess.CompletedProcess(args=cmd, returncode=1, stdout="", stderr="unexpected")
98+
99+
monkeypatch.setattr(terminal, "_run", fake_run)
100+
101+
backend = terminal.WeztermBackend()
102+
backend._send_enter("123")
103+
104+
assert any("send-key" in cmd for cmd in calls)
105+
assert not any("send-text" in cmd for cmd in calls)
106+
send_key_calls = [cmd for cmd in calls if "send-key" in cmd]
107+
assert len(send_key_calls) >= 3
108+
109+
110+
def test_wezterm_send_enter_invalid_method_falls_back_to_auto(monkeypatch) -> None:
111+
"""Invalid methods should behave like auto (try send-key then fall back)."""
112+
monkeypatch.setenv("CQ_WEZTERM_ENTER_METHOD", "bogus")
113+
monkeypatch.setattr(terminal, "_get_wezterm_bin", lambda: "/usr/bin/wezterm")
114+
monkeypatch.setattr(terminal.time, "sleep", lambda _: None)
115+
116+
calls: list[list[str]] = []
117+
118+
def fake_run(*args, **kwargs):
119+
cmd = args[0]
120+
calls.append(cmd)
121+
if "send-key" in cmd:
122+
return terminal.subprocess.CompletedProcess(args=cmd, returncode=1, stdout="", stderr="nope")
123+
if "send-text" in cmd:
124+
assert kwargs.get("input") == b"\r"
125+
return terminal.subprocess.CompletedProcess(args=cmd, returncode=0, stdout="", stderr="")
126+
return terminal.subprocess.CompletedProcess(args=cmd, returncode=1, stdout="", stderr="unexpected")
127+
128+
monkeypatch.setattr(terminal, "_run", fake_run)
129+
130+
backend = terminal.WeztermBackend()
131+
backend._send_enter("123")
132+
133+
assert any("send-key" in cmd for cmd in calls)
134+
assert any("send-text" in cmd for cmd in calls)
135+
136+
137+
def test_wezterm_send_enter_key_mode_success(monkeypatch) -> None:
138+
"""Key mode should early-return when send-key works."""
139+
monkeypatch.setenv("CQ_WEZTERM_ENTER_METHOD", "key")
140+
monkeypatch.setattr(terminal, "_get_wezterm_bin", lambda: "/usr/bin/wezterm")
141+
monkeypatch.setattr(terminal.time, "sleep", lambda _: None)
142+
143+
calls: list[list[str]] = []
144+
145+
def fake_run(*args, **kwargs):
146+
cmd = args[0]
147+
calls.append(cmd)
148+
if "send-key" in cmd:
149+
return terminal.subprocess.CompletedProcess(args=cmd, returncode=0, stdout="", stderr="")
150+
return terminal.subprocess.CompletedProcess(args=cmd, returncode=1, stdout="", stderr="unexpected")
151+
152+
monkeypatch.setattr(terminal, "_run", fake_run)
153+
154+
backend = terminal.WeztermBackend()
155+
backend._send_enter("123")
156+
157+
assert any("send-key" in cmd for cmd in calls)
158+
assert not any("send-text" in cmd for cmd in calls)

0 commit comments

Comments
 (0)