claude-scope 0.5.3 → 0.5.5

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: " ",
@@ -266,13 +260,9 @@ var NativeGit = class {
266
260
  }
267
261
  async latestTag() {
268
262
  try {
269
- const { stdout } = await execFileAsync(
270
- "git",
271
- ["describe", "--tags", "--abbrev=0"],
272
- {
273
- cwd: this.cwd
274
- }
275
- );
263
+ const { stdout } = await execFileAsync("git", ["describe", "--tags", "--abbrev=0"], {
264
+ cwd: this.cwd
265
+ });
276
266
  return stdout.trim();
277
267
  } catch {
278
268
  return null;
@@ -283,6 +273,55 @@ function createGit(cwd) {
283
273
  return new NativeGit(cwd);
284
274
  }
285
275
 
276
+ // src/ui/utils/style-utils.ts
277
+ function withLabel(prefix, value) {
278
+ if (prefix === "") return value;
279
+ return `${prefix}: ${value}`;
280
+ }
281
+ function withIndicator(value) {
282
+ return `\u25CF ${value}`;
283
+ }
284
+ function withFancy(value) {
285
+ return `\xAB${value}\xBB`;
286
+ }
287
+ function withBrackets(value) {
288
+ return `[${value}]`;
289
+ }
290
+ function withAngleBrackets(value) {
291
+ return `\u27E8${value}\u27E9`;
292
+ }
293
+ function progressBar(percent, width = 10) {
294
+ const clamped = Math.max(0, Math.min(100, percent));
295
+ const filled = Math.round(clamped / 100 * width);
296
+ const empty = width - filled;
297
+ return "\u2588".repeat(filled) + "\u2591".repeat(empty);
298
+ }
299
+
300
+ // src/widgets/git/styles.ts
301
+ var gitStyles = {
302
+ balanced: (data) => {
303
+ return data.branch;
304
+ },
305
+ compact: (data) => {
306
+ return data.branch;
307
+ },
308
+ playful: (data) => {
309
+ return `\u{1F500} ${data.branch}`;
310
+ },
311
+ verbose: (data) => {
312
+ return `branch: ${data.branch} (HEAD)`;
313
+ },
314
+ labeled: (data) => {
315
+ return withLabel("Git", data.branch);
316
+ },
317
+ indicator: (data) => {
318
+ return withIndicator(data.branch);
319
+ },
320
+ fancy: (data) => {
321
+ return withBrackets(data.branch);
322
+ }
323
+ };
324
+
286
325
  // src/widgets/git/git-widget.ts
287
326
  var GitWidget = class {
288
327
  id = "git";
@@ -298,6 +337,7 @@ var GitWidget = class {
298
337
  git = null;
299
338
  enabled = true;
300
339
  cwd = null;
340
+ styleFn = gitStyles.balanced;
301
341
  /**
302
342
  * @param gitFactory - Optional factory function for creating IGit instances
303
343
  * If not provided, uses default createGit (production)
@@ -306,6 +346,12 @@ var GitWidget = class {
306
346
  constructor(gitFactory) {
307
347
  this.gitFactory = gitFactory || createGit;
308
348
  }
349
+ setStyle(style = "balanced") {
350
+ const fn = gitStyles[style];
351
+ if (fn) {
352
+ this.styleFn = fn;
353
+ }
354
+ }
309
355
  async initialize(context) {
310
356
  this.enabled = context.config?.enabled !== false;
311
357
  }
@@ -319,7 +365,8 @@ var GitWidget = class {
319
365
  if (!branch) {
320
366
  return null;
321
367
  }
322
- return branch;
368
+ const renderData = { branch };
369
+ return this.styleFn(renderData);
323
370
  } catch {
324
371
  return null;
325
372
  }
@@ -337,13 +384,32 @@ var GitWidget = class {
337
384
  }
338
385
  };
339
386
 
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";
387
+ // src/widgets/git-tag/styles.ts
388
+ var gitTagStyles = {
389
+ balanced: (data) => {
390
+ return data.tag || "\u2014";
391
+ },
392
+ compact: (data) => {
393
+ if (!data.tag) return "\u2014";
394
+ return data.tag.replace(/^v/, "");
395
+ },
396
+ playful: (data) => {
397
+ return `\u{1F3F7}\uFE0F ${data.tag || "\u2014"}`;
398
+ },
399
+ verbose: (data) => {
400
+ if (!data.tag) return "version: none";
401
+ return `version ${data.tag}`;
402
+ },
403
+ labeled: (data) => {
404
+ return withLabel("Tag", data.tag || "none");
405
+ },
406
+ indicator: (data) => {
407
+ return withIndicator(data.tag || "\u2014");
408
+ },
409
+ fancy: (data) => {
410
+ return withAngleBrackets(data.tag || "\u2014");
411
+ }
412
+ };
347
413
 
348
414
  // src/widgets/git/git-tag-widget.ts
349
415
  var GitTagWidget = class {
@@ -360,7 +426,7 @@ var GitTagWidget = class {
360
426
  git = null;
361
427
  enabled = true;
362
428
  cwd = null;
363
- latestTag = null;
429
+ styleFn = gitTagStyles.balanced;
364
430
  /**
365
431
  * @param gitFactory - Optional factory function for creating IGit instances
366
432
  * If not provided, uses default createGit (production)
@@ -369,6 +435,12 @@ var GitTagWidget = class {
369
435
  constructor(gitFactory) {
370
436
  this.gitFactory = gitFactory || createGit;
371
437
  }
438
+ setStyle(style = "balanced") {
439
+ const fn = gitTagStyles[style];
440
+ if (fn) {
441
+ this.styleFn = fn;
442
+ }
443
+ }
372
444
  async initialize(context) {
373
445
  this.enabled = context.config?.enabled !== false;
374
446
  }
@@ -377,12 +449,9 @@ var GitTagWidget = class {
377
449
  return null;
378
450
  }
379
451
  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}`;
452
+ const latestTag = await (this.git.latestTag?.() ?? Promise.resolve(null));
453
+ const renderData = { tag: latestTag };
454
+ return this.styleFn(renderData);
386
455
  } catch {
387
456
  return null;
388
457
  }
@@ -400,6 +469,37 @@ var GitTagWidget = class {
400
469
  }
401
470
  };
402
471
 
472
+ // src/widgets/model/styles.ts
473
+ function getShortName(displayName) {
474
+ return displayName.replace(/^Claude\s+/, "");
475
+ }
476
+ var modelStyles = {
477
+ balanced: (data) => {
478
+ return data.displayName;
479
+ },
480
+ compact: (data) => {
481
+ return getShortName(data.displayName);
482
+ },
483
+ playful: (data) => {
484
+ return `\u{1F916} ${getShortName(data.displayName)}`;
485
+ },
486
+ technical: (data) => {
487
+ return data.id;
488
+ },
489
+ symbolic: (data) => {
490
+ return `\u25C6 ${getShortName(data.displayName)}`;
491
+ },
492
+ labeled: (data) => {
493
+ return withLabel("Model", getShortName(data.displayName));
494
+ },
495
+ indicator: (data) => {
496
+ return withIndicator(getShortName(data.displayName));
497
+ },
498
+ fancy: (data) => {
499
+ return withBrackets(getShortName(data.displayName));
500
+ }
501
+ };
502
+
403
503
  // src/widgets/core/stdin-data-widget.ts
404
504
  var StdinDataWidget = class {
405
505
  /**
@@ -469,8 +569,39 @@ var ModelWidget = class extends StdinDataWidget {
469
569
  0
470
570
  // First line
471
571
  );
472
- renderWithData(data, context) {
473
- return data.model.display_name;
572
+ styleFn = modelStyles.balanced;
573
+ setStyle(style = "balanced") {
574
+ const fn = modelStyles[style];
575
+ if (fn) {
576
+ this.styleFn = fn;
577
+ }
578
+ }
579
+ renderWithData(data, _context) {
580
+ const renderData = {
581
+ displayName: data.model.display_name,
582
+ id: data.model.id
583
+ };
584
+ return this.styleFn(renderData);
585
+ }
586
+ };
587
+
588
+ // src/ui/utils/colors.ts
589
+ var reset = "\x1B[0m";
590
+ var red = "\x1B[31m";
591
+ var gray = "\x1B[90m";
592
+ var lightGray = "\x1B[37m";
593
+ var bold = "\x1B[1m";
594
+
595
+ // src/ui/theme/default-theme.ts
596
+ var DEFAULT_THEME = {
597
+ context: {
598
+ low: gray,
599
+ medium: gray,
600
+ high: gray
601
+ },
602
+ lines: {
603
+ added: gray,
604
+ removed: gray
474
605
  }
475
606
  };
476
607
 
@@ -495,37 +626,45 @@ function formatDuration(ms) {
495
626
  return parts.join(" ");
496
627
  }
497
628
  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);
629
+ return `$${usd.toFixed(2)}`;
514
630
  }
515
631
  function colorize(text, color) {
516
632
  return `${color}${text}${ANSI_COLORS.RESET}`;
517
633
  }
518
634
 
519
- // src/ui/theme/default-theme.ts
520
- var DEFAULT_THEME = {
521
- context: {
522
- low: gray,
523
- medium: gray,
524
- high: gray
635
+ // src/widgets/context/styles.ts
636
+ var contextStyles = {
637
+ balanced: (data) => {
638
+ const bar = progressBar(data.percent, 10);
639
+ return `[${bar}] ${data.percent}%`;
525
640
  },
526
- lines: {
527
- added: gray,
528
- removed: gray
641
+ compact: (data) => {
642
+ return `${data.percent}%`;
643
+ },
644
+ playful: (data) => {
645
+ const bar = progressBar(data.percent, 10);
646
+ return `\u{1F9E0} [${bar}] ${data.percent}%`;
647
+ },
648
+ verbose: (data) => {
649
+ const usedFormatted = data.used.toLocaleString();
650
+ const maxFormatted = data.contextWindowSize.toLocaleString();
651
+ return `${usedFormatted} / ${maxFormatted} tokens (${data.percent}%)`;
652
+ },
653
+ symbolic: (data) => {
654
+ const filled = Math.round(data.percent / 100 * 5);
655
+ const empty = 5 - filled;
656
+ return `${"\u25AE".repeat(filled)}${"\u25AF".repeat(empty)} ${data.percent}%`;
657
+ },
658
+ "compact-verbose": (data) => {
659
+ const usedK = data.used >= 1e3 ? `${Math.floor(data.used / 1e3)}K` : data.used.toString();
660
+ const maxK = data.contextWindowSize >= 1e3 ? `${Math.floor(data.contextWindowSize / 1e3)}K` : data.contextWindowSize.toString();
661
+ return `${data.percent}% (${usedK}/${maxK})`;
662
+ },
663
+ indicator: (data) => {
664
+ return `\u25CF ${data.percent}%`;
665
+ },
666
+ fancy: (data) => {
667
+ return `\u27E8${data.percent}%\u27E9`;
529
668
  }
530
669
  };
531
670
 
@@ -541,18 +680,30 @@ var ContextWidget = class extends StdinDataWidget {
541
680
  // First line
542
681
  );
543
682
  colors;
683
+ styleFn = contextStyles.balanced;
544
684
  constructor(colors) {
545
685
  super();
546
686
  this.colors = colors ?? DEFAULT_THEME.context;
547
687
  }
548
- renderWithData(data, context) {
688
+ setStyle(style = "balanced") {
689
+ const fn = contextStyles[style];
690
+ if (fn) {
691
+ this.styleFn = fn;
692
+ }
693
+ }
694
+ renderWithData(data, _context) {
549
695
  const { current_usage, context_window_size } = data.context_window;
550
696
  if (!current_usage) return null;
551
697
  const used = current_usage.input_tokens + current_usage.cache_creation_input_tokens + current_usage.cache_read_input_tokens + current_usage.output_tokens;
552
698
  const percent = Math.round(used / context_window_size * 100);
553
- const bar = progressBar(percent, DEFAULTS.PROGRESS_BAR_WIDTH);
699
+ const renderData = {
700
+ used,
701
+ contextWindowSize: context_window_size,
702
+ percent
703
+ };
704
+ const output = this.styleFn(renderData);
554
705
  const color = this.getContextColor(percent);
555
- return colorize(`[${bar}] ${percent}%`, color);
706
+ return colorize(output, color);
556
707
  }
557
708
  getContextColor(percent) {
558
709
  const clampedPercent = Math.max(0, Math.min(100, percent));
@@ -566,6 +717,28 @@ var ContextWidget = class extends StdinDataWidget {
566
717
  }
567
718
  };
568
719
 
720
+ // src/widgets/cost/styles.ts
721
+ var costStyles = {
722
+ balanced: (data) => {
723
+ return formatCostUSD(data.costUsd);
724
+ },
725
+ compact: (data) => {
726
+ return formatCostUSD(data.costUsd);
727
+ },
728
+ playful: (data) => {
729
+ return `\u{1F4B0} ${formatCostUSD(data.costUsd)}`;
730
+ },
731
+ labeled: (data) => {
732
+ return withLabel("Cost", formatCostUSD(data.costUsd));
733
+ },
734
+ indicator: (data) => {
735
+ return withIndicator(formatCostUSD(data.costUsd));
736
+ },
737
+ fancy: (data) => {
738
+ return withFancy(formatCostUSD(data.costUsd));
739
+ }
740
+ };
741
+
569
742
  // src/widgets/cost-widget.ts
570
743
  var CostWidget = class extends StdinDataWidget {
571
744
  id = "cost";
@@ -577,12 +750,71 @@ var CostWidget = class extends StdinDataWidget {
577
750
  0
578
751
  // First line
579
752
  );
580
- renderWithData(data, context) {
753
+ styleFn = costStyles.balanced;
754
+ setStyle(style = "balanced") {
755
+ const fn = costStyles[style];
756
+ if (fn) {
757
+ this.styleFn = fn;
758
+ }
759
+ }
760
+ renderWithData(data, _context) {
581
761
  if (!data.cost || data.cost.total_cost_usd === void 0) return null;
582
- return formatCostUSD(data.cost.total_cost_usd);
762
+ const renderData = {
763
+ costUsd: data.cost.total_cost_usd
764
+ };
765
+ return this.styleFn(renderData);
583
766
  }
584
767
  };
585
768
 
769
+ // src/widgets/lines/styles.ts
770
+ function createLinesStyles(colors) {
771
+ return {
772
+ balanced: (data) => {
773
+ const addedStr = colorize(`+${data.added}`, colors.added);
774
+ const removedStr = colorize(`-${data.removed}`, colors.removed);
775
+ return `${addedStr}/${removedStr}`;
776
+ },
777
+ compact: (data) => {
778
+ const addedStr = colorize(`+${data.added}`, colors.added);
779
+ const removedStr = colorize(`-${data.removed}`, colors.removed);
780
+ return `${addedStr}${removedStr}`;
781
+ },
782
+ playful: (data) => {
783
+ const addedStr = colorize(`\u2795${data.added}`, colors.added);
784
+ const removedStr = colorize(`\u2796${data.removed}`, colors.removed);
785
+ return `${addedStr} ${removedStr}`;
786
+ },
787
+ verbose: (data) => {
788
+ const parts = [];
789
+ if (data.added > 0) {
790
+ parts.push(colorize(`+${data.added} added`, colors.added));
791
+ }
792
+ if (data.removed > 0) {
793
+ parts.push(colorize(`-${data.removed} removed`, colors.removed));
794
+ }
795
+ return parts.join(", ");
796
+ },
797
+ labeled: (data) => {
798
+ const addedStr = colorize(`+${data.added}`, colors.added);
799
+ const removedStr = colorize(`-${data.removed}`, colors.removed);
800
+ const lines = `${addedStr}/${removedStr}`;
801
+ return withLabel("Lines", lines);
802
+ },
803
+ indicator: (data) => {
804
+ const addedStr = colorize(`+${data.added}`, colors.added);
805
+ const removedStr = colorize(`-${data.removed}`, colors.removed);
806
+ const lines = `${addedStr}/${removedStr}`;
807
+ return withIndicator(lines);
808
+ },
809
+ fancy: (data) => {
810
+ const addedStr = colorize(`+${data.added}`, colors.added);
811
+ const removedStr = colorize(`-${data.removed}`, colors.removed);
812
+ const lines = `${addedStr}|${removedStr}`;
813
+ return withAngleBrackets(lines);
814
+ }
815
+ };
816
+ }
817
+
586
818
  // src/widgets/lines-widget.ts
587
819
  var LinesWidget = class extends StdinDataWidget {
588
820
  id = "lines";
@@ -595,16 +827,62 @@ var LinesWidget = class extends StdinDataWidget {
595
827
  // First line
596
828
  );
597
829
  colors;
830
+ linesStyles;
831
+ styleFn;
598
832
  constructor(colors) {
599
833
  super();
600
834
  this.colors = colors ?? DEFAULT_THEME.lines;
835
+ this.linesStyles = createLinesStyles(this.colors);
836
+ this.styleFn = this.linesStyles.balanced;
837
+ }
838
+ setStyle(style = "balanced") {
839
+ const fn = this.linesStyles[style];
840
+ if (fn) {
841
+ this.styleFn = fn;
842
+ }
601
843
  }
602
- renderWithData(data, context) {
844
+ renderWithData(data, _context) {
603
845
  const added = data.cost?.total_lines_added ?? 0;
604
846
  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}`;
847
+ const renderData = { added, removed };
848
+ return this.styleFn(renderData);
849
+ }
850
+ };
851
+
852
+ // src/widgets/duration/styles.ts
853
+ var durationStyles = {
854
+ balanced: (data) => {
855
+ return formatDuration(data.durationMs);
856
+ },
857
+ compact: (data) => {
858
+ const totalSeconds = Math.floor(data.durationMs / 1e3);
859
+ const hours = Math.floor(totalSeconds / 3600);
860
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
861
+ if (hours > 0) {
862
+ return `${hours}h${minutes}m`;
863
+ }
864
+ return `${minutes}m`;
865
+ },
866
+ playful: (data) => {
867
+ const totalSeconds = Math.floor(data.durationMs / 1e3);
868
+ const hours = Math.floor(totalSeconds / 3600);
869
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
870
+ if (hours > 0) {
871
+ return `\u231B ${hours}h ${minutes}m`;
872
+ }
873
+ return `\u231B ${minutes}m`;
874
+ },
875
+ technical: (data) => {
876
+ return `${Math.floor(data.durationMs)}ms`;
877
+ },
878
+ labeled: (data) => {
879
+ return withLabel("Time", formatDuration(data.durationMs));
880
+ },
881
+ indicator: (data) => {
882
+ return withIndicator(formatDuration(data.durationMs));
883
+ },
884
+ fancy: (data) => {
885
+ return withAngleBrackets(formatDuration(data.durationMs));
608
886
  }
609
887
  };
610
888
 
@@ -619,9 +897,80 @@ var DurationWidget = class extends StdinDataWidget {
619
897
  0
620
898
  // First line
621
899
  );
622
- renderWithData(data, context) {
900
+ styleFn = durationStyles.balanced;
901
+ setStyle(style = "balanced") {
902
+ const fn = durationStyles[style];
903
+ if (fn) {
904
+ this.styleFn = fn;
905
+ }
906
+ }
907
+ renderWithData(data, _context) {
623
908
  if (!data.cost || data.cost.total_duration_ms === void 0) return null;
624
- return formatDuration(data.cost.total_duration_ms);
909
+ const renderData = {
910
+ durationMs: data.cost.total_duration_ms
911
+ };
912
+ return this.styleFn(renderData);
913
+ }
914
+ };
915
+
916
+ // src/widgets/git-changes/styles.ts
917
+ var gitChangesStyles = {
918
+ balanced: (data) => {
919
+ const parts = [];
920
+ if (data.insertions > 0) parts.push(`+${data.insertions}`);
921
+ if (data.deletions > 0) parts.push(`-${data.deletions}`);
922
+ return parts.join(" ");
923
+ },
924
+ compact: (data) => {
925
+ const parts = [];
926
+ if (data.insertions > 0) parts.push(`+${data.insertions}`);
927
+ if (data.deletions > 0) parts.push(`-${data.deletions}`);
928
+ return parts.join("/");
929
+ },
930
+ playful: (data) => {
931
+ const parts = [];
932
+ if (data.insertions > 0) parts.push(`\u2B06${data.insertions}`);
933
+ if (data.deletions > 0) parts.push(`\u2B07${data.deletions}`);
934
+ return parts.join(" ");
935
+ },
936
+ verbose: (data) => {
937
+ const parts = [];
938
+ if (data.insertions > 0) parts.push(`+${data.insertions} insertions`);
939
+ if (data.deletions > 0) parts.push(`-${data.deletions} deletions`);
940
+ return parts.join(", ");
941
+ },
942
+ technical: (data) => {
943
+ const parts = [];
944
+ if (data.insertions > 0) parts.push(`${data.insertions}`);
945
+ if (data.deletions > 0) parts.push(`${data.deletions}`);
946
+ return parts.join("/");
947
+ },
948
+ symbolic: (data) => {
949
+ const parts = [];
950
+ if (data.insertions > 0) parts.push(`\u25B2${data.insertions}`);
951
+ if (data.deletions > 0) parts.push(`\u25BC${data.deletions}`);
952
+ return parts.join(" ");
953
+ },
954
+ labeled: (data) => {
955
+ const parts = [];
956
+ if (data.insertions > 0) parts.push(`+${data.insertions}`);
957
+ if (data.deletions > 0) parts.push(`-${data.deletions}`);
958
+ const changes = parts.join(" ");
959
+ return withLabel("Diff", changes);
960
+ },
961
+ indicator: (data) => {
962
+ const parts = [];
963
+ if (data.insertions > 0) parts.push(`+${data.insertions}`);
964
+ if (data.deletions > 0) parts.push(`-${data.deletions}`);
965
+ const changes = parts.join(" ");
966
+ return withIndicator(changes);
967
+ },
968
+ fancy: (data) => {
969
+ const parts = [];
970
+ if (data.insertions > 0) parts.push(`+${data.insertions}`);
971
+ if (data.deletions > 0) parts.push(`-${data.deletions}`);
972
+ const changes = parts.join("|");
973
+ return withAngleBrackets(changes);
625
974
  }
626
975
  };
627
976
 
@@ -640,6 +989,7 @@ var GitChangesWidget = class {
640
989
  git = null;
641
990
  enabled = true;
642
991
  cwd = null;
992
+ styleFn = gitChangesStyles.balanced;
643
993
  /**
644
994
  * @param gitFactory - Optional factory function for creating IGit instances
645
995
  * If not provided, uses default createGit (production)
@@ -648,6 +998,12 @@ var GitChangesWidget = class {
648
998
  constructor(gitFactory) {
649
999
  this.gitFactory = gitFactory || createGit;
650
1000
  }
1001
+ setStyle(style = "balanced") {
1002
+ const fn = gitChangesStyles[style];
1003
+ if (fn) {
1004
+ this.styleFn = fn;
1005
+ }
1006
+ }
651
1007
  async initialize(context) {
652
1008
  this.enabled = context.config?.enabled !== false;
653
1009
  }
@@ -687,10 +1043,8 @@ var GitChangesWidget = class {
687
1043
  if (changes.insertions === 0 && changes.deletions === 0) {
688
1044
  return null;
689
1045
  }
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(",");
1046
+ const renderData = changes;
1047
+ return this.styleFn(renderData);
694
1048
  }
695
1049
  isEnabled() {
696
1050
  return this.enabled;
@@ -846,6 +1200,82 @@ var ConfigProvider = class {
846
1200
  }
847
1201
  };
848
1202
 
1203
+ // src/widgets/config-count/styles.ts
1204
+ var configCountStyles = {
1205
+ balanced: (data) => {
1206
+ const { claudeMdCount, rulesCount, mcpCount, hooksCount } = data;
1207
+ const parts = [];
1208
+ if (claudeMdCount > 0) {
1209
+ parts.push(`CLAUDE.md:${claudeMdCount}`);
1210
+ }
1211
+ if (rulesCount > 0) {
1212
+ parts.push(`rules:${rulesCount}`);
1213
+ }
1214
+ if (mcpCount > 0) {
1215
+ parts.push(`MCPs:${mcpCount}`);
1216
+ }
1217
+ if (hooksCount > 0) {
1218
+ parts.push(`hooks:${hooksCount}`);
1219
+ }
1220
+ return parts.join(" \u2502 ");
1221
+ },
1222
+ compact: (data) => {
1223
+ const { claudeMdCount, rulesCount, mcpCount, hooksCount } = data;
1224
+ const parts = [];
1225
+ if (claudeMdCount > 0) {
1226
+ parts.push(`${claudeMdCount} docs`);
1227
+ }
1228
+ if (rulesCount > 0) {
1229
+ parts.push(`${rulesCount} rules`);
1230
+ }
1231
+ if (mcpCount > 0) {
1232
+ parts.push(`${mcpCount} MCPs`);
1233
+ }
1234
+ if (hooksCount > 0) {
1235
+ const hookLabel = hooksCount === 1 ? "hook" : "hooks";
1236
+ parts.push(`${hooksCount} ${hookLabel}`);
1237
+ }
1238
+ return parts.join(" \u2502 ");
1239
+ },
1240
+ playful: (data) => {
1241
+ const { claudeMdCount, rulesCount, mcpCount, hooksCount } = data;
1242
+ const parts = [];
1243
+ if (claudeMdCount > 0) {
1244
+ parts.push(`\u{1F4C4} CLAUDE.md:${claudeMdCount}`);
1245
+ }
1246
+ if (rulesCount > 0) {
1247
+ parts.push(`\u{1F4DC} rules:${rulesCount}`);
1248
+ }
1249
+ if (mcpCount > 0) {
1250
+ parts.push(`\u{1F50C} MCPs:${mcpCount}`);
1251
+ }
1252
+ if (hooksCount > 0) {
1253
+ parts.push(`\u{1FA9D} hooks:${hooksCount}`);
1254
+ }
1255
+ return parts.join(" \u2502 ");
1256
+ },
1257
+ verbose: (data) => {
1258
+ const { claudeMdCount, rulesCount, mcpCount, hooksCount } = data;
1259
+ const parts = [];
1260
+ if (claudeMdCount > 0) {
1261
+ parts.push(`${claudeMdCount} CLAUDE.md`);
1262
+ }
1263
+ if (rulesCount > 0) {
1264
+ parts.push(`${rulesCount} rules`);
1265
+ }
1266
+ if (mcpCount > 0) {
1267
+ parts.push(`${mcpCount} MCP servers`);
1268
+ }
1269
+ if (hooksCount > 0) {
1270
+ parts.push(`${hooksCount} hook`);
1271
+ }
1272
+ return parts.join(" \u2502 ");
1273
+ }
1274
+ };
1275
+
1276
+ // src/core/style-types.ts
1277
+ var DEFAULT_WIDGET_STYLE = "balanced";
1278
+
849
1279
  // src/widgets/config-count-widget.ts
850
1280
  var ConfigCountWidget = class {
851
1281
  id = "config-count";
@@ -860,6 +1290,13 @@ var ConfigCountWidget = class {
860
1290
  configProvider = new ConfigProvider();
861
1291
  configs;
862
1292
  cwd;
1293
+ styleFn = configCountStyles.balanced;
1294
+ setStyle(style = DEFAULT_WIDGET_STYLE) {
1295
+ const fn = configCountStyles[style];
1296
+ if (fn) {
1297
+ this.styleFn = fn;
1298
+ }
1299
+ }
863
1300
  async initialize() {
864
1301
  }
865
1302
  async update(data) {
@@ -878,20 +1315,13 @@ var ConfigCountWidget = class {
878
1315
  return null;
879
1316
  }
880
1317
  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;
1318
+ const renderData = {
1319
+ claudeMdCount,
1320
+ rulesCount,
1321
+ mcpCount,
1322
+ hooksCount
1323
+ };
1324
+ return this.styleFn(renderData);
895
1325
  }
896
1326
  async cleanup() {
897
1327
  }
@@ -942,10 +1372,10 @@ function getRankValue(rank) {
942
1372
  "8": 8,
943
1373
  "9": 9,
944
1374
  "10": 10,
945
- "J": 11,
946
- "Q": 12,
947
- "K": 13,
948
- "A": 14
1375
+ J: 11,
1376
+ Q: 12,
1377
+ K: 13,
1378
+ A: 14
949
1379
  };
950
1380
  return values[rank];
951
1381
  }
@@ -1246,7 +1676,11 @@ function evaluateHand(hole, board) {
1246
1676
  const sfHighCard = getStraightFlushHighCard(allCards, flushSuit);
1247
1677
  if (sfHighCard === 14) {
1248
1678
  const participatingCards = getStraightFlushIndices(allCards, 14, flushSuit);
1249
- return { rank: 10 /* RoyalFlush */, ...HAND_DISPLAY[10 /* RoyalFlush */], participatingCards };
1679
+ return {
1680
+ rank: 10 /* RoyalFlush */,
1681
+ ...HAND_DISPLAY[10 /* RoyalFlush */],
1682
+ participatingCards
1683
+ };
1250
1684
  }
1251
1685
  }
1252
1686
  if (flush) {
@@ -1254,13 +1688,21 @@ function evaluateHand(hole, board) {
1254
1688
  const sfHighCard = getStraightFlushHighCard(allCards, flushSuit);
1255
1689
  if (sfHighCard !== null) {
1256
1690
  const participatingCards = getStraightFlushIndices(allCards, sfHighCard, flushSuit);
1257
- return { rank: 9 /* StraightFlush */, ...HAND_DISPLAY[9 /* StraightFlush */], participatingCards };
1691
+ return {
1692
+ rank: 9 /* StraightFlush */,
1693
+ ...HAND_DISPLAY[9 /* StraightFlush */],
1694
+ participatingCards
1695
+ };
1258
1696
  }
1259
1697
  }
1260
1698
  if (maxCount === 4) {
1261
1699
  const rank = getMostCommonRank(allCards);
1262
1700
  const participatingCards = findCardsOfRank(allCards, rank);
1263
- return { rank: 8 /* FourOfAKind */, ...HAND_DISPLAY[8 /* FourOfAKind */], participatingCards };
1701
+ return {
1702
+ rank: 8 /* FourOfAKind */,
1703
+ ...HAND_DISPLAY[8 /* FourOfAKind */],
1704
+ participatingCards
1705
+ };
1264
1706
  }
1265
1707
  if (maxCount === 3 && pairCount >= 1) {
1266
1708
  const participatingCards = getFullHouseIndices(allCards);
@@ -1279,7 +1721,11 @@ function evaluateHand(hole, board) {
1279
1721
  if (maxCount === 3) {
1280
1722
  const rank = getMostCommonRank(allCards);
1281
1723
  const participatingCards = findCardsOfRank(allCards, rank);
1282
- return { rank: 4 /* ThreeOfAKind */, ...HAND_DISPLAY[4 /* ThreeOfAKind */], participatingCards };
1724
+ return {
1725
+ rank: 4 /* ThreeOfAKind */,
1726
+ ...HAND_DISPLAY[4 /* ThreeOfAKind */],
1727
+ participatingCards
1728
+ };
1283
1729
  }
1284
1730
  if (pairCount >= 2) {
1285
1731
  const [rank1, rank2] = getTwoPairRanks(allCards);
@@ -1294,9 +1740,101 @@ function evaluateHand(hole, board) {
1294
1740
  return { rank: 2 /* OnePair */, ...HAND_DISPLAY[2 /* OnePair */], participatingCards };
1295
1741
  }
1296
1742
  const highestIdx = getHighestCardIndex(allCards);
1297
- return { rank: 1 /* HighCard */, ...HAND_DISPLAY[1 /* HighCard */], participatingCards: [highestIdx] };
1743
+ return {
1744
+ rank: 1 /* HighCard */,
1745
+ ...HAND_DISPLAY[1 /* HighCard */],
1746
+ participatingCards: [highestIdx]
1747
+ };
1298
1748
  }
1299
1749
 
1750
+ // src/widgets/poker/styles.ts
1751
+ var HAND_ABBREVIATIONS = {
1752
+ "Royal Flush": "RF",
1753
+ "Straight Flush": "SF",
1754
+ "Four of a Kind": "4K",
1755
+ "Full House": "FH",
1756
+ "Flush": "FL",
1757
+ "Straight": "ST",
1758
+ "Three of a Kind": "3K",
1759
+ "Two Pair": "2P",
1760
+ "One Pair": "1P",
1761
+ "High Card": "HC",
1762
+ "Nothing": "\u2014"
1763
+ };
1764
+ function formatCardByParticipation(cardData, isParticipating) {
1765
+ const color = isRedSuit(cardData.card.suit) ? red : gray;
1766
+ const cardText = formatCard(cardData.card);
1767
+ if (isParticipating) {
1768
+ return `${color}${bold}(${cardText})${reset} `;
1769
+ } else {
1770
+ return `${color}${cardText}${reset} `;
1771
+ }
1772
+ }
1773
+ function formatCardCompact(cardData, isParticipating) {
1774
+ const color = isRedSuit(cardData.card.suit) ? red : gray;
1775
+ const cardText = formatCardTextCompact(cardData.card);
1776
+ if (isParticipating) {
1777
+ return `${color}${bold}(${cardText})${reset}`;
1778
+ } else {
1779
+ return `${color}${cardText}${reset}`;
1780
+ }
1781
+ }
1782
+ function formatCardTextCompact(card) {
1783
+ const rankSymbols = {
1784
+ "10": "T",
1785
+ "11": "J",
1786
+ "12": "Q",
1787
+ "13": "K",
1788
+ "14": "A"
1789
+ };
1790
+ const rank = String(card.rank);
1791
+ const rankSymbol = rankSymbols[rank] ?? rank;
1792
+ return `${rankSymbol}${card.suit}`;
1793
+ }
1794
+ function formatHandResult(handResult) {
1795
+ if (!handResult) {
1796
+ return "\u2014";
1797
+ }
1798
+ const playerParticipates = handResult.participatingIndices.some((idx) => idx < 2);
1799
+ if (!playerParticipates) {
1800
+ return `Nothing \u{1F0CF}`;
1801
+ } else {
1802
+ return `${handResult.name}! ${handResult.emoji}`;
1803
+ }
1804
+ }
1805
+ function getHandAbbreviation(handResult) {
1806
+ if (!handResult) {
1807
+ return "\u2014 (\u2014)";
1808
+ }
1809
+ const abbreviation = HAND_ABBREVIATIONS[handResult.name] ?? "\u2014";
1810
+ return `${abbreviation} (${handResult.name})`;
1811
+ }
1812
+ var pokerStyles = {
1813
+ balanced: (data) => {
1814
+ const { holeCards, boardCards, handResult } = data;
1815
+ const participatingSet = new Set(handResult?.participatingIndices || []);
1816
+ const handStr = holeCards.map((hc, idx) => formatCardByParticipation(hc, participatingSet.has(idx))).join("");
1817
+ const boardStr = boardCards.map((bc, idx) => formatCardByParticipation(bc, participatingSet.has(idx + 2))).join("");
1818
+ const handLabel = colorize("Hand:", lightGray);
1819
+ const boardLabel = colorize("Board:", lightGray);
1820
+ return `${handLabel} ${handStr}| ${boardLabel} ${boardStr}\u2192 ${formatHandResult(handResult)}`;
1821
+ },
1822
+ compact: (data) => {
1823
+ return pokerStyles.balanced(data);
1824
+ },
1825
+ playful: (data) => {
1826
+ return pokerStyles.balanced(data);
1827
+ },
1828
+ "compact-verbose": (data) => {
1829
+ const { holeCards, boardCards, handResult } = data;
1830
+ const participatingSet = new Set(handResult?.participatingIndices || []);
1831
+ const handStr = holeCards.map((hc, idx) => formatCardCompact(hc, participatingSet.has(idx))).join("");
1832
+ const boardStr = boardCards.map((bc, idx) => formatCardCompact(bc, participatingSet.has(idx + 2))).join("");
1833
+ const abbreviation = getHandAbbreviation(handResult);
1834
+ return `${handStr}| ${boardStr}\u2192 ${abbreviation}`;
1835
+ }
1836
+ };
1837
+
1300
1838
  // src/widgets/poker-widget.ts
1301
1839
  var PokerWidget = class extends StdinDataWidget {
1302
1840
  id = "poker";
@@ -1314,8 +1852,12 @@ var PokerWidget = class extends StdinDataWidget {
1314
1852
  lastUpdateTimestamp = 0;
1315
1853
  THROTTLE_MS = 5e3;
1316
1854
  // 5 seconds
1317
- constructor() {
1318
- super();
1855
+ styleFn = pokerStyles.balanced;
1856
+ setStyle(style = DEFAULT_WIDGET_STYLE) {
1857
+ const fn = pokerStyles[style];
1858
+ if (fn) {
1859
+ this.styleFn = fn;
1860
+ }
1319
1861
  }
1320
1862
  /**
1321
1863
  * Generate new poker hand on each update
@@ -1356,30 +1898,37 @@ var PokerWidget = class extends StdinDataWidget {
1356
1898
  * Format card with appropriate color (red for ♥♦, gray for ♠♣)
1357
1899
  */
1358
1900
  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
- }
1901
+ const color = isRedSuit(card.suit) ? "red" : "gray";
1902
+ return formatCard(card);
1375
1903
  }
1376
1904
  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}`;
1905
+ const holeCardsData = this.holeCards.map((hc, idx) => ({
1906
+ card: hc.card,
1907
+ isParticipating: (this.handResult?.participatingIndices || []).includes(idx)
1908
+ }));
1909
+ const boardCardsData = this.boardCards.map((bc, idx) => ({
1910
+ card: bc.card,
1911
+ isParticipating: (this.handResult?.participatingIndices || []).includes(idx + 2)
1912
+ }));
1913
+ const handResult = this.handResult ? {
1914
+ name: this.getHandName(this.handResult.text),
1915
+ emoji: this.getHandEmoji(this.handResult.text),
1916
+ participatingIndices: this.handResult.participatingIndices
1917
+ } : null;
1918
+ const renderData = {
1919
+ holeCards: holeCardsData,
1920
+ boardCards: boardCardsData,
1921
+ handResult
1922
+ };
1923
+ return this.styleFn(renderData);
1924
+ }
1925
+ getHandName(text) {
1926
+ const match = text.match(/^([^!]+)/);
1927
+ return match ? match[1].trim() : "Nothing";
1928
+ }
1929
+ getHandEmoji(text) {
1930
+ const match = text.match(/([🃏♠️♥️♦️♣️🎉✨🌟])/);
1931
+ return match ? match[1] : "\u{1F0CF}";
1383
1932
  }
1384
1933
  };
1385
1934
 
@@ -1395,11 +1944,17 @@ var EmptyLineWidget = class extends StdinDataWidget {
1395
1944
  // Fourth line (0-indexed)
1396
1945
  );
1397
1946
  /**
1398
- * Return a single space to create a blank separator line.
1399
- * Using a space character instead of empty string ensures the line is visible.
1947
+ * All styles return the same value (Braille Pattern Blank).
1948
+ * This method exists for API consistency with other widgets.
1949
+ */
1950
+ setStyle(_style) {
1951
+ }
1952
+ /**
1953
+ * Return Braille Pattern Blank to create a visible empty separator line.
1954
+ * U+2800 occupies cell width but appears blank, ensuring the line renders.
1400
1955
  */
1401
1956
  renderWithData(_data, _context) {
1402
- return " ";
1957
+ return "\u2800";
1403
1958
  }
1404
1959
  };
1405
1960
 
@@ -1558,9 +2113,7 @@ var StdinProvider = class {
1558
2113
  }
1559
2114
  const result = StdinDataSchema.validate(data);
1560
2115
  if (!result.success) {
1561
- throw new StdinValidationError(
1562
- `Validation failed: ${formatError(result.error)}`
1563
- );
2116
+ throw new StdinValidationError(`Validation failed: ${formatError(result.error)}`);
1564
2117
  }
1565
2118
  return result.data;
1566
2119
  }
@@ -1618,10 +2171,10 @@ async function main() {
1618
2171
  for (const widget of registry.getAll()) {
1619
2172
  await widget.update(stdinData);
1620
2173
  }
1621
- const lines = await renderer.render(
1622
- registry.getEnabledWidgets(),
1623
- { width: 80, timestamp: Date.now() }
1624
- );
2174
+ const lines = await renderer.render(registry.getEnabledWidgets(), {
2175
+ width: 80,
2176
+ timestamp: Date.now()
2177
+ });
1625
2178
  return lines.join("\n");
1626
2179
  } catch (error) {
1627
2180
  const fallback = await tryGitFallback();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-scope",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
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",