@cryer/star-cli 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,8 @@ import {
12
13
  collectWorkingDiff,
13
14
  compactMessages,
14
15
  createDefaultRegistry,
16
+ createIgnorePredicate,
17
+ defaultAgentTasks,
15
18
  defaultTaskManager,
16
19
  discoverSkills,
17
20
  estimateTokens,
@@ -27,6 +30,8 @@ import {
27
30
  isGitRepo,
28
31
  isSensitivePath,
29
32
  listSnapshots,
33
+ loadTodos,
34
+ parseTodoArgs,
30
35
  projectConfigPath,
31
36
  reconcileToolCalls,
32
37
  resolveShell,
@@ -34,8 +39,9 @@ import {
34
39
  sessionsDir,
35
40
  starHome,
36
41
  summarizeMessages,
37
- undoTurnSnapshots
38
- } from "./chunk-MJ2O7NWI.js";
42
+ undoTurnSnapshots,
43
+ userMemoryPath
44
+ } from "./chunk-DZWTIMJ2.js";
39
45
 
40
46
  // src/main.tsx
41
47
  import { Command } from "commander";
@@ -269,14 +275,17 @@ var UsageTracker = class {
269
275
  };
270
276
 
271
277
  // 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";
278
+ import { Box as Box11, Text as Text12, render, useApp, useInput as useInput6 } from "ink";
279
+ import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef3, useState as useState3 } from "react";
274
280
 
275
281
  // src/config/save.ts
276
282
  import fs from "fs";
277
283
  import path2 from "path";
278
284
  import { parse, stringify } from "smol-toml";
279
285
  async function addAllowRule(rule) {
286
+ return addPermissionRule("allow", rule);
287
+ }
288
+ async function addPermissionRule(kind, rule) {
280
289
  const filePath = globalConfigPath();
281
290
  let raw = {};
282
291
  try {
@@ -286,9 +295,9 @@ async function addAllowRule(rule) {
286
295
  if (error.code !== "ENOENT") throw error;
287
296
  }
288
297
  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];
298
+ const rules = Array.isArray(permissions[kind]) ? permissions[kind] : [];
299
+ if (rules.includes(rule)) return false;
300
+ permissions[kind] = [...rules, rule];
292
301
  raw.permissions = permissions;
293
302
  await fs.promises.mkdir(path2.dirname(filePath), { recursive: true });
294
303
  await fs.promises.writeFile(filePath, stringify(raw), "utf8");
@@ -332,7 +341,8 @@ var ModelConfigSchema = z.object({
332
341
  completionPrice: z.number().nonnegative().optional()
333
342
  });
334
343
  var PermissionsConfigSchema = z.object({
335
- allow: z.array(z.string()).default([])
344
+ allow: z.array(z.string()).default([]),
345
+ deny: z.array(z.string()).default([])
336
346
  });
337
347
  var HookConfigSchema = z.object({
338
348
  event: z.enum(["PreToolUse", "PostToolUse", "Stop"]),
@@ -361,6 +371,20 @@ var ConfigSchema = z.object({
361
371
  // gracefully (some relays never send the terminal chunks). Applies once
362
372
  // streaming has started; the first part gets a longer, fixed allowance.
363
373
  streamIdleTimeoutSec: z.number().positive().default(20),
374
+ // Seconds to wait for the very first stream part before giving up; slow
375
+ // thinking-model relays can buffer for minutes before sending anything.
376
+ streamFirstChunkTimeoutSec: z.number().positive().default(300),
377
+ // Extra attempts per model request when a stream fails transiently
378
+ // (network error, 429/5xx, idle watchdog cutoff) or comes back empty, so a
379
+ // relay hiccup does not silently end a half-finished turn. 0 disables.
380
+ // Retries after a timeout/empty failure scale both stream timeouts up
381
+ // (attempt number ×, capped at 3x) since the relay is likely overloaded.
382
+ streamMaxRetries: z.number().int().min(0).default(3),
383
+ // How many times a turn may be nudged to continue when the model ends it
384
+ // with a text-only reply that announces pending work ("我将…", "I will…")
385
+ // instead of tool calls — a common premature-stop failure of weaker
386
+ // agentic models. Never fires in plan mode. 0 disables.
387
+ maxAutoContinues: z.number().int().min(0).default(2),
364
388
  contextCompaction: z.enum(["summary", "truncate"]).default("summary"),
365
389
  // Optional per-session dollar budget; when set, the REPL stops the session
366
390
  // once the estimated cost for the session reaches this amount.
@@ -370,7 +394,7 @@ var ConfigSchema = z.object({
370
394
  // STAR_NO_NOTIFY=1 disables without touching the config.
371
395
  notifyBell: z.boolean().default(true),
372
396
  notifyBellThresholdSec: z.number().positive().default(10),
373
- permissions: PermissionsConfigSchema.default({ allow: [] }),
397
+ permissions: PermissionsConfigSchema.default({ allow: [], deny: [] }),
374
398
  hooks: z.array(HookConfigSchema).default([])
375
399
  });
376
400
  function contextWindowTokens(config, modelName) {
@@ -1253,6 +1277,61 @@ function stringifyPart(value) {
1253
1277
  return typeof value === "string" ? value : JSON.stringify(value, null, 2);
1254
1278
  }
1255
1279
 
1280
+ // src/cli/commands/builtin.ts
1281
+ import { readFileSync } from "fs";
1282
+
1283
+ // src/session/search.ts
1284
+ var SNIPPET_LENGTH = 100;
1285
+ function flattenMessage(message) {
1286
+ const content = message.content;
1287
+ if (typeof content === "string") return content;
1288
+ if (!Array.isArray(content)) return "";
1289
+ const parts = [];
1290
+ for (const part of content) {
1291
+ if (part.type === "text") {
1292
+ parts.push(part.text);
1293
+ } else if (part.type === "tool-call") {
1294
+ parts.push(JSON.stringify(part.args));
1295
+ } else if (part.type === "tool-result") {
1296
+ parts.push(typeof part.result === "string" ? part.result : JSON.stringify(part.result));
1297
+ }
1298
+ }
1299
+ return parts.join("\n");
1300
+ }
1301
+ function makeSnippet(text, query) {
1302
+ const flat = text.replace(/\s+/g, " ").trim();
1303
+ const index = flat.toLowerCase().indexOf(query.toLowerCase());
1304
+ if (index === -1 || flat.length <= SNIPPET_LENGTH) return flat.slice(0, SNIPPET_LENGTH);
1305
+ const side = Math.max(0, Math.floor((SNIPPET_LENGTH - query.length) / 2));
1306
+ let start = Math.max(0, index - side);
1307
+ const end = Math.min(flat.length, start + SNIPPET_LENGTH);
1308
+ start = Math.max(0, end - SNIPPET_LENGTH);
1309
+ return `${start > 0 ? "\u2026" : ""}${flat.slice(start, end)}${end < flat.length ? "\u2026" : ""}`;
1310
+ }
1311
+ async function searchSessions(query, limit = 10) {
1312
+ const hits = [];
1313
+ const needle = query.toLowerCase();
1314
+ for (const meta of await SessionStore.list()) {
1315
+ if (hits.length >= limit) break;
1316
+ const store = await SessionStore.open(meta.id);
1317
+ if (!store) continue;
1318
+ const text = (await store.messages()).map(flattenMessage).filter((part) => part.length > 0).join("\n");
1319
+ if (text.toLowerCase().includes(needle)) {
1320
+ hits.push({ meta, snippet: makeSnippet(text, query) });
1321
+ }
1322
+ }
1323
+ return hits;
1324
+ }
1325
+ function formatSearchResults(hits, query) {
1326
+ if (hits.length === 0) return `No sessions matching "${query}".`;
1327
+ const lines = hits.map(({ meta, snippet }) => {
1328
+ const title = meta.title || "(untitled)";
1329
+ return `${shortSessionId(meta.id)} ${title} (${relativeTime(meta.updatedAt)})
1330
+ ${snippet}`;
1331
+ });
1332
+ return [...lines, "Resume with /resume <id>"].join("\n");
1333
+ }
1334
+
1256
1335
  // src/cli/diff-preview.ts
1257
1336
  import { readFile as readFile3 } from "fs/promises";
1258
1337
  import path10 from "path";
@@ -1440,31 +1519,41 @@ async function generateDiffPreview(toolName, args, cwd) {
1440
1519
 
1441
1520
  // src/cli/commands/builtin.ts
1442
1521
  function registerBuiltinCommands(registry) {
1443
- registry.register({
1522
+ const register = (command) => registry.register(command);
1523
+ register({
1444
1524
  name: "help",
1445
1525
  description: "List available commands",
1446
1526
  usage: "/help",
1527
+ category: "General",
1447
1528
  run(_args, ctx) {
1448
- const lines = registry.list().map((cmd) => {
1529
+ const groups = /* @__PURE__ */ new Map();
1530
+ for (const cmd of registry.list()) {
1531
+ const category = cmd.category ?? "Other";
1449
1532
  const usage = cmd.usage ?? `/${cmd.name}`;
1450
- return `${usage} - ${cmd.description}`;
1451
- });
1533
+ const lines = groups.get(category) ?? [];
1534
+ lines.push(` ${usage} - ${cmd.description}`);
1535
+ groups.set(category, lines);
1536
+ }
1537
+ const sections = [...groups.entries()].map(([name, lines]) => [name, ...lines].join("\n"));
1452
1538
  ctx.addSystemMessage(`Available commands:
1453
- ${lines.join("\n")}`);
1539
+
1540
+ ${sections.join("\n\n")}`);
1454
1541
  }
1455
1542
  });
1456
- registry.register({
1543
+ register({
1457
1544
  name: "clear",
1458
1545
  description: "Clear message history",
1459
1546
  usage: "/clear",
1547
+ category: "General",
1460
1548
  run(_args, ctx) {
1461
1549
  ctx.clearMessages();
1462
1550
  }
1463
1551
  });
1464
- registry.register({
1552
+ register({
1465
1553
  name: "new",
1466
1554
  description: "Start a new session with a clean context",
1467
1555
  usage: "/new",
1556
+ category: "Sessions",
1468
1557
  async run(_args, ctx) {
1469
1558
  if (!ctx.newSession) {
1470
1559
  ctx.addSystemMessage("/new: this context cannot start a new session.");
@@ -1473,10 +1562,11 @@ ${lines.join("\n")}`);
1473
1562
  ctx.addSystemMessage(await ctx.newSession());
1474
1563
  }
1475
1564
  });
1476
- registry.register({
1565
+ register({
1477
1566
  name: "clear-sessions",
1478
1567
  description: "Delete stored sessions: this directory by default, every session with --all (the current session is kept)",
1479
1568
  usage: "/clear-sessions [--all]",
1569
+ category: "Sessions",
1480
1570
  async run(args, ctx) {
1481
1571
  const arg = args.trim();
1482
1572
  if (arg !== "" && arg !== "--all") {
@@ -1492,157 +1582,242 @@ ${lines.join("\n")}`);
1492
1582
  ctx.addSystemMessage(await ctx.clearSessions(arg === "--all"));
1493
1583
  }
1494
1584
  });
1495
- registry.register({
1585
+ register({
1496
1586
  name: "exit",
1497
1587
  description: "Exit the application",
1498
1588
  usage: "/exit",
1589
+ category: "General",
1499
1590
  run(_args, ctx) {
1500
1591
  ctx.exit();
1501
1592
  }
1502
1593
  });
1503
- registry.register({
1594
+ register({
1504
1595
  name: "q",
1505
1596
  description: "Exit the application (alias of /exit)",
1506
1597
  usage: "/q",
1598
+ category: "General",
1507
1599
  run(_args, ctx) {
1508
1600
  ctx.exit();
1509
1601
  }
1510
1602
  });
1511
- registry.register({
1603
+ register({
1512
1604
  name: "model",
1513
- description: "List available models or switch the current model",
1605
+ description: "Switch the current model (opens a picker when no name is given)",
1514
1606
  usage: "/model [name]",
1607
+ category: "Settings",
1515
1608
  async run(args, ctx) {
1516
1609
  if (!args) {
1517
- ctx.addSystemMessage(ctx.listModels());
1610
+ ctx.addSystemMessage(await ctx.pickModel());
1518
1611
  } else {
1519
1612
  ctx.addSystemMessage(await ctx.switchModel(args));
1520
1613
  }
1521
1614
  }
1522
1615
  });
1523
- registry.register({
1616
+ register({
1524
1617
  name: "resume",
1525
- description: "List sessions or resume a session by id",
1618
+ description: "Resume a session by id, or pick one from a list (--all for every directory)",
1526
1619
  usage: "/resume [sessionId | --all]",
1620
+ category: "Sessions",
1527
1621
  async run(args, ctx) {
1528
1622
  const arg = args.trim();
1529
1623
  if (arg === "--all") {
1530
- ctx.addSystemMessage(await ctx.listSessions(true));
1624
+ ctx.addSystemMessage(await ctx.pickSession(true));
1531
1625
  } else if (!arg) {
1532
- ctx.addSystemMessage(await ctx.listSessions());
1626
+ ctx.addSystemMessage(await ctx.pickSession(false));
1533
1627
  } else {
1534
1628
  ctx.addSystemMessage(await ctx.resumeSession(arg));
1535
1629
  }
1536
1630
  }
1537
1631
  });
1538
- registry.register({
1632
+ register({
1633
+ name: "fork",
1634
+ description: "Fork the current session into a new one and switch to it",
1635
+ usage: "/fork",
1636
+ category: "Sessions",
1637
+ async run(_args, ctx) {
1638
+ ctx.addSystemMessage(await ctx.forkSession());
1639
+ }
1640
+ });
1641
+ register({
1642
+ name: "search",
1643
+ description: "Full-text search across all stored sessions",
1644
+ usage: "/search <query>",
1645
+ category: "Sessions",
1646
+ async run(args, ctx) {
1647
+ const query = args.trim();
1648
+ if (!query) {
1649
+ ctx.addSystemMessage("Usage: /search <query>");
1650
+ return;
1651
+ }
1652
+ ctx.addSystemMessage(formatSearchResults(await searchSessions(query), query));
1653
+ }
1654
+ });
1655
+ register({
1539
1656
  name: "todo",
1540
1657
  description: "Show the current todo list",
1541
1658
  usage: "/todo",
1659
+ category: "Info",
1542
1660
  async run(_args, ctx) {
1543
1661
  ctx.addSystemMessage(await ctx.showTodos());
1544
1662
  }
1545
1663
  });
1546
- registry.register({
1664
+ register({
1547
1665
  name: "tasks",
1548
1666
  description: "List background shell tasks",
1549
1667
  usage: "/tasks",
1668
+ category: "Info",
1550
1669
  run(_args, ctx) {
1551
1670
  ctx.addSystemMessage(ctx.listTasks());
1552
1671
  }
1553
1672
  });
1554
- registry.register({
1673
+ register({
1555
1674
  name: "cost",
1556
1675
  description: "Show API token usage for this session",
1557
1676
  usage: "/cost",
1677
+ category: "Info",
1558
1678
  run(_args, ctx) {
1559
1679
  ctx.addSystemMessage(ctx.showUsage());
1560
1680
  }
1561
1681
  });
1562
- registry.register({
1682
+ register({
1563
1683
  name: "usage",
1564
1684
  description: "Show token usage aggregated across all sessions",
1565
1685
  usage: "/usage",
1686
+ category: "Info",
1566
1687
  async run(_args, ctx) {
1567
1688
  ctx.addSystemMessage(await ctx.showGlobalUsage());
1568
1689
  }
1569
1690
  });
1570
- registry.register({
1691
+ register({
1571
1692
  name: "config",
1572
1693
  description: "Show the current configuration",
1573
1694
  usage: "/config",
1695
+ category: "Info",
1574
1696
  run(_args, ctx) {
1575
1697
  ctx.addSystemMessage(ctx.describeConfig());
1576
1698
  }
1577
1699
  });
1578
- registry.register({
1700
+ register({
1579
1701
  name: "compact",
1580
1702
  description: "Compact the conversation history to free up context",
1581
1703
  usage: "/compact",
1704
+ category: "Sessions",
1582
1705
  async run(_args, ctx) {
1583
1706
  ctx.addSystemMessage(await ctx.compactContext());
1584
1707
  }
1585
1708
  });
1586
- registry.register({
1709
+ register({
1587
1710
  name: "export",
1588
1711
  description: "Export the current session to a Markdown file",
1589
1712
  usage: "/export [path]",
1713
+ category: "Sessions",
1590
1714
  async run(args, ctx) {
1591
1715
  ctx.addSystemMessage(await ctx.exportSession(args));
1592
1716
  }
1593
1717
  });
1594
- registry.register({
1718
+ register({
1595
1719
  name: "permission",
1596
- description: "Show or set the global permission mode (ask | auto | readonly | yolo)",
1720
+ description: "Set the permission mode (opens a picker when no mode is given)",
1597
1721
  usage: "/permission [ask|auto|readonly|yolo]",
1722
+ category: "Settings",
1598
1723
  async run(args, ctx) {
1599
- ctx.addSystemMessage(await ctx.permissionMode(args.trim()));
1724
+ const arg = args.trim();
1725
+ if (!arg) {
1726
+ ctx.addSystemMessage(await ctx.pickPermissionMode());
1727
+ } else {
1728
+ ctx.addSystemMessage(await ctx.permissionMode(arg));
1729
+ }
1600
1730
  }
1601
1731
  });
1602
- registry.register({
1732
+ register({
1603
1733
  name: "plan",
1604
1734
  description: "Toggle plan mode: read-only research, then approve the plan before executing",
1605
1735
  usage: "/plan",
1736
+ category: "Settings",
1606
1737
  async run(_args, ctx) {
1607
1738
  ctx.addSystemMessage(await ctx.planMode());
1608
1739
  }
1609
1740
  });
1610
- registry.register({
1741
+ register({
1742
+ name: "memory",
1743
+ description: "Show user memory (MEMORY.md), or add an entry",
1744
+ usage: "/memory [add <text>]",
1745
+ category: "Settings",
1746
+ run(args, ctx) {
1747
+ const arg = args.trim();
1748
+ if (arg === "add" || arg.startsWith("add ")) {
1749
+ const text = arg.slice(3).trim();
1750
+ if (!text) {
1751
+ ctx.addSystemMessage("Usage: /memory add <text>");
1752
+ return;
1753
+ }
1754
+ appendUserMemory(text);
1755
+ ctx.addSystemMessage(`Saved to user memory: - ${text}`);
1756
+ return;
1757
+ }
1758
+ if (arg) {
1759
+ ctx.addSystemMessage(`Unknown argument "${arg}". Usage: /memory [add <text>]`);
1760
+ return;
1761
+ }
1762
+ const file = userMemoryPath();
1763
+ let content = "";
1764
+ try {
1765
+ content = readFileSync(file, "utf8").trim();
1766
+ } catch {
1767
+ content = "";
1768
+ }
1769
+ if (!content) {
1770
+ ctx.addSystemMessage(
1771
+ `No user memory yet (${file}).
1772
+ Add entries with /memory add <text> or ask the agent to remember something.`
1773
+ );
1774
+ return;
1775
+ }
1776
+ ctx.addSystemMessage(`User memory (${file}):
1777
+ ${content}`);
1778
+ }
1779
+ });
1780
+ register({
1611
1781
  name: "undo",
1612
1782
  description: "Undo the last conversation turn: revert its file changes (write_file/edit_file) and retract its messages",
1613
1783
  usage: "/undo",
1784
+ category: "Changes",
1614
1785
  async run(_args, ctx) {
1615
1786
  ctx.addSystemMessage(await ctx.undo());
1616
1787
  }
1617
1788
  });
1618
- registry.register({
1789
+ register({
1619
1790
  name: "rewind",
1620
1791
  description: "List file-change checkpoints, or rewind to just before one: restore files and retract the conversation",
1621
1792
  usage: "/rewind [n]",
1793
+ category: "Changes",
1622
1794
  async run(args, ctx) {
1623
1795
  ctx.addSystemMessage(await ctx.rewind(args.trim()));
1624
1796
  }
1625
1797
  });
1626
- registry.register({
1798
+ register({
1627
1799
  name: "init",
1628
1800
  description: "Generate an AGENTS.md for the current project",
1629
1801
  usage: "/init [force]",
1802
+ category: "Settings",
1630
1803
  async run(args, ctx) {
1631
1804
  ctx.addSystemMessage(await ctx.initProject(args));
1632
1805
  }
1633
1806
  });
1634
- registry.register({
1807
+ register({
1635
1808
  name: "doctor",
1636
1809
  description: "Run environment and configuration checks",
1637
1810
  usage: "/doctor",
1811
+ category: "Info",
1638
1812
  async run(_args, ctx) {
1639
1813
  ctx.addSystemMessage(await ctx.runDoctor());
1640
1814
  }
1641
1815
  });
1642
- registry.register({
1816
+ register({
1643
1817
  name: "skills",
1644
1818
  description: "List available skills (project scope overrides user scope)",
1645
1819
  usage: "/skills",
1820
+ category: "Info",
1646
1821
  run(_args, ctx) {
1647
1822
  const skills = discoverSkills(ctx.cwd ?? process.cwd());
1648
1823
  if (skills.length === 0) {
@@ -1656,10 +1831,11 @@ ${lines.join("\n")}`);
1656
1831
  ${lines.join("\n")}`);
1657
1832
  }
1658
1833
  });
1659
- registry.register({
1834
+ register({
1660
1835
  name: "commit",
1661
1836
  description: "Analyze uncommitted changes and create a git commit (Conventional Commits)",
1662
1837
  usage: "/commit [instructions]",
1838
+ category: "Changes",
1663
1839
  async run(args, ctx) {
1664
1840
  const cwd = ctx.cwd ?? process.cwd();
1665
1841
  if (!isGitRepo(cwd)) {
@@ -1678,10 +1854,11 @@ ${lines.join("\n")}`);
1678
1854
  ctx.addSystemMessage("/commit: this context cannot submit prompts to the model.");
1679
1855
  }
1680
1856
  });
1681
- registry.register({
1857
+ register({
1682
1858
  name: "copy",
1683
1859
  description: "Copy the last assistant reply to the clipboard (`all` for the whole conversation)",
1684
1860
  usage: "/copy [all]",
1861
+ category: "Sessions",
1685
1862
  async run(args, ctx) {
1686
1863
  const arg = args.trim();
1687
1864
  if (arg !== "" && arg !== "all") {
@@ -1706,10 +1883,11 @@ ${lines.join("\n")}`);
1706
1883
  );
1707
1884
  }
1708
1885
  });
1709
- registry.register({
1886
+ register({
1710
1887
  name: "diff",
1711
1888
  description: "Show uncommitted changes (git status + colored diff)",
1712
1889
  usage: "/diff",
1890
+ category: "Changes",
1713
1891
  run(_args, ctx) {
1714
1892
  const cwd = ctx.cwd ?? process.cwd();
1715
1893
  if (!isGitRepo(cwd)) {
@@ -1745,7 +1923,7 @@ ${result.diff}${note ? `
1745
1923
  }
1746
1924
 
1747
1925
  // src/cli/commands/custom.ts
1748
- import { readFileSync, readdirSync } from "fs";
1926
+ import { readFileSync as readFileSync2, readdirSync } from "fs";
1749
1927
  import path11 from "path";
1750
1928
  var NAME_PATTERN = /^[a-z0-9-]+$/;
1751
1929
  var DESCRIPTION_PATTERN = /^<!--\s*description:\s*(.+?)\s*-->\s*$/;
@@ -1753,7 +1931,7 @@ var DEFAULT_DESCRIPTION = "Custom prompt command";
1753
1931
  function parseCommandFile(filePath, name) {
1754
1932
  let raw;
1755
1933
  try {
1756
- raw = readFileSync(filePath, "utf8");
1934
+ raw = readFileSync2(filePath, "utf8");
1757
1935
  } catch {
1758
1936
  return null;
1759
1937
  }
@@ -1799,6 +1977,7 @@ function registerCustomCommands(registry, cwd, home) {
1799
1977
  name: command.name,
1800
1978
  description: command.description,
1801
1979
  usage: `/${command.name} [args]`,
1980
+ category: "Custom",
1802
1981
  run(args, ctx) {
1803
1982
  const prompt = command.template.split("$ARGUMENTS").join(args);
1804
1983
  if (ctx.submitPrompt) {
@@ -2215,6 +2394,33 @@ function formatCheckpointList(snapshots, cwd) {
2215
2394
  ].join("\n");
2216
2395
  }
2217
2396
 
2397
+ // src/cli/commands/suggest.ts
2398
+ var MAX_SUGGESTIONS = 5;
2399
+ function isSubsequence(query, target) {
2400
+ let i = 0;
2401
+ for (const ch of target) {
2402
+ if (ch === query[i]) i += 1;
2403
+ if (i === query.length) return true;
2404
+ }
2405
+ return query.length === 0;
2406
+ }
2407
+ function filterCommands(input, commands) {
2408
+ if (!input.startsWith("/")) return [];
2409
+ if (/\s/.test(input)) return [];
2410
+ const prefix = input.slice(1).toLowerCase();
2411
+ const byName = (a, b) => a.name.localeCompare(b.name);
2412
+ const prefixed = commands.filter((cmd) => cmd.name.toLowerCase().startsWith(prefix)).sort(byName);
2413
+ const fuzzy = commands.filter(
2414
+ (cmd) => !cmd.name.toLowerCase().startsWith(prefix) && isSubsequence(prefix, cmd.name.toLowerCase())
2415
+ ).sort(byName);
2416
+ return [...prefixed, ...fuzzy].slice(0, MAX_SUGGESTIONS);
2417
+ }
2418
+ function didYouMeanSuffix(names) {
2419
+ if (names.length === 0) return "";
2420
+ const listed = names.slice(0, 3).map((name) => `/${name}`);
2421
+ return ` Did you mean: ${listed.join(", ")}?`;
2422
+ }
2423
+
2218
2424
  // src/cli/components/ExitPlanPrompt.tsx
2219
2425
  import { Box, Text, useInput } from "ink";
2220
2426
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -2235,15 +2441,6 @@ function ExitPlanPrompt({ onDecision }) {
2235
2441
  import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
2236
2442
  import { useEffect, useRef, useState } from "react";
2237
2443
 
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
2444
  // src/cli/path-suggest.ts
2248
2445
  import { readdir } from "fs/promises";
2249
2446
  import path15 from "path";
@@ -2264,6 +2461,7 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
2264
2461
  const dirAbs = path15.resolve(cwd, dirPart === "" ? "." : dirPart);
2265
2462
  const entries = await readdir(dirAbs, { withFileTypes: true }).catch(() => null);
2266
2463
  if (!entries) return [];
2464
+ const ignore = createIgnorePredicate(cwd);
2267
2465
  const lower = basePrefix.toLowerCase();
2268
2466
  const matches = [];
2269
2467
  let scanned = 0;
@@ -2275,6 +2473,7 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
2275
2473
  if (isDir && SKIP_DIRS.has(entry.name)) continue;
2276
2474
  if (entry.name.startsWith(".") && !basePrefix.startsWith(".")) continue;
2277
2475
  if (!entry.name.toLowerCase().startsWith(lower)) continue;
2476
+ if (ignore?.(path15.join(dirAbs, entry.name), isDir)) continue;
2278
2477
  matches.push({ path: `${dirPart}${entry.name}${isDir ? "/" : ""}`, isDir });
2279
2478
  }
2280
2479
  matches.sort((a, b) => a.path.localeCompare(b.path));
@@ -2289,6 +2488,7 @@ function InputBox({
2289
2488
  commands,
2290
2489
  cwd,
2291
2490
  refill,
2491
+ initialHistory,
2292
2492
  onSubmit,
2293
2493
  onInterrupt,
2294
2494
  onExit,
@@ -2296,7 +2496,7 @@ function InputBox({
2296
2496
  }) {
2297
2497
  const [value, setValue] = useState("");
2298
2498
  const [cursor, setCursor] = useState(0);
2299
- const [history, setHistory] = useState([]);
2499
+ const [history, setHistory] = useState(() => initialHistory ?? []);
2300
2500
  const [highlight, setHighlight] = useState(0);
2301
2501
  const [suggestionsDismissed, setSuggestionsDismissed] = useState(false);
2302
2502
  const [pathSuggestions, setPathSuggestions] = useState([]);
@@ -2379,6 +2579,16 @@ function InputBox({
2379
2579
  return;
2380
2580
  }
2381
2581
  if (key.return) {
2582
+ if (key.shift) {
2583
+ edit(`${value.slice(0, cursor)}
2584
+ ${value.slice(cursor)}`, cursor + 1);
2585
+ return;
2586
+ }
2587
+ if (value[cursor - 1] === "\\") {
2588
+ edit(`${value.slice(0, cursor - 1)}
2589
+ ${value.slice(cursor)}`, cursor);
2590
+ return;
2591
+ }
2382
2592
  const text = value.trim();
2383
2593
  if (text.length > 0) {
2384
2594
  setHistory((prev) => [...prev, text]);
@@ -2389,6 +2599,11 @@ function InputBox({
2389
2599
  draftRef.current = "";
2390
2600
  return;
2391
2601
  }
2602
+ if (input === "\n" || input === "\r" || /^\[13;\d+u$/.test(input)) {
2603
+ edit(`${value.slice(0, cursor)}
2604
+ ${value.slice(cursor)}`, cursor + 1);
2605
+ return;
2606
+ }
2392
2607
  if (key.tab) {
2393
2608
  if (key.shift) return;
2394
2609
  if (suggestionCount > 0) {
@@ -2465,25 +2680,25 @@ function InputBox({
2465
2680
  return;
2466
2681
  }
2467
2682
  if (input && !key.ctrl && !key.meta) {
2468
- edit(value.slice(0, cursor) + input + value.slice(cursor), cursor + input.length);
2683
+ const text = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
2684
+ edit(value.slice(0, cursor) + text + value.slice(cursor), cursor + text.length);
2469
2685
  }
2470
2686
  });
2471
2687
  const before = value.slice(0, cursor);
2472
- const at = value[cursor] ?? " ";
2688
+ const at = value[cursor];
2473
2689
  const after = value.slice(cursor + 1);
2690
+ const cursorChar = at === "\n" || at === void 0 ? " " : at;
2691
+ const tail = (at === "\n" ? "\n" : "") + after;
2692
+ const styled = `\x1B[36m> \x1B[39m${before}\x1B[7m${cursorChar}\x1B[27m${tail}`;
2474
2693
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
2475
- /* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
2476
- /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "> " }),
2477
- /* @__PURE__ */ jsx2(Text2, { children: before }),
2478
- /* @__PURE__ */ jsx2(Text2, { inverse: true, children: at }),
2479
- /* @__PURE__ */ jsx2(Text2, { children: after })
2480
- ] }),
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}` }),
2694
+ /* @__PURE__ */ jsx2(Box2, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { children: styled }) }),
2695
+ suggestions.length > 0 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: suggestions.map((cmd, index) => {
2696
+ const label = cmd.usage ?? `/${cmd.name}`;
2697
+ return index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `${label} - ${cmd.description}` }, cmd.name) : /* @__PURE__ */ jsxs2(Text2, { children: [
2698
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: label }),
2484
2699
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` - ${cmd.description}` })
2485
- ] }, cmd.name)
2486
- ) }),
2700
+ ] }, cmd.name);
2701
+ }) }),
2487
2702
  showPathSuggestions && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: pathSuggestions.map(
2488
2703
  (sugg, index) => index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `@${sugg.path}` }, sugg.path) : /* @__PURE__ */ jsxs2(Text2, { children: [
2489
2704
  /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: `@${sugg.path}` }),
@@ -2591,10 +2806,77 @@ function RewindConfirmPrompt({ summary, onDecision }) {
2591
2806
  ] });
2592
2807
  }
2593
2808
 
2809
+ // src/cli/components/SelectPrompt.tsx
2810
+ import { Box as Box6, Text as Text7, useInput as useInput5 } from "ink";
2811
+ import { useRef as useRef2, useState as useState2 } from "react";
2812
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2813
+ var SELECT_PROMPT_MAX_VISIBLE = 9;
2814
+ function SelectPrompt({ title, options, onSelect, onCancel }) {
2815
+ const [index, setIndex] = useState2(0);
2816
+ const indexRef = useRef2(0);
2817
+ const move = (delta) => {
2818
+ indexRef.current = Math.max(0, Math.min(indexRef.current + delta, options.length - 1));
2819
+ setIndex(indexRef.current);
2820
+ };
2821
+ useInput5((input, key) => {
2822
+ if (key.escape) {
2823
+ onCancel();
2824
+ return;
2825
+ }
2826
+ if (key.return) {
2827
+ const option = options[indexRef.current];
2828
+ if (option) onSelect(option.value);
2829
+ return;
2830
+ }
2831
+ if (key.downArrow || input === "j") {
2832
+ move(1);
2833
+ } else if (key.upArrow || input === "k") {
2834
+ move(-1);
2835
+ }
2836
+ });
2837
+ const maxStart = Math.max(0, options.length - SELECT_PROMPT_MAX_VISIBLE);
2838
+ const start = Math.min(Math.max(0, index - Math.floor(SELECT_PROMPT_MAX_VISIBLE / 2)), maxStart);
2839
+ const visible = options.slice(start, start + SELECT_PROMPT_MAX_VISIBLE);
2840
+ const above = start;
2841
+ const below = options.length - start - visible.length;
2842
+ return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
2843
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: title }),
2844
+ above > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2845
+ "\u2026 ",
2846
+ above,
2847
+ " more above"
2848
+ ] }),
2849
+ visible.map((option, i) => {
2850
+ const active = start + i === index;
2851
+ return /* @__PURE__ */ jsxs7(Text7, { children: [
2852
+ /* @__PURE__ */ jsxs7(Text7, { bold: active, inverse: active, children: [
2853
+ active ? "\u276F " : " ",
2854
+ option.label
2855
+ ] }),
2856
+ option.hint && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2857
+ " (",
2858
+ option.hint,
2859
+ ")"
2860
+ ] }),
2861
+ option.description && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2862
+ " \u2014 ",
2863
+ option.description
2864
+ ] })
2865
+ ] }, option.value);
2866
+ }),
2867
+ below > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2868
+ "\u2026 ",
2869
+ below,
2870
+ " more below"
2871
+ ] }),
2872
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "[\u2191/\u2193 or j/k] move [Enter] select [Esc] cancel" })
2873
+ ] });
2874
+ }
2875
+
2594
2876
  // src/cli/components/StatusBar.tsx
2595
- import { Box as Box6, Text as Text7, useStdout } from "ink";
2877
+ import { Box as Box7, Text as Text8, useStdout } from "ink";
2596
2878
  import { memo as memo2 } from "react";
2597
- import { jsxs as jsxs7 } from "react/jsx-runtime";
2879
+ import { jsxs as jsxs8 } from "react/jsx-runtime";
2598
2880
  var BG_LABEL_MAX = 40;
2599
2881
  var WIDE_COLUMNS = 120;
2600
2882
  function cwdLabel(cwd, wide) {
@@ -2616,36 +2898,36 @@ var StatusBar = memo2(function StatusBar2({
2616
2898
  const wide = (stdout?.columns ?? 80) >= WIDE_COLUMNS;
2617
2899
  const joined = backgroundTasks.join(", ");
2618
2900
  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: [
2901
+ return /* @__PURE__ */ jsxs8(Box7, { justifyContent: "space-between", children: [
2902
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2621
2903
  cwdLabel(cwd, wide),
2622
2904
  gitBranch ? ` [${gitBranch}]` : ""
2623
2905
  ] }),
2624
- backgroundTasks.length > 0 && /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
2906
+ backgroundTasks.length > 0 && /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
2625
2907
  "bg: ",
2626
2908
  backgroundTasks.length,
2627
2909
  " (",
2628
2910
  labels,
2629
2911
  ")"
2630
2912
  ] }),
2631
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2913
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2632
2914
  "model: ",
2633
2915
  model
2634
2916
  ] }),
2635
- contextPercent != null && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2917
+ contextPercent != null && /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2636
2918
  "ctx: ",
2637
2919
  contextPercent,
2638
2920
  "%"
2639
2921
  ] }),
2640
- sessionCostUsd != null && sessionCostUsd > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2922
+ sessionCostUsd != null && sessionCostUsd > 0 && /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2641
2923
  "$",
2642
2924
  formatDollars(sessionCostUsd)
2643
2925
  ] }),
2644
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2926
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2645
2927
  tokens2,
2646
2928
  " tokens"
2647
2929
  ] }),
2648
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2930
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2649
2931
  "mode: ",
2650
2932
  permissionMode
2651
2933
  ] })
@@ -2653,22 +2935,22 @@ var StatusBar = memo2(function StatusBar2({
2653
2935
  });
2654
2936
 
2655
2937
  // src/cli/components/StreamingMessage.tsx
2656
- import { Box as Box7, Text as Text8 } from "ink";
2938
+ import { Box as Box8, Text as Text9 } from "ink";
2657
2939
  import { memo as memo3 } from "react";
2658
- import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
2940
+ import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
2659
2941
  var StreamingMessage = memo3(function StreamingMessage2({
2660
2942
  text,
2661
2943
  continuation
2662
2944
  }) {
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 })
2945
+ return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", marginBottom: 1, children: [
2946
+ !continuation && /* @__PURE__ */ jsx8(Text9, { bold: true, color: "green", children: "star" }),
2947
+ /* @__PURE__ */ jsx8(Text9, { color: "green", children: text })
2666
2948
  ] });
2667
2949
  });
2668
2950
 
2669
2951
  // src/cli/components/ThinkingIndicator.tsx
2670
- import { Box as Box8, Text as Text9 } from "ink";
2671
- import { jsxs as jsxs9 } from "react/jsx-runtime";
2952
+ import { Box as Box9, Text as Text10 } from "ink";
2953
+ import { jsxs as jsxs10 } from "react/jsx-runtime";
2672
2954
  var SPINNER_FRAMES = ["-", "\\", "|", "/"];
2673
2955
  var REASONING_TAIL_LENGTH = 200;
2674
2956
  var THOUGHT_SUMMARY_LENGTH = 100;
@@ -2684,19 +2966,44 @@ function ThinkingIndicator({
2684
2966
  }) {
2685
2967
  const tail = reasoning ? truncateTail(reasoning, REASONING_TAIL_LENGTH) : "";
2686
2968
  const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
2687
- return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", children: [
2688
- /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
2969
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
2970
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2689
2971
  spinner,
2690
2972
  " ",
2691
2973
  activity ?? "star is thinking\u2026"
2692
2974
  ] }),
2693
- tail !== "" && /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
2975
+ tail !== "" && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2694
2976
  " ",
2695
2977
  tail
2696
2978
  ] })
2697
2979
  ] });
2698
2980
  }
2699
2981
 
2982
+ // src/cli/components/TodoPanel.tsx
2983
+ import { Box as Box10, Text as Text11 } from "ink";
2984
+ import { jsx as jsx9, jsxs as jsxs11 } from "react/jsx-runtime";
2985
+ var SYMBOLS = {
2986
+ pending: "[ ]",
2987
+ in_progress: "[~]",
2988
+ done: "[x]"
2989
+ };
2990
+ function TodoPanel({ todos }) {
2991
+ if (todos.length === 0) return null;
2992
+ return /* @__PURE__ */ jsx9(Box10, { flexDirection: "column", children: todos.map((todo) => /* @__PURE__ */ jsxs11(
2993
+ Text11,
2994
+ {
2995
+ dimColor: todo.status === "done",
2996
+ color: todo.status === "in_progress" ? "cyan" : void 0,
2997
+ children: [
2998
+ SYMBOLS[todo.status],
2999
+ " ",
3000
+ todo.title
3001
+ ]
3002
+ },
3003
+ todo.id
3004
+ )) });
3005
+ }
3006
+
2700
3007
  // src/cli/components/ToolCallCard.tsx
2701
3008
  function formatToolCard(card) {
2702
3009
  const header = `${card.name} ${card.argsSummary}${card.isError ? " [error]" : ""}`;
@@ -2712,6 +3019,51 @@ function isDoubleEscape(previous, now, windowMs = DOUBLE_ESC_WINDOW_MS) {
2712
3019
  return previous !== null && now - previous <= windowMs;
2713
3020
  }
2714
3021
 
3022
+ // src/cli/history.ts
3023
+ import fs10 from "fs";
3024
+ import path16 from "path";
3025
+ var DEFAULT_LIMIT = 500;
3026
+ var MAX_FILE_LINES = 1e3;
3027
+ var TRIM_TO_LINES = 500;
3028
+ function historyPath() {
3029
+ return path16.join(starHome(), "history");
3030
+ }
3031
+ function readLines(file) {
3032
+ const raw = fs10.readFileSync(file, "utf8");
3033
+ return raw.split("\n").map((line) => line.replace(/\r$/, "")).filter((line) => line.trim().length > 0);
3034
+ }
3035
+ function loadHistory(limit = DEFAULT_LIMIT) {
3036
+ try {
3037
+ return readLines(historyPath()).slice(-limit);
3038
+ } catch {
3039
+ return [];
3040
+ }
3041
+ }
3042
+ function appendHistory(entry) {
3043
+ const line = entry.replace(/[\r\n]+/g, " ").trim();
3044
+ if (line.length === 0) return;
3045
+ try {
3046
+ const file = historyPath();
3047
+ let existing = [];
3048
+ try {
3049
+ existing = readLines(file);
3050
+ } catch {
3051
+ }
3052
+ if (existing[existing.length - 1] === line) return;
3053
+ if (existing.length >= MAX_FILE_LINES) {
3054
+ const kept = [...existing.slice(-(TRIM_TO_LINES - 1)), line];
3055
+ fs10.mkdirSync(path16.dirname(file), { recursive: true });
3056
+ fs10.writeFileSync(file, `${kept.join("\n")}
3057
+ `);
3058
+ return;
3059
+ }
3060
+ fs10.mkdirSync(path16.dirname(file), { recursive: true });
3061
+ fs10.appendFileSync(file, `${line}
3062
+ `);
3063
+ } catch {
3064
+ }
3065
+ }
3066
+
2715
3067
  // src/cli/notify.ts
2716
3068
  var BELL = "\x07";
2717
3069
  function shouldNotifyBell(input, isTTY) {
@@ -2827,18 +3179,30 @@ async function checkForUpdate(currentVersion, fetcher = defaultFetcher2) {
2827
3179
  }
2828
3180
 
2829
3181
  // src/cli/repl.tsx
2830
- import { jsx as jsx8, jsxs as jsxs10 } from "react/jsx-runtime";
3182
+ import { jsx as jsx10, jsxs as jsxs12 } from "react/jsx-runtime";
2831
3183
  var CLEAR_TERMINAL = process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H";
2832
3184
  function emptyUsage() {
2833
3185
  return { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
2834
3186
  }
2835
3187
  function runningTaskLabels() {
2836
- return defaultTaskManager.list().filter((t) => t.status === "running").map((t) => t.description ?? t.command);
3188
+ const shells = defaultTaskManager.list().filter((t) => t.status === "running").map((t) => t.description ?? t.command);
3189
+ const agents = defaultAgentTasks.list().filter((t) => t.status === "running").map((t) => t.description ?? t.id);
3190
+ return [...shells, ...agents];
2837
3191
  }
2838
3192
  var SESSION_MODE_CYCLE = ["ask", "auto", "readonly", "plan"];
2839
3193
  function formatUsage(usage) {
2840
3194
  return `API usage this session: ${usage.requests} requests, ${usage.promptTokens} prompt + ${usage.completionTokens} completion = ${usage.totalTokens} tokens`;
2841
3195
  }
3196
+ var SESSION_PICKER_CAP = 20;
3197
+ async function sessionPreview(id) {
3198
+ const store = await SessionStore.open(id);
3199
+ if (!store) return null;
3200
+ const first = (await store.messages()).find((m) => m.role === "user");
3201
+ if (!first) return null;
3202
+ const text = coreMessageText(first).replace(/\s+/g, " ").trim();
3203
+ if (!text) return null;
3204
+ return text.length > 60 ? `${text.slice(0, 60)}\u2026` : text;
3205
+ }
2842
3206
  function Repl({
2843
3207
  backend,
2844
3208
  model,
@@ -2850,55 +3214,59 @@ function Repl({
2850
3214
  initialUsage
2851
3215
  }) {
2852
3216
  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);
2870
- const backendRef = useRef2(backend);
2871
- const nextIdRef = useRef2(initialDisplay.length);
2872
- const abortRef = useRef2(null);
2873
- const streamedRef = useRef2("");
2874
- const thinkingRef = useRef2(false);
2875
- const reasoningRef = useRef2("");
2876
- const tickerStopRef = useRef2(null);
2877
- const flushedRef = useRef2({ streamed: "", reasoning: "" });
2878
- const toolCardsRef = useRef2(/* @__PURE__ */ new Map());
2879
- const pendingRef = useRef2(null);
2880
- const alwaysAllowedRef = useRef2(/* @__PURE__ */ new Set());
2881
- const prevModeRef = useRef2(null);
2882
- const planApprovalRef = useRef2(false);
2883
- const pendingRewindRef = useRef2(null);
2884
- const modelNameRef = useRef2(model);
2885
- const sessionStoreRef = useRef2(sessionStore);
2886
- const usageRef = useRef2(initialUsage ? { ...initialUsage } : emptyUsage());
2887
- const turnChunksRef = useRef2(0);
2888
- const messagesRef = useRef2(initialDisplay);
2889
- const [queueItems, setQueueItems] = useState2([]);
2890
- const queueRef = useRef2(new PromptQueue());
2891
- const [pendingImages, setPendingImages] = useState2([]);
2892
- const pendingImagesRef = useRef2([]);
2893
- const imageSeqRef = useRef2(1);
2894
- const [inputRefill, setInputRefill] = useState2();
2895
- const refillSeqRef = useRef2(0);
2896
- const lastEscRef = useRef2(null);
2897
- const [gitBranch, setGitBranch] = useState2(null);
2898
- const [contextPercent, setContextPercent] = useState2(null);
2899
- const budgetWarnedRef = useRef2(false);
2900
- const budgetExceededRef = useRef2(false);
2901
- const runStreamRef = useRef2(
3217
+ const [initialDisplay] = useState3(() => buildDisplayMessages(initialMessages ?? []));
3218
+ const [messages, setMessages] = useState3(initialDisplay);
3219
+ const [epoch, setEpoch] = useState3(0);
3220
+ const [streamingText, setStreamingText] = useState3(null);
3221
+ const [isStreaming, setIsStreaming] = useState3(false);
3222
+ const [thinking, setThinking] = useState3(false);
3223
+ const [thinkingText, setThinkingText] = useState3("");
3224
+ const [thoughtSummary, setThoughtSummary] = useState3(null);
3225
+ const [, setUsageVersion] = useState3(0);
3226
+ const [modelName, setModelName] = useState3(model);
3227
+ const [pending, setPending] = useState3(null);
3228
+ const [spinnerTick, setSpinnerTick] = useState3(0);
3229
+ const [activity, setActivity] = useState3(null);
3230
+ const [bgLabels, setBgLabels] = useState3(() => runningTaskLabels());
3231
+ const [todos, setTodos] = useState3([]);
3232
+ const [permissionModeState, setPermissionModeState] = useState3(permissionMode);
3233
+ const [planApproval, setPlanApprovalState] = useState3(false);
3234
+ const [pendingRewind, setPendingRewindState] = useState3(null);
3235
+ const [picker, setPickerState] = useState3(null);
3236
+ const backendRef = useRef3(backend);
3237
+ const nextIdRef = useRef3(initialDisplay.length);
3238
+ const abortRef = useRef3(null);
3239
+ const streamedRef = useRef3("");
3240
+ const thinkingRef = useRef3(false);
3241
+ const reasoningRef = useRef3("");
3242
+ const tickerStopRef = useRef3(null);
3243
+ const flushedRef = useRef3({ streamed: "", reasoning: "" });
3244
+ const toolCardsRef = useRef3(/* @__PURE__ */ new Map());
3245
+ const pendingRef = useRef3(null);
3246
+ const alwaysAllowedRef = useRef3(/* @__PURE__ */ new Set());
3247
+ const prevModeRef = useRef3(null);
3248
+ const planApprovalRef = useRef3(false);
3249
+ const pendingRewindRef = useRef3(null);
3250
+ const pickerRef = useRef3(null);
3251
+ const modelNameRef = useRef3(model);
3252
+ const sessionStoreRef = useRef3(sessionStore);
3253
+ const usageRef = useRef3(initialUsage ? { ...initialUsage } : emptyUsage());
3254
+ const turnChunksRef = useRef3(0);
3255
+ const messagesRef = useRef3(initialDisplay);
3256
+ const [queueItems, setQueueItems] = useState3([]);
3257
+ const queueRef = useRef3(new PromptQueue());
3258
+ const [pendingImages, setPendingImages] = useState3([]);
3259
+ const pendingImagesRef = useRef3([]);
3260
+ const imageSeqRef = useRef3(1);
3261
+ const [inputRefill, setInputRefill] = useState3();
3262
+ const refillSeqRef = useRef3(0);
3263
+ const lastEscRef = useRef3(null);
3264
+ const [gitBranch, setGitBranch] = useState3(null);
3265
+ const [contextPercent, setContextPercent] = useState3(null);
3266
+ const [initialHistory] = useState3(() => loadHistory());
3267
+ const budgetWarnedRef = useRef3(false);
3268
+ const budgetExceededRef = useRef3(false);
3269
+ const runStreamRef = useRef3(
2902
3270
  null
2903
3271
  );
2904
3272
  const pushMessage = useCallback(
@@ -2945,6 +3313,34 @@ function Repl({
2945
3313
  pendingRewindRef.current = p;
2946
3314
  setPendingRewindState(p);
2947
3315
  }, []);
3316
+ const setPicker = useCallback((p) => {
3317
+ pickerRef.current = p;
3318
+ setPickerState(p);
3319
+ }, []);
3320
+ const showPicker = useCallback(
3321
+ (title, options) => {
3322
+ if (pickerRef.current) return Promise.resolve(null);
3323
+ return new Promise((resolve) => {
3324
+ setPicker({ title, options, resolve });
3325
+ });
3326
+ },
3327
+ [setPicker]
3328
+ );
3329
+ const handlePickerSelect = useCallback(
3330
+ (value) => {
3331
+ const p = pickerRef.current;
3332
+ if (!p) return;
3333
+ setPicker(null);
3334
+ p.resolve(value);
3335
+ },
3336
+ [setPicker]
3337
+ );
3338
+ const handlePickerCancel = useCallback(() => {
3339
+ const p = pickerRef.current;
3340
+ if (!p) return;
3341
+ setPicker(null);
3342
+ p.resolve(null);
3343
+ }, [setPicker]);
2948
3344
  const stageClipboardImage = useCallback((img) => {
2949
3345
  pendingImagesRef.current = [
2950
3346
  ...pendingImagesRef.current,
@@ -3019,6 +3415,10 @@ function Repl({
3019
3415
  if (message) pushMessage("system", message);
3020
3416
  });
3021
3417
  }, [pushMessage]);
3418
+ useEffect2(() => {
3419
+ loadTodos(cwd).then(setTodos).catch(() => {
3420
+ });
3421
+ }, [cwd]);
3022
3422
  useEffect2(() => {
3023
3423
  const onUpdate = (task) => {
3024
3424
  setBgLabels(runningTaskLabels());
@@ -3034,10 +3434,28 @@ function Repl({
3034
3434
  });
3035
3435
  }
3036
3436
  };
3437
+ const onAgentUpdate = (task) => {
3438
+ setBgLabels(runningTaskLabels());
3439
+ const label = task.description ? `: ${task.description}` : "";
3440
+ pushMessage(
3441
+ "system",
3442
+ task.status === "running" ? `Background subagent ${task.id} started${label}` : `Background subagent ${task.id} ${task.status}${label}`
3443
+ );
3444
+ if (task.status !== "running") {
3445
+ notifyBell({
3446
+ enabled: config.notifyBell,
3447
+ thresholdSec: config.notifyBellThresholdSec,
3448
+ noNotifyEnv: process.env.STAR_NO_NOTIFY === "1"
3449
+ });
3450
+ }
3451
+ };
3037
3452
  defaultTaskManager.on("update", onUpdate);
3453
+ defaultAgentTasks.on("update", onAgentUpdate);
3038
3454
  return () => {
3039
3455
  defaultTaskManager.off("update", onUpdate);
3456
+ defaultAgentTasks.off("update", onAgentUpdate);
3040
3457
  defaultTaskManager.cleanup();
3458
+ defaultAgentTasks.cleanup();
3041
3459
  };
3042
3460
  }, [pushMessage, config]);
3043
3461
  const interrupt = useCallback(() => {
@@ -3077,17 +3495,16 @@ function Repl({
3077
3495
  }, [pushMessage, redrawMessages]);
3078
3496
  const handleExit = useCallback(() => {
3079
3497
  const killed = defaultTaskManager.cleanup();
3080
- if (killed.length > 0) {
3081
- pushMessage(
3082
- "system",
3083
- `Stopped ${killed.length} background task(s): ${killed.map((t) => t.id).join(", ")}`
3084
- );
3498
+ const killedAgents = defaultAgentTasks.cleanup();
3499
+ const stopped = [...killed.map((t) => t.id), ...killedAgents.map((t) => t.id)];
3500
+ if (stopped.length > 0) {
3501
+ pushMessage("system", `Stopped ${stopped.length} background task(s): ${stopped.join(", ")}`);
3085
3502
  }
3086
3503
  exit();
3087
3504
  }, [exit, pushMessage]);
3088
- useInput5((_input, key) => {
3505
+ useInput6((_input, key) => {
3089
3506
  if (key.escape) {
3090
- const busy = abortRef.current !== null || pendingRef.current !== null || planApprovalRef.current || pendingRewindRef.current !== null;
3507
+ const busy = abortRef.current !== null || pendingRef.current !== null || planApprovalRef.current || pendingRewindRef.current !== null || pickerRef.current !== null;
3091
3508
  if (busy) {
3092
3509
  lastEscRef.current = null;
3093
3510
  interrupt();
@@ -3104,7 +3521,7 @@ function Repl({
3104
3521
  }
3105
3522
  lastEscRef.current = null;
3106
3523
  if (key.shift && key.tab) {
3107
- if (abortRef.current || pendingRef.current || planApprovalRef.current || pendingRewindRef.current)
3524
+ if (abortRef.current || pendingRef.current || planApprovalRef.current || pendingRewindRef.current || pickerRef.current)
3108
3525
  return;
3109
3526
  const current = config.permissionMode;
3110
3527
  const index = SESSION_MODE_CYCLE.indexOf(current);
@@ -3266,6 +3683,10 @@ function Repl({
3266
3683
  name: event.name,
3267
3684
  argsSummary: summarizeArgs(event.args)
3268
3685
  });
3686
+ if (event.name === "todo_write") {
3687
+ const list = parseTodoArgs(event.args);
3688
+ if (list) setTodos(list);
3689
+ }
3269
3690
  setActivity(`running ${event.name}: ${summarizeArgs(event.args, 60)}`);
3270
3691
  } else if (event.type === "tool-result") {
3271
3692
  const card = toolCardsRef.current.get(event.id) ?? {
@@ -3298,6 +3719,13 @@ function Repl({
3298
3719
  sessionStoreRef.current?.addUsage(event.usage).catch(() => {
3299
3720
  });
3300
3721
  }
3722
+ } else if (event.type === "retry") {
3723
+ pushMessage(
3724
+ "system",
3725
+ `Request failed, retrying (${event.attempt}/${event.maxAttempts}): ${event.reason}`
3726
+ );
3727
+ } else if (event.type === "notice") {
3728
+ pushMessage("system", event.message);
3301
3729
  } else if (event.type === "error") {
3302
3730
  pushMessage("system", `Error: ${formatStreamError(event.error)}`);
3303
3731
  }
@@ -3461,6 +3889,97 @@ ${lines.join("\n")}`;
3461
3889
  return formatSessionEntries(entries, { showCwd: all });
3462
3890
  },
3463
3891
  resumeSession: resume,
3892
+ pickModel: async () => {
3893
+ const models = listModels(config);
3894
+ if (models.length === 0) return "No models configured.";
3895
+ const options = models.map((m) => {
3896
+ const parts = [`${m.provider}/${m.model}`, `${contextWindowTokens(config, m.name)} ctx`];
3897
+ if (m.promptPrice !== void 0 && m.completionPrice !== void 0) {
3898
+ parts.push(`$${m.promptPrice}/$${m.completionPrice} per 1M tokens`);
3899
+ }
3900
+ return {
3901
+ value: m.name,
3902
+ label: m.name,
3903
+ description: parts.join(" \xB7 "),
3904
+ hint: m.name === modelNameRef.current ? "current" : void 0
3905
+ };
3906
+ });
3907
+ const picked = await showPicker("Select a model", options);
3908
+ if (!picked || picked === modelNameRef.current) {
3909
+ return `Model unchanged \u2014 still "${modelNameRef.current}".`;
3910
+ }
3911
+ return switchModel(picked);
3912
+ },
3913
+ pickPermissionMode: async () => {
3914
+ const current = config.permissionMode;
3915
+ const descriptions = {
3916
+ ask: "reads allowed; writes/exec ask for confirmation",
3917
+ auto: "everything allowed except hard-denied dangerous commands/paths",
3918
+ readonly: "read-only; all writes/exec denied",
3919
+ yolo: "allow everything, never ask (disables ALL safety checks)",
3920
+ plan: "read-only research, then approve a plan before executing (this session only)"
3921
+ };
3922
+ const options = ["ask", "auto", "readonly", "yolo", "plan"].map((m) => ({
3923
+ value: m,
3924
+ label: m,
3925
+ description: descriptions[m],
3926
+ hint: m === current ? "current" : void 0
3927
+ }));
3928
+ const picked = await showPicker("Select a permission mode", options);
3929
+ if (!picked || picked === current) {
3930
+ return `Permission mode unchanged \u2014 still ${current}.`;
3931
+ }
3932
+ if (picked === "plan") return ctx.planMode();
3933
+ return ctx.permissionMode(picked);
3934
+ },
3935
+ pickSession: async (all) => {
3936
+ const entries = (await listSessionEntries(all ? void 0 : cwd)).slice(
3937
+ 0,
3938
+ SESSION_PICKER_CAP
3939
+ );
3940
+ if (entries.length === 0) {
3941
+ return all ? "No sessions found." : "No sessions found for this directory.";
3942
+ }
3943
+ const options = await Promise.all(
3944
+ entries.map(async ({ meta, messageCount }) => {
3945
+ const parts = [`${messageCount} messages \xB7 ${relativeTime(meta.updatedAt)}`];
3946
+ const preview = await sessionPreview(meta.id);
3947
+ if (preview) parts.push(preview);
3948
+ if (all) parts.push(`[${meta.cwd}]`);
3949
+ return {
3950
+ value: meta.id,
3951
+ label: meta.title || shortSessionId(meta.id),
3952
+ description: parts.join(" \xB7 "),
3953
+ hint: meta.id === sessionStoreRef.current?.id ? "current" : void 0
3954
+ };
3955
+ })
3956
+ );
3957
+ const picked = await showPicker(
3958
+ all ? "Resume a session (all directories)" : "Resume a session",
3959
+ options
3960
+ );
3961
+ if (!picked) return "Session unchanged \u2014 picker cancelled.";
3962
+ return resume(picked);
3963
+ },
3964
+ forkSession: async () => {
3965
+ const current = backendRef.current;
3966
+ const store = sessionStoreRef.current;
3967
+ if (!(current instanceof AgentLoop) || !store) {
3968
+ return "Nothing to fork \u2014 no active session.";
3969
+ }
3970
+ const messages2 = await store.messages();
3971
+ if (messages2.length === 0) {
3972
+ return "Nothing to fork \u2014 the current session has no messages yet.";
3973
+ }
3974
+ const fork = await SessionStore.create(cwd, modelNameRef.current);
3975
+ await fork.replaceMessages(messages2);
3976
+ const oldMeta = await store.meta();
3977
+ const title = `Fork of ${oldMeta.title || shortSessionId(oldMeta.id)}`;
3978
+ await fork.setTitle(title);
3979
+ current.setSessionStore(fork);
3980
+ sessionStoreRef.current = fork;
3981
+ return `Forked into new session ${fork.id} ("${title}") and switched to it. Checkpoints and rewind history do not carry over.`;
3982
+ },
3464
3983
  newSession: async () => {
3465
3984
  const current = backendRef.current;
3466
3985
  if (!(current instanceof AgentLoop)) {
@@ -3645,6 +4164,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
3645
4164
  `maxSteps: ${config.maxSteps}`,
3646
4165
  `contextMaxTokens: ${config.contextMaxTokens}`,
3647
4166
  `streamIdleTimeoutSec: ${config.streamIdleTimeoutSec}`,
4167
+ `streamFirstChunkTimeoutSec: ${config.streamFirstChunkTimeoutSec}`,
4168
+ `streamMaxRetries: ${config.streamMaxRetries}`,
4169
+ `maxAutoContinues: ${config.maxAutoContinues}`,
3648
4170
  `permissions.allow (${config.permissions.allow.length}): ${config.permissions.allow.join(", ") || "(none)"}`,
3649
4171
  `hooks (${config.hooks.length}): ${config.hooks.map((h) => h.event).join(", ") || "(none)"}`
3650
4172
  ].join("\n")
@@ -3664,7 +4186,8 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
3664
4186
  applyMessages,
3665
4187
  redrawMessages,
3666
4188
  applySessionMode,
3667
- setPendingRewind
4189
+ setPendingRewind,
4190
+ showPicker
3668
4191
  ]);
3669
4192
  const runShellBang = useCallback(
3670
4193
  async (raw) => {
@@ -3704,6 +4227,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
3704
4227
  );
3705
4228
  const handleSubmit = useCallback(
3706
4229
  (text) => {
4230
+ appendHistory(text);
3707
4231
  if (text.startsWith("!")) {
3708
4232
  if (abortRef.current) {
3709
4233
  queueRef.current.enqueue({ text, images: [] });
@@ -3718,7 +4242,11 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
3718
4242
  if (!parsed) return;
3719
4243
  const command = registry.get(parsed.name);
3720
4244
  if (!command) {
3721
- pushMessage("system", `Unknown command: /${parsed.name} (try /help)`);
4245
+ const suggestions = registry.complete(parsed.name).map((cmd) => cmd.name);
4246
+ pushMessage(
4247
+ "system",
4248
+ `Unknown command: /${parsed.name} (try /help)${didYouMeanSuffix(suggestions)}`
4249
+ );
3722
4250
  return;
3723
4251
  }
3724
4252
  void command.run(parsed.args, registry.ctx);
@@ -3744,7 +4272,11 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
3744
4272
  [registry, pushMessage, runStream, runShellBang, config, clearPendingImages, sessionCostUsd]
3745
4273
  );
3746
4274
  const commandHints = useMemo(
3747
- () => registry.list().map((cmd) => ({ name: cmd.name, description: cmd.description })),
4275
+ () => registry.list().map((cmd) => ({
4276
+ name: cmd.name,
4277
+ description: cmd.description,
4278
+ usage: cmd.usage ?? `/${cmd.name}`
4279
+ })),
3748
4280
  [registry]
3749
4281
  );
3750
4282
  useEffect2(() => {
@@ -3759,9 +4291,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
3759
4291
  }
3760
4292
  }, [cwd, config, isStreaming, epoch]);
3761
4293
  const sessionCost = sessionCostUsd();
3762
- return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
3763
- /* @__PURE__ */ jsx8(MessageList, { messages }, `messages-${epoch}`),
3764
- (thinking || activity !== null) && /* @__PURE__ */ jsx8(
4294
+ return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", children: [
4295
+ /* @__PURE__ */ jsx10(MessageList, { messages }, `messages-${epoch}`),
4296
+ (thinking || activity !== null) && /* @__PURE__ */ jsx10(
3765
4297
  ThinkingIndicator,
3766
4298
  {
3767
4299
  reasoning: thinkingText,
@@ -3769,9 +4301,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
3769
4301
  activity: activity ?? void 0
3770
4302
  }
3771
4303
  ),
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(
4304
+ !thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */ jsx10(Text12, { dimColor: true, children: thoughtSummary }),
4305
+ streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */ jsx10(StreamingMessage, { text: streamingText, continuation: turnChunksRef.current > 0 }),
4306
+ pending && /* @__PURE__ */ jsx10(
3775
4307
  PermissionPrompt,
3776
4308
  {
3777
4309
  request: pending.request,
@@ -3779,32 +4311,43 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
3779
4311
  onDecision: handleDecision
3780
4312
  }
3781
4313
  ),
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: [
4314
+ planApproval && /* @__PURE__ */ jsx10(ExitPlanPrompt, { onDecision: handlePlanDecision }),
4315
+ pendingRewind && /* @__PURE__ */ jsx10(RewindConfirmPrompt, { summary: pendingRewind.summary, onDecision: handleRewindDecision }),
4316
+ picker && /* @__PURE__ */ jsx10(
4317
+ SelectPrompt,
4318
+ {
4319
+ title: picker.title,
4320
+ options: picker.options,
4321
+ onSelect: handlePickerSelect,
4322
+ onCancel: handlePickerCancel
4323
+ }
4324
+ ),
4325
+ queueItems.map((item) => /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3785
4326
  "queued: ",
3786
4327
  item.text.length > 80 ? `${item.text.slice(0, 80)}\u2026` : item.text
3787
4328
  ] }, item.id)),
3788
- pendingImages.map((staged) => /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
4329
+ pendingImages.map((staged) => /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3789
4330
  "[image attached: ",
3790
4331
  staged.image.path,
3791
4332
  "]"
3792
4333
  ] }, staged.id)),
3793
- /* @__PURE__ */ jsx8(
4334
+ /* @__PURE__ */ jsx10(TodoPanel, { todos }),
4335
+ /* @__PURE__ */ jsx10(
3794
4336
  InputBox,
3795
4337
  {
3796
4338
  isStreaming,
3797
- disabled: pending !== null || planApproval || pendingRewind !== null,
4339
+ disabled: pending !== null || planApproval || pendingRewind !== null || picker !== null,
3798
4340
  commands: commandHints,
3799
4341
  cwd,
3800
4342
  refill: inputRefill,
4343
+ initialHistory,
3801
4344
  onSubmit: handleSubmit,
3802
4345
  onInterrupt: interrupt,
3803
4346
  onExit: handleExit,
3804
4347
  onPasteImage: handlePasteImage
3805
4348
  }
3806
4349
  ),
3807
- /* @__PURE__ */ jsx8(
4350
+ /* @__PURE__ */ jsx10(
3808
4351
  StatusBar,
3809
4352
  {
3810
4353
  cwd,
@@ -3824,7 +4367,7 @@ function renderRepl(backend, opts) {
3824
4367
  defaultTaskManager.cleanup();
3825
4368
  });
3826
4369
  return render(
3827
- /* @__PURE__ */ jsx8(
4370
+ /* @__PURE__ */ jsx10(
3828
4371
  Repl,
3829
4372
  {
3830
4373
  backend,
@@ -3841,7 +4384,7 @@ function renderRepl(backend, opts) {
3841
4384
  }
3842
4385
 
3843
4386
  // src/config/loader.ts
3844
- import fs10 from "fs";
4387
+ import fs11 from "fs";
3845
4388
  import { parse as parse2 } from "smol-toml";
3846
4389
  var PartialConfigSchema = ConfigSchema.partial();
3847
4390
  function parseTomlFile(content, filePath) {
@@ -3869,8 +4412,8 @@ function applyOverrides(config, overrides) {
3869
4412
  return merged;
3870
4413
  }
3871
4414
  function readTomlFileSync(filePath) {
3872
- if (!fs10.existsSync(filePath)) return {};
3873
- const content = fs10.readFileSync(filePath, "utf8");
4415
+ if (!fs11.existsSync(filePath)) return {};
4416
+ const content = fs11.readFileSync(filePath, "utf8");
3874
4417
  return parseTomlFile(content, filePath);
3875
4418
  }
3876
4419
  function loadConfigSync(cwd, overrides) {
@@ -3884,7 +4427,13 @@ function loadConfigSync(cwd, overrides) {
3884
4427
  var SYSTEM_PROMPT = `You are Star CLI, an AI coding agent running in the user's terminal.
3885
4428
  You help with software engineering tasks: reading, writing and editing code, running shell commands, and managing todos.
3886
4429
  Be concise and direct. Use tools when they help accomplish the task.
3887
- The working directory is the user's project root; never touch files outside it without explicit instruction.`;
4430
+ For any task with two or more steps, start by creating a todo list with todo_write and keep it updated as you progress; only mark an item completed after you have verified its result.
4431
+ The working directory is the user's project root; never touch files outside it without explicit instruction.
4432
+ For conversion, batch-processing, or other scriptable tasks, write a script file into the working directory and run it instead of pasting long inline code into the shell; once the script's output is verified, delete the script unless the user asked to keep it.
4433
+ When a tool call or command fails, read the error output, work out the cause, and try again with a corrected or alternative approach \u2014 never repeat an identical failing call without changing something.
4434
+ Never consider the task finished until you have verified the result yourself: run the tests, the build, or a check command that proves the output is correct, and if verification fails or reveals gaps, keep fixing until it passes.
4435
+ Do not end your turn while the task is still incomplete; keep going until it is done or you are genuinely blocked, and if you are blocked, state exactly what is missing.
4436
+ Never end a reply by announcing what you will do next \u2014 either do it now with tool calls, or do not mention it. Narrating future actions is not progress.`;
3888
4437
  async function createLoop(config, modelName, cwd, sessionStore) {
3889
4438
  const model = createModel(config, modelName);
3890
4439
  const registry = createDefaultRegistry();
@@ -3968,6 +4517,19 @@ async function printMode(loop, prompt, cwd, json, imagePaths) {
3968
4517
  case "finish":
3969
4518
  usage.add(event.usage);
3970
4519
  break;
4520
+ case "retry":
4521
+ if (!json)
4522
+ process.stderr.write(
4523
+ `
4524
+ [retry ${event.attempt}/${event.maxAttempts}] ${event.reason}
4525
+ `
4526
+ );
4527
+ break;
4528
+ case "notice":
4529
+ if (!json) process.stderr.write(`
4530
+ [notice] ${event.message}
4531
+ `);
4532
+ break;
3971
4533
  case "error":
3972
4534
  if (!json) process.stderr.write(`
3973
4535
  [error] ${formatStreamError(event.error)}
@@ -4001,6 +4563,7 @@ async function printMode(loop, prompt, cwd, json, imagePaths) {
4001
4563
  }
4002
4564
  if (!json) process.stdout.write("\n");
4003
4565
  defaultTaskManager.cleanup();
4566
+ defaultAgentTasks.cleanup();
4004
4567
  return exitCode;
4005
4568
  }
4006
4569
  var program = new Command();