cc-pulse 1.3.2 → 1.4.0

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 (3) hide show
  1. package/README.md +82 -39
  2. package/dist/cli.js +191 -100
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,7 +7,7 @@ A real-time statusline for [Claude Code](https://docs.anthropic.com/en/docs/clau
7
7
 
8
8
  ![cc-pulse statusline](assets/demo.png)
9
9
 
10
- ## 🚀 Quick Start
10
+ ## Quick Start
11
11
 
12
12
  ```bash
13
13
  npm install -g cc-pulse
@@ -24,40 +24,79 @@ Add to `~/.claude/settings.json`:
24
24
  }
25
25
  ```
26
26
 
27
- Restart Claude Code — the statusline appears above the input area.
27
+ Restart Claude Code — the statusline appears below the input area.
28
28
 
29
- ## Features
29
+ ## Features
30
30
 
31
31
  | Feature | Description |
32
32
  |---------|-------------|
33
33
  | **Context usage** | Shows % used with color-coded bar — green to red as you approach limits |
34
- | **Token breakdown** | Input ↓, output ↑, and cache tokens at a glance |
35
- | **Model info** | Shows model with version (e.g., "Opus 4.6", "Sonnet 4.5") |
34
+ | **Token breakdown** | Input, output, and cache tokens at a glance |
35
+ | **Model info** | Shows model with version (e.g., "Opus 4.6", "Sonnet 4") |
36
36
  | **Cost tracking** | Session cost with color coding ($1 yellow, $2 orange, $5+ red) |
37
37
  | **MCP health** | Live connection status for all MCP servers |
38
38
  | **Hook monitoring** | Active hooks by event type, with broken path detection |
39
- | **Skills display** | Shows your custom slash commands/skills |
39
+ | **Skills display** | Adaptive display individual names, prefix grouping, or compact counts |
40
40
  | **Git status** | Branch name + new/modified/deleted file counts |
41
+ | **Responsive layout** | Width-aware wrapping + compact mode for smaller screens |
41
42
 
42
- ## 📊 What You Get
43
+ ## What You Get
43
44
 
44
45
  Six lines of information, updated on every message:
45
46
 
46
- ![cc-pulse statusline](assets/demo.png)
47
-
48
47
  | Line | Content |
49
48
  |------|---------|
50
49
  | **Identity** | Project name + working directory |
51
50
  | **Git** | Branch + file changes (new, modified, deleted) |
52
- | **Engine** | Tier, model, context used, tokens, cost, duration |
51
+ | **Engine** | Model, context used, tokens, cost, duration |
53
52
  | **MCP** | Server count + individual status (✓ connected, ✗ disconnected, ○ disabled) |
54
53
  | **Hooks** | Hook count by event type, with broken path warnings |
55
- | **Skills** | Custom slash commands count + names |
54
+ | **Skills** | Adaptive: names when few, prefix groups when many |
55
+
56
+ ## Responsive Display
57
+
58
+ The statusline adapts to your setup automatically:
56
59
 
57
- ## ⚙️ Configuration
60
+ **Skills** adapts based on count:
61
+ - **10 or fewer**: lists all names — `✦ Skills 5 beads,excalidraw,mermaid,tmux,repomix`
62
+ - **More than 10 with shared prefixes**: groups them — `✦ Skills 89 bmad:77 beads,excalidraw,mermaid,...`
63
+ - **More than 10, no groups**: caps at 10 names with overflow — `✦ Skills 15 a,b,c,d,e,f,g,h,i,j +5`
64
+
65
+ **Hooks** — adapts based on total count:
66
+ - **6 or fewer**: shows all names per event — `⚡Hooks 4 Submit:2 lint,format Post:2 test,deploy`
67
+ - **More than 6**: caps names to 3 per group — `⚡Hooks 12 Submit:5 lint,format,check +2`
68
+
69
+ **Width-aware wrapping** — when a line exceeds terminal width, components wrap onto indented continuation lines instead of being cut off.
70
+
71
+ **Compact mode** — toggle with `/pulse-compact` or set in config:
72
+ ```json
73
+ {
74
+ "compact": true
75
+ }
76
+ ```
77
+
78
+ In compact mode, everything collapses to counts only:
79
+ - `⬢ MCP 3/4` | `⚡Hooks 8` | `✦ Skills 89` | `Used 30%` | `$2.50`
80
+
81
+ ## Configuration
58
82
 
59
83
  Create `~/.config/claude-pulse/config.json` to customize. Only include what you want to change.
60
84
 
85
+ <details>
86
+ <summary><strong>Compact Mode</strong></summary>
87
+
88
+ Minimal display showing only counts and essential info. Toggle with the `/pulse-compact` slash command, or set manually:
89
+
90
+ ```json
91
+ {
92
+ "compact": true
93
+ }
94
+ ```
95
+
96
+ When enabled: skills, hooks, and MCP show counts only; context hides token breakdown; cost hides burn rate; CWD shortens.
97
+
98
+ </details>
99
+
61
100
  <details>
62
101
  <summary><strong>Context Window</strong></summary>
63
102
 
@@ -86,26 +125,7 @@ Shows how much of the context window is used. Colors shift as usage increases.
86
125
  - **Green**: < 70% used (safe)
87
126
  - **Yellow**: 70% used (warn)
88
127
  - **Orange**: 85% used (critical)
89
- - **Red + 🔴**: 95% used (danger)
90
-
91
- </details>
92
-
93
- <details>
94
- <summary><strong>Subscription Tier</strong></summary>
95
-
96
- Set your plan manually (auto-detection isn't reliable):
97
-
98
- ```json
99
- {
100
- "components": {
101
- "tier": {
102
- "override": "max"
103
- }
104
- }
105
- }
106
- ```
107
-
108
- Options: `"pro"`, `"max"`, `"api"`
128
+ - **Red**: 95% used (danger)
109
129
 
110
130
  </details>
111
131
 
@@ -159,6 +179,8 @@ Options: `"pro"`, `"max"`, `"api"`
159
179
  | `showNames: false` | `⚡Hooks 8 Submit:3 Post:2 End:1` |
160
180
  | Both `false` | `⚡Hooks 8` |
161
181
 
182
+ When you have many hooks (>6), names are automatically capped to 3 per event group with a `+N` overflow indicator.
183
+
162
184
  Broken hooks (invalid paths) show in red with ▲.
163
185
 
164
186
  </details>
@@ -198,16 +220,27 @@ Shows your custom slash commands from `~/.claude/skills/` and `.claude/skills/`.
198
220
  }
199
221
  ```
200
222
 
201
- | Setting | Result |
202
- |---------|--------|
203
- | Both `true` | `✦ Skills 5 commit,pr,branch` |
204
- | `showNames: false` | `✦ Skills 5` |
205
- | `maxDisplay: 3` | Shows first 3 names + overflow count |
223
+ The display adapts automatically based on how many skills you have — see [Responsive Display](#responsive-display) above.
206
224
 
207
225
  Broken skills (missing SKILL.md or invalid frontmatter) show in red with ▲.
208
226
 
209
227
  </details>
210
228
 
229
+ <details>
230
+ <summary><strong>Dividers</strong></summary>
231
+
232
+ Add horizontal line separators between status sections:
233
+
234
+ ```json
235
+ {
236
+ "dividers": true
237
+ }
238
+ ```
239
+
240
+ Off by default.
241
+
242
+ </details>
243
+
211
244
  <details>
212
245
  <summary><strong>Layout</strong></summary>
213
246
 
@@ -238,7 +271,7 @@ The 6-line structure is fixed. You can toggle lines and change separators:
238
271
 
239
272
  | Component | Key Options |
240
273
  |-----------|-------------|
241
- | `model` | `showIcon: true` adds emoji per model |
274
+ | `model` | `showIcon: true` (default), custom `icons: { opus, sonnet, haiku }` |
242
275
  | `session` | `showDuration: true`, `showId: false` |
243
276
  | `cache` | Shows cache hit rate |
244
277
  | `linesChanged` | Shows `+added -removed` |
@@ -248,7 +281,17 @@ All components accept `"enabled": false` to hide them.
248
281
 
249
282
  </details>
250
283
 
251
- ## 🛠️ Development
284
+ ## Slash Commands
285
+
286
+ cc-pulse ships with a skill you can install to your Claude Code skills directory:
287
+
288
+ | Command | Description |
289
+ |---------|-------------|
290
+ | `/pulse-compact` | Toggle compact mode on/off |
291
+
292
+ To install, copy `skills/pulse-compact/` to `~/.claude/skills/` or your project's `.claude/skills/`.
293
+
294
+ ## Development
252
295
 
253
296
  ```bash
254
297
  git clone https://github.com/ali-nr/claude-pulse.git
package/dist/cli.js CHANGED
@@ -1,18 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
3
7
  var __export = (target, all) => {
4
8
  for (var name in all)
5
9
  __defProp(target, name, {
6
10
  get: all[name],
7
11
  enumerable: true,
8
12
  configurable: true,
9
- set: (newValue) => all[name] = () => newValue
13
+ set: __exportSetter.bind(all, name)
10
14
  });
11
15
  };
12
16
  // package.json
13
17
  var package_default = {
14
18
  name: "cc-pulse",
15
- version: "1.3.2",
19
+ version: "1.4.0",
16
20
  description: "A customizable, real-time statusline for Claude Code",
17
21
  type: "module",
18
22
  bin: {
@@ -451,15 +455,27 @@ function renderHooks(config, theme) {
451
455
  const text2 = `${theme.yellow}⚡${hookLabel} ${summary.total}${theme.reset}`;
452
456
  return { text: text2 };
453
457
  }
454
- const eventParts = Object.entries(summary.events).map(([event, detail]) => {
458
+ const header = `${theme.yellow}⚡${hookLabel} ${summary.total}${theme.reset}`;
459
+ const MAX_NAMES_PER_GROUP = 3;
460
+ const compact = summary.total > 6;
461
+ const items = Object.entries(summary.events).map(([event, detail]) => {
455
462
  const label = EVENT_LABELS[event] ?? event;
456
- const goodNames = showNames && detail.names.length > 0 ? ` ${theme.flamingo}${detail.names.join(",")}${theme.reset}` : "";
457
- const brokenNames = detail.broken.length > 0 ? ` ${theme.red}${detail.broken.join(",")} ▲${theme.reset}` : "";
463
+ const brokenStr = detail.broken.length > 0 ? ` ${theme.red}${detail.broken.join(",")} ▲${theme.reset}` : "";
458
464
  const countStr = showCount ? `${theme.peach}${detail.count}${theme.reset}` : "";
459
- return `${theme.lavender}${label}:${theme.reset}${countStr}${goodNames}${brokenNames}`;
465
+ let goodNames = "";
466
+ if (showNames && detail.names.length > 0) {
467
+ const cap = compact ? MAX_NAMES_PER_GROUP : detail.names.length;
468
+ const displayed = detail.names.slice(0, cap);
469
+ const overflow = detail.names.length - cap;
470
+ goodNames = ` ${theme.flamingo}${displayed.join(",")}${theme.reset}`;
471
+ if (overflow > 0) {
472
+ goodNames += ` ${theme.overlay0}+${overflow}${theme.reset}`;
473
+ }
474
+ }
475
+ return `${theme.lavender}${label}:${theme.reset}${countStr}${goodNames}${brokenStr}`;
460
476
  });
461
- const text = `${theme.yellow}⚡${hookLabel} ${summary.total}${theme.reset} ${eventParts.join(" ")}`;
462
- return { text };
477
+ const text = items.length > 0 ? `${header} ${items.join(" ")}` : header;
478
+ return { text, header, items };
463
479
  }
464
480
  function getHooksSummary() {
465
481
  const events = {};
@@ -590,7 +606,7 @@ function renderMcp(config, theme) {
590
606
  }
591
607
  const displayServers = servers.slice(0, maxDisplay);
592
608
  const remaining = servers.length - maxDisplay;
593
- const serverParts = displayServers.map((server) => {
609
+ const serverItems = displayServers.map((server) => {
594
610
  let icon;
595
611
  let color;
596
612
  switch (server.status) {
@@ -612,17 +628,17 @@ function renderMcp(config, theme) {
612
628
  }
613
629
  return `${color}${server.name} ${icon}${theme.reset}`;
614
630
  });
615
- let serverStr = serverParts.join(" ");
616
631
  if (remaining > 0) {
617
- serverStr += ` ${theme.sky}+${remaining} more${theme.reset}`;
632
+ serverItems.push(`${theme.sky}+${remaining} more${theme.reset}`);
618
633
  }
619
634
  const countStr = `${connectedCount}/${servers.length}`;
635
+ const header = `${theme.sky}${label} ${theme.green}${countStr}${theme.reset}:`;
620
636
  if (config.showNames === false) {
621
637
  const text2 = `${theme.sky}${label} ${theme.green}${countStr}${theme.reset}`;
622
638
  return { text: text2, action: "/mcp" };
623
639
  }
624
- const text = `${theme.sky}${label} ${theme.green}${countStr}${theme.reset}: ${serverStr}`;
625
- return { text, action: "/mcp" };
640
+ const text = `${header} ${serverItems.join(" ")}`;
641
+ return { text, header, items: serverItems, action: "/mcp" };
626
642
  }
627
643
  function getMcpServers() {
628
644
  const state = loadState();
@@ -718,7 +734,7 @@ function parseMcpOutput(output) {
718
734
  }
719
735
  // src/components/model.ts
720
736
  function parseModelId(modelId) {
721
- const id = modelId.toLowerCase();
737
+ const id = modelId.toLowerCase().replace(/\[.*\]$/, "");
722
738
  const newFormat = id.match(/^claude-(\w+)-(\d+)-(\d+)-\d+$/);
723
739
  if (newFormat) {
724
740
  const [, family, major, minor] = newFormat;
@@ -729,6 +745,16 @@ function parseModelId(modelId) {
729
745
  const [, family, major] = newFormatNoMinor;
730
746
  return { family, version: major };
731
747
  }
748
+ const shortWithMinor = id.match(/^claude-(\w+)-(\d+)-(\d+)$/);
749
+ if (shortWithMinor) {
750
+ const [, family, major, minor] = shortWithMinor;
751
+ return { family, version: `${major}.${minor}` };
752
+ }
753
+ const shortNoMinor = id.match(/^claude-(\w+)-(\d+)$/);
754
+ if (shortNoMinor) {
755
+ const [, family, major] = shortNoMinor;
756
+ return { family, version: major };
757
+ }
732
758
  const oldFormat = id.match(/^claude-(\d+)-(\d+)-(\w+)-\d+$/);
733
759
  if (oldFormat) {
734
760
  const [, major, minor, family] = oldFormat;
@@ -747,7 +773,7 @@ function renderModel(input, config, theme) {
747
773
  }
748
774
  const modelId = input.model?.id ?? "";
749
775
  const displayName = input.model?.display_name ?? "";
750
- const icons = config.icons ?? { opus: "\uD83E\uDDE0", sonnet: "\uD83C\uDFB5", haiku: "" };
776
+ const icons = config.icons ?? { opus: "", sonnet: "", haiku: "" };
751
777
  const showIcon = config.showIcon !== false;
752
778
  let icon = "\uD83E\uDD16";
753
779
  let color = theme.text;
@@ -855,22 +881,65 @@ function renderSkills(config, theme) {
855
881
  }
856
882
  const validNames = summary.skills.filter((s) => s.valid).map((s) => s.name ?? s.folder).slice(0, maxDisplay);
857
883
  const brokenNames = summary.skills.filter((s) => !s.valid).map((s) => s.folder);
858
- const parts = [];
859
- if (showCount) {
860
- parts.push(`${theme.mauve}${summary.valid}${theme.reset}`);
861
- }
884
+ const countStr = showCount ? ` ${theme.mauve}${summary.valid}${theme.reset}` : "";
885
+ const header = `${theme.mauve}✦ ${label}${theme.reset}${countStr}`;
886
+ const items = [];
887
+ const GROUP_THRESHOLD = 10;
888
+ const MIN_PREFIX_COUNT = 3;
862
889
  if (showNames && validNames.length > 0) {
863
- const displayNames = validNames.join(",");
864
- const overflow = summary.valid > maxDisplay ? `+${summary.valid - maxDisplay}` : "";
865
- parts.push(`${theme.flamingo}${displayNames}${overflow ? ` ${theme.overlay0}${overflow}` : ""}${theme.reset}`);
890
+ if (validNames.length > GROUP_THRESHOLD) {
891
+ const { groups, ungrouped } = groupByPrefix(validNames, MIN_PREFIX_COUNT);
892
+ const hasGroups = Object.keys(groups).length > 0;
893
+ if (hasGroups) {
894
+ for (const [prefix, names] of Object.entries(groups).sort((a, b) => b[1].length - a[1].length)) {
895
+ items.push(`${theme.overlay1}${prefix}:${theme.peach}${names.length}${theme.reset}`);
896
+ }
897
+ if (ungrouped.length > 0) {
898
+ items.push(`${theme.flamingo}${ungrouped.join(",")}${theme.reset}`);
899
+ }
900
+ } else {
901
+ const CAP = 10;
902
+ const capped = validNames.slice(0, CAP);
903
+ items.push(`${theme.flamingo}${capped.join(",")}${theme.reset}`);
904
+ if (validNames.length > CAP) {
905
+ items.push(`${theme.overlay0}+${validNames.length - CAP}${theme.reset}`);
906
+ }
907
+ }
908
+ } else {
909
+ items.push(`${theme.flamingo}${validNames.join(",")}${theme.reset}`);
910
+ }
911
+ const overflow = summary.valid > maxDisplay ? summary.valid - maxDisplay : 0;
912
+ if (overflow > 0) {
913
+ items.push(`${theme.overlay0}+${overflow}${theme.reset}`);
914
+ }
866
915
  }
867
- let brokenStr = "";
868
916
  if (summary.broken > 0) {
869
- const brokenDisplay = brokenNames.length <= 2 ? brokenNames.join(",") : `${summary.broken} broken`;
870
- brokenStr = ` ${theme.red}▲${brokenDisplay}${theme.reset}`;
917
+ for (const name of brokenNames) {
918
+ items.push(`${theme.red}▲${name}${theme.reset}`);
919
+ }
920
+ }
921
+ const text = items.length > 0 ? `${header} ${items.join(" ")}` : header;
922
+ return { text, header, items };
923
+ }
924
+ function groupByPrefix(names, minCount) {
925
+ const buckets = {};
926
+ for (const name of names) {
927
+ const sep = name.includes(":") ? ":" : "-";
928
+ const prefix = name.split(sep)[0];
929
+ if (!buckets[prefix])
930
+ buckets[prefix] = [];
931
+ buckets[prefix].push(name);
932
+ }
933
+ const groups = {};
934
+ const ungrouped = [];
935
+ for (const [prefix, members] of Object.entries(buckets)) {
936
+ if (members.length >= minCount) {
937
+ groups[prefix] = members;
938
+ } else {
939
+ ungrouped.push(...members);
940
+ }
871
941
  }
872
- const text = `${theme.mauve}✦ ${label}${theme.reset}${parts.length ? ` ${parts.join(" ")}` : ""}${brokenStr}`;
873
- return { text };
942
+ return { groups, ungrouped };
874
943
  }
875
944
  function getSkillsSummary() {
876
945
  const skills = [];
@@ -999,58 +1068,6 @@ function renderSystem(input, config, theme) {
999
1068
  const text = parts.join(" │ ");
1000
1069
  return { text };
1001
1070
  }
1002
- // src/components/tier.ts
1003
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1004
- import { homedir as homedir6 } from "node:os";
1005
- import { join as join5 } from "node:path";
1006
- var cachedTier = null;
1007
- var cacheTime = 0;
1008
- var CACHE_TTL3 = 30000;
1009
- function renderTier(config, theme) {
1010
- if (config.enabled === false) {
1011
- return { text: "" };
1012
- }
1013
- const labels = config.labels ?? { pro: "PRO", max: "MAX", api: "API" };
1014
- const tier = config.override ?? detectTier();
1015
- let color = theme.blue;
1016
- let label = labels.pro;
1017
- if (tier === "max") {
1018
- color = theme.mauve;
1019
- label = labels.max;
1020
- } else if (tier === "api") {
1021
- color = theme.green;
1022
- label = labels.api;
1023
- }
1024
- const icon = "◆ ";
1025
- const text = `${color}${theme.bold}${icon}${label}${theme.reset}`;
1026
- return { text, action: "/usage" };
1027
- }
1028
- function detectTier() {
1029
- const now = Date.now();
1030
- if (cachedTier && now - cacheTime < CACHE_TTL3) {
1031
- return cachedTier;
1032
- }
1033
- const claudeJsonPath = join5(homedir6(), ".claude.json");
1034
- try {
1035
- if (existsSync5(claudeJsonPath)) {
1036
- const content = readFileSync5(claudeJsonPath, "utf-8");
1037
- const data = JSON.parse(content);
1038
- if (data.oauthAccount?.hasExtraUsageEnabled) {
1039
- cachedTier = "max";
1040
- } else if (data.oauthAccount) {
1041
- cachedTier = "pro";
1042
- } else {
1043
- cachedTier = "api";
1044
- }
1045
- } else {
1046
- cachedTier = "api";
1047
- }
1048
- } catch {
1049
- cachedTier = "pro";
1050
- }
1051
- cacheTime = now;
1052
- return cachedTier;
1053
- }
1054
1071
  // src/components/time.ts
1055
1072
  function renderTime(config, theme) {
1056
1073
  if (config.enabled === false) {
@@ -1084,12 +1101,12 @@ function renderTime(config, theme) {
1084
1101
  return { text };
1085
1102
  }
1086
1103
  // src/config.ts
1087
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
1088
- import { homedir as homedir7 } from "node:os";
1089
- import { join as join6 } from "node:path";
1104
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1105
+ import { homedir as homedir6 } from "node:os";
1106
+ import { join as join5 } from "node:path";
1090
1107
  var CONFIG_PATHS = [
1091
- join6(homedir7(), ".config", "claude-pulse", "config.json"),
1092
- join6(homedir7(), ".claude-pulse.json")
1108
+ join5(homedir6(), ".config", "claude-pulse", "config.json"),
1109
+ join5(homedir6(), ".claude-pulse.json")
1093
1110
  ];
1094
1111
  var FIXED_LINES = [
1095
1112
  { name: "identity", enabled: true, components: ["name", "cwd"], separator: " " },
@@ -1097,7 +1114,7 @@ var FIXED_LINES = [
1097
1114
  {
1098
1115
  name: "engine",
1099
1116
  enabled: true,
1100
- components: ["tier", "model", "context", "cost", "session"],
1117
+ components: ["model", "context", "cost", "session"],
1101
1118
  separator: " │ "
1102
1119
  },
1103
1120
  { name: "mcp", enabled: true, components: ["mcp"], separator: " │ " },
@@ -1107,13 +1124,9 @@ var FIXED_LINES = [
1107
1124
  var DEFAULT_CONFIG = {
1108
1125
  theme: "catppuccin",
1109
1126
  components: {
1110
- tier: {
1111
- enabled: true,
1112
- labels: { pro: "PRO", max: "MAX", api: "API" }
1113
- },
1114
1127
  model: {
1115
1128
  enabled: true,
1116
- showIcon: false
1129
+ showIcon: true
1117
1130
  },
1118
1131
  context: {
1119
1132
  enabled: true,
@@ -1182,9 +1195,9 @@ var DEFAULT_CONFIG = {
1182
1195
  };
1183
1196
  function loadConfig() {
1184
1197
  for (const configPath of CONFIG_PATHS) {
1185
- if (existsSync6(configPath)) {
1198
+ if (existsSync5(configPath)) {
1186
1199
  try {
1187
- const content = readFileSync6(configPath, "utf-8");
1200
+ const content = readFileSync5(configPath, "utf-8");
1188
1201
  const userConfig = JSON.parse(content);
1189
1202
  return mergeConfig(DEFAULT_CONFIG, userConfig);
1190
1203
  } catch {}
@@ -1211,6 +1224,10 @@ function mergeConfig(target, source) {
1211
1224
  const result = { ...target };
1212
1225
  if (source.theme !== undefined)
1213
1226
  result.theme = source.theme;
1227
+ if (source.compact !== undefined)
1228
+ result.compact = source.compact;
1229
+ if (source.dividers !== undefined)
1230
+ result.dividers = source.dividers;
1214
1231
  if (source.lines !== undefined)
1215
1232
  result.lines = source.lines;
1216
1233
  if (source.interactive !== undefined)
@@ -14805,7 +14822,9 @@ var McpServerSchema = exports_external.object({
14805
14822
  });
14806
14823
  var ComponentOutputSchema = exports_external.object({
14807
14824
  text: exports_external.string(),
14808
- action: exports_external.string().optional()
14825
+ action: exports_external.string().optional(),
14826
+ header: exports_external.string().optional(),
14827
+ items: exports_external.array(exports_external.string()).optional()
14809
14828
  });
14810
14829
  var TierConfigSchema = exports_external.object({
14811
14830
  enabled: exports_external.boolean().optional(),
@@ -14952,6 +14971,8 @@ var LinesConfigSchema = exports_external.object({
14952
14971
  });
14953
14972
  var PulseConfigSchema = exports_external.object({
14954
14973
  theme: exports_external.string(),
14974
+ compact: exports_external.boolean().optional(),
14975
+ dividers: exports_external.boolean().optional(),
14955
14976
  lines: LinesConfigSchema.optional(),
14956
14977
  components: ComponentConfigsSchema,
14957
14978
  interactive: exports_external.object({
@@ -15004,6 +15025,39 @@ var catppuccin = {
15004
15025
  dim: "\x1B[2m"
15005
15026
  };
15006
15027
 
15028
+ // src/truncate.ts
15029
+ var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
15030
+ function visibleLength(str) {
15031
+ return str.replace(ANSI_RE, "").length;
15032
+ }
15033
+ function wrapParts(parts, separator, maxWidth, indent = 2) {
15034
+ if (parts.length === 0)
15035
+ return "";
15036
+ const sepWidth = visibleLength(separator);
15037
+ const pad = " ".repeat(indent);
15038
+ const lines = [];
15039
+ let currentLine = parts[0];
15040
+ let currentWidth = visibleLength(parts[0]);
15041
+ for (let i = 1;i < parts.length; i++) {
15042
+ const partWidth = visibleLength(parts[i]);
15043
+ const wouldBe = currentWidth + sepWidth + partWidth;
15044
+ if (wouldBe > maxWidth) {
15045
+ lines.push(currentLine);
15046
+ currentLine = pad + parts[i];
15047
+ currentWidth = indent + partWidth;
15048
+ } else {
15049
+ currentLine += separator + parts[i];
15050
+ currentWidth = wouldBe;
15051
+ }
15052
+ }
15053
+ lines.push(currentLine);
15054
+ return lines.join(`
15055
+ `);
15056
+ }
15057
+ function getTerminalWidth() {
15058
+ return process.stdout.columns || Number(process.env.COLUMNS) || 80;
15059
+ }
15060
+
15007
15061
  // src/cli.ts
15008
15062
  var VERSION = package_default.version ?? "1.0.0";
15009
15063
  async function main() {
@@ -15042,30 +15096,67 @@ Configuration:
15042
15096
  const config2 = loadConfig();
15043
15097
  const theme = catppuccin;
15044
15098
  const lines = getLines(config2);
15099
+ const termWidth = getTerminalWidth();
15100
+ if (config2.compact) {
15101
+ if (config2.components.skills) {
15102
+ config2.components.skills.showNames = false;
15103
+ }
15104
+ if (config2.components.hooks) {
15105
+ config2.components.hooks.showNames = false;
15106
+ config2.components.hooks.showCount = false;
15107
+ }
15108
+ if (config2.components.mcp) {
15109
+ config2.components.mcp.showNames = false;
15110
+ }
15111
+ if (config2.components.context) {
15112
+ config2.components.context.showTokens = false;
15113
+ config2.components.context.showRate = false;
15114
+ }
15115
+ if (config2.components.cost) {
15116
+ config2.components.cost.showBurnRate = false;
15117
+ config2.components.cost.showProjection = false;
15118
+ }
15119
+ if (config2.components.cwd) {
15120
+ config2.components.cwd.maxLength = 15;
15121
+ }
15122
+ }
15045
15123
  const outputLines = [];
15046
15124
  for (const line of lines) {
15047
15125
  if (!line.enabled)
15048
15126
  continue;
15049
- const parts = [];
15050
15127
  const separator = ` ${theme.overlay2}│${theme.reset} `;
15051
15128
  const sep = line.separator ?? separator;
15129
+ const outputs = [];
15052
15130
  for (const componentName of line.components) {
15053
15131
  const output = renderComponent(componentName, input, config2, theme);
15054
15132
  if (output.text) {
15055
- parts.push(output.text);
15133
+ outputs.push(output);
15056
15134
  }
15057
15135
  }
15058
- if (parts.length > 0) {
15059
- outputLines.push(parts.join(sep));
15136
+ if (outputs.length === 0)
15137
+ continue;
15138
+ if (outputs.length === 1 && outputs[0].header && outputs[0].items?.length) {
15139
+ const { header, items } = outputs[0];
15140
+ const rendered = wrapParts([header, ...items], " ", termWidth);
15141
+ outputLines.push(rendered);
15142
+ } else {
15143
+ const parts = outputs.map((o) => o.text);
15144
+ outputLines.push(wrapParts(parts, sep, termWidth));
15060
15145
  }
15061
15146
  }
15062
- console.log(outputLines.join(`
15147
+ if (config2.dividers) {
15148
+ const divider = `${theme.overlay0}${"─".repeat(termWidth)}${theme.reset}`;
15149
+ console.log(outputLines.join(`
15150
+ ${divider}
15151
+ `) + `
15152
+ ${divider}`);
15153
+ } else {
15154
+ console.log(outputLines.join(`
15063
15155
  `));
15156
+ }
15064
15157
  }
15065
15158
  function renderComponent(name, input, config2, theme) {
15066
15159
  switch (name) {
15067
- case "tier":
15068
- return renderTier(config2.components.tier ?? {}, theme);
15069
15160
  case "model":
15070
15161
  return renderModel(input, config2.components.model ?? {}, theme);
15071
15162
  case "context":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-pulse",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "A customizable, real-time statusline for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {