mini-coder 0.6.5 → 0.7.1

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 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.75.5",
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.4.15",
16
+ "@biomejs/biome": "^2.5.0",
17
17
  "@types/bun": "^1.3.14",
18
- "prettier": "^3.8.3",
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.4.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.15", "@biomejs/cli-darwin-x64": "2.4.15", "@biomejs/cli-linux-arm64": "2.4.15", "@biomejs/cli-linux-arm64-musl": "2.4.15", "@biomejs/cli-linux-x64": "2.4.15", "@biomejs/cli-linux-x64-musl": "2.4.15", "@biomejs/cli-win32-arm64": "2.4.15", "@biomejs/cli-win32-x64": "2.4.15" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="],
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.4.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg=="],
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.4.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ=="],
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.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug=="],
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.4.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ=="],
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.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g=="],
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.4.15", "", { "os": "linux", "cpu": "x64" }, "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w=="],
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.4.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w=="],
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.4.15", "", { "os": "win32", "cpu": "x64" }, "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ=="],
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.75.5", "", { "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-zf1F5kXk1pqZeFShXOqq9ibUk8QdtRoLCDPAjO+hj44e3EUs9/GFO2qnhTC5+JA2uwVCx+WCNe1PiCjlBYWm5w=="],
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.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
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.6.5",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
- "packageManager": "bun@1.3.12",
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.4.15",
17
+ "@biomejs/biome": "^2.5.0",
18
18
  "@types/bun": "^1.3.14",
19
- "prettier": "^3.8.3",
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.75.5",
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"
@@ -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
@@ -1,3 +1,6 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import { cel, HStack, ProcessTerminal, VStack } from "@cel-tui/core";
2
5
  import type {
3
6
  AssistantMessage,
@@ -42,6 +45,52 @@ function clearOrAbort(state: TUIState) {
42
45
  }
43
46
  }
44
47
 
48
+ async function directoryExists(path: string): Promise<boolean> {
49
+ try {
50
+ return (await stat(path)).isDirectory();
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ async function handleSkillSlashCommand(prompt: string) {
57
+ const match = prompt.trim().match(/^\/([a-zA-Z0-9_-]+)(?:\s|$)/);
58
+
59
+ if (!match) return;
60
+
61
+ const skillName = match[1];
62
+ const skillRoots = [
63
+ join(homedir(), ".agents", "skills"),
64
+ join(process.cwd(), ".agents", "skills"),
65
+ ];
66
+
67
+ for (const root of skillRoots) {
68
+ if (!(await directoryExists(root))) {
69
+ continue;
70
+ }
71
+
72
+ const glob = new Bun.Glob("*/SKILL.md");
73
+
74
+ for await (const path of glob.scan({ cwd: root, absolute: true })) {
75
+ const file = Bun.file(path);
76
+
77
+ if (!(await file.exists())) {
78
+ continue;
79
+ }
80
+
81
+ const body = await file.text();
82
+ const frontmatter = body.match(/^---\s*\n([\s\S]*?)\n---/);
83
+ const name = frontmatter?.[1]
84
+ .match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]
85
+ ?.trim();
86
+
87
+ if (name === skillName) {
88
+ return body;
89
+ }
90
+ }
91
+ }
92
+ }
93
+
45
94
  export function initTUI(state: TUIState, leave: (s: string) => void) {
46
95
  // TODO: Cleanup accumulated sessions for this cwd.
47
96
  const { spinnerEvery, currentSpinner } = Spinner();
@@ -59,6 +108,8 @@ export function initTUI(state: TUIState, leave: (s: string) => void) {
59
108
  cel.render();
60
109
  }, 1000 / fps);
61
110
 
111
+ let menu = mainMenu(state);
112
+
62
113
  const onWindowKeyPress = (key: string) => {
63
114
  if (key === "ctrl+q" || key === "ctrl+c" || key === "ctrl+d") {
64
115
  // Quit
@@ -69,6 +120,10 @@ export function initTUI(state: TUIState, leave: (s: string) => void) {
69
120
  // Abort or clear prompt
70
121
  clearOrAbort(state);
71
122
  } else if (key === "ctrl+p") {
123
+ menu = mainMenu(state);
124
+ state.overlay = true;
125
+ } else if (key === "ctrl+f") {
126
+ menu = mainMenu(state, "fork");
72
127
  state.overlay = true;
73
128
  }
74
129
  };
@@ -78,6 +133,19 @@ export function initTUI(state: TUIState, leave: (s: string) => void) {
78
133
  };
79
134
 
80
135
  const onEditorKeyPress = (key: string) => {
136
+ const submit = async () => {
137
+ await streamAgentTUI(state);
138
+ };
139
+ const skillSubmit = async () => {
140
+ const skill = await handleSkillSlashCommand(state.prompt);
141
+ if (skill) {
142
+ state.streaming = true;
143
+ // This cause the prompt to flash the body before vanishing on submit.
144
+ // To avoid this I added a new state property
145
+ state.promptSkill = skill;
146
+ await streamAgentTUI(state);
147
+ }
148
+ };
81
149
  // onKeyPress
82
150
  if (key === "enter") {
83
151
  if (state.prompt === ":q") {
@@ -96,16 +164,15 @@ export function initTUI(state: TUIState, leave: (s: string) => void) {
96
164
  state.stickToBottom = true;
97
165
  return false;
98
166
  }
99
- const submit = async () => {
100
- await streamAgentTUI(state);
101
- };
167
+ if (state.prompt && state.prompt[0] === "/" && !state.streaming) {
168
+ skillSubmit();
169
+ return false;
170
+ }
102
171
  if (state.prompt && !state.streaming) submit();
103
172
  return false;
104
173
  }
105
174
  };
106
175
 
107
- const menu = mainMenu(state);
108
-
109
176
  cel.init(new ProcessTerminal());
110
177
  cel.viewport(() => {
111
178
  const layers = [
@@ -140,6 +207,8 @@ export function initTUI(state: TUIState, leave: (s: string) => void) {
140
207
  }
141
208
 
142
209
  async function streamAgentTUI(state: TUIState) {
210
+ // Just as a defensive thought, callers should set this ahead of time if doing other
211
+ // ops before starting the stream.
143
212
  state.streaming = true;
144
213
 
145
214
  const abortController = new AbortController();
@@ -152,6 +221,12 @@ async function streamAgentTUI(state: TUIState) {
152
221
  ];
153
222
 
154
223
  let userContent = state.prompt;
224
+ if (state.promptSkill) {
225
+ // TODO: Skill arguments? replace template marks in skill body for arguments?
226
+ // Right now we just appead the user prompt to the skill body
227
+ userContent = `${state.promptSkill}\n${state.prompt}`;
228
+ state.promptSkill = undefined;
229
+ }
155
230
  if (state.messages.length === 0) {
156
231
  const envReminder = await injectEnvReminder();
157
232
  userContent = `${envReminder}\n\n${userContent}`;
package/src/types.ts CHANGED
@@ -94,6 +94,7 @@ export type TUIMessage = {
94
94
  export type TUIState = {
95
95
  options: CliOptions;
96
96
  prompt: string;
97
+ promptSkill?: string;
97
98
  messages: Message[]; // Context messages
98
99
  tuiMessages: TUIMessage[];
99
100
  contextSize?: number;