@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.
- package/README.md +30 -0
- package/package.json +48 -0
- package/src/app.tsx +349 -0
- package/src/index.tsx +137 -0
- package/src/opentui-env.d.ts +61 -0
- package/src/panels/access/access-panel.tsx +597 -0
- package/src/panels/access/alert-list.tsx +77 -0
- package/src/panels/access/constraint-creator.tsx +128 -0
- package/src/panels/access/constraint-list.tsx +72 -0
- package/src/panels/access/credential-list.tsx +68 -0
- package/src/panels/access/delegation-chain-view.tsx +110 -0
- package/src/panels/access/delegation-completer.tsx +120 -0
- package/src/panels/access/delegation-creator.tsx +237 -0
- package/src/panels/access/delegation-list.tsx +74 -0
- package/src/panels/access/fraud-score-view.tsx +94 -0
- package/src/panels/access/manifest-creator.tsx +167 -0
- package/src/panels/access/manifest-list.tsx +105 -0
- package/src/panels/access/namespace-config-view.tsx +525 -0
- package/src/panels/access/permission-checker.tsx +231 -0
- package/src/panels/agents/agent-status-view.tsx +196 -0
- package/src/panels/agents/agents-panel.tsx +493 -0
- package/src/panels/agents/delegation-list.tsx +154 -0
- package/src/panels/agents/inbox-view.tsx +96 -0
- package/src/panels/agents/trajectories-tab.tsx +40 -0
- package/src/panels/api-console/api-console-panel.tsx +189 -0
- package/src/panels/api-console/codegen-viewer.tsx +36 -0
- package/src/panels/api-console/codegen.ts +112 -0
- package/src/panels/api-console/endpoint-list.tsx +57 -0
- package/src/panels/api-console/request-builder.tsx +69 -0
- package/src/panels/api-console/response-viewer.tsx +54 -0
- package/src/panels/connectors/available-tab.tsx +357 -0
- package/src/panels/connectors/connector-row.tsx +121 -0
- package/src/panels/connectors/connectors-panel.tsx +88 -0
- package/src/panels/connectors/error-parser.ts +116 -0
- package/src/panels/connectors/mounted-tab.tsx +179 -0
- package/src/panels/connectors/skills-tab.tsx +235 -0
- package/src/panels/connectors/template-generator.ts +211 -0
- package/src/panels/connectors/write-tab.tsx +514 -0
- package/src/panels/events/audit-tab.tsx +69 -0
- package/src/panels/events/audit-trail.tsx +75 -0
- package/src/panels/events/connector-detail.tsx +49 -0
- package/src/panels/events/connector-list.tsx +73 -0
- package/src/panels/events/connectors-tab.tsx +92 -0
- package/src/panels/events/event-replay.tsx +80 -0
- package/src/panels/events/events-panel.tsx +414 -0
- package/src/panels/events/events-tab.tsx +212 -0
- package/src/panels/events/lock-list.tsx +54 -0
- package/src/panels/events/locks-tab.tsx +103 -0
- package/src/panels/events/mcl-replay.tsx +77 -0
- package/src/panels/events/mcl-tab.tsx +83 -0
- package/src/panels/events/operations-tab-wrapper.tsx +62 -0
- package/src/panels/events/operations-tab.tsx +41 -0
- package/src/panels/events/replay-tab.tsx +76 -0
- package/src/panels/events/secrets-audit.tsx +64 -0
- package/src/panels/events/secrets-tab.tsx +75 -0
- package/src/panels/events/subscription-list.tsx +54 -0
- package/src/panels/events/subscriptions-tab.tsx +82 -0
- package/src/panels/files/file-aspects.tsx +93 -0
- package/src/panels/files/file-editor.tsx +160 -0
- package/src/panels/files/file-explorer-keybindings.ts +468 -0
- package/src/panels/files/file-explorer-panel.tsx +545 -0
- package/src/panels/files/file-lineage.tsx +163 -0
- package/src/panels/files/file-list-item.tsx +28 -0
- package/src/panels/files/file-metadata.tsx +62 -0
- package/src/panels/files/file-preview.tsx +108 -0
- package/src/panels/files/file-schema.tsx +89 -0
- package/src/panels/files/file-tree-node.tsx +44 -0
- package/src/panels/files/file-tree.tsx +169 -0
- package/src/panels/files/share-links-tab.tsx +33 -0
- package/src/panels/files/uploads-tab.tsx +45 -0
- package/src/panels/payments/approval-list.tsx +83 -0
- package/src/panels/payments/balance-card.tsx +43 -0
- package/src/panels/payments/budget-card.tsx +70 -0
- package/src/panels/payments/payments-panel.tsx +451 -0
- package/src/panels/payments/policy-list.tsx +64 -0
- package/src/panels/payments/reservation-list.tsx +78 -0
- package/src/panels/payments/transaction-list.tsx +103 -0
- package/src/panels/payments/transfer-form.tsx +109 -0
- package/src/panels/search/column-search.tsx +79 -0
- package/src/panels/search/knowledge-view.tsx +100 -0
- package/src/panels/search/memory-list.tsx +197 -0
- package/src/panels/search/playbook-list.tsx +77 -0
- package/src/panels/search/rlm-answer-view.tsx +105 -0
- package/src/panels/search/search-panel.tsx +405 -0
- package/src/panels/search/search-results.tsx +116 -0
- package/src/panels/stack/stack-panel.tsx +474 -0
- package/src/panels/versions/conflicts-tab.tsx +59 -0
- package/src/panels/versions/entry-detail.tsx +89 -0
- package/src/panels/versions/transaction-actions.tsx +34 -0
- package/src/panels/versions/transaction-list.tsx +90 -0
- package/src/panels/versions/versions-panel.tsx +276 -0
- package/src/panels/workflows/execution-list.tsx +102 -0
- package/src/panels/workflows/scheduler-view.tsx +135 -0
- package/src/panels/workflows/workflow-list.tsx +88 -0
- package/src/panels/workflows/workflows-panel.tsx +295 -0
- package/src/panels/zones/brick-detail.tsx +136 -0
- package/src/panels/zones/brick-list.tsx +56 -0
- package/src/panels/zones/cache-tab.tsx +118 -0
- package/src/panels/zones/drift-view.tsx +97 -0
- package/src/panels/zones/mcp-mounts-tab.tsx +38 -0
- package/src/panels/zones/memories-tab.tsx +37 -0
- package/src/panels/zones/reindex-status.tsx +84 -0
- package/src/panels/zones/workspaces-tab.tsx +37 -0
- package/src/panels/zones/zone-list.tsx +73 -0
- package/src/panels/zones/zones-panel.tsx +559 -0
- package/src/services/command-runner.ts +303 -0
- package/src/shared/accessibility-announcements.ts +44 -0
- package/src/shared/action-registry.ts +466 -0
- package/src/shared/brick-states.ts +91 -0
- package/src/shared/command-palette.ts +35 -0
- package/src/shared/components/announcement-bar.tsx +30 -0
- package/src/shared/components/app-confirm-dialog.tsx +29 -0
- package/src/shared/components/breadcrumb.tsx +21 -0
- package/src/shared/components/brick-gate.tsx +60 -0
- package/src/shared/components/command-output.tsx +95 -0
- package/src/shared/components/command-palette.tsx +97 -0
- package/src/shared/components/confirm-dialog.tsx +61 -0
- package/src/shared/components/diff-viewer.tsx +219 -0
- package/src/shared/components/empty-state.tsx +36 -0
- package/src/shared/components/error-bar.tsx +60 -0
- package/src/shared/components/error-boundary.tsx +53 -0
- package/src/shared/components/help-overlay.tsx +99 -0
- package/src/shared/components/identity-switcher.tsx +168 -0
- package/src/shared/components/loading-indicator.tsx +40 -0
- package/src/shared/components/pagination-bar.tsx +68 -0
- package/src/shared/components/pre-connection-screen.tsx +398 -0
- package/src/shared/components/scroll-indicator.tsx +46 -0
- package/src/shared/components/side-nav-utils.ts +68 -0
- package/src/shared/components/side-nav.tsx +287 -0
- package/src/shared/components/spinner.tsx +26 -0
- package/src/shared/components/status-bar.tsx +117 -0
- package/src/shared/components/styled-text.tsx +72 -0
- package/src/shared/components/sub-tab-bar-utils.ts +100 -0
- package/src/shared/components/sub-tab-bar.tsx +40 -0
- package/src/shared/components/tab-bar-utils.ts +36 -0
- package/src/shared/components/tab-bar.tsx +50 -0
- package/src/shared/components/text-input.tsx +73 -0
- package/src/shared/components/tooltip.tsx +53 -0
- package/src/shared/components/virtual-list.tsx +93 -0
- package/src/shared/components/welcome-screen.tsx +111 -0
- package/src/shared/hooks/use-api.ts +10 -0
- package/src/shared/hooks/use-brick-available.ts +42 -0
- package/src/shared/hooks/use-confirm.ts +66 -0
- package/src/shared/hooks/use-connection-state.ts +67 -0
- package/src/shared/hooks/use-copy.ts +31 -0
- package/src/shared/hooks/use-fresh-server.ts +62 -0
- package/src/shared/hooks/use-keyboard.ts +58 -0
- package/src/shared/hooks/use-list-navigation.ts +106 -0
- package/src/shared/hooks/use-swr.ts +117 -0
- package/src/shared/hooks/use-tab-fallback.ts +32 -0
- package/src/shared/hooks/use-text-input.ts +113 -0
- package/src/shared/hooks/use-visible-tabs.ts +61 -0
- package/src/shared/lib/circular-buffer.ts +82 -0
- package/src/shared/lib/clipboard.ts +14 -0
- package/src/shared/nav-items.ts +73 -0
- package/src/shared/navigation.ts +110 -0
- package/src/shared/status-breadcrumb.ts +74 -0
- package/src/shared/syntax-style.ts +3 -0
- package/src/shared/tab-visibility.ts +15 -0
- package/src/shared/text-style.ts +23 -0
- package/src/shared/theme.ts +179 -0
- package/src/shared/utils/format-size.ts +20 -0
- package/src/shared/utils/format-text.ts +10 -0
- package/src/shared/utils/format-time.ts +72 -0
- package/src/shared/utils/lru-cache.ts +75 -0
- package/src/stores/access-store-types.ts +154 -0
- package/src/stores/access-store.ts +674 -0
- package/src/stores/agents-store.ts +404 -0
- package/src/stores/announcement-store.ts +46 -0
- package/src/stores/api-console-store.ts +476 -0
- package/src/stores/connectors-store.ts +434 -0
- package/src/stores/create-api-action.ts +140 -0
- package/src/stores/delegation-store.ts +300 -0
- package/src/stores/error-store.ts +102 -0
- package/src/stores/events-store.ts +163 -0
- package/src/stores/files-store.ts +630 -0
- package/src/stores/first-run-store.ts +34 -0
- package/src/stores/global-store.ts +255 -0
- package/src/stores/infra-store.ts +461 -0
- package/src/stores/knowledge-store.ts +358 -0
- package/src/stores/lineage-store.ts +126 -0
- package/src/stores/mcp-store.ts +147 -0
- package/src/stores/payments-store.ts +545 -0
- package/src/stores/search-store-types.ts +155 -0
- package/src/stores/search-store.ts +656 -0
- package/src/stores/share-link-store.ts +151 -0
- package/src/stores/stack-store.ts +352 -0
- package/src/stores/ui-store.ts +161 -0
- package/src/stores/upload-store.ts +131 -0
- package/src/stores/versions-store.ts +355 -0
- package/src/stores/workflows-store.ts +402 -0
- package/src/stores/workspace-store.ts +185 -0
- 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
|
+
}
|