mini-coder 0.7.0 → 0.7.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/bun.lock +14 -14
- package/package.json +5 -5
- package/src/agent.ts +6 -7
- package/src/args.ts +12 -1
- package/src/error-handling.test.ts +163 -0
- package/src/headless.ts +1 -0
- package/src/oauth.ts +9 -2
- package/src/prompt.ts +1 -2
- package/src/session.ts +9 -6
- package/src/shared.ts +2 -1
- package/src/themes.ts +234 -0
- package/src/tool-bash.ts +1 -4
- package/src/tui-components.ts +4 -27
- package/src/tui-conversation.ts +48 -16
- package/src/tui-overlay.ts +47 -2
- package/src/tui.ts +20 -5
- package/src/types.ts +8 -0
package/bun.lock
CHANGED
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@cel-tui/components": "^0.8.3",
|
|
9
9
|
"@cel-tui/core": "^0.8.3",
|
|
10
|
-
"@earendil-works/pi-ai": "^0.
|
|
10
|
+
"@earendil-works/pi-ai": "^0.79.3",
|
|
11
11
|
"diff": "^9.0.0",
|
|
12
12
|
"simple-git": "^3.36.0",
|
|
13
13
|
"yaml": "^2.9.0",
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@biomejs/biome": "^2.
|
|
16
|
+
"@biomejs/biome": "^2.5.0",
|
|
17
17
|
"@types/bun": "^1.3.14",
|
|
18
|
-
"prettier": "^3.8.
|
|
18
|
+
"prettier": "^3.8.4",
|
|
19
19
|
"typescript": "^6.0.3",
|
|
20
20
|
},
|
|
21
21
|
},
|
|
@@ -75,23 +75,23 @@
|
|
|
75
75
|
|
|
76
76
|
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
|
77
77
|
|
|
78
|
-
"@biomejs/biome": ["@biomejs/biome@2.
|
|
78
|
+
"@biomejs/biome": ["@biomejs/biome@2.5.0", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.0", "@biomejs/cli-darwin-x64": "2.5.0", "@biomejs/cli-linux-arm64": "2.5.0", "@biomejs/cli-linux-arm64-musl": "2.5.0", "@biomejs/cli-linux-x64": "2.5.0", "@biomejs/cli-linux-x64-musl": "2.5.0", "@biomejs/cli-win32-arm64": "2.5.0", "@biomejs/cli-win32-x64": "2.5.0" }, "bin": { "biome": "bin/biome" } }, "sha512-4kURkd9hAPrdDM3C9n82ycYgx8hvQcW6MjKTEejruj8rK0N8P3OPpdy8BvI8kt3KWY4ycF5XtDOrktetEfhfuw=="],
|
|
79
79
|
|
|
80
|
-
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.
|
|
80
|
+
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Mn3Fwi3SA5fgmfCPqmzpWF2DLZnms3BVAhM088nTnGrTZmHS3wwIjcoZPqpXeNgd3DrrLH6xp8vTLIBuJoZiXw=="],
|
|
81
81
|
|
|
82
|
-
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.
|
|
82
|
+
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-rg3VPL5P8mYro6pqlXYXuJWph21slVp3SZtAqWSrkZs40d2gTzYmHF8E/X1iTID25btmNKltNDJ926sqVBp7DQ=="],
|
|
83
83
|
|
|
84
|
-
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.
|
|
84
|
+
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-tl+LW8fdD96/xdeWtWwc82LIOc5CoY7N2AsogLTp5R4ECErYt+8Jl/N68ezN9vzSiqPTxw6vjcihoLPYKZHrlw=="],
|
|
85
85
|
|
|
86
|
-
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.
|
|
86
|
+
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vQdM4oSGaf7ZNeGO9w5+Y8SBtyser9M6znxYbm7Ec8wInxJu1WiKxFYZW5Auj2d80bcVvefuGGRxoFOE0eee8g=="],
|
|
87
87
|
|
|
88
|
-
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.
|
|
88
|
+
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.0", "", { "os": "linux", "cpu": "x64" }, "sha512-zpEGf4RQbFEh8Vt7OmavLyyOzRbtcE9osCqrS1kfvt8jDvxwhKXLSf7n0ebr/ov0RJ9ssP+lhs6C8a9WwFvrQA=="],
|
|
89
89
|
|
|
90
|
-
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.
|
|
90
|
+
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+9hIcMngJ+yGUahXqZuZ8CoWKJE9SAZsFsM3QDvXpNsLbXZ9lqVzgBhOk/jTSYkOA0GLP9eu3teukqpLUojHMg=="],
|
|
91
91
|
|
|
92
|
-
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.
|
|
92
|
+
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-jB0wAvTLI4itx5VidqVUejPQFhRUxiZ9l9FvZ26D5fl6t3qme+ZB4PD3bTSeL1vZ8NI2Rx/zj6H9zcESuGHKGw=="],
|
|
93
93
|
|
|
94
|
-
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.
|
|
94
|
+
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.0", "", { "os": "win32", "cpu": "x64" }, "sha512-VT/lF+GId+67j8aDfLkxdxNoVApsPSTbyAtB3jJq0IWTrY77WXfbPfpngxq0bA6JCEv/7k8C9qWjDRKRznDlyw=="],
|
|
95
95
|
|
|
96
96
|
"@cel-tui/clew": ["@cel-tui/clew@0.8.3", "", { "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-7OFUe8ECleXjq9+vv2EvDcTXsiWNSQqVALD1LC/Gi9DzVp1aw0S6SloJuAv1KUVkwjZxil+mvYlk2LoC28mETQ=="],
|
|
97
97
|
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
|
|
102
102
|
"@cel-tui/types": ["@cel-tui/types@0.8.3", "", { "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-yEKXCY16U/W4A4JaVysv2/x/IBEHQAfb93CzL6HugN+N15ETB3Vn8tZrQwdyrOT/9WdlCQkeBH8Vj7BJHs4Hkw=="],
|
|
103
103
|
|
|
104
|
-
"@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.
|
|
104
|
+
"@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.79.3", "", { "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@google/genai": "1.52.0", "@mistralai/mistralai": "2.2.1", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", "partial-json": "0.1.7", "typebox": "1.1.38" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-lMSput/haP5uZAGbXhS5rAYd3GB7GYdJkoAUxg3VFummBeqGqGqllaTWrbHFN12kVGyVfWHhdySNXkiqVh65Iw=="],
|
|
105
105
|
|
|
106
106
|
"@google/genai": ["@google/genai@1.52.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q=="],
|
|
107
107
|
|
|
@@ -231,7 +231,7 @@
|
|
|
231
231
|
|
|
232
232
|
"path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="],
|
|
233
233
|
|
|
234
|
-
"prettier": ["prettier@3.8.
|
|
234
|
+
"prettier": ["prettier@3.8.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q=="],
|
|
235
235
|
|
|
236
236
|
"protobufjs": ["protobufjs@7.5.6", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.1", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg=="],
|
|
237
237
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mini-coder",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"packageManager": "bun@1.3.
|
|
5
|
+
"packageManager": "bun@1.3.13",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mc": "bin/mc.ts"
|
|
8
8
|
},
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
"format": "bun run prettier --write *.md && bun run biome format --write . && bun run biome check --write ."
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@biomejs/biome": "^2.
|
|
17
|
+
"@biomejs/biome": "^2.5.0",
|
|
18
18
|
"@types/bun": "^1.3.14",
|
|
19
|
-
"prettier": "^3.8.
|
|
19
|
+
"prettier": "^3.8.4",
|
|
20
20
|
"typescript": "^6.0.3"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@cel-tui/components": "^0.8.3",
|
|
24
24
|
"@cel-tui/core": "^0.8.3",
|
|
25
|
-
"@earendil-works/pi-ai": "^0.
|
|
25
|
+
"@earendil-works/pi-ai": "^0.79.3",
|
|
26
26
|
"diff": "^9.0.0",
|
|
27
27
|
"simple-git": "^3.36.0",
|
|
28
28
|
"yaml": "^2.9.0"
|
package/src/agent.ts
CHANGED
|
@@ -78,10 +78,6 @@ export async function* streamAgent(
|
|
|
78
78
|
|
|
79
79
|
// Main agent loop, continues until llm sends a response other than toolCall or has no tool calls.
|
|
80
80
|
while (true) {
|
|
81
|
-
let estimate = estimateTokens(JSON.stringify(llmCtx));
|
|
82
|
-
// 80k is the agreed uppon threshold to the DUMB ZONE
|
|
83
|
-
if (estimate > 80000) compactContext(llmCtx.messages);
|
|
84
|
-
|
|
85
81
|
const s = streamSimple(agentCtx.options.model, llmCtx, {
|
|
86
82
|
reasoning: agentCtx.options.effort,
|
|
87
83
|
signal: agentCtx.signal,
|
|
@@ -115,9 +111,6 @@ export async function* streamAgent(
|
|
|
115
111
|
yield { type: "message_update", partial };
|
|
116
112
|
}
|
|
117
113
|
|
|
118
|
-
estimate = estimateTokens(JSON.stringify(llmCtx));
|
|
119
|
-
// 80k is the agreed uppon threshold to the DUMB ZONE
|
|
120
|
-
if (estimate > 80000) compactContext(llmCtx.messages);
|
|
121
114
|
break;
|
|
122
115
|
}
|
|
123
116
|
|
|
@@ -134,6 +127,12 @@ export async function* streamAgent(
|
|
|
134
127
|
return;
|
|
135
128
|
}
|
|
136
129
|
}
|
|
130
|
+
|
|
131
|
+
// Experiment: use this inside the inner loop for "running" compaction
|
|
132
|
+
// after we are in the dumb zone.
|
|
133
|
+
const estimate = estimateTokens(JSON.stringify(llmCtx));
|
|
134
|
+
// 80k is the agreed uppon threshold to the DUMB ZONE
|
|
135
|
+
if (estimate > 80000) compactContext(llmCtx.messages);
|
|
137
136
|
}
|
|
138
137
|
|
|
139
138
|
const message = await s.result();
|
package/src/args.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { Value } from "typebox/value";
|
|
10
10
|
import { getAvailableProviders, isOAuthProvider, loginOAuth } from "./oauth";
|
|
11
11
|
import { DATA_DIR, SETTINGS_PATH } from "./shared.ts";
|
|
12
|
+
import { DEFAULT_TUI_THEME_ID } from "./themes.ts";
|
|
12
13
|
import {
|
|
13
14
|
type CliOptions,
|
|
14
15
|
CliOptionsSchema,
|
|
@@ -63,7 +64,14 @@ async function getSettings(): Promise<Settings | undefined> {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
const jsonText = await file.text();
|
|
66
|
-
|
|
67
|
+
let settings: unknown;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
settings = JSON.parse(jsonText) as unknown;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
73
|
+
throw new Error(`Invalid settings JSON: ${message}`);
|
|
74
|
+
}
|
|
67
75
|
|
|
68
76
|
return parseSettings(settings, "settings");
|
|
69
77
|
}
|
|
@@ -149,6 +157,7 @@ export async function handleArgv(argv: string[]): Promise<CliOptions> {
|
|
|
149
157
|
? DEFAULT_MODEL_ID
|
|
150
158
|
: getFirstModelConfig(settingsProvider, customProviders).id);
|
|
151
159
|
const settingsEffort = settings?.effort ?? DEFAULT_EFFORT;
|
|
160
|
+
const settingsTheme = settings?.theme ?? DEFAULT_TUI_THEME_ID;
|
|
152
161
|
let provider: string = settingsProvider;
|
|
153
162
|
let modelId = settingsModelId;
|
|
154
163
|
let effort: string = settingsEffort;
|
|
@@ -273,6 +282,7 @@ export async function handleArgv(argv: string[]): Promise<CliOptions> {
|
|
|
273
282
|
effort: cliSettings.effort,
|
|
274
283
|
prompt,
|
|
275
284
|
customProviders,
|
|
285
|
+
theme: settingsTheme,
|
|
276
286
|
});
|
|
277
287
|
const nextSettings = parseSettings(
|
|
278
288
|
{
|
|
@@ -280,6 +290,7 @@ export async function handleArgv(argv: string[]): Promise<CliOptions> {
|
|
|
280
290
|
model: options.model.id,
|
|
281
291
|
effort: options.effort,
|
|
282
292
|
customProviders,
|
|
293
|
+
theme: options.theme,
|
|
283
294
|
},
|
|
284
295
|
"settings",
|
|
285
296
|
);
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { runBashTool } from "./tool-bash.ts";
|
|
6
|
+
import { runEditTool } from "./tool-edit.ts";
|
|
7
|
+
import { runReadTool } from "./tool-read.ts";
|
|
8
|
+
import type { ToolRunnerEvent } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
const originalDataDir = Bun.env.MINI_CODER_DATA_DIR;
|
|
11
|
+
const testHome = await mkdtemp(join(tmpdir(), "mini-coder-errors-"));
|
|
12
|
+
const dataDir = join(testHome, "data");
|
|
13
|
+
Bun.env.MINI_CODER_DATA_DIR = dataDir;
|
|
14
|
+
|
|
15
|
+
const sessionsDir = join(dataDir, "sessions");
|
|
16
|
+
const { handleArgv } = await import("./args.ts");
|
|
17
|
+
const { getAvailableProviders } = await import("./oauth.ts");
|
|
18
|
+
const { getSession, listSessionsForCwd } = await import("./session.ts");
|
|
19
|
+
|
|
20
|
+
type ResultEvent = Extract<ToolRunnerEvent, { type: "result" }>;
|
|
21
|
+
|
|
22
|
+
async function collectToolEvents(
|
|
23
|
+
events: AsyncGenerator<ToolRunnerEvent>,
|
|
24
|
+
): Promise<ToolRunnerEvent[]> {
|
|
25
|
+
const collected: ToolRunnerEvent[] = [];
|
|
26
|
+
|
|
27
|
+
for await (const event of events) {
|
|
28
|
+
collected.push(event);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return collected;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resultEvent(events: ToolRunnerEvent[]): ResultEvent {
|
|
35
|
+
const result = events.findLast(
|
|
36
|
+
(event): event is ResultEvent => event.type === "result",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (!result) {
|
|
40
|
+
throw new Error("Tool did not yield a result");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function toolResult(
|
|
47
|
+
events: AsyncGenerator<ToolRunnerEvent>,
|
|
48
|
+
): Promise<ResultEvent> {
|
|
49
|
+
return resultEvent(await collectToolEvents(events));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
beforeEach(async () => {
|
|
53
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterAll(async () => {
|
|
57
|
+
if (originalDataDir === undefined) {
|
|
58
|
+
delete Bun.env.MINI_CODER_DATA_DIR;
|
|
59
|
+
} else {
|
|
60
|
+
Bun.env.MINI_CODER_DATA_DIR = originalDataDir;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await rm(testHome, { recursive: true, force: true });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("tool runner error handling", () => {
|
|
67
|
+
test("read and edit report missing files as tool results", async () => {
|
|
68
|
+
const missingPath = join(testHome, "missing.txt");
|
|
69
|
+
|
|
70
|
+
const readResult = await toolResult(runReadTool({ path: missingPath }));
|
|
71
|
+
expect(readResult.text).toBe(`File not found: ${missingPath}`);
|
|
72
|
+
|
|
73
|
+
const editResult = await toolResult(
|
|
74
|
+
runEditTool({ path: missingPath, oldText: "old", newText: "new" }),
|
|
75
|
+
);
|
|
76
|
+
expect(editResult.text).toBe(`File not found: ${missingPath}`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("edit refuses unsafe writes and preserves the file", async () => {
|
|
80
|
+
const path = join(testHome, "edit.txt");
|
|
81
|
+
await writeFile(path, "one\none\n");
|
|
82
|
+
|
|
83
|
+
const ambiguous = await toolResult(
|
|
84
|
+
runEditTool({ path, oldText: "one", newText: "two" }),
|
|
85
|
+
);
|
|
86
|
+
expect(ambiguous.text).toContain("Multiple matches found");
|
|
87
|
+
expect(await Bun.file(path).text()).toBe("one\none\n");
|
|
88
|
+
|
|
89
|
+
const missing = await toolResult(
|
|
90
|
+
runEditTool({ path, oldText: "missing", newText: "two" }),
|
|
91
|
+
);
|
|
92
|
+
expect(missing.text).toBe(`Old text not found in: ${path}`);
|
|
93
|
+
expect(await Bun.file(path).text()).toBe("one\none\n");
|
|
94
|
+
|
|
95
|
+
const abortController = new AbortController();
|
|
96
|
+
abortController.abort();
|
|
97
|
+
await writeFile(path, "hello");
|
|
98
|
+
|
|
99
|
+
const aborted = await toolResult(
|
|
100
|
+
runEditTool(
|
|
101
|
+
{ path, oldText: "hello", newText: "bye" },
|
|
102
|
+
abortController.signal,
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
expect(aborted.text).toBe("Aborted before write.");
|
|
106
|
+
expect(await Bun.file(path).text()).toBe("hello");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("bash reports non-zero exits instead of throwing", async () => {
|
|
110
|
+
const result = await toolResult(
|
|
111
|
+
runBashTool({ command: 'printf "bad"; exit 7' }),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(result.text).toBe("bad\n\nExit code: 7");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("startup file error handling", () => {
|
|
119
|
+
test("settings JSON parse failures are labelled", async () => {
|
|
120
|
+
await mkdir(dataDir, { recursive: true });
|
|
121
|
+
await writeFile(join(dataDir, "settings.json"), "{");
|
|
122
|
+
|
|
123
|
+
await expect(handleArgv([])).rejects.toThrow("Invalid settings JSON:");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("auth JSON parse failures are labelled", async () => {
|
|
127
|
+
await mkdir(dataDir, { recursive: true });
|
|
128
|
+
await writeFile(join(dataDir, "auth.json"), "{");
|
|
129
|
+
|
|
130
|
+
await expect(getAvailableProviders()).rejects.toThrow("Invalid auth JSON:");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("session file error handling", () => {
|
|
135
|
+
test("listSessionsForCwd ignores missing dirs and bad session files", async () => {
|
|
136
|
+
expect(await listSessionsForCwd()).toEqual([]);
|
|
137
|
+
|
|
138
|
+
await mkdir(sessionsDir, { recursive: true });
|
|
139
|
+
await writeFile(join(sessionsDir, "broken.json"), "{");
|
|
140
|
+
await writeFile(
|
|
141
|
+
join(sessionsDir, "elsewhere.json"),
|
|
142
|
+
JSON.stringify({ id: "elsewhere", cwd: "/elsewhere", messages: [] }),
|
|
143
|
+
);
|
|
144
|
+
await writeFile(
|
|
145
|
+
join(sessionsDir, "ok.json"),
|
|
146
|
+
JSON.stringify({
|
|
147
|
+
id: "ok",
|
|
148
|
+
cwd: process.cwd(),
|
|
149
|
+
messages: [{ role: "user", content: "hi", timestamp: 2 }],
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const sessions = await listSessionsForCwd();
|
|
154
|
+
expect(sessions.map((session) => session.id)).toEqual(["ok"]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("getSession returns undefined for bad session files", async () => {
|
|
158
|
+
await mkdir(sessionsDir, { recursive: true });
|
|
159
|
+
await writeFile(join(sessionsDir, "broken.json"), "{");
|
|
160
|
+
|
|
161
|
+
expect(await getSession("broken")).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
});
|
package/src/headless.ts
CHANGED
package/src/oauth.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
1
2
|
import readline from "node:readline";
|
|
2
3
|
|
|
3
4
|
import { getEnvApiKey, getProviders } from "@earendil-works/pi-ai";
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
type OAuthProviderId,
|
|
11
12
|
type OAuthSelectPrompt,
|
|
12
13
|
} from "@earendil-works/pi-ai/oauth";
|
|
13
|
-
import { AUTH_PATH as AUTH_FILE } from "./shared";
|
|
14
|
+
import { AUTH_PATH as AUTH_FILE, DATA_DIR } from "./shared";
|
|
14
15
|
import type { CliOptions, SavedOAuthCreds } from "./types";
|
|
15
16
|
|
|
16
17
|
type ReadlineInterface = ReturnType<typeof readline.createInterface>;
|
|
@@ -134,13 +135,19 @@ export async function getApiKey(options: CliOptions) {
|
|
|
134
135
|
export async function readCreds(): Promise<SavedOAuthCreds> {
|
|
135
136
|
const file = Bun.file(AUTH_FILE);
|
|
136
137
|
if (await file.exists()) {
|
|
137
|
-
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(await file.text());
|
|
140
|
+
} catch (err) {
|
|
141
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
142
|
+
throw new Error(`Invalid auth JSON: ${message}`);
|
|
143
|
+
}
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
return {};
|
|
141
147
|
}
|
|
142
148
|
|
|
143
149
|
async function writeCreds(creds: SavedOAuthCreds) {
|
|
150
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
144
151
|
await Bun.write(AUTH_FILE, JSON.stringify(await mergeCreds(creds)));
|
|
145
152
|
}
|
|
146
153
|
|
package/src/prompt.ts
CHANGED
|
@@ -11,12 +11,11 @@ export const MAIN_PROMPT = `You are a coding agent interacting with users via th
|
|
|
11
11
|
- Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without unnecessary superlatives, praise, or emotional validation.
|
|
12
12
|
- User messages and Tool results may include <system-reminder> tags. These contain system-generated reminders and bear no direct relation to the specific tool result in which they appear.
|
|
13
13
|
- You have access to bash, read and edit tools. Prefer using read and edit for file operations, use bash for finding read candidates or to run development commands.
|
|
14
|
-
- If a tool call fails or is denied, do NOT re-attempt the exact same call. Analyze why it failed and adjust your approach.
|
|
15
14
|
- Use recent online information, the current environment, and your training data combined for a complete answer.
|
|
16
15
|
- Ensure that you fulfill the user's expectation, requirements and contract **exactly**.
|
|
16
|
+
- Do not overstate what changed or what was verified. Summaries must match the diff.
|
|
17
17
|
- Use temp directory for temp files, scripts, plan files, or anything that doesn't match the requested output.
|
|
18
18
|
- Be concise. Use a professional colleague tone: direct, never condescending, and never rude.
|
|
19
|
-
- Do not overstate what changed or what was verified. Summaries must match the diff.
|
|
20
19
|
`;
|
|
21
20
|
|
|
22
21
|
async function getDir() {
|
package/src/session.ts
CHANGED
|
@@ -9,7 +9,6 @@ export async function ensureSessionsDir(): Promise<void> {
|
|
|
9
9
|
await mkdir(SESSIONS_DIR, { recursive: true });
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
// readSession: writes the session
|
|
13
12
|
export async function getSession(id: string): Promise<Session | undefined> {
|
|
14
13
|
const file = Bun.file(join(SESSIONS_DIR, `${id}.json`));
|
|
15
14
|
|
|
@@ -17,11 +16,16 @@ export async function getSession(id: string): Promise<Session | undefined> {
|
|
|
17
16
|
return;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
try {
|
|
20
|
+
const sessionJson = await file.text();
|
|
21
|
+
const parsed = JSON.parse(sessionJson) as unknown;
|
|
22
|
+
const valid = Value.Check(SessionSchema, parsed);
|
|
23
|
+
|
|
24
|
+
if (valid) return parsed;
|
|
25
|
+
} catch {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
23
28
|
|
|
24
|
-
if (valid) return parsed;
|
|
25
29
|
return;
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -65,7 +69,6 @@ export async function saveSession(s: Session) {
|
|
|
65
69
|
await Bun.write(file, JSON.stringify(s));
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
// updateSession: finds and appends to the existing setting file
|
|
69
72
|
export async function updateSession(id: string, messages: Message[]) {
|
|
70
73
|
const existing = await getSession(id);
|
|
71
74
|
if (existing) {
|
package/src/shared.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { parseDocument } from "yaml";
|
|
|
4
4
|
|
|
5
5
|
// Mixed bag of helpers that can be shared across the codebase
|
|
6
6
|
|
|
7
|
-
export const DATA_DIR =
|
|
7
|
+
export const DATA_DIR =
|
|
8
|
+
Bun.env.MINI_CODER_DATA_DIR ?? join(homedir(), ".config", "mini-coder");
|
|
8
9
|
export const SESSIONS_DIR = join(DATA_DIR, "sessions");
|
|
9
10
|
export const AUTH_PATH = join(DATA_DIR, "auth.json");
|
|
10
11
|
export const SETTINGS_PATH = join(DATA_DIR, "settings.json");
|
package/src/themes.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { SyntaxHighlightTheme } from "@cel-tui/components";
|
|
2
|
+
import type { Color, Theme } from "@cel-tui/core";
|
|
3
|
+
|
|
4
|
+
export const TUI_THEME_IDS = [
|
|
5
|
+
"ansi16",
|
|
6
|
+
"molokai-dark",
|
|
7
|
+
"molokai-light",
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
export type TUIThemeId = (typeof TUI_THEME_IDS)[number];
|
|
11
|
+
|
|
12
|
+
export interface TUIThemeDefinition {
|
|
13
|
+
id: TUIThemeId;
|
|
14
|
+
label: string;
|
|
15
|
+
palette: Theme;
|
|
16
|
+
syntax: SyntaxHighlightTheme;
|
|
17
|
+
rootFgColor?: Color;
|
|
18
|
+
rootBgColor?: Color;
|
|
19
|
+
userMessageBgColor?: Color;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ANSI_SLOT_HEX: Record<Color, string> = {
|
|
23
|
+
color00: "#000000",
|
|
24
|
+
color01: "#cd3131",
|
|
25
|
+
color02: "#0dbc79",
|
|
26
|
+
color03: "#e5e510",
|
|
27
|
+
color04: "#2472c8",
|
|
28
|
+
color05: "#bc3fbc",
|
|
29
|
+
color06: "#11a8cd",
|
|
30
|
+
color07: "#e5e5e5",
|
|
31
|
+
color08: "#666666",
|
|
32
|
+
color09: "#f14c4c",
|
|
33
|
+
color10: "#23d18b",
|
|
34
|
+
color11: "#f5f543",
|
|
35
|
+
color12: "#3b8eea",
|
|
36
|
+
color13: "#d670d6",
|
|
37
|
+
color14: "#29b8db",
|
|
38
|
+
color15: "#ffffff",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const theme = {
|
|
42
|
+
black: "color00",
|
|
43
|
+
red: "color01",
|
|
44
|
+
green: "color02",
|
|
45
|
+
yellow: "color03",
|
|
46
|
+
blue: "color04",
|
|
47
|
+
magenta: "color05",
|
|
48
|
+
cyan: "color06",
|
|
49
|
+
white: "color07",
|
|
50
|
+
bblack: "color08",
|
|
51
|
+
bred: "color09",
|
|
52
|
+
bgreen: "color10",
|
|
53
|
+
byellow: "color11",
|
|
54
|
+
bblue: "color12",
|
|
55
|
+
bmagenta: "color13",
|
|
56
|
+
bcyan: "color14",
|
|
57
|
+
bwhite: "color15",
|
|
58
|
+
} as const satisfies Record<string, Color>;
|
|
59
|
+
|
|
60
|
+
const ansi16Palette: Theme = {
|
|
61
|
+
color00: 0,
|
|
62
|
+
color01: 1,
|
|
63
|
+
color02: 2,
|
|
64
|
+
color03: 3,
|
|
65
|
+
color04: 4,
|
|
66
|
+
color05: 5,
|
|
67
|
+
color06: 6,
|
|
68
|
+
color07: 7,
|
|
69
|
+
color08: 8,
|
|
70
|
+
color09: 9,
|
|
71
|
+
color10: 10,
|
|
72
|
+
color11: 11,
|
|
73
|
+
color12: 12,
|
|
74
|
+
color13: 13,
|
|
75
|
+
color14: 14,
|
|
76
|
+
color15: 15,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const molokaiDarkPalette: Theme = {
|
|
80
|
+
color00: "#272822",
|
|
81
|
+
color01: "#f92672",
|
|
82
|
+
color02: "#a6e22e",
|
|
83
|
+
color03: "#e6db74",
|
|
84
|
+
color04: "#66d9ef",
|
|
85
|
+
color05: "#f92672",
|
|
86
|
+
color06: "#66d9ef",
|
|
87
|
+
color07: "#f8f8f2",
|
|
88
|
+
color08: "#6f705f",
|
|
89
|
+
color09: "#ff6188",
|
|
90
|
+
color10: "#a6e22e",
|
|
91
|
+
color11: "#ffd866",
|
|
92
|
+
color12: "#78dce8",
|
|
93
|
+
color13: "#ae81ff",
|
|
94
|
+
color14: "#66d9ef",
|
|
95
|
+
color15: "#ffffff",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const molokaiLightPalette: Theme = {
|
|
99
|
+
color00: "#272822",
|
|
100
|
+
color01: "#ff5f87",
|
|
101
|
+
color02: "#8bcf26",
|
|
102
|
+
color03: "#c7a100",
|
|
103
|
+
color04: "#61aeee",
|
|
104
|
+
color05: "#d16dff",
|
|
105
|
+
color06: "#00a8b5",
|
|
106
|
+
color07: "#f2efe4",
|
|
107
|
+
color08: "#5f6060",
|
|
108
|
+
color09: "#b0003a",
|
|
109
|
+
color10: "#3f7d00",
|
|
110
|
+
color11: "#725f00",
|
|
111
|
+
color12: "#005f9f",
|
|
112
|
+
color13: "#7f2caf",
|
|
113
|
+
color14: "#007885",
|
|
114
|
+
color15: "#fffdf5",
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const slotColor = (slot: Color) => ANSI_SLOT_HEX[slot];
|
|
118
|
+
|
|
119
|
+
function syntaxScope(
|
|
120
|
+
scope: string | readonly string[],
|
|
121
|
+
foreground: Color,
|
|
122
|
+
fontStyle?: string,
|
|
123
|
+
) {
|
|
124
|
+
return {
|
|
125
|
+
scope,
|
|
126
|
+
settings: {
|
|
127
|
+
foreground: slotColor(foreground),
|
|
128
|
+
...(fontStyle ? { fontStyle } : {}),
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const molokaiDarkSyntax = {
|
|
134
|
+
name: "mini-coder-molokai-dark",
|
|
135
|
+
type: "dark",
|
|
136
|
+
fg: slotColor("color07"),
|
|
137
|
+
tokenColors: [
|
|
138
|
+
syntaxScope(["comment", "markup.quote"], "color08", "italic"),
|
|
139
|
+
syntaxScope(["keyword", "operator"], "color05"),
|
|
140
|
+
syntaxScope(["string", "escape", "markup.list"], "color03"),
|
|
141
|
+
syntaxScope(["number", "regexp"], "color13"),
|
|
142
|
+
syntaxScope(["function", "command", "markup.code"], "color10"),
|
|
143
|
+
syntaxScope(["builtin", "property", "type", "meta"], "color06"),
|
|
144
|
+
syntaxScope("markup.heading", "color04", "bold"),
|
|
145
|
+
syntaxScope(["diff.deleted", "diff.file.old"], "color09"),
|
|
146
|
+
syntaxScope(["diff.inserted", "diff.file.new"], "color10"),
|
|
147
|
+
syntaxScope("diff.hunk", "color13"),
|
|
148
|
+
syntaxScope(["diff.header", "diff.no-newline"], "color08"),
|
|
149
|
+
],
|
|
150
|
+
} as const satisfies SyntaxHighlightTheme;
|
|
151
|
+
|
|
152
|
+
const molokaiLightSyntax = {
|
|
153
|
+
name: "mini-coder-molokai-light",
|
|
154
|
+
type: "light",
|
|
155
|
+
fg: slotColor("color00"),
|
|
156
|
+
tokenColors: [
|
|
157
|
+
syntaxScope(["comment", "markup.quote"], "color08", "italic"),
|
|
158
|
+
syntaxScope(["keyword", "operator"], "color13"),
|
|
159
|
+
syntaxScope(["string", "escape", "markup.list"], "color11"),
|
|
160
|
+
syntaxScope(["number", "regexp"], "color09"),
|
|
161
|
+
syntaxScope(["function", "command", "markup.code"], "color10"),
|
|
162
|
+
syntaxScope(["builtin", "property", "type", "meta"], "color14"),
|
|
163
|
+
syntaxScope("markup.heading", "color12", "bold"),
|
|
164
|
+
syntaxScope(["diff.deleted", "diff.file.old"], "color09"),
|
|
165
|
+
syntaxScope(["diff.inserted", "diff.file.new"], "color10"),
|
|
166
|
+
syntaxScope("diff.hunk", "color13"),
|
|
167
|
+
syntaxScope(["diff.header", "diff.no-newline"], "color08"),
|
|
168
|
+
],
|
|
169
|
+
} as const satisfies SyntaxHighlightTheme;
|
|
170
|
+
|
|
171
|
+
export const DEFAULT_TUI_THEME_ID: TUIThemeId = "ansi16";
|
|
172
|
+
|
|
173
|
+
export const TUI_THEMES = {
|
|
174
|
+
ansi16: {
|
|
175
|
+
id: "ansi16",
|
|
176
|
+
label: "ansi16",
|
|
177
|
+
palette: ansi16Palette,
|
|
178
|
+
syntax: "default",
|
|
179
|
+
userMessageBgColor: theme.bblack,
|
|
180
|
+
},
|
|
181
|
+
"molokai-dark": {
|
|
182
|
+
id: "molokai-dark",
|
|
183
|
+
label: "molokai dark",
|
|
184
|
+
palette: molokaiDarkPalette,
|
|
185
|
+
syntax: molokaiDarkSyntax,
|
|
186
|
+
rootFgColor: theme.bwhite,
|
|
187
|
+
rootBgColor: theme.black,
|
|
188
|
+
userMessageBgColor: theme.bblack,
|
|
189
|
+
},
|
|
190
|
+
"molokai-light": {
|
|
191
|
+
id: "molokai-light",
|
|
192
|
+
label: "molokai light",
|
|
193
|
+
palette: molokaiLightPalette,
|
|
194
|
+
syntax: molokaiLightSyntax,
|
|
195
|
+
rootFgColor: theme.black,
|
|
196
|
+
rootBgColor: theme.bwhite,
|
|
197
|
+
userMessageBgColor: theme.white,
|
|
198
|
+
},
|
|
199
|
+
} as const satisfies Record<TUIThemeId, TUIThemeDefinition>;
|
|
200
|
+
|
|
201
|
+
export const activeTuiTheme: Theme = { ...ansi16Palette };
|
|
202
|
+
|
|
203
|
+
export function applyTUITheme(id: TUIThemeId): void {
|
|
204
|
+
Object.assign(activeTuiTheme, TUI_THEMES[id].palette);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function getTUITheme(id: TUIThemeId): TUIThemeDefinition {
|
|
208
|
+
return TUI_THEMES[id];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function textColorForBackground(
|
|
212
|
+
bgColor: Color,
|
|
213
|
+
themeId: TUIThemeId,
|
|
214
|
+
): Color {
|
|
215
|
+
if (bgColor === theme.bblack) return theme.bwhite;
|
|
216
|
+
|
|
217
|
+
if (
|
|
218
|
+
themeId === "molokai-light" &&
|
|
219
|
+
(
|
|
220
|
+
[
|
|
221
|
+
theme.bred,
|
|
222
|
+
theme.bgreen,
|
|
223
|
+
theme.byellow,
|
|
224
|
+
theme.bblue,
|
|
225
|
+
theme.bmagenta,
|
|
226
|
+
theme.bcyan,
|
|
227
|
+
] as readonly Color[]
|
|
228
|
+
).includes(bgColor)
|
|
229
|
+
) {
|
|
230
|
+
return theme.bwhite;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return theme.black;
|
|
234
|
+
}
|
package/src/tool-bash.ts
CHANGED
|
@@ -66,10 +66,7 @@ export async function* runBashTool(
|
|
|
66
66
|
|
|
67
67
|
const exitCode = await proc.exited;
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
if (output.length) {
|
|
71
|
-
result += `${output}`;
|
|
72
|
-
}
|
|
69
|
+
const result = `${output.length ? output : "(no output)"}\n\nExit code: ${exitCode}`;
|
|
73
70
|
|
|
74
71
|
yield {
|
|
75
72
|
type: "result",
|
package/src/tui-components.ts
CHANGED
|
@@ -1,32 +1,9 @@
|
|
|
1
1
|
import { type Color, HStack, Text } from "@cel-tui/core";
|
|
2
2
|
import { onceEvery } from "./shared";
|
|
3
|
+
import { textColorForBackground, theme } from "./themes";
|
|
3
4
|
import type { TUIState } from "./types";
|
|
4
5
|
|
|
5
|
-
export
|
|
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
|
-
};
|
|
6
|
+
export { theme };
|
|
30
7
|
|
|
31
8
|
export function TextPill(
|
|
32
9
|
content: string,
|
|
@@ -100,7 +77,7 @@ export function ActivityPill(state: TUIState, spinnerFrame: string) {
|
|
|
100
77
|
|
|
101
78
|
export function ContextPill(state: TUIState) {
|
|
102
79
|
let text = "";
|
|
103
|
-
let bg = theme.bgreen;
|
|
80
|
+
let bg: Color = theme.bgreen;
|
|
104
81
|
if (!state.contextSize) {
|
|
105
82
|
text = "0%";
|
|
106
83
|
bg = theme.bwhite;
|
|
@@ -126,7 +103,7 @@ export function ContextPill(state: TUIState) {
|
|
|
126
103
|
|
|
127
104
|
text += ` (${state.options.model.contextWindow / 1000}k)`;
|
|
128
105
|
|
|
129
|
-
return TextPill(text, theme
|
|
106
|
+
return TextPill(text, textColorForBackground(bg, state.options.theme), bg);
|
|
130
107
|
}
|
|
131
108
|
|
|
132
109
|
export function GitPill(state: TUIState) {
|
package/src/tui-conversation.ts
CHANGED
|
@@ -1,36 +1,48 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
SyntaxHighlight,
|
|
3
|
+
type SyntaxHighlightTheme,
|
|
4
|
+
} from "@cel-tui/components";
|
|
5
|
+
import { type Color, HStack, type Node, Text, VStack } from "@cel-tui/core";
|
|
3
6
|
import { estimateTokens } from "./shared";
|
|
7
|
+
import { getTUITheme, textColorForBackground } from "./themes";
|
|
4
8
|
import { TextPill, theme } from "./tui-components";
|
|
5
9
|
import type { TUIMessage, TUIState, TUIToolCall } from "./types";
|
|
6
10
|
|
|
7
|
-
export function emptyState(): Node {
|
|
11
|
+
export function emptyState(state: TUIState): Node {
|
|
8
12
|
const randColor = theme.bgreen;
|
|
13
|
+
const shortcutFgColor =
|
|
14
|
+
state.options.theme === "ansi16"
|
|
15
|
+
? randColor
|
|
16
|
+
: textColorForBackground(theme.bblack, state.options.theme);
|
|
9
17
|
return HStack({ flex: 1, alignItems: "center" }, [
|
|
10
18
|
VStack({ flex: 1, alignItems: "center", gap: 1 }, [
|
|
11
19
|
HStack({ gap: 1 }, [
|
|
12
20
|
Text("mini"),
|
|
13
|
-
TextPill(
|
|
21
|
+
TextPill(
|
|
22
|
+
"coder",
|
|
23
|
+
textColorForBackground(randColor, state.options.theme),
|
|
24
|
+
randColor,
|
|
25
|
+
),
|
|
14
26
|
]),
|
|
15
27
|
VStack({ gap: 1 }, [
|
|
16
28
|
HStack({ gap: 1 }, [
|
|
17
|
-
TextPill("/new",
|
|
29
|
+
TextPill("/new", shortcutFgColor, theme.bblack, 13),
|
|
18
30
|
Text("Start a new session from the input box.", {
|
|
19
31
|
fgColor: theme.bblack,
|
|
20
32
|
}),
|
|
21
33
|
]),
|
|
22
34
|
HStack({ gap: 1 }, [
|
|
23
|
-
TextPill("ctrl+p",
|
|
35
|
+
TextPill("ctrl+p", shortcutFgColor, theme.bblack, 13),
|
|
24
36
|
Text("Menu for session history, and settings.", {
|
|
25
37
|
fgColor: theme.bblack,
|
|
26
38
|
}),
|
|
27
39
|
]),
|
|
28
40
|
HStack({ gap: 1 }, [
|
|
29
|
-
TextPill("ESC",
|
|
41
|
+
TextPill("ESC", shortcutFgColor, theme.bblack, 13),
|
|
30
42
|
Text("Abort agent response.", { fgColor: theme.bblack }),
|
|
31
43
|
]),
|
|
32
44
|
HStack({ gap: 1 }, [
|
|
33
|
-
TextPill("ctrl+c|d|q",
|
|
45
|
+
TextPill("ctrl+c|d|q", shortcutFgColor, theme.bblack, 13),
|
|
34
46
|
Text("Quit.", { fgColor: theme.bblack }),
|
|
35
47
|
]),
|
|
36
48
|
]),
|
|
@@ -38,7 +50,10 @@ export function emptyState(): Node {
|
|
|
38
50
|
]);
|
|
39
51
|
}
|
|
40
52
|
|
|
41
|
-
function ConversationMessageToolCall(
|
|
53
|
+
function ConversationMessageToolCall(
|
|
54
|
+
call: TUIToolCall,
|
|
55
|
+
syntaxTheme: SyntaxHighlightTheme,
|
|
56
|
+
) {
|
|
42
57
|
let outputNode: Node | null = null;
|
|
43
58
|
|
|
44
59
|
// Compress read and bash calls
|
|
@@ -64,7 +79,7 @@ function ConversationMessageToolCall(call: TUIToolCall) {
|
|
|
64
79
|
|
|
65
80
|
outputNode = VStack({ width: "100%" }, blocks);
|
|
66
81
|
} else if (call.tool === "edit") {
|
|
67
|
-
outputNode = SyntaxHighlight(call.output, "patch");
|
|
82
|
+
outputNode = SyntaxHighlight(call.output, "patch", { theme: syntaxTheme });
|
|
68
83
|
} else {
|
|
69
84
|
outputNode = Text(call.output, { wrap: "word" });
|
|
70
85
|
}
|
|
@@ -81,7 +96,7 @@ function ConversationMessageToolCall(call: TUIToolCall) {
|
|
|
81
96
|
|
|
82
97
|
// Syntax highlight bash args
|
|
83
98
|
if (call.tool === "bash") {
|
|
84
|
-
node = SyntaxHighlight(String(value), "bash");
|
|
99
|
+
node = SyntaxHighlight(String(value), "bash", { theme: syntaxTheme });
|
|
85
100
|
return node;
|
|
86
101
|
}
|
|
87
102
|
|
|
@@ -99,7 +114,11 @@ function ConversationMessageToolCall(call: TUIToolCall) {
|
|
|
99
114
|
]);
|
|
100
115
|
}
|
|
101
116
|
|
|
102
|
-
function ConversationMessage(
|
|
117
|
+
function ConversationMessage(
|
|
118
|
+
message: TUIMessage,
|
|
119
|
+
syntaxTheme: SyntaxHighlightTheme,
|
|
120
|
+
userMessageBgColor: Color | undefined,
|
|
121
|
+
) {
|
|
103
122
|
const blocks: Node[] = [];
|
|
104
123
|
|
|
105
124
|
if (message.thinking) {
|
|
@@ -118,17 +137,22 @@ function ConversationMessage(message: TUIMessage) {
|
|
|
118
137
|
VStack(
|
|
119
138
|
{
|
|
120
139
|
width: "100%",
|
|
121
|
-
bgColor: message.role === "user" ?
|
|
140
|
+
bgColor: message.role === "user" ? userMessageBgColor : undefined,
|
|
122
141
|
padding: { y: 1, x: message.role === "user" ? 1 : 0 },
|
|
123
142
|
},
|
|
124
|
-
[SyntaxHighlight(message.text, "markdown")],
|
|
143
|
+
[SyntaxHighlight(message.text, "markdown", { theme: syntaxTheme })],
|
|
125
144
|
),
|
|
126
145
|
);
|
|
127
146
|
}
|
|
128
147
|
|
|
129
148
|
if (message.toolCalls?.length) {
|
|
130
149
|
blocks.push(
|
|
131
|
-
VStack(
|
|
150
|
+
VStack(
|
|
151
|
+
{ gap: 1 },
|
|
152
|
+
message.toolCalls.map((call) =>
|
|
153
|
+
ConversationMessageToolCall(call, syntaxTheme),
|
|
154
|
+
),
|
|
155
|
+
),
|
|
132
156
|
);
|
|
133
157
|
}
|
|
134
158
|
|
|
@@ -158,6 +182,8 @@ function ConversationMessage(message: TUIMessage) {
|
|
|
158
182
|
}
|
|
159
183
|
|
|
160
184
|
export function Conversation(state: TUIState) {
|
|
185
|
+
const activeTheme = getTUITheme(state.options.theme);
|
|
186
|
+
|
|
161
187
|
return VStack(
|
|
162
188
|
{
|
|
163
189
|
flex: 1,
|
|
@@ -169,6 +195,12 @@ export function Conversation(state: TUIState) {
|
|
|
169
195
|
state.stickToBottom = offset >= maxOffset;
|
|
170
196
|
},
|
|
171
197
|
},
|
|
172
|
-
state.tuiMessages.map(
|
|
198
|
+
state.tuiMessages.map((message) =>
|
|
199
|
+
ConversationMessage(
|
|
200
|
+
message,
|
|
201
|
+
activeTheme.syntax,
|
|
202
|
+
activeTheme.userMessageBgColor,
|
|
203
|
+
),
|
|
204
|
+
),
|
|
173
205
|
);
|
|
174
206
|
}
|
package/src/tui-overlay.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HStack, Text, TextInput, VStack } from "@cel-tui/core";
|
|
1
|
+
import { cel, HStack, Text, TextInput, VStack } from "@cel-tui/core";
|
|
2
2
|
import {
|
|
3
3
|
getModels,
|
|
4
4
|
type Message,
|
|
@@ -9,6 +9,7 @@ import { saveSettings } from "./args";
|
|
|
9
9
|
import { getAvailableProviders } from "./oauth";
|
|
10
10
|
import { listSessionsForCwd } from "./session";
|
|
11
11
|
import { estimateTokens, formatTimestamp } from "./shared";
|
|
12
|
+
import { applyTUITheme, getTUITheme, TUI_THEME_IDS } from "./themes";
|
|
12
13
|
import { TextPill, theme } from "./tui-components";
|
|
13
14
|
import type { SelectOptions, SelectState, Session, TUIState } from "./types";
|
|
14
15
|
|
|
@@ -344,12 +345,28 @@ export function mainMenu(state: TUIState, initialPane = "main") {
|
|
|
344
345
|
})),
|
|
345
346
|
});
|
|
346
347
|
|
|
348
|
+
const themesPane = (): MenuPane => ({
|
|
349
|
+
label: "themes",
|
|
350
|
+
filter: "",
|
|
351
|
+
list: TUI_THEME_IDS.map((id) => {
|
|
352
|
+
const tuiTheme = getTUITheme(id);
|
|
353
|
+
return {
|
|
354
|
+
label:
|
|
355
|
+
id === state.options.theme
|
|
356
|
+
? `${tuiTheme.label} (current)`
|
|
357
|
+
: tuiTheme.label,
|
|
358
|
+
value: id,
|
|
359
|
+
};
|
|
360
|
+
}),
|
|
361
|
+
});
|
|
362
|
+
|
|
347
363
|
const mainPane: MenuPane = {
|
|
348
364
|
label: "main",
|
|
349
365
|
filter: state.prompt.length ? state.prompt : "",
|
|
350
366
|
list: [
|
|
351
367
|
{ label: "models and providers", value: "providers" },
|
|
352
368
|
{ label: "reasoning effort", value: "effort" },
|
|
369
|
+
{ label: "themes", value: "themes" },
|
|
353
370
|
{ label: "sessions", value: "sessions" },
|
|
354
371
|
{ label: "fork", value: "fork" },
|
|
355
372
|
],
|
|
@@ -359,7 +376,7 @@ export function mainMenu(state: TUIState, initialPane = "main") {
|
|
|
359
376
|
filter: "",
|
|
360
377
|
list: efforts,
|
|
361
378
|
};
|
|
362
|
-
const panes: MenuPane[] = [effortPane];
|
|
379
|
+
const panes: MenuPane[] = [effortPane, themesPane()];
|
|
363
380
|
let currentPane = initialPane === "fork" ? forkPane() : mainPane;
|
|
364
381
|
let selectedProvider: string | undefined;
|
|
365
382
|
|
|
@@ -387,6 +404,14 @@ export function mainMenu(state: TUIState, initialPane = "main") {
|
|
|
387
404
|
state.overlay = false;
|
|
388
405
|
};
|
|
389
406
|
|
|
407
|
+
const requestThemeRefresh = () => {
|
|
408
|
+
state.forceThemeRefresh = true;
|
|
409
|
+
setTimeout(() => {
|
|
410
|
+
state.forceThemeRefresh = false;
|
|
411
|
+
cel.render();
|
|
412
|
+
}, 0);
|
|
413
|
+
};
|
|
414
|
+
|
|
390
415
|
const toTUIMessage = (msg: Message) => {
|
|
391
416
|
const textFromContent = (
|
|
392
417
|
content: string | { type: string; text?: string }[],
|
|
@@ -509,6 +534,24 @@ export function mainMenu(state: TUIState, initialPane = "main") {
|
|
|
509
534
|
return;
|
|
510
535
|
}
|
|
511
536
|
|
|
537
|
+
if (currentPane.label === "themes") {
|
|
538
|
+
const nextTheme = TUI_THEME_IDS.find((id) => id === s.selected);
|
|
539
|
+
if (!nextTheme) return;
|
|
540
|
+
|
|
541
|
+
state.options.theme = nextTheme;
|
|
542
|
+
applyTUITheme(nextTheme);
|
|
543
|
+
saveSettings({
|
|
544
|
+
provider: state.options.provider,
|
|
545
|
+
model: state.options.model.id,
|
|
546
|
+
effort: state.options.effort,
|
|
547
|
+
customProviders: state.options.customProviders,
|
|
548
|
+
theme: nextTheme,
|
|
549
|
+
});
|
|
550
|
+
requestThemeRefresh();
|
|
551
|
+
closeMenu(s);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
512
555
|
if (currentPane.label === "providers") {
|
|
513
556
|
const provider = currentProviders.find((v) => v === s.selected);
|
|
514
557
|
if (!provider) return;
|
|
@@ -541,6 +584,7 @@ export function mainMenu(state: TUIState, initialPane = "main") {
|
|
|
541
584
|
model: model.id,
|
|
542
585
|
effort: state.options.effort,
|
|
543
586
|
customProviders: state.options.customProviders,
|
|
587
|
+
theme: state.options.theme,
|
|
544
588
|
});
|
|
545
589
|
closeMenu(s);
|
|
546
590
|
return;
|
|
@@ -556,6 +600,7 @@ export function mainMenu(state: TUIState, initialPane = "main") {
|
|
|
556
600
|
model: state.options.model.id,
|
|
557
601
|
effort: effort,
|
|
558
602
|
customProviders: state.options.customProviders,
|
|
603
|
+
theme: state.options.theme,
|
|
559
604
|
});
|
|
560
605
|
closeMenu(s);
|
|
561
606
|
}
|
package/src/tui.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "./prompt";
|
|
14
14
|
import { updateSession } from "./session";
|
|
15
15
|
import { estimateTokens, formatTimestamp, secureRandomString } from "./shared";
|
|
16
|
+
import { activeTuiTheme, applyTUITheme, getTUITheme } from "./themes";
|
|
16
17
|
import { bash, runBashTool } from "./tool-bash";
|
|
17
18
|
import { edit, runEditTool } from "./tool-edit";
|
|
18
19
|
import { read, runReadTool } from "./tool-read";
|
|
@@ -84,6 +85,9 @@ export function initTUI(state: TUIState, leave: (s: string) => void) {
|
|
|
84
85
|
};
|
|
85
86
|
|
|
86
87
|
const onEditorKeyPress = (key: string) => {
|
|
88
|
+
const submit = async () => {
|
|
89
|
+
await streamAgentTUI(state);
|
|
90
|
+
};
|
|
87
91
|
// onKeyPress
|
|
88
92
|
if (key === "enter") {
|
|
89
93
|
if (state.prompt === ":q") {
|
|
@@ -102,16 +106,15 @@ export function initTUI(state: TUIState, leave: (s: string) => void) {
|
|
|
102
106
|
state.stickToBottom = true;
|
|
103
107
|
return false;
|
|
104
108
|
}
|
|
105
|
-
const submit = async () => {
|
|
106
|
-
await streamAgentTUI(state);
|
|
107
|
-
};
|
|
108
109
|
if (state.prompt && !state.streaming) submit();
|
|
109
110
|
return false;
|
|
110
111
|
}
|
|
111
112
|
};
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
applyTUITheme(state.options.theme);
|
|
115
|
+
cel.init(new ProcessTerminal(), { theme: activeTuiTheme });
|
|
114
116
|
cel.viewport(() => {
|
|
117
|
+
const activeTheme = getTUITheme(state.options.theme);
|
|
115
118
|
const layers = [
|
|
116
119
|
VStack(
|
|
117
120
|
{
|
|
@@ -119,9 +122,12 @@ export function initTUI(state: TUIState, leave: (s: string) => void) {
|
|
|
119
122
|
gap: 1,
|
|
120
123
|
padding: { x: 1, y: 1 },
|
|
121
124
|
onKeyPress: onWindowKeyPress,
|
|
125
|
+
fgColor: activeTheme.rootFgColor,
|
|
126
|
+
bgColor: activeTheme.rootBgColor,
|
|
127
|
+
italic: state.forceThemeRefresh,
|
|
122
128
|
},
|
|
123
129
|
[
|
|
124
|
-
state.messages.length ? Conversation(state) : emptyState(),
|
|
130
|
+
state.messages.length ? Conversation(state) : emptyState(state),
|
|
125
131
|
HStack({ gap: 1 }, [
|
|
126
132
|
ModelPill(state),
|
|
127
133
|
TextPill(`../${state.cwd}`, theme.bwhite, theme.bblack),
|
|
@@ -144,6 +150,8 @@ export function initTUI(state: TUIState, leave: (s: string) => void) {
|
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
async function streamAgentTUI(state: TUIState) {
|
|
153
|
+
// Just as a defensive thought, callers should set this ahead of time if doing other
|
|
154
|
+
// ops before starting the stream.
|
|
147
155
|
state.streaming = true;
|
|
148
156
|
|
|
149
157
|
const abortController = new AbortController();
|
|
@@ -280,6 +288,13 @@ async function streamAgentTUI(state: TUIState) {
|
|
|
280
288
|
}
|
|
281
289
|
}
|
|
282
290
|
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
293
|
+
state.tuiMessages.push({
|
|
294
|
+
timestamp: formatTimestamp(Date.now()),
|
|
295
|
+
role: "assistant",
|
|
296
|
+
text,
|
|
297
|
+
});
|
|
283
298
|
} finally {
|
|
284
299
|
state.streaming = false;
|
|
285
300
|
if (!state.sessionId) {
|
package/src/types.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
Type,
|
|
11
11
|
} from "@earendil-works/pi-ai";
|
|
12
12
|
import type { OAuthCredentials } from "@earendil-works/pi-ai/oauth";
|
|
13
|
+
import { TUI_THEME_IDS, type TUIThemeId } from "./themes";
|
|
13
14
|
|
|
14
15
|
const ThinkingLevelSchema = Type.Unsafe<ThinkingLevel>(
|
|
15
16
|
Type.Union([
|
|
@@ -45,11 +46,16 @@ const ModelSchema = Type.Unsafe<Model<Api>>(
|
|
|
45
46
|
}),
|
|
46
47
|
);
|
|
47
48
|
|
|
49
|
+
const TUIThemeIdSchema = Type.Unsafe<TUIThemeId>(
|
|
50
|
+
Type.Union(TUI_THEME_IDS.map((id) => Type.Literal(id))),
|
|
51
|
+
);
|
|
52
|
+
|
|
48
53
|
export const SettingsSchema = Type.Object({
|
|
49
54
|
provider: Type.String(),
|
|
50
55
|
model: Type.String(),
|
|
51
56
|
effort: ThinkingLevelSchema,
|
|
52
57
|
customProviders: Type.Optional(Type.Array(ModelSchema)),
|
|
58
|
+
theme: Type.Optional(TUIThemeIdSchema),
|
|
53
59
|
});
|
|
54
60
|
export type Settings = Static<typeof SettingsSchema>;
|
|
55
61
|
|
|
@@ -59,6 +65,7 @@ export const CliOptionsSchema = Type.Object({
|
|
|
59
65
|
effort: ThinkingLevelSchema,
|
|
60
66
|
prompt: Type.Optional(Type.String()),
|
|
61
67
|
customProviders: Type.Optional(Type.Array(ModelSchema)),
|
|
68
|
+
theme: TUIThemeIdSchema,
|
|
62
69
|
});
|
|
63
70
|
export type CliOptions = Static<typeof CliOptionsSchema>;
|
|
64
71
|
|
|
@@ -104,6 +111,7 @@ export type TUIState = {
|
|
|
104
111
|
cwd: string;
|
|
105
112
|
gitBranch?: string;
|
|
106
113
|
overlay?: boolean | undefined;
|
|
114
|
+
forceThemeRefresh?: boolean | undefined;
|
|
107
115
|
sessionId?: string | undefined;
|
|
108
116
|
};
|
|
109
117
|
|