@cryer/star-cli 0.3.10 → 0.3.12

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,6 +6,7 @@ import {
6
6
  buildDisplayMessages,
7
7
  clearSessions,
8
8
  computeCostUsd,
9
+ deleteSession,
9
10
  estimateCost,
10
11
  folderIcon,
11
12
  formatByteCount,
@@ -14,11 +15,13 @@ import {
14
15
  formatSessionEntries,
15
16
  formatStreamError,
16
17
  formatTokens,
18
+ formatTokensCompact,
17
19
  listSessionEntries,
18
20
  loadSessionSnapshots,
19
21
  previewLines,
20
22
  relativeTime,
21
23
  renderMarkdown,
24
+ resolveIconMode,
22
25
  resolveMentions,
23
26
  resolveSessionId,
24
27
  sessionPreview,
@@ -28,7 +31,7 @@ import {
28
31
  thinkingIcon,
29
32
  toTerminalSafe,
30
33
  toolIcon
31
- } from "./chunk-PDFC7QVW.js";
34
+ } from "./chunk-2GAUAYCU.js";
32
35
  import {
33
36
  AgentLoop,
34
37
  WORKING_DIFF_MAX_LINES,
@@ -45,14 +48,14 @@ import {
45
48
  isDangerousCommand,
46
49
  isGitRepo,
47
50
  summarizeMessages
48
- } from "./chunk-MNVCCOI2.js";
51
+ } from "./chunk-F5RWZMQY.js";
49
52
  import {
50
53
  contextWindowTokens,
51
54
  coreMessageText,
52
55
  diffTreeNames,
53
56
  reconcileStarMessages,
54
57
  toCoreMessages
55
- } from "./chunk-IJGUW4PM.js";
58
+ } from "./chunk-MYHD3SZB.js";
56
59
  import {
57
60
  createModel,
58
61
  reasoningEffortMetadata
@@ -66,7 +69,7 @@ import {
66
69
  formatTaskFinished,
67
70
  formatTaskList,
68
71
  formatTaskStarted
69
- } from "./chunk-URC7JXKX.js";
72
+ } from "./chunk-Q5FJN4LN.js";
70
73
  import {
71
74
  MAX_IMAGE_DIMENSION,
72
75
  SKIP_DIRS,
@@ -95,7 +98,7 @@ import {
95
98
  starHome,
96
99
  upsertEnvContent,
97
100
  userMemoryPath
98
- } from "./chunk-A5PXPWHB.js";
101
+ } from "./chunk-LSYGHLRU.js";
99
102
 
100
103
  // src/cli/repl.tsx
101
104
  import { Box as Box13, Text as Text14, render, useApp } from "ink";
@@ -662,7 +665,7 @@ async function compactSession(opts) {
662
665
  return {
663
666
  message: `Compacted context: ${messages.length} -> ${next.length} messages (~${formatTokens(beforeTokens)} -> ~${formatTokens(afterTokens)} estimated tokens).`,
664
667
  compacted: true,
665
- messages: coreNext
668
+ messages: next
666
669
  };
667
670
  }
668
671
  async function applyCompactionSummary(original, compacted, config, model, temperature, signal) {
@@ -1586,13 +1589,30 @@ var PROVIDER_PRESETS = [
1586
1589
  modelHint: "deepseek-chat"
1587
1590
  }
1588
1591
  ];
1589
- var PROTOCOLS = ["openai-compatible", "anthropic", "openai-responses"];
1592
+ var PROTOCOLS = ["openai-responses", "openai-compatible", "anthropic"];
1593
+ var DEFAULT_CUSTOM_PROTOCOL = "openai-responses";
1590
1594
  var PROTOCOL_DESCRIPTIONS = {
1591
1595
  "openai-compatible": "POST /chat/completions \u2014 most providers",
1592
1596
  anthropic: "Anthropic Messages API",
1593
1597
  "openai-responses": "OpenAI Responses API (endpoints exposing only /responses)"
1594
1598
  };
1595
1599
  var CONNECT_DEFAULT_CONTEXT_TOKENS = 128e3;
1600
+ var CONFIG_DOCS_URL = "https://github.com/cryer/star-cli";
1601
+ function buildNewConfigHeader() {
1602
+ return [
1603
+ `# star-cli configuration \u2014 more config details on ${CONFIG_DOCS_URL}`,
1604
+ "",
1605
+ "# progress checkpoint: max model round-trips per turn; 0 = unlimited",
1606
+ "maxSteps = 0",
1607
+ "# context window in tokens; a [[models]] contextMaxTokens overrides it per model",
1608
+ "contextMaxTokens = 200000",
1609
+ "# auto-compact the history once the estimated usage crosses this threshold",
1610
+ "compactThresholdTokens = 100000",
1611
+ '# how compaction shrinks the history: "summary" (LLM summary) or "truncate"',
1612
+ 'contextCompaction = "summary"',
1613
+ ""
1614
+ ].join("\n");
1615
+ }
1596
1616
  function deriveProviderName(baseURL) {
1597
1617
  let host = baseURL;
1598
1618
  try {
@@ -1712,6 +1732,7 @@ function saveConnection(answers, configPath = globalConfigPath(), envPath = envF
1712
1732
  } catch {
1713
1733
  existing = "";
1714
1734
  }
1735
+ if (existing === "") existing = buildNewConfigHeader();
1715
1736
  const separator = existing === "" ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
1716
1737
  fs4.writeFileSync(
1717
1738
  configPath,
@@ -2261,7 +2282,7 @@ function filterCommands(input, commands) {
2261
2282
  const fuzzy = commands.filter(
2262
2283
  (cmd) => !cmd.name.toLowerCase().startsWith(prefix) && isSubsequence(prefix, cmd.name.toLowerCase())
2263
2284
  ).sort(byName);
2264
- return [...prefixed, ...fuzzy].slice(0, MAX_SUGGESTIONS);
2285
+ return [...prefixed, ...fuzzy];
2265
2286
  }
2266
2287
  function didYouMeanSuffix(names) {
2267
2288
  if (names.length === 0) return "";
@@ -2281,7 +2302,7 @@ function filterArgHints(input, commands) {
2281
2302
  const candidates = typeof source === "function" ? source(argPrefix) : source;
2282
2303
  const prefix = argPrefix.toLowerCase();
2283
2304
  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} ` }));
2305
+ return candidates.filter((candidate) => candidate.toLowerCase().startsWith(prefix)).map((value) => ({ value, replacement: `${stem}${value}` }));
2285
2306
  }
2286
2307
 
2287
2308
  // src/cli/commands/undo.ts
@@ -2607,14 +2628,38 @@ import { Box, Text } from "ink";
2607
2628
  import { useRef as useRef2, useState } from "react";
2608
2629
  import { jsx, jsxs } from "react/jsx-runtime";
2609
2630
  var SELECT_PROMPT_MAX_VISIBLE = 9;
2610
- function SelectPrompt({ title, options, onSelect, onCancel }) {
2631
+ function SelectPrompt({ title, options, onSelect, onCancel, onDelete }) {
2611
2632
  const [index, setIndex] = useState(0);
2612
2633
  const indexRef = useRef2(0);
2634
+ const [confirming, setConfirming] = useState(null);
2635
+ const confirmingRef = useRef2(null);
2636
+ const deletingRef = useRef2(false);
2637
+ const setConfirm = (option) => {
2638
+ confirmingRef.current = option;
2639
+ setConfirming(option);
2640
+ };
2613
2641
  const move = (delta) => {
2614
2642
  indexRef.current = Math.max(0, Math.min(indexRef.current + delta, options.length - 1));
2615
2643
  setIndex(indexRef.current);
2616
2644
  };
2617
2645
  useInput((input, key) => {
2646
+ const pending = confirmingRef.current;
2647
+ if (pending) {
2648
+ if (input === "y" || input === "Y") {
2649
+ setConfirm(null);
2650
+ if (onDelete && !deletingRef.current) {
2651
+ deletingRef.current = true;
2652
+ void onDelete(pending.value).catch(() => options).then((next) => {
2653
+ deletingRef.current = false;
2654
+ indexRef.current = Math.min(indexRef.current, Math.max(0, next.length - 1));
2655
+ setIndex(indexRef.current);
2656
+ });
2657
+ }
2658
+ } else if (input === "n" || input === "N" || key.escape) {
2659
+ setConfirm(null);
2660
+ }
2661
+ return;
2662
+ }
2618
2663
  if (key.escape) {
2619
2664
  onCancel();
2620
2665
  return;
@@ -2624,6 +2669,11 @@ function SelectPrompt({ title, options, onSelect, onCancel }) {
2624
2669
  if (option) onSelect(option.value);
2625
2670
  return;
2626
2671
  }
2672
+ if (onDelete && key.ctrl && input === "x") {
2673
+ const option = options[indexRef.current];
2674
+ if (option) setConfirm(option);
2675
+ return;
2676
+ }
2627
2677
  if (key.downArrow || input === "j") {
2628
2678
  move(1);
2629
2679
  } else if (key.upArrow || input === "k") {
@@ -2665,7 +2715,15 @@ function SelectPrompt({ title, options, onSelect, onCancel }) {
2665
2715
  below,
2666
2716
  " more below"
2667
2717
  ] }),
2668
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[\u2191/\u2193 or j/k] move [Enter] select [Esc] cancel" })
2718
+ confirming ? /* @__PURE__ */ jsxs(Text, { color: "red", children: [
2719
+ "Delete ",
2720
+ confirming.label,
2721
+ "? [y] yes [n] no"
2722
+ ] }) : /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
2723
+ "[\u2191/\u2193 or j/k] move [Enter] select",
2724
+ onDelete ? " [Ctrl+X] delete" : "",
2725
+ " [Esc] cancel"
2726
+ ] })
2669
2727
  ] });
2670
2728
  }
2671
2729
 
@@ -2966,7 +3024,7 @@ var InputBox = memo(function InputBox2({
2966
3024
  if (suggestions.length > 0) {
2967
3025
  const cmd = suggestions[activeIndex];
2968
3026
  if (!cmd) return;
2969
- const text = `/${cmd.name} `;
3027
+ const text = `/${cmd.name}`;
2970
3028
  edit(text, text.length);
2971
3029
  return;
2972
3030
  }
@@ -3095,7 +3153,11 @@ ${currentValue.slice(currentCursor)}`,
3095
3153
  );
3096
3154
  return;
3097
3155
  }
3098
- const text = expandPastes(currentValue).trim();
3156
+ let text = expandPastes(currentValue).trim();
3157
+ if (suggestions.length > 0 && text.startsWith("/") && text.length > 1) {
3158
+ const cmd = suggestions[activeIndex];
3159
+ if (cmd) text = `/${cmd.name}`;
3160
+ }
3099
3161
  if (text.length > 0) {
3100
3162
  if (!text.startsWith("/")) setHistory((prev) => [...prev, text]);
3101
3163
  onSubmit(text);
@@ -3219,18 +3281,47 @@ ${value.slice(cursor)}`, cursor + 1);
3219
3281
  const cursorChar = at === "\n" || at === void 0 || at === PASTE_DELIM ? " " : at;
3220
3282
  const tail = (at === "\n" ? "\n" : "") + stylePasteTokens(after);
3221
3283
  const styled = search ? `\x1B[36m(reverse-search) \x1B[39m'${search.query}': ${search.match === null ? "\x1B[2mno match\x1B[22m\x1B[7m \x1B[27m" : `${history[search.match]}\x1B[7m \x1B[27m`}` : `\x1B[36m\u276F \x1B[39m${stylePasteTokens(before)}\x1B[7m${cursorChar}\x1B[27m${tail}`;
3284
+ const menuStart = Math.min(
3285
+ Math.max(0, activeIndex - Math.floor(MAX_SUGGESTIONS / 2)),
3286
+ Math.max(0, suggestionCount - MAX_SUGGESTIONS)
3287
+ );
3288
+ const menuEnd = Math.min(suggestionCount, menuStart + MAX_SUGGESTIONS);
3222
3289
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
3223
3290
  /* @__PURE__ */ jsx2(Box2, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { children: styled }) }),
3224
- suggestions.length > 0 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: suggestions.map((cmd, index) => {
3225
- const label = cmd.usage ?? `/${cmd.name}`;
3226
- return index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `${label} - ${cmd.description}` }, cmd.name) : /* @__PURE__ */ jsxs2(Text2, { children: [
3227
- /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: label }),
3228
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` - ${cmd.description}` })
3229
- ] }, cmd.name);
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
- ) }),
3291
+ suggestions.length > 0 && /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingLeft: 2, children: [
3292
+ menuStart > 0 && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
3293
+ "\u2026 ",
3294
+ menuStart,
3295
+ " more above"
3296
+ ] }),
3297
+ suggestions.slice(menuStart, menuEnd).map((cmd, index) => {
3298
+ const label = cmd.usage ?? `/${cmd.name}`;
3299
+ return menuStart + index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `${label} - ${cmd.description}` }, cmd.name) : /* @__PURE__ */ jsxs2(Text2, { children: [
3300
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: label }),
3301
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` - ${cmd.description}` })
3302
+ ] }, cmd.name);
3303
+ }),
3304
+ menuEnd < suggestions.length && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
3305
+ "\u2026 ",
3306
+ suggestions.length - menuEnd,
3307
+ " more below"
3308
+ ] })
3309
+ ] }),
3310
+ argHints.length > 0 && /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingLeft: 2, children: [
3311
+ menuStart > 0 && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
3312
+ "\u2026 ",
3313
+ menuStart,
3314
+ " more above"
3315
+ ] }),
3316
+ argHints.slice(menuStart, menuEnd).map(
3317
+ (hint, index) => menuStart + index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: hint.value }, hint.value) : /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: hint.value }, hint.value)
3318
+ ),
3319
+ menuEnd < argHints.length && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
3320
+ "\u2026 ",
3321
+ argHints.length - menuEnd,
3322
+ " more below"
3323
+ ] })
3324
+ ] }),
3234
3325
  showPathSuggestions && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: pathSuggestions.map(
3235
3326
  (sugg, index) => index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `@${sugg.path}` }, sugg.path) : /* @__PURE__ */ jsxs2(Text2, { children: [
3236
3327
  /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: `@${sugg.path}` }),
@@ -3326,7 +3417,7 @@ function ConnectWizard({
3326
3417
  }) {
3327
3418
  const [step, setStep] = useState4("provider");
3328
3419
  const [preset, setPreset] = useState4(null);
3329
- const [protocol, setProtocol] = useState4("openai-compatible");
3420
+ const [protocol, setProtocol] = useState4(DEFAULT_CUSTOM_PROTOCOL);
3330
3421
  const [baseURL, setBaseURL] = useState4("");
3331
3422
  const [apiKey, setApiKey] = useState4("");
3332
3423
  const [modelId, setModelId] = useState4("");
@@ -3463,6 +3554,7 @@ function ConnectWizard({
3463
3554
  options: PROTOCOLS.map((p) => ({
3464
3555
  value: p,
3465
3556
  label: p,
3557
+ hint: p === DEFAULT_CUSTOM_PROTOCOL ? "default" : void 0,
3466
3558
  description: PROTOCOL_DESCRIPTIONS[p]
3467
3559
  })),
3468
3560
  onSelect: handleProtocolSelect,
@@ -3600,10 +3692,11 @@ var MessageList = memo2(function MessageList2({ messages }) {
3600
3692
  ] }) }, message.id);
3601
3693
  }
3602
3694
  if (message.role === "user") {
3695
+ const color = message.steer ? "magenta" : "cyan";
3603
3696
  return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", marginBottom, children: [
3604
3697
  /* @__PURE__ */ jsxs7(Box6, { children: [
3605
- /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "\u276F " }),
3606
- /* @__PURE__ */ jsxs7(Text7, { color: "cyan", children: [
3698
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color, children: message.steer ? "\xBB " : "\u276F " }),
3699
+ /* @__PURE__ */ jsxs7(Text7, { color, children: [
3607
3700
  message.text,
3608
3701
  message.interrupted && interruptedMarker(message.text)
3609
3702
  ] })
@@ -3722,6 +3815,7 @@ var StatusBar = memo3(function StatusBar2({
3722
3815
  tokens: tokens2,
3723
3816
  gitBranch,
3724
3817
  contextPercent,
3818
+ contextTokens,
3725
3819
  cachePercent,
3726
3820
  sessionCostUsd,
3727
3821
  backgroundTasks = []
@@ -3750,7 +3844,8 @@ var StatusBar = memo3(function StatusBar2({
3750
3844
  contextPercent != null && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
3751
3845
  "ctx: ",
3752
3846
  contextPercent,
3753
- "%"
3847
+ "%",
3848
+ contextTokens ? ` (${formatTokensCompact(contextTokens.used)}/${formatTokensCompact(contextTokens.window)})` : ""
3754
3849
  ] }),
3755
3850
  /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
3756
3851
  "cache: ",
@@ -3815,6 +3910,14 @@ function truncateTail(text, max) {
3815
3910
  if (flat.length <= max) return flat;
3816
3911
  return `\u2026${flat.slice(flat.length - max)}`;
3817
3912
  }
3913
+ function roughReasoningTokens(text) {
3914
+ let weight = 0;
3915
+ for (let i = 0; i < text.length; i++) {
3916
+ const code = text.charCodeAt(i);
3917
+ weight += code >= 12288 && code <= 12543 || code >= 13312 && code <= 19903 || code >= 19968 && code <= 40959 || code >= 63744 && code <= 64255 || code >= 65280 && code <= 65519 ? 4 : 1;
3918
+ }
3919
+ return Math.ceil(weight / 4);
3920
+ }
3818
3921
  var ThinkingIndicator = memo5(function ThinkingIndicator2({
3819
3922
  reasoning,
3820
3923
  frame,
@@ -3827,13 +3930,17 @@ var ThinkingIndicator = memo5(function ThinkingIndicator2({
3827
3930
  const spinner = SPINNER_FRAMES[(frame ?? tick) % SPINNER_FRAMES.length];
3828
3931
  const label = activity ?? `${thinkingIcon} star is thinking\u2026`;
3829
3932
  const elapsedSec = startedAt === void 0 ? void 0 : Math.max(0, Math.floor((Date.now() - startedAt) / 1e3));
3830
- const elapsed = elapsedSec !== void 0 && elapsedSec >= 3 ? ` (${formatElapsedSeconds(elapsedSec)})` : "";
3933
+ const stats = [];
3934
+ if (elapsedSec !== void 0 && elapsedSec >= 3) stats.push(formatElapsedSeconds(elapsedSec));
3935
+ const thoughtTokens = reasoning ? roughReasoningTokens(reasoning) : 0;
3936
+ if (thoughtTokens > 0) stats.push(`~${formatTokens(thoughtTokens)} thought`);
3937
+ const status = stats.length > 0 ? ` (${stats.join(" \xB7 ")})` : "";
3831
3938
  return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", children: [
3832
3939
  /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3833
3940
  spinner,
3834
3941
  " ",
3835
3942
  label,
3836
- elapsed
3943
+ status
3837
3944
  ] }),
3838
3945
  tail !== "" && /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
3839
3946
  " ",
@@ -3990,6 +4097,12 @@ var PromptQueue = class {
3990
4097
  this.items.push({ id: this.nextId++, ...prompt });
3991
4098
  return this.items.length;
3992
4099
  }
4100
+ // Jump the queue: steering prompts the running turn never delivered are
4101
+ // recovered here when the turn ends, and they were submitted before the
4102
+ // already-queued entries.
4103
+ enqueueFront(prompt) {
4104
+ this.items.unshift({ id: this.nextId++, ...prompt });
4105
+ }
3993
4106
  dequeue() {
3994
4107
  return this.items.shift();
3995
4108
  }
@@ -4035,9 +4148,56 @@ async function executeShellBang(raw, cwd, signal) {
4035
4148
  };
4036
4149
  }
4037
4150
 
4151
+ // src/cli/terminal-title.ts
4152
+ import path14 from "path";
4153
+ var SPINNER_FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4154
+ var TITLE_INTERVAL_MS = 120;
4155
+ var CONTROL_CHARS = /[\x00-\x1f\x7f]/g;
4156
+ function setTerminalTitle(title, deps = {}) {
4157
+ const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
4158
+ if (!isTTY) return;
4159
+ const write = deps.write ?? ((text) => process.stdout.write(text));
4160
+ write(`\x1B]0;${title.replace(CONTROL_CHARS, "")}\x07`);
4161
+ }
4162
+ var TerminalTitle = class _TerminalTitle {
4163
+ constructor(base, deps) {
4164
+ this.base = base;
4165
+ this.deps = deps;
4166
+ }
4167
+ base;
4168
+ deps;
4169
+ timer = null;
4170
+ frame = 0;
4171
+ static forCwd(cwd, deps = {}) {
4172
+ return new _TerminalTitle(`star-cli \u2014 ${path14.basename(cwd) || cwd}`, deps);
4173
+ }
4174
+ start() {
4175
+ if (this.timer) return;
4176
+ this.render(SPINNER_FRAMES2[this.frame] ?? "\u280B");
4177
+ this.timer = setInterval(() => {
4178
+ this.frame = (this.frame + 1) % SPINNER_FRAMES2.length;
4179
+ this.render(SPINNER_FRAMES2[this.frame] ?? "\u280B");
4180
+ }, TITLE_INTERVAL_MS);
4181
+ this.timer.unref();
4182
+ }
4183
+ // "done" leaves a bell glyph in the title until the next turn starts;
4184
+ // "idle" (interrupt, unmount) restores the bare title.
4185
+ stop(kind = "done") {
4186
+ if (this.timer) {
4187
+ clearInterval(this.timer);
4188
+ this.timer = null;
4189
+ }
4190
+ this.frame = 0;
4191
+ this.render(kind === "done" ? resolveIconMode(this.deps.env) === "emoji" ? "\u{1F514}" : "!" : "");
4192
+ }
4193
+ render(prefix) {
4194
+ setTerminalTitle(prefix ? `${prefix} ${this.base}` : this.base, this.deps);
4195
+ }
4196
+ };
4197
+
4038
4198
  // src/cli/update-check.ts
4039
4199
  import { mkdir, readFile as readFile4, rename, writeFile } from "fs/promises";
4040
- import path14 from "path";
4200
+ import path15 from "path";
4041
4201
  var REGISTRY_URL2 = "https://registry.npmjs.org/@cryer%2fstar-cli/latest";
4042
4202
  var TIMEOUT_MS = 5e3;
4043
4203
  var THROTTLE_MS = 24 * 60 * 60 * 1e3;
@@ -4045,7 +4205,7 @@ async function defaultFetcher2(url) {
4045
4205
  return fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
4046
4206
  }
4047
4207
  function defaultStatePath() {
4048
- return path14.join(starHome(), "update-check.json");
4208
+ return path15.join(starHome(), "update-check.json");
4049
4209
  }
4050
4210
  async function readState(statePath) {
4051
4211
  try {
@@ -4060,7 +4220,7 @@ async function readState(statePath) {
4060
4220
  }
4061
4221
  async function writeState(statePath, state) {
4062
4222
  try {
4063
- await mkdir(path14.dirname(statePath), { recursive: true });
4223
+ await mkdir(path15.dirname(statePath), { recursive: true });
4064
4224
  const tmp = `${statePath}.tmp-${process.pid}`;
4065
4225
  await writeFile(tmp, JSON.stringify(state), "utf8");
4066
4226
  await rename(tmp, statePath);
@@ -4131,7 +4291,7 @@ var BUSY_BLOCKED_COMMANDS = /* @__PURE__ */ new Set([
4131
4291
  function formatUsage(usage) {
4132
4292
  return `API usage this session: ${usage.requests} requests, ${formatTokens(usage.promptTokens)} prompt + ${formatTokens(usage.completionTokens)} completion = ${formatTokens(usage.totalTokens)} tokens`;
4133
4293
  }
4134
- var SESSION_PICKER_CAP = 20;
4294
+ var SESSION_PICKER_CAP = 100;
4135
4295
  var SESSION_PREVIEW_MAX = 60;
4136
4296
  var MAX_VISIBLE_QUEUED = 5;
4137
4297
  var REASONING_EFFORT_LEVELS = ["minimal", "low", "medium", "high", "max"];
@@ -4186,6 +4346,7 @@ function Repl({
4186
4346
  const planApprovalRef = useRef6(false);
4187
4347
  const pendingRewindRef = useRef6(null);
4188
4348
  const pickerRef = useRef6(null);
4349
+ const titleRef = useRef6(null);
4189
4350
  const pendingConnectRef = useRef6(null);
4190
4351
  const modelNameRef = useRef6(model);
4191
4352
  const sessionStoreRef = useRef6(sessionStore);
@@ -4205,7 +4366,7 @@ function Repl({
4205
4366
  const refillSeqRef = useRef6(0);
4206
4367
  const lastEscRef = useRef6(null);
4207
4368
  const [gitBranch, setGitBranch] = useState7(null);
4208
- const [contextPercent, setContextPercent] = useState7(null);
4369
+ const [contextUsage, setContextUsage] = useState7(null);
4209
4370
  const [initialHistory] = useState7(() => loadHistory());
4210
4371
  const budgetWarnedRef = useRef6(false);
4211
4372
  const budgetExceededRef = useRef6(false);
@@ -4213,11 +4374,11 @@ function Repl({
4213
4374
  null
4214
4375
  );
4215
4376
  const pushMessage = useCallback(
4216
- (role, text, note, tight, interrupted, diff) => {
4377
+ (role, text, note, tight, interrupted, diff, steer) => {
4217
4378
  setMessages((prev) => {
4218
4379
  const next = [
4219
4380
  ...prev,
4220
- { id: nextIdRef.current++, role, text, note, tight, interrupted, diff }
4381
+ { id: nextIdRef.current++, role, text, note, tight, interrupted, diff, steer }
4221
4382
  ];
4222
4383
  messagesRef.current = next;
4223
4384
  return next;
@@ -4268,10 +4429,10 @@ function Repl({
4268
4429
  setPendingConnectState(p);
4269
4430
  }, []);
4270
4431
  const showPicker = useCallback(
4271
- (title, options) => {
4432
+ (title, options, onDelete) => {
4272
4433
  if (pickerRef.current) return Promise.resolve(null);
4273
4434
  return new Promise((resolve) => {
4274
- setPicker({ title, options, resolve });
4435
+ setPicker({ title, options, resolve, onDelete });
4275
4436
  });
4276
4437
  },
4277
4438
  [setPicker]
@@ -4335,7 +4496,8 @@ function Repl({
4335
4496
  const result = saveConnection(answers);
4336
4497
  config.providers.push(result.provider);
4337
4498
  config.models.push(result.model);
4338
- return `Saved provider "${result.provider.name}" and model "${result.model.name}" to ${result.configPath}; the key is stored as ${result.provider.apiKeyEnv} in ${result.envPath}.`;
4499
+ return `Saved provider "${result.provider.name}" and model "${result.model.name}" to ${result.configPath}; the key is stored as ${result.provider.apiKeyEnv} in ${result.envPath}.
4500
+ Full configuration details: ${CONFIG_DOCS_URL}`;
4339
4501
  },
4340
4502
  [config]
4341
4503
  );
@@ -4398,6 +4560,7 @@ function Repl({
4398
4560
  tickerStopRef.current?.();
4399
4561
  abortRef.current?.abort();
4400
4562
  pendingRef.current?.resolve(false);
4563
+ titleRef.current?.stop("idle");
4401
4564
  };
4402
4565
  }, [attachConfirmHandler]);
4403
4566
  useEffect4(() => {
@@ -4664,7 +4827,7 @@ function Repl({
4664
4827
  setTodoSession(store.id);
4665
4828
  setTodos(await loadTodos(cwd, store.id));
4666
4829
  hydrateSnapshots(await loadSessionSnapshots(store.dir));
4667
- const display = buildDisplayMessages(toCoreMessages(resumed.messages));
4830
+ const display = buildDisplayMessages(resumed.messages);
4668
4831
  nextIdRef.current = display.length;
4669
4832
  applyMessages(display);
4670
4833
  usageRef.current = resumed.meta.usage ? { ...resumed.meta.usage } : emptyUsage();
@@ -4674,6 +4837,10 @@ function Repl({
4674
4837
  },
4675
4838
  [applyMessages, cwd, stopBackgroundWork]
4676
4839
  );
4840
+ const terminalTitle = useCallback(() => {
4841
+ titleRef.current ??= TerminalTitle.forCwd(cwd);
4842
+ return titleRef.current;
4843
+ }, [cwd]);
4677
4844
  const runStream = useCallback(
4678
4845
  async (input, extraImages = []) => {
4679
4846
  const resolved = await resolveMentions(input, cwd);
@@ -4700,6 +4867,7 @@ function Repl({
4700
4867
  setThoughtSummary(null);
4701
4868
  setStreamingText("");
4702
4869
  setIsStreaming(true);
4870
+ terminalTitle().start();
4703
4871
  flushedRef.current = { streamed: "", reasoning: "" };
4704
4872
  const commitStreamed = (tight) => {
4705
4873
  const uncommitted = streamedRef.current.slice(lastCommittedLenRef.current);
@@ -4859,6 +5027,7 @@ function Repl({
4859
5027
  lastCommittedLenRef.current = 0;
4860
5028
  setStreamingText(null);
4861
5029
  const interrupted = controller.signal.aborted;
5030
+ terminalTitle().stop(interrupted ? "idle" : "done");
4862
5031
  notifyBell({
4863
5032
  enabled: config.notifyBell,
4864
5033
  thresholdSec: config.notifyBellThresholdSec,
@@ -4901,6 +5070,14 @@ function Repl({
4901
5070
  `Session budget exceeded: $${formatDollars(cost ?? 0)} of $${formatDollars(budget ?? 0)} (sessionBudgetUsd). New prompts are blocked \u2014 raise sessionBudgetUsd in the config or start a /new session.`
4902
5071
  );
4903
5072
  }
5073
+ const backend2 = backendRef.current;
5074
+ if (backend2 instanceof AgentLoop) {
5075
+ const undelivered = backend2.takeUndeliveredSteers();
5076
+ for (const text of [...undelivered].reverse()) {
5077
+ queueRef.current.enqueueFront({ text, images: [] });
5078
+ }
5079
+ if (undelivered.length > 0) setQueueItems(queueRef.current.list());
5080
+ }
4904
5081
  const nextPrompt = queueRef.current.dequeue();
4905
5082
  if (nextPrompt) {
4906
5083
  if (status === "exceeded") {
@@ -4924,7 +5101,8 @@ function Repl({
4924
5101
  setPlanApproval,
4925
5102
  cwd,
4926
5103
  config,
4927
- sessionCostUsd
5104
+ sessionCostUsd,
5105
+ terminalTitle
4928
5106
  ]
4929
5107
  );
4930
5108
  useEffect4(() => {
@@ -5072,11 +5250,41 @@ ${lines.join("\n")}`;
5072
5250
  };
5073
5251
  })
5074
5252
  );
5253
+ const deleted = [];
5254
+ const onDelete = async (id) => {
5255
+ const picker2 = pickerRef.current;
5256
+ const option = (picker2?.options ?? options).find((o) => o.value === id);
5257
+ try {
5258
+ await deleteSession(id);
5259
+ } catch (error) {
5260
+ pushMessage(
5261
+ "system",
5262
+ `Failed to delete session ${shortSessionId(id)}: ${error instanceof Error ? error.message : String(error)}`
5263
+ );
5264
+ return picker2?.options ?? options;
5265
+ }
5266
+ deleted.push(option?.label ?? shortSessionId(id));
5267
+ const remaining = (picker2?.options ?? options).filter((o) => o.value !== id);
5268
+ if (picker2) {
5269
+ if (remaining.length === 0) {
5270
+ handlePickerCancel();
5271
+ } else {
5272
+ setPicker({ ...picker2, options: remaining });
5273
+ }
5274
+ }
5275
+ return remaining;
5276
+ };
5075
5277
  const picked = await showPicker(
5076
5278
  all ? "Resume a session (all directories)" : "Resume a session",
5077
- options
5279
+ options,
5280
+ onDelete
5078
5281
  );
5079
- if (!picked) return "Session unchanged \u2014 picker cancelled.";
5282
+ if (!picked) {
5283
+ if (deleted.length > 0) {
5284
+ return `Deleted ${deleted.length} session(s): ${deleted.join(", ")}.`;
5285
+ }
5286
+ return "Session unchanged \u2014 picker cancelled.";
5287
+ }
5080
5288
  return resume(picked);
5081
5289
  },
5082
5290
  forkSession: async () => {
@@ -5293,7 +5501,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
5293
5501
  if (result.messageIndex !== null && current instanceof AgentLoop) {
5294
5502
  retracted = await current.retractFromIndex(result.messageIndex);
5295
5503
  if (retracted > 0) {
5296
- const display = buildDisplayMessages([...current.getMessages()]);
5504
+ const display = buildDisplayMessages([...current.getStarMessages()]);
5297
5505
  nextIdRef.current = display.length;
5298
5506
  redrawMessages(display);
5299
5507
  }
@@ -5390,6 +5598,8 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
5390
5598
  setPendingRewind,
5391
5599
  setPendingConnect,
5392
5600
  showPicker,
5601
+ handlePickerCancel,
5602
+ setPicker,
5393
5603
  stopBackgroundWork
5394
5604
  ]);
5395
5605
  const runShellBang = useCallback(
@@ -5479,13 +5689,55 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
5479
5689
  const images = pendingImagesRef.current.map((staged) => staged.image);
5480
5690
  if (images.length > 0) clearPendingImages();
5481
5691
  if (abortRef.current || busyCommandRef.current) {
5692
+ const backend2 = backendRef.current;
5693
+ if (abortRef.current && images.length === 0 && backend2 instanceof AgentLoop) {
5694
+ void (async () => {
5695
+ const picked = await showPicker("Turn is running \u2014 how should this be sent?", [
5696
+ {
5697
+ value: "steer",
5698
+ label: "insert into the running turn",
5699
+ description: "delivered to the model at the next step boundary"
5700
+ },
5701
+ {
5702
+ value: "queue",
5703
+ label: "queue for after the turn",
5704
+ description: "sent as a new prompt once the current turn finishes"
5705
+ }
5706
+ ]);
5707
+ if (picked === "steer") {
5708
+ backend2.injectUserMessage(text);
5709
+ pushMessage(
5710
+ "user",
5711
+ text,
5712
+ "steering the running turn \u2014 the model sees it at the next step",
5713
+ void 0,
5714
+ void 0,
5715
+ void 0,
5716
+ true
5717
+ );
5718
+ return;
5719
+ }
5720
+ queueRef.current.enqueue({ text, images });
5721
+ setQueueItems(queueRef.current.list());
5722
+ })();
5723
+ return;
5724
+ }
5482
5725
  queueRef.current.enqueue({ text, images });
5483
5726
  setQueueItems(queueRef.current.list());
5484
5727
  return;
5485
5728
  }
5486
5729
  void runStream(text, images);
5487
5730
  },
5488
- [registry, pushMessage, runStream, runShellBang, config, clearPendingImages, sessionCostUsd]
5731
+ [
5732
+ registry,
5733
+ pushMessage,
5734
+ runStream,
5735
+ runShellBang,
5736
+ config,
5737
+ clearPendingImages,
5738
+ sessionCostUsd,
5739
+ showPicker
5740
+ ]
5489
5741
  );
5490
5742
  const commandHints = useMemo(
5491
5743
  () => registry.list().map((cmd) => ({
@@ -5497,15 +5749,26 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
5497
5749
  [registry]
5498
5750
  );
5499
5751
  useEffect4(() => {
5500
- setGitBranch(getGitSummaryCached(cwd)?.branch ?? null);
5501
- const current = backendRef.current;
5502
- if (current instanceof AgentLoop) {
5503
- const window_ = contextWindowTokens(config, modelNameRef.current);
5504
- const pct = (estimateTokens(current.getMessages()) + current.getToolSchemaTokens()) / window_ * 100;
5505
- setContextPercent(pct < 10 ? Math.round(pct * 10) / 10 : Math.round(pct));
5506
- } else {
5507
- setContextPercent(null);
5508
- }
5752
+ const refresh = () => {
5753
+ setGitBranch(getGitSummaryCached(cwd)?.branch ?? null);
5754
+ const current = backendRef.current;
5755
+ if (current instanceof AgentLoop) {
5756
+ const window_ = contextWindowTokens(config, modelNameRef.current);
5757
+ const used = current.estimateContextTokens();
5758
+ const pct = used / window_ * 100;
5759
+ setContextUsage({
5760
+ percent: pct < 10 ? Math.round(pct * 10) / 10 : Math.round(pct),
5761
+ used,
5762
+ window: window_
5763
+ });
5764
+ } else {
5765
+ setContextUsage(null);
5766
+ }
5767
+ };
5768
+ refresh();
5769
+ if (!isStreaming) return;
5770
+ const timer = setInterval(refresh, 2e3);
5771
+ return () => clearInterval(timer);
5509
5772
  }, [cwd, config, isStreaming, epoch, modelName]);
5510
5773
  const sessionCost = sessionCostUsd();
5511
5774
  return /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", children: [
@@ -5545,7 +5808,8 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
5545
5808
  title: picker.title,
5546
5809
  options: picker.options,
5547
5810
  onSelect: handlePickerSelect,
5548
- onCancel: handlePickerCancel
5811
+ onCancel: handlePickerCancel,
5812
+ onDelete: picker.onDelete
5549
5813
  }
5550
5814
  ),
5551
5815
  pendingConnect && /* @__PURE__ */ jsx11(
@@ -5598,7 +5862,8 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
5598
5862
  permissionMode: permissionModeState,
5599
5863
  tokens: usageRef.current.totalTokens,
5600
5864
  gitBranch,
5601
- contextPercent,
5865
+ contextPercent: contextUsage?.percent ?? null,
5866
+ contextTokens: contextUsage ? { used: contextUsage.used, window: contextUsage.window } : null,
5602
5867
  cachePercent: usageRef.current.cache ? cacheHitPercent(usageRef.current.cache) : null,
5603
5868
  sessionCostUsd: sessionCost,
5604
5869
  backgroundTasks: bgLabels
@@ -5634,4 +5899,4 @@ export {
5634
5899
  Repl,
5635
5900
  renderRepl
5636
5901
  };
5637
- //# sourceMappingURL=repl-GBVIW5QR.js.map
5902
+ //# sourceMappingURL=repl-SSCURTAH.js.map