@buckits/claude-statusline 2.3.2 β†’ 3.0.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.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  <br>
11
11
 
12
- ![Statusline Preview](assets/statusline-preview.svg)
12
+ ![Statusline Preview](assets/50-bars.png)
13
13
 
14
14
  <br>
15
15
 
@@ -34,7 +34,7 @@ Claude Code's default statusline is... minimal. You deserve better.
34
34
 
35
35
  ### 🎨 Gradient Progress Bar
36
36
 
37
- 50 segments that smoothly transition through colors as your context fills up. The gradient is calculated relative to the auto-compact threshold, not total capacityβ€”so you always know how close you are to summarization.
37
+ Smoothly transitions through colors as your context fills up. Choose your width during installβ€”Compact (25), Medium (38), Full (50), or Custom (any number 10-100). The gradient is calculated relative to the auto-compact threshold, not total capacityβ€”so you always know how close you are to summarization.
38
38
 
39
39
  ### ⚑ Auto-Compact Threshold
40
40
 
@@ -68,8 +68,9 @@ npx @buckits/claude-statusline
68
68
 
69
69
  That's it. The installer will:
70
70
  1. Ask where to install (global or local)
71
- 2. Copy the statusline script
72
- 3. Configure your settings
71
+ 2. Let you choose your progress bar width
72
+ 3. Copy the statusline script
73
+ 4. Configure your settings
73
74
 
74
75
  ### Options
75
76
 
@@ -92,23 +93,17 @@ npx @buckits/claude-statusline --global --uninstall
92
93
 
93
94
  ## What It Looks Like
94
95
 
95
- ### 🟒 Normal Usage (Green Zone)
96
+ ### Compact (25 bars)
96
97
 
97
- ![Green Zone](assets/statusline-green.svg)
98
+ ![Compact](assets/25-bars.png)
98
99
 
99
- Safe and sound. Plenty of context remaining.
100
+ ### Medium (38 bars)
100
101
 
101
- ### 🟑 Getting Busy (Yellow Zone)
102
+ ![Medium](assets/38-bars.png)
102
103
 
103
- ![Yellow Zone](assets/statusline-yellow.svg)
104
+ ### Full (50 bars)
104
105
 
105
- Making progress. Keep an eye on that threshold.
106
-
107
- ### πŸ”΄ Approaching Limit (Red Zone)
108
-
109
- ![Red Zone](assets/statusline-red.svg)
110
-
111
- Getting close to auto-compact. Consider wrapping up or starting fresh.
106
+ ![Full](assets/50-bars.png)
112
107
 
113
108
  ## 🀝 GSD Compatible
114
109
 
@@ -125,36 +120,22 @@ npx get-shit-done-cc
125
120
  ## Requirements
126
121
 
127
122
  - **Claude Code CLI** (obviously)
128
- - **jq** - for JSON parsing ([install guide](https://stedolan.github.io/jq/download/))
129
- - **Bash** - ships with macOS/Linux
123
+ - **Node.js** >= 14
130
124
  - **Git** - for git status features (optional)
131
125
 
132
- ### Installing jq
133
-
134
- ```bash
135
- # macOS
136
- brew install jq
137
-
138
- # Ubuntu/Debian
139
- sudo apt install jq
140
-
141
- # Windows (via chocolatey)
142
- choco install jq
143
- ```
144
-
145
126
  ## Manual Installation
146
127
 
147
128
  If you prefer to install manually:
148
129
 
149
- 1. Copy `statusline.sh` to `~/.claude/statusline.sh`
150
- 2. Make it executable: `chmod +x ~/.claude/statusline.sh`
130
+ 1. Copy `statusline.js` to `~/.claude/statusline.js`
131
+ 2. Make it executable: `chmod +x ~/.claude/statusline.js`
151
132
  3. Add to `~/.claude/settings.json`:
152
133
 
153
134
  ```json
154
135
  {
155
136
  "statusLine": {
156
137
  "type": "command",
157
- "command": "/Users/YOUR_USERNAME/.claude/statusline.sh"
138
+ "command": "/Users/YOUR_USERNAME/.claude/statusline.js --width 50"
158
139
  }
159
140
  }
160
141
  ```
@@ -164,12 +145,11 @@ If you prefer to install manually:
164
145
  ### Statusline not showing?
165
146
 
166
147
  1. Make sure you restarted Claude Code after installation
167
- 2. Check that `jq` is installed: `which jq`
168
- 3. Verify the script is executable: `ls -la ~/.claude/statusline.sh`
148
+ 2. Verify the script is executable: `ls -la ~/.claude/statusline.js`
169
149
 
170
150
  ### Wrong colors?
171
151
 
172
- Your terminal needs to support 256 colors. Most modern terminals do.
152
+ Your terminal needs to support true color (24-bit). Most modern terminals do.
173
153
 
174
154
  ### Git status not showing?
175
155
 
package/bin/install.js CHANGED
@@ -59,37 +59,10 @@ ${cyan} ╔═╗╦ ╔═╗╦ ╦╔╦╗╔═╗
59
59
  ${bold}The statusline Claude Code deserves${reset} ${dim}v${pkg.version}${reset}
60
60
 
61
61
  ${dim}───────────────────────────────────────────${reset}
62
- ${green}β–ˆ${reset}${green}β–ˆ${reset}${yellow}β–ˆ${reset}${yellow}β–ˆ${reset}${red}β–ˆ${reset}${red}β–ˆ${reset}β–‘β–‘β–‘β–‘βš‘β–‘β–‘β–‘ ${dim}Gradient progress β€’ Git status${reset}
62
+ ${green}β–ˆβ–ˆ${reset}${yellow}β–ˆβ–ˆ${reset}${red}β–ˆβ–ˆ${reset}${dim}β–‘β–‘β–‘β–‘${reset}${red}⚑${reset}${dim}β–‘β–‘β–‘ Gradient progress β€’ Git status${reset}
63
63
  ${dim}───────────────────────────────────────────${reset}
64
64
  `;
65
65
 
66
- const features = `
67
- ${cyan}✦${reset} ${bold}Gradient Progress Bar${reset}
68
- 50 segments that flow ${green}green${reset} β†’ ${yellow}yellow${reset} β†’ ${red}red${reset}
69
- as you approach the context limit
70
-
71
- ${cyan}✦${reset} ${bold}⚑ Auto-Compact Threshold${reset}
72
- Red lightning bolt shows exactly where
73
- Claude will auto-summarize (78%)
74
-
75
- ${cyan}✦${reset} ${bold}Git Integration${reset}
76
- Branch β€’ ${green}βœ“${reset}/${yellow}●${reset}/${green}✚${reset} status β€’ ${green}↑${reset}ahead ${red}↓${reset}behind
77
-
78
- ${cyan}✦${reset} ${bold}Session Cost Tracking${reset}
79
- See your running \$ cost in real-time
80
-
81
- ${cyan}✦${reset} ${bold}2-Line Dashboard${reset}
82
- Line 1: πŸ€– Model β€’ Cost β€’ Context
83
- Line 2: πŸ“ Project β€’ Branch β€’ Status
84
- `;
85
-
86
- const example = `
87
- ${dim}β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”${reset}
88
- ${dim}β”‚${reset} πŸ€– ${cyan}Opus 4.5${reset} ${green}(\$12.41)${reset} β”‚ [${green}β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ${yellow}β–ˆβ–ˆβ–ˆβ–ˆ${red}β–ˆβ–ˆ${reset}β–‘β–‘β–‘β–‘β–‘β–‘β–‘βš‘β–‘β–‘β–‘β–‘β–‘] ${yellow}62k${reset}/${cyan}200k${reset} ${dim}β”‚${reset}
89
- ${dim}β”‚${reset} πŸ“ ${cyan}my-project${reset} ${magenta}main${reset} ${green}βœ“${reset} β†’ ${cyan}origin/main${reset} ${green}↑2${reset} ${dim}β”‚${reset}
90
- ${dim}β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜${reset}
91
- `;
92
-
93
66
  // Help text
94
67
  if (hasHelp) {
95
68
  console.log(banner);
@@ -161,10 +134,10 @@ function uninstall(isGlobal) {
161
134
  let removed = false;
162
135
 
163
136
  // Remove statusline.sh
164
- const statuslinePath = path.join(targetDir, 'statusline.sh');
137
+ const statuslinePath = path.join(targetDir, 'statusline.js');
165
138
  if (fs.existsSync(statuslinePath)) {
166
139
  fs.unlinkSync(statuslinePath);
167
- console.log(` ${green}βœ“${reset} Removed statusline.sh`);
140
+ console.log(` ${green}βœ“${reset} Removed statusline.js`);
168
141
  removed = true;
169
142
  }
170
143
 
@@ -174,7 +147,7 @@ function uninstall(isGlobal) {
174
147
  const settings = readSettings(settingsPath);
175
148
 
176
149
  if (settings.statusLine && settings.statusLine.command &&
177
- settings.statusLine.command.includes('statusline.sh')) {
150
+ settings.statusLine.command.includes('statusline.js')) {
178
151
  delete settings.statusLine;
179
152
  writeSettings(settingsPath, settings);
180
153
  console.log(` ${green}βœ“${reset} Removed statusline from settings.json`);
@@ -202,7 +175,8 @@ function uninstall(isGlobal) {
202
175
  /**
203
176
  * Install statusline
204
177
  */
205
- function install(isGlobal) {
178
+ function install(isGlobal, barWidth) {
179
+ barWidth = barWidth || 50;
206
180
  const targetDir = isGlobal ? getGlobalDir() : getLocalDir();
207
181
  const locationLabel = isGlobal
208
182
  ? targetDir.replace(os.homedir(), '~')
@@ -217,11 +191,11 @@ function install(isGlobal) {
217
191
  }
218
192
 
219
193
  // Copy statusline.sh
220
- const statuslineSrc = path.join(__dirname, '..', 'statusline.sh');
221
- const statuslineDest = path.join(targetDir, 'statusline.sh');
194
+ const statuslineSrc = path.join(__dirname, '..', 'statusline.js');
195
+ const statuslineDest = path.join(targetDir, 'statusline.js');
222
196
  fs.copyFileSync(statuslineSrc, statuslineDest);
223
197
  fs.chmodSync(statuslineDest, '755');
224
- console.log(` ${green}βœ“${reset} Installed statusline.sh`);
198
+ console.log(` ${green}βœ“${reset} Installed statusline.js`);
225
199
 
226
200
  // Update settings.json
227
201
  const settingsPath = path.join(targetDir, 'settings.json');
@@ -248,7 +222,7 @@ function install(isGlobal) {
248
222
  // Set new format
249
223
  settings.statusLine = {
250
224
  type: 'command',
251
- command: statuslineDest
225
+ command: statuslineDest + ' --width ' + barWidth
252
226
  };
253
227
 
254
228
  writeSettings(settingsPath, settings);
@@ -276,13 +250,10 @@ function install(isGlobal) {
276
250
  function promptLocation() {
277
251
  if (!process.stdin.isTTY) {
278
252
  console.log(` ${dim}Non-interactive mode, defaulting to global install${reset}\n`);
279
- install(true);
253
+ install(true, 50);
280
254
  return;
281
255
  }
282
256
 
283
- console.log(features);
284
- console.log(example);
285
-
286
257
  const rl = readline.createInterface({
287
258
  input: process.stdin,
288
259
  output: process.stdout
@@ -309,12 +280,47 @@ function promptLocation() {
309
280
  This project only
310
281
  `);
311
282
 
312
- rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
313
- answered = true;
314
- rl.close();
283
+ rl.question(` Choice ${dim}[1]${reset}: `, (locAnswer) => {
284
+ const isGlobal = (locAnswer.trim() || '1') !== '2';
315
285
  console.log('');
316
- const choice = answer.trim() || '1';
317
- install(choice !== '2');
286
+ promptWidth(rl, (barWidth) => {
287
+ answered = true;
288
+ rl.close();
289
+ console.log('');
290
+ install(isGlobal, barWidth);
291
+ });
292
+ });
293
+ }
294
+
295
+ /**
296
+ * Interactive prompt for bar width
297
+ */
298
+ function promptWidth(rl, callback) {
299
+ console.log(` ${yellow}Progress bar width?${reset}
300
+
301
+ ${cyan}1${reset}) ${bold}Compact${reset} ${dim}[${green}${'β–ˆβ–ˆ'.repeat(3)}${reset}${dim}${'β–‘'.repeat(19)}${red}ϟ${reset}${dim}${'β–‘'.repeat(3)}]${reset} ${dim}25 bars${reset}
302
+ ${cyan}2${reset}) ${bold}Medium${reset} ${dim}[${green}${'β–ˆβ–ˆ'.repeat(5)}${reset}${dim}${'β–‘'.repeat(24)}${red}ϟ${reset}${dim}${'β–‘'.repeat(5)}]${reset} ${dim}38 bars${reset}
303
+ ${cyan}3${reset}) ${bold}Full${reset} ${dim}[${green}${'β–ˆβ–ˆ'.repeat(7)}${reset}${dim}${'β–‘'.repeat(28)}${red}ϟ${reset}${dim}${'β–‘'.repeat(8)}]${reset} ${dim}50 bars${reset}
304
+ ${cyan}4${reset}) ${bold}Custom${reset} ${dim}Enter your own number${reset}
305
+ `);
306
+
307
+ rl.question(` Choice ${dim}[3]${reset}: `, (answer) => {
308
+ const choice = answer.trim() || '3';
309
+ const widths = { '1': 25, '2': 38, '3': 50 };
310
+
311
+ if (choice === '4') {
312
+ rl.question(` Number of bars ${dim}(10-100)${reset}: `, (numAnswer) => {
313
+ const num = parseInt(numAnswer.trim(), 10);
314
+ if (num >= 10 && num <= 100) {
315
+ callback(num);
316
+ } else {
317
+ console.log(` ${yellow}⚠${reset} Invalid number, using default (50)\n`);
318
+ callback(50);
319
+ }
320
+ });
321
+ } else {
322
+ callback(widths[choice] || 50);
323
+ }
318
324
  });
319
325
  }
320
326
 
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@buckits/claude-statusline",
3
- "version": "2.3.2",
3
+ "version": "3.0.0",
4
4
  "description": "The statusline Claude Code deserves - gradient progress bar, auto-compact threshold marker, git status, cost tracking, 2-line dashboard",
5
5
  "bin": {
6
6
  "claude-statusline": "bin/install.js"
7
7
  },
8
8
  "files": [
9
9
  "bin/",
10
- "statusline.sh"
10
+ "statusline.js"
11
11
  ],
12
12
  "keywords": [
13
13
  "claude",
package/statusline.js ADDED
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const { execFileSync } = require('child_process');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ // Parse --width arg (default 50)
11
+ const widthArgIdx = process.argv.indexOf('--width');
12
+ const BAR_WIDTH = widthArgIdx !== -1 && process.argv[widthArgIdx + 1]
13
+ ? parseInt(process.argv[widthArgIdx + 1], 10) || 50
14
+ : 50;
15
+
16
+ // Read JSON input from stdin
17
+ let inputData = '';
18
+ process.stdin.setEncoding('utf8');
19
+ process.stdin.on('data', (chunk) => { inputData += chunk; });
20
+ process.stdin.on('end', () => {
21
+ try {
22
+ main(JSON.parse(inputData));
23
+ } catch (e) {
24
+ process.exit(1);
25
+ }
26
+ });
27
+
28
+ function main(input) {
29
+ // Extract values from JSON
30
+ const cwd = (input.workspace && input.workspace.current_dir) || input.cwd || '';
31
+ const model = (input.model && input.model.display_name) || '';
32
+
33
+ // Extract token information from context_window
34
+ const cw = input.context_window || {};
35
+ const cu = cw.current_usage || {};
36
+ const totalInput = Number(cw.total_input_tokens) || 0;
37
+ const totalOutput = Number(cw.total_output_tokens) || 0;
38
+ const cacheRead = Number(cu.cache_read_input_tokens) || 0;
39
+
40
+ // Total used = input + output + cached tokens being used
41
+ let usedTokens = totalInput + totalOutput + cacheRead;
42
+ const maxTokens = Number(cw.context_window_size) || 200000;
43
+
44
+ // Use the pre-calculated percentage if available (more accurate)
45
+ let percent;
46
+ const percentPrecalc = cw.used_percentage;
47
+ if (percentPrecalc != null && percentPrecalc !== '') {
48
+ percent = Math.trunc(Number(percentPrecalc));
49
+ // Recalculate used_tokens from percentage for display accuracy
50
+ usedTokens = Math.trunc(maxTokens * percent / 100);
51
+ } else {
52
+ // Fallback: calculate percentage from tokens
53
+ percent = maxTokens > 0 ? Math.trunc(usedTokens * 100 / maxTokens) : 0;
54
+ }
55
+
56
+ // Clamp percent to 0-100
57
+ if (percent < 0) percent = 0;
58
+ if (percent > 100) percent = 100;
59
+
60
+ // Progress bar settings
61
+ const barWidth = BAR_WIDTH;
62
+ const filled = Math.trunc(percent * barWidth / 100);
63
+
64
+ // ── Git information ──────────────────────────────────
65
+ let gitBranch = '';
66
+ let gitRemote = '';
67
+ let gitAhead = '';
68
+ let gitBehind = '';
69
+ let gitStatusIndicator = '';
70
+
71
+ if (cwd) {
72
+ try {
73
+ execFileSync('git', ['rev-parse', '--git-dir'], { cwd, stdio: 'pipe' });
74
+
75
+ // Branch name
76
+ try {
77
+ gitBranch = execFileSync('git', ['branch', '--show-current'],
78
+ { cwd, encoding: 'utf8', stdio: 'pipe' }).trim();
79
+ } catch (e) {}
80
+
81
+ // Upstream tracking branch
82
+ let upstream = '';
83
+ try {
84
+ upstream = execFileSync('git',
85
+ ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'],
86
+ { cwd, encoding: 'utf8', stdio: 'pipe' }).trim();
87
+ gitRemote = upstream;
88
+ } catch (e) {}
89
+
90
+ // Ahead/behind counts
91
+ if (upstream) {
92
+ try {
93
+ const ab = execFileSync('git',
94
+ ['rev-list', '--left-right', '--count', 'HEAD...' + upstream],
95
+ { cwd, encoding: 'utf8', stdio: 'pipe' }).trim();
96
+ const parts = ab.split(/\s+/);
97
+ const ahead = parseInt(parts[0], 10);
98
+ const behind = parseInt(parts[1], 10);
99
+ if (ahead > 0) gitAhead = String(ahead);
100
+ if (behind > 0) gitBehind = String(behind);
101
+ } catch (e) {}
102
+ }
103
+
104
+ // Git status (dirty/staged indicators)
105
+ try {
106
+ const porcelain = execFileSync('git', ['status', '--porcelain'],
107
+ { cwd, encoding: 'utf8', stdio: 'pipe' });
108
+
109
+ let hasUnstaged = false;
110
+ let hasStaged = false;
111
+
112
+ if (porcelain) {
113
+ const lines = porcelain.split('\n');
114
+ for (const line of lines) {
115
+ if (!line) continue;
116
+ // Check for staged changes (first column not space)
117
+ if (/^[MADRC]/.test(line)) hasStaged = true;
118
+ // Check for unstaged changes (second column not space, or untracked files)
119
+ if (/^\?\?/.test(line) || /^.[MD]/.test(line)) hasUnstaged = true;
120
+ }
121
+ }
122
+
123
+ // Build status indicator
124
+ if (hasUnstaged && hasStaged) {
125
+ gitStatusIndicator = '\x1b[1;33m●\x1b[1;32m✚\x1b[0m'; // Both
126
+ } else if (hasUnstaged) {
127
+ gitStatusIndicator = '\x1b[1;33m●\x1b[0m'; // Unstaged (yellow)
128
+ } else if (hasStaged) {
129
+ gitStatusIndicator = '\x1b[1;32m✚\x1b[0m'; // Staged (green)
130
+ } else {
131
+ gitStatusIndicator = '\x1b[1;32mβœ“\x1b[0m'; // Clean (green)
132
+ }
133
+ } catch (e) {}
134
+ } catch (e) {
135
+ // Not a git repo β€” skip git info
136
+ }
137
+ }
138
+
139
+ // Project name from directory
140
+ const projectName = cwd ? path.basename(cwd) : '';
141
+
142
+ // ── Token formatting ─────────────────────────────────
143
+
144
+ // Format tokens for display (e.g., 45.2k/200k)
145
+ function formatTokens(tokens) {
146
+ if (tokens >= 1000000) {
147
+ const millions = Math.trunc(tokens / 1000000);
148
+ const decimal = Math.trunc((tokens % 1000000) / 100000);
149
+ return millions + '.' + decimal + 'M';
150
+ } else if (tokens >= 1000) {
151
+ const thousands = Math.trunc(tokens / 1000);
152
+ const decimal = Math.trunc((tokens % 1000) / 100);
153
+ return thousands + '.' + decimal + 'k';
154
+ }
155
+ return String(tokens);
156
+ }
157
+
158
+ // Format tokens without decimals (e.g., 72k/200k)
159
+ function formatTokensInt(tokens) {
160
+ if (tokens >= 1000000) {
161
+ return Math.trunc(tokens / 1000000) + 'M';
162
+ } else if (tokens >= 1000) {
163
+ return Math.trunc(tokens / 1000) + 'k';
164
+ }
165
+ return String(tokens);
166
+ }
167
+
168
+ const usedFmtInt = formatTokensInt(usedTokens);
169
+ const maxFmtInt = formatTokensInt(maxTokens);
170
+
171
+ // ── Cost ──────────────────────────────────────────────
172
+ let costDisplay = '';
173
+ const costUsd = input.cost && input.cost.total_cost_usd;
174
+ if (costUsd != null && costUsd !== '' && costUsd !== null) {
175
+ costDisplay = '$' + Number(costUsd).toFixed(2);
176
+ }
177
+
178
+ // ── Tools / Skills / Agents counts ────────────────────
179
+ const ctx = input.context || {};
180
+ const skillsCount = Array.isArray(ctx.skills) ? ctx.skills.length : 0;
181
+ const agentsCount = Array.isArray(ctx.agents) ? ctx.agents.length : 0;
182
+ const mcpToolsCount = Array.isArray(ctx.mcp_tools) ? ctx.mcp_tools.length : 0;
183
+ const totalTools = skillsCount + agentsCount + mcpToolsCount;
184
+ let toolsDisplay = '';
185
+ if (totalTools > 0) {
186
+ toolsDisplay = '\uD83D\uDD27 ' + totalTools; // πŸ”§
187
+ }
188
+
189
+ // ── Background tasks ──────────────────────────────────
190
+ const bgTasksArr = input.background_tasks || input.running_agents || input.active_tasks;
191
+ let bgTasksCount = 0;
192
+ if (bgTasksArr != null) {
193
+ bgTasksCount = Array.isArray(bgTasksArr) ? bgTasksArr.length : 0;
194
+ }
195
+ let bgDisplay = '';
196
+ if (bgTasksCount > 0) {
197
+ bgDisplay = '\u231B ' + bgTasksCount; // ⏳
198
+ }
199
+
200
+ // ── Auto-compact threshold ────────────────────────────
201
+ const AUTO_COMPACT_THRESHOLD = 22;
202
+ let untilCompact = cw.until_compact != null ? Number(cw.until_compact)
203
+ : cw.until_auto_compact != null ? Number(cw.until_auto_compact)
204
+ : null;
205
+
206
+ if (untilCompact == null) {
207
+ const remainingPct = Number(cw.remaining_percentage) || 0;
208
+ if (remainingPct > AUTO_COMPACT_THRESHOLD) {
209
+ untilCompact = remainingPct - AUTO_COMPACT_THRESHOLD;
210
+ } else {
211
+ untilCompact = 0;
212
+ }
213
+ }
214
+
215
+ let compactColor = '38;5;46';
216
+ let compactIndicatorText = '';
217
+ let compactIndicatorPct = '';
218
+
219
+ if (untilCompact && untilCompact !== 0) {
220
+ if (untilCompact >= 25) compactColor = '38;5;46';
221
+ else if (untilCompact >= 20) compactColor = '38;5;154';
222
+ else if (untilCompact >= 15) compactColor = '38;5;226';
223
+ else if (untilCompact >= 10) compactColor = '38;5;220';
224
+ else if (untilCompact >= 7) compactColor = '38;5;214';
225
+ else if (untilCompact >= 4) compactColor = '38;5;208';
226
+ else compactColor = '38;5;196';
227
+
228
+ // Calculate tokens until compact threshold
229
+ const tokensUntilCompact = Math.trunc(untilCompact * maxTokens / 100);
230
+ const tokensUntilCompactFmt = formatTokens(tokensUntilCompact);
231
+
232
+ // Build indicator with tokens primary (gradient color) and percentage in parens (dim)
233
+ compactIndicatorText = '\u26A1' + tokensUntilCompactFmt + ' until compact'; // ⚑
234
+ compactIndicatorPct = '(' + untilCompact + '%)';
235
+ }
236
+
237
+ // ══════════════════════════════════════════════════════
238
+ // 2-LINE DASHBOARD LAYOUT
239
+ // ══════════════════════════════════════════════════════
240
+
241
+ // LINE 1: Model + Context
242
+ // Format: Opus 4.5 ($12.01) [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘βš‘β–‘β–‘β–‘β–‘β–‘] 74k/200k
243
+
244
+ const compactThresholdPct = 100 - AUTO_COMPACT_THRESHOLD; // 78%
245
+ const thresholdPosition = Math.trunc(compactThresholdPct * barWidth / 100);
246
+
247
+ // Build unified progress bar with per-bar gradient toward threshold
248
+ let unifiedBar = '[';
249
+ let unifiedRightmostColor = '38;2;0;255;0'; // Default green
250
+
251
+ // Smooth RGB gradient: green(0,255,0) β†’ yellow(255,255,0) β†’ red(255,0,0)
252
+ // t = 0.0 β†’ green, t = 0.5 β†’ yellow, t = 1.0 β†’ red
253
+ function gradientRGB(i) {
254
+ const t = thresholdPosition > 0 ? i / thresholdPosition : 0;
255
+ let r, g;
256
+ if (t <= 0.5) {
257
+ // Green β†’ Yellow: R ramps up, G stays max
258
+ r = Math.round(t * 2 * 255);
259
+ g = 255;
260
+ } else {
261
+ // Yellow β†’ Red: R stays max, G ramps down
262
+ r = 255;
263
+ g = Math.round((1 - t) * 2 * 255);
264
+ }
265
+ return { r, g, b: 0 };
266
+ }
267
+
268
+ for (let i = 0; i < barWidth; i++) {
269
+ if (i === thresholdPosition) {
270
+ // Threshold marker: bolt replaces this bar segment
271
+ if (i < filled) {
272
+ // Filled: gradient background with contrasting bold white bolt
273
+ const { r, g, b } = gradientRGB(i);
274
+ unifiedRightmostColor = '38;2;' + r + ';' + g + ';' + b;
275
+ unifiedBar += '\x1b[48;2;' + r + ';' + g + ';' + b + ';1;37m\u03DF\x1b[0m';
276
+ } else {
277
+ // Not filled: bright red bolt on theme-adaptive dark background
278
+ unifiedBar += '\x1b[100;38;2;255;0;0m\u03DF\x1b[0m';
279
+ }
280
+ } else if (i < filled) {
281
+ if (i > thresholdPosition) {
282
+ // Past threshold: always red
283
+ unifiedRightmostColor = '38;2;255;0;0';
284
+ unifiedBar += '\x1b[38;2;255;0;0m\u2588\x1b[0m';
285
+ } else {
286
+ const { r, g, b } = gradientRGB(i);
287
+ const color = '38;2;' + r + ';' + g + ';' + b;
288
+ unifiedRightmostColor = color;
289
+ unifiedBar += '\x1b[' + color + 'm\u2588\x1b[0m';
290
+ }
291
+ } else {
292
+ unifiedBar += '\u2591'; // β–‘
293
+ }
294
+ }
295
+
296
+ unifiedBar += ']';
297
+
298
+ // Assemble Line 1
299
+ let line1 = '';
300
+
301
+ // Robot icon and model name in cyan
302
+ if (model) {
303
+ line1 += '\uD83E\uDD16 \x1b[1;36m' + model + '\x1b[0m'; // πŸ€–
304
+ }
305
+
306
+ // Add cost after model name (bright green)
307
+ if (costDisplay) {
308
+ line1 += ' \x1b[1;32m(' + costDisplay + ')\x1b[0m';
309
+ }
310
+
311
+ // Add subtle separator between cost and progress bar
312
+ line1 += ' \x1b[2;37m\u2502\x1b[0m'; // β”‚
313
+
314
+ // Add progress bar and tokens to line 1
315
+ line1 += ' ' + unifiedBar
316
+ + ' \x1b[' + unifiedRightmostColor + 'm' + usedFmtInt + '\x1b[0m'
317
+ + '\x1b[1;36m/' + maxFmtInt + '\x1b[0m';
318
+
319
+ // LINE 2: Project/Git Info
320
+ // Format: πŸ“ myproject main βœ“ β†’ origin/main ↑11
321
+ let line2 = '';
322
+
323
+ // Folder icon and project name in cyan
324
+ if (projectName) {
325
+ line2 += '\uD83D\uDCC1 \x1b[1;36m' + projectName + '\x1b[0m'; // πŸ“
326
+ }
327
+
328
+ // Git branch and tracking
329
+ if (gitBranch) {
330
+ if (line2) line2 += ' ';
331
+
332
+ // Branch in magenta
333
+ line2 += '\x1b[1;35m' + gitBranch + '\x1b[0m';
334
+
335
+ // Git status indicator (after branch name)
336
+ if (gitStatusIndicator) {
337
+ line2 += ' ' + gitStatusIndicator;
338
+ }
339
+
340
+ // Arrow and tracking
341
+ line2 += ' \x1b[2;37m\u2192\x1b[0m'; // β†’
342
+
343
+ if (gitRemote) {
344
+ // Tracking branch in blue
345
+ line2 += ' \x1b[1;34m' + gitRemote + '\x1b[0m';
346
+
347
+ // Ahead/behind indicators
348
+ if (gitAhead || gitBehind) {
349
+ line2 += ' ';
350
+ if (gitAhead) line2 += '\x1b[0;32m\u2191' + gitAhead + '\x1b[0m'; // ↑
351
+ if (gitBehind) {
352
+ if (gitAhead) line2 += ' ';
353
+ line2 += '\x1b[0;31m\u2193' + gitBehind + '\x1b[0m'; // ↓
354
+ }
355
+ }
356
+ } else {
357
+ line2 += ' \x1b[2;37m(no upstream)\x1b[0m';
358
+ }
359
+ }
360
+
361
+ // ── GSD Update check ─────────────────────────────────
362
+ let gsdUpdateSuffix = '';
363
+ const gsdCacheFile = path.join(os.homedir(), '.claude', 'cache', 'gsd-update-check.json');
364
+
365
+ // Check for GSD in project first, then global
366
+ let gsdVersionFile = '';
367
+ if (cwd) {
368
+ const localGsd = path.join(cwd, '.claude', 'get-shit-done', 'VERSION');
369
+ if (fs.existsSync(localGsd)) {
370
+ gsdVersionFile = localGsd;
371
+ }
372
+ }
373
+ if (!gsdVersionFile) {
374
+ const globalGsd = path.join(os.homedir(), '.claude', 'get-shit-done', 'VERSION');
375
+ if (fs.existsSync(globalGsd)) {
376
+ gsdVersionFile = globalGsd;
377
+ }
378
+ }
379
+
380
+ if (gsdVersionFile) {
381
+ let installedVer = '';
382
+ try {
383
+ installedVer = fs.readFileSync(gsdVersionFile, 'utf8').trim();
384
+ } catch (e) {}
385
+ if (!installedVer) installedVer = '0.0.0';
386
+
387
+ // Get latest version from cache (populated by SessionStart hook)
388
+ if (fs.existsSync(gsdCacheFile)) {
389
+ try {
390
+ const cache = JSON.parse(fs.readFileSync(gsdCacheFile, 'utf8'));
391
+ const latestVer = cache.latest || 'unknown';
392
+
393
+ // Compare versions - append to line2 if they differ
394
+ if (latestVer !== 'unknown' && installedVer !== latestVer) {
395
+ gsdUpdateSuffix = ' | GSD ' + installedVer + '>' + latestVer;
396
+ }
397
+ } catch (e) {}
398
+ }
399
+ }
400
+
401
+ // Append GSD update to line2 if available
402
+ line2 += gsdUpdateSuffix;
403
+
404
+ // Output the dashboard (2 lines, no trailing newline)
405
+ process.stdout.write(line1 + '\x1b[K\n' + line2 + '\x1b[K');
406
+ }
package/statusline.sh DELETED
@@ -1,437 +0,0 @@
1
- #!/bin/bash
2
-
3
- # Claude Code Status Line Script
4
- # Read JSON input from stdin
5
-
6
- input=$(cat)
7
-
8
- # Extract values from JSON
9
- cwd=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // empty')
10
- model=$(echo "$input" | jq -r '.model.display_name // empty')
11
-
12
- # Extract token information from context_window
13
- # The actual context usage includes cache tokens from prompt caching
14
- total_input=$(echo "$input" | jq -r '.context_window.total_input_tokens // 0')
15
- total_output=$(echo "$input" | jq -r '.context_window.total_output_tokens // 0')
16
- cache_read=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0')
17
-
18
- # Total used = input + output + cached tokens being used
19
- used_tokens=$((total_input + total_output + cache_read))
20
- max_tokens=$(echo "$input" | jq -r '.context_window.context_window_size // 200000')
21
-
22
- # Use the pre-calculated percentage if available (more accurate)
23
- percent_precalc=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
24
- if [ -n "$percent_precalc" ]; then
25
- percent=$percent_precalc
26
- # Recalculate used_tokens from percentage for display accuracy
27
- used_tokens=$((max_tokens * percent / 100))
28
- else
29
- # Fallback: calculate percentage from tokens
30
- if [ "$max_tokens" -gt 0 ] 2>/dev/null; then
31
- percent=$((used_tokens * 100 / max_tokens))
32
- else
33
- percent=0
34
- fi
35
- fi
36
-
37
- # Clamp percent to 0-100
38
- [ "$percent" -lt 0 ] && percent=0
39
- [ "$percent" -gt 100 ] && percent=100
40
-
41
- # Progress bar settings - 50 squares (4k tokens each for 200k context)
42
- bar_width=50
43
- filled=$((percent * bar_width / 100))
44
- empty=$((bar_width - filled))
45
-
46
- # Get color for a specific bar position (0 to bar_width-1)
47
- # Each bar position gets its own color based on progression
48
- get_bar_color_for_position() {
49
- local pos=$1
50
- local total=$2
51
- # Calculate percentage for this specific bar position
52
- local bar_pct=$((pos * 100 / total))
53
-
54
- if [ "$bar_pct" -le 33 ]; then
55
- # Green range: 0-33%
56
- # Bright green (46) β†’ yellow-green (154) β†’ green (34)
57
- if [ "$bar_pct" -le 16 ]; then
58
- echo "38;5;46" # Bright green
59
- elif [ "$bar_pct" -le 25 ]; then
60
- echo "38;5;118" # Green-yellow
61
- else
62
- echo "38;5;154" # Yellow-green
63
- fi
64
- elif [ "$bar_pct" -le 66 ]; then
65
- # Orange range: 34-66%
66
- # Yellow (226) β†’ orange (214) β†’ dark orange (208)
67
- local adj_pct=$((bar_pct - 33))
68
- if [ "$adj_pct" -le 11 ]; then
69
- echo "38;5;226" # Yellow
70
- elif [ "$adj_pct" -le 22 ]; then
71
- echo "38;5;220" # Light orange
72
- else
73
- echo "38;5;214" # Orange
74
- fi
75
- else
76
- # Red range: 67-100%
77
- # Orange-red (208) β†’ red (196) β†’ dark red (160)
78
- local adj_pct=$((bar_pct - 66))
79
- if [ "$adj_pct" -le 11 ]; then
80
- echo "38;5;208" # Orange-red
81
- elif [ "$adj_pct" -le 22 ]; then
82
- echo "38;5;202" # Red-orange
83
- else
84
- echo "38;5;196" # Bright red
85
- fi
86
- fi
87
- }
88
-
89
- # Build progress bar with gradient colors
90
- # Track the color of the rightmost filled bar for the percentage text
91
- bar="["
92
- rightmost_color="38;5;46" # Default to bright green
93
- for ((i=0; i<filled; i++)); do
94
- bar_color=$(get_bar_color_for_position $i $bar_width)
95
- rightmost_color=$bar_color # Keep updating to track the last one
96
- bar+=$(printf "\033[${bar_color}mβ–ˆ\033[0m")
97
- done
98
- for ((i=0; i<empty; i++)); do
99
- bar+="β–‘"
100
- done
101
- bar+="]"
102
-
103
- # Get git information if in a git repo
104
- git_branch=""
105
- git_remote=""
106
- if [ -n "$cwd" ] && git -C "$cwd" rev-parse --git-dir >/dev/null 2>&1; then
107
- git_branch=$(git -C "$cwd" branch --show-current 2>/dev/null)
108
-
109
- # Get the upstream tracking branch
110
- upstream=$(git -C "$cwd" rev-parse --abbrev-ref --symbolic-full-name @{upstream} 2>/dev/null)
111
- if [ -n "$upstream" ]; then
112
- # upstream will be in format like "origin/POC" or "origin/main"
113
- git_remote="$upstream"
114
- fi
115
-
116
- # Get ahead/behind counts
117
- git_ahead=""
118
- git_behind=""
119
- if [ -n "$upstream" ]; then
120
- # Get number of commits ahead/behind
121
- ahead_behind=$(git -C "$cwd" rev-list --left-right --count HEAD...$upstream 2>/dev/null)
122
- if [ -n "$ahead_behind" ]; then
123
- ahead=$(echo "$ahead_behind" | awk '{print $1}')
124
- behind=$(echo "$ahead_behind" | awk '{print $2}')
125
- [ "$ahead" -gt 0 ] && git_ahead="$ahead"
126
- [ "$behind" -gt 0 ] && git_behind="$behind"
127
- fi
128
- fi
129
-
130
- # Get git status (dirty/staged indicators)
131
- git_status_indicator=""
132
- if git -C "$cwd" rev-parse --git-dir >/dev/null 2>&1; then
133
- porcelain=$(git -C "$cwd" status --porcelain 2>/dev/null)
134
-
135
- has_unstaged=false
136
- has_staged=false
137
-
138
- if [ -n "$porcelain" ]; then
139
- # Check for staged changes (first column not space)
140
- if echo "$porcelain" | grep -q '^[MADRC]'; then
141
- has_staged=true
142
- fi
143
-
144
- # Check for unstaged changes (second column not space, or untracked files)
145
- if echo "$porcelain" | grep -q '^\?\?' || echo "$porcelain" | grep -q '^.[MD]'; then
146
- has_unstaged=true
147
- fi
148
- fi
149
-
150
- # Build status indicator
151
- if [ "$has_unstaged" = true ] && [ "$has_staged" = true ]; then
152
- git_status_indicator=$(printf "\033[1;33m●\033[1;32m✚\033[0m") # Both
153
- elif [ "$has_unstaged" = true ]; then
154
- git_status_indicator=$(printf "\033[1;33m●\033[0m") # Unstaged (yellow)
155
- elif [ "$has_staged" = true ]; then
156
- git_status_indicator=$(printf "\033[1;32m✚\033[0m") # Staged (green)
157
- else
158
- git_status_indicator=$(printf "\033[1;32mβœ“\033[0m") # Clean (green)
159
- fi
160
- fi
161
- fi
162
-
163
- # Get project name from directory
164
- project_name=""
165
- if [ -n "$cwd" ]; then
166
- project_name=$(basename "$cwd")
167
- fi
168
-
169
- # Format tokens for display (e.g., 45.2k/200k)
170
- format_tokens() {
171
- local tokens=$1
172
- # Use pure bash arithmetic to avoid bc dependency
173
- if [ "$tokens" -ge 1000000 ]; then
174
- local millions=$((tokens / 1000000))
175
- local remainder=$((tokens % 1000000))
176
- local decimal=$((remainder / 100000))
177
- printf "%d.%dM" "$millions" "$decimal"
178
- elif [ "$tokens" -ge 1000 ]; then
179
- local thousands=$((tokens / 1000))
180
- local remainder=$((tokens % 1000))
181
- local decimal=$((remainder / 100))
182
- printf "%d.%dk" "$thousands" "$decimal"
183
- else
184
- echo "$tokens"
185
- fi
186
- }
187
-
188
- # Format tokens without decimals (e.g., 72k/200k)
189
- format_tokens_int() {
190
- local tokens=$1
191
- if [ "$tokens" -ge 1000000 ]; then
192
- local millions=$((tokens / 1000000))
193
- printf "%dM" "$millions"
194
- elif [ "$tokens" -ge 1000 ]; then
195
- local thousands=$((tokens / 1000))
196
- printf "%dk" "$thousands"
197
- else
198
- echo "$tokens"
199
- fi
200
- }
201
-
202
- used_fmt=$(format_tokens $used_tokens)
203
- max_fmt=$(format_tokens $max_tokens)
204
- used_fmt_int=$(format_tokens_int $used_tokens)
205
- max_fmt_int=$(format_tokens_int $max_tokens)
206
-
207
- # Extract cost information
208
- cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // empty')
209
- cost_display=""
210
- if [ -n "$cost_usd" ] && [ "$cost_usd" != "null" ]; then
211
- # Format cost to 2 decimal places
212
- cost_display=$(printf "\$%.2f" "$cost_usd")
213
- fi
214
-
215
- # Extract tools/skills/agents counts
216
- skills_count=$(echo "$input" | jq -r '.context.skills // [] | length')
217
- agents_count=$(echo "$input" | jq -r '.context.agents // [] | length')
218
- mcp_tools_count=$(echo "$input" | jq -r '.context.mcp_tools // [] | length')
219
- total_tools=$((skills_count + agents_count + mcp_tools_count))
220
- tools_display=""
221
- if [ "$total_tools" -gt 0 ]; then
222
- tools_display=$(printf "πŸ”§ %d" "$total_tools")
223
- fi
224
-
225
- # Extract background tasks (check for various possible field names)
226
- bg_tasks=$(echo "$input" | jq -r '.background_tasks // .running_agents // .active_tasks // empty | length')
227
- bg_display=""
228
- if [ -n "$bg_tasks" ] && [ "$bg_tasks" != "null" ] && [ "$bg_tasks" -gt 0 ]; then
229
- bg_display=$(printf "⏳ %d" "$bg_tasks")
230
- fi
231
-
232
- # Extract "until compact" percentage
233
- # Auto-compact triggers at a threshold (appears to be around 78% usage / 22% remaining)
234
- # The "until compact" value = remaining_percentage - threshold_remaining
235
- # If threshold is 22%, and you have 24% remaining, then until_compact = 24% - 22% = 2%
236
-
237
- # First check if there's an explicit field (unlikely but worth checking)
238
- until_compact=$(echo "$input" | jq -r '.context_window.until_compact // .context_window.until_auto_compact // empty')
239
-
240
- if [ -z "$until_compact" ]; then
241
- # Calculate based on auto-compact threshold
242
- # Auto-compact threshold appears to be 22% remaining (78% used)
243
- AUTO_COMPACT_THRESHOLD=22
244
-
245
- remaining_pct=$(echo "$input" | jq -r '.context_window.remaining_percentage // 0')
246
-
247
- if [ "$remaining_pct" -gt "$AUTO_COMPACT_THRESHOLD" ]; then
248
- until_compact=$((remaining_pct - AUTO_COMPACT_THRESHOLD))
249
- else
250
- # Already past threshold or at it
251
- until_compact=0
252
- fi
253
- fi
254
-
255
- compact_indicator=""
256
- compact_color="38;5;46"
257
- if [ -n "$until_compact" ] && [ "$until_compact" != "null" ] && [ "$until_compact" != "0" ]; then
258
- if [ "$until_compact" -ge 25 ]; then
259
- compact_color="38;5;46"
260
- elif [ "$until_compact" -ge 20 ]; then
261
- compact_color="38;5;154"
262
- elif [ "$until_compact" -ge 15 ]; then
263
- compact_color="38;5;226"
264
- elif [ "$until_compact" -ge 10 ]; then
265
- compact_color="38;5;220"
266
- elif [ "$until_compact" -ge 7 ]; then
267
- compact_color="38;5;214"
268
- elif [ "$until_compact" -ge 4 ]; then
269
- compact_color="38;5;208"
270
- else
271
- compact_color="38;5;196"
272
- fi
273
-
274
- # Calculate tokens until compact threshold
275
- # Auto-compact threshold is 22% remaining (78% used)
276
- # Tokens until compact = until_compact * context_window_size / 100
277
- remaining_pct=$(echo "$input" | jq -r '.context_window.remaining_percentage // 0')
278
- tokens_until_compact=$((until_compact * max_tokens / 100))
279
- tokens_until_compact_fmt=$(format_tokens $tokens_until_compact)
280
-
281
- # Build indicator with tokens primary (gradient color) and percentage in parens (dim)
282
- compact_indicator_text=$(printf "⚑%s until compact" "$tokens_until_compact_fmt")
283
- compact_indicator_pct=$(printf "(%d%%)" "$until_compact")
284
- fi
285
-
286
- # ==========================================
287
- # 2-LINE DASHBOARD LAYOUT
288
- # ==========================================
289
-
290
- # LINE 1: Model + Context
291
- # Format: Opus 4.5 ($12.01) [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘βš‘β–‘β–‘β–‘β–‘β–‘] 74k/200k
292
- line1=""
293
-
294
- # Robot icon and model name in cyan
295
- if [ -n "$model" ]; then
296
- line1+=$(printf "πŸ€– \033[1;36m%s\033[0m" "$model")
297
- fi
298
-
299
- # Add cost after model name (bright green)
300
- if [ -n "$cost_display" ]; then
301
- line1+=$(printf " \033[1;32m(%s)\033[0m" "$cost_display")
302
- fi
303
-
304
- # Add subtle separator between cost and progress bar
305
- line1+=$(printf " \033[2;37mβ”‚\033[0m")
306
-
307
- # Build progress bar for line 1
308
- # Calculate compact threshold position (78% of bar = 22% remaining)
309
- AUTO_COMPACT_THRESHOLD=22
310
- compact_threshold_pct=$((100 - AUTO_COMPACT_THRESHOLD)) # 78%
311
- threshold_position=$((compact_threshold_pct * bar_width / 100))
312
-
313
- # Build unified progress bar with per-bar gradient toward threshold
314
- unified_bar="["
315
- unified_rightmost_color="38;5;46" # Default green
316
-
317
- for ((i=0; i<bar_width; i++)); do
318
- if [ "$i" -eq "$threshold_position" ]; then
319
- # Insert red lightning bolt threshold marker
320
- unified_bar+=$(printf "\033[0;31m⚑\033[0m")
321
- fi
322
-
323
- if [ "$i" -lt "$filled" ]; then
324
- # Calculate color for this specific bar position
325
- # Gradient from green (position 0) to red (threshold position)
326
- if [ "$threshold_position" -gt 0 ]; then
327
- bar_position_pct=$((i * 100 / threshold_position))
328
- else
329
- bar_position_pct=0
330
- fi
331
-
332
- # Map position percentage to gradient colors
333
- if [ "$bar_position_pct" -le 20 ]; then
334
- bar_color="38;5;46" # Bright green
335
- elif [ "$bar_position_pct" -le 40 ]; then
336
- bar_color="38;5;118" # Green-yellow
337
- elif [ "$bar_position_pct" -le 60 ]; then
338
- bar_color="38;5;154" # Yellow-green
339
- elif [ "$bar_position_pct" -le 70 ]; then
340
- bar_color="38;5;226" # Yellow
341
- elif [ "$bar_position_pct" -le 80 ]; then
342
- bar_color="38;5;220" # Light orange
343
- elif [ "$bar_position_pct" -le 90 ]; then
344
- bar_color="38;5;214" # Orange
345
- elif [ "$bar_position_pct" -le 95 ]; then
346
- bar_color="38;5;208" # Orange-red
347
- else
348
- bar_color="38;5;196" # Red
349
- fi
350
-
351
- unified_rightmost_color=$bar_color
352
- unified_bar+=$(printf "\033[${bar_color}mβ–ˆ\033[0m")
353
- else
354
- unified_bar+="β–‘"
355
- fi
356
- done
357
-
358
- unified_bar+="]"
359
-
360
- # Add progress bar and tokens to line 1
361
- line1+=$(printf " %s \033[${unified_rightmost_color}m%s\033[0m\033[1;36m/%s\033[0m" "$unified_bar" "$used_fmt_int" "$max_fmt_int")
362
-
363
- # LINE 2: Project/Git Info
364
- # Format: πŸ“ trellis POC β†’ origin/POC ↑11
365
- line2=""
366
-
367
- # Folder icon and project name in cyan
368
- if [ -n "$project_name" ]; then
369
- line2+=$(printf "πŸ“ \033[1;36m%s\033[0m" "$project_name")
370
- fi
371
-
372
- # Git branch and tracking
373
- if [ -n "$git_branch" ]; then
374
- [ -n "$line2" ] && line2+=" "
375
-
376
- # Branch in magenta
377
- line2+=$(printf "\033[1;35m%s\033[0m" "$git_branch")
378
-
379
- # Git status indicator (after branch name)
380
- if [ -n "$git_status_indicator" ]; then
381
- line2+=$(printf " %s" "$git_status_indicator")
382
- fi
383
-
384
- # Arrow and tracking
385
- line2+=$(printf " \033[2;37m→\033[0m")
386
-
387
- if [ -n "$git_remote" ]; then
388
- # Tracking branch in blue
389
- line2+=$(printf " \033[1;34m%s\033[0m" "$git_remote")
390
-
391
- # Ahead/behind indicators
392
- if [ -n "$git_ahead" ] || [ -n "$git_behind" ]; then
393
- line2+=" "
394
- [ -n "$git_ahead" ] && line2+=$(printf "\033[0;32m↑%s\033[0m" "$git_ahead")
395
- if [ -n "$git_behind" ]; then
396
- [ -n "$git_ahead" ] && line2+=" "
397
- line2+=$(printf "\033[0;31m↓%s\033[0m" "$git_behind")
398
- fi
399
- fi
400
- else
401
- line2+=$(printf " \033[2;37m(no upstream)\033[0m")
402
- fi
403
- fi
404
-
405
- # GSD Update check (appended to line 2 if update available)
406
- gsd_update_suffix=""
407
- gsd_cache_file="$HOME/.claude/cache/gsd-update-check.json"
408
-
409
- # Check for GSD in project first, then global
410
- gsd_version_file=""
411
- if [ -n "$cwd" ] && [ -f "$cwd/.claude/get-shit-done/VERSION" ]; then
412
- gsd_version_file="$cwd/.claude/get-shit-done/VERSION"
413
- elif [ -f "$HOME/.claude/get-shit-done/VERSION" ]; then
414
- gsd_version_file="$HOME/.claude/get-shit-done/VERSION"
415
- fi
416
-
417
- if [ -n "$gsd_version_file" ]; then
418
- # Read installed version directly from the VERSION file we found
419
- installed_ver=$(cat "$gsd_version_file" 2>/dev/null | tr -d '[:space:]')
420
- [ -z "$installed_ver" ] && installed_ver="0.0.0"
421
-
422
- # Get latest version from cache (populated by SessionStart hook)
423
- if [ -f "$gsd_cache_file" ]; then
424
- latest_ver=$(jq -r '.latest // "unknown"' "$gsd_cache_file" 2>/dev/null)
425
-
426
- # Compare versions - append to line2 if they differ
427
- if [ "$latest_ver" != "unknown" ] && [ "$installed_ver" != "$latest_ver" ]; then
428
- gsd_update_suffix=$(printf " | GSD %s>%s" "$installed_ver" "$latest_ver")
429
- fi
430
- fi
431
- fi
432
-
433
- # Append GSD update to line2 if available
434
- line2+="$gsd_update_suffix"
435
-
436
- # Output the dashboard (2 lines, no trailing newline)
437
- printf "%s\033[K\n%s\033[K" "$line1" "$line2"