@v1nvn/statusline 0.27.3 → 0.29.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 +78 -51
- package/dist/index.js +2024 -673
- package/dist/index.js.map +1 -1
- package/dist/render.mjs +1452 -0
- package/dist/render.mjs.map +1 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import {
|
|
5
|
-
import { Command, Option } from "commander";
|
|
4
|
+
import { delimiter, dirname, join, sep } from "node:path";
|
|
6
5
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
|
+
import { Command } from "commander";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
8
9
|
//#region ../core/dist/index.js
|
|
9
10
|
function parseQuietly(program, args, recover) {
|
|
10
11
|
try {
|
|
@@ -23,28 +24,1240 @@ function printUsageAndExit(program) {
|
|
|
23
24
|
}
|
|
24
25
|
Number.MAX_SAFE_INTEGER;
|
|
25
26
|
//#endregion
|
|
27
|
+
//#region src/render/git.ts
|
|
28
|
+
var BRANCH_HEAD = "# branch.head ";
|
|
29
|
+
var BRANCH_AB = /^# branch\.ab \+(\d+) -(\d+)$/;
|
|
30
|
+
function gitEnv(env) {
|
|
31
|
+
return {
|
|
32
|
+
HOME: env.home,
|
|
33
|
+
LC_ALL: "C",
|
|
34
|
+
PATH: process.env.PATH ?? ""
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function gitText(dir, args, env) {
|
|
38
|
+
const run = spawnSync("git", [
|
|
39
|
+
"-C",
|
|
40
|
+
dir,
|
|
41
|
+
...args
|
|
42
|
+
], {
|
|
43
|
+
encoding: "utf8",
|
|
44
|
+
env: gitEnv(env)
|
|
45
|
+
});
|
|
46
|
+
return run.status === 0 ? run.stdout : null;
|
|
47
|
+
}
|
|
48
|
+
function lineCount(text) {
|
|
49
|
+
return text === null || text === "" ? 0 : (text.match(/\n/g) ?? []).length;
|
|
50
|
+
}
|
|
51
|
+
function count(text) {
|
|
52
|
+
const parsed = text === null ? NaN : Number(text.trim());
|
|
53
|
+
return Number.isFinite(parsed) ? Math.trunc(parsed) : 0;
|
|
54
|
+
}
|
|
55
|
+
function readGit(dir, env) {
|
|
56
|
+
const facts = {
|
|
57
|
+
ahead: 0,
|
|
58
|
+
behind: 0,
|
|
59
|
+
branch: "",
|
|
60
|
+
modified: 0,
|
|
61
|
+
staged: 0,
|
|
62
|
+
stashes: 0,
|
|
63
|
+
untracked: 0
|
|
64
|
+
};
|
|
65
|
+
if (dir === "") return facts;
|
|
66
|
+
if (spawnSync("git", [
|
|
67
|
+
"-C",
|
|
68
|
+
dir,
|
|
69
|
+
"rev-parse",
|
|
70
|
+
"--git-dir"
|
|
71
|
+
], {
|
|
72
|
+
encoding: "utf8",
|
|
73
|
+
env: gitEnv(env)
|
|
74
|
+
}).status !== 0) return facts;
|
|
75
|
+
const status = gitText(dir, [
|
|
76
|
+
"status",
|
|
77
|
+
"--porcelain=v2",
|
|
78
|
+
"--branch"
|
|
79
|
+
], env);
|
|
80
|
+
for (const line of (status ?? "").split("\n")) {
|
|
81
|
+
if (line.startsWith(BRANCH_HEAD)) {
|
|
82
|
+
facts.branch = line.slice(14);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const ab = BRANCH_AB.exec(line);
|
|
86
|
+
if (ab !== null) {
|
|
87
|
+
facts.ahead = count(ab[1]);
|
|
88
|
+
facts.behind = count(ab[2]);
|
|
89
|
+
} else if ((line.startsWith("1 ") || line.startsWith("2 ")) && line.length >= 4) {
|
|
90
|
+
if (line[2] !== ".") facts.staged += 1;
|
|
91
|
+
if (line[3] !== ".") facts.modified += 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (facts.branch === "(detached)") facts.branch = "";
|
|
95
|
+
facts.untracked = lineCount(gitText(dir, [
|
|
96
|
+
"ls-files",
|
|
97
|
+
"--others",
|
|
98
|
+
"--exclude-standard"
|
|
99
|
+
], env));
|
|
100
|
+
facts.stashes = lineCount(gitText(dir, ["stash", "list"], env));
|
|
101
|
+
return facts;
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/render/items.ts
|
|
105
|
+
var ITEMS = [
|
|
106
|
+
{
|
|
107
|
+
alternatives: [
|
|
108
|
+
"plain",
|
|
109
|
+
"block",
|
|
110
|
+
"pill",
|
|
111
|
+
"zen"
|
|
112
|
+
],
|
|
113
|
+
default: "plain",
|
|
114
|
+
item: "model"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
alternatives: [
|
|
118
|
+
"plain",
|
|
119
|
+
"dim",
|
|
120
|
+
"hidden"
|
|
121
|
+
],
|
|
122
|
+
default: "plain",
|
|
123
|
+
item: "effort"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
alternatives: ["none", "pills"],
|
|
127
|
+
default: "none",
|
|
128
|
+
item: "state"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
alternatives: [
|
|
132
|
+
"init",
|
|
133
|
+
"full",
|
|
134
|
+
"tail",
|
|
135
|
+
"base",
|
|
136
|
+
"icon"
|
|
137
|
+
],
|
|
138
|
+
default: "init",
|
|
139
|
+
item: "cwd"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
alternatives: [
|
|
143
|
+
"initials",
|
|
144
|
+
"full",
|
|
145
|
+
"last",
|
|
146
|
+
"icon",
|
|
147
|
+
"none"
|
|
148
|
+
],
|
|
149
|
+
default: "initials",
|
|
150
|
+
item: "branch"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
alternatives: [
|
|
154
|
+
"counts",
|
|
155
|
+
"icons",
|
|
156
|
+
"none"
|
|
157
|
+
],
|
|
158
|
+
default: "counts",
|
|
159
|
+
item: "status"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
alternatives: ["none", "arrows"],
|
|
163
|
+
default: "none",
|
|
164
|
+
item: "ahead"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
alternatives: ["none", "badge"],
|
|
168
|
+
default: "none",
|
|
169
|
+
item: "pr"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
alternatives: [
|
|
173
|
+
"flat",
|
|
174
|
+
"gauge",
|
|
175
|
+
"percent",
|
|
176
|
+
"none",
|
|
177
|
+
"flat6",
|
|
178
|
+
"flat4"
|
|
179
|
+
],
|
|
180
|
+
default: "flat",
|
|
181
|
+
item: "bar"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
alternatives: [
|
|
185
|
+
"full",
|
|
186
|
+
"compact",
|
|
187
|
+
"free",
|
|
188
|
+
"none"
|
|
189
|
+
],
|
|
190
|
+
default: "full",
|
|
191
|
+
item: "tokens"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
alternatives: [
|
|
195
|
+
"hit",
|
|
196
|
+
"coldin",
|
|
197
|
+
"fuse",
|
|
198
|
+
"none"
|
|
199
|
+
],
|
|
200
|
+
default: "hit",
|
|
201
|
+
item: "cache"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
alternatives: [
|
|
205
|
+
"plain",
|
|
206
|
+
"burn",
|
|
207
|
+
"none"
|
|
208
|
+
],
|
|
209
|
+
default: "plain",
|
|
210
|
+
item: "cost"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
alternatives: [
|
|
214
|
+
"clock",
|
|
215
|
+
"hours",
|
|
216
|
+
"none"
|
|
217
|
+
],
|
|
218
|
+
default: "clock",
|
|
219
|
+
item: "duration"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
alternatives: ["none", "diffstat"],
|
|
223
|
+
default: "none",
|
|
224
|
+
item: "lines"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
alternatives: ["none", "strip"],
|
|
228
|
+
default: "none",
|
|
229
|
+
item: "rate"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
alternatives: [
|
|
233
|
+
"plain",
|
|
234
|
+
"dots",
|
|
235
|
+
"dim",
|
|
236
|
+
"bare"
|
|
237
|
+
],
|
|
238
|
+
default: "plain",
|
|
239
|
+
item: "style"
|
|
240
|
+
}
|
|
241
|
+
];
|
|
242
|
+
var DEFAULT_LAYOUT = "{model effort state} {cwd branch status ahead pr} {bar tokens cache} {cost} {duration} {lines} {rate}";
|
|
243
|
+
var BY_ITEM = new Map(ITEMS.map((spec) => [spec.item, spec]));
|
|
244
|
+
function specFor(item) {
|
|
245
|
+
return BY_ITEM.get(item);
|
|
246
|
+
}
|
|
247
|
+
var ITEM_IDS = ITEMS.map((spec) => spec.item);
|
|
248
|
+
var DEFAULT_PICKS = Object.fromEntries(ITEMS.map(({ default: alt, item }) => [item, alt]));
|
|
249
|
+
var RUNG_ORDERS = {
|
|
250
|
+
bar: [
|
|
251
|
+
"flat",
|
|
252
|
+
"flat6",
|
|
253
|
+
"flat4",
|
|
254
|
+
"percent",
|
|
255
|
+
"none"
|
|
256
|
+
],
|
|
257
|
+
branch: [
|
|
258
|
+
"icon",
|
|
259
|
+
"full",
|
|
260
|
+
"initials",
|
|
261
|
+
"last",
|
|
262
|
+
"none"
|
|
263
|
+
],
|
|
264
|
+
cache: [
|
|
265
|
+
"hit",
|
|
266
|
+
"coldin",
|
|
267
|
+
"none"
|
|
268
|
+
],
|
|
269
|
+
cwd: [
|
|
270
|
+
"icon",
|
|
271
|
+
"full",
|
|
272
|
+
"init",
|
|
273
|
+
"tail",
|
|
274
|
+
"base"
|
|
275
|
+
],
|
|
276
|
+
duration: [
|
|
277
|
+
"clock",
|
|
278
|
+
"hours",
|
|
279
|
+
"none"
|
|
280
|
+
],
|
|
281
|
+
effort: [
|
|
282
|
+
"plain",
|
|
283
|
+
"dim",
|
|
284
|
+
"hidden"
|
|
285
|
+
],
|
|
286
|
+
status: [
|
|
287
|
+
"counts",
|
|
288
|
+
"icons",
|
|
289
|
+
"none"
|
|
290
|
+
],
|
|
291
|
+
tokens: [
|
|
292
|
+
"full",
|
|
293
|
+
"free",
|
|
294
|
+
"compact",
|
|
295
|
+
"none"
|
|
296
|
+
]
|
|
297
|
+
};
|
|
298
|
+
var FULL_STEPS = [
|
|
299
|
+
["duration", "none"],
|
|
300
|
+
["cache", "none"],
|
|
301
|
+
["tokens", "compact"],
|
|
302
|
+
["bar", "flat6"],
|
|
303
|
+
["status", "none"],
|
|
304
|
+
["branch", "initials"],
|
|
305
|
+
["cwd", "init"],
|
|
306
|
+
["branch", "last"],
|
|
307
|
+
["bar", "flat4"],
|
|
308
|
+
["model", "strip"],
|
|
309
|
+
["bar", "percent"],
|
|
310
|
+
["branch", "none"],
|
|
311
|
+
["cwd", "tail"],
|
|
312
|
+
["effort", "hidden"],
|
|
313
|
+
["cwd", "base"],
|
|
314
|
+
["tokens", "none"]
|
|
315
|
+
];
|
|
316
|
+
var L1_STEPS = [
|
|
317
|
+
["status", "none"],
|
|
318
|
+
["branch", "initials"],
|
|
319
|
+
["cwd", "init"],
|
|
320
|
+
["branch", "last"],
|
|
321
|
+
["model", "strip"],
|
|
322
|
+
["branch", "none"],
|
|
323
|
+
["cwd", "tail"],
|
|
324
|
+
["effort", "hidden"],
|
|
325
|
+
["cwd", "base"]
|
|
326
|
+
];
|
|
327
|
+
var L2_STEPS = [
|
|
328
|
+
["duration", "none"],
|
|
329
|
+
["cache", "none"],
|
|
330
|
+
["tokens", "compact"],
|
|
331
|
+
["bar", "flat6"],
|
|
332
|
+
["bar", "flat4"],
|
|
333
|
+
["tokens", "none"],
|
|
334
|
+
["bar", "percent"]
|
|
335
|
+
];
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/render/layout.ts
|
|
338
|
+
function parseClusters(layout) {
|
|
339
|
+
const clusters = [];
|
|
340
|
+
let words = [];
|
|
341
|
+
let word = "";
|
|
342
|
+
let open = false;
|
|
343
|
+
function pushWord() {
|
|
344
|
+
if (word !== "") {
|
|
345
|
+
words.push(word);
|
|
346
|
+
word = "";
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
for (const c of layout) if (c === "{") {
|
|
350
|
+
if (open) throw new Error(`layout '${layout}': '{' inside a cluster`);
|
|
351
|
+
open = true;
|
|
352
|
+
words = [];
|
|
353
|
+
} else if (c === "}") {
|
|
354
|
+
if (!open) throw new Error(`layout '${layout}': '}' outside a cluster`);
|
|
355
|
+
pushWord();
|
|
356
|
+
open = false;
|
|
357
|
+
if (words.length > 0) clusters.push(words);
|
|
358
|
+
} else if (c === " ") {
|
|
359
|
+
if (open) pushWord();
|
|
360
|
+
else if (word !== "") throw new Error(`layout '${layout}': '${word}' sits outside a cluster`);
|
|
361
|
+
} else if (/[a-z0-9]/.test(c)) word += c;
|
|
362
|
+
else throw new Error(`layout '${layout}': '${c}' is not layout grammar (braces, item ids, spaces)`);
|
|
363
|
+
if (open) throw new Error(`layout '${layout}': unterminated cluster`);
|
|
364
|
+
if (word !== "") throw new Error(`layout '${layout}': '${word}' sits outside a cluster`);
|
|
365
|
+
if (clusters.length === 0) throw new Error(`layout '${layout}': no clusters`);
|
|
366
|
+
return clusters;
|
|
367
|
+
}
|
|
368
|
+
function layoutItemsOf(layout) {
|
|
369
|
+
const items = [];
|
|
370
|
+
for (const cluster of parseClusters(layout)) for (const item of cluster) if (!items.includes(item)) items.push(item);
|
|
371
|
+
return items;
|
|
372
|
+
}
|
|
373
|
+
function layoutItems(layout, valid) {
|
|
374
|
+
const known = new Set(valid);
|
|
375
|
+
const items = layoutItemsOf(layout);
|
|
376
|
+
for (const item of items) if (!known.has(item)) throw new Error(`unknown layout item '${item}' — valid items: ${valid.join(" ")}`);
|
|
377
|
+
return items;
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/render/jq.ts
|
|
381
|
+
function isRecord(value) {
|
|
382
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
383
|
+
}
|
|
384
|
+
function orElse(value, fallback) {
|
|
385
|
+
return value === void 0 || value === null || value === false ? fallback : value;
|
|
386
|
+
}
|
|
387
|
+
function jqText(value) {
|
|
388
|
+
if (typeof value === "string") return value;
|
|
389
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
390
|
+
return JSON.stringify(value);
|
|
391
|
+
}
|
|
392
|
+
function tsvEscape(text) {
|
|
393
|
+
return text.replace(/\\/g, "\\\\").replace(/\t/g, "\\t").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region src/render/payload.ts
|
|
397
|
+
function member(source, key) {
|
|
398
|
+
return isRecord(source) ? source[key] : void 0;
|
|
399
|
+
}
|
|
400
|
+
function field$1(source, path) {
|
|
401
|
+
let current = source;
|
|
402
|
+
for (const key of path.split(".")) current = member(current, key);
|
|
403
|
+
return current;
|
|
404
|
+
}
|
|
405
|
+
function textOf(value) {
|
|
406
|
+
if (typeof value === "string") return value;
|
|
407
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
408
|
+
return "";
|
|
409
|
+
}
|
|
410
|
+
function scalar(source, path, fallback) {
|
|
411
|
+
return textOf(orElse(field$1(source, path), fallback));
|
|
412
|
+
}
|
|
413
|
+
function numberField(source, path) {
|
|
414
|
+
return Number(scalar(source, path, "0"));
|
|
415
|
+
}
|
|
416
|
+
function pctField(source) {
|
|
417
|
+
const intPart = scalar(source, "context_window.used_percentage", "0").split(".")[0];
|
|
418
|
+
return intPart === "" ? 0 : Number(intPart);
|
|
419
|
+
}
|
|
420
|
+
var RATE_LIMITS = [
|
|
421
|
+
["5h", "five_hour"],
|
|
422
|
+
["7d", "seven_day"],
|
|
423
|
+
["spend", "spend_limit"]
|
|
424
|
+
];
|
|
425
|
+
function rateRows(source) {
|
|
426
|
+
const limits = field$1(source, "rate_limits");
|
|
427
|
+
const rows = [];
|
|
428
|
+
for (const [label, key] of RATE_LIMITS) {
|
|
429
|
+
const limit = member(limits, key);
|
|
430
|
+
if (!isRecord(limit) || Object.keys(limit).length === 0) continue;
|
|
431
|
+
rows.push({
|
|
432
|
+
label,
|
|
433
|
+
pctText: scalar(limit, "used_percentage", "0"),
|
|
434
|
+
resets: Number(scalar(limit, "resets_at", "0"))
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return rows;
|
|
438
|
+
}
|
|
439
|
+
function parseRow(payload) {
|
|
440
|
+
const root = JSON.parse(payload);
|
|
441
|
+
const hitText = scalar(root, "prompt_cache.hit_ratio", "");
|
|
442
|
+
return {
|
|
443
|
+
agent: scalar(root, "agent.name", ""),
|
|
444
|
+
cost: numberField(root, "cost.total_cost_usd"),
|
|
445
|
+
ctxSize: numberField(root, "context_window.context_window_size"),
|
|
446
|
+
dir: scalar(root, "workspace.current_dir", ""),
|
|
447
|
+
durationMs: numberField(root, "cost.total_duration_ms"),
|
|
448
|
+
effort: scalar(root, "effort.level", ""),
|
|
449
|
+
expires: numberField(root, "prompt_cache.expires_at"),
|
|
450
|
+
hit: hitText === "" ? null : Number(hitText),
|
|
451
|
+
la: numberField(root, "cost.total_lines_added"),
|
|
452
|
+
lr: numberField(root, "cost.total_lines_removed"),
|
|
453
|
+
model: scalar(root, "model.display_name", ""),
|
|
454
|
+
pct: pctField(root),
|
|
455
|
+
prn: scalar(root, "pr.number", ""),
|
|
456
|
+
prs: scalar(root, "pr.review_state", ""),
|
|
457
|
+
rateRows: rateRows(root),
|
|
458
|
+
think: scalar(root, "thinking.enabled", "false") === "true",
|
|
459
|
+
tokens: numberField(root, "context_window.total_input_tokens"),
|
|
460
|
+
ttl: scalar(root, "prompt_cache.ttl", ""),
|
|
461
|
+
vim: scalar(root, "vim.mode", ""),
|
|
462
|
+
warm: scalar(root, "prompt_cache.warm", "false") === "true",
|
|
463
|
+
wt: scalar(root, "worktree.name", ""),
|
|
464
|
+
styleName: scalar(root, "output_style.name", "")
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/render/awk.ts
|
|
469
|
+
var TEN = 10n;
|
|
470
|
+
function exactParts(ax) {
|
|
471
|
+
const view = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(8));
|
|
472
|
+
view.setFloat64(0, ax);
|
|
473
|
+
const hi = view.getUint32(0);
|
|
474
|
+
const lo = view.getUint32(4);
|
|
475
|
+
const biased = hi >>> 20 & 2047;
|
|
476
|
+
const frac = BigInt(hi & 1048575) << 32n | BigInt(lo);
|
|
477
|
+
if (biased === 0) return {
|
|
478
|
+
exp: -1074,
|
|
479
|
+
mant: frac
|
|
480
|
+
};
|
|
481
|
+
return {
|
|
482
|
+
exp: biased - 1075,
|
|
483
|
+
mant: frac | 1n << 52n
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function roundScaled(ax, d) {
|
|
487
|
+
const { exp, mant } = exactParts(ax);
|
|
488
|
+
const pow10 = TEN ** BigInt(d);
|
|
489
|
+
const num = exp >= 0 ? (mant << BigInt(exp)) * pow10 : mant * pow10;
|
|
490
|
+
const den = exp >= 0 ? 1n : 1n << BigInt(-exp);
|
|
491
|
+
const q = num / den;
|
|
492
|
+
const twice = num % den * 2n;
|
|
493
|
+
if (twice > den || twice === den && q % 2n === 1n) return q + 1n;
|
|
494
|
+
return q;
|
|
495
|
+
}
|
|
496
|
+
function fmtFixed(x, d) {
|
|
497
|
+
const q = roundScaled(Math.abs(x), d);
|
|
498
|
+
const sign = x < 0 && q !== 0n ? "-" : "";
|
|
499
|
+
const digits = q.toString();
|
|
500
|
+
if (d === 0) return `${sign}${digits}`;
|
|
501
|
+
return `${sign}${digits.length <= d ? "0" : digits.slice(0, digits.length - d)}.${digits.padStart(d + 1, "0").slice(-d)}`;
|
|
502
|
+
}
|
|
503
|
+
function pad2(n) {
|
|
504
|
+
return String(n).padStart(2, "0");
|
|
505
|
+
}
|
|
506
|
+
function fmtK(n, d) {
|
|
507
|
+
return `${fmtFixed(n / 1e3, d)}k`;
|
|
508
|
+
}
|
|
509
|
+
function fmtM(n) {
|
|
510
|
+
return `${fmtFixed(n / 1e6, 0)}M`;
|
|
511
|
+
}
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/render/segments.ts
|
|
514
|
+
var BLUE = "\x1B[34m";
|
|
515
|
+
var CYAN$1 = "\x1B[36m";
|
|
516
|
+
var DIM = "\x1B[2m";
|
|
517
|
+
var GREEN$1 = "\x1B[32m";
|
|
518
|
+
var PURPLE = "\x1B[35m";
|
|
519
|
+
var RED$1 = "\x1B[31m";
|
|
520
|
+
var RESET$1 = "\x1B[0m";
|
|
521
|
+
var YELLOW$1 = "\x1B[33m";
|
|
522
|
+
var BOLD = "\x1B[1m";
|
|
523
|
+
var GRAY = "\x1B[38;2;68;71;90m";
|
|
524
|
+
var MARK = "\x1B[1;97m";
|
|
525
|
+
var SHORT_AT = 15;
|
|
526
|
+
var PART = [
|
|
527
|
+
"▏",
|
|
528
|
+
"▎",
|
|
529
|
+
"▍",
|
|
530
|
+
"▌",
|
|
531
|
+
"▋",
|
|
532
|
+
"▊",
|
|
533
|
+
"▉",
|
|
534
|
+
"█"
|
|
535
|
+
];
|
|
536
|
+
var HEAT = [
|
|
537
|
+
"\x1B[38;2;48;242;36m",
|
|
538
|
+
"\x1B[38;2;63;242;36m",
|
|
539
|
+
"\x1B[38;2;79;242;36m",
|
|
540
|
+
"\x1B[38;2;95;242;36m",
|
|
541
|
+
"\x1B[38;2;111;242;36m",
|
|
542
|
+
"\x1B[38;2;126;242;36m",
|
|
543
|
+
"\x1B[38;2;142;242;36m",
|
|
544
|
+
"\x1B[38;2;158;242;36m",
|
|
545
|
+
"\x1B[38;2;173;242;36m",
|
|
546
|
+
"\x1B[38;2;189;242;36m",
|
|
547
|
+
"\x1B[38;2;205;242;36m",
|
|
548
|
+
"\x1B[38;2;220;242;36m",
|
|
549
|
+
"\x1B[38;2;236;242;36m",
|
|
550
|
+
"\x1B[38;2;242;232;36m",
|
|
551
|
+
"\x1B[38;2;242;216;36m",
|
|
552
|
+
"\x1B[38;2;242;200;36m",
|
|
553
|
+
"\x1B[38;2;242;185;36m",
|
|
554
|
+
"\x1B[38;2;242;169;36m",
|
|
555
|
+
"\x1B[38;2;242;153;36m",
|
|
556
|
+
"\x1B[38;2;242;138;36m",
|
|
557
|
+
"\x1B[38;2;242;122;36m",
|
|
558
|
+
"\x1B[38;2;242;106;36m",
|
|
559
|
+
"\x1B[38;2;242;91;36m",
|
|
560
|
+
"\x1B[38;2;242;75;36m",
|
|
561
|
+
"\x1B[38;2;242;59;36m",
|
|
562
|
+
"\x1B[38;2;242;44;36m"
|
|
563
|
+
];
|
|
564
|
+
function trunc(n) {
|
|
565
|
+
return Math.trunc(n);
|
|
566
|
+
}
|
|
567
|
+
function modelPlain({ model }) {
|
|
568
|
+
return model === "" ? "" : `${CYAN$1}${model}${RESET$1}`;
|
|
569
|
+
}
|
|
570
|
+
function modelBlock({ model }) {
|
|
571
|
+
return model === "" ? "" : `\x1b[48;5;61m\x1b[38;5;231m ${model} \x1b[0m`;
|
|
572
|
+
}
|
|
573
|
+
function modelPill({ model }) {
|
|
574
|
+
return model === "" ? "" : `\x1b[48;5;61m\x1b[38;5;231m\u{e0b6} ${model} \u{e0b4}\x1b[0m`;
|
|
575
|
+
}
|
|
576
|
+
function modelZen({ model }) {
|
|
577
|
+
return model === "" ? "" : `${DIM}${model.replace(/[A-Z]/g, (c) => c.toLowerCase())}${RESET$1}`;
|
|
578
|
+
}
|
|
579
|
+
function effortPlain({ row }) {
|
|
580
|
+
return row.effort === "" ? "" : `${CYAN$1}${row.effort}${RESET$1}`;
|
|
581
|
+
}
|
|
582
|
+
function effortDim({ row }) {
|
|
583
|
+
return row.effort === "" ? "" : `${DIM}${row.effort}${RESET$1}`;
|
|
584
|
+
}
|
|
585
|
+
function statePill(label, bg, fg) {
|
|
586
|
+
return `\x1b[48;5;${bg}m\x1b[38;5;${fg}m\u{e0b6} ${label} \u{e0b4}\x1b[0m`;
|
|
587
|
+
}
|
|
588
|
+
function statePills({ row }) {
|
|
589
|
+
let out = "";
|
|
590
|
+
if (row.vim !== "") out = row.vim === "INSERT" ? statePill(row.vim, 97, 16) : statePill(row.vim, 240, 231);
|
|
591
|
+
if (row.think) out += ` ${statePill("THINK", 66, 16)}`;
|
|
592
|
+
if (row.agent !== "") out += ` ${statePill(row.agent, 60, 231)}`;
|
|
593
|
+
if (row.wt !== "") out += ` ${statePill(`wt:${row.wt}`, 131, 231)}`;
|
|
594
|
+
if (row.styleName !== "" && row.styleName !== "default") out += ` ${statePill(row.styleName, 95, 16)}`;
|
|
595
|
+
return out;
|
|
596
|
+
}
|
|
597
|
+
function splitPath(raw, home) {
|
|
598
|
+
let p = raw;
|
|
599
|
+
if (home !== "" && p.startsWith(home)) p = `~${p.slice(home.length)}`;
|
|
600
|
+
return {
|
|
601
|
+
lead: p.startsWith("/") ? "/" : "",
|
|
602
|
+
parts: p.split("/").filter((part) => part !== ""),
|
|
603
|
+
text: p
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function pathInit(raw, home) {
|
|
607
|
+
const { lead, parts, text } = splitPath(raw, home);
|
|
608
|
+
const n = parts.length;
|
|
609
|
+
if (n <= 2) return text;
|
|
610
|
+
let out = `${lead}${parts[0]}`;
|
|
611
|
+
for (let i = 1; i < n - 1; i++) {
|
|
612
|
+
const ini = parts[i].startsWith(".") ? parts[i].slice(0, 2) : parts[i].slice(0, 1);
|
|
613
|
+
out += `/${ini}`;
|
|
614
|
+
}
|
|
615
|
+
return `${out}/${parts[n - 1]}`;
|
|
616
|
+
}
|
|
617
|
+
function pathTail(raw, home, lim) {
|
|
618
|
+
const { lead, parts, text } = splitPath(raw, home);
|
|
619
|
+
const cnt = parts.length;
|
|
620
|
+
if (cnt <= lim || cnt <= 1) return text;
|
|
621
|
+
let out = `${lead}${parts[0]}/…`;
|
|
622
|
+
for (let i = cnt - lim; i < cnt; i++) out += `/${parts[i]}`;
|
|
623
|
+
return out;
|
|
624
|
+
}
|
|
625
|
+
function cwdInit({ home, row }) {
|
|
626
|
+
const { text } = splitPath(row.dir, home);
|
|
627
|
+
return Buffer.byteLength(text, "utf8") > SHORT_AT ? pathInit(row.dir, home) : text;
|
|
628
|
+
}
|
|
629
|
+
function cwdFull({ home, row }) {
|
|
630
|
+
return splitPath(row.dir, home).text;
|
|
631
|
+
}
|
|
632
|
+
function cwdTail({ home, row }) {
|
|
633
|
+
return pathTail(row.dir, home, 1);
|
|
634
|
+
}
|
|
635
|
+
function baseName(dir) {
|
|
636
|
+
return dir.slice(dir.lastIndexOf("/") + 1);
|
|
637
|
+
}
|
|
638
|
+
function cwdBase({ row }) {
|
|
639
|
+
return baseName(row.dir);
|
|
640
|
+
}
|
|
641
|
+
function cwdIcon({ row }) {
|
|
642
|
+
return `\u{f07b} ${baseName(row.dir)}`;
|
|
643
|
+
}
|
|
644
|
+
function branchInitials({ git }) {
|
|
645
|
+
const branch = git.branch;
|
|
646
|
+
if (branch === "") return "";
|
|
647
|
+
if (Buffer.byteLength(branch, "utf8") <= SHORT_AT) return branch;
|
|
648
|
+
const parts = branch.split("/").filter((part) => part !== "");
|
|
649
|
+
if (parts.length <= 1) return branch;
|
|
650
|
+
let out = "";
|
|
651
|
+
for (let i = 0; i < parts.length - 1; i++) out += `${parts[i].slice(0, 1)}/`;
|
|
652
|
+
return `${out}${parts[parts.length - 1]}`;
|
|
653
|
+
}
|
|
654
|
+
function branchFull({ git }) {
|
|
655
|
+
return git.branch;
|
|
656
|
+
}
|
|
657
|
+
function branchLast({ git }) {
|
|
658
|
+
return git.branch.slice(git.branch.lastIndexOf("/") + 1);
|
|
659
|
+
}
|
|
660
|
+
function branchIcon({ git }) {
|
|
661
|
+
return git.branch === "" ? "" : `\u{e0a0} ${git.branch}`;
|
|
662
|
+
}
|
|
663
|
+
function statusCounts({ git }) {
|
|
664
|
+
if (git.branch === "") return "";
|
|
665
|
+
let c = "";
|
|
666
|
+
if (git.staged > 0) c = `${GREEN$1}+${git.staged}${RESET$1}`;
|
|
667
|
+
if (git.modified > 0) c = `${c} ${YELLOW$1}~${git.modified}${RESET$1}`;
|
|
668
|
+
return c;
|
|
669
|
+
}
|
|
670
|
+
function statusIcons({ git }) {
|
|
671
|
+
if (git.branch === "") return "";
|
|
672
|
+
let out = "";
|
|
673
|
+
let sep = "";
|
|
674
|
+
if (git.staged > 0) {
|
|
675
|
+
out += `${GREEN$1}●${git.staged}${RESET$1}`;
|
|
676
|
+
sep = " ";
|
|
677
|
+
}
|
|
678
|
+
if (git.modified > 0) {
|
|
679
|
+
out += `${sep}${YELLOW$1}✎${git.modified}${RESET$1}`;
|
|
680
|
+
sep = " ";
|
|
681
|
+
}
|
|
682
|
+
if (git.untracked > 0) {
|
|
683
|
+
out += `${sep}${CYAN$1}+${git.untracked}${RESET$1}`;
|
|
684
|
+
sep = " ";
|
|
685
|
+
}
|
|
686
|
+
if (git.stashes > 0) out += `${sep}${PURPLE}⚑${git.stashes}${RESET$1}`;
|
|
687
|
+
return out;
|
|
688
|
+
}
|
|
689
|
+
function aheadArrows({ git }) {
|
|
690
|
+
let out = "";
|
|
691
|
+
if (git.ahead > 0) out = `${GREEN$1}↑${git.ahead}${RESET$1}`;
|
|
692
|
+
if (git.behind > 0) out += ` ${RED$1}↓${git.behind}${RESET$1}`;
|
|
693
|
+
return out;
|
|
694
|
+
}
|
|
695
|
+
function prBadge({ row }) {
|
|
696
|
+
if (row.prn === "") return "";
|
|
697
|
+
let col;
|
|
698
|
+
let mark;
|
|
699
|
+
switch (row.prs) {
|
|
700
|
+
case "approved":
|
|
701
|
+
col = GREEN$1;
|
|
702
|
+
mark = "✓";
|
|
703
|
+
break;
|
|
704
|
+
case "changes_requested":
|
|
705
|
+
col = RED$1;
|
|
706
|
+
mark = "✗";
|
|
707
|
+
break;
|
|
708
|
+
case "pending":
|
|
709
|
+
col = YELLOW$1;
|
|
710
|
+
mark = "⏳";
|
|
711
|
+
break;
|
|
712
|
+
default:
|
|
713
|
+
col = "\x1B[90m";
|
|
714
|
+
mark = "◌";
|
|
715
|
+
}
|
|
716
|
+
return `${BLUE}#${row.prn}${RESET$1} ${col}${mark} ${row.prs}${RESET$1}`;
|
|
717
|
+
}
|
|
718
|
+
function barFlatAt(input, w) {
|
|
719
|
+
const pct = input.row.pct;
|
|
720
|
+
let f = trunc(pct * w / 100);
|
|
721
|
+
if (f > w) f = w;
|
|
722
|
+
return `${pct >= 90 ? RED$1 : pct >= 70 ? YELLOW$1 : GREEN$1}${"█".repeat(f)}${"░".repeat(w - f)}${RESET$1}`;
|
|
723
|
+
}
|
|
724
|
+
function barFlat(input) {
|
|
725
|
+
return barFlatAt(input, 10);
|
|
726
|
+
}
|
|
727
|
+
function barGauge({ row }) {
|
|
728
|
+
const pct = row.pct;
|
|
729
|
+
const p = Math.max(0, Math.min(100, pct));
|
|
730
|
+
const full = trunc(p * 26 / 100);
|
|
731
|
+
const idx = trunc(p * 26 % 100 * 8 / 100);
|
|
732
|
+
const lead = p >= 50 ? `\x1b[38;2;242;${trunc(4845 * (15e3 + 1683 * (100 - p)) / 2e6)};36m` : `\x1b[38;2;${trunc(4845 * (185e3 - 1683 * (100 - p)) / 2e6)};242;36m`;
|
|
733
|
+
const moon = [
|
|
734
|
+
"○",
|
|
735
|
+
"◔",
|
|
736
|
+
"◑",
|
|
737
|
+
"◕",
|
|
738
|
+
"●"
|
|
739
|
+
][Math.min(trunc(p / 20), 4)];
|
|
740
|
+
let bar = "";
|
|
741
|
+
for (let i = 0; i < 26; i++) {
|
|
742
|
+
let col;
|
|
743
|
+
let cell;
|
|
744
|
+
if (i === 20) {
|
|
745
|
+
col = MARK;
|
|
746
|
+
cell = i <= full ? "┃" : "│";
|
|
747
|
+
} else if (i <= full) {
|
|
748
|
+
col = HEAT[i];
|
|
749
|
+
cell = i === full ? PART[idx] : "█";
|
|
750
|
+
} else {
|
|
751
|
+
col = GRAY;
|
|
752
|
+
cell = "░";
|
|
753
|
+
}
|
|
754
|
+
bar += `${col}${cell}`;
|
|
755
|
+
}
|
|
756
|
+
return `${moon} ${lead}${bar}${RESET$1} ${MARK}${pct}%${RESET$1}`;
|
|
757
|
+
}
|
|
758
|
+
function tokensFull({ row }) {
|
|
759
|
+
const t = row.tokens >= 1e3 ? fmtK(row.tokens, 1) : String(row.tokens);
|
|
760
|
+
let c = String(row.ctxSize);
|
|
761
|
+
if (row.ctxSize >= 1e6) c = fmtM(row.ctxSize);
|
|
762
|
+
else if (row.ctxSize >= 1e3) c = fmtK(row.ctxSize, 0);
|
|
763
|
+
return `${t}/${c}`;
|
|
764
|
+
}
|
|
765
|
+
function tokensCompact({ row }) {
|
|
766
|
+
return row.tokens >= 1e3 ? fmtK(row.tokens, 0) : String(row.tokens);
|
|
767
|
+
}
|
|
768
|
+
function tokensFree({ row }) {
|
|
769
|
+
const free = row.ctxSize - row.tokens;
|
|
770
|
+
return `${DIM}${free >= 1e3 ? fmtK(free, 0) : String(free)} free${RESET$1}`;
|
|
771
|
+
}
|
|
772
|
+
function cacheHitPct(row) {
|
|
773
|
+
return row.hit === null ? null : trunc(row.hit * 100);
|
|
774
|
+
}
|
|
775
|
+
function cacheHCol(hp) {
|
|
776
|
+
return hp >= 90 ? GREEN$1 : hp >= 50 ? YELLOW$1 : RED$1;
|
|
777
|
+
}
|
|
778
|
+
function cacheHit({ row }) {
|
|
779
|
+
const hp = cacheHitPct(row);
|
|
780
|
+
return hp === null ? "" : `${cacheHCol(hp)}⚡${hp}%${RESET$1}`;
|
|
781
|
+
}
|
|
782
|
+
function cacheColdin({ now, row }) {
|
|
783
|
+
const hp = cacheHitPct(row);
|
|
784
|
+
if (hp === null) return "";
|
|
785
|
+
if (row.warm && row.expires > now) {
|
|
786
|
+
const mins = trunc((row.expires - now) / 60);
|
|
787
|
+
return `${cacheHCol(hp)}⚡${hp}%${RESET$1} ${DIM}· cold in ${mins}m${RESET$1}`;
|
|
788
|
+
}
|
|
789
|
+
return `❄ ${DIM}cold · ${hp}%${RESET$1}`;
|
|
790
|
+
}
|
|
791
|
+
function cacheFuse({ now, row }) {
|
|
792
|
+
if (row.ttl === "" || row.expires === 0) return "";
|
|
793
|
+
const span = row.ttl === "1h" ? 3600 : 300;
|
|
794
|
+
let left = row.expires - now;
|
|
795
|
+
if (left < 0) left = 0;
|
|
796
|
+
if (left <= 0) return `${RED$1}❄ cold${RESET$1}`;
|
|
797
|
+
const full = trunc(left * 10 / span);
|
|
798
|
+
const rem = left * 10 % span;
|
|
799
|
+
let bar = left * 4 > span ? GREEN$1 : left * 25 > span * 2 ? YELLOW$1 : RED$1;
|
|
800
|
+
for (let i = 0; i < 10; i++) if (i < full) bar += "▰";
|
|
801
|
+
else if (i === full && rem * 20 > span) bar += PART[trunc(rem * 8 / span)];
|
|
802
|
+
else bar += `${DIM}▱`;
|
|
803
|
+
return `${bar}${RESET$1} ${DIM}${pad2(trunc(left / 60))}:${pad2(left % 60)}${RESET$1}`;
|
|
804
|
+
}
|
|
805
|
+
function costPlain({ row }) {
|
|
806
|
+
return row.cost >= .005 ? `${YELLOW$1}$${fmtFixed(row.cost, 2)}${RESET$1}` : "";
|
|
807
|
+
}
|
|
808
|
+
function costBurn(input) {
|
|
809
|
+
const hr = input.row.durationMs / 36e5;
|
|
810
|
+
const burn = hr > .02 ? fmtFixed(input.row.cost / hr, 2) : "0";
|
|
811
|
+
let out = costPlain(input);
|
|
812
|
+
if (burn !== "0.00") out += ` ${DIM}· $${burn}/hr${RESET$1}`;
|
|
813
|
+
return out;
|
|
814
|
+
}
|
|
815
|
+
function durationClock({ row }) {
|
|
816
|
+
return `${trunc(row.durationMs / 6e4)}m${pad2(trunc(row.durationMs % 6e4 / 1e3))}s`;
|
|
817
|
+
}
|
|
818
|
+
function durationHours({ row }) {
|
|
819
|
+
const m = trunc(row.durationMs / 6e4);
|
|
820
|
+
return `${trunc(m / 60)}h${pad2(m % 60)}m`;
|
|
821
|
+
}
|
|
822
|
+
function linesDiffstat({ row }) {
|
|
823
|
+
if (row.la <= 0 && row.lr <= 0) return "";
|
|
824
|
+
return `${GREEN$1}+${row.la}${RESET$1}${DIM}/${RESET$1}${RED$1}−${row.lr}${RESET$1}`;
|
|
825
|
+
}
|
|
826
|
+
function rateStrip({ now, row }) {
|
|
827
|
+
const segs = [];
|
|
828
|
+
for (const limit of row.rateRows) {
|
|
829
|
+
const [intpText, fracp = ""] = limit.pctText.split(".");
|
|
830
|
+
let f = 0;
|
|
831
|
+
let scale = 1;
|
|
832
|
+
for (const digit of fracp) {
|
|
833
|
+
f = f * 10 + Number(digit);
|
|
834
|
+
scale *= 10;
|
|
835
|
+
}
|
|
836
|
+
const intp = Number(intpText);
|
|
837
|
+
const value = intp * scale + f;
|
|
838
|
+
const col = value < 70 * scale ? GREEN$1 : value < 90 * scale ? YELLOW$1 : RED$1;
|
|
839
|
+
const full = trunc(value * 14 / (100 * scale));
|
|
840
|
+
const idx = trunc(value * 14 % (100 * scale) * 8 / (100 * scale));
|
|
841
|
+
let bar = col;
|
|
842
|
+
for (let i = 0; i < 14; i++) if (i < full) bar += "█";
|
|
843
|
+
else if (i === full) bar += PART[idx];
|
|
844
|
+
else bar += `${DIM}░`;
|
|
845
|
+
const lbl = fracp === "" || f * 2 < scale ? intp : f * 2 > scale ? intp + 1 : intp + intp % 2;
|
|
846
|
+
const s = limit.resets - now;
|
|
847
|
+
let dur;
|
|
848
|
+
if (s >= 3600) dur = `${trunc(s / 3600)}h${pad2(trunc(s % 3600 / 60))}m`;
|
|
849
|
+
else {
|
|
850
|
+
const sec = (s % 60 + 60) % 60;
|
|
851
|
+
dur = `${trunc((s - sec) / 60)}m${pad2(sec)}s`;
|
|
852
|
+
}
|
|
853
|
+
segs.push(`${DIM}${limit.label}${RESET$1} ${bar}${RESET$1} ${BOLD}${lbl}%${RESET$1} ${DIM}· resets ${dur}${RESET$1}`);
|
|
854
|
+
}
|
|
855
|
+
return segs.join(` ${DIM}│${RESET$1} `);
|
|
856
|
+
}
|
|
857
|
+
function emptySegment() {
|
|
858
|
+
return "";
|
|
859
|
+
}
|
|
860
|
+
function barFlat6(input) {
|
|
861
|
+
return barFlatAt(input, 6);
|
|
862
|
+
}
|
|
863
|
+
function barFlat4(input) {
|
|
864
|
+
return barFlatAt(input, 4);
|
|
865
|
+
}
|
|
866
|
+
function barPercent({ row }) {
|
|
867
|
+
return `${row.pct}%`;
|
|
868
|
+
}
|
|
869
|
+
var SEGMENTS = {
|
|
870
|
+
ahead: {
|
|
871
|
+
arrows: aheadArrows,
|
|
872
|
+
none: emptySegment
|
|
873
|
+
},
|
|
874
|
+
bar: {
|
|
875
|
+
flat4: barFlat4,
|
|
876
|
+
flat6: barFlat6,
|
|
877
|
+
flat: barFlat,
|
|
878
|
+
gauge: barGauge,
|
|
879
|
+
none: emptySegment,
|
|
880
|
+
percent: barPercent
|
|
881
|
+
},
|
|
882
|
+
branch: {
|
|
883
|
+
full: branchFull,
|
|
884
|
+
icon: branchIcon,
|
|
885
|
+
initials: branchInitials,
|
|
886
|
+
last: branchLast,
|
|
887
|
+
none: emptySegment
|
|
888
|
+
},
|
|
889
|
+
cache: {
|
|
890
|
+
coldin: cacheColdin,
|
|
891
|
+
fuse: cacheFuse,
|
|
892
|
+
hit: cacheHit,
|
|
893
|
+
none: emptySegment
|
|
894
|
+
},
|
|
895
|
+
cost: {
|
|
896
|
+
burn: costBurn,
|
|
897
|
+
none: emptySegment,
|
|
898
|
+
plain: costPlain
|
|
899
|
+
},
|
|
900
|
+
cwd: {
|
|
901
|
+
base: cwdBase,
|
|
902
|
+
full: cwdFull,
|
|
903
|
+
icon: cwdIcon,
|
|
904
|
+
init: cwdInit,
|
|
905
|
+
tail: cwdTail
|
|
906
|
+
},
|
|
907
|
+
duration: {
|
|
908
|
+
clock: durationClock,
|
|
909
|
+
hours: durationHours,
|
|
910
|
+
none: emptySegment
|
|
911
|
+
},
|
|
912
|
+
effort: {
|
|
913
|
+
dim: effortDim,
|
|
914
|
+
hidden: emptySegment,
|
|
915
|
+
plain: effortPlain
|
|
916
|
+
},
|
|
917
|
+
lines: {
|
|
918
|
+
diffstat: linesDiffstat,
|
|
919
|
+
none: emptySegment
|
|
920
|
+
},
|
|
921
|
+
model: {
|
|
922
|
+
block: modelBlock,
|
|
923
|
+
pill: modelPill,
|
|
924
|
+
plain: modelPlain,
|
|
925
|
+
zen: modelZen
|
|
926
|
+
},
|
|
927
|
+
pr: {
|
|
928
|
+
badge: prBadge,
|
|
929
|
+
none: emptySegment
|
|
930
|
+
},
|
|
931
|
+
rate: {
|
|
932
|
+
none: emptySegment,
|
|
933
|
+
strip: rateStrip
|
|
934
|
+
},
|
|
935
|
+
state: {
|
|
936
|
+
none: emptySegment,
|
|
937
|
+
pills: statePills
|
|
938
|
+
},
|
|
939
|
+
status: {
|
|
940
|
+
counts: statusCounts,
|
|
941
|
+
icons: statusIcons,
|
|
942
|
+
none: emptySegment
|
|
943
|
+
},
|
|
944
|
+
tokens: {
|
|
945
|
+
compact: tokensCompact,
|
|
946
|
+
full: tokensFull,
|
|
947
|
+
free: tokensFree,
|
|
948
|
+
none: emptySegment
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
function renderSegment(item, alt, input) {
|
|
952
|
+
const segment = SEGMENTS[item]?.[alt];
|
|
953
|
+
return segment === void 0 ? "" : segment(input);
|
|
954
|
+
}
|
|
955
|
+
function styleSeparators(alt) {
|
|
956
|
+
switch (alt) {
|
|
957
|
+
case "bare": return {
|
|
958
|
+
join: " ",
|
|
959
|
+
sep: " "
|
|
960
|
+
};
|
|
961
|
+
case "dim": return {
|
|
962
|
+
join: " ",
|
|
963
|
+
sep: `${DIM} │ ${RESET$1}`
|
|
964
|
+
};
|
|
965
|
+
case "dots": return {
|
|
966
|
+
join: " · ",
|
|
967
|
+
sep: " · "
|
|
968
|
+
};
|
|
969
|
+
default: return {
|
|
970
|
+
join: " ",
|
|
971
|
+
sep: " │ "
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
//#endregion
|
|
976
|
+
//#region src/render/engine.ts
|
|
977
|
+
var RUNG_ITEMS = Object.keys(RUNG_ORDERS);
|
|
978
|
+
function stripSgr(text) {
|
|
979
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
980
|
+
}
|
|
981
|
+
function vlen$1(text) {
|
|
982
|
+
const plain = stripSgr(text);
|
|
983
|
+
let n = Array.from(plain).length;
|
|
984
|
+
for (const ch of plain) if (ch === "⚡") n += 1;
|
|
985
|
+
return n;
|
|
986
|
+
}
|
|
987
|
+
function warn(message) {
|
|
988
|
+
process.stderr.write(`${message}\n`);
|
|
989
|
+
}
|
|
990
|
+
function stripModelSuffix(name) {
|
|
991
|
+
const at = name.lastIndexOf("[");
|
|
992
|
+
const cut = at === -1 ? name : name.slice(0, at);
|
|
993
|
+
return cut.endsWith(" ") ? cut.slice(0, -1) : cut;
|
|
994
|
+
}
|
|
995
|
+
function resolvePicks(picks) {
|
|
996
|
+
const resolved = {};
|
|
997
|
+
for (const spec of ITEMS) {
|
|
998
|
+
const pick = picks?.[spec.item] ?? spec.default;
|
|
999
|
+
if (spec.alternatives.includes(pick)) resolved[spec.item] = pick;
|
|
1000
|
+
else {
|
|
1001
|
+
warn(`statusline: ${spec.item}=${pick} is not available, using ${spec.item}=${spec.default}`);
|
|
1002
|
+
resolved[spec.item] = spec.default;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return resolved;
|
|
1006
|
+
}
|
|
1007
|
+
function parseLayout(layout, known) {
|
|
1008
|
+
let parsed;
|
|
1009
|
+
try {
|
|
1010
|
+
parsed = parseClusters(layout);
|
|
1011
|
+
} catch (e) {
|
|
1012
|
+
warn(`statusline: ${e.message}`);
|
|
1013
|
+
return [];
|
|
1014
|
+
}
|
|
1015
|
+
const clusters = [];
|
|
1016
|
+
for (const words of parsed) {
|
|
1017
|
+
const kept = words.filter((word) => {
|
|
1018
|
+
if (known.has(word)) return true;
|
|
1019
|
+
warn(`statusline: layout item '${word}' is not available, skipped`);
|
|
1020
|
+
return false;
|
|
1021
|
+
});
|
|
1022
|
+
if (kept.length > 0) clusters.push(kept);
|
|
1023
|
+
}
|
|
1024
|
+
return clusters;
|
|
1025
|
+
}
|
|
1026
|
+
function renderStatusline(input) {
|
|
1027
|
+
const row = parseRow(input.payload);
|
|
1028
|
+
const git = readGit(row.dir, { home: input.home });
|
|
1029
|
+
const picks = resolvePicks(input.picks);
|
|
1030
|
+
const { join, sep } = styleSeparators(picks.style);
|
|
1031
|
+
const clusters = parseLayout(input.layout ?? "{model effort state} {cwd branch status ahead pr} {bar tokens cache} {cost} {duration} {lines} {rate}", new Set(ITEMS.map((spec) => spec.item)));
|
|
1032
|
+
const wrapAt = clusters.length > 2 ? 2 : 1;
|
|
1033
|
+
let width = input.columns;
|
|
1034
|
+
if (width === void 0 || !Number.isInteger(width) || width < 0) width = 200;
|
|
1035
|
+
if (width < 20) width = 20;
|
|
1036
|
+
const avail = width - 3;
|
|
1037
|
+
let model = row.model;
|
|
1038
|
+
const initialRungs = RUNG_ITEMS.map((item) => [item, picks[item]]);
|
|
1039
|
+
function segmentInput() {
|
|
1040
|
+
return {
|
|
1041
|
+
git,
|
|
1042
|
+
home: input.home,
|
|
1043
|
+
model,
|
|
1044
|
+
now: input.now,
|
|
1045
|
+
row
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function compose(mode) {
|
|
1049
|
+
const from = mode === "l2" ? wrapAt : 0;
|
|
1050
|
+
const to = mode === "l1" ? wrapAt : clusters.length;
|
|
1051
|
+
let line = "";
|
|
1052
|
+
let first = true;
|
|
1053
|
+
for (let ci = from; ci < to; ci++) {
|
|
1054
|
+
let cseg = "";
|
|
1055
|
+
let cf = true;
|
|
1056
|
+
for (const item of clusters[ci]) {
|
|
1057
|
+
const out = renderSegment(item, picks[item], segmentInput());
|
|
1058
|
+
if (out === "") continue;
|
|
1059
|
+
if (cf) {
|
|
1060
|
+
cseg = out;
|
|
1061
|
+
cf = false;
|
|
1062
|
+
} else cseg += join + out;
|
|
1063
|
+
}
|
|
1064
|
+
if (cseg === "") continue;
|
|
1065
|
+
if (first) {
|
|
1066
|
+
line = cseg;
|
|
1067
|
+
first = false;
|
|
1068
|
+
} else line += sep + cseg;
|
|
1069
|
+
}
|
|
1070
|
+
return line;
|
|
1071
|
+
}
|
|
1072
|
+
function fits(line) {
|
|
1073
|
+
return vlen$1(line) <= avail;
|
|
1074
|
+
}
|
|
1075
|
+
function demote(item, target) {
|
|
1076
|
+
const order = RUNG_ORDERS[item];
|
|
1077
|
+
if (order === void 0) return;
|
|
1078
|
+
const at = order.indexOf(picks[item]);
|
|
1079
|
+
if (at === -1) return;
|
|
1080
|
+
if (order.slice(at + 1).includes(target)) picks[item] = target;
|
|
1081
|
+
}
|
|
1082
|
+
function resetRungs() {
|
|
1083
|
+
model = row.model;
|
|
1084
|
+
for (const [item, pick] of initialRungs) picks[item] = pick;
|
|
1085
|
+
}
|
|
1086
|
+
function applyStep(step) {
|
|
1087
|
+
if (step[0] === "model" && step[1] === "strip") model = stripModelSuffix(model);
|
|
1088
|
+
else demote(step[0], step[1]);
|
|
1089
|
+
}
|
|
1090
|
+
function fit(mode, steps) {
|
|
1091
|
+
let out = compose(mode);
|
|
1092
|
+
if (fits(out)) return out;
|
|
1093
|
+
for (const step of steps) {
|
|
1094
|
+
applyStep(step);
|
|
1095
|
+
out = compose(mode);
|
|
1096
|
+
if (fits(out)) return out;
|
|
1097
|
+
}
|
|
1098
|
+
return out;
|
|
1099
|
+
}
|
|
1100
|
+
function emit(line) {
|
|
1101
|
+
return `${input.noColor === true ? stripSgr(line) : line}\n`;
|
|
1102
|
+
}
|
|
1103
|
+
const full = fit("full", FULL_STEPS);
|
|
1104
|
+
if (fits(full)) return emit(full);
|
|
1105
|
+
resetRungs();
|
|
1106
|
+
const l1 = fit("l1", L1_STEPS);
|
|
1107
|
+
const l2 = fit("l2", L2_STEPS);
|
|
1108
|
+
return emit(l1) + emit(l2);
|
|
1109
|
+
}
|
|
1110
|
+
//#endregion
|
|
1111
|
+
//#region src/themes.ts
|
|
1112
|
+
var THEME_NAMES = [
|
|
1113
|
+
"classic",
|
|
1114
|
+
"custom",
|
|
1115
|
+
"lean",
|
|
1116
|
+
"quiet",
|
|
1117
|
+
"rich"
|
|
1118
|
+
];
|
|
1119
|
+
var THEMES = {
|
|
1120
|
+
quiet: {
|
|
1121
|
+
layout: "{model cwd}",
|
|
1122
|
+
summary: "model and directory, nothing else",
|
|
1123
|
+
variants: {
|
|
1124
|
+
cwd: "tail",
|
|
1125
|
+
model: "zen",
|
|
1126
|
+
style: "bare"
|
|
1127
|
+
}
|
|
1128
|
+
},
|
|
1129
|
+
classic: {
|
|
1130
|
+
layout: DEFAULT_LAYOUT,
|
|
1131
|
+
summary: "the shipped defaults, named",
|
|
1132
|
+
variants: {}
|
|
1133
|
+
},
|
|
1134
|
+
lean: {
|
|
1135
|
+
layout: DEFAULT_LAYOUT,
|
|
1136
|
+
summary: "text only, no graphics",
|
|
1137
|
+
variants: {
|
|
1138
|
+
model: "plain",
|
|
1139
|
+
effort: "dim",
|
|
1140
|
+
state: "none",
|
|
1141
|
+
cwd: "init",
|
|
1142
|
+
branch: "initials",
|
|
1143
|
+
status: "counts",
|
|
1144
|
+
ahead: "arrows",
|
|
1145
|
+
pr: "badge",
|
|
1146
|
+
bar: "percent",
|
|
1147
|
+
tokens: "full",
|
|
1148
|
+
cache: "hit",
|
|
1149
|
+
cost: "plain",
|
|
1150
|
+
duration: "clock",
|
|
1151
|
+
lines: "diffstat",
|
|
1152
|
+
rate: "none",
|
|
1153
|
+
style: "dots"
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1156
|
+
rich: {
|
|
1157
|
+
layout: DEFAULT_LAYOUT,
|
|
1158
|
+
summary: "every gauge and counter",
|
|
1159
|
+
variants: {
|
|
1160
|
+
model: "pill",
|
|
1161
|
+
effort: "plain",
|
|
1162
|
+
state: "pills",
|
|
1163
|
+
cwd: "icon",
|
|
1164
|
+
branch: "icon",
|
|
1165
|
+
status: "icons",
|
|
1166
|
+
ahead: "arrows",
|
|
1167
|
+
pr: "badge",
|
|
1168
|
+
bar: "gauge",
|
|
1169
|
+
tokens: "full",
|
|
1170
|
+
cache: "fuse",
|
|
1171
|
+
cost: "burn",
|
|
1172
|
+
duration: "clock",
|
|
1173
|
+
lines: "diffstat",
|
|
1174
|
+
rate: "strip",
|
|
1175
|
+
style: "plain"
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
1178
|
+
custom: {
|
|
1179
|
+
layout: DEFAULT_LAYOUT,
|
|
1180
|
+
summary: "bare; you decide everything",
|
|
1181
|
+
variants: {
|
|
1182
|
+
model: "zen",
|
|
1183
|
+
effort: "hidden",
|
|
1184
|
+
state: "none",
|
|
1185
|
+
cwd: "base",
|
|
1186
|
+
branch: "none",
|
|
1187
|
+
status: "none",
|
|
1188
|
+
ahead: "none",
|
|
1189
|
+
pr: "none",
|
|
1190
|
+
bar: "none",
|
|
1191
|
+
tokens: "none",
|
|
1192
|
+
cache: "none",
|
|
1193
|
+
cost: "none",
|
|
1194
|
+
duration: "none",
|
|
1195
|
+
lines: "none",
|
|
1196
|
+
rate: "none",
|
|
1197
|
+
style: "bare"
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
//#endregion
|
|
1202
|
+
//#region src/render/theme.ts
|
|
1203
|
+
function resolvePaint(input) {
|
|
1204
|
+
const name = input.theme;
|
|
1205
|
+
const theme = name === void 0 ? void 0 : THEMES[name];
|
|
1206
|
+
if (name !== void 0 && theme === void 0) warn(`statusline: theme=${name} is not a known theme, ignored`);
|
|
1207
|
+
return {
|
|
1208
|
+
layout: input.layout ?? theme?.layout ?? "{model effort state} {cwd branch status ahead pr} {bar tokens cache} {cost} {duration} {lines} {rate}",
|
|
1209
|
+
picks: {
|
|
1210
|
+
...theme?.variants ?? {},
|
|
1211
|
+
...input.picks
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
//#endregion
|
|
1216
|
+
//#region src/render/capture.ts
|
|
1217
|
+
var DATA_DIR = join(".claude", "plugins", "data", "statusline-agentic");
|
|
1218
|
+
//#endregion
|
|
26
1219
|
//#region src/resolve.ts
|
|
27
|
-
var DATA_REL = join(".claude", "plugins", "data", "statusline-agentic");
|
|
28
1220
|
function capturePath(home, surface) {
|
|
29
|
-
return join(home,
|
|
1221
|
+
return join(home, DATA_DIR, "captures", `${surface}.json`);
|
|
30
1222
|
}
|
|
31
1223
|
function backupPath(home) {
|
|
32
|
-
return join(home,
|
|
1224
|
+
return join(home, DATA_DIR, "backup.json");
|
|
1225
|
+
}
|
|
1226
|
+
function renderMjsPath(home) {
|
|
1227
|
+
return join(home, DATA_DIR, "render.mjs");
|
|
1228
|
+
}
|
|
1229
|
+
var RENDER_PROGRAM = `node "$HOME/${DATA_DIR.split(sep).join("/")}/render.mjs"`;
|
|
1230
|
+
var PANEL_PROGRAM = `${RENDER_PROGRAM} panel`;
|
|
1231
|
+
var KEY_SUFFIX = " || true";
|
|
1232
|
+
function keyValue(program, theme, layout, flags) {
|
|
1233
|
+
return `${[
|
|
1234
|
+
program,
|
|
1235
|
+
...theme === null ? [] : [`--theme=${theme}`],
|
|
1236
|
+
...layout === null ? [] : [`--layout='${layout}'`],
|
|
1237
|
+
...flags
|
|
1238
|
+
].join(" ")}${KEY_SUFFIX}`;
|
|
33
1239
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
function
|
|
38
|
-
return
|
|
1240
|
+
function mainKeyValue(theme, layout, flags) {
|
|
1241
|
+
return keyValue(RENDER_PROGRAM, theme, layout, flags);
|
|
1242
|
+
}
|
|
1243
|
+
function panelKeyValue(theme, flags) {
|
|
1244
|
+
return keyValue(PANEL_PROGRAM, theme, null, flags);
|
|
1245
|
+
}
|
|
1246
|
+
function commandMiddle(program, command) {
|
|
1247
|
+
if (!command.startsWith(program) || !command.endsWith(KEY_SUFFIX)) return null;
|
|
1248
|
+
const middle = command.slice(program.length, command.length - 8);
|
|
1249
|
+
return middle === "" || middle.startsWith(" --") ? middle : null;
|
|
39
1250
|
}
|
|
40
|
-
var subagentKeyValue = `${KEY_RESOLVER}; bash "\${d}runtime/subagent.sh" 2>/dev/null || true`;
|
|
41
1251
|
function isOurMainCommand(command) {
|
|
42
|
-
return command
|
|
1252
|
+
return commandMiddle(RENDER_PROGRAM, command) !== null;
|
|
43
1253
|
}
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
return command.slice(MAIN_PREFIX.length, command.length - 52 - 1);
|
|
1254
|
+
function isOurPanelCommand(command) {
|
|
1255
|
+
return commandMiddle(PANEL_PROGRAM, command) !== null;
|
|
47
1256
|
}
|
|
1257
|
+
var NO_DECISIONS = {
|
|
1258
|
+
layout: null,
|
|
1259
|
+
values: {}
|
|
1260
|
+
};
|
|
48
1261
|
function settingsCommand(home) {
|
|
49
1262
|
let raw;
|
|
50
1263
|
try {
|
|
@@ -63,112 +1276,61 @@ function settingsCommand(home) {
|
|
|
63
1276
|
const command = member.command;
|
|
64
1277
|
return typeof command === "string" ? command : null;
|
|
65
1278
|
}
|
|
66
|
-
function
|
|
67
|
-
const command = settingsCommand(home);
|
|
68
|
-
const middle = command === null ? null : mainKeyMiddle(command);
|
|
69
|
-
if (middle === null) return {
|
|
70
|
-
layout: null,
|
|
71
|
-
values: {}
|
|
72
|
-
};
|
|
73
|
-
const layout = /^STATUSLINE_LAB_LAYOUT='([^']*)'/.exec(middle);
|
|
74
|
-
if (layout === null) return {
|
|
75
|
-
layout: null,
|
|
76
|
-
values: {}
|
|
77
|
-
};
|
|
1279
|
+
function parseKeyFlags(flags) {
|
|
78
1280
|
const values = {};
|
|
79
|
-
|
|
1281
|
+
let theme;
|
|
1282
|
+
for (const [, name, alt] of flags.matchAll(/(?:^| )--([a-z][a-z0-9]*)=([a-z0-9]+)/g)) if (name === "theme") theme = alt;
|
|
1283
|
+
else if (name !== "now" && name !== "layout") values[name] = alt;
|
|
1284
|
+
const layout = /--layout='([^']*)'/.exec(flags);
|
|
80
1285
|
return {
|
|
81
|
-
layout: layout[1],
|
|
82
|
-
values
|
|
1286
|
+
layout: layout === null ? null : layout[1],
|
|
1287
|
+
values,
|
|
1288
|
+
...theme === void 0 ? {} : { theme }
|
|
83
1289
|
};
|
|
84
1290
|
}
|
|
85
|
-
function
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const delta = (parseInt(left[i] ?? "0", 10) || 0) - (parseInt(right[i] ?? "0", 10) || 0);
|
|
90
|
-
if (delta !== 0) return delta;
|
|
91
|
-
}
|
|
92
|
-
return 0;
|
|
93
|
-
}
|
|
94
|
-
function newestCacheDir(cacheRoot) {
|
|
95
|
-
let versions;
|
|
96
|
-
try {
|
|
97
|
-
versions = readdirSync(cacheRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
98
|
-
} catch {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
if (versions.length === 0) return null;
|
|
102
|
-
versions.sort(byVersion);
|
|
103
|
-
return join(cacheRoot, versions[versions.length - 1]);
|
|
104
|
-
}
|
|
105
|
-
function readAlternatives(dir) {
|
|
106
|
-
const declared = /* @__PURE__ */ new Map();
|
|
107
|
-
const components = join(dir, "components");
|
|
108
|
-
for (const file of readdirSync(components).sort()) {
|
|
109
|
-
if (!file.endsWith(".sh")) continue;
|
|
110
|
-
for (const line of readFileSync(join(components, file), "utf8").split("\n")) {
|
|
111
|
-
if (!line.startsWith("#")) break;
|
|
112
|
-
const match = /alternatives:\s*(.+)$/.exec(line);
|
|
113
|
-
if (match) declared.set(file.slice(0, -3), match[1].split("|").map((alt) => alt.trim().replace(/\s*\(current\)$/, "")));
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return declared;
|
|
117
|
-
}
|
|
118
|
-
function readItems(dir) {
|
|
119
|
-
const alternatives = readAlternatives(dir);
|
|
120
|
-
const defaults = /* @__PURE__ */ new Map();
|
|
121
|
-
for (const [, item, alt] of readFileSync(join(dir, "lib.sh"), "utf8").matchAll(/([a-z]+)\) echo ([a-z]+) ;;/g)) defaults.set(item, alt);
|
|
122
|
-
const match = /^COMPS="(.+)"$/m.exec(readFileSync(join(dir, "statusline.sh"), "utf8"));
|
|
123
|
-
if (match === null) throw new Error(`statusline.sh in ${dir} declares no COMPS order`);
|
|
124
|
-
return match[1].split(" ").map((item) => {
|
|
125
|
-
const alt = defaults.get(item);
|
|
126
|
-
if (alt === void 0) throw new Error(`lib.sh in ${dir} declares no default for '${item}'`);
|
|
127
|
-
return {
|
|
128
|
-
alternatives: alternatives.get(item) ?? [],
|
|
129
|
-
default: alt,
|
|
130
|
-
item
|
|
131
|
-
};
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
function readDefaultLayout(dir) {
|
|
135
|
-
const match = /^export DEFAULT_LAYOUT='(.*)'$/m.exec(readFileSync(join(dir, "lib.sh"), "utf8"));
|
|
136
|
-
if (match === null) throw new Error(`lib.sh in ${dir} declares no DEFAULT_LAYOUT`);
|
|
137
|
-
return match[1];
|
|
1291
|
+
function readKeyConfig(home) {
|
|
1292
|
+
const command = settingsCommand(home);
|
|
1293
|
+
const flags = command === null ? null : commandMiddle(RENDER_PROGRAM, command);
|
|
1294
|
+
return flags === null ? NO_DECISIONS : parseKeyFlags(flags);
|
|
138
1295
|
}
|
|
139
|
-
function
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
if (dir === null || !existsSync(join(dir, "statusline.sh"))) throw new Error(`no statusline runtime under ${home} — install the plugin first: claude plugin install statusline@agentic`);
|
|
143
|
-
return {
|
|
144
|
-
defaultLayout: readDefaultLayout(dir),
|
|
145
|
-
dir,
|
|
146
|
-
items: readItems(dir)
|
|
147
|
-
};
|
|
1296
|
+
function parsePanelCommand(command) {
|
|
1297
|
+
const flags = command === null ? null : commandMiddle(PANEL_PROGRAM, command);
|
|
1298
|
+
return flags === null ? NO_DECISIONS : parseKeyFlags(flags);
|
|
148
1299
|
}
|
|
149
1300
|
//#endregion
|
|
150
1301
|
//#region src/catalog.ts
|
|
151
1302
|
function catalog(options) {
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
1303
|
+
if (options.themes === true && options.items !== void 0) throw new Error("--themes cannot combine with item flags");
|
|
1304
|
+
const key = readKeyConfig(options.home);
|
|
1305
|
+
const block = Object.entries(THEMES).map(([name, theme]) => `${name}${key.theme === name ? "*" : ""}: ${theme.summary}`);
|
|
1306
|
+
if (options.themes === true) return block.join("\n");
|
|
1307
|
+
const resolved = resolvePaint({
|
|
1308
|
+
layout: key.layout ?? void 0,
|
|
1309
|
+
picks: key.values,
|
|
1310
|
+
theme: key.theme
|
|
1311
|
+
});
|
|
1312
|
+
const itemLines = (options.items ?? ITEM_IDS).map((item) => {
|
|
1313
|
+
const entry = specFor(item);
|
|
1314
|
+
if (entry === void 0) throw new Error(`unknown item '${item}' — valid items: ${ITEM_IDS.join(" ")}`);
|
|
1315
|
+
const current = resolved.picks[item] ?? entry.default;
|
|
159
1316
|
return `${item}: ${entry.alternatives.map((alt) => alt === current ? `${alt}*` : alt).join(" | ")}`;
|
|
160
|
-
})
|
|
1317
|
+
});
|
|
1318
|
+
return [
|
|
1319
|
+
...block,
|
|
1320
|
+
"",
|
|
1321
|
+
...itemLines
|
|
1322
|
+
].join("\n");
|
|
161
1323
|
}
|
|
162
1324
|
//#endregion
|
|
163
1325
|
//#region src/cli.ts
|
|
164
1326
|
var VERSION = {
|
|
165
1327
|
name: "@v1nvn/statusline",
|
|
166
|
-
version: "0.
|
|
1328
|
+
version: "0.29.0",
|
|
167
1329
|
description: "Browse statusline variants and configure the live status line and agent panel — the statusline CLI behind the statusline plugin.",
|
|
168
1330
|
type: "module",
|
|
169
1331
|
bin: "dist/index.js",
|
|
170
1332
|
scripts: {
|
|
171
|
-
"build": "vite build",
|
|
1333
|
+
"build": "vite build && vite build --mode render",
|
|
172
1334
|
"test": "vitest run",
|
|
173
1335
|
"test:watch": "vitest"
|
|
174
1336
|
},
|
|
@@ -199,24 +1361,6 @@ var VERSION = {
|
|
|
199
1361
|
"vitest": "^4.1.10"
|
|
200
1362
|
}
|
|
201
1363
|
}.version;
|
|
202
|
-
var ITEM_IDS = [
|
|
203
|
-
"model",
|
|
204
|
-
"effort",
|
|
205
|
-
"state",
|
|
206
|
-
"cwd",
|
|
207
|
-
"branch",
|
|
208
|
-
"status",
|
|
209
|
-
"ahead",
|
|
210
|
-
"pr",
|
|
211
|
-
"bar",
|
|
212
|
-
"tokens",
|
|
213
|
-
"cache",
|
|
214
|
-
"cost",
|
|
215
|
-
"duration",
|
|
216
|
-
"lines",
|
|
217
|
-
"rate",
|
|
218
|
-
"style"
|
|
219
|
-
];
|
|
220
1364
|
var QUIET = {
|
|
221
1365
|
writeOut: () => void 0,
|
|
222
1366
|
writeErr: () => void 0
|
|
@@ -236,17 +1380,22 @@ function quiet(name, command) {
|
|
|
236
1380
|
}).configureOutput(QUIET).allowExcessArguments(false);
|
|
237
1381
|
}
|
|
238
1382
|
function buildProgram(onSubcommand) {
|
|
239
|
-
const catalog = quiet("catalog", new Command("catalog")).description("print one line per item — * marks the live variant").option("--home <dir>", "operate on this home instead of $HOME");
|
|
1383
|
+
const catalog = quiet("catalog", new Command("catalog")).description("print the themes block (* marks the live theme) then one line per item — * marks the live variant").option("--home <dir>", "operate on this home instead of $HOME").option("--themes", "cut the listing to the themes block");
|
|
240
1384
|
for (const item of ITEM_IDS) catalog.option(`--${item}`, `cut the listing to the ${item} item`);
|
|
241
1385
|
catalog.action((options) => onSubcommand?.("catalog", options));
|
|
242
|
-
const
|
|
1386
|
+
const LAYOUT_HELP = "brace clusters of item ids, e.g. '{cwd branch} {model effort}'";
|
|
1387
|
+
const THEME_HELP = "base design the item flags override: quiet, lean, classic, rich, custom";
|
|
1388
|
+
const configure = quiet("configure", new Command("configure")).description("write both settings keys with the inline lab commands").option("--home <dir>", "operate on this home instead of $HOME").option("--layout <spec>", LAYOUT_HELP).option("--theme <name>", THEME_HELP).option("--force", "take over foreign settings keys");
|
|
243
1389
|
for (const item of ITEM_IDS) configure.option(`--${item} <alt>`, `variant for the ${item} item`);
|
|
244
1390
|
configure.action((options) => onSubcommand?.("configure", options));
|
|
1391
|
+
const preview = quiet("preview", new Command("preview")).description("render a candidate line and panel row; nothing is written").option("--home <dir>", "operate on this home instead of $HOME").option("--layout <spec>", LAYOUT_HELP).option("--theme <name>", THEME_HELP).option("--plain", "strip the color escapes so glyphs survive chat");
|
|
1392
|
+
for (const item of ITEM_IDS) preview.option(`--${item} <alt>`, `variant for the ${item} item`);
|
|
1393
|
+
preview.action((options) => onSubcommand?.("preview", options));
|
|
245
1394
|
const restore = quiet("restore", new Command("restore")).description("revert both settings keys to their pre-lab values, clean the data dir").option("--home <dir>", "operate on this home instead of $HOME").option("--dry-run", "print the plan, write nothing").option("--force", "splice the saved value over a key changed after the takeover");
|
|
246
1395
|
restore.action((options) => onSubcommand?.("restore", options));
|
|
247
1396
|
const status = quiet("status", new Command("status")).description("check install, keys, config, and backup — exit 0 healthy, 1 when a row needs action").option("--home <dir>", "operate on this home instead of $HOME");
|
|
248
1397
|
status.action((options) => onSubcommand?.("status", options));
|
|
249
|
-
return new Command().name("statusline").description("Configure the status line and agent panel designs").option("-V, --version", "print the version and exit").action(() => void 0).addCommand(catalog).addCommand(configure).addCommand(restore).addCommand(status);
|
|
1398
|
+
return new Command().name("statusline").description("Configure the status line and agent panel designs").option("-V, --version", "print the version and exit").action(() => void 0).addCommand(catalog).addCommand(configure).addCommand(preview).addCommand(restore).addCommand(status);
|
|
250
1399
|
}
|
|
251
1400
|
function subcommandHelp(name) {
|
|
252
1401
|
const command = buildProgram().commands.find((c) => c.name() === name);
|
|
@@ -277,6 +1426,7 @@ function parseArgs(args) {
|
|
|
277
1426
|
version: false,
|
|
278
1427
|
command: "catalog",
|
|
279
1428
|
...items.length > 0 ? { items } : {},
|
|
1429
|
+
...typeof options.themes === "boolean" ? { themes: options.themes } : {},
|
|
280
1430
|
...typeof options.home === "string" ? { home: options.home } : {}
|
|
281
1431
|
};
|
|
282
1432
|
}
|
|
@@ -294,35 +1444,297 @@ function parseArgs(args) {
|
|
|
294
1444
|
};
|
|
295
1445
|
const variants = {};
|
|
296
1446
|
for (const item of ITEM_IDS) if (typeof options[item] === "string") variants[item] = options[item];
|
|
297
|
-
|
|
1447
|
+
const common = {
|
|
298
1448
|
version: false,
|
|
299
|
-
command:
|
|
1449
|
+
command: chosen.command,
|
|
300
1450
|
...Object.keys(variants).length > 0 ? { variants } : {},
|
|
301
|
-
...typeof options.dryRun === "boolean" ? { dryRun: options.dryRun } : {},
|
|
302
|
-
...typeof options.fallback === "string" ? { fallback: options.fallback } : {},
|
|
303
|
-
...typeof options.force === "boolean" ? { force: options.force } : {},
|
|
304
1451
|
...typeof options.home === "string" ? { home: options.home } : {},
|
|
305
|
-
...typeof options.layout === "string" ? { layout: options.layout } : {}
|
|
1452
|
+
...typeof options.layout === "string" ? { layout: options.layout } : {},
|
|
1453
|
+
...typeof options.theme === "string" ? { theme: options.theme } : {}
|
|
1454
|
+
};
|
|
1455
|
+
return chosen.command === "configure" ? {
|
|
1456
|
+
...common,
|
|
1457
|
+
...typeof options.force === "boolean" ? { force: options.force } : {}
|
|
1458
|
+
} : {
|
|
1459
|
+
...common,
|
|
1460
|
+
...typeof options.plain === "boolean" ? { plain: options.plain } : {}
|
|
306
1461
|
};
|
|
307
1462
|
}
|
|
308
1463
|
//#endregion
|
|
309
|
-
//#region src/
|
|
310
|
-
var
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
1464
|
+
//#region src/configure.ts
|
|
1465
|
+
var BUNDLED_RENDERER = fileURLToPath(new URL("../dist/render.mjs", import.meta.url));
|
|
1466
|
+
function syncRenderer(home) {
|
|
1467
|
+
const source = readFileSync(BUNDLED_RENDERER);
|
|
1468
|
+
const dest = renderMjsPath(home);
|
|
1469
|
+
if (existsSync(dest) && readFileSync(dest).equals(source)) return;
|
|
1470
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
1471
|
+
const tmp = `${dest}.tmp`;
|
|
1472
|
+
writeFileSync(tmp, source);
|
|
1473
|
+
renameSync(tmp, dest);
|
|
1474
|
+
}
|
|
1475
|
+
var SETTINGS_KEYS = ["statusLine", "subagentStatusLine"];
|
|
1476
|
+
function isObject(value) {
|
|
1477
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1478
|
+
}
|
|
1479
|
+
function readOrNull(file) {
|
|
1480
|
+
try {
|
|
1481
|
+
return readFileSync(file, "utf8");
|
|
1482
|
+
} catch (e) {
|
|
1483
|
+
if (e.code === "ENOENT") return null;
|
|
1484
|
+
throw e;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
function parseSettings(file, raw) {
|
|
1488
|
+
let parsed;
|
|
1489
|
+
try {
|
|
1490
|
+
parsed = JSON.parse(raw);
|
|
1491
|
+
} catch (e) {
|
|
1492
|
+
throw new Error(`${file} is not valid JSON`, { cause: e });
|
|
1493
|
+
}
|
|
1494
|
+
if (!isObject(parsed)) throw new Error(`${file} does not hold a settings object`);
|
|
1495
|
+
return parsed;
|
|
1496
|
+
}
|
|
1497
|
+
function memberCommand(value) {
|
|
1498
|
+
if (!isObject(value) || value.type !== "command") return null;
|
|
1499
|
+
return typeof value.command === "string" ? value.command : null;
|
|
1500
|
+
}
|
|
1501
|
+
function isOurMember(key, value) {
|
|
1502
|
+
const command = memberCommand(value);
|
|
1503
|
+
if (command === null) return false;
|
|
1504
|
+
return key === "statusLine" ? isOurMainCommand(command) : isOurPanelCommand(command);
|
|
1505
|
+
}
|
|
1506
|
+
function planSettings(home, commands, force) {
|
|
1507
|
+
const file = join(home, ".claude", "settings.json");
|
|
1508
|
+
const wanted = commands;
|
|
1509
|
+
const raw = readOrNull(file);
|
|
1510
|
+
if (raw === null) return {
|
|
1511
|
+
adds: SETTINGS_KEYS.map((key) => [key, wanted[key]]),
|
|
1512
|
+
file,
|
|
1513
|
+
raw: null,
|
|
1514
|
+
repoints: []
|
|
1515
|
+
};
|
|
1516
|
+
const parsed = parseSettings(file, raw);
|
|
1517
|
+
const adds = [];
|
|
1518
|
+
const repoints = [];
|
|
1519
|
+
for (const key of SETTINGS_KEYS) {
|
|
1520
|
+
const command = wanted[key];
|
|
1521
|
+
const value = parsed[key];
|
|
1522
|
+
if (value === void 0) adds.push([key, command]);
|
|
1523
|
+
else if (memberCommand(value) === command) continue;
|
|
1524
|
+
else if (isOurMember(key, value) || force) repoints.push([key, command]);
|
|
1525
|
+
else throw new Error(`settings.json ${key} is foreign — rerun with --force added to your configuration flags to take it over`);
|
|
1526
|
+
}
|
|
1527
|
+
return {
|
|
1528
|
+
adds,
|
|
1529
|
+
file,
|
|
1530
|
+
raw,
|
|
1531
|
+
repoints
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
function settingsValue(command) {
|
|
1535
|
+
return JSON.stringify({
|
|
1536
|
+
command,
|
|
1537
|
+
type: "command"
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
function skipString(raw, at) {
|
|
1541
|
+
let i = at + 1;
|
|
1542
|
+
while (i < raw.length) if (raw[i] === "\\") i += 2;
|
|
1543
|
+
else if (raw[i] === "\"") return i + 1;
|
|
1544
|
+
else i += 1;
|
|
1545
|
+
throw new Error("unterminated string in settings.json");
|
|
1546
|
+
}
|
|
1547
|
+
function skipValue(raw, at) {
|
|
1548
|
+
const first = raw[at];
|
|
1549
|
+
if (first === "\"") return skipString(raw, at);
|
|
1550
|
+
if (first === "{" || first === "[") {
|
|
1551
|
+
let depth = 0;
|
|
1552
|
+
let i = at;
|
|
1553
|
+
while (i < raw.length) {
|
|
1554
|
+
const c = raw[i];
|
|
1555
|
+
if (c === "\"") i = skipString(raw, i);
|
|
1556
|
+
else if (c === "{" || c === "[") {
|
|
1557
|
+
depth += 1;
|
|
1558
|
+
i += 1;
|
|
1559
|
+
} else if (c === "}" || c === "]") {
|
|
1560
|
+
depth -= 1;
|
|
1561
|
+
i += 1;
|
|
1562
|
+
if (depth === 0) return i;
|
|
1563
|
+
} else i += 1;
|
|
1564
|
+
}
|
|
1565
|
+
throw new Error("unterminated container in settings.json");
|
|
1566
|
+
}
|
|
1567
|
+
let i = at;
|
|
1568
|
+
while (i < raw.length && !",}]".includes(raw[i] ?? "") && !/\s/.test(raw[i] ?? "~")) i += 1;
|
|
1569
|
+
return i;
|
|
1570
|
+
}
|
|
1571
|
+
function rootMembers(raw) {
|
|
1572
|
+
const members = [];
|
|
1573
|
+
let i = raw.indexOf("{") + 1;
|
|
1574
|
+
while (i < raw.length) {
|
|
1575
|
+
while (/\s/.test(raw[i] ?? "~")) i += 1;
|
|
1576
|
+
if (raw[i] === "}") break;
|
|
1577
|
+
if (raw[i] !== "\"") throw new Error("malformed member in settings.json");
|
|
1578
|
+
const nameStart = i;
|
|
1579
|
+
const nameEnd = skipString(raw, i);
|
|
1580
|
+
const name = JSON.parse(raw.slice(i, nameEnd));
|
|
1581
|
+
i = nameEnd;
|
|
1582
|
+
while (/\s/.test(raw[i] ?? "~")) i += 1;
|
|
1583
|
+
if (raw[i] !== ":") throw new Error("malformed member in settings.json");
|
|
1584
|
+
i += 1;
|
|
1585
|
+
while (/\s/.test(raw[i] ?? "~")) i += 1;
|
|
1586
|
+
const valueStart = i;
|
|
1587
|
+
const valueEnd = skipValue(raw, i);
|
|
1588
|
+
members.push({
|
|
1589
|
+
name,
|
|
1590
|
+
nameStart,
|
|
1591
|
+
valueEnd,
|
|
1592
|
+
valueStart
|
|
1593
|
+
});
|
|
1594
|
+
i = valueEnd;
|
|
1595
|
+
while (/\s/.test(raw[i] ?? "~")) i += 1;
|
|
1596
|
+
if (raw[i] === ",") i += 1;
|
|
1597
|
+
else if (raw[i] !== "}") throw new Error("malformed member in settings.json");
|
|
1598
|
+
}
|
|
1599
|
+
return members;
|
|
1600
|
+
}
|
|
1601
|
+
function rootMemberValueSpan(raw, key) {
|
|
1602
|
+
const member = rootMembers(raw).find((m) => m.name === key);
|
|
1603
|
+
return member === void 0 ? null : [member.valueStart, member.valueEnd];
|
|
1604
|
+
}
|
|
1605
|
+
function removeMembers(raw, keys) {
|
|
1606
|
+
let text = raw;
|
|
1607
|
+
for (const key of keys) {
|
|
1608
|
+
const members = rootMembers(text);
|
|
1609
|
+
const at = members.findIndex((m) => m.name === key);
|
|
1610
|
+
if (at === -1) throw new Error(`cannot find the "${key}" member to remove in settings.json`);
|
|
1611
|
+
if (members.length === 1) return "{}";
|
|
1612
|
+
const member = members[at];
|
|
1613
|
+
if (at > 0) {
|
|
1614
|
+
let start = member.nameStart;
|
|
1615
|
+
while (/\s/.test(text[start - 1] ?? "~")) start -= 1;
|
|
1616
|
+
text = `${text.slice(0, start - 1)}${text.slice(member.valueEnd)}`;
|
|
1617
|
+
} else text = `${text.slice(0, member.nameStart)}${text.slice(members[1].nameStart)}`;
|
|
1618
|
+
}
|
|
1619
|
+
return text;
|
|
1620
|
+
}
|
|
1621
|
+
function insertMembers(raw, members) {
|
|
1622
|
+
const close = raw.lastIndexOf("}");
|
|
1623
|
+
if (close === -1) throw new Error("settings.json has no closing brace to splice into");
|
|
1624
|
+
let at = close;
|
|
1625
|
+
while (/\s/.test(raw[at - 1] ?? "~")) at -= 1;
|
|
1626
|
+
const comma = raw.replace(/\s/g, "") === "{}" ? "" : ",";
|
|
1627
|
+
return `${raw.slice(0, at)}${comma}\n ${renderMembers(members)}${raw.slice(at)}`;
|
|
1628
|
+
}
|
|
1629
|
+
function renderMembers(members) {
|
|
1630
|
+
return members.map(([key, command]) => `"${key}": ${settingsValue(command)}`).join(",\n ");
|
|
1631
|
+
}
|
|
1632
|
+
/** Splice each rendered value over its root member. */
|
|
1633
|
+
function repointRootMembers(raw, entries) {
|
|
1634
|
+
let text = raw;
|
|
1635
|
+
for (const { key, value } of entries) {
|
|
1636
|
+
const span = rootMemberValueSpan(text, key);
|
|
1637
|
+
if (span === null) throw new Error(`cannot find the "${key}" member in settings.json`);
|
|
1638
|
+
text = `${text.slice(0, span[0])}${value}${text.slice(span[1])}`;
|
|
1639
|
+
}
|
|
1640
|
+
return text;
|
|
1641
|
+
}
|
|
1642
|
+
function splicedSettings(raw, plan) {
|
|
1643
|
+
return repointRootMembers(plan.adds.length > 0 ? insertMembers(raw, plan.adds) : raw, plan.repoints.map(([key, command]) => ({
|
|
1644
|
+
key,
|
|
1645
|
+
value: settingsValue(command)
|
|
1646
|
+
})));
|
|
1647
|
+
}
|
|
1648
|
+
function writeBackupIfAbsent(home, plan) {
|
|
1649
|
+
const file = backupPath(home);
|
|
1650
|
+
if (existsSync(file)) return;
|
|
1651
|
+
const keys = {};
|
|
1652
|
+
if (plan.raw !== null) for (const key of SETTINGS_KEYS) {
|
|
1653
|
+
const span = rootMemberValueSpan(plan.raw, key);
|
|
1654
|
+
if (span === null) continue;
|
|
1655
|
+
const memberRaw = plan.raw.slice(span[0], span[1]);
|
|
1656
|
+
if (isOurMember(key, JSON.parse(memberRaw))) continue;
|
|
1657
|
+
keys[key] = memberRaw;
|
|
1658
|
+
}
|
|
1659
|
+
const dir = dirname(file);
|
|
1660
|
+
mkdirSync(dir, { recursive: true });
|
|
1661
|
+
const tmp = `${file}.tmp`;
|
|
1662
|
+
writeFileSync(tmp, `${JSON.stringify({
|
|
1663
|
+
createdFile: plan.raw === null,
|
|
1664
|
+
keys
|
|
1665
|
+
}, null, 2)}\n`);
|
|
1666
|
+
renameSync(tmp, file);
|
|
1667
|
+
}
|
|
1668
|
+
function commitSettings(plan) {
|
|
1669
|
+
if (plan.raw === null) {
|
|
1670
|
+
mkdirSync(dirname(plan.file), { recursive: true });
|
|
1671
|
+
const members = plan.adds.map(([key, command]) => `"${key}": ${settingsValue(command)}`).join(",\n ");
|
|
1672
|
+
writeFileSync(plan.file, `{\n ${members}\n}\n`);
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
if (plan.adds.length === 0 && plan.repoints.length === 0) return;
|
|
1676
|
+
writeFileSync(plan.file, splicedSettings(plan.raw, plan));
|
|
1677
|
+
}
|
|
1678
|
+
function themeNamed(name, themes) {
|
|
1679
|
+
const theme = themes[name];
|
|
1680
|
+
if (theme === void 0) throw new Error(`unknown theme '${name}' — valid themes: ${THEME_NAMES.join(" ")}`);
|
|
1681
|
+
return theme;
|
|
1682
|
+
}
|
|
1683
|
+
function validateSelection(options) {
|
|
1684
|
+
const theme = options.theme === void 0 ? void 0 : themeNamed(options.theme, THEMES);
|
|
1685
|
+
const layout = options.layout ?? theme?.layout;
|
|
1686
|
+
if (layout === void 0) throw new Error("no layout — pass --layout <spec> or --theme <name>");
|
|
1687
|
+
layoutItems(layout, ITEM_IDS);
|
|
1688
|
+
for (const [item, alt] of Object.entries(options.variants ?? {})) {
|
|
1689
|
+
const entry = specFor(item);
|
|
1690
|
+
if (entry === void 0) throw new Error(`unknown item '${item}' — valid items: ${ITEM_IDS.join(" ")}`);
|
|
1691
|
+
if (!entry.alternatives.includes(alt)) throw new Error(`unknown variant '${alt}' for item '${item}' — valid: ${entry.alternatives.join(" | ")}`);
|
|
1692
|
+
}
|
|
1693
|
+
return theme;
|
|
1694
|
+
}
|
|
1695
|
+
function decisionFlags(base, decisions) {
|
|
1696
|
+
return ITEMS.flatMap(({ item }) => {
|
|
1697
|
+
const pick = decisions[item];
|
|
1698
|
+
return pick === void 0 || pick === base[item] ? [] : [`--${item}=${pick}`];
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
function configure(options) {
|
|
1702
|
+
if (options.theme === void 0 && options.layout === void 0 && Object.keys(options.variants ?? {}).length === 0) throw new Error("no theme and no item flags — on a terminal run 'statusline configure' for the wizard; in Claude Code use the /lab skill");
|
|
1703
|
+
const theme = validateSelection(options);
|
|
1704
|
+
const base = {
|
|
1705
|
+
...DEFAULT_PICKS,
|
|
1706
|
+
...theme?.variants
|
|
1707
|
+
};
|
|
1708
|
+
const decisions = options.variants ?? {};
|
|
1709
|
+
const baseLayout = theme?.layout ?? "{model effort state} {cwd branch status ahead pr} {bar tokens cache} {cost} {duration} {lines} {rate}";
|
|
1710
|
+
const layout = options.layout === void 0 || options.layout === baseLayout ? null : options.layout;
|
|
1711
|
+
const flags = decisionFlags(base, decisions);
|
|
1712
|
+
const plan = planSettings(options.home, {
|
|
1713
|
+
statusLine: mainKeyValue(options.theme ?? null, layout, flags),
|
|
1714
|
+
subagentStatusLine: panelKeyValue(options.theme ?? null, flags.filter((flag) => flag.startsWith("--style=")))
|
|
1715
|
+
}, options.force ?? false);
|
|
1716
|
+
if (plan.adds.length > 0 || plan.repoints.length > 0) writeBackupIfAbsent(options.home, plan);
|
|
1717
|
+
commitSettings(plan);
|
|
1718
|
+
syncRenderer(options.home);
|
|
1719
|
+
}
|
|
1720
|
+
//#endregion
|
|
1721
|
+
//#region src/demo-repo.ts
|
|
1722
|
+
var NAME = "Lab";
|
|
1723
|
+
var EMAIL = "lab@local";
|
|
1724
|
+
var D_INIT = "2026-09-01T10:00:00+00:00";
|
|
1725
|
+
var D_REMOTE = "2026-09-01T11:00:00+00:00";
|
|
1726
|
+
var D_LOCAL1 = "2026-09-02T09:00:00+00:00";
|
|
1727
|
+
var D_LOCAL2 = "2026-09-02T09:30:00+00:00";
|
|
1728
|
+
var D_STASH = "2026-09-02T10:00:00+00:00";
|
|
1729
|
+
var API_INIT = "export const api = {\n host: 'demo.local',\n};\n";
|
|
1730
|
+
function materializeDemoRepo(home) {
|
|
1731
|
+
const dir = join(home, "demo", "atlas-web");
|
|
1732
|
+
for (const sub of [
|
|
1733
|
+
"src",
|
|
1734
|
+
"docs",
|
|
1735
|
+
"scratch",
|
|
1736
|
+
"scripts"
|
|
1737
|
+
]) mkdirSync(join(dir, sub), { recursive: true });
|
|
326
1738
|
function git(args, date) {
|
|
327
1739
|
return execFileSync("git", [
|
|
328
1740
|
"-c",
|
|
@@ -439,434 +1851,289 @@ function materializeDemoRepo(home) {
|
|
|
439
1851
|
"src/index.js"
|
|
440
1852
|
]);
|
|
441
1853
|
write("src/api.js", `${API_INIT}// local tweak\n`);
|
|
442
|
-
write("src/style.css", "body { margin: 0; padding: 0; }\n");
|
|
443
|
-
write("docs/wip.txt", "");
|
|
444
|
-
write("scratch/todo.md", "");
|
|
445
|
-
write("scripts/deploy.sh", "");
|
|
446
|
-
write("note1.md", "");
|
|
447
|
-
write("note2.md", "");
|
|
448
|
-
return dir;
|
|
449
|
-
}
|
|
450
|
-
//#endregion
|
|
451
|
-
//#region src/payloads.ts
|
|
452
|
-
var PAYLOADS_DIR = fileURLToPath(new URL("../assets/payloads", import.meta.url));
|
|
453
|
-
var TICKS_DIR = fileURLToPath(new URL("../assets/ticks", import.meta.url));
|
|
454
|
-
var P1_WARM_IN = 1920;
|
|
455
|
-
var P1_RESETS_IN = {
|
|
456
|
-
five_hour: 13830,
|
|
457
|
-
seven_day: 518400,
|
|
458
|
-
spend_limit: 950400
|
|
459
|
-
};
|
|
460
|
-
var PANEL_STEP_S = 300;
|
|
461
|
-
function anchoredP1(repoDir, now) {
|
|
462
|
-
const payload = JSON.parse(readFileSync(join(PAYLOADS_DIR, "p1.json"), "utf8"));
|
|
463
|
-
const cache = payload.prompt_cache;
|
|
464
|
-
if (isObject(cache)) {
|
|
465
|
-
if (cache.warm === true) {
|
|
466
|
-
cache.expires_at = now + P1_WARM_IN;
|
|
467
|
-
cache.last_miss_at = now + P1_WARM_IN - 3900;
|
|
468
|
-
} else {
|
|
469
|
-
cache.expires_at = now - 10;
|
|
470
|
-
delete cache.last_miss_at;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
const limits = payload.rate_limits;
|
|
474
|
-
if (isObject(limits)) for (const [limitKey, offset] of Object.entries(P1_RESETS_IN)) {
|
|
475
|
-
const limit = limits[limitKey];
|
|
476
|
-
if (isObject(limit)) limit.resets_at = now + offset;
|
|
477
|
-
}
|
|
478
|
-
payload.workspace.current_dir = repoDir;
|
|
479
|
-
return `${JSON.stringify(payload, null, 2)}\n`;
|
|
480
|
-
}
|
|
481
|
-
function hasStart(value) {
|
|
482
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
483
|
-
}
|
|
484
|
-
function anchoredTick(now) {
|
|
485
|
-
const tick = JSON.parse(readFileSync(join(TICKS_DIR, "multi.json"), "utf8"));
|
|
486
|
-
const tasks = Array.isArray(tick.tasks) ? tick.tasks : [];
|
|
487
|
-
let started = 0;
|
|
488
|
-
const staggered = {
|
|
489
|
-
...tick,
|
|
490
|
-
tasks: tasks.map((task) => {
|
|
491
|
-
if (!hasStart(task.startTime)) return task;
|
|
492
|
-
started += 1;
|
|
493
|
-
return {
|
|
494
|
-
...task,
|
|
495
|
-
startTime: now - PANEL_STEP_S * started
|
|
496
|
-
};
|
|
497
|
-
})
|
|
498
|
-
};
|
|
499
|
-
return `${JSON.stringify(staggered, null, 2)}\n`;
|
|
500
|
-
}
|
|
501
|
-
function previewSources(home, now) {
|
|
502
|
-
const mainCapture = capturePath(home, "main");
|
|
503
|
-
if (existsSync(mainCapture)) {
|
|
504
|
-
const tickCapture = capturePath(home, "tick");
|
|
505
|
-
return {
|
|
506
|
-
cleanup: () => void 0,
|
|
507
|
-
main: readFileSync(mainCapture, "utf8"),
|
|
508
|
-
tick: existsSync(tickCapture) ? readFileSync(tickCapture, "utf8") : anchoredTick(now)
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
const demoHome = mkdtempSync(join(tmpdir(), "statusline-preview-"));
|
|
512
|
-
return {
|
|
513
|
-
cleanup: () => {
|
|
514
|
-
rmSync(demoHome, {
|
|
515
|
-
recursive: true,
|
|
516
|
-
force: true
|
|
517
|
-
});
|
|
518
|
-
},
|
|
519
|
-
main: anchoredP1(materializeDemoRepo(demoHome), now),
|
|
520
|
-
tick: anchoredTick(now)
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
function firstPanelRow(output) {
|
|
524
|
-
const [row] = output.split("\n");
|
|
525
|
-
if (row === "") return "";
|
|
526
|
-
const content = JSON.parse(row).content;
|
|
527
|
-
return typeof content === "string" ? content : "";
|
|
528
|
-
}
|
|
529
|
-
function runtimeRenderer(spec) {
|
|
530
|
-
const run = spawnSync("bash", [spec.bin], {
|
|
531
|
-
input: spec.stdin,
|
|
532
|
-
env: {
|
|
533
|
-
PATH: process.env.PATH ?? "",
|
|
534
|
-
...spec.env,
|
|
535
|
-
LC_ALL: "C",
|
|
536
|
-
TZ: "UTC"
|
|
537
|
-
},
|
|
538
|
-
timeout: 3e4
|
|
539
|
-
});
|
|
540
|
-
if (run.status !== 0) throw new Error(`render failed: ${run.stderr.toString("utf8")}`);
|
|
541
|
-
const stdout = run.stdout.toString("utf8");
|
|
542
|
-
const warn = run.stderr.toString("utf8").trim();
|
|
543
|
-
return warn === "" ? stdout : `${stdout}${warn}\n`;
|
|
544
|
-
}
|
|
545
|
-
//#endregion
|
|
546
|
-
//#region src/configure.ts
|
|
547
|
-
var SETTINGS_KEYS = ["statusLine", "subagentStatusLine"];
|
|
548
|
-
function layoutItems(layout, valid) {
|
|
549
|
-
const known = new Set(valid);
|
|
550
|
-
const items = [];
|
|
551
|
-
for (const cluster of parseClusters(layout)) for (const word of cluster) {
|
|
552
|
-
if (!known.has(word)) throw new Error(`unknown layout item '${word}' — valid items: ${valid.join(" ")}`);
|
|
553
|
-
if (!items.includes(word)) items.push(word);
|
|
554
|
-
}
|
|
555
|
-
return items;
|
|
556
|
-
}
|
|
557
|
-
function parseClusters(layout) {
|
|
558
|
-
const clusters = [];
|
|
559
|
-
let words = [];
|
|
560
|
-
let word = "";
|
|
561
|
-
let open = false;
|
|
562
|
-
function pushWord() {
|
|
563
|
-
if (word !== "") {
|
|
564
|
-
words.push(word);
|
|
565
|
-
word = "";
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
for (const c of layout) if (c === "{") {
|
|
569
|
-
if (open) throw new Error(`layout '${layout}': '{' inside a cluster`);
|
|
570
|
-
open = true;
|
|
571
|
-
words = [];
|
|
572
|
-
} else if (c === "}") {
|
|
573
|
-
if (!open) throw new Error(`layout '${layout}': '}' outside a cluster`);
|
|
574
|
-
pushWord();
|
|
575
|
-
open = false;
|
|
576
|
-
if (words.length > 0) clusters.push(words);
|
|
577
|
-
} else if (c === " ") {
|
|
578
|
-
if (open) pushWord();
|
|
579
|
-
else if (word !== "") throw new Error(`layout '${layout}': '${word}' sits outside a cluster`);
|
|
580
|
-
} else if (/[a-z0-9]/.test(c)) word += c;
|
|
581
|
-
else throw new Error(`layout '${layout}': '${c}' is not layout grammar (braces, item ids, spaces)`);
|
|
582
|
-
if (open) throw new Error(`layout '${layout}': unterminated cluster`);
|
|
583
|
-
if (word !== "") throw new Error(`layout '${layout}': '${word}' sits outside a cluster`);
|
|
584
|
-
if (clusters.length === 0) throw new Error(`layout '${layout}': no clusters`);
|
|
585
|
-
return clusters;
|
|
586
|
-
}
|
|
587
|
-
function isObject(value) {
|
|
588
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
589
|
-
}
|
|
590
|
-
function readOrNull(file) {
|
|
591
|
-
try {
|
|
592
|
-
return readFileSync(file, "utf8");
|
|
593
|
-
} catch (e) {
|
|
594
|
-
if (e.code === "ENOENT") return null;
|
|
595
|
-
throw e;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
function parseSettings(file, raw) {
|
|
599
|
-
let parsed;
|
|
600
|
-
try {
|
|
601
|
-
parsed = JSON.parse(raw);
|
|
602
|
-
} catch (e) {
|
|
603
|
-
throw new Error(`${file} is not valid JSON`, { cause: e });
|
|
604
|
-
}
|
|
605
|
-
if (!isObject(parsed)) throw new Error(`${file} does not hold a settings object`);
|
|
606
|
-
return parsed;
|
|
607
|
-
}
|
|
608
|
-
function memberCommand(value) {
|
|
609
|
-
if (!isObject(value) || value.type !== "command") return null;
|
|
610
|
-
return typeof value.command === "string" ? value.command : null;
|
|
611
|
-
}
|
|
612
|
-
function isOurMember(key, value) {
|
|
613
|
-
const command = memberCommand(value);
|
|
614
|
-
if (command === null) return false;
|
|
615
|
-
return key === "statusLine" ? isOurMainCommand(command) : command === subagentKeyValue;
|
|
616
|
-
}
|
|
617
|
-
function planSettings(home, mainCommand, force) {
|
|
618
|
-
const file = join(home, ".claude", "settings.json");
|
|
619
|
-
const wanted = {
|
|
620
|
-
statusLine: mainCommand,
|
|
621
|
-
subagentStatusLine: subagentKeyValue
|
|
622
|
-
};
|
|
623
|
-
const raw = readOrNull(file);
|
|
624
|
-
if (raw === null) return {
|
|
625
|
-
adds: SETTINGS_KEYS.map((key) => [key, wanted[key]]),
|
|
626
|
-
file,
|
|
627
|
-
raw: null,
|
|
628
|
-
repoints: []
|
|
629
|
-
};
|
|
630
|
-
const parsed = parseSettings(file, raw);
|
|
631
|
-
const adds = [];
|
|
632
|
-
const repoints = [];
|
|
633
|
-
for (const key of SETTINGS_KEYS) {
|
|
634
|
-
const command = wanted[key];
|
|
635
|
-
const value = parsed[key];
|
|
636
|
-
if (value === void 0) adds.push([key, command]);
|
|
637
|
-
else if (memberCommand(value) === command) continue;
|
|
638
|
-
else if (isOurMember(key, value) || force) repoints.push([key, command]);
|
|
639
|
-
else throw new Error(`settings.json ${key} is foreign — rerun with --force added to your configuration flags to take it over`);
|
|
640
|
-
}
|
|
641
|
-
return {
|
|
642
|
-
adds,
|
|
643
|
-
file,
|
|
644
|
-
raw,
|
|
645
|
-
repoints
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
function settingsValue(command) {
|
|
649
|
-
return JSON.stringify({
|
|
650
|
-
command,
|
|
651
|
-
type: "command"
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
function skipString(raw, at) {
|
|
655
|
-
let i = at + 1;
|
|
656
|
-
while (i < raw.length) if (raw[i] === "\\") i += 2;
|
|
657
|
-
else if (raw[i] === "\"") return i + 1;
|
|
658
|
-
else i += 1;
|
|
659
|
-
throw new Error("unterminated string in settings.json");
|
|
660
|
-
}
|
|
661
|
-
function skipValue(raw, at) {
|
|
662
|
-
const first = raw[at];
|
|
663
|
-
if (first === "\"") return skipString(raw, at);
|
|
664
|
-
if (first === "{" || first === "[") {
|
|
665
|
-
let depth = 0;
|
|
666
|
-
let i = at;
|
|
667
|
-
while (i < raw.length) {
|
|
668
|
-
const c = raw[i];
|
|
669
|
-
if (c === "\"") i = skipString(raw, i);
|
|
670
|
-
else if (c === "{" || c === "[") {
|
|
671
|
-
depth += 1;
|
|
672
|
-
i += 1;
|
|
673
|
-
} else if (c === "}" || c === "]") {
|
|
674
|
-
depth -= 1;
|
|
675
|
-
i += 1;
|
|
676
|
-
if (depth === 0) return i;
|
|
677
|
-
} else i += 1;
|
|
678
|
-
}
|
|
679
|
-
throw new Error("unterminated container in settings.json");
|
|
680
|
-
}
|
|
681
|
-
let i = at;
|
|
682
|
-
while (i < raw.length && !",}]".includes(raw[i] ?? "") && !/\s/.test(raw[i] ?? "~")) i += 1;
|
|
683
|
-
return i;
|
|
1854
|
+
write("src/style.css", "body { margin: 0; padding: 0; }\n");
|
|
1855
|
+
write("docs/wip.txt", "");
|
|
1856
|
+
write("scratch/todo.md", "");
|
|
1857
|
+
write("scripts/deploy.sh", "");
|
|
1858
|
+
write("note1.md", "");
|
|
1859
|
+
write("note2.md", "");
|
|
1860
|
+
return dir;
|
|
684
1861
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
1862
|
+
//#endregion
|
|
1863
|
+
//#region src/render/panel.ts
|
|
1864
|
+
var CYAN = "\x1B[36m";
|
|
1865
|
+
var GREEN = "\x1B[32m";
|
|
1866
|
+
var RED = "\x1B[31m";
|
|
1867
|
+
var RESET = "\x1B[0m";
|
|
1868
|
+
var YELLOW = "\x1B[33m";
|
|
1869
|
+
var MS_THRESHOLD = 2e11;
|
|
1870
|
+
var STEPS = [
|
|
1871
|
+
["descd", 1],
|
|
1872
|
+
["durd", 1],
|
|
1873
|
+
["statd", 1],
|
|
1874
|
+
["barb", 6],
|
|
1875
|
+
["modeld", 1],
|
|
1876
|
+
["barb", 4],
|
|
1877
|
+
["barb", 0],
|
|
1878
|
+
["modeld", 2],
|
|
1879
|
+
["statd", 2]
|
|
1880
|
+
];
|
|
1881
|
+
function field(source, key, fallback) {
|
|
1882
|
+
return orElse(source[key], fallback);
|
|
1883
|
+
}
|
|
1884
|
+
function cell(value) {
|
|
1885
|
+
return tsvEscape(jqText(value));
|
|
1886
|
+
}
|
|
1887
|
+
function truncateDesc(desc) {
|
|
1888
|
+
const chars = Array.from(desc);
|
|
1889
|
+
return chars.length > 24 ? `${chars.slice(0, 23).join("")}…` : desc;
|
|
1890
|
+
}
|
|
1891
|
+
function intValue(text) {
|
|
1892
|
+
return /^-?\d+$/.test(text) ? Number(text) : null;
|
|
1893
|
+
}
|
|
1894
|
+
function stylePick(picks) {
|
|
1895
|
+
const spec = specFor("style");
|
|
1896
|
+
if (spec === void 0) return "plain";
|
|
1897
|
+
const wanted = picks?.style ?? "";
|
|
1898
|
+
const alt = wanted === "" ? spec.default : wanted;
|
|
1899
|
+
if (!spec.alternatives.includes(alt)) {
|
|
1900
|
+
warn(`statusline: style=${alt} is not available, using style=${spec.default}`);
|
|
1901
|
+
return spec.default;
|
|
712
1902
|
}
|
|
713
|
-
return
|
|
714
|
-
}
|
|
715
|
-
function rootMemberValueSpan(raw, key) {
|
|
716
|
-
const member = rootMembers(raw).find((m) => m.name === key);
|
|
717
|
-
return member === void 0 ? null : [member.valueStart, member.valueEnd];
|
|
1903
|
+
return alt;
|
|
718
1904
|
}
|
|
719
|
-
function
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
const at = members.findIndex((m) => m.name === key);
|
|
724
|
-
if (at === -1) throw new Error(`cannot find the "${key}" member to remove in settings.json`);
|
|
725
|
-
if (members.length === 1) return "{}";
|
|
726
|
-
const member = members[at];
|
|
727
|
-
if (at > 0) {
|
|
728
|
-
let start = member.nameStart;
|
|
729
|
-
while (/\s/.test(text[start - 1] ?? "~")) start -= 1;
|
|
730
|
-
text = `${text.slice(0, start - 1)}${text.slice(member.valueEnd)}`;
|
|
731
|
-
} else text = `${text.slice(0, member.nameStart)}${text.slice(members[1].nameStart)}`;
|
|
732
|
-
}
|
|
733
|
-
return text;
|
|
1905
|
+
function availColumns(tick) {
|
|
1906
|
+
const text = jqText(isRecord(tick) ? field(tick, "columns", 200) : 200);
|
|
1907
|
+
const columns = /^\d+$/.test(text) ? Number(text) : 200;
|
|
1908
|
+
return Math.max(columns, 20) - 1;
|
|
734
1909
|
}
|
|
735
|
-
function
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1910
|
+
function extractFields(task) {
|
|
1911
|
+
return {
|
|
1912
|
+
ctx: cell(field(task, "contextWindowSize", 0)),
|
|
1913
|
+
desc: cell(truncateDesc(jqText(field(task, "description", "")))),
|
|
1914
|
+
effort: cell(field(task, "effort", "")),
|
|
1915
|
+
id: cell(field(task, "id", "")),
|
|
1916
|
+
label: cell(field(task, "label", "")),
|
|
1917
|
+
model: cell(field(task, "model", "")),
|
|
1918
|
+
name: cell(field(task, "name", "")),
|
|
1919
|
+
start: cell(field(task, "startTime", 0)),
|
|
1920
|
+
tokens: cell(field(task, "tokenCount", 0))
|
|
1921
|
+
};
|
|
742
1922
|
}
|
|
743
|
-
function
|
|
744
|
-
|
|
1923
|
+
function makeBar(pct, width) {
|
|
1924
|
+
let f = Math.trunc(pct * width / 100);
|
|
1925
|
+
if (f > width) f = width;
|
|
1926
|
+
let bar = "";
|
|
1927
|
+
for (let i = 0; i < width; i++) bar += i < f ? "█" : "░";
|
|
1928
|
+
return bar;
|
|
1929
|
+
}
|
|
1930
|
+
function duration(startText, now) {
|
|
1931
|
+
const start = intValue(startText);
|
|
1932
|
+
if (start === null || start <= 0) return "";
|
|
1933
|
+
const seconds = start > MS_THRESHOLD ? Math.trunc(start / 1e3) : start;
|
|
1934
|
+
const elapsed = Math.trunc(now) - seconds;
|
|
1935
|
+
if (elapsed < 0) return "";
|
|
1936
|
+
return `${Math.trunc(elapsed / 60)}m${pad2(Math.trunc(elapsed % 60))}s`;
|
|
1937
|
+
}
|
|
1938
|
+
function rowFormats(fields, now) {
|
|
1939
|
+
const tokens = intValue(fields.tokens) ?? 0;
|
|
1940
|
+
const ctx = intValue(fields.ctx) ?? 0;
|
|
1941
|
+
let ctxText = fields.ctx;
|
|
1942
|
+
if (ctx >= 1e6) ctxText = fmtM(ctx);
|
|
1943
|
+
else if (ctx >= 1e3) ctxText = fmtK(ctx, 0);
|
|
1944
|
+
return {
|
|
1945
|
+
ctx: ctxText,
|
|
1946
|
+
dur: duration(fields.start, now),
|
|
1947
|
+
tickTok: tokens >= 1e3 ? fmtK(tokens, 0) : String(tokens),
|
|
1948
|
+
tokens: tokens >= 1e3 ? fmtK(tokens, 1) : String(tokens)
|
|
1949
|
+
};
|
|
745
1950
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
if (
|
|
752
|
-
|
|
1951
|
+
function renderRow(fields, formats, state, sep) {
|
|
1952
|
+
let s = fields.label === "" ? fields.name : fields.label;
|
|
1953
|
+
if (fields.desc !== "" && fields.desc !== fields.label && state.descd === 0) s += ` ${fields.desc}`;
|
|
1954
|
+
if (fields.model !== "") {
|
|
1955
|
+
let name = fields.model;
|
|
1956
|
+
if (state.modeld >= 1) name = stripModelSuffix(name);
|
|
1957
|
+
const effort = state.modeld >= 2 ? "" : fields.effort;
|
|
1958
|
+
if (effort !== "") name += ` ${effort}`;
|
|
1959
|
+
s += `${sep}${CYAN}${name}${RESET}`;
|
|
753
1960
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
const file = backupPath(home);
|
|
764
|
-
if (existsSync(file)) return;
|
|
765
|
-
const keys = {};
|
|
766
|
-
if (plan.raw !== null) for (const key of SETTINGS_KEYS) {
|
|
767
|
-
const span = rootMemberValueSpan(plan.raw, key);
|
|
768
|
-
if (span === null) continue;
|
|
769
|
-
const memberRaw = plan.raw.slice(span[0], span[1]);
|
|
770
|
-
if (isOurMember(key, JSON.parse(memberRaw))) continue;
|
|
771
|
-
keys[key] = memberRaw;
|
|
1961
|
+
const ctx = intValue(fields.ctx);
|
|
1962
|
+
if (ctx !== null && ctx > 0) {
|
|
1963
|
+
const tokens = intValue(fields.tokens) ?? 0;
|
|
1964
|
+
const pct = Math.trunc(tokens * 100 / ctx);
|
|
1965
|
+
const barColor = pct >= 90 ? RED : pct >= 70 ? YELLOW : GREEN;
|
|
1966
|
+
if (state.barb > 0) s += `${sep}${barColor}${makeBar(pct, state.barb)}${RESET} ${pct}%`;
|
|
1967
|
+
else s += `${sep}${barColor}${pct}%${RESET}`;
|
|
1968
|
+
const stats = state.statd === 1 ? formats.tickTok : state.statd === 2 ? "" : `${formats.tokens}/${formats.ctx}`;
|
|
1969
|
+
if (stats !== "") s += ` ${stats}`;
|
|
772
1970
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1971
|
+
if (formats.dur !== "" && state.durd === 0) s += `${sep}${formats.dur}`;
|
|
1972
|
+
return s;
|
|
1973
|
+
}
|
|
1974
|
+
function vlen(text) {
|
|
1975
|
+
return Array.from(stripSgr(text)).length;
|
|
1976
|
+
}
|
|
1977
|
+
function fitRow(fields, formats, sep, avail) {
|
|
1978
|
+
const state = {
|
|
1979
|
+
barb: 10,
|
|
1980
|
+
descd: 0,
|
|
1981
|
+
durd: 0,
|
|
1982
|
+
modeld: 0,
|
|
1983
|
+
statd: 0
|
|
1984
|
+
};
|
|
1985
|
+
let out = renderRow(fields, formats, state, sep);
|
|
1986
|
+
if (vlen(out) <= avail) return out;
|
|
1987
|
+
for (const [key, value] of STEPS) {
|
|
1988
|
+
state[key] = value;
|
|
1989
|
+
out = renderRow(fields, formats, state, sep);
|
|
1990
|
+
if (vlen(out) <= avail) return out;
|
|
788
1991
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
bin: join(runtime.dir, "subagent.sh"),
|
|
808
|
-
env: {
|
|
809
|
-
COLUMNS: "200",
|
|
810
|
-
HOME: home,
|
|
811
|
-
NOW: now
|
|
812
|
-
},
|
|
813
|
-
stdin: sources.tick
|
|
814
|
-
}).replace(/\n+$/, ""))}\n`;
|
|
815
|
-
} finally {
|
|
816
|
-
sources.cleanup();
|
|
1992
|
+
return out;
|
|
1993
|
+
}
|
|
1994
|
+
function emitLine(id, content) {
|
|
1995
|
+
return `{"id":${JSON.stringify(id)},"content":${JSON.stringify(content)}}`;
|
|
1996
|
+
}
|
|
1997
|
+
function renderPanel(input) {
|
|
1998
|
+
const tick = JSON.parse(input.payload);
|
|
1999
|
+
const avail = availColumns(tick);
|
|
2000
|
+
const sep = styleSeparators(stylePick(input.picks)).sep;
|
|
2001
|
+
const tasks = isRecord(tick) && Array.isArray(tick.tasks) ? tick.tasks : [];
|
|
2002
|
+
let out = "";
|
|
2003
|
+
for (const task of tasks) {
|
|
2004
|
+
if (!isRecord(task)) continue;
|
|
2005
|
+
const fields = extractFields(task);
|
|
2006
|
+
if (fields.id === "") continue;
|
|
2007
|
+
const row = fitRow(fields, rowFormats(fields, input.now), sep, avail);
|
|
2008
|
+
const content = input.noColor === true ? stripSgr(row) : row;
|
|
2009
|
+
out += `${emitLine(fields.id, content)}\n`;
|
|
817
2010
|
}
|
|
2011
|
+
return out;
|
|
818
2012
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
2013
|
+
//#endregion
|
|
2014
|
+
//#region src/payloads.ts
|
|
2015
|
+
var PAYLOADS_DIR = fileURLToPath(new URL("../assets/payloads", import.meta.url));
|
|
2016
|
+
var TICKS_DIR = fileURLToPath(new URL("../assets/ticks", import.meta.url));
|
|
2017
|
+
var P1_WARM_IN = 1920;
|
|
2018
|
+
var P1_RESETS_IN = {
|
|
2019
|
+
five_hour: 13830,
|
|
2020
|
+
seven_day: 518400,
|
|
2021
|
+
spend_limit: 950400
|
|
2022
|
+
};
|
|
2023
|
+
var PANEL_STEP_S = 300;
|
|
2024
|
+
function anchoredP1(repoDir, now) {
|
|
2025
|
+
const payload = JSON.parse(readFileSync(join(PAYLOADS_DIR, "p1.json"), "utf8"));
|
|
2026
|
+
const cache = payload.prompt_cache;
|
|
2027
|
+
if (isObject(cache)) {
|
|
2028
|
+
if (cache.warm === true) {
|
|
2029
|
+
cache.expires_at = now + P1_WARM_IN;
|
|
2030
|
+
cache.last_miss_at = now + P1_WARM_IN - 3900;
|
|
2031
|
+
} else {
|
|
2032
|
+
cache.expires_at = now - 10;
|
|
2033
|
+
delete cache.last_miss_at;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
const limits = payload.rate_limits;
|
|
2037
|
+
if (isObject(limits)) for (const [limitKey, offset] of Object.entries(P1_RESETS_IN)) {
|
|
2038
|
+
const limit = limits[limitKey];
|
|
2039
|
+
if (isObject(limit)) limit.resets_at = now + offset;
|
|
2040
|
+
}
|
|
2041
|
+
payload.workspace.current_dir = repoDir;
|
|
2042
|
+
return `${JSON.stringify(payload, null, 2)}\n`;
|
|
822
2043
|
}
|
|
823
|
-
function
|
|
824
|
-
return
|
|
2044
|
+
function hasStart(value) {
|
|
2045
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
825
2046
|
}
|
|
826
|
-
function
|
|
827
|
-
const
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
2047
|
+
function anchoredTick(now) {
|
|
2048
|
+
const tick = JSON.parse(readFileSync(join(TICKS_DIR, "multi.json"), "utf8"));
|
|
2049
|
+
const tasks = Array.isArray(tick.tasks) ? tick.tasks : [];
|
|
2050
|
+
let started = 0;
|
|
2051
|
+
const staggered = {
|
|
2052
|
+
...tick,
|
|
2053
|
+
tasks: tasks.map((task) => {
|
|
2054
|
+
if (!hasStart(task.startTime)) return task;
|
|
2055
|
+
started += 1;
|
|
2056
|
+
return {
|
|
2057
|
+
...task,
|
|
2058
|
+
startTime: now - PANEL_STEP_S * started
|
|
2059
|
+
};
|
|
2060
|
+
})
|
|
834
2061
|
};
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
}
|
|
842
|
-
const values = {};
|
|
843
|
-
for (const item of items) {
|
|
844
|
-
let base;
|
|
845
|
-
if (options.fallback === "default") base = byItem.get(item)?.default;
|
|
846
|
-
else if (options.fallback === "existing") base = existingValue(existing.values, item);
|
|
847
|
-
else if (!explicit) base = existingValue(existing.values, item) ?? byItem.get(item)?.default;
|
|
848
|
-
if (base !== void 0) values[item] = base;
|
|
849
|
-
}
|
|
850
|
-
const missing = items.filter((item) => !(item in values) && !(item in variants));
|
|
851
|
-
if (missing.length > 0) throw new Error(`no variant for layout item${missing.length > 1 ? "s" : ""} ${missing.join(" ")} — pass --<item> <alt> for each or use --fallback=default|existing`);
|
|
852
|
-
for (const [item, alt] of Object.entries(variants)) values[item] = alt;
|
|
853
|
-
for (const item of items) {
|
|
854
|
-
const alt = values[item];
|
|
855
|
-
const offered = byItem.get(item)?.alternatives ?? [];
|
|
856
|
-
if (!offered.includes(alt)) throw new Error(`unknown variant '${alt}' for item '${item}' — valid: ${offered.join(" | ")}`);
|
|
857
|
-
}
|
|
858
|
-
if (options.dryRun) {
|
|
859
|
-
const now = String(Math.floor(Date.now() / 1e3));
|
|
2062
|
+
return `${JSON.stringify(staggered, null, 2)}\n`;
|
|
2063
|
+
}
|
|
2064
|
+
function previewSources(home, now) {
|
|
2065
|
+
const mainCapture = capturePath(home, "main");
|
|
2066
|
+
if (existsSync(mainCapture)) {
|
|
2067
|
+
const tickCapture = capturePath(home, "tick");
|
|
860
2068
|
return {
|
|
861
|
-
|
|
862
|
-
|
|
2069
|
+
cleanup: () => void 0,
|
|
2070
|
+
main: readFileSync(mainCapture, "utf8"),
|
|
2071
|
+
tick: existsSync(tickCapture) ? readFileSync(tickCapture, "utf8") : anchoredTick(now)
|
|
863
2072
|
};
|
|
864
2073
|
}
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
2074
|
+
const demoHome = mkdtempSync(join(tmpdir(), "statusline-preview-"));
|
|
2075
|
+
return {
|
|
2076
|
+
cleanup: () => {
|
|
2077
|
+
rmSync(demoHome, {
|
|
2078
|
+
recursive: true,
|
|
2079
|
+
force: true
|
|
2080
|
+
});
|
|
2081
|
+
},
|
|
2082
|
+
main: anchoredP1(materializeDemoRepo(demoHome), now),
|
|
2083
|
+
tick: anchoredTick(now)
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
function firstPanelRow(output) {
|
|
2087
|
+
const [row] = output.split("\n");
|
|
2088
|
+
if (row === "") return "";
|
|
2089
|
+
const content = JSON.parse(row).content;
|
|
2090
|
+
return typeof content === "string" ? content : "";
|
|
2091
|
+
}
|
|
2092
|
+
function renderPreview(bag) {
|
|
2093
|
+
const plain = bag.plain === true || (process.env.NO_COLOR ?? "") !== "";
|
|
2094
|
+
const paint = resolvePaint({
|
|
2095
|
+
layout: bag.layout,
|
|
2096
|
+
picks: bag.values,
|
|
2097
|
+
theme: bag.theme
|
|
2098
|
+
});
|
|
2099
|
+
return {
|
|
2100
|
+
line: renderStatusline({
|
|
2101
|
+
home: bag.home,
|
|
2102
|
+
layout: paint.layout,
|
|
2103
|
+
noColor: plain,
|
|
2104
|
+
now: Number(bag.now),
|
|
2105
|
+
payload: bag.main,
|
|
2106
|
+
picks: paint.picks,
|
|
2107
|
+
...bag.width === void 0 ? {} : { columns: bag.width }
|
|
2108
|
+
}).replace(/\n+$/, ""),
|
|
2109
|
+
panel: firstPanelRow(renderPanel({
|
|
2110
|
+
noColor: plain,
|
|
2111
|
+
now: Number(bag.now),
|
|
2112
|
+
payload: bag.tick,
|
|
2113
|
+
picks: paint.picks
|
|
2114
|
+
}).replace(/\n+$/, ""))
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
//#endregion
|
|
2118
|
+
//#region src/preview.ts
|
|
2119
|
+
function preview(options) {
|
|
2120
|
+
validateSelection(options);
|
|
2121
|
+
const sources = previewSources(options.home, Number(options.now));
|
|
2122
|
+
try {
|
|
2123
|
+
const { line, panel } = renderPreview({
|
|
2124
|
+
home: options.home,
|
|
2125
|
+
layout: options.layout,
|
|
2126
|
+
main: sources.main,
|
|
2127
|
+
now: options.now,
|
|
2128
|
+
plain: options.plain ?? !process.stdout.isTTY,
|
|
2129
|
+
tick: sources.tick,
|
|
2130
|
+
theme: options.theme,
|
|
2131
|
+
values: options.variants
|
|
2132
|
+
});
|
|
2133
|
+
return `preview at 200 columns — nothing written\n${line}\npanel ${panel}\n`;
|
|
2134
|
+
} finally {
|
|
2135
|
+
sources.cleanup();
|
|
2136
|
+
}
|
|
870
2137
|
}
|
|
871
2138
|
//#endregion
|
|
872
2139
|
//#region src/restore.ts
|
|
@@ -952,18 +2219,20 @@ function restore(options) {
|
|
|
952
2219
|
next = null;
|
|
953
2220
|
} else next = text;
|
|
954
2221
|
}
|
|
955
|
-
const dataDir = join(options.home,
|
|
2222
|
+
const dataDir = join(options.home, DATA_DIR);
|
|
956
2223
|
const capturesDir = join(dataDir, "captures");
|
|
957
2224
|
const backupFile = backupPath(options.home);
|
|
958
|
-
|
|
2225
|
+
const rendererFile = renderMjsPath(options.home);
|
|
2226
|
+
if (actions.length === 0 && !existsSync(backupFile) && !existsSync(capturesDir) && !existsSync(rendererFile)) return {
|
|
959
2227
|
mode: "nothing",
|
|
960
|
-
text: "nothing to restore — no lab keys in settings.json, no backup, no captures"
|
|
2228
|
+
text: "nothing to restore — no lab keys in settings.json, no backup, no captures, no renderer"
|
|
961
2229
|
};
|
|
962
2230
|
if (options.dryRun) {
|
|
963
2231
|
const lines = ["restore plan — nothing written"];
|
|
964
2232
|
for (const action of actions) lines.push(action.kind === "remove" ? ` remove ${action.key} (absent before the lab)` : ` splice the saved ${action.key} value back`);
|
|
965
2233
|
if (deleteFile) lines.push(" delete settings.json (the lab created it and it is now empty)");
|
|
966
2234
|
if (existsSync(capturesDir)) lines.push(" delete captures/");
|
|
2235
|
+
if (existsSync(rendererFile)) lines.push(" delete render.mjs");
|
|
967
2236
|
if (existsSync(backupFile)) lines.push(" delete backup.json");
|
|
968
2237
|
lines.push(" remove the data dir if empty");
|
|
969
2238
|
return {
|
|
@@ -974,6 +2243,7 @@ function restore(options) {
|
|
|
974
2243
|
if (deleteFile) rmSync(settings, { force: true });
|
|
975
2244
|
else if (next !== null && actions.length > 0) writeFileSync(settings, next);
|
|
976
2245
|
clearCaptures(capturesDir);
|
|
2246
|
+
rmSync(rendererFile, { force: true });
|
|
977
2247
|
rmSync(backupFile, { force: true });
|
|
978
2248
|
rmdirIfEmpty(dataDir);
|
|
979
2249
|
let line;
|
|
@@ -987,7 +2257,6 @@ function restore(options) {
|
|
|
987
2257
|
}
|
|
988
2258
|
//#endregion
|
|
989
2259
|
//#region src/status.ts
|
|
990
|
-
var INSTALL_FIX = "claude plugin install statusline@agentic";
|
|
991
2260
|
function keyState(key, value) {
|
|
992
2261
|
if (value === void 0) return { kind: "absent" };
|
|
993
2262
|
if (isOurMember(key, value)) return { kind: "ours" };
|
|
@@ -998,28 +2267,32 @@ function keyState(key, value) {
|
|
|
998
2267
|
}
|
|
999
2268
|
function keyRow(key, state, detail = "") {
|
|
1000
2269
|
if (state.kind === "ours") return `${key}: ours${detail === "" ? "" : ` — ${detail}`}`;
|
|
1001
|
-
if (state.kind === "absent") return `${key}: absent — fix: rerun configure --
|
|
1002
|
-
return `${key}: foreign${state.command === null ? "" : ` (${state.command})`} — fix: rerun configure --force --
|
|
2270
|
+
if (state.kind === "absent") return `${key}: absent — fix: rerun configure --theme classic`;
|
|
2271
|
+
return `${key}: foreign${state.command === null ? "" : ` (${state.command})`} — fix: rerun configure --force --theme classic`;
|
|
1003
2272
|
}
|
|
1004
|
-
function
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
return items;
|
|
2273
|
+
function keyItems(config) {
|
|
2274
|
+
const layout = config.layout === null ? [] : layoutItemsOf(config.layout);
|
|
2275
|
+
return [.../* @__PURE__ */ new Set([...layout, ...Object.keys(config.values)])];
|
|
1008
2276
|
}
|
|
1009
2277
|
function configDetail(config) {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
2278
|
+
const assignments = keyItems(config).filter((item) => Object.hasOwn(config.values, item)).map((item) => `${item}=${config.values[item]}`);
|
|
2279
|
+
return [...config.layout === null ? [] : [`layout='${config.layout}'`], ...assignments].join(" ");
|
|
2280
|
+
}
|
|
2281
|
+
function panelDetail(config) {
|
|
2282
|
+
return [...config.theme === void 0 ? [] : [`theme=${config.theme}`], ...Object.entries(config.values).map(([item, alt]) => `${item}=${alt}`)].join(" ");
|
|
2283
|
+
}
|
|
2284
|
+
function themeRow(config) {
|
|
2285
|
+
if (config.theme === void 0) return [];
|
|
2286
|
+
const swaps = Object.entries(config.values).map(([item, alt]) => `+${item}=${alt}`);
|
|
2287
|
+
return [`theme: ${[config.theme, ...swaps].join(" ")}`];
|
|
1013
2288
|
}
|
|
1014
2289
|
function findingText(finding) {
|
|
1015
2290
|
return finding.kind === "unknown-item" ? `unknown item '${finding.item}'` : `unknown variant '${finding.alt}' for '${finding.item}'`;
|
|
1016
2291
|
}
|
|
1017
|
-
function driftFindings(config
|
|
1018
|
-
if (config.layout === null) return [];
|
|
1019
|
-
const byItem = new Map(runtime.items.map((entry) => [entry.item, entry]));
|
|
2292
|
+
function driftFindings(config) {
|
|
1020
2293
|
const findings = [];
|
|
1021
|
-
for (const item of
|
|
1022
|
-
const entry =
|
|
2294
|
+
for (const item of keyItems(config)) {
|
|
2295
|
+
const entry = specFor(item);
|
|
1023
2296
|
if (entry === void 0) {
|
|
1024
2297
|
findings.push({
|
|
1025
2298
|
item,
|
|
@@ -1036,21 +2309,43 @@ function driftFindings(config, runtime) {
|
|
|
1036
2309
|
}
|
|
1037
2310
|
return findings;
|
|
1038
2311
|
}
|
|
1039
|
-
function
|
|
1040
|
-
|
|
1041
|
-
|
|
2312
|
+
function nodeOnPath(pathVar) {
|
|
2313
|
+
return pathVar.split(delimiter).filter((dir) => dir !== "").map((dir) => join(dir, "node")).find(existsSync) ?? null;
|
|
2314
|
+
}
|
|
2315
|
+
function rendererHash(bytes) {
|
|
2316
|
+
return createHash("sha256").update(bytes).digest("hex").slice(0, 12);
|
|
2317
|
+
}
|
|
2318
|
+
function rendererState(home) {
|
|
2319
|
+
if (!existsSync(BUNDLED_RENDERER)) return { kind: "no-bundle" };
|
|
2320
|
+
const cli = readFileSync(BUNDLED_RENDERER);
|
|
2321
|
+
const dataFile = renderMjsPath(home);
|
|
2322
|
+
if (!existsSync(dataFile)) return { kind: "missing" };
|
|
2323
|
+
const hash = rendererHash(readFileSync(dataFile));
|
|
2324
|
+
return hash === rendererHash(cli) ? {
|
|
2325
|
+
hash,
|
|
2326
|
+
kind: "current"
|
|
2327
|
+
} : {
|
|
2328
|
+
cli: rendererHash(cli),
|
|
2329
|
+
data: hash,
|
|
2330
|
+
kind: "stale"
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
function rendererRow(state) {
|
|
2334
|
+
if (state.kind === "current") return `renderer: current — ${state.hash}`;
|
|
2335
|
+
if (state.kind === "stale") return `renderer: stale — data ${state.data}, this CLI ${state.cli} — fix: rerun configure --force --theme classic`;
|
|
2336
|
+
if (state.kind === "no-bundle") return "renderer: this CLI ships no render.mjs — fix: rerun /lab to refresh the CLI";
|
|
2337
|
+
return "renderer: missing — fix: rerun configure --force --theme classic";
|
|
1042
2338
|
}
|
|
1043
|
-
function configRow(findings
|
|
2339
|
+
function configRow(findings) {
|
|
1044
2340
|
if (findings.length === 0) return "config: no drift";
|
|
1045
|
-
|
|
1046
|
-
return `config: drift — ${findings.map(findingText).join(", ")} — fix: ${fix}`;
|
|
2341
|
+
return `config: drift — ${findings.map(findingText).join(", ")} — fix: rerun configure --theme classic`;
|
|
1047
2342
|
}
|
|
1048
2343
|
function backupRow(home) {
|
|
1049
2344
|
let backup;
|
|
1050
2345
|
try {
|
|
1051
2346
|
backup = readBackup(home);
|
|
1052
2347
|
} catch {
|
|
1053
|
-
return `backup: unreadable — fix: delete ~/${
|
|
2348
|
+
return `backup: unreadable — fix: delete ~/${DATA_DIR}/backup.json`;
|
|
1054
2349
|
}
|
|
1055
2350
|
if (backup === null) return "backup: absent";
|
|
1056
2351
|
const saved = SETTINGS_KEYS.filter((key) => backup.keys[key] !== void 0);
|
|
@@ -1079,29 +2374,25 @@ function settingsMembers(home) {
|
|
|
1079
2374
|
const raw = readOrNull(file);
|
|
1080
2375
|
return raw === null ? {} : parseSettings(file, raw);
|
|
1081
2376
|
}
|
|
1082
|
-
function resolveOrNull(home) {
|
|
1083
|
-
try {
|
|
1084
|
-
return resolveRuntime({ home });
|
|
1085
|
-
} catch {
|
|
1086
|
-
return null;
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
2377
|
function status(options) {
|
|
1090
|
-
const runtime = resolveOrNull(options.home);
|
|
1091
2378
|
const members = settingsMembers(options.home);
|
|
1092
2379
|
const main = keyState("statusLine", members.statusLine);
|
|
1093
2380
|
const subagent = keyState("subagentStatusLine", members.subagentStatusLine);
|
|
1094
2381
|
const config = readKeyConfig(options.home);
|
|
1095
|
-
const
|
|
2382
|
+
const panelConfig = parsePanelCommand(memberCommand(members.subagentStatusLine));
|
|
2383
|
+
const findings = main.kind === "ours" ? driftFindings(config) : [];
|
|
2384
|
+
const node = nodeOnPath(options.path ?? process.env.PATH ?? "");
|
|
2385
|
+
const renderer = rendererState(options.home);
|
|
1096
2386
|
const rows = [
|
|
1097
|
-
|
|
2387
|
+
node === null ? "node: missing — fix: install node ≥ 18 from nodejs.org, then restart Claude Code" : `node: on PATH (${node})`,
|
|
2388
|
+
rendererRow(renderer),
|
|
1098
2389
|
keyRow("statusLine", main, configDetail(config)),
|
|
1099
|
-
keyRow("subagentStatusLine", subagent),
|
|
1100
|
-
...
|
|
2390
|
+
keyRow("subagentStatusLine", subagent, panelDetail(panelConfig)),
|
|
2391
|
+
...main.kind === "ours" ? [configRow(findings), ...themeRow(config)] : [],
|
|
1101
2392
|
backupRow(options.home),
|
|
1102
2393
|
capturesRow(options.home)
|
|
1103
2394
|
];
|
|
1104
|
-
const healthy =
|
|
2395
|
+
const healthy = node !== null && renderer.kind === "current" && main.kind === "ours" && subagent.kind === "ours" && findings.length === 0;
|
|
1105
2396
|
return {
|
|
1106
2397
|
healthy,
|
|
1107
2398
|
rows: [...rows, healthy ? "healthy" : "unhealthy"]
|
|
@@ -1165,8 +2456,8 @@ function terminalDeps(input = process.stdin, output = process.stdout) {
|
|
|
1165
2456
|
render(frame) {
|
|
1166
2457
|
output.write(`\x1b[2J\x1b[H${frame}\n`);
|
|
1167
2458
|
},
|
|
1168
|
-
preview(
|
|
1169
|
-
return
|
|
2459
|
+
preview(bag) {
|
|
2460
|
+
return renderPreview(bag);
|
|
1170
2461
|
}
|
|
1171
2462
|
};
|
|
1172
2463
|
}
|
|
@@ -1177,107 +2468,148 @@ var WIDTHS = [
|
|
|
1177
2468
|
120,
|
|
1178
2469
|
200
|
|
1179
2470
|
];
|
|
1180
|
-
var
|
|
2471
|
+
var THEME_KEYMAP = "j/k focus · w width · enter pick · q cancel";
|
|
2472
|
+
var REFINE_KEYMAP = "j/k move · h/l variant · s none · t themes · w width · enter save · q cancel";
|
|
2473
|
+
var STYLE = "style";
|
|
2474
|
+
function opensWizard(args, isTty) {
|
|
2475
|
+
return isTty && args.layout === void 0 && args.theme === void 0 && args.variants === void 0;
|
|
2476
|
+
}
|
|
1181
2477
|
async function createWizard(options, deps) {
|
|
1182
|
-
const
|
|
1183
|
-
const existing = readKeyConfig(options.home);
|
|
1184
|
-
const layout = existing.layout ?? runtime.defaultLayout;
|
|
1185
|
-
const byItem = new Map(runtime.items.map((item) => [item.item, item]));
|
|
1186
|
-
const names = layoutItems(layout, runtime.items.map((item) => item.item));
|
|
1187
|
-
const offered = [];
|
|
1188
|
-
const draft = /* @__PURE__ */ new Map();
|
|
1189
|
-
for (const name of names) {
|
|
1190
|
-
const entry = byItem.get(name);
|
|
1191
|
-
if (entry === void 0) continue;
|
|
1192
|
-
offered.push({
|
|
1193
|
-
alternatives: entry.alternatives,
|
|
1194
|
-
item: name
|
|
1195
|
-
});
|
|
1196
|
-
draft.set(name, name in existing.values && entry.alternatives.includes(existing.values[name]) ? existing.values[name] : entry.default);
|
|
1197
|
-
}
|
|
2478
|
+
const names = Object.keys(THEMES);
|
|
1198
2479
|
const sources = previewSources(options.home, Number(options.now));
|
|
1199
|
-
const
|
|
1200
|
-
|
|
2480
|
+
const tickBase = JSON.parse(sources.tick);
|
|
2481
|
+
let pass = "themes";
|
|
2482
|
+
let themeAt = 0;
|
|
2483
|
+
let picked = names[0];
|
|
2484
|
+
let base = DEFAULT_PICKS;
|
|
2485
|
+
let offered = [];
|
|
2486
|
+
const overrides = /* @__PURE__ */ new Map();
|
|
1201
2487
|
let focus = 0;
|
|
1202
2488
|
let widthAt = 0;
|
|
1203
|
-
function
|
|
1204
|
-
return Object.fromEntries([...draft].map(([item, alt]) => [`STATUSLINE_LAB_${item.toUpperCase()}`, alt]));
|
|
1205
|
-
}
|
|
1206
|
-
function env() {
|
|
2489
|
+
function bag(theme, layout, values = {}) {
|
|
1207
2490
|
return {
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
2491
|
+
home: options.home,
|
|
2492
|
+
...layout === void 0 ? {} : { layout },
|
|
2493
|
+
main: sources.main,
|
|
2494
|
+
now: options.now,
|
|
2495
|
+
tick: `${JSON.stringify({
|
|
2496
|
+
...tickBase,
|
|
2497
|
+
columns: WIDTHS[widthAt]
|
|
2498
|
+
}, null, 2)}\n`,
|
|
2499
|
+
theme,
|
|
2500
|
+
values,
|
|
2501
|
+
width: WIDTHS[widthAt]
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
function enterRefine(name) {
|
|
2505
|
+
picked = name;
|
|
2506
|
+
base = {
|
|
2507
|
+
...DEFAULT_PICKS,
|
|
2508
|
+
...THEMES[name].variants
|
|
1213
2509
|
};
|
|
2510
|
+
const items = layoutItems(THEMES[name].layout, ITEM_IDS);
|
|
2511
|
+
offered = [.../* @__PURE__ */ new Set([...items, STYLE])].flatMap((item) => {
|
|
2512
|
+
const entry = specFor(item);
|
|
2513
|
+
return entry === void 0 ? [] : [{
|
|
2514
|
+
alternatives: entry.alternatives,
|
|
2515
|
+
item
|
|
2516
|
+
}];
|
|
2517
|
+
});
|
|
2518
|
+
overrides.clear();
|
|
2519
|
+
focus = 0;
|
|
2520
|
+
}
|
|
2521
|
+
function effectivePick(item) {
|
|
2522
|
+
return overrides.get(item) ?? base[item];
|
|
2523
|
+
}
|
|
2524
|
+
function setPick(item, alt) {
|
|
2525
|
+
if (alt === base[item]) overrides.delete(item);
|
|
2526
|
+
else overrides.set(item, alt);
|
|
1214
2527
|
}
|
|
1215
2528
|
function cycle(delta) {
|
|
1216
2529
|
const focused = offered.at(focus);
|
|
1217
2530
|
if (focused === void 0 || focused.alternatives.length === 0) return;
|
|
1218
2531
|
const alts = focused.alternatives;
|
|
1219
|
-
const at = alts.indexOf(
|
|
1220
|
-
|
|
2532
|
+
const at = alts.indexOf(effectivePick(focused.item));
|
|
2533
|
+
setPick(focused.item, alts[(at + delta + alts.length) % alts.length]);
|
|
1221
2534
|
}
|
|
1222
2535
|
function step(key) {
|
|
1223
|
-
|
|
1224
|
-
|
|
2536
|
+
const down = key === "\x1B[B" || key === "j";
|
|
2537
|
+
const up = key === "\x1B[A" || key === "k";
|
|
2538
|
+
if (key === "w") widthAt = (widthAt + 1) % WIDTHS.length;
|
|
2539
|
+
else if (pass === "themes") {
|
|
2540
|
+
if (down) themeAt = (themeAt + 1) % names.length;
|
|
2541
|
+
else if (up) themeAt = (themeAt + names.length - 1) % names.length;
|
|
2542
|
+
} else if (key === "t") return "themes";
|
|
2543
|
+
else if (down) focus = (focus + 1) % offered.length;
|
|
2544
|
+
else if (up) focus = (focus + offered.length - 1) % offered.length;
|
|
1225
2545
|
else if (key === "\x1B[C" || key === "l") cycle(1);
|
|
1226
2546
|
else if (key === "\x1B[D" || key === "h") cycle(-1);
|
|
1227
2547
|
else if (key === "s") {
|
|
1228
2548
|
const focused = offered.at(focus);
|
|
1229
|
-
if (focused?.alternatives.includes("none"))
|
|
1230
|
-
}
|
|
2549
|
+
if (focused?.alternatives.includes("none")) setPick(focused.item, "none");
|
|
2550
|
+
}
|
|
2551
|
+
return pass;
|
|
1231
2552
|
}
|
|
1232
|
-
function
|
|
2553
|
+
function drawThemes() {
|
|
2554
|
+
const rows = [];
|
|
2555
|
+
let panel = "";
|
|
2556
|
+
for (let at = 0; at < names.length; at += 1) {
|
|
2557
|
+
const name = names[at];
|
|
2558
|
+
const theme = THEMES[name];
|
|
2559
|
+
const surfaces = deps.preview(bag(name));
|
|
2560
|
+
const focused = at === themeAt;
|
|
2561
|
+
if (focused) panel = surfaces.panel;
|
|
2562
|
+
rows.push(`${focused ? ">" : " "} ${name}: ${theme.summary}`);
|
|
2563
|
+
rows.push(` ${surfaces.line}`);
|
|
2564
|
+
}
|
|
2565
|
+
deps.render([
|
|
2566
|
+
`statusline configure · ${WIDTHS[widthAt]} columns (w cycles)`,
|
|
2567
|
+
"",
|
|
2568
|
+
...rows,
|
|
2569
|
+
"",
|
|
2570
|
+
`panel ${panel}`,
|
|
2571
|
+
"",
|
|
2572
|
+
THEME_KEYMAP
|
|
2573
|
+
].join("\n"));
|
|
2574
|
+
}
|
|
2575
|
+
function drawRefine() {
|
|
1233
2576
|
const focused = offered.at(focus);
|
|
1234
2577
|
if (focused === void 0) return;
|
|
1235
|
-
const
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
...
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
const
|
|
1245
|
-
bin: mainBin,
|
|
1246
|
-
env: env(),
|
|
1247
|
-
stdin: sources.main
|
|
1248
|
-
}).replace(/\n+$/, "");
|
|
1249
|
-
const tick = JSON.parse(sources.tick);
|
|
1250
|
-
tick.columns = WIDTHS[widthAt];
|
|
1251
|
-
const panel = deps.preview({
|
|
1252
|
-
bin: panelBin,
|
|
1253
|
-
env: {
|
|
1254
|
-
COLUMNS: String(WIDTHS[widthAt]),
|
|
1255
|
-
HOME: options.home,
|
|
1256
|
-
NOW: options.now
|
|
1257
|
-
},
|
|
1258
|
-
stdin: `${JSON.stringify(tick, null, 2)}\n`
|
|
1259
|
-
}).replace(/\n+$/, "");
|
|
1260
|
-
const rows = offered.map((entry, at) => `${at === focus ? ">" : " "} ${entry.item.padEnd(9)} ${draft.get(entry.item)}`);
|
|
2578
|
+
const values = Object.fromEntries(overrides);
|
|
2579
|
+
const samples = focused.alternatives.map((alt) => {
|
|
2580
|
+
const surfaces = deps.preview(bag(picked, focused.item === STYLE ? void 0 : `{${focused.item}}`, {
|
|
2581
|
+
...values,
|
|
2582
|
+
[focused.item]: alt
|
|
2583
|
+
}));
|
|
2584
|
+
return focused.item === STYLE ? surfaces.panel : surfaces.line;
|
|
2585
|
+
});
|
|
2586
|
+
const full = deps.preview(bag(picked, void 0, values));
|
|
2587
|
+
const rows = offered.map((entry, at) => `${at === focus ? ">" : " "} ${entry.item.padEnd(9)} ${effectivePick(entry.item)}`);
|
|
1261
2588
|
deps.render([
|
|
1262
2589
|
`statusline configure · ${WIDTHS[widthAt]} columns (w cycles)`,
|
|
1263
2590
|
"",
|
|
1264
|
-
` ${line}`,
|
|
1265
|
-
` panel ${
|
|
2591
|
+
` ${full.line}`,
|
|
2592
|
+
` panel ${full.panel}`,
|
|
1266
2593
|
"",
|
|
1267
2594
|
`${focused.item}: ${focused.alternatives.join(" | ")}`,
|
|
1268
|
-
...focused.alternatives.map((alt, at) => ` ${alt ===
|
|
2595
|
+
...focused.alternatives.map((alt, at) => ` ${alt === effectivePick(focused.item) ? "*" : " "} ${samples[at]}`),
|
|
1269
2596
|
"",
|
|
1270
2597
|
...rows,
|
|
1271
2598
|
"",
|
|
1272
|
-
|
|
2599
|
+
REFINE_KEYMAP
|
|
1273
2600
|
].join("\n"));
|
|
1274
2601
|
}
|
|
2602
|
+
function draw() {
|
|
2603
|
+
if (pass === "themes") drawThemes();
|
|
2604
|
+
else drawRefine();
|
|
2605
|
+
}
|
|
1275
2606
|
function finish() {
|
|
1276
2607
|
try {
|
|
1277
2608
|
configure({
|
|
2609
|
+
force: options.force,
|
|
1278
2610
|
home: options.home,
|
|
1279
|
-
|
|
1280
|
-
variants: Object.fromEntries(
|
|
2611
|
+
theme: picked,
|
|
2612
|
+
variants: Object.fromEntries(overrides)
|
|
1281
2613
|
});
|
|
1282
2614
|
} catch (e) {
|
|
1283
2615
|
deps.render(`save failed: ${e.message}\nfix it and rerun: npx -y @v1nvn/statusline@${VERSION} configure\n`);
|
|
@@ -1290,9 +2622,17 @@ async function createWizard(options, deps) {
|
|
|
1290
2622
|
const keys = deps.readKeys();
|
|
1291
2623
|
draw();
|
|
1292
2624
|
for await (const key of keys) {
|
|
1293
|
-
if (key === "\r")
|
|
2625
|
+
if (key === "\r") {
|
|
2626
|
+
if (pass === "themes") {
|
|
2627
|
+
enterRefine(names[themeAt]);
|
|
2628
|
+
pass = "refine";
|
|
2629
|
+
draw();
|
|
2630
|
+
continue;
|
|
2631
|
+
}
|
|
2632
|
+
return finish();
|
|
2633
|
+
}
|
|
1294
2634
|
if (key === "q" || key === "") break;
|
|
1295
|
-
step(key);
|
|
2635
|
+
pass = step(key);
|
|
1296
2636
|
draw();
|
|
1297
2637
|
}
|
|
1298
2638
|
deps.render("cancelled — nothing written\n");
|
|
@@ -1322,28 +2662,39 @@ else if (parsed.version) console.log(VERSION);
|
|
|
1322
2662
|
else if (parsed.command === "catalog") run(() => {
|
|
1323
2663
|
console.log(catalog({
|
|
1324
2664
|
home: homeOf(parsed.home),
|
|
1325
|
-
items: parsed.items
|
|
2665
|
+
items: parsed.items,
|
|
2666
|
+
themes: parsed.themes
|
|
1326
2667
|
}));
|
|
1327
2668
|
});
|
|
1328
2669
|
else if (parsed.command === "configure") {
|
|
1329
2670
|
const home = homeOf(parsed.home);
|
|
1330
|
-
if (parsed
|
|
2671
|
+
if (opensWizard(parsed, process.stdin.isTTY)) {
|
|
1331
2672
|
if (await createWizard({
|
|
2673
|
+
force: parsed.force,
|
|
1332
2674
|
home,
|
|
1333
2675
|
now: String(Math.floor(Date.now() / 1e3))
|
|
1334
2676
|
}, terminalDeps()) === "save-failed") process.exitCode = 1;
|
|
1335
2677
|
} else run(() => {
|
|
1336
|
-
|
|
1337
|
-
dryRun: parsed.dryRun,
|
|
1338
|
-
fallback: parsed.fallback,
|
|
2678
|
+
configure({
|
|
1339
2679
|
force: parsed.force,
|
|
1340
2680
|
home,
|
|
1341
2681
|
layout: parsed.layout,
|
|
2682
|
+
theme: parsed.theme,
|
|
1342
2683
|
variants: parsed.variants
|
|
1343
2684
|
});
|
|
1344
|
-
console.log(
|
|
2685
|
+
console.log("configured — live on the next paint");
|
|
1345
2686
|
});
|
|
1346
|
-
} else if (parsed.command === "
|
|
2687
|
+
} else if (parsed.command === "preview") run(() => {
|
|
2688
|
+
console.log(preview({
|
|
2689
|
+
home: homeOf(parsed.home),
|
|
2690
|
+
layout: parsed.layout,
|
|
2691
|
+
now: String(Math.floor(Date.now() / 1e3)),
|
|
2692
|
+
plain: parsed.plain,
|
|
2693
|
+
theme: parsed.theme,
|
|
2694
|
+
variants: parsed.variants
|
|
2695
|
+
}));
|
|
2696
|
+
});
|
|
2697
|
+
else if (parsed.command === "restore") run(() => {
|
|
1347
2698
|
const result = restore({
|
|
1348
2699
|
dryRun: parsed.dryRun,
|
|
1349
2700
|
force: parsed.force,
|