@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/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 { basename, dirname, join } from "node:path";
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, DATA_REL, "captures", `${surface}.json`);
1221
+ return join(home, DATA_DIR, "captures", `${surface}.json`);
30
1222
  }
31
1223
  function backupPath(home) {
32
- return join(home, DATA_REL, "backup.json");
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
- var KEY_RESOLVER = "d=$(printf '%s\\n' ~/.claude/plugins/cache/agentic/statusline/*/ | sort -V | tail -1)";
35
- var MAIN_PREFIX = `${KEY_RESOLVER}; `;
36
- var MAIN_SUFFIX = "bash \"${d}runtime/statusline.sh\" 2>/dev/null || true";
37
- function mainKeyValue(layout, assignments) {
38
- return `${MAIN_PREFIX}${[`STATUSLINE_LAB_LAYOUT='${layout}'`, ...assignments].join(" ")} ${MAIN_SUFFIX}`;
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.startsWith(MAIN_PREFIX) && command.endsWith(` ${MAIN_SUFFIX}`);
1252
+ return commandMiddle(RENDER_PROGRAM, command) !== null;
43
1253
  }
44
- function mainKeyMiddle(command) {
45
- if (!isOurMainCommand(command)) return null;
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 readKeyConfig(home) {
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
- for (const [, name, alt] of middle.matchAll(/(?:^| )STATUSLINE_LAB_([A-Z][A-Z0-9_]*)=([a-z0-9]+)/g)) values[name.toLowerCase()] = alt;
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 byVersion(a, b) {
86
- const left = a.split(".");
87
- const right = b.split(".");
88
- for (let i = 0; i < left.length || i < right.length; i += 1) {
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 resolveRuntime({ home }) {
140
- const pluginDir = newestCacheDir(join(home, ".claude", "plugins", "cache", "agentic", "statusline"));
141
- const dir = pluginDir === null ? null : join(pluginDir, "runtime");
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
- const runtime = resolveRuntime({ home: options.home });
153
- const live = readKeyConfig(options.home).values;
154
- const byItem = new Map(runtime.items.map((item) => [item.item, item]));
155
- return (options.items ?? runtime.items.map((item) => item.item)).map((item) => {
156
- const entry = byItem.get(item);
157
- if (entry === void 0) throw new Error(`unknown item '${item}' — valid items: ${[...byItem.keys()].join(" ")}`);
158
- const current = live[item] ?? entry.default;
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
- }).join("\n");
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.27.3",
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 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>", "brace clusters of item ids, e.g. '{cwd branch} {model effort}'").addOption(new Option("--fallback <mode>", "fill unflagged layout items from defaults or the existing config").choices(["default", "existing"])).option("--dry-run", "render both surfaces, write nothing").option("--force", "take over foreign settings keys");
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
- return {
1447
+ const common = {
298
1448
  version: false,
299
- command: "configure",
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/demo-repo.ts
310
- var NAME = "Lab";
311
- var EMAIL = "lab@local";
312
- var D_INIT = "2026-09-01T10:00:00+00:00";
313
- var D_REMOTE = "2026-09-01T11:00:00+00:00";
314
- var D_LOCAL1 = "2026-09-02T09:00:00+00:00";
315
- var D_LOCAL2 = "2026-09-02T09:30:00+00:00";
316
- var D_STASH = "2026-09-02T10:00:00+00:00";
317
- var API_INIT = "export const api = {\n host: 'demo.local',\n};\n";
318
- function materializeDemoRepo(home) {
319
- const dir = join(home, "demo", "atlas-web");
320
- for (const sub of [
321
- "src",
322
- "docs",
323
- "scratch",
324
- "scripts"
325
- ]) mkdirSync(join(dir, sub), { recursive: true });
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
- function rootMembers(raw) {
686
- const members = [];
687
- let i = raw.indexOf("{") + 1;
688
- while (i < raw.length) {
689
- while (/\s/.test(raw[i] ?? "~")) i += 1;
690
- if (raw[i] === "}") break;
691
- if (raw[i] !== "\"") throw new Error("malformed member in settings.json");
692
- const nameStart = i;
693
- const nameEnd = skipString(raw, i);
694
- const name = JSON.parse(raw.slice(i, nameEnd));
695
- i = nameEnd;
696
- while (/\s/.test(raw[i] ?? "~")) i += 1;
697
- if (raw[i] !== ":") throw new Error("malformed member in settings.json");
698
- i += 1;
699
- while (/\s/.test(raw[i] ?? "~")) i += 1;
700
- const valueStart = i;
701
- const valueEnd = skipValue(raw, i);
702
- members.push({
703
- name,
704
- nameStart,
705
- valueEnd,
706
- valueStart
707
- });
708
- i = valueEnd;
709
- while (/\s/.test(raw[i] ?? "~")) i += 1;
710
- if (raw[i] === ",") i += 1;
711
- else if (raw[i] !== "}") throw new Error("malformed member in settings.json");
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 members;
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 removeMembers(raw, keys) {
720
- let text = raw;
721
- for (const key of keys) {
722
- const members = rootMembers(text);
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 insertMembers(raw, members) {
736
- const close = raw.lastIndexOf("}");
737
- if (close === -1) throw new Error("settings.json has no closing brace to splice into");
738
- let at = close;
739
- while (/\s/.test(raw[at - 1] ?? "~")) at -= 1;
740
- const comma = raw.replace(/\s/g, "") === "{}" ? "" : ",";
741
- return `${raw.slice(0, at)}${comma}\n ${renderMembers(members)}${raw.slice(at)}`;
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 renderMembers(members) {
744
- return members.map(([key, command]) => `"${key}": ${settingsValue(command)}`).join(",\n ");
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
- /** Splice each rendered value over its root member. */
747
- function repointRootMembers(raw, entries) {
748
- let text = raw;
749
- for (const { key, value } of entries) {
750
- const span = rootMemberValueSpan(text, key);
751
- if (span === null) throw new Error(`cannot find the "${key}" member in settings.json`);
752
- text = `${text.slice(0, span[0])}${value}${text.slice(span[1])}`;
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
- return text;
755
- }
756
- function splicedSettings(raw, plan) {
757
- return repointRootMembers(plan.adds.length > 0 ? insertMembers(raw, plan.adds) : raw, plan.repoints.map(([key, command]) => ({
758
- key,
759
- value: settingsValue(command)
760
- })));
761
- }
762
- function writeBackupIfAbsent(home, plan) {
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
- const dir = dirname(file);
774
- mkdirSync(dir, { recursive: true });
775
- const tmp = `${file}.tmp`;
776
- writeFileSync(tmp, `${JSON.stringify({
777
- createdFile: plan.raw === null,
778
- keys
779
- }, null, 2)}\n`);
780
- renameSync(tmp, file);
781
- }
782
- function commitSettings(plan) {
783
- if (plan.raw === null) {
784
- mkdirSync(dirname(plan.file), { recursive: true });
785
- const members = plan.adds.map(([key, command]) => `"${key}": ${settingsValue(command)}`).join(",\n ");
786
- writeFileSync(plan.file, `{\n ${members}\n}\n`);
787
- return;
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
- if (plan.adds.length === 0 && plan.repoints.length === 0) return;
790
- writeFileSync(plan.file, splicedSettings(plan.raw, plan));
791
- }
792
- function renderPreview(runtime, home, values, layout, now) {
793
- const sources = previewSources(home, Number(now));
794
- try {
795
- const variantEnv = Object.fromEntries(Object.entries(values).map(([item, alt]) => [`STATUSLINE_LAB_${item.toUpperCase()}`, alt]));
796
- return `dry-run at 200 columns — nothing written\n${runtimeRenderer({
797
- bin: join(runtime.dir, "statusline.sh"),
798
- env: {
799
- COLUMNS: "200",
800
- HOME: home,
801
- NOW: now,
802
- STATUSLINE_LAB_LAYOUT: layout,
803
- ...variantEnv
804
- },
805
- stdin: sources.main
806
- }).replace(/\n+$/, "")}\npanel ${firstPanelRow(runtimeRenderer({
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
- function printedConfig(runtime, home) {
820
- const existing = readKeyConfig(home);
821
- return [...[`layout='${existing.layout ?? runtime.defaultLayout}'`, ...runtime.items.map((item) => `${item.item}=${existingValue(existing.values, item.item) ?? item.default}`)], "nothing written — pass variants (`statusline configure --model block`) or run bare on a TTY for the wizard"].join("\n");
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 existingValue(values, item) {
824
- return item in values ? values[item] : void 0;
2044
+ function hasStart(value) {
2045
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
825
2046
  }
826
- function configure(options) {
827
- const runtime = resolveRuntime({ home: options.home });
828
- const existing = readKeyConfig(options.home);
829
- const variants = options.variants ?? {};
830
- const explicit = options.layout !== void 0 || Object.keys(variants).length > 0;
831
- if (!explicit && options.fallback === void 0 && !options.dryRun) return {
832
- mode: "printed",
833
- text: printedConfig(runtime, options.home)
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
- const layout = options.layout === void 0 || options.layout === "" ? existing.layout ?? runtime.defaultLayout : options.layout;
836
- const byItem = new Map(runtime.items.map((item) => [item.item, item]));
837
- const items = layoutItems(layout, runtime.items.map((item) => item.item));
838
- for (const item of Object.keys(variants)) {
839
- if (!byItem.has(item)) throw new Error(`unknown item '${item}' — valid items: ${[...byItem.keys()].join(" ")}`);
840
- if (!items.includes(item)) throw new Error(`variant for '${item}' is not in the layout (layout items: ${items.join(" ")})`);
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
- mode: "dry-run",
862
- text: renderPreview(runtime, options.home, values, layout, now)
2069
+ cleanup: () => void 0,
2070
+ main: readFileSync(mainCapture, "utf8"),
2071
+ tick: existsSync(tickCapture) ? readFileSync(tickCapture, "utf8") : anchoredTick(now)
863
2072
  };
864
2073
  }
865
- const assignments = items.map((item) => `STATUSLINE_LAB_${item.toUpperCase()}=${values[item]}`);
866
- const plan = planSettings(options.home, mainKeyValue(layout, assignments), options.force ?? false);
867
- if (plan.adds.length > 0 || plan.repoints.length > 0) writeBackupIfAbsent(options.home, plan);
868
- commitSettings(plan);
869
- return { mode: "written" };
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, DATA_REL);
2222
+ const dataDir = join(options.home, DATA_DIR);
956
2223
  const capturesDir = join(dataDir, "captures");
957
2224
  const backupFile = backupPath(options.home);
958
- if (actions.length === 0 && !existsSync(backupFile) && !existsSync(capturesDir)) return {
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 --fallback=default`;
1002
- return `${key}: foreign${state.command === null ? "" : ` (${state.command})`} — fix: rerun configure --force --fallback=default`;
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 layoutItemsOf(layout) {
1005
- const items = [];
1006
- for (const cluster of parseClusters(layout)) for (const item of cluster) if (!items.includes(item)) items.push(item);
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
- if (config.layout === null) return "";
1011
- const assignments = layoutItemsOf(config.layout).filter((item) => Object.hasOwn(config.values, item)).map((item) => `${item}=${config.values[item]}`);
1012
- return [`layout='${config.layout}'`, ...assignments].join(" ");
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, runtime) {
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 layoutItemsOf(config.layout)) {
1022
- const entry = byItem.get(item);
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 runtimeRow(runtime) {
1040
- if (runtime === null) return `runtime: missing — fix: ${INSTALL_FIX}`;
1041
- return `runtime: ${basename(dirname(runtime.dir))} — ${runtime.items.length} items`;
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, runtime) {
2339
+ function configRow(findings) {
1044
2340
  if (findings.length === 0) return "config: no drift";
1045
- const fix = findings.some((finding) => finding.kind === "unknown-item") ? `rerun configure --layout '${runtime.defaultLayout}' --fallback=default` : "rerun configure --fallback=default";
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 ~/${DATA_REL}/backup.json`;
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 findings = runtime === null || main.kind !== "ours" ? [] : driftFindings(config, runtime);
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
- runtimeRow(runtime),
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
- ...runtime !== null && main.kind === "ours" ? [configRow(findings, runtime)] : [],
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 = runtime !== null && main.kind === "ours" && subagent.kind === "ours" && findings.length === 0;
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(spec) {
1169
- return runtimeRenderer(spec);
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 KEYMAP = "j/k move · h/l variant · s none · w width · enter save · q cancel";
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 runtime = resolveRuntime({ home: options.home });
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 mainBin = join(runtime.dir, "statusline.sh");
1200
- const panelBin = join(runtime.dir, "subagent.sh");
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 variantEnv() {
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
- COLUMNS: String(WIDTHS[widthAt]),
1209
- HOME: options.home,
1210
- NOW: options.now,
1211
- STATUSLINE_LAB_LAYOUT: layout,
1212
- ...variantEnv()
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(draft.get(focused.item) ?? "");
1220
- draft.set(focused.item, alts[(at + delta + alts.length) % alts.length]);
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
- if (key === "\x1B[B" || key === "j") focus = (focus + 1) % offered.length;
1224
- else if (key === "\x1B[A" || key === "k") focus = (focus + offered.length - 1) % offered.length;
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")) draft.set(focused.item, "none");
1230
- } else if (key === "w") widthAt = (widthAt + 1) % WIDTHS.length;
2549
+ if (focused?.alternatives.includes("none")) setPick(focused.item, "none");
2550
+ }
2551
+ return pass;
1231
2552
  }
1232
- function draw() {
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 samples = focused.alternatives.map((alt) => deps.preview({
1236
- bin: mainBin,
1237
- env: {
1238
- ...env(),
1239
- STATUSLINE_LAB_LAYOUT: `{${focused.item}}`,
1240
- [`STATUSLINE_LAB_${focused.item.toUpperCase()}`]: alt
1241
- },
1242
- stdin: sources.main
1243
- }).replace(/\n+$/, ""));
1244
- const line = deps.preview({
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 ${firstPanelRow(panel)}`,
2591
+ ` ${full.line}`,
2592
+ ` panel ${full.panel}`,
1266
2593
  "",
1267
2594
  `${focused.item}: ${focused.alternatives.join(" | ")}`,
1268
- ...focused.alternatives.map((alt, at) => ` ${alt === draft.get(focused.item) ? "*" : " "} ${samples[at]}`),
2595
+ ...focused.alternatives.map((alt, at) => ` ${alt === effectivePick(focused.item) ? "*" : " "} ${samples[at]}`),
1269
2596
  "",
1270
2597
  ...rows,
1271
2598
  "",
1272
- KEYMAP
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
- layout,
1280
- variants: Object.fromEntries(draft)
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") return finish();
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.layout === void 0 && parsed.variants === void 0 && parsed.fallback === void 0 && !parsed.dryRun && !parsed.force && process.stdin.isTTY) {
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
- const result = configure({
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(result.mode === "written" ? "configured — live on the next paint" : result.text);
2685
+ console.log("configured — live on the next paint");
1345
2686
  });
1346
- } else if (parsed.command === "restore") run(() => {
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,