ispbills-icli 8.7.6 → 8.7.7

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/package.json +1 -1
  2. package/src/tui/components.js +101 -114
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ispbills-icli",
3
- "version": "8.7.6",
3
+ "version": "8.7.7",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
@@ -299,8 +299,9 @@ const SUBAGENT = /^\s*[✓✗x]?\s*\[(\d+)\/(\d+)\]\s*(.+?)\s*[::]\s*(.+?)\s*$
299
299
  const LEAD_EMOJI = /^\s*([\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{2190}-\u{21FF}\u{FE0F}\u{2049}\u{203C}]+)\s*/u;
300
300
 
301
301
  /**
302
- * Classify a status line string into op type, type tag, and display parts.
303
- * Returns { opType: 'read'|'edit'|'step'|'other', typeTag, verb, action, subject, done, ok }
302
+ * Classify a status line into op type, a dim type-tag, and display parts.
303
+ * Tag is metadata only (file ext, device protocol, service) never colored.
304
+ * Op type drives color: read=green, edit=red, step=blue.
304
305
  */
305
306
  function parseToolLine(lineStr) {
306
307
  const s = String(lineStr).trim();
@@ -313,73 +314,72 @@ function parseToolLine(lineStr) {
313
314
  const ci = body.indexOf(':');
314
315
  const action = (ci > -1 ? body.slice(0, ci) : body).trim();
315
316
  const subject = (ci > -1 ? body.slice(ci + 1) : '').trim();
316
- const combined = action + ' ' + subject;
317
+ const aL = action.toLowerCase();
318
+ const sL = subject.toLowerCase();
317
319
 
318
- // ── Op type ──────────────────────────────────────────────────────────────
320
+ // ── Op type + descriptive verb (from action keywords) ────────────────────
319
321
  let opType = 'other';
320
322
  let verb = '';
321
- if (/search|lookup|look.?up|reading|read|fetch|check|analys|correlat|simulat|predict|searching|looking|checking|analysing|correlating|predicting|simulating|retrieve/i.test(action)) {
322
- opType = 'read'; verb = 'Read';
323
- } else if (/edit|write|save|saving|generat|rollback|running on|exec|apply|creat|update/i.test(action)) {
324
- opType = 'edit'; verb = 'Edit';
325
- } else if (/assign|spawn|sub.?agent|delegat|step|next/i.test(action)) {
326
- opType = 'step'; verb = 'Step';
327
- }
328
-
329
- // ── Type tag from subject/action ─────────────────────────────────────────
323
+ if (/running on\b/i.test(aL)) { opType = 'edit'; verb = 'Exec'; }
324
+ else if (/\b(save|saving|generat|rollback|write|creat|update|apply)\b/i.test(aL)) { opType = 'edit'; verb = aL.includes('save') || aL.includes('saving') ? 'Save' : aL.includes('generat') ? 'Generate' : aL.includes('rollback') ? 'Rollback' : 'Edit'; }
325
+ else if (/\b(search|searching)\b/i.test(aL)) { opType = 'read'; verb = 'Search'; }
326
+ else if (/\b(lookup|look.?up|looking)\b/i.test(aL)) { opType = 'read'; verb = 'Lookup'; }
327
+ else if (/\b(check|checking|analys|correlat|simulat|predict|retrieve)\b/i.test(aL)) { opType = 'read'; verb = 'Check'; }
328
+ else if (/\b(fetch|reading|read)\b/i.test(aL)) { opType = 'read'; verb = 'Read'; }
329
+ else if (/\b(assign|spawn|sub.?agent|delegat)\b/i.test(aL)) { opType = 'step'; verb = 'Spawn'; }
330
+
331
+ // ── Type tag — ONLY from explicit context; never inherit device from subject ──
332
+ // Tag identifies the protocol/format involved, not the device mentioned.
330
333
  let typeTag = '';
331
- const hay = combined.toLowerCase();
332
- if (/\.(js|jsx|mjs|cjs)\b/.test(hay)) typeTag = 'JS';
333
- else if (/\.(ts|tsx)\b/.test(hay)) typeTag = 'TS';
334
- else if (/\.php\b/.test(hay)) typeTag = 'PHP';
335
- else if (/\.py\b/.test(hay)) typeTag = 'PY';
336
- else if (/\.(rb)\b/.test(hay)) typeTag = 'RB';
337
- else if (/\.(go)\b/.test(hay)) typeTag = 'GO';
338
- else if (/\.(sh|bash)\b/.test(hay)) typeTag = 'SH';
339
- else if (/nas#|mikrotik|routeros|\/export|\/ip |\/ppp|\/interface|\/system|\/queue/.test(hay)) typeTag = 'ROS';
340
- else if (/olt#|optical|onu|gpon|epon/.test(hay)) typeTag = 'OLT';
341
- else if (/github/.test(hay)) typeTag = 'GIT';
342
- else if (/http|url|web page|web search/.test(hay)) typeTag = 'WEB';
343
- else if (/customer|subscriber|user/.test(hay)) typeTag = 'CRM';
344
- else if (/firmware|version|upgrade/.test(hay)) typeTag = 'FW';
345
- else if (/sub.?agent|specialist|role/.test(hay)) typeTag = 'AI';
346
- else if (/shell|command|script/.test(hay)) typeTag = 'SH';
347
- else if (/checkpoint|backup|config/.test(hay)) typeTag = 'CFG';
348
- else if (/docs|documentation/.test(hay)) typeTag = 'DOC';
349
- else if (/signal|optical|dbm/.test(hay)) typeTag = 'OPT';
350
-
351
- return { opType, typeTag, verb, action, subject, done, ok, full: s };
334
+ // File extensions in subject or action (most specific)
335
+ const allText = aL + ' ' + sL;
336
+ if (/\.(js|jsx|mjs|cjs)\b/.test(allText)) typeTag = 'JS';
337
+ else if (/\.(ts|tsx)\b/.test(allText)) typeTag = 'TS';
338
+ else if (/\.php\b/.test(allText)) typeTag = 'PHP';
339
+ else if (/\.py\b/.test(allText)) typeTag = 'PY';
340
+ else if (/\.(rb)\b/.test(allText)) typeTag = 'RB';
341
+ else if (/\.(go)\b/.test(allText)) typeTag = 'GO';
342
+ else if (/\.(sh|bash)\b/.test(allText)) typeTag = 'SH';
343
+ // Device protocol — ONLY when action is a device exec ("running on")
344
+ else if (/running on/i.test(aL)) {
345
+ if (/nas#|mikrotik|\/export|\/ip |\/ppp|\/interface|\/system|\/queue|\/routing/.test(allText)) typeTag = 'ROS';
346
+ else if (/olt#|gpon|epon|optical/.test(allText)) typeTag = 'OLT';
347
+ else typeTag = 'DEV';
348
+ }
349
+ // Action-based tags
350
+ else if (/github/i.test(aL)) typeTag = 'GIT';
351
+ else if (/web|url|page|http/i.test(aL)) typeTag = 'WEB';
352
+ else if (/customer|subscriber/i.test(aL)) typeTag = 'CRM';
353
+ else if (/firmware|version/i.test(aL)) typeTag = 'FW';
354
+ else if (/sub.?agent|specialist|assign/i.test(aL)) typeTag = 'AI';
355
+ else if (/shell|command/i.test(aL)) typeTag = 'SH';
356
+ else if (/checkpoint|backup/i.test(aL)) typeTag = 'CFG';
357
+ else if (/config/i.test(aL)) typeTag = 'CFG';
358
+ else if (/docs|documentation/i.test(aL)) typeTag = 'DOC';
359
+ else if (/signal|optical|dbm/i.test(aL)) typeTag = 'OPT';
360
+ else if (/device|router|nas|olt/i.test(aL)) typeTag = 'DEV';
361
+
362
+ // Display text: verb + detail
363
+ // When action has no colon split, subject is empty — show trimmed action as detail
364
+ const display = subject || action;
365
+
366
+ return { opType, typeTag, verb, action, subject, display, done, ok, full: s };
352
367
  }
353
368
 
354
369
  export function StatusLines({ lines = [], busy = false, navigable = false }) {
355
370
  const [sel, setSel] = useState(-1);
356
371
  const [expanded, setExpanded] = useState(new Set());
357
372
 
358
- // Reset selection when lines change
359
- useEffect(() => {
360
- setSel(-1);
361
- setExpanded(new Set());
362
- }, [lines.length]);
373
+ useEffect(() => { setSel(-1); setExpanded(new Set()); }, [lines.length]);
363
374
 
364
375
  useInput((input, key) => {
365
376
  if (!navigable) return;
366
- if (key.upArrow || input === 'k') {
367
- setSel((i) => Math.max(0, (i < 0 ? lines.length - 1 : i) - 1));
368
- return;
369
- }
370
- if (key.downArrow || input === 'j') {
371
- setSel((i) => Math.min(lines.length - 1, (i < 0 ? -1 : i) + 1));
372
- return;
373
- }
377
+ if (key.upArrow || input === 'k') { setSel((i) => Math.max(0, (i < 0 ? lines.length - 1 : i) - 1)); return; }
378
+ if (key.downArrow || input === 'j') { setSel((i) => Math.min(lines.length - 1, (i < 0 ? -1 : i) + 1)); return; }
374
379
  if ((key.return || input === ' ') && sel >= 0) {
375
- setExpanded((prev) => {
376
- const next = new Set(prev);
377
- next.has(sel) ? next.delete(sel) : next.add(sel);
378
- return next;
379
- });
380
+ setExpanded((prev) => { const n = new Set(prev); n.has(sel) ? n.delete(sel) : n.add(sel); return n; });
380
381
  return;
381
382
  }
382
- // Esc clears selection
383
383
  if (key.escape) { setSel(-1); return; }
384
384
  });
385
385
 
@@ -389,101 +389,88 @@ export function StatusLines({ lines = [], busy = false, navigable = false }) {
389
389
  return html`
390
390
  <${Box} flexDirection="column" marginTop=${1}>
391
391
  ${lines.map((line, i) => {
392
- const lineStr = String(line);
392
+ const lineStr = String(line);
393
393
  const isRunning = busy && i === lastIdx;
394
- const isSel = navigable && sel === i;
395
- const isExp = expanded.has(i);
394
+ const isSel = navigable && sel === i;
395
+ const isExp = expanded.has(i);
396
396
 
397
- // ── Sub-agent tree line ────────────────────────────────────────────
397
+ // ── Sub-agent tree line ─────────────────────────────────────────
398
398
  const sm = lineStr.match(SUBAGENT);
399
399
  if (sm) {
400
400
  const [, idx, total, host, result] = sm;
401
401
  const isCompleted = /^\s*[✓✗x]/.test(lineStr);
402
- const running = busy && !isCompleted;
402
+ const running = busy && !isCompleted;
403
403
  const good = !/fail|error|down|offline|unreachable|timeout|deny/i.test(result);
404
404
  const branch = Number(idx) === Number(total) ? glyphs.branchEnd : glyphs.branchMid;
405
- const hostTrunc = host.length > 22 ? host.slice(0, 20) + '…' : host;
406
- const resultTrunc = result.length > 30 ? result.slice(0, 28) + '…' : result;
407
405
  return html`
408
- <${Box} key=${'s' + i} flexDirection="column">
409
- <${Box}>
410
- <${Text} color=${C.muted}> <//>
411
- <${Text} color=${C.accent}>${branch} <//>
412
- <${Text} color=${C.muted}>${idx}/${total} <//>
413
- <${Text} color=${C.white} bold>${hostTrunc}<//>
414
- <${Text} color=${C.muted}> ${dash()} <//>
415
- ${running
416
- ? html`<${Text} color=${C.warning}><${Spinner} type=${spinnerType()} /><//> <${Text} color=${C.muted}>running<//>`
417
- : html`<${Text} color=${good ? C.success : C.error} bold>${good ? glyphs.check : glyphs.cross} ${resultTrunc}<//>`}
418
- <//>
406
+ <${Box} key=${`sa${i}`}>
407
+ <${Text} color=${C.muted}> <//>
408
+ <${Text} color=${C.accent}>${branch} <//>
409
+ <${Text} color=${C.muted}>${idx}/${total} <//>
410
+ <${Text} color=${C.white} bold>${host.slice(0, 20)}<//>
411
+ <${Text} color=${C.muted}> ${dash()} <//>
412
+ ${running
413
+ ? html`<${Text} color=${C.warning}><${Spinner} type=${spinnerType()} /><//> <${Text} color=${C.muted}>running<//>`
414
+ : html`<${Text} color=${good ? C.success : C.error} bold>${good ? glyphs.check : glyphs.cross} ${result.slice(0, 32)}<///>`}
419
415
  <//>`;
420
416
  }
421
417
 
422
- // ── Regular tool status line ───────────────────────────────────────
423
- const { opType, typeTag, verb, action, subject, done, ok } = parseToolLine(lineStr);
418
+ // ── Regular tool status line ────────────────────────────────────
419
+ const { opType, typeTag, verb, display, done, ok, full } = parseToolLine(lineStr);
424
420
 
425
421
  const opColor = opType === 'read' ? C.success
426
- : opType === 'edit' ? C.error
427
- : opType === 'step' ? C.brand
422
+ : opType === 'edit' ? C.error
423
+ : opType === 'step' ? C.brand
428
424
  : C.muted;
429
425
 
430
- // Dot glyph (colored bullet)
431
- const dot = '';
426
+ // Type tag: always dim/muted — it's just metadata, never colored
427
+ const tag = typeTag ? typeTag.slice(0, 3).padEnd(3) : ' ';
432
428
 
433
- // Type tag display (fixed 3-char width)
434
- const tag = (typeTag || '···').slice(0, 3).padEnd(3);
429
+ // Verb label: padded to 8 chars for alignment
430
+ const verbLabel = (verb || 'Run').slice(0, 8).padEnd(8);
435
431
 
436
- // Subject truncation
437
- const subjectTrunc = subject.length > 36 ? subject.slice(0, 34) + '…' : subject;
438
- const actionTrunc = action.length > 28 ? action.slice(0, 26) + '…' : action;
432
+ // Detail text: as much as fits
433
+ const detail = display.length > 52 ? display.slice(0, 50) + '…' : display;
439
434
 
440
- // Status indicator on the right
435
+ // Status icon
441
436
  const statusIcon = isRunning
442
437
  ? html`<${Text} color=${C.warning}><${Spinner} type=${spinnerType()} /><//>`
443
- : done && ok ? html`<${Text} color=${C.success}> ${glyphs.check}<//>`
444
- : done && !ok ? html`<${Text} color=${C.error}> ${glyphs.cross}<//>`
438
+ : (done && ok) ? html`<${Text} color=${C.success}> ${glyphs.check}<//>`
439
+ : (done && !ok) ? html`<${Text} color=${C.error}> ${glyphs.cross}<//>`
445
440
  : null;
446
441
 
447
- // Expand toggle hint for navigable lines
448
- const expHint = navigable && isSel
449
- ? html`<${Text} color=${C.muted} dimColor> [${isExp ? '−' : '+'}]<//>` : null;
450
-
451
442
  return html`
452
- <${Box} key=${'s' + i} flexDirection="column">
453
- <!-- Main line -->
454
- <${Box} backgroundColor=${isSel ? '#1a1a2e' : undefined}>
443
+ <${Box} key=${`sl${i}`} flexDirection="column">
444
+ <${Box}>
445
+ <${Text} color=${C.muted}> <//>
446
+ <!-- Type tag: dim neutral — NOT colored by op type -->
447
+ <${Text} color=${C.muted} dimColor>${tag}<//>
455
448
  <${Text} color=${C.muted}> <//>
456
- <${Text} color=${opColor} dimColor>${tag}<//>
457
- <${Text} color=${C.muted}> <//>
458
- <${Text} color=${opColor}>${dot} <//>
459
- <${Text} color=${opColor} bold=${opType === 'read'}>${verb || action.slice(0, 12)}<//>
460
- ${subject
461
- ? html`<${Text} color=${C.white} bold=${opType === 'read'}> ${subjectTrunc.slice(0, 40)}<//>`
462
- : html`<${Text} color=${C.muted} bold=${opType === 'read'}> ${actionTrunc}<//>` }
449
+ <!-- Colored dot — the ONLY colored part -->
450
+ <${Text} color=${opColor}>● <//>
451
+ <!-- Verb: bold for reads, normal otherwise -->
452
+ <${Text} color=${C.white} bold=${opType === 'read'}>${verbLabel}<//>
453
+ <!-- Detail: plain white -->
454
+ <${Text} color=${C.white} dimColor> ${detail}<//>
463
455
  ${statusIcon}
464
- ${expHint}
456
+ ${isSel ? html`<${Text} color=${C.muted} dimColor> [${isExp ? '−' : '+'}]<//>` : null}
465
457
  <//>
466
-
467
- <!-- Expanded detail -->
468
458
  ${isExp ? html`
469
- <${Box} flexDirection="column" marginLeft=${4}>
470
- <${Text} color=${C.separator}>${'┌' + '─'.repeat(50)}<//>
471
- <${Box}>
472
- <${Text} color=${C.separator}>│ <//>
473
- <${Text} color=${C.muted} italic>${lineStr.slice(0, 120)}<//>
459
+ <${Box} flexDirection="column" paddingLeft=${8}>
460
+ <${Text} color=${C.separator}>${'┌' + '─'.repeat(48)}<//>
461
+ <${Box}><${Text} color=${C.separator}>│ <//>
462
+ <${Text} color=${C.muted}>${full.slice(0, 100)}<//>
474
463
  <//>
475
- ${subject.length > 40 ? html`
476
- <${Box}>
477
- <${Text} color=${C.separator}>│ <//>
478
- <${Text} color=${C.muted} italic>${subject.slice(40)}<//>
479
- <//>` : null}
480
- <${Text} color=${C.separator}>${'└' + '─'.repeat(50)}<//>
464
+ ${full.length > 100 ? html`<${Box}><${Text} color=${C.separator}>│ <//>
465
+ <${Text} color=${C.muted}>${full.slice(100, 200)}<//>
466
+ <//>` : null}
467
+ <${Text} color=${C.separator}>${'└' + '─'.repeat(48)}<//>
481
468
  <//>` : null}
482
469
  <//>`;
483
470
  })}
484
471
  ${navigable && lines.length > 1
485
472
  ? html`<${Box} paddingLeft=${2} marginTop=${0}>
486
- <${Text} color=${C.muted} dimColor>j/k navigate ${midDot()} Enter expand ${midDot()} Esc dismiss<//>
473
+ <${Text} color=${C.muted} dimColor>j/k select Enter expand Esc dismiss<//>
487
474
  <//>`
488
475
  : null}
489
476
  <//>`;