@vigent/cli 0.2.1 → 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 +9 -4
- package/dist/app.js +102 -20
- package/dist/choices.js +67 -3
- package/dist/cli.js +13 -2
- package/dist/config.js +7 -4
- package/dist/diffview.js +6 -3
- package/dist/follow.js +75 -0
- package/dist/icons.js +104 -0
- package/dist/import.js +2 -1
- package/dist/keys.js +1 -0
- package/dist/picker.js +148 -76
- package/dist/session.js +25 -1
- package/dist/tree.js +1 -0
- 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
|
|
|
@@ -94,7 +94,7 @@ The sidebar's ENGINES block shows each one: `●` signed in, `◐` installed but
|
|
|
94
94
|
| `codex` | `codex exec --json` (`exec resume` to continue) | `-c sandbox_mode="workspace-write"` |
|
|
95
95
|
| `opencode` | `opencode run --format json --dir <worktree>` | none |
|
|
96
96
|
|
|
97
|
-
**Model and effort:** `:model` (`<Space>M`) opens a picker with the models the agent's engine offers,
|
|
97
|
+
**Model and effort:** `:model` (`<Space>M`) opens a picker with the models the agent's engine offers (Claude Code's model names, Codex's own catalog from `codex debug models`, OpenCode's `opencode models`, or pi's for the providers you have a key for). In the picker, `j`/`k` move, `g`/`G` jump to the top or bottom, Enter picks and `q` or Esc closes. Press `/` to search, or to type any model name. `:effort` (`<Space>e`) sets the reasoning effort (pi: thinking level). Both apply from the next prompt, show in the winbar and survive a restart. With no agent open, they set the defaults for new agents.
|
|
98
98
|
|
|
99
99
|

|
|
100
100
|
|
|
@@ -201,16 +201,19 @@ vigent theme import kanagawa # any colorscheme your editor can load (plug
|
|
|
201
201
|
vigent theme import ./molokai.vim # a .vim file, no editor needed
|
|
202
202
|
vigent theme new mine --from nord # start your own theme, pre-filled with nord's colors
|
|
203
203
|
vigent --theme lazyvim # LazyVim's look, on your terminal's own (transparent) background
|
|
204
|
+
vigent --theme nvim # follow your Neovim: the colorscheme your config sets
|
|
204
205
|
```
|
|
205
206
|
|
|
206
|
-
|
|
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
|
+
|
|
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):
|
|
207
210
|
|
|
208
211
|
```toml
|
|
209
212
|
theme = "tokyonight"
|
|
210
213
|
|
|
211
214
|
[ui]
|
|
212
215
|
sidebar_width = 34
|
|
213
|
-
icons = "unicode" # or "ascii"
|
|
216
|
+
icons = "unicode" # or "nerd" (Nerd Font icons), "ascii"
|
|
214
217
|
transparent = true # the terminal's background, with any theme
|
|
215
218
|
|
|
216
219
|
[highlights] # applied on top of any theme
|
|
@@ -218,6 +221,8 @@ Normal = { bg = "NONE" }
|
|
|
218
221
|
CursorLine = "guibg=#2a2f3a"
|
|
219
222
|
```
|
|
220
223
|
|
|
224
|
+
**Icons and fonts:** `icons = "nerd"` (or `:settings`, Icons) uses Nerd Font icons like AstroNvim and LazyVim: a git branch icon, status and engine icons, an icon per tool call (terminal, pencil, file, search…) and file-type icons in the diff view, colored from your theme. vigent draws in your terminal's font, the same as Neovim in a terminal, so for these icons set your terminal's font to a [Nerd Font](https://www.nerdfonts.com) (for example JetBrainsMono Nerd Font).
|
|
225
|
+
|
|
221
226
|
25 built in, including `lazyvim` (LazyVim's look, transparent), the tokyonight, catppuccin and rose-pine families, `dracula`, `onedark`, `everforest`, `nightfox`, `gruvbox`, `kanagawa`, `nord`, `monokai-pro`, `ayu-mirage`, and GitHub and solarized in dark and light. The picker shows each one's colors:
|
|
222
227
|
|
|
223
228
|

|
package/dist/app.js
CHANGED
|
@@ -17,14 +17,15 @@ 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";
|
|
24
24
|
import { transcriptMarkdown } from "./export.js";
|
|
25
25
|
import { renderMarkdown } from "./markdown.js";
|
|
26
26
|
import { ChoicePicker, PICKER_ICONS, ThemePicker } from "./picker.js";
|
|
27
|
-
import { EFFORTS,
|
|
27
|
+
import { EFFORTS, effortChoicesFor, modelChoices } from "./choices.js";
|
|
28
|
+
import { ICON_SETS, NERD, toolIcon } from "./icons.js";
|
|
28
29
|
import { mergeWorktree, openPullRequest } from "./review.js";
|
|
29
30
|
import { BUILTIN_THEMES, extractPalette, listThemes, loadTheme, RESET, sgr, themeFileFor, themesDir } from "./theme.js";
|
|
30
31
|
import { parseHighlightCommand } from "./vimcolors.js";
|
|
@@ -58,6 +59,27 @@ const ICONS = {
|
|
|
58
59
|
warn: "◐",
|
|
59
60
|
off: "○",
|
|
60
61
|
},
|
|
62
|
+
nerd: {
|
|
63
|
+
spinner: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
64
|
+
state: { waiting: NERD.hourglass, idle: NERD.circleO, paused: NERD.pause, error: NERD.times },
|
|
65
|
+
tee: "├╴",
|
|
66
|
+
last: "└╴",
|
|
67
|
+
pipe: "│ ",
|
|
68
|
+
sep: "│",
|
|
69
|
+
worktree: `${NERD.branch} `,
|
|
70
|
+
prompt: "❯ ",
|
|
71
|
+
folded: NERD.caretRight,
|
|
72
|
+
open: NERD.caretDown,
|
|
73
|
+
cursor: "▍",
|
|
74
|
+
minus: "−",
|
|
75
|
+
bullet: "•",
|
|
76
|
+
queued: NERD.clock,
|
|
77
|
+
open_agent: "▸",
|
|
78
|
+
here: NERD.circle,
|
|
79
|
+
ok: NERD.check,
|
|
80
|
+
warn: NERD.warning,
|
|
81
|
+
off: NERD.circleO,
|
|
82
|
+
},
|
|
61
83
|
ascii: {
|
|
62
84
|
spinner: ["|", "/", "-", "\\"],
|
|
63
85
|
state: { waiting: "~", idle: "o", paused: "=", error: "x" },
|
|
@@ -734,6 +756,13 @@ export class App {
|
|
|
734
756
|
else
|
|
735
757
|
this.openModelPicker();
|
|
736
758
|
return;
|
|
759
|
+
case "engine":
|
|
760
|
+
case "eng":
|
|
761
|
+
if (arg)
|
|
762
|
+
this.chooseEngine(arg);
|
|
763
|
+
else
|
|
764
|
+
this.openEnginePicker();
|
|
765
|
+
return;
|
|
737
766
|
case "effort":
|
|
738
767
|
case "think":
|
|
739
768
|
if (arg)
|
|
@@ -948,7 +977,8 @@ export class App {
|
|
|
948
977
|
/** `:effort <level>` */
|
|
949
978
|
chooseEffort(level, t = this.modelTarget()) {
|
|
950
979
|
const allowed = ["default", ...EFFORTS[t.engine]];
|
|
951
|
-
if
|
|
980
|
+
// Codex's levels differ per model and version; Codex itself says if one doesn't fit
|
|
981
|
+
if (!allowed.includes(level) && !(t.engine === "codex" && /^[a-z]+$/.test(level))) {
|
|
952
982
|
this.setMessage(`${ENGINE_NAMES[t.engine]} takes: ${allowed.join(", ")}`, "ErrorMsg");
|
|
953
983
|
return;
|
|
954
984
|
}
|
|
@@ -973,6 +1003,37 @@ export class App {
|
|
|
973
1003
|
this.tui.requestRender();
|
|
974
1004
|
});
|
|
975
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
|
+
}
|
|
976
1037
|
/** `:model`: pick from what the engine offers, or type any name. */
|
|
977
1038
|
openModelPicker(t = this.modelTarget()) {
|
|
978
1039
|
const current = t.session
|
|
@@ -985,8 +1046,16 @@ export class App {
|
|
|
985
1046
|
/** `:effort`: the reasoning effort / thinking levels the engine accepts. */
|
|
986
1047
|
openEffortPicker(t = this.modelTarget()) {
|
|
987
1048
|
const current = (t.session ? t.session.effort : this.newAgentEffort[t.engine]) ?? "default";
|
|
988
|
-
this.openChoices(`Effort · ${t.label}`, effortChoices(t.engine), current, false, (v) => this.chooseEffort(v, t));
|
|
1049
|
+
this.openChoices(`Effort · ${t.label}`, this.effortChoices(t.engine, this.modelOf(t)), current, false, (v) => this.chooseEffort(v, t));
|
|
1050
|
+
}
|
|
1051
|
+
/** the model an agent (or new agents) asked for, if any */
|
|
1052
|
+
modelOf(t) {
|
|
1053
|
+
if (!t.session)
|
|
1054
|
+
return this.newAgentModel[t.engine];
|
|
1055
|
+
return "chosenModel" in t.session ? t.session.chosenModel : t.session.model;
|
|
989
1056
|
}
|
|
1057
|
+
/** where effort levels come from (replaced in tests) */
|
|
1058
|
+
effortChoices = (engine, model) => effortChoicesFor(engine, model);
|
|
990
1059
|
/** where model lists come from (replaced in tests) */
|
|
991
1060
|
modelChoices = (engine) => modelChoices(engine);
|
|
992
1061
|
openChoices(title, items, current, custom, done) {
|
|
@@ -1033,7 +1102,13 @@ export class App {
|
|
|
1033
1102
|
},
|
|
1034
1103
|
hint: "the terminal's own background shows through (its blur, opacity or wallpaper)",
|
|
1035
1104
|
},
|
|
1036
|
-
{
|
|
1105
|
+
{
|
|
1106
|
+
section: "Look",
|
|
1107
|
+
label: "Icons",
|
|
1108
|
+
value: () => ui.icons,
|
|
1109
|
+
change: () => ((ui.icons = ICON_SETS[(ICON_SETS.indexOf(ui.icons) + 1) % ICON_SETS.length]), refresh()),
|
|
1110
|
+
hint: "nerd: Nerd Font icons (needs a Nerd Font) · ascii: fonts without box-drawing characters",
|
|
1111
|
+
},
|
|
1037
1112
|
{ section: "Look", label: "24-bit color", value: () => onOff(ui.termguicolors), change: () => ((ui.termguicolors = !ui.termguicolors), refresh()), hint: "off: the nearest of the 256 standard colors (vim's 'termguicolors')" },
|
|
1038
1113
|
{ section: "Layout", label: "Sidebar", value: () => onOff(ui.sidebar), change: () => ((ui.sidebar = !ui.sidebar), refresh()) },
|
|
1039
1114
|
{
|
|
@@ -1052,7 +1127,7 @@ export class App {
|
|
|
1052
1127
|
change: (d) => {
|
|
1053
1128
|
this.preferredEngine = cycle(["auto", "pi", "claude", "codex", "opencode"], this.preferredEngine, d);
|
|
1054
1129
|
},
|
|
1055
|
-
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",
|
|
1056
1131
|
},
|
|
1057
1132
|
{
|
|
1058
1133
|
section: "Agents",
|
|
@@ -1575,7 +1650,7 @@ export class App {
|
|
|
1575
1650
|
}
|
|
1576
1651
|
const width = this.terminal.columns - 4;
|
|
1577
1652
|
const unified = unifiedView(text);
|
|
1578
|
-
const side = splitView(text, width - 3);
|
|
1653
|
+
const side = splitView(text, width - 3, this.ui.icons === "nerd");
|
|
1579
1654
|
const n = unified.jumps.length;
|
|
1580
1655
|
const title = (layout) => `${wt.branch} vs ${wt.base ?? "base"} · ${n} hunk${n === 1 ? "" : "s"} · ]c [c jump · s ${layout}`;
|
|
1581
1656
|
const views = [
|
|
@@ -1937,6 +2012,9 @@ export class App {
|
|
|
1937
2012
|
case "e":
|
|
1938
2013
|
this.openEffortPicker();
|
|
1939
2014
|
return true;
|
|
2015
|
+
case "E":
|
|
2016
|
+
this.openEnginePicker();
|
|
2017
|
+
return true;
|
|
1940
2018
|
case "c":
|
|
1941
2019
|
this.openHealth();
|
|
1942
2020
|
return true;
|
|
@@ -2042,7 +2120,7 @@ export class App {
|
|
|
2042
2120
|
const rows = [];
|
|
2043
2121
|
const add = (text, plain, indent = center(plain)) => rows.push({ text: indent + text, plain: indent + plain });
|
|
2044
2122
|
// LazyVim's dashboard: a big logo, then one key per action
|
|
2045
|
-
const logo = this.ui.icons
|
|
2123
|
+
const logo = this.ui.icons !== "ascii" && paneW >= LOGO[0].length + 2 && paneH >= 22;
|
|
2046
2124
|
if (logo) {
|
|
2047
2125
|
const groups = ["Function", "Function", "Special", "Special", "Keyword", "Keyword"];
|
|
2048
2126
|
LOGO.forEach((l, i) => add(P.p(groups[i] ?? "Title", l), l));
|
|
@@ -2054,7 +2132,7 @@ export class App {
|
|
|
2054
2132
|
const tagline = "coding agents, the vim way · each one in its own git worktree";
|
|
2055
2133
|
add(P.p("Comment", tagline), tagline);
|
|
2056
2134
|
add("", "");
|
|
2057
|
-
const actions = DASHBOARD.map(([key, icon, label]) => [key, this.ui.icons === "unicode" ? icon : "", label]);
|
|
2135
|
+
const actions = DASHBOARD.map(([key, icon, label, nerd]) => [key, this.ui.icons === "nerd" ? nerd : this.ui.icons === "unicode" ? icon : "", label]);
|
|
2058
2136
|
const labelW = Math.max(...actions.map(([, , l]) => l.length));
|
|
2059
2137
|
const blockW = 2 + labelW + 6;
|
|
2060
2138
|
const indent = center("x".repeat(blockW));
|
|
@@ -2075,7 +2153,7 @@ export class App {
|
|
|
2075
2153
|
});
|
|
2076
2154
|
add(text, plain, indentE);
|
|
2077
2155
|
const ready = this.engines.filter((e) => e.connected).length;
|
|
2078
|
-
const foot = `${this.ui.icons === "unicode" ? "⚡ " : ""}vigent ${VERSION_TEXT} · ${listThemes().length} themes · ${ready} of ${this.engines.length} engines ready`;
|
|
2156
|
+
const foot = `${this.ui.icons === "nerd" ? `${NERD.bolt} ` : this.ui.icons === "unicode" ? "⚡ " : ""}vigent ${VERSION_TEXT} · ${listThemes().length} themes · ${ready} of ${this.engines.length} engines ready`;
|
|
2079
2157
|
add(P.p("Comment", foot), foot);
|
|
2080
2158
|
}
|
|
2081
2159
|
if (!this.workspace.root) {
|
|
@@ -2245,7 +2323,8 @@ export class App {
|
|
|
2245
2323
|
case "tool": {
|
|
2246
2324
|
const t = item.tool;
|
|
2247
2325
|
const fold = t.folded ? I.folded : I.open;
|
|
2248
|
-
|
|
2326
|
+
const icon = this.ui.icons === "nerd" ? `${toolIcon(t.name)} ` : "";
|
|
2327
|
+
push(P.p("NonText", ` ${fold} `) + P.p("Special", icon) + P.p("VigentTool", t.name) + P.p("Comment", ` ${t.summary}`), ` ${fold} ${icon}${t.name} ${t.summary}`, { item: n });
|
|
2249
2328
|
if (!t.folded && t.output) {
|
|
2250
2329
|
const block = nextBlock++;
|
|
2251
2330
|
const outLines = t.output.split("\n");
|
|
@@ -2404,6 +2483,7 @@ const EX_HELP = [
|
|
|
2404
2483
|
[":checkhealth", "which engines are installed and signed in"],
|
|
2405
2484
|
[":model [name]", "pick the agent's model (no agent: for new agents)"],
|
|
2406
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"],
|
|
2407
2487
|
[":settings", "theme, look, engine, worktrees and keys in one place (w saves)"],
|
|
2408
2488
|
[":w [file]", "save the transcript as Markdown"],
|
|
2409
2489
|
[":worktrees", "list agent worktrees"],
|
|
@@ -2474,14 +2554,16 @@ const LOGO = [
|
|
|
2474
2554
|
" ╚═══╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ",
|
|
2475
2555
|
];
|
|
2476
2556
|
/** The dashboard's keys (active while no agent is open): key, icon, label. */
|
|
2557
|
+
/** key, unicode icon, label, Nerd Font icon */
|
|
2477
2558
|
const DASHBOARD = [
|
|
2478
|
-
["i", "✎", "New agent: type a task, Enter"],
|
|
2479
|
-
["n", "+", "New agent by name"],
|
|
2480
|
-
["t", "◐", "Theme"],
|
|
2481
|
-
["s", "⚙", "Settings"],
|
|
2482
|
-
["m", "◆", "Model"],
|
|
2483
|
-
["e", "↯", "Effort"],
|
|
2484
|
-
["
|
|
2485
|
-
["
|
|
2486
|
-
["
|
|
2559
|
+
["i", "✎", "New agent: type a task, Enter", NERD.pencil],
|
|
2560
|
+
["n", "+", "New agent by name", NERD.plus],
|
|
2561
|
+
["t", "◐", "Theme", NERD.palette],
|
|
2562
|
+
["s", "⚙", "Settings", NERD.gear],
|
|
2563
|
+
["m", "◆", "Model", NERD.chip],
|
|
2564
|
+
["e", "↯", "Effort", NERD.bolt],
|
|
2565
|
+
["E", "⚑", "Engine: pi, Claude Code, Codex, OpenCode", NERD.wrench],
|
|
2566
|
+
["c", "✓", "Check engines", NERD.check],
|
|
2567
|
+
["?", "⌘", "Keys", NERD.keyboard],
|
|
2568
|
+
["q", "×", "Quit", NERD.power],
|
|
2487
2569
|
];
|
package/dist/choices.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What `:model` and `:effort` offer, per engine. Model lists come from the
|
|
3
|
-
* engine itself where it can say (pi-ai's catalog, `
|
|
3
|
+
* engine itself where it can say (pi-ai's catalog, `codex debug models`,
|
|
4
|
+
* `opencode models`); any
|
|
4
5
|
* name can still be typed in the picker.
|
|
5
6
|
*/
|
|
6
7
|
import { execFile } from "node:child_process";
|
|
8
|
+
import { ENGINE_BINS } from "./engines.js";
|
|
7
9
|
/** Reasoning effort / thinking levels each engine accepts. */
|
|
8
10
|
export const EFFORTS = {
|
|
9
11
|
pi: ["off", "minimal", "low", "medium", "high", "xhigh"],
|
|
10
12
|
claude: ["low", "medium", "high", "xhigh", "max"],
|
|
11
|
-
|
|
13
|
+
// what Codex's models take between them; each model's own levels come from `codex debug models`
|
|
14
|
+
codex: ["minimal", "low", "medium", "high", "xhigh", "max", "ultra"],
|
|
12
15
|
opencode: ["minimal", "low", "medium", "high", "max"],
|
|
13
16
|
};
|
|
14
17
|
export function effortChoices(engine) {
|
|
@@ -36,7 +39,10 @@ export async function modelChoices(engine, env = process.env) {
|
|
|
36
39
|
{ value: "haiku", note: "latest Haiku" },
|
|
37
40
|
];
|
|
38
41
|
case "codex":
|
|
39
|
-
return [
|
|
42
|
+
return [
|
|
43
|
+
{ value: "default", note: "Codex's own choice" },
|
|
44
|
+
...(await codexModels(env)).map((m) => ({ value: m.slug, note: m.description ? `${m.name} · ${m.description}` : m.name })),
|
|
45
|
+
];
|
|
40
46
|
case "opencode":
|
|
41
47
|
return [{ value: "default", note: "OpenCode's own choice" }, ...(await opencodeModels(env)).map((value) => ({ value }))];
|
|
42
48
|
case "pi": {
|
|
@@ -71,3 +77,61 @@ function opencodeModels(env) {
|
|
|
71
77
|
});
|
|
72
78
|
});
|
|
73
79
|
}
|
|
80
|
+
let codexCatalog;
|
|
81
|
+
/**
|
|
82
|
+
* The models your Codex offers, from `codex debug models` (its own catalog,
|
|
83
|
+
* so it follows your Codex version and account). Asked once per run; empty
|
|
84
|
+
* when Codex isn't installed or can't say.
|
|
85
|
+
*/
|
|
86
|
+
export function codexModels(env = process.env) {
|
|
87
|
+
if (env !== process.env)
|
|
88
|
+
return readCodexModels(env);
|
|
89
|
+
codexCatalog ??= readCodexModels(env).then((list) => {
|
|
90
|
+
if (!list.length)
|
|
91
|
+
codexCatalog = undefined; // try again next time
|
|
92
|
+
return list;
|
|
93
|
+
});
|
|
94
|
+
return codexCatalog;
|
|
95
|
+
}
|
|
96
|
+
function readCodexModels(env) {
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
execFile(ENGINE_BINS.codex, ["debug", "models"], { env, timeout: 20000, maxBuffer: 16 * 1024 * 1024 }, (err, stdout) => {
|
|
99
|
+
if (err)
|
|
100
|
+
return resolve([]);
|
|
101
|
+
resolve(parseCodexModels(String(stdout)));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** The models `codex debug models` lists for people to pick (not the hidden ones). */
|
|
106
|
+
export function parseCodexModels(json) {
|
|
107
|
+
try {
|
|
108
|
+
const models = JSON.parse(json)?.models ?? [];
|
|
109
|
+
return models
|
|
110
|
+
.filter((m) => m && typeof m.slug === "string" && (m.visibility === undefined || m.visibility === "list"))
|
|
111
|
+
.map((m) => ({
|
|
112
|
+
slug: m.slug,
|
|
113
|
+
name: typeof m.display_name === "string" ? m.display_name : m.slug,
|
|
114
|
+
description: typeof m.description === "string" ? m.description : undefined,
|
|
115
|
+
efforts: Array.isArray(m.supported_reasoning_levels)
|
|
116
|
+
? m.supported_reasoning_levels.map((l) => (typeof l === "string" ? l : l?.effort)).filter((e) => typeof e === "string")
|
|
117
|
+
: [],
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `:effort` for an agent: the levels its engine takes. For Codex, the levels
|
|
126
|
+
* of the model it uses (all of Codex's levels when that's its default).
|
|
127
|
+
*/
|
|
128
|
+
export async function effortChoicesFor(engine, model, env = process.env) {
|
|
129
|
+
if (engine !== "codex")
|
|
130
|
+
return effortChoices(engine);
|
|
131
|
+
const catalog = await codexModels(env);
|
|
132
|
+
const own = catalog.find((m) => m.slug === model)?.efforts;
|
|
133
|
+
const levels = own?.length ? own : [...new Set(catalog.flatMap((m) => m.efforts))];
|
|
134
|
+
if (!levels.length)
|
|
135
|
+
return effortChoices(engine);
|
|
136
|
+
return [{ value: "default", note: "what the model uses" }, ...EFFORTS.codex.filter((e) => levels.includes(e)).map((value) => ({ value })), ...levels.filter((e) => !EFFORTS.codex.includes(e)).map((value) => ({ value }))];
|
|
137
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { createDemoSessions } from "./demo.js";
|
|
|
14
14
|
import { buildKeymap, KEY_MODES } from "./keys.js";
|
|
15
15
|
import { formatBytes, prepareViews, riftriBackends, riftriGc, riftriHealth, riftriStateDir } from "./riftri.js";
|
|
16
16
|
import { DemoSession, PiSession } from "./session.js";
|
|
17
|
+
import { followedColorscheme, followTheme, isFollow, refreshFollow } from "./follow.js";
|
|
17
18
|
import { BUILTIN_THEMES, listThemes, loadTheme, Painter, themesDir } from "./theme.js";
|
|
18
19
|
import { themeNewTemplate } from "./theme-new.js";
|
|
19
20
|
import { branchName, createWorktree, findWorktree, listWorktrees, removeWorktree, repoRoot, worktreesDir, } from "./worktree.js";
|
|
@@ -206,7 +207,9 @@ async function main() {
|
|
|
206
207
|
const keys = buildKeymap(config.leader, config.keys);
|
|
207
208
|
warnings.push(...keys.warnings);
|
|
208
209
|
try {
|
|
209
|
-
if (config.theme)
|
|
210
|
+
if (isFollow(config.theme))
|
|
211
|
+
followTheme(config.theme);
|
|
212
|
+
else if (config.theme)
|
|
210
213
|
loadTheme(config.theme);
|
|
211
214
|
}
|
|
212
215
|
catch (err) {
|
|
@@ -259,7 +262,8 @@ async function main() {
|
|
|
259
262
|
const themeName = flag(argv, "--theme") ?? config.theme ?? "default";
|
|
260
263
|
let theme = BUILTIN_THEMES.default;
|
|
261
264
|
try {
|
|
262
|
-
theme =
|
|
265
|
+
// theme = "nvim" / "vim": your editor's colorscheme (kept up to date below)
|
|
266
|
+
theme = isFollow(themeName) ? followTheme(themeName) : loadTheme(themeName);
|
|
263
267
|
}
|
|
264
268
|
catch (err) {
|
|
265
269
|
if (flag(argv, "--theme")) {
|
|
@@ -426,6 +430,13 @@ async function main() {
|
|
|
426
430
|
tui.start();
|
|
427
431
|
if (warnings.length > 0)
|
|
428
432
|
app.notify(warnings.join(" · "));
|
|
433
|
+
if (isFollow(themeName)) {
|
|
434
|
+
const editor = themeName;
|
|
435
|
+
refreshFollow(editor, (t) => {
|
|
436
|
+
app.setTheme(t);
|
|
437
|
+
app.notify(`colorscheme from ${editor}: ${followedColorscheme(editor) ?? editor}`, "Comment");
|
|
438
|
+
});
|
|
439
|
+
}
|
|
429
440
|
tui.requestRender(true);
|
|
430
441
|
return undefined;
|
|
431
442
|
}
|
package/dist/config.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* sidebar_width = 30
|
|
9
9
|
* number = true
|
|
10
10
|
* tilde = true
|
|
11
|
-
* icons = "unicode" # or "ascii"
|
|
11
|
+
* icons = "unicode" # or "nerd" (a Nerd Font), "ascii"
|
|
12
12
|
*
|
|
13
13
|
* [highlights] # applied on top of whichever theme is active
|
|
14
14
|
* Normal = { bg = "NONE" } # transparent background
|
|
@@ -19,6 +19,7 @@ import * as fs from "node:fs";
|
|
|
19
19
|
import * as path from "node:path";
|
|
20
20
|
import { parse as parseToml } from "smol-toml";
|
|
21
21
|
import { EFFORTS } from "./choices.js";
|
|
22
|
+
import { ICON_SETS } from "./icons.js";
|
|
22
23
|
import { isEngineId } from "./engines.js";
|
|
23
24
|
import { WORKTREE_BACKENDS } from "./riftri.js";
|
|
24
25
|
import { KEY_MODES } from "./keys.js";
|
|
@@ -223,8 +224,8 @@ export function setOption(ui, name, value) {
|
|
|
223
224
|
return;
|
|
224
225
|
}
|
|
225
226
|
case "icons":
|
|
226
|
-
if (value
|
|
227
|
-
return `'${name}' should be "unicode" or "ascii"`;
|
|
227
|
+
if (!ICON_SETS.includes(value))
|
|
228
|
+
return `'${name}' should be "unicode", "nerd" or "ascii"`;
|
|
228
229
|
ui.icons = value;
|
|
229
230
|
return;
|
|
230
231
|
case "transparent":
|
|
@@ -316,6 +317,8 @@ export const CONFIG_TEMPLATE = `# vigent config: ~/.config/vigent/config.toml
|
|
|
316
317
|
# Built-in: default, lazyvim (transparent), tokyonight, tokyonight-moon, gruvbox,
|
|
317
318
|
# catppuccin, catppuccin-latte, nord, rose-pine, kanagawa. Or any theme in ~/.config/vigent/themes, e.g. one made
|
|
318
319
|
# with vigent theme import (your nvim/vim colorscheme) or vigent theme new.
|
|
320
|
+
# "nvim" (or "vim") follows your editor: the colorscheme your config and plugins
|
|
321
|
+
# set (AstroNvim, LazyVim, NvChad...), updated each time vigent starts.
|
|
319
322
|
theme = "default"
|
|
320
323
|
|
|
321
324
|
# Model used for new pi agents.
|
|
@@ -347,7 +350,7 @@ sidebar = true
|
|
|
347
350
|
sidebar_width = 30
|
|
348
351
|
number = true # session numbers in the sidebar
|
|
349
352
|
tilde = true # ~ on empty sidebar rows
|
|
350
|
-
icons = "unicode" # "ascii" for fonts without box-drawing characters
|
|
353
|
+
icons = "unicode" # "nerd" for Nerd Font icons (like AstroNvim/LazyVim), "ascii" for fonts without box-drawing characters
|
|
351
354
|
timeoutlen = 1000 # ms to wait for the rest of a multi-key mapping
|
|
352
355
|
# termguicolors = false # for terminals without 24-bit color: use the nearest of 256
|
|
353
356
|
# transparent = true # use the terminal's background with any theme ("auto": the theme decides)
|
package/dist/diffview.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Both return the line indices of hunk headers so ]c / [c can jump.
|
|
9
9
|
*/
|
|
10
10
|
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
11
|
+
import { fileIcon } from "./icons.js";
|
|
11
12
|
function lineGroup(line) {
|
|
12
13
|
if (line.startsWith("+++") || line.startsWith("---"))
|
|
13
14
|
return "Title";
|
|
@@ -38,8 +39,8 @@ function cell(no, text, width, group) {
|
|
|
38
39
|
[group, body + " ".repeat(Math.max(0, width - 5 - visibleWidth(body)))],
|
|
39
40
|
];
|
|
40
41
|
}
|
|
41
|
-
/** Side-by-side layout for a window `width` columns wide (inside its border). */
|
|
42
|
-
export function splitView(text, width) {
|
|
42
|
+
/** Side-by-side layout for a window `width` columns wide (inside its border). With `icons`, file names get their Nerd Font icon. */
|
|
43
|
+
export function splitView(text, width, icons = false) {
|
|
43
44
|
const col = Math.max(12, Math.floor((width - 3) / 2));
|
|
44
45
|
const lines = [];
|
|
45
46
|
const jumps = [];
|
|
@@ -63,7 +64,9 @@ export function splitView(text, width) {
|
|
|
63
64
|
}
|
|
64
65
|
const file = /^\+\+\+ (?:b\/)?(.*)$/.exec(line);
|
|
65
66
|
if (file) {
|
|
66
|
-
|
|
67
|
+
const name = file[1] === "/dev/null" ? "(deleted file)" : file[1];
|
|
68
|
+
const [glyph, group] = fileIcon(name);
|
|
69
|
+
lines.push(icons ? [[group, `${glyph} `], ["Title", name]] : [["Title", name]]);
|
|
67
70
|
continue;
|
|
68
71
|
}
|
|
69
72
|
if (/^(---|index |new file|deleted file|similarity|rename |\\ No newline)/.test(line))
|
package/dist/follow.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `theme = "nvim"` (or `"vim"`): vigent uses the colorscheme your editor
|
|
3
|
+
* uses, with your config and plugins (AstroNvim, LazyVim, NvChad...).
|
|
4
|
+
*
|
|
5
|
+
* The editor's colors are kept as the theme file `nvim.json` (`vim.json`).
|
|
6
|
+
* At start vigent shows that copy at once, and imports again in the
|
|
7
|
+
* background: when you've changed your editor's colorscheme since, the new
|
|
8
|
+
* one replaces it while vigent runs. The first time there's no copy yet, so
|
|
9
|
+
* vigent imports before it opens.
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { importTheme } from "./import.js";
|
|
15
|
+
import { loadTheme, themesDir } from "./theme.js";
|
|
16
|
+
export const FOLLOW_EDITORS = ["nvim", "vim"];
|
|
17
|
+
export function isFollow(theme) {
|
|
18
|
+
return FOLLOW_EDITORS.includes(theme ?? "");
|
|
19
|
+
}
|
|
20
|
+
/** The theme file that holds the editor's colors. */
|
|
21
|
+
export function followFile(editor, dir = themesDir()) {
|
|
22
|
+
return path.join(dir, `${editor}.json`);
|
|
23
|
+
}
|
|
24
|
+
/** The colorscheme the copy came from (e.g. "astrodark"), if it says. */
|
|
25
|
+
export function followedColorscheme(editor, dir = themesDir()) {
|
|
26
|
+
try {
|
|
27
|
+
const c = JSON.parse(fs.readFileSync(followFile(editor, dir), "utf8")).colorscheme;
|
|
28
|
+
return typeof c === "string" && c ? c : undefined;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** The editor's theme: the copy from last time, or (the first time) an import now. Throws if the editor can't say. */
|
|
35
|
+
export function followTheme(editor, dir = themesDir()) {
|
|
36
|
+
const file = followFile(editor, dir);
|
|
37
|
+
if (!fs.existsSync(file))
|
|
38
|
+
importTheme({ editor, as: editor, outDir: dir });
|
|
39
|
+
return loadTheme(file);
|
|
40
|
+
}
|
|
41
|
+
/** The copy's colors in a fixed order (the editor lists its groups in any order). */
|
|
42
|
+
function read(file) {
|
|
43
|
+
try {
|
|
44
|
+
const sorted = (v) => v && typeof v === "object" && !Array.isArray(v)
|
|
45
|
+
? Object.fromEntries(Object.keys(v).sort().map((k) => [k, sorted(v[k])]))
|
|
46
|
+
: v;
|
|
47
|
+
return JSON.stringify(sorted(JSON.parse(fs.readFileSync(file, "utf8"))));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Import the editor's colorscheme again without blocking (in a child
|
|
55
|
+
* `vigent theme import`); `onChange` gets the theme when it differs from the copy.
|
|
56
|
+
*/
|
|
57
|
+
export function refreshFollow(editor, onChange, cli = process.argv[1]) {
|
|
58
|
+
const file = followFile(editor);
|
|
59
|
+
const before = read(file);
|
|
60
|
+
const child = spawn(process.execPath, [cli, "theme", "import", `--${editor}`, "--as", editor], { stdio: "ignore" });
|
|
61
|
+
child.on("error", () => { });
|
|
62
|
+
child.on("exit", (code) => {
|
|
63
|
+
if (code !== 0)
|
|
64
|
+
return;
|
|
65
|
+
const after = read(file);
|
|
66
|
+
if (after === undefined || after === before)
|
|
67
|
+
return;
|
|
68
|
+
try {
|
|
69
|
+
onChange(loadTheme(file));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// a copy that doesn't load: keep what's on screen
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
package/dist/icons.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nerd Font icons (`icons = "nerd"`), the glyphs AstroNvim, LazyVim and
|
|
3
|
+
* nvim-web-devicons draw. They need a Nerd Font in the terminal
|
|
4
|
+
* (https://www.nerdfonts.com); codepoints are Nerd Fonts v3.
|
|
5
|
+
*/
|
|
6
|
+
export const ICON_SETS = ["unicode", "nerd", "ascii"];
|
|
7
|
+
export const NERD = {
|
|
8
|
+
branch: "", // nf-dev-git_branch
|
|
9
|
+
check: "", // nf-fa-check
|
|
10
|
+
times: "", // nf-fa-times
|
|
11
|
+
warning: "", // nf-fa-warning
|
|
12
|
+
circle: "", // nf-fa-circle
|
|
13
|
+
circleO: "", // nf-fa-circle_o
|
|
14
|
+
pause: "", // nf-fa-pause
|
|
15
|
+
clock: "", // nf-fa-clock_o
|
|
16
|
+
hourglass: "", // nf-fa-hourglass_half
|
|
17
|
+
search: "", // nf-fa-search
|
|
18
|
+
pencil: "", // nf-fa-pencil_square_o
|
|
19
|
+
plus: "", // nf-fa-plus
|
|
20
|
+
palette: "", // nf-fa-palette
|
|
21
|
+
gear: "", // nf-fa-gear
|
|
22
|
+
chip: "", // nf-fa-microchip
|
|
23
|
+
bolt: "", // nf-fa-bolt
|
|
24
|
+
keyboard: "", // nf-fa-keyboard_o
|
|
25
|
+
power: "", // nf-fa-power_off
|
|
26
|
+
terminal: "", // nf-oct-terminal
|
|
27
|
+
file: "", // nf-fa-file
|
|
28
|
+
fileText: "", // nf-fa-file_text
|
|
29
|
+
globe: "", // nf-fa-globe
|
|
30
|
+
wrench: "", // nf-fa-wrench
|
|
31
|
+
list: "", // nf-fa-list
|
|
32
|
+
caretRight: "", // nf-fa-caret_right
|
|
33
|
+
caretDown: "", // nf-fa-caret_down
|
|
34
|
+
};
|
|
35
|
+
/** An icon for a tool call, by the tool's name (Claude Code, Codex, OpenCode and pi names). */
|
|
36
|
+
export function toolIcon(name) {
|
|
37
|
+
const n = name.toLowerCase();
|
|
38
|
+
if (/bash|shell|exec|command|run/.test(n))
|
|
39
|
+
return NERD.terminal;
|
|
40
|
+
if (/edit|write|patch|apply/.test(n))
|
|
41
|
+
return NERD.pencil;
|
|
42
|
+
if (/read|view|cat|open/.test(n))
|
|
43
|
+
return NERD.fileText;
|
|
44
|
+
if (/grep|glob|search|find|^ls$|list/.test(n))
|
|
45
|
+
return NERD.search;
|
|
46
|
+
if (/web|fetch|http|url/.test(n))
|
|
47
|
+
return NERD.globe;
|
|
48
|
+
if (/todo|plan|task/.test(n))
|
|
49
|
+
return NERD.list;
|
|
50
|
+
return NERD.wrench;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A file's icon and the highlight group to color it with, by name or
|
|
54
|
+
* extension (like nvim-web-devicons, colored from the theme).
|
|
55
|
+
*/
|
|
56
|
+
export function fileIcon(file) {
|
|
57
|
+
const base = file.split(/[/\\]/).pop()?.toLowerCase() ?? "";
|
|
58
|
+
const byName = {
|
|
59
|
+
"package.json": ["", "DiagnosticError"], // nf-dev-npm
|
|
60
|
+
"package-lock.json": ["", "DiagnosticError"],
|
|
61
|
+
dockerfile: ["", "DiagnosticInfo"], // nf-linux-docker
|
|
62
|
+
".gitignore": ["", "DiagnosticWarn"], // nf-dev-git
|
|
63
|
+
makefile: ["", "Comment"], // nf-dev-gnu
|
|
64
|
+
license: ["", "DiagnosticWarn"], // nf-seti-license
|
|
65
|
+
};
|
|
66
|
+
if (byName[base])
|
|
67
|
+
return byName[base];
|
|
68
|
+
const ext = base.includes(".") ? base.slice(base.lastIndexOf(".") + 1) : "";
|
|
69
|
+
const byExt = {
|
|
70
|
+
ts: ["", "Function"], // nf-seti-typescript
|
|
71
|
+
tsx: ["", "Function"], // nf-dev-react
|
|
72
|
+
mts: ["", "Function"],
|
|
73
|
+
js: ["", "DiagnosticWarn"], // nf-dev-javascript
|
|
74
|
+
jsx: ["", "Function"],
|
|
75
|
+
mjs: ["", "DiagnosticWarn"],
|
|
76
|
+
cjs: ["", "DiagnosticWarn"],
|
|
77
|
+
json: ["", "DiagnosticWarn"], // nf-seti-json
|
|
78
|
+
md: ["", "Normal"], // nf-dev-markdown
|
|
79
|
+
py: ["", "DiagnosticWarn"], // nf-dev-python
|
|
80
|
+
rs: ["", "Special"], // nf-dev-rust
|
|
81
|
+
go: ["", "Function"], // nf-seti-go
|
|
82
|
+
lua: ["", "Function"], // nf-seti-lua
|
|
83
|
+
vim: ["", "String"], // nf-seti-vim
|
|
84
|
+
html: ["", "Special"], // nf-dev-html5
|
|
85
|
+
css: ["", "Function"], // nf-dev-css3
|
|
86
|
+
scss: ["", "Statement"],
|
|
87
|
+
sh: ["", "String"], // nf-oct-terminal
|
|
88
|
+
bash: ["", "String"],
|
|
89
|
+
zsh: ["", "String"],
|
|
90
|
+
toml: ["", "Comment"], // nf-seti-config
|
|
91
|
+
yml: ["", "Comment"],
|
|
92
|
+
yaml: ["", "Comment"],
|
|
93
|
+
lock: ["", "Comment"], // nf-fa-lock
|
|
94
|
+
svg: ["", "DiagnosticWarn"], // nf-fa-file_image_o
|
|
95
|
+
png: ["", "Constant"],
|
|
96
|
+
jpg: ["", "Constant"],
|
|
97
|
+
java: ["", "DiagnosticError"], // nf-dev-java
|
|
98
|
+
rb: ["", "DiagnosticError"], // nf-dev-ruby
|
|
99
|
+
c: ["", "Function"], // nf-custom-c
|
|
100
|
+
cpp: ["", "Function"], // nf-custom-cpp
|
|
101
|
+
h: ["", "Statement"],
|
|
102
|
+
};
|
|
103
|
+
return byExt[ext] ?? [NERD.file, "Comment"];
|
|
104
|
+
}
|
package/dist/import.js
CHANGED
|
@@ -255,6 +255,7 @@ export function importTheme(opts = {}) {
|
|
|
255
255
|
const dir = opts.outDir ?? themesDir();
|
|
256
256
|
fs.mkdirSync(dir, { recursive: true });
|
|
257
257
|
const file = path.join(dir, `${name}.json`);
|
|
258
|
-
|
|
258
|
+
const colorscheme = dump?.name || undefined;
|
|
259
|
+
fs.writeFileSync(file, `${JSON.stringify({ name, background, source: via, colorscheme, groups }, null, "\t")}\n`);
|
|
259
260
|
return { name, file, groups: Object.keys(groups).length, via };
|
|
260
261
|
}
|
package/dist/keys.js
CHANGED
package/dist/picker.js
CHANGED
|
@@ -8,9 +8,15 @@ import { fuzzyFilter, parseKey, truncateToWidth, visibleWidth } from "@mariozech
|
|
|
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: "❯" },
|
|
11
|
+
nerd: { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│", lt: "├", rt: "┤", current: "\uf00c", prompt: "\uf002" },
|
|
11
12
|
ascii: { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|", lt: "+", rt: "+", current: "*", prompt: ">" },
|
|
12
13
|
};
|
|
13
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
|
+
*/
|
|
14
20
|
export class ThemePicker {
|
|
15
21
|
painter;
|
|
16
22
|
themes;
|
|
@@ -21,6 +27,8 @@ export class ThemePicker {
|
|
|
21
27
|
requestRender;
|
|
22
28
|
look;
|
|
23
29
|
query = "";
|
|
30
|
+
/** typing goes to the search (after `/`) */
|
|
31
|
+
searching = false;
|
|
24
32
|
index = 0;
|
|
25
33
|
scroll = 0;
|
|
26
34
|
constructor(painter, themes, current, icons, onPreview, onDone, requestRender, look = () => undefined) {
|
|
@@ -51,42 +59,63 @@ export class ThemePicker {
|
|
|
51
59
|
}
|
|
52
60
|
handleInput(data) {
|
|
53
61
|
const key = parseKey(data) ?? "";
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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;
|
|
64
82
|
}
|
|
65
|
-
|
|
66
|
-
case "tab":
|
|
67
|
-
case "ctrl+n":
|
|
68
|
-
case "ctrl+j":
|
|
69
|
-
this.move(1);
|
|
70
|
-
break;
|
|
71
|
-
case "up":
|
|
72
|
-
case "shift+tab":
|
|
73
|
-
case "ctrl+p":
|
|
74
|
-
case "ctrl+k":
|
|
75
|
-
this.move(-1);
|
|
76
|
-
break;
|
|
77
|
-
case "backspace":
|
|
78
|
-
this.query = this.query.slice(0, -1);
|
|
79
|
-
this.resetSelection();
|
|
80
|
-
break;
|
|
81
|
-
case "ctrl+u":
|
|
83
|
+
else if (key === "ctrl+u") {
|
|
82
84
|
this.query = "";
|
|
83
85
|
this.resetSelection();
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}
|
|
90
119
|
}
|
|
91
120
|
this.requestRender();
|
|
92
121
|
}
|
|
@@ -119,15 +148,22 @@ export class ThemePicker {
|
|
|
119
148
|
border(I.v);
|
|
120
149
|
const items = this.filtered();
|
|
121
150
|
const title = " Colorschemes ";
|
|
122
|
-
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} `;
|
|
123
153
|
const top = border(I.tl + I.h) +
|
|
124
154
|
P.on("FloatTitle", bg) +
|
|
125
155
|
title +
|
|
126
156
|
RESET +
|
|
127
157
|
border(I.h.repeat(Math.max(0, inner - 1 - visibleWidth(title))) + I.tr);
|
|
128
158
|
const lines = [top];
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
}
|
|
131
167
|
lines.push(border(I.lt + I.h.repeat(inner) + I.rt));
|
|
132
168
|
const shown = items.slice(this.scroll, this.scroll + VISIBLE);
|
|
133
169
|
for (let i = 0; i < VISIBLE; i++) {
|
|
@@ -171,9 +207,10 @@ export class ThemePicker {
|
|
|
171
207
|
}
|
|
172
208
|
}
|
|
173
209
|
/**
|
|
174
|
-
* A
|
|
175
|
-
* Tab)
|
|
176
|
-
*
|
|
210
|
+
* A list to pick from (`:model`, `:effort`), vim-style. `j`/`k` (↓/↑, Ctrl-n/p,
|
|
211
|
+
* Tab) move, `g`/`G` jump to the top/bottom, Enter picks, `q`/Esc closes.
|
|
212
|
+
* `/` starts a search: type to filter, Esc ends it. With `custom`, Enter on a
|
|
213
|
+
* search that matches nothing picks the text itself (a model the list doesn't know).
|
|
177
214
|
*/
|
|
178
215
|
export class ChoicePicker {
|
|
179
216
|
painter;
|
|
@@ -184,6 +221,8 @@ export class ChoicePicker {
|
|
|
184
221
|
onDone;
|
|
185
222
|
requestRender;
|
|
186
223
|
query = "";
|
|
224
|
+
/** typing goes to the search (after `/`) */
|
|
225
|
+
searching = false;
|
|
187
226
|
index = 0;
|
|
188
227
|
scroll = 0;
|
|
189
228
|
items = [];
|
|
@@ -215,45 +254,70 @@ export class ChoicePicker {
|
|
|
215
254
|
handleInput(data) {
|
|
216
255
|
const key = parseKey(data) ?? "";
|
|
217
256
|
const items = this.filtered();
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
this.index = (this.index - 1 + items.length) % items.length;
|
|
243
|
-
break;
|
|
244
|
-
case "backspace":
|
|
245
|
-
this.query = this.query.slice(0, -1);
|
|
257
|
+
const move = (by) => {
|
|
258
|
+
if (items.length)
|
|
259
|
+
this.index = (this.index + by + items.length) % items.length;
|
|
260
|
+
};
|
|
261
|
+
if (key === "ctrl+c")
|
|
262
|
+
return this.onDone(null);
|
|
263
|
+
if (key === "enter" || key === "return") {
|
|
264
|
+
const picked = items[this.index]?.value ?? (this.custom && this.query.trim() ? this.query.trim() : undefined);
|
|
265
|
+
if (picked !== undefined)
|
|
266
|
+
this.onDone(picked);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (["down", "tab", "ctrl+n", "ctrl+j"].includes(key))
|
|
270
|
+
move(1);
|
|
271
|
+
else if (["up", "shift+tab", "ctrl+p", "ctrl+k"].includes(key))
|
|
272
|
+
move(-1);
|
|
273
|
+
else if (this.searching) {
|
|
274
|
+
if (key === "escape")
|
|
275
|
+
this.searching = false; // the filter stays; j/k move through it
|
|
276
|
+
else if (key === "backspace") {
|
|
277
|
+
if (this.query)
|
|
278
|
+
this.query = this.query.slice(0, -1);
|
|
279
|
+
else
|
|
280
|
+
this.searching = false;
|
|
246
281
|
this.index = 0;
|
|
247
|
-
|
|
248
|
-
|
|
282
|
+
}
|
|
283
|
+
else if (key === "ctrl+u") {
|
|
249
284
|
this.query = "";
|
|
250
285
|
this.index = 0;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
286
|
+
}
|
|
287
|
+
else if (data.length >= 1 && !data.startsWith("\x1b") && data >= " ") {
|
|
288
|
+
this.query += data;
|
|
289
|
+
this.index = 0;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
switch (data) {
|
|
294
|
+
case "j":
|
|
295
|
+
move(1);
|
|
296
|
+
break;
|
|
297
|
+
case "k":
|
|
298
|
+
move(-1);
|
|
299
|
+
break;
|
|
300
|
+
case "g":
|
|
255
301
|
this.index = 0;
|
|
256
|
-
|
|
302
|
+
break;
|
|
303
|
+
case "G":
|
|
304
|
+
this.index = Math.max(0, items.length - 1);
|
|
305
|
+
break;
|
|
306
|
+
case "/":
|
|
307
|
+
this.searching = true;
|
|
308
|
+
break;
|
|
309
|
+
case "q":
|
|
310
|
+
return this.onDone(null);
|
|
311
|
+
default:
|
|
312
|
+
if (key === "escape") {
|
|
313
|
+
if (this.query) {
|
|
314
|
+
this.query = ""; // first Esc drops the filter, the next one closes
|
|
315
|
+
this.index = 0;
|
|
316
|
+
}
|
|
317
|
+
else
|
|
318
|
+
return this.onDone(null);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
257
321
|
}
|
|
258
322
|
if (this.index < this.scroll)
|
|
259
323
|
this.scroll = this.index;
|
|
@@ -272,7 +336,14 @@ export class ChoicePicker {
|
|
|
272
336
|
const items = this.filtered();
|
|
273
337
|
const title = ` ${this.title} `;
|
|
274
338
|
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)];
|
|
275
|
-
|
|
339
|
+
if (this.searching) {
|
|
340
|
+
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));
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
const hint = this.query ? `/${this.query}` : `/ to search${this.custom ? " or type any name" : ""}`;
|
|
344
|
+
const text = truncateToWidth(hint, Math.max(0, inner - 4), "…");
|
|
345
|
+
lines.push(row(P.on("Comment", bg) + ` ${I.prompt} ${text}` + RESET, visibleWidth(text) + 3));
|
|
346
|
+
}
|
|
276
347
|
lines.push(border(I.lt + I.h.repeat(inner) + I.rt));
|
|
277
348
|
const shown = items.slice(this.scroll, this.scroll + VISIBLE);
|
|
278
349
|
for (let i = 0; i < VISIBLE; i++) {
|
|
@@ -294,7 +365,8 @@ export class ChoicePicker {
|
|
|
294
365
|
const note = c.note ? truncateToWidth(` ${c.note}`, Math.max(0, inner - 2 - visibleWidth(name)), "…") : "";
|
|
295
366
|
lines.push(row(P.on(group, rowBg) + ` ${name}` + P.on("Comment", rowBg) + note + RESET, 1 + visibleWidth(name) + visibleWidth(note), rowBg));
|
|
296
367
|
}
|
|
297
|
-
const
|
|
368
|
+
const keys = this.searching ? "type to filter · Esc done" : "j/k move · Enter pick · q close";
|
|
369
|
+
const count = ` ${items.length ? this.index + 1 : 0}/${items.length} · ${keys} `;
|
|
298
370
|
lines.push(border(I.bl + I.h.repeat(Math.max(0, inner - visibleWidth(count) - 1))) + P.on("Comment", bg) + count + RESET + border(I.h + I.br));
|
|
299
371
|
return lines;
|
|
300
372
|
}
|
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;
|
package/dist/tree.js
CHANGED