mini-coder 0.7.3 → 0.8.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/AGENTS.md +114 -0
- package/README.md +53 -66
- package/bin/mini-coder.ts +2 -0
- package/demo.gif +0 -0
- package/package.json +17 -20
- package/src/agent.ts +177 -255
- package/src/cli.ts +101 -0
- package/src/config.ts +150 -0
- package/src/prompt.ts +54 -207
- package/src/session.ts +124 -69
- package/src/tools/bash.ts +89 -0
- package/src/tools/common.ts +32 -0
- package/src/tools/edit.ts +41 -0
- package/src/tools/index.ts +47 -0
- package/src/tools/read.ts +64 -0
- package/src/tui/commands.ts +63 -0
- package/src/tui/editor.ts +291 -0
- package/src/tui/highlight.ts +189 -0
- package/src/tui/stream.ts +142 -0
- package/src/tui/styles.ts +42 -0
- package/src/tui/term.ts +436 -0
- package/src/tui/theme.ts +121 -0
- package/src/tui/tui.ts +595 -0
- package/src/tui/usage.ts +67 -0
- package/tsconfig.json +8 -8
- package/bin/mc.ts +0 -11
- package/bun.lock +0 -346
- package/nono-mini-coder.json +0 -42
- package/src/args.ts +0 -300
- package/src/error-handling.test.ts +0 -163
- package/src/git.ts +0 -23
- package/src/headless.ts +0 -66
- package/src/index.ts +0 -43
- package/src/oauth.ts +0 -157
- package/src/shared.ts +0 -119
- package/src/themes.ts +0 -234
- package/src/tool-bash.ts +0 -77
- package/src/tool-edit.ts +0 -121
- package/src/tool-read.ts +0 -100
- package/src/tui-components.ts +0 -127
- package/src/tui-conversation.ts +0 -218
- package/src/tui-editor.ts +0 -29
- package/src/tui-overlay.ts +0 -618
- package/src/tui.ts +0 -314
- package/src/types.ts +0 -194
- package/src/update.ts +0 -171
package/src/tool-read.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { Buffer } from "node:buffer";
|
|
2
|
-
import { extname, isAbsolute, join } from "node:path";
|
|
3
|
-
import { type Tool, Type } from "@earendil-works/pi-ai";
|
|
4
|
-
import type { ToolRunnerEvent } from "./types";
|
|
5
|
-
|
|
6
|
-
const imageMimeTypes: Record<string, string> = {
|
|
7
|
-
".bmp": "image/bmp",
|
|
8
|
-
".gif": "image/gif",
|
|
9
|
-
".jpeg": "image/jpeg",
|
|
10
|
-
".jpg": "image/jpeg",
|
|
11
|
-
".png": "image/png",
|
|
12
|
-
".svg": "image/svg+xml",
|
|
13
|
-
".webp": "image/webp",
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const description = `Read tool
|
|
17
|
-
|
|
18
|
-
Read a file by path.
|
|
19
|
-
|
|
20
|
-
For text files, returns line-numbered text. Use \`offset\` and \`limit\` to read a specific range.
|
|
21
|
-
You can also read images files.
|
|
22
|
-
`;
|
|
23
|
-
|
|
24
|
-
export const read: Tool = {
|
|
25
|
-
name: "read",
|
|
26
|
-
description,
|
|
27
|
-
parameters: Type.Object({
|
|
28
|
-
path: Type.String({
|
|
29
|
-
description:
|
|
30
|
-
"File path. Absolute or relative to the current working directory.",
|
|
31
|
-
}),
|
|
32
|
-
offset: Type.Optional(
|
|
33
|
-
Type.Number({
|
|
34
|
-
description:
|
|
35
|
-
"Optional 1-based line number to start reading from. Text files only.",
|
|
36
|
-
}),
|
|
37
|
-
),
|
|
38
|
-
limit: Type.Optional(
|
|
39
|
-
Type.Number({
|
|
40
|
-
description:
|
|
41
|
-
"Optional maximum number of lines to read. Text files only.",
|
|
42
|
-
}),
|
|
43
|
-
),
|
|
44
|
-
}),
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export async function* runReadTool(
|
|
48
|
-
args: Record<string, any>,
|
|
49
|
-
signal?: AbortSignal,
|
|
50
|
-
): AsyncGenerator<ToolRunnerEvent> {
|
|
51
|
-
const filePath = isAbsolute(args.path)
|
|
52
|
-
? args.path
|
|
53
|
-
: join(process.cwd(), args.path);
|
|
54
|
-
const file = Bun.file(filePath);
|
|
55
|
-
const exists = await file.exists();
|
|
56
|
-
|
|
57
|
-
if (!exists) {
|
|
58
|
-
yield { type: "result", text: `File not found: ${filePath}` };
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (signal?.aborted) {
|
|
63
|
-
yield { type: "result", text: "Aborted before read." };
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const mimeType = imageMimeTypes[extname(filePath).toLowerCase()] ?? file.type;
|
|
68
|
-
if (mimeType.startsWith("image/")) {
|
|
69
|
-
const data = Buffer.from(await file.arrayBuffer()).toString("base64");
|
|
70
|
-
const text = `Image read: ${filePath}\nMIME type: ${mimeType}\nSize: ${file.size} bytes`;
|
|
71
|
-
yield {
|
|
72
|
-
type: "result",
|
|
73
|
-
text,
|
|
74
|
-
image: { data, mimeType },
|
|
75
|
-
};
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const content = await file.text();
|
|
80
|
-
const lines = content.split(/\r?\n/);
|
|
81
|
-
if (content.endsWith("\n")) lines.pop();
|
|
82
|
-
|
|
83
|
-
const offset = args.offset ?? 1;
|
|
84
|
-
const startIndex = offset - 1;
|
|
85
|
-
const endIndex = args.limit
|
|
86
|
-
? Math.min(lines.length, startIndex + args.limit)
|
|
87
|
-
: lines.length;
|
|
88
|
-
const width = String(endIndex).length;
|
|
89
|
-
const body = lines
|
|
90
|
-
.slice(startIndex, endIndex)
|
|
91
|
-
.map((line, idx) => `${String(offset + idx).padStart(width)} | ${line}`)
|
|
92
|
-
.join("\n");
|
|
93
|
-
|
|
94
|
-
let text = `File: ${filePath}\nLines: ${offset}-${endIndex} of ${lines.length}\n\n${body}`;
|
|
95
|
-
if (endIndex < lines.length) {
|
|
96
|
-
text += `\n\nMore lines available. Use offset ${endIndex + 1} to continue.`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
yield { type: "result", text };
|
|
100
|
-
}
|
package/src/tui-components.ts
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { type Color, HStack, Text } from "@cel-tui/core";
|
|
2
|
-
import { onceEvery } from "./shared";
|
|
3
|
-
import { textColorForBackground, theme } from "./themes";
|
|
4
|
-
import type { TUIState } from "./types";
|
|
5
|
-
|
|
6
|
-
export { theme };
|
|
7
|
-
|
|
8
|
-
export function TextPill(
|
|
9
|
-
content: string,
|
|
10
|
-
fgColor: Color,
|
|
11
|
-
bgColor: Color,
|
|
12
|
-
size?: number | undefined,
|
|
13
|
-
) {
|
|
14
|
-
return HStack({ gap: 1, width: size }, [
|
|
15
|
-
HStack({ bgColor, padding: { x: 1 } }, [
|
|
16
|
-
Text(content, { bold: true, fgColor }),
|
|
17
|
-
]),
|
|
18
|
-
]);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function Spinner() {
|
|
22
|
-
const spinnerFrames = [
|
|
23
|
-
"⠁",
|
|
24
|
-
"⠂",
|
|
25
|
-
"⠄",
|
|
26
|
-
"⡀",
|
|
27
|
-
"⡈",
|
|
28
|
-
"⡐",
|
|
29
|
-
"⡠",
|
|
30
|
-
"⣀",
|
|
31
|
-
"⣁",
|
|
32
|
-
"⣂",
|
|
33
|
-
"⣄",
|
|
34
|
-
"⣌",
|
|
35
|
-
"⣔",
|
|
36
|
-
"⣤",
|
|
37
|
-
"⣥",
|
|
38
|
-
"⣦",
|
|
39
|
-
"⣮",
|
|
40
|
-
"⣶",
|
|
41
|
-
"⣷",
|
|
42
|
-
"⣿",
|
|
43
|
-
"⡿",
|
|
44
|
-
"⠿",
|
|
45
|
-
"⢟",
|
|
46
|
-
"⠟",
|
|
47
|
-
"⡛",
|
|
48
|
-
"⠛",
|
|
49
|
-
"⠫",
|
|
50
|
-
"⢋",
|
|
51
|
-
"⠋",
|
|
52
|
-
"⠍",
|
|
53
|
-
"⡉",
|
|
54
|
-
"⠉",
|
|
55
|
-
"⠑",
|
|
56
|
-
"⠡",
|
|
57
|
-
"⢁",
|
|
58
|
-
];
|
|
59
|
-
let spinnerTick = 0;
|
|
60
|
-
const spinnerEvery = onceEvery(4, () => spinnerTick++);
|
|
61
|
-
const currentSpinner = () =>
|
|
62
|
-
spinnerFrames[spinnerTick % spinnerFrames.length];
|
|
63
|
-
|
|
64
|
-
return { spinnerEvery, currentSpinner };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function ActivityPill(state: TUIState, spinnerFrame: string) {
|
|
68
|
-
let label = "";
|
|
69
|
-
const frame = state.streaming ? spinnerFrame : "";
|
|
70
|
-
|
|
71
|
-
if (frame) {
|
|
72
|
-
label = frame;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return Text(label);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function ContextPill(state: TUIState) {
|
|
79
|
-
let text = "";
|
|
80
|
-
let bg: Color = theme.bgreen;
|
|
81
|
-
if (!state.contextSize) {
|
|
82
|
-
text = "0%";
|
|
83
|
-
bg = theme.bwhite;
|
|
84
|
-
} else {
|
|
85
|
-
// Colors show the progress in the "smart window" or how much
|
|
86
|
-
// before the dumb zone. The user facing percentage is the model
|
|
87
|
-
// amount. An elegant way to show both :)
|
|
88
|
-
const max = state.options.model.contextWindow;
|
|
89
|
-
const smartMax = 80000;
|
|
90
|
-
const percent = Math.floor((state.contextSize / max) * 100);
|
|
91
|
-
const smartPercent = Math.floor((state.contextSize / smartMax) * 100);
|
|
92
|
-
if (smartPercent > 90) {
|
|
93
|
-
bg = theme.bred;
|
|
94
|
-
} else if (smartPercent > 85) {
|
|
95
|
-
bg = theme.red;
|
|
96
|
-
} else if (smartPercent > 80) {
|
|
97
|
-
bg = theme.yellow;
|
|
98
|
-
} else if (smartPercent > 60) {
|
|
99
|
-
bg = theme.byellow;
|
|
100
|
-
}
|
|
101
|
-
text = `~${percent}%`;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
text += ` (${state.options.model.contextWindow / 1000}k)`;
|
|
105
|
-
|
|
106
|
-
return TextPill(text, textColorForBackground(bg, state.options.theme), bg);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function GitPill(state: TUIState) {
|
|
110
|
-
return TextPill(state.gitBranch ?? "No git.", theme.bwhite, theme.bblack);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function ModelPill(state: TUIState) {
|
|
114
|
-
// Like loot: Gold/purple/blue/green/white -> xhigh/high/medium/low/minimal
|
|
115
|
-
switch (state.options.effort) {
|
|
116
|
-
case "xhigh":
|
|
117
|
-
return TextPill(state.options.model.name, theme.black, theme.yellow);
|
|
118
|
-
case "high":
|
|
119
|
-
return TextPill(state.options.model.name, theme.black, theme.magenta);
|
|
120
|
-
case "medium":
|
|
121
|
-
return TextPill(state.options.model.name, theme.black, theme.blue);
|
|
122
|
-
case "low":
|
|
123
|
-
return TextPill(state.options.model.name, theme.black, theme.green);
|
|
124
|
-
}
|
|
125
|
-
// Minimal
|
|
126
|
-
return TextPill(state.options.model.name, theme.bwhite, theme.bblack);
|
|
127
|
-
}
|
package/src/tui-conversation.ts
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
SyntaxHighlight,
|
|
3
|
-
type SyntaxHighlightTheme,
|
|
4
|
-
} from "@cel-tui/components";
|
|
5
|
-
import { type Color, HStack, type Node, Text, VStack } from "@cel-tui/core";
|
|
6
|
-
import { estimateTokens } from "./shared";
|
|
7
|
-
import { getTUITheme, textColorForBackground } from "./themes";
|
|
8
|
-
import { TextPill, theme } from "./tui-components";
|
|
9
|
-
import type { TUIMessage, TUIState, TUIToolCall } from "./types";
|
|
10
|
-
|
|
11
|
-
export function emptyState(state: TUIState): Node {
|
|
12
|
-
const randColor = theme.bgreen;
|
|
13
|
-
const shortcutFgColor =
|
|
14
|
-
state.options.theme === "ansi16"
|
|
15
|
-
? randColor
|
|
16
|
-
: textColorForBackground(theme.bblack, state.options.theme);
|
|
17
|
-
const updateNotice = state.availableUpdate
|
|
18
|
-
? [
|
|
19
|
-
HStack({ gap: 1 }, [
|
|
20
|
-
TextPill("update", shortcutFgColor, theme.bblack, 13),
|
|
21
|
-
Text(
|
|
22
|
-
`mini-coder ${state.availableUpdate.latestVersion} is available. Run mc --update.`,
|
|
23
|
-
{ fgColor: theme.bblack },
|
|
24
|
-
),
|
|
25
|
-
]),
|
|
26
|
-
]
|
|
27
|
-
: [];
|
|
28
|
-
return HStack({ flex: 1, alignItems: "center" }, [
|
|
29
|
-
VStack({ flex: 1, alignItems: "center", gap: 1 }, [
|
|
30
|
-
HStack({ gap: 1 }, [
|
|
31
|
-
Text("mini"),
|
|
32
|
-
TextPill(
|
|
33
|
-
"coder",
|
|
34
|
-
textColorForBackground(randColor, state.options.theme),
|
|
35
|
-
randColor,
|
|
36
|
-
),
|
|
37
|
-
]),
|
|
38
|
-
VStack({ gap: 1 }, [
|
|
39
|
-
HStack({ gap: 1 }, [
|
|
40
|
-
TextPill("/new", shortcutFgColor, theme.bblack, 13),
|
|
41
|
-
Text("Start a new session from the input box.", {
|
|
42
|
-
fgColor: theme.bblack,
|
|
43
|
-
}),
|
|
44
|
-
]),
|
|
45
|
-
HStack({ gap: 1 }, [
|
|
46
|
-
TextPill("ctrl+p", shortcutFgColor, theme.bblack, 13),
|
|
47
|
-
Text("Menu for session history, and settings.", {
|
|
48
|
-
fgColor: theme.bblack,
|
|
49
|
-
}),
|
|
50
|
-
]),
|
|
51
|
-
HStack({ gap: 1 }, [
|
|
52
|
-
TextPill("ESC", shortcutFgColor, theme.bblack, 13),
|
|
53
|
-
Text("Abort agent response.", { fgColor: theme.bblack }),
|
|
54
|
-
]),
|
|
55
|
-
HStack({ gap: 1 }, [
|
|
56
|
-
TextPill("ctrl+c|d|q", shortcutFgColor, theme.bblack, 13),
|
|
57
|
-
Text("Quit.", { fgColor: theme.bblack }),
|
|
58
|
-
]),
|
|
59
|
-
...updateNotice,
|
|
60
|
-
]),
|
|
61
|
-
]),
|
|
62
|
-
]);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function ConversationMessageToolCall(
|
|
66
|
-
call: TUIToolCall,
|
|
67
|
-
syntaxTheme: SyntaxHighlightTheme,
|
|
68
|
-
) {
|
|
69
|
-
let outputNode: Node | null = null;
|
|
70
|
-
|
|
71
|
-
// Compress read and bash calls
|
|
72
|
-
if (call.tool === "read") {
|
|
73
|
-
outputNode = Text(`Read ~${estimateTokens(call.output)} tokens`, {
|
|
74
|
-
wrap: "word",
|
|
75
|
-
});
|
|
76
|
-
} else if (call.tool === "bash") {
|
|
77
|
-
const tail = call.output.trim().slice(-200);
|
|
78
|
-
const blocks: Node[] = [];
|
|
79
|
-
|
|
80
|
-
if (tail.length !== call.output.trim().length) {
|
|
81
|
-
blocks.push(
|
|
82
|
-
Text("Showing the last 200 characters", {
|
|
83
|
-
fgColor: theme.bblack,
|
|
84
|
-
italic: true,
|
|
85
|
-
}),
|
|
86
|
-
Text(`[...] ${tail}`, { fgColor: theme.white, wrap: "word" }),
|
|
87
|
-
);
|
|
88
|
-
} else {
|
|
89
|
-
blocks.push(Text(tail, { fgColor: theme.white, wrap: "word" }));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
outputNode = VStack({ width: "100%" }, blocks);
|
|
93
|
-
} else if (call.tool === "edit") {
|
|
94
|
-
outputNode = SyntaxHighlight(call.output, "patch", { theme: syntaxTheme });
|
|
95
|
-
} else {
|
|
96
|
-
outputNode = Text(call.output, { wrap: "word" });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let argumentNodes: Node[] = [];
|
|
100
|
-
|
|
101
|
-
if (call.tool === "edit") {
|
|
102
|
-
argumentNodes = [
|
|
103
|
-
Text(`Writing... ~${estimateTokens(JSON.stringify(call.args))} tokens`),
|
|
104
|
-
];
|
|
105
|
-
} else {
|
|
106
|
-
argumentNodes = Object.entries(call.args).map(([key, value]) => {
|
|
107
|
-
let node: Node | null = Text(String(value));
|
|
108
|
-
|
|
109
|
-
// Syntax highlight bash args
|
|
110
|
-
if (call.tool === "bash") {
|
|
111
|
-
node = SyntaxHighlight(String(value), "bash", { theme: syntaxTheme });
|
|
112
|
-
return node;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return HStack({ gap: 1 }, [
|
|
116
|
-
Text(`${key}`, { italic: true, fgColor: theme.white }),
|
|
117
|
-
node,
|
|
118
|
-
]);
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return VStack({}, [
|
|
123
|
-
TextPill(call.tool, theme.black, theme.bwhite),
|
|
124
|
-
...argumentNodes,
|
|
125
|
-
outputNode,
|
|
126
|
-
]);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function ConversationMessage(
|
|
130
|
-
message: TUIMessage,
|
|
131
|
-
syntaxTheme: SyntaxHighlightTheme,
|
|
132
|
-
userMessageBgColor: Color | undefined,
|
|
133
|
-
) {
|
|
134
|
-
const blocks: Node[] = [];
|
|
135
|
-
|
|
136
|
-
if (message.thinking) {
|
|
137
|
-
// Compress thinking blocks
|
|
138
|
-
const estThinkingTok = estimateTokens(message.thinking);
|
|
139
|
-
blocks.push(
|
|
140
|
-
Text(`Thinking... ~${estThinkingTok} tokens`, {
|
|
141
|
-
wrap: "word",
|
|
142
|
-
fgColor: theme.bblack,
|
|
143
|
-
}),
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (message.text.length) {
|
|
148
|
-
blocks.push(
|
|
149
|
-
VStack(
|
|
150
|
-
{
|
|
151
|
-
width: "100%",
|
|
152
|
-
bgColor: message.role === "user" ? userMessageBgColor : undefined,
|
|
153
|
-
padding: { y: 1, x: message.role === "user" ? 1 : 0 },
|
|
154
|
-
},
|
|
155
|
-
[SyntaxHighlight(message.text, "markdown", { theme: syntaxTheme })],
|
|
156
|
-
),
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (message.toolCalls?.length) {
|
|
161
|
-
blocks.push(
|
|
162
|
-
VStack(
|
|
163
|
-
{ gap: 1 },
|
|
164
|
-
message.toolCalls.map((call) =>
|
|
165
|
-
ConversationMessageToolCall(call, syntaxTheme),
|
|
166
|
-
),
|
|
167
|
-
),
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (!message.thinking && !message.text.length && !message.toolCalls?.length) {
|
|
172
|
-
blocks.push(
|
|
173
|
-
Text(`Loading...`, {
|
|
174
|
-
wrap: "word",
|
|
175
|
-
fgColor: theme.bblack,
|
|
176
|
-
}),
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Msg footer:
|
|
181
|
-
blocks.push(
|
|
182
|
-
Text(`on ${message.timestamp} by ${message.role}`, {
|
|
183
|
-
fgColor: theme.bblack,
|
|
184
|
-
italic: true,
|
|
185
|
-
}),
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
return VStack(
|
|
189
|
-
{
|
|
190
|
-
gap: 1,
|
|
191
|
-
},
|
|
192
|
-
blocks,
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export function Conversation(state: TUIState) {
|
|
197
|
-
const activeTheme = getTUITheme(state.options.theme);
|
|
198
|
-
|
|
199
|
-
return VStack(
|
|
200
|
-
{
|
|
201
|
-
flex: 1,
|
|
202
|
-
gap: 2,
|
|
203
|
-
overflow: "scroll",
|
|
204
|
-
scrollOffset: state.stickToBottom ? Infinity : state.scrollOffset,
|
|
205
|
-
onScroll(offset, maxOffset) {
|
|
206
|
-
state.scrollOffset = offset;
|
|
207
|
-
state.stickToBottom = offset >= maxOffset;
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
state.tuiMessages.map((message) =>
|
|
211
|
-
ConversationMessage(
|
|
212
|
-
message,
|
|
213
|
-
activeTheme.syntax,
|
|
214
|
-
activeTheme.userMessageBgColor,
|
|
215
|
-
),
|
|
216
|
-
),
|
|
217
|
-
);
|
|
218
|
-
}
|
package/src/tui-editor.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { cel, Text, TextInput } from "@cel-tui/core";
|
|
2
|
-
import type { TUIState } from "./types";
|
|
3
|
-
|
|
4
|
-
export function Editor(
|
|
5
|
-
state: TUIState,
|
|
6
|
-
onChange: (value: string) => void,
|
|
7
|
-
onKeyPress: (key: string) => void,
|
|
8
|
-
) {
|
|
9
|
-
let isEditorFocused = !state.overlay;
|
|
10
|
-
return TextInput({
|
|
11
|
-
value: state.prompt,
|
|
12
|
-
minHeight: 3,
|
|
13
|
-
maxHeight: 10,
|
|
14
|
-
padding: { x: 1 },
|
|
15
|
-
placeholder: Text("Message...", { italic: true }),
|
|
16
|
-
onChange,
|
|
17
|
-
onKeyPress,
|
|
18
|
-
// TODO: Update `cel-tui` with an autofocus prop to fix this pattern
|
|
19
|
-
focused: isEditorFocused,
|
|
20
|
-
onFocus: () => {
|
|
21
|
-
isEditorFocused = true;
|
|
22
|
-
cel.render();
|
|
23
|
-
},
|
|
24
|
-
onBlur: () => {
|
|
25
|
-
isEditorFocused = false;
|
|
26
|
-
cel.render();
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
}
|