@cryer/star-cli 0.3.0 → 0.3.2
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 +17 -9
- package/dist/{chunk-DZWTIMJ2.js → chunk-BV4PLM6X.js} +318 -86
- package/dist/chunk-BV4PLM6X.js.map +1 -0
- package/dist/chunk-LADQCYKL.js +113 -0
- package/dist/chunk-LADQCYKL.js.map +1 -0
- package/dist/{loop-EIODKYPT.js → loop-XKC5HQBT.js} +3 -2
- package/dist/main.js +1748 -406
- package/dist/main.js.map +1 -1
- package/dist/stream-OY2MKWYE.js +166 -0
- package/dist/stream-OY2MKWYE.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-DZWTIMJ2.js.map +0 -1
- package/dist/stream-Y55NRF2S.js +0 -82
- package/dist/stream-Y55NRF2S.js.map +0 -1
- /package/dist/{loop-EIODKYPT.js.map → loop-XKC5HQBT.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,
|
|
@@ -23,25 +24,32 @@ import {
|
|
|
23
24
|
formatTaskStarted,
|
|
24
25
|
formatTodos,
|
|
25
26
|
getGitSummary,
|
|
26
|
-
globalConfigPath,
|
|
27
27
|
hydrateSnapshots,
|
|
28
28
|
isAllowedByRules,
|
|
29
29
|
isDangerousCommand,
|
|
30
30
|
isGitRepo,
|
|
31
31
|
isSensitivePath,
|
|
32
32
|
listSnapshots,
|
|
33
|
+
listTurnSnapshots,
|
|
33
34
|
loadTodos,
|
|
34
35
|
parseTodoArgs,
|
|
35
|
-
|
|
36
|
+
probeImageDimensions,
|
|
36
37
|
reconcileToolCalls,
|
|
38
|
+
resetTodos,
|
|
37
39
|
resolveShell,
|
|
40
|
+
resolveSnapshotContent,
|
|
38
41
|
rewindToSnapshot,
|
|
42
|
+
summarizeMessages,
|
|
43
|
+
undoTurnSnapshots
|
|
44
|
+
} from "./chunk-BV4PLM6X.js";
|
|
45
|
+
import {
|
|
46
|
+
envFilePath,
|
|
47
|
+
globalConfigPath,
|
|
48
|
+
projectConfigPath,
|
|
39
49
|
sessionsDir,
|
|
40
50
|
starHome,
|
|
41
|
-
summarizeMessages,
|
|
42
|
-
undoTurnSnapshots,
|
|
43
51
|
userMemoryPath
|
|
44
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-LADQCYKL.js";
|
|
45
53
|
|
|
46
54
|
// src/main.tsx
|
|
47
55
|
import { Command } from "commander";
|
|
@@ -109,10 +117,125 @@ function buildDisplayMessages(messages) {
|
|
|
109
117
|
}
|
|
110
118
|
|
|
111
119
|
// src/cli/mentions.ts
|
|
112
|
-
import { readFile, stat } from "fs/promises";
|
|
120
|
+
import { readFile as readFile2, readdir, stat } from "fs/promises";
|
|
121
|
+
import path2 from "path";
|
|
122
|
+
|
|
123
|
+
// src/cli/image.ts
|
|
124
|
+
import { execFile } from "child_process";
|
|
125
|
+
import { randomUUID } from "crypto";
|
|
126
|
+
import { readFile, unlink, writeFile } from "fs/promises";
|
|
127
|
+
import os from "os";
|
|
113
128
|
import path from "path";
|
|
129
|
+
import { promisify } from "util";
|
|
130
|
+
var RESIZE_TIMEOUT_MS = 1e4;
|
|
131
|
+
var execFileAsync = promisify(execFile);
|
|
132
|
+
function windowsResizeScript(inputFile, outputFile, maxDimension) {
|
|
133
|
+
const input = inputFile.replace(/'/g, "''");
|
|
134
|
+
const output = outputFile.replace(/'/g, "''");
|
|
135
|
+
return [
|
|
136
|
+
"Add-Type -AssemblyName System.Drawing",
|
|
137
|
+
`$img = [System.Drawing.Image]::FromFile('${input}')`,
|
|
138
|
+
`$scale = [Math]::Min(1.0, ${maxDimension} / [Math]::Max($img.Width, $img.Height))`,
|
|
139
|
+
"$w = [int][Math]::Max(1, [Math]::Round($img.Width * $scale))",
|
|
140
|
+
"$h = [int][Math]::Max(1, [Math]::Round($img.Height * $scale))",
|
|
141
|
+
"$bmp = New-Object System.Drawing.Bitmap($w, $h)",
|
|
142
|
+
"$g = [System.Drawing.Graphics]::FromImage($bmp)",
|
|
143
|
+
"$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic",
|
|
144
|
+
"$g.DrawImage($img, 0, 0, $w, $h)",
|
|
145
|
+
`$bmp.Save('${output}', [System.Drawing.Imaging.ImageFormat]::Png)`,
|
|
146
|
+
"$g.Dispose(); $bmp.Dispose(); $img.Dispose()"
|
|
147
|
+
].join("; ");
|
|
148
|
+
}
|
|
149
|
+
function resizeImagePlan(platform, inputFile, outputFile, maxDimension, tools = {}) {
|
|
150
|
+
if (platform === "win32") {
|
|
151
|
+
return {
|
|
152
|
+
command: "powershell.exe",
|
|
153
|
+
args: [
|
|
154
|
+
"-NoProfile",
|
|
155
|
+
"-NonInteractive",
|
|
156
|
+
"-Command",
|
|
157
|
+
windowsResizeScript(inputFile, outputFile, maxDimension)
|
|
158
|
+
]
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (platform === "darwin") {
|
|
162
|
+
return {
|
|
163
|
+
command: "sips",
|
|
164
|
+
args: ["-Z", String(maxDimension), inputFile, "--out", outputFile]
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (platform === "linux") {
|
|
168
|
+
if (tools.hasConvert) {
|
|
169
|
+
return {
|
|
170
|
+
command: "convert",
|
|
171
|
+
args: [inputFile, "-resize", `${maxDimension}x${maxDimension}>`, outputFile]
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (tools.hasFfmpeg) {
|
|
175
|
+
return {
|
|
176
|
+
command: "ffmpeg",
|
|
177
|
+
args: [
|
|
178
|
+
"-y",
|
|
179
|
+
"-i",
|
|
180
|
+
inputFile,
|
|
181
|
+
"-vf",
|
|
182
|
+
`scale='if(gt(iw,ih),min(iw,${maxDimension}),-1)':'if(gt(iw,ih),-1,min(ih,${maxDimension}))'`,
|
|
183
|
+
outputFile
|
|
184
|
+
]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
async function commandExists(command) {
|
|
192
|
+
const checker = process.platform === "win32" ? "where" : "which";
|
|
193
|
+
try {
|
|
194
|
+
await execFileAsync(checker, [command], { timeout: RESIZE_TIMEOUT_MS });
|
|
195
|
+
return true;
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function downsampleImageIfNeeded(image) {
|
|
201
|
+
const buf = Buffer.from(image.data, "base64");
|
|
202
|
+
const dims = probeImageDimensions(buf);
|
|
203
|
+
if (!dims || Math.max(dims.width, dims.height) <= MAX_IMAGE_DIMENSION) {
|
|
204
|
+
return { image, oversized: false, resized: false };
|
|
205
|
+
}
|
|
206
|
+
const inputFile = path.join(os.tmpdir(), `star-image-${randomUUID()}.src`);
|
|
207
|
+
const outputFile = path.join(os.tmpdir(), `star-image-${randomUUID()}.png`);
|
|
208
|
+
try {
|
|
209
|
+
const [hasConvert, hasFfmpeg] = process.platform === "linux" ? await Promise.all([commandExists("convert"), commandExists("ffmpeg")]) : [false, false];
|
|
210
|
+
const plan = resizeImagePlan(process.platform, inputFile, outputFile, MAX_IMAGE_DIMENSION, {
|
|
211
|
+
hasConvert,
|
|
212
|
+
hasFfmpeg
|
|
213
|
+
});
|
|
214
|
+
if (!plan) return { image, oversized: true, resized: false };
|
|
215
|
+
await writeFile(inputFile, buf);
|
|
216
|
+
await execFileAsync(plan.command, plan.args, { timeout: RESIZE_TIMEOUT_MS });
|
|
217
|
+
const out = await readFile(outputFile).catch(() => null);
|
|
218
|
+
if (!out || out.length === 0) return { image, oversized: true, resized: false };
|
|
219
|
+
return {
|
|
220
|
+
image: { path: image.path, mimeType: "image/png", data: out.toString("base64") },
|
|
221
|
+
oversized: true,
|
|
222
|
+
resized: true
|
|
223
|
+
};
|
|
224
|
+
} catch {
|
|
225
|
+
return { image, oversized: true, resized: false };
|
|
226
|
+
} finally {
|
|
227
|
+
await unlink(inputFile).catch(() => {
|
|
228
|
+
});
|
|
229
|
+
await unlink(outputFile).catch(() => {
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/cli/mentions.ts
|
|
114
235
|
var MAX_MENTION_BYTES = 100 * 1024;
|
|
115
236
|
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
237
|
+
var MAX_DIR_MENTION_ENTRIES = 200;
|
|
238
|
+
var MAX_DIR_MENTION_DEPTH = 10;
|
|
116
239
|
var IMAGE_MIME_TYPES = {
|
|
117
240
|
".png": "image/png",
|
|
118
241
|
".jpg": "image/jpeg",
|
|
@@ -121,17 +244,22 @@ var IMAGE_MIME_TYPES = {
|
|
|
121
244
|
".webp": "image/webp"
|
|
122
245
|
};
|
|
123
246
|
function imageMimeType(filePath) {
|
|
124
|
-
return IMAGE_MIME_TYPES[
|
|
247
|
+
return IMAGE_MIME_TYPES[path2.extname(filePath).toLowerCase()] ?? null;
|
|
125
248
|
}
|
|
126
249
|
async function readImageInput(filePath, cwd) {
|
|
127
250
|
const mimeType = imageMimeType(filePath);
|
|
128
251
|
if (!mimeType) return null;
|
|
129
|
-
const abs =
|
|
252
|
+
const abs = path2.resolve(cwd, filePath);
|
|
130
253
|
const st = await stat(abs).catch(() => null);
|
|
131
254
|
if (!st || !st.isFile() || st.size > MAX_IMAGE_BYTES) return null;
|
|
132
|
-
const buf = await
|
|
255
|
+
const buf = await readFile2(abs).catch(() => null);
|
|
133
256
|
if (!buf) return null;
|
|
134
|
-
|
|
257
|
+
const result = await downsampleImageIfNeeded({
|
|
258
|
+
path: filePath,
|
|
259
|
+
mimeType,
|
|
260
|
+
data: buf.toString("base64")
|
|
261
|
+
});
|
|
262
|
+
return result.image;
|
|
135
263
|
}
|
|
136
264
|
var MENTION_RE = /(?<=^|\s)@([A-Za-z]:[\\/][A-Za-z0-9._\-/\\]*|[A-Za-z0-9._\-/\\]+)/g;
|
|
137
265
|
function parseMentions(text) {
|
|
@@ -165,6 +293,51 @@ function isBinaryContent(buf) {
|
|
|
165
293
|
const probe = buf.subarray(0, 8e3);
|
|
166
294
|
return probe.includes(0);
|
|
167
295
|
}
|
|
296
|
+
async function listDirEntries(dir, prefix, depth, state, ignore) {
|
|
297
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
298
|
+
if (!entries) return;
|
|
299
|
+
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) => {
|
|
300
|
+
const dirDiff = Number(b.isDirectory()) - Number(a.isDirectory());
|
|
301
|
+
return dirDiff !== 0 ? dirDiff : a.name.localeCompare(b.name);
|
|
302
|
+
});
|
|
303
|
+
for (let i = 0; i < visible.length; i++) {
|
|
304
|
+
const entry = visible[i];
|
|
305
|
+
if (!entry) continue;
|
|
306
|
+
state.total += 1;
|
|
307
|
+
const last = i === visible.length - 1;
|
|
308
|
+
const isDir = entry.isDirectory();
|
|
309
|
+
if (state.shown < MAX_DIR_MENTION_ENTRIES) {
|
|
310
|
+
state.shown += 1;
|
|
311
|
+
state.lines.push(`${prefix}${last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${entry.name}${isDir ? "/" : ""}`);
|
|
312
|
+
if (isDir && depth < MAX_DIR_MENTION_DEPTH) {
|
|
313
|
+
await listDirEntries(
|
|
314
|
+
path2.join(dir, entry.name),
|
|
315
|
+
`${prefix}${last ? " " : "\u2502 "}`,
|
|
316
|
+
depth + 1,
|
|
317
|
+
state,
|
|
318
|
+
ignore
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
} else if (isDir && depth < MAX_DIR_MENTION_DEPTH) {
|
|
322
|
+
await listDirEntries(path2.join(dir, entry.name), prefix, depth + 1, state, ignore);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async function directoryBlock(abs, mention, ignore) {
|
|
327
|
+
const display = `${mention.replace(/\\/g, "/").replace(/\/+$/, "")}/`;
|
|
328
|
+
const state = { lines: [`${display}`], shown: 0, total: 0 };
|
|
329
|
+
const probe = await readdir(abs).catch(() => null);
|
|
330
|
+
if (!probe) return null;
|
|
331
|
+
await listDirEntries(abs, "", 1, state, ignore);
|
|
332
|
+
if (state.total > state.shown) {
|
|
333
|
+
state.lines.push(
|
|
334
|
+
`... (truncated, ${state.total - state.shown} more entries \u2014 use glob/grep tools or mention a subdirectory to see more)`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return `--- @${display} (directory) ---
|
|
338
|
+
${state.lines.join("\n")}
|
|
339
|
+
--- end ---`;
|
|
340
|
+
}
|
|
168
341
|
async function resolveMentions(text, cwd) {
|
|
169
342
|
const { cleanText, mentions } = parseMentions(text);
|
|
170
343
|
if (mentions.length === 0) {
|
|
@@ -174,8 +347,9 @@ async function resolveMentions(text, cwd) {
|
|
|
174
347
|
const attached = [];
|
|
175
348
|
const skipped = [];
|
|
176
349
|
const images = [];
|
|
350
|
+
let ignore;
|
|
177
351
|
for (const mention of mentions) {
|
|
178
|
-
const abs =
|
|
352
|
+
const abs = path2.resolve(cwd, mention);
|
|
179
353
|
if (isSensitivePath(abs)) {
|
|
180
354
|
skipped.push({ path: mention, reason: "sensitive file" });
|
|
181
355
|
continue;
|
|
@@ -191,13 +365,18 @@ async function resolveMentions(text, cwd) {
|
|
|
191
365
|
skipped.push({ path: mention, reason: "image too large" });
|
|
192
366
|
continue;
|
|
193
367
|
}
|
|
194
|
-
const buf2 = await
|
|
368
|
+
const buf2 = await readFile2(abs).catch(() => null);
|
|
195
369
|
if (!buf2) {
|
|
196
370
|
skipped.push({ path: mention, reason: "unreadable" });
|
|
197
371
|
continue;
|
|
198
372
|
}
|
|
373
|
+
const result = await downsampleImageIfNeeded({
|
|
374
|
+
path: mention,
|
|
375
|
+
mimeType,
|
|
376
|
+
data: buf2.toString("base64")
|
|
377
|
+
});
|
|
199
378
|
attached.push(mention);
|
|
200
|
-
images.push(
|
|
379
|
+
images.push(result.image);
|
|
201
380
|
continue;
|
|
202
381
|
}
|
|
203
382
|
const st = await stat(abs).catch(() => null);
|
|
@@ -206,14 +385,23 @@ async function resolveMentions(text, cwd) {
|
|
|
206
385
|
continue;
|
|
207
386
|
}
|
|
208
387
|
if (st.isDirectory()) {
|
|
209
|
-
|
|
388
|
+
if (ignore === void 0) {
|
|
389
|
+
ignore = createIgnorePredicate(cwd) ?? null;
|
|
390
|
+
}
|
|
391
|
+
const block = await directoryBlock(abs, mention, ignore ?? void 0);
|
|
392
|
+
if (!block) {
|
|
393
|
+
skipped.push({ path: mention, reason: "unreadable" });
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
attached.push(mention);
|
|
397
|
+
blocks.push(block);
|
|
210
398
|
continue;
|
|
211
399
|
}
|
|
212
400
|
if (st.size > MAX_MENTION_BYTES) {
|
|
213
401
|
skipped.push({ path: mention, reason: `exceeds ${MAX_MENTION_BYTES / 1024}KB limit` });
|
|
214
402
|
continue;
|
|
215
403
|
}
|
|
216
|
-
const buf = await
|
|
404
|
+
const buf = await readFile2(abs).catch(() => null);
|
|
217
405
|
if (!buf) {
|
|
218
406
|
skipped.push({ path: mention, reason: "unreadable" });
|
|
219
407
|
continue;
|
|
@@ -275,12 +463,12 @@ var UsageTracker = class {
|
|
|
275
463
|
};
|
|
276
464
|
|
|
277
465
|
// src/cli/repl.tsx
|
|
278
|
-
import { Box as
|
|
279
|
-
import { useCallback, useEffect as useEffect2, useMemo, useRef as
|
|
466
|
+
import { Box as Box13, Text as Text14, render, useApp, useInput as useInput8 } from "ink";
|
|
467
|
+
import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef5, useState as useState5 } from "react";
|
|
280
468
|
|
|
281
469
|
// src/config/save.ts
|
|
282
470
|
import fs from "fs";
|
|
283
|
-
import
|
|
471
|
+
import path3 from "path";
|
|
284
472
|
import { parse, stringify } from "smol-toml";
|
|
285
473
|
async function addAllowRule(rule) {
|
|
286
474
|
return addPermissionRule("allow", rule);
|
|
@@ -299,7 +487,7 @@ async function addPermissionRule(kind, rule) {
|
|
|
299
487
|
if (rules.includes(rule)) return false;
|
|
300
488
|
permissions[kind] = [...rules, rule];
|
|
301
489
|
raw.permissions = permissions;
|
|
302
|
-
await fs.promises.mkdir(
|
|
490
|
+
await fs.promises.mkdir(path3.dirname(filePath), { recursive: true });
|
|
303
491
|
await fs.promises.writeFile(filePath, stringify(raw), "utf8");
|
|
304
492
|
return true;
|
|
305
493
|
}
|
|
@@ -313,7 +501,7 @@ async function savePermissionMode(mode) {
|
|
|
313
501
|
if (error.code !== "ENOENT") throw error;
|
|
314
502
|
}
|
|
315
503
|
raw.permissionMode = mode;
|
|
316
|
-
await fs.promises.mkdir(
|
|
504
|
+
await fs.promises.mkdir(path3.dirname(filePath), { recursive: true });
|
|
317
505
|
await fs.promises.writeFile(filePath, stringify(raw), "utf8");
|
|
318
506
|
}
|
|
319
507
|
|
|
@@ -321,6 +509,10 @@ async function savePermissionMode(mode) {
|
|
|
321
509
|
import { z } from "zod";
|
|
322
510
|
var ProviderConfigSchema = z.object({
|
|
323
511
|
name: z.string(),
|
|
512
|
+
// openai-responses talks to the Responses API (/responses) instead of chat
|
|
513
|
+
// completions — the wire format codex uses, and the native API of
|
|
514
|
+
// reasoning-model relays that otherwise hide reasoning behind empty chat
|
|
515
|
+
// completions. streamChat sends strictSchemas:false + store:false with it.
|
|
324
516
|
protocol: z.enum(["openai-compatible", "anthropic", "openai-responses"]).default("openai-compatible"),
|
|
325
517
|
baseURL: z.string(),
|
|
326
518
|
apiKeyEnv: z.string().optional(),
|
|
@@ -377,13 +569,18 @@ var ConfigSchema = z.object({
|
|
|
377
569
|
// Extra attempts per model request when a stream fails transiently
|
|
378
570
|
// (network error, 429/5xx, idle watchdog cutoff) or comes back empty, so a
|
|
379
571
|
// relay hiccup does not silently end a half-finished turn. 0 disables.
|
|
380
|
-
//
|
|
381
|
-
//
|
|
572
|
+
// The wait between attempts honors Retry-After headers and otherwise backs
|
|
573
|
+
// off exponentially with jitter (llm/retry.ts); retries after a
|
|
574
|
+
// timeout/empty failure also scale both stream timeouts up (attempt number
|
|
575
|
+
// ×, capped at 3x) since the relay is likely overloaded.
|
|
382
576
|
streamMaxRetries: z.number().int().min(0).default(3),
|
|
383
|
-
// How many
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
577
|
+
// How many consecutive unproductive replies may be nudged before the turn
|
|
578
|
+
// is handed back: a text-only reply that announces pending work ("我将…",
|
|
579
|
+
// "I will…") or answers a nudge with more words, or an empty reply that
|
|
580
|
+
// survived the stream retry budget (steered with a nudge — a changed
|
|
581
|
+
// request — since identical resends already failed). Tool calls reset the
|
|
582
|
+
// count, so a productive task keeps going; exhaustion surfaces a notice
|
|
583
|
+
// instead of stopping silently. Never fires in plan mode. 0 disables.
|
|
387
584
|
maxAutoContinues: z.number().int().min(0).default(2),
|
|
388
585
|
contextCompaction: z.enum(["summary", "truncate"]).default("summary"),
|
|
389
586
|
// Optional per-session dollar budget; when set, the REPL stops the session
|
|
@@ -420,6 +617,71 @@ function resolveModelConfig(config, modelName) {
|
|
|
420
617
|
return model;
|
|
421
618
|
}
|
|
422
619
|
|
|
620
|
+
// src/llm/sse-normalize.ts
|
|
621
|
+
var SseNormalizeTransform = class extends TransformStream {
|
|
622
|
+
constructor() {
|
|
623
|
+
let buffer = "";
|
|
624
|
+
let pending = null;
|
|
625
|
+
const flushPending = (controller) => {
|
|
626
|
+
if (pending === null) return;
|
|
627
|
+
controller.enqueue(`data: ${pending}
|
|
628
|
+
|
|
629
|
+
`);
|
|
630
|
+
pending = null;
|
|
631
|
+
};
|
|
632
|
+
const handleLine = (raw, controller) => {
|
|
633
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
634
|
+
if (line === "") {
|
|
635
|
+
flushPending(controller);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (line.startsWith("data:")) {
|
|
639
|
+
flushPending(controller);
|
|
640
|
+
pending = line.slice(5).replace(/^ /, "");
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (line.startsWith("{") && pending !== null) {
|
|
644
|
+
flushPending(controller);
|
|
645
|
+
pending = line;
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
flushPending(controller);
|
|
649
|
+
controller.enqueue(`${line}
|
|
650
|
+
`);
|
|
651
|
+
};
|
|
652
|
+
super({
|
|
653
|
+
transform(chunk, controller) {
|
|
654
|
+
buffer += chunk;
|
|
655
|
+
let index = buffer.indexOf("\n");
|
|
656
|
+
while (index !== -1) {
|
|
657
|
+
handleLine(buffer.slice(0, index), controller);
|
|
658
|
+
buffer = buffer.slice(index + 1);
|
|
659
|
+
index = buffer.indexOf("\n");
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
flush(controller) {
|
|
663
|
+
if (buffer.length > 0) handleLine(buffer, controller);
|
|
664
|
+
flushPending(controller);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
function createSseNormalizingFetch(baseFetch = fetch) {
|
|
670
|
+
return async (input, init) => {
|
|
671
|
+
const response = await baseFetch(input, init);
|
|
672
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
673
|
+
if (!response.body || !contentType.includes("text/event-stream")) {
|
|
674
|
+
return response;
|
|
675
|
+
}
|
|
676
|
+
const body = response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new SseNormalizeTransform()).pipeThrough(new TextEncoderStream());
|
|
677
|
+
return new Response(body, {
|
|
678
|
+
status: response.status,
|
|
679
|
+
statusText: response.statusText,
|
|
680
|
+
headers: response.headers
|
|
681
|
+
});
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
423
685
|
// src/llm/provider.ts
|
|
424
686
|
function resolveApiKey(provider) {
|
|
425
687
|
const fromEnv = provider.apiKeyEnv ? process.env[provider.apiKeyEnv] : void 0;
|
|
@@ -458,22 +720,26 @@ function createModel(config, modelName) {
|
|
|
458
720
|
return createOpenAI({
|
|
459
721
|
baseURL: provider.baseURL,
|
|
460
722
|
apiKey,
|
|
461
|
-
headers: provider.headers
|
|
723
|
+
headers: provider.headers,
|
|
724
|
+
// Relays fronting the Responses API sometimes batch several JSON
|
|
725
|
+
// events into one SSE data payload, which the SDK's parser cannot
|
|
726
|
+
// read; normalize the wire format back to one event per data line.
|
|
727
|
+
fetch: createSseNormalizingFetch()
|
|
462
728
|
}).responses(modelConfig.model);
|
|
463
729
|
}
|
|
464
730
|
}
|
|
465
731
|
|
|
466
732
|
// src/session/checkpoints.ts
|
|
467
733
|
import fs2 from "fs/promises";
|
|
468
|
-
import
|
|
734
|
+
import path4 from "path";
|
|
469
735
|
function checkpointsDir(sessionDir) {
|
|
470
|
-
return
|
|
736
|
+
return path4.join(sessionDir, "checkpoints");
|
|
471
737
|
}
|
|
472
738
|
function indexPath(sessionDir) {
|
|
473
|
-
return
|
|
739
|
+
return path4.join(checkpointsDir(sessionDir), "index.json");
|
|
474
740
|
}
|
|
475
741
|
function checkpointContentPath(sessionDir, id) {
|
|
476
|
-
return
|
|
742
|
+
return path4.join(checkpointsDir(sessionDir), `${id}.snapshot`);
|
|
477
743
|
}
|
|
478
744
|
async function listCheckpointRecords(sessionDir) {
|
|
479
745
|
let raw;
|
|
@@ -521,11 +787,11 @@ async function loadSessionSnapshots(sessionDir) {
|
|
|
521
787
|
|
|
522
788
|
// src/session/clear.ts
|
|
523
789
|
import fs4 from "fs/promises";
|
|
524
|
-
import
|
|
790
|
+
import path6 from "path";
|
|
525
791
|
|
|
526
792
|
// src/session/store.ts
|
|
527
793
|
import fs3 from "fs/promises";
|
|
528
|
-
import
|
|
794
|
+
import path5 from "path";
|
|
529
795
|
function generateId(now) {
|
|
530
796
|
const pad = (n) => String(n).padStart(2, "0");
|
|
531
797
|
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
@@ -547,13 +813,13 @@ var SessionStore = class _SessionStore {
|
|
|
547
813
|
initialized = false;
|
|
548
814
|
constructor(id) {
|
|
549
815
|
this.id = id;
|
|
550
|
-
this.dir =
|
|
816
|
+
this.dir = path5.join(sessionsDir(), id);
|
|
551
817
|
}
|
|
552
818
|
metaPath() {
|
|
553
|
-
return
|
|
819
|
+
return path5.join(this.dir, "meta.json");
|
|
554
820
|
}
|
|
555
821
|
messagesPath() {
|
|
556
|
-
return
|
|
822
|
+
return path5.join(this.dir, "messages.jsonl");
|
|
557
823
|
}
|
|
558
824
|
static async create(cwd, model) {
|
|
559
825
|
const store = new _SessionStore(generateId(/* @__PURE__ */ new Date()));
|
|
@@ -587,7 +853,7 @@ var SessionStore = class _SessionStore {
|
|
|
587
853
|
const metas = [];
|
|
588
854
|
for (const entry of entries) {
|
|
589
855
|
try {
|
|
590
|
-
const raw = await fs3.readFile(
|
|
856
|
+
const raw = await fs3.readFile(path5.join(sessionsDir(), entry, "meta.json"), "utf8");
|
|
591
857
|
const meta = JSON.parse(raw);
|
|
592
858
|
if (meta.id === entry && (cwd === void 0 || meta.cwd === cwd)) metas.push(meta);
|
|
593
859
|
} catch {
|
|
@@ -714,7 +980,7 @@ async function clearSessions(cwd, excludeId) {
|
|
|
714
980
|
let removed2 = 0;
|
|
715
981
|
for (const meta of metas) {
|
|
716
982
|
if (meta.id === excludeId) continue;
|
|
717
|
-
await fs4.rm(
|
|
983
|
+
await fs4.rm(path6.join(sessionsDir(), meta.id), { recursive: true, force: true });
|
|
718
984
|
removed2 += 1;
|
|
719
985
|
}
|
|
720
986
|
return removed2;
|
|
@@ -728,7 +994,7 @@ async function clearSessions(cwd, excludeId) {
|
|
|
728
994
|
let removed = 0;
|
|
729
995
|
for (const entry of entries) {
|
|
730
996
|
if (entry === excludeId) continue;
|
|
731
|
-
await fs4.rm(
|
|
997
|
+
await fs4.rm(path6.join(sessionsDir(), entry), { recursive: true, force: true });
|
|
732
998
|
removed += 1;
|
|
733
999
|
}
|
|
734
1000
|
return removed;
|
|
@@ -736,11 +1002,11 @@ async function clearSessions(cwd, excludeId) {
|
|
|
736
1002
|
|
|
737
1003
|
// src/session/list.ts
|
|
738
1004
|
import fs5 from "fs/promises";
|
|
739
|
-
import
|
|
1005
|
+
import path7 from "path";
|
|
740
1006
|
async function countMessages(id) {
|
|
741
1007
|
let raw;
|
|
742
1008
|
try {
|
|
743
|
-
raw = await fs5.readFile(
|
|
1009
|
+
raw = await fs5.readFile(path7.join(sessionsDir(), id, "messages.jsonl"), "utf8");
|
|
744
1010
|
} catch {
|
|
745
1011
|
return 0;
|
|
746
1012
|
}
|
|
@@ -998,11 +1264,11 @@ function formatUsageDashboard(stats, models, options = {}) {
|
|
|
998
1264
|
|
|
999
1265
|
// src/version.ts
|
|
1000
1266
|
import fs6 from "fs";
|
|
1001
|
-
import
|
|
1267
|
+
import path8 from "path";
|
|
1002
1268
|
import { fileURLToPath } from "url";
|
|
1003
1269
|
function readPackageVersion() {
|
|
1004
1270
|
try {
|
|
1005
|
-
const pkgPath =
|
|
1271
|
+
const pkgPath = path8.join(path8.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
1006
1272
|
const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf8"));
|
|
1007
1273
|
if (typeof pkg === "object" && pkg !== null) {
|
|
1008
1274
|
const version = pkg.version;
|
|
@@ -1023,16 +1289,30 @@ function budgetState(costUsd, budgetUsd) {
|
|
|
1023
1289
|
return "ok";
|
|
1024
1290
|
}
|
|
1025
1291
|
|
|
1292
|
+
// src/cli/cache-stats.ts
|
|
1293
|
+
function cacheUsageReported(usage) {
|
|
1294
|
+
return usage.cachedPromptTokens != null || usage.cacheReadInputTokens != null;
|
|
1295
|
+
}
|
|
1296
|
+
function addToCacheTotals(totals, usage) {
|
|
1297
|
+
totals.cachedTokens += (usage.cachedPromptTokens ?? 0) + (usage.cacheReadInputTokens ?? 0);
|
|
1298
|
+
totals.promptTokens += usage.promptTokens + (usage.cacheReadInputTokens ?? 0);
|
|
1299
|
+
}
|
|
1300
|
+
function cacheHitPercent(totals) {
|
|
1301
|
+
if (totals.promptTokens <= 0) return null;
|
|
1302
|
+
const rate = totals.cachedTokens / totals.promptTokens;
|
|
1303
|
+
return Math.round(Math.min(1, Math.max(0, rate)) * 100);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1026
1306
|
// src/cli/clipboard.ts
|
|
1027
|
-
import { execFile } from "child_process";
|
|
1028
|
-
import { randomUUID } from "crypto";
|
|
1029
|
-
import { readFile as
|
|
1030
|
-
import
|
|
1031
|
-
import
|
|
1032
|
-
import { promisify } from "util";
|
|
1307
|
+
import { execFile as execFile2 } from "child_process";
|
|
1308
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1309
|
+
import { readFile as readFile3, unlink as unlink2 } from "fs/promises";
|
|
1310
|
+
import os2 from "os";
|
|
1311
|
+
import path9 from "path";
|
|
1312
|
+
import { promisify as promisify2 } from "util";
|
|
1033
1313
|
var CLIPBOARD_TIMEOUT_MS = 5e3;
|
|
1034
1314
|
var MAX_CLIPBOARD_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
1035
|
-
var
|
|
1315
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
1036
1316
|
function windowsClipboardImageScript(tempFile) {
|
|
1037
1317
|
const escaped = tempFile.replace(/'/g, "''");
|
|
1038
1318
|
return [
|
|
@@ -1092,40 +1372,52 @@ function clipboardCopyCommand(platform) {
|
|
|
1092
1372
|
if (platform === "linux") return { command: "xclip", args: ["-selection", "clipboard"] };
|
|
1093
1373
|
return null;
|
|
1094
1374
|
}
|
|
1095
|
-
async function
|
|
1375
|
+
async function commandExists2(command) {
|
|
1096
1376
|
const checker = process.platform === "win32" ? "where" : "which";
|
|
1097
1377
|
try {
|
|
1098
|
-
await
|
|
1378
|
+
await execFileAsync2(checker, [command], { timeout: CLIPBOARD_TIMEOUT_MS });
|
|
1099
1379
|
return true;
|
|
1100
1380
|
} catch {
|
|
1101
1381
|
return false;
|
|
1102
1382
|
}
|
|
1103
1383
|
}
|
|
1384
|
+
async function finalizeClipboardImage(image) {
|
|
1385
|
+
const result = await downsampleImageIfNeeded(image);
|
|
1386
|
+
return { image: result.image, oversized: result.oversized && !result.resized };
|
|
1387
|
+
}
|
|
1104
1388
|
async function readClipboardImage() {
|
|
1105
|
-
const tempFile =
|
|
1106
|
-
const hasPngpaste = process.platform === "darwin" ? await
|
|
1389
|
+
const tempFile = path9.join(os2.tmpdir(), `star-clipboard-${randomUUID2()}.png`);
|
|
1390
|
+
const hasPngpaste = process.platform === "darwin" ? await commandExists2("pngpaste") : false;
|
|
1107
1391
|
const plan = clipboardImagePlan(process.platform, tempFile, hasPngpaste);
|
|
1108
1392
|
if (!plan) return null;
|
|
1109
1393
|
try {
|
|
1110
1394
|
if (plan.kind === "stdout") {
|
|
1111
|
-
const { stdout } = await
|
|
1395
|
+
const { stdout } = await execFileAsync2(plan.command, plan.args, {
|
|
1112
1396
|
timeout: CLIPBOARD_TIMEOUT_MS,
|
|
1113
1397
|
maxBuffer: MAX_CLIPBOARD_IMAGE_BYTES,
|
|
1114
1398
|
encoding: "buffer"
|
|
1115
1399
|
});
|
|
1116
1400
|
const buf2 = stdout;
|
|
1117
1401
|
if (buf2.length === 0) return null;
|
|
1118
|
-
return {
|
|
1402
|
+
return finalizeClipboardImage({
|
|
1403
|
+
path: "clipboard.png",
|
|
1404
|
+
mimeType: "image/png",
|
|
1405
|
+
data: buf2.toString("base64")
|
|
1406
|
+
});
|
|
1119
1407
|
}
|
|
1120
|
-
await
|
|
1121
|
-
const buf = await
|
|
1408
|
+
await execFileAsync2(plan.command, plan.args, { timeout: CLIPBOARD_TIMEOUT_MS });
|
|
1409
|
+
const buf = await readFile3(tempFile).catch(() => null);
|
|
1122
1410
|
if (!buf || buf.length === 0) return null;
|
|
1123
|
-
return {
|
|
1411
|
+
return finalizeClipboardImage({
|
|
1412
|
+
path: "clipboard.png",
|
|
1413
|
+
mimeType: "image/png",
|
|
1414
|
+
data: buf.toString("base64")
|
|
1415
|
+
});
|
|
1124
1416
|
} catch {
|
|
1125
1417
|
return null;
|
|
1126
1418
|
} finally {
|
|
1127
1419
|
if (plan.kind === "temp-file") {
|
|
1128
|
-
await
|
|
1420
|
+
await unlink2(tempFile).catch(() => {
|
|
1129
1421
|
});
|
|
1130
1422
|
}
|
|
1131
1423
|
}
|
|
@@ -1134,7 +1426,7 @@ async function copyText(text) {
|
|
|
1134
1426
|
const plan = clipboardCopyCommand(process.platform);
|
|
1135
1427
|
if (!plan) return false;
|
|
1136
1428
|
return new Promise((resolve) => {
|
|
1137
|
-
const child =
|
|
1429
|
+
const child = execFile2(
|
|
1138
1430
|
plan.command,
|
|
1139
1431
|
plan.args,
|
|
1140
1432
|
{ timeout: CLIPBOARD_TIMEOUT_MS },
|
|
@@ -1148,7 +1440,7 @@ async function copyText(text) {
|
|
|
1148
1440
|
|
|
1149
1441
|
// src/cli/commands/actions.ts
|
|
1150
1442
|
import fs7 from "fs/promises";
|
|
1151
|
-
import
|
|
1443
|
+
import path10 from "path";
|
|
1152
1444
|
var MIN_COMPACT_MESSAGES = 4;
|
|
1153
1445
|
async function compactSession(opts) {
|
|
1154
1446
|
const { backend, sessionStore, config, model } = opts;
|
|
@@ -1209,9 +1501,9 @@ async function exportSession(opts) {
|
|
|
1209
1501
|
return "Nothing to export: the session has no messages.";
|
|
1210
1502
|
}
|
|
1211
1503
|
const sessionId = sessionStore?.id ?? `unknown-${Date.now()}`;
|
|
1212
|
-
const target = arg ?
|
|
1504
|
+
const target = arg ? path10.resolve(cwd, arg) : path10.resolve(cwd, `star-session-${sessionId}.md`);
|
|
1213
1505
|
const existed = await fileExists(target);
|
|
1214
|
-
await fs7.mkdir(
|
|
1506
|
+
await fs7.mkdir(path10.dirname(target), { recursive: true });
|
|
1215
1507
|
await fs7.writeFile(target, renderSessionMarkdown(sessionId, messages));
|
|
1216
1508
|
return existed ? `Exported ${messages.length} messages to ${target} (overwrote existing file).` : `Exported ${messages.length} messages to ${target}.`;
|
|
1217
1509
|
}
|
|
@@ -1333,8 +1625,8 @@ function formatSearchResults(hits, query) {
|
|
|
1333
1625
|
}
|
|
1334
1626
|
|
|
1335
1627
|
// src/cli/diff-preview.ts
|
|
1336
|
-
import { readFile as
|
|
1337
|
-
import
|
|
1628
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1629
|
+
import path11 from "path";
|
|
1338
1630
|
var CONTEXT_LINES = 2;
|
|
1339
1631
|
var MAX_HUNK_LINES = 15;
|
|
1340
1632
|
var MAX_TOTAL_LINES = 12;
|
|
@@ -1495,10 +1787,10 @@ async function generateDiffPreview(toolName, args, cwd) {
|
|
|
1495
1787
|
if (toolName !== "edit_file" && toolName !== "write_file") return null;
|
|
1496
1788
|
const filePath = getStringArg(args, "path");
|
|
1497
1789
|
if (filePath === void 0) return null;
|
|
1498
|
-
const absolute =
|
|
1790
|
+
const absolute = path11.resolve(cwd, filePath);
|
|
1499
1791
|
let existing;
|
|
1500
1792
|
try {
|
|
1501
|
-
existing = await
|
|
1793
|
+
existing = await readFile4(absolute, "utf8");
|
|
1502
1794
|
} catch {
|
|
1503
1795
|
existing = null;
|
|
1504
1796
|
}
|
|
@@ -1795,6 +2087,19 @@ ${content}`);
|
|
|
1795
2087
|
ctx.addSystemMessage(await ctx.rewind(args.trim()));
|
|
1796
2088
|
}
|
|
1797
2089
|
});
|
|
2090
|
+
register({
|
|
2091
|
+
name: "connect",
|
|
2092
|
+
description: "Interactive wizard: add an LLM provider + model, store the key safely, and save to config",
|
|
2093
|
+
usage: "/connect",
|
|
2094
|
+
category: "Settings",
|
|
2095
|
+
async run(_args, ctx) {
|
|
2096
|
+
if (!ctx.connect) {
|
|
2097
|
+
ctx.addSystemMessage("/connect: this context cannot run the interactive wizard.");
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
ctx.addSystemMessage(await ctx.connect());
|
|
2101
|
+
}
|
|
2102
|
+
});
|
|
1798
2103
|
register({
|
|
1799
2104
|
name: "init",
|
|
1800
2105
|
description: "Generate an AGENTS.md for the current project",
|
|
@@ -1922,9 +2227,260 @@ ${result.diff}${note ? `
|
|
|
1922
2227
|
});
|
|
1923
2228
|
}
|
|
1924
2229
|
|
|
2230
|
+
// src/cli/commands/connect.ts
|
|
2231
|
+
import { spawn } from "child_process";
|
|
2232
|
+
import fs9 from "fs";
|
|
2233
|
+
import path12 from "path";
|
|
2234
|
+
|
|
2235
|
+
// src/config/env.ts
|
|
2236
|
+
import fs8 from "fs";
|
|
2237
|
+
var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2238
|
+
function parseEnvContent(content) {
|
|
2239
|
+
const vars = {};
|
|
2240
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
2241
|
+
const line = rawLine.trim();
|
|
2242
|
+
if (!line || line.startsWith("#")) continue;
|
|
2243
|
+
const eq = line.indexOf("=");
|
|
2244
|
+
if (eq <= 0) continue;
|
|
2245
|
+
const key = line.slice(0, eq).trim();
|
|
2246
|
+
if (!ENV_KEY_PATTERN.test(key)) continue;
|
|
2247
|
+
let value = line.slice(eq + 1).trim();
|
|
2248
|
+
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
2249
|
+
value = value.slice(1, -1);
|
|
2250
|
+
}
|
|
2251
|
+
vars[key] = value;
|
|
2252
|
+
}
|
|
2253
|
+
return vars;
|
|
2254
|
+
}
|
|
2255
|
+
function loadEnvFile(filePath = envFilePath()) {
|
|
2256
|
+
let content;
|
|
2257
|
+
try {
|
|
2258
|
+
content = fs8.readFileSync(filePath, "utf8");
|
|
2259
|
+
} catch {
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
for (const [key, value] of Object.entries(parseEnvContent(content))) {
|
|
2263
|
+
if (process.env[key] === void 0) {
|
|
2264
|
+
process.env[key] = value;
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
function upsertEnvContent(content, key, value) {
|
|
2269
|
+
const entry = `${key}=${value.replace(/[\r\n]/g, "")}`;
|
|
2270
|
+
const pattern = new RegExp(`^\\s*${key}\\s*=`);
|
|
2271
|
+
const lines = content.split("\n");
|
|
2272
|
+
while (lines.length > 0 && (lines[lines.length - 1] ?? "").trim() === "") lines.pop();
|
|
2273
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2274
|
+
if (pattern.test(lines[i] ?? "")) {
|
|
2275
|
+
lines[i] = entry;
|
|
2276
|
+
return `${lines.join("\n")}
|
|
2277
|
+
`;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
lines.push(entry);
|
|
2281
|
+
return `${lines.join("\n")}
|
|
2282
|
+
`;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// src/cli/commands/connect.ts
|
|
2286
|
+
var PROVIDER_PRESETS = [
|
|
2287
|
+
{
|
|
2288
|
+
key: "openai",
|
|
2289
|
+
label: "OpenAI",
|
|
2290
|
+
baseURL: "https://api.openai.com/v1",
|
|
2291
|
+
protocol: "openai-compatible",
|
|
2292
|
+
modelHint: "gpt-4o"
|
|
2293
|
+
},
|
|
2294
|
+
{
|
|
2295
|
+
key: "anthropic",
|
|
2296
|
+
label: "Anthropic",
|
|
2297
|
+
baseURL: "https://api.anthropic.com",
|
|
2298
|
+
protocol: "anthropic",
|
|
2299
|
+
modelHint: "claude-sonnet-4-5"
|
|
2300
|
+
},
|
|
2301
|
+
{
|
|
2302
|
+
key: "moonshot",
|
|
2303
|
+
label: "Kimi (Moonshot)",
|
|
2304
|
+
baseURL: "https://api.moonshot.cn/v1",
|
|
2305
|
+
protocol: "openai-compatible",
|
|
2306
|
+
modelHint: "kimi-k2-0905-preview"
|
|
2307
|
+
},
|
|
2308
|
+
{
|
|
2309
|
+
key: "deepseek",
|
|
2310
|
+
label: "DeepSeek",
|
|
2311
|
+
baseURL: "https://api.deepseek.com/v1",
|
|
2312
|
+
protocol: "openai-compatible",
|
|
2313
|
+
modelHint: "deepseek-chat"
|
|
2314
|
+
}
|
|
2315
|
+
];
|
|
2316
|
+
var PROTOCOLS = ["openai-compatible", "anthropic", "openai-responses"];
|
|
2317
|
+
var PROTOCOL_DESCRIPTIONS = {
|
|
2318
|
+
"openai-compatible": "POST /chat/completions \u2014 most providers",
|
|
2319
|
+
anthropic: "Anthropic Messages API",
|
|
2320
|
+
"openai-responses": "OpenAI Responses API (endpoints exposing only /responses)"
|
|
2321
|
+
};
|
|
2322
|
+
var CONNECT_DEFAULT_CONTEXT_TOKENS = 128e3;
|
|
2323
|
+
function deriveProviderName(baseURL) {
|
|
2324
|
+
let host = baseURL;
|
|
2325
|
+
try {
|
|
2326
|
+
host = new URL(baseURL).hostname;
|
|
2327
|
+
} catch {
|
|
2328
|
+
host = baseURL.replace(/^[a-z]+:\/\//i, "").split(/[/:?#]/)[0] ?? baseURL;
|
|
2329
|
+
}
|
|
2330
|
+
const labels = host.toLowerCase().split(".");
|
|
2331
|
+
const skip = /* @__PURE__ */ new Set(["www", "api"]);
|
|
2332
|
+
const meaningful = labels.filter((label) => label !== "" && !skip.has(label));
|
|
2333
|
+
const raw = meaningful[0] ?? "provider";
|
|
2334
|
+
const slug = raw.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
2335
|
+
return slug || "provider";
|
|
2336
|
+
}
|
|
2337
|
+
function dedupeName(base, taken) {
|
|
2338
|
+
const names = new Set(taken);
|
|
2339
|
+
if (!names.has(base)) return base;
|
|
2340
|
+
for (let i = 2; ; i++) {
|
|
2341
|
+
const candidate = `${base}-${i}`;
|
|
2342
|
+
if (!names.has(candidate)) return candidate;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
function apiKeyEnvFor(providerName) {
|
|
2346
|
+
return `STAR_API_KEY_${providerName.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
2347
|
+
}
|
|
2348
|
+
function maskApiKey(key) {
|
|
2349
|
+
if (key.length <= 8) return "\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
2350
|
+
return `${key.slice(0, 3)}\u2026${key.slice(-4)}`;
|
|
2351
|
+
}
|
|
2352
|
+
function tomlString(value) {
|
|
2353
|
+
return JSON.stringify(value);
|
|
2354
|
+
}
|
|
2355
|
+
function buildProviderTomlBlock(provider) {
|
|
2356
|
+
return [
|
|
2357
|
+
"[[providers]]",
|
|
2358
|
+
`name = ${tomlString(provider.name)}`,
|
|
2359
|
+
"# wire format: openai-compatible | anthropic | openai-responses",
|
|
2360
|
+
`protocol = ${tomlString(provider.protocol)}`,
|
|
2361
|
+
`baseURL = ${tomlString(provider.baseURL)}`,
|
|
2362
|
+
"# environment variable holding the API key; the key itself lives in",
|
|
2363
|
+
"# ~/.star-cli/.env (written by /connect), never in this file",
|
|
2364
|
+
`apiKeyEnv = ${tomlString(provider.apiKeyEnv ?? "")}`,
|
|
2365
|
+
"# inline key instead of apiKeyEnv (not recommended):",
|
|
2366
|
+
'# apiKey = "sk-..."',
|
|
2367
|
+
"# extra HTTP headers sent to the provider:",
|
|
2368
|
+
'# headers = { "X-Title" = "star-cli" }',
|
|
2369
|
+
"",
|
|
2370
|
+
""
|
|
2371
|
+
].join("\n");
|
|
2372
|
+
}
|
|
2373
|
+
function buildModelTomlBlock(model) {
|
|
2374
|
+
return [
|
|
2375
|
+
"[[models]]",
|
|
2376
|
+
`name = ${tomlString(model.name)}`,
|
|
2377
|
+
`provider = ${tomlString(model.provider)}`,
|
|
2378
|
+
`model = ${tomlString(model.model)}`,
|
|
2379
|
+
"# per-model context window in tokens \u2014 overrides the top-level",
|
|
2380
|
+
"# contextMaxTokens for compaction and the ctx % in the status bar",
|
|
2381
|
+
`contextMaxTokens = ${model.contextMaxTokens ?? CONNECT_DEFAULT_CONTEXT_TOKENS}`,
|
|
2382
|
+
"# pricing in USD per 1M tokens \u2014 set your provider's rates; BOTH",
|
|
2383
|
+
"# promptPrice and completionPrice are required together for the $",
|
|
2384
|
+
"# estimates in /cost and /usage (0 prices a token at $0)",
|
|
2385
|
+
`promptPrice = ${model.promptPrice ?? 0}`,
|
|
2386
|
+
`completionPrice = ${model.completionPrice ?? 0}`,
|
|
2387
|
+
"# cap on generated tokens per reply:",
|
|
2388
|
+
"# maxTokens = 8192",
|
|
2389
|
+
"",
|
|
2390
|
+
""
|
|
2391
|
+
].join("\n");
|
|
2392
|
+
}
|
|
2393
|
+
function saveConnection(answers, configPath = globalConfigPath(), envPath = envFilePath()) {
|
|
2394
|
+
const provider = {
|
|
2395
|
+
name: answers.providerName,
|
|
2396
|
+
protocol: answers.protocol,
|
|
2397
|
+
baseURL: answers.baseURL,
|
|
2398
|
+
apiKeyEnv: answers.apiKeyEnv
|
|
2399
|
+
};
|
|
2400
|
+
const model = {
|
|
2401
|
+
name: answers.modelName,
|
|
2402
|
+
provider: answers.providerName,
|
|
2403
|
+
model: answers.modelId,
|
|
2404
|
+
contextMaxTokens: CONNECT_DEFAULT_CONTEXT_TOKENS,
|
|
2405
|
+
promptPrice: 0,
|
|
2406
|
+
completionPrice: 0
|
|
2407
|
+
};
|
|
2408
|
+
fs9.mkdirSync(path12.dirname(envPath), { recursive: true });
|
|
2409
|
+
let envContent = "";
|
|
2410
|
+
try {
|
|
2411
|
+
envContent = fs9.readFileSync(envPath, "utf8");
|
|
2412
|
+
} catch {
|
|
2413
|
+
envContent = "";
|
|
2414
|
+
}
|
|
2415
|
+
fs9.writeFileSync(
|
|
2416
|
+
envPath,
|
|
2417
|
+
upsertEnvContent(envContent, provider.apiKeyEnv ?? "", answers.apiKey),
|
|
2418
|
+
{
|
|
2419
|
+
encoding: "utf8",
|
|
2420
|
+
mode: 384
|
|
2421
|
+
}
|
|
2422
|
+
);
|
|
2423
|
+
try {
|
|
2424
|
+
fs9.chmodSync(envPath, 384);
|
|
2425
|
+
} catch {
|
|
2426
|
+
}
|
|
2427
|
+
if (provider.apiKeyEnv) {
|
|
2428
|
+
process.env[provider.apiKeyEnv] = answers.apiKey;
|
|
2429
|
+
}
|
|
2430
|
+
fs9.mkdirSync(path12.dirname(configPath), { recursive: true });
|
|
2431
|
+
let existing = "";
|
|
2432
|
+
try {
|
|
2433
|
+
existing = fs9.readFileSync(configPath, "utf8");
|
|
2434
|
+
} catch {
|
|
2435
|
+
existing = "";
|
|
2436
|
+
}
|
|
2437
|
+
const separator = existing === "" ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
2438
|
+
fs9.writeFileSync(
|
|
2439
|
+
configPath,
|
|
2440
|
+
existing + separator + buildProviderTomlBlock(provider) + buildModelTomlBlock(model),
|
|
2441
|
+
"utf8"
|
|
2442
|
+
);
|
|
2443
|
+
return { provider, model, configPath, envPath };
|
|
2444
|
+
}
|
|
2445
|
+
function setDefaultModelInToml(content, modelName) {
|
|
2446
|
+
const line = `defaultModel = ${tomlString(modelName)}`;
|
|
2447
|
+
const lines = content.split("\n");
|
|
2448
|
+
let inTopLevel = true;
|
|
2449
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2450
|
+
const current = lines[i] ?? "";
|
|
2451
|
+
if (/^\s*\[/.test(current)) inTopLevel = false;
|
|
2452
|
+
if (inTopLevel && /^\s*defaultModel\s*=/.test(current)) {
|
|
2453
|
+
lines[i] = line;
|
|
2454
|
+
return lines.join("\n");
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
return `${line}
|
|
2458
|
+
${content}`;
|
|
2459
|
+
}
|
|
2460
|
+
function saveDefaultModel(modelName, configPath = globalConfigPath()) {
|
|
2461
|
+
let content = "";
|
|
2462
|
+
try {
|
|
2463
|
+
content = fs9.readFileSync(configPath, "utf8");
|
|
2464
|
+
} catch {
|
|
2465
|
+
content = "";
|
|
2466
|
+
}
|
|
2467
|
+
fs9.mkdirSync(path12.dirname(configPath), { recursive: true });
|
|
2468
|
+
fs9.writeFileSync(configPath, setDefaultModelInToml(content, modelName), "utf8");
|
|
2469
|
+
}
|
|
2470
|
+
function openConfigFile(filePath) {
|
|
2471
|
+
const [command, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", filePath]] : process.platform === "darwin" ? ["open", [filePath]] : ["xdg-open", [filePath]];
|
|
2472
|
+
try {
|
|
2473
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
2474
|
+
child.on("error", () => {
|
|
2475
|
+
});
|
|
2476
|
+
child.unref();
|
|
2477
|
+
} catch {
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
1925
2481
|
// src/cli/commands/custom.ts
|
|
1926
2482
|
import { readFileSync as readFileSync2, readdirSync } from "fs";
|
|
1927
|
-
import
|
|
2483
|
+
import path13 from "path";
|
|
1928
2484
|
var NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
1929
2485
|
var DESCRIPTION_PATTERN = /^<!--\s*description:\s*(.+?)\s*-->\s*$/;
|
|
1930
2486
|
var DEFAULT_DESCRIPTION = "Custom prompt command";
|
|
@@ -1959,14 +2515,14 @@ function scanDir(dir, into) {
|
|
|
1959
2515
|
if (!entry.endsWith(".md")) continue;
|
|
1960
2516
|
const name = entry.slice(0, -".md".length);
|
|
1961
2517
|
if (!NAME_PATTERN.test(name)) continue;
|
|
1962
|
-
const parsed = parseCommandFile(
|
|
2518
|
+
const parsed = parseCommandFile(path13.join(dir, entry), name);
|
|
1963
2519
|
if (parsed) into.set(name, parsed);
|
|
1964
2520
|
}
|
|
1965
2521
|
}
|
|
1966
2522
|
function loadCustomCommands(cwd, home = starHome()) {
|
|
1967
2523
|
const byName = /* @__PURE__ */ new Map();
|
|
1968
|
-
scanDir(
|
|
1969
|
-
scanDir(
|
|
2524
|
+
scanDir(path13.join(home, "commands"), byName);
|
|
2525
|
+
scanDir(path13.join(cwd, ".star", "commands"), byName);
|
|
1970
2526
|
return [...byName.values()];
|
|
1971
2527
|
}
|
|
1972
2528
|
function registerCustomCommands(registry, cwd, home) {
|
|
@@ -1992,8 +2548,8 @@ function registerCustomCommands(registry, cwd, home) {
|
|
|
1992
2548
|
}
|
|
1993
2549
|
|
|
1994
2550
|
// src/cli/commands/doctor.ts
|
|
1995
|
-
import
|
|
1996
|
-
import
|
|
2551
|
+
import fs10 from "fs/promises";
|
|
2552
|
+
import path14 from "path";
|
|
1997
2553
|
var MIN_NODE_MAJOR = 20;
|
|
1998
2554
|
var REGISTRY_URL = "https://registry.npmjs.org/";
|
|
1999
2555
|
var REGISTRY_TIMEOUT_MS = 5e3;
|
|
@@ -2007,7 +2563,7 @@ function nodeMajor(version) {
|
|
|
2007
2563
|
}
|
|
2008
2564
|
async function pathExists(target) {
|
|
2009
2565
|
try {
|
|
2010
|
-
await
|
|
2566
|
+
await fs10.access(target);
|
|
2011
2567
|
return true;
|
|
2012
2568
|
} catch {
|
|
2013
2569
|
return false;
|
|
@@ -2045,7 +2601,7 @@ async function runDoctor(opts) {
|
|
|
2045
2601
|
detail: `${spec.label} (${spec.shell})`
|
|
2046
2602
|
});
|
|
2047
2603
|
}
|
|
2048
|
-
const configPath =
|
|
2604
|
+
const configPath = path14.join(home, "config.toml");
|
|
2049
2605
|
const configExists = await pathExists(configPath);
|
|
2050
2606
|
checks.push({
|
|
2051
2607
|
name: "STAR_HOME / config",
|
|
@@ -2082,14 +2638,14 @@ async function runDoctor(opts) {
|
|
|
2082
2638
|
});
|
|
2083
2639
|
}
|
|
2084
2640
|
}
|
|
2085
|
-
const sessions =
|
|
2641
|
+
const sessions = path14.join(home, "sessions");
|
|
2086
2642
|
let writable = false;
|
|
2087
2643
|
let writeError = "";
|
|
2088
2644
|
try {
|
|
2089
|
-
await
|
|
2090
|
-
const probe =
|
|
2091
|
-
await
|
|
2092
|
-
await
|
|
2645
|
+
await fs10.mkdir(sessions, { recursive: true });
|
|
2646
|
+
const probe = path14.join(sessions, `.doctor-probe-${process.pid}`);
|
|
2647
|
+
await fs10.writeFile(probe, "ok");
|
|
2648
|
+
await fs10.unlink(probe);
|
|
2093
2649
|
writable = true;
|
|
2094
2650
|
} catch (error) {
|
|
2095
2651
|
writeError = error instanceof Error ? error.message : String(error);
|
|
@@ -2124,15 +2680,15 @@ function formatDoctorReport(report) {
|
|
|
2124
2680
|
}
|
|
2125
2681
|
|
|
2126
2682
|
// src/cli/commands/init-project.ts
|
|
2127
|
-
import
|
|
2128
|
-
import
|
|
2683
|
+
import fs11 from "fs/promises";
|
|
2684
|
+
import path15 from "path";
|
|
2129
2685
|
import { generateText } from "ai";
|
|
2130
2686
|
var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", "coverage"]);
|
|
2131
2687
|
var MAX_ENTRIES = 20;
|
|
2132
2688
|
var README_HEAD_LINES = 5;
|
|
2133
2689
|
async function fileExists2(target) {
|
|
2134
2690
|
try {
|
|
2135
|
-
await
|
|
2691
|
+
await fs11.access(target);
|
|
2136
2692
|
return true;
|
|
2137
2693
|
} catch {
|
|
2138
2694
|
return false;
|
|
@@ -2140,7 +2696,7 @@ async function fileExists2(target) {
|
|
|
2140
2696
|
}
|
|
2141
2697
|
async function readPackageJson(cwd) {
|
|
2142
2698
|
try {
|
|
2143
|
-
const raw = await
|
|
2699
|
+
const raw = await fs11.readFile(path15.join(cwd, "package.json"), "utf8");
|
|
2144
2700
|
const parsed = JSON.parse(raw);
|
|
2145
2701
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
2146
2702
|
return parsed;
|
|
@@ -2157,10 +2713,10 @@ function stringRecord(value) {
|
|
|
2157
2713
|
return out;
|
|
2158
2714
|
}
|
|
2159
2715
|
async function detectPackageManager(cwd) {
|
|
2160
|
-
if (await fileExists2(
|
|
2161
|
-
if (await fileExists2(
|
|
2162
|
-
if (await fileExists2(
|
|
2163
|
-
if (await fileExists2(
|
|
2716
|
+
if (await fileExists2(path15.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2717
|
+
if (await fileExists2(path15.join(cwd, "yarn.lock"))) return "yarn";
|
|
2718
|
+
if (await fileExists2(path15.join(cwd, "package-lock.json"))) return "npm";
|
|
2719
|
+
if (await fileExists2(path15.join(cwd, "bun.lockb"))) return "bun";
|
|
2164
2720
|
return null;
|
|
2165
2721
|
}
|
|
2166
2722
|
function inferCommands(pm, scripts) {
|
|
@@ -2175,7 +2731,7 @@ function inferCommands(pm, scripts) {
|
|
|
2175
2731
|
}
|
|
2176
2732
|
async function listEntries(cwd) {
|
|
2177
2733
|
try {
|
|
2178
|
-
const dirents = await
|
|
2734
|
+
const dirents = await fs11.readdir(cwd, { withFileTypes: true });
|
|
2179
2735
|
const entries = [];
|
|
2180
2736
|
for (const dirent of dirents) {
|
|
2181
2737
|
if (dirent.isDirectory()) {
|
|
@@ -2194,7 +2750,7 @@ async function listEntries(cwd) {
|
|
|
2194
2750
|
async function readReadmeHead(cwd) {
|
|
2195
2751
|
for (const name of ["README.md", "readme.md", "README"]) {
|
|
2196
2752
|
try {
|
|
2197
|
-
const raw = await
|
|
2753
|
+
const raw = await fs11.readFile(path15.join(cwd, name), "utf8");
|
|
2198
2754
|
return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).slice(0, README_HEAD_LINES);
|
|
2199
2755
|
} catch {
|
|
2200
2756
|
}
|
|
@@ -2207,14 +2763,14 @@ async function scanProject(cwd) {
|
|
|
2207
2763
|
const scripts = stringRecord(pkg?.scripts);
|
|
2208
2764
|
return {
|
|
2209
2765
|
name: typeof pkg?.name === "string" ? pkg.name : null,
|
|
2210
|
-
dirName:
|
|
2766
|
+
dirName: path15.basename(cwd),
|
|
2211
2767
|
packageManager,
|
|
2212
2768
|
scripts,
|
|
2213
2769
|
dependencies: Object.keys(stringRecord(pkg?.dependencies)),
|
|
2214
2770
|
devDependencies: Object.keys(stringRecord(pkg?.devDependencies)),
|
|
2215
2771
|
commands: inferCommands(packageManager, scripts),
|
|
2216
|
-
hasTsconfig: await fileExists2(
|
|
2217
|
-
isGitRepo: await fileExists2(
|
|
2772
|
+
hasTsconfig: await fileExists2(path15.join(cwd, "tsconfig.json")),
|
|
2773
|
+
isGitRepo: await fileExists2(path15.join(cwd, ".git")),
|
|
2218
2774
|
readmeHead: await readReadmeHead(cwd),
|
|
2219
2775
|
entries: await listEntries(cwd)
|
|
2220
2776
|
};
|
|
@@ -2281,7 +2837,7 @@ Write the AGENTS.md.`
|
|
|
2281
2837
|
}
|
|
2282
2838
|
async function initProject(opts) {
|
|
2283
2839
|
const { cwd, args, model } = opts;
|
|
2284
|
-
const target =
|
|
2840
|
+
const target = path15.join(cwd, "AGENTS.md");
|
|
2285
2841
|
const force = args.split(/\s+/).includes("force");
|
|
2286
2842
|
if (!force && await fileExists2(target)) {
|
|
2287
2843
|
return {
|
|
@@ -2315,7 +2871,7 @@ async function initProject(opts) {
|
|
|
2315
2871
|
content = renderAgentsMd(facts);
|
|
2316
2872
|
note = " No model available; used the template \u2014 review and refine manually.";
|
|
2317
2873
|
}
|
|
2318
|
-
await
|
|
2874
|
+
await fs11.writeFile(target, content);
|
|
2319
2875
|
const lines = content.split("\n").length;
|
|
2320
2876
|
const how = usedLlm ? "generated with model refinement" : "generated from template";
|
|
2321
2877
|
return {
|
|
@@ -2357,7 +2913,7 @@ function parseSlashCommand(input) {
|
|
|
2357
2913
|
}
|
|
2358
2914
|
|
|
2359
2915
|
// src/cli/commands/rewind.ts
|
|
2360
|
-
import
|
|
2916
|
+
import path16 from "path";
|
|
2361
2917
|
function planRewind(snapshots, id) {
|
|
2362
2918
|
const target = snapshots.find((snapshot) => snapshot.id === id);
|
|
2363
2919
|
if (!target) return null;
|
|
@@ -2371,7 +2927,7 @@ function planRewind(snapshots, id) {
|
|
|
2371
2927
|
return { target, affected, messageIndex };
|
|
2372
2928
|
}
|
|
2373
2929
|
function displayPath(filePath, cwd) {
|
|
2374
|
-
const relative =
|
|
2930
|
+
const relative = path16.relative(cwd, filePath);
|
|
2375
2931
|
return relative !== "" && !relative.startsWith("..") ? relative : filePath;
|
|
2376
2932
|
}
|
|
2377
2933
|
function formatTime(timestamp) {
|
|
@@ -2421,29 +2977,110 @@ function didYouMeanSuffix(names) {
|
|
|
2421
2977
|
return ` Did you mean: ${listed.join(", ")}?`;
|
|
2422
2978
|
}
|
|
2423
2979
|
|
|
2424
|
-
// src/cli/
|
|
2980
|
+
// src/cli/commands/undo.ts
|
|
2981
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
2982
|
+
import path17 from "path";
|
|
2983
|
+
function displayPath2(filePath, cwd) {
|
|
2984
|
+
const relative = path17.relative(cwd, filePath);
|
|
2985
|
+
return relative !== "" && !relative.startsWith("..") ? relative : filePath;
|
|
2986
|
+
}
|
|
2987
|
+
async function buildUndoDiffs(snapshots, cwd) {
|
|
2988
|
+
const diffs = [];
|
|
2989
|
+
for (const snapshot of snapshots) {
|
|
2990
|
+
let current = "";
|
|
2991
|
+
try {
|
|
2992
|
+
current = await readFile5(snapshot.path, "utf8");
|
|
2993
|
+
} catch {
|
|
2994
|
+
}
|
|
2995
|
+
const before = snapshot.existed ? await resolveSnapshotContent(snapshot) : "";
|
|
2996
|
+
const lines = before === null ? [{ kind: "marker", text: "(snapshot content unavailable)" }] : buildDiffLines(current, before);
|
|
2997
|
+
diffs.push({ label: displayPath2(snapshot.path, cwd), lines });
|
|
2998
|
+
}
|
|
2999
|
+
return diffs;
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
// src/cli/components/ConnectWizard.tsx
|
|
3003
|
+
import { Box as Box4, Text as Text4, useInput as useInput4 } from "ink";
|
|
3004
|
+
import { useRef as useRef4, useState as useState4 } from "react";
|
|
3005
|
+
|
|
3006
|
+
// src/cli/components/SelectPrompt.tsx
|
|
2425
3007
|
import { Box, Text, useInput } from "ink";
|
|
3008
|
+
import { useRef, useState } from "react";
|
|
2426
3009
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2427
|
-
|
|
3010
|
+
var SELECT_PROMPT_MAX_VISIBLE = 9;
|
|
3011
|
+
function SelectPrompt({ title, options, onSelect, onCancel }) {
|
|
3012
|
+
const [index, setIndex] = useState(0);
|
|
3013
|
+
const indexRef = useRef(0);
|
|
3014
|
+
const move = (delta) => {
|
|
3015
|
+
indexRef.current = Math.max(0, Math.min(indexRef.current + delta, options.length - 1));
|
|
3016
|
+
setIndex(indexRef.current);
|
|
3017
|
+
};
|
|
2428
3018
|
useInput((input, key) => {
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
3019
|
+
if (key.escape) {
|
|
3020
|
+
onCancel();
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
if (key.return) {
|
|
3024
|
+
const option = options[indexRef.current];
|
|
3025
|
+
if (option) onSelect(option.value);
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
if (key.downArrow || input === "j") {
|
|
3029
|
+
move(1);
|
|
3030
|
+
} else if (key.upArrow || input === "k") {
|
|
3031
|
+
move(-1);
|
|
3032
|
+
}
|
|
2432
3033
|
});
|
|
3034
|
+
const maxStart = Math.max(0, options.length - SELECT_PROMPT_MAX_VISIBLE);
|
|
3035
|
+
const start = Math.min(Math.max(0, index - Math.floor(SELECT_PROMPT_MAX_VISIBLE / 2)), maxStart);
|
|
3036
|
+
const visible = options.slice(start, start + SELECT_PROMPT_MAX_VISIBLE);
|
|
3037
|
+
const above = start;
|
|
3038
|
+
const below = options.length - start - visible.length;
|
|
2433
3039
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
2434
|
-
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children:
|
|
2435
|
-
/* @__PURE__ */
|
|
2436
|
-
|
|
2437
|
-
|
|
3040
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: title }),
|
|
3041
|
+
above > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
3042
|
+
"\u2026 ",
|
|
3043
|
+
above,
|
|
3044
|
+
" more above"
|
|
3045
|
+
] }),
|
|
3046
|
+
visible.map((option, i) => {
|
|
3047
|
+
const active = start + i === index;
|
|
3048
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
3049
|
+
/* @__PURE__ */ jsxs(Text, { bold: active, inverse: active, children: [
|
|
3050
|
+
active ? "\u276F " : " ",
|
|
3051
|
+
option.label
|
|
3052
|
+
] }),
|
|
3053
|
+
option.hint && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
3054
|
+
" (",
|
|
3055
|
+
option.hint,
|
|
3056
|
+
")"
|
|
3057
|
+
] }),
|
|
3058
|
+
option.description && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
3059
|
+
" \u2014 ",
|
|
3060
|
+
option.description
|
|
3061
|
+
] })
|
|
3062
|
+
] }, option.value);
|
|
3063
|
+
}),
|
|
3064
|
+
below > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
3065
|
+
"\u2026 ",
|
|
3066
|
+
below,
|
|
3067
|
+
" more below"
|
|
3068
|
+
] }),
|
|
3069
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "[\u2191/\u2193 or j/k] move [Enter] select [Esc] cancel" })
|
|
3070
|
+
] });
|
|
2438
3071
|
}
|
|
2439
3072
|
|
|
3073
|
+
// src/cli/components/TextField.tsx
|
|
3074
|
+
import { Box as Box3, Text as Text3, useInput as useInput3 } from "ink";
|
|
3075
|
+
import { useRef as useRef3, useState as useState3 } from "react";
|
|
3076
|
+
|
|
2440
3077
|
// src/cli/components/InputBox.tsx
|
|
2441
|
-
import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
|
|
2442
|
-
import { useEffect, useRef, useState } from "react";
|
|
3078
|
+
import { Box as Box2, Text as Text2, useInput as useInput2, useStdout } from "ink";
|
|
3079
|
+
import { useEffect, useRef as useRef2, useState as useState2 } from "react";
|
|
2443
3080
|
|
|
2444
3081
|
// src/cli/path-suggest.ts
|
|
2445
|
-
import { readdir } from "fs/promises";
|
|
2446
|
-
import
|
|
3082
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
3083
|
+
import path18 from "path";
|
|
2447
3084
|
var MAX_SCAN_ENTRIES = 2e3;
|
|
2448
3085
|
function extractAtToken(value, cursor) {
|
|
2449
3086
|
const before = value.slice(0, cursor);
|
|
@@ -2458,8 +3095,8 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
|
|
|
2458
3095
|
const dirPart = slash >= 0 ? normalized.slice(0, slash + 1) : "";
|
|
2459
3096
|
const basePrefix = slash >= 0 ? normalized.slice(slash + 1) : normalized;
|
|
2460
3097
|
if (dirPart.split("/").some((segment) => SKIP_DIRS.has(segment))) return [];
|
|
2461
|
-
const dirAbs =
|
|
2462
|
-
const entries = await
|
|
3098
|
+
const dirAbs = path18.resolve(cwd, dirPart === "" ? "." : dirPart);
|
|
3099
|
+
const entries = await readdir2(dirAbs, { withFileTypes: true }).catch(() => null);
|
|
2463
3100
|
if (!entries) return [];
|
|
2464
3101
|
const ignore = createIgnorePredicate(cwd);
|
|
2465
3102
|
const lower = basePrefix.toLowerCase();
|
|
@@ -2473,7 +3110,7 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
|
|
|
2473
3110
|
if (isDir && SKIP_DIRS.has(entry.name)) continue;
|
|
2474
3111
|
if (entry.name.startsWith(".") && !basePrefix.startsWith(".")) continue;
|
|
2475
3112
|
if (!entry.name.toLowerCase().startsWith(lower)) continue;
|
|
2476
|
-
if (ignore?.(
|
|
3113
|
+
if (ignore?.(path18.join(dirAbs, entry.name), isDir)) continue;
|
|
2477
3114
|
matches.push({ path: `${dirPart}${entry.name}${isDir ? "/" : ""}`, isDir });
|
|
2478
3115
|
}
|
|
2479
3116
|
matches.sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -2482,6 +3119,95 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
|
|
|
2482
3119
|
|
|
2483
3120
|
// src/cli/components/InputBox.tsx
|
|
2484
3121
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
3122
|
+
function moveCursorLines(value, cursor, delta) {
|
|
3123
|
+
const lineStart = value.lastIndexOf("\n", cursor - 1) + 1;
|
|
3124
|
+
const col = cursor - lineStart;
|
|
3125
|
+
if (delta === -1) {
|
|
3126
|
+
if (lineStart === 0) return null;
|
|
3127
|
+
const prevEnd = lineStart - 1;
|
|
3128
|
+
const prevStart = value.lastIndexOf("\n", prevEnd - 1) + 1;
|
|
3129
|
+
return prevStart + Math.min(col, prevEnd - prevStart);
|
|
3130
|
+
}
|
|
3131
|
+
const lineEndIndex = value.indexOf("\n", cursor);
|
|
3132
|
+
if (lineEndIndex === -1) return null;
|
|
3133
|
+
const nextStart = lineEndIndex + 1;
|
|
3134
|
+
const nextEndIndex = value.indexOf("\n", nextStart);
|
|
3135
|
+
const nextEnd = nextEndIndex === -1 ? value.length : nextEndIndex;
|
|
3136
|
+
return nextStart + Math.min(col, nextEnd - nextStart);
|
|
3137
|
+
}
|
|
3138
|
+
function findHistoryMatch(history, query, from, step) {
|
|
3139
|
+
const needle = query.toLowerCase();
|
|
3140
|
+
for (let i = from; i >= 0 && i < history.length; i += step) {
|
|
3141
|
+
if ((history[i] ?? "").toLowerCase().includes(needle)) return i;
|
|
3142
|
+
}
|
|
3143
|
+
return null;
|
|
3144
|
+
}
|
|
3145
|
+
var PASTE_COLLAPSE_MIN_LINES = 10;
|
|
3146
|
+
var PASTE_COLLAPSE_MIN_CHARS = 500;
|
|
3147
|
+
var PASTE_MERGE_WINDOW_MS = 100;
|
|
3148
|
+
var PASTE_MERGE_MIN_CHARS = 32;
|
|
3149
|
+
var PASTE_DELIM = "\u200B";
|
|
3150
|
+
var PASTE_TOKEN_RE = /\u200B\[pasted #(\d+): \d+ lines?\]\u200B/g;
|
|
3151
|
+
var BRACKETED_PASTE_START = "\x1B[200~";
|
|
3152
|
+
var BRACKETED_PASTE_END = "\x1B[201~";
|
|
3153
|
+
var PARTIAL_BRACKET_RE = /\u001B(?:\[(?:2(?:0(?:[01])?)?)?)?$/;
|
|
3154
|
+
function feedBracketedPaste(state, raw) {
|
|
3155
|
+
let stream = state.pending + raw;
|
|
3156
|
+
state.pending = "";
|
|
3157
|
+
if (stream.startsWith("[200~") || stream.startsWith("[201~")) {
|
|
3158
|
+
stream = `\x1B${stream}`;
|
|
3159
|
+
}
|
|
3160
|
+
const segments = [];
|
|
3161
|
+
let buffer = state.buffer;
|
|
3162
|
+
let bracketed = state.active;
|
|
3163
|
+
let i = 0;
|
|
3164
|
+
const pushText = (text) => {
|
|
3165
|
+
const cleaned = text.replaceAll("\x1B", "");
|
|
3166
|
+
if (cleaned.length > 0) segments.push({ type: "text", text: cleaned });
|
|
3167
|
+
};
|
|
3168
|
+
const holdPartial = (tail) => {
|
|
3169
|
+
const partial = PARTIAL_BRACKET_RE.exec(tail);
|
|
3170
|
+
if (!partial) return tail;
|
|
3171
|
+
state.pending = partial[0];
|
|
3172
|
+
return tail.slice(0, tail.length - partial[0].length);
|
|
3173
|
+
};
|
|
3174
|
+
while (i < stream.length) {
|
|
3175
|
+
if (bracketed) {
|
|
3176
|
+
const idx = stream.indexOf(BRACKETED_PASTE_END, i);
|
|
3177
|
+
if (idx === -1) {
|
|
3178
|
+
buffer += holdPartial(stream.slice(i));
|
|
3179
|
+
i = stream.length;
|
|
3180
|
+
} else {
|
|
3181
|
+
buffer += stream.slice(i, idx);
|
|
3182
|
+
segments.push({ type: "paste", text: buffer });
|
|
3183
|
+
buffer = "";
|
|
3184
|
+
bracketed = false;
|
|
3185
|
+
i = idx + BRACKETED_PASTE_END.length;
|
|
3186
|
+
}
|
|
3187
|
+
} else {
|
|
3188
|
+
const idx = stream.indexOf(BRACKETED_PASTE_START, i);
|
|
3189
|
+
if (idx === -1) {
|
|
3190
|
+
pushText(holdPartial(stream.slice(i)));
|
|
3191
|
+
i = stream.length;
|
|
3192
|
+
} else {
|
|
3193
|
+
pushText(stream.slice(i, idx));
|
|
3194
|
+
buffer = "";
|
|
3195
|
+
bracketed = true;
|
|
3196
|
+
i = idx + BRACKETED_PASTE_START.length;
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
state.active = bracketed;
|
|
3201
|
+
state.buffer = buffer;
|
|
3202
|
+
return segments.length === 0 ? null : segments;
|
|
3203
|
+
}
|
|
3204
|
+
function pasteToken(id, lineCount) {
|
|
3205
|
+
const label = `[pasted #${id}: ${lineCount} ${lineCount === 1 ? "line" : "lines"}]`;
|
|
3206
|
+
return `${PASTE_DELIM}${label}${PASTE_DELIM}`;
|
|
3207
|
+
}
|
|
3208
|
+
function stylePasteTokens(segment) {
|
|
3209
|
+
return segment.replace(PASTE_TOKEN_RE, (match) => `\x1B[2m${match}\x1B[22m`);
|
|
3210
|
+
}
|
|
2485
3211
|
function InputBox({
|
|
2486
3212
|
isStreaming,
|
|
2487
3213
|
disabled,
|
|
@@ -2494,28 +3220,126 @@ function InputBox({
|
|
|
2494
3220
|
onExit,
|
|
2495
3221
|
onPasteImage
|
|
2496
3222
|
}) {
|
|
2497
|
-
const [value, setValue] =
|
|
2498
|
-
const [cursor, setCursor] =
|
|
2499
|
-
const [history, setHistory] =
|
|
2500
|
-
const [highlight, setHighlight] =
|
|
2501
|
-
const [suggestionsDismissed, setSuggestionsDismissed] =
|
|
2502
|
-
const [pathSuggestions, setPathSuggestions] =
|
|
2503
|
-
const historyIndexRef =
|
|
2504
|
-
const draftRef =
|
|
2505
|
-
const refillSeqRef =
|
|
3223
|
+
const [value, setValue] = useState2("");
|
|
3224
|
+
const [cursor, setCursor] = useState2(0);
|
|
3225
|
+
const [history, setHistory] = useState2(() => initialHistory ?? []);
|
|
3226
|
+
const [highlight, setHighlight] = useState2(0);
|
|
3227
|
+
const [suggestionsDismissed, setSuggestionsDismissed] = useState2(false);
|
|
3228
|
+
const [pathSuggestions, setPathSuggestions] = useState2([]);
|
|
3229
|
+
const historyIndexRef = useRef2(null);
|
|
3230
|
+
const draftRef = useRef2("");
|
|
3231
|
+
const refillSeqRef = useRef2(0);
|
|
3232
|
+
const [search, setSearch] = useState2(null);
|
|
3233
|
+
const searchSavedRef = useRef2(null);
|
|
3234
|
+
const pastesRef = useRef2(/* @__PURE__ */ new Map());
|
|
3235
|
+
const pasteSeqRef = useRef2(0);
|
|
3236
|
+
const bracketRef = useRef2({ active: false, buffer: "", pending: "" });
|
|
3237
|
+
const lastPasteRef = useRef2(
|
|
3238
|
+
null
|
|
3239
|
+
);
|
|
3240
|
+
const valueRef = useRef2(value);
|
|
3241
|
+
const cursorRef = useRef2(cursor);
|
|
3242
|
+
useEffect(() => {
|
|
3243
|
+
valueRef.current = value;
|
|
3244
|
+
cursorRef.current = cursor;
|
|
3245
|
+
});
|
|
3246
|
+
const { stdout } = useStdout();
|
|
3247
|
+
useEffect(() => {
|
|
3248
|
+
if (!stdout?.isTTY) return;
|
|
3249
|
+
stdout.write("\x1B[?2004h");
|
|
3250
|
+
return () => {
|
|
3251
|
+
stdout.write("\x1B[?2004l");
|
|
3252
|
+
};
|
|
3253
|
+
}, [stdout]);
|
|
3254
|
+
const expandPastes = (raw) => raw.replace(PASTE_TOKEN_RE, (match, id) => pastesRef.current.get(Number(id)) ?? match);
|
|
3255
|
+
const clearPastes = () => {
|
|
3256
|
+
pastesRef.current.clear();
|
|
3257
|
+
pasteSeqRef.current = 0;
|
|
3258
|
+
lastPasteRef.current = null;
|
|
3259
|
+
const bracket = bracketRef.current;
|
|
3260
|
+
bracket.active = false;
|
|
3261
|
+
bracket.buffer = "";
|
|
3262
|
+
bracket.pending = "";
|
|
3263
|
+
};
|
|
2506
3264
|
const edit = (next, nextCursor) => {
|
|
3265
|
+
const idx = historyIndexRef.current;
|
|
3266
|
+
if (idx !== null && next !== history[idx]) historyIndexRef.current = null;
|
|
3267
|
+
const clamped = Math.max(0, Math.min(nextCursor, next.length));
|
|
3268
|
+
valueRef.current = next;
|
|
3269
|
+
cursorRef.current = clamped;
|
|
2507
3270
|
setValue(next);
|
|
2508
|
-
setCursor(
|
|
3271
|
+
setCursor(clamped);
|
|
2509
3272
|
setHighlight(0);
|
|
2510
3273
|
setSuggestionsDismissed(false);
|
|
2511
3274
|
};
|
|
3275
|
+
const normalizeNewlines = (text) => text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
3276
|
+
const collapseOrInsert = (text) => {
|
|
3277
|
+
const currentValue = valueRef.current;
|
|
3278
|
+
const currentCursor = cursorRef.current;
|
|
3279
|
+
const lineCount = text.split("\n").length;
|
|
3280
|
+
if (lineCount >= PASTE_COLLAPSE_MIN_LINES || text.length >= PASTE_COLLAPSE_MIN_CHARS) {
|
|
3281
|
+
const id = ++pasteSeqRef.current;
|
|
3282
|
+
pastesRef.current.set(id, text);
|
|
3283
|
+
const token = pasteToken(id, lineCount);
|
|
3284
|
+
const nextCursor = currentCursor + token.length;
|
|
3285
|
+
edit(
|
|
3286
|
+
currentValue.slice(0, currentCursor) + token + currentValue.slice(currentCursor),
|
|
3287
|
+
nextCursor
|
|
3288
|
+
);
|
|
3289
|
+
lastPasteRef.current = { id, token, cursor: nextCursor, time: Date.now() };
|
|
3290
|
+
return;
|
|
3291
|
+
}
|
|
3292
|
+
edit(
|
|
3293
|
+
currentValue.slice(0, currentCursor) + text + currentValue.slice(currentCursor),
|
|
3294
|
+
currentCursor + text.length
|
|
3295
|
+
);
|
|
3296
|
+
};
|
|
3297
|
+
const insertPlainText = (rawText) => {
|
|
3298
|
+
const text = normalizeNewlines(rawText);
|
|
3299
|
+
const currentValue = valueRef.current;
|
|
3300
|
+
const currentCursor = cursorRef.current;
|
|
3301
|
+
const last = lastPasteRef.current;
|
|
3302
|
+
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)) {
|
|
3303
|
+
const merged = (pastesRef.current.get(last.id) ?? "") + text;
|
|
3304
|
+
pastesRef.current.set(last.id, merged);
|
|
3305
|
+
const token = pasteToken(last.id, merged.split("\n").length);
|
|
3306
|
+
const start = currentCursor - last.token.length;
|
|
3307
|
+
const nextCursor = start + token.length;
|
|
3308
|
+
edit(currentValue.slice(0, start) + token + currentValue.slice(currentCursor), nextCursor);
|
|
3309
|
+
lastPasteRef.current = { id: last.id, token, cursor: nextCursor, time: Date.now() };
|
|
3310
|
+
return;
|
|
3311
|
+
}
|
|
3312
|
+
collapseOrInsert(text);
|
|
3313
|
+
};
|
|
3314
|
+
const flushBracketPaste = () => {
|
|
3315
|
+
const bracket = bracketRef.current;
|
|
3316
|
+
const content = bracket.buffer;
|
|
3317
|
+
bracket.active = false;
|
|
3318
|
+
bracket.buffer = "";
|
|
3319
|
+
bracket.pending = "";
|
|
3320
|
+
if (content.length > 0) collapseOrInsert(normalizeNewlines(content));
|
|
3321
|
+
};
|
|
3322
|
+
const applyPasteSegments = (segments) => {
|
|
3323
|
+
if (search) {
|
|
3324
|
+
const query = search.query + normalizeNewlines(segments.map((seg) => seg.text).join(""));
|
|
3325
|
+
setSearch({ query, match: findHistoryMatch(history, query, history.length - 1, -1) });
|
|
3326
|
+
return;
|
|
3327
|
+
}
|
|
3328
|
+
for (const seg of segments) {
|
|
3329
|
+
if (seg.type === "paste") {
|
|
3330
|
+
collapseOrInsert(normalizeNewlines(seg.text));
|
|
3331
|
+
} else {
|
|
3332
|
+
insertPlainText(seg.text);
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
};
|
|
2512
3336
|
useEffect(() => {
|
|
2513
3337
|
if (refill && refill.seq !== refillSeqRef.current) {
|
|
2514
3338
|
refillSeqRef.current = refill.seq;
|
|
2515
3339
|
edit(refill.text, refill.text.length);
|
|
2516
3340
|
}
|
|
2517
3341
|
});
|
|
2518
|
-
const suggestionsEnabled = !isStreaming && !disabled && !suggestionsDismissed;
|
|
3342
|
+
const suggestionsEnabled = !isStreaming && !disabled && !suggestionsDismissed && !search;
|
|
2519
3343
|
const suggestions = suggestionsEnabled && commands ? filterCommands(value, commands) : [];
|
|
2520
3344
|
useEffect(() => {
|
|
2521
3345
|
if (!suggestionsEnabled || !cwd) {
|
|
@@ -2556,11 +3380,87 @@ function InputBox({
|
|
|
2556
3380
|
);
|
|
2557
3381
|
};
|
|
2558
3382
|
useInput2((input, key) => {
|
|
3383
|
+
if (!disabled) {
|
|
3384
|
+
const bracket = bracketRef.current;
|
|
3385
|
+
const feedable = input.length > 0 && !key.ctrl && !key.meta;
|
|
3386
|
+
if (bracket.active && (!feedable || input === "\r")) {
|
|
3387
|
+
flushBracketPaste();
|
|
3388
|
+
if (feedable) return;
|
|
3389
|
+
}
|
|
3390
|
+
if (feedable) {
|
|
3391
|
+
const segments = feedBracketedPaste(bracket, input);
|
|
3392
|
+
if (segments === null) return;
|
|
3393
|
+
const only = segments.length === 1 ? segments[0] : void 0;
|
|
3394
|
+
if (!only || only.type !== "text" || only.text !== input) {
|
|
3395
|
+
applyPasteSegments(segments);
|
|
3396
|
+
return;
|
|
3397
|
+
}
|
|
3398
|
+
} else {
|
|
3399
|
+
bracket.pending = "";
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
if (key.ctrl && input === "r") {
|
|
3403
|
+
if (isStreaming || disabled) return;
|
|
3404
|
+
if (search) {
|
|
3405
|
+
const from = (search.match ?? history.length) - 1;
|
|
3406
|
+
const older = findHistoryMatch(history, search.query, from, -1);
|
|
3407
|
+
if (older !== null) setSearch({ ...search, match: older });
|
|
3408
|
+
} else {
|
|
3409
|
+
searchSavedRef.current = { value, cursor };
|
|
3410
|
+
setSearch({ query: "", match: history.length > 0 ? history.length - 1 : null });
|
|
3411
|
+
}
|
|
3412
|
+
return;
|
|
3413
|
+
}
|
|
3414
|
+
if (search) {
|
|
3415
|
+
const exitSearch = (accept) => {
|
|
3416
|
+
const saved = searchSavedRef.current;
|
|
3417
|
+
setSearch(null);
|
|
3418
|
+
searchSavedRef.current = null;
|
|
3419
|
+
const match = accept && search.match !== null ? history[search.match] : void 0;
|
|
3420
|
+
if (match !== void 0) {
|
|
3421
|
+
edit(match, match.length);
|
|
3422
|
+
} else if (saved) {
|
|
3423
|
+
edit(saved.value, saved.cursor);
|
|
3424
|
+
}
|
|
3425
|
+
};
|
|
3426
|
+
if (key.escape || key.ctrl && (input === "c" || input === "g")) {
|
|
3427
|
+
exitSearch(false);
|
|
3428
|
+
return;
|
|
3429
|
+
}
|
|
3430
|
+
if (key.return) {
|
|
3431
|
+
exitSearch(true);
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
3434
|
+
const requery = (query) => setSearch({ query, match: findHistoryMatch(history, query, history.length - 1, -1) });
|
|
3435
|
+
if (key.backspace || key.delete) {
|
|
3436
|
+
requery(search.query.slice(0, -1));
|
|
3437
|
+
return;
|
|
3438
|
+
}
|
|
3439
|
+
if (key.upArrow) {
|
|
3440
|
+
const from = (search.match ?? history.length) - 1;
|
|
3441
|
+
const older = findHistoryMatch(history, search.query, from, -1);
|
|
3442
|
+
if (older !== null) setSearch({ ...search, match: older });
|
|
3443
|
+
return;
|
|
3444
|
+
}
|
|
3445
|
+
if (key.downArrow) {
|
|
3446
|
+
if (search.match !== null) {
|
|
3447
|
+
const newer = findHistoryMatch(history, search.query, search.match + 1, 1);
|
|
3448
|
+
if (newer !== null) setSearch({ ...search, match: newer });
|
|
3449
|
+
}
|
|
3450
|
+
return;
|
|
3451
|
+
}
|
|
3452
|
+
if (input && !key.ctrl && !key.meta) {
|
|
3453
|
+
const text = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
3454
|
+
requery(search.query + text);
|
|
3455
|
+
}
|
|
3456
|
+
return;
|
|
3457
|
+
}
|
|
2559
3458
|
if (key.ctrl && input === "c") {
|
|
2560
3459
|
if (isStreaming || disabled) {
|
|
2561
3460
|
onInterrupt();
|
|
2562
3461
|
} else {
|
|
2563
3462
|
edit("", 0);
|
|
3463
|
+
clearPastes();
|
|
2564
3464
|
historyIndexRef.current = null;
|
|
2565
3465
|
}
|
|
2566
3466
|
return;
|
|
@@ -2579,27 +3479,28 @@ function InputBox({
|
|
|
2579
3479
|
return;
|
|
2580
3480
|
}
|
|
2581
3481
|
if (key.return) {
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
3482
|
+
const currentValue = valueRef.current;
|
|
3483
|
+
const currentCursor = cursorRef.current;
|
|
3484
|
+
if (currentValue[currentCursor - 1] === "\\") {
|
|
3485
|
+
edit(
|
|
3486
|
+
`${currentValue.slice(0, currentCursor - 1)}
|
|
3487
|
+
${currentValue.slice(currentCursor)}`,
|
|
3488
|
+
currentCursor
|
|
3489
|
+
);
|
|
2590
3490
|
return;
|
|
2591
3491
|
}
|
|
2592
|
-
const text =
|
|
3492
|
+
const text = expandPastes(currentValue).trim();
|
|
2593
3493
|
if (text.length > 0) {
|
|
2594
|
-
setHistory((prev) => [...prev, text]);
|
|
3494
|
+
if (!text.startsWith("/")) setHistory((prev) => [...prev, text]);
|
|
2595
3495
|
onSubmit(text);
|
|
2596
3496
|
}
|
|
2597
3497
|
edit("", 0);
|
|
3498
|
+
clearPastes();
|
|
2598
3499
|
historyIndexRef.current = null;
|
|
2599
3500
|
draftRef.current = "";
|
|
2600
3501
|
return;
|
|
2601
3502
|
}
|
|
2602
|
-
if (input === "\n" || input === "\r"
|
|
3503
|
+
if (input === "\n" || input === "\r") {
|
|
2603
3504
|
edit(`${value.slice(0, cursor)}
|
|
2604
3505
|
${value.slice(cursor)}`, cursor + 1);
|
|
2605
3506
|
return;
|
|
@@ -2611,34 +3512,47 @@ ${value.slice(cursor)}`, cursor + 1);
|
|
|
2611
3512
|
return;
|
|
2612
3513
|
}
|
|
2613
3514
|
} else if (key.upArrow) {
|
|
3515
|
+
if (historyIndexRef.current !== null) {
|
|
3516
|
+
if (historyIndexRef.current > 0) {
|
|
3517
|
+
historyIndexRef.current -= 1;
|
|
3518
|
+
const entry2 = history[historyIndexRef.current] ?? "";
|
|
3519
|
+
edit(entry2, entry2.length);
|
|
3520
|
+
}
|
|
3521
|
+
return;
|
|
3522
|
+
}
|
|
2614
3523
|
if (suggestionCount > 0) {
|
|
2615
3524
|
setHighlight((prev) => (prev - 1 + suggestionCount) % suggestionCount);
|
|
2616
3525
|
return;
|
|
2617
3526
|
}
|
|
2618
|
-
|
|
2619
|
-
if (
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
} else if (historyIndexRef.current > 0) {
|
|
2623
|
-
historyIndexRef.current -= 1;
|
|
3527
|
+
const moved = moveCursorLines(value, cursor, -1);
|
|
3528
|
+
if (moved !== null) {
|
|
3529
|
+
setCursor(moved);
|
|
3530
|
+
return;
|
|
2624
3531
|
}
|
|
3532
|
+
if (history.length === 0) return;
|
|
3533
|
+
draftRef.current = value;
|
|
3534
|
+
historyIndexRef.current = history.length - 1;
|
|
2625
3535
|
const entry = history[historyIndexRef.current] ?? "";
|
|
2626
3536
|
edit(entry, entry.length);
|
|
2627
3537
|
return;
|
|
2628
3538
|
} else if (key.downArrow) {
|
|
3539
|
+
if (historyIndexRef.current !== null) {
|
|
3540
|
+
if (historyIndexRef.current < history.length - 1) {
|
|
3541
|
+
historyIndexRef.current += 1;
|
|
3542
|
+
const entry = history[historyIndexRef.current] ?? "";
|
|
3543
|
+
edit(entry, entry.length);
|
|
3544
|
+
} else {
|
|
3545
|
+
historyIndexRef.current = null;
|
|
3546
|
+
edit(draftRef.current, draftRef.current.length);
|
|
3547
|
+
}
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
2629
3550
|
if (suggestionCount > 0) {
|
|
2630
3551
|
setHighlight((prev) => (prev + 1) % suggestionCount);
|
|
2631
3552
|
return;
|
|
2632
3553
|
}
|
|
2633
|
-
|
|
2634
|
-
if (
|
|
2635
|
-
historyIndexRef.current += 1;
|
|
2636
|
-
const entry = history[historyIndexRef.current] ?? "";
|
|
2637
|
-
edit(entry, entry.length);
|
|
2638
|
-
} else {
|
|
2639
|
-
historyIndexRef.current = null;
|
|
2640
|
-
edit(draftRef.current, draftRef.current.length);
|
|
2641
|
-
}
|
|
3554
|
+
const moved = moveCursorLines(value, cursor, 1);
|
|
3555
|
+
if (moved !== null) setCursor(moved);
|
|
2642
3556
|
return;
|
|
2643
3557
|
} else if (key.leftArrow) {
|
|
2644
3558
|
setCursor((prev) => Math.max(0, prev - 1));
|
|
@@ -2676,20 +3590,29 @@ ${value.slice(cursor)}`, cursor + 1);
|
|
|
2676
3590
|
}
|
|
2677
3591
|
if (key.backspace || key.delete) {
|
|
2678
3592
|
if (cursor === 0) return;
|
|
2679
|
-
|
|
3593
|
+
let remove = 1;
|
|
3594
|
+
for (const match of value.matchAll(PASTE_TOKEN_RE)) {
|
|
3595
|
+
if (match.index + match[0].length === cursor) {
|
|
3596
|
+
remove = match[0].length;
|
|
3597
|
+
const id = Number(match[1]);
|
|
3598
|
+
pastesRef.current.delete(id);
|
|
3599
|
+
if (lastPasteRef.current?.id === id) lastPasteRef.current = null;
|
|
3600
|
+
break;
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
edit(value.slice(0, cursor - remove) + value.slice(cursor), cursor - remove);
|
|
2680
3604
|
return;
|
|
2681
3605
|
}
|
|
2682
3606
|
if (input && !key.ctrl && !key.meta) {
|
|
2683
|
-
|
|
2684
|
-
edit(value.slice(0, cursor) + text + value.slice(cursor), cursor + text.length);
|
|
3607
|
+
insertPlainText(input);
|
|
2685
3608
|
}
|
|
2686
3609
|
});
|
|
2687
3610
|
const before = value.slice(0, cursor);
|
|
2688
3611
|
const at = value[cursor];
|
|
2689
3612
|
const after = value.slice(cursor + 1);
|
|
2690
|
-
const cursorChar = at === "\n" || at === void 0 ? " " : at;
|
|
2691
|
-
const tail = (at === "\n" ? "\n" : "") + after;
|
|
2692
|
-
const styled = `\x1B[36m> \x1B[39m${before}\x1B[7m${cursorChar}\x1B[27m${tail}`;
|
|
3613
|
+
const cursorChar = at === "\n" || at === void 0 || at === PASTE_DELIM ? " " : at;
|
|
3614
|
+
const tail = (at === "\n" ? "\n" : "") + stylePasteTokens(after);
|
|
3615
|
+
const styled = search ? `\x1B[36m(reverse-search) \x1B[39m'${search.query}': ${search.match === null ? "\x1B[2mno match\x1B[22m\x1B[7m \x1B[27m" : `${history[search.match]}\x1B[7m \x1B[27m`}` : `\x1B[36m> \x1B[39m${stylePasteTokens(before)}\x1B[7m${cursorChar}\x1B[27m${tail}`;
|
|
2693
3616
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
2694
3617
|
/* @__PURE__ */ jsx2(Box2, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { children: styled }) }),
|
|
2695
3618
|
suggestions.length > 0 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: suggestions.map((cmd, index) => {
|
|
@@ -2708,175 +3631,422 @@ ${value.slice(cursor)}`, cursor + 1);
|
|
|
2708
3631
|
] });
|
|
2709
3632
|
}
|
|
2710
3633
|
|
|
3634
|
+
// src/cli/components/TextField.tsx
|
|
3635
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
3636
|
+
var MASK_CAP = 24;
|
|
3637
|
+
function TextField({
|
|
3638
|
+
label,
|
|
3639
|
+
initialValue = "",
|
|
3640
|
+
masked = false,
|
|
3641
|
+
placeholder,
|
|
3642
|
+
onSubmit,
|
|
3643
|
+
onCancel
|
|
3644
|
+
}) {
|
|
3645
|
+
const [value, setValue] = useState3(initialValue);
|
|
3646
|
+
const valueRef = useRef3(initialValue);
|
|
3647
|
+
const bracketRef = useRef3({ active: false, buffer: "", pending: "" });
|
|
3648
|
+
const flushBracket = () => {
|
|
3649
|
+
const bracket = bracketRef.current;
|
|
3650
|
+
const content = bracket.buffer.replace(/[\r\n]+/g, "");
|
|
3651
|
+
bracket.active = false;
|
|
3652
|
+
bracket.buffer = "";
|
|
3653
|
+
bracket.pending = "";
|
|
3654
|
+
if (content.length > 0) {
|
|
3655
|
+
valueRef.current += content;
|
|
3656
|
+
setValue(valueRef.current);
|
|
3657
|
+
}
|
|
3658
|
+
};
|
|
3659
|
+
useInput3((input, key) => {
|
|
3660
|
+
if (key.escape) {
|
|
3661
|
+
onCancel();
|
|
3662
|
+
return;
|
|
3663
|
+
}
|
|
3664
|
+
if (key.return) {
|
|
3665
|
+
flushBracket();
|
|
3666
|
+
onSubmit(valueRef.current.trim());
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
if (key.backspace || key.delete) {
|
|
3670
|
+
flushBracket();
|
|
3671
|
+
valueRef.current = valueRef.current.slice(0, -1);
|
|
3672
|
+
setValue(valueRef.current);
|
|
3673
|
+
return;
|
|
3674
|
+
}
|
|
3675
|
+
if (key.ctrl || key.meta || !input) return;
|
|
3676
|
+
const segments = feedBracketedPaste(bracketRef.current, input);
|
|
3677
|
+
if (segments === null) return;
|
|
3678
|
+
const text = segments.map((segment) => segment.text).join("").replace(/[\r\n]+/g, "");
|
|
3679
|
+
if (text.length === 0) return;
|
|
3680
|
+
valueRef.current += text;
|
|
3681
|
+
setValue(valueRef.current);
|
|
3682
|
+
});
|
|
3683
|
+
const echo = masked ? "\u2022".repeat(Math.min(value.length, MASK_CAP)) : value;
|
|
3684
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
3685
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
3686
|
+
label,
|
|
3687
|
+
" ",
|
|
3688
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: echo }),
|
|
3689
|
+
value === "" && placeholder ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: placeholder }) : null
|
|
3690
|
+
] }),
|
|
3691
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "[Enter] confirm [Esc] cancel" })
|
|
3692
|
+
] });
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
// src/cli/components/ConnectWizard.tsx
|
|
3696
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
3697
|
+
function YN({ question, onAnswer }) {
|
|
3698
|
+
useInput4((input, key) => {
|
|
3699
|
+
const ch = input.toLowerCase();
|
|
3700
|
+
if (ch === "y") onAnswer(true);
|
|
3701
|
+
else if (ch === "n" || key.escape) onAnswer(false);
|
|
3702
|
+
});
|
|
3703
|
+
return /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
3704
|
+
question,
|
|
3705
|
+
" ",
|
|
3706
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "[y] yes [n] no" })
|
|
3707
|
+
] });
|
|
3708
|
+
}
|
|
3709
|
+
function ConnectWizard({
|
|
3710
|
+
existingProviderNames,
|
|
3711
|
+
existingModelNames,
|
|
3712
|
+
onSave,
|
|
3713
|
+
onSetDefault,
|
|
3714
|
+
onOpenConfig,
|
|
3715
|
+
onFinish,
|
|
3716
|
+
onCancel
|
|
3717
|
+
}) {
|
|
3718
|
+
const [step, setStep] = useState4("provider");
|
|
3719
|
+
const [preset, setPreset] = useState4(null);
|
|
3720
|
+
const [protocol, setProtocol] = useState4("openai-compatible");
|
|
3721
|
+
const [baseURL, setBaseURL] = useState4("");
|
|
3722
|
+
const [apiKey, setApiKey] = useState4("");
|
|
3723
|
+
const [modelId, setModelId] = useState4("");
|
|
3724
|
+
const [answers, setAnswers] = useState4(null);
|
|
3725
|
+
const [error, setError] = useState4(null);
|
|
3726
|
+
const [busy, setBusy] = useState4(false);
|
|
3727
|
+
const [notes, setNotes] = useState4([]);
|
|
3728
|
+
const busyRef = useRef4(false);
|
|
3729
|
+
const notesRef = useRef4([]);
|
|
3730
|
+
const pushNote = (note) => {
|
|
3731
|
+
notesRef.current = [...notesRef.current, note];
|
|
3732
|
+
setNotes(notesRef.current);
|
|
3733
|
+
};
|
|
3734
|
+
const providerOptions = [
|
|
3735
|
+
...PROVIDER_PRESETS.map((p) => ({ value: p.key, label: p.label, description: p.baseURL })),
|
|
3736
|
+
{
|
|
3737
|
+
value: "custom",
|
|
3738
|
+
label: "Custom",
|
|
3739
|
+
description: "any OpenAI/Anthropic-compatible endpoint"
|
|
3740
|
+
}
|
|
3741
|
+
];
|
|
3742
|
+
const handleProviderSelect = (value) => {
|
|
3743
|
+
const found = PROVIDER_PRESETS.find((p) => p.key === value) ?? null;
|
|
3744
|
+
setPreset(found);
|
|
3745
|
+
if (found) {
|
|
3746
|
+
setProtocol(found.protocol);
|
|
3747
|
+
setBaseURL(found.baseURL);
|
|
3748
|
+
setStep("baseUrl");
|
|
3749
|
+
} else {
|
|
3750
|
+
setStep("protocol");
|
|
3751
|
+
}
|
|
3752
|
+
};
|
|
3753
|
+
const handleProtocolSelect = (value) => {
|
|
3754
|
+
setProtocol(value);
|
|
3755
|
+
setStep("baseUrl");
|
|
3756
|
+
};
|
|
3757
|
+
const submitBaseURL = (value) => {
|
|
3758
|
+
if (!value) {
|
|
3759
|
+
setError("baseURL is required");
|
|
3760
|
+
return;
|
|
3761
|
+
}
|
|
3762
|
+
setError(null);
|
|
3763
|
+
setBaseURL(value);
|
|
3764
|
+
setStep("apiKey");
|
|
3765
|
+
};
|
|
3766
|
+
const submitApiKey = (value) => {
|
|
3767
|
+
if (!value) {
|
|
3768
|
+
setError("API key is required");
|
|
3769
|
+
return;
|
|
3770
|
+
}
|
|
3771
|
+
setError(null);
|
|
3772
|
+
setApiKey(value);
|
|
3773
|
+
setStep("model");
|
|
3774
|
+
};
|
|
3775
|
+
const submitModel = (value) => {
|
|
3776
|
+
if (!value) {
|
|
3777
|
+
setError("model id is required");
|
|
3778
|
+
return;
|
|
3779
|
+
}
|
|
3780
|
+
setError(null);
|
|
3781
|
+
setModelId(value);
|
|
3782
|
+
const providerName = dedupeName(deriveProviderName(baseURL), existingProviderNames);
|
|
3783
|
+
setAnswers({
|
|
3784
|
+
providerName,
|
|
3785
|
+
protocol,
|
|
3786
|
+
baseURL,
|
|
3787
|
+
apiKeyEnv: apiKeyEnvFor(providerName),
|
|
3788
|
+
apiKey,
|
|
3789
|
+
modelName: dedupeName(value, existingModelNames),
|
|
3790
|
+
modelId: value
|
|
3791
|
+
});
|
|
3792
|
+
setStep("confirm");
|
|
3793
|
+
};
|
|
3794
|
+
const handleConfirm = async (yes) => {
|
|
3795
|
+
if (!yes) {
|
|
3796
|
+
onCancel();
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3799
|
+
if (busyRef.current || !answers) return;
|
|
3800
|
+
busyRef.current = true;
|
|
3801
|
+
setBusy(true);
|
|
3802
|
+
try {
|
|
3803
|
+
pushNote(await onSave(answers));
|
|
3804
|
+
} catch (saveError) {
|
|
3805
|
+
busyRef.current = false;
|
|
3806
|
+
setBusy(false);
|
|
3807
|
+
setError(
|
|
3808
|
+
`Save failed: ${saveError instanceof Error ? saveError.message : String(saveError)}`
|
|
3809
|
+
);
|
|
3810
|
+
return;
|
|
3811
|
+
}
|
|
3812
|
+
busyRef.current = false;
|
|
3813
|
+
setBusy(false);
|
|
3814
|
+
setStep("setDefault");
|
|
3815
|
+
};
|
|
3816
|
+
const handleSetDefault = async (yes) => {
|
|
3817
|
+
if (busyRef.current || !answers) return;
|
|
3818
|
+
busyRef.current = true;
|
|
3819
|
+
setBusy(true);
|
|
3820
|
+
try {
|
|
3821
|
+
pushNote(yes ? await onSetDefault(answers.modelName) : "Default model unchanged.");
|
|
3822
|
+
} catch (setDefaultError) {
|
|
3823
|
+
pushNote(
|
|
3824
|
+
`Could not set the default model: ${setDefaultError instanceof Error ? setDefaultError.message : String(setDefaultError)}`
|
|
3825
|
+
);
|
|
3826
|
+
}
|
|
3827
|
+
busyRef.current = false;
|
|
3828
|
+
setBusy(false);
|
|
3829
|
+
setStep("openFile");
|
|
3830
|
+
};
|
|
3831
|
+
const handleOpenFile = (yes) => {
|
|
3832
|
+
if (yes) pushNote(onOpenConfig());
|
|
3833
|
+
onFinish(notesRef.current.join("\n"));
|
|
3834
|
+
};
|
|
3835
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
3836
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "Connect a provider" }),
|
|
3837
|
+
notes.map((note, index) => (
|
|
3838
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: notes are append-only
|
|
3839
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: note }, index)
|
|
3840
|
+
)),
|
|
3841
|
+
step === "provider" && /* @__PURE__ */ jsx4(
|
|
3842
|
+
SelectPrompt,
|
|
3843
|
+
{
|
|
3844
|
+
title: "Which provider?",
|
|
3845
|
+
options: providerOptions,
|
|
3846
|
+
onSelect: handleProviderSelect,
|
|
3847
|
+
onCancel
|
|
3848
|
+
}
|
|
3849
|
+
),
|
|
3850
|
+
step === "protocol" && /* @__PURE__ */ jsx4(
|
|
3851
|
+
SelectPrompt,
|
|
3852
|
+
{
|
|
3853
|
+
title: "API format (protocol)?",
|
|
3854
|
+
options: PROTOCOLS.map((p) => ({
|
|
3855
|
+
value: p,
|
|
3856
|
+
label: p,
|
|
3857
|
+
description: PROTOCOL_DESCRIPTIONS[p]
|
|
3858
|
+
})),
|
|
3859
|
+
onSelect: handleProtocolSelect,
|
|
3860
|
+
onCancel
|
|
3861
|
+
}
|
|
3862
|
+
),
|
|
3863
|
+
step === "baseUrl" && /* @__PURE__ */ jsx4(
|
|
3864
|
+
TextField,
|
|
3865
|
+
{
|
|
3866
|
+
label: "baseURL:",
|
|
3867
|
+
initialValue: preset?.baseURL ?? "",
|
|
3868
|
+
placeholder: "https://api.example.com/v1",
|
|
3869
|
+
onSubmit: submitBaseURL,
|
|
3870
|
+
onCancel
|
|
3871
|
+
}
|
|
3872
|
+
),
|
|
3873
|
+
step === "apiKey" && /* @__PURE__ */ jsx4(
|
|
3874
|
+
TextField,
|
|
3875
|
+
{
|
|
3876
|
+
label: "API key:",
|
|
3877
|
+
masked: true,
|
|
3878
|
+
placeholder: "paste your key (hidden)",
|
|
3879
|
+
onSubmit: submitApiKey,
|
|
3880
|
+
onCancel
|
|
3881
|
+
}
|
|
3882
|
+
),
|
|
3883
|
+
step === "model" && /* @__PURE__ */ jsx4(
|
|
3884
|
+
TextField,
|
|
3885
|
+
{
|
|
3886
|
+
label: "Model id:",
|
|
3887
|
+
placeholder: preset?.modelHint ?? "e.g. gpt-4o",
|
|
3888
|
+
onSubmit: submitModel,
|
|
3889
|
+
onCancel
|
|
3890
|
+
}
|
|
3891
|
+
),
|
|
3892
|
+
step === "confirm" && answers && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
3893
|
+
/* @__PURE__ */ jsx4(Text4, { children: ` provider: ${answers.providerName} (${answers.protocol})` }),
|
|
3894
|
+
/* @__PURE__ */ jsx4(Text4, { children: ` baseURL: ${answers.baseURL}` }),
|
|
3895
|
+
/* @__PURE__ */ jsx4(Text4, { children: ` API key: ${maskApiKey(answers.apiKey)} \u2192 ${answers.apiKeyEnv} (stored in ~/.star-cli/.env)` }),
|
|
3896
|
+
/* @__PURE__ */ jsx4(Text4, { children: ` model: ${answers.modelName} \u2192 ${answers.modelId}` }),
|
|
3897
|
+
busy ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Saving\u2026" }) : /* @__PURE__ */ jsx4(YN, { question: "Save this provider and model?", onAnswer: handleConfirm })
|
|
3898
|
+
] }),
|
|
3899
|
+
step === "setDefault" && (busy ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Saving\u2026" }) : /* @__PURE__ */ jsx4(
|
|
3900
|
+
YN,
|
|
3901
|
+
{
|
|
3902
|
+
question: `Set "${answers?.modelName}" as the default model and switch to it?`,
|
|
3903
|
+
onAnswer: handleSetDefault
|
|
3904
|
+
}
|
|
3905
|
+
)),
|
|
3906
|
+
step === "openFile" && /* @__PURE__ */ jsx4(YN, { question: "Open the config file now?", onAnswer: handleOpenFile }),
|
|
3907
|
+
error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error })
|
|
3908
|
+
] });
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
// src/cli/components/ExitPlanPrompt.tsx
|
|
3912
|
+
import { Box as Box5, Text as Text5, useInput as useInput5 } from "ink";
|
|
3913
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
3914
|
+
function ExitPlanPrompt({ onDecision }) {
|
|
3915
|
+
useInput5((input, key) => {
|
|
3916
|
+
const ch = input.toLowerCase();
|
|
3917
|
+
if (ch === "y") onDecision("yes");
|
|
3918
|
+
else if (ch === "n" || key.escape) onDecision("no");
|
|
3919
|
+
});
|
|
3920
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
3921
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "Plan ready (plan mode)" }),
|
|
3922
|
+
/* @__PURE__ */ jsx5(Text5, { children: "The plan above was produced without making any changes." }),
|
|
3923
|
+
/* @__PURE__ */ jsx5(Text5, { children: "[y] approve and execute [n] keep refining (Esc = n)" })
|
|
3924
|
+
] });
|
|
3925
|
+
}
|
|
3926
|
+
|
|
2711
3927
|
// src/cli/components/MessageList.tsx
|
|
2712
|
-
import { Box as
|
|
3928
|
+
import { Box as Box6, Static, Text as Text7 } from "ink";
|
|
2713
3929
|
import { memo } from "react";
|
|
2714
3930
|
|
|
2715
3931
|
// src/cli/components/DiffLines.tsx
|
|
2716
|
-
import { Text as
|
|
2717
|
-
import { Fragment, jsx as
|
|
3932
|
+
import { Text as Text6 } from "ink";
|
|
3933
|
+
import { Fragment, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2718
3934
|
function DiffLineView({ line }) {
|
|
2719
|
-
if (line.kind === "add") return /* @__PURE__ */
|
|
3935
|
+
if (line.kind === "add") return /* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
|
|
2720
3936
|
"+ ",
|
|
2721
3937
|
line.text
|
|
2722
3938
|
] });
|
|
2723
|
-
if (line.kind === "del") return /* @__PURE__ */
|
|
3939
|
+
if (line.kind === "del") return /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
|
|
2724
3940
|
"- ",
|
|
2725
3941
|
line.text
|
|
2726
3942
|
] });
|
|
2727
|
-
if (line.kind === "marker") return /* @__PURE__ */
|
|
2728
|
-
return /* @__PURE__ */
|
|
3943
|
+
if (line.kind === "marker") return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: line.text });
|
|
3944
|
+
return /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
2729
3945
|
" ",
|
|
2730
3946
|
line.text
|
|
2731
3947
|
] });
|
|
2732
3948
|
}
|
|
2733
3949
|
function DiffLines({ lines }) {
|
|
2734
|
-
return /* @__PURE__ */
|
|
3950
|
+
return /* @__PURE__ */ jsx6(Fragment, { children: lines.map((line, index) => (
|
|
2735
3951
|
// biome-ignore lint/suspicious/noArrayIndexKey: diff lines have no stable identity
|
|
2736
|
-
/* @__PURE__ */
|
|
3952
|
+
/* @__PURE__ */ jsx6(DiffLineView, { line }, index)
|
|
2737
3953
|
)) });
|
|
2738
3954
|
}
|
|
2739
3955
|
|
|
2740
3956
|
// src/cli/components/MessageList.tsx
|
|
2741
|
-
import { jsx as
|
|
3957
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2742
3958
|
var roleStyles = {
|
|
2743
3959
|
user: { label: "you", color: "cyan" },
|
|
2744
3960
|
assistant: { label: "star", color: "green" },
|
|
2745
3961
|
system: { label: "system", color: "yellow" },
|
|
2746
3962
|
tool: { label: "tool", color: "magenta" }
|
|
2747
3963
|
};
|
|
3964
|
+
var interruptedMarker = (text) => /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: text.length > 0 ? " [interrupted]" : "[interrupted]" });
|
|
2748
3965
|
var MessageList = memo(function MessageList2({ messages }) {
|
|
2749
|
-
return /* @__PURE__ */
|
|
3966
|
+
return /* @__PURE__ */ jsx7(Static, { items: messages, children: (message) => {
|
|
2750
3967
|
const marginBottom = message.tight ? 0 : 1;
|
|
2751
3968
|
if (message.role === "assistant-cont") {
|
|
2752
|
-
return /* @__PURE__ */
|
|
3969
|
+
return /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", marginBottom, children: /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
|
|
3970
|
+
message.text,
|
|
3971
|
+
message.interrupted && interruptedMarker(message.text)
|
|
3972
|
+
] }) }, message.id);
|
|
2753
3973
|
}
|
|
2754
3974
|
const style = roleStyles[message.role];
|
|
2755
|
-
return /* @__PURE__ */
|
|
2756
|
-
/* @__PURE__ */
|
|
2757
|
-
/* @__PURE__ */
|
|
2758
|
-
|
|
2759
|
-
|
|
3975
|
+
return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", marginBottom, children: [
|
|
3976
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, color: style.color, children: style.label }),
|
|
3977
|
+
/* @__PURE__ */ jsxs7(Text7, { color: style.color, children: [
|
|
3978
|
+
message.text,
|
|
3979
|
+
message.interrupted && interruptedMarker(message.text)
|
|
3980
|
+
] }),
|
|
3981
|
+
message.diff && /* @__PURE__ */ jsx7(DiffLines, { lines: message.diff }),
|
|
3982
|
+
message.note && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: message.note })
|
|
2760
3983
|
] }, message.id);
|
|
2761
3984
|
} });
|
|
2762
3985
|
});
|
|
2763
3986
|
|
|
2764
3987
|
// src/cli/components/PermissionPrompt.tsx
|
|
2765
|
-
import { Box as
|
|
2766
|
-
import { jsx as
|
|
3988
|
+
import { Box as Box7, Text as Text8, useInput as useInput6 } from "ink";
|
|
3989
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2767
3990
|
function PermissionPrompt({ request, preview, onDecision }) {
|
|
2768
|
-
|
|
3991
|
+
useInput6((input) => {
|
|
2769
3992
|
const ch = input.toLowerCase();
|
|
2770
3993
|
if (ch === "y") onDecision("yes");
|
|
2771
3994
|
else if (ch === "n") onDecision("no");
|
|
2772
3995
|
else if (ch === "a") onDecision("always");
|
|
2773
3996
|
});
|
|
2774
|
-
return /* @__PURE__ */
|
|
2775
|
-
/* @__PURE__ */
|
|
3997
|
+
return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
3998
|
+
/* @__PURE__ */ jsxs8(Text8, { bold: true, color: "yellow", children: [
|
|
2776
3999
|
"Permission required: ",
|
|
2777
4000
|
request.toolName,
|
|
2778
4001
|
" (",
|
|
2779
4002
|
request.level,
|
|
2780
4003
|
")"
|
|
2781
4004
|
] }),
|
|
2782
|
-
preview ? /* @__PURE__ */
|
|
2783
|
-
/* @__PURE__ */
|
|
4005
|
+
preview ? /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", children: [
|
|
4006
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2784
4007
|
preview.type === "new-file" ? "New file: " : "",
|
|
2785
4008
|
preview.label
|
|
2786
4009
|
] }),
|
|
2787
|
-
/* @__PURE__ */
|
|
2788
|
-
] }) : /* @__PURE__ */
|
|
2789
|
-
/* @__PURE__ */
|
|
4010
|
+
/* @__PURE__ */ jsx8(DiffLines, { lines: preview.lines })
|
|
4011
|
+
] }) : /* @__PURE__ */ jsx8(Text8, { children: summarizeArgs(request.args) }),
|
|
4012
|
+
/* @__PURE__ */ jsx8(Text8, { children: "[y] allow [n] deny [a] always (saved to config)" })
|
|
2790
4013
|
] });
|
|
2791
4014
|
}
|
|
2792
4015
|
|
|
2793
4016
|
// src/cli/components/RewindConfirmPrompt.tsx
|
|
2794
|
-
import { Box as
|
|
2795
|
-
import { jsx as
|
|
2796
|
-
function RewindConfirmPrompt({
|
|
2797
|
-
|
|
4017
|
+
import { Box as Box8, Text as Text9, useInput as useInput7 } from "ink";
|
|
4018
|
+
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
4019
|
+
function RewindConfirmPrompt({
|
|
4020
|
+
summary,
|
|
4021
|
+
title = "Rewind checkpoints",
|
|
4022
|
+
confirmLabel = "rewind",
|
|
4023
|
+
diffs,
|
|
4024
|
+
onDecision
|
|
4025
|
+
}) {
|
|
4026
|
+
useInput7((input, key) => {
|
|
2798
4027
|
const ch = input.toLowerCase();
|
|
2799
4028
|
if (ch === "y") onDecision("yes");
|
|
2800
4029
|
else if (ch === "n" || key.escape) onDecision("no");
|
|
2801
4030
|
});
|
|
2802
|
-
return /* @__PURE__ */
|
|
2803
|
-
/* @__PURE__ */
|
|
2804
|
-
/* @__PURE__ */
|
|
2805
|
-
/* @__PURE__ */
|
|
2806
|
-
|
|
2807
|
-
}
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
function SelectPrompt({ title, options, onSelect, onCancel }) {
|
|
2815
|
-
const [index, setIndex] = useState2(0);
|
|
2816
|
-
const indexRef = useRef2(0);
|
|
2817
|
-
const move = (delta) => {
|
|
2818
|
-
indexRef.current = Math.max(0, Math.min(indexRef.current + delta, options.length - 1));
|
|
2819
|
-
setIndex(indexRef.current);
|
|
2820
|
-
};
|
|
2821
|
-
useInput5((input, key) => {
|
|
2822
|
-
if (key.escape) {
|
|
2823
|
-
onCancel();
|
|
2824
|
-
return;
|
|
2825
|
-
}
|
|
2826
|
-
if (key.return) {
|
|
2827
|
-
const option = options[indexRef.current];
|
|
2828
|
-
if (option) onSelect(option.value);
|
|
2829
|
-
return;
|
|
2830
|
-
}
|
|
2831
|
-
if (key.downArrow || input === "j") {
|
|
2832
|
-
move(1);
|
|
2833
|
-
} else if (key.upArrow || input === "k") {
|
|
2834
|
-
move(-1);
|
|
2835
|
-
}
|
|
2836
|
-
});
|
|
2837
|
-
const maxStart = Math.max(0, options.length - SELECT_PROMPT_MAX_VISIBLE);
|
|
2838
|
-
const start = Math.min(Math.max(0, index - Math.floor(SELECT_PROMPT_MAX_VISIBLE / 2)), maxStart);
|
|
2839
|
-
const visible = options.slice(start, start + SELECT_PROMPT_MAX_VISIBLE);
|
|
2840
|
-
const above = start;
|
|
2841
|
-
const below = options.length - start - visible.length;
|
|
2842
|
-
return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
2843
|
-
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: title }),
|
|
2844
|
-
above > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2845
|
-
"\u2026 ",
|
|
2846
|
-
above,
|
|
2847
|
-
" more above"
|
|
2848
|
-
] }),
|
|
2849
|
-
visible.map((option, i) => {
|
|
2850
|
-
const active = start + i === index;
|
|
2851
|
-
return /* @__PURE__ */ jsxs7(Text7, { children: [
|
|
2852
|
-
/* @__PURE__ */ jsxs7(Text7, { bold: active, inverse: active, children: [
|
|
2853
|
-
active ? "\u276F " : " ",
|
|
2854
|
-
option.label
|
|
2855
|
-
] }),
|
|
2856
|
-
option.hint && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2857
|
-
" (",
|
|
2858
|
-
option.hint,
|
|
2859
|
-
")"
|
|
2860
|
-
] }),
|
|
2861
|
-
option.description && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2862
|
-
" \u2014 ",
|
|
2863
|
-
option.description
|
|
2864
|
-
] })
|
|
2865
|
-
] }, option.value);
|
|
2866
|
-
}),
|
|
2867
|
-
below > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2868
|
-
"\u2026 ",
|
|
2869
|
-
below,
|
|
2870
|
-
" more below"
|
|
2871
|
-
] }),
|
|
2872
|
-
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "[\u2191/\u2193 or j/k] move [Enter] select [Esc] cancel" })
|
|
4031
|
+
return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
4032
|
+
/* @__PURE__ */ jsx9(Text9, { bold: true, color: "yellow", children: title }),
|
|
4033
|
+
/* @__PURE__ */ jsx9(Text9, { children: summary }),
|
|
4034
|
+
diffs?.map((diff) => /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", children: [
|
|
4035
|
+
/* @__PURE__ */ jsx9(Text9, { dimColor: true, children: diff.label }),
|
|
4036
|
+
/* @__PURE__ */ jsx9(DiffLines, { lines: diff.lines })
|
|
4037
|
+
] }, diff.label)),
|
|
4038
|
+
/* @__PURE__ */ jsxs9(Text9, { children: [
|
|
4039
|
+
"[y] ",
|
|
4040
|
+
confirmLabel,
|
|
4041
|
+
" [n] cancel (Esc = n)"
|
|
4042
|
+
] })
|
|
2873
4043
|
] });
|
|
2874
4044
|
}
|
|
2875
4045
|
|
|
2876
4046
|
// src/cli/components/StatusBar.tsx
|
|
2877
|
-
import { Box as
|
|
4047
|
+
import { Box as Box9, Text as Text10, useStdout as useStdout2 } from "ink";
|
|
2878
4048
|
import { memo as memo2 } from "react";
|
|
2879
|
-
import { jsxs as
|
|
4049
|
+
import { jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2880
4050
|
var BG_LABEL_MAX = 40;
|
|
2881
4051
|
var WIDE_COLUMNS = 120;
|
|
2882
4052
|
function cwdLabel(cwd, wide) {
|
|
@@ -2891,43 +4061,48 @@ var StatusBar = memo2(function StatusBar2({
|
|
|
2891
4061
|
tokens: tokens2,
|
|
2892
4062
|
gitBranch,
|
|
2893
4063
|
contextPercent,
|
|
4064
|
+
cachePercent,
|
|
2894
4065
|
sessionCostUsd,
|
|
2895
4066
|
backgroundTasks = []
|
|
2896
4067
|
}) {
|
|
2897
|
-
const { stdout } =
|
|
4068
|
+
const { stdout } = useStdout2();
|
|
2898
4069
|
const wide = (stdout?.columns ?? 80) >= WIDE_COLUMNS;
|
|
2899
4070
|
const joined = backgroundTasks.join(", ");
|
|
2900
4071
|
const labels = joined.length > BG_LABEL_MAX ? `${joined.slice(0, BG_LABEL_MAX)}\u2026` : joined;
|
|
2901
|
-
return /* @__PURE__ */
|
|
2902
|
-
/* @__PURE__ */
|
|
4072
|
+
return /* @__PURE__ */ jsxs10(Box9, { justifyContent: "space-between", children: [
|
|
4073
|
+
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
2903
4074
|
cwdLabel(cwd, wide),
|
|
2904
4075
|
gitBranch ? ` [${gitBranch}]` : ""
|
|
2905
4076
|
] }),
|
|
2906
|
-
backgroundTasks.length > 0 && /* @__PURE__ */
|
|
4077
|
+
backgroundTasks.length > 0 && /* @__PURE__ */ jsxs10(Text10, { color: "yellow", children: [
|
|
2907
4078
|
"bg: ",
|
|
2908
4079
|
backgroundTasks.length,
|
|
2909
4080
|
" (",
|
|
2910
4081
|
labels,
|
|
2911
4082
|
")"
|
|
2912
4083
|
] }),
|
|
2913
|
-
/* @__PURE__ */
|
|
4084
|
+
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
2914
4085
|
"model: ",
|
|
2915
4086
|
model
|
|
2916
4087
|
] }),
|
|
2917
|
-
contextPercent != null && /* @__PURE__ */
|
|
4088
|
+
contextPercent != null && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
2918
4089
|
"ctx: ",
|
|
2919
4090
|
contextPercent,
|
|
2920
4091
|
"%"
|
|
2921
4092
|
] }),
|
|
2922
|
-
|
|
4093
|
+
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
4094
|
+
"cache: ",
|
|
4095
|
+
cachePercent != null ? `${cachePercent}%` : "Not provided"
|
|
4096
|
+
] }),
|
|
4097
|
+
sessionCostUsd != null && sessionCostUsd > 0 && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
2923
4098
|
"$",
|
|
2924
4099
|
formatDollars(sessionCostUsd)
|
|
2925
4100
|
] }),
|
|
2926
|
-
/* @__PURE__ */
|
|
4101
|
+
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
2927
4102
|
tokens2,
|
|
2928
4103
|
" tokens"
|
|
2929
4104
|
] }),
|
|
2930
|
-
/* @__PURE__ */
|
|
4105
|
+
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
2931
4106
|
"mode: ",
|
|
2932
4107
|
permissionMode
|
|
2933
4108
|
] })
|
|
@@ -2935,22 +4110,22 @@ var StatusBar = memo2(function StatusBar2({
|
|
|
2935
4110
|
});
|
|
2936
4111
|
|
|
2937
4112
|
// src/cli/components/StreamingMessage.tsx
|
|
2938
|
-
import { Box as
|
|
4113
|
+
import { Box as Box10, Text as Text11 } from "ink";
|
|
2939
4114
|
import { memo as memo3 } from "react";
|
|
2940
|
-
import { jsx as
|
|
4115
|
+
import { jsx as jsx10, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2941
4116
|
var StreamingMessage = memo3(function StreamingMessage2({
|
|
2942
4117
|
text,
|
|
2943
4118
|
continuation
|
|
2944
4119
|
}) {
|
|
2945
|
-
return /* @__PURE__ */
|
|
2946
|
-
!continuation && /* @__PURE__ */
|
|
2947
|
-
/* @__PURE__ */
|
|
4120
|
+
return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", marginBottom: 1, children: [
|
|
4121
|
+
!continuation && /* @__PURE__ */ jsx10(Text11, { bold: true, color: "green", children: "star" }),
|
|
4122
|
+
/* @__PURE__ */ jsx10(Text11, { color: "green", children: text })
|
|
2948
4123
|
] });
|
|
2949
4124
|
});
|
|
2950
4125
|
|
|
2951
4126
|
// src/cli/components/ThinkingIndicator.tsx
|
|
2952
|
-
import { Box as
|
|
2953
|
-
import { jsxs as
|
|
4127
|
+
import { Box as Box11, Text as Text12 } from "ink";
|
|
4128
|
+
import { jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2954
4129
|
var SPINNER_FRAMES = ["-", "\\", "|", "/"];
|
|
2955
4130
|
var REASONING_TAIL_LENGTH = 200;
|
|
2956
4131
|
var THOUGHT_SUMMARY_LENGTH = 100;
|
|
@@ -2966,13 +4141,13 @@ function ThinkingIndicator({
|
|
|
2966
4141
|
}) {
|
|
2967
4142
|
const tail = reasoning ? truncateTail(reasoning, REASONING_TAIL_LENGTH) : "";
|
|
2968
4143
|
const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
2969
|
-
return /* @__PURE__ */
|
|
2970
|
-
/* @__PURE__ */
|
|
4144
|
+
return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", children: [
|
|
4145
|
+
/* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
|
|
2971
4146
|
spinner,
|
|
2972
4147
|
" ",
|
|
2973
4148
|
activity ?? "star is thinking\u2026"
|
|
2974
4149
|
] }),
|
|
2975
|
-
tail !== "" && /* @__PURE__ */
|
|
4150
|
+
tail !== "" && /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
|
|
2976
4151
|
" ",
|
|
2977
4152
|
tail
|
|
2978
4153
|
] })
|
|
@@ -2980,35 +4155,71 @@ function ThinkingIndicator({
|
|
|
2980
4155
|
}
|
|
2981
4156
|
|
|
2982
4157
|
// src/cli/components/TodoPanel.tsx
|
|
2983
|
-
import { Box as
|
|
2984
|
-
|
|
4158
|
+
import { Box as Box12, Text as Text13 } from "ink";
|
|
4159
|
+
|
|
4160
|
+
// src/cli/terminal-text.ts
|
|
4161
|
+
var UNSAFE_CHARS = /[\x00-\x09\x0b-\x1f\x7f]/;
|
|
4162
|
+
var CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
4163
|
+
var TABS = /\t/g;
|
|
4164
|
+
function toTerminalSafe(text) {
|
|
4165
|
+
if (!UNSAFE_CHARS.test(text)) return text;
|
|
4166
|
+
return text.replace(TABS, " ").replace(CONTROL_CHARS, "");
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
// src/cli/components/TodoPanel.tsx
|
|
4170
|
+
import { jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2985
4171
|
var SYMBOLS = {
|
|
2986
4172
|
pending: "[ ]",
|
|
2987
4173
|
in_progress: "[~]",
|
|
2988
4174
|
done: "[x]"
|
|
2989
4175
|
};
|
|
4176
|
+
var MAX_VISIBLE_TODOS = 10;
|
|
4177
|
+
function visibleTodoWindow(todos, max = MAX_VISIBLE_TODOS) {
|
|
4178
|
+
if (todos.length <= max) return { items: [...todos], hiddenBefore: 0, hiddenAfter: 0 };
|
|
4179
|
+
const active = todos.findIndex((todo) => todo.status === "in_progress");
|
|
4180
|
+
const start = active > 0 ? Math.min(active, todos.length - max) : 0;
|
|
4181
|
+
const items = todos.slice(start, start + max);
|
|
4182
|
+
return {
|
|
4183
|
+
items,
|
|
4184
|
+
hiddenBefore: start,
|
|
4185
|
+
hiddenAfter: todos.length - start - items.length
|
|
4186
|
+
};
|
|
4187
|
+
}
|
|
2990
4188
|
function TodoPanel({ todos }) {
|
|
2991
4189
|
if (todos.length === 0) return null;
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
{
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
4190
|
+
const { items, hiddenBefore, hiddenAfter } = visibleTodoWindow(todos);
|
|
4191
|
+
return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
|
|
4192
|
+
hiddenBefore > 0 && /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
|
|
4193
|
+
" \u2026 ",
|
|
4194
|
+
hiddenBefore,
|
|
4195
|
+
" more above"
|
|
4196
|
+
] }),
|
|
4197
|
+
items.map((todo) => /* @__PURE__ */ jsxs13(
|
|
4198
|
+
Text13,
|
|
4199
|
+
{
|
|
4200
|
+
dimColor: todo.status === "done",
|
|
4201
|
+
color: todo.status === "in_progress" ? "cyan" : void 0,
|
|
4202
|
+
children: [
|
|
4203
|
+
SYMBOLS[todo.status],
|
|
4204
|
+
" ",
|
|
4205
|
+
toTerminalSafe(todo.title)
|
|
4206
|
+
]
|
|
4207
|
+
},
|
|
4208
|
+
todo.id
|
|
4209
|
+
)),
|
|
4210
|
+
hiddenAfter > 0 && /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
|
|
4211
|
+
" \u2026 ",
|
|
4212
|
+
hiddenAfter,
|
|
4213
|
+
" more"
|
|
4214
|
+
] })
|
|
4215
|
+
] });
|
|
3005
4216
|
}
|
|
3006
4217
|
|
|
3007
4218
|
// src/cli/components/ToolCallCard.tsx
|
|
3008
4219
|
function formatToolCard(card) {
|
|
3009
4220
|
const header = `${card.name} ${card.argsSummary}${card.isError ? " [error]" : ""}`;
|
|
3010
4221
|
if (card.result === void 0) return header;
|
|
3011
|
-
const preview = previewLines(card.result, 10);
|
|
4222
|
+
const preview = previewLines(toTerminalSafe(card.result), 10);
|
|
3012
4223
|
return `${header}
|
|
3013
4224
|
${preview.text}${preview.truncated ? "\n... (truncated)" : ""}`;
|
|
3014
4225
|
}
|
|
@@ -3020,28 +4231,44 @@ function isDoubleEscape(previous, now, windowMs = DOUBLE_ESC_WINDOW_MS) {
|
|
|
3020
4231
|
}
|
|
3021
4232
|
|
|
3022
4233
|
// src/cli/history.ts
|
|
3023
|
-
import
|
|
3024
|
-
import
|
|
3025
|
-
var
|
|
3026
|
-
var
|
|
3027
|
-
var TRIM_TO_LINES = 500;
|
|
4234
|
+
import fs12 from "fs";
|
|
4235
|
+
import path19 from "path";
|
|
4236
|
+
var HISTORY_LIMIT = 50;
|
|
4237
|
+
var JUNK_ENTRY_PATTERN = /^(.)\1{19,}$/;
|
|
3028
4238
|
function historyPath() {
|
|
3029
|
-
return
|
|
4239
|
+
return path19.join(starHome(), "history");
|
|
3030
4240
|
}
|
|
3031
4241
|
function readLines(file) {
|
|
3032
|
-
const raw =
|
|
4242
|
+
const raw = fs12.readFileSync(file, "utf8");
|
|
3033
4243
|
return raw.split("\n").map((line) => line.replace(/\r$/, "")).filter((line) => line.trim().length > 0);
|
|
3034
4244
|
}
|
|
3035
|
-
function
|
|
4245
|
+
function writeHistory(file, lines) {
|
|
4246
|
+
fs12.mkdirSync(path19.dirname(file), { recursive: true });
|
|
4247
|
+
fs12.writeFileSync(file, `${lines.join("\n")}
|
|
4248
|
+
`);
|
|
4249
|
+
}
|
|
4250
|
+
function isJunkHistoryEntry(line) {
|
|
4251
|
+
return JUNK_ENTRY_PATTERN.test(line);
|
|
4252
|
+
}
|
|
4253
|
+
function isSkippableEntry(line) {
|
|
4254
|
+
return isJunkHistoryEntry(line) || line.startsWith("/");
|
|
4255
|
+
}
|
|
4256
|
+
function loadHistory(limit = HISTORY_LIMIT) {
|
|
3036
4257
|
try {
|
|
3037
|
-
|
|
4258
|
+
const file = historyPath();
|
|
4259
|
+
const lines = readLines(file);
|
|
4260
|
+
const clean = lines.filter((line) => !isSkippableEntry(line));
|
|
4261
|
+
if (clean.length !== lines.length || clean.length > HISTORY_LIMIT) {
|
|
4262
|
+
writeHistory(file, clean.slice(-HISTORY_LIMIT));
|
|
4263
|
+
}
|
|
4264
|
+
return clean.slice(-limit);
|
|
3038
4265
|
} catch {
|
|
3039
4266
|
return [];
|
|
3040
4267
|
}
|
|
3041
4268
|
}
|
|
3042
4269
|
function appendHistory(entry) {
|
|
3043
4270
|
const line = entry.replace(/[\r\n]+/g, " ").trim();
|
|
3044
|
-
if (line.length === 0) return;
|
|
4271
|
+
if (line.length === 0 || isSkippableEntry(line)) return;
|
|
3045
4272
|
try {
|
|
3046
4273
|
const file = historyPath();
|
|
3047
4274
|
let existing = [];
|
|
@@ -3050,15 +4277,12 @@ function appendHistory(entry) {
|
|
|
3050
4277
|
} catch {
|
|
3051
4278
|
}
|
|
3052
4279
|
if (existing[existing.length - 1] === line) return;
|
|
3053
|
-
if (existing.length >=
|
|
3054
|
-
|
|
3055
|
-
fs10.mkdirSync(path16.dirname(file), { recursive: true });
|
|
3056
|
-
fs10.writeFileSync(file, `${kept.join("\n")}
|
|
3057
|
-
`);
|
|
4280
|
+
if (existing.length >= HISTORY_LIMIT) {
|
|
4281
|
+
writeHistory(file, [...existing.slice(-(HISTORY_LIMIT - 1)), line]);
|
|
3058
4282
|
return;
|
|
3059
4283
|
}
|
|
3060
|
-
|
|
3061
|
-
|
|
4284
|
+
fs12.mkdirSync(path19.dirname(file), { recursive: true });
|
|
4285
|
+
fs12.appendFileSync(file, `${line}
|
|
3062
4286
|
`);
|
|
3063
4287
|
} catch {
|
|
3064
4288
|
}
|
|
@@ -3179,7 +4403,7 @@ async function checkForUpdate(currentVersion, fetcher = defaultFetcher2) {
|
|
|
3179
4403
|
}
|
|
3180
4404
|
|
|
3181
4405
|
// src/cli/repl.tsx
|
|
3182
|
-
import { jsx as
|
|
4406
|
+
import { jsx as jsx11, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
3183
4407
|
var CLEAR_TERMINAL = process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H";
|
|
3184
4408
|
function emptyUsage() {
|
|
3185
4409
|
return { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
@@ -3194,6 +4418,7 @@ function formatUsage(usage) {
|
|
|
3194
4418
|
return `API usage this session: ${usage.requests} requests, ${usage.promptTokens} prompt + ${usage.completionTokens} completion = ${usage.totalTokens} tokens`;
|
|
3195
4419
|
}
|
|
3196
4420
|
var SESSION_PICKER_CAP = 20;
|
|
4421
|
+
var MAX_VISIBLE_QUEUED = 5;
|
|
3197
4422
|
async function sessionPreview(id) {
|
|
3198
4423
|
const store = await SessionStore.open(id);
|
|
3199
4424
|
if (!store) return null;
|
|
@@ -3214,65 +4439,67 @@ function Repl({
|
|
|
3214
4439
|
initialUsage
|
|
3215
4440
|
}) {
|
|
3216
4441
|
const { exit } = useApp();
|
|
3217
|
-
const [initialDisplay] =
|
|
3218
|
-
const [messages, setMessages] =
|
|
3219
|
-
const [epoch, setEpoch] =
|
|
3220
|
-
const [streamingText, setStreamingText] =
|
|
3221
|
-
const [isStreaming, setIsStreaming] =
|
|
3222
|
-
const [thinking, setThinking] =
|
|
3223
|
-
const [thinkingText, setThinkingText] =
|
|
3224
|
-
const [thoughtSummary, setThoughtSummary] =
|
|
3225
|
-
const [, setUsageVersion] =
|
|
3226
|
-
const [modelName, setModelName] =
|
|
3227
|
-
const [pending, setPending] =
|
|
3228
|
-
const [spinnerTick, setSpinnerTick] =
|
|
3229
|
-
const [activity, setActivity] =
|
|
3230
|
-
const [bgLabels, setBgLabels] =
|
|
3231
|
-
const [todos, setTodos] =
|
|
3232
|
-
const [permissionModeState, setPermissionModeState] =
|
|
3233
|
-
const [planApproval, setPlanApprovalState] =
|
|
3234
|
-
const [pendingRewind, setPendingRewindState] =
|
|
3235
|
-
const [picker, setPickerState] =
|
|
3236
|
-
const
|
|
3237
|
-
const
|
|
3238
|
-
const
|
|
3239
|
-
const
|
|
3240
|
-
const
|
|
3241
|
-
const
|
|
3242
|
-
const
|
|
3243
|
-
const
|
|
3244
|
-
const
|
|
3245
|
-
const
|
|
3246
|
-
const
|
|
3247
|
-
const
|
|
3248
|
-
const
|
|
3249
|
-
const
|
|
3250
|
-
const
|
|
3251
|
-
const
|
|
3252
|
-
const
|
|
3253
|
-
const
|
|
3254
|
-
const
|
|
3255
|
-
const
|
|
3256
|
-
const
|
|
3257
|
-
const
|
|
3258
|
-
const [
|
|
3259
|
-
const
|
|
3260
|
-
const
|
|
3261
|
-
const
|
|
3262
|
-
const
|
|
3263
|
-
const
|
|
3264
|
-
const
|
|
3265
|
-
const
|
|
3266
|
-
const [
|
|
3267
|
-
const
|
|
3268
|
-
const
|
|
3269
|
-
const
|
|
4442
|
+
const [initialDisplay] = useState5(() => buildDisplayMessages(initialMessages ?? []));
|
|
4443
|
+
const [messages, setMessages] = useState5(initialDisplay);
|
|
4444
|
+
const [epoch, setEpoch] = useState5(0);
|
|
4445
|
+
const [streamingText, setStreamingText] = useState5(null);
|
|
4446
|
+
const [isStreaming, setIsStreaming] = useState5(false);
|
|
4447
|
+
const [thinking, setThinking] = useState5(false);
|
|
4448
|
+
const [thinkingText, setThinkingText] = useState5("");
|
|
4449
|
+
const [thoughtSummary, setThoughtSummary] = useState5(null);
|
|
4450
|
+
const [, setUsageVersion] = useState5(0);
|
|
4451
|
+
const [modelName, setModelName] = useState5(model);
|
|
4452
|
+
const [pending, setPending] = useState5(null);
|
|
4453
|
+
const [spinnerTick, setSpinnerTick] = useState5(0);
|
|
4454
|
+
const [activity, setActivity] = useState5(null);
|
|
4455
|
+
const [bgLabels, setBgLabels] = useState5(() => runningTaskLabels());
|
|
4456
|
+
const [todos, setTodos] = useState5([]);
|
|
4457
|
+
const [permissionModeState, setPermissionModeState] = useState5(permissionMode);
|
|
4458
|
+
const [planApproval, setPlanApprovalState] = useState5(false);
|
|
4459
|
+
const [pendingRewind, setPendingRewindState] = useState5(null);
|
|
4460
|
+
const [picker, setPickerState] = useState5(null);
|
|
4461
|
+
const [pendingConnect, setPendingConnectState] = useState5(null);
|
|
4462
|
+
const backendRef = useRef5(backend);
|
|
4463
|
+
const nextIdRef = useRef5(initialDisplay.length);
|
|
4464
|
+
const abortRef = useRef5(null);
|
|
4465
|
+
const streamedRef = useRef5("");
|
|
4466
|
+
const thinkingRef = useRef5(false);
|
|
4467
|
+
const reasoningRef = useRef5("");
|
|
4468
|
+
const tickerStopRef = useRef5(null);
|
|
4469
|
+
const flushedRef = useRef5({ streamed: "", reasoning: "" });
|
|
4470
|
+
const toolCardsRef = useRef5(/* @__PURE__ */ new Map());
|
|
4471
|
+
const pendingRef = useRef5(null);
|
|
4472
|
+
const alwaysAllowedRef = useRef5(/* @__PURE__ */ new Set());
|
|
4473
|
+
const prevModeRef = useRef5(null);
|
|
4474
|
+
const planApprovalRef = useRef5(false);
|
|
4475
|
+
const pendingRewindRef = useRef5(null);
|
|
4476
|
+
const pickerRef = useRef5(null);
|
|
4477
|
+
const pendingConnectRef = useRef5(null);
|
|
4478
|
+
const modelNameRef = useRef5(model);
|
|
4479
|
+
const sessionStoreRef = useRef5(sessionStore);
|
|
4480
|
+
const usageRef = useRef5(initialUsage ? { ...initialUsage } : emptyUsage());
|
|
4481
|
+
const turnChunksRef = useRef5(0);
|
|
4482
|
+
const messagesRef = useRef5(initialDisplay);
|
|
4483
|
+
const [queueItems, setQueueItems] = useState5([]);
|
|
4484
|
+
const queueRef = useRef5(new PromptQueue());
|
|
4485
|
+
const [pendingImages, setPendingImages] = useState5([]);
|
|
4486
|
+
const pendingImagesRef = useRef5([]);
|
|
4487
|
+
const imageSeqRef = useRef5(1);
|
|
4488
|
+
const [inputRefill, setInputRefill] = useState5();
|
|
4489
|
+
const refillSeqRef = useRef5(0);
|
|
4490
|
+
const lastEscRef = useRef5(null);
|
|
4491
|
+
const [gitBranch, setGitBranch] = useState5(null);
|
|
4492
|
+
const [contextPercent, setContextPercent] = useState5(null);
|
|
4493
|
+
const [initialHistory] = useState5(() => loadHistory());
|
|
4494
|
+
const budgetWarnedRef = useRef5(false);
|
|
4495
|
+
const budgetExceededRef = useRef5(false);
|
|
4496
|
+
const runStreamRef = useRef5(
|
|
3270
4497
|
null
|
|
3271
4498
|
);
|
|
3272
4499
|
const pushMessage = useCallback(
|
|
3273
|
-
(role, text, note, tight) => {
|
|
4500
|
+
(role, text, note, tight, interrupted) => {
|
|
3274
4501
|
setMessages((prev) => {
|
|
3275
|
-
const next = [...prev, { id: nextIdRef.current++, role, text, note, tight }];
|
|
4502
|
+
const next = [...prev, { id: nextIdRef.current++, role, text, note, tight, interrupted }];
|
|
3276
4503
|
messagesRef.current = next;
|
|
3277
4504
|
return next;
|
|
3278
4505
|
});
|
|
@@ -3294,10 +4521,10 @@ function Repl({
|
|
|
3294
4521
|
[applyMessages]
|
|
3295
4522
|
);
|
|
3296
4523
|
const pushAssistantChunk = useCallback(
|
|
3297
|
-
(text, tight) => {
|
|
4524
|
+
(text, tight, interrupted = false) => {
|
|
3298
4525
|
const role = turnChunksRef.current === 0 ? "assistant" : "assistant-cont";
|
|
3299
4526
|
turnChunksRef.current += 1;
|
|
3300
|
-
pushMessage(role, text, void 0, tight);
|
|
4527
|
+
pushMessage(role, text, void 0, tight, interrupted);
|
|
3301
4528
|
},
|
|
3302
4529
|
[pushMessage]
|
|
3303
4530
|
);
|
|
@@ -3317,6 +4544,10 @@ function Repl({
|
|
|
3317
4544
|
pickerRef.current = p;
|
|
3318
4545
|
setPickerState(p);
|
|
3319
4546
|
}, []);
|
|
4547
|
+
const setPendingConnect = useCallback((p) => {
|
|
4548
|
+
pendingConnectRef.current = p;
|
|
4549
|
+
setPendingConnectState(p);
|
|
4550
|
+
}, []);
|
|
3320
4551
|
const showPicker = useCallback(
|
|
3321
4552
|
(title, options) => {
|
|
3322
4553
|
if (pickerRef.current) return Promise.resolve(null);
|
|
@@ -3360,10 +4591,17 @@ function Repl({
|
|
|
3360
4591
|
[config]
|
|
3361
4592
|
);
|
|
3362
4593
|
const handlePasteImage = useCallback(() => {
|
|
3363
|
-
void readClipboardImage().then((
|
|
3364
|
-
if (
|
|
4594
|
+
void readClipboardImage().then((result) => {
|
|
4595
|
+
if (!result) return;
|
|
4596
|
+
if (result.oversized) {
|
|
4597
|
+
pushMessage(
|
|
4598
|
+
"system",
|
|
4599
|
+
`Image exceeds ${MAX_IMAGE_DIMENSION}px on its longest side and could not be resized automatically; the model may reject it.`
|
|
4600
|
+
);
|
|
4601
|
+
}
|
|
4602
|
+
stageClipboardImage(result.image);
|
|
3365
4603
|
});
|
|
3366
|
-
}, [stageClipboardImage]);
|
|
4604
|
+
}, [stageClipboardImage, pushMessage]);
|
|
3367
4605
|
const handleRewindDecision = useCallback(
|
|
3368
4606
|
(decision) => {
|
|
3369
4607
|
const p = pendingRewindRef.current;
|
|
@@ -3373,6 +4611,32 @@ function Repl({
|
|
|
3373
4611
|
},
|
|
3374
4612
|
[setPendingRewind]
|
|
3375
4613
|
);
|
|
4614
|
+
const handleConnectSave = useCallback(
|
|
4615
|
+
async (answers) => {
|
|
4616
|
+
const result = saveConnection(answers);
|
|
4617
|
+
config.providers.push(result.provider);
|
|
4618
|
+
config.models.push(result.model);
|
|
4619
|
+
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}.`;
|
|
4620
|
+
},
|
|
4621
|
+
[config]
|
|
4622
|
+
);
|
|
4623
|
+
const handleConnectOpenConfig = useCallback(() => {
|
|
4624
|
+
const file = globalConfigPath();
|
|
4625
|
+
openConfigFile(file);
|
|
4626
|
+
return `Opening ${file} with the system default app (open it manually if nothing showed up).`;
|
|
4627
|
+
}, []);
|
|
4628
|
+
const finishConnect = useCallback(
|
|
4629
|
+
(message) => {
|
|
4630
|
+
const p = pendingConnectRef.current;
|
|
4631
|
+
if (!p) return;
|
|
4632
|
+
setPendingConnect(null);
|
|
4633
|
+
p.resolve(message);
|
|
4634
|
+
},
|
|
4635
|
+
[setPendingConnect]
|
|
4636
|
+
);
|
|
4637
|
+
const cancelConnect = useCallback(() => {
|
|
4638
|
+
finishConnect("Connect cancelled \u2014 nothing was saved.");
|
|
4639
|
+
}, [finishConnect]);
|
|
3376
4640
|
const applySessionMode = useCallback(
|
|
3377
4641
|
(mode) => {
|
|
3378
4642
|
if (mode === config.permissionMode) return;
|
|
@@ -3415,10 +4679,12 @@ function Repl({
|
|
|
3415
4679
|
if (message) pushMessage("system", message);
|
|
3416
4680
|
});
|
|
3417
4681
|
}, [pushMessage]);
|
|
4682
|
+
const resumedAtStart = initialMessages !== void 0;
|
|
3418
4683
|
useEffect2(() => {
|
|
4684
|
+
if (!resumedAtStart) return;
|
|
3419
4685
|
loadTodos(cwd).then(setTodos).catch(() => {
|
|
3420
4686
|
});
|
|
3421
|
-
}, [cwd]);
|
|
4687
|
+
}, [cwd, resumedAtStart]);
|
|
3422
4688
|
useEffect2(() => {
|
|
3423
4689
|
const onUpdate = (task) => {
|
|
3424
4690
|
setBgLabels(runningTaskLabels());
|
|
@@ -3502,9 +4768,9 @@ function Repl({
|
|
|
3502
4768
|
}
|
|
3503
4769
|
exit();
|
|
3504
4770
|
}, [exit, pushMessage]);
|
|
3505
|
-
|
|
4771
|
+
useInput8((_input, key) => {
|
|
3506
4772
|
if (key.escape) {
|
|
3507
|
-
const busy = abortRef.current !== null || pendingRef.current !== null || planApprovalRef.current || pendingRewindRef.current !== null || pickerRef.current !== null;
|
|
4773
|
+
const busy = abortRef.current !== null || pendingRef.current !== null || planApprovalRef.current || pendingRewindRef.current !== null || pendingConnectRef.current !== null || pickerRef.current !== null;
|
|
3508
4774
|
if (busy) {
|
|
3509
4775
|
lastEscRef.current = null;
|
|
3510
4776
|
interrupt();
|
|
@@ -3521,7 +4787,7 @@ function Repl({
|
|
|
3521
4787
|
}
|
|
3522
4788
|
lastEscRef.current = null;
|
|
3523
4789
|
if (key.shift && key.tab) {
|
|
3524
|
-
if (abortRef.current || pendingRef.current || planApprovalRef.current || pendingRewindRef.current || pickerRef.current)
|
|
4790
|
+
if (abortRef.current || pendingRef.current || planApprovalRef.current || pendingRewindRef.current || pendingConnectRef.current || pickerRef.current)
|
|
3525
4791
|
return;
|
|
3526
4792
|
const current = config.permissionMode;
|
|
3527
4793
|
const index = SESSION_MODE_CYCLE.indexOf(current);
|
|
@@ -3577,6 +4843,15 @@ function Repl({
|
|
|
3577
4843
|
},
|
|
3578
4844
|
[config, cwd, attachConfirmHandler]
|
|
3579
4845
|
);
|
|
4846
|
+
const handleConnectSetDefault = useCallback(
|
|
4847
|
+
async (modelName2) => {
|
|
4848
|
+
saveDefaultModel(modelName2);
|
|
4849
|
+
config.defaultModel = modelName2;
|
|
4850
|
+
const switched = await switchModel(modelName2);
|
|
4851
|
+
return `Default model set to "${modelName2}" (saved to config). ${switched}`;
|
|
4852
|
+
},
|
|
4853
|
+
[config, switchModel]
|
|
4854
|
+
);
|
|
3580
4855
|
const resume = useCallback(
|
|
3581
4856
|
async (id) => {
|
|
3582
4857
|
const current = backendRef.current;
|
|
@@ -3592,6 +4867,7 @@ function Repl({
|
|
|
3592
4867
|
return `Session not found: ${id}`;
|
|
3593
4868
|
}
|
|
3594
4869
|
await current.loadMessages(resumed.messages);
|
|
4870
|
+
setTodos(await loadTodos(cwd));
|
|
3595
4871
|
const store = await SessionStore.open(resolvedId);
|
|
3596
4872
|
if (store) {
|
|
3597
4873
|
hydrateSnapshots(await loadSessionSnapshots(store.dir));
|
|
@@ -3603,7 +4879,7 @@ function Repl({
|
|
|
3603
4879
|
setUsageVersion((v) => v + 1);
|
|
3604
4880
|
return `Resumed session ${resolvedId} (${resumed.messages.length} messages).`;
|
|
3605
4881
|
},
|
|
3606
|
-
[applyMessages]
|
|
4882
|
+
[applyMessages, cwd]
|
|
3607
4883
|
);
|
|
3608
4884
|
const runStream = useCallback(
|
|
3609
4885
|
async (input, extraImages = []) => {
|
|
@@ -3673,9 +4949,9 @@ function Repl({
|
|
|
3673
4949
|
);
|
|
3674
4950
|
}
|
|
3675
4951
|
}
|
|
3676
|
-
streamedRef.current += event.text;
|
|
4952
|
+
streamedRef.current += toTerminalSafe(event.text);
|
|
3677
4953
|
} else if (event.type === "reasoning") {
|
|
3678
|
-
reasoningRef.current += event.text;
|
|
4954
|
+
reasoningRef.current += toTerminalSafe(event.text);
|
|
3679
4955
|
} else if (event.type === "tool-call") {
|
|
3680
4956
|
commitStreamed(true);
|
|
3681
4957
|
toolCardsRef.current.set(event.id, {
|
|
@@ -3715,14 +4991,19 @@ function Repl({
|
|
|
3715
4991
|
usage.promptTokens += event.usage.promptTokens;
|
|
3716
4992
|
usage.completionTokens += event.usage.completionTokens;
|
|
3717
4993
|
usage.totalTokens += event.usage.totalTokens;
|
|
4994
|
+
if (cacheUsageReported(event.usage)) {
|
|
4995
|
+
if (!usage.cache) usage.cache = { cachedTokens: 0, promptTokens: 0 };
|
|
4996
|
+
addToCacheTotals(usage.cache, event.usage);
|
|
4997
|
+
}
|
|
3718
4998
|
setUsageVersion((v) => v + 1);
|
|
3719
4999
|
sessionStoreRef.current?.addUsage(event.usage).catch(() => {
|
|
3720
5000
|
});
|
|
3721
5001
|
}
|
|
3722
5002
|
} else if (event.type === "retry") {
|
|
5003
|
+
const wait = event.delayMs !== void 0 && event.delayMs >= 1e3 ? ` in ${Math.round(event.delayMs / 1e3)}s` : "";
|
|
3723
5004
|
pushMessage(
|
|
3724
5005
|
"system",
|
|
3725
|
-
`Request failed, retrying (${event.attempt}/${event.maxAttempts}): ${event.reason}`
|
|
5006
|
+
`Request failed, retrying (${event.attempt}/${event.maxAttempts})${wait}: ${event.reason}`
|
|
3726
5007
|
);
|
|
3727
5008
|
} else if (event.type === "notice") {
|
|
3728
5009
|
pushMessage("system", event.message);
|
|
@@ -3761,9 +5042,9 @@ function Repl({
|
|
|
3761
5042
|
elapsedMs: Date.now() - turnStartedAt
|
|
3762
5043
|
});
|
|
3763
5044
|
if (finalText.length > 0) {
|
|
3764
|
-
pushAssistantChunk(
|
|
5045
|
+
pushAssistantChunk(finalText, false, interrupted);
|
|
3765
5046
|
} else if (interrupted && turnChunksRef.current > 0) {
|
|
3766
|
-
pushAssistantChunk("
|
|
5047
|
+
pushAssistantChunk("", false, true);
|
|
3767
5048
|
}
|
|
3768
5049
|
for (const card of toolCardsRef.current.values()) {
|
|
3769
5050
|
pushMessage("tool", formatToolCard(card));
|
|
@@ -3992,12 +5273,20 @@ ${lines.join("\n")}`;
|
|
|
3992
5273
|
await current.loadMessages(first?.role === "system" ? [first] : []);
|
|
3993
5274
|
clearSnapshots();
|
|
3994
5275
|
redrawMessages([]);
|
|
5276
|
+
resetTodos();
|
|
5277
|
+
setTodos([]);
|
|
3995
5278
|
usageRef.current = emptyUsage();
|
|
3996
5279
|
budgetWarnedRef.current = false;
|
|
3997
5280
|
budgetExceededRef.current = false;
|
|
3998
5281
|
setUsageVersion((v) => v + 1);
|
|
3999
5282
|
return `Started a new session (${store.id}) with a clean context.`;
|
|
4000
5283
|
},
|
|
5284
|
+
connect: async () => {
|
|
5285
|
+
if (pendingConnectRef.current) return "The connect wizard is already open.";
|
|
5286
|
+
return new Promise((resolve) => {
|
|
5287
|
+
setPendingConnect({ resolve });
|
|
5288
|
+
});
|
|
5289
|
+
},
|
|
4001
5290
|
clearSessions: async (all) => {
|
|
4002
5291
|
const currentId = sessionStoreRef.current?.id;
|
|
4003
5292
|
const removed = await clearSessions(all ? void 0 : cwd, currentId);
|
|
@@ -4051,6 +5340,26 @@ ${estimateCost(usageRef.current, modelNameRef.current, modelConfig)}`;
|
|
|
4051
5340
|
if (!(current instanceof AgentLoop)) {
|
|
4052
5341
|
return "Nothing to undo.";
|
|
4053
5342
|
}
|
|
5343
|
+
const preview = current.previewLastTurnRetraction();
|
|
5344
|
+
if (preview.removed === 0) {
|
|
5345
|
+
return "Nothing to undo (no conversation turn to retract).";
|
|
5346
|
+
}
|
|
5347
|
+
const diffs = await buildUndoDiffs(
|
|
5348
|
+
preview.turn !== void 0 ? listTurnSnapshots(preview.turn) : [],
|
|
5349
|
+
cwd
|
|
5350
|
+
);
|
|
5351
|
+
const confirmed = await new Promise((resolve) => {
|
|
5352
|
+
setPendingRewind({
|
|
5353
|
+
title: "Undo last turn",
|
|
5354
|
+
confirmLabel: "undo",
|
|
5355
|
+
summary: `${preview.removed} message(s) will be retracted, ${diffs.length} file change(s) reverted.`,
|
|
5356
|
+
diffs,
|
|
5357
|
+
resolve
|
|
5358
|
+
});
|
|
5359
|
+
});
|
|
5360
|
+
if (!confirmed) {
|
|
5361
|
+
return "Undo cancelled.";
|
|
5362
|
+
}
|
|
4054
5363
|
const { removed, turn } = await current.retractLastTurn();
|
|
4055
5364
|
if (removed === 0) {
|
|
4056
5365
|
return "Nothing to undo (no conversation turn to retract).";
|
|
@@ -4187,6 +5496,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
4187
5496
|
redrawMessages,
|
|
4188
5497
|
applySessionMode,
|
|
4189
5498
|
setPendingRewind,
|
|
5499
|
+
setPendingConnect,
|
|
4190
5500
|
showPicker
|
|
4191
5501
|
]);
|
|
4192
5502
|
const runShellBang = useCallback(
|
|
@@ -4291,9 +5601,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
4291
5601
|
}
|
|
4292
5602
|
}, [cwd, config, isStreaming, epoch]);
|
|
4293
5603
|
const sessionCost = sessionCostUsd();
|
|
4294
|
-
return /* @__PURE__ */
|
|
4295
|
-
/* @__PURE__ */
|
|
4296
|
-
(thinking || activity !== null) && /* @__PURE__ */
|
|
5604
|
+
return /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", children: [
|
|
5605
|
+
/* @__PURE__ */ jsx11(MessageList, { messages }, `messages-${epoch}`),
|
|
5606
|
+
(thinking || activity !== null) && /* @__PURE__ */ jsx11(
|
|
4297
5607
|
ThinkingIndicator,
|
|
4298
5608
|
{
|
|
4299
5609
|
reasoning: thinkingText,
|
|
@@ -4301,9 +5611,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
4301
5611
|
activity: activity ?? void 0
|
|
4302
5612
|
}
|
|
4303
5613
|
),
|
|
4304
|
-
!thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */
|
|
4305
|
-
streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */
|
|
4306
|
-
pending && /* @__PURE__ */
|
|
5614
|
+
!thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */ jsx11(Text14, { dimColor: true, children: thoughtSummary }),
|
|
5615
|
+
streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */ jsx11(StreamingMessage, { text: streamingText, continuation: turnChunksRef.current > 0 }),
|
|
5616
|
+
pending && /* @__PURE__ */ jsx11(
|
|
4307
5617
|
PermissionPrompt,
|
|
4308
5618
|
{
|
|
4309
5619
|
request: pending.request,
|
|
@@ -4311,9 +5621,18 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
4311
5621
|
onDecision: handleDecision
|
|
4312
5622
|
}
|
|
4313
5623
|
),
|
|
4314
|
-
planApproval && /* @__PURE__ */
|
|
4315
|
-
pendingRewind && /* @__PURE__ */
|
|
4316
|
-
|
|
5624
|
+
planApproval && /* @__PURE__ */ jsx11(ExitPlanPrompt, { onDecision: handlePlanDecision }),
|
|
5625
|
+
pendingRewind && /* @__PURE__ */ jsx11(
|
|
5626
|
+
RewindConfirmPrompt,
|
|
5627
|
+
{
|
|
5628
|
+
summary: pendingRewind.summary,
|
|
5629
|
+
title: pendingRewind.title,
|
|
5630
|
+
confirmLabel: pendingRewind.confirmLabel,
|
|
5631
|
+
diffs: pendingRewind.diffs,
|
|
5632
|
+
onDecision: handleRewindDecision
|
|
5633
|
+
}
|
|
5634
|
+
),
|
|
5635
|
+
picker && /* @__PURE__ */ jsx11(
|
|
4317
5636
|
SelectPrompt,
|
|
4318
5637
|
{
|
|
4319
5638
|
title: picker.title,
|
|
@@ -4322,21 +5641,38 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
4322
5641
|
onCancel: handlePickerCancel
|
|
4323
5642
|
}
|
|
4324
5643
|
),
|
|
4325
|
-
|
|
5644
|
+
pendingConnect && /* @__PURE__ */ jsx11(
|
|
5645
|
+
ConnectWizard,
|
|
5646
|
+
{
|
|
5647
|
+
existingProviderNames: config.providers.map((p) => p.name),
|
|
5648
|
+
existingModelNames: config.models.map((m) => m.name),
|
|
5649
|
+
onSave: handleConnectSave,
|
|
5650
|
+
onSetDefault: handleConnectSetDefault,
|
|
5651
|
+
onOpenConfig: handleConnectOpenConfig,
|
|
5652
|
+
onFinish: finishConnect,
|
|
5653
|
+
onCancel: cancelConnect
|
|
5654
|
+
}
|
|
5655
|
+
),
|
|
5656
|
+
queueItems.slice(0, MAX_VISIBLE_QUEUED).map((item) => /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
|
|
4326
5657
|
"queued: ",
|
|
4327
5658
|
item.text.length > 80 ? `${item.text.slice(0, 80)}\u2026` : item.text
|
|
4328
5659
|
] }, item.id)),
|
|
4329
|
-
|
|
5660
|
+
queueItems.length > MAX_VISIBLE_QUEUED && /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
|
|
5661
|
+
"\u2026 ",
|
|
5662
|
+
queueItems.length - MAX_VISIBLE_QUEUED,
|
|
5663
|
+
" more queued"
|
|
5664
|
+
] }),
|
|
5665
|
+
pendingImages.map((staged) => /* @__PURE__ */ jsxs14(Text14, { dimColor: true, children: [
|
|
4330
5666
|
"[image attached: ",
|
|
4331
5667
|
staged.image.path,
|
|
4332
5668
|
"]"
|
|
4333
5669
|
] }, staged.id)),
|
|
4334
|
-
/* @__PURE__ */
|
|
4335
|
-
/* @__PURE__ */
|
|
5670
|
+
/* @__PURE__ */ jsx11(TodoPanel, { todos }),
|
|
5671
|
+
/* @__PURE__ */ jsx11(
|
|
4336
5672
|
InputBox,
|
|
4337
5673
|
{
|
|
4338
5674
|
isStreaming,
|
|
4339
|
-
disabled: pending !== null || planApproval || pendingRewind !== null || picker !== null,
|
|
5675
|
+
disabled: pending !== null || planApproval || pendingRewind !== null || pendingConnect !== null || picker !== null,
|
|
4340
5676
|
commands: commandHints,
|
|
4341
5677
|
cwd,
|
|
4342
5678
|
refill: inputRefill,
|
|
@@ -4347,7 +5683,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
4347
5683
|
onPasteImage: handlePasteImage
|
|
4348
5684
|
}
|
|
4349
5685
|
),
|
|
4350
|
-
/* @__PURE__ */
|
|
5686
|
+
/* @__PURE__ */ jsx11(
|
|
4351
5687
|
StatusBar,
|
|
4352
5688
|
{
|
|
4353
5689
|
cwd,
|
|
@@ -4356,6 +5692,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
4356
5692
|
tokens: usageRef.current.totalTokens,
|
|
4357
5693
|
gitBranch,
|
|
4358
5694
|
contextPercent,
|
|
5695
|
+
cachePercent: usageRef.current.cache ? cacheHitPercent(usageRef.current.cache) : null,
|
|
4359
5696
|
sessionCostUsd: sessionCost,
|
|
4360
5697
|
backgroundTasks: bgLabels
|
|
4361
5698
|
}
|
|
@@ -4367,7 +5704,7 @@ function renderRepl(backend, opts) {
|
|
|
4367
5704
|
defaultTaskManager.cleanup();
|
|
4368
5705
|
});
|
|
4369
5706
|
return render(
|
|
4370
|
-
/* @__PURE__ */
|
|
5707
|
+
/* @__PURE__ */ jsx11(
|
|
4371
5708
|
Repl,
|
|
4372
5709
|
{
|
|
4373
5710
|
backend,
|
|
@@ -4384,7 +5721,7 @@ function renderRepl(backend, opts) {
|
|
|
4384
5721
|
}
|
|
4385
5722
|
|
|
4386
5723
|
// src/config/loader.ts
|
|
4387
|
-
import
|
|
5724
|
+
import fs13 from "fs";
|
|
4388
5725
|
import { parse as parse2 } from "smol-toml";
|
|
4389
5726
|
var PartialConfigSchema = ConfigSchema.partial();
|
|
4390
5727
|
function parseTomlFile(content, filePath) {
|
|
@@ -4412,11 +5749,12 @@ function applyOverrides(config, overrides) {
|
|
|
4412
5749
|
return merged;
|
|
4413
5750
|
}
|
|
4414
5751
|
function readTomlFileSync(filePath) {
|
|
4415
|
-
if (!
|
|
4416
|
-
const content =
|
|
5752
|
+
if (!fs13.existsSync(filePath)) return {};
|
|
5753
|
+
const content = fs13.readFileSync(filePath, "utf8");
|
|
4417
5754
|
return parseTomlFile(content, filePath);
|
|
4418
5755
|
}
|
|
4419
5756
|
function loadConfigSync(cwd, overrides) {
|
|
5757
|
+
loadEnvFile();
|
|
4420
5758
|
const global = readTomlFileSync(globalConfigPath());
|
|
4421
5759
|
const project = readTomlFileSync(projectConfigPath(cwd));
|
|
4422
5760
|
const merged = applyOverrides(mergeConfig(global, project), overrides);
|
|
@@ -4517,14 +5855,17 @@ async function printMode(loop, prompt, cwd, json, imagePaths) {
|
|
|
4517
5855
|
case "finish":
|
|
4518
5856
|
usage.add(event.usage);
|
|
4519
5857
|
break;
|
|
4520
|
-
case "retry":
|
|
4521
|
-
if (!json)
|
|
5858
|
+
case "retry": {
|
|
5859
|
+
if (!json) {
|
|
5860
|
+
const wait = event.delayMs !== void 0 && event.delayMs >= 1e3 ? ` in ${Math.round(event.delayMs / 1e3)}s` : "";
|
|
4522
5861
|
process.stderr.write(
|
|
4523
5862
|
`
|
|
4524
|
-
[retry ${event.attempt}/${event.maxAttempts}] ${event.reason}
|
|
5863
|
+
[retry ${event.attempt}/${event.maxAttempts}${wait}] ${event.reason}
|
|
4525
5864
|
`
|
|
4526
5865
|
);
|
|
5866
|
+
}
|
|
4527
5867
|
break;
|
|
5868
|
+
}
|
|
4528
5869
|
case "notice":
|
|
4529
5870
|
if (!json) process.stderr.write(`
|
|
4530
5871
|
[notice] ${event.message}
|
|
@@ -4659,6 +6000,7 @@ program.name("star").description("Star CLI \u2014 an AI agent command-line inter
|
|
|
4659
6000
|
}
|
|
4660
6001
|
if (resumed) {
|
|
4661
6002
|
await loop.loadMessages(resumed.messages);
|
|
6003
|
+
await loadTodos(cwd);
|
|
4662
6004
|
if (sessionStore) {
|
|
4663
6005
|
hydrateSnapshots(await loadSessionSnapshots(sessionStore.dir));
|
|
4664
6006
|
}
|