@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/README.md +16 -8
- package/dist/{chunk-DZWTIMJ2.js → chunk-K4SIXWCI.js} +273 -14
- package/dist/chunk-K4SIXWCI.js.map +1 -0
- package/dist/{loop-EIODKYPT.js → loop-ED6AAMWV.js} +2 -2
- package/dist/main.js +1606 -387
- package/dist/main.js.map +1 -1
- package/dist/{stream-Y55NRF2S.js → stream-YQCRUBO3.js} +26 -2
- package/dist/stream-YQCRUBO3.js.map +1 -0
- package/package.json +25 -16
- package/dist/chunk-DZWTIMJ2.js.map +0 -1
- package/dist/stream-Y55NRF2S.js.map +0 -1
- /package/dist/{loop-EIODKYPT.js.map → loop-ED6AAMWV.js.map} +0 -0
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-
|
|
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[
|
|
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 =
|
|
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
|
|
253
|
+
const buf = await readFile2(abs).catch(() => null);
|
|
133
254
|
if (!buf) return null;
|
|
134
|
-
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
279
|
-
import { useCallback, useEffect as useEffect2, useMemo, useRef as
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
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
|
|
657
|
+
import path4 from "path";
|
|
469
658
|
function checkpointsDir(sessionDir) {
|
|
470
|
-
return
|
|
659
|
+
return path4.join(sessionDir, "checkpoints");
|
|
471
660
|
}
|
|
472
661
|
function indexPath(sessionDir) {
|
|
473
|
-
return
|
|
662
|
+
return path4.join(checkpointsDir(sessionDir), "index.json");
|
|
474
663
|
}
|
|
475
664
|
function checkpointContentPath(sessionDir, id) {
|
|
476
|
-
return
|
|
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
|
|
713
|
+
import path6 from "path";
|
|
525
714
|
|
|
526
715
|
// src/session/store.ts
|
|
527
716
|
import fs3 from "fs/promises";
|
|
528
|
-
import
|
|
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 =
|
|
739
|
+
this.dir = path5.join(sessionsDir(), id);
|
|
551
740
|
}
|
|
552
741
|
metaPath() {
|
|
553
|
-
return
|
|
742
|
+
return path5.join(this.dir, "meta.json");
|
|
554
743
|
}
|
|
555
744
|
messagesPath() {
|
|
556
|
-
return
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
928
|
+
import path7 from "path";
|
|
740
929
|
async function countMessages(id) {
|
|
741
930
|
let raw;
|
|
742
931
|
try {
|
|
743
|
-
raw = await fs5.readFile(
|
|
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
|
|
1190
|
+
import path8 from "path";
|
|
1002
1191
|
import { fileURLToPath } from "url";
|
|
1003
1192
|
function readPackageVersion() {
|
|
1004
1193
|
try {
|
|
1005
|
-
const pkgPath =
|
|
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
|
|
1030
|
-
import
|
|
1031
|
-
import
|
|
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
|
|
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
|
|
1298
|
+
async function commandExists2(command) {
|
|
1096
1299
|
const checker = process.platform === "win32" ? "where" : "which";
|
|
1097
1300
|
try {
|
|
1098
|
-
await
|
|
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 =
|
|
1106
|
-
const hasPngpaste = process.platform === "darwin" ? await
|
|
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
|
|
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 {
|
|
1325
|
+
return finalizeClipboardImage({
|
|
1326
|
+
path: "clipboard.png",
|
|
1327
|
+
mimeType: "image/png",
|
|
1328
|
+
data: buf2.toString("base64")
|
|
1329
|
+
});
|
|
1119
1330
|
}
|
|
1120
|
-
await
|
|
1121
|
-
const buf = await
|
|
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 {
|
|
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
|
|
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 =
|
|
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
|
|
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 ?
|
|
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(
|
|
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
|
|
1337
|
-
import
|
|
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 =
|
|
1713
|
+
const absolute = path11.resolve(cwd, filePath);
|
|
1499
1714
|
let existing;
|
|
1500
1715
|
try {
|
|
1501
|
-
existing = await
|
|
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
|
|
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(
|
|
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(
|
|
1969
|
-
scanDir(
|
|
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
|
|
1996
|
-
import
|
|
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
|
|
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 =
|
|
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 =
|
|
2564
|
+
const sessions = path14.join(home, "sessions");
|
|
2086
2565
|
let writable = false;
|
|
2087
2566
|
let writeError = "";
|
|
2088
2567
|
try {
|
|
2089
|
-
await
|
|
2090
|
-
const probe =
|
|
2091
|
-
await
|
|
2092
|
-
await
|
|
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
|
|
2128
|
-
import
|
|
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
|
|
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
|
|
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(
|
|
2161
|
-
if (await fileExists2(
|
|
2162
|
-
if (await fileExists2(
|
|
2163
|
-
if (await fileExists2(
|
|
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
|
|
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
|
|
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:
|
|
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(
|
|
2217
|
-
isGitRepo: await fileExists2(
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
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:
|
|
2435
|
-
/* @__PURE__ */
|
|
2436
|
-
|
|
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
|
|
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 =
|
|
2462
|
-
const entries = await
|
|
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?.(
|
|
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
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
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] =
|
|
2498
|
-
const [cursor, setCursor] =
|
|
2499
|
-
const [history, setHistory] =
|
|
2500
|
-
const [highlight, setHighlight] =
|
|
2501
|
-
const [suggestionsDismissed, setSuggestionsDismissed] =
|
|
2502
|
-
const [pathSuggestions, setPathSuggestions] =
|
|
2503
|
-
const historyIndexRef =
|
|
2504
|
-
const draftRef =
|
|
2505
|
-
const refillSeqRef =
|
|
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(
|
|
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
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
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 =
|
|
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"
|
|
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
|
-
|
|
2619
|
-
if (
|
|
2620
|
-
|
|
2621
|
-
|
|
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
|
-
|
|
2634
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2717
|
-
import { Fragment, jsx as
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
2728
|
-
return /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
3875
|
+
/* @__PURE__ */ jsx6(DiffLineView, { line }, index)
|
|
2737
3876
|
)) });
|
|
2738
3877
|
}
|
|
2739
3878
|
|
|
2740
3879
|
// src/cli/components/MessageList.tsx
|
|
2741
|
-
import { jsx as
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
2756
|
-
/* @__PURE__ */
|
|
2757
|
-
/* @__PURE__ */
|
|
2758
|
-
|
|
2759
|
-
|
|
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
|
|
2766
|
-
import { jsx as
|
|
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
|
-
|
|
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__ */
|
|
2775
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2783
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2788
|
-
] }) : /* @__PURE__ */
|
|
2789
|
-
/* @__PURE__ */
|
|
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
|
|
2795
|
-
import { jsx as
|
|
2796
|
-
function RewindConfirmPrompt({
|
|
2797
|
-
|
|
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__ */
|
|
2803
|
-
/* @__PURE__ */
|
|
2804
|
-
/* @__PURE__ */
|
|
2805
|
-
/* @__PURE__ */
|
|
2806
|
-
|
|
2807
|
-
}
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
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
|
|
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
|
|
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 } =
|
|
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__ */
|
|
2902
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
4007
|
+
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
2914
4008
|
"model: ",
|
|
2915
4009
|
model
|
|
2916
4010
|
] }),
|
|
2917
|
-
contextPercent != null && /* @__PURE__ */
|
|
4011
|
+
contextPercent != null && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
2918
4012
|
"ctx: ",
|
|
2919
4013
|
contextPercent,
|
|
2920
4014
|
"%"
|
|
2921
4015
|
] }),
|
|
2922
|
-
|
|
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__ */
|
|
4024
|
+
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
2927
4025
|
tokens2,
|
|
2928
4026
|
" tokens"
|
|
2929
4027
|
] }),
|
|
2930
|
-
/* @__PURE__ */
|
|
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
|
|
4036
|
+
import { Box as Box10, Text as Text11 } from "ink";
|
|
2939
4037
|
import { memo as memo3 } from "react";
|
|
2940
|
-
import { jsx as
|
|
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__ */
|
|
2946
|
-
!continuation && /* @__PURE__ */
|
|
2947
|
-
/* @__PURE__ */
|
|
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
|
|
2953
|
-
import { jsxs as
|
|
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__ */
|
|
2970
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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
|
|
2984
|
-
import { jsx as
|
|
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__ */
|
|
2993
|
-
|
|
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
|
|
3024
|
-
import
|
|
3025
|
-
var
|
|
3026
|
-
var
|
|
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
|
|
4126
|
+
return path19.join(starHome(), "history");
|
|
3030
4127
|
}
|
|
3031
4128
|
function readLines(file) {
|
|
3032
|
-
const raw =
|
|
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
|
|
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
|
-
|
|
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 >=
|
|
3054
|
-
|
|
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
|
-
|
|
3061
|
-
|
|
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
|
|
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] =
|
|
3218
|
-
const [messages, setMessages] =
|
|
3219
|
-
const [epoch, setEpoch] =
|
|
3220
|
-
const [streamingText, setStreamingText] =
|
|
3221
|
-
const [isStreaming, setIsStreaming] =
|
|
3222
|
-
const [thinking, setThinking] =
|
|
3223
|
-
const [thinkingText, setThinkingText] =
|
|
3224
|
-
const [thoughtSummary, setThoughtSummary] =
|
|
3225
|
-
const [, setUsageVersion] =
|
|
3226
|
-
const [modelName, setModelName] =
|
|
3227
|
-
const [pending, setPending] =
|
|
3228
|
-
const [spinnerTick, setSpinnerTick] =
|
|
3229
|
-
const [activity, setActivity] =
|
|
3230
|
-
const [bgLabels, setBgLabels] =
|
|
3231
|
-
const [todos, setTodos] =
|
|
3232
|
-
const [permissionModeState, setPermissionModeState] =
|
|
3233
|
-
const [planApproval, setPlanApprovalState] =
|
|
3234
|
-
const [pendingRewind, setPendingRewindState] =
|
|
3235
|
-
const [picker, setPickerState] =
|
|
3236
|
-
const
|
|
3237
|
-
const
|
|
3238
|
-
const
|
|
3239
|
-
const
|
|
3240
|
-
const
|
|
3241
|
-
const
|
|
3242
|
-
const
|
|
3243
|
-
const
|
|
3244
|
-
const
|
|
3245
|
-
const
|
|
3246
|
-
const
|
|
3247
|
-
const
|
|
3248
|
-
const
|
|
3249
|
-
const
|
|
3250
|
-
const
|
|
3251
|
-
const
|
|
3252
|
-
const
|
|
3253
|
-
const
|
|
3254
|
-
const
|
|
3255
|
-
const
|
|
3256
|
-
const
|
|
3257
|
-
const
|
|
3258
|
-
const [
|
|
3259
|
-
const
|
|
3260
|
-
const
|
|
3261
|
-
const
|
|
3262
|
-
const
|
|
3263
|
-
const
|
|
3264
|
-
const
|
|
3265
|
-
const
|
|
3266
|
-
const [
|
|
3267
|
-
const
|
|
3268
|
-
const
|
|
3269
|
-
const
|
|
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((
|
|
3364
|
-
if (
|
|
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
|
-
|
|
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(
|
|
4930
|
+
pushAssistantChunk(finalText, false, interrupted);
|
|
3765
4931
|
} else if (interrupted && turnChunksRef.current > 0) {
|
|
3766
|
-
pushAssistantChunk("
|
|
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__ */
|
|
4295
|
-
/* @__PURE__ */
|
|
4296
|
-
(thinking || activity !== null) && /* @__PURE__ */
|
|
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__ */
|
|
4305
|
-
streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */
|
|
4306
|
-
pending && /* @__PURE__ */
|
|
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__ */
|
|
4315
|
-
pendingRewind && /* @__PURE__ */
|
|
4316
|
-
|
|
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
|
-
|
|
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__ */
|
|
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__ */
|
|
4335
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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
|
|
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 (!
|
|
4416
|
-
const content =
|
|
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
|
}
|