mini-coder 0.5.6 → 0.5.8
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 +5 -2
- package/bun.lock +13 -13
- package/package.json +3 -3
- package/src/agent.ts +299 -41
- package/src/cli.ts +11 -2
- package/src/headless.ts +185 -47
- package/src/index.ts +105 -12
- package/src/input.ts +1 -0
- package/src/prompt.ts +86 -78
- package/src/session.ts +56 -8
- package/src/submit.ts +53 -11
- package/src/tools.ts +401 -44
- package/src/ui/agent.ts +16 -2
- package/src/ui/commands.test.ts +72 -0
- package/src/ui/commands.ts +13 -0
- package/src/ui/conversation.test.ts +91 -0
- package/src/ui/conversation.ts +122 -3
- package/src/ui/help.test.ts +1 -0
- package/src/ui/help.ts +1 -0
- package/src/ui/input.test.ts +29 -1
- package/src/ui/input.ts +45 -51
- package/src/ui/render-performance.test.ts +1 -0
- package/src/ui/status.test.ts +1 -0
- package/src/ui.ts +85 -18
package/src/ui/input.ts
CHANGED
|
@@ -25,39 +25,18 @@ export interface InputController {
|
|
|
25
25
|
onKeyPress: (key: string) => boolean | undefined;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let prefix = values[0]!;
|
|
35
|
-
for (let i = 1; i < values.length && prefix.length > 0; i++) {
|
|
36
|
-
const value = values[i]!;
|
|
37
|
-
let j = 0;
|
|
38
|
-
while (j < prefix.length && j < value.length && prefix[j] === value[j]) {
|
|
39
|
-
j++;
|
|
40
|
-
}
|
|
41
|
-
prefix = prefix.slice(0, j);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return prefix;
|
|
28
|
+
/** A selectable path-autocomplete match for the current input draft. */
|
|
29
|
+
export interface InputPathMatch {
|
|
30
|
+
/** Path label shown in the overlay. */
|
|
31
|
+
label: string;
|
|
32
|
+
/** Full draft value to apply when this match is selected. */
|
|
33
|
+
value: string;
|
|
45
34
|
}
|
|
46
35
|
|
|
47
|
-
|
|
48
|
-
* Attempt to autocomplete the final path token in the current draft.
|
|
49
|
-
*
|
|
50
|
-
* @param value - Current input draft.
|
|
51
|
-
* @param cwd - Working directory used to resolve relative paths.
|
|
52
|
-
* @returns The completed input value, or `null` when no completion is available.
|
|
53
|
-
*/
|
|
54
|
-
export function autocompleteInputPath(
|
|
55
|
-
value: string,
|
|
56
|
-
cwd: string,
|
|
57
|
-
): string | null {
|
|
36
|
+
function listInputPathMatches(value: string, cwd: string): InputPathMatch[] {
|
|
58
37
|
const tokenMatch = /(^|\s)(\S+)$/.exec(value);
|
|
59
38
|
if (!tokenMatch?.[2]) {
|
|
60
|
-
return
|
|
39
|
+
return [];
|
|
61
40
|
}
|
|
62
41
|
|
|
63
42
|
const token = tokenMatch[2];
|
|
@@ -80,35 +59,50 @@ export function autocompleteInputPath(
|
|
|
80
59
|
}
|
|
81
60
|
})();
|
|
82
61
|
if (!entries) {
|
|
83
|
-
return
|
|
62
|
+
return [];
|
|
84
63
|
}
|
|
85
64
|
|
|
86
65
|
const showHidden = partial.startsWith(".");
|
|
87
|
-
|
|
66
|
+
return entries
|
|
88
67
|
.filter((entry) => (showHidden ? true : !entry.name.startsWith(".")))
|
|
89
68
|
.filter((entry) => entry.name.startsWith(partial))
|
|
90
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
completedName = `${match.name}${match.isDirectory() ? "/" : ""}`;
|
|
100
|
-
} else {
|
|
101
|
-
const prefix = getLongestCommonPrefix(matches.map((entry) => entry.name));
|
|
102
|
-
if (prefix.length > partial.length) {
|
|
103
|
-
completedName = prefix;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
69
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
70
|
+
.map((entry) => {
|
|
71
|
+
const completedPath = `${dirToken}${entry.name}${entry.isDirectory() ? "/" : ""}`;
|
|
72
|
+
return {
|
|
73
|
+
label: completedPath,
|
|
74
|
+
value: `${value.slice(0, tokenStart)}${completedPath}`,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
106
78
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Find selectable path matches for the final path token in the current draft.
|
|
81
|
+
*
|
|
82
|
+
* @param value - Current input draft.
|
|
83
|
+
* @param cwd - Working directory used to resolve relative paths.
|
|
84
|
+
* @returns Matching path completions with their applied draft values.
|
|
85
|
+
*/
|
|
86
|
+
export function findInputPathMatches(
|
|
87
|
+
value: string,
|
|
88
|
+
cwd: string,
|
|
89
|
+
): InputPathMatch[] {
|
|
90
|
+
return listInputPathMatches(value, cwd);
|
|
91
|
+
}
|
|
110
92
|
|
|
111
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Attempt to autocomplete the final path token in the current draft.
|
|
95
|
+
*
|
|
96
|
+
* @param value - Current input draft.
|
|
97
|
+
* @param cwd - Working directory used to resolve relative paths.
|
|
98
|
+
* @returns The completed input value, or `null` when direct completion is unavailable.
|
|
99
|
+
*/
|
|
100
|
+
export function autocompleteInputPath(
|
|
101
|
+
value: string,
|
|
102
|
+
cwd: string,
|
|
103
|
+
): string | null {
|
|
104
|
+
const matches = listInputPathMatches(value, cwd);
|
|
105
|
+
return matches.length === 1 ? matches[0]!.value : null;
|
|
112
106
|
}
|
|
113
107
|
|
|
114
108
|
/**
|
package/src/ui/status.test.ts
CHANGED
package/src/ui.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
12
12
|
import { platform } from "node:os";
|
|
13
|
+
import { Select } from "@cel-tui/components";
|
|
13
14
|
import {
|
|
14
15
|
cel,
|
|
15
16
|
HStack,
|
|
@@ -21,7 +22,11 @@ import {
|
|
|
21
22
|
import type { Node } from "@cel-tui/types";
|
|
22
23
|
import type { AppState } from "./index.ts";
|
|
23
24
|
import { reloadPromptContext, shutdown } from "./index.ts";
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
appendMessage,
|
|
27
|
+
createUiMessage,
|
|
28
|
+
createUiTodoMessage,
|
|
29
|
+
} from "./session.ts";
|
|
25
30
|
import type { Theme } from "./theme.ts";
|
|
26
31
|
import {
|
|
27
32
|
createUiAgentController,
|
|
@@ -37,9 +42,14 @@ import {
|
|
|
37
42
|
import type { InputController } from "./ui/input.ts";
|
|
38
43
|
import {
|
|
39
44
|
autocompleteInputPath,
|
|
45
|
+
findInputPathMatches,
|
|
40
46
|
renderInputArea as renderInputAreaNode,
|
|
41
47
|
} from "./ui/input.ts";
|
|
42
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
type ActiveOverlay,
|
|
50
|
+
OVERLAY_MAX_VISIBLE,
|
|
51
|
+
renderOverlay,
|
|
52
|
+
} from "./ui/overlay.ts";
|
|
43
53
|
import { renderStatusBar } from "./ui/status.ts";
|
|
44
54
|
|
|
45
55
|
export type { InputController } from "./ui/input.ts";
|
|
@@ -286,6 +296,48 @@ function dismissOverlay(): void {
|
|
|
286
296
|
cel.render();
|
|
287
297
|
}
|
|
288
298
|
|
|
299
|
+
function openPathAutocompleteOverlay(state: AppState): void {
|
|
300
|
+
const matches = findInputPathMatches(inputValue, state.cwd);
|
|
301
|
+
if (matches.length <= 1) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const select = Select({
|
|
306
|
+
items: matches.map((match) => ({
|
|
307
|
+
label: match.label,
|
|
308
|
+
value: match.value,
|
|
309
|
+
filterText: match.label,
|
|
310
|
+
})),
|
|
311
|
+
maxVisible: OVERLAY_MAX_VISIBLE,
|
|
312
|
+
placeholder: "type to filter paths...",
|
|
313
|
+
focused: true,
|
|
314
|
+
highlightColor: state.theme.accentText,
|
|
315
|
+
onSelect: (value) => {
|
|
316
|
+
inputValue = value;
|
|
317
|
+
dismissOverlay();
|
|
318
|
+
},
|
|
319
|
+
onBlur: dismissOverlay,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
openOverlay({ select, title: "Path matches" });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function handleTabKeyPress(state: AppState): void {
|
|
326
|
+
if (inputValue.startsWith("/")) {
|
|
327
|
+
commandController.showCommandAutocomplete(state);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const completedInput = autocompleteInputPath(inputValue, state.cwd);
|
|
332
|
+
if (completedInput) {
|
|
333
|
+
inputValue = completedInput;
|
|
334
|
+
cel.render();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
openPathAutocompleteOverlay(state);
|
|
339
|
+
}
|
|
340
|
+
|
|
289
341
|
/**
|
|
290
342
|
* Render the active overlay when one is open.
|
|
291
343
|
*
|
|
@@ -344,15 +396,7 @@ export function createInputController(state: AppState): InputController {
|
|
|
344
396
|
return false;
|
|
345
397
|
}
|
|
346
398
|
if (key === "tab") {
|
|
347
|
-
|
|
348
|
-
commandController.showCommandAutocomplete(state);
|
|
349
|
-
} else {
|
|
350
|
-
const completedInput = autocompleteInputPath(inputValue, state.cwd);
|
|
351
|
-
if (completedInput) {
|
|
352
|
-
inputValue = completedInput;
|
|
353
|
-
cel.render();
|
|
354
|
-
}
|
|
355
|
-
}
|
|
399
|
+
handleTabKeyPress(state);
|
|
356
400
|
return false;
|
|
357
401
|
}
|
|
358
402
|
},
|
|
@@ -426,6 +470,18 @@ function scrollConversationToBottom(): void {
|
|
|
426
470
|
stickToBottom = true;
|
|
427
471
|
}
|
|
428
472
|
|
|
473
|
+
function appendUiMessage(
|
|
474
|
+
message: AppState["messages"][number],
|
|
475
|
+
state: AppState,
|
|
476
|
+
): void {
|
|
477
|
+
if (state.session) {
|
|
478
|
+
appendMessage(state.db, state.session.id, message);
|
|
479
|
+
}
|
|
480
|
+
state.messages.push(message);
|
|
481
|
+
scrollConversationToBottom();
|
|
482
|
+
cel.render();
|
|
483
|
+
}
|
|
484
|
+
|
|
429
485
|
/**
|
|
430
486
|
* Append a UI-only info message to the conversation log.
|
|
431
487
|
*
|
|
@@ -436,13 +492,23 @@ function scrollConversationToBottom(): void {
|
|
|
436
492
|
* @param state - Application state.
|
|
437
493
|
*/
|
|
438
494
|
function appendInfoMessage(text: string, state: AppState): void {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
495
|
+
appendUiMessage(createUiMessage(text), state);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Append a UI-only todo snapshot to the conversation log.
|
|
500
|
+
*
|
|
501
|
+
* When no persisted session exists yet, the message stays in memory and is
|
|
502
|
+
* backfilled if the user later starts a session by sending a message.
|
|
503
|
+
*
|
|
504
|
+
* @param todos - Todo snapshot to append.
|
|
505
|
+
* @param state - Application state.
|
|
506
|
+
*/
|
|
507
|
+
function appendTodoMessage(
|
|
508
|
+
todos: Parameters<typeof createUiTodoMessage>[0],
|
|
509
|
+
state: AppState,
|
|
510
|
+
): void {
|
|
511
|
+
appendUiMessage(createUiTodoMessage(todos), state);
|
|
446
512
|
}
|
|
447
513
|
|
|
448
514
|
/** Command controller bound to the module-scoped UI runtime hooks. */
|
|
@@ -453,6 +519,7 @@ const commandController = createCommandController({
|
|
|
453
519
|
inputValue = value;
|
|
454
520
|
},
|
|
455
521
|
appendInfoMessage,
|
|
522
|
+
appendTodoMessage,
|
|
456
523
|
scrollConversationToBottom,
|
|
457
524
|
render: () => {
|
|
458
525
|
cel.render();
|