quotacap 0.0.41 → 0.0.43

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 (56) hide show
  1. package/dist/adapters/claude.js +16 -5
  2. package/dist/adapters/codex.d.ts +2 -1
  3. package/dist/adapters/codex.js +19 -8
  4. package/dist/adapters/goldens.d.ts +4 -0
  5. package/dist/adapters/goldens.js +20 -0
  6. package/dist/adapters/grok.d.ts +2 -1
  7. package/dist/adapters/grok.js +26 -12
  8. package/dist/adapters/index.d.ts +20 -6
  9. package/dist/adapters/index.js +96 -37
  10. package/dist/adapters/kimi.d.ts +2 -1
  11. package/dist/adapters/kimi.js +24 -13
  12. package/dist/adapters/muse.d.ts +2 -1
  13. package/dist/adapters/muse.js +30 -15
  14. package/dist/adapters/opencode-go.d.ts +1 -1
  15. package/dist/adapters/opencode-go.js +8 -1
  16. package/dist/adapters/pty.d.ts +7 -0
  17. package/dist/adapters/pty.js +32 -2
  18. package/dist/adapters/vt.d.ts +19 -0
  19. package/dist/adapters/vt.js +506 -0
  20. package/dist/cli/providers.js +2 -1
  21. package/dist/config.d.ts +12 -23
  22. package/dist/config.js +24 -0
  23. package/dist/diagnostics/bundle.d.ts +22 -0
  24. package/dist/diagnostics/bundle.js +125 -0
  25. package/dist/diagnostics/failure.d.ts +8 -1
  26. package/dist/diagnostics/failure.js +43 -2
  27. package/dist/format/rows.d.ts +1 -1
  28. package/dist/format/rows.js +9 -6
  29. package/dist/mcp/server.d.ts +2 -2
  30. package/dist/runtime/poll.d.ts +4 -0
  31. package/dist/runtime/poll.js +30 -1
  32. package/dist/runtime/service.js +3 -1
  33. package/dist/version.js +1 -1
  34. package/dist/web/src/App.js +1 -1
  35. package/dist/web/src/components/AdviceDrawer.js +1 -1
  36. package/dist/web/src/components/OpencodeGoBanner.d.ts +1 -1
  37. package/dist/web/src/components/OpencodeGoBanner.js +9 -2
  38. package/dist/web/src/components/PaceBar.d.ts +9 -4
  39. package/dist/web/src/components/PaceBar.js +26 -7
  40. package/dist/web/src/components/ProviderCard.js +2 -3
  41. package/dist/web/src/components/ProviderDrawer.js +3 -3
  42. package/dist/web/src/components/ProviderIcon.js +1 -1
  43. package/dist/web/src/components/ProviderRow.d.ts +1 -1
  44. package/dist/web/src/components/ProviderRow.js +2 -4
  45. package/dist/web/src/components/Recommendation.js +1 -1
  46. package/dist/web/src/components/ResetRail.js +1 -1
  47. package/dist/web/src/components/WindowsRow.d.ts +1 -1
  48. package/dist/web/src/components/WindowsRow.js +4 -4
  49. package/dist/web/src/components/settings/SettingsDrawer.js +1 -1
  50. package/dist/webAssets.js +1 -1
  51. package/dist/webHtml.js +1 -1
  52. package/package.json +5 -5
  53. package/web/dist/assets/{index-Dk6l5h27.css → index-C6ltEeGL.css} +1 -1
  54. package/web/dist/assets/index-DRDDTtxF.js +13 -0
  55. package/web/dist/index.html +2 -2
  56. package/web/dist/assets/index-BUK8xUHm.js +0 -12
@@ -22,6 +22,13 @@ export interface PtyRunOptions {
22
22
  */
23
23
  submitInput?: string;
24
24
  submitAfterMs?: number;
25
+ /**
26
+ * Grace delay after the completion pattern first matches, before the
27
+ * child is killed. Usage dialogs paint asynchronously (header first,
28
+ * timestamp later); without this the kill can land mid-paint. Bounded
29
+ * and fixed (not quiescence-detected: spinners never settle). Default 400.
30
+ */
31
+ completionSettleMs?: number;
25
32
  /**
26
33
  * Opt-in terminal-query responder: answers DSR, DA1 and OSC 4/10/11
27
34
  * capability queries the child emits. Off by default; TUIs that never
@@ -222,6 +222,9 @@ async function runPtyBun(opts) {
222
222
  let transcript = "";
223
223
  let exited = false;
224
224
  let exitCode;
225
+ const startedAt = Date.now();
226
+ const marks = { start: 0 };
227
+ const mark = (k) => { marks[k] = Date.now() - startedAt; };
225
228
  // Genuine PTY via the Bun.spawn `terminal` option (verified on Bun 1.3.11):
226
229
  // the child's stdin, stdout, and stderr are all the PTY slave, input goes
227
230
  // through proc.terminal.write, and merged output arrives in `data`.
@@ -435,9 +438,11 @@ async function runPtyBun(opts) {
435
438
  });
436
439
  }
437
440
  }
441
+ mark("input");
438
442
  if (opts.completionRegex) {
439
443
  const deadline = Date.now() + opts.timeoutMs;
440
444
  let done = false;
445
+ let matched = false;
441
446
  while (Date.now() < deadline) {
442
447
  checkCap();
443
448
  checkAborted();
@@ -445,6 +450,8 @@ async function runPtyBun(opts) {
445
450
  checkAbort(clean);
446
451
  if (regexTest(opts.completionRegex, clean)) {
447
452
  done = true;
453
+ matched = true;
454
+ mark("completion");
448
455
  break;
449
456
  }
450
457
  if (exited) {
@@ -468,8 +475,15 @@ async function runPtyBun(opts) {
468
475
  checkpoint: "completion timeout",
469
476
  durationMs: opts.timeoutMs,
470
477
  stdout: transcript,
478
+ timings: { ...marks, completion: Date.now() - startedAt },
471
479
  });
472
480
  }
481
+ if (matched && !exited) {
482
+ await delay(opts.completionSettleMs ?? 400);
483
+ checkCap();
484
+ checkAborted();
485
+ checkAbort(stripAnsi(transcript));
486
+ }
473
487
  }
474
488
  else {
475
489
  const deadline = Date.now() + opts.timeoutMs;
@@ -513,7 +527,8 @@ async function runPtyBun(opts) {
513
527
  teardownAbort();
514
528
  if (e instanceof DiagnosticError)
515
529
  throw e;
516
- throw diagnosticError(e, { source: "pty", stdout: transcript, exitCode });
530
+ mark("end");
531
+ throw diagnosticError(e, { source: "pty", stdout: transcript, exitCode, timings: { ...marks } });
517
532
  }
518
533
  }
519
534
  async function runPtyNode(opts) {
@@ -542,6 +557,9 @@ async function runPtyNode(opts) {
542
557
  let transcript = "";
543
558
  let exited = false;
544
559
  let exitCode;
560
+ const startedAt = Date.now();
561
+ const marks = { start: 0 };
562
+ const mark = (k) => { marks[k] = Date.now() - startedAt; };
545
563
  const pty = getPty();
546
564
  let ptyProcess;
547
565
  try {
@@ -747,9 +765,11 @@ async function runPtyNode(opts) {
747
765
  });
748
766
  }
749
767
  }
768
+ mark("input");
750
769
  if (opts.completionRegex) {
751
770
  const deadline = Date.now() + opts.timeoutMs;
752
771
  let done = false;
772
+ let matched = false;
753
773
  while (Date.now() < deadline) {
754
774
  checkCap();
755
775
  checkAborted();
@@ -757,6 +777,8 @@ async function runPtyNode(opts) {
757
777
  checkAbort(clean);
758
778
  if (regexTest(opts.completionRegex, clean)) {
759
779
  done = true;
780
+ matched = true;
781
+ mark("completion");
760
782
  break;
761
783
  }
762
784
  if (exited) {
@@ -780,8 +802,15 @@ async function runPtyNode(opts) {
780
802
  checkpoint: "completion timeout",
781
803
  durationMs: opts.timeoutMs,
782
804
  stdout: transcript,
805
+ timings: { ...marks, completion: Date.now() - startedAt },
783
806
  });
784
807
  }
808
+ if (matched && !exited) {
809
+ await delay(opts.completionSettleMs ?? 400);
810
+ checkCap();
811
+ checkAborted();
812
+ checkAbort(stripAnsi(transcript));
813
+ }
785
814
  }
786
815
  else {
787
816
  const deadline = Date.now() + opts.timeoutMs;
@@ -808,7 +837,8 @@ async function runPtyNode(opts) {
808
837
  await kill();
809
838
  if (e instanceof DiagnosticError)
810
839
  throw e;
811
- throw diagnosticError(e, { source: "pty", stdout: transcript, exitCode });
840
+ mark("end");
841
+ throw diagnosticError(e, { source: "pty", stdout: transcript, exitCode, timings: { ...marks } });
812
842
  }
813
843
  finally {
814
844
  cleanup();
@@ -0,0 +1,19 @@
1
+ import type { ParsedQuota } from "./types.js";
2
+ export interface GridDims {
3
+ cols: number;
4
+ rows: number;
5
+ }
6
+ export declare const DEFAULT_GRID_DIMS: GridDims;
7
+ export declare function cellWidth(cp: number): 0 | 1 | 2;
8
+ /** Render the final visible screen of a PTY transcript. */
9
+ export declare function renderScreen(transcript: string, dims?: Partial<GridDims>): string;
10
+ /**
11
+ * Screen-first parsing with byte-soup fallback. The rendered screen is
12
+ * precise (overwrite artifacts resolved) but lossy in one case: a TUI that
13
+ * leaves alt-screen and clears on kill wipes the dialog the transcript
14
+ * captured earlier (observed: grok prints a resume line on exit). The
15
+ * cumulative soup never loses content but keeps cursor artifacts. So: parse
16
+ * the screen; on throw, or on an estimated reset the soup resolves exactly,
17
+ * parse the soup instead. Worst case equals the old soup-only behavior.
18
+ */
19
+ export declare function parseScreens(text: string, dims: Partial<GridDims> | undefined, parse: (cleaned: string) => ParsedQuota): ParsedQuota;
@@ -0,0 +1,506 @@
1
+ // Minimal VT100/xterm screen renderer: replays a PTY transcript onto a
2
+ // cols×rows grid and returns the visible screen text. Cursor-addressed TUIs
3
+ // write fragments out of order ("Weekly lim" + CUP + "t (Tier)"); regexing
4
+ // the raw byte stream sees "Weekly limt", but the screen shows the truth.
5
+ // Parsing the rendered screen kills that whole artifact class (the 2026-09-28
6
+ // "Resets: October5, 10:22" grok failure was a cursor step over a blank cell,
7
+ // which the screen shows as a space and the byte stream drops entirely).
8
+ //
9
+ // Supported: CUP/HVP, CUU/CUD/CUF/CUB/CNL/CPL/CHA/VPA, ED, EL, IL/DL, DCH/ECH,
10
+ // SU/SD, DECSTBM scroll regions, SGR (ignored), alt-screen enter (clears),
11
+ // save/restore cursor (CSI s/u and DECSC/DECRC), RI/IND/NEL/HT/BS/CR/LF,
12
+ // OSC skip, RIS. Unknown sequences are ignored, never fatal.
13
+ import { stripAnsi } from "./pty.js";
14
+ export const DEFAULT_GRID_DIMS = { cols: 140, rows: 50 };
15
+ // Approximate wcwidth: 0 for combining/format marks, 2 for wide ranges,
16
+ // 1 otherwise. Box drawing (U+2500-257F) and braille (U+2800-28FF) are
17
+ // width 1, which the fast path below gets right.
18
+ const ZERO_WIDTH = [
19
+ [0x0300, 0x036f],
20
+ [0x0488, 0x0489],
21
+ [0x0591, 0x05bd],
22
+ [0x05bf, 0x05bf],
23
+ [0x05c1, 0x05c2],
24
+ [0x05c4, 0x05c5],
25
+ [0x05c7, 0x05c7],
26
+ [0x0610, 0x0617],
27
+ [0x064b, 0x065b],
28
+ [0x0670, 0x0670],
29
+ [0x06d6, 0x06dc],
30
+ [0x200b, 0x200f],
31
+ [0x2028, 0x202e],
32
+ [0xfe00, 0xfe0f],
33
+ [0xfe20, 0xfe2f],
34
+ [0xe0100, 0xe01ef],
35
+ ];
36
+ const WIDE = [
37
+ [0x1100, 0x115f],
38
+ [0x231a, 0x231b],
39
+ [0x2329, 0x232a],
40
+ [0x23e9, 0x23ec],
41
+ [0x23f0, 0x23f0],
42
+ [0x23f3, 0x23f3],
43
+ [0x25fd, 0x25fe],
44
+ [0x2614, 0x2615],
45
+ [0x2648, 0x2653],
46
+ [0x267f, 0x267f],
47
+ [0x2693, 0x2693],
48
+ [0x26a1, 0x26a1],
49
+ [0x26aa, 0x26ab],
50
+ [0x26bd, 0x26be],
51
+ [0x26c4, 0x26c5],
52
+ [0x26ce, 0x26ce],
53
+ [0x26ea, 0x26ea],
54
+ [0x26f2, 0x26f3],
55
+ [0x26f5, 0x26f5],
56
+ [0x26fa, 0x26fa],
57
+ [0x2705, 0x2705],
58
+ [0x270a, 0x270b],
59
+ [0x2728, 0x2728],
60
+ [0x274c, 0x274c],
61
+ [0x274e, 0x274e],
62
+ [0x2753, 0x2755],
63
+ [0x2757, 0x2757],
64
+ [0x2795, 0x2797],
65
+ [0x27b0, 0x27b0],
66
+ [0x27bf, 0x27bf],
67
+ [0x2b1b, 0x2b1c],
68
+ [0x2b50, 0x2b50],
69
+ [0x2b55, 0x2b55],
70
+ [0x2e80, 0x303e],
71
+ [0x3041, 0x33ff],
72
+ [0x3400, 0x4dbf],
73
+ [0x4e00, 0x9fff],
74
+ [0xa000, 0xa4cf],
75
+ [0xac00, 0xd7a3],
76
+ [0xf900, 0xfaff],
77
+ [0xfe10, 0xfe19],
78
+ [0xfe30, 0xfe52],
79
+ [0xfe54, 0xfe66],
80
+ [0xfe68, 0xfe6b],
81
+ [0xff00, 0xff60],
82
+ [0xffe0, 0xffe6],
83
+ [0x16fe0, 0x16fe3],
84
+ [0x17000, 0x187ec],
85
+ [0x18800, 0x18af2],
86
+ [0x1b000, 0x1b001],
87
+ [0x1f004, 0x1f004],
88
+ [0x1f0cf, 0x1f0ff],
89
+ [0x1f18e, 0x1f251],
90
+ [0x1f300, 0x1f5ff],
91
+ [0x1f600, 0x1f64f],
92
+ [0x1f680, 0x1f6ff],
93
+ [0x1f900, 0x1f9ff],
94
+ [0x20000, 0x2fffd],
95
+ [0x30000, 0x3fffd],
96
+ ];
97
+ function inRanges(ranges, cp) {
98
+ let lo = 0;
99
+ let hi = ranges.length - 1;
100
+ while (lo <= hi) {
101
+ const mid = (lo + hi) >> 1;
102
+ const [a, b] = ranges[mid];
103
+ if (cp < a)
104
+ hi = mid - 1;
105
+ else if (cp > b)
106
+ lo = mid + 1;
107
+ else
108
+ return true;
109
+ }
110
+ return false;
111
+ }
112
+ export function cellWidth(cp) {
113
+ if (cp < 0x0300 || (cp > 0xe01ef))
114
+ return 1;
115
+ if (cp < 0x1100)
116
+ return inRanges(ZERO_WIDTH, cp) ? 0 : 1;
117
+ if (inRanges(ZERO_WIDTH, cp))
118
+ return 0;
119
+ return inRanges(WIDE, cp) ? 2 : 1;
120
+ }
121
+ function blankRow(cols) {
122
+ return new Array(cols).fill(" ");
123
+ }
124
+ function parseParams(body) {
125
+ const priv = body.startsWith("?") || body.startsWith(">");
126
+ const nums = body
127
+ .replace(/^[?><=]/, "")
128
+ .split(";")
129
+ .map((p) => {
130
+ const n = parseInt(p, 10);
131
+ return Number.isFinite(n) ? n : 0;
132
+ });
133
+ return { nums, private: priv };
134
+ }
135
+ /** Render the final visible screen of a PTY transcript. */
136
+ export function renderScreen(transcript, dims) {
137
+ const cols = Math.max(1, dims?.cols ?? DEFAULT_GRID_DIMS.cols);
138
+ const rows = Math.max(1, dims?.rows ?? DEFAULT_GRID_DIMS.rows);
139
+ const grid = [];
140
+ for (let r = 0; r < rows; r++)
141
+ grid.push(blankRow(cols));
142
+ let x = 0;
143
+ let y = 0;
144
+ let savedX = 0;
145
+ let savedY = 0;
146
+ let scrollTop = 0;
147
+ let scrollBottom = rows - 1;
148
+ const clearAll = () => {
149
+ for (let r = 0; r < rows; r++)
150
+ grid[r] = blankRow(cols);
151
+ };
152
+ const scrollUp = (n) => {
153
+ for (let i = 0; i < n; i++) {
154
+ grid.splice(scrollTop, 1);
155
+ grid.splice(scrollBottom, 0, blankRow(cols));
156
+ }
157
+ };
158
+ const scrollDown = (n) => {
159
+ for (let i = 0; i < n; i++) {
160
+ grid.splice(scrollBottom, 1);
161
+ grid.splice(scrollTop, 0, blankRow(cols));
162
+ }
163
+ };
164
+ const lineFeed = () => {
165
+ if (y >= scrollBottom)
166
+ scrollUp(1);
167
+ else
168
+ y++;
169
+ };
170
+ const writeCell = (ch, w) => {
171
+ if (x + w > cols) {
172
+ x = 0;
173
+ lineFeed();
174
+ }
175
+ // A wide char straddling the right margin wraps instead of splitting.
176
+ if (x + w > cols)
177
+ return;
178
+ grid[y][x] = ch;
179
+ if (w === 2 && x + 1 < cols)
180
+ grid[y][x + 1] = null;
181
+ x += w;
182
+ if (x >= cols) {
183
+ x = 0;
184
+ lineFeed();
185
+ }
186
+ };
187
+ let i = 0;
188
+ const n = transcript.length;
189
+ while (i < n) {
190
+ const c = transcript[i];
191
+ if (c === "\x1b") {
192
+ const next = transcript[i + 1];
193
+ if (next === "[") {
194
+ // CSI: params + intermediates + final byte.
195
+ let j = i + 2;
196
+ while (j < n) {
197
+ const code = transcript.charCodeAt(j);
198
+ if (code >= 0x40 && code <= 0x7e)
199
+ break;
200
+ j++;
201
+ }
202
+ if (j >= n)
203
+ break; // Truncated sequence at kill time: stop.
204
+ const body = transcript.slice(i + 2, j);
205
+ const fin = transcript[j];
206
+ i = j + 1;
207
+ const { nums } = parseParams(body);
208
+ const p1 = nums[0] || 1;
209
+ switch (fin) {
210
+ case "H":
211
+ case "f": {
212
+ const r = (nums[0] || 1) - 1;
213
+ const col = (nums[1] || 1) - 1;
214
+ y = Math.min(rows - 1, Math.max(0, r));
215
+ x = Math.min(cols - 1, Math.max(0, col));
216
+ break;
217
+ }
218
+ case "A":
219
+ y = Math.max(0, y - p1);
220
+ break;
221
+ case "B":
222
+ y = Math.min(rows - 1, y + p1);
223
+ break;
224
+ case "C":
225
+ x = Math.min(cols - 1, x + p1);
226
+ break;
227
+ case "D":
228
+ x = Math.max(0, x - p1);
229
+ break;
230
+ case "E":
231
+ x = 0;
232
+ y = Math.min(rows - 1, y + p1);
233
+ break;
234
+ case "F":
235
+ x = 0;
236
+ y = Math.max(0, y - p1);
237
+ break;
238
+ case "G":
239
+ x = Math.min(cols - 1, Math.max(0, p1 - 1));
240
+ break;
241
+ case "d":
242
+ y = Math.min(rows - 1, Math.max(0, p1 - 1));
243
+ break;
244
+ case "J": {
245
+ const mode = nums[0] || 0;
246
+ if (mode === 2 || mode === 3) {
247
+ clearAll();
248
+ }
249
+ else if (mode === 1) {
250
+ for (let r = 0; r < y; r++)
251
+ grid[r] = blankRow(cols);
252
+ for (let c0 = 0; c0 <= x && c0 < cols; c0++)
253
+ grid[y][c0] = " ";
254
+ }
255
+ else {
256
+ for (let c0 = x; c0 < cols; c0++)
257
+ grid[y][c0] = " ";
258
+ for (let r = y + 1; r < rows; r++)
259
+ grid[r] = blankRow(cols);
260
+ }
261
+ break;
262
+ }
263
+ case "K": {
264
+ const mode = nums[0] || 0;
265
+ if (mode === 1) {
266
+ for (let c0 = 0; c0 <= x && c0 < cols; c0++)
267
+ grid[y][c0] = " ";
268
+ }
269
+ else if (mode === 2) {
270
+ grid[y] = blankRow(cols);
271
+ }
272
+ else {
273
+ for (let c0 = x; c0 < cols; c0++)
274
+ grid[y][c0] = " ";
275
+ }
276
+ break;
277
+ }
278
+ case "L": {
279
+ // Insert lines at cursor within the scroll region.
280
+ const count = Math.min(p1, scrollBottom - y + 1);
281
+ for (let k = 0; k < count; k++) {
282
+ grid.splice(scrollBottom, 1);
283
+ grid.splice(Math.max(scrollTop, y), 0, blankRow(cols));
284
+ }
285
+ break;
286
+ }
287
+ case "M": {
288
+ // Delete lines at cursor within the scroll region.
289
+ const count = Math.min(p1, scrollBottom - y + 1);
290
+ for (let k = 0; k < count; k++) {
291
+ grid.splice(Math.max(scrollTop, y), 1);
292
+ grid.splice(scrollBottom, 0, blankRow(cols));
293
+ }
294
+ break;
295
+ }
296
+ case "P": {
297
+ // Delete characters at cursor.
298
+ const count = Math.min(p1, cols - x);
299
+ grid[y].splice(x, count);
300
+ while (grid[y].length < cols)
301
+ grid[y].push(" ");
302
+ break;
303
+ }
304
+ case "@": {
305
+ // Insert blank characters at cursor.
306
+ const count = Math.min(p1, cols - x);
307
+ grid[y].splice(x, 0, ...blankRow(count));
308
+ grid[y].length = cols;
309
+ break;
310
+ }
311
+ case "X": {
312
+ // Erase characters at cursor.
313
+ const count = Math.min(p1, cols - x);
314
+ for (let c0 = x; c0 < x + count; c0++)
315
+ grid[y][c0] = " ";
316
+ break;
317
+ }
318
+ case "S":
319
+ scrollUp(Math.min(p1, scrollBottom - scrollTop + 1));
320
+ break;
321
+ case "T":
322
+ scrollDown(Math.min(p1, scrollBottom - scrollTop + 1));
323
+ break;
324
+ case "r": {
325
+ // DECSTBM scroll region (1-based, inclusive).
326
+ const top = Math.max(1, nums[0] || 1);
327
+ const bottom = nums[1] || rows;
328
+ if (top <= bottom && top <= rows) {
329
+ scrollTop = Math.min(rows - 1, top - 1);
330
+ scrollBottom = Math.min(rows - 1, bottom - 1);
331
+ }
332
+ x = 0;
333
+ y = scrollTop;
334
+ break;
335
+ }
336
+ case "s":
337
+ savedX = x;
338
+ savedY = y;
339
+ break;
340
+ case "u":
341
+ x = Math.min(cols - 1, savedX);
342
+ y = Math.min(rows - 1, savedY);
343
+ break;
344
+ case "h":
345
+ case "l": {
346
+ // Alt-screen enter wipes to a fresh buffer; its exit cannot
347
+ // restore the main screen, so it is a no-op. All other private
348
+ // modes (cursor visibility, mouse, sync output) are ignored.
349
+ if (fin === "h" && /(?:^|;)1049(?:;|$)/.test(body.replace(/^[?><=]/, ""))) {
350
+ clearAll();
351
+ x = 0;
352
+ y = 0;
353
+ }
354
+ break;
355
+ }
356
+ default:
357
+ break; // SGR (m), queries (c/n), tabs, colors: no screen effect.
358
+ }
359
+ continue;
360
+ }
361
+ if (next === "]") {
362
+ // OSC: skip to BEL or ST.
363
+ let j = i + 2;
364
+ while (j < n) {
365
+ if (transcript[j] === "\x07") {
366
+ j++;
367
+ break;
368
+ }
369
+ if (transcript[j] === "\x1b" && transcript[j + 1] === "\\") {
370
+ j += 2;
371
+ break;
372
+ }
373
+ j++;
374
+ }
375
+ i = j;
376
+ continue;
377
+ }
378
+ if (next === "7") {
379
+ savedX = x;
380
+ savedY = y;
381
+ i += 2;
382
+ continue;
383
+ }
384
+ if (next === "8") {
385
+ x = Math.min(cols - 1, savedX);
386
+ y = Math.min(rows - 1, savedY);
387
+ i += 2;
388
+ continue;
389
+ }
390
+ if (next === "M") {
391
+ // Reverse index.
392
+ if (y <= scrollTop)
393
+ scrollDown(1);
394
+ else
395
+ y--;
396
+ i += 2;
397
+ continue;
398
+ }
399
+ if (next === "D") {
400
+ lineFeed();
401
+ i += 2;
402
+ continue;
403
+ }
404
+ if (next === "E") {
405
+ x = 0;
406
+ lineFeed();
407
+ i += 2;
408
+ continue;
409
+ }
410
+ if (next === "c") {
411
+ clearAll();
412
+ x = 0;
413
+ y = 0;
414
+ scrollTop = 0;
415
+ scrollBottom = rows - 1;
416
+ i += 2;
417
+ continue;
418
+ }
419
+ if (next === "(" || next === ")" || next === "#" || next === "=" || next === ">") {
420
+ i += 2; // Charset selects, keypad modes: no screen effect.
421
+ continue;
422
+ }
423
+ // Unknown ESC sequence: skip the introducer, keep scanning.
424
+ i += next === undefined ? 1 : 2;
425
+ continue;
426
+ }
427
+ if (c === "\r") {
428
+ x = 0;
429
+ i++;
430
+ continue;
431
+ }
432
+ if (c === "\n") {
433
+ // NEL-style: PTY output carries \r\n (ONLCR) so the \r already ran;
434
+ // plain fixtures carry bare \n and expect a fresh line start.
435
+ x = 0;
436
+ lineFeed();
437
+ i++;
438
+ continue;
439
+ }
440
+ if (c === "\b") {
441
+ x = Math.max(0, x - 1);
442
+ i++;
443
+ continue;
444
+ }
445
+ if (c === "\t") {
446
+ x = Math.min(cols - 1, x + 8 - (x % 8));
447
+ i++;
448
+ continue;
449
+ }
450
+ const code = transcript.charCodeAt(i);
451
+ if (code < 0x20 || code === 0x7f) {
452
+ i++; // Other C0 controls (BEL etc.): no screen effect.
453
+ continue;
454
+ }
455
+ const cp = transcript.codePointAt(i) ?? code;
456
+ const ch = String.fromCodePoint(cp);
457
+ i += ch.length;
458
+ const w = cellWidth(cp);
459
+ if (w === 0)
460
+ continue;
461
+ writeCell(ch, w);
462
+ }
463
+ const lines = [];
464
+ for (const row of grid) {
465
+ let end = row.length;
466
+ while (end > 0 && (row[end - 1] === "" || row[end - 1] === null || row[end - 1] === " "))
467
+ end--;
468
+ lines.push(row.slice(0, end).map((cell) => (cell === null ? "" : cell)).join(""));
469
+ }
470
+ while (lines.length > 0 && lines[lines.length - 1] === "")
471
+ lines.pop();
472
+ return lines.join("\n");
473
+ }
474
+ /**
475
+ * Screen-first parsing with byte-soup fallback. The rendered screen is
476
+ * precise (overwrite artifacts resolved) but lossy in one case: a TUI that
477
+ * leaves alt-screen and clears on kill wipes the dialog the transcript
478
+ * captured earlier (observed: grok prints a resume line on exit). The
479
+ * cumulative soup never loses content but keeps cursor artifacts. So: parse
480
+ * the screen; on throw, or on an estimated reset the soup resolves exactly,
481
+ * parse the soup instead. Worst case equals the old soup-only behavior.
482
+ */
483
+ export function parseScreens(text, dims, parse) {
484
+ const screen = renderScreen(text, dims);
485
+ try {
486
+ const first = parse(screen);
487
+ if (!first.resetsAtEstimated)
488
+ return first;
489
+ const soup = stripAnsi(text);
490
+ if (soup === screen)
491
+ return first;
492
+ try {
493
+ const second = parse(soup);
494
+ return second.resetsAtEstimated ? first : second;
495
+ }
496
+ catch {
497
+ return first;
498
+ }
499
+ }
500
+ catch (screenErr) {
501
+ const soup = stripAnsi(text);
502
+ if (soup === screen)
503
+ throw screenErr;
504
+ return parse(soup);
505
+ }
506
+ }
@@ -248,7 +248,8 @@ export function registerProvidersCommand(program, deps = {}) {
248
248
  return;
249
249
  }
250
250
  }
251
- console.log(`enabled ${id} — restart quotacap daemon to apply`);
251
+ console.log(`enabled ${id}`);
252
+ console.log("Next: run quotacap service restart to start polling");
252
253
  });
253
254
  providers
254
255
  .command("disable <id>")