@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.
Files changed (109) hide show
  1. package/LICENSE +34 -0
  2. package/NOTICE +35 -0
  3. package/README.md +15 -0
  4. package/bin/koi +12 -0
  5. package/dist/highlights-eq9cgrbb.scm +604 -0
  6. package/dist/highlights-ghv9g403.scm +205 -0
  7. package/dist/highlights-hk7bwhj4.scm +284 -0
  8. package/dist/highlights-r812a2qc.scm +150 -0
  9. package/dist/highlights-x6tmsnaa.scm +115 -0
  10. package/dist/injections-73j83es3.scm +27 -0
  11. package/dist/main.js +489918 -0
  12. package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  13. package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
  14. package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  15. package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  16. package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
  17. package/package.json +51 -0
  18. package/src/agent/check-permissions.ts +239 -0
  19. package/src/agent/hooks/message-utils.ts +305 -0
  20. package/src/agent/hooks/types.ts +32 -0
  21. package/src/agent/hooks.ts +1560 -0
  22. package/src/agent/mode.ts +163 -0
  23. package/src/agent/monitor-registry.ts +308 -0
  24. package/src/agent/permission-ui.ts +71 -0
  25. package/src/agent/plan-ui.ts +74 -0
  26. package/src/agent/question-ui.ts +58 -0
  27. package/src/agent/session-fork.ts +299 -0
  28. package/src/agent/session-snapshots.ts +216 -0
  29. package/src/agent/session-store.ts +649 -0
  30. package/src/agent/session-tasks.ts +305 -0
  31. package/src/agent/session.ts +27 -0
  32. package/src/agent/subagent-registry.ts +176 -0
  33. package/src/agent/subagent.ts +194 -0
  34. package/src/agent/tool-orchestration.ts +55 -0
  35. package/src/agent/tools.ts +8 -0
  36. package/src/cli/args.ts +6 -0
  37. package/src/cli/commands.ts +5 -0
  38. package/src/commands/skills/index.ts +23 -0
  39. package/src/config/models.ts +6 -0
  40. package/src/config/settings.ts +392 -0
  41. package/src/main.tsx +64 -0
  42. package/src/services/mcp/client.ts +194 -0
  43. package/src/services/mcp/config.ts +232 -0
  44. package/src/services/mcp/connection-manager.ts +258 -0
  45. package/src/services/mcp/index.ts +80 -0
  46. package/src/services/mcp/mcp-commands.ts +114 -0
  47. package/src/services/mcp/stdio-transport.ts +246 -0
  48. package/src/services/mcp/types.ts +155 -0
  49. package/src/skills/SkillsMenu.tsx +370 -0
  50. package/src/skills/bundled/batch.ts +106 -0
  51. package/src/skills/bundled/debug.ts +86 -0
  52. package/src/skills/bundled/loremIpsum.ts +101 -0
  53. package/src/skills/bundled/remember.ts +97 -0
  54. package/src/skills/bundled/simplify.ts +100 -0
  55. package/src/skills/bundled/skillify.ts +123 -0
  56. package/src/skills/bundled/stuck.ts +101 -0
  57. package/src/skills/bundled/updateConfig.ts +228 -0
  58. package/src/skills/bundled.ts +46 -0
  59. package/src/skills/frontmatter.ts +179 -0
  60. package/src/skills/index.ts +87 -0
  61. package/src/skills/invoke.ts +231 -0
  62. package/src/skills/loader.ts +710 -0
  63. package/src/skills/substitution.ts +169 -0
  64. package/src/skills/types.ts +201 -0
  65. package/src/tools/agent.ts +143 -0
  66. package/src/tools/ask-user-question.ts +46 -0
  67. package/src/tools/bash.ts +148 -0
  68. package/src/tools/edit.ts +164 -0
  69. package/src/tools/glob.ts +102 -0
  70. package/src/tools/grep.ts +248 -0
  71. package/src/tools/index.ts +73 -0
  72. package/src/tools/list-mcp-resources.ts +74 -0
  73. package/src/tools/ls.ts +85 -0
  74. package/src/tools/mcp.ts +76 -0
  75. package/src/tools/monitor.ts +159 -0
  76. package/src/tools/plan-mode.ts +134 -0
  77. package/src/tools/read-mcp-resource.ts +79 -0
  78. package/src/tools/read.ts +137 -0
  79. package/src/tools/skill.ts +176 -0
  80. package/src/tools/task.ts +349 -0
  81. package/src/tools/types.ts +52 -0
  82. package/src/tools/webfetch-domains.ts +239 -0
  83. package/src/tools/webfetch.ts +533 -0
  84. package/src/tools/write.ts +101 -0
  85. package/src/tui/app.tsx +1178 -0
  86. package/src/tui/components/chat-panel.tsx +1071 -0
  87. package/src/tui/components/command-panel.tsx +261 -0
  88. package/src/tui/components/confirm-modal.tsx +135 -0
  89. package/src/tui/components/connect-modal.tsx +435 -0
  90. package/src/tui/components/connecting-modal.tsx +167 -0
  91. package/src/tui/components/edit-pending-modal.tsx +103 -0
  92. package/src/tui/components/exit-modal.tsx +131 -0
  93. package/src/tui/components/fork-modal.tsx +377 -0
  94. package/src/tui/components/image-preview-modal.tsx +141 -0
  95. package/src/tui/components/image-utils.ts +128 -0
  96. package/src/tui/components/info-bar.tsx +103 -0
  97. package/src/tui/components/input-box.tsx +352 -0
  98. package/src/tui/components/mcp/MCPSettings.tsx +386 -0
  99. package/src/tui/components/mcp/index.ts +7 -0
  100. package/src/tui/components/model-modal.tsx +310 -0
  101. package/src/tui/components/pending-area.tsx +88 -0
  102. package/src/tui/components/rename-modal.tsx +119 -0
  103. package/src/tui/components/session-modal.tsx +233 -0
  104. package/src/tui/components/side-bar.tsx +349 -0
  105. package/src/tui/components/tool-output.ts +6 -0
  106. package/src/tui/hooks/user-prompt-history.ts +114 -0
  107. package/src/tui/theme.ts +63 -0
  108. package/src/types/commands.ts +80 -0
  109. 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
+ }