mini-coder 0.5.0 → 0.5.2
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 +2 -0
- package/bun.lock +39 -40
- package/package.json +10 -11
- package/src/agent.ts +49 -1
- package/src/errors.ts +15 -0
- package/src/git.ts +50 -17
- package/src/index.ts +151 -11
- package/src/prompt.ts +19 -9
- package/src/session.ts +211 -6
- package/src/settings.ts +72 -3
- package/src/skills.ts +11 -7
- package/src/submit.ts +4 -6
- package/src/tools.ts +3 -2
- package/src/ui/agent.ts +1 -4
- package/src/ui/commands.test.ts +3 -0
- package/src/ui/commands.ts +1 -4
- package/src/ui/conversation.test.ts +165 -1
- package/src/ui/conversation.ts +268 -36
- package/src/ui/help.test.ts +18 -0
- package/src/ui/help.ts +6 -0
- package/src/ui/input.test.ts +5 -1
- package/src/ui/input.ts +7 -1
- package/src/ui/status.test.ts +4 -0
- package/src/ui.ts +96 -11
- package/src/version.ts +48 -0
package/src/ui.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
12
|
import { platform } from "node:os";
|
|
13
13
|
import {
|
|
14
14
|
cel,
|
|
@@ -57,6 +57,20 @@ const PULSE_WIDTH = 5;
|
|
|
57
57
|
/** Maximum number of committed messages rendered before older history is chunked. */
|
|
58
58
|
const CONVERSATION_CHUNK_MESSAGES = 50;
|
|
59
59
|
|
|
60
|
+
/** Centralized interactive quit rules for keypresses and submitted input. */
|
|
61
|
+
const QUIT_RULES: Readonly<{
|
|
62
|
+
/** Submitted raw inputs that trigger graceful quit. */
|
|
63
|
+
inputs: ReadonlySet<string>;
|
|
64
|
+
/** Keypresses that always trigger graceful quit. */
|
|
65
|
+
keysAlways: ReadonlySet<string>;
|
|
66
|
+
/** Keypresses that trigger graceful quit only when input is empty. */
|
|
67
|
+
keysWhenEmptyInput: ReadonlySet<string>;
|
|
68
|
+
}> = {
|
|
69
|
+
inputs: new Set([":q"]),
|
|
70
|
+
keysAlways: new Set(["ctrl+c"]),
|
|
71
|
+
keysWhenEmptyInput: new Set(["ctrl+d"]),
|
|
72
|
+
};
|
|
73
|
+
|
|
60
74
|
// ---------------------------------------------------------------------------
|
|
61
75
|
// UI state (module-scoped, not in AppState)
|
|
62
76
|
// ---------------------------------------------------------------------------
|
|
@@ -92,6 +106,23 @@ let stdinWasRaw = false;
|
|
|
92
106
|
/** Active overlay for interactive commands (/model, /effort, etc.). */
|
|
93
107
|
let activeOverlay: ActiveOverlay | null = null;
|
|
94
108
|
|
|
109
|
+
/** Determine whether a raw input line should trigger a graceful quit. */
|
|
110
|
+
export function isQuitInput(raw: string): boolean {
|
|
111
|
+
const trimmed = raw.trim();
|
|
112
|
+
return QUIT_RULES.inputs.has(trimmed);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Determine whether a keypress should trigger a graceful quit. */
|
|
116
|
+
export function isQuitKey(key: string, input: string): boolean {
|
|
117
|
+
if (QUIT_RULES.keysAlways.has(key)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (input === "" && QUIT_RULES.keysWhenEmptyInput.has(key)) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
95
126
|
/**
|
|
96
127
|
* Reset all module-scoped UI state.
|
|
97
128
|
*
|
|
@@ -299,6 +330,14 @@ export function createInputController(state: AppState): InputController {
|
|
|
299
330
|
onKeyPress: (key) => {
|
|
300
331
|
if (key === "enter") {
|
|
301
332
|
const raw = inputValue;
|
|
333
|
+
|
|
334
|
+
if (isQuitInput(raw)) {
|
|
335
|
+
inputValue = "";
|
|
336
|
+
cel.render();
|
|
337
|
+
requestGracefulExit(state);
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
302
341
|
inputValue = "";
|
|
303
342
|
cel.render();
|
|
304
343
|
handleInput(raw, state);
|
|
@@ -338,10 +377,49 @@ export function renderInputArea(
|
|
|
338
377
|
// Runtime helpers and controllers
|
|
339
378
|
// ---------------------------------------------------------------------------
|
|
340
379
|
|
|
341
|
-
/**
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
380
|
+
/** Browser process handle used by the platform opener helper. */
|
|
381
|
+
interface BrowserOpenProcess {
|
|
382
|
+
/** Detach the browser opener so the app does not wait on it. */
|
|
383
|
+
unref: () => void;
|
|
384
|
+
/** Optional error listener used by real child-process implementations. */
|
|
385
|
+
on?: (event: "error", listener: (error: Error) => void) => void;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Optional runtime overrides for browser launching. */
|
|
389
|
+
interface OpenInBrowserRuntime {
|
|
390
|
+
/** Platform used to choose the opener binary. */
|
|
391
|
+
platform?: NodeJS.Platform;
|
|
392
|
+
/** Process launcher used for tests. */
|
|
393
|
+
spawn?: (
|
|
394
|
+
command: string,
|
|
395
|
+
args: string[],
|
|
396
|
+
options: { detached: boolean; stdio: "ignore" },
|
|
397
|
+
) => BrowserOpenProcess;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Open a URL in the user's default browser without invoking a shell. */
|
|
401
|
+
export function openInBrowser(
|
|
402
|
+
url: string,
|
|
403
|
+
runtime?: OpenInBrowserRuntime,
|
|
404
|
+
): void {
|
|
405
|
+
const command =
|
|
406
|
+
(runtime?.platform ?? platform()) === "darwin" ? "open" : "xdg-open";
|
|
407
|
+
const launch =
|
|
408
|
+
runtime?.spawn ??
|
|
409
|
+
((
|
|
410
|
+
cmd: string,
|
|
411
|
+
args: string[],
|
|
412
|
+
options: { detached: boolean; stdio: "ignore" },
|
|
413
|
+
) => {
|
|
414
|
+
return spawn(cmd, args, options);
|
|
415
|
+
});
|
|
416
|
+
const child = launch(command, [url], {
|
|
417
|
+
detached: true,
|
|
418
|
+
stdio: "ignore",
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
child.on?.("error", () => {});
|
|
422
|
+
child.unref();
|
|
345
423
|
}
|
|
346
424
|
|
|
347
425
|
function scrollConversationToBottom(): void {
|
|
@@ -418,6 +496,11 @@ async function gracefulExit(state: AppState): Promise<void> {
|
|
|
418
496
|
process.exit(0);
|
|
419
497
|
}
|
|
420
498
|
|
|
499
|
+
/** Request a graceful exit and hard-fail if shutdown errors. */
|
|
500
|
+
function requestGracefulExit(state: AppState): void {
|
|
501
|
+
gracefulExit(state).catch(() => process.exit(1));
|
|
502
|
+
}
|
|
503
|
+
|
|
421
504
|
/** Restore the terminal to the shell before suspending. */
|
|
422
505
|
function suspendTerminalUi(): void {
|
|
423
506
|
stopDividerAnimation();
|
|
@@ -490,6 +573,7 @@ function renderConversationLog(state: AppState, width: number): Node {
|
|
|
490
573
|
gap: CONVERSATION_GAP,
|
|
491
574
|
overflow: "scroll",
|
|
492
575
|
scrollbar: true,
|
|
576
|
+
justifyContent: state.messages.length === 0 ? "center" : undefined,
|
|
493
577
|
scrollOffset: stickToBottom ? Infinity : scrollOffset,
|
|
494
578
|
onScroll: (offset, maxOffset) => {
|
|
495
579
|
const wasStickToBottom = stickToBottom;
|
|
@@ -539,12 +623,8 @@ export function renderBaseLayout(
|
|
|
539
623
|
commandController.showInputHistoryOverlay(state);
|
|
540
624
|
return;
|
|
541
625
|
}
|
|
542
|
-
if (key
|
|
543
|
-
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
if (key === "ctrl+d" && inputValue === "") {
|
|
547
|
-
gracefulExit(state).catch(() => process.exit(1));
|
|
626
|
+
if (isQuitKey(key, inputValue)) {
|
|
627
|
+
requestGracefulExit(state);
|
|
548
628
|
return;
|
|
549
629
|
}
|
|
550
630
|
if (key === "ctrl+z") {
|
|
@@ -612,4 +692,9 @@ export function startUI(state: AppState): void {
|
|
|
612
692
|
if (state.running) {
|
|
613
693
|
startDividerAnimation();
|
|
614
694
|
}
|
|
695
|
+
|
|
696
|
+
// Show warnings from custom provider discovery
|
|
697
|
+
for (const warning of state.startupWarnings) {
|
|
698
|
+
appendInfoMessage(warning, state);
|
|
699
|
+
}
|
|
615
700
|
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version helpers for the interactive empty-state banner.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
/** App name shown in the empty conversation banner. */
|
|
11
|
+
export const APP_NAME = "mini-coder";
|
|
12
|
+
|
|
13
|
+
/** Fallback label used when running from a local checkout instead of a packaged install. */
|
|
14
|
+
export const DEV_VERSION_LABEL = "dev";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the version label shown in the empty conversation banner.
|
|
18
|
+
*
|
|
19
|
+
* Packaged installs read their version from the colocated `package.json`.
|
|
20
|
+
* Local checkouts fall back to a simple `dev` label so banner metadata never
|
|
21
|
+
* risks interfering with normal startup.
|
|
22
|
+
*
|
|
23
|
+
* @param appRoot - App root containing `package.json`.
|
|
24
|
+
* @returns A display label such as `v0.5.1` or `dev`.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveAppVersionLabel(
|
|
27
|
+
appRoot = join(import.meta.dir, ".."),
|
|
28
|
+
): string {
|
|
29
|
+
if (existsSync(join(appRoot, ".git"))) {
|
|
30
|
+
return DEV_VERSION_LABEL;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const packageJsonPath = join(appRoot, "package.json");
|
|
34
|
+
if (!existsSync(packageJsonPath)) {
|
|
35
|
+
return DEV_VERSION_LABEL;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
|
|
40
|
+
version?: unknown;
|
|
41
|
+
};
|
|
42
|
+
return typeof parsed.version === "string" && parsed.version !== ""
|
|
43
|
+
? `v${parsed.version}`
|
|
44
|
+
: DEV_VERSION_LABEL;
|
|
45
|
+
} catch {
|
|
46
|
+
return DEV_VERSION_LABEL;
|
|
47
|
+
}
|
|
48
|
+
}
|