claude-scope 0.5.4 → 0.5.8

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.
package/README.md CHANGED
@@ -55,7 +55,32 @@ Or for global install:
55
55
 
56
56
  ## Usage
57
57
 
58
- Once configured, claude-scope displays the current git branch in your statusline.
58
+ Once configured, claude-scope displays real-time session information in your statusline including:
59
+ - Git branch and changes
60
+ - Model information
61
+ - Context usage with progress bar
62
+ - Session duration
63
+ - Cost estimation
64
+ - Lines added/removed
65
+ - Configuration counts
66
+ - Poker hand (entertainment)
67
+
68
+ ### Widget Display Styles
69
+
70
+ Each widget supports multiple display styles for customization:
71
+
72
+ | Style | Description |
73
+ |-------|-------------|
74
+ | `balanced` | Default balanced style (minimalism + informativeness) |
75
+ | `compact` | Maximally compact display |
76
+ | `playful` | Fun style with informative emojis |
77
+ | `verbose` | Detailed text descriptions |
78
+ | `technical` | Technical details (model IDs, milliseconds, etc.) |
79
+ | `symbolic` | Symbol-based representation |
80
+ | `labeled` | Prefix labels for clarity |
81
+ | `indicator` | Bullet indicator prefix |
82
+ | `fancy` | Decorative formatting (brackets, quotes) |
83
+ | `compact-verbose` | Compact with K-formatted numbers |
59
84
 
60
85
  **Note:** This is an early release with basic functionality. Additional features (repository status, session analytics, etc.) are planned for future releases.
61
86
 
@@ -112,12 +112,6 @@ var TIME = {
112
112
  /** Seconds per hour */
113
113
  SECONDS_PER_HOUR: 3600
114
114
  };
115
- var COST_THRESHOLDS = {
116
- /** Below this value, show 4 decimal places ($0.0012) */
117
- SMALL: 0.01,
118
- /** Above this value, show no decimal places ($123) */
119
- LARGE: 100
120
- };
121
115
  var DEFAULTS = {
122
116
  /** Default separator between widgets */
123
117
  SEPARATOR: " ",
@@ -254,25 +248,23 @@ var NativeGit = class {
254
248
  const { stdout } = await execFileAsync("git", args, {
255
249
  cwd: this.cwd
256
250
  });
251
+ const fileMatch = stdout.match(/(\d+)\s+file(s?)\s+changed/);
257
252
  const insertionMatch = stdout.match(/(\d+)\s+insertion/);
258
253
  const deletionMatch = stdout.match(/(\d+)\s+deletion/);
254
+ const fileCount = fileMatch ? parseInt(fileMatch[1], 10) : 0;
259
255
  const insertions = insertionMatch ? parseInt(insertionMatch[1], 10) : 0;
260
256
  const deletions = deletionMatch ? parseInt(deletionMatch[1], 10) : 0;
261
257
  const files = insertions > 0 || deletions > 0 ? [{ file: "(total)", insertions, deletions }] : [];
262
- return { files };
258
+ return { fileCount, files };
263
259
  } catch {
264
- return { files: [] };
260
+ return { fileCount: 0, files: [] };
265
261
  }
266
262
  }
267
263
  async latestTag() {
268
264
  try {
269
- const { stdout } = await execFileAsync(
270
- "git",
271
- ["describe", "--tags", "--abbrev=0"],
272
- {
273
- cwd: this.cwd
274
- }
275
- );
265
+ const { stdout } = await execFileAsync("git", ["describe", "--tags", "--abbrev=0"], {
266
+ cwd: this.cwd
267
+ });
276
268
  return stdout.trim();
277
269
  } catch {
278
270
  return null;
@@ -283,6 +275,95 @@ function createGit(cwd) {
283
275
  return new NativeGit(cwd);
284
276
  }
285
277
 
278
+ // src/ui/utils/style-utils.ts
279
+ function withLabel(prefix, value) {
280
+ if (prefix === "") return value;
281
+ return `${prefix}: ${value}`;
282
+ }
283
+ function withIndicator(value) {
284
+ return `\u25CF ${value}`;
285
+ }
286
+ function progressBar(percent, width = 10) {
287
+ const clamped = Math.max(0, Math.min(100, percent));
288
+ const filled = Math.round(clamped / 100 * width);
289
+ const empty = width - filled;
290
+ return "\u2588".repeat(filled) + "\u2591".repeat(empty);
291
+ }
292
+
293
+ // src/widgets/git/styles.ts
294
+ var gitStyles = {
295
+ minimal: (data) => {
296
+ return data.branch;
297
+ },
298
+ balanced: (data) => {
299
+ if (data.changes && data.changes.files > 0) {
300
+ const parts = [];
301
+ if (data.changes.insertions > 0) parts.push(`+${data.changes.insertions}`);
302
+ if (data.changes.deletions > 0) parts.push(`-${data.changes.deletions}`);
303
+ if (parts.length > 0) {
304
+ return `${data.branch} [${parts.join(" ")}]`;
305
+ }
306
+ }
307
+ return data.branch;
308
+ },
309
+ compact: (data) => {
310
+ if (data.changes && data.changes.files > 0) {
311
+ const parts = [];
312
+ if (data.changes.insertions > 0) parts.push(`+${data.changes.insertions}`);
313
+ if (data.changes.deletions > 0) parts.push(`-${data.changes.deletions}`);
314
+ if (parts.length > 0) {
315
+ return `${data.branch} ${parts.join("/")}`;
316
+ }
317
+ }
318
+ return data.branch;
319
+ },
320
+ playful: (data) => {
321
+ if (data.changes && data.changes.files > 0) {
322
+ const parts = [];
323
+ if (data.changes.insertions > 0) parts.push(`\u2B06${data.changes.insertions}`);
324
+ if (data.changes.deletions > 0) parts.push(`\u2B07${data.changes.deletions}`);
325
+ if (parts.length > 0) {
326
+ return `\u{1F500} ${data.branch} ${parts.join(" ")}`;
327
+ }
328
+ }
329
+ return `\u{1F500} ${data.branch}`;
330
+ },
331
+ verbose: (data) => {
332
+ if (data.changes && data.changes.files > 0) {
333
+ const parts = [];
334
+ if (data.changes.insertions > 0) parts.push(`+${data.changes.insertions} insertions`);
335
+ if (data.changes.deletions > 0) parts.push(`-${data.changes.deletions} deletions`);
336
+ if (parts.length > 0) {
337
+ return `branch: ${data.branch} [${parts.join(", ")}]`;
338
+ }
339
+ }
340
+ return `branch: ${data.branch} (HEAD)`;
341
+ },
342
+ labeled: (data) => {
343
+ if (data.changes && data.changes.files > 0) {
344
+ const parts = [];
345
+ if (data.changes.insertions > 0) parts.push(`+${data.changes.insertions}`);
346
+ if (data.changes.deletions > 0) parts.push(`-${data.changes.deletions}`);
347
+ if (parts.length > 0) {
348
+ const changes = `${data.changes.files} files: ${parts.join("/")}`;
349
+ return `Git: ${data.branch} [${changes}]`;
350
+ }
351
+ }
352
+ return `Git: ${data.branch}`;
353
+ },
354
+ indicator: (data) => {
355
+ if (data.changes && data.changes.files > 0) {
356
+ const parts = [];
357
+ if (data.changes.insertions > 0) parts.push(`+${data.changes.insertions}`);
358
+ if (data.changes.deletions > 0) parts.push(`-${data.changes.deletions}`);
359
+ if (parts.length > 0) {
360
+ return `\u25CF ${data.branch} [${parts.join(" ")}]`;
361
+ }
362
+ }
363
+ return withIndicator(data.branch);
364
+ }
365
+ };
366
+
286
367
  // src/widgets/git/git-widget.ts
287
368
  var GitWidget = class {
288
369
  id = "git";
@@ -298,6 +379,7 @@ var GitWidget = class {
298
379
  git = null;
299
380
  enabled = true;
300
381
  cwd = null;
382
+ styleFn = gitStyles.balanced;
301
383
  /**
302
384
  * @param gitFactory - Optional factory function for creating IGit instances
303
385
  * If not provided, uses default createGit (production)
@@ -306,6 +388,12 @@ var GitWidget = class {
306
388
  constructor(gitFactory) {
307
389
  this.gitFactory = gitFactory || createGit;
308
390
  }
391
+ setStyle(style = "balanced") {
392
+ const fn = gitStyles[style];
393
+ if (fn) {
394
+ this.styleFn = fn;
395
+ }
396
+ }
309
397
  async initialize(context) {
310
398
  this.enabled = context.config?.enabled !== false;
311
399
  }
@@ -319,7 +407,24 @@ var GitWidget = class {
319
407
  if (!branch) {
320
408
  return null;
321
409
  }
322
- return branch;
410
+ let changes;
411
+ try {
412
+ const diffSummary = await this.git.diffSummary();
413
+ if (diffSummary.fileCount > 0) {
414
+ let insertions = 0;
415
+ let deletions = 0;
416
+ for (const file of diffSummary.files) {
417
+ insertions += file.insertions || 0;
418
+ deletions += file.deletions || 0;
419
+ }
420
+ if (insertions > 0 || deletions > 0) {
421
+ changes = { files: diffSummary.fileCount, insertions, deletions };
422
+ }
423
+ }
424
+ } catch {
425
+ }
426
+ const renderData = { branch, changes };
427
+ return this.styleFn(renderData);
323
428
  } catch {
324
429
  return null;
325
430
  }
@@ -337,13 +442,29 @@ var GitWidget = class {
337
442
  }
338
443
  };
339
444
 
340
- // src/ui/utils/colors.ts
341
- var reset = "\x1B[0m";
342
- var red = "\x1B[31m";
343
- var green = "\x1B[32m";
344
- var gray = "\x1B[90m";
345
- var lightGray = "\x1B[37m";
346
- var bold = "\x1B[1m";
445
+ // src/widgets/git-tag/styles.ts
446
+ var gitTagStyles = {
447
+ balanced: (data) => {
448
+ return data.tag || "\u2014";
449
+ },
450
+ compact: (data) => {
451
+ if (!data.tag) return "\u2014";
452
+ return data.tag.replace(/^v/, "");
453
+ },
454
+ playful: (data) => {
455
+ return `\u{1F3F7}\uFE0F ${data.tag || "\u2014"}`;
456
+ },
457
+ verbose: (data) => {
458
+ if (!data.tag) return "version: none";
459
+ return `version ${data.tag}`;
460
+ },
461
+ labeled: (data) => {
462
+ return withLabel("Tag", data.tag || "none");
463
+ },
464
+ indicator: (data) => {
465
+ return withIndicator(data.tag || "\u2014");
466
+ }
467
+ };
347
468
 
348
469
  // src/widgets/git/git-tag-widget.ts
349
470
  var GitTagWidget = class {
@@ -360,7 +481,7 @@ var GitTagWidget = class {
360
481
  git = null;
361
482
  enabled = true;
362
483
  cwd = null;
363
- latestTag = null;
484
+ styleFn = gitTagStyles.balanced;
364
485
  /**
365
486
  * @param gitFactory - Optional factory function for creating IGit instances
366
487
  * If not provided, uses default createGit (production)
@@ -369,6 +490,12 @@ var GitTagWidget = class {
369
490
  constructor(gitFactory) {
370
491
  this.gitFactory = gitFactory || createGit;
371
492
  }
493
+ setStyle(style = "balanced") {
494
+ const fn = gitTagStyles[style];
495
+ if (fn) {
496
+ this.styleFn = fn;
497
+ }
498
+ }
372
499
  async initialize(context) {
373
500
  this.enabled = context.config?.enabled !== false;
374
501
  }
@@ -377,12 +504,9 @@ var GitTagWidget = class {
377
504
  return null;
378
505
  }
379
506
  try {
380
- this.latestTag = await (this.git.latestTag?.() ?? Promise.resolve(null));
381
- if (!this.latestTag) {
382
- return `${gray}Tag:${reset} no tag`;
383
- }
384
- const tagValue = `${green}${this.latestTag}${reset}`;
385
- return `${gray}Tag:${reset} ${tagValue}`;
507
+ const latestTag = await (this.git.latestTag?.() ?? Promise.resolve(null));
508
+ const renderData = { tag: latestTag };
509
+ return this.styleFn(renderData);
386
510
  } catch {
387
511
  return null;
388
512
  }
@@ -400,6 +524,34 @@ var GitTagWidget = class {
400
524
  }
401
525
  };
402
526
 
527
+ // src/widgets/model/styles.ts
528
+ function getShortName(displayName) {
529
+ return displayName.replace(/^Claude\s+/, "");
530
+ }
531
+ var modelStyles = {
532
+ balanced: (data) => {
533
+ return data.displayName;
534
+ },
535
+ compact: (data) => {
536
+ return getShortName(data.displayName);
537
+ },
538
+ playful: (data) => {
539
+ return `\u{1F916} ${getShortName(data.displayName)}`;
540
+ },
541
+ technical: (data) => {
542
+ return data.id;
543
+ },
544
+ symbolic: (data) => {
545
+ return `\u25C6 ${getShortName(data.displayName)}`;
546
+ },
547
+ labeled: (data) => {
548
+ return withLabel("Model", getShortName(data.displayName));
549
+ },
550
+ indicator: (data) => {
551
+ return withIndicator(getShortName(data.displayName));
552
+ }
553
+ };
554
+
403
555
  // src/widgets/core/stdin-data-widget.ts
404
556
  var StdinDataWidget = class {
405
557
  /**
@@ -469,8 +621,39 @@ var ModelWidget = class extends StdinDataWidget {
469
621
  0
470
622
  // First line
471
623
  );
472
- renderWithData(data, context) {
473
- return data.model.display_name;
624
+ styleFn = modelStyles.balanced;
625
+ setStyle(style = "balanced") {
626
+ const fn = modelStyles[style];
627
+ if (fn) {
628
+ this.styleFn = fn;
629
+ }
630
+ }
631
+ renderWithData(data, _context) {
632
+ const renderData = {
633
+ displayName: data.model.display_name,
634
+ id: data.model.id
635
+ };
636
+ return this.styleFn(renderData);
637
+ }
638
+ };
639
+
640
+ // src/ui/utils/colors.ts
641
+ var reset = "\x1B[0m";
642
+ var red = "\x1B[31m";
643
+ var gray = "\x1B[90m";
644
+ var lightGray = "\x1B[37m";
645
+ var bold = "\x1B[1m";
646
+
647
+ // src/ui/theme/default-theme.ts
648
+ var DEFAULT_THEME = {
649
+ context: {
650
+ low: gray,
651
+ medium: gray,
652
+ high: gray
653
+ },
654
+ lines: {
655
+ added: gray,
656
+ removed: gray
474
657
  }
475
658
  };
476
659
 
@@ -495,37 +678,42 @@ function formatDuration(ms) {
495
678
  return parts.join(" ");
496
679
  }
497
680
  function formatCostUSD(usd) {
498
- const absUsd = Math.abs(usd);
499
- if (usd < 0) {
500
- return `$${usd.toFixed(2)}`;
501
- } else if (absUsd < COST_THRESHOLDS.SMALL) {
502
- return `$${usd.toFixed(4)}`;
503
- } else if (absUsd < COST_THRESHOLDS.LARGE) {
504
- return `$${usd.toFixed(2)}`;
505
- } else {
506
- return `$${Math.floor(usd).toFixed(0)}`;
507
- }
508
- }
509
- function progressBar(percent, width = DEFAULTS.PROGRESS_BAR_WIDTH) {
510
- const clampedPercent = Math.max(0, Math.min(100, percent));
511
- const filled = Math.round(clampedPercent / 100 * width);
512
- const empty = width - filled;
513
- return "\u2588".repeat(filled) + "\u2591".repeat(empty);
681
+ return `$${usd.toFixed(2)}`;
514
682
  }
515
683
  function colorize(text, color) {
516
684
  return `${color}${text}${ANSI_COLORS.RESET}`;
517
685
  }
518
686
 
519
- // src/ui/theme/default-theme.ts
520
- var DEFAULT_THEME = {
521
- context: {
522
- low: gray,
523
- medium: gray,
524
- high: gray
687
+ // src/widgets/context/styles.ts
688
+ var contextStyles = {
689
+ balanced: (data) => {
690
+ const bar = progressBar(data.percent, 10);
691
+ return `[${bar}] ${data.percent}%`;
525
692
  },
526
- lines: {
527
- added: gray,
528
- removed: gray
693
+ compact: (data) => {
694
+ return `${data.percent}%`;
695
+ },
696
+ playful: (data) => {
697
+ const bar = progressBar(data.percent, 10);
698
+ return `\u{1F9E0} [${bar}] ${data.percent}%`;
699
+ },
700
+ verbose: (data) => {
701
+ const usedFormatted = data.used.toLocaleString();
702
+ const maxFormatted = data.contextWindowSize.toLocaleString();
703
+ return `${usedFormatted} / ${maxFormatted} tokens (${data.percent}%)`;
704
+ },
705
+ symbolic: (data) => {
706
+ const filled = Math.round(data.percent / 100 * 5);
707
+ const empty = 5 - filled;
708
+ return `${"\u25AE".repeat(filled)}${"\u25AF".repeat(empty)} ${data.percent}%`;
709
+ },
710
+ "compact-verbose": (data) => {
711
+ const usedK = data.used >= 1e3 ? `${Math.floor(data.used / 1e3)}K` : data.used.toString();
712
+ const maxK = data.contextWindowSize >= 1e3 ? `${Math.floor(data.contextWindowSize / 1e3)}K` : data.contextWindowSize.toString();
713
+ return `${data.percent}% (${usedK}/${maxK})`;
714
+ },
715
+ indicator: (data) => {
716
+ return `\u25CF ${data.percent}%`;
529
717
  }
530
718
  };
531
719
 
@@ -541,18 +729,30 @@ var ContextWidget = class extends StdinDataWidget {
541
729
  // First line
542
730
  );
543
731
  colors;
732
+ styleFn = contextStyles.balanced;
544
733
  constructor(colors) {
545
734
  super();
546
735
  this.colors = colors ?? DEFAULT_THEME.context;
547
736
  }
548
- renderWithData(data, context) {
737
+ setStyle(style = "balanced") {
738
+ const fn = contextStyles[style];
739
+ if (fn) {
740
+ this.styleFn = fn;
741
+ }
742
+ }
743
+ renderWithData(data, _context) {
549
744
  const { current_usage, context_window_size } = data.context_window;
550
745
  if (!current_usage) return null;
551
746
  const used = current_usage.input_tokens + current_usage.cache_creation_input_tokens + current_usage.cache_read_input_tokens + current_usage.output_tokens;
552
747
  const percent = Math.round(used / context_window_size * 100);
553
- const bar = progressBar(percent, DEFAULTS.PROGRESS_BAR_WIDTH);
748
+ const renderData = {
749
+ used,
750
+ contextWindowSize: context_window_size,
751
+ percent
752
+ };
753
+ const output = this.styleFn(renderData);
554
754
  const color = this.getContextColor(percent);
555
- return colorize(`[${bar}] ${percent}%`, color);
755
+ return colorize(output, color);
556
756
  }
557
757
  getContextColor(percent) {
558
758
  const clampedPercent = Math.max(0, Math.min(100, percent));
@@ -566,6 +766,25 @@ var ContextWidget = class extends StdinDataWidget {
566
766
  }
567
767
  };
568
768
 
769
+ // src/widgets/cost/styles.ts
770
+ var costStyles = {
771
+ balanced: (data) => {
772
+ return formatCostUSD(data.costUsd);
773
+ },
774
+ compact: (data) => {
775
+ return formatCostUSD(data.costUsd);
776
+ },
777
+ playful: (data) => {
778
+ return `\u{1F4B0} ${formatCostUSD(data.costUsd)}`;
779
+ },
780
+ labeled: (data) => {
781
+ return withLabel("Cost", formatCostUSD(data.costUsd));
782
+ },
783
+ indicator: (data) => {
784
+ return withIndicator(formatCostUSD(data.costUsd));
785
+ }
786
+ };
787
+
569
788
  // src/widgets/cost-widget.ts
570
789
  var CostWidget = class extends StdinDataWidget {
571
790
  id = "cost";
@@ -577,12 +796,65 @@ var CostWidget = class extends StdinDataWidget {
577
796
  0
578
797
  // First line
579
798
  );
580
- renderWithData(data, context) {
799
+ styleFn = costStyles.balanced;
800
+ setStyle(style = "balanced") {
801
+ const fn = costStyles[style];
802
+ if (fn) {
803
+ this.styleFn = fn;
804
+ }
805
+ }
806
+ renderWithData(data, _context) {
581
807
  if (!data.cost || data.cost.total_cost_usd === void 0) return null;
582
- return formatCostUSD(data.cost.total_cost_usd);
808
+ const renderData = {
809
+ costUsd: data.cost.total_cost_usd
810
+ };
811
+ return this.styleFn(renderData);
583
812
  }
584
813
  };
585
814
 
815
+ // src/widgets/lines/styles.ts
816
+ function createLinesStyles(colors) {
817
+ return {
818
+ balanced: (data) => {
819
+ const addedStr = colorize(`+${data.added}`, colors.added);
820
+ const removedStr = colorize(`-${data.removed}`, colors.removed);
821
+ return `${addedStr}/${removedStr}`;
822
+ },
823
+ compact: (data) => {
824
+ const addedStr = colorize(`+${data.added}`, colors.added);
825
+ const removedStr = colorize(`-${data.removed}`, colors.removed);
826
+ return `${addedStr}${removedStr}`;
827
+ },
828
+ playful: (data) => {
829
+ const addedStr = colorize(`\u2795${data.added}`, colors.added);
830
+ const removedStr = colorize(`\u2796${data.removed}`, colors.removed);
831
+ return `${addedStr} ${removedStr}`;
832
+ },
833
+ verbose: (data) => {
834
+ const parts = [];
835
+ if (data.added > 0) {
836
+ parts.push(colorize(`+${data.added} added`, colors.added));
837
+ }
838
+ if (data.removed > 0) {
839
+ parts.push(colorize(`-${data.removed} removed`, colors.removed));
840
+ }
841
+ return parts.join(", ");
842
+ },
843
+ labeled: (data) => {
844
+ const addedStr = colorize(`+${data.added}`, colors.added);
845
+ const removedStr = colorize(`-${data.removed}`, colors.removed);
846
+ const lines = `${addedStr}/${removedStr}`;
847
+ return withLabel("Lines", lines);
848
+ },
849
+ indicator: (data) => {
850
+ const addedStr = colorize(`+${data.added}`, colors.added);
851
+ const removedStr = colorize(`-${data.removed}`, colors.removed);
852
+ const lines = `${addedStr}/${removedStr}`;
853
+ return withIndicator(lines);
854
+ }
855
+ };
856
+ }
857
+
586
858
  // src/widgets/lines-widget.ts
587
859
  var LinesWidget = class extends StdinDataWidget {
588
860
  id = "lines";
@@ -595,16 +867,59 @@ var LinesWidget = class extends StdinDataWidget {
595
867
  // First line
596
868
  );
597
869
  colors;
870
+ linesStyles;
871
+ styleFn;
598
872
  constructor(colors) {
599
873
  super();
600
874
  this.colors = colors ?? DEFAULT_THEME.lines;
875
+ this.linesStyles = createLinesStyles(this.colors);
876
+ this.styleFn = this.linesStyles.balanced;
877
+ }
878
+ setStyle(style = "balanced") {
879
+ const fn = this.linesStyles[style];
880
+ if (fn) {
881
+ this.styleFn = fn;
882
+ }
601
883
  }
602
- renderWithData(data, context) {
884
+ renderWithData(data, _context) {
603
885
  const added = data.cost?.total_lines_added ?? 0;
604
886
  const removed = data.cost?.total_lines_removed ?? 0;
605
- const addedStr = colorize(`+${added}`, this.colors.added);
606
- const removedStr = colorize(`-${removed}`, this.colors.removed);
607
- return `${addedStr}/${removedStr}`;
887
+ const renderData = { added, removed };
888
+ return this.styleFn(renderData);
889
+ }
890
+ };
891
+
892
+ // src/widgets/duration/styles.ts
893
+ var durationStyles = {
894
+ balanced: (data) => {
895
+ return formatDuration(data.durationMs);
896
+ },
897
+ compact: (data) => {
898
+ const totalSeconds = Math.floor(data.durationMs / 1e3);
899
+ const hours = Math.floor(totalSeconds / 3600);
900
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
901
+ if (hours > 0) {
902
+ return `${hours}h${minutes}m`;
903
+ }
904
+ return `${minutes}m`;
905
+ },
906
+ playful: (data) => {
907
+ const totalSeconds = Math.floor(data.durationMs / 1e3);
908
+ const hours = Math.floor(totalSeconds / 3600);
909
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
910
+ if (hours > 0) {
911
+ return `\u231B ${hours}h ${minutes}m`;
912
+ }
913
+ return `\u231B ${minutes}m`;
914
+ },
915
+ technical: (data) => {
916
+ return `${Math.floor(data.durationMs)}ms`;
917
+ },
918
+ labeled: (data) => {
919
+ return withLabel("Time", formatDuration(data.durationMs));
920
+ },
921
+ indicator: (data) => {
922
+ return withIndicator(formatDuration(data.durationMs));
608
923
  }
609
924
  };
610
925
 
@@ -619,83 +934,19 @@ var DurationWidget = class extends StdinDataWidget {
619
934
  0
620
935
  // First line
621
936
  );
622
- renderWithData(data, context) {
623
- if (!data.cost || data.cost.total_duration_ms === void 0) return null;
624
- return formatDuration(data.cost.total_duration_ms);
625
- }
626
- };
627
-
628
- // src/widgets/git/git-changes-widget.ts
629
- var GitChangesWidget = class {
630
- id = "git-changes";
631
- metadata = createWidgetMetadata(
632
- "Git Changes",
633
- "Displays git diff statistics",
634
- "1.0.0",
635
- "claude-scope",
636
- 0
637
- // First line
638
- );
639
- gitFactory;
640
- git = null;
641
- enabled = true;
642
- cwd = null;
643
- /**
644
- * @param gitFactory - Optional factory function for creating IGit instances
645
- * If not provided, uses default createGit (production)
646
- * Tests can inject MockGit factory here
647
- */
648
- constructor(gitFactory) {
649
- this.gitFactory = gitFactory || createGit;
650
- }
651
- async initialize(context) {
652
- this.enabled = context.config?.enabled !== false;
653
- }
654
- async update(data) {
655
- if (data.cwd !== this.cwd) {
656
- this.cwd = data.cwd;
657
- this.git = this.gitFactory(data.cwd);
658
- }
659
- }
660
- async render(context) {
661
- if (!this.enabled || !this.git || !this.cwd) {
662
- return null;
663
- }
664
- let changes;
665
- try {
666
- const summary = await this.git.diffSummary(["--shortstat"]);
667
- let insertions = 0;
668
- let deletions = 0;
669
- if (summary.files && summary.files.length > 0) {
670
- for (const file of summary.files) {
671
- if (typeof file.insertions === "number") {
672
- insertions += file.insertions;
673
- }
674
- if (typeof file.deletions === "number") {
675
- deletions += file.deletions;
676
- }
677
- }
678
- }
679
- if (insertions === 0 && deletions === 0) {
680
- return null;
681
- }
682
- changes = { insertions, deletions };
683
- } catch {
684
- return null;
937
+ styleFn = durationStyles.balanced;
938
+ setStyle(style = "balanced") {
939
+ const fn = durationStyles[style];
940
+ if (fn) {
941
+ this.styleFn = fn;
685
942
  }
686
- if (!changes) return null;
687
- if (changes.insertions === 0 && changes.deletions === 0) {
688
- return null;
689
- }
690
- const parts = [];
691
- if (changes.insertions > 0) parts.push(`+${changes.insertions}`);
692
- if (changes.deletions > 0) parts.push(`-${changes.deletions}`);
693
- return parts.join(",");
694
943
  }
695
- isEnabled() {
696
- return this.enabled;
697
- }
698
- async cleanup() {
944
+ renderWithData(data, _context) {
945
+ if (!data.cost || data.cost.total_duration_ms === void 0) return null;
946
+ const renderData = {
947
+ durationMs: data.cost.total_duration_ms
948
+ };
949
+ return this.styleFn(renderData);
699
950
  }
700
951
  };
701
952
 
@@ -846,6 +1097,82 @@ var ConfigProvider = class {
846
1097
  }
847
1098
  };
848
1099
 
1100
+ // src/widgets/config-count/styles.ts
1101
+ var configCountStyles = {
1102
+ balanced: (data) => {
1103
+ const { claudeMdCount, rulesCount, mcpCount, hooksCount } = data;
1104
+ const parts = [];
1105
+ if (claudeMdCount > 0) {
1106
+ parts.push(`CLAUDE.md:${claudeMdCount}`);
1107
+ }
1108
+ if (rulesCount > 0) {
1109
+ parts.push(`rules:${rulesCount}`);
1110
+ }
1111
+ if (mcpCount > 0) {
1112
+ parts.push(`MCPs:${mcpCount}`);
1113
+ }
1114
+ if (hooksCount > 0) {
1115
+ parts.push(`hooks:${hooksCount}`);
1116
+ }
1117
+ return parts.join(" \u2502 ");
1118
+ },
1119
+ compact: (data) => {
1120
+ const { claudeMdCount, rulesCount, mcpCount, hooksCount } = data;
1121
+ const parts = [];
1122
+ if (claudeMdCount > 0) {
1123
+ parts.push(`${claudeMdCount} docs`);
1124
+ }
1125
+ if (rulesCount > 0) {
1126
+ parts.push(`${rulesCount} rules`);
1127
+ }
1128
+ if (mcpCount > 0) {
1129
+ parts.push(`${mcpCount} MCPs`);
1130
+ }
1131
+ if (hooksCount > 0) {
1132
+ const hookLabel = hooksCount === 1 ? "hook" : "hooks";
1133
+ parts.push(`${hooksCount} ${hookLabel}`);
1134
+ }
1135
+ return parts.join(" \u2502 ");
1136
+ },
1137
+ playful: (data) => {
1138
+ const { claudeMdCount, rulesCount, mcpCount, hooksCount } = data;
1139
+ const parts = [];
1140
+ if (claudeMdCount > 0) {
1141
+ parts.push(`\u{1F4C4} CLAUDE.md:${claudeMdCount}`);
1142
+ }
1143
+ if (rulesCount > 0) {
1144
+ parts.push(`\u{1F4DC} rules:${rulesCount}`);
1145
+ }
1146
+ if (mcpCount > 0) {
1147
+ parts.push(`\u{1F50C} MCPs:${mcpCount}`);
1148
+ }
1149
+ if (hooksCount > 0) {
1150
+ parts.push(`\u{1FA9D} hooks:${hooksCount}`);
1151
+ }
1152
+ return parts.join(" \u2502 ");
1153
+ },
1154
+ verbose: (data) => {
1155
+ const { claudeMdCount, rulesCount, mcpCount, hooksCount } = data;
1156
+ const parts = [];
1157
+ if (claudeMdCount > 0) {
1158
+ parts.push(`${claudeMdCount} CLAUDE.md`);
1159
+ }
1160
+ if (rulesCount > 0) {
1161
+ parts.push(`${rulesCount} rules`);
1162
+ }
1163
+ if (mcpCount > 0) {
1164
+ parts.push(`${mcpCount} MCP servers`);
1165
+ }
1166
+ if (hooksCount > 0) {
1167
+ parts.push(`${hooksCount} hook`);
1168
+ }
1169
+ return parts.join(" \u2502 ");
1170
+ }
1171
+ };
1172
+
1173
+ // src/core/style-types.ts
1174
+ var DEFAULT_WIDGET_STYLE = "balanced";
1175
+
849
1176
  // src/widgets/config-count-widget.ts
850
1177
  var ConfigCountWidget = class {
851
1178
  id = "config-count";
@@ -860,6 +1187,13 @@ var ConfigCountWidget = class {
860
1187
  configProvider = new ConfigProvider();
861
1188
  configs;
862
1189
  cwd;
1190
+ styleFn = configCountStyles.balanced;
1191
+ setStyle(style = DEFAULT_WIDGET_STYLE) {
1192
+ const fn = configCountStyles[style];
1193
+ if (fn) {
1194
+ this.styleFn = fn;
1195
+ }
1196
+ }
863
1197
  async initialize() {
864
1198
  }
865
1199
  async update(data) {
@@ -878,20 +1212,13 @@ var ConfigCountWidget = class {
878
1212
  return null;
879
1213
  }
880
1214
  const { claudeMdCount, rulesCount, mcpCount, hooksCount } = this.configs;
881
- const parts = [];
882
- if (claudeMdCount > 0) {
883
- parts.push(`\u{1F4C4} ${claudeMdCount} CLAUDE.md`);
884
- }
885
- if (rulesCount > 0) {
886
- parts.push(`\u{1F4DC} ${rulesCount} rules`);
887
- }
888
- if (mcpCount > 0) {
889
- parts.push(`\u{1F50C} ${mcpCount} MCPs`);
890
- }
891
- if (hooksCount > 0) {
892
- parts.push(`\u{1FA9D} ${hooksCount} hooks`);
893
- }
894
- return parts.join(" \u2502 ") || null;
1215
+ const renderData = {
1216
+ claudeMdCount,
1217
+ rulesCount,
1218
+ mcpCount,
1219
+ hooksCount
1220
+ };
1221
+ return this.styleFn(renderData);
895
1222
  }
896
1223
  async cleanup() {
897
1224
  }
@@ -942,10 +1269,10 @@ function getRankValue(rank) {
942
1269
  "8": 8,
943
1270
  "9": 9,
944
1271
  "10": 10,
945
- "J": 11,
946
- "Q": 12,
947
- "K": 13,
948
- "A": 14
1272
+ J: 11,
1273
+ Q: 12,
1274
+ K: 13,
1275
+ A: 14
949
1276
  };
950
1277
  return values[rank];
951
1278
  }
@@ -1246,7 +1573,11 @@ function evaluateHand(hole, board) {
1246
1573
  const sfHighCard = getStraightFlushHighCard(allCards, flushSuit);
1247
1574
  if (sfHighCard === 14) {
1248
1575
  const participatingCards = getStraightFlushIndices(allCards, 14, flushSuit);
1249
- return { rank: 10 /* RoyalFlush */, ...HAND_DISPLAY[10 /* RoyalFlush */], participatingCards };
1576
+ return {
1577
+ rank: 10 /* RoyalFlush */,
1578
+ ...HAND_DISPLAY[10 /* RoyalFlush */],
1579
+ participatingCards
1580
+ };
1250
1581
  }
1251
1582
  }
1252
1583
  if (flush) {
@@ -1254,13 +1585,21 @@ function evaluateHand(hole, board) {
1254
1585
  const sfHighCard = getStraightFlushHighCard(allCards, flushSuit);
1255
1586
  if (sfHighCard !== null) {
1256
1587
  const participatingCards = getStraightFlushIndices(allCards, sfHighCard, flushSuit);
1257
- return { rank: 9 /* StraightFlush */, ...HAND_DISPLAY[9 /* StraightFlush */], participatingCards };
1588
+ return {
1589
+ rank: 9 /* StraightFlush */,
1590
+ ...HAND_DISPLAY[9 /* StraightFlush */],
1591
+ participatingCards
1592
+ };
1258
1593
  }
1259
1594
  }
1260
1595
  if (maxCount === 4) {
1261
1596
  const rank = getMostCommonRank(allCards);
1262
1597
  const participatingCards = findCardsOfRank(allCards, rank);
1263
- return { rank: 8 /* FourOfAKind */, ...HAND_DISPLAY[8 /* FourOfAKind */], participatingCards };
1598
+ return {
1599
+ rank: 8 /* FourOfAKind */,
1600
+ ...HAND_DISPLAY[8 /* FourOfAKind */],
1601
+ participatingCards
1602
+ };
1264
1603
  }
1265
1604
  if (maxCount === 3 && pairCount >= 1) {
1266
1605
  const participatingCards = getFullHouseIndices(allCards);
@@ -1279,7 +1618,11 @@ function evaluateHand(hole, board) {
1279
1618
  if (maxCount === 3) {
1280
1619
  const rank = getMostCommonRank(allCards);
1281
1620
  const participatingCards = findCardsOfRank(allCards, rank);
1282
- return { rank: 4 /* ThreeOfAKind */, ...HAND_DISPLAY[4 /* ThreeOfAKind */], participatingCards };
1621
+ return {
1622
+ rank: 4 /* ThreeOfAKind */,
1623
+ ...HAND_DISPLAY[4 /* ThreeOfAKind */],
1624
+ participatingCards
1625
+ };
1283
1626
  }
1284
1627
  if (pairCount >= 2) {
1285
1628
  const [rank1, rank2] = getTwoPairRanks(allCards);
@@ -1294,8 +1637,100 @@ function evaluateHand(hole, board) {
1294
1637
  return { rank: 2 /* OnePair */, ...HAND_DISPLAY[2 /* OnePair */], participatingCards };
1295
1638
  }
1296
1639
  const highestIdx = getHighestCardIndex(allCards);
1297
- return { rank: 1 /* HighCard */, ...HAND_DISPLAY[1 /* HighCard */], participatingCards: [highestIdx] };
1640
+ return {
1641
+ rank: 1 /* HighCard */,
1642
+ ...HAND_DISPLAY[1 /* HighCard */],
1643
+ participatingCards: [highestIdx]
1644
+ };
1645
+ }
1646
+
1647
+ // src/widgets/poker/styles.ts
1648
+ var HAND_ABBREVIATIONS = {
1649
+ "Royal Flush": "RF",
1650
+ "Straight Flush": "SF",
1651
+ "Four of a Kind": "4K",
1652
+ "Full House": "FH",
1653
+ "Flush": "FL",
1654
+ "Straight": "ST",
1655
+ "Three of a Kind": "3K",
1656
+ "Two Pair": "2P",
1657
+ "One Pair": "1P",
1658
+ "High Card": "HC",
1659
+ "Nothing": "\u2014"
1660
+ };
1661
+ function formatCardByParticipation(cardData, isParticipating) {
1662
+ const color = isRedSuit(cardData.card.suit) ? red : gray;
1663
+ const cardText = formatCard(cardData.card);
1664
+ if (isParticipating) {
1665
+ return `${color}${bold}(${cardText})${reset} `;
1666
+ } else {
1667
+ return `${color}${cardText}${reset} `;
1668
+ }
1669
+ }
1670
+ function formatCardCompact(cardData, isParticipating) {
1671
+ const color = isRedSuit(cardData.card.suit) ? red : gray;
1672
+ const cardText = formatCardTextCompact(cardData.card);
1673
+ if (isParticipating) {
1674
+ return `${color}${bold}(${cardText})${reset}`;
1675
+ } else {
1676
+ return `${color}${cardText}${reset}`;
1677
+ }
1678
+ }
1679
+ function formatCardTextCompact(card) {
1680
+ const rankSymbols = {
1681
+ "10": "T",
1682
+ "11": "J",
1683
+ "12": "Q",
1684
+ "13": "K",
1685
+ "14": "A"
1686
+ };
1687
+ const rank = String(card.rank);
1688
+ const rankSymbol = rankSymbols[rank] ?? rank;
1689
+ return `${rankSymbol}${card.suit}`;
1690
+ }
1691
+ function formatHandResult(handResult) {
1692
+ if (!handResult) {
1693
+ return "\u2014";
1694
+ }
1695
+ const playerParticipates = handResult.participatingIndices.some((idx) => idx < 2);
1696
+ if (!playerParticipates) {
1697
+ return `Nothing \u{1F0CF}`;
1698
+ } else {
1699
+ return `${handResult.name}! ${handResult.emoji}`;
1700
+ }
1701
+ }
1702
+ function getHandAbbreviation(handResult) {
1703
+ if (!handResult) {
1704
+ return "\u2014 (\u2014)";
1705
+ }
1706
+ const abbreviation = HAND_ABBREVIATIONS[handResult.name] ?? "\u2014";
1707
+ return `${abbreviation} (${handResult.name})`;
1298
1708
  }
1709
+ var pokerStyles = {
1710
+ balanced: (data) => {
1711
+ const { holeCards, boardCards, handResult } = data;
1712
+ const participatingSet = new Set(handResult?.participatingIndices || []);
1713
+ const handStr = holeCards.map((hc, idx) => formatCardByParticipation(hc, participatingSet.has(idx))).join("");
1714
+ const boardStr = boardCards.map((bc, idx) => formatCardByParticipation(bc, participatingSet.has(idx + 2))).join("");
1715
+ const handLabel = colorize("Hand:", lightGray);
1716
+ const boardLabel = colorize("Board:", lightGray);
1717
+ return `${handLabel} ${handStr}| ${boardLabel} ${boardStr}\u2192 ${formatHandResult(handResult)}`;
1718
+ },
1719
+ compact: (data) => {
1720
+ return pokerStyles.balanced(data);
1721
+ },
1722
+ playful: (data) => {
1723
+ return pokerStyles.balanced(data);
1724
+ },
1725
+ "compact-verbose": (data) => {
1726
+ const { holeCards, boardCards, handResult } = data;
1727
+ const participatingSet = new Set(handResult?.participatingIndices || []);
1728
+ const handStr = holeCards.map((hc, idx) => formatCardCompact(hc, participatingSet.has(idx))).join("");
1729
+ const boardStr = boardCards.map((bc, idx) => formatCardCompact(bc, participatingSet.has(idx + 2))).join("");
1730
+ const abbreviation = getHandAbbreviation(handResult);
1731
+ return `${handStr}| ${boardStr}\u2192 ${abbreviation}`;
1732
+ }
1733
+ };
1299
1734
 
1300
1735
  // src/widgets/poker-widget.ts
1301
1736
  var PokerWidget = class extends StdinDataWidget {
@@ -1314,8 +1749,12 @@ var PokerWidget = class extends StdinDataWidget {
1314
1749
  lastUpdateTimestamp = 0;
1315
1750
  THROTTLE_MS = 5e3;
1316
1751
  // 5 seconds
1317
- constructor() {
1318
- super();
1752
+ styleFn = pokerStyles.balanced;
1753
+ setStyle(style = DEFAULT_WIDGET_STYLE) {
1754
+ const fn = pokerStyles[style];
1755
+ if (fn) {
1756
+ this.styleFn = fn;
1757
+ }
1319
1758
  }
1320
1759
  /**
1321
1760
  * Generate new poker hand on each update
@@ -1356,30 +1795,37 @@ var PokerWidget = class extends StdinDataWidget {
1356
1795
  * Format card with appropriate color (red for ♥♦, gray for ♠♣)
1357
1796
  */
1358
1797
  formatCardColor(card) {
1359
- const color = isRedSuit(card.suit) ? red : gray;
1360
- return colorize(`[${formatCard(card)}]`, color);
1361
- }
1362
- /**
1363
- * Format card based on participation in best hand
1364
- * Participating cards: (K♠) with color + BOLD
1365
- * Non-participating cards: K♠ with color, no brackets
1366
- */
1367
- formatCardByParticipation(cardData, isParticipating) {
1368
- const color = isRedSuit(cardData.card.suit) ? red : gray;
1369
- const cardText = formatCard(cardData.card);
1370
- if (isParticipating) {
1371
- return `${color}${bold}(${cardText})${reset} `;
1372
- } else {
1373
- return `${color}${cardText}${reset} `;
1374
- }
1798
+ const color = isRedSuit(card.suit) ? "red" : "gray";
1799
+ return formatCard(card);
1375
1800
  }
1376
1801
  renderWithData(_data, _context) {
1377
- const participatingSet = new Set(this.handResult?.participatingIndices || []);
1378
- const handStr = this.holeCards.map((hc, idx) => this.formatCardByParticipation(hc, participatingSet.has(idx))).join("");
1379
- const boardStr = this.boardCards.map((bc, idx) => this.formatCardByParticipation(bc, participatingSet.has(idx + 2))).join("");
1380
- const handLabel = colorize("Hand:", lightGray);
1381
- const boardLabel = colorize("Board:", lightGray);
1382
- return `${handLabel} ${handStr} | ${boardLabel} ${boardStr} \u2192 ${this.handResult?.text}`;
1802
+ const holeCardsData = this.holeCards.map((hc, idx) => ({
1803
+ card: hc.card,
1804
+ isParticipating: (this.handResult?.participatingIndices || []).includes(idx)
1805
+ }));
1806
+ const boardCardsData = this.boardCards.map((bc, idx) => ({
1807
+ card: bc.card,
1808
+ isParticipating: (this.handResult?.participatingIndices || []).includes(idx + 2)
1809
+ }));
1810
+ const handResult = this.handResult ? {
1811
+ name: this.getHandName(this.handResult.text),
1812
+ emoji: this.getHandEmoji(this.handResult.text),
1813
+ participatingIndices: this.handResult.participatingIndices
1814
+ } : null;
1815
+ const renderData = {
1816
+ holeCards: holeCardsData,
1817
+ boardCards: boardCardsData,
1818
+ handResult
1819
+ };
1820
+ return this.styleFn(renderData);
1821
+ }
1822
+ getHandName(text) {
1823
+ const match = text.match(/^([^!]+)/);
1824
+ return match ? match[1].trim() : "Nothing";
1825
+ }
1826
+ getHandEmoji(text) {
1827
+ const match = text.match(/([🃏♠️♥️♦️♣️🎉✨🌟])/);
1828
+ return match ? match[1] : "\u{1F0CF}";
1383
1829
  }
1384
1830
  };
1385
1831
 
@@ -1394,6 +1840,12 @@ var EmptyLineWidget = class extends StdinDataWidget {
1394
1840
  3
1395
1841
  // Fourth line (0-indexed)
1396
1842
  );
1843
+ /**
1844
+ * All styles return the same value (Braille Pattern Blank).
1845
+ * This method exists for API consistency with other widgets.
1846
+ */
1847
+ setStyle(_style) {
1848
+ }
1397
1849
  /**
1398
1850
  * Return Braille Pattern Blank to create a visible empty separator line.
1399
1851
  * U+2800 occupies cell width but appears blank, ensuring the line renders.
@@ -1558,9 +2010,7 @@ var StdinProvider = class {
1558
2010
  }
1559
2011
  const result = StdinDataSchema.validate(data);
1560
2012
  if (!result.success) {
1561
- throw new StdinValidationError(
1562
- `Validation failed: ${formatError(result.error)}`
1563
- );
2013
+ throw new StdinValidationError(`Validation failed: ${formatError(result.error)}`);
1564
2014
  }
1565
2015
  return result.data;
1566
2016
  }
@@ -1605,7 +2055,6 @@ async function main() {
1605
2055
  await registry.register(new DurationWidget());
1606
2056
  await registry.register(new GitWidget());
1607
2057
  await registry.register(new GitTagWidget());
1608
- await registry.register(new GitChangesWidget());
1609
2058
  await registry.register(new ConfigCountWidget());
1610
2059
  await registry.register(new PokerWidget());
1611
2060
  await registry.register(new EmptyLineWidget());
@@ -1618,10 +2067,10 @@ async function main() {
1618
2067
  for (const widget of registry.getAll()) {
1619
2068
  await widget.update(stdinData);
1620
2069
  }
1621
- const lines = await renderer.render(
1622
- registry.getEnabledWidgets(),
1623
- { width: 80, timestamp: Date.now() }
1624
- );
2070
+ const lines = await renderer.render(registry.getEnabledWidgets(), {
2071
+ width: 80,
2072
+ timestamp: Date.now()
2073
+ });
1625
2074
  return lines.join("\n");
1626
2075
  } catch (error) {
1627
2076
  const fallback = await tryGitFallback();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-scope",
3
- "version": "0.5.4",
3
+ "version": "0.5.8",
4
4
  "description": "Claude Code plugin for session status and analytics",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,6 +28,7 @@
28
28
  "dev": "tsx src/index.ts"
29
29
  },
30
30
  "devDependencies": {
31
+ "@biomejs/biome": "^2.3.11",
31
32
  "@types/node": "^22.10.2",
32
33
  "c8": "^10.1.3",
33
34
  "chai": "^6.2.2",