@vigent/cli 0.2.2 → 0.2.3
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 +2 -2
- package/dist/app.js +45 -2
- package/dist/keys.js +1 -0
- package/dist/picker.js +71 -36
- package/dist/session.js +25 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ pi runs inside vigent. Claude Code, Codex and OpenCode are the CLIs you may alre
|
|
|
82
82
|
:checkhealth " installed? signed in? how to connect the rest
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
The sidebar's ENGINES block shows each one: `●` signed in, `◐` installed but not signed in (with the command to sign in, like `codex login`), `○` not installed. Without `--engine`, new agents use `engine` from the config. Its default, `"auto"`, picks pi if it has an API key, otherwise the first signed-in CLI. A CLI agent survives a restart too: vigent saves the CLI's conversation id and continues that conversation. Its earlier messages stay in the CLI's own history.
|
|
85
|
+
The sidebar's ENGINES block shows each one: `●` signed in, `◐` installed but not signed in (with the command to sign in, like `codex login`), `○` not installed. Without `--engine`, new agents use `engine` from the config. Its default, `"auto"`, picks pi if it has an API key, otherwise the first signed-in CLI. **`:engine`** (`<Space>E`, or `E` on the dashboard) switches it while vigent runs: a menu lists auto, pi, Claude Code, Codex and OpenCode with which are ready, and `:engine claude` sets it directly. Agents already open keep their engine, since their conversation lives there; `:new <task>` starts one on the new engine. A CLI agent survives a restart too: vigent saves the CLI's conversation id and continues that conversation. Its earlier messages stay in the CLI's own history.
|
|
86
86
|
|
|
87
87
|

|
|
88
88
|
|
|
@@ -206,7 +206,7 @@ vigent --theme nvim # follow your Neovim: the colorscheme your c
|
|
|
206
206
|
|
|
207
207
|
**Follow your editor:** with `theme = "nvim"` (or `"vim"`) in config.toml, vigent uses whatever colorscheme your Neovim config and plugins set, whether that's AstroNvim's `astrodark`, LazyVim's `tokyonight` or anything else. Change it in Neovim, and vigent picks it up the next time it starts. vigent keeps a copy (`themes/nvim.json`) so it opens instantly, and checks Neovim in the background.
|
|
208
208
|
|
|
209
|
-
Inside vigent, `:colorscheme` opens a picker with live preview. `:hi Group guifg=… guibg=…` changes any color on the spot, `:set transparent` lets your terminal's background (its blur, opacity or wallpaper) show through any theme, and `:theme save mine` keeps what's on screen as your own theme file. To keep settings, put them in `~/.config/vigent/config.toml` (`vigent config init` writes a starter file):
|
|
209
|
+
Inside vigent, `:colorscheme` (`<Space>t`) opens a picker with live preview: `j`/`k` move and preview, Enter keeps it, `q` or Esc puts the old one back, and `/` searches. `:hi Group guifg=… guibg=…` changes any color on the spot, `:set transparent` lets your terminal's background (its blur, opacity or wallpaper) show through any theme, and `:theme save mine` keeps what's on screen as your own theme file. To keep settings, put them in `~/.config/vigent/config.toml` (`vigent config init` writes a starter file):
|
|
210
210
|
|
|
211
211
|
```toml
|
|
212
212
|
theme = "tokyonight"
|
package/dist/app.js
CHANGED
|
@@ -17,7 +17,7 @@ import * as fs from "node:fs";
|
|
|
17
17
|
import { setDetached } from "./agents.js";
|
|
18
18
|
import { VERSION_TEXT } from "./version.js";
|
|
19
19
|
import { backendName, formatBytes, riftriBackends, RIFTRI_INSTALL } from "./riftri.js";
|
|
20
|
-
import { ENGINE_NAMES, isEngineId } from "./engines.js";
|
|
20
|
+
import { defaultEngine as defaultEngineFor, ENGINE_NAMES, isEngineId } from "./engines.js";
|
|
21
21
|
import { copyToClipboard } from "./clipboard.js";
|
|
22
22
|
import { splitView, unifiedView } from "./diffview.js";
|
|
23
23
|
import { TREE_GLYPHS } from "./tree.js";
|
|
@@ -756,6 +756,13 @@ export class App {
|
|
|
756
756
|
else
|
|
757
757
|
this.openModelPicker();
|
|
758
758
|
return;
|
|
759
|
+
case "engine":
|
|
760
|
+
case "eng":
|
|
761
|
+
if (arg)
|
|
762
|
+
this.chooseEngine(arg);
|
|
763
|
+
else
|
|
764
|
+
this.openEnginePicker();
|
|
765
|
+
return;
|
|
759
766
|
case "effort":
|
|
760
767
|
case "think":
|
|
761
768
|
if (arg)
|
|
@@ -996,6 +1003,37 @@ export class App {
|
|
|
996
1003
|
this.tui.requestRender();
|
|
997
1004
|
});
|
|
998
1005
|
}
|
|
1006
|
+
/** `:engine`: which engine new agents run on, with what's ready on this machine. */
|
|
1007
|
+
openEnginePicker() {
|
|
1008
|
+
const ids = ["auto", "pi", "claude", "codex", "opencode"];
|
|
1009
|
+
const items = ids.map((id) => ({ value: id, note: this.engineNote(id) }));
|
|
1010
|
+
this.openChoices("Engine for new agents", items, this.preferredEngine, false, (v) => this.chooseEngine(v));
|
|
1011
|
+
}
|
|
1012
|
+
/** "Claude Code · ready", "Codex · not signed in: codex login", "auto: … (now Claude Code)" */
|
|
1013
|
+
engineNote(id) {
|
|
1014
|
+
if (id === "auto")
|
|
1015
|
+
return `pick for me (now ${ENGINE_NAMES[defaultEngineFor(this.engines)]})`;
|
|
1016
|
+
const e = this.engines?.find((x) => x.id === id);
|
|
1017
|
+
if (!e)
|
|
1018
|
+
return ENGINE_NAMES[id];
|
|
1019
|
+
const status = e.connected ? "ready" : !e.installed ? "not installed" : e.connect ? `not signed in: ${e.connect}` : "not signed in";
|
|
1020
|
+
return `${ENGINE_NAMES[id]} · ${status}`;
|
|
1021
|
+
}
|
|
1022
|
+
/** `:engine <name>`: new agents run on it. Agents already open keep theirs (their conversation lives there). */
|
|
1023
|
+
chooseEngine(value) {
|
|
1024
|
+
if (value !== "auto" && !isEngineId(value)) {
|
|
1025
|
+
this.setMessage("engines: auto, pi, claude, codex, opencode", "ErrorMsg");
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
this.preferredEngine = value;
|
|
1029
|
+
const now = this.defaultEngine();
|
|
1030
|
+
const e = this.engines?.find((x) => x.id === now);
|
|
1031
|
+
const warn = e && !e.connected ? ` (${e.installed ? `not signed in${e.connect ? `: run ${e.connect}` : ""}` : "not installed"})` : "";
|
|
1032
|
+
const s = this.focusedSession;
|
|
1033
|
+
const open = s && s.engine !== now ? ` · '${s.name}' stays on ${ENGINE_NAMES[s.engine]}; :new <task> starts one on ${ENGINE_NAMES[now]}` : "";
|
|
1034
|
+
this.setMessage(`new agents run on ${ENGINE_NAMES[now]}${value === "auto" ? " (auto)" : ""}${warn}${open} · :settings, w keeps it`, warn ? "WarningMsg" : "ModeMsg");
|
|
1035
|
+
this.repaint();
|
|
1036
|
+
}
|
|
999
1037
|
/** `:model`: pick from what the engine offers, or type any name. */
|
|
1000
1038
|
openModelPicker(t = this.modelTarget()) {
|
|
1001
1039
|
const current = t.session
|
|
@@ -1089,7 +1127,7 @@ export class App {
|
|
|
1089
1127
|
change: (d) => {
|
|
1090
1128
|
this.preferredEngine = cycle(["auto", "pi", "claude", "codex", "opencode"], this.preferredEngine, d);
|
|
1091
1129
|
},
|
|
1092
|
-
hint: "auto: pi with an API key, else the first signed-in CLI · :
|
|
1130
|
+
hint: "auto: pi with an API key, else the first signed-in CLI · :engine picks it from anywhere",
|
|
1093
1131
|
},
|
|
1094
1132
|
{
|
|
1095
1133
|
section: "Agents",
|
|
@@ -1974,6 +2012,9 @@ export class App {
|
|
|
1974
2012
|
case "e":
|
|
1975
2013
|
this.openEffortPicker();
|
|
1976
2014
|
return true;
|
|
2015
|
+
case "E":
|
|
2016
|
+
this.openEnginePicker();
|
|
2017
|
+
return true;
|
|
1977
2018
|
case "c":
|
|
1978
2019
|
this.openHealth();
|
|
1979
2020
|
return true;
|
|
@@ -2442,6 +2483,7 @@ const EX_HELP = [
|
|
|
2442
2483
|
[":checkhealth", "which engines are installed and signed in"],
|
|
2443
2484
|
[":model [name]", "pick the agent's model (no agent: for new agents)"],
|
|
2444
2485
|
[":effort [level]", "pick its reasoning effort: low … max (pi: thinking level)"],
|
|
2486
|
+
[":engine [name]", "pick the engine new agents run on: auto, pi, claude, codex, opencode"],
|
|
2445
2487
|
[":settings", "theme, look, engine, worktrees and keys in one place (w saves)"],
|
|
2446
2488
|
[":w [file]", "save the transcript as Markdown"],
|
|
2447
2489
|
[":worktrees", "list agent worktrees"],
|
|
@@ -2520,6 +2562,7 @@ const DASHBOARD = [
|
|
|
2520
2562
|
["s", "⚙", "Settings", NERD.gear],
|
|
2521
2563
|
["m", "◆", "Model", NERD.chip],
|
|
2522
2564
|
["e", "↯", "Effort", NERD.bolt],
|
|
2565
|
+
["E", "⚑", "Engine: pi, Claude Code, Codex, OpenCode", NERD.wrench],
|
|
2523
2566
|
["c", "✓", "Check engines", NERD.check],
|
|
2524
2567
|
["?", "⌘", "Keys", NERD.keyboard],
|
|
2525
2568
|
["q", "×", "Quit", NERD.power],
|
package/dist/keys.js
CHANGED
package/dist/picker.js
CHANGED
|
@@ -12,6 +12,11 @@ export const PICKER_ICONS = {
|
|
|
12
12
|
ascii: { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|", lt: "+", rt: "+", current: "*", prompt: ">" },
|
|
13
13
|
};
|
|
14
14
|
const VISIBLE = 14;
|
|
15
|
+
/**
|
|
16
|
+
* `:colorscheme`, vim-style with live preview: `j`/`k` (↓/↑, Ctrl-n/p, Tab)
|
|
17
|
+
* move and preview, `g`/`G` jump, Enter keeps it, `q`/Esc puts the old one
|
|
18
|
+
* back. `/` starts a search: type to filter, Esc ends it.
|
|
19
|
+
*/
|
|
15
20
|
export class ThemePicker {
|
|
16
21
|
painter;
|
|
17
22
|
themes;
|
|
@@ -22,6 +27,8 @@ export class ThemePicker {
|
|
|
22
27
|
requestRender;
|
|
23
28
|
look;
|
|
24
29
|
query = "";
|
|
30
|
+
/** typing goes to the search (after `/`) */
|
|
31
|
+
searching = false;
|
|
25
32
|
index = 0;
|
|
26
33
|
scroll = 0;
|
|
27
34
|
constructor(painter, themes, current, icons, onPreview, onDone, requestRender, look = () => undefined) {
|
|
@@ -52,42 +59,63 @@ export class ThemePicker {
|
|
|
52
59
|
}
|
|
53
60
|
handleInput(data) {
|
|
54
61
|
const key = parseKey(data) ?? "";
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
if (key === "ctrl+c")
|
|
63
|
+
return this.onDone(null);
|
|
64
|
+
if (key === "enter" || key === "return") {
|
|
65
|
+
this.onDone(this.filtered()[this.index] ?? null);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (["down", "tab", "ctrl+n", "ctrl+j"].includes(key))
|
|
69
|
+
this.move(1);
|
|
70
|
+
else if (["up", "shift+tab", "ctrl+p", "ctrl+k"].includes(key))
|
|
71
|
+
this.move(-1);
|
|
72
|
+
else if (this.searching) {
|
|
73
|
+
if (key === "escape")
|
|
74
|
+
this.searching = false; // the filter stays; j/k move through it
|
|
75
|
+
else if (key === "backspace") {
|
|
76
|
+
if (this.query) {
|
|
77
|
+
this.query = this.query.slice(0, -1);
|
|
78
|
+
this.resetSelection();
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
this.searching = false;
|
|
65
82
|
}
|
|
66
|
-
|
|
67
|
-
case "tab":
|
|
68
|
-
case "ctrl+n":
|
|
69
|
-
case "ctrl+j":
|
|
70
|
-
this.move(1);
|
|
71
|
-
break;
|
|
72
|
-
case "up":
|
|
73
|
-
case "shift+tab":
|
|
74
|
-
case "ctrl+p":
|
|
75
|
-
case "ctrl+k":
|
|
76
|
-
this.move(-1);
|
|
77
|
-
break;
|
|
78
|
-
case "backspace":
|
|
79
|
-
this.query = this.query.slice(0, -1);
|
|
80
|
-
this.resetSelection();
|
|
81
|
-
break;
|
|
82
|
-
case "ctrl+u":
|
|
83
|
+
else if (key === "ctrl+u") {
|
|
83
84
|
this.query = "";
|
|
84
85
|
this.resetSelection();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
}
|
|
87
|
+
else if (data.length >= 1 && !data.startsWith("\x1b") && data >= " ") {
|
|
88
|
+
this.query += data;
|
|
89
|
+
this.resetSelection();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
switch (data) {
|
|
94
|
+
case "j":
|
|
95
|
+
this.move(1);
|
|
96
|
+
break;
|
|
97
|
+
case "k":
|
|
98
|
+
this.move(-1);
|
|
99
|
+
break;
|
|
100
|
+
case "g":
|
|
101
|
+
this.move(-this.index);
|
|
102
|
+
break;
|
|
103
|
+
case "G":
|
|
104
|
+
this.move(this.filtered().length - 1 - this.index);
|
|
105
|
+
break;
|
|
106
|
+
case "/":
|
|
107
|
+
this.searching = true;
|
|
108
|
+
break;
|
|
109
|
+
case "q":
|
|
110
|
+
return this.onDone(null);
|
|
111
|
+
default:
|
|
112
|
+
if (key === "escape") {
|
|
113
|
+
if (!this.query)
|
|
114
|
+
return this.onDone(null);
|
|
115
|
+
this.query = ""; // first Esc drops the filter, the next one closes
|
|
116
|
+
this.resetSelection();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
91
119
|
}
|
|
92
120
|
this.requestRender();
|
|
93
121
|
}
|
|
@@ -120,15 +148,22 @@ export class ThemePicker {
|
|
|
120
148
|
border(I.v);
|
|
121
149
|
const items = this.filtered();
|
|
122
150
|
const title = " Colorschemes ";
|
|
123
|
-
const
|
|
151
|
+
const keys = this.searching ? "type to filter · Esc done" : "j/k move · Enter pick · q close";
|
|
152
|
+
const count = ` ${items.length === 0 ? 0 : this.index + 1}/${items.length} · ${keys} `;
|
|
124
153
|
const top = border(I.tl + I.h) +
|
|
125
154
|
P.on("FloatTitle", bg) +
|
|
126
155
|
title +
|
|
127
156
|
RESET +
|
|
128
157
|
border(I.h.repeat(Math.max(0, inner - 1 - visibleWidth(title))) + I.tr);
|
|
129
158
|
const lines = [top];
|
|
130
|
-
|
|
131
|
-
|
|
159
|
+
if (this.searching) {
|
|
160
|
+
const prompt = `${I.prompt} ${this.query}`;
|
|
161
|
+
lines.push(row(P.on("Title", bg) + ` ${I.prompt} ` + RESET + P.on("NormalFloat", bg) + this.query + P.on("PmenuSel", undefined) + " " + RESET, visibleWidth(prompt) + 2));
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
const hint = truncateToWidth(this.query ? `/${this.query}` : "/ to search", Math.max(0, inner - 4), "…");
|
|
165
|
+
lines.push(row(P.on("Comment", bg) + ` ${I.prompt} ${hint}` + RESET, visibleWidth(hint) + 3));
|
|
166
|
+
}
|
|
132
167
|
lines.push(border(I.lt + I.h.repeat(inner) + I.rt));
|
|
133
168
|
const shown = items.slice(this.scroll, this.scroll + VISIBLE);
|
|
134
169
|
for (let i = 0; i < VISIBLE; i++) {
|
package/dist/session.js
CHANGED
|
@@ -272,13 +272,16 @@ export class PiSession extends Session {
|
|
|
272
272
|
}
|
|
273
273
|
session.subscribe((event) => this.onEvent(event));
|
|
274
274
|
}
|
|
275
|
+
/** the run ended on a failed request (shown as an error, not as silence) */
|
|
276
|
+
failed = false;
|
|
275
277
|
onEvent(event) {
|
|
276
278
|
switch (event.type) {
|
|
277
279
|
case "agent_start":
|
|
280
|
+
this.failed = false;
|
|
278
281
|
this.setState("running");
|
|
279
282
|
break;
|
|
280
283
|
case "agent_end":
|
|
281
|
-
this.setState("idle");
|
|
284
|
+
this.setState(this.failed ? "error" : "idle");
|
|
282
285
|
this.refreshStat(0);
|
|
283
286
|
break;
|
|
284
287
|
case "message_start": {
|
|
@@ -309,6 +312,12 @@ export class PiSession extends Session {
|
|
|
309
312
|
this.items.pop(); // a turn that only called tools
|
|
310
313
|
this.emit();
|
|
311
314
|
}
|
|
315
|
+
// a failed request ends the reply with an error instead of throwing: say so
|
|
316
|
+
const err = piError(event.message);
|
|
317
|
+
if (err) {
|
|
318
|
+
this.failed = true;
|
|
319
|
+
this.pushInfo(err);
|
|
320
|
+
}
|
|
312
321
|
break;
|
|
313
322
|
}
|
|
314
323
|
case "tool_execution_start": {
|
|
@@ -419,6 +428,9 @@ export function itemsFromMessages(messages) {
|
|
|
419
428
|
}
|
|
420
429
|
if (m.usage)
|
|
421
430
|
tokens += (m.usage.input ?? 0) + (m.usage.output ?? 0);
|
|
431
|
+
const err = piError(m);
|
|
432
|
+
if (err)
|
|
433
|
+
items.push({ kind: "info", text: err });
|
|
422
434
|
}
|
|
423
435
|
else if (m?.role === "toolResult") {
|
|
424
436
|
const item = byCallId.get(m.toolCallId);
|
|
@@ -428,6 +440,18 @@ export function itemsFromMessages(messages) {
|
|
|
428
440
|
}
|
|
429
441
|
return { items, tokens };
|
|
430
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* The error of a reply whose request failed (pi ends it with stopReason
|
|
445
|
+
* "error" and an errorMessage), with a hint when the key or login was refused.
|
|
446
|
+
*/
|
|
447
|
+
export function piError(message) {
|
|
448
|
+
if (message?.role !== "assistant" || message.stopReason !== "error")
|
|
449
|
+
return undefined;
|
|
450
|
+
const text = String(message.errorMessage ?? "").trim() || "the request failed";
|
|
451
|
+
const auth = /\b(401|403)\b|auth|api[ _-]?key|unauthori[sz]ed|forbidden|credential|expired|login/i.test(text);
|
|
452
|
+
const hint = auth ? " · pi's API key or login was refused; :checkhealth shows the engines, and :new <task> --engine claude starts one on Claude Code" : "";
|
|
453
|
+
return `error: ${text}${hint}`;
|
|
454
|
+
}
|
|
431
455
|
function extractText(content) {
|
|
432
456
|
if (typeof content === "string")
|
|
433
457
|
return content;
|