@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
|
@@ -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,6 +127,8 @@ 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>",
|
|
132
134
|
"<leader>E": ":engine<CR>",
|
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
|
+
}
|
package/dist/picker.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Type to filter, arrows / Ctrl-n / Ctrl-p / Tab to move (each move previews
|
|
5
5
|
* the theme live), Enter to keep it, Esc to go back to the previous theme.
|
|
6
6
|
*/
|
|
7
|
-
import { fuzzyFilter, parseKey, truncateToWidth, visibleWidth } from "
|
|
7
|
+
import { fuzzyFilter, parseKey, truncateToWidth, visibleWidth } from "./tui.js";
|
|
8
8
|
import { RESET, sgr } from "./theme.js";
|
|
9
9
|
export const PICKER_ICONS = {
|
|
10
10
|
unicode: { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│", lt: "├", rt: "┤", current: "●", prompt: "❯" },
|
|
@@ -227,7 +227,9 @@ export class ChoicePicker {
|
|
|
227
227
|
scroll = 0;
|
|
228
228
|
items = [];
|
|
229
229
|
loading = true;
|
|
230
|
-
|
|
230
|
+
onBackOut;
|
|
231
|
+
rows;
|
|
232
|
+
constructor(painter, icons, title, items, current, custom, onDone, requestRender, opts = {}) {
|
|
231
233
|
this.painter = painter;
|
|
232
234
|
this.icons = icons;
|
|
233
235
|
this.title = title;
|
|
@@ -235,12 +237,15 @@ export class ChoicePicker {
|
|
|
235
237
|
this.custom = custom;
|
|
236
238
|
this.onDone = onDone;
|
|
237
239
|
this.requestRender = requestRender;
|
|
240
|
+
this.searching = opts.search ?? false;
|
|
241
|
+
this.rows = opts.rows ?? VISIBLE;
|
|
242
|
+
this.onBackOut = opts.onBackOut;
|
|
238
243
|
Promise.resolve(items)
|
|
239
244
|
.then((list) => {
|
|
240
245
|
this.items = list;
|
|
241
246
|
this.loading = false;
|
|
242
247
|
this.index = Math.max(0, list.findIndex((c) => c.value === current));
|
|
243
|
-
this.scroll = Math.max(0, this.index -
|
|
248
|
+
this.scroll = Math.max(0, this.index - this.rows + 1);
|
|
244
249
|
this.requestRender();
|
|
245
250
|
})
|
|
246
251
|
.catch(() => {
|
|
@@ -276,6 +281,8 @@ export class ChoicePicker {
|
|
|
276
281
|
else if (key === "backspace") {
|
|
277
282
|
if (this.query)
|
|
278
283
|
this.query = this.query.slice(0, -1);
|
|
284
|
+
else if (this.onBackOut)
|
|
285
|
+
return this.onBackOut();
|
|
279
286
|
else
|
|
280
287
|
this.searching = false;
|
|
281
288
|
this.index = 0;
|
|
@@ -321,8 +328,8 @@ export class ChoicePicker {
|
|
|
321
328
|
}
|
|
322
329
|
if (this.index < this.scroll)
|
|
323
330
|
this.scroll = this.index;
|
|
324
|
-
if (this.index >= this.scroll +
|
|
325
|
-
this.scroll = this.index -
|
|
331
|
+
if (this.index >= this.scroll + this.rows)
|
|
332
|
+
this.scroll = this.index - this.rows + 1;
|
|
326
333
|
this.requestRender();
|
|
327
334
|
}
|
|
328
335
|
invalidate() { }
|
|
@@ -334,7 +341,7 @@ export class ChoicePicker {
|
|
|
334
341
|
const border = (s) => P.on("FloatBorder", bg) + s + RESET;
|
|
335
342
|
const row = (content, used, rowBg = bg) => border(I.v) + content + P.on("NormalFloat", rowBg) + " ".repeat(Math.max(0, inner - used)) + RESET + border(I.v);
|
|
336
343
|
const items = this.filtered();
|
|
337
|
-
const title = ` ${this.title}
|
|
344
|
+
const title = truncateToWidth(` ${this.title} `, Math.max(0, inner - 2), "…");
|
|
338
345
|
const lines = [border(I.tl + I.h) + P.on("FloatTitle", bg) + title + RESET + border(I.h.repeat(Math.max(0, inner - 1 - visibleWidth(title))) + I.tr)];
|
|
339
346
|
if (this.searching) {
|
|
340
347
|
lines.push(row(P.on("Title", bg) + ` ${I.prompt} ` + RESET + P.on("NormalFloat", bg) + this.query + P.on("PmenuSel", undefined) + " " + RESET, visibleWidth(this.query) + 4));
|
|
@@ -345,15 +352,15 @@ export class ChoicePicker {
|
|
|
345
352
|
lines.push(row(P.on("Comment", bg) + ` ${I.prompt} ${text}` + RESET, visibleWidth(text) + 3));
|
|
346
353
|
}
|
|
347
354
|
lines.push(border(I.lt + I.h.repeat(inner) + I.rt));
|
|
348
|
-
const shown = items.slice(this.scroll, this.scroll +
|
|
349
|
-
for (let i = 0; i <
|
|
355
|
+
const shown = items.slice(this.scroll, this.scroll + this.rows);
|
|
356
|
+
for (let i = 0; i < this.rows; i++) {
|
|
350
357
|
const c = shown[i];
|
|
351
358
|
if (!c) {
|
|
352
359
|
let text = "";
|
|
353
360
|
if (i === 0 && this.loading)
|
|
354
361
|
text = " loading…";
|
|
355
362
|
else if (i === 0 && items.length === 0)
|
|
356
|
-
text = this.custom && this.query.trim() ? ` Enter uses '${this.query.trim()}'` : " no match";
|
|
363
|
+
text = this.custom && this.query.trim() ? ` Enter uses '${this.query.trim()}'` : this.custom && this.items.length === 0 ? " type it, then Enter" : " no match";
|
|
357
364
|
lines.push(row(P.on("Comment", bg) + truncateToWidth(text, inner, "…") + RESET, Math.min(inner, visibleWidth(text))));
|
|
358
365
|
continue;
|
|
359
366
|
}
|
|
@@ -361,9 +368,12 @@ export class ChoicePicker {
|
|
|
361
368
|
const rowBg = selected ? (P.attr("PmenuSel").bg ?? P.attr("Visual").bg) : bg;
|
|
362
369
|
const group = selected ? "PmenuSel" : "NormalFloat";
|
|
363
370
|
const marker = c.value === this.current ? `${I.current} ` : " ";
|
|
364
|
-
|
|
371
|
+
// notes line up in a column after the longest name (up to a third of the width)
|
|
372
|
+
const nameW = Math.min(Math.floor(inner / 3), Math.max(...items.map((x) => visibleWidth(x.value))));
|
|
373
|
+
const padded = c.note ? c.value + " ".repeat(Math.max(0, nameW - visibleWidth(c.value))) : c.value;
|
|
374
|
+
const name = truncateToWidth(`${marker}${padded}`, Math.max(10, inner - 4), "…");
|
|
365
375
|
const note = c.note ? truncateToWidth(` ${c.note}`, Math.max(0, inner - 2 - visibleWidth(name)), "…") : "";
|
|
366
|
-
lines.push(row(P.on(group, rowBg) + ` ${name}` + P.on("Comment", rowBg) + note + RESET, 1 + visibleWidth(name) + visibleWidth(note), rowBg));
|
|
376
|
+
lines.push(row(P.on(group, rowBg) + ` ${name}` + P.on(selected ? "PmenuSel" : "Comment", rowBg) + note + RESET, 1 + visibleWidth(name) + visibleWidth(note), rowBg));
|
|
367
377
|
}
|
|
368
378
|
const keys = this.searching ? "type to filter · Esc done" : "j/k move · Enter pick · q close";
|
|
369
379
|
const count = ` ${items.length ? this.index + 1 : 0}/${items.length} · ${keys} `;
|