@v1nvn/statusline 0.28.0 → 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 } 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,6 +24,1090 @@ 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
26
1111
  //#region src/themes.ts
27
1112
  var THEME_NAMES = [
28
1113
  "classic",
@@ -31,127 +1116,148 @@ var THEME_NAMES = [
31
1116
  "quiet",
32
1117
  "rich"
33
1118
  ];
34
- var CUSTOM_SEEDS = {
35
- branch: "last",
36
- cwd: "base",
37
- model: "zen",
38
- style: "bare"
39
- };
40
- function offers(item, variant) {
41
- return item.default === variant || item.alternatives.includes(variant);
42
- }
43
- function absenceSeed(item) {
44
- if (offers(item, "none")) return "none";
45
- if (offers(item, "hidden")) return "hidden";
46
- const named = CUSTOM_SEEDS[item.item];
47
- if (named === void 0) throw new Error(`no absence variant for '${item.item}' — name its seed`);
48
- return named;
49
- }
50
- function themesFor(runtime) {
51
- function variantsOf(pick) {
52
- return Object.fromEntries(runtime.items.map((item) => [item.item, pick(item)]));
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
+ }
53
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`);
54
1207
  return {
55
- quiet: {
56
- layout: "{model cwd}",
57
- summary: "model and directory, nothing else",
58
- variants: {
59
- cwd: "tail",
60
- model: "zen",
61
- style: "bare"
62
- }
63
- },
64
- classic: {
65
- layout: runtime.defaultLayout,
66
- summary: "the shipped defaults, named",
67
- variants: variantsOf((item) => item.default)
68
- },
69
- lean: {
70
- layout: runtime.defaultLayout,
71
- summary: "text only, no graphics",
72
- variants: {
73
- model: "plain",
74
- effort: "dim",
75
- state: "none",
76
- cwd: "init",
77
- branch: "initials",
78
- status: "counts",
79
- ahead: "arrows",
80
- pr: "badge",
81
- bar: "percent",
82
- tokens: "full",
83
- cache: "hit",
84
- cost: "plain",
85
- duration: "clock",
86
- lines: "diffstat",
87
- rate: "none",
88
- style: "dots"
89
- }
90
- },
91
- rich: {
92
- layout: runtime.defaultLayout,
93
- summary: "every gauge and counter",
94
- variants: {
95
- model: "pill",
96
- effort: "plain",
97
- state: "pills",
98
- cwd: "icon",
99
- branch: "icon",
100
- status: "icons",
101
- ahead: "arrows",
102
- pr: "badge",
103
- bar: "gauge",
104
- tokens: "full",
105
- cache: "fuse",
106
- cost: "burn",
107
- duration: "clock",
108
- lines: "diffstat",
109
- rate: "strip",
110
- style: "plain"
111
- }
112
- },
113
- custom: {
114
- layout: runtime.defaultLayout,
115
- summary: "bare; you decide everything",
116
- variants: variantsOf(absenceSeed)
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
117
1212
  }
118
1213
  };
119
1214
  }
120
1215
  //#endregion
121
- //#region src/live-theme.ts
122
- function sameAssignments(key, theme) {
123
- return Object.entries(key).every(([item, variant]) => theme[item] === variant) && Object.entries(theme).every(([item, variant]) => key[item] === variant);
124
- }
125
- function liveTheme(key, runtime) {
126
- const themes = themesFor(runtime);
127
- for (const name of Object.keys(themes)) {
128
- const { layout, variants } = themes[name];
129
- if (key.layout === layout && sameAssignments(key.values, variants)) return name;
130
- }
131
- }
1216
+ //#region src/render/capture.ts
1217
+ var DATA_DIR = join(".claude", "plugins", "data", "statusline-agentic");
132
1218
  //#endregion
133
1219
  //#region src/resolve.ts
134
- var DATA_REL = join(".claude", "plugins", "data", "statusline-agentic");
135
1220
  function capturePath(home, surface) {
136
- return join(home, DATA_REL, "captures", `${surface}.json`);
1221
+ return join(home, DATA_DIR, "captures", `${surface}.json`);
137
1222
  }
138
1223
  function backupPath(home) {
139
- 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}`;
1239
+ }
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);
140
1245
  }
141
- var KEY_RESOLVER = "d=$(printf '%s\\n' ~/.claude/plugins/cache/agentic/statusline/*/ | sort -V | tail -1)";
142
- var MAIN_PREFIX = `${KEY_RESOLVER}; `;
143
- var MAIN_SUFFIX = "bash \"${d}runtime/statusline.sh\" 2>/dev/null || true";
144
- function mainKeyValue(layout, assignments) {
145
- return `${MAIN_PREFIX}${[`STATUSLINE_LAB_LAYOUT='${layout}'`, ...assignments].join(" ")} ${MAIN_SUFFIX}`;
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;
146
1250
  }
147
- var subagentKeyValue = `${KEY_RESOLVER}; bash "\${d}runtime/subagent.sh" 2>/dev/null || true`;
148
1251
  function isOurMainCommand(command) {
149
- return command.startsWith(MAIN_PREFIX) && command.endsWith(` ${MAIN_SUFFIX}`);
1252
+ return commandMiddle(RENDER_PROGRAM, command) !== null;
150
1253
  }
151
- function mainKeyMiddle(command) {
152
- if (!isOurMainCommand(command)) return null;
153
- return command.slice(MAIN_PREFIX.length, command.length - 52 - 1);
1254
+ function isOurPanelCommand(command) {
1255
+ return commandMiddle(PANEL_PROGRAM, command) !== null;
154
1256
  }
1257
+ var NO_DECISIONS = {
1258
+ layout: null,
1259
+ values: {}
1260
+ };
155
1261
  function settingsCommand(home) {
156
1262
  let raw;
157
1263
  try {
@@ -170,106 +1276,43 @@ function settingsCommand(home) {
170
1276
  const command = member.command;
171
1277
  return typeof command === "string" ? command : null;
172
1278
  }
173
- function readKeyConfig(home) {
174
- const command = settingsCommand(home);
175
- const middle = command === null ? null : mainKeyMiddle(command);
176
- if (middle === null) return {
177
- layout: null,
178
- values: {}
179
- };
180
- const layout = /^STATUSLINE_LAB_LAYOUT='([^']*)'/.exec(middle);
181
- if (layout === null) return {
182
- layout: null,
183
- values: {}
184
- };
1279
+ function parseKeyFlags(flags) {
185
1280
  const values = {};
186
- 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);
187
1285
  return {
188
- layout: layout[1],
189
- values
1286
+ layout: layout === null ? null : layout[1],
1287
+ values,
1288
+ ...theme === void 0 ? {} : { theme }
190
1289
  };
191
1290
  }
192
- function byVersion(a, b) {
193
- const left = a.split(".");
194
- const right = b.split(".");
195
- for (let i = 0; i < left.length || i < right.length; i += 1) {
196
- const delta = (parseInt(left[i] ?? "0", 10) || 0) - (parseInt(right[i] ?? "0", 10) || 0);
197
- if (delta !== 0) return delta;
198
- }
199
- return 0;
200
- }
201
- function newestCacheDir(cacheRoot) {
202
- let versions;
203
- try {
204
- versions = readdirSync(cacheRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
205
- } catch {
206
- return null;
207
- }
208
- if (versions.length === 0) return null;
209
- versions.sort(byVersion);
210
- return join(cacheRoot, versions[versions.length - 1]);
211
- }
212
- function readAlternatives(dir) {
213
- const declared = /* @__PURE__ */ new Map();
214
- const components = join(dir, "components");
215
- for (const file of readdirSync(components).sort()) {
216
- if (!file.endsWith(".sh")) continue;
217
- for (const line of readFileSync(join(components, file), "utf8").split("\n")) {
218
- if (!line.startsWith("#")) break;
219
- const match = /alternatives:\s*(.+)$/.exec(line);
220
- if (match) declared.set(file.slice(0, -3), match[1].split("|").map((alt) => alt.trim().replace(/\s*\(current\)$/, "")));
221
- }
222
- }
223
- return declared;
224
- }
225
- function readItems(dir) {
226
- const alternatives = readAlternatives(dir);
227
- const defaults = /* @__PURE__ */ new Map();
228
- for (const [, item, alt] of readFileSync(join(dir, "lib.sh"), "utf8").matchAll(/([a-z]+)\) echo ([a-z]+) ;;/g)) defaults.set(item, alt);
229
- const match = /^COMPS="(.+)"$/m.exec(readFileSync(join(dir, "statusline.sh"), "utf8"));
230
- if (match === null) throw new Error(`statusline.sh in ${dir} declares no COMPS order`);
231
- return match[1].split(" ").map((item) => {
232
- const alt = defaults.get(item);
233
- if (alt === void 0) throw new Error(`lib.sh in ${dir} declares no default for '${item}'`);
234
- return {
235
- alternatives: alternatives.get(item) ?? [],
236
- default: alt,
237
- item
238
- };
239
- });
240
- }
241
- function readDefaultLayout(dir) {
242
- const match = /^export DEFAULT_LAYOUT='(.*)'$/m.exec(readFileSync(join(dir, "lib.sh"), "utf8"));
243
- if (match === null) throw new Error(`lib.sh in ${dir} declares no DEFAULT_LAYOUT`);
244
- return match[1];
245
- }
246
- function resolveRuntimeDir(dir) {
247
- return {
248
- defaultLayout: readDefaultLayout(dir),
249
- dir,
250
- items: readItems(dir)
251
- };
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);
252
1295
  }
253
- function resolveRuntime({ home }) {
254
- const pluginDir = newestCacheDir(join(home, ".claude", "plugins", "cache", "agentic", "statusline"));
255
- const dir = pluginDir === null ? null : join(pluginDir, "runtime");
256
- 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`);
257
- return resolveRuntimeDir(dir);
1296
+ function parsePanelCommand(command) {
1297
+ const flags = command === null ? null : commandMiddle(PANEL_PROGRAM, command);
1298
+ return flags === null ? NO_DECISIONS : parseKeyFlags(flags);
258
1299
  }
259
1300
  //#endregion
260
1301
  //#region src/catalog.ts
261
1302
  function catalog(options) {
262
1303
  if (options.themes === true && options.items !== void 0) throw new Error("--themes cannot combine with item flags");
263
- const runtime = resolveRuntime({ home: options.home });
264
1304
  const key = readKeyConfig(options.home);
265
- const live = liveTheme(key, runtime);
266
- const block = Object.entries(themesFor(runtime)).map(([name, theme]) => `${name}${live === name ? "*" : ""}: ${theme.summary}`);
1305
+ const block = Object.entries(THEMES).map(([name, theme]) => `${name}${key.theme === name ? "*" : ""}: ${theme.summary}`);
267
1306
  if (options.themes === true) return block.join("\n");
268
- const byItem = new Map(runtime.items.map((item) => [item.item, item]));
269
- const itemLines = (options.items ?? runtime.items.map((item) => item.item)).map((item) => {
270
- const entry = byItem.get(item);
271
- if (entry === void 0) throw new Error(`unknown item '${item}' — valid items: ${[...byItem.keys()].join(" ")}`);
272
- const current = key.values[item] ?? entry.default;
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;
273
1316
  return `${item}: ${entry.alternatives.map((alt) => alt === current ? `${alt}*` : alt).join(" | ")}`;
274
1317
  });
275
1318
  return [
@@ -282,12 +1325,12 @@ function catalog(options) {
282
1325
  //#region src/cli.ts
283
1326
  var VERSION = {
284
1327
  name: "@v1nvn/statusline",
285
- version: "0.28.0",
1328
+ version: "0.29.0",
286
1329
  description: "Browse statusline variants and configure the live status line and agent panel — the statusline CLI behind the statusline plugin.",
287
1330
  type: "module",
288
1331
  bin: "dist/index.js",
289
1332
  scripts: {
290
- "build": "vite build",
1333
+ "build": "vite build && vite build --mode render",
291
1334
  "test": "vitest run",
292
1335
  "test:watch": "vitest"
293
1336
  },
@@ -318,24 +1361,6 @@ var VERSION = {
318
1361
  "vitest": "^4.1.10"
319
1362
  }
320
1363
  }.version;
321
- var ITEM_IDS = [
322
- "model",
323
- "effort",
324
- "state",
325
- "cwd",
326
- "branch",
327
- "status",
328
- "ahead",
329
- "pr",
330
- "bar",
331
- "tokens",
332
- "cache",
333
- "cost",
334
- "duration",
335
- "lines",
336
- "rate",
337
- "style"
338
- ];
339
1364
  var QUIET = {
340
1365
  writeOut: () => void 0,
341
1366
  writeErr: () => void 0
@@ -437,46 +1462,17 @@ function parseArgs(args) {
437
1462
  }
438
1463
  //#endregion
439
1464
  //#region src/configure.ts
440
- var SETTINGS_KEYS = ["statusLine", "subagentStatusLine"];
441
- function layoutItems(layout, valid) {
442
- const known = new Set(valid);
443
- const items = [];
444
- for (const cluster of parseClusters(layout)) for (const word of cluster) {
445
- if (!known.has(word)) throw new Error(`unknown layout item '${word}' — valid items: ${valid.join(" ")}`);
446
- if (!items.includes(word)) items.push(word);
447
- }
448
- return items;
449
- }
450
- function parseClusters(layout) {
451
- const clusters = [];
452
- let words = [];
453
- let word = "";
454
- let open = false;
455
- function pushWord() {
456
- if (word !== "") {
457
- words.push(word);
458
- word = "";
459
- }
460
- }
461
- for (const c of layout) if (c === "{") {
462
- if (open) throw new Error(`layout '${layout}': '{' inside a cluster`);
463
- open = true;
464
- words = [];
465
- } else if (c === "}") {
466
- if (!open) throw new Error(`layout '${layout}': '}' outside a cluster`);
467
- pushWord();
468
- open = false;
469
- if (words.length > 0) clusters.push(words);
470
- } else if (c === " ") {
471
- if (open) pushWord();
472
- else if (word !== "") throw new Error(`layout '${layout}': '${word}' sits outside a cluster`);
473
- } else if (/[a-z0-9]/.test(c)) word += c;
474
- else throw new Error(`layout '${layout}': '${c}' is not layout grammar (braces, item ids, spaces)`);
475
- if (open) throw new Error(`layout '${layout}': unterminated cluster`);
476
- if (word !== "") throw new Error(`layout '${layout}': '${word}' sits outside a cluster`);
477
- if (clusters.length === 0) throw new Error(`layout '${layout}': no clusters`);
478
- return clusters;
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);
479
1474
  }
1475
+ var SETTINGS_KEYS = ["statusLine", "subagentStatusLine"];
480
1476
  function isObject(value) {
481
1477
  return typeof value === "object" && value !== null && !Array.isArray(value);
482
1478
  }
@@ -505,14 +1501,11 @@ function memberCommand(value) {
505
1501
  function isOurMember(key, value) {
506
1502
  const command = memberCommand(value);
507
1503
  if (command === null) return false;
508
- return key === "statusLine" ? isOurMainCommand(command) : command === subagentKeyValue;
1504
+ return key === "statusLine" ? isOurMainCommand(command) : isOurPanelCommand(command);
509
1505
  }
510
- function planSettings(home, mainCommand, force) {
1506
+ function planSettings(home, commands, force) {
511
1507
  const file = join(home, ".claude", "settings.json");
512
- const wanted = {
513
- statusLine: mainCommand,
514
- subagentStatusLine: subagentKeyValue
515
- };
1508
+ const wanted = commands;
516
1509
  const raw = readOrNull(file);
517
1510
  if (raw === null) return {
518
1511
  adds: SETTINGS_KEYS.map((key) => [key, wanted[key]]),
@@ -687,44 +1680,42 @@ function themeNamed(name, themes) {
687
1680
  if (theme === void 0) throw new Error(`unknown theme '${name}' — valid themes: ${THEME_NAMES.join(" ")}`);
688
1681
  return theme;
689
1682
  }
690
- function resolveSelection(runtime, options) {
691
- const variants = options.variants ?? {};
692
- const themes = themesFor(runtime);
693
- const theme = options.theme === void 0 ? void 0 : themeNamed(options.theme, themes);
1683
+ function validateSelection(options) {
1684
+ const theme = options.theme === void 0 ? void 0 : themeNamed(options.theme, THEMES);
694
1685
  const layout = options.layout ?? theme?.layout;
695
1686
  if (layout === void 0) throw new Error("no layout — pass --layout <spec> or --theme <name>");
696
- const byItem = new Map(runtime.items.map((item) => [item.item, item]));
697
- const items = layoutItems(layout, runtime.items.map((item) => item.item));
698
- const values = {
699
- ...theme?.variants ?? {},
700
- ...variants
701
- };
702
- const missing = items.filter((item) => !(item in values));
703
- if (missing.length > 0) {
704
- const hints = missing.map((item) => `--${item} <alt>`).join(" ");
705
- const names = missing.map((item) => `'${item}'`).join(" ");
706
- const plural = missing.length > 1 ? "s" : "";
707
- throw new Error(theme === void 0 ? `no variant for layout item${plural} ${names} — pass ${hints}, or name a theme: --theme ${THEME_NAMES.join("|")}` : `theme '${options.theme}' has no pick for layout item${plural} ${names} — pass ${hints}, or drop ${missing.length > 1 ? "them" : "it"} from the layout`);
708
- }
709
- for (const [item, alt] of Object.entries(values)) {
710
- const entry = byItem.get(item);
711
- if (entry === void 0) throw new Error(`unknown item '${item}' — valid items: ${[...byItem.keys()].join(" ")}`);
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(" ")}`);
712
1691
  if (!entry.alternatives.includes(alt)) throw new Error(`unknown variant '${alt}' for item '${item}' — valid: ${entry.alternatives.join(" | ")}`);
713
1692
  }
714
- return {
715
- layout,
716
- ordered: [...items, ...runtime.items.map((entry) => entry.item).filter((item) => !items.includes(item) && item in values)],
717
- values
718
- };
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
+ });
719
1700
  }
720
1701
  function configure(options) {
721
- const runtime = resolveRuntime({ home: options.home });
722
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");
723
- const { layout, ordered, values } = resolveSelection(runtime, options);
724
- const assignments = ordered.map((item) => `STATUSLINE_LAB_${item.toUpperCase()}=${values[item]}`);
725
- const plan = planSettings(options.home, mainKeyValue(layout, assignments), options.force ?? false);
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);
726
1716
  if (plan.adds.length > 0 || plan.repoints.length > 0) writeBackupIfAbsent(options.home, plan);
727
1717
  commitSettings(plan);
1718
+ syncRenderer(options.home);
728
1719
  }
729
1720
  //#endregion
730
1721
  //#region src/demo-repo.ts
@@ -869,6 +1860,157 @@ function materializeDemoRepo(home) {
869
1860
  return dir;
870
1861
  }
871
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;
1902
+ }
1903
+ return alt;
1904
+ }
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;
1909
+ }
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
+ };
1922
+ }
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
+ };
1950
+ }
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}`;
1960
+ }
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}`;
1970
+ }
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;
1991
+ }
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`;
2010
+ }
2011
+ return out;
2012
+ }
2013
+ //#endregion
872
2014
  //#region src/payloads.ts
873
2015
  var PAYLOADS_DIR = fileURLToPath(new URL("../assets/payloads", import.meta.url));
874
2016
  var TICKS_DIR = fileURLToPath(new URL("../assets/ticks", import.meta.url));
@@ -947,68 +2089,46 @@ function firstPanelRow(output) {
947
2089
  const content = JSON.parse(row).content;
948
2090
  return typeof content === "string" ? content : "";
949
2091
  }
950
- function runtimeRenderer(spec) {
951
- const run = spawnSync("bash", [spec.bin], {
952
- input: spec.stdin,
953
- env: {
954
- PATH: process.env.PATH ?? "",
955
- ...spec.env,
956
- LC_ALL: "C",
957
- TZ: "UTC"
958
- },
959
- timeout: 3e4
960
- });
961
- if (run.status !== 0) throw new Error(`render failed: ${run.stderr.toString("utf8")}`);
962
- const stdout = run.stdout.toString("utf8");
963
- const warn = run.stderr.toString("utf8").trim();
964
- return warn === "" ? stdout : `${stdout}${warn}\n`;
965
- }
966
2092
  function renderPreview(bag) {
967
- const variantEnv = Object.fromEntries(Object.entries(bag.values).map(([item, alt]) => [`STATUSLINE_LAB_${item.toUpperCase()}`, alt]));
968
2093
  const plain = bag.plain === true || (process.env.NO_COLOR ?? "") !== "";
969
- const env = {
970
- ...variantEnv,
971
- ...plain ? { NO_COLOR: "1" } : {}
972
- };
2094
+ const paint = resolvePaint({
2095
+ layout: bag.layout,
2096
+ picks: bag.values,
2097
+ theme: bag.theme
2098
+ });
973
2099
  return {
974
- line: runtimeRenderer({
975
- bin: join(bag.runtime.dir, "statusline.sh"),
976
- env: {
977
- ...env,
978
- COLUMNS: String(bag.width ?? 200),
979
- HOME: bag.home,
980
- NOW: bag.now,
981
- STATUSLINE_LAB_LAYOUT: bag.layout
982
- },
983
- stdin: bag.main
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 }
984
2108
  }).replace(/\n+$/, ""),
985
- panel: firstPanelRow(runtimeRenderer({
986
- bin: join(bag.runtime.dir, "subagent.sh"),
987
- env: {
988
- ...env,
989
- HOME: bag.home,
990
- NOW: bag.now
991
- },
992
- stdin: bag.tick
2109
+ panel: firstPanelRow(renderPanel({
2110
+ noColor: plain,
2111
+ now: Number(bag.now),
2112
+ payload: bag.tick,
2113
+ picks: paint.picks
993
2114
  }).replace(/\n+$/, ""))
994
2115
  };
995
2116
  }
996
2117
  //#endregion
997
2118
  //#region src/preview.ts
998
2119
  function preview(options) {
999
- const runtime = resolveRuntime({ home: options.home });
1000
- const { layout, values } = resolveSelection(runtime, options);
2120
+ validateSelection(options);
1001
2121
  const sources = previewSources(options.home, Number(options.now));
1002
2122
  try {
1003
2123
  const { line, panel } = renderPreview({
1004
2124
  home: options.home,
1005
- layout,
2125
+ layout: options.layout,
1006
2126
  main: sources.main,
1007
2127
  now: options.now,
1008
2128
  plain: options.plain ?? !process.stdout.isTTY,
1009
- runtime,
1010
2129
  tick: sources.tick,
1011
- values
2130
+ theme: options.theme,
2131
+ values: options.variants
1012
2132
  });
1013
2133
  return `preview at 200 columns — nothing written\n${line}\npanel ${panel}\n`;
1014
2134
  } finally {
@@ -1099,18 +2219,20 @@ function restore(options) {
1099
2219
  next = null;
1100
2220
  } else next = text;
1101
2221
  }
1102
- const dataDir = join(options.home, DATA_REL);
2222
+ const dataDir = join(options.home, DATA_DIR);
1103
2223
  const capturesDir = join(dataDir, "captures");
1104
2224
  const backupFile = backupPath(options.home);
1105
- 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 {
1106
2227
  mode: "nothing",
1107
- 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"
1108
2229
  };
1109
2230
  if (options.dryRun) {
1110
2231
  const lines = ["restore plan — nothing written"];
1111
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`);
1112
2233
  if (deleteFile) lines.push(" delete settings.json (the lab created it and it is now empty)");
1113
2234
  if (existsSync(capturesDir)) lines.push(" delete captures/");
2235
+ if (existsSync(rendererFile)) lines.push(" delete render.mjs");
1114
2236
  if (existsSync(backupFile)) lines.push(" delete backup.json");
1115
2237
  lines.push(" remove the data dir if empty");
1116
2238
  return {
@@ -1121,6 +2243,7 @@ function restore(options) {
1121
2243
  if (deleteFile) rmSync(settings, { force: true });
1122
2244
  else if (next !== null && actions.length > 0) writeFileSync(settings, next);
1123
2245
  clearCaptures(capturesDir);
2246
+ rmSync(rendererFile, { force: true });
1124
2247
  rmSync(backupFile, { force: true });
1125
2248
  rmdirIfEmpty(dataDir);
1126
2249
  let line;
@@ -1134,7 +2257,6 @@ function restore(options) {
1134
2257
  }
1135
2258
  //#endregion
1136
2259
  //#region src/status.ts
1137
- var INSTALL_FIX = "claude plugin install statusline@agentic";
1138
2260
  function keyState(key, value) {
1139
2261
  if (value === void 0) return { kind: "absent" };
1140
2262
  if (isOurMember(key, value)) return { kind: "ours" };
@@ -1148,25 +2270,29 @@ function keyRow(key, state, detail = "") {
1148
2270
  if (state.kind === "absent") return `${key}: absent — fix: rerun configure --theme classic`;
1149
2271
  return `${key}: foreign${state.command === null ? "" : ` (${state.command})`} — fix: rerun configure --force --theme classic`;
1150
2272
  }
1151
- function layoutItemsOf(layout) {
1152
- const items = [];
1153
- for (const cluster of parseClusters(layout)) for (const item of cluster) if (!items.includes(item)) items.push(item);
1154
- 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)])];
1155
2276
  }
1156
2277
  function configDetail(config) {
1157
- if (config.layout === null) return "";
1158
- const assignments = layoutItemsOf(config.layout).filter((item) => Object.hasOwn(config.values, item)).map((item) => `${item}=${config.values[item]}`);
1159
- 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(" ")}`];
1160
2288
  }
1161
2289
  function findingText(finding) {
1162
2290
  return finding.kind === "unknown-item" ? `unknown item '${finding.item}'` : `unknown variant '${finding.alt}' for '${finding.item}'`;
1163
2291
  }
1164
- function driftFindings(config, runtime) {
1165
- if (config.layout === null) return [];
1166
- const byItem = new Map(runtime.items.map((entry) => [entry.item, entry]));
2292
+ function driftFindings(config) {
1167
2293
  const findings = [];
1168
- for (const item of layoutItemsOf(config.layout)) {
1169
- const entry = byItem.get(item);
2294
+ for (const item of keyItems(config)) {
2295
+ const entry = specFor(item);
1170
2296
  if (entry === void 0) {
1171
2297
  findings.push({
1172
2298
  item,
@@ -1183,9 +2309,32 @@ function driftFindings(config, runtime) {
1183
2309
  }
1184
2310
  return findings;
1185
2311
  }
1186
- function runtimeRow(runtime) {
1187
- if (runtime === null) return `runtime: missing — fix: ${INSTALL_FIX}`;
1188
- 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";
1189
2338
  }
1190
2339
  function configRow(findings) {
1191
2340
  if (findings.length === 0) return "config: no drift";
@@ -1196,7 +2345,7 @@ function backupRow(home) {
1196
2345
  try {
1197
2346
  backup = readBackup(home);
1198
2347
  } catch {
1199
- return `backup: unreadable — fix: delete ~/${DATA_REL}/backup.json`;
2348
+ return `backup: unreadable — fix: delete ~/${DATA_DIR}/backup.json`;
1200
2349
  }
1201
2350
  if (backup === null) return "backup: absent";
1202
2351
  const saved = SETTINGS_KEYS.filter((key) => backup.keys[key] !== void 0);
@@ -1225,30 +2374,25 @@ function settingsMembers(home) {
1225
2374
  const raw = readOrNull(file);
1226
2375
  return raw === null ? {} : parseSettings(file, raw);
1227
2376
  }
1228
- function resolveOrNull(home) {
1229
- try {
1230
- return resolveRuntime({ home });
1231
- } catch {
1232
- return null;
1233
- }
1234
- }
1235
2377
  function status(options) {
1236
- const runtime = resolveOrNull(options.home);
1237
2378
  const members = settingsMembers(options.home);
1238
2379
  const main = keyState("statusLine", members.statusLine);
1239
2380
  const subagent = keyState("subagentStatusLine", members.subagentStatusLine);
1240
2381
  const config = readKeyConfig(options.home);
1241
- const theme = runtime === null ? void 0 : liveTheme(config, runtime);
1242
- 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);
1243
2386
  const rows = [
1244
- 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),
1245
2389
  keyRow("statusLine", main, configDetail(config)),
1246
- keyRow("subagentStatusLine", subagent),
1247
- ...runtime !== null && main.kind === "ours" ? [configRow(findings), ...theme === void 0 ? [] : [`theme: ${theme}`]] : [],
2390
+ keyRow("subagentStatusLine", subagent, panelDetail(panelConfig)),
2391
+ ...main.kind === "ours" ? [configRow(findings), ...themeRow(config)] : [],
1248
2392
  backupRow(options.home),
1249
2393
  capturesRow(options.home)
1250
2394
  ];
1251
- 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;
1252
2396
  return {
1253
2397
  healthy,
1254
2398
  rows: [...rows, healthy ? "healthy" : "unhealthy"]
@@ -1326,58 +2470,67 @@ var WIDTHS = [
1326
2470
  ];
1327
2471
  var THEME_KEYMAP = "j/k focus · w width · enter pick · q cancel";
1328
2472
  var REFINE_KEYMAP = "j/k move · h/l variant · s none · t themes · w width · enter save · q cancel";
2473
+ var STYLE = "style";
1329
2474
  function opensWizard(args, isTty) {
1330
2475
  return isTty && args.layout === void 0 && args.theme === void 0 && args.variants === void 0;
1331
2476
  }
1332
2477
  async function createWizard(options, deps) {
1333
- const runtime = resolveRuntime({ home: options.home });
1334
- const themes = themesFor(runtime);
1335
- const names = Object.keys(themes);
1336
- const byItem = new Map(runtime.items.map((item) => [item.item, item]));
2478
+ const names = Object.keys(THEMES);
1337
2479
  const sources = previewSources(options.home, Number(options.now));
1338
2480
  const tickBase = JSON.parse(sources.tick);
1339
2481
  let pass = "themes";
1340
2482
  let themeAt = 0;
1341
- let draftLayout = "";
2483
+ let picked = names[0];
2484
+ let base = DEFAULT_PICKS;
1342
2485
  let offered = [];
1343
- const draft = /* @__PURE__ */ new Map();
2486
+ const overrides = /* @__PURE__ */ new Map();
1344
2487
  let focus = 0;
1345
2488
  let widthAt = 0;
1346
- function bag(layout, values) {
2489
+ function bag(theme, layout, values = {}) {
1347
2490
  return {
1348
2491
  home: options.home,
1349
- layout,
2492
+ ...layout === void 0 ? {} : { layout },
1350
2493
  main: sources.main,
1351
2494
  now: options.now,
1352
- runtime,
1353
2495
  tick: `${JSON.stringify({
1354
2496
  ...tickBase,
1355
2497
  columns: WIDTHS[widthAt]
1356
2498
  }, null, 2)}\n`,
2499
+ theme,
1357
2500
  values,
1358
2501
  width: WIDTHS[widthAt]
1359
2502
  };
1360
2503
  }
1361
2504
  function enterRefine(name) {
1362
- const theme = themes[name];
1363
- draftLayout = theme.layout;
1364
- offered = layoutItems(theme.layout, runtime.items.map((item) => item.item)).flatMap((item) => {
1365
- const entry = byItem.get(item);
2505
+ picked = name;
2506
+ base = {
2507
+ ...DEFAULT_PICKS,
2508
+ ...THEMES[name].variants
2509
+ };
2510
+ const items = layoutItems(THEMES[name].layout, ITEM_IDS);
2511
+ offered = [.../* @__PURE__ */ new Set([...items, STYLE])].flatMap((item) => {
2512
+ const entry = specFor(item);
1366
2513
  return entry === void 0 ? [] : [{
1367
2514
  alternatives: entry.alternatives,
1368
2515
  item
1369
2516
  }];
1370
2517
  });
1371
- draft.clear();
1372
- for (const [item, alt] of Object.entries(theme.variants)) draft.set(item, alt);
2518
+ overrides.clear();
1373
2519
  focus = 0;
1374
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);
2527
+ }
1375
2528
  function cycle(delta) {
1376
2529
  const focused = offered.at(focus);
1377
2530
  if (focused === void 0 || focused.alternatives.length === 0) return;
1378
2531
  const alts = focused.alternatives;
1379
- const at = alts.indexOf(draft.get(focused.item) ?? "");
1380
- 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]);
1381
2534
  }
1382
2535
  function step(key) {
1383
2536
  const down = key === "\x1B[B" || key === "j";
@@ -1393,7 +2546,7 @@ async function createWizard(options, deps) {
1393
2546
  else if (key === "\x1B[D" || key === "h") cycle(-1);
1394
2547
  else if (key === "s") {
1395
2548
  const focused = offered.at(focus);
1396
- if (focused?.alternatives.includes("none")) draft.set(focused.item, "none");
2549
+ if (focused?.alternatives.includes("none")) setPick(focused.item, "none");
1397
2550
  }
1398
2551
  return pass;
1399
2552
  }
@@ -1402,8 +2555,8 @@ async function createWizard(options, deps) {
1402
2555
  let panel = "";
1403
2556
  for (let at = 0; at < names.length; at += 1) {
1404
2557
  const name = names[at];
1405
- const theme = themes[name];
1406
- const surfaces = deps.preview(bag(theme.layout, theme.variants));
2558
+ const theme = THEMES[name];
2559
+ const surfaces = deps.preview(bag(name));
1407
2560
  const focused = at === themeAt;
1408
2561
  if (focused) panel = surfaces.panel;
1409
2562
  rows.push(`${focused ? ">" : " "} ${name}: ${theme.summary}`);
@@ -1422,13 +2575,16 @@ async function createWizard(options, deps) {
1422
2575
  function drawRefine() {
1423
2576
  const focused = offered.at(focus);
1424
2577
  if (focused === void 0) return;
1425
- const values = Object.fromEntries(draft);
1426
- const samples = focused.alternatives.map((alt) => deps.preview(bag(`{${focused.item}}`, {
1427
- ...values,
1428
- [focused.item]: alt
1429
- })).line);
1430
- const full = deps.preview(bag(draftLayout, values));
1431
- 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)}`);
1432
2588
  deps.render([
1433
2589
  `statusline configure · ${WIDTHS[widthAt]} columns (w cycles)`,
1434
2590
  "",
@@ -1436,7 +2592,7 @@ async function createWizard(options, deps) {
1436
2592
  ` panel ${full.panel}`,
1437
2593
  "",
1438
2594
  `${focused.item}: ${focused.alternatives.join(" | ")}`,
1439
- ...focused.alternatives.map((alt, at) => ` ${alt === draft.get(focused.item) ? "*" : " "} ${samples[at]}`),
2595
+ ...focused.alternatives.map((alt, at) => ` ${alt === effectivePick(focused.item) ? "*" : " "} ${samples[at]}`),
1440
2596
  "",
1441
2597
  ...rows,
1442
2598
  "",
@@ -1452,8 +2608,8 @@ async function createWizard(options, deps) {
1452
2608
  configure({
1453
2609
  force: options.force,
1454
2610
  home: options.home,
1455
- layout: draftLayout,
1456
- variants: Object.fromEntries(draft)
2611
+ theme: picked,
2612
+ variants: Object.fromEntries(overrides)
1457
2613
  });
1458
2614
  } catch (e) {
1459
2615
  deps.render(`save failed: ${e.message}\nfix it and rerun: npx -y @v1nvn/statusline@${VERSION} configure\n`);