mini-coder 0.6.2 → 0.6.5
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 +108 -188
- package/package.json +5 -5
- package/src/agent.ts +11 -1
- package/src/git.ts +23 -0
- package/src/index.ts +3 -8
- package/src/oauth.ts +45 -12
- package/src/prompt.ts +3 -8
- package/src/session.ts +6 -4
- package/src/tool-bash.ts +2 -20
- package/src/tui-components.ts +1 -1
- package/src/tui-conversation.ts +123 -221
- package/src/tui-overlay.ts +64 -2
- package/src/tui.ts +84 -27
- package/src/types.ts +17 -1
package/src/tui-overlay.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { HStack, Text, TextInput, VStack } from "@cel-tui/core";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getModels,
|
|
4
|
+
type Message,
|
|
5
|
+
type ThinkingLevel,
|
|
6
|
+
} from "@earendil-works/pi-ai";
|
|
3
7
|
import { getOAuthProviders } from "@earendil-works/pi-ai/oauth";
|
|
4
8
|
import { saveSettings } from "./args";
|
|
5
9
|
import { getAvailableProviders } from "./oauth";
|
|
6
10
|
import { listSessionsForCwd } from "./session";
|
|
7
|
-
import { estimateTokens } from "./shared";
|
|
11
|
+
import { estimateTokens, formatTimestamp } from "./shared";
|
|
8
12
|
import { TextPill, theme } from "./tui-components";
|
|
9
13
|
import type { SelectOptions, SelectState, Session, TUIState } from "./types";
|
|
10
14
|
|
|
@@ -302,6 +306,61 @@ export function mainMenu(state: TUIState) {
|
|
|
302
306
|
state.overlay = false;
|
|
303
307
|
};
|
|
304
308
|
|
|
309
|
+
const toTUIMessage = (msg: Message) => {
|
|
310
|
+
const textFromContent = (
|
|
311
|
+
content: string | { type: string; text?: string }[],
|
|
312
|
+
) =>
|
|
313
|
+
typeof content === "string"
|
|
314
|
+
? content
|
|
315
|
+
: content
|
|
316
|
+
.filter((c) => c.type === "text")
|
|
317
|
+
.map((c) => c.text ?? "")
|
|
318
|
+
.join("")
|
|
319
|
+
.trim();
|
|
320
|
+
|
|
321
|
+
if (msg.role === "user") {
|
|
322
|
+
return {
|
|
323
|
+
timestamp: formatTimestamp(msg.timestamp),
|
|
324
|
+
role: "user" as const,
|
|
325
|
+
text: textFromContent(msg.content)
|
|
326
|
+
.replaceAll(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "")
|
|
327
|
+
.trim(),
|
|
328
|
+
};
|
|
329
|
+
} else {
|
|
330
|
+
const text = msg.content
|
|
331
|
+
.filter((c) => c.type === "text")
|
|
332
|
+
.map((c) => c.text)
|
|
333
|
+
.join("")
|
|
334
|
+
.trim();
|
|
335
|
+
const thinking = msg.content
|
|
336
|
+
.filter((c) => c.type === "thinking")
|
|
337
|
+
.map((c) => c.thinking)
|
|
338
|
+
.join("")
|
|
339
|
+
.trim();
|
|
340
|
+
const toolCalls = msg.content
|
|
341
|
+
.filter((c) => c.type === "toolCall")
|
|
342
|
+
.map((c) => {
|
|
343
|
+
const toolResult = state.messages.find(
|
|
344
|
+
(m) => m.role === "toolResult" && m.toolCallId === c.id,
|
|
345
|
+
);
|
|
346
|
+
return {
|
|
347
|
+
id: c.id,
|
|
348
|
+
tool: c.name,
|
|
349
|
+
args: c.arguments,
|
|
350
|
+
output: toolResult ? textFromContent(toolResult.content) : "",
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
timestamp: formatTimestamp(msg.timestamp),
|
|
356
|
+
role: "assistant" as const,
|
|
357
|
+
text,
|
|
358
|
+
thinking,
|
|
359
|
+
toolCalls,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
305
364
|
const select = useSelectOverlay({
|
|
306
365
|
...mainPane,
|
|
307
366
|
onSelect: (s) => {
|
|
@@ -331,6 +390,9 @@ export function mainMenu(state: TUIState) {
|
|
|
331
390
|
|
|
332
391
|
state.sessionId = session.id;
|
|
333
392
|
state.messages = session.messages;
|
|
393
|
+
state.tuiMessages = session.messages
|
|
394
|
+
.filter((message) => message.role !== "toolResult")
|
|
395
|
+
.map(toTUIMessage);
|
|
334
396
|
state.prompt = "";
|
|
335
397
|
state.contextSize = estimateTokens(JSON.stringify(state.messages));
|
|
336
398
|
state.scrollOffset = 0;
|
package/src/tui.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { cel, HStack, ProcessTerminal, VStack } from "@cel-tui/core";
|
|
2
|
-
import
|
|
3
|
-
|
|
2
|
+
import type {
|
|
3
|
+
AssistantMessage,
|
|
4
|
+
ToolResultMessage,
|
|
5
|
+
} from "@earendil-works/pi-ai";
|
|
6
|
+
import { streamAgent } from "./agent";
|
|
7
|
+
import { getBranchLabel } from "./git";
|
|
4
8
|
import {
|
|
5
9
|
buildSystemPrompt,
|
|
6
10
|
injectEnvReminder,
|
|
@@ -8,7 +12,7 @@ import {
|
|
|
8
12
|
MAIN_PROMPT,
|
|
9
13
|
} from "./prompt";
|
|
10
14
|
import { updateSession } from "./session";
|
|
11
|
-
import { estimateTokens, secureRandomString } from "./shared";
|
|
15
|
+
import { estimateTokens, formatTimestamp, secureRandomString } from "./shared";
|
|
12
16
|
import { bash, runBashTool } from "./tool-bash";
|
|
13
17
|
import { edit, runEditTool } from "./tool-edit";
|
|
14
18
|
import { read, runReadTool } from "./tool-read";
|
|
@@ -24,10 +28,7 @@ import {
|
|
|
24
28
|
import { Conversation, emptyState } from "./tui-conversation";
|
|
25
29
|
import { Editor } from "./tui-editor";
|
|
26
30
|
import { mainMenu } from "./tui-overlay";
|
|
27
|
-
import type { AgentContex, ToolAndRunner, TUIState } from "./types";
|
|
28
|
-
|
|
29
|
-
// TODO: move all git things to `git.ts`
|
|
30
|
-
const git = simpleGit();
|
|
31
|
+
import type { AgentContex, ToolAndRunner, TUIMessage, TUIState } from "./types";
|
|
31
32
|
|
|
32
33
|
function clearOrAbort(state: TUIState) {
|
|
33
34
|
// Are we mid stream? Abort it.
|
|
@@ -88,6 +89,7 @@ export function initTUI(state: TUIState, leave: (s: string) => void) {
|
|
|
88
89
|
if (state.prompt === ":n" || state.prompt === "/new") {
|
|
89
90
|
state.sessionId = undefined;
|
|
90
91
|
state.messages = [];
|
|
92
|
+
state.tuiMessages = [];
|
|
91
93
|
state.prompt = "";
|
|
92
94
|
state.contextSize = 0;
|
|
93
95
|
state.scrollOffset = 0;
|
|
@@ -159,6 +161,11 @@ async function streamAgentTUI(state: TUIState) {
|
|
|
159
161
|
content: userContent,
|
|
160
162
|
timestamp: Date.now(),
|
|
161
163
|
});
|
|
164
|
+
state.tuiMessages.push({
|
|
165
|
+
timestamp: formatTimestamp(Date.now()),
|
|
166
|
+
role: "user",
|
|
167
|
+
text: state.prompt,
|
|
168
|
+
});
|
|
162
169
|
state.prompt = "";
|
|
163
170
|
|
|
164
171
|
const systemPrompt = await buildSystemPrompt(MAIN_PROMPT);
|
|
@@ -170,25 +177,84 @@ async function streamAgentTUI(state: TUIState) {
|
|
|
170
177
|
signal: state.abortController?.signal,
|
|
171
178
|
};
|
|
172
179
|
|
|
173
|
-
|
|
174
|
-
|
|
180
|
+
const toTUIMessage = (partial: AssistantMessage) => {
|
|
181
|
+
const text = partial.content
|
|
182
|
+
.filter((c) => c.type === "text")
|
|
183
|
+
.map((c) => c.text)
|
|
184
|
+
.join("")
|
|
185
|
+
.trim();
|
|
186
|
+
const thinking = partial.content
|
|
187
|
+
.filter((c) => c.type === "thinking")
|
|
188
|
+
.map((c) => c.thinking)
|
|
189
|
+
.join("")
|
|
190
|
+
.trim();
|
|
191
|
+
const toolCalls = partial.content
|
|
192
|
+
.filter((c) => c.type === "toolCall")
|
|
193
|
+
.map((c) => {
|
|
194
|
+
return {
|
|
195
|
+
id: c.id,
|
|
196
|
+
tool: c.name,
|
|
197
|
+
args: c.arguments,
|
|
198
|
+
output: "",
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
timestamp: formatTimestamp(partial.timestamp),
|
|
204
|
+
role: "assistant" as const,
|
|
205
|
+
text,
|
|
206
|
+
thinking,
|
|
207
|
+
toolCalls,
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const updateToolCall = (
|
|
212
|
+
partial: ToolResultMessage,
|
|
213
|
+
tuiMessages: TUIMessage[],
|
|
214
|
+
) => {
|
|
215
|
+
tuiMessages.forEach((c) => {
|
|
216
|
+
const parentCall = c.toolCalls?.find((t) => t.id === partial.toolCallId);
|
|
217
|
+
if (parentCall) {
|
|
218
|
+
parentCall.output = partial.content
|
|
219
|
+
.filter((c) => c.type === "text")
|
|
220
|
+
.map((c) => c.text)
|
|
221
|
+
.join("")
|
|
222
|
+
.trim();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
|
|
175
227
|
const agent = streamAgent(ctx);
|
|
176
228
|
try {
|
|
177
229
|
for await (const ev of agent) {
|
|
178
230
|
switch (ev.type) {
|
|
179
231
|
case "message_start":
|
|
232
|
+
state.tuiMessages.push(toTUIMessage(ev.partial));
|
|
233
|
+
break;
|
|
180
234
|
case "message_update":
|
|
235
|
+
state.tuiMessages[state.tuiMessages.length - 1] = toTUIMessage(
|
|
236
|
+
ev.partial,
|
|
237
|
+
);
|
|
181
238
|
break;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
239
|
+
case "message_end": {
|
|
240
|
+
state.tuiMessages[state.tuiMessages.length - 1] = toTUIMessage(
|
|
241
|
+
ev.message,
|
|
242
|
+
);
|
|
243
|
+
const { systemPrompt, tools, messages } = ctx;
|
|
244
|
+
state.contextSize = estimateTokens(
|
|
245
|
+
JSON.stringify({ systemPrompt, tools, messages }),
|
|
246
|
+
);
|
|
185
247
|
break;
|
|
248
|
+
}
|
|
186
249
|
|
|
187
250
|
case "tool_message_start":
|
|
251
|
+
updateToolCall(ev.partial, state.tuiMessages);
|
|
252
|
+
break;
|
|
188
253
|
case "tool_message_update":
|
|
254
|
+
updateToolCall(ev.partial, state.tuiMessages);
|
|
189
255
|
break;
|
|
190
|
-
|
|
191
256
|
case "tool_message_end": {
|
|
257
|
+
updateToolCall(ev.message, state.tuiMessages);
|
|
192
258
|
const withReminder = insertToolUsageReminder(
|
|
193
259
|
state.messages,
|
|
194
260
|
ev.message,
|
|
@@ -203,7 +269,10 @@ async function streamAgentTUI(state: TUIState) {
|
|
|
203
269
|
state.messages[idx] = withReminder;
|
|
204
270
|
}
|
|
205
271
|
|
|
206
|
-
|
|
272
|
+
const { systemPrompt, tools, messages } = ctx;
|
|
273
|
+
state.contextSize = estimateTokens(
|
|
274
|
+
JSON.stringify({ systemPrompt, tools, messages }),
|
|
275
|
+
);
|
|
207
276
|
}
|
|
208
277
|
}
|
|
209
278
|
}
|
|
@@ -213,20 +282,8 @@ async function streamAgentTUI(state: TUIState) {
|
|
|
213
282
|
const id = secureRandomString(10);
|
|
214
283
|
state.sessionId = id;
|
|
215
284
|
}
|
|
216
|
-
// TODO: Should we make this delta only so compaction doesn;t affect saves?
|
|
217
|
-
// I'm not sure since it we do, there is no trace in logs about compaction
|
|
218
|
-
// and that would mean the logs don't repesent the truth. Confusing decision.
|
|
219
285
|
await updateSession(state.sessionId, state.messages);
|
|
220
|
-
|
|
221
|
-
// Compact after saving, if the next turn fails because of compaction, the session is recoverable.
|
|
222
|
-
// Compact at 80k tokens, the dumb zone threshold.
|
|
223
|
-
if (estimateTokens(JSON.stringify(state.messages)) > 80000)
|
|
224
|
-
compactContext(state.messages);
|
|
225
286
|
}
|
|
226
287
|
|
|
227
|
-
|
|
228
|
-
const gitStatus = (await git.status()).isClean() ? "" : "*";
|
|
229
|
-
const gitBranch = (await git.branch()).current;
|
|
230
|
-
state.gitBranch = `${gitBranch}${gitStatus}`;
|
|
231
|
-
} catch (_) {}
|
|
288
|
+
state.gitBranch = await getBranchLabel();
|
|
232
289
|
}
|
package/src/types.ts
CHANGED
|
@@ -76,10 +76,26 @@ export const SessionSchema = Type.Object({
|
|
|
76
76
|
export type Session = Static<typeof SessionSchema>;
|
|
77
77
|
export type Sessions = Session[];
|
|
78
78
|
|
|
79
|
+
export type TUIToolCall = {
|
|
80
|
+
id: string;
|
|
81
|
+
tool: string;
|
|
82
|
+
args: Record<string, any>;
|
|
83
|
+
output: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type TUIMessage = {
|
|
87
|
+
timestamp: string;
|
|
88
|
+
role: "user" | "assistant";
|
|
89
|
+
text: string;
|
|
90
|
+
thinking?: string;
|
|
91
|
+
toolCalls?: TUIToolCall[];
|
|
92
|
+
};
|
|
93
|
+
|
|
79
94
|
export type TUIState = {
|
|
80
95
|
options: CliOptions;
|
|
81
96
|
prompt: string;
|
|
82
|
-
messages: Message[];
|
|
97
|
+
messages: Message[]; // Context messages
|
|
98
|
+
tuiMessages: TUIMessage[];
|
|
83
99
|
contextSize?: number;
|
|
84
100
|
stickToBottom: boolean;
|
|
85
101
|
scrollOffset: number;
|