@vigent/cli 0.2.2 → 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 +86 -28
- package/dist/agents.js +29 -0
- package/dist/app.js +565 -21
- 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 +4 -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 +92 -47
- package/dist/serve-page.js +247 -0
- package/dist/serve.js +267 -0
- package/dist/session.js +145 -10
- 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 +4 -4
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `:review` window: the agent's changes hunk by hunk.
|
|
3
|
+
*
|
|
4
|
+
* j/k ]c/[c next / previous hunk J/K next / previous file
|
|
5
|
+
* x drop the hunk (take it out of the worktree)
|
|
6
|
+
* X drop the whole file u undo the last drop
|
|
7
|
+
* q / Esc close
|
|
8
|
+
*/
|
|
9
|
+
import { truncateToWidth, visibleWidth } from "./tui.js";
|
|
10
|
+
import { patchFor } from "./hunks.js";
|
|
11
|
+
import { keyFromInput } from "./keys.js";
|
|
12
|
+
import { RESET } from "./theme.js";
|
|
13
|
+
export class HunkReview {
|
|
14
|
+
painter;
|
|
15
|
+
icons;
|
|
16
|
+
title;
|
|
17
|
+
files;
|
|
18
|
+
height;
|
|
19
|
+
host;
|
|
20
|
+
flat = [];
|
|
21
|
+
index = 0;
|
|
22
|
+
top = 0;
|
|
23
|
+
undo = [];
|
|
24
|
+
dropped = 0;
|
|
25
|
+
message = "";
|
|
26
|
+
busy = false;
|
|
27
|
+
constructor(painter, icons, title, files, height, host) {
|
|
28
|
+
this.painter = painter;
|
|
29
|
+
this.icons = icons;
|
|
30
|
+
this.title = title;
|
|
31
|
+
this.files = files;
|
|
32
|
+
this.height = height;
|
|
33
|
+
this.host = host;
|
|
34
|
+
this.flatten();
|
|
35
|
+
}
|
|
36
|
+
flatten() {
|
|
37
|
+
this.flat = this.files.flatMap((file, fileIdx) => file.hunks.map((hunk) => ({ file, hunk, fileIdx })));
|
|
38
|
+
this.index = Math.min(this.index, Math.max(0, this.flat.length - 1));
|
|
39
|
+
}
|
|
40
|
+
rows(width) {
|
|
41
|
+
const out = [];
|
|
42
|
+
this.files.forEach((file, fi) => {
|
|
43
|
+
const n = file.hunks.length;
|
|
44
|
+
out.push({ text: `${file.path} ${file.binary ? "(binary: X drops it)" : `${n} hunk${n === 1 ? "" : "s"}`}`, group: "Title", hunk: -1, header: true });
|
|
45
|
+
for (const hunk of file.hunks) {
|
|
46
|
+
const idx = this.flat.findIndex((h) => h.hunk === hunk && h.fileIdx === fi);
|
|
47
|
+
out.push({ text: hunk.header, group: "Special", hunk: idx, header: true });
|
|
48
|
+
for (const l of hunk.lines) {
|
|
49
|
+
out.push({
|
|
50
|
+
text: truncateToWidth(l.replace(/\t/g, " "), width, "…"),
|
|
51
|
+
group: l.startsWith("+") ? "Added" : l.startsWith("-") ? "Removed" : l.startsWith("\\") ? "Comment" : "NormalFloat",
|
|
52
|
+
hunk: idx,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
handleInput(data) {
|
|
60
|
+
const key = keyFromInput(data);
|
|
61
|
+
if (this.busy)
|
|
62
|
+
return;
|
|
63
|
+
switch (key) {
|
|
64
|
+
case "q":
|
|
65
|
+
case "<Esc>":
|
|
66
|
+
case "<C-c>":
|
|
67
|
+
this.host.close();
|
|
68
|
+
return;
|
|
69
|
+
case "j":
|
|
70
|
+
case "n":
|
|
71
|
+
case "<Down>":
|
|
72
|
+
case "<Tab>":
|
|
73
|
+
this.move(1);
|
|
74
|
+
break;
|
|
75
|
+
case "k":
|
|
76
|
+
case "N":
|
|
77
|
+
case "<Up>":
|
|
78
|
+
case "<S-Tab>":
|
|
79
|
+
this.move(-1);
|
|
80
|
+
break;
|
|
81
|
+
case "]":
|
|
82
|
+
case "[":
|
|
83
|
+
this.pendingBracket = key;
|
|
84
|
+
return;
|
|
85
|
+
case "c":
|
|
86
|
+
if (this.pendingBracket)
|
|
87
|
+
this.move(this.pendingBracket === "]" ? 1 : -1);
|
|
88
|
+
break;
|
|
89
|
+
case "J":
|
|
90
|
+
this.moveFile(1);
|
|
91
|
+
break;
|
|
92
|
+
case "K":
|
|
93
|
+
this.moveFile(-1);
|
|
94
|
+
break;
|
|
95
|
+
case "g":
|
|
96
|
+
this.index = 0;
|
|
97
|
+
break;
|
|
98
|
+
case "G":
|
|
99
|
+
this.index = Math.max(0, this.flat.length - 1);
|
|
100
|
+
break;
|
|
101
|
+
case "x":
|
|
102
|
+
this.dropHunk();
|
|
103
|
+
return;
|
|
104
|
+
case "X":
|
|
105
|
+
this.dropFile();
|
|
106
|
+
return;
|
|
107
|
+
case "u":
|
|
108
|
+
this.undoLast();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.pendingBracket = undefined;
|
|
112
|
+
this.host.requestRender();
|
|
113
|
+
}
|
|
114
|
+
pendingBracket;
|
|
115
|
+
move(d) {
|
|
116
|
+
if (this.flat.length === 0)
|
|
117
|
+
return;
|
|
118
|
+
this.index = Math.max(0, Math.min(this.flat.length - 1, this.index + d));
|
|
119
|
+
}
|
|
120
|
+
moveFile(d) {
|
|
121
|
+
const cur = this.flat[this.index]?.fileIdx ?? 0;
|
|
122
|
+
const target = d > 0 ? this.flat.findIndex((h) => h.fileIdx > cur) : this.flat.findIndex((h) => h.fileIdx === Math.max(0, cur - 1));
|
|
123
|
+
if (target >= 0)
|
|
124
|
+
this.index = target;
|
|
125
|
+
}
|
|
126
|
+
run(patch, label, action) {
|
|
127
|
+
this.busy = true;
|
|
128
|
+
this.message = action === "drop" ? "dropping…" : "restoring…";
|
|
129
|
+
this.host.requestRender();
|
|
130
|
+
(action === "drop" ? this.host.drop(patch) : this.host.restore(patch))
|
|
131
|
+
.then((files) => {
|
|
132
|
+
if (action === "drop") {
|
|
133
|
+
this.undo.push({ patch, label });
|
|
134
|
+
this.dropped++;
|
|
135
|
+
this.message = `dropped ${label} · u undoes`;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
this.dropped--;
|
|
139
|
+
this.message = `restored ${label}`;
|
|
140
|
+
}
|
|
141
|
+
this.files = files;
|
|
142
|
+
this.flatten();
|
|
143
|
+
})
|
|
144
|
+
.catch((err) => {
|
|
145
|
+
this.message = `git couldn't ${action === "drop" ? "drop" : "restore"} ${label}: ${err.message}`;
|
|
146
|
+
})
|
|
147
|
+
.finally(() => {
|
|
148
|
+
this.busy = false;
|
|
149
|
+
this.host.requestRender();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
dropHunk() {
|
|
153
|
+
const cur = this.flat[this.index];
|
|
154
|
+
if (!cur)
|
|
155
|
+
return;
|
|
156
|
+
const n = cur.file.hunks.indexOf(cur.hunk) + 1;
|
|
157
|
+
this.run(patchFor(cur.file, [cur.hunk]), `hunk ${n} of ${cur.file.path}`, "drop");
|
|
158
|
+
}
|
|
159
|
+
dropFile() {
|
|
160
|
+
const cur = this.flat[this.index];
|
|
161
|
+
const file = cur?.file ?? this.files[0];
|
|
162
|
+
if (!file)
|
|
163
|
+
return;
|
|
164
|
+
if (file.binary && file.hunks.length === 0) {
|
|
165
|
+
this.message = "binary files can't be dropped here yet";
|
|
166
|
+
this.host.requestRender();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.run(patchFor(file), file.path, "drop");
|
|
170
|
+
}
|
|
171
|
+
undoLast() {
|
|
172
|
+
const last = this.undo.pop();
|
|
173
|
+
if (!last) {
|
|
174
|
+
this.message = "nothing to undo";
|
|
175
|
+
this.host.requestRender();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.run(last.patch, last.label, "restore");
|
|
179
|
+
}
|
|
180
|
+
invalidate() { }
|
|
181
|
+
render(width) {
|
|
182
|
+
const P = this.painter;
|
|
183
|
+
const I = this.icons;
|
|
184
|
+
const inner = Math.max(30, width) - 2;
|
|
185
|
+
const bg = P.attr("NormalFloat").bg;
|
|
186
|
+
const sel = P.attr("PmenuSel").bg ?? P.attr("Visual").bg;
|
|
187
|
+
const border = (s) => P.on("FloatBorder", bg) + s + RESET;
|
|
188
|
+
const row = (content, used, rowBg = bg) => border(I.v) + content + P.on("NormalFloat", rowBg) + " ".repeat(Math.max(0, inner - used)) + RESET + border(I.v);
|
|
189
|
+
const total = this.flat.length;
|
|
190
|
+
const title = ` ${this.title} · ${total} hunk${total === 1 ? "" : "s"} left${this.dropped ? ` · ${this.dropped} dropped` : ""} `;
|
|
191
|
+
const lines = [border(I.tl + I.h) + P.on("FloatTitle", bg) + truncateToWidth(title, inner - 2, "…") + RESET + border(I.h.repeat(Math.max(0, inner - 1 - visibleWidth(truncateToWidth(title, inner - 2, "…")))) + I.tr)];
|
|
192
|
+
const body = Math.max(3, this.height() - 4);
|
|
193
|
+
const rows = this.rows(inner - 3);
|
|
194
|
+
if (total === 0) {
|
|
195
|
+
rows.length = 0;
|
|
196
|
+
rows.push({ text: this.dropped ? "every change is dropped: nothing would be merged (u undoes)" : "no changes", group: "Comment", hunk: -1 });
|
|
197
|
+
}
|
|
198
|
+
// keep the selected hunk's header in view, a little below the top
|
|
199
|
+
const at = rows.findIndex((r) => r.hunk === this.index && r.header);
|
|
200
|
+
if (at >= 0 && (at < this.top || at >= this.top + body - 3))
|
|
201
|
+
this.top = Math.max(0, at - 2);
|
|
202
|
+
this.top = Math.min(this.top, Math.max(0, rows.length - body));
|
|
203
|
+
for (const r of rows.slice(this.top, this.top + body)) {
|
|
204
|
+
const on = r.hunk === this.index && total > 0;
|
|
205
|
+
const rowBg = on && r.header ? sel : bg;
|
|
206
|
+
const bar = on ? P.on("Title", rowBg) + "▌ " : P.on("NormalFloat", rowBg) + " ";
|
|
207
|
+
const text = truncateToWidth(r.text, inner - 3, "…");
|
|
208
|
+
lines.push(row(bar + P.on(on && r.header ? "PmenuSel" : r.group, rowBg) + text, 2 + visibleWidth(text), rowBg));
|
|
209
|
+
}
|
|
210
|
+
while (lines.length < body + 1)
|
|
211
|
+
lines.push(row("", 0));
|
|
212
|
+
const msg = this.message || "x drop hunk · X drop file · u undo · j/k hunks · J/K files · q close · :merge takes what's left";
|
|
213
|
+
lines.push(border(I.lt + I.h.repeat(inner) + I.rt));
|
|
214
|
+
lines.push(row(P.on(this.message ? "ModeMsg" : "Comment", bg) + ` ${truncateToWidth(msg, inner - 2, "…")}`, Math.min(inner, visibleWidth(msg) + 1)));
|
|
215
|
+
lines.push(border(I.bl + I.h.repeat(inner) + I.br));
|
|
216
|
+
return lines;
|
|
217
|
+
}
|
|
218
|
+
}
|
package/dist/hunks.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `:review`: keep or drop the agent's changes one hunk at a time.
|
|
3
|
+
*
|
|
4
|
+
* The diff is the agent's whole change, fork point → working tree (commits,
|
|
5
|
+
* uncommitted edits and new files). Dropping a hunk reverse-applies just that
|
|
6
|
+
* hunk to the worktree (`git apply -R`), so `:merge` / `:pr`, which commit
|
|
7
|
+
* what's in the worktree, no longer carry it. Undo applies it again.
|
|
8
|
+
*/
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
/** Split `git diff` output into files and hunks. */
|
|
11
|
+
export function parseDiff(text) {
|
|
12
|
+
const files = [];
|
|
13
|
+
let file;
|
|
14
|
+
let hunk;
|
|
15
|
+
for (const line of text.replace(/\n$/, "").split("\n")) {
|
|
16
|
+
if (line.startsWith("diff --git ")) {
|
|
17
|
+
const m = /^diff --git a\/(.*) b\/(.*)$/.exec(line);
|
|
18
|
+
file = { path: m ? m[2] : line.slice(11), header: [line], hunks: [], binary: false };
|
|
19
|
+
files.push(file);
|
|
20
|
+
hunk = undefined;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!file)
|
|
24
|
+
continue;
|
|
25
|
+
if (line.startsWith("@@")) {
|
|
26
|
+
hunk = { header: line, lines: [] };
|
|
27
|
+
file.hunks.push(hunk);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (hunk) {
|
|
31
|
+
// "… N more lines" from a truncated diff is not part of any hunk
|
|
32
|
+
if (/^[ +\-\\]/.test(line) || line === "")
|
|
33
|
+
hunk.lines.push(line);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (/^Binary files /.test(line) || line === "GIT binary patch")
|
|
37
|
+
file.binary = true;
|
|
38
|
+
file.header.push(line);
|
|
39
|
+
}
|
|
40
|
+
return files;
|
|
41
|
+
}
|
|
42
|
+
/** A patch with one file's header and just these of its hunks. */
|
|
43
|
+
export function patchFor(file, hunks = file.hunks) {
|
|
44
|
+
// a new file's single hunk must create the whole file; a partial patch can't say "new file"
|
|
45
|
+
return `${[...file.header, ...hunks.flatMap((h) => [h.header, ...h.lines])].join("\n")}\n`;
|
|
46
|
+
}
|
|
47
|
+
function gitApply(cwd, patch, reverse) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const child = execFile("git", ["apply", ...(reverse ? ["-R"] : []), "--whitespace=nowarn", "--recount", "-"], { cwd }, (err, _stdout, stderr) => (err ? reject(new Error(String(stderr).trim().split("\n")[0].replace(/^error: /, "") || err.message)) : resolve()));
|
|
50
|
+
child.stdin?.end(patch);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** Take a change out of the agent's worktree. */
|
|
54
|
+
export function dropPatch(worktree, patch) {
|
|
55
|
+
return gitApply(worktree, patch, true);
|
|
56
|
+
}
|
|
57
|
+
/** Put a dropped change back. */
|
|
58
|
+
export function restorePatch(worktree, patch) {
|
|
59
|
+
return gitApply(worktree, patch, false);
|
|
60
|
+
}
|
package/dist/icons.js
CHANGED
|
@@ -31,6 +31,7 @@ export const NERD = {
|
|
|
31
31
|
list: "", // nf-fa-list
|
|
32
32
|
caretRight: "", // nf-fa-caret_right
|
|
33
33
|
caretDown: "", // nf-fa-caret_down
|
|
34
|
+
bell: "", // nf-fa-bell
|
|
34
35
|
};
|
|
35
36
|
/** An icon for a tool call, by the tool's name (Claude Code, Codex, OpenCode and pi names). */
|
|
36
37
|
export function toolIcon(name) {
|
package/dist/keys.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Like vim, a key that starts a longer mapping waits `timeoutlen` ms for the
|
|
14
14
|
* rest; that is what makes `jk` = `<Esc>` in insert mode work.
|
|
15
15
|
*/
|
|
16
|
-
import { parseKey } from "
|
|
16
|
+
import { parseKey } from "./tui.js";
|
|
17
17
|
import { didYouMean } from "./suggest.js";
|
|
18
18
|
export const KEY_MODES = ["normal", "insert", "visual", "command"];
|
|
19
19
|
/** vigent's default <leader>, as in most Neovim configs (vim's own default is "\\") */
|
|
@@ -127,8 +127,11 @@ export const DEFAULT_KEYS = {
|
|
|
127
127
|
"<leader>D": ":dash<CR>",
|
|
128
128
|
"<leader>u": "session.tree",
|
|
129
129
|
"<leader>s": ":settings<CR>",
|
|
130
|
+
"<leader>r": ":review<CR>",
|
|
131
|
+
"<leader>T": ":templates<CR>",
|
|
130
132
|
"<leader>M": ":model<CR>",
|
|
131
133
|
"<leader>e": ":effort<CR>",
|
|
134
|
+
"<leader>E": ":engine<CR>",
|
|
132
135
|
ZZ: "app.quit",
|
|
133
136
|
ZQ: "app.quit_force",
|
|
134
137
|
q: "app.quit_hint",
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `vigent` command. Before anything else loads, it turns on Node's
|
|
4
|
+
* compile cache (Node 22.1 and later): the compiled code of vigent and its
|
|
5
|
+
* packages is kept on disk, so later starts skip compiling it again. That
|
|
6
|
+
* matters most for the pi engine's SDK, which is large. Set
|
|
7
|
+
* NODE_DISABLE_COMPILE_CACHE=1 to turn it off.
|
|
8
|
+
*/
|
|
9
|
+
import module from "node:module";
|
|
10
|
+
try {
|
|
11
|
+
module.enableCompileCache?.();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// no cache: a read-only temp folder, say. Starting matters more.
|
|
15
|
+
}
|
|
16
|
+
await import("./cli.js");
|
package/dist/markdown.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* bullet lists, and > quotes (Comment). Code blocks are numbered so
|
|
5
5
|
* VISUAL-mode `ib` and `yib` can select them.
|
|
6
6
|
*/
|
|
7
|
-
import { wrapTextWithAnsi } from "
|
|
7
|
+
import { wrapTextWithAnsi } from "./tui.js";
|
|
8
8
|
import { RESET } from "./theme.js";
|
|
9
9
|
function inline(P, text) {
|
|
10
10
|
let out = "";
|
package/dist/notify.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telling you an agent is done while you look elsewhere: the terminal bell,
|
|
3
|
+
* and a desktop notification in terminals that show one for OSC 9 (iTerm2,
|
|
4
|
+
* WezTerm, Ghostty, kitty). Inside tmux the notification is passed through
|
|
5
|
+
* to the outer terminal.
|
|
6
|
+
*/
|
|
7
|
+
export const NOTIFY_MODES = ["auto", "bell", "desktop", "off"];
|
|
8
|
+
/** Does this terminal turn OSC 9 into a desktop notification? */
|
|
9
|
+
export function showsDesktopNotifications(env = process.env) {
|
|
10
|
+
const program = env.TERM_PROGRAM ?? "";
|
|
11
|
+
return (["iTerm.app", "WezTerm", "ghostty"].includes(program) ||
|
|
12
|
+
Boolean(env.KITTY_WINDOW_ID) ||
|
|
13
|
+
Boolean(env.GHOSTTY_RESOURCES_DIR) ||
|
|
14
|
+
Boolean(env.WEZTERM_PANE) ||
|
|
15
|
+
/kitty|ghostty/.test(env.TERM ?? ""));
|
|
16
|
+
}
|
|
17
|
+
/** The bytes to write for a notification: a bell, an OSC 9 notification, both, or nothing. */
|
|
18
|
+
export function alertSequence(text, mode, env = process.env) {
|
|
19
|
+
if (mode === "off")
|
|
20
|
+
return "";
|
|
21
|
+
const desktop = mode === "desktop" || (mode === "auto" && showsDesktopNotifications(env));
|
|
22
|
+
let out = mode === "desktop" ? "" : "\x07";
|
|
23
|
+
if (desktop) {
|
|
24
|
+
// no control characters inside the notification text
|
|
25
|
+
const osc = `\x1b]9;${text.replace(/[\x00-\x1f\x7f]/g, " ")}\x07`;
|
|
26
|
+
// tmux swallows OSC 9 unless it's wrapped in a passthrough
|
|
27
|
+
out += env.TMUX ? `\x1bPtmux;${osc.replace(/\x1b/g, "\x1b\x1b")}\x1b\\` : osc;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
/** Ask the terminal to report focus changes (\x1b[I / \x1b[O), so a visible agent doesn't notify. */
|
|
32
|
+
export const FOCUS_REPORTING_ON = "\x1b[?1004h";
|
|
33
|
+
export const FOCUS_REPORTING_OFF = "\x1b[?1004l";
|
package/dist/pi-sdk.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pi engine's packages, loaded only when a pi agent needs them.
|
|
3
|
+
*
|
|
4
|
+
* They're the bulk of an install (about 200 MB with every model provider's
|
|
5
|
+
* SDK), and optional: the light install (`npm install --prefix ~/.vigent
|
|
6
|
+
* --omit=optional @vigent/cli`) leaves them out for people who only run Claude
|
|
7
|
+
* Code, Codex or OpenCode. Then pi shows as not installed, and nothing else
|
|
8
|
+
* changes. (npm ignores --omit=optional with -g, hence the folder.)
|
|
9
|
+
*/
|
|
10
|
+
export const PI_PACKAGE = "@mariozechner/pi-coding-agent";
|
|
11
|
+
export const PI_INSTALL = "install vigent again without --omit=optional to add it";
|
|
12
|
+
/** Whether the pi engine is installed next to vigent (found the way an import would find it). */
|
|
13
|
+
export function piInstalled(resolve = (s) => import.meta.resolve(s)) {
|
|
14
|
+
try {
|
|
15
|
+
resolve(PI_PACKAGE);
|
|
16
|
+
resolve("@mariozechner/pi-ai");
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function missing(err) {
|
|
24
|
+
const code = err?.code;
|
|
25
|
+
return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
|
|
26
|
+
}
|
|
27
|
+
const NOT_INSTALLED = `pi isn't installed (this is vigent's light install): ${PI_INSTALL}`;
|
|
28
|
+
/** pi's agent SDK. */
|
|
29
|
+
export async function loadPiSdk() {
|
|
30
|
+
try {
|
|
31
|
+
return await import("@mariozechner/pi-coding-agent");
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
throw missing(err) ? new Error(NOT_INSTALLED) : err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** pi's model catalog and providers. */
|
|
38
|
+
export async function loadPiAi() {
|
|
39
|
+
try {
|
|
40
|
+
return await import("@mariozechner/pi-ai");
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
throw missing(err) ? new Error(NOT_INSTALLED) : err;
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/pi-ui.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi extensions inside vigent.
|
|
3
|
+
*
|
|
4
|
+
* pi loads extensions (~/.pi/agent/extensions/, .pi/extensions/), skills and
|
|
5
|
+
* prompt templates by itself. What it needs from the program around it is a
|
|
6
|
+
* UI: an extension that asks before `rm -rf` calls ctx.ui.confirm(), a
|
|
7
|
+
* command answers with ctx.ui.notify(). This is that UI, drawn the vigent way:
|
|
8
|
+
* confirm is a y/n question, select and input are pickers, notify goes to the
|
|
9
|
+
* transcript and the message line, setStatus shows in the winbar. The parts
|
|
10
|
+
* that only make sense in pi's own TUI (custom editors, widgets, headers and
|
|
11
|
+
* footers) do nothing here, as they do in pi's print mode.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* pi's ExtensionUIContext over a host that may come later: until vigent
|
|
15
|
+
* attaches one (or in tests without a screen), dialogs answer like pi's print
|
|
16
|
+
* mode: no selection, no confirmation, no input.
|
|
17
|
+
*/
|
|
18
|
+
export function extensionUi(host, theme) {
|
|
19
|
+
const nothing = () => { };
|
|
20
|
+
return {
|
|
21
|
+
select: async (title, options) => host()?.select(title, options),
|
|
22
|
+
confirm: async (title, message) => (await host()?.confirm(title, message)) ?? false,
|
|
23
|
+
input: async (title, placeholder) => host()?.input(title, placeholder),
|
|
24
|
+
editor: async (title, prefill) => host()?.input(title, prefill),
|
|
25
|
+
notify: (message, type = "info") => host()?.notify(String(message), type),
|
|
26
|
+
setStatus: (key, text) => host()?.setStatus(key, text),
|
|
27
|
+
onTerminalInput: () => nothing,
|
|
28
|
+
setWorkingMessage: nothing,
|
|
29
|
+
setWorkingVisible: nothing,
|
|
30
|
+
setWorkingIndicator: nothing,
|
|
31
|
+
setHiddenThinkingLabel: nothing,
|
|
32
|
+
setWidget: nothing,
|
|
33
|
+
setFooter: nothing,
|
|
34
|
+
setHeader: nothing,
|
|
35
|
+
setTitle: nothing,
|
|
36
|
+
custom: async () => undefined,
|
|
37
|
+
pasteToEditor: nothing,
|
|
38
|
+
setEditorText: nothing,
|
|
39
|
+
getEditorText: () => "",
|
|
40
|
+
addAutocompleteProvider: nothing,
|
|
41
|
+
setEditorComponent: nothing,
|
|
42
|
+
getEditorComponent: () => undefined,
|
|
43
|
+
get theme() {
|
|
44
|
+
return theme();
|
|
45
|
+
},
|
|
46
|
+
getAllThemes: () => [],
|
|
47
|
+
getTheme: () => undefined,
|
|
48
|
+
setTheme: () => ({ success: false, error: "vigent draws its own theme (:colorscheme)" }),
|
|
49
|
+
getToolsExpanded: () => false,
|
|
50
|
+
setToolsExpanded: nothing,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Read what pi loaded from a live AgentSession. Anything missing reads as empty. */
|
|
54
|
+
export function piResources(session) {
|
|
55
|
+
const loader = session?.resourceLoader;
|
|
56
|
+
const safe = (fn, fallback) => {
|
|
57
|
+
try {
|
|
58
|
+
return fn() ?? fallback;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return fallback;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const ext = safe(() => loader.getExtensions(), { extensions: [], errors: [] });
|
|
65
|
+
return {
|
|
66
|
+
extensions: [
|
|
67
|
+
...(ext.extensions ?? []).map((e) => ({ path: String(e.resolvedPath ?? e.path ?? "") })),
|
|
68
|
+
...(ext.errors ?? []).map((e) => ({ path: String(e.path ?? ""), error: String(e.error ?? "failed to load") })),
|
|
69
|
+
],
|
|
70
|
+
commands: safe(() => session.extensionRunner.getRegisteredCommands(), []).map((c) => ({
|
|
71
|
+
name: String(c.invocationName ?? c.name),
|
|
72
|
+
description: c.description,
|
|
73
|
+
})),
|
|
74
|
+
skills: safe(() => loader.getSkills().skills, []).map((s) => ({ name: String(s.name), description: s.description, path: s.filePath })),
|
|
75
|
+
prompts: safe(() => session.promptTemplates, []).map((p) => ({ name: String(p.name), description: p.description })),
|
|
76
|
+
contextFiles: safe(() => loader.getAgentsFiles().agentsFiles, []).map((f) => String(f.path)),
|
|
77
|
+
};
|
|
78
|
+
}
|