@azure-id/orc 1.9.2 → 2.1.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 +722 -0
- package/README-id.md +56 -42
- package/README.md +58 -41
- package/bin/build-agents.js +125 -29
- package/bin/clear-logs.js +620 -0
- package/bin/cli.js +1873 -180
- package/bin/fix.js +459 -0
- package/bin/gotcha-import.js +1081 -0
- package/bin/gotcha.js +1566 -0
- package/bin/graph-query.js +1 -1
- package/bin/graph.js +717 -717
- package/bin/habit.js +1538 -0
- package/bin/mockrun-catalog.js +284 -276
- package/bin/onboarding-content.js +2 -1
- package/bin/pricing.json +28 -21
- package/bin/run-undo.js +398 -0
- package/bin/trace-write.js +869 -0
- package/bin/verify-contracts.js +627 -100
- package/bin/verify-package.js +68 -10
- package/bin/webui/api.js +151 -6
- package/bin/webui/app.html +239 -232
- package/bin/webui/css/00-tokens.css +110 -92
- package/bin/webui/css/04-motion.css +87 -0
- package/bin/webui/css/06-responsive.css +203 -178
- package/bin/webui/css/panels/behaviour.css +205 -0
- package/bin/webui/css/panels/hookui.css +79 -0
- package/bin/webui/fixtures/behaviour.js +562 -0
- package/bin/webui/fixtures/extra.js +9 -9
- package/bin/webui/fixtures/flow.js +81 -81
- package/bin/webui/fixtures/hookui.js +506 -431
- package/bin/webui/fixtures/index.js +66 -3
- package/bin/webui/fixtures/knowledge.js +9 -3
- package/bin/webui/fixtures/maintenance.js +123 -42
- package/bin/webui/fixtures/rules.js +1007 -1002
- package/bin/webui/fixtures/settings.js +6 -7
- package/bin/webui/fixtures/stats.js +25 -6
- package/bin/webui/fixtures/wait.js +98 -97
- package/bin/webui/i18n/en/behaviour.json +156 -0
- package/bin/webui/i18n/en/hookui.json +290 -194
- package/bin/webui/i18n/en/maintenance.json +71 -52
- package/bin/webui/i18n/en/nav.json +25 -24
- package/bin/webui/i18n/en/settings.json +60 -60
- package/bin/webui/i18n/en/tour.json +37 -35
- package/bin/webui/i18n/id/behaviour.json +156 -0
- package/bin/webui/i18n/id/hookui.json +290 -194
- package/bin/webui/i18n/id/maintenance.json +71 -52
- package/bin/webui/i18n/id/nav.json +25 -24
- package/bin/webui/i18n/id/settings.json +60 -60
- package/bin/webui/i18n/id/tour.json +37 -35
- package/bin/webui/js/01-i18n.js +155 -154
- package/bin/webui/js/05-banners.js +181 -172
- package/bin/webui/js/06-edit.js +193 -183
- package/bin/webui/js/90-tour.js +498 -494
- package/bin/webui/js/91-shortcuts.js +126 -126
- package/bin/webui/js/99-boot.js +121 -118
- package/bin/webui/js/panels/behaviour.js +1087 -0
- package/bin/webui/js/panels/hookui.js +2196 -1691
- package/bin/webui/js/panels/maintenance.js +336 -235
- package/bin/webui/js/panels/settings.js +726 -719
- package/mock-run/INDEX.md +110 -107
- package/mock-run/extra-slots.md +1 -1
- package/mock-run/gotcha-import.md +118 -0
- package/mock-run/habits.md +129 -0
- package/mock-run/orc-aftermath.md +392 -392
- package/mock-run/orc-budget.md +29 -33
- package/mock-run/orc-cli.md +200 -200
- package/mock-run/orc-explain.md +86 -86
- package/mock-run/orc-extra.md +389 -392
- package/mock-run/orc-fix.md +87 -0
- package/mock-run/orc-pattern.md +112 -112
- package/mock-run/orc-quick.md +6 -1
- package/mock-run/orc.md +157 -157
- package/package.json +1 -1
- package/templates/agents/MODEL-MAPPING.md +37 -34
- package/templates/agents/orc-advisor-opus-5-xhigh.md +1 -7
- package/templates/agents/orc-analyze-mini-opus-5-med.md +1 -6
- package/templates/agents/orc-analyze-mini-sonnet-5-high.md +2 -5
- package/templates/agents/{orc-claude-writer-opus-4-8-high.md → orc-claude-writer-opus-5-low.md} +48 -53
- package/templates/agents/orc-context-combiner-opus-5-high.md +1 -11
- package/templates/agents/orc-executor-haiku-4-5.md +14 -6
- package/templates/agents/orc-executor-opus-4-7-high.md +14 -6
- package/templates/agents/orc-executor-opus-4-7-med.md +14 -6
- package/templates/agents/orc-executor-opus-4-8-high.md +14 -6
- package/templates/agents/orc-executor-opus-5-high.md +14 -6
- package/templates/agents/orc-executor-opus-5-low.md +14 -6
- package/templates/agents/orc-executor-opus-5-med.md +14 -6
- package/templates/agents/orc-executor-sonnet-4-6-high.md +14 -6
- package/templates/agents/orc-executor-sonnet-4-6-med.md +14 -6
- package/templates/agents/orc-executor-sonnet-5-high.md +15 -7
- package/templates/agents/orc-executor-sonnet-5-low.md +155 -0
- package/templates/agents/orc-executor-sonnet-5-med.md +155 -0
- package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +1 -10
- package/templates/agents/orc-judge-opus-5-xhigh.md +5 -9
- package/templates/agents/orc-learn-writer-opus-5-low.md +1 -7
- package/templates/agents/orc-pattern-codifier-opus-5-med.md +1 -8
- package/templates/agents/orc-pattern-codifier-sonnet-5-high.md +58 -63
- package/templates/agents/orc-planner-mini-opus-5-med.md +1 -4
- package/templates/agents/orc-planner-mini-sonnet-5-high.md +2 -3
- package/templates/agents/orc-planner-opus-5-med.md +1 -4
- package/templates/agents/orc-recon-opus-5-low.md +1 -8
- package/templates/agents/orc-recon-sonnet-4-6-med.md +1 -8
- package/templates/agents/orc-retro-opus-5-med.md +6 -8
- package/templates/agents/orc-retro-sonnet-5-high.md +7 -8
- package/templates/agents/orc-reviewer-opus-5-low.md +96 -0
- package/templates/agents/orc-scout-opus-5-low.md +1 -6
- package/templates/agents/orc-scout-sonnet-4-6-high.md +35 -39
- package/templates/agents/orc-system-analyst-opus-5-high.md +1 -6
- package/templates/agents/orc-test-author-opus-5-med.md +4 -5
- package/templates/agents/orc-trace-writer-haiku-4-5.md +3 -7
- package/templates/agents/orc-verifier-opus-5-med.md +16 -8
- package/templates/agents/{orc-wiki-scanner-opus-4-8-high.md → orc-wiki-scanner-opus-5-low.md} +74 -79
- package/templates/agents/orc-wiki-scanner-sonnet-5-high.md +97 -106
- package/templates/commands/orc-analyze.md +13 -21
- package/templates/commands/orc-fast.md +10 -15
- package/templates/commands/orc-fix.md +18 -0
- package/templates/commands/orc-poly.md +12 -21
- package/templates/commands/orc-pr-driver.md +11 -30
- package/templates/commands/orc-pr-setup.md +10 -31
- package/templates/commands/orc-route.md +11 -41
- package/templates/commands/orc-test.md +5 -60
- package/templates/commands/orc-wiki.md +64 -64
- package/templates/hooks/README.md +109 -7
- package/templates/hooks/orc-session-hook.js +339 -0
- package/templates/hooks/orc-statusline-render.js +317 -23
- package/templates/hooks/orc-statusline.js +165 -50
- package/templates/hooks/orc-subagent-line.js +23 -5
- package/templates/hooks/orc-weather-fetch.js +123 -0
- package/templates/skills/_shared/README.md +9 -0
- package/templates/skills/_shared/code-graph.md +47 -55
- package/templates/skills/_shared/config-precedence.md +3 -1
- package/templates/skills/_shared/extra-dispatch.md +78 -93
- package/templates/skills/_shared/gotchas.md +248 -177
- package/templates/skills/_shared/habits.md +101 -0
- package/templates/skills/_shared/lane-contract.md +84 -0
- package/templates/skills/_shared/opus5-only.md +14 -14
- package/templates/skills/_shared/phases/README.md +142 -83
- package/templates/skills/_shared/phases/analyst-gates.md +10 -21
- package/templates/skills/_shared/phases/execution.md +8 -14
- package/templates/skills/_shared/phases/house-rules.md +27 -32
- package/templates/skills/_shared/phases/intake.md +127 -133
- package/templates/skills/_shared/phases/mock-example.md +46 -56
- package/templates/skills/_shared/phases/plan-handoff.md +91 -97
- package/templates/skills/_shared/phases/planning.md +7 -17
- package/templates/skills/_shared/phases/preflight.md +19 -42
- package/templates/skills/_shared/phases/review.md +24 -28
- package/templates/skills/_shared/phases/rules.md +18 -42
- package/templates/skills/_shared/phases/scoring.md +55 -65
- package/templates/skills/_shared/phases/security-checklist.md +46 -50
- package/templates/skills/_shared/phases/security.md +45 -55
- package/templates/skills/_shared/phases/ship.md +6 -15
- package/templates/skills/_shared/phases/stop-resume.md +2 -5
- package/templates/skills/_shared/phases/summary.md +73 -48
- package/templates/skills/_shared/phases/testgen.md +41 -51
- package/templates/skills/_shared/phases/trace-verbs.md +447 -0
- package/templates/skills/_shared/phases/trace.md +66 -297
- package/templates/skills/_shared/phases/verify.md +62 -70
- package/templates/skills/_shared/phases/wave-grouping.md +128 -133
- package/templates/skills/_shared/phases/wiki-consult.md +10 -6
- package/templates/skills/_shared/read-ladder.md +2 -55
- package/templates/skills/_shared/return-validation.md +17 -70
- package/templates/skills/_shared/review-slice.md +106 -0
- package/templates/skills/_shared/smoke-gate.md +46 -28
- package/templates/skills/_shared/wait.md +1 -0
- package/templates/skills/context-combiner/SKILL.md +15 -44
- package/templates/skills/orc/README.md +2 -2
- package/templates/skills/orc/SKILL.md +35 -63
- package/templates/skills/orc/config.md +25 -27
- package/templates/skills/orc/examples/full-run-mock.md +2 -1
- package/templates/skills/orc/references/effort-and-mode.md +17 -17
- package/templates/skills/orc/references/pattern-gate.md +89 -89
- package/templates/skills/orc/references/phases/intake.md +41 -47
- package/templates/skills/orc/references/phases/integration.md +13 -19
- package/templates/skills/orc/references/preflight-report.md +11 -12
- package/templates/skills/orc/references/ultra-mode.md +9 -7
- package/templates/skills/orc/schemas/checkpoint.md +122 -122
- package/templates/skills/orc/subskills/orc-execution/SKILL.md +27 -73
- package/templates/skills/orc/subskills/orc-execution/core.md +12 -99
- package/templates/skills/orc/subskills/orc-execution/subagent.md +14 -13
- package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +12 -53
- package/templates/skills/orc/subskills/orc-review-verify/core.md +52 -135
- package/templates/skills/orc/subskills/orc-review-verify/subagent.md +7 -7
- package/templates/skills/orc/subskills/orc-testgen/SKILL.md +11 -22
- package/templates/skills/orc/subskills/orc-testgen/core.md +20 -59
- package/templates/skills/orc/subskills/orc-testgen/subagent.md +7 -7
- package/templates/skills/orc-advisor/SKILL.md +56 -60
- package/templates/skills/orc-analyze/SKILL.md +33 -67
- package/templates/skills/orc-analyze/schemas/report-audit.md +2 -1
- package/templates/skills/orc-analyze/schemas/report-prose.md +2 -1
- package/templates/skills/orc-analyze-mini/SKILL.md +35 -70
- package/templates/skills/orc-claude/SKILL.md +14 -14
- package/templates/skills/orc-claude/examples/claude-run-mock.md +65 -65
- package/templates/skills/orc-diy/README.md +31 -0
- package/templates/skills/orc-diy/SKILL.md +27 -78
- package/templates/skills/orc-diy/references/blocks/pattern.md +18 -18
- package/templates/skills/orc-fast/SKILL.md +49 -75
- package/templates/skills/orc-fix/SKILL.md +110 -0
- package/templates/skills/orc-judge/SKILL.md +77 -82
- package/templates/skills/orc-mini/SKILL.md +64 -112
- package/templates/skills/orc-mini/examples/mini-run-mock.md +4 -2
- package/templates/skills/orc-pattern/SKILL.md +27 -48
- package/templates/skills/orc-poly/SKILL.md +32 -61
- package/templates/skills/orc-pr-driver/SKILL.md +25 -51
- package/templates/skills/orc-pr-driver/references/green-gate.md +113 -105
- package/templates/skills/orc-pr-setup/SKILL.md +20 -45
- package/templates/skills/orc-quick/README.md +44 -3
- package/templates/skills/orc-quick/SKILL.md +77 -108
- package/templates/skills/orc-quick/references/dispatch-gate.md +18 -6
- package/templates/skills/orc-quick/references/gh-mode.md +50 -1
- package/templates/skills/orc-quick/references/look.md +3 -1
- package/templates/skills/orc-retro/SKILL.md +20 -19
- package/templates/skills/orc-retro/examples/retro-mock.md +17 -17
- package/templates/skills/orc-route/SKILL.md +25 -45
- package/templates/skills/orc-test/SKILL.md +16 -37
- package/templates/skills/orc-verify/SKILL.md +14 -32
- package/templates/skills/orc-wait/SKILL.md +156 -163
- package/templates/skills/orc-wiki/README.md +325 -325
- package/templates/skills/orc-wiki/SKILL.md +195 -195
- package/templates/skills/orc-wiki/examples/wiki-run-mock.md +37 -37
- package/templates/skills/orc-wiki/references/extra.md +2 -2
- package/templates/skills/orc-wiki/references/partial-refresh.md +276 -271
- package/templates/skills/orc-wiki/references/phases/phase-0.md +2 -7
- package/templates/skills/orc-wiki/references/phases/phase-1.md +1 -6
- package/templates/skills/orc-wiki/references/phases/phase-2.md +4 -9
- package/templates/skills/orc-wiki/references/phases/phase-3.md +1 -6
- package/templates/skills/orc-wiki/references/phases/phase-3c.md +4 -8
- package/templates/agents/orc-claude-writer-opus-5-med.md +0 -55
- package/templates/agents/orc-reviewer-opus-5-med.md +0 -60
- package/templates/agents/orc-wiki-scanner-opus-5-med.md +0 -81
|
@@ -1,719 +1,726 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/* panels/settings.js — orc ui client
|
|
3
|
-
The toolbar, the tier accordions, every control kind, the score ladder.
|
|
4
|
-
|
|
5
|
-
Loaded by app.html in the order its numeric prefix names. Classic
|
|
6
|
-
script, no import/export: an ES module import carries no query string,
|
|
7
|
-
and every static request here needs the per-launch session token. */
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
/* ================================================================ SETTINGS == */
|
|
11
|
-
|
|
12
|
-
// The tier IDS are the CLI's (`k.tier`); only their display name and blurb are
|
|
13
|
-
// ours to translate. A tier name alone does not say why a key lives there, and
|
|
14
|
-
// "advanced" reads as "do not touch" when it means "you need a reason".
|
|
15
|
-
//
|
|
16
|
-
// Written out as a literal map rather than `t("settings.tier." + tier)` so the
|
|
17
|
-
// keys stay GREPPABLE: a build of a string key from a fragment is a key no
|
|
18
|
-
// coverage check can see, and the check is the only thing standing between a
|
|
19
|
-
// renamed key and a raw dotted string on somebody's screen.
|
|
20
|
-
const TIER_LABEL_KEY = { common: "settings.tier.common", advanced: "settings.tier.advanced" };
|
|
21
|
-
const TIER_DESC_KEY = { common: "settings.tierDesc.common", advanced: "settings.tierDesc.advanced" };
|
|
22
|
-
const TIER_LABEL = (tier) => (TIER_LABEL_KEY[tier] ? t(TIER_LABEL_KEY[tier]) : tier);
|
|
23
|
-
const TIER_DESC = (tier) => (TIER_DESC_KEY[tier] ? t(TIER_DESC_KEY[tier]) : "");
|
|
24
|
-
|
|
25
|
-
// The settings toolbar: find a key by name or description across every tier,
|
|
26
|
-
// narrow to just what you have changed, and open/close all sections. It filters
|
|
27
|
-
// what is ALREADY rendered — no refetch, so it stays instant at 36 keys.
|
|
28
|
-
function settingsToolbar(d, tiers) {
|
|
29
|
-
const bar = el("div", "toolbar");
|
|
30
|
-
|
|
31
|
-
const search = el("div", "search");
|
|
32
|
-
const input = el("input", "text-input");
|
|
33
|
-
input.type = "search";
|
|
34
|
-
input.id = "settings-filter";
|
|
35
|
-
input.placeholder = t("settings.filter");
|
|
36
|
-
input.setAttribute("aria-label", t("settings.filterAria"));
|
|
37
|
-
search.append(input);
|
|
38
|
-
bar.append(search);
|
|
39
|
-
|
|
40
|
-
const changedWrap = el("label", "toggle");
|
|
41
|
-
const changed = el("input");
|
|
42
|
-
changed.type = "checkbox";
|
|
43
|
-
const overridden = d.keys.filter((k) => k.is_overridden).length;
|
|
44
|
-
changedWrap.append(changed, document.createTextNode(" " + t("settings.changedOnly", { n: overridden })));
|
|
45
|
-
changedWrap.title = overridden ? t("settings.changedTitle") : t("settings.changedNone");
|
|
46
|
-
if (!overridden) changed.disabled = true;
|
|
47
|
-
bar.append(changedWrap);
|
|
48
|
-
|
|
49
|
-
const result = el("span", "toolbar-result");
|
|
50
|
-
bar.append(result);
|
|
51
|
-
|
|
52
|
-
const toggleAll = el("button", "btn btn-ghost btn-sm", t("settings.collapseAll"));
|
|
53
|
-
toggleAll.type = "button";
|
|
54
|
-
toggleAll.addEventListener("click", () => {
|
|
55
|
-
const anyOpen = tiers.some((x) => !x.wrap.classList.contains("collapsed"));
|
|
56
|
-
for (const x of tiers) {
|
|
57
|
-
x.wrap.classList.toggle("collapsed", anyOpen);
|
|
58
|
-
x.wrap.querySelector(".tier-head").setAttribute("aria-expanded", String(!anyOpen));
|
|
59
|
-
}
|
|
60
|
-
toggleAll.textContent = anyOpen ? t("settings.expandAll") : t("settings.collapseAll");
|
|
61
|
-
});
|
|
62
|
-
bar.append(toggleAll);
|
|
63
|
-
|
|
64
|
-
const apply = () => {
|
|
65
|
-
const q = input.value.trim().toLowerCase();
|
|
66
|
-
const onlyChanged = changed.checked;
|
|
67
|
-
let shown = 0;
|
|
68
|
-
for (const x of tiers) {
|
|
69
|
-
let visible = 0;
|
|
70
|
-
for (const row of x.rows.children) {
|
|
71
|
-
const k = row.dataset.key || "";
|
|
72
|
-
const desc = (row.querySelector(".setting-desc") || {}).textContent || "";
|
|
73
|
-
const hit =
|
|
74
|
-
(!q || k.toLowerCase().includes(q) || desc.toLowerCase().includes(q)) &&
|
|
75
|
-
(!onlyChanged || row.dataset.overridden === "1");
|
|
76
|
-
row.classList.toggle("hidden", !hit);
|
|
77
|
-
if (hit) visible++;
|
|
78
|
-
}
|
|
79
|
-
shown += visible;
|
|
80
|
-
x.count.textContent = visible === x.total ? tn(x.total, "settings.keys") : t("settings.ofTotal", { n: visible, total: x.total });
|
|
81
|
-
// A tier with no matches is hidden outright — an empty titled box is
|
|
82
|
-
// noise between the ones that DID match.
|
|
83
|
-
x.wrap.classList.toggle("hidden", visible === 0);
|
|
84
|
-
// A filter that matches inside a closed section would look like no match
|
|
85
|
-
// at all, so filtering forces the section open.
|
|
86
|
-
if ((q || onlyChanged) && visible) {
|
|
87
|
-
x.wrap.classList.remove("collapsed");
|
|
88
|
-
x.wrap.querySelector(".tier-head").setAttribute("aria-expanded", "true");
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
result.textContent = q || onlyChanged ? tn(shown, "settings.matches") : "";
|
|
92
|
-
result.classList.toggle("toolbar-result-none", (q || onlyChanged) && shown === 0);
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
input.addEventListener("input", apply);
|
|
96
|
-
changed.addEventListener("change", apply);
|
|
97
|
-
// Esc clears rather than blurs: the filter is the thing in your way.
|
|
98
|
-
input.addEventListener("keydown", (e) => {
|
|
99
|
-
if (e.key === "Escape" && input.value) {
|
|
100
|
-
e.stopPropagation();
|
|
101
|
-
input.value = "";
|
|
102
|
-
apply();
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
actions
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
out.append(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
h.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (k.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
left
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
block
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
rung
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
input.
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if (
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
body
|
|
687
|
-
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
/* panels/settings.js — orc ui client
|
|
3
|
+
The toolbar, the tier accordions, every control kind, the score ladder.
|
|
4
|
+
|
|
5
|
+
Loaded by app.html in the order its numeric prefix names. Classic
|
|
6
|
+
script, no import/export: an ES module import carries no query string,
|
|
7
|
+
and every static request here needs the per-launch session token. */
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/* ================================================================ SETTINGS == */
|
|
11
|
+
|
|
12
|
+
// The tier IDS are the CLI's (`k.tier`); only their display name and blurb are
|
|
13
|
+
// ours to translate. A tier name alone does not say why a key lives there, and
|
|
14
|
+
// "advanced" reads as "do not touch" when it means "you need a reason".
|
|
15
|
+
//
|
|
16
|
+
// Written out as a literal map rather than `t("settings.tier." + tier)` so the
|
|
17
|
+
// keys stay GREPPABLE: a build of a string key from a fragment is a key no
|
|
18
|
+
// coverage check can see, and the check is the only thing standing between a
|
|
19
|
+
// renamed key and a raw dotted string on somebody's screen.
|
|
20
|
+
const TIER_LABEL_KEY = { common: "settings.tier.common", advanced: "settings.tier.advanced" };
|
|
21
|
+
const TIER_DESC_KEY = { common: "settings.tierDesc.common", advanced: "settings.tierDesc.advanced" };
|
|
22
|
+
const TIER_LABEL = (tier) => (TIER_LABEL_KEY[tier] ? t(TIER_LABEL_KEY[tier]) : tier);
|
|
23
|
+
const TIER_DESC = (tier) => (TIER_DESC_KEY[tier] ? t(TIER_DESC_KEY[tier]) : "");
|
|
24
|
+
|
|
25
|
+
// The settings toolbar: find a key by name or description across every tier,
|
|
26
|
+
// narrow to just what you have changed, and open/close all sections. It filters
|
|
27
|
+
// what is ALREADY rendered — no refetch, so it stays instant at 36 keys.
|
|
28
|
+
function settingsToolbar(d, tiers) {
|
|
29
|
+
const bar = el("div", "toolbar");
|
|
30
|
+
|
|
31
|
+
const search = el("div", "search");
|
|
32
|
+
const input = el("input", "text-input");
|
|
33
|
+
input.type = "search";
|
|
34
|
+
input.id = "settings-filter";
|
|
35
|
+
input.placeholder = t("settings.filter");
|
|
36
|
+
input.setAttribute("aria-label", t("settings.filterAria"));
|
|
37
|
+
search.append(input);
|
|
38
|
+
bar.append(search);
|
|
39
|
+
|
|
40
|
+
const changedWrap = el("label", "toggle");
|
|
41
|
+
const changed = el("input");
|
|
42
|
+
changed.type = "checkbox";
|
|
43
|
+
const overridden = d.keys.filter((k) => k.is_overridden).length;
|
|
44
|
+
changedWrap.append(changed, document.createTextNode(" " + t("settings.changedOnly", { n: overridden })));
|
|
45
|
+
changedWrap.title = overridden ? t("settings.changedTitle") : t("settings.changedNone");
|
|
46
|
+
if (!overridden) changed.disabled = true;
|
|
47
|
+
bar.append(changedWrap);
|
|
48
|
+
|
|
49
|
+
const result = el("span", "toolbar-result");
|
|
50
|
+
bar.append(result);
|
|
51
|
+
|
|
52
|
+
const toggleAll = el("button", "btn btn-ghost btn-sm", t("settings.collapseAll"));
|
|
53
|
+
toggleAll.type = "button";
|
|
54
|
+
toggleAll.addEventListener("click", () => {
|
|
55
|
+
const anyOpen = tiers.some((x) => !x.wrap.classList.contains("collapsed"));
|
|
56
|
+
for (const x of tiers) {
|
|
57
|
+
x.wrap.classList.toggle("collapsed", anyOpen);
|
|
58
|
+
x.wrap.querySelector(".tier-head").setAttribute("aria-expanded", String(!anyOpen));
|
|
59
|
+
}
|
|
60
|
+
toggleAll.textContent = anyOpen ? t("settings.expandAll") : t("settings.collapseAll");
|
|
61
|
+
});
|
|
62
|
+
bar.append(toggleAll);
|
|
63
|
+
|
|
64
|
+
const apply = () => {
|
|
65
|
+
const q = input.value.trim().toLowerCase();
|
|
66
|
+
const onlyChanged = changed.checked;
|
|
67
|
+
let shown = 0;
|
|
68
|
+
for (const x of tiers) {
|
|
69
|
+
let visible = 0;
|
|
70
|
+
for (const row of x.rows.children) {
|
|
71
|
+
const k = row.dataset.key || "";
|
|
72
|
+
const desc = (row.querySelector(".setting-desc") || {}).textContent || "";
|
|
73
|
+
const hit =
|
|
74
|
+
(!q || k.toLowerCase().includes(q) || desc.toLowerCase().includes(q)) &&
|
|
75
|
+
(!onlyChanged || row.dataset.overridden === "1");
|
|
76
|
+
row.classList.toggle("hidden", !hit);
|
|
77
|
+
if (hit) visible++;
|
|
78
|
+
}
|
|
79
|
+
shown += visible;
|
|
80
|
+
x.count.textContent = visible === x.total ? tn(x.total, "settings.keys") : t("settings.ofTotal", { n: visible, total: x.total });
|
|
81
|
+
// A tier with no matches is hidden outright — an empty titled box is
|
|
82
|
+
// noise between the ones that DID match.
|
|
83
|
+
x.wrap.classList.toggle("hidden", visible === 0);
|
|
84
|
+
// A filter that matches inside a closed section would look like no match
|
|
85
|
+
// at all, so filtering forces the section open.
|
|
86
|
+
if ((q || onlyChanged) && visible) {
|
|
87
|
+
x.wrap.classList.remove("collapsed");
|
|
88
|
+
x.wrap.querySelector(".tier-head").setAttribute("aria-expanded", "true");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
result.textContent = q || onlyChanged ? tn(shown, "settings.matches") : "";
|
|
92
|
+
result.classList.toggle("toolbar-result-none", (q || onlyChanged) && shown === 0);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
input.addEventListener("input", apply);
|
|
96
|
+
changed.addEventListener("change", apply);
|
|
97
|
+
// Esc clears rather than blurs: the filter is the thing in your way.
|
|
98
|
+
input.addEventListener("keydown", (e) => {
|
|
99
|
+
if (e.key === "Escape" && input.value) {
|
|
100
|
+
e.stopPropagation();
|
|
101
|
+
input.value = "";
|
|
102
|
+
apply();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// `#/settings?key=<key>` (v2.1.0 W7 — the Maintenance row links its day
|
|
106
|
+
// value here) opens the panel filtered to that one key.
|
|
107
|
+
const want = (/[?&]key=([a-z0-9_]+)/.exec(location.hash) || [])[1];
|
|
108
|
+
if (want) {
|
|
109
|
+
input.value = want;
|
|
110
|
+
setTimeout(apply, 0);
|
|
111
|
+
}
|
|
112
|
+
return bar;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
PANELS.settings = function (host) {
|
|
116
|
+
const actions = el("div", "row-actions");
|
|
117
|
+
const profBtn = el("button", "btn btn-sm", t("settings.profiles"));
|
|
118
|
+
profBtn.type = "button";
|
|
119
|
+
profBtn.addEventListener("click", showProfiles);
|
|
120
|
+
const recBtn = el("button", "btn btn-sm", t("settings.recommend"));
|
|
121
|
+
recBtn.type = "button";
|
|
122
|
+
recBtn.addEventListener("click", showRecommend);
|
|
123
|
+
actions.append(profBtn, recBtn);
|
|
124
|
+
head(host, t("settings.title"), t("settings.sub"), actions);
|
|
125
|
+
|
|
126
|
+
const body = el("div", "stack");
|
|
127
|
+
host.append(body);
|
|
128
|
+
renderSettings(body);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
async function renderSettings(body) {
|
|
132
|
+
body.replaceChildren(skeleton(8));
|
|
133
|
+
let d;
|
|
134
|
+
try {
|
|
135
|
+
d = (await read("/api/config")).data;
|
|
136
|
+
} catch (e) {
|
|
137
|
+
body.replaceChildren(empty(t("settings.readFail"), String(e.message)));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const out = frag();
|
|
142
|
+
|
|
143
|
+
// One edit set per render, so a re-render is also the discard: there is no
|
|
144
|
+
// way for a staged value to outlive the data it was staged against.
|
|
145
|
+
let bar = null;
|
|
146
|
+
const edits = editSet(() => {
|
|
147
|
+
if (bar) bar.paint();
|
|
148
|
+
for (const row of body.querySelectorAll(".setting[data-key]")) {
|
|
149
|
+
row.classList.toggle("staged", edits.map.has(row.dataset.key));
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const pathCard = card(t("settings.file.title"));
|
|
154
|
+
pathCard.append(
|
|
155
|
+
kvList([
|
|
156
|
+
[t("settings.file.file"), d.config_path],
|
|
157
|
+
[t("settings.file.state"), d.exists ? t("settings.file.exists") : t("settings.file.absent")],
|
|
158
|
+
])
|
|
159
|
+
);
|
|
160
|
+
// Permanently on, deliberately not a key — say so, or somebody hunts for the
|
|
161
|
+
// switch that does not exist.
|
|
162
|
+
pathCard.append(el("div", "note", t("settings.file.traceNote")));
|
|
163
|
+
if ((d.legacy_keys || []).length)
|
|
164
|
+
for (const l of d.legacy_keys)
|
|
165
|
+
pathCard.append(el("div", "note", `\`${l.key}\` is still in the file — it was renamed to \`${l.renamed_to}\`, and is read as that.`));
|
|
166
|
+
out.append(pathCard);
|
|
167
|
+
|
|
168
|
+
// The ladder is a DIAGRAM, not an editor, and it re-morphs when opus5_only
|
|
169
|
+
// flips — which is how the precedence rule gets taught rather than described.
|
|
170
|
+
out.append(ladderCard(d.score_table));
|
|
171
|
+
|
|
172
|
+
// WHO DECIDES WHAT (v1.0.0 W16). The ladder above answers one family;
|
|
173
|
+
// this answers all of them, and it is the picture a flat key list cannot
|
|
174
|
+
// draw: a contested setting is not decided by the key you are looking at,
|
|
175
|
+
// it is decided by the highest rank that resolved.
|
|
176
|
+
out.append(ranksCard(d.families, d.families_resolved, d.rank_states));
|
|
177
|
+
|
|
178
|
+
// 36 keys in three flat lists is a wall, and the answer to "where is the one
|
|
179
|
+
// I want" was scrolling. Each tier is now its own collapsible card, and the
|
|
180
|
+
// toolbar filters across ALL of them at once — so finding a key never depends
|
|
181
|
+
// on knowing which tier somebody filed it under.
|
|
182
|
+
const tiers = [];
|
|
183
|
+
out.append(settingsToolbar(d, tiers));
|
|
184
|
+
|
|
185
|
+
for (const tier of ["common", "advanced"]) {
|
|
186
|
+
const keys = d.keys.filter((k) => k.tier === tier);
|
|
187
|
+
if (!keys.length) continue;
|
|
188
|
+
|
|
189
|
+
const allInert = keys.every((k) => k.is_shadowed);
|
|
190
|
+
const wrap = el("div", "card tier");
|
|
191
|
+
wrap.dataset.tier = tier;
|
|
192
|
+
|
|
193
|
+
const h = el("button", "tier-head");
|
|
194
|
+
h.type = "button";
|
|
195
|
+
h.setAttribute("aria-expanded", "true");
|
|
196
|
+
h.append(el("span", "tier-caret", "▸"));
|
|
197
|
+
h.append(el("h2", null, TIER_LABEL(tier)));
|
|
198
|
+
const count = el("span", "tier-count", tn(keys.length, "settings.keys"));
|
|
199
|
+
h.append(count);
|
|
200
|
+
if (allInert) h.append(chip(t("settings.inert"), "warn"));
|
|
201
|
+
|
|
202
|
+
const rows = el("div", "tier-rows");
|
|
203
|
+
for (const k of keys) rows.append(settingRow(k, body, edits));
|
|
204
|
+
|
|
205
|
+
// Collapse is height-animated rather than a display swap, so the rows below
|
|
206
|
+
// it move with the section instead of teleporting.
|
|
207
|
+
h.addEventListener("click", () => {
|
|
208
|
+
const open = h.getAttribute("aria-expanded") === "true";
|
|
209
|
+
h.setAttribute("aria-expanded", String(!open));
|
|
210
|
+
wrap.classList.toggle("collapsed", open);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// The collapse animates `grid-template-rows: 1fr -> 0fr`, which needs a real
|
|
214
|
+
// element child to collapse against — so the body is wrapped, not folded in
|
|
215
|
+
// place. `height: auto` cannot be transitioned; this can.
|
|
216
|
+
const inner = el("div", "tier-body-inner");
|
|
217
|
+
inner.append(el("div", "tier-desc", TIER_DESC(tier)), rows);
|
|
218
|
+
const bodyWrap = el("div", "tier-body");
|
|
219
|
+
bodyWrap.append(inner);
|
|
220
|
+
|
|
221
|
+
wrap.append(h, bodyWrap);
|
|
222
|
+
tiers.push({ tier, wrap, rows, count, total: keys.length });
|
|
223
|
+
out.append(wrap);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if ((d.hand_edited || []).length) {
|
|
227
|
+
const c = card(t("settings.handEdited.title"));
|
|
228
|
+
c.append(el("div", "note", t("settings.handEdited.note")));
|
|
229
|
+
for (const k of d.hand_edited) {
|
|
230
|
+
const row = el("div", "setting" + (k.is_shadowed ? " shadowed" : ""));
|
|
231
|
+
const left = el("div");
|
|
232
|
+
const name = el("div", "setting-name");
|
|
233
|
+
name.append(document.createTextNode(k.key));
|
|
234
|
+
if (k.is_shadowed) {
|
|
235
|
+
const lock = el("span", "lock");
|
|
236
|
+
lock.append(document.createTextNode("🔒 " + t("settings.shadowed")));
|
|
237
|
+
name.append(lock);
|
|
238
|
+
}
|
|
239
|
+
left.append(name);
|
|
240
|
+
left.append(el("div", "setting-desc", t("settings.handEdited.readonly")));
|
|
241
|
+
if (k.shadow_reason) left.append(el("div", "shadow-why", k.shadow_reason));
|
|
242
|
+
const right = el("div", "setting-control");
|
|
243
|
+
right.append(el("div", "readonly-value", String(k.value)));
|
|
244
|
+
row.append(left, right);
|
|
245
|
+
c.append(row);
|
|
246
|
+
}
|
|
247
|
+
out.append(c);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// A key ORC REMOVED, still sitting in the user's file. It is NOT an editable
|
|
251
|
+
// setting and it is NOT a hand-edited override, so it gets its own block —
|
|
252
|
+
// rendering it as either would tell the user a dead line still does
|
|
253
|
+
// something. The CLI computed the row; this only draws it.
|
|
254
|
+
if ((d.retired_keys || []).length) {
|
|
255
|
+
const c = card(t("settings.retired.title"));
|
|
256
|
+
c.append(el("div", "note", t("settings.retired.note")));
|
|
257
|
+
for (const r of d.retired_keys) {
|
|
258
|
+
const row = el("div", "setting shadowed");
|
|
259
|
+
const left = el("div");
|
|
260
|
+
const name = el("div", "setting-name");
|
|
261
|
+
name.append(document.createTextNode(r.key));
|
|
262
|
+
const lock = el("span", "lock");
|
|
263
|
+
lock.append(document.createTextNode("🔒 " + t("settings.retired.badge")));
|
|
264
|
+
name.append(lock);
|
|
265
|
+
left.append(name);
|
|
266
|
+
left.append(el("div", "setting-desc", t("settings.retired.readonly")));
|
|
267
|
+
left.append(el("div", "shadow-why", `${t("settings.retired.removedIn")} ${r.removed_in} — ${r.why}`));
|
|
268
|
+
const right = el("div", "setting-control");
|
|
269
|
+
right.append(el("div", "readonly-value", String(r.value)));
|
|
270
|
+
row.append(left, right);
|
|
271
|
+
c.append(row);
|
|
272
|
+
}
|
|
273
|
+
out.append(c);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Last block on the panel, and it sticks to the bottom of the viewport once
|
|
277
|
+
// something is pending: with 36 keys across three tiers, an Apply you have to
|
|
278
|
+
// go looking for is an Apply that gets forgotten.
|
|
279
|
+
bar = settingsEditBar(edits, body);
|
|
280
|
+
out.append(bar);
|
|
281
|
+
|
|
282
|
+
body.replaceChildren(out);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// WHO DECIDES WHAT (v1.0.0 W16) — the contested families, resolved.
|
|
286
|
+
//
|
|
287
|
+
// THE PANEL DECIDES NOTHING HERE. `families` is the CLI's static registry (which
|
|
288
|
+
// ranks exist, in which order, and the question each family answers) and
|
|
289
|
+
// `families_resolved` is what those ranks actually did against the config on
|
|
290
|
+
// disk right now. Re-deriving a precedence in the browser is the exact drift
|
|
291
|
+
// this panel exists to make impossible — the Flow-stepper rule, applied to
|
|
292
|
+
// config.
|
|
293
|
+
//
|
|
294
|
+
// Only CONTESTED families are drawn. A family with one rank has no precedence
|
|
295
|
+
// to teach, and a card that lists thirty uncontested rows to show two
|
|
296
|
+
// interesting ones is a card nobody reads to the bottom of.
|
|
297
|
+
//
|
|
298
|
+
// Every state word is the CLI's own, from the closed set it publishes as
|
|
299
|
+
// `rank_states` — never a friendlier synonym (`resolved`, `not-read`, `inert`,
|
|
300
|
+
// `demoted`, `absent`, `partly-resolved`). A `why` is CLI prose and is never
|
|
301
|
+
// translated: it names keys, runs, profiles and commands by their real ids.
|
|
302
|
+
const RANK_STATE_KIND = {
|
|
303
|
+
resolved: "ok",
|
|
304
|
+
"partly-resolved": "ok",
|
|
305
|
+
"not-read": null,
|
|
306
|
+
inert: "warn",
|
|
307
|
+
demoted: "warn",
|
|
308
|
+
absent: null,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
function ranksCard(families, resolved, states) {
|
|
312
|
+
const c = card(t("settings.ranks.title"));
|
|
313
|
+
c.id = "ranks-card";
|
|
314
|
+
c.append(el("div", "note", t("settings.ranks.note")));
|
|
315
|
+
|
|
316
|
+
const names = Object.keys(families || {}).filter((n) => (families[n] || {}).contested);
|
|
317
|
+
if (!names.length) {
|
|
318
|
+
c.append(el("div", "empty", t("settings.ranks.none")));
|
|
319
|
+
return c;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const name of names) {
|
|
323
|
+
const fam = families[name] || {};
|
|
324
|
+
const res = (resolved || {})[name] || {};
|
|
325
|
+
const block = el("div", "rank-family");
|
|
326
|
+
|
|
327
|
+
const h = el("div", "rank-family-head");
|
|
328
|
+
// The family id is a CLI id. The QUESTION beside it is the CLI's too —
|
|
329
|
+
// it is the whole reason the grouping means anything.
|
|
330
|
+
h.append(el("span", "rank-family-name", name));
|
|
331
|
+
h.append(el("span", "rank-family-q", fam.question || ""));
|
|
332
|
+
block.append(h);
|
|
333
|
+
|
|
334
|
+
// WHAT ANSWERED, stated before the ladder rather than left to be inferred
|
|
335
|
+
// from which row happens to be green. `resolved_by` is null on a terminal
|
|
336
|
+
// rank, which is not a gap: it means nothing above the floor resolved.
|
|
337
|
+
if (res.resolved_at) {
|
|
338
|
+
const by = el("div", "rank-answer");
|
|
339
|
+
by.append(
|
|
340
|
+
document.createTextNode(
|
|
341
|
+
res.resolved_by
|
|
342
|
+
? t("settings.ranks.answeredBy", { prio: res.resolved_at, key: res.resolved_by })
|
|
343
|
+
: t("settings.ranks.answeredByDefault", { prio: res.resolved_at })
|
|
344
|
+
)
|
|
345
|
+
);
|
|
346
|
+
block.append(by);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
for (const r of res.ranks || fam.ranks || []) {
|
|
350
|
+
const row = el("div", "rank-row" + (r.state === "resolved" ? " is-resolved" : ""));
|
|
351
|
+
row.append(el("span", "rank-prio", r.prio));
|
|
352
|
+
// A rank is either a KEY or the family's terminal floor. The floor has no
|
|
353
|
+
// key on purpose — it is "the thing that happens when nobody chose" — and
|
|
354
|
+
// rendering it as a key would invite somebody to look for it in the file.
|
|
355
|
+
row.append(el("span", "rank-key" + (r.key ? "" : " is-terminal"), r.key || r.terminal || ""));
|
|
356
|
+
const st = r.state || "absent";
|
|
357
|
+
// An unknown state is RENDERED, not swallowed: the CLI owns this set, and
|
|
358
|
+
// a panel that silently drops a word it does not recognise is a panel that
|
|
359
|
+
// hides the next state somebody adds.
|
|
360
|
+
row.append(chip(st, RANK_STATE_KIND[st] === undefined ? null : RANK_STATE_KIND[st]));
|
|
361
|
+
if (r.why) row.append(el("div", "rank-why", r.why));
|
|
362
|
+
block.append(row);
|
|
363
|
+
}
|
|
364
|
+
c.append(block);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// The closed set, once, at the bottom. It is what makes an unfamiliar chip
|
|
368
|
+
// above readable without leaving the page.
|
|
369
|
+
if ((states || []).length) {
|
|
370
|
+
const legend = el("div", "rank-legend");
|
|
371
|
+
legend.append(el("span", "rank-legend-label", t("settings.ranks.legend")));
|
|
372
|
+
for (const s of states) legend.append(chip(s, RANK_STATE_KIND[s] === undefined ? null : RANK_STATE_KIND[s]));
|
|
373
|
+
c.append(legend);
|
|
374
|
+
}
|
|
375
|
+
return c;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function ladderCard(table) {
|
|
379
|
+
const c = card(t("settings.ladder.title"));
|
|
380
|
+
c.id = "ladder-card"; // the FLIP morph finds it by id, never by a :has() query
|
|
381
|
+
// `base` is the CLAUDE table that resolves. Since v0.50.0 `active` can be a
|
|
382
|
+
// COMPOSITE (`extra+opus5_only`) because an `orc extra` route row overlays
|
|
383
|
+
// the bands it covers — so branching on `active` here would fall through to
|
|
384
|
+
// the default arm and draw the WRONG ladder the moment Extra is armed. The
|
|
385
|
+
// overlay itself is the Extra panel's rail; this card stays the Claude
|
|
386
|
+
// ladder, which is exactly what it has always been.
|
|
387
|
+
const active = table.base || table.active;
|
|
388
|
+
c.append(
|
|
389
|
+
el(
|
|
390
|
+
"div",
|
|
391
|
+
"note",
|
|
392
|
+
active === "opus5_only"
|
|
393
|
+
? t("settings.ladder.opus5")
|
|
394
|
+
: active === "rubric_bands_override"
|
|
395
|
+
? t("settings.ladder.override")
|
|
396
|
+
: t("settings.ladder.default")
|
|
397
|
+
)
|
|
398
|
+
);
|
|
399
|
+
const ladder = el("div", "ladder");
|
|
400
|
+
ladder.id = "ladder";
|
|
401
|
+
const rows = active === "opus5_only" ? table.opus5_only : table.default;
|
|
402
|
+
for (const r of rows) {
|
|
403
|
+
const rung = el("div", "rung");
|
|
404
|
+
rung.dataset.agent = r.agent;
|
|
405
|
+
rung.append(el("span", "rung-band", `[${r.from},${r.to}${r.inclusive_to ? "]" : ")"}`));
|
|
406
|
+
const right = el("div");
|
|
407
|
+
const bar = el("div", "rung-bar");
|
|
408
|
+
bar.style.width = Math.max(6, ((r.to - r.from) / 100) * 100) + "%";
|
|
409
|
+
right.append(bar, el("div", "rung-agent", r.agent));
|
|
410
|
+
rung.append(right);
|
|
411
|
+
ladder.append(rung);
|
|
412
|
+
}
|
|
413
|
+
c.append(ladder);
|
|
414
|
+
c.append(el("div", "ladder-note", t("settings.ladder.note")));
|
|
415
|
+
return c;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// FLIP: measure the old rungs, swap in the new table, then animate each rung
|
|
419
|
+
// from where it used to be. The morph is what makes the precedence rule land.
|
|
420
|
+
function morphLadder(oldCard, newCard) {
|
|
421
|
+
const before = new Map();
|
|
422
|
+
for (const r of oldCard.querySelectorAll(".rung")) before.set(r.dataset.agent, r.getBoundingClientRect());
|
|
423
|
+
oldCard.replaceWith(newCard);
|
|
424
|
+
if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
425
|
+
for (const r of newCard.querySelectorAll(".rung")) {
|
|
426
|
+
const from = before.get(r.dataset.agent);
|
|
427
|
+
if (!from) {
|
|
428
|
+
r.animate([{ opacity: 0, transform: "scaleY(.4)" }, { opacity: 1, transform: "none" }], { duration: 220, easing: "cubic-bezier(.2,.7,.3,1)" });
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const to = r.getBoundingClientRect();
|
|
432
|
+
const dy = from.top - to.top;
|
|
433
|
+
if (!dy) continue;
|
|
434
|
+
r.animate([{ transform: `translateY(${dy}px)` }, { transform: "none" }], { duration: 260, easing: "cubic-bezier(.2,.7,.3,1)" });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function settingRow(k, panelBody, edits) {
|
|
439
|
+
const row = el("div", "setting" + (k.is_shadowed ? " shadowed" : ""));
|
|
440
|
+
row.dataset.key = k.key;
|
|
441
|
+
// Read by the toolbar's "changed only" filter. It is on the row rather than
|
|
442
|
+
// recomputed from the control, because the control's shape differs per kind.
|
|
443
|
+
row.dataset.overridden = k.is_overridden ? "1" : "0";
|
|
444
|
+
|
|
445
|
+
const left = el("div");
|
|
446
|
+
const name = el("div", "setting-name");
|
|
447
|
+
name.append(document.createTextNode(k.key));
|
|
448
|
+
if (k.is_overridden) name.append(el("span", "dot"));
|
|
449
|
+
if (k.is_shadowed) {
|
|
450
|
+
const lock = el("span", "lock");
|
|
451
|
+
lock.append(document.createTextNode("🔒 " + t("settings.shadowed")));
|
|
452
|
+
name.append(lock);
|
|
453
|
+
}
|
|
454
|
+
left.append(name);
|
|
455
|
+
// `k.desc` and `k.shadow_reason` are the CLI's registry text — never
|
|
456
|
+
// translated. They name keys, values and precedence rules by their real ids.
|
|
457
|
+
left.append(el("div", "setting-desc", k.desc));
|
|
458
|
+
if (k.shadow_reason) left.append(el("div", "shadow-why", k.shadow_reason));
|
|
459
|
+
// WHICH LANES READ THIS (v1.0.0 W16). `k.lanes[]` is the CLI's own
|
|
460
|
+
// `CONFIG_META[].lanes[]` — the same list `orc lane config` filters on, and
|
|
461
|
+
// the same list a two-way lint already refuses to let drift. It answers the
|
|
462
|
+
// question a settings screen otherwise leaves you guessing at: I changed
|
|
463
|
+
// this, so what did I just change? A lane name is a CLI id and is never
|
|
464
|
+
// translated.
|
|
465
|
+
//
|
|
466
|
+
// AN EMPTY LIST IS AN ANSWER AND KEEPS ITS ROW. Ten keys are permanently
|
|
467
|
+
// empty and the lint has an allowlist naming every one of them: they are
|
|
468
|
+
// OPERATING keys of the `orc extra` bridge — a lane calls `orc extra
|
|
469
|
+
// dispatch` and the CLI reads `extra_timeout_s`, so no lane ever names it.
|
|
470
|
+
// Skipping the line would make "no lane reads this" look identical to "we
|
|
471
|
+
// did not render it", which is the distinction this whole row exists to
|
|
472
|
+
// draw.
|
|
473
|
+
{
|
|
474
|
+
const lanes = el("div", "setting-lanes");
|
|
475
|
+
lanes.append(el("span", "setting-lanes-label", t("settings.lanes.label")));
|
|
476
|
+
if ((k.lanes || []).length) for (const lane of k.lanes) lanes.append(chip(lane, "lane"));
|
|
477
|
+
else lanes.append(el("span", "setting-lanes-none", t("settings.lanes.none")));
|
|
478
|
+
left.append(lanes);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const right = el("div", "setting-control");
|
|
482
|
+
right.append(controlFor(k, edits));
|
|
483
|
+
if (k.is_overridden) {
|
|
484
|
+
const reset = el("button", "btn btn-ghost btn-sm", t("setting.resetTo", { value: String(k.default) }));
|
|
485
|
+
reset.type = "button";
|
|
486
|
+
reset.addEventListener("click", () => edits.reset(k.key));
|
|
487
|
+
right.append(reset);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
row.append(left, right);
|
|
491
|
+
return row;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// The control follows the VALIDATOR, not a hand-kept table: enum → segmented,
|
|
495
|
+
// int/range → stepper with the options list as presets, path/repo/model → a
|
|
496
|
+
// text input whose validation is the CLI's own exit code.
|
|
497
|
+
//
|
|
498
|
+
// Since v0.44.1 a control STAGES its value instead of writing it. Each one also
|
|
499
|
+
// repaints its own selected state from the staged value, because nothing
|
|
500
|
+
// re-renders until Apply — a segmented control that does not follow your click
|
|
501
|
+
// would look broken, and a click that neither writes nor moves is worse than no
|
|
502
|
+
// control at all.
|
|
503
|
+
function controlFor(k, edits) {
|
|
504
|
+
const c = k.control || { kind: "text" };
|
|
505
|
+
const original = String(k.value);
|
|
506
|
+
const stage = (value) => edits.set(k.key, String(value), original);
|
|
507
|
+
|
|
508
|
+
if (c.kind === "enum") {
|
|
509
|
+
const choices = c.choices || k.options || [];
|
|
510
|
+
const seg = el("div", "seg");
|
|
511
|
+
const paint = (v) => {
|
|
512
|
+
for (const b of seg.children) b.setAttribute("aria-pressed", String(b.dataset.value === String(v)));
|
|
513
|
+
};
|
|
514
|
+
for (const opt of choices) {
|
|
515
|
+
const b = el("button", null, String(opt));
|
|
516
|
+
b.type = "button";
|
|
517
|
+
b.dataset.value = String(opt);
|
|
518
|
+
b.addEventListener("click", () => {
|
|
519
|
+
stage(String(opt));
|
|
520
|
+
paint(String(opt));
|
|
521
|
+
});
|
|
522
|
+
seg.append(b);
|
|
523
|
+
}
|
|
524
|
+
paint(original);
|
|
525
|
+
return seg;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (c.kind === "int" || c.kind === "range") {
|
|
529
|
+
const wrap = el("div", "stepper");
|
|
530
|
+
const input = el("input");
|
|
531
|
+
input.type = "number";
|
|
532
|
+
input.value = original;
|
|
533
|
+
if (c.min !== null && c.min !== undefined) input.min = String(c.min);
|
|
534
|
+
if (c.max !== null && c.max !== undefined) input.max = String(c.max);
|
|
535
|
+
// No `set` button here any more: the edit bar's Apply is the one commit
|
|
536
|
+
// point, so a second button beside every number would be two ideas of what
|
|
537
|
+
// "save" means.
|
|
538
|
+
input.addEventListener("input", () => stage(input.value));
|
|
539
|
+
wrap.append(input);
|
|
540
|
+
const presets = (k.options || []).filter((o) => String(o) !== original);
|
|
541
|
+
if (presets.length) {
|
|
542
|
+
const seg = el("div", "seg");
|
|
543
|
+
for (const p of presets.slice(0, 5)) {
|
|
544
|
+
const b = el("button", null, String(p));
|
|
545
|
+
b.type = "button";
|
|
546
|
+
b.addEventListener("click", () => {
|
|
547
|
+
input.value = String(p);
|
|
548
|
+
stage(String(p));
|
|
549
|
+
});
|
|
550
|
+
seg.append(b);
|
|
551
|
+
}
|
|
552
|
+
const box = el("div");
|
|
553
|
+
box.append(wrap, seg);
|
|
554
|
+
box.className = "setting-control";
|
|
555
|
+
return box;
|
|
556
|
+
}
|
|
557
|
+
return wrap;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (c.kind === "subset") {
|
|
561
|
+
const chosen = new Set(
|
|
562
|
+
original
|
|
563
|
+
.replace(/^\[|\]$/g, "")
|
|
564
|
+
.split(",")
|
|
565
|
+
.map((s) => s.trim())
|
|
566
|
+
.filter(Boolean)
|
|
567
|
+
);
|
|
568
|
+
const seg = el("div", "seg");
|
|
569
|
+
for (const opt of c.choices || []) {
|
|
570
|
+
const b = el("button", null, String(opt));
|
|
571
|
+
b.type = "button";
|
|
572
|
+
b.setAttribute("aria-pressed", String(chosen.has(opt)));
|
|
573
|
+
b.addEventListener("click", () => {
|
|
574
|
+
chosen.has(opt) ? chosen.delete(opt) : chosen.add(opt);
|
|
575
|
+
b.setAttribute("aria-pressed", String(chosen.has(opt)));
|
|
576
|
+
stage([...chosen].join(",") || "");
|
|
577
|
+
});
|
|
578
|
+
seg.append(b);
|
|
579
|
+
}
|
|
580
|
+
return seg;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// path / repo / free text — validation comes from the CLI's exit code, which
|
|
584
|
+
// means the rules can never drift from the ones a terminal user gets.
|
|
585
|
+
const wrap = el("div");
|
|
586
|
+
wrap.className = "setting-control";
|
|
587
|
+
const input = el("input", "text-input");
|
|
588
|
+
input.value = original;
|
|
589
|
+
input.addEventListener("input", () => stage(input.value));
|
|
590
|
+
wrap.append(input);
|
|
591
|
+
return wrap;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// The Settings edit bar. Reset is `orc config reset` with NO key — the CLI's
|
|
595
|
+
// own "put every key back to its default", not a loop of per-key writes. It is
|
|
596
|
+
// a real write, so it is confirmed and it discards anything staged first: a
|
|
597
|
+
// reset that silently kept four pending edits queued behind it would apply them
|
|
598
|
+
// straight back over the defaults.
|
|
599
|
+
function settingsEditBar(edits, panelBody) {
|
|
600
|
+
const bar = editBar(edits, {
|
|
601
|
+
resetLabel: t("edits.resetAll"),
|
|
602
|
+
onApply: async (btn) => {
|
|
603
|
+
await applyEdits(edits, { set: "/api/config/set", reset: "/api/config/reset" }, btn);
|
|
604
|
+
await rerenderSettings(panelBody);
|
|
605
|
+
},
|
|
606
|
+
onReset: () => {
|
|
607
|
+
modal({
|
|
608
|
+
title: t("edits.resetAllTitle"),
|
|
609
|
+
body: (() => {
|
|
610
|
+
const box = el("div", "stack stack-sm");
|
|
611
|
+
box.append(el("div", null, t("edits.resetAllBody")));
|
|
612
|
+
box.append(el("div", "action-cmd", "orc config reset"));
|
|
613
|
+
return box;
|
|
614
|
+
})(),
|
|
615
|
+
actions: [
|
|
616
|
+
{ label: t("common.cancel"), onClick: (c) => c() },
|
|
617
|
+
{
|
|
618
|
+
label: t("edits.resetAllApply"),
|
|
619
|
+
cls: "btn-danger",
|
|
620
|
+
onClick: async (close) => {
|
|
621
|
+
close();
|
|
622
|
+
edits.clear();
|
|
623
|
+
const r = await post("/api/config/reset", {});
|
|
624
|
+
toast(r.command, r.ok ? "ok" : "bad", r.output);
|
|
625
|
+
await rerenderSettings(panelBody);
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
],
|
|
629
|
+
});
|
|
630
|
+
},
|
|
631
|
+
onCancel: () => rerenderSettings(panelBody),
|
|
632
|
+
});
|
|
633
|
+
return bar;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// A re-render must not lose the ladder morph, so the ladder is swapped through
|
|
637
|
+
// FLIP while everything else is replaced outright.
|
|
638
|
+
async function rerenderSettings(panelBody) {
|
|
639
|
+
const old = panelBody.querySelector("#ladder-card");
|
|
640
|
+
const snapshot = old ? old.cloneNode(true) : null;
|
|
641
|
+
await renderSettings(panelBody);
|
|
642
|
+
const fresh = panelBody.querySelector("#ladder-card");
|
|
643
|
+
// Only morph when the table ACTUALLY changed — i.e. opus5_only was flipped.
|
|
644
|
+
// Any other setting re-renders without a distracting animation.
|
|
645
|
+
if (snapshot && fresh && snapshot.textContent !== fresh.textContent) {
|
|
646
|
+
// Put the old one back for a beat so FLIP has real geometry to measure.
|
|
647
|
+
fresh.parentNode.replaceChild(snapshot, fresh);
|
|
648
|
+
morphLadder(snapshot, fresh);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function showProfiles() {
|
|
653
|
+
const d = (await read("/api/config/profiles")).data;
|
|
654
|
+
const body = el("div", "stack stack-sm");
|
|
655
|
+
body.append(el("div", "note", t("profiles.note")));
|
|
656
|
+
for (const p of d.profiles) {
|
|
657
|
+
const c = el("div", "action");
|
|
658
|
+
const left = el("div");
|
|
659
|
+
// Profile name and description come from the CLI's registry — untranslated.
|
|
660
|
+
left.append(el("div", "setting-name", p.name));
|
|
661
|
+
left.append(el("div", "setting-desc", p.desc));
|
|
662
|
+
if (p.changes.length) {
|
|
663
|
+
const list = el("div", "note");
|
|
664
|
+
list.textContent = t("profiles.wouldChange", {
|
|
665
|
+
list: p.changes.map((c2) => `${c2.key} ${c2.from} → ${c2.to}`).join(", "),
|
|
666
|
+
});
|
|
667
|
+
left.append(list);
|
|
668
|
+
} else left.append(el("div", "note", t("profiles.noChange")));
|
|
669
|
+
const apply = el("button", "btn btn-sm btn-allow-busy" + (p.changes.length ? " btn-primary" : ""), t("profiles.apply"));
|
|
670
|
+
apply.type = "button";
|
|
671
|
+
apply.disabled = !p.changes.length;
|
|
672
|
+
apply.addEventListener("click", async () => {
|
|
673
|
+
const r = await post("/api/config/profile", { name: p.name });
|
|
674
|
+
toast(r.command, r.ok ? "ok" : "bad", r.output);
|
|
675
|
+
close();
|
|
676
|
+
route();
|
|
677
|
+
});
|
|
678
|
+
c.append(left, apply);
|
|
679
|
+
body.append(c);
|
|
680
|
+
}
|
|
681
|
+
const close = modal({ title: t("profiles.title"), body, actions: [{ label: t("common.close"), onClick: (c) => c() }] });
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async function showRecommend() {
|
|
685
|
+
const d = (await read("/api/config/recommend")).data;
|
|
686
|
+
const body = el("div", "stack stack-sm");
|
|
687
|
+
body.append(el("div", "note", t("recommend.note")));
|
|
688
|
+
const list = el("ul");
|
|
689
|
+
for (const r of d.reasons) {
|
|
690
|
+
const li = el("li", "note", "• " + r);
|
|
691
|
+
list.append(li);
|
|
692
|
+
}
|
|
693
|
+
body.append(list);
|
|
694
|
+
const pick = el("div", "action");
|
|
695
|
+
const left = el("div");
|
|
696
|
+
left.append(el("div", "setting-name", d.recommended));
|
|
697
|
+
left.append(el("div", "setting-desc", d.desc));
|
|
698
|
+
const apply = el("button", "btn btn-sm btn-primary btn-allow-busy", t("recommend.applyIt", { name: d.recommended }));
|
|
699
|
+
apply.type = "button";
|
|
700
|
+
apply.addEventListener("click", async () => {
|
|
701
|
+
const r = await post("/api/config/profile", { name: d.recommended });
|
|
702
|
+
toast(r.command, r.ok ? "ok" : "bad", r.output);
|
|
703
|
+
close();
|
|
704
|
+
route();
|
|
705
|
+
});
|
|
706
|
+
pick.append(left, apply);
|
|
707
|
+
body.append(pick);
|
|
708
|
+
const close = modal({ title: t("recommend.title"), body, actions: [{ label: t("common.close"), onClick: (c) => c() }] });
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/* ==================================================================== RUNS == */
|
|
712
|
+
|
|
713
|
+
/* THE LIST IS THE DETAIL VIEW (v0.43.6).
|
|
714
|
+
This panel used to be a list with a detail CARD underneath it: clicking the
|
|
715
|
+
fourth run rendered its checkpoint below run forty. On a repo with any
|
|
716
|
+
history that means scrolling past the entire list to read what you just
|
|
717
|
+
clicked, and then scrolling back up to click the next one — the list grows,
|
|
718
|
+
so the problem grows with it, which is the shape of a design that does not
|
|
719
|
+
survive its own success.
|
|
720
|
+
|
|
721
|
+
Now every row EXPANDS IN PLACE. The detail is a child of the row, animated
|
|
722
|
+
open with the same `grid-template-rows: 0fr -> 1fr` fold the settings tiers
|
|
723
|
+
use, so what you clicked stays exactly where your eye already is. One row is
|
|
724
|
+
open at a time — an accordion, not a set of toggles — because two open runs
|
|
725
|
+
re-create the scrolling problem in miniature. Detail is fetched on FIRST open
|
|
726
|
+
and then kept, so re-opening a row is instant and costs nothing. */
|