@cryer/star-cli 0.2.1 → 0.2.2

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/main.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  SKIP_DIRS,
5
5
  TodoStore,
6
6
  WORKING_DIFF_MAX_LINES,
7
+ appendUserMemory,
7
8
  bashTool,
8
9
  buildAllowRule,
9
10
  buildCommitPrompt,
@@ -12,6 +13,7 @@ import {
12
13
  collectWorkingDiff,
13
14
  compactMessages,
14
15
  createDefaultRegistry,
16
+ createIgnorePredicate,
15
17
  defaultTaskManager,
16
18
  discoverSkills,
17
19
  estimateTokens,
@@ -34,8 +36,9 @@ import {
34
36
  sessionsDir,
35
37
  starHome,
36
38
  summarizeMessages,
37
- undoTurnSnapshots
38
- } from "./chunk-MJ2O7NWI.js";
39
+ undoTurnSnapshots,
40
+ userMemoryPath
41
+ } from "./chunk-GN236FD4.js";
39
42
 
40
43
  // src/main.tsx
41
44
  import { Command } from "commander";
@@ -269,14 +272,17 @@ var UsageTracker = class {
269
272
  };
270
273
 
271
274
  // src/cli/repl.tsx
272
- import { Box as Box9, Text as Text10, render, useApp, useInput as useInput5 } from "ink";
273
- import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
275
+ import { Box as Box10, Text as Text11, render, useApp, useInput as useInput6 } from "ink";
276
+ import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState3 } from "react";
274
277
 
275
278
  // src/config/save.ts
276
279
  import fs from "fs";
277
280
  import path2 from "path";
278
281
  import { parse, stringify } from "smol-toml";
279
282
  async function addAllowRule(rule) {
283
+ return addPermissionRule("allow", rule);
284
+ }
285
+ async function addPermissionRule(kind, rule) {
280
286
  const filePath = globalConfigPath();
281
287
  let raw = {};
282
288
  try {
@@ -286,9 +292,9 @@ async function addAllowRule(rule) {
286
292
  if (error.code !== "ENOENT") throw error;
287
293
  }
288
294
  const permissions = typeof raw.permissions === "object" && raw.permissions !== null ? raw.permissions : {};
289
- const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
290
- if (allow.includes(rule)) return false;
291
- permissions.allow = [...allow, rule];
295
+ const rules = Array.isArray(permissions[kind]) ? permissions[kind] : [];
296
+ if (rules.includes(rule)) return false;
297
+ permissions[kind] = [...rules, rule];
292
298
  raw.permissions = permissions;
293
299
  await fs.promises.mkdir(path2.dirname(filePath), { recursive: true });
294
300
  await fs.promises.writeFile(filePath, stringify(raw), "utf8");
@@ -332,7 +338,8 @@ var ModelConfigSchema = z.object({
332
338
  completionPrice: z.number().nonnegative().optional()
333
339
  });
334
340
  var PermissionsConfigSchema = z.object({
335
- allow: z.array(z.string()).default([])
341
+ allow: z.array(z.string()).default([]),
342
+ deny: z.array(z.string()).default([])
336
343
  });
337
344
  var HookConfigSchema = z.object({
338
345
  event: z.enum(["PreToolUse", "PostToolUse", "Stop"]),
@@ -370,7 +377,7 @@ var ConfigSchema = z.object({
370
377
  // STAR_NO_NOTIFY=1 disables without touching the config.
371
378
  notifyBell: z.boolean().default(true),
372
379
  notifyBellThresholdSec: z.number().positive().default(10),
373
- permissions: PermissionsConfigSchema.default({ allow: [] }),
380
+ permissions: PermissionsConfigSchema.default({ allow: [], deny: [] }),
374
381
  hooks: z.array(HookConfigSchema).default([])
375
382
  });
376
383
  function contextWindowTokens(config, modelName) {
@@ -1253,6 +1260,61 @@ function stringifyPart(value) {
1253
1260
  return typeof value === "string" ? value : JSON.stringify(value, null, 2);
1254
1261
  }
1255
1262
 
1263
+ // src/cli/commands/builtin.ts
1264
+ import { readFileSync } from "fs";
1265
+
1266
+ // src/session/search.ts
1267
+ var SNIPPET_LENGTH = 100;
1268
+ function flattenMessage(message) {
1269
+ const content = message.content;
1270
+ if (typeof content === "string") return content;
1271
+ if (!Array.isArray(content)) return "";
1272
+ const parts = [];
1273
+ for (const part of content) {
1274
+ if (part.type === "text") {
1275
+ parts.push(part.text);
1276
+ } else if (part.type === "tool-call") {
1277
+ parts.push(JSON.stringify(part.args));
1278
+ } else if (part.type === "tool-result") {
1279
+ parts.push(typeof part.result === "string" ? part.result : JSON.stringify(part.result));
1280
+ }
1281
+ }
1282
+ return parts.join("\n");
1283
+ }
1284
+ function makeSnippet(text, query) {
1285
+ const flat = text.replace(/\s+/g, " ").trim();
1286
+ const index = flat.toLowerCase().indexOf(query.toLowerCase());
1287
+ if (index === -1 || flat.length <= SNIPPET_LENGTH) return flat.slice(0, SNIPPET_LENGTH);
1288
+ const side = Math.max(0, Math.floor((SNIPPET_LENGTH - query.length) / 2));
1289
+ let start = Math.max(0, index - side);
1290
+ const end = Math.min(flat.length, start + SNIPPET_LENGTH);
1291
+ start = Math.max(0, end - SNIPPET_LENGTH);
1292
+ return `${start > 0 ? "\u2026" : ""}${flat.slice(start, end)}${end < flat.length ? "\u2026" : ""}`;
1293
+ }
1294
+ async function searchSessions(query, limit = 10) {
1295
+ const hits = [];
1296
+ const needle = query.toLowerCase();
1297
+ for (const meta of await SessionStore.list()) {
1298
+ if (hits.length >= limit) break;
1299
+ const store = await SessionStore.open(meta.id);
1300
+ if (!store) continue;
1301
+ const text = (await store.messages()).map(flattenMessage).filter((part) => part.length > 0).join("\n");
1302
+ if (text.toLowerCase().includes(needle)) {
1303
+ hits.push({ meta, snippet: makeSnippet(text, query) });
1304
+ }
1305
+ }
1306
+ return hits;
1307
+ }
1308
+ function formatSearchResults(hits, query) {
1309
+ if (hits.length === 0) return `No sessions matching "${query}".`;
1310
+ const lines = hits.map(({ meta, snippet }) => {
1311
+ const title = meta.title || "(untitled)";
1312
+ return `${shortSessionId(meta.id)} ${title} (${relativeTime(meta.updatedAt)})
1313
+ ${snippet}`;
1314
+ });
1315
+ return [...lines, "Resume with /resume <id>"].join("\n");
1316
+ }
1317
+
1256
1318
  // src/cli/diff-preview.ts
1257
1319
  import { readFile as readFile3 } from "fs/promises";
1258
1320
  import path10 from "path";
@@ -1440,31 +1502,41 @@ async function generateDiffPreview(toolName, args, cwd) {
1440
1502
 
1441
1503
  // src/cli/commands/builtin.ts
1442
1504
  function registerBuiltinCommands(registry) {
1443
- registry.register({
1505
+ const register = (command) => registry.register(command);
1506
+ register({
1444
1507
  name: "help",
1445
1508
  description: "List available commands",
1446
1509
  usage: "/help",
1510
+ category: "General",
1447
1511
  run(_args, ctx) {
1448
- const lines = registry.list().map((cmd) => {
1512
+ const groups = /* @__PURE__ */ new Map();
1513
+ for (const cmd of registry.list()) {
1514
+ const category = cmd.category ?? "Other";
1449
1515
  const usage = cmd.usage ?? `/${cmd.name}`;
1450
- return `${usage} - ${cmd.description}`;
1451
- });
1516
+ const lines = groups.get(category) ?? [];
1517
+ lines.push(` ${usage} - ${cmd.description}`);
1518
+ groups.set(category, lines);
1519
+ }
1520
+ const sections = [...groups.entries()].map(([name, lines]) => [name, ...lines].join("\n"));
1452
1521
  ctx.addSystemMessage(`Available commands:
1453
- ${lines.join("\n")}`);
1522
+
1523
+ ${sections.join("\n\n")}`);
1454
1524
  }
1455
1525
  });
1456
- registry.register({
1526
+ register({
1457
1527
  name: "clear",
1458
1528
  description: "Clear message history",
1459
1529
  usage: "/clear",
1530
+ category: "General",
1460
1531
  run(_args, ctx) {
1461
1532
  ctx.clearMessages();
1462
1533
  }
1463
1534
  });
1464
- registry.register({
1535
+ register({
1465
1536
  name: "new",
1466
1537
  description: "Start a new session with a clean context",
1467
1538
  usage: "/new",
1539
+ category: "Sessions",
1468
1540
  async run(_args, ctx) {
1469
1541
  if (!ctx.newSession) {
1470
1542
  ctx.addSystemMessage("/new: this context cannot start a new session.");
@@ -1473,10 +1545,11 @@ ${lines.join("\n")}`);
1473
1545
  ctx.addSystemMessage(await ctx.newSession());
1474
1546
  }
1475
1547
  });
1476
- registry.register({
1548
+ register({
1477
1549
  name: "clear-sessions",
1478
1550
  description: "Delete stored sessions: this directory by default, every session with --all (the current session is kept)",
1479
1551
  usage: "/clear-sessions [--all]",
1552
+ category: "Sessions",
1480
1553
  async run(args, ctx) {
1481
1554
  const arg = args.trim();
1482
1555
  if (arg !== "" && arg !== "--all") {
@@ -1492,157 +1565,242 @@ ${lines.join("\n")}`);
1492
1565
  ctx.addSystemMessage(await ctx.clearSessions(arg === "--all"));
1493
1566
  }
1494
1567
  });
1495
- registry.register({
1568
+ register({
1496
1569
  name: "exit",
1497
1570
  description: "Exit the application",
1498
1571
  usage: "/exit",
1572
+ category: "General",
1499
1573
  run(_args, ctx) {
1500
1574
  ctx.exit();
1501
1575
  }
1502
1576
  });
1503
- registry.register({
1577
+ register({
1504
1578
  name: "q",
1505
1579
  description: "Exit the application (alias of /exit)",
1506
1580
  usage: "/q",
1581
+ category: "General",
1507
1582
  run(_args, ctx) {
1508
1583
  ctx.exit();
1509
1584
  }
1510
1585
  });
1511
- registry.register({
1586
+ register({
1512
1587
  name: "model",
1513
- description: "List available models or switch the current model",
1588
+ description: "Switch the current model (opens a picker when no name is given)",
1514
1589
  usage: "/model [name]",
1590
+ category: "Settings",
1515
1591
  async run(args, ctx) {
1516
1592
  if (!args) {
1517
- ctx.addSystemMessage(ctx.listModels());
1593
+ ctx.addSystemMessage(await ctx.pickModel());
1518
1594
  } else {
1519
1595
  ctx.addSystemMessage(await ctx.switchModel(args));
1520
1596
  }
1521
1597
  }
1522
1598
  });
1523
- registry.register({
1599
+ register({
1524
1600
  name: "resume",
1525
- description: "List sessions or resume a session by id",
1601
+ description: "Resume a session by id, or pick one from a list (--all for every directory)",
1526
1602
  usage: "/resume [sessionId | --all]",
1603
+ category: "Sessions",
1527
1604
  async run(args, ctx) {
1528
1605
  const arg = args.trim();
1529
1606
  if (arg === "--all") {
1530
- ctx.addSystemMessage(await ctx.listSessions(true));
1607
+ ctx.addSystemMessage(await ctx.pickSession(true));
1531
1608
  } else if (!arg) {
1532
- ctx.addSystemMessage(await ctx.listSessions());
1609
+ ctx.addSystemMessage(await ctx.pickSession(false));
1533
1610
  } else {
1534
1611
  ctx.addSystemMessage(await ctx.resumeSession(arg));
1535
1612
  }
1536
1613
  }
1537
1614
  });
1538
- registry.register({
1615
+ register({
1616
+ name: "fork",
1617
+ description: "Fork the current session into a new one and switch to it",
1618
+ usage: "/fork",
1619
+ category: "Sessions",
1620
+ async run(_args, ctx) {
1621
+ ctx.addSystemMessage(await ctx.forkSession());
1622
+ }
1623
+ });
1624
+ register({
1625
+ name: "search",
1626
+ description: "Full-text search across all stored sessions",
1627
+ usage: "/search <query>",
1628
+ category: "Sessions",
1629
+ async run(args, ctx) {
1630
+ const query = args.trim();
1631
+ if (!query) {
1632
+ ctx.addSystemMessage("Usage: /search <query>");
1633
+ return;
1634
+ }
1635
+ ctx.addSystemMessage(formatSearchResults(await searchSessions(query), query));
1636
+ }
1637
+ });
1638
+ register({
1539
1639
  name: "todo",
1540
1640
  description: "Show the current todo list",
1541
1641
  usage: "/todo",
1642
+ category: "Info",
1542
1643
  async run(_args, ctx) {
1543
1644
  ctx.addSystemMessage(await ctx.showTodos());
1544
1645
  }
1545
1646
  });
1546
- registry.register({
1647
+ register({
1547
1648
  name: "tasks",
1548
1649
  description: "List background shell tasks",
1549
1650
  usage: "/tasks",
1651
+ category: "Info",
1550
1652
  run(_args, ctx) {
1551
1653
  ctx.addSystemMessage(ctx.listTasks());
1552
1654
  }
1553
1655
  });
1554
- registry.register({
1656
+ register({
1555
1657
  name: "cost",
1556
1658
  description: "Show API token usage for this session",
1557
1659
  usage: "/cost",
1660
+ category: "Info",
1558
1661
  run(_args, ctx) {
1559
1662
  ctx.addSystemMessage(ctx.showUsage());
1560
1663
  }
1561
1664
  });
1562
- registry.register({
1665
+ register({
1563
1666
  name: "usage",
1564
1667
  description: "Show token usage aggregated across all sessions",
1565
1668
  usage: "/usage",
1669
+ category: "Info",
1566
1670
  async run(_args, ctx) {
1567
1671
  ctx.addSystemMessage(await ctx.showGlobalUsage());
1568
1672
  }
1569
1673
  });
1570
- registry.register({
1674
+ register({
1571
1675
  name: "config",
1572
1676
  description: "Show the current configuration",
1573
1677
  usage: "/config",
1678
+ category: "Info",
1574
1679
  run(_args, ctx) {
1575
1680
  ctx.addSystemMessage(ctx.describeConfig());
1576
1681
  }
1577
1682
  });
1578
- registry.register({
1683
+ register({
1579
1684
  name: "compact",
1580
1685
  description: "Compact the conversation history to free up context",
1581
1686
  usage: "/compact",
1687
+ category: "Sessions",
1582
1688
  async run(_args, ctx) {
1583
1689
  ctx.addSystemMessage(await ctx.compactContext());
1584
1690
  }
1585
1691
  });
1586
- registry.register({
1692
+ register({
1587
1693
  name: "export",
1588
1694
  description: "Export the current session to a Markdown file",
1589
1695
  usage: "/export [path]",
1696
+ category: "Sessions",
1590
1697
  async run(args, ctx) {
1591
1698
  ctx.addSystemMessage(await ctx.exportSession(args));
1592
1699
  }
1593
1700
  });
1594
- registry.register({
1701
+ register({
1595
1702
  name: "permission",
1596
- description: "Show or set the global permission mode (ask | auto | readonly | yolo)",
1703
+ description: "Set the permission mode (opens a picker when no mode is given)",
1597
1704
  usage: "/permission [ask|auto|readonly|yolo]",
1705
+ category: "Settings",
1598
1706
  async run(args, ctx) {
1599
- ctx.addSystemMessage(await ctx.permissionMode(args.trim()));
1707
+ const arg = args.trim();
1708
+ if (!arg) {
1709
+ ctx.addSystemMessage(await ctx.pickPermissionMode());
1710
+ } else {
1711
+ ctx.addSystemMessage(await ctx.permissionMode(arg));
1712
+ }
1600
1713
  }
1601
1714
  });
1602
- registry.register({
1715
+ register({
1603
1716
  name: "plan",
1604
1717
  description: "Toggle plan mode: read-only research, then approve the plan before executing",
1605
1718
  usage: "/plan",
1719
+ category: "Settings",
1606
1720
  async run(_args, ctx) {
1607
1721
  ctx.addSystemMessage(await ctx.planMode());
1608
1722
  }
1609
1723
  });
1610
- registry.register({
1724
+ register({
1725
+ name: "memory",
1726
+ description: "Show user memory (MEMORY.md), or add an entry",
1727
+ usage: "/memory [add <text>]",
1728
+ category: "Settings",
1729
+ run(args, ctx) {
1730
+ const arg = args.trim();
1731
+ if (arg === "add" || arg.startsWith("add ")) {
1732
+ const text = arg.slice(3).trim();
1733
+ if (!text) {
1734
+ ctx.addSystemMessage("Usage: /memory add <text>");
1735
+ return;
1736
+ }
1737
+ appendUserMemory(text);
1738
+ ctx.addSystemMessage(`Saved to user memory: - ${text}`);
1739
+ return;
1740
+ }
1741
+ if (arg) {
1742
+ ctx.addSystemMessage(`Unknown argument "${arg}". Usage: /memory [add <text>]`);
1743
+ return;
1744
+ }
1745
+ const file = userMemoryPath();
1746
+ let content = "";
1747
+ try {
1748
+ content = readFileSync(file, "utf8").trim();
1749
+ } catch {
1750
+ content = "";
1751
+ }
1752
+ if (!content) {
1753
+ ctx.addSystemMessage(
1754
+ `No user memory yet (${file}).
1755
+ Add entries with /memory add <text> or ask the agent to remember something.`
1756
+ );
1757
+ return;
1758
+ }
1759
+ ctx.addSystemMessage(`User memory (${file}):
1760
+ ${content}`);
1761
+ }
1762
+ });
1763
+ register({
1611
1764
  name: "undo",
1612
1765
  description: "Undo the last conversation turn: revert its file changes (write_file/edit_file) and retract its messages",
1613
1766
  usage: "/undo",
1767
+ category: "Changes",
1614
1768
  async run(_args, ctx) {
1615
1769
  ctx.addSystemMessage(await ctx.undo());
1616
1770
  }
1617
1771
  });
1618
- registry.register({
1772
+ register({
1619
1773
  name: "rewind",
1620
1774
  description: "List file-change checkpoints, or rewind to just before one: restore files and retract the conversation",
1621
1775
  usage: "/rewind [n]",
1776
+ category: "Changes",
1622
1777
  async run(args, ctx) {
1623
1778
  ctx.addSystemMessage(await ctx.rewind(args.trim()));
1624
1779
  }
1625
1780
  });
1626
- registry.register({
1781
+ register({
1627
1782
  name: "init",
1628
1783
  description: "Generate an AGENTS.md for the current project",
1629
1784
  usage: "/init [force]",
1785
+ category: "Settings",
1630
1786
  async run(args, ctx) {
1631
1787
  ctx.addSystemMessage(await ctx.initProject(args));
1632
1788
  }
1633
1789
  });
1634
- registry.register({
1790
+ register({
1635
1791
  name: "doctor",
1636
1792
  description: "Run environment and configuration checks",
1637
1793
  usage: "/doctor",
1794
+ category: "Info",
1638
1795
  async run(_args, ctx) {
1639
1796
  ctx.addSystemMessage(await ctx.runDoctor());
1640
1797
  }
1641
1798
  });
1642
- registry.register({
1799
+ register({
1643
1800
  name: "skills",
1644
1801
  description: "List available skills (project scope overrides user scope)",
1645
1802
  usage: "/skills",
1803
+ category: "Info",
1646
1804
  run(_args, ctx) {
1647
1805
  const skills = discoverSkills(ctx.cwd ?? process.cwd());
1648
1806
  if (skills.length === 0) {
@@ -1656,10 +1814,11 @@ ${lines.join("\n")}`);
1656
1814
  ${lines.join("\n")}`);
1657
1815
  }
1658
1816
  });
1659
- registry.register({
1817
+ register({
1660
1818
  name: "commit",
1661
1819
  description: "Analyze uncommitted changes and create a git commit (Conventional Commits)",
1662
1820
  usage: "/commit [instructions]",
1821
+ category: "Changes",
1663
1822
  async run(args, ctx) {
1664
1823
  const cwd = ctx.cwd ?? process.cwd();
1665
1824
  if (!isGitRepo(cwd)) {
@@ -1678,10 +1837,11 @@ ${lines.join("\n")}`);
1678
1837
  ctx.addSystemMessage("/commit: this context cannot submit prompts to the model.");
1679
1838
  }
1680
1839
  });
1681
- registry.register({
1840
+ register({
1682
1841
  name: "copy",
1683
1842
  description: "Copy the last assistant reply to the clipboard (`all` for the whole conversation)",
1684
1843
  usage: "/copy [all]",
1844
+ category: "Sessions",
1685
1845
  async run(args, ctx) {
1686
1846
  const arg = args.trim();
1687
1847
  if (arg !== "" && arg !== "all") {
@@ -1706,10 +1866,11 @@ ${lines.join("\n")}`);
1706
1866
  );
1707
1867
  }
1708
1868
  });
1709
- registry.register({
1869
+ register({
1710
1870
  name: "diff",
1711
1871
  description: "Show uncommitted changes (git status + colored diff)",
1712
1872
  usage: "/diff",
1873
+ category: "Changes",
1713
1874
  run(_args, ctx) {
1714
1875
  const cwd = ctx.cwd ?? process.cwd();
1715
1876
  if (!isGitRepo(cwd)) {
@@ -1745,7 +1906,7 @@ ${result.diff}${note ? `
1745
1906
  }
1746
1907
 
1747
1908
  // src/cli/commands/custom.ts
1748
- import { readFileSync, readdirSync } from "fs";
1909
+ import { readFileSync as readFileSync2, readdirSync } from "fs";
1749
1910
  import path11 from "path";
1750
1911
  var NAME_PATTERN = /^[a-z0-9-]+$/;
1751
1912
  var DESCRIPTION_PATTERN = /^<!--\s*description:\s*(.+?)\s*-->\s*$/;
@@ -1753,7 +1914,7 @@ var DEFAULT_DESCRIPTION = "Custom prompt command";
1753
1914
  function parseCommandFile(filePath, name) {
1754
1915
  let raw;
1755
1916
  try {
1756
- raw = readFileSync(filePath, "utf8");
1917
+ raw = readFileSync2(filePath, "utf8");
1757
1918
  } catch {
1758
1919
  return null;
1759
1920
  }
@@ -1799,6 +1960,7 @@ function registerCustomCommands(registry, cwd, home) {
1799
1960
  name: command.name,
1800
1961
  description: command.description,
1801
1962
  usage: `/${command.name} [args]`,
1963
+ category: "Custom",
1802
1964
  run(args, ctx) {
1803
1965
  const prompt = command.template.split("$ARGUMENTS").join(args);
1804
1966
  if (ctx.submitPrompt) {
@@ -2215,6 +2377,33 @@ function formatCheckpointList(snapshots, cwd) {
2215
2377
  ].join("\n");
2216
2378
  }
2217
2379
 
2380
+ // src/cli/commands/suggest.ts
2381
+ var MAX_SUGGESTIONS = 5;
2382
+ function isSubsequence(query, target) {
2383
+ let i = 0;
2384
+ for (const ch of target) {
2385
+ if (ch === query[i]) i += 1;
2386
+ if (i === query.length) return true;
2387
+ }
2388
+ return query.length === 0;
2389
+ }
2390
+ function filterCommands(input, commands) {
2391
+ if (!input.startsWith("/")) return [];
2392
+ if (/\s/.test(input)) return [];
2393
+ const prefix = input.slice(1).toLowerCase();
2394
+ const byName = (a, b) => a.name.localeCompare(b.name);
2395
+ const prefixed = commands.filter((cmd) => cmd.name.toLowerCase().startsWith(prefix)).sort(byName);
2396
+ const fuzzy = commands.filter(
2397
+ (cmd) => !cmd.name.toLowerCase().startsWith(prefix) && isSubsequence(prefix, cmd.name.toLowerCase())
2398
+ ).sort(byName);
2399
+ return [...prefixed, ...fuzzy].slice(0, MAX_SUGGESTIONS);
2400
+ }
2401
+ function didYouMeanSuffix(names) {
2402
+ if (names.length === 0) return "";
2403
+ const listed = names.slice(0, 3).map((name) => `/${name}`);
2404
+ return ` Did you mean: ${listed.join(", ")}?`;
2405
+ }
2406
+
2218
2407
  // src/cli/components/ExitPlanPrompt.tsx
2219
2408
  import { Box, Text, useInput } from "ink";
2220
2409
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -2235,15 +2424,6 @@ function ExitPlanPrompt({ onDecision }) {
2235
2424
  import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
2236
2425
  import { useEffect, useRef, useState } from "react";
2237
2426
 
2238
- // src/cli/commands/suggest.ts
2239
- var MAX_SUGGESTIONS = 5;
2240
- function filterCommands(input, commands) {
2241
- if (!input.startsWith("/")) return [];
2242
- if (/\s/.test(input)) return [];
2243
- const prefix = input.slice(1).toLowerCase();
2244
- return commands.filter((cmd) => cmd.name.toLowerCase().startsWith(prefix)).sort((a, b) => a.name.localeCompare(b.name)).slice(0, MAX_SUGGESTIONS);
2245
- }
2246
-
2247
2427
  // src/cli/path-suggest.ts
2248
2428
  import { readdir } from "fs/promises";
2249
2429
  import path15 from "path";
@@ -2264,6 +2444,7 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
2264
2444
  const dirAbs = path15.resolve(cwd, dirPart === "" ? "." : dirPart);
2265
2445
  const entries = await readdir(dirAbs, { withFileTypes: true }).catch(() => null);
2266
2446
  if (!entries) return [];
2447
+ const ignore = createIgnorePredicate(cwd);
2267
2448
  const lower = basePrefix.toLowerCase();
2268
2449
  const matches = [];
2269
2450
  let scanned = 0;
@@ -2275,6 +2456,7 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
2275
2456
  if (isDir && SKIP_DIRS.has(entry.name)) continue;
2276
2457
  if (entry.name.startsWith(".") && !basePrefix.startsWith(".")) continue;
2277
2458
  if (!entry.name.toLowerCase().startsWith(lower)) continue;
2459
+ if (ignore?.(path15.join(dirAbs, entry.name), isDir)) continue;
2278
2460
  matches.push({ path: `${dirPart}${entry.name}${isDir ? "/" : ""}`, isDir });
2279
2461
  }
2280
2462
  matches.sort((a, b) => a.path.localeCompare(b.path));
@@ -2289,6 +2471,7 @@ function InputBox({
2289
2471
  commands,
2290
2472
  cwd,
2291
2473
  refill,
2474
+ initialHistory,
2292
2475
  onSubmit,
2293
2476
  onInterrupt,
2294
2477
  onExit,
@@ -2296,7 +2479,7 @@ function InputBox({
2296
2479
  }) {
2297
2480
  const [value, setValue] = useState("");
2298
2481
  const [cursor, setCursor] = useState(0);
2299
- const [history, setHistory] = useState([]);
2482
+ const [history, setHistory] = useState(() => initialHistory ?? []);
2300
2483
  const [highlight, setHighlight] = useState(0);
2301
2484
  const [suggestionsDismissed, setSuggestionsDismissed] = useState(false);
2302
2485
  const [pathSuggestions, setPathSuggestions] = useState([]);
@@ -2478,12 +2661,13 @@ function InputBox({
2478
2661
  /* @__PURE__ */ jsx2(Text2, { inverse: true, children: at }),
2479
2662
  /* @__PURE__ */ jsx2(Text2, { children: after })
2480
2663
  ] }),
2481
- suggestions.length > 0 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: suggestions.map(
2482
- (cmd, index) => index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `/${cmd.name} - ${cmd.description}` }, cmd.name) : /* @__PURE__ */ jsxs2(Text2, { children: [
2483
- /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: `/${cmd.name}` }),
2664
+ suggestions.length > 0 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: suggestions.map((cmd, index) => {
2665
+ const label = cmd.usage ?? `/${cmd.name}`;
2666
+ return index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `${label} - ${cmd.description}` }, cmd.name) : /* @__PURE__ */ jsxs2(Text2, { children: [
2667
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: label }),
2484
2668
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` - ${cmd.description}` })
2485
- ] }, cmd.name)
2486
- ) }),
2669
+ ] }, cmd.name);
2670
+ }) }),
2487
2671
  showPathSuggestions && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: pathSuggestions.map(
2488
2672
  (sugg, index) => index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `@${sugg.path}` }, sugg.path) : /* @__PURE__ */ jsxs2(Text2, { children: [
2489
2673
  /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: `@${sugg.path}` }),
@@ -2591,10 +2775,72 @@ function RewindConfirmPrompt({ summary, onDecision }) {
2591
2775
  ] });
2592
2776
  }
2593
2777
 
2778
+ // src/cli/components/SelectPrompt.tsx
2779
+ import { Box as Box6, Text as Text7, useInput as useInput5 } from "ink";
2780
+ import { useState as useState2 } from "react";
2781
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2782
+ var SELECT_PROMPT_MAX_VISIBLE = 9;
2783
+ function SelectPrompt({ title, options, onSelect, onCancel }) {
2784
+ const [index, setIndex] = useState2(0);
2785
+ useInput5((input, key) => {
2786
+ if (key.escape) {
2787
+ onCancel();
2788
+ return;
2789
+ }
2790
+ if (key.return) {
2791
+ const option = options[index];
2792
+ if (option) onSelect(option.value);
2793
+ return;
2794
+ }
2795
+ if (key.downArrow || input === "j") {
2796
+ setIndex((i) => Math.min(i + 1, options.length - 1));
2797
+ } else if (key.upArrow || input === "k") {
2798
+ setIndex((i) => Math.max(i - 1, 0));
2799
+ }
2800
+ });
2801
+ const maxStart = Math.max(0, options.length - SELECT_PROMPT_MAX_VISIBLE);
2802
+ const start = Math.min(Math.max(0, index - Math.floor(SELECT_PROMPT_MAX_VISIBLE / 2)), maxStart);
2803
+ const visible = options.slice(start, start + SELECT_PROMPT_MAX_VISIBLE);
2804
+ const above = start;
2805
+ const below = options.length - start - visible.length;
2806
+ return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
2807
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: title }),
2808
+ above > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2809
+ "\u2026 ",
2810
+ above,
2811
+ " more above"
2812
+ ] }),
2813
+ visible.map((option, i) => {
2814
+ const active = start + i === index;
2815
+ return /* @__PURE__ */ jsxs7(Text7, { children: [
2816
+ /* @__PURE__ */ jsxs7(Text7, { bold: active, inverse: active, children: [
2817
+ active ? "\u276F " : " ",
2818
+ option.label
2819
+ ] }),
2820
+ option.hint && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2821
+ " (",
2822
+ option.hint,
2823
+ ")"
2824
+ ] }),
2825
+ option.description && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2826
+ " \u2014 ",
2827
+ option.description
2828
+ ] })
2829
+ ] }, option.value);
2830
+ }),
2831
+ below > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2832
+ "\u2026 ",
2833
+ below,
2834
+ " more below"
2835
+ ] }),
2836
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "[\u2191/\u2193 or j/k] move [Enter] select [Esc] cancel" })
2837
+ ] });
2838
+ }
2839
+
2594
2840
  // src/cli/components/StatusBar.tsx
2595
- import { Box as Box6, Text as Text7, useStdout } from "ink";
2841
+ import { Box as Box7, Text as Text8, useStdout } from "ink";
2596
2842
  import { memo as memo2 } from "react";
2597
- import { jsxs as jsxs7 } from "react/jsx-runtime";
2843
+ import { jsxs as jsxs8 } from "react/jsx-runtime";
2598
2844
  var BG_LABEL_MAX = 40;
2599
2845
  var WIDE_COLUMNS = 120;
2600
2846
  function cwdLabel(cwd, wide) {
@@ -2616,36 +2862,36 @@ var StatusBar = memo2(function StatusBar2({
2616
2862
  const wide = (stdout?.columns ?? 80) >= WIDE_COLUMNS;
2617
2863
  const joined = backgroundTasks.join(", ");
2618
2864
  const labels = joined.length > BG_LABEL_MAX ? `${joined.slice(0, BG_LABEL_MAX)}\u2026` : joined;
2619
- return /* @__PURE__ */ jsxs7(Box6, { justifyContent: "space-between", children: [
2620
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2865
+ return /* @__PURE__ */ jsxs8(Box7, { justifyContent: "space-between", children: [
2866
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2621
2867
  cwdLabel(cwd, wide),
2622
2868
  gitBranch ? ` [${gitBranch}]` : ""
2623
2869
  ] }),
2624
- backgroundTasks.length > 0 && /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
2870
+ backgroundTasks.length > 0 && /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
2625
2871
  "bg: ",
2626
2872
  backgroundTasks.length,
2627
2873
  " (",
2628
2874
  labels,
2629
2875
  ")"
2630
2876
  ] }),
2631
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2877
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2632
2878
  "model: ",
2633
2879
  model
2634
2880
  ] }),
2635
- contextPercent != null && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2881
+ contextPercent != null && /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2636
2882
  "ctx: ",
2637
2883
  contextPercent,
2638
2884
  "%"
2639
2885
  ] }),
2640
- sessionCostUsd != null && sessionCostUsd > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2886
+ sessionCostUsd != null && sessionCostUsd > 0 && /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2641
2887
  "$",
2642
2888
  formatDollars(sessionCostUsd)
2643
2889
  ] }),
2644
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2890
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2645
2891
  tokens2,
2646
2892
  " tokens"
2647
2893
  ] }),
2648
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2894
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2649
2895
  "mode: ",
2650
2896
  permissionMode
2651
2897
  ] })
@@ -2653,22 +2899,22 @@ var StatusBar = memo2(function StatusBar2({
2653
2899
  });
2654
2900
 
2655
2901
  // src/cli/components/StreamingMessage.tsx
2656
- import { Box as Box7, Text as Text8 } from "ink";
2902
+ import { Box as Box8, Text as Text9 } from "ink";
2657
2903
  import { memo as memo3 } from "react";
2658
- import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
2904
+ import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
2659
2905
  var StreamingMessage = memo3(function StreamingMessage2({
2660
2906
  text,
2661
2907
  continuation
2662
2908
  }) {
2663
- return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", marginBottom: 1, children: [
2664
- !continuation && /* @__PURE__ */ jsx7(Text8, { bold: true, color: "green", children: "star" }),
2665
- /* @__PURE__ */ jsx7(Text8, { color: "green", children: text })
2909
+ return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", marginBottom: 1, children: [
2910
+ !continuation && /* @__PURE__ */ jsx8(Text9, { bold: true, color: "green", children: "star" }),
2911
+ /* @__PURE__ */ jsx8(Text9, { color: "green", children: text })
2666
2912
  ] });
2667
2913
  });
2668
2914
 
2669
2915
  // src/cli/components/ThinkingIndicator.tsx
2670
- import { Box as Box8, Text as Text9 } from "ink";
2671
- import { jsxs as jsxs9 } from "react/jsx-runtime";
2916
+ import { Box as Box9, Text as Text10 } from "ink";
2917
+ import { jsxs as jsxs10 } from "react/jsx-runtime";
2672
2918
  var SPINNER_FRAMES = ["-", "\\", "|", "/"];
2673
2919
  var REASONING_TAIL_LENGTH = 200;
2674
2920
  var THOUGHT_SUMMARY_LENGTH = 100;
@@ -2684,13 +2930,13 @@ function ThinkingIndicator({
2684
2930
  }) {
2685
2931
  const tail = reasoning ? truncateTail(reasoning, REASONING_TAIL_LENGTH) : "";
2686
2932
  const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
2687
- return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", children: [
2688
- /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
2933
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
2934
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2689
2935
  spinner,
2690
2936
  " ",
2691
2937
  activity ?? "star is thinking\u2026"
2692
2938
  ] }),
2693
- tail !== "" && /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
2939
+ tail !== "" && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2694
2940
  " ",
2695
2941
  tail
2696
2942
  ] })
@@ -2712,6 +2958,51 @@ function isDoubleEscape(previous, now, windowMs = DOUBLE_ESC_WINDOW_MS) {
2712
2958
  return previous !== null && now - previous <= windowMs;
2713
2959
  }
2714
2960
 
2961
+ // src/cli/history.ts
2962
+ import fs10 from "fs";
2963
+ import path16 from "path";
2964
+ var DEFAULT_LIMIT = 500;
2965
+ var MAX_FILE_LINES = 1e3;
2966
+ var TRIM_TO_LINES = 500;
2967
+ function historyPath() {
2968
+ return path16.join(starHome(), "history");
2969
+ }
2970
+ function readLines(file) {
2971
+ const raw = fs10.readFileSync(file, "utf8");
2972
+ return raw.split("\n").map((line) => line.replace(/\r$/, "")).filter((line) => line.trim().length > 0);
2973
+ }
2974
+ function loadHistory(limit = DEFAULT_LIMIT) {
2975
+ try {
2976
+ return readLines(historyPath()).slice(-limit);
2977
+ } catch {
2978
+ return [];
2979
+ }
2980
+ }
2981
+ function appendHistory(entry) {
2982
+ const line = entry.replace(/[\r\n]+/g, " ").trim();
2983
+ if (line.length === 0) return;
2984
+ try {
2985
+ const file = historyPath();
2986
+ let existing = [];
2987
+ try {
2988
+ existing = readLines(file);
2989
+ } catch {
2990
+ }
2991
+ if (existing[existing.length - 1] === line) return;
2992
+ if (existing.length >= MAX_FILE_LINES) {
2993
+ const kept = [...existing.slice(-(TRIM_TO_LINES - 1)), line];
2994
+ fs10.mkdirSync(path16.dirname(file), { recursive: true });
2995
+ fs10.writeFileSync(file, `${kept.join("\n")}
2996
+ `);
2997
+ return;
2998
+ }
2999
+ fs10.mkdirSync(path16.dirname(file), { recursive: true });
3000
+ fs10.appendFileSync(file, `${line}
3001
+ `);
3002
+ } catch {
3003
+ }
3004
+ }
3005
+
2715
3006
  // src/cli/notify.ts
2716
3007
  var BELL = "\x07";
2717
3008
  function shouldNotifyBell(input, isTTY) {
@@ -2827,7 +3118,7 @@ async function checkForUpdate(currentVersion, fetcher = defaultFetcher2) {
2827
3118
  }
2828
3119
 
2829
3120
  // src/cli/repl.tsx
2830
- import { jsx as jsx8, jsxs as jsxs10 } from "react/jsx-runtime";
3121
+ import { jsx as jsx9, jsxs as jsxs11 } from "react/jsx-runtime";
2831
3122
  var CLEAR_TERMINAL = process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H";
2832
3123
  function emptyUsage() {
2833
3124
  return { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
@@ -2839,6 +3130,16 @@ var SESSION_MODE_CYCLE = ["ask", "auto", "readonly", "plan"];
2839
3130
  function formatUsage(usage) {
2840
3131
  return `API usage this session: ${usage.requests} requests, ${usage.promptTokens} prompt + ${usage.completionTokens} completion = ${usage.totalTokens} tokens`;
2841
3132
  }
3133
+ var SESSION_PICKER_CAP = 20;
3134
+ async function sessionPreview(id) {
3135
+ const store = await SessionStore.open(id);
3136
+ if (!store) return null;
3137
+ const first = (await store.messages()).find((m) => m.role === "user");
3138
+ if (!first) return null;
3139
+ const text = coreMessageText(first).replace(/\s+/g, " ").trim();
3140
+ if (!text) return null;
3141
+ return text.length > 60 ? `${text.slice(0, 60)}\u2026` : text;
3142
+ }
2842
3143
  function Repl({
2843
3144
  backend,
2844
3145
  model,
@@ -2850,23 +3151,24 @@ function Repl({
2850
3151
  initialUsage
2851
3152
  }) {
2852
3153
  const { exit } = useApp();
2853
- const [initialDisplay] = useState2(() => buildDisplayMessages(initialMessages ?? []));
2854
- const [messages, setMessages] = useState2(initialDisplay);
2855
- const [epoch, setEpoch] = useState2(0);
2856
- const [streamingText, setStreamingText] = useState2(null);
2857
- const [isStreaming, setIsStreaming] = useState2(false);
2858
- const [thinking, setThinking] = useState2(false);
2859
- const [thinkingText, setThinkingText] = useState2("");
2860
- const [thoughtSummary, setThoughtSummary] = useState2(null);
2861
- const [, setUsageVersion] = useState2(0);
2862
- const [modelName, setModelName] = useState2(model);
2863
- const [pending, setPending] = useState2(null);
2864
- const [spinnerTick, setSpinnerTick] = useState2(0);
2865
- const [activity, setActivity] = useState2(null);
2866
- const [bgLabels, setBgLabels] = useState2(() => runningTaskLabels());
2867
- const [permissionModeState, setPermissionModeState] = useState2(permissionMode);
2868
- const [planApproval, setPlanApprovalState] = useState2(false);
2869
- const [pendingRewind, setPendingRewindState] = useState2(null);
3154
+ const [initialDisplay] = useState3(() => buildDisplayMessages(initialMessages ?? []));
3155
+ const [messages, setMessages] = useState3(initialDisplay);
3156
+ const [epoch, setEpoch] = useState3(0);
3157
+ const [streamingText, setStreamingText] = useState3(null);
3158
+ const [isStreaming, setIsStreaming] = useState3(false);
3159
+ const [thinking, setThinking] = useState3(false);
3160
+ const [thinkingText, setThinkingText] = useState3("");
3161
+ const [thoughtSummary, setThoughtSummary] = useState3(null);
3162
+ const [, setUsageVersion] = useState3(0);
3163
+ const [modelName, setModelName] = useState3(model);
3164
+ const [pending, setPending] = useState3(null);
3165
+ const [spinnerTick, setSpinnerTick] = useState3(0);
3166
+ const [activity, setActivity] = useState3(null);
3167
+ const [bgLabels, setBgLabels] = useState3(() => runningTaskLabels());
3168
+ const [permissionModeState, setPermissionModeState] = useState3(permissionMode);
3169
+ const [planApproval, setPlanApprovalState] = useState3(false);
3170
+ const [pendingRewind, setPendingRewindState] = useState3(null);
3171
+ const [picker, setPickerState] = useState3(null);
2870
3172
  const backendRef = useRef2(backend);
2871
3173
  const nextIdRef = useRef2(initialDisplay.length);
2872
3174
  const abortRef = useRef2(null);
@@ -2881,21 +3183,23 @@ function Repl({
2881
3183
  const prevModeRef = useRef2(null);
2882
3184
  const planApprovalRef = useRef2(false);
2883
3185
  const pendingRewindRef = useRef2(null);
3186
+ const pickerRef = useRef2(null);
2884
3187
  const modelNameRef = useRef2(model);
2885
3188
  const sessionStoreRef = useRef2(sessionStore);
2886
3189
  const usageRef = useRef2(initialUsage ? { ...initialUsage } : emptyUsage());
2887
3190
  const turnChunksRef = useRef2(0);
2888
3191
  const messagesRef = useRef2(initialDisplay);
2889
- const [queueItems, setQueueItems] = useState2([]);
3192
+ const [queueItems, setQueueItems] = useState3([]);
2890
3193
  const queueRef = useRef2(new PromptQueue());
2891
- const [pendingImages, setPendingImages] = useState2([]);
3194
+ const [pendingImages, setPendingImages] = useState3([]);
2892
3195
  const pendingImagesRef = useRef2([]);
2893
3196
  const imageSeqRef = useRef2(1);
2894
- const [inputRefill, setInputRefill] = useState2();
3197
+ const [inputRefill, setInputRefill] = useState3();
2895
3198
  const refillSeqRef = useRef2(0);
2896
3199
  const lastEscRef = useRef2(null);
2897
- const [gitBranch, setGitBranch] = useState2(null);
2898
- const [contextPercent, setContextPercent] = useState2(null);
3200
+ const [gitBranch, setGitBranch] = useState3(null);
3201
+ const [contextPercent, setContextPercent] = useState3(null);
3202
+ const [initialHistory] = useState3(() => loadHistory());
2899
3203
  const budgetWarnedRef = useRef2(false);
2900
3204
  const budgetExceededRef = useRef2(false);
2901
3205
  const runStreamRef = useRef2(
@@ -2945,6 +3249,34 @@ function Repl({
2945
3249
  pendingRewindRef.current = p;
2946
3250
  setPendingRewindState(p);
2947
3251
  }, []);
3252
+ const setPicker = useCallback((p) => {
3253
+ pickerRef.current = p;
3254
+ setPickerState(p);
3255
+ }, []);
3256
+ const showPicker = useCallback(
3257
+ (title, options) => {
3258
+ if (pickerRef.current) return Promise.resolve(null);
3259
+ return new Promise((resolve) => {
3260
+ setPicker({ title, options, resolve });
3261
+ });
3262
+ },
3263
+ [setPicker]
3264
+ );
3265
+ const handlePickerSelect = useCallback(
3266
+ (value) => {
3267
+ const p = pickerRef.current;
3268
+ if (!p) return;
3269
+ setPicker(null);
3270
+ p.resolve(value);
3271
+ },
3272
+ [setPicker]
3273
+ );
3274
+ const handlePickerCancel = useCallback(() => {
3275
+ const p = pickerRef.current;
3276
+ if (!p) return;
3277
+ setPicker(null);
3278
+ p.resolve(null);
3279
+ }, [setPicker]);
2948
3280
  const stageClipboardImage = useCallback((img) => {
2949
3281
  pendingImagesRef.current = [
2950
3282
  ...pendingImagesRef.current,
@@ -3085,9 +3417,9 @@ function Repl({
3085
3417
  }
3086
3418
  exit();
3087
3419
  }, [exit, pushMessage]);
3088
- useInput5((_input, key) => {
3420
+ useInput6((_input, key) => {
3089
3421
  if (key.escape) {
3090
- const busy = abortRef.current !== null || pendingRef.current !== null || planApprovalRef.current || pendingRewindRef.current !== null;
3422
+ const busy = abortRef.current !== null || pendingRef.current !== null || planApprovalRef.current || pendingRewindRef.current !== null || pickerRef.current !== null;
3091
3423
  if (busy) {
3092
3424
  lastEscRef.current = null;
3093
3425
  interrupt();
@@ -3104,7 +3436,7 @@ function Repl({
3104
3436
  }
3105
3437
  lastEscRef.current = null;
3106
3438
  if (key.shift && key.tab) {
3107
- if (abortRef.current || pendingRef.current || planApprovalRef.current || pendingRewindRef.current)
3439
+ if (abortRef.current || pendingRef.current || planApprovalRef.current || pendingRewindRef.current || pickerRef.current)
3108
3440
  return;
3109
3441
  const current = config.permissionMode;
3110
3442
  const index = SESSION_MODE_CYCLE.indexOf(current);
@@ -3461,6 +3793,97 @@ ${lines.join("\n")}`;
3461
3793
  return formatSessionEntries(entries, { showCwd: all });
3462
3794
  },
3463
3795
  resumeSession: resume,
3796
+ pickModel: async () => {
3797
+ const models = listModels(config);
3798
+ if (models.length === 0) return "No models configured.";
3799
+ const options = models.map((m) => {
3800
+ const parts = [`${m.provider}/${m.model}`, `${contextWindowTokens(config, m.name)} ctx`];
3801
+ if (m.promptPrice !== void 0 && m.completionPrice !== void 0) {
3802
+ parts.push(`$${m.promptPrice}/$${m.completionPrice} per 1M tokens`);
3803
+ }
3804
+ return {
3805
+ value: m.name,
3806
+ label: m.name,
3807
+ description: parts.join(" \xB7 "),
3808
+ hint: m.name === modelNameRef.current ? "current" : void 0
3809
+ };
3810
+ });
3811
+ const picked = await showPicker("Select a model", options);
3812
+ if (!picked || picked === modelNameRef.current) {
3813
+ return `Model unchanged \u2014 still "${modelNameRef.current}".`;
3814
+ }
3815
+ return switchModel(picked);
3816
+ },
3817
+ pickPermissionMode: async () => {
3818
+ const current = config.permissionMode;
3819
+ const descriptions = {
3820
+ ask: "reads allowed; writes/exec ask for confirmation",
3821
+ auto: "everything allowed except hard-denied dangerous commands/paths",
3822
+ readonly: "read-only; all writes/exec denied",
3823
+ yolo: "allow everything, never ask (disables ALL safety checks)",
3824
+ plan: "read-only research, then approve a plan before executing (this session only)"
3825
+ };
3826
+ const options = ["ask", "auto", "readonly", "yolo", "plan"].map((m) => ({
3827
+ value: m,
3828
+ label: m,
3829
+ description: descriptions[m],
3830
+ hint: m === current ? "current" : void 0
3831
+ }));
3832
+ const picked = await showPicker("Select a permission mode", options);
3833
+ if (!picked || picked === current) {
3834
+ return `Permission mode unchanged \u2014 still ${current}.`;
3835
+ }
3836
+ if (picked === "plan") return ctx.planMode();
3837
+ return ctx.permissionMode(picked);
3838
+ },
3839
+ pickSession: async (all) => {
3840
+ const entries = (await listSessionEntries(all ? void 0 : cwd)).slice(
3841
+ 0,
3842
+ SESSION_PICKER_CAP
3843
+ );
3844
+ if (entries.length === 0) {
3845
+ return all ? "No sessions found." : "No sessions found for this directory.";
3846
+ }
3847
+ const options = await Promise.all(
3848
+ entries.map(async ({ meta, messageCount }) => {
3849
+ const parts = [`${messageCount} messages \xB7 ${relativeTime(meta.updatedAt)}`];
3850
+ const preview = await sessionPreview(meta.id);
3851
+ if (preview) parts.push(preview);
3852
+ if (all) parts.push(`[${meta.cwd}]`);
3853
+ return {
3854
+ value: meta.id,
3855
+ label: meta.title || shortSessionId(meta.id),
3856
+ description: parts.join(" \xB7 "),
3857
+ hint: meta.id === sessionStoreRef.current?.id ? "current" : void 0
3858
+ };
3859
+ })
3860
+ );
3861
+ const picked = await showPicker(
3862
+ all ? "Resume a session (all directories)" : "Resume a session",
3863
+ options
3864
+ );
3865
+ if (!picked) return "Session unchanged \u2014 picker cancelled.";
3866
+ return resume(picked);
3867
+ },
3868
+ forkSession: async () => {
3869
+ const current = backendRef.current;
3870
+ const store = sessionStoreRef.current;
3871
+ if (!(current instanceof AgentLoop) || !store) {
3872
+ return "Nothing to fork \u2014 no active session.";
3873
+ }
3874
+ const messages2 = await store.messages();
3875
+ if (messages2.length === 0) {
3876
+ return "Nothing to fork \u2014 the current session has no messages yet.";
3877
+ }
3878
+ const fork = await SessionStore.create(cwd, modelNameRef.current);
3879
+ await fork.replaceMessages(messages2);
3880
+ const oldMeta = await store.meta();
3881
+ const title = `Fork of ${oldMeta.title || shortSessionId(oldMeta.id)}`;
3882
+ await fork.setTitle(title);
3883
+ current.setSessionStore(fork);
3884
+ sessionStoreRef.current = fork;
3885
+ return `Forked into new session ${fork.id} ("${title}") and switched to it. Checkpoints and rewind history do not carry over.`;
3886
+ },
3464
3887
  newSession: async () => {
3465
3888
  const current = backendRef.current;
3466
3889
  if (!(current instanceof AgentLoop)) {
@@ -3664,7 +4087,8 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
3664
4087
  applyMessages,
3665
4088
  redrawMessages,
3666
4089
  applySessionMode,
3667
- setPendingRewind
4090
+ setPendingRewind,
4091
+ showPicker
3668
4092
  ]);
3669
4093
  const runShellBang = useCallback(
3670
4094
  async (raw) => {
@@ -3704,6 +4128,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
3704
4128
  );
3705
4129
  const handleSubmit = useCallback(
3706
4130
  (text) => {
4131
+ appendHistory(text);
3707
4132
  if (text.startsWith("!")) {
3708
4133
  if (abortRef.current) {
3709
4134
  queueRef.current.enqueue({ text, images: [] });
@@ -3718,7 +4143,11 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
3718
4143
  if (!parsed) return;
3719
4144
  const command = registry.get(parsed.name);
3720
4145
  if (!command) {
3721
- pushMessage("system", `Unknown command: /${parsed.name} (try /help)`);
4146
+ const suggestions = registry.complete(parsed.name).map((cmd) => cmd.name);
4147
+ pushMessage(
4148
+ "system",
4149
+ `Unknown command: /${parsed.name} (try /help)${didYouMeanSuffix(suggestions)}`
4150
+ );
3722
4151
  return;
3723
4152
  }
3724
4153
  void command.run(parsed.args, registry.ctx);
@@ -3744,7 +4173,11 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
3744
4173
  [registry, pushMessage, runStream, runShellBang, config, clearPendingImages, sessionCostUsd]
3745
4174
  );
3746
4175
  const commandHints = useMemo(
3747
- () => registry.list().map((cmd) => ({ name: cmd.name, description: cmd.description })),
4176
+ () => registry.list().map((cmd) => ({
4177
+ name: cmd.name,
4178
+ description: cmd.description,
4179
+ usage: cmd.usage ?? `/${cmd.name}`
4180
+ })),
3748
4181
  [registry]
3749
4182
  );
3750
4183
  useEffect2(() => {
@@ -3759,9 +4192,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
3759
4192
  }
3760
4193
  }, [cwd, config, isStreaming, epoch]);
3761
4194
  const sessionCost = sessionCostUsd();
3762
- return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
3763
- /* @__PURE__ */ jsx8(MessageList, { messages }, `messages-${epoch}`),
3764
- (thinking || activity !== null) && /* @__PURE__ */ jsx8(
4195
+ return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", children: [
4196
+ /* @__PURE__ */ jsx9(MessageList, { messages }, `messages-${epoch}`),
4197
+ (thinking || activity !== null) && /* @__PURE__ */ jsx9(
3765
4198
  ThinkingIndicator,
3766
4199
  {
3767
4200
  reasoning: thinkingText,
@@ -3769,9 +4202,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
3769
4202
  activity: activity ?? void 0
3770
4203
  }
3771
4204
  ),
3772
- !thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */ jsx8(Text10, { dimColor: true, children: thoughtSummary }),
3773
- streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */ jsx8(StreamingMessage, { text: streamingText, continuation: turnChunksRef.current > 0 }),
3774
- pending && /* @__PURE__ */ jsx8(
4205
+ !thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */ jsx9(Text11, { dimColor: true, children: thoughtSummary }),
4206
+ streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */ jsx9(StreamingMessage, { text: streamingText, continuation: turnChunksRef.current > 0 }),
4207
+ pending && /* @__PURE__ */ jsx9(
3775
4208
  PermissionPrompt,
3776
4209
  {
3777
4210
  request: pending.request,
@@ -3779,32 +4212,42 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
3779
4212
  onDecision: handleDecision
3780
4213
  }
3781
4214
  ),
3782
- planApproval && /* @__PURE__ */ jsx8(ExitPlanPrompt, { onDecision: handlePlanDecision }),
3783
- pendingRewind && /* @__PURE__ */ jsx8(RewindConfirmPrompt, { summary: pendingRewind.summary, onDecision: handleRewindDecision }),
3784
- queueItems.map((item) => /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
4215
+ planApproval && /* @__PURE__ */ jsx9(ExitPlanPrompt, { onDecision: handlePlanDecision }),
4216
+ pendingRewind && /* @__PURE__ */ jsx9(RewindConfirmPrompt, { summary: pendingRewind.summary, onDecision: handleRewindDecision }),
4217
+ picker && /* @__PURE__ */ jsx9(
4218
+ SelectPrompt,
4219
+ {
4220
+ title: picker.title,
4221
+ options: picker.options,
4222
+ onSelect: handlePickerSelect,
4223
+ onCancel: handlePickerCancel
4224
+ }
4225
+ ),
4226
+ queueItems.map((item) => /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3785
4227
  "queued: ",
3786
4228
  item.text.length > 80 ? `${item.text.slice(0, 80)}\u2026` : item.text
3787
4229
  ] }, item.id)),
3788
- pendingImages.map((staged) => /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
4230
+ pendingImages.map((staged) => /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
3789
4231
  "[image attached: ",
3790
4232
  staged.image.path,
3791
4233
  "]"
3792
4234
  ] }, staged.id)),
3793
- /* @__PURE__ */ jsx8(
4235
+ /* @__PURE__ */ jsx9(
3794
4236
  InputBox,
3795
4237
  {
3796
4238
  isStreaming,
3797
- disabled: pending !== null || planApproval || pendingRewind !== null,
4239
+ disabled: pending !== null || planApproval || pendingRewind !== null || picker !== null,
3798
4240
  commands: commandHints,
3799
4241
  cwd,
3800
4242
  refill: inputRefill,
4243
+ initialHistory,
3801
4244
  onSubmit: handleSubmit,
3802
4245
  onInterrupt: interrupt,
3803
4246
  onExit: handleExit,
3804
4247
  onPasteImage: handlePasteImage
3805
4248
  }
3806
4249
  ),
3807
- /* @__PURE__ */ jsx8(
4250
+ /* @__PURE__ */ jsx9(
3808
4251
  StatusBar,
3809
4252
  {
3810
4253
  cwd,
@@ -3824,7 +4267,7 @@ function renderRepl(backend, opts) {
3824
4267
  defaultTaskManager.cleanup();
3825
4268
  });
3826
4269
  return render(
3827
- /* @__PURE__ */ jsx8(
4270
+ /* @__PURE__ */ jsx9(
3828
4271
  Repl,
3829
4272
  {
3830
4273
  backend,
@@ -3841,7 +4284,7 @@ function renderRepl(backend, opts) {
3841
4284
  }
3842
4285
 
3843
4286
  // src/config/loader.ts
3844
- import fs10 from "fs";
4287
+ import fs11 from "fs";
3845
4288
  import { parse as parse2 } from "smol-toml";
3846
4289
  var PartialConfigSchema = ConfigSchema.partial();
3847
4290
  function parseTomlFile(content, filePath) {
@@ -3869,8 +4312,8 @@ function applyOverrides(config, overrides) {
3869
4312
  return merged;
3870
4313
  }
3871
4314
  function readTomlFileSync(filePath) {
3872
- if (!fs10.existsSync(filePath)) return {};
3873
- const content = fs10.readFileSync(filePath, "utf8");
4315
+ if (!fs11.existsSync(filePath)) return {};
4316
+ const content = fs11.readFileSync(filePath, "utf8");
3874
4317
  return parseTomlFile(content, filePath);
3875
4318
  }
3876
4319
  function loadConfigSync(cwd, overrides) {