@mindstudio-ai/local-model-tunnel 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +30 -115
  2. package/dist/chunk-PTK4SJQK.js +1768 -0
  3. package/dist/chunk-PTK4SJQK.js.map +1 -0
  4. package/dist/cli.d.ts +0 -2
  5. package/dist/cli.js +8 -517
  6. package/dist/cli.js.map +1 -1
  7. package/dist/index.d.ts +24 -5
  8. package/dist/index.js +6 -13
  9. package/dist/index.js.map +1 -1
  10. package/dist/tui-56JFPKBP.js +1561 -0
  11. package/dist/tui-56JFPKBP.js.map +1 -0
  12. package/package.json +11 -4
  13. package/dist/api.d.ts +0 -88
  14. package/dist/api.d.ts.map +0 -1
  15. package/dist/api.js +0 -168
  16. package/dist/api.js.map +0 -1
  17. package/dist/cli.d.ts.map +0 -1
  18. package/dist/config.d.ts +0 -27
  19. package/dist/config.d.ts.map +0 -1
  20. package/dist/config.js +0 -109
  21. package/dist/config.js.map +0 -1
  22. package/dist/helpers.d.ts +0 -4
  23. package/dist/helpers.d.ts.map +0 -1
  24. package/dist/helpers.js +0 -33
  25. package/dist/helpers.js.map +0 -1
  26. package/dist/index.d.ts.map +0 -1
  27. package/dist/ollama.d.ts +0 -11
  28. package/dist/ollama.d.ts.map +0 -1
  29. package/dist/ollama.js +0 -36
  30. package/dist/ollama.js.map +0 -1
  31. package/dist/providers/comfyui.d.ts +0 -29
  32. package/dist/providers/comfyui.d.ts.map +0 -1
  33. package/dist/providers/comfyui.js +0 -359
  34. package/dist/providers/comfyui.js.map +0 -1
  35. package/dist/providers/index.d.ts +0 -63
  36. package/dist/providers/index.d.ts.map +0 -1
  37. package/dist/providers/index.js +0 -126
  38. package/dist/providers/index.js.map +0 -1
  39. package/dist/providers/lmstudio.d.ts +0 -11
  40. package/dist/providers/lmstudio.d.ts.map +0 -1
  41. package/dist/providers/lmstudio.js +0 -106
  42. package/dist/providers/lmstudio.js.map +0 -1
  43. package/dist/providers/ollama.d.ts +0 -11
  44. package/dist/providers/ollama.d.ts.map +0 -1
  45. package/dist/providers/ollama.js +0 -59
  46. package/dist/providers/ollama.js.map +0 -1
  47. package/dist/providers/stable-diffusion.d.ts +0 -41
  48. package/dist/providers/stable-diffusion.d.ts.map +0 -1
  49. package/dist/providers/stable-diffusion.js +0 -283
  50. package/dist/providers/stable-diffusion.js.map +0 -1
  51. package/dist/providers/types.d.ts +0 -196
  52. package/dist/providers/types.d.ts.map +0 -1
  53. package/dist/providers/types.js +0 -19
  54. package/dist/providers/types.js.map +0 -1
  55. package/dist/quickstart/QuickstartScreen.d.ts +0 -5
  56. package/dist/quickstart/QuickstartScreen.d.ts.map +0 -1
  57. package/dist/quickstart/QuickstartScreen.js +0 -617
  58. package/dist/quickstart/QuickstartScreen.js.map +0 -1
  59. package/dist/quickstart/detect.d.ts +0 -22
  60. package/dist/quickstart/detect.d.ts.map +0 -1
  61. package/dist/quickstart/detect.js +0 -243
  62. package/dist/quickstart/detect.js.map +0 -1
  63. package/dist/quickstart/index.d.ts +0 -4
  64. package/dist/quickstart/index.d.ts.map +0 -1
  65. package/dist/quickstart/index.js +0 -274
  66. package/dist/quickstart/index.js.map +0 -1
  67. package/dist/quickstart/installers.d.ts +0 -109
  68. package/dist/quickstart/installers.d.ts.map +0 -1
  69. package/dist/quickstart/installers.js +0 -1296
  70. package/dist/quickstart/installers.js.map +0 -1
  71. package/dist/runner.d.ts +0 -19
  72. package/dist/runner.d.ts.map +0 -1
  73. package/dist/runner.js +0 -314
  74. package/dist/runner.js.map +0 -1
  75. package/dist/tui/App.d.ts +0 -7
  76. package/dist/tui/App.d.ts.map +0 -1
  77. package/dist/tui/App.js +0 -53
  78. package/dist/tui/App.js.map +0 -1
  79. package/dist/tui/TunnelRunner.d.ts +0 -19
  80. package/dist/tui/TunnelRunner.d.ts.map +0 -1
  81. package/dist/tui/TunnelRunner.js +0 -228
  82. package/dist/tui/TunnelRunner.js.map +0 -1
  83. package/dist/tui/components/Header.d.ts +0 -9
  84. package/dist/tui/components/Header.d.ts.map +0 -1
  85. package/dist/tui/components/Header.js +0 -21
  86. package/dist/tui/components/Header.js.map +0 -1
  87. package/dist/tui/components/ModelsPanel.d.ts +0 -7
  88. package/dist/tui/components/ModelsPanel.d.ts.map +0 -1
  89. package/dist/tui/components/ModelsPanel.js +0 -28
  90. package/dist/tui/components/ModelsPanel.js.map +0 -1
  91. package/dist/tui/components/ProvidersPanel.d.ts +0 -7
  92. package/dist/tui/components/ProvidersPanel.d.ts.map +0 -1
  93. package/dist/tui/components/ProvidersPanel.js +0 -6
  94. package/dist/tui/components/ProvidersPanel.js.map +0 -1
  95. package/dist/tui/components/RequestLog.d.ts +0 -8
  96. package/dist/tui/components/RequestLog.d.ts.map +0 -1
  97. package/dist/tui/components/RequestLog.js +0 -60
  98. package/dist/tui/components/RequestLog.js.map +0 -1
  99. package/dist/tui/components/StatusBar.d.ts +0 -10
  100. package/dist/tui/components/StatusBar.d.ts.map +0 -1
  101. package/dist/tui/components/StatusBar.js +0 -7
  102. package/dist/tui/components/StatusBar.js.map +0 -1
  103. package/dist/tui/components/index.d.ts +0 -6
  104. package/dist/tui/components/index.d.ts.map +0 -1
  105. package/dist/tui/components/index.js +0 -6
  106. package/dist/tui/components/index.js.map +0 -1
  107. package/dist/tui/events.d.ts +0 -35
  108. package/dist/tui/events.d.ts.map +0 -1
  109. package/dist/tui/events.js +0 -26
  110. package/dist/tui/events.js.map +0 -1
  111. package/dist/tui/hooks/index.d.ts +0 -5
  112. package/dist/tui/hooks/index.d.ts.map +0 -1
  113. package/dist/tui/hooks/index.js +0 -5
  114. package/dist/tui/hooks/index.js.map +0 -1
  115. package/dist/tui/hooks/useConnection.d.ts +0 -10
  116. package/dist/tui/hooks/useConnection.d.ts.map +0 -1
  117. package/dist/tui/hooks/useConnection.js +0 -42
  118. package/dist/tui/hooks/useConnection.js.map +0 -1
  119. package/dist/tui/hooks/useModels.d.ts +0 -9
  120. package/dist/tui/hooks/useModels.d.ts.map +0 -1
  121. package/dist/tui/hooks/useModels.js +0 -28
  122. package/dist/tui/hooks/useModels.js.map +0 -1
  123. package/dist/tui/hooks/useProviders.d.ts +0 -9
  124. package/dist/tui/hooks/useProviders.d.ts.map +0 -1
  125. package/dist/tui/hooks/useProviders.js +0 -30
  126. package/dist/tui/hooks/useProviders.js.map +0 -1
  127. package/dist/tui/hooks/useRequests.d.ts +0 -9
  128. package/dist/tui/hooks/useRequests.d.ts.map +0 -1
  129. package/dist/tui/hooks/useRequests.js +0 -60
  130. package/dist/tui/hooks/useRequests.js.map +0 -1
  131. package/dist/tui/index.d.ts +0 -2
  132. package/dist/tui/index.d.ts.map +0 -1
  133. package/dist/tui/index.js +0 -19
  134. package/dist/tui/index.js.map +0 -1
  135. package/dist/tui/screens/ConfigScreen.d.ts +0 -2
  136. package/dist/tui/screens/ConfigScreen.d.ts.map +0 -1
  137. package/dist/tui/screens/ConfigScreen.js +0 -18
  138. package/dist/tui/screens/ConfigScreen.js.map +0 -1
  139. package/dist/tui/screens/HomeScreen.d.ts +0 -2
  140. package/dist/tui/screens/HomeScreen.d.ts.map +0 -1
  141. package/dist/tui/screens/HomeScreen.js +0 -156
  142. package/dist/tui/screens/HomeScreen.js.map +0 -1
  143. package/dist/tui/screens/ModelsScreen.d.ts +0 -2
  144. package/dist/tui/screens/ModelsScreen.d.ts.map +0 -1
  145. package/dist/tui/screens/ModelsScreen.js +0 -59
  146. package/dist/tui/screens/ModelsScreen.js.map +0 -1
  147. package/dist/tui/screens/StatusScreen.d.ts +0 -2
  148. package/dist/tui/screens/StatusScreen.d.ts.map +0 -1
  149. package/dist/tui/screens/StatusScreen.js +0 -53
  150. package/dist/tui/screens/StatusScreen.js.map +0 -1
  151. package/dist/tui/screens/index.d.ts +0 -9
  152. package/dist/tui/screens/index.d.ts.map +0 -1
  153. package/dist/tui/screens/index.js +0 -38
  154. package/dist/tui/screens/index.js.map +0 -1
  155. package/dist/tui/types.d.ts +0 -30
  156. package/dist/tui/types.d.ts.map +0 -1
  157. package/dist/tui/types.js +0 -2
  158. package/dist/tui/types.js.map +0 -1
  159. package/dist/workflows/index.d.ts +0 -47
  160. package/dist/workflows/index.d.ts.map +0 -1
  161. package/dist/workflows/index.js +0 -95
  162. package/dist/workflows/index.js.map +0 -1
  163. package/dist/workflows/ltx-video.d.ts +0 -45
  164. package/dist/workflows/ltx-video.d.ts.map +0 -1
  165. package/dist/workflows/ltx-video.js +0 -114
  166. package/dist/workflows/ltx-video.js.map +0 -1
  167. package/dist/workflows/wan2.1.d.ts +0 -44
  168. package/dist/workflows/wan2.1.d.ts.map +0 -1
  169. package/dist/workflows/wan2.1.js +0 -119
  170. package/dist/workflows/wan2.1.js.map +0 -1
@@ -0,0 +1,1561 @@
1
+ import {
2
+ TunnelRunner,
3
+ detectAllProviderStatuses,
4
+ discoverAllModels,
5
+ discoverAllModelsWithParameters,
6
+ getApiKey,
7
+ getConfigPath,
8
+ getEnvironment,
9
+ getProviderStatuses,
10
+ getRegisteredModels,
11
+ pollDeviceAuth,
12
+ registerLocalModel,
13
+ requestDeviceAuth,
14
+ requestEvents,
15
+ setApiKey,
16
+ verifyApiKey
17
+ } from "./chunk-PTK4SJQK.js";
18
+
19
+ // src/tui/index.tsx
20
+ import { render } from "ink";
21
+
22
+ // src/tui/App.tsx
23
+ import { useEffect as useEffect13, useCallback as useCallback10, useState as useState12 } from "react";
24
+ import { Box as Box8, useApp, useStdout as useStdout5 } from "ink";
25
+
26
+ // src/tui/components/Header.tsx
27
+ import os from "os";
28
+ import { Box, Text } from "ink";
29
+ import { createRequire } from "module";
30
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
31
+ var require2 = createRequire(import.meta.url);
32
+ var pkg = require2("../package.json");
33
+ var LogoString = ` .=+-. :++.
34
+ *@@@@@+ :%@@@@%:
35
+ .%@@@@@@#..@@@@@@@=
36
+ .*@@@@@@@--@@@@@@@#.**.
37
+ *@@@@@@@.-@@@@@@@@.#@@*
38
+ .#@@@@@@@-.@@@@@@@* #@@@@%.
39
+ =@@@@@@@-.@@@@@@@#.-@@@@@@+
40
+ :@@@@@@: +@@@@@#. .@@@@@@:
41
+ .++: .-*-. .++:`;
42
+ var getConnectionDisplay = (status) => {
43
+ switch (status) {
44
+ case "connected":
45
+ return { color: "green", text: "Connected to Cloud" };
46
+ case "connecting":
47
+ return { color: "yellow", text: "Connecting..." };
48
+ case "not_authenticated":
49
+ return { color: "yellow", text: "Not Authenticated" };
50
+ case "disconnected":
51
+ return { color: "red", text: "Disconnected" };
52
+ default:
53
+ return { color: "red", text: "Error" };
54
+ }
55
+ };
56
+ function Header({
57
+ connection,
58
+ environment,
59
+ configPath,
60
+ connectionError
61
+ }) {
62
+ const { color: connectionColor, text: connectionText } = getConnectionDisplay(connection);
63
+ return /* @__PURE__ */ jsxs(
64
+ Box,
65
+ {
66
+ flexDirection: "row",
67
+ alignItems: "center",
68
+ borderStyle: "round",
69
+ borderColor: "cyan",
70
+ paddingX: 1,
71
+ paddingY: 1,
72
+ width: "100%",
73
+ children: [
74
+ /* @__PURE__ */ jsx(Box, { paddingLeft: 3, children: /* @__PURE__ */ jsx(Text, { color: "cyan", children: LogoString }) }),
75
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 4, children: [
76
+ /* @__PURE__ */ jsxs(Box, { children: [
77
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "white", children: "MindStudio Local Tunnel" }),
78
+ environment !== "prod" && /* @__PURE__ */ jsxs(Fragment, { children: [
79
+ /* @__PURE__ */ jsx(Text, { children: " " }),
80
+ /* @__PURE__ */ jsx(Text, { color: "yellow", bold: true, children: "[LOCAL]" })
81
+ ] })
82
+ ] }),
83
+ /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
84
+ "v",
85
+ pkg.version
86
+ ] }),
87
+ /* @__PURE__ */ jsxs(Text, { color: connectionColor, children: [
88
+ "\u25CF ",
89
+ connectionText
90
+ ] }),
91
+ connectionError && /* @__PURE__ */ jsx(Text, { color: "red", children: connectionError }),
92
+ /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
93
+ "Config: ",
94
+ configPath.replace(os.homedir(), "~")
95
+ ] })
96
+ ] })
97
+ ]
98
+ }
99
+ );
100
+ }
101
+
102
+ // src/tui/components/NavigationMenu.tsx
103
+ import { useState, useEffect } from "react";
104
+ import { Box as Box2, Text as Text2, useInput } from "ink";
105
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
106
+ function NavigationMenu({ items, onSelect, title }) {
107
+ const getDefaultIndex = () => {
108
+ const backIdx = items.findIndex((i) => i.id === "back");
109
+ if (backIdx >= 0) return backIdx;
110
+ const firstIdx = items.findIndex((i) => !i.disabled && !i.isSeparator);
111
+ return firstIdx >= 0 ? firstIdx : 0;
112
+ };
113
+ const [selectedIndex, setSelectedIndex] = useState(getDefaultIndex);
114
+ useEffect(() => {
115
+ setSelectedIndex(getDefaultIndex());
116
+ }, [items]);
117
+ const findNextEnabled = (from, direction) => {
118
+ let idx = from;
119
+ for (let i = 0; i < items.length; i++) {
120
+ idx = (idx + direction + items.length) % items.length;
121
+ if (!items[idx].disabled && !items[idx].isSeparator) return idx;
122
+ }
123
+ return from;
124
+ };
125
+ useInput((input, key) => {
126
+ if (input === "q" || key.escape) {
127
+ const backItem = items.find((i) => i.id === "back");
128
+ if (backItem) {
129
+ onSelect("back");
130
+ } else if (input === "q") {
131
+ onSelect("quit");
132
+ }
133
+ return;
134
+ }
135
+ if (key.upArrow) {
136
+ setSelectedIndex((prev) => findNextEnabled(prev, -1));
137
+ } else if (key.downArrow) {
138
+ setSelectedIndex((prev) => findNextEnabled(prev, 1));
139
+ } else if (key.return) {
140
+ const item = items[selectedIndex];
141
+ if (item && !item.disabled) {
142
+ onSelect(item.id);
143
+ }
144
+ }
145
+ });
146
+ const separatorExtraLines = items.filter((item, idx) => item.isSeparator && idx > 0).length;
147
+ const menuHeight = items.length + 4 + separatorExtraLines;
148
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, marginBottom: 1, borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: "gray", children: [
149
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "gray", dimColor: true, children: title ?? "Actions" }) }),
150
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: items.map((item, index) => {
151
+ if (item.isSeparator) {
152
+ 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);
153
+ }
154
+ const isSelected = index === selectedIndex;
155
+ const prefix = isSelected ? "\u276F" : " ";
156
+ if (item.disabled) {
157
+ return /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsxs2(Text2, { color: "gray", wrap: "truncate-end", children: [
158
+ prefix,
159
+ " ",
160
+ item.label,
161
+ item.disabledReason ? ` (${item.disabledReason})` : ""
162
+ ] }) }, item.id);
163
+ }
164
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
165
+ /* @__PURE__ */ jsxs2(Text2, { color: isSelected ? "cyan" : "white", bold: isSelected, wrap: "truncate-end", children: [
166
+ prefix,
167
+ " ",
168
+ item.label
169
+ ] }),
170
+ isSelected && /* @__PURE__ */ jsxs2(Text2, { color: "gray", wrap: "truncate-end", children: [
171
+ " - ",
172
+ item.description
173
+ ] })
174
+ ] }, item.id);
175
+ }) }),
176
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, height: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "gray", dimColor: true, wrap: "truncate-end", children: items.some((i) => i.id === "back") ? "Up/Down Navigate \u2022 Enter Select \u2022 q/Esc Back" : "Up/Down Navigate \u2022 Enter Select \u2022 q Quit" }) })
177
+ ] });
178
+ }
179
+
180
+ // src/tui/hooks/useConnection.ts
181
+ import { useState as useState2, useEffect as useEffect2, useCallback } from "react";
182
+ function useConnection() {
183
+ const [status, setStatus] = useState2("connecting");
184
+ const [error, setError] = useState2(null);
185
+ const environment = getEnvironment();
186
+ const connect = useCallback(async () => {
187
+ setStatus("connecting");
188
+ setError(null);
189
+ const apiKey = getApiKey();
190
+ if (!apiKey) {
191
+ setStatus("not_authenticated");
192
+ return;
193
+ }
194
+ try {
195
+ const isValid = await verifyApiKey();
196
+ if (isValid) {
197
+ setStatus("connected");
198
+ } else {
199
+ setStatus("not_authenticated");
200
+ }
201
+ } catch (err) {
202
+ setStatus("error");
203
+ setError(err instanceof Error ? err.message : "Connection failed");
204
+ }
205
+ }, []);
206
+ useEffect2(() => {
207
+ connect();
208
+ }, [connect]);
209
+ return {
210
+ status,
211
+ environment,
212
+ error,
213
+ retry: connect
214
+ };
215
+ }
216
+
217
+ // src/tui/hooks/useProviders.ts
218
+ import { useState as useState3, useEffect as useEffect3, useCallback as useCallback2 } from "react";
219
+ function useProviders(pollInterval = 1e4) {
220
+ const [providers, setProviders] = useState3([]);
221
+ const [loading, setLoading] = useState3(true);
222
+ const refresh = useCallback2(async () => {
223
+ try {
224
+ const statuses = await getProviderStatuses();
225
+ setProviders(statuses);
226
+ } catch {
227
+ } finally {
228
+ setLoading(false);
229
+ }
230
+ }, []);
231
+ useEffect3(() => {
232
+ refresh();
233
+ const interval = setInterval(refresh, pollInterval);
234
+ return () => clearInterval(interval);
235
+ }, [refresh, pollInterval]);
236
+ return {
237
+ providers,
238
+ loading,
239
+ refresh
240
+ };
241
+ }
242
+
243
+ // src/tui/hooks/useModels.ts
244
+ import { useState as useState4, useEffect as useEffect4, useCallback as useCallback3 } from "react";
245
+ function useModels() {
246
+ const [models, setModels] = useState4([]);
247
+ const [loading, setLoading] = useState4(true);
248
+ const refresh = useCallback3(async () => {
249
+ setLoading(true);
250
+ try {
251
+ const discoveredModels = await discoverAllModels();
252
+ setModels(discoveredModels);
253
+ } catch {
254
+ } finally {
255
+ setLoading(false);
256
+ }
257
+ }, []);
258
+ useEffect4(() => {
259
+ refresh();
260
+ }, [refresh]);
261
+ return {
262
+ models,
263
+ loading,
264
+ refresh
265
+ };
266
+ }
267
+
268
+ // src/tui/hooks/useRequests.ts
269
+ import { useState as useState5, useEffect as useEffect5, useCallback as useCallback4, useRef } from "react";
270
+ function useRequests(maxHistory = 50) {
271
+ const [requests, setRequests] = useState5([]);
272
+ const requestsRef = useRef(/* @__PURE__ */ new Map());
273
+ useEffect5(() => {
274
+ const interval = setInterval(() => {
275
+ setRequests((prev) => {
276
+ const hasActive = prev.some((r) => r.status === "processing");
277
+ return hasActive ? [...prev] : prev;
278
+ });
279
+ }, 1e3);
280
+ return () => clearInterval(interval);
281
+ }, []);
282
+ useEffect5(() => {
283
+ const unsubStart = requestEvents.onStart((event) => {
284
+ const entry = {
285
+ id: event.id,
286
+ modelId: event.modelId,
287
+ requestType: event.requestType,
288
+ status: "processing",
289
+ startTime: event.timestamp
290
+ };
291
+ requestsRef.current.set(event.id, entry);
292
+ setRequests((prev) => [...prev, entry].slice(-maxHistory));
293
+ });
294
+ const unsubProgress = requestEvents.onProgress((event) => {
295
+ const existing = requestsRef.current.get(event.id);
296
+ if (existing && existing.status === "processing" && event.content) {
297
+ const updated = {
298
+ ...existing,
299
+ content: event.content
300
+ };
301
+ requestsRef.current.set(event.id, updated);
302
+ setRequests(
303
+ (prev) => prev.map((r) => r.id === event.id ? updated : r)
304
+ );
305
+ }
306
+ });
307
+ const unsubComplete = requestEvents.onComplete((event) => {
308
+ const existing = requestsRef.current.get(event.id);
309
+ if (existing) {
310
+ const updated = {
311
+ ...existing,
312
+ status: event.success ? "completed" : "failed",
313
+ endTime: Date.now(),
314
+ duration: event.duration,
315
+ result: event.result,
316
+ error: event.error
317
+ };
318
+ requestsRef.current.set(event.id, updated);
319
+ setRequests(
320
+ (prev) => prev.map((r) => r.id === event.id ? updated : r)
321
+ );
322
+ }
323
+ });
324
+ return () => {
325
+ unsubStart();
326
+ unsubProgress();
327
+ unsubComplete();
328
+ };
329
+ }, [maxHistory]);
330
+ const activeCount = requests.filter((r) => r.status === "processing").length;
331
+ const clear = useCallback4(() => {
332
+ requestsRef.current.clear();
333
+ setRequests([]);
334
+ }, []);
335
+ return {
336
+ requests,
337
+ activeCount,
338
+ clear
339
+ };
340
+ }
341
+
342
+ // src/tui/hooks/useRegisteredModels.ts
343
+ import { useState as useState6, useEffect as useEffect6, useCallback as useCallback5 } from "react";
344
+ function useRegisteredModels(connectionStatus) {
345
+ const [registeredNames, setRegisteredNames] = useState6(
346
+ /* @__PURE__ */ new Set()
347
+ );
348
+ const refresh = useCallback5(async () => {
349
+ if (connectionStatus !== "connected") {
350
+ setRegisteredNames(/* @__PURE__ */ new Set());
351
+ return;
352
+ }
353
+ try {
354
+ const models = await getRegisteredModels();
355
+ setRegisteredNames(new Set(models));
356
+ } catch {
357
+ }
358
+ }, [connectionStatus]);
359
+ useEffect6(() => {
360
+ refresh();
361
+ }, [refresh]);
362
+ return {
363
+ registeredNames,
364
+ refresh
365
+ };
366
+ }
367
+
368
+ // src/tui/pages/DashboardPage.tsx
369
+ import { useMemo } from "react";
370
+ import { Box as Box4, Text as Text4, useStdout as useStdout2 } from "ink";
371
+ import Spinner2 from "ink-spinner";
372
+
373
+ // src/tui/components/RequestLog.tsx
374
+ import { Box as Box3, Text as Text3, useStdout } from "ink";
375
+ import Spinner from "ink-spinner";
376
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
377
+ function formatTime(timestamp) {
378
+ const date = new Date(timestamp);
379
+ return date.toLocaleTimeString("en-US", {
380
+ hour12: false,
381
+ hour: "2-digit",
382
+ minute: "2-digit",
383
+ second: "2-digit"
384
+ });
385
+ }
386
+ function formatDuration(ms) {
387
+ if (ms < 1e3) return `${ms}ms`;
388
+ return `${(ms / 1e3).toFixed(1)}s`;
389
+ }
390
+ function getRequestTypeLabel(type) {
391
+ switch (type) {
392
+ case "llm_chat":
393
+ return { label: "text", color: "gray" };
394
+ case "image_generation":
395
+ return { label: "image", color: "magenta" };
396
+ case "video_generation":
397
+ return { label: "video", color: "cyan" };
398
+ default:
399
+ return { label: type, color: "gray" };
400
+ }
401
+ }
402
+ function snippetLine(content, maxWidth) {
403
+ const flat = content.replace(/\s+/g, " ").trim();
404
+ if (flat.length <= maxWidth) return flat;
405
+ return "\u2026" + flat.slice(-(maxWidth - 1));
406
+ }
407
+ function RequestItem({ request, width }) {
408
+ const time = formatTime(request.startTime);
409
+ const typeLabel = getRequestTypeLabel(request.requestType);
410
+ const snippetIndent = " ";
411
+ const snippetWidth = width - snippetIndent.length - 2;
412
+ if (request.status === "processing") {
413
+ const elapsed = Date.now() - request.startTime;
414
+ const snippet = request.content && request.requestType === "llm_chat" ? snippetLine(request.content, snippetWidth) : null;
415
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
416
+ /* @__PURE__ */ jsxs3(Box3, { children: [
417
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: /* @__PURE__ */ jsx3(Spinner, { type: "dots" }) }),
418
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
419
+ " ",
420
+ time,
421
+ " "
422
+ ] }),
423
+ /* @__PURE__ */ jsx3(Text3, { color: "white", children: request.modelId }),
424
+ /* @__PURE__ */ jsx3(Text3, { color: "gray", children: " " }),
425
+ /* @__PURE__ */ jsx3(Text3, { color: typeLabel.color, children: typeLabel.label }),
426
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
427
+ " ",
428
+ formatDuration(elapsed),
429
+ "..."
430
+ ] })
431
+ ] }),
432
+ snippet && /* @__PURE__ */ jsxs3(Text3, { color: "gray", dimColor: true, wrap: "truncate-end", children: [
433
+ snippetIndent,
434
+ snippet
435
+ ] })
436
+ ] });
437
+ }
438
+ if (request.status === "completed") {
439
+ const duration = request.duration ? formatDuration(request.duration) : "";
440
+ let resultInfo = "";
441
+ if (request.result?.chars) {
442
+ resultInfo = ` \xB7 ${request.result.chars} chars`;
443
+ } else if (request.result?.imageSize) {
444
+ resultInfo = ` \xB7 ${Math.round(request.result.imageSize / 1024)}KB`;
445
+ } else if (request.result?.videoSize) {
446
+ resultInfo = ` \xB7 ${Math.round(request.result.videoSize / 1024 / 1024)}MB`;
447
+ }
448
+ const snippet = request.content && request.requestType === "llm_chat" ? snippetLine(request.content, snippetWidth) : null;
449
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
450
+ /* @__PURE__ */ jsxs3(Box3, { children: [
451
+ /* @__PURE__ */ jsx3(Text3, { color: "green", children: "\u2713" }),
452
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
453
+ " ",
454
+ time,
455
+ " "
456
+ ] }),
457
+ /* @__PURE__ */ jsx3(Text3, { color: "white", children: request.modelId }),
458
+ /* @__PURE__ */ jsx3(Text3, { color: "gray", children: " " }),
459
+ /* @__PURE__ */ jsx3(Text3, { color: typeLabel.color, children: typeLabel.label }),
460
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
461
+ " ",
462
+ duration,
463
+ resultInfo
464
+ ] })
465
+ ] }),
466
+ snippet && /* @__PURE__ */ jsxs3(Text3, { color: "gray", dimColor: true, wrap: "truncate-end", children: [
467
+ snippetIndent,
468
+ snippet
469
+ ] })
470
+ ] });
471
+ }
472
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: /* @__PURE__ */ jsxs3(Box3, { children: [
473
+ /* @__PURE__ */ jsx3(Text3, { color: "red", children: "\u25CF" }),
474
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
475
+ " ",
476
+ time,
477
+ " "
478
+ ] }),
479
+ /* @__PURE__ */ jsx3(Text3, { color: "white", children: request.modelId }),
480
+ /* @__PURE__ */ jsx3(Text3, { color: "gray", children: " " }),
481
+ /* @__PURE__ */ jsx3(Text3, { color: typeLabel.color, children: typeLabel.label }),
482
+ /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
483
+ " ",
484
+ request.error || "Failed"
485
+ ] })
486
+ ] }) });
487
+ }
488
+ function RequestLog({ requests, maxVisible = 8, hasModels = true }) {
489
+ const { stdout } = useStdout();
490
+ const width = stdout?.columns ?? 80;
491
+ const activeRequests = requests.filter((r) => r.status === "processing");
492
+ const completedRequests = requests.filter((r) => r.status !== "processing");
493
+ const itemLines = (r) => r.requestType === "llm_chat" && r.content ? 2 : 1;
494
+ let completedToShow = [];
495
+ let linesUsed = activeRequests.reduce((sum, r) => sum + itemLines(r), 0);
496
+ for (let i = completedRequests.length - 1; i >= 0 && linesUsed < maxVisible; i--) {
497
+ const r = completedRequests[i];
498
+ const lines = itemLines(r);
499
+ if (linesUsed + lines <= maxVisible) {
500
+ completedToShow.unshift(r);
501
+ linesUsed += lines;
502
+ } else {
503
+ break;
504
+ }
505
+ }
506
+ const visibleRequests = [...completedToShow, ...activeRequests];
507
+ return /* @__PURE__ */ jsxs3(
508
+ Box3,
509
+ {
510
+ flexDirection: "column",
511
+ flexGrow: 1,
512
+ width: "100%",
513
+ paddingX: 1,
514
+ marginTop: 1,
515
+ children: [
516
+ /* @__PURE__ */ jsxs3(Box3, { children: [
517
+ /* @__PURE__ */ jsx3(Text3, { bold: true, underline: true, color: "white", children: "Generation Requests" }),
518
+ activeRequests.length > 0 && /* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
519
+ " (",
520
+ activeRequests.length,
521
+ " active)"
522
+ ] })
523
+ ] }),
524
+ requests.length === 0 ? /* @__PURE__ */ jsx3(Box3, { flexGrow: 1, alignItems: "center", justifyContent: "center", 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)) })
525
+ ]
526
+ }
527
+ );
528
+ }
529
+
530
+ // src/tui/hooks/useSetupProviders.ts
531
+ import { useState as useState7, useEffect as useEffect7, useCallback as useCallback6 } from "react";
532
+ function useSetupProviders() {
533
+ const [providers, setProviders] = useState7([]);
534
+ const [loading, setLoading] = useState7(true);
535
+ const refresh = useCallback6(async () => {
536
+ setLoading(true);
537
+ const statuses = await detectAllProviderStatuses();
538
+ setProviders(statuses);
539
+ setLoading(false);
540
+ }, []);
541
+ useEffect7(() => {
542
+ refresh();
543
+ }, [refresh]);
544
+ return { providers, loading, refresh };
545
+ }
546
+
547
+ // src/tui/pages/DashboardPage.tsx
548
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
549
+ function getCapabilityLabel(capability) {
550
+ switch (capability) {
551
+ case "text":
552
+ return { label: "Text Generation", color: "gray" };
553
+ case "image":
554
+ return { label: "Image Generation", color: "magenta" };
555
+ case "video":
556
+ return { label: "Video Generation", color: "cyan" };
557
+ default:
558
+ return { label: capability, color: "gray" };
559
+ }
560
+ }
561
+ function DashboardPage({
562
+ requests,
563
+ models,
564
+ registeredNames,
565
+ modelsLoading,
566
+ onNavigate
567
+ }) {
568
+ const { stdout } = useStdout2();
569
+ const { providers, loading: setupLoading } = useSetupProviders();
570
+ const installedProviders = providers.filter(({ status }) => status.installed);
571
+ const provNameWidth = Math.max(
572
+ ...installedProviders.map((p) => p.provider.displayName.length),
573
+ 8
574
+ );
575
+ const provStatusWidth = "Local Server Running".length;
576
+ const allModelNames = new Set(models.map((m) => m.name));
577
+ const unavailableRegistered = [...registeredNames].filter(
578
+ (name) => !allModelNames.has(name)
579
+ );
580
+ const menuItems = useMemo(() => {
581
+ return [
582
+ {
583
+ id: "register",
584
+ label: "Sync Models",
585
+ description: "Sync models with MindStudio Cloud"
586
+ },
587
+ {
588
+ id: "refresh",
589
+ label: "Refresh Providers",
590
+ description: "Re-detect local AI providers and models"
591
+ },
592
+ {
593
+ id: "setup",
594
+ label: "Manage Providers",
595
+ description: "Manage local AI providers"
596
+ },
597
+ {
598
+ id: "auth",
599
+ label: "Re-authenticate",
600
+ description: "Re-authenticate with MindStudio"
601
+ },
602
+ {
603
+ id: "quit",
604
+ label: "Exit",
605
+ description: "Quit the application"
606
+ }
607
+ ];
608
+ }, []);
609
+ const termHeight = (stdout?.rows ?? 24) - 4;
610
+ const headerLines = 14;
611
+ const providerContentLines = setupLoading ? 1 : installedProviders.length === 0 ? 2 : installedProviders.length;
612
+ const providersLines = 3 + providerContentLines;
613
+ const modelContentLines = modelsLoading ? 1 : models.length === 0 && unavailableRegistered.length === 0 ? 2 : models.length + (unavailableRegistered.length > 0 ? 1 + unavailableRegistered.length : 0);
614
+ const modelsLines = 3 + modelContentLines;
615
+ const requestLogOverhead = 3;
616
+ const menuLines = menuItems.length + 6;
617
+ const usedLines = headerLines + providersLines + modelsLines + requestLogOverhead + menuLines;
618
+ const maxVisible = Math.max(3, termHeight - usedLines);
619
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", flexGrow: 1, children: [
620
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [
621
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "white", underline: true, children: "Providers" }),
622
+ setupLoading ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
623
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }) }),
624
+ /* @__PURE__ */ jsx4(Text4, { children: " Detecting providers..." })
625
+ ] }) : installedProviders.length === 0 ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
626
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "No providers installed." }),
627
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: 'Use "Manage Providers" below to install one.' })
628
+ ] }) : /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginTop: 1, children: installedProviders.map(({ provider, status }) => {
629
+ const url = provider.baseUrl;
630
+ const statusColor = status.running ? "green" : "yellow";
631
+ const statusText = status.running ? "Local Server Running" : "Installed (not running)";
632
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
633
+ /* @__PURE__ */ jsx4(Text4, { color: "white", children: provider.displayName.padEnd(provNameWidth + 2) }),
634
+ /* @__PURE__ */ jsx4(Text4, { color: statusColor, children: statusText.padEnd(provStatusWidth + 2) }),
635
+ status.running && /* @__PURE__ */ jsx4(Text4, { color: "gray", children: url })
636
+ ] }, provider.name);
637
+ }) })
638
+ ] }),
639
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [
640
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "white", underline: true, children: "Models" }),
641
+ modelsLoading ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
642
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: /* @__PURE__ */ jsx4(Spinner2, { type: "dots" }) }),
643
+ /* @__PURE__ */ jsx4(Text4, { children: " Discovering models..." })
644
+ ] }) : models.length === 0 && unavailableRegistered.length === 0 ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
645
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "No models found." }),
646
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "Download models using your provider (e.g., ollama pull llama3.2)" })
647
+ ] }) : /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
648
+ models.map((model) => {
649
+ const cap = getCapabilityLabel(model.capability);
650
+ const isRegistered = registeredNames.has(model.name);
651
+ const displayProvider = providers.find((p) => p.provider.name === model.provider)?.provider.displayName ?? model.provider;
652
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
653
+ /* @__PURE__ */ jsx4(Text4, { color: isRegistered ? "green" : "gray", children: isRegistered ? "\u25CF" : "\u25CB" }),
654
+ /* @__PURE__ */ jsx4(Text4, { color: "white", children: ` ${model.name}` }),
655
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " - " }),
656
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: displayProvider }),
657
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " - " }),
658
+ /* @__PURE__ */ jsx4(Text4, { color: cap.color, children: cap.label })
659
+ ] }, model.name);
660
+ }),
661
+ unavailableRegistered.length > 0 && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: models.length > 0 ? 1 : 0, children: [
662
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "Registered but not currently available:" }),
663
+ unavailableRegistered.map((name) => /* @__PURE__ */ jsxs4(Box4, { children: [
664
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "\u25CB" }),
665
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: ` ${name}` })
666
+ ] }, name))
667
+ ] })
668
+ ] })
669
+ ] }),
670
+ /* @__PURE__ */ jsx4(
671
+ RequestLog,
672
+ {
673
+ requests,
674
+ maxVisible,
675
+ hasModels: models.length > 0
676
+ }
677
+ ),
678
+ /* @__PURE__ */ jsx4(NavigationMenu, { items: menuItems, onSelect: onNavigate })
679
+ ] });
680
+ }
681
+
682
+ // src/tui/pages/RegisterPage.tsx
683
+ import { useEffect as useEffect9 } from "react";
684
+ import { Box as Box5, Text as Text5 } from "ink";
685
+ import Spinner3 from "ink-spinner";
686
+
687
+ // src/tui/hooks/useRegister.ts
688
+ import { useState as useState8, useCallback as useCallback7, useRef as useRef2, useEffect as useEffect8 } from "react";
689
+ var MODEL_TYPE_MAP = {
690
+ text: "llm_chat",
691
+ image: "image_generation",
692
+ video: "video_generation"
693
+ };
694
+ function useRegister() {
695
+ const [status, setStatus] = useState8("idle");
696
+ const [progress, setProgress] = useState8({
697
+ current: 0,
698
+ total: 0
699
+ });
700
+ const [registeredModels, setRegisteredModels] = useState8(
701
+ []
702
+ );
703
+ const [error, setError] = useState8(null);
704
+ const cancelledRef = useRef2(false);
705
+ useEffect8(() => {
706
+ return () => {
707
+ cancelledRef.current = true;
708
+ };
709
+ }, []);
710
+ const cancel = useCallback7(() => {
711
+ cancelledRef.current = true;
712
+ setStatus("idle");
713
+ }, []);
714
+ const startRegister = useCallback7(() => {
715
+ cancelledRef.current = false;
716
+ setError(null);
717
+ setRegisteredModels([]);
718
+ const run = async () => {
719
+ try {
720
+ setStatus("discovering");
721
+ const localModels = await discoverAllModelsWithParameters();
722
+ if (cancelledRef.current) return;
723
+ if (localModels.length === 0) {
724
+ setError("No local models found.");
725
+ setStatus("error");
726
+ return;
727
+ }
728
+ const existingRegistered = await getRegisteredModels();
729
+ if (cancelledRef.current) return;
730
+ const registeredNames = new Set(existingRegistered);
731
+ const unregisteredModels = localModels.filter(
732
+ (m) => !registeredNames.has(m.name)
733
+ );
734
+ const allModels = localModels.map((m) => ({
735
+ name: m.name,
736
+ provider: m.provider,
737
+ capability: m.capability,
738
+ isNew: !registeredNames.has(m.name)
739
+ }));
740
+ if (unregisteredModels.length === 0) {
741
+ setRegisteredModels(allModels);
742
+ setProgress({ current: 0, total: 0 });
743
+ setStatus("done");
744
+ return;
745
+ }
746
+ setStatus("registering");
747
+ setProgress({ current: 0, total: unregisteredModels.length });
748
+ for (let i = 0; i < unregisteredModels.length; i++) {
749
+ if (cancelledRef.current) return;
750
+ const model = unregisteredModels[i];
751
+ const modelType = MODEL_TYPE_MAP[model.capability];
752
+ await registerLocalModel({
753
+ modelName: model.name,
754
+ provider: model.provider,
755
+ modelType,
756
+ parameters: model.parameters
757
+ });
758
+ setProgress({ current: i + 1, total: unregisteredModels.length });
759
+ }
760
+ if (cancelledRef.current) return;
761
+ const finalModels = localModels.map((m) => ({
762
+ name: m.name,
763
+ provider: m.provider,
764
+ capability: m.capability,
765
+ isNew: !registeredNames.has(m.name)
766
+ }));
767
+ setRegisteredModels(finalModels);
768
+ setStatus("done");
769
+ } catch (err) {
770
+ if (!cancelledRef.current) {
771
+ setError(err instanceof Error ? err.message : "Registration failed");
772
+ setStatus("error");
773
+ }
774
+ }
775
+ };
776
+ run();
777
+ }, []);
778
+ return {
779
+ status,
780
+ progress,
781
+ registeredModels,
782
+ error,
783
+ startRegister,
784
+ cancel
785
+ };
786
+ }
787
+
788
+ // src/tui/pages/RegisterPage.tsx
789
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
790
+ function RegisterPage() {
791
+ const { status, progress, registeredModels, error, startRegister, cancel } = useRegister();
792
+ useEffect9(() => {
793
+ startRegister();
794
+ return () => cancel();
795
+ }, []);
796
+ if (status === "idle") {
797
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginTop: 1, paddingX: 1, children: /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Starting model registration..." }) }) });
798
+ }
799
+ if (status === "error") {
800
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginTop: 1, paddingX: 1, children: /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
801
+ "Registration failed: ",
802
+ error
803
+ ] }) }) });
804
+ }
805
+ if (status === "discovering") {
806
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginTop: 1, paddingX: 1, children: /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
807
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: /* @__PURE__ */ jsx5(Spinner3, { type: "dots" }) }),
808
+ /* @__PURE__ */ jsx5(Text5, { children: " Discovering local models..." })
809
+ ] }) });
810
+ }
811
+ if (status === "registering") {
812
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginTop: 1, paddingX: 1, children: /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
813
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: /* @__PURE__ */ jsx5(Spinner3, { type: "dots" }) }),
814
+ /* @__PURE__ */ jsxs5(Text5, { children: [
815
+ " ",
816
+ "Registering ",
817
+ progress.current,
818
+ "/",
819
+ progress.total,
820
+ " models..."
821
+ ] })
822
+ ] }) });
823
+ }
824
+ const newModels = registeredModels.filter((m) => m.isNew);
825
+ const existingModels = registeredModels.filter((m) => !m.isNew);
826
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, paddingX: 1, children: [
827
+ newModels.length > 0 ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
828
+ /* @__PURE__ */ jsxs5(Text5, { color: "green", children: [
829
+ "Registered ",
830
+ newModels.length,
831
+ " new model",
832
+ newModels.length !== 1 ? "s" : "",
833
+ ":"
834
+ ] }),
835
+ newModels.map((m) => /* @__PURE__ */ jsxs5(Box5, { children: [
836
+ /* @__PURE__ */ jsx5(Text5, { color: "green", children: " \u2713 " }),
837
+ /* @__PURE__ */ jsxs5(Text5, { children: [
838
+ m.name,
839
+ " "
840
+ ] }),
841
+ /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
842
+ "[",
843
+ m.provider,
844
+ "]"
845
+ ] })
846
+ ] }, m.name))
847
+ ] }) : /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "green", children: "All models already registered." }) }),
848
+ existingModels.length > 0 && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, children: [
849
+ /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
850
+ "Already registered (",
851
+ existingModels.length,
852
+ "):"
853
+ ] }),
854
+ existingModels.map((m) => /* @__PURE__ */ jsxs5(Box5, { children: [
855
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: " \u25CF " }),
856
+ /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
857
+ m.name,
858
+ " [",
859
+ m.provider,
860
+ "]"
861
+ ] })
862
+ ] }, m.name))
863
+ ] })
864
+ ] });
865
+ }
866
+
867
+ // src/tui/pages/SetupPage.tsx
868
+ import { useState as useState9, useMemo as useMemo3, useEffect as useEffect10 } from "react";
869
+ import { Box as Box6, Text as Text7, useInput as useInput2, useStdout as useStdout4 } from "ink";
870
+ import Spinner4 from "ink-spinner";
871
+
872
+ // src/tui/components/MarkdownText.tsx
873
+ import { useMemo as useMemo2 } from "react";
874
+ import { Text as Text6, useStdout as useStdout3 } from "ink";
875
+ import chalk from "chalk";
876
+ import { marked } from "marked";
877
+ import { markedTerminal } from "marked-terminal";
878
+ import { jsx as jsx6 } from "react/jsx-runtime";
879
+ var codeStyle = chalk.cyan;
880
+ var identity = (s) => s;
881
+ function renderMarkdown(content, width) {
882
+ marked.use(
883
+ markedTerminal({
884
+ width,
885
+ codespan: codeStyle,
886
+ link: identity,
887
+ href: identity
888
+ })
889
+ );
890
+ marked.use({
891
+ renderer: {
892
+ code({ text }) {
893
+ const lines = text.trim().split("\n").map((l) => " " + codeStyle(l)).join("\n");
894
+ return lines + "\n\n";
895
+ },
896
+ link({ href, text }) {
897
+ if (text && text !== href) {
898
+ return `${text} (${href})`;
899
+ }
900
+ return href;
901
+ }
902
+ }
903
+ });
904
+ return marked.parse(content).trimEnd();
905
+ }
906
+
907
+ // src/tui/pages/SetupPage.tsx
908
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
909
+ function ProviderDetailView({
910
+ provider,
911
+ onBack
912
+ }) {
913
+ const [scrollOffset, setScrollOffset] = useState9(0);
914
+ const { stdout } = useStdout4();
915
+ const termHeight = (stdout?.rows ?? 24) - 4;
916
+ const headerHeight = 14;
917
+ const footerLines = 6;
918
+ const contentPadding = 2;
919
+ const viewHeight = termHeight - headerHeight - footerLines - contentPadding;
920
+ const contentWidth = (stdout?.columns ?? 80) - 4;
921
+ const renderedLines = useMemo3(() => {
922
+ const rendered = renderMarkdown(provider.readme, contentWidth);
923
+ return rendered.split("\n");
924
+ }, [provider.readme, contentWidth]);
925
+ const maxScroll = Math.max(0, renderedLines.length - viewHeight);
926
+ useInput2((input, key) => {
927
+ if (input === "q" || key.escape || key.return) {
928
+ onBack();
929
+ return;
930
+ }
931
+ if (key.upArrow) {
932
+ setScrollOffset((prev) => Math.max(0, prev - 1));
933
+ } else if (key.downArrow) {
934
+ setScrollOffset((prev) => Math.min(maxScroll, prev + 1));
935
+ }
936
+ });
937
+ const visibleContent = renderedLines.slice(scrollOffset, scrollOffset + viewHeight).join("\n");
938
+ const scrollbar = useMemo3(() => {
939
+ if (maxScroll === 0) return null;
940
+ const thumbSize = Math.max(
941
+ 1,
942
+ Math.round(viewHeight / renderedLines.length * viewHeight)
943
+ );
944
+ const thumbPos = Math.round(
945
+ scrollOffset / maxScroll * (viewHeight - thumbSize)
946
+ );
947
+ return Array.from(
948
+ { length: viewHeight },
949
+ (_, i) => i >= thumbPos && i < thumbPos + thumbSize
950
+ );
951
+ }, [scrollOffset, maxScroll, viewHeight, renderedLines.length]);
952
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
953
+ /* @__PURE__ */ jsxs6(Box6, { height: viewHeight, children: [
954
+ /* @__PURE__ */ jsx7(
955
+ Box6,
956
+ {
957
+ flexDirection: "column",
958
+ paddingX: 1,
959
+ paddingY: 1,
960
+ flexGrow: 1,
961
+ overflow: "hidden",
962
+ children: /* @__PURE__ */ jsx7(Text7, { children: visibleContent })
963
+ }
964
+ ),
965
+ scrollbar && /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", children: scrollbar.map((isThumb, i) => /* @__PURE__ */ jsx7(
966
+ Text7,
967
+ {
968
+ color: isThumb ? "cyan" : "gray",
969
+ dimColor: !isThumb,
970
+ children: isThumb ? "\u2503" : "\u2502"
971
+ },
972
+ i
973
+ )) })
974
+ ] }),
975
+ /* @__PURE__ */ jsxs6(
976
+ Box6,
977
+ {
978
+ flexDirection: "column",
979
+ paddingX: 1,
980
+ borderStyle: "single",
981
+ borderTop: true,
982
+ borderBottom: false,
983
+ borderLeft: false,
984
+ borderRight: false,
985
+ borderColor: "gray",
986
+ children: [
987
+ /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "gray", dimColor: true, children: "Actions" }) }),
988
+ /* @__PURE__ */ jsxs6(Box6, { children: [
989
+ /* @__PURE__ */ jsxs6(Text7, { color: "cyan", bold: true, children: [
990
+ "\u276F",
991
+ " Back"
992
+ ] }),
993
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", children: " - Return to providers" })
994
+ ] }),
995
+ /* @__PURE__ */ jsx7(Box6, { marginTop: 1, height: 1, children: /* @__PURE__ */ jsxs6(Text7, { color: "gray", dimColor: true, wrap: "truncate-end", children: [
996
+ "Up/Down Scroll ",
997
+ "\u2022",
998
+ " Enter/q/Esc Back",
999
+ maxScroll > 0 && ` \u2022 ${Math.round(scrollOffset / maxScroll * 100)}%`
1000
+ ] }) })
1001
+ ]
1002
+ }
1003
+ )
1004
+ ] });
1005
+ }
1006
+ function SetupPage({ onBack }) {
1007
+ const { providers, loading } = useSetupProviders();
1008
+ const [selectedProvider, setSelectedProvider] = useState9(null);
1009
+ const running = useMemo3(
1010
+ () => providers.filter((p) => p.status.running),
1011
+ [providers]
1012
+ );
1013
+ const installed = useMemo3(
1014
+ () => providers.filter((p) => p.status.installed && !p.status.running),
1015
+ [providers]
1016
+ );
1017
+ const notInstalled = useMemo3(
1018
+ () => providers.filter((p) => !p.status.installed),
1019
+ [providers]
1020
+ );
1021
+ const allProviders = useMemo3(
1022
+ () => [...running, ...installed, ...notInstalled],
1023
+ [running, installed, notInstalled]
1024
+ );
1025
+ const totalItems = allProviders.length + 1;
1026
+ const backIndex = allProviders.length;
1027
+ const [cursorIndex, setCursorIndex] = useState9(backIndex);
1028
+ useEffect10(() => {
1029
+ setCursorIndex(backIndex);
1030
+ }, [backIndex]);
1031
+ useInput2((input, key) => {
1032
+ if (selectedProvider) return;
1033
+ if (input === "q" || key.escape) {
1034
+ onBack();
1035
+ return;
1036
+ }
1037
+ if (key.upArrow) {
1038
+ setCursorIndex((prev) => Math.max(0, prev - 1));
1039
+ } else if (key.downArrow) {
1040
+ setCursorIndex((prev) => Math.min(totalItems - 1, prev + 1));
1041
+ } else if (key.return) {
1042
+ if (cursorIndex === backIndex) {
1043
+ onBack();
1044
+ } else if (allProviders[cursorIndex]) {
1045
+ setSelectedProvider(allProviders[cursorIndex].provider.name);
1046
+ }
1047
+ }
1048
+ });
1049
+ if (selectedProvider) {
1050
+ const found = providers.find((p) => p.provider.name === selectedProvider);
1051
+ if (found) {
1052
+ return /* @__PURE__ */ jsx7(
1053
+ ProviderDetailView,
1054
+ {
1055
+ provider: found.provider,
1056
+ onBack: () => setSelectedProvider(null)
1057
+ }
1058
+ );
1059
+ }
1060
+ }
1061
+ return /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [
1062
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "white", underline: true, children: "Manage Providers" }),
1063
+ /* @__PURE__ */ jsx7(Text7, { color: "gray", children: "Select a provider to view its setup guide." }),
1064
+ loading ? /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
1065
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: /* @__PURE__ */ jsx7(Spinner4, { type: "dots" }) }),
1066
+ /* @__PURE__ */ jsx7(Text7, { children: " Detecting providers..." })
1067
+ ] }) : /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
1068
+ running.length > 0 && /* @__PURE__ */ jsxs6(Fragment2, { children: [
1069
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "green", children: "Running" }),
1070
+ running.map(({ provider }, i) => {
1071
+ const index = i;
1072
+ const isSelected = index === cursorIndex;
1073
+ return /* @__PURE__ */ jsxs6(
1074
+ Box6,
1075
+ {
1076
+ flexDirection: "column",
1077
+ marginTop: i > 0 ? 1 : 0,
1078
+ children: [
1079
+ /* @__PURE__ */ jsx7(Box6, { children: /* @__PURE__ */ jsxs6(
1080
+ Text7,
1081
+ {
1082
+ color: isSelected ? "cyan" : "white",
1083
+ bold: isSelected,
1084
+ children: [
1085
+ isSelected ? "\u276F" : " ",
1086
+ " ",
1087
+ "\u25CF",
1088
+ " ",
1089
+ provider.displayName
1090
+ ]
1091
+ }
1092
+ ) }),
1093
+ /* @__PURE__ */ jsxs6(Text7, { color: "gray", wrap: "wrap", children: [
1094
+ " ",
1095
+ provider.description
1096
+ ] })
1097
+ ]
1098
+ },
1099
+ provider.name
1100
+ );
1101
+ })
1102
+ ] }),
1103
+ installed.length > 0 && /* @__PURE__ */ jsxs6(
1104
+ Box6,
1105
+ {
1106
+ flexDirection: "column",
1107
+ marginTop: running.length > 0 ? 1 : 0,
1108
+ children: [
1109
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "yellow", children: "Installed" }),
1110
+ installed.map(({ provider, status }, i) => {
1111
+ const index = running.length + i;
1112
+ const isSelected = index === cursorIndex;
1113
+ return /* @__PURE__ */ jsxs6(
1114
+ Box6,
1115
+ {
1116
+ flexDirection: "column",
1117
+ marginTop: i > 0 ? 1 : 0,
1118
+ children: [
1119
+ /* @__PURE__ */ jsx7(Box6, { children: /* @__PURE__ */ jsxs6(
1120
+ Text7,
1121
+ {
1122
+ color: isSelected ? "cyan" : "white",
1123
+ bold: isSelected,
1124
+ children: [
1125
+ isSelected ? "\u276F" : " ",
1126
+ " ",
1127
+ "\u25CB",
1128
+ " ",
1129
+ provider.displayName
1130
+ ]
1131
+ }
1132
+ ) }),
1133
+ /* @__PURE__ */ jsxs6(Text7, { color: "gray", wrap: "wrap", children: [
1134
+ " ",
1135
+ provider.description
1136
+ ] })
1137
+ ]
1138
+ },
1139
+ provider.name
1140
+ );
1141
+ })
1142
+ ]
1143
+ }
1144
+ ),
1145
+ notInstalled.length > 0 && /* @__PURE__ */ jsxs6(
1146
+ Box6,
1147
+ {
1148
+ flexDirection: "column",
1149
+ marginTop: running.length > 0 || installed.length > 0 ? 1 : 0,
1150
+ children: [
1151
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "gray", children: "Not Installed" }),
1152
+ notInstalled.map(({ provider }, i) => {
1153
+ const index = running.length + installed.length + i;
1154
+ const isSelected = index === cursorIndex;
1155
+ return /* @__PURE__ */ jsxs6(
1156
+ Box6,
1157
+ {
1158
+ flexDirection: "column",
1159
+ marginTop: i > 0 ? 1 : 0,
1160
+ children: [
1161
+ /* @__PURE__ */ jsx7(Box6, { children: /* @__PURE__ */ jsxs6(
1162
+ Text7,
1163
+ {
1164
+ color: isSelected ? "cyan" : "white",
1165
+ bold: isSelected,
1166
+ children: [
1167
+ isSelected ? "\u276F" : " ",
1168
+ " ",
1169
+ provider.displayName
1170
+ ]
1171
+ }
1172
+ ) }),
1173
+ /* @__PURE__ */ jsxs6(Text7, { color: "gray", wrap: "wrap", children: [
1174
+ " ",
1175
+ provider.description
1176
+ ] })
1177
+ ]
1178
+ },
1179
+ provider.name
1180
+ );
1181
+ })
1182
+ ]
1183
+ }
1184
+ ),
1185
+ /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs6(
1186
+ Text7,
1187
+ {
1188
+ color: cursorIndex === backIndex ? "cyan" : "white",
1189
+ bold: cursorIndex === backIndex,
1190
+ children: [
1191
+ cursorIndex === backIndex ? "\u276F" : " ",
1192
+ " Back"
1193
+ ]
1194
+ }
1195
+ ) })
1196
+ ] }),
1197
+ /* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text7, { color: "gray", dimColor: true, children: [
1198
+ "Up/Down Navigate ",
1199
+ "\u2022",
1200
+ " Enter Select ",
1201
+ "\u2022",
1202
+ " q/Esc Back"
1203
+ ] }) })
1204
+ ] }) });
1205
+ }
1206
+
1207
+ // src/tui/pages/OnboardingPage.tsx
1208
+ import { useEffect as useEffect12, useCallback as useCallback9, useState as useState11, useMemo as useMemo4 } from "react";
1209
+ import { Box as Box7, Text as Text8, useInput as useInput3 } from "ink";
1210
+ import Spinner5 from "ink-spinner";
1211
+ import chalk2 from "chalk";
1212
+
1213
+ // src/tui/hooks/useAuth.ts
1214
+ import { useState as useState10, useCallback as useCallback8, useRef as useRef3, useEffect as useEffect11 } from "react";
1215
+ import open from "open";
1216
+ var POLL_INTERVAL = 2e3;
1217
+ var MAX_ATTEMPTS = 30;
1218
+ function useAuth() {
1219
+ const [status, setStatus] = useState10("idle");
1220
+ const [authUrl, setAuthUrl] = useState10(null);
1221
+ const [timeRemaining, setTimeRemaining] = useState10(0);
1222
+ const cancelledRef = useRef3(false);
1223
+ const timerRef = useRef3(null);
1224
+ useEffect11(() => {
1225
+ return () => {
1226
+ cancelledRef.current = true;
1227
+ if (timerRef.current) clearInterval(timerRef.current);
1228
+ };
1229
+ }, []);
1230
+ const cancel = useCallback8(() => {
1231
+ cancelledRef.current = true;
1232
+ if (timerRef.current) {
1233
+ clearInterval(timerRef.current);
1234
+ timerRef.current = null;
1235
+ }
1236
+ setStatus("idle");
1237
+ setAuthUrl(null);
1238
+ setTimeRemaining(0);
1239
+ }, []);
1240
+ const startAuth = useCallback8(() => {
1241
+ cancelledRef.current = false;
1242
+ const run = async () => {
1243
+ try {
1244
+ const { url, token } = await requestDeviceAuth();
1245
+ if (cancelledRef.current) return;
1246
+ setAuthUrl(url);
1247
+ setStatus("waiting");
1248
+ const totalTime = MAX_ATTEMPTS * POLL_INTERVAL / 1e3;
1249
+ setTimeRemaining(totalTime);
1250
+ timerRef.current = setInterval(() => {
1251
+ setTimeRemaining((prev) => {
1252
+ const next = prev - 1;
1253
+ if (next <= 0 && timerRef.current) {
1254
+ clearInterval(timerRef.current);
1255
+ timerRef.current = null;
1256
+ }
1257
+ return Math.max(0, next);
1258
+ });
1259
+ }, 1e3);
1260
+ await open(url);
1261
+ for (let i = 0; i < MAX_ATTEMPTS; i++) {
1262
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL));
1263
+ if (cancelledRef.current) return;
1264
+ const result = await pollDeviceAuth(token);
1265
+ if (result.status === "completed" && result.apiKey) {
1266
+ if (timerRef.current) clearInterval(timerRef.current);
1267
+ setApiKey(result.apiKey);
1268
+ setStatus("success");
1269
+ return;
1270
+ }
1271
+ if (result.status === "expired") {
1272
+ if (timerRef.current) clearInterval(timerRef.current);
1273
+ setStatus("expired");
1274
+ return;
1275
+ }
1276
+ }
1277
+ if (!cancelledRef.current) {
1278
+ setStatus("timeout");
1279
+ }
1280
+ } catch {
1281
+ if (!cancelledRef.current) {
1282
+ setStatus("expired");
1283
+ }
1284
+ }
1285
+ };
1286
+ run();
1287
+ }, []);
1288
+ return {
1289
+ status,
1290
+ authUrl,
1291
+ timeRemaining,
1292
+ startAuth,
1293
+ cancel
1294
+ };
1295
+ }
1296
+
1297
+ // src/tui/pages/OnboardingPage.tsx
1298
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1299
+ var SHIMMER_SPEED = 35;
1300
+ function useShimmerLogo() {
1301
+ const [frame, setFrame] = useState11(0);
1302
+ const lines = useMemo4(() => LogoString.split("\n"), []);
1303
+ const totalChars = useMemo4(() => {
1304
+ let count = 0;
1305
+ for (const line of lines) {
1306
+ for (const ch of line) {
1307
+ if (ch !== " " && ch !== " ") count++;
1308
+ }
1309
+ }
1310
+ return count;
1311
+ }, [lines]);
1312
+ const cycleLength = totalChars + 40;
1313
+ useEffect12(() => {
1314
+ const interval = setInterval(() => {
1315
+ setFrame((f) => (f + 1) % cycleLength);
1316
+ }, SHIMMER_SPEED);
1317
+ return () => clearInterval(interval);
1318
+ }, [cycleLength]);
1319
+ return useMemo4(() => {
1320
+ const sweepPos = frame;
1321
+ const holdEnd = totalChars + 20;
1322
+ let charIdx = 0;
1323
+ return lines.map((line) => {
1324
+ let result = "";
1325
+ for (let i = 0; i < line.length; i++) {
1326
+ const ch = line[i];
1327
+ if (ch === " " || ch === " ") {
1328
+ result += ch;
1329
+ continue;
1330
+ }
1331
+ let brightness;
1332
+ if (sweepPos <= totalChars) {
1333
+ const lag = charIdx;
1334
+ const t = sweepPos - lag;
1335
+ brightness = t <= 0 ? 0.1 : Math.min(1, t / 8);
1336
+ } else if (sweepPos <= holdEnd) {
1337
+ brightness = 1;
1338
+ } else {
1339
+ const fadeProgress = (sweepPos - holdEnd) / (cycleLength - holdEnd);
1340
+ brightness = Math.max(0.1, 1 - fadeProgress);
1341
+ }
1342
+ if (brightness >= 0.9) {
1343
+ result += chalk2.cyanBright.bold(ch);
1344
+ } else if (brightness >= 0.6) {
1345
+ result += chalk2.cyan(ch);
1346
+ } else if (brightness >= 0.3) {
1347
+ result += chalk2.rgb(0, 100, 120)(ch);
1348
+ } else {
1349
+ result += chalk2.rgb(0, 50, 60)(ch);
1350
+ }
1351
+ charIdx++;
1352
+ }
1353
+ return result;
1354
+ }).join("\n");
1355
+ }, [frame, lines, totalChars, cycleLength]);
1356
+ }
1357
+ function OnboardingPage({ onComplete }) {
1358
+ const {
1359
+ status: authStatus,
1360
+ authUrl,
1361
+ timeRemaining,
1362
+ startAuth,
1363
+ cancel: cancelAuth
1364
+ } = useAuth();
1365
+ const shimmerLogo = useShimmerLogo();
1366
+ useEffect12(() => {
1367
+ if (authStatus === "success") {
1368
+ const timer = setTimeout(() => onComplete(), 1500);
1369
+ return () => clearTimeout(timer);
1370
+ }
1371
+ }, [authStatus, onComplete]);
1372
+ useEffect12(() => {
1373
+ return () => cancelAuth();
1374
+ }, []);
1375
+ const handleAction = useCallback9(() => {
1376
+ cancelAuth();
1377
+ startAuth();
1378
+ }, [cancelAuth, startAuth]);
1379
+ const canAct = authStatus === "idle" || authStatus === "expired" || authStatus === "timeout";
1380
+ useInput3((_input, key) => {
1381
+ if (canAct && !key.ctrl) {
1382
+ handleAction();
1383
+ }
1384
+ });
1385
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", flexGrow: 1, children: [
1386
+ /* @__PURE__ */ jsx8(Box7, { flexGrow: 1 }),
1387
+ /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", alignItems: "center", children: [
1388
+ /* @__PURE__ */ jsx8(Text8, { children: shimmerLogo }),
1389
+ /* @__PURE__ */ jsx8(Box7, { flexDirection: "column", alignItems: "center", marginTop: 2, children: /* @__PURE__ */ jsx8(Text8, { bold: true, color: "white", children: "MindStudio Local Tunnel" }) }),
1390
+ /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", alignItems: "center", children: [
1391
+ authStatus === "idle" && /* @__PURE__ */ jsxs7(Fragment3, { children: [
1392
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", children: "Connect your MindStudio account to get started." }),
1393
+ /* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "cyan", bold: true, children: "Press any key to Connect Account" }) })
1394
+ ] }),
1395
+ (authStatus === "expired" || authStatus === "timeout") && /* @__PURE__ */ jsxs7(Fragment3, { children: [
1396
+ /* @__PURE__ */ jsx8(Text8, { color: "red", children: authStatus === "expired" ? "Authorization expired." : "Authorization timed out." }),
1397
+ /* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { color: "cyan", bold: true, children: "Press any key to Try Again" }) })
1398
+ ] }),
1399
+ authStatus === "waiting" && /* @__PURE__ */ jsxs7(Fragment3, { children: [
1400
+ /* @__PURE__ */ jsxs7(Box7, { children: [
1401
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: /* @__PURE__ */ jsx8(Spinner5, { type: "dots" }) }),
1402
+ /* @__PURE__ */ jsxs7(Text8, { children: [
1403
+ " ",
1404
+ "Waiting for browser authorization... (",
1405
+ timeRemaining,
1406
+ "s remaining)"
1407
+ ] })
1408
+ ] }),
1409
+ authUrl && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", alignItems: "center", marginTop: 1, children: [
1410
+ /* @__PURE__ */ jsx8(Text8, { color: "gray", children: "If browser didn't open, visit:" }),
1411
+ /* @__PURE__ */ jsx8(Text8, { color: "cyan", children: authUrl })
1412
+ ] })
1413
+ ] }),
1414
+ authStatus === "success" && /* @__PURE__ */ jsxs7(Text8, { color: "green", children: [
1415
+ "\u2713",
1416
+ " Authenticated!"
1417
+ ] })
1418
+ ] })
1419
+ ] }),
1420
+ /* @__PURE__ */ jsx8(Box7, { flexGrow: 1 })
1421
+ ] });
1422
+ }
1423
+
1424
+ // src/tui/App.tsx
1425
+ import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1426
+ function App({ runner }) {
1427
+ const { exit } = useApp();
1428
+ const { stdout } = useStdout5();
1429
+ const {
1430
+ status: connectionStatus,
1431
+ environment,
1432
+ error: connectionError,
1433
+ retry: retryConnection
1434
+ } = useConnection();
1435
+ const { refresh: refreshProviders } = useProviders();
1436
+ const {
1437
+ models,
1438
+ loading: modelsLoading,
1439
+ refresh: refreshModels
1440
+ } = useModels();
1441
+ const { requests } = useRequests();
1442
+ const { registeredNames, refresh: refreshRegistered } = useRegisteredModels(connectionStatus);
1443
+ const shouldOnboard = getApiKey() === void 0;
1444
+ const [page, setPage] = useState12(
1445
+ shouldOnboard ? "onboarding" : "dashboard"
1446
+ );
1447
+ useEffect13(() => {
1448
+ if (page === "dashboard") {
1449
+ refreshAll();
1450
+ }
1451
+ }, [page]);
1452
+ useEffect13(() => {
1453
+ if (connectionStatus === "connected" && models.length > 0) {
1454
+ runner.start(models.map((m) => m.name));
1455
+ }
1456
+ }, [connectionStatus, models, runner]);
1457
+ useEffect13(() => () => runner.stop(), [runner]);
1458
+ const refreshAll = useCallback10(async () => {
1459
+ await Promise.all([
1460
+ refreshProviders(),
1461
+ refreshModels(),
1462
+ refreshRegistered()
1463
+ ]);
1464
+ }, [refreshProviders, refreshModels, refreshRegistered]);
1465
+ const handleQuit = useCallback10(() => {
1466
+ runner.stop();
1467
+ exit();
1468
+ }, [runner, exit]);
1469
+ const handleOnboardingComplete = useCallback10(() => {
1470
+ retryConnection();
1471
+ refreshAll();
1472
+ setPage("dashboard");
1473
+ }, [retryConnection, refreshAll]);
1474
+ const handleNavigate = useCallback10(
1475
+ (id) => {
1476
+ switch (id) {
1477
+ case "auth":
1478
+ setPage("onboarding");
1479
+ break;
1480
+ case "register":
1481
+ setPage("register");
1482
+ break;
1483
+ case "setup":
1484
+ setPage("setup");
1485
+ break;
1486
+ case "refresh":
1487
+ refreshAll();
1488
+ break;
1489
+ case "quit":
1490
+ handleQuit();
1491
+ break;
1492
+ }
1493
+ },
1494
+ [refreshModels, refreshRegistered, refreshAll, handleQuit]
1495
+ );
1496
+ const subpageMenuItems = [
1497
+ { id: "back", label: "Back", description: "Return to dashboard" }
1498
+ ];
1499
+ const handleSubpageNavigate = useCallback10(
1500
+ (id) => {
1501
+ if (id === "back") {
1502
+ setPage("dashboard");
1503
+ } else {
1504
+ handleNavigate(id);
1505
+ }
1506
+ },
1507
+ [handleNavigate]
1508
+ );
1509
+ const termHeight = (stdout?.rows ?? 24) - 4;
1510
+ return /* @__PURE__ */ jsx9(Box8, { flexDirection: "column", height: termHeight, overflow: "hidden", children: page === "onboarding" ? /* @__PURE__ */ jsx9(OnboardingPage, { onComplete: handleOnboardingComplete }) : /* @__PURE__ */ jsxs8(Fragment4, { children: [
1511
+ /* @__PURE__ */ jsx9(
1512
+ Header,
1513
+ {
1514
+ connection: connectionStatus,
1515
+ environment,
1516
+ configPath: getConfigPath(),
1517
+ connectionError
1518
+ }
1519
+ ),
1520
+ page === "dashboard" && /* @__PURE__ */ jsx9(
1521
+ DashboardPage,
1522
+ {
1523
+ requests,
1524
+ models,
1525
+ registeredNames,
1526
+ modelsLoading,
1527
+ onNavigate: handleNavigate
1528
+ }
1529
+ ),
1530
+ page === "setup" && /* @__PURE__ */ jsx9(SetupPage, { onBack: () => setPage("dashboard") }),
1531
+ page === "register" && /* @__PURE__ */ jsx9(RegisterPage, {}),
1532
+ page !== "dashboard" && page !== "setup" && /* @__PURE__ */ jsx9(Box8, { flexGrow: 1 }),
1533
+ page !== "dashboard" && page !== "setup" && /* @__PURE__ */ jsx9(
1534
+ NavigationMenu,
1535
+ {
1536
+ items: subpageMenuItems,
1537
+ onSelect: handleSubpageNavigate
1538
+ }
1539
+ )
1540
+ ] }) });
1541
+ }
1542
+
1543
+ // src/tui/index.tsx
1544
+ import { jsx as jsx10 } from "react/jsx-runtime";
1545
+ async function startTUI() {
1546
+ console.clear();
1547
+ const runner = new TunnelRunner();
1548
+ const { waitUntilExit } = render(
1549
+ /* @__PURE__ */ jsx10(App, { runner }),
1550
+ {
1551
+ exitOnCtrlC: true,
1552
+ incrementalRendering: true
1553
+ }
1554
+ );
1555
+ await waitUntilExit();
1556
+ runner.stop();
1557
+ }
1558
+ export {
1559
+ startTUI
1560
+ };
1561
+ //# sourceMappingURL=tui-56JFPKBP.js.map