@zhuxixi/pi-agent-board 0.4.2 → 0.5.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.
@@ -8,6 +8,7 @@ import { CURSOR_MARKER, Key, matchesKey, truncateToWidth, visibleWidth } from "@
8
8
  import { isProbablyEmptyPiInputLine } from "../core/pty-input.mjs";
9
9
  import { findHttpUrlAtCells, findWordRangeAtCells } from "../core/pty-links.mjs";
10
10
  import { createAttachOutputRenderScheduler, nextAttachRender, projectPtyCursor, shouldScheduleAttachRenderForMessage } from "../core/pty-attach-render.mjs";
11
+ import { evaluateAttachReconnect, shouldEscapeAttach } from "../core/pty-attach-reconnect.mjs";
11
12
  import { installImeCursorCoalesce } from "../core/ime-cursor-coalesce.mjs";
12
13
  import { createJiggleRetryController } from "../core/pty-attach-jiggle-controller.mjs";
13
14
  import { clampInt, parseMouseInputChunk, resolveWheelLines, scrollViewportTop, selectionDragScrollLines } from "../core/pty-scroll.mjs";
@@ -133,6 +134,13 @@ export class PtyAttachComponent implements Component {
133
134
  private osc52Carry = "";
134
135
  private passthroughCarry = "";
135
136
  private readonly connectStartedAt = Date.now();
137
+ // Set once the first socket connect succeeds; drives the reconnect give-up
138
+ // policy (short window after a live session dies, long window during cold
139
+ // host start — see pty-attach-reconnect.mjs).
140
+ private everConnected = false;
141
+ // Timestamp of the first disconnect that started the current reconnect
142
+ // cycle; reset on every successful (re)connect.
143
+ private disconnectedAt: number | null = null;
136
144
  // Lines scrolled per mouse-wheel event; configurable via $AGENT_BOARD_WHEEL_LINES.
137
145
  private readonly mouseWheelLines = resolveWheelLines();
138
146
  private readonly term: XtermLike;
@@ -235,7 +243,10 @@ export class PtyAttachComponent implements Component {
235
243
  matchesKey(data, Key.left) ||
236
244
  matchesKey(data, Key.ctrl("]"))
237
245
  ) {
238
- if (this.childInputLooksEmpty()) {
246
+ // While the socket is down the key can never reach the child, so
247
+ // escape unconditionally — the view must always be exitable, even
248
+ // after the host crashes mid-output (issue #48).
249
+ if (shouldEscapeAttach(this.connected, this.childInputLooksEmpty())) {
239
250
  this.detach();
240
251
  return;
241
252
  }
@@ -288,7 +299,7 @@ export class PtyAttachComponent implements Component {
288
299
 
289
300
  private loadingDetail(elapsedSeconds: number): string {
290
301
  if (this.status === "attached") return "Attached · waiting for the session to render…";
291
- if (this.status.startsWith("error") || this.status === "host exited") return this.status;
302
+ if (this.status.startsWith("error") || this.status === "host exited" || this.status === "host not reachable") return this.status;
292
303
  if (this.status === "disconnected") return `Reconnecting to the session host… ${elapsedSeconds}s`;
293
304
  return `Starting the session host… ${elapsedSeconds}s`;
294
305
  }
@@ -315,7 +326,10 @@ export class PtyAttachComponent implements Component {
315
326
 
316
327
  private connect(): void {
317
328
  if (this.closed || this.connected || this.socket) return;
318
- if (!existsSync(this.opts.socketPath)) {
329
+ // On Windows the control socket is a named pipe — it never exists as a
330
+ // filesystem entry, so existsSync can't gate the connect; a missing pipe
331
+ // surfaces as an error event on the socket and retries via scheduleReconnect.
332
+ if (process.platform !== "win32" && !existsSync(this.opts.socketPath)) {
319
333
  this.status = `starting host… ${Math.ceil((Date.now() - this.connectStartedAt) / 1000)}s`;
320
334
  this.scheduleReconnect();
321
335
  return;
@@ -325,6 +339,8 @@ export class PtyAttachComponent implements Component {
325
339
  socket.on("connect", () => {
326
340
  this.clearRetry();
327
341
  this.connected = true;
342
+ this.everConnected = true;
343
+ this.disconnectedAt = null;
328
344
  this.status = "attached";
329
345
  this.send({ type: "hello", clientId: `ui-${Date.now()}`, wantOutput: true });
330
346
  this.jiggleRetry.start(this.cols, this.rows);
@@ -336,6 +352,7 @@ export class PtyAttachComponent implements Component {
336
352
  socket.on("close", () => {
337
353
  this.socket = null;
338
354
  this.connected = false;
355
+ this.disconnectedAt ??= Date.now();
339
356
  if (!this.closed && this.status !== "host exited") {
340
357
  this.status = "disconnected";
341
358
  this.scheduleReconnect();
@@ -345,6 +362,7 @@ export class PtyAttachComponent implements Component {
345
362
  socket.on("error", (err) => {
346
363
  this.socket = null;
347
364
  this.connected = false;
365
+ this.disconnectedAt ??= Date.now();
348
366
  if (this.closed) return;
349
367
  this.status = `waiting for host… ${err.message}`;
350
368
  this.scheduleReconnect();
@@ -365,6 +383,17 @@ export class PtyAttachComponent implements Component {
365
383
 
366
384
  private scheduleReconnect(): void {
367
385
  if (this.closed || this.retryTimer) return;
386
+ const verdict = evaluateAttachReconnect({
387
+ everConnected: this.everConnected,
388
+ disconnectedAt: this.disconnectedAt,
389
+ connectStartedAt: this.connectStartedAt,
390
+ now: Date.now(),
391
+ });
392
+ if (verdict.giveUp) {
393
+ this.status = verdict.status ?? "host exited";
394
+ this.scheduleRender();
395
+ return;
396
+ }
368
397
  this.retryTimer = setTimeout(() => {
369
398
  this.retryTimer = null;
370
399
  this.connect();