remote-claude 0.2.13 → 0.2.14

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/README.md CHANGED
@@ -11,7 +11,7 @@ Claude Code 只能在启动它的那个终端窗口里操作。一旦离开电
11
11
  - **飞书里直接操作** — 手机/平板打开飞书,就能看到 Claude 的实时输出,发消息、选选项、批准权限,和终端里一模一样。
12
12
  - **用手机无缝延续电脑上做的工作** — 电脑上打开的Claude进程,也可以用飞书共享操作,开会、午休、通勤、上厕所时,都可以用手机延续之前在电脑上的工作。
13
13
  - **在电脑上也可以无缝延续手机上的工作** - 在lark端也可以打开新的Claude进程启动新的工作,回到电脑前还可以`attach`共享操作同一个Claude进程,延续手机端的工作。
14
- - **多端共享操作** — 多个终端 + 飞书可以共享操作同一个claude进程,回到家里ssh登录到服务器上也可以通过`attach`继续操作在公司ssh登录到服务器上打开的claude进程操作。
14
+ - **多端共享操作** — 多个终端 + 飞书可以共享操作同一个claude/codex进程,回到家里ssh登录到服务器上也可以通过`attach`继续操作在公司ssh登录到服务器上打开的claude/codex进程操作。
15
15
  - **机制安全** - 完全不侵入 Claude 进程,remote 功能完全通过终端交互来实现,不必担心 Claude 进程意外崩溃导致工作进展丢失。
16
16
 
17
17
  ## 飞书端体验
@@ -68,7 +68,7 @@ remote-claude attach <会话名>
68
68
 
69
69
  1. 登录[飞书开放平台](https://open.feishu.cn/),创建企业自建应用
70
70
  2. 获取 **App ID** 和 **App Secret**
71
- 3. 用`cla`或`cl`启动一次claude, 按照交互提示填入**App ID** 和 **App Secret**
71
+ 3. 用`cla`或`cl`启动一次claude(或用cx或cdx启动一次codex), 按照交互提示填入**App ID** 和 **App Secret**
72
72
  4. [飞书开放平台]的企业自建应用页面`添加应用能力`(机器人能力)
73
73
  5. 企业自建应用页面配置事件回调(如果第3步没启动成功这里配置不了):
74
74
  - `事件与回调` -> `事件配置` -> `订阅方式`右边的笔图标 -> `选择:使用长连接接收事件` -> `点击保存` -> `下面添加事件: 接收消息 v2.0 (im.message.receive_v1)`
@@ -198,9 +198,9 @@ remote-claude attach <会话名>
198
198
  7. 企业自建应用页面: `创建版本` -> `发布到线上`
199
199
  8. 至此,完成飞书机器人配置
200
200
 
201
- #### 4.2 通过飞书机器人操作claude
201
+ #### 4.2 通过飞书机器人操作claude/codex
202
202
 
203
- 1. 从飞书搜素刚刚创建的飞书机器人(第一次搜比较慢,如果搜不到可能是忘记发布了)
203
+ 1. 从飞书搜索刚刚创建的飞书机器人(第一次搜比较慢,如果搜不到可能是忘记发布了)
204
204
  2. 飞书中与机器人对话,可用命令:
205
205
  - `/menu` 展示菜单卡片,后续操作都操作这个卡片上的按钮即可
206
206
 
package/init.sh CHANGED
@@ -40,6 +40,11 @@ setup_path() {
40
40
  # 不存在则创建
41
41
  [[ -f "$PROFILE" ]] || touch "$PROFILE"
42
42
 
43
+ # 未写入 source .bashrc 则追加
44
+ if ! grep -qF '.bashrc' "$PROFILE" 2>/dev/null; then
45
+ echo '[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"' >> "$PROFILE"
46
+ fi
47
+
43
48
  # 未写入则追加
44
49
  if ! grep -qF '$HOME/.local/bin' "$PROFILE" 2>/dev/null; then
45
50
  echo "" >> "$PROFILE"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-claude",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "双端共享 Claude CLI 工具",
5
5
  "bin": {
6
6
  "remote-claude": "bin/remote-claude",
@@ -5,7 +5,7 @@ ClaudeParser 和 CodexParser 均继承此类。
5
5
  """
6
6
 
7
7
  from abc import ABC, abstractmethod
8
- from typing import List
8
+ from typing import Any, Dict, List
9
9
 
10
10
  import pyte
11
11
 
@@ -29,6 +29,9 @@ class BaseParser(ABC):
29
29
  last_input_ansi_text: str = ''
30
30
  last_parse_timing: str = ''
31
31
  last_layout_mode: str = 'normal'
32
+ # 诊断用:记录最近一次 parse 的区域统计与组件统计
33
+ last_region_stats: Dict[str, Any] = None
34
+ last_component_stats: Dict[str, Any] = None
32
35
 
33
36
  @abstractmethod
34
37
  def parse(self, screen: pyte.Screen) -> List:
@@ -11,10 +11,14 @@
11
11
  """
12
12
 
13
13
  import codecs
14
+ import logging
14
15
  import pyte
16
+ from pyte.screens import Margins
15
17
  from typing import List, Tuple, Optional
16
18
  from dataclasses import dataclass
17
19
 
20
+ logger = logging.getLogger(__name__)
21
+
18
22
 
19
23
  # ANSI 颜色名称到飞书颜色的映射
20
24
  # 飞书 Markdown 只支持: green, red, grey 三种颜色
@@ -55,11 +59,38 @@ class StyledSpan:
55
59
  strikethrough: bool = False
56
60
 
57
61
 
58
- class _DimAwareScreen(pyte.Screen):
59
- """pyte 不支持 SGR 2 (dim/faint),子类化后将 dim 映射为灰色前景"""
62
+ class _ExtendedStream(pyte.Stream):
63
+ """pyte.Stream 子类:补充 SU(ESC[nS)和 SD(ESC[nT)支持。
64
+
65
+ pyte 原生 CSI dispatch 不包含 'S' 和 'T',Codex 的 ESC[2S 会被完全忽略。
66
+ """
67
+ csi = {**pyte.Stream.csi, 'S': 'scroll_up', 'T': 'scroll_down'}
68
+
69
+
70
+ class _DebugStream(_ExtendedStream):
71
+ """_ExtendedStream 子类:记录被 pyte 丢弃的未识别转义序列到 DEBUG 日志。
72
+
73
+ 仅在 --debug-screen 开启时使用,替换默认 _ExtendedStream。
74
+ """
75
+ _undef_logger = logging.getLogger('pyte.Stream.undefined')
76
+
77
+ def _undefined(self, *args, **kwargs):
78
+ self._undef_logger.debug(f"pyte 未识别序列: args={args!r} kwargs={kwargs!r}")
79
+
80
+
81
+ class _DimAwareScreen(pyte.HistoryScreen):
82
+ """pyte.HistoryScreen 子类:
83
+ 1. SGR 2 (dim/faint) 映射为灰色前景
84
+ 2. 补充 SU/SD(ESC[nS/ESC[nT)滚动支持,滚出行保存到 history.top
85
+ """
60
86
 
61
87
  # dim 状态下使用的灰色(与终端 dim 效果近似)
62
88
  _DIM_FG = '808080'
89
+ _DEFAULT_HISTORY = 5000
90
+
91
+ def __init__(self, columns, lines, **kwargs):
92
+ kwargs.setdefault('history', self._DEFAULT_HISTORY)
93
+ super().__init__(columns, lines, **kwargs)
63
94
 
64
95
  def select_graphic_rendition(self, *attrs):
65
96
  # 检查是否包含 SGR 2 (dim)
@@ -70,13 +101,35 @@ class _DimAwareScreen(pyte.Screen):
70
101
  if has_dim and self.cursor.attrs.fg == 'default':
71
102
  self.cursor.attrs = self.cursor.attrs._replace(fg=self._DIM_FG)
72
103
 
104
+ def scroll_up(self, count=1):
105
+ """ESC[nS — SU:上滚 n 行,滚出行保存到 history.top"""
106
+ top, bottom = self.margins or Margins(0, self.lines - 1)
107
+ saved_y = self.cursor.y
108
+ self.cursor.y = bottom
109
+ for _ in range(count):
110
+ self.index()
111
+ self.cursor.y = saved_y
112
+
113
+ def scroll_down(self, count=1):
114
+ """ESC[nT — SD:下滚 n 行"""
115
+ top, bottom = self.margins or Margins(0, self.lines - 1)
116
+ saved_y = self.cursor.y
117
+ self.cursor.y = top
118
+ for _ in range(count):
119
+ self.reverse_index()
120
+ self.cursor.y = saved_y
121
+
73
122
 
74
123
  class RichTextRenderer:
75
124
  """将 pyte 屏幕内容转换为飞书富文本"""
76
125
 
77
- def __init__(self, columns: int = 200, lines: int = 500):
126
+ def __init__(self, columns: int = 200, lines: int = 500, debug_stream: bool = False):
78
127
  self.screen = _DimAwareScreen(columns, lines)
79
- self.stream = pyte.Stream(self.screen)
128
+ # debug_stream=True 时使用 _DebugStream,记录 pyte 未识别序列
129
+ if debug_stream:
130
+ self.stream = _DebugStream(self.screen)
131
+ else:
132
+ self.stream = _ExtendedStream(self.screen)
80
133
  self.stream.use_utf8 = True
81
134
  # 增量 UTF-8 解码器:保持跨 chunk 的解码状态,防止多字节序列被截断
82
135
  self._utf8_decoder = codecs.getincrementaldecoder('utf-8')(errors='replace')
@@ -84,6 +137,18 @@ class RichTextRenderer:
84
137
  def feed(self, data: bytes) -> None:
85
138
  """喂入原始终端数据"""
86
139
  text = self._utf8_decoder.decode(data)
140
+ # 改动4:检测 UTF-8 解码产生的替换字符(U+FFFD),说明原始字节流有非法序列
141
+ if '\ufffd' in text:
142
+ # 定位替换字符位置(最多报告前 5 处)
143
+ positions = [i for i, c in enumerate(text) if c == '\ufffd'][:5]
144
+ contexts = []
145
+ for pos in positions:
146
+ ctx_start = max(0, pos - 4)
147
+ ctx_end = min(len(text), pos + 5)
148
+ ctx = repr(text[ctx_start:ctx_end])
149
+ contexts.append(f"pos={pos} ctx={ctx}")
150
+ logger.warning(f"UTF-8 解码替换字符(\\ufffd): count={text.count(chr(0xfffd))} "
151
+ f"positions={contexts}")
87
152
  self.stream.feed(text)
88
153
 
89
154
  def clear(self) -> None:
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)