mixhub-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agy_claude_filter.py +1042 -0
- bin/hub +265 -0
- bin/mixhub +7 -0
- bin/mixhub.js +15 -0
- channels.py +2801 -0
- cost_report.py +623 -0
- handoff.py +1294 -0
- icons_font.py +211 -0
- mixhub_cli-0.1.0.dist-info/METADATA +98 -0
- mixhub_cli-0.1.0.dist-info/RECORD +21 -0
- mixhub_cli-0.1.0.dist-info/WHEEL +4 -0
- mixhub_cli-0.1.0.dist-info/entry_points.txt +3 -0
- mixhub_cli.py +25 -0
- protocols.py +643 -0
- rules_sync.py +291 -0
- server.py +2045 -0
- slim_storage.py +142 -0
- static/app.js +466 -0
- static/index.html +58 -0
- static/style.css +1 -0
- tui.py +2927 -0
agy_claude_filter.py
ADDED
|
@@ -0,0 +1,1042 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
agy_claude_filter.py — Zero-latency PTY ANSI stream filter that transforms `agy`
|
|
4
|
+
terminal output to match Claude Code's clean, low-noise visual palette:
|
|
5
|
+
1. User messages (`> ...`): Full-width dark-gray box (ANSI 236) + crisp white text (ANSI 255)
|
|
6
|
+
2. Assistant body text: Crisp pure white (ANSI 255) instead of bluish-purple tint (ANSI 189)
|
|
7
|
+
3. Markdown headings (`###`): Pure white bold (ANSI 255;1) instead of neon cyan (ANSI 39;1)
|
|
8
|
+
4. Inline code: Soft lavender text (ANSI 147, #afafff) with NO background block (49)
|
|
9
|
+
5. Subagent live context badge: `Agent(self: ...)` -> `Agent(Argon: ...) · ctx XXXk`
|
|
10
|
+
6. Mouse click-to-move-cursor in prompt input box (`> ...`): left-clicking any character
|
|
11
|
+
in the bottom prompt line moves the input cursor directly to that character.
|
|
12
|
+
|
|
13
|
+
Set AGY_RAW_COLORS=1 to bypass filtering at any time.
|
|
14
|
+
Non-interactive invocations (`-p` / `--print` / piped stdout) bypass the PTY completely.
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
import fcntl
|
|
18
|
+
except ImportError:
|
|
19
|
+
fcntl = None
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
try:
|
|
23
|
+
import pty
|
|
24
|
+
except ImportError:
|
|
25
|
+
pty = None
|
|
26
|
+
import re
|
|
27
|
+
import select
|
|
28
|
+
import signal
|
|
29
|
+
import struct
|
|
30
|
+
import sys
|
|
31
|
+
try:
|
|
32
|
+
import termios
|
|
33
|
+
except ImportError:
|
|
34
|
+
termios = None
|
|
35
|
+
import time
|
|
36
|
+
try:
|
|
37
|
+
import tty
|
|
38
|
+
except ImportError:
|
|
39
|
+
tty = None
|
|
40
|
+
import unicodedata
|
|
41
|
+
|
|
42
|
+
def _resolve_real_agy() -> str:
|
|
43
|
+
env_bin = os.environ.get("AGY_REAL_BIN")
|
|
44
|
+
if env_bin and os.path.exists(env_bin):
|
|
45
|
+
return env_bin
|
|
46
|
+
bin_candidate = os.path.expanduser("~/.local/bin/agy-bin")
|
|
47
|
+
if os.path.exists(bin_candidate):
|
|
48
|
+
return bin_candidate
|
|
49
|
+
return os.path.expanduser("~/.local/bin/agy")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
REAL_AGY = _resolve_real_agy()
|
|
53
|
+
SUB_LIVE_PATH = "/tmp/agy-subagent-live.json"
|
|
54
|
+
ACTIVE_EXT_PATH = "/tmp/agy-active-ext.json"
|
|
55
|
+
_IN_PTY_MAIN = False
|
|
56
|
+
SUB_LIVE_CACHE = {"ts": 0.0, "model": "Argon", "role": "", "ctx": "", "ext_tag": ""}
|
|
57
|
+
CURRENT_CID = ""
|
|
58
|
+
AGENT_ROLE_RE = re.compile(rb"Agent\(self:\s*[^)\r\n]*\)")
|
|
59
|
+
AGENT_TIMER_RE = re.compile(
|
|
60
|
+
rb"(Agent\([^\r\n]*?\xc2\xb7(?:\x1b\[[0-9;]*m|\s)*\d+(?:h\d+m|m(?:\d+s)?|s))(?!\s*\xc2\xb7\s*ctx)"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Regexes to track single-line and multi-line wrapped prompt blocks and cursor coordinates in `agy` output
|
|
64
|
+
PROMPT_HEAD_RE = re.compile(
|
|
65
|
+
rb"(?:\x1b\[K(?:>|\x1b\[[0-9;]*m>\x1b\[m) ([^\r\n\x1b]*)(?:\x1b\[\?7h)?(?:\x1b\[(\d+);(\d+)H|\x1b\[(\d+)G)?|\r\n(?:>|\x1b\[[0-9;]*m>\x1b\[m) ([^\r\n\x1b]*)(?=(?:(?:\x1b\[\?7l)?\r\n(?:\x1b\[\?7l)?(?:\x1b\[K)? [^\r\n\x1b]*(?:\x1b\[K)?)*\r\n(?:\x1b\[[0-9;]*m)*\xe2\x94\x80))"
|
|
66
|
+
)
|
|
67
|
+
PROMPT_CONT_RE = re.compile(
|
|
68
|
+
rb"^(?:\x1b\[\?7l)?\r\n(?:\x1b\[\?7l)?(?:\x1b\[K)? ([^\r\n\x1b]*)(?:\x1b\[K)?(?:\x1b\[\?7h)?(?:\x1b\[(\d+);(\d+)H|\x1b\[(\d+)G)?"
|
|
69
|
+
)
|
|
70
|
+
INCREMENTAL_WRAP_RE = re.compile(
|
|
71
|
+
rb"\r\n(?:\x1b\[\?7l)?(?:\x1b\[K)? ([^\r\n\x1b]+)\x1b\[K\r\n(?:\x1b\[[0-9;]*m)*\xe2\x94\x80"
|
|
72
|
+
)
|
|
73
|
+
ASCII_FIRST_RE = re.compile(
|
|
74
|
+
rb"\x1b\[\?25l(?:\r|\n)*\x1b\[J\x1b\[\d+A\x1b\[2C([\x20-\x7e]+)\r?\n"
|
|
75
|
+
)
|
|
76
|
+
ASCII_INC_RE = re.compile(
|
|
77
|
+
rb"\x1b\[\?25l([\x20-\x7e]+)(?:\x1b\[(\d+)D)?\x1b\[\?25h"
|
|
78
|
+
)
|
|
79
|
+
ASCII_BS_RE = re.compile(rb"\x1b\[\?25l(?:(\x08+)|\x1b\[(\d+)D)\x1b\[\?25h")
|
|
80
|
+
CURSOR_STEP_RE = re.compile(
|
|
81
|
+
rb"\x1b\[(\d+);(\d+)H|\x1b\[(\d+)G|\x1b\[(\d*)D|\x1b\[(\d*)C|\x1b\[(\d*)A|\x1bM|\x08"
|
|
82
|
+
)
|
|
83
|
+
FOOTER_CURSOR_RE = re.compile(rb"\r\x1b\[(\d*)A(?:\x1b\[(\d*)C)?\x1b\[\?25h")
|
|
84
|
+
FOOTER_END_RE = re.compile(rb"\r\x1b\[(\d*)A(?:\x1b\[(\d*)C)?$")
|
|
85
|
+
UP_ESCAPE_RE = re.compile(rb"\x1b\[(\d*)A|\x1bM")
|
|
86
|
+
MOUSE_EVENT_RE = re.compile(
|
|
87
|
+
rb"\x1b\[<(\d+);(\d+);(\d+)([Mm])|\x1b\[M([\x20-\xff])([\x21-\xff])([\x21-\xff])"
|
|
88
|
+
)
|
|
89
|
+
SGR_MOUSE_RE = MOUSE_EVENT_RE
|
|
90
|
+
DSR_REPLY_RE = re.compile(rb"\x1b\[(\d+);(\d+)R")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _char_disp_width(ch: str) -> int:
|
|
94
|
+
if not ch or ord(ch) < 32:
|
|
95
|
+
return 0
|
|
96
|
+
if ord(ch) < 127:
|
|
97
|
+
return 1
|
|
98
|
+
if unicodedata.combining(ch):
|
|
99
|
+
return 0
|
|
100
|
+
return 2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def col_to_char_idx(prompt_text: str, target_col: int) -> int:
|
|
104
|
+
"""
|
|
105
|
+
Map a 1-based terminal visual column (`target_col`, where `> ` or ` ` occupies cols 1..2
|
|
106
|
+
and character 0 starts at col 3) to character index `0..len(prompt_text)`.
|
|
107
|
+
For 2-column CJK characters occupying `[col, col + 1]`, clicking either half (`col` or `col + 1`)
|
|
108
|
+
selects that character (`idx`).
|
|
109
|
+
"""
|
|
110
|
+
if target_col <= 3 or not prompt_text:
|
|
111
|
+
return 0
|
|
112
|
+
col = 3
|
|
113
|
+
for idx, ch in enumerate(prompt_text):
|
|
114
|
+
cw = _char_disp_width(ch)
|
|
115
|
+
if cw <= 0:
|
|
116
|
+
continue
|
|
117
|
+
if target_col < col + cw:
|
|
118
|
+
return idx
|
|
119
|
+
col += cw
|
|
120
|
+
return len(prompt_text)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def char_idx_to_col(prompt_text: str, char_idx: int) -> int:
|
|
124
|
+
"""Map a character index `0..len(prompt_text)` on a prompt line to its 1-based visual column."""
|
|
125
|
+
col = 3
|
|
126
|
+
for ch in prompt_text[:max(0, char_idx)]:
|
|
127
|
+
col += _char_disp_width(ch)
|
|
128
|
+
return col
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class PromptCursorTracker:
|
|
132
|
+
"""
|
|
133
|
+
Tracks single-line and multi-line wrapped prompt text and cursor position from both stdin
|
|
134
|
+
keystrokes and `agy` ANSI stdout, converting visual `(row, col)` mouse clicks into exact
|
|
135
|
+
`\x1b[D` / `\x1b[C` character steps.
|
|
136
|
+
"""
|
|
137
|
+
def __init__(self):
|
|
138
|
+
self.text = ""
|
|
139
|
+
self.lines: list[str] = [""]
|
|
140
|
+
self.top_row: int | None = None
|
|
141
|
+
self.cur_row: int | None = None
|
|
142
|
+
self.cur_col: int = 3
|
|
143
|
+
self.cur_idx: int = 0
|
|
144
|
+
self.cur_line_idx: int = 0
|
|
145
|
+
self.last_prompt_row: int | None = None
|
|
146
|
+
self.term_rows = 40
|
|
147
|
+
self.term_cols = 100
|
|
148
|
+
self._tail_buf = b""
|
|
149
|
+
self._stdin_utf8_buf = b""
|
|
150
|
+
self._stdin_esc_buf = b""
|
|
151
|
+
self._stdin_esc_ts = 0.0
|
|
152
|
+
self._in_paste = False
|
|
153
|
+
self._alt_screen = False
|
|
154
|
+
self.need_dsr = False
|
|
155
|
+
self._last_footer_down = 4
|
|
156
|
+
self._pending_stdin_left = 0
|
|
157
|
+
self._pending_stdin_chars = 0
|
|
158
|
+
self._allow_output_shrink = False
|
|
159
|
+
|
|
160
|
+
def reset(self) -> None:
|
|
161
|
+
self.text = ""
|
|
162
|
+
self.lines = [""]
|
|
163
|
+
self.top_row = None
|
|
164
|
+
self.cur_row = None
|
|
165
|
+
self.cur_col = 3
|
|
166
|
+
self.cur_idx = 0
|
|
167
|
+
self.cur_line_idx = 0
|
|
168
|
+
self.last_prompt_row = None
|
|
169
|
+
self._stdin_utf8_buf = b""
|
|
170
|
+
self._stdin_esc_buf = b""
|
|
171
|
+
self._stdin_esc_ts = 0.0
|
|
172
|
+
self._in_paste = False
|
|
173
|
+
self._pending_stdin_left = 0
|
|
174
|
+
self._pending_stdin_chars = 0
|
|
175
|
+
self._allow_output_shrink = False
|
|
176
|
+
|
|
177
|
+
def _sync_cursor_from_idx(self) -> None:
|
|
178
|
+
if not self.lines:
|
|
179
|
+
self.lines = [""]
|
|
180
|
+
self.cur_idx = max(0, min(len(self.text), self.cur_idx))
|
|
181
|
+
rem = self.cur_idx
|
|
182
|
+
for i, line in enumerate(self.lines):
|
|
183
|
+
if rem < len(line) or i == len(self.lines) - 1:
|
|
184
|
+
self.cur_line_idx = i
|
|
185
|
+
self.cur_col = char_idx_to_col(line, rem)
|
|
186
|
+
break
|
|
187
|
+
rem -= len(line)
|
|
188
|
+
if self.top_row is not None:
|
|
189
|
+
self.cur_row = self.top_row + self.cur_line_idx
|
|
190
|
+
|
|
191
|
+
def _recompute_lines(self) -> None:
|
|
192
|
+
if not self.text:
|
|
193
|
+
self.lines = [""]
|
|
194
|
+
self.cur_idx = 0
|
|
195
|
+
self.cur_line_idx = 0
|
|
196
|
+
self.cur_col = 3
|
|
197
|
+
if self.top_row is not None:
|
|
198
|
+
self.cur_row = self.top_row
|
|
199
|
+
return
|
|
200
|
+
avail_w = max(10, self.term_cols - 2)
|
|
201
|
+
lines: list[str] = []
|
|
202
|
+
cur_line: list[str] = []
|
|
203
|
+
cur_w = 0
|
|
204
|
+
for ch in self.text:
|
|
205
|
+
if ch == "\n":
|
|
206
|
+
lines.append("".join(cur_line))
|
|
207
|
+
cur_line = []
|
|
208
|
+
cur_w = 0
|
|
209
|
+
continue
|
|
210
|
+
cw = _char_disp_width(ch)
|
|
211
|
+
if cur_w + cw > avail_w and cur_line:
|
|
212
|
+
lines.append("".join(cur_line))
|
|
213
|
+
cur_line = [ch]
|
|
214
|
+
cur_w = cw
|
|
215
|
+
else:
|
|
216
|
+
cur_line.append(ch)
|
|
217
|
+
cur_w += cw
|
|
218
|
+
lines.append("".join(cur_line))
|
|
219
|
+
self.lines = lines or [""]
|
|
220
|
+
self._sync_cursor_from_idx()
|
|
221
|
+
|
|
222
|
+
def _move_word_left(self) -> None:
|
|
223
|
+
while self.cur_idx > 0 and self.text[self.cur_idx - 1].isspace():
|
|
224
|
+
self.cur_idx -= 1
|
|
225
|
+
while self.cur_idx > 0 and not self.text[self.cur_idx - 1].isspace():
|
|
226
|
+
self.cur_idx -= 1
|
|
227
|
+
self._sync_cursor_from_idx()
|
|
228
|
+
|
|
229
|
+
def _move_word_right(self) -> None:
|
|
230
|
+
n = len(self.text)
|
|
231
|
+
while self.cur_idx < n and not self.text[self.cur_idx].isspace():
|
|
232
|
+
self.cur_idx += 1
|
|
233
|
+
while self.cur_idx < n and self.text[self.cur_idx].isspace():
|
|
234
|
+
self.cur_idx += 1
|
|
235
|
+
self._sync_cursor_from_idx()
|
|
236
|
+
|
|
237
|
+
def _delete_word_left(self) -> bool:
|
|
238
|
+
old = self.cur_idx
|
|
239
|
+
while self.cur_idx > 0 and self.text[self.cur_idx - 1].isspace():
|
|
240
|
+
self.cur_idx -= 1
|
|
241
|
+
while self.cur_idx > 0 and not self.text[self.cur_idx - 1].isspace():
|
|
242
|
+
self.cur_idx -= 1
|
|
243
|
+
if self.cur_idx < old:
|
|
244
|
+
self.text = self.text[: self.cur_idx] + self.text[old:]
|
|
245
|
+
return True
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
def feed_stdin(self, data: bytes) -> None:
|
|
249
|
+
if not data:
|
|
250
|
+
return
|
|
251
|
+
now = time.monotonic()
|
|
252
|
+
if self._stdin_esc_buf:
|
|
253
|
+
if now - self._stdin_esc_ts <= 0.05 and len(self._stdin_esc_buf) <= 512:
|
|
254
|
+
data = self._stdin_esc_buf + data
|
|
255
|
+
self._stdin_esc_buf = b""
|
|
256
|
+
buf = self._stdin_utf8_buf + data
|
|
257
|
+
self._stdin_utf8_buf = b""
|
|
258
|
+
i = 0
|
|
259
|
+
n = len(buf)
|
|
260
|
+
text_changed = False
|
|
261
|
+
while i < n:
|
|
262
|
+
b = buf[i]
|
|
263
|
+
if b == 0x1B:
|
|
264
|
+
if i + 1 >= n:
|
|
265
|
+
self._stdin_esc_buf = buf[i:]
|
|
266
|
+
self._stdin_esc_ts = now
|
|
267
|
+
break
|
|
268
|
+
nxt = buf[i + 1]
|
|
269
|
+
if nxt == ord("["):
|
|
270
|
+
j = i + 2
|
|
271
|
+
while j < n and not (0x40 <= buf[j] <= 0x7E):
|
|
272
|
+
j += 1
|
|
273
|
+
if j >= n:
|
|
274
|
+
if n - i <= 256:
|
|
275
|
+
self._stdin_esc_buf = buf[i:]
|
|
276
|
+
self._stdin_esc_ts = now
|
|
277
|
+
break
|
|
278
|
+
seq = buf[i : j + 1]
|
|
279
|
+
i = j + 1
|
|
280
|
+
if seq == b"\x1b[200~":
|
|
281
|
+
self._in_paste = True
|
|
282
|
+
elif seq == b"\x1b[201~":
|
|
283
|
+
self._in_paste = False
|
|
284
|
+
elif seq == b"\x1b[D":
|
|
285
|
+
if self.cur_idx > 0:
|
|
286
|
+
self.cur_idx -= 1
|
|
287
|
+
self._pending_stdin_left += 1
|
|
288
|
+
self._sync_cursor_from_idx()
|
|
289
|
+
elif seq == b"\x1b[C":
|
|
290
|
+
self._pending_stdin_left = 0
|
|
291
|
+
if self.cur_idx < len(self.text):
|
|
292
|
+
self.cur_idx += 1
|
|
293
|
+
self._sync_cursor_from_idx()
|
|
294
|
+
elif seq in (b"\x1b[H", b"\x1b[1~", b"\x1b[7~"):
|
|
295
|
+
self._pending_stdin_left = 0
|
|
296
|
+
self.cur_idx = 0
|
|
297
|
+
self._sync_cursor_from_idx()
|
|
298
|
+
elif seq in (b"\x1b[F", b"\x1b[4~", b"\x1b[8~"):
|
|
299
|
+
self._pending_stdin_left = 0
|
|
300
|
+
self.cur_idx = len(self.text)
|
|
301
|
+
self._sync_cursor_from_idx()
|
|
302
|
+
elif seq == b"\x1b[3~":
|
|
303
|
+
self._pending_stdin_left = 0
|
|
304
|
+
if 0 <= self.cur_idx < len(self.text):
|
|
305
|
+
self.text = self.text[: self.cur_idx] + self.text[self.cur_idx + 1 :]
|
|
306
|
+
text_changed = True
|
|
307
|
+
elif seq in (b"\x1b[1;3D", b"\x1b[1;5D"):
|
|
308
|
+
self._pending_stdin_left = 0
|
|
309
|
+
self._move_word_left()
|
|
310
|
+
elif seq in (b"\x1b[1;3C", b"\x1b[1;5C"):
|
|
311
|
+
self._pending_stdin_left = 0
|
|
312
|
+
self._move_word_right()
|
|
313
|
+
elif seq in (b"\x1b[A", b"\x1b[B"):
|
|
314
|
+
self._pending_stdin_left = 0
|
|
315
|
+
self._allow_output_shrink = True
|
|
316
|
+
continue
|
|
317
|
+
if nxt in (ord("]"), ord("P"), ord("_"), ord("^"), ord("X")):
|
|
318
|
+
# OSC (`\x1b]...\x07` or `\x1b\\`), DCS (`\x1bP...\x1b\\`), APC/PM/SOS
|
|
319
|
+
j = i + 2
|
|
320
|
+
found_end = False
|
|
321
|
+
while j < n:
|
|
322
|
+
if buf[j] == 0x07:
|
|
323
|
+
i = j + 1
|
|
324
|
+
found_end = True
|
|
325
|
+
break
|
|
326
|
+
if buf[j] == 0x1B and j + 1 < n and buf[j + 1] == ord("\\"):
|
|
327
|
+
i = j + 2
|
|
328
|
+
found_end = True
|
|
329
|
+
break
|
|
330
|
+
j += 1
|
|
331
|
+
if not found_end:
|
|
332
|
+
if n - i <= 512:
|
|
333
|
+
self._stdin_esc_buf = buf[i:]
|
|
334
|
+
self._stdin_esc_ts = now
|
|
335
|
+
break
|
|
336
|
+
continue
|
|
337
|
+
if nxt == ord("O"):
|
|
338
|
+
if i + 2 < n:
|
|
339
|
+
code = buf[i + 2]
|
|
340
|
+
i += 3
|
|
341
|
+
if code == ord("D") and self.cur_idx > 0:
|
|
342
|
+
self.cur_idx -= 1
|
|
343
|
+
self._pending_stdin_left += 1
|
|
344
|
+
self._sync_cursor_from_idx()
|
|
345
|
+
elif code == ord("C") and self.cur_idx < len(self.text):
|
|
346
|
+
self._pending_stdin_left = 0
|
|
347
|
+
self.cur_idx += 1
|
|
348
|
+
self._sync_cursor_from_idx()
|
|
349
|
+
elif code == ord("H"):
|
|
350
|
+
self._pending_stdin_left = 0
|
|
351
|
+
self.cur_idx = 0
|
|
352
|
+
self._sync_cursor_from_idx()
|
|
353
|
+
elif code == ord("F"):
|
|
354
|
+
self._pending_stdin_left = 0
|
|
355
|
+
self.cur_idx = len(self.text)
|
|
356
|
+
self._sync_cursor_from_idx()
|
|
357
|
+
continue
|
|
358
|
+
self._stdin_esc_buf = buf[i:]
|
|
359
|
+
self._stdin_esc_ts = now
|
|
360
|
+
break
|
|
361
|
+
i += 2
|
|
362
|
+
self._pending_stdin_left = 0
|
|
363
|
+
if nxt in (ord("b"), ord("B")):
|
|
364
|
+
self._move_word_left()
|
|
365
|
+
elif nxt in (ord("f"), ord("F")):
|
|
366
|
+
self._move_word_right()
|
|
367
|
+
elif nxt in (0x7F, 0x08):
|
|
368
|
+
if self._delete_word_left():
|
|
369
|
+
text_changed = True
|
|
370
|
+
continue
|
|
371
|
+
if b in (0x0D, 0x0A):
|
|
372
|
+
if self._in_paste:
|
|
373
|
+
self.text = self.text[: self.cur_idx] + "\n" + self.text[self.cur_idx :]
|
|
374
|
+
self.cur_idx += 1
|
|
375
|
+
text_changed = True
|
|
376
|
+
else:
|
|
377
|
+
self.reset()
|
|
378
|
+
text_changed = False
|
|
379
|
+
i += 1
|
|
380
|
+
elif b in (0x7F, 0x08):
|
|
381
|
+
self._pending_stdin_left = 0
|
|
382
|
+
if self.cur_idx > 0:
|
|
383
|
+
self.text = self.text[: self.cur_idx - 1] + self.text[self.cur_idx :]
|
|
384
|
+
self.cur_idx -= 1
|
|
385
|
+
text_changed = True
|
|
386
|
+
i += 1
|
|
387
|
+
elif b == 0x01:
|
|
388
|
+
self._pending_stdin_left = 0
|
|
389
|
+
self.cur_idx = 0
|
|
390
|
+
self._sync_cursor_from_idx()
|
|
391
|
+
i += 1
|
|
392
|
+
elif b == 0x05:
|
|
393
|
+
self._pending_stdin_left = 0
|
|
394
|
+
self.cur_idx = len(self.text)
|
|
395
|
+
self._sync_cursor_from_idx()
|
|
396
|
+
i += 1
|
|
397
|
+
elif b == 0x02:
|
|
398
|
+
if self.cur_idx > 0:
|
|
399
|
+
self.cur_idx -= 1
|
|
400
|
+
self._pending_stdin_left += 1
|
|
401
|
+
self._sync_cursor_from_idx()
|
|
402
|
+
i += 1
|
|
403
|
+
elif b == 0x06:
|
|
404
|
+
self._pending_stdin_left = 0
|
|
405
|
+
if self.cur_idx < len(self.text):
|
|
406
|
+
self.cur_idx += 1
|
|
407
|
+
self._sync_cursor_from_idx()
|
|
408
|
+
i += 1
|
|
409
|
+
elif b == 0x04:
|
|
410
|
+
self._pending_stdin_left = 0
|
|
411
|
+
if 0 <= self.cur_idx < len(self.text):
|
|
412
|
+
self.text = self.text[: self.cur_idx] + self.text[self.cur_idx + 1 :]
|
|
413
|
+
text_changed = True
|
|
414
|
+
i += 1
|
|
415
|
+
elif b == 0x0B:
|
|
416
|
+
self._pending_stdin_left = 0
|
|
417
|
+
if self.cur_idx < len(self.text):
|
|
418
|
+
self.text = self.text[: self.cur_idx]
|
|
419
|
+
text_changed = True
|
|
420
|
+
i += 1
|
|
421
|
+
elif b == 0x15:
|
|
422
|
+
self._pending_stdin_left = 0
|
|
423
|
+
self.text = ""
|
|
424
|
+
self.cur_idx = 0
|
|
425
|
+
text_changed = True
|
|
426
|
+
i += 1
|
|
427
|
+
elif b == 0x17:
|
|
428
|
+
self._pending_stdin_left = 0
|
|
429
|
+
if self._delete_word_left():
|
|
430
|
+
text_changed = True
|
|
431
|
+
i += 1
|
|
432
|
+
elif b == 0x03:
|
|
433
|
+
self.reset()
|
|
434
|
+
text_changed = False
|
|
435
|
+
i += 1
|
|
436
|
+
elif b == 0x09:
|
|
437
|
+
self._pending_stdin_left = 0
|
|
438
|
+
self._allow_output_shrink = True
|
|
439
|
+
i += 1
|
|
440
|
+
elif b < 0x20:
|
|
441
|
+
i += 1
|
|
442
|
+
else:
|
|
443
|
+
j = i
|
|
444
|
+
while j < n and buf[j] >= 0x20 and buf[j] != 0x7F and buf[j] != 0x1B:
|
|
445
|
+
j += 1
|
|
446
|
+
chunk = buf[i:j]
|
|
447
|
+
i = j
|
|
448
|
+
self._pending_stdin_left = 0
|
|
449
|
+
s = ""
|
|
450
|
+
for trim in range(min(4, len(chunk))):
|
|
451
|
+
try:
|
|
452
|
+
s = (chunk[: len(chunk) - trim] if trim else chunk).decode("utf-8")
|
|
453
|
+
if trim and i == n:
|
|
454
|
+
self._stdin_utf8_buf = chunk[len(chunk) - trim :]
|
|
455
|
+
break
|
|
456
|
+
except UnicodeDecodeError:
|
|
457
|
+
s = ""
|
|
458
|
+
else:
|
|
459
|
+
s = chunk.decode("utf-8", errors="ignore")
|
|
460
|
+
if s:
|
|
461
|
+
self.text = self.text[: self.cur_idx] + s + self.text[self.cur_idx :]
|
|
462
|
+
self.cur_idx += len(s)
|
|
463
|
+
self._pending_stdin_chars += len(s)
|
|
464
|
+
text_changed = True
|
|
465
|
+
if text_changed:
|
|
466
|
+
self._recompute_lines()
|
|
467
|
+
|
|
468
|
+
def _row_col_to_char_idx(self, row: int | None, col: int, line_idx_hint: int | None = None) -> int:
|
|
469
|
+
if not self.lines:
|
|
470
|
+
return 0
|
|
471
|
+
if len(self.lines) == 1:
|
|
472
|
+
return col_to_char_idx(self.lines[0], col)
|
|
473
|
+
if self.top_row is not None and row is not None:
|
|
474
|
+
line_idx = max(0, min(len(self.lines) - 1, row - self.top_row))
|
|
475
|
+
elif line_idx_hint is not None:
|
|
476
|
+
line_idx = max(0, min(len(self.lines) - 1, line_idx_hint))
|
|
477
|
+
else:
|
|
478
|
+
line_idx = max(0, min(len(self.lines) - 1, self.cur_line_idx))
|
|
479
|
+
prefix_chars = sum(len(x) for x in self.lines[:line_idx])
|
|
480
|
+
return prefix_chars + col_to_char_idx(self.lines[line_idx], col)
|
|
481
|
+
|
|
482
|
+
def update_from_output(self, raw_bytes: bytes):
|
|
483
|
+
if not raw_bytes:
|
|
484
|
+
return
|
|
485
|
+
if b"\x1b[?1049h" in raw_bytes:
|
|
486
|
+
self._alt_screen = True
|
|
487
|
+
if b"\x1b[?1049l" in raw_bytes:
|
|
488
|
+
self._alt_screen = False
|
|
489
|
+
combined = self._tail_buf + raw_bytes
|
|
490
|
+
if len(combined) > 8192:
|
|
491
|
+
combined = combined[-8192:]
|
|
492
|
+
new_start = max(0, len(combined) - len(raw_bytes))
|
|
493
|
+
self._tail_buf = combined
|
|
494
|
+
buf = combined
|
|
495
|
+
matched_prompt_in_chunk = False
|
|
496
|
+
footer_handled_in_head = False
|
|
497
|
+
|
|
498
|
+
for m in PROMPT_HEAD_RE.finditer(buf):
|
|
499
|
+
raw_l0 = m.group(1) if m.group(1) is not None else m.group(5)
|
|
500
|
+
try:
|
|
501
|
+
line0 = (raw_l0 or b"").decode("utf-8", errors="ignore")
|
|
502
|
+
except Exception:
|
|
503
|
+
line0 = ""
|
|
504
|
+
r0 = int(m.group(2)) if m.group(2) else None
|
|
505
|
+
c0 = (
|
|
506
|
+
int(m.group(3))
|
|
507
|
+
if m.group(3)
|
|
508
|
+
else (int(m.group(4)) if m.group(4) else char_idx_to_col(line0, len(line0)))
|
|
509
|
+
)
|
|
510
|
+
lines = [line0]
|
|
511
|
+
cur_r = r0
|
|
512
|
+
cur_c = c0
|
|
513
|
+
pos = m.end()
|
|
514
|
+
while True:
|
|
515
|
+
cm = PROMPT_CONT_RE.match(buf[pos:])
|
|
516
|
+
if not cm:
|
|
517
|
+
break
|
|
518
|
+
try:
|
|
519
|
+
cont_txt = cm.group(1).decode("utf-8", errors="ignore")
|
|
520
|
+
except Exception:
|
|
521
|
+
cont_txt = ""
|
|
522
|
+
if cont_txt.startswith("─"):
|
|
523
|
+
break
|
|
524
|
+
lines.append(cont_txt)
|
|
525
|
+
if cm.group(2):
|
|
526
|
+
cur_r = int(cm.group(2))
|
|
527
|
+
cur_c = int(cm.group(3))
|
|
528
|
+
if r0 is None:
|
|
529
|
+
r0 = max(1, cur_r - (len(lines) - 1))
|
|
530
|
+
elif cm.group(4):
|
|
531
|
+
if cur_r is not None:
|
|
532
|
+
cur_r += 1
|
|
533
|
+
cur_c = int(cm.group(4))
|
|
534
|
+
else:
|
|
535
|
+
if cur_r is not None:
|
|
536
|
+
cur_r += 1
|
|
537
|
+
cur_c = char_idx_to_col(cont_txt, len(cont_txt))
|
|
538
|
+
pos += cm.end()
|
|
539
|
+
|
|
540
|
+
cur_line_idx = len(lines) - 1
|
|
541
|
+
after_prompt = buf[pos:]
|
|
542
|
+
show_idx = after_prompt.find(b"\x1b[?25h")
|
|
543
|
+
if show_idx != -1:
|
|
544
|
+
frame_slice = after_prompt[:show_idx]
|
|
545
|
+
end_with_tail = pos + show_idx + 6
|
|
546
|
+
else:
|
|
547
|
+
frame_slice = after_prompt[:64]
|
|
548
|
+
end_with_tail = pos + len(frame_slice)
|
|
549
|
+
|
|
550
|
+
if end_with_tail <= new_start:
|
|
551
|
+
continue
|
|
552
|
+
|
|
553
|
+
# Split at the first \r or \n so footer excursions (\n\n...\x1b[3A\x1b[58D) never corrupt prompt cur_c
|
|
554
|
+
nl_pos = -1
|
|
555
|
+
for k, ch_b in enumerate(frame_slice):
|
|
556
|
+
if ch_b in (0x0A, 0x0D):
|
|
557
|
+
nl_pos = k
|
|
558
|
+
break
|
|
559
|
+
intra_prompt_slice = frame_slice[:nl_pos] if nl_pos != -1 else frame_slice
|
|
560
|
+
footer_slice = frame_slice[nl_pos:] if nl_pos != -1 else b""
|
|
561
|
+
|
|
562
|
+
for sm in CURSOR_STEP_RE.finditer(intra_prompt_slice):
|
|
563
|
+
tok = sm.group(0)
|
|
564
|
+
if sm.group(1): # \x1b[row;colH
|
|
565
|
+
cand_r = int(sm.group(1))
|
|
566
|
+
cand_c = int(sm.group(2))
|
|
567
|
+
if r0 is None:
|
|
568
|
+
r0 = max(1, cand_r - (len(lines) - 1))
|
|
569
|
+
if r0 <= cand_r < r0 + len(lines):
|
|
570
|
+
cur_r, cur_c = cand_r, cand_c
|
|
571
|
+
cur_line_idx = cand_r - r0
|
|
572
|
+
elif sm.group(3): # \x1b[colG
|
|
573
|
+
cur_c = int(sm.group(3))
|
|
574
|
+
elif sm.group(4) is not None: # \x1b[nD
|
|
575
|
+
n = int(sm.group(4)) if sm.group(4) else 1
|
|
576
|
+
cur_c = max(3, cur_c - n)
|
|
577
|
+
elif sm.group(5) is not None: # \x1b[nC
|
|
578
|
+
n = int(sm.group(5)) if sm.group(5) else 1
|
|
579
|
+
cur_c += n
|
|
580
|
+
elif sm.group(6) is not None: # \x1b[nA
|
|
581
|
+
n = int(sm.group(6)) if sm.group(6) else 1
|
|
582
|
+
if cur_r is not None:
|
|
583
|
+
cur_r = max(1, cur_r - n)
|
|
584
|
+
cur_line_idx = max(0, cur_line_idx - n)
|
|
585
|
+
elif tok == b"\x1bM":
|
|
586
|
+
if cur_r is not None:
|
|
587
|
+
cur_r = max(1, cur_r - 1)
|
|
588
|
+
cur_line_idx = max(0, cur_line_idx - 1)
|
|
589
|
+
elif tok == b"\x08":
|
|
590
|
+
cur_c = max(3, cur_c - 1)
|
|
591
|
+
|
|
592
|
+
if footer_slice and show_idx != -1:
|
|
593
|
+
fm_end = FOOTER_END_RE.search(footer_slice)
|
|
594
|
+
if fm_end:
|
|
595
|
+
footer_body = footer_slice[: fm_end.start()]
|
|
596
|
+
up_in_body = sum(
|
|
597
|
+
(int(um.group(1)) if um.group(1) else 1)
|
|
598
|
+
for um in UP_ESCAPE_RE.finditer(footer_body)
|
|
599
|
+
)
|
|
600
|
+
down_rows = max(0, footer_body.count(b"\n") - up_in_body)
|
|
601
|
+
if down_rows > 0:
|
|
602
|
+
self._last_footer_down = down_rows
|
|
603
|
+
up_rows = int(fm_end.group(1)) if fm_end.group(1) else 1
|
|
604
|
+
right_shift = int(fm_end.group(2)) if fm_end.group(2) else (1 if fm_end.group(2) is not None else 0)
|
|
605
|
+
cur_line_idx = max(
|
|
606
|
+
0, min(len(lines) - 1, (len(lines) - 1) + down_rows - up_rows)
|
|
607
|
+
)
|
|
608
|
+
if r0 is not None:
|
|
609
|
+
cur_r = r0 + cur_line_idx
|
|
610
|
+
if right_shift >= 2:
|
|
611
|
+
cur_c = 1 + right_shift
|
|
612
|
+
footer_handled_in_head = True
|
|
613
|
+
|
|
614
|
+
out_text = "".join(lines)
|
|
615
|
+
if r0 is not None:
|
|
616
|
+
self.top_row = r0
|
|
617
|
+
if (
|
|
618
|
+
self.text
|
|
619
|
+
and self.text.startswith(out_text)
|
|
620
|
+
and len(self.text) > len(out_text)
|
|
621
|
+
and not self._allow_output_shrink
|
|
622
|
+
):
|
|
623
|
+
self._pending_stdin_chars = len(self.text) - len(out_text)
|
|
624
|
+
continue
|
|
625
|
+
|
|
626
|
+
self._allow_output_shrink = False
|
|
627
|
+
self._pending_stdin_left = 0
|
|
628
|
+
self._pending_stdin_chars = 0
|
|
629
|
+
self.lines = lines
|
|
630
|
+
self.text = out_text
|
|
631
|
+
self.cur_row = cur_r
|
|
632
|
+
self.cur_line_idx = cur_line_idx
|
|
633
|
+
self.cur_col = max(3, cur_c)
|
|
634
|
+
self.cur_idx = self._row_col_to_char_idx(self.cur_row, self.cur_col, line_idx_hint=cur_line_idx)
|
|
635
|
+
matched_prompt_in_chunk = True
|
|
636
|
+
if self.top_row is None and self.last_prompt_row is None:
|
|
637
|
+
self.need_dsr = True
|
|
638
|
+
|
|
639
|
+
matched_wrap_in_chunk = False
|
|
640
|
+
if not matched_prompt_in_chunk:
|
|
641
|
+
for wm in INCREMENTAL_WRAP_RE.finditer(raw_bytes):
|
|
642
|
+
try:
|
|
643
|
+
cont_txt = wm.group(1).decode("utf-8", errors="ignore")
|
|
644
|
+
except Exception:
|
|
645
|
+
cont_txt = ""
|
|
646
|
+
if cont_txt and not cont_txt.startswith("─"):
|
|
647
|
+
if self.text.endswith(cont_txt) and len(self.text) > len(cont_txt):
|
|
648
|
+
prefix = self.text[: -len(cont_txt)]
|
|
649
|
+
self.lines = [prefix, cont_txt]
|
|
650
|
+
self._sync_cursor_from_idx()
|
|
651
|
+
matched_wrap_in_chunk = True
|
|
652
|
+
|
|
653
|
+
for fm1 in ASCII_FIRST_RE.finditer(raw_bytes):
|
|
654
|
+
try:
|
|
655
|
+
first_s = fm1.group(1).decode("utf-8", errors="ignore")
|
|
656
|
+
except Exception:
|
|
657
|
+
first_s = ""
|
|
658
|
+
if not first_s:
|
|
659
|
+
continue
|
|
660
|
+
if self._pending_stdin_chars > 0:
|
|
661
|
+
used = min(self._pending_stdin_chars, len(first_s))
|
|
662
|
+
self._pending_stdin_chars -= used
|
|
663
|
+
first_s = first_s[used:]
|
|
664
|
+
if first_s:
|
|
665
|
+
self.text = self.text[: self.cur_idx] + first_s + self.text[self.cur_idx :]
|
|
666
|
+
self.cur_idx += len(first_s)
|
|
667
|
+
self._recompute_lines()
|
|
668
|
+
|
|
669
|
+
for im in ASCII_INC_RE.finditer(raw_bytes):
|
|
670
|
+
try:
|
|
671
|
+
inc_s = im.group(1).decode("utf-8", errors="ignore")
|
|
672
|
+
except Exception:
|
|
673
|
+
inc_s = ""
|
|
674
|
+
back_n = int(im.group(2)) if im.group(2) else 0
|
|
675
|
+
ins_len = max(0, len(inc_s) - back_n)
|
|
676
|
+
ins_s = inc_s[:ins_len]
|
|
677
|
+
if not ins_s:
|
|
678
|
+
continue
|
|
679
|
+
if self._pending_stdin_chars > 0:
|
|
680
|
+
used = min(self._pending_stdin_chars, len(ins_s))
|
|
681
|
+
self._pending_stdin_chars -= used
|
|
682
|
+
ins_s = ins_s[used:]
|
|
683
|
+
if ins_s:
|
|
684
|
+
self.text = self.text[: self.cur_idx] + ins_s + self.text[self.cur_idx :]
|
|
685
|
+
self.cur_idx += len(ins_s)
|
|
686
|
+
self._recompute_lines()
|
|
687
|
+
|
|
688
|
+
for bm in ASCII_BS_RE.finditer(raw_bytes):
|
|
689
|
+
n_bs = len(bm.group(1)) if bm.group(1) else int(bm.group(2))
|
|
690
|
+
if self._pending_stdin_left > 0:
|
|
691
|
+
used = min(self._pending_stdin_left, n_bs)
|
|
692
|
+
self._pending_stdin_left -= used
|
|
693
|
+
n_bs -= used
|
|
694
|
+
if n_bs > 0:
|
|
695
|
+
self.cur_idx = max(0, self.cur_idx - n_bs)
|
|
696
|
+
self._sync_cursor_from_idx()
|
|
697
|
+
|
|
698
|
+
if not footer_handled_in_head:
|
|
699
|
+
for fm in FOOTER_CURSOR_RE.finditer(raw_bytes):
|
|
700
|
+
up_rows = int(fm.group(1)) if fm.group(1) else 1
|
|
701
|
+
right_shift = int(fm.group(2)) if fm.group(2) else (1 if fm.group(2) is not None else 0)
|
|
702
|
+
if right_shift >= 2 and (matched_prompt_in_chunk or matched_wrap_in_chunk or not self.text):
|
|
703
|
+
down_rows = self._last_footer_down
|
|
704
|
+
target_line_idx = max(
|
|
705
|
+
0, min(len(self.lines) - 1, (len(self.lines) - 1) + down_rows - up_rows)
|
|
706
|
+
)
|
|
707
|
+
self.cur_line_idx = target_line_idx
|
|
708
|
+
self.cur_col = 1 + right_shift
|
|
709
|
+
self.cur_idx = self._row_col_to_char_idx(
|
|
710
|
+
self.cur_row, self.cur_col, line_idx_hint=target_line_idx
|
|
711
|
+
)
|
|
712
|
+
self._sync_cursor_from_idx()
|
|
713
|
+
if self.top_row is None and self.last_prompt_row is None:
|
|
714
|
+
self.need_dsr = True
|
|
715
|
+
|
|
716
|
+
def arrows_for_click(self, click_col: int, click_row: int) -> bytes:
|
|
717
|
+
"""Return `\x1b[D` or `\x1b[C` sequence to move cursor to `(click_row, click_col)` in prompt."""
|
|
718
|
+
if self._alt_screen or not self.text:
|
|
719
|
+
return b""
|
|
720
|
+
if self.top_row is not None:
|
|
721
|
+
# Accept clicks on any line of the prompt (`top_row .. top_row + len(lines) - 1`) with 1-row tolerance
|
|
722
|
+
if not (self.top_row - 1 <= click_row <= self.top_row + len(self.lines)):
|
|
723
|
+
return b""
|
|
724
|
+
line_idx = max(0, min(len(self.lines) - 1, click_row - self.top_row))
|
|
725
|
+
else:
|
|
726
|
+
if not (1 <= click_row <= self.term_rows):
|
|
727
|
+
return b""
|
|
728
|
+
if len(self.lines) == 1:
|
|
729
|
+
self.last_prompt_row = click_row
|
|
730
|
+
line_idx = 0
|
|
731
|
+
else:
|
|
732
|
+
if self.last_prompt_row is None or click_row < self.last_prompt_row:
|
|
733
|
+
self.last_prompt_row = click_row
|
|
734
|
+
elif click_row - self.last_prompt_row > len(self.lines) + 2:
|
|
735
|
+
self.last_prompt_row = click_row - (len(self.lines) - 1)
|
|
736
|
+
line_idx = max(0, min(len(self.lines) - 1, click_row - self.last_prompt_row))
|
|
737
|
+
|
|
738
|
+
prefix_chars = sum(len(x) for x in self.lines[:line_idx])
|
|
739
|
+
target_idx = prefix_chars + col_to_char_idx(self.lines[line_idx], click_col)
|
|
740
|
+
delta = target_idx - self.cur_idx
|
|
741
|
+
self.cur_idx = target_idx
|
|
742
|
+
self.cur_line_idx = line_idx
|
|
743
|
+
if self.top_row is not None:
|
|
744
|
+
self.cur_row = max(self.top_row, min(self.top_row + len(self.lines) - 1, click_row))
|
|
745
|
+
self.cur_col = char_idx_to_col(self.lines[line_idx], target_idx - prefix_chars)
|
|
746
|
+
if delta < 0:
|
|
747
|
+
self._pending_stdin_left += -delta
|
|
748
|
+
return b"\x1b[D" * (-delta)
|
|
749
|
+
if delta > 0:
|
|
750
|
+
self._pending_stdin_left = 0
|
|
751
|
+
return b"\x1b[C" * delta
|
|
752
|
+
return b""
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _get_subagent_badge() -> tuple[str, str, str, str]:
|
|
756
|
+
now = time.monotonic()
|
|
757
|
+
if now - SUB_LIVE_CACHE["ts"] < 1.5:
|
|
758
|
+
return (
|
|
759
|
+
SUB_LIVE_CACHE["model"],
|
|
760
|
+
SUB_LIVE_CACHE["role"],
|
|
761
|
+
SUB_LIVE_CACHE["ctx"],
|
|
762
|
+
SUB_LIVE_CACHE["ext_tag"],
|
|
763
|
+
)
|
|
764
|
+
SUB_LIVE_CACHE["ts"] = now
|
|
765
|
+
model_str, role_str, ctx_str, ext_tag = "Argon", "", "", ""
|
|
766
|
+
allow_live = _IN_PTY_MAIN or (SUB_LIVE_PATH != "/tmp/agy-subagent-live.json") or (ACTIVE_EXT_PATH != "/tmp/agy-active-ext.json")
|
|
767
|
+
if allow_live:
|
|
768
|
+
try:
|
|
769
|
+
if os.path.exists(SUB_LIVE_PATH):
|
|
770
|
+
with open(SUB_LIVE_PATH, encoding="utf-8") as f:
|
|
771
|
+
d = json.load(f)
|
|
772
|
+
by_cid = d.get("by_cid") or {}
|
|
773
|
+
entry = by_cid.get(CURRENT_CID) if (CURRENT_CID and CURRENT_CID in by_cid) else d
|
|
774
|
+
if isinstance(entry, dict) and time.time() - float(entry.get("ts") or 0) < 90:
|
|
775
|
+
model_str = str(entry.get("model") or "Argon")
|
|
776
|
+
raw_role = str(entry.get("role") or "").strip()
|
|
777
|
+
role_str = re.sub(r"^(?:Flash|Argon)\s*[·:]\s*", "", raw_role, flags=re.I)
|
|
778
|
+
ctx_str = str(entry.get("ctx") or "")
|
|
779
|
+
except Exception:
|
|
780
|
+
pass
|
|
781
|
+
try:
|
|
782
|
+
if os.path.exists(ACTIVE_EXT_PATH):
|
|
783
|
+
with open(ACTIVE_EXT_PATH, encoding="utf-8") as f:
|
|
784
|
+
ae = json.load(f)
|
|
785
|
+
pid = int(ae.get("pid") or 0)
|
|
786
|
+
if pid > 0:
|
|
787
|
+
os.kill(pid, 0)
|
|
788
|
+
em = str(ae.get("model") or ae.get("tool") or "ext").replace("claude-", "")
|
|
789
|
+
et = str(ae.get("title") or "顾问").strip()[:12]
|
|
790
|
+
el = max(0, int(time.time() - float(ae.get("start_ts") or time.time())))
|
|
791
|
+
ext_tag = f" \x1b[38;5;141m◆\x1b[m Ext({em}: {et}) · {el}s"
|
|
792
|
+
except Exception:
|
|
793
|
+
pass
|
|
794
|
+
SUB_LIVE_CACHE["model"] = model_str
|
|
795
|
+
SUB_LIVE_CACHE["role"] = role_str
|
|
796
|
+
SUB_LIVE_CACHE["ctx"] = ctx_str
|
|
797
|
+
SUB_LIVE_CACHE["ext_tag"] = ext_tag
|
|
798
|
+
return model_str, role_str, ctx_str, ext_tag
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
REPLACEMENTS = (
|
|
802
|
+
# 1. User message lines: full-width dark gray background box (48;5;236) + white text (38;5;255)
|
|
803
|
+
(b"\x1b[K\x1b[38;5;111;1m", b"\x1b[48;5;236m\x1b[K\x1b[38;5;255m"),
|
|
804
|
+
(b"\x1b[K\x1b[38;2;138;180;248;1m", b"\x1b[48;5;236m\x1b[K\x1b[38;5;255m"),
|
|
805
|
+
# 2. Inline code: remove background block (48;5;236 -> 49), use soft lavender foreground (38;5;147)
|
|
806
|
+
(b"\x1b[38;5;111;48;5;236", b"\x1b[38;5;147;49"),
|
|
807
|
+
(b"\x1b[38;2;138;180;248;48;5;236", b"\x1b[38;5;147;49"),
|
|
808
|
+
(b"\x1b[38;2;174;203;250;48;2;4;30;73", b"\x1b[38;5;147;49"),
|
|
809
|
+
(b"\x1b[38;2;138;180;248;48;2;4;30;73", b"\x1b[38;5;147;49"),
|
|
810
|
+
# 3. Markdown headings: pure white bold instead of bright cyan (38;5;39)
|
|
811
|
+
(b"\x1b[38;5;39;1m", b"\x1b[38;5;255;1m"),
|
|
812
|
+
(b"\x1b[38;5;39;49m", b"\x1b[38;5;255;49;1m"),
|
|
813
|
+
# 4. Assistant body text: crisp pure white (255) instead of bluish-tinted gray (189 / #e8eaed)
|
|
814
|
+
(b"\x1b[38;5;189m", b"\x1b[38;5;255m"),
|
|
815
|
+
(b"\x1b[38;5;189;", b"\x1b[38;5;255;"),
|
|
816
|
+
(b"\x1b[38;2;232;234;237m", b"\x1b[38;5;255m"),
|
|
817
|
+
(b"\x1b[38;2;232;234;237;", b"\x1b[38;5;255;"),
|
|
818
|
+
# 5. Input prompt `>`: crisp white instead of bright blue
|
|
819
|
+
(b"\x1b[38;5;111m>\x1b[m", b"\x1b[38;5;255;1m>\x1b[m"),
|
|
820
|
+
(b"\x1b[38;2;138;180;248m>\x1b[m", b"\x1b[38;5;255;1m>\x1b[m"),
|
|
821
|
+
# 6. Prevent `agy` from disabling outer terminal SGR mouse reporting (`?1000l` / `?1002l` / `?1003l` / `?1006l`)
|
|
822
|
+
(b"\x1b[?1000l", b"\x1b[?1000h\x1b[?1006h"),
|
|
823
|
+
(b"\x1b[?1002l", b"\x1b[?1000h\x1b[?1006h"),
|
|
824
|
+
(b"\x1b[?1003l", b"\x1b[?1000h\x1b[?1006h"),
|
|
825
|
+
(b"\x1b[?1006l", b"\x1b[?1000h\x1b[?1006h"),
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
MAX_PAT_LEN = max(len(src) for src, _ in REPLACEMENTS)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def transform_chunk(data: bytes, carry: bytes = b"") -> tuple[bytes, bytes]:
|
|
832
|
+
"""
|
|
833
|
+
Transform ANSI sequences in `carry + data`, holding back ONLY a trailing prefix that
|
|
834
|
+
could complete one of `REPLACEMENTS` across read boundaries (never trapping `\x1b[?25h`).
|
|
835
|
+
"""
|
|
836
|
+
buf = carry + data
|
|
837
|
+
hold = b""
|
|
838
|
+
scan_start = max(0, len(buf) - MAX_PAT_LEN)
|
|
839
|
+
idx = buf.find(b"\x1b", scan_start)
|
|
840
|
+
while idx != -1:
|
|
841
|
+
tail = buf[idx:]
|
|
842
|
+
if any(src.startswith(tail) and tail != src for src, _ in REPLACEMENTS):
|
|
843
|
+
buf, hold = buf[:idx], tail
|
|
844
|
+
break
|
|
845
|
+
idx = buf.find(b"\x1b", idx + 1)
|
|
846
|
+
for src, dst in REPLACEMENTS:
|
|
847
|
+
if src in buf:
|
|
848
|
+
buf = buf.replace(src, dst)
|
|
849
|
+
if b"Agent(" in buf:
|
|
850
|
+
sub_mod, sub_role, sub_ctx, ext_tag = _get_subagent_badge()
|
|
851
|
+
if b"Agent(self:" in buf:
|
|
852
|
+
if sub_role:
|
|
853
|
+
buf = AGENT_ROLE_RE.sub(f"Agent({sub_mod}: {sub_role})".encode("utf-8"), buf)
|
|
854
|
+
else:
|
|
855
|
+
buf = buf.replace(b"Agent(self:", f"Agent({sub_mod}:".encode("utf-8"))
|
|
856
|
+
if (sub_ctx or ext_tag) and b"\xc2\xb7" in buf:
|
|
857
|
+
suffix_str = (f" · ctx {sub_ctx}" if sub_ctx else "") + ext_tag
|
|
858
|
+
buf = AGENT_TIMER_RE.sub(rb"\1" + suffix_str.encode("utf-8"), buf)
|
|
859
|
+
if b"\x1b[?1049l" in buf:
|
|
860
|
+
buf = buf + b"\x1b[?1000h\x1b[?1006h"
|
|
861
|
+
return buf, hold
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def filter_stdin_mouse(in_bytes: bytes, tracker: PromptCursorTracker) -> bytes:
|
|
865
|
+
"""
|
|
866
|
+
Intercept SGR (`\x1b[<btn;col;rowM/m`) and X10 (`\x1b[M<cb><cx><cy>`) mouse sequences
|
|
867
|
+
and DSR replies (`\x1b[row;colR`), swallowing non-left-press mouse events so zero mouse
|
|
868
|
+
bytes ever leak into `agy`.
|
|
869
|
+
"""
|
|
870
|
+
if tracker._alt_screen:
|
|
871
|
+
return in_bytes
|
|
872
|
+
|
|
873
|
+
if b"\x1b[" in in_bytes and b"R" in in_bytes:
|
|
874
|
+
for dm in DSR_REPLY_RE.finditer(in_bytes):
|
|
875
|
+
dsr_row = int(dm.group(1))
|
|
876
|
+
if 1 <= dsr_row <= tracker.term_rows:
|
|
877
|
+
tracker.last_prompt_row = max(1, dsr_row - tracker.cur_line_idx)
|
|
878
|
+
in_bytes = DSR_REPLY_RE.sub(b"", in_bytes)
|
|
879
|
+
if not in_bytes:
|
|
880
|
+
return b""
|
|
881
|
+
|
|
882
|
+
if b"\x1b[<" not in in_bytes and b"\x1b[M" not in in_bytes:
|
|
883
|
+
tracker.feed_stdin(in_bytes)
|
|
884
|
+
return in_bytes
|
|
885
|
+
|
|
886
|
+
out = bytearray()
|
|
887
|
+
last_end = 0
|
|
888
|
+
for m in MOUSE_EVENT_RE.finditer(in_bytes):
|
|
889
|
+
seg = in_bytes[last_end:m.start()]
|
|
890
|
+
if seg:
|
|
891
|
+
tracker.feed_stdin(seg)
|
|
892
|
+
out.extend(seg)
|
|
893
|
+
last_end = m.end()
|
|
894
|
+
if tracker._in_paste:
|
|
895
|
+
out.extend(m.group(0))
|
|
896
|
+
continue
|
|
897
|
+
if m.group(1) is not None:
|
|
898
|
+
btn = int(m.group(1))
|
|
899
|
+
col = int(m.group(2))
|
|
900
|
+
row = int(m.group(3))
|
|
901
|
+
act = m.group(4)
|
|
902
|
+
is_left_press = (btn == 0 and act == b"M")
|
|
903
|
+
else:
|
|
904
|
+
btn = m.group(5)[0] - 32
|
|
905
|
+
col = m.group(6)[0] - 32
|
|
906
|
+
row = m.group(7)[0] - 32
|
|
907
|
+
is_left_press = (btn == 0)
|
|
908
|
+
if is_left_press:
|
|
909
|
+
arrows = tracker.arrows_for_click(col, row)
|
|
910
|
+
if arrows:
|
|
911
|
+
out.extend(arrows)
|
|
912
|
+
tail_seg = in_bytes[last_end:]
|
|
913
|
+
if tail_seg:
|
|
914
|
+
tracker.feed_stdin(tail_seg)
|
|
915
|
+
out.extend(tail_seg)
|
|
916
|
+
return bytes(out)
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def sync_winsize(src_fd: int, dst_fd: int, tracker: PromptCursorTracker | None = None) -> None:
|
|
920
|
+
try:
|
|
921
|
+
ws = fcntl.ioctl(src_fd, termios.TIOCGWINSZ, b"\x00" * 8)
|
|
922
|
+
rows, cols, _, _ = struct.unpack("HHHH", ws)
|
|
923
|
+
if tracker is not None and rows > 0 and cols > 0:
|
|
924
|
+
tracker.term_rows = rows
|
|
925
|
+
tracker.term_cols = cols
|
|
926
|
+
fcntl.ioctl(dst_fd, termios.TIOCSWINSZ, ws)
|
|
927
|
+
except OSError:
|
|
928
|
+
pass
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def main(argv: list[str] | None = None) -> int:
|
|
932
|
+
global CURRENT_CID, _IN_PTY_MAIN, REAL_AGY
|
|
933
|
+
_IN_PTY_MAIN = True
|
|
934
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
935
|
+
if args and os.path.isabs(args[0]) and os.path.isfile(args[0]) and os.access(args[0], os.X_OK):
|
|
936
|
+
REAL_AGY = args.pop(0)
|
|
937
|
+
else:
|
|
938
|
+
REAL_AGY = _resolve_real_agy()
|
|
939
|
+
for idx, a in enumerate(args):
|
|
940
|
+
if a in ("--conversation", "-c") and idx + 1 < len(args):
|
|
941
|
+
CURRENT_CID = args[idx + 1]
|
|
942
|
+
break
|
|
943
|
+
non_interactive = (
|
|
944
|
+
os.environ.get("AGY_RAW_COLORS") == "1"
|
|
945
|
+
or os.environ.get("AGY_FILTER_ACTIVE") == "1"
|
|
946
|
+
or not sys.stdin.isatty()
|
|
947
|
+
or not sys.stdout.isatty()
|
|
948
|
+
or any(
|
|
949
|
+
a in ("-p", "--print", "--prompt", "-v", "--version", "version", "help", "--help", "-h", "models", "mcp", "plugin", "plugins", "update", "auth", "login", "logout", "config", "completion")
|
|
950
|
+
for a in args
|
|
951
|
+
)
|
|
952
|
+
)
|
|
953
|
+
if non_interactive:
|
|
954
|
+
os.execvp(REAL_AGY, ["agy", *args])
|
|
955
|
+
|
|
956
|
+
if os.path.basename(sys.argv[0]) == "agy" and not any(
|
|
957
|
+
a == "--mode" or a.startswith("--mode=") for a in args
|
|
958
|
+
):
|
|
959
|
+
args = ["--mode", "auto", *args]
|
|
960
|
+
|
|
961
|
+
os.environ["AGY_FILTER_ACTIVE"] = "1"
|
|
962
|
+
stdin_fd = sys.stdin.fileno()
|
|
963
|
+
stdout_fd = sys.stdout.fileno()
|
|
964
|
+
orig_termios = termios.tcgetattr(stdin_fd)
|
|
965
|
+
tracker = PromptCursorTracker()
|
|
966
|
+
|
|
967
|
+
pid, master_fd = pty.fork()
|
|
968
|
+
if pid == 0:
|
|
969
|
+
os.execvp(REAL_AGY, ["agy", *args])
|
|
970
|
+
|
|
971
|
+
sync_winsize(stdin_fd, master_fd, tracker)
|
|
972
|
+
|
|
973
|
+
def _on_sigwinch(signum, frame):
|
|
974
|
+
sync_winsize(stdin_fd, master_fd, tracker)
|
|
975
|
+
tracker.top_row = None
|
|
976
|
+
tracker.last_prompt_row = None
|
|
977
|
+
tracker.need_dsr = True
|
|
978
|
+
try:
|
|
979
|
+
os.kill(pid, signal.SIGWINCH)
|
|
980
|
+
except OSError:
|
|
981
|
+
pass
|
|
982
|
+
|
|
983
|
+
signal.signal(signal.SIGWINCH, _on_sigwinch)
|
|
984
|
+
|
|
985
|
+
carry = b""
|
|
986
|
+
# Enable SGR button click reporting (`?1000h` + `?1006h`); DSR (`\x1b[6n`) is queried after prompt renders
|
|
987
|
+
try:
|
|
988
|
+
os.write(stdout_fd, b"\x1b[?1000h\x1b[?1006h")
|
|
989
|
+
except OSError:
|
|
990
|
+
pass
|
|
991
|
+
try:
|
|
992
|
+
tty.setraw(stdin_fd)
|
|
993
|
+
while True:
|
|
994
|
+
rlist, _, _ = select.select([stdin_fd, master_fd], [], [])
|
|
995
|
+
if stdin_fd in rlist:
|
|
996
|
+
try:
|
|
997
|
+
in_bytes = os.read(stdin_fd, 4096)
|
|
998
|
+
except OSError:
|
|
999
|
+
break
|
|
1000
|
+
if not in_bytes:
|
|
1001
|
+
break
|
|
1002
|
+
filtered_in = filter_stdin_mouse(in_bytes, tracker)
|
|
1003
|
+
if filtered_in:
|
|
1004
|
+
os.write(master_fd, filtered_in)
|
|
1005
|
+
if master_fd in rlist:
|
|
1006
|
+
try:
|
|
1007
|
+
out_bytes = os.read(master_fd, 65536)
|
|
1008
|
+
except OSError:
|
|
1009
|
+
break
|
|
1010
|
+
if not out_bytes:
|
|
1011
|
+
break
|
|
1012
|
+
tracker.update_from_output(out_bytes)
|
|
1013
|
+
transformed, carry = transform_chunk(out_bytes, carry)
|
|
1014
|
+
if tracker.need_dsr:
|
|
1015
|
+
tracker.need_dsr = False
|
|
1016
|
+
transformed = transformed + b"\x1b[6n"
|
|
1017
|
+
if transformed:
|
|
1018
|
+
os.write(stdout_fd, transformed)
|
|
1019
|
+
if carry:
|
|
1020
|
+
for src, dst in REPLACEMENTS:
|
|
1021
|
+
carry = carry.replace(src, dst)
|
|
1022
|
+
os.write(stdout_fd, carry)
|
|
1023
|
+
finally:
|
|
1024
|
+
try:
|
|
1025
|
+
os.write(stdout_fd, b"\x1b[?1000l\x1b[?1006l")
|
|
1026
|
+
except OSError:
|
|
1027
|
+
pass
|
|
1028
|
+
try:
|
|
1029
|
+
termios.tcsetattr(stdin_fd, termios.TCSAFLUSH, orig_termios)
|
|
1030
|
+
except termios.error:
|
|
1031
|
+
pass
|
|
1032
|
+
try:
|
|
1033
|
+
os.close(master_fd)
|
|
1034
|
+
except OSError:
|
|
1035
|
+
pass
|
|
1036
|
+
|
|
1037
|
+
_, status = os.waitpid(pid, 0)
|
|
1038
|
+
return os.waitstatus_to_exitcode(status)
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
if __name__ == "__main__":
|
|
1042
|
+
sys.exit(main())
|