@vigent/cli 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -48
- package/dist/agents.js +29 -0
- package/dist/app.js +521 -20
- package/dist/attach.js +102 -0
- package/dist/choices.js +4 -1
- package/dist/cli-engines.js +15 -5
- package/dist/cli.js +25 -15
- package/dist/config.js +26 -2
- package/dist/diffview.js +1 -1
- package/dist/engines.js +18 -11
- package/dist/float.js +1 -1
- package/dist/hunk-review.js +218 -0
- package/dist/hunks.js +60 -0
- package/dist/icons.js +1 -0
- package/dist/keys.js +3 -1
- package/dist/main.js +16 -0
- package/dist/markdown.js +1 -1
- package/dist/notify.js +33 -0
- package/dist/pi-sdk.js +45 -0
- package/dist/pi-ui.js +78 -0
- package/dist/picker.js +21 -11
- package/dist/serve-page.js +247 -0
- package/dist/serve.js +267 -0
- package/dist/session.js +122 -11
- package/dist/settings.js +1 -1
- package/dist/templates.js +125 -0
- package/dist/theme.js +2 -0
- package/dist/tui.js +13 -0
- package/package.json +5 -5
package/dist/attach.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@path` in a prompt attaches a file from the agent's folder.
|
|
3
|
+
*
|
|
4
|
+
* Each engine gets it its own way: Claude Code reads `@path` in a prompt
|
|
5
|
+
* itself; OpenCode takes the files with `-f`; for Codex and pi, vigent adds
|
|
6
|
+
* the file's contents under the prompt. The transcript shows what you typed.
|
|
7
|
+
*/
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
/** files larger than this aren't inlined (Codex, pi) */
|
|
12
|
+
export const MAX_INLINE_BYTES = 100 * 1024;
|
|
13
|
+
/** The files an agent can be pointed at: tracked and new ones, without what .gitignore hides. */
|
|
14
|
+
export function listFiles(cwd) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
execFile("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], { cwd, maxBuffer: 64 * 1024 * 1024 }, (err, stdout) => {
|
|
17
|
+
if (err)
|
|
18
|
+
return resolve(walk(cwd));
|
|
19
|
+
const files = [...new Set(String(stdout).split("\0").filter(Boolean))].filter((f) => fs.existsSync(path.join(cwd, f)));
|
|
20
|
+
resolve(files.sort(byDepthThenName));
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Not a git repository: a shallow walk, skipping the usual heavy folders. */
|
|
25
|
+
function walk(cwd, max = 5000) {
|
|
26
|
+
const out = [];
|
|
27
|
+
const skip = new Set(["node_modules", ".git", "dist", "build", "target", ".next", ".venv", "venv"]);
|
|
28
|
+
const visit = (dir, depth) => {
|
|
29
|
+
if (out.length >= max || depth > 6)
|
|
30
|
+
return;
|
|
31
|
+
let entries = [];
|
|
32
|
+
try {
|
|
33
|
+
entries = fs.readdirSync(path.join(cwd, dir), { withFileTypes: true });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
for (const e of entries) {
|
|
39
|
+
const rel = dir ? `${dir}/${e.name}` : e.name;
|
|
40
|
+
if (e.isDirectory()) {
|
|
41
|
+
if (!skip.has(e.name) && !e.name.startsWith("."))
|
|
42
|
+
visit(rel, depth + 1);
|
|
43
|
+
}
|
|
44
|
+
else if (e.isFile())
|
|
45
|
+
out.push(rel);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
visit("", 0);
|
|
49
|
+
return out.sort(byDepthThenName);
|
|
50
|
+
}
|
|
51
|
+
function byDepthThenName(a, b) {
|
|
52
|
+
return a.split("/").length - b.split("/").length || a.localeCompare(b);
|
|
53
|
+
}
|
|
54
|
+
/** a path that can follow `@`: letters, digits, `_ . / @ + -`, not ending in `.` or `@` (so "see @a.js." works) */
|
|
55
|
+
const PATH = String.raw `[\p{L}\p{N}_./@+-]*[\p{L}\p{N}_/+-]`;
|
|
56
|
+
/** `@path` at the start of a word; group 1 is the path */
|
|
57
|
+
export const MENTION = new RegExp(String.raw `(?:^|(?<=\s))@(${PATH})`, "gu");
|
|
58
|
+
/** Whether `@file` would read back as this file (names with spaces can't). */
|
|
59
|
+
export function mentionable(file) {
|
|
60
|
+
return new RegExp(`^${PATH}$`, "u").test(file);
|
|
61
|
+
}
|
|
62
|
+
/** The `@path` mentions in a prompt that name files in `cwd` (in order, once each). */
|
|
63
|
+
export function mentions(prompt, cwd) {
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const m of prompt.matchAll(MENTION)) {
|
|
66
|
+
const rel = m[1];
|
|
67
|
+
const abs = path.resolve(cwd, rel);
|
|
68
|
+
if (!abs.startsWith(path.resolve(cwd) + path.sep) && abs !== path.resolve(cwd))
|
|
69
|
+
continue; // stays inside the folder
|
|
70
|
+
if (!out.includes(rel) && fs.existsSync(abs) && fs.statSync(abs).isFile())
|
|
71
|
+
out.push(rel);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/** Turn a prompt with `@path` mentions into what this engine needs. */
|
|
76
|
+
export function expandPrompt(prompt, engine, cwd) {
|
|
77
|
+
const files = mentions(prompt, cwd);
|
|
78
|
+
if (files.length === 0)
|
|
79
|
+
return { prompt, files: [], skipped: [] };
|
|
80
|
+
if (engine === "claude")
|
|
81
|
+
return { prompt, files: [], skipped: [] }; // Claude Code reads @path itself
|
|
82
|
+
if (engine === "opencode")
|
|
83
|
+
return { prompt, files: files.map((f) => path.join(cwd, f)), skipped: [] };
|
|
84
|
+
const blocks = [];
|
|
85
|
+
const skipped = [];
|
|
86
|
+
for (const f of files) {
|
|
87
|
+
const buf = fs.readFileSync(path.join(cwd, f));
|
|
88
|
+
if (buf.includes(0)) {
|
|
89
|
+
skipped.push(`${f} (binary)`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (buf.length > MAX_INLINE_BYTES) {
|
|
93
|
+
skipped.push(`${f} (${Math.round(buf.length / 1024)} KB, over ${MAX_INLINE_BYTES / 1024} KB)`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const text = buf.toString("utf8");
|
|
97
|
+
blocks.push(`<file path="${f}">\n${text}${text.endsWith("\n") ? "" : "\n"}</file>`);
|
|
98
|
+
}
|
|
99
|
+
if (blocks.length === 0)
|
|
100
|
+
return { prompt, files: [], skipped };
|
|
101
|
+
return { prompt: `${prompt}\n\nAttached files:\n\n${blocks.join("\n\n")}`, files: [], skipped };
|
|
102
|
+
}
|
package/dist/choices.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { execFile } from "node:child_process";
|
|
8
8
|
import { ENGINE_BINS } from "./engines.js";
|
|
9
|
+
import { loadPiAi } from "./pi-sdk.js";
|
|
9
10
|
/** Reasoning effort / thinking levels each engine accepts. */
|
|
10
11
|
export const EFFORTS = {
|
|
11
12
|
pi: ["off", "minimal", "low", "medium", "high", "xhigh"],
|
|
@@ -46,7 +47,9 @@ export async function modelChoices(engine, env = process.env) {
|
|
|
46
47
|
case "opencode":
|
|
47
48
|
return [{ value: "default", note: "OpenCode's own choice" }, ...(await opencodeModels(env)).map((value) => ({ value }))];
|
|
48
49
|
case "pi": {
|
|
49
|
-
const ai = await
|
|
50
|
+
const ai = await loadPiAi().catch(() => undefined);
|
|
51
|
+
if (!ai)
|
|
52
|
+
return [{ value: "default", note: "pi isn't installed" }];
|
|
50
53
|
const providers = Object.keys(PI_KEYS).filter((p) => PI_KEYS[p].some((k) => env[k]));
|
|
51
54
|
const out = [];
|
|
52
55
|
for (const provider of providers.length ? providers : ["anthropic"]) {
|
package/dist/cli-engines.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
|
+
import { expandPrompt } from "./attach.js";
|
|
13
14
|
import { ENGINE_BINS, ENGINE_NAMES } from "./engines.js";
|
|
14
15
|
import { Session } from "./session.js";
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
@@ -76,8 +77,9 @@ export const claudeAdapter = {
|
|
|
76
77
|
case "result": {
|
|
77
78
|
const u = e.usage ?? {};
|
|
78
79
|
const tokens = (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.output_tokens ?? 0);
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
const cost = typeof e.total_cost_usd === "number" ? e.total_cost_usd : undefined;
|
|
81
|
+
if (tokens || cost !== undefined)
|
|
82
|
+
out.push({ type: "usage", tokens, ...(cost !== undefined ? { cost } : {}) });
|
|
81
83
|
if (e.session_id)
|
|
82
84
|
out.push({ type: "session", id: e.session_id });
|
|
83
85
|
const ok = !e.is_error && e.subtype === "success";
|
|
@@ -209,6 +211,7 @@ export const opencodeAdapter = {
|
|
|
209
211
|
...(o.model ? ["-m", o.model] : []),
|
|
210
212
|
...(o.effort ? ["--variant", o.effort] : []),
|
|
211
213
|
...(o.resumeId ? ["--session", o.resumeId] : []),
|
|
214
|
+
...(o.files ?? []).flatMap((f) => ["-f", f]),
|
|
212
215
|
"--",
|
|
213
216
|
prompt,
|
|
214
217
|
];
|
|
@@ -238,8 +241,9 @@ export const opencodeAdapter = {
|
|
|
238
241
|
case "step_finish": {
|
|
239
242
|
const t = part.tokens ?? {};
|
|
240
243
|
const tokens = (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) + (t.cache?.read ?? 0) + (t.cache?.write ?? 0);
|
|
241
|
-
|
|
242
|
-
|
|
244
|
+
const cost = typeof part.cost === "number" ? part.cost : undefined;
|
|
245
|
+
if (tokens || cost !== undefined)
|
|
246
|
+
out.push({ type: "usage", tokens, ...(cost !== undefined ? { cost } : {}) });
|
|
243
247
|
break;
|
|
244
248
|
}
|
|
245
249
|
case "error": {
|
|
@@ -302,8 +306,12 @@ export class CliSession extends Session {
|
|
|
302
306
|
this.pushUser(text);
|
|
303
307
|
this.setState("running");
|
|
304
308
|
this.aborted = false;
|
|
305
|
-
const
|
|
309
|
+
const attached = expandPrompt(text, this.adapter.id, this.cwd);
|
|
310
|
+
if (attached.skipped.length)
|
|
311
|
+
this.pushInfo(`not attached: ${attached.skipped.join(", ")}`);
|
|
312
|
+
const args = this.adapter.args(attached.prompt, {
|
|
306
313
|
cwd: this.cwd,
|
|
314
|
+
files: attached.files,
|
|
307
315
|
model: this.requestedModel,
|
|
308
316
|
effort: this.effort,
|
|
309
317
|
resumeId: this.resumeId,
|
|
@@ -381,6 +389,8 @@ export class CliSession extends Session {
|
|
|
381
389
|
}
|
|
382
390
|
case "usage":
|
|
383
391
|
this.tokens += ev.tokens;
|
|
392
|
+
if (ev.cost !== undefined)
|
|
393
|
+
this.cost = (this.cost ?? 0) + ev.cost;
|
|
384
394
|
this.emit();
|
|
385
395
|
break;
|
|
386
396
|
case "error":
|
package/dist/cli.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
-
import { ProcessTerminal, TUI } from "
|
|
7
|
-
import { loadAgentMeta,
|
|
6
|
+
import { ProcessTerminal, TUI } from "./tui.js";
|
|
7
|
+
import { loadAgentMeta, reopenableAgents, saveAgentMeta, sessionDir } from "./agents.js";
|
|
8
8
|
import { App } from "./app.js";
|
|
9
|
+
import { FOCUS_REPORTING_OFF, FOCUS_REPORTING_ON } from "./notify.js";
|
|
9
10
|
import { staleBuild, VERSION_TEXT } from "./version.js";
|
|
10
11
|
import { CliSession } from "./cli-engines.js";
|
|
11
12
|
import { defaultEngine, detectEngines, ENGINE_BINS, isEngineId, onPath, piConnected, quickEngines } from "./engines.js";
|
|
@@ -23,6 +24,7 @@ const HELP = `vigent — vim-native agent orchestrator (pi engine)
|
|
|
23
24
|
usage:
|
|
24
25
|
vigent --version which vigent this is (and the commit, from a clone)
|
|
25
26
|
vigent [folder] [--demo] [--theme <name>] [--model <id>] [--engine pi|claude|codex|opencode]
|
|
27
|
+
vigent --serve [--local] also follow your agents from your phone (:serve)
|
|
26
28
|
|
|
27
29
|
engines (what runs new agents; :checkhealth shows which are signed in):
|
|
28
30
|
pi built in, uses ANTHROPIC_API_KEY / OPENAI_API_KEY / ... or pi's login
|
|
@@ -313,6 +315,7 @@ async function main() {
|
|
|
313
315
|
},
|
|
314
316
|
});
|
|
315
317
|
// model and effort for new agents, per engine, from [engines.<id>]
|
|
318
|
+
app.templates = config.templates;
|
|
316
319
|
for (const [id, opts] of Object.entries(config.engines)) {
|
|
317
320
|
if (opts?.model)
|
|
318
321
|
app.newAgentModel[id] = opts.model;
|
|
@@ -331,7 +334,7 @@ async function main() {
|
|
|
331
334
|
saveAgentMeta(wt, {
|
|
332
335
|
...meta,
|
|
333
336
|
model: s instanceof CliSession ? (requested ?? "") : s.model,
|
|
334
|
-
...(s instanceof CliSession ? { requestedModel: requested } : {}),
|
|
337
|
+
...(s instanceof CliSession ? { requestedModel: requested, usage: { tokens: s.tokens, ...(s.cost !== undefined ? { cost: s.cost } : {}) } } : {}),
|
|
335
338
|
effort: s.effort,
|
|
336
339
|
});
|
|
337
340
|
};
|
|
@@ -365,8 +368,13 @@ async function main() {
|
|
|
365
368
|
/** Reopen a saved agent with the engine it was started with. */
|
|
366
369
|
const reopen = (meta, worktree) => {
|
|
367
370
|
const engine = meta.engine && isEngineId(meta.engine) ? meta.engine : "pi";
|
|
368
|
-
if (engine !== "pi")
|
|
369
|
-
|
|
371
|
+
if (engine !== "pi") {
|
|
372
|
+
const s = cliSession(meta.name, engine, meta.requestedModel, worktree.path, worktree, meta.resumeId, meta.effort);
|
|
373
|
+
// the CLI keeps the conversation; vigent keeps the running totals
|
|
374
|
+
s.tokens = meta.usage?.tokens ?? 0;
|
|
375
|
+
s.cost = meta.usage?.cost;
|
|
376
|
+
return s;
|
|
377
|
+
}
|
|
370
378
|
return new PiSession(meta.name, meta.model, { cwd: worktree.path, worktree, sessionDir: sessionDir(worktree.path), effort: meta.effort });
|
|
371
379
|
};
|
|
372
380
|
app.attachSession = (wt) => {
|
|
@@ -389,6 +397,7 @@ async function main() {
|
|
|
389
397
|
s.abort();
|
|
390
398
|
s.dispose();
|
|
391
399
|
}
|
|
400
|
+
terminal.write(FOCUS_REPORTING_OFF);
|
|
392
401
|
tui.stop();
|
|
393
402
|
process.exit(0);
|
|
394
403
|
};
|
|
@@ -403,17 +412,13 @@ async function main() {
|
|
|
403
412
|
}
|
|
404
413
|
else if (root && config.worktree.enabled) {
|
|
405
414
|
// reopen the agents that were running when vigent last quit
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (view && !view.ready) {
|
|
412
|
-
warnings.push(`couldn't reopen '${meta.name}': ${view.reason}`);
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
+
// riftri views on OverlayFS lose their mount on reboot: they're repaired before any agent's record is read
|
|
416
|
+
const { agents, blocked } = reopenableAgents(root, config.worktree, prepareViews);
|
|
417
|
+
for (const b of blocked)
|
|
418
|
+
warnings.push(`couldn't reopen the agent in ${b.path}: ${b.reason}`);
|
|
419
|
+
for (const { meta, worktree, backend } of agents) {
|
|
415
420
|
try {
|
|
416
|
-
app.addSession(reopen(meta, { ...worktree, riftri:
|
|
421
|
+
app.addSession(reopen(meta, { ...worktree, riftri: backend }), false);
|
|
417
422
|
}
|
|
418
423
|
catch (err) {
|
|
419
424
|
warnings.push(`couldn't reopen '${meta.name}': ${err.message}`);
|
|
@@ -428,6 +433,8 @@ async function main() {
|
|
|
428
433
|
tui.setFocus(app);
|
|
429
434
|
terminal.setTitle("vigent");
|
|
430
435
|
tui.start();
|
|
436
|
+
// focus reports let a notification skip the agent you're looking at
|
|
437
|
+
terminal.write(FOCUS_REPORTING_ON);
|
|
431
438
|
if (warnings.length > 0)
|
|
432
439
|
app.notify(warnings.join(" · "));
|
|
433
440
|
if (isFollow(themeName)) {
|
|
@@ -438,6 +445,9 @@ async function main() {
|
|
|
438
445
|
});
|
|
439
446
|
}
|
|
440
447
|
tui.requestRender(true);
|
|
448
|
+
// follow from your phone from the start (same as :serve)
|
|
449
|
+
if (argv.includes("--serve"))
|
|
450
|
+
void app.execServe(argv.includes("--local") ? "local" : "");
|
|
441
451
|
return undefined;
|
|
442
452
|
}
|
|
443
453
|
main()
|
package/dist/config.js
CHANGED
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
* CursorLine = "guibg=#2a2f3a"
|
|
16
16
|
* VigentSidebar = { link = "NormalFloat" }
|
|
17
17
|
*/
|
|
18
|
+
import { NOTIFY_MODES } from "./notify.js";
|
|
18
19
|
import * as fs from "node:fs";
|
|
19
20
|
import * as path from "node:path";
|
|
20
21
|
import { parse as parseToml } from "smol-toml";
|
|
21
22
|
import { EFFORTS } from "./choices.js";
|
|
23
|
+
import { parseTemplates } from "./templates.js";
|
|
22
24
|
import { ICON_SETS } from "./icons.js";
|
|
23
25
|
import { isEngineId } from "./engines.js";
|
|
24
26
|
import { WORKTREE_BACKENDS } from "./riftri.js";
|
|
@@ -36,13 +38,14 @@ export const DEFAULT_UI = {
|
|
|
36
38
|
termguicolors: process.env.TERM_PROGRAM !== "Apple_Terminal",
|
|
37
39
|
transparent: "auto",
|
|
38
40
|
whichkey: true,
|
|
41
|
+
notify: "auto",
|
|
39
42
|
};
|
|
40
43
|
export function configPath() {
|
|
41
44
|
return path.join(configDir(), "config.toml");
|
|
42
45
|
}
|
|
43
46
|
/** Load the config file. Problems become warnings so a typo never stops vigent from starting. */
|
|
44
47
|
export function loadConfig(file = configPath()) {
|
|
45
|
-
const config = { ui: { ...DEFAULT_UI }, highlights: {}, keys: {}, worktree: { ...DEFAULT_WORKTREE }, engines: {} };
|
|
48
|
+
const config = { ui: { ...DEFAULT_UI }, highlights: {}, keys: {}, worktree: { ...DEFAULT_WORKTREE }, engines: {}, templates: {} };
|
|
46
49
|
const warnings = [];
|
|
47
50
|
if (!fs.existsSync(file))
|
|
48
51
|
return { config, warnings };
|
|
@@ -104,6 +107,9 @@ export function loadConfig(file = configPath()) {
|
|
|
104
107
|
config.engines[id] = e;
|
|
105
108
|
}
|
|
106
109
|
break;
|
|
110
|
+
case "templates":
|
|
111
|
+
config.templates = parseTemplates(value, warnings);
|
|
112
|
+
break;
|
|
107
113
|
case "keys":
|
|
108
114
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
109
115
|
warnings.push("config: [keys] should be a table");
|
|
@@ -200,6 +206,7 @@ const ALIASES = {
|
|
|
200
206
|
transp: "transparent",
|
|
201
207
|
whichkey: "whichkey",
|
|
202
208
|
wk: "whichkey",
|
|
209
|
+
notify: "notify",
|
|
203
210
|
};
|
|
204
211
|
/** Set one option from a config value. Returns an error message, or undefined on success. */
|
|
205
212
|
export function setOption(ui, name, value) {
|
|
@@ -223,6 +230,11 @@ export function setOption(ui, name, value) {
|
|
|
223
230
|
ui.sidebar_width = n;
|
|
224
231
|
return;
|
|
225
232
|
}
|
|
233
|
+
case "notify":
|
|
234
|
+
if (!NOTIFY_MODES.includes(value))
|
|
235
|
+
return `'${name}' should be "auto", "bell", "desktop" or "off"`;
|
|
236
|
+
ui.notify = value;
|
|
237
|
+
return;
|
|
226
238
|
case "icons":
|
|
227
239
|
if (!ICON_SETS.includes(value))
|
|
228
240
|
return `'${name}' should be "unicode", "nerd" or "ascii"`;
|
|
@@ -327,7 +339,7 @@ theme = "default"
|
|
|
327
339
|
# --- Engines ---------------------------------------------------------------
|
|
328
340
|
# What runs new agents: "pi" (built in), or a coding-agent CLI you already
|
|
329
341
|
# have installed and signed in: "claude" (Claude Code), "codex", "opencode".
|
|
330
|
-
# "auto" picks pi if it has an API key
|
|
342
|
+
# "auto" picks the first signed-in CLI (your own login), else pi if it has an API key.
|
|
331
343
|
# Per agent: :new fix the race --engine codex. Status: :checkhealth
|
|
332
344
|
engine = "auto"
|
|
333
345
|
|
|
@@ -340,6 +352,17 @@ engine = "auto"
|
|
|
340
352
|
# [engines.opencode]
|
|
341
353
|
# model = "anthropic/claude-sonnet-4-5"
|
|
342
354
|
|
|
355
|
+
# --- Templates -------------------------------------------------------------
|
|
356
|
+
# Prompts you start agents with often. :new -t fix <task> starts an agent and
|
|
357
|
+
# sends the prompt, {task} replaced by what you typed; :templates picks one.
|
|
358
|
+
# Built in: fix, test, review, explain (one of yours with the same name
|
|
359
|
+
# replaces it). engine, model, effort and worktree are optional.
|
|
360
|
+
# [templates.migrate]
|
|
361
|
+
# description = "a database migration, with a rollback"
|
|
362
|
+
# prompt = "Write a migration that {task}. Include the rollback, and test both."
|
|
363
|
+
# engine = "claude"
|
|
364
|
+
# effort = "high"
|
|
365
|
+
|
|
343
366
|
# --- Leader ----------------------------------------------------------------
|
|
344
367
|
# What <leader> means in [keys]. Vim's default is "\\".
|
|
345
368
|
leader = "<Space>"
|
|
@@ -355,6 +378,7 @@ timeoutlen = 1000 # ms to wait for the rest of a multi-key mapping
|
|
|
355
378
|
# termguicolors = false # for terminals without 24-bit color: use the nearest of 256
|
|
356
379
|
# transparent = true # use the terminal's background with any theme ("auto": the theme decides)
|
|
357
380
|
whichkey = true # after <leader>, show what each next key does (like which-key.nvim)
|
|
381
|
+
notify = "auto" # an agent finished while you looked elsewhere: "auto", "bell", "desktop", "off"
|
|
358
382
|
|
|
359
383
|
# --- Colors (also live: :hi Group guifg=#... guibg=#...) -------------------
|
|
360
384
|
# Applied on top of whichever theme is active. A value is a table, a vim
|
package/dist/diffview.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Both return the line indices of hunk headers so ]c / [c can jump.
|
|
9
9
|
*/
|
|
10
|
-
import { truncateToWidth, visibleWidth } from "
|
|
10
|
+
import { truncateToWidth, visibleWidth } from "./tui.js";
|
|
11
11
|
import { fileIcon } from "./icons.js";
|
|
12
12
|
function lineGroup(line) {
|
|
13
13
|
if (line.startsWith("+++") || line.startsWith("---"))
|
package/dist/engines.js
CHANGED
|
@@ -10,6 +10,7 @@ import { execFile } from "node:child_process";
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as os from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { PI_INSTALL, piInstalled } from "./pi-sdk.js";
|
|
13
14
|
export const ENGINE_IDS = ["pi", "claude", "codex", "opencode"];
|
|
14
15
|
export const ENGINE_NAMES = {
|
|
15
16
|
pi: "pi",
|
|
@@ -53,22 +54,25 @@ export function onPath(bin, env = process.env) {
|
|
|
53
54
|
export function piConnected(env = process.env) {
|
|
54
55
|
return detectPi(env).connected;
|
|
55
56
|
}
|
|
57
|
+
const CLI_IDS = ["claude", "codex", "opencode"];
|
|
56
58
|
/**
|
|
57
|
-
* The engine new agents use when none is given:
|
|
58
|
-
*
|
|
59
|
-
*
|
|
59
|
+
* The engine new agents use when none is given ("auto"): the first signed-in
|
|
60
|
+
* CLI (Claude Code, Codex, OpenCode), so your own login and settings are used;
|
|
61
|
+
* pi only when no CLI is ready and pi has a key. While the sign-in checks run,
|
|
62
|
+
* an installed CLI still wins over pi. With nothing ready, the first installed
|
|
63
|
+
* CLI, else pi (starting an agent then says how to connect one).
|
|
60
64
|
*/
|
|
61
65
|
export function defaultEngine(engines, env = process.env) {
|
|
62
66
|
if (engines) {
|
|
63
|
-
const
|
|
67
|
+
const cli = (ok) => CLI_IDS.map((id) => engines.find((e) => e.id === id)).find((e) => e && ok(e))?.id;
|
|
68
|
+
const ready = cli((e) => e.connected) ?? cli((e) => e.installed && Boolean(e.checking));
|
|
64
69
|
if (ready)
|
|
65
|
-
return ready
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
return ready;
|
|
71
|
+
if (engines.find((e) => e.id === "pi")?.connected)
|
|
72
|
+
return "pi";
|
|
73
|
+
return cli((e) => e.installed) ?? "pi";
|
|
68
74
|
}
|
|
69
|
-
|
|
70
|
-
return "pi";
|
|
71
|
-
for (const id of ["claude", "codex", "opencode"])
|
|
75
|
+
for (const id of CLI_IDS)
|
|
72
76
|
if (onPath(ENGINE_BINS[id], env))
|
|
73
77
|
return id;
|
|
74
78
|
return "pi";
|
|
@@ -88,7 +92,10 @@ function strip(s) {
|
|
|
88
92
|
function firstVersion(s) {
|
|
89
93
|
return /\d+\.\d+\.\d+[\w.-]*/.exec(s)?.[0];
|
|
90
94
|
}
|
|
91
|
-
function detectPi(env) {
|
|
95
|
+
export function detectPi(env, installed = piInstalled()) {
|
|
96
|
+
if (!installed) {
|
|
97
|
+
return { id: "pi", name: ENGINE_NAMES.pi, installed: false, connected: false, detail: "not installed (light install)", install: PI_INSTALL };
|
|
98
|
+
}
|
|
92
99
|
const keys = PI_KEYS.filter((k) => env[k]);
|
|
93
100
|
let stored = false;
|
|
94
101
|
try {
|
package/dist/float.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* optionally with selectable rows (`:dash`) and jump points (`]c` / `[c`
|
|
4
4
|
* between diff hunks).
|
|
5
5
|
*/
|
|
6
|
-
import { parseKey, truncateToWidth, visibleWidth } from "
|
|
6
|
+
import { parseKey, truncateToWidth, visibleWidth } from "./tui.js";
|
|
7
7
|
import { RESET } from "./theme.js";
|
|
8
8
|
export class TextFloat {
|
|
9
9
|
painter;
|