amicus 1.0.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 (93) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +477 -0
  4. package/bin/amicus.js +382 -0
  5. package/electron/assets/icon.png +0 -0
  6. package/electron/assets/icon.svg +5 -0
  7. package/electron/fold.js +163 -0
  8. package/electron/ipc-setup.js +176 -0
  9. package/electron/load-failsafe.js +85 -0
  10. package/electron/main.js +468 -0
  11. package/electron/preload-setup.js +38 -0
  12. package/electron/preload.js +33 -0
  13. package/electron/setup-ui-alias-script.js +218 -0
  14. package/electron/setup-ui-aliases.js +85 -0
  15. package/electron/setup-ui-keys-script.js +115 -0
  16. package/electron/setup-ui-keys.js +97 -0
  17. package/electron/setup-ui-model.js +138 -0
  18. package/electron/setup-ui-styles.js +327 -0
  19. package/electron/setup-ui.js +465 -0
  20. package/electron/summary.js +118 -0
  21. package/electron/toolbar.js +229 -0
  22. package/electron/window-position.js +35 -0
  23. package/package.json +98 -0
  24. package/scripts/postinstall.js +193 -0
  25. package/scripts/setup-hooks.js +42 -0
  26. package/skill/SKILL.md +976 -0
  27. package/skills/second-opinion/COUNCIL-DESIGN.md +227 -0
  28. package/skills/second-opinion/MODEL-NOTES.md +104 -0
  29. package/skills/second-opinion/SKILL.md +389 -0
  30. package/src/cli-handlers.js +188 -0
  31. package/src/cli.js +400 -0
  32. package/src/conflict.js +144 -0
  33. package/src/context-compression.js +102 -0
  34. package/src/context.js +199 -0
  35. package/src/drift.js +144 -0
  36. package/src/environment.js +157 -0
  37. package/src/headless.js +742 -0
  38. package/src/index.js +106 -0
  39. package/src/jsonl-parser.js +180 -0
  40. package/src/mcp-server.js +625 -0
  41. package/src/mcp-tools.js +407 -0
  42. package/src/opencode-client.js +615 -0
  43. package/src/prompt-builder.js +355 -0
  44. package/src/prompts/cowork-agent-prompt.js +118 -0
  45. package/src/session-manager.js +414 -0
  46. package/src/session.js +180 -0
  47. package/src/sidecar/context-builder.js +297 -0
  48. package/src/sidecar/continue.js +212 -0
  49. package/src/sidecar/crash-handler.js +56 -0
  50. package/src/sidecar/fanout-leg.js +107 -0
  51. package/src/sidecar/fanout-output.js +46 -0
  52. package/src/sidecar/fanout.js +236 -0
  53. package/src/sidecar/interactive.js +217 -0
  54. package/src/sidecar/models.js +135 -0
  55. package/src/sidecar/progress.js +218 -0
  56. package/src/sidecar/read.js +183 -0
  57. package/src/sidecar/resume.js +221 -0
  58. package/src/sidecar/session-utils.js +288 -0
  59. package/src/sidecar/setup-window.js +79 -0
  60. package/src/sidecar/setup.js +280 -0
  61. package/src/sidecar/start.js +251 -0
  62. package/src/utils/agent-mapping.js +138 -0
  63. package/src/utils/alias-audit.js +98 -0
  64. package/src/utils/alias-resolver.js +77 -0
  65. package/src/utils/api-key-store.js +259 -0
  66. package/src/utils/api-key-validation.js +97 -0
  67. package/src/utils/auth-json.js +109 -0
  68. package/src/utils/config.js +291 -0
  69. package/src/utils/curated-models.js +82 -0
  70. package/src/utils/env-compat.js +38 -0
  71. package/src/utils/env-loader.js +54 -0
  72. package/src/utils/idle-watchdog.js +225 -0
  73. package/src/utils/input-validators.js +127 -0
  74. package/src/utils/lifecycle.js +43 -0
  75. package/src/utils/logger.js +84 -0
  76. package/src/utils/mcp-discovery.js +194 -0
  77. package/src/utils/mcp-validators.js +78 -0
  78. package/src/utils/model-catalog.js +103 -0
  79. package/src/utils/model-fetcher.js +179 -0
  80. package/src/utils/model-validator.js +207 -0
  81. package/src/utils/path-setup.js +41 -0
  82. package/src/utils/port-pid.js +39 -0
  83. package/src/utils/prompt-source.js +53 -0
  84. package/src/utils/result-schema.js +261 -0
  85. package/src/utils/server-setup.js +93 -0
  86. package/src/utils/session-abort.js +53 -0
  87. package/src/utils/session-lock.js +95 -0
  88. package/src/utils/shared-server.js +216 -0
  89. package/src/utils/start-helpers.js +76 -0
  90. package/src/utils/thinking-validators.js +92 -0
  91. package/src/utils/update-notifier-loader.js +18 -0
  92. package/src/utils/updater.js +157 -0
  93. package/src/utils/validators.js +300 -0
@@ -0,0 +1,465 @@
1
+ /** Setup UI - Wizard Orchestrator: API Keys → Models → Aliases → Review */
2
+ const { buildKeysStepHTML, PROVIDERS } = require('./setup-ui-keys');
3
+ const { buildModelStepHTML, MODEL_CHOICES, PROVIDER_NAMES } = require('./setup-ui-model');
4
+ const { buildAliasEditorHTML } = require('./setup-ui-aliases');
5
+ const { buildWizardCSS } = require('./setup-ui-styles');
6
+ const { buildKeysScript } = require('./setup-ui-keys-script');
7
+ const { buildAliasScript } = require('./setup-ui-alias-script');
8
+ const { getDefaultAliases } = require('../src/utils/config');
9
+ const { getBrandName } = require('./toolbar');
10
+
11
+ /**
12
+ * @param {object} [options={}]
13
+ * @param {string} [options.client='code-local'] - Client type for branding
14
+ */
15
+ function buildSetupHTML(options = {}) {
16
+ const { client = 'code-local' } = options;
17
+ const brandName = getBrandName(client);
18
+ const keysHtml = buildKeysStepHTML(PROVIDERS);
19
+ const modelHtml = buildModelStepHTML(MODEL_CHOICES);
20
+ const aliasHtml = buildAliasEditorHTML(getDefaultAliases());
21
+ const css = buildWizardCSS();
22
+ const providersJson = JSON.stringify(PROVIDERS);
23
+ const modelChoicesJson = JSON.stringify(MODEL_CHOICES);
24
+ const providerNamesJson = JSON.stringify(PROVIDER_NAMES);
25
+ const defaultAliasesJson = JSON.stringify(getDefaultAliases());
26
+ return `<!DOCTYPE html>
27
+ <html><head><meta charset="utf-8"><title>Amicus Setup</title>
28
+ <style>${css}</style></head><body>
29
+ <div class="header"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 2v12" stroke="#D97757" stroke-width="2" stroke-linecap="round"/><path d="M10 2v5c0 2-3 3-7 5" stroke="#D97757" stroke-width="2" stroke-linecap="round" stroke-opacity="0.6"/></svg><span class="header-title">${brandName} Setup</span></div>
30
+ <div class="progress-bar"><div class="progress-step active" id="step-1"><span class="progress-dot">1</span><span>API Keys</span></div><div class="progress-connector"></div><div class="progress-step" id="step-2"><span class="progress-dot">2</span><span>Models</span></div><div class="progress-connector"></div><div class="progress-step" id="step-3"><span class="progress-dot">3</span><span>Routing</span></div><div class="progress-connector"></div><div class="progress-step" id="step-4"><span class="progress-dot">4</span><span>Review</span></div></div>
31
+ <div class="content">
32
+ <div class="wizard-step visible" id="wizard-step-1"><div id="import-notice"></div>${keysHtml}</div>
33
+ <div class="wizard-step" id="wizard-step-2">${modelHtml}</div>
34
+ <div class="wizard-step" id="wizard-step-3">${aliasHtml}</div>
35
+ <div class="wizard-step" id="wizard-step-4">
36
+ <div class="step-content">
37
+ <h1>Setup Complete</h1>
38
+ <p class="subtitle">Review your configuration before saving.</p>
39
+ <div class="review-section"><div class="review-label">API Keys</div><div class="review-value" id="review-keys">None configured</div></div>
40
+ <div class="review-section"><div class="review-label">Default Model</div><div class="review-value" id="review-model">Not selected</div></div>
41
+ <div class="review-section"><div class="review-label">Routing</div><div class="review-value" id="review-routing">&mdash;</div></div>
42
+ <div class="review-section"><div class="review-label">Aliases</div><div class="review-value" id="review-aliases">&mdash;</div></div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ <div class="footer"><div class="footer-brand"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M3 2v12" stroke="#D97757" stroke-width="2" stroke-linecap="round"/><path d="M10 2v5c0 2-3 3-7 5" stroke="#D97757" stroke-width="2" stroke-linecap="round" stroke-opacity="0.6"/></svg> ${brandName}</div><div class="footer-nav"><button class="nav-btn" id="back-btn" style="display:none">Back</button><button class="nav-btn primary" id="next-btn" disabled>Next</button><button class="nav-btn primary" id="finish-btn" style="display:none">Finish</button></div></div>
47
+ ${buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, defaultAliasesJson)}
48
+ </body></html>`;
49
+ }
50
+
51
+ function buildWizardScript(providersJson, modelChoicesJson, providerNamesJson, defaultAliasesJson) {
52
+ const keysJs = buildKeysScript();
53
+ const aliasJs = buildAliasScript();
54
+ return `<script>
55
+ window.onerror = function(msg, src, line, col, err) { console.error('WIZARD ERROR:', msg, 'at', src, line, col, err); };
56
+ window.onunhandledrejection = function(e) { console.error('WIZARD UNHANDLED REJECTION:', e.reason); };
57
+
58
+ var providers = ${providersJson};
59
+ var currentStep = 1, configuredKeys = {}, keyHints = {};
60
+ var selectedProvider = null;
61
+ var modelChoicesData = ${modelChoicesJson};
62
+ var providerNamesData = ${providerNamesJson};
63
+ var defaultAliases = ${defaultAliasesJson};
64
+ var routingChoices = {};
65
+ var aliasEdits = {};
66
+ window.availableModels = null;
67
+ var keyValid = false, validatedKey = '';
68
+ var $ = function(id) { return document.getElementById(id); };
69
+ var keyInput = $('api-key-input'), testBtn = $('test-btn'), eyeBtn = $('eye-btn');
70
+ var removeBtn = $('remove-btn'), nextBtn = $('next-btn'), backBtn = $('back-btn');
71
+ var finishBtn = $('finish-btn'), statusMsg = $('status-msg');
72
+ var keySection = $('key-section'), keyLabel = $('key-label'), helpLink = $('help-link');
73
+
74
+ // Init: load existing keys
75
+ (async function() {
76
+ try {
77
+ var data = await window.sidecarSetup.invoke('sidecar:get-api-keys');
78
+ if (data && data.status) {
79
+ Object.keys(data.status).forEach(function(p) {
80
+ if (data.status[p]) {
81
+ configuredKeys[p] = true;
82
+ var c = document.getElementById('check-' + p);
83
+ if (c) { c.textContent = '\\u2713'; }
84
+ }
85
+ });
86
+ if (data.hints) { keyHints = data.hints; }
87
+ updateNextState();
88
+ if (data.imported && data.imported.length > 0) {
89
+ var notice = document.getElementById('import-notice');
90
+ if (notice) {
91
+ var noticeDiv = document.createElement('div');
92
+ noticeDiv.className = 'import-notice';
93
+ noticeDiv.textContent = 'Imported ' + data.imported.length + ' key(s) from OpenCode: ' + data.imported.join(', ');
94
+ var dismissBtn = document.createElement('span');
95
+ dismissBtn.className = 'dismiss';
96
+ dismissBtn.textContent = String.fromCharCode(0xD7);
97
+ dismissBtn.addEventListener('click', function() { notice.removeChild(noticeDiv); });
98
+ noticeDiv.appendChild(dismissBtn);
99
+ notice.appendChild(noticeDiv);
100
+ }
101
+ }
102
+ }
103
+ } catch (_e) {}
104
+ })();
105
+
106
+ // Init: load existing config for model pre-selection and alias edits
107
+ (async function() {
108
+ try {
109
+ var cfg = await window.sidecarSetup.invoke('sidecar:get-config');
110
+ if (cfg && cfg.default) {
111
+ document.querySelectorAll('input[name="default-model"]').forEach(function(r) {
112
+ r.checked = (r.value === cfg.default);
113
+ });
114
+ }
115
+ if (cfg && cfg.default && cfg.default.indexOf('/') !== -1) {
116
+ // F5: a search-picked full model id matches no radio — restore it so
117
+ // reopening setup and clicking Finish never silently reverts the default.
118
+ window.customDefaultModel = cfg.default;
119
+ }
120
+ if (cfg && cfg.aliases) {
121
+ modelChoicesData.forEach(function(mc) {
122
+ var currentModel = cfg.aliases[mc.alias];
123
+ if (currentModel) {
124
+ var provs = Object.keys(mc.routes);
125
+ for (var i = 0; i < provs.length; i++) {
126
+ if (mc.routes[provs[i]] === currentModel) { routingChoices[mc.alias] = provs[i]; break; }
127
+ }
128
+ }
129
+ });
130
+ Object.keys(cfg.aliases).forEach(function(k) {
131
+ if (cfg.aliases[k] !== defaultAliases[k]) { aliasEdits[k] = cfg.aliases[k]; }
132
+ });
133
+ applyAliasEditsToUI();
134
+ }
135
+ } catch (_e) {}
136
+ })();
137
+
138
+ function applyAliasEditsToUI() {
139
+ Object.keys(aliasEdits).forEach(function(k) {
140
+ var row = document.querySelector('.alias-row[data-alias="' + k + '"]');
141
+ if (!row) { return; }
142
+ if (aliasEdits[k] === null) { row.classList.add('alias-deleted'); return; }
143
+ var modelSpan = row.querySelector('.alias-model');
144
+ if (modelSpan) { modelSpan.textContent = aliasEdits[k]; }
145
+ });
146
+ }
147
+
148
+ function showStep(step) {
149
+ currentStep = step;
150
+ [1, 2, 3, 4].forEach(function(s) {
151
+ document.getElementById('wizard-step-' + s).classList.toggle('visible', s === step);
152
+ var prog = document.getElementById('step-' + s);
153
+ prog.classList.remove('active', 'done');
154
+ if (s < step) { prog.classList.add('done'); }
155
+ if (s === step) { prog.classList.add('active'); }
156
+ });
157
+ backBtn.style.display = step > 1 ? '' : 'none';
158
+ nextBtn.style.display = step < 4 ? '' : 'none';
159
+ finishBtn.style.display = step === 4 ? '' : 'none';
160
+ if (step === 4) { buildReview(); }
161
+ if (step === 2) { updateRoutingPills(); ensureCatalogLoaded(); }
162
+ if (step === 3) {
163
+ updateAliasRoutes();
164
+ if (!window.availableModels) { fetchAvailableModels(); }
165
+ }
166
+ updateNextState();
167
+ }
168
+
169
+ function updateNextState() {
170
+ if (currentStep === 1) {
171
+ nextBtn.disabled = !Object.values(configuredKeys).some(function(v) { return v; });
172
+ } else { nextBtn.disabled = false; }
173
+ }
174
+
175
+ function updateRoutingPills() {
176
+ var hasAnyKey = Object.values(configuredKeys).some(function(v) { return v; });
177
+ var firstAvailableAlias = null;
178
+ modelChoicesData.forEach(function(mc) {
179
+ var provs = Object.keys(mc.routes);
180
+ var available = provs.filter(function(p) { return configuredKeys[p]; });
181
+ var card = document.querySelector('.model-card input[value="' + mc.alias + '"]');
182
+ var cardLabel = card ? card.closest('.model-card') : null;
183
+ var isAvailable = !hasAnyKey || available.length > 0;
184
+
185
+ // Enable/disable the model card
186
+ if (card) {
187
+ card.disabled = !isAvailable;
188
+ if (cardLabel) {
189
+ cardLabel.classList.toggle('model-unavailable', !isAvailable);
190
+ }
191
+ }
192
+ if (isAvailable && !firstAvailableAlias) { firstAvailableAlias = mc.alias; }
193
+
194
+ // Update "no key" hint visibility
195
+ var noKeyHint = cardLabel ? cardLabel.querySelector('.no-key-hint') : null;
196
+ if (!isAvailable && !noKeyHint && cardLabel) {
197
+ var hint = document.createElement('span');
198
+ hint.className = 'no-key-hint';
199
+ hint.textContent = 'No API key configured';
200
+ cardLabel.appendChild(hint);
201
+ } else if (isAvailable && noKeyHint) {
202
+ noKeyHint.remove();
203
+ }
204
+
205
+ // Auto-switch routing if selected provider's key was removed
206
+ var currentRoute = routingChoices[mc.alias];
207
+ if (currentRoute && !configuredKeys[currentRoute] && available.length > 0) {
208
+ routingChoices[mc.alias] = available[0];
209
+ }
210
+
211
+ // Update route toggle/static/pills
212
+ if (provs.length < 2) { return; }
213
+ var bestProvider = available.length > 0 ? available[0] : provs[0];
214
+ var toggle = document.querySelector('.route-toggle[data-alias="' + mc.alias + '"]');
215
+ var staticEl = document.querySelector('.route-static[data-alias="' + mc.alias + '"]');
216
+ if (!toggle) { return; }
217
+ if (available.length >= 2) {
218
+ toggle.style.display = 'flex';
219
+ if (staticEl) { staticEl.style.display = 'none'; }
220
+ } else {
221
+ toggle.style.display = 'none';
222
+ if (staticEl) {
223
+ staticEl.style.display = '';
224
+ staticEl.textContent = 'via ' + (providerNamesData[bestProvider] || bestProvider);
225
+ }
226
+ }
227
+ var selected = routingChoices[mc.alias] || bestProvider;
228
+ toggle.querySelectorAll('.route-pill').forEach(function(pill) {
229
+ pill.classList.toggle('active', pill.getAttribute('data-provider') === selected);
230
+ });
231
+ });
232
+
233
+ // Auto-select first available model if current selection is disabled
234
+ var checkedRadio = document.querySelector('input[name="default-model"]:checked');
235
+ if (checkedRadio && checkedRadio.disabled && firstAvailableAlias) {
236
+ var newRadio = document.querySelector('input[name="default-model"][value="' + firstAvailableAlias + '"]');
237
+ if (newRadio) { newRadio.checked = true; }
238
+ }
239
+ if (!checkedRadio && firstAvailableAlias) {
240
+ var fallback = document.querySelector('input[name="default-model"][value="' + firstAvailableAlias + '"]');
241
+ if (fallback) { fallback.checked = true; }
242
+ }
243
+ }
244
+
245
+ function updateAliasRoutes() {
246
+ var hasAnyKey = Object.values(configuredKeys).some(function(v) { return v; });
247
+ if (!hasAnyKey) { return; }
248
+ // Build lookup: alias → best model string, only using providers with keys
249
+ var routedModels = {};
250
+ modelChoicesData.forEach(function(mc) {
251
+ var prov = routingChoices[mc.alias];
252
+ // Only use cached routing choice if that provider's key still exists
253
+ if (prov && mc.routes[prov] && configuredKeys[prov]) {
254
+ routedModels[mc.alias] = mc.routes[prov];
255
+ } else {
256
+ // Pick first provider with a configured key
257
+ var provs = Object.keys(mc.routes);
258
+ for (var i = 0; i < provs.length; i++) {
259
+ if (configuredKeys[provs[i]]) {
260
+ routedModels[mc.alias] = mc.routes[provs[i]];
261
+ routingChoices[mc.alias] = provs[i]; // Update cached choice
262
+ break;
263
+ }
264
+ }
265
+ }
266
+ });
267
+ // Update the example box
268
+ var exampleModel = document.querySelector('.example-model');
269
+ if (exampleModel && routedModels.gemini) {
270
+ exampleModel.textContent = routedModels.gemini;
271
+ }
272
+ document.querySelectorAll('.alias-row').forEach(function(row) {
273
+ var alias = row.getAttribute('data-alias');
274
+ if (!alias || row.classList.contains('alias-deleted')) { return; }
275
+ if (row.querySelector('.alias-model-select')) { return; }
276
+ var modelSpan = row.querySelector('.alias-model');
277
+ if (!modelSpan) { return; }
278
+ // For MODEL_CHOICES aliases: update text to match available routing
279
+ if (routedModels[alias]) {
280
+ modelSpan.textContent = routedModels[alias];
281
+ aliasEdits[alias] = routedModels[alias];
282
+ row.classList.remove('alias-no-key');
283
+ return;
284
+ }
285
+ // Check if the model's provider has a configured key
286
+ var model = aliasEdits[alias] || modelSpan.textContent;
287
+ var prefix = model.split('/')[0];
288
+ var noKey = false;
289
+ if (prefix === 'openrouter') {
290
+ noKey = !configuredKeys.openrouter;
291
+ } else if (configuredKeys.hasOwnProperty(prefix)) {
292
+ noKey = !configuredKeys[prefix];
293
+ }
294
+ row.classList.toggle('alias-no-key', noKey);
295
+ });
296
+ }
297
+
298
+ function buildReview() {
299
+ var kn = Object.keys(configuredKeys).filter(function(k) { return configuredKeys[k]; });
300
+ document.getElementById('review-keys').textContent =
301
+ kn.length > 0 ? kn.map(function(k) { return k + ' \\u2713'; }).join(', ') : 'None';
302
+ var r = document.querySelector('input[name="default-model"]:checked');
303
+ document.getElementById('review-model').textContent = window.customDefaultModel || (r ? r.value : 'Not selected');
304
+ var routeLines = [];
305
+ modelChoicesData.forEach(function(mc) {
306
+ var prov = routingChoices[mc.alias];
307
+ if (!prov) {
308
+ var provs = Object.keys(mc.routes);
309
+ prov = provs.find(function(p) { return configuredKeys[p]; }) || provs[0];
310
+ }
311
+ var provName = providerNamesData[prov] || prov;
312
+ routeLines.push(mc.alias + ' \\u2192 ' + provName);
313
+ });
314
+ document.getElementById('review-routing').textContent = routeLines.join(', ');
315
+ var editCount = Object.keys(aliasEdits).length;
316
+ var reviewAliases = document.getElementById('review-aliases');
317
+ if (reviewAliases) {
318
+ reviewAliases.textContent = editCount > 0 ? editCount + ' alias(es) modified' : 'No changes';
319
+ }
320
+ }
321
+
322
+ nextBtn.addEventListener('click', function() { if (currentStep < 4) { showStep(currentStep + 1); } });
323
+ backBtn.addEventListener('click', function() { if (currentStep > 1) { showStep(currentStep - 1); } });
324
+
325
+ // Route pill click handler
326
+ document.addEventListener('click', function(e) {
327
+ var pill = e.target.closest('.route-pill');
328
+ if (!pill) { return; }
329
+ var alias = pill.getAttribute('data-alias');
330
+ var provider = pill.getAttribute('data-provider');
331
+ if (!alias || !provider) { return; }
332
+ routingChoices[alias] = provider;
333
+ var toggle = pill.parentElement;
334
+ toggle.querySelectorAll('.route-pill').forEach(function(p) { p.classList.toggle('active', p === pill); });
335
+ });
336
+
337
+ finishBtn.addEventListener('click', async function() {
338
+ finishBtn.disabled = true; finishBtn.textContent = 'Saving...';
339
+ try {
340
+ var r = document.querySelector('input[name="default-model"]:checked');
341
+ var dm = window.customDefaultModel || (r ? r.value : 'gemini');
342
+ var routingOverrides = {};
343
+ modelChoicesData.forEach(function(mc) {
344
+ var prov = routingChoices[mc.alias];
345
+ if (!prov) {
346
+ var provs = Object.keys(mc.routes);
347
+ prov = provs.find(function(p) { return configuredKeys[p]; }) || provs[0];
348
+ }
349
+ routingOverrides[mc.alias] = mc.routes[prov];
350
+ });
351
+ Object.keys(aliasEdits).forEach(function(k) {
352
+ routingOverrides[k] = aliasEdits[k];
353
+ });
354
+ await window.sidecarSetup.invoke('sidecar:save-config', dm, routingOverrides);
355
+ var kc = Object.values(configuredKeys).filter(function(v) { return v; }).length;
356
+ await window.sidecarSetup.invoke('sidecar:setup-done', dm, kc);
357
+ } catch (_e) { finishBtn.disabled = false; finishBtn.textContent = 'Finish'; }
358
+ });
359
+
360
+ async function fetchAvailableModels() {
361
+ try {
362
+ var groups = await window.sidecarSetup.invoke('sidecar:fetch-models');
363
+ if (groups && groups.length > 0) { window.availableModels = groups; }
364
+ } catch (_e) {}
365
+ }
366
+
367
+ // ===== F5: searchable catalog picker (Step 2) =====
368
+ var catalogRows = null, catalogFetchedAt = null;
369
+ window.customDefaultModel = null;
370
+
371
+ async function ensureCatalogLoaded() {
372
+ if (catalogRows) { return; }
373
+ try {
374
+ var info = await window.sidecarSetup.invoke('sidecar:get-catalog');
375
+ applyCatalog(info);
376
+ } catch (_e) {}
377
+ }
378
+
379
+ function applyCatalog(info) {
380
+ catalogRows = (info && info.models) || [];
381
+ catalogFetchedAt = info && info.fetchedAt;
382
+ var section = $('model-search-section');
383
+ if (!section) { return; }
384
+ section.style.display = catalogRows.length > 0 ? '' : 'none';
385
+ renderSearchMeta();
386
+ renderSearchResults();
387
+ }
388
+
389
+ function renderSearchMeta() {
390
+ var meta = $('model-search-meta');
391
+ if (!meta) { return; }
392
+ var when = catalogFetchedAt ? new Date(catalogFetchedAt).toLocaleString() : 'never';
393
+ meta.textContent = catalogRows.length + ' models \\u00b7 catalog fetched ' + when;
394
+ }
395
+
396
+ function fmtCtx(n) { return n == null ? '' : ' \\u00b7 ctx ' + n; }
397
+ function fmtPrice(p) {
398
+ if (!p || p.prompt == null) { return ''; }
399
+ var x = Number(p.prompt) * 1e6;
400
+ return isNaN(x) ? '' : ' \\u00b7 $' + x.toFixed(2) + '/M in';
401
+ }
402
+
403
+ function renderSearchResults() {
404
+ var box = $('model-search-results');
405
+ if (!box) { return; }
406
+ var q = ($('model-search-input').value || '').toLowerCase();
407
+ var rows = !q ? [] : catalogRows.filter(function(m) {
408
+ return m.id.toLowerCase().indexOf(q) !== -1 || (m.name || '').toLowerCase().indexOf(q) !== -1;
409
+ }).slice(0, 50);
410
+ box.innerHTML = '';
411
+ rows.forEach(function(m) {
412
+ var div = document.createElement('div');
413
+ div.className = 'search-row' + (window.customDefaultModel === m.id ? ' selected' : '');
414
+ div.setAttribute('data-model-id', m.id);
415
+ var title = document.createElement('div');
416
+ title.className = 'search-row-id';
417
+ title.textContent = m.id;
418
+ var sub = document.createElement('div');
419
+ sub.className = 'search-row-sub';
420
+ sub.textContent = (m.name || '') + fmtCtx(m.contextLength) + fmtPrice(m.pricing);
421
+ div.appendChild(title); div.appendChild(sub);
422
+ div.addEventListener('click', function() { selectCustomModel(m.id); });
423
+ box.appendChild(div);
424
+ });
425
+ if (q && rows.length === 0) {
426
+ box.textContent = 'No models match "' + q + '"';
427
+ }
428
+ }
429
+
430
+ function selectCustomModel(id) {
431
+ window.customDefaultModel = id;
432
+ document.querySelectorAll('input[name="default-model"]').forEach(function(r) { r.checked = false; });
433
+ var box = $('model-search-results');
434
+ var keep = box ? box.scrollTop : 0;
435
+ renderSearchResults();
436
+ if (box) { box.scrollTop = keep; }
437
+ }
438
+
439
+ document.addEventListener('input', function(e) {
440
+ if (e.target && e.target.id === 'model-search-input') { renderSearchResults(); }
441
+ });
442
+ document.addEventListener('change', function(e) {
443
+ if (e.target && e.target.name === 'default-model' && e.target.checked) {
444
+ window.customDefaultModel = null;
445
+ renderSearchResults();
446
+ }
447
+ });
448
+ document.addEventListener('click', async function(e) {
449
+ if (e.target && e.target.id === 'model-search-refresh') {
450
+ e.target.disabled = true;
451
+ try {
452
+ var info = await window.sidecarSetup.invoke('sidecar:refresh-catalog');
453
+ applyCatalog(info);
454
+ } catch (_e2) {}
455
+ e.target.disabled = false;
456
+ }
457
+ });
458
+
459
+ ${aliasJs}
460
+
461
+ ${keysJs}
462
+ </script>`;
463
+ }
464
+
465
+ module.exports = { buildSetupHTML, PROVIDERS };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Summary Generation via OpenCode API
3
+ *
4
+ * Makes HTTP requests to the OpenCode server to request and poll
5
+ * for model-generated fold summaries.
6
+ */
7
+
8
+ const http = require('http');
9
+ const { logger } = require('../src/utils/logger');
10
+
11
+ /**
12
+ * Make an HTTP request to the OpenCode server.
13
+ * @param {string} method - HTTP method
14
+ * @param {string} urlPath - URL path (e.g., '/session/abc/prompt_async')
15
+ * @param {number} port - OpenCode server port
16
+ * @param {object} [body] - JSON body to send
17
+ * @returns {Promise<object>} Parsed JSON response
18
+ */
19
+ function apiRequest(method, urlPath, port, body) {
20
+ return new Promise((resolve, reject) => {
21
+ const options = {
22
+ hostname: '127.0.0.1',
23
+ port,
24
+ path: urlPath,
25
+ method,
26
+ headers: { 'Content-Type': 'application/json' }
27
+ };
28
+
29
+ const req = http.request(options, (res) => {
30
+ let data = '';
31
+ res.on('data', (chunk) => { data += chunk; });
32
+ res.on('end', () => {
33
+ try {
34
+ resolve(JSON.parse(data));
35
+ } catch (_e) {
36
+ resolve({ raw: data });
37
+ }
38
+ });
39
+ });
40
+
41
+ req.on('error', reject);
42
+ if (body) { req.write(JSON.stringify(body)); }
43
+ req.end();
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Request the model to generate a handoff summary via the OpenCode API.
49
+ * Sends the SUMMARY_TEMPLATE as a new message, polls for the response.
50
+ * @param {string} sessionId - OpenCode session ID
51
+ * @param {number} port - OpenCode server port
52
+ * @param {Function} getSummaryTemplate - Function returning the summary prompt
53
+ * @returns {Promise<string>} Generated summary text
54
+ */
55
+ async function requestSummaryFromModel(sessionId, port, getSummaryTemplate) {
56
+ if (!sessionId) { return ''; }
57
+
58
+ const summaryPrompt = getSummaryTemplate();
59
+ logger.info('Requesting fold summary from model', { sessionId });
60
+
61
+ // Send summary prompt asynchronously
62
+ await apiRequest('POST', `/session/${sessionId}/prompt_async`, port, {
63
+ parts: [{ type: 'text', text: summaryPrompt }]
64
+ });
65
+
66
+ // Poll for the model's response (up to 60 seconds)
67
+ const startTime = Date.now();
68
+ const timeoutMs = 60000;
69
+ let lastMessageCount = 0;
70
+ let stablePolls = 0;
71
+ let summaryText = '';
72
+
73
+ while ((Date.now() - startTime) < timeoutMs) {
74
+ await new Promise(resolve => setTimeout(resolve, 2000));
75
+
76
+ try {
77
+ const messages = await apiRequest('GET', `/session/${sessionId}/message`, port);
78
+ const msgArray = Array.isArray(messages) ? messages : [];
79
+
80
+ // Look for the latest assistant text content
81
+ let latestAssistantText = '';
82
+ let assistantFinished = false;
83
+
84
+ for (const msg of msgArray) {
85
+ if (msg.info?.role === 'assistant') {
86
+ if (msg.info.time?.completed) { assistantFinished = true; }
87
+ if (msg.parts) {
88
+ for (const part of msg.parts) {
89
+ if (part.type === 'text' && part.text) {
90
+ latestAssistantText = part.text;
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ // Check if stable (same message count for 2 polls and assistant finished)
98
+ if (assistantFinished && msgArray.length === lastMessageCount) {
99
+ stablePolls++;
100
+ if (stablePolls >= 2) {
101
+ summaryText = latestAssistantText;
102
+ break;
103
+ }
104
+ } else {
105
+ stablePolls = 0;
106
+ }
107
+ lastMessageCount = msgArray.length;
108
+
109
+ } catch (err) {
110
+ logger.debug('Summary poll error', { error: err.message });
111
+ }
112
+ }
113
+
114
+ logger.info('Summary captured', { length: summaryText.length });
115
+ return summaryText;
116
+ }
117
+
118
+ module.exports = { apiRequest, requestSummaryFromModel };