remote-claude 0.2.13 → 0.2.15

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.
package/server/server.py CHANGED
@@ -68,6 +68,63 @@ except Exception:
68
68
  HISTORY_BUFFER_SIZE = 100 * 1024 # 100KB
69
69
 
70
70
 
71
+ # ── VirtualScreen:将 HistoryScreen.history.top + buffer 合并为统一视图 ──────
72
+
73
+ class _VirtualBuffer:
74
+ """统一访问接口:history rows [0..offset) + screen.buffer [offset..offset+lines)"""
75
+ __slots__ = ('_history', '_buffer', '_offset')
76
+
77
+ def __init__(self, history, buffer, offset):
78
+ self._history = history # list[dict],来自 list(screen.history.top)
79
+ self._buffer = buffer # pyte screen.buffer
80
+ self._offset = offset # len(history)
81
+
82
+ def __getitem__(self, row):
83
+ if row < self._offset:
84
+ return self._history[row]
85
+ return self._buffer[row - self._offset]
86
+
87
+
88
+ class _VirtualCursor:
89
+ """模拟 pyte cursor,y 偏移 history 行数"""
90
+ __slots__ = ('y',)
91
+
92
+ def __init__(self, y: int):
93
+ self.y = y
94
+
95
+
96
+ class VirtualScreen:
97
+ """HistoryScreen wrapper:将 history.top + buffer 合并为 parser 可直接读取的统一屏幕视图。
98
+
99
+ parser 无需任何修改:
100
+ - _split_regions 从 cursor.y+5 向上扫描找分割线,先找到当前屏幕中的分割线就停了
101
+ - output_rows 自然包含 history 中的行号(0 到 offset-1)
102
+ - _get_row_text/buffer[row] 通过 VirtualBuffer 透明返回对应行
103
+ """
104
+ __slots__ = ('_screen', '_history', '_offset')
105
+
106
+ def __init__(self, screen):
107
+ self._screen = screen
108
+ self._history = list(screen.history.top) if hasattr(screen, 'history') else []
109
+ self._offset = len(self._history)
110
+
111
+ @property
112
+ def columns(self):
113
+ return self._screen.columns
114
+
115
+ @property
116
+ def lines(self):
117
+ return self._offset + self._screen.lines
118
+
119
+ @property
120
+ def cursor(self):
121
+ return _VirtualCursor(self._offset + self._screen.cursor.y)
122
+
123
+ @property
124
+ def buffer(self):
125
+ return _VirtualBuffer(self._history, self._screen.buffer, self._offset)
126
+
127
+
71
128
  # ── 全量快照架构 ─────────────────────────────────────────────────────────────
72
129
 
73
130
  @dataclass
@@ -129,9 +186,17 @@ class OutputWatcher:
129
186
  self._debug_verbose = debug_verbose # --debug-verbose 开启后输出 indicator/repr 等诊断信息
130
187
  safe_name = _safe_filename(session_name)
131
188
  self._debug_file = f"/tmp/remote-claude/{safe_name}_messages.log"
189
+ # PTY 原始字节流日志(仅 --debug-screen 开启时使用)
190
+ self._raw_log_fd = None
191
+ if debug_screen:
192
+ raw_log_path = f"/tmp/remote-claude/{safe_name}_pty_raw.log"
193
+ try:
194
+ self._raw_log_fd = open(raw_log_path, "a", encoding="ascii", buffering=1)
195
+ except Exception:
196
+ pass
132
197
  # 持久化 pyte 渲染器:PTY 数据直接实时喂入,flush 时直接读 screen
133
198
  from rich_text_renderer import RichTextRenderer
134
- self._renderer = RichTextRenderer(columns=cols, lines=rows)
199
+ self._renderer = RichTextRenderer(columns=cols, lines=rows, debug_stream=debug_screen)
135
200
  # 持久化解析器(跨帧保留 dot_row_cache);由调用方注入(可插拔架构)
136
201
  import logging as _logging
137
202
  _logging.getLogger('ComponentParser').setLevel(_logging.DEBUG)
@@ -175,6 +240,15 @@ class OutputWatcher:
175
240
  # 诊断日志:记录 PTY 数据到达
176
241
  if data:
177
242
  logger.debug(f"[diag-feed] len={len(data)} data={data[:50]!r}")
243
+ # 改动3:--debug-screen 开启时追加原始字节到 _pty_raw.log(base64 编码)
244
+ if self._raw_log_fd is not None and data:
245
+ try:
246
+ import base64 as _b64
247
+ ts = time.strftime('%H:%M:%S') + f'.{int(time.time() * 1000) % 1000:03d}'
248
+ encoded = _b64.b64encode(data).decode('ascii')
249
+ self._raw_log_fd.write(f"{ts} len={len(data)} {encoded}\n")
250
+ except Exception:
251
+ pass
178
252
  try:
179
253
  loop = asyncio.get_running_loop()
180
254
  except RuntimeError:
@@ -217,8 +291,10 @@ class OutputWatcher:
217
291
  self._write_screen_debug(self._renderer.screen)
218
292
  t1 = time.time()
219
293
 
220
- # 1. 解析(ScreenParser 不改;pyte 已在 feed() 里实时更新)
221
- components = self._parser.parse(self._renderer.screen)
294
+ # 1. 构建 VirtualScreen(history.top + buffer)并解析
295
+ # _write_screen_debug 仍用原始 screen(只写当前帧快照,不含 history)
296
+ vscreen = VirtualScreen(self._renderer.screen)
297
+ components = self._parser.parse(vscreen)
222
298
  input_text = self._parser.last_input_text
223
299
  input_ansi_text = self._parser.last_input_ansi_text
224
300
 
@@ -256,10 +332,10 @@ class OutputWatcher:
256
332
  last_ob_blink = b.is_streaming
257
333
  last_ob_content = b.content[:40]
258
334
  last_ob_start_row = b.start_row
259
- # 直接读 pyte screen buffer 获取原始字符属性(用于变化检测)
335
+ # vscreen.buffer 获取原始字符属性(支持 history 行)
260
336
  if b.start_row >= 0:
261
337
  try:
262
- char = self._renderer.screen.buffer[b.start_row][0]
338
+ char = vscreen.buffer[b.start_row][0]
263
339
  last_ob_indicator_char = str(getattr(char, 'data', ''))
264
340
  last_ob_indicator_fg = str(getattr(char, 'fg', ''))
265
341
  last_ob_indicator_bold = bool(getattr(char, 'bold', False))
@@ -329,7 +405,7 @@ class OutputWatcher:
329
405
  b.is_streaming = True
330
406
  break
331
407
 
332
- # 5. 直接使用 visible_blocks(pyte 2000 行已保留全部历史)
408
+ # 5. 直接使用 visible_blocks(VirtualScreen 已包含 history.top + 当前屏幕)
333
409
  all_blocks = visible_blocks
334
410
 
335
411
  # 5b. 后台 agent 摘要:BottomBar 有 agent 信息但面板未展开时,
@@ -513,8 +589,80 @@ class OutputWatcher:
513
589
  except Exception:
514
590
  pass
515
591
 
592
+ @staticmethod
593
+ def _char_to_ansi(char) -> str:
594
+ """将 pyte Char 的颜色/样式属性转为 ANSI SGR 前缀序列。
595
+
596
+ 返回带 ANSI 转义的字符字符串,调用方需在行尾追加 \\033[0m 重置。
597
+ 仅在属性非默认时才输出对应 SGR,降低输出体积。
598
+ """
599
+ # 标准颜色名到 ANSI 码
600
+ _FG_NAMES = {
601
+ 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
602
+ 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37,
603
+ 'brown': 33, # pyte 把 ANSI 33 解析为 brown
604
+ }
605
+ _BG_NAMES = {
606
+ 'black': 40, 'red': 41, 'green': 42, 'yellow': 43,
607
+ 'blue': 44, 'magenta': 45, 'cyan': 46, 'white': 47,
608
+ 'brown': 43,
609
+ }
610
+ _BRIGHT_FG = {
611
+ 'brightblack': 90, 'brightred': 91, 'brightgreen': 92, 'brightyellow': 93,
612
+ 'brightblue': 94, 'brightmagenta': 95, 'brightcyan': 96, 'brightwhite': 97,
613
+ }
614
+ _BRIGHT_BG = {
615
+ 'brightblack': 100, 'brightred': 101, 'brightgreen': 102, 'brightyellow': 103,
616
+ 'brightblue': 104, 'brightmagenta': 105, 'brightcyan': 106, 'brightwhite': 107,
617
+ }
618
+
619
+ sgr = []
620
+
621
+ # bold / blink
622
+ if getattr(char, 'bold', False):
623
+ sgr.append('1')
624
+ if getattr(char, 'blink', False):
625
+ sgr.append('5')
626
+
627
+ # 前景色
628
+ fg = getattr(char, 'fg', 'default')
629
+ if fg and fg != 'default':
630
+ fg_low = str(fg).lower()
631
+ if fg_low in _FG_NAMES:
632
+ sgr.append(str(_FG_NAMES[fg_low]))
633
+ elif fg_low in _BRIGHT_FG:
634
+ sgr.append(str(_BRIGHT_FG[fg_low]))
635
+ elif len(fg_low) == 6 and all(c in '0123456789abcdef' for c in fg_low):
636
+ # 24-bit true color
637
+ r = int(fg_low[0:2], 16)
638
+ g = int(fg_low[2:4], 16)
639
+ b = int(fg_low[4:6], 16)
640
+ sgr.append(f'38;2;{r};{g};{b}')
641
+
642
+ # 背景色
643
+ bg = getattr(char, 'bg', 'default')
644
+ if bg and bg != 'default':
645
+ bg_low = str(bg).lower()
646
+ if bg_low in _BG_NAMES:
647
+ sgr.append(str(_BG_NAMES[bg_low]))
648
+ elif bg_low in _BRIGHT_BG:
649
+ sgr.append(str(_BRIGHT_BG[bg_low]))
650
+ elif len(bg_low) == 6 and all(c in '0123456789abcdef' for c in bg_low):
651
+ r = int(bg_low[0:2], 16)
652
+ g = int(bg_low[2:4], 16)
653
+ b = int(bg_low[4:6], 16)
654
+ sgr.append(f'48;2;{r};{g};{b}')
655
+
656
+ if sgr:
657
+ return f'\033[{";".join(sgr)}m{char.data}\033[0m'
658
+ return char.data
659
+
516
660
  def _write_screen_debug(self, screen):
517
- """将 pyte 屏幕内容写入调试文件(_screen.log)"""
661
+ """将 pyte 屏幕内容写入调试文件(_screen.log)
662
+
663
+ 每个字符的 fg/bg 颜色通过 ANSI SGR 序列直接嵌入,
664
+ cat _screen.log 即可在终端看到与 pyte 渲染一致的着色效果。
665
+ """
518
666
  base = f"/tmp/remote-claude/{_safe_filename(self._session_name)}"
519
667
  try:
520
668
  # pyte 屏幕快照(覆盖写,只保留最新一帧)
@@ -526,20 +674,51 @@ class OutputWatcher:
526
674
  "",
527
675
  ]
528
676
  for row in range(scan_limit + 1):
529
- buf = [' '] * screen.columns
530
- for col, char in screen.buffer[row].items():
531
- buf[col] = char.data
532
- rstripped = ''.join(buf).rstrip()
533
- if not rstripped:
534
- lines.append(f"{row:3d} |")
677
+ # 构建带颜色的行内容(用于终端直接 cat 显示)
678
+ colored_buf = []
679
+ plain_buf = [' '] * screen.columns
680
+ for col in range(screen.columns):
681
+ char = screen.buffer[row].get(col)
682
+ if char is None:
683
+ colored_buf.append(' ')
684
+ continue
685
+ plain_buf[col] = char.data
686
+ colored_buf.append(self._char_to_ansi(char))
687
+
688
+ rstripped_plain = ''.join(plain_buf).rstrip()
689
+ if not rstripped_plain:
690
+ # 检查是否有背景色(对 Codex 背景色分割线很重要)
691
+ row_bgs = set()
692
+ for col in range(screen.columns):
693
+ char = screen.buffer[row].get(col)
694
+ if char is not None:
695
+ bg = getattr(char, 'bg', 'default')
696
+ if bg and bg != 'default':
697
+ row_bgs.add(str(bg))
698
+ if row_bgs:
699
+ bg_str = ','.join(sorted(row_bgs))
700
+ lines.append(f"{row:3d} |[bg:{bg_str} ···]")
701
+ else:
702
+ lines.append(f"{row:3d} |")
535
703
  continue
704
+
536
705
  try:
537
706
  c0 = screen.buffer[row][0]
538
707
  col0_blink = getattr(c0, "blink", False)
539
708
  except (KeyError, IndexError):
540
709
  col0_blink = False
710
+
541
711
  blink_mark = "B" if col0_blink else " "
542
- lines.append(f"{row:3d}{blink_mark}|{rstripped}")
712
+ # rstrip 着色内容:找最后一个有内容的列号(正确处理 CJK 占 2 列的情况)
713
+ # plain_buf 按列号索引,len(rstripped_plain) 会因 CJK 字符导致列数偏小
714
+ last_col = 0
715
+ for col in range(screen.columns - 1, -1, -1):
716
+ if plain_buf[col] != ' ':
717
+ last_col = col + 1
718
+ break
719
+ colored_line = ''.join(colored_buf[:last_col])
720
+ lines.append(f"{row:3d}{blink_mark}|{colored_line}")
721
+
543
722
  with open(screen_path, "w", encoding="utf-8") as f:
544
723
  f.write("\n".join(lines))
545
724
  except Exception:
@@ -707,7 +886,7 @@ class ProxyServer:
707
886
 
708
887
  # PTY 终端尺寸:与 lark_client 的 pyte 渲染器保持一致
709
888
  PTY_COLS = 220
710
- PTY_ROWS = 2000
889
+ PTY_ROWS = 100 # 行数缩减至 100,历史内容通过 HistoryScreen.history.top 保存(5000 行容量)
711
890
 
712
891
  def _get_parser(self):
713
892
  """根据 cli_type 返回对应的解析器实例"""
@@ -965,7 +1144,7 @@ class ProxyServer:
965
1144
  """处理终端大小变化:同步更新 PTY 和 pyte 渲染尺寸,清空 raw buffer。
966
1145
  Claude 收到 SIGWINCH 后会全屏重绘,buffer 清空后自然恢复为新尺寸的完整屏幕数据。"""
967
1146
  try:
968
- # output_watcher 的 rows 固定为 PTY_ROWS(2000),不跟随客户端终端尺寸变化
1147
+ # output_watcher 的 rows 固定为 PTY_ROWS(100),不跟随客户端终端尺寸变化
969
1148
  # terminal client 直接渲染 PTY 原始输出,不依赖 output_watcher,无需同步 rows
970
1149
  self.output_watcher.resize(msg.cols, self.PTY_ROWS)
971
1150
  winsize = struct.pack('HHHH', msg.rows, msg.cols, 0, 0)