opencode-auto-agent 1.3.0 → 1.3.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/package.json +1 -1
- package/src/commands/init.js +51 -18
- package/src/commands/setup.js +62 -27
- package/src/lib/ui.js +66 -11
- package/src/lib/ui.test.js +16 -1
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -112,26 +112,22 @@ export async function init(targetDir, { preset, force, nonInteractive } = {}) {
|
|
|
112
112
|
continue;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
for (const m of models) {
|
|
125
|
-
if (m.id !== defaultModel?.id) {
|
|
126
|
-
items.push({ label: m.id, value: m.id });
|
|
127
|
-
}
|
|
128
|
-
}
|
|
115
|
+
const recommendedProvider = defaultModel?.provider || providerFromId(defaultId);
|
|
116
|
+
const providerItems = buildProviderItems(models, recommendedProvider);
|
|
117
|
+
|
|
118
|
+
const providerChoice = await select(
|
|
119
|
+
`Provider for ${c.bold(role)} ${c.gray(`\u2014 ${roleDef.description}`)}`,
|
|
120
|
+
providerItems
|
|
121
|
+
);
|
|
129
122
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
123
|
+
const providerModels = models.filter((m) => m.provider === providerChoice.value);
|
|
124
|
+
const modelItems = buildModelItemsForProvider(providerModels, defaultModel?.id);
|
|
125
|
+
const modelChoice = await select(
|
|
126
|
+
`Model for ${c.bold(role)} ${c.gray(`(${providerChoice.value})`)}`,
|
|
127
|
+
modelItems
|
|
133
128
|
);
|
|
134
|
-
|
|
129
|
+
|
|
130
|
+
modelMapping[role] = modelChoice.value;
|
|
135
131
|
}
|
|
136
132
|
|
|
137
133
|
// Show summary table
|
|
@@ -399,6 +395,43 @@ notifications:
|
|
|
399
395
|
|
|
400
396
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
401
397
|
|
|
398
|
+
function providerFromId(modelId) {
|
|
399
|
+
return modelId.includes("/") ? modelId.split("/")[0] : modelId;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function buildProviderItems(models, preferredProvider) {
|
|
403
|
+
const counts = new Map();
|
|
404
|
+
for (const m of models) {
|
|
405
|
+
counts.set(m.provider, (counts.get(m.provider) || 0) + 1);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const providers = [...counts.keys()].sort((a, b) => {
|
|
409
|
+
if (a === preferredProvider) return -1;
|
|
410
|
+
if (b === preferredProvider) return 1;
|
|
411
|
+
return a.localeCompare(b);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
return providers.map((provider) => ({
|
|
415
|
+
label: provider,
|
|
416
|
+
value: provider,
|
|
417
|
+
hint: `${counts.get(provider)} models${provider === preferredProvider ? " (recommended)" : ""}`,
|
|
418
|
+
}));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function buildModelItemsForProvider(providerModels, preferredModelId) {
|
|
422
|
+
const sorted = [...providerModels].sort((a, b) => {
|
|
423
|
+
if (a.id === preferredModelId) return -1;
|
|
424
|
+
if (b.id === preferredModelId) return 1;
|
|
425
|
+
return a.model.localeCompare(b.model);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
return sorted.map((m) => ({
|
|
429
|
+
label: m.model,
|
|
430
|
+
value: m.id,
|
|
431
|
+
hint: m.id === preferredModelId ? "(recommended)" : "",
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
|
|
402
435
|
function presetDescription(name) {
|
|
403
436
|
const desc = {
|
|
404
437
|
java: "General Java project (Maven/Gradle)",
|
package/src/commands/setup.js
CHANGED
|
@@ -131,35 +131,25 @@ export async function setup(targetDir, preset, { models: reconfigModels, nonInte
|
|
|
131
131
|
for (const role of AGENTS) {
|
|
132
132
|
const currentModel = modelMapping[role] || DEFAULT_MODEL_MAP[role];
|
|
133
133
|
const currentFound = findModel(models, currentModel);
|
|
134
|
+
const defaultFound = findModel(models, DEFAULT_MODEL_MAP[role]);
|
|
135
|
+
const preferredProvider =
|
|
136
|
+
currentFound?.provider ||
|
|
137
|
+
defaultFound?.provider ||
|
|
138
|
+
providerFromId(currentModel);
|
|
139
|
+
|
|
140
|
+
const providerChoice = await select(
|
|
141
|
+
`Provider for ${c.bold(role)} ${c.gray(`\u2014 ${AGENT_ROLES[role].description}`)}`,
|
|
142
|
+
buildProviderItems(models, preferredProvider)
|
|
143
|
+
);
|
|
134
144
|
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (currentFound) {
|
|
140
|
-
items.push({ label: currentFound.id, value: currentFound.id, hint: "(current)" });
|
|
141
|
-
seen.add(currentFound.id);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const defaultId = DEFAULT_MODEL_MAP[role];
|
|
145
|
-
const defaultFound = findModel(models, defaultId);
|
|
146
|
-
if (defaultFound && !seen.has(defaultFound.id)) {
|
|
147
|
-
items.push({ label: defaultFound.id, value: defaultFound.id, hint: "(recommended)" });
|
|
148
|
-
seen.add(defaultFound.id);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
for (const m of models) {
|
|
152
|
-
if (!seen.has(m.id)) {
|
|
153
|
-
items.push({ label: m.id, value: m.id });
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const result = await select(
|
|
158
|
-
`Model for ${c.bold(role)} ${c.gray(`\u2014 ${AGENT_ROLES[role].description}`)}`,
|
|
159
|
-
items
|
|
145
|
+
const providerModels = models.filter((m) => m.provider === providerChoice.value);
|
|
146
|
+
const modelChoice = await select(
|
|
147
|
+
`Model for ${c.bold(role)} ${c.gray(`(${providerChoice.value})`)}`,
|
|
148
|
+
buildModelItemsForProvider(providerModels, currentModel, defaultFound?.id)
|
|
160
149
|
);
|
|
161
|
-
|
|
162
|
-
|
|
150
|
+
|
|
151
|
+
if (modelChoice.value !== currentModel) modelsChanged = true;
|
|
152
|
+
modelMapping[role] = modelChoice.value;
|
|
163
153
|
}
|
|
164
154
|
} else {
|
|
165
155
|
warn("No models found. Keeping current model assignments.", "Configure opencode providers first.");
|
|
@@ -311,6 +301,51 @@ function buildAgentPrompt(role, preset) {
|
|
|
311
301
|
return prompts[role] || `You are the ${role} agent.`;
|
|
312
302
|
}
|
|
313
303
|
|
|
304
|
+
function providerFromId(modelId) {
|
|
305
|
+
return modelId.includes("/") ? modelId.split("/")[0] : modelId;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function buildProviderItems(models, preferredProvider) {
|
|
309
|
+
const counts = new Map();
|
|
310
|
+
for (const m of models) {
|
|
311
|
+
counts.set(m.provider, (counts.get(m.provider) || 0) + 1);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const providers = [...counts.keys()].sort((a, b) => {
|
|
315
|
+
if (a === preferredProvider) return -1;
|
|
316
|
+
if (b === preferredProvider) return 1;
|
|
317
|
+
return a.localeCompare(b);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return providers.map((provider) => ({
|
|
321
|
+
label: provider,
|
|
322
|
+
value: provider,
|
|
323
|
+
hint: `${counts.get(provider)} models${provider === preferredProvider ? " (recommended)" : ""}`,
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function buildModelItemsForProvider(providerModels, currentModelId, recommendedModelId) {
|
|
328
|
+
const sorted = [...providerModels].sort((a, b) => {
|
|
329
|
+
if (a.id === currentModelId) return -1;
|
|
330
|
+
if (b.id === currentModelId) return 1;
|
|
331
|
+
if (a.id === recommendedModelId) return -1;
|
|
332
|
+
if (b.id === recommendedModelId) return 1;
|
|
333
|
+
return a.model.localeCompare(b.model);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return sorted.map((m) => {
|
|
337
|
+
let hint = "";
|
|
338
|
+
if (m.id === currentModelId) hint = "(current)";
|
|
339
|
+
else if (m.id === recommendedModelId) hint = "(recommended)";
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
label: m.model,
|
|
343
|
+
value: m.id,
|
|
344
|
+
hint,
|
|
345
|
+
};
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
314
349
|
/**
|
|
315
350
|
* Patch or add the `model:` field in markdown YAML frontmatter.
|
|
316
351
|
*/
|
package/src/lib/ui.js
CHANGED
|
@@ -246,23 +246,33 @@ export async function select(title, items) {
|
|
|
246
246
|
|
|
247
247
|
return new Promise((resolve) => {
|
|
248
248
|
let cursor = 0;
|
|
249
|
-
const
|
|
249
|
+
const maxVisible = getMaxVisibleMenuItems();
|
|
250
|
+
const totalLines = Math.min(items.length, maxVisible) + 2;
|
|
250
251
|
let hasRendered = false;
|
|
251
252
|
|
|
252
253
|
const render = () => {
|
|
254
|
+
const { start, end } = getVisibleWindow(items.length, cursor, maxVisible);
|
|
255
|
+
const visible = items.slice(start, end);
|
|
256
|
+
|
|
253
257
|
if (hasRendered) {
|
|
254
258
|
stdout.write(`\x1b[${totalLines}A`);
|
|
255
259
|
}
|
|
256
260
|
|
|
257
261
|
stdout.write(`\x1b[2K ${c.cyan("?")} ${c.bold(title)}\n`);
|
|
258
|
-
for (let i = 0; i <
|
|
259
|
-
const
|
|
260
|
-
const
|
|
262
|
+
for (let i = 0; i < visible.length; i++) {
|
|
263
|
+
const itemIndex = start + i;
|
|
264
|
+
const item = visible[i];
|
|
265
|
+
const isSelected = itemIndex === cursor;
|
|
261
266
|
const pointer = isSelected ? c.cyan("\u25B6") : " ";
|
|
262
267
|
const label = isSelected ? c.cyan(item.label) : c.white(item.label);
|
|
263
268
|
const hint = item.hint ? c.gray(` ${item.hint}`) : "";
|
|
264
269
|
stdout.write(`\x1b[2K ${pointer} ${label}${hint}\n`);
|
|
265
270
|
}
|
|
271
|
+
|
|
272
|
+
const range = `${start + 1}-${end}`;
|
|
273
|
+
const moreAbove = start > 0 ? c.gray("↑") : c.gray(" ");
|
|
274
|
+
const moreBelow = end < items.length ? c.gray("↓") : c.gray(" ");
|
|
275
|
+
stdout.write(`\x1b[2K ${c.gray(`[${range}/${items.length}] ${moreAbove}${moreBelow} arrows/jk + enter`)}` + "\n");
|
|
266
276
|
hasRendered = true;
|
|
267
277
|
};
|
|
268
278
|
|
|
@@ -328,26 +338,35 @@ export async function multiSelect(title, items) {
|
|
|
328
338
|
return new Promise((resolve) => {
|
|
329
339
|
let cursor = 0;
|
|
330
340
|
const selected = buildSelectedIndexSet(items);
|
|
331
|
-
const
|
|
341
|
+
const maxVisible = getMaxVisibleMenuItems();
|
|
342
|
+
const totalLines = Math.min(items.length, maxVisible) + 2;
|
|
332
343
|
let hasRendered = false;
|
|
333
344
|
|
|
334
345
|
const render = () => {
|
|
346
|
+
const { start, end } = getVisibleWindow(items.length, cursor, maxVisible);
|
|
347
|
+
const visible = items.slice(start, end);
|
|
348
|
+
|
|
335
349
|
if (hasRendered) {
|
|
336
350
|
stdout.write(`\x1b[${totalLines}A`);
|
|
337
351
|
}
|
|
338
352
|
|
|
339
|
-
stdout.write(`\x1b[2K ${c.cyan("?")} ${c.bold(title)} ${c.gray("(space to toggle
|
|
340
|
-
for (let i = 0; i <
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
const
|
|
353
|
+
stdout.write(`\x1b[2K ${c.cyan("?")} ${c.bold(title)} ${c.gray("(space to toggle)")}\n`);
|
|
354
|
+
for (let i = 0; i < visible.length; i++) {
|
|
355
|
+
const itemIndex = start + i;
|
|
356
|
+
const item = visible[i];
|
|
357
|
+
const isCursor = itemIndex === cursor;
|
|
358
|
+
const isSelected = selected.has(itemIndex);
|
|
344
359
|
const pointer = isCursor ? c.cyan("\u25B6") : " ";
|
|
345
360
|
const check = isSelected ? c.green("\u25C9") : c.gray("\u25CB");
|
|
346
361
|
const label = isCursor ? c.cyan(item.label) : c.white(item.label);
|
|
347
362
|
const hint = item.hint ? c.gray(` ${item.hint}`) : "";
|
|
348
363
|
stdout.write(`\x1b[2K ${pointer} ${check} ${label}${hint}\n`);
|
|
349
364
|
}
|
|
350
|
-
|
|
365
|
+
|
|
366
|
+
const range = `${start + 1}-${end}`;
|
|
367
|
+
const moreAbove = start > 0 ? c.gray("↑") : c.gray(" ");
|
|
368
|
+
const moreBelow = end < items.length ? c.gray("↓") : c.gray(" ");
|
|
369
|
+
stdout.write(`\x1b[2K ${c.gray(`[${range}/${items.length}] ${moreAbove}${moreBelow} arrows/jk + space + enter`)}` + "\n");
|
|
351
370
|
hasRendered = true;
|
|
352
371
|
};
|
|
353
372
|
|
|
@@ -450,6 +469,42 @@ export function buildSelectedIndexSet(items) {
|
|
|
450
469
|
return indexes;
|
|
451
470
|
}
|
|
452
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Calculate the visible start/end indexes for long menu lists.
|
|
474
|
+
* @param {number} total
|
|
475
|
+
* @param {number} cursor
|
|
476
|
+
* @param {number} maxVisible
|
|
477
|
+
* @returns {{start:number,end:number}}
|
|
478
|
+
*/
|
|
479
|
+
export function getVisibleWindow(total, cursor, maxVisible) {
|
|
480
|
+
if (total <= 0) return { start: 0, end: 0 };
|
|
481
|
+
if (maxVisible >= total) return { start: 0, end: total };
|
|
482
|
+
|
|
483
|
+
const half = Math.floor(maxVisible / 2);
|
|
484
|
+
let start = cursor - half;
|
|
485
|
+
let end = start + maxVisible;
|
|
486
|
+
|
|
487
|
+
if (start < 0) {
|
|
488
|
+
start = 0;
|
|
489
|
+
end = maxVisible;
|
|
490
|
+
}
|
|
491
|
+
if (end > total) {
|
|
492
|
+
end = total;
|
|
493
|
+
start = total - maxVisible;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return { start, end };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Compute the max menu rows based on terminal height.
|
|
501
|
+
* Leaves room for prompt output and avoids terminal overflow.
|
|
502
|
+
*/
|
|
503
|
+
export function getMaxVisibleMenuItems(termRows = stdout.rows || 24) {
|
|
504
|
+
const safe = Math.max(6, termRows - 8);
|
|
505
|
+
return Math.min(20, safe);
|
|
506
|
+
}
|
|
507
|
+
|
|
453
508
|
function supportsInteractiveMenu() {
|
|
454
509
|
return Boolean(stdin.isTTY && stdout.isTTY && typeof stdin.setRawMode === "function");
|
|
455
510
|
}
|
package/src/lib/ui.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
|
|
4
|
-
import { buildSelectedIndexSet } from "./ui.js";
|
|
4
|
+
import { buildSelectedIndexSet, getVisibleWindow, getMaxVisibleMenuItems } from "./ui.js";
|
|
5
5
|
|
|
6
6
|
test("buildSelectedIndexSet keeps original indexes", () => {
|
|
7
7
|
const items = [
|
|
@@ -24,3 +24,18 @@ test("buildSelectedIndexSet handles empty selection", () => {
|
|
|
24
24
|
const selected = buildSelectedIndexSet(items);
|
|
25
25
|
assert.equal(selected.size, 0);
|
|
26
26
|
});
|
|
27
|
+
|
|
28
|
+
test("getVisibleWindow keeps cursor centered when possible", () => {
|
|
29
|
+
const w = getVisibleWindow(100, 50, 10);
|
|
30
|
+
assert.deepEqual(w, { start: 45, end: 55 });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("getVisibleWindow clamps to beginning and end", () => {
|
|
34
|
+
assert.deepEqual(getVisibleWindow(100, 2, 10), { start: 0, end: 10 });
|
|
35
|
+
assert.deepEqual(getVisibleWindow(100, 98, 10), { start: 90, end: 100 });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("getMaxVisibleMenuItems enforces safe min/max", () => {
|
|
39
|
+
assert.equal(getMaxVisibleMenuItems(12), 6);
|
|
40
|
+
assert.equal(getMaxVisibleMenuItems(40), 20);
|
|
41
|
+
});
|