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