amicus 4.4.0 → 4.5.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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +162 -0
- package/README.md +17 -2
- package/bin/amicus.js +10 -0
- package/docs/DISTRIBUTION.md +234 -0
- package/docs/ROADMAP.md +226 -0
- package/docs/SHIMS.md +62 -0
- package/docs/architecture.md +104 -0
- package/docs/configuration.md +395 -0
- package/docs/council.md +970 -0
- package/docs/doc-system.md +92 -0
- package/docs/electron-testing.md +471 -0
- package/docs/jsdoc-setup.md +75 -0
- package/docs/opencode-integration.md +114 -0
- package/docs/publishing.md +60 -0
- package/docs/schemas.md +56 -0
- package/docs/testing.md +589 -0
- package/docs/troubleshooting.md +298 -0
- package/docs/usage.md +849 -0
- package/electron/fold.js +1 -1
- package/electron/main.js +4 -1
- package/electron/setup-ui-aliases.js +6 -6
- package/electron/workspace-ui/live-model.js +12 -1
- package/electron/workspace-ui/md-lite.js +52 -8
- package/electron/workspace-ui/workspace-app.js +39 -17
- package/electron/workspace-ui/workspace-matrix.js +46 -9
- package/electron/workspace-ui/workspace-panels.js +88 -19
- package/electron/workspace-ui/workspace-render.js +17 -1
- package/electron/workspace-ui/workspace-verbs.js +48 -2
- package/package.json +8 -3
- package/schemas/council-run-live.schema.json +1 -1
- package/schemas/council-run.schema.json +34 -0
- package/schemas/error.schema.json +1 -1
- package/schemas/event.schema.json +1 -1
- package/schemas/pack.schema.json +30 -0
- package/schemas/progress.schema.json +13 -1
- package/schemas/run-live.schema.json +1 -1
- package/schemas/run.schema.json +2 -1
- package/schemas/spend.schema.json +52 -4
- package/schemas/wave-live.schema.json +1 -1
- package/schemas/wave.schema.json +2 -1
- package/skills/second-opinion/SKILL.md +5 -0
- package/src/cli-handlers-council-run.js +51 -8
- package/src/cli-handlers-pack.js +238 -0
- package/src/cli-handlers-run.js +36 -8
- package/src/cli-handlers-spend.js +20 -2
- package/src/cli-handlers-template.js +53 -0
- package/src/cli-handlers-watch.js +11 -0
- package/src/cli.js +68 -5
- package/src/council/briefings-debate.js +27 -7
- package/src/council/briefings-stage2.js +155 -25
- package/src/council/briefings.js +24 -1
- package/src/council/findings.js +199 -9
- package/src/council/parse-stage2.js +10 -2
- package/src/council/presets-cli.js +23 -11
- package/src/council/report.js +19 -8
- package/src/council/run-assemble.js +42 -1
- package/src/council/run-budget.js +64 -11
- package/src/council/run-chair.js +4 -1
- package/src/council/run-debate.js +4 -2
- package/src/council/run-finalize.js +102 -0
- package/src/council/run-launch.js +29 -1
- package/src/council/run-server.js +248 -0
- package/src/council/run-stage2.js +118 -0
- package/src/council/run-stages.js +134 -110
- package/src/council/run-state.js +40 -1
- package/src/council/run.js +45 -47
- package/src/council/tally.js +10 -0
- package/src/headless.js +180 -7
- package/src/mcp-council-run.js +108 -4
- package/src/mcp-server.js +203 -7
- package/src/mcp-tools.js +15 -5
- package/src/observe/council-legs.js +60 -3
- package/src/observe/live-doc.js +18 -1
- package/src/observe/watch-render.js +4 -1
- package/src/pack/pack-cli.js +38 -0
- package/src/pack/pack-forward.js +96 -0
- package/src/pack/pack-resolve.js +297 -0
- package/src/pack/pack-store.js +130 -0
- package/src/pack/pack-validate.js +113 -0
- package/src/sidecar/child-sessions.js +1 -2
- package/src/sidecar/fanout-leg-fallback.js +69 -21
- package/src/sidecar/fanout-leg.js +6 -0
- package/src/sidecar/fanout-signals.js +61 -0
- package/src/sidecar/fanout-wave-io.js +75 -0
- package/src/sidecar/fanout.js +82 -74
- package/src/sidecar/progress-fields.js +26 -4
- package/src/sidecar/progress.js +42 -1
- package/src/sidecar/session-utils.js +23 -14
- package/src/sidecar/start.js +5 -4
- package/src/sidecar/workspace-auto-open.js +69 -0
- package/src/sidecar/workspace-window.js +46 -1
- package/src/spend-query.js +17 -5
- package/src/template/apply.js +88 -0
- package/src/template/render.js +86 -0
- package/src/template/store.js +106 -0
- package/src/utils/config.js +65 -25
- package/src/utils/error-doc.js +5 -0
- package/src/utils/lifecycle.js +37 -1
- package/src/utils/path-fence.js +39 -1
- package/src/utils/pricing.js +26 -10
- package/src/utils/result-schema-rebuild.js +1 -0
- package/src/utils/result-schema.js +8 -2
- package/src/utils/server-setup.js +79 -1
- package/src/utils/spend-ledger.js +24 -3
- package/src/workspace/artifact-guard.js +66 -7
- package/src/workspace/fold-format.js +33 -4
- package/src/workspace/live-normalize.js +28 -15
- package/src/workspace/run-detail.js +13 -1
package/electron/fold.js
CHANGED
|
@@ -66,7 +66,7 @@ function createFoldHandler(state) {
|
|
|
66
66
|
`Session: ${state.sessionId || state.taskId}`,
|
|
67
67
|
`Client: ${state.client}`,
|
|
68
68
|
`CWD: ${state.cwd}`,
|
|
69
|
-
|
|
69
|
+
'Mode: interactive',
|
|
70
70
|
'---',
|
|
71
71
|
summary || 'Session ended without summary.'
|
|
72
72
|
].join('\n');
|
package/electron/main.js
CHANGED
|
@@ -142,7 +142,10 @@ function createAmicusWindow() {
|
|
|
142
142
|
let updateInfo = null;
|
|
143
143
|
const updateInfoRaw = process.env.AMICUS_UPDATE_INFO;
|
|
144
144
|
if (updateInfoRaw) {
|
|
145
|
-
try { updateInfo = JSON.parse(updateInfoRaw); } catch (_) {
|
|
145
|
+
try { updateInfo = JSON.parse(updateInfoRaw); } catch (_) {
|
|
146
|
+
// A malformed AMICUS_UPDATE_INFO is not worth failing launch over:
|
|
147
|
+
// updateInfo stays null and the direct check below takes over.
|
|
148
|
+
}
|
|
146
149
|
}
|
|
147
150
|
if (!updateInfo) {
|
|
148
151
|
const { getUpdateInfo, initUpdateCheck } = require('../src/utils/updater');
|
|
@@ -22,7 +22,7 @@ const ALIAS_GROUPS = [
|
|
|
22
22
|
* @returns {string} HTML fragment with search, groups, rows, and add button
|
|
23
23
|
*/
|
|
24
24
|
function buildAliasEditorHTML(aliases) {
|
|
25
|
-
const searchInput =
|
|
25
|
+
const searchInput = '<input type="text" id="alias-search" class="alias-search" placeholder="Search aliases..." autocomplete="off" spellcheck="false">';
|
|
26
26
|
|
|
27
27
|
const groups = ALIAS_GROUPS.map(group => {
|
|
28
28
|
const rows = group.keys
|
|
@@ -31,10 +31,10 @@ function buildAliasEditorHTML(aliases) {
|
|
|
31
31
|
const model = aliases[key];
|
|
32
32
|
return `<div class="alias-row" data-alias="${key}">` +
|
|
33
33
|
`<span class="alias-name">${key}</span>` +
|
|
34
|
-
|
|
34
|
+
'<span class="alias-arrow">\u2192</span>' +
|
|
35
35
|
`<span class="alias-model">${model}</span>` +
|
|
36
36
|
`<button class="alias-delete" data-alias="${key}">\u00d7</button>` +
|
|
37
|
-
|
|
37
|
+
'</div>';
|
|
38
38
|
}).join('\n ');
|
|
39
39
|
|
|
40
40
|
const count = group.keys.filter(key => aliases[key] !== undefined).length;
|
|
@@ -51,9 +51,9 @@ function buildAliasEditorHTML(aliases) {
|
|
|
51
51
|
const exampleModel = aliases[exampleAlias] || 'openrouter/google/gemini-3.1-flash-lite-preview';
|
|
52
52
|
|
|
53
53
|
// SVG icons for the example box
|
|
54
|
-
const terminalIcon =
|
|
55
|
-
const arrowIcon =
|
|
56
|
-
const modelIcon =
|
|
54
|
+
const terminalIcon = '<svg class="alias-icon-accent" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="1" y="2" width="14" height="12" rx="2" stroke-width="1.5"/><path d="M4 6l2.5 2L4 10" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.5 10H11" class="alias-icon-faint-path" stroke-width="1.5" stroke-linecap="round"/></svg>';
|
|
55
|
+
const arrowIcon = '<svg class="alias-icon-accent" width="20" height="12" viewBox="0 0 20 12" fill="none"><path d="M2 6h14" stroke-width="1.5" stroke-linecap="round"/><path d="M13 2l4 4-4 4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
56
|
+
const modelIcon = '<svg class="alias-icon-ok" width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="5" r="3" stroke-width="1.5"/><path d="M8 8v3" stroke-width="1.5" stroke-linecap="round"/><circle cx="4" cy="13" r="1.5" stroke-width="1.2"/><circle cx="8" cy="13" r="1.5" stroke-width="1.2"/><circle cx="12" cy="13" r="1.5" stroke-width="1.2"/><path d="M4 11.5L8 11M8 11l4 .5" stroke-width="1" stroke-linecap="round"/></svg>';
|
|
57
57
|
|
|
58
58
|
const exampleBox = `<div class="routing-example">
|
|
59
59
|
<div class="example-label">How it works</div>
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
// or idle-timed-out run would poll forever and never flip to its terminal rendering.
|
|
12
12
|
// Must stay byte-identical to src/workspace/run-detail.js TERMINAL_STATUSES, which itself
|
|
13
13
|
// mirrors the shipped src/observe/live-doc.js TERMINAL set. A drift pin asserts this.
|
|
14
|
-
|
|
14
|
+
// ⚠️ v4.4.1 A1: 'timed-out' added alongside 'timeout'. Both spellings are real and are written
|
|
15
|
+
// by different producers — see src/observe/live-doc.js:18. Inert for the workspace (a council
|
|
16
|
+
// run.json's status vocabulary is aborted|complete|error|partial), carried for byte-identity.
|
|
17
|
+
var TERMINAL_STATUSES = ['complete', 'partial', 'error', 'crashed', 'aborted', 'timeout', 'timed-out', 'idle-timeout'];
|
|
15
18
|
|
|
16
19
|
// ⚠️ DE-ROT (F41): STAGE_LABELS must be mirrored here too. The live loop labels stages that
|
|
17
20
|
// START AFTER the run was opened, and those names are absent from the frozen derived.stageRail,
|
|
@@ -54,6 +57,14 @@
|
|
|
54
57
|
// leg (terminal rows from runStats are already alias-only, so the fallback is exact there).
|
|
55
58
|
var alias = seat.modelInput || seat.model;
|
|
56
59
|
var label = labelOf ? labelOf(alias) : null;
|
|
60
|
+
// ⚠️ v4.4.1 RN-9: this hand-rolled flip is LOAD-BEARING — do NOT "fix" it into
|
|
61
|
+
// AmicusRender.display({model: seat.model, label: label}, blindOn). The other two copies
|
|
62
|
+
// (workspace-panels.js's review/judge titles) were routed through display() because their
|
|
63
|
+
// pair's `model` IS the identity they must print. Here it is not: the seat's printable
|
|
64
|
+
// identity is the council ALIAS resolved on the line above (F36 — a live leg's `seat.model`
|
|
65
|
+
// is the RESOLVED executable id, e.g. `google/gemini-2.5-pro`, which labelMap never keys on),
|
|
66
|
+
// so display()'s blind-OFF arm would print the resolved id and undo F36. This module is also
|
|
67
|
+
// node-tested with NO DOM and no window.AmicusRender to call. Deliberate third copy.
|
|
57
68
|
var name = blindOn && label ? label : alias;
|
|
58
69
|
var tokens = (seat.tokensIn === null || seat.tokensIn === undefined) &&
|
|
59
70
|
(seat.tokensOut === null || seat.tokensOut === undefined)
|
|
@@ -15,16 +15,49 @@
|
|
|
15
15
|
var UL_RE = /^\s*[-*]\s+/;
|
|
16
16
|
var OL_RE = /^\s*\d+[.)]\s+/;
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Split one line into literal / inline-code segments in a SINGLE linear pass.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ v4.4.1 A1/D1 (Confirmed 4/4), with its SEVERITY CLAIM CORRECTED. The
|
|
22
|
+
* previous form re-`exec`ed a freshly sliced `rest` each iteration
|
|
23
|
+
* (`rest = rest.slice(...)`) — quadratic as WRITTEN, but not as it ran. V8's
|
|
24
|
+
* `String.prototype.slice` returns a SlicedString (parent pointer + offset),
|
|
25
|
+
* not a copy, so the old code measured ~8 ms at artifact-guard.js's 200 KB cap
|
|
26
|
+
* and scaled LINEARLY (~2x per doubling out to 2.4 MB). A deliberately-copying
|
|
27
|
+
* control took 78,232 ms at that same 200 KB — that is the freeze the finding
|
|
28
|
+
* described, and it is not what shipped. Numbers:
|
|
29
|
+
* `.superpowers/sdd/task-10-report.md:127-148`.
|
|
30
|
+
*
|
|
31
|
+
* What the rewrite removes is therefore a silent dependence on an undocumented
|
|
32
|
+
* engine optimisation that nothing here states or tests — a latent PORTABILITY
|
|
33
|
+
* hazard on any engine without sliced strings — and not a live performance
|
|
34
|
+
* defect or an exploitable-DoS-that-was. The `lastIndex` cursor walks the
|
|
35
|
+
* ORIGINAL string once and never copies a tail, so linearity is now a property
|
|
36
|
+
* of the algorithm rather than of the engine.
|
|
37
|
+
*
|
|
38
|
+
* Output is identical to the old function for every input: the pattern is
|
|
39
|
+
* context-free — no `^`, `\b`, lookaround or backreference — so a /g scan
|
|
40
|
+
* resuming at `lastIndex` lands on exactly the same match positions that
|
|
41
|
+
* re-`exec`ing the remainder did. The three boundary cases match too: no
|
|
42
|
+
* match at all yields one literal segment, a trailing match yields no empty
|
|
43
|
+
* tail segment, and empty input yields [].
|
|
44
|
+
*
|
|
45
|
+
* The regex is constructed per call and deliberately NOT hoisted to module
|
|
46
|
+
* scope: a /g regex carries mutable `lastIndex`, so one shared instance would
|
|
47
|
+
* leak cursor state between calls and silently drop spans.
|
|
48
|
+
*/
|
|
18
49
|
function parseInline(text) {
|
|
19
50
|
var out = [];
|
|
20
|
-
var
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
51
|
+
var s = String(text);
|
|
52
|
+
var re = /`([^`\n]+)`/g;
|
|
53
|
+
var pos = 0;
|
|
54
|
+
var m;
|
|
55
|
+
while ((m = re.exec(s)) !== null) {
|
|
56
|
+
if (m.index > pos) { out.push({ code: false, text: s.slice(pos, m.index) }); }
|
|
25
57
|
out.push({ code: true, text: m[1] });
|
|
26
|
-
|
|
58
|
+
pos = re.lastIndex;
|
|
27
59
|
}
|
|
60
|
+
if (pos < s.length) { out.push({ code: false, text: s.slice(pos) }); }
|
|
28
61
|
return out;
|
|
29
62
|
}
|
|
30
63
|
|
|
@@ -43,7 +76,11 @@
|
|
|
43
76
|
continue;
|
|
44
77
|
}
|
|
45
78
|
var h = H_RE.exec(line);
|
|
46
|
-
|
|
79
|
+
// ⚠️ v4.4.1 D3: trim the heading text. H_RE's `\s+` eats the run of
|
|
80
|
+
// whitespace after the hashes, but `(.*)$` keeps everything to end of
|
|
81
|
+
// line — so `# Title ` rendered a heading with trailing blanks baked
|
|
82
|
+
// into its text node.
|
|
83
|
+
if (h) { blocks.push({ t: 'h', level: h[1].length, text: h[2].trim() }); i += 1; continue; }
|
|
47
84
|
if (UL_RE.test(line)) {
|
|
48
85
|
var ul = [];
|
|
49
86
|
while (i < lines.length && UL_RE.test(lines[i])) { ul.push(lines[i].replace(UL_RE, '')); i += 1; }
|
|
@@ -91,7 +128,14 @@
|
|
|
91
128
|
var b = blocks[i];
|
|
92
129
|
var el;
|
|
93
130
|
if (b.t === 'h') {
|
|
94
|
-
|
|
131
|
+
// ⚠️ v4.4.1 D4: the `Math.min(6, …)` that used to wrap this was
|
|
132
|
+
// unreachable — H_RE's `#{1,4}` bounds level to 1–4, so the tag is
|
|
133
|
+
// always h3–h6 and the clamp could never fire. Dead defensive code is
|
|
134
|
+
// worse than none here: it made the h6 ceiling look enforced when the
|
|
135
|
+
// real guarantee lives in H_RE. If that `#{1,4}` is ever widened, THIS
|
|
136
|
+
// line must widen with it — h7 is not an element. The
|
|
137
|
+
// `'#'.repeat(4) + ' X'` → h6 test pins the true boundary.
|
|
138
|
+
el = d.createElement('h' + (b.level + 2));
|
|
95
139
|
applyInline(el, parseInline(b.text), d);
|
|
96
140
|
} else if (b.t === 'code') {
|
|
97
141
|
el = d.createElement('pre');
|
|
@@ -27,6 +27,13 @@
|
|
|
27
27
|
// null on a non-debate run, an aborted/skipped debate, or a parse failure —
|
|
28
28
|
// drillIntoJudge's judge-*.md fallback covers all three.
|
|
29
29
|
blind: false,
|
|
30
|
+
// Task 19 (RN-5) + fix-wave (RN-5 amendment): the (run id, status) pair renderDetail() last
|
|
31
|
+
// computed state.blind's default for. Together they gate the recompute (in renderDetail(),
|
|
32
|
+
// below) to a run CHANGE or a STATUS change only: a same-run/same-status re-render (the
|
|
33
|
+
// blind toggle) keeps the user's own choice, while a same-run/CHANGED-status re-render (the
|
|
34
|
+
// live loop's running -> terminal refresh, or the abort-confirm re-read) still auto-reveals.
|
|
35
|
+
detailRunId: null,
|
|
36
|
+
detailRunStatus: null,
|
|
30
37
|
labelByModel: {},
|
|
31
38
|
listTimer: null,
|
|
32
39
|
liveTimer: null, // LIVE (Task 15)
|
|
@@ -103,8 +110,23 @@
|
|
|
103
110
|
return;
|
|
104
111
|
}
|
|
105
112
|
|
|
106
|
-
// blind default: computed
|
|
107
|
-
|
|
113
|
+
// blind default: computed from status (resolution 9) — only on a run CHANGE or a STATUS
|
|
114
|
+
// change for the same run. Task 19 (RN-5): a same-run/same-status re-render
|
|
115
|
+
// (renderDetail_preserveBlind, below, calls straight back in here on every blind toggle)
|
|
116
|
+
// must keep the user's own choice instead of recomputing the default every call —
|
|
117
|
+
// recomputing unconditionally is what forced the old code to paint twice and, via
|
|
118
|
+
// wireLazyPanels() a few lines down, collapse any lazy panel the user had open.
|
|
119
|
+
// ⚠️ Fix-wave (RN-5 amendment, controller ruling): run id ALONE also suppressed the
|
|
120
|
+
// pre-existing running -> terminal auto-reveal, since the live loop's terminal refresh
|
|
121
|
+
// (workspace-verbs.js's startLiveLoop tick) and the abort-confirm re-read both call
|
|
122
|
+
// openRun() on the SAME run id — same run, but a real status transition. Keying on run id
|
|
123
|
+
// AND status recomputes (auto-reveals) on that transition while still preserving a same-
|
|
124
|
+
// run/same-status call (the blind toggle).
|
|
125
|
+
if (state.detailRunId !== d.runId || state.detailRunStatus !== d.run.status) {
|
|
126
|
+
state.blind = window.AmicusLive.defaultBlind(d.run.status);
|
|
127
|
+
state.detailRunId = d.runId;
|
|
128
|
+
state.detailRunStatus = d.run.status;
|
|
129
|
+
}
|
|
108
130
|
$('blind-toggle').checked = state.blind;
|
|
109
131
|
state.labelByModel = {};
|
|
110
132
|
d.derived.names.forEach(function (p) { state.labelByModel[p.model] = p.label; });
|
|
@@ -178,7 +200,6 @@
|
|
|
178
200
|
});
|
|
179
201
|
|
|
180
202
|
function renderDetail_preserveBlind() {
|
|
181
|
-
var keep = state.blind;
|
|
182
203
|
// ⚠️ Fix-wave item 1: renderDetail() itself early-returns safely for an unreadable run
|
|
183
204
|
// (!d || d.error || !d.derived) — but this wrapper used to run past that guard
|
|
184
205
|
// unconditionally, dereferencing the (nonexistent) derived model via renderSeatsPanel()
|
|
@@ -187,22 +208,22 @@
|
|
|
187
208
|
// the error branch unhides #run-view before the derived-model guard, so the Blind
|
|
188
209
|
// checkbox is live with nothing behind it.
|
|
189
210
|
if (!state.detail || state.detail.error || !state.detail.derived) {
|
|
190
|
-
$('blind-toggle').checked =
|
|
211
|
+
$('blind-toggle').checked = state.blind;
|
|
191
212
|
return;
|
|
192
213
|
}
|
|
214
|
+
// ⚠️ Task 19 (RN-5): this used to call renderDetail() (which unconditionally stomped
|
|
215
|
+
// state.blind back to the run's status default), restore the user's pre-call value,
|
|
216
|
+
// and then re-paint header chips/seats/matrix/verdict/cost a SECOND time to compensate
|
|
217
|
+
// for the first call having painted with the wrong (default) blind state — a double
|
|
218
|
+
// paint, and (via wireLazyPanels(), called inside that first renderDetail()) a collapse
|
|
219
|
+
// of any lazy panel the user had open. Fixed at the root instead of compensated for:
|
|
220
|
+
// renderDetail() now recomputes the blind default only on a run CHANGE or a STATUS change
|
|
221
|
+
// (state.detailRunId/state.detailRunStatus, above) — this call changes neither, so it
|
|
222
|
+
// keeps state.blind exactly as the change listener above just set it, and
|
|
223
|
+
// workspace-panels.js's wireLazyPanels() (its own same-run guard, `lastWiredRunId`)
|
|
224
|
+
// refreshes any open panel in place rather than collapsing it (fix-wave, Fix 1). One
|
|
225
|
+
// renderDetail() call now paints correctly the first time — nothing left to restore.
|
|
193
226
|
renderDetail();
|
|
194
|
-
state.blind = keep;
|
|
195
|
-
$('blind-toggle').checked = keep;
|
|
196
|
-
// ⚠️ R4 COUNCIL REVIEW (fourth live paid council, major, unanimous): renderDetail() (just
|
|
197
|
-
// called above) resets state.blind to the run's DEFAULT before this function restores the
|
|
198
|
-
// user's chosen `keep` value — renderHeaderChips was painted during that inner call with
|
|
199
|
-
// the (temporarily wrong) default blind state and, unlike seats/matrix/verdict/cost below,
|
|
200
|
-
// was never repainted afterward. Re-render it here too, now that state.blind is correct.
|
|
201
|
-
window.AmicusRender.renderHeaderChips($('run-chips'), state.detail.run, state.blind, labelOf);
|
|
202
|
-
P.renderSeatsPanel();
|
|
203
|
-
P.renderMatrixPanel();
|
|
204
|
-
P.renderVerdictPanel();
|
|
205
|
-
window.AmicusRender.renderCost($('cost-body'), state.detail.derived.cost, state.blind, labelOf);
|
|
206
227
|
}
|
|
207
228
|
|
|
208
229
|
$('run-list').addEventListener('keydown', function (e) {
|
|
@@ -227,7 +248,8 @@
|
|
|
227
248
|
|
|
228
249
|
// ⚠️ PRE-FLIGHT (P4) + DE-ROT (F09): register the three prose `toggle` listeners exactly ONCE,
|
|
229
250
|
// here at boot. wireLazyPanels() (called from renderDetail, on every run-open and blind-toggle)
|
|
230
|
-
//
|
|
251
|
+
// rewrites the per-run `loaders` spec map every call and — fix-wave, Fix 1 — refreshes any
|
|
252
|
+
// already-open lazy panel in place on a same-run call; it never adds a listener.
|
|
231
253
|
P.proseLoader('reviews-panel');
|
|
232
254
|
P.proseLoader('bundle-panel');
|
|
233
255
|
P.proseLoader('judges-panel');
|
|
@@ -175,20 +175,57 @@
|
|
|
175
175
|
return chairHost;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* Wrap EVERY occurrence of `needle` in a `<mark>` (DOM-safe: splitText + replaceChild,
|
|
180
|
+
* never innerHTML).
|
|
181
|
+
*
|
|
182
|
+
* ⚠️ v4.4.1 RN-3 + DOC-6: this used to do exactly ONE `indexOf`/`splitText` per collected
|
|
183
|
+
* text node, so a finding id mentioned twice inside a single text node was highlighted once
|
|
184
|
+
* — and the reader, drilling in from a dispute cell, believed they had seen every reference
|
|
185
|
+
* to it in that judge's prose. This docblock nonetheless promised "every occurrence" (DOC-6),
|
|
186
|
+
* which is what made two `wsgate04` reviewers file the same bug from opposite directions.
|
|
187
|
+
* Behaviour and doc now agree.
|
|
188
|
+
*
|
|
189
|
+
* The rescan continues from `tail`, not from `cursor`, because `splitText` MUTATES the node
|
|
190
|
+
* being walked: the first call truncates `cursor` to the text BEFORE the match and returns the
|
|
191
|
+
* match-plus-remainder; the second peels the remainder off into a NEW node the TreeWalker's
|
|
192
|
+
* already-collected list does not contain. Advancing to that new node is what makes the loop
|
|
193
|
+
* both complete (it can reach later matches) and terminating (it can never re-find the match
|
|
194
|
+
* it just replaced).
|
|
195
|
+
*
|
|
196
|
+
* NOT idempotent — a second call re-walks the text nodes inside the marks this one created and
|
|
197
|
+
* nests a second `<mark>`. drillIntoJudge (workspace-panels.js) calls clearHighlight() first
|
|
198
|
+
* for exactly that reason.
|
|
199
|
+
*
|
|
200
|
+
* ⚠️ v4.4.1 M1: clearHighlight() (below) unwraps each `<mark>` back into a plain text node but
|
|
201
|
+
* never calls `normalize()`, so the sibling text nodes either side of the old mark are left
|
|
202
|
+
* un-merged — the drill-clear-drill cycle fragments the prose a little more every time. Because
|
|
203
|
+
* the scan above only ever calls `.indexOf`/`.splitText` on ONE text node at a time, a needle
|
|
204
|
+
* whose match now straddles one of those leftover boundaries is invisible to it — silently
|
|
205
|
+
* under-marking on a re-drilled panel, the opposite of this docblock's "every occurrence"
|
|
206
|
+
* promise. `container.normalize()` below re-merges adjacent text nodes before every scan, so
|
|
207
|
+
* highlightText is robust to fragmentation regardless of how the container got that way.
|
|
208
|
+
*/
|
|
179
209
|
function highlightText(container, needle) {
|
|
180
|
-
if (!needle) { return; }
|
|
210
|
+
if (!needle) { return; } // also the loop's termination guard: a 0-length needle never advances
|
|
211
|
+
// v4.4.1 M1: undo any un-merged fragmentation clearHighlight() left behind (it does not
|
|
212
|
+
// normalize) — without this, a needle whose match spans a leftover node boundary is missed.
|
|
213
|
+
container.normalize();
|
|
181
214
|
var walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
182
215
|
var nodes = [];
|
|
183
216
|
while (walker.nextNode()) { nodes.push(walker.currentNode); }
|
|
184
217
|
nodes.forEach(function (node) {
|
|
185
|
-
var
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
218
|
+
var cursor = node;
|
|
219
|
+
for (;;) {
|
|
220
|
+
var idx = cursor.nodeValue.indexOf(needle);
|
|
221
|
+
if (idx === -1) { return; }
|
|
222
|
+
var match = cursor.splitText(idx); // `match` now starts with the needle
|
|
223
|
+
var tail = match.splitText(needle.length); // `tail` is everything after it
|
|
224
|
+
var mark = document.createElement('mark');
|
|
225
|
+
mark.textContent = needle;
|
|
226
|
+
match.parentNode.replaceChild(mark, match);
|
|
227
|
+
cursor = tail;
|
|
228
|
+
}
|
|
192
229
|
});
|
|
193
230
|
}
|
|
194
231
|
|
|
@@ -19,6 +19,24 @@
|
|
|
19
19
|
// inverted back to the model id that keys state.labelByModel, so blind labels would break.
|
|
20
20
|
function sanitizeName(model) { return String(model).replace(/[^a-zA-Z0-9._-]/g, '-'); }
|
|
21
21
|
|
|
22
|
+
// ⚠️ Task 18 (RN-1): review-/judge- filenames used to be recomputed here via a bare
|
|
23
|
+
// sanitizeName(model) call, which is NOT injective — two distinct bench models that sanitize
|
|
24
|
+
// to the same name would both resolve to the SAME filename, so drillIntoJudge's
|
|
25
|
+
// `[data-artifact="..."]` lookup handed back whichever section matched first (model A's prose
|
|
26
|
+
// rendered under model B's name). derived.artifactsByModel (src/workspace/artifact-guard.js's
|
|
27
|
+
// artifactAllowlist, threaded through by run-detail.js) already carries the disambiguated
|
|
28
|
+
// (possibly `~2`/`~3`-suffixed) name per raw model — consult it FIRST. Fall back to the legacy
|
|
29
|
+
// computation only when the map itself is absent: older detail payloads (pre-v4.5 runs,
|
|
30
|
+
// live-doc consumers not yet updated to build the map) never carry it, and re-deriving via
|
|
31
|
+
// sanitizeName is exactly what those payloads always did, so it stays correct for them too.
|
|
32
|
+
function resolveArtifactName(model, kind) {
|
|
33
|
+
var A = window.AmicusApp;
|
|
34
|
+
var map = A.state.detail.derived && A.state.detail.derived.artifactsByModel;
|
|
35
|
+
var entry = map && map[model];
|
|
36
|
+
if (entry && entry[kind]) { return entry[kind]; }
|
|
37
|
+
return kind + '-' + sanitizeName(model) + '.md';
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
function renderSeatsPanel() {
|
|
23
41
|
var A = window.AmicusApp;
|
|
24
42
|
var d = A.state.detail;
|
|
@@ -60,12 +78,16 @@
|
|
|
60
78
|
// ⚠️ PRE-FLIGHT (P4): the load is AWAITABLE — drillIntoJudge needs to know when it has
|
|
61
79
|
// settled (the old code guessed with setTimeout(render, 300), which could fire before an
|
|
62
80
|
// unbounded N-artifact IPC round trip finished and silently render nothing). loadPanel()
|
|
63
|
-
// is idempotent per panel id and returns its in-flight promise;
|
|
64
|
-
// (`loading`) and the per-run spec (`loaders`) are keyed by panel id
|
|
65
|
-
// by wireLazyPanels() on
|
|
66
|
-
// artifact requests.
|
|
81
|
+
// is idempotent per panel id and returns its in-flight promise; the promise cache
|
|
82
|
+
// (`loading`) and the per-run spec (`loaders`) are both keyed by panel id, but only
|
|
83
|
+
// `loading` is cleared by wireLazyPanels() — on a run CHANGE only (Task 19, RN-5) — and
|
|
84
|
+
// that clearing is what stops F09's stale-run artifact requests.
|
|
67
85
|
var loaders = {}; // panelId -> {bodyId, files} (rewritten per run by wireLazyPanels)
|
|
68
86
|
var loading = {}; // panelId -> Promise (cleared per run by wireLazyPanels)
|
|
87
|
+
// Task 19 (RN-5): the run wireLazyPanels() last reset panels/loading for — gates the reset
|
|
88
|
+
// below to run CHANGES only. A same-run call (renderDetail() runs this on every blind toggle
|
|
89
|
+
// too, and on the live loop's terminal refresh) instead refreshes any open panel (Fix 1).
|
|
90
|
+
var lastWiredRunId = null;
|
|
69
91
|
|
|
70
92
|
function loadPanel(panelId, bodyId, files) {
|
|
71
93
|
var A = window.AmicusApp;
|
|
@@ -107,18 +129,31 @@
|
|
|
107
129
|
}
|
|
108
130
|
|
|
109
131
|
/**
|
|
110
|
-
* Rewrites the per-run spec map
|
|
111
|
-
*
|
|
112
|
-
*
|
|
132
|
+
* Rewrites the per-run spec map on every call. On a run CHANGE (tracked via the module-level
|
|
133
|
+
* `lastWiredRunId`, above), resets panel open/loaded state and drops the previous run's
|
|
134
|
+
* cached load promises — exactly what F09's stale-run protection needs. On a SAME-run call
|
|
135
|
+
* (Task 19, RN-5: renderDetail() calls this on every blind toggle too, and the live loop's
|
|
136
|
+
* terminal refresh) any panel the user already has open is instead refreshed in place — see
|
|
137
|
+
* Fix 1 below — never left showing stale-blind content, never collapsed. Registers no
|
|
138
|
+
* listeners itself.
|
|
113
139
|
*/
|
|
114
140
|
function wireLazyPanels() {
|
|
115
141
|
var A = window.AmicusApp;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
142
|
+
// ⚠️ Fix-wave (Fix 4): keyed off `A.state.detail.runId`, not `A.state.runId` — the latter is
|
|
143
|
+
// set synchronously at the top of openRun(), before its workspace:get-run reply lands, so an
|
|
144
|
+
// out-of-order reply could make the two diverge. workspace-app.js's own run-change gate
|
|
145
|
+
// (renderDetail(), above `d.runId`) reads off the SAME `state.detail.runId`, so the two
|
|
146
|
+
// provably agree on whether this is a run change.
|
|
147
|
+
var sameRun = A.state.detail.runId === lastWiredRunId;
|
|
148
|
+
if (!sameRun) {
|
|
149
|
+
['reviews-panel', 'bundle-panel', 'judges-panel'].forEach(function (id) {
|
|
150
|
+
var p = A.$(id);
|
|
151
|
+
p.dataset.loaded = '0';
|
|
152
|
+
p.open = false;
|
|
153
|
+
delete loading[id];
|
|
154
|
+
});
|
|
155
|
+
lastWiredRunId = A.state.detail.runId;
|
|
156
|
+
}
|
|
122
157
|
var bench = A.state.detail.run.bench || [];
|
|
123
158
|
var debated = !!A.state.detail.run.debate;
|
|
124
159
|
// ⚠️ CODE REVIEW (round 2, finding 2): readRunArtifact's error for a genuinely-missing
|
|
@@ -134,19 +169,30 @@
|
|
|
134
169
|
// than new).
|
|
135
170
|
var artifacts = A.state.detail.artifacts || {};
|
|
136
171
|
function present(name) { return !!(artifacts[name] && artifacts[name].present); }
|
|
172
|
+
// ⚠️ v4.4.1 RN-9: these two titles used to hand-roll `A.state.blind && label ? label : m`
|
|
173
|
+
// inline. Both now go through AmicusRender.display() — the single blind-flip definition the
|
|
174
|
+
// re-vote title below already used — so the next blind-mode ruling lands in one place instead
|
|
175
|
+
// of being re-applied by hand in every file that happens to render an identity.
|
|
137
176
|
loaders['reviews-panel'] = { bodyId: 'reviews-body', files: function () {
|
|
138
177
|
return bench.map(function (m) {
|
|
139
178
|
var label = A.state.labelByModel[m];
|
|
140
|
-
return { name:
|
|
179
|
+
return { name: resolveArtifactName(m, 'review'), title: window.AmicusRender.display({ model: m, label: label }, A.state.blind) };
|
|
141
180
|
}).filter(function (f) { return present(f.name); });
|
|
142
181
|
} };
|
|
143
182
|
loaders['bundle-panel'] = { bodyId: 'bundle-body', files: function () {
|
|
144
|
-
|
|
183
|
+
// ⚠️ v4.4.1 RN-4: the presence filter is NOT optional here either. Without it, a run whose
|
|
184
|
+
// Stage 2 never ran (a one-seat bench, an abort before the cross-review, a cost ceiling)
|
|
185
|
+
// requested a file the manifest already knows is absent and rendered readRunArtifact's raw
|
|
186
|
+
// error string in the panel — "absolute host path and all", per this file's own round-2
|
|
187
|
+
// note above `present()`. reviews-panel and judges-panel have always filtered; this was the
|
|
188
|
+
// odd one out.
|
|
189
|
+
return [{ name: 'bundle-stage2.md', title: 'bundle-stage2.md (verbatim)' }]
|
|
190
|
+
.filter(function (f) { return present(f.name); });
|
|
145
191
|
} };
|
|
146
192
|
loaders['judges-panel'] = { bodyId: 'judges-body', files: function () {
|
|
147
193
|
var files = bench.map(function (m) {
|
|
148
194
|
var label = A.state.labelByModel[m];
|
|
149
|
-
return { name:
|
|
195
|
+
return { name: resolveArtifactName(m, 'judge'), title: 'Judge ' + window.AmicusRender.display({ model: m, label: label }, A.state.blind) };
|
|
150
196
|
});
|
|
151
197
|
if (debated) {
|
|
152
198
|
// ⚠️ DE-ROT (F38): on a --debate run, a matrix dispute cell can be a RE-VOTE whose
|
|
@@ -157,13 +203,31 @@
|
|
|
157
203
|
// judge- titles above, which mirror the brief verbatim), so it goes through
|
|
158
204
|
// AmicusRender.display() — the single blind-flip definition — rather than adding a
|
|
159
205
|
// fourth hand-rolled copy of the same ternary.
|
|
206
|
+
// ⚠️ Task 18 fix-wave (RN-1, review finding 1): this name used to be recomputed via a
|
|
207
|
+
// bare sanitizeName(m) call, ignoring the disambiguation map entirely — for a colliding
|
|
208
|
+
// pair BOTH models resolved to the same bare revote-<sanitized>.md name, reintroducing
|
|
209
|
+
// for re-votes the exact cross-match bug Task 18 fixed for review-/judge-. Routed
|
|
210
|
+
// through resolveArtifactName(m, 'revote') like the other three sites; its built-in
|
|
211
|
+
// legacy fallback keeps older detail payloads (no artifactsByModel map) correct too.
|
|
160
212
|
files = files.concat(bench.map(function (m) {
|
|
161
213
|
var label = A.state.labelByModel[m];
|
|
162
|
-
return { name:
|
|
214
|
+
return { name: resolveArtifactName(m, 'revote'), title: 'Re-vote ' + window.AmicusRender.display({ model: m, label: label }, A.state.blind) };
|
|
163
215
|
}));
|
|
164
216
|
}
|
|
165
217
|
return files.filter(function (f) { return present(f.name); });
|
|
166
218
|
} };
|
|
219
|
+
// ⚠️ Fix-wave (Fix 1, RN-9): a same-run call (the blind toggle, or the live loop's
|
|
220
|
+
// running -> terminal refresh) must re-render any panel the user already has open, or it
|
|
221
|
+
// keeps showing content painted under the PREVIOUS blind state. renderProseSections()
|
|
222
|
+
// (workspace-render.js) clears its container before repainting, so this replaces sections
|
|
223
|
+
// in place rather than appending duplicates. Drop the cached promise first so loadPanel()
|
|
224
|
+
// actually re-fetches instead of returning its already-settled one.
|
|
225
|
+
if (sameRun) {
|
|
226
|
+
['reviews-panel', 'bundle-panel', 'judges-panel'].forEach(function (id) {
|
|
227
|
+
var p = A.$(id);
|
|
228
|
+
if (p.open) { delete loading[id]; loadPanel(id, loaders[id].bodyId, loaders[id].files); }
|
|
229
|
+
});
|
|
230
|
+
}
|
|
167
231
|
}
|
|
168
232
|
|
|
169
233
|
// ⚠️ DE-ROT (F38): on a --debate run the FINAL tally.json is rebuilt from the debate's
|
|
@@ -183,9 +247,14 @@
|
|
|
183
247
|
var rv = ((A.state.debate && A.state.debate.revotes) || []).find(function (r) {
|
|
184
248
|
return r.judge === judgePair.model && r.id === findingId;
|
|
185
249
|
});
|
|
250
|
+
// ⚠️ Task 18 fix-wave (RN-1, review finding 1): this branch used to recompute the name via
|
|
251
|
+
// bare sanitizeName(judgePair.model), independently of the (already-fixed) judge branch
|
|
252
|
+
// right below it — for a colliding pair, drilling a re-vote on the SECOND model resolved
|
|
253
|
+
// to the bare name and cross-matched the FIRST model's genuine revote section. Both arms
|
|
254
|
+
// of this ternary now go through the same disambiguation-aware helper.
|
|
186
255
|
var artifactName = rv
|
|
187
|
-
?
|
|
188
|
-
:
|
|
256
|
+
? resolveArtifactName(judgePair.model, 'revote')
|
|
257
|
+
: resolveArtifactName(judgePair.model, 'judge');
|
|
189
258
|
var section = A.$('judges-body').querySelector('[data-artifact="' + artifactName + '"]');
|
|
190
259
|
// A genuinely absent artifact is not an error here — the panel renders its own
|
|
191
260
|
// "<file> not written yet" empty state (spec §9, last row).
|
|
@@ -93,7 +93,13 @@
|
|
|
93
93
|
]),
|
|
94
94
|
el('div', { className: 'run-row-sub' }, [
|
|
95
95
|
relTime(row.startedAt),
|
|
96
|
-
String(row.bench.length)
|
|
96
|
+
// ⚠️ v4.4.1 RN-12: `String(row.bench.length)` was unguarded. scanCouncilRuns is written
|
|
97
|
+
// to "never throw on bad input" and degrade to a row instead — but this painter draws
|
|
98
|
+
// EVERY row, so one row arriving without a `bench` array threw a TypeError that blanked
|
|
99
|
+
// the entire run list, killing the data layer's degrade-never-throw guarantee one layer
|
|
100
|
+
// up. (scanCouncilRuns' own error rows take the early return above; this guards the
|
|
101
|
+
// contract with any OTHER row source, which is what the guarantee is actually worth.)
|
|
102
|
+
(Array.isArray(row.bench) ? row.bench.length : 0) + ' seats',
|
|
97
103
|
'chair ' + displayModel(row.chair, blindOn, labelOf),
|
|
98
104
|
row.overallVerdict || '',
|
|
99
105
|
row.costDisplay || '',
|
|
@@ -199,8 +205,18 @@
|
|
|
199
205
|
cells.forEach(function (c, i) {
|
|
200
206
|
var td = row.children[i];
|
|
201
207
|
if (td && td.textContent !== c) { td.textContent = c; }
|
|
208
|
+
if (td) { td.className = i >= 4 && i <= 6 ? 'num' : (i === 8 ? 'stalled-flag' : ''); }
|
|
202
209
|
});
|
|
203
210
|
});
|
|
211
|
+
// RN-11 (v4.5): the keyed update added and removed rows but never MOVED
|
|
212
|
+
// them, so table order was frozen at first render — wrong the moment a
|
|
213
|
+
// repair solo or new wave changes the composed doc's leg order mid-run.
|
|
214
|
+
seats.forEach(function (seat, i) {
|
|
215
|
+
var key = String(seat.id || seat.model);
|
|
216
|
+
var current = tbody.children[i];
|
|
217
|
+
var target = Array.prototype.slice.call(tbody.children).find(function (r) { return r.dataset.key === key; });
|
|
218
|
+
if (target && target !== current) { tbody.insertBefore(target, current || null); }
|
|
219
|
+
});
|
|
204
220
|
Array.prototype.slice.call(tbody.children).forEach(function (row) {
|
|
205
221
|
if (!seen[row.dataset.key]) { row.remove(); }
|
|
206
222
|
});
|
|
@@ -20,6 +20,13 @@
|
|
|
20
20
|
function doFold() {
|
|
21
21
|
var A = window.AmicusApp;
|
|
22
22
|
var btn = A.$('fold-btn');
|
|
23
|
+
// ⚠️ v4.4.1 LC-8: this chain had NO rejection handler. workspace:fold's IPC handler catches
|
|
24
|
+
// its own errors and answers {ok:false, error}, so a REJECTION means the channel itself failed
|
|
25
|
+
// — and the user saw the Fold button sitting in its pre-click state with no explanation, while
|
|
26
|
+
// the renderer logged an unhandled rejection (which jest-circus fails the running test on).
|
|
27
|
+
// Two-argument .then(onFulfilled, onRejected), NOT .then().catch() — see the long note in
|
|
28
|
+
// startLiveLoop below: with a trailing .catch, a THROW inside onFulfilled would be routed here
|
|
29
|
+
// too and misreported as a channel failure.
|
|
23
30
|
A.invoke('workspace:fold', A.state.runId).then(function (res) {
|
|
24
31
|
if (res.ok) {
|
|
25
32
|
btn.textContent = 'Folded ✓';
|
|
@@ -28,6 +35,9 @@
|
|
|
28
35
|
} else {
|
|
29
36
|
btn.title = res.error || 'fold failed';
|
|
30
37
|
}
|
|
38
|
+
}, function (err) {
|
|
39
|
+
console.error('workspace fold: workspace:fold failed', err);
|
|
40
|
+
btn.title = 'fold failed — the workspace channel is unavailable' + (err && err.message ? ': ' + err.message : '');
|
|
31
41
|
});
|
|
32
42
|
}
|
|
33
43
|
|
|
@@ -180,7 +190,23 @@
|
|
|
180
190
|
R.renderBanner(A.$('banner'),
|
|
181
191
|
'No leg activity' + (mins ? ' for ' + mins + 'm' : '') + ' — the run may be dead. Abort to reclaim it; everything on disk stays browsable.',
|
|
182
192
|
'live');
|
|
183
|
-
|
|
193
|
+
// ⚠️ v4.4.1 RN-6 — ARBITRATED, and deliberately NOT fixed as the backlog proposed. RN-6
|
|
194
|
+
// asked for a matching `hidden = true` in the clearing arm below, believing this line is
|
|
195
|
+
// what puts the Abort button on screen. It is not: workspace-app.js's renderDetail already
|
|
196
|
+
// sets `$('abort-btn').hidden = isTerminal` on every run-open (:128), and startLiveLoop only
|
|
197
|
+
// runs on a non-terminal run — so the button is ALREADY visible for the whole life of a live
|
|
198
|
+
// run, by design, and this assignment is a no-op on every path that reaches it. The proposed
|
|
199
|
+
// clearing-arm write would HIDE the button the moment a momentary stall recovered, leaving a
|
|
200
|
+
// healthy, still-running council with no way to abort it: the inverse defect, and worse.
|
|
201
|
+
// Full reasoning + the regression pin: "the Abort button survives a stall -> recover cycle"
|
|
202
|
+
// in tests/workspace/live-loop.test.js.
|
|
203
|
+
//
|
|
204
|
+
// v4.4.1 M4: this line is NOT "the remedy" (that framing, from Task 16, is what this note
|
|
205
|
+
// corrects) — it is redundant on every path except one: a run that dies WHILE stalled,
|
|
206
|
+
// whose terminal openRun() refresh then rejects. On THAT path it leaves Abort visible on a
|
|
207
|
+
// run that is already dead — a false affordance, not a working safety net. Kept anyway
|
|
208
|
+
// because it is harmless: aborting a dead run is a no-op.
|
|
209
|
+
A.$('abort-btn').hidden = false;
|
|
184
210
|
} else if (A.$('banner').classList.contains('live')) {
|
|
185
211
|
// Restores whatever the DURABLE banner actually says (nothing, a schemaVersion mismatch,
|
|
186
212
|
// run.error, …) instead of just blanking it — a live-layer banner (info-unavailable or
|
|
@@ -216,7 +242,27 @@
|
|
|
216
242
|
return;
|
|
217
243
|
}
|
|
218
244
|
stopLiveLoop();
|
|
219
|
-
|
|
245
|
+
// ⚠️ v4.4.1 LC-8: the re-read was fire-and-forget (wsgate01 C5) — inconsistent with the
|
|
246
|
+
// live loop's terminal branch above, which was explicitly given this same handler for this
|
|
247
|
+
// same reason. If the re-read fails, the status chip never leaves 'running' and the UI shows
|
|
248
|
+
// a live run that isn't. `.catch` (not the two-argument form) is EXACTLY equivalent here and
|
|
249
|
+
// matches the sibling at the terminal branch above: there is no onFulfilled for a trailing
|
|
250
|
+
// .catch to swallow a throw out of, which is the only thing the two-argument form buys.
|
|
251
|
+
A.openRun(A.state.runId).catch(function (err) { // re-read: status flips to aborted, grey chip, no live poll
|
|
252
|
+
console.error('workspace abort: post-abort get-run refresh failed', err);
|
|
253
|
+
window.AmicusRender.renderBanner(A.$('banner'),
|
|
254
|
+
'Abort succeeded, but refreshing the run failed — reopen the run to see its final state.', '');
|
|
255
|
+
});
|
|
256
|
+
}, function (err) {
|
|
257
|
+
// ⚠️ v4.4.1 LC-8 (same handler, same class): a REJECTED invoke() left `btn.disabled = true`
|
|
258
|
+
// and the dialog open forever — the confirm button wedged with no way back — plus an
|
|
259
|
+
// unhandled rejection. workspace:abort-run's IPC handler answers {ok:false} on its own
|
|
260
|
+
// errors, so reaching here means the channel failed, not the abort.
|
|
261
|
+
console.error('workspace abort: workspace:abort-run failed', err);
|
|
262
|
+
btn.disabled = false;
|
|
263
|
+
$('dialog-abort').hidden = true;
|
|
264
|
+
window.AmicusRender.renderBanner(A.$('banner'),
|
|
265
|
+
'Abort failed: the workspace channel is unavailable' + (err && err.message ? ' — ' + err.message : ''), '');
|
|
220
266
|
});
|
|
221
267
|
});
|
|
222
268
|
|