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.
@@ -1,10 +1,14 @@
1
1
  import { HStack, Text, TextInput, VStack } from "@cel-tui/core";
2
- import { getModels, type ThinkingLevel } from "@earendil-works/pi-ai";
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 simpleGit from "simple-git";
3
- import { compactContext, streamAgent } from "./agent";
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
- // We send a reference to state.messages, so things just render.
174
- // We just need to react to some updates.
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
- case "message_end":
184
- state.contextSize = estimateTokens(JSON.stringify(ctx));
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
- state.contextSize = estimateTokens(JSON.stringify(ctx));
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
- try {
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;