ultrahope 0.1.6 → 0.1.7
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/dist/git-ultrahope.js +378 -122
- package/dist/index.js +382 -123
- package/package.json +1 -1
package/dist/git-ultrahope.js
CHANGED
|
@@ -90,6 +90,17 @@ var InvalidModelError = class extends Error {
|
|
|
90
90
|
this.name = "InvalidModelError";
|
|
91
91
|
}
|
|
92
92
|
};
|
|
93
|
+
var InputLengthExceededError = class extends Error {
|
|
94
|
+
constructor(count, limit, plan = "free", message) {
|
|
95
|
+
super(
|
|
96
|
+
message ?? `Input length ${count} exceeds the ${plan} plan limit of ${limit} characters.`
|
|
97
|
+
);
|
|
98
|
+
this.count = count;
|
|
99
|
+
this.limit = limit;
|
|
100
|
+
this.plan = plan;
|
|
101
|
+
this.name = "InputLengthExceededError";
|
|
102
|
+
}
|
|
103
|
+
};
|
|
93
104
|
async function getErrorText(response, error) {
|
|
94
105
|
if (error) {
|
|
95
106
|
try {
|
|
@@ -149,6 +160,15 @@ function handle402Error(error) {
|
|
|
149
160
|
log("generate error (402 daily_limit)", error);
|
|
150
161
|
throw new DailyLimitExceededError(count, limit, resetsAt);
|
|
151
162
|
}
|
|
163
|
+
function throwInputLengthExceededError(error) {
|
|
164
|
+
const payload = error;
|
|
165
|
+
const count = typeof payload?.count === "number" ? payload.count : 0;
|
|
166
|
+
const limit = typeof payload?.limit === "number" ? payload.limit : 0;
|
|
167
|
+
const plan = payload?.plan === "free" ? payload.plan : "free";
|
|
168
|
+
const message = typeof payload?.message === "string" ? payload.message : `Input length ${count} exceeds the ${plan} plan limit of ${limit} characters.`;
|
|
169
|
+
log("generate error (400 input_too_long)", error);
|
|
170
|
+
throw new InputLengthExceededError(count, limit, plan, message);
|
|
171
|
+
}
|
|
152
172
|
function throwInvalidModelError(error) {
|
|
153
173
|
const payload = error;
|
|
154
174
|
const message = payload?.message ?? "Model is not supported.";
|
|
@@ -160,6 +180,13 @@ function throwInvalidModelError(error) {
|
|
|
160
180
|
log("generate error (400 invalid_model)", error);
|
|
161
181
|
throw new InvalidModelError(model, allowedModels, message);
|
|
162
182
|
}
|
|
183
|
+
function handle400Error(error) {
|
|
184
|
+
const payload = error;
|
|
185
|
+
if (payload?.error === "input_too_long") {
|
|
186
|
+
throwInputLengthExceededError(error);
|
|
187
|
+
}
|
|
188
|
+
throwInvalidModelError(error);
|
|
189
|
+
}
|
|
163
190
|
function createApiClient(token) {
|
|
164
191
|
const headers = {
|
|
165
192
|
"Content-Type": "application/json"
|
|
@@ -203,7 +230,7 @@ function createApiClient(token) {
|
|
|
203
230
|
} catch {
|
|
204
231
|
errorPayload = await getErrorText(res, null);
|
|
205
232
|
}
|
|
206
|
-
|
|
233
|
+
handle400Error(errorPayload);
|
|
207
234
|
}
|
|
208
235
|
if (!res.ok) {
|
|
209
236
|
const text = await getErrorText(res, null);
|
|
@@ -268,6 +295,9 @@ function createApiClient(token) {
|
|
|
268
295
|
log("command_execution error (402)", error);
|
|
269
296
|
handle402Error(error);
|
|
270
297
|
}
|
|
298
|
+
if (response.status === 400) {
|
|
299
|
+
handle400Error(error);
|
|
300
|
+
}
|
|
271
301
|
if (!response.ok) {
|
|
272
302
|
const text = await getErrorText(response, error);
|
|
273
303
|
log("command_execution error", { status: response.status, text });
|
|
@@ -296,7 +326,7 @@ function createApiClient(token) {
|
|
|
296
326
|
handle402Error(error);
|
|
297
327
|
}
|
|
298
328
|
if (response.status === 400) {
|
|
299
|
-
|
|
329
|
+
handle400Error(error);
|
|
300
330
|
}
|
|
301
331
|
if (!response.ok) {
|
|
302
332
|
const text = await getErrorText(response, error);
|
|
@@ -350,7 +380,7 @@ function createApiClient(token) {
|
|
|
350
380
|
handle402Error(error);
|
|
351
381
|
}
|
|
352
382
|
if (response.status === 400) {
|
|
353
|
-
|
|
383
|
+
handle400Error(error);
|
|
354
384
|
}
|
|
355
385
|
if (!response.ok) {
|
|
356
386
|
const text = await getErrorText(response, error);
|
|
@@ -377,7 +407,7 @@ function createApiClient(token) {
|
|
|
377
407
|
handle402Error(error);
|
|
378
408
|
}
|
|
379
409
|
if (response.status === 400) {
|
|
380
|
-
|
|
410
|
+
handle400Error(error);
|
|
381
411
|
}
|
|
382
412
|
if (!response.ok) {
|
|
383
413
|
const text = await getErrorText(response, error);
|
|
@@ -540,9 +570,6 @@ var theme = {
|
|
|
540
570
|
};
|
|
541
571
|
|
|
542
572
|
// lib/ui.ts
|
|
543
|
-
function formatTotalCost(cost) {
|
|
544
|
-
return `$${cost.toFixed(6)}`;
|
|
545
|
-
}
|
|
546
573
|
var ui = {
|
|
547
574
|
success: (msg) => `${theme.success}\u2714${theme.reset} ${theme.primary}${msg}${theme.reset}`,
|
|
548
575
|
progress: (msg) => `${theme.progress}\u25B6${theme.reset} ${theme.primary}${msg}${theme.reset}`,
|
|
@@ -756,6 +783,16 @@ async function handleCommandExecutionError(error, options) {
|
|
|
756
783
|
console.error(error.formatMessage());
|
|
757
784
|
process.exit(1);
|
|
758
785
|
}
|
|
786
|
+
if (error instanceof InputLengthExceededError) {
|
|
787
|
+
console.error("\x1B[31m\u2716\x1B[0m Input is too long for the Free plan.");
|
|
788
|
+
console.error(
|
|
789
|
+
` Max allowed characters: ${error.limit}. Received: ${error.count}.`
|
|
790
|
+
);
|
|
791
|
+
console.error(
|
|
792
|
+
" Please shorten your input or upgrade to Pro for unlimited input length."
|
|
793
|
+
);
|
|
794
|
+
process.exit(1);
|
|
795
|
+
}
|
|
759
796
|
const message = error instanceof Error ? error.message : String(error);
|
|
760
797
|
console.error(`Error: Failed to start command execution. ${message}`);
|
|
761
798
|
process.exit(1);
|
|
@@ -1152,6 +1189,63 @@ function formatDiffStats(stats) {
|
|
|
1152
1189
|
return parts.join(", ");
|
|
1153
1190
|
}
|
|
1154
1191
|
|
|
1192
|
+
// lib/renderer.ts
|
|
1193
|
+
import * as readline2 from "readline";
|
|
1194
|
+
var SPINNER_FRAMES = [
|
|
1195
|
+
"\u280B",
|
|
1196
|
+
"\u2819",
|
|
1197
|
+
"\u2839",
|
|
1198
|
+
"\u2838",
|
|
1199
|
+
"\u283C",
|
|
1200
|
+
"\u2834",
|
|
1201
|
+
"\u2826",
|
|
1202
|
+
"\u2827",
|
|
1203
|
+
"\u2807",
|
|
1204
|
+
"\u280F"
|
|
1205
|
+
];
|
|
1206
|
+
function isTTY(output) {
|
|
1207
|
+
return output.isTTY === true;
|
|
1208
|
+
}
|
|
1209
|
+
function createRenderer(output) {
|
|
1210
|
+
let pendingHeight = 0;
|
|
1211
|
+
let committedHeight = 0;
|
|
1212
|
+
const render = (content) => {
|
|
1213
|
+
if (!isTTY(output)) {
|
|
1214
|
+
output.write(content);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (pendingHeight > 0) {
|
|
1218
|
+
readline2.moveCursor(output, 0, -pendingHeight);
|
|
1219
|
+
readline2.cursorTo(output, 0);
|
|
1220
|
+
readline2.clearScreenDown(output);
|
|
1221
|
+
}
|
|
1222
|
+
output.write(content);
|
|
1223
|
+
pendingHeight = content.split("\n").length - 1;
|
|
1224
|
+
};
|
|
1225
|
+
const flush = () => {
|
|
1226
|
+
committedHeight += pendingHeight;
|
|
1227
|
+
pendingHeight = 0;
|
|
1228
|
+
};
|
|
1229
|
+
const clearAll = () => {
|
|
1230
|
+
if (!isTTY(output)) {
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
const totalHeight = pendingHeight + committedHeight;
|
|
1234
|
+
if (totalHeight > 0) {
|
|
1235
|
+
readline2.moveCursor(output, 0, -totalHeight);
|
|
1236
|
+
readline2.cursorTo(output, 0);
|
|
1237
|
+
readline2.clearScreenDown(output);
|
|
1238
|
+
}
|
|
1239
|
+
pendingHeight = 0;
|
|
1240
|
+
committedHeight = 0;
|
|
1241
|
+
};
|
|
1242
|
+
const reset = () => {
|
|
1243
|
+
pendingHeight = 0;
|
|
1244
|
+
committedHeight = 0;
|
|
1245
|
+
};
|
|
1246
|
+
return { render, flush, clearAll, reset };
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1155
1249
|
// lib/selector.ts
|
|
1156
1250
|
import { spawn } from "child_process";
|
|
1157
1251
|
import {
|
|
@@ -1214,9 +1308,8 @@ function getSelectedCandidate(slots, selectedIndex) {
|
|
|
1214
1308
|
return slot?.status === "ready" ? slot.candidate : void 0;
|
|
1215
1309
|
}
|
|
1216
1310
|
|
|
1217
|
-
//
|
|
1218
|
-
|
|
1219
|
-
var SPINNER_FRAMES = [
|
|
1311
|
+
// ../shared/terminal-selector-view-model.ts
|
|
1312
|
+
var DEFAULT_SPINNER_FRAMES = [
|
|
1220
1313
|
"\u280B",
|
|
1221
1314
|
"\u2819",
|
|
1222
1315
|
"\u2839",
|
|
@@ -1228,51 +1321,199 @@ var SPINNER_FRAMES = [
|
|
|
1228
1321
|
"\u2807",
|
|
1229
1322
|
"\u280F"
|
|
1230
1323
|
];
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1324
|
+
var HINT_ACTION_ORDER = [
|
|
1325
|
+
"navigate",
|
|
1326
|
+
"confirm",
|
|
1327
|
+
"clickConfirm",
|
|
1328
|
+
"edit",
|
|
1329
|
+
"reroll",
|
|
1330
|
+
"refine",
|
|
1331
|
+
"quit"
|
|
1332
|
+
];
|
|
1333
|
+
var HINT_ACTION_GROUPS = [
|
|
1334
|
+
["navigate", "confirm", "clickConfirm"],
|
|
1335
|
+
["edit", "reroll", "refine"],
|
|
1336
|
+
["quit"]
|
|
1337
|
+
];
|
|
1338
|
+
var DEFAULT_HINT_LABELS = {
|
|
1339
|
+
cli: {
|
|
1340
|
+
navigate: "\u2191\u2193 navigate",
|
|
1341
|
+
confirm: "\u23CE confirm",
|
|
1342
|
+
clickConfirm: "click confirm",
|
|
1343
|
+
edit: "(e)dit",
|
|
1344
|
+
reroll: "(r)eroll",
|
|
1345
|
+
refine: "(R)efine",
|
|
1346
|
+
quit: "(q)uit"
|
|
1347
|
+
},
|
|
1348
|
+
web: {
|
|
1349
|
+
navigate: "\u2191\u2193 navigate",
|
|
1350
|
+
confirm: "enter confirm",
|
|
1351
|
+
clickConfirm: "click confirm",
|
|
1352
|
+
edit: "(e)dit",
|
|
1353
|
+
reroll: "(r)eroll",
|
|
1354
|
+
refine: "(R)efine",
|
|
1355
|
+
quit: "(q)uit"
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
var SELECTOR_HINT_ACTION_LABELS = DEFAULT_HINT_LABELS;
|
|
1359
|
+
var DEFAULT_SELECTOR_COPY = {
|
|
1360
|
+
runningLabel: "Generating commit messages...",
|
|
1361
|
+
selectionLabel: "Select a commit message",
|
|
1362
|
+
itemLabelSingular: "commit message",
|
|
1363
|
+
itemLabelPlural: "commit messages"
|
|
1364
|
+
};
|
|
1365
|
+
var DEFAULT_SELECTOR_CAPABILITIES = {
|
|
1366
|
+
clickConfirm: false,
|
|
1367
|
+
edit: false,
|
|
1368
|
+
refine: false
|
|
1369
|
+
};
|
|
1370
|
+
function formatDuration(ms) {
|
|
1371
|
+
const safeMs = Math.max(0, Math.round(ms));
|
|
1372
|
+
if (safeMs < 1e3) {
|
|
1373
|
+
return `${safeMs}ms`;
|
|
1374
|
+
}
|
|
1375
|
+
const seconds = (safeMs / 1e3).toFixed(1).replace(/\.0$/, "");
|
|
1376
|
+
return `${seconds}s`;
|
|
1377
|
+
}
|
|
1378
|
+
function formatSelectorHintActions(actions, target, options = {}) {
|
|
1379
|
+
const labels = SELECTOR_HINT_ACTION_LABELS[target];
|
|
1380
|
+
const ordered = normalizeHintActions(actions);
|
|
1381
|
+
if (options.separator) {
|
|
1382
|
+
return ordered.map((action) => labels[action]).join(options.separator);
|
|
1383
|
+
}
|
|
1384
|
+
return HINT_ACTION_GROUPS.map(
|
|
1385
|
+
(group) => group.filter((action) => ordered.includes(action)).map((action) => labels[action]).join(" ")
|
|
1386
|
+
).filter((groupText) => groupText !== "").join(" | ");
|
|
1387
|
+
}
|
|
1388
|
+
function normalizeHintActions(actions) {
|
|
1389
|
+
const set = new Set(actions);
|
|
1390
|
+
const ordered = [];
|
|
1391
|
+
for (const action of HINT_ACTION_ORDER) {
|
|
1392
|
+
if (set.has(action)) {
|
|
1393
|
+
ordered.push(action);
|
|
1246
1394
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1395
|
+
}
|
|
1396
|
+
return ordered;
|
|
1397
|
+
}
|
|
1398
|
+
function resolveHintActions(input) {
|
|
1399
|
+
if (input.readyCount <= 0) {
|
|
1400
|
+
return ["quit"];
|
|
1401
|
+
}
|
|
1402
|
+
const actions = [
|
|
1403
|
+
"navigate",
|
|
1404
|
+
"confirm",
|
|
1405
|
+
"reroll",
|
|
1406
|
+
"quit"
|
|
1407
|
+
];
|
|
1408
|
+
if (input.capabilities.clickConfirm) {
|
|
1409
|
+
actions.push("clickConfirm");
|
|
1410
|
+
}
|
|
1411
|
+
if (input.capabilities.edit) {
|
|
1412
|
+
actions.push("edit");
|
|
1413
|
+
}
|
|
1414
|
+
if (input.capabilities.refine) {
|
|
1415
|
+
actions.push("refine");
|
|
1416
|
+
}
|
|
1417
|
+
return normalizeHintActions(actions);
|
|
1418
|
+
}
|
|
1419
|
+
function formatReadyMeta(slot) {
|
|
1420
|
+
const { candidate } = slot;
|
|
1421
|
+
if (!candidate.model) {
|
|
1422
|
+
return "";
|
|
1423
|
+
}
|
|
1424
|
+
const formattedModel = formatModelName(candidate.model);
|
|
1425
|
+
const formattedDuration = candidate.generationMs == null ? "" : ` ${formatDuration(candidate.generationMs)}`;
|
|
1426
|
+
if (candidate.cost != null) {
|
|
1427
|
+
return `${formattedModel} ${formatCost(candidate.cost)}${formattedDuration}`;
|
|
1428
|
+
}
|
|
1429
|
+
return `${formattedModel}${formattedDuration}`;
|
|
1430
|
+
}
|
|
1431
|
+
function createSlotViewModel(slot, index, selectedIndex) {
|
|
1432
|
+
if (slot.status === "pending") {
|
|
1433
|
+
return {
|
|
1434
|
+
status: "pending",
|
|
1435
|
+
selected: false,
|
|
1436
|
+
radio: "\u25CB",
|
|
1437
|
+
title: "Generating...",
|
|
1438
|
+
meta: slot.model ? formatModelName(slot.model) : void 0,
|
|
1439
|
+
muted: true
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
if (slot.status === "error") {
|
|
1443
|
+
return {
|
|
1444
|
+
status: "error",
|
|
1445
|
+
selected: false,
|
|
1446
|
+
radio: "\u25CB",
|
|
1447
|
+
title: slot.content,
|
|
1448
|
+
muted: true
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
const selected = index === selectedIndex;
|
|
1452
|
+
const title = slot.candidate.content.split("\n")[0]?.trim() || "";
|
|
1453
|
+
const meta = formatReadyMeta(slot);
|
|
1454
|
+
return {
|
|
1455
|
+
status: "ready",
|
|
1456
|
+
selected,
|
|
1457
|
+
radio: selected ? "\u25CF" : "\u25CB",
|
|
1458
|
+
title,
|
|
1459
|
+
meta: meta || void 0,
|
|
1460
|
+
muted: !selected
|
|
1253
1461
|
};
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1462
|
+
}
|
|
1463
|
+
function resolveEditedSummary(input) {
|
|
1464
|
+
const selectedSlot = input.state.slots[input.state.selectedIndex];
|
|
1465
|
+
if (selectedSlot?.status !== "ready") {
|
|
1466
|
+
return void 0;
|
|
1467
|
+
}
|
|
1468
|
+
const edited = input.editedSelections?.get(selectedSlot.candidate.slotId);
|
|
1469
|
+
if (!edited) {
|
|
1470
|
+
return void 0;
|
|
1471
|
+
}
|
|
1472
|
+
return edited.split("\n")[0]?.slice(0, 120) || "";
|
|
1473
|
+
}
|
|
1474
|
+
function buildSelectorViewModel(input) {
|
|
1475
|
+
const spinnerFrames = input.spinnerFrames ?? DEFAULT_SPINNER_FRAMES;
|
|
1476
|
+
const copy = { ...DEFAULT_SELECTOR_COPY, ...input.copy };
|
|
1477
|
+
const capabilities = {
|
|
1478
|
+
...DEFAULT_SELECTOR_CAPABILITIES,
|
|
1479
|
+
...input.capabilities
|
|
1266
1480
|
};
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1481
|
+
const readyCount = getReadyCount(input.state.slots);
|
|
1482
|
+
const totalCost = getTotalCost(input.state.slots);
|
|
1483
|
+
const frame = Math.floor(input.nowMs / 80) % spinnerFrames.length;
|
|
1484
|
+
const generatedLabel = readyCount === 1 ? `Generated 1 ${copy.itemLabelSingular}` : `Generated ${readyCount} ${copy.itemLabelPlural}`;
|
|
1485
|
+
const hintActions = resolveHintActions({
|
|
1486
|
+
readyCount,
|
|
1487
|
+
capabilities
|
|
1488
|
+
});
|
|
1489
|
+
return {
|
|
1490
|
+
header: {
|
|
1491
|
+
mode: input.state.isGenerating ? "running" : "done",
|
|
1492
|
+
spinner: spinnerFrames[frame],
|
|
1493
|
+
progress: `${readyCount}/${input.state.totalSlots}`,
|
|
1494
|
+
totalCostLabel: totalCost > 0 ? formatTotalCostLabel(totalCost) : void 0,
|
|
1495
|
+
runningLabel: copy.runningLabel,
|
|
1496
|
+
generatedLabel
|
|
1497
|
+
},
|
|
1498
|
+
hint: {
|
|
1499
|
+
kind: readyCount > 0 ? "ready" : "empty",
|
|
1500
|
+
selectionLabel: readyCount > 0 ? copy.selectionLabel : void 0,
|
|
1501
|
+
actions: hintActions
|
|
1502
|
+
},
|
|
1503
|
+
slots: input.state.slots.map(
|
|
1504
|
+
(slot, index) => createSlotViewModel(slot, index, input.state.selectedIndex)
|
|
1505
|
+
),
|
|
1506
|
+
editedSummary: resolveEditedSummary(input)
|
|
1270
1507
|
};
|
|
1271
|
-
return { render, flush, clearAll, reset };
|
|
1272
1508
|
}
|
|
1273
1509
|
|
|
1274
1510
|
// lib/selector.ts
|
|
1275
1511
|
var TTY_PATH = "/dev/tty";
|
|
1512
|
+
var CLI_HINT_GROUPS = [
|
|
1513
|
+
["navigate", "confirm", "clickConfirm"],
|
|
1514
|
+
["edit", "reroll", "refine"],
|
|
1515
|
+
["quit"]
|
|
1516
|
+
];
|
|
1276
1517
|
function collapseToReady(slots) {
|
|
1277
1518
|
const readySlots = slots.filter((s) => s.status === "ready");
|
|
1278
1519
|
slots.length = 0;
|
|
@@ -1280,77 +1521,58 @@ function collapseToReady(slots) {
|
|
|
1280
1521
|
slots.push(slot);
|
|
1281
1522
|
}
|
|
1282
1523
|
}
|
|
1283
|
-
function
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
const
|
|
1292
|
-
const line2 = `${theme.dim} ${radio2} ${slot.content}${theme.reset}`;
|
|
1293
|
-
return [line2];
|
|
1294
|
-
}
|
|
1295
|
-
const candidate = slot.candidate;
|
|
1296
|
-
const title = candidate.content.split("\n")[0]?.trim() || "";
|
|
1297
|
-
const formatDuration = (ms) => {
|
|
1298
|
-
const safeMs = Math.max(0, Math.round(ms));
|
|
1299
|
-
if (safeMs < 1e3) {
|
|
1300
|
-
return `${safeMs}ms`;
|
|
1301
|
-
}
|
|
1302
|
-
const seconds = (safeMs / 1e3).toFixed(1).replace(/\.0$/, "");
|
|
1303
|
-
return `${seconds}s`;
|
|
1304
|
-
};
|
|
1305
|
-
const formattedModel = candidate.model ? formatModelName(candidate.model) : "";
|
|
1306
|
-
const formattedDuration = candidate.generationMs == null ? "" : ` ${formatDuration(candidate.generationMs)}`;
|
|
1307
|
-
const modelInfo = candidate.model ? candidate.cost ? `${formattedModel} ${formatCost(candidate.cost)}${formattedDuration}` : `${formattedModel}${formattedDuration}` : "";
|
|
1308
|
-
if (selected) {
|
|
1309
|
-
const radio2 = "\u25CF";
|
|
1310
|
-
const line2 = ` ${radio2} ${theme.bold}${title}${theme.reset}`;
|
|
1311
|
-
const meta2 = modelInfo ? ` ${theme.progress}${modelInfo}${theme.reset}` : "";
|
|
1524
|
+
function renderSlotMeta(meta, muted) {
|
|
1525
|
+
return muted ? `${theme.dim}${meta}${theme.reset}` : `${theme.primary}${meta}${theme.reset}`;
|
|
1526
|
+
}
|
|
1527
|
+
function renderCliSlotLines(slot) {
|
|
1528
|
+
const radio = slot.selected ? `${theme.success}${slot.radio}${theme.reset}` : `${theme.dim}${slot.radio}${theme.reset}`;
|
|
1529
|
+
const linePrefix = ` ${radio} `;
|
|
1530
|
+
if (slot.status === "ready" && slot.selected) {
|
|
1531
|
+
const line2 = `${linePrefix}${theme.primary}${theme.bold}${slot.title}${theme.reset}`;
|
|
1532
|
+
const meta2 = slot.meta ? ` ${renderSlotMeta(slot.meta, false)}` : "";
|
|
1312
1533
|
return meta2 ? [line2, meta2] : [line2];
|
|
1313
1534
|
}
|
|
1314
|
-
const
|
|
1315
|
-
const
|
|
1316
|
-
const meta = modelInfo ? `${theme.dim} ${modelInfo}${theme.reset}` : "";
|
|
1535
|
+
const line = `${linePrefix}${theme.dim}${slot.title}${theme.reset}`;
|
|
1536
|
+
const meta = slot.meta ? ` ${renderSlotMeta(slot.meta, true)}` : "";
|
|
1317
1537
|
return meta ? [line, meta] : [line];
|
|
1318
1538
|
}
|
|
1539
|
+
function renderCliHintLine(actions, readyCount) {
|
|
1540
|
+
const actionSet = new Set(actions);
|
|
1541
|
+
const renderedGroups = CLI_HINT_GROUPS.map(
|
|
1542
|
+
(group) => group.filter((action) => actionSet.has(action)).map((action) => {
|
|
1543
|
+
const label = formatSelectorHintActions([action], "cli");
|
|
1544
|
+
if (action === "navigate" && readyCount <= 1) {
|
|
1545
|
+
return `${theme.dim}${label}${theme.reset}`;
|
|
1546
|
+
}
|
|
1547
|
+
return `${theme.primary}${label}${theme.reset}`;
|
|
1548
|
+
}).join(" ")
|
|
1549
|
+
).filter((groupText) => groupText !== "");
|
|
1550
|
+
const separator = ` ${theme.primary}|${theme.reset} `;
|
|
1551
|
+
return ` ${renderedGroups.join(separator)}`;
|
|
1552
|
+
}
|
|
1319
1553
|
function renderSelector(state, nowMs, renderer, editedSelections) {
|
|
1320
|
-
const { slots, selectedIndex, isGenerating, totalSlots } = state;
|
|
1321
1554
|
const lines = [];
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1555
|
+
const viewModel = buildSelectorViewModel({
|
|
1556
|
+
state,
|
|
1557
|
+
nowMs,
|
|
1558
|
+
spinnerFrames: SPINNER_FRAMES,
|
|
1559
|
+
editedSelections,
|
|
1560
|
+
capabilities: {
|
|
1561
|
+
edit: true,
|
|
1562
|
+
refine: true
|
|
1563
|
+
}
|
|
1564
|
+
});
|
|
1565
|
+
const costSuffix = viewModel.header.totalCostLabel ? ` (total: ${viewModel.header.totalCostLabel})` : "";
|
|
1566
|
+
if (viewModel.header.mode === "running") {
|
|
1329
1567
|
lines.push(
|
|
1330
|
-
`${theme.progress}${spinner}${theme.reset} ${theme.primary}
|
|
1568
|
+
`${theme.progress}${viewModel.header.spinner}${theme.reset} ${theme.primary}${viewModel.header.runningLabel} ${viewModel.header.progress}${costSuffix}${theme.reset}`
|
|
1331
1569
|
);
|
|
1332
1570
|
} else {
|
|
1333
|
-
|
|
1334
|
-
lines.push(ui.success(label));
|
|
1335
|
-
}
|
|
1336
|
-
const selectedSlot = slots[selectedIndex];
|
|
1337
|
-
const isEditedSelection = selectedSlot?.status === "ready" && editedSelections?.has(selectedSlot.candidate.slotId) === true;
|
|
1338
|
-
const hasReady = readyCount > 0;
|
|
1339
|
-
if (hasReady) {
|
|
1340
|
-
if (isEditedSelection) {
|
|
1341
|
-
lines.push(ui.success("Select a commit message"));
|
|
1342
|
-
} else {
|
|
1343
|
-
const hint = ui.hint(
|
|
1344
|
-
"\u2191\u2193 navigate \u23CE confirm e edit r reroll R refine q quit"
|
|
1345
|
-
);
|
|
1346
|
-
lines.push(ui.prompt(`Select a commit message ${hint}`));
|
|
1347
|
-
}
|
|
1348
|
-
} else {
|
|
1349
|
-
lines.push(ui.hint(" q quit"));
|
|
1571
|
+
lines.push(ui.success(`${viewModel.header.generatedLabel}${costSuffix}`));
|
|
1350
1572
|
}
|
|
1351
1573
|
lines.push("");
|
|
1352
|
-
for (
|
|
1353
|
-
const slotLines =
|
|
1574
|
+
for (const slot of viewModel.slots) {
|
|
1575
|
+
const slotLines = renderCliSlotLines(slot);
|
|
1354
1576
|
for (const line of slotLines) {
|
|
1355
1577
|
lines.push(line);
|
|
1356
1578
|
}
|
|
@@ -1358,13 +1580,19 @@ function renderSelector(state, nowMs, renderer, editedSelections) {
|
|
|
1358
1580
|
lines.push("");
|
|
1359
1581
|
}
|
|
1360
1582
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1583
|
+
const readyCount = viewModel.slots.filter(
|
|
1584
|
+
(slot) => slot.status === "ready"
|
|
1585
|
+
).length;
|
|
1586
|
+
if (viewModel.hint.kind === "ready") {
|
|
1587
|
+
lines.push(renderCliHintLine(viewModel.hint.actions, readyCount));
|
|
1588
|
+
} else {
|
|
1589
|
+
lines.push(
|
|
1590
|
+
ui.hint(` ${formatSelectorHintActions(viewModel.hint.actions, "cli")}`)
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
if (viewModel.editedSummary) {
|
|
1594
|
+
lines.push(ui.success(`Edited: ${viewModel.editedSummary}`));
|
|
1595
|
+
lines.push("");
|
|
1368
1596
|
}
|
|
1369
1597
|
renderer.render(`${lines.join("\n")}
|
|
1370
1598
|
`);
|
|
@@ -1632,6 +1860,24 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1632
1860
|
cancelGeneration();
|
|
1633
1861
|
const totalCost = getTotalCost(slots);
|
|
1634
1862
|
const quota = getLatestQuota(slots);
|
|
1863
|
+
if (clearOutput) {
|
|
1864
|
+
const viewModel = buildSelectorViewModel({
|
|
1865
|
+
state,
|
|
1866
|
+
nowMs: Date.now(),
|
|
1867
|
+
spinnerFrames: SPINNER_FRAMES,
|
|
1868
|
+
editedSelections,
|
|
1869
|
+
capabilities: { edit: true, refine: true }
|
|
1870
|
+
});
|
|
1871
|
+
const costSuffix = viewModel.header.totalCostLabel ? ` (total: ${viewModel.header.totalCostLabel})` : "";
|
|
1872
|
+
const selectedTitle = selectedContent.split("\n")[0]?.trim() || selectedContent;
|
|
1873
|
+
renderer.clearAll();
|
|
1874
|
+
ttyOutput.write(
|
|
1875
|
+
`${ui.success(`${viewModel.header.generatedLabel}${costSuffix}`)}
|
|
1876
|
+
`
|
|
1877
|
+
);
|
|
1878
|
+
ttyOutput.write(`${ui.success(`Selected: ${selectedTitle}`)}
|
|
1879
|
+
`);
|
|
1880
|
+
}
|
|
1635
1881
|
resolveOnce({
|
|
1636
1882
|
action: "confirm",
|
|
1637
1883
|
selected: selectedContent,
|
|
@@ -1640,7 +1886,7 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1640
1886
|
totalCost: totalCost > 0 ? totalCost : void 0,
|
|
1641
1887
|
quota
|
|
1642
1888
|
});
|
|
1643
|
-
cleanup(
|
|
1889
|
+
cleanup(false);
|
|
1644
1890
|
};
|
|
1645
1891
|
const rerollSelection = () => {
|
|
1646
1892
|
if (!hasReadySlot(slots)) return;
|
|
@@ -1733,7 +1979,7 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1733
1979
|
rerollSelection();
|
|
1734
1980
|
return;
|
|
1735
1981
|
}
|
|
1736
|
-
if (key.name === "r" && (key.shift || key.
|
|
1982
|
+
if (key.name === "r" && (key.shift || key.sequence === "R")) {
|
|
1737
1983
|
await refineSelection();
|
|
1738
1984
|
return;
|
|
1739
1985
|
}
|
|
@@ -2056,7 +2302,10 @@ function stageAllChanges() {
|
|
|
2056
2302
|
}
|
|
2057
2303
|
function commitWithMessage(message) {
|
|
2058
2304
|
try {
|
|
2059
|
-
execSync2(`git commit -m ${JSON.stringify(message)}`, {
|
|
2305
|
+
return execSync2(`git commit -m ${JSON.stringify(message)}`, {
|
|
2306
|
+
stdio: "pipe",
|
|
2307
|
+
encoding: "utf-8"
|
|
2308
|
+
}).trim();
|
|
2060
2309
|
} catch {
|
|
2061
2310
|
process.exit(1);
|
|
2062
2311
|
}
|
|
@@ -2200,12 +2449,19 @@ async function commit(args2) {
|
|
|
2200
2449
|
continue;
|
|
2201
2450
|
}
|
|
2202
2451
|
if (result.action === "confirm" && result.selected) {
|
|
2203
|
-
|
|
2204
|
-
const
|
|
2205
|
-
|
|
2206
|
-
|
|
2452
|
+
recordSelection(result.selectedCandidate?.generationId);
|
|
2453
|
+
const label = `git commit -m ${JSON.stringify(result.selected)}`;
|
|
2454
|
+
const renderer = createRenderer(process.stderr);
|
|
2455
|
+
renderer.render(`${SPINNER_FRAMES[0]} ${label}
|
|
2207
2456
|
`);
|
|
2208
|
-
commitWithMessage(result.selected);
|
|
2457
|
+
const output = commitWithMessage(result.selected);
|
|
2458
|
+
renderer.clearAll();
|
|
2459
|
+
console.log(ui.success(label));
|
|
2460
|
+
if (output) {
|
|
2461
|
+
for (const line of output.split("\n")) {
|
|
2462
|
+
console.log(ui.hint(` ${line}`));
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2209
2465
|
if (result.quota) {
|
|
2210
2466
|
showQuotaInfo(result.quota);
|
|
2211
2467
|
}
|
package/dist/index.js
CHANGED
|
@@ -87,6 +87,17 @@ var InvalidModelError = class extends Error {
|
|
|
87
87
|
this.name = "InvalidModelError";
|
|
88
88
|
}
|
|
89
89
|
};
|
|
90
|
+
var InputLengthExceededError = class extends Error {
|
|
91
|
+
constructor(count, limit, plan = "free", message) {
|
|
92
|
+
super(
|
|
93
|
+
message ?? `Input length ${count} exceeds the ${plan} plan limit of ${limit} characters.`
|
|
94
|
+
);
|
|
95
|
+
this.count = count;
|
|
96
|
+
this.limit = limit;
|
|
97
|
+
this.plan = plan;
|
|
98
|
+
this.name = "InputLengthExceededError";
|
|
99
|
+
}
|
|
100
|
+
};
|
|
90
101
|
async function getErrorText(response, error) {
|
|
91
102
|
if (error) {
|
|
92
103
|
try {
|
|
@@ -146,6 +157,15 @@ function handle402Error(error) {
|
|
|
146
157
|
log("generate error (402 daily_limit)", error);
|
|
147
158
|
throw new DailyLimitExceededError(count, limit, resetsAt);
|
|
148
159
|
}
|
|
160
|
+
function throwInputLengthExceededError(error) {
|
|
161
|
+
const payload = error;
|
|
162
|
+
const count = typeof payload?.count === "number" ? payload.count : 0;
|
|
163
|
+
const limit = typeof payload?.limit === "number" ? payload.limit : 0;
|
|
164
|
+
const plan = payload?.plan === "free" ? payload.plan : "free";
|
|
165
|
+
const message = typeof payload?.message === "string" ? payload.message : `Input length ${count} exceeds the ${plan} plan limit of ${limit} characters.`;
|
|
166
|
+
log("generate error (400 input_too_long)", error);
|
|
167
|
+
throw new InputLengthExceededError(count, limit, plan, message);
|
|
168
|
+
}
|
|
149
169
|
function throwInvalidModelError(error) {
|
|
150
170
|
const payload = error;
|
|
151
171
|
const message = payload?.message ?? "Model is not supported.";
|
|
@@ -157,6 +177,13 @@ function throwInvalidModelError(error) {
|
|
|
157
177
|
log("generate error (400 invalid_model)", error);
|
|
158
178
|
throw new InvalidModelError(model, allowedModels, message);
|
|
159
179
|
}
|
|
180
|
+
function handle400Error(error) {
|
|
181
|
+
const payload = error;
|
|
182
|
+
if (payload?.error === "input_too_long") {
|
|
183
|
+
throwInputLengthExceededError(error);
|
|
184
|
+
}
|
|
185
|
+
throwInvalidModelError(error);
|
|
186
|
+
}
|
|
160
187
|
function createApiClient(token) {
|
|
161
188
|
const headers = {
|
|
162
189
|
"Content-Type": "application/json"
|
|
@@ -200,7 +227,7 @@ function createApiClient(token) {
|
|
|
200
227
|
} catch {
|
|
201
228
|
errorPayload = await getErrorText(res, null);
|
|
202
229
|
}
|
|
203
|
-
|
|
230
|
+
handle400Error(errorPayload);
|
|
204
231
|
}
|
|
205
232
|
if (!res.ok) {
|
|
206
233
|
const text = await getErrorText(res, null);
|
|
@@ -265,6 +292,9 @@ function createApiClient(token) {
|
|
|
265
292
|
log("command_execution error (402)", error);
|
|
266
293
|
handle402Error(error);
|
|
267
294
|
}
|
|
295
|
+
if (response.status === 400) {
|
|
296
|
+
handle400Error(error);
|
|
297
|
+
}
|
|
268
298
|
if (!response.ok) {
|
|
269
299
|
const text = await getErrorText(response, error);
|
|
270
300
|
log("command_execution error", { status: response.status, text });
|
|
@@ -293,7 +323,7 @@ function createApiClient(token) {
|
|
|
293
323
|
handle402Error(error);
|
|
294
324
|
}
|
|
295
325
|
if (response.status === 400) {
|
|
296
|
-
|
|
326
|
+
handle400Error(error);
|
|
297
327
|
}
|
|
298
328
|
if (!response.ok) {
|
|
299
329
|
const text = await getErrorText(response, error);
|
|
@@ -347,7 +377,7 @@ function createApiClient(token) {
|
|
|
347
377
|
handle402Error(error);
|
|
348
378
|
}
|
|
349
379
|
if (response.status === 400) {
|
|
350
|
-
|
|
380
|
+
handle400Error(error);
|
|
351
381
|
}
|
|
352
382
|
if (!response.ok) {
|
|
353
383
|
const text = await getErrorText(response, error);
|
|
@@ -374,7 +404,7 @@ function createApiClient(token) {
|
|
|
374
404
|
handle402Error(error);
|
|
375
405
|
}
|
|
376
406
|
if (response.status === 400) {
|
|
377
|
-
|
|
407
|
+
handle400Error(error);
|
|
378
408
|
}
|
|
379
409
|
if (!response.ok) {
|
|
380
410
|
const text = await getErrorText(response, error);
|
|
@@ -760,6 +790,16 @@ async function handleCommandExecutionError(error, options) {
|
|
|
760
790
|
console.error(error.formatMessage());
|
|
761
791
|
process.exit(1);
|
|
762
792
|
}
|
|
793
|
+
if (error instanceof InputLengthExceededError) {
|
|
794
|
+
console.error("\x1B[31m\u2716\x1B[0m Input is too long for the Free plan.");
|
|
795
|
+
console.error(
|
|
796
|
+
` Max allowed characters: ${error.limit}. Received: ${error.count}.`
|
|
797
|
+
);
|
|
798
|
+
console.error(
|
|
799
|
+
" Please shorten your input or upgrade to Pro for unlimited input length."
|
|
800
|
+
);
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
763
803
|
const message = error instanceof Error ? error.message : String(error);
|
|
764
804
|
console.error(`Error: Failed to start command execution. ${message}`);
|
|
765
805
|
process.exit(1);
|
|
@@ -1171,6 +1211,63 @@ function formatDiffStats(stats) {
|
|
|
1171
1211
|
return parts.join(", ");
|
|
1172
1212
|
}
|
|
1173
1213
|
|
|
1214
|
+
// lib/renderer.ts
|
|
1215
|
+
import * as readline2 from "readline";
|
|
1216
|
+
var SPINNER_FRAMES = [
|
|
1217
|
+
"\u280B",
|
|
1218
|
+
"\u2819",
|
|
1219
|
+
"\u2839",
|
|
1220
|
+
"\u2838",
|
|
1221
|
+
"\u283C",
|
|
1222
|
+
"\u2834",
|
|
1223
|
+
"\u2826",
|
|
1224
|
+
"\u2827",
|
|
1225
|
+
"\u2807",
|
|
1226
|
+
"\u280F"
|
|
1227
|
+
];
|
|
1228
|
+
function isTTY(output) {
|
|
1229
|
+
return output.isTTY === true;
|
|
1230
|
+
}
|
|
1231
|
+
function createRenderer(output) {
|
|
1232
|
+
let pendingHeight = 0;
|
|
1233
|
+
let committedHeight = 0;
|
|
1234
|
+
const render = (content) => {
|
|
1235
|
+
if (!isTTY(output)) {
|
|
1236
|
+
output.write(content);
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
if (pendingHeight > 0) {
|
|
1240
|
+
readline2.moveCursor(output, 0, -pendingHeight);
|
|
1241
|
+
readline2.cursorTo(output, 0);
|
|
1242
|
+
readline2.clearScreenDown(output);
|
|
1243
|
+
}
|
|
1244
|
+
output.write(content);
|
|
1245
|
+
pendingHeight = content.split("\n").length - 1;
|
|
1246
|
+
};
|
|
1247
|
+
const flush = () => {
|
|
1248
|
+
committedHeight += pendingHeight;
|
|
1249
|
+
pendingHeight = 0;
|
|
1250
|
+
};
|
|
1251
|
+
const clearAll = () => {
|
|
1252
|
+
if (!isTTY(output)) {
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
const totalHeight = pendingHeight + committedHeight;
|
|
1256
|
+
if (totalHeight > 0) {
|
|
1257
|
+
readline2.moveCursor(output, 0, -totalHeight);
|
|
1258
|
+
readline2.cursorTo(output, 0);
|
|
1259
|
+
readline2.clearScreenDown(output);
|
|
1260
|
+
}
|
|
1261
|
+
pendingHeight = 0;
|
|
1262
|
+
committedHeight = 0;
|
|
1263
|
+
};
|
|
1264
|
+
const reset = () => {
|
|
1265
|
+
pendingHeight = 0;
|
|
1266
|
+
committedHeight = 0;
|
|
1267
|
+
};
|
|
1268
|
+
return { render, flush, clearAll, reset };
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1174
1271
|
// lib/selector.ts
|
|
1175
1272
|
import { spawn } from "child_process";
|
|
1176
1273
|
import {
|
|
@@ -1233,9 +1330,8 @@ function getSelectedCandidate(slots, selectedIndex) {
|
|
|
1233
1330
|
return slot?.status === "ready" ? slot.candidate : void 0;
|
|
1234
1331
|
}
|
|
1235
1332
|
|
|
1236
|
-
//
|
|
1237
|
-
|
|
1238
|
-
var SPINNER_FRAMES = [
|
|
1333
|
+
// ../shared/terminal-selector-view-model.ts
|
|
1334
|
+
var DEFAULT_SPINNER_FRAMES = [
|
|
1239
1335
|
"\u280B",
|
|
1240
1336
|
"\u2819",
|
|
1241
1337
|
"\u2839",
|
|
@@ -1247,51 +1343,199 @@ var SPINNER_FRAMES = [
|
|
|
1247
1343
|
"\u2807",
|
|
1248
1344
|
"\u280F"
|
|
1249
1345
|
];
|
|
1250
|
-
|
|
1251
|
-
|
|
1346
|
+
var HINT_ACTION_ORDER = [
|
|
1347
|
+
"navigate",
|
|
1348
|
+
"confirm",
|
|
1349
|
+
"clickConfirm",
|
|
1350
|
+
"edit",
|
|
1351
|
+
"reroll",
|
|
1352
|
+
"refine",
|
|
1353
|
+
"quit"
|
|
1354
|
+
];
|
|
1355
|
+
var HINT_ACTION_GROUPS = [
|
|
1356
|
+
["navigate", "confirm", "clickConfirm"],
|
|
1357
|
+
["edit", "reroll", "refine"],
|
|
1358
|
+
["quit"]
|
|
1359
|
+
];
|
|
1360
|
+
var DEFAULT_HINT_LABELS = {
|
|
1361
|
+
cli: {
|
|
1362
|
+
navigate: "\u2191\u2193 navigate",
|
|
1363
|
+
confirm: "\u23CE confirm",
|
|
1364
|
+
clickConfirm: "click confirm",
|
|
1365
|
+
edit: "(e)dit",
|
|
1366
|
+
reroll: "(r)eroll",
|
|
1367
|
+
refine: "(R)efine",
|
|
1368
|
+
quit: "(q)uit"
|
|
1369
|
+
},
|
|
1370
|
+
web: {
|
|
1371
|
+
navigate: "\u2191\u2193 navigate",
|
|
1372
|
+
confirm: "enter confirm",
|
|
1373
|
+
clickConfirm: "click confirm",
|
|
1374
|
+
edit: "(e)dit",
|
|
1375
|
+
reroll: "(r)eroll",
|
|
1376
|
+
refine: "(R)efine",
|
|
1377
|
+
quit: "(q)uit"
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
var SELECTOR_HINT_ACTION_LABELS = DEFAULT_HINT_LABELS;
|
|
1381
|
+
var DEFAULT_SELECTOR_COPY = {
|
|
1382
|
+
runningLabel: "Generating commit messages...",
|
|
1383
|
+
selectionLabel: "Select a commit message",
|
|
1384
|
+
itemLabelSingular: "commit message",
|
|
1385
|
+
itemLabelPlural: "commit messages"
|
|
1386
|
+
};
|
|
1387
|
+
var DEFAULT_SELECTOR_CAPABILITIES = {
|
|
1388
|
+
clickConfirm: false,
|
|
1389
|
+
edit: false,
|
|
1390
|
+
refine: false
|
|
1391
|
+
};
|
|
1392
|
+
function formatDuration(ms) {
|
|
1393
|
+
const safeMs = Math.max(0, Math.round(ms));
|
|
1394
|
+
if (safeMs < 1e3) {
|
|
1395
|
+
return `${safeMs}ms`;
|
|
1396
|
+
}
|
|
1397
|
+
const seconds = (safeMs / 1e3).toFixed(1).replace(/\.0$/, "");
|
|
1398
|
+
return `${seconds}s`;
|
|
1399
|
+
}
|
|
1400
|
+
function formatSelectorHintActions(actions, target, options = {}) {
|
|
1401
|
+
const labels = SELECTOR_HINT_ACTION_LABELS[target];
|
|
1402
|
+
const ordered = normalizeHintActions(actions);
|
|
1403
|
+
if (options.separator) {
|
|
1404
|
+
return ordered.map((action) => labels[action]).join(options.separator);
|
|
1405
|
+
}
|
|
1406
|
+
return HINT_ACTION_GROUPS.map(
|
|
1407
|
+
(group) => group.filter((action) => ordered.includes(action)).map((action) => labels[action]).join(" ")
|
|
1408
|
+
).filter((groupText) => groupText !== "").join(" | ");
|
|
1409
|
+
}
|
|
1410
|
+
function normalizeHintActions(actions) {
|
|
1411
|
+
const set = new Set(actions);
|
|
1412
|
+
const ordered = [];
|
|
1413
|
+
for (const action of HINT_ACTION_ORDER) {
|
|
1414
|
+
if (set.has(action)) {
|
|
1415
|
+
ordered.push(action);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return ordered;
|
|
1419
|
+
}
|
|
1420
|
+
function resolveHintActions(input) {
|
|
1421
|
+
if (input.readyCount <= 0) {
|
|
1422
|
+
return ["quit"];
|
|
1423
|
+
}
|
|
1424
|
+
const actions = [
|
|
1425
|
+
"navigate",
|
|
1426
|
+
"confirm",
|
|
1427
|
+
"reroll",
|
|
1428
|
+
"quit"
|
|
1429
|
+
];
|
|
1430
|
+
if (input.capabilities.clickConfirm) {
|
|
1431
|
+
actions.push("clickConfirm");
|
|
1432
|
+
}
|
|
1433
|
+
if (input.capabilities.edit) {
|
|
1434
|
+
actions.push("edit");
|
|
1435
|
+
}
|
|
1436
|
+
if (input.capabilities.refine) {
|
|
1437
|
+
actions.push("refine");
|
|
1438
|
+
}
|
|
1439
|
+
return normalizeHintActions(actions);
|
|
1252
1440
|
}
|
|
1253
|
-
function
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1441
|
+
function formatReadyMeta(slot) {
|
|
1442
|
+
const { candidate } = slot;
|
|
1443
|
+
if (!candidate.model) {
|
|
1444
|
+
return "";
|
|
1445
|
+
}
|
|
1446
|
+
const formattedModel = formatModelName(candidate.model);
|
|
1447
|
+
const formattedDuration = candidate.generationMs == null ? "" : ` ${formatDuration(candidate.generationMs)}`;
|
|
1448
|
+
if (candidate.cost != null) {
|
|
1449
|
+
return `${formattedModel} ${formatCost(candidate.cost)}${formattedDuration}`;
|
|
1450
|
+
}
|
|
1451
|
+
return `${formattedModel}${formattedDuration}`;
|
|
1452
|
+
}
|
|
1453
|
+
function createSlotViewModel(slot, index, selectedIndex) {
|
|
1454
|
+
if (slot.status === "pending") {
|
|
1455
|
+
return {
|
|
1456
|
+
status: "pending",
|
|
1457
|
+
selected: false,
|
|
1458
|
+
radio: "\u25CB",
|
|
1459
|
+
title: "Generating...",
|
|
1460
|
+
meta: slot.model ? formatModelName(slot.model) : void 0,
|
|
1461
|
+
muted: true
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
if (slot.status === "error") {
|
|
1465
|
+
return {
|
|
1466
|
+
status: "error",
|
|
1467
|
+
selected: false,
|
|
1468
|
+
radio: "\u25CB",
|
|
1469
|
+
title: slot.content,
|
|
1470
|
+
muted: true
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
const selected = index === selectedIndex;
|
|
1474
|
+
const title = slot.candidate.content.split("\n")[0]?.trim() || "";
|
|
1475
|
+
const meta = formatReadyMeta(slot);
|
|
1476
|
+
return {
|
|
1477
|
+
status: "ready",
|
|
1478
|
+
selected,
|
|
1479
|
+
radio: selected ? "\u25CF" : "\u25CB",
|
|
1480
|
+
title,
|
|
1481
|
+
meta: meta || void 0,
|
|
1482
|
+
muted: !selected
|
|
1272
1483
|
};
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1484
|
+
}
|
|
1485
|
+
function resolveEditedSummary(input) {
|
|
1486
|
+
const selectedSlot = input.state.slots[input.state.selectedIndex];
|
|
1487
|
+
if (selectedSlot?.status !== "ready") {
|
|
1488
|
+
return void 0;
|
|
1489
|
+
}
|
|
1490
|
+
const edited = input.editedSelections?.get(selectedSlot.candidate.slotId);
|
|
1491
|
+
if (!edited) {
|
|
1492
|
+
return void 0;
|
|
1493
|
+
}
|
|
1494
|
+
return edited.split("\n")[0]?.slice(0, 120) || "";
|
|
1495
|
+
}
|
|
1496
|
+
function buildSelectorViewModel(input) {
|
|
1497
|
+
const spinnerFrames = input.spinnerFrames ?? DEFAULT_SPINNER_FRAMES;
|
|
1498
|
+
const copy = { ...DEFAULT_SELECTOR_COPY, ...input.copy };
|
|
1499
|
+
const capabilities = {
|
|
1500
|
+
...DEFAULT_SELECTOR_CAPABILITIES,
|
|
1501
|
+
...input.capabilities
|
|
1285
1502
|
};
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1503
|
+
const readyCount = getReadyCount(input.state.slots);
|
|
1504
|
+
const totalCost = getTotalCost(input.state.slots);
|
|
1505
|
+
const frame = Math.floor(input.nowMs / 80) % spinnerFrames.length;
|
|
1506
|
+
const generatedLabel = readyCount === 1 ? `Generated 1 ${copy.itemLabelSingular}` : `Generated ${readyCount} ${copy.itemLabelPlural}`;
|
|
1507
|
+
const hintActions = resolveHintActions({
|
|
1508
|
+
readyCount,
|
|
1509
|
+
capabilities
|
|
1510
|
+
});
|
|
1511
|
+
return {
|
|
1512
|
+
header: {
|
|
1513
|
+
mode: input.state.isGenerating ? "running" : "done",
|
|
1514
|
+
spinner: spinnerFrames[frame],
|
|
1515
|
+
progress: `${readyCount}/${input.state.totalSlots}`,
|
|
1516
|
+
totalCostLabel: totalCost > 0 ? formatTotalCostLabel(totalCost) : void 0,
|
|
1517
|
+
runningLabel: copy.runningLabel,
|
|
1518
|
+
generatedLabel
|
|
1519
|
+
},
|
|
1520
|
+
hint: {
|
|
1521
|
+
kind: readyCount > 0 ? "ready" : "empty",
|
|
1522
|
+
selectionLabel: readyCount > 0 ? copy.selectionLabel : void 0,
|
|
1523
|
+
actions: hintActions
|
|
1524
|
+
},
|
|
1525
|
+
slots: input.state.slots.map(
|
|
1526
|
+
(slot, index) => createSlotViewModel(slot, index, input.state.selectedIndex)
|
|
1527
|
+
),
|
|
1528
|
+
editedSummary: resolveEditedSummary(input)
|
|
1289
1529
|
};
|
|
1290
|
-
return { render, flush, clearAll, reset };
|
|
1291
1530
|
}
|
|
1292
1531
|
|
|
1293
1532
|
// lib/selector.ts
|
|
1294
1533
|
var TTY_PATH = "/dev/tty";
|
|
1534
|
+
var CLI_HINT_GROUPS = [
|
|
1535
|
+
["navigate", "confirm", "clickConfirm"],
|
|
1536
|
+
["edit", "reroll", "refine"],
|
|
1537
|
+
["quit"]
|
|
1538
|
+
];
|
|
1295
1539
|
function collapseToReady(slots) {
|
|
1296
1540
|
const readySlots = slots.filter((s) => s.status === "ready");
|
|
1297
1541
|
slots.length = 0;
|
|
@@ -1299,77 +1543,58 @@ function collapseToReady(slots) {
|
|
|
1299
1543
|
slots.push(slot);
|
|
1300
1544
|
}
|
|
1301
1545
|
}
|
|
1302
|
-
function
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
const
|
|
1311
|
-
const line2 = `${theme.dim} ${radio2} ${slot.content}${theme.reset}`;
|
|
1312
|
-
return [line2];
|
|
1313
|
-
}
|
|
1314
|
-
const candidate = slot.candidate;
|
|
1315
|
-
const title = candidate.content.split("\n")[0]?.trim() || "";
|
|
1316
|
-
const formatDuration = (ms) => {
|
|
1317
|
-
const safeMs = Math.max(0, Math.round(ms));
|
|
1318
|
-
if (safeMs < 1e3) {
|
|
1319
|
-
return `${safeMs}ms`;
|
|
1320
|
-
}
|
|
1321
|
-
const seconds = (safeMs / 1e3).toFixed(1).replace(/\.0$/, "");
|
|
1322
|
-
return `${seconds}s`;
|
|
1323
|
-
};
|
|
1324
|
-
const formattedModel = candidate.model ? formatModelName(candidate.model) : "";
|
|
1325
|
-
const formattedDuration = candidate.generationMs == null ? "" : ` ${formatDuration(candidate.generationMs)}`;
|
|
1326
|
-
const modelInfo = candidate.model ? candidate.cost ? `${formattedModel} ${formatCost(candidate.cost)}${formattedDuration}` : `${formattedModel}${formattedDuration}` : "";
|
|
1327
|
-
if (selected) {
|
|
1328
|
-
const radio2 = "\u25CF";
|
|
1329
|
-
const line2 = ` ${radio2} ${theme.bold}${title}${theme.reset}`;
|
|
1330
|
-
const meta2 = modelInfo ? ` ${theme.progress}${modelInfo}${theme.reset}` : "";
|
|
1546
|
+
function renderSlotMeta(meta, muted) {
|
|
1547
|
+
return muted ? `${theme.dim}${meta}${theme.reset}` : `${theme.primary}${meta}${theme.reset}`;
|
|
1548
|
+
}
|
|
1549
|
+
function renderCliSlotLines(slot) {
|
|
1550
|
+
const radio = slot.selected ? `${theme.success}${slot.radio}${theme.reset}` : `${theme.dim}${slot.radio}${theme.reset}`;
|
|
1551
|
+
const linePrefix = ` ${radio} `;
|
|
1552
|
+
if (slot.status === "ready" && slot.selected) {
|
|
1553
|
+
const line2 = `${linePrefix}${theme.primary}${theme.bold}${slot.title}${theme.reset}`;
|
|
1554
|
+
const meta2 = slot.meta ? ` ${renderSlotMeta(slot.meta, false)}` : "";
|
|
1331
1555
|
return meta2 ? [line2, meta2] : [line2];
|
|
1332
1556
|
}
|
|
1333
|
-
const
|
|
1334
|
-
const
|
|
1335
|
-
const meta = modelInfo ? `${theme.dim} ${modelInfo}${theme.reset}` : "";
|
|
1557
|
+
const line = `${linePrefix}${theme.dim}${slot.title}${theme.reset}`;
|
|
1558
|
+
const meta = slot.meta ? ` ${renderSlotMeta(slot.meta, true)}` : "";
|
|
1336
1559
|
return meta ? [line, meta] : [line];
|
|
1337
1560
|
}
|
|
1561
|
+
function renderCliHintLine(actions, readyCount) {
|
|
1562
|
+
const actionSet = new Set(actions);
|
|
1563
|
+
const renderedGroups = CLI_HINT_GROUPS.map(
|
|
1564
|
+
(group) => group.filter((action) => actionSet.has(action)).map((action) => {
|
|
1565
|
+
const label = formatSelectorHintActions([action], "cli");
|
|
1566
|
+
if (action === "navigate" && readyCount <= 1) {
|
|
1567
|
+
return `${theme.dim}${label}${theme.reset}`;
|
|
1568
|
+
}
|
|
1569
|
+
return `${theme.primary}${label}${theme.reset}`;
|
|
1570
|
+
}).join(" ")
|
|
1571
|
+
).filter((groupText) => groupText !== "");
|
|
1572
|
+
const separator = ` ${theme.primary}|${theme.reset} `;
|
|
1573
|
+
return ` ${renderedGroups.join(separator)}`;
|
|
1574
|
+
}
|
|
1338
1575
|
function renderSelector(state, nowMs, renderer, editedSelections) {
|
|
1339
|
-
const { slots, selectedIndex, isGenerating, totalSlots } = state;
|
|
1340
1576
|
const lines = [];
|
|
1341
|
-
const
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1577
|
+
const viewModel = buildSelectorViewModel({
|
|
1578
|
+
state,
|
|
1579
|
+
nowMs,
|
|
1580
|
+
spinnerFrames: SPINNER_FRAMES,
|
|
1581
|
+
editedSelections,
|
|
1582
|
+
capabilities: {
|
|
1583
|
+
edit: true,
|
|
1584
|
+
refine: true
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
const costSuffix = viewModel.header.totalCostLabel ? ` (total: ${viewModel.header.totalCostLabel})` : "";
|
|
1588
|
+
if (viewModel.header.mode === "running") {
|
|
1348
1589
|
lines.push(
|
|
1349
|
-
`${theme.progress}${spinner}${theme.reset} ${theme.primary}
|
|
1590
|
+
`${theme.progress}${viewModel.header.spinner}${theme.reset} ${theme.primary}${viewModel.header.runningLabel} ${viewModel.header.progress}${costSuffix}${theme.reset}`
|
|
1350
1591
|
);
|
|
1351
1592
|
} else {
|
|
1352
|
-
|
|
1353
|
-
lines.push(ui.success(label));
|
|
1354
|
-
}
|
|
1355
|
-
const selectedSlot = slots[selectedIndex];
|
|
1356
|
-
const isEditedSelection = selectedSlot?.status === "ready" && editedSelections?.has(selectedSlot.candidate.slotId) === true;
|
|
1357
|
-
const hasReady = readyCount > 0;
|
|
1358
|
-
if (hasReady) {
|
|
1359
|
-
if (isEditedSelection) {
|
|
1360
|
-
lines.push(ui.success("Select a commit message"));
|
|
1361
|
-
} else {
|
|
1362
|
-
const hint = ui.hint(
|
|
1363
|
-
"\u2191\u2193 navigate \u23CE confirm e edit r reroll R refine q quit"
|
|
1364
|
-
);
|
|
1365
|
-
lines.push(ui.prompt(`Select a commit message ${hint}`));
|
|
1366
|
-
}
|
|
1367
|
-
} else {
|
|
1368
|
-
lines.push(ui.hint(" q quit"));
|
|
1593
|
+
lines.push(ui.success(`${viewModel.header.generatedLabel}${costSuffix}`));
|
|
1369
1594
|
}
|
|
1370
1595
|
lines.push("");
|
|
1371
|
-
for (
|
|
1372
|
-
const slotLines =
|
|
1596
|
+
for (const slot of viewModel.slots) {
|
|
1597
|
+
const slotLines = renderCliSlotLines(slot);
|
|
1373
1598
|
for (const line of slotLines) {
|
|
1374
1599
|
lines.push(line);
|
|
1375
1600
|
}
|
|
@@ -1377,13 +1602,19 @@ function renderSelector(state, nowMs, renderer, editedSelections) {
|
|
|
1377
1602
|
lines.push("");
|
|
1378
1603
|
}
|
|
1379
1604
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1605
|
+
const readyCount = viewModel.slots.filter(
|
|
1606
|
+
(slot) => slot.status === "ready"
|
|
1607
|
+
).length;
|
|
1608
|
+
if (viewModel.hint.kind === "ready") {
|
|
1609
|
+
lines.push(renderCliHintLine(viewModel.hint.actions, readyCount));
|
|
1610
|
+
} else {
|
|
1611
|
+
lines.push(
|
|
1612
|
+
ui.hint(` ${formatSelectorHintActions(viewModel.hint.actions, "cli")}`)
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
if (viewModel.editedSummary) {
|
|
1616
|
+
lines.push(ui.success(`Edited: ${viewModel.editedSummary}`));
|
|
1617
|
+
lines.push("");
|
|
1387
1618
|
}
|
|
1388
1619
|
renderer.render(`${lines.join("\n")}
|
|
1389
1620
|
`);
|
|
@@ -1651,6 +1882,24 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1651
1882
|
cancelGeneration();
|
|
1652
1883
|
const totalCost = getTotalCost(slots);
|
|
1653
1884
|
const quota = getLatestQuota(slots);
|
|
1885
|
+
if (clearOutput) {
|
|
1886
|
+
const viewModel = buildSelectorViewModel({
|
|
1887
|
+
state,
|
|
1888
|
+
nowMs: Date.now(),
|
|
1889
|
+
spinnerFrames: SPINNER_FRAMES,
|
|
1890
|
+
editedSelections,
|
|
1891
|
+
capabilities: { edit: true, refine: true }
|
|
1892
|
+
});
|
|
1893
|
+
const costSuffix = viewModel.header.totalCostLabel ? ` (total: ${viewModel.header.totalCostLabel})` : "";
|
|
1894
|
+
const selectedTitle = selectedContent.split("\n")[0]?.trim() || selectedContent;
|
|
1895
|
+
renderer.clearAll();
|
|
1896
|
+
ttyOutput.write(
|
|
1897
|
+
`${ui.success(`${viewModel.header.generatedLabel}${costSuffix}`)}
|
|
1898
|
+
`
|
|
1899
|
+
);
|
|
1900
|
+
ttyOutput.write(`${ui.success(`Selected: ${selectedTitle}`)}
|
|
1901
|
+
`);
|
|
1902
|
+
}
|
|
1654
1903
|
resolveOnce({
|
|
1655
1904
|
action: "confirm",
|
|
1656
1905
|
selected: selectedContent,
|
|
@@ -1659,7 +1908,7 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1659
1908
|
totalCost: totalCost > 0 ? totalCost : void 0,
|
|
1660
1909
|
quota
|
|
1661
1910
|
});
|
|
1662
|
-
cleanup(
|
|
1911
|
+
cleanup(false);
|
|
1663
1912
|
};
|
|
1664
1913
|
const rerollSelection = () => {
|
|
1665
1914
|
if (!hasReadySlot(slots)) return;
|
|
@@ -1752,7 +2001,7 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1752
2001
|
rerollSelection();
|
|
1753
2002
|
return;
|
|
1754
2003
|
}
|
|
1755
|
-
if (key.name === "r" && (key.shift || key.
|
|
2004
|
+
if (key.name === "r" && (key.shift || key.sequence === "R")) {
|
|
1756
2005
|
await refineSelection();
|
|
1757
2006
|
return;
|
|
1758
2007
|
}
|
|
@@ -2037,9 +2286,12 @@ function getJjDiff(revision) {
|
|
|
2037
2286
|
}
|
|
2038
2287
|
function describeRevision(revision, message) {
|
|
2039
2288
|
try {
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2289
|
+
const result = spawnSync(
|
|
2290
|
+
"jj",
|
|
2291
|
+
["describe", "-r", revision, "-m", message],
|
|
2292
|
+
{ stdio: "pipe", encoding: "utf-8" }
|
|
2293
|
+
);
|
|
2294
|
+
return [result.stdout, result.stderr].filter(Boolean).join("").trim();
|
|
2043
2295
|
} catch {
|
|
2044
2296
|
process.exit(1);
|
|
2045
2297
|
}
|
|
@@ -2159,15 +2411,22 @@ async function runInteractiveDescribe(options, models, createCandidates, context
|
|
|
2159
2411
|
continue;
|
|
2160
2412
|
}
|
|
2161
2413
|
if (result.action === "confirm" && result.selected) {
|
|
2162
|
-
|
|
2414
|
+
recordSelection(
|
|
2163
2415
|
context.apiClient,
|
|
2164
2416
|
result.selectedCandidate?.generationId
|
|
2165
2417
|
);
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
describeRevision(options.revision, result.selected);
|
|
2418
|
+
const label = `jj describe -r ${options.revision} -m ${JSON.stringify(result.selected)}`;
|
|
2419
|
+
const renderer = createRenderer(process.stderr);
|
|
2420
|
+
renderer.render(`${SPINNER_FRAMES[0]} ${label}
|
|
2421
|
+
`);
|
|
2422
|
+
const output = describeRevision(options.revision, result.selected);
|
|
2423
|
+
renderer.clearAll();
|
|
2424
|
+
console.log(ui.success(label));
|
|
2425
|
+
if (output) {
|
|
2426
|
+
for (const line of output.split("\n")) {
|
|
2427
|
+
console.log(ui.hint(` ${line}`));
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2171
2430
|
if (result.quota) {
|
|
2172
2431
|
showQuotaInfo(result.quota);
|
|
2173
2432
|
}
|
|
@@ -2730,7 +2989,7 @@ function parseArgs(args2) {
|
|
|
2730
2989
|
// package.json
|
|
2731
2990
|
var package_default = {
|
|
2732
2991
|
name: "ultrahope",
|
|
2733
|
-
version: "0.1.
|
|
2992
|
+
version: "0.1.7",
|
|
2734
2993
|
description: "LLM-powered development workflow assistant",
|
|
2735
2994
|
type: "module",
|
|
2736
2995
|
license: "MIT",
|