amness 0.0.0-stage → 0.1.2
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 +237 -2
- package/dist/cli/acpcli.d.ts +7 -0
- package/dist/cli/acpcli.js +14 -0
- package/dist/cli/amness.d.ts +2 -0
- package/dist/cli/amness.js +694 -0
- package/dist/cli/benchcli.d.ts +5 -0
- package/dist/cli/benchcli.js +160 -0
- package/dist/cli/config.d.ts +79 -0
- package/dist/cli/config.js +111 -0
- package/dist/cli/demo.d.ts +13 -0
- package/dist/cli/demo.js +44 -0
- package/dist/cli/detect.d.ts +2 -0
- package/dist/cli/detect.js +20 -0
- package/dist/cli/evalcli.d.ts +15 -0
- package/dist/cli/evalcli.js +198 -0
- package/dist/cli/format.d.ts +5 -0
- package/dist/cli/format.js +55 -0
- package/dist/cli/headless.d.ts +2 -0
- package/dist/cli/headless.js +169 -0
- package/dist/cli/keystore.d.ts +9 -0
- package/dist/cli/keystore.js +108 -0
- package/dist/cli/model.d.ts +11 -0
- package/dist/cli/model.js +19 -0
- package/dist/cli/plugincli.d.ts +2 -0
- package/dist/cli/plugincli.js +176 -0
- package/dist/cli/schedulecli.d.ts +2 -0
- package/dist/cli/schedulecli.js +66 -0
- package/dist/cli/state.d.ts +11 -0
- package/dist/cli/state.js +19 -0
- package/dist/cli/tui/ansi.d.ts +38 -0
- package/dist/cli/tui/ansi.js +142 -0
- package/dist/cli/tui/app.d.ts +328 -0
- package/dist/cli/tui/app.js +1358 -0
- package/dist/cli/tui/blocks.d.ts +151 -0
- package/dist/cli/tui/blocks.js +128 -0
- package/dist/cli/tui/brand.d.ts +9 -0
- package/dist/cli/tui/brand.js +16 -0
- package/dist/cli/tui/canvas.d.ts +92 -0
- package/dist/cli/tui/canvas.js +199 -0
- package/dist/cli/tui/cards.d.ts +44 -0
- package/dist/cli/tui/cards.js +215 -0
- package/dist/cli/tui/clipboard.d.ts +5 -0
- package/dist/cli/tui/clipboard.js +29 -0
- package/dist/cli/tui/commands.d.ts +10 -0
- package/dist/cli/tui/commands.js +650 -0
- package/dist/cli/tui/complete.d.ts +39 -0
- package/dist/cli/tui/complete.js +113 -0
- package/dist/cli/tui/custom.d.ts +9 -0
- package/dist/cli/tui/custom.js +34 -0
- package/dist/cli/tui/diffview.d.ts +25 -0
- package/dist/cli/tui/diffview.js +130 -0
- package/dist/cli/tui/editor.d.ts +60 -0
- package/dist/cli/tui/editor.js +230 -0
- package/dist/cli/tui/full.d.ts +86 -0
- package/dist/cli/tui/full.js +630 -0
- package/dist/cli/tui/keys.d.ts +31 -0
- package/dist/cli/tui/keys.js +134 -0
- package/dist/cli/tui/markdown.d.ts +37 -0
- package/dist/cli/tui/markdown.js +155 -0
- package/dist/cli/tui/mentions.d.ts +15 -0
- package/dist/cli/tui/mentions.js +48 -0
- package/dist/cli/tui/modal.d.ts +26 -0
- package/dist/cli/tui/modal.js +107 -0
- package/dist/cli/tui/overlays.d.ts +98 -0
- package/dist/cli/tui/overlays.js +237 -0
- package/dist/cli/tui/pager.d.ts +15 -0
- package/dist/cli/tui/pager.js +66 -0
- package/dist/cli/tui/pricing.d.ts +9 -0
- package/dist/cli/tui/pricing.js +23 -0
- package/dist/cli/tui/rich.d.ts +16 -0
- package/dist/cli/tui/rich.js +72 -0
- package/dist/cli/tui/screen.d.ts +30 -0
- package/dist/cli/tui/screen.js +62 -0
- package/dist/cli/tui/session.d.ts +77 -0
- package/dist/cli/tui/session.js +103 -0
- package/dist/cli/tui/sidebar.d.ts +84 -0
- package/dist/cli/tui/sidebar.js +114 -0
- package/dist/cli/tui/text.d.ts +15 -0
- package/dist/cli/tui/text.js +94 -0
- package/dist/cli/tui/theme.d.ts +59 -0
- package/dist/cli/tui/theme.js +148 -0
- package/dist/cli/tui/themes.d.ts +56 -0
- package/dist/cli/tui/themes.js +80 -0
- package/dist/cli/tui/tokens.d.ts +39 -0
- package/dist/cli/tui/tokens.js +71 -0
- package/dist/cli/tui/view.d.ts +60 -0
- package/dist/cli/tui/view.js +178 -0
- package/dist/cli/ui.d.ts +34 -0
- package/dist/cli/ui.js +224 -0
- package/dist/src/acp.d.ts +28 -0
- package/dist/src/acp.js +111 -0
- package/dist/src/agents.d.ts +23 -0
- package/dist/src/agents.js +71 -0
- package/dist/src/anthropic.d.ts +30 -0
- package/dist/src/anthropic.js +91 -0
- package/dist/src/approver.d.ts +15 -0
- package/dist/src/approver.js +40 -0
- package/dist/src/bench.d.ts +67 -0
- package/dist/src/bench.js +201 -0
- package/dist/src/bundle.d.ts +36 -0
- package/dist/src/bundle.js +55 -0
- package/dist/src/chaos.d.ts +15 -0
- package/dist/src/chaos.js +26 -0
- package/dist/src/code.d.ts +187 -0
- package/dist/src/code.js +590 -0
- package/dist/src/codeindex.d.ts +65 -0
- package/dist/src/codeindex.js +300 -0
- package/dist/src/deep.d.ts +62 -0
- package/dist/src/deep.js +177 -0
- package/dist/src/diff.d.ts +8 -0
- package/dist/src/diff.js +74 -0
- package/dist/src/dynamic.d.ts +42 -0
- package/dist/src/dynamic.js +133 -0
- package/dist/src/embed.d.ts +8 -0
- package/dist/src/embed.js +25 -0
- package/dist/src/evalcases.d.ts +11 -0
- package/dist/src/evalcases.js +196 -0
- package/dist/src/evals.d.ts +197 -0
- package/dist/src/evals.js +340 -0
- package/dist/src/gitflow.d.ts +27 -0
- package/dist/src/gitflow.js +56 -0
- package/dist/src/grade.d.ts +42 -0
- package/dist/src/grade.js +96 -0
- package/dist/src/guidance.d.ts +15 -0
- package/dist/src/guidance.js +68 -0
- package/dist/src/hardcases.d.ts +2 -0
- package/dist/src/hardcases.js +94 -0
- package/dist/src/harness.d.ts +411 -0
- package/dist/src/harness.js +725 -0
- package/dist/src/hooks.d.ts +44 -0
- package/dist/src/hooks.js +116 -0
- package/dist/src/images.d.ts +13 -0
- package/dist/src/images.js +27 -0
- package/dist/src/index.d.ts +57 -0
- package/dist/src/index.js +57 -0
- package/dist/src/ledger.d.ts +58 -0
- package/dist/src/ledger.js +78 -0
- package/dist/src/live.d.ts +16 -0
- package/dist/src/live.js +80 -0
- package/dist/src/lsp.d.ts +31 -0
- package/dist/src/lsp.js +147 -0
- package/dist/src/memory.d.ts +128 -0
- package/dist/src/memory.js +390 -0
- package/dist/src/metrics.d.ts +3 -0
- package/dist/src/metrics.js +42 -0
- package/dist/src/mining.d.ts +49 -0
- package/dist/src/mining.js +126 -0
- package/dist/src/openai.d.ts +50 -0
- package/dist/src/openai.js +307 -0
- package/dist/src/parallel.d.ts +66 -0
- package/dist/src/parallel.js +135 -0
- package/dist/src/patch.d.ts +45 -0
- package/dist/src/patch.js +219 -0
- package/dist/src/permissions.d.ts +46 -0
- package/dist/src/permissions.js +245 -0
- package/dist/src/plugins.d.ts +148 -0
- package/dist/src/plugins.js +225 -0
- package/dist/src/pluginscan.d.ts +8 -0
- package/dist/src/pluginscan.js +20 -0
- package/dist/src/procs.d.ts +41 -0
- package/dist/src/procs.js +94 -0
- package/dist/src/profiles.d.ts +36 -0
- package/dist/src/profiles.js +61 -0
- package/dist/src/progress.d.ts +29 -0
- package/dist/src/progress.js +75 -0
- package/dist/src/prompts.d.ts +10 -0
- package/dist/src/prompts.js +28 -0
- package/dist/src/ratelimit.d.ts +34 -0
- package/dist/src/ratelimit.js +116 -0
- package/dist/src/replay.d.ts +25 -0
- package/dist/src/replay.js +99 -0
- package/dist/src/review.d.ts +20 -0
- package/dist/src/review.js +49 -0
- package/dist/src/route.d.ts +9 -0
- package/dist/src/route.js +30 -0
- package/dist/src/router.d.ts +61 -0
- package/dist/src/router.js +126 -0
- package/dist/src/sandbox.d.ts +13 -0
- package/dist/src/sandbox.js +68 -0
- package/dist/src/schedule.d.ts +20 -0
- package/dist/src/schedule.js +59 -0
- package/dist/src/schema.d.ts +13 -0
- package/dist/src/schema.js +36 -0
- package/dist/src/secrets.d.ts +10 -0
- package/dist/src/secrets.js +41 -0
- package/dist/src/shell.d.ts +34 -0
- package/dist/src/shell.js +95 -0
- package/dist/src/skills.d.ts +20 -0
- package/dist/src/skills.js +112 -0
- package/dist/src/snapshot.d.ts +20 -0
- package/dist/src/snapshot.js +93 -0
- package/dist/src/stats.d.ts +24 -0
- package/dist/src/stats.js +62 -0
- package/dist/src/strategies.d.ts +16 -0
- package/dist/src/strategies.js +35 -0
- package/dist/src/suites.d.ts +30 -0
- package/dist/src/suites.js +112 -0
- package/dist/src/trace.d.ts +63 -0
- package/dist/src/trace.js +153 -0
- package/dist/src/tune.d.ts +38 -0
- package/dist/src/tune.js +49 -0
- package/dist/src/web.d.ts +19 -0
- package/dist/src/web.js +56 -0
- package/dist/src/worktree.d.ts +24 -0
- package/dist/src/worktree.js +85 -0
- package/docs/agent-loop.md +222 -0
- package/docs/architecture.md +228 -0
- package/docs/bench.md +292 -0
- package/docs/budgets.md +185 -0
- package/docs/build-your-own-harness.md +424 -0
- package/docs/cookbook.md +375 -0
- package/docs/development.md +150 -0
- package/docs/evaluation.md +450 -0
- package/docs/features.md +1320 -0
- package/docs/getting-started.md +205 -0
- package/docs/headless.md +188 -0
- package/docs/images/approval.png +0 -0
- package/docs/images/chat.png +0 -0
- package/docs/images/daylight.png +0 -0
- package/docs/images/loop.png +0 -0
- package/docs/images/palette.png +0 -0
- package/docs/images/running.png +0 -0
- package/docs/images/subagent-done.png +0 -0
- package/docs/images/subagent-running.png +0 -0
- package/docs/images/welcome.png +0 -0
- package/docs/index.md +25 -0
- package/docs/library.md +267 -0
- package/docs/plugins.md +691 -0
- package/docs/providers.md +175 -0
- package/docs/safety.md +250 -0
- package/docs/usage.md +348 -0
- package/docs/why.md +155 -0
- package/docs/writing-guide.md +39 -0
- package/package.json +66 -4
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export const PASTE_ON = "\x1b[?2004h", PASTE_OFF = "\x1b[?2004l";
|
|
2
|
+
/** Mouse reporting: button events + drag motion, SGR encoding (so coordinates beyond 223 work). */
|
|
3
|
+
export const MOUSE_ON = "\x1b[?1000h\x1b[?1002h\x1b[?1006h", MOUSE_OFF = "\x1b[?1006l\x1b[?1002l\x1b[?1000l";
|
|
4
|
+
/** SGR mouse report `ESC [ < b ; x ; y (M|m)` -> event. Coordinates are 0-based cells. */
|
|
5
|
+
function mouseFrom(params, fin) {
|
|
6
|
+
const [b, x, y] = params.slice(1).split(";").map(Number);
|
|
7
|
+
if (![b, x, y].every(Number.isFinite))
|
|
8
|
+
return undefined;
|
|
9
|
+
const base = { x: x - 1, y: y - 1 };
|
|
10
|
+
if (b & 4)
|
|
11
|
+
base.shift = true;
|
|
12
|
+
if (b & 8)
|
|
13
|
+
base.alt = true;
|
|
14
|
+
if (b & 16)
|
|
15
|
+
base.ctrl = true;
|
|
16
|
+
if (b & 64)
|
|
17
|
+
return { ...base, button: "none", action: (b & 1) === 0 ? "wheelup" : "wheeldown" };
|
|
18
|
+
const button = ["left", "middle", "right", "none"][b & 3];
|
|
19
|
+
return { ...base, button, action: fin === "m" ? "release" : b & 32 ? (button === "none" ? "move" : "drag") : "press" };
|
|
20
|
+
}
|
|
21
|
+
const mods = (m) => ({ shift: !!((m - 1) & 1) || undefined, alt: !!((m - 1) & 2) || undefined, ctrl: !!((m - 1) & 4) || undefined });
|
|
22
|
+
const clean = (k) => Object.fromEntries(Object.entries(k).filter(([, v]) => v !== undefined));
|
|
23
|
+
const FINAL = { A: "up", B: "down", C: "right", D: "left", H: "home", F: "end", Z: "backtab", P: "f1", Q: "f2", R: "f3", S: "f4" };
|
|
24
|
+
const TILDE = { "1": "home", "2": "insert", "3": "delete", "4": "end", "5": "pageup", "6": "pagedown", "7": "home", "8": "end" };
|
|
25
|
+
export class KeyParser {
|
|
26
|
+
buf = "";
|
|
27
|
+
pasting = null;
|
|
28
|
+
/** Feed raw input; returns the keys that are complete. A trailing lone ESC (or partial sequence) is held until `flush()`. */
|
|
29
|
+
feed(chunk) {
|
|
30
|
+
this.buf += chunk;
|
|
31
|
+
const out = [];
|
|
32
|
+
for (;;) {
|
|
33
|
+
if (this.pasting !== null) {
|
|
34
|
+
const end = this.buf.indexOf("\x1b[201~");
|
|
35
|
+
if (end < 0) {
|
|
36
|
+
this.pasting += this.buf;
|
|
37
|
+
this.buf = "";
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
out.push({ name: "paste", text: (this.pasting + this.buf.slice(0, end)).replace(/\r\n?/g, "\n") });
|
|
41
|
+
this.buf = this.buf.slice(end + 6);
|
|
42
|
+
this.pasting = null;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!this.buf)
|
|
46
|
+
return out;
|
|
47
|
+
const r = this.one();
|
|
48
|
+
if (!r)
|
|
49
|
+
return out; // incomplete: wait for more bytes
|
|
50
|
+
this.buf = this.buf.slice(r.len);
|
|
51
|
+
if (r.key)
|
|
52
|
+
out.push(r.key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Called after a short idle: whatever is pending (a lone ESC) is a real key press. */
|
|
56
|
+
flush() {
|
|
57
|
+
if (this.buf === "\x1b") {
|
|
58
|
+
this.buf = "";
|
|
59
|
+
return [{ name: "escape" }];
|
|
60
|
+
}
|
|
61
|
+
if (this.pasting === null && this.buf) {
|
|
62
|
+
const b = this.buf;
|
|
63
|
+
this.buf = "";
|
|
64
|
+
return [{ name: "char", text: b }];
|
|
65
|
+
}
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
get pending() { return this.buf.length > 0; }
|
|
69
|
+
one() {
|
|
70
|
+
const b = this.buf, c = b[0];
|
|
71
|
+
if (c === "\x1b") {
|
|
72
|
+
if (b.length === 1)
|
|
73
|
+
return null;
|
|
74
|
+
if (b[1] === "[") {
|
|
75
|
+
const m = b.slice(2).match(/^([0-9;:?<=>]*)([A-Za-z~])/);
|
|
76
|
+
if (!m)
|
|
77
|
+
return b.length < 24 ? null : { len: 1, key: { name: "escape" } };
|
|
78
|
+
const len = 2 + m[0].length, [p0 = "", p1 = ""] = m[1].split(";"), fin = m[2];
|
|
79
|
+
if (m[1] === "200" && fin === "~") {
|
|
80
|
+
this.pasting = "";
|
|
81
|
+
return { len };
|
|
82
|
+
}
|
|
83
|
+
if (m[1].startsWith("<") && (fin === "M" || fin === "m")) {
|
|
84
|
+
const mouse = mouseFrom(m[1], fin);
|
|
85
|
+
return { len, key: mouse ? { name: "mouse", mouse } : undefined };
|
|
86
|
+
}
|
|
87
|
+
const mod = p1 ? Number(p1.split(":")[0]) : 1;
|
|
88
|
+
if (fin === "u") { // kitty keyboard protocol
|
|
89
|
+
const code = Number(p0), name = code === 13 ? "enter" : code === 9 ? "tab" : code === 27 ? "escape" : code === 127 ? "backspace" : "char";
|
|
90
|
+
return { len, key: clean(name === "char" ? { name, text: String.fromCodePoint(code), ...mods(mod) } : { name, ...mods(mod) }) };
|
|
91
|
+
}
|
|
92
|
+
const name = fin === "~" ? TILDE[p0] : FINAL[fin];
|
|
93
|
+
return { len, key: name ? clean({ name, ...mods(mod) }) : undefined };
|
|
94
|
+
}
|
|
95
|
+
if (b[1] === "O") {
|
|
96
|
+
if (b.length < 3)
|
|
97
|
+
return null;
|
|
98
|
+
const name = FINAL[b[2]];
|
|
99
|
+
return { len: 3, key: name ? { name } : undefined };
|
|
100
|
+
}
|
|
101
|
+
if (b[1] === "\x1b")
|
|
102
|
+
return { len: 1, key: { name: "escape" } };
|
|
103
|
+
const inner = this.ctrlOrChar(b[1]); // ESC + key = alt+key
|
|
104
|
+
return { len: 2, key: inner ? clean({ ...inner, alt: true }) : undefined };
|
|
105
|
+
}
|
|
106
|
+
// a run of printable text becomes one key (typed chars arrive one by one; a burst is a paste without bracketed-paste support)
|
|
107
|
+
const run = b.match(/^[^\x00-\x1f\x7f]+/u);
|
|
108
|
+
if (run) {
|
|
109
|
+
const rest = b.slice(run[0].length);
|
|
110
|
+
if (rest && /^[\r\n]/.test(rest) && run[0].length > 1 && !this.buf.slice(run[0].length).startsWith("\x1b")) {
|
|
111
|
+
const nl = rest.match(/^[^\x1b]*/)[0]; // text + newlines in one chunk without markers: treat as a paste
|
|
112
|
+
if (/[^\r\n]/.test(nl) || nl.length > 1)
|
|
113
|
+
return { len: run[0].length + nl.length, key: { name: "paste", text: (run[0] + nl).replace(/\r\n?/g, "\n").replace(/\n$/, "") } };
|
|
114
|
+
}
|
|
115
|
+
return { len: run[0].length, key: { name: "char", text: run[0] } };
|
|
116
|
+
}
|
|
117
|
+
return { len: 1, key: this.ctrlOrChar(c) };
|
|
118
|
+
}
|
|
119
|
+
ctrlOrChar(c) {
|
|
120
|
+
switch (c) {
|
|
121
|
+
case "\r": return { name: "enter" };
|
|
122
|
+
case "\n": return { name: "j", ctrl: true };
|
|
123
|
+
case "\t": return { name: "tab" };
|
|
124
|
+
case "\x7f": return { name: "backspace" };
|
|
125
|
+
case "\x08": return { name: "backspace", ctrl: true };
|
|
126
|
+
case "\x1b": return { name: "escape" };
|
|
127
|
+
case "\x00": return { name: "space", ctrl: true };
|
|
128
|
+
}
|
|
129
|
+
const code = c.codePointAt(0);
|
|
130
|
+
if (code >= 1 && code <= 26)
|
|
131
|
+
return { name: String.fromCharCode(96 + code), ctrl: true };
|
|
132
|
+
return code >= 32 ? { name: "char", text: c } : undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { clipWidth } from "./text.ts";
|
|
2
|
+
export type HlKind = "keyword" | "string" | "number" | "comment" | "plain";
|
|
3
|
+
export type Skin = {
|
|
4
|
+
bold(s: string): string;
|
|
5
|
+
italic(s: string): string;
|
|
6
|
+
strike(s: string): string;
|
|
7
|
+
code(s: string): string;
|
|
8
|
+
link(text: string, url: string): string;
|
|
9
|
+
dim(s: string): string;
|
|
10
|
+
/** styled lines for a heading (may include an underline rule) */
|
|
11
|
+
heading(level: number, text: string, width: number): string[];
|
|
12
|
+
quote(inner: string): string;
|
|
13
|
+
bullet(marker: string): string;
|
|
14
|
+
hr(width: number): string;
|
|
15
|
+
/** a whole fenced code block, already framed and highlighted */
|
|
16
|
+
codeBlock(lines: string[], lang: string, width: number): string[];
|
|
17
|
+
table(rows: string[][], width: number): string[];
|
|
18
|
+
/** hook for `inline` text in table cells etc. */
|
|
19
|
+
hl(kind: HlKind, text: string): string;
|
|
20
|
+
};
|
|
21
|
+
/** Tiny line-based highlighter: comments, strings, numbers, keywords. `paint` decides how each kind looks. */
|
|
22
|
+
export declare function highlightWith(line: string, lang: string, paint: (k: HlKind, s: string) => string): string;
|
|
23
|
+
export declare const defaultSkin: Skin;
|
|
24
|
+
/** Back-compat: highlight with the default skin's colours. */
|
|
25
|
+
export declare const highlight: (line: string, lang: string) => string;
|
|
26
|
+
/** Inline spans: `code`, **bold**, *italic*, ~~strike~~, [text](url). Code is protected first so nothing inside it is reformatted. */
|
|
27
|
+
export declare function inline(s: string, skin?: Skin): string;
|
|
28
|
+
export declare function renderMarkdown(src: string, width: number, skin?: Skin): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Split streamed markdown into blocks that are safe to render now (everything up to the last blank line that is not
|
|
31
|
+
* inside a code fence) and the unfinished remainder, which keeps growing.
|
|
32
|
+
*/
|
|
33
|
+
export declare function takeCompleteBlocks(buf: string): {
|
|
34
|
+
done: string;
|
|
35
|
+
rest: string;
|
|
36
|
+
};
|
|
37
|
+
export { clipWidth };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Markdown -> ANSI lines for assistant messages, plus block splitting so streamed text can be rendered as it arrives.
|
|
2
|
+
// All styling goes through a Skin, so the same parser serves the inline UI (defaultSkin) and the themed full-screen UI.
|
|
3
|
+
import { bold, cyan, dim, gray, green, magenta, yellow } from "../ui.js";
|
|
4
|
+
import { cellWidth, clipWidth, strip, wrapAnsi } from "./text.js";
|
|
5
|
+
const KW = {
|
|
6
|
+
js: "const let var function return if else for while do import export from as class new async await try catch finally throw type interface extends implements default switch case break continue null undefined true false this typeof instanceof of in void static public private readonly enum",
|
|
7
|
+
py: "def class return if elif else for while import from as with try except finally raise lambda None True False and or not in is pass yield async await global self",
|
|
8
|
+
sh: "if then else elif fi for while do done case esac function in export local echo cd set unset return exit",
|
|
9
|
+
rs: "fn let mut pub struct enum impl trait use mod match if else for while loop return self Self true false const static ref move async await unsafe where type",
|
|
10
|
+
go: "func var const type struct interface package import return if else for range switch case default go defer chan map nil true false select break continue",
|
|
11
|
+
json: "true false null",
|
|
12
|
+
};
|
|
13
|
+
const FAMILY = { js: "js", javascript: "js", jsx: "js", ts: "js", tsx: "js", typescript: "js", mjs: "js", cjs: "js", java: "js", c: "js", cpp: "js", cs: "js", kt: "js", swift: "js",
|
|
14
|
+
py: "py", python: "py", rb: "py", ruby: "py", sh: "sh", bash: "sh", zsh: "sh", shell: "sh", rs: "rs", rust: "rs", go: "go", json: "json", jsonc: "json" };
|
|
15
|
+
const HASH = new Set(["py", "sh", "rb", "yaml", "yml", "toml", "python", "bash", "zsh", "ruby", "shell", "conf", "dockerfile"]);
|
|
16
|
+
/** Tiny line-based highlighter: comments, strings, numbers, keywords. `paint` decides how each kind looks. */
|
|
17
|
+
export function highlightWith(line, lang, paint) {
|
|
18
|
+
const fam = FAMILY[lang.toLowerCase()], kw = fam ? new Set(KW[fam].split(" ")) : new Set();
|
|
19
|
+
const comment = HASH.has(lang.toLowerCase()) ? "#.*$" : fam || !lang ? "\\/\\/.*$" : "$^";
|
|
20
|
+
const re = new RegExp(`(${comment})|("(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|\`(?:\\\\.|[^\`\\\\])*\`)|(\\b\\d[\\d_.]*\\b)|(\\b[A-Za-z_$][\\w$]*\\b)`, "g");
|
|
21
|
+
let out = "", last = 0;
|
|
22
|
+
for (let m; (m = re.exec(line));) {
|
|
23
|
+
out += paint("plain", line.slice(last, m.index));
|
|
24
|
+
out += m[1] ? paint("comment", m[0]) : m[2] ? paint("string", m[0]) : m[3] ? paint("number", m[0]) : kw.has(m[4]) ? paint("keyword", m[0]) : paint("plain", m[0]);
|
|
25
|
+
last = m.index + m[0].length;
|
|
26
|
+
if (m[0] === "")
|
|
27
|
+
re.lastIndex++;
|
|
28
|
+
}
|
|
29
|
+
return out + paint("plain", line.slice(last));
|
|
30
|
+
}
|
|
31
|
+
const red2 = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
32
|
+
export const defaultSkin = {
|
|
33
|
+
bold, dim, italic: (s) => `\x1b[3m${s}\x1b[23m`, strike: (s) => `\x1b[9m${s}\x1b[29m`, code: cyan, link: (t, u) => `${t} ${dim("(" + u + ")")}`,
|
|
34
|
+
heading(level, text, width) {
|
|
35
|
+
const t = text, out = [level === 1 ? bold(cyan(t)) : level === 2 ? bold(cyan(t)) : bold(t)];
|
|
36
|
+
if (level <= 2)
|
|
37
|
+
out.push(gray("─".repeat(Math.min(width, Math.max(8, cellWidth(strip(t)))))));
|
|
38
|
+
return out;
|
|
39
|
+
},
|
|
40
|
+
quote: (inner) => gray("▎ ") + dim(inner), bullet: (m) => cyan(m), hr: (w) => gray("─".repeat(Math.min(w, 60))),
|
|
41
|
+
hl: (k, s) => (k === "comment" ? gray(s) : k === "string" ? green(s) : k === "number" ? yellow(s) : k === "keyword" ? magenta(s) : s),
|
|
42
|
+
codeBlock(body, lang, width) {
|
|
43
|
+
const out = [];
|
|
44
|
+
if (lang)
|
|
45
|
+
out.push(gray(" ╭─ ") + dim(lang));
|
|
46
|
+
for (const b of body) {
|
|
47
|
+
const styled = lang === "diff" ? (b.startsWith("+") ? green(b) : b.startsWith("-") ? red2(b) : b.startsWith("@@") ? cyan(b) : gray(b)) : highlightWith(b, lang, defaultSkin.hl);
|
|
48
|
+
for (const r of wrapAnsi(styled, width - 4, 2))
|
|
49
|
+
out.push(gray(" │ ") + r);
|
|
50
|
+
}
|
|
51
|
+
if (lang || body.length)
|
|
52
|
+
out.push(gray(" ╰─"));
|
|
53
|
+
return out;
|
|
54
|
+
},
|
|
55
|
+
table(rows, width) {
|
|
56
|
+
const n = Math.max(...rows.map((r) => r.length)), widths = Array.from({ length: n }, (_, i) => Math.max(...rows.map((r) => cellWidth(inline(r[i] ?? "", defaultSkin)))));
|
|
57
|
+
const total = widths.reduce((a, b) => a + b, 0) + 3 * n + 1;
|
|
58
|
+
if (total > width)
|
|
59
|
+
return rows.map((r, i) => (i === 0 ? bold(r.join(" | ")) : r.join(" | "))); // too wide: fall back to plain rows
|
|
60
|
+
const line = (r, head) => "│ " + widths.map((w, i) => { const c = inline(r[i] ?? "", defaultSkin); return (head ? bold(c) : c) + " ".repeat(w - cellWidth(c)); }).join(" │ ") + " │";
|
|
61
|
+
const rule = (l, m, r) => gray(l + widths.map((w) => "─".repeat(w + 2)).join(m) + r);
|
|
62
|
+
return [rule("┌", "┬", "┐"), line(rows[0], true), rule("├", "┼", "┤"), ...rows.slice(1).map((r) => line(r, false)), rule("└", "┴", "┘")].map((l) => (l.startsWith("│") ? gray("│") + l.slice(1) : l));
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
/** Back-compat: highlight with the default skin's colours. */
|
|
66
|
+
export const highlight = (line, lang) => highlightWith(line, lang, defaultSkin.hl);
|
|
67
|
+
/** Inline spans: `code`, **bold**, *italic*, ~~strike~~, [text](url). Code is protected first so nothing inside it is reformatted. */
|
|
68
|
+
export function inline(s, skin = defaultSkin) {
|
|
69
|
+
const codes = [];
|
|
70
|
+
s = s.replace(/`([^`\n]+)`/g, (_m, c) => `\x00${codes.push(skin.code(c)) - 1}\x00`);
|
|
71
|
+
s = s.replace(/\[([^\[\]\n]+)\]\((https?:[^)\s]+)\)/g, (_m, t, u) => skin.link(t, u));
|
|
72
|
+
s = s.replace(/\*\*([^*\n]+)\*\*|__([^_\n]+)__/g, (_m, a, b) => skin.bold(a ?? b));
|
|
73
|
+
s = s.replace(/(?<![\w*])\*([^*\s][^*\n]*?)\*(?![\w*])|(?<![\w_])_([^_\s][^_\n]*?)_(?![\w_])/g, (_m, a, b) => skin.italic(a ?? b));
|
|
74
|
+
s = s.replace(/~~([^~\n]+)~~/g, (_m, t) => skin.strike(t));
|
|
75
|
+
return s.replace(/\x00(\d+)\x00/g, (_m, i) => codes[Number(i)]);
|
|
76
|
+
}
|
|
77
|
+
const isTableSep = (l) => /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(l) && l.includes("-") && l.includes("|");
|
|
78
|
+
const cells = (l) => l.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim());
|
|
79
|
+
export function renderMarkdown(src, width, skin = defaultSkin) {
|
|
80
|
+
const out = [], L = src.replace(/\r\n?/g, "\n").split("\n");
|
|
81
|
+
const push = (line, indent = 0) => out.push(...wrapAnsi(line, width, indent));
|
|
82
|
+
let blank = false;
|
|
83
|
+
for (let i = 0; i < L.length; i++) {
|
|
84
|
+
const line = L[i];
|
|
85
|
+
const fence = line.match(/^(\s*)(`{3,}|~{3,})\s*([\w+-]*)/);
|
|
86
|
+
if (fence) {
|
|
87
|
+
const lang = fence[3], body = [];
|
|
88
|
+
for (i++; i < L.length && !/^\s*(`{3,}|~{3,})\s*$/.test(L[i]); i++)
|
|
89
|
+
body.push(L[i].slice(Math.min(fence[1].length, L[i].length - L[i].trimStart().length)));
|
|
90
|
+
out.push(...skin.codeBlock(body, lang, width));
|
|
91
|
+
blank = false;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!line.trim()) {
|
|
95
|
+
if (!blank && out.length)
|
|
96
|
+
out.push("");
|
|
97
|
+
blank = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
blank = false;
|
|
101
|
+
const h = line.match(/^(#{1,6})\s+(.*?)\s*#*$/);
|
|
102
|
+
if (h) {
|
|
103
|
+
for (const r of skin.heading(h[1].length, inline(h[2], skin), width))
|
|
104
|
+
push(r);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
|
|
108
|
+
out.push(skin.hr(width));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (line.includes("|") && i + 1 < L.length && isTableSep(L[i + 1])) {
|
|
112
|
+
const rows = [cells(line)];
|
|
113
|
+
i += 2;
|
|
114
|
+
for (; i < L.length && L[i].includes("|") && L[i].trim(); i++)
|
|
115
|
+
rows.push(cells(L[i]));
|
|
116
|
+
i--;
|
|
117
|
+
out.push(...skin.table(rows, width));
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const q = line.match(/^\s*>\s?(.*)$/);
|
|
121
|
+
if (q) {
|
|
122
|
+
push(skin.quote(inline(q[1], skin)), 2);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const li = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
|
|
126
|
+
if (li) {
|
|
127
|
+
const depth = Math.floor(li[1].length / 2), marker = /\d/.test(li[2]) ? li[2] : depth % 2 ? "◦" : "•";
|
|
128
|
+
const pad = " ".repeat(depth);
|
|
129
|
+
push(`${pad}${skin.bullet(marker)} ${inline(li[3], skin)}`, pad.length + cellWidth(marker) + 1);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
push(inline(line, skin));
|
|
133
|
+
}
|
|
134
|
+
while (out.length && out[out.length - 1] === "")
|
|
135
|
+
out.pop();
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Split streamed markdown into blocks that are safe to render now (everything up to the last blank line that is not
|
|
140
|
+
* inside a code fence) and the unfinished remainder, which keeps growing.
|
|
141
|
+
*/
|
|
142
|
+
export function takeCompleteBlocks(buf) {
|
|
143
|
+
const lines = buf.split("\n");
|
|
144
|
+
let fence = false, cut = -1;
|
|
145
|
+
for (let i = 0; i < lines.length - 1; i++) { // the last element has no newline yet, so it is never complete
|
|
146
|
+
if (/^\s*(`{3,}|~{3,})/.test(lines[i]))
|
|
147
|
+
fence = !fence;
|
|
148
|
+
if (!fence && !lines[i].trim() && i > 0)
|
|
149
|
+
cut = i;
|
|
150
|
+
}
|
|
151
|
+
if (cut < 0)
|
|
152
|
+
return { done: "", rest: buf };
|
|
153
|
+
return { done: lines.slice(0, cut).join("\n"), rest: lines.slice(cut + 1).join("\n") };
|
|
154
|
+
}
|
|
155
|
+
export { clipWidth };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Image } from "../../src/index.ts";
|
|
2
|
+
export type Attached = {
|
|
3
|
+
path: string;
|
|
4
|
+
lines: number;
|
|
5
|
+
dir?: boolean;
|
|
6
|
+
image?: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** Turn `@path` mentions into attached file contents (the typed text stays as written). Only paths inside `cwd` are read. */
|
|
9
|
+
export declare function expandMentions(text: string, cwd: string, maxChars?: number): {
|
|
10
|
+
text: string;
|
|
11
|
+
attached: Attached[];
|
|
12
|
+
images: Image[];
|
|
13
|
+
problems: string[];
|
|
14
|
+
};
|
|
15
|
+
export declare const attachedNote: (a: Attached[]) => string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve, sep } from "node:path";
|
|
3
|
+
import { MAX_IMAGES, isImagePath, readImage } from "../../src/index.js";
|
|
4
|
+
/** Turn `@path` mentions into attached file contents (the typed text stays as written). Only paths inside `cwd` are read. */
|
|
5
|
+
export function expandMentions(text, cwd, maxChars = 40_000) {
|
|
6
|
+
const root = resolve(cwd), attached = [], images = [], problems = [], blocks = [], seen = new Set();
|
|
7
|
+
for (const m of text.matchAll(/(^|\s)@([^\s@]+)/g)) {
|
|
8
|
+
const rel = m[2].replace(/[.,;:!?)\]]+$/, "");
|
|
9
|
+
if (seen.has(rel))
|
|
10
|
+
continue;
|
|
11
|
+
seen.add(rel);
|
|
12
|
+
const abs = resolve(root, rel);
|
|
13
|
+
if ((abs !== root && !abs.startsWith(root + sep)) || !existsSync(abs))
|
|
14
|
+
continue;
|
|
15
|
+
try {
|
|
16
|
+
if (isImagePath(abs) && statSync(abs).isFile()) {
|
|
17
|
+
if (images.length >= MAX_IMAGES) {
|
|
18
|
+
problems.push(`${rel}: only ${MAX_IMAGES} images per message`);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
images.push(readImage(abs));
|
|
23
|
+
attached.push({ path: rel, lines: 0, image: true });
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
problems.push(e.message);
|
|
27
|
+
}
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (statSync(abs).isDirectory()) {
|
|
31
|
+
const names = readdirSync(abs).sort().slice(0, 80);
|
|
32
|
+
blocks.push(`<directory path="${rel}">\n${names.join("\n")}\n</directory>`);
|
|
33
|
+
attached.push({ path: rel, lines: names.length, dir: true });
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const raw = readFileSync(abs, "utf8");
|
|
37
|
+
if (raw.includes("\0"))
|
|
38
|
+
continue;
|
|
39
|
+
const body = raw.length > maxChars ? raw.slice(0, maxChars) + `\n… [truncated: ${raw.length - maxChars} more characters; use read_file for the rest]` : raw;
|
|
40
|
+
blocks.push(`<file path="${rel}">\n${body}\n</file>`);
|
|
41
|
+
attached.push({ path: rel, lines: raw.split("\n").length - (raw.endsWith("\n") ? 1 : 0) });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch { /* unreadable: leave the mention as plain text */ }
|
|
45
|
+
}
|
|
46
|
+
return { text: blocks.length ? `${text}\n\n<attached>\n${blocks.join("\n")}\n</attached>` : text, attached, images, problems };
|
|
47
|
+
}
|
|
48
|
+
export const attachedNote = (a) => (a.length ? "attached " + a.map((x) => `${x.path} (${x.image ? "image" : x.dir ? `${x.lines} entr${x.lines === 1 ? "y" : "ies"}` : `${x.lines} line${x.lines === 1 ? "" : "s"}`})`).join(", ") : "");
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Canvas } from "./canvas.ts";
|
|
2
|
+
import { type Overlay } from "./overlays.ts";
|
|
3
|
+
import type { Theme } from "./themes.ts";
|
|
4
|
+
export type ModalHit = {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
w: number;
|
|
8
|
+
h: number;
|
|
9
|
+
kind: "option" | "item" | "scrollup" | "scrolldown";
|
|
10
|
+
index: number;
|
|
11
|
+
};
|
|
12
|
+
export type ModalResult = {
|
|
13
|
+
hits: ModalHit[];
|
|
14
|
+
cursor?: {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
};
|
|
18
|
+
box: {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
w: number;
|
|
22
|
+
h: number;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/** Draw whichever overlay is active as a centered modal. Returns clickable regions. */
|
|
26
|
+
export declare function drawModal(c: Canvas, t: Theme, o: Overlay, frame: number): ModalResult | undefined;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Modal dialogs for the full-screen UI: a dimmed backdrop, a shadowed card, real previews, clickable choices.
|
|
2
|
+
import { A, mix } from "./ansi.js";
|
|
3
|
+
import { renderDiffRows, langOf } from "./diffview.js";
|
|
4
|
+
import { renderMarkdown, highlightWith } from "./markdown.js";
|
|
5
|
+
import { ChoiceOverlay, InputOverlay, PickerOverlay, TextOverlay } from "./overlays.js";
|
|
6
|
+
import { BOLD, F, chip, fit, split, themeSkin } from "./rich.js";
|
|
7
|
+
import { cellWidth, clipWidth, wrapAnsi } from "./text.js";
|
|
8
|
+
/** Draw whichever overlay is active as a centered modal. Returns clickable regions. */
|
|
9
|
+
export function drawModal(c, t, o, frame) {
|
|
10
|
+
const dimTo = t.bg === -1 ? 0x000000 : t.bg;
|
|
11
|
+
const card = (w, h, border, title, icon) => {
|
|
12
|
+
const x = Math.floor((c.w - w) / 2), y = Math.max(1, Math.floor((c.h - h) / 2.2));
|
|
13
|
+
c.dim(0, 0, c.w, c.h, 0.62, dimTo, t.text === -1 ? 0xdddddd : t.text, t.bg === -1 ? 0x101010 : t.bg);
|
|
14
|
+
c.fill(x + 1, y + 1, w, h, mix(t.bg === -1 ? 0x101010 : t.bg, 0x000000, 0.55)); // soft shadow
|
|
15
|
+
c.box(x, y, w, h, { border, bg: t.surface, style: "round" });
|
|
16
|
+
if (title)
|
|
17
|
+
c.ansi(x + 2, y, " " + (icon ? F(border, icon) + " " : "") + BOLD(F(t.text, title)) + " ", {}, w - 4);
|
|
18
|
+
return { x, y };
|
|
19
|
+
};
|
|
20
|
+
const hits = [];
|
|
21
|
+
if (o instanceof ChoiceOverlay) {
|
|
22
|
+
const w = Math.min(c.w - 6, 100), inner = w - 4, d = o.detail;
|
|
23
|
+
const tone = o.title.startsWith("Run") && d?.kind === "command" && d.danger ? t.warn : d?.kind === "plan" ? t.accent2 : t.accent;
|
|
24
|
+
const rows = [];
|
|
25
|
+
if (d?.kind === "diff") {
|
|
26
|
+
const r = renderDiffRows(d.diff, { width: inner, theme: t, lang: langOf(d.path), mode: "stacked", maxRows: 14 });
|
|
27
|
+
rows.push(...r.rows);
|
|
28
|
+
if (r.hidden)
|
|
29
|
+
rows.push(fit(F(t.faint, ` … ${r.hidden} more lines`), inner, t.surface2));
|
|
30
|
+
}
|
|
31
|
+
else if (d?.kind === "command") {
|
|
32
|
+
rows.push(fit(" " + F(t.accent, "$ ") + BOLD(F(t.text, clipWidth(d.cmd, inner - 5))), inner, t.surface2));
|
|
33
|
+
if (d.cmd.length > inner - 5)
|
|
34
|
+
for (const l of wrapAnsi(F(t.dim, d.cmd.slice(inner - 5)), inner - 4, 0).slice(0, 4))
|
|
35
|
+
rows.push(fit(" " + l, inner, t.surface2));
|
|
36
|
+
if (d.danger)
|
|
37
|
+
rows.push(fit(" " + F(t.warn, "⚠ " + d.danger), inner, t.surface2));
|
|
38
|
+
}
|
|
39
|
+
else if (d?.kind === "plan") {
|
|
40
|
+
for (const l of renderMarkdown(d.md, inner - 2, themeSkin(t)).slice(0, 16))
|
|
41
|
+
rows.push(fit(" " + l, inner, t.surface));
|
|
42
|
+
}
|
|
43
|
+
else
|
|
44
|
+
for (const l of o.body.slice(0, 12))
|
|
45
|
+
rows.push(fit(" " + l, inner, t.surface));
|
|
46
|
+
const h = Math.min(c.h - 4, rows.length + o.options.length + 6);
|
|
47
|
+
const { x, y } = card(w, h, tone, o.title, d?.kind === "diff" ? "✎" : d?.kind === "command" ? "❯" : d?.kind === "plan" ? "☰" : "?");
|
|
48
|
+
let yy = y + 1;
|
|
49
|
+
for (const r of rows.slice(0, h - o.options.length - 5)) {
|
|
50
|
+
c.ansi(x + 2, yy++, r, { bg: t.surface }, inner);
|
|
51
|
+
}
|
|
52
|
+
yy++;
|
|
53
|
+
c.ansi(x + 2, yy++, BOLD(F(t.text, o.question)), { bg: t.surface }, inner);
|
|
54
|
+
o.options.forEach((opt, i) => {
|
|
55
|
+
const on = i === o.sel, bg = on ? t.selection : t.surface;
|
|
56
|
+
c.ansi(x + 2, yy, fit((on ? F(tone, "▶ ") : " ") + F(on ? tone : t.faint, `${i + 1}`) + " " + (on ? BOLD(F(t.text, opt.label)) : F(t.text, opt.label)) + (opt.hint ? " " + F(t.faint, opt.hint) : ""), inner, bg));
|
|
57
|
+
hits.push({ x: x + 2, y: yy, w: inner, h: 1, kind: "option", index: i });
|
|
58
|
+
yy++;
|
|
59
|
+
});
|
|
60
|
+
c.ansi(x + 2, y + h - 2, F(t.faint, "↑↓ choose · enter confirm · 1-9 quick pick · esc cancel"), { bg: t.surface }, inner);
|
|
61
|
+
return { hits, box: { x, y, w, h } };
|
|
62
|
+
}
|
|
63
|
+
if (o instanceof PickerOverlay) {
|
|
64
|
+
const list = o.visible(), w = Math.min(c.w - 8, 84), inner = w - 4, maxRows = Math.max(3, Math.min(14, c.h - 10, list.length || 1));
|
|
65
|
+
o.sel = Math.max(0, Math.min(o.sel, list.length - 1));
|
|
66
|
+
const top = Math.max(0, Math.min(o.sel - Math.floor(maxRows / 2), list.length - maxRows)), shown = list.slice(top, top + maxRows);
|
|
67
|
+
const h = shown.length + 6, { x, y } = card(w, h, t.accent, o.title, "⌘");
|
|
68
|
+
const field = fit(" " + F(t.accent, "›") + " " + (o.filter ? F(t.text, o.filter) : F(t.faint, "type to filter…")) + F(t.accent, "▌"), inner, t.surface2);
|
|
69
|
+
c.ansi(x + 2, y + 1, split(field.replace(/\s+$/, ""), "", inner, t.surface2), {}, inner);
|
|
70
|
+
c.ansi(x + 2 + inner - 12, y + 1, fit(F(t.faint, `${list.length}/${o.items.length}`.padStart(11)), 12, t.surface2));
|
|
71
|
+
shown.forEach((it, i) => {
|
|
72
|
+
const idx = top + i, on = idx === o.sel, bg = on ? t.selection : t.surface, yy = y + 3 + i;
|
|
73
|
+
c.ansi(x + 2, yy, split((on ? F(t.accent, "▌ ") : " ") + (on ? BOLD(F(t.text, clipWidth(it.label, inner - 30))) : F(t.text, clipWidth(it.label, inner - 30))), it.hint ? F(t.faint, clipWidth(it.hint, Math.max(8, inner - 34))) + " " : "", inner, bg));
|
|
74
|
+
hits.push({ x: x + 2, y: yy, w: inner, h: 1, kind: "item", index: idx });
|
|
75
|
+
});
|
|
76
|
+
if (!shown.length)
|
|
77
|
+
c.ansi(x + 2, y + 3, F(t.faint, " no matches"), { bg: t.surface }, inner);
|
|
78
|
+
if (top > 0)
|
|
79
|
+
c.ansi(x + w - 10, y + 2, F(t.faint, `↑ ${top} more`), { bg: t.surface });
|
|
80
|
+
if (top + maxRows < list.length)
|
|
81
|
+
c.ansi(x + w - 12, y + h - 3, F(t.faint, `↓ ${list.length - top - maxRows} more`), { bg: t.surface });
|
|
82
|
+
c.ansi(x + 2, y + h - 2, F(t.faint, "↑↓ move · enter select · esc cancel · click to choose"), { bg: t.surface }, inner);
|
|
83
|
+
return { hits, box: { x, y, w, h }, cursor: { x: x + 5 + cellWidth(o.filter), y: y + 1 } };
|
|
84
|
+
}
|
|
85
|
+
if (o instanceof InputOverlay) {
|
|
86
|
+
const w = Math.min(c.w - 8, 72), inner = w - 4, h = 7, { x, y } = card(w, h, t.accent, undefined);
|
|
87
|
+
c.ansi(x + 2, y + 1, BOLD(F(t.text, o.prompt.length > inner ? o.prompt.slice(0, inner - 1) + "…" : o.prompt)), { bg: t.surface }, inner);
|
|
88
|
+
const shown = o.secret ? "•".repeat(o.ed.text.length) : o.ed.text.split("\n")[0];
|
|
89
|
+
c.box(x + 2, y + 2, inner, 3, { border: t.accent, bg: t.surface2, style: "round" });
|
|
90
|
+
c.ansi(x + 4, y + 3, F(t.text, clipWidth(shown, inner - 4)), { bg: t.surface2 }, inner - 4);
|
|
91
|
+
c.ansi(x + 2, y + h - 2, F(t.faint, o.hint), { bg: t.surface }, inner);
|
|
92
|
+
return { hits, box: { x, y, w, h }, cursor: { x: x + 4 + Math.min(cellWidth(shown), inner - 5), y: y + 3 } };
|
|
93
|
+
}
|
|
94
|
+
if (o instanceof TextOverlay) {
|
|
95
|
+
const w = Math.min(c.w - 6, 110), inner = w - 4, visible = Math.min(o.lines.length, c.h - 8), h = visible + 4, { x, y } = card(w, h, t.accent2, o.title[0].toUpperCase() + o.title.slice(1), "?");
|
|
96
|
+
const maxScroll = Math.max(0, o.lines.length - visible);
|
|
97
|
+
o.scroll = Math.max(0, Math.min(o.scroll, maxScroll));
|
|
98
|
+
o.lines.slice(o.scroll, o.scroll + visible).forEach((l, i) => c.ansi(x + 2, y + 1 + i, fit(l, inner, t.surface), {}, inner));
|
|
99
|
+
c.ansi(x + 2, y + h - 2, F(t.faint, (maxScroll ? "↑↓ scroll · " : "") + "any other key closes"), { bg: t.surface }, inner);
|
|
100
|
+
return { hits, box: { x, y, w, h } };
|
|
101
|
+
}
|
|
102
|
+
void A;
|
|
103
|
+
void highlightWith;
|
|
104
|
+
void chip;
|
|
105
|
+
void frame;
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Editor } from "./editor.ts";
|
|
2
|
+
import type { Key } from "./keys.ts";
|
|
3
|
+
import { type Option } from "./view.ts";
|
|
4
|
+
export type Drawn = {
|
|
5
|
+
lines: string[];
|
|
6
|
+
cursor?: {
|
|
7
|
+
row: number;
|
|
8
|
+
col: number;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export interface Overlay {
|
|
12
|
+
render(width: number): Drawn;
|
|
13
|
+
key(k: Key): void;
|
|
14
|
+
}
|
|
15
|
+
/** What a permission prompt is about, in a form the full-screen UI can draw properly (the inline UI uses `body`). */
|
|
16
|
+
export type ChoiceDetail = {
|
|
17
|
+
kind: "diff";
|
|
18
|
+
path: string;
|
|
19
|
+
diff: string;
|
|
20
|
+
} | {
|
|
21
|
+
kind: "command";
|
|
22
|
+
cmd: string;
|
|
23
|
+
danger?: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "plan";
|
|
26
|
+
md: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "text";
|
|
29
|
+
text: string;
|
|
30
|
+
};
|
|
31
|
+
/** Standard text-editing keys, shared by the main input and text overlays. Returns true if the key was consumed. */
|
|
32
|
+
export declare function editKey(ed: Editor, k: Key): boolean;
|
|
33
|
+
/** Numbered choices inside a titled box (permission prompts). Resolves with the chosen index, or undefined on Esc. */
|
|
34
|
+
export declare class ChoiceOverlay implements Overlay {
|
|
35
|
+
sel: number;
|
|
36
|
+
title: string;
|
|
37
|
+
body: string[];
|
|
38
|
+
question: string;
|
|
39
|
+
options: Option[];
|
|
40
|
+
done: (i: number | undefined) => void;
|
|
41
|
+
detail?: ChoiceDetail;
|
|
42
|
+
constructor(title: string, body: string[], question: string, options: Option[], done: (i: number | undefined) => void, paint?: (s: string) => string, detail?: ChoiceDetail);
|
|
43
|
+
paint?: (s: string) => string;
|
|
44
|
+
render(width: number): Drawn;
|
|
45
|
+
key(k: Key): void;
|
|
46
|
+
}
|
|
47
|
+
export type PickItem<T> = {
|
|
48
|
+
label: string;
|
|
49
|
+
value: T;
|
|
50
|
+
hint?: string;
|
|
51
|
+
};
|
|
52
|
+
/** Filterable list. Type to filter, ↑/↓ to move, enter to pick, esc to cancel. */
|
|
53
|
+
export declare class PickerOverlay<T> implements Overlay {
|
|
54
|
+
sel: number;
|
|
55
|
+
filter: string;
|
|
56
|
+
title: string;
|
|
57
|
+
items: PickItem<T>[];
|
|
58
|
+
done: (v: T | undefined) => void;
|
|
59
|
+
max: number;
|
|
60
|
+
constructor(title: string, items: PickItem<T>[], done: (v: T | undefined) => void, initial?: number, max?: number);
|
|
61
|
+
visible(): PickItem<T>[];
|
|
62
|
+
/** Called whenever the highlighted item changes (live theme preview). */
|
|
63
|
+
onMove?: (v: T | undefined) => void;
|
|
64
|
+
/** Choose the i-th visible item (mouse click). */
|
|
65
|
+
pick(i: number): void;
|
|
66
|
+
private moved;
|
|
67
|
+
render(width: number): Drawn;
|
|
68
|
+
key(k: Key): void;
|
|
69
|
+
}
|
|
70
|
+
/** One-shot text prompt ("what should it do instead?", API key). Esc cancels. */
|
|
71
|
+
export declare class InputOverlay implements Overlay {
|
|
72
|
+
ed: Editor;
|
|
73
|
+
prompt: string;
|
|
74
|
+
secret: boolean;
|
|
75
|
+
done: (v: string | undefined) => void;
|
|
76
|
+
hint: string;
|
|
77
|
+
constructor(prompt: string, done: (v: string | undefined) => void, o?: {
|
|
78
|
+
secret?: boolean;
|
|
79
|
+
initial?: string;
|
|
80
|
+
hint?: string;
|
|
81
|
+
});
|
|
82
|
+
render(width: number): Drawn;
|
|
83
|
+
key(k: Key): void;
|
|
84
|
+
}
|
|
85
|
+
/** Static text (help) closed by any key. */
|
|
86
|
+
export declare class TextOverlay implements Overlay {
|
|
87
|
+
lines: string[];
|
|
88
|
+
done: () => void;
|
|
89
|
+
scroll: number;
|
|
90
|
+
title: string;
|
|
91
|
+
constructor(lines: string[], done: () => void, title?: string);
|
|
92
|
+
render(): Drawn;
|
|
93
|
+
key(k?: Key): void;
|
|
94
|
+
}
|
|
95
|
+
export declare const helpOverlay: (cmds: {
|
|
96
|
+
name: string;
|
|
97
|
+
help: string;
|
|
98
|
+
}[], done: () => void, width: number) => TextOverlay;
|