@cryer/star-cli 0.2.1 → 0.3.0
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 +23 -9
- package/dist/{chunk-MJ2O7NWI.js → chunk-DZWTIMJ2.js} +713 -276
- package/dist/chunk-DZWTIMJ2.js.map +1 -0
- package/dist/{loop-R3RQCNFT.js → loop-EIODKYPT.js} +2 -2
- package/dist/main.js +733 -170
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-MJ2O7NWI.js.map +0 -1
- /package/dist/{loop-R3RQCNFT.js.map → loop-EIODKYPT.js.map} +0 -0
package/dist/main.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
SKIP_DIRS,
|
|
5
5
|
TodoStore,
|
|
6
6
|
WORKING_DIFF_MAX_LINES,
|
|
7
|
+
appendUserMemory,
|
|
7
8
|
bashTool,
|
|
8
9
|
buildAllowRule,
|
|
9
10
|
buildCommitPrompt,
|
|
@@ -12,6 +13,8 @@ import {
|
|
|
12
13
|
collectWorkingDiff,
|
|
13
14
|
compactMessages,
|
|
14
15
|
createDefaultRegistry,
|
|
16
|
+
createIgnorePredicate,
|
|
17
|
+
defaultAgentTasks,
|
|
15
18
|
defaultTaskManager,
|
|
16
19
|
discoverSkills,
|
|
17
20
|
estimateTokens,
|
|
@@ -27,6 +30,8 @@ import {
|
|
|
27
30
|
isGitRepo,
|
|
28
31
|
isSensitivePath,
|
|
29
32
|
listSnapshots,
|
|
33
|
+
loadTodos,
|
|
34
|
+
parseTodoArgs,
|
|
30
35
|
projectConfigPath,
|
|
31
36
|
reconcileToolCalls,
|
|
32
37
|
resolveShell,
|
|
@@ -34,8 +39,9 @@ import {
|
|
|
34
39
|
sessionsDir,
|
|
35
40
|
starHome,
|
|
36
41
|
summarizeMessages,
|
|
37
|
-
undoTurnSnapshots
|
|
38
|
-
|
|
42
|
+
undoTurnSnapshots,
|
|
43
|
+
userMemoryPath
|
|
44
|
+
} from "./chunk-DZWTIMJ2.js";
|
|
39
45
|
|
|
40
46
|
// src/main.tsx
|
|
41
47
|
import { Command } from "commander";
|
|
@@ -269,14 +275,17 @@ var UsageTracker = class {
|
|
|
269
275
|
};
|
|
270
276
|
|
|
271
277
|
// src/cli/repl.tsx
|
|
272
|
-
import { Box as
|
|
273
|
-
import { useCallback, useEffect as useEffect2, useMemo, useRef as
|
|
278
|
+
import { Box as Box11, Text as Text12, render, useApp, useInput as useInput6 } from "ink";
|
|
279
|
+
import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef3, useState as useState3 } from "react";
|
|
274
280
|
|
|
275
281
|
// src/config/save.ts
|
|
276
282
|
import fs from "fs";
|
|
277
283
|
import path2 from "path";
|
|
278
284
|
import { parse, stringify } from "smol-toml";
|
|
279
285
|
async function addAllowRule(rule) {
|
|
286
|
+
return addPermissionRule("allow", rule);
|
|
287
|
+
}
|
|
288
|
+
async function addPermissionRule(kind, rule) {
|
|
280
289
|
const filePath = globalConfigPath();
|
|
281
290
|
let raw = {};
|
|
282
291
|
try {
|
|
@@ -286,9 +295,9 @@ async function addAllowRule(rule) {
|
|
|
286
295
|
if (error.code !== "ENOENT") throw error;
|
|
287
296
|
}
|
|
288
297
|
const permissions = typeof raw.permissions === "object" && raw.permissions !== null ? raw.permissions : {};
|
|
289
|
-
const
|
|
290
|
-
if (
|
|
291
|
-
permissions
|
|
298
|
+
const rules = Array.isArray(permissions[kind]) ? permissions[kind] : [];
|
|
299
|
+
if (rules.includes(rule)) return false;
|
|
300
|
+
permissions[kind] = [...rules, rule];
|
|
292
301
|
raw.permissions = permissions;
|
|
293
302
|
await fs.promises.mkdir(path2.dirname(filePath), { recursive: true });
|
|
294
303
|
await fs.promises.writeFile(filePath, stringify(raw), "utf8");
|
|
@@ -332,7 +341,8 @@ var ModelConfigSchema = z.object({
|
|
|
332
341
|
completionPrice: z.number().nonnegative().optional()
|
|
333
342
|
});
|
|
334
343
|
var PermissionsConfigSchema = z.object({
|
|
335
|
-
allow: z.array(z.string()).default([])
|
|
344
|
+
allow: z.array(z.string()).default([]),
|
|
345
|
+
deny: z.array(z.string()).default([])
|
|
336
346
|
});
|
|
337
347
|
var HookConfigSchema = z.object({
|
|
338
348
|
event: z.enum(["PreToolUse", "PostToolUse", "Stop"]),
|
|
@@ -361,6 +371,20 @@ var ConfigSchema = z.object({
|
|
|
361
371
|
// gracefully (some relays never send the terminal chunks). Applies once
|
|
362
372
|
// streaming has started; the first part gets a longer, fixed allowance.
|
|
363
373
|
streamIdleTimeoutSec: z.number().positive().default(20),
|
|
374
|
+
// Seconds to wait for the very first stream part before giving up; slow
|
|
375
|
+
// thinking-model relays can buffer for minutes before sending anything.
|
|
376
|
+
streamFirstChunkTimeoutSec: z.number().positive().default(300),
|
|
377
|
+
// Extra attempts per model request when a stream fails transiently
|
|
378
|
+
// (network error, 429/5xx, idle watchdog cutoff) or comes back empty, so a
|
|
379
|
+
// relay hiccup does not silently end a half-finished turn. 0 disables.
|
|
380
|
+
// Retries after a timeout/empty failure scale both stream timeouts up
|
|
381
|
+
// (attempt number ×, capped at 3x) since the relay is likely overloaded.
|
|
382
|
+
streamMaxRetries: z.number().int().min(0).default(3),
|
|
383
|
+
// How many times a turn may be nudged to continue when the model ends it
|
|
384
|
+
// with a text-only reply that announces pending work ("我将…", "I will…")
|
|
385
|
+
// instead of tool calls — a common premature-stop failure of weaker
|
|
386
|
+
// agentic models. Never fires in plan mode. 0 disables.
|
|
387
|
+
maxAutoContinues: z.number().int().min(0).default(2),
|
|
364
388
|
contextCompaction: z.enum(["summary", "truncate"]).default("summary"),
|
|
365
389
|
// Optional per-session dollar budget; when set, the REPL stops the session
|
|
366
390
|
// once the estimated cost for the session reaches this amount.
|
|
@@ -370,7 +394,7 @@ var ConfigSchema = z.object({
|
|
|
370
394
|
// STAR_NO_NOTIFY=1 disables without touching the config.
|
|
371
395
|
notifyBell: z.boolean().default(true),
|
|
372
396
|
notifyBellThresholdSec: z.number().positive().default(10),
|
|
373
|
-
permissions: PermissionsConfigSchema.default({ allow: [] }),
|
|
397
|
+
permissions: PermissionsConfigSchema.default({ allow: [], deny: [] }),
|
|
374
398
|
hooks: z.array(HookConfigSchema).default([])
|
|
375
399
|
});
|
|
376
400
|
function contextWindowTokens(config, modelName) {
|
|
@@ -1253,6 +1277,61 @@ function stringifyPart(value) {
|
|
|
1253
1277
|
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
1254
1278
|
}
|
|
1255
1279
|
|
|
1280
|
+
// src/cli/commands/builtin.ts
|
|
1281
|
+
import { readFileSync } from "fs";
|
|
1282
|
+
|
|
1283
|
+
// src/session/search.ts
|
|
1284
|
+
var SNIPPET_LENGTH = 100;
|
|
1285
|
+
function flattenMessage(message) {
|
|
1286
|
+
const content = message.content;
|
|
1287
|
+
if (typeof content === "string") return content;
|
|
1288
|
+
if (!Array.isArray(content)) return "";
|
|
1289
|
+
const parts = [];
|
|
1290
|
+
for (const part of content) {
|
|
1291
|
+
if (part.type === "text") {
|
|
1292
|
+
parts.push(part.text);
|
|
1293
|
+
} else if (part.type === "tool-call") {
|
|
1294
|
+
parts.push(JSON.stringify(part.args));
|
|
1295
|
+
} else if (part.type === "tool-result") {
|
|
1296
|
+
parts.push(typeof part.result === "string" ? part.result : JSON.stringify(part.result));
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return parts.join("\n");
|
|
1300
|
+
}
|
|
1301
|
+
function makeSnippet(text, query) {
|
|
1302
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
1303
|
+
const index = flat.toLowerCase().indexOf(query.toLowerCase());
|
|
1304
|
+
if (index === -1 || flat.length <= SNIPPET_LENGTH) return flat.slice(0, SNIPPET_LENGTH);
|
|
1305
|
+
const side = Math.max(0, Math.floor((SNIPPET_LENGTH - query.length) / 2));
|
|
1306
|
+
let start = Math.max(0, index - side);
|
|
1307
|
+
const end = Math.min(flat.length, start + SNIPPET_LENGTH);
|
|
1308
|
+
start = Math.max(0, end - SNIPPET_LENGTH);
|
|
1309
|
+
return `${start > 0 ? "\u2026" : ""}${flat.slice(start, end)}${end < flat.length ? "\u2026" : ""}`;
|
|
1310
|
+
}
|
|
1311
|
+
async function searchSessions(query, limit = 10) {
|
|
1312
|
+
const hits = [];
|
|
1313
|
+
const needle = query.toLowerCase();
|
|
1314
|
+
for (const meta of await SessionStore.list()) {
|
|
1315
|
+
if (hits.length >= limit) break;
|
|
1316
|
+
const store = await SessionStore.open(meta.id);
|
|
1317
|
+
if (!store) continue;
|
|
1318
|
+
const text = (await store.messages()).map(flattenMessage).filter((part) => part.length > 0).join("\n");
|
|
1319
|
+
if (text.toLowerCase().includes(needle)) {
|
|
1320
|
+
hits.push({ meta, snippet: makeSnippet(text, query) });
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
return hits;
|
|
1324
|
+
}
|
|
1325
|
+
function formatSearchResults(hits, query) {
|
|
1326
|
+
if (hits.length === 0) return `No sessions matching "${query}".`;
|
|
1327
|
+
const lines = hits.map(({ meta, snippet }) => {
|
|
1328
|
+
const title = meta.title || "(untitled)";
|
|
1329
|
+
return `${shortSessionId(meta.id)} ${title} (${relativeTime(meta.updatedAt)})
|
|
1330
|
+
${snippet}`;
|
|
1331
|
+
});
|
|
1332
|
+
return [...lines, "Resume with /resume <id>"].join("\n");
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1256
1335
|
// src/cli/diff-preview.ts
|
|
1257
1336
|
import { readFile as readFile3 } from "fs/promises";
|
|
1258
1337
|
import path10 from "path";
|
|
@@ -1440,31 +1519,41 @@ async function generateDiffPreview(toolName, args, cwd) {
|
|
|
1440
1519
|
|
|
1441
1520
|
// src/cli/commands/builtin.ts
|
|
1442
1521
|
function registerBuiltinCommands(registry) {
|
|
1443
|
-
registry.register(
|
|
1522
|
+
const register = (command) => registry.register(command);
|
|
1523
|
+
register({
|
|
1444
1524
|
name: "help",
|
|
1445
1525
|
description: "List available commands",
|
|
1446
1526
|
usage: "/help",
|
|
1527
|
+
category: "General",
|
|
1447
1528
|
run(_args, ctx) {
|
|
1448
|
-
const
|
|
1529
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1530
|
+
for (const cmd of registry.list()) {
|
|
1531
|
+
const category = cmd.category ?? "Other";
|
|
1449
1532
|
const usage = cmd.usage ?? `/${cmd.name}`;
|
|
1450
|
-
|
|
1451
|
-
|
|
1533
|
+
const lines = groups.get(category) ?? [];
|
|
1534
|
+
lines.push(` ${usage} - ${cmd.description}`);
|
|
1535
|
+
groups.set(category, lines);
|
|
1536
|
+
}
|
|
1537
|
+
const sections = [...groups.entries()].map(([name, lines]) => [name, ...lines].join("\n"));
|
|
1452
1538
|
ctx.addSystemMessage(`Available commands:
|
|
1453
|
-
|
|
1539
|
+
|
|
1540
|
+
${sections.join("\n\n")}`);
|
|
1454
1541
|
}
|
|
1455
1542
|
});
|
|
1456
|
-
|
|
1543
|
+
register({
|
|
1457
1544
|
name: "clear",
|
|
1458
1545
|
description: "Clear message history",
|
|
1459
1546
|
usage: "/clear",
|
|
1547
|
+
category: "General",
|
|
1460
1548
|
run(_args, ctx) {
|
|
1461
1549
|
ctx.clearMessages();
|
|
1462
1550
|
}
|
|
1463
1551
|
});
|
|
1464
|
-
|
|
1552
|
+
register({
|
|
1465
1553
|
name: "new",
|
|
1466
1554
|
description: "Start a new session with a clean context",
|
|
1467
1555
|
usage: "/new",
|
|
1556
|
+
category: "Sessions",
|
|
1468
1557
|
async run(_args, ctx) {
|
|
1469
1558
|
if (!ctx.newSession) {
|
|
1470
1559
|
ctx.addSystemMessage("/new: this context cannot start a new session.");
|
|
@@ -1473,10 +1562,11 @@ ${lines.join("\n")}`);
|
|
|
1473
1562
|
ctx.addSystemMessage(await ctx.newSession());
|
|
1474
1563
|
}
|
|
1475
1564
|
});
|
|
1476
|
-
|
|
1565
|
+
register({
|
|
1477
1566
|
name: "clear-sessions",
|
|
1478
1567
|
description: "Delete stored sessions: this directory by default, every session with --all (the current session is kept)",
|
|
1479
1568
|
usage: "/clear-sessions [--all]",
|
|
1569
|
+
category: "Sessions",
|
|
1480
1570
|
async run(args, ctx) {
|
|
1481
1571
|
const arg = args.trim();
|
|
1482
1572
|
if (arg !== "" && arg !== "--all") {
|
|
@@ -1492,157 +1582,242 @@ ${lines.join("\n")}`);
|
|
|
1492
1582
|
ctx.addSystemMessage(await ctx.clearSessions(arg === "--all"));
|
|
1493
1583
|
}
|
|
1494
1584
|
});
|
|
1495
|
-
|
|
1585
|
+
register({
|
|
1496
1586
|
name: "exit",
|
|
1497
1587
|
description: "Exit the application",
|
|
1498
1588
|
usage: "/exit",
|
|
1589
|
+
category: "General",
|
|
1499
1590
|
run(_args, ctx) {
|
|
1500
1591
|
ctx.exit();
|
|
1501
1592
|
}
|
|
1502
1593
|
});
|
|
1503
|
-
|
|
1594
|
+
register({
|
|
1504
1595
|
name: "q",
|
|
1505
1596
|
description: "Exit the application (alias of /exit)",
|
|
1506
1597
|
usage: "/q",
|
|
1598
|
+
category: "General",
|
|
1507
1599
|
run(_args, ctx) {
|
|
1508
1600
|
ctx.exit();
|
|
1509
1601
|
}
|
|
1510
1602
|
});
|
|
1511
|
-
|
|
1603
|
+
register({
|
|
1512
1604
|
name: "model",
|
|
1513
|
-
description: "
|
|
1605
|
+
description: "Switch the current model (opens a picker when no name is given)",
|
|
1514
1606
|
usage: "/model [name]",
|
|
1607
|
+
category: "Settings",
|
|
1515
1608
|
async run(args, ctx) {
|
|
1516
1609
|
if (!args) {
|
|
1517
|
-
ctx.addSystemMessage(ctx.
|
|
1610
|
+
ctx.addSystemMessage(await ctx.pickModel());
|
|
1518
1611
|
} else {
|
|
1519
1612
|
ctx.addSystemMessage(await ctx.switchModel(args));
|
|
1520
1613
|
}
|
|
1521
1614
|
}
|
|
1522
1615
|
});
|
|
1523
|
-
|
|
1616
|
+
register({
|
|
1524
1617
|
name: "resume",
|
|
1525
|
-
description: "
|
|
1618
|
+
description: "Resume a session by id, or pick one from a list (--all for every directory)",
|
|
1526
1619
|
usage: "/resume [sessionId | --all]",
|
|
1620
|
+
category: "Sessions",
|
|
1527
1621
|
async run(args, ctx) {
|
|
1528
1622
|
const arg = args.trim();
|
|
1529
1623
|
if (arg === "--all") {
|
|
1530
|
-
ctx.addSystemMessage(await ctx.
|
|
1624
|
+
ctx.addSystemMessage(await ctx.pickSession(true));
|
|
1531
1625
|
} else if (!arg) {
|
|
1532
|
-
ctx.addSystemMessage(await ctx.
|
|
1626
|
+
ctx.addSystemMessage(await ctx.pickSession(false));
|
|
1533
1627
|
} else {
|
|
1534
1628
|
ctx.addSystemMessage(await ctx.resumeSession(arg));
|
|
1535
1629
|
}
|
|
1536
1630
|
}
|
|
1537
1631
|
});
|
|
1538
|
-
|
|
1632
|
+
register({
|
|
1633
|
+
name: "fork",
|
|
1634
|
+
description: "Fork the current session into a new one and switch to it",
|
|
1635
|
+
usage: "/fork",
|
|
1636
|
+
category: "Sessions",
|
|
1637
|
+
async run(_args, ctx) {
|
|
1638
|
+
ctx.addSystemMessage(await ctx.forkSession());
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1641
|
+
register({
|
|
1642
|
+
name: "search",
|
|
1643
|
+
description: "Full-text search across all stored sessions",
|
|
1644
|
+
usage: "/search <query>",
|
|
1645
|
+
category: "Sessions",
|
|
1646
|
+
async run(args, ctx) {
|
|
1647
|
+
const query = args.trim();
|
|
1648
|
+
if (!query) {
|
|
1649
|
+
ctx.addSystemMessage("Usage: /search <query>");
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
ctx.addSystemMessage(formatSearchResults(await searchSessions(query), query));
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
register({
|
|
1539
1656
|
name: "todo",
|
|
1540
1657
|
description: "Show the current todo list",
|
|
1541
1658
|
usage: "/todo",
|
|
1659
|
+
category: "Info",
|
|
1542
1660
|
async run(_args, ctx) {
|
|
1543
1661
|
ctx.addSystemMessage(await ctx.showTodos());
|
|
1544
1662
|
}
|
|
1545
1663
|
});
|
|
1546
|
-
|
|
1664
|
+
register({
|
|
1547
1665
|
name: "tasks",
|
|
1548
1666
|
description: "List background shell tasks",
|
|
1549
1667
|
usage: "/tasks",
|
|
1668
|
+
category: "Info",
|
|
1550
1669
|
run(_args, ctx) {
|
|
1551
1670
|
ctx.addSystemMessage(ctx.listTasks());
|
|
1552
1671
|
}
|
|
1553
1672
|
});
|
|
1554
|
-
|
|
1673
|
+
register({
|
|
1555
1674
|
name: "cost",
|
|
1556
1675
|
description: "Show API token usage for this session",
|
|
1557
1676
|
usage: "/cost",
|
|
1677
|
+
category: "Info",
|
|
1558
1678
|
run(_args, ctx) {
|
|
1559
1679
|
ctx.addSystemMessage(ctx.showUsage());
|
|
1560
1680
|
}
|
|
1561
1681
|
});
|
|
1562
|
-
|
|
1682
|
+
register({
|
|
1563
1683
|
name: "usage",
|
|
1564
1684
|
description: "Show token usage aggregated across all sessions",
|
|
1565
1685
|
usage: "/usage",
|
|
1686
|
+
category: "Info",
|
|
1566
1687
|
async run(_args, ctx) {
|
|
1567
1688
|
ctx.addSystemMessage(await ctx.showGlobalUsage());
|
|
1568
1689
|
}
|
|
1569
1690
|
});
|
|
1570
|
-
|
|
1691
|
+
register({
|
|
1571
1692
|
name: "config",
|
|
1572
1693
|
description: "Show the current configuration",
|
|
1573
1694
|
usage: "/config",
|
|
1695
|
+
category: "Info",
|
|
1574
1696
|
run(_args, ctx) {
|
|
1575
1697
|
ctx.addSystemMessage(ctx.describeConfig());
|
|
1576
1698
|
}
|
|
1577
1699
|
});
|
|
1578
|
-
|
|
1700
|
+
register({
|
|
1579
1701
|
name: "compact",
|
|
1580
1702
|
description: "Compact the conversation history to free up context",
|
|
1581
1703
|
usage: "/compact",
|
|
1704
|
+
category: "Sessions",
|
|
1582
1705
|
async run(_args, ctx) {
|
|
1583
1706
|
ctx.addSystemMessage(await ctx.compactContext());
|
|
1584
1707
|
}
|
|
1585
1708
|
});
|
|
1586
|
-
|
|
1709
|
+
register({
|
|
1587
1710
|
name: "export",
|
|
1588
1711
|
description: "Export the current session to a Markdown file",
|
|
1589
1712
|
usage: "/export [path]",
|
|
1713
|
+
category: "Sessions",
|
|
1590
1714
|
async run(args, ctx) {
|
|
1591
1715
|
ctx.addSystemMessage(await ctx.exportSession(args));
|
|
1592
1716
|
}
|
|
1593
1717
|
});
|
|
1594
|
-
|
|
1718
|
+
register({
|
|
1595
1719
|
name: "permission",
|
|
1596
|
-
description: "
|
|
1720
|
+
description: "Set the permission mode (opens a picker when no mode is given)",
|
|
1597
1721
|
usage: "/permission [ask|auto|readonly|yolo]",
|
|
1722
|
+
category: "Settings",
|
|
1598
1723
|
async run(args, ctx) {
|
|
1599
|
-
|
|
1724
|
+
const arg = args.trim();
|
|
1725
|
+
if (!arg) {
|
|
1726
|
+
ctx.addSystemMessage(await ctx.pickPermissionMode());
|
|
1727
|
+
} else {
|
|
1728
|
+
ctx.addSystemMessage(await ctx.permissionMode(arg));
|
|
1729
|
+
}
|
|
1600
1730
|
}
|
|
1601
1731
|
});
|
|
1602
|
-
|
|
1732
|
+
register({
|
|
1603
1733
|
name: "plan",
|
|
1604
1734
|
description: "Toggle plan mode: read-only research, then approve the plan before executing",
|
|
1605
1735
|
usage: "/plan",
|
|
1736
|
+
category: "Settings",
|
|
1606
1737
|
async run(_args, ctx) {
|
|
1607
1738
|
ctx.addSystemMessage(await ctx.planMode());
|
|
1608
1739
|
}
|
|
1609
1740
|
});
|
|
1610
|
-
|
|
1741
|
+
register({
|
|
1742
|
+
name: "memory",
|
|
1743
|
+
description: "Show user memory (MEMORY.md), or add an entry",
|
|
1744
|
+
usage: "/memory [add <text>]",
|
|
1745
|
+
category: "Settings",
|
|
1746
|
+
run(args, ctx) {
|
|
1747
|
+
const arg = args.trim();
|
|
1748
|
+
if (arg === "add" || arg.startsWith("add ")) {
|
|
1749
|
+
const text = arg.slice(3).trim();
|
|
1750
|
+
if (!text) {
|
|
1751
|
+
ctx.addSystemMessage("Usage: /memory add <text>");
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
appendUserMemory(text);
|
|
1755
|
+
ctx.addSystemMessage(`Saved to user memory: - ${text}`);
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
if (arg) {
|
|
1759
|
+
ctx.addSystemMessage(`Unknown argument "${arg}". Usage: /memory [add <text>]`);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
const file = userMemoryPath();
|
|
1763
|
+
let content = "";
|
|
1764
|
+
try {
|
|
1765
|
+
content = readFileSync(file, "utf8").trim();
|
|
1766
|
+
} catch {
|
|
1767
|
+
content = "";
|
|
1768
|
+
}
|
|
1769
|
+
if (!content) {
|
|
1770
|
+
ctx.addSystemMessage(
|
|
1771
|
+
`No user memory yet (${file}).
|
|
1772
|
+
Add entries with /memory add <text> or ask the agent to remember something.`
|
|
1773
|
+
);
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
ctx.addSystemMessage(`User memory (${file}):
|
|
1777
|
+
${content}`);
|
|
1778
|
+
}
|
|
1779
|
+
});
|
|
1780
|
+
register({
|
|
1611
1781
|
name: "undo",
|
|
1612
1782
|
description: "Undo the last conversation turn: revert its file changes (write_file/edit_file) and retract its messages",
|
|
1613
1783
|
usage: "/undo",
|
|
1784
|
+
category: "Changes",
|
|
1614
1785
|
async run(_args, ctx) {
|
|
1615
1786
|
ctx.addSystemMessage(await ctx.undo());
|
|
1616
1787
|
}
|
|
1617
1788
|
});
|
|
1618
|
-
|
|
1789
|
+
register({
|
|
1619
1790
|
name: "rewind",
|
|
1620
1791
|
description: "List file-change checkpoints, or rewind to just before one: restore files and retract the conversation",
|
|
1621
1792
|
usage: "/rewind [n]",
|
|
1793
|
+
category: "Changes",
|
|
1622
1794
|
async run(args, ctx) {
|
|
1623
1795
|
ctx.addSystemMessage(await ctx.rewind(args.trim()));
|
|
1624
1796
|
}
|
|
1625
1797
|
});
|
|
1626
|
-
|
|
1798
|
+
register({
|
|
1627
1799
|
name: "init",
|
|
1628
1800
|
description: "Generate an AGENTS.md for the current project",
|
|
1629
1801
|
usage: "/init [force]",
|
|
1802
|
+
category: "Settings",
|
|
1630
1803
|
async run(args, ctx) {
|
|
1631
1804
|
ctx.addSystemMessage(await ctx.initProject(args));
|
|
1632
1805
|
}
|
|
1633
1806
|
});
|
|
1634
|
-
|
|
1807
|
+
register({
|
|
1635
1808
|
name: "doctor",
|
|
1636
1809
|
description: "Run environment and configuration checks",
|
|
1637
1810
|
usage: "/doctor",
|
|
1811
|
+
category: "Info",
|
|
1638
1812
|
async run(_args, ctx) {
|
|
1639
1813
|
ctx.addSystemMessage(await ctx.runDoctor());
|
|
1640
1814
|
}
|
|
1641
1815
|
});
|
|
1642
|
-
|
|
1816
|
+
register({
|
|
1643
1817
|
name: "skills",
|
|
1644
1818
|
description: "List available skills (project scope overrides user scope)",
|
|
1645
1819
|
usage: "/skills",
|
|
1820
|
+
category: "Info",
|
|
1646
1821
|
run(_args, ctx) {
|
|
1647
1822
|
const skills = discoverSkills(ctx.cwd ?? process.cwd());
|
|
1648
1823
|
if (skills.length === 0) {
|
|
@@ -1656,10 +1831,11 @@ ${lines.join("\n")}`);
|
|
|
1656
1831
|
${lines.join("\n")}`);
|
|
1657
1832
|
}
|
|
1658
1833
|
});
|
|
1659
|
-
|
|
1834
|
+
register({
|
|
1660
1835
|
name: "commit",
|
|
1661
1836
|
description: "Analyze uncommitted changes and create a git commit (Conventional Commits)",
|
|
1662
1837
|
usage: "/commit [instructions]",
|
|
1838
|
+
category: "Changes",
|
|
1663
1839
|
async run(args, ctx) {
|
|
1664
1840
|
const cwd = ctx.cwd ?? process.cwd();
|
|
1665
1841
|
if (!isGitRepo(cwd)) {
|
|
@@ -1678,10 +1854,11 @@ ${lines.join("\n")}`);
|
|
|
1678
1854
|
ctx.addSystemMessage("/commit: this context cannot submit prompts to the model.");
|
|
1679
1855
|
}
|
|
1680
1856
|
});
|
|
1681
|
-
|
|
1857
|
+
register({
|
|
1682
1858
|
name: "copy",
|
|
1683
1859
|
description: "Copy the last assistant reply to the clipboard (`all` for the whole conversation)",
|
|
1684
1860
|
usage: "/copy [all]",
|
|
1861
|
+
category: "Sessions",
|
|
1685
1862
|
async run(args, ctx) {
|
|
1686
1863
|
const arg = args.trim();
|
|
1687
1864
|
if (arg !== "" && arg !== "all") {
|
|
@@ -1706,10 +1883,11 @@ ${lines.join("\n")}`);
|
|
|
1706
1883
|
);
|
|
1707
1884
|
}
|
|
1708
1885
|
});
|
|
1709
|
-
|
|
1886
|
+
register({
|
|
1710
1887
|
name: "diff",
|
|
1711
1888
|
description: "Show uncommitted changes (git status + colored diff)",
|
|
1712
1889
|
usage: "/diff",
|
|
1890
|
+
category: "Changes",
|
|
1713
1891
|
run(_args, ctx) {
|
|
1714
1892
|
const cwd = ctx.cwd ?? process.cwd();
|
|
1715
1893
|
if (!isGitRepo(cwd)) {
|
|
@@ -1745,7 +1923,7 @@ ${result.diff}${note ? `
|
|
|
1745
1923
|
}
|
|
1746
1924
|
|
|
1747
1925
|
// src/cli/commands/custom.ts
|
|
1748
|
-
import { readFileSync, readdirSync } from "fs";
|
|
1926
|
+
import { readFileSync as readFileSync2, readdirSync } from "fs";
|
|
1749
1927
|
import path11 from "path";
|
|
1750
1928
|
var NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
1751
1929
|
var DESCRIPTION_PATTERN = /^<!--\s*description:\s*(.+?)\s*-->\s*$/;
|
|
@@ -1753,7 +1931,7 @@ var DEFAULT_DESCRIPTION = "Custom prompt command";
|
|
|
1753
1931
|
function parseCommandFile(filePath, name) {
|
|
1754
1932
|
let raw;
|
|
1755
1933
|
try {
|
|
1756
|
-
raw =
|
|
1934
|
+
raw = readFileSync2(filePath, "utf8");
|
|
1757
1935
|
} catch {
|
|
1758
1936
|
return null;
|
|
1759
1937
|
}
|
|
@@ -1799,6 +1977,7 @@ function registerCustomCommands(registry, cwd, home) {
|
|
|
1799
1977
|
name: command.name,
|
|
1800
1978
|
description: command.description,
|
|
1801
1979
|
usage: `/${command.name} [args]`,
|
|
1980
|
+
category: "Custom",
|
|
1802
1981
|
run(args, ctx) {
|
|
1803
1982
|
const prompt = command.template.split("$ARGUMENTS").join(args);
|
|
1804
1983
|
if (ctx.submitPrompt) {
|
|
@@ -2215,6 +2394,33 @@ function formatCheckpointList(snapshots, cwd) {
|
|
|
2215
2394
|
].join("\n");
|
|
2216
2395
|
}
|
|
2217
2396
|
|
|
2397
|
+
// src/cli/commands/suggest.ts
|
|
2398
|
+
var MAX_SUGGESTIONS = 5;
|
|
2399
|
+
function isSubsequence(query, target) {
|
|
2400
|
+
let i = 0;
|
|
2401
|
+
for (const ch of target) {
|
|
2402
|
+
if (ch === query[i]) i += 1;
|
|
2403
|
+
if (i === query.length) return true;
|
|
2404
|
+
}
|
|
2405
|
+
return query.length === 0;
|
|
2406
|
+
}
|
|
2407
|
+
function filterCommands(input, commands) {
|
|
2408
|
+
if (!input.startsWith("/")) return [];
|
|
2409
|
+
if (/\s/.test(input)) return [];
|
|
2410
|
+
const prefix = input.slice(1).toLowerCase();
|
|
2411
|
+
const byName = (a, b) => a.name.localeCompare(b.name);
|
|
2412
|
+
const prefixed = commands.filter((cmd) => cmd.name.toLowerCase().startsWith(prefix)).sort(byName);
|
|
2413
|
+
const fuzzy = commands.filter(
|
|
2414
|
+
(cmd) => !cmd.name.toLowerCase().startsWith(prefix) && isSubsequence(prefix, cmd.name.toLowerCase())
|
|
2415
|
+
).sort(byName);
|
|
2416
|
+
return [...prefixed, ...fuzzy].slice(0, MAX_SUGGESTIONS);
|
|
2417
|
+
}
|
|
2418
|
+
function didYouMeanSuffix(names) {
|
|
2419
|
+
if (names.length === 0) return "";
|
|
2420
|
+
const listed = names.slice(0, 3).map((name) => `/${name}`);
|
|
2421
|
+
return ` Did you mean: ${listed.join(", ")}?`;
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2218
2424
|
// src/cli/components/ExitPlanPrompt.tsx
|
|
2219
2425
|
import { Box, Text, useInput } from "ink";
|
|
2220
2426
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -2235,15 +2441,6 @@ function ExitPlanPrompt({ onDecision }) {
|
|
|
2235
2441
|
import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
|
|
2236
2442
|
import { useEffect, useRef, useState } from "react";
|
|
2237
2443
|
|
|
2238
|
-
// src/cli/commands/suggest.ts
|
|
2239
|
-
var MAX_SUGGESTIONS = 5;
|
|
2240
|
-
function filterCommands(input, commands) {
|
|
2241
|
-
if (!input.startsWith("/")) return [];
|
|
2242
|
-
if (/\s/.test(input)) return [];
|
|
2243
|
-
const prefix = input.slice(1).toLowerCase();
|
|
2244
|
-
return commands.filter((cmd) => cmd.name.toLowerCase().startsWith(prefix)).sort((a, b) => a.name.localeCompare(b.name)).slice(0, MAX_SUGGESTIONS);
|
|
2245
|
-
}
|
|
2246
|
-
|
|
2247
2444
|
// src/cli/path-suggest.ts
|
|
2248
2445
|
import { readdir } from "fs/promises";
|
|
2249
2446
|
import path15 from "path";
|
|
@@ -2264,6 +2461,7 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
|
|
|
2264
2461
|
const dirAbs = path15.resolve(cwd, dirPart === "" ? "." : dirPart);
|
|
2265
2462
|
const entries = await readdir(dirAbs, { withFileTypes: true }).catch(() => null);
|
|
2266
2463
|
if (!entries) return [];
|
|
2464
|
+
const ignore = createIgnorePredicate(cwd);
|
|
2267
2465
|
const lower = basePrefix.toLowerCase();
|
|
2268
2466
|
const matches = [];
|
|
2269
2467
|
let scanned = 0;
|
|
@@ -2275,6 +2473,7 @@ async function suggestPaths(token, cwd, maxScanEntries = MAX_SCAN_ENTRIES, maxSu
|
|
|
2275
2473
|
if (isDir && SKIP_DIRS.has(entry.name)) continue;
|
|
2276
2474
|
if (entry.name.startsWith(".") && !basePrefix.startsWith(".")) continue;
|
|
2277
2475
|
if (!entry.name.toLowerCase().startsWith(lower)) continue;
|
|
2476
|
+
if (ignore?.(path15.join(dirAbs, entry.name), isDir)) continue;
|
|
2278
2477
|
matches.push({ path: `${dirPart}${entry.name}${isDir ? "/" : ""}`, isDir });
|
|
2279
2478
|
}
|
|
2280
2479
|
matches.sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -2289,6 +2488,7 @@ function InputBox({
|
|
|
2289
2488
|
commands,
|
|
2290
2489
|
cwd,
|
|
2291
2490
|
refill,
|
|
2491
|
+
initialHistory,
|
|
2292
2492
|
onSubmit,
|
|
2293
2493
|
onInterrupt,
|
|
2294
2494
|
onExit,
|
|
@@ -2296,7 +2496,7 @@ function InputBox({
|
|
|
2296
2496
|
}) {
|
|
2297
2497
|
const [value, setValue] = useState("");
|
|
2298
2498
|
const [cursor, setCursor] = useState(0);
|
|
2299
|
-
const [history, setHistory] = useState([]);
|
|
2499
|
+
const [history, setHistory] = useState(() => initialHistory ?? []);
|
|
2300
2500
|
const [highlight, setHighlight] = useState(0);
|
|
2301
2501
|
const [suggestionsDismissed, setSuggestionsDismissed] = useState(false);
|
|
2302
2502
|
const [pathSuggestions, setPathSuggestions] = useState([]);
|
|
@@ -2379,6 +2579,16 @@ function InputBox({
|
|
|
2379
2579
|
return;
|
|
2380
2580
|
}
|
|
2381
2581
|
if (key.return) {
|
|
2582
|
+
if (key.shift) {
|
|
2583
|
+
edit(`${value.slice(0, cursor)}
|
|
2584
|
+
${value.slice(cursor)}`, cursor + 1);
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
if (value[cursor - 1] === "\\") {
|
|
2588
|
+
edit(`${value.slice(0, cursor - 1)}
|
|
2589
|
+
${value.slice(cursor)}`, cursor);
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2382
2592
|
const text = value.trim();
|
|
2383
2593
|
if (text.length > 0) {
|
|
2384
2594
|
setHistory((prev) => [...prev, text]);
|
|
@@ -2389,6 +2599,11 @@ function InputBox({
|
|
|
2389
2599
|
draftRef.current = "";
|
|
2390
2600
|
return;
|
|
2391
2601
|
}
|
|
2602
|
+
if (input === "\n" || input === "\r" || /^\[13;\d+u$/.test(input)) {
|
|
2603
|
+
edit(`${value.slice(0, cursor)}
|
|
2604
|
+
${value.slice(cursor)}`, cursor + 1);
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2392
2607
|
if (key.tab) {
|
|
2393
2608
|
if (key.shift) return;
|
|
2394
2609
|
if (suggestionCount > 0) {
|
|
@@ -2465,25 +2680,25 @@ function InputBox({
|
|
|
2465
2680
|
return;
|
|
2466
2681
|
}
|
|
2467
2682
|
if (input && !key.ctrl && !key.meta) {
|
|
2468
|
-
|
|
2683
|
+
const text = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
2684
|
+
edit(value.slice(0, cursor) + text + value.slice(cursor), cursor + text.length);
|
|
2469
2685
|
}
|
|
2470
2686
|
});
|
|
2471
2687
|
const before = value.slice(0, cursor);
|
|
2472
|
-
const at = value[cursor]
|
|
2688
|
+
const at = value[cursor];
|
|
2473
2689
|
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}`;
|
|
2474
2693
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
2475
|
-
/* @__PURE__ */
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
/* @__PURE__ */ jsx2(Text2, { inverse: true, children:
|
|
2479
|
-
|
|
2480
|
-
] }),
|
|
2481
|
-
suggestions.length > 0 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: suggestions.map(
|
|
2482
|
-
(cmd, index) => index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `/${cmd.name} - ${cmd.description}` }, cmd.name) : /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
2483
|
-
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: `/${cmd.name}` }),
|
|
2694
|
+
/* @__PURE__ */ jsx2(Box2, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { children: styled }) }),
|
|
2695
|
+
suggestions.length > 0 && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: suggestions.map((cmd, index) => {
|
|
2696
|
+
const label = cmd.usage ?? `/${cmd.name}`;
|
|
2697
|
+
return index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `${label} - ${cmd.description}` }, cmd.name) : /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
2698
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: label }),
|
|
2484
2699
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` - ${cmd.description}` })
|
|
2485
|
-
] }, cmd.name)
|
|
2486
|
-
) }),
|
|
2700
|
+
] }, cmd.name);
|
|
2701
|
+
}) }),
|
|
2487
2702
|
showPathSuggestions && /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, children: pathSuggestions.map(
|
|
2488
2703
|
(sugg, index) => index === activeIndex ? /* @__PURE__ */ jsx2(Text2, { bold: true, inverse: true, children: `@${sugg.path}` }, sugg.path) : /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
2489
2704
|
/* @__PURE__ */ jsx2(Text2, { color: "cyan", children: `@${sugg.path}` }),
|
|
@@ -2591,10 +2806,77 @@ function RewindConfirmPrompt({ summary, onDecision }) {
|
|
|
2591
2806
|
] });
|
|
2592
2807
|
}
|
|
2593
2808
|
|
|
2809
|
+
// src/cli/components/SelectPrompt.tsx
|
|
2810
|
+
import { Box as Box6, Text as Text7, useInput as useInput5 } from "ink";
|
|
2811
|
+
import { useRef as useRef2, useState as useState2 } from "react";
|
|
2812
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2813
|
+
var SELECT_PROMPT_MAX_VISIBLE = 9;
|
|
2814
|
+
function SelectPrompt({ title, options, onSelect, onCancel }) {
|
|
2815
|
+
const [index, setIndex] = useState2(0);
|
|
2816
|
+
const indexRef = useRef2(0);
|
|
2817
|
+
const move = (delta) => {
|
|
2818
|
+
indexRef.current = Math.max(0, Math.min(indexRef.current + delta, options.length - 1));
|
|
2819
|
+
setIndex(indexRef.current);
|
|
2820
|
+
};
|
|
2821
|
+
useInput5((input, key) => {
|
|
2822
|
+
if (key.escape) {
|
|
2823
|
+
onCancel();
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
if (key.return) {
|
|
2827
|
+
const option = options[indexRef.current];
|
|
2828
|
+
if (option) onSelect(option.value);
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
if (key.downArrow || input === "j") {
|
|
2832
|
+
move(1);
|
|
2833
|
+
} else if (key.upArrow || input === "k") {
|
|
2834
|
+
move(-1);
|
|
2835
|
+
}
|
|
2836
|
+
});
|
|
2837
|
+
const maxStart = Math.max(0, options.length - SELECT_PROMPT_MAX_VISIBLE);
|
|
2838
|
+
const start = Math.min(Math.max(0, index - Math.floor(SELECT_PROMPT_MAX_VISIBLE / 2)), maxStart);
|
|
2839
|
+
const visible = options.slice(start, start + SELECT_PROMPT_MAX_VISIBLE);
|
|
2840
|
+
const above = start;
|
|
2841
|
+
const below = options.length - start - visible.length;
|
|
2842
|
+
return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
2843
|
+
/* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: title }),
|
|
2844
|
+
above > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2845
|
+
"\u2026 ",
|
|
2846
|
+
above,
|
|
2847
|
+
" more above"
|
|
2848
|
+
] }),
|
|
2849
|
+
visible.map((option, i) => {
|
|
2850
|
+
const active = start + i === index;
|
|
2851
|
+
return /* @__PURE__ */ jsxs7(Text7, { children: [
|
|
2852
|
+
/* @__PURE__ */ jsxs7(Text7, { bold: active, inverse: active, children: [
|
|
2853
|
+
active ? "\u276F " : " ",
|
|
2854
|
+
option.label
|
|
2855
|
+
] }),
|
|
2856
|
+
option.hint && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2857
|
+
" (",
|
|
2858
|
+
option.hint,
|
|
2859
|
+
")"
|
|
2860
|
+
] }),
|
|
2861
|
+
option.description && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2862
|
+
" \u2014 ",
|
|
2863
|
+
option.description
|
|
2864
|
+
] })
|
|
2865
|
+
] }, option.value);
|
|
2866
|
+
}),
|
|
2867
|
+
below > 0 && /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2868
|
+
"\u2026 ",
|
|
2869
|
+
below,
|
|
2870
|
+
" more below"
|
|
2871
|
+
] }),
|
|
2872
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "[\u2191/\u2193 or j/k] move [Enter] select [Esc] cancel" })
|
|
2873
|
+
] });
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2594
2876
|
// src/cli/components/StatusBar.tsx
|
|
2595
|
-
import { Box as
|
|
2877
|
+
import { Box as Box7, Text as Text8, useStdout } from "ink";
|
|
2596
2878
|
import { memo as memo2 } from "react";
|
|
2597
|
-
import { jsxs as
|
|
2879
|
+
import { jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2598
2880
|
var BG_LABEL_MAX = 40;
|
|
2599
2881
|
var WIDE_COLUMNS = 120;
|
|
2600
2882
|
function cwdLabel(cwd, wide) {
|
|
@@ -2616,36 +2898,36 @@ var StatusBar = memo2(function StatusBar2({
|
|
|
2616
2898
|
const wide = (stdout?.columns ?? 80) >= WIDE_COLUMNS;
|
|
2617
2899
|
const joined = backgroundTasks.join(", ");
|
|
2618
2900
|
const labels = joined.length > BG_LABEL_MAX ? `${joined.slice(0, BG_LABEL_MAX)}\u2026` : joined;
|
|
2619
|
-
return /* @__PURE__ */
|
|
2620
|
-
/* @__PURE__ */
|
|
2901
|
+
return /* @__PURE__ */ jsxs8(Box7, { justifyContent: "space-between", children: [
|
|
2902
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2621
2903
|
cwdLabel(cwd, wide),
|
|
2622
2904
|
gitBranch ? ` [${gitBranch}]` : ""
|
|
2623
2905
|
] }),
|
|
2624
|
-
backgroundTasks.length > 0 && /* @__PURE__ */
|
|
2906
|
+
backgroundTasks.length > 0 && /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
|
|
2625
2907
|
"bg: ",
|
|
2626
2908
|
backgroundTasks.length,
|
|
2627
2909
|
" (",
|
|
2628
2910
|
labels,
|
|
2629
2911
|
")"
|
|
2630
2912
|
] }),
|
|
2631
|
-
/* @__PURE__ */
|
|
2913
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2632
2914
|
"model: ",
|
|
2633
2915
|
model
|
|
2634
2916
|
] }),
|
|
2635
|
-
contextPercent != null && /* @__PURE__ */
|
|
2917
|
+
contextPercent != null && /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2636
2918
|
"ctx: ",
|
|
2637
2919
|
contextPercent,
|
|
2638
2920
|
"%"
|
|
2639
2921
|
] }),
|
|
2640
|
-
sessionCostUsd != null && sessionCostUsd > 0 && /* @__PURE__ */
|
|
2922
|
+
sessionCostUsd != null && sessionCostUsd > 0 && /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2641
2923
|
"$",
|
|
2642
2924
|
formatDollars(sessionCostUsd)
|
|
2643
2925
|
] }),
|
|
2644
|
-
/* @__PURE__ */
|
|
2926
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2645
2927
|
tokens2,
|
|
2646
2928
|
" tokens"
|
|
2647
2929
|
] }),
|
|
2648
|
-
/* @__PURE__ */
|
|
2930
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2649
2931
|
"mode: ",
|
|
2650
2932
|
permissionMode
|
|
2651
2933
|
] })
|
|
@@ -2653,22 +2935,22 @@ var StatusBar = memo2(function StatusBar2({
|
|
|
2653
2935
|
});
|
|
2654
2936
|
|
|
2655
2937
|
// src/cli/components/StreamingMessage.tsx
|
|
2656
|
-
import { Box as
|
|
2938
|
+
import { Box as Box8, Text as Text9 } from "ink";
|
|
2657
2939
|
import { memo as memo3 } from "react";
|
|
2658
|
-
import { jsx as
|
|
2940
|
+
import { jsx as jsx8, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2659
2941
|
var StreamingMessage = memo3(function StreamingMessage2({
|
|
2660
2942
|
text,
|
|
2661
2943
|
continuation
|
|
2662
2944
|
}) {
|
|
2663
|
-
return /* @__PURE__ */
|
|
2664
|
-
!continuation && /* @__PURE__ */
|
|
2665
|
-
/* @__PURE__ */
|
|
2945
|
+
return /* @__PURE__ */ jsxs9(Box8, { flexDirection: "column", marginBottom: 1, children: [
|
|
2946
|
+
!continuation && /* @__PURE__ */ jsx8(Text9, { bold: true, color: "green", children: "star" }),
|
|
2947
|
+
/* @__PURE__ */ jsx8(Text9, { color: "green", children: text })
|
|
2666
2948
|
] });
|
|
2667
2949
|
});
|
|
2668
2950
|
|
|
2669
2951
|
// src/cli/components/ThinkingIndicator.tsx
|
|
2670
|
-
import { Box as
|
|
2671
|
-
import { jsxs as
|
|
2952
|
+
import { Box as Box9, Text as Text10 } from "ink";
|
|
2953
|
+
import { jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2672
2954
|
var SPINNER_FRAMES = ["-", "\\", "|", "/"];
|
|
2673
2955
|
var REASONING_TAIL_LENGTH = 200;
|
|
2674
2956
|
var THOUGHT_SUMMARY_LENGTH = 100;
|
|
@@ -2684,19 +2966,44 @@ function ThinkingIndicator({
|
|
|
2684
2966
|
}) {
|
|
2685
2967
|
const tail = reasoning ? truncateTail(reasoning, REASONING_TAIL_LENGTH) : "";
|
|
2686
2968
|
const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
2687
|
-
return /* @__PURE__ */
|
|
2688
|
-
/* @__PURE__ */
|
|
2969
|
+
return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", children: [
|
|
2970
|
+
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
2689
2971
|
spinner,
|
|
2690
2972
|
" ",
|
|
2691
2973
|
activity ?? "star is thinking\u2026"
|
|
2692
2974
|
] }),
|
|
2693
|
-
tail !== "" && /* @__PURE__ */
|
|
2975
|
+
tail !== "" && /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
2694
2976
|
" ",
|
|
2695
2977
|
tail
|
|
2696
2978
|
] })
|
|
2697
2979
|
] });
|
|
2698
2980
|
}
|
|
2699
2981
|
|
|
2982
|
+
// src/cli/components/TodoPanel.tsx
|
|
2983
|
+
import { Box as Box10, Text as Text11 } from "ink";
|
|
2984
|
+
import { jsx as jsx9, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2985
|
+
var SYMBOLS = {
|
|
2986
|
+
pending: "[ ]",
|
|
2987
|
+
in_progress: "[~]",
|
|
2988
|
+
done: "[x]"
|
|
2989
|
+
};
|
|
2990
|
+
function TodoPanel({ todos }) {
|
|
2991
|
+
if (todos.length === 0) return null;
|
|
2992
|
+
return /* @__PURE__ */ jsx9(Box10, { flexDirection: "column", children: todos.map((todo) => /* @__PURE__ */ jsxs11(
|
|
2993
|
+
Text11,
|
|
2994
|
+
{
|
|
2995
|
+
dimColor: todo.status === "done",
|
|
2996
|
+
color: todo.status === "in_progress" ? "cyan" : void 0,
|
|
2997
|
+
children: [
|
|
2998
|
+
SYMBOLS[todo.status],
|
|
2999
|
+
" ",
|
|
3000
|
+
todo.title
|
|
3001
|
+
]
|
|
3002
|
+
},
|
|
3003
|
+
todo.id
|
|
3004
|
+
)) });
|
|
3005
|
+
}
|
|
3006
|
+
|
|
2700
3007
|
// src/cli/components/ToolCallCard.tsx
|
|
2701
3008
|
function formatToolCard(card) {
|
|
2702
3009
|
const header = `${card.name} ${card.argsSummary}${card.isError ? " [error]" : ""}`;
|
|
@@ -2712,6 +3019,51 @@ function isDoubleEscape(previous, now, windowMs = DOUBLE_ESC_WINDOW_MS) {
|
|
|
2712
3019
|
return previous !== null && now - previous <= windowMs;
|
|
2713
3020
|
}
|
|
2714
3021
|
|
|
3022
|
+
// src/cli/history.ts
|
|
3023
|
+
import fs10 from "fs";
|
|
3024
|
+
import path16 from "path";
|
|
3025
|
+
var DEFAULT_LIMIT = 500;
|
|
3026
|
+
var MAX_FILE_LINES = 1e3;
|
|
3027
|
+
var TRIM_TO_LINES = 500;
|
|
3028
|
+
function historyPath() {
|
|
3029
|
+
return path16.join(starHome(), "history");
|
|
3030
|
+
}
|
|
3031
|
+
function readLines(file) {
|
|
3032
|
+
const raw = fs10.readFileSync(file, "utf8");
|
|
3033
|
+
return raw.split("\n").map((line) => line.replace(/\r$/, "")).filter((line) => line.trim().length > 0);
|
|
3034
|
+
}
|
|
3035
|
+
function loadHistory(limit = DEFAULT_LIMIT) {
|
|
3036
|
+
try {
|
|
3037
|
+
return readLines(historyPath()).slice(-limit);
|
|
3038
|
+
} catch {
|
|
3039
|
+
return [];
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
function appendHistory(entry) {
|
|
3043
|
+
const line = entry.replace(/[\r\n]+/g, " ").trim();
|
|
3044
|
+
if (line.length === 0) return;
|
|
3045
|
+
try {
|
|
3046
|
+
const file = historyPath();
|
|
3047
|
+
let existing = [];
|
|
3048
|
+
try {
|
|
3049
|
+
existing = readLines(file);
|
|
3050
|
+
} catch {
|
|
3051
|
+
}
|
|
3052
|
+
if (existing[existing.length - 1] === line) return;
|
|
3053
|
+
if (existing.length >= MAX_FILE_LINES) {
|
|
3054
|
+
const kept = [...existing.slice(-(TRIM_TO_LINES - 1)), line];
|
|
3055
|
+
fs10.mkdirSync(path16.dirname(file), { recursive: true });
|
|
3056
|
+
fs10.writeFileSync(file, `${kept.join("\n")}
|
|
3057
|
+
`);
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
fs10.mkdirSync(path16.dirname(file), { recursive: true });
|
|
3061
|
+
fs10.appendFileSync(file, `${line}
|
|
3062
|
+
`);
|
|
3063
|
+
} catch {
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
|
|
2715
3067
|
// src/cli/notify.ts
|
|
2716
3068
|
var BELL = "\x07";
|
|
2717
3069
|
function shouldNotifyBell(input, isTTY) {
|
|
@@ -2827,18 +3179,30 @@ async function checkForUpdate(currentVersion, fetcher = defaultFetcher2) {
|
|
|
2827
3179
|
}
|
|
2828
3180
|
|
|
2829
3181
|
// src/cli/repl.tsx
|
|
2830
|
-
import { jsx as
|
|
3182
|
+
import { jsx as jsx10, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2831
3183
|
var CLEAR_TERMINAL = process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H";
|
|
2832
3184
|
function emptyUsage() {
|
|
2833
3185
|
return { requests: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
2834
3186
|
}
|
|
2835
3187
|
function runningTaskLabels() {
|
|
2836
|
-
|
|
3188
|
+
const shells = defaultTaskManager.list().filter((t) => t.status === "running").map((t) => t.description ?? t.command);
|
|
3189
|
+
const agents = defaultAgentTasks.list().filter((t) => t.status === "running").map((t) => t.description ?? t.id);
|
|
3190
|
+
return [...shells, ...agents];
|
|
2837
3191
|
}
|
|
2838
3192
|
var SESSION_MODE_CYCLE = ["ask", "auto", "readonly", "plan"];
|
|
2839
3193
|
function formatUsage(usage) {
|
|
2840
3194
|
return `API usage this session: ${usage.requests} requests, ${usage.promptTokens} prompt + ${usage.completionTokens} completion = ${usage.totalTokens} tokens`;
|
|
2841
3195
|
}
|
|
3196
|
+
var SESSION_PICKER_CAP = 20;
|
|
3197
|
+
async function sessionPreview(id) {
|
|
3198
|
+
const store = await SessionStore.open(id);
|
|
3199
|
+
if (!store) return null;
|
|
3200
|
+
const first = (await store.messages()).find((m) => m.role === "user");
|
|
3201
|
+
if (!first) return null;
|
|
3202
|
+
const text = coreMessageText(first).replace(/\s+/g, " ").trim();
|
|
3203
|
+
if (!text) return null;
|
|
3204
|
+
return text.length > 60 ? `${text.slice(0, 60)}\u2026` : text;
|
|
3205
|
+
}
|
|
2842
3206
|
function Repl({
|
|
2843
3207
|
backend,
|
|
2844
3208
|
model,
|
|
@@ -2850,55 +3214,59 @@ function Repl({
|
|
|
2850
3214
|
initialUsage
|
|
2851
3215
|
}) {
|
|
2852
3216
|
const { exit } = useApp();
|
|
2853
|
-
const [initialDisplay] =
|
|
2854
|
-
const [messages, setMessages] =
|
|
2855
|
-
const [epoch, setEpoch] =
|
|
2856
|
-
const [streamingText, setStreamingText] =
|
|
2857
|
-
const [isStreaming, setIsStreaming] =
|
|
2858
|
-
const [thinking, setThinking] =
|
|
2859
|
-
const [thinkingText, setThinkingText] =
|
|
2860
|
-
const [thoughtSummary, setThoughtSummary] =
|
|
2861
|
-
const [, setUsageVersion] =
|
|
2862
|
-
const [modelName, setModelName] =
|
|
2863
|
-
const [pending, setPending] =
|
|
2864
|
-
const [spinnerTick, setSpinnerTick] =
|
|
2865
|
-
const [activity, setActivity] =
|
|
2866
|
-
const [bgLabels, setBgLabels] =
|
|
2867
|
-
const [
|
|
2868
|
-
const [
|
|
2869
|
-
const [
|
|
2870
|
-
const
|
|
2871
|
-
const
|
|
2872
|
-
const
|
|
2873
|
-
const
|
|
2874
|
-
const
|
|
2875
|
-
const
|
|
2876
|
-
const
|
|
2877
|
-
const
|
|
2878
|
-
const
|
|
2879
|
-
const
|
|
2880
|
-
const
|
|
2881
|
-
const
|
|
2882
|
-
const
|
|
2883
|
-
const
|
|
2884
|
-
const
|
|
2885
|
-
const
|
|
2886
|
-
const
|
|
2887
|
-
const
|
|
2888
|
-
const
|
|
2889
|
-
const
|
|
2890
|
-
const
|
|
2891
|
-
const
|
|
2892
|
-
const
|
|
2893
|
-
const
|
|
2894
|
-
const [
|
|
2895
|
-
const
|
|
2896
|
-
const
|
|
2897
|
-
const [
|
|
2898
|
-
const
|
|
2899
|
-
const
|
|
2900
|
-
const
|
|
2901
|
-
const
|
|
3217
|
+
const [initialDisplay] = useState3(() => buildDisplayMessages(initialMessages ?? []));
|
|
3218
|
+
const [messages, setMessages] = useState3(initialDisplay);
|
|
3219
|
+
const [epoch, setEpoch] = useState3(0);
|
|
3220
|
+
const [streamingText, setStreamingText] = useState3(null);
|
|
3221
|
+
const [isStreaming, setIsStreaming] = useState3(false);
|
|
3222
|
+
const [thinking, setThinking] = useState3(false);
|
|
3223
|
+
const [thinkingText, setThinkingText] = useState3("");
|
|
3224
|
+
const [thoughtSummary, setThoughtSummary] = useState3(null);
|
|
3225
|
+
const [, setUsageVersion] = useState3(0);
|
|
3226
|
+
const [modelName, setModelName] = useState3(model);
|
|
3227
|
+
const [pending, setPending] = useState3(null);
|
|
3228
|
+
const [spinnerTick, setSpinnerTick] = useState3(0);
|
|
3229
|
+
const [activity, setActivity] = useState3(null);
|
|
3230
|
+
const [bgLabels, setBgLabels] = useState3(() => runningTaskLabels());
|
|
3231
|
+
const [todos, setTodos] = useState3([]);
|
|
3232
|
+
const [permissionModeState, setPermissionModeState] = useState3(permissionMode);
|
|
3233
|
+
const [planApproval, setPlanApprovalState] = useState3(false);
|
|
3234
|
+
const [pendingRewind, setPendingRewindState] = useState3(null);
|
|
3235
|
+
const [picker, setPickerState] = useState3(null);
|
|
3236
|
+
const backendRef = useRef3(backend);
|
|
3237
|
+
const nextIdRef = useRef3(initialDisplay.length);
|
|
3238
|
+
const abortRef = useRef3(null);
|
|
3239
|
+
const streamedRef = useRef3("");
|
|
3240
|
+
const thinkingRef = useRef3(false);
|
|
3241
|
+
const reasoningRef = useRef3("");
|
|
3242
|
+
const tickerStopRef = useRef3(null);
|
|
3243
|
+
const flushedRef = useRef3({ streamed: "", reasoning: "" });
|
|
3244
|
+
const toolCardsRef = useRef3(/* @__PURE__ */ new Map());
|
|
3245
|
+
const pendingRef = useRef3(null);
|
|
3246
|
+
const alwaysAllowedRef = useRef3(/* @__PURE__ */ new Set());
|
|
3247
|
+
const prevModeRef = useRef3(null);
|
|
3248
|
+
const planApprovalRef = useRef3(false);
|
|
3249
|
+
const pendingRewindRef = useRef3(null);
|
|
3250
|
+
const pickerRef = useRef3(null);
|
|
3251
|
+
const modelNameRef = useRef3(model);
|
|
3252
|
+
const sessionStoreRef = useRef3(sessionStore);
|
|
3253
|
+
const usageRef = useRef3(initialUsage ? { ...initialUsage } : emptyUsage());
|
|
3254
|
+
const turnChunksRef = useRef3(0);
|
|
3255
|
+
const messagesRef = useRef3(initialDisplay);
|
|
3256
|
+
const [queueItems, setQueueItems] = useState3([]);
|
|
3257
|
+
const queueRef = useRef3(new PromptQueue());
|
|
3258
|
+
const [pendingImages, setPendingImages] = useState3([]);
|
|
3259
|
+
const pendingImagesRef = useRef3([]);
|
|
3260
|
+
const imageSeqRef = useRef3(1);
|
|
3261
|
+
const [inputRefill, setInputRefill] = useState3();
|
|
3262
|
+
const refillSeqRef = useRef3(0);
|
|
3263
|
+
const lastEscRef = useRef3(null);
|
|
3264
|
+
const [gitBranch, setGitBranch] = useState3(null);
|
|
3265
|
+
const [contextPercent, setContextPercent] = useState3(null);
|
|
3266
|
+
const [initialHistory] = useState3(() => loadHistory());
|
|
3267
|
+
const budgetWarnedRef = useRef3(false);
|
|
3268
|
+
const budgetExceededRef = useRef3(false);
|
|
3269
|
+
const runStreamRef = useRef3(
|
|
2902
3270
|
null
|
|
2903
3271
|
);
|
|
2904
3272
|
const pushMessage = useCallback(
|
|
@@ -2945,6 +3313,34 @@ function Repl({
|
|
|
2945
3313
|
pendingRewindRef.current = p;
|
|
2946
3314
|
setPendingRewindState(p);
|
|
2947
3315
|
}, []);
|
|
3316
|
+
const setPicker = useCallback((p) => {
|
|
3317
|
+
pickerRef.current = p;
|
|
3318
|
+
setPickerState(p);
|
|
3319
|
+
}, []);
|
|
3320
|
+
const showPicker = useCallback(
|
|
3321
|
+
(title, options) => {
|
|
3322
|
+
if (pickerRef.current) return Promise.resolve(null);
|
|
3323
|
+
return new Promise((resolve) => {
|
|
3324
|
+
setPicker({ title, options, resolve });
|
|
3325
|
+
});
|
|
3326
|
+
},
|
|
3327
|
+
[setPicker]
|
|
3328
|
+
);
|
|
3329
|
+
const handlePickerSelect = useCallback(
|
|
3330
|
+
(value) => {
|
|
3331
|
+
const p = pickerRef.current;
|
|
3332
|
+
if (!p) return;
|
|
3333
|
+
setPicker(null);
|
|
3334
|
+
p.resolve(value);
|
|
3335
|
+
},
|
|
3336
|
+
[setPicker]
|
|
3337
|
+
);
|
|
3338
|
+
const handlePickerCancel = useCallback(() => {
|
|
3339
|
+
const p = pickerRef.current;
|
|
3340
|
+
if (!p) return;
|
|
3341
|
+
setPicker(null);
|
|
3342
|
+
p.resolve(null);
|
|
3343
|
+
}, [setPicker]);
|
|
2948
3344
|
const stageClipboardImage = useCallback((img) => {
|
|
2949
3345
|
pendingImagesRef.current = [
|
|
2950
3346
|
...pendingImagesRef.current,
|
|
@@ -3019,6 +3415,10 @@ function Repl({
|
|
|
3019
3415
|
if (message) pushMessage("system", message);
|
|
3020
3416
|
});
|
|
3021
3417
|
}, [pushMessage]);
|
|
3418
|
+
useEffect2(() => {
|
|
3419
|
+
loadTodos(cwd).then(setTodos).catch(() => {
|
|
3420
|
+
});
|
|
3421
|
+
}, [cwd]);
|
|
3022
3422
|
useEffect2(() => {
|
|
3023
3423
|
const onUpdate = (task) => {
|
|
3024
3424
|
setBgLabels(runningTaskLabels());
|
|
@@ -3034,10 +3434,28 @@ function Repl({
|
|
|
3034
3434
|
});
|
|
3035
3435
|
}
|
|
3036
3436
|
};
|
|
3437
|
+
const onAgentUpdate = (task) => {
|
|
3438
|
+
setBgLabels(runningTaskLabels());
|
|
3439
|
+
const label = task.description ? `: ${task.description}` : "";
|
|
3440
|
+
pushMessage(
|
|
3441
|
+
"system",
|
|
3442
|
+
task.status === "running" ? `Background subagent ${task.id} started${label}` : `Background subagent ${task.id} ${task.status}${label}`
|
|
3443
|
+
);
|
|
3444
|
+
if (task.status !== "running") {
|
|
3445
|
+
notifyBell({
|
|
3446
|
+
enabled: config.notifyBell,
|
|
3447
|
+
thresholdSec: config.notifyBellThresholdSec,
|
|
3448
|
+
noNotifyEnv: process.env.STAR_NO_NOTIFY === "1"
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
};
|
|
3037
3452
|
defaultTaskManager.on("update", onUpdate);
|
|
3453
|
+
defaultAgentTasks.on("update", onAgentUpdate);
|
|
3038
3454
|
return () => {
|
|
3039
3455
|
defaultTaskManager.off("update", onUpdate);
|
|
3456
|
+
defaultAgentTasks.off("update", onAgentUpdate);
|
|
3040
3457
|
defaultTaskManager.cleanup();
|
|
3458
|
+
defaultAgentTasks.cleanup();
|
|
3041
3459
|
};
|
|
3042
3460
|
}, [pushMessage, config]);
|
|
3043
3461
|
const interrupt = useCallback(() => {
|
|
@@ -3077,17 +3495,16 @@ function Repl({
|
|
|
3077
3495
|
}, [pushMessage, redrawMessages]);
|
|
3078
3496
|
const handleExit = useCallback(() => {
|
|
3079
3497
|
const killed = defaultTaskManager.cleanup();
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
);
|
|
3498
|
+
const killedAgents = defaultAgentTasks.cleanup();
|
|
3499
|
+
const stopped = [...killed.map((t) => t.id), ...killedAgents.map((t) => t.id)];
|
|
3500
|
+
if (stopped.length > 0) {
|
|
3501
|
+
pushMessage("system", `Stopped ${stopped.length} background task(s): ${stopped.join(", ")}`);
|
|
3085
3502
|
}
|
|
3086
3503
|
exit();
|
|
3087
3504
|
}, [exit, pushMessage]);
|
|
3088
|
-
|
|
3505
|
+
useInput6((_input, key) => {
|
|
3089
3506
|
if (key.escape) {
|
|
3090
|
-
const busy = abortRef.current !== null || pendingRef.current !== null || planApprovalRef.current || pendingRewindRef.current !== null;
|
|
3507
|
+
const busy = abortRef.current !== null || pendingRef.current !== null || planApprovalRef.current || pendingRewindRef.current !== null || pickerRef.current !== null;
|
|
3091
3508
|
if (busy) {
|
|
3092
3509
|
lastEscRef.current = null;
|
|
3093
3510
|
interrupt();
|
|
@@ -3104,7 +3521,7 @@ function Repl({
|
|
|
3104
3521
|
}
|
|
3105
3522
|
lastEscRef.current = null;
|
|
3106
3523
|
if (key.shift && key.tab) {
|
|
3107
|
-
if (abortRef.current || pendingRef.current || planApprovalRef.current || pendingRewindRef.current)
|
|
3524
|
+
if (abortRef.current || pendingRef.current || planApprovalRef.current || pendingRewindRef.current || pickerRef.current)
|
|
3108
3525
|
return;
|
|
3109
3526
|
const current = config.permissionMode;
|
|
3110
3527
|
const index = SESSION_MODE_CYCLE.indexOf(current);
|
|
@@ -3266,6 +3683,10 @@ function Repl({
|
|
|
3266
3683
|
name: event.name,
|
|
3267
3684
|
argsSummary: summarizeArgs(event.args)
|
|
3268
3685
|
});
|
|
3686
|
+
if (event.name === "todo_write") {
|
|
3687
|
+
const list = parseTodoArgs(event.args);
|
|
3688
|
+
if (list) setTodos(list);
|
|
3689
|
+
}
|
|
3269
3690
|
setActivity(`running ${event.name}: ${summarizeArgs(event.args, 60)}`);
|
|
3270
3691
|
} else if (event.type === "tool-result") {
|
|
3271
3692
|
const card = toolCardsRef.current.get(event.id) ?? {
|
|
@@ -3298,6 +3719,13 @@ function Repl({
|
|
|
3298
3719
|
sessionStoreRef.current?.addUsage(event.usage).catch(() => {
|
|
3299
3720
|
});
|
|
3300
3721
|
}
|
|
3722
|
+
} else if (event.type === "retry") {
|
|
3723
|
+
pushMessage(
|
|
3724
|
+
"system",
|
|
3725
|
+
`Request failed, retrying (${event.attempt}/${event.maxAttempts}): ${event.reason}`
|
|
3726
|
+
);
|
|
3727
|
+
} else if (event.type === "notice") {
|
|
3728
|
+
pushMessage("system", event.message);
|
|
3301
3729
|
} else if (event.type === "error") {
|
|
3302
3730
|
pushMessage("system", `Error: ${formatStreamError(event.error)}`);
|
|
3303
3731
|
}
|
|
@@ -3461,6 +3889,97 @@ ${lines.join("\n")}`;
|
|
|
3461
3889
|
return formatSessionEntries(entries, { showCwd: all });
|
|
3462
3890
|
},
|
|
3463
3891
|
resumeSession: resume,
|
|
3892
|
+
pickModel: async () => {
|
|
3893
|
+
const models = listModels(config);
|
|
3894
|
+
if (models.length === 0) return "No models configured.";
|
|
3895
|
+
const options = models.map((m) => {
|
|
3896
|
+
const parts = [`${m.provider}/${m.model}`, `${contextWindowTokens(config, m.name)} ctx`];
|
|
3897
|
+
if (m.promptPrice !== void 0 && m.completionPrice !== void 0) {
|
|
3898
|
+
parts.push(`$${m.promptPrice}/$${m.completionPrice} per 1M tokens`);
|
|
3899
|
+
}
|
|
3900
|
+
return {
|
|
3901
|
+
value: m.name,
|
|
3902
|
+
label: m.name,
|
|
3903
|
+
description: parts.join(" \xB7 "),
|
|
3904
|
+
hint: m.name === modelNameRef.current ? "current" : void 0
|
|
3905
|
+
};
|
|
3906
|
+
});
|
|
3907
|
+
const picked = await showPicker("Select a model", options);
|
|
3908
|
+
if (!picked || picked === modelNameRef.current) {
|
|
3909
|
+
return `Model unchanged \u2014 still "${modelNameRef.current}".`;
|
|
3910
|
+
}
|
|
3911
|
+
return switchModel(picked);
|
|
3912
|
+
},
|
|
3913
|
+
pickPermissionMode: async () => {
|
|
3914
|
+
const current = config.permissionMode;
|
|
3915
|
+
const descriptions = {
|
|
3916
|
+
ask: "reads allowed; writes/exec ask for confirmation",
|
|
3917
|
+
auto: "everything allowed except hard-denied dangerous commands/paths",
|
|
3918
|
+
readonly: "read-only; all writes/exec denied",
|
|
3919
|
+
yolo: "allow everything, never ask (disables ALL safety checks)",
|
|
3920
|
+
plan: "read-only research, then approve a plan before executing (this session only)"
|
|
3921
|
+
};
|
|
3922
|
+
const options = ["ask", "auto", "readonly", "yolo", "plan"].map((m) => ({
|
|
3923
|
+
value: m,
|
|
3924
|
+
label: m,
|
|
3925
|
+
description: descriptions[m],
|
|
3926
|
+
hint: m === current ? "current" : void 0
|
|
3927
|
+
}));
|
|
3928
|
+
const picked = await showPicker("Select a permission mode", options);
|
|
3929
|
+
if (!picked || picked === current) {
|
|
3930
|
+
return `Permission mode unchanged \u2014 still ${current}.`;
|
|
3931
|
+
}
|
|
3932
|
+
if (picked === "plan") return ctx.planMode();
|
|
3933
|
+
return ctx.permissionMode(picked);
|
|
3934
|
+
},
|
|
3935
|
+
pickSession: async (all) => {
|
|
3936
|
+
const entries = (await listSessionEntries(all ? void 0 : cwd)).slice(
|
|
3937
|
+
0,
|
|
3938
|
+
SESSION_PICKER_CAP
|
|
3939
|
+
);
|
|
3940
|
+
if (entries.length === 0) {
|
|
3941
|
+
return all ? "No sessions found." : "No sessions found for this directory.";
|
|
3942
|
+
}
|
|
3943
|
+
const options = await Promise.all(
|
|
3944
|
+
entries.map(async ({ meta, messageCount }) => {
|
|
3945
|
+
const parts = [`${messageCount} messages \xB7 ${relativeTime(meta.updatedAt)}`];
|
|
3946
|
+
const preview = await sessionPreview(meta.id);
|
|
3947
|
+
if (preview) parts.push(preview);
|
|
3948
|
+
if (all) parts.push(`[${meta.cwd}]`);
|
|
3949
|
+
return {
|
|
3950
|
+
value: meta.id,
|
|
3951
|
+
label: meta.title || shortSessionId(meta.id),
|
|
3952
|
+
description: parts.join(" \xB7 "),
|
|
3953
|
+
hint: meta.id === sessionStoreRef.current?.id ? "current" : void 0
|
|
3954
|
+
};
|
|
3955
|
+
})
|
|
3956
|
+
);
|
|
3957
|
+
const picked = await showPicker(
|
|
3958
|
+
all ? "Resume a session (all directories)" : "Resume a session",
|
|
3959
|
+
options
|
|
3960
|
+
);
|
|
3961
|
+
if (!picked) return "Session unchanged \u2014 picker cancelled.";
|
|
3962
|
+
return resume(picked);
|
|
3963
|
+
},
|
|
3964
|
+
forkSession: async () => {
|
|
3965
|
+
const current = backendRef.current;
|
|
3966
|
+
const store = sessionStoreRef.current;
|
|
3967
|
+
if (!(current instanceof AgentLoop) || !store) {
|
|
3968
|
+
return "Nothing to fork \u2014 no active session.";
|
|
3969
|
+
}
|
|
3970
|
+
const messages2 = await store.messages();
|
|
3971
|
+
if (messages2.length === 0) {
|
|
3972
|
+
return "Nothing to fork \u2014 the current session has no messages yet.";
|
|
3973
|
+
}
|
|
3974
|
+
const fork = await SessionStore.create(cwd, modelNameRef.current);
|
|
3975
|
+
await fork.replaceMessages(messages2);
|
|
3976
|
+
const oldMeta = await store.meta();
|
|
3977
|
+
const title = `Fork of ${oldMeta.title || shortSessionId(oldMeta.id)}`;
|
|
3978
|
+
await fork.setTitle(title);
|
|
3979
|
+
current.setSessionStore(fork);
|
|
3980
|
+
sessionStoreRef.current = fork;
|
|
3981
|
+
return `Forked into new session ${fork.id} ("${title}") and switched to it. Checkpoints and rewind history do not carry over.`;
|
|
3982
|
+
},
|
|
3464
3983
|
newSession: async () => {
|
|
3465
3984
|
const current = backendRef.current;
|
|
3466
3985
|
if (!(current instanceof AgentLoop)) {
|
|
@@ -3645,6 +4164,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
3645
4164
|
`maxSteps: ${config.maxSteps}`,
|
|
3646
4165
|
`contextMaxTokens: ${config.contextMaxTokens}`,
|
|
3647
4166
|
`streamIdleTimeoutSec: ${config.streamIdleTimeoutSec}`,
|
|
4167
|
+
`streamFirstChunkTimeoutSec: ${config.streamFirstChunkTimeoutSec}`,
|
|
4168
|
+
`streamMaxRetries: ${config.streamMaxRetries}`,
|
|
4169
|
+
`maxAutoContinues: ${config.maxAutoContinues}`,
|
|
3648
4170
|
`permissions.allow (${config.permissions.allow.length}): ${config.permissions.allow.join(", ") || "(none)"}`,
|
|
3649
4171
|
`hooks (${config.hooks.length}): ${config.hooks.map((h) => h.event).join(", ") || "(none)"}`
|
|
3650
4172
|
].join("\n")
|
|
@@ -3664,7 +4186,8 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
3664
4186
|
applyMessages,
|
|
3665
4187
|
redrawMessages,
|
|
3666
4188
|
applySessionMode,
|
|
3667
|
-
setPendingRewind
|
|
4189
|
+
setPendingRewind,
|
|
4190
|
+
showPicker
|
|
3668
4191
|
]);
|
|
3669
4192
|
const runShellBang = useCallback(
|
|
3670
4193
|
async (raw) => {
|
|
@@ -3704,6 +4227,7 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
3704
4227
|
);
|
|
3705
4228
|
const handleSubmit = useCallback(
|
|
3706
4229
|
(text) => {
|
|
4230
|
+
appendHistory(text);
|
|
3707
4231
|
if (text.startsWith("!")) {
|
|
3708
4232
|
if (abortRef.current) {
|
|
3709
4233
|
queueRef.current.enqueue({ text, images: [] });
|
|
@@ -3718,7 +4242,11 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
3718
4242
|
if (!parsed) return;
|
|
3719
4243
|
const command = registry.get(parsed.name);
|
|
3720
4244
|
if (!command) {
|
|
3721
|
-
|
|
4245
|
+
const suggestions = registry.complete(parsed.name).map((cmd) => cmd.name);
|
|
4246
|
+
pushMessage(
|
|
4247
|
+
"system",
|
|
4248
|
+
`Unknown command: /${parsed.name} (try /help)${didYouMeanSuffix(suggestions)}`
|
|
4249
|
+
);
|
|
3722
4250
|
return;
|
|
3723
4251
|
}
|
|
3724
4252
|
void command.run(parsed.args, registry.ctx);
|
|
@@ -3744,7 +4272,11 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
3744
4272
|
[registry, pushMessage, runStream, runShellBang, config, clearPendingImages, sessionCostUsd]
|
|
3745
4273
|
);
|
|
3746
4274
|
const commandHints = useMemo(
|
|
3747
|
-
() => registry.list().map((cmd) => ({
|
|
4275
|
+
() => registry.list().map((cmd) => ({
|
|
4276
|
+
name: cmd.name,
|
|
4277
|
+
description: cmd.description,
|
|
4278
|
+
usage: cmd.usage ?? `/${cmd.name}`
|
|
4279
|
+
})),
|
|
3748
4280
|
[registry]
|
|
3749
4281
|
);
|
|
3750
4282
|
useEffect2(() => {
|
|
@@ -3759,9 +4291,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
3759
4291
|
}
|
|
3760
4292
|
}, [cwd, config, isStreaming, epoch]);
|
|
3761
4293
|
const sessionCost = sessionCostUsd();
|
|
3762
|
-
return /* @__PURE__ */
|
|
3763
|
-
/* @__PURE__ */
|
|
3764
|
-
(thinking || activity !== null) && /* @__PURE__ */
|
|
4294
|
+
return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", children: [
|
|
4295
|
+
/* @__PURE__ */ jsx10(MessageList, { messages }, `messages-${epoch}`),
|
|
4296
|
+
(thinking || activity !== null) && /* @__PURE__ */ jsx10(
|
|
3765
4297
|
ThinkingIndicator,
|
|
3766
4298
|
{
|
|
3767
4299
|
reasoning: thinkingText,
|
|
@@ -3769,9 +4301,9 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
3769
4301
|
activity: activity ?? void 0
|
|
3770
4302
|
}
|
|
3771
4303
|
),
|
|
3772
|
-
!thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */
|
|
3773
|
-
streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */
|
|
3774
|
-
pending && /* @__PURE__ */
|
|
4304
|
+
!thinking && activity === null && thoughtSummary !== null && /* @__PURE__ */ jsx10(Text12, { dimColor: true, children: thoughtSummary }),
|
|
4305
|
+
streamingText !== null && (streamingText !== "" || turnChunksRef.current === 0) && /* @__PURE__ */ jsx10(StreamingMessage, { text: streamingText, continuation: turnChunksRef.current > 0 }),
|
|
4306
|
+
pending && /* @__PURE__ */ jsx10(
|
|
3775
4307
|
PermissionPrompt,
|
|
3776
4308
|
{
|
|
3777
4309
|
request: pending.request,
|
|
@@ -3779,32 +4311,43 @@ ${plan.affected.length} file change(s) will be reverted, ${messageCount} message
|
|
|
3779
4311
|
onDecision: handleDecision
|
|
3780
4312
|
}
|
|
3781
4313
|
),
|
|
3782
|
-
planApproval && /* @__PURE__ */
|
|
3783
|
-
pendingRewind && /* @__PURE__ */
|
|
3784
|
-
|
|
4314
|
+
planApproval && /* @__PURE__ */ jsx10(ExitPlanPrompt, { onDecision: handlePlanDecision }),
|
|
4315
|
+
pendingRewind && /* @__PURE__ */ jsx10(RewindConfirmPrompt, { summary: pendingRewind.summary, onDecision: handleRewindDecision }),
|
|
4316
|
+
picker && /* @__PURE__ */ jsx10(
|
|
4317
|
+
SelectPrompt,
|
|
4318
|
+
{
|
|
4319
|
+
title: picker.title,
|
|
4320
|
+
options: picker.options,
|
|
4321
|
+
onSelect: handlePickerSelect,
|
|
4322
|
+
onCancel: handlePickerCancel
|
|
4323
|
+
}
|
|
4324
|
+
),
|
|
4325
|
+
queueItems.map((item) => /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
|
|
3785
4326
|
"queued: ",
|
|
3786
4327
|
item.text.length > 80 ? `${item.text.slice(0, 80)}\u2026` : item.text
|
|
3787
4328
|
] }, item.id)),
|
|
3788
|
-
pendingImages.map((staged) => /* @__PURE__ */
|
|
4329
|
+
pendingImages.map((staged) => /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
|
|
3789
4330
|
"[image attached: ",
|
|
3790
4331
|
staged.image.path,
|
|
3791
4332
|
"]"
|
|
3792
4333
|
] }, staged.id)),
|
|
3793
|
-
/* @__PURE__ */
|
|
4334
|
+
/* @__PURE__ */ jsx10(TodoPanel, { todos }),
|
|
4335
|
+
/* @__PURE__ */ jsx10(
|
|
3794
4336
|
InputBox,
|
|
3795
4337
|
{
|
|
3796
4338
|
isStreaming,
|
|
3797
|
-
disabled: pending !== null || planApproval || pendingRewind !== null,
|
|
4339
|
+
disabled: pending !== null || planApproval || pendingRewind !== null || picker !== null,
|
|
3798
4340
|
commands: commandHints,
|
|
3799
4341
|
cwd,
|
|
3800
4342
|
refill: inputRefill,
|
|
4343
|
+
initialHistory,
|
|
3801
4344
|
onSubmit: handleSubmit,
|
|
3802
4345
|
onInterrupt: interrupt,
|
|
3803
4346
|
onExit: handleExit,
|
|
3804
4347
|
onPasteImage: handlePasteImage
|
|
3805
4348
|
}
|
|
3806
4349
|
),
|
|
3807
|
-
/* @__PURE__ */
|
|
4350
|
+
/* @__PURE__ */ jsx10(
|
|
3808
4351
|
StatusBar,
|
|
3809
4352
|
{
|
|
3810
4353
|
cwd,
|
|
@@ -3824,7 +4367,7 @@ function renderRepl(backend, opts) {
|
|
|
3824
4367
|
defaultTaskManager.cleanup();
|
|
3825
4368
|
});
|
|
3826
4369
|
return render(
|
|
3827
|
-
/* @__PURE__ */
|
|
4370
|
+
/* @__PURE__ */ jsx10(
|
|
3828
4371
|
Repl,
|
|
3829
4372
|
{
|
|
3830
4373
|
backend,
|
|
@@ -3841,7 +4384,7 @@ function renderRepl(backend, opts) {
|
|
|
3841
4384
|
}
|
|
3842
4385
|
|
|
3843
4386
|
// src/config/loader.ts
|
|
3844
|
-
import
|
|
4387
|
+
import fs11 from "fs";
|
|
3845
4388
|
import { parse as parse2 } from "smol-toml";
|
|
3846
4389
|
var PartialConfigSchema = ConfigSchema.partial();
|
|
3847
4390
|
function parseTomlFile(content, filePath) {
|
|
@@ -3869,8 +4412,8 @@ function applyOverrides(config, overrides) {
|
|
|
3869
4412
|
return merged;
|
|
3870
4413
|
}
|
|
3871
4414
|
function readTomlFileSync(filePath) {
|
|
3872
|
-
if (!
|
|
3873
|
-
const content =
|
|
4415
|
+
if (!fs11.existsSync(filePath)) return {};
|
|
4416
|
+
const content = fs11.readFileSync(filePath, "utf8");
|
|
3874
4417
|
return parseTomlFile(content, filePath);
|
|
3875
4418
|
}
|
|
3876
4419
|
function loadConfigSync(cwd, overrides) {
|
|
@@ -3884,7 +4427,13 @@ function loadConfigSync(cwd, overrides) {
|
|
|
3884
4427
|
var SYSTEM_PROMPT = `You are Star CLI, an AI coding agent running in the user's terminal.
|
|
3885
4428
|
You help with software engineering tasks: reading, writing and editing code, running shell commands, and managing todos.
|
|
3886
4429
|
Be concise and direct. Use tools when they help accomplish the task.
|
|
3887
|
-
|
|
4430
|
+
For any task with two or more steps, start by creating a todo list with todo_write and keep it updated as you progress; only mark an item completed after you have verified its result.
|
|
4431
|
+
The working directory is the user's project root; never touch files outside it without explicit instruction.
|
|
4432
|
+
For conversion, batch-processing, or other scriptable tasks, write a script file into the working directory and run it instead of pasting long inline code into the shell; once the script's output is verified, delete the script unless the user asked to keep it.
|
|
4433
|
+
When a tool call or command fails, read the error output, work out the cause, and try again with a corrected or alternative approach \u2014 never repeat an identical failing call without changing something.
|
|
4434
|
+
Never consider the task finished until you have verified the result yourself: run the tests, the build, or a check command that proves the output is correct, and if verification fails or reveals gaps, keep fixing until it passes.
|
|
4435
|
+
Do not end your turn while the task is still incomplete; keep going until it is done or you are genuinely blocked, and if you are blocked, state exactly what is missing.
|
|
4436
|
+
Never end a reply by announcing what you will do next \u2014 either do it now with tool calls, or do not mention it. Narrating future actions is not progress.`;
|
|
3888
4437
|
async function createLoop(config, modelName, cwd, sessionStore) {
|
|
3889
4438
|
const model = createModel(config, modelName);
|
|
3890
4439
|
const registry = createDefaultRegistry();
|
|
@@ -3968,6 +4517,19 @@ async function printMode(loop, prompt, cwd, json, imagePaths) {
|
|
|
3968
4517
|
case "finish":
|
|
3969
4518
|
usage.add(event.usage);
|
|
3970
4519
|
break;
|
|
4520
|
+
case "retry":
|
|
4521
|
+
if (!json)
|
|
4522
|
+
process.stderr.write(
|
|
4523
|
+
`
|
|
4524
|
+
[retry ${event.attempt}/${event.maxAttempts}] ${event.reason}
|
|
4525
|
+
`
|
|
4526
|
+
);
|
|
4527
|
+
break;
|
|
4528
|
+
case "notice":
|
|
4529
|
+
if (!json) process.stderr.write(`
|
|
4530
|
+
[notice] ${event.message}
|
|
4531
|
+
`);
|
|
4532
|
+
break;
|
|
3971
4533
|
case "error":
|
|
3972
4534
|
if (!json) process.stderr.write(`
|
|
3973
4535
|
[error] ${formatStreamError(event.error)}
|
|
@@ -4001,6 +4563,7 @@ async function printMode(loop, prompt, cwd, json, imagePaths) {
|
|
|
4001
4563
|
}
|
|
4002
4564
|
if (!json) process.stdout.write("\n");
|
|
4003
4565
|
defaultTaskManager.cleanup();
|
|
4566
|
+
defaultAgentTasks.cleanup();
|
|
4004
4567
|
return exitCode;
|
|
4005
4568
|
}
|
|
4006
4569
|
var program = new Command();
|