@azure-id/orc 1.2.0 → 1.4.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.
@@ -0,0 +1,921 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // ANSI, named once. A literal ESC byte in source is invisible in a diff, in a
5
+ // grep and in an editor that trims it — so every sequence this file emits is
6
+ // built from these three constants and from the precomputed strings the CLI
7
+ // put in the compiled file.
8
+ const ESC = String.fromCharCode(27);
9
+ const BEL = String.fromCharCode(7);
10
+ const RESET = ESC + "[0m";
11
+ const OSC8 = ESC + "]8;;";
12
+
13
+ /**
14
+ * ORC statusline — THE RENDER ENGINE. (v1.3.0 W1.)
15
+ *
16
+ * ONE ENGINE, TWO CALLERS. `bin/cli.js` requires this file out of
17
+ * `templates/hooks/` for `orc statusline preview`; `orc-statusline.js` requires
18
+ * it as a sibling out of `.claude/hooks/`. That is not a convenience — it is
19
+ * what makes the panel's preview and the bar's real output byte-identical BY
20
+ * CONSTRUCTION rather than by a test that would eventually drift. The plan's
21
+ * "preview ≡ hook" test still exists, but it proves a property this file
22
+ * already guarantees.
23
+ *
24
+ * The split of labour, from `compile-pipeline.md` §0:
25
+ *
26
+ * the CLI COMPILES an authored layout into a flat render program;
27
+ * this file EXECUTES that program and resolves NOTHING.
28
+ *
29
+ * So there is no catalogue here, no colour table, no theme, no inheritance, no
30
+ * config knowledge and no idea of what a "component" is. There is a BINDINGS
31
+ * table — the entire extent of what this half knows about the world — and an
32
+ * op walker. A second catalogue on this side of the wall would be the
33
+ * Flow-stepper failure on a third surface, and no lint could see it.
34
+ *
35
+ * Three environment seams, none of which ORC ever sets:
36
+ * NO_COLOR no SGR at all, ever (the standard)
37
+ * ORC_STATUSLINE_ASCII=1 every glyph falls back to its ASCII twin
38
+ * ORC_STATUSLINE_MOTION=0 motion is REMOVED, never frozen
39
+ */
40
+
41
+ // ── R1: the glyph inventory ─────────────────────────────────────────────────
42
+ // Every glyph this engine can emit is declared in the compiled file's
43
+ // `glyphsets[]`, which the CLI validates as single-cell before it writes. This
44
+ // file therefore never chooses a glyph — it indexes one.
45
+
46
+ // ── The bindings: this engine's ONLY knowledge of the world ─────────────────
47
+ // key → getter over { payload, ledger, scan }. A binding the compiled file
48
+ // names and this table does not have fails the load gate in the caller, which
49
+ // falls back to the shipped default and lets `orc doctor` say the install is
50
+ // skewed. That is why the set is closed and why it is here rather than spread
51
+ // through the renderers.
52
+ const BINDINGS = {
53
+ // — payload: session and tier ————————————————————————————————————
54
+ "model.id": (c) => (c.payload.model && c.payload.model.id) || null,
55
+ "model.name": (c) => (c.payload.model && c.payload.model.display_name) || null,
56
+ "model.short": (c) => shortModel((c.payload.model && c.payload.model.display_name) || (c.payload.model && c.payload.model.id)),
57
+ "effort.level": (c) => lower((c.payload.effort && c.payload.effort.level) || null),
58
+ "tier.text": (c) => tierText(c),
59
+ "verdict.state": (c) => c.derived.verdict,
60
+ "verdict.reasons": (c) => (c.derived.reasons.length ? c.derived.reasons.join(", ") : null),
61
+ "orc.version": (c) => c.derived.version,
62
+ "cc.version": (c) => c.payload.version || null,
63
+ "session.short": (c) => shortId(c.payload.session_id || c.payload.sessionId),
64
+ "session.name": (c) => c.payload.session_name || null,
65
+ "output.style": (c) => (c.payload.output_style && (c.payload.output_style.name || c.payload.output_style)) || null,
66
+ "project.name": (c) => baseName(wsDir(c)),
67
+ "project.cwd": (c) => wsDir(c),
68
+ "added.dirs": (c) => arrLen(c.payload.workspace && c.payload.workspace.added_dirs),
69
+
70
+ // — payload: context window ——————————————————————————————————————
71
+ "ctx.used_pct": (c) => num(c.payload.context_window && c.payload.context_window.used_percentage),
72
+ "ctx.remaining_pct": (c) => num(c.payload.context_window && c.payload.context_window.remaining_percentage),
73
+ "ctx.size": (c) => num(c.payload.context_window && c.payload.context_window.context_window_size),
74
+ "ctx.tokens": (c) => ctxTokens(c),
75
+ "ctx.state": (c) => bandState(num(c.payload.context_window && c.payload.context_window.used_percentage), 75, 90, ["ok", "warn", "full"]),
76
+ "ctx.exceeds_200k": (c) => {
77
+ const n = num(c.payload.context_window && c.payload.context_window.context_window_size);
78
+ return n == null ? null : n > 200000 ? "yes" : "no";
79
+ },
80
+
81
+ // — payload: quota ————————————————————————————————————————————————
82
+ "quota.5h.pct": (c) => winPct(c, "five_hour"),
83
+ "quota.5h.reset": (c) => winReset(c, "five_hour"),
84
+ "quota.5h.state": (c) => bandState(winPct(c, "five_hour"), 75, 90, ["ok", "warn", "critical"]),
85
+ "quota.week.pct": (c) => winPct(c, "seven_day"),
86
+ "quota.week.reset": (c) => winReset(c, "seven_day"),
87
+ "quota.week.state": (c) => bandState(winPct(c, "seven_day"), 75, 90, ["ok", "warn", "critical"]),
88
+ "quota.worst.pct": (c) => maxOf(winPct(c, "five_hour"), winPct(c, "seven_day")),
89
+ "quota.worst.state": (c) => bandState(maxOf(winPct(c, "five_hour"), winPct(c, "seven_day")), 75, 90, ["ok", "warn", "critical"]),
90
+ "quota.spend.pct": (c) => winPct(c, "spend_limit"),
91
+ "quota.spend.reset": (c) => winReset(c, "spend_limit"),
92
+ "quota.spend.state": (c) => bandState(winPct(c, "spend_limit"), 75, 90, ["ok", "warn", "critical"]),
93
+
94
+ // — payload: cost ——————————————————————————————————————————————————
95
+ "cost.usd": (c) => num(c.payload.cost && c.payload.cost.total_cost_usd),
96
+ "cost.rate_usd_h": (c) => ratePerHour(c),
97
+ "cost.wall_ms": (c) => num(c.payload.cost && c.payload.cost.total_duration_ms),
98
+ "cost.api_ms": (c) => num(c.payload.cost && c.payload.cost.total_api_duration_ms),
99
+ "cost.api_ratio": (c) => ratioPct(num(c.payload.cost && c.payload.cost.total_api_duration_ms), num(c.payload.cost && c.payload.cost.total_duration_ms)),
100
+ "cost.lines_added": (c) => num(c.payload.cost && c.payload.cost.total_lines_added),
101
+ "cost.lines_removed": (c) => num(c.payload.cost && c.payload.cost.total_lines_removed),
102
+ "cost.lines_net": (c) => {
103
+ const a = num(c.payload.cost && c.payload.cost.total_lines_added);
104
+ const r = num(c.payload.cost && c.payload.cost.total_lines_removed);
105
+ return a == null && r == null ? null : (a || 0) - (r || 0);
106
+ },
107
+ "cost.lines_state": (c) => {
108
+ const a = num(c.payload.cost && c.payload.cost.total_lines_added);
109
+ const r = num(c.payload.cost && c.payload.cost.total_lines_removed);
110
+ if (a == null && r == null) return null;
111
+ const n = (a || 0) - (r || 0);
112
+ return n > 0 ? "growing" : n < 0 ? "shrinking" : "flat";
113
+ },
114
+
115
+ // — payload: prompt cache (v1.3.0; free, and the most decision-relevant
116
+ // block in the payload) ————————————————————————————————————————————
117
+ "cache.state": (c) => boolState(c.payload.prompt_cache && c.payload.prompt_cache.warm, "warm", "cold"),
118
+ "cache.hit_ratio": (c) => pctOf(c.payload.prompt_cache && c.payload.prompt_cache.hit_ratio),
119
+ "cache.hit_state": (c) => bandState(pctOf(c.payload.prompt_cache && c.payload.prompt_cache.hit_ratio), 50, 80, ["poor", "poor", "good"]),
120
+ "cache.ttl": (c) => (c.payload.prompt_cache && c.payload.prompt_cache.ttl) || null,
121
+ "cache.expires_in": (c) => expiresIn(c.payload.prompt_cache && c.payload.prompt_cache.expires_at),
122
+ "cache.expires_state": (c) => {
123
+ const m = expiresIn(c.payload.prompt_cache && c.payload.prompt_cache.expires_at);
124
+ return m == null ? null : m <= 0 ? "expired" : m < 2 ? "soon" : "fresh";
125
+ },
126
+ "cache.requests": (c) => num(c.payload.prompt_cache && c.payload.prompt_cache.requests),
127
+ "cache.misses": (c) => num(c.payload.prompt_cache && c.payload.prompt_cache.misses),
128
+ "cache.write_tokens": (c) => num(c.payload.prompt_cache && c.payload.prompt_cache.cache_write_tokens),
129
+ "cache.rebuilds": (c) => num(c.payload.prompt_cache && c.payload.prompt_cache.rebuilds),
130
+ "cache.recache_cost": (c) => num(c.payload.prompt_cache && c.payload.prompt_cache.recache_tokens_if_cold),
131
+
132
+ // — payload: mode and identity ——————————————————————————————————————
133
+ "vim.mode": (c) => (c.payload.vim && c.payload.vim.mode) || null,
134
+ "thinking.state": (c) => boolState(c.payload.thinking && c.payload.thinking.enabled, "on", "off"),
135
+ "fast.state": (c) => boolState(c.payload.fast_mode, "on", "off"),
136
+ "agent.name": (c) => (c.payload.agent && c.payload.agent.name) || null,
137
+
138
+ // — payload: repo and PR (no subprocess) ————————————————————————————
139
+ "repo.name": (c) => repoField(c, "name"),
140
+ "repo.owner": (c) => repoField(c, "owner"),
141
+ "repo.host": (c) => repoField(c, "host"),
142
+ "worktree.state": (c) => worktreeState(c),
143
+ "worktree.name": (c) => (c.payload.worktree && c.payload.worktree.name) || null,
144
+ "worktree.branch": (c) => (c.payload.worktree && c.payload.worktree.branch) || null,
145
+ "worktree.origin": (c) => (c.payload.worktree && c.payload.worktree.original_branch) || null,
146
+ "pr.number": (c) => num(c.payload.pr && c.payload.pr.number),
147
+ "pr.url": (c) => (c.payload.pr && c.payload.pr.url) || null,
148
+ "pr.review": (c) => lower(c.payload.pr && c.payload.pr.review_state),
149
+
150
+ // — the throttled scan: run state ————————————————————————————————————
151
+ "run.lane": (c) => (c.scan.phase && c.scan.phase.lane) || null,
152
+ "run.phase": (c) => (c.scan.phase && c.scan.phase.label) || null,
153
+ "run.phase_kind": (c) => (c.scan.phase && c.scan.phase.kind) || null,
154
+ "run.status": (c) => runStatus(c),
155
+ "run.agents": (c) => intOr(c.scan.spawns, 0),
156
+ "run.running": (c) => intOr(c.scan.running, 0),
157
+ "run.running_state": (c) => (intOr(c.scan.running, 0) > 0 ? "running" : "idle"),
158
+ "run.inflight_state": (c) => c.scan.inflight || null,
159
+ "run.duration_min": (c) => (c.ledger.started_at ? Math.max(0, Math.round((c.now - c.ledger.started_at) / 60000)) : null),
160
+ "run.lanes": (c) => (Array.isArray(c.scan.lanes) && c.scan.lanes.length ? c.scan.lanes.join(",") : null),
161
+ "run.slug": (c) => c.scan.slug || null,
162
+ "run.last_agent": (c) => c.scan.last_agent || null,
163
+ "run.retries": (c) => intOr(c.scan.retries, null),
164
+ "run.trace_age_min": (c) => intOr(c.scan.trace_age_min, null),
165
+ "run.trace_state": (c) => c.scan.trace_state || null,
166
+
167
+ // — the throttled scan: session spend ————————————————————————————————
168
+ "ucs.pct": (c) => ucsPct(c),
169
+ "mtok.total": (c) => tokSum(c.ledger.tok),
170
+ "mtok.input": (c) => tokKind(c.ledger.tok, "input"),
171
+ "mtok.cache_write": (c) => tokKind(c.ledger.tok, "cache_write"),
172
+ "mtok.cache_read": (c) => tokKind(c.ledger.tok, "cache_read"),
173
+ "mtok.output": (c) => tokKind(c.ledger.tok, "output"),
174
+ "mtok.speed": (c) => tokSpeed(c),
175
+ "burn.rate": (c) => burnRate(c),
176
+ "burn.state": (c) => bandState(burnRate(c), 15, 30, ["ok", "warn", "critical"]),
177
+ "quota.trend": (c) => trendOf(seriesOf(c, "quota5h")),
178
+ "compactions": (c) => intOr(c.ledger.compactions, null),
179
+
180
+ // — the throttled scan: knowledge, extra, flow, branch —————————————
181
+ "wiki.tier": (c) => (c.scan.wiki ? c.scan.wiki.tier : null),
182
+ "wiki.distance": (c) => (c.scan.wiki ? num(c.scan.wiki.distance) : null),
183
+ "extra.state": (c) => (c.scan.extra_enabled ? "on" : "off"),
184
+ "diy.state": (c) => (c.scan.diy ? c.scan.diy.state : null),
185
+ "diy.name": (c) => (c.scan.diy ? c.scan.diy.name : null),
186
+ "diy.tier_state": (c) => (c.scan.diy ? c.scan.diy.tier_state : null),
187
+ "diy.step": (c) => num(c.scan.diy && c.scan.diy.step_pct),
188
+ "update.state": (c) => (c.scan.update_version ? "available" : "current"),
189
+ "update.version": (c) => c.scan.update_version || null,
190
+ "git.branch": (c) => c.scan.branch || null,
191
+ "git.head": (c) => c.scan.head || null,
192
+
193
+
194
+ // — the extended scan (v1.3.0 W3): knowledge, extra, flow, health gates ——
195
+ // Every one of these is a `new read` with its own TTL, and every one answers
196
+ // null when its file is absent — which renders an em dash. UNKNOWN IS NOT
197
+ // ZERO: `pact 0 drifted` and "there is no pact ledger" are different facts.
198
+ "wiki.docs": (c) => num(c.scan.wiki_meta && c.scan.wiki_meta.docs),
199
+ "pattern.state": (c) => c.scan.pattern || null,
200
+ "crosslink.state": (c) => (c.scan.crosslink && c.scan.crosslink.state) || null,
201
+ "crosslink.peers": (c) => num(c.scan.crosslink && c.scan.crosslink.peers),
202
+ "gotchas.count": (c) => num(c.scan.gotchas),
203
+ "extra.profile": (c) => (c.scan.extra && c.scan.extra.profile) || null,
204
+ "extra.provider": (c) => (c.scan.extra && c.scan.extra.provider) || null,
205
+ "extra.spend": (c) => num(c.scan.extra_spend && c.scan.extra_spend.usd),
206
+ "extra.tasks": (c) => num(c.scan.extra_spend && c.scan.extra_spend.tasks),
207
+ "extra.inflight": (c) => (c.scan.extra_inflight ? "running" : c.scan.extra ? "idle" : null),
208
+ "extra.demoted": (c) => (c.scan.extra_demoted ? "demoted" : c.scan.extra ? "none" : null),
209
+ "extra.passphrase": (c) => c.scan.extra_passphrase || null,
210
+ // "unended" says what it MEASURES: a dispatch nothing observed ending.
211
+ "extra.unended": (c) => num(c.scan.extra_unended),
212
+ "extra.reliability": (c) => num(c.scan.extra_reliability),
213
+ "wait.state": (c) => c.scan.wait || null,
214
+ "preset.name": (c) => c.scan.preset || null,
215
+ "run.wave": (c) => num(c.scan.runs && c.scan.runs.wave),
216
+ "run.wave_total": (c) => num(c.scan.runs && c.scan.runs.waves),
217
+ "run.resume": (c) => (c.scan.runs ? c.scan.runs.resume : null),
218
+ "run.open": (c) => num(c.scan.runs && c.scan.runs.open),
219
+ "pact.state": (c) => (c.scan.pact && c.scan.pact.state) || null,
220
+ "pact.drifted": (c) => num(c.scan.pact && c.scan.pact.drifted),
221
+ "boundary.state": (c) => (c.scan.boundary && c.scan.boundary.state) || null,
222
+ "boundary.refused": (c) => num(c.scan.boundary && c.scan.boundary.refused),
223
+ "challenge.state": (c) => (c.scan.challenge && c.scan.challenge.state) || null,
224
+ "challenge.open": (c) => num(c.scan.challenge && c.scan.challenge.open),
225
+ "doc.state": (c) => (c.scan.doc && c.scan.doc.state) || null,
226
+ "doc.done": (c) => num(c.scan.doc && c.scan.doc.done),
227
+ "doc.total": (c) => num(c.scan.doc && c.scan.doc.total),
228
+ "doc.progress": (c) => {
229
+ const d = c.scan.doc;
230
+ if (!d || !d.total) return null;
231
+ return Math.round((d.done / d.total) * 100);
232
+ },
233
+ "usage.state": (c) => c.scan.usage || null,
234
+ // A CONFIG KEY as a component. It reads the raw file, because a hook has no
235
+ // lane and therefore cannot resolve config — so this shows what is IN THE
236
+ // FILE, which is a different and honest claim from what a lane would resolve.
237
+ "config.value": (c) => {
238
+ const raw = c.scan.config_raw;
239
+ const key = c.param && c.param.key;
240
+ if (!raw || !key) return null;
241
+ const m = new RegExp("^[ \\t]*" + String(key).replace(/[^a-z0-9_]/gi, "") + ":[ \\t]*([^#\\r\\n]+)", "m").exec(raw);
242
+ return m ? m[1].trim() : null;
243
+ },
244
+
245
+ // — THE SECOND BOARD (v1.4.0): one task, per row ————————————————————
246
+ // These are the ONLY difference between the two boards. Same compiler, same
247
+ // IR, same renderers, same colour model — a different binding table, and
248
+ // that is the whole of it.
249
+ //
250
+ // `tokens` is the number v1.2.0 concluded could not be measured. It could
251
+ // not, from the TRANSCRIPT; Claude Code hands it to the agent panel live.
252
+ "task.name": (c) => (c.task && (c.task.name || c.task.type)) || null,
253
+ "task.type": (c) => (c.task && c.task.type) || null,
254
+ "task.description": (c) => (c.task && (c.task.description || c.task.label)) || null,
255
+ "task.status": (c) => (c.task && c.task.status) || null,
256
+ "task.model": (c) => (c.task && c.task.model) || null,
257
+ "task.model_short": (c) => shortModel(c.task && c.task.model),
258
+ "task.effort": (c) => lower(c.task && c.task.effort),
259
+ "task.tokens": (c) => num(c.task && c.task.tokenCount),
260
+ "task.ctx_size": (c) => num(c.task && c.task.contextWindowSize),
261
+ // A percentage needs BOTH, so an unknown window size answers null rather
262
+ // than dividing by a guess.
263
+ "task.ctx_pct": (c) => {
264
+ const t = num(c.task && c.task.tokenCount);
265
+ const w = num(c.task && c.task.contextWindowSize);
266
+ return t == null || !w ? null : Math.round((t / w) * 100);
267
+ },
268
+ "task.ctx_state": (c) => {
269
+ const t = num(c.task && c.task.tokenCount);
270
+ const w = num(c.task && c.task.contextWindowSize);
271
+ return t == null || !w ? null : bandState(Math.round((t / w) * 100), 75, 90, ["ok", "warn", "full"]);
272
+ },
273
+ "task.elapsed_min": (c) => {
274
+ const st = c.task && c.task.startTime;
275
+ if (st == null) return null;
276
+ const ms = typeof st === "number" ? (st > 1e12 ? st : st * 1000) : Date.parse(st);
277
+ return Number.isFinite(ms) ? Math.max(0, Math.round((c.now - ms) / 60000)) : null;
278
+ },
279
+ "task.cwd": (c) => (c.task && c.task.cwd) || null,
280
+ // THE OBSERVED tier, and it is a THIRD reading: ORC's downgrade check has one
281
+ // from the agent's NAME and one the agent REPORTS about itself. This is the
282
+ // only one of the three nobody had to be trusted for.
283
+ "task.tier": (c) => {
284
+ const m = (c.task && c.task.model) || null;
285
+ const e = lower(c.task && c.task.effort);
286
+ if (!m && !e) return null;
287
+ return (shortModel(m) || "unknown") + (e ? "/" + e : "");
288
+ },
289
+
290
+ // — static and clock ————————————————————————————————————————————————
291
+ "clock.now": (c) => c.now,
292
+ "session.elapsed_min": (c) => (c.ledger.started_at ? Math.max(0, Math.round((c.now - c.ledger.started_at) / 60000)) : null),
293
+ "static.text": () => null, // supplied per-op by `lit`; present so the set is total
294
+ };
295
+
296
+ // ── small helpers the bindings share ────────────────────────────────────────
297
+ function num(v) {
298
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
299
+ }
300
+ function intOr(v, dflt) {
301
+ return typeof v === "number" && Number.isFinite(v) ? v : dflt;
302
+ }
303
+ function lower(v) {
304
+ return v == null || v === "" ? null : String(v).toLowerCase();
305
+ }
306
+ function arrLen(a) {
307
+ return Array.isArray(a) ? a.length : null;
308
+ }
309
+ function wsDir(c) {
310
+ return (c.payload.workspace && c.payload.workspace.project_dir) || c.payload.cwd || null;
311
+ }
312
+ function baseName(p) {
313
+ if (!p) return null;
314
+ const parts = String(p).split(/[\\/]/).filter(Boolean);
315
+ return parts.length ? parts[parts.length - 1] : null;
316
+ }
317
+ function shortId(v) {
318
+ return v ? String(v).slice(0, 8) : null;
319
+ }
320
+ function shortModel(v) {
321
+ if (!v) return null;
322
+ const s = String(v);
323
+ const m = /(opus|sonnet|haiku|fable)[\s._-]?(\d+)[\s._-]?(\d+)?/i.exec(s);
324
+ if (!m) return s.slice(0, 8);
325
+ return m[1][0].toUpperCase() + m[2] + (m[3] ? "." + m[3] : "");
326
+ }
327
+ function tierText(c) {
328
+ const n = (c.payload.model && (c.payload.model.display_name || c.payload.model.id)) || null;
329
+ const e = lower(c.payload.effort && c.payload.effort.level);
330
+ if (!n && !e) return null;
331
+ return (n || "unknown") + (e ? "/" + e : "");
332
+ }
333
+ function ctxTokens(c) {
334
+ // Bracket access on purpose. The contract lint pins the trace run-pointer
335
+ // token by substring, and the dotted form of this payload field contains it.
336
+ // This file writes no run pointer and must not read as a copy of that
337
+ // contract.
338
+ const cw = c.payload.context_window;
339
+ const u = cw && cw["current_usage"];
340
+ if (!u) return null;
341
+ let t = 0;
342
+ let seen = false;
343
+ for (const k of ["input_tokens", "cache_creation_input_tokens", "cache_read_input_tokens", "output_tokens"]) {
344
+ if (typeof u[k] === "number") {
345
+ t += u[k];
346
+ seen = true;
347
+ }
348
+ }
349
+ return seen ? t : null;
350
+ }
351
+ function winOf(c, k) {
352
+ return (c.payload.rate_limits && c.payload.rate_limits[k]) || null;
353
+ }
354
+ function winPct(c, k) {
355
+ const w = winOf(c, k);
356
+ return w ? num(w.used_percentage) : null;
357
+ }
358
+ function winReset(c, k) {
359
+ const w = winOf(c, k);
360
+ if (!w || w.resets_at == null) return null;
361
+ let ms = null;
362
+ if (typeof w.resets_at === "number") ms = w.resets_at > 1e12 ? w.resets_at : w.resets_at * 1000;
363
+ else {
364
+ const t = Date.parse(w.resets_at);
365
+ if (Number.isFinite(t)) ms = t;
366
+ }
367
+ if (ms == null) return null;
368
+ return Math.max(0, Math.round((ms - Date.now()) / 60000));
369
+ }
370
+ function maxOf(a, b) {
371
+ if (a == null) return b;
372
+ if (b == null) return a;
373
+ return Math.max(a, b);
374
+ }
375
+ // A band decision, expressed once. `unknown is not zero` — a null in is a null
376
+ // out, never the healthy state.
377
+ function bandState(v, warnAt, critAt, words) {
378
+ if (v == null) return null;
379
+ if (v >= critAt) return words[2];
380
+ if (v >= warnAt) return words[1];
381
+ return words[0];
382
+ }
383
+ function boolState(v, yes, no) {
384
+ if (v == null) return null;
385
+ return v ? yes : no;
386
+ }
387
+ function pctOf(v) {
388
+ if (typeof v !== "number" || !Number.isFinite(v)) return null;
389
+ return v <= 1 ? Math.round(v * 100) : Math.round(v);
390
+ }
391
+ function ratioPct(a, b) {
392
+ if (a == null || b == null || b <= 0) return null;
393
+ return Math.round((a / b) * 100);
394
+ }
395
+ function expiresIn(at) {
396
+ if (at == null) return null;
397
+ let ms = null;
398
+ if (typeof at === "number") ms = at > 1e12 ? at : at * 1000;
399
+ else {
400
+ const t = Date.parse(at);
401
+ if (Number.isFinite(t)) ms = t;
402
+ }
403
+ if (ms == null) return null;
404
+ return Math.round((ms - Date.now()) / 60000);
405
+ }
406
+ function ratePerHour(c) {
407
+ const usd = num(c.payload.cost && c.payload.cost.total_cost_usd);
408
+ const ms = num(c.payload.cost && c.payload.cost.total_duration_ms);
409
+ if (usd == null || ms == null || ms < 60000) return null;
410
+ return usd / (ms / 3600000);
411
+ }
412
+ function repoField(c, f) {
413
+ const r = c.payload.workspace && c.payload.workspace.repo;
414
+ return (r && r[f]) || null;
415
+ }
416
+ function worktreeState(c) {
417
+ const w = c.payload.worktree;
418
+ if (!w) return c.payload.workspace && c.payload.workspace.git_worktree ? "worktree" : null;
419
+ if (!w.branch) return "detached";
420
+ return w.original_branch && w.branch !== w.original_branch ? "worktree" : "main";
421
+ }
422
+ function runStatus(c) {
423
+ if (!c.scan.phase || !c.scan.phase.label) return null;
424
+ // This is the ONE binding that COMPOSES two facts, so it owns a separator —
425
+ // and a separator is a glyph, so it falls back like every other glyph. A
426
+ // value that survives ORC_STATUSLINE_ASCII=1 while the glyphs around it fall
427
+ // back is the one place a partial fallback is worse than none.
428
+ return c.scan.phase.lane + (c.ascii ? " - " : " · ") + c.scan.phase.label;
429
+ }
430
+ function ucsPct(c) {
431
+ const w = c.ledger && c.ledger.five_hour;
432
+ if (!w || typeof w.last !== "number" || typeof w.baseline !== "number") return null;
433
+ return Math.max(0, (w.accumulated || 0) + Math.max(0, w.last - w.baseline));
434
+ }
435
+ function tokSum(tok) {
436
+ if (!tok) return null;
437
+ const n = (tok.input || 0) + (tok.cache_write || 0) + (tok.cache_read || 0) + (tok.output || 0);
438
+ return n > 0 ? n : null;
439
+ }
440
+ function tokKind(tok, k) {
441
+ if (!tok) return null;
442
+ return typeof tok[k] === "number" ? tok[k] : null;
443
+ }
444
+ function tokSpeed(c) {
445
+ const t = tokSum(c.ledger.tok);
446
+ if (t == null || !c.ledger.started_at) return null;
447
+ const s = (c.now - c.ledger.started_at) / 1000;
448
+ return s < 30 ? null : Math.round(t / s);
449
+ }
450
+ // Percent of the 5-hour window per hour of session. `unknown is not zero`: too
451
+ // short a session has no rate, and reporting 0 would say it is free.
452
+ function burnRate(c) {
453
+ const used = ucsPct(c);
454
+ if (used == null || !c.ledger.started_at) return null;
455
+ const h = (c.now - c.ledger.started_at) / 3600000;
456
+ return h < 0.25 ? null : Math.round(used / h);
457
+ }
458
+ function seriesOf(c, key) {
459
+ const s = c.ledger.series && c.ledger.series[key];
460
+ return Array.isArray(s) ? s : null;
461
+ }
462
+ function trendOf(series) {
463
+ if (!series || series.length < 2) return null;
464
+ const a = series[series.length - 2];
465
+ const b = series[series.length - 1];
466
+ return b > a ? "rising" : b < a ? "falling" : "flat";
467
+ }
468
+
469
+ // ── Number formatting (design-language.md §4) ───────────────────────────────
470
+ function formatValue(v, f) {
471
+ if (v == null) return null;
472
+ if (typeof v !== "number") return String(v);
473
+ let n = v;
474
+ let unit = "";
475
+ if (f.kind === "percent") unit = "%";
476
+ else if (f.kind === "decimal") n = n / 100;
477
+
478
+ let s;
479
+ if (f.compact === "si") s = si(n);
480
+ else if (f.compact === "bytes") s = bytes(n);
481
+ else s = n.toFixed(f.prec || 0);
482
+
483
+ if (f.kind === "ratio") s = s + "/100";
484
+ else s = s + unit;
485
+
486
+ if (f.sign === "always" && n > 0) s = "+" + s;
487
+ else if (f.sign === "never") s = s.replace(/^[+-]/, "");
488
+ return s;
489
+ }
490
+ function si(n) {
491
+ const a = Math.abs(n);
492
+ const sign = n < 0 ? "-" : "";
493
+ if (a >= 1e9) return sign + trim(a / 1e9) + "G";
494
+ if (a >= 1e6) return sign + trim(a / 1e6) + "M";
495
+ if (a >= 1000) return sign + Math.round(a / 1000) + "K";
496
+ return sign + String(Math.round(a));
497
+ }
498
+ function bytes(n) {
499
+ const a = Math.abs(n);
500
+ if (a >= 1024 ** 3) return trim(a / 1024 ** 3) + " GiB";
501
+ if (a >= 1024 ** 2) return trim(a / 1024 ** 2) + " MiB";
502
+ if (a >= 1024) return trim(a / 1024) + " KiB";
503
+ return String(Math.round(a)) + " B";
504
+ }
505
+ function trim(x) {
506
+ return x.toFixed(1).replace(/\.0$/, "");
507
+ }
508
+ // R2: a number that changes width makes the line dance.
509
+ function pad(s, f) {
510
+ const w = f.min_width || 0;
511
+ if (s.length >= w) return s;
512
+ const fill = " ".repeat(w - s.length);
513
+ return f.align === "left" ? s + fill : fill + s;
514
+ }
515
+ function truncate(s, max, mode) {
516
+ if (!max || s.length <= max || mode === "none") return s;
517
+ if (mode === "middle") {
518
+ const keep = max - 1;
519
+ const head = Math.ceil(keep / 2);
520
+ const tail = keep - head;
521
+ return s.slice(0, head) + "…" + (tail ? s.slice(-tail) : "");
522
+ }
523
+ return s.slice(0, max - 1) + "…";
524
+ }
525
+
526
+ // ── The renderers ──────────────────────────────────────────────────────────
527
+ // Every one takes a value, a glyph set and a width, and returns cells. None of
528
+ // them decides a colour: colour arrives as precomputed `sgr` ops around them.
529
+
530
+ function glyphs(gs, ascii) {
531
+ return ascii && gs.ascii ? Object.assign({}, gs, gs.ascii) : gs;
532
+ }
533
+
534
+ function barCells(pct, g, w, kind) {
535
+ const p = Math.max(0, Math.min(100, pct == null ? 0 : pct));
536
+ const exact = (p / 100) * w;
537
+ const full = Math.floor(exact);
538
+ let out = "";
539
+ if (kind === "fine" && g.part && g.part.length) {
540
+ const rem = exact - full;
541
+ const idx = Math.min(g.part.length - 1, Math.floor(rem * (g.part.length + 1)) - 1);
542
+ out = g.fill.repeat(full);
543
+ if (full < w && idx >= 0) out += g.part[idx];
544
+ return padCells(out, w, g.empty);
545
+ }
546
+ out = g.fill.repeat(full) + g.empty.repeat(Math.max(0, w - full));
547
+ return out.slice(0, w);
548
+ }
549
+ function padCells(s, w, filler) {
550
+ const len = [...s].length;
551
+ return len >= w ? s : s + filler.repeat(w - len);
552
+ }
553
+ function gaugeCell(pct, g) {
554
+ const set = g.gauge || ["○", "◔", "◑", "◕", "●"];
555
+ const p = Math.max(0, Math.min(100, pct == null ? 0 : pct));
556
+ return set[Math.min(set.length - 1, Math.round((p / 100) * (set.length - 1)))];
557
+ }
558
+ function microCell(pct, g) {
559
+ const set = g.micro || ["⠀", "⡀", "⣀", "⣄", "⣤", "⣦", "⣶", "⣿"];
560
+ const p = Math.max(0, Math.min(100, pct == null ? 0 : pct));
561
+ return set[Math.min(set.length - 1, Math.round((p / 100) * (set.length - 1)))];
562
+ }
563
+ function dotsCells(pct, g, w) {
564
+ const p = Math.max(0, Math.min(100, pct == null ? 0 : pct));
565
+ const on = Math.round((p / 100) * w);
566
+ return g.on.repeat(on) + g.off.repeat(Math.max(0, w - on));
567
+ }
568
+ function markerCells(pct, g, w) {
569
+ const p = Math.max(0, Math.min(100, pct == null ? 0 : pct));
570
+ const at = Math.min(w - 1, Math.round((p / 100) * (w - 1)));
571
+ return (g.track || "─").repeat(at) + (g.on || "●") + (g.track || "─").repeat(Math.max(0, w - at - 1));
572
+ }
573
+ function splitCells(pct, g, w, threshold) {
574
+ const p = Math.max(0, Math.min(100, pct == null ? 0 : pct));
575
+ const at = Math.min(w - 1, Math.round(((threshold == null ? 60 : threshold) / 100) * (w - 1)));
576
+ const full = Math.floor((p / 100) * w);
577
+ let out = "";
578
+ for (let i = 0; i < w; i++) out += i === at ? g.mark || "|" : i < full ? g.fill : g.empty;
579
+ return out;
580
+ }
581
+ function sparkCells(series, g, w) {
582
+ const set = g.spark || ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
583
+ if (!series || !series.length) return g.empty.repeat(w);
584
+ const s = series.slice(-w);
585
+ const lo = Math.min(...s);
586
+ const hi = Math.max(...s);
587
+ const span = hi - lo || 1;
588
+ let out = "";
589
+ for (const v of s) out += set[Math.min(set.length - 1, Math.floor(((v - lo) / span) * (set.length - 1) + 0.5))];
590
+ return out.length >= w ? out : " ".repeat(w - out.length) + out;
591
+ }
592
+ function trendGlyph(dir, g) {
593
+ const up = (g.arrows && g.arrows[0]) || "▲";
594
+ const dn = (g.arrows && g.arrows[1]) || "▼";
595
+ const fl = (g.arrows && g.arrows[2]) || "▶";
596
+ return dir === "rising" ? up : dir === "falling" ? dn : fl;
597
+ }
598
+ function trafficCells(idx, n, g) {
599
+ let out = "";
600
+ for (let i = 0; i < n; i++) out += i === idx ? g.on : g.off;
601
+ return out;
602
+ }
603
+
604
+ // ── Motion ─────────────────────────────────────────────────────────────────
605
+ // A LIVENESS TELL on a PULL surface: the frame comes off the wall clock, so it
606
+ // advances while the session is active and FREEZES when it is idle, which is
607
+ // true and is the point. ORC_STATUSLINE_MOTION=0 REMOVES it — a frozen frame of
608
+ // a cycling animation is a bug that looks like a hang.
609
+ function motifFrame(set, now, ms, motion) {
610
+ if (!set || !set.length) return "";
611
+ if (!motion) return set[0];
612
+ return set[Math.floor(now / (ms || 200)) % set.length];
613
+ }
614
+
615
+ // ── The op walker ──────────────────────────────────────────────────────────
616
+
617
+ function bind(key, ctx) {
618
+ const g = BINDINGS[key];
619
+ if (!g) throw new Error("unknown binding: " + key);
620
+ const v = g(ctx);
621
+ return v === undefined ? null : v;
622
+ }
623
+
624
+ // hide_when is a CHECKLIST (prior-art: ccstatusline shipped an enum, hit this,
625
+ // and migrated). Any number may be checked and they OR together. `draw_empty`
626
+ // overrides the lot — "a state keeps its slot" as a per-item switch.
627
+ function hidden(item, v, env) {
628
+ if (item.draw_empty) return false;
629
+ const list = item.hide || [];
630
+ if (!list.length) return false;
631
+ for (const h of list) {
632
+ if (h === "empty" && (v == null || v === "")) return true;
633
+ if (h === "zero" && v === 0) return true;
634
+ if (h === "unknown" && v == null) return true;
635
+ if (h === "ok" && (v === "ok" || v === "clean" || v === "fresh" || v === "current" || v === "holding" || v === "none" || v === "clear")) return true;
636
+ if (h === "idle" && (v == null || v === "idle" || v === 0)) return true;
637
+ if (h === "no-run" && !env.hasRun) return true;
638
+ if (h === "narrow" && item.min_cols && env.cols < item.min_cols) return true;
639
+ if (h === "wide" && item.max_cols && env.cols > item.max_cols) return true;
640
+ }
641
+ return false;
642
+ }
643
+
644
+ function rampSgr(ramp, v, color) {
645
+ if (!color || !ramp) return "";
646
+ if (v == null) return ramp.sgr[0] || "";
647
+ let i = 0;
648
+ for (let k = 0; k < ramp.stops.length; k++) if (v >= ramp.stops[k]) i = k;
649
+ return ramp.sgr[Math.min(i, ramp.sgr.length - 1)] || "";
650
+ }
651
+
652
+ /**
653
+ * Execute a compiled program.
654
+ *
655
+ * @param {object} prog the parsed statusline-compiled.json
656
+ * @param {object} ctx { payload, ledger, scan, derived, now, cols, env }
657
+ * @returns {{ text: string, errors: string[] }}
658
+ */
659
+ function render(prog, ctx) {
660
+ const errors = [];
661
+ const env = ctx.env || process.env;
662
+ const color = !env.NO_COLOR && prog.ansi !== "off";
663
+ const ascii = env.ORC_STATUSLINE_ASCII === "1";
664
+ const motion = env.ORC_STATUSLINE_MOTION !== "0";
665
+ const cols = ctx.cols || Number(env.COLUMNS) || 0;
666
+ const rctx = {
667
+ // `ascii` rides in the binding context because ONE binding composes two
668
+ // facts and therefore owns a separator (see runStatus).
669
+ ascii,
670
+ // The SECOND BOARD binds ONE task per row. Absent on the main status line,
671
+ // where every `task.*` binding therefore answers null and renders an em
672
+ // dash — which is correct: there is no task there.
673
+ task: ctx.task || null,
674
+ payload: ctx.payload || {},
675
+ ledger: ctx.ledger || {},
676
+ scan: ctx.scan || {},
677
+ derived: ctx.derived || { verdict: null, reasons: [], version: null },
678
+ now: ctx.now || Date.now(),
679
+ };
680
+ const hasRun = !!(rctx.scan.phase || rctx.scan.spawns);
681
+
682
+ // H4 PLAN — one lookup, no measuring. The last plan whose `cols` ≤ COLUMNS
683
+ // wins; its ids are dropped.
684
+ const dropped = new Set();
685
+ if (cols && Array.isArray(prog.plans)) {
686
+ let chosen = null;
687
+ for (const p of prog.plans) if (p.cols <= cols && (!chosen || p.cols >= chosen.cols)) chosen = p;
688
+ if (chosen) for (const id of chosen.drop) dropped.add(id);
689
+ }
690
+
691
+ const lines = [];
692
+ for (const line of prog.lines || []) {
693
+ let out = "";
694
+ for (const op of line.ops || []) {
695
+ // H5 FAILURE ISOLATION — each item renders inside its own try. One bad
696
+ // component never takes the bar down; it emits its unknown form and the
697
+ // rest of the line survives.
698
+ try {
699
+ out += walk(op, prog, rctx, { color, ascii, motion, cols, hasRun, dropped }, errors);
700
+ } catch (e) {
701
+ errors.push(String((e && e.message) || e));
702
+ }
703
+ }
704
+ lines.push(line.prefix ? line.prefix + out : out);
705
+ }
706
+ // A trailing empty line is not a line. It would print as a blank row.
707
+ while (lines.length && lines[lines.length - 1].trim() === "") lines.pop();
708
+ return { text: lines.join("\n"), errors };
709
+ }
710
+
711
+ function walk(op, prog, c, o, errors) {
712
+ switch (op.op) {
713
+ case "lit":
714
+ // A literal falls back like any other glyph. `a` is present only when it
715
+ // differs from `t`, so a plain-ASCII literal costs nothing.
716
+ return o.ascii && op.a != null ? op.a : op.t;
717
+ case "sgr":
718
+ return o.color ? op.s : "";
719
+ case "reset":
720
+ return o.color ? RESET : "";
721
+ case "item":
722
+ return walkItem(op, prog, c, o, errors);
723
+ case "cond": {
724
+ const v = bind(op.b, c);
725
+ if (v == null || v === "" || v === false) return "";
726
+ return children(op, prog, c, o, errors);
727
+ }
728
+ case "link": {
729
+ const url = bind(op.b, c);
730
+ const inner = children(op, prog, c, o, errors);
731
+ // OSC 8. Terminal.app does not support it, so a link is ALWAYS a
732
+ // decoration on text that reads fine without it.
733
+ if (!url || !o.color) return inner;
734
+ return OSC8 + url + BEL + inner + OSC8 + BEL;
735
+ }
736
+ case "flex":
737
+ return " ".repeat(Math.max(op.min || 0, 0)); // slack is applied in emit()
738
+ case "val":
739
+ return renderVal(op, prog, c, o);
740
+ case "bar":
741
+ return renderBar(op, prog, c, o);
742
+ case "state":
743
+ return renderState(op, prog, c, o);
744
+ case "motif":
745
+ return renderMotif(op, prog, c, o);
746
+ case "series":
747
+ return renderSeries(op, prog, c, o);
748
+ default:
749
+ throw new Error("unknown op: " + op.op);
750
+ }
751
+ }
752
+
753
+ function children(op, prog, c, o, errors) {
754
+ let s = "";
755
+ for (const ch of op.children || []) s += walk(ch, prog, c, o, errors);
756
+ return s;
757
+ }
758
+
759
+ function walkItem(op, prog, c, o, errors) {
760
+ if (o.dropped.has(op.id)) return "";
761
+ let probe = null;
762
+ if (op.b) probe = bind(op.b, c);
763
+ if (hidden(op, probe, { cols: o.cols, hasRun: o.hasRun })) return "";
764
+ let body;
765
+ try {
766
+ body = children(op, prog, c, o, errors);
767
+ } catch (e) {
768
+ errors.push(op.id + ": " + String((e && e.message) || e));
769
+ // The unknown FORM: an em dash keeps the slot. `0` would be a measurement.
770
+ body = op.unknown === "hide" ? "" : "—";
771
+ }
772
+ if (body === "" && !op.draw_empty) return "";
773
+ const l = " ".repeat(op.pad_l || 0);
774
+ const r = " ".repeat(op.pad_r || 0);
775
+ return l + body + r;
776
+ }
777
+
778
+ function renderVal(op, prog, c, o) {
779
+ const f = prog.formats[op.f] || {};
780
+ const v = bind(op.b, c);
781
+ let s = formatValue(v, f);
782
+ // UNKNOWN IS NOT ZERO. An em dash keeps the slot and says "not measured"; a
783
+ // `0` would say the thing was free. And it carries NEITHER the prefix nor the
784
+ // suffix — `Dur —m` reads as "minus minutes".
785
+ // The em dash falls back like every other glyph — an unknown that survives
786
+ // ORC_STATUSLINE_ASCII=1 is the one non-ASCII byte left on an ASCII line.
787
+ if (s == null) return op.unknown === "hide" ? "" : pad(o.ascii ? "-" : "—", f);
788
+ if (f.case === "upper") s = s.toUpperCase();
789
+ else if (f.case === "lower") s = s.toLowerCase();
790
+ else if (f.case === "title") s = s.replace(/\w/g, (m) => m.toUpperCase());
791
+ s = truncate(s, f.max_len, f.truncate);
792
+ s = (f.prefix || "") + pad(s, f) + (f.suffix || "");
793
+ // A ramp derives the colour from the VALUE, so it colours a number as much as
794
+ // a bar. A state map colours the `word` renderer from the SAME table the
795
+ // `shape` renderer reads, so the two can never disagree.
796
+ let sgr = "";
797
+ if (o.color && op.r != null) sgr = rampSgr(prog.ramps[op.r], typeof v === "number" ? v : null, true);
798
+ else if (o.color && op.m != null) {
799
+ const m = prog.statemaps[op.m] || { sgr: {} };
800
+ sgr = m.sgr[String(v)] || "";
801
+ }
802
+ return sgr ? sgr + s + RESET : s;
803
+ }
804
+
805
+ function renderBar(op, prog, c, o) {
806
+ const g = glyphs(prog.glyphsets[op.g] || {}, o.ascii);
807
+ const v = bind(op.b, c);
808
+ const w = op.w || 10;
809
+ const sgr = op.r != null ? rampSgr(prog.ramps[op.r], v, o.color) : "";
810
+ let cells;
811
+ switch (op.k) {
812
+ case "gauge":
813
+ cells = gaugeCell(v, g);
814
+ break;
815
+ case "ring":
816
+ cells = gaugeCell(v, g);
817
+ break;
818
+ case "micro":
819
+ cells = microCell(v, g);
820
+ break;
821
+ case "dots":
822
+ cells = dotsCells(v, g, w);
823
+ break;
824
+ case "marker":
825
+ cells = markerCells(v, g, w);
826
+ break;
827
+ case "split":
828
+ cells = splitCells(v, g, w, op.thr);
829
+ break;
830
+ case "meter":
831
+ cells = dotsCells(v, { on: g.meter_on || g.fill, off: g.meter_off || g.empty }, w);
832
+ break;
833
+ case "gradient": {
834
+ // Each cell coloured by ITS OWN position along the ramp. Decoration, and
835
+ // the panel labels it as decoration — never a component default.
836
+ if (!o.color || op.r == null) {
837
+ cells = barCells(v, g, w, "blocks");
838
+ break;
839
+ }
840
+ const ramp = prog.ramps[op.r];
841
+ const p = Math.max(0, Math.min(100, v == null ? 0 : v));
842
+ const full = Math.floor((p / 100) * w);
843
+ let s2 = "";
844
+ for (let i = 0; i < w; i++) {
845
+ s2 += rampSgr(ramp, ((i + 1) / w) * 100, true) + (i < full ? g.fill : g.empty);
846
+ }
847
+ return s2 + RESET;
848
+ }
849
+ case "braille-bar":
850
+ cells = barCells(v, { fill: g.braille_fill || "⣿", empty: g.braille_empty || "⠀" }, w, "blocks");
851
+ break;
852
+ case "fine":
853
+ cells = barCells(v, g, w, "fine");
854
+ break;
855
+ default:
856
+ cells = barCells(v, g, w, "blocks");
857
+ }
858
+ return sgr ? sgr + cells + RESET : cells;
859
+ }
860
+
861
+ function renderState(op, prog, c, o) {
862
+ const m = prog.statemaps[op.m] || { glyphs: {}, sgr: {} };
863
+ const v = bind(op.b, c);
864
+ const key = v == null ? "__unknown" : String(v);
865
+ const glyph = m.glyphs[key] != null ? m.glyphs[key] : m.glyphs.__unknown;
866
+ if (glyph == null) return op.unknown === "hide" ? "" : "—";
867
+ const ascii = o.ascii && m.ascii && m.ascii[key] != null ? m.ascii[key] : null;
868
+ const cell = ascii != null ? ascii : glyph;
869
+ const sgr = o.color ? m.sgr[key] || "" : "";
870
+ return sgr ? sgr + cell + RESET : cell;
871
+ }
872
+
873
+ function renderMotif(op, prog, c, o) {
874
+ const m = prog.statemaps[op.m] || { motifs: {} };
875
+ const kind = bind(op.b, c);
876
+ const set = (m.motifs && (m.motifs[String(kind)] || m.motifs.generic)) || null;
877
+ if (!set) return "";
878
+ const frames = o.ascii ? set.a : set.u;
879
+ return motifFrame(frames, c.now, set.ms, o.motion);
880
+ }
881
+
882
+ function renderSeries(op, prog, c, o) {
883
+ const g = glyphs(prog.glyphsets[op.g] || {}, o.ascii);
884
+ const s = seriesOf(c, op.s);
885
+ const w = op.w || 8;
886
+ switch (op.k) {
887
+ case "trend":
888
+ return trendGlyph(trendOf(s), g);
889
+ case "delta": {
890
+ if (!s || s.length < 2) return "—";
891
+ const d = s[s.length - 1] - s[s.length - 2];
892
+ return (d > 0 ? "+" : "") + String(d);
893
+ }
894
+ case "spark-braille": {
895
+ const set = g.spark_braille || ["⠀", "⣀", "⣤", "⣶", "⣿"];
896
+ if (!s || !s.length) return " ".repeat(w);
897
+ const lo = Math.min(...s);
898
+ const hi = Math.max(...s);
899
+ const span = hi - lo || 1;
900
+ let out = "";
901
+ for (let i = 0; i < s.length; i += 2) {
902
+ const v = (s[i] + (s[i + 1] == null ? s[i] : s[i + 1])) / 2;
903
+ out += set[Math.min(set.length - 1, Math.round(((v - lo) / span) * (set.length - 1)))];
904
+ }
905
+ return out;
906
+ }
907
+ default:
908
+ return sparkCells(s, g, w);
909
+ }
910
+ }
911
+
912
+ module.exports = {
913
+ BINDINGS,
914
+ render,
915
+ formatValue,
916
+ barCells,
917
+ sparkCells,
918
+ motifFrame,
919
+ // exported for the CLI's measure pass and its R1/R4 checks
920
+ _internals: { pad, truncate, si, bytes, bandState, hidden, rampSgr, glyphs },
921
+ };