dmux 5.9.0 → 5.11.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 (159) hide show
  1. package/README.ja.md +1 -1
  2. package/README.md +5 -2
  3. package/dist/DmuxApp.d.ts.map +1 -1
  4. package/dist/DmuxApp.js +32 -6
  5. package/dist/DmuxApp.js.map +1 -1
  6. package/dist/actions/implementations/renameAction.d.ts.map +1 -1
  7. package/dist/actions/implementations/renameAction.js +11 -2
  8. package/dist/actions/implementations/renameAction.js.map +1 -1
  9. package/dist/components/panes/MergePane.d.ts.map +1 -1
  10. package/dist/components/panes/MergePane.js +3 -22
  11. package/dist/components/panes/MergePane.js.map +1 -1
  12. package/dist/components/panes/PaneCard.d.ts.map +1 -1
  13. package/dist/components/panes/PaneCard.js +13 -5
  14. package/dist/components/panes/PaneCard.js.map +1 -1
  15. package/dist/components/popups/inferenceSetupPopup.d.ts +21 -0
  16. package/dist/components/popups/inferenceSetupPopup.d.ts.map +1 -0
  17. package/dist/components/popups/inferenceSetupPopup.js +939 -0
  18. package/dist/components/popups/inferenceSetupPopup.js.map +1 -0
  19. package/dist/components/popups/kebabMenuPopup.d.ts +7 -0
  20. package/dist/components/popups/kebabMenuPopup.d.ts.map +1 -1
  21. package/dist/components/popups/kebabMenuPopup.js +48 -4
  22. package/dist/components/popups/kebabMenuPopup.js.map +1 -1
  23. package/dist/components/popups/settingsPopup.js +4 -0
  24. package/dist/components/popups/settingsPopup.js.map +1 -1
  25. package/dist/dashboard.js +2 -2
  26. package/dist/hooks/useAgentStatus.d.ts.map +1 -1
  27. package/dist/hooks/useAgentStatus.js +9 -2
  28. package/dist/hooks/useAgentStatus.js.map +1 -1
  29. package/dist/hooks/useInputHandling.d.ts +3 -1
  30. package/dist/hooks/useInputHandling.d.ts.map +1 -1
  31. package/dist/hooks/useInputHandling.js +30 -0
  32. package/dist/hooks/useInputHandling.js.map +1 -1
  33. package/dist/hooks/usePaneLoading.d.ts +8 -5
  34. package/dist/hooks/usePaneLoading.d.ts.map +1 -1
  35. package/dist/hooks/usePaneLoading.js +115 -68
  36. package/dist/hooks/usePaneLoading.js.map +1 -1
  37. package/dist/hooks/usePaneSync.d.ts +4 -7
  38. package/dist/hooks/usePaneSync.d.ts.map +1 -1
  39. package/dist/hooks/usePaneSync.js +12 -30
  40. package/dist/hooks/usePaneSync.js.map +1 -1
  41. package/dist/hooks/usePanes.d.ts.map +1 -1
  42. package/dist/hooks/usePanes.js +69 -30
  43. package/dist/hooks/usePanes.js.map +1 -1
  44. package/dist/hooks/useSidebarMouse.d.ts +24 -0
  45. package/dist/hooks/useSidebarMouse.d.ts.map +1 -0
  46. package/dist/hooks/useSidebarMouse.js +50 -0
  47. package/dist/hooks/useSidebarMouse.js.map +1 -0
  48. package/dist/i18n/locales/en.d.ts.map +1 -1
  49. package/dist/i18n/locales/en.js +2 -0
  50. package/dist/i18n/locales/en.js.map +1 -1
  51. package/dist/i18n/locales/ja.d.ts.map +1 -1
  52. package/dist/i18n/locales/ja.js +2 -0
  53. package/dist/i18n/locales/ja.js.map +1 -1
  54. package/dist/index.js +25 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/services/CodexAppServerClient.d.ts +58 -0
  57. package/dist/services/CodexAppServerClient.d.ts.map +1 -0
  58. package/dist/services/CodexAppServerClient.js +263 -0
  59. package/dist/services/CodexAppServerClient.js.map +1 -0
  60. package/dist/services/GrokBuildClient.d.ts +17 -0
  61. package/dist/services/GrokBuildClient.d.ts.map +1 -0
  62. package/dist/services/GrokBuildClient.js +150 -0
  63. package/dist/services/GrokBuildClient.js.map +1 -0
  64. package/dist/services/InferenceService.d.ts +22 -0
  65. package/dist/services/InferenceService.d.ts.map +1 -0
  66. package/dist/services/InferenceService.js +342 -0
  67. package/dist/services/InferenceService.js.map +1 -0
  68. package/dist/services/PaneAnalyzer.d.ts +1 -16
  69. package/dist/services/PaneAnalyzer.d.ts.map +1 -1
  70. package/dist/services/PaneAnalyzer.js +17 -101
  71. package/dist/services/PaneAnalyzer.js.map +1 -1
  72. package/dist/services/PaneWorkerManager.d.ts +1 -1
  73. package/dist/services/PaneWorkerManager.d.ts.map +1 -1
  74. package/dist/services/PaneWorkerManager.js +9 -5
  75. package/dist/services/PaneWorkerManager.js.map +1 -1
  76. package/dist/services/PopupManager.d.ts +6 -1
  77. package/dist/services/PopupManager.d.ts.map +1 -1
  78. package/dist/services/PopupManager.js +30 -0
  79. package/dist/services/PopupManager.js.map +1 -1
  80. package/dist/services/StatusDetector.d.ts.map +1 -1
  81. package/dist/services/StatusDetector.js +15 -8
  82. package/dist/services/StatusDetector.js.map +1 -1
  83. package/dist/services/TerminalPaneNamingService.d.ts +44 -0
  84. package/dist/services/TerminalPaneNamingService.d.ts.map +1 -0
  85. package/dist/services/TerminalPaneNamingService.js +273 -0
  86. package/dist/services/TerminalPaneNamingService.js.map +1 -0
  87. package/dist/services/TmuxService.d.ts +5 -0
  88. package/dist/services/TmuxService.d.ts.map +1 -1
  89. package/dist/services/TmuxService.js +10 -3
  90. package/dist/services/TmuxService.js.map +1 -1
  91. package/dist/types.d.ts +21 -0
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/utils/agentLaunch.d.ts +3 -2
  94. package/dist/utils/agentLaunch.d.ts.map +1 -1
  95. package/dist/utils/agentLaunch.js +9 -4
  96. package/dist/utils/agentLaunch.js.map +1 -1
  97. package/dist/utils/aiMerge.d.ts +4 -2
  98. package/dist/utils/aiMerge.d.ts.map +1 -1
  99. package/dist/utils/aiMerge.js +14 -59
  100. package/dist/utils/aiMerge.js.map +1 -1
  101. package/dist/utils/envKeySetup.d.ts +10 -0
  102. package/dist/utils/envKeySetup.d.ts.map +1 -0
  103. package/dist/utils/envKeySetup.js +58 -0
  104. package/dist/utils/envKeySetup.js.map +1 -0
  105. package/dist/utils/generated-agents-doc.d.ts +1 -1
  106. package/dist/utils/generated-agents-doc.js +1 -1
  107. package/dist/utils/inferenceProviders.d.ts +34 -0
  108. package/dist/utils/inferenceProviders.d.ts.map +1 -0
  109. package/dist/utils/inferenceProviders.js +265 -0
  110. package/dist/utils/inferenceProviders.js.map +1 -0
  111. package/dist/utils/inferenceSetup.d.ts +19 -0
  112. package/dist/utils/inferenceSetup.d.ts.map +1 -0
  113. package/dist/utils/inferenceSetup.js +37 -0
  114. package/dist/utils/inferenceSetup.js.map +1 -0
  115. package/dist/utils/inferenceWizard.d.ts +61 -0
  116. package/dist/utils/inferenceWizard.d.ts.map +1 -0
  117. package/dist/utils/inferenceWizard.js +175 -0
  118. package/dist/utils/inferenceWizard.js.map +1 -0
  119. package/dist/utils/onboarding.d.ts +4 -2
  120. package/dist/utils/onboarding.d.ts.map +1 -1
  121. package/dist/utils/onboarding.js +26 -53
  122. package/dist/utils/onboarding.js.map +1 -1
  123. package/dist/utils/openRouterApiKeySetup.d.ts +7 -0
  124. package/dist/utils/openRouterApiKeySetup.d.ts.map +1 -1
  125. package/dist/utils/openRouterApiKeySetup.js +21 -0
  126. package/dist/utils/openRouterApiKeySetup.js.map +1 -1
  127. package/dist/utils/paneAgentTracking.d.ts +31 -0
  128. package/dist/utils/paneAgentTracking.d.ts.map +1 -0
  129. package/dist/utils/paneAgentTracking.js +261 -0
  130. package/dist/utils/paneAgentTracking.js.map +1 -0
  131. package/dist/utils/popup.d.ts.map +1 -1
  132. package/dist/utils/popup.js +17 -5
  133. package/dist/utils/popup.js.map +1 -1
  134. package/dist/utils/prSummary.d.ts +1 -1
  135. package/dist/utils/prSummary.js +3 -3
  136. package/dist/utils/prSummary.js.map +1 -1
  137. package/dist/utils/settingsManager.d.ts.map +1 -1
  138. package/dist/utils/settingsManager.js +43 -0
  139. package/dist/utils/settingsManager.js.map +1 -1
  140. package/dist/utils/shellPaneDetection.d.ts.map +1 -1
  141. package/dist/utils/shellPaneDetection.js +2 -0
  142. package/dist/utils/shellPaneDetection.js.map +1 -1
  143. package/dist/utils/sidebarClickMap.d.ts +28 -0
  144. package/dist/utils/sidebarClickMap.d.ts.map +1 -0
  145. package/dist/utils/sidebarClickMap.js +91 -0
  146. package/dist/utils/sidebarClickMap.js.map +1 -0
  147. package/dist/utils/sidebarMouse.d.ts +55 -0
  148. package/dist/utils/sidebarMouse.d.ts.map +1 -0
  149. package/dist/utils/sidebarMouse.js +101 -0
  150. package/dist/utils/sidebarMouse.js.map +1 -0
  151. package/dist/utils/sidebarScrollOffset.d.ts +9 -0
  152. package/dist/utils/sidebarScrollOffset.d.ts.map +1 -0
  153. package/dist/utils/sidebarScrollOffset.js +23 -0
  154. package/dist/utils/sidebarScrollOffset.js.map +1 -0
  155. package/dist/utils/slug.d.ts.map +1 -1
  156. package/dist/utils/slug.js +9 -41
  157. package/dist/utils/slug.js.map +1 -1
  158. package/native/macos/prebuilt/dmux-helper.app/Contents/MacOS/dmux-helper +0 -0
  159. package/package.json +3 -3
@@ -0,0 +1,939 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone popup for configuring inference providers.
4
+ *
5
+ * A progressive wizard: provider picker (fuzzy search) → credential entry when
6
+ * missing → dynamically discovered model picker → optional backup target →
7
+ * review-and-save summary. Writes the standard popup result shape
8
+ * `{ primary, backup }` consumed by PopupManager.
9
+ */
10
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
11
+ import { render, Box, Text, useApp, useInput } from 'ink';
12
+ import { pathToFileURL } from 'url';
13
+ import CleanTextInput from '../inputs/CleanTextInput.js';
14
+ import Spinner from '../indicators/Spinner.js';
15
+ import { PopupContainer, PopupWrapper, writeCancelAndExit, writeSuccessAndExit, } from './shared/index.js';
16
+ import { POPUP_CONFIG } from './config.js';
17
+ import { getInferenceApiKey, resolveInferenceProvider, storeInferenceCredential, } from '../../utils/inferenceProviders.js';
18
+ import { persistEnvironmentKeyToShell } from '../../utils/envKeySetup.js';
19
+ import { discoverInferenceModels } from '../../services/InferenceService.js';
20
+ import { getCodexAppServerClient, resetCodexAppServerClient, } from '../../services/CodexAppServerClient.js';
21
+ import { getGrokBuildStatus, loginToGrokBuild, } from '../../services/GrokBuildClient.js';
22
+ import { buildCustomPartialTarget, clampSelection, detectChatGptAccount, detectGrokBuildAccount, detectProviderCredentials, getVisibleWindow, isValidCustomBaseUrl, isValidEnvKeyName, normalizeEnvKeyName, rankInferenceModels, rankInferenceProviders, sanitizeSearchQuery, } from '../../utils/inferenceWizard.js';
23
+ const LIST_MAX_VISIBLE = 8;
24
+ const ROW_MAX_WIDTH = 78;
25
+ const LIST_COL_WIDTH = 48;
26
+ const DETAIL_COL_WIDTH = 34;
27
+ // Fixed 2-character marker gutter so every row label starts at the same x.
28
+ const MARKER_SELECTED = '▸ ';
29
+ const MARKER_BLANK = ' ';
30
+ function truncate(text, maxWidth = ROW_MAX_WIDTH) {
31
+ // eslint-disable-next-line no-control-regex
32
+ const clean = text.replace(/[\x00-\x1f\x7f]/g, '');
33
+ return clean.length > maxWidth ? `${clean.slice(0, maxWidth - 1)}…` : clean;
34
+ }
35
+ function targetTitle(target) {
36
+ return target === 'primary' ? 'Primary' : 'Backup';
37
+ }
38
+ function ScrollList({ items, selectedIndex, maxVisible = LIST_MAX_VISIBLE, renderItem, }) {
39
+ const { start, end } = getVisibleWindow(selectedIndex, items.length, maxVisible);
40
+ return (React.createElement(Box, { flexDirection: "column" },
41
+ start > 0 && React.createElement(Text, { dimColor: true },
42
+ ' ↑ ',
43
+ start,
44
+ " more"),
45
+ items.slice(start, end).map((item, offset) => {
46
+ const index = start + offset;
47
+ return (React.createElement(React.Fragment, { key: index }, renderItem(item, index === selectedIndex, index)));
48
+ }),
49
+ end < items.length && React.createElement(Text, { dimColor: true },
50
+ ' ↓ ',
51
+ items.length - end,
52
+ " more")));
53
+ }
54
+ /** Full-width bordered search input shared by the list screens. */
55
+ const SearchField = ({ value, onChange, onSubmit, placeholder }) => (React.createElement(Box, { borderStyle: "round", borderColor: POPUP_CONFIG.inputBorderColor, paddingX: 1, width: "100%" },
56
+ React.createElement(CleanTextInput, { value: value, onChange: onChange, onSubmit: onSubmit, placeholder: placeholder, maxWidth: ROW_MAX_WIDTH, disableUpDownArrows: true, disableEscape: true, ignoreFocus: true })));
57
+ /**
58
+ * A write-only secret field: characters are captured but rendered as bullets
59
+ * and the buffer never leaves this component except through onSubmit.
60
+ */
61
+ const MaskedSecretInput = ({ onSubmit, onBack }) => {
62
+ const [length, setLength] = useState(0);
63
+ const bufferRef = useRef('');
64
+ useInput((input, key) => {
65
+ if (key.escape) {
66
+ bufferRef.current = '';
67
+ onBack();
68
+ return;
69
+ }
70
+ if (key.return) {
71
+ const secret = bufferRef.current.trim();
72
+ if (secret) {
73
+ bufferRef.current = '';
74
+ onSubmit(secret);
75
+ }
76
+ return;
77
+ }
78
+ if (key.backspace || key.delete) {
79
+ bufferRef.current = bufferRef.current.slice(0, -1);
80
+ setLength(bufferRef.current.length);
81
+ return;
82
+ }
83
+ if (key.ctrl && input === 'u') {
84
+ bufferRef.current = '';
85
+ setLength(0);
86
+ return;
87
+ }
88
+ if (key.ctrl || key.meta || key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.tab) {
89
+ return;
90
+ }
91
+ const printable = input
92
+ .replace(/\u001b\[\d+~/g, '')
93
+ // eslint-disable-next-line no-control-regex
94
+ .replace(/[\x00-\x1f\x7f]/g, '');
95
+ if (printable) {
96
+ bufferRef.current += printable;
97
+ setLength(bufferRef.current.length);
98
+ }
99
+ });
100
+ const shown = Math.min(length, 40);
101
+ return (React.createElement(Box, { borderStyle: POPUP_CONFIG.inputBorderStyle, borderColor: POPUP_CONFIG.inputBorderColor, paddingX: 1 },
102
+ React.createElement(Text, { color: POPUP_CONFIG.titleColor }, '> '),
103
+ React.createElement(Text, null,
104
+ '•'.repeat(shown),
105
+ length > shown ? '…' : ''),
106
+ React.createElement(Text, { inverse: true }, " "),
107
+ length === 0 && React.createElement(Text, { dimColor: true }, " paste or type your API key")));
108
+ };
109
+ const ProviderScreen = ({ target, badges, onChoose, onBack, }) => {
110
+ const [query, setQuery] = useState('');
111
+ const [selectedIndex, setSelectedIndex] = useState(0);
112
+ const ranked = useMemo(() => rankInferenceProviders(query), [query]);
113
+ const prevQueryRef = useRef(query);
114
+ if (prevQueryRef.current !== query) {
115
+ prevQueryRef.current = query;
116
+ if (selectedIndex !== 0)
117
+ setSelectedIndex(0);
118
+ }
119
+ useInput((_input, key) => {
120
+ if (key.escape) {
121
+ if (query)
122
+ setQuery('');
123
+ else
124
+ onBack();
125
+ return;
126
+ }
127
+ if (key.downArrow) {
128
+ setSelectedIndex((prev) => clampSelection(prev + 1, ranked.length));
129
+ return;
130
+ }
131
+ if (key.upArrow) {
132
+ setSelectedIndex((prev) => clampSelection(prev - 1, ranked.length));
133
+ }
134
+ });
135
+ const handleSubmit = () => {
136
+ const provider = ranked[clampSelection(selectedIndex, ranked.length)];
137
+ if (provider)
138
+ onChoose(provider);
139
+ };
140
+ const selected = ranked[clampSelection(selectedIndex, ranked.length)];
141
+ return (React.createElement(Box, { flexDirection: "column" },
142
+ React.createElement(Text, { bold: true },
143
+ targetTitle(target),
144
+ " provider"),
145
+ React.createElement(Text, { dimColor: true }, target === 'primary'
146
+ ? 'Powers pane analysis, names, summaries, and merges.'
147
+ : 'Used automatically when the primary provider fails.'),
148
+ React.createElement(Box, { marginTop: 1 },
149
+ React.createElement(SearchField, { value: query, onChange: setQuery, onSubmit: handleSubmit, placeholder: "Search providers\u2026" })),
150
+ React.createElement(Box, { marginTop: 1 }, ranked.length === 0 ? (React.createElement(Text, { color: POPUP_CONFIG.errorColor },
151
+ "No providers match \u201C",
152
+ truncate(query, 40),
153
+ "\u201D. Press ESC to clear the search.")) : (React.createElement(React.Fragment, null,
154
+ React.createElement(Box, { width: LIST_COL_WIDTH, flexDirection: "column" },
155
+ React.createElement(ScrollList, { items: ranked, selectedIndex: selectedIndex, renderItem: (provider, isSelected) => {
156
+ const badge = badges.get(provider.id);
157
+ const badgeText = badge
158
+ ? badge.kind === 'api-key'
159
+ ? '✓ key detected'
160
+ : '✓ signed in'
161
+ : provider.custom
162
+ ? 'your endpoint'
163
+ : '';
164
+ return (React.createElement(Box, { key: provider.id, width: LIST_COL_WIDTH, paddingRight: 1 },
165
+ React.createElement(Text, { color: isSelected ? POPUP_CONFIG.titleColor : undefined, bold: isSelected },
166
+ isSelected ? MARKER_SELECTED : MARKER_BLANK,
167
+ truncate(provider.name, 30)),
168
+ React.createElement(Box, { flexGrow: 1 }),
169
+ badgeText && (React.createElement(Text, { color: badge ? POPUP_CONFIG.successColor : undefined, dimColor: !badge }, badgeText))));
170
+ } })),
171
+ React.createElement(Box, { width: DETAIL_COL_WIDTH, flexDirection: "column", borderStyle: "single", borderColor: "gray", borderTop: false, borderRight: false, borderBottom: false, paddingLeft: 1 }, selected && (React.createElement(React.Fragment, null,
172
+ React.createElement(Text, { bold: true }, truncate(selected.name, DETAIL_COL_WIDTH - 2)),
173
+ React.createElement(Text, { dimColor: true, wrap: "wrap" }, selected.description),
174
+ badges.get(selected.id)?.kind === 'api-key' && (React.createElement(Box, { marginTop: 1 },
175
+ React.createElement(Text, { color: POPUP_CONFIG.successColor, wrap: "wrap" },
176
+ "Found ",
177
+ badges.get(selected.id).envKey,
178
+ " \u2014 no key entry needed.")))))))))));
179
+ };
180
+ const CUSTOM_FIELDS = [
181
+ {
182
+ key: 'name',
183
+ label: 'Display name',
184
+ placeholder: 'Acme AI (optional)',
185
+ hint: 'Shown in dmux settings and summaries.',
186
+ },
187
+ {
188
+ key: 'baseUrl',
189
+ label: 'Base URL',
190
+ placeholder: 'https://api.acme.dev/v1',
191
+ hint: 'An OpenAI-compatible endpoint exposing /models and /chat/completions.',
192
+ },
193
+ {
194
+ key: 'envKey',
195
+ label: 'API-key environment variable',
196
+ placeholder: 'ACME_API_KEY',
197
+ hint: 'Uppercase letters, digits, and underscores.',
198
+ },
199
+ ];
200
+ const CustomScreen = ({ onComplete, onBack }) => {
201
+ const [fieldIndex, setFieldIndex] = useState(0);
202
+ const [values, setValues] = useState({ name: '', baseUrl: '', envKey: '' });
203
+ const [error, setError] = useState(null);
204
+ const field = CUSTOM_FIELDS[fieldIndex];
205
+ useInput((_input, key) => {
206
+ if (key.escape) {
207
+ setError(null);
208
+ if (fieldIndex > 0)
209
+ setFieldIndex(fieldIndex - 1);
210
+ else
211
+ onBack();
212
+ }
213
+ });
214
+ const handleSubmit = () => {
215
+ const value = values[field.key].trim();
216
+ if (field.key === 'baseUrl' && !isValidCustomBaseUrl(value)) {
217
+ setError('Enter a full http:// or https:// URL.');
218
+ return;
219
+ }
220
+ if (field.key === 'envKey') {
221
+ if (!isValidEnvKeyName(value)) {
222
+ setError('Use uppercase letters, digits, and underscores (for example ACME_API_KEY).');
223
+ return;
224
+ }
225
+ onComplete({ ...values, envKey: normalizeEnvKeyName(value) });
226
+ return;
227
+ }
228
+ setError(null);
229
+ setFieldIndex(fieldIndex + 1);
230
+ };
231
+ return (React.createElement(Box, { flexDirection: "column" },
232
+ React.createElement(Text, { bold: true }, "Custom OpenAI-compatible provider"),
233
+ React.createElement(Text, { dimColor: true },
234
+ "Step ",
235
+ fieldIndex + 1,
236
+ " of ",
237
+ CUSTOM_FIELDS.length),
238
+ CUSTOM_FIELDS.slice(0, fieldIndex).map((done) => (React.createElement(Box, { key: done.key, marginTop: done.key === 'name' ? 1 : 0 },
239
+ React.createElement(Text, { color: POPUP_CONFIG.successColor }, "\u2713 "),
240
+ React.createElement(Text, { dimColor: true },
241
+ done.label,
242
+ ": ",
243
+ values[done.key].trim() || (done.key === 'name' ? 'Custom provider' : ''))))),
244
+ React.createElement(Box, { marginTop: 1, flexDirection: "column" },
245
+ React.createElement(Text, null, field.label),
246
+ React.createElement(CleanTextInput, { value: values[field.key], onChange: (next) => {
247
+ setError(null);
248
+ setValues((prev) => ({ ...prev, [field.key]: next }));
249
+ }, onSubmit: handleSubmit, placeholder: field.placeholder, maxWidth: ROW_MAX_WIDTH, disableUpDownArrows: true, disableEscape: true, ignoreFocus: true }),
250
+ React.createElement(Text, { dimColor: true }, field.hint)),
251
+ error && (React.createElement(Box, { marginTop: 1 },
252
+ React.createElement(Text, { color: POPUP_CONFIG.errorColor }, error)))));
253
+ };
254
+ const CredentialScreen = ({ provider, onReady, onBack }) => {
255
+ const [phase, setPhase] = useState('checking');
256
+ const [errorMessage, setErrorMessage] = useState('');
257
+ const aliveRef = useRef(true);
258
+ const envKey = provider.envKeys[0];
259
+ useEffect(() => {
260
+ (async () => {
261
+ try {
262
+ const existing = await getInferenceApiKey(provider);
263
+ if (!aliveRef.current)
264
+ return;
265
+ if (existing) {
266
+ onReady(`Using ${existing.envKey} from your environment`);
267
+ return;
268
+ }
269
+ }
270
+ catch {
271
+ // Fall through to manual entry.
272
+ }
273
+ if (aliveRef.current)
274
+ setPhase(envKey ? 'enter' : 'error');
275
+ if (aliveRef.current && !envKey) {
276
+ setErrorMessage(`${provider.name} does not define an API-key environment variable.`);
277
+ }
278
+ })();
279
+ return () => {
280
+ aliveRef.current = false;
281
+ };
282
+ // eslint-disable-next-line react-hooks/exhaustive-deps
283
+ }, []);
284
+ useInput((_input, key) => {
285
+ if (phase !== 'enter' && key.escape) {
286
+ onBack();
287
+ return;
288
+ }
289
+ if (phase === 'error' && key.return && envKey) {
290
+ setErrorMessage('');
291
+ setPhase('enter');
292
+ }
293
+ });
294
+ const handleSecret = async (secret) => {
295
+ setPhase('saving');
296
+ try {
297
+ await storeInferenceCredential(envKey, secret);
298
+ const { shellConfigPath } = await persistEnvironmentKeyToShell(envKey, secret);
299
+ if (!aliveRef.current)
300
+ return;
301
+ onReady(`Saved ${envKey} to ${shellConfigPath}`);
302
+ }
303
+ catch (error) {
304
+ if (!aliveRef.current)
305
+ return;
306
+ setErrorMessage(error instanceof Error ? error.message : String(error));
307
+ setPhase('error');
308
+ }
309
+ };
310
+ return (React.createElement(Box, { flexDirection: "column" },
311
+ React.createElement(Text, { bold: true },
312
+ provider.name,
313
+ " API key"),
314
+ phase === 'checking' && (React.createElement(Box, { marginTop: 1 },
315
+ React.createElement(Spinner, { color: POPUP_CONFIG.titleColor }),
316
+ React.createElement(Text, null, " Checking for an existing key\u2026"))),
317
+ phase === 'enter' && (React.createElement(React.Fragment, null,
318
+ React.createElement(Text, { dimColor: true },
319
+ "No ",
320
+ envKey,
321
+ " was found. Paste one to continue."),
322
+ React.createElement(Box, { marginTop: 1, flexDirection: "column" },
323
+ React.createElement(Text, null, envKey),
324
+ React.createElement(MaskedSecretInput, { onSubmit: (secret) => void handleSecret(secret), onBack: onBack }),
325
+ React.createElement(Text, { dimColor: true }, "Input stays hidden. The key is saved to your shell profile and ~/.dmux for future sessions.")))),
326
+ phase === 'saving' && (React.createElement(Box, { marginTop: 1 },
327
+ React.createElement(Spinner, { color: POPUP_CONFIG.titleColor }),
328
+ React.createElement(Text, null,
329
+ " Saving ",
330
+ envKey,
331
+ "\u2026"))),
332
+ phase === 'error' && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
333
+ React.createElement(Text, { color: POPUP_CONFIG.errorColor }, truncate(errorMessage, 160)),
334
+ envKey ? (React.createElement(Text, { dimColor: true }, "Enter to try again \u2022 ESC to go back")) : (React.createElement(Text, { dimColor: true }, "ESC to go back"))))));
335
+ };
336
+ const ChatGptScreen = ({ onReady, onBack }) => {
337
+ const [phase, setPhase] = useState('checking');
338
+ const [authUrl, setAuthUrl] = useState(null);
339
+ const [errorMessage, setErrorMessage] = useState('');
340
+ const aliveRef = useRef(true);
341
+ useEffect(() => {
342
+ return () => {
343
+ aliveRef.current = false;
344
+ };
345
+ }, []);
346
+ const checkAccount = async () => {
347
+ setPhase('checking');
348
+ try {
349
+ const account = await getCodexAppServerClient().getAccount();
350
+ if (!aliveRef.current)
351
+ return;
352
+ if (account?.type === 'chatgpt') {
353
+ onReady(`Using ChatGPT${account.email ? ` as ${account.email}` : ''}`);
354
+ return;
355
+ }
356
+ setPhase('signedOut');
357
+ }
358
+ catch (error) {
359
+ if (!aliveRef.current)
360
+ return;
361
+ setErrorMessage(`Codex CLI is required for ChatGPT subscription inference: ${error instanceof Error ? error.message : String(error)}`);
362
+ setPhase('error');
363
+ }
364
+ };
365
+ useEffect(() => {
366
+ void checkAccount();
367
+ // eslint-disable-next-line react-hooks/exhaustive-deps
368
+ }, []);
369
+ const startLogin = async () => {
370
+ setPhase('waiting');
371
+ setAuthUrl(null);
372
+ try {
373
+ const client = getCodexAppServerClient();
374
+ const login = await client.startChatGptLogin();
375
+ if (!aliveRef.current)
376
+ return;
377
+ setAuthUrl(login.authUrl || login.verificationUrl || null);
378
+ await client.waitForLogin(login.loginId);
379
+ if (!aliveRef.current)
380
+ return;
381
+ const account = await client.getAccount();
382
+ if (!aliveRef.current)
383
+ return;
384
+ if (account?.type === 'chatgpt') {
385
+ onReady(`Using ChatGPT${account.email ? ` as ${account.email}` : ''}`);
386
+ }
387
+ else {
388
+ setErrorMessage('Codex is not signed in with a ChatGPT subscription.');
389
+ setPhase('error');
390
+ }
391
+ }
392
+ catch (error) {
393
+ if (!aliveRef.current)
394
+ return;
395
+ setErrorMessage(error instanceof Error ? error.message : String(error));
396
+ setPhase('error');
397
+ }
398
+ };
399
+ useInput((_input, key) => {
400
+ if (key.escape) {
401
+ onBack();
402
+ return;
403
+ }
404
+ if (key.return) {
405
+ if (phase === 'signedOut')
406
+ void startLogin();
407
+ if (phase === 'error') {
408
+ resetCodexAppServerClient();
409
+ setErrorMessage('');
410
+ void checkAccount();
411
+ }
412
+ }
413
+ });
414
+ return (React.createElement(Box, { flexDirection: "column" },
415
+ React.createElement(Text, { bold: true }, "ChatGPT subscription"),
416
+ phase === 'checking' && (React.createElement(Box, { marginTop: 1 },
417
+ React.createElement(Spinner, { color: POPUP_CONFIG.titleColor }),
418
+ React.createElement(Text, null, " Checking your Codex login\u2026"))),
419
+ phase === 'signedOut' && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
420
+ React.createElement(Text, null, "No ChatGPT login was found."),
421
+ React.createElement(Text, { dimColor: true }, "Press Enter to sign in \u2014 your browser will open to authorize Codex."))),
422
+ phase === 'waiting' && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
423
+ React.createElement(Box, null,
424
+ React.createElement(Spinner, { color: POPUP_CONFIG.titleColor }),
425
+ React.createElement(Text, null, " Waiting for you to finish signing in\u2026")),
426
+ authUrl && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
427
+ React.createElement(Text, { dimColor: true }, "If your browser did not open, visit:"),
428
+ React.createElement(Text, { color: POPUP_CONFIG.titleColor, wrap: "wrap" }, authUrl))))),
429
+ phase === 'error' && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
430
+ React.createElement(Text, { color: POPUP_CONFIG.errorColor, wrap: "wrap" }, truncate(errorMessage, 240)),
431
+ React.createElement(Text, { dimColor: true }, "Enter to retry \u2022 ESC to go back")))));
432
+ };
433
+ function cleanGrokLoginOutput(value) {
434
+ return value
435
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
436
+ .replace(/\r/g, '')
437
+ .split('\n')
438
+ .map((line) => line.trim())
439
+ .filter(Boolean)
440
+ .slice(-6);
441
+ }
442
+ const GrokBuildScreen = ({ onReady, onBack }) => {
443
+ const [phase, setPhase] = useState('checking');
444
+ const [loginOutput, setLoginOutput] = useState('');
445
+ const [errorMessage, setErrorMessage] = useState('');
446
+ const aliveRef = useRef(true);
447
+ const loginAbortRef = useRef(null);
448
+ useEffect(() => {
449
+ return () => {
450
+ aliveRef.current = false;
451
+ loginAbortRef.current?.abort();
452
+ };
453
+ }, []);
454
+ const checkAccount = async () => {
455
+ setPhase('checking');
456
+ try {
457
+ const status = await getGrokBuildStatus();
458
+ if (!aliveRef.current)
459
+ return;
460
+ if (status.authenticated) {
461
+ onReady('Using your Grok Build subscription');
462
+ return;
463
+ }
464
+ setPhase('signedOut');
465
+ }
466
+ catch (error) {
467
+ if (!aliveRef.current)
468
+ return;
469
+ setErrorMessage(error instanceof Error ? error.message : String(error));
470
+ setPhase('error');
471
+ }
472
+ };
473
+ useEffect(() => {
474
+ void checkAccount();
475
+ // eslint-disable-next-line react-hooks/exhaustive-deps
476
+ }, []);
477
+ const startLogin = async () => {
478
+ setPhase('waiting');
479
+ setLoginOutput('');
480
+ setErrorMessage('');
481
+ const controller = new AbortController();
482
+ loginAbortRef.current = controller;
483
+ try {
484
+ await loginToGrokBuild((chunk) => {
485
+ if (aliveRef.current)
486
+ setLoginOutput((previous) => `${previous}${chunk}`.slice(-8_000));
487
+ }, controller.signal);
488
+ if (!aliveRef.current)
489
+ return;
490
+ onReady('Using your Grok Build subscription');
491
+ }
492
+ catch (error) {
493
+ if (!aliveRef.current || controller.signal.aborted)
494
+ return;
495
+ setErrorMessage(error instanceof Error ? error.message : String(error));
496
+ setPhase('error');
497
+ }
498
+ finally {
499
+ if (loginAbortRef.current === controller)
500
+ loginAbortRef.current = null;
501
+ }
502
+ };
503
+ useInput((_input, key) => {
504
+ if (key.escape) {
505
+ loginAbortRef.current?.abort();
506
+ onBack();
507
+ return;
508
+ }
509
+ if (key.return) {
510
+ if (phase === 'signedOut')
511
+ void startLogin();
512
+ if (phase === 'error')
513
+ void checkAccount();
514
+ }
515
+ });
516
+ const outputLines = cleanGrokLoginOutput(loginOutput);
517
+ return (React.createElement(Box, { flexDirection: "column" },
518
+ React.createElement(Text, { bold: true }, "Grok Build subscription"),
519
+ React.createElement(Text, { dimColor: true }, "Uses the SpaceXAI login managed by the Grok Build CLI."),
520
+ phase === 'checking' && (React.createElement(Box, { marginTop: 1 },
521
+ React.createElement(Spinner, { color: POPUP_CONFIG.titleColor }),
522
+ React.createElement(Text, null, " Checking your Grok Build login\u2026"))),
523
+ phase === 'signedOut' && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
524
+ React.createElement(Text, null, "No Grok Build subscription login was found."),
525
+ React.createElement(Text, { dimColor: true }, "Press Enter to sign in with SpaceXAI in your browser."))),
526
+ phase === 'waiting' && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
527
+ React.createElement(Box, null,
528
+ React.createElement(Spinner, { color: POPUP_CONFIG.titleColor }),
529
+ React.createElement(Text, null, " Waiting for Grok Build sign-in\u2026")),
530
+ outputLines.map((line, index) => (React.createElement(Text, { key: `${index}-${line}`, dimColor: true, wrap: "wrap" }, truncate(line, 160)))))),
531
+ phase === 'error' && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
532
+ React.createElement(Text, { color: POPUP_CONFIG.errorColor, wrap: "wrap" }, truncate(errorMessage, 240)),
533
+ React.createElement(Text, { dimColor: true }, "Enter to check again \u2022 ESC to go back")))));
534
+ };
535
+ const ModelsScreen = ({ target, draft, onModelsLoaded, onChoose, onBack, }) => {
536
+ const [phase, setPhase] = useState(draft.models ? 'ready' : 'loading');
537
+ const [errorMessage, setErrorMessage] = useState('');
538
+ const [query, setQuery] = useState('');
539
+ const [manualId, setManualId] = useState('');
540
+ const [selectedIndex, setSelectedIndex] = useState(0);
541
+ const aliveRef = useRef(true);
542
+ const providerName = draft.provider?.name || draft.partialTarget?.providerName || 'provider';
543
+ const models = draft.models || [];
544
+ const loadModels = async () => {
545
+ if (!draft.partialTarget)
546
+ return;
547
+ setPhase('loading');
548
+ setErrorMessage('');
549
+ try {
550
+ const discovered = await discoverInferenceModels(draft.partialTarget);
551
+ if (!aliveRef.current)
552
+ return;
553
+ if (discovered.length === 0) {
554
+ setErrorMessage(`${providerName} returned an empty model catalog.`);
555
+ setPhase('error');
556
+ return;
557
+ }
558
+ onModelsLoaded(discovered);
559
+ setPhase('ready');
560
+ }
561
+ catch (error) {
562
+ if (!aliveRef.current)
563
+ return;
564
+ setErrorMessage(error instanceof Error ? error.message : String(error));
565
+ setPhase('error');
566
+ }
567
+ };
568
+ useEffect(() => {
569
+ if (!draft.models)
570
+ void loadModels();
571
+ return () => {
572
+ aliveRef.current = false;
573
+ };
574
+ // eslint-disable-next-line react-hooks/exhaustive-deps
575
+ }, []);
576
+ const ranked = useMemo(() => rankInferenceModels(models, query), [models, query]);
577
+ const prevQueryRef = useRef(query);
578
+ if (prevQueryRef.current !== query) {
579
+ prevQueryRef.current = query;
580
+ if (selectedIndex !== 0)
581
+ setSelectedIndex(0);
582
+ }
583
+ useInput((input, key) => {
584
+ if (phase === 'ready') {
585
+ if (key.escape) {
586
+ if (query)
587
+ setQuery('');
588
+ else
589
+ onBack();
590
+ return;
591
+ }
592
+ if (key.downArrow) {
593
+ setSelectedIndex((prev) => clampSelection(prev + 1, ranked.length));
594
+ return;
595
+ }
596
+ if (key.upArrow) {
597
+ setSelectedIndex((prev) => clampSelection(prev - 1, ranked.length));
598
+ }
599
+ return;
600
+ }
601
+ if (key.escape) {
602
+ onBack();
603
+ return;
604
+ }
605
+ if (phase === 'error' && key.ctrl && input === 'r') {
606
+ void loadModels();
607
+ }
608
+ });
609
+ const handleSubmit = () => {
610
+ const model = ranked[clampSelection(selectedIndex, ranked.length)];
611
+ if (model) {
612
+ onChoose(model.id);
613
+ return;
614
+ }
615
+ const fallback = sanitizeSearchQuery(query);
616
+ if (fallback)
617
+ onChoose(fallback);
618
+ };
619
+ const selected = ranked[clampSelection(selectedIndex, ranked.length)];
620
+ return (React.createElement(Box, { flexDirection: "column" },
621
+ React.createElement(Text, { bold: true },
622
+ targetTitle(target),
623
+ " model \u00B7 ",
624
+ providerName),
625
+ draft.credentialNote && (React.createElement(Text, { dimColor: true },
626
+ "\u2713 ",
627
+ truncate(draft.credentialNote))),
628
+ phase === 'loading' && (React.createElement(Box, { marginTop: 1 },
629
+ React.createElement(Spinner, { color: POPUP_CONFIG.titleColor }),
630
+ React.createElement(Text, null,
631
+ " Loading models from ",
632
+ providerName,
633
+ "\u2026"))),
634
+ phase === 'ready' && (React.createElement(React.Fragment, null,
635
+ React.createElement(Box, { marginTop: 1 },
636
+ React.createElement(SearchField, { value: query, onChange: setQuery, onSubmit: handleSubmit, placeholder: `Search ${models.length} models…` })),
637
+ React.createElement(Box, { marginTop: 1 }, ranked.length === 0 ? (React.createElement(Box, { flexDirection: "column" },
638
+ React.createElement(Text, { color: POPUP_CONFIG.errorColor },
639
+ "No models match \u201C",
640
+ truncate(query, 40),
641
+ "\u201D."),
642
+ React.createElement(Text, { dimColor: true }, "Press Enter to use it as a custom model id, or ESC to clear."))) : (React.createElement(React.Fragment, null,
643
+ React.createElement(Box, { width: LIST_COL_WIDTH, flexDirection: "column" },
644
+ React.createElement(Text, { dimColor: true }, query
645
+ ? `${ranked.length} of ${models.length} models`
646
+ : `${models.length} models`),
647
+ React.createElement(ScrollList, { items: ranked, selectedIndex: selectedIndex, renderItem: (model, isSelected) => (React.createElement(Box, { key: model.id, width: LIST_COL_WIDTH, paddingRight: 1 },
648
+ React.createElement(Text, { color: isSelected ? POPUP_CONFIG.titleColor : undefined, bold: isSelected },
649
+ isSelected ? MARKER_SELECTED : MARKER_BLANK,
650
+ truncate(model.name, LIST_COL_WIDTH - 2)))) })),
651
+ React.createElement(Box, { width: DETAIL_COL_WIDTH, flexDirection: "column", borderStyle: "single", borderColor: "gray", borderTop: false, borderRight: false, borderBottom: false, paddingLeft: 1 }, selected && (React.createElement(React.Fragment, null,
652
+ React.createElement(Text, { bold: true, wrap: "wrap" }, truncate(selected.name, DETAIL_COL_WIDTH - 2)),
653
+ selected.id !== selected.name && (React.createElement(Text, { dimColor: true, wrap: "wrap" }, selected.id)),
654
+ selected.description && (React.createElement(Box, { marginTop: 1 },
655
+ React.createElement(Text, { dimColor: true, wrap: "wrap" }, truncate(selected.description, 200)))))))))))),
656
+ phase === 'error' && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
657
+ React.createElement(Text, { color: POPUP_CONFIG.errorColor, wrap: "wrap" },
658
+ "Could not load the model catalog: ",
659
+ truncate(errorMessage, 200)),
660
+ React.createElement(Box, { marginTop: 1, flexDirection: "column" },
661
+ React.createElement(Text, null, "Enter a model id manually"),
662
+ React.createElement(CleanTextInput, { value: manualId, onChange: setManualId, onSubmit: () => {
663
+ const id = manualId.trim();
664
+ if (id)
665
+ onChoose(id);
666
+ }, placeholder: "for example gpt-5-mini", maxWidth: ROW_MAX_WIDTH, disableUpDownArrows: true, disableEscape: true, ignoreFocus: true }),
667
+ React.createElement(Text, { dimColor: true }, "Ctrl+R to retry loading the catalog"))))));
668
+ };
669
+ const BACKUP_OPTIONS = [
670
+ {
671
+ label: 'Finish with just the primary',
672
+ description: 'You can add a backup later from Settings → Inference providers.',
673
+ wantBackup: false,
674
+ },
675
+ {
676
+ label: 'Add a backup model',
677
+ description: 'dmux fails over to it automatically when the primary errors out.',
678
+ wantBackup: true,
679
+ },
680
+ ];
681
+ const BackupOfferScreen = ({ onDecide, onBack }) => {
682
+ const [selectedIndex, setSelectedIndex] = useState(0);
683
+ useInput((_input, key) => {
684
+ if (key.escape) {
685
+ onBack();
686
+ return;
687
+ }
688
+ if (key.downArrow) {
689
+ setSelectedIndex((prev) => clampSelection(prev + 1, BACKUP_OPTIONS.length));
690
+ return;
691
+ }
692
+ if (key.upArrow) {
693
+ setSelectedIndex((prev) => clampSelection(prev - 1, BACKUP_OPTIONS.length));
694
+ return;
695
+ }
696
+ if (key.return) {
697
+ onDecide(BACKUP_OPTIONS[selectedIndex].wantBackup);
698
+ }
699
+ });
700
+ return (React.createElement(Box, { flexDirection: "column" },
701
+ React.createElement(Text, { bold: true }, "Add a backup?"),
702
+ React.createElement(Text, { dimColor: true }, "A second provider/model keeps AI features working during outages."),
703
+ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, BACKUP_OPTIONS.map((option, index) => {
704
+ const isSelected = index === selectedIndex;
705
+ return (React.createElement(Box, { key: option.label, flexDirection: "column" },
706
+ React.createElement(Text, { color: isSelected ? POPUP_CONFIG.titleColor : undefined, bold: isSelected },
707
+ isSelected ? MARKER_SELECTED : MARKER_BLANK,
708
+ option.label),
709
+ isSelected && React.createElement(Text, { dimColor: true },
710
+ ' ',
711
+ option.description)));
712
+ }))));
713
+ };
714
+ const SummaryScreen = ({ drafts, onSave, onBack }) => {
715
+ useInput((_input, key) => {
716
+ if (key.escape) {
717
+ onBack();
718
+ return;
719
+ }
720
+ if (key.return)
721
+ onSave();
722
+ });
723
+ const describe = (draft) => {
724
+ if (!draft.partialTarget || !draft.modelId)
725
+ return null;
726
+ const name = draft.provider?.name || draft.partialTarget.providerName || draft.partialTarget.providerId;
727
+ return `${name} / ${draft.modelId}`;
728
+ };
729
+ const backupLine = describe(drafts.backup);
730
+ return (React.createElement(Box, { flexDirection: "column" },
731
+ React.createElement(Text, { bold: true }, "Review configuration"),
732
+ React.createElement(Box, { marginTop: 1, flexDirection: "column" },
733
+ React.createElement(Box, null,
734
+ React.createElement(Text, { dimColor: true }, 'Primary '),
735
+ React.createElement(Text, { color: POPUP_CONFIG.successColor }, truncate(describe(drafts.primary) || '', 68))),
736
+ React.createElement(Box, null,
737
+ React.createElement(Text, { dimColor: true }, 'Backup '),
738
+ backupLine ? (React.createElement(Text, { color: POPUP_CONFIG.successColor }, truncate(backupLine, 68))) : (React.createElement(Text, { dimColor: true }, "none")))),
739
+ React.createElement(Box, { marginTop: 1 },
740
+ React.createElement(Text, { dimColor: true }, "Saved globally for all dmux projects."))));
741
+ };
742
+ export const InferenceSetupApp = ({ resultFile, onComplete }) => {
743
+ const { exit } = useApp();
744
+ const [steps, setSteps] = useState([{ kind: 'provider', target: 'primary' }]);
745
+ const [drafts, setDrafts] = useState({
746
+ primary: {},
747
+ backup: {},
748
+ });
749
+ const [badges, setBadges] = useState(new Map());
750
+ const step = steps[steps.length - 1];
751
+ useEffect(() => {
752
+ return () => {
753
+ // Provider badges and ChatGPT model discovery share one app-server
754
+ // process. Ink's exit only unmounts the tree, so close it explicitly to
755
+ // let the popup process terminate cleanly.
756
+ resetCodexAppServerClient();
757
+ };
758
+ }, []);
759
+ useEffect(() => {
760
+ let alive = true;
761
+ void detectProviderCredentials().then((detected) => {
762
+ if (!alive)
763
+ return;
764
+ setBadges((prev) => new Map([...prev, ...detected]));
765
+ });
766
+ void detectChatGptAccount().then((account) => {
767
+ if (!alive || !account)
768
+ return;
769
+ setBadges((prev) => {
770
+ const next = new Map(prev);
771
+ next.set('chatgpt', { kind: 'chatgpt', email: account.email });
772
+ return next;
773
+ });
774
+ });
775
+ void detectGrokBuildAccount().then((account) => {
776
+ if (!alive || !account)
777
+ return;
778
+ setBadges((prev) => {
779
+ const next = new Map(prev);
780
+ next.set('grok-build', { kind: 'grok-build' });
781
+ return next;
782
+ });
783
+ });
784
+ return () => {
785
+ alive = false;
786
+ };
787
+ }, []);
788
+ const push = (next) => setSteps((prev) => [...prev, next]);
789
+ const replaceTop = (next) => setSteps((prev) => [...prev.slice(0, -1), next]);
790
+ const cancel = () => {
791
+ if (resultFile)
792
+ writeCancelAndExit(resultFile, exit);
793
+ else {
794
+ onComplete?.(null);
795
+ exit();
796
+ }
797
+ };
798
+ const back = () => {
799
+ if (steps.length > 1)
800
+ setSteps((prev) => prev.slice(0, -1));
801
+ else
802
+ cancel();
803
+ };
804
+ useInput((input, key) => {
805
+ if (key.ctrl && input === 'c')
806
+ cancel();
807
+ });
808
+ const updateDraft = (target, patch) => {
809
+ setDrafts((prev) => ({ ...prev, [target]: { ...prev[target], ...patch } }));
810
+ };
811
+ const handleProviderChosen = (target, provider) => {
812
+ updateDraft(target, {
813
+ provider,
814
+ partialTarget: { providerId: provider.id },
815
+ credentialNote: undefined,
816
+ models: undefined,
817
+ modelId: undefined,
818
+ });
819
+ if (provider.custom)
820
+ push({ kind: 'custom', target });
821
+ else if (provider.protocol === 'chatgpt')
822
+ push({ kind: 'chatgpt', target });
823
+ else if (provider.protocol === 'grok-build')
824
+ push({ kind: 'grokBuild', target });
825
+ else
826
+ push({ kind: 'credential', target });
827
+ };
828
+ const handleCustomComplete = (target, fields) => {
829
+ const partialTarget = buildCustomPartialTarget(fields);
830
+ const provider = resolveInferenceProvider({ ...partialTarget, modelId: '__setup__' });
831
+ if (!provider)
832
+ return;
833
+ updateDraft(target, { provider, partialTarget, models: undefined, modelId: undefined });
834
+ push({ kind: 'credential', target });
835
+ };
836
+ const handleCredentialReady = (target, note) => {
837
+ updateDraft(target, { credentialNote: note });
838
+ // Replace instead of push so ESC from the model list returns to the
839
+ // provider picker rather than re-triggering the credential check.
840
+ replaceTop({ kind: 'models', target });
841
+ };
842
+ const handleModelChosen = (target, modelId) => {
843
+ updateDraft(target, { modelId });
844
+ if (target === 'primary')
845
+ push({ kind: 'backupOffer' });
846
+ else
847
+ push({ kind: 'summary' });
848
+ };
849
+ const handleBackupDecision = (wantBackup) => {
850
+ if (wantBackup) {
851
+ push({ kind: 'provider', target: 'backup' });
852
+ }
853
+ else {
854
+ setDrafts((prev) => ({ ...prev, backup: {} }));
855
+ push({ kind: 'summary' });
856
+ }
857
+ };
858
+ const handleSave = () => {
859
+ const { primary, backup } = drafts;
860
+ if (!primary.partialTarget || !primary.modelId)
861
+ return;
862
+ const result = {
863
+ primary: { ...primary.partialTarget, modelId: primary.modelId },
864
+ backup: backup.partialTarget && backup.modelId
865
+ ? { ...backup.partialTarget, modelId: backup.modelId }
866
+ : null,
867
+ };
868
+ if (resultFile)
869
+ writeSuccessAndExit(resultFile, result, exit);
870
+ else {
871
+ onComplete?.(result);
872
+ exit();
873
+ }
874
+ };
875
+ const footer = (() => {
876
+ switch (step.kind) {
877
+ case 'provider':
878
+ return `↑↓ navigate • Enter select • ${steps.length > 1 ? 'ESC back' : POPUP_CONFIG.cancelHint}`;
879
+ case 'custom':
880
+ return 'Enter continue • ESC back';
881
+ case 'credential':
882
+ return 'Enter save key • ESC back';
883
+ case 'chatgpt':
884
+ return 'Enter continue • ESC back';
885
+ case 'grokBuild':
886
+ return 'Enter sign in • ESC back';
887
+ case 'models':
888
+ return '↑↓ navigate • Enter select • ESC back';
889
+ case 'backupOffer':
890
+ return '↑↓ navigate • Enter choose • ESC back';
891
+ case 'summary':
892
+ return 'Enter save • ESC back';
893
+ }
894
+ })();
895
+ const screen = (() => {
896
+ switch (step.kind) {
897
+ case 'provider':
898
+ return (React.createElement(ProviderScreen, { key: `provider-${step.target}`, target: step.target, badges: badges, onChoose: (provider) => handleProviderChosen(step.target, provider), onBack: back }));
899
+ case 'custom':
900
+ return (React.createElement(CustomScreen, { key: `custom-${step.target}`, onComplete: (fields) => handleCustomComplete(step.target, fields), onBack: back }));
901
+ case 'credential': {
902
+ const provider = drafts[step.target].provider;
903
+ if (!provider)
904
+ return null;
905
+ return (React.createElement(CredentialScreen, { key: `credential-${step.target}-${provider.id}`, provider: provider, onReady: (note) => handleCredentialReady(step.target, note), onBack: back }));
906
+ }
907
+ case 'chatgpt':
908
+ return (React.createElement(ChatGptScreen, { key: `chatgpt-${step.target}`, onReady: (note) => handleCredentialReady(step.target, note), onBack: back }));
909
+ case 'grokBuild':
910
+ return (React.createElement(GrokBuildScreen, { key: `grok-build-${step.target}`, onReady: (note) => handleCredentialReady(step.target, note), onBack: back }));
911
+ case 'models':
912
+ return (React.createElement(ModelsScreen, { key: `models-${step.target}-${drafts[step.target].provider?.id || 'custom'}`, target: step.target, draft: drafts[step.target], onModelsLoaded: (models) => updateDraft(step.target, { models }), onChoose: (modelId) => handleModelChosen(step.target, modelId), onBack: back }));
913
+ case 'backupOffer':
914
+ return React.createElement(BackupOfferScreen, { key: "backup-offer", onDecide: handleBackupDecision, onBack: back });
915
+ case 'summary':
916
+ return React.createElement(SummaryScreen, { key: "summary", drafts: drafts, onSave: handleSave, onBack: back });
917
+ }
918
+ })();
919
+ // Match the 90-column tmux popup's 88-column interior and keep first-run
920
+ // inline onboarding from stretching across an ultra-wide terminal.
921
+ const content = React.createElement(PopupContainer, { footer: footer, width: 88 }, screen);
922
+ return resultFile ? (React.createElement(PopupWrapper, { resultFile: resultFile, allowEscapeToCancel: false }, content)) : content;
923
+ };
924
+ /* ------------------------------------------------------------------ */
925
+ /* Entry point */
926
+ /* ------------------------------------------------------------------ */
927
+ function main() {
928
+ const resultFile = process.argv[2];
929
+ if (!resultFile) {
930
+ console.error('Error: Result file path required');
931
+ process.exit(1);
932
+ }
933
+ render(React.createElement(InferenceSetupApp, { resultFile: resultFile }));
934
+ }
935
+ const entryPointHref = process.argv[1] ? pathToFileURL(process.argv[1]).href : '';
936
+ if (import.meta.url === entryPointHref) {
937
+ main();
938
+ }
939
+ //# sourceMappingURL=inferenceSetupPopup.js.map