auvezy-terminal-remote 0.3.0 → 0.4.0

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/dist/cli.js CHANGED
@@ -61,9 +61,11 @@ function ensureDefaultUserConfig(input) {
61
61
  const userCommands = Array.isArray(src.commands) && src.commands.length > 0 ? src.commands : null;
62
62
  const commandsLegacy = userCommands !== null && userCommands.some((c) => typeof c.group !== "string" || c.group.length === 0);
63
63
  const commands = userCommands === null || commandsLegacy ? DEFAULT_COMMANDS : userCommands;
64
- return { ...src, shortcuts, commands };
64
+ const useInputBarValue = typeof src.input?.useInputBar === "boolean" ? src.input.useInputBar : DEFAULT_INPUT.useInputBar;
65
+ const inputPrefs = { useInputBar: useInputBarValue };
66
+ return { ...src, shortcuts, commands, input: inputPrefs };
65
67
  }
66
- var SHORTCUT_GROUPS, DEFAULT_SHORTCUTS, COMMAND_GROUPS, DEFAULT_COMMANDS;
68
+ var SHORTCUT_GROUPS, DEFAULT_SHORTCUTS, COMMAND_GROUPS, DEFAULT_COMMANDS, DEFAULT_INPUT;
67
69
  var init_defaults = __esm({
68
70
  "shared/dist/defaults.js"() {
69
71
  "use strict";
@@ -253,6 +255,9 @@ var init_defaults = __esm({
253
255
  }
254
256
  ];
255
257
  DEFAULT_COMMANDS = COMMAND_GROUPS.flatMap((g) => g.items.map((c) => ({ ...c, group: g.id })));
258
+ DEFAULT_INPUT = {
259
+ useInputBar: true
260
+ };
256
261
  }
257
262
  });
258
263
 
@@ -818,7 +823,7 @@ var init_config_routes = __esm({
818
823
  });
819
824
 
820
825
  // backend/dist/constants.js
821
- var WS_FLUSH_INTERVAL_MS, WS_MAX_CHUNK_BYTES, WS_HIGH_WATERMARK_BYTES, FILE_LOCK_RETRIES, FILE_LOCK_RETRY_INTERVAL_MS, FILE_LOCK_STALE_MS, IP_MONITOR_INTERVAL_MS, IP_MONITOR_STABILITY_THRESHOLD, PTY_DEFAULT_COLS, PTY_DEFAULT_ROWS, PTY_TERM_NAME, SHUTDOWN_WS_FLUSH_DELAY_MS, SHUTDOWN_FORCE_EXIT_MS, DOUBLE_CTRL_C_WINDOW_MS, PORT_FINDER_MAX_ATTEMPTS, STOP_INSTANCE_GRACE_MS, STOP_INSTANCE_POLL_INTERVAL_MS, ATTACH_RECONNECT_DELAYS_MS;
826
+ var WS_FLUSH_INTERVAL_MS, WS_MAX_CHUNK_BYTES, WS_HIGH_WATERMARK_BYTES, FILE_LOCK_RETRIES, FILE_LOCK_RETRY_INTERVAL_MS, FILE_LOCK_STALE_MS, IP_MONITOR_INTERVAL_MS, IP_MONITOR_STABILITY_THRESHOLD, PTY_DEFAULT_COLS, PTY_DEFAULT_ROWS, PTY_TERM_NAME, DOUBLE_PULSE_DELAY_MS, SHUTDOWN_WS_FLUSH_DELAY_MS, SHUTDOWN_FORCE_EXIT_MS, DOUBLE_CTRL_C_WINDOW_MS, PORT_FINDER_MAX_ATTEMPTS, STOP_INSTANCE_GRACE_MS, STOP_INSTANCE_POLL_INTERVAL_MS, ATTACH_RECONNECT_DELAYS_MS;
822
827
  var init_constants2 = __esm({
823
828
  "backend/dist/constants.js"() {
824
829
  "use strict";
@@ -833,6 +838,7 @@ var init_constants2 = __esm({
833
838
  PTY_DEFAULT_COLS = 80;
834
839
  PTY_DEFAULT_ROWS = 24;
835
840
  PTY_TERM_NAME = "xterm-256color";
841
+ DOUBLE_PULSE_DELAY_MS = 50;
836
842
  SHUTDOWN_WS_FLUSH_DELAY_MS = 500;
837
843
  SHUTDOWN_FORCE_EXIT_MS = 2e3;
838
844
  DOUBLE_CTRL_C_WINDOW_MS = 500;
@@ -1627,6 +1633,16 @@ var init_pty_manager = __esm({
1627
1633
  _exited = false;
1628
1634
  _cols = PTY_DEFAULT_COLS;
1629
1635
  _rows = PTY_DEFAULT_ROWS;
1636
+ /**
1637
+ * 当前是否处于 alt-screen(DECSET 1049 / 1047 / 47)。
1638
+ * 通过扫描 PTY 输出序列实时维护:
1639
+ * - vim/htop/tmux/less 等全屏 TUI 进入时切 true(它们自己整屏重画,对
1640
+ * resize 反应正常,不需要 double-pulse hack)
1641
+ * - claude/zsh prompt 等增量重画 TUI 始终为 false → 需要 double-pulse
1642
+ */
1643
+ _inAltScreen = false;
1644
+ /** double-pulse resize 的中间帧定时器 */
1645
+ _doublePulseTimer = null;
1630
1646
  get cols() {
1631
1647
  return this._cols;
1632
1648
  }
@@ -1662,6 +1678,7 @@ var init_pty_manager = __esm({
1662
1678
  env: { ...process.env, ...opts.env }
1663
1679
  });
1664
1680
  this.process.onData((data) => {
1681
+ this.scanAltScreenToggle(data);
1665
1682
  this.emit("data", data);
1666
1683
  });
1667
1684
  this.process.onExit(({ exitCode, signal }) => {
@@ -1699,6 +1716,18 @@ var init_pty_manager = __esm({
1699
1716
  * webapp resize → ws → PTY.resize → emit 'resize' → broadcast →
1700
1717
  * webapp 收到 terminal_resize → 触发 fit → 又算出同尺寸 → 再发 resize → ...
1701
1718
  * 同尺寸跳过让链路在第二步就断掉
1719
+ *
1720
+ * Double-pulse hack(仅 normal-screen / 增量重画 TUI):
1721
+ * Claude Code (Ink) / blessed / prompt-toolkit / readline REPL 等用相对
1722
+ * 坐标增量重画的程序,收到 SIGWINCH 后只对"宽度变窄"分支才会清屏 +
1723
+ * 重新 layout(Ink 源码:`if (currentWidth < lastTerminalWidth) clear()`)。
1724
+ * 宽度变宽时既不清前帧也不重排已渲染历史 → 视觉上"没响应"。
1725
+ *
1726
+ * workaround:先 resize(cols-1) 让 Ink 走 width-shrink 分支强制清屏,
1727
+ * 50ms 后再 resize(cols) 回到目标尺寸。代价是程序会多一帧重画。
1728
+ *
1729
+ * alt-screen 程序(vim/tmux/htop/less)自己会在 SIGWINCH 时整屏重画,
1730
+ * 不需要也不应该 double-pulse(多余 SIGWINCH 让它们闪一下)→ 短路。
1702
1731
  */
1703
1732
  resize(cols, rows) {
1704
1733
  if (!this.process || this._exited)
@@ -1707,8 +1736,30 @@ var init_pty_manager = __esm({
1707
1736
  logger.debug({ cols, rows }, "PTY resize \u8DF3\u8FC7\uFF08\u540C\u5C3A\u5BF8\uFF09");
1708
1737
  return;
1709
1738
  }
1710
- logger.info({ cols, rows, prevCols: this._cols, prevRows: this._rows }, "PTY resize \u6267\u884C");
1739
+ logger.info({ cols, rows, prevCols: this._cols, prevRows: this._rows, alt: this._inAltScreen }, "PTY resize \u6267\u884C");
1711
1740
  try {
1741
+ if (this._doublePulseTimer) {
1742
+ clearTimeout(this._doublePulseTimer);
1743
+ this._doublePulseTimer = null;
1744
+ }
1745
+ const shouldDoublePulse = !this._inAltScreen && cols > this._cols && cols > 2;
1746
+ if (shouldDoublePulse) {
1747
+ this.process.resize(cols - 1, rows);
1748
+ this._doublePulseTimer = setTimeout(() => {
1749
+ this._doublePulseTimer = null;
1750
+ if (!this.process || this._exited)
1751
+ return;
1752
+ try {
1753
+ this.process.resize(cols, rows);
1754
+ this._cols = cols;
1755
+ this._rows = rows;
1756
+ this.emit("resize", cols, rows);
1757
+ } catch (err) {
1758
+ logger.error({ err, cols, rows }, "PTY resize \u7B2C\u4E8C\u8109\u51B2\u5931\u8D25");
1759
+ }
1760
+ }, DOUBLE_PULSE_DELAY_MS);
1761
+ return;
1762
+ }
1712
1763
  this.process.resize(cols, rows);
1713
1764
  this._cols = cols;
1714
1765
  this._rows = rows;
@@ -1723,6 +1774,10 @@ var init_pty_manager = __esm({
1723
1774
  * 幂等:多次调用安全
1724
1775
  */
1725
1776
  destroy() {
1777
+ if (this._doublePulseTimer) {
1778
+ clearTimeout(this._doublePulseTimer);
1779
+ this._doublePulseTimer = null;
1780
+ }
1726
1781
  if (!this.process)
1727
1782
  return;
1728
1783
  try {
@@ -1733,6 +1788,32 @@ var init_pty_manager = __esm({
1733
1788
  }
1734
1789
  this.process = null;
1735
1790
  }
1791
+ /**
1792
+ * 当前是否处于 alt-screen(仅供 resize 决策用,不暴露给上层)
1793
+ */
1794
+ get inAltScreen() {
1795
+ return this._inAltScreen;
1796
+ }
1797
+ /**
1798
+ * 扫描 PTY 输出,识别 alt-screen 切换序列:
1799
+ * - DECSET 1049(最常用,xterm 标准 + 保存光标位置):进入 / 退出
1800
+ * - DECSET 1047(仅 alt buffer 切换):进入 / 退出
1801
+ * - DECSET 47(最老的 alt-buffer,无光标保存):进入 / 退出
1802
+ *
1803
+ * 不需要完整 ANSI 解析器——这三个序列形态固定,正则简单匹配即可。
1804
+ * 同一 chunk 内可能既有 enter 又有 exit(罕见,但可能),按顺序处理。
1805
+ */
1806
+ scanAltScreenToggle(data) {
1807
+ const re = /\x1b\[\?(1049|1047|47)([hl])/g;
1808
+ let m;
1809
+ while ((m = re.exec(data)) !== null) {
1810
+ const isEnter = m[2] === "h";
1811
+ if (this._inAltScreen !== isEnter) {
1812
+ this._inAltScreen = isEnter;
1813
+ logger.debug({ inAltScreen: isEnter }, "PTY alt-screen \u72B6\u6001\u5207\u6362");
1814
+ }
1815
+ }
1816
+ }
1736
1817
  };
1737
1818
  }
1738
1819
  });
@@ -2008,7 +2089,7 @@ function handleWsMessage(ws, raw, cb) {
2008
2089
  break;
2009
2090
  case "resize":
2010
2091
  if (typeof msg.cols === "number" && typeof msg.rows === "number") {
2011
- cb.onResize(msg.cols, msg.rows);
2092
+ cb.onResize(msg.cols, msg.rows, ws, msg.master === true);
2012
2093
  } else {
2013
2094
  logger.warn({ msg }, "resize \u7F3A cols/rows \u5B57\u6BB5\u6216\u7C7B\u578B\u9519\u8BEF");
2014
2095
  }
@@ -2018,6 +2099,13 @@ function handleWsMessage(ws, raw, cb) {
2018
2099
  ws.send(JSON.stringify({ type: "heartbeat", timestamp: Date.now() }));
2019
2100
  }
2020
2101
  break;
2102
+ case "client_log":
2103
+ if (typeof msg.message === "string" && typeof msg.level === "string") {
2104
+ const ts = typeof msg.ts === "number" ? new Date(msg.ts).toISOString().slice(11, 23) : "?";
2105
+ process.stderr.write(`[client ${ts} ${msg.level}] ${msg.message}
2106
+ `);
2107
+ }
2108
+ break;
2021
2109
  default: {
2022
2110
  const t = msg.type;
2023
2111
  logger.warn({ type: t }, "\u672A\u77E5 WS \u6D88\u606F\u7C7B\u578B\uFF0C\u5DF2\u5FFD\u7565");
@@ -2124,6 +2212,7 @@ var init_ansi_filter = __esm({
2124
2212
  });
2125
2213
 
2126
2214
  // backend/dist/session/session-controller.js
2215
+ import { WebSocket as WebSocket3 } from "ws";
2127
2216
  var SessionController;
2128
2217
  var init_session_controller = __esm({
2129
2218
  "backend/dist/session/session-controller.js"() {
@@ -2157,6 +2246,12 @@ var init_session_controller = __esm({
2157
2246
  wsBackpressureEvents = 0;
2158
2247
  /** 可选的 hook 接收器(阶段 3 启用) */
2159
2248
  hookReceiver = null;
2249
+ /**
2250
+ * PTY 尺寸主控连接:声明 master=true 的客户端 WebSocket 引用。
2251
+ * 仅它能改 PTY cols/rows;其他客户端的 resize 被忽略。断开时自动释放
2252
+ * (onDisconnect 处理)。null = 无主控,先到先得
2253
+ */
2254
+ masterClient = null;
2160
2255
  /** ANSI 过滤器(阶段 8 启用;null = 关闭过滤直接透传) */
2161
2256
  ansiFilter;
2162
2257
  /** 可选的 PushService(阶段 9 启用;用于 hook 触发时推送通知) */
@@ -2340,8 +2435,18 @@ var init_session_controller = __esm({
2340
2435
  onUserInput: (data) => {
2341
2436
  this.pty.write(data);
2342
2437
  },
2343
- onResize: (cols, rows) => {
2438
+ onResize: (cols, rows, source, master) => {
2344
2439
  const counts = this.ws.getClientCounts();
2440
+ if (master) {
2441
+ this.masterClient = source;
2442
+ logger.info({ cols, rows, type }, "PTY \u4E3B\u63A7\u5207\u6362\uFF1A\u5BA2\u6237\u7AEF\u58F0\u660E master");
2443
+ this.pty.resize(cols, rows);
2444
+ return;
2445
+ }
2446
+ if (this.masterClient && this.masterClient !== source) {
2447
+ logger.debug({ cols, rows }, "\u4E3B\u63A7\u88AB\u5176\u4ED6\u5BA2\u6237\u7AEF\u6301\u6709\uFF0C\u5FFD\u7565\u6B64 resize");
2448
+ return;
2449
+ }
2345
2450
  if (counts.webapp > 0 && type === "attach") {
2346
2451
  logger.debug({ type, cols, rows, counts }, "webapp \u5728\u7EBF\uFF0Cattach \u7684 resize \u88AB\u5FFD\u7565");
2347
2452
  return;
@@ -2363,6 +2468,10 @@ var init_session_controller = __esm({
2363
2468
  });
2364
2469
  this.ws.onDisconnect((counts) => {
2365
2470
  logger.debug(counts, "\u5BA2\u6237\u7AEF\u65AD\u5F00\u540E\u5269\u4F59\u7EDF\u8BA1");
2471
+ if (this.masterClient && this.masterClient.readyState !== WebSocket3.OPEN) {
2472
+ logger.info("PTY \u4E3B\u63A7\u8FDE\u63A5\u5DF2\u65AD\u5F00\uFF0C\u91CA\u653E\u4E3B\u63A7\u9501");
2473
+ this.masterClient = null;
2474
+ }
2366
2475
  if (counts.webapp === 0 && counts.attach > 0) {
2367
2476
  this.ws.broadcast({
2368
2477
  type: "terminal_resize",
@@ -4215,18 +4324,30 @@ ${hint}
4215
4324
  if (isHeadless) {
4216
4325
  startPty("immediate");
4217
4326
  } else if (mustWaitEnter) {
4218
- void waitForUserConfirm().then(() => startPty("enter")).catch((err) => {
4327
+ const wait = waitForUserConfirm();
4328
+ void wait.promise.then((r) => {
4329
+ if (r.done === "enter")
4330
+ startPty("enter");
4331
+ }).catch((err) => {
4219
4332
  logger.error({ err }, "\u7B49\u5F85 Enter \u5931\u8D25");
4220
4333
  shutdown(1);
4221
4334
  });
4222
4335
  } else {
4336
+ const wait = waitForUserConfirm({ silent: true });
4337
+ const triggerSpawn = (reason) => {
4338
+ wait.cancel();
4339
+ startPty(reason);
4340
+ };
4223
4341
  ws.onConnect((_ws, type) => {
4224
4342
  if (type === "webapp")
4225
- startPty("webapp");
4343
+ triggerSpawn("webapp");
4344
+ });
4345
+ void wait.promise.then((r) => {
4346
+ if (r.done === "enter")
4347
+ triggerSpawn("enter");
4226
4348
  });
4227
- void waitForUserConfirm({ silent: true }).then(() => startPty("enter"));
4228
4349
  if (cfg.spawnTimeoutSec > 0) {
4229
- setTimeout(() => startPty("timeout"), cfg.spawnTimeoutSec * 1e3).unref();
4350
+ setTimeout(() => triggerSpawn("timeout"), cfg.spawnTimeoutSec * 1e3).unref();
4230
4351
  }
4231
4352
  }
4232
4353
  void registry.register({
@@ -4308,28 +4429,52 @@ function collectLocalHostnames() {
4308
4429
  return set;
4309
4430
  }
4310
4431
  function waitForUserConfirm(opts = {}) {
4311
- if (!process.stdin.isTTY)
4312
- return Promise.resolve();
4432
+ if (!process.stdin.isTTY) {
4433
+ return {
4434
+ promise: Promise.resolve({ done: "enter" }),
4435
+ cancel: () => {
4436
+ }
4437
+ };
4438
+ }
4313
4439
  if (!opts.silent) {
4314
4440
  process.stderr.write(" \u6309 Enter \u542F\u52A8\u5B50\u8FDB\u7A0B\uFF08\u6216 Ctrl+C \u9000\u51FA backend\uFF09...");
4315
4441
  }
4316
- return new Promise((resolve9) => {
4317
- const onData = (chunk) => {
4442
+ let cancelled = false;
4443
+ let onData = null;
4444
+ let resolveFn = null;
4445
+ const detach = () => {
4446
+ if (onData) {
4447
+ process.stdin.removeListener("data", onData);
4448
+ onData = null;
4449
+ }
4450
+ };
4451
+ const promise = new Promise((resolve9) => {
4452
+ resolveFn = resolve9;
4453
+ onData = (chunk) => {
4454
+ if (cancelled)
4455
+ return;
4318
4456
  if (chunk.length === 0)
4319
4457
  return;
4320
- cleanup();
4321
- if (!opts.silent) {
4458
+ detach();
4459
+ if (!opts.silent)
4322
4460
  process.stderr.write("\r\x1B[K");
4323
- }
4324
- resolve9();
4325
- };
4326
- const cleanup = () => {
4327
- process.stdin.removeListener("data", onData);
4328
- process.stdin.pause();
4461
+ resolve9({ done: "enter" });
4329
4462
  };
4330
4463
  process.stdin.resume();
4331
4464
  process.stdin.on("data", onData);
4332
4465
  });
4466
+ return {
4467
+ promise,
4468
+ cancel: () => {
4469
+ if (cancelled)
4470
+ return;
4471
+ cancelled = true;
4472
+ detach();
4473
+ if (!opts.silent)
4474
+ process.stderr.write("\r\x1B[K");
4475
+ resolveFn?.({ done: "cancelled" });
4476
+ }
4477
+ };
4333
4478
  }
4334
4479
  var BUILTIN_FULL_ALT_TUIS;
4335
4480
  var init_index = __esm({
@@ -4454,7 +4599,7 @@ var init_cli_stop = __esm({
4454
4599
  });
4455
4600
 
4456
4601
  // backend/dist/attach/attach-client.js
4457
- import WebSocket3 from "ws";
4602
+ import WebSocket4 from "ws";
4458
4603
  import { EventEmitter as EventEmitter4 } from "node:events";
4459
4604
  function normalizeAttachUrl(input) {
4460
4605
  let url;
@@ -4509,7 +4654,7 @@ var init_attach_client = __esm({
4509
4654
  if (this.destroyed)
4510
4655
  return;
4511
4656
  this.setStatus("connecting");
4512
- const ws = new WebSocket3(this.url);
4657
+ const ws = new WebSocket4(this.url);
4513
4658
  this.ws = ws;
4514
4659
  ws.on("open", () => {
4515
4660
  this.reconnectAttempt = 0;
@@ -4574,7 +4719,7 @@ var init_attach_client = __esm({
4574
4719
  }
4575
4720
  // ────────────────── 内部 ──────────────────
4576
4721
  send(msg) {
4577
- if (!this.ws || this.ws.readyState !== WebSocket3.OPEN)
4722
+ if (!this.ws || this.ws.readyState !== WebSocket4.OPEN)
4578
4723
  return;
4579
4724
  try {
4580
4725
  this.ws.send(JSON.stringify(msg));