@meowlynxsea/koi 0.1.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/LICENSE +34 -0
- package/NOTICE +35 -0
- package/README.md +15 -0
- package/bin/koi +12 -0
- package/dist/highlights-eq9cgrbb.scm +604 -0
- package/dist/highlights-ghv9g403.scm +205 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/highlights-r812a2qc.scm +150 -0
- package/dist/highlights-x6tmsnaa.scm +115 -0
- package/dist/injections-73j83es3.scm +27 -0
- package/dist/main.js +489918 -0
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/package.json +51 -0
- package/src/agent/check-permissions.ts +239 -0
- package/src/agent/hooks/message-utils.ts +305 -0
- package/src/agent/hooks/types.ts +32 -0
- package/src/agent/hooks.ts +1560 -0
- package/src/agent/mode.ts +163 -0
- package/src/agent/monitor-registry.ts +308 -0
- package/src/agent/permission-ui.ts +71 -0
- package/src/agent/plan-ui.ts +74 -0
- package/src/agent/question-ui.ts +58 -0
- package/src/agent/session-fork.ts +299 -0
- package/src/agent/session-snapshots.ts +216 -0
- package/src/agent/session-store.ts +649 -0
- package/src/agent/session-tasks.ts +305 -0
- package/src/agent/session.ts +27 -0
- package/src/agent/subagent-registry.ts +176 -0
- package/src/agent/subagent.ts +194 -0
- package/src/agent/tool-orchestration.ts +55 -0
- package/src/agent/tools.ts +8 -0
- package/src/cli/args.ts +6 -0
- package/src/cli/commands.ts +5 -0
- package/src/commands/skills/index.ts +23 -0
- package/src/config/models.ts +6 -0
- package/src/config/settings.ts +392 -0
- package/src/main.tsx +64 -0
- package/src/services/mcp/client.ts +194 -0
- package/src/services/mcp/config.ts +232 -0
- package/src/services/mcp/connection-manager.ts +258 -0
- package/src/services/mcp/index.ts +80 -0
- package/src/services/mcp/mcp-commands.ts +114 -0
- package/src/services/mcp/stdio-transport.ts +246 -0
- package/src/services/mcp/types.ts +155 -0
- package/src/skills/SkillsMenu.tsx +370 -0
- package/src/skills/bundled/batch.ts +106 -0
- package/src/skills/bundled/debug.ts +86 -0
- package/src/skills/bundled/loremIpsum.ts +101 -0
- package/src/skills/bundled/remember.ts +97 -0
- package/src/skills/bundled/simplify.ts +100 -0
- package/src/skills/bundled/skillify.ts +123 -0
- package/src/skills/bundled/stuck.ts +101 -0
- package/src/skills/bundled/updateConfig.ts +228 -0
- package/src/skills/bundled.ts +46 -0
- package/src/skills/frontmatter.ts +179 -0
- package/src/skills/index.ts +87 -0
- package/src/skills/invoke.ts +231 -0
- package/src/skills/loader.ts +710 -0
- package/src/skills/substitution.ts +169 -0
- package/src/skills/types.ts +201 -0
- package/src/tools/agent.ts +143 -0
- package/src/tools/ask-user-question.ts +46 -0
- package/src/tools/bash.ts +148 -0
- package/src/tools/edit.ts +164 -0
- package/src/tools/glob.ts +102 -0
- package/src/tools/grep.ts +248 -0
- package/src/tools/index.ts +73 -0
- package/src/tools/list-mcp-resources.ts +74 -0
- package/src/tools/ls.ts +85 -0
- package/src/tools/mcp.ts +76 -0
- package/src/tools/monitor.ts +159 -0
- package/src/tools/plan-mode.ts +134 -0
- package/src/tools/read-mcp-resource.ts +79 -0
- package/src/tools/read.ts +137 -0
- package/src/tools/skill.ts +176 -0
- package/src/tools/task.ts +349 -0
- package/src/tools/types.ts +52 -0
- package/src/tools/webfetch-domains.ts +239 -0
- package/src/tools/webfetch.ts +533 -0
- package/src/tools/write.ts +101 -0
- package/src/tui/app.tsx +1178 -0
- package/src/tui/components/chat-panel.tsx +1071 -0
- package/src/tui/components/command-panel.tsx +261 -0
- package/src/tui/components/confirm-modal.tsx +135 -0
- package/src/tui/components/connect-modal.tsx +435 -0
- package/src/tui/components/connecting-modal.tsx +167 -0
- package/src/tui/components/edit-pending-modal.tsx +103 -0
- package/src/tui/components/exit-modal.tsx +131 -0
- package/src/tui/components/fork-modal.tsx +377 -0
- package/src/tui/components/image-preview-modal.tsx +141 -0
- package/src/tui/components/image-utils.ts +128 -0
- package/src/tui/components/info-bar.tsx +103 -0
- package/src/tui/components/input-box.tsx +352 -0
- package/src/tui/components/mcp/MCPSettings.tsx +386 -0
- package/src/tui/components/mcp/index.ts +7 -0
- package/src/tui/components/model-modal.tsx +310 -0
- package/src/tui/components/pending-area.tsx +88 -0
- package/src/tui/components/rename-modal.tsx +119 -0
- package/src/tui/components/session-modal.tsx +233 -0
- package/src/tui/components/side-bar.tsx +349 -0
- package/src/tui/components/tool-output.ts +6 -0
- package/src/tui/hooks/user-prompt-history.ts +114 -0
- package/src/tui/theme.ts +63 -0
- package/src/types/commands.ts +80 -0
- package/src/types/cross-spawn.d.ts +24 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Settings Component
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
6
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
7
|
+
import { createTextAttributes, type TextareaRenderable } from "@opentui/core";
|
|
8
|
+
import type { McpServerConfig } from "../../../services/mcp/types.js";
|
|
9
|
+
import {
|
|
10
|
+
getAllMcpConfigs,
|
|
11
|
+
getMcpConfig,
|
|
12
|
+
setMcpConfig,
|
|
13
|
+
removeMcpConfig,
|
|
14
|
+
isMcpServerDisabled,
|
|
15
|
+
validateMcpConfig,
|
|
16
|
+
loadMcpConfigs,
|
|
17
|
+
} from "../../../services/mcp/config.js";
|
|
18
|
+
import { connectMcpServer, disconnectMcpServer, toggleMcpServer, getMcpConnections } from "../../../services/mcp/connection-manager.js";
|
|
19
|
+
|
|
20
|
+
interface MCPSettingsProps {
|
|
21
|
+
isActive: boolean;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
onMcpChange?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type View = "list" | "add" | "edit";
|
|
27
|
+
type ServerType = "stdio" | "sse" | "http" | "ws";
|
|
28
|
+
|
|
29
|
+
export function MCPSettings({ isActive, onClose, onMcpChange }: MCPSettingsProps) {
|
|
30
|
+
const { width } = useTerminalDimensions();
|
|
31
|
+
const [view, setView] = useState<View>("list");
|
|
32
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
33
|
+
const [servers, setServers] = useState<string[]>([]);
|
|
34
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
35
|
+
|
|
36
|
+
// Form state
|
|
37
|
+
const [editName, setEditName] = useState("");
|
|
38
|
+
const [editType, setEditType] = useState<ServerType>("stdio");
|
|
39
|
+
const [editCommand, setEditCommand] = useState("");
|
|
40
|
+
const [editArgs, setEditArgs] = useState("");
|
|
41
|
+
const [editUrl, setEditUrl] = useState("");
|
|
42
|
+
const [editHeaders, setEditHeaders] = useState("");
|
|
43
|
+
|
|
44
|
+
// Textarea refs
|
|
45
|
+
const nameRef = useRef<TextareaRenderable>(null);
|
|
46
|
+
const commandRef = useRef<TextareaRenderable>(null);
|
|
47
|
+
const argsRef = useRef<TextareaRenderable>(null);
|
|
48
|
+
const urlRef = useRef<TextareaRenderable>(null);
|
|
49
|
+
const headersRef = useRef<TextareaRenderable>(null);
|
|
50
|
+
|
|
51
|
+
const panelWidth = Math.min(75, Math.max(50, Math.floor(width * 0.8)));
|
|
52
|
+
|
|
53
|
+
const refreshServers = useCallback(() => {
|
|
54
|
+
loadMcpConfigs();
|
|
55
|
+
const configs = getAllMcpConfigs();
|
|
56
|
+
setServers(Array.from(configs.keys()));
|
|
57
|
+
setSelectedIndex(0);
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (isActive) {
|
|
62
|
+
refreshServers();
|
|
63
|
+
setView("list");
|
|
64
|
+
setMessage(null);
|
|
65
|
+
}
|
|
66
|
+
}, [isActive, refreshServers]);
|
|
67
|
+
|
|
68
|
+
const getServerStatus = (name: string) => {
|
|
69
|
+
const connection = getMcpConnections().get(name);
|
|
70
|
+
if (connection) return connection.status;
|
|
71
|
+
if (isMcpServerDisabled(name)) return "disabled";
|
|
72
|
+
return "disconnected";
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const getServerType = (name: string) => {
|
|
76
|
+
const config = getMcpConfig(name);
|
|
77
|
+
return config?.type ?? "unknown";
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleConnect = async (name: string) => {
|
|
81
|
+
setMessage(`Connecting to ${name}...`);
|
|
82
|
+
const result = await connectMcpServer(name);
|
|
83
|
+
setMessage(result.success ? `Connected to ${name}` : `Failed: ${result.error}`);
|
|
84
|
+
refreshServers();
|
|
85
|
+
onMcpChange?.();
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const handleToggle = async (name: string) => {
|
|
89
|
+
const connection = getMcpConnections().get(name);
|
|
90
|
+
const status = connection?.status ?? "disconnected";
|
|
91
|
+
|
|
92
|
+
if (status === "connected" || status === "failed") {
|
|
93
|
+
await toggleMcpServer(name, false);
|
|
94
|
+
setMessage(`Disabled ${name}`);
|
|
95
|
+
refreshServers();
|
|
96
|
+
onMcpChange?.();
|
|
97
|
+
} else {
|
|
98
|
+
await toggleMcpServer(name, true);
|
|
99
|
+
setMessage(`Enabled ${name}`);
|
|
100
|
+
refreshServers();
|
|
101
|
+
onMcpChange?.();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const handleDelete = (name: string) => {
|
|
106
|
+
removeMcpConfig(name);
|
|
107
|
+
disconnectMcpServer(name).catch(() => {});
|
|
108
|
+
setMessage(`Removed ${name}`);
|
|
109
|
+
refreshServers();
|
|
110
|
+
onMcpChange?.();
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleAddNew = () => {
|
|
114
|
+
setEditName("");
|
|
115
|
+
setEditType("stdio");
|
|
116
|
+
setEditCommand("");
|
|
117
|
+
setEditArgs("");
|
|
118
|
+
setEditUrl("");
|
|
119
|
+
setEditHeaders("");
|
|
120
|
+
setView("add");
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const handleEdit = (name: string) => {
|
|
124
|
+
const config = getMcpConfig(name);
|
|
125
|
+
if (!config) {
|
|
126
|
+
setMessage(`Config not found for ${name}`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
setEditName(name);
|
|
131
|
+
if (config.type === "stdio" && config.command) {
|
|
132
|
+
setEditType("stdio");
|
|
133
|
+
setEditCommand(config.command);
|
|
134
|
+
setEditArgs(config.args?.join(" ") ?? "");
|
|
135
|
+
} else {
|
|
136
|
+
setEditType((config.type ?? "sse") as ServerType);
|
|
137
|
+
setEditUrl(config.url ?? "");
|
|
138
|
+
setEditHeaders(Object.entries(config.headers ?? {}).map(([k, v]) => `${k}:${v}`).join("\n"));
|
|
139
|
+
}
|
|
140
|
+
setView("edit");
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const handleSave = () => {
|
|
144
|
+
let config: McpServerConfig;
|
|
145
|
+
|
|
146
|
+
if (editType === "stdio") {
|
|
147
|
+
if (!editCommand.trim()) { setMessage("Command is required for stdio servers"); return; }
|
|
148
|
+
config = { type: "stdio", command: editCommand.trim(), args: editArgs.trim() ? editArgs.trim().split(/\s+/) : [] };
|
|
149
|
+
} else {
|
|
150
|
+
if (!editUrl.trim()) { setMessage("URL is required for remote servers"); return; }
|
|
151
|
+
const headers: Record<string, string> = {};
|
|
152
|
+
for (const line of editHeaders.split("\n")) {
|
|
153
|
+
const [key, ...valueParts] = line.split(":");
|
|
154
|
+
if (key && valueParts.length > 0) headers[key.trim()] = valueParts.join(":").trim();
|
|
155
|
+
}
|
|
156
|
+
config = { type: editType, url: editUrl.trim(), headers: Object.keys(headers).length > 0 ? headers : undefined };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const nameToUse = editName.trim() || (editType === "stdio" ? editCommand.trim() : editUrl.trim());
|
|
160
|
+
if (!nameToUse) { setMessage("Could not determine server name"); return; }
|
|
161
|
+
|
|
162
|
+
const validation = validateMcpConfig(nameToUse, config);
|
|
163
|
+
if (!validation.valid) { setMessage(validation.errors?.join(", ") ?? "Invalid config"); return; }
|
|
164
|
+
|
|
165
|
+
if (view === "add" && getMcpConfig(nameToUse)) { setMessage(`Server '${nameToUse}' already exists`); return; }
|
|
166
|
+
|
|
167
|
+
setMcpConfig(nameToUse, config, "user");
|
|
168
|
+
setMessage(`Saved ${nameToUse} - connecting...`);
|
|
169
|
+
setView("list");
|
|
170
|
+
refreshServers();
|
|
171
|
+
|
|
172
|
+
// Auto-connect after saving
|
|
173
|
+
void (async () => {
|
|
174
|
+
const result = await connectMcpServer(nameToUse);
|
|
175
|
+
setMessage(result.success ? `Connected to ${nameToUse}` : `Saved but failed to connect: ${result.error}`);
|
|
176
|
+
refreshServers();
|
|
177
|
+
onMcpChange?.();
|
|
178
|
+
})();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const handleBack = () => { setView("list"); setMessage(null); };
|
|
182
|
+
|
|
183
|
+
useKeyboard((key) => {
|
|
184
|
+
if (!isActive) return;
|
|
185
|
+
|
|
186
|
+
if (key.name === "escape") {
|
|
187
|
+
if (view !== "list") { handleBack(); } else { onClose(); }
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (view === "list") {
|
|
192
|
+
if (key.name === "up") { setSelectedIndex(i => Math.max(0, i - 1)); return; }
|
|
193
|
+
if (key.name === "down") { setSelectedIndex(i => Math.min(servers.length - 1, i + 1)); return; }
|
|
194
|
+
if (key.name === "return") {
|
|
195
|
+
const selectedServer = servers[selectedIndex];
|
|
196
|
+
if (selectedServer) { handleEdit(selectedServer); }
|
|
197
|
+
else if (servers.length === 0) { handleAddNew(); }
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (key.name === "n" || key.name === "N") { handleAddNew(); return; }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (view === "edit" || view === "add") {
|
|
204
|
+
if (key.name === "return") { handleSave(); return; }
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!isActive) return null;
|
|
209
|
+
|
|
210
|
+
const statusColors: Record<string, string> = {
|
|
211
|
+
connected: "#00ff99", failed: "#ff6b6b", disabled: "#fbbf24", disconnected: "#6c6c7c", pending: "#00d9ff",
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const TYPE_BUTTON_ENABLED_BG = "#ff6b9d";
|
|
215
|
+
const TYPE_BUTTON_DISABLED_BG = "#4a4a5a";
|
|
216
|
+
|
|
217
|
+
// List View
|
|
218
|
+
if (view === "list") {
|
|
219
|
+
return (
|
|
220
|
+
<box position="absolute" top={0} left={0} width="100%" height="100%" backgroundColor="#00000080" alignItems="center" justifyContent="center">
|
|
221
|
+
<box width={panelWidth} flexDirection="column" borderStyle="rounded" borderColor="#4a4a5a" backgroundColor="#1a1a2e" paddingX={2} paddingY={1}>
|
|
222
|
+
<text attributes={createTextAttributes({ bold: true })} fg="#ff79c6">MCP Servers</text>
|
|
223
|
+
|
|
224
|
+
<box flexDirection="column" flexGrow={1} overflow="hidden" marginTop={1}>
|
|
225
|
+
{servers.length === 0 ? (
|
|
226
|
+
<box height={1}><text fg="#6c6c7c">No MCP servers configured. Press N to add one.</text></box>
|
|
227
|
+
) : servers.map((name, index) => {
|
|
228
|
+
const status = getServerStatus(name);
|
|
229
|
+
const type = getServerType(name);
|
|
230
|
+
const color = statusColors[status] ?? "#6c6c7c";
|
|
231
|
+
const isSelected = index === selectedIndex;
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<box
|
|
235
|
+
key={name}
|
|
236
|
+
height={1}
|
|
237
|
+
backgroundColor={isSelected ? "#44475a" : undefined}
|
|
238
|
+
paddingLeft={2}
|
|
239
|
+
flexDirection="row"
|
|
240
|
+
>
|
|
241
|
+
<text fg={color}>● </text>
|
|
242
|
+
<text width={20} fg={isSelected ? "#ff79c6" : "#f8f8f2"}>{name}</text>
|
|
243
|
+
<text width={8} fg="#6c6c7c" attributes={createTextAttributes({ dim: true })}>[{type}]</text>
|
|
244
|
+
<text fg={color}>{status}</text>
|
|
245
|
+
<box flexGrow={1} />
|
|
246
|
+
{(status === "disconnected" || status === "failed") ? (
|
|
247
|
+
<text fg="#2dd4bf" onMouseUp={() => void handleConnect(name)}>Connect</text>
|
|
248
|
+
) : null}
|
|
249
|
+
<text fg="#fbbf24" marginLeft={1} onMouseUp={() => handleToggle(name)}>
|
|
250
|
+
{status === "disabled" ? "Enable" : "Disable"}
|
|
251
|
+
</text>
|
|
252
|
+
<text fg="#f43f5e" marginLeft={1} onMouseUp={() => handleDelete(name)}>Remove</text>
|
|
253
|
+
</box>
|
|
254
|
+
);
|
|
255
|
+
})}
|
|
256
|
+
</box>
|
|
257
|
+
|
|
258
|
+
<box marginTop={1} flexDirection="row" justifyContent="space-between">
|
|
259
|
+
<text fg="#6c6c7c" attributes={createTextAttributes({ dim: true })}>[N] Add [Enter] Edit [Esc] Close</text>
|
|
260
|
+
{message ? <text fg="#fbbf24">{message}</text> : null}
|
|
261
|
+
</box>
|
|
262
|
+
</box>
|
|
263
|
+
</box>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Edit/Add View
|
|
268
|
+
return (
|
|
269
|
+
<box position="absolute" top={0} left={0} width="100%" height="100%" backgroundColor="#00000080" alignItems="center" justifyContent="center">
|
|
270
|
+
<box width={panelWidth} flexDirection="column" borderStyle="rounded" borderColor="#4a4a5a" backgroundColor="#1a1a2e" paddingX={2} paddingY={1}>
|
|
271
|
+
<text attributes={createTextAttributes({ bold: true })} fg="#ff79c6">
|
|
272
|
+
{view === "add" ? "Add MCP Server" : `Edit: ${editName}`}
|
|
273
|
+
</text>
|
|
274
|
+
|
|
275
|
+
<box flexDirection="column" flexGrow={1} marginTop={1} gap={1}>
|
|
276
|
+
{view === "add" && (
|
|
277
|
+
<box height={1} flexDirection="row" alignItems="center">
|
|
278
|
+
<text width={12} fg="#f8f8f2">Name:</text>
|
|
279
|
+
<textarea
|
|
280
|
+
ref={nameRef}
|
|
281
|
+
initialValue={editName}
|
|
282
|
+
height={1}
|
|
283
|
+
width={30}
|
|
284
|
+
onContentChange={() => {
|
|
285
|
+
const text = nameRef.current?.editBuffer.getText() ?? "";
|
|
286
|
+
setEditName(text);
|
|
287
|
+
}}
|
|
288
|
+
/>
|
|
289
|
+
</box>
|
|
290
|
+
)}
|
|
291
|
+
|
|
292
|
+
<box height={1} flexDirection="row" alignItems="center">
|
|
293
|
+
<text width={12} fg="#f8f8f2">Type:</text>
|
|
294
|
+
<box flexDirection="row" gap={1}>
|
|
295
|
+
{(["stdio", "sse", "http", "ws"] as ServerType[]).map((t) => (
|
|
296
|
+
<box
|
|
297
|
+
key={t}
|
|
298
|
+
paddingLeft={1}
|
|
299
|
+
paddingRight={1}
|
|
300
|
+
backgroundColor={editType === t ? TYPE_BUTTON_ENABLED_BG : TYPE_BUTTON_DISABLED_BG}
|
|
301
|
+
onMouseUp={() => setEditType(t)}
|
|
302
|
+
>
|
|
303
|
+
<text fg={editType === t ? "#ffffff" : "#a0a0b0"} attributes={createTextAttributes({ bold: true })}>
|
|
304
|
+
{t.toUpperCase()}
|
|
305
|
+
</text>
|
|
306
|
+
</box>
|
|
307
|
+
))}
|
|
308
|
+
</box>
|
|
309
|
+
</box>
|
|
310
|
+
|
|
311
|
+
{editType === "stdio" ? (
|
|
312
|
+
<>
|
|
313
|
+
<box height={1} flexDirection="row" alignItems="center">
|
|
314
|
+
<text width={12} fg="#f8f8f2">Command:</text>
|
|
315
|
+
<textarea
|
|
316
|
+
ref={commandRef}
|
|
317
|
+
initialValue={editCommand}
|
|
318
|
+
height={1}
|
|
319
|
+
width={40}
|
|
320
|
+
onContentChange={() => {
|
|
321
|
+
const text = commandRef.current?.editBuffer.getText() ?? "";
|
|
322
|
+
setEditCommand(text);
|
|
323
|
+
}}
|
|
324
|
+
/>
|
|
325
|
+
</box>
|
|
326
|
+
<box height={1} flexDirection="row" alignItems="center">
|
|
327
|
+
<text width={12} fg="#f8f8f2">Args:</text>
|
|
328
|
+
<textarea
|
|
329
|
+
ref={argsRef}
|
|
330
|
+
initialValue={editArgs}
|
|
331
|
+
height={1}
|
|
332
|
+
width={40}
|
|
333
|
+
onContentChange={() => {
|
|
334
|
+
const text = argsRef.current?.editBuffer.getText() ?? "";
|
|
335
|
+
setEditArgs(text);
|
|
336
|
+
}}
|
|
337
|
+
/>
|
|
338
|
+
</box>
|
|
339
|
+
</>
|
|
340
|
+
) : (
|
|
341
|
+
<>
|
|
342
|
+
<box height={1} flexDirection="row" alignItems="center">
|
|
343
|
+
<text width={12} fg="#f8f8f2">URL:</text>
|
|
344
|
+
<textarea
|
|
345
|
+
ref={urlRef}
|
|
346
|
+
initialValue={editUrl}
|
|
347
|
+
height={1}
|
|
348
|
+
width={50}
|
|
349
|
+
onContentChange={() => {
|
|
350
|
+
const text = urlRef.current?.editBuffer.getText() ?? "";
|
|
351
|
+
setEditUrl(text);
|
|
352
|
+
}}
|
|
353
|
+
/>
|
|
354
|
+
</box>
|
|
355
|
+
<box height={2} flexDirection="row" alignItems="flex-start">
|
|
356
|
+
<text width={12} fg="#f8f8f2">Headers:</text>
|
|
357
|
+
<textarea
|
|
358
|
+
ref={headersRef}
|
|
359
|
+
initialValue={editHeaders}
|
|
360
|
+
height={2}
|
|
361
|
+
width={50}
|
|
362
|
+
onContentChange={() => {
|
|
363
|
+
const text = headersRef.current?.editBuffer.getText() ?? "";
|
|
364
|
+
setEditHeaders(text);
|
|
365
|
+
}}
|
|
366
|
+
/>
|
|
367
|
+
</box>
|
|
368
|
+
</>
|
|
369
|
+
)}
|
|
370
|
+
</box>
|
|
371
|
+
|
|
372
|
+
<box marginTop={1} flexDirection="row" justifyContent="space-between" alignItems="center">
|
|
373
|
+
<box flexDirection="row" gap={2}>
|
|
374
|
+
<box paddingX={2} backgroundColor="#2dd4bf" onMouseUp={handleSave}>
|
|
375
|
+
<text fg="white" attributes={createTextAttributes({ bold: true })}>[Save]</text>
|
|
376
|
+
</box>
|
|
377
|
+
<box paddingX={2} backgroundColor="#f43f5e" onMouseUp={handleBack}>
|
|
378
|
+
<text fg="white" attributes={createTextAttributes({ bold: true })}>[Cancel]</text>
|
|
379
|
+
</box>
|
|
380
|
+
</box>
|
|
381
|
+
{message ? <text fg="#fbbf24">{message}</text> : null}
|
|
382
|
+
</box>
|
|
383
|
+
</box>
|
|
384
|
+
</box>
|
|
385
|
+
);
|
|
386
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Selection Modal
|
|
3
|
+
*
|
|
4
|
+
* Shows configured providers as section headers with their models as
|
|
5
|
+
* selectable items. Uses Pi SDK model registry.
|
|
6
|
+
*
|
|
7
|
+
* Supports Primary / Auxiliary model selection via tab switcher
|
|
8
|
+
* in the top-right corner. Press Tab to toggle between modes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
12
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
13
|
+
import { createTextAttributes } from "@opentui/core";
|
|
14
|
+
import type { MouseEvent } from "@opentui/core";
|
|
15
|
+
import {
|
|
16
|
+
getConfiguredProviders,
|
|
17
|
+
getCurrentModel,
|
|
18
|
+
getAuxiliaryModel,
|
|
19
|
+
setCurrentModel,
|
|
20
|
+
setAuxiliaryModel,
|
|
21
|
+
getProviderModels,
|
|
22
|
+
type ModelRef,
|
|
23
|
+
} from "../../config/settings.js";
|
|
24
|
+
|
|
25
|
+
interface ModelModalProps {
|
|
26
|
+
isActive: boolean;
|
|
27
|
+
onClose: () => void;
|
|
28
|
+
onSelectPrimary?: (model: ModelRef) => void;
|
|
29
|
+
onSelectAuxiliary?: (model: ModelRef) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface FlatItem {
|
|
33
|
+
type: "header" | "model";
|
|
34
|
+
provider?: string;
|
|
35
|
+
modelId?: string;
|
|
36
|
+
modelName?: string;
|
|
37
|
+
modelIndex?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function ModelModal({
|
|
41
|
+
isActive,
|
|
42
|
+
onClose,
|
|
43
|
+
onSelectPrimary,
|
|
44
|
+
onSelectAuxiliary,
|
|
45
|
+
}: ModelModalProps) {
|
|
46
|
+
const { height } = useTerminalDimensions();
|
|
47
|
+
const configuredProviders = getConfiguredProviders();
|
|
48
|
+
const [selectedModelIndex, setSelectedModelIndex] = useState(0);
|
|
49
|
+
const [activeTab, setActiveTab] = useState<"primary" | "auxiliary">("primary");
|
|
50
|
+
const scrollOffsetRef = useRef(0);
|
|
51
|
+
|
|
52
|
+
const listHeight = Math.min(12, Math.floor(height * 0.4));
|
|
53
|
+
const primaryModel = getCurrentModel();
|
|
54
|
+
const auxiliaryModel = getAuxiliaryModel();
|
|
55
|
+
|
|
56
|
+
// Reset when opened
|
|
57
|
+
useLayoutEffect(() => {
|
|
58
|
+
if (isActive) {
|
|
59
|
+
setSelectedModelIndex(0);
|
|
60
|
+
setActiveTab("primary");
|
|
61
|
+
scrollOffsetRef.current = 0;
|
|
62
|
+
}
|
|
63
|
+
}, [isActive]);
|
|
64
|
+
|
|
65
|
+
// Build flat list of providers + models
|
|
66
|
+
const { flatItems, modelCount } = useMemo(() => {
|
|
67
|
+
const items: FlatItem[] = [];
|
|
68
|
+
let mIdx = 0;
|
|
69
|
+
for (const provider of configuredProviders) {
|
|
70
|
+
items.push({ type: "header", provider });
|
|
71
|
+
const models = getProviderModels(provider);
|
|
72
|
+
for (const model of models) {
|
|
73
|
+
items.push({
|
|
74
|
+
type: "model",
|
|
75
|
+
provider,
|
|
76
|
+
modelId: model.id,
|
|
77
|
+
modelName: model.name || model.id,
|
|
78
|
+
modelIndex: mIdx,
|
|
79
|
+
});
|
|
80
|
+
mIdx++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { flatItems: items, modelCount: mIdx };
|
|
84
|
+
}, [configuredProviders]);
|
|
85
|
+
|
|
86
|
+
// Clamp selected index
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (selectedModelIndex >= modelCount && modelCount > 0) {
|
|
89
|
+
setSelectedModelIndex(modelCount - 1);
|
|
90
|
+
}
|
|
91
|
+
}, [modelCount, selectedModelIndex]);
|
|
92
|
+
|
|
93
|
+
// Effective scroll offset
|
|
94
|
+
const effectiveScrollOffset = scrollOffsetRef.current;
|
|
95
|
+
|
|
96
|
+
useKeyboard((key) => {
|
|
97
|
+
if (!isActive) return;
|
|
98
|
+
|
|
99
|
+
if (key.name === "tab" || key.name === "TAB") {
|
|
100
|
+
setActiveTab((prev) => (prev === "primary" ? "auxiliary" : "primary"));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (key.name === "escape") {
|
|
105
|
+
onClose();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Navigation with direct scroll calculation
|
|
110
|
+
if (key.name === "up" || key.name === "down") {
|
|
111
|
+
const newIndex = key.name === "up"
|
|
112
|
+
? Math.max(0, selectedModelIndex - 1)
|
|
113
|
+
: Math.min(modelCount - 1, selectedModelIndex + 1);
|
|
114
|
+
|
|
115
|
+
const newFlatIndex = flatItems.findIndex(
|
|
116
|
+
(i) => i.type === "model" && i.modelIndex === newIndex
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
let newScrollOffset = scrollOffsetRef.current;
|
|
120
|
+
if (newFlatIndex !== -1) {
|
|
121
|
+
if (newFlatIndex < scrollOffsetRef.current) {
|
|
122
|
+
newScrollOffset = newFlatIndex;
|
|
123
|
+
} else if (newFlatIndex > scrollOffsetRef.current + listHeight - 1) {
|
|
124
|
+
newScrollOffset = newFlatIndex - listHeight + 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
scrollOffsetRef.current = newScrollOffset;
|
|
129
|
+
setSelectedModelIndex(newIndex);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (key.name === "return") {
|
|
133
|
+
const selectedItem = flatItems.find(
|
|
134
|
+
(i) => i.type === "model" && i.modelIndex === selectedModelIndex
|
|
135
|
+
);
|
|
136
|
+
if (selectedItem?.provider && selectedItem.modelId) {
|
|
137
|
+
const ref = {
|
|
138
|
+
provider: selectedItem.provider,
|
|
139
|
+
modelId: selectedItem.modelId,
|
|
140
|
+
};
|
|
141
|
+
if (activeTab === "primary") {
|
|
142
|
+
setCurrentModel(ref);
|
|
143
|
+
onSelectPrimary?.(ref);
|
|
144
|
+
} else {
|
|
145
|
+
setAuxiliaryModel(ref);
|
|
146
|
+
onSelectAuxiliary?.(ref);
|
|
147
|
+
}
|
|
148
|
+
onClose();
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (!isActive) return null;
|
|
155
|
+
|
|
156
|
+
const visibleItems = flatItems.slice(effectiveScrollOffset, effectiveScrollOffset + listHeight);
|
|
157
|
+
|
|
158
|
+
const isCurrent = (provider?: string, modelId?: string) => {
|
|
159
|
+
const target = activeTab === "primary" ? primaryModel : auxiliaryModel;
|
|
160
|
+
return target?.provider === provider && target?.modelId === modelId;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const handleMouseSelect = (
|
|
164
|
+
e: MouseEvent,
|
|
165
|
+
provider?: string,
|
|
166
|
+
modelId?: string
|
|
167
|
+
) => {
|
|
168
|
+
e.stopPropagation();
|
|
169
|
+
if (provider && modelId) {
|
|
170
|
+
const ref = { provider, modelId };
|
|
171
|
+
if (activeTab === "primary") {
|
|
172
|
+
setCurrentModel(ref);
|
|
173
|
+
onSelectPrimary?.(ref);
|
|
174
|
+
} else {
|
|
175
|
+
setAuxiliaryModel(ref);
|
|
176
|
+
onSelectAuxiliary?.(ref);
|
|
177
|
+
}
|
|
178
|
+
onClose();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<box
|
|
184
|
+
position="absolute"
|
|
185
|
+
top={0}
|
|
186
|
+
left={0}
|
|
187
|
+
width="100%"
|
|
188
|
+
height="100%"
|
|
189
|
+
backgroundColor="#00000080"
|
|
190
|
+
alignItems="center"
|
|
191
|
+
justifyContent="center"
|
|
192
|
+
>
|
|
193
|
+
<box
|
|
194
|
+
width={60}
|
|
195
|
+
flexDirection="column"
|
|
196
|
+
borderStyle="rounded"
|
|
197
|
+
borderColor="#4a4a5a"
|
|
198
|
+
backgroundColor="#1a1a2e"
|
|
199
|
+
paddingX={2}
|
|
200
|
+
paddingY={1}
|
|
201
|
+
>
|
|
202
|
+
{/* Header row with tabs */}
|
|
203
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
204
|
+
<text attributes={createTextAttributes({ bold: true })} fg="#ff79c6">
|
|
205
|
+
Select Model
|
|
206
|
+
</text>
|
|
207
|
+
|
|
208
|
+
<box flexDirection="row" gap={1}>
|
|
209
|
+
<box
|
|
210
|
+
paddingX={1}
|
|
211
|
+
backgroundColor={
|
|
212
|
+
activeTab === "primary" ? "#44475a" : undefined
|
|
213
|
+
}
|
|
214
|
+
onMouseUp={(e: MouseEvent) => {
|
|
215
|
+
e.stopPropagation();
|
|
216
|
+
setActiveTab("primary");
|
|
217
|
+
}}
|
|
218
|
+
>
|
|
219
|
+
<text
|
|
220
|
+
fg={activeTab === "primary" ? "#ff79c6" : "#6c6c7c"}
|
|
221
|
+
attributes={createTextAttributes({ bold: activeTab === "primary" })}
|
|
222
|
+
>
|
|
223
|
+
Primary
|
|
224
|
+
</text>
|
|
225
|
+
</box>
|
|
226
|
+
<box
|
|
227
|
+
paddingX={1}
|
|
228
|
+
backgroundColor={
|
|
229
|
+
activeTab === "auxiliary" ? "#44475a" : undefined
|
|
230
|
+
}
|
|
231
|
+
onMouseUp={(e: MouseEvent) => {
|
|
232
|
+
e.stopPropagation();
|
|
233
|
+
setActiveTab("auxiliary");
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
<text
|
|
237
|
+
fg={activeTab === "auxiliary" ? "#ff79c6" : "#6c6c7c"}
|
|
238
|
+
attributes={createTextAttributes({ bold: activeTab === "auxiliary" })}
|
|
239
|
+
>
|
|
240
|
+
Auxiliary
|
|
241
|
+
</text>
|
|
242
|
+
</box>
|
|
243
|
+
</box>
|
|
244
|
+
</box>
|
|
245
|
+
|
|
246
|
+
<box
|
|
247
|
+
height={listHeight}
|
|
248
|
+
flexDirection="column"
|
|
249
|
+
overflow="hidden"
|
|
250
|
+
marginTop={1}
|
|
251
|
+
>
|
|
252
|
+
{configuredProviders.length === 0 && (
|
|
253
|
+
<box height={1}>
|
|
254
|
+
<text fg="#6c6c7c">
|
|
255
|
+
No providers configured. Use /connect to add one.
|
|
256
|
+
</text>
|
|
257
|
+
</box>
|
|
258
|
+
)}
|
|
259
|
+
{visibleItems.map((item, idx) => {
|
|
260
|
+
const flatIndex = effectiveScrollOffset + idx;
|
|
261
|
+
if (item.type === "header") {
|
|
262
|
+
return (
|
|
263
|
+
<box
|
|
264
|
+
key={`h-${item.provider}-${flatIndex}`}
|
|
265
|
+
height={1}
|
|
266
|
+
marginTop={1}
|
|
267
|
+
>
|
|
268
|
+
<text
|
|
269
|
+
fg="#ff79c6"
|
|
270
|
+
attributes={createTextAttributes({ bold: true })}
|
|
271
|
+
>
|
|
272
|
+
{item.provider}
|
|
273
|
+
</text>
|
|
274
|
+
</box>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const isSelected = item.modelIndex === selectedModelIndex;
|
|
278
|
+
const current = isCurrent(item.provider, item.modelId);
|
|
279
|
+
return (
|
|
280
|
+
<box
|
|
281
|
+
key={`m-${item.modelId}-${flatIndex}`}
|
|
282
|
+
height={1}
|
|
283
|
+
backgroundColor={isSelected ? "#44475a" : undefined}
|
|
284
|
+
paddingLeft={2}
|
|
285
|
+
flexDirection="row"
|
|
286
|
+
onMouseUp={(e: MouseEvent) =>
|
|
287
|
+
handleMouseSelect(e, item.provider, item.modelId)
|
|
288
|
+
}
|
|
289
|
+
>
|
|
290
|
+
<text fg={isSelected ? "#ff79c6" : "#f8f8f2"}>
|
|
291
|
+
{current ? "● " : " "}
|
|
292
|
+
{item.modelName}
|
|
293
|
+
</text>
|
|
294
|
+
</box>
|
|
295
|
+
);
|
|
296
|
+
})}
|
|
297
|
+
</box>
|
|
298
|
+
|
|
299
|
+
<box marginTop={1} flexDirection="row" justifyContent="space-between">
|
|
300
|
+
<text fg="#6c6c7c" attributes={createTextAttributes({ dim: true })}>
|
|
301
|
+
↑↓ Navigate Enter Select Esc Cancel
|
|
302
|
+
</text>
|
|
303
|
+
<text fg="#6c6c7c" attributes={createTextAttributes({ dim: true })}>
|
|
304
|
+
Tab Switch
|
|
305
|
+
</text>
|
|
306
|
+
</box>
|
|
307
|
+
</box>
|
|
308
|
+
</box>
|
|
309
|
+
);
|
|
310
|
+
}
|