@vigent/cli 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -28
- package/dist/agents.js +29 -0
- package/dist/app.js +565 -21
- package/dist/attach.js +102 -0
- package/dist/choices.js +4 -1
- package/dist/cli-engines.js +15 -5
- package/dist/cli.js +25 -15
- package/dist/config.js +26 -2
- package/dist/diffview.js +1 -1
- package/dist/engines.js +18 -11
- package/dist/float.js +1 -1
- package/dist/hunk-review.js +218 -0
- package/dist/hunks.js +60 -0
- package/dist/icons.js +1 -0
- package/dist/keys.js +4 -1
- package/dist/main.js +16 -0
- package/dist/markdown.js +1 -1
- package/dist/notify.js +33 -0
- package/dist/pi-sdk.js +45 -0
- package/dist/pi-ui.js +78 -0
- package/dist/picker.js +92 -47
- package/dist/serve-page.js +247 -0
- package/dist/serve.js +267 -0
- package/dist/session.js +145 -10
- package/dist/settings.js +1 -1
- package/dist/templates.js +125 -0
- package/dist/theme.js +2 -0
- package/dist/tui.js +13 -0
- package/package.json +4 -4
package/dist/picker.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Type to filter, arrows / Ctrl-n / Ctrl-p / Tab to move (each move previews
|
|
5
5
|
* the theme live), Enter to keep it, Esc to go back to the previous theme.
|
|
6
6
|
*/
|
|
7
|
-
import { fuzzyFilter, parseKey, truncateToWidth, visibleWidth } from "
|
|
7
|
+
import { fuzzyFilter, parseKey, truncateToWidth, visibleWidth } from "./tui.js";
|
|
8
8
|
import { RESET, sgr } from "./theme.js";
|
|
9
9
|
export const PICKER_ICONS = {
|
|
10
10
|
unicode: { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│", lt: "├", rt: "┤", current: "●", prompt: "❯" },
|
|
@@ -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++) {
|
|
@@ -192,7 +227,9 @@ export class ChoicePicker {
|
|
|
192
227
|
scroll = 0;
|
|
193
228
|
items = [];
|
|
194
229
|
loading = true;
|
|
195
|
-
|
|
230
|
+
onBackOut;
|
|
231
|
+
rows;
|
|
232
|
+
constructor(painter, icons, title, items, current, custom, onDone, requestRender, opts = {}) {
|
|
196
233
|
this.painter = painter;
|
|
197
234
|
this.icons = icons;
|
|
198
235
|
this.title = title;
|
|
@@ -200,12 +237,15 @@ export class ChoicePicker {
|
|
|
200
237
|
this.custom = custom;
|
|
201
238
|
this.onDone = onDone;
|
|
202
239
|
this.requestRender = requestRender;
|
|
240
|
+
this.searching = opts.search ?? false;
|
|
241
|
+
this.rows = opts.rows ?? VISIBLE;
|
|
242
|
+
this.onBackOut = opts.onBackOut;
|
|
203
243
|
Promise.resolve(items)
|
|
204
244
|
.then((list) => {
|
|
205
245
|
this.items = list;
|
|
206
246
|
this.loading = false;
|
|
207
247
|
this.index = Math.max(0, list.findIndex((c) => c.value === current));
|
|
208
|
-
this.scroll = Math.max(0, this.index -
|
|
248
|
+
this.scroll = Math.max(0, this.index - this.rows + 1);
|
|
209
249
|
this.requestRender();
|
|
210
250
|
})
|
|
211
251
|
.catch(() => {
|
|
@@ -241,6 +281,8 @@ export class ChoicePicker {
|
|
|
241
281
|
else if (key === "backspace") {
|
|
242
282
|
if (this.query)
|
|
243
283
|
this.query = this.query.slice(0, -1);
|
|
284
|
+
else if (this.onBackOut)
|
|
285
|
+
return this.onBackOut();
|
|
244
286
|
else
|
|
245
287
|
this.searching = false;
|
|
246
288
|
this.index = 0;
|
|
@@ -286,8 +328,8 @@ export class ChoicePicker {
|
|
|
286
328
|
}
|
|
287
329
|
if (this.index < this.scroll)
|
|
288
330
|
this.scroll = this.index;
|
|
289
|
-
if (this.index >= this.scroll +
|
|
290
|
-
this.scroll = this.index -
|
|
331
|
+
if (this.index >= this.scroll + this.rows)
|
|
332
|
+
this.scroll = this.index - this.rows + 1;
|
|
291
333
|
this.requestRender();
|
|
292
334
|
}
|
|
293
335
|
invalidate() { }
|
|
@@ -299,7 +341,7 @@ export class ChoicePicker {
|
|
|
299
341
|
const border = (s) => P.on("FloatBorder", bg) + s + RESET;
|
|
300
342
|
const row = (content, used, rowBg = bg) => border(I.v) + content + P.on("NormalFloat", rowBg) + " ".repeat(Math.max(0, inner - used)) + RESET + border(I.v);
|
|
301
343
|
const items = this.filtered();
|
|
302
|
-
const title = ` ${this.title}
|
|
344
|
+
const title = truncateToWidth(` ${this.title} `, Math.max(0, inner - 2), "…");
|
|
303
345
|
const lines = [border(I.tl + I.h) + P.on("FloatTitle", bg) + title + RESET + border(I.h.repeat(Math.max(0, inner - 1 - visibleWidth(title))) + I.tr)];
|
|
304
346
|
if (this.searching) {
|
|
305
347
|
lines.push(row(P.on("Title", bg) + ` ${I.prompt} ` + RESET + P.on("NormalFloat", bg) + this.query + P.on("PmenuSel", undefined) + " " + RESET, visibleWidth(this.query) + 4));
|
|
@@ -310,15 +352,15 @@ export class ChoicePicker {
|
|
|
310
352
|
lines.push(row(P.on("Comment", bg) + ` ${I.prompt} ${text}` + RESET, visibleWidth(text) + 3));
|
|
311
353
|
}
|
|
312
354
|
lines.push(border(I.lt + I.h.repeat(inner) + I.rt));
|
|
313
|
-
const shown = items.slice(this.scroll, this.scroll +
|
|
314
|
-
for (let i = 0; i <
|
|
355
|
+
const shown = items.slice(this.scroll, this.scroll + this.rows);
|
|
356
|
+
for (let i = 0; i < this.rows; i++) {
|
|
315
357
|
const c = shown[i];
|
|
316
358
|
if (!c) {
|
|
317
359
|
let text = "";
|
|
318
360
|
if (i === 0 && this.loading)
|
|
319
361
|
text = " loading…";
|
|
320
362
|
else if (i === 0 && items.length === 0)
|
|
321
|
-
text = this.custom && this.query.trim() ? ` Enter uses '${this.query.trim()}'` : " no match";
|
|
363
|
+
text = this.custom && this.query.trim() ? ` Enter uses '${this.query.trim()}'` : this.custom && this.items.length === 0 ? " type it, then Enter" : " no match";
|
|
322
364
|
lines.push(row(P.on("Comment", bg) + truncateToWidth(text, inner, "…") + RESET, Math.min(inner, visibleWidth(text))));
|
|
323
365
|
continue;
|
|
324
366
|
}
|
|
@@ -326,9 +368,12 @@ export class ChoicePicker {
|
|
|
326
368
|
const rowBg = selected ? (P.attr("PmenuSel").bg ?? P.attr("Visual").bg) : bg;
|
|
327
369
|
const group = selected ? "PmenuSel" : "NormalFloat";
|
|
328
370
|
const marker = c.value === this.current ? `${I.current} ` : " ";
|
|
329
|
-
|
|
371
|
+
// notes line up in a column after the longest name (up to a third of the width)
|
|
372
|
+
const nameW = Math.min(Math.floor(inner / 3), Math.max(...items.map((x) => visibleWidth(x.value))));
|
|
373
|
+
const padded = c.note ? c.value + " ".repeat(Math.max(0, nameW - visibleWidth(c.value))) : c.value;
|
|
374
|
+
const name = truncateToWidth(`${marker}${padded}`, Math.max(10, inner - 4), "…");
|
|
330
375
|
const note = c.note ? truncateToWidth(` ${c.note}`, Math.max(0, inner - 2 - visibleWidth(name)), "…") : "";
|
|
331
|
-
lines.push(row(P.on(group, rowBg) + ` ${name}` + P.on("Comment", rowBg) + note + RESET, 1 + visibleWidth(name) + visibleWidth(note), rowBg));
|
|
376
|
+
lines.push(row(P.on(group, rowBg) + ` ${name}` + P.on(selected ? "PmenuSel" : "Comment", rowBg) + note + RESET, 1 + visibleWidth(name) + visibleWidth(note), rowBg));
|
|
332
377
|
}
|
|
333
378
|
const keys = this.searching ? "type to filter · Esc done" : "j/k move · Enter pick · q close";
|
|
334
379
|
const count = ` ${items.length ? this.index + 1 : 0}/${items.length} · ${keys} `;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
const esc = (s) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
2
|
+
export function servePage(project, palette) {
|
|
3
|
+
const p = {
|
|
4
|
+
...palette,
|
|
5
|
+
bg_dark: palette.bg_dark ?? palette.bg,
|
|
6
|
+
bg_alt: palette.bg_alt ?? palette.bg,
|
|
7
|
+
selection: palette.selection ?? palette.blue,
|
|
8
|
+
muted: palette.muted ?? palette.fg,
|
|
9
|
+
subtle: palette.subtle ?? palette.fg,
|
|
10
|
+
accent: palette.accent ?? palette.yellow,
|
|
11
|
+
};
|
|
12
|
+
return `<!doctype html>
|
|
13
|
+
<html lang="en">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8">
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
17
|
+
<meta name="theme-color" content="${esc(p.bg_dark)}">
|
|
18
|
+
<title>vigent · ${esc(project)}</title>
|
|
19
|
+
<style>
|
|
20
|
+
:root {
|
|
21
|
+
--bg: ${p.bg}; --bg-dark: ${p.bg_dark}; --bg-alt: ${p.bg_alt}; --sel: ${p.selection};
|
|
22
|
+
--fg: ${p.fg}; --muted: ${p.muted}; --subtle: ${p.subtle}; --accent: ${p.accent};
|
|
23
|
+
--blue: ${p.blue}; --green: ${p.green}; --yellow: ${p.yellow}; --red: ${p.red}; --cyan: ${p.cyan}; --magenta: ${p.magenta};
|
|
24
|
+
--mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
|
|
25
|
+
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
26
|
+
}
|
|
27
|
+
* { box-sizing: border-box; }
|
|
28
|
+
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--fg); font: 15px/1.5 var(--sans); -webkit-text-size-adjust: 100%; }
|
|
29
|
+
button, textarea, input { font: inherit; color: inherit; }
|
|
30
|
+
header { position: sticky; top: 0; z-index: 2; display: flex; align-items: center; gap: 10px; padding: 12px 16px; padding-top: max(12px, env(safe-area-inset-top)); background: var(--bg-dark); border-bottom: 1px solid var(--sel); }
|
|
31
|
+
header h1 { margin: 0; font: 600 16px/1.2 var(--mono); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
32
|
+
header .project { color: var(--muted); font-weight: 400; }
|
|
33
|
+
.back { border: 0; background: none; color: var(--blue); font-size: 22px; line-height: 1; padding: 4px 8px 4px 0; cursor: pointer; }
|
|
34
|
+
.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--subtle); flex: none; }
|
|
35
|
+
.dot.live { background: var(--green); }
|
|
36
|
+
main { padding: 12px 16px 160px; max-width: 760px; margin: 0 auto; }
|
|
37
|
+
.empty, .note { color: var(--muted); text-align: center; padding: 40px 12px; }
|
|
38
|
+
.card { display: block; width: 100%; text-align: left; border: 1px solid var(--sel); background: var(--bg-dark); border-radius: 12px; padding: 12px 14px; margin: 0 0 10px; cursor: pointer; }
|
|
39
|
+
.card:active { background: var(--bg-alt); }
|
|
40
|
+
.row { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
41
|
+
.name { font: 600 15px/1.3 var(--mono); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
42
|
+
.meta { color: var(--muted); font: 12px/1.4 var(--mono); margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
43
|
+
.last { color: var(--fg); opacity: .85; font-size: 13px; margin-top: 6px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
44
|
+
.badge { font: 600 11px/1 var(--mono); padding: 4px 7px; border-radius: 999px; background: var(--bg-alt); color: var(--muted); flex: none; text-transform: lowercase; }
|
|
45
|
+
.badge.running { color: var(--bg); background: var(--green); }
|
|
46
|
+
.badge.waiting { color: var(--bg); background: var(--yellow); }
|
|
47
|
+
.badge.error { color: var(--bg); background: var(--red); }
|
|
48
|
+
.badge.paused { color: var(--bg); background: var(--subtle); }
|
|
49
|
+
.new { color: var(--yellow); font: 600 12px var(--mono); }
|
|
50
|
+
.add { color: var(--green); } .del { color: var(--red); }
|
|
51
|
+
.item { margin: 14px 0; }
|
|
52
|
+
.user { font: 600 14px/1.5 var(--mono); color: var(--accent); white-space: pre-wrap; word-break: break-word; }
|
|
53
|
+
.user::before { content: "❯ "; color: var(--blue); }
|
|
54
|
+
.agent { white-space: pre-wrap; word-break: break-word; }
|
|
55
|
+
.agent code { font: 13px var(--mono); background: var(--bg-alt); padding: 1px 5px; border-radius: 4px; color: var(--green); }
|
|
56
|
+
.agent pre, details pre { font: 12.5px/1.45 var(--mono); background: var(--bg-dark); border: 1px solid var(--sel); border-radius: 8px; padding: 10px 12px; overflow-x: auto; white-space: pre; margin: 8px 0; }
|
|
57
|
+
.agent pre code { background: none; padding: 0; color: var(--fg); }
|
|
58
|
+
.agent .h { font-weight: 700; color: var(--accent); }
|
|
59
|
+
.streaming::after { content: "▍"; color: var(--accent); animation: blink 1s steps(1) infinite; }
|
|
60
|
+
@keyframes blink { 50% { opacity: 0; } }
|
|
61
|
+
details { font: 13px/1.4 var(--mono); }
|
|
62
|
+
summary { color: var(--muted); cursor: pointer; list-style: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
63
|
+
summary::-webkit-details-marker { display: none; }
|
|
64
|
+
summary::before { content: "▸ "; } details[open] summary::before { content: "▾ "; }
|
|
65
|
+
summary b { color: var(--cyan); font-weight: 600; }
|
|
66
|
+
.info { color: var(--muted); font-size: 13px; font-style: italic; white-space: pre-wrap; }
|
|
67
|
+
.composer { position: fixed; left: 0; right: 0; bottom: 0; z-index: 3; background: var(--bg-dark); border-top: 1px solid var(--sel); padding: 10px 12px; padding-bottom: max(10px, env(safe-area-inset-bottom)); }
|
|
68
|
+
.composer .inner { max-width: 760px; margin: 0 auto; display: flex; gap: 8px; align-items: flex-end; }
|
|
69
|
+
textarea { flex: 1; min-height: 44px; max-height: 40vh; resize: none; border: 1px solid var(--sel); background: var(--bg); border-radius: 12px; padding: 10px 12px; font: 15px/1.4 var(--sans); outline: none; }
|
|
70
|
+
textarea:focus, input:focus { border-color: var(--blue); }
|
|
71
|
+
.btn { border: 0; border-radius: 12px; padding: 0 16px; height: 44px; font-weight: 600; cursor: pointer; background: var(--blue); color: var(--bg); flex: none; }
|
|
72
|
+
.btn.stop { background: var(--red); }
|
|
73
|
+
.btn:disabled { opacity: .5; }
|
|
74
|
+
.login { max-width: 360px; margin: 18vh auto 0; padding: 0 20px; text-align: center; }
|
|
75
|
+
.login h2 { font: 700 26px var(--mono); margin: 0 0 6px; color: var(--accent); }
|
|
76
|
+
.login p { color: var(--muted); margin: 0 0 22px; }
|
|
77
|
+
.login input { width: 100%; text-align: center; letter-spacing: 3px; font: 600 22px var(--mono); padding: 12px; border-radius: 12px; border: 1px solid var(--sel); background: var(--bg-dark); outline: none; }
|
|
78
|
+
.login .btn { width: 100%; margin-top: 12px; }
|
|
79
|
+
.err { color: var(--red); min-height: 1.5em; margin-top: 10px; font-size: 14px; }
|
|
80
|
+
.toast { position: fixed; left: 50%; transform: translateX(-50%); bottom: 96px; background: var(--bg-alt); border: 1px solid var(--sel); padding: 8px 14px; border-radius: 999px; font-size: 13px; z-index: 4; }
|
|
81
|
+
</style>
|
|
82
|
+
</head>
|
|
83
|
+
<body>
|
|
84
|
+
<div id="app"></div>
|
|
85
|
+
<script>
|
|
86
|
+
"use strict";
|
|
87
|
+
const store = { get(k) { try { return sessionStorage.getItem(k); } catch { return null; } }, set(k, v) { try { sessionStorage.setItem(k, v); } catch {} } };
|
|
88
|
+
let code = store.get("vigent-code") || "";
|
|
89
|
+
if (location.hash.length > 1) { code = decodeURIComponent(location.hash.slice(1)); store.set("vigent-code", code); history.replaceState(null, "", location.pathname); }
|
|
90
|
+
const state = { agents: [], open: null, items: [], version: -1, live: false, project: ${JSON.stringify(project)}, sending: false };
|
|
91
|
+
const $app = document.getElementById("app");
|
|
92
|
+
const h = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => "&#" + c.charCodeAt(0) + ";");
|
|
93
|
+
const tok = (n) => n >= 1e6 ? (n / 1e6).toFixed(1) + "M" : n >= 1e3 ? (n / 1e3).toFixed(1) + "k" : String(n);
|
|
94
|
+
const usd = (c) => c === undefined || c === null ? "" : " · $" + (c > 0 && c < 0.01 ? c.toFixed(3) : c.toFixed(2));
|
|
95
|
+
|
|
96
|
+
async function api(path, body) {
|
|
97
|
+
const r = await fetch(path, { method: body ? "POST" : "GET", headers: { authorization: "Bearer " + code, ...(body ? { "content-type": "application/json" } : {}) }, body: body ? JSON.stringify(body) : undefined, cache: "no-store" });
|
|
98
|
+
if (r.status === 401 || r.status === 423) { const e = await r.json().catch(() => ({})); throw Object.assign(new Error(e.error || "wrong access code"), { auth: true }); }
|
|
99
|
+
const data = await r.json().catch(() => ({}));
|
|
100
|
+
if (!r.ok) throw new Error(data.error || r.statusText);
|
|
101
|
+
return data;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function toast(text) {
|
|
105
|
+
const t = document.createElement("div"); t.className = "toast"; t.textContent = text; document.body.appendChild(t);
|
|
106
|
+
setTimeout(() => t.remove(), 2200);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function markdown(text) {
|
|
110
|
+
return text.split(/(\\x60\\x60\\x60[^\\n]*\\n[\\s\\S]*?(?:\\x60\\x60\\x60|$))/).map((part) => {
|
|
111
|
+
const fence = /^\\x60\\x60\\x60([^\\n]*)\\n([\\s\\S]*?)(?:\\x60\\x60\\x60)?$/.exec(part);
|
|
112
|
+
if (fence) return "<pre><code>" + h(fence[2].replace(/\\n$/, "")) + "</code></pre>";
|
|
113
|
+
return h(part)
|
|
114
|
+
.replace(/^(#{1,4}) (.*)$/gm, (_m, _hashes, t) => '<span class="h">' + t + "</span>")
|
|
115
|
+
.replace(/\\x60([^\\x60\\n]+)\\x60/g, "<code>$1</code>")
|
|
116
|
+
.replace(/\\*\\*([^*\\n]+)\\*\\*/g, "<strong>$1</strong>");
|
|
117
|
+
}).join("");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderLogin(error) {
|
|
121
|
+
$app.innerHTML = '<div class="login"><h2>vigent</h2><p>Type the access code vigent shows after <code>:serve</code>.</p>' +
|
|
122
|
+
'<form id="f"><input id="c" autocomplete="one-time-code" autocapitalize="none" spellcheck="false" placeholder="xxxx-xxxx" value="' + h(code) + '"><button class="btn">Follow</button></form>' +
|
|
123
|
+
'<div class="err">' + h(error || "") + "</div></div>";
|
|
124
|
+
document.getElementById("f").onsubmit = (e) => { e.preventDefault(); code = document.getElementById("c").value.trim().toLowerCase(); store.set("vigent-code", code); start(); };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function header(title, back) {
|
|
128
|
+
return "<header>" + (back ? '<button class="back" id="back" aria-label="All agents">‹</button>' : "") +
|
|
129
|
+
"<h1>" + title + '</h1><span class="dot' + (state.live ? " live" : "") + '" title="' + (state.live ? "live" : "reconnecting") + '"></span></header>';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function meta(a) {
|
|
133
|
+
return h(a.engineName) + " · " + h(a.model || "default") + (a.effort ? " · " + h(a.effort) : "") +
|
|
134
|
+
(a.branch ? " · ⎇ " + h(a.branch) : "") +
|
|
135
|
+
(a.added || a.removed ? ' · <span class="add">+' + a.added + '</span> <span class="del">−' + a.removed + "</span>" : "") +
|
|
136
|
+
(a.tokens ? " · " + tok(a.tokens) + " tok" + usd(a.cost) : "");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function composer(placeholder, running) {
|
|
140
|
+
return '<div class="composer"><form class="inner" id="send"><textarea id="t" rows="1" placeholder="' + h(placeholder) + '"></textarea>' +
|
|
141
|
+
(running ? '<button type="button" class="btn stop" id="stop">Stop</button>' : "") + '<button class="btn" id="go">Send</button></form></div>';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function wireComposer(onSend) {
|
|
145
|
+
const t = document.getElementById("t");
|
|
146
|
+
const grow = () => { t.style.height = "auto"; t.style.height = Math.min(t.scrollHeight, innerHeight * 0.4) + "px"; };
|
|
147
|
+
t.oninput = grow;
|
|
148
|
+
t.onkeydown = (e) => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); document.getElementById("send").requestSubmit(); } };
|
|
149
|
+
document.getElementById("send").onsubmit = async (e) => {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
const text = t.value.trim();
|
|
152
|
+
if (!text || state.sending) return;
|
|
153
|
+
state.sending = true; document.getElementById("go").disabled = true;
|
|
154
|
+
try { await onSend(text); t.value = ""; grow(); } catch (err) { toast(err.message); }
|
|
155
|
+
state.sending = false; const go = document.getElementById("go"); if (go) go.disabled = false;
|
|
156
|
+
};
|
|
157
|
+
const stop = document.getElementById("stop");
|
|
158
|
+
if (stop) stop.onclick = async () => { try { await api("/api/agents/" + state.open + "/stop", {}); toast("stopped"); } catch (err) { toast(err.message); } };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function renderList() {
|
|
162
|
+
const draft = document.getElementById("t")?.value || "";
|
|
163
|
+
const cards = state.agents.map((a) =>
|
|
164
|
+
'<button class="card" data-id="' + a.id + '"><div class="row"><span class="name">' + h(a.name) + "</span>" +
|
|
165
|
+
(a.unread ? '<span class="new">● new</span>' : "") + '<span class="badge ' + h(a.state) + '">' + h(a.state) + (a.queued ? " +" + a.queued : "") + "</span></div>" +
|
|
166
|
+
'<div class="meta">' + meta(a) + "</div>" + (a.last ? '<div class="last">' + (a.last.kind === "user" ? "❯ " : "") + h(a.last.text) + "</div>" : "") + "</button>").join("");
|
|
167
|
+
$app.innerHTML = header('vigent <span class="project">· ' + h(state.project) + "</span>") +
|
|
168
|
+
"<main>" + (cards || '<div class="empty">No agents yet. Describe a task below to start one.</div>') + "</main>" + composer("New agent: describe a task", false);
|
|
169
|
+
for (const c of document.querySelectorAll(".card")) c.onclick = () => openAgent(Number(c.dataset.id));
|
|
170
|
+
document.getElementById("t").value = draft;
|
|
171
|
+
wireComposer(async (text) => { const r = await api("/api/agents", { text }); toast("started"); openAgent(r.id); });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function itemHtml(i, last) {
|
|
175
|
+
if (i.kind === "user") return '<div class="item user">' + h(i.text) + "</div>";
|
|
176
|
+
if (i.kind === "agent") return '<div class="item agent' + (i.streaming && last ? " streaming" : "") + '">' + markdown(i.text) + "</div>";
|
|
177
|
+
if (i.kind === "tool" && i.tool) return '<details class="item"><summary><b>' + h(i.tool.name) + "</b> " + h(i.tool.summary) + "</summary>" + (i.tool.output ? "<pre>" + h(i.tool.output) + "</pre>" : "") + "</details>";
|
|
178
|
+
return '<div class="item info">' + h(i.text) + "</div>";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function renderAgent() {
|
|
182
|
+
const a = state.agents.find((x) => x.id === state.open);
|
|
183
|
+
if (!a) { state.open = null; return renderList(); }
|
|
184
|
+
const nearBottom = innerHeight + scrollY >= document.body.scrollHeight - 80;
|
|
185
|
+
const open = new Set([...document.querySelectorAll("details[open]")].map((d) => d.dataset.i));
|
|
186
|
+
const draft = document.getElementById("t")?.value || "";
|
|
187
|
+
const hadFocus = document.activeElement?.id === "t";
|
|
188
|
+
$app.innerHTML = header(h(a.name), true) +
|
|
189
|
+
'<main><div class="meta" style="white-space:normal">' + meta(a) + ' · <span class="badge ' + h(a.state) + '">' + h(a.state) + "</span></div>" +
|
|
190
|
+
(state.items.length ? state.items.map((i, n) => itemHtml(i, n === state.items.length - 1).replace("<details", '<details data-i="' + n + '"' + (open.has(String(n)) ? " open" : ""))).join("") : '<div class="note">Nothing yet.</div>') +
|
|
191
|
+
"</main>" + composer(a.state === "running" ? "Queue a follow-up…" : "Follow up…", a.state === "running");
|
|
192
|
+
document.getElementById("back").onclick = () => { state.open = null; history.back(); };
|
|
193
|
+
const t = document.getElementById("t"); t.value = draft; if (hadFocus) t.focus();
|
|
194
|
+
wireComposer(async (text) => { const r = await api("/api/agents/" + a.id + "/prompt", { text }); toast(r.queued ? "queued" : "sent"); });
|
|
195
|
+
if (nearBottom) scrollTo(0, document.body.scrollHeight);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let loading = null;
|
|
199
|
+
async function loadAgent() {
|
|
200
|
+
if (state.open === null) return;
|
|
201
|
+
if (loading) { loading.again = true; return; }
|
|
202
|
+
loading = { again: false };
|
|
203
|
+
try {
|
|
204
|
+
const r = await api("/api/agents/" + state.open);
|
|
205
|
+
state.items = r.items; state.version = r.agent.version;
|
|
206
|
+
if (state.open === r.agent.id) renderAgent();
|
|
207
|
+
} catch (err) { if (err.auth) return renderLogin(err.message); toast(err.message); }
|
|
208
|
+
const again = loading.again; loading = null;
|
|
209
|
+
if (again) loadAgent();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function openAgent(id) {
|
|
213
|
+
state.open = id; state.items = []; state.version = -1;
|
|
214
|
+
history.pushState({ id }, "", "#agent-" + id);
|
|
215
|
+
renderAgent(); scrollTo(0, 0); loadAgent();
|
|
216
|
+
}
|
|
217
|
+
addEventListener("popstate", () => { if (state.open !== null) { state.open = null; renderList(); } });
|
|
218
|
+
|
|
219
|
+
let source;
|
|
220
|
+
function listen() {
|
|
221
|
+
source?.close();
|
|
222
|
+
source = new EventSource("/api/events?code=" + encodeURIComponent(code));
|
|
223
|
+
source.onopen = () => { state.live = true; document.querySelector(".dot")?.classList.add("live"); };
|
|
224
|
+
source.onerror = () => { state.live = false; document.querySelector(".dot")?.classList.remove("live"); };
|
|
225
|
+
source.addEventListener("agents", (e) => {
|
|
226
|
+
state.agents = JSON.parse(e.data).agents;
|
|
227
|
+
if (state.open === null) return renderList();
|
|
228
|
+
const a = state.agents.find((x) => x.id === state.open);
|
|
229
|
+
if (!a) { state.open = null; return renderList(); }
|
|
230
|
+
if (a.version !== state.version) loadAgent(); else renderAgent();
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function start() {
|
|
235
|
+
if (!code) return renderLogin();
|
|
236
|
+
try {
|
|
237
|
+
const r = await api("/api/state");
|
|
238
|
+
state.project = r.project; state.agents = r.agents;
|
|
239
|
+
renderList(); listen();
|
|
240
|
+
} catch (err) { renderLogin(err.auth ? err.message : "can't reach vigent: " + err.message); }
|
|
241
|
+
}
|
|
242
|
+
start();
|
|
243
|
+
</script>
|
|
244
|
+
</body>
|
|
245
|
+
</html>
|
|
246
|
+
`;
|
|
247
|
+
}
|