mini-coder 0.5.14 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -109
- package/bin/mc.ts +8 -11
- package/bun.lock +79 -269
- package/nono-mini-coder.json +42 -0
- package/package.json +17 -22
- package/src/agent.ts +243 -1403
- package/src/args.ts +289 -0
- package/src/headless.ts +41 -359
- package/src/index.ts +29 -1016
- package/src/oauth.ts +117 -0
- package/src/prompt.ts +219 -284
- package/src/session.ts +55 -1306
- package/src/shared.ts +117 -38
- package/src/tool-bash.ts +110 -0
- package/src/tool-edit.ts +133 -0
- package/src/tool-read.ts +80 -293
- package/src/tui-components.ts +150 -0
- package/src/tui-conversation.ts +271 -0
- package/src/tui-editor.ts +29 -0
- package/src/tui-overlay.ts +403 -0
- package/src/tui.ts +228 -0
- package/src/types.ts +164 -0
- package/tsconfig.json +17 -0
- package/BENCHMARK.md +0 -107
- package/LICENSE +0 -9
- package/PROGRESS.md +0 -5
- package/assets/icon-1-minimal.svg +0 -31
- package/assets/icon-2-dark-terminal.svg +0 -48
- package/assets/icon-3-gradient-modern.svg +0 -45
- package/assets/icon-4-filled-bold.svg +0 -54
- package/assets/icon-5-community-badge.svg +0 -63
- package/assets/mc-claude-smart.png +0 -0
- package/assets/mc-gpt-smart.png +0 -0
- package/assets/preview-0-5-0.png +0 -0
- package/assets/preview.gif +0 -0
- package/benchmark-baseline.sh +0 -15
- package/benchmark-loop.sh +0 -19
- package/skills-lock.json +0 -15
- package/src/assistant-output.ts +0 -73
- package/src/cli.ts +0 -134
- package/src/delegation.ts +0 -238
- package/src/errors.ts +0 -15
- package/src/git.ts +0 -247
- package/src/input.ts +0 -168
- package/src/mcp.ts +0 -609
- package/src/paths.ts +0 -37
- package/src/session-message.ts +0 -385
- package/src/settings.ts +0 -449
- package/src/skills.ts +0 -271
- package/src/submit.ts +0 -376
- package/src/text.ts +0 -71
- package/src/theme.ts +0 -330
- package/src/tool-common.ts +0 -93
- package/src/tool-delegate.ts +0 -125
- package/src/tool-grep.ts +0 -606
- package/src/tool-shell.ts +0 -1051
- package/src/tools.ts +0 -1179
- package/src/ui/agent.ts +0 -320
- package/src/ui/commands.test.ts +0 -957
- package/src/ui/commands.ts +0 -848
- package/src/ui/conversation.test.ts +0 -585
- package/src/ui/conversation.ts +0 -1836
- package/src/ui/help.ts +0 -158
- package/src/ui/input.test.ts +0 -64
- package/src/ui/input.ts +0 -138
- package/src/ui/overlay.ts +0 -59
- package/src/ui/runtime.ts +0 -69
- package/src/ui/status.ts +0 -220
- package/src/ui.ts +0 -1190
- package/src/version.ts +0 -48
package/src/tool-read.ts
CHANGED
|
@@ -1,313 +1,100 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
validateBuiltinToolArgs,
|
|
16
|
-
} from "./tool-common.ts";
|
|
17
|
-
|
|
18
|
-
const readToolParameters = Type.Object({
|
|
19
|
-
path: Type.String({
|
|
20
|
-
description: "File path (absolute or relative to cwd)",
|
|
21
|
-
}),
|
|
22
|
-
offset: Type.Optional(
|
|
23
|
-
Type.Integer({
|
|
24
|
-
minimum: 0,
|
|
25
|
-
description: "Zero-based line offset to start reading from",
|
|
26
|
-
}),
|
|
27
|
-
),
|
|
28
|
-
limit: Type.Optional(
|
|
29
|
-
Type.Integer({
|
|
30
|
-
minimum: 1,
|
|
31
|
-
description: "Maximum number of lines to read in this call",
|
|
32
|
-
}),
|
|
33
|
-
),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
/** Default line window returned when `limit` is omitted. */
|
|
37
|
-
export const DEFAULT_READ_LIMIT = 200;
|
|
38
|
-
|
|
39
|
-
/** Number of logical lines to append between progressive UI updates. */
|
|
40
|
-
const READ_STREAM_CHUNK_LINES = 40;
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { extname, isAbsolute, join } from "node:path";
|
|
3
|
+
import { type Tool, Type } from "@mariozechner/pi-ai";
|
|
4
|
+
import type { ToolRunnerEvent } from "./types";
|
|
5
|
+
|
|
6
|
+
const imageMimeTypes: Record<string, string> = {
|
|
7
|
+
".bmp": "image/bmp",
|
|
8
|
+
".gif": "image/gif",
|
|
9
|
+
".jpeg": "image/jpeg",
|
|
10
|
+
".jpg": "image/jpeg",
|
|
11
|
+
".png": "image/png",
|
|
12
|
+
".svg": "image/svg+xml",
|
|
13
|
+
".webp": "image/webp",
|
|
14
|
+
};
|
|
41
15
|
|
|
42
|
-
|
|
43
|
-
export type ReadArgs = Static<typeof readToolParameters>;
|
|
16
|
+
const description = `## Read tool
|
|
44
17
|
|
|
45
|
-
|
|
46
|
-
export interface ReadContinuationHint {
|
|
47
|
-
/** Next zero-based line offset to request. */
|
|
48
|
-
offset: number;
|
|
49
|
-
/** Suggested line limit for the follow-up read. */
|
|
50
|
-
limit: number;
|
|
51
|
-
}
|
|
18
|
+
Read a file by path.
|
|
52
19
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
signal?: AbortSignal;
|
|
57
|
-
/** Callback for progressive content updates. */
|
|
58
|
-
onUpdate?: ToolUpdateCallback;
|
|
59
|
-
}
|
|
20
|
+
For text files, returns line-numbered text. Use \`offset\` and \`limit\` to read a specific range.
|
|
21
|
+
You can also read images files.
|
|
22
|
+
`;
|
|
60
23
|
|
|
61
|
-
|
|
62
|
-
export const readTool: Tool<typeof readToolParameters> = {
|
|
24
|
+
export const read: Tool = {
|
|
63
25
|
name: "read",
|
|
64
|
-
description
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
26
|
+
description,
|
|
27
|
+
parameters: Type.Object({
|
|
28
|
+
path: Type.String({
|
|
29
|
+
description:
|
|
30
|
+
"File path. Absolute or relative to the current working directory.",
|
|
31
|
+
}),
|
|
32
|
+
offset: Type.Optional(
|
|
33
|
+
Type.Number({
|
|
34
|
+
description:
|
|
35
|
+
"Optional 1-based line number to start reading from. Text files only.",
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
limit: Type.Optional(
|
|
39
|
+
Type.Number({
|
|
40
|
+
description:
|
|
41
|
+
"Optional maximum number of lines to read. Text files only.",
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
}),
|
|
69
45
|
};
|
|
70
46
|
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
for (let index = 0; index < content.length; index++) {
|
|
84
|
-
const char = content[index];
|
|
85
|
-
if (char === "\n") {
|
|
86
|
-
lines.push(content.slice(start, index + 1));
|
|
87
|
-
start = index + 1;
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (char === "\r" && content[index + 1] === "\n") {
|
|
91
|
-
lines.push(content.slice(start, index + 2));
|
|
92
|
-
start = index + 2;
|
|
93
|
-
index += 1;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (start < content.length) {
|
|
98
|
-
lines.push(content.slice(start));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return lines;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function waitForNextTick(): Promise<void> {
|
|
105
|
-
return new Promise((resolve) => setImmediate(resolve));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Format the trailing continuation hint appended to truncated read results. */
|
|
109
|
-
export function formatReadContinuationHint(
|
|
110
|
-
offset: number,
|
|
111
|
-
limit: number,
|
|
112
|
-
): string {
|
|
113
|
-
return `[use offset=${offset} limit=${limit} to continue]`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Parse a standalone read continuation hint block. */
|
|
117
|
-
export function parseReadContinuationHint(
|
|
118
|
-
text: string,
|
|
119
|
-
): ReadContinuationHint | null {
|
|
120
|
-
const match = /^\[use offset=(\d+) limit=(\d+) to continue\]$/.exec(text);
|
|
121
|
-
if (!match) {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const offsetText = match[1];
|
|
126
|
-
const limitText = match[2];
|
|
127
|
-
if (!offsetText || !limitText) {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
offset: Number.parseInt(offsetText, 10),
|
|
133
|
-
limit: Number.parseInt(limitText, 10),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Parse a legacy flattened read result into file content plus any continuation hint. */
|
|
138
|
-
export function parseReadResult(text: string): {
|
|
139
|
-
body: string;
|
|
140
|
-
continuation: ReadContinuationHint | null;
|
|
141
|
-
} {
|
|
142
|
-
const trailerMatch =
|
|
143
|
-
/(?:\r?\n){2}(\[use offset=\d+ limit=\d+ to continue\])$/.exec(text);
|
|
144
|
-
if (!trailerMatch) {
|
|
145
|
-
return { body: text, continuation: null };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const continuation = parseReadContinuationHint(trailerMatch[1] ?? "");
|
|
149
|
-
if (!continuation) {
|
|
150
|
-
return { body: text, continuation: null };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
body: text.slice(0, trailerMatch.index),
|
|
155
|
-
continuation,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function streamReadPreview(
|
|
160
|
-
lines: readonly string[],
|
|
161
|
-
opts: ReadOpts | undefined,
|
|
162
|
-
): Promise<void> {
|
|
163
|
-
if (!opts?.onUpdate || lines.length === 0) {
|
|
47
|
+
export async function* runReadTool(
|
|
48
|
+
args: Record<string, any>,
|
|
49
|
+
signal?: AbortSignal,
|
|
50
|
+
): AsyncGenerator<ToolRunnerEvent> {
|
|
51
|
+
const filePath = isAbsolute(args.path)
|
|
52
|
+
? args.path
|
|
53
|
+
: join(process.cwd(), args.path);
|
|
54
|
+
const file = Bun.file(filePath);
|
|
55
|
+
const exists = await file.exists();
|
|
56
|
+
|
|
57
|
+
if (!exists) {
|
|
58
|
+
yield { type: "result", text: `File not found: ${filePath}` };
|
|
164
59
|
return;
|
|
165
60
|
}
|
|
166
61
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
opts.onUpdate(textResult(lines.slice(0, index).join(""), false));
|
|
174
|
-
await waitForNextTick();
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function loadReadContent(
|
|
179
|
-
args: ReadArgs,
|
|
180
|
-
cwd: string,
|
|
181
|
-
): ToolExecResult | { content: string } {
|
|
182
|
-
const filePath = resolveReadPath(args.path, cwd);
|
|
183
|
-
if (!existsSync(filePath)) {
|
|
184
|
-
return textResult(`File not found: ${args.path}`, true);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
if (!statSync(filePath).isFile()) {
|
|
189
|
-
return textResult(`Not a file: ${args.path}`, true);
|
|
190
|
-
}
|
|
191
|
-
} catch (error) {
|
|
192
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
193
|
-
return textResult(`Failed to stat ${args.path}: ${message}`, true);
|
|
62
|
+
if (signal?.aborted) {
|
|
63
|
+
yield { type: "result", text: "Aborted before read." };
|
|
64
|
+
return;
|
|
194
65
|
}
|
|
195
66
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
67
|
+
const mimeType = imageMimeTypes[extname(filePath).toLowerCase()] ?? file.type;
|
|
68
|
+
if (mimeType.startsWith("image/")) {
|
|
69
|
+
const data = Buffer.from(await file.arrayBuffer()).toString("base64");
|
|
70
|
+
const text = `Image read: ${filePath}\nMIME type: ${mimeType}\nSize: ${file.size} bytes`;
|
|
71
|
+
yield {
|
|
72
|
+
type: "result",
|
|
73
|
+
text,
|
|
74
|
+
image: { data, mimeType },
|
|
199
75
|
};
|
|
200
|
-
|
|
201
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
202
|
-
return textResult(`Failed to read ${args.path}: ${message}`, true);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function getReadSlice(
|
|
207
|
-
args: ReadArgs,
|
|
208
|
-
content: string,
|
|
209
|
-
):
|
|
210
|
-
| ToolExecResult
|
|
211
|
-
| {
|
|
212
|
-
lines: string[];
|
|
213
|
-
offset: number;
|
|
214
|
-
limit: number;
|
|
215
|
-
} {
|
|
216
|
-
const lines = splitLinesPreservingEndings(content);
|
|
217
|
-
const offset = args.offset ?? 0;
|
|
218
|
-
const limit = args.limit ?? DEFAULT_READ_LIMIT;
|
|
219
|
-
|
|
220
|
-
if (offset > lines.length || (lines.length > 0 && offset === lines.length)) {
|
|
221
|
-
return textResult(
|
|
222
|
-
`Offset ${offset} is out of range for ${args.path} (${lines.length} lines)`,
|
|
223
|
-
true,
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
lines: lines.slice(offset, offset + limit),
|
|
229
|
-
offset,
|
|
230
|
-
limit,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function buildReadResult(
|
|
235
|
-
body: string,
|
|
236
|
-
continuation: ReadContinuationHint | null,
|
|
237
|
-
): ToolExecResult {
|
|
238
|
-
if (!continuation) {
|
|
239
|
-
return textResult(body, false);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
content: [
|
|
244
|
-
{ type: "text", text: body },
|
|
245
|
-
{
|
|
246
|
-
type: "text",
|
|
247
|
-
text: formatReadContinuationHint(
|
|
248
|
-
continuation.offset,
|
|
249
|
-
continuation.limit,
|
|
250
|
-
),
|
|
251
|
-
},
|
|
252
|
-
],
|
|
253
|
-
isError: false,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Read a UTF-8 text file, optionally by line window.
|
|
259
|
-
*
|
|
260
|
-
* When `limit` is omitted, the tool returns a bounded initial slice and adds a
|
|
261
|
-
* continuation hint when more content remains.
|
|
262
|
-
*
|
|
263
|
-
* @param args - Read arguments.
|
|
264
|
-
* @param cwd - Working directory for resolving relative paths.
|
|
265
|
-
* @param opts - Optional abort signal and progressive update callback.
|
|
266
|
-
* @returns A text tool result containing file content or an error.
|
|
267
|
-
*/
|
|
268
|
-
export async function executeRead(
|
|
269
|
-
args: ReadArgs,
|
|
270
|
-
cwd: string,
|
|
271
|
-
opts?: ReadOpts,
|
|
272
|
-
): Promise<ToolExecResult> {
|
|
273
|
-
const loaded = loadReadContent(args, cwd);
|
|
274
|
-
if ("isError" in loaded) {
|
|
275
|
-
return loaded;
|
|
76
|
+
return;
|
|
276
77
|
}
|
|
277
78
|
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
79
|
+
const content = await file.text();
|
|
80
|
+
const lines = content.split(/\r?\n/);
|
|
81
|
+
if (content.endsWith("\n")) lines.pop();
|
|
282
82
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
83
|
+
const offset = args.offset ?? 1;
|
|
84
|
+
const startIndex = offset - 1;
|
|
85
|
+
const endIndex = args.limit
|
|
86
|
+
? Math.min(lines.length, startIndex + args.limit)
|
|
87
|
+
: lines.length;
|
|
88
|
+
const width = String(endIndex).length;
|
|
89
|
+
const body = lines
|
|
90
|
+
.slice(startIndex, endIndex)
|
|
91
|
+
.map((line, idx) => `${String(offset + idx).padStart(width)} | ${line}`)
|
|
92
|
+
.join("\n");
|
|
287
93
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
return buildReadResult(body, null);
|
|
94
|
+
let text = `File: ${filePath}\nLines: ${offset}-${endIndex} of ${lines.length}\n\n${body}`;
|
|
95
|
+
if (endIndex < lines.length) {
|
|
96
|
+
text += `\n\nMore lines available. Use offset ${endIndex + 1} to continue.`;
|
|
292
97
|
}
|
|
293
98
|
|
|
294
|
-
|
|
295
|
-
offset: slice.offset + slice.lines.length,
|
|
296
|
-
limit: slice.limit,
|
|
297
|
-
});
|
|
99
|
+
yield { type: "result", text };
|
|
298
100
|
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Tool handler that validates read arguments before execution.
|
|
302
|
-
*
|
|
303
|
-
* @param args - Raw parsed tool-call arguments.
|
|
304
|
-
* @param cwd - Working directory for path resolution.
|
|
305
|
-
* @param signal - Optional abort signal.
|
|
306
|
-
* @param onUpdate - Optional progressive output callback.
|
|
307
|
-
* @returns The read tool result.
|
|
308
|
-
*/
|
|
309
|
-
export const readToolHandler: ToolHandler = (args, cwd, signal, onUpdate) =>
|
|
310
|
-
executeRead(validateBuiltinToolArgs(readTool, args), cwd, {
|
|
311
|
-
...(signal ? { signal } : {}),
|
|
312
|
-
...(onUpdate ? { onUpdate } : {}),
|
|
313
|
-
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { type Color, HStack, Text } from "@cel-tui/core";
|
|
2
|
+
import { onceEvery } from "./shared";
|
|
3
|
+
import type { TUIState } from "./types";
|
|
4
|
+
|
|
5
|
+
export const theme = {
|
|
6
|
+
black: "color00" as Color,
|
|
7
|
+
bblack: "color08" as Color,
|
|
8
|
+
|
|
9
|
+
red: "color01" as Color,
|
|
10
|
+
bred: "color09" as Color,
|
|
11
|
+
|
|
12
|
+
green: "color02" as Color,
|
|
13
|
+
bgreen: "color10" as Color,
|
|
14
|
+
|
|
15
|
+
yellow: "color03" as Color,
|
|
16
|
+
byellow: "color11" as Color,
|
|
17
|
+
|
|
18
|
+
blue: "color04" as Color,
|
|
19
|
+
bblue: "color12" as Color,
|
|
20
|
+
|
|
21
|
+
magenta: "color05" as Color,
|
|
22
|
+
bmagenta: "color13" as Color,
|
|
23
|
+
|
|
24
|
+
cyan: "color06" as Color,
|
|
25
|
+
bcyan: "color14" as Color,
|
|
26
|
+
|
|
27
|
+
white: "color07" as Color,
|
|
28
|
+
bwhite: "color15" as Color,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function TextPill(
|
|
32
|
+
content: string,
|
|
33
|
+
fgColor: Color,
|
|
34
|
+
bgColor: Color,
|
|
35
|
+
size?: number | undefined,
|
|
36
|
+
) {
|
|
37
|
+
return HStack({ gap: 1, width: size }, [
|
|
38
|
+
HStack({ bgColor, padding: { x: 1 } }, [
|
|
39
|
+
Text(content, { bold: true, fgColor }),
|
|
40
|
+
]),
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function Spinner() {
|
|
45
|
+
const spinnerFrames = [
|
|
46
|
+
"⠁",
|
|
47
|
+
"⠂",
|
|
48
|
+
"⠄",
|
|
49
|
+
"⡀",
|
|
50
|
+
"⡈",
|
|
51
|
+
"⡐",
|
|
52
|
+
"⡠",
|
|
53
|
+
"⣀",
|
|
54
|
+
"⣁",
|
|
55
|
+
"⣂",
|
|
56
|
+
"⣄",
|
|
57
|
+
"⣌",
|
|
58
|
+
"⣔",
|
|
59
|
+
"⣤",
|
|
60
|
+
"⣥",
|
|
61
|
+
"⣦",
|
|
62
|
+
"⣮",
|
|
63
|
+
"⣶",
|
|
64
|
+
"⣷",
|
|
65
|
+
"⣿",
|
|
66
|
+
"⡿",
|
|
67
|
+
"⠿",
|
|
68
|
+
"⢟",
|
|
69
|
+
"⠟",
|
|
70
|
+
"⡛",
|
|
71
|
+
"⠛",
|
|
72
|
+
"⠫",
|
|
73
|
+
"⢋",
|
|
74
|
+
"⠋",
|
|
75
|
+
"⠍",
|
|
76
|
+
"⡉",
|
|
77
|
+
"⠉",
|
|
78
|
+
"⠑",
|
|
79
|
+
"⠡",
|
|
80
|
+
"⢁",
|
|
81
|
+
];
|
|
82
|
+
let spinnerTick = 0;
|
|
83
|
+
const spinnerEvery = onceEvery(4, () => spinnerTick++);
|
|
84
|
+
const currentSpinner = () =>
|
|
85
|
+
spinnerFrames[spinnerTick % spinnerFrames.length];
|
|
86
|
+
|
|
87
|
+
return { spinnerEvery, currentSpinner };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function ActivityPill(state: TUIState, spinnerFrame: string) {
|
|
91
|
+
let label = "idle";
|
|
92
|
+
const frame = state.streaming ? spinnerFrame : "";
|
|
93
|
+
|
|
94
|
+
if (frame) {
|
|
95
|
+
label = frame;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return Text(label);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function ContextPill(state: TUIState) {
|
|
102
|
+
let text = "";
|
|
103
|
+
let bg = theme.bgreen;
|
|
104
|
+
if (!state.contextSize) {
|
|
105
|
+
text = "0%";
|
|
106
|
+
bg = theme.bwhite;
|
|
107
|
+
} else {
|
|
108
|
+
// Colors show the progress in the "smart window" or how much
|
|
109
|
+
// before the dumb zone. The user facing percentage is the model
|
|
110
|
+
// amount. An elegant way to show both :)
|
|
111
|
+
const max = state.options.model.contextWindow;
|
|
112
|
+
const smartMax = 80000;
|
|
113
|
+
const percent = Math.floor((state.contextSize / max) * 100);
|
|
114
|
+
const smartPercent = Math.floor((state.contextSize / smartMax) * 100);
|
|
115
|
+
if (smartPercent > 90) {
|
|
116
|
+
bg = theme.bred;
|
|
117
|
+
} else if (smartPercent > 80) {
|
|
118
|
+
bg = theme.red;
|
|
119
|
+
} else if (smartPercent > 70) {
|
|
120
|
+
bg = theme.yellow;
|
|
121
|
+
} else if (smartPercent > 50) {
|
|
122
|
+
bg = theme.byellow;
|
|
123
|
+
}
|
|
124
|
+
text = `~${percent}%`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
text += ` (${state.options.model.contextWindow / 1000}k)`;
|
|
128
|
+
|
|
129
|
+
return TextPill(text, theme.bblack, bg);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function GitPill(state: TUIState) {
|
|
133
|
+
return TextPill(state.gitBranch ?? "No git.", theme.bwhite, theme.bblack);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function ModelPill(state: TUIState) {
|
|
137
|
+
// Like loot: Gold/purple/blue/green/white -> xhigh/high/medium/low/minimal
|
|
138
|
+
switch (state.options.effort) {
|
|
139
|
+
case "xhigh":
|
|
140
|
+
return TextPill(state.options.model.name, theme.black, theme.yellow);
|
|
141
|
+
case "high":
|
|
142
|
+
return TextPill(state.options.model.name, theme.black, theme.magenta);
|
|
143
|
+
case "medium":
|
|
144
|
+
return TextPill(state.options.model.name, theme.black, theme.blue);
|
|
145
|
+
case "low":
|
|
146
|
+
return TextPill(state.options.model.name, theme.black, theme.green);
|
|
147
|
+
}
|
|
148
|
+
// Minimal
|
|
149
|
+
return TextPill(state.options.model.name, theme.bwhite, theme.bblack);
|
|
150
|
+
}
|