@cryer/star-cli 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  AgentLoop,
4
+ MAX_IMAGE_DIMENSION,
4
5
  SKIP_DIRS,
5
6
  TodoStore,
6
7
  WORKING_DIFF_MAX_LINES,
@@ -14,8 +15,10 @@ import {
14
15
  compactMessages,
15
16
  createDefaultRegistry,
16
17
  createIgnorePredicate,
18
+ defaultAgentTasks,
17
19
  defaultTaskManager,
18
20
  discoverSkills,
21
+ envFilePath,
19
22
  estimateTokens,
20
23
  formatTaskFinished,
21
24
  formatTaskList,
@@ -29,16 +32,22 @@ import {
29
32
  isGitRepo,
30
33
  isSensitivePath,
31
34
  listSnapshots,
35
+ listTurnSnapshots,
36
+ loadTodos,
37
+ parseTodoArgs,
38
+ probeImageDimensions,
32
39
  projectConfigPath,
33
40
  reconcileToolCalls,
41
+ resetTodos,
34
42
  resolveShell,
43
+ resolveSnapshotContent,
35
44
  rewindToSnapshot,
36
45
  sessionsDir,
37
46
  starHome,
38
47
  summarizeMessages,
39
48
  undoTurnSnapshots,
40
49
  userMemoryPath
41
- } from "./chunk-GN236FD4.js";
50
+ } from "./chunk-K4SIXWCI.js";
42
51
 
43
52
  // src/main.tsx
44
53
  import { Command } from "commander";
@@ -106,10 +115,125 @@ function buildDisplayMessages(messages) {
106
115
  }
107
116
 
108
117
  // src/cli/mentions.ts
109
- import { readFile, stat } from "fs/promises";
118
+ import { readFile as readFile2, readdir, stat } from "fs/promises";
119
+ import path2 from "path";
120
+
121
+ // src/cli/image.ts
122
+ import { execFile } from "child_process";
123
+ import { randomUUID } from "crypto";
124
+ import { readFile, unlink, writeFile } from "fs/promises";
125
+ import os from "os";
110
126
  import path from "path";
127
+ import { promisify } from "util";
128
+ var RESIZE_TIMEOUT_MS = 1e4;
129
+ var execFileAsync = promisify(execFile);
130
+ function windowsResizeScript(inputFile, outputFile, maxDimension) {
131
+ const input = inputFile.replace(/'/g, "''");
132
+ const output = outputFile.replace(/'/g, "''");
133
+ return [
134
+ "Add-Type -AssemblyName System.Drawing",
135
+ `$img = [System.Drawing.Image]::FromFile('${input}')`,
136
+ `$scale = [Math]::Min(1.0, ${maxDimension} / [Math]::Max($img.Width, $img.Height))`,
137
+ "$w = [int][Math]::Max(1, [Math]::Round($img.Width * $scale))",
138
+ "$h = [int][Math]::Max(1, [Math]::Round($img.Height * $scale))",
139
+ "$bmp = New-Object System.Drawing.Bitmap($w, $h)",
140
+ "$g = [System.Drawing.Graphics]::FromImage($bmp)",
141
+ "$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic",
142
+ "$g.DrawImage($img, 0, 0, $w, $h)",
143
+ `$bmp.Save('${output}', [System.Drawing.Imaging.ImageFormat]::Png)`,
144
+ "$g.Dispose(); $bmp.Dispose(); $img.Dispose()"
145
+ ].join("; ");
146
+ }
147
+ function resizeImagePlan(platform, inputFile, outputFile, maxDimension, tools = {}) {
148
+ if (platform === "win32") {
149
+ return {
150
+ command: "powershell.exe",
151
+ args: [
152
+ "-NoProfile",
153
+ "-NonInteractive",
154
+ "-Command",
155
+ windowsResizeScript(inputFile, outputFile, maxDimension)
156
+ ]
157
+ };
158
+ }
159
+ if (platform === "darwin") {
160
+ return {
161
+ command: "sips",
162
+ args: ["-Z", String(maxDimension), inputFile, "--out", outputFile]
163
+ };
164
+ }
165
+ if (platform === "linux") {
166
+ if (tools.hasConvert) {
167
+ return {
168
+ command: "convert",
169
+ args: [inputFile, "-resize", `${maxDimension}x${maxDimension}>`, outputFile]
170
+ };
171
+ }
172
+ if (tools.hasFfmpeg) {
173
+ return {
174
+ command: "ffmpeg",
175
+ args: [
176
+ "-y",
177
+ "-i",
178
+ inputFile,
179
+ "-vf",
180
+ `scale='if(gt(iw,ih),min(iw,${maxDimension}),-1)':'if(gt(iw,ih),-1,min(ih,${maxDimension}))'`,
181
+ outputFile
182
+ ]
183
+ };
184
+ }
185
+ return null;
186
+ }
187
+ return null;
188
+ }
189
+ async function commandExists(command) {
190
+ const checker = process.platform === "win32" ? "where" : "which";
191
+ try {
192
+ await execFileAsync(checker, [command], { timeout: RESIZE_TIMEOUT_MS });
193
+ return true;
194
+ } catch {
195
+ return false;
196
+ }
197
+ }
198
+ async function downsampleImageIfNeeded(image) {
199
+ const buf = Buffer.from(image.data, "base64");
200
+ const dims = probeImageDimensions(buf);
201
+ if (!dims || Math.max(dims.width, dims.height) <= MAX_IMAGE_DIMENSION) {
202
+ return { image, oversized: false, resized: false };
203
+ }
204
+ const inputFile = path.join(os.tmpdir(), `star-image-${randomUUID()}.src`);
205
+ const outputFile = path.join(os.tmpdir(), `star-image-${randomUUID()}.png`);
206
+ try {
207
+ const [hasConvert, hasFfmpeg] = process.platform === "linux" ? await Promise.all([commandExists("convert"), commandExists("ffmpeg")]) : [false, false];
208
+ const plan = resizeImagePlan(process.platform, inputFile, outputFile, MAX_IMAGE_DIMENSION, {
209
+ hasConvert,
210
+ hasFfmpeg
211
+ });
212
+ if (!plan) return { image, oversized: true, resized: false };
213
+ await writeFile(inputFile, buf);
214
+ await execFileAsync(plan.command, plan.args, { timeout: RESIZE_TIMEOUT_MS });
215
+ const out = await readFile(outputFile).catch(() => null);
216
+ if (!out || out.length === 0) return { image, oversized: true, resized: false };
217
+ return {
218
+ image: { path: image.path, mimeType: "image/png", data: out.toString("base64") },
219
+ oversized: true,
220
+ resized: true
221
+ };
222
+ } catch {
223
+ return { image, oversized: true, resized: false };
224
+ } finally {
225
+ await unlink(inputFile).catch(() => {
226
+ });
227
+ await unlink(outputFile).catch(() => {
228
+ });
229
+ }
230
+ }
231
+
232
+ // src/cli/mentions.ts
111
233
  var MAX_MENTION_BYTES = 100 * 1024;
112
234
  var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
235
+ var MAX_DIR_MENTION_ENTRIES = 200;
236
+ var MAX_DIR_MENTION_DEPTH = 10;
113
237
  var IMAGE_MIME_TYPES = {
114
238
  ".png": "image/png",
115
239
  ".jpg": "image/jpeg",
@@ -118,17 +242,22 @@ var IMAGE_MIME_TYPES = {
118
242
  ".webp": "image/webp"
119
243
  };
120
244
  function imageMimeType(filePath) {
121
- return IMAGE_MIME_TYPES[path.extname(filePath).toLowerCase()] ?? null;
245
+ return IMAGE_MIME_TYPES[path2.extname(filePath).toLowerCase()] ?? null;
122
246
  }
123
247
  async function readImageInput(filePath, cwd) {
124
248
  const mimeType = imageMimeType(filePath);
125
249
  if (!mimeType) return null;
126
- const abs = path.resolve(cwd, filePath);
250
+ const abs = path2.resolve(cwd, filePath);
127
251
  const st = await stat(abs).catch(() => null);
128
252
  if (!st || !st.isFile() || st.size > MAX_IMAGE_BYTES) return null;
129
- const buf = await readFile(abs).catch(() => null);
253
+ const buf = await readFile2(abs).catch(() => null);
130
254
  if (!buf) return null;
131
- return { path: filePath, mimeType, data: buf.toString("base64") };
255
+ const result = await downsampleImageIfNeeded({
256
+ path: filePath,
257
+ mimeType,
258
+ data: buf.toString("base64")
259
+ });
260
+ return result.image;
132
261
  }
133
262
  var MENTION_RE = /(?<=^|\s)@([A-Za-z]:[\\/][A-Za-z0-9._\-/\\]*|[A-Za-z0-9._\-/\\]+)/g;
134
263
  function parseMentions(text) {
@@ -162,6 +291,51 @@ function isBinaryContent(buf) {
162
291
  const probe = buf.subarray(0, 8e3);
163
292
  return probe.includes(0);
164
293
  }
294
+ async function listDirEntries(dir, prefix, depth, state, ignore) {
295
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
296
+ if (!entries) return;
297
+ const visible = entries.filter((entry) => entry.isDirectory() || entry.isFile()).filter((entry) => !isSensitivePath(entry.name)).filter((entry) => !ignore?.(path2.join(dir, entry.name), entry.isDirectory())).sort((a, b) => {
298
+ const dirDiff = Number(b.isDirectory()) - Number(a.isDirectory());
299
+ return dirDiff !== 0 ? dirDiff : a.name.localeCompare(b.name);
300
+ });
301
+ for (let i = 0; i < visible.length; i++) {
302
+ const entry = visible[i];
303
+ if (!entry) continue;
304
+ state.total += 1;
305
+ const last = i === visible.length - 1;
306
+ const isDir = entry.isDirectory();
307
+ if (state.shown < MAX_DIR_MENTION_ENTRIES) {
308
+ state.shown += 1;
309
+ state.lines.push(`${prefix}${last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${entry.name}${isDir ? "/" : ""}`);
310
+ if (isDir && depth < MAX_DIR_MENTION_DEPTH) {
311
+ await listDirEntries(
312
+ path2.join(dir, entry.name),
313
+ `${prefix}${last ? " " : "\u2502 "}`,
314
+ depth + 1,
315
+ state,
316
+ ignore
317
+ );
318
+ }
319
+ } else if (isDir && depth < MAX_DIR_MENTION_DEPTH) {
320
+ await listDirEntries(path2.join(dir, entry.name), prefix, depth + 1, state, ignore);
321
+ }
322
+ }
323
+ }
324
+ async function directoryBlock(abs, mention, ignore) {
325
+ const display = `${mention.replace(/\\/g, "/").replace(/\/+$/, "")}/`;
326
+ const state = { lines: [`${display}`], shown: 0, total: 0 };
327
+ const probe = await readdir(abs).catch(() => null);
328
+ if (!probe) return null;
329
+ await listDirEntries(abs, "", 1, state, ignore);
330
+ if (state.total > state.shown) {
331
+ state.lines.push(
332
+ `... (truncated, ${state.total - state.shown} more entries \u2014 use glob/grep tools or mention a subdirectory to see more)`
333
+ );
334
+ }
335
+ return `--- @${display} (directory) ---
336
+ ${state.lines.join("\n")}
337
+ --- end ---`;
338
+ }
165
339
  async function resolveMentions(text, cwd) {
166
340
  const { cleanText, mentions } = parseMentions(text);
167
341
  if (mentions.length === 0) {
@@ -171,8 +345,9 @@ async function resolveMentions(text, cwd) {
171
345
  const attached = [];
172
346
  const skipped = [];
173
347
  const images = [];
348
+ let ignore;
174
349
  for (const mention of mentions) {
175
- const abs = path.resolve(cwd, mention);
350
+ const abs = path2.resolve(cwd, mention);
176
351
  if (isSensitivePath(abs)) {
177
352
  skipped.push({ path: mention, reason: "sensitive file" });
178
353
  continue;
@@ -188,13 +363,18 @@ async function resolveMentions(text, cwd) {
188
363
  skipped.push({ path: mention, reason: "image too large" });
189
364
  continue;
190
365
  }
191
- const buf2 = await readFile(abs).catch(() => null);
366
+ const buf2 = await readFile2(abs).catch(() => null);
192
367
  if (!buf2) {
193
368
  skipped.push({ path: mention, reason: "unreadable" });
194
369
  continue;
195
370
  }
371
+ const result = await downsampleImageIfNeeded({
372
+ path: mention,
373
+ mimeType,
374
+ data: buf2.toString("base64")
375
+ });
196
376
  attached.push(mention);
197
- images.push({ path: mention, mimeType, data: buf2.toString("base64") });
377
+ images.push(result.image);
198
378
  continue;
199
379
  }
200
380
  const st = await stat(abs).catch(() => null);
@@ -203,14 +383,23 @@ async function resolveMentions(text, cwd) {
203
383
  continue;
204
384
  }
205
385
  if (st.isDirectory()) {
206
- skipped.push({ path: mention, reason: "is a directory" });
386
+ if (ignore === void 0) {
387
+ ignore = createIgnorePredicate(cwd) ?? null;
388
+ }
389
+ const block = await directoryBlock(abs, mention, ignore ?? void 0);
390
+ if (!block) {
391
+ skipped.push({ path: mention, reason: "unreadable" });
392
+ continue;
393
+ }
394
+ attached.push(mention);
395
+ blocks.push(block);
207
396
  continue;
208
397
  }
209
398
  if (st.size > MAX_MENTION_BYTES) {
210
399
  skipped.push({ path: mention, reason: `exceeds ${MAX_MENTION_BYTES / 1024}KB limit` });
211
400
  continue;
212
401
  }
213
- const buf = await readFile(abs).catch(() => null);
402
+ const buf = await readFile2(abs).catch(() => null);
214
403
  if (!buf) {
215
404
  skipped.push({ path: mention, reason: "unreadable" });
216
405
  continue;
@@ -272,12 +461,12 @@ var UsageTracker = class {
272
461
  };
273
462
 
274
463
  // src/cli/repl.tsx
275
- import { Box as Box10, Text as Text11, render, useApp, useInput as useInput6 } from "ink";
276
- import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState3 } from "react";
464
+ import { Box as Box13, Text as Text14, render, useApp, useInput as useInput8 } from "ink";
465
+ import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef5, useState as useState5 } from "react";
277
466
 
278
467
  // src/config/save.ts
279
468
  import fs from "fs";
280
- import path2 from "path";
469
+ import path3 from "path";
281
470
  import { parse, stringify } from "smol-toml";
282
471
  async function addAllowRule(rule) {
283
472
  return addPermissionRule("allow", rule);
@@ -296,7 +485,7 @@ async function addPermissionRule(kind, rule) {
296
485
  if (rules.includes(rule)) return false;
297
486
  permissions[kind] = [...rules, rule];
298
487
  raw.permissions = permissions;
299
- await fs.promises.mkdir(path2.dirname(filePath), { recursive: true });
488
+ await fs.promises.mkdir(path3.dirname(filePath), { recursive: true });
300
489
  await fs.promises.writeFile(filePath, stringify(raw), "utf8");
301
490
  return true;
302
491
  }
@@ -310,7 +499,7 @@ async function savePermissionMode(mode) {
310
499
  if (error.code !== "ENOENT") throw error;
311
500
  }
312
501
  raw.permissionMode = mode;
313
- await fs.promises.mkdir(path2.dirname(filePath), { recursive: true });
502
+ await fs.promises.mkdir(path3.dirname(filePath), { recursive: true });
314
503
  await fs.promises.writeFile(filePath, stringify(raw), "utf8");
315
504
  }
316
505
 
@@ -368,6 +557,23 @@ var ConfigSchema = z.object({
368
557
  // gracefully (some relays never send the terminal chunks). Applies once
369
558
  // streaming has started; the first part gets a longer, fixed allowance.
370
559
  streamIdleTimeoutSec: z.number().positive().default(20),
560
+ // Seconds to wait for the very first stream part before giving up; slow
561
+ // thinking-model relays can buffer for minutes before sending anything.
562
+ streamFirstChunkTimeoutSec: z.number().positive().default(300),
563
+ // Extra attempts per model request when a stream fails transiently
564
+ // (network error, 429/5xx, idle watchdog cutoff) or comes back empty, so a
565
+ // relay hiccup does not silently end a half-finished turn. 0 disables.
566
+ // Retries after a timeout/empty failure scale both stream timeouts up
567
+ // (attempt number ×, capped at 3x) since the relay is likely overloaded.
568
+ streamMaxRetries: z.number().int().min(0).default(3),
569
+ // How many consecutive unproductive replies may be nudged before the turn
570
+ // is handed back: a text-only reply that announces pending work ("我将…",
571
+ // "I will…") or answers a nudge with more words, or an empty reply the
572
+ // model finished deliberately (steered with a nudge after one identical
573
+ // resend instead of burning the stream retry budget). Tool calls reset the
574
+ // count, so a productive task keeps going; exhaustion surfaces a notice
575
+ // instead of stopping silently. Never fires in plan mode. 0 disables.
576
+ maxAutoContinues: z.number().int().min(0).default(2),
371
577
  contextCompaction: z.enum(["summary", "truncate"]).default("summary"),
372
578
  // Optional per-session dollar budget; when set, the REPL stops the session
373
579
  // once the estimated cost for the session reaches this amount.
@@ -448,15 +654,15 @@ function createModel(config, modelName) {
448
654
 
449
655
  // src/session/checkpoints.ts
450
656
  import fs2 from "fs/promises";
451
- import path3 from "path";
657
+ import path4 from "path";
452
658
  function checkpointsDir(sessionDir) {
453
- return path3.join(sessionDir, "checkpoints");
659
+ return path4.join(sessionDir, "checkpoints");
454
660
  }
455
661
  function indexPath(sessionDir) {
456
- return path3.join(checkpointsDir(sessionDir), "index.json");
662
+ return path4.join(checkpointsDir(sessionDir), "index.json");
457
663
  }
458
664
  function checkpointContentPath(sessionDir, id) {
459
- return path3.join(checkpointsDir(sessionDir), `${id}.snapshot`);
665
+ return path4.join(checkpointsDir(sessionDir), `${id}.snapshot`);
460
666
  }
461
667
  async function listCheckpointRecords(sessionDir) {
462
668
  let raw;
@@ -504,11 +710,11 @@ async function loadSessionSnapshots(sessionDir) {
504
710
 
505
711
  // src/session/clear.ts
506
712
  import fs4 from "fs/promises";
507
- import path5 from "path";
713
+ import path6 from "path";
508
714
 
509
715
  // src/session/store.ts
510
716
  import fs3 from "fs/promises";
511
- import path4 from "path";
717
+ import path5 from "path";
512
718
  function generateId(now) {
513
719
  const pad = (n) => String(n).padStart(2, "0");
514
720
  const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
@@ -530,13 +736,13 @@ var SessionStore = class _SessionStore {
530
736
  initialized = false;
531
737
  constructor(id) {
532
738
  this.id = id;
533
- this.dir = path4.join(sessionsDir(), id);
739
+ this.dir = path5.join(sessionsDir(), id);
534
740
  }
535
741
  metaPath() {
536
- return path4.join(this.dir, "meta.json");
742
+ return path5.join(this.dir, "meta.json");
537
743
  }
538
744
  messagesPath() {
539
- return path4.join(this.dir, "messages.jsonl");
745
+ return path5.join(this.dir, "messages.jsonl");
540
746
  }
541
747
  static async create(cwd, model) {
542
748
  const store = new _SessionStore(generateId(/* @__PURE__ */ new Date()));
@@ -570,7 +776,7 @@ var SessionStore = class _SessionStore {
570
776
  const metas = [];
571
777
  for (const entry of entries) {
572
778
  try {
573
- const raw = await fs3.readFile(path4.join(sessionsDir(), entry, "meta.json"), "utf8");
779
+ const raw = await fs3.readFile(path5.join(sessionsDir(), entry, "meta.json"), "utf8");
574
780
  const meta = JSON.parse(raw);
575
781
  if (meta.id === entry && (cwd === void 0 || meta.cwd === cwd)) metas.push(meta);
576
782
  } catch {
@@ -697,7 +903,7 @@ async function clearSessions(cwd, excludeId) {
697
903
  let removed2 = 0;
698
904
  for (const meta of metas) {
699
905
  if (meta.id === excludeId) continue;
700
- await fs4.rm(path5.join(sessionsDir(), meta.id), { recursive: true, force: true });
906
+ await fs4.rm(path6.join(sessionsDir(), meta.id), { recursive: true, force: true });
701
907
  removed2 += 1;
702
908
  }
703
909
  return removed2;
@@ -711,7 +917,7 @@ async function clearSessions(cwd, excludeId) {
711
917
  let removed = 0;
712
918
  for (const entry of entries) {
713
919
  if (entry === excludeId) continue;
714
- await fs4.rm(path5.join(sessionsDir(), entry), { recursive: true, force: true });
920
+ await fs4.rm(path6.join(sessionsDir(), entry), { recursive: true, force: true });
715
921
  removed += 1;
716
922
  }
717
923
  return removed;
@@ -719,11 +925,11 @@ async function clearSessions(cwd, excludeId) {
719
925
 
720
926
  // src/session/list.ts
721
927
  import fs5 from "fs/promises";
722
- import path6 from "path";
928
+ import path7 from "path";
723
929
  async function countMessages(id) {
724
930
  let raw;
725
931
  try {
726
- raw = await fs5.readFile(path6.join(sessionsDir(), id, "messages.jsonl"), "utf8");
932
+ raw = await fs5.readFile(path7.join(sessionsDir(), id, "messages.jsonl"), "utf8");
727
933
  } catch {
728
934
  return 0;
729
935
  }
@@ -981,11 +1187,11 @@ function formatUsageDashboard(stats, models, options = {}) {
981
1187
 
982
1188
  // src/version.ts
983
1189
  import fs6 from "fs";
984
- import path7 from "path";
1190
+ import path8 from "path";
985
1191
  import { fileURLToPath } from "url";
986
1192
  function readPackageVersion() {
987
1193
  try {
988
- const pkgPath = path7.join(path7.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
1194
+ const pkgPath = path8.join(path8.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
989
1195
  const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf8"));
990
1196
  if (typeof pkg === "object" && pkg !== null) {
991
1197
  const version = pkg.version;
@@ -1006,16 +1212,30 @@ function budgetState(costUsd, budgetUsd) {
1006
1212
  return "ok";
1007
1213
  }
1008
1214
 
1215
+ // src/cli/cache-stats.ts
1216
+ function cacheUsageReported(usage) {
1217
+ return usage.cachedPromptTokens != null || usage.cacheReadInputTokens != null;
1218
+ }
1219
+ function addToCacheTotals(totals, usage) {
1220
+ totals.cachedTokens += (usage.cachedPromptTokens ?? 0) + (usage.cacheReadInputTokens ?? 0);
1221
+ totals.promptTokens += usage.promptTokens + (usage.cacheReadInputTokens ?? 0);
1222
+ }
1223
+ function cacheHitPercent(totals) {
1224
+ if (totals.promptTokens <= 0) return null;
1225
+ const rate = totals.cachedTokens / totals.promptTokens;
1226
+ return Math.round(Math.min(1, Math.max(0, rate)) * 100);
1227
+ }
1228
+
1009
1229
  // src/cli/clipboard.ts
1010
- import { execFile } from "child_process";
1011
- import { randomUUID } from "crypto";
1012
- import { readFile as readFile2, unlink } from "fs/promises";
1013
- import os from "os";
1014
- import path8 from "path";
1015
- import { promisify } from "util";
1230
+ import { execFile as execFile2 } from "child_process";
1231
+ import { randomUUID as randomUUID2 } from "crypto";
1232
+ import { readFile as readFile3, unlink as unlink2 } from "fs/promises";
1233
+ import os2 from "os";
1234
+ import path9 from "path";
1235
+ import { promisify as promisify2 } from "util";
1016
1236
  var CLIPBOARD_TIMEOUT_MS = 5e3;
1017
1237
  var MAX_CLIPBOARD_IMAGE_BYTES = 10 * 1024 * 1024;
1018
- var execFileAsync = promisify(execFile);
1238
+ var execFileAsync2 = promisify2(execFile2);
1019
1239
  function windowsClipboardImageScript(tempFile) {
1020
1240
  const escaped = tempFile.replace(/'/g, "''");
1021
1241
  return [
@@ -1075,40 +1295,52 @@ function clipboardCopyCommand(platform) {
1075
1295
  if (platform === "linux") return { command: "xclip", args: ["-selection", "clipboard"] };
1076
1296
  return null;
1077
1297
  }
1078
- async function commandExists(command) {
1298
+ async function commandExists2(command) {
1079
1299
  const checker = process.platform === "win32" ? "where" : "which";
1080
1300
  try {
1081
- await execFileAsync(checker, [command], { timeout: CLIPBOARD_TIMEOUT_MS });
1301
+ await execFileAsync2(checker, [command], { timeout: CLIPBOARD_TIMEOUT_MS });
1082
1302
  return true;
1083
1303
  } catch {
1084
1304
  return false;
1085
1305
  }
1086
1306
  }
1307
+ async function finalizeClipboardImage(image) {
1308
+ const result = await downsampleImageIfNeeded(image);
1309
+ return { image: result.image, oversized: result.oversized && !result.resized };
1310
+ }
1087
1311
  async function readClipboardImage() {
1088
- const tempFile = path8.join(os.tmpdir(), `star-clipboard-${randomUUID()}.png`);
1089
- const hasPngpaste = process.platform === "darwin" ? await commandExists("pngpaste") : false;
1312
+ const tempFile = path9.join(os2.tmpdir(), `star-clipboard-${randomUUID2()}.png`);
1313
+ const hasPngpaste = process.platform === "darwin" ? await commandExists2("pngpaste") : false;
1090
1314
  const plan = clipboardImagePlan(process.platform, tempFile, hasPngpaste);
1091
1315
  if (!plan) return null;
1092
1316
  try {
1093
1317
  if (plan.kind === "stdout") {
1094
- const { stdout } = await execFileAsync(plan.command, plan.args, {
1318
+ const { stdout } = await execFileAsync2(plan.command, plan.args, {
1095
1319
  timeout: CLIPBOARD_TIMEOUT_MS,
1096
1320
  maxBuffer: MAX_CLIPBOARD_IMAGE_BYTES,
1097
1321
  encoding: "buffer"
1098
1322
  });
1099
1323
  const buf2 = stdout;
1100
1324
  if (buf2.length === 0) return null;
1101
- return { path: "clipboard.png", mimeType: "image/png", data: buf2.toString("base64") };
1325
+ return finalizeClipboardImage({
1326
+ path: "clipboard.png",
1327
+ mimeType: "image/png",
1328
+ data: buf2.toString("base64")
1329
+ });
1102
1330
  }
1103
- await execFileAsync(plan.command, plan.args, { timeout: CLIPBOARD_TIMEOUT_MS });
1104
- const buf = await readFile2(tempFile).catch(() => null);
1331
+ await execFileAsync2(plan.command, plan.args, { timeout: CLIPBOARD_TIMEOUT_MS });
1332
+ const buf = await readFile3(tempFile).catch(() => null);
1105
1333
  if (!buf || buf.length === 0) return null;
1106
- return { path: "clipboard.png", mimeType: "image/png", data: buf.toString("base64") };
1334
+ return finalizeClipboardImage({
1335
+ path: "clipboard.png",
1336
+ mimeType: "image/png",
1337
+ data: buf.toString("base64")
1338
+ });
1107
1339
  } catch {
1108
1340
  return null;
1109
1341
  } finally {
1110
1342
  if (plan.kind === "temp-file") {
1111
- await unlink(tempFile).catch(() => {
1343
+ await unlink2(tempFile).catch(() => {
1112
1344
  });
1113
1345
  }
1114
1346
  }
@@ -1117,7 +1349,7 @@ async function copyText(text) {
1117
1349
  const plan = clipboardCopyCommand(process.platform);
1118
1350
  if (!plan) return false;
1119
1351
  return new Promise((resolve) => {
1120
- const child = execFile(
1352
+ const child = execFile2(
1121
1353
  plan.command,
1122
1354
  plan.args,
1123
1355
  { timeout: CLIPBOARD_TIMEOUT_MS },
@@ -1131,7 +1363,7 @@ async function copyText(text) {
1131
1363
 
1132
1364
  // src/cli/commands/actions.ts
1133
1365
  import fs7 from "fs/promises";
1134
- import path9 from "path";
1366
+ import path10 from "path";
1135
1367
  var MIN_COMPACT_MESSAGES = 4;
1136
1368
  async function compactSession(opts) {
1137
1369
  const { backend, sessionStore, config, model } = opts;
@@ -1192,9 +1424,9 @@ async function exportSession(opts) {
1192
1424
  return "Nothing to export: the session has no messages.";
1193
1425
  }
1194
1426
  const sessionId = sessionStore?.id ?? `unknown-${Date.now()}`;
1195
- const target = arg ? path9.resolve(cwd, arg) : path9.resolve(cwd, `star-session-${sessionId}.md`);
1427
+ const target = arg ? path10.resolve(cwd, arg) : path10.resolve(cwd, `star-session-${sessionId}.md`);
1196
1428
  const existed = await fileExists(target);
1197
- await fs7.mkdir(path9.dirname(target), { recursive: true });
1429
+ await fs7.mkdir(path10.dirname(target), { recursive: true });
1198
1430
  await fs7.writeFile(target, renderSessionMarkdown(sessionId, messages));
1199
1431
  return existed ? `Exported ${messages.length} messages to ${target} (overwrote existing file).` : `Exported ${messages.length} messages to ${target}.`;
1200
1432
  }
@@ -1316,8 +1548,8 @@ function formatSearchResults(hits, query) {
1316
1548
  }
1317
1549
 
1318
1550
  // src/cli/diff-preview.ts
1319
- import { readFile as readFile3 } from "fs/promises";
1320
- import path10 from "path";
1551
+ import { readFile as readFile4 } from "fs/promises";
1552
+ import path11 from "path";
1321
1553
  var CONTEXT_LINES = 2;
1322
1554
  var MAX_HUNK_LINES = 15;
1323
1555
  var MAX_TOTAL_LINES = 12;
@@ -1478,10 +1710,10 @@ async function generateDiffPreview(toolName, args, cwd) {
1478
1710
  if (toolName !== "edit_file" && toolName !== "write_file") return null;
1479
1711
  const filePath = getStringArg(args, "path");
1480
1712
  if (filePath === void 0) return null;
1481
- const absolute = path10.resolve(cwd, filePath);
1713
+ const absolute = path11.resolve(cwd, filePath);
1482
1714
  let existing;
1483
1715
  try {
1484
- existing = await readFile3(absolute, "utf8");
1716
+ existing = await readFile4(absolute, "utf8");
1485
1717
  } catch {
1486
1718
  existing = null;
1487
1719
  }
@@ -1778,6 +2010,19 @@ ${content}`);
1778
2010
  ctx.addSystemMessage(await ctx.rewind(args.trim()));
1779
2011
  }
1780
2012
  });
2013
+ register({
2014
+ name: "connect",
2015
+ description: "Interactive wizard: add an LLM provider + model, store the key safely, and save to config",
2016
+ usage: "/connect",
2017
+ category: "Settings",
2018
+ async run(_args, ctx) {
2019
+ if (!ctx.connect) {
2020
+ ctx.addSystemMessage("/connect: this context cannot run the interactive wizard.");
2021
+ return;
2022
+ }
2023
+ ctx.addSystemMessage(await ctx.connect());
2024
+ }
2025
+ });
1781
2026
  register({
1782
2027
  name: "init",
1783
2028
  description: "Generate an AGENTS.md for the current project",
@@ -1905,9 +2150,260 @@ ${result.diff}${note ? `
1905
2150
  });
1906
2151
  }
1907
2152
 
2153
+ // src/cli/commands/connect.ts
2154
+ import { spawn } from "child_process";
2155
+ import fs9 from "fs";
2156
+ import path12 from "path";
2157
+
2158
+ // src/config/env.ts
2159
+ import fs8 from "fs";
2160
+ var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
2161
+ function parseEnvContent(content) {
2162
+ const vars = {};
2163
+ for (const rawLine of content.split(/\r?\n/)) {
2164
+ const line = rawLine.trim();
2165
+ if (!line || line.startsWith("#")) continue;
2166
+ const eq = line.indexOf("=");
2167
+ if (eq <= 0) continue;
2168
+ const key = line.slice(0, eq).trim();
2169
+ if (!ENV_KEY_PATTERN.test(key)) continue;
2170
+ let value = line.slice(eq + 1).trim();
2171
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
2172
+ value = value.slice(1, -1);
2173
+ }
2174
+ vars[key] = value;
2175
+ }
2176
+ return vars;
2177
+ }
2178
+ function loadEnvFile(filePath = envFilePath()) {
2179
+ let content;
2180
+ try {
2181
+ content = fs8.readFileSync(filePath, "utf8");
2182
+ } catch {
2183
+ return;
2184
+ }
2185
+ for (const [key, value] of Object.entries(parseEnvContent(content))) {
2186
+ if (process.env[key] === void 0) {
2187
+ process.env[key] = value;
2188
+ }
2189
+ }
2190
+ }
2191
+ function upsertEnvContent(content, key, value) {
2192
+ const entry = `${key}=${value.replace(/[\r\n]/g, "")}`;
2193
+ const pattern = new RegExp(`^\\s*${key}\\s*=`);
2194
+ const lines = content.split("\n");
2195
+ while (lines.length > 0 && (lines[lines.length - 1] ?? "").trim() === "") lines.pop();
2196
+ for (let i = 0; i < lines.length; i++) {
2197
+ if (pattern.test(lines[i] ?? "")) {
2198
+ lines[i] = entry;
2199
+ return `${lines.join("\n")}
2200
+ `;
2201
+ }
2202
+ }
2203
+ lines.push(entry);
2204
+ return `${lines.join("\n")}
2205
+ `;
2206
+ }
2207
+
2208
+ // src/cli/commands/connect.ts
2209
+ var PROVIDER_PRESETS = [
2210
+ {
2211
+ key: "openai",
2212
+ label: "OpenAI",
2213
+ baseURL: "https://api.openai.com/v1",
2214
+ protocol: "openai-compatible",
2215
+ modelHint: "gpt-4o"
2216
+ },
2217
+ {
2218
+ key: "anthropic",
2219
+ label: "Anthropic",
2220
+ baseURL: "https://api.anthropic.com",
2221
+ protocol: "anthropic",
2222
+ modelHint: "claude-sonnet-4-5"
2223
+ },
2224
+ {
2225
+ key: "moonshot",
2226
+ label: "Kimi (Moonshot)",
2227
+ baseURL: "https://api.moonshot.cn/v1",
2228
+ protocol: "openai-compatible",
2229
+ modelHint: "kimi-k2-0905-preview"
2230
+ },
2231
+ {
2232
+ key: "deepseek",
2233
+ label: "DeepSeek",
2234
+ baseURL: "https://api.deepseek.com/v1",
2235
+ protocol: "openai-compatible",
2236
+ modelHint: "deepseek-chat"
2237
+ }
2238
+ ];
2239
+ var PROTOCOLS = ["openai-compatible", "anthropic", "openai-responses"];
2240
+ var PROTOCOL_DESCRIPTIONS = {
2241
+ "openai-compatible": "POST /chat/completions \u2014 most providers",
2242
+ anthropic: "Anthropic Messages API",
2243
+ "openai-responses": "OpenAI Responses API (endpoints exposing only /responses)"
2244
+ };
2245
+ var CONNECT_DEFAULT_CONTEXT_TOKENS = 128e3;
2246
+ function deriveProviderName(baseURL) {
2247
+ let host = baseURL;
2248
+ try {
2249
+ host = new URL(baseURL).hostname;
2250
+ } catch {
2251
+ host = baseURL.replace(/^[a-z]+:\/\//i, "").split(/[/:?#]/)[0] ?? baseURL;
2252
+ }
2253
+ const labels = host.toLowerCase().split(".");
2254
+ const skip = /* @__PURE__ */ new Set(["www", "api"]);
2255
+ const meaningful = labels.filter((label) => label !== "" && !skip.has(label));
2256
+ const raw = meaningful[0] ?? "provider";
2257
+ const slug = raw.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
2258
+ return slug || "provider";
2259
+ }
2260
+ function dedupeName(base, taken) {
2261
+ const names = new Set(taken);
2262
+ if (!names.has(base)) return base;
2263
+ for (let i = 2; ; i++) {
2264
+ const candidate = `${base}-${i}`;
2265
+ if (!names.has(candidate)) return candidate;
2266
+ }
2267
+ }
2268
+ function apiKeyEnvFor(providerName) {
2269
+ return `STAR_API_KEY_${providerName.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
2270
+ }
2271
+ function maskApiKey(key) {
2272
+ if (key.length <= 8) return "\u2022\u2022\u2022\u2022\u2022\u2022";
2273
+ return `${key.slice(0, 3)}\u2026${key.slice(-4)}`;
2274
+ }
2275
+ function tomlString(value) {
2276
+ return JSON.stringify(value);
2277
+ }
2278
+ function buildProviderTomlBlock(provider) {
2279
+ return [
2280
+ "[[providers]]",
2281
+ `name = ${tomlString(provider.name)}`,
2282
+ "# wire format: openai-compatible | anthropic | openai-responses",
2283
+ `protocol = ${tomlString(provider.protocol)}`,
2284
+ `baseURL = ${tomlString(provider.baseURL)}`,
2285
+ "# environment variable holding the API key; the key itself lives in",
2286
+ "# ~/.star-cli/.env (written by /connect), never in this file",
2287
+ `apiKeyEnv = ${tomlString(provider.apiKeyEnv ?? "")}`,
2288
+ "# inline key instead of apiKeyEnv (not recommended):",
2289
+ '# apiKey = "sk-..."',
2290
+ "# extra HTTP headers sent to the provider:",
2291
+ '# headers = { "X-Title" = "star-cli" }',
2292
+ "",
2293
+ ""
2294
+ ].join("\n");
2295
+ }
2296
+ function buildModelTomlBlock(model) {
2297
+ return [
2298
+ "[[models]]",
2299
+ `name = ${tomlString(model.name)}`,
2300
+ `provider = ${tomlString(model.provider)}`,
2301
+ `model = ${tomlString(model.model)}`,
2302
+ "# per-model context window in tokens \u2014 overrides the top-level",
2303
+ "# contextMaxTokens for compaction and the ctx % in the status bar",
2304
+ `contextMaxTokens = ${model.contextMaxTokens ?? CONNECT_DEFAULT_CONTEXT_TOKENS}`,
2305
+ "# pricing in USD per 1M tokens \u2014 set your provider's rates; BOTH",
2306
+ "# promptPrice and completionPrice are required together for the $",
2307
+ "# estimates in /cost and /usage (0 prices a token at $0)",
2308
+ `promptPrice = ${model.promptPrice ?? 0}`,
2309
+ `completionPrice = ${model.completionPrice ?? 0}`,
2310
+ "# cap on generated tokens per reply:",
2311
+ "# maxTokens = 8192",
2312
+ "",
2313
+ ""
2314
+ ].join("\n");
2315
+ }
2316
+ function saveConnection(answers, configPath = globalConfigPath(), envPath = envFilePath()) {
2317
+ const provider = {
2318
+ name: answers.providerName,
2319
+ protocol: answers.protocol,
2320
+ baseURL: answers.baseURL,
2321
+ apiKeyEnv: answers.apiKeyEnv
2322
+ };
2323
+ const model = {
2324
+ name: answers.modelName,
2325
+ provider: answers.providerName,
2326
+ model: answers.modelId,
2327
+ contextMaxTokens: CONNECT_DEFAULT_CONTEXT_TOKENS,
2328
+ promptPrice: 0,
2329
+ completionPrice: 0
2330
+ };
2331
+ fs9.mkdirSync(path12.dirname(envPath), { recursive: true });
2332
+ let envContent = "";
2333
+ try {
2334
+ envContent = fs9.readFileSync(envPath, "utf8");
2335
+ } catch {
2336
+ envContent = "";
2337
+ }
2338
+ fs9.writeFileSync(
2339
+ envPath,
2340
+ upsertEnvContent(envContent, provider.apiKeyEnv ?? "", answers.apiKey),
2341
+ {
2342
+ encoding: "utf8",
2343
+ mode: 384
2344
+ }
2345
+ );
2346
+ try {
2347
+ fs9.chmodSync(envPath, 384);
2348
+ } catch {
2349
+ }
2350
+ if (provider.apiKeyEnv) {
2351
+ process.env[provider.apiKeyEnv] = answers.apiKey;
2352
+ }
2353
+ fs9.mkdirSync(path12.dirname(configPath), { recursive: true });
2354
+ let existing = "";
2355
+ try {
2356
+ existing = fs9.readFileSync(configPath, "utf8");
2357
+ } catch {
2358
+ existing = "";
2359
+ }
2360
+ const separator = existing === "" ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
2361
+ fs9.writeFileSync(
2362
+ configPath,
2363
+ existing + separator + buildProviderTomlBlock(provider) + buildModelTomlBlock(model),
2364
+ "utf8"
2365
+ );
2366
+ return { provider, model, configPath, envPath };
2367
+ }
2368
+ function setDefaultModelInToml(content, modelName) {
2369
+ const line = `defaultModel = ${tomlString(modelName)}`;
2370
+ const lines = content.split("\n");
2371
+ let inTopLevel = true;
2372
+ for (let i = 0; i < lines.length; i++) {
2373
+ const current = lines[i] ?? "";
2374
+ if (/^\s*\[/.test(current)) inTopLevel = false;
2375
+ if (inTopLevel && /^\s*defaultModel\s*=/.test(current)) {
2376
+ lines[i] = line;
2377
+ return lines.join("\n");
2378
+ }
2379
+ }
2380
+ return `${line}
2381
+ ${content}`;
2382
+ }
2383
+ function saveDefaultModel(modelName, configPath = globalConfigPath()) {
2384
+ let content = "";
2385
+ try {
2386
+ content = fs9.readFileSync(configPath, "utf8");
2387
+ } catch {
2388
+ content = "";
2389
+ }
2390
+ fs9.mkdirSync(path12.dirname(configPath), { recursive: true });
2391
+ fs9.writeFileSync(configPath, setDefaultModelInToml(content, modelName), "utf8");
2392
+ }
2393
+ function openConfigFile(filePath) {
2394
+ const [command, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", filePath]] : process.platform === "darwin" ? ["open", [filePath]] : ["xdg-open", [filePath]];
2395
+ try {
2396
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
2397
+ child.on("error", () => {
2398
+ });
2399
+ child.unref();
2400
+ } catch {
2401
+ }
2402
+ }
2403
+
1908
2404
  // src/cli/commands/custom.ts
1909
2405
  import { readFileSync as readFileSync2, readdirSync } from "fs";
1910
- import path11 from "path";
2406
+ import path13 from "path";
1911
2407
  var NAME_PATTERN = /^[a-z0-9-]+$/;
1912
2408
  var DESCRIPTION_PATTERN = /^<!--\s*description:\s*(.+?)\s*-->\s*$/;
1913
2409
  var DEFAULT_DESCRIPTION = "Custom prompt command";
@@ -1942,14 +2438,14 @@ function scanDir(dir, into) {
1942
2438
  if (!entry.endsWith(".md")) continue;
1943
2439
  const name = entry.slice(0, -".md".length);
1944
2440
  if (!NAME_PATTERN.test(name)) continue;
1945
- const parsed = parseCommandFile(path11.join(dir, entry), name);
2441
+ const parsed = parseCommandFile(path13.join(dir, entry), name);
1946
2442
  if (parsed) into.set(name, parsed);
1947
2443
  }
1948
2444
  }
1949
2445
  function loadCustomCommands(cwd, home = starHome()) {
1950
2446
  const byName = /* @__PURE__ */ new Map();
1951
- scanDir(path11.join(home, "commands"), byName);
1952
- scanDir(path11.join(cwd, ".star", "commands"), byName);
2447
+ scanDir(path13.join(home, "commands"), byName);
2448
+ scanDir(path13.join(cwd, ".star", "commands"), byName);
1953
2449
  return [...byName.values()];
1954
2450
  }
1955
2451
  function registerCustomCommands(registry, cwd, home) {
@@ -1975,8 +2471,8 @@ function registerCustomCommands(registry, cwd, home) {
1975
2471
  }
1976
2472
 
1977
2473
  // src/cli/commands/doctor.ts
1978
- import fs8 from "fs/promises";
1979
- import path12 from "path";
2474
+ import fs10 from "fs/promises";
2475
+ import path14 from "path";
1980
2476
  var MIN_NODE_MAJOR = 20;
1981
2477
  var REGISTRY_URL = "https://registry.npmjs.org/";
1982
2478
  var REGISTRY_TIMEOUT_MS = 5e3;
@@ -1990,7 +2486,7 @@ function nodeMajor(version) {
1990
2486
  }
1991
2487
  async function pathExists(target) {
1992
2488
  try {
1993
- await fs8.access(target);
2489
+ await fs10.access(target);
1994
2490
  return true;
1995
2491
  } catch {
1996
2492
  return false;
@@ -2028,7 +2524,7 @@ async function runDoctor(opts) {
2028
2524
  detail: `${spec.label} (${spec.shell})`
2029
2525
  });
2030
2526
  }
2031
- const configPath = path12.join(home, "config.toml");
2527
+ const configPath = path14.join(home, "config.toml");
2032
2528
  const configExists = await pathExists(configPath);
2033
2529
  checks.push({
2034
2530
  name: "STAR_HOME / config",
@@ -2065,14 +2561,14 @@ async function runDoctor(opts) {
2065
2561
  });
2066
2562
  }
2067
2563
  }
2068
- const sessions = path12.join(home, "sessions");
2564
+ const sessions = path14.join(home, "sessions");
2069
2565
  let writable = false;
2070
2566
  let writeError = "";
2071
2567
  try {
2072
- await fs8.mkdir(sessions, { recursive: true });
2073
- const probe = path12.join(sessions, `.doctor-probe-${process.pid}`);
2074
- await fs8.writeFile(probe, "ok");
2075
- await fs8.unlink(probe);
2568
+ await fs10.mkdir(sessions, { recursive: true });
2569
+ const probe = path14.join(sessions, `.doctor-probe-${process.pid}`);
2570
+ await fs10.writeFile(probe, "ok");
2571
+ await fs10.unlink(probe);
2076
2572
  writable = true;
2077
2573
  } catch (error) {
2078
2574
  writeError = error instanceof Error ? error.message : String(error);
@@ -2107,15 +2603,15 @@ function formatDoctorReport(report) {
2107
2603
  }
2108
2604
 
2109
2605
  // src/cli/commands/init-project.ts
2110
- import fs9 from "fs/promises";
2111
- import path13 from "path";
2606
+ import fs11 from "fs/promises";
2607
+ import path15 from "path";
2112
2608
  import { generateText } from "ai";
2113
2609
  var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", "coverage"]);
2114
2610
  var MAX_ENTRIES = 20;
2115
2611
  var README_HEAD_LINES = 5;
2116
2612
  async function fileExists2(target) {
2117
2613
  try {
2118
- await fs9.access(target);
2614
+ await fs11.access(target);
2119
2615
  return true;
2120
2616
  } catch {
2121
2617
  return false;
@@ -2123,7 +2619,7 @@ async function fileExists2(target) {
2123
2619
  }
2124
2620
  async function readPackageJson(cwd) {
2125
2621
  try {
2126
- const raw = await fs9.readFile(path13.join(cwd, "package.json"), "utf8");
2622
+ const raw = await fs11.readFile(path15.join(cwd, "package.json"), "utf8");
2127
2623
  const parsed = JSON.parse(raw);
2128
2624
  if (typeof parsed !== "object" || parsed === null) return null;
2129
2625
  return parsed;
@@ -2140,10 +2636,10 @@ function stringRecord(value) {
2140
2636
  return out;
2141
2637
  }
2142
2638
  async function detectPackageManager(cwd) {
2143
- if (await fileExists2(path13.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2144
- if (await fileExists2(path13.join(cwd, "yarn.lock"))) return "yarn";
2145
- if (await fileExists2(path13.join(cwd, "package-lock.json"))) return "npm";
2146
- if (await fileExists2(path13.join(cwd, "bun.lockb"))) return "bun";
2639
+ if (await fileExists2(path15.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2640
+ if (await fileExists2(path15.join(cwd, "yarn.lock"))) return "yarn";
2641
+ if (await fileExists2(path15.join(cwd, "package-lock.json"))) return "npm";
2642
+ if (await fileExists2(path15.join(cwd, "bun.lockb"))) return "bun";
2147
2643
  return null;
2148
2644
  }
2149
2645
  function inferCommands(pm, scripts) {
@@ -2158,7 +2654,7 @@ function inferCommands(pm, scripts) {
2158
2654
  }
2159
2655
  async function listEntries(cwd) {
2160
2656
  try {
2161
- const dirents = await fs9.readdir(cwd, { withFileTypes: true });
2657
+ const dirents = await fs11.readdir(cwd, { withFileTypes: true });
2162
2658
  const entries = [];
2163
2659
  for (const dirent of dirents) {
2164
2660
  if (dirent.isDirectory()) {
@@ -2177,7 +2673,7 @@ async function listEntries(cwd) {
2177
2673
  async function readReadmeHead(cwd) {
2178
2674
  for (const name of ["README.md", "readme.md", "README"]) {
2179
2675
  try {
2180
- const raw = await fs9.readFile(path13.join(cwd, name), "utf8");
2676
+ const raw = await fs11.readFile(path15.join(cwd, name), "utf8");
2181
2677
  return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).slice(0, README_HEAD_LINES);
2182
2678
  } catch {
2183
2679
  }
@@ -2190,14 +2686,14 @@ async function scanProject(cwd) {
2190
2686
  const scripts = stringRecord(pkg?.scripts);
2191
2687
  return {
2192
2688
  name: typeof pkg?.name === "string" ? pkg.name : null,
2193
- dirName: path13.basename(cwd),
2689
+ dirName: path15.basename(cwd),
2194
2690
  packageManager,
2195
2691
  scripts,
2196
2692
  dependencies: Object.keys(stringRecord(pkg?.dependencies)),
2197
2693
  devDependencies: Object.keys(stringRecord(pkg?.devDependencies)),
2198
2694
  commands: inferCommands(packageManager, scripts),
2199
- hasTsconfig: await fileExists2(path13.join(cwd, "tsconfig.json")),
2200
- isGitRepo: await fileExists2(path13.join(cwd, ".git")),
2695
+ hasTsconfig: await fileExists2(path15.join(cwd, "tsconfig.json")),
2696
+ isGitRepo: await fileExists2(path15.join(cwd, ".git")),
2201
2697
  readmeHead: await readReadmeHead(cwd),
2202
2698
  entries: await listEntries(cwd)
2203
2699
  };
@@ -2264,7 +2760,7 @@ Write the AGENTS.md.`
2264
2760
  }
2265
2761
  async function initProject(opts) {
2266
2762
  const { cwd, args, model } = opts;
2267
- const target = path13.join(cwd, "AGENTS.md");
2763
+ const target = path15.join(cwd, "AGENTS.md");
2268
2764
  const force = args.split(/\s+/).includes("force");
2269
2765
  if (!force && await fileExists2(target)) {
2270
2766
  return {
@@ -2298,7 +2794,7 @@ async function initProject(opts) {
2298
2794
  content = renderAgentsMd(facts);
2299
2795
  note = " No model available; used the template \u2014 review and refine manually.";
2300
2796
  }
2301
- await fs9.writeFile(target, content);
2797
+ await fs11.writeFile(target, content);
2302
2798
  const lines = content.split("\n").length;
2303
2799
  const how = usedLlm ? "generated with model refinement" : "generated from template";
2304
2800
  return {
@@ -2340,7 +2836,7 @@ function parseSlashCommand(input) {
2340
2836
  }
2341
2837
 
2342
2838
  // src/cli/commands/rewind.ts
2343
- import path14 from "path";
2839
+ import path16 from "path";
2344
2840
  function planRewind(snapshots, id) {
2345
2841
  const target = snapshots.find((snapshot) => snapshot.id === id);
2346
2842
  if (!target) return null;
@@ -2354,7 +2850,7 @@ function planRewind(snapshots, id) {
2354
2850
  return { target, affected, messageIndex };
2355
2851
  }
2356
2852
  function displayPath(filePath, cwd) {
2357
- const relative = path14.relative(cwd, filePath);
2853
+ const relative = path16.relative(cwd, filePath);
2358
2854
  return relative !== "" && !relative.startsWith("..") ? relative : filePath;
2359
2855
  }
2360
2856
  function formatTime(timestamp) {
@@ -2404,29 +2900,110 @@ function didYouMeanSuffix(names) {
2404
2900
  return ` Did you mean: ${listed.join(", ")}?`;
2405
2901
  }
2406
2902
 
2407
- // src/cli/components/ExitPlanPrompt.tsx
2903
+ // src/cli/commands/undo.ts
2904
+ import { readFile as readFile5 } from "fs/promises";
2905
+ import path17 from "path";
2906
+ function displayPath2(filePath, cwd) {
2907
+ const relative = path17.relative(cwd, filePath);
2908
+ return relative !== "" && !relative.startsWith("..") ? relative : filePath;
2909
+ }
2910
+ async function buildUndoDiffs(snapshots, cwd) {
2911
+ const diffs = [];
2912
+ for (const snapshot of snapshots) {
2913
+ let current = "";
2914
+ try {
2915
+ current = await readFile5(snapshot.path, "utf8");
2916
+ } catch {
2917
+ }
2918
+ const before = snapshot.existed ? await resolveSnapshotContent(snapshot) : "";
2919
+ const lines = before === null ? [{ kind: "marker", text: "(snapshot content unavailable)" }] : buildDiffLines(current, before);
2920
+ diffs.push({ label: displayPath2(snapshot.path, cwd), lines });
2921
+ }
2922
+ return diffs;
2923
+ }
2924
+
2925
+ // src/cli/components/ConnectWizard.tsx
2926
+ import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
2927
+ import { useRef as useRef4, useState as useState4 } from "react";
2928
+
2929
+ // src/cli/components/SelectPrompt.tsx
2408
2930
  import { Box, Text, useInput } from "ink";
2931
+ import { useRef, useState } from "react";
2409
2932
  import { jsx, jsxs } from "react/jsx-runtime";
2410
- function ExitPlanPrompt({ onDecision }) {
2933
+ var SELECT_PROMPT_MAX_VISIBLE = 9;
2934
+ function SelectPrompt({ title, options, onSelect, onCancel }) {
2935
+ const [index, setIndex] = useState(0);
2936
+ const indexRef = useRef(0);
2937
+ const move = (delta) => {
2938
+ indexRef.current = Math.max(0, Math.min(indexRef.current + delta, options.length - 1));
2939
+ setIndex(indexRef.current);
2940
+ };
2411
2941
  useInput((input, key) => {
2412
- const ch = input.toLowerCase();
2413
- if (ch === "y") onDecision("yes");
2414
- else if (ch === "n" || key.escape) onDecision("no");
2942
+ if (key.escape) {
2943
+ onCancel();
2944
+ return;
2945
+ }
2946
+ if (key.return) {
2947
+ const option = options[indexRef.current];
2948
+ if (option) onSelect(option.value);
2949
+ return;
2950
+ }
2951
+ if (key.downArrow || input === "j") {
2952
+ move(1);
2953
+ } else if (key.upArrow || input === "k") {
2954
+ move(-1);
2955
+ }
2415
2956
  });
2957
+ const maxStart = Math.max(0, options.length - SELECT_PROMPT_MAX_VISIBLE);
2958
+ const start = Math.min(Math.max(0, index - Math.floor(SELECT_PROMPT_MAX_VISIBLE / 2)), maxStart);
2959
+ const visible = options.slice(start, start + SELECT_PROMPT_MAX_VISIBLE);
2960
+ const above = start;
2961
+ const below = options.length - start - visible.length;
2416
2962
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
2417
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Plan ready (plan mode)" }),
2418
- /* @__PURE__ */ jsx(Text, { children: "The plan above was produced without making any changes." }),
2419
- /* @__PURE__ */ jsx(Text, { children: "[y] approve and execute [n] keep refining (Esc = n)" })
2963
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: title }),
2964
+ above > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
2965
+ "\u2026 ",
2966
+ above,
2967
+ " more above"
2968
+ ] }),
2969
+ visible.map((option, i) => {
2970
+ const active = start + i === index;
2971
+ return /* @__PURE__ */ jsxs(Text, { children: [
2972
+ /* @__PURE__ */ jsxs(Text, { bold: active, inverse: active, children: [
2973
+ active ? "\u276F " : " ",
2974
+ option.label
2975
+ ] }),
2976
+ option.hint && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
2977
+ " (",
2978
+ option.hint,
2979
+ ")"
2980
+ ] }),
2981
+ option.description && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
2982
+ " \u2014 ",
2983
+ option.description
2984
+ ] })
2985
+ ] }, option.value);
2986
+ }),
2987
+ below > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
2988
+ "\u2026 ",
2989
+ below,
2990
+ " more below"
2991
+ ] }),
2992
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[\u2191/\u2193 or j/k] move [Enter] select [Esc] cancel" })
2420
2993
  ] });
2421
2994
  }
2422
2995
 
2996
+ // src/cli/components/TextField.tsx
2997
+ import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
2998
+ import { useRef as useRef3, useState as useState3 } from "react";
2999
+
2423
3000
  // src/cli/components/InputBox.tsx
2424
- import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
2425
- import { useEffect, useRef, useState } from "react";
3001
+ import { Box as Box2, Text as Text2, useInput as useInput2, useStdout } from "ink";
3002
+ import { useEffect, useRef as useRef2, useState as useState2 } from "react";
2426
3003
 
2427
3004
  // src/cli/path-suggest.ts
2428
- import { readdir } from "fs/promises";
2429
- import path15 from "path";
3005
+ import { readdir as readdir2 } from "fs/promises";
3006
+ import path18 from "path";
2430
3007
  var MAX_SCAN_ENTRIES = 2e3;
2431
3008
  function extractAtToken(value, cursor) {
2432
3009
  const before = value.slice(0, cursor);
@@ -2441,8 +3018,8 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
2441
3018
  const dirPart = slash >= 0 ? normalized.slice(0, slash + 1) : "";
2442
3019
  const basePrefix = slash >= 0 ? normalized.slice(slash + 1) : normalized;
2443
3020
  if (dirPart.split("/").some((segment) => SKIP_DIRS.has(segment))) return [];
2444
- const dirAbs = path15.resolve(cwd, dirPart === "" ? "." : dirPart);
2445
- const entries = await readdir(dirAbs, { withFileTypes: true }).catch(() => null);
3021
+ const dirAbs = path18.resolve(cwd, dirPart === "" ? "." : dirPart);
3022
+ const entries = await readdir2(dirAbs, { withFileTypes: true }).catch(() => null);
2446
3023
  if (!entries) return [];
2447
3024
  const ignore = createIgnorePredicate(cwd);
2448
3025
  const lower = basePrefix.toLowerCase();
@@ -2456,7 +3033,7 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
2456
3033
  if (isDir && SKIP_DIRS.has(entry.name)) continue;
2457
3034
  if (entry.name.startsWith(".") && !basePrefix.startsWith(".")) continue;
2458
3035
  if (!entry.name.toLowerCase().startsWith(lower)) continue;
2459
- if (ignore?.(path15.join(dirAbs, entry.name), isDir)) continue;
3036
+ if (ignore?.(path18.join(dirAbs, entry.name), isDir)) continue;
2460
3037
  matches.push({ path: `${dirPart}${entry.name}${isDir ? "/" : ""}`, isDir });
2461
3038
  }
2462
3039
  matches.sort((a, b) => a.path.localeCompare(b.path));
@@ -2465,40 +3042,227 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
2465
3042
 
2466
3043
  // src/cli/components/InputBox.tsx
2467
3044
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2468
- function InputBox({
2469
- isStreaming,
2470
- disabled,
2471
- commands,
2472
- cwd,
2473
- refill,
2474
- initialHistory,
2475
- onSubmit,
2476
- onInterrupt,
2477
- onExit,
2478
- onPasteImage
2479
- }) {
2480
- const [value, setValue] = useState("");
2481
- const [cursor, setCursor] = useState(0);
2482
- const [history, setHistory] = useState(() => initialHistory ?? []);
2483
- const [highlight, setHighlight] = useState(0);
2484
- const [suggestionsDismissed, setSuggestionsDismissed] = useState(false);
2485
- const [pathSuggestions, setPathSuggestions] = useState([]);
2486
- const historyIndexRef = useRef(null);
2487
- const draftRef = useRef("");
2488
- const refillSeqRef = useRef(0);
2489
- const edit = (next, nextCursor) => {
2490
- setValue(next);
2491
- setCursor(Math.max(0, Math.min(nextCursor, next.length)));
2492
- setHighlight(0);
3045
+ function moveCursorLines(value, cursor, delta) {
3046
+ const lineStart = value.lastIndexOf("\n", cursor - 1) + 1;
3047
+ const col = cursor - lineStart;
3048
+ if (delta === -1) {
3049
+ if (lineStart === 0) return null;
3050
+ const prevEnd = lineStart - 1;
3051
+ const prevStart = value.lastIndexOf("\n", prevEnd - 1) + 1;
3052
+ return prevStart + Math.min(col, prevEnd - prevStart);
3053
+ }
3054
+ const lineEndIndex = value.indexOf("\n", cursor);
3055
+ if (lineEndIndex === -1) return null;
3056
+ const nextStart = lineEndIndex + 1;
3057
+ const nextEndIndex = value.indexOf("\n", nextStart);
3058
+ const nextEnd = nextEndIndex === -1 ? value.length : nextEndIndex;
3059
+ return nextStart + Math.min(col, nextEnd - nextStart);
3060
+ }
3061
+ function findHistoryMatch(history, query, from, step) {
3062
+ const needle = query.toLowerCase();
3063
+ for (let i = from; i >= 0 && i < history.length; i += step) {
3064
+ if ((history[i] ?? "").toLowerCase().includes(needle)) return i;
3065
+ }
3066
+ return null;
3067
+ }
3068
+ var PASTE_COLLAPSE_MIN_LINES = 10;
3069
+ var PASTE_COLLAPSE_MIN_CHARS = 500;
3070
+ var PASTE_MERGE_WINDOW_MS = 100;
3071
+ var PASTE_MERGE_MIN_CHARS = 32;
3072
+ var PASTE_DELIM = "\u200B";
3073
+ var PASTE_TOKEN_RE = /\u200B\[pasted #(\d+): \d+ lines?\]\u200B/g;
3074
+ var BRACKETED_PASTE_START = "\x1B[200~";
3075
+ var BRACKETED_PASTE_END = "\x1B[201~";
3076
+ var PARTIAL_BRACKET_RE = /\u001B(?:\[(?:2(?:0(?:[01])?)?)?)?$/;
3077
+ function feedBracketedPaste(state, raw) {
3078
+ let stream = state.pending + raw;
3079
+ state.pending = "";
3080
+ if (stream.startsWith("[200~") || stream.startsWith("[201~")) {
3081
+ stream = `\x1B${stream}`;
3082
+ }
3083
+ const segments = [];
3084
+ let buffer = state.buffer;
3085
+ let bracketed = state.active;
3086
+ let i = 0;
3087
+ const pushText = (text) => {
3088
+ const cleaned = text.replaceAll("\x1B", "");
3089
+ if (cleaned.length > 0) segments.push({ type: "text", text: cleaned });
3090
+ };
3091
+ const holdPartial = (tail) => {
3092
+ const partial = PARTIAL_BRACKET_RE.exec(tail);
3093
+ if (!partial) return tail;
3094
+ state.pending = partial[0];
3095
+ return tail.slice(0, tail.length - partial[0].length);
3096
+ };
3097
+ while (i < stream.length) {
3098
+ if (bracketed) {
3099
+ const idx = stream.indexOf(BRACKETED_PASTE_END, i);
3100
+ if (idx === -1) {
3101
+ buffer += holdPartial(stream.slice(i));
3102
+ i = stream.length;
3103
+ } else {
3104
+ buffer += stream.slice(i, idx);
3105
+ segments.push({ type: "paste", text: buffer });
3106
+ buffer = "";
3107
+ bracketed = false;
3108
+ i = idx + BRACKETED_PASTE_END.length;
3109
+ }
3110
+ } else {
3111
+ const idx = stream.indexOf(BRACKETED_PASTE_START, i);
3112
+ if (idx === -1) {
3113
+ pushText(holdPartial(stream.slice(i)));
3114
+ i = stream.length;
3115
+ } else {
3116
+ pushText(stream.slice(i, idx));
3117
+ buffer = "";
3118
+ bracketed = true;
3119
+ i = idx + BRACKETED_PASTE_START.length;
3120
+ }
3121
+ }
3122
+ }
3123
+ state.active = bracketed;
3124
+ state.buffer = buffer;
3125
+ return segments.length === 0 ? null : segments;
3126
+ }
3127
+ function pasteToken(id, lineCount) {
3128
+ const label = `[pasted #${id}: ${lineCount} ${lineCount === 1 ? "line" : "lines"}]`;
3129
+ return `${PASTE_DELIM}${label}${PASTE_DELIM}`;
3130
+ }
3131
+ function stylePasteTokens(segment) {
3132
+ return segment.replace(PASTE_TOKEN_RE, (match) => `\x1B[2m${match}\x1B[22m`);
3133
+ }
3134
+ function InputBox({
3135
+ isStreaming,
3136
+ disabled,
3137
+ commands,
3138
+ cwd,
3139
+ refill,
3140
+ initialHistory,
3141
+ onSubmit,
3142
+ onInterrupt,
3143
+ onExit,
3144
+ onPasteImage
3145
+ }) {
3146
+ const [value, setValue] = useState2("");
3147
+ const [cursor, setCursor] = useState2(0);
3148
+ const [history, setHistory] = useState2(() => initialHistory ?? []);
3149
+ const [highlight, setHighlight] = useState2(0);
3150
+ const [suggestionsDismissed, setSuggestionsDismissed] = useState2(false);
3151
+ const [pathSuggestions, setPathSuggestions] = useState2([]);
3152
+ const historyIndexRef = useRef2(null);
3153
+ const draftRef = useRef2("");
3154
+ const refillSeqRef = useRef2(0);
3155
+ const [search, setSearch] = useState2(null);
3156
+ const searchSavedRef = useRef2(null);
3157
+ const pastesRef = useRef2(/* @__PURE__ */ new Map());
3158
+ const pasteSeqRef = useRef2(0);
3159
+ const bracketRef = useRef2({ active: false, buffer: "", pending: "" });
3160
+ const lastPasteRef = useRef2(
3161
+ null
3162
+ );
3163
+ const valueRef = useRef2(value);
3164
+ const cursorRef = useRef2(cursor);
3165
+ useEffect(() => {
3166
+ valueRef.current = value;
3167
+ cursorRef.current = cursor;
3168
+ });
3169
+ const { stdout } = useStdout();
3170
+ useEffect(() => {
3171
+ if (!stdout?.isTTY) return;
3172
+ stdout.write("\x1B[?2004h");
3173
+ return () => {
3174
+ stdout.write("\x1B[?2004l");
3175
+ };
3176
+ }, [stdout]);
3177
+ const expandPastes = (raw) => raw.replace(PASTE_TOKEN_RE, (match, id) => pastesRef.current.get(Number(id)) ?? match);
3178
+ const clearPastes = () => {
3179
+ pastesRef.current.clear();
3180
+ pasteSeqRef.current = 0;
3181
+ lastPasteRef.current = null;
3182
+ const bracket = bracketRef.current;
3183
+ bracket.active = false;
3184
+ bracket.buffer = "";
3185
+ bracket.pending = "";
3186
+ };
3187
+ const edit = (next, nextCursor) => {
3188
+ const idx = historyIndexRef.current;
3189
+ if (idx !== null && next !== history[idx]) historyIndexRef.current = null;
3190
+ const clamped = Math.max(0, Math.min(nextCursor, next.length));
3191
+ valueRef.current = next;
3192
+ cursorRef.current = clamped;
3193
+ setValue(next);
3194
+ setCursor(clamped);
3195
+ setHighlight(0);
2493
3196
  setSuggestionsDismissed(false);
2494
3197
  };
3198
+ const normalizeNewlines = (text) => text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
3199
+ const collapseOrInsert = (text) => {
3200
+ const currentValue = valueRef.current;
3201
+ const currentCursor = cursorRef.current;
3202
+ const lineCount = text.split("\n").length;
3203
+ if (lineCount >= PASTE_COLLAPSE_MIN_LINES || text.length >= PASTE_COLLAPSE_MIN_CHARS) {
3204
+ const id = ++pasteSeqRef.current;
3205
+ pastesRef.current.set(id, text);
3206
+ const token = pasteToken(id, lineCount);
3207
+ const nextCursor = currentCursor + token.length;
3208
+ edit(
3209
+ currentValue.slice(0, currentCursor) + token + currentValue.slice(currentCursor),
3210
+ nextCursor
3211
+ );
3212
+ lastPasteRef.current = { id, token, cursor: nextCursor, time: Date.now() };
3213
+ return;
3214
+ }
3215
+ edit(
3216
+ currentValue.slice(0, currentCursor) + text + currentValue.slice(currentCursor),
3217
+ currentCursor + text.length
3218
+ );
3219
+ };
3220
+ const insertPlainText = (rawText) => {
3221
+ const text = normalizeNewlines(rawText);
3222
+ const currentValue = valueRef.current;
3223
+ const currentCursor = cursorRef.current;
3224
+ const last = lastPasteRef.current;
3225
+ if (last && Date.now() - last.time <= PASTE_MERGE_WINDOW_MS && currentCursor === last.cursor && currentValue.endsWith(last.token, currentCursor) && (text.includes("\n") || text.length >= PASTE_MERGE_MIN_CHARS)) {
3226
+ const merged = (pastesRef.current.get(last.id) ?? "") + text;
3227
+ pastesRef.current.set(last.id, merged);
3228
+ const token = pasteToken(last.id, merged.split("\n").length);
3229
+ const start = currentCursor - last.token.length;
3230
+ const nextCursor = start + token.length;
3231
+ edit(currentValue.slice(0, start) + token + currentValue.slice(currentCursor), nextCursor);
3232
+ lastPasteRef.current = { id: last.id, token, cursor: nextCursor, time: Date.now() };
3233
+ return;
3234
+ }
3235
+ collapseOrInsert(text);
3236
+ };
3237
+ const flushBracketPaste = () => {
3238
+ const bracket = bracketRef.current;
3239
+ const content = bracket.buffer;
3240
+ bracket.active = false;
3241
+ bracket.buffer = "";
3242
+ bracket.pending = "";
3243
+ if (content.length > 0) collapseOrInsert(normalizeNewlines(content));
3244
+ };
3245
+ const applyPasteSegments = (segments) => {
3246
+ if (search) {
3247
+ const query = search.query + normalizeNewlines(segments.map((seg) => seg.text).join(""));
3248
+ setSearch({ query, match: findHistoryMatch(history, query, history.length - 1, -1) });
3249
+ return;
3250
+ }
3251
+ for (const seg of segments) {
3252
+ if (seg.type === "paste") {
3253
+ collapseOrInsert(normalizeNewlines(seg.text));
3254
+ } else {
3255
+ insertPlainText(seg.text);
3256
+ }
3257
+ }
3258
+ };
2495
3259
  useEffect(() => {
2496
3260
  if (refill && refill.seq !== refillSeqRef.current) {
2497
3261
  refillSeqRef.current = refill.seq;
2498
3262
  edit(refill.text, refill.text.length);
2499
3263
  }
2500
3264
  });
2501
- const suggestionsEnabled = !isStreaming && !disabled && !suggestionsDismissed;
3265
+ const suggestionsEnabled = !isStreaming && !disabled && !suggestionsDismissed && !search;
2502
3266
  const suggestions = suggestionsEnabled && commands ? filterCommands(value, commands) : [];
2503
3267
  useEffect(() => {
2504
3268
  if (!suggestionsEnabled || !cwd) {
@@ -2539,11 +3303,87 @@ function InputBox({
2539
3303
  );
2540
3304
  };
2541
3305
  useInput2((input, key) => {
3306
+ if (!disabled) {
3307
+ const bracket = bracketRef.current;
3308
+ const feedable = input.length > 0 && !key.ctrl && !key.meta;
3309
+ if (bracket.active && (!feedable || input === "\r")) {
3310
+ flushBracketPaste();
3311
+ if (feedable) return;
3312
+ }
3313
+ if (feedable) {
3314
+ const segments = feedBracketedPaste(bracket, input);
3315
+ if (segments === null) return;
3316
+ const only = segments.length === 1 ? segments[0] : void 0;
3317
+ if (!only || only.type !== "text" || only.text !== input) {
3318
+ applyPasteSegments(segments);
3319
+ return;
3320
+ }
3321
+ } else {
3322
+ bracket.pending = "";
3323
+ }
3324
+ }
3325
+ if (key.ctrl && input === "r") {
3326
+ if (isStreaming || disabled) return;
3327
+ if (search) {
3328
+ const from = (search.match ?? history.length) - 1;
3329
+ const older = findHistoryMatch(history, search.query, from, -1);
3330
+ if (older !== null) setSearch({ ...search, match: older });
3331
+ } else {
3332
+ searchSavedRef.current = { value, cursor };
3333
+ setSearch({ query: "", match: history.length > 0 ? history.length - 1 : null });
3334
+ }
3335
+ return;
3336
+ }
3337
+ if (search) {
3338
+ const exitSearch = (accept) => {
3339
+ const saved = searchSavedRef.current;
3340
+ setSearch(null);
3341
+ searchSavedRef.current = null;
3342
+ const match = accept && search.match !== null ? history[search.match] : void 0;
3343
+ if (match !== void 0) {
3344
+ edit(match, match.length);
3345
+ } else if (saved) {
3346
+ edit(saved.value, saved.cursor);
3347
+ }
3348
+ };
3349
+ if (key.escape || key.ctrl && (input === "c" || input === "g")) {
3350
+ exitSearch(false);
3351
+ return;
3352
+ }
3353
+ if (key.return) {
3354
+ exitSearch(true);
3355
+ return;
3356
+ }
3357
+ const requery = (query) => setSearch({ query, match: findHistoryMatch(history, query, history.length - 1, -1) });
3358
+ if (key.backspace || key.delete) {
3359
+ requery(search.query.slice(0, -1));
3360
+ return;
3361
+ }
3362
+ if (key.upArrow) {
3363
+ const from = (search.match ?? history.length) - 1;
3364
+ const older = findHistoryMatch(history, search.query, from, -1);
3365
+ if (older !== null) setSearch({ ...search, match: older });
3366
+ return;
3367
+ }
3368
+ if (key.downArrow) {
3369
+ if (search.match !== null) {
3370
+ const newer = findHistoryMatch(history, search.query, search.match + 1, 1);
3371
+ if (newer !== null) setSearch({ ...search, match: newer });
3372
+ }
3373
+ return;
3374
+ }
3375
+ if (input && !key.ctrl && !key.meta) {
3376
+ const text = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
3377
+ requery(search.query + text);
3378
+ }
3379
+ return;
3380
+ }
2542
3381
  if (key.ctrl && input === "c") {
2543
3382
  if (isStreaming || disabled) {
2544
3383
  onInterrupt();
2545
3384
  } else {
2546
3385
  edit("", 0);
3386
+ clearPastes();
2547
3387
  historyIndexRef.current = null;
2548
3388
  }
2549
3389
  return;
@@ -2562,16 +3402,32 @@ function InputBox({
2562
3402
  return;
2563
3403
  }
2564
3404
  if (key.return) {
2565
- const text = value.trim();
3405
+ const currentValue = valueRef.current;
3406
+ const currentCursor = cursorRef.current;
3407
+ if (currentValue[currentCursor - 1] === "\\") {
3408
+ edit(
3409
+ `${currentValue.slice(0, currentCursor - 1)}
3410
+ ${currentValue.slice(currentCursor)}`,
3411
+ currentCursor
3412
+ );
3413
+ return;
3414
+ }
3415
+ const text = expandPastes(currentValue).trim();
2566
3416
  if (text.length > 0) {
2567
- setHistory((prev) => [...prev, text]);
3417
+ if (!text.startsWith("/")) setHistory((prev) => [...prev, text]);
2568
3418
  onSubmit(text);
2569
3419
  }
2570
3420
  edit("", 0);
3421
+ clearPastes();
2571
3422
  historyIndexRef.current = null;
2572
3423
  draftRef.current = "";
2573
3424
  return;
2574
3425
  }
3426
+ if (input === "\n" || input === "\r") {
3427
+ edit(`${value.slice(0, cursor)}
3428
+ ${value.slice(cursor)}`, cursor + 1);
3429
+ return;
3430
+ }
2575
3431
  if (key.tab) {
2576
3432
  if (key.shift) return;
2577
3433
  if (suggestionCount > 0) {
@@ -2579,34 +3435,47 @@ function InputBox({
2579
3435
  return;
2580
3436
  }
2581
3437
  } else if (key.upArrow) {
3438
+ if (historyIndexRef.current !== null) {
3439
+ if (historyIndexRef.current > 0) {
3440
+ historyIndexRef.current -= 1;
3441
+ const entry2 = history[historyIndexRef.current] ?? "";
3442
+ edit(entry2, entry2.length);
3443
+ }
3444
+ return;
3445
+ }
2582
3446
  if (suggestionCount > 0) {
2583
3447
  setHighlight((prev) => (prev - 1 + suggestionCount) % suggestionCount);
2584
3448
  return;
2585
3449
  }
2586
- if (history.length === 0) return;
2587
- if (historyIndexRef.current === null) {
2588
- draftRef.current = value;
2589
- historyIndexRef.current = history.length - 1;
2590
- } else if (historyIndexRef.current > 0) {
2591
- historyIndexRef.current -= 1;
3450
+ const moved = moveCursorLines(value, cursor, -1);
3451
+ if (moved !== null) {
3452
+ setCursor(moved);
3453
+ return;
2592
3454
  }
3455
+ if (history.length === 0) return;
3456
+ draftRef.current = value;
3457
+ historyIndexRef.current = history.length - 1;
2593
3458
  const entry = history[historyIndexRef.current] ?? "";
2594
3459
  edit(entry, entry.length);
2595
3460
  return;
2596
3461
  } else if (key.downArrow) {
3462
+ if (historyIndexRef.current !== null) {
3463
+ if (historyIndexRef.current < history.length - 1) {
3464
+ historyIndexRef.current += 1;
3465
+ const entry = history[historyIndexRef.current] ?? "";
3466
+ edit(entry, entry.length);
3467
+ } else {
3468
+ historyIndexRef.current = null;
3469
+ edit(draftRef.current, draftRef.current.length);
3470
+ }
3471
+ return;
3472
+ }
2597
3473
  if (suggestionCount > 0) {
2598
3474
  setHighlight((prev) => (prev + 1) % suggestionCount);
2599
3475
  return;
2600
3476
  }
2601
- if (historyIndexRef.current === null) return;
2602
- if (historyIndexRef.current < history.length - 1) {
2603
- historyIndexRef.current += 1;
2604
- const entry = history[historyIndexRef.current] ?? "";
2605
- edit(entry, entry.length);
2606
- } else {
2607
- historyIndexRef.current = null;
2608
- edit(draftRef.current, draftRef.current.length);
2609
- }
3477
+ const moved = moveCursorLines(value, cursor, 1);
3478
+ if (moved !== null) setCursor(moved);
2610
3479
  return;
2611
3480
  } else if (key.leftArrow) {
2612
3481
  setCursor((prev) => Math.max(0, prev - 1));
@@ -2644,23 +3513,31 @@ function InputBox({
2644
3513
  }
2645
3514
  if (key.backspace || key.delete) {
2646
3515
  if (cursor === 0) return;
2647
- edit(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1);
3516
+ let remove = 1;
3517
+ for (const match of value.matchAll(PASTE_TOKEN_RE)) {
3518
+ if (match.index + match[0].length === cursor) {
3519
+ remove = match[0].length;
3520
+ const id = Number(match[1]);
3521
+ pastesRef.current.delete(id);
3522
+ if (lastPasteRef.current?.id === id) lastPasteRef.current = null;
3523
+ break;
3524
+ }
3525
+ }
3526
+ edit(value.slice(0, cursor - remove) + value.slice(cursor), cursor - remove);
2648
3527
  return;
2649
3528
  }
2650
3529
  if (input && !key.ctrl && !key.meta) {
2651
- edit(value.slice(0, cursor) + input + value.slice(cursor), cursor + input.length);
3530
+ insertPlainText(input);
2652
3531
  }
2653
3532
  });
2654
3533
  const before = value.slice(0, cursor);
2655
- const at = value[cursor] ?? " ";
3534
+ const at = value[cursor];
2656
3535
  const after = value.slice(cursor + 1);
3536
+ const cursorChar = at === "\n" || at === void 0 || at === PASTE_DELIM ? " " : at;
3537
+ const tail = (at === "\n" ? "\n" : "") + stylePasteTokens(after);
3538
+ 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> \x1B[39m${stylePasteTokens(before)}\x1B[7m${cursorChar}\x1B[27m${tail}`;
2657
3539
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
2658
- /* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
2659
- /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "> " }),
2660
- /* @__PURE__ */ jsx2(Text2, { children: before }),
2661
- /* @__PURE__ */ jsx2(Text2, { inverse: true, children: at }),
2662
- /* @__PURE__ */ jsx2(Text2, { children: after })
2663
- ] }),
3540
+ /* @__PURE__ */ jsx2(Box2, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { children: styled }) }),
2664
3541
  suggestions.length > 0 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: suggestions.map((cmd, index) => {
2665
3542
  const label = cmd.usage ?? `/${cmd.name}`;
2666
3543
  return index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `${label} - ${cmd.description}` }, cmd.name) : /* @__PURE__ */ jsxs2(Text2, { children: [
@@ -2677,170 +3554,422 @@ function InputBox({
2677
3554
  ] });
2678
3555
  }
2679
3556
 
3557
+ // src/cli/components/TextField.tsx
3558
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
3559
+ var MASK_CAP = 24;
3560
+ function TextField({
3561
+ label,
3562
+ initialValue = "",
3563
+ masked = false,
3564
+ placeholder,
3565
+ onSubmit,
3566
+ onCancel
3567
+ }) {
3568
+ const [value, setValue] = useState3(initialValue);
3569
+ const valueRef = useRef3(initialValue);
3570
+ const bracketRef = useRef3({ active: false, buffer: "", pending: "" });
3571
+ const flushBracket = () => {
3572
+ const bracket = bracketRef.current;
3573
+ const content = bracket.buffer.replace(/[\r\n]+/g, "");
3574
+ bracket.active = false;
3575
+ bracket.buffer = "";
3576
+ bracket.pending = "";
3577
+ if (content.length > 0) {
3578
+ valueRef.current += content;
3579
+ setValue(valueRef.current);
3580
+ }
3581
+ };
3582
+ useInput3((input, key) => {
3583
+ if (key.escape) {
3584
+ onCancel();
3585
+ return;
3586
+ }
3587
+ if (key.return) {
3588
+ flushBracket();
3589
+ onSubmit(valueRef.current.trim());
3590
+ return;
3591
+ }
3592
+ if (key.backspace || key.delete) {
3593
+ flushBracket();
3594
+ valueRef.current = valueRef.current.slice(0, -1);
3595
+ setValue(valueRef.current);
3596
+ return;
3597
+ }
3598
+ if (key.ctrl || key.meta || !input) return;
3599
+ const segments = feedBracketedPaste(bracketRef.current, input);
3600
+ if (segments === null) return;
3601
+ const text = segments.map((segment) => segment.text).join("").replace(/[\r\n]+/g, "");
3602
+ if (text.length === 0) return;
3603
+ valueRef.current += text;
3604
+ setValue(valueRef.current);
3605
+ });
3606
+ const echo = masked ? "\u2022".repeat(Math.min(value.length, MASK_CAP)) : value;
3607
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
3608
+ /* @__PURE__ */ jsxs3(Text3, { children: [
3609
+ label,
3610
+ " ",
3611
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: echo }),
3612
+ value === "" && placeholder ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: placeholder }) : null
3613
+ ] }),
3614
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "[Enter] confirm [Esc] cancel" })
3615
+ ] });
3616
+ }
3617
+
3618
+ // src/cli/components/ConnectWizard.tsx
3619
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
3620
+ function YN({ question, onAnswer }) {
3621
+ useInput4((input, key) => {
3622
+ const ch = input.toLowerCase();
3623
+ if (ch === "y") onAnswer(true);
3624
+ else if (ch === "n" || key.escape) onAnswer(false);
3625
+ });
3626
+ return /* @__PURE__ */ jsxs4(Text4, { children: [
3627
+ question,
3628
+ " ",
3629
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "[y] yes [n] no" })
3630
+ ] });
3631
+ }
3632
+ function ConnectWizard({
3633
+ existingProviderNames,
3634
+ existingModelNames,
3635
+ onSave,
3636
+ onSetDefault,
3637
+ onOpenConfig,
3638
+ onFinish,
3639
+ onCancel
3640
+ }) {
3641
+ const [step, setStep] = useState4("provider");
3642
+ const [preset, setPreset] = useState4(null);
3643
+ const [protocol, setProtocol] = useState4("openai-compatible");
3644
+ const [baseURL, setBaseURL] = useState4("");
3645
+ const [apiKey, setApiKey] = useState4("");
3646
+ const [modelId, setModelId] = useState4("");
3647
+ const [answers, setAnswers] = useState4(null);
3648
+ const [error, setError] = useState4(null);
3649
+ const [busy, setBusy] = useState4(false);
3650
+ const [notes, setNotes] = useState4([]);
3651
+ const busyRef = useRef4(false);
3652
+ const notesRef = useRef4([]);
3653
+ const pushNote = (note) => {
3654
+ notesRef.current = [...notesRef.current, note];
3655
+ setNotes(notesRef.current);
3656
+ };
3657
+ const providerOptions = [
3658
+ ...PROVIDER_PRESETS.map((p) => ({ value: p.key, label: p.label, description: p.baseURL })),
3659
+ {
3660
+ value: "custom",
3661
+ label: "Custom",
3662
+ description: "any OpenAI/Anthropic-compatible endpoint"
3663
+ }
3664
+ ];
3665
+ const handleProviderSelect = (value) => {
3666
+ const found = PROVIDER_PRESETS.find((p) => p.key === value) ?? null;
3667
+ setPreset(found);
3668
+ if (found) {
3669
+ setProtocol(found.protocol);
3670
+ setBaseURL(found.baseURL);
3671
+ setStep("baseUrl");
3672
+ } else {
3673
+ setStep("protocol");
3674
+ }
3675
+ };
3676
+ const handleProtocolSelect = (value) => {
3677
+ setProtocol(value);
3678
+ setStep("baseUrl");
3679
+ };
3680
+ const submitBaseURL = (value) => {
3681
+ if (!value) {
3682
+ setError("baseURL is required");
3683
+ return;
3684
+ }
3685
+ setError(null);
3686
+ setBaseURL(value);
3687
+ setStep("apiKey");
3688
+ };
3689
+ const submitApiKey = (value) => {
3690
+ if (!value) {
3691
+ setError("API key is required");
3692
+ return;
3693
+ }
3694
+ setError(null);
3695
+ setApiKey(value);
3696
+ setStep("model");
3697
+ };
3698
+ const submitModel = (value) => {
3699
+ if (!value) {
3700
+ setError("model id is required");
3701
+ return;
3702
+ }
3703
+ setError(null);
3704
+ setModelId(value);
3705
+ const providerName = dedupeName(deriveProviderName(baseURL), existingProviderNames);
3706
+ setAnswers({
3707
+ providerName,
3708
+ protocol,
3709
+ baseURL,
3710
+ apiKeyEnv: apiKeyEnvFor(providerName),
3711
+ apiKey,
3712
+ modelName: dedupeName(value, existingModelNames),
3713
+ modelId: value
3714
+ });
3715
+ setStep("confirm");
3716
+ };
3717
+ const handleConfirm = async (yes) => {
3718
+ if (!yes) {
3719
+ onCancel();
3720
+ return;
3721
+ }
3722
+ if (busyRef.current || !answers) return;
3723
+ busyRef.current = true;
3724
+ setBusy(true);
3725
+ try {
3726
+ pushNote(await onSave(answers));
3727
+ } catch (saveError) {
3728
+ busyRef.current = false;
3729
+ setBusy(false);
3730
+ setError(
3731
+ `Save failed: ${saveError instanceof Error ? saveError.message : String(saveError)}`
3732
+ );
3733
+ return;
3734
+ }
3735
+ busyRef.current = false;
3736
+ setBusy(false);
3737
+ setStep("setDefault");
3738
+ };
3739
+ const handleSetDefault = async (yes) => {
3740
+ if (busyRef.current || !answers) return;
3741
+ busyRef.current = true;
3742
+ setBusy(true);
3743
+ try {
3744
+ pushNote(yes ? await onSetDefault(answers.modelName) : "Default model unchanged.");
3745
+ } catch (setDefaultError) {
3746
+ pushNote(
3747
+ `Could not set the default model: ${setDefaultError instanceof Error ? setDefaultError.message : String(setDefaultError)}`
3748
+ );
3749
+ }
3750
+ busyRef.current = false;
3751
+ setBusy(false);
3752
+ setStep("openFile");
3753
+ };
3754
+ const handleOpenFile = (yes) => {
3755
+ if (yes) pushNote(onOpenConfig());
3756
+ onFinish(notesRef.current.join("\n"));
3757
+ };
3758
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
3759
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "Connect a provider" }),
3760
+ notes.map((note, index) => (
3761
+ // biome-ignore lint/suspicious/noArrayIndexKey: notes are append-only
3762
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: note }, index)
3763
+ )),
3764
+ step === "provider" && /* @__PURE__ */ jsx4(
3765
+ SelectPrompt,
3766
+ {
3767
+ title: "Which provider?",
3768
+ options: providerOptions,
3769
+ onSelect: handleProviderSelect,
3770
+ onCancel
3771
+ }
3772
+ ),
3773
+ step === "protocol" && /* @__PURE__ */ jsx4(
3774
+ SelectPrompt,
3775
+ {
3776
+ title: "API format (protocol)?",
3777
+ options: PROTOCOLS.map((p) => ({
3778
+ value: p,
3779
+ label: p,
3780
+ description: PROTOCOL_DESCRIPTIONS[p]
3781
+ })),
3782
+ onSelect: handleProtocolSelect,
3783
+ onCancel
3784
+ }
3785
+ ),
3786
+ step === "baseUrl" && /* @__PURE__ */ jsx4(
3787
+ TextField,
3788
+ {
3789
+ label: "baseURL:",
3790
+ initialValue: preset?.baseURL ?? "",
3791
+ placeholder: "https://api.example.com/v1",
3792
+ onSubmit: submitBaseURL,
3793
+ onCancel
3794
+ }
3795
+ ),
3796
+ step === "apiKey" && /* @__PURE__ */ jsx4(
3797
+ TextField,
3798
+ {
3799
+ label: "API key:",
3800
+ masked: true,
3801
+ placeholder: "paste your key (hidden)",
3802
+ onSubmit: submitApiKey,
3803
+ onCancel
3804
+ }
3805
+ ),
3806
+ step === "model" && /* @__PURE__ */ jsx4(
3807
+ TextField,
3808
+ {
3809
+ label: "Model id:",
3810
+ placeholder: preset?.modelHint ?? "e.g. gpt-4o",
3811
+ onSubmit: submitModel,
3812
+ onCancel
3813
+ }
3814
+ ),
3815
+ step === "confirm" && answers && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
3816
+ /* @__PURE__ */ jsx4(Text4, { children: ` provider: ${answers.providerName} (${answers.protocol})` }),
3817
+ /* @__PURE__ */ jsx4(Text4, { children: ` baseURL: ${answers.baseURL}` }),
3818
+ /* @__PURE__ */ jsx4(Text4, { children: ` API key: ${maskApiKey(answers.apiKey)} \u2192 ${answers.apiKeyEnv} (stored in ~/.star-cli/.env)` }),
3819
+ /* @__PURE__ */ jsx4(Text4, { children: ` model: ${answers.modelName} \u2192 ${answers.modelId}` }),
3820
+ busy ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Saving\u2026" }) : /* @__PURE__ */ jsx4(YN, { question: "Save this provider and model?", onAnswer: handleConfirm })
3821
+ ] }),
3822
+ step === "setDefault" && (busy ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Saving\u2026" }) : /* @__PURE__ */ jsx4(
3823
+ YN,
3824
+ {
3825
+ question: `Set "${answers?.modelName}" as the default model and switch to it?`,
3826
+ onAnswer: handleSetDefault
3827
+ }
3828
+ )),
3829
+ step === "openFile" && /* @__PURE__ */ jsx4(YN, { question: "Open the config file now?", onAnswer: handleOpenFile }),
3830
+ error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error })
3831
+ ] });
3832
+ }
3833
+
3834
+ // src/cli/components/ExitPlanPrompt.tsx
3835
+ import { Box as Box5, Text as Text5, useInput as useInput5 } from "ink";
3836
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
3837
+ function ExitPlanPrompt({ onDecision }) {
3838
+ useInput5((input, key) => {
3839
+ const ch = input.toLowerCase();
3840
+ if (ch === "y") onDecision("yes");
3841
+ else if (ch === "n" || key.escape) onDecision("no");
3842
+ });
3843
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
3844
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "Plan ready (plan mode)" }),
3845
+ /* @__PURE__ */ jsx5(Text5, { children: "The plan above was produced without making any changes." }),
3846
+ /* @__PURE__ */ jsx5(Text5, { children: "[y] approve and execute [n] keep refining (Esc = n)" })
3847
+ ] });
3848
+ }
3849
+
2680
3850
  // src/cli/components/MessageList.tsx
2681
- import { Box as Box3, Static, Text as Text4 } from "ink";
3851
+ import { Box as Box6, Static, Text as Text7 } from "ink";
2682
3852
  import { memo } from "react";
2683
3853
 
2684
3854
  // src/cli/components/DiffLines.tsx
2685
- import { Text as Text3 } from "ink";
2686
- import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
3855
+ import { Text as Text6 } from "ink";
3856
+ import { Fragment, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2687
3857
  function DiffLineView({ line }) {
2688
- if (line.kind === "add") return /* @__PURE__ */ jsxs3(Text3, { color: "green", children: [
3858
+ if (line.kind === "add") return /* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
2689
3859
  "+ ",
2690
3860
  line.text
2691
3861
  ] });
2692
- if (line.kind === "del") return /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
3862
+ if (line.kind === "del") return /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
2693
3863
  "- ",
2694
3864
  line.text
2695
3865
  ] });
2696
- if (line.kind === "marker") return /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: line.text });
2697
- return /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
3866
+ if (line.kind === "marker") return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: line.text });
3867
+ return /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
2698
3868
  " ",
2699
3869
  line.text
2700
3870
  ] });
2701
3871
  }
2702
3872
  function DiffLines({ lines }) {
2703
- return /* @__PURE__ */ jsx3(Fragment, { children: lines.map((line, index) => (
3873
+ return /* @__PURE__ */ jsx6(Fragment, { children: lines.map((line, index) => (
2704
3874
  // biome-ignore lint/suspicious/noArrayIndexKey: diff lines have no stable identity
2705
- /* @__PURE__ */ jsx3(DiffLineView, { line }, index)
3875
+ /* @__PURE__ */ jsx6(DiffLineView, { line }, index)
2706
3876
  )) });
2707
3877
  }
2708
3878
 
2709
3879
  // src/cli/components/MessageList.tsx
2710
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
3880
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2711
3881
  var roleStyles = {
2712
3882
  user: { label: "you", color: "cyan" },
2713
3883
  assistant: { label: "star", color: "green" },
2714
3884
  system: { label: "system", color: "yellow" },
2715
3885
  tool: { label: "tool", color: "magenta" }
2716
3886
  };
3887
+ var interruptedMarker = (text) => /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: text.length > 0 ? " [interrupted]" : "[interrupted]" });
2717
3888
  var MessageList = memo(function MessageList2({ messages }) {
2718
- return /* @__PURE__ */ jsx4(Static, { items: messages, children: (message) => {
3889
+ return /* @__PURE__ */ jsx7(Static, { items: messages, children: (message) => {
2719
3890
  const marginBottom = message.tight ? 0 : 1;
2720
3891
  if (message.role === "assistant-cont") {
2721
- return /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", marginBottom, children: /* @__PURE__ */ jsx4(Text4, { color: "green", children: message.text }) }, message.id);
3892
+ return /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", marginBottom, children: /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
3893
+ message.text,
3894
+ message.interrupted && interruptedMarker(message.text)
3895
+ ] }) }, message.id);
2722
3896
  }
2723
3897
  const style = roleStyles[message.role];
2724
- return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", marginBottom, children: [
2725
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: style.color, children: style.label }),
2726
- /* @__PURE__ */ jsx4(Text4, { color: style.color, children: message.text }),
2727
- message.diff && /* @__PURE__ */ jsx4(DiffLines, { lines: message.diff }),
2728
- message.note && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: message.note })
3898
+ return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", marginBottom, children: [
3899
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: style.color, children: style.label }),
3900
+ /* @__PURE__ */ jsxs7(Text7, { color: style.color, children: [
3901
+ message.text,
3902
+ message.interrupted && interruptedMarker(message.text)
3903
+ ] }),
3904
+ message.diff && /* @__PURE__ */ jsx7(DiffLines, { lines: message.diff }),
3905
+ message.note && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: message.note })
2729
3906
  ] }, message.id);
2730
3907
  } });
2731
3908
  });
2732
3909
 
2733
3910
  // src/cli/components/PermissionPrompt.tsx
2734
- import { Box as Box4, Text as Text5, useInput as useInput3 } from "ink";
2735
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
3911
+ import { Box as Box7, Text as Text8, useInput as useInput6 } from "ink";
3912
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2736
3913
  function PermissionPrompt({ request, preview, onDecision }) {
2737
- useInput3((input) => {
3914
+ useInput6((input) => {
2738
3915
  const ch = input.toLowerCase();
2739
3916
  if (ch === "y") onDecision("yes");
2740
3917
  else if (ch === "n") onDecision("no");
2741
3918
  else if (ch === "a") onDecision("always");
2742
3919
  });
2743
- return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
2744
- /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "yellow", children: [
3920
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
3921
+ /* @__PURE__ */ jsxs8(Text8, { bold: true, color: "yellow", children: [
2745
3922
  "Permission required: ",
2746
3923
  request.toolName,
2747
3924
  " (",
2748
3925
  request.level,
2749
3926
  ")"
2750
3927
  ] }),
2751
- preview ? /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
2752
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3928
+ preview ? /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
3929
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2753
3930
  preview.type === "new-file" ? "New file: " : "",
2754
3931
  preview.label
2755
3932
  ] }),
2756
- /* @__PURE__ */ jsx5(DiffLines, { lines: preview.lines })
2757
- ] }) : /* @__PURE__ */ jsx5(Text5, { children: summarizeArgs(request.args) }),
2758
- /* @__PURE__ */ jsx5(Text5, { children: "[y] allow [n] deny [a] always (saved to config)" })
3933
+ /* @__PURE__ */ jsx8(DiffLines, { lines: preview.lines })
3934
+ ] }) : /* @__PURE__ */ jsx8(Text8, { children: summarizeArgs(request.args) }),
3935
+ /* @__PURE__ */ jsx8(Text8, { children: "[y] allow [n] deny [a] always (saved to config)" })
2759
3936
  ] });
2760
3937
  }
2761
3938
 
2762
3939
  // src/cli/components/RewindConfirmPrompt.tsx
2763
- import { Box as Box5, Text as Text6, useInput as useInput4 } from "ink";
2764
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2765
- function RewindConfirmPrompt({ summary, onDecision }) {
2766
- useInput4((input, key) => {
3940
+ import { Box as Box8, Text as Text9, useInput as useInput7 } from "ink";
3941
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
3942
+ function RewindConfirmPrompt({
3943
+ summary,
3944
+ title = "Rewind checkpoints",
3945
+ confirmLabel = "rewind",
3946
+ diffs,
3947
+ onDecision
3948
+ }) {
3949
+ useInput7((input, key) => {
2767
3950
  const ch = input.toLowerCase();
2768
3951
  if (ch === "y") onDecision("yes");
2769
3952
  else if (ch === "n" || key.escape) onDecision("no");
2770
3953
  });
2771
- return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
2772
- /* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: "Rewind checkpoints" }),
2773
- /* @__PURE__ */ jsx6(Text6, { children: summary }),
2774
- /* @__PURE__ */ jsx6(Text6, { children: "[y] rewind [n] cancel (Esc = n)" })
2775
- ] });
2776
- }
2777
-
2778
- // src/cli/components/SelectPrompt.tsx
2779
- import { Box as Box6, Text as Text7, useInput as useInput5 } from "ink";
2780
- import { useState as useState2 } from "react";
2781
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2782
- var SELECT_PROMPT_MAX_VISIBLE = 9;
2783
- function SelectPrompt({ title, options, onSelect, onCancel }) {
2784
- const [index, setIndex] = useState2(0);
2785
- useInput5((input, key) => {
2786
- if (key.escape) {
2787
- onCancel();
2788
- return;
2789
- }
2790
- if (key.return) {
2791
- const option = options[index];
2792
- if (option) onSelect(option.value);
2793
- return;
2794
- }
2795
- if (key.downArrow || input === "j") {
2796
- setIndex((i) => Math.min(i + 1, options.length - 1));
2797
- } else if (key.upArrow || input === "k") {
2798
- setIndex((i) => Math.max(i - 1, 0));
2799
- }
2800
- });
2801
- const maxStart = Math.max(0, options.length - SELECT_PROMPT_MAX_VISIBLE);
2802
- const start = Math.min(Math.max(0, index - Math.floor(SELECT_PROMPT_MAX_VISIBLE / 2)), maxStart);
2803
- const visible = options.slice(start, start + SELECT_PROMPT_MAX_VISIBLE);
2804
- const above = start;
2805
- const below = options.length - start - visible.length;
2806
- return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
2807
- /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: title }),
2808
- above > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2809
- "\u2026 ",
2810
- above,
2811
- " more above"
2812
- ] }),
2813
- visible.map((option, i) => {
2814
- const active = start + i === index;
2815
- return /* @__PURE__ */ jsxs7(Text7, { children: [
2816
- /* @__PURE__ */ jsxs7(Text7, { bold: active, inverse: active, children: [
2817
- active ? "\u276F " : " ",
2818
- option.label
2819
- ] }),
2820
- option.hint && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2821
- " (",
2822
- option.hint,
2823
- ")"
2824
- ] }),
2825
- option.description && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2826
- " \u2014 ",
2827
- option.description
2828
- ] })
2829
- ] }, option.value);
2830
- }),
2831
- below > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2832
- "\u2026 ",
2833
- below,
2834
- " more below"
2835
- ] }),
2836
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "[\u2191/\u2193 or j/k] move [Enter] select [Esc] cancel" })
3954
+ return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
3955
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "yellow", children: title }),
3956
+ /* @__PURE__ */ jsx9(Text9, { children: summary }),
3957
+ diffs?.map((diff) => /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", children: [
3958
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: diff.label }),
3959
+ /* @__PURE__ */ jsx9(DiffLines, { lines: diff.lines })
3960
+ ] }, diff.label)),
3961
+ /* @__PURE__ */ jsxs9(Text9, { children: [
3962
+ "[y] ",
3963
+ confirmLabel,
3964
+ " [n] cancel (Esc = n)"
3965
+ ] })
2837
3966
  ] });
2838
3967
  }
2839
3968
 
2840
3969
  // src/cli/components/StatusBar.tsx
2841
- import { Box as Box7, Text as Text8, useStdout } from "ink";
3970
+ import { Box as Box9, Text as Text10, useStdout as useStdout2 } from "ink";
2842
3971
  import { memo as memo2 } from "react";
2843
- import { jsxs as jsxs8 } from "react/jsx-runtime";
3972
+ import { jsxs as jsxs10 } from "react/jsx-runtime";
2844
3973
  var BG_LABEL_MAX = 40;
2845
3974
  var WIDE_COLUMNS = 120;
2846
3975
  function cwdLabel(cwd, wide) {
@@ -2855,43 +3984,48 @@ var StatusBar = memo2(function StatusBar2({
2855
3984
  tokens: tokens2,
2856
3985
  gitBranch,
2857
3986
  contextPercent,
3987
+ cachePercent,
2858
3988
  sessionCostUsd,
2859
3989
  backgroundTasks = []
2860
3990
  }) {
2861
- const { stdout } = useStdout();
3991
+ const { stdout } = useStdout2();
2862
3992
  const wide = (stdout?.columns ?? 80) >= WIDE_COLUMNS;
2863
3993
  const joined = backgroundTasks.join(", ");
2864
3994
  const labels = joined.length > BG_LABEL_MAX ? `${joined.slice(0, BG_LABEL_MAX)}\u2026` : joined;
2865
- return /* @__PURE__ */ jsxs8(Box7, { justifyContent: "space-between", children: [
2866
- /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
3995
+ return /* @__PURE__ */ jsxs10(Box9, { justifyContent: "space-between", children: [
3996
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2867
3997
  cwdLabel(cwd, wide),
2868
3998
  gitBranch ? ` [${gitBranch}]` : ""
2869
3999
  ] }),
2870
- backgroundTasks.length > 0 && /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
4000
+ backgroundTasks.length > 0 && /* @__PURE__ */ jsxs10(Text10, { color: "yellow", children: [
2871
4001
  "bg: ",
2872
4002
  backgroundTasks.length,
2873
4003
  " (",
2874
4004
  labels,
2875
4005
  ")"
2876
4006
  ] }),
2877
- /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
4007
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2878
4008
  "model: ",
2879
4009
  model
2880
4010
  ] }),
2881
- contextPercent != null && /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
4011
+ contextPercent != null && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2882
4012
  "ctx: ",
2883
4013
  contextPercent,
2884
4014
  "%"
2885
4015
  ] }),
2886
- sessionCostUsd != null && sessionCostUsd > 0 && /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
4016
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
4017
+ "cache: ",
4018
+ cachePercent != null ? `${cachePercent}%` : "Not provided"
4019
+ ] }),
4020
+ sessionCostUsd != null && sessionCostUsd > 0 && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2887
4021
  "$",
2888
4022
  formatDollars(sessionCostUsd)
2889
4023
  ] }),
2890
- /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
4024
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2891
4025
  tokens2,
2892
4026
  " tokens"
2893
4027
  ] }),
2894
- /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
4028
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2895
4029
  "mode: ",
2896
4030
  permissionMode
2897
4031
  ] })
@@ -2899,22 +4033,22 @@ var StatusBar = memo2(function StatusBar2({
2899
4033
  });
2900
4034
 
2901
4035
  // src/cli/components/StreamingMessage.tsx
2902
- import { Box as Box8, Text as Text9 } from "ink";
4036
+ import { Box as Box10, Text as Text11 } from "ink";
2903
4037
  import { memo as memo3 } from "react";
2904
- import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
4038
+ import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
2905
4039
  var StreamingMessage = memo3(function StreamingMessage2({
2906
4040
  text,
2907
4041
  continuation
2908
4042
  }) {
2909
- return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", marginBottom: 1, children: [
2910
- !continuation && /* @__PURE__ */ jsx8(Text9, { bold: true, color: "green", children: "star" }),
2911
- /* @__PURE__ */ jsx8(Text9, { color: "green", children: text })
4043
+ return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", marginBottom: 1, children: [
4044
+ !continuation && /* @__PURE__ */ jsx10(Text11, { bold: true, color: "green", children: "star" }),
4045
+ /* @__PURE__ */ jsx10(Text11, { color: "green", children: text })
2912
4046
  ] });
2913
4047
  });
2914
4048
 
2915
4049
  // src/cli/components/ThinkingIndicator.tsx
2916
- import { Box as Box9, Text as Text10 } from "ink";
2917
- import { jsxs as jsxs10 } from "react/jsx-runtime";
4050
+ import { Box as Box11, Text as Text12 } from "ink";
4051
+ import { jsxs as jsxs12 } from "react/jsx-runtime";
2918
4052
  var SPINNER_FRAMES = ["-", "\\", "|", "/"];
2919
4053
  var REASONING_TAIL_LENGTH = 200;
2920
4054
  var THOUGHT_SUMMARY_LENGTH = 100;
@@ -2930,19 +4064,44 @@ function ThinkingIndicator({
2930
4064
  }) {
2931
4065
  const tail = reasoning ? truncateTail(reasoning, REASONING_TAIL_LENGTH) : "";
2932
4066
  const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
2933
- return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
2934
- /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
4067
+ return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", children: [
4068
+ /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
2935
4069
  spinner,
2936
4070
  " ",
2937
4071
  activity ?? "star is thinking\u2026"
2938
4072
  ] }),
2939
- tail !== "" && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
4073
+ tail !== "" && /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
2940
4074
  " ",
2941
4075
  tail
2942
4076
  ] })
2943
4077
  ] });
2944
4078
  }
2945
4079
 
4080
+ // src/cli/components/TodoPanel.tsx
4081
+ import { Box as Box12, Text as Text13 } from "ink";
4082
+ import { jsx as jsx11, jsxs as jsxs13 } from "react/jsx-runtime";
4083
+ var SYMBOLS = {
4084
+ pending: "[ ]",
4085
+ in_progress: "[~]",
4086
+ done: "[x]"
4087
+ };
4088
+ function TodoPanel({ todos }) {
4089
+ if (todos.length === 0) return null;
4090
+ return /* @__PURE__ */ jsx11(Box12, { flexDirection: "column", children: todos.map((todo) => /* @__PURE__ */ jsxs13(
4091
+ Text13,
4092
+ {
4093
+ dimColor: todo.status === "done",
4094
+ color: todo.status === "in_progress" ? "cyan" : void 0,
4095
+ children: [
4096
+ SYMBOLS[todo.status],
4097
+ " ",
4098
+ todo.title
4099
+ ]
4100
+ },
4101
+ todo.id
4102
+ )) });
4103
+ }
4104
+
2946
4105
  // src/cli/components/ToolCallCard.tsx
2947
4106
  function formatToolCard(card) {
2948
4107
  const header = `${card.name} ${card.argsSummary}${card.isError ? " [error]" : ""}`;
@@ -2959,28 +4118,44 @@ function isDoubleEscape(previous, now, windowMs = DOUBLE_ESC_WINDOW_MS) {
2959
4118
  }
2960
4119
 
2961
4120
  // src/cli/history.ts
2962
- import fs10 from "fs";
2963
- import path16 from "path";
2964
- var DEFAULT_LIMIT = 500;
2965
- var MAX_FILE_LINES = 1e3;
2966
- var TRIM_TO_LINES = 500;
4121
+ import fs12 from "fs";
4122
+ import path19 from "path";
4123
+ var HISTORY_LIMIT = 50;
4124
+ var JUNK_ENTRY_PATTERN = /^(.)\1{19,}$/;
2967
4125
  function historyPath() {
2968
- return path16.join(starHome(), "history");
4126
+ return path19.join(starHome(), "history");
2969
4127
  }
2970
4128
  function readLines(file) {
2971
- const raw = fs10.readFileSync(file, "utf8");
4129
+ const raw = fs12.readFileSync(file, "utf8");
2972
4130
  return raw.split("\n").map((line) => line.replace(/\r$/, "")).filter((line) => line.trim().length > 0);
2973
4131
  }
2974
- function loadHistory(limit = DEFAULT_LIMIT) {
4132
+ function writeHistory(file, lines) {
4133
+ fs12.mkdirSync(path19.dirname(file), { recursive: true });
4134
+ fs12.writeFileSync(file, `${lines.join("\n")}
4135
+ `);
4136
+ }
4137
+ function isJunkHistoryEntry(line) {
4138
+ return JUNK_ENTRY_PATTERN.test(line);
4139
+ }
4140
+ function isSkippableEntry(line) {
4141
+ return isJunkHistoryEntry(line) || line.startsWith("/");
4142
+ }
4143
+ function loadHistory(limit = HISTORY_LIMIT) {
2975
4144
  try {
2976
- return readLines(historyPath()).slice(-limit);
4145
+ const file = historyPath();
4146
+ const lines = readLines(file);
4147
+ const clean = lines.filter((line) => !isSkippableEntry(line));
4148
+ if (clean.length !== lines.length || clean.length > HISTORY_LIMIT) {
4149
+ writeHistory(file, clean.slice(-HISTORY_LIMIT));
4150
+ }
4151
+ return clean.slice(-limit);
2977
4152
  } catch {
2978
4153
  return [];
2979
4154
  }
2980
4155
  }
2981
4156
  function appendHistory(entry) {
2982
4157
  const line = entry.replace(/[\r\n]+/g, " ").trim();
2983
- if (line.length === 0) return;
4158
+ if (line.length === 0 || isSkippableEntry(line)) return;
2984
4159
  try {
2985
4160
  const file = historyPath();
2986
4161
  let existing = [];
@@ -2989,15 +4164,12 @@ function appendHistory(entry) {
2989
4164
  } catch {
2990
4165
  }
2991
4166
  if (existing[existing.length - 1] === line) return;
2992
- if (existing.length >= MAX_FILE_LINES) {
2993
- const kept = [...existing.slice(-(TRIM_TO_LINES - 1)), line];
2994
- fs10.mkdirSync(path16.dirname(file), { recursive: true });
2995
- fs10.writeFileSync(file, `${kept.join("\n")}
2996
- `);
4167
+ if (existing.length >= HISTORY_LIMIT) {
4168
+ writeHistory(file, [...existing.slice(-(HISTORY_LIMIT - 1)), line]);
2997
4169
  return;
2998
4170
  }
2999
- fs10.mkdirSync(path16.dirname(file), { recursive: true });
3000
- fs10.appendFileSync(file, `${line}
4171
+ fs12.mkdirSync(path19.dirname(file), { recursive: true });
4172
+ fs12.appendFileSync(file, `${line}
3001
4173
  `);
3002
4174
  } catch {
3003
4175
  }
@@ -3118,13 +4290,15 @@ async function checkForUpdate(currentVersion, fetcher = defaultFetcher2) {
3118
4290
  }
3119
4291
 
3120
4292
  // src/cli/repl.tsx
3121
- import { jsx as jsx9, jsxs as jsxs11 } from "react/jsx-runtime";
4293
+ import { jsx as jsx12, jsxs as jsxs14 } from "react/jsx-runtime";
3122
4294
  var CLEAR_TERMINAL = process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H";
3123
4295
  function emptyUsage() {
3124
4296
  return { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
3125
4297
  }
3126
4298
  function runningTaskLabels() {
3127
- return defaultTaskManager.list().filter((t) => t.status === "running").map((t) => t.description ?? t.command);
4299
+ const shells = defaultTaskManager.list().filter((t) => t.status === "running").map((t) => t.description ?? t.command);
4300
+ const agents = defaultAgentTasks.list().filter((t) => t.status === "running").map((t) => t.description ?? t.id);
4301
+ return [...shells, ...agents];
3128
4302
  }
3129
4303
  var SESSION_MODE_CYCLE = ["ask", "auto", "readonly", "plan"];
3130
4304
  function formatUsage(usage) {
@@ -3151,64 +4325,67 @@ function Repl({
3151
4325
  initialUsage
3152
4326
  }) {
3153
4327
  const { exit } = useApp();
3154
- const [initialDisplay] = useState3(() => buildDisplayMessages(initialMessages ?? []));
3155
- const [messages, setMessages] = useState3(initialDisplay);
3156
- const [epoch, setEpoch] = useState3(0);
3157
- const [streamingText, setStreamingText] = useState3(null);
3158
- const [isStreaming, setIsStreaming] = useState3(false);
3159
- const [thinking, setThinking] = useState3(false);
3160
- const [thinkingText, setThinkingText] = useState3("");
3161
- const [thoughtSummary, setThoughtSummary] = useState3(null);
3162
- const [, setUsageVersion] = useState3(0);
3163
- const [modelName, setModelName] = useState3(model);
3164
- const [pending, setPending] = useState3(null);
3165
- const [spinnerTick, setSpinnerTick] = useState3(0);
3166
- const [activity, setActivity] = useState3(null);
3167
- const [bgLabels, setBgLabels] = useState3(() => runningTaskLabels());
3168
- const [permissionModeState, setPermissionModeState] = useState3(permissionMode);
3169
- const [planApproval, setPlanApprovalState] = useState3(false);
3170
- const [pendingRewind, setPendingRewindState] = useState3(null);
3171
- const [picker, setPickerState] = useState3(null);
3172
- const backendRef = useRef2(backend);
3173
- const nextIdRef = useRef2(initialDisplay.length);
3174
- const abortRef = useRef2(null);
3175
- const streamedRef = useRef2("");
3176
- const thinkingRef = useRef2(false);
3177
- const reasoningRef = useRef2("");
3178
- const tickerStopRef = useRef2(null);
3179
- const flushedRef = useRef2({ streamed: "", reasoning: "" });
3180
- const toolCardsRef = useRef2(/* @__PURE__ */ new Map());
3181
- const pendingRef = useRef2(null);
3182
- const alwaysAllowedRef = useRef2(/* @__PURE__ */ new Set());
3183
- const prevModeRef = useRef2(null);
3184
- const planApprovalRef = useRef2(false);
3185
- const pendingRewindRef = useRef2(null);
3186
- const pickerRef = useRef2(null);
3187
- const modelNameRef = useRef2(model);
3188
- const sessionStoreRef = useRef2(sessionStore);
3189
- const usageRef = useRef2(initialUsage ? { ...initialUsage } : emptyUsage());
3190
- const turnChunksRef = useRef2(0);
3191
- const messagesRef = useRef2(initialDisplay);
3192
- const [queueItems, setQueueItems] = useState3([]);
3193
- const queueRef = useRef2(new PromptQueue());
3194
- const [pendingImages, setPendingImages] = useState3([]);
3195
- const pendingImagesRef = useRef2([]);
3196
- const imageSeqRef = useRef2(1);
3197
- const [inputRefill, setInputRefill] = useState3();
3198
- const refillSeqRef = useRef2(0);
3199
- const lastEscRef = useRef2(null);
3200
- const [gitBranch, setGitBranch] = useState3(null);
3201
- const [contextPercent, setContextPercent] = useState3(null);
3202
- const [initialHistory] = useState3(() => loadHistory());
3203
- const budgetWarnedRef = useRef2(false);
3204
- const budgetExceededRef = useRef2(false);
3205
- const runStreamRef = useRef2(
4328
+ const [initialDisplay] = useState5(() => buildDisplayMessages(initialMessages ?? []));
4329
+ const [messages, setMessages] = useState5(initialDisplay);
4330
+ const [epoch, setEpoch] = useState5(0);
4331
+ const [streamingText, setStreamingText] = useState5(null);
4332
+ const [isStreaming, setIsStreaming] = useState5(false);
4333
+ const [thinking, setThinking] = useState5(false);
4334
+ const [thinkingText, setThinkingText] = useState5("");
4335
+ const [thoughtSummary, setThoughtSummary] = useState5(null);
4336
+ const [, setUsageVersion] = useState5(0);
4337
+ const [modelName, setModelName] = useState5(model);
4338
+ const [pending, setPending] = useState5(null);
4339
+ const [spinnerTick, setSpinnerTick] = useState5(0);
4340
+ const [activity, setActivity] = useState5(null);
4341
+ const [bgLabels, setBgLabels] = useState5(() => runningTaskLabels());
4342
+ const [todos, setTodos] = useState5([]);
4343
+ const [permissionModeState, setPermissionModeState] = useState5(permissionMode);
4344
+ const [planApproval, setPlanApprovalState] = useState5(false);
4345
+ const [pendingRewind, setPendingRewindState] = useState5(null);
4346
+ const [picker, setPickerState] = useState5(null);
4347
+ const [pendingConnect, setPendingConnectState] = useState5(null);
4348
+ const backendRef = useRef5(backend);
4349
+ const nextIdRef = useRef5(initialDisplay.length);
4350
+ const abortRef = useRef5(null);
4351
+ const streamedRef = useRef5("");
4352
+ const thinkingRef = useRef5(false);
4353
+ const reasoningRef = useRef5("");
4354
+ const tickerStopRef = useRef5(null);
4355
+ const flushedRef = useRef5({ streamed: "", reasoning: "" });
4356
+ const toolCardsRef = useRef5(/* @__PURE__ */ new Map());
4357
+ const pendingRef = useRef5(null);
4358
+ const alwaysAllowedRef = useRef5(/* @__PURE__ */ new Set());
4359
+ const prevModeRef = useRef5(null);
4360
+ const planApprovalRef = useRef5(false);
4361
+ const pendingRewindRef = useRef5(null);
4362
+ const pickerRef = useRef5(null);
4363
+ const pendingConnectRef = useRef5(null);
4364
+ const modelNameRef = useRef5(model);
4365
+ const sessionStoreRef = useRef5(sessionStore);
4366
+ const usageRef = useRef5(initialUsage ? { ...initialUsage } : emptyUsage());
4367
+ const turnChunksRef = useRef5(0);
4368
+ const messagesRef = useRef5(initialDisplay);
4369
+ const [queueItems, setQueueItems] = useState5([]);
4370
+ const queueRef = useRef5(new PromptQueue());
4371
+ const [pendingImages, setPendingImages] = useState5([]);
4372
+ const pendingImagesRef = useRef5([]);
4373
+ const imageSeqRef = useRef5(1);
4374
+ const [inputRefill, setInputRefill] = useState5();
4375
+ const refillSeqRef = useRef5(0);
4376
+ const lastEscRef = useRef5(null);
4377
+ const [gitBranch, setGitBranch] = useState5(null);
4378
+ const [contextPercent, setContextPercent] = useState5(null);
4379
+ const [initialHistory] = useState5(() => loadHistory());
4380
+ const budgetWarnedRef = useRef5(false);
4381
+ const budgetExceededRef = useRef5(false);
4382
+ const runStreamRef = useRef5(
3206
4383
  null
3207
4384
  );
3208
4385
  const pushMessage = useCallback(
3209
- (role, text, note, tight) => {
4386
+ (role, text, note, tight, interrupted) => {
3210
4387
  setMessages((prev) => {
3211
- const next = [...prev, { id: nextIdRef.current++, role, text, note, tight }];
4388
+ const next = [...prev, { id: nextIdRef.current++, role, text, note, tight, interrupted }];
3212
4389
  messagesRef.current = next;
3213
4390
  return next;
3214
4391
  });
@@ -3230,10 +4407,10 @@ function Repl({
3230
4407
  [applyMessages]
3231
4408
  );
3232
4409
  const pushAssistantChunk = useCallback(
3233
- (text, tight) => {
4410
+ (text, tight, interrupted = false) => {
3234
4411
  const role = turnChunksRef.current === 0 ? "assistant" : "assistant-cont";
3235
4412
  turnChunksRef.current += 1;
3236
- pushMessage(role, text, void 0, tight);
4413
+ pushMessage(role, text, void 0, tight, interrupted);
3237
4414
  },
3238
4415
  [pushMessage]
3239
4416
  );
@@ -3253,6 +4430,10 @@ function Repl({
3253
4430
  pickerRef.current = p;
3254
4431
  setPickerState(p);
3255
4432
  }, []);
4433
+ const setPendingConnect = useCallback((p) => {
4434
+ pendingConnectRef.current = p;
4435
+ setPendingConnectState(p);
4436
+ }, []);
3256
4437
  const showPicker = useCallback(
3257
4438
  (title, options) => {
3258
4439
  if (pickerRef.current) return Promise.resolve(null);
@@ -3296,10 +4477,17 @@ function Repl({
3296
4477
  [config]
3297
4478
  );
3298
4479
  const handlePasteImage = useCallback(() => {
3299
- void readClipboardImage().then((img) => {
3300
- if (img) stageClipboardImage(img);
4480
+ void readClipboardImage().then((result) => {
4481
+ if (!result) return;
4482
+ if (result.oversized) {
4483
+ pushMessage(
4484
+ "system",
4485
+ `Image exceeds ${MAX_IMAGE_DIMENSION}px on its longest side and could not be resized automatically; the model may reject it.`
4486
+ );
4487
+ }
4488
+ stageClipboardImage(result.image);
3301
4489
  });
3302
- }, [stageClipboardImage]);
4490
+ }, [stageClipboardImage, pushMessage]);
3303
4491
  const handleRewindDecision = useCallback(
3304
4492
  (decision) => {
3305
4493
  const p = pendingRewindRef.current;
@@ -3309,6 +4497,32 @@ function Repl({
3309
4497
  },
3310
4498
  [setPendingRewind]
3311
4499
  );
4500
+ const handleConnectSave = useCallback(
4501
+ async (answers) => {
4502
+ const result = saveConnection(answers);
4503
+ config.providers.push(result.provider);
4504
+ config.models.push(result.model);
4505
+ 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}.`;
4506
+ },
4507
+ [config]
4508
+ );
4509
+ const handleConnectOpenConfig = useCallback(() => {
4510
+ const file = globalConfigPath();
4511
+ openConfigFile(file);
4512
+ return `Opening ${file} with the system default app (open it manually if nothing showed up).`;
4513
+ }, []);
4514
+ const finishConnect = useCallback(
4515
+ (message) => {
4516
+ const p = pendingConnectRef.current;
4517
+ if (!p) return;
4518
+ setPendingConnect(null);
4519
+ p.resolve(message);
4520
+ },
4521
+ [setPendingConnect]
4522
+ );
4523
+ const cancelConnect = useCallback(() => {
4524
+ finishConnect("Connect cancelled \u2014 nothing was saved.");
4525
+ }, [finishConnect]);
3312
4526
  const applySessionMode = useCallback(
3313
4527
  (mode) => {
3314
4528
  if (mode === config.permissionMode) return;
@@ -3351,6 +4565,12 @@ function Repl({
3351
4565
  if (message) pushMessage("system", message);
3352
4566
  });
3353
4567
  }, [pushMessage]);
4568
+ const resumedAtStart = initialMessages !== void 0;
4569
+ useEffect2(() => {
4570
+ if (!resumedAtStart) return;
4571
+ loadTodos(cwd).then(setTodos).catch(() => {
4572
+ });
4573
+ }, [cwd, resumedAtStart]);
3354
4574
  useEffect2(() => {
3355
4575
  const onUpdate = (task) => {
3356
4576
  setBgLabels(runningTaskLabels());
@@ -3366,10 +4586,28 @@ function Repl({
3366
4586
  });
3367
4587
  }
3368
4588
  };
4589
+ const onAgentUpdate = (task) => {
4590
+ setBgLabels(runningTaskLabels());
4591
+ const label = task.description ? `: ${task.description}` : "";
4592
+ pushMessage(
4593
+ "system",
4594
+ task.status === "running" ? `Background subagent ${task.id} started${label}` : `Background subagent ${task.id} ${task.status}${label}`
4595
+ );
4596
+ if (task.status !== "running") {
4597
+ notifyBell({
4598
+ enabled: config.notifyBell,
4599
+ thresholdSec: config.notifyBellThresholdSec,
4600
+ noNotifyEnv: process.env.STAR_NO_NOTIFY === "1"
4601
+ });
4602
+ }
4603
+ };
3369
4604
  defaultTaskManager.on("update", onUpdate);
4605
+ defaultAgentTasks.on("update", onAgentUpdate);
3370
4606
  return () => {
3371
4607
  defaultTaskManager.off("update", onUpdate);
4608
+ defaultAgentTasks.off("update", onAgentUpdate);
3372
4609
  defaultTaskManager.cleanup();
4610
+ defaultAgentTasks.cleanup();
3373
4611
  };
3374
4612
  }, [pushMessage, config]);
3375
4613
  const interrupt = useCallback(() => {
@@ -3409,17 +4647,16 @@ function Repl({
3409
4647
  }, [pushMessage, redrawMessages]);
3410
4648
  const handleExit = useCallback(() => {
3411
4649
  const killed = defaultTaskManager.cleanup();
3412
- if (killed.length > 0) {
3413
- pushMessage(
3414
- "system",
3415
- `Stopped ${killed.length} background task(s): ${killed.map((t) => t.id).join(", ")}`
3416
- );
4650
+ const killedAgents = defaultAgentTasks.cleanup();
4651
+ const stopped = [...killed.map((t) => t.id), ...killedAgents.map((t) => t.id)];
4652
+ if (stopped.length > 0) {
4653
+ pushMessage("system", `Stopped ${stopped.length} background task(s): ${stopped.join(", ")}`);
3417
4654
  }
3418
4655
  exit();
3419
4656
  }, [exit, pushMessage]);
3420
- useInput6((_input, key) => {
4657
+ useInput8((_input, key) => {
3421
4658
  if (key.escape) {
3422
- const busy = abortRef.current !== null || pendingRef.current !== null || planApprovalRef.current || pendingRewindRef.current !== null || pickerRef.current !== null;
4659
+ const busy = abortRef.current !== null || pendingRef.current !== null || planApprovalRef.current || pendingRewindRef.current !== null || pendingConnectRef.current !== null || pickerRef.current !== null;
3423
4660
  if (busy) {
3424
4661
  lastEscRef.current = null;
3425
4662
  interrupt();
@@ -3436,7 +4673,7 @@ function Repl({
3436
4673
  }
3437
4674
  lastEscRef.current = null;
3438
4675
  if (key.shift && key.tab) {
3439
- if (abortRef.current || pendingRef.current || planApprovalRef.current || pendingRewindRef.current || pickerRef.current)
4676
+ if (abortRef.current || pendingRef.current || planApprovalRef.current || pendingRewindRef.current || pendingConnectRef.current || pickerRef.current)
3440
4677
  return;
3441
4678
  const current = config.permissionMode;
3442
4679
  const index = SESSION_MODE_CYCLE.indexOf(current);
@@ -3492,6 +4729,15 @@ function Repl({
3492
4729
  },
3493
4730
  [config, cwd, attachConfirmHandler]
3494
4731
  );
4732
+ const handleConnectSetDefault = useCallback(
4733
+ async (modelName2) => {
4734
+ saveDefaultModel(modelName2);
4735
+ config.defaultModel = modelName2;
4736
+ const switched = await switchModel(modelName2);
4737
+ return `Default model set to "${modelName2}" (saved to config). ${switched}`;
4738
+ },
4739
+ [config, switchModel]
4740
+ );
3495
4741
  const resume = useCallback(
3496
4742
  async (id) => {
3497
4743
  const current = backendRef.current;
@@ -3507,6 +4753,7 @@ function Repl({
3507
4753
  return `Session not found: ${id}`;
3508
4754
  }
3509
4755
  await current.loadMessages(resumed.messages);
4756
+ setTodos(await loadTodos(cwd));
3510
4757
  const store = await SessionStore.open(resolvedId);
3511
4758
  if (store) {
3512
4759
  hydrateSnapshots(await loadSessionSnapshots(store.dir));
@@ -3518,7 +4765,7 @@ function Repl({
3518
4765
  setUsageVersion((v) => v + 1);
3519
4766
  return `Resumed session ${resolvedId} (${resumed.messages.length} messages).`;
3520
4767
  },
3521
- [applyMessages]
4768
+ [applyMessages, cwd]
3522
4769
  );
3523
4770
  const runStream = useCallback(
3524
4771
  async (input, extraImages = []) => {
@@ -3598,6 +4845,10 @@ function Repl({
3598
4845
  name: event.name,
3599
4846
  argsSummary: summarizeArgs(event.args)
3600
4847
  });
4848
+ if (event.name === "todo_write") {
4849
+ const list = parseTodoArgs(event.args);
4850
+ if (list) setTodos(list);
4851
+ }
3601
4852
  setActivity(`running ${event.name}: ${summarizeArgs(event.args, 60)}`);
3602
4853
  } else if (event.type === "tool-result") {
3603
4854
  const card = toolCardsRef.current.get(event.id) ?? {
@@ -3626,10 +4877,21 @@ function Repl({
3626
4877
  usage.promptTokens += event.usage.promptTokens;
3627
4878
  usage.completionTokens += event.usage.completionTokens;
3628
4879
  usage.totalTokens += event.usage.totalTokens;
4880
+ if (cacheUsageReported(event.usage)) {
4881
+ if (!usage.cache) usage.cache = { cachedTokens: 0, promptTokens: 0 };
4882
+ addToCacheTotals(usage.cache, event.usage);
4883
+ }
3629
4884
  setUsageVersion((v) => v + 1);
3630
4885
  sessionStoreRef.current?.addUsage(event.usage).catch(() => {
3631
4886
  });
3632
4887
  }
4888
+ } else if (event.type === "retry") {
4889
+ pushMessage(
4890
+ "system",
4891
+ `Request failed, retrying (${event.attempt}/${event.maxAttempts}): ${event.reason}`
4892
+ );
4893
+ } else if (event.type === "notice") {
4894
+ pushMessage("system", event.message);
3633
4895
  } else if (event.type === "error") {
3634
4896
  pushMessage("system", `Error: ${formatStreamError(event.error)}`);
3635
4897
  }
@@ -3665,9 +4927,9 @@ function Repl({
3665
4927
  elapsedMs: Date.now() - turnStartedAt
3666
4928
  });
3667
4929
  if (finalText.length > 0) {
3668
- pushAssistantChunk(`${finalText}${interrupted ? " [interrupted]" : ""}`, false);
4930
+ pushAssistantChunk(finalText, false, interrupted);
3669
4931
  } else if (interrupted && turnChunksRef.current > 0) {
3670
- pushAssistantChunk("[interrupted]", false);
4932
+ pushAssistantChunk("", false, true);
3671
4933
  }
3672
4934
  for (const card of toolCardsRef.current.values()) {
3673
4935
  pushMessage("tool", formatToolCard(card));
@@ -3896,12 +5158,20 @@ ${lines.join("\n")}`;
3896
5158
  await current.loadMessages(first?.role === "system" ? [first] : []);
3897
5159
  clearSnapshots();
3898
5160
  redrawMessages([]);
5161
+ resetTodos();
5162
+ setTodos([]);
3899
5163
  usageRef.current = emptyUsage();
3900
5164
  budgetWarnedRef.current = false;
3901
5165
  budgetExceededRef.current = false;
3902
5166
  setUsageVersion((v) => v + 1);
3903
5167
  return `Started a new session (${store.id}) with a clean context.`;
3904
5168
  },
5169
+ connect: async () => {
5170
+ if (pendingConnectRef.current) return "The connect wizard is already open.";
5171
+ return new Promise((resolve) => {
5172
+ setPendingConnect({ resolve });
5173
+ });
5174
+ },
3905
5175
  clearSessions: async (all) => {
3906
5176
  const currentId = sessionStoreRef.current?.id;
3907
5177
  const removed = await clearSessions(all ? void 0 : cwd, currentId);
@@ -3955,6 +5225,26 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
3955
5225
  if (!(current instanceof AgentLoop)) {
3956
5226
  return "Nothing to undo.";
3957
5227
  }
5228
+ const preview = current.previewLastTurnRetraction();
5229
+ if (preview.removed === 0) {
5230
+ return "Nothing to undo (no conversation turn to retract).";
5231
+ }
5232
+ const diffs = await buildUndoDiffs(
5233
+ preview.turn !== void 0 ? listTurnSnapshots(preview.turn) : [],
5234
+ cwd
5235
+ );
5236
+ const confirmed = await new Promise((resolve) => {
5237
+ setPendingRewind({
5238
+ title: "Undo last turn",
5239
+ confirmLabel: "undo",
5240
+ summary: `${preview.removed} message(s) will be retracted, ${diffs.length} file change(s) reverted.`,
5241
+ diffs,
5242
+ resolve
5243
+ });
5244
+ });
5245
+ if (!confirmed) {
5246
+ return "Undo cancelled.";
5247
+ }
3958
5248
  const { removed, turn } = await current.retractLastTurn();
3959
5249
  if (removed === 0) {
3960
5250
  return "Nothing to undo (no conversation turn to retract).";
@@ -4068,6 +5358,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
4068
5358
  `maxSteps: ${config.maxSteps}`,
4069
5359
  `contextMaxTokens: ${config.contextMaxTokens}`,
4070
5360
  `streamIdleTimeoutSec: ${config.streamIdleTimeoutSec}`,
5361
+ `streamFirstChunkTimeoutSec: ${config.streamFirstChunkTimeoutSec}`,
5362
+ `streamMaxRetries: ${config.streamMaxRetries}`,
5363
+ `maxAutoContinues: ${config.maxAutoContinues}`,
4071
5364
  `permissions.allow (${config.permissions.allow.length}): ${config.permissions.allow.join(", ") || "(none)"}`,
4072
5365
  `hooks (${config.hooks.length}): ${config.hooks.map((h) => h.event).join(", ") || "(none)"}`
4073
5366
  ].join("\n")
@@ -4088,6 +5381,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
4088
5381
  redrawMessages,
4089
5382
  applySessionMode,
4090
5383
  setPendingRewind,
5384
+ setPendingConnect,
4091
5385
  showPicker
4092
5386
  ]);
4093
5387
  const runShellBang = useCallback(
@@ -4192,9 +5486,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
4192
5486
  }
4193
5487
  }, [cwd, config, isStreaming, epoch]);
4194
5488
  const sessionCost = sessionCostUsd();
4195
- return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", children: [
4196
- /* @__PURE__ */ jsx9(MessageList, { messages }, `messages-${epoch}`),
4197
- (thinking || activity !== null) && /* @__PURE__ */ jsx9(
5489
+ return /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", children: [
5490
+ /* @__PURE__ */ jsx12(MessageList, { messages }, `messages-${epoch}`),
5491
+ (thinking || activity !== null) && /* @__PURE__ */ jsx12(
4198
5492
  ThinkingIndicator,
4199
5493
  {
4200
5494
  reasoning: thinkingText,
@@ -4202,9 +5496,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
4202
5496
  activity: activity ?? void 0
4203
5497
  }
4204
5498
  ),
4205
- !thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */ jsx9(Text11, { dimColor: true, children: thoughtSummary }),
4206
- streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */ jsx9(StreamingMessage, { text: streamingText, continuation: turnChunksRef.current > 0 }),
4207
- pending && /* @__PURE__ */ jsx9(
5499
+ !thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */ jsx12(Text14, { dimColor: true, children: thoughtSummary }),
5500
+ streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */ jsx12(StreamingMessage, { text: streamingText, continuation: turnChunksRef.current > 0 }),
5501
+ pending && /* @__PURE__ */ jsx12(
4208
5502
  PermissionPrompt,
4209
5503
  {
4210
5504
  request: pending.request,
@@ -4212,9 +5506,18 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
4212
5506
  onDecision: handleDecision
4213
5507
  }
4214
5508
  ),
4215
- planApproval && /* @__PURE__ */ jsx9(ExitPlanPrompt, { onDecision: handlePlanDecision }),
4216
- pendingRewind && /* @__PURE__ */ jsx9(RewindConfirmPrompt, { summary: pendingRewind.summary, onDecision: handleRewindDecision }),
4217
- picker && /* @__PURE__ */ jsx9(
5509
+ planApproval && /* @__PURE__ */ jsx12(ExitPlanPrompt, { onDecision: handlePlanDecision }),
5510
+ pendingRewind && /* @__PURE__ */ jsx12(
5511
+ RewindConfirmPrompt,
5512
+ {
5513
+ summary: pendingRewind.summary,
5514
+ title: pendingRewind.title,
5515
+ confirmLabel: pendingRewind.confirmLabel,
5516
+ diffs: pendingRewind.diffs,
5517
+ onDecision: handleRewindDecision
5518
+ }
5519
+ ),
5520
+ picker && /* @__PURE__ */ jsx12(
4218
5521
  SelectPrompt,
4219
5522
  {
4220
5523
  title: picker.title,
@@ -4223,20 +5526,33 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
4223
5526
  onCancel: handlePickerCancel
4224
5527
  }
4225
5528
  ),
4226
- queueItems.map((item) => /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
5529
+ pendingConnect && /* @__PURE__ */ jsx12(
5530
+ ConnectWizard,
5531
+ {
5532
+ existingProviderNames: config.providers.map((p) => p.name),
5533
+ existingModelNames: config.models.map((m) => m.name),
5534
+ onSave: handleConnectSave,
5535
+ onSetDefault: handleConnectSetDefault,
5536
+ onOpenConfig: handleConnectOpenConfig,
5537
+ onFinish: finishConnect,
5538
+ onCancel: cancelConnect
5539
+ }
5540
+ ),
5541
+ queueItems.map((item) => /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
4227
5542
  "queued: ",
4228
5543
  item.text.length > 80 ? `${item.text.slice(0, 80)}\u2026` : item.text
4229
5544
  ] }, item.id)),
4230
- pendingImages.map((staged) => /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
5545
+ pendingImages.map((staged) => /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
4231
5546
  "[image attached: ",
4232
5547
  staged.image.path,
4233
5548
  "]"
4234
5549
  ] }, staged.id)),
4235
- /* @__PURE__ */ jsx9(
5550
+ /* @__PURE__ */ jsx12(TodoPanel, { todos }),
5551
+ /* @__PURE__ */ jsx12(
4236
5552
  InputBox,
4237
5553
  {
4238
5554
  isStreaming,
4239
- disabled: pending !== null || planApproval || pendingRewind !== null || picker !== null,
5555
+ disabled: pending !== null || planApproval || pendingRewind !== null || pendingConnect !== null || picker !== null,
4240
5556
  commands: commandHints,
4241
5557
  cwd,
4242
5558
  refill: inputRefill,
@@ -4247,7 +5563,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
4247
5563
  onPasteImage: handlePasteImage
4248
5564
  }
4249
5565
  ),
4250
- /* @__PURE__ */ jsx9(
5566
+ /* @__PURE__ */ jsx12(
4251
5567
  StatusBar,
4252
5568
  {
4253
5569
  cwd,
@@ -4256,6 +5572,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
4256
5572
  tokens: usageRef.current.totalTokens,
4257
5573
  gitBranch,
4258
5574
  contextPercent,
5575
+ cachePercent: usageRef.current.cache ? cacheHitPercent(usageRef.current.cache) : null,
4259
5576
  sessionCostUsd: sessionCost,
4260
5577
  backgroundTasks: bgLabels
4261
5578
  }
@@ -4267,7 +5584,7 @@ function renderRepl(backend, opts) {
4267
5584
  defaultTaskManager.cleanup();
4268
5585
  });
4269
5586
  return render(
4270
- /* @__PURE__ */ jsx9(
5587
+ /* @__PURE__ */ jsx12(
4271
5588
  Repl,
4272
5589
  {
4273
5590
  backend,
@@ -4284,7 +5601,7 @@ function renderRepl(backend, opts) {
4284
5601
  }
4285
5602
 
4286
5603
  // src/config/loader.ts
4287
- import fs11 from "fs";
5604
+ import fs13 from "fs";
4288
5605
  import { parse as parse2 } from "smol-toml";
4289
5606
  var PartialConfigSchema = ConfigSchema.partial();
4290
5607
  function parseTomlFile(content, filePath) {
@@ -4312,11 +5629,12 @@ function applyOverrides(config, overrides) {
4312
5629
  return merged;
4313
5630
  }
4314
5631
  function readTomlFileSync(filePath) {
4315
- if (!fs11.existsSync(filePath)) return {};
4316
- const content = fs11.readFileSync(filePath, "utf8");
5632
+ if (!fs13.existsSync(filePath)) return {};
5633
+ const content = fs13.readFileSync(filePath, "utf8");
4317
5634
  return parseTomlFile(content, filePath);
4318
5635
  }
4319
5636
  function loadConfigSync(cwd, overrides) {
5637
+ loadEnvFile();
4320
5638
  const global = readTomlFileSync(globalConfigPath());
4321
5639
  const project = readTomlFileSync(projectConfigPath(cwd));
4322
5640
  const merged = applyOverrides(mergeConfig(global, project), overrides);
@@ -4327,7 +5645,13 @@ function loadConfigSync(cwd, overrides) {
4327
5645
  var SYSTEM_PROMPT = `You are Star CLI, an AI coding agent running in the user's terminal.
4328
5646
  You help with software engineering tasks: reading, writing and editing code, running shell commands, and managing todos.
4329
5647
  Be concise and direct. Use tools when they help accomplish the task.
4330
- The working directory is the user's project root; never touch files outside it without explicit instruction.`;
5648
+ For any task with two or more steps, start by creating a todo list with todo_write and keep it updated as you progress; only mark an item completed after you have verified its result.
5649
+ The working directory is the user's project root; never touch files outside it without explicit instruction.
5650
+ For conversion, batch-processing, or other scriptable tasks, write a script file into the working directory and run it instead of pasting long inline code into the shell; once the script's output is verified, delete the script unless the user asked to keep it.
5651
+ When a tool call or command fails, read the error output, work out the cause, and try again with a corrected or alternative approach \u2014 never repeat an identical failing call without changing something.
5652
+ Never consider the task finished until you have verified the result yourself: run the tests, the build, or a check command that proves the output is correct, and if verification fails or reveals gaps, keep fixing until it passes.
5653
+ Do not end your turn while the task is still incomplete; keep going until it is done or you are genuinely blocked, and if you are blocked, state exactly what is missing.
5654
+ Never end a reply by announcing what you will do next \u2014 either do it now with tool calls, or do not mention it. Narrating future actions is not progress.`;
4331
5655
  async function createLoop(config, modelName, cwd, sessionStore) {
4332
5656
  const model = createModel(config, modelName);
4333
5657
  const registry = createDefaultRegistry();
@@ -4411,6 +5735,19 @@ async function printMode(loop, prompt, cwd, json, imagePaths) {
4411
5735
  case "finish":
4412
5736
  usage.add(event.usage);
4413
5737
  break;
5738
+ case "retry":
5739
+ if (!json)
5740
+ process.stderr.write(
5741
+ `
5742
+ [retry ${event.attempt}/${event.maxAttempts}] ${event.reason}
5743
+ `
5744
+ );
5745
+ break;
5746
+ case "notice":
5747
+ if (!json) process.stderr.write(`
5748
+ [notice] ${event.message}
5749
+ `);
5750
+ break;
4414
5751
  case "error":
4415
5752
  if (!json) process.stderr.write(`
4416
5753
  [error] ${formatStreamError(event.error)}
@@ -4444,6 +5781,7 @@ async function printMode(loop, prompt, cwd, json, imagePaths) {
4444
5781
  }
4445
5782
  if (!json) process.stdout.write("\n");
4446
5783
  defaultTaskManager.cleanup();
5784
+ defaultAgentTasks.cleanup();
4447
5785
  return exitCode;
4448
5786
  }
4449
5787
  var program = new Command();
@@ -4539,6 +5877,7 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
4539
5877
  }
4540
5878
  if (resumed) {
4541
5879
  await loop.loadMessages(resumed.messages);
5880
+ await loadTodos(cwd);
4542
5881
  if (sessionStore) {
4543
5882
  hydrateSnapshots(await loadSessionSnapshots(sessionStore.dir));
4544
5883
  }