@mindstudio-ai/local-model-tunnel 0.3.4 → 0.4.1

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.
@@ -0,0 +1,2839 @@
1
+ import {
2
+ TunnelRunner,
3
+ deleteLocalInterfacePath,
4
+ detectAllProviderStatuses,
5
+ discoverAllModelsWithParameters,
6
+ getApiBaseUrl,
7
+ getApiKey,
8
+ getConfigPath,
9
+ getEditorSessions,
10
+ getEnvironment,
11
+ getLocalInterfacePath,
12
+ getLocalInterfacesDir,
13
+ getSyncedModels,
14
+ getUserId,
15
+ pollDeviceAuth,
16
+ requestDeviceAuth,
17
+ requestEvents,
18
+ setApiKey,
19
+ setLocalInterfacePath,
20
+ setUserId,
21
+ syncModels,
22
+ verifyApiKey
23
+ } from "./chunk-KLOTDVWL.js";
24
+
25
+ // src/tui/index.tsx
26
+ import { render } from "ink";
27
+ import { execFileSync, execSync as execSync2 } from "child_process";
28
+
29
+ // src/tui/App.tsx
30
+ import { useEffect as useEffect16, useCallback as useCallback10, useState as useState16, useRef as useRef9 } from "react";
31
+ import { Box as Box10, useApp, useStdout as useStdout7 } from "ink";
32
+
33
+ // src/tui/components/Header.tsx
34
+ import { useState, useEffect, useMemo } from "react";
35
+ import os from "os";
36
+ import { Box, Text } from "ink";
37
+ import chalk from "chalk";
38
+ import { createRequire } from "module";
39
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
40
+ var require2 = createRequire(import.meta.url);
41
+ var pkg = require2("../package.json");
42
+ var LogoString = ` .=+-. :++.
43
+ *@@@@@+ :%@@@@%:
44
+ .%@@@@@@#..@@@@@@@=
45
+ .*@@@@@@@--@@@@@@@#.**.
46
+ *@@@@@@@.-@@@@@@@@.#@@*
47
+ .#@@@@@@@-.@@@@@@@* #@@@@%.
48
+ =@@@@@@@-.@@@@@@@#.-@@@@@@+
49
+ :@@@@@@: +@@@@@#. .@@@@@@:
50
+ .++: .-*-. .++:`;
51
+ function useActiveShimmerLogo(active) {
52
+ const [frame, setFrame] = useState(0);
53
+ const lines = useMemo(() => LogoString.split("\n"), []);
54
+ const totalChars = useMemo(() => {
55
+ let count = 0;
56
+ for (const line of lines) {
57
+ for (const ch of line) {
58
+ if (ch !== " " && ch !== " ") count++;
59
+ }
60
+ }
61
+ return count;
62
+ }, [lines]);
63
+ useEffect(() => {
64
+ if (!active) return;
65
+ const interval = setInterval(() => {
66
+ setFrame((f) => f + 1);
67
+ }, 25);
68
+ return () => clearInterval(interval);
69
+ }, [active]);
70
+ return useMemo(() => {
71
+ if (!active) return null;
72
+ const waveLength = 20;
73
+ let charIdx = 0;
74
+ return lines.map((line) => {
75
+ let result = "";
76
+ for (let i = 0; i < line.length; i++) {
77
+ const ch = line[i];
78
+ if (ch === " " || ch === " ") {
79
+ result += ch;
80
+ continue;
81
+ }
82
+ const phase = (charIdx - frame * 0.5) / waveLength * Math.PI * 2;
83
+ const brightness = 0.65 + 0.35 * Math.sin(phase);
84
+ if (brightness >= 0.85) {
85
+ result += chalk.cyanBright(ch);
86
+ } else if (brightness >= 0.65) {
87
+ result += chalk.cyan(ch);
88
+ } else {
89
+ result += chalk.rgb(0, 140, 160)(ch);
90
+ }
91
+ charIdx++;
92
+ }
93
+ return result;
94
+ }).join("\n");
95
+ }, [active, frame, lines, totalChars]);
96
+ }
97
+ var getConnectionDisplay = (status) => {
98
+ switch (status) {
99
+ case "connected":
100
+ return { color: "green", text: "Connected to Cloud" };
101
+ case "connecting":
102
+ return { color: "yellow", text: "Connecting..." };
103
+ case "not_authenticated":
104
+ return { color: "yellow", text: "Not Authenticated" };
105
+ case "disconnected":
106
+ return { color: "red", text: "Disconnected" };
107
+ default:
108
+ return { color: "red", text: "Error" };
109
+ }
110
+ };
111
+ function Header({
112
+ connection,
113
+ environment,
114
+ configPath,
115
+ connectionError,
116
+ compact,
117
+ hasActiveRequest
118
+ }) {
119
+ const { color: connectionColor, text: connectionText } = getConnectionDisplay(connection);
120
+ const shimmerLogo = useActiveShimmerLogo(!compact && !!hasActiveRequest);
121
+ return /* @__PURE__ */ jsxs(
122
+ Box,
123
+ {
124
+ flexDirection: "row",
125
+ alignItems: "center",
126
+ borderStyle: "round",
127
+ borderColor: "cyan",
128
+ paddingX: 1,
129
+ paddingY: 1,
130
+ width: "100%",
131
+ children: [
132
+ !compact && /* @__PURE__ */ jsx(Box, { paddingLeft: 3, children: shimmerLogo ? /* @__PURE__ */ jsx(Text, { children: shimmerLogo }) : /* @__PURE__ */ jsx(Text, { color: "cyan", children: LogoString }) }),
133
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: compact ? 0 : 4, children: [
134
+ /* @__PURE__ */ jsxs(Box, { children: [
135
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "white", children: "MindStudio Local Tunnel" }),
136
+ compact && /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
137
+ " v",
138
+ pkg.version
139
+ ] }),
140
+ environment !== "prod" && /* @__PURE__ */ jsxs(Fragment, { children: [
141
+ /* @__PURE__ */ jsx(Text, { children: " " }),
142
+ /* @__PURE__ */ jsx(Text, { color: "yellow", bold: true, children: "[LOCAL]" })
143
+ ] })
144
+ ] }),
145
+ /* @__PURE__ */ jsxs(Text, { color: connectionColor, children: [
146
+ "\u25CF ",
147
+ connectionText
148
+ ] }),
149
+ connectionError && /* @__PURE__ */ jsx(Text, { color: "red", children: connectionError }),
150
+ /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
151
+ "Config: ",
152
+ configPath.replace(os.homedir(), "~")
153
+ ] }),
154
+ !compact && /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
155
+ "v",
156
+ pkg.version
157
+ ] })
158
+ ] })
159
+ ]
160
+ }
161
+ );
162
+ }
163
+
164
+ // src/tui/components/NavigationMenu.tsx
165
+ import { useState as useState2, useEffect as useEffect2 } from "react";
166
+ import { Box as Box2, Text as Text2, useInput, useStdout } from "ink";
167
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
168
+ function NavigationMenu({
169
+ items,
170
+ onSelect,
171
+ title
172
+ }) {
173
+ const { stdout } = useStdout();
174
+ const compact = (stdout?.rows ?? 24) < 40;
175
+ const getDefaultIndex = () => {
176
+ const firstIdx = items.findIndex((i) => !i.disabled && !i.isSeparator);
177
+ return firstIdx >= 0 ? firstIdx : 0;
178
+ };
179
+ const [selectedIndex, setSelectedIndex] = useState2(getDefaultIndex);
180
+ useEffect2(() => {
181
+ setSelectedIndex(getDefaultIndex());
182
+ }, [items]);
183
+ const selectableItems = items.filter((i) => !i.isSeparator);
184
+ const findNextEnabled = (from, direction) => {
185
+ let idx = from;
186
+ for (let i = 0; i < items.length; i++) {
187
+ idx = (idx + direction + items.length) % items.length;
188
+ if (!items[idx].disabled && !items[idx].isSeparator) return idx;
189
+ }
190
+ return from;
191
+ };
192
+ useInput((input, key) => {
193
+ if (input === "q" || key.escape) {
194
+ const backItem = items.find((i) => i.id === "back");
195
+ if (backItem) {
196
+ onSelect("back");
197
+ } else if (input === "q") {
198
+ onSelect("quit");
199
+ }
200
+ return;
201
+ }
202
+ if (key.upArrow || compact && key.leftArrow) {
203
+ setSelectedIndex((prev) => findNextEnabled(prev, -1));
204
+ } else if (key.downArrow || compact && key.rightArrow) {
205
+ setSelectedIndex((prev) => findNextEnabled(prev, 1));
206
+ } else if (key.return) {
207
+ const item = items[selectedIndex];
208
+ if (item && !item.disabled) {
209
+ onSelect(item.id);
210
+ }
211
+ }
212
+ });
213
+ const hasBack = items.some((i) => i.id === "back");
214
+ if (compact) {
215
+ const selectedItem = items[selectedIndex];
216
+ return /* @__PURE__ */ jsx2(
217
+ Box2,
218
+ {
219
+ flexDirection: "column",
220
+ paddingX: 1,
221
+ borderStyle: "single",
222
+ borderTop: true,
223
+ borderBottom: false,
224
+ borderLeft: false,
225
+ borderRight: false,
226
+ borderColor: "gray",
227
+ children: /* @__PURE__ */ jsx2(Box2, { height: 1, overflow: "hidden", gap: 1, children: items.map((item, index) => {
228
+ if (item.isSeparator) return null;
229
+ const isSelected = index === selectedIndex;
230
+ return /* @__PURE__ */ jsx2(
231
+ Text2,
232
+ {
233
+ color: item.disabled ? "gray" : isSelected ? "cyan" : "white",
234
+ bold: isSelected,
235
+ wrap: "truncate-end",
236
+ children: isSelected ? `\u276F ${item.label}` : ` ${item.label}`
237
+ },
238
+ item.id
239
+ );
240
+ }) })
241
+ }
242
+ );
243
+ }
244
+ const separatorExtraLines = items.filter(
245
+ (item, idx) => item.isSeparator && idx > 0
246
+ ).length;
247
+ const menuHeight = items.length + 4 + separatorExtraLines;
248
+ return /* @__PURE__ */ jsxs2(
249
+ Box2,
250
+ {
251
+ flexDirection: "column",
252
+ paddingX: 1,
253
+ marginBottom: 1,
254
+ borderStyle: "single",
255
+ borderTop: true,
256
+ borderBottom: false,
257
+ borderLeft: false,
258
+ borderRight: false,
259
+ borderColor: "gray",
260
+ children: [
261
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "gray", children: title ?? "Actions" }) }),
262
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: items.map((item, index) => {
263
+ if (item.isSeparator) {
264
+ return /* @__PURE__ */ jsx2(Box2, { marginTop: index > 0 ? 1 : 0, children: item.label ? /* @__PURE__ */ jsx2(Text2, { bold: true, color: item.color ?? "gray", wrap: "truncate-end", children: item.label }) : null }, item.id);
265
+ }
266
+ const isSelected = index === selectedIndex;
267
+ const prefix = isSelected ? "\u276F" : " ";
268
+ if (item.disabled) {
269
+ return /* @__PURE__ */ jsx2(Box2, { height: 1, overflow: "hidden", children: /* @__PURE__ */ jsxs2(Text2, { color: "gray", wrap: "truncate-end", children: [
270
+ prefix,
271
+ " ",
272
+ item.label,
273
+ item.disabledReason ? ` (${item.disabledReason})` : ""
274
+ ] }) }, item.id);
275
+ }
276
+ return /* @__PURE__ */ jsxs2(Box2, { height: 1, overflow: "hidden", children: [
277
+ /* @__PURE__ */ jsxs2(
278
+ Text2,
279
+ {
280
+ color: isSelected ? "cyan" : "white",
281
+ bold: isSelected,
282
+ wrap: "truncate-end",
283
+ children: [
284
+ prefix,
285
+ " ",
286
+ item.label
287
+ ]
288
+ }
289
+ ),
290
+ isSelected && /* @__PURE__ */ jsxs2(Text2, { color: "gray", wrap: "truncate-end", children: [
291
+ " ",
292
+ "- ",
293
+ item.description
294
+ ] })
295
+ ] }, item.id);
296
+ }) }),
297
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, height: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "gray", wrap: "truncate-end", children: hasBack ? "Up/Down Navigate \u2022 Enter Select \u2022 q/Esc Back" : "Up/Down Navigate \u2022 Enter Select \u2022 q Quit" }) })
298
+ ]
299
+ }
300
+ );
301
+ }
302
+
303
+ // src/tui/hooks/useConnection.ts
304
+ import { useState as useState3, useEffect as useEffect3, useCallback } from "react";
305
+ function useConnection() {
306
+ const [status, setStatus] = useState3("connecting");
307
+ const [error, setError] = useState3(null);
308
+ const environment = getEnvironment();
309
+ const connect = useCallback(async () => {
310
+ setStatus("connecting");
311
+ setError(null);
312
+ const apiKey = getApiKey();
313
+ const userId = getUserId();
314
+ if (!apiKey || !userId) {
315
+ setStatus("not_authenticated");
316
+ return;
317
+ }
318
+ try {
319
+ const isValid = await verifyApiKey();
320
+ if (isValid) {
321
+ setStatus("connected");
322
+ } else {
323
+ setStatus("not_authenticated");
324
+ }
325
+ } catch (err) {
326
+ setStatus("error");
327
+ setError(err instanceof Error ? err.message : "Connection failed");
328
+ }
329
+ }, []);
330
+ useEffect3(() => {
331
+ connect();
332
+ }, [connect]);
333
+ return {
334
+ status,
335
+ environment,
336
+ error,
337
+ retry: connect
338
+ };
339
+ }
340
+
341
+ // src/tui/interfaces/hooks/useEditorSessions.ts
342
+ import { useState as useState4, useEffect as useEffect4, useCallback as useCallback2, useRef } from "react";
343
+ function useEditorSessions() {
344
+ const [sessions, setSessions] = useState4([]);
345
+ const [loading, setLoading] = useState4(true);
346
+ const [error, setError] = useState4(null);
347
+ const [refreshStatus, setRefreshStatus] = useState4("idle");
348
+ const initialLoadDone = useRef(false);
349
+ const timerRef = useRef();
350
+ const pollRef = useRef();
351
+ const refresh = useCallback2(async () => {
352
+ if (!initialLoadDone.current) {
353
+ setLoading(true);
354
+ } else {
355
+ setRefreshStatus("refreshing");
356
+ }
357
+ setError(null);
358
+ try {
359
+ const data = await getEditorSessions();
360
+ setSessions(data);
361
+ initialLoadDone.current = true;
362
+ setRefreshStatus("refreshed");
363
+ clearTimeout(timerRef.current);
364
+ timerRef.current = setTimeout(() => setRefreshStatus("idle"), 1500);
365
+ } catch (err) {
366
+ setError(err instanceof Error ? err.message : "Failed to fetch sessions");
367
+ setRefreshStatus("idle");
368
+ } finally {
369
+ setLoading(false);
370
+ }
371
+ }, []);
372
+ useEffect4(() => {
373
+ refresh();
374
+ }, [refresh]);
375
+ useEffect4(() => {
376
+ pollRef.current = setInterval(async () => {
377
+ if (!initialLoadDone.current) return;
378
+ try {
379
+ const data = await getEditorSessions();
380
+ setSessions(data);
381
+ } catch {
382
+ }
383
+ }, 5e3);
384
+ return () => clearInterval(pollRef.current);
385
+ }, []);
386
+ useEffect4(() => {
387
+ return () => clearTimeout(timerRef.current);
388
+ }, []);
389
+ return { sessions, loading, error, refreshStatus, refresh };
390
+ }
391
+
392
+ // src/tui/models/hooks/useSetupProviders.ts
393
+ import { useState as useState5, useEffect as useEffect5, useCallback as useCallback3, useRef as useRef2 } from "react";
394
+ function useSetupProviders() {
395
+ const [providers, setProviders] = useState5([]);
396
+ const [loading, setLoading] = useState5(true);
397
+ const [refreshing, setRefreshing] = useState5(false);
398
+ const initialLoadDone = useRef2(false);
399
+ const refresh = useCallback3(async () => {
400
+ if (!initialLoadDone.current) {
401
+ setLoading(true);
402
+ } else {
403
+ setRefreshing(true);
404
+ }
405
+ const statuses = await detectAllProviderStatuses();
406
+ setProviders(statuses);
407
+ initialLoadDone.current = true;
408
+ setLoading(false);
409
+ setRefreshing(false);
410
+ }, []);
411
+ useEffect5(() => {
412
+ refresh();
413
+ }, [refresh]);
414
+ return { providers, loading, refreshing, refresh };
415
+ }
416
+
417
+ // src/tui/models/hooks/useModels.ts
418
+ import { useState as useState6, useEffect as useEffect6, useCallback as useCallback4, useRef as useRef3 } from "react";
419
+ function useModels() {
420
+ const [models, setModels] = useState6([]);
421
+ const [warnings, setWarnings] = useState6([]);
422
+ const [loading, setLoading] = useState6(true);
423
+ const [refreshing, setRefreshing] = useState6(false);
424
+ const initialLoadDone = useRef3(false);
425
+ const refresh = useCallback4(async () => {
426
+ if (!initialLoadDone.current) {
427
+ setLoading(true);
428
+ } else {
429
+ setRefreshing(true);
430
+ }
431
+ try {
432
+ const discoveredModels = await discoverAllModelsWithParameters();
433
+ setModels(discoveredModels.filter((m) => !m.statusHint));
434
+ setWarnings(discoveredModels.filter((m) => !!m.statusHint));
435
+ initialLoadDone.current = true;
436
+ return discoveredModels;
437
+ } catch {
438
+ return [];
439
+ } finally {
440
+ setLoading(false);
441
+ setRefreshing(false);
442
+ }
443
+ }, []);
444
+ useEffect6(() => {
445
+ refresh();
446
+ }, [refresh]);
447
+ return {
448
+ models,
449
+ warnings,
450
+ loading,
451
+ refreshing,
452
+ refresh
453
+ };
454
+ }
455
+
456
+ // src/tui/models/hooks/useRequests.ts
457
+ import { useState as useState7, useEffect as useEffect7, useCallback as useCallback5, useRef as useRef4 } from "react";
458
+ function useRequests(maxHistory = 50) {
459
+ const [requests, setRequests] = useState7([]);
460
+ const requestsRef = useRef4(/* @__PURE__ */ new Map());
461
+ useEffect7(() => {
462
+ const interval = setInterval(() => {
463
+ setRequests((prev) => {
464
+ const hasActive = prev.some((r) => r.status === "processing");
465
+ return hasActive ? [...prev] : prev;
466
+ });
467
+ }, 1e3);
468
+ return () => clearInterval(interval);
469
+ }, []);
470
+ useEffect7(() => {
471
+ const unsubStart = requestEvents.onStart((event) => {
472
+ const entry = {
473
+ id: event.id,
474
+ modelId: event.modelId,
475
+ requestType: event.requestType,
476
+ status: "processing",
477
+ startTime: event.timestamp
478
+ };
479
+ requestsRef.current.set(event.id, entry);
480
+ setRequests((prev) => [...prev, entry].slice(-maxHistory));
481
+ });
482
+ const unsubProgress = requestEvents.onProgress((event) => {
483
+ const existing = requestsRef.current.get(event.id);
484
+ if (existing && existing.status === "processing") {
485
+ const updated = {
486
+ ...existing,
487
+ ...event.content !== void 0 && { content: event.content },
488
+ ...event.step !== void 0 && { step: event.step },
489
+ ...event.totalSteps !== void 0 && {
490
+ totalSteps: event.totalSteps
491
+ }
492
+ };
493
+ requestsRef.current.set(event.id, updated);
494
+ setRequests(
495
+ (prev) => prev.map((r) => r.id === event.id ? updated : r)
496
+ );
497
+ }
498
+ });
499
+ const unsubComplete = requestEvents.onComplete((event) => {
500
+ const existing = requestsRef.current.get(event.id);
501
+ if (existing) {
502
+ const updated = {
503
+ ...existing,
504
+ status: event.success ? "completed" : "failed",
505
+ endTime: Date.now(),
506
+ duration: event.duration,
507
+ result: event.result,
508
+ error: event.error
509
+ };
510
+ requestsRef.current.set(event.id, updated);
511
+ setRequests(
512
+ (prev) => prev.map((r) => r.id === event.id ? updated : r)
513
+ );
514
+ }
515
+ });
516
+ return () => {
517
+ unsubStart();
518
+ unsubProgress();
519
+ unsubComplete();
520
+ };
521
+ }, [maxHistory]);
522
+ const activeCount = requests.filter((r) => r.status === "processing").length;
523
+ const clear = useCallback5(() => {
524
+ requestsRef.current.clear();
525
+ setRequests([]);
526
+ }, []);
527
+ return {
528
+ requests,
529
+ activeCount,
530
+ clear
531
+ };
532
+ }
533
+
534
+ // src/tui/models/hooks/useRegisteredModels.ts
535
+ import { useState as useState8, useEffect as useEffect8, useCallback as useCallback6 } from "react";
536
+ function useSyncedModels(connectionStatus) {
537
+ const [syncedNames, setSyncedNames] = useState8(/* @__PURE__ */ new Set());
538
+ const [syncedModels, setSyncedModels] = useState8([]);
539
+ const refresh = useCallback6(async () => {
540
+ if (connectionStatus !== "connected") {
541
+ setSyncedNames(/* @__PURE__ */ new Set());
542
+ setSyncedModels([]);
543
+ return;
544
+ }
545
+ try {
546
+ const models = await getSyncedModels();
547
+ setSyncedNames(new Set(models.map((m) => m.name)));
548
+ setSyncedModels(models);
549
+ } catch {
550
+ }
551
+ }, [connectionStatus]);
552
+ useEffect8(() => {
553
+ refresh();
554
+ }, [refresh]);
555
+ return {
556
+ syncedNames,
557
+ syncedModels,
558
+ refresh
559
+ };
560
+ }
561
+
562
+ // src/tui/models/pages/DashboardPage.tsx
563
+ import { useMemo as useMemo2 } from "react";
564
+ import { Box as Box4, Text as Text4, useStdout as useStdout3 } from "ink";
565
+ import Spinner2 from "ink-spinner";
566
+
567
+ // src/tui/models/components/RequestLog.tsx
568
+ import { Box as Box3, Text as Text3, useStdout as useStdout2 } from "ink";
569
+ import Spinner from "ink-spinner";
570
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
571
+ function formatTime(timestamp) {
572
+ const date = new Date(timestamp);
573
+ return date.toLocaleTimeString("en-US", {
574
+ hour12: false,
575
+ hour: "2-digit",
576
+ minute: "2-digit",
577
+ second: "2-digit"
578
+ });
579
+ }
580
+ function formatDuration(ms) {
581
+ if (ms < 1e3) return `${ms}ms`;
582
+ return `${(ms / 1e3).toFixed(1)}s`;
583
+ }
584
+ function getRequestTypeLabel(type) {
585
+ switch (type) {
586
+ case "llm_chat":
587
+ return { label: "text", color: "gray" };
588
+ case "image_generation":
589
+ return { label: "image", color: "gray" };
590
+ case "video_generation":
591
+ return { label: "video", color: "gray" };
592
+ default:
593
+ return { label: type, color: "gray" };
594
+ }
595
+ }
596
+ function snippetLine(content, maxWidth) {
597
+ const flat = content.replace(/\s+/g, " ").trim();
598
+ if (flat.length <= maxWidth) return flat;
599
+ return "\u2026" + flat.slice(-(maxWidth - 1));
600
+ }
601
+ function RequestItem({
602
+ request,
603
+ width
604
+ }) {
605
+ const time = formatTime(request.startTime);
606
+ const typeLabel = getRequestTypeLabel(request.requestType);
607
+ const snippetIndent = " ";
608
+ const snippetWidth = width - snippetIndent.length - 2;
609
+ if (request.status === "processing") {
610
+ const elapsed = Date.now() - request.startTime;
611
+ const snippet = request.content && request.requestType === "llm_chat" ? snippetLine(request.content, snippetWidth) : null;
612
+ const stepProgress = request.step !== void 0 && request.totalSteps ? `Step ${request.step}/${request.totalSteps}` : null;
613
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
614
+ /* @__PURE__ */ jsxs3(Box3, { children: [
615
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: /* @__PURE__ */ jsx3(Spinner, { type: "dots" }) }),
616
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
617
+ " ",
618
+ time,
619
+ " "
620
+ ] }),
621
+ /* @__PURE__ */ jsx3(Text3, { color: "white", children: request.modelId }),
622
+ /* @__PURE__ */ jsx3(Text3, { color: "gray", children: " " }),
623
+ /* @__PURE__ */ jsx3(Text3, { color: typeLabel.color, children: typeLabel.label }),
624
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
625
+ " ",
626
+ formatDuration(elapsed),
627
+ "..."
628
+ ] })
629
+ ] }),
630
+ snippet && /* @__PURE__ */ jsxs3(Text3, { color: "gray", wrap: "truncate-end", children: [
631
+ snippetIndent,
632
+ snippet
633
+ ] }),
634
+ stepProgress && /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
635
+ snippetIndent,
636
+ stepProgress
637
+ ] })
638
+ ] });
639
+ }
640
+ if (request.status === "completed") {
641
+ const duration = request.duration ? formatDuration(request.duration) : "";
642
+ let resultInfo = "";
643
+ if (request.result?.chars) {
644
+ resultInfo = ` \xB7 ${request.result.chars} chars`;
645
+ } else if (request.result?.imageSize) {
646
+ resultInfo = ` \xB7 ${Math.round(request.result.imageSize / 1024)}KB`;
647
+ } else if (request.result?.videoSize) {
648
+ resultInfo = ` \xB7 ${Math.round(request.result.videoSize / 1024 / 1024)}MB`;
649
+ }
650
+ const snippet = request.content && request.requestType === "llm_chat" ? snippetLine(request.content, snippetWidth) : null;
651
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
652
+ /* @__PURE__ */ jsxs3(Box3, { children: [
653
+ /* @__PURE__ */ jsx3(Text3, { color: "green", children: "\u2713" }),
654
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
655
+ " ",
656
+ time,
657
+ " "
658
+ ] }),
659
+ /* @__PURE__ */ jsx3(Text3, { color: "white", children: request.modelId }),
660
+ /* @__PURE__ */ jsx3(Text3, { color: "gray", children: " " }),
661
+ /* @__PURE__ */ jsx3(Text3, { color: typeLabel.color, children: typeLabel.label }),
662
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
663
+ " ",
664
+ duration,
665
+ resultInfo
666
+ ] })
667
+ ] }),
668
+ snippet && /* @__PURE__ */ jsxs3(Text3, { color: "gray", wrap: "truncate-end", children: [
669
+ snippetIndent,
670
+ snippet
671
+ ] })
672
+ ] });
673
+ }
674
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: /* @__PURE__ */ jsxs3(Box3, { children: [
675
+ /* @__PURE__ */ jsx3(Text3, { color: "red", children: "\u25CF" }),
676
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
677
+ " ",
678
+ time,
679
+ " "
680
+ ] }),
681
+ /* @__PURE__ */ jsx3(Text3, { color: "white", children: request.modelId }),
682
+ /* @__PURE__ */ jsx3(Text3, { color: "gray", children: " " }),
683
+ /* @__PURE__ */ jsx3(Text3, { color: typeLabel.color, children: typeLabel.label }),
684
+ /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
685
+ " ",
686
+ request.error || "Failed"
687
+ ] })
688
+ ] }) });
689
+ }
690
+ function RequestLog({
691
+ requests,
692
+ maxVisible = 8,
693
+ hasModels = true
694
+ }) {
695
+ const { stdout } = useStdout2();
696
+ const width = stdout?.columns ?? 80;
697
+ const activeRequests = requests.filter((r) => r.status === "processing");
698
+ const completedRequests = requests.filter((r) => r.status !== "processing");
699
+ const itemLines = (r) => {
700
+ if (r.requestType === "llm_chat" && r.content) return 2;
701
+ if (r.status === "processing" && r.step !== void 0) return 2;
702
+ return 1;
703
+ };
704
+ let completedToShow = [];
705
+ let linesUsed = activeRequests.reduce((sum, r) => sum + itemLines(r), 0);
706
+ for (let i = completedRequests.length - 1; i >= 0 && linesUsed < maxVisible; i--) {
707
+ const r = completedRequests[i];
708
+ const lines = itemLines(r);
709
+ if (linesUsed + lines <= maxVisible) {
710
+ completedToShow.unshift(r);
711
+ linesUsed += lines;
712
+ } else {
713
+ break;
714
+ }
715
+ }
716
+ const visibleRequests = [...completedToShow, ...activeRequests];
717
+ return /* @__PURE__ */ jsxs3(
718
+ Box3,
719
+ {
720
+ flexDirection: "column",
721
+ flexGrow: 1,
722
+ width: "100%",
723
+ paddingX: 1,
724
+ marginTop: 1,
725
+ children: [
726
+ /* @__PURE__ */ jsxs3(Box3, { children: [
727
+ /* @__PURE__ */ jsx3(Text3, { bold: true, underline: true, color: "white", children: "Generation Requests" }),
728
+ activeRequests.length > 0 && /* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
729
+ " (",
730
+ activeRequests.length,
731
+ " active)"
732
+ ] })
733
+ ] }),
734
+ requests.length === 0 ? /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx3(Text3, { color: "gray", children: hasModels ? "Tunnel is live \u2014 requests will appear here when models are used in MindStudio" : "Start a model to begin receiving generation requests." }) }) : /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginTop: 1, children: visibleRequests.map((request) => /* @__PURE__ */ jsx3(RequestItem, { request, width }, request.id)) })
735
+ ]
736
+ }
737
+ );
738
+ }
739
+
740
+ // src/tui/models/pages/DashboardPage.tsx
741
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
742
+ function getWorkflowCount(model) {
743
+ const param = model.parameters?.find((p) => p.type === "comfyWorkflow");
744
+ if (!param) return null;
745
+ return param.comfyWorkflowOptions.availableWorkflows.length;
746
+ }
747
+ function getCapabilityLabel(capability) {
748
+ switch (capability) {
749
+ case "text":
750
+ return { label: "Text Generation", color: "gray" };
751
+ case "image":
752
+ return { label: "Image Generation", color: "gray" };
753
+ case "video":
754
+ return { label: "Video Generation", color: "gray" };
755
+ default:
756
+ return { label: capability, color: "gray" };
757
+ }
758
+ }
759
+ function DashboardPage({
760
+ requests,
761
+ models,
762
+ modelWarnings = [],
763
+ providers,
764
+ providersLoading,
765
+ syncedNames,
766
+ modelsLoading,
767
+ syncStatus = "idle",
768
+ editorSessions,
769
+ editorsLoading,
770
+ onNavigate
771
+ }) {
772
+ const { stdout } = useStdout3();
773
+ const installedProviders = providers.filter(({ status }) => status.installed);
774
+ const provNameWidth = Math.max(
775
+ ...installedProviders.map((p) => p.provider.displayName.length),
776
+ 8
777
+ );
778
+ const provStatusWidth = "Local Server Running".length;
779
+ const allModelNames = new Set(models.map((m) => m.name));
780
+ const unavailableSynced = [...syncedNames].filter(
781
+ (name) => !allModelNames.has(name)
782
+ );
783
+ const syncDescription = syncStatus === "syncing" ? "Syncing..." : syncStatus === "synced" ? "\u2713 Synced" : "Re-detect providers and sync models to MindStudio";
784
+ const menuItems = useMemo2(() => {
785
+ return [
786
+ {
787
+ id: "interfaces",
788
+ label: "Connect to Agent",
789
+ description: "Connect your local editor to a MindStudio interface or script"
790
+ },
791
+ {
792
+ id: "refresh",
793
+ label: "Sync Models",
794
+ description: syncDescription
795
+ },
796
+ {
797
+ id: "setup",
798
+ label: "Manage Providers",
799
+ description: "Manage local AI providers"
800
+ },
801
+ {
802
+ id: "auth",
803
+ label: "Re-authenticate",
804
+ description: "Re-authenticate with MindStudio"
805
+ },
806
+ {
807
+ id: "quit",
808
+ label: "Exit",
809
+ description: "Quit the application"
810
+ }
811
+ ];
812
+ }, [syncDescription]);
813
+ const termHeight = (stdout?.rows ?? 24) - 4;
814
+ const compactHeader = (stdout?.rows ?? 24) <= 45 || (stdout?.columns ?? 80) <= 90;
815
+ const headerLines = compactHeader ? 7 : 14;
816
+ const providerContentLines = providersLoading ? 1 : installedProviders.length === 0 ? 2 : installedProviders.length;
817
+ const providersLines = 3 + providerContentLines;
818
+ const editorsContentLines = editorsLoading ? 1 : editorSessions.length === 0 ? 1 : editorSessions.length;
819
+ const editorsLines = 3 + editorsContentLines;
820
+ const modelContentLines = modelsLoading ? 1 : models.length === 0 && unavailableSynced.length === 0 && modelWarnings.length === 0 ? 2 : models.length + modelWarnings.length + (unavailableSynced.length > 0 ? 1 + unavailableSynced.length : 0);
821
+ const modelsLines = 3 + modelContentLines;
822
+ const requestLogOverhead = 3;
823
+ const compactMenu = termHeight + 4 < 40;
824
+ const menuLines = compactMenu ? 2 : menuItems.length + 6;
825
+ const usedLines = headerLines + providersLines + editorsLines + modelsLines + requestLogOverhead + menuLines;
826
+ const maxVisible = Math.max(3, termHeight - usedLines);
827
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", flexGrow: 1, children: [
828
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [
829
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "white", underline: true, children: "Open Agents" }),
830
+ editorsLoading ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
831
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }) }),
832
+ /* @__PURE__ */ jsx4(Text4, { children: " Loading editor sessions..." })
833
+ ] }) : editorSessions.length === 0 ? /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "Agents you are editing in the MindStudio IDE will appear here." }) }) : /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginTop: 1, children: editorSessions.map((session) => /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { color: "white", children: session.appName }) }, session.appId)) })
834
+ ] }),
835
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [
836
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "white", underline: true, children: "AI Model Providers" }),
837
+ providersLoading ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
838
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }) }),
839
+ /* @__PURE__ */ jsx4(Text4, { children: " Detecting providers..." })
840
+ ] }) : installedProviders.length === 0 ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
841
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "No providers installed." }),
842
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: 'Use "Manage Providers" below to install one.' })
843
+ ] }) : /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginTop: 1, children: installedProviders.map(({ provider, status }) => {
844
+ const url = provider.baseUrl;
845
+ const statusColor = status.running ? "green" : "yellow";
846
+ const statusText = status.running ? "Local Server Running" : "Installed (not running)";
847
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
848
+ /* @__PURE__ */ jsx4(Text4, { color: "white", children: provider.displayName.padEnd(provNameWidth + 2) }),
849
+ /* @__PURE__ */ jsx4(Text4, { color: statusColor, children: statusText.padEnd(provStatusWidth + 2) }),
850
+ status.running && /* @__PURE__ */ jsx4(Text4, { color: "gray", children: url })
851
+ ] }, provider.name);
852
+ }) })
853
+ ] }),
854
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [
855
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "white", underline: true, children: "Local Models" }),
856
+ modelsLoading ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
857
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }) }),
858
+ /* @__PURE__ */ jsx4(Text4, { children: " Discovering models..." })
859
+ ] }) : models.length === 0 && unavailableSynced.length === 0 && modelWarnings.length === 0 ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
860
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "No models found." }),
861
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "Download models using your provider (e.g., ollama pull llama3.2)" })
862
+ ] }) : /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
863
+ models.map((model) => {
864
+ const cap = getCapabilityLabel(model.capability);
865
+ const isSynced = syncedNames.has(model.name);
866
+ const displayProvider = providers.find((p) => p.provider.name === model.provider)?.provider.displayName ?? model.provider;
867
+ const workflowCount = getWorkflowCount(model);
868
+ const workflowSuffix = workflowCount !== null ? ` (${workflowCount} workflow${workflowCount !== 1 ? "s" : ""}, ${isSynced ? workflowCount : 0} synced)` : "";
869
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
870
+ /* @__PURE__ */ jsx4(Text4, { color: isSynced ? "green" : "gray", children: isSynced ? "\u25CF" : "\u25CB" }),
871
+ /* @__PURE__ */ jsx4(Text4, { color: "white", children: ` ${model.name}` }),
872
+ workflowSuffix && /* @__PURE__ */ jsx4(Text4, { color: "gray", children: workflowSuffix }),
873
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " - " }),
874
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: displayProvider }),
875
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " - " }),
876
+ /* @__PURE__ */ jsx4(Text4, { color: cap.color, children: cap.label })
877
+ ] }, `${model.provider}:${model.name}`);
878
+ }),
879
+ modelWarnings.map((warning) => {
880
+ const displayProvider = providers.find((p) => p.provider.name === warning.provider)?.provider.displayName ?? warning.provider;
881
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
882
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "\u25CB" }),
883
+ /* @__PURE__ */ jsx4(Text4, { color: "white", children: ` ${warning.name}` }),
884
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " - " }),
885
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: displayProvider }),
886
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " - " }),
887
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: warning.statusHint })
888
+ ] }, `${warning.provider}:${warning.name}`);
889
+ }),
890
+ unavailableSynced.length > 0 && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: models.length > 0 ? 1 : 0, children: [
891
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "Synced but provider not running:" }),
892
+ unavailableSynced.map((name) => /* @__PURE__ */ jsxs4(Box4, { children: [
893
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "\u25CB" }),
894
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: ` ${name}` })
895
+ ] }, name))
896
+ ] })
897
+ ] })
898
+ ] }),
899
+ /* @__PURE__ */ jsx4(
900
+ RequestLog,
901
+ {
902
+ requests,
903
+ maxVisible,
904
+ hasModels: models.length > 0
905
+ }
906
+ ),
907
+ /* @__PURE__ */ jsx4(NavigationMenu, { items: menuItems, onSelect: onNavigate })
908
+ ] });
909
+ }
910
+
911
+ // src/tui/models/pages/SetupPage.tsx
912
+ import { useState as useState9, useMemo as useMemo4, useEffect as useEffect9 } from "react";
913
+ import { Box as Box5, Text as Text6, useInput as useInput2, useStdout as useStdout5 } from "ink";
914
+ import Spinner3 from "ink-spinner";
915
+
916
+ // src/tui/components/MarkdownText.tsx
917
+ import { useMemo as useMemo3 } from "react";
918
+ import { Text as Text5, useStdout as useStdout4 } from "ink";
919
+ import chalk2 from "chalk";
920
+ import { marked } from "marked";
921
+ import { markedTerminal } from "marked-terminal";
922
+ import { jsx as jsx5 } from "react/jsx-runtime";
923
+ var codeStyle = chalk2.cyan;
924
+ var identity = (s) => s;
925
+ function renderMarkdown(content, width) {
926
+ marked.use(
927
+ markedTerminal({
928
+ width,
929
+ codespan: codeStyle,
930
+ link: identity,
931
+ href: identity
932
+ })
933
+ );
934
+ marked.use({
935
+ renderer: {
936
+ code({ text }) {
937
+ const lines = text.trim().split("\n").map((l) => " " + codeStyle(l)).join("\n");
938
+ return lines + "\n\n";
939
+ },
940
+ link({ href, text }) {
941
+ if (text && text !== href) {
942
+ return `${text} (${href})`;
943
+ }
944
+ return href;
945
+ }
946
+ }
947
+ });
948
+ return marked.parse(content).trimEnd();
949
+ }
950
+
951
+ // src/tui/models/pages/SetupPage.tsx
952
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
953
+ function ProviderDetailView({
954
+ provider,
955
+ onBack
956
+ }) {
957
+ const [scrollOffset, setScrollOffset] = useState9(0);
958
+ const { stdout } = useStdout5();
959
+ const termHeight = (stdout?.rows ?? 24) - 4;
960
+ const headerHeight = 14;
961
+ const footerLines = 6;
962
+ const contentPadding = 2;
963
+ const viewHeight = termHeight - headerHeight - footerLines - contentPadding;
964
+ const contentWidth = (stdout?.columns ?? 80) - 4;
965
+ const renderedLines = useMemo4(() => {
966
+ const rendered = renderMarkdown(provider.readme, contentWidth);
967
+ return rendered.split("\n");
968
+ }, [provider.readme, contentWidth]);
969
+ const maxScroll = Math.max(0, renderedLines.length - viewHeight);
970
+ useInput2((input, key) => {
971
+ if (input === "q" || key.escape || key.return) {
972
+ onBack();
973
+ return;
974
+ }
975
+ if (key.upArrow) {
976
+ setScrollOffset((prev) => Math.max(0, prev - 1));
977
+ } else if (key.downArrow) {
978
+ setScrollOffset((prev) => Math.min(maxScroll, prev + 1));
979
+ }
980
+ });
981
+ const visibleContent = renderedLines.slice(scrollOffset, scrollOffset + viewHeight).join("\n");
982
+ const scrollbar = useMemo4(() => {
983
+ if (maxScroll === 0) return null;
984
+ const thumbSize = Math.max(
985
+ 1,
986
+ Math.round(viewHeight / renderedLines.length * viewHeight)
987
+ );
988
+ const thumbPos = Math.round(
989
+ scrollOffset / maxScroll * (viewHeight - thumbSize)
990
+ );
991
+ return Array.from(
992
+ { length: viewHeight },
993
+ (_, i) => i >= thumbPos && i < thumbPos + thumbSize
994
+ );
995
+ }, [scrollOffset, maxScroll, viewHeight, renderedLines.length]);
996
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
997
+ /* @__PURE__ */ jsxs5(Box5, { height: viewHeight, children: [
998
+ /* @__PURE__ */ jsx6(
999
+ Box5,
1000
+ {
1001
+ flexDirection: "column",
1002
+ paddingX: 1,
1003
+ paddingY: 1,
1004
+ flexGrow: 1,
1005
+ overflow: "hidden",
1006
+ children: /* @__PURE__ */ jsx6(Text6, { children: visibleContent })
1007
+ }
1008
+ ),
1009
+ scrollbar && /* @__PURE__ */ jsx6(Box5, { flexDirection: "column", children: scrollbar.map((isThumb, i) => /* @__PURE__ */ jsx6(
1010
+ Text6,
1011
+ {
1012
+ color: isThumb ? "cyan" : "gray",
1013
+ dimColor: !isThumb,
1014
+ children: isThumb ? "\u2503" : "\u2502"
1015
+ },
1016
+ i
1017
+ )) })
1018
+ ] }),
1019
+ /* @__PURE__ */ jsxs5(
1020
+ Box5,
1021
+ {
1022
+ flexDirection: "column",
1023
+ paddingX: 1,
1024
+ borderStyle: "single",
1025
+ borderTop: true,
1026
+ borderBottom: false,
1027
+ borderLeft: false,
1028
+ borderRight: false,
1029
+ borderColor: "gray",
1030
+ children: [
1031
+ /* @__PURE__ */ jsx6(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { color: "gray", children: "Actions" }) }),
1032
+ /* @__PURE__ */ jsxs5(Box5, { children: [
1033
+ /* @__PURE__ */ jsxs5(Text6, { color: "cyan", bold: true, children: [
1034
+ "\u276F",
1035
+ " Back"
1036
+ ] }),
1037
+ /* @__PURE__ */ jsx6(Text6, { color: "gray", children: " - Return to providers" })
1038
+ ] }),
1039
+ /* @__PURE__ */ jsx6(Box5, { marginTop: 1, height: 1, children: /* @__PURE__ */ jsxs5(Text6, { color: "gray", wrap: "truncate-end", children: [
1040
+ "Up/Down Scroll ",
1041
+ "\u2022",
1042
+ " Enter/q/Esc Back",
1043
+ maxScroll > 0 && ` \u2022 ${Math.round(scrollOffset / maxScroll * 100)}%`
1044
+ ] }) })
1045
+ ]
1046
+ }
1047
+ )
1048
+ ] });
1049
+ }
1050
+ function SetupPage({ onBack }) {
1051
+ const { providers, loading } = useSetupProviders();
1052
+ const [selectedProvider, setSelectedProvider] = useState9(null);
1053
+ const running = useMemo4(
1054
+ () => providers.filter((p) => p.status.running),
1055
+ [providers]
1056
+ );
1057
+ const installed = useMemo4(
1058
+ () => providers.filter((p) => p.status.installed && !p.status.running),
1059
+ [providers]
1060
+ );
1061
+ const notInstalled = useMemo4(
1062
+ () => providers.filter((p) => !p.status.installed),
1063
+ [providers]
1064
+ );
1065
+ const allProviders = useMemo4(
1066
+ () => [...running, ...installed, ...notInstalled],
1067
+ [running, installed, notInstalled]
1068
+ );
1069
+ const totalItems = allProviders.length + 1;
1070
+ const backIndex = allProviders.length;
1071
+ const [cursorIndex, setCursorIndex] = useState9(0);
1072
+ useEffect9(() => {
1073
+ setCursorIndex(0);
1074
+ }, [allProviders.length]);
1075
+ useInput2((input, key) => {
1076
+ if (selectedProvider) return;
1077
+ if (input === "q" || key.escape) {
1078
+ onBack();
1079
+ return;
1080
+ }
1081
+ if (key.upArrow) {
1082
+ setCursorIndex((prev) => Math.max(0, prev - 1));
1083
+ } else if (key.downArrow) {
1084
+ setCursorIndex((prev) => Math.min(totalItems - 1, prev + 1));
1085
+ } else if (key.return) {
1086
+ if (cursorIndex === backIndex) {
1087
+ onBack();
1088
+ } else if (allProviders[cursorIndex]) {
1089
+ setSelectedProvider(allProviders[cursorIndex].provider.name);
1090
+ }
1091
+ }
1092
+ });
1093
+ if (selectedProvider) {
1094
+ const found = providers.find((p) => p.provider.name === selectedProvider);
1095
+ if (found) {
1096
+ return /* @__PURE__ */ jsx6(
1097
+ ProviderDetailView,
1098
+ {
1099
+ provider: found.provider,
1100
+ onBack: () => setSelectedProvider(null)
1101
+ }
1102
+ );
1103
+ }
1104
+ }
1105
+ return /* @__PURE__ */ jsx6(Box5, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [
1106
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "white", underline: true, children: "Manage Providers" }),
1107
+ /* @__PURE__ */ jsx6(Text6, { color: "gray", children: "Select a provider to view its setup guide." }),
1108
+ loading ? /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
1109
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: /* @__PURE__ */ jsx6(Spinner3, { type: "dots" }) }),
1110
+ /* @__PURE__ */ jsx6(Text6, { children: " Detecting providers..." })
1111
+ ] }) : /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
1112
+ running.length > 0 && /* @__PURE__ */ jsxs5(Fragment2, { children: [
1113
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "green", children: "Running" }),
1114
+ running.map(({ provider }, i) => {
1115
+ const index = i;
1116
+ const isSelected = index === cursorIndex;
1117
+ return /* @__PURE__ */ jsxs5(
1118
+ Box5,
1119
+ {
1120
+ flexDirection: "column",
1121
+ marginTop: i > 0 ? 1 : 0,
1122
+ children: [
1123
+ /* @__PURE__ */ jsx6(Box5, { children: /* @__PURE__ */ jsxs5(
1124
+ Text6,
1125
+ {
1126
+ color: isSelected ? "cyan" : "white",
1127
+ bold: isSelected,
1128
+ children: [
1129
+ isSelected ? "\u276F" : " ",
1130
+ " ",
1131
+ "\u25CF",
1132
+ " ",
1133
+ provider.displayName
1134
+ ]
1135
+ }
1136
+ ) }),
1137
+ /* @__PURE__ */ jsxs5(Text6, { color: "gray", wrap: "wrap", children: [
1138
+ " ",
1139
+ provider.description
1140
+ ] })
1141
+ ]
1142
+ },
1143
+ provider.name
1144
+ );
1145
+ })
1146
+ ] }),
1147
+ installed.length > 0 && /* @__PURE__ */ jsxs5(
1148
+ Box5,
1149
+ {
1150
+ flexDirection: "column",
1151
+ marginTop: running.length > 0 ? 1 : 0,
1152
+ children: [
1153
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "yellow", children: "Installed" }),
1154
+ installed.map(({ provider, status }, i) => {
1155
+ const index = running.length + i;
1156
+ const isSelected = index === cursorIndex;
1157
+ return /* @__PURE__ */ jsxs5(
1158
+ Box5,
1159
+ {
1160
+ flexDirection: "column",
1161
+ marginTop: i > 0 ? 1 : 0,
1162
+ children: [
1163
+ /* @__PURE__ */ jsx6(Box5, { children: /* @__PURE__ */ jsxs5(
1164
+ Text6,
1165
+ {
1166
+ color: isSelected ? "cyan" : "white",
1167
+ bold: isSelected,
1168
+ children: [
1169
+ isSelected ? "\u276F" : " ",
1170
+ " ",
1171
+ "\u25CB",
1172
+ " ",
1173
+ provider.displayName
1174
+ ]
1175
+ }
1176
+ ) }),
1177
+ /* @__PURE__ */ jsxs5(Text6, { color: "gray", wrap: "wrap", children: [
1178
+ " ",
1179
+ provider.description
1180
+ ] })
1181
+ ]
1182
+ },
1183
+ provider.name
1184
+ );
1185
+ })
1186
+ ]
1187
+ }
1188
+ ),
1189
+ notInstalled.length > 0 && /* @__PURE__ */ jsxs5(
1190
+ Box5,
1191
+ {
1192
+ flexDirection: "column",
1193
+ marginTop: running.length > 0 || installed.length > 0 ? 1 : 0,
1194
+ children: [
1195
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "gray", children: "Not Installed" }),
1196
+ notInstalled.map(({ provider }, i) => {
1197
+ const index = running.length + installed.length + i;
1198
+ const isSelected = index === cursorIndex;
1199
+ return /* @__PURE__ */ jsxs5(
1200
+ Box5,
1201
+ {
1202
+ flexDirection: "column",
1203
+ marginTop: i > 0 ? 1 : 0,
1204
+ children: [
1205
+ /* @__PURE__ */ jsx6(Box5, { children: /* @__PURE__ */ jsxs5(
1206
+ Text6,
1207
+ {
1208
+ color: isSelected ? "cyan" : "white",
1209
+ bold: isSelected,
1210
+ children: [
1211
+ isSelected ? "\u276F" : " ",
1212
+ " ",
1213
+ provider.displayName
1214
+ ]
1215
+ }
1216
+ ) }),
1217
+ /* @__PURE__ */ jsxs5(Text6, { color: "gray", wrap: "wrap", children: [
1218
+ " ",
1219
+ provider.description
1220
+ ] })
1221
+ ]
1222
+ },
1223
+ provider.name
1224
+ );
1225
+ })
1226
+ ]
1227
+ }
1228
+ ),
1229
+ /* @__PURE__ */ jsx6(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(
1230
+ Text6,
1231
+ {
1232
+ color: cursorIndex === backIndex ? "cyan" : "white",
1233
+ bold: cursorIndex === backIndex,
1234
+ children: [
1235
+ cursorIndex === backIndex ? "\u276F" : " ",
1236
+ " Back"
1237
+ ]
1238
+ }
1239
+ ) })
1240
+ ] }),
1241
+ /* @__PURE__ */ jsx6(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text6, { color: "gray", children: [
1242
+ "Up/Down Navigate ",
1243
+ "\u2022",
1244
+ " Enter Select ",
1245
+ "\u2022",
1246
+ " q/Esc Back"
1247
+ ] }) })
1248
+ ] }) });
1249
+ }
1250
+
1251
+ // src/tui/interfaces/pages/InterfacesPage.tsx
1252
+ import { useState as useState13, useMemo as useMemo6, useEffect as useEffect13, useRef as useRef7 } from "react";
1253
+ import { Box as Box8, Text as Text9, useInput as useInput5 } from "ink";
1254
+
1255
+ // src/tui/interfaces/hooks/useLocalInterface.ts
1256
+ import { useState as useState10, useCallback as useCallback7, useRef as useRef5, useEffect as useEffect10 } from "react";
1257
+ import { spawn } from "child_process";
1258
+ import crypto from "crypto";
1259
+ import fs from "fs";
1260
+ import path from "path";
1261
+ var INTERFACE_SCAFFOLD_REPO = "https://github.com/mindstudio-ai/spa-bundle-scaffold";
1262
+ var SCRIPT_SCAFFOLD_REPO = "https://github.com/mindstudio-ai/script-scaffold";
1263
+ var MAX_OUTPUT_LINES = 500;
1264
+ function useLocalInterface({
1265
+ mode,
1266
+ appId,
1267
+ stepId,
1268
+ workflowId,
1269
+ sessionId
1270
+ }) {
1271
+ const key = `${appId}:${stepId}`;
1272
+ const [hasLocalCopy, setHasLocalCopy] = useState10(() => {
1273
+ const existing = getLocalInterfacePath(key);
1274
+ if (!existing) return false;
1275
+ try {
1276
+ return fs.existsSync(existing);
1277
+ } catch {
1278
+ return false;
1279
+ }
1280
+ });
1281
+ const [phase, setPhase] = useState10("idle");
1282
+ const [outputLines, setOutputLines] = useState10([]);
1283
+ const [errorMessage, setErrorMessage] = useState10(null);
1284
+ const processRef = useRef5(null);
1285
+ const mountedRef = useRef5(true);
1286
+ const stoppedRef = useRef5(false);
1287
+ useEffect10(() => {
1288
+ mountedRef.current = true;
1289
+ return () => {
1290
+ mountedRef.current = false;
1291
+ if (processRef.current) {
1292
+ processRef.current.kill("SIGTERM");
1293
+ processRef.current = null;
1294
+ }
1295
+ };
1296
+ }, []);
1297
+ const appendOutput = useCallback7((line) => {
1298
+ if (!mountedRef.current) return;
1299
+ setOutputLines((prev) => {
1300
+ const next = [...prev, line];
1301
+ return next.length > MAX_OUTPUT_LINES ? next.slice(next.length - MAX_OUTPUT_LINES) : next;
1302
+ });
1303
+ }, []);
1304
+ const runCommand = useCallback7(
1305
+ (command, args, options = {}) => {
1306
+ return new Promise((resolve, reject) => {
1307
+ const fullCommand = [command, ...args].join(" ");
1308
+ const proc = spawn(fullCommand, [], {
1309
+ cwd: options.cwd,
1310
+ shell: true,
1311
+ env: { ...process.env, FORCE_COLOR: "1", ...options.env }
1312
+ });
1313
+ processRef.current = proc;
1314
+ const handleData = (data) => {
1315
+ const lines = data.toString().split("\n");
1316
+ for (const line of lines) {
1317
+ if (line.length > 0) {
1318
+ appendOutput(line);
1319
+ }
1320
+ }
1321
+ };
1322
+ proc.stdout?.on("data", handleData);
1323
+ proc.stderr?.on("data", handleData);
1324
+ proc.on("close", (code) => {
1325
+ processRef.current = null;
1326
+ resolve(code ?? 0);
1327
+ });
1328
+ proc.on("error", (err) => {
1329
+ processRef.current = null;
1330
+ reject(err);
1331
+ });
1332
+ });
1333
+ },
1334
+ [appendOutput]
1335
+ );
1336
+ const getScaffoldRepo = () => mode === "script" ? SCRIPT_SCAFFOLD_REPO : INTERFACE_SCAFFOLD_REPO;
1337
+ const getDirPrefix = () => mode === "script" ? "script" : "interface";
1338
+ const getDevLocalArgs = () => {
1339
+ const env = {};
1340
+ if (getEnvironment() === "local") {
1341
+ env.MINDSTUDIO_API_URL = getApiBaseUrl();
1342
+ }
1343
+ if (mode === "script") {
1344
+ env.MINDSTUDIO_API_KEY = getApiKey() ?? "";
1345
+ return {
1346
+ args: [
1347
+ "run",
1348
+ "dev:local",
1349
+ "--",
1350
+ "--app",
1351
+ appId,
1352
+ "--workflow",
1353
+ workflowId,
1354
+ "--step",
1355
+ stepId
1356
+ ],
1357
+ env
1358
+ };
1359
+ }
1360
+ return {
1361
+ args: ["run", "dev:local", "--", sessionId ?? ""],
1362
+ env
1363
+ };
1364
+ };
1365
+ const start = useCallback7(() => {
1366
+ setErrorMessage(null);
1367
+ setOutputLines([]);
1368
+ stoppedRef.current = false;
1369
+ const run = async () => {
1370
+ const localPath = getLocalInterfacePath(key);
1371
+ const dirExists = localPath && fs.existsSync(localPath);
1372
+ try {
1373
+ if (!dirExists) {
1374
+ setPhase("cloning");
1375
+ const interfacesDir = getLocalInterfacesDir();
1376
+ fs.mkdirSync(interfacesDir, { recursive: true });
1377
+ const shortId = crypto.randomBytes(4).toString("hex");
1378
+ const dirName = `${getDirPrefix()}-${shortId}`;
1379
+ const targetDir = path.join(interfacesDir, dirName);
1380
+ appendOutput(`Cloning scaffold into ${targetDir}...`);
1381
+ const cloneCode = await runCommand("git", [
1382
+ "clone",
1383
+ "--depth",
1384
+ "1",
1385
+ getScaffoldRepo(),
1386
+ targetDir
1387
+ ]);
1388
+ if (!mountedRef.current) return;
1389
+ if (cloneCode !== 0) {
1390
+ throw new Error(`git clone failed with exit code ${cloneCode}`);
1391
+ }
1392
+ setPhase("installing");
1393
+ appendOutput("Installing dependencies...");
1394
+ const installCode = await runCommand("npm", ["install"], {
1395
+ cwd: targetDir
1396
+ });
1397
+ if (!mountedRef.current) return;
1398
+ if (installCode !== 0) {
1399
+ throw new Error(`npm install failed with exit code ${installCode}`);
1400
+ }
1401
+ setLocalInterfacePath(key, targetDir);
1402
+ setHasLocalCopy(true);
1403
+ setPhase("running");
1404
+ const { args, env } = getDevLocalArgs();
1405
+ appendOutput("Starting local dev server...");
1406
+ await runCommand("npm", args, { cwd: targetDir, env });
1407
+ if (mountedRef.current) {
1408
+ setPhase("idle");
1409
+ setOutputLines([]);
1410
+ }
1411
+ } else {
1412
+ setPhase("running");
1413
+ const { args, env } = getDevLocalArgs();
1414
+ appendOutput("Starting local dev server...");
1415
+ await runCommand("npm", args, { cwd: localPath, env });
1416
+ if (mountedRef.current) {
1417
+ setPhase("idle");
1418
+ setOutputLines([]);
1419
+ }
1420
+ }
1421
+ } catch (err) {
1422
+ if (mountedRef.current) {
1423
+ setPhase("error");
1424
+ setErrorMessage(err instanceof Error ? err.message : "Unknown error");
1425
+ }
1426
+ }
1427
+ };
1428
+ run();
1429
+ }, [
1430
+ key,
1431
+ mode,
1432
+ appId,
1433
+ stepId,
1434
+ workflowId,
1435
+ sessionId,
1436
+ appendOutput,
1437
+ runCommand
1438
+ ]);
1439
+ const stop = useCallback7(() => {
1440
+ stoppedRef.current = true;
1441
+ if (processRef.current) {
1442
+ processRef.current.kill("SIGTERM");
1443
+ const proc = processRef.current;
1444
+ setTimeout(() => {
1445
+ if (proc && !proc.killed) {
1446
+ proc.kill("SIGKILL");
1447
+ }
1448
+ }, 2e3);
1449
+ processRef.current = null;
1450
+ }
1451
+ setPhase("idle");
1452
+ }, []);
1453
+ const deleteLocalCopy = useCallback7(() => {
1454
+ const localPath = getLocalInterfacePath(key);
1455
+ if (!localPath) return;
1456
+ setPhase("deleting");
1457
+ setOutputLines([]);
1458
+ appendOutput(`Deleting ${localPath}...`);
1459
+ try {
1460
+ fs.rmSync(localPath, { recursive: true, force: true });
1461
+ deleteLocalInterfacePath(key);
1462
+ setHasLocalCopy(false);
1463
+ appendOutput("Deleted successfully.");
1464
+ } catch (err) {
1465
+ setErrorMessage(err instanceof Error ? err.message : "Failed to delete");
1466
+ }
1467
+ setPhase("idle");
1468
+ }, [key, appendOutput]);
1469
+ return {
1470
+ phase,
1471
+ hasLocalCopy,
1472
+ localPath: getLocalInterfacePath(key),
1473
+ outputLines,
1474
+ errorMessage,
1475
+ start,
1476
+ stop,
1477
+ deleteLocalCopy
1478
+ };
1479
+ }
1480
+
1481
+ // src/tui/interfaces/pages/InterfaceSessionView.tsx
1482
+ import { useState as useState11, useEffect as useEffect11 } from "react";
1483
+ import { Box as Box6, Text as Text7, useInput as useInput3 } from "ink";
1484
+ import os2 from "os";
1485
+ import open from "open";
1486
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1487
+ function InterfaceSessionView({
1488
+ item,
1489
+ onStart,
1490
+ onDelete,
1491
+ onBack,
1492
+ hasLocalCopy,
1493
+ localPath
1494
+ }) {
1495
+ const menuItems = [
1496
+ { id: "start", label: "Start Locally" }
1497
+ ];
1498
+ if (hasLocalCopy) {
1499
+ menuItems.push({ id: "reveal", label: "Open Folder" });
1500
+ menuItems.push({ id: "delete", label: "Delete Local Copy" });
1501
+ }
1502
+ menuItems.push({ id: "back", label: "Back" });
1503
+ const [cursorIndex, setCursorIndex] = useState11(0);
1504
+ useEffect11(() => {
1505
+ setCursorIndex(0);
1506
+ }, [hasLocalCopy]);
1507
+ useInput3((input, key) => {
1508
+ if (input === "q" || key.escape) {
1509
+ onBack();
1510
+ return;
1511
+ }
1512
+ if (key.upArrow) {
1513
+ setCursorIndex((prev) => Math.max(0, prev - 1));
1514
+ } else if (key.downArrow) {
1515
+ setCursorIndex((prev) => Math.min(menuItems.length - 1, prev + 1));
1516
+ } else if (key.return) {
1517
+ const selected = menuItems[cursorIndex];
1518
+ if (!selected) return;
1519
+ switch (selected.id) {
1520
+ case "start":
1521
+ onStart();
1522
+ break;
1523
+ case "reveal":
1524
+ if (localPath) {
1525
+ open(localPath);
1526
+ }
1527
+ break;
1528
+ case "delete":
1529
+ onDelete();
1530
+ break;
1531
+ case "back":
1532
+ onBack();
1533
+ break;
1534
+ }
1535
+ }
1536
+ });
1537
+ const name = `${item.step.workflowName} - ${item.step.displayName}`;
1538
+ const displayPath = localPath?.replace(os2.homedir(), "~");
1539
+ let sessionInfo = null;
1540
+ if (item.kind === "interface") {
1541
+ const hotUpdateDomain = item.step.spaEditorSession?.hotUpdateDomain ?? "";
1542
+ sessionInfo = hotUpdateDomain.replace(/^https?:\/\//, "").split(".")[0] || null;
1543
+ }
1544
+ return /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [
1545
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "white", underline: true, children: name }),
1546
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
1547
+ sessionInfo && /* @__PURE__ */ jsxs6(Text7, { color: "gray", children: [
1548
+ "Session: ",
1549
+ sessionInfo
1550
+ ] }),
1551
+ hasLocalCopy && localPath ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
1552
+ /* @__PURE__ */ jsx7(Text7, { color: "green", children: "Local copy exists" }),
1553
+ /* @__PURE__ */ jsxs6(Text7, { color: "gray", children: [
1554
+ "Path: ",
1555
+ displayPath
1556
+ ] })
1557
+ ] }) : /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: 'No local copy. "Start Locally" will clone the scaffold and install dependencies.' })
1558
+ ] }),
1559
+ /* @__PURE__ */ jsx7(Box6, { marginTop: 1, flexDirection: "column", children: menuItems.map((menuItem, i) => {
1560
+ const isSelected = i === cursorIndex;
1561
+ return /* @__PURE__ */ jsxs6(
1562
+ Text7,
1563
+ {
1564
+ color: isSelected ? "cyan" : "white",
1565
+ bold: isSelected,
1566
+ children: [
1567
+ isSelected ? "\u276F" : " ",
1568
+ " ",
1569
+ menuItem.label
1570
+ ]
1571
+ },
1572
+ menuItem.id
1573
+ );
1574
+ }) }),
1575
+ /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text7, { color: "gray", children: [
1576
+ "Up/Down Navigate ",
1577
+ "\u2022",
1578
+ " Enter Select ",
1579
+ "\u2022",
1580
+ " q/Esc Back"
1581
+ ] }) })
1582
+ ] }) });
1583
+ }
1584
+
1585
+ // src/tui/interfaces/pages/InterfaceRunningView.tsx
1586
+ import { useState as useState12, useMemo as useMemo5, useRef as useRef6, useEffect as useEffect12 } from "react";
1587
+ import { Box as Box7, Text as Text8, useInput as useInput4, useStdout as useStdout6 } from "ink";
1588
+ import { execSync } from "child_process";
1589
+ import os3 from "os";
1590
+ import open2 from "open";
1591
+ import Spinner4 from "ink-spinner";
1592
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1593
+ function copyToClipboard(text) {
1594
+ try {
1595
+ if (process.platform === "darwin") {
1596
+ execSync("pbcopy", { input: text });
1597
+ } else if (process.platform === "win32") {
1598
+ execSync("clip", { input: text });
1599
+ } else {
1600
+ execSync("xclip -selection clipboard", { input: text });
1601
+ }
1602
+ return true;
1603
+ } catch {
1604
+ return false;
1605
+ }
1606
+ }
1607
+ function getPhaseLabel(phase) {
1608
+ switch (phase) {
1609
+ case "cloning":
1610
+ return { text: "Cloning scaffold...", color: "cyan", showSpinner: true };
1611
+ case "installing":
1612
+ return {
1613
+ text: "Installing dependencies...",
1614
+ color: "cyan",
1615
+ showSpinner: true
1616
+ };
1617
+ case "running":
1618
+ return { text: "Dev server running", color: "green", showSpinner: true };
1619
+ case "error":
1620
+ return { text: "Error", color: "red", showSpinner: false };
1621
+ case "deleting":
1622
+ return {
1623
+ text: "Deleting local copy...",
1624
+ color: "yellow",
1625
+ showSpinner: true
1626
+ };
1627
+ default:
1628
+ return { text: "Idle", color: "gray", showSpinner: false };
1629
+ }
1630
+ }
1631
+ function InterfaceRunningView({
1632
+ name,
1633
+ phase,
1634
+ outputLines,
1635
+ errorMessage,
1636
+ localPath,
1637
+ onStop,
1638
+ onBack
1639
+ }) {
1640
+ const { stdout } = useStdout6();
1641
+ const termHeight = (stdout?.rows ?? 24) - 4;
1642
+ const isActive = phase === "cloning" || phase === "installing" || phase === "running" || phase === "deleting";
1643
+ const displayPath = localPath?.replace(os3.homedir(), "~");
1644
+ const menuItems = useMemo5(() => {
1645
+ const items = [];
1646
+ if (isActive && displayPath) {
1647
+ items.push({
1648
+ id: "claude",
1649
+ label: "Copy Claude Code Command",
1650
+ copyValue: `cd ${displayPath} && claude`
1651
+ });
1652
+ items.push({
1653
+ id: "codex",
1654
+ label: "Copy Codex Command",
1655
+ copyValue: `cd ${displayPath} && codex`
1656
+ });
1657
+ items.push({ id: "reveal", label: "Open Folder" });
1658
+ }
1659
+ items.push({ id: "action", label: isActive ? "Stop" : "Back" });
1660
+ return items;
1661
+ }, [isActive, displayPath]);
1662
+ const [cursorIndex, setCursorIndex] = useState12(0);
1663
+ const [copiedId, setCopiedId] = useState12(null);
1664
+ const copiedTimerRef = useRef6();
1665
+ useEffect12(() => {
1666
+ return () => clearTimeout(copiedTimerRef.current);
1667
+ }, []);
1668
+ const headerHeight = 14;
1669
+ const menuHeight = menuItems.length;
1670
+ const chromeLines = 7 + menuHeight;
1671
+ const logHeight = Math.max(3, termHeight - headerHeight - chromeLines);
1672
+ const maxScroll = Math.max(0, outputLines.length - logHeight);
1673
+ const effectiveOffset = maxScroll;
1674
+ const copyItem = (item) => {
1675
+ if (!item.copyValue) return;
1676
+ const success = copyToClipboard(item.copyValue);
1677
+ if (success) {
1678
+ setCopiedId(item.id);
1679
+ clearTimeout(copiedTimerRef.current);
1680
+ copiedTimerRef.current = setTimeout(() => setCopiedId(null), 2e3);
1681
+ }
1682
+ };
1683
+ useInput4((input, key) => {
1684
+ if (key.upArrow) {
1685
+ setCursorIndex((prev) => Math.max(0, prev - 1));
1686
+ } else if (key.downArrow) {
1687
+ setCursorIndex((prev) => Math.min(menuItems.length - 1, prev + 1));
1688
+ } else if (key.return) {
1689
+ const item = menuItems[cursorIndex];
1690
+ if (!item) return;
1691
+ if (item.copyValue) {
1692
+ copyItem(item);
1693
+ } else if (item.id === "reveal" && localPath) {
1694
+ open2(localPath);
1695
+ } else if (isActive) {
1696
+ onStop();
1697
+ } else {
1698
+ onBack();
1699
+ }
1700
+ } else if (input === "q" || key.escape) {
1701
+ if (isActive) {
1702
+ onStop();
1703
+ } else {
1704
+ onBack();
1705
+ }
1706
+ }
1707
+ });
1708
+ const visibleLines = outputLines.slice(
1709
+ effectiveOffset,
1710
+ effectiveOffset + logHeight
1711
+ );
1712
+ const phaseInfo = getPhaseLabel(phase);
1713
+ return /* @__PURE__ */ jsx8(Box7, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [
1714
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: "white", underline: true, children: name }),
1715
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
1716
+ phaseInfo.showSpinner && /* @__PURE__ */ jsxs7(Text8, { color: phaseInfo.color, children: [
1717
+ /* @__PURE__ */ jsx8(Spinner4, { type: "dots" }),
1718
+ " "
1719
+ ] }),
1720
+ /* @__PURE__ */ jsx8(Text8, { color: phaseInfo.color, children: phaseInfo.text })
1721
+ ] }),
1722
+ errorMessage && /* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "red", children: errorMessage }) }),
1723
+ /* @__PURE__ */ jsx8(Box7, { marginTop: 1, height: logHeight, children: /* @__PURE__ */ jsx8(Box7, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: visibleLines.map((line, i) => /* @__PURE__ */ jsx8(Text8, { wrap: "truncate-end", color: "gray", children: line }, effectiveOffset + i)) }) }),
1724
+ /* @__PURE__ */ jsx8(Box7, { marginTop: 1, flexDirection: "column", children: menuItems.map((item, i) => {
1725
+ const isSelected = i === cursorIndex;
1726
+ const isCopied = copiedId === item.id;
1727
+ return /* @__PURE__ */ jsxs7(Box7, { children: [
1728
+ /* @__PURE__ */ jsxs7(Text8, { color: isSelected ? "cyan" : "white", bold: isSelected, children: [
1729
+ isSelected ? "\u276F" : " ",
1730
+ " ",
1731
+ item.label
1732
+ ] }),
1733
+ isCopied && /* @__PURE__ */ jsxs7(Text8, { color: "green", children: [
1734
+ " ",
1735
+ "\u2713",
1736
+ " Copied!"
1737
+ ] })
1738
+ ] }, item.id);
1739
+ }) }),
1740
+ /* @__PURE__ */ jsx8(Box7, { marginTop: 1, height: 1, children: /* @__PURE__ */ jsxs7(Text8, { color: "gray", wrap: "truncate-end", children: [
1741
+ "Up/Down Navigate ",
1742
+ "\u2022",
1743
+ " Enter Select ",
1744
+ "\u2022",
1745
+ " q/Esc",
1746
+ " ",
1747
+ isActive ? "Stop" : "Back"
1748
+ ] }) })
1749
+ ] }) });
1750
+ }
1751
+
1752
+ // src/tui/interfaces/pages/InterfacesPage.tsx
1753
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1754
+ function getSessionStatus(item) {
1755
+ if (item.kind === "script") return "running";
1756
+ const status = item.step.spaEditorSession?.status;
1757
+ if (status === "running" || status === "compiling") return status;
1758
+ if (status === "starting") return "starting";
1759
+ return "stopped";
1760
+ }
1761
+ function isItemSelectable(item) {
1762
+ const status = getSessionStatus(item);
1763
+ return status === "running" || status === "compiling";
1764
+ }
1765
+ function getStatusLabel(status) {
1766
+ switch (status) {
1767
+ case "running":
1768
+ case "compiling":
1769
+ return { text: "Online", color: "green" };
1770
+ case "starting":
1771
+ return { text: "Starting", color: "yellow" };
1772
+ case "stopped":
1773
+ return { text: "Offline", color: "gray" };
1774
+ }
1775
+ }
1776
+ function getRefreshSuffix(status) {
1777
+ switch (status) {
1778
+ case "refreshing":
1779
+ return "Refreshing...";
1780
+ case "refreshed":
1781
+ return "\u2713 Refreshed";
1782
+ case "idle":
1783
+ return null;
1784
+ }
1785
+ }
1786
+ function formatCount(interfaces, scripts) {
1787
+ const parts = [];
1788
+ if (interfaces > 0) {
1789
+ parts.push(`${interfaces} Interface${interfaces !== 1 ? "s" : ""}`);
1790
+ }
1791
+ if (scripts > 0) {
1792
+ parts.push(`${scripts} Script${scripts !== 1 ? "s" : ""}`);
1793
+ }
1794
+ return parts.join(", ");
1795
+ }
1796
+ function OfflineView({ item, onBack }) {
1797
+ useInput5((input, key) => {
1798
+ if (input === "q" || key.escape || key.return) {
1799
+ onBack();
1800
+ }
1801
+ });
1802
+ const name = item.kind === "interface" ? `${item.step.workflowName} - ${item.step.displayName}` : `${item.step.workflowName} - ${item.step.displayName}`;
1803
+ return /* @__PURE__ */ jsx9(Box8, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [
1804
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "white", underline: true, children: name }),
1805
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
1806
+ /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "This interface is offline." }),
1807
+ /* @__PURE__ */ jsx9(Text9, { color: "gray", children: "Start the interface designer in MindStudio to connect to it." })
1808
+ ] }),
1809
+ /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text9, { color: "cyan", bold: true, children: [
1810
+ "\u276F",
1811
+ " Back"
1812
+ ] }) }),
1813
+ /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { color: "gray", children: "Enter/q/Esc Back" }) })
1814
+ ] }) });
1815
+ }
1816
+ function AgentListView({
1817
+ sessions,
1818
+ onBack,
1819
+ onSelectAgent,
1820
+ onRefresh,
1821
+ refreshStatus
1822
+ }) {
1823
+ const refreshIndex = sessions.length;
1824
+ const backIndex = sessions.length + 1;
1825
+ const totalItems = sessions.length + 2;
1826
+ const [cursorIndex, setCursorIndex] = useState13(0);
1827
+ useEffect13(() => {
1828
+ setCursorIndex(sessions.length > 0 ? 0 : backIndex);
1829
+ }, [sessions.length, backIndex]);
1830
+ useInput5((input, key) => {
1831
+ if (input === "q" || key.escape) {
1832
+ onBack();
1833
+ return;
1834
+ }
1835
+ if (key.upArrow) {
1836
+ setCursorIndex((prev) => Math.max(0, prev - 1));
1837
+ } else if (key.downArrow) {
1838
+ setCursorIndex((prev) => Math.min(totalItems - 1, prev + 1));
1839
+ } else if (key.return) {
1840
+ if (cursorIndex === backIndex) {
1841
+ onBack();
1842
+ } else if (cursorIndex === refreshIndex) {
1843
+ onRefresh();
1844
+ } else if (sessions[cursorIndex]) {
1845
+ onSelectAgent(sessions[cursorIndex].appId);
1846
+ }
1847
+ }
1848
+ });
1849
+ return /* @__PURE__ */ jsx9(Box8, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [
1850
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "white", underline: true, children: "Choose an Agent" }),
1851
+ /* @__PURE__ */ jsx9(Text9, { color: "gray", children: "Don't see your agent? Make sure it's open in the MindStudio editor." }),
1852
+ sessions.length === 0 ? /* @__PURE__ */ jsx9(Box8, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "No active editor sessions." }) }) : /* @__PURE__ */ jsx9(Box8, { flexDirection: "column", marginTop: 1, children: sessions.map((session, i) => {
1853
+ const isSelected = i === cursorIndex;
1854
+ const stats = formatCount(
1855
+ session.customInterfaceSteps.length,
1856
+ session.scriptSteps.length
1857
+ );
1858
+ return /* @__PURE__ */ jsxs8(
1859
+ Box8,
1860
+ {
1861
+ flexDirection: "column",
1862
+ marginTop: i > 0 ? 1 : 0,
1863
+ children: [
1864
+ /* @__PURE__ */ jsxs8(Box8, { children: [
1865
+ /* @__PURE__ */ jsxs8(
1866
+ Text9,
1867
+ {
1868
+ color: isSelected ? "cyan" : "white",
1869
+ bold: isSelected,
1870
+ children: [
1871
+ isSelected ? "\u276F" : " ",
1872
+ " ",
1873
+ session.appName
1874
+ ]
1875
+ }
1876
+ ),
1877
+ isSelected && /* @__PURE__ */ jsx9(Text9, { color: "gray", children: " - Connect to this Agent" })
1878
+ ] }),
1879
+ /* @__PURE__ */ jsxs8(Text9, { color: "gray", children: [
1880
+ " ",
1881
+ "https://app.mindstudio.ai/agents/",
1882
+ session.appId,
1883
+ "/edit"
1884
+ ] }),
1885
+ stats !== "" && /* @__PURE__ */ jsxs8(Text9, { color: "gray", children: [
1886
+ " ",
1887
+ stats
1888
+ ] })
1889
+ ]
1890
+ },
1891
+ session.appId
1892
+ );
1893
+ }) }),
1894
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 1, children: [
1895
+ /* @__PURE__ */ jsxs8(Box8, { children: [
1896
+ /* @__PURE__ */ jsxs8(
1897
+ Text9,
1898
+ {
1899
+ color: cursorIndex === refreshIndex ? "cyan" : "white",
1900
+ bold: cursorIndex === refreshIndex,
1901
+ children: [
1902
+ cursorIndex === refreshIndex ? "\u276F" : " ",
1903
+ " Refresh"
1904
+ ]
1905
+ }
1906
+ ),
1907
+ getRefreshSuffix(refreshStatus) && /* @__PURE__ */ jsxs8(Text9, { color: "gray", children: [
1908
+ " ",
1909
+ getRefreshSuffix(refreshStatus)
1910
+ ] })
1911
+ ] }),
1912
+ /* @__PURE__ */ jsxs8(
1913
+ Text9,
1914
+ {
1915
+ color: cursorIndex === backIndex ? "cyan" : "white",
1916
+ bold: cursorIndex === backIndex,
1917
+ children: [
1918
+ cursorIndex === backIndex ? "\u276F" : " ",
1919
+ " Back"
1920
+ ]
1921
+ }
1922
+ )
1923
+ ] }),
1924
+ /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text9, { color: "gray", children: [
1925
+ "Up/Down Navigate ",
1926
+ "\u2022",
1927
+ " Enter Select ",
1928
+ "\u2022",
1929
+ " q/Esc Back"
1930
+ ] }) })
1931
+ ] }) });
1932
+ }
1933
+ function AgentDetailView({
1934
+ session,
1935
+ onBack,
1936
+ onSelect,
1937
+ onRefresh,
1938
+ refreshStatus
1939
+ }) {
1940
+ const [offlineItem, setOfflineItem] = useState13(null);
1941
+ const interfaces = useMemo6(
1942
+ () => session.customInterfaceSteps.map((step) => ({
1943
+ kind: "interface",
1944
+ appId: session.appId,
1945
+ appName: session.appName,
1946
+ step
1947
+ })),
1948
+ [session]
1949
+ );
1950
+ const scripts = useMemo6(
1951
+ () => session.scriptSteps.map((step) => ({
1952
+ kind: "script",
1953
+ appId: session.appId,
1954
+ appName: session.appName,
1955
+ step
1956
+ })),
1957
+ [session]
1958
+ );
1959
+ const allItems = useMemo6(
1960
+ () => [...interfaces, ...scripts],
1961
+ [interfaces, scripts]
1962
+ );
1963
+ const refreshIndex = allItems.length;
1964
+ const backIndex = allItems.length + 1;
1965
+ const totalItems = allItems.length + 2;
1966
+ const [cursorIndex, setCursorIndex] = useState13(0);
1967
+ useEffect13(() => {
1968
+ setCursorIndex(0);
1969
+ }, [allItems.length]);
1970
+ useInput5((input, key) => {
1971
+ if (offlineItem) return;
1972
+ if (input === "q" || key.escape) {
1973
+ onBack();
1974
+ return;
1975
+ }
1976
+ if (key.upArrow) {
1977
+ setCursorIndex((prev) => Math.max(0, prev - 1));
1978
+ } else if (key.downArrow) {
1979
+ setCursorIndex((prev) => Math.min(totalItems - 1, prev + 1));
1980
+ } else if (key.return) {
1981
+ if (cursorIndex === backIndex) {
1982
+ onBack();
1983
+ } else if (cursorIndex === refreshIndex) {
1984
+ onRefresh();
1985
+ } else {
1986
+ const item = allItems[cursorIndex];
1987
+ if (item) {
1988
+ if (isItemSelectable(item)) {
1989
+ onSelect(item);
1990
+ } else {
1991
+ setOfflineItem(item);
1992
+ }
1993
+ }
1994
+ }
1995
+ }
1996
+ });
1997
+ if (offlineItem) {
1998
+ return /* @__PURE__ */ jsx9(OfflineView, { item: offlineItem, onBack: () => setOfflineItem(null) });
1999
+ }
2000
+ const scriptsOffset = interfaces.length;
2001
+ return /* @__PURE__ */ jsx9(Box8, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [
2002
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "white", underline: true, children: session.appName }),
2003
+ /* @__PURE__ */ jsxs8(Text9, { color: "gray", children: [
2004
+ "https://app.mindstudio.ai/agents/",
2005
+ session.appId,
2006
+ "/edit"
2007
+ ] }),
2008
+ /* @__PURE__ */ jsx9(Text9, { color: "gray", children: "Select an interface or script to connect your local editor." }),
2009
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 1, children: [
2010
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "white", children: "Interfaces" }),
2011
+ interfaces.length > 0 ? /* @__PURE__ */ jsx9(Box8, { flexDirection: "column", marginTop: 1, children: interfaces.map((item, i) => {
2012
+ const isSelected = i === cursorIndex;
2013
+ const status = getSessionStatus(item);
2014
+ const statusLabel = getStatusLabel(status);
2015
+ return /* @__PURE__ */ jsxs8(
2016
+ Box8,
2017
+ {
2018
+ flexDirection: "column",
2019
+ marginTop: i > 0 ? 1 : 0,
2020
+ children: [
2021
+ /* @__PURE__ */ jsxs8(
2022
+ Text9,
2023
+ {
2024
+ color: isSelected ? "cyan" : "white",
2025
+ bold: isSelected,
2026
+ children: [
2027
+ isSelected ? "\u276F" : " ",
2028
+ " ",
2029
+ item.step.workflowName,
2030
+ " -",
2031
+ " ",
2032
+ item.step.displayName
2033
+ ]
2034
+ }
2035
+ ),
2036
+ /* @__PURE__ */ jsx9(Box8, { children: /* @__PURE__ */ jsxs8(Text9, { color: statusLabel.color, children: [
2037
+ " ",
2038
+ statusLabel.text
2039
+ ] }) })
2040
+ ]
2041
+ },
2042
+ `${item.step.workflowId}:${item.step.stepId}`
2043
+ );
2044
+ }) }) : /* @__PURE__ */ jsx9(Text9, { color: "gray", children: " No interfaces in this agent." }),
2045
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 1, children: [
2046
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: "white", children: "Scripts" }),
2047
+ scripts.length > 0 ? /* @__PURE__ */ jsx9(Box8, { flexDirection: "column", marginTop: 1, children: scripts.map((item, i) => {
2048
+ const index = scriptsOffset + i;
2049
+ const isSelected = index === cursorIndex;
2050
+ return /* @__PURE__ */ jsxs8(
2051
+ Box8,
2052
+ {
2053
+ flexDirection: "column",
2054
+ marginTop: i > 0 ? 1 : 0,
2055
+ children: [
2056
+ /* @__PURE__ */ jsxs8(
2057
+ Text9,
2058
+ {
2059
+ color: isSelected ? "cyan" : "white",
2060
+ bold: isSelected,
2061
+ children: [
2062
+ isSelected ? "\u276F" : " ",
2063
+ " ",
2064
+ item.step.workflowName,
2065
+ " -",
2066
+ " ",
2067
+ item.step.displayName
2068
+ ]
2069
+ }
2070
+ ),
2071
+ /* @__PURE__ */ jsxs8(Text9, { color: "gray", children: [
2072
+ " ",
2073
+ item.step.entryFile
2074
+ ] })
2075
+ ]
2076
+ },
2077
+ `${item.step.workflowId}:${item.step.stepId}`
2078
+ );
2079
+ }) }) : /* @__PURE__ */ jsx9(Text9, { color: "gray", children: " No scripts in this agent." })
2080
+ ] }),
2081
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginTop: 1, children: [
2082
+ /* @__PURE__ */ jsxs8(Box8, { children: [
2083
+ /* @__PURE__ */ jsxs8(
2084
+ Text9,
2085
+ {
2086
+ color: cursorIndex === refreshIndex ? "cyan" : "white",
2087
+ bold: cursorIndex === refreshIndex,
2088
+ children: [
2089
+ cursorIndex === refreshIndex ? "\u276F" : " ",
2090
+ " Refresh"
2091
+ ]
2092
+ }
2093
+ ),
2094
+ getRefreshSuffix(refreshStatus) && /* @__PURE__ */ jsxs8(Text9, { color: "gray", children: [
2095
+ " ",
2096
+ getRefreshSuffix(refreshStatus)
2097
+ ] })
2098
+ ] }),
2099
+ /* @__PURE__ */ jsxs8(
2100
+ Text9,
2101
+ {
2102
+ color: cursorIndex === backIndex ? "cyan" : "white",
2103
+ bold: cursorIndex === backIndex,
2104
+ children: [
2105
+ cursorIndex === backIndex ? "\u276F" : " ",
2106
+ " Back"
2107
+ ]
2108
+ }
2109
+ )
2110
+ ] })
2111
+ ] }),
2112
+ /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text9, { color: "gray", children: [
2113
+ "Up/Down Navigate ",
2114
+ "\u2022",
2115
+ " Enter Select ",
2116
+ "\u2022",
2117
+ " q/Esc Back"
2118
+ ] }) })
2119
+ ] }) });
2120
+ }
2121
+ function LocalDevView({
2122
+ item,
2123
+ onBack
2124
+ }) {
2125
+ const mode = item.kind === "script" ? "script" : "interface";
2126
+ let sessionId = "";
2127
+ if (item.kind === "interface") {
2128
+ const hotUpdateDomain = item.step.spaEditorSession?.hotUpdateDomain ?? "";
2129
+ sessionId = hotUpdateDomain.replace(/^https?:\/\//, "").split(".")[0] || "";
2130
+ }
2131
+ const {
2132
+ phase,
2133
+ hasLocalCopy,
2134
+ localPath,
2135
+ outputLines,
2136
+ errorMessage,
2137
+ start,
2138
+ stop,
2139
+ deleteLocalCopy
2140
+ } = useLocalInterface({
2141
+ mode,
2142
+ appId: item.appId,
2143
+ stepId: item.step.stepId,
2144
+ workflowId: item.step.workflowId,
2145
+ sessionId
2146
+ });
2147
+ const name = `${item.step.workflowName} - ${item.step.displayName}`;
2148
+ const isActive = phase === "cloning" || phase === "installing" || phase === "running" || phase === "deleting";
2149
+ const wasActiveRef = useRef7(false);
2150
+ useEffect13(() => {
2151
+ if (isActive) {
2152
+ wasActiveRef.current = true;
2153
+ } else if (wasActiveRef.current && phase === "idle") {
2154
+ wasActiveRef.current = false;
2155
+ onBack();
2156
+ }
2157
+ }, [phase, isActive, onBack]);
2158
+ if (isActive || phase === "error") {
2159
+ return /* @__PURE__ */ jsx9(
2160
+ InterfaceRunningView,
2161
+ {
2162
+ name,
2163
+ phase,
2164
+ outputLines,
2165
+ errorMessage,
2166
+ localPath,
2167
+ onStop: stop,
2168
+ onBack
2169
+ }
2170
+ );
2171
+ }
2172
+ return /* @__PURE__ */ jsx9(
2173
+ InterfaceSessionView,
2174
+ {
2175
+ item,
2176
+ onStart: start,
2177
+ onDelete: deleteLocalCopy,
2178
+ onBack,
2179
+ hasLocalCopy,
2180
+ localPath
2181
+ }
2182
+ );
2183
+ }
2184
+ function InterfacesPage({
2185
+ onBack,
2186
+ sessions,
2187
+ refreshStatus,
2188
+ refresh
2189
+ }) {
2190
+ const [selectedAppId, setSelectedAppId] = useState13(null);
2191
+ const [selectedItem, setSelectedItem] = useState13(null);
2192
+ if (selectedItem) {
2193
+ return /* @__PURE__ */ jsx9(LocalDevView, { item: selectedItem, onBack: () => setSelectedItem(null) });
2194
+ }
2195
+ if (selectedAppId) {
2196
+ const session = sessions.find((s) => s.appId === selectedAppId);
2197
+ if (session) {
2198
+ return /* @__PURE__ */ jsx9(
2199
+ AgentDetailView,
2200
+ {
2201
+ session,
2202
+ onBack: () => setSelectedAppId(null),
2203
+ onSelect: (item) => {
2204
+ setSelectedItem(item);
2205
+ },
2206
+ onRefresh: refresh,
2207
+ refreshStatus
2208
+ }
2209
+ );
2210
+ }
2211
+ }
2212
+ return /* @__PURE__ */ jsx9(
2213
+ AgentListView,
2214
+ {
2215
+ sessions,
2216
+ onBack,
2217
+ onSelectAgent: setSelectedAppId,
2218
+ onRefresh: refresh,
2219
+ refreshStatus
2220
+ }
2221
+ );
2222
+ }
2223
+
2224
+ // src/tui/pages/OnboardingPage.tsx
2225
+ import { useEffect as useEffect15, useCallback as useCallback9, useState as useState15, useMemo as useMemo7 } from "react";
2226
+ import { Box as Box9, Text as Text10, useInput as useInput6 } from "ink";
2227
+ import Spinner5 from "ink-spinner";
2228
+ import chalk3 from "chalk";
2229
+
2230
+ // src/tui/hooks/useAuth.ts
2231
+ import { useState as useState14, useCallback as useCallback8, useRef as useRef8, useEffect as useEffect14 } from "react";
2232
+ import open3 from "open";
2233
+ var POLL_INTERVAL = 2e3;
2234
+ var MAX_ATTEMPTS = 30;
2235
+ function useAuth() {
2236
+ const [status, setStatus] = useState14("idle");
2237
+ const [authUrl, setAuthUrl] = useState14(null);
2238
+ const [timeRemaining, setTimeRemaining] = useState14(0);
2239
+ const cancelledRef = useRef8(false);
2240
+ const timerRef = useRef8(null);
2241
+ useEffect14(() => {
2242
+ return () => {
2243
+ cancelledRef.current = true;
2244
+ if (timerRef.current) clearInterval(timerRef.current);
2245
+ };
2246
+ }, []);
2247
+ const cancel = useCallback8(() => {
2248
+ cancelledRef.current = true;
2249
+ if (timerRef.current) {
2250
+ clearInterval(timerRef.current);
2251
+ timerRef.current = null;
2252
+ }
2253
+ setStatus("idle");
2254
+ setAuthUrl(null);
2255
+ setTimeRemaining(0);
2256
+ }, []);
2257
+ const startAuth = useCallback8(() => {
2258
+ cancelledRef.current = false;
2259
+ const run = async () => {
2260
+ try {
2261
+ const { url, token } = await requestDeviceAuth();
2262
+ if (cancelledRef.current) return;
2263
+ setAuthUrl(url);
2264
+ setStatus("waiting");
2265
+ const totalTime = MAX_ATTEMPTS * POLL_INTERVAL / 1e3;
2266
+ setTimeRemaining(totalTime);
2267
+ timerRef.current = setInterval(() => {
2268
+ setTimeRemaining((prev) => {
2269
+ const next = prev - 1;
2270
+ if (next <= 0 && timerRef.current) {
2271
+ clearInterval(timerRef.current);
2272
+ timerRef.current = null;
2273
+ }
2274
+ return Math.max(0, next);
2275
+ });
2276
+ }, 1e3);
2277
+ await open3(url);
2278
+ for (let i = 0; i < MAX_ATTEMPTS; i++) {
2279
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL));
2280
+ if (cancelledRef.current) return;
2281
+ const result = await pollDeviceAuth(token);
2282
+ if (result.status === "completed" && result.apiKey) {
2283
+ if (timerRef.current) clearInterval(timerRef.current);
2284
+ setApiKey(result.apiKey);
2285
+ if (result.userId) {
2286
+ setUserId(result.userId);
2287
+ }
2288
+ setStatus("success");
2289
+ return;
2290
+ }
2291
+ if (result.status === "expired") {
2292
+ if (timerRef.current) clearInterval(timerRef.current);
2293
+ setStatus("expired");
2294
+ return;
2295
+ }
2296
+ }
2297
+ if (!cancelledRef.current) {
2298
+ setStatus("timeout");
2299
+ }
2300
+ } catch {
2301
+ if (!cancelledRef.current) {
2302
+ setStatus("expired");
2303
+ }
2304
+ }
2305
+ };
2306
+ run();
2307
+ }, []);
2308
+ return {
2309
+ status,
2310
+ authUrl,
2311
+ timeRemaining,
2312
+ startAuth,
2313
+ cancel
2314
+ };
2315
+ }
2316
+
2317
+ // src/tui/pages/OnboardingPage.tsx
2318
+ import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
2319
+ var SHIMMER_SPEED = 35;
2320
+ function useShimmerLogo() {
2321
+ const [frame, setFrame] = useState15(0);
2322
+ const lines = useMemo7(() => LogoString.split("\n"), []);
2323
+ const totalChars = useMemo7(() => {
2324
+ let count = 0;
2325
+ for (const line of lines) {
2326
+ for (const ch of line) {
2327
+ if (ch !== " " && ch !== " ") count++;
2328
+ }
2329
+ }
2330
+ return count;
2331
+ }, [lines]);
2332
+ const cycleLength = totalChars + 40;
2333
+ useEffect15(() => {
2334
+ const interval = setInterval(() => {
2335
+ setFrame((f) => (f + 1) % cycleLength);
2336
+ }, SHIMMER_SPEED);
2337
+ return () => clearInterval(interval);
2338
+ }, [cycleLength]);
2339
+ return useMemo7(() => {
2340
+ const sweepPos = frame;
2341
+ const holdEnd = totalChars + 20;
2342
+ let charIdx = 0;
2343
+ return lines.map((line) => {
2344
+ let result = "";
2345
+ for (let i = 0; i < line.length; i++) {
2346
+ const ch = line[i];
2347
+ if (ch === " " || ch === " ") {
2348
+ result += ch;
2349
+ continue;
2350
+ }
2351
+ let brightness;
2352
+ if (sweepPos <= totalChars) {
2353
+ const lag = charIdx;
2354
+ const t = sweepPos - lag;
2355
+ brightness = t <= 0 ? 0.1 : Math.min(1, t / 8);
2356
+ } else if (sweepPos <= holdEnd) {
2357
+ brightness = 1;
2358
+ } else {
2359
+ const fadeProgress = (sweepPos - holdEnd) / (cycleLength - holdEnd);
2360
+ brightness = Math.max(0.1, 1 - fadeProgress);
2361
+ }
2362
+ if (brightness >= 0.9) {
2363
+ result += chalk3.cyanBright.bold(ch);
2364
+ } else if (brightness >= 0.6) {
2365
+ result += chalk3.cyan(ch);
2366
+ } else if (brightness >= 0.3) {
2367
+ result += chalk3.rgb(0, 100, 120)(ch);
2368
+ } else {
2369
+ result += chalk3.rgb(0, 50, 60)(ch);
2370
+ }
2371
+ charIdx++;
2372
+ }
2373
+ return result;
2374
+ }).join("\n");
2375
+ }, [frame, lines, totalChars, cycleLength]);
2376
+ }
2377
+ function OnboardingPage({ onComplete }) {
2378
+ const {
2379
+ status: authStatus,
2380
+ authUrl,
2381
+ timeRemaining,
2382
+ startAuth,
2383
+ cancel: cancelAuth
2384
+ } = useAuth();
2385
+ const shimmerLogo = useShimmerLogo();
2386
+ useEffect15(() => {
2387
+ if (authStatus === "success") {
2388
+ const timer = setTimeout(() => onComplete(), 1500);
2389
+ return () => clearTimeout(timer);
2390
+ }
2391
+ }, [authStatus, onComplete]);
2392
+ useEffect15(() => {
2393
+ return () => cancelAuth();
2394
+ }, []);
2395
+ const handleAction = useCallback9(() => {
2396
+ cancelAuth();
2397
+ startAuth();
2398
+ }, [cancelAuth, startAuth]);
2399
+ const canAct = authStatus === "idle" || authStatus === "expired" || authStatus === "timeout";
2400
+ useInput6((_input, key) => {
2401
+ if (canAct && !key.ctrl) {
2402
+ handleAction();
2403
+ }
2404
+ });
2405
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", flexGrow: 1, children: [
2406
+ /* @__PURE__ */ jsx10(Box9, { flexGrow: 1 }),
2407
+ /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", alignItems: "center", children: [
2408
+ /* @__PURE__ */ jsx10(Text10, { children: shimmerLogo }),
2409
+ /* @__PURE__ */ jsx10(Box9, { flexDirection: "column", alignItems: "center", marginTop: 2, children: /* @__PURE__ */ jsx10(Text10, { bold: true, color: "white", children: "MindStudio Local Tunnel" }) }),
2410
+ /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", alignItems: "center", children: [
2411
+ authStatus === "idle" && /* @__PURE__ */ jsxs9(Fragment4, { children: [
2412
+ /* @__PURE__ */ jsx10(Text10, { color: "gray", children: "Connect your MindStudio account to get started." }),
2413
+ /* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: "Press any key to Connect Account" }) })
2414
+ ] }),
2415
+ (authStatus === "expired" || authStatus === "timeout") && /* @__PURE__ */ jsxs9(Fragment4, { children: [
2416
+ /* @__PURE__ */ jsx10(Text10, { color: "red", children: authStatus === "expired" ? "Authorization expired." : "Authorization timed out." }),
2417
+ /* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: "Press any key to Try Again" }) })
2418
+ ] }),
2419
+ authStatus === "waiting" && /* @__PURE__ */ jsxs9(Fragment4, { children: [
2420
+ /* @__PURE__ */ jsxs9(Box9, { children: [
2421
+ /* @__PURE__ */ jsx10(Text10, { color: "cyan", children: /* @__PURE__ */ jsx10(Spinner5, { type: "dots" }) }),
2422
+ /* @__PURE__ */ jsxs9(Text10, { children: [
2423
+ " ",
2424
+ "Waiting for browser authorization... (",
2425
+ timeRemaining,
2426
+ "s remaining)"
2427
+ ] })
2428
+ ] }),
2429
+ authUrl && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", alignItems: "center", marginTop: 1, children: [
2430
+ /* @__PURE__ */ jsx10(Text10, { color: "gray", children: "If browser didn't open, visit:" }),
2431
+ /* @__PURE__ */ jsx10(Text10, { color: "cyan", children: authUrl })
2432
+ ] })
2433
+ ] }),
2434
+ authStatus === "success" && /* @__PURE__ */ jsxs9(Text10, { color: "green", children: [
2435
+ "\u2713",
2436
+ " Authenticated!"
2437
+ ] })
2438
+ ] })
2439
+ ] }),
2440
+ /* @__PURE__ */ jsx10(Box9, { flexGrow: 1 })
2441
+ ] });
2442
+ }
2443
+
2444
+ // src/tui/App.tsx
2445
+ import { Fragment as Fragment5, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
2446
+ var MODEL_TYPE_MAP = {
2447
+ text: "llm_chat",
2448
+ image: "image_generation",
2449
+ video: "video_generation"
2450
+ };
2451
+ function App({ runner }) {
2452
+ const { exit } = useApp();
2453
+ const { stdout } = useStdout7();
2454
+ const {
2455
+ status: connectionStatus,
2456
+ environment,
2457
+ error: connectionError,
2458
+ retry: retryConnection
2459
+ } = useConnection();
2460
+ const editorSessions = useEditorSessions();
2461
+ const {
2462
+ providers,
2463
+ loading: providersLoading,
2464
+ refresh: refreshProviders
2465
+ } = useSetupProviders();
2466
+ const {
2467
+ models,
2468
+ warnings: modelWarnings,
2469
+ loading: modelsLoading,
2470
+ refresh: refreshModels
2471
+ } = useModels();
2472
+ const { requests, activeCount: activeRequestCount } = useRequests();
2473
+ const {
2474
+ syncedNames,
2475
+ syncedModels,
2476
+ refresh: refreshSynced
2477
+ } = useSyncedModels(connectionStatus);
2478
+ const shouldOnboard = getApiKey() === void 0 || getUserId() === void 0;
2479
+ const [page, setPage] = useState16(
2480
+ shouldOnboard ? "onboarding" : "dashboard"
2481
+ );
2482
+ const [syncStatus, setSyncStatus] = useState16(
2483
+ "idle"
2484
+ );
2485
+ const syncTimerRef = useRef9(void 0);
2486
+ const lastSyncPayloadRef = useRef9("");
2487
+ useEffect16(() => {
2488
+ if (connectionStatus === "not_authenticated") {
2489
+ setPage("onboarding");
2490
+ }
2491
+ }, [connectionStatus]);
2492
+ useEffect16(() => {
2493
+ if (page === "dashboard") {
2494
+ refreshAll();
2495
+ }
2496
+ }, [page]);
2497
+ useEffect16(() => {
2498
+ if (connectionStatus === "connected" && syncedModels.length > 0) {
2499
+ runner.start(syncedModels);
2500
+ }
2501
+ }, [connectionStatus, syncedModels, runner]);
2502
+ useEffect16(() => () => runner.stop(), [runner]);
2503
+ const refreshAll = useCallback10(
2504
+ async (silent = false) => {
2505
+ if (!silent) setSyncStatus("syncing");
2506
+ const [discoveredModels] = await Promise.all([
2507
+ refreshModels(),
2508
+ refreshProviders()
2509
+ ]);
2510
+ const modelsToSync = discoveredModels.filter((m) => !m.statusHint).map((m) => ({
2511
+ name: m.name,
2512
+ provider: m.provider,
2513
+ type: MODEL_TYPE_MAP[m.capability] || "llm_chat",
2514
+ parameters: m.parameters
2515
+ }));
2516
+ const payload = JSON.stringify(modelsToSync);
2517
+ if (payload !== lastSyncPayloadRef.current && modelsToSync.length > 0) {
2518
+ try {
2519
+ await syncModels(modelsToSync);
2520
+ lastSyncPayloadRef.current = payload;
2521
+ } catch {
2522
+ }
2523
+ }
2524
+ await refreshSynced();
2525
+ if (!silent) {
2526
+ setSyncStatus("synced");
2527
+ clearTimeout(syncTimerRef.current);
2528
+ syncTimerRef.current = setTimeout(() => setSyncStatus("idle"), 1500);
2529
+ }
2530
+ },
2531
+ [refreshProviders, refreshModels, refreshSynced]
2532
+ );
2533
+ useEffect16(() => {
2534
+ if (connectionStatus !== "connected" || page !== "dashboard") return;
2535
+ const interval = setInterval(() => refreshAll(true), 1500);
2536
+ return () => clearInterval(interval);
2537
+ }, [connectionStatus, page, refreshAll]);
2538
+ const handleQuit = useCallback10(() => {
2539
+ runner.stop();
2540
+ exit();
2541
+ }, [runner, exit]);
2542
+ const handleOnboardingComplete = useCallback10(() => {
2543
+ retryConnection();
2544
+ refreshAll();
2545
+ setPage("dashboard");
2546
+ }, [retryConnection, refreshAll]);
2547
+ const handleNavigate = useCallback10(
2548
+ (id) => {
2549
+ switch (id) {
2550
+ case "interfaces":
2551
+ setPage("interfaces");
2552
+ break;
2553
+ case "auth":
2554
+ setPage("onboarding");
2555
+ break;
2556
+ case "setup":
2557
+ setPage("setup");
2558
+ break;
2559
+ case "refresh":
2560
+ refreshAll();
2561
+ break;
2562
+ case "quit":
2563
+ handleQuit();
2564
+ break;
2565
+ }
2566
+ },
2567
+ [refreshAll, handleQuit]
2568
+ );
2569
+ const subpageMenuItems = [
2570
+ { id: "back", label: "Back", description: "Return to dashboard" }
2571
+ ];
2572
+ const handleSubpageNavigate = useCallback10(
2573
+ (id) => {
2574
+ if (id === "back") {
2575
+ setPage("dashboard");
2576
+ } else {
2577
+ handleNavigate(id);
2578
+ }
2579
+ },
2580
+ [handleNavigate]
2581
+ );
2582
+ const [termSize, setTermSize] = useState16({
2583
+ rows: stdout?.rows ?? 24,
2584
+ columns: stdout?.columns ?? 80
2585
+ });
2586
+ useEffect16(() => {
2587
+ if (!stdout) return;
2588
+ const onResize = () => {
2589
+ stdout.write("\x1B[2J\x1B[H");
2590
+ setTermSize({ rows: stdout.rows, columns: stdout.columns });
2591
+ };
2592
+ stdout.on("resize", onResize);
2593
+ return () => {
2594
+ stdout.off("resize", onResize);
2595
+ };
2596
+ }, [stdout]);
2597
+ const termHeight = termSize.rows - 4;
2598
+ const compactHeader = termSize.rows <= 45 || termSize.columns <= 90;
2599
+ return /* @__PURE__ */ jsx11(Box10, { flexDirection: "column", height: termHeight, overflow: "hidden", children: page === "onboarding" ? /* @__PURE__ */ jsx11(OnboardingPage, { onComplete: handleOnboardingComplete }) : /* @__PURE__ */ jsxs10(Fragment5, { children: [
2600
+ /* @__PURE__ */ jsx11(
2601
+ Header,
2602
+ {
2603
+ connection: connectionStatus,
2604
+ environment,
2605
+ configPath: getConfigPath(),
2606
+ connectionError,
2607
+ compact: compactHeader,
2608
+ hasActiveRequest: activeRequestCount > 0
2609
+ }
2610
+ ),
2611
+ page === "dashboard" && /* @__PURE__ */ jsx11(
2612
+ DashboardPage,
2613
+ {
2614
+ requests,
2615
+ models,
2616
+ modelWarnings,
2617
+ providers,
2618
+ providersLoading,
2619
+ syncedNames,
2620
+ modelsLoading,
2621
+ syncStatus,
2622
+ editorSessions: editorSessions.sessions,
2623
+ editorsLoading: editorSessions.loading,
2624
+ onNavigate: handleNavigate
2625
+ }
2626
+ ),
2627
+ page === "setup" && /* @__PURE__ */ jsx11(SetupPage, { onBack: () => setPage("dashboard") }),
2628
+ page === "interfaces" && /* @__PURE__ */ jsx11(
2629
+ InterfacesPage,
2630
+ {
2631
+ onBack: () => setPage("dashboard"),
2632
+ sessions: editorSessions.sessions,
2633
+ refreshStatus: editorSessions.refreshStatus,
2634
+ refresh: editorSessions.refresh
2635
+ }
2636
+ ),
2637
+ page !== "dashboard" && page !== "setup" && page !== "interfaces" && /* @__PURE__ */ jsx11(Box10, { flexGrow: 1 }),
2638
+ page !== "dashboard" && page !== "setup" && page !== "interfaces" && /* @__PURE__ */ jsx11(
2639
+ NavigationMenu,
2640
+ {
2641
+ items: subpageMenuItems,
2642
+ onSelect: handleSubpageNavigate
2643
+ }
2644
+ )
2645
+ ] }) });
2646
+ }
2647
+
2648
+ // src/update.ts
2649
+ import { createRequire as createRequire2 } from "module";
2650
+ var require3 = createRequire2(import.meta.url);
2651
+ var pkg2 = require3("../package.json");
2652
+ function getCurrentVersion() {
2653
+ return pkg2.version;
2654
+ }
2655
+ async function fetchLatestVersion() {
2656
+ try {
2657
+ const res = await fetch(
2658
+ "https://registry.npmjs.org/@mindstudio-ai/local-model-tunnel/latest",
2659
+ { signal: AbortSignal.timeout(5e3) }
2660
+ );
2661
+ if (!res.ok) return null;
2662
+ const data = await res.json();
2663
+ return data.version ?? null;
2664
+ } catch {
2665
+ return null;
2666
+ }
2667
+ }
2668
+ function isNewerVersion(current, latest) {
2669
+ const currentParts = current.split(".").map(Number);
2670
+ const latestParts = latest.split(".").map(Number);
2671
+ for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
2672
+ const c = currentParts[i] ?? 0;
2673
+ const l = latestParts[i] ?? 0;
2674
+ if (l > c) return true;
2675
+ if (l < c) return false;
2676
+ }
2677
+ return false;
2678
+ }
2679
+ async function checkForUpdate() {
2680
+ const currentVersion = getCurrentVersion();
2681
+ const latestVersion = await fetchLatestVersion();
2682
+ if (!latestVersion) return null;
2683
+ if (!isNewerVersion(currentVersion, latestVersion)) return null;
2684
+ return { currentVersion, latestVersion };
2685
+ }
2686
+
2687
+ // src/tui/components/UpdatePrompt.tsx
2688
+ import { useState as useState17, useEffect as useEffect17, useMemo as useMemo8 } from "react";
2689
+ import { Box as Box11, Text as Text11, useInput as useInput7, useStdout as useStdout8 } from "ink";
2690
+ import chalk4 from "chalk";
2691
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
2692
+ var SHIMMER_SPEED2 = 35;
2693
+ function useShimmerLogo2() {
2694
+ const [frame, setFrame] = useState17(0);
2695
+ const lines = useMemo8(() => LogoString.split("\n"), []);
2696
+ const totalChars = useMemo8(() => {
2697
+ let count = 0;
2698
+ for (const line of lines) {
2699
+ for (const ch of line) {
2700
+ if (ch !== " " && ch !== " ") count++;
2701
+ }
2702
+ }
2703
+ return count;
2704
+ }, [lines]);
2705
+ const cycleLength = totalChars + 40;
2706
+ useEffect17(() => {
2707
+ const interval = setInterval(() => {
2708
+ setFrame((f) => (f + 1) % cycleLength);
2709
+ }, SHIMMER_SPEED2);
2710
+ return () => clearInterval(interval);
2711
+ }, [cycleLength]);
2712
+ return useMemo8(() => {
2713
+ const sweepPos = frame;
2714
+ const holdEnd = totalChars + 20;
2715
+ let charIdx = 0;
2716
+ return lines.map((line) => {
2717
+ let result = "";
2718
+ for (let i = 0; i < line.length; i++) {
2719
+ const ch = line[i];
2720
+ if (ch === " " || ch === " ") {
2721
+ result += ch;
2722
+ continue;
2723
+ }
2724
+ let brightness;
2725
+ if (sweepPos <= totalChars) {
2726
+ const lag = charIdx;
2727
+ const t = sweepPos - lag;
2728
+ brightness = t <= 0 ? 0.1 : Math.min(1, t / 8);
2729
+ } else if (sweepPos <= holdEnd) {
2730
+ brightness = 1;
2731
+ } else {
2732
+ const fadeProgress = (sweepPos - holdEnd) / (cycleLength - holdEnd);
2733
+ brightness = Math.max(0.1, 1 - fadeProgress);
2734
+ }
2735
+ if (brightness >= 0.9) {
2736
+ result += chalk4.cyanBright.bold(ch);
2737
+ } else if (brightness >= 0.6) {
2738
+ result += chalk4.cyan(ch);
2739
+ } else if (brightness >= 0.3) {
2740
+ result += chalk4.rgb(0, 100, 120)(ch);
2741
+ } else {
2742
+ result += chalk4.rgb(0, 50, 60)(ch);
2743
+ }
2744
+ charIdx++;
2745
+ }
2746
+ return result;
2747
+ }).join("\n");
2748
+ }, [frame, lines, totalChars, cycleLength]);
2749
+ }
2750
+ function UpdatePrompt({
2751
+ currentVersion,
2752
+ latestVersion,
2753
+ onChoice
2754
+ }) {
2755
+ const { stdout } = useStdout8();
2756
+ const shimmerLogo = useShimmerLogo2();
2757
+ useInput7(() => {
2758
+ onChoice(true);
2759
+ });
2760
+ const termHeight = (stdout?.rows ?? 24) - 4;
2761
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", height: termHeight, children: [
2762
+ /* @__PURE__ */ jsx12(Box11, { flexGrow: 1 }),
2763
+ /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", alignItems: "center", children: [
2764
+ /* @__PURE__ */ jsx12(Text11, { children: shimmerLogo }),
2765
+ /* @__PURE__ */ jsx12(Box11, { flexDirection: "column", alignItems: "center", marginTop: 2, children: /* @__PURE__ */ jsx12(Text11, { bold: true, color: "white", children: "MindStudio Local Tunnel" }) }),
2766
+ /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", alignItems: "center", children: [
2767
+ /* @__PURE__ */ jsxs11(Text11, { color: "gray", children: [
2768
+ "Update required ",
2769
+ "\u2022",
2770
+ " v",
2771
+ currentVersion,
2772
+ " ",
2773
+ "\u2192",
2774
+ " v",
2775
+ latestVersion
2776
+ ] }),
2777
+ /* @__PURE__ */ jsx12(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text11, { color: "cyan", bold: true, children: "Press any key to update" }) })
2778
+ ] })
2779
+ ] }),
2780
+ /* @__PURE__ */ jsx12(Box11, { flexGrow: 1 })
2781
+ ] });
2782
+ }
2783
+
2784
+ // src/tui/index.tsx
2785
+ import { jsx as jsx13 } from "react/jsx-runtime";
2786
+ async function promptForUpdate(currentVersion, latestVersion) {
2787
+ return new Promise((resolve) => {
2788
+ const { unmount } = render(
2789
+ /* @__PURE__ */ jsx13(
2790
+ UpdatePrompt,
2791
+ {
2792
+ currentVersion,
2793
+ latestVersion,
2794
+ onChoice: (shouldUpdate) => {
2795
+ unmount();
2796
+ resolve(shouldUpdate);
2797
+ }
2798
+ }
2799
+ ),
2800
+ { exitOnCtrlC: true }
2801
+ );
2802
+ });
2803
+ }
2804
+ async function startTUI() {
2805
+ console.clear();
2806
+ const update = await checkForUpdate();
2807
+ if (update) {
2808
+ const shouldUpdate = await promptForUpdate(
2809
+ update.currentVersion,
2810
+ update.latestVersion
2811
+ );
2812
+ if (shouldUpdate) {
2813
+ console.log("\nUpdating to v" + update.latestVersion + "...\n");
2814
+ try {
2815
+ execSync2("npm install -g @mindstudio-ai/local-model-tunnel@latest", {
2816
+ stdio: "inherit"
2817
+ });
2818
+ console.log("\nRestarting...\n");
2819
+ execFileSync(process.execPath, process.argv.slice(1), {
2820
+ stdio: "inherit"
2821
+ });
2822
+ } catch {
2823
+ console.error("\nUpdate failed. Continuing with current version.\n");
2824
+ }
2825
+ return;
2826
+ }
2827
+ console.clear();
2828
+ }
2829
+ const runner = new TunnelRunner();
2830
+ const { waitUntilExit } = render(/* @__PURE__ */ jsx13(App, { runner }), {
2831
+ exitOnCtrlC: true
2832
+ });
2833
+ await waitUntilExit();
2834
+ runner.stop();
2835
+ }
2836
+ export {
2837
+ startTUI
2838
+ };
2839
+ //# sourceMappingURL=tui-EAU7OA4X.js.map