@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,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connect Provider Modal
|
|
3
|
+
*
|
|
4
|
+
* Multi-step flow: provider selection → existing config view / auth input →
|
|
5
|
+
* verification (animated) → result.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useEffect, useRef, useState } from "react";
|
|
9
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
10
|
+
import { createTextAttributes } from "@opentui/core";
|
|
11
|
+
import type { TextareaRenderable, MouseEvent } from "@opentui/core";
|
|
12
|
+
import {
|
|
13
|
+
getAllProviders,
|
|
14
|
+
configureProvider,
|
|
15
|
+
removeProvider,
|
|
16
|
+
isProviderConfigured,
|
|
17
|
+
getProviderConfig,
|
|
18
|
+
validateProviderCredential,
|
|
19
|
+
type ProviderConfig,
|
|
20
|
+
} from "../../config/settings.js";
|
|
21
|
+
import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
|
|
22
|
+
|
|
23
|
+
interface ConnectModalProps {
|
|
24
|
+
isActive: boolean;
|
|
25
|
+
onClose: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type Step = "provider" | "existing" | "auth" | "verify" | "result";
|
|
29
|
+
|
|
30
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
31
|
+
|
|
32
|
+
function maskCredential(cred: string): string {
|
|
33
|
+
if (cred.length <= 8) return "•".repeat(cred.length);
|
|
34
|
+
return cred.slice(0, 4) + "•".repeat(cred.length - 8) + cred.slice(-4);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function ConnectModal({ isActive, onClose }: ConnectModalProps) {
|
|
38
|
+
const { height } = useTerminalDimensions();
|
|
39
|
+
const [step, setStep] = useState<Step>("provider");
|
|
40
|
+
const [providers] = useState(() => getAllProviders());
|
|
41
|
+
const [selectedProviderIndex, setSelectedProviderIndex] = useState(0);
|
|
42
|
+
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
|
|
43
|
+
const [authInput, setAuthInput] = useState("");
|
|
44
|
+
const [verifyResult, setVerifyResult] = useState<{ success: boolean; message: string } | null>(null);
|
|
45
|
+
const [spinnerFrame, setSpinnerFrame] = useState(0);
|
|
46
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
47
|
+
const inputRef = useRef<TextareaRenderable>(null);
|
|
48
|
+
|
|
49
|
+
const listHeight = Math.min(10, Math.floor(height * 0.35));
|
|
50
|
+
|
|
51
|
+
// Reset when opened
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (isActive) {
|
|
54
|
+
setStep("provider");
|
|
55
|
+
setSelectedProviderIndex(0);
|
|
56
|
+
setSelectedProvider(null);
|
|
57
|
+
setAuthInput("");
|
|
58
|
+
setVerifyResult(null);
|
|
59
|
+
setSpinnerFrame(0);
|
|
60
|
+
setScrollOffset(0);
|
|
61
|
+
}
|
|
62
|
+
}, [isActive]);
|
|
63
|
+
|
|
64
|
+
// Focus input on auth step
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (isActive && step === "auth") {
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
const ta = inputRef.current;
|
|
69
|
+
if (ta) {
|
|
70
|
+
ta.editBuffer.replaceText("");
|
|
71
|
+
ta.focus();
|
|
72
|
+
}
|
|
73
|
+
}, 10);
|
|
74
|
+
}
|
|
75
|
+
}, [isActive, step]);
|
|
76
|
+
|
|
77
|
+
// Spinner animation during verify
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!isActive || step !== "verify") return;
|
|
80
|
+
const interval = setInterval(() => {
|
|
81
|
+
setSpinnerFrame((f) => (f + 1) % SPINNER.length);
|
|
82
|
+
}, 80);
|
|
83
|
+
return () => clearInterval(interval);
|
|
84
|
+
}, [isActive, step]);
|
|
85
|
+
|
|
86
|
+
// Real verification
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!isActive || step !== "verify") return;
|
|
89
|
+
|
|
90
|
+
let cancelled = false;
|
|
91
|
+
|
|
92
|
+
void (async () => {
|
|
93
|
+
try {
|
|
94
|
+
const result = await validateProviderCredential(
|
|
95
|
+
selectedProvider!,
|
|
96
|
+
authInput.trim()
|
|
97
|
+
);
|
|
98
|
+
if (cancelled) return;
|
|
99
|
+
|
|
100
|
+
const config: ProviderConfig = {
|
|
101
|
+
provider: selectedProvider!,
|
|
102
|
+
authMethod: getOAuthProvider(selectedProvider!) ? "oauth" : "apikey",
|
|
103
|
+
credential: authInput.trim(),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
setVerifyResult({
|
|
107
|
+
success: result.valid,
|
|
108
|
+
message: result.valid
|
|
109
|
+
? `Connected to ${selectedProvider}!`
|
|
110
|
+
: `Failed to connect to ${selectedProvider}: ${result.error}`,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (result.valid && selectedProvider) {
|
|
114
|
+
configureProvider(config);
|
|
115
|
+
}
|
|
116
|
+
} catch (err: unknown) {
|
|
117
|
+
if (cancelled) return;
|
|
118
|
+
setVerifyResult({
|
|
119
|
+
success: false,
|
|
120
|
+
message: `Failed to connect to ${selectedProvider}: ${err instanceof Error ? err.message : String(err)}`,
|
|
121
|
+
});
|
|
122
|
+
} finally {
|
|
123
|
+
if (!cancelled) {
|
|
124
|
+
setStep("result");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
})();
|
|
128
|
+
|
|
129
|
+
return () => {
|
|
130
|
+
cancelled = true;
|
|
131
|
+
};
|
|
132
|
+
}, [isActive, step, authInput, selectedProvider]);
|
|
133
|
+
|
|
134
|
+
// Auto-scroll provider list
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (selectedProviderIndex < scrollOffset) {
|
|
137
|
+
setScrollOffset(selectedProviderIndex);
|
|
138
|
+
} else if (selectedProviderIndex >= scrollOffset + listHeight) {
|
|
139
|
+
setScrollOffset(selectedProviderIndex - listHeight + 1);
|
|
140
|
+
}
|
|
141
|
+
}, [selectedProviderIndex, listHeight, scrollOffset]);
|
|
142
|
+
|
|
143
|
+
const handleSelectProvider = (provider: string) => {
|
|
144
|
+
setSelectedProvider(provider);
|
|
145
|
+
if (isProviderConfigured(provider)) {
|
|
146
|
+
setStep("existing");
|
|
147
|
+
} else {
|
|
148
|
+
setStep("auth");
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const handleClearConfig = () => {
|
|
153
|
+
if (selectedProvider) {
|
|
154
|
+
removeProvider(selectedProvider);
|
|
155
|
+
}
|
|
156
|
+
setStep("provider");
|
|
157
|
+
setSelectedProvider(null);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const handleOverwrite = () => {
|
|
161
|
+
setStep("auth");
|
|
162
|
+
setAuthInput("");
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
useKeyboard((key) => {
|
|
166
|
+
if (!isActive) return;
|
|
167
|
+
if (key.name === "escape") {
|
|
168
|
+
if (step === "provider" || step === "result") {
|
|
169
|
+
onClose();
|
|
170
|
+
} else if (step === "existing") {
|
|
171
|
+
setStep("provider");
|
|
172
|
+
setSelectedProvider(null);
|
|
173
|
+
} else {
|
|
174
|
+
setStep("provider");
|
|
175
|
+
setSelectedProvider(null);
|
|
176
|
+
setAuthInput("");
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (step === "provider") {
|
|
181
|
+
if (key.name === "up") {
|
|
182
|
+
setSelectedProviderIndex((prev) => Math.max(0, prev - 1));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (key.name === "down") {
|
|
186
|
+
setSelectedProviderIndex((prev) => Math.max(0, Math.min(providers.length - 1, prev + 1)));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (key.name === "return") {
|
|
190
|
+
const provider = providers[selectedProviderIndex];
|
|
191
|
+
if (provider) {
|
|
192
|
+
handleSelectProvider(provider);
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (step === "existing") {
|
|
198
|
+
if (key.name === "o" || key.name === "O") {
|
|
199
|
+
handleOverwrite();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (key.name === "c" || key.name === "C") {
|
|
203
|
+
handleClearConfig();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (key.name === "return" || key.name === "l" || key.name === "L") {
|
|
207
|
+
setStep("provider");
|
|
208
|
+
setSelectedProvider(null);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (step === "auth") {
|
|
213
|
+
if (key.name === "return") {
|
|
214
|
+
if (authInput.trim()) {
|
|
215
|
+
setStep("verify");
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (step === "result") {
|
|
221
|
+
if (key.name === "return") {
|
|
222
|
+
onClose();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const handleAuthChange = () => {
|
|
229
|
+
const text = inputRef.current?.editBuffer.getText() ?? "";
|
|
230
|
+
setAuthInput(text);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const isOAuthProvider = (p?: string | null) => (p ? !!getOAuthProvider(p) : false);
|
|
234
|
+
|
|
235
|
+
const existingConfig = selectedProvider ? getProviderConfig(selectedProvider) : undefined;
|
|
236
|
+
|
|
237
|
+
if (!isActive) return null;
|
|
238
|
+
|
|
239
|
+
const visibleProviders = providers.slice(scrollOffset, scrollOffset + listHeight);
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<box
|
|
243
|
+
position="absolute"
|
|
244
|
+
top={0}
|
|
245
|
+
left={0}
|
|
246
|
+
width="100%"
|
|
247
|
+
height="100%"
|
|
248
|
+
backgroundColor="#00000080"
|
|
249
|
+
alignItems="center"
|
|
250
|
+
justifyContent="center"
|
|
251
|
+
>
|
|
252
|
+
<box
|
|
253
|
+
width={60}
|
|
254
|
+
flexDirection="column"
|
|
255
|
+
borderStyle="rounded"
|
|
256
|
+
borderColor="#4a4a5a"
|
|
257
|
+
backgroundColor="#1a1a2e"
|
|
258
|
+
paddingX={2}
|
|
259
|
+
paddingY={1}
|
|
260
|
+
>
|
|
261
|
+
{step === "provider" && (
|
|
262
|
+
<>
|
|
263
|
+
<text attributes={createTextAttributes({ bold: true })} fg="#ff79c6">
|
|
264
|
+
Select Provider
|
|
265
|
+
</text>
|
|
266
|
+
<box height={listHeight} flexDirection="column" overflow="hidden" marginTop={1}>
|
|
267
|
+
{visibleProviders.map((p, i) => {
|
|
268
|
+
const actualIndex = scrollOffset + i;
|
|
269
|
+
const configured = isProviderConfigured(p);
|
|
270
|
+
return (
|
|
271
|
+
<box
|
|
272
|
+
key={p}
|
|
273
|
+
height={1}
|
|
274
|
+
backgroundColor={actualIndex === selectedProviderIndex ? "#44475a" : undefined}
|
|
275
|
+
paddingLeft={1}
|
|
276
|
+
flexDirection="row"
|
|
277
|
+
onMouseUp={(e: MouseEvent) => {
|
|
278
|
+
e.stopPropagation();
|
|
279
|
+
handleSelectProvider(p);
|
|
280
|
+
}}
|
|
281
|
+
>
|
|
282
|
+
<text fg={actualIndex === selectedProviderIndex ? "#ff79c6" : "#f8f8f2"}>
|
|
283
|
+
{configured ? "● " : " "}
|
|
284
|
+
{p}
|
|
285
|
+
</text>
|
|
286
|
+
{configured && (
|
|
287
|
+
<text fg="#00ff99" marginLeft={1}>
|
|
288
|
+
configured
|
|
289
|
+
</text>
|
|
290
|
+
)}
|
|
291
|
+
</box>
|
|
292
|
+
);
|
|
293
|
+
})}
|
|
294
|
+
</box>
|
|
295
|
+
<box marginTop={1}>
|
|
296
|
+
<text fg="#6c6c7c" attributes={createTextAttributes({ dim: true })}>
|
|
297
|
+
↑↓ Navigate Enter Select Esc Cancel
|
|
298
|
+
</text>
|
|
299
|
+
</box>
|
|
300
|
+
</>
|
|
301
|
+
)}
|
|
302
|
+
|
|
303
|
+
{step === "existing" && existingConfig && (
|
|
304
|
+
<>
|
|
305
|
+
<text attributes={createTextAttributes({ bold: true })} fg="#ff79c6">
|
|
306
|
+
{selectedProvider} — Already Configured
|
|
307
|
+
</text>
|
|
308
|
+
<box marginTop={1}>
|
|
309
|
+
<text fg="#6c6c7c">
|
|
310
|
+
Auth method: {existingConfig.authMethod.toUpperCase()}
|
|
311
|
+
</text>
|
|
312
|
+
</box>
|
|
313
|
+
<box marginTop={1}>
|
|
314
|
+
<text fg="#f8f8f2">
|
|
315
|
+
{existingConfig.authMethod === "apikey"
|
|
316
|
+
? `API Key: ${maskCredential(existingConfig.credential)}`
|
|
317
|
+
: `OAuth token: ${maskCredential(existingConfig.credential)}`}
|
|
318
|
+
</text>
|
|
319
|
+
</box>
|
|
320
|
+
<box marginTop={1} flexDirection="row" gap={2}>
|
|
321
|
+
<box
|
|
322
|
+
paddingX={2}
|
|
323
|
+
backgroundColor="#2dd4bf"
|
|
324
|
+
onMouseUp={(e: MouseEvent) => {
|
|
325
|
+
e.stopPropagation();
|
|
326
|
+
handleOverwrite();
|
|
327
|
+
}}
|
|
328
|
+
>
|
|
329
|
+
<text attributes={createTextAttributes({ bold: true })} fg="white">
|
|
330
|
+
Overwrite (O)
|
|
331
|
+
</text>
|
|
332
|
+
</box>
|
|
333
|
+
<box
|
|
334
|
+
paddingX={2}
|
|
335
|
+
backgroundColor="#f43f5e"
|
|
336
|
+
onMouseUp={(e: MouseEvent) => {
|
|
337
|
+
e.stopPropagation();
|
|
338
|
+
handleClearConfig();
|
|
339
|
+
}}
|
|
340
|
+
>
|
|
341
|
+
<text attributes={createTextAttributes({ bold: true })} fg="white">
|
|
342
|
+
Clear (C)
|
|
343
|
+
</text>
|
|
344
|
+
</box>
|
|
345
|
+
<box
|
|
346
|
+
paddingX={2}
|
|
347
|
+
backgroundColor="#6272a4"
|
|
348
|
+
onMouseUp={(e: MouseEvent) => {
|
|
349
|
+
e.stopPropagation();
|
|
350
|
+
setStep("provider");
|
|
351
|
+
setSelectedProvider(null);
|
|
352
|
+
}}
|
|
353
|
+
>
|
|
354
|
+
<text attributes={createTextAttributes({ bold: true })} fg="white">
|
|
355
|
+
Leave (L)
|
|
356
|
+
</text>
|
|
357
|
+
</box>
|
|
358
|
+
</box>
|
|
359
|
+
<box marginTop={1}>
|
|
360
|
+
<text fg="#6c6c7c" attributes={createTextAttributes({ dim: true })}>
|
|
361
|
+
O Overwrite C Clear L Leave Esc Back
|
|
362
|
+
</text>
|
|
363
|
+
</box>
|
|
364
|
+
</>
|
|
365
|
+
)}
|
|
366
|
+
|
|
367
|
+
{step === "auth" && (
|
|
368
|
+
<>
|
|
369
|
+
<text attributes={createTextAttributes({ bold: true })} fg="#ff79c6">
|
|
370
|
+
Authenticate: {selectedProvider}
|
|
371
|
+
</text>
|
|
372
|
+
<box marginTop={1}>
|
|
373
|
+
<text fg="#6c6c7c">
|
|
374
|
+
{isOAuthProvider(selectedProvider)
|
|
375
|
+
? "This provider requires OAuth. Authenticate in your browser and paste the token."
|
|
376
|
+
: "Enter your API key or credentials for this provider."}
|
|
377
|
+
</text>
|
|
378
|
+
</box>
|
|
379
|
+
<box marginTop={1} height={1} backgroundColor="#16213e" paddingX={1}>
|
|
380
|
+
<textarea
|
|
381
|
+
ref={inputRef}
|
|
382
|
+
initialValue=""
|
|
383
|
+
focused={isActive}
|
|
384
|
+
showCursor
|
|
385
|
+
height={1}
|
|
386
|
+
wrapMode="none"
|
|
387
|
+
textColor="#f8f8f2"
|
|
388
|
+
backgroundColor="#16213e"
|
|
389
|
+
onContentChange={handleAuthChange}
|
|
390
|
+
/>
|
|
391
|
+
</box>
|
|
392
|
+
<box marginTop={1}>
|
|
393
|
+
<text fg="#6c6c7c" attributes={createTextAttributes({ dim: true })}>
|
|
394
|
+
Enter Confirm Esc Back
|
|
395
|
+
</text>
|
|
396
|
+
</box>
|
|
397
|
+
</>
|
|
398
|
+
)}
|
|
399
|
+
|
|
400
|
+
{step === "verify" && (
|
|
401
|
+
<>
|
|
402
|
+
<text attributes={createTextAttributes({ bold: true })} fg="#ff79c6">
|
|
403
|
+
Verifying
|
|
404
|
+
</text>
|
|
405
|
+
<box marginTop={1} flexDirection="row" alignItems="center">
|
|
406
|
+
<text fg="#00f5ff">{SPINNER[spinnerFrame]}</text>
|
|
407
|
+
<text fg="#f8f8f2" marginLeft={1}>
|
|
408
|
+
Connecting to {selectedProvider}...
|
|
409
|
+
</text>
|
|
410
|
+
</box>
|
|
411
|
+
</>
|
|
412
|
+
)}
|
|
413
|
+
|
|
414
|
+
{step === "result" && verifyResult && (
|
|
415
|
+
<>
|
|
416
|
+
<text
|
|
417
|
+
attributes={createTextAttributes({ bold: true })}
|
|
418
|
+
fg={verifyResult.success ? "#00ff99" : "#fb7185"}
|
|
419
|
+
>
|
|
420
|
+
{verifyResult.success ? "Success" : "Failed"}
|
|
421
|
+
</text>
|
|
422
|
+
<box marginTop={1}>
|
|
423
|
+
<text fg="#f8f8f2">{verifyResult.message}</text>
|
|
424
|
+
</box>
|
|
425
|
+
<box marginTop={1}>
|
|
426
|
+
<text fg="#6c6c7c" attributes={createTextAttributes({ dim: true })}>
|
|
427
|
+
Enter/Esc Close
|
|
428
|
+
</text>
|
|
429
|
+
</box>
|
|
430
|
+
</>
|
|
431
|
+
)}
|
|
432
|
+
</box>
|
|
433
|
+
</box>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connecting Modal
|
|
3
|
+
*
|
|
4
|
+
* Modal dialog showing MCP connection progress with spinner animation.
|
|
5
|
+
* Cannot be closed manually - auto-dismisses when all connections complete.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect } from "react";
|
|
9
|
+
import { createTextAttributes, type SpinnerVariant } from "@opentui/core";
|
|
10
|
+
import type { McpConnectionProgress } from "../../services/mcp/index.js";
|
|
11
|
+
|
|
12
|
+
interface ConnectingModalProps {
|
|
13
|
+
isActive: boolean;
|
|
14
|
+
progress: McpConnectionProgress | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Spinner frames for loading animation
|
|
18
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
19
|
+
|
|
20
|
+
function Spinner({ variant = "dots" }: { variant?: SpinnerVariant }) {
|
|
21
|
+
const [frame, setFrame] = useState(0);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!variant || variant === "dots") {
|
|
25
|
+
const interval = setInterval(() => {
|
|
26
|
+
setFrame((f) => (f + 1) % SPINNER_FRAMES.length);
|
|
27
|
+
}, 80);
|
|
28
|
+
return () => clearInterval(interval);
|
|
29
|
+
}
|
|
30
|
+
}, [variant]);
|
|
31
|
+
|
|
32
|
+
// For line spinner
|
|
33
|
+
if (variant === "line") {
|
|
34
|
+
return <text fg="#60a5fa">{">".repeat((frame % 4) + 1)}</text>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return <text fg="#60a5fa">{SPINNER_FRAMES[frame]}</text>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function StatusIcon({ status }: { status: "connecting" | "connected" | "failed" | "disabled" }) {
|
|
41
|
+
switch (status) {
|
|
42
|
+
case "connecting":
|
|
43
|
+
return <text fg="#fbbf24">◐</text>;
|
|
44
|
+
case "connected":
|
|
45
|
+
return <text fg="#34d399">✓</text>;
|
|
46
|
+
case "failed":
|
|
47
|
+
return <text fg="#f87171">✗</text>;
|
|
48
|
+
case "disabled":
|
|
49
|
+
return <text fg="#9ca3af">○</text>;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ProgressBar({ completed, total }: { completed: number; total: number }) {
|
|
54
|
+
const width = 30;
|
|
55
|
+
const filled = Math.round((completed / total) * width);
|
|
56
|
+
const empty = width - filled;
|
|
57
|
+
|
|
58
|
+
// Use a single text with styled spans instead of nested text components
|
|
59
|
+
return (
|
|
60
|
+
<text>
|
|
61
|
+
<span fg="#22c55e">[</span>
|
|
62
|
+
<span fg="#22c55e">{"=".repeat(filled)}</span>
|
|
63
|
+
<span fg="#4b5563">{"·".repeat(empty)}</span>
|
|
64
|
+
<span fg="#22c55e">]</span>
|
|
65
|
+
</text>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function ConnectingModal({ isActive, progress }: ConnectingModalProps) {
|
|
70
|
+
// Don't render if not active or progress is null
|
|
71
|
+
if (!isActive) return null;
|
|
72
|
+
|
|
73
|
+
const defaultProgress: McpConnectionProgress = progress ?? {
|
|
74
|
+
total: 0,
|
|
75
|
+
completed: 0,
|
|
76
|
+
currentServer: "Initializing...",
|
|
77
|
+
status: "connecting",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const { total, completed, currentServer, status, error } = defaultProgress;
|
|
81
|
+
const isComplete = completed >= total && total > 0;
|
|
82
|
+
|
|
83
|
+
// Format percentage
|
|
84
|
+
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<box
|
|
88
|
+
position="absolute"
|
|
89
|
+
top={0}
|
|
90
|
+
left={0}
|
|
91
|
+
width="100%"
|
|
92
|
+
height="100%"
|
|
93
|
+
backgroundColor="#00000090"
|
|
94
|
+
alignItems="center"
|
|
95
|
+
justifyContent="center"
|
|
96
|
+
>
|
|
97
|
+
<box
|
|
98
|
+
borderStyle="rounded"
|
|
99
|
+
borderColor={isComplete ? (status === "failed" ? "#f87171" : "#34d399") : "#60a5fa"}
|
|
100
|
+
backgroundColor="#1a1a2e"
|
|
101
|
+
paddingX={3}
|
|
102
|
+
paddingY={2}
|
|
103
|
+
flexDirection="column"
|
|
104
|
+
minWidth={50}
|
|
105
|
+
maxWidth={60}
|
|
106
|
+
>
|
|
107
|
+
{/* Header */}
|
|
108
|
+
<box flexDirection="row" alignItems="center" justifyContent="center" marginBottom={1}>
|
|
109
|
+
<Spinner variant="dots" />
|
|
110
|
+
<text marginLeft={1} attributes={createTextAttributes({ bold: true })} fg="#60a5fa">
|
|
111
|
+
Connecting MCP Servers
|
|
112
|
+
</text>
|
|
113
|
+
</box>
|
|
114
|
+
|
|
115
|
+
{/* Progress info */}
|
|
116
|
+
<box flexDirection="column" alignItems="center" marginBottom={1}>
|
|
117
|
+
<text fg="#9ca3af">
|
|
118
|
+
{completed} / {total} servers
|
|
119
|
+
</text>
|
|
120
|
+
<ProgressBar completed={completed} total={total} />
|
|
121
|
+
<text fg="#60a5fa" attributes={createTextAttributes({ bold: true })}>
|
|
122
|
+
{percentage}%
|
|
123
|
+
</text>
|
|
124
|
+
</box>
|
|
125
|
+
|
|
126
|
+
{/* Current server being processed */}
|
|
127
|
+
<box flexDirection="row" alignItems="center" marginBottom={1}>
|
|
128
|
+
<StatusIcon status={status} />
|
|
129
|
+
<text marginLeft={1} fg="#e5e7eb">
|
|
130
|
+
{currentServer}
|
|
131
|
+
</text>
|
|
132
|
+
</box>
|
|
133
|
+
|
|
134
|
+
{/* Error message if any */}
|
|
135
|
+
{error && (
|
|
136
|
+
<box flexDirection="column" marginTop={1}>
|
|
137
|
+
<text fg="#f87171" attributes={createTextAttributes({ bold: true })}>
|
|
138
|
+
Error:
|
|
139
|
+
</text>
|
|
140
|
+
<text fg="#fca5a5" marginLeft={1}>
|
|
141
|
+
{error}
|
|
142
|
+
</text>
|
|
143
|
+
</box>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{/* Status message */}
|
|
147
|
+
<box flexDirection="row" justifyContent="center" marginTop={1}>
|
|
148
|
+
{isComplete ? (
|
|
149
|
+
status === "failed" ? (
|
|
150
|
+
<text fg="#fbbf24">
|
|
151
|
+
Connection failed. Check settings with /mcp
|
|
152
|
+
</text>
|
|
153
|
+
) : (
|
|
154
|
+
<text fg="#34d399">
|
|
155
|
+
All servers connected!
|
|
156
|
+
</text>
|
|
157
|
+
)
|
|
158
|
+
) : (
|
|
159
|
+
<text fg="#9ca3af">
|
|
160
|
+
Please wait...
|
|
161
|
+
</text>
|
|
162
|
+
)}
|
|
163
|
+
</box>
|
|
164
|
+
</box>
|
|
165
|
+
</box>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit Pending Modal
|
|
3
|
+
*
|
|
4
|
+
* Modal for editing a queued/sheer message before it is delivered.
|
|
5
|
+
* Contains an InputBox for multi-line editing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useRef, useEffect } from "react";
|
|
9
|
+
import { useKeyboard } from "@opentui/react";
|
|
10
|
+
import { createTextAttributes, type TextareaRenderable } from "@opentui/core";
|
|
11
|
+
import { InputBox } from "./input-box.js";
|
|
12
|
+
|
|
13
|
+
interface EditPendingModalProps {
|
|
14
|
+
isActive: boolean;
|
|
15
|
+
initialText: string;
|
|
16
|
+
type: "sheer" | "queued";
|
|
17
|
+
onConfirm: (text: string) => void;
|
|
18
|
+
onCancel: () => void;
|
|
19
|
+
width?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function EditPendingModal({
|
|
23
|
+
isActive,
|
|
24
|
+
initialText,
|
|
25
|
+
type,
|
|
26
|
+
onConfirm,
|
|
27
|
+
onCancel,
|
|
28
|
+
width = 70,
|
|
29
|
+
}: EditPendingModalProps) {
|
|
30
|
+
const textareaRef = useRef<TextareaRenderable | null>(null);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (isActive && textareaRef.current) {
|
|
34
|
+
textareaRef.current.editBuffer.setText(initialText);
|
|
35
|
+
}
|
|
36
|
+
}, [isActive, initialText]);
|
|
37
|
+
|
|
38
|
+
useKeyboard((key) => {
|
|
39
|
+
if (!isActive) return;
|
|
40
|
+
if (key.name === "escape") {
|
|
41
|
+
onCancel();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!isActive) return null;
|
|
46
|
+
|
|
47
|
+
const label = type === "sheer" ? "Edit Sheer" : "Edit Queued";
|
|
48
|
+
|
|
49
|
+
const handleConfirm = () => {
|
|
50
|
+
const text = textareaRef.current?.editBuffer.getText() ?? "";
|
|
51
|
+
if (text.trim()) {
|
|
52
|
+
onConfirm(text.trim());
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<box
|
|
58
|
+
position="absolute"
|
|
59
|
+
top={0}
|
|
60
|
+
left={0}
|
|
61
|
+
width="100%"
|
|
62
|
+
height="100%"
|
|
63
|
+
backgroundColor="#00000080"
|
|
64
|
+
alignItems="center"
|
|
65
|
+
justifyContent="center"
|
|
66
|
+
>
|
|
67
|
+
<box
|
|
68
|
+
flexDirection="column"
|
|
69
|
+
alignSelf="center"
|
|
70
|
+
borderStyle="rounded"
|
|
71
|
+
borderColor="#4a4a5a"
|
|
72
|
+
backgroundColor="#1a1a2e"
|
|
73
|
+
paddingX={2}
|
|
74
|
+
paddingY={1}
|
|
75
|
+
width={width}
|
|
76
|
+
>
|
|
77
|
+
<text alignSelf="center" attributes={createTextAttributes({ bold: true })} fg="#fbbf24">
|
|
78
|
+
{label}
|
|
79
|
+
</text>
|
|
80
|
+
<box marginTop={1} marginBottom={1}>
|
|
81
|
+
<InputBox
|
|
82
|
+
onSubmit={handleConfirm}
|
|
83
|
+
focused={true}
|
|
84
|
+
disabled={false}
|
|
85
|
+
width={width - 4}
|
|
86
|
+
/>
|
|
87
|
+
</box>
|
|
88
|
+
<box alignSelf="center" flexDirection="row" gap={2}>
|
|
89
|
+
<box
|
|
90
|
+
paddingX={2}
|
|
91
|
+
backgroundColor="#2dd4bf"
|
|
92
|
+
onMouseUp={handleConfirm}
|
|
93
|
+
>
|
|
94
|
+
<text fg="white" attributes={createTextAttributes({ bold: true })}>Confirm</text>
|
|
95
|
+
</box>
|
|
96
|
+
<box paddingX={2} backgroundColor="#f43f5e" onMouseUp={onCancel}>
|
|
97
|
+
<text fg="white" attributes={createTextAttributes({ bold: true })}>Cancel</text>
|
|
98
|
+
</box>
|
|
99
|
+
</box>
|
|
100
|
+
</box>
|
|
101
|
+
</box>
|
|
102
|
+
);
|
|
103
|
+
}
|