mini-coder 0.6.0 → 0.6.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/README.md +1 -0
- package/bun.lock +51 -47
- package/nono-mini-coder.json +42 -0
- package/package.json +2 -2
- package/src/agent.ts +9 -3
- package/src/args.ts +1 -1
- package/src/headless.ts +23 -8
- package/src/oauth.ts +2 -2
- package/src/prompt.ts +60 -87
- package/src/session.ts +1 -1
- package/src/tool-bash.ts +2 -14
- package/src/tool-edit.ts +7 -19
- package/src/tool-read.ts +100 -0
- package/src/tui-components.ts +8 -8
- package/src/tui-conversation.ts +29 -20
- package/src/tui-overlay.ts +2 -2
- package/src/tui.ts +2 -6
- package/src/types.ts +7 -3
- package/src/tool-task.ts +0 -114
package/src/prompt.ts
CHANGED
|
@@ -2,74 +2,23 @@ import { promises } from "node:fs";
|
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
3
|
import { homedir, platform } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import type { Message,
|
|
5
|
+
import type { Message, ToolResultMessage } from "@earendil-works/pi-ai";
|
|
6
6
|
import simpleGit, { type StatusResult } from "simple-git";
|
|
7
7
|
import { parseSkillFrontmatter } from "./shared";
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
# Safety rules
|
|
9
|
+
export const MAIN_PROMPT = `You are a coding agent interacting with users via the mini-coder harness. You help users by reading files, executing commands, and editting code.
|
|
11
10
|
|
|
12
|
-
-
|
|
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
|
+
- 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
|
+
- 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.
|
|
13
15
|
- Use recent online information, the current environment, and your training data combined for a complete answer.
|
|
14
16
|
- Ensure that you fulfill the user's expectation, requirements and contract **exactly**.
|
|
15
|
-
- Be defensive with existing changes and destructive commands, they could harm your user's changes.
|
|
16
17
|
- Use temp directory for temp files, scripts, plan files, or anything that doesn't match the requested output.
|
|
17
|
-
-
|
|
18
|
-
- Avoid over-enginnering, hacks or creative solutions. The boring, simple and repliable is always preferred.
|
|
18
|
+
- Be concise. Use a professional colleague tone: direct, never condescending, and never rude.
|
|
19
19
|
- Do not overstate what changed or what was verified. Summaries must match the diff.
|
|
20
20
|
`;
|
|
21
21
|
|
|
22
|
-
export const MAIN_PROMPT = `# You are "mini-coder", a coding agent.
|
|
23
|
-
|
|
24
|
-
IMPORTANT: Be defensive with existing changes and destructive commands.
|
|
25
|
-
IMPORTANT: Do not overstate what changed or what was verified. Summaries must match the diff.
|
|
26
|
-
|
|
27
|
-
## Role
|
|
28
|
-
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.
|
|
29
|
-
|
|
30
|
-
You help users by reading files, executing commands, editing code, and writing new files. 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.
|
|
31
|
-
|
|
32
|
-
## Tool Usage
|
|
33
|
-
- The **task** tool is the preferred way to work. Use it for multi-step research, exploration, or implementation tasks.
|
|
34
|
-
- Use **bash**, **edit**, and other tools only for single, immediate actions.
|
|
35
|
-
- **Do NOT** chain multiple bash/edit calls for multi-step work. Use **task** instead.
|
|
36
|
-
- If a job needs more than one tool call, use the **task** tool instead of chaining individual calls.
|
|
37
|
-
|
|
38
|
-
<example>
|
|
39
|
-
When referencing specific functions or pieces of code, include the pattern \`file_path:line_number\`.
|
|
40
|
-
For example: "Clients are handled in the \`connectToServer\` function in src/services/process.ts:712."
|
|
41
|
-
</example>
|
|
42
|
-
|
|
43
|
-
## Workflow
|
|
44
|
-
- Stay rooted on the user's request. Don't wander into tangents or explore out of curiosity.
|
|
45
|
-
- Gather only the information needed to fulfill the request, then stop exploring and complete it.
|
|
46
|
-
- Narrate your edits with brief commentary during long tasks so the user can follow progress.
|
|
47
|
-
- Verify your changes via compilation, tests, or manual checks whenever possible.
|
|
48
|
-
|
|
49
|
-
## Tone
|
|
50
|
-
- Be concise. Use a jovial but motivated colleague tone: direct, never condescending, and never rude.
|
|
51
|
-
|
|
52
|
-
## Error Handling
|
|
53
|
-
- If a tool call fails or is denied, do NOT re-attempt the exact same call. Analyze why it failed and adjust your approach.
|
|
54
|
-
|
|
55
|
-
IMPORTANT: Never guess or assume. Verify claims before making them.
|
|
56
|
-
IMPORTANT: Do not over-scope work or add scope during implementation.
|
|
57
|
-
|
|
58
|
-
${safetyPrompt}
|
|
59
|
-
`;
|
|
60
|
-
|
|
61
|
-
export const TASK_PROMPT = `# You are an efficient, elite-level task Agent
|
|
62
|
-
|
|
63
|
-
## Role
|
|
64
|
-
Execute the assigned task precisely and efficiently. Prioritize correctness over speed. Focus on facts and objective technical details.
|
|
65
|
-
|
|
66
|
-
## Output Requirements
|
|
67
|
-
- Your final response must include all actions taken and exact diffs of any changes made.
|
|
68
|
-
- Provide a concise report of what was done, what was verified, and any decisions made.
|
|
69
|
-
|
|
70
|
-
${safetyPrompt}
|
|
71
|
-
`;
|
|
72
|
-
|
|
73
22
|
async function getDir() {
|
|
74
23
|
const ignoreFile = Bun.file(".gitignore");
|
|
75
24
|
let ignoreContent = "";
|
|
@@ -222,44 +171,68 @@ export async function injectEnvReminder(): Promise<string> {
|
|
|
222
171
|
return `<system-reminder>\n${envStatus}\n</system-reminder>`;
|
|
223
172
|
}
|
|
224
173
|
|
|
174
|
+
const doomLoopReminder = `<system-reminder>
|
|
175
|
+
You may be entering a tool-call doom loop: recent tool usage is repetitive or not clearly progressing.
|
|
176
|
+
|
|
177
|
+
- Stop repeating the same or similar tool call unless new evidence requires it.
|
|
178
|
+
- Re-read the user's request and summarize what is known, what failed, and what is still needed.
|
|
179
|
+
- Change strategy before calling more tools: narrow the next check, use a different source of evidence, or ask the user if blocked.
|
|
180
|
+
- If the request is complete, stop calling tools and provide the final answer.
|
|
181
|
+
</system-reminder>`;
|
|
182
|
+
|
|
183
|
+
// Checks recent tool usage for simple repeated-call patterns and inserts an
|
|
184
|
+
// anti-doom-loop reminder when the agent appears stuck.
|
|
225
185
|
export function insertToolUsageReminder(
|
|
226
186
|
messages: Message[],
|
|
227
187
|
toolMessage: ToolResultMessage,
|
|
228
|
-
) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
const lastUserMessageIndex = messages.findLastIndex((m) => m.role === "user");
|
|
240
|
-
const messagesSinceLastUser = messages.slice(lastUserMessageIndex + 1);
|
|
241
|
-
|
|
242
|
-
messagesSinceLastUser.forEach((m) => {
|
|
243
|
-
if (m.role === "assistant") {
|
|
244
|
-
const toolCallsBlocks = m.content.filter((b) => b.type === "toolCall");
|
|
245
|
-
toolCalls.push(...toolCallsBlocks);
|
|
246
|
-
}
|
|
188
|
+
): ToolResultMessage {
|
|
189
|
+
const lastReminderIdx = messages.findLastIndex((m) => {
|
|
190
|
+
return (
|
|
191
|
+
m.role === "toolResult" &&
|
|
192
|
+
m.content.find(
|
|
193
|
+
(b) => b.type === "text" && b.text.includes(doomLoopReminder),
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
const lastUserMessageIdx = messages.findLastIndex((m) => {
|
|
198
|
+
return m.role === "user";
|
|
247
199
|
});
|
|
248
|
-
const
|
|
249
|
-
|
|
200
|
+
const messagesSinceLast = messages.slice(
|
|
201
|
+
Math.max(lastReminderIdx, lastUserMessageIdx) + 1,
|
|
202
|
+
);
|
|
203
|
+
const minMessagesForReminder = 4;
|
|
204
|
+
|
|
205
|
+
if (messagesSinceLast.length < minMessagesForReminder) return toolMessage;
|
|
250
206
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
207
|
+
let errorCount = 0;
|
|
208
|
+
let sameToolCount = 0;
|
|
209
|
+
const seenArgs = new Set<string>();
|
|
254
210
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
</system-reminder>
|
|
211
|
+
for (const msg of messagesSinceLast) {
|
|
212
|
+
if (msg.role === "toolResult" && msg.isError) errorCount++;
|
|
258
213
|
|
|
259
|
-
|
|
214
|
+
if (msg.role === "assistant") {
|
|
215
|
+
const calls = msg.content.filter((b) => b.type === "toolCall");
|
|
216
|
+
for (const c of calls) {
|
|
217
|
+
const args = JSON.stringify(c.arguments);
|
|
218
|
+
if (seenArgs.has(args)) sameToolCount++;
|
|
219
|
+
seenArgs.add(args)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
260
222
|
}
|
|
261
223
|
|
|
262
|
-
|
|
224
|
+
if (
|
|
225
|
+
errorCount >= minMessagesForReminder ||
|
|
226
|
+
sameToolCount >= minMessagesForReminder
|
|
227
|
+
) {
|
|
228
|
+
return {
|
|
229
|
+
...toolMessage,
|
|
230
|
+
content: [
|
|
231
|
+
...toolMessage.content,
|
|
232
|
+
{ type: "text", text: doomLoopReminder },
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
263
236
|
|
|
264
237
|
return toolMessage;
|
|
265
238
|
}
|
package/src/session.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import type { Message } from "@
|
|
3
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
4
4
|
import { Value } from "typebox/value";
|
|
5
5
|
import { SESSIONS_DIR } from "./shared";
|
|
6
6
|
import { type Session, SessionSchema } from "./types";
|
package/src/tool-bash.ts
CHANGED
|
@@ -1,24 +1,12 @@
|
|
|
1
|
-
import { type Tool, Type } from "@
|
|
1
|
+
import { type Tool, Type } from "@earendil-works/pi-ai";
|
|
2
2
|
import { secureRandomString } from "./shared";
|
|
3
3
|
import type { ToolRunnerEvent } from "./types";
|
|
4
4
|
|
|
5
5
|
const OUTPUT_THRESHOLD = 16000;
|
|
6
|
-
const description =
|
|
6
|
+
const description = `Bash CLI tool
|
|
7
7
|
|
|
8
8
|
Execute shell commands on the user's environment.
|
|
9
9
|
|
|
10
|
-
Best practices:
|
|
11
|
-
|
|
12
|
-
- Use \`ls\` to list files.
|
|
13
|
-
- Use \`fd\` or \`find\` to locate files/directories by name, type, size, time, permissions, etc.
|
|
14
|
-
- Use \`rg\` or \`grep\` to search inside files for matching patterns.
|
|
15
|
-
- Use \`cat -n\` or \`nl\` to read small files, or when you need the whole file.
|
|
16
|
-
- Use \`sed -n\` with ranges to read sections of files. Prefer targeted reads. Avoid dumping very large (more than ~200 lines) files all at once.
|
|
17
|
-
- Use \`cp\`, \`mv\`, and \`mkdir\` for file and directory operations, and \`rm\` to remove files and directories.
|
|
18
|
-
- Use \`curl\` for web access. Use redirection to temp files for targeted reads.
|
|
19
|
-
- Use development tools like \`git\`, \`gh\`, \`jq\`, etc, when appropriate.
|
|
20
|
-
- Prefer \`cp -i\`, \`mv -i\`, \`rm -i\` when learning.
|
|
21
|
-
- NEVER run destructive commands (\`rm -rf\`, \`git reset --hard\`, overwriting files) without confirming the target first.
|
|
22
10
|
- Chain commands **only** when failure should stop the flow. Avoid long chains, **2 to 3 maximum**.
|
|
23
11
|
- Avoid overly complex one-liners; readability matters.
|
|
24
12
|
- Quote filenames: use \`"$file"\` not \`$file\`.
|
package/src/tool-edit.ts
CHANGED
|
@@ -1,35 +1,23 @@
|
|
|
1
1
|
import { isAbsolute, join } from "node:path";
|
|
2
|
-
import { type Tool, Type } from "@
|
|
2
|
+
import { type Tool, Type } from "@earendil-works/pi-ai";
|
|
3
3
|
import { createPatch } from "diff";
|
|
4
4
|
import type { ToolRunnerEvent } from "./types";
|
|
5
5
|
|
|
6
|
-
const description =
|
|
6
|
+
const description = `Edit tool
|
|
7
7
|
|
|
8
8
|
A find-and-replace file editor. Use it to create new files or modify existing ones safely. Always prefer this tool over bash editing methods (sed, awk, etc).
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Rules
|
|
11
|
+
|
|
11
12
|
- The tool refuses to edit on multiple matches of \`oldText\`. Be specific with your matching text.
|
|
12
13
|
- Prefer patch-based edits (small targeted replacements) for multi-line or semantic changes.
|
|
13
14
|
- Do NOT reproduce entire files. Use shell file operations (\`cp\`, \`mv\`, etc) for wholesale file replacement instead.
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
Failure modes
|
|
17
|
+
|
|
16
18
|
- If \`oldText\` is not found, the edit fails. Verify the exact text first.
|
|
17
19
|
- If \`oldText\` matches multiple locations, the edit fails. Narrow your match and retry.
|
|
18
20
|
- If the file does not exist and \`oldText\` is non-empty, the edit fails.
|
|
19
|
-
|
|
20
|
-
<example>
|
|
21
|
-
Edit a single line:
|
|
22
|
-
path: src/utils.ts
|
|
23
|
-
oldText: const MAX_RETRIES = 3;
|
|
24
|
-
newText: const MAX_RETRIES = 5;
|
|
25
|
-
</example>
|
|
26
|
-
|
|
27
|
-
<example>
|
|
28
|
-
Patch-based edit (preferred for multi-line changes):
|
|
29
|
-
path: src/utils.ts
|
|
30
|
-
oldText: function oldHelper() {\n return 1;\n}
|
|
31
|
-
newText: function newHelper() {\n return 2;\n}\n\nfunction oldHelper() {\n return 1;\n}
|
|
32
|
-
</example>
|
|
33
21
|
`;
|
|
34
22
|
|
|
35
23
|
export const edit: Tool = {
|
|
@@ -126,7 +114,7 @@ export async function* runEditTool(
|
|
|
126
114
|
|
|
127
115
|
yield {
|
|
128
116
|
type: "result",
|
|
129
|
-
text:
|
|
117
|
+
text: patch,
|
|
130
118
|
};
|
|
131
119
|
|
|
132
120
|
return;
|
package/src/tool-read.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { extname, isAbsolute, join } from "node:path";
|
|
3
|
+
import { type Tool, Type } from "@earendil-works/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
|
+
};
|
|
15
|
+
|
|
16
|
+
const description = `Read tool
|
|
17
|
+
|
|
18
|
+
Read a file by path.
|
|
19
|
+
|
|
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
|
+
`;
|
|
23
|
+
|
|
24
|
+
export const read: Tool = {
|
|
25
|
+
name: "read",
|
|
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
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
|
|
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}` };
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (signal?.aborted) {
|
|
63
|
+
yield { type: "result", text: "Aborted before read." };
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
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 },
|
|
75
|
+
};
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const content = await file.text();
|
|
80
|
+
const lines = content.split(/\r?\n/);
|
|
81
|
+
if (content.endsWith("\n")) lines.pop();
|
|
82
|
+
|
|
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");
|
|
93
|
+
|
|
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.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
yield { type: "result", text };
|
|
100
|
+
}
|
package/src/tui-components.ts
CHANGED
|
@@ -114,11 +114,11 @@ export function ContextPill(state: TUIState) {
|
|
|
114
114
|
const smartPercent = Math.floor((state.contextSize / smartMax) * 100);
|
|
115
115
|
if (smartPercent > 90) {
|
|
116
116
|
bg = theme.bred;
|
|
117
|
-
} else if (smartPercent >
|
|
117
|
+
} else if (smartPercent > 85) {
|
|
118
118
|
bg = theme.red;
|
|
119
|
-
} else if (smartPercent >
|
|
119
|
+
} else if (smartPercent > 80) {
|
|
120
120
|
bg = theme.yellow;
|
|
121
|
-
} else if (smartPercent >
|
|
121
|
+
} else if (smartPercent > 60) {
|
|
122
122
|
bg = theme.byellow;
|
|
123
123
|
}
|
|
124
124
|
text = `~${percent}%`;
|
|
@@ -126,7 +126,7 @@ export function ContextPill(state: TUIState) {
|
|
|
126
126
|
|
|
127
127
|
text += ` (${state.options.model.contextWindow / 1000}k)`;
|
|
128
128
|
|
|
129
|
-
return TextPill(text, theme.
|
|
129
|
+
return TextPill(text, theme.black, bg);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
export function GitPill(state: TUIState) {
|
|
@@ -137,13 +137,13 @@ export function ModelPill(state: TUIState) {
|
|
|
137
137
|
// Like loot: Gold/purple/blue/green/white -> xhigh/high/medium/low/minimal
|
|
138
138
|
switch (state.options.effort) {
|
|
139
139
|
case "xhigh":
|
|
140
|
-
return TextPill(state.options.model.name, theme.
|
|
140
|
+
return TextPill(state.options.model.name, theme.black, theme.yellow);
|
|
141
141
|
case "high":
|
|
142
|
-
return TextPill(state.options.model.name, theme.
|
|
142
|
+
return TextPill(state.options.model.name, theme.black, theme.magenta);
|
|
143
143
|
case "medium":
|
|
144
|
-
return TextPill(state.options.model.name, theme.
|
|
144
|
+
return TextPill(state.options.model.name, theme.black, theme.blue);
|
|
145
145
|
case "low":
|
|
146
|
-
return TextPill(state.options.model.name, theme.
|
|
146
|
+
return TextPill(state.options.model.name, theme.black, theme.green);
|
|
147
147
|
}
|
|
148
148
|
// Minimal
|
|
149
149
|
return TextPill(state.options.model.name, theme.bwhite, theme.bblack);
|
package/src/tui-conversation.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
Message,
|
|
6
6
|
ToolResultMessage,
|
|
7
7
|
UserMessage,
|
|
8
|
-
} from "@
|
|
8
|
+
} from "@earendil-works/pi-ai";
|
|
9
9
|
import { estimateTokens, relativeTime } from "./shared";
|
|
10
10
|
import { TextPill, theme } from "./tui-components";
|
|
11
11
|
import type { TUIState } from "./types";
|
|
@@ -33,9 +33,6 @@ function agentMessageNode(msg: AssistantMessage): Node {
|
|
|
33
33
|
} else if ("command" in block.arguments) {
|
|
34
34
|
text = block.arguments.command;
|
|
35
35
|
node = SyntaxHighlight(text, "bash");
|
|
36
|
-
} else if ("prompt" in block.arguments) {
|
|
37
|
-
text = block.arguments.prompt;
|
|
38
|
-
node = SyntaxHighlight(text, "markdown");
|
|
39
36
|
} else {
|
|
40
37
|
text = JSON.stringify(block.arguments);
|
|
41
38
|
node = Text(text);
|
|
@@ -119,22 +116,33 @@ function userMessageNode(msg: UserMessage): Node {
|
|
|
119
116
|
}
|
|
120
117
|
|
|
121
118
|
function toolMessageNode(msg: ToolResultMessage): Node {
|
|
119
|
+
// TODO: read tool image output
|
|
122
120
|
// Output only shows last 10 lines of scroll.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
if (block.type === "text") {
|
|
133
|
-
return Text(block.text, { wrap: "word", fgColor: theme.bblack });
|
|
134
|
-
}
|
|
135
|
-
return Text(""); // TODO: image case needs attention
|
|
121
|
+
const text = msg.content
|
|
122
|
+
.filter((c) => c.type === "text")
|
|
123
|
+
.map((c) => c.text)
|
|
124
|
+
.join("")
|
|
125
|
+
.trim();
|
|
126
|
+
|
|
127
|
+
return VStack({ padding: { x: 4 }, gap: 1 }, [
|
|
128
|
+
Text(`~${estimateTokens(text)} tokens, ${text.split("\n").length} lines.`, {
|
|
129
|
+
fgColor: theme.bblack,
|
|
136
130
|
}),
|
|
137
|
-
|
|
131
|
+
VStack(
|
|
132
|
+
{
|
|
133
|
+
flex: 1,
|
|
134
|
+
maxHeight: msg.toolName === "edit" ? 20 : 10,
|
|
135
|
+
overflow: "scroll",
|
|
136
|
+
scrollOffset: Infinity,
|
|
137
|
+
onScroll: () => false,
|
|
138
|
+
},
|
|
139
|
+
[
|
|
140
|
+
msg.toolName === "edit"
|
|
141
|
+
? SyntaxHighlight(text, "diff")
|
|
142
|
+
: Text(text, { wrap: "word", fgColor: theme.white }),
|
|
143
|
+
],
|
|
144
|
+
),
|
|
145
|
+
]);
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
const messageBodyCache = new WeakMap<Message, { key: number; node: Node }>();
|
|
@@ -190,6 +198,7 @@ function cachedMessageBody(msg: Message): Node {
|
|
|
190
198
|
}
|
|
191
199
|
|
|
192
200
|
function conversationMessageNode(msg: Message): Node {
|
|
201
|
+
const label = msg.role === "toolResult" ? `${msg.toolName} result` : msg.role;
|
|
193
202
|
return VStack({ gap: 1 }, [
|
|
194
203
|
cachedMessageBody(msg),
|
|
195
204
|
HStack({ gap: 1, justifyContent: "end" }, [
|
|
@@ -197,7 +206,7 @@ function conversationMessageNode(msg: Message): Node {
|
|
|
197
206
|
fgColor: theme.bblack,
|
|
198
207
|
italic: true,
|
|
199
208
|
}),
|
|
200
|
-
TextPill(
|
|
209
|
+
TextPill(label, theme.bwhite, theme.bblack),
|
|
201
210
|
]),
|
|
202
211
|
]);
|
|
203
212
|
}
|
|
@@ -249,7 +258,7 @@ export function Conversation(state: TUIState) {
|
|
|
249
258
|
return VStack(
|
|
250
259
|
{
|
|
251
260
|
flex: 1,
|
|
252
|
-
gap:
|
|
261
|
+
gap: 3,
|
|
253
262
|
overflow: "scroll",
|
|
254
263
|
scrollOffset: state.stickToBottom ? Infinity : state.scrollOffset,
|
|
255
264
|
onScroll(offset, maxOffset) {
|
package/src/tui-overlay.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HStack, Text, TextInput, VStack } from "@cel-tui/core";
|
|
2
|
-
import { getModels, type ThinkingLevel } from "@
|
|
3
|
-
import { getOAuthProviders } from "@
|
|
2
|
+
import { getModels, type ThinkingLevel } from "@earendil-works/pi-ai";
|
|
3
|
+
import { getOAuthProviders } from "@earendil-works/pi-ai/oauth";
|
|
4
4
|
import { saveSettings } from "./args";
|
|
5
5
|
import { getAvailableProviders } from "./oauth";
|
|
6
6
|
import { listSessionsForCwd } from "./session";
|
package/src/tui.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { updateSession } from "./session";
|
|
|
11
11
|
import { estimateTokens, secureRandomString } from "./shared";
|
|
12
12
|
import { bash, runBashTool } from "./tool-bash";
|
|
13
13
|
import { edit, runEditTool } from "./tool-edit";
|
|
14
|
-
import {
|
|
14
|
+
import { read, runReadTool } from "./tool-read";
|
|
15
15
|
import {
|
|
16
16
|
ActivityPill,
|
|
17
17
|
ContextPill,
|
|
@@ -43,7 +43,6 @@ function clearOrAbort(state: TUIState) {
|
|
|
43
43
|
|
|
44
44
|
export function initTUI(state: TUIState, leave: (s: string) => void) {
|
|
45
45
|
// TODO: Cleanup accumulated sessions for this cwd.
|
|
46
|
-
|
|
47
46
|
const { spinnerEvery, currentSpinner } = Spinner();
|
|
48
47
|
|
|
49
48
|
// Stable 60fps rendering.
|
|
@@ -147,10 +146,7 @@ async function streamAgentTUI(state: TUIState) {
|
|
|
147
146
|
const tools: ToolAndRunner[] = [
|
|
148
147
|
{ tool: bash, runner: runBashTool },
|
|
149
148
|
{ tool: edit, runner: runEditTool },
|
|
150
|
-
{
|
|
151
|
-
tool: task,
|
|
152
|
-
runner: (args, signal) => runTaskTool(state.options, args, signal),
|
|
153
|
-
},
|
|
149
|
+
{ tool: read, runner: runReadTool },
|
|
154
150
|
];
|
|
155
151
|
|
|
156
152
|
let userContent = state.prompt;
|
package/src/types.ts
CHANGED
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
type Tool,
|
|
9
9
|
type ToolResultMessage,
|
|
10
10
|
Type,
|
|
11
|
-
} from "@
|
|
12
|
-
import type { OAuthCredentials } from "@
|
|
11
|
+
} from "@earendil-works/pi-ai";
|
|
12
|
+
import type { OAuthCredentials } from "@earendil-works/pi-ai/oauth";
|
|
13
13
|
|
|
14
14
|
const ThinkingLevelSchema = Type.Unsafe<ThinkingLevel>(
|
|
15
15
|
Type.Union([
|
|
@@ -129,7 +129,11 @@ export type AgentToolEvent =
|
|
|
129
129
|
|
|
130
130
|
export type ToolRunnerEvent =
|
|
131
131
|
| { type: "output"; text: string }
|
|
132
|
-
| {
|
|
132
|
+
| {
|
|
133
|
+
type: "result";
|
|
134
|
+
text: string;
|
|
135
|
+
image?: { data: string; mimeType: string };
|
|
136
|
+
};
|
|
133
137
|
|
|
134
138
|
export type ToolAndRunner = {
|
|
135
139
|
tool: Tool;
|