@vigent/cli 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +261 -2
- package/dist/agents.js +65 -0
- package/dist/app.js +2487 -0
- package/dist/builtin-themes.js +352 -0
- package/dist/choices.js +73 -0
- package/dist/cli-engines.js +537 -0
- package/dist/cli.js +440 -0
- package/dist/clipboard.js +56 -0
- package/dist/color.js +126 -0
- package/dist/config-edit.js +61 -0
- package/dist/config.js +406 -0
- package/dist/demo.js +81 -0
- package/dist/diffview.js +94 -0
- package/dist/engines.js +195 -0
- package/dist/export.js +29 -0
- package/dist/float.js +182 -0
- package/dist/import.js +260 -0
- package/dist/keys.js +589 -0
- package/dist/markdown.js +80 -0
- package/dist/palette.js +83 -0
- package/dist/picker.js +301 -0
- package/dist/review.js +158 -0
- package/dist/riftri.js +284 -0
- package/dist/session.js +460 -0
- package/dist/settings.js +174 -0
- package/dist/suggest.js +31 -0
- package/dist/theme-new.js +49 -0
- package/dist/theme.js +487 -0
- package/dist/tree.js +115 -0
- package/dist/version.js +45 -0
- package/dist/vimcolors.js +206 -0
- package/dist/worktree.js +357 -0
- package/package.json +64 -4
package/dist/config.js
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ~/.config/vigent/config.toml
|
|
3
|
+
*
|
|
4
|
+
* theme = "tokyonight"
|
|
5
|
+
*
|
|
6
|
+
* [ui]
|
|
7
|
+
* sidebar = true
|
|
8
|
+
* sidebar_width = 30
|
|
9
|
+
* number = true
|
|
10
|
+
* tilde = true
|
|
11
|
+
* icons = "unicode" # or "ascii"
|
|
12
|
+
*
|
|
13
|
+
* [highlights] # applied on top of whichever theme is active
|
|
14
|
+
* Normal = { bg = "NONE" } # transparent background
|
|
15
|
+
* CursorLine = "guibg=#2a2f3a"
|
|
16
|
+
* VigentSidebar = { link = "NormalFloat" }
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { parse as parseToml } from "smol-toml";
|
|
21
|
+
import { EFFORTS } from "./choices.js";
|
|
22
|
+
import { isEngineId } from "./engines.js";
|
|
23
|
+
import { WORKTREE_BACKENDS } from "./riftri.js";
|
|
24
|
+
import { KEY_MODES } from "./keys.js";
|
|
25
|
+
import { configDir, normalizeGroups } from "./theme.js";
|
|
26
|
+
import { DEFAULT_WORKTREE } from "./worktree.js";
|
|
27
|
+
export const DEFAULT_UI = {
|
|
28
|
+
sidebar: true,
|
|
29
|
+
sidebar_width: 30,
|
|
30
|
+
number: true,
|
|
31
|
+
tilde: true,
|
|
32
|
+
icons: "unicode",
|
|
33
|
+
timeoutlen: 1000,
|
|
34
|
+
// macOS Terminal.app can't show 24-bit color
|
|
35
|
+
termguicolors: process.env.TERM_PROGRAM !== "Apple_Terminal",
|
|
36
|
+
transparent: "auto",
|
|
37
|
+
whichkey: true,
|
|
38
|
+
};
|
|
39
|
+
export function configPath() {
|
|
40
|
+
return path.join(configDir(), "config.toml");
|
|
41
|
+
}
|
|
42
|
+
/** Load the config file. Problems become warnings so a typo never stops vigent from starting. */
|
|
43
|
+
export function loadConfig(file = configPath()) {
|
|
44
|
+
const config = { ui: { ...DEFAULT_UI }, highlights: {}, keys: {}, worktree: { ...DEFAULT_WORKTREE }, engines: {} };
|
|
45
|
+
const warnings = [];
|
|
46
|
+
if (!fs.existsSync(file))
|
|
47
|
+
return { config, warnings };
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = parseToml(fs.readFileSync(file, "utf8"));
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
warnings.push(`${path.basename(file)}: ${err.message.split("\n")[0]}`);
|
|
54
|
+
return { config, warnings };
|
|
55
|
+
}
|
|
56
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
57
|
+
switch (key) {
|
|
58
|
+
case "theme":
|
|
59
|
+
case "colorscheme":
|
|
60
|
+
case "model":
|
|
61
|
+
case "leader":
|
|
62
|
+
case "mapleader":
|
|
63
|
+
if (typeof value === "string") {
|
|
64
|
+
const field = key === "colorscheme" ? "theme" : key === "mapleader" ? "leader" : key;
|
|
65
|
+
config[field] = value;
|
|
66
|
+
}
|
|
67
|
+
else
|
|
68
|
+
warnings.push(`config: '${key}' should be a string`);
|
|
69
|
+
break;
|
|
70
|
+
case "engine":
|
|
71
|
+
if (typeof value === "string" && (value === "auto" || isEngineId(value)))
|
|
72
|
+
config.engine = value;
|
|
73
|
+
else
|
|
74
|
+
warnings.push("config: 'engine' should be \"auto\", \"pi\", \"claude\", \"codex\" or \"opencode\"");
|
|
75
|
+
break;
|
|
76
|
+
case "engines":
|
|
77
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
78
|
+
warnings.push("config: [engines] should be a table");
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
for (const [id, opts] of Object.entries(value)) {
|
|
82
|
+
if (!isEngineId(id)) {
|
|
83
|
+
warnings.push(`config: unknown engine [engines.${id}] (pi, claude, codex or opencode)`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!opts || typeof opts !== "object" || Array.isArray(opts)) {
|
|
87
|
+
warnings.push(`config: [engines.${id}] should be a table`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const e = {};
|
|
91
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
92
|
+
if (k === "args" && Array.isArray(v) && v.every((a) => typeof a === "string"))
|
|
93
|
+
e.args = v;
|
|
94
|
+
else if (k === "model" && typeof v === "string")
|
|
95
|
+
e.model = v;
|
|
96
|
+
else if (k === "effort" && typeof v === "string" && EFFORTS[id].includes(v))
|
|
97
|
+
e.effort = v;
|
|
98
|
+
else if (k === "effort")
|
|
99
|
+
warnings.push(`config [engines.${id}]: 'effort' should be one of ${EFFORTS[id].join(", ")}`);
|
|
100
|
+
else
|
|
101
|
+
warnings.push(`config [engines.${id}]: ${k === "args" ? "'args' should be a list of strings" : k === "model" ? "'model' should be a string" : `unknown option '${k}'`}`);
|
|
102
|
+
}
|
|
103
|
+
config.engines[id] = e;
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
case "keys":
|
|
107
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
108
|
+
warnings.push("config: [keys] should be a table");
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
for (const [k, v] of Object.entries(value)) {
|
|
112
|
+
if (KEY_MODES.includes(k) && v && typeof v === "object" && !Array.isArray(v)) {
|
|
113
|
+
config.keys[k] = { ...config.keys[k], ...v };
|
|
114
|
+
}
|
|
115
|
+
else if (KEY_MODES.includes(k)) {
|
|
116
|
+
warnings.push(`config: [keys.${k}] should be a table`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// keys directly under [keys] are normal-mode mappings
|
|
120
|
+
config.keys.normal = { ...config.keys.normal, [k]: v };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
case "ui":
|
|
125
|
+
if (value && typeof value === "object") {
|
|
126
|
+
for (const [opt, v] of Object.entries(value)) {
|
|
127
|
+
const msg = setOption(config.ui, opt, v);
|
|
128
|
+
if (msg)
|
|
129
|
+
warnings.push(`config [ui]: ${msg}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
case "highlights":
|
|
134
|
+
config.highlights = normalizeGroups(value);
|
|
135
|
+
break;
|
|
136
|
+
case "worktree":
|
|
137
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
138
|
+
warnings.push("config: [worktree] should be a table");
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
for (const [opt, v] of Object.entries(value)) {
|
|
142
|
+
const msg = setWorktreeOption(config.worktree, opt, v);
|
|
143
|
+
if (msg)
|
|
144
|
+
warnings.push(`config [worktree]: ${msg}`);
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
default:
|
|
148
|
+
warnings.push(`config: unknown key '${key}'`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { config, warnings };
|
|
152
|
+
}
|
|
153
|
+
function setWorktreeOption(wt, name, value) {
|
|
154
|
+
switch (name) {
|
|
155
|
+
case "enabled":
|
|
156
|
+
if (typeof value !== "boolean")
|
|
157
|
+
return "'enabled' should be true or false";
|
|
158
|
+
wt.enabled = value;
|
|
159
|
+
return;
|
|
160
|
+
case "dir":
|
|
161
|
+
case "base":
|
|
162
|
+
if (typeof value !== "string")
|
|
163
|
+
return `'${name}' should be a string`;
|
|
164
|
+
if (name === "dir" && !value.trim())
|
|
165
|
+
return "'dir' can't be empty";
|
|
166
|
+
wt[name] = value;
|
|
167
|
+
return;
|
|
168
|
+
case "backend":
|
|
169
|
+
if (typeof value !== "string" || !WORKTREE_BACKENDS.includes(value))
|
|
170
|
+
return "'backend' should be \"auto\", \"riftri\" or \"git\"";
|
|
171
|
+
wt.backend = value;
|
|
172
|
+
return;
|
|
173
|
+
case "branch":
|
|
174
|
+
if (typeof value !== "string")
|
|
175
|
+
return "'branch' should be a string";
|
|
176
|
+
if (!value.includes("{name}"))
|
|
177
|
+
return "'branch' should contain {name}, e.g. \"{type}/{name}\" or \"agent/{name}\"";
|
|
178
|
+
wt.branch = value;
|
|
179
|
+
return;
|
|
180
|
+
default:
|
|
181
|
+
return `unknown option '${name}'`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const ALIASES = {
|
|
185
|
+
sidebar: "sidebar",
|
|
186
|
+
sb: "sidebar",
|
|
187
|
+
sidebar_width: "sidebar_width",
|
|
188
|
+
sidebarwidth: "sidebar_width",
|
|
189
|
+
sbw: "sidebar_width",
|
|
190
|
+
number: "number",
|
|
191
|
+
nu: "number",
|
|
192
|
+
tilde: "tilde",
|
|
193
|
+
icons: "icons",
|
|
194
|
+
timeoutlen: "timeoutlen",
|
|
195
|
+
tm: "timeoutlen",
|
|
196
|
+
termguicolors: "termguicolors",
|
|
197
|
+
tgc: "termguicolors",
|
|
198
|
+
transparent: "transparent",
|
|
199
|
+
transp: "transparent",
|
|
200
|
+
whichkey: "whichkey",
|
|
201
|
+
wk: "whichkey",
|
|
202
|
+
};
|
|
203
|
+
/** Set one option from a config value. Returns an error message, or undefined on success. */
|
|
204
|
+
export function setOption(ui, name, value) {
|
|
205
|
+
const key = ALIASES[name];
|
|
206
|
+
if (!key)
|
|
207
|
+
return `unknown option '${name}'`;
|
|
208
|
+
switch (key) {
|
|
209
|
+
case "sidebar":
|
|
210
|
+
case "number":
|
|
211
|
+
case "tilde":
|
|
212
|
+
case "termguicolors":
|
|
213
|
+
case "whichkey":
|
|
214
|
+
if (typeof value !== "boolean")
|
|
215
|
+
return `'${name}' should be true or false`;
|
|
216
|
+
ui[key] = value;
|
|
217
|
+
return;
|
|
218
|
+
case "sidebar_width": {
|
|
219
|
+
const n = Number(value);
|
|
220
|
+
if (!Number.isInteger(n) || n < 16 || n > 80)
|
|
221
|
+
return `'${name}' should be a number from 16 to 80`;
|
|
222
|
+
ui.sidebar_width = n;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
case "icons":
|
|
226
|
+
if (value !== "unicode" && value !== "ascii")
|
|
227
|
+
return `'${name}' should be "unicode" or "ascii"`;
|
|
228
|
+
ui.icons = value;
|
|
229
|
+
return;
|
|
230
|
+
case "transparent":
|
|
231
|
+
if (value === true || value === false || value === "auto")
|
|
232
|
+
ui.transparent = value;
|
|
233
|
+
else if (value === "true" || value === "false")
|
|
234
|
+
ui.transparent = value === "true";
|
|
235
|
+
else
|
|
236
|
+
return `'${name}' should be true, false or "auto" (follow the theme)`;
|
|
237
|
+
return;
|
|
238
|
+
case "timeoutlen": {
|
|
239
|
+
const n = Number(value);
|
|
240
|
+
if (!Number.isInteger(n) || n < 0 || n > 10000)
|
|
241
|
+
return `'${name}' should be milliseconds from 0 to 10000`;
|
|
242
|
+
ui.timeoutlen = n;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Apply a vim-style `:set` argument: `number`, `nonumber`, `number!`,
|
|
249
|
+
* `invnumber`, `sidebar_width=40`, `number?`. Returns the message to show.
|
|
250
|
+
*/
|
|
251
|
+
export function applySetCommand(ui, arg,
|
|
252
|
+
/** what's on screen now, for toggling an option that follows the theme */
|
|
253
|
+
now = {}) {
|
|
254
|
+
const show = (k) => {
|
|
255
|
+
const v = ui[k];
|
|
256
|
+
return typeof v === "boolean" ? (v ? ` ${k}` : `no${k}`) : ` ${k}=${v}`;
|
|
257
|
+
};
|
|
258
|
+
if (!arg.trim()) {
|
|
259
|
+
return { message: Object.keys(ui).map(show).join(" ") };
|
|
260
|
+
}
|
|
261
|
+
const parts = arg.trim().split(/\s+/);
|
|
262
|
+
const shown = [];
|
|
263
|
+
for (const part of parts) {
|
|
264
|
+
let m;
|
|
265
|
+
if ((m = /^([\w]+)\?$/.exec(part))) {
|
|
266
|
+
const key = ALIASES[m[1]];
|
|
267
|
+
if (!key)
|
|
268
|
+
return { message: `E518: Unknown option: ${m[1]}`, error: true };
|
|
269
|
+
shown.push(show(key));
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if ((m = /^([\w]+)=(.*)$/.exec(part))) {
|
|
273
|
+
const err = setOption(ui, m[1], m[2]);
|
|
274
|
+
if (err)
|
|
275
|
+
return { message: `E518: ${err}`, error: true };
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if ((m = /^([\w]+)!$/.exec(part)) || (m = /^inv([\w]+)$/.exec(part))) {
|
|
279
|
+
const key = ALIASES[m[1]];
|
|
280
|
+
if (key === "transparent") {
|
|
281
|
+
// following the theme: flip what's on screen
|
|
282
|
+
ui.transparent = !(ui.transparent === "auto" ? Boolean(now.transparent) : ui.transparent);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (!key || typeof ui[key] !== "boolean")
|
|
286
|
+
return { message: `E518: Unknown option: ${part}`, error: true };
|
|
287
|
+
ui[key] = !ui[key];
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if ((m = /^([\w]+)&$/.exec(part))) {
|
|
291
|
+
// vim's :set option& : back to the default
|
|
292
|
+
const key = ALIASES[m[1]];
|
|
293
|
+
if (!key)
|
|
294
|
+
return { message: `E518: Unknown option: ${m[1]}`, error: true };
|
|
295
|
+
ui[key] = DEFAULT_UI[key];
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const m2 = /^no([\w]+)$/.exec(part);
|
|
299
|
+
const off = Boolean(m2 && ALIASES[m2[1]]);
|
|
300
|
+
const key = ALIASES[off ? m2[1] : part];
|
|
301
|
+
if (!key)
|
|
302
|
+
return { message: `E518: Unknown option: ${part}`, error: true };
|
|
303
|
+
if (typeof ui[key] !== "boolean" && key !== "transparent") {
|
|
304
|
+
shown.push(show(key));
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
ui[key] = !off;
|
|
308
|
+
}
|
|
309
|
+
return { message: shown.join(" ") };
|
|
310
|
+
}
|
|
311
|
+
export const CONFIG_TEMPLATE = `# vigent config: ~/.config/vigent/config.toml
|
|
312
|
+
# Reference: https://github.com/farming-labs/vigent/blob/main/docs/config.md
|
|
313
|
+
# Check it any time with: vigent config check
|
|
314
|
+
|
|
315
|
+
# --- Theme -----------------------------------------------------------------
|
|
316
|
+
# Built-in: default, lazyvim (transparent), tokyonight, tokyonight-moon, gruvbox,
|
|
317
|
+
# catppuccin, catppuccin-latte, nord, rose-pine, kanagawa. Or any theme in ~/.config/vigent/themes, e.g. one made
|
|
318
|
+
# with vigent theme import (your nvim/vim colorscheme) or vigent theme new.
|
|
319
|
+
theme = "default"
|
|
320
|
+
|
|
321
|
+
# Model used for new pi agents.
|
|
322
|
+
# model = "claude-sonnet-4-5"
|
|
323
|
+
|
|
324
|
+
# --- Engines ---------------------------------------------------------------
|
|
325
|
+
# What runs new agents: "pi" (built in), or a coding-agent CLI you already
|
|
326
|
+
# have installed and signed in: "claude" (Claude Code), "codex", "opencode".
|
|
327
|
+
# "auto" picks pi if it has an API key, else the first signed-in CLI.
|
|
328
|
+
# Per agent: :new fix the race --engine codex. Status: :checkhealth
|
|
329
|
+
engine = "auto"
|
|
330
|
+
|
|
331
|
+
# Each CLI runs as it is, with its own login. Extra arguments and a model:
|
|
332
|
+
# [engines.claude]
|
|
333
|
+
# args = ["--permission-mode", "acceptEdits"] # the default
|
|
334
|
+
# model = "sonnet"
|
|
335
|
+
# [engines.codex]
|
|
336
|
+
# args = ["-c", 'sandbox_mode="workspace-write"'] # the default
|
|
337
|
+
# [engines.opencode]
|
|
338
|
+
# model = "anthropic/claude-sonnet-4-5"
|
|
339
|
+
|
|
340
|
+
# --- Leader ----------------------------------------------------------------
|
|
341
|
+
# What <leader> means in [keys]. Vim's default is "\\".
|
|
342
|
+
leader = "<Space>"
|
|
343
|
+
|
|
344
|
+
# --- Options (also live: :set nonumber, :set sidebar_width=40, ...) --------
|
|
345
|
+
[ui]
|
|
346
|
+
sidebar = true
|
|
347
|
+
sidebar_width = 30
|
|
348
|
+
number = true # session numbers in the sidebar
|
|
349
|
+
tilde = true # ~ on empty sidebar rows
|
|
350
|
+
icons = "unicode" # "ascii" for fonts without box-drawing characters
|
|
351
|
+
timeoutlen = 1000 # ms to wait for the rest of a multi-key mapping
|
|
352
|
+
# termguicolors = false # for terminals without 24-bit color: use the nearest of 256
|
|
353
|
+
# transparent = true # use the terminal's background with any theme ("auto": the theme decides)
|
|
354
|
+
whichkey = true # after <leader>, show what each next key does (like which-key.nvim)
|
|
355
|
+
|
|
356
|
+
# --- Colors (also live: :hi Group guifg=#... guibg=#...) -------------------
|
|
357
|
+
# Applied on top of whichever theme is active. A value is a table, a vim
|
|
358
|
+
# :hi string, or the name of a group to link to.
|
|
359
|
+
[highlights]
|
|
360
|
+
# Normal = { bg = "NONE" } # use the terminal's background
|
|
361
|
+
# CursorLine = "guibg=#2a2f3a"
|
|
362
|
+
# Comment = "gui=NONE" # no italic comments
|
|
363
|
+
# VigentModeNormal = { fg = "#1a1b26", bg = "#7aa2f7", bold = true }
|
|
364
|
+
|
|
365
|
+
# --- Worktrees --------------------------------------------------------------
|
|
366
|
+
# Each agent gets its own git worktree and branch, so agents never touch your
|
|
367
|
+
# checkout or each other. ":new fix flaky checkout test" creates the branch
|
|
368
|
+
# fix/flaky-checkout-test in ../<repo>-worktrees/fix-flaky-checkout-test.
|
|
369
|
+
[worktree]
|
|
370
|
+
enabled = true
|
|
371
|
+
dir = "../{repo}-worktrees" # relative to the repo root
|
|
372
|
+
branch = "{type}/{name}" # {type} = feat, fix, docs, refactor, ... from the agent's name
|
|
373
|
+
# base = "main" # default: the branch you have checked out
|
|
374
|
+
# With riftri installed (https://riftri.dev), unchanged files in agent
|
|
375
|
+
# worktrees share copy-on-write storage, so each new agent costs almost no
|
|
376
|
+
# disk. "auto" uses riftri when it can and git otherwise; "git" turns it off.
|
|
377
|
+
backend = "auto"
|
|
378
|
+
|
|
379
|
+
# --- Keys (also live: :nmap, :imap, :cmap, :unmap; list them with :help) ---
|
|
380
|
+
# Left side: vim notation, e.g. j gg <C-d> <S-Tab> <M-x> <CR> <leader>t
|
|
381
|
+
# Right side:
|
|
382
|
+
# "session.next" an action (all of them: :help)
|
|
383
|
+
# ":colorscheme nord<CR>" run an ex command
|
|
384
|
+
# ":new " open the command line with that text
|
|
385
|
+
# "gg" other keys, with their built-in meaning
|
|
386
|
+
# ["fold.open_all", "scroll.bottom"] several, in order
|
|
387
|
+
# false unbind a built-in key
|
|
388
|
+
|
|
389
|
+
[keys.normal]
|
|
390
|
+
"<leader>n" = ":new " # type a name, then Enter
|
|
391
|
+
"<leader>x" = ":kill<CR>"
|
|
392
|
+
"<leader>t" = "theme.picker"
|
|
393
|
+
"<leader>h" = "app.help"
|
|
394
|
+
"<Tab>" = "session.next"
|
|
395
|
+
"<S-Tab>" = "session.prev"
|
|
396
|
+
# "J" = "scroll.half_down"
|
|
397
|
+
# "K" = "scroll.half_up"
|
|
398
|
+
# "q" = false # stop q from showing the quit hint
|
|
399
|
+
|
|
400
|
+
[keys.insert]
|
|
401
|
+
# "jk" = "<Esc>" # leave INSERT by typing jk quickly
|
|
402
|
+
# "<C-s>" = "edit.submit"
|
|
403
|
+
|
|
404
|
+
[keys.command]
|
|
405
|
+
# "<C-p>" = "edit.clear"
|
|
406
|
+
`;
|
package/dist/demo.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Scripted demo content so the UI can be explored without API keys. */
|
|
2
|
+
import { DemoSession } from "./session.js";
|
|
3
|
+
import { branchName } from "./worktree.js";
|
|
4
|
+
export function createDemoSessions() {
|
|
5
|
+
const auth = new DemoSession("auth-refactor", "claude-sonnet-5-5", {
|
|
6
|
+
branch: branchName("auth-refactor"),
|
|
7
|
+
stat: { files: 2, added: 41, removed: 18 },
|
|
8
|
+
});
|
|
9
|
+
auth.pushUser("Refactor the session middleware to use the new token store.");
|
|
10
|
+
auth.play([
|
|
11
|
+
{ delay: 300, kind: "state", state: "running" },
|
|
12
|
+
{
|
|
13
|
+
delay: 400,
|
|
14
|
+
kind: "agent",
|
|
15
|
+
streamMs: 90,
|
|
16
|
+
text: "I'll start by reading the current middleware to understand how sessions are validated today.",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
delay: 500,
|
|
20
|
+
kind: "tool",
|
|
21
|
+
tool: {
|
|
22
|
+
name: "read",
|
|
23
|
+
summary: "src/middleware/auth.ts",
|
|
24
|
+
output: "import { verify } from '../lib/jwt';\n\nexport async function authMiddleware(req, res, next) {\n const token = req.headers.authorization?.split(' ')[1];\n if (!token) return res.status(401).end();\n // legacy: validates against the jwt secret directly\n const claims = verify(token, process.env.JWT_SECRET);\n req.user = claims.sub;\n next();\n}",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
delay: 900,
|
|
29
|
+
kind: "tool",
|
|
30
|
+
tool: {
|
|
31
|
+
name: "edit",
|
|
32
|
+
summary: "src/middleware/auth.ts (+41 −18)",
|
|
33
|
+
output: "- const claims = verify(token, process.env.JWT_SECRET);\n- req.user = claims.sub;\n+ const session = await tokenStore.resolve(token);\n+ if (!session) return res.status(401).end();\n+ req.user = session.userId;",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
delay: 800,
|
|
38
|
+
kind: "tool",
|
|
39
|
+
tool: { name: "bash", summary: "npm test -- auth", output: "PASS test/middleware/auth.test.ts\n ✓ rejects missing token (3 ms)\n ✓ resolves session from TokenStore (5 ms)\n ✓ rejects revoked tokens (2 ms)\n\nTests: 14 passed, 14 total" },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
delay: 700,
|
|
43
|
+
kind: "agent",
|
|
44
|
+
streamMs: 80,
|
|
45
|
+
text: [
|
|
46
|
+
"Done. The middleware now resolves tokens through **TokenStore** instead of verifying JWTs inline:",
|
|
47
|
+
"- revoked tokens are rejected with a `401`",
|
|
48
|
+
"- all 14 tests pass",
|
|
49
|
+
"```ts",
|
|
50
|
+
"const session = await tokenStore.resolve(token);",
|
|
51
|
+
"if (!session) return res.status(401).end();",
|
|
52
|
+
"```",
|
|
53
|
+
"Want me to update the docs too?",
|
|
54
|
+
].join("\n"),
|
|
55
|
+
},
|
|
56
|
+
{ delay: 300, kind: "state", state: "waiting" },
|
|
57
|
+
]);
|
|
58
|
+
const flaky = new DemoSession("fix-flaky-test", "claude-haiku-5-5", {
|
|
59
|
+
branch: branchName("fix-flaky-test"),
|
|
60
|
+
stat: { files: 1, added: 6, removed: 2 },
|
|
61
|
+
});
|
|
62
|
+
flaky.pushUser("The checkout e2e test fails ~1 in 5 runs. Find the race and fix it.");
|
|
63
|
+
flaky.play([
|
|
64
|
+
{ delay: 1200, kind: "state", state: "running" },
|
|
65
|
+
{ delay: 600, kind: "agent", streamMs: 110, text: "Reproducing first — running the suite 10 times to catch the failure." },
|
|
66
|
+
{
|
|
67
|
+
delay: 1500,
|
|
68
|
+
kind: "tool",
|
|
69
|
+
tool: { name: "bash", summary: "for i in $(seq 10); do npm run e2e -- checkout; done", output: "run 1..6: pass\nrun 7: FAIL expected cart badge '3', got '2'\nrun 8..10: pass" },
|
|
70
|
+
},
|
|
71
|
+
{ delay: 2500, kind: "agent", streamMs: 120, text: "Caught it: the badge assertion runs before the cart mutation settles. The test clicks 'add' and asserts immediately instead of awaiting the network idle. Fixing with a proper waitFor on the cart response." },
|
|
72
|
+
{ delay: 200, kind: "state", state: "waiting" },
|
|
73
|
+
]);
|
|
74
|
+
const docs = new DemoSession("docs-pass", "claude-haiku-5-5");
|
|
75
|
+
docs.pushUser("Sweep the README for stale CLI flags.");
|
|
76
|
+
docs.play([
|
|
77
|
+
{ delay: 500, kind: "agent", text: "Swept README.md and docs/cli.md: fixed 3 stale flags (--watch → --dev, removed --legacy-peer, documented --json). Committed as docs: refresh CLI flags." },
|
|
78
|
+
{ delay: 100, kind: "state", state: "idle" },
|
|
79
|
+
]);
|
|
80
|
+
return [auth, flaky, docs];
|
|
81
|
+
}
|
package/dist/diffview.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* :diff layouts for the floating diff window.
|
|
3
|
+
*
|
|
4
|
+
* unified: git's own output, colored.
|
|
5
|
+
* split: side by side, old on the left and new on the right, with line
|
|
6
|
+
* numbers; removed and added lines are paired up row by row.
|
|
7
|
+
*
|
|
8
|
+
* Both return the line indices of hunk headers so ]c / [c can jump.
|
|
9
|
+
*/
|
|
10
|
+
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
11
|
+
function lineGroup(line) {
|
|
12
|
+
if (line.startsWith("+++") || line.startsWith("---"))
|
|
13
|
+
return "Title";
|
|
14
|
+
if (line.startsWith("+"))
|
|
15
|
+
return "Added";
|
|
16
|
+
if (line.startsWith("-"))
|
|
17
|
+
return "Removed";
|
|
18
|
+
if (line.startsWith("@@"))
|
|
19
|
+
return "Special";
|
|
20
|
+
if (/^(diff --git|index |new file|deleted file|similarity|rename )/.test(line))
|
|
21
|
+
return "Comment";
|
|
22
|
+
return "NormalFloat";
|
|
23
|
+
}
|
|
24
|
+
export function unifiedView(text) {
|
|
25
|
+
const raw = text.replace(/\n$/, "").split("\n");
|
|
26
|
+
return {
|
|
27
|
+
lines: raw.map((l) => [[lineGroup(l), l]]),
|
|
28
|
+
jumps: raw.map((l, i) => (l.startsWith("@@") ? i : -1)).filter((i) => i >= 0),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function cell(no, text, width, group) {
|
|
32
|
+
if (text === undefined)
|
|
33
|
+
return [["NonText", " ".repeat(width)]];
|
|
34
|
+
const num = no === undefined ? " " : String(no).padStart(4);
|
|
35
|
+
const body = truncateToWidth(text.replace(/\t/g, " "), Math.max(0, width - 5), "…");
|
|
36
|
+
return [
|
|
37
|
+
["LineNr", `${num} `],
|
|
38
|
+
[group, body + " ".repeat(Math.max(0, width - 5 - visibleWidth(body)))],
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
/** Side-by-side layout for a window `width` columns wide (inside its border). */
|
|
42
|
+
export function splitView(text, width) {
|
|
43
|
+
const col = Math.max(12, Math.floor((width - 3) / 2));
|
|
44
|
+
const lines = [];
|
|
45
|
+
const jumps = [];
|
|
46
|
+
let oldNo = 0;
|
|
47
|
+
let newNo = 0;
|
|
48
|
+
let dels = [];
|
|
49
|
+
let adds = [];
|
|
50
|
+
const row = (l, r, lg = "NormalFloat", rg = "NormalFloat") => lines.push([...cell(l?.no, l?.text, col, lg), ["NonText", " │ "], ...cell(r?.no, r?.text, col, rg)]);
|
|
51
|
+
const flush = () => {
|
|
52
|
+
for (let i = 0; i < Math.max(dels.length, adds.length); i++)
|
|
53
|
+
row(dels[i], adds[i], "Removed", "Added");
|
|
54
|
+
dels = [];
|
|
55
|
+
adds = [];
|
|
56
|
+
};
|
|
57
|
+
for (const line of text.replace(/\n$/, "").split("\n")) {
|
|
58
|
+
if (line.startsWith("diff --git")) {
|
|
59
|
+
flush();
|
|
60
|
+
if (lines.length > 0)
|
|
61
|
+
lines.push([]);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const file = /^\+\+\+ (?:b\/)?(.*)$/.exec(line);
|
|
65
|
+
if (file) {
|
|
66
|
+
lines.push([["Title", file[1] === "/dev/null" ? "(deleted file)" : file[1]]]);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (/^(---|index |new file|deleted file|similarity|rename |\\ No newline)/.test(line))
|
|
70
|
+
continue;
|
|
71
|
+
const hunk = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@(.*)$/.exec(line);
|
|
72
|
+
if (hunk) {
|
|
73
|
+
flush();
|
|
74
|
+
oldNo = Number(hunk[1]);
|
|
75
|
+
newNo = Number(hunk[2]);
|
|
76
|
+
jumps.push(lines.length);
|
|
77
|
+
lines.push([["Special", line]]);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (line.startsWith("-")) {
|
|
81
|
+
dels.push({ no: oldNo++, text: line.slice(1) });
|
|
82
|
+
}
|
|
83
|
+
else if (line.startsWith("+")) {
|
|
84
|
+
adds.push({ no: newNo++, text: line.slice(1) });
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
flush();
|
|
88
|
+
const t = line.startsWith(" ") ? line.slice(1) : line;
|
|
89
|
+
row({ no: oldNo++, text: t }, { no: newNo++, text: t });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
flush();
|
|
93
|
+
return { lines, jumps };
|
|
94
|
+
}
|