@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.
- package/CHANGELOG.md +296 -0
- package/README-id.md +44 -0
- package/README.md +54 -78
- package/bin/cli.js +2615 -5
- package/bin/verify-contracts.js +60 -1
- package/bin/verify-package.js +588 -568
- package/bin/webui/api.js +71 -0
- package/bin/webui/app.html +213 -210
- package/bin/webui/css/06-responsive.css +28 -0
- package/bin/webui/css/panels/hookui.css +379 -0
- package/bin/webui/fixtures/hookui.js +409 -0
- package/bin/webui/fixtures/index.js +17 -0
- package/bin/webui/i18n/en/hookui.json +119 -0
- package/bin/webui/i18n/en/nav.json +22 -21
- package/bin/webui/i18n/en/overview.json +4 -1
- package/bin/webui/i18n/en/tour.json +3 -1
- package/bin/webui/i18n/id/hookui.json +119 -0
- package/bin/webui/i18n/id/nav.json +22 -21
- package/bin/webui/i18n/id/overview.json +4 -1
- package/bin/webui/i18n/id/tour.json +3 -1
- package/bin/webui/js/01-i18n.js +152 -151
- package/bin/webui/js/90-tour.js +6 -0
- package/bin/webui/js/panels/hookui.js +891 -0
- package/bin/webui/js/panels/overview.js +7 -0
- package/package.json +1 -1
- package/templates/hooks/README.md +280 -0
- package/templates/hooks/orc-statusline-render.js +921 -0
- package/templates/hooks/orc-statusline.js +1090 -87
- package/templates/hooks/orc-subagent-line.js +219 -0
package/bin/cli.js
CHANGED
|
@@ -86,6 +86,17 @@ const EXTRA_VALUE_FLAGS = new Set([
|
|
|
86
86
|
"--risk",
|
|
87
87
|
]);
|
|
88
88
|
|
|
89
|
+
// The value-taking flags of `orc statusline`. Every one of them can legally
|
|
90
|
+
// precede a positional, and a value read as a positional is a component named
|
|
91
|
+
// after a colour.
|
|
92
|
+
const SL_VALUE_FLAGS = new Set([
|
|
93
|
+
"--render", "--label", "--color", "--label-color", "--value-color", "--bg",
|
|
94
|
+
"--ramp", "--glyphs", "--format", "--case", "--truncate", "--compact",
|
|
95
|
+
"--prefix", "--suffix", "--emphasis", "--hide-when", "--width", "--precision",
|
|
96
|
+
"--min-width", "--min-cols", "--max-cols", "--priority", "--separator",
|
|
97
|
+
"--max-width", "--theme", "--state", "--set",
|
|
98
|
+
]);
|
|
99
|
+
|
|
89
100
|
function flag(name) {
|
|
90
101
|
const i = args.indexOf(name);
|
|
91
102
|
if (i === -1) return undefined;
|
|
@@ -118,6 +129,12 @@ function positionals() {
|
|
|
118
129
|
i++;
|
|
119
130
|
continue;
|
|
120
131
|
}
|
|
132
|
+
// v1.3.0 — `orc statusline set 1 1 --color green` must not read "green" as
|
|
133
|
+
// the component id. Same failure, same fix.
|
|
134
|
+
if (SL_VALUE_FLAGS.has(a)) {
|
|
135
|
+
i++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
121
138
|
if (a.startsWith("-")) continue;
|
|
122
139
|
out.push(a);
|
|
123
140
|
}
|
|
@@ -220,6 +237,16 @@ function installGuards(claudeDir) {
|
|
|
220
237
|
JSON.stringify({ version: currentVersion() }) + "\n"
|
|
221
238
|
);
|
|
222
239
|
} catch (_) {}
|
|
240
|
+
// The statusline's phase rail (v1.2.1). Regenerated every install from the
|
|
241
|
+
// CLI's own registries, so the hook renders a phase list it never derives.
|
|
242
|
+
// Same fail-silent contract: a missing manifest hides the `status:` segment,
|
|
243
|
+
// it never breaks a render.
|
|
244
|
+
try {
|
|
245
|
+
fs.writeFileSync(
|
|
246
|
+
path.join(hooksDest, "orc-lane-rails.json"),
|
|
247
|
+
JSON.stringify(laneRailsManifest()) + "\n"
|
|
248
|
+
);
|
|
249
|
+
} catch (_) {}
|
|
223
250
|
|
|
224
251
|
const guardCmd = nodeCmd(path.join(hooksDest, "orc-effort-guard.js"));
|
|
225
252
|
const statusCmd = nodeCmd(path.join(hooksDest, "orc-statusline.js"));
|
|
@@ -279,6 +306,31 @@ function installGuards(claudeDir) {
|
|
|
279
306
|
);
|
|
280
307
|
}
|
|
281
308
|
|
|
309
|
+
// 2b) subagentStatusLine — the SECOND board (v1.4.0). Same non-destructive
|
|
310
|
+
// rule: set only if the user has none, update only if it is already ours.
|
|
311
|
+
//
|
|
312
|
+
// Wiring it is always safe even with the board OFF, and that is the
|
|
313
|
+
// point: the hook writes the per-agent TOKEN COUNT it is handed whether
|
|
314
|
+
// or not the custom row is rendering. v1.2.0 concluded that number could
|
|
315
|
+
// not be measured — it cannot, from the transcript; Claude Code hands it
|
|
316
|
+
// to the agent panel live, and `orc usage report` reads what this records.
|
|
317
|
+
const subCmd = nodeCmd(path.join(hooksDest, "orc-subagent-line.js"));
|
|
318
|
+
if (!settings.subagentStatusLine) {
|
|
319
|
+
settings.subagentStatusLine = { type: "command", command: subCmd };
|
|
320
|
+
console.log(" add settings.json → subagentStatusLine (per-agent token record)");
|
|
321
|
+
} else if (
|
|
322
|
+
settings.subagentStatusLine.command &&
|
|
323
|
+
settings.subagentStatusLine.command.includes("orc-subagent-line")
|
|
324
|
+
) {
|
|
325
|
+
settings.subagentStatusLine.command = subCmd;
|
|
326
|
+
console.log(" upd settings.json → subagentStatusLine path");
|
|
327
|
+
} else {
|
|
328
|
+
console.log(
|
|
329
|
+
" skip settings.json → subagentStatusLine (you already have one; to record\n" +
|
|
330
|
+
` per-agent tokens, add: ${subCmd})`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
282
334
|
// 3) Trace hook — PreToolUse(Task|Agent) SPAWN + SubagentStop RETURN
|
|
283
335
|
// skeleton. Idempotent; non-destructive. Behavior-trace logging is
|
|
284
336
|
// PERMANENT (always on); the hook bootstraps log_dir + the run pointer
|
|
@@ -346,6 +398,7 @@ function shippedFootprint() {
|
|
|
346
398
|
for (const rel of listSrcRel(SRC_AGENTS)) files.push("agents/" + rel);
|
|
347
399
|
for (const rel of listSrcRel(SRC_HOOKS)) files.push("hooks/" + rel);
|
|
348
400
|
files.push("hooks/orc-version.json"); // generated by installGuards, ORC-owned
|
|
401
|
+
files.push("hooks/orc-lane-rails.json"); // generated by installGuards, ORC-owned
|
|
349
402
|
return files;
|
|
350
403
|
}
|
|
351
404
|
|
|
@@ -1171,6 +1224,11 @@ const CONFIG_FAMILIES = {
|
|
|
1171
1224
|
// first. EVERY default here is off or ask: nothing in this family may stop a
|
|
1172
1225
|
// run the user did not ask it to stop.
|
|
1173
1226
|
wait: { contested: false, question: "when a run stops to wait for quota, and how much it finishes first" },
|
|
1227
|
+
// v1.3.0 — the CLI Hook Interface. ONE key, and it answers ONE question: does
|
|
1228
|
+
// the status line render the shipped two lines, or yours. The LAYOUT is not
|
|
1229
|
+
// config — it lives in its own file with one writer — because a colour and a
|
|
1230
|
+
// renderer are not settings, they are a document.
|
|
1231
|
+
statusline: { contested: false, question: "whether the status line renders your composed layout or the shipped one" },
|
|
1174
1232
|
};
|
|
1175
1233
|
|
|
1176
1234
|
// Ordered, tiered metadata. Common first, then advanced.
|
|
@@ -1217,6 +1275,17 @@ const CONFIG_META = [
|
|
|
1217
1275
|
{ key: "usage_gate", def: "off", tier: "common", answers: [{ family: "wait", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-diy", "orc-doc", "orc-fast", "orc-mini", "orc-quick", "orc-wiki"], validate: vEnum("off", "warn", "stop", "wait"), options: ["off", "warn", "stop", "wait"], desc: "What ORC does when the 5-hour or 7-day window is nearly full, checked before a wave: off = nothing (the default — ORC never stops you until you ask it to), warn = print the reading and continue, stop = write the hand-back and stop, wait = hand back, then wait out the reset in detached hops that cost zero tokens. A missing or stale reading is `unknown` and NEVER stops a run. `/orc-wait block <reason>` suppresses this for one run; a typed `/orc-wait` is never suppressed." },
|
|
1218
1276
|
{ key: "usage_stop_pct", def: 10, tier: "common", answers: [{ family: "wait", prio: "P2", mode: "replace" }], gated_by: "usage_gate", lanes: ["orc", "orc-diy", "orc-doc", "orc-fast", "orc-mini", "orc-quick", "orc-wiki"], validate: vInt(1), options: [5, 10, 15, 20, 30], desc: "How much of a window must REMAIN for a wave to start. The WORST of the two windows decides — a 7-day window at 96% is not a green light because the 5-hour one is at 20%. Inert while usage_gate is off." },
|
|
1219
1277
|
{ key: "wait_default_mode", def: "ask", tier: "common", answers: [{ family: "wait", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-diy", "orc-doc", "orc-fast", "orc-mini", "orc-quick", "orc-wiki"], validate: vEnum("ask", "safe", "soft", "hard"), options: ["ask", "safe", "soft", "hard"], desc: "The mode a wait uses when you do not name one: ask = ONE question with the cost of each spelled out (the default — there is no stop behaviour you did not choose), safe = finish the current wave first and lose nothing, soft = stop at the next model turn but FORCE the checkpoint (and do not stop if that write fails), hard = stop at the next model turn with the hand-back only, which can lose an in-flight return. A mode named on the command line always wins." },
|
|
1278
|
+
// v1.3.0 — the CLI Hook Interface. DEFAULT OFF, and off is specified as
|
|
1279
|
+
// BYTE-IDENTICAL to the shipped two lines; that is a test, not an intention.
|
|
1280
|
+
// Its lanes[] is permanently EMPTY and it joins SEED_EMPTY: it is an
|
|
1281
|
+
// operating key of a HOOK, and a hook has no lane. That is an answer, not a
|
|
1282
|
+
// to-do. Enabled with nothing saved is not a state — the config write is
|
|
1283
|
+
// REFUSED when the layout does not validate, naming the reason.
|
|
1284
|
+
// v1.4.0 — the SECOND board. Same shape, same reason for an empty lanes[]:
|
|
1285
|
+
// it is an operating key of a hook. Off by default, and off means Claude
|
|
1286
|
+
// Code's own agent-panel row, unchanged.
|
|
1287
|
+
{ key: "subagent_line_custom", def: "off", tier: "common", answers: [{ family: "statusline", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("off", "on"), options: ["off", "on"], desc: "Whether the agent panel renders YOUR composed row for each subagent instead of Claude Code's own. Off is Claude Code's row, unchanged. Compose it in `orc ui` > CLI Hook Interface, on the subagent board. INDEPENDENT of the per-agent token record: ORC writes what the agent panel reports whether this is on or off, because that measurement is handed over either way and throwing it out because a display setting is off would be the wrong trade." },
|
|
1288
|
+
{ key: "statusline_custom", def: "off", tier: "common", answers: [{ family: "statusline", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("off", "on"), options: ["off", "on"], desc: "Whether the status line renders YOUR composed layout instead of the shipped two lines. Off is byte-identical to what ships. Compose the layout in `orc ui` > CLI Hook Interface (the CLI half exists so the panel has something to shell). Turning this on with an invalid or missing layout is refused, naming the reason; a layout that later becomes unreadable falls back to the shipped lines silently and is reported by `orc doctor`." },
|
|
1220
1289
|
{ key: "wait_hop_minutes", def: 30, tier: "advanced", answers: [{ family: "wait", prio: "P2", mode: "replace" }], lanes: [], validate: vInt(1), options: [5, 10, 15, 30], desc: "How long ONE detached hop waits before ORC re-reads the window. Short on purpose: each wake-up is session activity, and session activity is the only thing that makes the statusline write a fresh reading. One long sleep wakes into a reading as stale as the sleep was long." },
|
|
1221
1290
|
{ key: "wait_max_hops", def: 5, tier: "advanced", answers: [{ family: "wait", prio: "P2", mode: "replace" }], lanes: [], validate: vInt(1), options: [1, 2, 3, 5, 8, 12], desc: "How many hops before ORC gives up and stops with the hand-back. A wrong reset time must cost you a bounded wait, never a session that never comes back." },
|
|
1222
1291
|
// --- v0.50.0 — orc extra: dispatch ORC's WORKERS to non-Claude agents -----
|
|
@@ -4089,6 +4158,150 @@ function lanePhaseRows(lane) {
|
|
|
4089
4158
|
return rows;
|
|
4090
4159
|
}
|
|
4091
4160
|
|
|
4161
|
+
// ---------------------------------------------------------------------------
|
|
4162
|
+
// The STATUSLINE RAIL MANIFEST (v1.2.1)
|
|
4163
|
+
//
|
|
4164
|
+
// `templates/hooks/orc-statusline.js` shows which phase an ORC run is in. It
|
|
4165
|
+
// cannot call this CLI: a statusline re-renders on every keystroke, so a
|
|
4166
|
+
// subprocess per render is not on the table. It also must never hold its own
|
|
4167
|
+
// idea of what the phases are — that is the Flow-stepper failure (`orc ui`
|
|
4168
|
+
// v0.43.7) applied to a second surface, and a second phase table is drift no
|
|
4169
|
+
// lint could see.
|
|
4170
|
+
//
|
|
4171
|
+
// So the CLI COMPUTES and the hook RENDERS: `orc init` / `orc update` writes
|
|
4172
|
+
// `hooks/orc-lane-rails.json` from the registries below, exactly the way
|
|
4173
|
+
// `hooks/orc-version.json` is stamped at install time.
|
|
4174
|
+
//
|
|
4175
|
+
// FRAMES ARE NOT IN HERE. The glyphs live in the hook, because they are
|
|
4176
|
+
// presentation: a motif change must not need a reinstall. What ships is the
|
|
4177
|
+
// KIND — a closed word the hook maps to a frame set.
|
|
4178
|
+
// ---------------------------------------------------------------------------
|
|
4179
|
+
|
|
4180
|
+
// A phase's motif kind. The set is CLOSED, and a phase this table does not name
|
|
4181
|
+
// gets `generic` — the honest sweep, not a guess. Ported in spirit from
|
|
4182
|
+
// orc-cli's motifs.js, keyed on the PHASE rather than the lane: the lane name is
|
|
4183
|
+
// already the first word of the statusline's `status:` segment, so the glyph is
|
|
4184
|
+
// free to say what ORC is DOING.
|
|
4185
|
+
const PHASE_KINDS = ["look", "ask", "plan", "do", "check", "ship", "wait", "generic"];
|
|
4186
|
+
const PHASE_KIND = {
|
|
4187
|
+
// reading, scanning, gathering evidence
|
|
4188
|
+
preflight: "look",
|
|
4189
|
+
trace: "look",
|
|
4190
|
+
"wiki-consult": "look",
|
|
4191
|
+
"analyst-gates": "look",
|
|
4192
|
+
"house-rules": "look",
|
|
4193
|
+
analysis: "look",
|
|
4194
|
+
combine: "look",
|
|
4195
|
+
// waiting on the user
|
|
4196
|
+
intake: "ask",
|
|
4197
|
+
// deciding what to do and in what order
|
|
4198
|
+
planning: "plan",
|
|
4199
|
+
"plan-handoff": "plan",
|
|
4200
|
+
"wave-grouping": "plan",
|
|
4201
|
+
scoring: "plan",
|
|
4202
|
+
// executors running
|
|
4203
|
+
execution: "do",
|
|
4204
|
+
// grading what came back
|
|
4205
|
+
review: "check",
|
|
4206
|
+
security: "check",
|
|
4207
|
+
"security-checklist": "check",
|
|
4208
|
+
verify: "check",
|
|
4209
|
+
testgen: "check",
|
|
4210
|
+
"mock-example": "check",
|
|
4211
|
+
"ultra-gate": "check",
|
|
4212
|
+
// handing it over
|
|
4213
|
+
ship: "ship",
|
|
4214
|
+
summary: "ship",
|
|
4215
|
+
// stopped on purpose
|
|
4216
|
+
"stop-resume": "wait",
|
|
4217
|
+
};
|
|
4218
|
+
|
|
4219
|
+
// A word in an own-phase heading that decides its kind. Checked in order, so
|
|
4220
|
+
// the first match wins; no match falls through to `generic`. This is how a
|
|
4221
|
+
// lane's own pipeline (`Q1 LOOK`, `D4 DRAFT`, `C6 ADVISE`) gets a motif without
|
|
4222
|
+
// a hand-maintained row per phase per lane.
|
|
4223
|
+
const OWN_PHASE_KIND_WORDS = [
|
|
4224
|
+
[/\b(ask|question|intake|interview|frame|scope|choose|pick|gate)\b/i, "ask"],
|
|
4225
|
+
[/\b(look|preflight|read|scan|ingest|evidence|load|context|recon|harvest)\b/i, "look"],
|
|
4226
|
+
[/\b(plan|outline|cluster|shape|diverge|converge|order|batch|wave)\b/i, "plan"],
|
|
4227
|
+
[/\b(do|write|build|execute|implement|draft|generate|dispatch|compile)\b/i, "do"],
|
|
4228
|
+
[/\b(check|review|verify|judge|challenge|stress|grade|lint|audit|conserv)\b/i, "check"],
|
|
4229
|
+
[/\b(ship|finish|exit|deliver|hand|summar|report|card|record)\b/i, "ship"],
|
|
4230
|
+
[/\b(wait|stop|resume|pause|suspend)\b/i, "wait"],
|
|
4231
|
+
];
|
|
4232
|
+
|
|
4233
|
+
// The display label for one phase. 18 visible chars, because line 2 of the
|
|
4234
|
+
// statusline already carries five other segments.
|
|
4235
|
+
//
|
|
4236
|
+
// A shared phase is its own id — those ids ARE the words ORC uses everywhere
|
|
4237
|
+
// else, and renaming them for a status bar would invent a second vocabulary.
|
|
4238
|
+
// An own phase is its heading up to the first bracket or dash, which is what
|
|
4239
|
+
// turns `## Q1 — LOOK (silent — no questions here)` into `Q1 LOOK`.
|
|
4240
|
+
function railLabel(row) {
|
|
4241
|
+
if (!row.heading) return String(row.id).slice(0, 18);
|
|
4242
|
+
let s = String(row.heading).replace(/^#+\s*/, "");
|
|
4243
|
+
s = s.split(/\s+[—–-]\s+|\s*\(/)[0].trim();
|
|
4244
|
+
// A heading whose whole first clause is just the code (`## C6 — Advise`)
|
|
4245
|
+
// loses the only interesting word, so keep the second clause too when the
|
|
4246
|
+
// first is a bare code.
|
|
4247
|
+
if (/^[A-Za-z]{1,2}\d+[a-z]?$/.test(s)) {
|
|
4248
|
+
const rest = String(row.heading)
|
|
4249
|
+
.replace(/^#+\s*/, "")
|
|
4250
|
+
.slice(s.length)
|
|
4251
|
+
.replace(/^\s*[—–-]\s*/, "")
|
|
4252
|
+
.split(/\s*\(/)[0]
|
|
4253
|
+
.trim();
|
|
4254
|
+
if (rest) s = s + " " + rest;
|
|
4255
|
+
}
|
|
4256
|
+
s = s.replace(/\s+/g, " ").trim();
|
|
4257
|
+
if (s.length <= 18) return s;
|
|
4258
|
+
// Cut at a word boundary. `D2 Supporting docu` is a typo the reader has to
|
|
4259
|
+
// decode; `D2 Supporting` is a label.
|
|
4260
|
+
const cut = s.slice(0, 18);
|
|
4261
|
+
const sp = cut.lastIndexOf(" ");
|
|
4262
|
+
return (sp > 3 ? cut.slice(0, sp) : cut).trim();
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
function railKind(row) {
|
|
4266
|
+
if (PHASE_KIND[row.id]) return PHASE_KIND[row.id];
|
|
4267
|
+
const hay = `${row.id} ${row.heading || ""}`;
|
|
4268
|
+
for (const [re, kind] of OWN_PHASE_KIND_WORDS) if (re.test(hay)) return kind;
|
|
4269
|
+
return "generic";
|
|
4270
|
+
}
|
|
4271
|
+
|
|
4272
|
+
// The role families `templates/hooks/orc-trace.js` writes as `PHASE-EDGE <fam>`.
|
|
4273
|
+
// This is a MIRROR of that hook's `roleFamily()`, and it is the one new drift
|
|
4274
|
+
// surface this feature adds — a golden test compares the two by source text,
|
|
4275
|
+
// the way the `OPUS5_BANDS` alias is asserted.
|
|
4276
|
+
const TRACE_FAMILIES = ["combine", "analysis", "planning", "execution", "review", "verify", "testgen", "ultra-gate"];
|
|
4277
|
+
|
|
4278
|
+
// The whole object, deterministic from the registries above. `--json is not a
|
|
4279
|
+
// summary`: this is what the file holds and what `orc lane rails` prints.
|
|
4280
|
+
function laneRailsManifest() {
|
|
4281
|
+
const lanes = {};
|
|
4282
|
+
for (const lane of LANE_NAMES) {
|
|
4283
|
+
const t = LANE_TRACE[lane];
|
|
4284
|
+
// A lane that opens no run writes no trace file, so no `run-<lane>-…` name
|
|
4285
|
+
// can ever name it and the resolver could never reach the row. Omitting it
|
|
4286
|
+
// is an answer, not a gap.
|
|
4287
|
+
if (!t || !t.token) continue;
|
|
4288
|
+
const rows = [];
|
|
4289
|
+
for (const p of lanePhaseRows(lane))
|
|
4290
|
+
rows.push({ id: p.id, label: railLabel(p), kind: railKind(p), verbs: [] });
|
|
4291
|
+
for (const p of LANE_OWN_PHASES[lane] || [])
|
|
4292
|
+
rows.push({
|
|
4293
|
+
id: p.id,
|
|
4294
|
+
label: railLabel(p),
|
|
4295
|
+
kind: railKind(p),
|
|
4296
|
+
verbs: Array.isArray(p.trace_verbs) ? p.trace_verbs.slice() : [],
|
|
4297
|
+
});
|
|
4298
|
+
lanes[t.token] = { lane, tier: t.tier, phases: rows };
|
|
4299
|
+
}
|
|
4300
|
+
const families = {};
|
|
4301
|
+
for (const f of TRACE_FAMILIES) families[f] = { label: f, kind: PHASE_KIND[f] || "generic" };
|
|
4302
|
+
return { version: currentVersion(), kinds: PHASE_KINDS, families, lanes };
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4092
4305
|
// \`orc lane phases <lane>\` / \`--all\`. Exit 0 in every state; 2 for an unknown
|
|
4093
4306
|
// lane or no argument — the \`orc lane calls\` contract, unchanged.
|
|
4094
4307
|
//
|
|
@@ -4284,6 +4497,33 @@ function laneConfigCmd(lane, claudeDir) {
|
|
|
4284
4497
|
}
|
|
4285
4498
|
|
|
4286
4499
|
|
|
4500
|
+
// `orc lane rails [--json]` — the statusline's phase rail, exactly as it is
|
|
4501
|
+
// written to `hooks/orc-lane-rails.json` at install. It exists so the manifest
|
|
4502
|
+
// is inspectable without opening a hook directory, and so a golden test reads
|
|
4503
|
+
// the same object the hook does. Exit 0 in every state.
|
|
4504
|
+
//
|
|
4505
|
+
// `--json is not a summary`: this prints the WHOLE object, not a count of it.
|
|
4506
|
+
function laneRailsCmd() {
|
|
4507
|
+
const m = laneRailsManifest();
|
|
4508
|
+
if (wantsJson()) {
|
|
4509
|
+
emitJson(Object.assign({ ok: true }, m));
|
|
4510
|
+
return 0;
|
|
4511
|
+
}
|
|
4512
|
+
console.log(`\nORC statusline rails ${ui.color.gray("v" + m.version)}`);
|
|
4513
|
+
console.log(ui.color.gray(` kinds ${m.kinds.join(", ")}`));
|
|
4514
|
+
console.log(ui.color.gray(` families ${Object.keys(m.families).join(", ")}`));
|
|
4515
|
+
for (const [token, row] of Object.entries(m.lanes)) {
|
|
4516
|
+
console.log(`\n ${ui.color.bold(token)} ${ui.color.gray(row.lane + " · " + row.tier)}`);
|
|
4517
|
+
for (const p of row.phases)
|
|
4518
|
+
console.log(
|
|
4519
|
+
" " + String(p.kind).padEnd(8) + " " + String(p.label).padEnd(20) +
|
|
4520
|
+
ui.color.gray(p.verbs.length ? "verbs: " + p.verbs.join(", ") : "")
|
|
4521
|
+
);
|
|
4522
|
+
}
|
|
4523
|
+
console.log("");
|
|
4524
|
+
return 0;
|
|
4525
|
+
}
|
|
4526
|
+
|
|
4287
4527
|
// `orc lane` — the noun. `list` and `config` today; `phases` lands in W12 and
|
|
4288
4528
|
// `calls` in W10.
|
|
4289
4529
|
function lane() {
|
|
@@ -4299,13 +4539,16 @@ function lane() {
|
|
|
4299
4539
|
process.exit(laneCallsCmd(pos[2], claudeDir));
|
|
4300
4540
|
case "phases":
|
|
4301
4541
|
process.exit(lanePhasesCmd(pos[2], claudeDir));
|
|
4542
|
+
case "rails":
|
|
4543
|
+
process.exit(laneRailsCmd());
|
|
4302
4544
|
default:
|
|
4303
4545
|
console.error(
|
|
4304
4546
|
`Unknown: orc lane ${pos[1]}\n` +
|
|
4305
4547
|
"Usage: orc lane list [--json]\n" +
|
|
4306
4548
|
" orc lane config <lane> [--json]\n" +
|
|
4307
4549
|
" orc lane calls <lane> [--json] | orc lane calls --all [--json]\n" +
|
|
4308
|
-
" orc lane phases <lane> [--json] | orc lane phases --all [--json]"
|
|
4550
|
+
" orc lane phases <lane> [--json] | orc lane phases --all [--json]" +
|
|
4551
|
+
"\n orc lane rails [--json]"
|
|
4309
4552
|
);
|
|
4310
4553
|
process.exit(2);
|
|
4311
4554
|
}
|
|
@@ -32117,6 +32360,48 @@ function doctor() {
|
|
|
32117
32360
|
else if (dstat.state === "STALE") warn("diy-stale", `orc-diy flow STALE: ${dstat.reason}`, { reason: dstat.reason });
|
|
32118
32361
|
else ok(`orc-diy flow READY (${dstat.reason})`);
|
|
32119
32362
|
|
|
32363
|
+
// 5a) the composed status line (v1.3.0). THREE findings, and only when the
|
|
32364
|
+
// feature is ARMED: a layout somebody built and never turned on is not a
|
|
32365
|
+
// problem, it is a draft, and a doctor that warns about a normal state is a
|
|
32366
|
+
// doctor people learn to ignore.
|
|
32367
|
+
//
|
|
32368
|
+
// The hook FALLS BACK silently — it has to, because a hook cannot refuse —
|
|
32369
|
+
// so it records WHY in `statusline-state.json`, and this is where that
|
|
32370
|
+
// recording becomes a sentence. A status line that quietly went back to the
|
|
32371
|
+
// default and never said why is a bug the user cannot report.
|
|
32372
|
+
try {
|
|
32373
|
+
const cfg = resolvedConfig(claudeDir);
|
|
32374
|
+
if (String(cfg.statusline_custom || "off") !== "on") {
|
|
32375
|
+
ok("custom status line off (fine — ORC's built-in two lines are rendering)");
|
|
32376
|
+
} else {
|
|
32377
|
+
const st = (() => {
|
|
32378
|
+
try {
|
|
32379
|
+
return JSON.parse(fs.readFileSync(path.join(claudeDir, "orc", "statusline-state.json"), "utf8"));
|
|
32380
|
+
} catch (_) {
|
|
32381
|
+
return null;
|
|
32382
|
+
}
|
|
32383
|
+
})();
|
|
32384
|
+
const v = slValidate(slLoadOrDefault(claudeDir));
|
|
32385
|
+
// An ORPHANED component — a layout naming an id this ORC no longer ships
|
|
32386
|
+
// — is REPORTED, NEVER AUTO-REPAIRED, and the fix names the item. Which
|
|
32387
|
+
// component replaces a retired one is a design decision, not a repair.
|
|
32388
|
+
const orphan = v.errors.find((e) => /^unknown component/.test(e));
|
|
32389
|
+
if (orphan)
|
|
32390
|
+
warn("statusline-layout-orphaned", `status line: ${orphan}`, {
|
|
32391
|
+
fix: "orc statusline remove <line>:<pos> (or place a component that says the same thing)",
|
|
32392
|
+
});
|
|
32393
|
+
else if (!v.ok)
|
|
32394
|
+
warn("statusline-layout-invalid", `status line: ${v.errors[0]}`, {
|
|
32395
|
+
fix: "orc statusline validate",
|
|
32396
|
+
});
|
|
32397
|
+
else if (st && st.finding && Date.now() - (st.at || 0) < 24 * 60 * 60 * 1000)
|
|
32398
|
+
warn("statusline-layout-unreadable", `status line: the hook fell back to ORC's built-in lines (${st.finding})`, {
|
|
32399
|
+
fix: "orc statusline compile",
|
|
32400
|
+
});
|
|
32401
|
+
else ok("custom status line armed and valid");
|
|
32402
|
+
}
|
|
32403
|
+
} catch (_) {}
|
|
32404
|
+
|
|
32120
32405
|
// 5b) the wiki (v0.49.1). Exactly TWO findings, and the restraint is the
|
|
32121
32406
|
// design: a doctor that warns about normal states teaches people to ignore
|
|
32122
32407
|
// doctor. Both route to the Knowledge panel via FINDING_ROUTE — a caution
|
|
@@ -33384,6 +33669,35 @@ function usageResetMs(v) {
|
|
|
33384
33669
|
// reason — never 0. Unknown is not zero.
|
|
33385
33670
|
const USAGE_TOP_N = 5;
|
|
33386
33671
|
|
|
33672
|
+
// The per-agent token record (v1.4.0), written by `orc-subagent-line.js` from
|
|
33673
|
+
// what the agent panel hands it. THREE honest limits ride with every number
|
|
33674
|
+
// this returns, and each is a field rather than a footnote:
|
|
33675
|
+
//
|
|
33676
|
+
// - it is a FLOOR (`floor: true` per task). The hook sees a task only while
|
|
33677
|
+
// it is in the panel, so an agent that started and finished between two
|
|
33678
|
+
// renders is invisible, and a count read just before an agent finished is
|
|
33679
|
+
// short by whatever came after.
|
|
33680
|
+
// - `wired` says whether the hook is installed at all. Without it, "no rows"
|
|
33681
|
+
// and "no agents ran" are the same shape and different facts — so a row
|
|
33682
|
+
// reads `unavailable` rather than `not-seen`.
|
|
33683
|
+
// - it is SESSION-SCOPED, like every other bridge in these hooks. A record
|
|
33684
|
+
// from another session is not this session's spend.
|
|
33685
|
+
function usageSubagentTokens(claudeDir, sessionId) {
|
|
33686
|
+
const out = { wired: false, byAgent: {}, tasks: 0, floor: true };
|
|
33687
|
+
try {
|
|
33688
|
+
out.wired = fs.existsSync(path.join(claudeDir, "hooks", "orc-subagent-line.js"));
|
|
33689
|
+
const j = JSON.parse(fs.readFileSync(path.join(claudeDir, "orc", "subagent-usage.json"), "utf8"));
|
|
33690
|
+
if (!j || !j.tasks) return out;
|
|
33691
|
+
if (sessionId && j.session_id && j.session_id !== sessionId) return out;
|
|
33692
|
+
for (const t of Object.values(j.tasks)) {
|
|
33693
|
+
if (!t || !t.name || typeof t.tokens !== "number") continue;
|
|
33694
|
+
out.byAgent[t.name] = (out.byAgent[t.name] || 0) + t.tokens;
|
|
33695
|
+
out.tasks++;
|
|
33696
|
+
}
|
|
33697
|
+
} catch (_) {}
|
|
33698
|
+
return out;
|
|
33699
|
+
}
|
|
33700
|
+
|
|
33387
33701
|
// "12m43s" | "1m7s" | "45s" → seconds. Anything else → null, never 0.
|
|
33388
33702
|
function usageDurSeconds(s) {
|
|
33389
33703
|
if (!s) return null;
|
|
@@ -33534,6 +33848,11 @@ function usageReportCmd(claudeDir) {
|
|
|
33534
33848
|
|
|
33535
33849
|
const run = usageRunConsumers(claudeDir);
|
|
33536
33850
|
const foreign = usageForeignSpend(claudeDir);
|
|
33851
|
+
// v1.4.0 — the number this command has been reporting as `null` since v1.2.0.
|
|
33852
|
+
// It still cannot be read from the transcript, and that half of the v1.2.0
|
|
33853
|
+
// finding stands. Claude Code hands it to the AGENT PANEL, and
|
|
33854
|
+
// `orc-subagent-line.js` writes down what it was handed.
|
|
33855
|
+
const observed = usageSubagentTokens(claudeDir, led && led.session_id);
|
|
33537
33856
|
|
|
33538
33857
|
// Rank by measured wall time. A row with nothing measured keeps its slot.
|
|
33539
33858
|
const top = [];
|
|
@@ -33546,8 +33865,14 @@ function usageReportCmd(claudeDir) {
|
|
|
33546
33865
|
running: a.running,
|
|
33547
33866
|
wall_seconds: a.wall_seconds,
|
|
33548
33867
|
unmeasured_dispatches: a.unmeasured,
|
|
33549
|
-
|
|
33550
|
-
|
|
33868
|
+
// A FLOOR, NOT A TOTAL, and it says so in its own field. The subagent
|
|
33869
|
+
// hook only sees a task while it is in the agent panel: an agent that
|
|
33870
|
+
// started and finished between two renders is never seen, and a count
|
|
33871
|
+
// read at the last render before an agent finished is short by whatever
|
|
33872
|
+
// came after. A floor reported as a total is the same class of lie as a
|
|
33873
|
+
// zero reported for an unknown.
|
|
33874
|
+
tokens: observed.byAgent[a.agent] != null ? observed.byAgent[a.agent] : null,
|
|
33875
|
+
tokens_source: observed.byAgent[a.agent] != null ? "agent-panel-floor" : observed.wired ? "not-seen" : "unavailable",
|
|
33551
33876
|
});
|
|
33552
33877
|
}
|
|
33553
33878
|
for (const r of foreign.rows.slice(-USAGE_TOP_N)) {
|
|
@@ -33592,8 +33917,15 @@ function usageReportCmd(claudeDir) {
|
|
|
33592
33917
|
unreadable_spend_lines: foreign.unreadable,
|
|
33593
33918
|
// Say it on every emission. A reader who does not know this reads a
|
|
33594
33919
|
// wall-time ranking as a token ranking.
|
|
33595
|
-
|
|
33596
|
-
|
|
33920
|
+
// v1.4.0 — the note changed because the FACT changed, and it says exactly
|
|
33921
|
+
// which half moved. The transcript still records nothing; the AGENT PANEL
|
|
33922
|
+
// always did, and ORC now writes down what it is handed. What comes back is
|
|
33923
|
+
// a FLOOR, and it is labelled one everywhere it appears.
|
|
33924
|
+
tokens_note: observed.wired
|
|
33925
|
+
? "A Claude agent's tokens are a FLOOR, not a total: the transcript records none, so this is what the agent panel reported while each agent was visible in it — an agent that started and finished between two renders is not counted, and a count read just before an agent finished is short by whatever came after. `not-seen` means exactly that and never 0. Rows are ranked by MEASURED WALL TIME. Only `orc extra` foreign workers report a real four-kind vector."
|
|
33926
|
+
: "Claude Code records no token usage for a dispatched subagent in the transcript, so a Claude agent's tokens are null and never 0. The AGENT PANEL does report one — run `orc update` to wire `subagentStatusLine`, and this becomes a measured floor. Rows are ranked by MEASURED WALL TIME. Only `orc extra` foreign workers report real token vectors.",
|
|
33927
|
+
tokens_floor: observed.wired,
|
|
33928
|
+
tokens_observed_tasks: observed.tasks,
|
|
33597
33929
|
};
|
|
33598
33930
|
if (asJson) emitJson(out, code);
|
|
33599
33931
|
|
|
@@ -33997,6 +34329,2278 @@ function waitCmd() {
|
|
|
33997
34329
|
}
|
|
33998
34330
|
}
|
|
33999
34331
|
|
|
34332
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
34333
|
+
// `orc statusline` — the CLI Hook Interface. (v1.3.0 W1.)
|
|
34334
|
+
//
|
|
34335
|
+
// THE SPLIT OF LABOUR, and it is the whole design:
|
|
34336
|
+
//
|
|
34337
|
+
// the CLI COMPILES an authored layout into a flat render program;
|
|
34338
|
+
// the hook EXECUTES that program and resolves NOTHING.
|
|
34339
|
+
//
|
|
34340
|
+
// Three separate reasons, any one of which would be enough. (1) THE BUDGET:
|
|
34341
|
+
// Claude Code debounces at 300 ms and CANCELS the in-flight script, and W0
|
|
34342
|
+
// measured a bare `node` start on Windows at ~285 ms of that — so the hook has
|
|
34343
|
+
// roughly 15 ms, and resolving a theme, walking an inheritance chain and
|
|
34344
|
+
// interpolating a ramp on every keystroke is the wrong trade by four orders of
|
|
34345
|
+
// magnitude. (2) THE LAW: *the CLI computes, the hook renders* — if the hook
|
|
34346
|
+
// resolved inheritance there would be two ideas of what inheritance means, the
|
|
34347
|
+
// panel's and the bar's, on the one surface where the user compares them side by
|
|
34348
|
+
// side. (3) VALIDATION HAS TO HAPPEN SOMEWHERE IT CAN REFUSE. A hook cannot
|
|
34349
|
+
// refuse; it can only fail silently or paint garbage.
|
|
34350
|
+
//
|
|
34351
|
+
// This is the `orc diy` shape — config → compile → lock — applied to a second
|
|
34352
|
+
// surface, and it inherits that subsystem's staleness discipline wholesale.
|
|
34353
|
+
//
|
|
34354
|
+
// THE PANEL IS THE RECOMMENDED SURFACE. These flags exist so the panel has
|
|
34355
|
+
// something to shell; composing a three-line layout by typing `--slot` is worse
|
|
34356
|
+
// than the board, and `--help` says so.
|
|
34357
|
+
|
|
34358
|
+
const SL_SCHEMA = 1;
|
|
34359
|
+
|
|
34360
|
+
// ── The renderers ───────────────────────────────────────────────────────────
|
|
34361
|
+
// `kind` tells the compiler which op to lower to. `needs` is what the component
|
|
34362
|
+
// must offer for this renderer to be legal on it — the semantic tier of the
|
|
34363
|
+
// validator reads it, so a bar on a value with no maximum is refused BY NAME
|
|
34364
|
+
// rather than drawn as a lie.
|
|
34365
|
+
const STATUSLINE_RENDERERS = {
|
|
34366
|
+
// text shapes
|
|
34367
|
+
bare: { kind: "text", form: "value", needs: null },
|
|
34368
|
+
plain: { kind: "text", form: "label value", needs: null },
|
|
34369
|
+
"label-value": { kind: "text", form: "LABEL: value", needs: null },
|
|
34370
|
+
bracket: { kind: "text", form: "[LABEL: value]", needs: null, brackets: ["[", "]"] },
|
|
34371
|
+
paren: { kind: "text", form: "(value)", needs: null, brackets: ["(", ")"] },
|
|
34372
|
+
angle: { kind: "text", form: "⟨LABEL value⟩", needs: null, brackets: ["⟨", "⟩"] },
|
|
34373
|
+
badge: { kind: "text", form: "▏LABEL value▕", needs: null, brackets: ["▏", "▕"], reverse: true },
|
|
34374
|
+
pill: { kind: "text", form: "⟪LABEL⟫value", needs: null, brackets: ["⟪", "⟫"], reverse: true },
|
|
34375
|
+
stack: { kind: "text", form: "LABELvalue", needs: null },
|
|
34376
|
+
fraction: { kind: "text", form: "3/7", needs: "pair" },
|
|
34377
|
+
link: { kind: "link", form: "value (OSC 8)", needs: "url" },
|
|
34378
|
+
// proportion shapes — every one needs a BOUNDED numeric, or a bar is a lie
|
|
34379
|
+
bar: { kind: "bar", sub: "blocks", width: [4, 16], dflt_w: 10, needs: "bounded", with_value: true },
|
|
34380
|
+
"bar-only": { kind: "bar", sub: "blocks", width: [4, 16], dflt_w: 10, needs: "bounded" },
|
|
34381
|
+
blocks: { kind: "bar", sub: "blocks", width: [2, 16], dflt_w: 10, needs: "bounded" },
|
|
34382
|
+
meter: { kind: "bar", sub: "meter", width: [2, 16], dflt_w: 10, needs: "bounded" },
|
|
34383
|
+
fine: { kind: "bar", sub: "fine", width: [2, 16], dflt_w: 10, needs: "bounded" },
|
|
34384
|
+
"braille-bar": { kind: "bar", sub: "braille-bar", width: [2, 16], dflt_w: 10, needs: "bounded" },
|
|
34385
|
+
dots: { kind: "bar", sub: "dots", width: [2, 10], dflt_w: 5, needs: "ordinal" },
|
|
34386
|
+
gauge: { kind: "bar", sub: "gauge", width: [1, 1], dflt_w: 1, needs: "bounded" },
|
|
34387
|
+
ring: { kind: "bar", sub: "ring", width: [1, 1], dflt_w: 1, needs: "bounded" },
|
|
34388
|
+
micro: { kind: "bar", sub: "micro", width: [1, 1], dflt_w: 1, needs: "bounded" },
|
|
34389
|
+
gradient: { kind: "bar", sub: "gradient", width: [4, 16], dflt_w: 10, needs: "bounded", decoration: true },
|
|
34390
|
+
marker: { kind: "bar", sub: "marker", width: [4, 16], dflt_w: 10, needs: "bounded" },
|
|
34391
|
+
split: { kind: "bar", sub: "split", width: [4, 16], dflt_w: 10, needs: "bounded" },
|
|
34392
|
+
// state shapes
|
|
34393
|
+
dot: { kind: "state", needs: "states", shapes: false },
|
|
34394
|
+
shape: { kind: "state", needs: "states", shapes: true },
|
|
34395
|
+
icon: { kind: "state", needs: "states", shapes: true },
|
|
34396
|
+
word: { kind: "text", form: "STATE", needs: "states" },
|
|
34397
|
+
traffic: { kind: "state", needs: "ordinal", shapes: true, multi: true },
|
|
34398
|
+
// history shapes
|
|
34399
|
+
spark: { kind: "series", sub: "spark", width: [4, 16], dflt_w: 8, needs: "series" },
|
|
34400
|
+
"spark-braille": { kind: "series", sub: "spark-braille", width: [4, 16], dflt_w: 8, needs: "series" },
|
|
34401
|
+
trend: { kind: "series", sub: "trend", needs: "series" },
|
|
34402
|
+
delta: { kind: "series", sub: "delta", needs: "series" },
|
|
34403
|
+
// motion
|
|
34404
|
+
motif: { kind: "motif", needs: "motif" },
|
|
34405
|
+
pulse: { kind: "motif", sub: "pulse", needs: "motif" },
|
|
34406
|
+
};
|
|
34407
|
+
|
|
34408
|
+
// ── The glyph sets (design-language.md §2) ──────────────────────────────────
|
|
34409
|
+
// R1: every glyph here is EXACTLY one column in a monospaced terminal, and a
|
|
34410
|
+
// test walks all of them. Fullwidth forms, bare emoji and combining marks are
|
|
34411
|
+
// banned outright — get width wrong and the whole line after it shifts, every
|
|
34412
|
+
// render, forever. Nerd Font glyphs are NOT shipped: we cannot detect the font,
|
|
34413
|
+
// and ORC does not put a box on someone's bar.
|
|
34414
|
+
const STATUSLINE_GLYPHSETS = {
|
|
34415
|
+
blocks: {
|
|
34416
|
+
fill: "█", empty: "░", part: ["▏", "▎", "▍", "▌", "▋", "▊", "▉"],
|
|
34417
|
+
on: "●", off: "○", meter_on: "▮", meter_off: "▯", mark: "│", track: "─",
|
|
34418
|
+
arrows: ["▲", "▼", "▶"],
|
|
34419
|
+
gauge: ["○", "◔", "◑", "◕", "●"],
|
|
34420
|
+
micro: ["⠀", "⡀", "⣀", "⣄", "⣤", "⣦", "⣶", "⣿"],
|
|
34421
|
+
spark: ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"],
|
|
34422
|
+
braille_fill: "⣿", braille_empty: "⠀",
|
|
34423
|
+
ascii: { part: ["#"], fill: "#", empty: ".", on: "o", off: ".", meter_on: "|", meter_off: ".", mark: "|", track: "-", arrows: ["^", "v", ">"], gauge: [".", ":", "o", "O", "0"], micro: [" ", ".", ":", "-", "=", "+", "*", "#"], spark: ["_", ".", ",", "-", "=", "~", "^", "#"], braille_fill: "#", braille_empty: " " },
|
|
34424
|
+
},
|
|
34425
|
+
bars: {
|
|
34426
|
+
fill: "▮", empty: "▯", part: ["▏", "▎", "▍", "▌", "▋", "▊", "▉"],
|
|
34427
|
+
on: "◆", off: "◇", meter_on: "▮", meter_off: "▯", mark: "│", track: "─",
|
|
34428
|
+
arrows: ["↑", "↓", "→"],
|
|
34429
|
+
gauge: ["○", "◔", "◑", "◕", "●"],
|
|
34430
|
+
micro: ["⠀", "⡀", "⣀", "⣄", "⣤", "⣦", "⣶", "⣿"],
|
|
34431
|
+
spark: ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"],
|
|
34432
|
+
braille_fill: "⣿", braille_empty: "⠀",
|
|
34433
|
+
ascii: { part: ["|"], fill: "|", empty: ".", on: "*", off: ".", meter_on: "|", meter_off: ".", mark: "|", track: "-", arrows: ["^", "v", ">"], gauge: [".", ":", "o", "O", "0"], micro: [" ", ".", ":", "-", "=", "+", "*", "#"], spark: ["_", ".", ",", "-", "=", "~", "^", "#"], braille_fill: "#", braille_empty: " " },
|
|
34434
|
+
},
|
|
34435
|
+
pipes: {
|
|
34436
|
+
fill: "|", empty: "·", part: ["|"],
|
|
34437
|
+
on: "•", off: "·", meter_on: "|", meter_off: "·", mark: "!", track: "-",
|
|
34438
|
+
arrows: ["^", "v", ">"],
|
|
34439
|
+
gauge: [".", ":", "o", "O", "0"],
|
|
34440
|
+
micro: [" ", ".", ":", "-", "=", "+", "*", "#"],
|
|
34441
|
+
spark: ["_", ".", ",", "-", "=", "~", "^", "#"],
|
|
34442
|
+
braille_fill: "#", braille_empty: " ",
|
|
34443
|
+
ascii: null,
|
|
34444
|
+
},
|
|
34445
|
+
braille: {
|
|
34446
|
+
fill: "⣿", empty: "⠀", part: ["⡀", "⣀", "⣄", "⣤", "⣦", "⣶", "⣾"],
|
|
34447
|
+
on: "⣿", off: "⠄", meter_on: "⣿", meter_off: "⠄", mark: "⡇", track: "⠤",
|
|
34448
|
+
arrows: ["⡅", "⢪", "⠶"],
|
|
34449
|
+
gauge: ["⠀", "⣀", "⣤", "⣶", "⣿"],
|
|
34450
|
+
micro: ["⠀", "⡀", "⣀", "⣄", "⣤", "⣦", "⣶", "⣿"],
|
|
34451
|
+
spark: ["⣀", "⣄", "⣤", "⣦", "⣶", "⣾", "⣿", "⣿"],
|
|
34452
|
+
braille_fill: "⣿", braille_empty: "⠀",
|
|
34453
|
+
ascii: { part: ["#"], fill: "#", empty: " ", on: "#", off: ".", meter_on: "#", meter_off: ".", mark: "|", track: "-", arrows: ["^", "v", ">"], gauge: [" ", ".", "o", "O", "#"], micro: [" ", ".", ":", "-", "=", "+", "*", "#"], spark: ["_", ".", ",", "-", "=", "~", "^", "#"], braille_fill: "#", braille_empty: " " },
|
|
34454
|
+
},
|
|
34455
|
+
shade: {
|
|
34456
|
+
fill: "▓", empty: "▒", part: ["░", "▒", "▓"],
|
|
34457
|
+
on: "◉", off: "◎", meter_on: "▓", meter_off: "▒", mark: "│", track: "─",
|
|
34458
|
+
arrows: ["▲", "▼", "▶"],
|
|
34459
|
+
gauge: ["○", "◔", "◑", "◕", "●"],
|
|
34460
|
+
micro: ["⠀", "⡀", "⣀", "⣄", "⣤", "⣦", "⣶", "⣿"],
|
|
34461
|
+
spark: ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"],
|
|
34462
|
+
braille_fill: "⣿", braille_empty: "⠀",
|
|
34463
|
+
ascii: { part: ["#"], fill: "#", empty: "-", on: "*", off: ".", meter_on: "#", meter_off: "-", mark: "|", track: "-", arrows: ["^", "v", ">"], gauge: [".", ":", "o", "O", "0"], micro: [" ", ".", ":", "-", "=", "+", "*", "#"], spark: ["_", ".", ",", "-", "=", "~", "^", "#"], braille_fill: "#", braille_empty: " " },
|
|
34464
|
+
},
|
|
34465
|
+
minimal: {
|
|
34466
|
+
fill: "━", empty: "─", part: ["─"],
|
|
34467
|
+
on: "●", off: "○", meter_on: "━", meter_off: "─", mark: "┃", track: "─",
|
|
34468
|
+
arrows: ["▲", "▼", "▶"],
|
|
34469
|
+
gauge: ["○", "◔", "◑", "◕", "●"],
|
|
34470
|
+
micro: ["⠀", "⡀", "⣀", "⣄", "⣤", "⣦", "⣶", "⣿"],
|
|
34471
|
+
spark: ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"],
|
|
34472
|
+
braille_fill: "⣿", braille_empty: "⠀",
|
|
34473
|
+
ascii: { part: ["="], fill: "=", empty: "-", on: "o", off: ".", meter_on: "=", meter_off: "-", mark: "|", track: "-", arrows: ["^", "v", ">"], gauge: [".", ":", "o", "O", "0"], micro: [" ", ".", ":", "-", "=", "+", "*", "#"], spark: ["_", ".", ",", "-", "=", "~", "^", "#"], braille_fill: "#", braille_empty: " " },
|
|
34474
|
+
},
|
|
34475
|
+
ascii: {
|
|
34476
|
+
fill: "#", empty: ".", part: ["#"],
|
|
34477
|
+
on: "o", off: ".", meter_on: "|", meter_off: ".", mark: "|", track: "-",
|
|
34478
|
+
arrows: ["^", "v", ">"],
|
|
34479
|
+
gauge: [".", ":", "o", "O", "0"],
|
|
34480
|
+
micro: [" ", ".", ":", "-", "=", "+", "*", "#"],
|
|
34481
|
+
spark: ["_", ".", ",", "-", "=", "~", "^", "#"],
|
|
34482
|
+
braille_fill: "#", braille_empty: " ",
|
|
34483
|
+
ascii: null,
|
|
34484
|
+
},
|
|
34485
|
+
};
|
|
34486
|
+
|
|
34487
|
+
// ── Colour ──────────────────────────────────────────────────────────────────
|
|
34488
|
+
// Named ANSI slots are OFFERED FIRST and hex is behind Custom, because a named
|
|
34489
|
+
// slot follows the user's OWN terminal theme — and we do not know their
|
|
34490
|
+
// background. Every colour resolves to a precomputed SGR string at compile time;
|
|
34491
|
+
// the hook never converts a triple.
|
|
34492
|
+
const SL_ANSI_SLOTS = {
|
|
34493
|
+
black: 30, red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37,
|
|
34494
|
+
"bright-black": 90, "bright-red": 91, "bright-green": 92, "bright-yellow": 93,
|
|
34495
|
+
"bright-blue": 94, "bright-magenta": 95, "bright-cyan": 96, "bright-white": 97,
|
|
34496
|
+
default: 39,
|
|
34497
|
+
};
|
|
34498
|
+
|
|
34499
|
+
// The ramps map a 0–100 value onto a colour. Stops are user-movable, which is
|
|
34500
|
+
// how somebody moves their own "getting worrying" line.
|
|
34501
|
+
const STATUSLINE_RAMPS = {
|
|
34502
|
+
heat: { stops: [0, 60, 85], colors: ["green", "yellow", "red"], why: "high is bad" },
|
|
34503
|
+
cool: { stops: [0, 60, 85], colors: ["red", "yellow", "green"], why: "high is good" },
|
|
34504
|
+
mono: { stops: [0, 60, 85], colors: [null, null, null], emphasis: ["dim", "normal", "bold"], why: "NO_COLOR-safe: emphasis, not hue" },
|
|
34505
|
+
state: { stops: null, colors: null, why: "the component's own state colours" },
|
|
34506
|
+
};
|
|
34507
|
+
|
|
34508
|
+
// A theme is a coordinated set of NAMED-SLOT assignments applied across every
|
|
34509
|
+
// placed component at once. It is a STARTING POINT, never a lock: any
|
|
34510
|
+
// per-component colour overrides it, and the panel marks which components have
|
|
34511
|
+
// left the theme so a user can find their own overrides again.
|
|
34512
|
+
const STATUSLINE_THEMES = {
|
|
34513
|
+
terminal: { ok: "green", warn: "yellow", critical: "red", muted: "bright-black", accent: "cyan", label: "bright-black", value: "default" },
|
|
34514
|
+
dim: { ok: "green", warn: "yellow", critical: "red", muted: "bright-black", accent: "bright-black", label: "bright-black", value: "bright-black" },
|
|
34515
|
+
"high-contrast": { ok: "bright-green", warn: "bright-yellow", critical: "bright-red", muted: "white", accent: "bright-cyan", label: "bright-white", value: "bright-white" },
|
|
34516
|
+
mono: { ok: null, warn: null, critical: null, muted: null, accent: null, label: null, value: null },
|
|
34517
|
+
};
|
|
34518
|
+
|
|
34519
|
+
const SL_EMPHASIS = { normal: null, bold: 1, dim: 2, italic: 3, underline: 4, reverse: 7, strike: 9 };
|
|
34520
|
+
// SGR 5 is REFUSED outright. It is hostile, half of terminals disable it, and
|
|
34521
|
+
// there is no state on a status line that deserves it.
|
|
34522
|
+
const SL_REFUSED_EMPHASIS = ["blink"];
|
|
34523
|
+
|
|
34524
|
+
const STATUSLINE_HIDE_WHEN = [
|
|
34525
|
+
{ id: "never", says: "always render. A state keeps its slot." },
|
|
34526
|
+
{ id: "empty", says: "hide when the value is absent" },
|
|
34527
|
+
{ id: "zero", says: "hide when the value is exactly 0" },
|
|
34528
|
+
{ id: "unknown", says: "hide when the value could not be computed" },
|
|
34529
|
+
{ id: "ok", says: "hide while healthy — show me only problems" },
|
|
34530
|
+
{ id: "idle", says: "hide when no run is active" },
|
|
34531
|
+
{ id: "no-run", says: "hide when no ORC run was opened this session" },
|
|
34532
|
+
{ id: "narrow", says: "hide below this component's min_cols" },
|
|
34533
|
+
{ id: "wide", says: "hide above this component's max_cols" },
|
|
34534
|
+
];
|
|
34535
|
+
|
|
34536
|
+
const SL_FORMATS = ["percent", "ratio", "fraction", "decimal", "plain"];
|
|
34537
|
+
const SL_COMPACT = ["off", "si", "bytes"];
|
|
34538
|
+
const SL_CASES = ["none", "upper", "lower", "title"];
|
|
34539
|
+
const SL_TRUNCATE = ["end", "middle", "none"];
|
|
34540
|
+
|
|
34541
|
+
// ── The component registry ─────────────────────────────────────────────────
|
|
34542
|
+
// Mirrored row for row in `components-catalog.md`, with a golden test in BOTH
|
|
34543
|
+
// directions — the EXTRA_SLOTS / DIY_STEPS precedent. A component here with no
|
|
34544
|
+
// catalogue row fails; a catalogue row naming no component fails.
|
|
34545
|
+
//
|
|
34546
|
+
// Each row's `defaults` is most of the design work: a good default is what makes
|
|
34547
|
+
// a fresh placement look right with zero configuration.
|
|
34548
|
+
//
|
|
34549
|
+
// cost free = already in the payload · scan = already read by the one
|
|
34550
|
+
// throttled scan · new-read = adds a read INSIDE that scan · refused
|
|
34551
|
+
// unknown dash = renders `—` and keeps its slot · hide = vanishes.
|
|
34552
|
+
// NEVER `0` — a zero is a measurement.
|
|
34553
|
+
// bounded the value has a real 0..max, so a proportion renderer means
|
|
34554
|
+
// something. Without it a bar is a lie and the validator says so.
|
|
34555
|
+
const SL_TEXT = ["bare", "plain", "label-value", "bracket", "paren", "angle", "badge", "pill", "stack"];
|
|
34556
|
+
const SL_PROP = ["bar", "bar-only", "blocks", "meter", "fine", "braille-bar", "dots", "gauge", "ring", "micro", "gradient", "marker", "split"];
|
|
34557
|
+
|
|
34558
|
+
// The motif kinds ARE this component's state set: an unnamed phase gets
|
|
34559
|
+
// `generic`, never a guess. They are declared here rather than inferred from
|
|
34560
|
+
// SL_MOTIFS so the validator can check a `color_by_state` key against them.
|
|
34561
|
+
const SL_MOTIF_KINDS = ["look", "ask", "plan", "do", "check", "ship", "wait", "generic"];
|
|
34562
|
+
|
|
34563
|
+
function slRow(o) {
|
|
34564
|
+
return Object.assign(
|
|
34565
|
+
{
|
|
34566
|
+
group: "A",
|
|
34567
|
+
// Which BOARD this component belongs to. `status` is the main line;
|
|
34568
|
+
// `subagent` is the per-agent row. A component that binds one task means
|
|
34569
|
+
// nothing on the main line and vice versa, so the palette and the
|
|
34570
|
+
// validator both read this rather than guessing from the binding name.
|
|
34571
|
+
board: "status",
|
|
34572
|
+
label: null,
|
|
34573
|
+
renderers: ["bare"],
|
|
34574
|
+
defaults: {},
|
|
34575
|
+
states: null,
|
|
34576
|
+
shapes: null,
|
|
34577
|
+
bounded: false,
|
|
34578
|
+
// [min, max] when this component WRAPS others. `group` is the only one.
|
|
34579
|
+
children: null,
|
|
34580
|
+
series: null,
|
|
34581
|
+
unknown: "dash",
|
|
34582
|
+
cost: "free",
|
|
34583
|
+
refused_reason: null,
|
|
34584
|
+
params: null,
|
|
34585
|
+
binding: null,
|
|
34586
|
+
state_binding: null,
|
|
34587
|
+
time_based: false,
|
|
34588
|
+
},
|
|
34589
|
+
o
|
|
34590
|
+
);
|
|
34591
|
+
}
|
|
34592
|
+
|
|
34593
|
+
const STATUSLINE_COMPONENTS = [
|
|
34594
|
+
// ── Group A — Session and tier. Straight from the payload. Free. ─────────
|
|
34595
|
+
slRow({ id: "verdict", group: "A", summary: "The ORC-ready verdict icon.", renderers: ["icon", "shape", "dot", "word", "badge", "pill"], defaults: { render: "icon" }, states: ["ready", "boosted", "degrade"], shapes: { ready: "✅", boosted: "🚀", degrade: "⛔" }, binding: "verdict.state", state_binding: "verdict.state" }),
|
|
34596
|
+
slRow({ id: "verdict-reason", group: "A", summary: "Why the verdict is a warning. Empty when it is not.", renderers: ["bare", "paren", "bracket"], defaults: { render: "paren" }, states: ["ready", "boosted", "degrade"], unknown: "hide", binding: "verdict.reasons", state_binding: "verdict.state" }),
|
|
34597
|
+
slRow({ id: "orc-version", group: "A", label: "ORC", summary: "The installed ORC version.", renderers: ["bare", "plain", "label-value", "badge", "pill"], defaults: { render: "plain" }, binding: "orc.version" }),
|
|
34598
|
+
slRow({ id: "model", group: "A", summary: "The model this session is on.", renderers: ["bare", "plain", "label-value", "badge", "pill"], defaults: { render: "bare" }, binding: "model.name" }),
|
|
34599
|
+
slRow({ id: "model-short", group: "A", summary: "The model, abbreviated (`O5`).", renderers: ["bare", "badge", "pill"], defaults: { render: "bare" }, binding: "model.short" }),
|
|
34600
|
+
slRow({ id: "effort", group: "A", summary: "The reasoning effort level.", renderers: ["bare", "plain", "label-value", "dots", "traffic"], defaults: { render: "bare" }, states: ["low", "medium", "high", "xhigh", "max", "unknown"], shapes: { low: "▁", medium: "▃", high: "▅", xhigh: "▇", max: "█", unknown: "?" }, binding: "effort.level", state_binding: "effort.level" }),
|
|
34601
|
+
slRow({ id: "tier", group: "A", summary: "Model and effort as one word.", renderers: ["plain", "label-value", "badge", "pill"], defaults: { render: "plain" }, states: ["ready", "boosted", "degrade"], binding: "tier.text", state_binding: "verdict.state" }),
|
|
34602
|
+
slRow({ id: "context", group: "A", label: "context", summary: "How full the context window is.", renderers: SL_PROP.concat(["plain", "label-value", "bracket", "bare"]), defaults: { render: "plain", format: "percent", min_width: 3, align: "right", ramp: "heat" }, states: ["ok", "warn", "full"], shapes: { ok: "●", warn: "◐", full: "○" }, bounded: true, binding: "ctx.used_pct", state_binding: "ctx.state" }),
|
|
34603
|
+
slRow({ id: "context-left", group: "A", label: "left", summary: "How much context REMAINS.", renderers: ["bare", "plain", "label-value"].concat(SL_PROP), defaults: { render: "plain", format: "percent", min_width: 3, align: "right", ramp: "cool" }, states: ["ok", "warn", "full"], bounded: true, binding: "ctx.remaining_pct", state_binding: "ctx.state" }),
|
|
34604
|
+
slRow({ id: "context-tokens", group: "A", label: "ctx", summary: "Context tokens in use, all four kinds summed.", renderers: ["bare", "plain", "label-value"], defaults: { render: "plain", compact: "si" }, binding: "ctx.tokens" }),
|
|
34605
|
+
slRow({ id: "output-style", group: "A", label: "style", summary: "The active output style.", renderers: ["bare", "plain", "label-value", "badge"], defaults: { render: "plain" }, unknown: "hide", binding: "output.style" }),
|
|
34606
|
+
slRow({ id: "project", group: "A", summary: "The project folder name.", renderers: ["bare", "plain", "badge", "pill"], defaults: { render: "bare" }, binding: "project.name" }),
|
|
34607
|
+
slRow({ id: "cwd", group: "A", summary: "The working directory.", renderers: ["bare", "plain"], defaults: { render: "bare", truncate: "middle", max_len: 28 }, binding: "project.cwd" }),
|
|
34608
|
+
slRow({ id: "session-short", group: "A", label: "sid", summary: "The first 8 characters of the session id.", renderers: ["bare", "label-value"], defaults: { render: "label-value" }, unknown: "hide", binding: "session.short" }),
|
|
34609
|
+
|
|
34610
|
+
// ── Group B — Quota and spend ────────────────────────────────────────────
|
|
34611
|
+
slRow({ id: "quota-5h", group: "B", label: "5h", summary: "The 5-hour usage window.", renderers: SL_PROP.concat(["plain", "label-value", "bare", "spark"]), defaults: { render: "plain", format: "percent", min_width: 3, align: "right", ramp: "heat" }, states: ["ok", "warn", "critical"], shapes: { ok: "●", warn: "◐", critical: "○" }, bounded: true, series: "quota5h", binding: "quota.5h.pct", state_binding: "quota.5h.state" }),
|
|
34612
|
+
slRow({ id: "quota-5h-reset", group: "B", label: "reset", summary: "Minutes until the 5-hour window resets.", renderers: ["bare", "plain", "label-value", "paren"], defaults: { render: "paren", suffix: "m" }, binding: "quota.5h.reset", time_based: true }),
|
|
34613
|
+
slRow({ id: "quota-week", group: "B", label: "wk", summary: "The 7-day usage window.", renderers: SL_PROP.concat(["plain", "label-value", "bare", "spark"]), defaults: { render: "plain", format: "percent", min_width: 3, align: "right", ramp: "heat" }, states: ["ok", "warn", "critical"], shapes: { ok: "●", warn: "◐", critical: "○" }, bounded: true, series: "quotawk", binding: "quota.week.pct", state_binding: "quota.week.state" }),
|
|
34614
|
+
slRow({ id: "quota-worst", group: "B", label: "quota", summary: "Whichever window is fuller. THE WORST WINDOW DECIDES.", renderers: SL_PROP.concat(["plain", "shape", "traffic", "label-value", "bare"]), defaults: { render: "blocks", ramp: "heat", width: 10 }, states: ["ok", "warn", "critical"], shapes: { ok: "●", warn: "◐", critical: "○" }, bounded: true, binding: "quota.worst.pct", state_binding: "quota.worst.state" }),
|
|
34615
|
+
slRow({ id: "quota-trend", group: "B", summary: "Which way the 5-hour window is moving.", renderers: ["trend", "delta", "spark"], defaults: { render: "trend" }, states: ["rising", "flat", "falling"], shapes: { rising: "▲", flat: "▶", falling: "▼" }, series: "quota5h", cost: "scan", binding: "quota.trend", state_binding: "quota.trend" }),
|
|
34616
|
+
slRow({ id: "ucs", group: "B", label: "ucs", summary: "How far the 5-hour window moved while THIS session ran. A delta of an account-wide window, not a private meter.", renderers: ["plain", "label-value", "bar", "blocks", "bare", "spark", "delta"], defaults: { render: "plain", format: "percent", min_width: 3, align: "right" }, bounded: true, series: "ucs", cost: "scan", binding: "ucs.pct" }),
|
|
34617
|
+
slRow({ id: "burn-rate", group: "B", label: "rate", summary: "Percent of the 5-hour window per hour. Needs 15 minutes of session before it means anything.", renderers: ["bare", "plain", "label-value", "spark"], defaults: { render: "plain", suffix: "%/h" }, states: ["ok", "warn", "critical"], series: "burn", cost: "scan", binding: "burn.rate", state_binding: "burn.state" }),
|
|
34618
|
+
slRow({ id: "mtok", group: "B", label: "MTok", summary: "MAIN token: this session's own turns, four kinds summed. Claude Code records NO token usage for a dispatched subagent, so this is the conversation's cost, not the run's.", renderers: ["plain", "label-value", "bare", "spark", "spark-braille"], defaults: { render: "plain", compact: "si" }, series: "mtok", cost: "scan", binding: "mtok.total" }),
|
|
34619
|
+
slRow({ id: "mtok-kind", group: "B", summary: "ONE token kind. Clone it per kind — the four are never blended in a report.", renderers: ["plain", "label-value", "bare", "spark"], defaults: { render: "label-value", compact: "si" }, series: "mtokkind", cost: "scan", params: { kind: { options: ["input", "cache_write", "cache_read", "output"], dflt: "cache_read" } }, binding: "mtok.cache_read" }),
|
|
34620
|
+
slRow({ id: "quota-spend", group: "B", label: "spend", summary: "The spend-limit window — a THIRD window ORC does not render today.", renderers: SL_PROP.concat(["plain", "label-value", "bare"]), defaults: { render: "plain", format: "percent", min_width: 3, align: "right", ramp: "heat" }, states: ["ok", "warn", "critical"], bounded: true, unknown: "hide", binding: "quota.spend.pct", state_binding: "quota.spend.state" }),
|
|
34621
|
+
slRow({ id: "quota-spend-reset", group: "B", label: "reset", summary: "Minutes until the spend limit resets.", renderers: ["bare", "plain", "paren"], defaults: { render: "paren", suffix: "m" }, unknown: "hide", binding: "quota.spend.reset", time_based: true }),
|
|
34622
|
+
slRow({ id: "cost-usd", group: "B", summary: "Session cost in dollars, straight from the payload. No price table, no join.", renderers: ["bare", "plain", "label-value", "spark"], defaults: { render: "bare", prefix: "$", precision: 2 }, series: "cost", binding: "cost.usd" }),
|
|
34623
|
+
slRow({ id: "cost-rate", group: "B", label: "/h", summary: "Dollars per hour of session.", renderers: ["bare", "plain", "label-value"], defaults: { render: "plain", prefix: "$", precision: 2 }, binding: "cost.rate_usd_h" }),
|
|
34624
|
+
slRow({ id: "api-time", group: "B", label: "api", summary: "Time spent waiting on the API.", renderers: ["bare", "plain", "label-value"], defaults: { render: "plain", suffix: "m" }, binding: "cost.api_ms" }),
|
|
34625
|
+
slRow({ id: "api-ratio", group: "B", label: "api", summary: "How much of this session was waiting rather than thinking.", renderers: ["bar", "blocks", "gauge", "ring", "plain", "fine", "dots"], defaults: { render: "gauge" }, bounded: true, binding: "cost.api_ratio" }),
|
|
34626
|
+
slRow({ id: "wall-time", group: "B", label: "wall", summary: "Total session wall time.", renderers: ["bare", "plain", "label-value"], defaults: { render: "plain", suffix: "m" }, binding: "cost.wall_ms", time_based: true }),
|
|
34627
|
+
slRow({ id: "tok-speed", group: "B", label: "tok/s", summary: "Main tokens per second of session.", renderers: ["bare", "plain", "label-value", "spark"], defaults: { render: "plain" }, series: "speed", cost: "scan", binding: "mtok.speed" }),
|
|
34628
|
+
|
|
34629
|
+
// ── Group C — Run state. From the ONE throttled scan. ────────────────────
|
|
34630
|
+
slRow({ id: "status", group: "C", label: "status", summary: "The lane and phase ORC is in right now. The ONE segment allowed to vanish: a phase the disk cannot prove is HIDDEN, never guessed.", renderers: ["plain", "label-value", "badge", "pill", "bare"], defaults: { render: "plain" }, unknown: "hide", cost: "scan", binding: "run.status" }),
|
|
34631
|
+
slRow({ id: "phase", group: "C", summary: "The phase alone.", renderers: ["bare", "plain", "label-value", "badge"], defaults: { render: "bare" }, unknown: "hide", cost: "scan", binding: "run.phase" }),
|
|
34632
|
+
slRow({ id: "phase-motif", group: "C", summary: "The animated mark. A LIVENESS TELL on a pull surface: it advances while the session is active and freezes when idle, which is true and is the point.", renderers: ["motif", "pulse", "icon", "shape"], defaults: { render: "motif" }, states: SL_MOTIF_KINDS, shapes: { look: "◔", ask: "?", plan: "▁", do: "▰", check: "◇", ship: "›", wait: "·", generic: "•" }, unknown: "hide", cost: "scan", binding: "run.phase_kind", state_binding: "run.phase_kind", time_based: true }),
|
|
34633
|
+
slRow({ id: "lane", group: "C", summary: "The running lane.", renderers: ["bare", "plain", "badge", "pill"], defaults: { render: "bare" }, unknown: "hide", cost: "scan", binding: "run.lane" }),
|
|
34634
|
+
slRow({ id: "agents", group: "C", label: "agents", summary: "Subagents dispatched this session.", renderers: ["plain", "label-value", "bare", "spark"], defaults: { render: "plain", min_width: 2, align: "right" }, series: "agents", cost: "scan", binding: "run.agents" }),
|
|
34635
|
+
slRow({ id: "agents-running", group: "C", label: "running", summary: "Agents still in flight. Never hidden by default — an agent in flight is the thing a user most needs to see.", renderers: ["plain", "label-value", "bare", "dot", "shape", "dots", "paren"], defaults: { render: "paren" }, states: ["idle", "running"], shapes: { idle: "○", running: "●" }, cost: "scan", binding: "run.running", state_binding: "run.running_state" }),
|
|
34636
|
+
slRow({ id: "duration", group: "C", label: "Dur", summary: "How long this session has been open.", renderers: ["plain", "label-value", "bare"], defaults: { render: "plain", min_width: 4, align: "right", suffix: "m" }, cost: "scan", binding: "run.duration_min", time_based: true }),
|
|
34637
|
+
slRow({ id: "run-slug", group: "C", label: "run", summary: "The active run's slug.", renderers: ["bare", "plain", "label-value"], defaults: { render: "bare", truncate: "middle", max_len: 20 }, unknown: "hide", cost: "scan", binding: "run.slug" }),
|
|
34638
|
+
slRow({ id: "inflight", group: "C", label: "inflight", summary: "Whether a dispatch is still open. UNKNOWN is never CLEAR.", renderers: ["word", "shape", "dot", "badge", "traffic"], defaults: { render: "shape" }, states: ["clear", "in-flight", "unknown"], shapes: { clear: "○", "in-flight": "●", unknown: "?" }, cost: "scan", binding: "run.inflight_state", state_binding: "run.inflight_state" }),
|
|
34639
|
+
slRow({ id: "last-agent", group: "C", summary: "The last agent name the trace saw.", renderers: ["bare", "plain", "badge"], defaults: { render: "bare", truncate: "middle", max_len: 24 }, unknown: "hide", cost: "scan", binding: "run.last_agent" }),
|
|
34640
|
+
slRow({ id: "retry-count", group: "C", label: "retry", summary: "Retries recorded in the active trace.", renderers: ["plain", "bare", "dots"], defaults: { render: "plain" }, unknown: "hide", cost: "scan", binding: "run.retries" }),
|
|
34641
|
+
slRow({ id: "trace-age", group: "C", label: "last", summary: "Minutes since the active trace last moved.", renderers: ["plain", "label-value", "bare", "shape"], defaults: { render: "plain", suffix: "m" }, states: ["live", "idle", "stale"], shapes: { live: "●", idle: "◐", stale: "○" }, cost: "scan", binding: "run.trace_age_min", state_binding: "run.trace_state", time_based: true }),
|
|
34642
|
+
slRow({ id: "lanes", group: "C", label: "lanes", summary: "Every lane that dispatched this session.", renderers: ["plain", "label-value", "bare"], defaults: { render: "plain", truncate: "end", max_len: 24 }, cost: "scan", binding: "run.lanes" }),
|
|
34643
|
+
|
|
34644
|
+
// ── Group K — Cache health. Free, and the most decision-relevant block in
|
|
34645
|
+
// the payload. ───────────────────────────────────────────────────────
|
|
34646
|
+
slRow({ id: "cache", group: "K", label: "cache", summary: "Whether the prompt cache is warm.", renderers: ["word", "shape", "dot", "badge"], defaults: { render: "shape" }, states: ["warm", "cold"], shapes: { warm: "●", cold: "○" }, binding: "cache.state", state_binding: "cache.state" }),
|
|
34647
|
+
slRow({ id: "cache-hit", group: "K", label: "hit", summary: "Cache hit ratio.", renderers: SL_PROP.concat(["plain", "bare", "spark", "label-value"]), defaults: { render: "bar", ramp: "cool", width: 10, format: "percent", min_width: 3, align: "right" }, states: ["poor", "good"], bounded: true, series: "cachehit", binding: "cache.hit_ratio", state_binding: "cache.hit_state" }),
|
|
34648
|
+
slRow({ id: "cache-ttl", group: "K", label: "ttl", summary: "The cache TTL in force.", renderers: ["bare", "plain", "badge", "label-value"], defaults: { render: "bare" }, unknown: "hide", binding: "cache.ttl" }),
|
|
34649
|
+
slRow({ id: "cache-expires", group: "K", label: "exp", summary: "Minutes until the cache goes cold.", renderers: ["bare", "plain", "gauge", "ring", "bar", "marker", "label-value"], defaults: { render: "gauge" }, states: ["fresh", "soon", "expired"], shapes: { fresh: "●", soon: "◐", expired: "○" }, bounded: true, unknown: "hide", binding: "cache.expires_in", state_binding: "cache.expires_state", time_based: true }),
|
|
34650
|
+
slRow({ id: "cache-requests", group: "K", label: "req", summary: "Requests served this session.", renderers: ["plain", "label-value", "bare", "fraction"], defaults: { render: "plain" }, binding: "cache.requests" }),
|
|
34651
|
+
slRow({ id: "cache-misses", group: "K", label: "miss", summary: "Cache misses.", renderers: ["plain", "label-value", "bare"], defaults: { render: "plain" }, binding: "cache.misses" }),
|
|
34652
|
+
slRow({ id: "cache-write", group: "K", label: "cw", summary: "Tokens written to the cache.", renderers: ["plain", "label-value", "bare", "spark"], defaults: { render: "plain", compact: "si" }, series: "cachewrite", binding: "cache.write_tokens" }),
|
|
34653
|
+
slRow({ id: "cache-rebuilds", group: "K", label: "rb", summary: "Times the cache has been rebuilt.", renderers: ["plain", "bare", "label-value"], defaults: { render: "plain" }, unknown: "hide", binding: "cache.rebuilds" }),
|
|
34654
|
+
slRow({ id: "cache-recache-cost", group: "K", label: "cold", summary: "Tokens it would cost to rebuild the cache from cold.", renderers: ["plain", "label-value", "bare"], defaults: { render: "plain", compact: "si" }, unknown: "hide", binding: "cache.recache_cost" }),
|
|
34655
|
+
|
|
34656
|
+
// ── Group L — Session mode and identity ─────────────────────────────────
|
|
34657
|
+
slRow({ id: "cc-version", group: "L", label: "cc", summary: "Claude Code's own version.", renderers: ["bare", "plain", "label-value", "badge"], defaults: { render: "label-value" }, unknown: "hide", binding: "cc.version" }),
|
|
34658
|
+
slRow({ id: "session-name", group: "L", summary: "The session's name, if it has one.", renderers: ["bare", "plain", "badge", "pill"], defaults: { render: "bare", truncate: "middle", max_len: 20 }, unknown: "hide", binding: "session.name" }),
|
|
34659
|
+
slRow({ id: "vim-mode", group: "L", summary: "The vim mode, when vim bindings are on.", renderers: ["word", "badge", "shape"], defaults: { render: "badge" }, states: ["NORMAL", "INSERT", "VISUAL", "REPLACE"], shapes: { NORMAL: "N", INSERT: "I", VISUAL: "V", REPLACE: "R" }, unknown: "hide", binding: "vim.mode", state_binding: "vim.mode" }),
|
|
34660
|
+
slRow({ id: "thinking", group: "L", label: "think", summary: "Whether extended thinking is on.", renderers: ["word", "shape", "icon", "dot"], defaults: { render: "shape" }, states: ["on", "off"], shapes: { on: "●", off: "○" }, unknown: "hide", binding: "thinking.state", state_binding: "thinking.state" }),
|
|
34661
|
+
slRow({ id: "fast-mode", group: "L", label: "fast", summary: "Whether fast mode is on.", renderers: ["word", "shape", "icon", "badge"], defaults: { render: "shape" }, states: ["on", "off"], shapes: { on: "▶", off: "·" }, unknown: "hide", binding: "fast.state", state_binding: "fast.state" }),
|
|
34662
|
+
slRow({ id: "agent-name", group: "L", summary: "The agent this session is running as.", renderers: ["bare", "plain", "badge", "pill"], defaults: { render: "badge" }, unknown: "hide", binding: "agent.name" }),
|
|
34663
|
+
slRow({ id: "added-dirs", group: "L", label: "+dirs", summary: "How many extra directories are in the workspace.", renderers: ["plain", "label-value", "bare"], defaults: { render: "plain" }, unknown: "hide", binding: "added.dirs" }),
|
|
34664
|
+
slRow({ id: "exceeds-200k", group: "L", summary: "Whether the context window is larger than 200K.", renderers: ["word", "shape", "icon"], defaults: { render: "shape" }, states: ["no", "yes"], shapes: { no: "·", yes: "▲" }, unknown: "hide", binding: "ctx.exceeds_200k", state_binding: "ctx.exceeds_200k" }),
|
|
34665
|
+
slRow({ id: "compactions", group: "L", label: "compact", summary: "Compactions this session.", renderers: ["plain", "label-value", "bare"], defaults: { render: "plain" }, unknown: "hide", cost: "scan", binding: "compactions" }),
|
|
34666
|
+
|
|
34667
|
+
// ── Group H — Project and VCS. The free half; the three refused rows are
|
|
34668
|
+
// at the end and carry their reason. ────────────────────────────────
|
|
34669
|
+
slRow({ id: "branch", group: "H", summary: "The current git branch, read from .git/HEAD with NO subprocess.", renderers: ["bare", "plain", "label-value", "badge", "pill"], defaults: { render: "bare", truncate: "middle", max_len: 24 }, unknown: "hide", cost: "scan", binding: "git.branch" }),
|
|
34670
|
+
slRow({ id: "head-short", group: "H", summary: "The short commit hash.", renderers: ["bare", "plain", "paren"], defaults: { render: "bare" }, unknown: "hide", cost: "scan", binding: "git.head" }),
|
|
34671
|
+
slRow({ id: "repo-name", group: "H", summary: "The repository name, from the payload — no git call.", renderers: ["bare", "plain", "badge"], defaults: { render: "bare" }, unknown: "hide", binding: "repo.name" }),
|
|
34672
|
+
slRow({ id: "repo-owner", group: "H", summary: "The repository owner.", renderers: ["bare", "plain"], defaults: { render: "bare" }, unknown: "hide", binding: "repo.owner" }),
|
|
34673
|
+
slRow({ id: "repo-host", group: "H", summary: "The forge this repo lives on.", renderers: ["bare", "badge"], defaults: { render: "badge" }, unknown: "hide", binding: "repo.host" }),
|
|
34674
|
+
slRow({ id: "worktree", group: "H", summary: "Whether this is a worktree, and which kind.", renderers: ["bare", "shape", "badge", "word"], defaults: { render: "shape" }, states: ["main", "worktree", "detached"], shapes: { main: "·", worktree: "⑂", detached: "!" }, unknown: "hide", binding: "worktree.state", state_binding: "worktree.state" }),
|
|
34675
|
+
slRow({ id: "worktree-branch", group: "H", summary: "The worktree's branch.", renderers: ["bare", "plain", "paren"], defaults: { render: "bare" }, unknown: "hide", binding: "worktree.branch" }),
|
|
34676
|
+
slRow({ id: "worktree-origin", group: "H", label: "from", summary: "The branch the worktree came from.", renderers: ["bare", "plain", "paren"], defaults: { render: "paren" }, unknown: "hide", binding: "worktree.origin" }),
|
|
34677
|
+
slRow({ id: "lines-added", group: "H", label: "+", summary: "Lines added this session — a diff size with no subprocess.", renderers: ["bare", "plain", "label-value"], defaults: { render: "plain", sign: "always" }, binding: "cost.lines_added" }),
|
|
34678
|
+
slRow({ id: "lines-removed", group: "H", label: "−", summary: "Lines removed this session.", renderers: ["bare", "plain", "label-value"], defaults: { render: "plain" }, binding: "cost.lines_removed" }),
|
|
34679
|
+
slRow({ id: "lines-net", group: "H", label: "Δ", summary: "Net lines this session.", renderers: ["bare", "plain", "trend", "spark", "label-value"], defaults: { render: "plain", sign: "always" }, states: ["growing", "shrinking", "flat"], shapes: { growing: "▲", shrinking: "▼", flat: "▶" }, series: "lines", binding: "cost.lines_net", state_binding: "cost.lines_state" }),
|
|
34680
|
+
slRow({ id: "pr-number", group: "H", label: "PR", summary: "The pull request for this branch. The link is ALWAYS a decoration on text that reads fine without it — Terminal.app does not support OSC 8.", renderers: ["bare", "plain", "label-value", "badge", "link"], defaults: { render: "link", prefix: "#" }, unknown: "hide", binding: "pr.number" }),
|
|
34681
|
+
slRow({ id: "pr-review", group: "H", summary: "The PR's review state.", renderers: ["word", "shape", "badge"], defaults: { render: "shape" }, states: ["pending", "approved", "changes"], shapes: { pending: "◐", approved: "●", changes: "▲" }, unknown: "hide", binding: "pr.review", state_binding: "pr.review" }),
|
|
34682
|
+
slRow({ id: "git-dirty", group: "H", label: "±", summary: "Uncommitted changes.", renderers: ["plain", "label-value", "bare"], defaults: { render: "plain" }, cost: "refused", refused_reason: "it needs a `git status` subprocess, and W0 measured one at 53ms against roughly 15ms of headroom on a surface that re-renders on every keystroke. `lines-added` / `lines-removed` / `lines-net` answer most of the same question from the payload, for free." }),
|
|
34683
|
+
slRow({ id: "ahead-behind", group: "H", summary: "Commits ahead of / behind the remote.", renderers: ["plain", "bare"], defaults: { render: "plain" }, cost: "refused", refused_reason: "it needs a `git rev-list` subprocess per render. The wiki segment used to do exactly this and it pushed the whole status line past Claude Code's 300ms cancel line — see the v1.3.0 changelog." }),
|
|
34684
|
+
slRow({ id: "staged", group: "H", label: "staged", summary: "Staged files.", renderers: ["plain", "bare"], defaults: { render: "plain" }, cost: "refused", refused_reason: "it needs a `git status` subprocess. Same measurement as `git-dirty`." }),
|
|
34685
|
+
|
|
34686
|
+
// ── Group I — Static, structural and user-defined ───────────────────────
|
|
34687
|
+
slRow({ id: "text", group: "I", summary: "Any text you type. Also where a Nerd Font glyph goes, if you have one — ORC ships none, because it cannot detect the font.", renderers: ["bare", "badge", "pill", "bracket", "angle"], defaults: { render: "bare" }, unknown: "hide", params: { text: { free: true, dflt: "" } }, binding: "static.text" }),
|
|
34688
|
+
slRow({ id: "clock", group: "I", summary: "The wall clock. Needs `refreshInterval` to tick.", renderers: ["bare", "plain", "label-value"], defaults: { render: "bare" }, params: { format: { options: ["HH:mm", "HH:mm:ss", "h:mm a"], dflt: "HH:mm" } }, binding: "clock.now", time_based: true }),
|
|
34689
|
+
slRow({ id: "elapsed", group: "I", summary: "Minutes since this session opened.", renderers: ["bare", "plain", "label-value"], defaults: { render: "plain", suffix: "m" }, binding: "session.elapsed_min", time_based: true }),
|
|
34690
|
+
slRow({ id: "spacer", group: "I", summary: "Literal spaces. Does not count against the 5-per-line limit.", renderers: ["bare"], defaults: { render: "bare" }, unknown: "hide", params: { width: { options: [1, 2, 3, 4, 6, 8], dflt: 1 } }, binding: "static.text" }),
|
|
34691
|
+
slRow({ id: "divider", group: "I", summary: "A styled break between GROUPS of components — the second separator kind. Does not count against the 5-per-line limit.", renderers: ["bare"], defaults: { render: "bare" }, unknown: "hide", params: { text: { free: true, dflt: " │ " } }, binding: "static.text" }),
|
|
34692
|
+
slRow({ id: "fill", group: "I", summary: "Pushes everything after it to the right edge. Exact, because Claude Code sets COLUMNS before the script runs. Does not count against the 5-per-line limit.", renderers: ["bare"], defaults: { render: "bare" }, unknown: "hide", params: { weight: { options: [1, 2, 3], dflt: 1 } }, binding: "static.text" }),
|
|
34693
|
+
|
|
34694
|
+
|
|
34695
|
+
// ── Group D — Knowledge. Every row here comes out of wiki-meta.json, the
|
|
34696
|
+
// pattern cache or the crosslink config: small JSON files on a 60-second
|
|
34697
|
+
// clock. None of them shells out.
|
|
34698
|
+
slRow({ id: "wiki", group: "D", label: "wiki", summary: "How old the project wiki is, from the wiki's own oldest anchor. The tier is COMPUTED on read — a stored status word goes stale. This is the FLOOR: `orc wiki status` computes the coverage-relative tier, which needs one git walk per document and cannot ride on this surface.", renderers: ["word", "shape", "dot", "badge", "traffic"], defaults: { render: "word" }, states: ["fresh", "aging", "stale", "unregistered", "none"], shapes: { fresh: "●", aging: "◐", stale: "○", unregistered: "?", none: "·" }, cost: "new-read", binding: "wiki.tier", state_binding: "wiki.tier" }),
|
|
34699
|
+
slRow({ id: "wiki-distance", group: "D", label: "c", summary: "Commits since the wiki was scanned.", renderers: ["bare", "plain", "label-value", "paren"], defaults: { render: "paren", suffix: "c" }, states: ["fresh", "aging", "stale"], cost: "new-read", binding: "wiki.distance", state_binding: "wiki.tier" }),
|
|
34700
|
+
slRow({ id: "wiki-worst", group: "D", label: "worst", summary: "The wiki document deciding its tier.", renderers: ["bare", "plain", "label-value"], defaults: { render: "bare", truncate: "middle", max_len: 24 }, unknown: "hide", cost: "refused", refused_reason: "freshness is COVERAGE-RELATIVE, so finding the worst document means one `git rev-list` PER DOCUMENT against that document's own covered files. `orc wiki status` computes it; a status line cannot afford N subprocesses on a surface that redraws on every keystroke." }),
|
|
34701
|
+
slRow({ id: "wiki-coverage", group: "D", label: "cov", summary: "How much of the repo the wiki covers.", renderers: ["plain", "bare", "label-value"], defaults: { render: "plain", format: "percent" }, cost: "refused", refused_reason: "coverage is COMPUTED, not stored: it needs the tracked-file list, which is a `git ls-files` subprocess, and then a match per document. `orc wiki coverage` is where that answer lives. A status line reading a field wiki-meta.json does not have would render a confident number nobody measured." }),
|
|
34702
|
+
slRow({ id: "wiki-docs", group: "D", label: "docs", summary: "How many wiki documents are registered.", renderers: ["plain", "label-value", "bare"], defaults: { render: "plain" }, cost: "new-read", binding: "wiki.docs" }),
|
|
34703
|
+
slRow({ id: "wiki-blindspot", group: "D", label: "blind", summary: "Whether the wiki has a STRUCTURAL blind spot. That is a coverage gap, not doc rot.", renderers: ["plain", "bare"], defaults: { render: "plain" }, unknown: "hide", cost: "refused", refused_reason: "a blind spot is files changed since the floor that NO document covers — the same per-document git walk `wiki-worst` needs. `orc wiki status` reports it, and `orc wiki impact` escalates it, which is the right place for that signal." }),
|
|
34704
|
+
slRow({ id: "pattern", group: "D", label: "pattern", summary: "Whether a code pattern is cached for this project.", renderers: ["word", "plain", "label-value", "bare", "shape"], defaults: { render: "plain" }, states: ["cached", "none"], shapes: { cached: "●", none: "○" }, cost: "new-read", binding: "pattern.state", state_binding: "pattern.state" }),
|
|
34705
|
+
slRow({ id: "crosslink", group: "D", label: "peers", summary: "How many peer repositories are linked.", renderers: ["plain", "label-value", "bare", "dots"], defaults: { render: "plain" }, states: ["linked", "none"], shapes: { linked: "●", none: "○" }, cost: "new-read", binding: "crosslink.peers", state_binding: "crosslink.state" }),
|
|
34706
|
+
slRow({ id: "gotchas", group: "D", label: "gotchas", summary: "How many repair memories are kept.", renderers: ["plain", "label-value", "bare"], defaults: { render: "plain" }, cost: "new-read", binding: "gotchas.count" }),
|
|
34707
|
+
|
|
34708
|
+
// ── Group E — Extra (foreign dispatch) ──────────────────────────────────
|
|
34709
|
+
slRow({ id: "extra", group: "E", label: "orc-extra", summary: "Whether foreign dispatch is armed. A run that sends work off Claude says so.", renderers: ["word", "plain", "label-value", "dot", "shape", "badge"], defaults: { render: "label-value" }, states: ["on", "off"], shapes: { on: "●", off: "○" }, cost: "scan", binding: "extra.state", state_binding: "extra.state" }),
|
|
34710
|
+
slRow({ id: "extra-profile", group: "E", summary: "The extra profile a wave would use.", renderers: ["bare", "plain", "badge", "pill"], defaults: { render: "bare" }, unknown: "hide", cost: "new-read", binding: "extra.profile" }),
|
|
34711
|
+
slRow({ id: "extra-provider", group: "E", summary: "The provider behind that profile.", renderers: ["bare", "badge", "pill"], defaults: { render: "badge" }, unknown: "hide", cost: "new-read", binding: "extra.provider" }),
|
|
34712
|
+
slRow({ id: "extra-spend", group: "E", label: "extra", summary: "What foreign dispatch has cost. A figure ORC did not price itself is never printed — it reads as an em dash.", renderers: ["plain", "label-value", "bare", "spark"], defaults: { render: "plain", prefix: "$", precision: 2 }, series: "extraspend", cost: "new-read", binding: "extra.spend" }),
|
|
34713
|
+
slRow({ id: "extra-tasks", group: "E", label: "sent", summary: "How many tasks went off Claude.", renderers: ["plain", "label-value", "bare"], defaults: { render: "plain" }, cost: "new-read", binding: "extra.tasks" }),
|
|
34714
|
+
slRow({ id: "extra-inflight", group: "E", summary: "Whether a foreign dispatch is open.", renderers: ["dot", "shape", "dots", "pulse"], defaults: { render: "shape" }, states: ["idle", "running"], shapes: { idle: "○", running: "●" }, cost: "new-read", binding: "extra.inflight", state_binding: "extra.inflight" }),
|
|
34715
|
+
slRow({ id: "extra-demoted", group: "E", label: "demoted", summary: "Whether a provider was demoted this run. A lane that quietly stops is the mirror of one that quietly sends work away.", renderers: ["word", "shape", "badge"], defaults: { render: "shape" }, states: ["none", "demoted"], shapes: { none: "·", demoted: "▼" }, unknown: "hide", cost: "new-read", binding: "extra.demoted", state_binding: "extra.demoted" }),
|
|
34716
|
+
slRow({ id: "extra-passphrase", group: "E", label: "key", summary: "The vault deadline. It is a DEADLINE, not a second factor.", renderers: ["word", "shape", "badge", "traffic"], defaults: { render: "shape" }, states: ["active", "expiring", "expired", "absent"], shapes: { active: "●", expiring: "◐", expired: "○", absent: "·" }, cost: "new-read", binding: "extra.passphrase", state_binding: "extra.passphrase" }),
|
|
34717
|
+
slRow({ id: "extra-orphans", group: "E", label: "orphans", summary: "Dispatches nothing observed ending.", renderers: ["plain", "label-value", "bare"], defaults: { render: "plain" }, cost: "new-read", binding: "extra.unended" }),
|
|
34718
|
+
slRow({ id: "extra-reliability", group: "E", label: "rel", summary: "Per-profile reliability. NEVER a rate below 10 dispatches — it reads as an em dash instead.", renderers: ["plain", "bar", "dots", "bare", "blocks"], defaults: { render: "plain", format: "percent", min_width: 3, align: "right" }, bounded: true, cost: "new-read", binding: "extra.reliability" }),
|
|
34719
|
+
|
|
34720
|
+
// ── Group F — Flow and lanes ────────────────────────────────────────────
|
|
34721
|
+
slRow({ id: "diy", group: "F", label: "diy", summary: "Your custom flow's compile state.", renderers: ["word", "plain", "label-value", "badge", "shape"], defaults: { render: "label-value" }, states: ["ready", "stale", "unconfigured"], shapes: { ready: "●", stale: "◐", unconfigured: "○" }, unknown: "hide", cost: "scan", binding: "diy.state", state_binding: "diy.state" }),
|
|
34722
|
+
slRow({ id: "diy-tier", group: "F", summary: "Whether the session tier matches the compiled flow's.", renderers: ["bare", "plain", "shape", "word"], defaults: { render: "shape" }, states: ["match", "mismatch"], shapes: { match: "·", mismatch: "▲" }, unknown: "hide", cost: "scan", binding: "diy.tier_state", state_binding: "diy.tier_state" }),
|
|
34723
|
+
slRow({ id: "diy-step", group: "F", summary: "Where the compiled flow is in its own step list.", renderers: ["dots", "traffic", "marker", "bare", "plain"], defaults: { render: "dots" }, bounded: true, unknown: "hide", cost: "scan", binding: "diy.step" }),
|
|
34724
|
+
slRow({ id: "wait", group: "F", label: "wait", summary: "Whether a wait is running or blocked. Nothing runs during a wait.", renderers: ["word", "shape", "icon", "badge", "pulse"], defaults: { render: "shape" }, states: ["none", "waiting", "blocked"], shapes: { none: "·", waiting: "◐", blocked: "▲" }, unknown: "hide", cost: "new-read", binding: "wait.state", state_binding: "wait.state" }),
|
|
34725
|
+
slRow({ id: "preset-name", group: "F", summary: "Which status-line preset this layout came from.", renderers: ["bare", "badge", "pill"], defaults: { render: "badge" }, unknown: "hide", cost: "new-read", binding: "preset.name" }),
|
|
34726
|
+
|
|
34727
|
+
// ── Group C remainder — the run's own progress, from RESUME.md's one
|
|
34728
|
+
// byte-stable line. Reading that line is how a listing never has to open
|
|
34729
|
+
// a checkpoint.
|
|
34730
|
+
slRow({ id: "wave", group: "C", label: "wave", summary: "Which wave the run is in.", renderers: ["plain", "label-value", "bare", "bar", "blocks", "dots", "fraction", "fine"], defaults: { render: "plain" }, bounded: true, unknown: "hide", cost: "new-read", binding: "run.wave" }),
|
|
34731
|
+
slRow({ id: "task-progress", group: "C", summary: "Waves done out of waves planned.", renderers: ["bar", "blocks", "dots", "fraction", "fine", "plain"], defaults: { render: "dots" }, bounded: true, unknown: "hide", cost: "new-read", binding: "run.wave_total" }),
|
|
34732
|
+
slRow({ id: "resume", group: "C", label: "resume", summary: "Whether a run is waiting to be picked up. RESUME.md existing IS the flag.", renderers: ["word", "shape", "icon", "badge"], defaults: { render: "shape" }, states: ["none", "waiting"], shapes: { none: "·", waiting: "◐" }, unknown: "hide", cost: "new-read", binding: "run.resume", state_binding: "run.resume" }),
|
|
34733
|
+
slRow({ id: "open-runs", group: "C", label: "runs", summary: "How many runs are open. A CLOSED run is never counted.", renderers: ["plain", "label-value", "bare"], defaults: { render: "plain" }, cost: "new-read", binding: "run.open" }),
|
|
34734
|
+
slRow({ id: "pause-next", group: "C", label: "pause", summary: "How many waves until the next batch pause.", renderers: ["plain", "label-value", "fraction", "bare"], defaults: { render: "plain" }, unknown: "hide", cost: "new-read", binding: "run.wave" }),
|
|
34735
|
+
slRow({ id: "phase-step", group: "C", summary: "Where the run is in its own phase list.", renderers: ["bare", "traffic", "dots", "marker", "plain"], defaults: { render: "dots" }, bounded: true, unknown: "hide", cost: "new-read", binding: "run.wave" }),
|
|
34736
|
+
|
|
34737
|
+
// ── Group G — Health and gates. One small ledger each, on the slow clock:
|
|
34738
|
+
// a pact does not drift between keystrokes.
|
|
34739
|
+
slRow({ id: "update", group: "G", summary: "Whether a newer ORC exists. Cache-only — never a network call on this surface.", renderers: ["word", "badge", "icon", "shape"], defaults: { render: "badge" }, states: ["current", "available"], shapes: { current: "·", available: "▲" }, unknown: "hide", cost: "scan", binding: "update.version", state_binding: "update.state" }),
|
|
34740
|
+
slRow({ id: "pact", group: "G", label: "pact", summary: "Promises in doubt. UNCHECKABLE is the honest state and never reads as a failure.", renderers: ["plain", "label-value", "shape", "dots", "traffic", "word"], defaults: { render: "plain" }, states: ["holding", "drifted", "broken", "uncheckable"], shapes: { holding: "●", drifted: "◐", broken: "▲", uncheckable: "?" }, cost: "new-read", binding: "pact.drifted", state_binding: "pact.state" }),
|
|
34741
|
+
slRow({ id: "boundary", group: "G", label: "boundary", summary: "Refused areas. An area with no card is UNKNOWN, never assumed safe.", renderers: ["plain", "label-value", "shape", "word"], defaults: { render: "plain" }, states: ["clear", "refused", "unknown"], shapes: { clear: "·", refused: "▲", unknown: "?" }, cost: "new-read", binding: "boundary.refused", state_binding: "boundary.state" }),
|
|
34742
|
+
slRow({ id: "challenge", group: "G", label: "challenge", summary: "Open findings on the artefact being judged. PASS is computed, never declared.", renderers: ["plain", "label-value", "shape", "fraction", "word"], defaults: { render: "plain" }, states: ["pass", "open", "stalled"], shapes: { pass: "●", open: "◐", stalled: "○" }, unknown: "hide", cost: "new-read", binding: "challenge.open", state_binding: "challenge.state" }),
|
|
34743
|
+
slRow({ id: "doc", group: "G", label: "doc", summary: "The document's state. `shipped-drifted` names the sections that moved.", renderers: ["plain", "label-value", "badge", "shape", "word"], defaults: { render: "label-value" }, states: ["draft", "shipped", "shipped-drifted"], shapes: { draft: "◐", shipped: "●", "shipped-drifted": "▲" }, unknown: "hide", cost: "new-read", binding: "doc.state", state_binding: "doc.state" }),
|
|
34744
|
+
slRow({ id: "doc-progress", group: "G", summary: "Sections written out of sections planned.", renderers: ["bar", "blocks", "dots", "fraction", "fine", "plain"], defaults: { render: "fraction" }, bounded: true, unknown: "hide", cost: "new-read", binding: "doc.progress" }),
|
|
34745
|
+
slRow({ id: "usage-gate", group: "B", label: "usage", summary: "Whether a wave would be allowed to start. UNKNOWN IS NOT LOW, and it never stops a run.", renderers: ["word", "shape", "dot", "badge", "traffic"], defaults: { render: "shape" }, states: ["ok", "low", "unknown"], shapes: { ok: "●", low: "▲", unknown: "?" }, cost: "new-read", binding: "usage.state", state_binding: "usage.state" }),
|
|
34746
|
+
|
|
34747
|
+
// ── Group I remainder ───────────────────────────────────────────────────
|
|
34748
|
+
slRow({ id: "config", group: "I", summary: "Any ORC config key, as a component. It shows what is IN THE FILE — a hook has no lane, so it cannot resolve a key the way a lane would.", renderers: ["plain", "label-value", "bare", "badge"], defaults: { render: "label-value" }, cost: "new-read", params: { key: { free: true, dflt: "extra_enabled" } }, binding: "config.value" }),
|
|
34749
|
+
slRow({ id: "group", group: "I", summary: "Two to four parts drawn as ONE object — shared brackets, one background, one emphasis. It counts as ONE slot against the five, because the limit is about how much a line SAYS and a group says one thing.", renderers: ["bracket", "badge", "pill", "angle"], defaults: { render: "pill" }, unknown: "hide", children: [2, 4], binding: "static.text" }),
|
|
34750
|
+
slRow({ id: "icon-static", group: "I", summary: "One glyph you choose, from the shipped sets. Not an icon font — ORC ships none, because it cannot detect one.", renderers: ["bare"], defaults: { render: "bare" }, unknown: "hide", params: { glyph: { free: true, dflt: "●" } }, binding: "static.text" }),
|
|
34751
|
+
|
|
34752
|
+
// ── REFUSED, with the measurement recorded ──────────────────────────────
|
|
34753
|
+
slRow({ id: "doctor", group: "G", label: "doctor", summary: "How many findings `orc doctor` has.", renderers: ["plain", "label-value"], defaults: { render: "plain" }, cost: "refused", refused_reason: "`orc doctor` is a whole subprocess that walks the install, the payload, the manifest and the settings. W0 measured a single `git` subprocess at 53ms against roughly 15ms of headroom, and this is far larger than that. Run `orc doctor` when you want the answer; a status line is not the place for it." }),
|
|
34754
|
+
slRow({ id: "aftermath", group: "G", label: "churn", summary: "Whether recent runs held up.", renderers: ["plain", "bare"], defaults: { render: "plain" }, cost: "refused", refused_reason: "it needs `git log` over a date range — a subprocess whose cost grows with the repository. Churn is a signal that wants reading once, not three times a second: run `/orc-aftermath`." }),
|
|
34755
|
+
|
|
34756
|
+
// ── Group T — ONE SUBAGENT. The second board only (v1.4.0). ────────────
|
|
34757
|
+
// Every row here binds the TASK the row is about, so none of them means
|
|
34758
|
+
// anything on the main status line — `board: "subagent"` is how the
|
|
34759
|
+
// palette and the validator know.
|
|
34760
|
+
slRow({ id: "task-name", group: "T", board: "subagent", summary: "The agent's name.", renderers: ["bare", "plain", "badge", "pill", "label-value"], defaults: { render: "bare", truncate: "middle", max_len: 28 }, binding: "task.name" }),
|
|
34761
|
+
slRow({ id: "task-tier", group: "T", board: "subagent", summary: "The model and effort this agent is ACTUALLY running at — observed, not derived from its name and not self-reported. It is the only one of ORC's three readings nobody had to be trusted for.", renderers: ["bare", "plain", "label-value", "badge", "pill"], defaults: { render: "bare" }, binding: "task.tier" }),
|
|
34762
|
+
slRow({ id: "task-model", group: "T", board: "subagent", summary: "The model this agent resolved to.", renderers: ["bare", "plain", "badge", "pill"], defaults: { render: "bare" }, binding: "task.model" }),
|
|
34763
|
+
slRow({ id: "task-effort", group: "T", board: "subagent", summary: "The effort level this agent resolved to.", renderers: ["bare", "plain", "label-value"], defaults: { render: "bare" }, states: ["low", "medium", "high", "xhigh", "max"], binding: "task.effort", state_binding: "task.effort" }),
|
|
34764
|
+
slRow({ id: "task-status", group: "T", board: "subagent", summary: "What this agent is doing.", renderers: ["word", "shape", "badge", "bare"], defaults: { render: "word" }, states: ["pending", "running", "completed", "failed"], shapes: { pending: "○", running: "●", completed: "✓", failed: "▲" }, binding: "task.status", state_binding: "task.status" }),
|
|
34765
|
+
slRow({ id: "task-tokens", group: "T", board: "subagent", summary: "How many tokens this agent has used. THE NUMBER v1.2.0 SAID COULD NOT BE MEASURED — it cannot, from the transcript; Claude Code hands it to the agent panel live.", renderers: ["plain", "label-value", "bare"], defaults: { render: "plain", compact: "si" }, binding: "task.tokens" }),
|
|
34766
|
+
slRow({ id: "task-context", group: "T", board: "subagent", summary: "How full this agent's own context window is.", renderers: SL_PROP.concat(["plain", "label-value", "bare"]), defaults: { render: "fine", width: 8, format: "percent", min_width: 3, align: "right", ramp: "heat" }, states: ["ok", "warn", "full"], shapes: { ok: "●", warn: "◐", full: "○" }, bounded: true, binding: "task.ctx_pct", state_binding: "task.ctx_state" }),
|
|
34767
|
+
slRow({ id: "task-window", group: "T", board: "subagent", summary: "The size of this agent's context window.", renderers: ["plain", "label-value", "bare"], defaults: { render: "plain", compact: "si" }, unknown: "hide", binding: "task.ctx_size" }),
|
|
34768
|
+
slRow({ id: "task-elapsed", group: "T", board: "subagent", label: "for", summary: "How long this agent has been running.", renderers: ["plain", "label-value", "bare"], defaults: { render: "plain", min_width: 3, align: "right", suffix: "m" }, binding: "task.elapsed_min", time_based: true }),
|
|
34769
|
+
slRow({ id: "task-what", group: "T", board: "subagent", summary: "What this agent was asked to do.", renderers: ["bare", "plain"], defaults: { render: "bare", truncate: "end", max_len: 40 }, unknown: "hide", binding: "task.description" }),
|
|
34770
|
+
|
|
34771
|
+
// ── Group J — Composite shortcuts. One slot, several facts. ─────────────
|
|
34772
|
+
slRow({ id: "tier-block", group: "J", summary: "Verdict + version + model/effort as one object.", renderers: ["plain", "badge", "pill"], defaults: { render: "pill" }, states: ["ready", "boosted", "degrade"], binding: "tier.text", state_binding: "verdict.state", composite: ["verdict", "orc-version", "tier"] }),
|
|
34773
|
+
slRow({ id: "quota-block", group: "J", label: "quota", summary: "Both windows as one object.", renderers: ["plain", "badge", "pill"], defaults: { render: "plain" }, states: ["ok", "warn", "critical"], binding: "quota.worst.pct", state_binding: "quota.worst.state", composite: ["quota-5h", "quota-week"] }),
|
|
34774
|
+
slRow({ id: "run-block", group: "J", summary: "Motif + status + agents as one object.", renderers: ["plain", "badge", "pill"], defaults: { render: "plain" }, unknown: "hide", cost: "scan", binding: "run.status", state_binding: "run.phase_kind", composite: ["phase-motif", "status", "agents"] }),
|
|
34775
|
+
];
|
|
34776
|
+
|
|
34777
|
+
const SL_BY_ID = new Map(STATUSLINE_COMPONENTS.map((c) => [c.id, c]));
|
|
34778
|
+
|
|
34779
|
+
// ── The layout file: ONE writer ─────────────────────────────────────────────
|
|
34780
|
+
// `.claude/orc/statusline-layout.json` is AUTHORED — sparse, human-diffable,
|
|
34781
|
+
// what the user meant. `statusline-compiled.json` is DERIVED — flat, total, what
|
|
34782
|
+
// the machine runs. THE HOOK NEVER READS THE AUTHORED FILE. Not as a fallback,
|
|
34783
|
+
// not on a cache miss, not ever: one consumer per file, the rule `doc.json` and
|
|
34784
|
+
// `wiki-meta.json` already follow.
|
|
34785
|
+
// TWO BOARDS, ONE COMPILER (v1.4.0). `status` is the main line; `subagent` is
|
|
34786
|
+
// the per-agent row Claude Code renders in the agent panel. They share the
|
|
34787
|
+
// compiler, the IR, every renderer, the glyph sets, the colour model and the
|
|
34788
|
+
// validator — what differs is the component set, the file names and the config
|
|
34789
|
+
// key. A second compiler is exactly what this table exists to prevent.
|
|
34790
|
+
const SL_BOARDS = {
|
|
34791
|
+
status: {
|
|
34792
|
+
id: "status",
|
|
34793
|
+
lines: 3,
|
|
34794
|
+
key: "statusline_custom",
|
|
34795
|
+
files: ["statusline-layout.json", "statusline-compiled.json", "statusline.lock.json"],
|
|
34796
|
+
setting: "statusLine",
|
|
34797
|
+
},
|
|
34798
|
+
subagent: {
|
|
34799
|
+
id: "subagent",
|
|
34800
|
+
// ONE line by construction: Claude Code renders one row per task, so the
|
|
34801
|
+
// three-line board and its dense-prefix rule simply do not apply here.
|
|
34802
|
+
lines: 1,
|
|
34803
|
+
key: "subagent_line_custom",
|
|
34804
|
+
files: ["subagent-layout.json", "subagent-compiled.json", "subagent.lock.json"],
|
|
34805
|
+
setting: "subagentStatusLine",
|
|
34806
|
+
},
|
|
34807
|
+
};
|
|
34808
|
+
|
|
34809
|
+
function slBoard() {
|
|
34810
|
+
const b = String(flag("--board") || "status");
|
|
34811
|
+
return SL_BOARDS[b] || SL_BOARDS.status;
|
|
34812
|
+
}
|
|
34813
|
+
|
|
34814
|
+
function slPaths(claudeDir, board) {
|
|
34815
|
+
const b = board || slBoard();
|
|
34816
|
+
const dir = path.join(claudeDir, "orc");
|
|
34817
|
+
return {
|
|
34818
|
+
dir,
|
|
34819
|
+
board: b,
|
|
34820
|
+
layout: path.join(dir, b.files[0]),
|
|
34821
|
+
compiled: path.join(dir, b.files[1]),
|
|
34822
|
+
lock: path.join(dir, b.files[2]),
|
|
34823
|
+
};
|
|
34824
|
+
}
|
|
34825
|
+
|
|
34826
|
+
function slBlankLayout(board) {
|
|
34827
|
+
const b = board || slBoard();
|
|
34828
|
+
const lines = [];
|
|
34829
|
+
for (let i = 0; i < b.lines; i++)
|
|
34830
|
+
lines.push({ separator: " · ", max_width: 0, theme: null, prefix: i ? " " : "", items: [] });
|
|
34831
|
+
return {
|
|
34832
|
+
schema: SL_SCHEMA,
|
|
34833
|
+
board: b.id,
|
|
34834
|
+
orc_version: currentVersion(),
|
|
34835
|
+
preset: null,
|
|
34836
|
+
ansi: "auto",
|
|
34837
|
+
theme: "terminal",
|
|
34838
|
+
glyphs: "blocks",
|
|
34839
|
+
align_columns: false,
|
|
34840
|
+
lines,
|
|
34841
|
+
};
|
|
34842
|
+
}
|
|
34843
|
+
|
|
34844
|
+
function slReadLayout(claudeDir, board) {
|
|
34845
|
+
const p = slPaths(claudeDir, board).layout;
|
|
34846
|
+
try {
|
|
34847
|
+
const j = JSON.parse(fs.readFileSync(p, "utf8").replace(/^/, ""));
|
|
34848
|
+
if (!j || j.schema !== SL_SCHEMA) return null;
|
|
34849
|
+
// A layout with fewer than three lines is a layout from a smaller board.
|
|
34850
|
+
// Pad rather than refuse — the dense-prefix rule only cares about what is
|
|
34851
|
+
// FILLED, and an absent line 3 and an empty line 3 are the same fact.
|
|
34852
|
+
const want = (SL_BOARDS[j.board] || slBoard()).lines;
|
|
34853
|
+
while (j.lines.length < want) j.lines.push({ separator: " · ", max_width: 0, theme: null, prefix: " ", items: [] });
|
|
34854
|
+
j.lines.length = Math.min(j.lines.length, want);
|
|
34855
|
+
return j;
|
|
34856
|
+
} catch (_) {
|
|
34857
|
+
return null;
|
|
34858
|
+
}
|
|
34859
|
+
}
|
|
34860
|
+
|
|
34861
|
+
function slWriteLayout(claudeDir, layout) {
|
|
34862
|
+
const p = slPaths(claudeDir, SL_BOARDS[layout.board] || SL_BOARDS.status);
|
|
34863
|
+
fs.mkdirSync(p.dir, { recursive: true });
|
|
34864
|
+
layout.orc_version = currentVersion();
|
|
34865
|
+
fs.writeFileSync(p.layout, JSON.stringify(layout, null, 2) + "\n");
|
|
34866
|
+
}
|
|
34867
|
+
|
|
34868
|
+
let SL_NEXT_ID = 0;
|
|
34869
|
+
function slNewId(layout) {
|
|
34870
|
+
// A stable instance id: a reorder must never rename an item, because the
|
|
34871
|
+
// responsive drop plan and every doctor finding name items by id.
|
|
34872
|
+
const used = new Set();
|
|
34873
|
+
for (const l of layout.lines) for (const it of l.items) used.add(it.id);
|
|
34874
|
+
let n = 1;
|
|
34875
|
+
while (used.has("i" + n)) n++;
|
|
34876
|
+
SL_NEXT_ID = n;
|
|
34877
|
+
return "i" + n;
|
|
34878
|
+
}
|
|
34879
|
+
|
|
34880
|
+
// ── S2 RESOLVE — the five-link chain, with provenance ───────────────────────
|
|
34881
|
+
// catalogue default → theme → file defaults → line defaults → item. LATER WINS.
|
|
34882
|
+
//
|
|
34883
|
+
// A NULL IS NOT AN ABSENCE. `color: null` means INHERIT; `color: "none"` means
|
|
34884
|
+
// explicitly no colour, stop inheriting. Conflating them makes it impossible to
|
|
34885
|
+
// turn off something a theme turned on — the same `absent` vs `not-read`
|
|
34886
|
+
// distinction the v1.0.0 config resolver already draws.
|
|
34887
|
+
function slResolveItem(item, layout, line) {
|
|
34888
|
+
const comp = SL_BY_ID.get(item.type);
|
|
34889
|
+
if (!comp) return null;
|
|
34890
|
+
const from = {};
|
|
34891
|
+
const out = {};
|
|
34892
|
+
const put = (k, v, src) => {
|
|
34893
|
+
if (v === undefined || v === null) return;
|
|
34894
|
+
out[k] = v;
|
|
34895
|
+
from[k] = src;
|
|
34896
|
+
};
|
|
34897
|
+
|
|
34898
|
+
// 1 — the catalogue's own defaults: the starting design.
|
|
34899
|
+
for (const [k, v] of Object.entries(comp.defaults || {})) put(k, v, "catalogue");
|
|
34900
|
+
put("label", comp.label, "catalogue");
|
|
34901
|
+
put("render", (comp.defaults && comp.defaults.render) || comp.renderers[0], "catalogue");
|
|
34902
|
+
put("unknown", comp.unknown, "catalogue");
|
|
34903
|
+
|
|
34904
|
+
// 2 — the theme. It assigns NAMED SLOTS, never hex, so it follows the user's
|
|
34905
|
+
// own terminal.
|
|
34906
|
+
const themeName = line.theme || layout.theme || "terminal";
|
|
34907
|
+
const theme = STATUSLINE_THEMES[themeName] || STATUSLINE_THEMES.terminal;
|
|
34908
|
+
put("label_color", theme.label, "theme:" + themeName);
|
|
34909
|
+
put("value_color", theme.value, "theme:" + themeName);
|
|
34910
|
+
|
|
34911
|
+
// 3 — file defaults.
|
|
34912
|
+
put("glyphs", layout.glyphs, "file");
|
|
34913
|
+
if (layout.emphasis) put("emphasis", layout.emphasis, "file");
|
|
34914
|
+
if (layout.case) put("case", layout.case, "file");
|
|
34915
|
+
|
|
34916
|
+
// 4 — line defaults.
|
|
34917
|
+
if (line.glyphs) put("glyphs", line.glyphs, "line");
|
|
34918
|
+
if (line.emphasis) put("emphasis", line.emphasis, "line");
|
|
34919
|
+
|
|
34920
|
+
// 5 — the item. What the user set on this chip.
|
|
34921
|
+
for (const [k, v] of Object.entries(item)) {
|
|
34922
|
+
if (k === "id" || k === "type") continue;
|
|
34923
|
+
if (v === null || v === undefined) continue; // null = inherit
|
|
34924
|
+
put(k, v, "item");
|
|
34925
|
+
}
|
|
34926
|
+
// "none" is the explicit stop: it survives resolution as an absence of colour
|
|
34927
|
+
// rather than as an absence of a setting.
|
|
34928
|
+
for (const k of ["color", "label_color", "value_color", "bg"]) {
|
|
34929
|
+
if (out[k] === "none") out[k] = null;
|
|
34930
|
+
}
|
|
34931
|
+
// `color` is a SHORTHAND that sets both, and it only wins where the user did
|
|
34932
|
+
// not set the specific one.
|
|
34933
|
+
if (item.color && item.color !== "auto" && item.color !== "none") {
|
|
34934
|
+
if (item.label_color == null) put("label_color", item.color, "item:color");
|
|
34935
|
+
if (item.value_color == null) put("value_color", item.color, "item:color");
|
|
34936
|
+
}
|
|
34937
|
+
|
|
34938
|
+
out.id = item.id;
|
|
34939
|
+
out.type = item.type;
|
|
34940
|
+
out._comp = comp;
|
|
34941
|
+
out._from = from;
|
|
34942
|
+
return out;
|
|
34943
|
+
}
|
|
34944
|
+
|
|
34945
|
+
// ── S3 VALIDATE — three tiers, ALL errors collected ────────────────────────
|
|
34946
|
+
// A validator that stops at the first problem makes a user fix a five-error
|
|
34947
|
+
// layout five times.
|
|
34948
|
+
//
|
|
34949
|
+
// THE DENSE-PREFIX INVARIANT, stated once so the CLI, the panel and the tests
|
|
34950
|
+
// all cite the same sentence:
|
|
34951
|
+
//
|
|
34952
|
+
// A line may hold a component only if every line above it holds at least one.
|
|
34953
|
+
function slValidate(layout) {
|
|
34954
|
+
const errors = [];
|
|
34955
|
+
const warnings = [];
|
|
34956
|
+
const E = (msg) => errors.push(msg);
|
|
34957
|
+
const W = (msg) => warnings.push(msg);
|
|
34958
|
+
|
|
34959
|
+
if (!layout || !Array.isArray(layout.lines)) {
|
|
34960
|
+
return { ok: false, errors: ["the layout has no lines[] — it is not a layout"], warnings: [] };
|
|
34961
|
+
}
|
|
34962
|
+
const board = SL_BOARDS[layout.board] || SL_BOARDS.status;
|
|
34963
|
+
if (layout.lines.length > board.lines)
|
|
34964
|
+
E(`this board holds ${board.lines} line${board.lines === 1 ? "" : "s"}; this layout has ${layout.lines.length}`);
|
|
34965
|
+
|
|
34966
|
+
// ── Tier 1 — structural (the board rules). These are REFUSALS. ──────────
|
|
34967
|
+
const counts = layout.lines.map((l) => (l.items || []).filter((it) => !slIsStructural(it.type)).length);
|
|
34968
|
+
const rawCounts = layout.lines.map((l) => (l.items || []).length);
|
|
34969
|
+
if (counts[0] === 0 && rawCounts[0] === 0)
|
|
34970
|
+
E("line 1 is empty — the dense-prefix rule requires at least one component on line 1");
|
|
34971
|
+
for (let i = 1; i < layout.lines.length; i++) {
|
|
34972
|
+
if (rawCounts[i] > 0 && rawCounts[i - 1] === 0)
|
|
34973
|
+
E(`line ${i + 1} holds ${rawCounts[i]} component${rawCounts[i] === 1 ? "" : "s"} but line ${i} is empty — fill line ${i} first`);
|
|
34974
|
+
}
|
|
34975
|
+
layout.lines.forEach((l, i) => {
|
|
34976
|
+
if (counts[i] > 5) E(`line ${i + 1} holds ${counts[i]} components (max 5) — remove one`);
|
|
34977
|
+
});
|
|
34978
|
+
|
|
34979
|
+
const seenIds = new Set();
|
|
34980
|
+
layout.lines.forEach((line, li) => {
|
|
34981
|
+
(line.items || []).forEach((item, pi) => {
|
|
34982
|
+
const where = `line ${li + 1} position ${pi + 1}`;
|
|
34983
|
+
if (!item.id) E(`${where}: the item has no instance id`);
|
|
34984
|
+
else if (seenIds.has(item.id)) E(`duplicate instance id "${item.id}" (${where})`);
|
|
34985
|
+
else seenIds.add(item.id);
|
|
34986
|
+
|
|
34987
|
+
const comp = SL_BY_ID.get(item.type);
|
|
34988
|
+
if (!comp) {
|
|
34989
|
+
E(`unknown component "${item.type}" on ${where}${slDidYouMean(item.type)}`);
|
|
34990
|
+
return;
|
|
34991
|
+
}
|
|
34992
|
+
// A component that binds ONE TASK means nothing on the main line, and a
|
|
34993
|
+
// session-wide component means nothing on a per-agent row. Refused by
|
|
34994
|
+
// name, with the board it belongs to — the two catalogues are one
|
|
34995
|
+
// catalogue with a column, not two lists somebody has to keep in step.
|
|
34996
|
+
if (comp.board !== board.id)
|
|
34997
|
+
E(`"${comp.id}" belongs to the ${comp.board === "subagent" ? "subagent" : "status"} board — this is the ${board.id} board`);
|
|
34998
|
+
const r = item.render || (comp.defaults && comp.defaults.render) || comp.renderers[0];
|
|
34999
|
+
if (!comp.renderers.includes(r))
|
|
35000
|
+
E(`component "${comp.id}" does not offer renderer "${r}" — it offers: ${comp.renderers.join(", ")}`);
|
|
35001
|
+
|
|
35002
|
+
// GROUPS DO NOT NEST. One level is a visual object; two is a layout
|
|
35003
|
+
// engine, and a status line is not the place for one.
|
|
35004
|
+
if (comp.children) {
|
|
35005
|
+
const kids = item.children || [];
|
|
35006
|
+
const [lo, hi] = comp.children;
|
|
35007
|
+
if (kids.length < lo || kids.length > hi)
|
|
35008
|
+
E(`group "${item.id}" holds ${kids.length} components — a group holds ${lo} to ${hi}`);
|
|
35009
|
+
for (const k of kids) {
|
|
35010
|
+
const kc = SL_BY_ID.get(k.type);
|
|
35011
|
+
if (!kc) {
|
|
35012
|
+
E(`unknown component "${k.type}" inside group "${item.id}"${slDidYouMean(k.type)}`);
|
|
35013
|
+
continue;
|
|
35014
|
+
}
|
|
35015
|
+
if (kc.children) E(`group "${item.id}" contains group "${k.id}" — groups do not nest`);
|
|
35016
|
+
if (kc.cost === "refused") E(`"${kc.id}" is refused: ${kc.refused_reason}`);
|
|
35017
|
+
if (k.id) {
|
|
35018
|
+
if (seenIds.has(k.id)) E(`duplicate instance id "${k.id}" (inside group "${item.id}")`);
|
|
35019
|
+
else seenIds.add(k.id);
|
|
35020
|
+
}
|
|
35021
|
+
}
|
|
35022
|
+
} else if (item.children && item.children.length) {
|
|
35023
|
+
E(`"${comp.id}" does not hold other components — only a group does`);
|
|
35024
|
+
}
|
|
35025
|
+
|
|
35026
|
+
// ── Tier 2 — semantic. Does this component support this? ───────────
|
|
35027
|
+
if (comp.cost === "refused")
|
|
35028
|
+
E(`"${comp.id}" is refused: ${comp.refused_reason}`);
|
|
35029
|
+
|
|
35030
|
+
const rend = STATUSLINE_RENDERERS[r];
|
|
35031
|
+
if (rend) {
|
|
35032
|
+
// ORDINAL — one of N positions lit. Meaningful over a bounded number
|
|
35033
|
+
// OR over a closed state set, and a lie over neither.
|
|
35034
|
+
if (rend.needs === "ordinal" && !comp.bounded && !comp.states)
|
|
35035
|
+
E(`"${r}" on "${comp.id}" — ${comp.id} has neither a maximum nor a closed state set, so there is nothing for a position to mean.`);
|
|
35036
|
+
if (rend.needs === "bounded" && !comp.bounded)
|
|
35037
|
+
E(`"${r}" on "${comp.id}" — ${comp.id} has no maximum, so a bar has no meaning. Use "spark" or "plain".`);
|
|
35038
|
+
if (rend.needs === "states" && !comp.states)
|
|
35039
|
+
E(`"${r}" on "${comp.id}" — ${comp.id} has no states, so there is nothing for a shape to say.`);
|
|
35040
|
+
if (rend.needs === "series" && !comp.series)
|
|
35041
|
+
E(`"${r}" on "${comp.id}" — no history is kept for ${comp.id}. Series are kept for: ${slSeriesComponents().join(", ")}.`);
|
|
35042
|
+
if (rend.needs === "motif" && !comp.state_binding)
|
|
35043
|
+
E(`"${r}" on "${comp.id}" — motion is only for a component with a phase kind.`);
|
|
35044
|
+
if (rend.width && item.width != null) {
|
|
35045
|
+
const [lo, hi] = rend.width;
|
|
35046
|
+
if (item.width < lo || item.width > hi)
|
|
35047
|
+
E(`width ${item.width} on "${r}" — ${r} takes ${lo === hi ? `exactly ${lo} cell` : `${lo}..${hi} cells`}`);
|
|
35048
|
+
}
|
|
35049
|
+
}
|
|
35050
|
+
if (item.ramp && item.ramp !== "state" && !comp.bounded)
|
|
35051
|
+
E(`ramp "${item.ramp}" on "${comp.id}" — ${comp.id} has no numeric value; ramps need one`);
|
|
35052
|
+
if (item.ramp && !STATUSLINE_RAMPS[item.ramp]) E(`unknown ramp "${item.ramp}" on ${where}`);
|
|
35053
|
+
if (item.glyphs && !STATUSLINE_GLYPHSETS[item.glyphs]) E(`unknown glyph set "${item.glyphs}" on ${where}`);
|
|
35054
|
+
if (item.color_by_state) {
|
|
35055
|
+
for (const k of Object.keys(item.color_by_state)) {
|
|
35056
|
+
if (!comp.states || !comp.states.includes(k))
|
|
35057
|
+
E(`color_by_state key "${k}" — "${comp.id}" has states: ${(comp.states || []).join(", ") || "none"}`);
|
|
35058
|
+
}
|
|
35059
|
+
}
|
|
35060
|
+
for (const em of item.emphasis || []) {
|
|
35061
|
+
if (SL_REFUSED_EMPHASIS.includes(em))
|
|
35062
|
+
E(`emphasis "${em}" is refused — it is hostile, half of terminals disable it, and no state on a status line deserves it`);
|
|
35063
|
+
else if (!(em in SL_EMPHASIS)) E(`unknown emphasis "${em}" on ${where}`);
|
|
35064
|
+
}
|
|
35065
|
+
for (const h of item.hide_when || []) {
|
|
35066
|
+
if (!STATUSLINE_HIDE_WHEN.some((x) => x.id === h)) E(`unknown hide condition "${h}" on ${where}`);
|
|
35067
|
+
}
|
|
35068
|
+
if (item.format && !SL_FORMATS.includes(item.format)) E(`unknown format "${item.format}" on ${where}`);
|
|
35069
|
+
if (item.compact && !SL_COMPACT.includes(item.compact)) E(`unknown compact mode "${item.compact}" on ${where}`);
|
|
35070
|
+
if (item.case && !SL_CASES.includes(item.case)) E(`unknown case "${item.case}" on ${where}`);
|
|
35071
|
+
if (item.truncate && !SL_TRUNCATE.includes(item.truncate)) E(`unknown truncate mode "${item.truncate}" on ${where}`);
|
|
35072
|
+
for (const k of ["label_color", "value_color", "bg", "color"]) {
|
|
35073
|
+
const v = item[k];
|
|
35074
|
+
if (v == null || v === "auto" || v === "none") continue;
|
|
35075
|
+
if (!slParseColor(v)) E(`"${v}" is not a colour on ${where} — use a named slot (${Object.keys(SL_ANSI_SLOTS).slice(0, 4).join(", ")}, …), a #rrggbb hex, or ramp:<name>`);
|
|
35076
|
+
}
|
|
35077
|
+
if (comp.params) {
|
|
35078
|
+
const given = item.params || {};
|
|
35079
|
+
for (const k of Object.keys(given)) {
|
|
35080
|
+
if (!comp.params[k]) E(`"${comp.id}" takes no parameter "${k}" — it takes: ${Object.keys(comp.params).join(", ")}`);
|
|
35081
|
+
}
|
|
35082
|
+
}
|
|
35083
|
+
|
|
35084
|
+
// ── Tier 3 — design. The rules a human gets wrong and a machine can
|
|
35085
|
+
// check. WARNINGS NEVER BLOCK: an error is a refusal, a warning is a
|
|
35086
|
+
// fact the user then owns.
|
|
35087
|
+
// R3 — colour is a signal or it is decoration; it cannot be both.
|
|
35088
|
+
if (comp.states && item.color && item.color !== "auto")
|
|
35089
|
+
W(`"${comp.id}" carries meaning in its colour (${comp.states.join("/")}) and you set a flat colour — a green ⛔ is a status line that lies`);
|
|
35090
|
+
// R4 — a bare `dot` is refused: its only distinction is hue, so under
|
|
35091
|
+
// NO_COLOR two states render identically.
|
|
35092
|
+
if (r === "dot" && comp.states && comp.states.length > 1)
|
|
35093
|
+
E(`"dot" on "${comp.id}" draws ONE glyph for ${comp.states.length} states (${comp.states.join(", ")}), so they are indistinguishable under NO_COLOR — use "shape", which is the honest version of it, or "word"`);
|
|
35094
|
+
// R2 — a number that changes width makes the line dance.
|
|
35095
|
+
const isLast = pi === (line.items || []).length - 1;
|
|
35096
|
+
if (comp.bounded && item.min_width === 0 && !isLast)
|
|
35097
|
+
W(`"${comp.id}" on ${where} has min_width 0 with something to its right — every value change will shift the rest of the line`);
|
|
35098
|
+
});
|
|
35099
|
+
});
|
|
35100
|
+
|
|
35101
|
+
// Cross-item design warnings.
|
|
35102
|
+
layout.lines.forEach((line, li) => {
|
|
35103
|
+
const bindings = new Map();
|
|
35104
|
+
for (const it of line.items || []) {
|
|
35105
|
+
const c = SL_BY_ID.get(it.type);
|
|
35106
|
+
if (!c || !c.binding) continue;
|
|
35107
|
+
bindings.set(c.binding, (bindings.get(c.binding) || 0) + 1);
|
|
35108
|
+
}
|
|
35109
|
+
for (const [b, n] of bindings) {
|
|
35110
|
+
if (n > 1) {
|
|
35111
|
+
const names = (line.items || []).filter((it) => (SL_BY_ID.get(it.type) || {}).binding === b).map((it) => it.type);
|
|
35112
|
+
W(`line ${li + 1} shows the same value twice (${names.join(", ")})`);
|
|
35113
|
+
}
|
|
35114
|
+
}
|
|
35115
|
+
const items = line.items || [];
|
|
35116
|
+
if (items.length && items.every((it) => (it.hide_when || []).length && !it.draw_empty))
|
|
35117
|
+
W(`line ${li + 1} can render empty — every component on it can hide, so the line will collapse to nothing`);
|
|
35118
|
+
});
|
|
35119
|
+
|
|
35120
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
35121
|
+
}
|
|
35122
|
+
|
|
35123
|
+
// spacer / divider / fill do not count against the 5-per-line limit: the limit
|
|
35124
|
+
// is about how much a line SAYS, and none of the three says anything.
|
|
35125
|
+
function slIsStructural(type) {
|
|
35126
|
+
return type === "spacer" || type === "divider" || type === "fill";
|
|
35127
|
+
}
|
|
35128
|
+
|
|
35129
|
+
function slSeriesComponents() {
|
|
35130
|
+
return STATUSLINE_COMPONENTS.filter((c) => c.series).map((c) => c.id);
|
|
35131
|
+
}
|
|
35132
|
+
|
|
35133
|
+
function slDidYouMean(id) {
|
|
35134
|
+
const want = String(id || "").toLowerCase();
|
|
35135
|
+
let best = null;
|
|
35136
|
+
let bestScore = 0;
|
|
35137
|
+
for (const c of STATUSLINE_COMPONENTS) {
|
|
35138
|
+
const a = c.id;
|
|
35139
|
+
let hits = 0;
|
|
35140
|
+
for (const ch of new Set(want)) if (a.includes(ch)) hits++;
|
|
35141
|
+
const score = hits / Math.max(a.length, want.length, 1);
|
|
35142
|
+
if (score > bestScore) {
|
|
35143
|
+
bestScore = score;
|
|
35144
|
+
best = a;
|
|
35145
|
+
}
|
|
35146
|
+
}
|
|
35147
|
+
return best && bestScore > 0.5 ? ` — did you mean "${best}"?` : "";
|
|
35148
|
+
}
|
|
35149
|
+
|
|
35150
|
+
function slParseColor(v) {
|
|
35151
|
+
const s = String(v);
|
|
35152
|
+
if (s in SL_ANSI_SLOTS) return { kind: "slot", slot: s };
|
|
35153
|
+
if (/^#[0-9a-fA-F]{6}$/.test(s)) return { kind: "hex", hex: s };
|
|
35154
|
+
if (/^ramp:/.test(s)) {
|
|
35155
|
+
const n = s.slice(5);
|
|
35156
|
+
return STATUSLINE_RAMPS[n] ? { kind: "ramp", ramp: n } : null;
|
|
35157
|
+
}
|
|
35158
|
+
return null;
|
|
35159
|
+
}
|
|
35160
|
+
|
|
35161
|
+
// Every SGR string is precomputed HERE, at compile time. The hook never converts
|
|
35162
|
+
// a hex triple, never looks up a named slot and never interpolates a ramp for a
|
|
35163
|
+
// fixed colour.
|
|
35164
|
+
function slSgr(colorSpec, emphasis, bg) {
|
|
35165
|
+
const codes = [];
|
|
35166
|
+
for (const e of emphasis || []) if (SL_EMPHASIS[e]) codes.push(SL_EMPHASIS[e]);
|
|
35167
|
+
const c = colorSpec ? slParseColor(colorSpec) : null;
|
|
35168
|
+
if (c && c.kind === "slot") codes.push(SL_ANSI_SLOTS[c.slot]);
|
|
35169
|
+
else if (c && c.kind === "hex") {
|
|
35170
|
+
const h = c.hex.slice(1);
|
|
35171
|
+
codes.push(`38;2;${parseInt(h.slice(0, 2), 16)};${parseInt(h.slice(2, 4), 16)};${parseInt(h.slice(4, 6), 16)}`);
|
|
35172
|
+
}
|
|
35173
|
+
const b = bg ? slParseColor(bg) : null;
|
|
35174
|
+
if (b && b.kind === "slot") codes.push(SL_ANSI_SLOTS[b.slot] + 10);
|
|
35175
|
+
else if (b && b.kind === "hex") {
|
|
35176
|
+
const h = b.hex.slice(1);
|
|
35177
|
+
codes.push(`48;2;${parseInt(h.slice(0, 2), 16)};${parseInt(h.slice(2, 4), 16)};${parseInt(h.slice(4, 6), 16)}`);
|
|
35178
|
+
}
|
|
35179
|
+
if (!codes.length) return null;
|
|
35180
|
+
return SL_ESC + "[" + codes.join(";") + "m";
|
|
35181
|
+
}
|
|
35182
|
+
const SL_ESC = String.fromCharCode(27);
|
|
35183
|
+
const SL_RESET = SL_ESC + "[0m";
|
|
35184
|
+
|
|
35185
|
+
|
|
35186
|
+
// ── S4 LOWER + S5 MEASURE + S6 EMIT ────────────────────────────────────────
|
|
35187
|
+
// The compiled file is a flat op list per line. The hook has NO renderer logic
|
|
35188
|
+
// — it walks ops and concatenates strings.
|
|
35189
|
+
//
|
|
35190
|
+
// TABLES ARE INDEXED, NOT INLINED. `f`, `g`, `m`, `r` index into `formats[]`,
|
|
35191
|
+
// `glyphsets[]`, `statemaps[]`, `ramps[]` at the top of the file, so a layout
|
|
35192
|
+
// with eight bars carries ONE glyph set rather than eight copies — and the
|
|
35193
|
+
// compiled file stays diffable, which matters when a maintainer reads one.
|
|
35194
|
+
function slCompile(layout) {
|
|
35195
|
+
const formats = [];
|
|
35196
|
+
const glyphsets = [];
|
|
35197
|
+
const statemaps = [];
|
|
35198
|
+
const ramps = [];
|
|
35199
|
+
const bindings = new Set();
|
|
35200
|
+
const providers = new Set(["payload"]);
|
|
35201
|
+
const seriesWanted = new Set();
|
|
35202
|
+
let timeBased = false;
|
|
35203
|
+
|
|
35204
|
+
const idxOf = (arr, obj) => {
|
|
35205
|
+
const key = JSON.stringify(obj);
|
|
35206
|
+
for (let i = 0; i < arr.length; i++) if (JSON.stringify(arr[i]) === key) return i;
|
|
35207
|
+
arr.push(obj);
|
|
35208
|
+
return arr.length - 1;
|
|
35209
|
+
};
|
|
35210
|
+
|
|
35211
|
+
// The PROVIDER is derived from the BINDING, not from the cost. `branch` and
|
|
35212
|
+
// `wiki` are both `scan`, but they are two different reads with two different
|
|
35213
|
+
// TTLs, and lumping them together means a layout with a branch on it pays for
|
|
35214
|
+
// a trace scan it never asked for — which is precisely the waste the read
|
|
35215
|
+
// planner exists to remove.
|
|
35216
|
+
const providerOf = (b) =>
|
|
35217
|
+
b.startsWith("git.") ? "scan.git"
|
|
35218
|
+
: b.startsWith("wiki.") ? "scan.wiki"
|
|
35219
|
+
: b.startsWith("diy.") ? "scan.diy"
|
|
35220
|
+
: b.startsWith("update.") ? "scan.update"
|
|
35221
|
+
: "scan.trace";
|
|
35222
|
+
// W3's groups all read files the shipped status line never opens, so they
|
|
35223
|
+
// share one gate — the extended scan — and each sub-read inside it carries
|
|
35224
|
+
// its own TTL. One gate, many clocks.
|
|
35225
|
+
const EXTENDED = /^(wiki\.(coverage|docs|worst|blindspot)|pattern\.|crosslink\.|gotchas\.|extra\.(profile|provider|spend|tasks|inflight|demoted|passphrase|orphans|reliability)|wait\.|preset\.|run\.(wave|wave_total|resume|open)|pact\.|boundary\.|challenge\.|doc\.|usage\.|config\.)/;
|
|
35226
|
+
const note = (comp, item) => {
|
|
35227
|
+
// A GROUP'S CHILDREN ARE READS TOO. Without this the read planner would
|
|
35228
|
+
// fetch nothing for a group and every child would render an em dash — the
|
|
35229
|
+
// exact class of silent failure the planner exists to avoid.
|
|
35230
|
+
if (comp.children && item && Array.isArray(item.children)) {
|
|
35231
|
+
for (const k of item.children) {
|
|
35232
|
+
const kc = SL_BY_ID.get(k.type);
|
|
35233
|
+
if (kc) note(kc, k);
|
|
35234
|
+
}
|
|
35235
|
+
}
|
|
35236
|
+
for (const b of [comp.binding, comp.state_binding]) {
|
|
35237
|
+
if (!b) continue;
|
|
35238
|
+
bindings.add(b);
|
|
35239
|
+
if (comp.cost === "scan") providers.add(providerOf(b));
|
|
35240
|
+
else if (comp.cost === "new-read") providers.add(EXTENDED.test(b) ? "scan.extended" : providerOf(b));
|
|
35241
|
+
}
|
|
35242
|
+
if (comp.series) seriesWanted.add(comp.series);
|
|
35243
|
+
if (comp.time_based) timeBased = true;
|
|
35244
|
+
};
|
|
35245
|
+
|
|
35246
|
+
const lines = [];
|
|
35247
|
+
const plans = [{ cols: 0, drop: [] }];
|
|
35248
|
+
const dropCandidates = [];
|
|
35249
|
+
|
|
35250
|
+
layout.lines.forEach((line, li) => {
|
|
35251
|
+
const ops = [];
|
|
35252
|
+
const items = (line.items || []).filter((it) => SL_BY_ID.has(it.type));
|
|
35253
|
+
const sep = line.separator == null ? " · " : line.separator;
|
|
35254
|
+
let visible = 0;
|
|
35255
|
+
|
|
35256
|
+
items.forEach((item, pi) => {
|
|
35257
|
+
const r = slResolveItem(item, layout, line);
|
|
35258
|
+
if (!r) return;
|
|
35259
|
+
const comp = r._comp;
|
|
35260
|
+
note(comp, item);
|
|
35261
|
+
|
|
35262
|
+
// The separator rides INSIDE the item's `cond`, so a vanished component
|
|
35263
|
+
// takes its separator with it. This is Starship's conditional group, and
|
|
35264
|
+
// it is what stops a dangling ` · · `. No user ever configures it.
|
|
35265
|
+
const children = [];
|
|
35266
|
+
if (visible > 0 && !slIsStructural(item.type)) children.push(slLit(sep));
|
|
35267
|
+
visible++;
|
|
35268
|
+
|
|
35269
|
+
for (const op of slLowerItem(r, comp, { formats, glyphsets, statemaps, ramps, idxOf, layout })) children.push(op);
|
|
35270
|
+
|
|
35271
|
+
const hide = (r.hide_when || []).filter((h) => h !== "never");
|
|
35272
|
+
const itemOp = {
|
|
35273
|
+
op: "item",
|
|
35274
|
+
id: item.id,
|
|
35275
|
+
b: comp.binding || null,
|
|
35276
|
+
hide,
|
|
35277
|
+
draw_empty: !!r.draw_empty,
|
|
35278
|
+
unknown: r.unknown || "dash",
|
|
35279
|
+
pad_l: (r.padding && r.padding[0]) || 0,
|
|
35280
|
+
pad_r: (r.padding && r.padding[1]) || 0,
|
|
35281
|
+
children,
|
|
35282
|
+
};
|
|
35283
|
+
if (r.min_cols) itemOp.min_cols = r.min_cols;
|
|
35284
|
+
if (r.max_cols) itemOp.max_cols = r.max_cols;
|
|
35285
|
+
ops.push(itemOp);
|
|
35286
|
+
|
|
35287
|
+
// The responsive drop order: priority first (1 = keep longest), then
|
|
35288
|
+
// RIGHT TO LEFT, because the eye reads left.
|
|
35289
|
+
dropCandidates.push({ id: item.id, line: li, pos: pi, priority: r.priority || 3, min_cols: r.min_cols || 0 });
|
|
35290
|
+
});
|
|
35291
|
+
|
|
35292
|
+
lines.push({ prefix: line.prefix || "", separator: sep, ops, static_width: slMeasure(ops, { formats, glyphsets, statemaps }) });
|
|
35293
|
+
});
|
|
35294
|
+
|
|
35295
|
+
// S5 — the responsive plans. Derived from each item's min_cols and priority;
|
|
35296
|
+
// the hook does ONE lookup and no measuring at render time.
|
|
35297
|
+
dropCandidates.sort((a, b) => b.priority - a.priority || b.pos - a.pos);
|
|
35298
|
+
const widths = [160, 120, 100, 80, 72];
|
|
35299
|
+
const totalStatic = lines.reduce((m, l) => Math.max(m, l.static_width), 0);
|
|
35300
|
+
for (const w of widths) {
|
|
35301
|
+
if (totalStatic <= w) continue;
|
|
35302
|
+
const drop = [];
|
|
35303
|
+
let est = totalStatic;
|
|
35304
|
+
for (const c of dropCandidates) {
|
|
35305
|
+
if (est <= w) break;
|
|
35306
|
+
drop.push(c.id);
|
|
35307
|
+
est -= 8; // a conservative per-item estimate; the exact width is measured
|
|
35308
|
+
}
|
|
35309
|
+
if (drop.length) plans.push({ cols: w, drop });
|
|
35310
|
+
}
|
|
35311
|
+
plans.sort((a, b) => a.cols - b.cols);
|
|
35312
|
+
|
|
35313
|
+
const compiled = {
|
|
35314
|
+
schema: SL_SCHEMA,
|
|
35315
|
+
orc_version: currentVersion(),
|
|
35316
|
+
compiled_at: new Date().toISOString(),
|
|
35317
|
+
ansi: layout.ansi === "off" ? "off" : "auto",
|
|
35318
|
+
board: layout.board || "status",
|
|
35319
|
+
// The preset RIDES IN THE COMPILED FILE. `preset-name` needs it and the
|
|
35320
|
+
// hook must never open the authored layout — one consumer per file, and
|
|
35321
|
+
// there is no exception for a field that would be convenient.
|
|
35322
|
+
preset: layout.preset || null,
|
|
35323
|
+
formats,
|
|
35324
|
+
glyphsets,
|
|
35325
|
+
statemaps,
|
|
35326
|
+
ramps,
|
|
35327
|
+
lines,
|
|
35328
|
+
plans,
|
|
35329
|
+
};
|
|
35330
|
+
|
|
35331
|
+
// `needs_refresh_interval` — the compiler knows which bindings are TIME-BASED,
|
|
35332
|
+
// so it can tell the user whether their layout needs `refreshInterval` rather
|
|
35333
|
+
// than making them guess. Claude Code's event triggers go quiet exactly while
|
|
35334
|
+
// a subagent runs, which is when an ORC user most wants the line to move.
|
|
35335
|
+
const lock = {
|
|
35336
|
+
schema: SL_SCHEMA,
|
|
35337
|
+
orc_version: currentVersion(),
|
|
35338
|
+
compiled_at: compiled.compiled_at,
|
|
35339
|
+
layout_hash: slHash(JSON.stringify(layout)),
|
|
35340
|
+
compiled_hash: slHash(JSON.stringify(compiled)),
|
|
35341
|
+
catalog_hash: slCatalogHash(),
|
|
35342
|
+
bindings: [...bindings].sort(),
|
|
35343
|
+
providers: [...providers].sort(),
|
|
35344
|
+
series: [...seriesWanted].sort(),
|
|
35345
|
+
needs_refresh_interval: timeBased ? 5 : null,
|
|
35346
|
+
warnings: [],
|
|
35347
|
+
};
|
|
35348
|
+
return { compiled, lock };
|
|
35349
|
+
}
|
|
35350
|
+
|
|
35351
|
+
function slLowerItem(r, comp, T) {
|
|
35352
|
+
const ops = [];
|
|
35353
|
+
const rend = STATUSLINE_RENDERERS[r.render] || STATUSLINE_RENDERERS.bare;
|
|
35354
|
+
const label = r.label == null ? null : String(r.label);
|
|
35355
|
+
const caseOf = r.case || "none";
|
|
35356
|
+
const applyCase = (s) => (caseOf === "upper" ? s.toUpperCase() : caseOf === "lower" ? s.toLowerCase() : caseOf === "title" ? s.replace(/\b\w/g, (m) => m.toUpperCase()) : s);
|
|
35357
|
+
|
|
35358
|
+
const fmt = {
|
|
35359
|
+
kind: r.format || "plain",
|
|
35360
|
+
prec: r.precision || 0,
|
|
35361
|
+
min_width: r.min_width || 0,
|
|
35362
|
+
align: r.align || "right",
|
|
35363
|
+
compact: r.compact || "off",
|
|
35364
|
+
sign: r.sign || "auto",
|
|
35365
|
+
truncate: r.truncate || "end",
|
|
35366
|
+
max_len: r.max_len || 0,
|
|
35367
|
+
case: caseOf,
|
|
35368
|
+
// prefix/suffix ride in the FORMAT, not around the item: `Dur —m` reads as
|
|
35369
|
+
// "minus minutes". An unknown value carries neither.
|
|
35370
|
+
prefix: r.prefix ? String(r.prefix) : "",
|
|
35371
|
+
suffix: r.suffix ? String(r.suffix) : "",
|
|
35372
|
+
};
|
|
35373
|
+
const fi = T.idxOf(T.formats, fmt);
|
|
35374
|
+
const gs = STATUSLINE_GLYPHSETS[r.glyphs] || STATUSLINE_GLYPHSETS.blocks;
|
|
35375
|
+
const gi = T.idxOf(T.glyphsets, gs);
|
|
35376
|
+
|
|
35377
|
+
const labelSgr = slSgr(r.label_color, r.emphasis, r.bg);
|
|
35378
|
+
const valueSgr = r.value_color && String(r.value_color).startsWith("ramp:") ? null : slSgr(r.value_color, r.emphasis, r.bg);
|
|
35379
|
+
let ri = null;
|
|
35380
|
+
const rampName = r.ramp || (String(r.value_color || "").startsWith("ramp:") ? String(r.value_color).slice(5) : null);
|
|
35381
|
+
if (rampName && STATUSLINE_RAMPS[rampName] && rampName !== "state") {
|
|
35382
|
+
const rp = STATUSLINE_RAMPS[rampName];
|
|
35383
|
+
const stops = r.ramp_stops || rp.stops;
|
|
35384
|
+
ri = T.idxOf(T.ramps, { stops, sgr: rp.colors.map((c, i) => slSgr(c, rp.emphasis ? [rp.emphasis[i]] : null, null) || "") });
|
|
35385
|
+
}
|
|
35386
|
+
|
|
35387
|
+
// The state map: glyph, ASCII twin and colour per state. Built ONCE here so
|
|
35388
|
+
// the `state` renderer and the `word` renderer can never disagree about what
|
|
35389
|
+
// colour `degrade` is.
|
|
35390
|
+
const stateMap = () => {
|
|
35391
|
+
const map = { glyphs: {}, sgr: {}, ascii: {} };
|
|
35392
|
+
for (const st of comp.states || []) {
|
|
35393
|
+
map.glyphs[st] = (comp.shapes && comp.shapes[st]) || st.slice(0, 1);
|
|
35394
|
+
map.ascii[st] = slAsciiOf(map.glyphs[st]);
|
|
35395
|
+
const c = (r.color_by_state && r.color_by_state[st]) || slStateColor(st, r, T.layout);
|
|
35396
|
+
const sg = slSgr(c, r.emphasis, r.bg);
|
|
35397
|
+
if (sg) map.sgr[st] = sg;
|
|
35398
|
+
}
|
|
35399
|
+
// R4 again: the unknown form is an em dash, never a blank and never a zero.
|
|
35400
|
+
map.glyphs.__unknown = "—";
|
|
35401
|
+
map.ascii.__unknown = "-";
|
|
35402
|
+
return map;
|
|
35403
|
+
};
|
|
35404
|
+
const stateSgrMap = () => T.idxOf(T.statemaps, stateMap());
|
|
35405
|
+
|
|
35406
|
+
const brackets = r.brackets || rend.brackets || null;
|
|
35407
|
+
const open = brackets ? brackets[0] : "";
|
|
35408
|
+
const close = brackets ? brackets[1] : "";
|
|
35409
|
+
|
|
35410
|
+
const pushLabel = (text, sep) => {
|
|
35411
|
+
if (!text) return;
|
|
35412
|
+
if (labelSgr) ops.push({ op: "sgr", s: labelSgr });
|
|
35413
|
+
ops.push(slLit(applyCase(text) + sep));
|
|
35414
|
+
if (labelSgr) ops.push({ op: "reset" });
|
|
35415
|
+
};
|
|
35416
|
+
|
|
35417
|
+
// A GROUP: its children are lowered inside its brackets and share its
|
|
35418
|
+
// background, so a run of chips reads as one object. It is the one place an
|
|
35419
|
+
// item op has an item op inside it, and the nesting stops there.
|
|
35420
|
+
if (comp.children) {
|
|
35421
|
+
if (open) ops.push(slLit(open));
|
|
35422
|
+
const inner = [];
|
|
35423
|
+
(r.children || []).forEach((k, i) => {
|
|
35424
|
+
const kc = SL_BY_ID.get(k.type);
|
|
35425
|
+
if (!kc) return;
|
|
35426
|
+
if (i) inner.push(slLit(" "));
|
|
35427
|
+
const kr = slResolveItem(Object.assign({ id: k.id || "g" + i }, k), T.layout, { theme: r.theme || null });
|
|
35428
|
+
if (!kr) return;
|
|
35429
|
+
for (const o of slLowerItem(kr, kc, T)) inner.push(o);
|
|
35430
|
+
});
|
|
35431
|
+
for (const o of inner) ops.push(o);
|
|
35432
|
+
if (close) ops.push(slLit(close));
|
|
35433
|
+
return ops;
|
|
35434
|
+
}
|
|
35435
|
+
|
|
35436
|
+
if (open) ops.push(slLit(open));
|
|
35437
|
+
|
|
35438
|
+
const valueOp = () => {
|
|
35439
|
+
if (rend.kind === "bar") {
|
|
35440
|
+
return { op: "bar", b: comp.binding, k: rend.sub, g: gi, w: r.width || rend.dflt_w || 10, r: ri, thr: r.threshold == null ? null : r.threshold };
|
|
35441
|
+
}
|
|
35442
|
+
if (rend.kind === "series") {
|
|
35443
|
+
return { op: "series", b: comp.binding, s: comp.series, k: rend.sub, g: gi, w: r.width || rend.dflt_w || 8 };
|
|
35444
|
+
}
|
|
35445
|
+
if (rend.kind === "motif") {
|
|
35446
|
+
const mi = T.idxOf(T.statemaps, { motifs: SL_MOTIFS });
|
|
35447
|
+
return { op: "motif", b: comp.state_binding || comp.binding, m: mi };
|
|
35448
|
+
}
|
|
35449
|
+
if (rend.kind === "state") {
|
|
35450
|
+
const map = stateMap();
|
|
35451
|
+
return { op: "state", b: comp.state_binding || comp.binding, m: T.idxOf(T.statemaps, map) };
|
|
35452
|
+
}
|
|
35453
|
+
// text
|
|
35454
|
+
const op = { op: "val", b: comp.binding, f: fi };
|
|
35455
|
+
if (r.render === "word" && comp.state_binding) op.b = comp.state_binding;
|
|
35456
|
+
// A ramp derives the colour FROM THE VALUE, so it applies to a number just
|
|
35457
|
+
// as much as to a bar — `context 38%` green and `context 94%` red is the
|
|
35458
|
+
// whole point of one.
|
|
35459
|
+
if (ri != null) op.r = ri;
|
|
35460
|
+
else if (comp.states && r.render === "word") op.m = stateSgrMap();
|
|
35461
|
+
return op;
|
|
35462
|
+
};
|
|
35463
|
+
|
|
35464
|
+
const wrapValue = (inner) => {
|
|
35465
|
+
// A state, a motif and a ramped bar all carry their OWN colour. Wrapping
|
|
35466
|
+
// them in the theme's value colour as well emits two sequences for one
|
|
35467
|
+
// glyph, and the outer one is the wrong answer.
|
|
35468
|
+
// A state, a motif, a ramped bar and a ramped/state-coloured number all
|
|
35469
|
+
// carry their OWN colour. Wrapping them in the theme's value colour as well
|
|
35470
|
+
// emits two sequences for one glyph, and the outer one is the wrong answer.
|
|
35471
|
+
if (!valueSgr) return [inner];
|
|
35472
|
+
if (inner.op === "state" || inner.op === "motif") return [inner];
|
|
35473
|
+
if (inner.op === "bar" && inner.r != null) return [inner];
|
|
35474
|
+
if (inner.op === "val" && (inner.r != null || inner.m != null)) return [inner];
|
|
35475
|
+
return [{ op: "sgr", s: valueSgr }, inner, { op: "reset" }];
|
|
35476
|
+
};
|
|
35477
|
+
|
|
35478
|
+
switch (r.render) {
|
|
35479
|
+
case "bare":
|
|
35480
|
+
case "word":
|
|
35481
|
+
case "dot":
|
|
35482
|
+
case "shape":
|
|
35483
|
+
case "icon":
|
|
35484
|
+
case "traffic":
|
|
35485
|
+
case "motif":
|
|
35486
|
+
case "pulse":
|
|
35487
|
+
case "spark":
|
|
35488
|
+
case "spark-braille":
|
|
35489
|
+
case "trend":
|
|
35490
|
+
case "delta":
|
|
35491
|
+
case "bar-only":
|
|
35492
|
+
case "blocks":
|
|
35493
|
+
case "meter":
|
|
35494
|
+
case "fine":
|
|
35495
|
+
case "braille-bar":
|
|
35496
|
+
case "dots":
|
|
35497
|
+
case "gauge":
|
|
35498
|
+
case "ring":
|
|
35499
|
+
case "micro":
|
|
35500
|
+
case "gradient":
|
|
35501
|
+
case "marker":
|
|
35502
|
+
case "split":
|
|
35503
|
+
for (const o of wrapValue(valueOp())) ops.push(o);
|
|
35504
|
+
break;
|
|
35505
|
+
case "plain":
|
|
35506
|
+
pushLabel(label, " ");
|
|
35507
|
+
for (const o of wrapValue(valueOp())) ops.push(o);
|
|
35508
|
+
break;
|
|
35509
|
+
case "label-value":
|
|
35510
|
+
pushLabel(label, ": ");
|
|
35511
|
+
for (const o of wrapValue(valueOp())) ops.push(o);
|
|
35512
|
+
break;
|
|
35513
|
+
case "bracket":
|
|
35514
|
+
case "angle":
|
|
35515
|
+
case "badge":
|
|
35516
|
+
pushLabel(label, label ? " " : "");
|
|
35517
|
+
for (const o of wrapValue(valueOp())) ops.push(o);
|
|
35518
|
+
break;
|
|
35519
|
+
case "paren":
|
|
35520
|
+
for (const o of wrapValue(valueOp())) ops.push(o);
|
|
35521
|
+
break;
|
|
35522
|
+
case "pill":
|
|
35523
|
+
// The two-tone chip: the label in reverse video, the value outside it.
|
|
35524
|
+
if (label) {
|
|
35525
|
+
const chip = slSgr(r.label_color, (r.emphasis || []).concat(["reverse"]), r.bg);
|
|
35526
|
+
if (chip) ops.push({ op: "sgr", s: chip });
|
|
35527
|
+
ops.push(slLit(applyCase(label)));
|
|
35528
|
+
if (chip) ops.push({ op: "reset" });
|
|
35529
|
+
}
|
|
35530
|
+
for (const o of wrapValue(valueOp())) ops.push(o);
|
|
35531
|
+
break;
|
|
35532
|
+
case "stack":
|
|
35533
|
+
pushLabel(label, "");
|
|
35534
|
+
for (const o of wrapValue(valueOp())) ops.push(o);
|
|
35535
|
+
break;
|
|
35536
|
+
case "fraction":
|
|
35537
|
+
for (const o of wrapValue(valueOp())) ops.push(o);
|
|
35538
|
+
break;
|
|
35539
|
+
case "bar": {
|
|
35540
|
+
// The user's own worked example: number THEN bar.
|
|
35541
|
+
for (const o of wrapValue({ op: "val", b: comp.binding, f: fi })) ops.push(o);
|
|
35542
|
+
ops.push(slLit(" "));
|
|
35543
|
+
ops.push({ op: "bar", b: comp.binding, k: "blocks", g: gi, w: r.width || 10, r: ri, thr: null });
|
|
35544
|
+
break;
|
|
35545
|
+
}
|
|
35546
|
+
case "link": {
|
|
35547
|
+
// OSC 8. ALWAYS a decoration on text that reads fine without it —
|
|
35548
|
+
// Terminal.app does not support it.
|
|
35549
|
+
ops.push({ op: "link", b: "pr.url", children: wrapValue(valueOp()) });
|
|
35550
|
+
break;
|
|
35551
|
+
}
|
|
35552
|
+
default:
|
|
35553
|
+
for (const o of wrapValue(valueOp())) ops.push(o);
|
|
35554
|
+
}
|
|
35555
|
+
|
|
35556
|
+
if (close) ops.push(slLit(close));
|
|
35557
|
+
return ops;
|
|
35558
|
+
}
|
|
35559
|
+
|
|
35560
|
+
// The motif frames stay on THIS side of the wall as data in the compiled file:
|
|
35561
|
+
// they are presentation, and a motif change must not need a reinstall.
|
|
35562
|
+
const SL_MOTIFS = {
|
|
35563
|
+
look: { u: ["◔", "◑", "◕", "●"], a: [".", "o", "O", "0"], ms: 260 },
|
|
35564
|
+
ask: { u: ["?", "¿", "?", "·"], a: ["?", "?", "?", "."], ms: 500 },
|
|
35565
|
+
plan: { u: ["▁", "▃", "▅", "▇"], a: ["_", "-", "=", "#"], ms: 220 },
|
|
35566
|
+
do: { u: ["▰", "▱", "▰", "▱"], a: ["=", "-", "=", "-"], ms: 180 },
|
|
35567
|
+
check: { u: ["◇", "◈", "◆", "◈"], a: ["<", "=", ">", "="], ms: 240 },
|
|
35568
|
+
ship: { u: ["›", "»", "≫", "»"], a: [">", ">", "=", ">"], ms: 200 },
|
|
35569
|
+
wait: { u: ["·", "˙", "·", "˙"], a: [".", "'", ".", "'"], ms: 700 },
|
|
35570
|
+
generic: { u: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], a: ["-", "\\", "|", "/"], ms: 90 },
|
|
35571
|
+
};
|
|
35572
|
+
|
|
35573
|
+
const SL_ASCII_MAP = {
|
|
35574
|
+
"█": "#", "░": ".", "▓": "#", "▒": "-", "▮": "|", "▯": ".", "━": "=", "─": "-",
|
|
35575
|
+
"●": "o", "○": ".", "◐": "0", "◑": "0", "◔": ":", "◕": "O", "◆": "*", "◇": ".",
|
|
35576
|
+
"◉": "*", "◎": ".", "▲": "^", "▼": "v", "▶": ">", "⣿": "#", "⠀": " ", "⠄": ".",
|
|
35577
|
+
"│": "|", "┃": "|", "⑂": "Y", "▏": "|", "▕": "|", "⟪": "<", "⟫": ">", "⟨": "<", "⟩": ">",
|
|
35578
|
+
"▁": "_", "▃": "-", "▅": "=", "▇": "#", "—": "-", "·": ".", "?": "?", "!": "!",
|
|
35579
|
+
"↔": "<>", "⟪": "<", "⟫": ">", "▰": "=", "▱": "-", "◈": "=", "›": ">", "»": ">",
|
|
35580
|
+
"≫": ">", "˙": "'", "⠋": "-", "▏": "|", "▕": "|", "│": "|", "┃": "|", "━": "=",
|
|
35581
|
+
"─": "-", "▎": "|", "▍": "|", "▌": "|", "▋": "|", "▊": "|", "▉": "|",
|
|
35582
|
+
// The three verdict icons are the ONE grandfathered emoji set — they already
|
|
35583
|
+
// ship and people read them. An emoji is two cells, so each ASCII twin is TWO
|
|
35584
|
+
// characters: R1 says a fallback must be the same width as the glyph it
|
|
35585
|
+
// replaces, and a one-character twin would shift the whole line under
|
|
35586
|
+
// ORC_STATUSLINE_ASCII=1.
|
|
35587
|
+
"✅": "OK", "🚀": "++", "⛔": "!!",
|
|
35588
|
+
};
|
|
35589
|
+
// A literal is a glyph like any other. The line separator ` · ` is not
|
|
35590
|
+
// ASCII, and without this it survived ORC_STATUSLINE_ASCII=1 while every glyph
|
|
35591
|
+
// around it fell back — which is the one place a partial fallback is worse than
|
|
35592
|
+
// none, because the line no longer lines up. `a` is emitted only when it
|
|
35593
|
+
// DIFFERS, so a plain-ASCII literal costs nothing in the compiled file.
|
|
35594
|
+
function slLit(t) {
|
|
35595
|
+
const a = slAsciiOf(t);
|
|
35596
|
+
return a === t ? { op: "lit", t } : { op: "lit", t, a };
|
|
35597
|
+
}
|
|
35598
|
+
|
|
35599
|
+
function slAsciiOf(g) {
|
|
35600
|
+
if (!g) return g;
|
|
35601
|
+
let out = "";
|
|
35602
|
+
for (const ch of g) out += SL_ASCII_MAP[ch] != null ? SL_ASCII_MAP[ch] : /^[\x20-\x7e]$/.test(ch) ? ch : "?";
|
|
35603
|
+
return out;
|
|
35604
|
+
}
|
|
35605
|
+
|
|
35606
|
+
// A state's colour, when the user has not overridden it. `color: "auto"` is a
|
|
35607
|
+
// REAL VALUE — it means "the component decides", which for a stateful component
|
|
35608
|
+
// is this.
|
|
35609
|
+
function slStateColor(state, r, layout) {
|
|
35610
|
+
if (r.color && r.color !== "auto" && r.color !== "none") return r.color;
|
|
35611
|
+
const theme = STATUSLINE_THEMES[(r.theme || (layout && layout.theme)) || "terminal"] || STATUSLINE_THEMES.terminal;
|
|
35612
|
+
const good = ["ready", "ok", "clear", "fresh", "warm", "good", "holding", "current", "live", "on", "approved", "main", "none", "low", "idle", "flat", "no"];
|
|
35613
|
+
const bad = ["degrade", "critical", "full", "stale", "broken", "expired", "changes", "detached", "in-flight", "yes", "max"];
|
|
35614
|
+
const warn = ["boosted", "warn", "aging", "soon", "poor", "drifted", "cold", "unregistered", "unknown", "running", "rising", "detached"];
|
|
35615
|
+
const s = String(state).toLowerCase();
|
|
35616
|
+
if (bad.includes(s)) return theme.critical;
|
|
35617
|
+
if (warn.includes(s)) return theme.warn;
|
|
35618
|
+
if (good.includes(s)) return theme.ok;
|
|
35619
|
+
return theme.accent;
|
|
35620
|
+
}
|
|
35621
|
+
|
|
35622
|
+
function slHash(s) {
|
|
35623
|
+
return require("crypto").createHash("sha256").update(s).digest("hex");
|
|
35624
|
+
}
|
|
35625
|
+
|
|
35626
|
+
// An ORC upgrade that adds, removes or CHANGES a component invalidates every
|
|
35627
|
+
// compiled layout on the machine, and the hook falls back to the built-in
|
|
35628
|
+
// default rather than executing a program compiled against a catalogue that no
|
|
35629
|
+
// longer exists. This is `flow.lock.json`'s orc_version check, made precise.
|
|
35630
|
+
let SL_CATALOG_HASH = null;
|
|
35631
|
+
function slCatalogHash() {
|
|
35632
|
+
if (SL_CATALOG_HASH) return SL_CATALOG_HASH;
|
|
35633
|
+
const shape = STATUSLINE_COMPONENTS.map((c) => ({
|
|
35634
|
+
id: c.id, group: c.group, label: c.label, renderers: c.renderers, defaults: c.defaults,
|
|
35635
|
+
states: c.states, shapes: c.shapes, bounded: c.bounded, series: c.series,
|
|
35636
|
+
unknown: c.unknown, cost: c.cost, binding: c.binding, state_binding: c.state_binding,
|
|
35637
|
+
}));
|
|
35638
|
+
SL_CATALOG_HASH = slHash(
|
|
35639
|
+
JSON.stringify(shape) +
|
|
35640
|
+
JSON.stringify(Object.keys(STATUSLINE_RENDERERS)) +
|
|
35641
|
+
JSON.stringify(Object.keys(STATUSLINE_GLYPHSETS)) +
|
|
35642
|
+
JSON.stringify(Object.keys(STATUSLINE_RAMPS)) +
|
|
35643
|
+
JSON.stringify(Object.keys(STATUSLINE_THEMES))
|
|
35644
|
+
);
|
|
35645
|
+
return SL_CATALOG_HASH;
|
|
35646
|
+
}
|
|
35647
|
+
|
|
35648
|
+
// Static width: `lit` is exact, `val` is min_width..the format's maximum, `bar`
|
|
35649
|
+
// is exactly `w`, `state` is the widest glyph in its map. Reported to the panel
|
|
35650
|
+
// so a user sees `~74 cells` before they ever open a terminal.
|
|
35651
|
+
function slMeasure(ops, T) {
|
|
35652
|
+
let w = 0;
|
|
35653
|
+
const walk = (list) => {
|
|
35654
|
+
for (const op of list) {
|
|
35655
|
+
switch (op.op) {
|
|
35656
|
+
case "lit":
|
|
35657
|
+
w += [...op.t].length;
|
|
35658
|
+
break;
|
|
35659
|
+
case "val": {
|
|
35660
|
+
const f = T.formats[op.f] || {};
|
|
35661
|
+
w += Math.max(f.min_width || 0, f.kind === "percent" ? 4 : 3);
|
|
35662
|
+
break;
|
|
35663
|
+
}
|
|
35664
|
+
case "bar":
|
|
35665
|
+
w += op.k === "gauge" || op.k === "ring" || op.k === "micro" ? 1 : op.w || 10;
|
|
35666
|
+
break;
|
|
35667
|
+
case "series":
|
|
35668
|
+
w += op.k === "trend" ? 1 : op.k === "delta" ? 3 : op.w || 8;
|
|
35669
|
+
break;
|
|
35670
|
+
case "state":
|
|
35671
|
+
case "motif":
|
|
35672
|
+
w += 1;
|
|
35673
|
+
break;
|
|
35674
|
+
case "item":
|
|
35675
|
+
w += (op.pad_l || 0) + (op.pad_r || 0);
|
|
35676
|
+
walk(op.children || []);
|
|
35677
|
+
break;
|
|
35678
|
+
case "cond":
|
|
35679
|
+
case "link":
|
|
35680
|
+
walk(op.children || []);
|
|
35681
|
+
break;
|
|
35682
|
+
default:
|
|
35683
|
+
break;
|
|
35684
|
+
}
|
|
35685
|
+
}
|
|
35686
|
+
};
|
|
35687
|
+
walk(ops);
|
|
35688
|
+
return w;
|
|
35689
|
+
}
|
|
35690
|
+
|
|
35691
|
+
// ── Presets ─────────────────────────────────────────────────────────────────
|
|
35692
|
+
// `apply` REPLACES the layout, so it is always confirmed and the loss is named —
|
|
35693
|
+
// the `orc diy init --force` rule.
|
|
35694
|
+
function slItem(type, o) {
|
|
35695
|
+
return Object.assign({ id: null, type }, o || {});
|
|
35696
|
+
}
|
|
35697
|
+
const STATUSLINE_PRESETS = {
|
|
35698
|
+
"orc-default": {
|
|
35699
|
+
summary: "Today's two lines, exactly. The byte-identical baseline.",
|
|
35700
|
+
build: () => [
|
|
35701
|
+
[slItem("verdict"), slItem("orc-version"), slItem("tier"), slItem("context"), slItem("quota-worst")],
|
|
35702
|
+
[slItem("phase-motif"), slItem("status"), slItem("agents"), slItem("duration"), slItem("branch")],
|
|
35703
|
+
[],
|
|
35704
|
+
],
|
|
35705
|
+
},
|
|
35706
|
+
minimal: {
|
|
35707
|
+
summary: "One line, four slots, the dim theme. The support answer for a busy terminal.",
|
|
35708
|
+
theme: "dim",
|
|
35709
|
+
build: () => [[slItem("verdict"), slItem("orc-version"), slItem("context", { render: "fine", width: 10 }), slItem("branch")], [], []],
|
|
35710
|
+
},
|
|
35711
|
+
"cost-watch": {
|
|
35712
|
+
summary: "What this session is spending, in every unit the payload gives for free.",
|
|
35713
|
+
build: () => [
|
|
35714
|
+
[slItem("verdict"), slItem("tier"), slItem("context", { render: "fine", width: 8 })],
|
|
35715
|
+
[slItem("quota-5h", { render: "bar", width: 10 }), slItem("quota-week", { render: "bar", width: 10 }), slItem("ucs"), slItem("mtok"), slItem("cost-usd")],
|
|
35716
|
+
[],
|
|
35717
|
+
],
|
|
35718
|
+
},
|
|
35719
|
+
"run-watch": {
|
|
35720
|
+
summary: "What ORC is doing, promoted to line 1. Motion on.",
|
|
35721
|
+
build: () => [
|
|
35722
|
+
[slItem("phase-motif"), slItem("status"), slItem("agents"), slItem("agents-running"), slItem("duration")],
|
|
35723
|
+
[slItem("inflight"), slItem("lane"), slItem("last-agent")],
|
|
35724
|
+
[slItem("verdict"), slItem("tier"), slItem("context"), slItem("quota-worst"), slItem("branch")],
|
|
35725
|
+
],
|
|
35726
|
+
},
|
|
35727
|
+
knowledge: {
|
|
35728
|
+
summary: "Almost always empty: everything hides while healthy, so a component appearing IS the signal.",
|
|
35729
|
+
build: () => [
|
|
35730
|
+
[slItem("verdict"), slItem("tier"), slItem("context")],
|
|
35731
|
+
[slItem("inflight", { hide_when: ["ok"] }), slItem("trace-age", { hide_when: ["ok"] })],
|
|
35732
|
+
[],
|
|
35733
|
+
],
|
|
35734
|
+
},
|
|
35735
|
+
"cache-watch": {
|
|
35736
|
+
summary: "The prompt-cache block — free, and the most decision-relevant data in the payload.",
|
|
35737
|
+
build: () => [
|
|
35738
|
+
[slItem("verdict"), slItem("tier"), slItem("context")],
|
|
35739
|
+
[slItem("cache"), slItem("cache-hit", { render: "bar", width: 10 }), slItem("cache-expires"), slItem("cache-recache-cost")],
|
|
35740
|
+
[],
|
|
35741
|
+
],
|
|
35742
|
+
},
|
|
35743
|
+
// ── the second board (v1.4.0) ──────────────────────────────────────────
|
|
35744
|
+
"agent-default": {
|
|
35745
|
+
board: "subagent",
|
|
35746
|
+
summary: "What the agent is, what it is running at, and what it has cost so far.",
|
|
35747
|
+
build: () => [[slItem("task-name"), slItem("task-tier"), slItem("task-tokens"), slItem("task-elapsed")]],
|
|
35748
|
+
},
|
|
35749
|
+
"agent-watch": {
|
|
35750
|
+
board: "subagent",
|
|
35751
|
+
summary: "For a run you are watching: status, its own context window, and the clock.",
|
|
35752
|
+
build: () => [[slItem("task-status"), slItem("task-name"), slItem("task-context", { render: "fine", width: 8 }), slItem("task-elapsed")]],
|
|
35753
|
+
},
|
|
35754
|
+
"agent-tier": {
|
|
35755
|
+
board: "subagent",
|
|
35756
|
+
summary: "The downgrade check, made visible: the model and effort ORC actually got, per agent.",
|
|
35757
|
+
build: () => [[slItem("task-name"), slItem("task-model"), slItem("task-effort"), slItem("task-status")]],
|
|
35758
|
+
},
|
|
35759
|
+
mono: {
|
|
35760
|
+
summary: "No colour anywhere — shapes and emphasis only. Proves R4 by being shipped, and it is the right preset for a README screenshot.",
|
|
35761
|
+
theme: "mono",
|
|
35762
|
+
ansi: "off",
|
|
35763
|
+
build: () => [
|
|
35764
|
+
[slItem("verdict", { render: "shape" }), slItem("orc-version"), slItem("tier"), slItem("context", { render: "blocks", width: 10 })],
|
|
35765
|
+
[slItem("status"), slItem("agents"), slItem("duration"), slItem("branch")],
|
|
35766
|
+
[],
|
|
35767
|
+
],
|
|
35768
|
+
},
|
|
35769
|
+
};
|
|
35770
|
+
|
|
35771
|
+
function slBuildPreset(name) {
|
|
35772
|
+
const p = STATUSLINE_PRESETS[name];
|
|
35773
|
+
if (!p) return null;
|
|
35774
|
+
const layout = slBlankLayout(SL_BOARDS[p.board || "status"]);
|
|
35775
|
+
layout.preset = name;
|
|
35776
|
+
if (p.theme) layout.theme = p.theme;
|
|
35777
|
+
if (p.ansi) layout.ansi = p.ansi;
|
|
35778
|
+
const rows = p.build();
|
|
35779
|
+
rows.forEach((items, i) => {
|
|
35780
|
+
layout.lines[i].items = items.map((it) => Object.assign({}, it, { id: null }));
|
|
35781
|
+
});
|
|
35782
|
+
// Assign stable ids after the whole shape exists, so line 2 never reuses a
|
|
35783
|
+
// line 1 id.
|
|
35784
|
+
let n = 1;
|
|
35785
|
+
for (const l of layout.lines) for (const it of l.items) it.id = "i" + n++;
|
|
35786
|
+
return layout;
|
|
35787
|
+
}
|
|
35788
|
+
|
|
35789
|
+
// A preset carries `active` from the CLI, matched on the ITEM SET and ignoring
|
|
35790
|
+
// cosmetic-only fields — the `orc diy` presets rule, so renaming a colour does
|
|
35791
|
+
// not lose the shape.
|
|
35792
|
+
function slPresetsFor(boardId) {
|
|
35793
|
+
return Object.entries(STATUSLINE_PRESETS).filter(([, p]) => (p.board || "status") === boardId);
|
|
35794
|
+
}
|
|
35795
|
+
|
|
35796
|
+
function slPresetActive(layout, name) {
|
|
35797
|
+
const p = slBuildPreset(name);
|
|
35798
|
+
if (!p) return false;
|
|
35799
|
+
const shape = (l) => l.lines.map((x) => (x.items || []).map((i) => i.type + ":" + (i.render || "")).join(","));
|
|
35800
|
+
return JSON.stringify(shape(p)) === JSON.stringify(shape(layout));
|
|
35801
|
+
}
|
|
35802
|
+
|
|
35803
|
+
|
|
35804
|
+
|
|
35805
|
+
// ── group and expand (v1.3.0 W5) ───────────────────────────────────────────
|
|
35806
|
+
// A GROUP wraps 2-4 placed components into one visual object and counts as ONE
|
|
35807
|
+
// slot. EXPAND is its inverse, and it is also how a composite becomes editable:
|
|
35808
|
+
// `tier-block` is three components in one chip until you want to restyle just
|
|
35809
|
+
// the version, at which point it becomes three chips.
|
|
35810
|
+
function slGroupCmd(claudeDir, p) {
|
|
35811
|
+
const layout = slLoadOrDefault(claudeDir);
|
|
35812
|
+
const refs = p.slice(2).map(slParseRef).filter(Boolean);
|
|
35813
|
+
if (refs.length < 2 || refs.length > 4) {
|
|
35814
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "bad-count", hint: "a group holds 2 to 4 components" }, 2);
|
|
35815
|
+
console.error("usage: orc statusline group <line>:<pos> <line>:<pos> [...] (2 to 4)");
|
|
35816
|
+
process.exit(2);
|
|
35817
|
+
}
|
|
35818
|
+
// ONE LINE. A group that spanned two lines would not be a visual object; it
|
|
35819
|
+
// would be two.
|
|
35820
|
+
const lines = new Set(refs.map((r) => r.line));
|
|
35821
|
+
if (lines.size !== 1) {
|
|
35822
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "cross-line", hint: "a group is one visual object, so its parts are on one line" }, 2);
|
|
35823
|
+
console.error("a group's components must all be on the same line");
|
|
35824
|
+
process.exit(2);
|
|
35825
|
+
}
|
|
35826
|
+
const li = refs[0].line;
|
|
35827
|
+
const line = layout.lines[li - 1];
|
|
35828
|
+
const picked = refs
|
|
35829
|
+
.map((r) => (line.items || [])[r.pos - 1])
|
|
35830
|
+
.filter(Boolean);
|
|
35831
|
+
if (picked.length !== refs.length) {
|
|
35832
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "no-item" }, 1);
|
|
35833
|
+
console.error("one of those positions is empty");
|
|
35834
|
+
process.exit(1);
|
|
35835
|
+
}
|
|
35836
|
+
const at = Math.min(...refs.map((r) => r.pos)) - 1;
|
|
35837
|
+
// The id is claimed BEFORE the children leave the line. Taking it afterwards
|
|
35838
|
+
// would hand the group the id one of its own children still holds — the
|
|
35839
|
+
// children are nested, not deleted, so their ids are still in use.
|
|
35840
|
+
const gid = slNewId(layout);
|
|
35841
|
+
for (const it of picked) line.items.splice(line.items.indexOf(it), 1);
|
|
35842
|
+
const g = { id: gid, type: "group", children: picked.map((it) => Object.assign({}, it)) };
|
|
35843
|
+
line.items.splice(at, 0, g);
|
|
35844
|
+
return slSaveAndCompile(claudeDir, layout, { action: "group", id: g.id, line: li, held: picked.map((x) => x.type) });
|
|
35845
|
+
}
|
|
35846
|
+
|
|
35847
|
+
function slExpandCmd(claudeDir, p) {
|
|
35848
|
+
const layout = slLoadOrDefault(claudeDir);
|
|
35849
|
+
const at = slParseRef(p[2]);
|
|
35850
|
+
if (!at) {
|
|
35851
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "bad-ref" }, 2);
|
|
35852
|
+
console.error("usage: orc statusline expand <line>:<pos>");
|
|
35853
|
+
process.exit(2);
|
|
35854
|
+
}
|
|
35855
|
+
const line = layout.lines[at.line - 1];
|
|
35856
|
+
const item = line && (line.items || [])[at.pos - 1];
|
|
35857
|
+
if (!item) {
|
|
35858
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "no-item", line: at.line, pos: at.pos }, 1);
|
|
35859
|
+
console.error(`there is no component at line ${at.line} position ${at.pos}`);
|
|
35860
|
+
process.exit(1);
|
|
35861
|
+
}
|
|
35862
|
+
const comp = SL_BY_ID.get(item.type);
|
|
35863
|
+
const parts = item.children && item.children.length
|
|
35864
|
+
? item.children
|
|
35865
|
+
: comp && comp.composite
|
|
35866
|
+
? comp.composite.map((tid) => ({ type: tid }))
|
|
35867
|
+
: null;
|
|
35868
|
+
if (!parts) {
|
|
35869
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "not-composite", type: item.type, hint: "only a group or a composite shortcut expands" }, 1);
|
|
35870
|
+
console.error(`"${item.type}" is not a group and holds no parts`);
|
|
35871
|
+
process.exit(1);
|
|
35872
|
+
}
|
|
35873
|
+
// Expanding can take a line past five. That is a REFUSAL with the count
|
|
35874
|
+
// named, not a silent truncation — losing a component to make room is the
|
|
35875
|
+
// one thing an expand must never do.
|
|
35876
|
+
line.items.splice(at.pos - 1, 1, ...parts.map((k) => Object.assign({}, k, { id: null })));
|
|
35877
|
+
let n = 1;
|
|
35878
|
+
for (const l of layout.lines) for (const it of l.items) if (!it.id) it.id = "i" + (n++ + 900);
|
|
35879
|
+
return slSaveAndCompile(claudeDir, layout, { action: "expand", line: at.line, pos: at.pos, into: parts.map((k) => k.type) });
|
|
35880
|
+
}
|
|
35881
|
+
|
|
35882
|
+
// Two `config` components on different keys, or two `mtok-kind` components on
|
|
35883
|
+
// different kinds, is a normal thing to want.
|
|
35884
|
+
function slCloneCmd(claudeDir, p) {
|
|
35885
|
+
const layout = slLoadOrDefault(claudeDir);
|
|
35886
|
+
const at = slParseRef(p[2]);
|
|
35887
|
+
if (!at) {
|
|
35888
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "bad-ref" }, 2);
|
|
35889
|
+
console.error("usage: orc statusline clone <line>:<pos>");
|
|
35890
|
+
process.exit(2);
|
|
35891
|
+
}
|
|
35892
|
+
const line = layout.lines[at.line - 1];
|
|
35893
|
+
const item = line && (line.items || [])[at.pos - 1];
|
|
35894
|
+
if (!item) {
|
|
35895
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "no-item" }, 1);
|
|
35896
|
+
console.error(`there is no component at line ${at.line} position ${at.pos}`);
|
|
35897
|
+
process.exit(1);
|
|
35898
|
+
}
|
|
35899
|
+
const copy = JSON.parse(JSON.stringify(item));
|
|
35900
|
+
copy.id = slNewId(layout);
|
|
35901
|
+
line.items.splice(at.pos, 0, copy);
|
|
35902
|
+
return slSaveAndCompile(claudeDir, layout, { action: "clone", id: copy.id, type: item.type, line: at.line });
|
|
35903
|
+
}
|
|
35904
|
+
|
|
35905
|
+
// ── The preview: ONE engine, two callers ───────────────────────────────────
|
|
35906
|
+
// `orc statusline preview` renders through the SAME module the hook does
|
|
35907
|
+
// (`templates/hooks/orc-statusline-render.js`), so the panel's preview and the
|
|
35908
|
+
// bar's real output are byte-identical BY CONSTRUCTION rather than by a test
|
|
35909
|
+
// that would eventually drift.
|
|
35910
|
+
let SL_ENGINE = null;
|
|
35911
|
+
function slEngine() {
|
|
35912
|
+
if (SL_ENGINE) return SL_ENGINE;
|
|
35913
|
+
SL_ENGINE = require(path.join(SRC_HOOKS, "orc-statusline-render.js"));
|
|
35914
|
+
return SL_ENGINE;
|
|
35915
|
+
}
|
|
35916
|
+
|
|
35917
|
+
// The fixture states the preview renders against. `--state <k>=<v>` overrides
|
|
35918
|
+
// any field. They carry ONE OF EVERY STATE including the ugly ones — you cannot
|
|
35919
|
+
// design a STALE chip on a fresh wiki.
|
|
35920
|
+
// The task a subagent-board preview is drawn against. It carries what the agent
|
|
35921
|
+
// panel really hands over — including `tokenCount`, which is the number v1.2.0
|
|
35922
|
+
// concluded could not be measured.
|
|
35923
|
+
const SL_TASK_FIXTURES = {
|
|
35924
|
+
healthy: { id: "t1", name: "orc-executor-opus-5-low", type: "orc-executor-opus-5-low", description: "wire the retry ladder", status: "running", model: "claude-opus-5", effort: "low", tokenCount: 84000, contextWindowSize: 200000, startTime: 1767225600000 - 17 * 60000 },
|
|
35925
|
+
degraded: { id: "t2", name: "orc-executor-sonnet-4-6-med", type: "orc-executor-sonnet-4-6-med", description: "rename two files", status: "failed", model: "claude-sonnet-4-6", effort: "medium", tokenCount: 191000, contextWindowSize: 200000, startTime: 1767225600000 - 96 * 60000 },
|
|
35926
|
+
empty: { id: "t3" },
|
|
35927
|
+
};
|
|
35928
|
+
|
|
35929
|
+
const SL_FIXTURES = {
|
|
35930
|
+
healthy: {
|
|
35931
|
+
label: "a healthy session mid-run",
|
|
35932
|
+
payload: {
|
|
35933
|
+
model: { id: "claude-opus-5", display_name: "Opus 5" },
|
|
35934
|
+
effort: { level: "high" },
|
|
35935
|
+
version: "2.1.80",
|
|
35936
|
+
context_window: { used_percentage: 38, remaining_percentage: 62, context_window_size: 200000 },
|
|
35937
|
+
rate_limits: { five_hour: { used_percentage: 61, resets_at: null }, seven_day: { used_percentage: 18, resets_at: null } },
|
|
35938
|
+
cost: { total_cost_usd: 0.42, total_duration_ms: 2880000, total_api_duration_ms: 960000, total_lines_added: 412, total_lines_removed: 88 },
|
|
35939
|
+
prompt_cache: { warm: true, ttl: "5m", hit_ratio: 0.91, requests: 214, misses: 19, cache_write_tokens: 48000, recache_tokens_if_cold: 45000 },
|
|
35940
|
+
workspace: { project_dir: "/repo", repo: { host: "github.com", owner: "azure-id", name: "orc" } },
|
|
35941
|
+
},
|
|
35942
|
+
ledger: { five_hour: { baseline: 55, last: 61, accumulated: 0, resets: 0 }, tok: { input: 12000, cache_write: 40000, cache_read: 350000, output: 10000 }, series: { quota5h: [55, 56, 58, 59, 60, 61], ucs: [0, 1, 2, 4, 5, 6], mtok: [10, 40, 120, 260, 380, 412] } },
|
|
35943
|
+
scan: { spawns: 7, running: 2, lanes: ["quick"], phase: { lane: "quick", label: "Q3 DO", kind: "do" }, branch: "main", inflight: "clear", wiki: { tier: "fresh", distance: 3 }, extra_enabled: true, trace_age_min: 0, trace_state: "live" },
|
|
35944
|
+
},
|
|
35945
|
+
degraded: {
|
|
35946
|
+
label: "the wrong tier, a full window, and nothing running",
|
|
35947
|
+
payload: {
|
|
35948
|
+
model: { id: "claude-sonnet-5", display_name: "Sonnet 5" },
|
|
35949
|
+
effort: { level: "low" },
|
|
35950
|
+
context_window: { used_percentage: 94, remaining_percentage: 6 },
|
|
35951
|
+
rate_limits: { five_hour: { used_percentage: 93, resets_at: null }, seven_day: { used_percentage: 88, resets_at: null } },
|
|
35952
|
+
prompt_cache: { warm: false, hit_ratio: 0.12, requests: 4, misses: 3 },
|
|
35953
|
+
workspace: { project_dir: "/repo" },
|
|
35954
|
+
},
|
|
35955
|
+
ledger: {},
|
|
35956
|
+
scan: { spawns: 0, running: 0, lanes: [], phase: null, inflight: "unknown" },
|
|
35957
|
+
},
|
|
35958
|
+
empty: {
|
|
35959
|
+
label: "a payload with nothing in it — every component on its unknown form",
|
|
35960
|
+
payload: {},
|
|
35961
|
+
ledger: {},
|
|
35962
|
+
scan: {},
|
|
35963
|
+
},
|
|
35964
|
+
};
|
|
35965
|
+
|
|
35966
|
+
function slDerived(payload) {
|
|
35967
|
+
// The verdict is computed HERE so `verdict` is a component like any other,
|
|
35968
|
+
// and so the hook and the preview agree about it. The rules are the shipped
|
|
35969
|
+
// acceptance matrix, unchanged: only a POSITIVELY-known bad tier degrades.
|
|
35970
|
+
const model = String((payload.model && payload.model.id) || "");
|
|
35971
|
+
const effort = String((payload.effort && payload.effort.level) || "").toLowerCase();
|
|
35972
|
+
const rank = { low: 1, medium: 2, high: 3, xhigh: 4, max: 5 }[effort] || 0;
|
|
35973
|
+
const isOpus48 = /opus[\s._-]?4[\s._-]?8\b/i.test(model);
|
|
35974
|
+
const isOpus5 = /opus[\s._-]?5\b/i.test(model);
|
|
35975
|
+
const isFable5 = /fable[\s._-]?5\b/i.test(model);
|
|
35976
|
+
const known = !!model;
|
|
35977
|
+
const reasons = [];
|
|
35978
|
+
let verdict = "ready";
|
|
35979
|
+
if (known && !isOpus48 && !isOpus5 && !isFable5) {
|
|
35980
|
+
reasons.push("model≠Opus5/Opus4.8/Fable5");
|
|
35981
|
+
verdict = "degrade";
|
|
35982
|
+
}
|
|
35983
|
+
if (rank && rank < 3 && !((isOpus5 || isFable5) && rank >= 2)) {
|
|
35984
|
+
reasons.push("effort≠high");
|
|
35985
|
+
verdict = "degrade";
|
|
35986
|
+
}
|
|
35987
|
+
const worst = Math.max(
|
|
35988
|
+
(payload.rate_limits && payload.rate_limits.five_hour && payload.rate_limits.five_hour.used_percentage) || 0,
|
|
35989
|
+
(payload.rate_limits && payload.rate_limits.seven_day && payload.rate_limits.seven_day.used_percentage) || 0
|
|
35990
|
+
);
|
|
35991
|
+
if (worst >= 90) {
|
|
35992
|
+
reasons.push("quota≥90%");
|
|
35993
|
+
verdict = "degrade";
|
|
35994
|
+
}
|
|
35995
|
+
if (verdict !== "degrade") {
|
|
35996
|
+
if ((isOpus48 && rank >= 4) || ((isOpus5 || isFable5) && rank >= 2)) verdict = "boosted";
|
|
35997
|
+
}
|
|
35998
|
+
return { verdict, reasons, version: currentVersion() };
|
|
35999
|
+
}
|
|
36000
|
+
|
|
36001
|
+
function slPreview(compiled, fixtureName, cols, overrides) {
|
|
36002
|
+
const fx = SL_FIXTURES[fixtureName] || SL_FIXTURES.healthy;
|
|
36003
|
+
const payload = JSON.parse(JSON.stringify(fx.payload));
|
|
36004
|
+
for (const [k, v] of Object.entries(overrides || {})) slDeepSet(payload, k, v);
|
|
36005
|
+
const now = 1767225600000; // a FIXED instant: a preview must be deterministic
|
|
36006
|
+
const ctx = {
|
|
36007
|
+
payload,
|
|
36008
|
+
ledger: Object.assign({ started_at: now - 2880000 }, fx.ledger),
|
|
36009
|
+
scan: fx.scan || {},
|
|
36010
|
+
derived: slDerived(payload),
|
|
36011
|
+
task: SL_TASK_FIXTURES[fixtureName] || SL_TASK_FIXTURES.healthy,
|
|
36012
|
+
now,
|
|
36013
|
+
cols: cols || 0,
|
|
36014
|
+
env: Object.assign({}, process.env, { COLUMNS: String(cols || 120) }),
|
|
36015
|
+
};
|
|
36016
|
+
return slEngine().render(compiled, ctx);
|
|
36017
|
+
}
|
|
36018
|
+
|
|
36019
|
+
function slDeepSet(obj, dotted, value) {
|
|
36020
|
+
const parts = String(dotted).split(".");
|
|
36021
|
+
let o = obj;
|
|
36022
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
36023
|
+
if (typeof o[parts[i]] !== "object" || o[parts[i]] == null) o[parts[i]] = {};
|
|
36024
|
+
o = o[parts[i]];
|
|
36025
|
+
}
|
|
36026
|
+
const last = parts[parts.length - 1];
|
|
36027
|
+
const n = Number(value);
|
|
36028
|
+
o[last] = value === "true" ? true : value === "false" ? false : Number.isFinite(n) && value !== "" ? n : value;
|
|
36029
|
+
}
|
|
36030
|
+
|
|
36031
|
+
// ── The commands ───────────────────────────────────────────────────────────
|
|
36032
|
+
// All project-scoped, all with `--json`, all validating through the ONE
|
|
36033
|
+
// validator. Exit codes follow the house convention: 0 ok, 1 a state the user
|
|
36034
|
+
// must fix, 2 a bad argument.
|
|
36035
|
+
function statusline() {
|
|
36036
|
+
const claudeDir = resolveClaudeDir();
|
|
36037
|
+
const p = positionals();
|
|
36038
|
+
const sub = p[1] || "show";
|
|
36039
|
+
switch (sub) {
|
|
36040
|
+
case "components":
|
|
36041
|
+
return slComponentsCmd();
|
|
36042
|
+
case "show":
|
|
36043
|
+
return slShowCmd(claudeDir);
|
|
36044
|
+
case "explain":
|
|
36045
|
+
return slExplainCmd(claudeDir, p[2]);
|
|
36046
|
+
case "validate":
|
|
36047
|
+
return slValidateCmd(claudeDir);
|
|
36048
|
+
case "preview":
|
|
36049
|
+
return slPreviewCmd(claudeDir);
|
|
36050
|
+
case "set":
|
|
36051
|
+
return slSetCmd(claudeDir, p);
|
|
36052
|
+
case "move":
|
|
36053
|
+
return slMoveCmd(claudeDir, p);
|
|
36054
|
+
case "remove":
|
|
36055
|
+
return slRemoveCmd(claudeDir, p);
|
|
36056
|
+
case "line":
|
|
36057
|
+
return slLineCmd(claudeDir, p);
|
|
36058
|
+
case "presets":
|
|
36059
|
+
return slPresetsCmd(claudeDir);
|
|
36060
|
+
case "apply":
|
|
36061
|
+
return slApplyCmd(claudeDir, p[2]);
|
|
36062
|
+
case "reset":
|
|
36063
|
+
return slResetCmd(claudeDir);
|
|
36064
|
+
case "group":
|
|
36065
|
+
return slGroupCmd(claudeDir, p);
|
|
36066
|
+
case "expand":
|
|
36067
|
+
return slExpandCmd(claudeDir, p);
|
|
36068
|
+
case "clone":
|
|
36069
|
+
return slCloneCmd(claudeDir, p);
|
|
36070
|
+
case "compile":
|
|
36071
|
+
return slCompileCmd(claudeDir, true);
|
|
36072
|
+
default:
|
|
36073
|
+
console.error(`usage: orc statusline components|show|explain|validate|preview [--json]
|
|
36074
|
+
orc statusline set <line> <pos> <type> [--render R] [--label L] [--color C] [--width N] …
|
|
36075
|
+
orc statusline move <from-line>:<pos> <to-line>:<pos>
|
|
36076
|
+
orc statusline remove <line>:<pos>
|
|
36077
|
+
orc statusline line <n> --separator "…" [--max-width N] [--theme T]
|
|
36078
|
+
orc statusline group <line>:<pos> <line>:<pos> [...] wrap 2-4 as one object
|
|
36079
|
+
orc statusline expand <line>:<pos> a composite, or a group, back into its parts
|
|
36080
|
+
orc statusline clone <line>:<pos>
|
|
36081
|
+
orc statusline presets [--json] | apply <name> | reset | compile
|
|
36082
|
+
|
|
36083
|
+
COMPOSE THIS IN \`orc ui\` ▸ CLI Hook Interface. These flags exist so the panel
|
|
36084
|
+
has something to shell — typing a three-line layout is worse than the board.`);
|
|
36085
|
+
process.exit(2);
|
|
36086
|
+
}
|
|
36087
|
+
}
|
|
36088
|
+
|
|
36089
|
+
// THE PANEL DRAWS THIS AND DERIVES NONE OF IT. Groups, renderers, states, cost,
|
|
36090
|
+
// availability in this project, and a rendered preview string per renderer.
|
|
36091
|
+
function slComponentsCmd() {
|
|
36092
|
+
const board = slBoard();
|
|
36093
|
+
const groups = {};
|
|
36094
|
+
const out = [];
|
|
36095
|
+
for (const c of STATUSLINE_COMPONENTS) {
|
|
36096
|
+
if (c.board !== board.id) continue;
|
|
36097
|
+
const row = {
|
|
36098
|
+
id: c.id,
|
|
36099
|
+
group: c.group,
|
|
36100
|
+
label: c.label,
|
|
36101
|
+
summary: c.summary || "",
|
|
36102
|
+
renderers: c.renderers,
|
|
36103
|
+
defaults: c.defaults,
|
|
36104
|
+
states: c.states,
|
|
36105
|
+
shapes: c.shapes,
|
|
36106
|
+
bounded: c.bounded,
|
|
36107
|
+
series: c.series,
|
|
36108
|
+
unknown: c.unknown,
|
|
36109
|
+
cost: c.cost,
|
|
36110
|
+
board: c.board,
|
|
36111
|
+
refused_reason: c.refused_reason,
|
|
36112
|
+
params: c.params || null,
|
|
36113
|
+
binding: c.binding,
|
|
36114
|
+
composite: c.composite || null,
|
|
36115
|
+
time_based: !!c.time_based,
|
|
36116
|
+
previews: slRendererPreviews(c),
|
|
36117
|
+
};
|
|
36118
|
+
out.push(row);
|
|
36119
|
+
(groups[c.group] = groups[c.group] || []).push(c.id);
|
|
36120
|
+
}
|
|
36121
|
+
if (wantsJson()) {
|
|
36122
|
+
return emitJson({
|
|
36123
|
+
ok: true,
|
|
36124
|
+
schema: SL_SCHEMA,
|
|
36125
|
+
catalog_hash: slCatalogHash(),
|
|
36126
|
+
count: out.length,
|
|
36127
|
+
groups: SL_GROUP_NAMES,
|
|
36128
|
+
components: out,
|
|
36129
|
+
renderers: Object.fromEntries(Object.entries(STATUSLINE_RENDERERS).map(([k, v]) => [k, { kind: v.kind, form: v.form || null, needs: v.needs, width: v.width || null, decoration: !!v.decoration }])),
|
|
36130
|
+
glyph_sets: Object.keys(STATUSLINE_GLYPHSETS),
|
|
36131
|
+
ramps: Object.fromEntries(Object.entries(STATUSLINE_RAMPS).map(([k, v]) => [k, { stops: v.stops, colors: v.colors, why: v.why }])),
|
|
36132
|
+
themes: STATUSLINE_THEMES,
|
|
36133
|
+
hide_when: STATUSLINE_HIDE_WHEN,
|
|
36134
|
+
formats: SL_FORMATS,
|
|
36135
|
+
compact: SL_COMPACT,
|
|
36136
|
+
cases: SL_CASES,
|
|
36137
|
+
truncate: SL_TRUNCATE,
|
|
36138
|
+
emphasis: Object.keys(SL_EMPHASIS),
|
|
36139
|
+
refused_emphasis: SL_REFUSED_EMPHASIS,
|
|
36140
|
+
colors: Object.keys(SL_ANSI_SLOTS),
|
|
36141
|
+
board: board.id,
|
|
36142
|
+
boards: Object.keys(SL_BOARDS),
|
|
36143
|
+
max_per_line: 5,
|
|
36144
|
+
lines: board.lines,
|
|
36145
|
+
config_key: board.key,
|
|
36146
|
+
setting: board.setting,
|
|
36147
|
+
dense_prefix: "A line may hold a component only if every line above it holds at least one.",
|
|
36148
|
+
}, 0);
|
|
36149
|
+
}
|
|
36150
|
+
console.log("");
|
|
36151
|
+
console.log(ui.header("Status-line components"));
|
|
36152
|
+
for (const [g, name] of Object.entries(SL_GROUP_NAMES)) {
|
|
36153
|
+
const ids = groups[g];
|
|
36154
|
+
if (!ids || !ids.length) continue;
|
|
36155
|
+
console.log("");
|
|
36156
|
+
console.log(" " + ui.color.cyan(name));
|
|
36157
|
+
for (const id of ids) {
|
|
36158
|
+
const c = SL_BY_ID.get(id);
|
|
36159
|
+
const cost = c.cost === "refused" ? ui.color.red("refused") : c.cost === "free" ? ui.color.gray("free") : ui.color.gray(c.cost);
|
|
36160
|
+
console.log(` ${id.padEnd(20)} ${cost.padEnd(18)} ${ui.color.gray(String(c.summary || "").slice(0, 60))}`);
|
|
36161
|
+
}
|
|
36162
|
+
}
|
|
36163
|
+
console.log("");
|
|
36164
|
+
console.log(ui.color.gray(` ${out.length} components on the ${board.id} board · compose them in \`orc ui\` ▸ CLI Hook Interface`));
|
|
36165
|
+
console.log("");
|
|
36166
|
+
}
|
|
36167
|
+
|
|
36168
|
+
const SL_GROUP_NAMES = {
|
|
36169
|
+
A: "Session and tier",
|
|
36170
|
+
B: "Quota and spend",
|
|
36171
|
+
C: "Run state",
|
|
36172
|
+
K: "Cache health",
|
|
36173
|
+
L: "Session mode and identity",
|
|
36174
|
+
D: "Knowledge",
|
|
36175
|
+
E: "Extra (foreign dispatch)",
|
|
36176
|
+
F: "Flow and lanes",
|
|
36177
|
+
G: "Health and gates",
|
|
36178
|
+
T: "One subagent (the agent-panel row)",
|
|
36179
|
+
H: "Project and VCS",
|
|
36180
|
+
I: "Static and structural",
|
|
36181
|
+
J: "Composite shortcuts",
|
|
36182
|
+
};
|
|
36183
|
+
|
|
36184
|
+
// A dropdown of the word `braille-bar` tells nobody anything, so every renderer
|
|
36185
|
+
// ships a rendered SAMPLE at this component's real shape.
|
|
36186
|
+
function slRendererPreviews(comp) {
|
|
36187
|
+
const out = {};
|
|
36188
|
+
for (const r of comp.renderers) {
|
|
36189
|
+
try {
|
|
36190
|
+
const layout = slBlankLayout();
|
|
36191
|
+
layout.ansi = "off";
|
|
36192
|
+
layout.lines[0].items = [{ id: "p1", type: comp.id, render: r }];
|
|
36193
|
+
const { compiled } = slCompile(layout);
|
|
36194
|
+
out[r] = slPreview(compiled, "healthy", 120, {}).text;
|
|
36195
|
+
} catch (_) {
|
|
36196
|
+
out[r] = null;
|
|
36197
|
+
}
|
|
36198
|
+
}
|
|
36199
|
+
return out;
|
|
36200
|
+
}
|
|
36201
|
+
|
|
36202
|
+
function slLoadOrDefault(claudeDir) {
|
|
36203
|
+
const b = slBoard();
|
|
36204
|
+
return slReadLayout(claudeDir, b) || slBuildPreset(b.id === "subagent" ? "agent-default" : "orc-default");
|
|
36205
|
+
}
|
|
36206
|
+
|
|
36207
|
+
function slShowCmd(claudeDir) {
|
|
36208
|
+
const layout = slReadLayout(claudeDir);
|
|
36209
|
+
const eff = layout || slBuildPreset("orc-default");
|
|
36210
|
+
const v = slValidate(eff);
|
|
36211
|
+
const cfg = resolvedConfig(claudeDir);
|
|
36212
|
+
const on = String(cfg.statusline_custom || "off") === "on";
|
|
36213
|
+
let preview = null;
|
|
36214
|
+
try {
|
|
36215
|
+
preview = slPreview(slCompile(eff).compiled, "healthy", 120, {}).text;
|
|
36216
|
+
} catch (_) {}
|
|
36217
|
+
const lines = eff.lines.map((l, i) => ({
|
|
36218
|
+
line: i + 1,
|
|
36219
|
+
separator: l.separator,
|
|
36220
|
+
theme: l.theme,
|
|
36221
|
+
max_width: l.max_width || 0,
|
|
36222
|
+
count: (l.items || []).length,
|
|
36223
|
+
counted: (l.items || []).filter((it) => !slIsStructural(it.type)).length,
|
|
36224
|
+
full: (l.items || []).filter((it) => !slIsStructural(it.type)).length >= 5,
|
|
36225
|
+
items: (l.items || []).map((it, pi) => {
|
|
36226
|
+
const r = slResolveItem(it, eff, l);
|
|
36227
|
+
return {
|
|
36228
|
+
pos: pi + 1,
|
|
36229
|
+
id: it.id,
|
|
36230
|
+
type: it.type,
|
|
36231
|
+
render: r ? r.render : it.render,
|
|
36232
|
+
label: r ? r.label : null,
|
|
36233
|
+
label_color: r ? r.label_color || null : null,
|
|
36234
|
+
value_color: r ? r.value_color || null : null,
|
|
36235
|
+
ramp: r ? r.ramp || null : null,
|
|
36236
|
+
emphasis: r ? r.emphasis || [] : [],
|
|
36237
|
+
hide_when: r ? r.hide_when || [] : [],
|
|
36238
|
+
unknown: r ? r.unknown : null,
|
|
36239
|
+
known: SL_BY_ID.has(it.type),
|
|
36240
|
+
};
|
|
36241
|
+
}),
|
|
36242
|
+
}));
|
|
36243
|
+
if (wantsJson())
|
|
36244
|
+
return emitJson({
|
|
36245
|
+
ok: v.ok,
|
|
36246
|
+
enabled: on,
|
|
36247
|
+
saved: !!layout,
|
|
36248
|
+
preset: eff.preset,
|
|
36249
|
+
theme: eff.theme,
|
|
36250
|
+
glyphs: eff.glyphs,
|
|
36251
|
+
ansi: eff.ansi,
|
|
36252
|
+
align_columns: !!eff.align_columns,
|
|
36253
|
+
lines,
|
|
36254
|
+
errors: v.errors,
|
|
36255
|
+
warnings: v.warnings,
|
|
36256
|
+
preview,
|
|
36257
|
+
dense_prefix: "A line may hold a component only if every line above it holds at least one.",
|
|
36258
|
+
}, v.ok ? 0 : 1);
|
|
36259
|
+
console.log("");
|
|
36260
|
+
console.log(ui.header("Status line") + " " + (on ? ui.mark.ok("custom layout ON") : ui.color.gray("custom layout off — the shipped two lines are rendering")));
|
|
36261
|
+
if (!layout) console.log(ui.color.gray(" nothing saved yet — this is the shipped default"));
|
|
36262
|
+
console.log("");
|
|
36263
|
+
for (const l of lines) {
|
|
36264
|
+
const cells = l.items.map((i) => ui.color.cyan(i.type) + ui.color.gray("/" + i.render)).join(ui.color.gray(" · "));
|
|
36265
|
+
console.log(` ${ui.color.gray("line " + l.line)} ${cells || ui.color.gray("(empty)")} ${l.full ? ui.color.yellow("5/5") : ui.color.gray(l.counted + "/5")}`);
|
|
36266
|
+
}
|
|
36267
|
+
if (preview) {
|
|
36268
|
+
console.log("");
|
|
36269
|
+
console.log(ui.color.gray(" preview (fixture: a healthy session mid-run)"));
|
|
36270
|
+
for (const row of preview.split("\n")) console.log(" " + row);
|
|
36271
|
+
}
|
|
36272
|
+
for (const e of v.errors) console.log(" " + ui.mark.bad(e));
|
|
36273
|
+
for (const w of v.warnings) console.log(" " + ui.mark.warn(w));
|
|
36274
|
+
console.log("");
|
|
36275
|
+
if (!v.ok) process.exit(1);
|
|
36276
|
+
}
|
|
36277
|
+
|
|
36278
|
+
// The resolution chain for ONE item — which of catalogue / theme / file / line /
|
|
36279
|
+
// item set each field. `orc lane config`'s answer, at component scale. Without
|
|
36280
|
+
// it a user cannot find their own overrides again after a theme change.
|
|
36281
|
+
function slExplainCmd(claudeDir, ref) {
|
|
36282
|
+
const layout = slLoadOrDefault(claudeDir);
|
|
36283
|
+
const at = slParseRef(ref);
|
|
36284
|
+
if (!at) {
|
|
36285
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "bad-ref", hint: "use <line>:<pos>, e.g. 1:2" }, 2);
|
|
36286
|
+
console.error("usage: orc statusline explain <line>:<pos>");
|
|
36287
|
+
process.exit(2);
|
|
36288
|
+
}
|
|
36289
|
+
const line = layout.lines[at.line - 1];
|
|
36290
|
+
const item = line && (line.items || [])[at.pos - 1];
|
|
36291
|
+
if (!item) {
|
|
36292
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "no-item", line: at.line, pos: at.pos }, 1);
|
|
36293
|
+
console.error(`there is no component at line ${at.line} position ${at.pos}`);
|
|
36294
|
+
process.exit(1);
|
|
36295
|
+
}
|
|
36296
|
+
const r = slResolveItem(item, layout, line);
|
|
36297
|
+
const chain = Object.entries(r._from).map(([field, source]) => ({ field, value: r[field], source }));
|
|
36298
|
+
if (wantsJson()) return emitJson({ ok: true, id: item.id, type: item.type, line: at.line, pos: at.pos, resolved: chain, order: ["catalogue", "theme", "file", "line", "item"] }, 0);
|
|
36299
|
+
console.log("");
|
|
36300
|
+
console.log(ui.header(item.type) + ui.color.gray(` line ${at.line} position ${at.pos}`));
|
|
36301
|
+
console.log("");
|
|
36302
|
+
for (const c of chain) console.log(` ${String(c.field).padEnd(16)} ${String(JSON.stringify(c.value)).padEnd(24)} ${ui.color.gray("← " + c.source)}`);
|
|
36303
|
+
console.log("");
|
|
36304
|
+
console.log(ui.color.gray(" catalogue → theme → file → line → item. Later wins."));
|
|
36305
|
+
console.log("");
|
|
36306
|
+
}
|
|
36307
|
+
|
|
36308
|
+
function slValidateCmd(claudeDir) {
|
|
36309
|
+
const layout = slLoadOrDefault(claudeDir);
|
|
36310
|
+
const v = slValidate(layout);
|
|
36311
|
+
if (wantsJson()) return emitJson({ ok: v.ok, errors: v.errors, warnings: v.warnings }, v.ok ? 0 : 1);
|
|
36312
|
+
console.log("");
|
|
36313
|
+
if (v.ok) console.log(" " + ui.mark.ok("the layout is valid"));
|
|
36314
|
+
for (const e of v.errors) console.log(" " + ui.mark.bad(e));
|
|
36315
|
+
for (const w of v.warnings) console.log(" " + ui.mark.warn(w));
|
|
36316
|
+
console.log("");
|
|
36317
|
+
process.exit(v.ok ? 0 : 1);
|
|
36318
|
+
}
|
|
36319
|
+
|
|
36320
|
+
function slPreviewCmd(claudeDir) {
|
|
36321
|
+
const layout = slLoadOrDefault(claudeDir);
|
|
36322
|
+
const v = slValidate(layout);
|
|
36323
|
+
if (!v.ok) {
|
|
36324
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "invalid", errors: v.errors }, 1);
|
|
36325
|
+
console.error("the layout does not validate:\n " + v.errors.join("\n "));
|
|
36326
|
+
process.exit(1);
|
|
36327
|
+
}
|
|
36328
|
+
const cols = Number(flag("--width")) || 120;
|
|
36329
|
+
const state = String(flag("--state") || "healthy");
|
|
36330
|
+
const overrides = {};
|
|
36331
|
+
for (let i = 0; i < args.length; i++) {
|
|
36332
|
+
if (args[i] === "--set" && args[i + 1]) {
|
|
36333
|
+
const [k, val] = String(args[i + 1]).split("=");
|
|
36334
|
+
if (k) overrides[k] = val;
|
|
36335
|
+
}
|
|
36336
|
+
}
|
|
36337
|
+
const { compiled } = slCompile(layout);
|
|
36338
|
+
const base = slPreview(compiled, state, cols, overrides);
|
|
36339
|
+
// R4: every design must survive three strippings, and the panel shows all
|
|
36340
|
+
// three as small rows under the main preview — not as an afterthought tab.
|
|
36341
|
+
const strip = (env) => {
|
|
36342
|
+
const fx = SL_FIXTURES[state] || SL_FIXTURES.healthy;
|
|
36343
|
+
const now = 1767225600000;
|
|
36344
|
+
const payload = JSON.parse(JSON.stringify(fx.payload));
|
|
36345
|
+
for (const [k, val] of Object.entries(overrides)) slDeepSet(payload, k, val);
|
|
36346
|
+
return slEngine().render(compiled, {
|
|
36347
|
+
payload,
|
|
36348
|
+
ledger: Object.assign({ started_at: now - 2880000 }, fx.ledger),
|
|
36349
|
+
scan: fx.scan || {},
|
|
36350
|
+
derived: slDerived(payload),
|
|
36351
|
+
task: SL_TASK_FIXTURES[state] || SL_TASK_FIXTURES.healthy,
|
|
36352
|
+
now,
|
|
36353
|
+
cols,
|
|
36354
|
+
env: Object.assign({}, process.env, { COLUMNS: String(cols) }, env),
|
|
36355
|
+
}).text;
|
|
36356
|
+
};
|
|
36357
|
+
const out = {
|
|
36358
|
+
ok: true,
|
|
36359
|
+
width: cols,
|
|
36360
|
+
state,
|
|
36361
|
+
fixture: (SL_FIXTURES[state] || SL_FIXTURES.healthy).label,
|
|
36362
|
+
fixtures: Object.fromEntries(Object.entries(SL_FIXTURES).map(([k, f]) => [k, f.label])),
|
|
36363
|
+
text: base.text,
|
|
36364
|
+
strippings: {
|
|
36365
|
+
no_color: strip({ NO_COLOR: "1" }),
|
|
36366
|
+
ascii: strip({ ORC_STATUSLINE_ASCII: "1" }),
|
|
36367
|
+
no_motion: strip({ ORC_STATUSLINE_MOTION: "0" }),
|
|
36368
|
+
},
|
|
36369
|
+
static_width: compiled.lines.map((l) => l.static_width),
|
|
36370
|
+
errors: base.errors,
|
|
36371
|
+
};
|
|
36372
|
+
if (wantsJson()) return emitJson(out, 0);
|
|
36373
|
+
console.log("");
|
|
36374
|
+
for (const row of base.text.split("\n")) console.log(row);
|
|
36375
|
+
console.log("");
|
|
36376
|
+
console.log(ui.color.gray(" NO_COLOR"));
|
|
36377
|
+
for (const row of out.strippings.no_color.split("\n")) console.log(" " + row);
|
|
36378
|
+
console.log(ui.color.gray(" ORC_STATUSLINE_ASCII=1"));
|
|
36379
|
+
for (const row of out.strippings.ascii.split("\n")) console.log(" " + row);
|
|
36380
|
+
console.log(ui.color.gray(" ORC_STATUSLINE_MOTION=0"));
|
|
36381
|
+
for (const row of out.strippings.no_motion.split("\n")) console.log(" " + row);
|
|
36382
|
+
console.log("");
|
|
36383
|
+
}
|
|
36384
|
+
|
|
36385
|
+
function slParseRef(s) {
|
|
36386
|
+
const m = /^(\d+):(\d+)$/.exec(String(s || ""));
|
|
36387
|
+
if (!m) return null;
|
|
36388
|
+
return { line: Number(m[1]), pos: Number(m[2]) };
|
|
36389
|
+
}
|
|
36390
|
+
|
|
36391
|
+
// Places or edits ONE item. Refuses an illegal placement BY NAME — the panel
|
|
36392
|
+
// makes the illegal drop impossible, but the panel is a convenience and this is
|
|
36393
|
+
// the guarantee.
|
|
36394
|
+
function slSetCmd(claudeDir, p) {
|
|
36395
|
+
const layout = slLoadOrDefault(claudeDir);
|
|
36396
|
+
const li = Number(p[2]);
|
|
36397
|
+
const pi = Number(p[3]);
|
|
36398
|
+
const type = p[4];
|
|
36399
|
+
if (!li || li < 1 || li > 3 || !pi || pi < 1) {
|
|
36400
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "bad-position", hint: "orc statusline set <line 1-3> <pos> <type>" }, 2);
|
|
36401
|
+
console.error("usage: orc statusline set <line 1-3> <pos> <type> [--render R] …");
|
|
36402
|
+
process.exit(2);
|
|
36403
|
+
}
|
|
36404
|
+
const line = layout.lines[li - 1];
|
|
36405
|
+
const existing = (line.items || [])[pi - 1];
|
|
36406
|
+
if (!type && !existing) {
|
|
36407
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "no-type", hint: "name a component id, or edit an existing position" }, 2);
|
|
36408
|
+
console.error("there is nothing at that position — name a component id");
|
|
36409
|
+
process.exit(2);
|
|
36410
|
+
}
|
|
36411
|
+
const item = existing || { id: slNewId(layout), type };
|
|
36412
|
+
if (type) item.type = type;
|
|
36413
|
+
for (const [k, f] of [["render", "--render"], ["label", "--label"], ["color", "--color"], ["label_color", "--label-color"], ["value_color", "--value-color"], ["bg", "--bg"], ["ramp", "--ramp"], ["glyphs", "--glyphs"], ["format", "--format"], ["case", "--case"], ["truncate", "--truncate"], ["compact", "--compact"], ["prefix", "--prefix"], ["suffix", "--suffix"]]) {
|
|
36414
|
+
const v = flag(f);
|
|
36415
|
+
if (typeof v === "string") item[k] = v;
|
|
36416
|
+
}
|
|
36417
|
+
for (const [k, f] of [["width", "--width"], ["precision", "--precision"], ["min_width", "--min-width"], ["min_cols", "--min-cols"], ["max_cols", "--max-cols"], ["priority", "--priority"]]) {
|
|
36418
|
+
const v = flag(f);
|
|
36419
|
+
if (typeof v === "string" && v !== "") item[k] = Number(v);
|
|
36420
|
+
}
|
|
36421
|
+
const emph = flag("--emphasis");
|
|
36422
|
+
if (typeof emph === "string") item.emphasis = emph.split(",").map((s) => s.trim()).filter(Boolean);
|
|
36423
|
+
const hide = flag("--hide-when");
|
|
36424
|
+
if (typeof hide === "string") item.hide_when = hide === "never" ? [] : hide.split(",").map((s) => s.trim()).filter(Boolean);
|
|
36425
|
+
if (flag("--draw-empty") === true) item.draw_empty = true;
|
|
36426
|
+
if (!existing) {
|
|
36427
|
+
line.items = line.items || [];
|
|
36428
|
+
line.items.splice(Math.min(pi - 1, line.items.length), 0, item);
|
|
36429
|
+
}
|
|
36430
|
+
return slSaveAndCompile(claudeDir, layout, { action: "set", id: item.id, line: li, pos: pi, type: item.type });
|
|
36431
|
+
}
|
|
36432
|
+
|
|
36433
|
+
function slMoveCmd(claudeDir, p) {
|
|
36434
|
+
const layout = slLoadOrDefault(claudeDir);
|
|
36435
|
+
const from = slParseRef(p[2]);
|
|
36436
|
+
const to = slParseRef(p[3]);
|
|
36437
|
+
if (!from || !to || to.line < 1 || to.line > 3) {
|
|
36438
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "bad-ref", hint: "orc statusline move <line>:<pos> <line>:<pos>" }, 2);
|
|
36439
|
+
console.error("usage: orc statusline move <line>:<pos> <line>:<pos>");
|
|
36440
|
+
process.exit(2);
|
|
36441
|
+
}
|
|
36442
|
+
const src = layout.lines[from.line - 1];
|
|
36443
|
+
const item = src && (src.items || [])[from.pos - 1];
|
|
36444
|
+
if (!item) {
|
|
36445
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "no-item", line: from.line, pos: from.pos }, 1);
|
|
36446
|
+
console.error(`there is no component at line ${from.line} position ${from.pos}`);
|
|
36447
|
+
process.exit(1);
|
|
36448
|
+
}
|
|
36449
|
+
src.items.splice(from.pos - 1, 1);
|
|
36450
|
+
const dst = layout.lines[to.line - 1];
|
|
36451
|
+
dst.items = dst.items || [];
|
|
36452
|
+
dst.items.splice(Math.min(to.pos - 1, dst.items.length), 0, item);
|
|
36453
|
+
return slSaveAndCompile(claudeDir, layout, { action: "move", id: item.id, from, to });
|
|
36454
|
+
}
|
|
36455
|
+
|
|
36456
|
+
function slRemoveCmd(claudeDir, p) {
|
|
36457
|
+
const layout = slLoadOrDefault(claudeDir);
|
|
36458
|
+
const at = slParseRef(p[2]);
|
|
36459
|
+
if (!at) {
|
|
36460
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "bad-ref", hint: "orc statusline remove <line>:<pos>" }, 2);
|
|
36461
|
+
console.error("usage: orc statusline remove <line>:<pos>");
|
|
36462
|
+
process.exit(2);
|
|
36463
|
+
}
|
|
36464
|
+
const line = layout.lines[at.line - 1];
|
|
36465
|
+
const item = line && (line.items || [])[at.pos - 1];
|
|
36466
|
+
if (!item) {
|
|
36467
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "no-item", line: at.line, pos: at.pos }, 1);
|
|
36468
|
+
console.error(`there is no component at line ${at.line} position ${at.pos}`);
|
|
36469
|
+
process.exit(1);
|
|
36470
|
+
}
|
|
36471
|
+
line.items.splice(at.pos - 1, 1);
|
|
36472
|
+
return slSaveAndCompile(claudeDir, layout, { action: "remove", id: item.id, line: at.line, pos: at.pos });
|
|
36473
|
+
}
|
|
36474
|
+
|
|
36475
|
+
function slLineCmd(claudeDir, p) {
|
|
36476
|
+
const layout = slLoadOrDefault(claudeDir);
|
|
36477
|
+
const n = Number(p[2]);
|
|
36478
|
+
if (!n || n < 1 || n > 3) {
|
|
36479
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "bad-line" }, 2);
|
|
36480
|
+
console.error('usage: orc statusline line <1-3> [--separator "…"] [--max-width N] [--theme T] [--prefix "…"]');
|
|
36481
|
+
process.exit(2);
|
|
36482
|
+
}
|
|
36483
|
+
const line = layout.lines[n - 1];
|
|
36484
|
+
const sep = flag("--separator");
|
|
36485
|
+
if (typeof sep === "string") line.separator = sep;
|
|
36486
|
+
const pre = flag("--prefix");
|
|
36487
|
+
if (typeof pre === "string") line.prefix = pre;
|
|
36488
|
+
const mw = flag("--max-width");
|
|
36489
|
+
if (typeof mw === "string") line.max_width = Number(mw) || 0;
|
|
36490
|
+
const th = flag("--theme");
|
|
36491
|
+
if (typeof th === "string") {
|
|
36492
|
+
if (!STATUSLINE_THEMES[th]) {
|
|
36493
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "unknown-theme", theme: th, known: Object.keys(STATUSLINE_THEMES) }, 2);
|
|
36494
|
+
console.error(`unknown theme "${th}" — known: ${Object.keys(STATUSLINE_THEMES).join(", ")}`);
|
|
36495
|
+
process.exit(2);
|
|
36496
|
+
}
|
|
36497
|
+
line.theme = th;
|
|
36498
|
+
}
|
|
36499
|
+
return slSaveAndCompile(claudeDir, layout, { action: "line", line: n });
|
|
36500
|
+
}
|
|
36501
|
+
|
|
36502
|
+
function slPresetsCmd(claudeDir) {
|
|
36503
|
+
const layout = slReadLayout(claudeDir);
|
|
36504
|
+
const rows = slPresetsFor(slBoard().id).map(([name, p]) => {
|
|
36505
|
+
let preview = null;
|
|
36506
|
+
try {
|
|
36507
|
+
preview = slPreview(slCompile(slBuildPreset(name)).compiled, "healthy", 120, {}).text;
|
|
36508
|
+
} catch (_) {}
|
|
36509
|
+
return { name, summary: p.summary, board: p.board || "status", active: layout ? slPresetActive(layout, name) : false, preview };
|
|
36510
|
+
});
|
|
36511
|
+
if (wantsJson()) return emitJson({ ok: true, presets: rows }, 0);
|
|
36512
|
+
console.log("");
|
|
36513
|
+
console.log(ui.header("Presets"));
|
|
36514
|
+
for (const r of rows) {
|
|
36515
|
+
console.log("");
|
|
36516
|
+
console.log(" " + (r.active ? ui.mark.ok(r.name) : ui.color.cyan(r.name)) + " " + ui.color.gray(r.summary));
|
|
36517
|
+
if (r.preview) for (const row of r.preview.split("\n")) console.log(" " + row);
|
|
36518
|
+
}
|
|
36519
|
+
console.log("");
|
|
36520
|
+
}
|
|
36521
|
+
|
|
36522
|
+
function slApplyCmd(claudeDir, name) {
|
|
36523
|
+
const mine = slPresetsFor(slBoard().id).map(([n]) => n);
|
|
36524
|
+
if (!name || !STATUSLINE_PRESETS[name]) {
|
|
36525
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "unknown-preset", known: mine }, 2);
|
|
36526
|
+
console.error(`usage: orc statusline apply <${mine.join("|")}>`);
|
|
36527
|
+
process.exit(2);
|
|
36528
|
+
}
|
|
36529
|
+
// A preset from the OTHER board is refused by name, with the flag that would
|
|
36530
|
+
// have worked. Applying it would replace this board's layout with components
|
|
36531
|
+
// it cannot hold, and the validator would then refuse every one of them.
|
|
36532
|
+
if ((STATUSLINE_PRESETS[name].board || "status") !== slBoard().id) {
|
|
36533
|
+
const other = STATUSLINE_PRESETS[name].board || "status";
|
|
36534
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "wrong-board", preset: name, board: other, hint: `orc statusline apply ${name} --board ${other}` }, 2);
|
|
36535
|
+
console.error(`"${name}" is a ${other}-board preset — try: orc statusline apply ${name} --board ${other}`);
|
|
36536
|
+
process.exit(2);
|
|
36537
|
+
}
|
|
36538
|
+
// `apply` REPLACES the layout, so the loss is NAMED. A count is not consent.
|
|
36539
|
+
const before = slReadLayout(claudeDir);
|
|
36540
|
+
const lost = before ? before.lines.reduce((n, l) => n + (l.items || []).length, 0) : 0;
|
|
36541
|
+
const layout = slBuildPreset(name);
|
|
36542
|
+
const res = slSaveAndCompile(claudeDir, layout, { action: "apply", preset: name, replaced_items: lost });
|
|
36543
|
+
return res;
|
|
36544
|
+
}
|
|
36545
|
+
|
|
36546
|
+
function slResetCmd(claudeDir) {
|
|
36547
|
+
const before = slReadLayout(claudeDir);
|
|
36548
|
+
const lost = before ? before.lines.reduce((n, l) => n + (l.items || []).length, 0) : 0;
|
|
36549
|
+
const dflt = slBoard().id === "subagent" ? "agent-default" : "orc-default";
|
|
36550
|
+
return slSaveAndCompile(claudeDir, slBuildPreset(dflt), { action: "reset", replaced_items: lost });
|
|
36551
|
+
}
|
|
36552
|
+
|
|
36553
|
+
// `compile` runs AUTOMATICALLY after every mutating command, so there is no
|
|
36554
|
+
// "you forgot to compile" state to fall into — the one real ergonomic complaint
|
|
36555
|
+
// about `orc diy`. The standalone form is for after an upgrade and for the
|
|
36556
|
+
// panel's explicit recompile button.
|
|
36557
|
+
function slCompileCmd(claudeDir, standalone) {
|
|
36558
|
+
const layout = slLoadOrDefault(claudeDir);
|
|
36559
|
+
return slSaveAndCompile(claudeDir, layout, { action: "compile" }, !standalone);
|
|
36560
|
+
}
|
|
36561
|
+
|
|
36562
|
+
function slSaveAndCompile(claudeDir, layout, info, skipWrite) {
|
|
36563
|
+
const v = slValidate(layout);
|
|
36564
|
+
if (!v.ok) {
|
|
36565
|
+
// A structural failure is a REFUSAL. Nothing is written.
|
|
36566
|
+
if (wantsJson()) return emitJson({ ok: false, reason: "invalid", action: info.action, errors: v.errors, warnings: v.warnings, wrote: false }, 1);
|
|
36567
|
+
console.error("");
|
|
36568
|
+
for (const e of v.errors) console.error(" " + ui.mark.bad(e));
|
|
36569
|
+
console.error("");
|
|
36570
|
+
console.error(ui.color.gray(" nothing was written."));
|
|
36571
|
+
process.exit(1);
|
|
36572
|
+
}
|
|
36573
|
+
const paths = slPaths(claudeDir, SL_BOARDS[layout.board] || SL_BOARDS.status);
|
|
36574
|
+
const { compiled, lock } = slCompile(layout);
|
|
36575
|
+
lock.warnings = v.warnings;
|
|
36576
|
+
fs.mkdirSync(paths.dir, { recursive: true });
|
|
36577
|
+
if (!skipWrite) slWriteLayout(claudeDir, layout);
|
|
36578
|
+
fs.writeFileSync(paths.compiled, JSON.stringify(compiled, null, 2) + "\n");
|
|
36579
|
+
fs.writeFileSync(paths.lock, JSON.stringify(lock, null, 2) + "\n");
|
|
36580
|
+
let preview = null;
|
|
36581
|
+
try {
|
|
36582
|
+
preview = slPreview(compiled, "healthy", 120, {}).text;
|
|
36583
|
+
} catch (_) {}
|
|
36584
|
+
const out = Object.assign({ ok: true, wrote: true, warnings: v.warnings, needs_refresh_interval: lock.needs_refresh_interval, static_width: compiled.lines.map((l) => l.static_width), preview }, info);
|
|
36585
|
+
if (wantsJson()) return emitJson(out, 0);
|
|
36586
|
+
console.log("");
|
|
36587
|
+
console.log(" " + ui.mark.ok(`${info.action} — compiled`));
|
|
36588
|
+
if (preview) for (const row of preview.split("\n")) console.log(" " + row);
|
|
36589
|
+
for (const w of v.warnings) console.log(" " + ui.mark.warn(w));
|
|
36590
|
+
if (lock.needs_refresh_interval)
|
|
36591
|
+
console.log(
|
|
36592
|
+
" " +
|
|
36593
|
+
ui.color.gray(
|
|
36594
|
+
`this layout shows time. Without \`refreshInterval\` in .claude/settings.json it updates only when a message arrives — and Claude Code's triggers go quiet exactly while a subagent runs.`
|
|
36595
|
+
)
|
|
36596
|
+
);
|
|
36597
|
+
const cfg = resolvedConfig(claudeDir);
|
|
36598
|
+
if (String(cfg.statusline_custom || "off") !== "on")
|
|
36599
|
+
console.log(" " + ui.color.gray("the custom layout is OFF — turn it on with `orc config set statusline_custom on`"));
|
|
36600
|
+
console.log("");
|
|
36601
|
+
}
|
|
36602
|
+
|
|
36603
|
+
|
|
34000
36604
|
// ── the `--json` crash envelope (v0.49.2) ───────────────────────────────────
|
|
34001
36605
|
// A read asked for JSON must answer in JSON or not at all. Before this, an
|
|
34002
36606
|
// unexpected throw inside any `--json` route printed a Node stack to stderr and
|
|
@@ -34117,6 +36721,12 @@ function jsonCrash(err) {
|
|
|
34117
36721
|
process.exit(1);
|
|
34118
36722
|
}
|
|
34119
36723
|
break;
|
|
36724
|
+
// v1.3.0 W1 — the CLI Hook Interface. `orc ui` is the recommended surface;
|
|
36725
|
+
// these subcommands exist so the panel has something to shell, which is
|
|
36726
|
+
// what makes UI/CLI drift structurally impossible.
|
|
36727
|
+
case "statusline":
|
|
36728
|
+
statusline();
|
|
36729
|
+
break;
|
|
34120
36730
|
case "ui":
|
|
34121
36731
|
uiCmd();
|
|
34122
36732
|
break;
|