mini-coder 0.5.13 → 0.6.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/README.md +25 -108
- package/bin/mc.ts +8 -11
- package/bun.lock +79 -269
- package/package.json +17 -22
- package/src/agent.ts +242 -915
- package/src/args.ts +289 -0
- package/src/headless.ts +43 -385
- package/src/index.ts +29 -836
- package/src/oauth.ts +117 -0
- package/src/prompt.ts +227 -276
- package/src/session.ts +57 -961
- package/src/shared.ts +117 -38
- package/src/tool-bash.ts +110 -0
- package/src/tool-edit.ts +133 -0
- package/src/tool-task.ts +114 -0
- package/src/tui-components.ts +150 -0
- package/src/tui-conversation.ts +262 -0
- package/src/tui-editor.ts +29 -0
- package/src/tui-overlay.ts +403 -0
- package/src/tui.ts +236 -0
- package/src/types.ts +160 -0
- package/tsconfig.json +17 -0
- package/BENCHMARK.md +0 -107
- package/LICENSE +0 -9
- package/PROGRESS.md +0 -4
- package/assets/icon-1-minimal.svg +0 -31
- package/assets/icon-2-dark-terminal.svg +0 -48
- package/assets/icon-3-gradient-modern.svg +0 -45
- package/assets/icon-4-filled-bold.svg +0 -54
- package/assets/icon-5-community-badge.svg +0 -63
- package/assets/mc-claude-smart.png +0 -0
- package/assets/mc-gpt-smart.png +0 -0
- package/assets/preview-0-5-0.png +0 -0
- package/assets/preview.gif +0 -0
- package/benchmark-baseline.sh +0 -15
- package/benchmark-loop.sh +0 -19
- package/skills-lock.json +0 -15
- package/src/cli.ts +0 -134
- package/src/errors.ts +0 -15
- package/src/git.ts +0 -247
- package/src/input.ts +0 -168
- package/src/mcp.ts +0 -609
- package/src/paths.ts +0 -37
- package/src/session-message.ts +0 -393
- package/src/settings.ts +0 -449
- package/src/skills.ts +0 -271
- package/src/submit.ts +0 -371
- package/src/text.ts +0 -71
- package/src/theme.ts +0 -330
- package/src/tool-common.ts +0 -93
- package/src/tool-grep.ts +0 -606
- package/src/tool-read.ts +0 -313
- package/src/tool-shell.ts +0 -1001
- package/src/tools.ts +0 -854
- package/src/ui/agent.ts +0 -317
- package/src/ui/commands.test.ts +0 -913
- package/src/ui/commands.ts +0 -834
- package/src/ui/conversation.test.ts +0 -585
- package/src/ui/conversation.ts +0 -1836
- package/src/ui/help.ts +0 -158
- package/src/ui/input.test.ts +0 -64
- package/src/ui/input.ts +0 -138
- package/src/ui/overlay.ts +0 -59
- package/src/ui/runtime.ts +0 -69
- package/src/ui/status.ts +0 -220
- package/src/ui.ts +0 -1190
- package/src/version.ts +0 -48
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { SyntaxHighlight } from "@cel-tui/components";
|
|
2
|
+
import { HStack, type Node, Text, VStack } from "@cel-tui/core";
|
|
3
|
+
import type {
|
|
4
|
+
AssistantMessage,
|
|
5
|
+
Message,
|
|
6
|
+
ToolResultMessage,
|
|
7
|
+
UserMessage,
|
|
8
|
+
} from "@mariozechner/pi-ai";
|
|
9
|
+
import { estimateTokens, relativeTime } from "./shared";
|
|
10
|
+
import { TextPill, theme } from "./tui-components";
|
|
11
|
+
import type { TUIState } from "./types";
|
|
12
|
+
|
|
13
|
+
function agentMessageNode(msg: AssistantMessage): Node {
|
|
14
|
+
let thinking = "";
|
|
15
|
+
let text = "";
|
|
16
|
+
const toolCalls: Node[] = [];
|
|
17
|
+
|
|
18
|
+
for (const block of msg.content) {
|
|
19
|
+
if (block.type === "thinking" && block.thinking.length > 0) {
|
|
20
|
+
thinking += block.thinking;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (block.type === "text" && block.text.length > 0) {
|
|
24
|
+
text += block.text;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (block.type === "toolCall" && block.arguments && block.name) {
|
|
28
|
+
let text = "";
|
|
29
|
+
let node: Node | undefined;
|
|
30
|
+
if ("path" in block.arguments) {
|
|
31
|
+
text = block.arguments.path;
|
|
32
|
+
node = Text(text);
|
|
33
|
+
} else if ("command" in block.arguments) {
|
|
34
|
+
text = block.arguments.command;
|
|
35
|
+
node = SyntaxHighlight(text, "bash");
|
|
36
|
+
} else if ("prompt" in block.arguments) {
|
|
37
|
+
text = block.arguments.prompt;
|
|
38
|
+
node = SyntaxHighlight(text, "markdown");
|
|
39
|
+
} else {
|
|
40
|
+
text = JSON.stringify(block.arguments);
|
|
41
|
+
node = Text(text);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
toolCalls.push(
|
|
45
|
+
VStack({ padding: { x: 4 }, gap: 1 }, [
|
|
46
|
+
TextPill(block.name, theme.white, theme.bblack),
|
|
47
|
+
node,
|
|
48
|
+
]),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const textBlocks: Node[] = [];
|
|
53
|
+
if (thinking.length > 0) {
|
|
54
|
+
const tokens = estimateTokens(thinking);
|
|
55
|
+
|
|
56
|
+
textBlocks.push(
|
|
57
|
+
Text(`Thinking... (~${tokens} tokens)`, {
|
|
58
|
+
fgColor: theme.bblack,
|
|
59
|
+
italic: true,
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (text.length > 0) {
|
|
64
|
+
textBlocks.push(SyntaxHighlight(text, "markdown"));
|
|
65
|
+
}
|
|
66
|
+
if (toolCalls.length > 0) {
|
|
67
|
+
textBlocks.push(...toolCalls);
|
|
68
|
+
}
|
|
69
|
+
const error =
|
|
70
|
+
((msg.stopReason === "error" || msg.stopReason === "aborted") &&
|
|
71
|
+
msg.errorMessage) ??
|
|
72
|
+
"Unknown error.";
|
|
73
|
+
if (error) {
|
|
74
|
+
textBlocks.push(
|
|
75
|
+
VStack({ padding: { x: 4 }, gap: 1 }, [
|
|
76
|
+
TextPill(msg.stopReason, theme.white, theme.bblack),
|
|
77
|
+
Text(error),
|
|
78
|
+
]),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (textBlocks.length === 0) {
|
|
83
|
+
textBlocks.push(
|
|
84
|
+
Text("Loading...", {
|
|
85
|
+
fgColor: theme.bblack,
|
|
86
|
+
italic: true,
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return VStack({ gap: 1 }, textBlocks);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function userMessageNode(msg: UserMessage): Node {
|
|
95
|
+
let text = "";
|
|
96
|
+
if (typeof msg.content === "string") {
|
|
97
|
+
text = msg.content;
|
|
98
|
+
} else {
|
|
99
|
+
text = msg.content
|
|
100
|
+
.filter((b) => b.type === "text")
|
|
101
|
+
.map((b) => b.text)
|
|
102
|
+
.join("");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Remove any reminders we might have attached before render
|
|
106
|
+
// Keep this fast, it runs on the render cycle.
|
|
107
|
+
text = text
|
|
108
|
+
.replaceAll(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "")
|
|
109
|
+
.trimStart();
|
|
110
|
+
|
|
111
|
+
return VStack(
|
|
112
|
+
{
|
|
113
|
+
padding: { x: 1, y: 1 },
|
|
114
|
+
bgColor: theme.bblack,
|
|
115
|
+
fgColor: theme.white,
|
|
116
|
+
},
|
|
117
|
+
[SyntaxHighlight(text, "markdown")],
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function toolMessageNode(msg: ToolResultMessage): Node {
|
|
122
|
+
// Output only shows last 10 lines of scroll.
|
|
123
|
+
return VStack(
|
|
124
|
+
{
|
|
125
|
+
height: 10,
|
|
126
|
+
padding: { x: 4 },
|
|
127
|
+
overflow: "scroll",
|
|
128
|
+
scrollOffset: Infinity,
|
|
129
|
+
onScroll: () => false,
|
|
130
|
+
},
|
|
131
|
+
msg.content.map((block) => {
|
|
132
|
+
if (block.type === "text") {
|
|
133
|
+
return Text(block.text, { wrap: "word", fgColor: theme.bblack });
|
|
134
|
+
}
|
|
135
|
+
return Text(""); // TODO: image case needs attention
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const messageBodyCache = new WeakMap<Message, { key: number; node: Node }>();
|
|
141
|
+
|
|
142
|
+
function messageCacheKey(msg: Message): number {
|
|
143
|
+
if (msg.role === "assistant") {
|
|
144
|
+
let key = 0;
|
|
145
|
+
for (const block of msg.content) {
|
|
146
|
+
if (block.type === "thinking") key += block.thinking.length;
|
|
147
|
+
if (block.type === "text") key += block.text.length;
|
|
148
|
+
if (block.type === "toolCall" && block.arguments)
|
|
149
|
+
key += JSON.stringify(block.arguments).length;
|
|
150
|
+
}
|
|
151
|
+
if (msg.stopReason) key += msg.stopReason.length;
|
|
152
|
+
if (msg.errorMessage) key += msg.errorMessage.length;
|
|
153
|
+
return key;
|
|
154
|
+
}
|
|
155
|
+
if (msg.role === "user") {
|
|
156
|
+
if (typeof msg.content === "string") return msg.content.length;
|
|
157
|
+
return msg.content.reduce(
|
|
158
|
+
(sum, b) => (b.type === "text" ? sum + b.text.length : sum),
|
|
159
|
+
0,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (msg.role === "toolResult") {
|
|
163
|
+
return msg.content.reduce(
|
|
164
|
+
(sum, b) => (b.type === "text" ? sum + b.text.length : sum),
|
|
165
|
+
0,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function cachedMessageBody(msg: Message): Node {
|
|
172
|
+
const key = messageCacheKey(msg);
|
|
173
|
+
const cached = messageBodyCache.get(msg);
|
|
174
|
+
if (cached && cached.key === key) {
|
|
175
|
+
return cached.node;
|
|
176
|
+
}
|
|
177
|
+
const node =
|
|
178
|
+
msg.role === "assistant"
|
|
179
|
+
? agentMessageNode(msg)
|
|
180
|
+
: msg.role === "user"
|
|
181
|
+
? userMessageNode(msg)
|
|
182
|
+
: msg.role === "toolResult"
|
|
183
|
+
? toolMessageNode(msg)
|
|
184
|
+
: Text("Unknown message?", {
|
|
185
|
+
wrap: "word",
|
|
186
|
+
fgColor: theme.bwhite,
|
|
187
|
+
});
|
|
188
|
+
messageBodyCache.set(msg, { key, node });
|
|
189
|
+
return node;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function conversationMessageNode(msg: Message): Node {
|
|
193
|
+
return VStack({ gap: 1 }, [
|
|
194
|
+
cachedMessageBody(msg),
|
|
195
|
+
HStack({ gap: 1, justifyContent: "end" }, [
|
|
196
|
+
Text(`${relativeTime(msg.timestamp)} ago.`, {
|
|
197
|
+
fgColor: theme.bblack,
|
|
198
|
+
italic: true,
|
|
199
|
+
}),
|
|
200
|
+
TextPill(msg.role, theme.bwhite, theme.bblack),
|
|
201
|
+
]),
|
|
202
|
+
]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const colors = [
|
|
206
|
+
theme.bgreen,
|
|
207
|
+
theme.byellow,
|
|
208
|
+
theme.bblue,
|
|
209
|
+
theme.bcyan,
|
|
210
|
+
theme.bmagenta,
|
|
211
|
+
theme.bred,
|
|
212
|
+
];
|
|
213
|
+
const randColor = colors[Math.floor(Math.random() * colors.length)];
|
|
214
|
+
|
|
215
|
+
export function emptyState(): Node {
|
|
216
|
+
return HStack({ flex: 1, alignItems: "center" }, [
|
|
217
|
+
VStack({ flex: 1, alignItems: "center", gap: 1 }, [
|
|
218
|
+
HStack({ gap: 1 }, [
|
|
219
|
+
Text("mini"),
|
|
220
|
+
TextPill("coder", theme.black, randColor),
|
|
221
|
+
]),
|
|
222
|
+
VStack({ gap: 1 }, [
|
|
223
|
+
HStack({ gap: 1 }, [
|
|
224
|
+
TextPill("/new", randColor, theme.bblack, 13),
|
|
225
|
+
Text("Start a new session from the input box.", {
|
|
226
|
+
fgColor: theme.bblack,
|
|
227
|
+
}),
|
|
228
|
+
]),
|
|
229
|
+
HStack({ gap: 1 }, [
|
|
230
|
+
TextPill("ctrl+p", randColor, theme.bblack, 13),
|
|
231
|
+
Text("Menu for session history, and settings.", {
|
|
232
|
+
fgColor: theme.bblack,
|
|
233
|
+
}),
|
|
234
|
+
]),
|
|
235
|
+
HStack({ gap: 1 }, [
|
|
236
|
+
TextPill("ESC", randColor, theme.bblack, 13),
|
|
237
|
+
Text("Abort agent response.", { fgColor: theme.bblack }),
|
|
238
|
+
]),
|
|
239
|
+
HStack({ gap: 1 }, [
|
|
240
|
+
TextPill("ctrl+c|d|q", randColor, theme.bblack, 13),
|
|
241
|
+
Text("Quit.", { fgColor: theme.bblack }),
|
|
242
|
+
]),
|
|
243
|
+
]),
|
|
244
|
+
]),
|
|
245
|
+
]);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function Conversation(state: TUIState) {
|
|
249
|
+
return VStack(
|
|
250
|
+
{
|
|
251
|
+
flex: 1,
|
|
252
|
+
gap: 1,
|
|
253
|
+
overflow: "scroll",
|
|
254
|
+
scrollOffset: state.stickToBottom ? Infinity : state.scrollOffset,
|
|
255
|
+
onScroll(offset, maxOffset) {
|
|
256
|
+
state.scrollOffset = offset;
|
|
257
|
+
state.stickToBottom = offset >= maxOffset;
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
state.messages.map(conversationMessageNode),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { HStack, Text, TextInput, VStack } from "@cel-tui/core";
|
|
2
|
+
import { getModels, type ThinkingLevel } from "@mariozechner/pi-ai";
|
|
3
|
+
import { getOAuthProviders } from "@mariozechner/pi-ai/oauth";
|
|
4
|
+
import { saveSettings } from "./args";
|
|
5
|
+
import { getAvailableProviders } from "./oauth";
|
|
6
|
+
import { listSessionsForCwd } from "./session";
|
|
7
|
+
import { estimateTokens } from "./shared";
|
|
8
|
+
import { TextPill, theme } from "./tui-components";
|
|
9
|
+
import type { SelectOptions, SelectState, Session, TUIState } from "./types";
|
|
10
|
+
|
|
11
|
+
export function SelectOverlay(
|
|
12
|
+
value: string,
|
|
13
|
+
selected: string,
|
|
14
|
+
list: { label: string; value: string }[],
|
|
15
|
+
label: string,
|
|
16
|
+
onOverlayKeyPress: (key: string) => boolean | undefined,
|
|
17
|
+
onChange: (newValue: string) => void,
|
|
18
|
+
onKeyPress: (key: string) => boolean | undefined,
|
|
19
|
+
) {
|
|
20
|
+
let isEditorFocused = true;
|
|
21
|
+
return VStack(
|
|
22
|
+
{
|
|
23
|
+
height: "100%",
|
|
24
|
+
justifyContent: "end",
|
|
25
|
+
onKeyPress: onOverlayKeyPress,
|
|
26
|
+
},
|
|
27
|
+
[
|
|
28
|
+
VStack(
|
|
29
|
+
{
|
|
30
|
+
bgColor: theme.white,
|
|
31
|
+
fgColor: theme.bblack,
|
|
32
|
+
gap: 1,
|
|
33
|
+
padding: { x: 1, y: 1 },
|
|
34
|
+
},
|
|
35
|
+
[
|
|
36
|
+
VStack(
|
|
37
|
+
{ flex: 1, minHeight: 5, maxHeight: 20, padding: { x: 1 } },
|
|
38
|
+
list.map((i) =>
|
|
39
|
+
i.value === selected
|
|
40
|
+
? Text(i.label, { fgColor: theme.black })
|
|
41
|
+
: Text(i.label, { fgColor: theme.bblack }),
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
|
|
45
|
+
HStack({ width: "100%" }, [
|
|
46
|
+
TextPill(label, theme.bwhite, theme.bblack),
|
|
47
|
+
]),
|
|
48
|
+
|
|
49
|
+
TextInput({
|
|
50
|
+
value,
|
|
51
|
+
minHeight: 3,
|
|
52
|
+
maxHeight: 10,
|
|
53
|
+
padding: { x: 1 },
|
|
54
|
+
placeholder: Text("Search...", {
|
|
55
|
+
fgColor: theme.bblack,
|
|
56
|
+
italic: true,
|
|
57
|
+
}),
|
|
58
|
+
fgColor: theme.bblack,
|
|
59
|
+
bgColor: theme.white,
|
|
60
|
+
onChange,
|
|
61
|
+
onKeyPress,
|
|
62
|
+
focused: isEditorFocused,
|
|
63
|
+
onFocus: () => {
|
|
64
|
+
isEditorFocused = true;
|
|
65
|
+
},
|
|
66
|
+
onBlur: () => {
|
|
67
|
+
isEditorFocused = false;
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
],
|
|
71
|
+
),
|
|
72
|
+
],
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function useSelectOverlay(initialOptions: SelectOptions) {
|
|
77
|
+
// Inner state
|
|
78
|
+
const s: SelectState = {
|
|
79
|
+
value: "",
|
|
80
|
+
selected: "",
|
|
81
|
+
label: "SELECT",
|
|
82
|
+
list: [],
|
|
83
|
+
};
|
|
84
|
+
let baseList: SelectOptions["list"] = [];
|
|
85
|
+
|
|
86
|
+
const applyOptions = (
|
|
87
|
+
options: Pick<SelectOptions, "filter" | "label" | "list">,
|
|
88
|
+
) => {
|
|
89
|
+
baseList = options.list;
|
|
90
|
+
s.value = options.filter ?? "";
|
|
91
|
+
s.label = options.label ? options.label.toUpperCase() : "SELECT";
|
|
92
|
+
s.list = s.value
|
|
93
|
+
? baseList.filter((i) => i.label.includes(s.value))
|
|
94
|
+
: baseList;
|
|
95
|
+
s.selected = s.list.length ? s.list[0].value : "";
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
applyOptions(initialOptions);
|
|
99
|
+
|
|
100
|
+
const onChange = (newValue: string) => {
|
|
101
|
+
s.value = newValue;
|
|
102
|
+
s.list = s.value
|
|
103
|
+
? baseList.filter((i) => i.label.includes(s.value))
|
|
104
|
+
: baseList;
|
|
105
|
+
s.selected = s.list.length ? s.list[0].value : "";
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const moveSelected = (direction: -1 | 1) => {
|
|
109
|
+
if (!s.list.length) return;
|
|
110
|
+
|
|
111
|
+
const currentIndex = s.list.findIndex((i) => i.value === s.selected);
|
|
112
|
+
const nextIndex =
|
|
113
|
+
currentIndex === -1
|
|
114
|
+
? direction === 1
|
|
115
|
+
? 0
|
|
116
|
+
: s.list.length - 1
|
|
117
|
+
: (currentIndex + direction + s.list.length) % s.list.length;
|
|
118
|
+
|
|
119
|
+
s.selected = s.list[nextIndex].value;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const onMoveKeyPress = (key: string) => {
|
|
123
|
+
if (key !== "up" && key !== "down") return;
|
|
124
|
+
|
|
125
|
+
moveSelected(key === "up" ? -1 : 1);
|
|
126
|
+
return false;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const onEditorKeyPress = (key: string) => {
|
|
130
|
+
const didMove = onMoveKeyPress(key);
|
|
131
|
+
if (didMove === false) return false;
|
|
132
|
+
|
|
133
|
+
if (key === "enter") {
|
|
134
|
+
const previousList = s.list;
|
|
135
|
+
const previousBaseList = baseList;
|
|
136
|
+
const applySelectionResult = () => {
|
|
137
|
+
if (s.list !== previousList) {
|
|
138
|
+
baseList = s.list;
|
|
139
|
+
} else {
|
|
140
|
+
s.list = previousBaseList;
|
|
141
|
+
s.selected = s.list.length ? s.list[0].value : "";
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
s.value = "";
|
|
146
|
+
const result = initialOptions.onSelect(s);
|
|
147
|
+
if (result instanceof Promise) {
|
|
148
|
+
result.then(applySelectionResult).catch(() => {
|
|
149
|
+
s.list = previousBaseList;
|
|
150
|
+
s.selected = s.list.length ? s.list[0].value : "";
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
applySelectionResult();
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const onOverlayKeyPress = (key: string) => {
|
|
160
|
+
const didMove = onMoveKeyPress(key);
|
|
161
|
+
if (didMove === false) return false;
|
|
162
|
+
|
|
163
|
+
if (key === "escape" || key === "ctrl+p") {
|
|
164
|
+
applyOptions(initialOptions);
|
|
165
|
+
initialOptions.onCancel();
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return () =>
|
|
171
|
+
SelectOverlay(
|
|
172
|
+
s.value,
|
|
173
|
+
s.selected,
|
|
174
|
+
s.list,
|
|
175
|
+
s.label,
|
|
176
|
+
onOverlayKeyPress,
|
|
177
|
+
onChange,
|
|
178
|
+
onEditorKeyPress,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sessionLabel(session: Session): string {
|
|
183
|
+
const firstUserMessage = session.messages.find(
|
|
184
|
+
(message) => message.role === "user",
|
|
185
|
+
);
|
|
186
|
+
if (!firstUserMessage) return session.id;
|
|
187
|
+
|
|
188
|
+
const text =
|
|
189
|
+
typeof firstUserMessage.content === "string"
|
|
190
|
+
? firstUserMessage.content
|
|
191
|
+
: firstUserMessage.content
|
|
192
|
+
.map((block) => (block.type === "text" ? block.text : ""))
|
|
193
|
+
.join(" ");
|
|
194
|
+
const snippet = text
|
|
195
|
+
.replaceAll(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "")
|
|
196
|
+
.trim()
|
|
197
|
+
.replace(/\s+/g, " ")
|
|
198
|
+
.slice(0, 80);
|
|
199
|
+
|
|
200
|
+
return snippet || session.id;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function mainMenu(state: TUIState) {
|
|
204
|
+
type MenuPane = Omit<SelectOptions, "onCancel" | "onSelect">;
|
|
205
|
+
|
|
206
|
+
const oauthProviders = getOAuthProviders();
|
|
207
|
+
const getProviderLabel = (provider: string) =>
|
|
208
|
+
oauthProviders.find((oauthProvider) => oauthProvider.id === provider)
|
|
209
|
+
?.name ?? provider;
|
|
210
|
+
const reasoningEfforts: ThinkingLevel[] = [
|
|
211
|
+
"minimal",
|
|
212
|
+
"low",
|
|
213
|
+
"medium",
|
|
214
|
+
"high",
|
|
215
|
+
"xhigh",
|
|
216
|
+
];
|
|
217
|
+
const efforts = reasoningEfforts.map((v) => ({ label: v, value: v }));
|
|
218
|
+
const getProviderModels = (provider: string) => {
|
|
219
|
+
const builtIn = getModels(provider as any).map((v) => ({
|
|
220
|
+
label: v.name,
|
|
221
|
+
value: v.id,
|
|
222
|
+
}));
|
|
223
|
+
const custom =
|
|
224
|
+
state.options.customProviders
|
|
225
|
+
?.filter((m) => m.provider === provider)
|
|
226
|
+
.map((v) => ({
|
|
227
|
+
label: v.name,
|
|
228
|
+
value: v.id,
|
|
229
|
+
})) ?? [];
|
|
230
|
+
return [...builtIn, ...custom];
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
let currentProviders: string[] = [];
|
|
234
|
+
let currentSessions: Session[] = [];
|
|
235
|
+
|
|
236
|
+
const providersPane = async (): Promise<MenuPane> => {
|
|
237
|
+
const builtIn = await getAvailableProviders();
|
|
238
|
+
const custom =
|
|
239
|
+
state.options.customProviders?.map((cp) => cp.provider) ?? [];
|
|
240
|
+
currentProviders = [...new Set([...builtIn, ...custom])];
|
|
241
|
+
return {
|
|
242
|
+
label: "providers",
|
|
243
|
+
filter: "",
|
|
244
|
+
list: currentProviders.map((provider) => ({
|
|
245
|
+
label: getProviderLabel(provider),
|
|
246
|
+
value: provider,
|
|
247
|
+
})),
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const sessionsPane = async (): Promise<MenuPane> => {
|
|
252
|
+
currentSessions = await listSessionsForCwd();
|
|
253
|
+
return {
|
|
254
|
+
label: "sessions",
|
|
255
|
+
filter: "",
|
|
256
|
+
list: currentSessions.map((session) => ({
|
|
257
|
+
label: sessionLabel(session),
|
|
258
|
+
value: session.id,
|
|
259
|
+
})),
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const mainPane: MenuPane = {
|
|
264
|
+
label: "main",
|
|
265
|
+
filter: state.prompt.length ? state.prompt : "",
|
|
266
|
+
list: [
|
|
267
|
+
{ label: "models and providers", value: "providers" },
|
|
268
|
+
{ label: "reasoning effort", value: "effort" },
|
|
269
|
+
{ label: "sessions", value: "sessions" },
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
const effortPane: MenuPane = {
|
|
273
|
+
label: "effort",
|
|
274
|
+
filter: "",
|
|
275
|
+
list: efforts,
|
|
276
|
+
};
|
|
277
|
+
const panes: MenuPane[] = [effortPane];
|
|
278
|
+
let currentPane = mainPane;
|
|
279
|
+
let selectedProvider: string | undefined;
|
|
280
|
+
|
|
281
|
+
const openPane = (s: SelectState, pane: MenuPane) => {
|
|
282
|
+
currentPane = pane;
|
|
283
|
+
s.value = pane.filter;
|
|
284
|
+
s.label = pane.label ? pane.label.toUpperCase() : "SELECT";
|
|
285
|
+
s.list = pane.list;
|
|
286
|
+
s.selected = s.list.length ? s.list[0].value : "";
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const resetMenu = (s: SelectState) => {
|
|
290
|
+
currentPane = mainPane;
|
|
291
|
+
selectedProvider = undefined;
|
|
292
|
+
currentProviders = [];
|
|
293
|
+
currentSessions = [];
|
|
294
|
+
s.value = "";
|
|
295
|
+
s.label = mainPane.label ? mainPane.label.toUpperCase() : "SELECT";
|
|
296
|
+
s.list = mainPane.list;
|
|
297
|
+
s.selected = s.list.length ? s.list[0].value : "";
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const closeMenu = (s: SelectState) => {
|
|
301
|
+
resetMenu(s);
|
|
302
|
+
state.overlay = false;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const select = useSelectOverlay({
|
|
306
|
+
...mainPane,
|
|
307
|
+
onSelect: (s) => {
|
|
308
|
+
if (!s.selected) return;
|
|
309
|
+
|
|
310
|
+
if (currentPane.label === "main") {
|
|
311
|
+
if (s.selected === "providers") {
|
|
312
|
+
return providersPane().then((pane) => {
|
|
313
|
+
openPane(s, pane);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (s.selected === "sessions") {
|
|
318
|
+
return sessionsPane().then((pane) => {
|
|
319
|
+
openPane(s, pane);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const nextPane = panes.find((pane) => pane.label === s.selected);
|
|
324
|
+
if (nextPane) openPane(s, nextPane);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (currentPane.label === "sessions") {
|
|
329
|
+
const session = currentSessions.find((v) => v.id === s.selected);
|
|
330
|
+
if (!session) return;
|
|
331
|
+
|
|
332
|
+
state.sessionId = session.id;
|
|
333
|
+
state.messages = session.messages;
|
|
334
|
+
state.prompt = "";
|
|
335
|
+
state.contextSize = estimateTokens(JSON.stringify(state.messages));
|
|
336
|
+
state.scrollOffset = 0;
|
|
337
|
+
state.stickToBottom = true;
|
|
338
|
+
closeMenu(s);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (currentPane.label === "providers") {
|
|
343
|
+
const provider = currentProviders.find((v) => v === s.selected);
|
|
344
|
+
if (!provider) return;
|
|
345
|
+
|
|
346
|
+
selectedProvider = provider;
|
|
347
|
+
openPane(s, {
|
|
348
|
+
label: "models",
|
|
349
|
+
filter: "",
|
|
350
|
+
list: getProviderModels(selectedProvider),
|
|
351
|
+
});
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (currentPane.label === "models") {
|
|
356
|
+
if (!selectedProvider) return;
|
|
357
|
+
|
|
358
|
+
const builtIn = getModels(selectedProvider as any).find(
|
|
359
|
+
(v) => v.id === s.selected,
|
|
360
|
+
);
|
|
361
|
+
const custom = state.options.customProviders?.find(
|
|
362
|
+
(v) => v.provider === selectedProvider && v.id === s.selected,
|
|
363
|
+
);
|
|
364
|
+
const model = builtIn ?? custom;
|
|
365
|
+
if (!model) return;
|
|
366
|
+
|
|
367
|
+
state.options.provider = selectedProvider;
|
|
368
|
+
state.options.model = model;
|
|
369
|
+
saveSettings({
|
|
370
|
+
provider: selectedProvider,
|
|
371
|
+
model: model.id,
|
|
372
|
+
effort: state.options.effort,
|
|
373
|
+
customProviders: state.options.customProviders,
|
|
374
|
+
});
|
|
375
|
+
closeMenu(s);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (currentPane.label === "effort") {
|
|
380
|
+
const effort = reasoningEfforts.find((v) => v === s.selected);
|
|
381
|
+
if (!effort) return;
|
|
382
|
+
|
|
383
|
+
state.options.effort = effort;
|
|
384
|
+
saveSettings({
|
|
385
|
+
provider: state.options.provider,
|
|
386
|
+
model: state.options.model.id,
|
|
387
|
+
effort: effort,
|
|
388
|
+
customProviders: state.options.customProviders,
|
|
389
|
+
});
|
|
390
|
+
closeMenu(s);
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
onCancel: () => {
|
|
394
|
+
currentPane = mainPane;
|
|
395
|
+
selectedProvider = undefined;
|
|
396
|
+
currentProviders = [];
|
|
397
|
+
currentSessions = [];
|
|
398
|
+
state.overlay = false;
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
return select;
|
|
403
|
+
}
|