@cryer/star-cli 0.3.0 → 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,
@@ -17,6 +18,7 @@ import {
17
18
  defaultAgentTasks,
18
19
  defaultTaskManager,
19
20
  discoverSkills,
21
+ envFilePath,
20
22
  estimateTokens,
21
23
  formatTaskFinished,
22
24
  formatTaskList,
@@ -30,18 +32,22 @@ import {
30
32
  isGitRepo,
31
33
  isSensitivePath,
32
34
  listSnapshots,
35
+ listTurnSnapshots,
33
36
  loadTodos,
34
37
  parseTodoArgs,
38
+ probeImageDimensions,
35
39
  projectConfigPath,
36
40
  reconcileToolCalls,
41
+ resetTodos,
37
42
  resolveShell,
43
+ resolveSnapshotContent,
38
44
  rewindToSnapshot,
39
45
  sessionsDir,
40
46
  starHome,
41
47
  summarizeMessages,
42
48
  undoTurnSnapshots,
43
49
  userMemoryPath
44
- } from "./chunk-DZWTIMJ2.js";
50
+ } from "./chunk-K4SIXWCI.js";
45
51
 
46
52
  // src/main.tsx
47
53
  import { Command } from "commander";
@@ -109,10 +115,125 @@ function buildDisplayMessages(messages) {
109
115
  }
110
116
 
111
117
  // src/cli/mentions.ts
112
- 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";
113
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
114
233
  var MAX_MENTION_BYTES = 100 * 1024;
115
234
  var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
235
+ var MAX_DIR_MENTION_ENTRIES = 200;
236
+ var MAX_DIR_MENTION_DEPTH = 10;
116
237
  var IMAGE_MIME_TYPES = {
117
238
  ".png": "image/png",
118
239
  ".jpg": "image/jpeg",
@@ -121,17 +242,22 @@ var IMAGE_MIME_TYPES = {
121
242
  ".webp": "image/webp"
122
243
  };
123
244
  function imageMimeType(filePath) {
124
- return IMAGE_MIME_TYPES[path.extname(filePath).toLowerCase()] ?? null;
245
+ return IMAGE_MIME_TYPES[path2.extname(filePath).toLowerCase()] ?? null;
125
246
  }
126
247
  async function readImageInput(filePath, cwd) {
127
248
  const mimeType = imageMimeType(filePath);
128
249
  if (!mimeType) return null;
129
- const abs = path.resolve(cwd, filePath);
250
+ const abs = path2.resolve(cwd, filePath);
130
251
  const st = await stat(abs).catch(() => null);
131
252
  if (!st || !st.isFile() || st.size > MAX_IMAGE_BYTES) return null;
132
- const buf = await readFile(abs).catch(() => null);
253
+ const buf = await readFile2(abs).catch(() => null);
133
254
  if (!buf) return null;
134
- 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;
135
261
  }
136
262
  var MENTION_RE = /(?<=^|\s)@([A-Za-z]:[\\/][A-Za-z0-9._\-/\\]*|[A-Za-z0-9._\-/\\]+)/g;
137
263
  function parseMentions(text) {
@@ -165,6 +291,51 @@ function isBinaryContent(buf) {
165
291
  const probe = buf.subarray(0, 8e3);
166
292
  return probe.includes(0);
167
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
+ }
168
339
  async function resolveMentions(text, cwd) {
169
340
  const { cleanText, mentions } = parseMentions(text);
170
341
  if (mentions.length === 0) {
@@ -174,8 +345,9 @@ async function resolveMentions(text, cwd) {
174
345
  const attached = [];
175
346
  const skipped = [];
176
347
  const images = [];
348
+ let ignore;
177
349
  for (const mention of mentions) {
178
- const abs = path.resolve(cwd, mention);
350
+ const abs = path2.resolve(cwd, mention);
179
351
  if (isSensitivePath(abs)) {
180
352
  skipped.push({ path: mention, reason: "sensitive file" });
181
353
  continue;
@@ -191,13 +363,18 @@ async function resolveMentions(text, cwd) {
191
363
  skipped.push({ path: mention, reason: "image too large" });
192
364
  continue;
193
365
  }
194
- const buf2 = await readFile(abs).catch(() => null);
366
+ const buf2 = await readFile2(abs).catch(() => null);
195
367
  if (!buf2) {
196
368
  skipped.push({ path: mention, reason: "unreadable" });
197
369
  continue;
198
370
  }
371
+ const result = await downsampleImageIfNeeded({
372
+ path: mention,
373
+ mimeType,
374
+ data: buf2.toString("base64")
375
+ });
199
376
  attached.push(mention);
200
- images.push({ path: mention, mimeType, data: buf2.toString("base64") });
377
+ images.push(result.image);
201
378
  continue;
202
379
  }
203
380
  const st = await stat(abs).catch(() => null);
@@ -206,14 +383,23 @@ async function resolveMentions(text, cwd) {
206
383
  continue;
207
384
  }
208
385
  if (st.isDirectory()) {
209
- 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);
210
396
  continue;
211
397
  }
212
398
  if (st.size > MAX_MENTION_BYTES) {
213
399
  skipped.push({ path: mention, reason: `exceeds ${MAX_MENTION_BYTES / 1024}KB limit` });
214
400
  continue;
215
401
  }
216
- const buf = await readFile(abs).catch(() => null);
402
+ const buf = await readFile2(abs).catch(() => null);
217
403
  if (!buf) {
218
404
  skipped.push({ path: mention, reason: "unreadable" });
219
405
  continue;
@@ -275,12 +461,12 @@ var UsageTracker = class {
275
461
  };
276
462
 
277
463
  // src/cli/repl.tsx
278
- import { Box as Box11, Text as Text12, render, useApp, useInput as useInput6 } from "ink";
279
- import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef3, useState as useState3 } from "react";
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";
280
466
 
281
467
  // src/config/save.ts
282
468
  import fs from "fs";
283
- import path2 from "path";
469
+ import path3 from "path";
284
470
  import { parse, stringify } from "smol-toml";
285
471
  async function addAllowRule(rule) {
286
472
  return addPermissionRule("allow", rule);
@@ -299,7 +485,7 @@ async function addPermissionRule(kind, rule) {
299
485
  if (rules.includes(rule)) return false;
300
486
  permissions[kind] = [...rules, rule];
301
487
  raw.permissions = permissions;
302
- await fs.promises.mkdir(path2.dirname(filePath), { recursive: true });
488
+ await fs.promises.mkdir(path3.dirname(filePath), { recursive: true });
303
489
  await fs.promises.writeFile(filePath, stringify(raw), "utf8");
304
490
  return true;
305
491
  }
@@ -313,7 +499,7 @@ async function savePermissionMode(mode) {
313
499
  if (error.code !== "ENOENT") throw error;
314
500
  }
315
501
  raw.permissionMode = mode;
316
- await fs.promises.mkdir(path2.dirname(filePath), { recursive: true });
502
+ await fs.promises.mkdir(path3.dirname(filePath), { recursive: true });
317
503
  await fs.promises.writeFile(filePath, stringify(raw), "utf8");
318
504
  }
319
505
 
@@ -380,10 +566,13 @@ var ConfigSchema = z.object({
380
566
  // Retries after a timeout/empty failure scale both stream timeouts up
381
567
  // (attempt number ×, capped at 3x) since the relay is likely overloaded.
382
568
  streamMaxRetries: z.number().int().min(0).default(3),
383
- // How many times a turn may be nudged to continue when the model ends it
384
- // with a text-only reply that announces pending work ("我将…", "I will…")
385
- // instead of tool calls — a common premature-stop failure of weaker
386
- // agentic models. Never fires in plan mode. 0 disables.
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.
387
576
  maxAutoContinues: z.number().int().min(0).default(2),
388
577
  contextCompaction: z.enum(["summary", "truncate"]).default("summary"),
389
578
  // Optional per-session dollar budget; when set, the REPL stops the session
@@ -465,15 +654,15 @@ function createModel(config, modelName) {
465
654
 
466
655
  // src/session/checkpoints.ts
467
656
  import fs2 from "fs/promises";
468
- import path3 from "path";
657
+ import path4 from "path";
469
658
  function checkpointsDir(sessionDir) {
470
- return path3.join(sessionDir, "checkpoints");
659
+ return path4.join(sessionDir, "checkpoints");
471
660
  }
472
661
  function indexPath(sessionDir) {
473
- return path3.join(checkpointsDir(sessionDir), "index.json");
662
+ return path4.join(checkpointsDir(sessionDir), "index.json");
474
663
  }
475
664
  function checkpointContentPath(sessionDir, id) {
476
- return path3.join(checkpointsDir(sessionDir), `${id}.snapshot`);
665
+ return path4.join(checkpointsDir(sessionDir), `${id}.snapshot`);
477
666
  }
478
667
  async function listCheckpointRecords(sessionDir) {
479
668
  let raw;
@@ -521,11 +710,11 @@ async function loadSessionSnapshots(sessionDir) {
521
710
 
522
711
  // src/session/clear.ts
523
712
  import fs4 from "fs/promises";
524
- import path5 from "path";
713
+ import path6 from "path";
525
714
 
526
715
  // src/session/store.ts
527
716
  import fs3 from "fs/promises";
528
- import path4 from "path";
717
+ import path5 from "path";
529
718
  function generateId(now) {
530
719
  const pad = (n) => String(n).padStart(2, "0");
531
720
  const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
@@ -547,13 +736,13 @@ var SessionStore = class _SessionStore {
547
736
  initialized = false;
548
737
  constructor(id) {
549
738
  this.id = id;
550
- this.dir = path4.join(sessionsDir(), id);
739
+ this.dir = path5.join(sessionsDir(), id);
551
740
  }
552
741
  metaPath() {
553
- return path4.join(this.dir, "meta.json");
742
+ return path5.join(this.dir, "meta.json");
554
743
  }
555
744
  messagesPath() {
556
- return path4.join(this.dir, "messages.jsonl");
745
+ return path5.join(this.dir, "messages.jsonl");
557
746
  }
558
747
  static async create(cwd, model) {
559
748
  const store = new _SessionStore(generateId(/* @__PURE__ */ new Date()));
@@ -587,7 +776,7 @@ var SessionStore = class _SessionStore {
587
776
  const metas = [];
588
777
  for (const entry of entries) {
589
778
  try {
590
- 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");
591
780
  const meta = JSON.parse(raw);
592
781
  if (meta.id === entry && (cwd === void 0 || meta.cwd === cwd)) metas.push(meta);
593
782
  } catch {
@@ -714,7 +903,7 @@ async function clearSessions(cwd, excludeId) {
714
903
  let removed2 = 0;
715
904
  for (const meta of metas) {
716
905
  if (meta.id === excludeId) continue;
717
- 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 });
718
907
  removed2 += 1;
719
908
  }
720
909
  return removed2;
@@ -728,7 +917,7 @@ async function clearSessions(cwd, excludeId) {
728
917
  let removed = 0;
729
918
  for (const entry of entries) {
730
919
  if (entry === excludeId) continue;
731
- await fs4.rm(path5.join(sessionsDir(), entry), { recursive: true, force: true });
920
+ await fs4.rm(path6.join(sessionsDir(), entry), { recursive: true, force: true });
732
921
  removed += 1;
733
922
  }
734
923
  return removed;
@@ -736,11 +925,11 @@ async function clearSessions(cwd, excludeId) {
736
925
 
737
926
  // src/session/list.ts
738
927
  import fs5 from "fs/promises";
739
- import path6 from "path";
928
+ import path7 from "path";
740
929
  async function countMessages(id) {
741
930
  let raw;
742
931
  try {
743
- raw = await fs5.readFile(path6.join(sessionsDir(), id, "messages.jsonl"), "utf8");
932
+ raw = await fs5.readFile(path7.join(sessionsDir(), id, "messages.jsonl"), "utf8");
744
933
  } catch {
745
934
  return 0;
746
935
  }
@@ -998,11 +1187,11 @@ function formatUsageDashboard(stats, models, options = {}) {
998
1187
 
999
1188
  // src/version.ts
1000
1189
  import fs6 from "fs";
1001
- import path7 from "path";
1190
+ import path8 from "path";
1002
1191
  import { fileURLToPath } from "url";
1003
1192
  function readPackageVersion() {
1004
1193
  try {
1005
- 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");
1006
1195
  const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf8"));
1007
1196
  if (typeof pkg === "object" && pkg !== null) {
1008
1197
  const version = pkg.version;
@@ -1023,16 +1212,30 @@ function budgetState(costUsd, budgetUsd) {
1023
1212
  return "ok";
1024
1213
  }
1025
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
+
1026
1229
  // src/cli/clipboard.ts
1027
- import { execFile } from "child_process";
1028
- import { randomUUID } from "crypto";
1029
- import { readFile as readFile2, unlink } from "fs/promises";
1030
- import os from "os";
1031
- import path8 from "path";
1032
- 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";
1033
1236
  var CLIPBOARD_TIMEOUT_MS = 5e3;
1034
1237
  var MAX_CLIPBOARD_IMAGE_BYTES = 10 * 1024 * 1024;
1035
- var execFileAsync = promisify(execFile);
1238
+ var execFileAsync2 = promisify2(execFile2);
1036
1239
  function windowsClipboardImageScript(tempFile) {
1037
1240
  const escaped = tempFile.replace(/'/g, "''");
1038
1241
  return [
@@ -1092,40 +1295,52 @@ function clipboardCopyCommand(platform) {
1092
1295
  if (platform === "linux") return { command: "xclip", args: ["-selection", "clipboard"] };
1093
1296
  return null;
1094
1297
  }
1095
- async function commandExists(command) {
1298
+ async function commandExists2(command) {
1096
1299
  const checker = process.platform === "win32" ? "where" : "which";
1097
1300
  try {
1098
- await execFileAsync(checker, [command], { timeout: CLIPBOARD_TIMEOUT_MS });
1301
+ await execFileAsync2(checker, [command], { timeout: CLIPBOARD_TIMEOUT_MS });
1099
1302
  return true;
1100
1303
  } catch {
1101
1304
  return false;
1102
1305
  }
1103
1306
  }
1307
+ async function finalizeClipboardImage(image) {
1308
+ const result = await downsampleImageIfNeeded(image);
1309
+ return { image: result.image, oversized: result.oversized && !result.resized };
1310
+ }
1104
1311
  async function readClipboardImage() {
1105
- const tempFile = path8.join(os.tmpdir(), `star-clipboard-${randomUUID()}.png`);
1106
- 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;
1107
1314
  const plan = clipboardImagePlan(process.platform, tempFile, hasPngpaste);
1108
1315
  if (!plan) return null;
1109
1316
  try {
1110
1317
  if (plan.kind === "stdout") {
1111
- const { stdout } = await execFileAsync(plan.command, plan.args, {
1318
+ const { stdout } = await execFileAsync2(plan.command, plan.args, {
1112
1319
  timeout: CLIPBOARD_TIMEOUT_MS,
1113
1320
  maxBuffer: MAX_CLIPBOARD_IMAGE_BYTES,
1114
1321
  encoding: "buffer"
1115
1322
  });
1116
1323
  const buf2 = stdout;
1117
1324
  if (buf2.length === 0) return null;
1118
- 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
+ });
1119
1330
  }
1120
- await execFileAsync(plan.command, plan.args, { timeout: CLIPBOARD_TIMEOUT_MS });
1121
- 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);
1122
1333
  if (!buf || buf.length === 0) return null;
1123
- 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
+ });
1124
1339
  } catch {
1125
1340
  return null;
1126
1341
  } finally {
1127
1342
  if (plan.kind === "temp-file") {
1128
- await unlink(tempFile).catch(() => {
1343
+ await unlink2(tempFile).catch(() => {
1129
1344
  });
1130
1345
  }
1131
1346
  }
@@ -1134,7 +1349,7 @@ async function copyText(text) {
1134
1349
  const plan = clipboardCopyCommand(process.platform);
1135
1350
  if (!plan) return false;
1136
1351
  return new Promise((resolve) => {
1137
- const child = execFile(
1352
+ const child = execFile2(
1138
1353
  plan.command,
1139
1354
  plan.args,
1140
1355
  { timeout: CLIPBOARD_TIMEOUT_MS },
@@ -1148,7 +1363,7 @@ async function copyText(text) {
1148
1363
 
1149
1364
  // src/cli/commands/actions.ts
1150
1365
  import fs7 from "fs/promises";
1151
- import path9 from "path";
1366
+ import path10 from "path";
1152
1367
  var MIN_COMPACT_MESSAGES = 4;
1153
1368
  async function compactSession(opts) {
1154
1369
  const { backend, sessionStore, config, model } = opts;
@@ -1209,9 +1424,9 @@ async function exportSession(opts) {
1209
1424
  return "Nothing to export: the session has no messages.";
1210
1425
  }
1211
1426
  const sessionId = sessionStore?.id ?? `unknown-${Date.now()}`;
1212
- 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`);
1213
1428
  const existed = await fileExists(target);
1214
- await fs7.mkdir(path9.dirname(target), { recursive: true });
1429
+ await fs7.mkdir(path10.dirname(target), { recursive: true });
1215
1430
  await fs7.writeFile(target, renderSessionMarkdown(sessionId, messages));
1216
1431
  return existed ? `Exported ${messages.length} messages to ${target} (overwrote existing file).` : `Exported ${messages.length} messages to ${target}.`;
1217
1432
  }
@@ -1333,8 +1548,8 @@ function formatSearchResults(hits, query) {
1333
1548
  }
1334
1549
 
1335
1550
  // src/cli/diff-preview.ts
1336
- import { readFile as readFile3 } from "fs/promises";
1337
- import path10 from "path";
1551
+ import { readFile as readFile4 } from "fs/promises";
1552
+ import path11 from "path";
1338
1553
  var CONTEXT_LINES = 2;
1339
1554
  var MAX_HUNK_LINES = 15;
1340
1555
  var MAX_TOTAL_LINES = 12;
@@ -1495,10 +1710,10 @@ async function generateDiffPreview(toolName, args, cwd) {
1495
1710
  if (toolName !== "edit_file" && toolName !== "write_file") return null;
1496
1711
  const filePath = getStringArg(args, "path");
1497
1712
  if (filePath === void 0) return null;
1498
- const absolute = path10.resolve(cwd, filePath);
1713
+ const absolute = path11.resolve(cwd, filePath);
1499
1714
  let existing;
1500
1715
  try {
1501
- existing = await readFile3(absolute, "utf8");
1716
+ existing = await readFile4(absolute, "utf8");
1502
1717
  } catch {
1503
1718
  existing = null;
1504
1719
  }
@@ -1795,6 +2010,19 @@ ${content}`);
1795
2010
  ctx.addSystemMessage(await ctx.rewind(args.trim()));
1796
2011
  }
1797
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
+ });
1798
2026
  register({
1799
2027
  name: "init",
1800
2028
  description: "Generate an AGENTS.md for the current project",
@@ -1922,9 +2150,260 @@ ${result.diff}${note ? `
1922
2150
  });
1923
2151
  }
1924
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
+
1925
2404
  // src/cli/commands/custom.ts
1926
2405
  import { readFileSync as readFileSync2, readdirSync } from "fs";
1927
- import path11 from "path";
2406
+ import path13 from "path";
1928
2407
  var NAME_PATTERN = /^[a-z0-9-]+$/;
1929
2408
  var DESCRIPTION_PATTERN = /^<!--\s*description:\s*(.+?)\s*-->\s*$/;
1930
2409
  var DEFAULT_DESCRIPTION = "Custom prompt command";
@@ -1959,14 +2438,14 @@ function scanDir(dir, into) {
1959
2438
  if (!entry.endsWith(".md")) continue;
1960
2439
  const name = entry.slice(0, -".md".length);
1961
2440
  if (!NAME_PATTERN.test(name)) continue;
1962
- const parsed = parseCommandFile(path11.join(dir, entry), name);
2441
+ const parsed = parseCommandFile(path13.join(dir, entry), name);
1963
2442
  if (parsed) into.set(name, parsed);
1964
2443
  }
1965
2444
  }
1966
2445
  function loadCustomCommands(cwd, home = starHome()) {
1967
2446
  const byName = /* @__PURE__ */ new Map();
1968
- scanDir(path11.join(home, "commands"), byName);
1969
- scanDir(path11.join(cwd, ".star", "commands"), byName);
2447
+ scanDir(path13.join(home, "commands"), byName);
2448
+ scanDir(path13.join(cwd, ".star", "commands"), byName);
1970
2449
  return [...byName.values()];
1971
2450
  }
1972
2451
  function registerCustomCommands(registry, cwd, home) {
@@ -1992,8 +2471,8 @@ function registerCustomCommands(registry, cwd, home) {
1992
2471
  }
1993
2472
 
1994
2473
  // src/cli/commands/doctor.ts
1995
- import fs8 from "fs/promises";
1996
- import path12 from "path";
2474
+ import fs10 from "fs/promises";
2475
+ import path14 from "path";
1997
2476
  var MIN_NODE_MAJOR = 20;
1998
2477
  var REGISTRY_URL = "https://registry.npmjs.org/";
1999
2478
  var REGISTRY_TIMEOUT_MS = 5e3;
@@ -2007,7 +2486,7 @@ function nodeMajor(version) {
2007
2486
  }
2008
2487
  async function pathExists(target) {
2009
2488
  try {
2010
- await fs8.access(target);
2489
+ await fs10.access(target);
2011
2490
  return true;
2012
2491
  } catch {
2013
2492
  return false;
@@ -2045,7 +2524,7 @@ async function runDoctor(opts) {
2045
2524
  detail: `${spec.label} (${spec.shell})`
2046
2525
  });
2047
2526
  }
2048
- const configPath = path12.join(home, "config.toml");
2527
+ const configPath = path14.join(home, "config.toml");
2049
2528
  const configExists = await pathExists(configPath);
2050
2529
  checks.push({
2051
2530
  name: "STAR_HOME / config",
@@ -2082,14 +2561,14 @@ async function runDoctor(opts) {
2082
2561
  });
2083
2562
  }
2084
2563
  }
2085
- const sessions = path12.join(home, "sessions");
2564
+ const sessions = path14.join(home, "sessions");
2086
2565
  let writable = false;
2087
2566
  let writeError = "";
2088
2567
  try {
2089
- await fs8.mkdir(sessions, { recursive: true });
2090
- const probe = path12.join(sessions, `.doctor-probe-${process.pid}`);
2091
- await fs8.writeFile(probe, "ok");
2092
- 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);
2093
2572
  writable = true;
2094
2573
  } catch (error) {
2095
2574
  writeError = error instanceof Error ? error.message : String(error);
@@ -2124,15 +2603,15 @@ function formatDoctorReport(report) {
2124
2603
  }
2125
2604
 
2126
2605
  // src/cli/commands/init-project.ts
2127
- import fs9 from "fs/promises";
2128
- import path13 from "path";
2606
+ import fs11 from "fs/promises";
2607
+ import path15 from "path";
2129
2608
  import { generateText } from "ai";
2130
2609
  var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", "coverage"]);
2131
2610
  var MAX_ENTRIES = 20;
2132
2611
  var README_HEAD_LINES = 5;
2133
2612
  async function fileExists2(target) {
2134
2613
  try {
2135
- await fs9.access(target);
2614
+ await fs11.access(target);
2136
2615
  return true;
2137
2616
  } catch {
2138
2617
  return false;
@@ -2140,7 +2619,7 @@ async function fileExists2(target) {
2140
2619
  }
2141
2620
  async function readPackageJson(cwd) {
2142
2621
  try {
2143
- const raw = await fs9.readFile(path13.join(cwd, "package.json"), "utf8");
2622
+ const raw = await fs11.readFile(path15.join(cwd, "package.json"), "utf8");
2144
2623
  const parsed = JSON.parse(raw);
2145
2624
  if (typeof parsed !== "object" || parsed === null) return null;
2146
2625
  return parsed;
@@ -2157,10 +2636,10 @@ function stringRecord(value) {
2157
2636
  return out;
2158
2637
  }
2159
2638
  async function detectPackageManager(cwd) {
2160
- if (await fileExists2(path13.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2161
- if (await fileExists2(path13.join(cwd, "yarn.lock"))) return "yarn";
2162
- if (await fileExists2(path13.join(cwd, "package-lock.json"))) return "npm";
2163
- 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";
2164
2643
  return null;
2165
2644
  }
2166
2645
  function inferCommands(pm, scripts) {
@@ -2175,7 +2654,7 @@ function inferCommands(pm, scripts) {
2175
2654
  }
2176
2655
  async function listEntries(cwd) {
2177
2656
  try {
2178
- const dirents = await fs9.readdir(cwd, { withFileTypes: true });
2657
+ const dirents = await fs11.readdir(cwd, { withFileTypes: true });
2179
2658
  const entries = [];
2180
2659
  for (const dirent of dirents) {
2181
2660
  if (dirent.isDirectory()) {
@@ -2194,7 +2673,7 @@ async function listEntries(cwd) {
2194
2673
  async function readReadmeHead(cwd) {
2195
2674
  for (const name of ["README.md", "readme.md", "README"]) {
2196
2675
  try {
2197
- const raw = await fs9.readFile(path13.join(cwd, name), "utf8");
2676
+ const raw = await fs11.readFile(path15.join(cwd, name), "utf8");
2198
2677
  return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).slice(0, README_HEAD_LINES);
2199
2678
  } catch {
2200
2679
  }
@@ -2207,14 +2686,14 @@ async function scanProject(cwd) {
2207
2686
  const scripts = stringRecord(pkg?.scripts);
2208
2687
  return {
2209
2688
  name: typeof pkg?.name === "string" ? pkg.name : null,
2210
- dirName: path13.basename(cwd),
2689
+ dirName: path15.basename(cwd),
2211
2690
  packageManager,
2212
2691
  scripts,
2213
2692
  dependencies: Object.keys(stringRecord(pkg?.dependencies)),
2214
2693
  devDependencies: Object.keys(stringRecord(pkg?.devDependencies)),
2215
2694
  commands: inferCommands(packageManager, scripts),
2216
- hasTsconfig: await fileExists2(path13.join(cwd, "tsconfig.json")),
2217
- isGitRepo: await fileExists2(path13.join(cwd, ".git")),
2695
+ hasTsconfig: await fileExists2(path15.join(cwd, "tsconfig.json")),
2696
+ isGitRepo: await fileExists2(path15.join(cwd, ".git")),
2218
2697
  readmeHead: await readReadmeHead(cwd),
2219
2698
  entries: await listEntries(cwd)
2220
2699
  };
@@ -2281,7 +2760,7 @@ Write the AGENTS.md.`
2281
2760
  }
2282
2761
  async function initProject(opts) {
2283
2762
  const { cwd, args, model } = opts;
2284
- const target = path13.join(cwd, "AGENTS.md");
2763
+ const target = path15.join(cwd, "AGENTS.md");
2285
2764
  const force = args.split(/\s+/).includes("force");
2286
2765
  if (!force && await fileExists2(target)) {
2287
2766
  return {
@@ -2315,7 +2794,7 @@ async function initProject(opts) {
2315
2794
  content = renderAgentsMd(facts);
2316
2795
  note = " No model available; used the template \u2014 review and refine manually.";
2317
2796
  }
2318
- await fs9.writeFile(target, content);
2797
+ await fs11.writeFile(target, content);
2319
2798
  const lines = content.split("\n").length;
2320
2799
  const how = usedLlm ? "generated with model refinement" : "generated from template";
2321
2800
  return {
@@ -2357,7 +2836,7 @@ function parseSlashCommand(input) {
2357
2836
  }
2358
2837
 
2359
2838
  // src/cli/commands/rewind.ts
2360
- import path14 from "path";
2839
+ import path16 from "path";
2361
2840
  function planRewind(snapshots, id) {
2362
2841
  const target = snapshots.find((snapshot) => snapshot.id === id);
2363
2842
  if (!target) return null;
@@ -2371,7 +2850,7 @@ function planRewind(snapshots, id) {
2371
2850
  return { target, affected, messageIndex };
2372
2851
  }
2373
2852
  function displayPath(filePath, cwd) {
2374
- const relative = path14.relative(cwd, filePath);
2853
+ const relative = path16.relative(cwd, filePath);
2375
2854
  return relative !== "" && !relative.startsWith("..") ? relative : filePath;
2376
2855
  }
2377
2856
  function formatTime(timestamp) {
@@ -2421,29 +2900,110 @@ function didYouMeanSuffix(names) {
2421
2900
  return ` Did you mean: ${listed.join(", ")}?`;
2422
2901
  }
2423
2902
 
2424
- // 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
2425
2930
  import { Box, Text, useInput } from "ink";
2931
+ import { useRef, useState } from "react";
2426
2932
  import { jsx, jsxs } from "react/jsx-runtime";
2427
- 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
+ };
2428
2941
  useInput((input, key) => {
2429
- const ch = input.toLowerCase();
2430
- if (ch === "y") onDecision("yes");
2431
- 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
+ }
2432
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;
2433
2962
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
2434
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "Plan ready (plan mode)" }),
2435
- /* @__PURE__ */ jsx(Text, { children: "The plan above was produced without making any changes." }),
2436
- /* @__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" })
2437
2993
  ] });
2438
2994
  }
2439
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
+
2440
3000
  // src/cli/components/InputBox.tsx
2441
- import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
2442
- 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";
2443
3003
 
2444
3004
  // src/cli/path-suggest.ts
2445
- import { readdir } from "fs/promises";
2446
- import path15 from "path";
3005
+ import { readdir as readdir2 } from "fs/promises";
3006
+ import path18 from "path";
2447
3007
  var MAX_SCAN_ENTRIES = 2e3;
2448
3008
  function extractAtToken(value, cursor) {
2449
3009
  const before = value.slice(0, cursor);
@@ -2458,8 +3018,8 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
2458
3018
  const dirPart = slash >= 0 ? normalized.slice(0, slash + 1) : "";
2459
3019
  const basePrefix = slash >= 0 ? normalized.slice(slash + 1) : normalized;
2460
3020
  if (dirPart.split("/").some((segment) => SKIP_DIRS.has(segment))) return [];
2461
- const dirAbs = path15.resolve(cwd, dirPart === "" ? "." : dirPart);
2462
- 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);
2463
3023
  if (!entries) return [];
2464
3024
  const ignore = createIgnorePredicate(cwd);
2465
3025
  const lower = basePrefix.toLowerCase();
@@ -2473,7 +3033,7 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
2473
3033
  if (isDir && SKIP_DIRS.has(entry.name)) continue;
2474
3034
  if (entry.name.startsWith(".") && !basePrefix.startsWith(".")) continue;
2475
3035
  if (!entry.name.toLowerCase().startsWith(lower)) continue;
2476
- if (ignore?.(path15.join(dirAbs, entry.name), isDir)) continue;
3036
+ if (ignore?.(path18.join(dirAbs, entry.name), isDir)) continue;
2477
3037
  matches.push({ path: `${dirPart}${entry.name}${isDir ? "/" : ""}`, isDir });
2478
3038
  }
2479
3039
  matches.sort((a, b) => a.path.localeCompare(b.path));
@@ -2482,40 +3042,227 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
2482
3042
 
2483
3043
  // src/cli/components/InputBox.tsx
2484
3044
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2485
- function InputBox({
2486
- isStreaming,
2487
- disabled,
2488
- commands,
2489
- cwd,
2490
- refill,
2491
- initialHistory,
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,
2492
3141
  onSubmit,
2493
3142
  onInterrupt,
2494
3143
  onExit,
2495
3144
  onPasteImage
2496
3145
  }) {
2497
- const [value, setValue] = useState("");
2498
- const [cursor, setCursor] = useState(0);
2499
- const [history, setHistory] = useState(() => initialHistory ?? []);
2500
- const [highlight, setHighlight] = useState(0);
2501
- const [suggestionsDismissed, setSuggestionsDismissed] = useState(false);
2502
- const [pathSuggestions, setPathSuggestions] = useState([]);
2503
- const historyIndexRef = useRef(null);
2504
- const draftRef = useRef("");
2505
- const refillSeqRef = useRef(0);
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
+ };
2506
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;
2507
3193
  setValue(next);
2508
- setCursor(Math.max(0, Math.min(nextCursor, next.length)));
3194
+ setCursor(clamped);
2509
3195
  setHighlight(0);
2510
3196
  setSuggestionsDismissed(false);
2511
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
+ };
2512
3259
  useEffect(() => {
2513
3260
  if (refill && refill.seq !== refillSeqRef.current) {
2514
3261
  refillSeqRef.current = refill.seq;
2515
3262
  edit(refill.text, refill.text.length);
2516
3263
  }
2517
3264
  });
2518
- const suggestionsEnabled = !isStreaming && !disabled && !suggestionsDismissed;
3265
+ const suggestionsEnabled = !isStreaming && !disabled && !suggestionsDismissed && !search;
2519
3266
  const suggestions = suggestionsEnabled && commands ? filterCommands(value, commands) : [];
2520
3267
  useEffect(() => {
2521
3268
  if (!suggestionsEnabled || !cwd) {
@@ -2556,11 +3303,87 @@ function InputBox({
2556
3303
  );
2557
3304
  };
2558
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
+ }
2559
3381
  if (key.ctrl && input === "c") {
2560
3382
  if (isStreaming || disabled) {
2561
3383
  onInterrupt();
2562
3384
  } else {
2563
3385
  edit("", 0);
3386
+ clearPastes();
2564
3387
  historyIndexRef.current = null;
2565
3388
  }
2566
3389
  return;
@@ -2579,27 +3402,28 @@ function InputBox({
2579
3402
  return;
2580
3403
  }
2581
3404
  if (key.return) {
2582
- if (key.shift) {
2583
- edit(`${value.slice(0, cursor)}
2584
- ${value.slice(cursor)}`, cursor + 1);
2585
- return;
2586
- }
2587
- if (value[cursor - 1] === "\\") {
2588
- edit(`${value.slice(0, cursor - 1)}
2589
- ${value.slice(cursor)}`, cursor);
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
+ );
2590
3413
  return;
2591
3414
  }
2592
- const text = value.trim();
3415
+ const text = expandPastes(currentValue).trim();
2593
3416
  if (text.length > 0) {
2594
- setHistory((prev) => [...prev, text]);
3417
+ if (!text.startsWith("/")) setHistory((prev) => [...prev, text]);
2595
3418
  onSubmit(text);
2596
3419
  }
2597
3420
  edit("", 0);
3421
+ clearPastes();
2598
3422
  historyIndexRef.current = null;
2599
3423
  draftRef.current = "";
2600
3424
  return;
2601
3425
  }
2602
- if (input === "\n" || input === "\r" || /^\[13;\d+u$/.test(input)) {
3426
+ if (input === "\n" || input === "\r") {
2603
3427
  edit(`${value.slice(0, cursor)}
2604
3428
  ${value.slice(cursor)}`, cursor + 1);
2605
3429
  return;
@@ -2611,34 +3435,47 @@ ${value.slice(cursor)}`, cursor + 1);
2611
3435
  return;
2612
3436
  }
2613
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
+ }
2614
3446
  if (suggestionCount > 0) {
2615
3447
  setHighlight((prev) => (prev - 1 + suggestionCount) % suggestionCount);
2616
3448
  return;
2617
3449
  }
2618
- if (history.length === 0) return;
2619
- if (historyIndexRef.current === null) {
2620
- draftRef.current = value;
2621
- historyIndexRef.current = history.length - 1;
2622
- } else if (historyIndexRef.current > 0) {
2623
- historyIndexRef.current -= 1;
3450
+ const moved = moveCursorLines(value, cursor, -1);
3451
+ if (moved !== null) {
3452
+ setCursor(moved);
3453
+ return;
2624
3454
  }
3455
+ if (history.length === 0) return;
3456
+ draftRef.current = value;
3457
+ historyIndexRef.current = history.length - 1;
2625
3458
  const entry = history[historyIndexRef.current] ?? "";
2626
3459
  edit(entry, entry.length);
2627
3460
  return;
2628
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
+ }
2629
3473
  if (suggestionCount > 0) {
2630
3474
  setHighlight((prev) => (prev + 1) % suggestionCount);
2631
3475
  return;
2632
3476
  }
2633
- if (historyIndexRef.current === null) return;
2634
- if (historyIndexRef.current < history.length - 1) {
2635
- historyIndexRef.current += 1;
2636
- const entry = history[historyIndexRef.current] ?? "";
2637
- edit(entry, entry.length);
2638
- } else {
2639
- historyIndexRef.current = null;
2640
- edit(draftRef.current, draftRef.current.length);
2641
- }
3477
+ const moved = moveCursorLines(value, cursor, 1);
3478
+ if (moved !== null) setCursor(moved);
2642
3479
  return;
2643
3480
  } else if (key.leftArrow) {
2644
3481
  setCursor((prev) => Math.max(0, prev - 1));
@@ -2676,20 +3513,29 @@ ${value.slice(cursor)}`, cursor + 1);
2676
3513
  }
2677
3514
  if (key.backspace || key.delete) {
2678
3515
  if (cursor === 0) return;
2679
- 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);
2680
3527
  return;
2681
3528
  }
2682
3529
  if (input && !key.ctrl && !key.meta) {
2683
- const text = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
2684
- edit(value.slice(0, cursor) + text + value.slice(cursor), cursor + text.length);
3530
+ insertPlainText(input);
2685
3531
  }
2686
3532
  });
2687
3533
  const before = value.slice(0, cursor);
2688
3534
  const at = value[cursor];
2689
3535
  const after = value.slice(cursor + 1);
2690
- const cursorChar = at === "\n" || at === void 0 ? " " : at;
2691
- const tail = (at === "\n" ? "\n" : "") + after;
2692
- const styled = `\x1B[36m> \x1B[39m${before}\x1B[7m${cursorChar}\x1B[27m${tail}`;
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}`;
2693
3539
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
2694
3540
  /* @__PURE__ */ jsx2(Box2, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { children: styled }) }),
2695
3541
  suggestions.length > 0 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: suggestions.map((cmd, index) => {
@@ -2708,175 +3554,422 @@ ${value.slice(cursor)}`, cursor + 1);
2708
3554
  ] });
2709
3555
  }
2710
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
+
2711
3850
  // src/cli/components/MessageList.tsx
2712
- import { Box as Box3, Static, Text as Text4 } from "ink";
3851
+ import { Box as Box6, Static, Text as Text7 } from "ink";
2713
3852
  import { memo } from "react";
2714
3853
 
2715
3854
  // src/cli/components/DiffLines.tsx
2716
- import { Text as Text3 } from "ink";
2717
- 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";
2718
3857
  function DiffLineView({ line }) {
2719
- if (line.kind === "add") return /* @__PURE__ */ jsxs3(Text3, { color: "green", children: [
3858
+ if (line.kind === "add") return /* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
2720
3859
  "+ ",
2721
3860
  line.text
2722
3861
  ] });
2723
- if (line.kind === "del") return /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
3862
+ if (line.kind === "del") return /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
2724
3863
  "- ",
2725
3864
  line.text
2726
3865
  ] });
2727
- if (line.kind === "marker") return /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: line.text });
2728
- 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: [
2729
3868
  " ",
2730
3869
  line.text
2731
3870
  ] });
2732
3871
  }
2733
3872
  function DiffLines({ lines }) {
2734
- return /* @__PURE__ */ jsx3(Fragment, { children: lines.map((line, index) => (
3873
+ return /* @__PURE__ */ jsx6(Fragment, { children: lines.map((line, index) => (
2735
3874
  // biome-ignore lint/suspicious/noArrayIndexKey: diff lines have no stable identity
2736
- /* @__PURE__ */ jsx3(DiffLineView, { line }, index)
3875
+ /* @__PURE__ */ jsx6(DiffLineView, { line }, index)
2737
3876
  )) });
2738
3877
  }
2739
3878
 
2740
3879
  // src/cli/components/MessageList.tsx
2741
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
3880
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2742
3881
  var roleStyles = {
2743
3882
  user: { label: "you", color: "cyan" },
2744
3883
  assistant: { label: "star", color: "green" },
2745
3884
  system: { label: "system", color: "yellow" },
2746
3885
  tool: { label: "tool", color: "magenta" }
2747
3886
  };
3887
+ var interruptedMarker = (text) => /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: text.length > 0 ? " [interrupted]" : "[interrupted]" });
2748
3888
  var MessageList = memo(function MessageList2({ messages }) {
2749
- return /* @__PURE__ */ jsx4(Static, { items: messages, children: (message) => {
3889
+ return /* @__PURE__ */ jsx7(Static, { items: messages, children: (message) => {
2750
3890
  const marginBottom = message.tight ? 0 : 1;
2751
3891
  if (message.role === "assistant-cont") {
2752
- 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);
2753
3896
  }
2754
3897
  const style = roleStyles[message.role];
2755
- return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", marginBottom, children: [
2756
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: style.color, children: style.label }),
2757
- /* @__PURE__ */ jsx4(Text4, { color: style.color, children: message.text }),
2758
- message.diff && /* @__PURE__ */ jsx4(DiffLines, { lines: message.diff }),
2759
- 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 })
2760
3906
  ] }, message.id);
2761
3907
  } });
2762
3908
  });
2763
3909
 
2764
3910
  // src/cli/components/PermissionPrompt.tsx
2765
- import { Box as Box4, Text as Text5, useInput as useInput3 } from "ink";
2766
- 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";
2767
3913
  function PermissionPrompt({ request, preview, onDecision }) {
2768
- useInput3((input) => {
3914
+ useInput6((input) => {
2769
3915
  const ch = input.toLowerCase();
2770
3916
  if (ch === "y") onDecision("yes");
2771
3917
  else if (ch === "n") onDecision("no");
2772
3918
  else if (ch === "a") onDecision("always");
2773
3919
  });
2774
- return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
2775
- /* @__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: [
2776
3922
  "Permission required: ",
2777
3923
  request.toolName,
2778
3924
  " (",
2779
3925
  request.level,
2780
3926
  ")"
2781
3927
  ] }),
2782
- preview ? /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
2783
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
3928
+ preview ? /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
3929
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2784
3930
  preview.type === "new-file" ? "New file: " : "",
2785
3931
  preview.label
2786
3932
  ] }),
2787
- /* @__PURE__ */ jsx5(DiffLines, { lines: preview.lines })
2788
- ] }) : /* @__PURE__ */ jsx5(Text5, { children: summarizeArgs(request.args) }),
2789
- /* @__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)" })
2790
3936
  ] });
2791
3937
  }
2792
3938
 
2793
3939
  // src/cli/components/RewindConfirmPrompt.tsx
2794
- import { Box as Box5, Text as Text6, useInput as useInput4 } from "ink";
2795
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2796
- function RewindConfirmPrompt({ summary, onDecision }) {
2797
- 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) => {
2798
3950
  const ch = input.toLowerCase();
2799
3951
  if (ch === "y") onDecision("yes");
2800
3952
  else if (ch === "n" || key.escape) onDecision("no");
2801
3953
  });
2802
- return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
2803
- /* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: "Rewind checkpoints" }),
2804
- /* @__PURE__ */ jsx6(Text6, { children: summary }),
2805
- /* @__PURE__ */ jsx6(Text6, { children: "[y] rewind [n] cancel (Esc = n)" })
2806
- ] });
2807
- }
2808
-
2809
- // src/cli/components/SelectPrompt.tsx
2810
- import { Box as Box6, Text as Text7, useInput as useInput5 } from "ink";
2811
- import { useRef as useRef2, useState as useState2 } from "react";
2812
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2813
- var SELECT_PROMPT_MAX_VISIBLE = 9;
2814
- function SelectPrompt({ title, options, onSelect, onCancel }) {
2815
- const [index, setIndex] = useState2(0);
2816
- const indexRef = useRef2(0);
2817
- const move = (delta) => {
2818
- indexRef.current = Math.max(0, Math.min(indexRef.current + delta, options.length - 1));
2819
- setIndex(indexRef.current);
2820
- };
2821
- useInput5((input, key) => {
2822
- if (key.escape) {
2823
- onCancel();
2824
- return;
2825
- }
2826
- if (key.return) {
2827
- const option = options[indexRef.current];
2828
- if (option) onSelect(option.value);
2829
- return;
2830
- }
2831
- if (key.downArrow || input === "j") {
2832
- move(1);
2833
- } else if (key.upArrow || input === "k") {
2834
- move(-1);
2835
- }
2836
- });
2837
- const maxStart = Math.max(0, options.length - SELECT_PROMPT_MAX_VISIBLE);
2838
- const start = Math.min(Math.max(0, index - Math.floor(SELECT_PROMPT_MAX_VISIBLE / 2)), maxStart);
2839
- const visible = options.slice(start, start + SELECT_PROMPT_MAX_VISIBLE);
2840
- const above = start;
2841
- const below = options.length - start - visible.length;
2842
- return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
2843
- /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: title }),
2844
- above > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2845
- "\u2026 ",
2846
- above,
2847
- " more above"
2848
- ] }),
2849
- visible.map((option, i) => {
2850
- const active = start + i === index;
2851
- return /* @__PURE__ */ jsxs7(Text7, { children: [
2852
- /* @__PURE__ */ jsxs7(Text7, { bold: active, inverse: active, children: [
2853
- active ? "\u276F " : " ",
2854
- option.label
2855
- ] }),
2856
- option.hint && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2857
- " (",
2858
- option.hint,
2859
- ")"
2860
- ] }),
2861
- option.description && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2862
- " \u2014 ",
2863
- option.description
2864
- ] })
2865
- ] }, option.value);
2866
- }),
2867
- below > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2868
- "\u2026 ",
2869
- below,
2870
- " more below"
2871
- ] }),
2872
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "[\u2191/\u2193 or j/k] move [Enter] select [Esc] cancel" })
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
+ ] })
2873
3966
  ] });
2874
3967
  }
2875
3968
 
2876
3969
  // src/cli/components/StatusBar.tsx
2877
- import { Box as Box7, Text as Text8, useStdout } from "ink";
3970
+ import { Box as Box9, Text as Text10, useStdout as useStdout2 } from "ink";
2878
3971
  import { memo as memo2 } from "react";
2879
- import { jsxs as jsxs8 } from "react/jsx-runtime";
3972
+ import { jsxs as jsxs10 } from "react/jsx-runtime";
2880
3973
  var BG_LABEL_MAX = 40;
2881
3974
  var WIDE_COLUMNS = 120;
2882
3975
  function cwdLabel(cwd, wide) {
@@ -2891,43 +3984,48 @@ var StatusBar = memo2(function StatusBar2({
2891
3984
  tokens: tokens2,
2892
3985
  gitBranch,
2893
3986
  contextPercent,
3987
+ cachePercent,
2894
3988
  sessionCostUsd,
2895
3989
  backgroundTasks = []
2896
3990
  }) {
2897
- const { stdout } = useStdout();
3991
+ const { stdout } = useStdout2();
2898
3992
  const wide = (stdout?.columns ?? 80) >= WIDE_COLUMNS;
2899
3993
  const joined = backgroundTasks.join(", ");
2900
3994
  const labels = joined.length > BG_LABEL_MAX ? `${joined.slice(0, BG_LABEL_MAX)}\u2026` : joined;
2901
- return /* @__PURE__ */ jsxs8(Box7, { justifyContent: "space-between", children: [
2902
- /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
3995
+ return /* @__PURE__ */ jsxs10(Box9, { justifyContent: "space-between", children: [
3996
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2903
3997
  cwdLabel(cwd, wide),
2904
3998
  gitBranch ? ` [${gitBranch}]` : ""
2905
3999
  ] }),
2906
- backgroundTasks.length > 0 && /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
4000
+ backgroundTasks.length > 0 && /* @__PURE__ */ jsxs10(Text10, { color: "yellow", children: [
2907
4001
  "bg: ",
2908
4002
  backgroundTasks.length,
2909
4003
  " (",
2910
4004
  labels,
2911
4005
  ")"
2912
4006
  ] }),
2913
- /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
4007
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2914
4008
  "model: ",
2915
4009
  model
2916
4010
  ] }),
2917
- contextPercent != null && /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
4011
+ contextPercent != null && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2918
4012
  "ctx: ",
2919
4013
  contextPercent,
2920
4014
  "%"
2921
4015
  ] }),
2922
- 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: [
2923
4021
  "$",
2924
4022
  formatDollars(sessionCostUsd)
2925
4023
  ] }),
2926
- /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
4024
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2927
4025
  tokens2,
2928
4026
  " tokens"
2929
4027
  ] }),
2930
- /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
4028
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2931
4029
  "mode: ",
2932
4030
  permissionMode
2933
4031
  ] })
@@ -2935,22 +4033,22 @@ var StatusBar = memo2(function StatusBar2({
2935
4033
  });
2936
4034
 
2937
4035
  // src/cli/components/StreamingMessage.tsx
2938
- import { Box as Box8, Text as Text9 } from "ink";
4036
+ import { Box as Box10, Text as Text11 } from "ink";
2939
4037
  import { memo as memo3 } from "react";
2940
- import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
4038
+ import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
2941
4039
  var StreamingMessage = memo3(function StreamingMessage2({
2942
4040
  text,
2943
4041
  continuation
2944
4042
  }) {
2945
- return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", marginBottom: 1, children: [
2946
- !continuation && /* @__PURE__ */ jsx8(Text9, { bold: true, color: "green", children: "star" }),
2947
- /* @__PURE__ */ jsx8(Text9, { color: "green", children: text })
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 })
2948
4046
  ] });
2949
4047
  });
2950
4048
 
2951
4049
  // src/cli/components/ThinkingIndicator.tsx
2952
- import { Box as Box9, Text as Text10 } from "ink";
2953
- 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";
2954
4052
  var SPINNER_FRAMES = ["-", "\\", "|", "/"];
2955
4053
  var REASONING_TAIL_LENGTH = 200;
2956
4054
  var THOUGHT_SUMMARY_LENGTH = 100;
@@ -2966,13 +4064,13 @@ function ThinkingIndicator({
2966
4064
  }) {
2967
4065
  const tail = reasoning ? truncateTail(reasoning, REASONING_TAIL_LENGTH) : "";
2968
4066
  const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
2969
- return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
2970
- /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
4067
+ return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", children: [
4068
+ /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
2971
4069
  spinner,
2972
4070
  " ",
2973
4071
  activity ?? "star is thinking\u2026"
2974
4072
  ] }),
2975
- tail !== "" && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
4073
+ tail !== "" && /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
2976
4074
  " ",
2977
4075
  tail
2978
4076
  ] })
@@ -2980,8 +4078,8 @@ function ThinkingIndicator({
2980
4078
  }
2981
4079
 
2982
4080
  // src/cli/components/TodoPanel.tsx
2983
- import { Box as Box10, Text as Text11 } from "ink";
2984
- import { jsx as jsx9, jsxs as jsxs11 } from "react/jsx-runtime";
4081
+ import { Box as Box12, Text as Text13 } from "ink";
4082
+ import { jsx as jsx11, jsxs as jsxs13 } from "react/jsx-runtime";
2985
4083
  var SYMBOLS = {
2986
4084
  pending: "[ ]",
2987
4085
  in_progress: "[~]",
@@ -2989,8 +4087,8 @@ var SYMBOLS = {
2989
4087
  };
2990
4088
  function TodoPanel({ todos }) {
2991
4089
  if (todos.length === 0) return null;
2992
- return /* @__PURE__ */ jsx9(Box10, { flexDirection: "column", children: todos.map((todo) => /* @__PURE__ */ jsxs11(
2993
- Text11,
4090
+ return /* @__PURE__ */ jsx11(Box12, { flexDirection: "column", children: todos.map((todo) => /* @__PURE__ */ jsxs13(
4091
+ Text13,
2994
4092
  {
2995
4093
  dimColor: todo.status === "done",
2996
4094
  color: todo.status === "in_progress" ? "cyan" : void 0,
@@ -3020,28 +4118,44 @@ function isDoubleEscape(previous, now, windowMs = DOUBLE_ESC_WINDOW_MS) {
3020
4118
  }
3021
4119
 
3022
4120
  // src/cli/history.ts
3023
- import fs10 from "fs";
3024
- import path16 from "path";
3025
- var DEFAULT_LIMIT = 500;
3026
- var MAX_FILE_LINES = 1e3;
3027
- var TRIM_TO_LINES = 500;
4121
+ import fs12 from "fs";
4122
+ import path19 from "path";
4123
+ var HISTORY_LIMIT = 50;
4124
+ var JUNK_ENTRY_PATTERN = /^(.)\1{19,}$/;
3028
4125
  function historyPath() {
3029
- return path16.join(starHome(), "history");
4126
+ return path19.join(starHome(), "history");
3030
4127
  }
3031
4128
  function readLines(file) {
3032
- const raw = fs10.readFileSync(file, "utf8");
4129
+ const raw = fs12.readFileSync(file, "utf8");
3033
4130
  return raw.split("\n").map((line) => line.replace(/\r$/, "")).filter((line) => line.trim().length > 0);
3034
4131
  }
3035
- 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) {
3036
4144
  try {
3037
- 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);
3038
4152
  } catch {
3039
4153
  return [];
3040
4154
  }
3041
4155
  }
3042
4156
  function appendHistory(entry) {
3043
4157
  const line = entry.replace(/[\r\n]+/g, " ").trim();
3044
- if (line.length === 0) return;
4158
+ if (line.length === 0 || isSkippableEntry(line)) return;
3045
4159
  try {
3046
4160
  const file = historyPath();
3047
4161
  let existing = [];
@@ -3050,15 +4164,12 @@ function appendHistory(entry) {
3050
4164
  } catch {
3051
4165
  }
3052
4166
  if (existing[existing.length - 1] === line) return;
3053
- if (existing.length >= MAX_FILE_LINES) {
3054
- const kept = [...existing.slice(-(TRIM_TO_LINES - 1)), line];
3055
- fs10.mkdirSync(path16.dirname(file), { recursive: true });
3056
- fs10.writeFileSync(file, `${kept.join("\n")}
3057
- `);
4167
+ if (existing.length >= HISTORY_LIMIT) {
4168
+ writeHistory(file, [...existing.slice(-(HISTORY_LIMIT - 1)), line]);
3058
4169
  return;
3059
4170
  }
3060
- fs10.mkdirSync(path16.dirname(file), { recursive: true });
3061
- fs10.appendFileSync(file, `${line}
4171
+ fs12.mkdirSync(path19.dirname(file), { recursive: true });
4172
+ fs12.appendFileSync(file, `${line}
3062
4173
  `);
3063
4174
  } catch {
3064
4175
  }
@@ -3179,7 +4290,7 @@ async function checkForUpdate(currentVersion, fetcher = defaultFetcher2) {
3179
4290
  }
3180
4291
 
3181
4292
  // src/cli/repl.tsx
3182
- import { jsx as jsx10, jsxs as jsxs12 } from "react/jsx-runtime";
4293
+ import { jsx as jsx12, jsxs as jsxs14 } from "react/jsx-runtime";
3183
4294
  var CLEAR_TERMINAL = process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H";
3184
4295
  function emptyUsage() {
3185
4296
  return { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
@@ -3214,65 +4325,67 @@ function Repl({
3214
4325
  initialUsage
3215
4326
  }) {
3216
4327
  const { exit } = useApp();
3217
- const [initialDisplay] = useState3(() => buildDisplayMessages(initialMessages ?? []));
3218
- const [messages, setMessages] = useState3(initialDisplay);
3219
- const [epoch, setEpoch] = useState3(0);
3220
- const [streamingText, setStreamingText] = useState3(null);
3221
- const [isStreaming, setIsStreaming] = useState3(false);
3222
- const [thinking, setThinking] = useState3(false);
3223
- const [thinkingText, setThinkingText] = useState3("");
3224
- const [thoughtSummary, setThoughtSummary] = useState3(null);
3225
- const [, setUsageVersion] = useState3(0);
3226
- const [modelName, setModelName] = useState3(model);
3227
- const [pending, setPending] = useState3(null);
3228
- const [spinnerTick, setSpinnerTick] = useState3(0);
3229
- const [activity, setActivity] = useState3(null);
3230
- const [bgLabels, setBgLabels] = useState3(() => runningTaskLabels());
3231
- const [todos, setTodos] = useState3([]);
3232
- const [permissionModeState, setPermissionModeState] = useState3(permissionMode);
3233
- const [planApproval, setPlanApprovalState] = useState3(false);
3234
- const [pendingRewind, setPendingRewindState] = useState3(null);
3235
- const [picker, setPickerState] = useState3(null);
3236
- const backendRef = useRef3(backend);
3237
- const nextIdRef = useRef3(initialDisplay.length);
3238
- const abortRef = useRef3(null);
3239
- const streamedRef = useRef3("");
3240
- const thinkingRef = useRef3(false);
3241
- const reasoningRef = useRef3("");
3242
- const tickerStopRef = useRef3(null);
3243
- const flushedRef = useRef3({ streamed: "", reasoning: "" });
3244
- const toolCardsRef = useRef3(/* @__PURE__ */ new Map());
3245
- const pendingRef = useRef3(null);
3246
- const alwaysAllowedRef = useRef3(/* @__PURE__ */ new Set());
3247
- const prevModeRef = useRef3(null);
3248
- const planApprovalRef = useRef3(false);
3249
- const pendingRewindRef = useRef3(null);
3250
- const pickerRef = useRef3(null);
3251
- const modelNameRef = useRef3(model);
3252
- const sessionStoreRef = useRef3(sessionStore);
3253
- const usageRef = useRef3(initialUsage ? { ...initialUsage } : emptyUsage());
3254
- const turnChunksRef = useRef3(0);
3255
- const messagesRef = useRef3(initialDisplay);
3256
- const [queueItems, setQueueItems] = useState3([]);
3257
- const queueRef = useRef3(new PromptQueue());
3258
- const [pendingImages, setPendingImages] = useState3([]);
3259
- const pendingImagesRef = useRef3([]);
3260
- const imageSeqRef = useRef3(1);
3261
- const [inputRefill, setInputRefill] = useState3();
3262
- const refillSeqRef = useRef3(0);
3263
- const lastEscRef = useRef3(null);
3264
- const [gitBranch, setGitBranch] = useState3(null);
3265
- const [contextPercent, setContextPercent] = useState3(null);
3266
- const [initialHistory] = useState3(() => loadHistory());
3267
- const budgetWarnedRef = useRef3(false);
3268
- const budgetExceededRef = useRef3(false);
3269
- const runStreamRef = useRef3(
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(
3270
4383
  null
3271
4384
  );
3272
4385
  const pushMessage = useCallback(
3273
- (role, text, note, tight) => {
4386
+ (role, text, note, tight, interrupted) => {
3274
4387
  setMessages((prev) => {
3275
- const next = [...prev, { id: nextIdRef.current++, role, text, note, tight }];
4388
+ const next = [...prev, { id: nextIdRef.current++, role, text, note, tight, interrupted }];
3276
4389
  messagesRef.current = next;
3277
4390
  return next;
3278
4391
  });
@@ -3294,10 +4407,10 @@ function Repl({
3294
4407
  [applyMessages]
3295
4408
  );
3296
4409
  const pushAssistantChunk = useCallback(
3297
- (text, tight) => {
4410
+ (text, tight, interrupted = false) => {
3298
4411
  const role = turnChunksRef.current === 0 ? "assistant" : "assistant-cont";
3299
4412
  turnChunksRef.current += 1;
3300
- pushMessage(role, text, void 0, tight);
4413
+ pushMessage(role, text, void 0, tight, interrupted);
3301
4414
  },
3302
4415
  [pushMessage]
3303
4416
  );
@@ -3317,6 +4430,10 @@ function Repl({
3317
4430
  pickerRef.current = p;
3318
4431
  setPickerState(p);
3319
4432
  }, []);
4433
+ const setPendingConnect = useCallback((p) => {
4434
+ pendingConnectRef.current = p;
4435
+ setPendingConnectState(p);
4436
+ }, []);
3320
4437
  const showPicker = useCallback(
3321
4438
  (title, options) => {
3322
4439
  if (pickerRef.current) return Promise.resolve(null);
@@ -3360,10 +4477,17 @@ function Repl({
3360
4477
  [config]
3361
4478
  );
3362
4479
  const handlePasteImage = useCallback(() => {
3363
- void readClipboardImage().then((img) => {
3364
- 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);
3365
4489
  });
3366
- }, [stageClipboardImage]);
4490
+ }, [stageClipboardImage, pushMessage]);
3367
4491
  const handleRewindDecision = useCallback(
3368
4492
  (decision) => {
3369
4493
  const p = pendingRewindRef.current;
@@ -3373,6 +4497,32 @@ function Repl({
3373
4497
  },
3374
4498
  [setPendingRewind]
3375
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]);
3376
4526
  const applySessionMode = useCallback(
3377
4527
  (mode) => {
3378
4528
  if (mode === config.permissionMode) return;
@@ -3415,10 +4565,12 @@ function Repl({
3415
4565
  if (message) pushMessage("system", message);
3416
4566
  });
3417
4567
  }, [pushMessage]);
4568
+ const resumedAtStart = initialMessages !== void 0;
3418
4569
  useEffect2(() => {
4570
+ if (!resumedAtStart) return;
3419
4571
  loadTodos(cwd).then(setTodos).catch(() => {
3420
4572
  });
3421
- }, [cwd]);
4573
+ }, [cwd, resumedAtStart]);
3422
4574
  useEffect2(() => {
3423
4575
  const onUpdate = (task) => {
3424
4576
  setBgLabels(runningTaskLabels());
@@ -3502,9 +4654,9 @@ function Repl({
3502
4654
  }
3503
4655
  exit();
3504
4656
  }, [exit, pushMessage]);
3505
- useInput6((_input, key) => {
4657
+ useInput8((_input, key) => {
3506
4658
  if (key.escape) {
3507
- 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;
3508
4660
  if (busy) {
3509
4661
  lastEscRef.current = null;
3510
4662
  interrupt();
@@ -3521,7 +4673,7 @@ function Repl({
3521
4673
  }
3522
4674
  lastEscRef.current = null;
3523
4675
  if (key.shift && key.tab) {
3524
- 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)
3525
4677
  return;
3526
4678
  const current = config.permissionMode;
3527
4679
  const index = SESSION_MODE_CYCLE.indexOf(current);
@@ -3577,6 +4729,15 @@ function Repl({
3577
4729
  },
3578
4730
  [config, cwd, attachConfirmHandler]
3579
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
+ );
3580
4741
  const resume = useCallback(
3581
4742
  async (id) => {
3582
4743
  const current = backendRef.current;
@@ -3592,6 +4753,7 @@ function Repl({
3592
4753
  return `Session not found: ${id}`;
3593
4754
  }
3594
4755
  await current.loadMessages(resumed.messages);
4756
+ setTodos(await loadTodos(cwd));
3595
4757
  const store = await SessionStore.open(resolvedId);
3596
4758
  if (store) {
3597
4759
  hydrateSnapshots(await loadSessionSnapshots(store.dir));
@@ -3603,7 +4765,7 @@ function Repl({
3603
4765
  setUsageVersion((v) => v + 1);
3604
4766
  return `Resumed session ${resolvedId} (${resumed.messages.length} messages).`;
3605
4767
  },
3606
- [applyMessages]
4768
+ [applyMessages, cwd]
3607
4769
  );
3608
4770
  const runStream = useCallback(
3609
4771
  async (input, extraImages = []) => {
@@ -3715,6 +4877,10 @@ function Repl({
3715
4877
  usage.promptTokens += event.usage.promptTokens;
3716
4878
  usage.completionTokens += event.usage.completionTokens;
3717
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
+ }
3718
4884
  setUsageVersion((v) => v + 1);
3719
4885
  sessionStoreRef.current?.addUsage(event.usage).catch(() => {
3720
4886
  });
@@ -3761,9 +4927,9 @@ function Repl({
3761
4927
  elapsedMs: Date.now() - turnStartedAt
3762
4928
  });
3763
4929
  if (finalText.length > 0) {
3764
- pushAssistantChunk(`${finalText}${interrupted ? " [interrupted]" : ""}`, false);
4930
+ pushAssistantChunk(finalText, false, interrupted);
3765
4931
  } else if (interrupted && turnChunksRef.current > 0) {
3766
- pushAssistantChunk("[interrupted]", false);
4932
+ pushAssistantChunk("", false, true);
3767
4933
  }
3768
4934
  for (const card of toolCardsRef.current.values()) {
3769
4935
  pushMessage("tool", formatToolCard(card));
@@ -3992,12 +5158,20 @@ ${lines.join("\n")}`;
3992
5158
  await current.loadMessages(first?.role === "system" ? [first] : []);
3993
5159
  clearSnapshots();
3994
5160
  redrawMessages([]);
5161
+ resetTodos();
5162
+ setTodos([]);
3995
5163
  usageRef.current = emptyUsage();
3996
5164
  budgetWarnedRef.current = false;
3997
5165
  budgetExceededRef.current = false;
3998
5166
  setUsageVersion((v) => v + 1);
3999
5167
  return `Started a new session (${store.id}) with a clean context.`;
4000
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
+ },
4001
5175
  clearSessions: async (all) => {
4002
5176
  const currentId = sessionStoreRef.current?.id;
4003
5177
  const removed = await clearSessions(all ? void 0 : cwd, currentId);
@@ -4051,6 +5225,26 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
4051
5225
  if (!(current instanceof AgentLoop)) {
4052
5226
  return "Nothing to undo.";
4053
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
+ }
4054
5248
  const { removed, turn } = await current.retractLastTurn();
4055
5249
  if (removed === 0) {
4056
5250
  return "Nothing to undo (no conversation turn to retract).";
@@ -4187,6 +5381,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
4187
5381
  redrawMessages,
4188
5382
  applySessionMode,
4189
5383
  setPendingRewind,
5384
+ setPendingConnect,
4190
5385
  showPicker
4191
5386
  ]);
4192
5387
  const runShellBang = useCallback(
@@ -4291,9 +5486,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
4291
5486
  }
4292
5487
  }, [cwd, config, isStreaming, epoch]);
4293
5488
  const sessionCost = sessionCostUsd();
4294
- return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", children: [
4295
- /* @__PURE__ */ jsx10(MessageList, { messages }, `messages-${epoch}`),
4296
- (thinking || activity !== null) && /* @__PURE__ */ jsx10(
5489
+ return /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", children: [
5490
+ /* @__PURE__ */ jsx12(MessageList, { messages }, `messages-${epoch}`),
5491
+ (thinking || activity !== null) && /* @__PURE__ */ jsx12(
4297
5492
  ThinkingIndicator,
4298
5493
  {
4299
5494
  reasoning: thinkingText,
@@ -4301,9 +5496,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
4301
5496
  activity: activity ?? void 0
4302
5497
  }
4303
5498
  ),
4304
- !thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */ jsx10(Text12, { dimColor: true, children: thoughtSummary }),
4305
- streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */ jsx10(StreamingMessage, { text: streamingText, continuation: turnChunksRef.current > 0 }),
4306
- pending && /* @__PURE__ */ jsx10(
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(
4307
5502
  PermissionPrompt,
4308
5503
  {
4309
5504
  request: pending.request,
@@ -4311,9 +5506,18 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
4311
5506
  onDecision: handleDecision
4312
5507
  }
4313
5508
  ),
4314
- planApproval && /* @__PURE__ */ jsx10(ExitPlanPrompt, { onDecision: handlePlanDecision }),
4315
- pendingRewind && /* @__PURE__ */ jsx10(RewindConfirmPrompt, { summary: pendingRewind.summary, onDecision: handleRewindDecision }),
4316
- picker && /* @__PURE__ */ jsx10(
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(
4317
5521
  SelectPrompt,
4318
5522
  {
4319
5523
  title: picker.title,
@@ -4322,21 +5526,33 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
4322
5526
  onCancel: handlePickerCancel
4323
5527
  }
4324
5528
  ),
4325
- queueItems.map((item) => /* @__PURE__ */ jsxs12(Text12, { 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: [
4326
5542
  "queued: ",
4327
5543
  item.text.length > 80 ? `${item.text.slice(0, 80)}\u2026` : item.text
4328
5544
  ] }, item.id)),
4329
- pendingImages.map((staged) => /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
5545
+ pendingImages.map((staged) => /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
4330
5546
  "[image attached: ",
4331
5547
  staged.image.path,
4332
5548
  "]"
4333
5549
  ] }, staged.id)),
4334
- /* @__PURE__ */ jsx10(TodoPanel, { todos }),
4335
- /* @__PURE__ */ jsx10(
5550
+ /* @__PURE__ */ jsx12(TodoPanel, { todos }),
5551
+ /* @__PURE__ */ jsx12(
4336
5552
  InputBox,
4337
5553
  {
4338
5554
  isStreaming,
4339
- disabled: pending !== null || planApproval || pendingRewind !== null || picker !== null,
5555
+ disabled: pending !== null || planApproval || pendingRewind !== null || pendingConnect !== null || picker !== null,
4340
5556
  commands: commandHints,
4341
5557
  cwd,
4342
5558
  refill: inputRefill,
@@ -4347,7 +5563,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
4347
5563
  onPasteImage: handlePasteImage
4348
5564
  }
4349
5565
  ),
4350
- /* @__PURE__ */ jsx10(
5566
+ /* @__PURE__ */ jsx12(
4351
5567
  StatusBar,
4352
5568
  {
4353
5569
  cwd,
@@ -4356,6 +5572,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
4356
5572
  tokens: usageRef.current.totalTokens,
4357
5573
  gitBranch,
4358
5574
  contextPercent,
5575
+ cachePercent: usageRef.current.cache ? cacheHitPercent(usageRef.current.cache) : null,
4359
5576
  sessionCostUsd: sessionCost,
4360
5577
  backgroundTasks: bgLabels
4361
5578
  }
@@ -4367,7 +5584,7 @@ function renderRepl(backend, opts) {
4367
5584
  defaultTaskManager.cleanup();
4368
5585
  });
4369
5586
  return render(
4370
- /* @__PURE__ */ jsx10(
5587
+ /* @__PURE__ */ jsx12(
4371
5588
  Repl,
4372
5589
  {
4373
5590
  backend,
@@ -4384,7 +5601,7 @@ function renderRepl(backend, opts) {
4384
5601
  }
4385
5602
 
4386
5603
  // src/config/loader.ts
4387
- import fs11 from "fs";
5604
+ import fs13 from "fs";
4388
5605
  import { parse as parse2 } from "smol-toml";
4389
5606
  var PartialConfigSchema = ConfigSchema.partial();
4390
5607
  function parseTomlFile(content, filePath) {
@@ -4412,11 +5629,12 @@ function applyOverrides(config, overrides) {
4412
5629
  return merged;
4413
5630
  }
4414
5631
  function readTomlFileSync(filePath) {
4415
- if (!fs11.existsSync(filePath)) return {};
4416
- const content = fs11.readFileSync(filePath, "utf8");
5632
+ if (!fs13.existsSync(filePath)) return {};
5633
+ const content = fs13.readFileSync(filePath, "utf8");
4417
5634
  return parseTomlFile(content, filePath);
4418
5635
  }
4419
5636
  function loadConfigSync(cwd, overrides) {
5637
+ loadEnvFile();
4420
5638
  const global = readTomlFileSync(globalConfigPath());
4421
5639
  const project = readTomlFileSync(projectConfigPath(cwd));
4422
5640
  const merged = applyOverrides(mergeConfig(global, project), overrides);
@@ -4659,6 +5877,7 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
4659
5877
  }
4660
5878
  if (resumed) {
4661
5879
  await loop.loadMessages(resumed.messages);
5880
+ await loadTodos(cwd);
4662
5881
  if (sessionStore) {
4663
5882
  hydrateSnapshots(await loadSessionSnapshots(sessionStore.dir));
4664
5883
  }