clawmonitor 1.1.2 → 1.1.3

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 (2) hide show
  1. package/bin/clawmonitor.js +81 -59
  2. package/package.json +1 -1
@@ -206,7 +206,11 @@ const durColor = (d) => {
206
206
  // Pad a string to exactly N terminal columns (right-pad with spaces)
207
207
  const padTo = (s, n) => {
208
208
  const w = strWidth(s);
209
- if (w >= n) return s;
209
+ if (w > n) {
210
+ // Hard truncate to fit — strip ANSI, truncate, re-add border color
211
+ const plain = stripAnsi(s);
212
+ return truncTo(plain, n);
213
+ }
210
214
  return s + ' '.repeat(n - w);
211
215
  };
212
216
 
@@ -259,14 +263,14 @@ const strWidth = (s) => {
259
263
 
260
264
  // truncTo returns the truncated string. Also sets truncTo.consumed for caller.
261
265
  let _truncConsumed = 0;
262
- const truncTo = (s, maxCols) => {
266
+ const truncTo = (s, maxCols, noEllipsis) => {
263
267
  _truncConsumed = 0;
264
268
  if (!s) return s;
265
269
  // Fast path: pure ASCII
266
270
  if (/^[\x20-\x7E]*$/.test(s)) {
267
271
  if (s.length <= maxCols) { _truncConsumed = s.length; return s; }
268
272
  _truncConsumed = maxCols;
269
- return s.slice(0, maxCols) + '\u2026';
273
+ return noEllipsis ? s.slice(0, maxCols) : s.slice(0, maxCols) + '\u2026';
270
274
  }
271
275
  let w = 0, chars = [];
272
276
  for (const {segment} of segmenter.segment(s)) {
@@ -279,7 +283,7 @@ const truncTo = (s, maxCols) => {
279
283
  }
280
284
  const result = chars.join('');
281
285
  if (_truncConsumed < s.length) {
282
- return result + '\u2026';
286
+ return noEllipsis ? result : result + '\u2026';
283
287
  }
284
288
  return result;
285
289
  };
@@ -312,17 +316,17 @@ function wrapArg(key, val, maxCols, keyW) {
312
316
  const out = [];
313
317
  let text = s;
314
318
  let lineIdx = 0;
315
- while (text.length > 0 && lineIdx < (opts.full ? 999 : 3)) {
316
- const maxL = opts.full ? 999 : 3;
317
- const isLast = lineIdx === maxL - 1;
319
+ const maxL = opts.full ? 999 : 3;
320
+ while (text.length > 0 && lineIdx < maxL) {
321
+ const isLast = !opts.full && lineIdx === maxL - 1;
318
322
  const budget = isLast ? maxValW - 1 : maxValW;
319
- const chunk = truncTo(text, budget);
323
+ const chunk = truncTo(text, budget, !isLast); // only add … on last line
320
324
  out.push({ key, indent: lineIdx === 0 ? null : indent, val: chunk });
321
325
  text = text.slice(_truncConsumed);
322
326
  lineIdx++;
323
327
  if (!text) break;
324
328
  if (isLast && text) {
325
- out[out.length - 1].val = truncTo(chunk, budget - 1) + '\u2026';
329
+ out[out.length - 1].val = chunk.endsWith('\u2026') ? chunk : chunk + '\u2026';
326
330
  break;
327
331
  }
328
332
  }
@@ -386,11 +390,24 @@ const colorToken = (t) => {
386
390
 
387
391
  // Wrap tokens into lines, respecting token boundaries
388
392
  function wrapTokens(tokens, maxCols, maxLines) {
393
+ const fullMode = maxLines >= 999;
389
394
  const lines = [];
390
395
  let lineTokens = [];
391
396
  let lineW = 0;
392
397
  let linesUsed = 0;
393
398
 
399
+ // Split a string to fit maxCols without adding ellipsis
400
+ const splitFit = (s, cols) => {
401
+ let w = 0, i = 0;
402
+ for (const {segment} of segmenter.segment(s)) {
403
+ const cw = charWidth(segment);
404
+ if (w + cw > cols) break;
405
+ w += cw;
406
+ i += segment.length;
407
+ }
408
+ return { text: s.slice(0, i), consumed: i, rest: s.slice(i) };
409
+ };
410
+
394
411
  for (let ti = 0; ti < tokens.length; ti++) {
395
412
  const t = tokens[ti];
396
413
  if (lineTokens.length === 0 && t.type === 'ws') continue;
@@ -405,31 +422,32 @@ function wrapTokens(tokens, maxCols, maxLines) {
405
422
  continue;
406
423
  }
407
424
 
408
- // Token doesn't fit — split it to fill remaining space
425
+ // Token doesn't fit — split it
409
426
  if (lineTokens.length > 0 && remaining > 5) {
410
- // Split token: put first part on current line
411
- const head = truncTo(t.text, remaining);
412
- lineTokens.push({ ...t, text: head });
427
+ const sp = splitFit(t.text, remaining);
428
+ lineTokens.push({ ...t, text: sp.text });
413
429
  lines.push(lineTokens);
414
430
  linesUsed++;
415
431
  lineTokens = [];
416
432
  lineW = 0;
417
433
 
418
- // Put rest on next line(s)
419
- let rest = t.text.slice(_truncConsumed);
420
- if (rest && linesUsed < maxLines) {
421
- while (rest && linesUsed < maxLines) {
422
- const isLast = linesUsed === maxLines - 1;
423
- const budget = isLast ? maxCols - 1 : maxCols;
424
- const chunk = truncTo(rest, budget);
425
- lines.push([{ ...t, text: chunk }]);
434
+ let rest = sp.rest;
435
+ while (rest && linesUsed < maxLines) {
436
+ const isLast = !fullMode && linesUsed === maxLines - 1;
437
+ if (isLast) {
438
+ // Last line: truncate with ellipsis
439
+ const chunk = truncTo(rest, maxCols - 1);
440
+ lines.push([{ ...t, text: chunk + '\u2026' }]);
441
+ linesUsed++;
442
+ rest = '';
443
+ } else {
444
+ const sp2 = splitFit(rest, maxCols);
445
+ lines.push([{ ...t, text: sp2.text }]);
426
446
  linesUsed++;
427
- rest = rest.slice(_truncConsumed);
428
- if (chunk.endsWith('\u2026')) break;
447
+ rest = sp2.rest;
429
448
  }
430
449
  }
431
450
  } else {
432
- // Flush current line, start fresh
433
451
  if (lineTokens.length > 0) {
434
452
  lines.push(lineTokens);
435
453
  linesUsed++;
@@ -438,28 +456,34 @@ function wrapTokens(tokens, maxCols, maxLines) {
438
456
  }
439
457
  if (linesUsed >= maxLines) break;
440
458
 
441
- // Split token onto new line(s)
442
459
  let rest = t.text;
443
460
  while (rest && linesUsed < maxLines) {
444
- const isLast = linesUsed === maxLines - 1;
445
- const budget = isLast ? maxCols - 1 : maxCols;
446
- const chunk = truncTo(rest, budget);
447
- lines.push([{ ...t, text: chunk }]);
448
- linesUsed++;
449
- rest = rest.slice(_truncConsumed);
450
- if (chunk.endsWith('\u2026')) break;
461
+ const isLast = !fullMode && linesUsed === maxLines - 1;
462
+ if (isLast) {
463
+ const chunk = truncTo(rest, maxCols - 1);
464
+ lines.push([{ ...t, text: chunk + '\u2026' }]);
465
+ linesUsed++;
466
+ rest = '';
467
+ } else {
468
+ const sp = splitFit(rest, maxCols);
469
+ lines.push([{ ...t, text: sp.text }]);
470
+ linesUsed++;
471
+ rest = sp.rest;
472
+ }
451
473
  }
452
474
  }
453
475
  }
454
476
  if (lineTokens.length > 0 && linesUsed < maxLines) lines.push(lineTokens);
455
477
 
456
- while (lines.length > maxLines) lines.pop();
457
- if (lines.length >= maxLines) {
458
- const last = lines[lines.length - 1];
459
- const lastStr = last.map(t => t.text).join('');
460
- if (!lastStr.endsWith('\u2026')) {
461
- const truncated = truncTo(lastStr, maxCols - 1);
462
- lines[lines.length - 1] = [{ type: 'plain', text: truncated + '\u2026' }];
478
+ if (!fullMode) {
479
+ while (lines.length > maxLines) lines.pop();
480
+ if (lines.length >= maxLines) {
481
+ const last = lines[lines.length - 1];
482
+ const lastStr = last.map(t => t.text).join('');
483
+ if (!lastStr.endsWith('\u2026')) {
484
+ const truncated = truncTo(lastStr, maxCols - 1);
485
+ lines[lines.length - 1] = [{ type: 'plain', text: truncated + '\u2026' }];
486
+ }
463
487
  }
464
488
  }
465
489
 
@@ -471,22 +495,20 @@ const renderTokenLines = (tokenLines) => tokenLines.map(line => line.map(colorTo
471
495
 
472
496
  // Wrap result text into up to N lines, each fitting within maxCols
473
497
  function wrapResult(text, maxCols, maxLines) {
498
+ const fullMode = maxLines >= 999;
474
499
  const flat = text.replace(/[\r\n]/g, ' ').replace(/\s+/g, ' ').trim();
475
500
  const lines = [];
476
- let remaining = flat;
477
- while (remaining && lines.length < maxLines) {
478
- const isLast = lines.length === maxLines - 1;
501
+ let pos = 0;
502
+ while (pos < flat.length && lines.length < maxLines) {
503
+ const isLast = !fullMode && lines.length === maxLines - 1;
504
+ // Non-last lines: no ellipsis, strict budget
505
+ // Last line: ellipsis, budget - 1 for …
479
506
  const budget = isLast ? maxCols - 1 : maxCols;
480
- const chunk = truncTo(remaining, budget);
481
- lines.push(chunk);
482
- remaining = remaining.slice(_truncConsumed);
483
- if (!remaining) break;
484
- if (isLast) {
485
- // Force … on last line if more text remains
486
- if (!lines[lines.length - 1].endsWith('\u2026'))
487
- lines[lines.length - 1] = truncTo(chunk, budget - 1) + '\u2026';
488
- break;
489
- }
507
+ const chunk = truncTo(flat.slice(pos), budget, !isLast); // noEllipsis for non-last
508
+ lines.push(isLast && !chunk.endsWith('\u2026') && pos + _truncConsumed < flat.length ? chunk + '\u2026' : chunk);
509
+ pos += _truncConsumed;
510
+ if (pos >= flat.length) break;
511
+ if (isLast) break;
490
512
  }
491
513
  return lines;
492
514
  }
@@ -507,7 +529,7 @@ function fmt(e, label) {
507
529
  }
508
530
 
509
531
  // Card layout — full width
510
- const W = process.stdout.columns || 80;
532
+ const W = parseInt(process.env.COLUMNS) || process.stdout.columns || 80;
511
533
  const lines = [];
512
534
 
513
535
  // Top border
@@ -541,12 +563,12 @@ function fmt(e, label) {
541
563
  const isJson = flat.startsWith('{') || flat.startsWith('[');
542
564
  if (isJson) {
543
565
  const tokens = highlightJson(flat);
544
- const tokenLines = wrapTokens(tokens, W - 10, opts.full ? 999 : 2);
566
+ const tokenLines = wrapTokens(tokens, W - 5, opts.full ? 999 : 2);
545
567
  for (const tl of renderTokenLines(tokenLines)) {
546
568
  lines.push(padTo(`${T.x('│')} ${tl}`, W - 1) + T.x('│'));
547
569
  }
548
570
  } else {
549
- const rLines = wrapResult(flat, W - 10, opts.full ? 999 : 2);
571
+ const rLines = wrapResult(flat, W - 5, opts.full ? 999 : 2);
550
572
  for (const rl of rLines) {
551
573
  lines.push(padTo(`${T.x('│')} ${T.d(rl)}`, W - 1) + T.x('│'));
552
574
  }
@@ -562,7 +584,7 @@ function fmt(e, label) {
562
584
  function fmtLiveCall(tc, label) {
563
585
  const t = fmtT(tc.timestamp);
564
586
  const id = tc.id?.slice(0, 10) || '?';
565
- const W = process.stdout.columns || 80;
587
+ const W = parseInt(process.env.COLUMNS) || process.stdout.columns || 80;
566
588
 
567
589
  if (opts.compact) {
568
590
  const args = Object.entries(tc.arguments || {}).slice(0, 2).map(([k, v]) => T.x(`${k}=`) + trunc(v, 50)).join(' ');
@@ -601,18 +623,18 @@ function fmtLiveResult(msg, ts) {
601
623
  if (opts.compact) {
602
624
  console.log(` ${T.x('↳')} ${meta} ${T.d(trunc(rStr, 70))}`);
603
625
  } else {
604
- const W = process.stdout.columns || 80;
626
+ const W = parseInt(process.env.COLUMNS) || process.stdout.columns || 80;
605
627
  console.log(padTo(`${T.x('│')} ${icon} ${T.b(name)} ${meta}`, W - 1) + T.x('│'));
606
628
  const flat = rStr.replace(/[\r\n]/g, ' ').replace(/\s+/g, ' ').trim();
607
629
  const isJson = flat.startsWith('{') || flat.startsWith('[');
608
630
  if (isJson) {
609
631
  const tokens = highlightJson(flat);
610
- const tokenLines = wrapTokens(tokens, W - 10, opts.full ? 999 : 2);
632
+ const tokenLines = wrapTokens(tokens, W - 5, opts.full ? 999 : 2);
611
633
  for (const tl of renderTokenLines(tokenLines)) {
612
634
  console.log(padTo(`${T.x('│')} ${tl}`, W - 1) + T.x('│'));
613
635
  }
614
636
  } else {
615
- const rLines = wrapResult(flat, W - 10, opts.full ? 999 : 2);
637
+ const rLines = wrapResult(flat, W - 5, opts.full ? 999 : 2);
616
638
  for (const rl of rLines) {
617
639
  console.log(padTo(`${T.x('│')} ${T.d(rl)}`, W - 1) + T.x('│'));
618
640
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmonitor",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "Real-time OpenClaw tool call monitor with TUI, JSON highlighting, and zero dependencies",
5
5
  "bin": "./bin/clawmonitor.js",
6
6
  "scripts": {