amicus 4.7.1 → 4.8.1
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 +988 -1
- package/README.md +5 -4
- package/docs/CITATIONS.md +122 -0
- package/docs/ROADMAP.md +33 -5
- package/docs/SHIMS.md +1 -1
- package/docs/configuration.md +3 -3
- package/docs/council.md +179 -32
- package/docs/doc-system.md +1 -1
- package/docs/publishing.md +2 -0
- package/docs/troubleshooting.md +3 -3
- package/docs/usage.md +2 -2
- package/electron/ipc-setup.js +18 -2
- package/electron/main.js +46 -3
- package/electron/setup-ui-aliases.js +2 -2
- package/electron/setup-ui-model.js +99 -9
- package/electron/setup-ui-styles.js +22 -0
- package/electron/setup-ui.js +231 -29
- package/electron/workspace-ui/index.html +9 -0
- package/electron/workspace-ui/live-dead-seats.js +228 -0
- package/electron/workspace-ui/live-model.js +10 -236
- package/electron/workspace-ui/live-seats.js +126 -0
- package/electron/workspace-ui/workspace-app.js +6 -41
- package/electron/workspace-ui/workspace-banners.js +95 -0
- package/electron/workspace-ui/workspace-lazy.js +55 -12
- package/electron/workspace-ui/workspace-matrix.js +2 -2
- package/electron/workspace-ui/workspace-panels.js +42 -10
- package/electron/workspace-ui/workspace-render.js +2 -2
- package/electron/workspace-ui/workspace-seats.js +101 -17
- package/package.json +4 -1
- package/schemas/council-run-live.schema.json +1 -0
- package/schemas/council-run.schema.json +19 -0
- package/schemas/council-tally.schema.json +34 -2
- package/schemas/council-verdict.schema.json +15 -0
- package/skills/second-opinion/COUNCIL-DESIGN.md +9 -4
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
- package/skills/second-opinion/SKILL.md +25 -11
- package/src/cli-handlers-doctor.js +18 -14
- package/src/council/anonymize.js +80 -11
- package/src/council/briefings-chair.js +272 -0
- package/src/council/briefings-stage2.js +12 -140
- package/src/council/debate.js +120 -15
- package/src/council/ledger-join.js +284 -0
- package/src/council/ledger-stats.js +100 -0
- package/src/council/ledger.js +177 -88
- package/src/council/parse-stage2.js +1 -1
- package/src/council/peer-split.js +196 -0
- package/src/council/report-html.js +12 -5
- package/src/council/report-md.js +146 -0
- package/src/council/report.js +188 -112
- package/src/council/run-assemble.js +100 -79
- package/src/council/run-chair.js +17 -1
- package/src/council/run-debate-revote.js +268 -0
- package/src/council/run-debate.js +92 -102
- package/src/council/run-finish.js +70 -0
- package/src/council/run-launch.js +45 -18
- package/src/council/run-retry-group.js +266 -0
- package/src/council/run-retry-keys.js +74 -0
- package/src/council/run-retry-launch.js +55 -0
- package/src/council/run-retry-notes.js +65 -13
- package/src/council/run-retry.js +166 -161
- package/src/council/run-stage1-launch.js +27 -10
- package/src/council/run-stage1-rows.js +220 -0
- package/src/council/run-stage1-superseded.js +156 -0
- package/src/council/run-stage2.js +65 -5
- package/src/council/run-stages.js +72 -69
- package/src/council/run-state.js +1 -1
- package/src/council/run-stats-entry.js +71 -0
- package/src/council/run-verdict-files.js +52 -0
- package/src/council/run.js +42 -52
- package/src/council/seats.js +262 -0
- package/src/council/stage1-bind.js +142 -0
- package/src/council/street-cred.js +258 -0
- package/src/council/tally.js +100 -49
- package/src/council/verdict.js +49 -1
- package/src/headless.js +20 -6
- package/src/mcp-tools.js +71 -1
- package/src/observe/council-legs.js +7 -1
- package/src/observe/live-doc.js +3 -3
- package/src/sidecar/fanout-leg.js +22 -1
- package/src/sidecar/fanout-wave-io.js +26 -1
- package/src/sidecar/fanout.js +4 -10
- package/src/sidecar/leg-ids.js +19 -0
- package/src/sidecar/models-probe.js +7 -4
- package/src/sidecar/reopen-spend.js +1 -1
- package/src/sidecar/setup.js +137 -2
- package/src/utils/alias-audit.js +81 -3
- package/src/utils/config.js +109 -11
- package/src/utils/curated-models.js +17 -2
- package/src/utils/degrade.js +5 -0
- package/src/utils/doctor-alias-check.js +152 -0
- package/src/utils/model-canonicalization.js +64 -0
- package/src/utils/model-shortlist.js +100 -0
- package/src/utils/no-output-backstop.js +1 -1
- package/src/utils/provider-default-picker.js +93 -45
- package/src/utils/provider-default-prompt.js +1 -1
- package/src/utils/quick-picks.js +2 -2
- package/src/utils/remediation-hints.js +37 -0
- package/src/utils/session-index-prune.js +297 -0
- package/src/utils/session-metadata-tmp-sweep.js +1 -1
- package/src/workspace/artifact-guard.js +8 -114
- package/src/workspace/artifact-names.js +222 -0
- package/src/workspace/fold-format.js +9 -6
- package/src/workspace/live-normalize.js +6 -2
- package/src/workspace/matrix-model.js +141 -19
- package/src/workspace/run-detail.js +30 -4
- package/src/workspace/seat-space.js +143 -0
|
@@ -20,6 +20,26 @@ const PROVIDER_NAMES = {
|
|
|
20
20
|
deepseek: 'DeepSeek'
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* F5: HTML-escape a value for safe interpolation into BOTH an attribute
|
|
25
|
+
* value (double-quoted) and element text content. Catalog ids are
|
|
26
|
+
* data-controlled (this repo's own convention documents them as such --
|
|
27
|
+
* see renderSearchResults in electron/setup-ui.js, which uses createElement
|
|
28
|
+
* + textContent instead of a template string for this exact row source),
|
|
29
|
+
* so every id/name reaching a template string here must be escaped rather
|
|
30
|
+
* than trusted.
|
|
31
|
+
* @param {*} value
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
function escapeAttr(value) {
|
|
35
|
+
return String(value === null || value === undefined ? '' : value)
|
|
36
|
+
.replace(/&/g, '&')
|
|
37
|
+
.replace(/</g, '<')
|
|
38
|
+
.replace(/>/g, '>')
|
|
39
|
+
.replace(/"/g, '"')
|
|
40
|
+
.replace(/'/g, ''');
|
|
41
|
+
}
|
|
42
|
+
|
|
23
43
|
/**
|
|
24
44
|
* Check if a model has at least one route with a configured key.
|
|
25
45
|
* When no keys are configured at all (empty configuredKeys), all models are available
|
|
@@ -63,15 +83,68 @@ function buildModelSearchHTML() {
|
|
|
63
83
|
</div>`;
|
|
64
84
|
}
|
|
65
85
|
|
|
86
|
+
/**
|
|
87
|
+
* issue 138: the family -> model second level for one card. Renders EVERY model
|
|
88
|
+
* in one <select> (scrollable and type-ahead searchable, so nothing is
|
|
89
|
+
* hidden), grouped "Suggested" / "All N models". Returns '' when no
|
|
90
|
+
* shortlist was supplied (or its `total` is 0).
|
|
91
|
+
*
|
|
92
|
+
* That '' is NOT dropped by the caller — buildModelStepHTML always splices
|
|
93
|
+
* it into the card template as its own line, so a card with no shortlist
|
|
94
|
+
* gains one whitespace-only line versus the pre-issue-138 HTML string. It is NOT
|
|
95
|
+
* byte-for-byte identical to that old output. It IS behaviorally identical:
|
|
96
|
+
* no <select> is emitted, and whitespace-only text nodes are inert once
|
|
97
|
+
* parsed as HTML, so the rendered card is unchanged.
|
|
98
|
+
*
|
|
99
|
+
* CONTROLLER RULING R1 (issue 138, 2026-08-24): every <option> carries
|
|
100
|
+
* data-or="<openrouterId>" (empty string when the row has no OpenRouter
|
|
101
|
+
* form), so a later task can read the user's chosen route without
|
|
102
|
+
* re-deriving a gateway prefix from the value id.
|
|
103
|
+
* @param {string} alias
|
|
104
|
+
* @param {{recommendedId:string, suggested:Array<object>, rest:Array<object>, total:number}} [shortlist]
|
|
105
|
+
* @returns {string} HTML fragment
|
|
106
|
+
*/
|
|
107
|
+
function buildModelPickHTML(alias, shortlist) {
|
|
108
|
+
if (!shortlist || !shortlist.total) { return ''; }
|
|
109
|
+
const opt = (r) => {
|
|
110
|
+
const price = r.pricePerMInput === null ? '' : ` · $${r.pricePerMInput.toFixed(2)}/M`;
|
|
111
|
+
const sel = r.isRecommended ? ' selected' : '';
|
|
112
|
+
return `<option value="${escapeAttr(r.id)}" data-or="${escapeAttr(r.openrouterId || '')}"${sel}>${escapeAttr(r.id)}${price}</option>`;
|
|
113
|
+
};
|
|
114
|
+
// Escaping-discipline consistency pass (council review, PR 196): alias
|
|
115
|
+
// is one of the five hardcoded FAMILIES names, not catalog data, so this
|
|
116
|
+
// is not closing a live vulnerability -- it matches the escapeAttr() use
|
|
117
|
+
// a few lines up for r.id/r.openrouterId, which ARE catalog-derived, so
|
|
118
|
+
// every attribute interpolation in this function follows the same rule.
|
|
119
|
+
let html = `<select class="model-pick" data-alias="${escapeAttr(alias)}">`;
|
|
120
|
+
html += `<optgroup label="Suggested">${shortlist.suggested.map(opt).join('')}</optgroup>`;
|
|
121
|
+
if (shortlist.rest.length > 0) {
|
|
122
|
+
// council review, PR 196 (F2): this optgroup holds ONLY `rest` -- label it
|
|
123
|
+
// by rest.length, not shortlist.total, or the label overstates what's in
|
|
124
|
+
// it (e.g. "All 14 models" over 6 rows when 8 are already under
|
|
125
|
+
// "Suggested" above). Singular/plural handled explicitly so a 9-row
|
|
126
|
+
// vendor with one leftover row doesn't read "1 models".
|
|
127
|
+
const restCount = shortlist.rest.length;
|
|
128
|
+
const restLabel = restCount === 1 ? '1 more model' : `${restCount} more models`;
|
|
129
|
+
html += `<optgroup label="${restLabel}">${shortlist.rest.map(opt).join('')}</optgroup>`;
|
|
130
|
+
}
|
|
131
|
+
return html + '</select>';
|
|
132
|
+
}
|
|
133
|
+
|
|
66
134
|
/**
|
|
67
135
|
* Build the HTML fragment for Step 2 (Model Selection).
|
|
68
136
|
* @param {Array<{alias:string, label:string, blurb:string, source:string, routes:Object<string,string>}>} choices
|
|
69
137
|
* Resolved rows from resolveQuickPicks(). Each row has separate label + blurb fields.
|
|
70
138
|
* @param {string} [selectedAlias] - Pre-selected alias; defaults to first available choice.
|
|
71
139
|
* @param {Object<string,boolean>} [configuredKeys] - Provider IDs the user has keys for.
|
|
140
|
+
* @param {Object<string,object>} [shortlists] - issue 138: per-alias vendor shortlist
|
|
141
|
+
* from buildModelShortlist(), used to render the model-level <select>.
|
|
142
|
+
* Defaults to {}; omitting it (or passing {}) is behaviorally identical to
|
|
143
|
+
* today's card (no <select> for that alias) but not byte-for-byte identical
|
|
144
|
+
* to the pre-issue-138 HTML string — see buildModelPickHTML's docstring.
|
|
72
145
|
* @returns {string} HTML fragment
|
|
73
146
|
*/
|
|
74
|
-
function buildModelStepHTML(choices, selectedAlias, configuredKeys = {}) {
|
|
147
|
+
function buildModelStepHTML(choices, selectedAlias, configuredKeys = {}, shortlists = {}) {
|
|
75
148
|
// Determine availability for each model
|
|
76
149
|
const availability = choices.map(c => {
|
|
77
150
|
const providers = Object.keys(c.routes);
|
|
@@ -102,6 +175,20 @@ function buildModelStepHTML(choices, selectedAlias, configuredKeys = {}) {
|
|
|
102
175
|
|
|
103
176
|
// Resolved id for the write-preview (prefer bestProvider route)
|
|
104
177
|
const previewId = c.routes[bestProvider] || Object.values(c.routes)[0] || '';
|
|
178
|
+
// Escaping-discipline consistency pass (council review, PR 196; extended
|
|
179
|
+
// by a second pass, N-c): NOT closing a live vulnerability -- c.alias is
|
|
180
|
+
// one of the five hardcoded FAMILIES names and previewId is filtered
|
|
181
|
+
// through resolveQuickPicks' anchored idPattern regexes (or a hardcoded
|
|
182
|
+
// fallback), neither of which can carry a payload. This applies the
|
|
183
|
+
// same escapeAttr() used a few lines up (buildModelPickHTML, for
|
|
184
|
+
// genuinely catalog-derived r.id/r.openrouterId) to every c.alias /
|
|
185
|
+
// previewId interpolation in this card template -- attributes
|
|
186
|
+
// (data-alias, the radio value) and text content alike (.model-alias,
|
|
187
|
+
// .model-resolved, .write-preview-id, the write-preview <code>s) --
|
|
188
|
+
// so a reader of this template does not have to work out which
|
|
189
|
+
// interpolations are "safe" and which are escaped; the rule is uniform.
|
|
190
|
+
const escapedAlias = escapeAttr(c.alias);
|
|
191
|
+
const escapedPreviewId = escapeAttr(previewId);
|
|
105
192
|
|
|
106
193
|
// Offline badge for fallback rows
|
|
107
194
|
const badge = c.source === 'fallback'
|
|
@@ -114,23 +201,26 @@ function buildModelStepHTML(choices, selectedAlias, configuredKeys = {}) {
|
|
|
114
201
|
const pills = providers.map(p => {
|
|
115
202
|
const isActive = p === bestProvider;
|
|
116
203
|
const cls = isActive ? 'route-pill active' : 'route-pill';
|
|
117
|
-
return `<button class="${cls}" data-alias="${
|
|
204
|
+
return `<button class="${cls}" data-alias="${escapedAlias}" data-provider="${p}">${PROVIDER_NAMES[p]}</button>`;
|
|
118
205
|
}).join('');
|
|
119
206
|
const toggleDisplay = showToggle ? '' : ' style="display:none"';
|
|
120
207
|
const staticDisplay = showToggle ? ' style="display:none"' : '';
|
|
121
|
-
routeHtml = `<span class="route-toggle" data-alias="${
|
|
122
|
-
routeHtml += `<span class="route-static" data-alias="${
|
|
208
|
+
routeHtml = `<span class="route-toggle" data-alias="${escapedAlias}"${toggleDisplay}>${pills}</span>`;
|
|
209
|
+
routeHtml += `<span class="route-static" data-alias="${escapedAlias}"${staticDisplay}>via ${PROVIDER_NAMES[bestProvider]}</span>`;
|
|
123
210
|
} else {
|
|
124
211
|
routeHtml = `<span class="route-static">via ${PROVIDER_NAMES[bestProvider]}</span>`;
|
|
125
212
|
}
|
|
126
213
|
|
|
214
|
+
const modelPickHtml = buildModelPickHTML(c.alias, shortlists[c.alias]);
|
|
215
|
+
|
|
127
216
|
return `<label class="${cardClass}">
|
|
128
|
-
<input type="radio" name="default-model" value="${
|
|
129
|
-
<span class="model-alias">${
|
|
217
|
+
<input type="radio" name="default-model" value="${escapedAlias}" ${checked}${disabled}>
|
|
218
|
+
<span class="model-alias">${escapedAlias}</span>
|
|
130
219
|
<span class="model-label">${c.label} — ${c.blurb}</span>${badge}
|
|
131
|
-
<span class="model-resolved">${
|
|
220
|
+
<span class="model-resolved" data-alias="${escapedAlias}">${escapedPreviewId}</span>
|
|
132
221
|
${routeHtml}
|
|
133
|
-
|
|
222
|
+
${modelPickHtml}
|
|
223
|
+
<span class="write-preview" data-alias="${escapedAlias}">will set <code>${escapedAlias}</code> → <code class="write-preview-id">${escapedPreviewId}</code></span>
|
|
134
224
|
</label>`;
|
|
135
225
|
}).join('\n ');
|
|
136
226
|
|
|
@@ -145,4 +235,4 @@ function buildModelStepHTML(choices, selectedAlias, configuredKeys = {}) {
|
|
|
145
235
|
</div>`;
|
|
146
236
|
}
|
|
147
237
|
|
|
148
|
-
module.exports = { buildModelSearchHTML, buildModelStepHTML, PROVIDER_NAMES };
|
|
238
|
+
module.exports = { buildModelSearchHTML, buildModelStepHTML, buildModelPickHTML, PROVIDER_NAMES, escapeAttr };
|
|
@@ -192,6 +192,28 @@ function __rawWizardCSS() {
|
|
|
192
192
|
margin-left: auto; font-size: 11px; color: var(--text-faint); font-style: italic;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
/* Model-level picker (Step 2 card) — issue 138. Mirrors .alias-model-select's
|
|
196
|
+
token-driven approach (background/border/radius/color/font/outline/cursor),
|
|
197
|
+
but this control lives inside a wrapping .model-card row alongside the
|
|
198
|
+
radio and route pills, not the Step-3 alias table's own linear row —
|
|
199
|
+
so it takes flex-basis:100% (its own line, below the route pills) rather
|
|
200
|
+
than alias-model-select's flex:1, and a plain --border (not --accent,
|
|
201
|
+
which the alias editor reserves for its active-edit state) so it reads
|
|
202
|
+
as a subordinate refinement, not the card's primary affordance. The
|
|
203
|
+
option/optgroup rules are load-bearing: Windows does not inherit a
|
|
204
|
+
<select>'s background/color into its <option>/<optgroup> children. */
|
|
205
|
+
.model-pick {
|
|
206
|
+
flex-basis: 100%; margin-top: 4px; padding: 3px 6px;
|
|
207
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
208
|
+
border-radius: var(--r-3); color: var(--text-muted); font-size: 11px;
|
|
209
|
+
font-family: var(--font-mono);
|
|
210
|
+
outline: none; cursor: pointer; max-width: 280px;
|
|
211
|
+
}
|
|
212
|
+
.model-pick:hover { border-color: var(--border-strong); }
|
|
213
|
+
.model-pick:focus { border-color: var(--accent); }
|
|
214
|
+
.model-pick option { background: var(--surface); color: var(--text); }
|
|
215
|
+
.model-pick optgroup { color: var(--text-muted); font-style: normal; }
|
|
216
|
+
|
|
195
217
|
/* Routing example */
|
|
196
218
|
.routing-example {
|
|
197
219
|
background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-8);
|
package/electron/setup-ui.js
CHANGED
|
@@ -20,15 +20,19 @@ const { listDirectProviders } = require('../src/utils/provider-registry');
|
|
|
20
20
|
* @param {string} [options.client='code-local'] - Client type for branding
|
|
21
21
|
* @param {Array} [options.quickPicks] - Resolved quick-pick rows from resolveQuickPicks(catalog).
|
|
22
22
|
* Defaults to pinned fallbacks when not provided.
|
|
23
|
+
* @param {Object<string,object>} [options.shortlists] - issue 138: per-alias vendor
|
|
24
|
+
* shortlist from buildModelShortlist(), passed through to buildModelStepHTML
|
|
25
|
+
* for the model-level <select>. Defaults to {} (no drill-down rendered).
|
|
23
26
|
*/
|
|
24
27
|
function buildSetupHTML(options = {}) {
|
|
25
28
|
const {
|
|
26
29
|
client = 'code-local',
|
|
27
30
|
quickPicks = resolveQuickPicks([]), // pinned fallbacks when not provided
|
|
31
|
+
shortlists = {},
|
|
28
32
|
} = options;
|
|
29
33
|
const brandName = getBrandName(client);
|
|
30
34
|
const keysHtml = buildKeysStepHTML(PROVIDERS);
|
|
31
|
-
const modelHtml = buildModelStepHTML(quickPicks);
|
|
35
|
+
const modelHtml = buildModelStepHTML(quickPicks, undefined, undefined, shortlists);
|
|
32
36
|
const aliasHtml = buildAliasEditorHTML(getDefaultAliases());
|
|
33
37
|
const css = buildWizardCSS();
|
|
34
38
|
const providersJson = JSON.stringify(PROVIDERS);
|
|
@@ -82,8 +86,17 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
82
86
|
var directProviders = ${directProvidersJson};
|
|
83
87
|
var routingChoices = {};
|
|
84
88
|
var explicitRouteChoices = {};
|
|
89
|
+
// issue 138: alias -> a SPECIFIC model id the user drilled down to. Empty
|
|
90
|
+
// means "use the family flagship", i.e. today's behavior.
|
|
91
|
+
var modelChoiceIds = {};
|
|
92
|
+
var modelOpenrouterIds = {};
|
|
85
93
|
var aliasEdits = {};
|
|
86
94
|
var aliasDisplay = {};
|
|
95
|
+
// N1: the alias map as loaded from disk (init below), so buildReview can
|
|
96
|
+
// tell an actual change from a value-identical re-write. Stays {} until
|
|
97
|
+
// init resolves (a fresh/offline config has nothing saved yet, so every
|
|
98
|
+
// computed write IS new).
|
|
99
|
+
var savedAliases = {};
|
|
87
100
|
window.availableModels = null;
|
|
88
101
|
var keyValid = false, validatedKey = '';
|
|
89
102
|
var $ = function(id) { return document.getElementById(id); };
|
|
@@ -141,6 +154,13 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
141
154
|
window.customDefaultModel = cfg.default;
|
|
142
155
|
}
|
|
143
156
|
if (cfg && cfg.aliases) {
|
|
157
|
+
// N-a (council review, PR 196): a defensive copy, not the live cfg.aliases
|
|
158
|
+
// reference. Nothing in this file currently mutates cfg.aliases after
|
|
159
|
+
// this point (checked: every '.aliases[' site below is a read), so
|
|
160
|
+
// buildReview's N1 diff is not measured to be wrong today -- but the
|
|
161
|
+
// copy is one line and removes a fragile "never mutate this" invariant
|
|
162
|
+
// future edits would otherwise have to remember.
|
|
163
|
+
savedAliases = Object.assign({}, cfg.aliases); // N1: buildReview diffs against this
|
|
144
164
|
modelChoicesData.forEach(function(mc) {
|
|
145
165
|
var currentModel = cfg.aliases[mc.alias];
|
|
146
166
|
if (currentModel) {
|
|
@@ -148,6 +168,62 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
148
168
|
for (var i = 0; i < provs.length; i++) {
|
|
149
169
|
if (mc.routes[provs[i]] === currentModel) { routingChoices[mc.alias] = provs[i]; break; }
|
|
150
170
|
}
|
|
171
|
+
// F3: the shortlist <select> is server-rendered with its OWN
|
|
172
|
+
// recommendedId-derived "selected" option and never consults
|
|
173
|
+
// modelChoiceIds at init -- so a saved drill-down pick silently
|
|
174
|
+
// reverted to the family flagship on every reopen (Step 3's
|
|
175
|
+
// alias table showed the saved value while Step 2 showed a
|
|
176
|
+
// different one). Mirror the customDefaultModel restore just
|
|
177
|
+
// above: when the saved alias value names one of THIS card's
|
|
178
|
+
// shortlist rows, seed modelChoiceIds (the .model-pick change
|
|
179
|
+
// handler's own state) and push the same value into the DOM
|
|
180
|
+
// <select> so a no-op reopen-and-Finish round-trips cleanly.
|
|
181
|
+
var sel = document.querySelector('.model-pick[data-alias="' + mc.alias + '"]');
|
|
182
|
+
if (sel) {
|
|
183
|
+
var matchedByValue = false;
|
|
184
|
+
for (var j = 0; j < sel.options.length; j++) {
|
|
185
|
+
if (sel.options[j].value === currentModel) {
|
|
186
|
+
sel.value = currentModel;
|
|
187
|
+
modelChoiceIds[mc.alias] = currentModel;
|
|
188
|
+
modelOpenrouterIds[mc.alias] = sel.options[j].getAttribute('data-or') || null;
|
|
189
|
+
matchedByValue = true;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// F3-OR (council review, issue 138): an explicit-OpenRouter
|
|
194
|
+
// drilled pick is saved as "openrouter/<vendor>/<model>"
|
|
195
|
+
// (pickRouteFor's explicit-OR branch below) -- that form
|
|
196
|
+
// matches no option's bare 'value', only its 'data-or'. Every
|
|
197
|
+
// option carries 'data-or' (CONTROLLER RULING R1, issue 138 --
|
|
198
|
+
// see buildModelPickHTML's docstring in setup-ui-model.js), so
|
|
199
|
+
// search for THAT match when the bare-value search above
|
|
200
|
+
// found nothing.
|
|
201
|
+
// Restoring modelChoiceIds alone here would round-trip the
|
|
202
|
+
// dropdown selection but make Finish write the BARE id back --
|
|
203
|
+
// silently converting the user's explicit "via OpenRouter"
|
|
204
|
+
// choice into direct-first policy routing on the very next
|
|
205
|
+
// save, which is worse than the visible flagship-revert this
|
|
206
|
+
// block exists to fix. So restore the WHOLE state pickRouteFor
|
|
207
|
+
// needs to reproduce the OR form: the option's bare value (for
|
|
208
|
+
// the <select> and modelChoiceIds), its data-or
|
|
209
|
+
// (modelOpenrouterIds), and the two flags pickRouteFor's
|
|
210
|
+
// explicit-OR branch checks -- routingChoices[alias] ===
|
|
211
|
+
// 'openrouter' and explicitRouteChoices[alias] -- which are
|
|
212
|
+
// exactly the flags the route-pill click handler sets, so the
|
|
213
|
+
// rendered pill agrees with the restored state too.
|
|
214
|
+
if (!matchedByValue) {
|
|
215
|
+
for (var k = 0; k < sel.options.length; k++) {
|
|
216
|
+
if (sel.options[k].getAttribute('data-or') === currentModel) {
|
|
217
|
+
sel.value = sel.options[k].value;
|
|
218
|
+
modelChoiceIds[mc.alias] = sel.options[k].value;
|
|
219
|
+
modelOpenrouterIds[mc.alias] = currentModel;
|
|
220
|
+
routingChoices[mc.alias] = 'openrouter';
|
|
221
|
+
explicitRouteChoices[mc.alias] = true;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
151
227
|
}
|
|
152
228
|
});
|
|
153
229
|
Object.keys(cfg.aliases).forEach(function(k) {
|
|
@@ -224,6 +300,20 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
224
300
|
// the row's first route. Returns the full model id or null.
|
|
225
301
|
function pickRouteFor(mc) {
|
|
226
302
|
if (!mc) { return null; }
|
|
303
|
+
// issue 138: an explicit per-model choice overrides the family flagship.
|
|
304
|
+
var picked = modelChoiceIds[mc.alias];
|
|
305
|
+
if (picked) {
|
|
306
|
+
// Deliberately NEVER toBareIfDirect(picked) here (unlike the auto-pick
|
|
307
|
+
// canonicalization below): picked/modelOpenrouterIds come straight
|
|
308
|
+
// from the shortlist's own id/data-or, and for a DIVERGENT_VENDOR
|
|
309
|
+
// (e.g. anthropic) that id can already BE its only-callable
|
|
310
|
+
// openrouter/<vendor>/... form -- stripping the prefix would
|
|
311
|
+
// fabricate a direct id nothing serves.
|
|
312
|
+
if (routingChoices[mc.alias] === 'openrouter' && explicitRouteChoices[mc.alias]) {
|
|
313
|
+
return modelOpenrouterIds[mc.alias] || picked;
|
|
314
|
+
}
|
|
315
|
+
return picked;
|
|
316
|
+
}
|
|
227
317
|
var provs = Object.keys(mc.routes);
|
|
228
318
|
var prov = routingChoices[mc.alias];
|
|
229
319
|
if (!prov || !mc.routes[prov]) {
|
|
@@ -363,21 +453,52 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
363
453
|
kn.length > 0 ? kn.map(function(k) { return k + ' \\u2713'; }).join(', ') : 'None';
|
|
364
454
|
var r = document.querySelector('input[name="default-model"]:checked');
|
|
365
455
|
document.getElementById('review-model').textContent = window.customDefaultModel || (r ? r.value : 'Not selected');
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
456
|
+
// F4: mirror the EXACT call Finish makes (collectAliasWrites), not just
|
|
457
|
+
// the checked radio, so Step 4 can never under-report what Finish is
|
|
458
|
+
// about to write. Before this fix, buildReview re-derived "writes" from
|
|
459
|
+
// the checked radio alone -- a drilled-down pick on a card that was NOT
|
|
460
|
+
// the checked default (or any drilled pick at all, under a custom
|
|
461
|
+
// default) was invisible here and fell through to the literal 'No
|
|
462
|
+
// alias changes', even though Finish wrote it. sidecar:save-config has
|
|
463
|
+
// no confirmation step, so this review IS the only gate.
|
|
464
|
+
var aliasWritesPreview = collectAliasWrites(r ? r.value : null, !!window.customDefaultModel);
|
|
465
|
+
// N1: aliasWritesPreview is EVERY alias Finish would write, including
|
|
466
|
+
// ones whose value is already what's on disk -- after the F1 fix that
|
|
467
|
+
// is the NORMAL case on a plain reopen (init below seeds modelChoiceIds
|
|
468
|
+
// from cfg.aliases, so the drilled-down alias, the recommendedId, and
|
|
469
|
+
// the saved value are now the same string by construction). Finish
|
|
470
|
+
// still writes the full map (a value-identical write is harmless), but
|
|
471
|
+
// this review must only SHOW entries that actually differ from
|
|
472
|
+
// savedAliases -- otherwise a no-op reopen reports "N alias(es)
|
|
473
|
+
// modified" on the one screen that has no confirmation step after it.
|
|
474
|
+
// N-b (council review, PR 196): an alias that was NEVER in savedAliases
|
|
475
|
+
// reads as undefined there, while a delete-write (Step 3's delete
|
|
476
|
+
// button, for one of the five default aliases -- see aliasEdits[alias]
|
|
477
|
+
// = null in setup-ui-alias-script.js) is null. null !== undefined is
|
|
478
|
+
// true, so without this normalization a default alias that was never
|
|
479
|
+
// explicitly saved (a config written by an older/partial flow that
|
|
480
|
+
// skipped default-seeding -- addAlias() in src/sidecar/setup.js is one
|
|
481
|
+
// such path) would show "alias -> (deleted)" for deleting something
|
|
482
|
+
// that was never there. saveConfig() already drops falsy alias values
|
|
483
|
+
// (src/utils/config.js), so that write is a true no-op on disk; the
|
|
484
|
+
// review must agree. Reading savedAliases[alias] as null (not
|
|
485
|
+
// undefined) when the key is absent makes "delete of an absent alias"
|
|
486
|
+
// compare equal to "still absent" without changing any other case --
|
|
487
|
+
// every other savedAliases value here is a non-empty string.
|
|
488
|
+
var changedWrites = {};
|
|
489
|
+
Object.keys(aliasWritesPreview).forEach(function(alias) {
|
|
490
|
+
var oldVal = Object.prototype.hasOwnProperty.call(savedAliases, alias) ? savedAliases[alias] : null;
|
|
491
|
+
if (aliasWritesPreview[alias] !== oldVal) {
|
|
492
|
+
changedWrites[alias] = aliasWritesPreview[alias];
|
|
372
493
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
494
|
+
});
|
|
495
|
+
var writes = Object.keys(changedWrites).map(function(alias) {
|
|
496
|
+
var val = changedWrites[alias];
|
|
497
|
+
return alias + ' \\u2192 ' + (val === null ? '(deleted)' : val);
|
|
498
|
+
});
|
|
378
499
|
document.getElementById('review-routing').textContent =
|
|
379
500
|
writes.length > 0 ? writes.join(', ') : 'No alias changes';
|
|
380
|
-
var editCount = Object.keys(
|
|
501
|
+
var editCount = Object.keys(changedWrites).length;
|
|
381
502
|
var reviewAliases = document.getElementById('review-aliases');
|
|
382
503
|
if (reviewAliases) {
|
|
383
504
|
reviewAliases.textContent = editCount > 0 ? editCount + ' alias(es) modified' : 'No changes';
|
|
@@ -401,27 +522,97 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
401
522
|
updateWritePreviews();
|
|
402
523
|
});
|
|
403
524
|
|
|
525
|
+
// issue 138: model-level drill-down <select> change handler.
|
|
526
|
+
document.addEventListener('change', function(e) {
|
|
527
|
+
var sel = e.target && e.target.closest ? e.target.closest('.model-pick') : null;
|
|
528
|
+
if (!sel) { return; }
|
|
529
|
+
var alias = sel.getAttribute('data-alias');
|
|
530
|
+
if (!alias) { return; }
|
|
531
|
+
modelChoiceIds[alias] = sel.value;
|
|
532
|
+
var opt = sel.options[sel.selectedIndex];
|
|
533
|
+
modelOpenrouterIds[alias] = (opt && opt.getAttribute('data-or')) || null;
|
|
534
|
+
updateWritePreviews();
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// issue 138 (fix round 1, Finding 1; precedence description corrected in
|
|
538
|
+
// the F2/F6 fix wave; modelChoiceIds provenance corrected in the N2 fix
|
|
539
|
+
// wave): assemble aliasWrites for Finish -- the aliasEdits (Step 3)
|
|
540
|
+
// overlay first; THEN, only for a checked quick-pick default (not a
|
|
541
|
+
// custom/searched one -- isCustomDefault skips this stage entirely), the
|
|
542
|
+
// selected alias's resolved route; then every OTHER alias whose
|
|
543
|
+
// drill-down <select> fired change -- "every OTHER" means every alias
|
|
544
|
+
// but the selected one ONLY when that earlier stage ran, so under a
|
|
545
|
+
// custom default the selected alias (if it has a drilled pick) is
|
|
546
|
+
// handled by THIS stage instead, not skipped. Before the Task-3 dropdown
|
|
547
|
+
// existed there was nothing to lose by writing only the selected alias;
|
|
548
|
+
// now a drilled-down pick on a card that is NOT the checked default
|
|
549
|
+
// would silently vanish without this. Precedence is NOT uniform across
|
|
550
|
+
// the two groups, unlike an earlier version of this comment claimed:
|
|
551
|
+
// only the selected alias clobbers its aliasEdits entry (the ONE place
|
|
552
|
+
// that's permitted -- user-locked decision #2); every OTHER drilled-down
|
|
553
|
+
// alias defers to aliasEdits and is written only when Step 3 left it
|
|
554
|
+
// untouched (see the hasOwnProperty guard below and ruling R6a, which
|
|
555
|
+
// already described the real behaviour correctly).
|
|
556
|
+
//
|
|
557
|
+
// modelChoiceIds is populated from TWO places, not one, unlike an
|
|
558
|
+
// earlier version of this comment claimed: the change handler above
|
|
559
|
+
// (a live drill-down pick), AND the init restore block (F3) -- which
|
|
560
|
+
// seeds it from cfg.aliases for every card whose SAVED value already
|
|
561
|
+
// names one of its shortlist rows, reading the id back out of that
|
|
562
|
+
// card's own server-rendered <option> elements. After the F1 fix that is
|
|
563
|
+
// the NORMAL case, not an edge case: a card the user never touched in
|
|
564
|
+
// THIS session routinely lands in modelChoiceIds anyway, because its
|
|
565
|
+
// saved value already matches its recommendedId. That is still correct
|
|
566
|
+
// to iterate here -- collectAliasWrites' job is to compute what Finish
|
|
567
|
+
// SHOULD write, and a value-identical write is harmless -- but it does
|
|
568
|
+
// mean this function can no longer be read as "only ever fires for
|
|
569
|
+
// aliases the user actually changed this session". buildReview is the
|
|
570
|
+
// layer responsible for not SHOWING those value-identical entries as
|
|
571
|
+
// changes (see its own N1 comment).
|
|
572
|
+
function collectAliasWrites(selectedAlias, isCustomDefault) {
|
|
573
|
+
var aliasWrites = {};
|
|
574
|
+
Object.keys(aliasEdits).forEach(function(k) {
|
|
575
|
+
aliasWrites[k] = aliasEdits[k];
|
|
576
|
+
});
|
|
577
|
+
function writeAliasRoute(alias) {
|
|
578
|
+
var mc = null;
|
|
579
|
+
for (var i = 0; i < modelChoicesData.length; i++) {
|
|
580
|
+
if (modelChoicesData[i].alias === alias) { mc = modelChoicesData[i]; break; }
|
|
581
|
+
}
|
|
582
|
+
if (!mc) { return; }
|
|
583
|
+
var routeId = pickRouteFor(mc);
|
|
584
|
+
if (routeId) { aliasWrites[mc.alias] = routeId; }
|
|
585
|
+
}
|
|
586
|
+
if (!isCustomDefault && selectedAlias) {
|
|
587
|
+
// Selecting a quick pick = explicit touch: upgrade that ONE alias
|
|
588
|
+
// to the resolved id via the chosen route (user-locked decision #2).
|
|
589
|
+
// This is the ONE place clobbering aliasEdits is permitted.
|
|
590
|
+
writeAliasRoute(selectedAlias);
|
|
591
|
+
}
|
|
592
|
+
// issue 138 (fix round 2, ruling R6a): every OTHER drilled-down alias is
|
|
593
|
+
// written ONLY when Step 3 left it untouched. hasOwnProperty.call, not
|
|
594
|
+
// the in operator and not a truthiness/not-undefined check:
|
|
595
|
+
// aliasEdits[alias] === null is a MEANINGFUL value here (ipc-setup.js:
|
|
596
|
+
// string = set, null = delete), so a falsy/undefined check would
|
|
597
|
+
// silently resurrect an alias the user explicitly deleted in Step 3,
|
|
598
|
+
// and the in operator or a not-undefined check can be fooled by an
|
|
599
|
+
// inherited Object.prototype property name (this codebase has been
|
|
600
|
+
// bitten by that class of bug before -- see the proto:null guard in
|
|
601
|
+
// src/sidecar/setup.js resolveChoice).
|
|
602
|
+
Object.keys(modelChoiceIds).forEach(function(alias) {
|
|
603
|
+
if (!isCustomDefault && alias === selectedAlias) { return; } // handled above, but only in that branch
|
|
604
|
+
if (Object.prototype.hasOwnProperty.call(aliasEdits, alias)) { return; }
|
|
605
|
+
writeAliasRoute(alias);
|
|
606
|
+
});
|
|
607
|
+
return aliasWrites;
|
|
608
|
+
}
|
|
609
|
+
|
|
404
610
|
finishBtn.addEventListener('click', async function() {
|
|
405
611
|
finishBtn.disabled = true; finishBtn.textContent = 'Saving...';
|
|
406
612
|
try {
|
|
407
613
|
var r = document.querySelector('input[name="default-model"]:checked');
|
|
408
614
|
var dm = window.customDefaultModel || (r ? r.value : null);
|
|
409
|
-
var aliasWrites =
|
|
410
|
-
Object.keys(aliasEdits).forEach(function(k) {
|
|
411
|
-
aliasWrites[k] = aliasEdits[k];
|
|
412
|
-
});
|
|
413
|
-
if (!window.customDefaultModel && r) {
|
|
414
|
-
// Selecting a quick pick = explicit touch: upgrade that ONE alias
|
|
415
|
-
// to the resolved id via the chosen route (user-locked decision #2).
|
|
416
|
-
var mc = null;
|
|
417
|
-
for (var i = 0; i < modelChoicesData.length; i++) {
|
|
418
|
-
if (modelChoicesData[i].alias === r.value) { mc = modelChoicesData[i]; break; }
|
|
419
|
-
}
|
|
420
|
-
if (mc) {
|
|
421
|
-
var routeId = pickRouteFor(mc);
|
|
422
|
-
if (routeId) { aliasWrites[mc.alias] = routeId; }
|
|
423
|
-
}
|
|
424
|
-
}
|
|
615
|
+
var aliasWrites = collectAliasWrites(r ? r.value : null, !!window.customDefaultModel);
|
|
425
616
|
await window.sidecarSetup.invoke('sidecar:save-config', dm, aliasWrites, (window.collectCouncilPicks && window.collectCouncilPicks()) || []);
|
|
426
617
|
var kc = Object.values(configuredKeys).filter(function(v) { return v; }).length;
|
|
427
618
|
await window.sidecarSetup.invoke('sidecar:setup-done', dm, kc);
|
|
@@ -551,6 +742,17 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
551
742
|
var idEl = el.querySelector('.write-preview-id');
|
|
552
743
|
if (idEl && routeId) { idEl.textContent = routeId; }
|
|
553
744
|
});
|
|
745
|
+
// issue 138: keep the resolved-id line in step with the route/model choice.
|
|
746
|
+
document.querySelectorAll('.model-resolved').forEach(function(el) {
|
|
747
|
+
var alias = el.getAttribute('data-alias');
|
|
748
|
+
var mc = null;
|
|
749
|
+
for (var i = 0; i < modelChoicesData.length; i++) {
|
|
750
|
+
if (modelChoicesData[i].alias === alias) { mc = modelChoicesData[i]; break; }
|
|
751
|
+
}
|
|
752
|
+
if (!mc) { return; }
|
|
753
|
+
var id = pickRouteFor(mc);
|
|
754
|
+
if (id) { el.textContent = id; }
|
|
755
|
+
});
|
|
554
756
|
}
|
|
555
757
|
|
|
556
758
|
document.addEventListener('input', function(e) {
|
|
@@ -99,6 +99,12 @@
|
|
|
99
99
|
</div>
|
|
100
100
|
|
|
101
101
|
<script src="./md-lite.js"></script>
|
|
102
|
+
<!-- v4.8 PR0: live-seats.js must load BEFORE live-model.js, whose bridge
|
|
103
|
+
reads window.AmicusLiveSeats in the browser. -->
|
|
104
|
+
<!-- v4.8 PR5c: live-dead-seats.js must load BEFORE live-seats.js, which re-exports
|
|
105
|
+
its deadSeats on window.AmicusLiveSeats. -->
|
|
106
|
+
<script src="./live-dead-seats.js"></script>
|
|
107
|
+
<script src="./live-seats.js"></script>
|
|
102
108
|
<script src="./live-model.js"></script>
|
|
103
109
|
<script src="./workspace-render.js"></script>
|
|
104
110
|
<script src="./workspace-matrix.js"></script>
|
|
@@ -112,6 +118,9 @@
|
|
|
112
118
|
Load order matters: workspace-app.js boots last and owns `state`. -->
|
|
113
119
|
<script src="./workspace-panels.js"></script>
|
|
114
120
|
<script src="./workspace-verbs.js"></script>
|
|
121
|
+
<!-- ⚠️ v4.8 PR5a fix-wave 3 extraction: workspace-banners.js must load BEFORE
|
|
122
|
+
workspace-app.js, whose renderBanners delegates to it. -->
|
|
123
|
+
<script src="./workspace-banners.js"></script>
|
|
115
124
|
<script src="./workspace-app.js"></script>
|
|
116
125
|
</body>
|
|
117
126
|
</html>
|