amicus 4.8.0 → 4.9.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 +355 -0
- package/README.md +10 -5
- package/docs/CITATIONS.md +13 -5
- package/docs/ROADMAP.md +101 -10
- package/docs/configuration.md +55 -5
- package/docs/council.md +102 -14
- package/docs/troubleshooting.md +9 -2
- package/docs/usage.md +128 -12
- package/electron/ipc-setup.js +39 -2
- package/electron/main.js +46 -3
- package/electron/offer-session.js +51 -0
- package/electron/setup-ui-model.js +99 -9
- package/electron/setup-ui-styles.js +22 -0
- package/electron/setup-ui.js +244 -32
- package/electron/workspace-ui/live-dead-seats.js +163 -91
- package/electron/workspace-ui/live-seats.js +4 -4
- package/electron/workspace-ui/workspace-banners.js +30 -7
- package/electron/workspace-ui/workspace-matrix.js +23 -3
- package/electron/workspace-ui/workspace-seats.js +95 -79
- package/package.json +2 -1
- package/schemas/council-run.schema.json +2 -2
- package/schemas/council-tally.schema.json +17 -1
- package/schemas/council-verdict.schema.json +12 -4
- package/schemas/run.schema.json +6 -1
- package/skills/second-opinion/COUNCIL-DESIGN.md +1 -1
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
- package/skills/second-opinion/MODEL-NOTES.md +88 -9
- package/skills/second-opinion/SEAT-BRIEFS.md +36 -4
- package/skills/second-opinion/SKILL.md +151 -36
- package/src/cli-council-run-bench.js +98 -6
- package/src/cli-handlers-council-run.js +18 -6
- package/src/cli-handlers-council.js +57 -7
- package/src/cli-handlers-doctor.js +12 -15
- package/src/cli.js +3 -1
- package/src/council/anonymize.js +2 -1
- package/src/council/briefings-chair-task.js +161 -0
- package/src/council/briefings-chair.js +33 -8
- package/src/council/briefings-debate.js +79 -13
- package/src/council/briefings-stage2-task.js +236 -0
- package/src/council/briefings-stage2.js +103 -26
- package/src/council/briefings-task.js +167 -0
- package/src/council/briefings.js +41 -4
- package/src/council/chair-fallback.js +95 -0
- package/src/council/debate.js +38 -21
- package/src/council/findings.js +3 -2
- package/src/council/ledger.js +2 -2
- package/src/council/parse-stage2.js +64 -16
- package/src/council/report-cost.js +61 -0
- package/src/council/report-html.js +26 -4
- package/src/council/report-md.js +30 -2
- package/src/council/report.js +40 -37
- package/src/council/run-assemble.js +21 -6
- package/src/council/run-chair.js +44 -95
- package/src/council/run-debate-revote.js +81 -49
- package/src/council/run-debate.js +51 -34
- package/src/council/run-finish.js +5 -3
- package/src/council/run-retry-keys.js +4 -4
- package/src/council/run-retry-launch.js +4 -4
- package/src/council/run-retry-notes.js +72 -15
- package/src/council/run-stage1-launch.js +4 -4
- package/src/council/run-stage1-rows.js +9 -6
- package/src/council/run-stage2.js +81 -47
- package/src/council/run-stages.js +9 -21
- package/src/council/run-stats-entry.js +46 -1
- package/src/council/run.js +28 -13
- package/src/council/seats.js +2 -2
- package/src/council/stage1-bind.js +3 -2
- package/src/council/verdict-seat-loss.js +124 -0
- package/src/council/verdict.js +108 -99
- package/src/headless.js +256 -49
- package/src/mcp-council-bench.js +64 -3
- package/src/mcp-council-run.js +10 -3
- package/src/mcp-server.js +52 -12
- package/src/mcp-tools.js +41 -5
- package/src/observe/council-legs.js +2 -2
- package/src/opencode-client.js +19 -1
- package/src/pack/pack-forward.js +15 -12
- package/src/pack/pack-resolve.js +1 -1
- package/src/prompt-builder.js +17 -1
- package/src/sidecar/fanout-leg.js +26 -0
- package/src/sidecar/fanout.js +1 -1
- package/src/sidecar/list-council.js +178 -0
- package/src/sidecar/list-limit.js +3 -1
- package/src/sidecar/list-search.js +2 -1
- package/src/sidecar/models.js +8 -1
- package/src/sidecar/read.js +34 -10
- package/src/sidecar/setup.js +124 -0
- package/src/template/render.js +16 -7
- package/src/utils/alias-audit.js +81 -3
- package/src/utils/alias-shadow-writer.js +220 -0
- package/src/utils/alias-shadow.js +294 -0
- package/src/utils/config.js +1 -1
- package/src/utils/curated-models.js +16 -8
- package/src/utils/degrade.js +12 -5
- package/src/utils/doctor-alias-check.js +149 -0
- package/src/utils/engine-log-parse.js +289 -0
- package/src/utils/engine-log-tail.js +114 -0
- package/src/utils/engine-log.js +250 -0
- package/src/utils/engine-skew-records.js +146 -0
- package/src/utils/engine-skew.js +300 -0
- package/src/utils/gateway-router.js +10 -2
- package/src/utils/model-canonicalization.js +64 -0
- package/src/utils/model-catalog.js +1 -1
- package/src/utils/model-shortlist.js +100 -0
- 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 +24 -0
- package/src/utils/result-schema.js +10 -0
- package/src/utils/text-sanitize.js +81 -0
- package/src/utils/ttft.js +57 -0
- package/src/utils/untrusted-fence.js +111 -1
- package/src/workspace/fold-format.js +28 -7
- package/src/workspace/live-normalize.js +2 -1
- package/src/workspace/matrix-model.js +6 -2
- package/src/workspace/run-detail.js +35 -9
- package/src/workspace/seat-space.js +10 -6
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);
|
|
@@ -77,13 +81,27 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
77
81
|
var selectedProvider = null;
|
|
78
82
|
var modelChoicesData = ${modelChoicesJson};
|
|
79
83
|
var providerNamesData = ${providerNamesJson};
|
|
80
|
-
var defaultAliases = ${defaultAliasesJson};
|
|
84
|
+
var defaultAliases = Object.assign(Object.create(null), ${defaultAliasesJson});
|
|
81
85
|
var PROVIDER_FAMILY_NAMES = ${familyNamesJson};
|
|
82
86
|
var directProviders = ${directProvidersJson};
|
|
83
87
|
var routingChoices = {};
|
|
84
88
|
var explicitRouteChoices = {};
|
|
85
|
-
|
|
86
|
-
|
|
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 = {};
|
|
93
|
+
// T3 (PR 199): every table keyed by USER-controlled alias names is seeded
|
|
94
|
+
// null-prototype, same as defaultAliases above -- an alias literally named
|
|
95
|
+
// __proto__ on a plain {} hits the Object.prototype setter (silent no-op
|
|
96
|
+
// write) and reads back Object.prototype, which then throws on .split('/')
|
|
97
|
+
// in updateAliasesForConfiguredKeys.
|
|
98
|
+
var aliasEdits = Object.create(null);
|
|
99
|
+
var aliasDisplay = Object.create(null);
|
|
100
|
+
// N1: the alias map as loaded from disk (init below), so buildReview can
|
|
101
|
+
// tell an actual change from a value-identical re-write. Stays empty until
|
|
102
|
+
// init resolves (a fresh/offline config has nothing saved yet, so every
|
|
103
|
+
// computed write IS new).
|
|
104
|
+
var savedAliases = Object.create(null);
|
|
87
105
|
window.availableModels = null;
|
|
88
106
|
var keyValid = false, validatedKey = '';
|
|
89
107
|
var $ = function(id) { return document.getElementById(id); };
|
|
@@ -141,6 +159,16 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
141
159
|
window.customDefaultModel = cfg.default;
|
|
142
160
|
}
|
|
143
161
|
if (cfg && cfg.aliases) {
|
|
162
|
+
// N-a (council review, PR 196): a defensive copy, not the live cfg.aliases
|
|
163
|
+
// reference. Nothing in this file currently mutates cfg.aliases after
|
|
164
|
+
// this point (checked: every '.aliases[' site below is a read), so
|
|
165
|
+
// buildReview's N1 diff is not measured to be wrong today -- but the
|
|
166
|
+
// copy is one line and removes a fragile "never mutate this" invariant
|
|
167
|
+
// future edits would otherwise have to remember.
|
|
168
|
+
// T3: null-prototype target -- Object.assign onto a plain {} would
|
|
169
|
+
// route a literal __proto__ alias key through the prototype setter
|
|
170
|
+
// instead of landing it as an own key.
|
|
171
|
+
savedAliases = Object.assign(Object.create(null), cfg.aliases); // N1: buildReview diffs against this
|
|
144
172
|
modelChoicesData.forEach(function(mc) {
|
|
145
173
|
var currentModel = cfg.aliases[mc.alias];
|
|
146
174
|
if (currentModel) {
|
|
@@ -148,6 +176,62 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
148
176
|
for (var i = 0; i < provs.length; i++) {
|
|
149
177
|
if (mc.routes[provs[i]] === currentModel) { routingChoices[mc.alias] = provs[i]; break; }
|
|
150
178
|
}
|
|
179
|
+
// F3: the shortlist <select> is server-rendered with its OWN
|
|
180
|
+
// recommendedId-derived "selected" option and never consults
|
|
181
|
+
// modelChoiceIds at init -- so a saved drill-down pick silently
|
|
182
|
+
// reverted to the family flagship on every reopen (Step 3's
|
|
183
|
+
// alias table showed the saved value while Step 2 showed a
|
|
184
|
+
// different one). Mirror the customDefaultModel restore just
|
|
185
|
+
// above: when the saved alias value names one of THIS card's
|
|
186
|
+
// shortlist rows, seed modelChoiceIds (the .model-pick change
|
|
187
|
+
// handler's own state) and push the same value into the DOM
|
|
188
|
+
// <select> so a no-op reopen-and-Finish round-trips cleanly.
|
|
189
|
+
var sel = document.querySelector('.model-pick[data-alias="' + mc.alias + '"]');
|
|
190
|
+
if (sel) {
|
|
191
|
+
var matchedByValue = false;
|
|
192
|
+
for (var j = 0; j < sel.options.length; j++) {
|
|
193
|
+
if (sel.options[j].value === currentModel) {
|
|
194
|
+
sel.value = currentModel;
|
|
195
|
+
modelChoiceIds[mc.alias] = currentModel;
|
|
196
|
+
modelOpenrouterIds[mc.alias] = sel.options[j].getAttribute('data-or') || null;
|
|
197
|
+
matchedByValue = true;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// F3-OR (council review, issue 138): an explicit-OpenRouter
|
|
202
|
+
// drilled pick is saved as "openrouter/<vendor>/<model>"
|
|
203
|
+
// (pickRouteFor's explicit-OR branch below) -- that form
|
|
204
|
+
// matches no option's bare 'value', only its 'data-or'. Every
|
|
205
|
+
// option carries 'data-or' (CONTROLLER RULING R1, issue 138 --
|
|
206
|
+
// see buildModelPickHTML's docstring in setup-ui-model.js), so
|
|
207
|
+
// search for THAT match when the bare-value search above
|
|
208
|
+
// found nothing.
|
|
209
|
+
// Restoring modelChoiceIds alone here would round-trip the
|
|
210
|
+
// dropdown selection but make Finish write the BARE id back --
|
|
211
|
+
// silently converting the user's explicit "via OpenRouter"
|
|
212
|
+
// choice into direct-first policy routing on the very next
|
|
213
|
+
// save, which is worse than the visible flagship-revert this
|
|
214
|
+
// block exists to fix. So restore the WHOLE state pickRouteFor
|
|
215
|
+
// needs to reproduce the OR form: the option's bare value (for
|
|
216
|
+
// the <select> and modelChoiceIds), its data-or
|
|
217
|
+
// (modelOpenrouterIds), and the two flags pickRouteFor's
|
|
218
|
+
// explicit-OR branch checks -- routingChoices[alias] ===
|
|
219
|
+
// 'openrouter' and explicitRouteChoices[alias] -- which are
|
|
220
|
+
// exactly the flags the route-pill click handler sets, so the
|
|
221
|
+
// rendered pill agrees with the restored state too.
|
|
222
|
+
if (!matchedByValue) {
|
|
223
|
+
for (var k = 0; k < sel.options.length; k++) {
|
|
224
|
+
if (sel.options[k].getAttribute('data-or') === currentModel) {
|
|
225
|
+
sel.value = sel.options[k].value;
|
|
226
|
+
modelChoiceIds[mc.alias] = sel.options[k].value;
|
|
227
|
+
modelOpenrouterIds[mc.alias] = currentModel;
|
|
228
|
+
routingChoices[mc.alias] = 'openrouter';
|
|
229
|
+
explicitRouteChoices[mc.alias] = true;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
151
235
|
}
|
|
152
236
|
});
|
|
153
237
|
Object.keys(cfg.aliases).forEach(function(k) {
|
|
@@ -224,6 +308,20 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
224
308
|
// the row's first route. Returns the full model id or null.
|
|
225
309
|
function pickRouteFor(mc) {
|
|
226
310
|
if (!mc) { return null; }
|
|
311
|
+
// issue 138: an explicit per-model choice overrides the family flagship.
|
|
312
|
+
var picked = modelChoiceIds[mc.alias];
|
|
313
|
+
if (picked) {
|
|
314
|
+
// Deliberately NEVER toBareIfDirect(picked) here (unlike the auto-pick
|
|
315
|
+
// canonicalization below): picked/modelOpenrouterIds come straight
|
|
316
|
+
// from the shortlist's own id/data-or, and for a DIVERGENT_VENDOR
|
|
317
|
+
// (e.g. anthropic) that id can already BE its only-callable
|
|
318
|
+
// openrouter/<vendor>/... form -- stripping the prefix would
|
|
319
|
+
// fabricate a direct id nothing serves.
|
|
320
|
+
if (routingChoices[mc.alias] === 'openrouter' && explicitRouteChoices[mc.alias]) {
|
|
321
|
+
return modelOpenrouterIds[mc.alias] || picked;
|
|
322
|
+
}
|
|
323
|
+
return picked;
|
|
324
|
+
}
|
|
227
325
|
var provs = Object.keys(mc.routes);
|
|
228
326
|
var prov = routingChoices[mc.alias];
|
|
229
327
|
if (!prov || !mc.routes[prov]) {
|
|
@@ -363,21 +461,53 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
363
461
|
kn.length > 0 ? kn.map(function(k) { return k + ' \\u2713'; }).join(', ') : 'None';
|
|
364
462
|
var r = document.querySelector('input[name="default-model"]:checked');
|
|
365
463
|
document.getElementById('review-model').textContent = window.customDefaultModel || (r ? r.value : 'Not selected');
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
464
|
+
// F4: mirror the EXACT call Finish makes (collectAliasWrites), not just
|
|
465
|
+
// the checked radio, so Step 4 can never under-report what Finish is
|
|
466
|
+
// about to write. Before this fix, buildReview re-derived "writes" from
|
|
467
|
+
// the checked radio alone -- a drilled-down pick on a card that was NOT
|
|
468
|
+
// the checked default (or any drilled pick at all, under a custom
|
|
469
|
+
// default) was invisible here and fell through to the literal 'No
|
|
470
|
+
// alias changes', even though Finish wrote it. sidecar:save-config has
|
|
471
|
+
// no confirmation step, so this review IS the only gate.
|
|
472
|
+
var aliasWritesPreview = collectAliasWrites(r ? r.value : null, !!window.customDefaultModel);
|
|
473
|
+
// N1: aliasWritesPreview is EVERY alias Finish would write, including
|
|
474
|
+
// ones whose value is already what's on disk -- after the F1 fix that
|
|
475
|
+
// is the NORMAL case on a plain reopen (init below seeds modelChoiceIds
|
|
476
|
+
// from cfg.aliases, so the drilled-down alias, the recommendedId, and
|
|
477
|
+
// the saved value are now the same string by construction). Finish
|
|
478
|
+
// still writes the full map (a value-identical write is harmless), but
|
|
479
|
+
// this review must only SHOW entries that actually differ from
|
|
480
|
+
// savedAliases -- otherwise a no-op reopen reports "N alias(es)
|
|
481
|
+
// modified" on the one screen that has no confirmation step after it.
|
|
482
|
+
// N-b (council review, PR 196): an alias that was NEVER in savedAliases
|
|
483
|
+
// reads as undefined there, while a delete-write (Step 3's delete
|
|
484
|
+
// button, for one of the five default aliases -- see aliasEdits[alias]
|
|
485
|
+
// = null in setup-ui-alias-script.js) is null. null !== undefined is
|
|
486
|
+
// true, so without this normalization a default alias that was never
|
|
487
|
+
// explicitly saved (a config written by an older/partial flow that
|
|
488
|
+
// skipped default-seeding -- addAlias() in src/sidecar/setup.js is one
|
|
489
|
+
// such path) would show "alias -> (deleted)" for deleting something
|
|
490
|
+
// that was never there. saveConfig() already drops falsy alias values
|
|
491
|
+
// (src/utils/config.js), so that write is a true no-op on disk; the
|
|
492
|
+
// review must agree. Reading savedAliases[alias] as null (not
|
|
493
|
+
// undefined) when the key is absent makes "delete of an absent alias"
|
|
494
|
+
// compare equal to "still absent" without changing any other case --
|
|
495
|
+
// every other savedAliases value here is a non-empty string.
|
|
496
|
+
// T3: keyed by user alias names -- null-prototype (see aliasEdits above)
|
|
497
|
+
var changedWrites = Object.create(null);
|
|
498
|
+
Object.keys(aliasWritesPreview).forEach(function(alias) {
|
|
499
|
+
var oldVal = Object.prototype.hasOwnProperty.call(savedAliases, alias) ? savedAliases[alias] : null;
|
|
500
|
+
if (aliasWritesPreview[alias] !== oldVal) {
|
|
501
|
+
changedWrites[alias] = aliasWritesPreview[alias];
|
|
372
502
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
503
|
+
});
|
|
504
|
+
var writes = Object.keys(changedWrites).map(function(alias) {
|
|
505
|
+
var val = changedWrites[alias];
|
|
506
|
+
return alias + ' \\u2192 ' + (val === null ? '(deleted)' : val);
|
|
507
|
+
});
|
|
378
508
|
document.getElementById('review-routing').textContent =
|
|
379
509
|
writes.length > 0 ? writes.join(', ') : 'No alias changes';
|
|
380
|
-
var editCount = Object.keys(
|
|
510
|
+
var editCount = Object.keys(changedWrites).length;
|
|
381
511
|
var reviewAliases = document.getElementById('review-aliases');
|
|
382
512
|
if (reviewAliases) {
|
|
383
513
|
reviewAliases.textContent = editCount > 0 ? editCount + ' alias(es) modified' : 'No changes';
|
|
@@ -401,27 +531,98 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
401
531
|
updateWritePreviews();
|
|
402
532
|
});
|
|
403
533
|
|
|
534
|
+
// issue 138: model-level drill-down <select> change handler.
|
|
535
|
+
document.addEventListener('change', function(e) {
|
|
536
|
+
var sel = e.target && e.target.closest ? e.target.closest('.model-pick') : null;
|
|
537
|
+
if (!sel) { return; }
|
|
538
|
+
var alias = sel.getAttribute('data-alias');
|
|
539
|
+
if (!alias) { return; }
|
|
540
|
+
modelChoiceIds[alias] = sel.value;
|
|
541
|
+
var opt = sel.options[sel.selectedIndex];
|
|
542
|
+
modelOpenrouterIds[alias] = (opt && opt.getAttribute('data-or')) || null;
|
|
543
|
+
updateWritePreviews();
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// issue 138 (fix round 1, Finding 1; precedence description corrected in
|
|
547
|
+
// the F2/F6 fix wave; modelChoiceIds provenance corrected in the N2 fix
|
|
548
|
+
// wave): assemble aliasWrites for Finish -- the aliasEdits (Step 3)
|
|
549
|
+
// overlay first; THEN, only for a checked quick-pick default (not a
|
|
550
|
+
// custom/searched one -- isCustomDefault skips this stage entirely), the
|
|
551
|
+
// selected alias's resolved route; then every OTHER alias whose
|
|
552
|
+
// drill-down <select> fired change -- "every OTHER" means every alias
|
|
553
|
+
// but the selected one ONLY when that earlier stage ran, so under a
|
|
554
|
+
// custom default the selected alias (if it has a drilled pick) is
|
|
555
|
+
// handled by THIS stage instead, not skipped. Before the Task-3 dropdown
|
|
556
|
+
// existed there was nothing to lose by writing only the selected alias;
|
|
557
|
+
// now a drilled-down pick on a card that is NOT the checked default
|
|
558
|
+
// would silently vanish without this. Precedence is NOT uniform across
|
|
559
|
+
// the two groups, unlike an earlier version of this comment claimed:
|
|
560
|
+
// only the selected alias clobbers its aliasEdits entry (the ONE place
|
|
561
|
+
// that's permitted -- user-locked decision #2); every OTHER drilled-down
|
|
562
|
+
// alias defers to aliasEdits and is written only when Step 3 left it
|
|
563
|
+
// untouched (see the hasOwnProperty guard below and ruling R6a, which
|
|
564
|
+
// already described the real behaviour correctly).
|
|
565
|
+
//
|
|
566
|
+
// modelChoiceIds is populated from TWO places, not one, unlike an
|
|
567
|
+
// earlier version of this comment claimed: the change handler above
|
|
568
|
+
// (a live drill-down pick), AND the init restore block (F3) -- which
|
|
569
|
+
// seeds it from cfg.aliases for every card whose SAVED value already
|
|
570
|
+
// names one of its shortlist rows, reading the id back out of that
|
|
571
|
+
// card's own server-rendered <option> elements. After the F1 fix that is
|
|
572
|
+
// the NORMAL case, not an edge case: a card the user never touched in
|
|
573
|
+
// THIS session routinely lands in modelChoiceIds anyway, because its
|
|
574
|
+
// saved value already matches its recommendedId. That is still correct
|
|
575
|
+
// to iterate here -- collectAliasWrites' job is to compute what Finish
|
|
576
|
+
// SHOULD write, and a value-identical write is harmless -- but it does
|
|
577
|
+
// mean this function can no longer be read as "only ever fires for
|
|
578
|
+
// aliases the user actually changed this session". buildReview is the
|
|
579
|
+
// layer responsible for not SHOWING those value-identical entries as
|
|
580
|
+
// changes (see its own N1 comment).
|
|
581
|
+
function collectAliasWrites(selectedAlias, isCustomDefault) {
|
|
582
|
+
// T3: keyed by user alias names -- null-prototype (see aliasEdits above)
|
|
583
|
+
var aliasWrites = Object.create(null);
|
|
584
|
+
Object.keys(aliasEdits).forEach(function(k) {
|
|
585
|
+
aliasWrites[k] = aliasEdits[k];
|
|
586
|
+
});
|
|
587
|
+
function writeAliasRoute(alias) {
|
|
588
|
+
var mc = null;
|
|
589
|
+
for (var i = 0; i < modelChoicesData.length; i++) {
|
|
590
|
+
if (modelChoicesData[i].alias === alias) { mc = modelChoicesData[i]; break; }
|
|
591
|
+
}
|
|
592
|
+
if (!mc) { return; }
|
|
593
|
+
var routeId = pickRouteFor(mc);
|
|
594
|
+
if (routeId) { aliasWrites[mc.alias] = routeId; }
|
|
595
|
+
}
|
|
596
|
+
if (!isCustomDefault && selectedAlias) {
|
|
597
|
+
// Selecting a quick pick = explicit touch: upgrade that ONE alias
|
|
598
|
+
// to the resolved id via the chosen route (user-locked decision #2).
|
|
599
|
+
// This is the ONE place clobbering aliasEdits is permitted.
|
|
600
|
+
writeAliasRoute(selectedAlias);
|
|
601
|
+
}
|
|
602
|
+
// issue 138 (fix round 2, ruling R6a): every OTHER drilled-down alias is
|
|
603
|
+
// written ONLY when Step 3 left it untouched. hasOwnProperty.call, not
|
|
604
|
+
// the in operator and not a truthiness/not-undefined check:
|
|
605
|
+
// aliasEdits[alias] === null is a MEANINGFUL value here (ipc-setup.js:
|
|
606
|
+
// string = set, null = delete), so a falsy/undefined check would
|
|
607
|
+
// silently resurrect an alias the user explicitly deleted in Step 3,
|
|
608
|
+
// and the in operator or a not-undefined check can be fooled by an
|
|
609
|
+
// inherited Object.prototype property name (this codebase has been
|
|
610
|
+
// bitten by that class of bug before -- see the proto:null guard in
|
|
611
|
+
// src/sidecar/setup.js resolveChoice).
|
|
612
|
+
Object.keys(modelChoiceIds).forEach(function(alias) {
|
|
613
|
+
if (!isCustomDefault && alias === selectedAlias) { return; } // handled above, but only in that branch
|
|
614
|
+
if (Object.prototype.hasOwnProperty.call(aliasEdits, alias)) { return; }
|
|
615
|
+
writeAliasRoute(alias);
|
|
616
|
+
});
|
|
617
|
+
return aliasWrites;
|
|
618
|
+
}
|
|
619
|
+
|
|
404
620
|
finishBtn.addEventListener('click', async function() {
|
|
405
621
|
finishBtn.disabled = true; finishBtn.textContent = 'Saving...';
|
|
406
622
|
try {
|
|
407
623
|
var r = document.querySelector('input[name="default-model"]:checked');
|
|
408
624
|
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
|
-
}
|
|
625
|
+
var aliasWrites = collectAliasWrites(r ? r.value : null, !!window.customDefaultModel);
|
|
425
626
|
await window.sidecarSetup.invoke('sidecar:save-config', dm, aliasWrites, (window.collectCouncilPicks && window.collectCouncilPicks()) || []);
|
|
426
627
|
var kc = Object.values(configuredKeys).filter(function(v) { return v; }).length;
|
|
427
628
|
await window.sidecarSetup.invoke('sidecar:setup-done', dm, kc);
|
|
@@ -551,6 +752,17 @@ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, d
|
|
|
551
752
|
var idEl = el.querySelector('.write-preview-id');
|
|
552
753
|
if (idEl && routeId) { idEl.textContent = routeId; }
|
|
553
754
|
});
|
|
755
|
+
// issue 138: keep the resolved-id line in step with the route/model choice.
|
|
756
|
+
document.querySelectorAll('.model-resolved').forEach(function(el) {
|
|
757
|
+
var alias = el.getAttribute('data-alias');
|
|
758
|
+
var mc = null;
|
|
759
|
+
for (var i = 0; i < modelChoicesData.length; i++) {
|
|
760
|
+
if (modelChoicesData[i].alias === alias) { mc = modelChoicesData[i]; break; }
|
|
761
|
+
}
|
|
762
|
+
if (!mc) { return; }
|
|
763
|
+
var id = pickRouteFor(mc);
|
|
764
|
+
if (id) { el.textContent = id; }
|
|
765
|
+
});
|
|
554
766
|
}
|
|
555
767
|
|
|
556
768
|
document.addEventListener('input', function(e) {
|