@linxiraos/pi-utils 1.0.0 → 1.0.2

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.
Files changed (132) hide show
  1. package/CHANGELOG.md +41 -6
  2. package/package.json +56 -57
  3. package/src/dirs.ts +99 -91
  4. package/src/fetch-retry.ts +11 -2
  5. package/src/json-parse.ts +18 -7
  6. package/src/logger.ts +55 -20
  7. package/src/ptree.ts +2 -2
  8. package/src/stderr-guard.ts +1 -1
  9. package/src/stream.ts +27 -3
  10. package/src/vendor/mermaid-ascii/ascii/canvas.ts +8 -6
  11. package/src/vendor/mermaid-ascii/ascii/draw.ts +21 -14
  12. package/src/vendor/mermaid-ascii/text-metrics.ts +3 -0
  13. package/dist/types/abortable.d.ts +0 -32
  14. package/dist/types/acp/connection.d.ts +0 -118
  15. package/dist/types/acp/protocol.d.ts +0 -526
  16. package/dist/types/acp/schema.d.ts +0 -41
  17. package/dist/types/acp/stream.d.ts +0 -8
  18. package/dist/types/acp/transport.d.ts +0 -81
  19. package/dist/types/acp.d.ts +0 -6
  20. package/dist/types/async.d.ts +0 -20
  21. package/dist/types/binary.d.ts +0 -17
  22. package/dist/types/browsers.d.ts +0 -68
  23. package/dist/types/chalk.d.ts +0 -125
  24. package/dist/types/cli.d.ts +0 -134
  25. package/dist/types/color.d.ts +0 -102
  26. package/dist/types/dates.d.ts +0 -7
  27. package/dist/types/dirs.d.ts +0 -282
  28. package/dist/types/docx/converter.d.ts +0 -46
  29. package/dist/types/docx/xml.d.ts +0 -26
  30. package/dist/types/docx/zip.d.ts +0 -6
  31. package/dist/types/docx.d.ts +0 -11
  32. package/dist/types/dom/core.d.ts +0 -431
  33. package/dist/types/dom/parser.d.ts +0 -7
  34. package/dist/types/dom/selector.d.ts +0 -5
  35. package/dist/types/dom.d.ts +0 -5
  36. package/dist/types/env.d.ts +0 -124
  37. package/dist/types/fetch-retry.d.ts +0 -96
  38. package/dist/types/file-lock.d.ts +0 -23
  39. package/dist/types/format.d.ts +0 -37
  40. package/dist/types/frontmatter.d.ts +0 -46
  41. package/dist/types/fs-error.d.ts +0 -31
  42. package/dist/types/glob.d.ts +0 -28
  43. package/dist/types/headers.d.ts +0 -34
  44. package/dist/types/index.d.ts +0 -39
  45. package/dist/types/json-parse.d.ts +0 -74
  46. package/dist/types/json.d.ts +0 -14
  47. package/dist/types/logger/rotating-file.d.ts +0 -18
  48. package/dist/types/logger.d.ts +0 -96
  49. package/dist/types/loop-phase.d.ts +0 -10
  50. package/dist/types/lru.d.ts +0 -46
  51. package/dist/types/marked/core.d.ts +0 -445
  52. package/dist/types/marked.d.ts +0 -2
  53. package/dist/types/mermaid-ascii.d.ts +0 -11
  54. package/dist/types/mime.d.ts +0 -29
  55. package/dist/types/module-timer.d.ts +0 -1
  56. package/dist/types/path-tree.d.ts +0 -76
  57. package/dist/types/path.d.ts +0 -2
  58. package/dist/types/peek-file.d.ts +0 -29
  59. package/dist/types/postmortem.d.ts +0 -104
  60. package/dist/types/process-name.d.ts +0 -7
  61. package/dist/types/procmgr.d.ts +0 -74
  62. package/dist/types/prompt.d.ts +0 -18
  63. package/dist/types/ptree.d.ts +0 -110
  64. package/dist/types/readability/readability.d.ts +0 -9
  65. package/dist/types/readability/readerable.d.ts +0 -10
  66. package/dist/types/readability/types.d.ts +0 -70
  67. package/dist/types/readability.d.ts +0 -4
  68. package/dist/types/ring.d.ts +0 -93
  69. package/dist/types/runtime-install.d.ts +0 -78
  70. package/dist/types/sanitize-text.d.ts +0 -29
  71. package/dist/types/snowflake.d.ts +0 -25
  72. package/dist/types/stderr-guard.d.ts +0 -22
  73. package/dist/types/stream.d.ts +0 -67
  74. package/dist/types/tab-spacing.d.ts +0 -24
  75. package/dist/types/temp.d.ts +0 -17
  76. package/dist/types/template.d.ts +0 -62
  77. package/dist/types/timing-buffer.d.ts +0 -22
  78. package/dist/types/tls-fetch.d.ts +0 -37
  79. package/dist/types/turndown/gfm.d.ts +0 -11
  80. package/dist/types/turndown/html.d.ts +0 -5
  81. package/dist/types/turndown/service.d.ts +0 -21
  82. package/dist/types/turndown/types.d.ts +0 -70
  83. package/dist/types/turndown.d.ts +0 -4
  84. package/dist/types/type-guards.d.ts +0 -5
  85. package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +0 -41
  86. package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +0 -89
  87. package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +0 -7
  88. package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +0 -12
  89. package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +0 -66
  90. package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +0 -48
  91. package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +0 -43
  92. package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +0 -7
  93. package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +0 -56
  94. package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +0 -65
  95. package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +0 -27
  96. package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +0 -17
  97. package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +0 -7
  98. package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +0 -11
  99. package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +0 -34
  100. package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +0 -11
  101. package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +0 -11
  102. package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +0 -26
  103. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +0 -31
  104. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +0 -11
  105. package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +0 -59
  106. package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +0 -17
  107. package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +0 -30
  108. package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +0 -55
  109. package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +0 -206
  110. package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +0 -51
  111. package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +0 -2
  112. package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +0 -6
  113. package/dist/types/vendor/mermaid-ascii/class/types.d.ts +0 -102
  114. package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +0 -6
  115. package/dist/types/vendor/mermaid-ascii/er/types.d.ts +0 -76
  116. package/dist/types/vendor/mermaid-ascii/index.d.ts +0 -1
  117. package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +0 -9
  118. package/dist/types/vendor/mermaid-ascii/parser.d.ts +0 -7
  119. package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +0 -6
  120. package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +0 -130
  121. package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +0 -21
  122. package/dist/types/vendor/mermaid-ascii/types.d.ts +0 -114
  123. package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +0 -25
  124. package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +0 -6
  125. package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +0 -145
  126. package/dist/types/version.d.ts +0 -18
  127. package/dist/types/vterm/buffer.d.ts +0 -99
  128. package/dist/types/vterm/terminal.d.ts +0 -44
  129. package/dist/types/vterm.d.ts +0 -8
  130. package/dist/types/which.d.ts +0 -37
  131. package/dist/types/worker-host.d.ts +0 -51
  132. package/dist/types/xml.d.ts +0 -31
package/src/logger.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Centralized logger for omp.
2
+ * Centralized logger for zeta.
3
3
  *
4
- * Default: rotating `~/.omp/logs/omp.<DATE>.<PID>.log`, no console output (writing
4
+ * Default: rotating `~/.zeta/logs/zeta.<DATE>.<PID>.log`, no console output (writing
5
5
  * to stdout/stderr would corrupt the TUI). Long-running headless services
6
6
  * (the auth broker, etc.) call {@link setTransports} to swap in a console
7
7
  * transport so a process supervisor (pm2, journald, k8s) captures the logs.
@@ -53,9 +53,11 @@ function emitToSinks(level: LogLevel, message: string, context: Record<string, u
53
53
  }
54
54
  }
55
55
 
56
- const PROCESS_LOG_PATTERN = /^omp\.\d{4}-\d{2}-\d{2}\.(\d+)\.log(?:\.\d+)?$/;
57
- const PROCESS_AUDIT_PATTERN = /^\.omp\.(\d+)-audit\.json$/;
58
- const RETAINED_STALE_LOG_FILES = 5;
56
+ const PROCESS_LOG_PATTERN = /^zeta\.(\d{4}-\d{2}-\d{2})\.(\d+)\.log(?:\.(\d+))?$/;
57
+ const PROCESS_AUDIT_PATTERN = /^\.zeta\.(\d+)-audit\.json$/;
58
+ const RETAINED_STALE_LOGS_PER_PROCESS_DAY = 1;
59
+ const RETAINED_STALE_AUDIT_FILES = 0;
60
+ const RETAINED_STALE_LOG_DAYS = 5;
59
61
 
60
62
  function processIsRunning(pid: number): boolean {
61
63
  try {
@@ -67,8 +69,10 @@ function processIsRunning(pid: number): boolean {
67
69
  }
68
70
 
69
71
  /**
70
- * Retain the newest completed-process logs globally and remove their one-use
71
- * audit files. Live PID namespaces are never touched.
72
+ * Retain one newest completed-process log per process/day within the current
73
+ * and previous four local calendar days, and remove one-use audit files. Live
74
+ * PID namespaces are never touched. The calendar-day boundary preserves daily
75
+ * diagnostic coverage while bounding completed-process storage and scans.
72
76
  */
73
77
  function pruneStaleProcessLogs(dir: string): void {
74
78
  let entries: fs.Dirent[];
@@ -77,38 +81,69 @@ function pruneStaleProcessLogs(dir: string): void {
77
81
  } catch {
78
82
  return;
79
83
  }
80
-
81
- const staleLogs: Array<{ path: string; mtimeMs: number }> = [];
84
+ const current = new Date();
85
+ const currentDate =
86
+ `${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, "0")}-` +
87
+ String(current.getDate()).padStart(2, "0");
88
+ const cutoff = new Date(current);
89
+ cutoff.setDate(cutoff.getDate() - (RETAINED_STALE_LOG_DAYS - 1));
90
+ const cutoffDate =
91
+ `${cutoff.getFullYear()}-${String(cutoff.getMonth() + 1).padStart(2, "0")}-` +
92
+ String(cutoff.getDate()).padStart(2, "0");
93
+
94
+ const staleLogsByProcessDay = new Map<string, Array<{ path: string; mtimeMs: number; rollover: number }>>();
82
95
  for (const entry of entries) {
83
96
  if (!entry.isFile()) continue;
84
97
  const logMatch = PROCESS_LOG_PATTERN.exec(entry.name);
85
98
  const auditMatch = PROCESS_AUDIT_PATTERN.exec(entry.name);
86
- const pidText = logMatch?.[1] ?? auditMatch?.[1];
99
+ const pidText = logMatch?.[2] ?? auditMatch?.[1];
87
100
  if (!pidText || processIsRunning(Number(pidText))) continue;
88
101
  const entryPath = path.join(dir, entry.name);
89
102
 
90
103
  if (auditMatch) {
104
+ if (RETAINED_STALE_AUDIT_FILES === 0) {
105
+ try {
106
+ fs.rmSync(entryPath, { force: true });
107
+ } catch {
108
+ // Retention is best-effort; logging must still initialize.
109
+ }
110
+ }
111
+ continue;
112
+ }
113
+ if (!logMatch?.[1]) continue;
114
+ if (logMatch[1] < cutoffDate || logMatch[1] > currentDate) {
91
115
  try {
92
116
  fs.rmSync(entryPath, { force: true });
93
117
  } catch {
94
- // Retention is best-effort; logging must still initialize.
118
+ // Another process may have pruned the same stale namespace.
95
119
  }
96
120
  continue;
97
121
  }
98
122
 
99
123
  try {
100
- staleLogs.push({ path: entryPath, mtimeMs: fs.statSync(entryPath).mtimeMs });
124
+ const key = `${pidText}:${logMatch[1]}`;
125
+ const staleLogs = staleLogsByProcessDay.get(key) ?? [];
126
+ staleLogs.push({
127
+ path: entryPath,
128
+ mtimeMs: fs.statSync(entryPath).mtimeMs,
129
+ rollover: Number(logMatch[3] ?? 0),
130
+ });
131
+ staleLogsByProcessDay.set(key, staleLogs);
101
132
  } catch {
102
133
  // Another process may have pruned the same stale namespace.
103
134
  }
104
135
  }
105
136
 
106
- staleLogs.sort((a, b) => b.mtimeMs - a.mtimeMs);
107
- for (const stale of staleLogs.slice(RETAINED_STALE_LOG_FILES)) {
108
- try {
109
- fs.rmSync(stale.path, { force: true });
110
- } catch {
111
- // Another process may have pruned the same stale namespace.
137
+ for (const staleLogs of staleLogsByProcessDay.values()) {
138
+ staleLogs.sort(
139
+ (a, b) => b.mtimeMs - a.mtimeMs || b.rollover - a.rollover || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0),
140
+ );
141
+ for (const stale of staleLogs.slice(RETAINED_STALE_LOGS_PER_PROCESS_DAY)) {
142
+ try {
143
+ fs.rmSync(stale.path, { force: true });
144
+ } catch {
145
+ // Another process may have pruned the same stale namespace.
146
+ }
112
147
  }
113
148
  }
114
149
  }
@@ -201,11 +236,11 @@ function makeFileTransport(dir?: string): RotatingFileSink {
201
236
  pruneStaleProcessLogs(logsDir);
202
237
  return new RotatingFileSink({
203
238
  directory: logsDir,
204
- filenamePrefix: "omp",
239
+ filenamePrefix: "zeta",
205
240
  filenameSuffix: String(process.pid),
206
241
  maxBytes: 10 * 1024 * 1024,
207
242
  maxFiles: 5,
208
- auditFile: path.join(logsDir, `.omp.${process.pid}-audit.json`),
243
+ auditFile: path.join(logsDir, `.zeta.${process.pid}-audit.json`),
209
244
  });
210
245
  }
211
246
 
package/src/ptree.ts CHANGED
@@ -217,11 +217,11 @@ export class ChildProcess<In extends InMask = InMask> {
217
217
  return this;
218
218
  }
219
219
 
220
- kill(reason?: Exception) {
220
+ kill(reason?: Exception, gracefulMs?: number) {
221
221
  if (reason && !this.#exitReasonPending) this.#exitReasonPending = reason;
222
222
  if (!this.proc.killed)
223
223
  void Process.fromPid(this.proc.pid)
224
- ?.terminate()
224
+ ?.terminate(gracefulMs === undefined ? undefined : { gracefulMs })
225
225
  ?.catch(e => void e);
226
226
  }
227
227
 
@@ -104,7 +104,7 @@ export function suppressTerminalStderr(options?: SuppressTerminalStderrOptions):
104
104
  try {
105
105
  const redirectPath = options?.redirectPath ?? getLogPath();
106
106
  // getLogsDir() only computes the path; the logger creates it lazily, so
107
- // on a fresh profile ~/.omp/logs may not exist yet. Create it here so
107
+ // on a fresh profile ~/.zeta/logs may not exist yet. Create it here so
108
108
  // diagnostics land in the log instead of falling through to /dev/null.
109
109
  fs.mkdirSync(path.dirname(redirectPath), { recursive: true });
110
110
  redirectFd = fs.openSync(redirectPath, "a");
package/src/stream.ts CHANGED
@@ -281,11 +281,16 @@ export async function* readSseJson<T>(
281
281
  * - `raw` is the list of decoded non-empty lines that made up the event,
282
282
  * preserved for diagnostic context (error reporting, debugging). The
283
283
  * dispatching blank line is not included.
284
+ * - `id` and `retry` are present only when the event carried valid fields with
285
+ * those names. Control-only events are yielded so reconnecting transports can
286
+ * retain the cursor and server-requested retry interval.
284
287
  */
285
288
  export interface ServerSentEvent {
286
289
  event: string | null;
287
290
  data: string;
288
291
  raw: string[];
292
+ id?: string;
293
+ retry?: number;
289
294
  }
290
295
 
291
296
  interface SseEventState {
@@ -296,6 +301,8 @@ interface SseEventState {
296
301
  // seen yet" (distinct from a `data:` field with an empty value).
297
302
  data: string | null;
298
303
  raw: string[];
304
+ id?: string;
305
+ retry?: number;
299
306
  }
300
307
 
301
308
  // Complete lines are decoded in one batch per source chunk. Each batch ends on
@@ -303,7 +310,7 @@ interface SseEventState {
303
310
  const SSE_DECODER = new TextDecoder("utf-8");
304
311
 
305
312
  function flushSseEvent(state: SseEventState): ServerSentEvent | null {
306
- if (state.event === null && state.data === null) {
313
+ if (state.event === null && state.data === null && state.id === undefined && state.retry === undefined) {
307
314
  state.raw = [];
308
315
  return null;
309
316
  }
@@ -312,9 +319,13 @@ function flushSseEvent(state: SseEventState): ServerSentEvent | null {
312
319
  data: state.data ?? "",
313
320
  raw: state.raw,
314
321
  };
322
+ if (state.id !== undefined) event.id = state.id;
323
+ if (state.retry !== undefined) event.retry = state.retry;
315
324
  state.event = null;
316
325
  state.data = null;
317
326
  state.raw = [];
327
+ state.id = undefined;
328
+ state.retry = undefined;
318
329
  return event;
319
330
  }
320
331
 
@@ -348,9 +359,22 @@ function pushSseLine(line: string, state: SseEventState): ServerSentEvent | null
348
359
  state.data += "\n";
349
360
  state.data += value;
350
361
  }
362
+ } else if (fieldName === "id") {
363
+ if (!value.includes("\0")) state.id = value;
364
+ } else if (fieldName === "retry" && value.length > 0) {
365
+ let valid = true;
366
+ for (let index = 0; index < value.length; index++) {
367
+ const code = value.charCodeAt(index);
368
+ if (code < 0x30 || code > 0x39) {
369
+ valid = false;
370
+ break;
371
+ }
372
+ }
373
+ if (valid) {
374
+ const retry = Number(value);
375
+ if (Number.isSafeInteger(retry)) state.retry = retry;
376
+ }
351
377
  }
352
- // `id` and `retry` are intentionally ignored — the providers we consume
353
- // don't use them, and the underlying transport handles reconnects itself.
354
378
  return null;
355
379
  }
356
380
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  import type { Canvas, DrawingCoord, RoleCanvas, CharRole, AsciiTheme, ColorMode } from './types'
10
10
  import { colorizeLine, DEFAULT_ASCII_THEME } from './ansi'
11
- import { displayWidth, toCells, WIDE_PAD } from '../text-metrics'
11
+ import { displayWidth, LABEL_SPACE, toCells, WIDE_PAD } from '../text-metrics'
12
12
 
13
13
  /**
14
14
  * Create a blank canvas filled with spaces.
@@ -189,7 +189,7 @@ export function isJunctionChar(c: string): boolean {
189
189
  * letter/digit test misses.
190
190
  */
191
191
  function isLabelChar(c: string): boolean {
192
- return c === WIDE_PAD || displayWidth(c) === 2 || /[\p{L}\p{N}]/u.test(c)
192
+ return c === LABEL_SPACE || c === WIDE_PAD || displayWidth(c) === 2 || /[\p{L}\p{N}]/u.test(c)
193
193
  }
194
194
 
195
195
  /**
@@ -268,7 +268,7 @@ export function mergeCanvases(
268
268
  for (let x = 0; x < overlay.length; x++) {
269
269
  for (let y = 0; y < overlay[0]!.length; y++) {
270
270
  const c = overlay[x]![y]!
271
- // WIDE_PAD cells are written atomically with their lead below
271
+ // Spaces are transparent; WIDE_PAD cells are written atomically with their lead below
272
272
  if (c === ' ' || c === WIDE_PAD) continue
273
273
  const mx = x + offset.x
274
274
  const my = y + offset.y
@@ -327,8 +327,8 @@ export function canvasToString(canvas: Canvas, options?: CanvasToStringOptions):
327
327
  let line = ''
328
328
  for (let x = 0; x <= maxX; x++) {
329
329
  const c = canvas[x]![y]!
330
- // Skip wide-glyph continuation cells: the glyph itself spans 2 columns
331
- if (c !== WIDE_PAD) line += c
330
+ // Skip wide-glyph continuation cells and restore opaque label spaces.
331
+ if (c !== WIDE_PAD) line += c === LABEL_SPACE ? ' ' : c
332
332
  }
333
333
  lines.push(line)
334
334
  } else {
@@ -338,7 +338,7 @@ export function canvasToString(canvas: Canvas, options?: CanvasToStringOptions):
338
338
  for (let x = 0; x <= maxX; x++) {
339
339
  const c = canvas[x]![y]!
340
340
  if (c === WIDE_PAD) continue
341
- chars.push(c)
341
+ chars.push(c === LABEL_SPACE ? ' ' : c)
342
342
  roles.push(roleCanvas[x]?.[y] ?? null)
343
343
  }
344
344
  lines.push(colorizeLine(chars, roles, theme, colorMode))
@@ -370,6 +370,8 @@ const VERTICAL_FLIP_MAP: Record<string, string> = {
370
370
  // Unicode corners
371
371
  '┌': '└', '└': '┌',
372
372
  '┐': '┘', '┘': '┐',
373
+ '╭': '╰', '╰': '╭',
374
+ '╮': '╯', '╯': '╮',
373
375
  // Unicode junctions (T-pieces flip vertically)
374
376
  '┬': '┴', '┴': '┬',
375
377
  // Box-start junctions (exit points from node boxes)
@@ -21,7 +21,7 @@ import { gridToDrawingCoord, lineToDrawing } from './grid'
21
21
  import { splitLines } from './multiline-utils'
22
22
  import { getCorners } from './shapes/corners'
23
23
  import { getShapeAttachmentPoint } from './shapes/index'
24
- import { displayWidth, toCells, WIDE_PAD } from '../text-metrics'
24
+ import { displayWidth, LABEL_SPACE, toCells, WIDE_PAD } from '../text-metrics'
25
25
 
26
26
  // ============================================================================
27
27
  // Node drawing — renders a node using shape-aware rendering
@@ -76,16 +76,18 @@ function drawBoxWithGridDimensions(node: AsciiNode, graph: AsciiGraph): Canvas {
76
76
  // Get corner characters for this shape type
77
77
  const corners = getCorners(node.shape, useAscii)
78
78
 
79
- // State-end uses double border to differentiate from state-start
80
- const isDoubleBox = node.shape === 'state-end'
81
- const hChar = useAscii ? (isDoubleBox ? '=' : '-') : (isDoubleBox ? '═' : '─')
82
- const vChar = useAscii ? (isDoubleBox ? '‖' : '|') : (isDoubleBox ? '║' : '│')
79
+ const isStateStart = node.shape === 'state-start'
80
+ const isStateEnd = node.shape === 'state-end'
81
+ const hChar = useAscii ? (isStateEnd ? '=' : '-') : (isStateEnd ? '═' : '─')
82
+ const vChar = useAscii ? (isStateEnd ? '‖' : '|') : (isStateEnd ? '║' : '│')
83
83
 
84
- // Double-box corners (for state-end)
85
- const doubleCorners = useAscii
84
+ const stateStartCorners = useAscii
85
+ ? { tl: '+', tr: '+', bl: '+', br: '+' }
86
+ : { tl: '╭', tr: '╮', bl: '╰', br: '╯' }
87
+ const stateEndCorners = useAscii
86
88
  ? { tl: '#', tr: '#', bl: '#', br: '#' }
87
89
  : { tl: '╔', tr: '╗', bl: '╚', br: '╝' }
88
- const effectiveCorners = isDoubleBox ? doubleCorners : corners
90
+ const effectiveCorners = isStateEnd ? stateEndCorners : isStateStart ? stateStartCorners : corners
89
91
 
90
92
  // Draw box border with shape-specific corners
91
93
  for (let x = from.x + 1; x < to.x; x++) box[x]![from.y] = hChar
@@ -97,8 +99,8 @@ function drawBoxWithGridDimensions(node: AsciiNode, graph: AsciiGraph): Canvas {
97
99
  box[from.x]![to.y] = effectiveCorners.bl
98
100
  box[to.x]![to.y] = effectiveCorners.br
99
101
 
100
- // Center the multi-line display label inside the box
101
- const label = node.displayLabel
102
+ // Pseudostates have no source label; restore their UML marker explicitly.
103
+ const label = node.displayLabel || (isStateStart ? (useAscii ? '*' : '●') : isStateEnd ? (useAscii ? '*' : '◎') : '')
102
104
  const lines = splitLines(label)
103
105
  const textCenterY = from.y + Math.floor(h / 2)
104
106
  const startY = textCenterY - Math.floor((lines.length - 1) / 2)
@@ -677,7 +679,7 @@ function drawTextOnLine(canvas: Canvas, line: DrawingCoord[], label: string, isU
677
679
  for (let i = 0; i < lines.length; i++) {
678
680
  const lineText = lines[i]!
679
681
  const startX = middleX - Math.floor(displayWidth(lineText) / 2)
680
- drawText(canvas, { x: startX, y: startY + i }, lineText)
682
+ drawText(canvas, { x: startX, y: startY + i }, lineText.replaceAll(' ', LABEL_SPACE))
681
683
  }
682
684
  }
683
685
 
@@ -1223,7 +1225,8 @@ function fillRolesFromCanvases(
1223
1225
 
1224
1226
  /**
1225
1227
  * Special handling for node boxes: border chars get 'border' role, text gets 'text' role.
1226
- * Detects text by checking if character is alphanumeric or common punctuation.
1228
+ * Common final-state border characters (`#` and `=`) count only on the outer edge,
1229
+ * so identical characters inside ordinary node labels retain the text role.
1227
1230
  */
1228
1231
  function fillRolesForNodeBox(
1229
1232
  roleCanvas: RoleCanvas,
@@ -1231,7 +1234,9 @@ function fillRolesForNodeBox(
1231
1234
  offset: DrawingCoord,
1232
1235
  ): void {
1233
1236
  const isBorderChar = (c: string) => /^[┌┐└┘├┤┬┴┼│─╭╮╰╯+\-|.':]$/.test(c)
1234
-
1237
+ const isStateEndBorderChar = (c: string) => /^[╔╗╚╝═║#=‖]$/.test(c)
1238
+ const maxX = canvas.length - 1
1239
+ const maxY = (canvas[0]?.length ?? 1) - 1
1235
1240
  for (let x = 0; x < canvas.length; x++) {
1236
1241
  for (let y = 0; y < (canvas[0]?.length ?? 0); y++) {
1237
1242
  const char = canvas[x]?.[y]
@@ -1240,7 +1245,9 @@ function fillRolesForNodeBox(
1240
1245
  const ry = y + offset.y
1241
1246
  // Use setRole which auto-expands the role canvas if needed
1242
1247
  if (rx >= 0 && ry >= 0) {
1243
- setRole(roleCanvas, rx, ry, isBorderChar(char) ? 'border' : 'text')
1248
+ const isOuterEdge = x === 0 || x === maxX || y === 0 || y === maxY
1249
+ const role = isBorderChar(char) || (isOuterEdge && isStateEndBorderChar(char)) ? 'border' : 'text'
1250
+ setRole(roleCanvas, rx, ry, role)
1244
1251
  }
1245
1252
  }
1246
1253
  }
@@ -29,6 +29,9 @@
29
29
  */
30
30
  export const WIDE_PAD = '\u0000'
31
31
 
32
+ /** Opaque label-space placeholder that serializes back to a regular space. */
33
+ export const LABEL_SPACE = '\u0001'
34
+
32
35
  const graphemeSegmenter = new Intl.Segmenter()
33
36
 
34
37
  /**
@@ -1,32 +0,0 @@
1
- export declare class AbortError extends Error {
2
- constructor(signal: AbortSignal);
3
- }
4
- /**
5
- * Abortable async iteration over a {@link ReadableStream}. Reads the source
6
- * reader directly and yields each chunk, so the consumer's `for await` drives a
7
- * single read loop with no intermediate stream or per-chunk enqueue.
8
- *
9
- * Unlike `stream.pipeThrough(..., { signal })`, this explicitly cancels the
10
- * source reader on abort or early `break`, propagating HTTP-client disconnects
11
- * and watchdog timeouts to the backend request instead of only stopping the
12
- * local consumer. On abort it throws {@link AbortError}; the lock is released
13
- * on completion, abort, throw, or early exit. The source is cancelled only on
14
- * abort or early exit — never on natural EOF.
15
- */
16
- export declare function abortableSource<T>(stream: ReadableStream<T>, signal?: AbortSignal): AsyncGenerator<T>;
17
- /**
18
- * Runs a promise-returning function (`pr`). If the given AbortSignal is aborted before or during
19
- * execution, the promise is rejected with a standard error.
20
- *
21
- * @param signal - Optional AbortSignal to cancel the operation
22
- * @param pr - Function returning a promise to run
23
- * @returns Promise resolving as `pr` would, or rejecting on abort
24
- */
25
- export declare function untilAborted<T>(signal: AbortSignal | undefined | null, pr: Promise<T> | (() => Promise<T>)): Promise<T>;
26
- /**
27
- * Memoizes a function with no arguments, calling it once and caching the result.
28
- *
29
- * @param fn - Function to be called once
30
- * @returns A function that returns the cached result of `fn`
31
- */
32
- export declare function once<T>(fn: () => T): () => T;
@@ -1,118 +0,0 @@
1
- import type { Agent, AuthenticateRequest, AuthenticateResponse, CloseSessionRequest, CloseSessionResponse, CompleteElicitationNotification, CreateElicitationRequest, CreateElicitationResponse, CreateTerminalRequest, ForkSessionRequest, ForkSessionResponse, InitializeRequest, InitializeResponse, ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, LoadSessionResponse, MaybePromise, NewSessionRequest, NewSessionResponse, PromptRequest, PromptResponse, ReadTextFileRequest, ReadTextFileResponse, RequestPermissionRequest, RequestPermissionResponse, ResumeSessionRequest, ResumeSessionResponse, SessionNotification, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest, SetSessionModeResponse, TerminalActionResponse, TerminalOutputResponse, WaitForTerminalExitResponse, WriteTextFileRequest, WriteTextFileResponse } from "./protocol.js";
2
- import type { Stream } from "./stream.js";
3
- import { RpcConnection } from "./transport.js";
4
- /** Terminal creation response returned by a client. */
5
- export interface CreateTerminalResponse {
6
- terminalId: string;
7
- }
8
- /** Client callbacks handled by a client-side connection. */
9
- export interface Client {
10
- requestPermission(params: RequestPermissionRequest): MaybePromise<RequestPermissionResponse>;
11
- sessionUpdate(params: SessionNotification): MaybePromise<void>;
12
- readTextFile?(params: ReadTextFileRequest): MaybePromise<ReadTextFileResponse>;
13
- writeTextFile?(params: WriteTextFileRequest): MaybePromise<WriteTextFileResponse | void>;
14
- createTerminal?(params: CreateTerminalRequest): MaybePromise<CreateTerminalResponse>;
15
- terminalOutput?(params: {
16
- sessionId: string;
17
- terminalId: string;
18
- }): MaybePromise<TerminalOutputResponse>;
19
- waitForTerminalExit?(params: {
20
- sessionId: string;
21
- terminalId: string;
22
- }): MaybePromise<WaitForTerminalExitResponse>;
23
- killTerminal?(params: {
24
- sessionId: string;
25
- terminalId: string;
26
- }): MaybePromise<TerminalActionResponse | void>;
27
- releaseTerminal?(params: {
28
- sessionId: string;
29
- terminalId: string;
30
- }): MaybePromise<TerminalActionResponse | void>;
31
- unstable_createElicitation?(params: CreateElicitationRequest): MaybePromise<CreateElicitationResponse>;
32
- unstable_completeElicitation?(params: CompleteElicitationNotification): MaybePromise<void>;
33
- extMethod?(method: string, params: Record<string, unknown>): MaybePromise<Record<string, unknown>>;
34
- extNotification?(method: string, params: Record<string, unknown>): MaybePromise<void>;
35
- }
36
- /** Agent-side ACP connection. */
37
- export declare class AgentSideConnection {
38
- #private;
39
- constructor(toAgent: (connection: AgentSideConnection) => Agent, stream: Stream);
40
- /** Signal aborted when the transport closes. */
41
- get signal(): AbortSignal;
42
- /** Promise resolved when the transport closes. */
43
- get closed(): Promise<void>;
44
- /** Sends a session update notification. */
45
- sessionUpdate(params: SessionNotification): Promise<void>;
46
- /** Requests permission from the client. */
47
- requestPermission(params: RequestPermissionRequest): Promise<RequestPermissionResponse>;
48
- /** Reads a client-side text file. */
49
- readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse>;
50
- /** Writes a client-side text file. */
51
- writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse>;
52
- /** Creates a client terminal and returns its control handle. */
53
- createTerminal(params: CreateTerminalRequest): Promise<TerminalHandle>;
54
- /** Creates an interactive elicitation on the client. */
55
- unstable_createElicitation(params: CreateElicitationRequest): Promise<CreateElicitationResponse>;
56
- /** Notifies a client that URL elicitation completed. */
57
- unstable_completeElicitation(params: CompleteElicitationNotification): Promise<void>;
58
- /** Sends a custom request. */
59
- request<Response = unknown, Params = unknown>(method: string, params?: Params): Promise<Response>;
60
- /** Sends a custom notification. */
61
- notify<Params = unknown>(method: string, params?: Params): Promise<void>;
62
- /** Sends a legacy extension request. */
63
- extMethod(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>>;
64
- /** Sends a legacy extension notification. */
65
- extNotification(method: string, params: Record<string, unknown>): Promise<void>;
66
- }
67
- /** Handle for controlling a client-owned terminal. */
68
- export declare class TerminalHandle {
69
- #private;
70
- /** Client terminal identifier. */
71
- readonly id: string;
72
- constructor(id: string, sessionId: string, connection: RpcConnection);
73
- /** Gets current terminal output. */
74
- currentOutput(): Promise<TerminalOutputResponse>;
75
- /** Waits for terminal exit. */
76
- waitForExit(): Promise<WaitForTerminalExitResponse>;
77
- /** Kills the terminal. */
78
- kill(): Promise<TerminalActionResponse>;
79
- /** Releases the terminal. */
80
- release(): Promise<TerminalActionResponse | void>;
81
- /** Releases the terminal during async disposal. */
82
- [Symbol.asyncDispose](): Promise<void>;
83
- }
84
- /** Client-side ACP connection used to call an agent. */
85
- export declare class ClientSideConnection {
86
- #private;
87
- constructor(toClient: (connection: ClientSideConnection) => Client, stream: Stream);
88
- /** Signal aborted when the transport closes. */
89
- get signal(): AbortSignal;
90
- /** Promise resolved when the transport closes. */
91
- get closed(): Promise<void>;
92
- /** Initializes protocol capabilities. */
93
- initialize(params: InitializeRequest): Promise<InitializeResponse>;
94
- /** Authenticates with an agent. */
95
- authenticate(params: AuthenticateRequest): Promise<AuthenticateResponse>;
96
- /** Creates a session. */
97
- newSession(params: NewSessionRequest): Promise<NewSessionResponse>;
98
- /** Loads a session. */
99
- loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse>;
100
- /** Lists sessions. */
101
- listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse>;
102
- /** Resumes a session. */
103
- resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse>;
104
- /** Forks a session. */
105
- unstable_forkSession(params: ForkSessionRequest): Promise<ForkSessionResponse>;
106
- /** Closes a session. */
107
- closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse>;
108
- /** Sets a session mode. */
109
- setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse>;
110
- /** Sets a session config option. */
111
- setSessionConfigOption(params: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse>;
112
- /** Sends a prompt. */
113
- prompt(params: PromptRequest): Promise<PromptResponse>;
114
- /** Cancels an active prompt. */
115
- cancel(params: {
116
- sessionId: string;
117
- }): Promise<void>;
118
- }