@cryer/star-cli 0.3.7 → 0.3.9

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.
@@ -6,84 +6,100 @@ import {
6
6
  buildDisplayMessages,
7
7
  clearSessions,
8
8
  computeCostUsd,
9
- coreMessageText,
10
- createModel,
11
9
  estimateCost,
10
+ folderIcon,
12
11
  formatByteCount,
13
12
  formatDollars,
14
13
  formatElapsedSeconds,
15
14
  formatSessionEntries,
16
15
  formatStreamError,
17
16
  formatTokens,
18
- listModels,
19
17
  listSessionEntries,
20
18
  loadSessionSnapshots,
21
19
  previewLines,
22
- reasoningEffortMetadata,
23
20
  relativeTime,
24
21
  renderMarkdown,
25
22
  resolveMentions,
26
- resolveModelConfig,
27
23
  resolveSessionId,
28
24
  sessionPreview,
29
25
  shortSessionId,
30
26
  splitCommittableLines,
31
27
  summarizeArgs,
28
+ thinkingIcon,
32
29
  toTerminalSafe,
33
- upsertEnvContent
34
- } from "./chunk-4X25DNTF.js";
30
+ toolIcon
31
+ } from "./chunk-VJFWTXWN.js";
35
32
  import {
36
33
  AgentLoop,
37
- MAX_IMAGE_DIMENSION,
38
- SKIP_DIRS,
39
- TodoStore,
40
34
  WORKING_DIFF_MAX_LINES,
41
35
  appendUserMemory,
42
- bashTool,
43
36
  buildAllowRule,
44
37
  buildCommitPrompt,
45
- clearSnapshots,
46
38
  collectCommitContext,
47
39
  collectWorkingDiff,
48
40
  compactMessages,
41
+ discoverSkills,
42
+ estimateTokens,
43
+ getGitSummaryCached,
44
+ isAllowedByRules,
45
+ isDangerousCommand,
46
+ isGitRepo,
47
+ summarizeMessages
48
+ } from "./chunk-RE3NOULF.js";
49
+ import {
49
50
  contextWindowTokens,
51
+ coreMessageText,
52
+ diffTreeNames,
53
+ reconcileStarMessages,
54
+ toCoreMessages
55
+ } from "./chunk-56ZIA57D.js";
56
+ import {
57
+ createModel,
58
+ reasoningEffortMetadata
59
+ } from "./chunk-MVAMPVTA.js";
60
+ import {
61
+ listModels,
62
+ resolveModelConfig
63
+ } from "./chunk-4IRY44TG.js";
64
+ import {
50
65
  createDefaultRegistry,
66
+ formatTaskFinished,
67
+ formatTaskList,
68
+ formatTaskStarted
69
+ } from "./chunk-OIJYNQFZ.js";
70
+ import {
71
+ MAX_IMAGE_DIMENSION,
72
+ SKIP_DIRS,
73
+ TodoStore,
74
+ bashTool,
75
+ clearSnapshots,
51
76
  createIgnorePredicate,
52
77
  defaultAgentTasks,
53
78
  defaultTaskManager,
54
- diffTreeNames,
55
- discoverSkills,
56
79
  downsampleImageIfNeeded,
57
80
  envFilePath,
58
- estimateTokens,
59
81
  formatAgentTaskUpdate,
60
- formatTaskFinished,
61
- formatTaskList,
62
- formatTaskStarted,
63
82
  formatTodos,
64
- getGitSummaryCached,
65
83
  globalConfigPath,
66
84
  hydrateSnapshots,
67
- isAllowedByRules,
68
- isDangerousCommand,
69
- isGitRepo,
70
85
  listSnapshots,
71
86
  listTurnSnapshots,
72
87
  loadTodos,
73
88
  parseTodoArgs,
74
- reconcileToolCalls,
75
89
  resetTodos,
76
90
  resolveShell,
77
91
  resolveSnapshotContent,
78
92
  rewindToSnapshot,
93
+ sessionsDir,
94
+ setTodoSession,
79
95
  starHome,
80
- summarizeMessages,
96
+ upsertEnvContent,
81
97
  userMemoryPath
82
- } from "./chunk-OOOW7FC4.js";
98
+ } from "./chunk-A5PXPWHB.js";
83
99
 
84
100
  // src/cli/repl.tsx
85
101
  import { Box as Box13, Text as Text14, render, useApp } from "ink";
86
- import { useCallback, useEffect as useEffect3, useMemo, useRef as useRef6, useState as useState5 } from "react";
102
+ import { useCallback, useEffect as useEffect4, useMemo, useRef as useRef6, useState as useState7 } from "react";
87
103
 
88
104
  // src/config/save.ts
89
105
  import fs from "fs";
@@ -99,6 +115,10 @@ async function writeFileAtomic(filePath, content) {
99
115
  const tmpPath = `${filePath}.${process.pid}.tmp`;
100
116
  await fs.promises.writeFile(tmpPath, content, "utf8");
101
117
  await fs.promises.rename(tmpPath, filePath);
118
+ try {
119
+ await fs.promises.chmod(filePath, 384);
120
+ } catch {
121
+ }
102
122
  }
103
123
  function upsertPermissionRuleText(content, kind, rules, hadPermissionsTable) {
104
124
  const newLine = `${kind} = [${rules.map(tomlString).join(", ")}]`;
@@ -265,8 +285,8 @@ async function saveReasoningEffort(modelName, effort) {
265
285
  async function resumeSession(id) {
266
286
  const store = await SessionStore.open(id);
267
287
  if (!store) return null;
268
- const [meta, loaded] = await Promise.all([store.meta(), store.messages()]);
269
- const messages = reconcileToolCalls(loaded);
288
+ const [meta, loaded] = await Promise.all([store.meta(), store.starMessages()]);
289
+ const messages = reconcileStarMessages(loaded);
270
290
  if (messages.length !== loaded.length) {
271
291
  await store.replaceMessages(messages);
272
292
  }
@@ -605,18 +625,18 @@ import fs2 from "fs/promises";
605
625
  import path3 from "path";
606
626
  var MIN_COMPACT_MESSAGES = 4;
607
627
  async function compactSession(opts) {
608
- const { backend, sessionStore, config, model } = opts;
628
+ const { backend, sessionStore, config } = opts;
609
629
  if (!(backend instanceof AgentLoop)) {
610
630
  return { message: "Current backend does not support compaction.", compacted: false };
611
631
  }
612
- const messages = [...backend.getMessages()];
632
+ const messages = [...backend.getStarMessages()];
613
633
  if (messages.length < MIN_COMPACT_MESSAGES) {
614
634
  return {
615
635
  message: `Nothing to compact: history has only ${messages.length} message(s).`,
616
636
  compacted: false
617
637
  };
618
638
  }
619
- const beforeTokens = estimateTokens(messages);
639
+ const beforeTokens = estimateTokens(toCoreMessages(messages));
620
640
  const compacted = compactMessages(messages, config.contextMaxTokens, { force: true });
621
641
  if (!compacted.compacted) {
622
642
  return {
@@ -624,32 +644,49 @@ async function compactSession(opts) {
624
644
  compacted: false
625
645
  };
626
646
  }
627
- const next = await applyCompactionSummary(messages, compacted, config, model);
647
+ const next = await applyCompactionSummary(
648
+ messages,
649
+ compacted,
650
+ config,
651
+ backend.getAuxModel(),
652
+ backend.getAuxTemperature(),
653
+ opts.signal
654
+ );
655
+ if (next === null || opts.signal?.aborted) {
656
+ return { message: "Compaction cancelled \u2014 history left unchanged.", compacted: false };
657
+ }
628
658
  await backend.loadMessages(next);
629
659
  await sessionStore?.replaceMessages(next);
630
- const afterTokens = estimateTokens(next);
660
+ const coreNext = toCoreMessages(next);
661
+ const afterTokens = estimateTokens(coreNext);
631
662
  return {
632
663
  message: `Compacted context: ${messages.length} -> ${next.length} messages (~${formatTokens(beforeTokens)} -> ~${formatTokens(afterTokens)} estimated tokens).`,
633
664
  compacted: true,
634
- messages: next
665
+ messages: coreNext
635
666
  };
636
667
  }
637
- async function applyCompactionSummary(original, compacted, config, model) {
638
- if (config.contextCompaction !== "summary" || !model) {
668
+ async function applyCompactionSummary(original, compacted, config, model, temperature, signal) {
669
+ if (config.contextCompaction !== "summary") {
639
670
  return compacted.messages;
640
671
  }
641
- const headCount = compacted.messages[0]?.role === "system" ? 1 : 0;
642
- const dropped = original.slice(headCount, headCount + compacted.droppedCount);
672
+ const headCount = compacted.messages[0]?.message.role === "system" ? 1 : 0;
673
+ const dropped = toCoreMessages(original.slice(headCount, headCount + compacted.droppedCount));
643
674
  try {
644
- const summary = await summarizeMessages(dropped, model);
645
- const messages = compacted.messages.slice();
646
- messages[headCount] = {
647
- role: "user",
648
- content: `[earlier conversation summarized]
675
+ const summary = await summarizeMessages(dropped, model, signal, temperature);
676
+ if (summary.trim().length > 0) {
677
+ const messages = compacted.messages.slice();
678
+ messages[headCount] = {
679
+ message: {
680
+ role: "user",
681
+ content: `[earlier conversation summarized]
649
682
  ${summary}`
650
- };
651
- return messages;
683
+ }
684
+ };
685
+ return messages;
686
+ }
687
+ return compacted.messages;
652
688
  } catch {
689
+ if (signal?.aborted) return null;
653
690
  return compacted.messages;
654
691
  }
655
692
  }
@@ -735,7 +772,11 @@ function stringifyPart(value) {
735
772
  import { readFileSync } from "fs";
736
773
 
737
774
  // src/session/search.ts
775
+ import fs3 from "fs/promises";
776
+ import path4 from "path";
777
+ import { StringDecoder } from "string_decoder";
738
778
  var SNIPPET_LENGTH = 100;
779
+ var SEARCH_READ_CHUNK_BYTES = 64 * 1024;
739
780
  function flattenMessage(message) {
740
781
  const content = message.content;
741
782
  if (typeof content === "string") return content;
@@ -762,17 +803,113 @@ function makeSnippet(text, query) {
762
803
  start = Math.max(0, end - SNIPPET_LENGTH);
763
804
  return `${start > 0 ? "\u2026" : ""}${flat.slice(start, end)}${end < flat.length ? "\u2026" : ""}`;
764
805
  }
806
+ async function streamSessionPieces(id, onPiece) {
807
+ let handle = null;
808
+ try {
809
+ handle = await fs3.open(path4.join(sessionsDir(), id, "messages.jsonl"), "r");
810
+ const decoder = new StringDecoder("utf8");
811
+ const buffer = Buffer.alloc(SEARCH_READ_CHUNK_BYTES);
812
+ let tail = "";
813
+ let stop = false;
814
+ const pushLine = (line) => {
815
+ if (stop) return;
816
+ const trimmed = line.trim();
817
+ if (!trimmed) return;
818
+ let message;
819
+ try {
820
+ message = JSON.parse(trimmed);
821
+ } catch {
822
+ return;
823
+ }
824
+ const piece = flattenMessage(message);
825
+ if (piece !== "") stop = onPiece(piece);
826
+ };
827
+ while (!stop) {
828
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
829
+ if (bytesRead === 0) break;
830
+ const chunk = tail + decoder.write(buffer.subarray(0, bytesRead));
831
+ const lines = chunk.split("\n");
832
+ tail = lines.pop() ?? "";
833
+ for (const line of lines) pushLine(line);
834
+ }
835
+ if (!stop) pushLine(tail + decoder.end());
836
+ } catch {
837
+ } finally {
838
+ await handle?.close().catch(() => {
839
+ });
840
+ }
841
+ }
842
+ async function sessionTextContains(id, needle) {
843
+ let lowerTail = "";
844
+ let found = false;
845
+ let joined = false;
846
+ await streamSessionPieces(id, (piece) => {
847
+ const segment = joined ? `
848
+ ${piece}` : piece;
849
+ joined = true;
850
+ const haystack = lowerTail + segment.toLowerCase();
851
+ lowerTail = needle.length > 1 ? haystack.slice(1 - needle.length) : "";
852
+ if (haystack.includes(needle)) {
853
+ found = true;
854
+ return true;
855
+ }
856
+ return false;
857
+ });
858
+ return found;
859
+ }
860
+ async function buildSessionSnippet(id, needle, query) {
861
+ const parts = [];
862
+ let lowerTail = "";
863
+ let mode = needle === "" ? "window" : "scan";
864
+ let flatLength = 0;
865
+ let flatLastChar = "";
866
+ let matchEndFlat = 0;
867
+ let stoppedEarly = false;
868
+ const foldCollapsed = (segment) => {
869
+ let folded = (flatLastChar + segment).replace(/\s+/g, " ");
870
+ if (flatLength === 0) {
871
+ if (folded.startsWith(" ")) folded = folded.slice(1);
872
+ } else {
873
+ folded = folded.slice(1);
874
+ }
875
+ flatLength += folded.length;
876
+ if (folded.length > 0) flatLastChar = folded.at(-1) ?? "";
877
+ };
878
+ await streamSessionPieces(id, (piece) => {
879
+ const segment = parts.length === 0 ? piece : `
880
+ ${piece}`;
881
+ parts.push(piece);
882
+ if (mode === "scan") {
883
+ const haystack = lowerTail + segment.toLowerCase();
884
+ lowerTail = needle.length > 1 ? haystack.slice(1 - needle.length) : "";
885
+ if (!haystack.includes(needle)) return false;
886
+ const collapsed = parts.join("\n").replace(/\s+/g, " ").replace(/^ /, "");
887
+ const index = collapsed.toLowerCase().indexOf(needle);
888
+ if (index === -1) {
889
+ mode = "head";
890
+ return false;
891
+ }
892
+ mode = "window";
893
+ matchEndFlat = index + needle.length;
894
+ flatLength = collapsed.length;
895
+ flatLastChar = collapsed.at(-1) ?? "";
896
+ } else if (mode === "window") {
897
+ foldCollapsed(segment);
898
+ }
899
+ const stop = mode === "window" && flatLength > matchEndFlat + SNIPPET_LENGTH;
900
+ if (stop) stoppedEarly = true;
901
+ return stop;
902
+ });
903
+ const text = parts.join("\n");
904
+ return makeSnippet(stoppedEarly ? `${text}\0` : text, query);
905
+ }
765
906
  async function searchSessions(query, limit = 10) {
766
907
  const hits = [];
767
908
  const needle = query.toLowerCase();
768
909
  for (const meta of await SessionStore.list()) {
769
910
  if (hits.length >= limit) break;
770
- const store = await SessionStore.open(meta.id);
771
- if (!store) continue;
772
- const text = (await store.messages()).map(flattenMessage).filter((part) => part.length > 0).join("\n");
773
- if (text.toLowerCase().includes(needle)) {
774
- hits.push({ meta, snippet: makeSnippet(text, query) });
775
- }
911
+ if (needle !== "" && !await sessionTextContains(meta.id, needle)) continue;
912
+ hits.push({ meta, snippet: await buildSessionSnippet(meta.id, needle, query) });
776
913
  }
777
914
  return hits;
778
915
  }
@@ -788,7 +925,7 @@ function formatSearchResults(hits, query) {
788
925
 
789
926
  // src/cli/diff-preview.ts
790
927
  import { readFile as readFile2 } from "fs/promises";
791
- import path4 from "path";
928
+ import path5 from "path";
792
929
  var CONTEXT_LINES = 2;
793
930
  var MAX_HUNK_LINES = 15;
794
931
  var MAX_TOTAL_LINES = 12;
@@ -949,7 +1086,7 @@ async function generateDiffPreview(toolName, args, cwd) {
949
1086
  if (toolName !== "edit_file" && toolName !== "write_file") return null;
950
1087
  const filePath = getStringArg(args, "path");
951
1088
  if (filePath === void 0) return null;
952
- const absolute = path4.resolve(cwd, filePath);
1089
+ const absolute = path5.resolve(cwd, filePath);
953
1090
  let existing;
954
1091
  try {
955
1092
  existing = await readFile2(absolute, "utf8");
@@ -1021,6 +1158,7 @@ ${sections.join("\n\n")}`);
1021
1158
  description: "Delete stored sessions: this directory by default, every session with --all (the current session is kept)",
1022
1159
  usage: "/clear-sessions [--all]",
1023
1160
  category: "Sessions",
1161
+ argHints: ["--all"],
1024
1162
  async run(args, ctx) {
1025
1163
  const arg = args.trim();
1026
1164
  if (arg !== "" && arg !== "--all") {
@@ -1174,6 +1312,7 @@ ${sections.join("\n\n")}`);
1174
1312
  description: "Set the permission mode (opens a picker when no mode is given)",
1175
1313
  usage: "/permission [ask|auto|readonly|yolo]",
1176
1314
  category: "Settings",
1315
+ argHints: ["ask", "auto", "readonly", "yolo", "plan"],
1177
1316
  async run(args, ctx) {
1178
1317
  const arg = args.trim();
1179
1318
  if (!arg) {
@@ -1197,6 +1336,7 @@ ${sections.join("\n\n")}`);
1197
1336
  description: "Show user memory (MEMORY.md), or add an entry",
1198
1337
  usage: "/memory [add <text>]",
1199
1338
  category: "Settings",
1339
+ argHints: ["add"],
1200
1340
  run(args, ctx) {
1201
1341
  const arg = args.trim();
1202
1342
  if (arg === "add" || arg.startsWith("add ")) {
@@ -1276,6 +1416,7 @@ ${content}`);
1276
1416
  description: "Generate an AGENTS.md for the current project",
1277
1417
  usage: "/init [force]",
1278
1418
  category: "Settings",
1419
+ argHints: ["force"],
1279
1420
  async run(args, ctx) {
1280
1421
  ctx.addSystemMessage(await ctx.initProject(args));
1281
1422
  }
@@ -1335,6 +1476,7 @@ ${lines.join("\n")}`);
1335
1476
  description: "Copy the last assistant reply to the clipboard (`all` for the whole conversation)",
1336
1477
  usage: "/copy [all]",
1337
1478
  category: "Sessions",
1479
+ argHints: ["all"],
1338
1480
  async run(args, ctx) {
1339
1481
  const arg = args.trim();
1340
1482
  if (arg !== "" && arg !== "all") {
@@ -1400,8 +1542,8 @@ ${result.diff}${note ? `
1400
1542
 
1401
1543
  // src/cli/commands/connect.ts
1402
1544
  import { spawn } from "child_process";
1403
- import fs3 from "fs";
1404
- import path5 from "path";
1545
+ import fs4 from "fs";
1546
+ import path6 from "path";
1405
1547
  var PROVIDER_PRESETS = [
1406
1548
  {
1407
1549
  key: "openai",
@@ -1541,14 +1683,14 @@ function saveConnection(answers, configPath = globalConfigPath(), envPath = envF
1541
1683
  promptPrice: 0,
1542
1684
  completionPrice: 0
1543
1685
  };
1544
- fs3.mkdirSync(path5.dirname(envPath), { recursive: true });
1686
+ fs4.mkdirSync(path6.dirname(envPath), { recursive: true });
1545
1687
  let envContent = "";
1546
1688
  try {
1547
- envContent = fs3.readFileSync(envPath, "utf8");
1689
+ envContent = fs4.readFileSync(envPath, "utf8");
1548
1690
  } catch {
1549
1691
  envContent = "";
1550
1692
  }
1551
- fs3.writeFileSync(
1693
+ fs4.writeFileSync(
1552
1694
  envPath,
1553
1695
  upsertEnvContent(envContent, provider.apiKeyEnv ?? "", answers.apiKey),
1554
1696
  {
@@ -1557,21 +1699,21 @@ function saveConnection(answers, configPath = globalConfigPath(), envPath = envF
1557
1699
  }
1558
1700
  );
1559
1701
  try {
1560
- fs3.chmodSync(envPath, 384);
1702
+ fs4.chmodSync(envPath, 384);
1561
1703
  } catch {
1562
1704
  }
1563
1705
  if (provider.apiKeyEnv) {
1564
1706
  process.env[provider.apiKeyEnv] = answers.apiKey;
1565
1707
  }
1566
- fs3.mkdirSync(path5.dirname(configPath), { recursive: true });
1708
+ fs4.mkdirSync(path6.dirname(configPath), { recursive: true });
1567
1709
  let existing = "";
1568
1710
  try {
1569
- existing = fs3.readFileSync(configPath, "utf8");
1711
+ existing = fs4.readFileSync(configPath, "utf8");
1570
1712
  } catch {
1571
1713
  existing = "";
1572
1714
  }
1573
1715
  const separator = existing === "" ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
1574
- fs3.writeFileSync(
1716
+ fs4.writeFileSync(
1575
1717
  configPath,
1576
1718
  existing + separator + buildProviderTomlBlock(provider) + buildModelTomlBlock(model),
1577
1719
  "utf8"
@@ -1596,12 +1738,12 @@ ${content}`;
1596
1738
  function saveDefaultModel(modelName, configPath = globalConfigPath()) {
1597
1739
  let content = "";
1598
1740
  try {
1599
- content = fs3.readFileSync(configPath, "utf8");
1741
+ content = fs4.readFileSync(configPath, "utf8");
1600
1742
  } catch {
1601
1743
  content = "";
1602
1744
  }
1603
- fs3.mkdirSync(path5.dirname(configPath), { recursive: true });
1604
- fs3.writeFileSync(configPath, setDefaultModelInToml(content, modelName), "utf8");
1745
+ fs4.mkdirSync(path6.dirname(configPath), { recursive: true });
1746
+ fs4.writeFileSync(configPath, setDefaultModelInToml(content, modelName), "utf8");
1605
1747
  }
1606
1748
  function openConfigFile(filePath) {
1607
1749
  const [command, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", filePath]] : process.platform === "darwin" ? ["open", [filePath]] : ["xdg-open", [filePath]];
@@ -1616,7 +1758,7 @@ function openConfigFile(filePath) {
1616
1758
 
1617
1759
  // src/cli/commands/custom.ts
1618
1760
  import { readFileSync as readFileSync2, readdirSync } from "fs";
1619
- import path6 from "path";
1761
+ import path7 from "path";
1620
1762
  var NAME_PATTERN = /^[a-z0-9-]+$/;
1621
1763
  var DESCRIPTION_PATTERN = /^<!--\s*description:\s*(.+?)\s*-->\s*$/;
1622
1764
  var DEFAULT_DESCRIPTION = "Custom prompt command";
@@ -1651,14 +1793,14 @@ function scanDir(dir, into) {
1651
1793
  if (!entry.endsWith(".md")) continue;
1652
1794
  const name = entry.slice(0, -".md".length);
1653
1795
  if (!NAME_PATTERN.test(name)) continue;
1654
- const parsed = parseCommandFile(path6.join(dir, entry), name);
1796
+ const parsed = parseCommandFile(path7.join(dir, entry), name);
1655
1797
  if (parsed) into.set(name, parsed);
1656
1798
  }
1657
1799
  }
1658
1800
  function loadCustomCommands(cwd, home = starHome()) {
1659
1801
  const byName = /* @__PURE__ */ new Map();
1660
- scanDir(path6.join(home, "commands"), byName);
1661
- scanDir(path6.join(cwd, ".star", "commands"), byName);
1802
+ scanDir(path7.join(home, "commands"), byName);
1803
+ scanDir(path7.join(cwd, ".star", "commands"), byName);
1662
1804
  return [...byName.values()];
1663
1805
  }
1664
1806
  function registerCustomCommands(registry, cwd, home) {
@@ -1684,8 +1826,8 @@ function registerCustomCommands(registry, cwd, home) {
1684
1826
  }
1685
1827
 
1686
1828
  // src/cli/commands/doctor.ts
1687
- import fs4 from "fs/promises";
1688
- import path7 from "path";
1829
+ import fs5 from "fs/promises";
1830
+ import path8 from "path";
1689
1831
  var MIN_NODE_MAJOR = 20;
1690
1832
  var REGISTRY_URL = "https://registry.npmjs.org/";
1691
1833
  var REGISTRY_TIMEOUT_MS = 5e3;
@@ -1699,7 +1841,7 @@ function nodeMajor(version) {
1699
1841
  }
1700
1842
  async function pathExists(target) {
1701
1843
  try {
1702
- await fs4.access(target);
1844
+ await fs5.access(target);
1703
1845
  return true;
1704
1846
  } catch {
1705
1847
  return false;
@@ -1737,7 +1879,7 @@ async function runDoctor(opts) {
1737
1879
  detail: `${spec.label} (${spec.shell})`
1738
1880
  });
1739
1881
  }
1740
- const configPath = path7.join(home, "config.toml");
1882
+ const configPath = path8.join(home, "config.toml");
1741
1883
  const configExists = await pathExists(configPath);
1742
1884
  checks.push({
1743
1885
  name: "STAR_HOME / config",
@@ -1774,14 +1916,14 @@ async function runDoctor(opts) {
1774
1916
  });
1775
1917
  }
1776
1918
  }
1777
- const sessions = path7.join(home, "sessions");
1919
+ const sessions = path8.join(home, "sessions");
1778
1920
  let writable = false;
1779
1921
  let writeError = "";
1780
1922
  try {
1781
- await fs4.mkdir(sessions, { recursive: true });
1782
- const probe = path7.join(sessions, `.doctor-probe-${process.pid}`);
1783
- await fs4.writeFile(probe, "ok");
1784
- await fs4.unlink(probe);
1923
+ await fs5.mkdir(sessions, { recursive: true });
1924
+ const probe = path8.join(sessions, `.doctor-probe-${process.pid}`);
1925
+ await fs5.writeFile(probe, "ok");
1926
+ await fs5.unlink(probe);
1785
1927
  writable = true;
1786
1928
  } catch (error) {
1787
1929
  writeError = error instanceof Error ? error.message : String(error);
@@ -1816,15 +1958,15 @@ function formatDoctorReport(report) {
1816
1958
  }
1817
1959
 
1818
1960
  // src/cli/commands/init-project.ts
1819
- import fs5 from "fs/promises";
1820
- import path8 from "path";
1961
+ import fs6 from "fs/promises";
1962
+ import path9 from "path";
1821
1963
  import { generateText } from "ai";
1822
1964
  var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", "coverage"]);
1823
1965
  var MAX_ENTRIES = 20;
1824
1966
  var README_HEAD_LINES = 5;
1825
1967
  async function fileExists2(target) {
1826
1968
  try {
1827
- await fs5.access(target);
1969
+ await fs6.access(target);
1828
1970
  return true;
1829
1971
  } catch {
1830
1972
  return false;
@@ -1832,7 +1974,7 @@ async function fileExists2(target) {
1832
1974
  }
1833
1975
  async function readPackageJson(cwd) {
1834
1976
  try {
1835
- const raw = await fs5.readFile(path8.join(cwd, "package.json"), "utf8");
1977
+ const raw = await fs6.readFile(path9.join(cwd, "package.json"), "utf8");
1836
1978
  const parsed = JSON.parse(raw);
1837
1979
  if (typeof parsed !== "object" || parsed === null) return null;
1838
1980
  return parsed;
@@ -1849,10 +1991,10 @@ function stringRecord(value) {
1849
1991
  return out;
1850
1992
  }
1851
1993
  async function detectPackageManager(cwd) {
1852
- if (await fileExists2(path8.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
1853
- if (await fileExists2(path8.join(cwd, "yarn.lock"))) return "yarn";
1854
- if (await fileExists2(path8.join(cwd, "package-lock.json"))) return "npm";
1855
- if (await fileExists2(path8.join(cwd, "bun.lockb"))) return "bun";
1994
+ if (await fileExists2(path9.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
1995
+ if (await fileExists2(path9.join(cwd, "yarn.lock"))) return "yarn";
1996
+ if (await fileExists2(path9.join(cwd, "package-lock.json"))) return "npm";
1997
+ if (await fileExists2(path9.join(cwd, "bun.lockb"))) return "bun";
1856
1998
  return null;
1857
1999
  }
1858
2000
  function inferCommands(pm, scripts) {
@@ -1867,7 +2009,7 @@ function inferCommands(pm, scripts) {
1867
2009
  }
1868
2010
  async function listEntries(cwd) {
1869
2011
  try {
1870
- const dirents = await fs5.readdir(cwd, { withFileTypes: true });
2012
+ const dirents = await fs6.readdir(cwd, { withFileTypes: true });
1871
2013
  const entries = [];
1872
2014
  for (const dirent of dirents) {
1873
2015
  if (dirent.isDirectory()) {
@@ -1886,7 +2028,7 @@ async function listEntries(cwd) {
1886
2028
  async function readReadmeHead(cwd) {
1887
2029
  for (const name of ["README.md", "readme.md", "README"]) {
1888
2030
  try {
1889
- const raw = await fs5.readFile(path8.join(cwd, name), "utf8");
2031
+ const raw = await fs6.readFile(path9.join(cwd, name), "utf8");
1890
2032
  return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).slice(0, README_HEAD_LINES);
1891
2033
  } catch {
1892
2034
  }
@@ -1899,14 +2041,14 @@ async function scanProject(cwd) {
1899
2041
  const scripts = stringRecord(pkg?.scripts);
1900
2042
  return {
1901
2043
  name: typeof pkg?.name === "string" ? pkg.name : null,
1902
- dirName: path8.basename(cwd),
2044
+ dirName: path9.basename(cwd),
1903
2045
  packageManager,
1904
2046
  scripts,
1905
2047
  dependencies: Object.keys(stringRecord(pkg?.dependencies)),
1906
2048
  devDependencies: Object.keys(stringRecord(pkg?.devDependencies)),
1907
2049
  commands: inferCommands(packageManager, scripts),
1908
- hasTsconfig: await fileExists2(path8.join(cwd, "tsconfig.json")),
1909
- isGitRepo: await fileExists2(path8.join(cwd, ".git")),
2050
+ hasTsconfig: await fileExists2(path9.join(cwd, "tsconfig.json")),
2051
+ isGitRepo: await fileExists2(path9.join(cwd, ".git")),
1910
2052
  readmeHead: await readReadmeHead(cwd),
1911
2053
  entries: await listEntries(cwd)
1912
2054
  };
@@ -1973,7 +2115,7 @@ Write the AGENTS.md.`
1973
2115
  }
1974
2116
  async function initProject(opts) {
1975
2117
  const { cwd, args, model } = opts;
1976
- const target = path8.join(cwd, "AGENTS.md");
2118
+ const target = path9.join(cwd, "AGENTS.md");
1977
2119
  const force = args.split(/\s+/).includes("force");
1978
2120
  if (!force && await fileExists2(target)) {
1979
2121
  return {
@@ -2007,7 +2149,7 @@ async function initProject(opts) {
2007
2149
  content = renderAgentsMd(facts);
2008
2150
  note = " No model available; used the template \u2014 review and refine manually.";
2009
2151
  }
2010
- await fs5.writeFile(target, content);
2152
+ await fs6.writeFile(target, content);
2011
2153
  const lines = content.split("\n").length;
2012
2154
  const how = usedLlm ? "generated with model refinement" : "generated from template";
2013
2155
  return {
@@ -2063,7 +2205,7 @@ function parseSlashCommand(input) {
2063
2205
  }
2064
2206
 
2065
2207
  // src/cli/commands/rewind.ts
2066
- import path9 from "path";
2208
+ import path10 from "path";
2067
2209
  function planRewind(snapshots, id) {
2068
2210
  const target = snapshots.find((snapshot) => snapshot.id === id);
2069
2211
  if (!target) return null;
@@ -2077,7 +2219,7 @@ function planRewind(snapshots, id) {
2077
2219
  return { target, affected, messageIndex };
2078
2220
  }
2079
2221
  function displayPath(filePath, cwd) {
2080
- const relative = path9.relative(cwd, filePath);
2222
+ const relative = path10.relative(cwd, filePath);
2081
2223
  return relative !== "" && !relative.startsWith("..") ? relative : filePath;
2082
2224
  }
2083
2225
  function formatTime(timestamp) {
@@ -2126,12 +2268,27 @@ function didYouMeanSuffix(names) {
2126
2268
  const listed = names.slice(0, 3).map((name) => `/${name}`);
2127
2269
  return ` Did you mean: ${listed.join(", ")}?`;
2128
2270
  }
2271
+ function filterArgHints(input, commands) {
2272
+ if (!input.startsWith("/")) return [];
2273
+ const spaceIndex = input.search(/\s/);
2274
+ if (spaceIndex === -1) return [];
2275
+ const name = input.slice(1, spaceIndex).toLowerCase();
2276
+ const command = commands.find((cmd) => cmd.name.toLowerCase() === name);
2277
+ if (!command?.argHints) return [];
2278
+ const tokenMatch = /\S+$/.exec(input.slice(spaceIndex));
2279
+ const argPrefix = tokenMatch ? tokenMatch[0] : "";
2280
+ const source = command.argHints;
2281
+ const candidates = typeof source === "function" ? source(argPrefix) : source;
2282
+ const prefix = argPrefix.toLowerCase();
2283
+ const stem = input.slice(0, input.length - argPrefix.length);
2284
+ return candidates.filter((candidate) => candidate.toLowerCase().startsWith(prefix)).slice(0, MAX_SUGGESTIONS).map((value) => ({ value, replacement: `${stem}${value} ` }));
2285
+ }
2129
2286
 
2130
2287
  // src/cli/commands/undo.ts
2131
2288
  import { readFile as readFile3 } from "fs/promises";
2132
- import path10 from "path";
2289
+ import path11 from "path";
2133
2290
  function displayPath2(filePath, cwd) {
2134
- const relative = path10.relative(cwd, filePath);
2291
+ const relative = path11.relative(cwd, filePath);
2135
2292
  return relative !== "" && !relative.startsWith("..") ? relative : filePath;
2136
2293
  }
2137
2294
  async function buildUndoDiffs(snapshots, cwd) {
@@ -2518,11 +2675,11 @@ import { useRef as useRef4, useState as useState3 } from "react";
2518
2675
 
2519
2676
  // src/cli/components/InputBox.tsx
2520
2677
  import { Box as Box2, Text as Text2, useStdout } from "ink";
2521
- import { useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
2678
+ import { memo, useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
2522
2679
 
2523
2680
  // src/cli/path-suggest.ts
2524
2681
  import { readdir } from "fs/promises";
2525
- import path11 from "path";
2682
+ import path12 from "path";
2526
2683
  var MAX_SCAN_ENTRIES = 2e3;
2527
2684
  function extractAtToken(value, cursor) {
2528
2685
  const before = value.slice(0, cursor);
@@ -2537,7 +2694,7 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
2537
2694
  const dirPart = slash >= 0 ? normalized.slice(0, slash + 1) : "";
2538
2695
  const basePrefix = slash >= 0 ? normalized.slice(slash + 1) : normalized;
2539
2696
  if (dirPart.split("/").some((segment) => SKIP_DIRS.has(segment))) return [];
2540
- const dirAbs = path11.resolve(cwd, dirPart === "" ? "." : dirPart);
2697
+ const dirAbs = path12.resolve(cwd, dirPart === "" ? "." : dirPart);
2541
2698
  const entries = await readdir(dirAbs, { withFileTypes: true }).catch(() => null);
2542
2699
  if (!entries) return [];
2543
2700
  const ignore = createIgnorePredicate(cwd);
@@ -2552,7 +2709,7 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
2552
2709
  if (isDir && SKIP_DIRS.has(entry.name)) continue;
2553
2710
  if (entry.name.startsWith(".") && !basePrefix.startsWith(".")) continue;
2554
2711
  if (!entry.name.toLowerCase().startsWith(lower)) continue;
2555
- if (ignore?.(path11.join(dirAbs, entry.name), isDir)) continue;
2712
+ if (ignore?.(path12.join(dirAbs, entry.name), isDir)) continue;
2556
2713
  matches.push({ path: `${dirPart}${entry.name}${isDir ? "/" : ""}`, isDir });
2557
2714
  }
2558
2715
  matches.sort((a, b) => a.path.localeCompare(b.path));
@@ -2650,7 +2807,7 @@ function pasteToken(id, lineCount) {
2650
2807
  function stylePasteTokens(segment) {
2651
2808
  return segment.replace(PASTE_TOKEN_RE, (match) => `\x1B[2m${match}\x1B[22m`);
2652
2809
  }
2653
- function InputBox({
2810
+ var InputBox = memo(function InputBox2({
2654
2811
  isStreaming,
2655
2812
  disabled,
2656
2813
  commands,
@@ -2783,6 +2940,7 @@ function InputBox({
2783
2940
  });
2784
2941
  const suggestionsEnabled = !isStreaming && !disabled && !suggestionsDismissed && !search;
2785
2942
  const suggestions = suggestionsEnabled && commands ? filterCommands(value, commands) : [];
2943
+ const argHints = suggestionsEnabled && commands && cursor === value.length ? filterArgHints(value, commands) : [];
2786
2944
  useEffect2(() => {
2787
2945
  if (!suggestionsEnabled || !cwd) {
2788
2946
  setPathSuggestions([]);
@@ -2801,8 +2959,8 @@ function InputBox({
2801
2959
  cancelled = true;
2802
2960
  };
2803
2961
  }, [value, cursor, cwd, suggestionsEnabled]);
2804
- const showPathSuggestions = suggestions.length === 0 && pathSuggestions.length > 0;
2805
- const suggestionCount = suggestions.length > 0 ? suggestions.length : pathSuggestions.length;
2962
+ const showPathSuggestions = suggestions.length === 0 && argHints.length === 0 && pathSuggestions.length > 0;
2963
+ const suggestionCount = suggestions.length > 0 ? suggestions.length : argHints.length > 0 ? argHints.length : pathSuggestions.length;
2806
2964
  const activeIndex = suggestionCount === 0 ? 0 : Math.min(highlight, suggestionCount - 1);
2807
2965
  const completeHighlighted = () => {
2808
2966
  if (suggestions.length > 0) {
@@ -2812,6 +2970,12 @@ function InputBox({
2812
2970
  edit(text, text.length);
2813
2971
  return;
2814
2972
  }
2973
+ if (argHints.length > 0) {
2974
+ const hint = argHints[activeIndex];
2975
+ if (!hint) return;
2976
+ edit(hint.replacement, hint.replacement.length);
2977
+ return;
2978
+ }
2815
2979
  const sugg = pathSuggestions[activeIndex];
2816
2980
  const token = extractAtToken(value, cursor);
2817
2981
  if (!sugg || !token) return;
@@ -3064,6 +3228,9 @@ ${value.slice(cursor)}`, cursor + 1);
3064
3228
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` - ${cmd.description}` })
3065
3229
  ] }, cmd.name);
3066
3230
  }) }),
3231
+ argHints.length > 0 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: argHints.map(
3232
+ (hint, index) => index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: hint.value }, hint.value) : /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: hint.value }, hint.value)
3233
+ ) }),
3067
3234
  showPathSuggestions && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: pathSuggestions.map(
3068
3235
  (sugg, index) => index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `@${sugg.path}` }, sugg.path) : /* @__PURE__ */ jsxs2(Text2, { children: [
3069
3236
  /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: `@${sugg.path}` }),
@@ -3071,7 +3238,7 @@ ${value.slice(cursor)}`, cursor + 1);
3071
3238
  ] }, sugg.path)
3072
3239
  ) })
3073
3240
  ] });
3074
- }
3241
+ });
3075
3242
 
3076
3243
  // src/cli/components/TextField.tsx
3077
3244
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -3368,7 +3535,7 @@ function ExitPlanPrompt({ onDecision }) {
3368
3535
 
3369
3536
  // src/cli/components/MessageList.tsx
3370
3537
  import { Box as Box6, Static, Text as Text7, useStdout as useStdout2 } from "ink";
3371
- import { memo } from "react";
3538
+ import { memo as memo2, useState as useState5 } from "react";
3372
3539
 
3373
3540
  // src/cli/components/DiffLines.tsx
3374
3541
  import { Text as Text6 } from "ink";
@@ -3403,10 +3570,28 @@ var roleStyles = {
3403
3570
  system: { label: "\u25CF system", color: "yellow" }
3404
3571
  };
3405
3572
  var interruptedMarker = (text) => /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: text.length > 0 ? " [interrupted]" : "[interrupted]" });
3406
- var MessageList = memo(function MessageList2({ messages }) {
3573
+ var INITIAL_RENDER_LIMIT = 100;
3574
+ function collapseInitialMessages(messages, limit = INITIAL_RENDER_LIMIT) {
3575
+ if (messages.length <= limit) return { hidden: 0, visible: [...messages] };
3576
+ return { hidden: messages.length - limit, visible: messages.slice(-limit) };
3577
+ }
3578
+ var MessageList = memo2(function MessageList2({ messages }) {
3407
3579
  const { stdout } = useStdout2();
3408
3580
  const termWidth = stdout.columns ?? 80;
3409
- return /* @__PURE__ */ jsx7(Static, { items: messages, children: (message) => {
3581
+ const [initial] = useState5(() => ({
3582
+ length: messages.length,
3583
+ view: collapseInitialMessages(messages)
3584
+ }));
3585
+ const placeholder = initial.view.hidden > 0 ? [{ id: -1, role: "system", text: "", collapsed: initial.view.hidden }] : [];
3586
+ const items = [...placeholder, ...initial.view.visible, ...messages.slice(initial.length)];
3587
+ return /* @__PURE__ */ jsx7(Static, { items, children: (message) => {
3588
+ if (message.collapsed !== void 0) {
3589
+ return /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
3590
+ "\u2026 ",
3591
+ message.collapsed,
3592
+ " earlier messages hidden \u2014 full history in the session file"
3593
+ ] }) }, message.id);
3594
+ }
3410
3595
  const marginBottom = message.tight ? 0 : 1;
3411
3596
  if (message.role === "assistant-cont") {
3412
3597
  return /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", marginBottom, children: /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
@@ -3431,7 +3616,7 @@ var MessageList = memo(function MessageList2({ messages }) {
3431
3616
  }
3432
3617
  if (message.role === "tool") {
3433
3618
  return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", marginBottom, children: [
3434
- /* @__PURE__ */ jsx7(Text7, { color: "magenta", children: message.text }),
3619
+ /* @__PURE__ */ jsx7(Text7, { color: message.dim ? void 0 : "magenta", dimColor: message.dim, children: message.text }),
3435
3620
  message.diff && /* @__PURE__ */ jsx7(DiffLines, { lines: message.diff })
3436
3621
  ] }, message.id);
3437
3622
  }
@@ -3521,73 +3706,7 @@ function RewindConfirmPrompt({
3521
3706
 
3522
3707
  // src/cli/components/StatusBar.tsx
3523
3708
  import { Box as Box9, Text as Text10, useStdout as useStdout3 } from "ink";
3524
- import { memo as memo2 } from "react";
3525
-
3526
- // src/cli/icons.ts
3527
- var EMOJI_TERM_PROGRAMS = /* @__PURE__ */ new Set([
3528
- "vscode",
3529
- "WezTerm",
3530
- "iTerm.app",
3531
- "Apple_Terminal",
3532
- "ghostty",
3533
- "kitty",
3534
- "Hyper",
3535
- "WarpTerminal"
3536
- ]);
3537
- function resolveIconMode(env = process.env) {
3538
- const override = env.STAR_ICONS;
3539
- if (override === "emoji" || override === "plain") return override;
3540
- if (env.WT_SESSION) return "emoji";
3541
- if (EMOJI_TERM_PROGRAMS.has(env.TERM_PROGRAM ?? "")) return "emoji";
3542
- const term = env.TERM ?? "";
3543
- if (term.includes("kitty") || term.includes("alacritty")) return "emoji";
3544
- return "plain";
3545
- }
3546
- var EMOJI_TOOL_ICONS = {
3547
- bash: "\u{1F4BB}",
3548
- read_file: "\u{1F4C4}",
3549
- write_file: "\u{1F4DD}",
3550
- edit_file: "\u270F\uFE0F",
3551
- glob: "\u{1F4C2}",
3552
- grep: "\u{1F50D}",
3553
- web_search: "\u{1F50E}",
3554
- web_fetch: "\u{1F310}",
3555
- todo_write: "\u{1F4CB}",
3556
- todo_read: "\u{1F4CB}",
3557
- task_list: "\u{1F9F5}",
3558
- task_output: "\u{1F9F5}",
3559
- task_kill: "\u{1F6D1}",
3560
- subagent: "\u{1F916}",
3561
- skill: "\u2728",
3562
- remember: "\u{1F9E0}"
3563
- };
3564
- var PLAIN_TOOL_ICONS = {
3565
- bash: "$",
3566
- read_file: "\u2261",
3567
- write_file: "+",
3568
- edit_file: "\u270E",
3569
- glob: "*",
3570
- grep: "/",
3571
- web_search: "?",
3572
- web_fetch: "\u21D3",
3573
- todo_write: "\u2023",
3574
- todo_read: "\u2023",
3575
- task_list: "\xB6",
3576
- task_output: "\xB6",
3577
- task_kill: "\xD7",
3578
- subagent: "\u25C9",
3579
- skill: "\u2727",
3580
- remember: "\u203B"
3581
- };
3582
- var iconMode = resolveIconMode();
3583
- function toolIcon(name) {
3584
- const map = iconMode === "emoji" ? EMOJI_TOOL_ICONS : PLAIN_TOOL_ICONS;
3585
- return map[name] ?? (iconMode === "emoji" ? "\u{1F527}" : "\u25B8");
3586
- }
3587
- var thinkingIcon = iconMode === "emoji" ? "\u{1F4AD}" : "\u2727";
3588
- var folderIcon = iconMode === "emoji" ? "\u{1F4C1} " : "";
3589
-
3590
- // src/cli/components/StatusBar.tsx
3709
+ import { memo as memo3 } from "react";
3591
3710
  import { jsxs as jsxs10 } from "react/jsx-runtime";
3592
3711
  var BG_LABEL_MAX = 40;
3593
3712
  var WIDE_COLUMNS = 120;
@@ -3596,7 +3715,7 @@ function cwdLabel(cwd, wide) {
3596
3715
  const base = cwd.replace(/[\\/]+$/, "").split(/[\\/]/).pop();
3597
3716
  return base && base.length > 0 ? base : cwd;
3598
3717
  }
3599
- var StatusBar = memo2(function StatusBar2({
3718
+ var StatusBar = memo3(function StatusBar2({
3600
3719
  cwd,
3601
3720
  model,
3602
3721
  permissionMode,
@@ -3654,9 +3773,9 @@ var StatusBar = memo2(function StatusBar2({
3654
3773
 
3655
3774
  // src/cli/components/StreamingMessage.tsx
3656
3775
  import { Box as Box10, Text as Text11, useStdout as useStdout4 } from "ink";
3657
- import { memo as memo3 } from "react";
3776
+ import { memo as memo4 } from "react";
3658
3777
  import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
3659
- var StreamingMessage = memo3(function StreamingMessage2({
3778
+ var StreamingMessage = memo4(function StreamingMessage2({
3660
3779
  text,
3661
3780
  continuation
3662
3781
  }) {
@@ -3669,6 +3788,24 @@ var StreamingMessage = memo3(function StreamingMessage2({
3669
3788
 
3670
3789
  // src/cli/components/ThinkingIndicator.tsx
3671
3790
  import { Box as Box11, Text as Text12 } from "ink";
3791
+ import { memo as memo5, useEffect as useEffect3, useState as useState6 } from "react";
3792
+
3793
+ // src/cli/ticker.ts
3794
+ var TICK_MS = 100;
3795
+ function nextFlush(prev, next) {
3796
+ if (prev.streamed === next.streamed && prev.reasoning === next.reasoning) return null;
3797
+ return next;
3798
+ }
3799
+ function startTicker(onTick, intervalMs = TICK_MS) {
3800
+ let tick = 0;
3801
+ const timer = setInterval(() => {
3802
+ tick += 1;
3803
+ onTick(tick);
3804
+ }, intervalMs);
3805
+ return () => clearInterval(timer);
3806
+ }
3807
+
3808
+ // src/cli/components/ThinkingIndicator.tsx
3672
3809
  import { jsxs as jsxs12 } from "react/jsx-runtime";
3673
3810
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3674
3811
  var REASONING_TAIL_LENGTH = 200;
@@ -3678,15 +3815,18 @@ function truncateTail(text, max) {
3678
3815
  if (flat.length <= max) return flat;
3679
3816
  return `\u2026${flat.slice(flat.length - max)}`;
3680
3817
  }
3681
- function ThinkingIndicator({
3818
+ var ThinkingIndicator = memo5(function ThinkingIndicator2({
3682
3819
  reasoning,
3683
- frame = 0,
3820
+ frame,
3684
3821
  activity,
3685
- elapsedSec
3822
+ startedAt
3686
3823
  }) {
3824
+ const [tick, setTick] = useState6(0);
3825
+ useEffect3(() => startTicker((t) => setTick(t)), []);
3687
3826
  const tail = reasoning ? truncateTail(reasoning, REASONING_TAIL_LENGTH) : "";
3688
- const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
3827
+ const spinner = SPINNER_FRAMES[(frame ?? tick) % SPINNER_FRAMES.length];
3689
3828
  const label = activity ?? `${thinkingIcon} star is thinking\u2026`;
3829
+ const elapsedSec = startedAt === void 0 ? void 0 : Math.max(0, Math.floor((Date.now() - startedAt) / 1e3));
3690
3830
  const elapsed = elapsedSec !== void 0 && elapsedSec >= 3 ? ` (${formatElapsedSeconds(elapsedSec)})` : "";
3691
3831
  return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", children: [
3692
3832
  /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
@@ -3700,10 +3840,11 @@ function ThinkingIndicator({
3700
3840
  tail
3701
3841
  ] })
3702
3842
  ] });
3703
- }
3843
+ });
3704
3844
 
3705
3845
  // src/cli/components/TodoPanel.tsx
3706
3846
  import { Box as Box12, Text as Text13 } from "ink";
3847
+ import { memo as memo6 } from "react";
3707
3848
  import { jsxs as jsxs13 } from "react/jsx-runtime";
3708
3849
  var SYMBOLS = {
3709
3850
  pending: "\u25CB",
@@ -3722,8 +3863,8 @@ function visibleTodoWindow(todos, max = MAX_VISIBLE_TODOS) {
3722
3863
  hiddenAfter: todos.length - start - items.length
3723
3864
  };
3724
3865
  }
3725
- function TodoPanel({ todos }) {
3726
- if (todos.length === 0) return null;
3866
+ var TodoPanel = memo6(function TodoPanel2({ todos }) {
3867
+ if (todos.length === 0 || todos.every((t) => t.status === "done")) return null;
3727
3868
  const { items, hiddenBefore, hiddenAfter } = visibleTodoWindow(todos);
3728
3869
  return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
3729
3870
  hiddenBefore > 0 && /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
@@ -3751,7 +3892,7 @@ function TodoPanel({ todos }) {
3751
3892
  " more"
3752
3893
  ] })
3753
3894
  ] });
3754
- }
3895
+ });
3755
3896
 
3756
3897
  // src/cli/components/ToolCallCard.tsx
3757
3898
  function formatToolCard(card) {
@@ -3769,20 +3910,20 @@ function isDoubleEscape(previous, now, windowMs = DOUBLE_ESC_WINDOW_MS) {
3769
3910
  }
3770
3911
 
3771
3912
  // src/cli/history.ts
3772
- import fs6 from "fs";
3773
- import path12 from "path";
3913
+ import fs7 from "fs";
3914
+ import path13 from "path";
3774
3915
  var HISTORY_LIMIT = 50;
3775
3916
  var JUNK_ENTRY_PATTERN = /^(.)\1{19,}$/;
3776
3917
  function historyPath() {
3777
- return path12.join(starHome(), "history");
3918
+ return path13.join(starHome(), "history");
3778
3919
  }
3779
3920
  function readLines(file) {
3780
- const raw = fs6.readFileSync(file, "utf8");
3921
+ const raw = fs7.readFileSync(file, "utf8");
3781
3922
  return raw.split("\n").map((line) => line.replace(/\r$/, "")).filter((line) => line.trim().length > 0);
3782
3923
  }
3783
3924
  function writeHistory(file, lines) {
3784
- fs6.mkdirSync(path12.dirname(file), { recursive: true });
3785
- fs6.writeFileSync(file, `${lines.join("\n")}
3925
+ fs7.mkdirSync(path13.dirname(file), { recursive: true });
3926
+ fs7.writeFileSync(file, `${lines.join("\n")}
3786
3927
  `);
3787
3928
  }
3788
3929
  function isJunkHistoryEntry(line) {
@@ -3819,8 +3960,8 @@ function appendHistory(entry) {
3819
3960
  writeHistory(file, [...existing.slice(-(HISTORY_LIMIT - 1)), line]);
3820
3961
  return;
3821
3962
  }
3822
- fs6.mkdirSync(path12.dirname(file), { recursive: true });
3823
- fs6.appendFileSync(file, `${line}
3963
+ fs7.mkdirSync(path13.dirname(file), { recursive: true });
3964
+ fs7.appendFileSync(file, `${line}
3824
3965
  `);
3825
3966
  } catch {
3826
3967
  }
@@ -3894,27 +4035,38 @@ async function executeShellBang(raw, cwd, signal) {
3894
4035
  };
3895
4036
  }
3896
4037
 
3897
- // src/cli/ticker.ts
3898
- var TICK_MS = 100;
3899
- function nextFlush(prev, next) {
3900
- if (prev.streamed === next.streamed && prev.reasoning === next.reasoning) return null;
3901
- return next;
3902
- }
3903
- function startTicker(onTick, intervalMs = TICK_MS) {
3904
- let tick = 0;
3905
- const timer = setInterval(() => {
3906
- tick += 1;
3907
- onTick(tick);
3908
- }, intervalMs);
3909
- return () => clearInterval(timer);
3910
- }
3911
-
3912
4038
  // src/cli/update-check.ts
4039
+ import { mkdir, readFile as readFile4, rename, writeFile } from "fs/promises";
4040
+ import path14 from "path";
3913
4041
  var REGISTRY_URL2 = "https://registry.npmjs.org/@cryer%2fstar-cli/latest";
3914
4042
  var TIMEOUT_MS = 5e3;
4043
+ var THROTTLE_MS = 24 * 60 * 60 * 1e3;
3915
4044
  async function defaultFetcher2(url) {
3916
4045
  return fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
3917
4046
  }
4047
+ function defaultStatePath() {
4048
+ return path14.join(starHome(), "update-check.json");
4049
+ }
4050
+ async function readState(statePath) {
4051
+ try {
4052
+ const raw = JSON.parse(await readFile4(statePath, "utf8"));
4053
+ if (typeof raw !== "object" || raw === null) return null;
4054
+ const { lastCheckAt, latest } = raw;
4055
+ if (typeof lastCheckAt !== "number" || !Number.isFinite(lastCheckAt)) return null;
4056
+ return { lastCheckAt, latest: typeof latest === "string" ? latest : null };
4057
+ } catch {
4058
+ return null;
4059
+ }
4060
+ }
4061
+ async function writeState(statePath, state) {
4062
+ try {
4063
+ await mkdir(path14.dirname(statePath), { recursive: true });
4064
+ const tmp = `${statePath}.tmp-${process.pid}`;
4065
+ await writeFile(tmp, JSON.stringify(state), "utf8");
4066
+ await rename(tmp, statePath);
4067
+ } catch {
4068
+ }
4069
+ }
3918
4070
  function isNewerVersion(latest, current) {
3919
4071
  const parse2 = (v) => v.split(".").slice(0, 3).map((part) => Number.parseInt(part, 10));
3920
4072
  const a = parse2(latest);
@@ -3927,14 +4079,25 @@ function isNewerVersion(latest, current) {
3927
4079
  }
3928
4080
  return false;
3929
4081
  }
3930
- async function checkForUpdate(currentVersion, fetcher = defaultFetcher2) {
4082
+ function noticeFor(latest, currentVersion) {
4083
+ if (typeof latest !== "string" || !isNewerVersion(latest, currentVersion)) return null;
4084
+ return `New version available: ${currentVersion} -> ${latest} \u2014 run: npm i -g @cryer/star-cli`;
4085
+ }
4086
+ async function checkForUpdate(currentVersion, fetcher = defaultFetcher2, statePath = defaultStatePath()) {
3931
4087
  try {
4088
+ const state = await readState(statePath);
4089
+ if (state && Date.now() - state.lastCheckAt < THROTTLE_MS) {
4090
+ return noticeFor(state.latest, currentVersion);
4091
+ }
3932
4092
  const res = await fetcher(REGISTRY_URL2);
3933
4093
  if (!res.ok) return null;
3934
4094
  const data = await res.json();
3935
4095
  const latest = typeof data === "object" && data !== null ? data.version : null;
3936
- if (typeof latest !== "string" || !isNewerVersion(latest, currentVersion)) return null;
3937
- return `New version available: ${currentVersion} -> ${latest} \u2014 run: npm i -g @cryer/star-cli`;
4096
+ await writeState(statePath, {
4097
+ lastCheckAt: Date.now(),
4098
+ latest: typeof latest === "string" ? latest : null
4099
+ });
4100
+ return noticeFor(typeof latest === "string" ? latest : null, currentVersion);
3938
4101
  } catch {
3939
4102
  return null;
3940
4103
  }
@@ -3983,30 +4146,30 @@ function Repl({
3983
4146
  initialUsage
3984
4147
  }) {
3985
4148
  const { exit } = useApp();
3986
- const [initialDisplay] = useState5(() => buildDisplayMessages(initialMessages ?? []));
3987
- const [messages, setMessages] = useState5(initialDisplay);
3988
- const [epoch, setEpoch] = useState5(0);
3989
- const [streamingText, setStreamingText] = useState5(null);
3990
- const [isStreaming, setIsStreaming] = useState5(false);
3991
- const [thinking, setThinking] = useState5(false);
3992
- const [thinkingText, setThinkingText] = useState5("");
3993
- const [thoughtSummary, setThoughtSummary] = useState5(null);
3994
- const [, setUsageVersion] = useState5(0);
3995
- const [modelName, setModelName] = useState5(model);
3996
- const [pending, setPending] = useState5(null);
3997
- const [spinnerTick, setSpinnerTick] = useState5(0);
3998
- const [activity, setActivity] = useState5(null);
3999
- const [bgLabels, setBgLabels] = useState5(() => runningTaskLabels());
4000
- const [todos, setTodos] = useState5([]);
4001
- const [permissionModeState, setPermissionModeState] = useState5(permissionMode);
4002
- const [planApproval, setPlanApprovalState] = useState5(false);
4003
- const [pendingRewind, setPendingRewindState] = useState5(null);
4004
- const [picker, setPickerState] = useState5(null);
4005
- const [pendingConnect, setPendingConnectState] = useState5(null);
4149
+ const [initialDisplay] = useState7(() => buildDisplayMessages(initialMessages ?? []));
4150
+ const [messages, setMessages] = useState7(initialDisplay);
4151
+ const [epoch, setEpoch] = useState7(0);
4152
+ const [streamingText, setStreamingText] = useState7(null);
4153
+ const [isStreaming, setIsStreaming] = useState7(false);
4154
+ const [thinking, setThinking] = useState7(false);
4155
+ const [thinkingText, setThinkingText] = useState7("");
4156
+ const [thoughtSummary, setThoughtSummary] = useState7(null);
4157
+ const [, setUsageVersion] = useState7(0);
4158
+ const [modelName, setModelName] = useState7(model);
4159
+ const [pending, setPending] = useState7(null);
4160
+ const [activity, setActivity] = useState7(null);
4161
+ const [bgLabels, setBgLabels] = useState7(() => runningTaskLabels());
4162
+ const [todos, setTodos] = useState7([]);
4163
+ const [permissionModeState, setPermissionModeState] = useState7(permissionMode);
4164
+ const [planApproval, setPlanApprovalState] = useState7(false);
4165
+ const [pendingRewind, setPendingRewindState] = useState7(null);
4166
+ const [picker, setPickerState] = useState7(null);
4167
+ const [pendingConnect, setPendingConnectState] = useState7(null);
4006
4168
  const backendRef = useRef6(backend);
4007
4169
  const nextIdRef = useRef6(initialDisplay.length);
4008
4170
  const abortRef = useRef6(null);
4009
4171
  const busyCommandRef = useRef6(null);
4172
+ const busyAbortRef = useRef6(null);
4010
4173
  const streamedRef = useRef6("");
4011
4174
  const turnStartedAtRef = useRef6(0);
4012
4175
  const lastCommittedLenRef = useRef6(0);
@@ -4018,6 +4181,7 @@ function Repl({
4018
4181
  const pendingRef = useRef6(null);
4019
4182
  const permissionChainRef = useRef6(Promise.resolve());
4020
4183
  const alwaysAllowedRef = useRef6(/* @__PURE__ */ new Set());
4184
+ const alwaysAllowedListRef = useRef6([]);
4021
4185
  const prevModeRef = useRef6(null);
4022
4186
  const planApprovalRef = useRef6(false);
4023
4187
  const pendingRewindRef = useRef6(null);
@@ -4026,23 +4190,23 @@ function Repl({
4026
4190
  const modelNameRef = useRef6(model);
4027
4191
  const sessionStoreRef = useRef6(sessionStore);
4028
4192
  const configRef = useRef6(config);
4029
- useEffect3(() => {
4193
+ useEffect4(() => {
4030
4194
  configRef.current = config;
4031
4195
  }, [config]);
4032
4196
  const usageRef = useRef6(initialUsage ? { ...initialUsage } : emptyUsage());
4033
4197
  const turnChunksRef = useRef6(0);
4034
4198
  const messagesRef = useRef6(initialDisplay);
4035
- const [queueItems, setQueueItems] = useState5([]);
4199
+ const [queueItems, setQueueItems] = useState7([]);
4036
4200
  const queueRef = useRef6(new PromptQueue());
4037
- const [pendingImages, setPendingImages] = useState5([]);
4201
+ const [pendingImages, setPendingImages] = useState7([]);
4038
4202
  const pendingImagesRef = useRef6([]);
4039
4203
  const imageSeqRef = useRef6(1);
4040
- const [inputRefill, setInputRefill] = useState5();
4204
+ const [inputRefill, setInputRefill] = useState7();
4041
4205
  const refillSeqRef = useRef6(0);
4042
4206
  const lastEscRef = useRef6(null);
4043
- const [gitBranch, setGitBranch] = useState5(null);
4044
- const [contextPercent, setContextPercent] = useState5(null);
4045
- const [initialHistory] = useState5(() => loadHistory());
4207
+ const [gitBranch, setGitBranch] = useState7(null);
4208
+ const [contextPercent, setContextPercent] = useState7(null);
4209
+ const [initialHistory] = useState7(() => loadHistory());
4046
4210
  const budgetWarnedRef = useRef6(false);
4047
4211
  const budgetExceededRef = useRef6(false);
4048
4212
  const runStreamRef = useRef6(
@@ -4208,7 +4372,7 @@ function Repl({
4208
4372
  const attachConfirmHandler = useCallback(
4209
4373
  (target) => {
4210
4374
  target.confirmHandler = (req) => {
4211
- if (isAllowedByRules([...alwaysAllowedRef.current], req)) {
4375
+ if (isAllowedByRules(alwaysAllowedListRef.current, req)) {
4212
4376
  return Promise.resolve(true);
4213
4377
  }
4214
4378
  const ask = async () => {
@@ -4228,7 +4392,7 @@ function Repl({
4228
4392
  },
4229
4393
  [setPendingPermission, cwd, pushMessage]
4230
4394
  );
4231
- useEffect3(() => {
4395
+ useEffect4(() => {
4232
4396
  attachConfirmHandler(backendRef.current);
4233
4397
  return () => {
4234
4398
  tickerStopRef.current?.();
@@ -4236,19 +4400,19 @@ function Repl({
4236
4400
  pendingRef.current?.resolve(false);
4237
4401
  };
4238
4402
  }, [attachConfirmHandler]);
4239
- useEffect3(() => {
4403
+ useEffect4(() => {
4240
4404
  if (process.env.STAR_NO_UPDATE_CHECK === "1") return;
4241
4405
  void checkForUpdate(VERSION).then((message) => {
4242
4406
  if (message) pushMessage("system", message);
4243
4407
  });
4244
4408
  }, [pushMessage]);
4245
4409
  const resumedAtStart = initialMessages !== void 0;
4246
- useEffect3(() => {
4410
+ useEffect4(() => {
4247
4411
  if (!resumedAtStart) return;
4248
- loadTodos(cwd).then(setTodos).catch(() => {
4412
+ loadTodos(cwd, sessionStoreRef.current?.id).then(setTodos).catch(() => {
4249
4413
  });
4250
4414
  }, [cwd, resumedAtStart]);
4251
- useEffect3(() => {
4415
+ useEffect4(() => {
4252
4416
  const onUpdate = (task) => {
4253
4417
  setBgLabels(runningTaskLabels());
4254
4418
  pushMessage(
@@ -4342,6 +4506,11 @@ function Repl({
4342
4506
  lastEscRef.current = null;
4343
4507
  return;
4344
4508
  }
4509
+ if (busyCommandRef.current) {
4510
+ lastEscRef.current = null;
4511
+ busyAbortRef.current?.abort();
4512
+ return;
4513
+ }
4345
4514
  const busy = abortRef.current !== null || pendingRef.current !== null;
4346
4515
  if (busy) {
4347
4516
  lastEscRef.current = null;
@@ -4359,7 +4528,7 @@ function Repl({
4359
4528
  }
4360
4529
  lastEscRef.current = null;
4361
4530
  if (key.shift && key.tab) {
4362
- if (abortRef.current || pendingRef.current || planApprovalRef.current || pendingRewindRef.current || pendingConnectRef.current || pickerRef.current)
4531
+ if (abortRef.current || pendingRef.current || busyCommandRef.current || planApprovalRef.current || pendingRewindRef.current || pendingConnectRef.current || pickerRef.current)
4363
4532
  return;
4364
4533
  const current = config.permissionMode;
4365
4534
  const index = SESSION_MODE_CYCLE.indexOf(current);
@@ -4374,6 +4543,7 @@ function Repl({
4374
4543
  if (decision === "always") {
4375
4544
  const rule = buildAllowRule(p.request);
4376
4545
  alwaysAllowedRef.current.add(rule);
4546
+ alwaysAllowedListRef.current = [...alwaysAllowedRef.current];
4377
4547
  void addAllowRule(rule).then((added) => {
4378
4548
  pushMessage(
4379
4549
  "system",
@@ -4404,11 +4574,13 @@ function Repl({
4404
4574
  providerMetadata: reasoningEffortMetadata(config, name),
4405
4575
  temperature: modelConfig.temperature,
4406
4576
  streamIdleTimeoutSec: modelConfig.streamIdleTimeoutSec,
4407
- streamFirstChunkTimeoutSec: modelConfig.streamFirstChunkTimeoutSec
4577
+ streamFirstChunkTimeoutSec: modelConfig.streamFirstChunkTimeoutSec,
4578
+ vision: modelConfig.vision,
4579
+ modelName: name
4408
4580
  });
4409
4581
  const prev = backendRef.current;
4410
4582
  if (prev instanceof AgentLoop) {
4411
- await loop.loadMessages([...prev.getMessages()]);
4583
+ await loop.loadMessages([...prev.getStarMessages()]);
4412
4584
  }
4413
4585
  attachConfirmHandler(loop);
4414
4586
  backendRef.current = loop;
@@ -4489,9 +4661,10 @@ function Repl({
4489
4661
  await current.loadMessages(resumed.messages);
4490
4662
  current.setSessionStore(store);
4491
4663
  sessionStoreRef.current = store;
4492
- setTodos(await loadTodos(cwd));
4664
+ setTodoSession(store.id);
4665
+ setTodos(await loadTodos(cwd, store.id));
4493
4666
  hydrateSnapshots(await loadSessionSnapshots(store.dir));
4494
- const display = buildDisplayMessages(resumed.messages);
4667
+ const display = buildDisplayMessages(toCoreMessages(resumed.messages));
4495
4668
  nextIdRef.current = display.length;
4496
4669
  applyMessages(display);
4497
4670
  usageRef.current = resumed.meta.usage ? { ...resumed.meta.usage } : emptyUsage();
@@ -4538,8 +4711,7 @@ function Repl({
4538
4711
  flushedRef.current = { ...flushedRef.current, streamed: "" };
4539
4712
  setStreamingText("");
4540
4713
  };
4541
- tickerStopRef.current = startTicker((tick) => {
4542
- setSpinnerTick(tick);
4714
+ tickerStopRef.current = startTicker(() => {
4543
4715
  const uncommitted = streamedRef.current.slice(lastCommittedLenRef.current);
4544
4716
  const split = splitCommittableLines(uncommitted, 8);
4545
4717
  if (split) {
@@ -4755,7 +4927,7 @@ function Repl({
4755
4927
  sessionCostUsd
4756
4928
  ]
4757
4929
  );
4758
- useEffect3(() => {
4930
+ useEffect4(() => {
4759
4931
  runStreamRef.current = runStream;
4760
4932
  }, [runStream]);
4761
4933
  const handlePlanDecision = useCallback(
@@ -4913,7 +5085,7 @@ ${lines.join("\n")}`;
4913
5085
  if (!(current instanceof AgentLoop) || !store) {
4914
5086
  return "Nothing to fork \u2014 no active session.";
4915
5087
  }
4916
- const messages2 = await store.messages();
5088
+ const messages2 = await store.starMessages();
4917
5089
  if (messages2.length === 0) {
4918
5090
  return "Nothing to fork \u2014 the current session has no messages yet.";
4919
5091
  }
@@ -4924,6 +5096,7 @@ ${lines.join("\n")}`;
4924
5096
  await fork.setTitle(title);
4925
5097
  current.setSessionStore(fork);
4926
5098
  sessionStoreRef.current = fork;
5099
+ setTodoSession(fork.id);
4927
5100
  return `Forked into new session ${fork.id} ("${title}") and switched to it. Checkpoints and rewind history do not carry over.`;
4928
5101
  },
4929
5102
  newSession: async () => {
@@ -4935,8 +5108,9 @@ ${lines.join("\n")}`;
4935
5108
  const store = await SessionStore.create(cwd, modelNameRef.current);
4936
5109
  current.setSessionStore(store);
4937
5110
  sessionStoreRef.current = store;
4938
- const [first] = current.getMessages();
4939
- await current.loadMessages(first?.role === "system" ? [first] : []);
5111
+ setTodoSession(store.id);
5112
+ const [first] = current.getStarMessages();
5113
+ await current.loadMessages(first?.message.role === "system" ? [first] : []);
4940
5114
  clearSnapshots();
4941
5115
  redrawMessages([]);
4942
5116
  resetTodos();
@@ -4981,20 +5155,16 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
4981
5155
  return `Busy \u2014 /${busyCommandRef.current} is still running.`;
4982
5156
  }
4983
5157
  busyCommandRef.current = "compact";
4984
- let summaryModel = null;
4985
- try {
4986
- summaryModel = createModel(config, modelNameRef.current);
4987
- } catch {
4988
- summaryModel = null;
4989
- }
4990
- tickerStopRef.current = startTicker((tick) => setSpinnerTick(tick));
5158
+ const controller = new AbortController();
5159
+ busyAbortRef.current = controller;
5160
+ turnStartedAtRef.current = Date.now();
4991
5161
  setActivity("compacting context\u2026");
4992
5162
  try {
4993
5163
  const result = await compactSession({
4994
5164
  backend: backendRef.current,
4995
5165
  sessionStore: sessionStoreRef.current,
4996
5166
  config,
4997
- model: summaryModel
5167
+ signal: controller.signal
4998
5168
  });
4999
5169
  if (result.compacted && result.messages) {
5000
5170
  const display = buildDisplayMessages(result.messages);
@@ -5007,6 +5177,7 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
5007
5177
  tickerStopRef.current = null;
5008
5178
  setActivity(null);
5009
5179
  busyCommandRef.current = null;
5180
+ busyAbortRef.current = null;
5010
5181
  const nextPrompt = queueRef.current.dequeue();
5011
5182
  if (nextPrompt) {
5012
5183
  setQueueItems(queueRef.current.list());
@@ -5200,6 +5371,8 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
5200
5371
  };
5201
5372
  const reg = new CommandRegistry();
5202
5373
  registerBuiltinCommands(reg);
5374
+ const modelCommand = reg.get("model");
5375
+ if (modelCommand) modelCommand.argHints = () => config.models.map((m) => m.name);
5203
5376
  registerCustomCommands(reg, cwd);
5204
5377
  return Object.assign(reg, { ctx });
5205
5378
  }, [
@@ -5318,16 +5491,17 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
5318
5491
  () => registry.list().map((cmd) => ({
5319
5492
  name: cmd.name,
5320
5493
  description: cmd.description,
5321
- usage: cmd.usage ?? `/${cmd.name}`
5494
+ usage: cmd.usage ?? `/${cmd.name}`,
5495
+ argHints: cmd.argHints
5322
5496
  })),
5323
5497
  [registry]
5324
5498
  );
5325
- useEffect3(() => {
5499
+ useEffect4(() => {
5326
5500
  setGitBranch(getGitSummaryCached(cwd)?.branch ?? null);
5327
5501
  const current = backendRef.current;
5328
5502
  if (current instanceof AgentLoop) {
5329
5503
  const window_ = contextWindowTokens(config, modelNameRef.current);
5330
- const pct = estimateTokens([...current.getMessages()]) / window_ * 100;
5504
+ const pct = (estimateTokens(current.getMessages()) + current.getToolSchemaTokens()) / window_ * 100;
5331
5505
  setContextPercent(pct < 10 ? Math.round(pct * 10) / 10 : Math.round(pct));
5332
5506
  } else {
5333
5507
  setContextPercent(null);
@@ -5340,9 +5514,8 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
5340
5514
  ThinkingIndicator,
5341
5515
  {
5342
5516
  reasoning: thinkingText,
5343
- frame: spinnerTick,
5344
5517
  activity: activity ?? void 0,
5345
- elapsedSec: Math.max(0, Math.floor((Date.now() - turnStartedAtRef.current) / 1e3))
5518
+ startedAt: turnStartedAtRef.current
5346
5519
  }
5347
5520
  ),
5348
5521
  !thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */ jsx11(Text14, { dimColor: true, children: thoughtSummary }),
@@ -5461,4 +5634,4 @@ export {
5461
5634
  Repl,
5462
5635
  renderRepl
5463
5636
  };
5464
- //# sourceMappingURL=repl-RT6HBM5B.js.map
5637
+ //# sourceMappingURL=repl-OO7C6V7M.js.map