mini-coder 0.7.3 → 0.8.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/AGENTS.md +114 -0
- package/README.md +53 -66
- package/bin/mini-coder.ts +2 -0
- package/demo.gif +0 -0
- package/package.json +17 -20
- package/src/agent.ts +177 -255
- package/src/cli.ts +101 -0
- package/src/config.ts +150 -0
- package/src/prompt.ts +54 -207
- package/src/session.ts +124 -69
- package/src/tools/bash.ts +89 -0
- package/src/tools/common.ts +32 -0
- package/src/tools/edit.ts +41 -0
- package/src/tools/index.ts +47 -0
- package/src/tools/read.ts +64 -0
- package/src/tui/commands.ts +63 -0
- package/src/tui/editor.ts +291 -0
- package/src/tui/highlight.ts +189 -0
- package/src/tui/stream.ts +142 -0
- package/src/tui/styles.ts +42 -0
- package/src/tui/term.ts +436 -0
- package/src/tui/theme.ts +121 -0
- package/src/tui/tui.ts +595 -0
- package/src/tui/usage.ts +67 -0
- package/tsconfig.json +8 -8
- package/bin/mc.ts +0 -11
- package/bun.lock +0 -346
- package/nono-mini-coder.json +0 -42
- package/src/args.ts +0 -300
- package/src/error-handling.test.ts +0 -163
- package/src/git.ts +0 -23
- package/src/headless.ts +0 -66
- package/src/index.ts +0 -43
- package/src/oauth.ts +0 -157
- package/src/shared.ts +0 -119
- package/src/themes.ts +0 -234
- package/src/tool-bash.ts +0 -77
- package/src/tool-edit.ts +0 -121
- package/src/tool-read.ts +0 -100
- package/src/tui-components.ts +0 -127
- package/src/tui-conversation.ts +0 -218
- package/src/tui-editor.ts +0 -29
- package/src/tui-overlay.ts +0 -618
- package/src/tui.ts +0 -314
- package/src/types.ts +0 -194
- package/src/update.ts +0 -171
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import Parser from "tree-sitter";
|
|
5
|
+
import JavaScript from "tree-sitter-javascript";
|
|
6
|
+
import TypeScript from "tree-sitter-typescript";
|
|
7
|
+
import Markdown from "@tree-sitter-grammars/tree-sitter-markdown";
|
|
8
|
+
import { HEADINGS, INLINE_CODE, NORMAL_BG, NORMAL_FG, STYLES, channels, sgrPlain, type Style } from "./theme.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Syntax highlighting for committed scrollback, once, over the whole block.
|
|
12
|
+
* Grammar queries ship with the grammar packages and are consumed as data, so
|
|
13
|
+
* a capture name is the only thing that reaches the palette. The colours come
|
|
14
|
+
* from the TokyoNight `night` palette in `theme.ts`, mapped the way
|
|
15
|
+
* `folke/tokyonight.nvim` maps capture names to highlight groups. Truecolor only.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
function styleFor(capture: string): Style | undefined {
|
|
19
|
+
for (let name = capture; ; name = name.slice(0, name.lastIndexOf("."))) {
|
|
20
|
+
const style = STYLES[name];
|
|
21
|
+
if (style !== undefined) return style;
|
|
22
|
+
if (!name.includes(".")) return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
|
|
28
|
+
/** A grammar package keeps its queries next to its sources. */
|
|
29
|
+
function querySource(pkg: string, ...path: string[]): string {
|
|
30
|
+
return readFileSync(join(dirname(require.resolve(`${pkg}/package.json`)), ...path), "utf8");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Syntax {
|
|
34
|
+
parser: Parser;
|
|
35
|
+
query: Parser.Query;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function syntax(language: Parser.Language, ...sources: string[]): Syntax {
|
|
39
|
+
const parser = new Parser();
|
|
40
|
+
parser.setLanguage(language);
|
|
41
|
+
return { parser, query: new Parser.Query(language, sources.join("\n")) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const JS_QUERIES = querySource("tree-sitter-javascript", "queries", "highlights.scm");
|
|
45
|
+
const TS_QUERIES = querySource("tree-sitter-typescript", "queries", "highlights.scm");
|
|
46
|
+
|
|
47
|
+
const JAVASCRIPT = syntax(JavaScript, JS_QUERIES);
|
|
48
|
+
/** TypeScript's own query only covers what it adds to JavaScript. */
|
|
49
|
+
const TYPESCRIPT = syntax(TypeScript.typescript, JS_QUERIES, TS_QUERIES);
|
|
50
|
+
const TSX = syntax(TypeScript.tsx, JS_QUERIES, TS_QUERIES);
|
|
51
|
+
|
|
52
|
+
/** Fence info string to grammar, for the languages we carry. */
|
|
53
|
+
const CODE: Record<string, Syntax | undefined> = {
|
|
54
|
+
js: JAVASCRIPT,
|
|
55
|
+
javascript: JAVASCRIPT,
|
|
56
|
+
jsx: JAVASCRIPT,
|
|
57
|
+
ts: TYPESCRIPT,
|
|
58
|
+
typescript: TYPESCRIPT,
|
|
59
|
+
tsx: TSX,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const MARKDOWN = syntax(
|
|
63
|
+
Markdown,
|
|
64
|
+
querySource("@tree-sitter-grammars/tree-sitter-markdown", "tree-sitter-markdown", "queries", "highlights.scm"),
|
|
65
|
+
);
|
|
66
|
+
const MARKDOWN_INLINE = syntax(
|
|
67
|
+
Markdown.inline,
|
|
68
|
+
querySource(
|
|
69
|
+
"@tree-sitter-grammars/tree-sitter-markdown",
|
|
70
|
+
"tree-sitter-markdown-inline",
|
|
71
|
+
"queries",
|
|
72
|
+
"highlights.scm",
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
/** One styled range of a line, in byte offsets into that line. */
|
|
77
|
+
interface Span {
|
|
78
|
+
start: number;
|
|
79
|
+
end: number;
|
|
80
|
+
style: Style;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function spansOf(syntax: Syntax, node: Parser.SyntaxNode, offset = 0): Span[] {
|
|
84
|
+
const spans: Span[] = [];
|
|
85
|
+
for (const capture of syntax.query.captures(node)) {
|
|
86
|
+
const style = styleFor(capture.name);
|
|
87
|
+
if (style === undefined) continue;
|
|
88
|
+
spans.push({ start: offset + capture.node.startIndex, end: offset + capture.node.endIndex, style });
|
|
89
|
+
}
|
|
90
|
+
return spans;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parse(syntax: Syntax, text: string): Parser.SyntaxNode {
|
|
94
|
+
return syntax.parser.parse(text).rootNode;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A style's own attributes, with the defaults spelled out: a span that names no
|
|
99
|
+
* color must not inherit the color of the span or token before it, and the
|
|
100
|
+
* fallback is the palette's `Normal`, never the terminal's own pair.
|
|
101
|
+
*/
|
|
102
|
+
function sgr(style: Style): string {
|
|
103
|
+
let out = "\x1b[";
|
|
104
|
+
if (style.bold) out += "1;";
|
|
105
|
+
if (style.italic) out += "3;";
|
|
106
|
+
if (style.underline) out += "4;";
|
|
107
|
+
out += `38;2;${channels(style.fg ?? NORMAL_FG)};48;2;${channels(style.bg ?? NORMAL_BG)}m`;
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Wraps every span in its color. Spans nest, so the innermost one wins; text
|
|
113
|
+
* outside any span falls back to `Normal`. The trailing close restores the same
|
|
114
|
+
* pair, so a span may cross a line break and the last line still ends plain —
|
|
115
|
+
* and no cell is ever left to the terminal. Because each row is painted on its
|
|
116
|
+
* own, a span that outlives a line break re-asserts itself on the next line.
|
|
117
|
+
*/
|
|
118
|
+
function paint(text: string, spans: Span[]): string {
|
|
119
|
+
const events: { at: number; open: boolean; span: Span }[] = [];
|
|
120
|
+
for (const span of spans) {
|
|
121
|
+
if (span.end <= span.start) continue;
|
|
122
|
+
events.push({ at: span.start, open: true, span }, { at: span.end, open: false, span });
|
|
123
|
+
}
|
|
124
|
+
events.sort((a, b) => a.at - b.at || Number(a.open) - Number(b.open));
|
|
125
|
+
|
|
126
|
+
const active: Span[] = [];
|
|
127
|
+
let out = "";
|
|
128
|
+
let plain = true;
|
|
129
|
+
let at = 0;
|
|
130
|
+
const emit = (end: number): void => {
|
|
131
|
+
if (end <= at) return;
|
|
132
|
+
const style = active.length === 0 ? undefined : active[active.length - 1].style;
|
|
133
|
+
if (style === undefined) {
|
|
134
|
+
if (!plain) out += sgrPlain();
|
|
135
|
+
plain = true;
|
|
136
|
+
} else {
|
|
137
|
+
out += sgr(style);
|
|
138
|
+
plain = false;
|
|
139
|
+
}
|
|
140
|
+
const chunk = text.slice(at, end);
|
|
141
|
+
out += style === undefined ? chunk : chunk.replace(/\n/g, `\n${sgr(style)}`);
|
|
142
|
+
at = end;
|
|
143
|
+
};
|
|
144
|
+
for (const event of events) {
|
|
145
|
+
emit(event.at);
|
|
146
|
+
if (event.open) active.push(event.span);
|
|
147
|
+
else active.splice(active.indexOf(event.span), 1);
|
|
148
|
+
}
|
|
149
|
+
emit(text.length);
|
|
150
|
+
return plain ? out : `${out}${sgrPlain()}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Colors one committed block of Markdown. Inline markup is a separate grammar,
|
|
155
|
+
* run over the ranges the block grammar hands over, as its injections do.
|
|
156
|
+
*/
|
|
157
|
+
export function highlightMarkdown(text: string): string {
|
|
158
|
+
const root = parse(MARKDOWN, text);
|
|
159
|
+
let spans = spansOf(MARKDOWN, root);
|
|
160
|
+
// Two things the grammar's queries predate: a whole heading is colored by its
|
|
161
|
+
// level, and inline code carries a background. Both replace what covers them.
|
|
162
|
+
for (const inline of root.descendantsOfType("inline")) {
|
|
163
|
+
const inlineRoot = parse(MARKDOWN_INLINE, inline.text);
|
|
164
|
+
spans.push(...spansOf(MARKDOWN_INLINE, inlineRoot, inline.startIndex));
|
|
165
|
+
for (const code of inlineRoot.descendantsOfType("code_span")) {
|
|
166
|
+
spans = recolor(spans, code, INLINE_CODE, inline.startIndex);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const heading of root.descendantsOfType(["atx_heading", "setext_heading"])) {
|
|
170
|
+
const marker = heading.namedChildren.find((child) => /^(atx|setext)_h/.test(child.type))?.type;
|
|
171
|
+
const level = marker === undefined ? 1 : Number(marker.replace(/\D/g, ""));
|
|
172
|
+
spans = recolor(spans, heading, HEADINGS[Math.min(level, HEADINGS.length) - 1]);
|
|
173
|
+
}
|
|
174
|
+
return paint(text, spans);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Replaces every span inside `node` with one span covering the whole node. */
|
|
178
|
+
function recolor(spans: Span[], node: Parser.SyntaxNode, style: Style, offset = 0): Span[] {
|
|
179
|
+
const start = offset + node.startIndex;
|
|
180
|
+
const end = offset + node.endIndex;
|
|
181
|
+
const kept = spans.filter((span) => span.start < start || span.end > end);
|
|
182
|
+
return [...kept, { start, end, style }];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Colors a fenced code block's body, by its fence info string. */
|
|
186
|
+
export function highlightCode(info: string, text: string): string {
|
|
187
|
+
const grammar = CODE[info.trim().toLowerCase()];
|
|
188
|
+
return grammar === undefined ? text : paint(text, spansOf(grammar, parse(grammar, text)));
|
|
189
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { highlightCode, highlightMarkdown } from "./highlight.ts";
|
|
2
|
+
import { dim } from "./styles.ts";
|
|
3
|
+
|
|
4
|
+
/** One logical display line: unwrapped text plus the style its rows inherit. */
|
|
5
|
+
export interface BodyLine {
|
|
6
|
+
text: string;
|
|
7
|
+
style?: (text: string) => string;
|
|
8
|
+
/** Row background, painted across the row's trailing cells by the renderer. */
|
|
9
|
+
bg?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A text stream projected onto display lines. `feed` and `flush` return the
|
|
14
|
+
* lines that became final; `pending` returns what is still in flight.
|
|
15
|
+
*/
|
|
16
|
+
export interface StreamRenderer {
|
|
17
|
+
feed(delta: string): BodyLine[];
|
|
18
|
+
pending(): BodyLine[];
|
|
19
|
+
flush(): BodyLine[];
|
|
20
|
+
reset(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A fence opening line, and the info string that names its language. */
|
|
24
|
+
const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})\s*(\S*)/;
|
|
25
|
+
const FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})\s*$/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Commits each complete line, colored. A fenced code block is held until its
|
|
29
|
+
* closing marker arrives, because neither the fence's language nor its body can
|
|
30
|
+
* be known before then. A run of prose is held until its block ends, because
|
|
31
|
+
* block context is what separates a setext heading from a paragraph and spans
|
|
32
|
+
* may cross lines; a blank line, a fence or the end of the turn ends the block.
|
|
33
|
+
* The live preview shows the held lines uncolored.
|
|
34
|
+
*/
|
|
35
|
+
export class MarkdownStream implements StreamRenderer {
|
|
36
|
+
private rest = "";
|
|
37
|
+
private fence: { marker: string; info: string; lines: string[] } | null = null;
|
|
38
|
+
private prose: string[] = [];
|
|
39
|
+
|
|
40
|
+
feed(delta: string): BodyLine[] {
|
|
41
|
+
this.rest += delta;
|
|
42
|
+
const lines = this.rest.split("\n");
|
|
43
|
+
this.rest = lines.pop()!;
|
|
44
|
+
return lines.flatMap((line) => this.commit(line));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pending(): BodyLine[] {
|
|
48
|
+
const held = this.fence === null ? this.prose : this.fence.lines;
|
|
49
|
+
const rest = this.rest === "" ? [] : [this.rest];
|
|
50
|
+
return [...held, ...rest].map((text) => ({ text }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
flush(): BodyLine[] {
|
|
54
|
+
const fence = this.fence;
|
|
55
|
+
const lines = fence === null ? this.releaseProse(this.rest) : this.releaseFence(null);
|
|
56
|
+
if (fence !== null && this.rest !== "") lines.push({ text: highlightMarkdown(this.rest) });
|
|
57
|
+
this.reset();
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
reset(): void {
|
|
62
|
+
this.rest = "";
|
|
63
|
+
this.fence = null;
|
|
64
|
+
this.prose = [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private commit(line: string): BodyLine[] {
|
|
68
|
+
const fence = this.fence;
|
|
69
|
+
if (fence !== null) {
|
|
70
|
+
const close = FENCE_CLOSE.exec(line);
|
|
71
|
+
if (close === null || close[1][0] !== fence.marker[0] || close[1].length < fence.marker.length) {
|
|
72
|
+
fence.lines.push(line);
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
return this.releaseFence(line);
|
|
76
|
+
}
|
|
77
|
+
const open = FENCE_OPEN.exec(line);
|
|
78
|
+
if (open !== null) {
|
|
79
|
+
this.fence = { marker: open[1], info: open[2], lines: [line] };
|
|
80
|
+
return this.releaseProse("");
|
|
81
|
+
}
|
|
82
|
+
if (line.trim() === "") return [...this.releaseProse(""), { text: line }];
|
|
83
|
+
this.prose.push(line);
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Emits the held prose as one text, so the grammar sees whole blocks, then
|
|
89
|
+
* hands back the lines. `tail` is the still incomplete line closing the block.
|
|
90
|
+
*/
|
|
91
|
+
private releaseProse(tail: string): BodyLine[] {
|
|
92
|
+
const prose = this.prose;
|
|
93
|
+
this.prose = [];
|
|
94
|
+
if (tail !== "") prose.push(tail);
|
|
95
|
+
if (prose.length === 0) return [];
|
|
96
|
+
// A block is only a block once its last line ends, so the text needs a
|
|
97
|
+
// terminating newline. It is not a line itself: the closing reset `paint`
|
|
98
|
+
// leaves after it belongs to the last one.
|
|
99
|
+
const lines = highlightMarkdown(`${prose.join("\n")}\n`).split("\n");
|
|
100
|
+
const reset = lines.pop()!;
|
|
101
|
+
lines[lines.length - 1] += reset;
|
|
102
|
+
return lines.map((text) => ({ text }));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Emits a held block: the fence lines colored, the body by its language. */
|
|
106
|
+
private releaseFence(closing: string | null): BodyLine[] {
|
|
107
|
+
const fence = this.fence!;
|
|
108
|
+
this.fence = null;
|
|
109
|
+
const lines: BodyLine[] = [{ text: highlightMarkdown(fence.lines.shift()!) }];
|
|
110
|
+
if (fence.lines.length > 0) {
|
|
111
|
+
for (const line of highlightCode(fence.info, fence.lines.join("\n")).split("\n")) {
|
|
112
|
+
lines.push({ text: line });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (closing !== null) lines.push({ text: highlightMarkdown(closing) });
|
|
116
|
+
return lines;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Keeps only the incomplete tail, dimmed; nothing is ever committed. */
|
|
121
|
+
export class TailStream implements StreamRenderer {
|
|
122
|
+
private rest = "";
|
|
123
|
+
|
|
124
|
+
feed(delta: string): BodyLine[] {
|
|
125
|
+
const text = this.rest + delta;
|
|
126
|
+
this.rest = text.slice(text.lastIndexOf("\n") + 1);
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
pending(): BodyLine[] {
|
|
131
|
+
return this.rest === "" ? [] : [{ text: this.rest, style: dim }];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
flush(): BodyLine[] {
|
|
135
|
+
this.reset();
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
reset(): void {
|
|
140
|
+
this.rest = "";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { NORMAL_FG, PALETTE, sgrFg } from "./theme.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inline foreground styling from the palette. The foreground is restored by
|
|
5
|
+
* re-asserting `Normal`'s, never by resetting: the row's background, set by the
|
|
6
|
+
* renderer, must survive, and no cell may fall back to the terminal's colours.
|
|
7
|
+
*/
|
|
8
|
+
function foreground(text: string, hex: string): string {
|
|
9
|
+
return `${sgrFg(hex)}${text}${sgrFg(NORMAL_FG)}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** `Comment`, the colour diff context lines share. */
|
|
13
|
+
export function dim(text: string): string {
|
|
14
|
+
return foreground(text, PALETTE.comment);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function red(text: string): string {
|
|
18
|
+
return foreground(text, PALETTE.red);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function green(text: string): string {
|
|
22
|
+
return foreground(text, PALETTE.green);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function yellow(text: string): string {
|
|
26
|
+
return foreground(text, PALETTE.yellow);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Diff hunk headers; the palette's `blue` sits closest to the old ANSI cyan. */
|
|
30
|
+
export function cyan(text: string): string {
|
|
31
|
+
return foreground(text, PALETTE.blue);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The user's own words; also `Function`, which user rows never collide with. */
|
|
35
|
+
export function blue(text: string): string {
|
|
36
|
+
return foreground(text, PALETTE.blue);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The tool accent: call-line heads. */
|
|
40
|
+
export function teal(text: string): string {
|
|
41
|
+
return foreground(text, PALETTE.teal);
|
|
42
|
+
}
|