@buckits/claude-statusline 2.3.3 β 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 +17 -37
- package/bin/install.js +50 -14
- package/package.json +2 -2
- package/statusline.js +406 -0
- package/statusline.sh +0 -437
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
<br>
|
|
11
11
|
|
|
12
|
-

|
|
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
|
-
|
|
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.
|
|
72
|
-
3.
|
|
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
|
-
###
|
|
96
|
+
### Compact (25 bars)
|
|
96
97
|
|
|
97
|
-

|
|
98
99
|
|
|
99
|
-
|
|
100
|
+
### Medium (38 bars)
|
|
100
101
|
|
|
101
|
-
|
|
102
|
+

|
|
102
103
|
|
|
103
|
-
|
|
104
|
+
### Full (50 bars)
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
### π΄ Approaching Limit (Red Zone)
|
|
108
|
-
|
|
109
|
-

|
|
110
|
-
|
|
111
|
-
Getting close to auto-compact. Consider wrapping up or starting fresh.
|
|
106
|
+

|
|
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
|
-
- **
|
|
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.
|
|
150
|
-
2. Make it executable: `chmod +x ~/.claude/statusline.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
@@ -134,10 +134,10 @@ function uninstall(isGlobal) {
|
|
|
134
134
|
let removed = false;
|
|
135
135
|
|
|
136
136
|
// Remove statusline.sh
|
|
137
|
-
const statuslinePath = path.join(targetDir, 'statusline.
|
|
137
|
+
const statuslinePath = path.join(targetDir, 'statusline.js');
|
|
138
138
|
if (fs.existsSync(statuslinePath)) {
|
|
139
139
|
fs.unlinkSync(statuslinePath);
|
|
140
|
-
console.log(` ${green}β${reset} Removed statusline.
|
|
140
|
+
console.log(` ${green}β${reset} Removed statusline.js`);
|
|
141
141
|
removed = true;
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -147,7 +147,7 @@ function uninstall(isGlobal) {
|
|
|
147
147
|
const settings = readSettings(settingsPath);
|
|
148
148
|
|
|
149
149
|
if (settings.statusLine && settings.statusLine.command &&
|
|
150
|
-
settings.statusLine.command.includes('statusline.
|
|
150
|
+
settings.statusLine.command.includes('statusline.js')) {
|
|
151
151
|
delete settings.statusLine;
|
|
152
152
|
writeSettings(settingsPath, settings);
|
|
153
153
|
console.log(` ${green}β${reset} Removed statusline from settings.json`);
|
|
@@ -175,7 +175,8 @@ function uninstall(isGlobal) {
|
|
|
175
175
|
/**
|
|
176
176
|
* Install statusline
|
|
177
177
|
*/
|
|
178
|
-
function install(isGlobal) {
|
|
178
|
+
function install(isGlobal, barWidth) {
|
|
179
|
+
barWidth = barWidth || 50;
|
|
179
180
|
const targetDir = isGlobal ? getGlobalDir() : getLocalDir();
|
|
180
181
|
const locationLabel = isGlobal
|
|
181
182
|
? targetDir.replace(os.homedir(), '~')
|
|
@@ -190,11 +191,11 @@ function install(isGlobal) {
|
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
// Copy statusline.sh
|
|
193
|
-
const statuslineSrc = path.join(__dirname, '..', 'statusline.
|
|
194
|
-
const statuslineDest = path.join(targetDir, 'statusline.
|
|
194
|
+
const statuslineSrc = path.join(__dirname, '..', 'statusline.js');
|
|
195
|
+
const statuslineDest = path.join(targetDir, 'statusline.js');
|
|
195
196
|
fs.copyFileSync(statuslineSrc, statuslineDest);
|
|
196
197
|
fs.chmodSync(statuslineDest, '755');
|
|
197
|
-
console.log(` ${green}β${reset} Installed statusline.
|
|
198
|
+
console.log(` ${green}β${reset} Installed statusline.js`);
|
|
198
199
|
|
|
199
200
|
// Update settings.json
|
|
200
201
|
const settingsPath = path.join(targetDir, 'settings.json');
|
|
@@ -221,7 +222,7 @@ function install(isGlobal) {
|
|
|
221
222
|
// Set new format
|
|
222
223
|
settings.statusLine = {
|
|
223
224
|
type: 'command',
|
|
224
|
-
command: statuslineDest
|
|
225
|
+
command: statuslineDest + ' --width ' + barWidth
|
|
225
226
|
};
|
|
226
227
|
|
|
227
228
|
writeSettings(settingsPath, settings);
|
|
@@ -249,7 +250,7 @@ function install(isGlobal) {
|
|
|
249
250
|
function promptLocation() {
|
|
250
251
|
if (!process.stdin.isTTY) {
|
|
251
252
|
console.log(` ${dim}Non-interactive mode, defaulting to global install${reset}\n`);
|
|
252
|
-
install(true);
|
|
253
|
+
install(true, 50);
|
|
253
254
|
return;
|
|
254
255
|
}
|
|
255
256
|
|
|
@@ -279,12 +280,47 @@ function promptLocation() {
|
|
|
279
280
|
This project only
|
|
280
281
|
`);
|
|
281
282
|
|
|
282
|
-
rl.question(` Choice ${dim}[1]${reset}: `, (
|
|
283
|
-
|
|
284
|
-
rl.close();
|
|
283
|
+
rl.question(` Choice ${dim}[1]${reset}: `, (locAnswer) => {
|
|
284
|
+
const isGlobal = (locAnswer.trim() || '1') !== '2';
|
|
285
285
|
console.log('');
|
|
286
|
-
|
|
287
|
-
|
|
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
|
+
}
|
|
288
324
|
});
|
|
289
325
|
}
|
|
290
326
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buckits/claude-statusline",
|
|
3
|
-
"version": "
|
|
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.
|
|
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"
|