mini-coder 0.6.5 → 0.7.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mini-coder",
3
- "version": "0.6.5",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "packageManager": "bun@1.3.12",
6
6
  "bin": {
@@ -50,15 +50,16 @@ function ConversationMessageToolCall(call: TUIToolCall) {
50
50
  const tail = call.output.trim().slice(-200);
51
51
  const blocks: Node[] = [];
52
52
 
53
- blocks.push(Text(tail, { fgColor: theme.white, wrap: "word" }));
54
-
55
53
  if (tail.length !== call.output.trim().length) {
56
54
  blocks.push(
57
55
  Text("Showing the last 200 characters", {
58
56
  fgColor: theme.bblack,
59
57
  italic: true,
60
58
  }),
59
+ Text(`[...] ${tail}`, { fgColor: theme.white, wrap: "word" }),
61
60
  );
61
+ } else {
62
+ blocks.push(Text(tail, { fgColor: theme.white, wrap: "word" }));
62
63
  }
63
64
 
64
65
  outputNode = VStack({ width: "100%" }, blocks);
@@ -183,6 +183,14 @@ export function useSelectOverlay(initialOptions: SelectOptions) {
183
183
  );
184
184
  }
185
185
 
186
+ function cleanSnippet(text: string): string {
187
+ return text
188
+ .replaceAll(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "")
189
+ .trim()
190
+ .replace(/\s+/g, " ")
191
+ .slice(0, 80);
192
+ }
193
+
186
194
  function sessionLabel(session: Session): string {
187
195
  const firstUserMessage = session.messages.find(
188
196
  (message) => message.role === "user",
@@ -195,16 +203,79 @@ function sessionLabel(session: Session): string {
195
203
  : firstUserMessage.content
196
204
  .map((block) => (block.type === "text" ? block.text : ""))
197
205
  .join(" ");
198
- const snippet = text
199
- .replaceAll(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "")
200
- .trim()
201
- .replace(/\s+/g, " ")
202
- .slice(0, 80);
206
+ const snippet = cleanSnippet(text);
203
207
 
204
208
  return snippet || session.id;
205
209
  }
206
210
 
207
- export function mainMenu(state: TUIState) {
211
+ function messageText(message: Message): string {
212
+ if (message.role === "user") {
213
+ return typeof message.content === "string"
214
+ ? message.content
215
+ : message.content
216
+ .map((content) =>
217
+ content.type === "text" ? content.text : "[image]",
218
+ )
219
+ .join(" ");
220
+ }
221
+
222
+ if (message.role === "toolResult") {
223
+ return message.content
224
+ .map((content) => (content.type === "text" ? content.text : "[image]"))
225
+ .join(" ");
226
+ }
227
+
228
+ return message.content
229
+ .map((content) => {
230
+ if (content.type === "text") return content.text;
231
+ if (content.type === "thinking") return content.thinking;
232
+ if (content.type === "toolCall") return `tool call: ${content.name}`;
233
+ return "";
234
+ })
235
+ .join(" ");
236
+ }
237
+
238
+ function messageLabel(message: Message, index: number): string {
239
+ const role =
240
+ message.role === "toolResult"
241
+ ? `toolResult:${message.toolName}`
242
+ : message.role;
243
+ const snippet = cleanSnippet(messageText(message));
244
+ const prefix = `${index + 1}. ${role} ${formatTimestamp(message.timestamp)}`;
245
+
246
+ return snippet ? `${prefix} — ${snippet}` : prefix;
247
+ }
248
+
249
+ function forkMessages(messages: Message[], selectedIndex: number): Message[] {
250
+ const forkedMessages = messages.slice(0, selectedIndex + 1);
251
+ const toolCallIds = new Set<string>();
252
+ const includedToolResultIds = new Set<string>();
253
+
254
+ for (const message of forkedMessages) {
255
+ if (message.role === "assistant") {
256
+ for (const content of message.content) {
257
+ if (content.type === "toolCall") toolCallIds.add(content.id);
258
+ }
259
+ } else if (message.role === "toolResult") {
260
+ includedToolResultIds.add(message.toolCallId);
261
+ }
262
+ }
263
+
264
+ for (const message of messages) {
265
+ if (
266
+ message.role === "toolResult" &&
267
+ toolCallIds.has(message.toolCallId) &&
268
+ !includedToolResultIds.has(message.toolCallId)
269
+ ) {
270
+ forkedMessages.push(message);
271
+ includedToolResultIds.add(message.toolCallId);
272
+ }
273
+ }
274
+
275
+ return forkedMessages;
276
+ }
277
+
278
+ export function mainMenu(state: TUIState, initialPane = "main") {
208
279
  type MenuPane = Omit<SelectOptions, "onCancel" | "onSelect">;
209
280
 
210
281
  const oauthProviders = getOAuthProviders();
@@ -264,6 +335,15 @@ export function mainMenu(state: TUIState) {
264
335
  };
265
336
  };
266
337
 
338
+ const forkPane = (): MenuPane => ({
339
+ label: "fork",
340
+ filter: "",
341
+ list: state.messages.map((message, index) => ({
342
+ label: messageLabel(message, index),
343
+ value: String(index),
344
+ })),
345
+ });
346
+
267
347
  const mainPane: MenuPane = {
268
348
  label: "main",
269
349
  filter: state.prompt.length ? state.prompt : "",
@@ -271,6 +351,7 @@ export function mainMenu(state: TUIState) {
271
351
  { label: "models and providers", value: "providers" },
272
352
  { label: "reasoning effort", value: "effort" },
273
353
  { label: "sessions", value: "sessions" },
354
+ { label: "fork", value: "fork" },
274
355
  ],
275
356
  };
276
357
  const effortPane: MenuPane = {
@@ -279,7 +360,7 @@ export function mainMenu(state: TUIState) {
279
360
  list: efforts,
280
361
  };
281
362
  const panes: MenuPane[] = [effortPane];
282
- let currentPane = mainPane;
363
+ let currentPane = initialPane === "fork" ? forkPane() : mainPane;
283
364
  let selectedProvider: string | undefined;
284
365
 
285
366
  const openPane = (s: SelectState, pane: MenuPane) => {
@@ -362,7 +443,7 @@ export function mainMenu(state: TUIState) {
362
443
  };
363
444
 
364
445
  const select = useSelectOverlay({
365
- ...mainPane,
446
+ ...currentPane,
366
447
  onSelect: (s) => {
367
448
  if (!s.selected) return;
368
449
 
@@ -379,6 +460,11 @@ export function mainMenu(state: TUIState) {
379
460
  });
380
461
  }
381
462
 
463
+ if (s.selected === "fork") {
464
+ openPane(s, forkPane());
465
+ return;
466
+ }
467
+
382
468
  const nextPane = panes.find((pane) => pane.label === s.selected);
383
469
  if (nextPane) openPane(s, nextPane);
384
470
  return;
@@ -401,6 +487,28 @@ export function mainMenu(state: TUIState) {
401
487
  return;
402
488
  }
403
489
 
490
+ if (currentPane.label === "fork") {
491
+ const selectedIndex = Number(s.selected);
492
+ if (
493
+ !Number.isInteger(selectedIndex) ||
494
+ !state.messages[selectedIndex]
495
+ ) {
496
+ return;
497
+ }
498
+
499
+ state.sessionId = undefined;
500
+ state.messages = forkMessages(state.messages, selectedIndex);
501
+ state.tuiMessages = state.messages
502
+ .filter((message) => message.role !== "toolResult")
503
+ .map(toTUIMessage);
504
+ state.prompt = "";
505
+ state.contextSize = estimateTokens(JSON.stringify(state.messages));
506
+ state.scrollOffset = 0;
507
+ state.stickToBottom = true;
508
+ closeMenu(s);
509
+ return;
510
+ }
511
+
404
512
  if (currentPane.label === "providers") {
405
513
  const provider = currentProviders.find((v) => v === s.selected);
406
514
  if (!provider) return;
package/src/tui.ts CHANGED
@@ -59,6 +59,8 @@ export function initTUI(state: TUIState, leave: (s: string) => void) {
59
59
  cel.render();
60
60
  }, 1000 / fps);
61
61
 
62
+ let menu = mainMenu(state);
63
+
62
64
  const onWindowKeyPress = (key: string) => {
63
65
  if (key === "ctrl+q" || key === "ctrl+c" || key === "ctrl+d") {
64
66
  // Quit
@@ -69,6 +71,10 @@ export function initTUI(state: TUIState, leave: (s: string) => void) {
69
71
  // Abort or clear prompt
70
72
  clearOrAbort(state);
71
73
  } else if (key === "ctrl+p") {
74
+ menu = mainMenu(state);
75
+ state.overlay = true;
76
+ } else if (key === "ctrl+f") {
77
+ menu = mainMenu(state, "fork");
72
78
  state.overlay = true;
73
79
  }
74
80
  };
@@ -104,8 +110,6 @@ export function initTUI(state: TUIState, leave: (s: string) => void) {
104
110
  }
105
111
  };
106
112
 
107
- const menu = mainMenu(state);
108
-
109
113
  cel.init(new ProcessTerminal());
110
114
  cel.viewport(() => {
111
115
  const layers = [