@nexus-ai-fs/tui 0.9.18

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 (193) hide show
  1. package/README.md +30 -0
  2. package/package.json +48 -0
  3. package/src/app.tsx +349 -0
  4. package/src/index.tsx +137 -0
  5. package/src/opentui-env.d.ts +61 -0
  6. package/src/panels/access/access-panel.tsx +597 -0
  7. package/src/panels/access/alert-list.tsx +77 -0
  8. package/src/panels/access/constraint-creator.tsx +128 -0
  9. package/src/panels/access/constraint-list.tsx +72 -0
  10. package/src/panels/access/credential-list.tsx +68 -0
  11. package/src/panels/access/delegation-chain-view.tsx +110 -0
  12. package/src/panels/access/delegation-completer.tsx +120 -0
  13. package/src/panels/access/delegation-creator.tsx +237 -0
  14. package/src/panels/access/delegation-list.tsx +74 -0
  15. package/src/panels/access/fraud-score-view.tsx +94 -0
  16. package/src/panels/access/manifest-creator.tsx +167 -0
  17. package/src/panels/access/manifest-list.tsx +105 -0
  18. package/src/panels/access/namespace-config-view.tsx +525 -0
  19. package/src/panels/access/permission-checker.tsx +231 -0
  20. package/src/panels/agents/agent-status-view.tsx +196 -0
  21. package/src/panels/agents/agents-panel.tsx +493 -0
  22. package/src/panels/agents/delegation-list.tsx +154 -0
  23. package/src/panels/agents/inbox-view.tsx +96 -0
  24. package/src/panels/agents/trajectories-tab.tsx +40 -0
  25. package/src/panels/api-console/api-console-panel.tsx +189 -0
  26. package/src/panels/api-console/codegen-viewer.tsx +36 -0
  27. package/src/panels/api-console/codegen.ts +112 -0
  28. package/src/panels/api-console/endpoint-list.tsx +57 -0
  29. package/src/panels/api-console/request-builder.tsx +69 -0
  30. package/src/panels/api-console/response-viewer.tsx +54 -0
  31. package/src/panels/connectors/available-tab.tsx +357 -0
  32. package/src/panels/connectors/connector-row.tsx +121 -0
  33. package/src/panels/connectors/connectors-panel.tsx +88 -0
  34. package/src/panels/connectors/error-parser.ts +116 -0
  35. package/src/panels/connectors/mounted-tab.tsx +179 -0
  36. package/src/panels/connectors/skills-tab.tsx +235 -0
  37. package/src/panels/connectors/template-generator.ts +211 -0
  38. package/src/panels/connectors/write-tab.tsx +514 -0
  39. package/src/panels/events/audit-tab.tsx +69 -0
  40. package/src/panels/events/audit-trail.tsx +75 -0
  41. package/src/panels/events/connector-detail.tsx +49 -0
  42. package/src/panels/events/connector-list.tsx +73 -0
  43. package/src/panels/events/connectors-tab.tsx +92 -0
  44. package/src/panels/events/event-replay.tsx +80 -0
  45. package/src/panels/events/events-panel.tsx +414 -0
  46. package/src/panels/events/events-tab.tsx +212 -0
  47. package/src/panels/events/lock-list.tsx +54 -0
  48. package/src/panels/events/locks-tab.tsx +103 -0
  49. package/src/panels/events/mcl-replay.tsx +77 -0
  50. package/src/panels/events/mcl-tab.tsx +83 -0
  51. package/src/panels/events/operations-tab-wrapper.tsx +62 -0
  52. package/src/panels/events/operations-tab.tsx +41 -0
  53. package/src/panels/events/replay-tab.tsx +76 -0
  54. package/src/panels/events/secrets-audit.tsx +64 -0
  55. package/src/panels/events/secrets-tab.tsx +75 -0
  56. package/src/panels/events/subscription-list.tsx +54 -0
  57. package/src/panels/events/subscriptions-tab.tsx +82 -0
  58. package/src/panels/files/file-aspects.tsx +93 -0
  59. package/src/panels/files/file-editor.tsx +160 -0
  60. package/src/panels/files/file-explorer-keybindings.ts +468 -0
  61. package/src/panels/files/file-explorer-panel.tsx +545 -0
  62. package/src/panels/files/file-lineage.tsx +163 -0
  63. package/src/panels/files/file-list-item.tsx +28 -0
  64. package/src/panels/files/file-metadata.tsx +62 -0
  65. package/src/panels/files/file-preview.tsx +108 -0
  66. package/src/panels/files/file-schema.tsx +89 -0
  67. package/src/panels/files/file-tree-node.tsx +44 -0
  68. package/src/panels/files/file-tree.tsx +169 -0
  69. package/src/panels/files/share-links-tab.tsx +33 -0
  70. package/src/panels/files/uploads-tab.tsx +45 -0
  71. package/src/panels/payments/approval-list.tsx +83 -0
  72. package/src/panels/payments/balance-card.tsx +43 -0
  73. package/src/panels/payments/budget-card.tsx +70 -0
  74. package/src/panels/payments/payments-panel.tsx +451 -0
  75. package/src/panels/payments/policy-list.tsx +64 -0
  76. package/src/panels/payments/reservation-list.tsx +78 -0
  77. package/src/panels/payments/transaction-list.tsx +103 -0
  78. package/src/panels/payments/transfer-form.tsx +109 -0
  79. package/src/panels/search/column-search.tsx +79 -0
  80. package/src/panels/search/knowledge-view.tsx +100 -0
  81. package/src/panels/search/memory-list.tsx +197 -0
  82. package/src/panels/search/playbook-list.tsx +77 -0
  83. package/src/panels/search/rlm-answer-view.tsx +105 -0
  84. package/src/panels/search/search-panel.tsx +405 -0
  85. package/src/panels/search/search-results.tsx +116 -0
  86. package/src/panels/stack/stack-panel.tsx +474 -0
  87. package/src/panels/versions/conflicts-tab.tsx +59 -0
  88. package/src/panels/versions/entry-detail.tsx +89 -0
  89. package/src/panels/versions/transaction-actions.tsx +34 -0
  90. package/src/panels/versions/transaction-list.tsx +90 -0
  91. package/src/panels/versions/versions-panel.tsx +276 -0
  92. package/src/panels/workflows/execution-list.tsx +102 -0
  93. package/src/panels/workflows/scheduler-view.tsx +135 -0
  94. package/src/panels/workflows/workflow-list.tsx +88 -0
  95. package/src/panels/workflows/workflows-panel.tsx +295 -0
  96. package/src/panels/zones/brick-detail.tsx +136 -0
  97. package/src/panels/zones/brick-list.tsx +56 -0
  98. package/src/panels/zones/cache-tab.tsx +118 -0
  99. package/src/panels/zones/drift-view.tsx +97 -0
  100. package/src/panels/zones/mcp-mounts-tab.tsx +38 -0
  101. package/src/panels/zones/memories-tab.tsx +37 -0
  102. package/src/panels/zones/reindex-status.tsx +84 -0
  103. package/src/panels/zones/workspaces-tab.tsx +37 -0
  104. package/src/panels/zones/zone-list.tsx +73 -0
  105. package/src/panels/zones/zones-panel.tsx +559 -0
  106. package/src/services/command-runner.ts +303 -0
  107. package/src/shared/accessibility-announcements.ts +44 -0
  108. package/src/shared/action-registry.ts +466 -0
  109. package/src/shared/brick-states.ts +91 -0
  110. package/src/shared/command-palette.ts +35 -0
  111. package/src/shared/components/announcement-bar.tsx +30 -0
  112. package/src/shared/components/app-confirm-dialog.tsx +29 -0
  113. package/src/shared/components/breadcrumb.tsx +21 -0
  114. package/src/shared/components/brick-gate.tsx +60 -0
  115. package/src/shared/components/command-output.tsx +95 -0
  116. package/src/shared/components/command-palette.tsx +97 -0
  117. package/src/shared/components/confirm-dialog.tsx +61 -0
  118. package/src/shared/components/diff-viewer.tsx +219 -0
  119. package/src/shared/components/empty-state.tsx +36 -0
  120. package/src/shared/components/error-bar.tsx +60 -0
  121. package/src/shared/components/error-boundary.tsx +53 -0
  122. package/src/shared/components/help-overlay.tsx +99 -0
  123. package/src/shared/components/identity-switcher.tsx +168 -0
  124. package/src/shared/components/loading-indicator.tsx +40 -0
  125. package/src/shared/components/pagination-bar.tsx +68 -0
  126. package/src/shared/components/pre-connection-screen.tsx +398 -0
  127. package/src/shared/components/scroll-indicator.tsx +46 -0
  128. package/src/shared/components/side-nav-utils.ts +68 -0
  129. package/src/shared/components/side-nav.tsx +287 -0
  130. package/src/shared/components/spinner.tsx +26 -0
  131. package/src/shared/components/status-bar.tsx +117 -0
  132. package/src/shared/components/styled-text.tsx +72 -0
  133. package/src/shared/components/sub-tab-bar-utils.ts +100 -0
  134. package/src/shared/components/sub-tab-bar.tsx +40 -0
  135. package/src/shared/components/tab-bar-utils.ts +36 -0
  136. package/src/shared/components/tab-bar.tsx +50 -0
  137. package/src/shared/components/text-input.tsx +73 -0
  138. package/src/shared/components/tooltip.tsx +53 -0
  139. package/src/shared/components/virtual-list.tsx +93 -0
  140. package/src/shared/components/welcome-screen.tsx +111 -0
  141. package/src/shared/hooks/use-api.ts +10 -0
  142. package/src/shared/hooks/use-brick-available.ts +42 -0
  143. package/src/shared/hooks/use-confirm.ts +66 -0
  144. package/src/shared/hooks/use-connection-state.ts +67 -0
  145. package/src/shared/hooks/use-copy.ts +31 -0
  146. package/src/shared/hooks/use-fresh-server.ts +62 -0
  147. package/src/shared/hooks/use-keyboard.ts +58 -0
  148. package/src/shared/hooks/use-list-navigation.ts +106 -0
  149. package/src/shared/hooks/use-swr.ts +117 -0
  150. package/src/shared/hooks/use-tab-fallback.ts +32 -0
  151. package/src/shared/hooks/use-text-input.ts +113 -0
  152. package/src/shared/hooks/use-visible-tabs.ts +61 -0
  153. package/src/shared/lib/circular-buffer.ts +82 -0
  154. package/src/shared/lib/clipboard.ts +14 -0
  155. package/src/shared/nav-items.ts +73 -0
  156. package/src/shared/navigation.ts +110 -0
  157. package/src/shared/status-breadcrumb.ts +74 -0
  158. package/src/shared/syntax-style.ts +3 -0
  159. package/src/shared/tab-visibility.ts +15 -0
  160. package/src/shared/text-style.ts +23 -0
  161. package/src/shared/theme.ts +179 -0
  162. package/src/shared/utils/format-size.ts +20 -0
  163. package/src/shared/utils/format-text.ts +10 -0
  164. package/src/shared/utils/format-time.ts +72 -0
  165. package/src/shared/utils/lru-cache.ts +75 -0
  166. package/src/stores/access-store-types.ts +154 -0
  167. package/src/stores/access-store.ts +674 -0
  168. package/src/stores/agents-store.ts +404 -0
  169. package/src/stores/announcement-store.ts +46 -0
  170. package/src/stores/api-console-store.ts +476 -0
  171. package/src/stores/connectors-store.ts +434 -0
  172. package/src/stores/create-api-action.ts +140 -0
  173. package/src/stores/delegation-store.ts +300 -0
  174. package/src/stores/error-store.ts +102 -0
  175. package/src/stores/events-store.ts +163 -0
  176. package/src/stores/files-store.ts +630 -0
  177. package/src/stores/first-run-store.ts +34 -0
  178. package/src/stores/global-store.ts +255 -0
  179. package/src/stores/infra-store.ts +461 -0
  180. package/src/stores/knowledge-store.ts +358 -0
  181. package/src/stores/lineage-store.ts +126 -0
  182. package/src/stores/mcp-store.ts +147 -0
  183. package/src/stores/payments-store.ts +545 -0
  184. package/src/stores/search-store-types.ts +155 -0
  185. package/src/stores/search-store.ts +656 -0
  186. package/src/stores/share-link-store.ts +151 -0
  187. package/src/stores/stack-store.ts +352 -0
  188. package/src/stores/ui-store.ts +161 -0
  189. package/src/stores/upload-store.ts +131 -0
  190. package/src/stores/versions-store.ts +355 -0
  191. package/src/stores/workflows-store.ts +402 -0
  192. package/src/stores/workspace-store.ts +185 -0
  193. package/src/stores/zones-store.ts +378 -0
@@ -0,0 +1,398 @@
1
+ /**
2
+ * PreConnectionScreen — shown when the server is not available (Decision 3A).
3
+ *
4
+ * Guides users through setup: init, start server, configure URL.
5
+ * Supports manual retry + opt-in auto-poll (Decision 14A).
6
+ *
7
+ * Fix (Codex review finding 1): Retry now calls initConfig() instead of
8
+ * testConnection() so config is re-read from disk after nexus init creates
9
+ * nexus.yaml. Also auto-reloads config when a local command completes.
10
+ */
11
+
12
+ import React, { useState, useEffect, useCallback, useRef } from "react";
13
+ import { useKeyboard } from "../hooks/use-keyboard.js";
14
+ import { useGlobalStore } from "../../stores/global-store.js";
15
+ import { detectConnectionState } from "../hooks/use-connection-state.js";
16
+ import { executeLocalCommand, useCommandRunnerStore } from "../../services/command-runner.js";
17
+ import { CommandOutput } from "./command-output.js";
18
+ import { Spinner } from "./spinner.js";
19
+ import { statusColor } from "../theme.js";
20
+ import { resolveConfig, FetchClient } from "@nexus-ai-fs/api-client";
21
+ import { useFilesStore } from "../../stores/files-store.js";
22
+ import { textStyle } from "../text-style.js";
23
+
24
+ const AUTO_POLL_INTERVAL = 5_000; // 5 seconds (Decision 14A)
25
+
26
+ export function PreConnectionScreen(): React.ReactNode {
27
+ const connectionStatus = useGlobalStore((s) => s.connectionStatus);
28
+ const connectionError = useGlobalStore((s) => s.connectionError);
29
+ const config = useGlobalStore((s) => s.config);
30
+ const initConfig = useGlobalStore((s) => s.initConfig);
31
+
32
+ const commandStatus = useCommandRunnerStore((s) => s.status);
33
+
34
+ const connState = detectConnectionState(connectionStatus, connectionError, config);
35
+
36
+ const [autoPoll, setAutoPoll] = useState(false);
37
+ const [retryCount, setRetryCount] = useState(0);
38
+ const [urlInput, setUrlInput] = useState("");
39
+ const [editingUrl, setEditingUrl] = useState(false);
40
+ const [apiKeyWarning, setApiKeyWarning] = useState<string | null>(null);
41
+
42
+ // Track previous commandStatus to detect completion
43
+ const prevCommandStatus = useRef(commandStatus);
44
+ // Track API key before init commands to detect changes
45
+ const prevApiKey = useRef<string | undefined>(undefined);
46
+
47
+ // When a local command finishes (success or error), re-read config from disk.
48
+ // Behavior depends on command type:
49
+ // - "nexus up" success → auto-reconnect (server just started)
50
+ // - "nexus init" success + API key changed → warn user to restart server
51
+ // - "nexus demo" / "nexus up" success → clear file cache (data may have changed)
52
+ // - All others → stay disconnected, user presses R when ready
53
+ useEffect(() => {
54
+ const prev = prevCommandStatus.current;
55
+ prevCommandStatus.current = commandStatus;
56
+
57
+ if (
58
+ (prev === "running") &&
59
+ (commandStatus === "success" || commandStatus === "error")
60
+ ) {
61
+ const label = useCommandRunnerStore.getState().commandLabel;
62
+ const isUpCommand = label.startsWith("nexus up");
63
+ const isDataCommand = label.startsWith("nexus demo") || isUpCommand;
64
+ const isInitCommand = label.startsWith("nexus init");
65
+
66
+ // Re-read config from disk without triggering connection test.
67
+ // resolveConfig() picks up new api_key/ports from nexus.yaml.
68
+ const newConfig = resolveConfig({ transformKeys: false });
69
+ const client = new FetchClient(newConfig);
70
+
71
+ // #3: Detect API key change after init commands
72
+ if (commandStatus === "success" && isInitCommand && prevApiKey.current !== undefined) {
73
+ if (newConfig.apiKey && newConfig.apiKey !== prevApiKey.current) {
74
+ setApiKeyWarning("API key changed. Restart server (Shift+U) to apply.");
75
+ }
76
+ }
77
+ prevApiKey.current = undefined;
78
+
79
+ // #6: Clear file cache after data-mutating commands
80
+ if (commandStatus === "success" && isDataCommand) {
81
+ useFilesStore.getState().clearCache();
82
+ }
83
+
84
+ // #1: Auto-reconnect after "nexus up" succeeds
85
+ if (commandStatus === "success" && isUpCommand && client) {
86
+ useGlobalStore.setState({ config: newConfig, client });
87
+ initConfig();
88
+ } else {
89
+ useGlobalStore.setState({
90
+ config: newConfig,
91
+ client,
92
+ // Stay disconnected — user presses R when ready
93
+ connectionStatus: "error",
94
+ connectionError: "Press R to connect after setup",
95
+ });
96
+ }
97
+ }
98
+ }, [commandStatus, initConfig]);
99
+
100
+ // Manual retry: re-read config from disk + test connection.
101
+ // This is critical for the no-config → init → retry flow: after nexus init
102
+ // writes nexus.yaml, we must call initConfig() (not just testConnection())
103
+ // because testConnection() returns immediately when client=null.
104
+ const handleRetry = useCallback(() => {
105
+ setRetryCount((c) => c + 1);
106
+ initConfig();
107
+ }, [initConfig]);
108
+
109
+ // Auto-poll: also uses initConfig() so it picks up new config from disk
110
+ useEffect(() => {
111
+ if (!autoPoll || connState === "ready") return;
112
+
113
+ const timer = setInterval(() => {
114
+ initConfig();
115
+ }, AUTO_POLL_INTERVAL);
116
+
117
+ return () => clearInterval(timer);
118
+ }, [autoPoll, connState, initConfig]);
119
+
120
+ // Stop auto-poll when connected
121
+ useEffect(() => {
122
+ if (connState === "ready") {
123
+ setAutoPoll(false);
124
+ }
125
+ }, [connState]);
126
+
127
+ // Connect to a different URL
128
+ const handleConnectUrl = useCallback(() => {
129
+ const url = urlInput.trim();
130
+ if (!url) return;
131
+ setEditingUrl(false);
132
+ initConfig({ baseUrl: url });
133
+ }, [urlInput, initConfig]);
134
+
135
+ const isCommandRunning = commandStatus === "running";
136
+ const hasCommandOutput = commandStatus === "success" || commandStatus === "error";
137
+
138
+ // Handle printable chars when editing URL
139
+ const handleUnhandledKey = useCallback(
140
+ (keyName: string) => {
141
+ if (!editingUrl) return;
142
+ if (keyName.length === 1) {
143
+ setUrlInput((u) => u + keyName);
144
+ } else if (keyName === "space") {
145
+ setUrlInput((u) => u + " ");
146
+ }
147
+ },
148
+ [editingUrl],
149
+ );
150
+
151
+ // Dismiss command output and return to menu
152
+ const dismissOutput = useCallback(() => {
153
+ useCommandRunnerStore.getState().reset();
154
+ }, []);
155
+
156
+ useKeyboard(
157
+ isCommandRunning
158
+ ? {}
159
+ : hasCommandOutput
160
+ ? {
161
+ // After a command finishes, only allow Esc to dismiss or re-run shortcuts
162
+ escape: dismissOutput,
163
+ backspace: dismissOutput,
164
+ r: handleRetry,
165
+ }
166
+ : editingUrl
167
+ ? {
168
+ return: handleConnectUrl,
169
+ escape: () => { setEditingUrl(false); setUrlInput(""); },
170
+ backspace: () => setUrlInput((u) => u.slice(0, -1)),
171
+ }
172
+ : {
173
+ r: handleRetry,
174
+ a: () => setAutoPoll((prev) => !prev),
175
+ i: () => {
176
+ prevApiKey.current = config.apiKey;
177
+ setApiKeyWarning(null);
178
+ useCommandRunnerStore.getState().reset();
179
+ executeLocalCommand("init", []);
180
+ },
181
+ s: () => {
182
+ prevApiKey.current = config.apiKey;
183
+ setApiKeyWarning(null);
184
+ useCommandRunnerStore.getState().reset();
185
+ executeLocalCommand("init", ["--preset", "shared"]);
186
+ },
187
+ d: () => {
188
+ prevApiKey.current = config.apiKey;
189
+ setApiKeyWarning(null);
190
+ useCommandRunnerStore.getState().reset();
191
+ executeLocalCommand("init", ["--preset", "demo", "--force"]);
192
+ },
193
+ u: () => {
194
+ // Start server (nexus up)
195
+ useCommandRunnerStore.getState().reset();
196
+ executeLocalCommand("up", []);
197
+ },
198
+ "shift+u": () => {
199
+ // Start server with local build (nexus up --build)
200
+ useCommandRunnerStore.getState().reset();
201
+ executeLocalCommand("up", ["--build"]);
202
+ },
203
+ p: () => {
204
+ // Seed demo data (nexus demo init)
205
+ useCommandRunnerStore.getState().reset();
206
+ executeLocalCommand("demo", ["init"]);
207
+ },
208
+ c: () => {
209
+ // Connect to a different URL
210
+ setEditingUrl(true);
211
+ setUrlInput(config.baseUrl ?? "http://localhost:2026");
212
+ },
213
+ },
214
+ isCommandRunning ? undefined : editingUrl ? handleUnhandledKey : undefined,
215
+ );
216
+
217
+ // Full-screen command output view when a command is running or has output
218
+ if (commandStatus !== "idle") {
219
+ return (
220
+ <box height="100%" width="100%" flexDirection="column">
221
+ <scrollbox flexGrow={1}>
222
+ <box flexDirection="column" width="100%" padding={1}>
223
+ <CommandOutput />
224
+ </box>
225
+ </scrollbox>
226
+ <box height={1} width="100%">
227
+ {commandStatus === "success" ? (
228
+ <text>
229
+ <span style={textStyle({ fg: "#4dff88", bold: true })}>{" ✓ Done"}</span>
230
+ <span style={textStyle({ fg: "#666666" })}>{" │ "}</span>
231
+ <span style={textStyle({ fg: "#00d4ff" })}>{"Esc"}</span>
232
+ <span style={textStyle({ fg: "#888888" })}>{":back "}</span>
233
+ <span style={textStyle({ fg: "#00d4ff" })}>{"R"}</span>
234
+ <span style={textStyle({ fg: "#888888" })}>{":retry"}</span>
235
+ </text>
236
+ ) : commandStatus === "error" ? (
237
+ <text>
238
+ <span style={textStyle({ fg: "#ff4444", bold: true })}>{" ✗ Failed"}</span>
239
+ <span style={textStyle({ fg: "#666666" })}>{" │ "}</span>
240
+ <span style={textStyle({ fg: "#00d4ff" })}>{"Esc"}</span>
241
+ <span style={textStyle({ fg: "#888888" })}>{":back "}</span>
242
+ <span style={textStyle({ fg: "#00d4ff" })}>{"R"}</span>
243
+ <span style={textStyle({ fg: "#888888" })}>{":retry"}</span>
244
+ </text>
245
+ ) : (
246
+ <text>
247
+ <span style={textStyle({ fg: "#ffaa00" })}>{" ◐ Running..."}</span>
248
+ </text>
249
+ )}
250
+ </box>
251
+ </box>
252
+ );
253
+ }
254
+
255
+ return (
256
+ <box height="100%" width="100%" justifyContent="center" alignItems="center">
257
+ <box
258
+ flexDirection="column"
259
+ borderStyle="double"
260
+ width={64}
261
+ padding={1}
262
+ >
263
+ {/* Logo with gradient: cyan → blue → magenta */}
264
+ <text style={textStyle({ fg: "#00d4ff", bold: true })}>
265
+ {" _ _ _____ __ __ _ _ ____"}
266
+ </text>
267
+ <text style={textStyle({ fg: "#00b8ff", bold: true })}>
268
+ {" | \\ | | ____| \\/ | | | / ___|"}
269
+ </text>
270
+ <text style={textStyle({ fg: "#4d8eff", bold: true })}>
271
+ {" | \\| | _| >\\/< | | | \\___ \\"}
272
+ </text>
273
+ <text style={textStyle({ fg: "#8066ff", bold: true })}>
274
+ {" | |\\ | |___/ /\\ \\| |_| |___) |"}
275
+ </text>
276
+ <text style={textStyle({ fg: "#b44dff", bold: true })}>
277
+ {" |_| \\_|_____/_/ \\_\\\\___/|____/"}
278
+ </text>
279
+ <text>{""}</text>
280
+
281
+ {/* Status-specific message */}
282
+ {connState === "no-config" && (
283
+ <>
284
+ <text>
285
+ <span style={textStyle({ fg: "#ffaa00", bold: true })}>{" ⚠ "}</span>
286
+ <span style={textStyle({ fg: "#ffaa00", bold: true })}>{"No API key configured"}</span>
287
+ </text>
288
+ <text>{""}</text>
289
+ <text style={textStyle({ fg: "#888888" })}>{" Set NEXUS_API_KEY or add api_key to ~/.nexus/config.yaml"}</text>
290
+ <text style={textStyle({ fg: "#888888" })}>{" Or press [I] to initialize a new project."}</text>
291
+ </>
292
+ )}
293
+
294
+ {connState === "no-server" && (
295
+ <>
296
+ <text>
297
+ <span style={textStyle({ fg: "#ff4444", bold: true })}>{" ✗ "}</span>
298
+ <span style={textStyle({ fg: "#ff4444", bold: true })}>{"Cannot connect to server"}</span>
299
+ </text>
300
+ <text>{""}</text>
301
+ <text style={textStyle({ fg: "#888888" })}>{` URL: ${config.baseUrl ?? "http://localhost:2026"}`}</text>
302
+ {connectionError && (
303
+ <text style={textStyle({ fg: "#ff6666" })}>{` Error: ${connectionError}`}</text>
304
+ )}
305
+ </>
306
+ )}
307
+
308
+ {connState === "auth-failed" && (
309
+ <>
310
+ <text>
311
+ <span style={textStyle({ fg: "#ff4444", bold: true })}>{" ✗ "}</span>
312
+ <span style={textStyle({ fg: "#ff4444", bold: true })}>{"Authentication failed"}</span>
313
+ </text>
314
+ <text>{""}</text>
315
+ <text style={textStyle({ fg: "#888888" })}>{` URL: ${config.baseUrl ?? "http://localhost:2026"}`}</text>
316
+ <text style={textStyle({ fg: "#ff6666" })}>{" Check your API key or credentials."}</text>
317
+ </>
318
+ )}
319
+
320
+ {connState === "connecting" && (
321
+ <Spinner label=" Connecting..." />
322
+ )}
323
+
324
+ {apiKeyWarning && (
325
+ <>
326
+ <text>{""}</text>
327
+ <text style={textStyle({ fg: "#ffaa00" })}>{` ⚠ ${apiKeyWarning}`}</text>
328
+ </>
329
+ )}
330
+
331
+ <text>{""}</text>
332
+
333
+ {/* URL editor */}
334
+ {editingUrl && (
335
+ <>
336
+ <text style={textStyle({ fg: "#00d4ff" })}>{" Enter server URL:"}</text>
337
+ <box height={1} width="100%">
338
+ <text style={textStyle({ fg: "#ffffff" })}>{` > ${urlInput}\u2588`}</text>
339
+ </box>
340
+ <text style={textStyle({ fg: "#666666" })}>{" Enter to connect, Esc to cancel"}</text>
341
+ <text>{""}</text>
342
+ </>
343
+ )}
344
+
345
+ {/* Actions */}
346
+ {connState !== "connecting" && !editingUrl && (
347
+ <>
348
+ <text style={textStyle({ fg: "#888888", bold: true })}>{" Setup"}</text>
349
+ <text>
350
+ <span style={textStyle({ fg: "#00d4ff", bold: true })}>{" [I] "}</span>
351
+ <span style={textStyle({ fg: "#cccccc" })}>{"Init local"}</span>
352
+ <span style={textStyle({ fg: "#666666" })}>{" (nexus init)"}</span>
353
+ </text>
354
+ <text>
355
+ <span style={textStyle({ fg: "#00d4ff", bold: true })}>{" [S] "}</span>
356
+ <span style={textStyle({ fg: "#cccccc" })}>{"Init shared Docker"}</span>
357
+ <span style={textStyle({ fg: "#666666" })}>{" (--preset shared)"}</span>
358
+ </text>
359
+ <text>
360
+ <span style={textStyle({ fg: "#00d4ff", bold: true })}>{" [D] "}</span>
361
+ <span style={textStyle({ fg: "#cccccc" })}>{"Init demo Docker"}</span>
362
+ <span style={textStyle({ fg: "#666666" })}>{" (--preset demo)"}</span>
363
+ </text>
364
+ <text>
365
+ <span style={textStyle({ fg: "#4dff88", bold: true })}>{" [U] "}</span>
366
+ <span style={textStyle({ fg: "#cccccc" })}>{"Start server"}</span>
367
+ <span style={textStyle({ fg: "#666666" })}>{" (nexus up)"}</span>
368
+ </text>
369
+ <text>
370
+ <span style={textStyle({ fg: "#4dff88", bold: true })}>{" [⇧U] "}</span>
371
+ <span style={textStyle({ fg: "#cccccc" })}>{"Build from source"}</span>
372
+ <span style={textStyle({ fg: "#666666" })}>{" (nexus up --build)"}</span>
373
+ </text>
374
+ <text>
375
+ <span style={textStyle({ fg: "#ffaa00", bold: true })}>{" [P] "}</span>
376
+ <span style={textStyle({ fg: "#cccccc" })}>{"Seed demo data"}</span>
377
+ <span style={textStyle({ fg: "#666666" })}>{" (nexus demo init)"}</span>
378
+ </text>
379
+ <text>{""}</text>
380
+ <text style={textStyle({ fg: "#888888", bold: true })}>{" Connection"}</text>
381
+ <text>
382
+ <span style={textStyle({ fg: "#b44dff", bold: true })}>{" [C] "}</span>
383
+ <span style={textStyle({ fg: "#cccccc" })}>{"Connect to a different URL"}</span>
384
+ </text>
385
+ <text>
386
+ <span style={textStyle({ fg: "#b44dff", bold: true })}>{" [R] "}</span>
387
+ <span style={textStyle({ fg: "#cccccc" })}>{`Retry connection${retryCount > 0 ? ` (${retryCount})` : ""}`}</span>
388
+ </text>
389
+ <text>
390
+ <span style={textStyle({ fg: autoPoll ? "#4dff88" : "#888888", bold: true })}>{" [A] "}</span>
391
+ <span style={textStyle({ fg: autoPoll ? "#4dff88" : "#cccccc" })}>{autoPoll ? "Auto-check: ON (every 5s)" : "Enable auto-check (every 5s)"}</span>
392
+ </text>
393
+ </>
394
+ )}
395
+ </box>
396
+ </box>
397
+ );
398
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Scroll position indicator wrapper.
3
+ * Shows ▲/▼ arrows when list is scrollable in either direction.
4
+ * @see Issue #3066, Phase A4
5
+ */
6
+
7
+ import React from "react";
8
+ import { statusColor } from "../theme.js";
9
+ import { textStyle } from "../text-style.js";
10
+
11
+ interface ScrollIndicatorProps {
12
+ /** Currently selected/focused index */
13
+ readonly selectedIndex: number;
14
+ /** Total number of items in the list */
15
+ readonly totalItems: number;
16
+ /** Number of visible items in the viewport (approximate) */
17
+ readonly visibleItems: number;
18
+ readonly children: React.ReactNode;
19
+ }
20
+
21
+ export function ScrollIndicator({
22
+ selectedIndex,
23
+ totalItems,
24
+ visibleItems,
25
+ children,
26
+ }: ScrollIndicatorProps): React.ReactNode {
27
+ const isScrollable = totalItems > visibleItems;
28
+ const showTop = isScrollable && selectedIndex > 0;
29
+ const showBottom = isScrollable && selectedIndex < totalItems - 1;
30
+
31
+ return (
32
+ <box flexDirection="column" height="100%" width="100%">
33
+ {showTop && (
34
+ <box height={1} width="100%" justifyContent="center">
35
+ <text style={textStyle({ fg: statusColor.dim })}>{"▲ more above"}</text>
36
+ </box>
37
+ )}
38
+ <box flexGrow={1}>{children}</box>
39
+ {showBottom && (
40
+ <box height={1} width="100%" justifyContent="center">
41
+ <text style={textStyle({ fg: statusColor.dim })}>{"▼ more below"}</text>
42
+ </box>
43
+ )}
44
+ </box>
45
+ );
46
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Pure utility functions for SideNav layout calculations.
3
+ *
4
+ * Separated from side-nav.tsx so tests can import without triggering
5
+ * JSX compilation (matching tab-bar-utils.ts pattern).
6
+ *
7
+ * @see Issue #3497
8
+ */
9
+
10
+ // =============================================================================
11
+ // Types
12
+ // =============================================================================
13
+
14
+ /** Responsive display mode for the sidebar. */
15
+ export type SideNavMode = "full" | "collapsed" | "hidden";
16
+
17
+ // =============================================================================
18
+ // Constants
19
+ // =============================================================================
20
+
21
+ /** Data not refreshed within this threshold (ms) is considered stale. */
22
+ export const STALE_THRESHOLD_MS = 60_000;
23
+
24
+ /** Minimum terminal width to show full labels. */
25
+ export const FULL_THRESHOLD = 120;
26
+
27
+ /** Minimum terminal width to show collapsed (icon + shortcut). */
28
+ export const COLLAPSED_THRESHOLD = 80;
29
+
30
+ /**
31
+ * Character width of the sidebar in full mode.
32
+ *
33
+ * Layout: " S:Label____◂ " — 2 (left pad) + 1 (shortcut) + 1 (:) + label + 2 (indicator + right pad)
34
+ * Longest full label is "Connectors" (10 chars) → 2 + 1 + 1 + 10 + 2 = 16.
35
+ * Add 2 for breathing room = 18.
36
+ */
37
+ export const SIDE_NAV_FULL_WIDTH = 18;
38
+
39
+ /**
40
+ * Character width of the sidebar in collapsed mode.
41
+ *
42
+ * Box border consumes 2 chars (left + right), leaving 4 inner chars.
43
+ * Layout: " ◎2◂" — 1 (pad) + 1 (icon) + 1 (shortcut) + 1 (indicator) = 4.
44
+ */
45
+ export const SIDE_NAV_COLLAPSED_WIDTH = 6;
46
+
47
+ // =============================================================================
48
+ // Functions
49
+ // =============================================================================
50
+
51
+ /** Determine the sidebar display mode from terminal width. */
52
+ export function getSideNavMode(columns: number): SideNavMode {
53
+ if (columns >= FULL_THRESHOLD) return "full";
54
+ if (columns >= COLLAPSED_THRESHOLD) return "collapsed";
55
+ return "hidden";
56
+ }
57
+
58
+ /** Get the sidebar pixel/character width for a given mode. */
59
+ export function getSideNavWidth(mode: SideNavMode): number {
60
+ switch (mode) {
61
+ case "full":
62
+ return SIDE_NAV_FULL_WIDTH;
63
+ case "collapsed":
64
+ return SIDE_NAV_COLLAPSED_WIDTH;
65
+ case "hidden":
66
+ return 0;
67
+ }
68
+ }