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.
Files changed (118) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +355 -0
  3. package/README.md +10 -5
  4. package/docs/CITATIONS.md +13 -5
  5. package/docs/ROADMAP.md +101 -10
  6. package/docs/configuration.md +55 -5
  7. package/docs/council.md +102 -14
  8. package/docs/troubleshooting.md +9 -2
  9. package/docs/usage.md +128 -12
  10. package/electron/ipc-setup.js +39 -2
  11. package/electron/main.js +46 -3
  12. package/electron/offer-session.js +51 -0
  13. package/electron/setup-ui-model.js +99 -9
  14. package/electron/setup-ui-styles.js +22 -0
  15. package/electron/setup-ui.js +244 -32
  16. package/electron/workspace-ui/live-dead-seats.js +163 -91
  17. package/electron/workspace-ui/live-seats.js +4 -4
  18. package/electron/workspace-ui/workspace-banners.js +30 -7
  19. package/electron/workspace-ui/workspace-matrix.js +23 -3
  20. package/electron/workspace-ui/workspace-seats.js +95 -79
  21. package/package.json +2 -1
  22. package/schemas/council-run.schema.json +2 -2
  23. package/schemas/council-tally.schema.json +17 -1
  24. package/schemas/council-verdict.schema.json +12 -4
  25. package/schemas/run.schema.json +6 -1
  26. package/skills/second-opinion/COUNCIL-DESIGN.md +1 -1
  27. package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
  28. package/skills/second-opinion/MODEL-NOTES.md +88 -9
  29. package/skills/second-opinion/SEAT-BRIEFS.md +36 -4
  30. package/skills/second-opinion/SKILL.md +151 -36
  31. package/src/cli-council-run-bench.js +98 -6
  32. package/src/cli-handlers-council-run.js +18 -6
  33. package/src/cli-handlers-council.js +57 -7
  34. package/src/cli-handlers-doctor.js +12 -15
  35. package/src/cli.js +3 -1
  36. package/src/council/anonymize.js +2 -1
  37. package/src/council/briefings-chair-task.js +161 -0
  38. package/src/council/briefings-chair.js +33 -8
  39. package/src/council/briefings-debate.js +79 -13
  40. package/src/council/briefings-stage2-task.js +236 -0
  41. package/src/council/briefings-stage2.js +103 -26
  42. package/src/council/briefings-task.js +167 -0
  43. package/src/council/briefings.js +41 -4
  44. package/src/council/chair-fallback.js +95 -0
  45. package/src/council/debate.js +38 -21
  46. package/src/council/findings.js +3 -2
  47. package/src/council/ledger.js +2 -2
  48. package/src/council/parse-stage2.js +64 -16
  49. package/src/council/report-cost.js +61 -0
  50. package/src/council/report-html.js +26 -4
  51. package/src/council/report-md.js +30 -2
  52. package/src/council/report.js +40 -37
  53. package/src/council/run-assemble.js +21 -6
  54. package/src/council/run-chair.js +44 -95
  55. package/src/council/run-debate-revote.js +81 -49
  56. package/src/council/run-debate.js +51 -34
  57. package/src/council/run-finish.js +5 -3
  58. package/src/council/run-retry-keys.js +4 -4
  59. package/src/council/run-retry-launch.js +4 -4
  60. package/src/council/run-retry-notes.js +72 -15
  61. package/src/council/run-stage1-launch.js +4 -4
  62. package/src/council/run-stage1-rows.js +9 -6
  63. package/src/council/run-stage2.js +81 -47
  64. package/src/council/run-stages.js +9 -21
  65. package/src/council/run-stats-entry.js +46 -1
  66. package/src/council/run.js +28 -13
  67. package/src/council/seats.js +2 -2
  68. package/src/council/stage1-bind.js +3 -2
  69. package/src/council/verdict-seat-loss.js +124 -0
  70. package/src/council/verdict.js +108 -99
  71. package/src/headless.js +256 -49
  72. package/src/mcp-council-bench.js +64 -3
  73. package/src/mcp-council-run.js +10 -3
  74. package/src/mcp-server.js +52 -12
  75. package/src/mcp-tools.js +41 -5
  76. package/src/observe/council-legs.js +2 -2
  77. package/src/opencode-client.js +19 -1
  78. package/src/pack/pack-forward.js +15 -12
  79. package/src/pack/pack-resolve.js +1 -1
  80. package/src/prompt-builder.js +17 -1
  81. package/src/sidecar/fanout-leg.js +26 -0
  82. package/src/sidecar/fanout.js +1 -1
  83. package/src/sidecar/list-council.js +178 -0
  84. package/src/sidecar/list-limit.js +3 -1
  85. package/src/sidecar/list-search.js +2 -1
  86. package/src/sidecar/models.js +8 -1
  87. package/src/sidecar/read.js +34 -10
  88. package/src/sidecar/setup.js +124 -0
  89. package/src/template/render.js +16 -7
  90. package/src/utils/alias-audit.js +81 -3
  91. package/src/utils/alias-shadow-writer.js +220 -0
  92. package/src/utils/alias-shadow.js +294 -0
  93. package/src/utils/config.js +1 -1
  94. package/src/utils/curated-models.js +16 -8
  95. package/src/utils/degrade.js +12 -5
  96. package/src/utils/doctor-alias-check.js +149 -0
  97. package/src/utils/engine-log-parse.js +289 -0
  98. package/src/utils/engine-log-tail.js +114 -0
  99. package/src/utils/engine-log.js +250 -0
  100. package/src/utils/engine-skew-records.js +146 -0
  101. package/src/utils/engine-skew.js +300 -0
  102. package/src/utils/gateway-router.js +10 -2
  103. package/src/utils/model-canonicalization.js +64 -0
  104. package/src/utils/model-catalog.js +1 -1
  105. package/src/utils/model-shortlist.js +100 -0
  106. package/src/utils/provider-default-picker.js +93 -45
  107. package/src/utils/provider-default-prompt.js +1 -1
  108. package/src/utils/quick-picks.js +2 -2
  109. package/src/utils/remediation-hints.js +24 -0
  110. package/src/utils/result-schema.js +10 -0
  111. package/src/utils/text-sanitize.js +81 -0
  112. package/src/utils/ttft.js +57 -0
  113. package/src/utils/untrusted-fence.js +111 -1
  114. package/src/workspace/fold-format.js +28 -7
  115. package/src/workspace/live-normalize.js +2 -1
  116. package/src/workspace/matrix-model.js +6 -2
  117. package/src/workspace/run-detail.js +35 -9
  118. package/src/workspace/seat-space.js +10 -6
@@ -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
- var aliasEdits = {};
86
- var aliasDisplay = {};
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
- var writes = [];
367
- var r2 = document.querySelector('input[name="default-model"]:checked');
368
- if (!window.customDefaultModel && r2) {
369
- var mc2 = null;
370
- for (var i2 = 0; i2 < modelChoicesData.length; i2++) {
371
- if (modelChoicesData[i2].alias === r2.value) { mc2 = modelChoicesData[i2]; break; }
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
- if (mc2) {
374
- var routeId2 = pickRouteFor(mc2);
375
- if (routeId2) { writes.push(mc2.alias + ' \\u2192 ' + routeId2); }
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(aliasEdits).length;
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) {