ultrahope 0.1.9 → 0.1.10

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.
@@ -1037,6 +1037,10 @@ var DEFAULT_MODELS = [
1037
1037
  "mistral/ministral-3b",
1038
1038
  "xai/grok-code-fast-1"
1039
1039
  ];
1040
+ var DEFAULT_ESCALATION_MODELS = [
1041
+ "anthropic/claude-sonnet-4.6",
1042
+ "openai/gpt-5.3-codex"
1043
+ ];
1040
1044
  var isAbortError = (error) => error instanceof Error && error.name === "AbortError";
1041
1045
  var isInvalidCliSessionIdError = (error) => error instanceof Error && error.message.includes("Invalid cliSessionId");
1042
1046
  var delay = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
@@ -1278,7 +1282,7 @@ function findNearestProjectConfig(cwd) {
1278
1282
  current = parent;
1279
1283
  }
1280
1284
  }
1281
- function readConfigModels(configPath) {
1285
+ function readConfigField(configPath, field) {
1282
1286
  let raw = "";
1283
1287
  try {
1284
1288
  raw = fs2.readFileSync(configPath, "utf-8");
@@ -1293,10 +1297,10 @@ function readConfigModels(configPath) {
1293
1297
  const message = error instanceof Error ? error.message : String(error);
1294
1298
  fail(`Failed to parse TOML config ${configPath}: ${message}`);
1295
1299
  }
1296
- if (parsed.models === void 0) {
1300
+ if (parsed[field] === void 0) {
1297
1301
  return void 0;
1298
1302
  }
1299
- return validateModels(parsed.models, configPath);
1303
+ return validateModels(parsed[field], configPath);
1300
1304
  }
1301
1305
  function resolveModels(cliModels) {
1302
1306
  if (cliModels && cliModels.length > 0) {
@@ -1304,20 +1308,43 @@ function resolveModels(cliModels) {
1304
1308
  }
1305
1309
  const projectConfigPath = findNearestProjectConfig(process.cwd());
1306
1310
  if (projectConfigPath) {
1307
- const projectModels = readConfigModels(projectConfigPath);
1311
+ const projectModels = readConfigField(projectConfigPath, "models");
1308
1312
  if (projectModels) {
1309
1313
  return projectModels;
1310
1314
  }
1311
1315
  }
1312
1316
  const globalConfigPath = getGlobalConfigPath();
1313
1317
  if (fs2.existsSync(globalConfigPath)) {
1314
- const globalModels = readConfigModels(globalConfigPath);
1318
+ const globalModels = readConfigField(globalConfigPath, "models");
1315
1319
  if (globalModels) {
1316
1320
  return globalModels;
1317
1321
  }
1318
1322
  }
1319
1323
  return DEFAULT_MODELS;
1320
1324
  }
1325
+ function resolveEscalationModels(cliEscalationModels) {
1326
+ if (cliEscalationModels && cliEscalationModels.length > 0) {
1327
+ return cliEscalationModels;
1328
+ }
1329
+ const projectConfigPath = findNearestProjectConfig(process.cwd());
1330
+ if (projectConfigPath) {
1331
+ const projectModels = readConfigField(
1332
+ projectConfigPath,
1333
+ "escalation_models"
1334
+ );
1335
+ if (projectModels) {
1336
+ return projectModels;
1337
+ }
1338
+ }
1339
+ const globalConfigPath = getGlobalConfigPath();
1340
+ if (fs2.existsSync(globalConfigPath)) {
1341
+ const globalModels = readConfigField(globalConfigPath, "escalation_models");
1342
+ if (globalModels) {
1343
+ return globalModels;
1344
+ }
1345
+ }
1346
+ return DEFAULT_ESCALATION_MODELS;
1347
+ }
1321
1348
 
1322
1349
  // lib/diff-stats.ts
1323
1350
  import { execSync } from "child_process";
@@ -1353,6 +1380,441 @@ function formatDiffStats(stats) {
1353
1380
 
1354
1381
  // lib/renderer.ts
1355
1382
  import * as readline2 from "readline";
1383
+
1384
+ // ../shared/terminal-selector-helpers.ts
1385
+ function formatModelName(model) {
1386
+ const parts = model.split("/");
1387
+ return parts.length > 1 ? parts[1] : model;
1388
+ }
1389
+ function formatCost(cost) {
1390
+ return `$${cost.toFixed(7).replace(/0+$/, "").replace(/\.$/, "")}`;
1391
+ }
1392
+ function formatTotalCostLabel(cost) {
1393
+ return `$${cost.toFixed(6)}`;
1394
+ }
1395
+ function getReadyCount(slots) {
1396
+ return slots.filter((slot) => slot.status === "ready").length;
1397
+ }
1398
+ function getTotalCost(slots) {
1399
+ return slots.reduce((sum, slot) => {
1400
+ if (slot.status === "ready" && slot.candidate.cost != null) {
1401
+ return sum + slot.candidate.cost;
1402
+ }
1403
+ return sum;
1404
+ }, 0);
1405
+ }
1406
+ function getLatestQuota(slots) {
1407
+ for (const slot of slots) {
1408
+ if (slot.status === "ready" && slot.candidate.quota) {
1409
+ return slot.candidate.quota;
1410
+ }
1411
+ }
1412
+ return void 0;
1413
+ }
1414
+ function normalizeCandidateLineBreaks(content) {
1415
+ const normalized = content.replace(/\r\n?|[\u2028\u2029]/g, "\n");
1416
+ return normalized.split("\n")[0]?.trim() || "";
1417
+ }
1418
+ function normalizeCandidateContentForDisplay(content) {
1419
+ const line = normalizeCandidateLineBreaks(content);
1420
+ if (!line) {
1421
+ return "";
1422
+ }
1423
+ const collapsed = line.replace(/\s+/g, " ").trim();
1424
+ if (!collapsed) {
1425
+ return "";
1426
+ }
1427
+ if (collapsed.length % 2 === 0) {
1428
+ const half = collapsed.length / 2;
1429
+ if (collapsed.slice(0, half) === collapsed.slice(half)) {
1430
+ return collapsed.slice(0, half);
1431
+ }
1432
+ }
1433
+ const words = collapsed.split(" ");
1434
+ if (words.length >= 6 && words.length % 2 === 0) {
1435
+ const half = words.length / 2;
1436
+ const firstHalf = words.slice(0, half).join(" ");
1437
+ const secondHalf = words.slice(half).join(" ");
1438
+ if (firstHalf === secondHalf) {
1439
+ return firstHalf;
1440
+ }
1441
+ }
1442
+ return collapsed;
1443
+ }
1444
+ function hasReadySlot(slots) {
1445
+ return getReadyCount(slots) > 0;
1446
+ }
1447
+ function selectNearestReady(slots, startIndex, direction) {
1448
+ for (let index = startIndex + direction; index >= 0 && index < slots.length; index += direction) {
1449
+ if (slots[index]?.status === "ready") {
1450
+ return index;
1451
+ }
1452
+ }
1453
+ return startIndex;
1454
+ }
1455
+ function getSelectedCandidate(slots, selectedIndex) {
1456
+ const slot = slots[selectedIndex];
1457
+ return slot?.status === "ready" ? slot.candidate : void 0;
1458
+ }
1459
+
1460
+ // ../shared/terminal-selector-view-model.ts
1461
+ var DEFAULT_SPINNER_FRAMES = [
1462
+ "\u280B",
1463
+ "\u2819",
1464
+ "\u2839",
1465
+ "\u2838",
1466
+ "\u283C",
1467
+ "\u2834",
1468
+ "\u2826",
1469
+ "\u2827",
1470
+ "\u2807",
1471
+ "\u280F"
1472
+ ];
1473
+ var HINT_ACTION_ORDER = [
1474
+ "navigate",
1475
+ "confirm",
1476
+ "clickConfirm",
1477
+ "edit",
1478
+ "refine",
1479
+ "escalate",
1480
+ "quit"
1481
+ ];
1482
+ var PROMPT_EDIT_HINT = "enter apply | esc back to select";
1483
+ var PROMPT_REFINE_HINT = "enter refine | esc back to select";
1484
+ var PROMPT_EDIT_PREFIX = " > ";
1485
+ var PROMPT_REFINE_PREFIX = " Refine: ";
1486
+ var PROMPT_REFINE_PLACEHOLDER = "e.g. more formal / shorter / in Japanese";
1487
+ var DEFAULT_SELECTOR_COPY = {
1488
+ runningLabel: "Generating commit messages...",
1489
+ selectionLabel: "Select a commit message",
1490
+ itemLabelSingular: "commit message",
1491
+ itemLabelPlural: "commit messages"
1492
+ };
1493
+ var DEFAULT_SELECTOR_CAPABILITIES = {
1494
+ clickConfirm: false,
1495
+ edit: false,
1496
+ refine: false,
1497
+ escalate: false
1498
+ };
1499
+ function formatDuration(ms) {
1500
+ const safeMs = Math.max(0, Math.round(ms));
1501
+ if (safeMs < 1e3) {
1502
+ return `${safeMs}ms`;
1503
+ }
1504
+ const seconds = (safeMs / 1e3).toFixed(1).replace(/\.0$/, "");
1505
+ return `${seconds}s`;
1506
+ }
1507
+ function resolveRenderMode(inputState) {
1508
+ return inputState.mode === "prompt" ? "prompt" : "list";
1509
+ }
1510
+ function resolvePromptLineLabel(kind) {
1511
+ return kind === "edit" ? "Edit mode" : "\u2192 Refine mode";
1512
+ }
1513
+ function buildPromptSlots(viewModelSlots, promptKind, targetIndex, bufferText) {
1514
+ if (promptKind === "edit") {
1515
+ return viewModelSlots.map((slot, index) => {
1516
+ const isTarget = index === targetIndex;
1517
+ return {
1518
+ ...slot,
1519
+ radio: isTarget ? ">" : "\u25CB",
1520
+ title: isTarget ? bufferText : slot.title,
1521
+ selected: isTarget,
1522
+ muted: isTarget ? false : slot.muted
1523
+ };
1524
+ });
1525
+ }
1526
+ return viewModelSlots;
1527
+ }
1528
+ function estimatePromptSlotLineCount(slots) {
1529
+ let count = 1;
1530
+ for (const slot of slots) {
1531
+ const lineCount = 1 + (slot.meta == null ? 0 : 1) + 1;
1532
+ count += lineCount;
1533
+ }
1534
+ return count;
1535
+ }
1536
+ function estimatePromptEditInputLineIndex(promptSlots, targetIndex) {
1537
+ let lineIndex = 2;
1538
+ for (const [index, slot] of promptSlots.entries()) {
1539
+ if (index === targetIndex) {
1540
+ return lineIndex;
1541
+ }
1542
+ const slotLineCount = 1 + (slot.meta == null ? 0 : 1) + 1;
1543
+ lineIndex += slotLineCount;
1544
+ }
1545
+ return Math.max(2, lineIndex);
1546
+ }
1547
+ function buildPromptViewModel(input, viewModel) {
1548
+ const state = input.state;
1549
+ const promptKind = state.promptKind ?? "refine";
1550
+ const targetIndex = state.promptTargetIndex ?? state.selectedIndex;
1551
+ const targetSlot = state.slots[targetIndex];
1552
+ const targetText = targetSlot?.status === "ready" ? normalizeCandidateContentForDisplay(targetSlot.candidate.content) : "(no selection)";
1553
+ const totalCost = getTotalCost(state.slots);
1554
+ const costLineLabel = totalCost > 0 ? formatTotalCostLabel(totalCost) : "$0.000000";
1555
+ const generatedCostSuffix = viewModel.header.totalCostLabel ? ` (total: ${viewModel.header.totalCostLabel})` : "";
1556
+ const generatedLine = `${viewModel.header.generatedLabel}${generatedCostSuffix}`;
1557
+ const modeLine = resolvePromptLineLabel(promptKind);
1558
+ const targetLineLabel = `Target [${Math.min(state.totalSlots, targetIndex + 1)}]:`;
1559
+ const duration = formatDuration(input.nowMs - state.createdAtMs);
1560
+ const costLine = `Cost/Time (current): ${costLineLabel} / ${duration}`;
1561
+ const isEdit = promptKind === "edit";
1562
+ const promptInputPrefix = isEdit ? PROMPT_EDIT_PREFIX : PROMPT_REFINE_PREFIX;
1563
+ const promptHintLine = isEdit ? PROMPT_EDIT_HINT : PROMPT_REFINE_HINT;
1564
+ const promptPlaceholderLine = isEdit ? void 0 : PROMPT_REFINE_PLACEHOLDER;
1565
+ const promptInputText = input.bufferText ?? "";
1566
+ const promptSlots = buildPromptSlots(
1567
+ viewModel.slots,
1568
+ promptKind,
1569
+ targetIndex,
1570
+ promptInputText
1571
+ );
1572
+ const promptSlotLineCount = estimatePromptSlotLineCount(promptSlots);
1573
+ const promptExtraLines = isEdit ? 1 : 4;
1574
+ const promptLineCount = 1 + promptSlotLineCount + promptExtraLines;
1575
+ const promptInputLineIndex = isEdit ? estimatePromptEditInputLineIndex(promptSlots, targetIndex) : promptLineCount - promptExtraLines;
1576
+ const promptInputPrefixWidth = promptInputPrefix.length;
1577
+ const selectedLine = promptKind === "edit" ? `Selected: ${targetText}` : void 0;
1578
+ return {
1579
+ kind: promptKind,
1580
+ generatedLine,
1581
+ selectedLine,
1582
+ modeLine,
1583
+ targetLineLabel,
1584
+ targetText,
1585
+ targetIndex,
1586
+ costLine,
1587
+ questionLine: "?",
1588
+ slots: promptSlots,
1589
+ promptLineCount,
1590
+ promptInputLineIndex,
1591
+ promptInputPrefix,
1592
+ promptInputPrefixWidth,
1593
+ promptInputText,
1594
+ promptPlaceholderLine,
1595
+ promptHintLine
1596
+ };
1597
+ }
1598
+ function normalizeHintActions(actions) {
1599
+ const set = new Set(actions);
1600
+ const ordered = [];
1601
+ for (const action of HINT_ACTION_ORDER) {
1602
+ if (set.has(action)) {
1603
+ ordered.push(action);
1604
+ }
1605
+ }
1606
+ return ordered;
1607
+ }
1608
+ function resolveHintActions(input) {
1609
+ const actions = ["navigate", "confirm", "quit"];
1610
+ if (input.capabilities.clickConfirm) {
1611
+ actions.push("clickConfirm");
1612
+ }
1613
+ if (input.capabilities.edit) {
1614
+ actions.push("edit");
1615
+ }
1616
+ if (input.capabilities.refine) {
1617
+ actions.push("refine");
1618
+ }
1619
+ if (input.capabilities.escalate) {
1620
+ actions.push("escalate");
1621
+ }
1622
+ return normalizeHintActions(actions);
1623
+ }
1624
+ function formatReadyMeta(slot) {
1625
+ const { candidate } = slot;
1626
+ if (!candidate.model) {
1627
+ return "";
1628
+ }
1629
+ const formattedModel = formatModelName(candidate.model);
1630
+ const formattedDuration = candidate.generationMs == null ? "" : ` ${formatDuration(candidate.generationMs)}`;
1631
+ if (candidate.cost != null) {
1632
+ return `${formattedModel} ${formatCost(candidate.cost)}${formattedDuration}`;
1633
+ }
1634
+ return `${formattedModel}${formattedDuration}`;
1635
+ }
1636
+ function createSlotViewModel(slot, index, selectedIndex) {
1637
+ if (slot.status === "pending") {
1638
+ return {
1639
+ status: "pending",
1640
+ selected: false,
1641
+ radio: "\u25CB",
1642
+ title: "Generating...",
1643
+ meta: slot.model ? formatModelName(slot.model) : void 0,
1644
+ muted: true
1645
+ };
1646
+ }
1647
+ if (slot.status === "error") {
1648
+ return {
1649
+ status: "error",
1650
+ selected: false,
1651
+ radio: "\u25CB",
1652
+ title: slot.content,
1653
+ muted: true
1654
+ };
1655
+ }
1656
+ const selected = index === selectedIndex;
1657
+ const title = slot.candidate.content.split("\n")[0]?.trim() || "";
1658
+ const meta = formatReadyMeta(slot);
1659
+ return {
1660
+ status: "ready",
1661
+ selected,
1662
+ radio: selected ? "\u25CF" : "\u25CB",
1663
+ title,
1664
+ meta: meta || void 0,
1665
+ muted: !selected
1666
+ };
1667
+ }
1668
+ function resolveEditedSummary(input) {
1669
+ const selectedSlot = input.state.slots[input.state.selectedIndex];
1670
+ if (selectedSlot?.status !== "ready") {
1671
+ return void 0;
1672
+ }
1673
+ const edited = input.editedSelections?.get(selectedSlot.candidate.slotId);
1674
+ if (!edited) {
1675
+ return void 0;
1676
+ }
1677
+ return edited.split("\n")[0]?.slice(0, 120) || "";
1678
+ }
1679
+ function buildSelectorViewModel(input) {
1680
+ const spinnerFrames = input.spinnerFrames ?? DEFAULT_SPINNER_FRAMES;
1681
+ const copy = { ...DEFAULT_SELECTOR_COPY, ...input.copy };
1682
+ const capabilities = {
1683
+ ...DEFAULT_SELECTOR_CAPABILITIES,
1684
+ ...input.capabilities
1685
+ };
1686
+ const readyCount = getReadyCount(input.state.slots);
1687
+ const totalCost = getTotalCost(input.state.slots);
1688
+ const frame = Math.floor(input.nowMs / 80) % spinnerFrames.length;
1689
+ const generatedLabel = readyCount === 1 ? `Generated 1 ${copy.itemLabelSingular}` : `Generated ${readyCount} ${copy.itemLabelPlural}`;
1690
+ const hintActions = resolveHintActions({
1691
+ readyCount,
1692
+ capabilities
1693
+ });
1694
+ return {
1695
+ mode: resolveRenderMode(input.state),
1696
+ header: {
1697
+ mode: input.state.isGenerating ? "running" : "done",
1698
+ spinner: spinnerFrames[frame],
1699
+ progress: `${readyCount}/${input.state.totalSlots}`,
1700
+ totalCostLabel: totalCost > 0 ? formatTotalCostLabel(totalCost) : void 0,
1701
+ runningLabel: copy.runningLabel,
1702
+ generatedLabel
1703
+ },
1704
+ hint: {
1705
+ kind: readyCount > 0 ? "ready" : "empty",
1706
+ selectionLabel: readyCount > 0 ? copy.selectionLabel : void 0,
1707
+ actions: hintActions
1708
+ },
1709
+ slots: input.state.slots.map(
1710
+ (slot, index) => createSlotViewModel(slot, index, input.state.selectedIndex)
1711
+ ),
1712
+ editedSummary: resolveEditedSummary(input)
1713
+ };
1714
+ }
1715
+ function selectorRenderFrame(input) {
1716
+ const viewModel = buildSelectorViewModel(input);
1717
+ if (viewModel.mode === "prompt") {
1718
+ return {
1719
+ mode: "prompt",
1720
+ viewModel,
1721
+ prompt: buildPromptViewModel(input, viewModel)
1722
+ };
1723
+ }
1724
+ return {
1725
+ mode: "list",
1726
+ viewModel
1727
+ };
1728
+ }
1729
+ function slotToRenderLines(slot) {
1730
+ const lines = [
1731
+ {
1732
+ type: "slot",
1733
+ radio: slot.radio,
1734
+ title: slot.title,
1735
+ selected: slot.selected,
1736
+ muted: slot.muted
1737
+ }
1738
+ ];
1739
+ if (slot.meta) {
1740
+ lines.push({ type: "slotMeta", text: slot.meta, muted: slot.muted });
1741
+ }
1742
+ return lines;
1743
+ }
1744
+ function buildHeaderLine(viewModel) {
1745
+ const costSuffix = viewModel.header.totalCostLabel != null ? ` (total: ${viewModel.header.totalCostLabel})` : "";
1746
+ if (viewModel.header.mode === "running") {
1747
+ return {
1748
+ type: "headerRunning",
1749
+ spinner: viewModel.header.spinner ?? "",
1750
+ label: viewModel.header.runningLabel,
1751
+ progress: viewModel.header.progress,
1752
+ costSuffix
1753
+ };
1754
+ }
1755
+ return {
1756
+ type: "headerDone",
1757
+ label: viewModel.header.generatedLabel,
1758
+ costSuffix
1759
+ };
1760
+ }
1761
+ function buildPromptRenderLines(frame) {
1762
+ const prompt = frame.prompt;
1763
+ if (!prompt) return [];
1764
+ const lines = [{ type: "blank" }];
1765
+ for (const slot of prompt.slots) {
1766
+ lines.push(...slotToRenderLines(slot));
1767
+ lines.push({ type: "blank" });
1768
+ }
1769
+ if (prompt.kind === "refine") {
1770
+ lines.push({
1771
+ type: "promptInput",
1772
+ prefix: prompt.promptInputPrefix,
1773
+ text: prompt.promptInputText
1774
+ });
1775
+ if (prompt.promptPlaceholderLine != null) {
1776
+ lines.push({ type: "placeholder", text: prompt.promptPlaceholderLine });
1777
+ lines.push({ type: "blank" });
1778
+ }
1779
+ }
1780
+ lines.push({
1781
+ type: "hint",
1782
+ text: prompt.promptHintLine,
1783
+ actions: [],
1784
+ readyCount: 0
1785
+ });
1786
+ return lines;
1787
+ }
1788
+ function buildListRenderLines(viewModel) {
1789
+ const lines = [{ type: "blank" }];
1790
+ for (const slot of viewModel.slots) {
1791
+ lines.push(...slotToRenderLines(slot));
1792
+ lines.push({ type: "blank" });
1793
+ }
1794
+ if (viewModel.editedSummary) {
1795
+ lines.push({ type: "blank" });
1796
+ lines.push({ type: "editedSummary", text: viewModel.editedSummary });
1797
+ }
1798
+ const readyCount = viewModel.slots.filter((s) => s.status === "ready").length;
1799
+ lines.push({
1800
+ type: "hint",
1801
+ text: "",
1802
+ actions: viewModel.hint.actions,
1803
+ readyCount
1804
+ });
1805
+ return lines;
1806
+ }
1807
+ function buildSelectorRenderLines(frame) {
1808
+ const lines = [buildHeaderLine(frame.viewModel)];
1809
+ if (frame.mode === "prompt") {
1810
+ lines.push(...buildPromptRenderLines(frame));
1811
+ } else {
1812
+ lines.push(...buildListRenderLines(frame.viewModel));
1813
+ }
1814
+ return lines;
1815
+ }
1816
+
1817
+ // lib/renderer.ts
1356
1818
  var SPINNER_FRAMES = [
1357
1819
  "\u280B",
1358
1820
  "\u2819",
@@ -1394,101 +1856,50 @@ var SELECTOR_HINT_LABELS = {
1394
1856
  clickConfirm: "click confirm",
1395
1857
  edit: "(e)dit",
1396
1858
  refine: "(r)efine",
1859
+ escalate: "(E)scalate",
1397
1860
  quit: "(q)uit"
1398
1861
  };
1399
- function renderSlotLine(slot) {
1400
- const selected = slot.selected;
1401
- const radio = `${selected ? `${theme.success}\u25CF` : `${theme.dim}\u25CB`}${theme.reset}`;
1402
- const linePrefix = ` ${radio} `;
1403
- const titleColor = selected ? theme.primary : theme.dim;
1404
- const titleFont = selected ? theme.bold : "";
1405
- const lines = [
1406
- `${linePrefix}${titleColor}${titleFont}${slot.title}${theme.reset}`
1407
- ];
1408
- if (slot.meta) {
1409
- const metaColor = slot.muted ? theme.dim : theme.primary;
1410
- lines.push(` ${metaColor}${slot.meta}${theme.reset}`);
1411
- }
1412
- return lines;
1413
- }
1414
- function renderHintLine(viewModelHintActions, readyCount) {
1415
- const hasReady = readyCount > 0;
1416
- const canNavigate = readyCount >= 2;
1417
- const canEdit = hasReady;
1418
- const canRefine = hasReady;
1419
- const canQuit = true;
1420
- const canConfirm = hasReady;
1421
- const canClickConfirm = hasReady;
1422
- const actionSet = new Set(viewModelHintActions);
1423
- const asHint = (label, enabled) => enabled ? label : `${theme.dim}${label}${theme.reset}`;
1424
- const labels = [
1425
- actionSet.has("navigate") ? asHint(SELECTOR_HINT_LABELS.navigate, canNavigate) : "",
1426
- actionSet.has("edit") ? asHint(SELECTOR_HINT_LABELS.edit, canEdit) : "",
1427
- actionSet.has("refine") ? asHint(SELECTOR_HINT_LABELS.refine, canRefine) : "",
1428
- actionSet.has("quit") ? asHint(SELECTOR_HINT_LABELS.quit, canQuit) : "",
1429
- actionSet.has("confirm") ? asHint(SELECTOR_HINT_LABELS.confirm, canConfirm) : "",
1430
- actionSet.has("clickConfirm") ? asHint(SELECTOR_HINT_LABELS.clickConfirm, canClickConfirm) : ""
1431
- ].filter((line) => line !== "");
1862
+ function renderHintActions(actions) {
1863
+ if (actions.length === 0) return "";
1864
+ const labels = actions.map((a) => SELECTOR_HINT_LABELS[a]).filter(Boolean);
1432
1865
  return labels.length > 0 ? ` ${labels.join(" | ")}` : "";
1433
1866
  }
1434
- function renderPromptLines(frame) {
1435
- const prompt = frame.prompt;
1436
- if (!prompt) {
1437
- return [];
1438
- }
1439
- const lines = [];
1440
- if (prompt.selectedLine) {
1441
- lines.push(ui.success(prompt.selectedLine));
1442
- }
1443
- if (prompt.kind === "edit") {
1444
- lines.push("");
1445
- lines.push(` ${ui.hint(prompt.modeLine)}`);
1446
- return lines;
1447
- }
1448
- lines.push("");
1449
- lines.push(ui.hint(prompt.modeLine));
1450
- lines.push(`${ui.hint(prompt.targetLineLabel)} ${prompt.targetText}`);
1451
- lines.push(prompt.costLine);
1452
- lines.push("");
1453
- lines.push(prompt.questionLine);
1454
- return lines;
1867
+ function renderLineToString(line) {
1868
+ switch (line.type) {
1869
+ case "headerRunning":
1870
+ return `${theme.progress}${line.spinner}${theme.reset} ${theme.primary}${line.label} ${line.progress}${line.costSuffix}${theme.reset}`;
1871
+ case "headerDone":
1872
+ return ui.success(`${line.label}${line.costSuffix}`);
1873
+ case "blank":
1874
+ return "";
1875
+ case "slot": {
1876
+ if (line.radio === ">") {
1877
+ return ` ${theme.success}>${theme.reset} ${line.title}`;
1878
+ }
1879
+ const radioStr = `${line.selected ? `${theme.success}\u25CF` : `${theme.dim}\u25CB`}${theme.reset}`;
1880
+ const titleColor = line.selected ? theme.primary : theme.dim;
1881
+ const titleFont = line.selected ? theme.bold : "";
1882
+ return ` ${radioStr} ${titleColor}${titleFont}${line.title}${theme.reset}`;
1883
+ }
1884
+ case "slotMeta": {
1885
+ const metaColor = line.muted ? theme.dim : theme.primary;
1886
+ return ` ${metaColor}${line.text}${theme.reset}`;
1887
+ }
1888
+ case "promptInput":
1889
+ return `${theme.primary}${line.prefix}${theme.reset}${line.text}`;
1890
+ case "placeholder":
1891
+ return ` ${theme.dim}${line.text}${theme.reset}`;
1892
+ case "hint":
1893
+ if (line.actions.length > 0) {
1894
+ return renderHintActions(line.actions);
1895
+ }
1896
+ return ` ${ui.hint(line.text)}`;
1897
+ case "editedSummary":
1898
+ return ui.success(`Edited: ${line.text}`);
1899
+ }
1455
1900
  }
1456
1901
  function renderSelectorLinesFromRenderFrame(frame) {
1457
- const lines = [];
1458
- const viewModel = frame.viewModel;
1459
- const costSuffix = viewModel.header.totalCostLabel != null ? ` (total: ${viewModel.header.totalCostLabel})` : "";
1460
- if (viewModel.header.mode === "running") {
1461
- lines.push(
1462
- `${theme.progress}${viewModel.header.spinner}${theme.reset} ${theme.primary}${viewModel.header.runningLabel} ${viewModel.header.progress}${costSuffix}${theme.reset}`
1463
- );
1464
- } else {
1465
- lines.push(ui.success(`${viewModel.header.generatedLabel}${costSuffix}`));
1466
- }
1467
- if (frame.mode === "prompt") {
1468
- lines.push(...renderPromptLines(frame));
1469
- return lines;
1470
- }
1471
- lines.push("");
1472
- for (const slot of viewModel.slots) {
1473
- for (const line of renderSlotLine(slot)) {
1474
- lines.push(line);
1475
- }
1476
- lines.push("");
1477
- }
1478
- if (viewModel.editedSummary) {
1479
- lines.push("");
1480
- lines.push(ui.success(`Edited: ${viewModel.editedSummary}`));
1481
- }
1482
- const readyCount = viewModel.slots.filter(
1483
- (slot) => slot.status === "ready"
1484
- ).length;
1485
- lines.push(
1486
- renderHintLine(viewModel.hint.actions, readyCount) || renderHintLine(
1487
- ["navigate", "edit", "refine", "quit", "confirm"],
1488
- readyCount
1489
- )
1490
- );
1491
- return lines;
1902
+ return buildSelectorRenderLines(frame).map(renderLineToString);
1492
1903
  }
1493
1904
  function renderSelectorTextFromRenderFrame(frame) {
1494
1905
  return `${renderSelectorLinesFromRenderFrame(frame).join("\n")}
@@ -1519,113 +1930,37 @@ function createRenderer(output) {
1519
1930
  };
1520
1931
  const clearAll = () => {
1521
1932
  if (!isTTY(output)) {
1522
- return;
1523
- }
1524
- const totalHeight = pendingHeight + committedHeight;
1525
- if (totalHeight > 0) {
1526
- readline2.moveCursor(output, 0, -totalHeight);
1527
- readline2.cursorTo(output, 0);
1528
- readline2.clearScreenDown(output);
1529
- }
1530
- pendingHeight = 0;
1531
- committedHeight = 0;
1532
- };
1533
- const reset = () => {
1534
- pendingHeight = 0;
1535
- committedHeight = 0;
1536
- };
1537
- return {
1538
- render,
1539
- renderSelectorFrame,
1540
- flush,
1541
- clearAll,
1542
- reset
1543
- };
1544
- }
1545
-
1546
- // lib/selector.ts
1547
- import { spawn } from "child_process";
1548
- import { mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
1549
- import { tmpdir } from "os";
1550
- import { join as join4 } from "path";
1551
- import * as readline3 from "readline";
1552
-
1553
- // ../shared/terminal-selector-helpers.ts
1554
- function formatModelName(model) {
1555
- const parts = model.split("/");
1556
- return parts.length > 1 ? parts[1] : model;
1557
- }
1558
- function formatCost(cost) {
1559
- return `$${cost.toFixed(7).replace(/0+$/, "").replace(/\.$/, "")}`;
1560
- }
1561
- function formatTotalCostLabel(cost) {
1562
- return `$${cost.toFixed(6)}`;
1563
- }
1564
- function getReadyCount(slots) {
1565
- return slots.filter((slot) => slot.status === "ready").length;
1566
- }
1567
- function getTotalCost(slots) {
1568
- return slots.reduce((sum, slot) => {
1569
- if (slot.status === "ready" && slot.candidate.cost != null) {
1570
- return sum + slot.candidate.cost;
1571
- }
1572
- return sum;
1573
- }, 0);
1574
- }
1575
- function getLatestQuota(slots) {
1576
- for (const slot of slots) {
1577
- if (slot.status === "ready" && slot.candidate.quota) {
1578
- return slot.candidate.quota;
1579
- }
1580
- }
1581
- return void 0;
1582
- }
1583
- function normalizeCandidateLineBreaks(content) {
1584
- const normalized = content.replace(/\r\n?|[\u2028\u2029]/g, "\n");
1585
- return normalized.split("\n")[0]?.trim() || "";
1586
- }
1587
- function normalizeCandidateContentForDisplay(content) {
1588
- const line = normalizeCandidateLineBreaks(content);
1589
- if (!line) {
1590
- return "";
1591
- }
1592
- const collapsed = line.replace(/\s+/g, " ").trim();
1593
- if (!collapsed) {
1594
- return "";
1595
- }
1596
- if (collapsed.length % 2 === 0) {
1597
- const half = collapsed.length / 2;
1598
- if (collapsed.slice(0, half) === collapsed.slice(half)) {
1599
- return collapsed.slice(0, half);
1600
- }
1601
- }
1602
- const words = collapsed.split(" ");
1603
- if (words.length >= 6 && words.length % 2 === 0) {
1604
- const half = words.length / 2;
1605
- const firstHalf = words.slice(0, half).join(" ");
1606
- const secondHalf = words.slice(half).join(" ");
1607
- if (firstHalf === secondHalf) {
1608
- return firstHalf;
1609
- }
1610
- }
1611
- return collapsed;
1612
- }
1613
- function hasReadySlot(slots) {
1614
- return getReadyCount(slots) > 0;
1615
- }
1616
- function selectNearestReady(slots, startIndex, direction) {
1617
- for (let index = startIndex + direction; index >= 0 && index < slots.length; index += direction) {
1618
- if (slots[index]?.status === "ready") {
1619
- return index;
1933
+ return;
1620
1934
  }
1621
- }
1622
- return startIndex;
1623
- }
1624
- function getSelectedCandidate(slots, selectedIndex) {
1625
- const slot = slots[selectedIndex];
1626
- return slot?.status === "ready" ? slot.candidate : void 0;
1935
+ const totalHeight = pendingHeight + committedHeight;
1936
+ if (totalHeight > 0) {
1937
+ readline2.moveCursor(output, 0, -totalHeight);
1938
+ readline2.cursorTo(output, 0);
1939
+ readline2.clearScreenDown(output);
1940
+ }
1941
+ pendingHeight = 0;
1942
+ committedHeight = 0;
1943
+ };
1944
+ const reset = () => {
1945
+ pendingHeight = 0;
1946
+ committedHeight = 0;
1947
+ };
1948
+ return {
1949
+ render,
1950
+ renderSelectorFrame,
1951
+ flush,
1952
+ clearAll,
1953
+ reset
1954
+ };
1627
1955
  }
1628
1956
 
1957
+ // lib/selector.ts
1958
+ import { spawn } from "child_process";
1959
+ import { mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
1960
+ import { tmpdir } from "os";
1961
+ import { join as join4 } from "path";
1962
+ import * as readline3 from "readline";
1963
+
1629
1964
  // ../shared/terminal-selector-flow.ts
1630
1965
  function cloneSlots(slots) {
1631
1966
  return slots.map((slot) => {
@@ -1910,6 +2245,20 @@ function transitionSelectorFlow(context, event) {
1910
2245
  }
1911
2246
  break;
1912
2247
  }
2248
+ case "ESCALATE": {
2249
+ if (next.mode !== "list" || !hasReadySlot(next.slots)) break;
2250
+ const selectedCandidate = getSelectedCandidate(
2251
+ next.slots,
2252
+ next.selectedIndex
2253
+ );
2254
+ const selected = selectedCandidate ? next.editedSelections.get(selectedCandidate.slotId) ?? selectedCandidate.content : void 0;
2255
+ result = {
2256
+ action: "escalate",
2257
+ selected,
2258
+ selectedCandidate
2259
+ };
2260
+ break;
2261
+ }
1913
2262
  case "QUIT": {
1914
2263
  if (next.mode === "prompt") {
1915
2264
  next.mode = "list";
@@ -1941,208 +2290,6 @@ function transitionSelectorFlow(context, event) {
1941
2290
  };
1942
2291
  }
1943
2292
 
1944
- // ../shared/terminal-selector-view-model.ts
1945
- var DEFAULT_SPINNER_FRAMES = [
1946
- "\u280B",
1947
- "\u2819",
1948
- "\u2839",
1949
- "\u2838",
1950
- "\u283C",
1951
- "\u2834",
1952
- "\u2826",
1953
- "\u2827",
1954
- "\u2807",
1955
- "\u280F"
1956
- ];
1957
- var HINT_ACTION_ORDER = [
1958
- "navigate",
1959
- "confirm",
1960
- "clickConfirm",
1961
- "edit",
1962
- "refine",
1963
- "quit"
1964
- ];
1965
- var DEFAULT_SELECTOR_COPY = {
1966
- runningLabel: "Generating commit messages...",
1967
- selectionLabel: "Select a commit message",
1968
- itemLabelSingular: "commit message",
1969
- itemLabelPlural: "commit messages"
1970
- };
1971
- var DEFAULT_SELECTOR_CAPABILITIES = {
1972
- clickConfirm: false,
1973
- edit: false,
1974
- refine: false
1975
- };
1976
- function formatDuration(ms) {
1977
- const safeMs = Math.max(0, Math.round(ms));
1978
- if (safeMs < 1e3) {
1979
- return `${safeMs}ms`;
1980
- }
1981
- const seconds = (safeMs / 1e3).toFixed(1).replace(/\.0$/, "");
1982
- return `${seconds}s`;
1983
- }
1984
- function resolveRenderMode(inputState) {
1985
- return inputState.mode === "prompt" ? "prompt" : "list";
1986
- }
1987
- function resolvePromptLineLabel(kind) {
1988
- return kind === "edit" ? "Edit mode" : "\u2192 Refine mode";
1989
- }
1990
- function buildPromptViewModel(input, viewModel) {
1991
- const state = input.state;
1992
- const promptKind = state.promptKind ?? "refine";
1993
- const targetIndex = state.promptTargetIndex ?? state.selectedIndex;
1994
- const targetSlot = state.slots[targetIndex];
1995
- const targetText = targetSlot?.status === "ready" ? normalizeCandidateContentForDisplay(targetSlot.candidate.content) : "(no selection)";
1996
- const totalCost = getTotalCost(state.slots);
1997
- const costLineLabel = totalCost > 0 ? formatTotalCostLabel(totalCost) : "$0.000000";
1998
- const generatedCostSuffix = viewModel.header.totalCostLabel ? ` (total: ${viewModel.header.totalCostLabel})` : "";
1999
- const generatedLine = `${viewModel.header.generatedLabel}${generatedCostSuffix}`;
2000
- const modeLine = resolvePromptLineLabel(promptKind);
2001
- const targetLineLabel = `Target [${Math.min(state.totalSlots, targetIndex + 1)}]:`;
2002
- const duration = formatDuration(input.nowMs - state.createdAtMs);
2003
- const costLine = `Cost/Time (current): ${costLineLabel} / ${duration}`;
2004
- const selectedLine = promptKind === "edit" ? `Selected: ${targetText}` : void 0;
2005
- return {
2006
- kind: promptKind,
2007
- generatedLine,
2008
- selectedLine,
2009
- modeLine,
2010
- targetLineLabel,
2011
- targetText,
2012
- targetIndex,
2013
- costLine,
2014
- questionLine: "?"
2015
- };
2016
- }
2017
- function normalizeHintActions(actions) {
2018
- const set = new Set(actions);
2019
- const ordered = [];
2020
- for (const action of HINT_ACTION_ORDER) {
2021
- if (set.has(action)) {
2022
- ordered.push(action);
2023
- }
2024
- }
2025
- return ordered;
2026
- }
2027
- function resolveHintActions(input) {
2028
- const actions = ["navigate", "confirm", "quit"];
2029
- if (input.capabilities.clickConfirm) {
2030
- actions.push("clickConfirm");
2031
- }
2032
- if (input.capabilities.edit) {
2033
- actions.push("edit");
2034
- }
2035
- if (input.capabilities.refine) {
2036
- actions.push("refine");
2037
- }
2038
- return normalizeHintActions(actions);
2039
- }
2040
- function formatReadyMeta(slot) {
2041
- const { candidate } = slot;
2042
- if (!candidate.model) {
2043
- return "";
2044
- }
2045
- const formattedModel = formatModelName(candidate.model);
2046
- const formattedDuration = candidate.generationMs == null ? "" : ` ${formatDuration(candidate.generationMs)}`;
2047
- if (candidate.cost != null) {
2048
- return `${formattedModel} ${formatCost(candidate.cost)}${formattedDuration}`;
2049
- }
2050
- return `${formattedModel}${formattedDuration}`;
2051
- }
2052
- function createSlotViewModel(slot, index, selectedIndex) {
2053
- if (slot.status === "pending") {
2054
- return {
2055
- status: "pending",
2056
- selected: false,
2057
- radio: "\u25CB",
2058
- title: "Generating...",
2059
- meta: slot.model ? formatModelName(slot.model) : void 0,
2060
- muted: true
2061
- };
2062
- }
2063
- if (slot.status === "error") {
2064
- return {
2065
- status: "error",
2066
- selected: false,
2067
- radio: "\u25CB",
2068
- title: slot.content,
2069
- muted: true
2070
- };
2071
- }
2072
- const selected = index === selectedIndex;
2073
- const title = slot.candidate.content.split("\n")[0]?.trim() || "";
2074
- const meta = formatReadyMeta(slot);
2075
- return {
2076
- status: "ready",
2077
- selected,
2078
- radio: selected ? "\u25CF" : "\u25CB",
2079
- title,
2080
- meta: meta || void 0,
2081
- muted: !selected
2082
- };
2083
- }
2084
- function resolveEditedSummary(input) {
2085
- const selectedSlot = input.state.slots[input.state.selectedIndex];
2086
- if (selectedSlot?.status !== "ready") {
2087
- return void 0;
2088
- }
2089
- const edited = input.editedSelections?.get(selectedSlot.candidate.slotId);
2090
- if (!edited) {
2091
- return void 0;
2092
- }
2093
- return edited.split("\n")[0]?.slice(0, 120) || "";
2094
- }
2095
- function buildSelectorViewModel(input) {
2096
- const spinnerFrames = input.spinnerFrames ?? DEFAULT_SPINNER_FRAMES;
2097
- const copy = { ...DEFAULT_SELECTOR_COPY, ...input.copy };
2098
- const capabilities = {
2099
- ...DEFAULT_SELECTOR_CAPABILITIES,
2100
- ...input.capabilities
2101
- };
2102
- const readyCount = getReadyCount(input.state.slots);
2103
- const totalCost = getTotalCost(input.state.slots);
2104
- const frame = Math.floor(input.nowMs / 80) % spinnerFrames.length;
2105
- const generatedLabel = readyCount === 1 ? `Generated 1 ${copy.itemLabelSingular}` : `Generated ${readyCount} ${copy.itemLabelPlural}`;
2106
- const hintActions = resolveHintActions({
2107
- readyCount,
2108
- capabilities
2109
- });
2110
- return {
2111
- mode: resolveRenderMode(input.state),
2112
- header: {
2113
- mode: input.state.isGenerating ? "running" : "done",
2114
- spinner: spinnerFrames[frame],
2115
- progress: `${readyCount}/${input.state.totalSlots}`,
2116
- totalCostLabel: totalCost > 0 ? formatTotalCostLabel(totalCost) : void 0,
2117
- runningLabel: copy.runningLabel,
2118
- generatedLabel
2119
- },
2120
- hint: {
2121
- kind: readyCount > 0 ? "ready" : "empty",
2122
- selectionLabel: readyCount > 0 ? copy.selectionLabel : void 0,
2123
- actions: hintActions
2124
- },
2125
- slots: input.state.slots.map(
2126
- (slot, index) => createSlotViewModel(slot, index, input.state.selectedIndex)
2127
- ),
2128
- editedSummary: resolveEditedSummary(input)
2129
- };
2130
- }
2131
- function selectorRenderFrame(input) {
2132
- const viewModel = buildSelectorViewModel(input);
2133
- if (viewModel.mode === "prompt") {
2134
- return {
2135
- mode: "prompt",
2136
- viewModel,
2137
- prompt: buildPromptViewModel(input, viewModel)
2138
- };
2139
- }
2140
- return {
2141
- mode: "list",
2142
- viewModel
2143
- };
2144
- }
2145
-
2146
2293
  // lib/line-editor.ts
2147
2294
  import stringWidth from "string-width";
2148
2295
  var WORD_SEPARATORS = "`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?";
@@ -2805,6 +2952,7 @@ var selectorRenderCopy = {
2805
2952
  var selectorRenderCapabilities = {
2806
2953
  edit: true,
2807
2954
  refine: true,
2955
+ escalate: true,
2808
2956
  clickConfirm: false
2809
2957
  };
2810
2958
  function renderError(error, slotsLength, output) {
@@ -2870,6 +3018,7 @@ async function selectCandidate(options) {
2870
3018
  initialListMode = "initial",
2871
3019
  initialGuideHint,
2872
3020
  inlineEditPrompt = false,
3021
+ isEscalation = false,
2873
3022
  io
2874
3023
  } = options;
2875
3024
  const abortController = new AbortController();
@@ -2929,6 +3078,12 @@ async function selectCandidate(options) {
2929
3078
  model: models?.[i]
2930
3079
  })
2931
3080
  );
3081
+ const renderCopy = isEscalation ? {
3082
+ ...selectorRenderCopy,
3083
+ runningLabel: "Generating commit messages with escalate models...",
3084
+ itemLabelPlural: "commit messages with escalate models",
3085
+ itemLabelSingular: "commit message with escalate models"
3086
+ } : selectorRenderCopy;
2932
3087
  return new Promise((resolve3) => {
2933
3088
  let resolved = false;
2934
3089
  const resolveOnce = (result) => {
@@ -2973,7 +3128,7 @@ async function selectCandidate(options) {
2973
3128
  },
2974
3129
  nowMs: Date.now(),
2975
3130
  spinnerFrames: SPINNER_FRAMES,
2976
- copy: selectorRenderCopy,
3131
+ copy: renderCopy,
2977
3132
  capabilities: selectorRenderCapabilities
2978
3133
  });
2979
3134
  renderer.render(renderSelectorTextFromRenderFrame(frame));
@@ -2989,7 +3144,7 @@ async function selectCandidate(options) {
2989
3144
  },
2990
3145
  nowMs: Date.now(),
2991
3146
  spinnerFrames: SPINNER_FRAMES,
2992
- copy: selectorRenderCopy,
3147
+ copy: renderCopy,
2993
3148
  capabilities: selectorRenderCapabilities
2994
3149
  });
2995
3150
  const selected = result.selectedCandidate?.content ?? result.selected ?? "";
@@ -3104,6 +3259,28 @@ async function selectCandidate(options) {
3104
3259
  cleanup(false);
3105
3260
  return;
3106
3261
  }
3262
+ if (result.action === "escalate") {
3263
+ const frame = selectorRenderFrame({
3264
+ state: {
3265
+ ...context,
3266
+ mode: context.mode,
3267
+ promptKind: context.promptKind,
3268
+ promptTargetIndex: context.promptTargetIndex
3269
+ },
3270
+ nowMs: Date.now(),
3271
+ spinnerFrames: SPINNER_FRAMES,
3272
+ copy: renderCopy,
3273
+ capabilities: selectorRenderCapabilities
3274
+ });
3275
+ const costSuffix = frame.viewModel.header.totalCostLabel ? ` (total: ${frame.viewModel.header.totalCostLabel})` : "";
3276
+ const generatedLine = `${frame.viewModel.header.generatedLabel}${costSuffix}`;
3277
+ renderer.clearAll();
3278
+ ttyWriter.write(`${ui.success(generatedLine)}
3279
+ `);
3280
+ resolveOnce(result);
3281
+ cleanup(false);
3282
+ return;
3283
+ }
3107
3284
  if (result.action === "abort") {
3108
3285
  resolveOnce(result);
3109
3286
  cleanup(false);
@@ -3191,7 +3368,7 @@ async function selectCandidate(options) {
3191
3368
  };
3192
3369
  const openRefinePrompt = async () => {
3193
3370
  await withPromptSuspended(async () => {
3194
- let lastRefineLineRow = 0;
3371
+ let lastCursorRow = 0;
3195
3372
  const guide = await editLine({
3196
3373
  input: ttyReader,
3197
3374
  output: ttyWriter,
@@ -3199,65 +3376,46 @@ async function selectCandidate(options) {
3199
3376
  initialValue: "",
3200
3377
  finalizeMode: "none",
3201
3378
  onRender: ({ buffer }) => {
3202
- if (lastRefineLineRow > 0) {
3203
- readline3.moveCursor(ttyWriter, 0, -lastRefineLineRow);
3379
+ if (lastCursorRow > 0) {
3380
+ readline3.moveCursor(ttyWriter, 0, -lastCursorRow);
3204
3381
  }
3205
3382
  readline3.cursorTo(ttyWriter, 0);
3206
3383
  readline3.clearScreenDown(ttyWriter);
3207
3384
  const frame = selectorRenderFrame({
3208
3385
  state: {
3209
3386
  ...context,
3210
- mode: "list",
3387
+ mode: context.mode,
3211
3388
  promptKind: context.promptKind,
3212
3389
  promptTargetIndex: context.promptTargetIndex
3213
3390
  },
3214
3391
  nowMs: Date.now(),
3215
3392
  spinnerFrames: SPINNER_FRAMES,
3216
- copy: selectorRenderCopy,
3217
- capabilities: selectorRenderCapabilities
3393
+ copy: renderCopy,
3394
+ capabilities: selectorRenderCapabilities,
3395
+ bufferText: buffer.getText()
3218
3396
  });
3219
- const lines = [];
3220
- const vm = frame.viewModel;
3221
- const costSuffix = vm.header.totalCostLabel ? ` (total: ${vm.header.totalCostLabel})` : "";
3222
- lines.push(ui.success(`${vm.header.generatedLabel}${costSuffix}`));
3223
- lines.push("");
3224
- for (const slot of vm.slots) {
3225
- const radio = slot.selected ? `${theme.success}\u25CF${theme.reset}` : `${theme.dim}\u25CB${theme.reset}`;
3226
- const titleColor = slot.selected ? theme.primary : theme.dim;
3227
- const titleFont = slot.selected ? theme.bold : "";
3228
- lines.push(
3229
- ` ${radio} ${titleColor}${titleFont}${slot.title}${theme.reset}`
3230
- );
3231
- if (slot.meta) {
3232
- const metaColor = slot.selected ? theme.primary : theme.dim;
3233
- lines.push(` ${metaColor}${slot.meta}${theme.reset}`);
3234
- }
3235
- lines.push("");
3236
- }
3237
- const refineLineIndex = lines.length;
3238
- const refinePrefix = ` ${theme.primary}Refine:${theme.reset} `;
3239
- lines.push(`${refinePrefix}${buffer.getText()}`);
3240
- lines.push(
3241
- ` ${theme.dim}e.g. more formal / shorter / in Japanese${theme.reset}`
3397
+ const prompt = frame.prompt;
3398
+ if (!prompt) return;
3399
+ const rendered = renderSelectorTextFromRenderFrame(frame).replace(
3400
+ /\n$/,
3401
+ ""
3242
3402
  );
3243
- lines.push("");
3244
- lines.push(` ${ui.hint("enter refine | esc back to select")}`);
3245
- ttyWriter.write(lines.join("\n"));
3246
- const moveUp = lines.length - 1 - refineLineIndex;
3403
+ ttyWriter.write(rendered);
3404
+ const moveUp = prompt.promptLineCount - 1 - prompt.promptInputLineIndex;
3247
3405
  if (moveUp > 0) {
3248
3406
  readline3.moveCursor(ttyWriter, 0, -moveUp);
3249
3407
  }
3250
3408
  readline3.cursorTo(ttyWriter, 0);
3251
- const prefixWidth = 10;
3409
+ const prefixWidth = prompt.promptInputPrefixWidth;
3252
3410
  const col = prefixWidth + buffer.getDisplayCursor();
3253
3411
  if (col > 0) {
3254
3412
  readline3.moveCursor(ttyWriter, col, 0);
3255
3413
  }
3256
- lastRefineLineRow = refineLineIndex;
3414
+ lastCursorRow = prompt.promptInputLineIndex;
3257
3415
  }
3258
3416
  });
3259
- if (lastRefineLineRow > 0) {
3260
- readline3.moveCursor(ttyWriter, 0, -lastRefineLineRow);
3417
+ if (lastCursorRow > 0) {
3418
+ readline3.moveCursor(ttyWriter, 0, -lastCursorRow);
3261
3419
  }
3262
3420
  readline3.cursorTo(ttyWriter, 0);
3263
3421
  readline3.clearScreenDown(ttyWriter);
@@ -3286,7 +3444,7 @@ async function selectCandidate(options) {
3286
3444
  }
3287
3445
  if (inlineEditPrompt) {
3288
3446
  await withPromptSuspended(async () => {
3289
- let lastEditLineRow = 0;
3447
+ let lastCursorRow = 0;
3290
3448
  const edited = await editLine({
3291
3449
  input: ttyReader,
3292
3450
  output: ttyWriter,
@@ -3294,64 +3452,45 @@ async function selectCandidate(options) {
3294
3452
  initialValue: selected.content,
3295
3453
  finalizeMode: "none",
3296
3454
  onRender: ({ buffer }) => {
3297
- if (lastEditLineRow > 0) {
3298
- readline3.moveCursor(ttyWriter, 0, -lastEditLineRow);
3455
+ if (lastCursorRow > 0) {
3456
+ readline3.moveCursor(ttyWriter, 0, -lastCursorRow);
3299
3457
  }
3300
3458
  readline3.cursorTo(ttyWriter, 0);
3301
3459
  readline3.clearScreenDown(ttyWriter);
3302
3460
  const frame = selectorRenderFrame({
3303
3461
  state: {
3304
3462
  ...context,
3305
- mode: "list",
3463
+ mode: context.mode,
3306
3464
  promptKind: context.promptKind,
3307
3465
  promptTargetIndex: context.promptTargetIndex
3308
3466
  },
3309
3467
  nowMs: Date.now(),
3310
3468
  spinnerFrames: SPINNER_FRAMES,
3311
- copy: selectorRenderCopy,
3312
- capabilities: selectorRenderCapabilities
3469
+ copy: renderCopy,
3470
+ capabilities: selectorRenderCapabilities,
3471
+ bufferText: buffer.getText()
3313
3472
  });
3314
- const lines = [];
3315
- const vm = frame.viewModel;
3316
- const costSuffix = vm.header.totalCostLabel ? ` (total: ${vm.header.totalCostLabel})` : "";
3317
- lines.push(
3318
- ui.success(`${vm.header.generatedLabel}${costSuffix}`)
3473
+ const prompt = frame.prompt;
3474
+ const rendered = renderSelectorTextFromRenderFrame(frame).replace(
3475
+ /\n$/,
3476
+ ""
3319
3477
  );
3320
- lines.push("");
3321
- let editLineIndex = 0;
3322
- for (const slot of vm.slots) {
3323
- if (slot.selected) {
3324
- editLineIndex = lines.length;
3325
- lines.push(
3326
- ` ${theme.success}>${theme.reset} ${buffer.getText()}`
3327
- );
3328
- } else {
3329
- lines.push(
3330
- ` ${theme.dim}\u25CB${theme.reset} ${theme.dim}${slot.title}${theme.reset}`
3331
- );
3332
- }
3333
- if (slot.meta) {
3334
- const metaColor = slot.selected ? theme.primary : theme.dim;
3335
- lines.push(` ${metaColor}${slot.meta}${theme.reset}`);
3336
- }
3337
- lines.push("");
3338
- }
3339
- lines.push(` ${ui.hint("enter apply | esc back to select")}`);
3340
- ttyWriter.write(lines.join("\n"));
3341
- const moveUp = lines.length - 1 - editLineIndex;
3478
+ ttyWriter.write(rendered);
3479
+ if (!prompt) return;
3480
+ const moveUp = prompt.promptLineCount - 1 - prompt.promptInputLineIndex;
3342
3481
  if (moveUp > 0) {
3343
3482
  readline3.moveCursor(ttyWriter, 0, -moveUp);
3344
3483
  }
3345
3484
  readline3.cursorTo(ttyWriter, 0);
3346
- const col = 4 + buffer.getDisplayCursor();
3485
+ const col = prompt.promptInputPrefixWidth + buffer.getDisplayCursor();
3347
3486
  if (col > 0) {
3348
3487
  readline3.moveCursor(ttyWriter, col, 0);
3349
3488
  }
3350
- lastEditLineRow = editLineIndex;
3489
+ lastCursorRow = prompt.promptInputLineIndex;
3351
3490
  }
3352
3491
  });
3353
- if (lastEditLineRow > 0) {
3354
- readline3.moveCursor(ttyWriter, 0, -lastEditLineRow);
3492
+ if (lastCursorRow > 0) {
3493
+ readline3.moveCursor(ttyWriter, 0, -lastCursorRow);
3355
3494
  }
3356
3495
  readline3.cursorTo(ttyWriter, 0);
3357
3496
  readline3.clearScreenDown(ttyWriter);
@@ -3410,7 +3549,12 @@ async function selectCandidate(options) {
3410
3549
  }
3411
3550
  return;
3412
3551
  }
3413
- if (key.name === "e") {
3552
+ if (key.name === "e" && key.shift) {
3553
+ if (!hasReadySlot(context.slots)) return;
3554
+ applyResult(transitionSelectorFlow(context, { type: "ESCALATE" }));
3555
+ return;
3556
+ }
3557
+ if (key.name === "e" && !key.shift) {
3414
3558
  if (!hasReadySlot(context.slots)) return;
3415
3559
  const transition = transitionSelectorFlow(context, {
3416
3560
  type: "OPEN_PROMPT",
@@ -3726,7 +3870,9 @@ async function commit(args2) {
3726
3870
  args: ["commit", ...args2],
3727
3871
  apiPath: "/v1/commit-message/stream"
3728
3872
  });
3729
- const models = resolveModels(options.cliModels);
3873
+ const baseModels = resolveModels(options.cliModels);
3874
+ const escalationModels = resolveEscalationModels();
3875
+ let models = baseModels;
3730
3876
  const diff = getStagedDiff();
3731
3877
  if (!diff.trim()) {
3732
3878
  console.error(
@@ -3793,6 +3939,7 @@ async function commit(args2) {
3793
3939
  };
3794
3940
  const stats = getGitStagedStats();
3795
3941
  console.log(ui.success(`Found ${formatDiffStats(stats)}`));
3942
+ let isEscalation = false;
3796
3943
  while (true) {
3797
3944
  const isRefineAttempt = refineMessage !== void 0;
3798
3945
  const { commandExecutionSignal, commandExecutionPromise, cliSessionId } = startCommandExecutionSession(isRefineAttempt);
@@ -3815,7 +3962,8 @@ async function commit(args2) {
3815
3962
  abortSignal: commandExecutionSignal,
3816
3963
  models,
3817
3964
  inlineEditPrompt: true,
3818
- initialGuideHint: guideHint
3965
+ initialGuideHint: guideHint,
3966
+ isEscalation
3819
3967
  });
3820
3968
  if (result.action === "abort") {
3821
3969
  if (result.error instanceof InvalidModelError) {
@@ -3827,6 +3975,14 @@ async function commit(args2) {
3827
3975
  console.error("Aborting commit.");
3828
3976
  process.exit(1);
3829
3977
  }
3978
+ if (result.action === "escalate") {
3979
+ console.log(ui.hint(" -> Escalate"));
3980
+ models = escalationModels;
3981
+ guideHint = void 0;
3982
+ refineMessage = void 0;
3983
+ isEscalation = true;
3984
+ continue;
3985
+ }
3830
3986
  if (result.action === "refine") {
3831
3987
  guideHint = result.guide;
3832
3988
  refineMessage = result.selected ?? result.selectedCandidate?.content;