@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 +17 -37
- package/bin/install.js +51 -45
- 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
|
@@ -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}
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
221
|
-
const statuslineDest = path.join(targetDir, 'statusline.
|
|
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.
|
|
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}: `, (
|
|
313
|
-
|
|
314
|
-
rl.close();
|
|
283
|
+
rl.question(` Choice ${dim}[1]${reset}: `, (locAnswer) => {
|
|
284
|
+
const isGlobal = (locAnswer.trim() || '1') !== '2';
|
|
315
285
|
console.log('');
|
|
316
|
-
|
|
317
|
-
|
|
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": "
|
|
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"
|