@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
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @nexus-ai-fs/tui
2
+
3
+ Terminal UI for Nexus.
4
+
5
+ ## Usage
6
+
7
+ Run the published package with Bun:
8
+
9
+ ```bash
10
+ bunx @nexus-ai-fs/tui
11
+ bunx @nexus-ai-fs/tui --url http://remote:2026 --api-key KEY
12
+ ```
13
+
14
+ The installed binary name is:
15
+
16
+ ```bash
17
+ nexus-tui
18
+ ```
19
+
20
+ ## Local Development
21
+
22
+ ```bash
23
+ cd packages/nexus-api-client
24
+ npm install
25
+ npm run build
26
+
27
+ cd packages/nexus-tui
28
+ bun install
29
+ bun run src/index.tsx
30
+ ```
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@nexus-ai-fs/tui",
3
+ "version": "0.9.18",
4
+ "description": "Terminal UI for Nexus — file explorer, API inspector, and monitoring dashboard",
5
+ "type": "module",
6
+ "bin": {
7
+ "nexus-tui": "./src/index.tsx"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "dev": "bun run src/index.tsx",
15
+ "start": "bun run src/index.tsx",
16
+ "test": "bun test",
17
+ "lint": "tsc --noEmit",
18
+ "clean": "rm -rf dist"
19
+ },
20
+ "dependencies": {
21
+ "@nexus-ai-fs/api-client": "^0.9.18",
22
+ "@opentui/core": "latest",
23
+ "@opentui/react": "latest",
24
+ "anser": "^2.3.5",
25
+ "react": "^19.0.0",
26
+ "zustand": "^5.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/bun": "latest",
30
+ "@types/react": "^19.0.0",
31
+ "typescript": "^5.5.0"
32
+ },
33
+ "keywords": [
34
+ "nexus",
35
+ "tui",
36
+ "terminal",
37
+ "opentui"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "license": "MIT",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/nexi-lab/nexus",
46
+ "directory": "packages/nexus-tui"
47
+ }
48
+ }
package/src/app.tsx ADDED
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Root application component.
3
+ *
4
+ * Lazy-loads panels on first navigation for fast startup.
5
+ * Shows PreConnectionScreen when the server is unavailable (Decision 3A).
6
+ */
7
+
8
+ import React, { lazy, Suspense, useState, useCallback, useEffect, useRef } from "react";
9
+ import { useGlobalStore, type PanelId } from "./stores/global-store.js";
10
+ import { useUiStore } from "./stores/ui-store.js";
11
+ import { useErrorStore } from "./stores/error-store.js";
12
+ import { useAnnouncementStore } from "./stores/announcement-store.js";
13
+ import { TabBar, type Tab } from "./shared/components/tab-bar.js";
14
+ import { StatusBar } from "./shared/components/status-bar.js";
15
+ import { ErrorBar } from "./shared/components/error-bar.js";
16
+ import { AnnouncementBar } from "./shared/components/announcement-bar.js";
17
+ import { ErrorBoundary } from "./shared/components/error-boundary.js";
18
+ import { Spinner } from "./shared/components/spinner.js";
19
+ import { useKeyboard } from "./shared/hooks/use-keyboard.js";
20
+ import { IdentitySwitcher } from "./shared/components/identity-switcher.js";
21
+ import { AppConfirmDialog } from "./shared/components/app-confirm-dialog.js";
22
+ import { HelpOverlay } from "./shared/components/help-overlay.js";
23
+ import { WelcomeScreen } from "./shared/components/welcome-screen.js";
24
+ import { PreConnectionScreen } from "./shared/components/pre-connection-screen.js";
25
+ import { CommandPalette } from "./shared/components/command-palette.js";
26
+ import { type CommandPaletteItem } from "./shared/command-palette.js";
27
+ import { useFreshServer } from "./shared/hooks/use-fresh-server.js";
28
+ import { detectConnectionState } from "./shared/hooks/use-connection-state.js";
29
+ import { useVisibleTabs, type TabDef } from "./shared/hooks/use-visible-tabs.js";
30
+ import { killAllProcesses } from "./services/command-runner.js";
31
+ import { PANEL_DESCRIPTORS } from "./shared/navigation.js";
32
+ import {
33
+ formatConnectionAnnouncement,
34
+ formatErrorAnnouncement,
35
+ formatPanelAnnouncement,
36
+ } from "./shared/accessibility-announcements.js";
37
+
38
+ // Lazy-loaded panels
39
+ const FileExplorerPanel = lazy(() => import("./panels/files/file-explorer-panel.js"));
40
+ const VersionsPanel = lazy(() => import("./panels/versions/versions-panel.js"));
41
+ const AgentsPanel = lazy(() => import("./panels/agents/agents-panel.js"));
42
+ const ZonesPanel = lazy(() => import("./panels/zones/zones-panel.js"));
43
+ const AccessPanel = lazy(() => import("./panels/access/access-panel.js"));
44
+ const PaymentsPanel = lazy(() => import("./panels/payments/payments-panel.js"));
45
+ const SearchPanel = lazy(() => import("./panels/search/search-panel.js"));
46
+ const WorkflowsPanel = lazy(() => import("./panels/workflows/workflows-panel.js"));
47
+ const EventsPanel = lazy(() => import("./panels/events/events-panel.js"));
48
+ const ApiConsolePanel = lazy(() => import("./panels/api-console/api-console-panel.js"));
49
+
50
+ type AppTab = Tab & TabDef<PanelId>;
51
+
52
+ const TABS: readonly AppTab[] = [
53
+ { id: "files", label: "Files", shortcut: "1", brick: null },
54
+ { id: "versions", label: "Ver", shortcut: "2", brick: "versioning" },
55
+ { id: "agents", label: "Agent", shortcut: "3", brick: ["agent_runtime", "delegation", "ipc"] },
56
+ { id: "zones", label: "Zone", shortcut: "4", brick: null },
57
+ { id: "access", label: "ACL", shortcut: "5", brick: ["access_manifest", "governance", "auth", "delegation"] },
58
+ { id: "payments", label: "Pay", shortcut: "6", brick: "pay" },
59
+ { id: "search", label: "Find", shortcut: "7", brick: null },
60
+ { id: "workflows", label: "Flow", shortcut: "8", brick: "workflows" },
61
+ { id: "infrastructure", label: "Event", shortcut: "9", brick: null },
62
+ { id: "console", label: "CLI", shortcut: "0", brick: null },
63
+ ];
64
+
65
+ function PanelRouter(): React.ReactNode {
66
+ const activePanel = useGlobalStore((s) => s.activePanel);
67
+
68
+ switch (activePanel) {
69
+ case "files":
70
+ return <FileExplorerPanel />;
71
+ case "versions":
72
+ return <VersionsPanel />;
73
+ case "agents":
74
+ return <AgentsPanel />;
75
+ case "zones":
76
+ return <ZonesPanel />;
77
+ case "access":
78
+ return <AccessPanel />;
79
+ case "payments":
80
+ return <PaymentsPanel />;
81
+ case "search":
82
+ return <SearchPanel />;
83
+ case "workflows":
84
+ return <WorkflowsPanel />;
85
+ case "infrastructure":
86
+ return <EventsPanel />;
87
+ case "console":
88
+ return <ApiConsolePanel />;
89
+ default:
90
+ return (
91
+ <box height="100%" width="100%" justifyContent="center" alignItems="center">
92
+ <text>{`Unknown panel: "${activePanel}"`}</text>
93
+ </box>
94
+ );
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Graceful shutdown: kill child processes, restore terminal, then exit (Decision 6A).
100
+ *
101
+ * We must manually reset the terminal before process.exit() because exit()
102
+ * bypasses OpenTUI's renderer.destroy() cleanup, leaving mouse tracking
103
+ * and alternate screen enabled — which causes raw escape sequences to leak
104
+ * into the shell after exit.
105
+ */
106
+ function shutdown(): void {
107
+ killAllProcesses();
108
+
109
+ // Restore stdin from raw mode first (stops reading mouse input)
110
+ if (process.stdin.setRawMode) {
111
+ process.stdin.setRawMode(false);
112
+ }
113
+ process.stdin.pause();
114
+
115
+ // Restore terminal: disable mouse tracking, leave alternate screen, show cursor.
116
+ // Use writeSync via fd to guarantee the sequences are flushed before exit.
117
+ const fs = require("fs");
118
+ const reset = [
119
+ "\x1b[?1003l", // disable all-motion mouse tracking
120
+ "\x1b[?1006l", // disable SGR mouse mode
121
+ "\x1b[?1000l", // disable normal mouse tracking
122
+ "\x1b[?1049l", // switch back to main screen
123
+ "\x1b[?25h", // show cursor
124
+ ].join("");
125
+ fs.writeSync(1, reset);
126
+
127
+ process.exit(0);
128
+ }
129
+
130
+ export function App(): React.ReactNode {
131
+ const activePanel = useGlobalStore((s) => s.activePanel);
132
+ const setActivePanel = useGlobalStore((s) => s.setActivePanel);
133
+ const connectionStatus = useGlobalStore((s) => s.connectionStatus);
134
+ const connectionError = useGlobalStore((s) => s.connectionError);
135
+ const config = useGlobalStore((s) => s.config);
136
+ const latestError = useErrorStore((s) => (s.errors.length > 0 ? s.errors[s.errors.length - 1] : null));
137
+ const announce = useAnnouncementStore((s) => s.announce);
138
+ const toggleZoom = useUiStore((s) => s.toggleZoom);
139
+ const zoomedPanel = useUiStore((s) => s.zoomedPanel);
140
+ const [identitySwitcherOpen, setIdentitySwitcherOpen] = useState(false);
141
+ const [helpOpen, setHelpOpen] = useState(false);
142
+ const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
143
+ const visibleTabs = useVisibleTabs(TABS);
144
+ const tabBarTabs = visibleTabs as readonly AppTab[];
145
+ const { isFresh } = useFreshServer();
146
+ const [welcomeDismissed, setWelcomeDismissed] = useState(false);
147
+ const showWelcome = isFresh === true && !welcomeDismissed;
148
+
149
+ // Determine if we should show the pre-connection screen (Decision 3A)
150
+ // Only hide it when fully connected — "connecting" still shows the pre-connection
151
+ // screen with a spinner to avoid flashing the main UI during connection attempts.
152
+ const connState = detectConnectionState(connectionStatus, connectionError, config);
153
+ const showPreConnection = connState !== "ready";
154
+ const previousPanelRef = useRef<PanelId | null>(null);
155
+ const previousConnectionRef = useRef(connectionStatus);
156
+ const lastErrorIdRef = useRef<string | null>(null);
157
+ const panelLabel = PANEL_DESCRIPTORS[activePanel]?.breadcrumbLabel ?? activePanel;
158
+
159
+ const setOverlayActive = useUiStore((s) => s.setOverlayActive);
160
+ useEffect(() => {
161
+ setOverlayActive(identitySwitcherOpen || helpOpen || commandPaletteOpen || showWelcome);
162
+ }, [identitySwitcherOpen, helpOpen, commandPaletteOpen, showWelcome, setOverlayActive]);
163
+
164
+ useEffect(() => {
165
+ const visibleIds = visibleTabs.map((tab) => tab.id);
166
+ if (visibleIds.length > 0 && !visibleIds.includes(activePanel)) {
167
+ setActivePanel(visibleIds[0]!);
168
+ }
169
+ }, [activePanel, setActivePanel, visibleTabs]);
170
+
171
+ useEffect(() => {
172
+ if (previousPanelRef.current !== null && previousPanelRef.current !== activePanel) {
173
+ announce(formatPanelAnnouncement(panelLabel));
174
+ }
175
+ previousPanelRef.current = activePanel;
176
+ }, [activePanel, panelLabel, announce]);
177
+
178
+ useEffect(() => {
179
+ if (previousConnectionRef.current !== connectionStatus) {
180
+ announce(
181
+ formatConnectionAnnouncement(connectionStatus, connectionError),
182
+ connectionStatus === "error" ? "error" : connectionStatus === "connected" ? "success" : "info",
183
+ );
184
+ }
185
+ previousConnectionRef.current = connectionStatus;
186
+ }, [connectionStatus, connectionError, announce]);
187
+
188
+ useEffect(() => {
189
+ if (!latestError || lastErrorIdRef.current === latestError.id) return;
190
+ lastErrorIdRef.current = latestError.id;
191
+ if (
192
+ latestError.source === "global"
193
+ && connectionStatus === "error"
194
+ && latestError.message === connectionError
195
+ ) {
196
+ return;
197
+ }
198
+ announce(formatErrorAnnouncement(latestError.message), "error");
199
+ }, [latestError, connectionStatus, connectionError, announce]);
200
+
201
+ const toggleIdentitySwitcher = useCallback(() => {
202
+ setIdentitySwitcherOpen((prev) => !prev);
203
+ }, []);
204
+
205
+ const closeIdentitySwitcher = useCallback(() => {
206
+ setIdentitySwitcherOpen(false);
207
+ }, []);
208
+
209
+ const closeCommandPalette = useCallback(() => {
210
+ setCommandPaletteOpen(false);
211
+ }, []);
212
+
213
+ const commandPaletteItems = React.useMemo<readonly CommandPaletteItem[]>(() => {
214
+ const panelCommands: CommandPaletteItem[] = tabBarTabs.map((tab) => ({
215
+ id: `panel:${tab.id}`,
216
+ title: `Switch to ${tab.label}`,
217
+ section: "Panels",
218
+ hint: tab.shortcut,
219
+ keywords: [tab.id, tab.label, "panel", "switch"],
220
+ run: () => setActivePanel(tab.id as PanelId),
221
+ }));
222
+
223
+ const appCommands: CommandPaletteItem[] = [
224
+ {
225
+ id: "app:help",
226
+ title: "Open help overlay",
227
+ section: "Global",
228
+ hint: "?",
229
+ keywords: ["help", "shortcuts", "bindings"],
230
+ run: () => setHelpOpen(true),
231
+ },
232
+ {
233
+ id: "app:identity",
234
+ title: "Open identity switcher",
235
+ section: "Global",
236
+ hint: "Ctrl+I",
237
+ keywords: ["identity", "agent", "subject", "zone"],
238
+ run: () => setIdentitySwitcherOpen(true),
239
+ },
240
+ {
241
+ id: "app:disconnect",
242
+ title: "Disconnect and return to setup",
243
+ section: "Global",
244
+ hint: "Ctrl+D",
245
+ keywords: ["disconnect", "setup", "reconnect"],
246
+ run: () => useGlobalStore.getState().setConnectionStatus("error", "Disconnected by user"),
247
+ },
248
+ {
249
+ id: "app:zoom",
250
+ title: zoomedPanel === activePanel ? "Exit zoom" : `Zoom ${activePanel}`,
251
+ section: "Global",
252
+ hint: "z",
253
+ keywords: ["zoom", "fullscreen", activePanel],
254
+ run: () => toggleZoom(activePanel),
255
+ },
256
+ {
257
+ id: "app:quit",
258
+ title: "Quit Nexus TUI",
259
+ section: "Global",
260
+ hint: "q",
261
+ keywords: ["quit", "exit", "close"],
262
+ run: shutdown,
263
+ },
264
+ ];
265
+
266
+ return [...appCommands, ...panelCommands];
267
+ }, [tabBarTabs, setActivePanel, zoomedPanel, activePanel, toggleZoom]);
268
+
269
+ useKeyboard(
270
+ showPreConnection
271
+ ? {
272
+ // Pre-connection screen handles its own keybindings
273
+ "q": shutdown,
274
+ }
275
+ : identitySwitcherOpen || helpOpen || commandPaletteOpen || showWelcome
276
+ ? {
277
+ // When an overlay is open, only dismiss keys work from app level.
278
+ "ctrl+i": toggleIdentitySwitcher,
279
+ }
280
+ : {
281
+ // Check fileEditorOpen synchronously inside each handler so we don't
282
+ // depend on React re-render timing — OpenTUI broadcasts to ALL handlers.
283
+ "1": () => { if (!useUiStore.getState().fileEditorOpen) setActivePanel("files"); },
284
+ "2": () => { if (!useUiStore.getState().fileEditorOpen) setActivePanel("versions"); },
285
+ "3": () => { if (!useUiStore.getState().fileEditorOpen) setActivePanel("agents"); },
286
+ "4": () => { if (!useUiStore.getState().fileEditorOpen) setActivePanel("zones"); },
287
+ "5": () => { if (!useUiStore.getState().fileEditorOpen) setActivePanel("access"); },
288
+ "6": () => { if (!useUiStore.getState().fileEditorOpen) setActivePanel("payments"); },
289
+ "7": () => { if (!useUiStore.getState().fileEditorOpen) setActivePanel("search"); },
290
+ "8": () => { if (!useUiStore.getState().fileEditorOpen) setActivePanel("workflows"); },
291
+ "9": () => { if (!useUiStore.getState().fileEditorOpen) setActivePanel("infrastructure"); },
292
+ "0": () => { if (!useUiStore.getState().fileEditorOpen) setActivePanel("console"); },
293
+ "ctrl+p": () => { if (!useUiStore.getState().fileEditorOpen) setCommandPaletteOpen(true); },
294
+ ":": () => { if (!useUiStore.getState().fileEditorOpen) setCommandPaletteOpen(true); },
295
+ "ctrl+i": toggleIdentitySwitcher,
296
+ "ctrl+d": () => {
297
+ // Disconnect and go back to setup menu
298
+ useGlobalStore.getState().setConnectionStatus("error", "Disconnected by user");
299
+ },
300
+ "z": () => { if (!useUiStore.getState().fileEditorOpen) toggleZoom(activePanel); },
301
+ "?": () => { if (!useUiStore.getState().fileEditorOpen) setHelpOpen(true); },
302
+ "q": () => { if (!useUiStore.getState().fileEditorOpen) shutdown(); },
303
+ },
304
+ );
305
+
306
+ // Pre-connection screen (Decision 3A): shown when server is unavailable
307
+ if (showPreConnection) {
308
+ return (
309
+ <box height="100%" width="100%" flexDirection="column">
310
+ <PreConnectionScreen />
311
+ <StatusBar />
312
+ </box>
313
+ );
314
+ }
315
+
316
+ return (
317
+ <box height="100%" width="100%" flexDirection="column">
318
+ {/* Tab bar (hidden when zoomed) */}
319
+ {!zoomedPanel && <TabBar tabs={tabBarTabs} activeTab={activePanel} onSelect={(id) => setActivePanel(id as PanelId)} />}
320
+
321
+ {/* Main content */}
322
+ <box flexGrow={1}>
323
+ <ErrorBoundary>
324
+ <Suspense
325
+ fallback={
326
+ <box height="100%" width="100%" justifyContent="center" alignItems="center">
327
+ <Spinner label="Loading panel..." />
328
+ </box>
329
+ }
330
+ >
331
+ <PanelRouter />
332
+ </Suspense>
333
+ </ErrorBoundary>
334
+ </box>
335
+
336
+ {/* Overlays */}
337
+ {showWelcome && <WelcomeScreen onDismiss={() => setWelcomeDismissed(true)} />}
338
+ <IdentitySwitcher visible={identitySwitcherOpen} onClose={closeIdentitySwitcher} />
339
+ <CommandPalette visible={commandPaletteOpen} commands={commandPaletteItems} onClose={closeCommandPalette} />
340
+ <AppConfirmDialog />
341
+ <HelpOverlay visible={helpOpen} panel={activePanel} onDismiss={() => setHelpOpen(false)} />
342
+
343
+ {/* Error bar + Status bar */}
344
+ <AnnouncementBar />
345
+ <ErrorBar />
346
+ <StatusBar />
347
+ </box>
348
+ );
349
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * nexus-tui entry point.
4
+ *
5
+ * Parses CLI args, resolves config, and renders the TUI via OpenTUI.
6
+ *
7
+ * Usage:
8
+ * bunx @nexus-ai-fs/tui
9
+ * bunx @nexus-ai-fs/tui --url http://remote:2026 --api-key nx_live_myagent
10
+ * bunx @nexus-ai-fs/tui --agent-id bot-worker-1 --zone-id org_acme
11
+ */
12
+
13
+ import { createCliRenderer } from "@opentui/core";
14
+ import { createRoot } from "@opentui/react";
15
+ import { resolveConfig } from "@nexus-ai-fs/api-client";
16
+ import { useGlobalStore } from "./stores/global-store.js";
17
+ import { App } from "./app.js";
18
+
19
+ interface CliArgs {
20
+ url?: string;
21
+ apiKey?: string;
22
+ agentId?: string;
23
+ subject?: string;
24
+ zoneId?: string;
25
+ }
26
+
27
+ // Parse CLI arguments
28
+ function parseArgs(): CliArgs {
29
+ const args = process.argv.slice(2);
30
+ const result: CliArgs = {};
31
+
32
+ for (let i = 0; i < args.length; i++) {
33
+ const arg = args[i];
34
+ const next = args[i + 1];
35
+
36
+ if ((arg === "--url" || arg === "-u") && next) {
37
+ result.url = next;
38
+ i++;
39
+ } else if ((arg === "--api-key" || arg === "-k") && next) {
40
+ result.apiKey = next;
41
+ i++;
42
+ } else if ((arg === "--agent-id") && next) {
43
+ result.agentId = next;
44
+ i++;
45
+ } else if ((arg === "--subject") && next) {
46
+ result.subject = next;
47
+ i++;
48
+ } else if ((arg === "--zone-id") && next) {
49
+ result.zoneId = next;
50
+ i++;
51
+ } else if (arg === "--help" || arg === "-h") {
52
+ console.log(`
53
+ nexus-tui — Terminal UI for Nexus
54
+
55
+ Usage:
56
+ nexus-tui [options]
57
+
58
+ Published package:
59
+ bunx @nexus-ai-fs/tui [options]
60
+
61
+ Options:
62
+ --url, -u <url> Nexus server URL (default: NEXUS_URL or http://localhost:2026)
63
+ --api-key, -k <key> API key (default: NEXUS_API_KEY env var)
64
+ --agent-id <id> Agent identity (X-Agent-ID header)
65
+ --subject <subject> Subject override (X-Nexus-Subject header)
66
+ --zone-id <id> Zone isolation (X-Nexus-Zone-ID header)
67
+ --help, -h Show this help message
68
+
69
+ Environment Variables:
70
+ NEXUS_URL Server URL
71
+ NEXUS_API_KEY API key
72
+ NEXUS_AGENT_ID Agent identity
73
+ NEXUS_SUBJECT Subject override
74
+ NEXUS_ZONE_ID Zone isolation
75
+
76
+ Config File:
77
+ ~/.nexus/config.yaml Auto-discovered (same as nexus CLI)
78
+
79
+ Local development:
80
+ cd packages/nexus-api-client && npm install && npm run build
81
+ cd packages/nexus-tui && bun install && bun run src/index.tsx
82
+ `.trim());
83
+ process.exit(0);
84
+ }
85
+ }
86
+
87
+ return result;
88
+ }
89
+
90
+ async function main(): Promise<void> {
91
+ const cliArgs = parseArgs();
92
+
93
+ // Resolve config: CLI args > env vars > config file > defaults
94
+ // Disable key transformation — all TUI store types use snake_case matching the API wire format
95
+ const config = resolveConfig({
96
+ baseUrl: cliArgs.url,
97
+ apiKey: cliArgs.apiKey,
98
+ agentId: cliArgs.agentId,
99
+ subject: cliArgs.subject,
100
+ zoneId: cliArgs.zoneId,
101
+ transformKeys: false,
102
+ });
103
+
104
+ // Initialize global store — testConnection() is called automatically by initConfig()
105
+ // when a client is available (consolidates health + features + auth check, Decision 5A)
106
+ useGlobalStore.getState().initConfig(config);
107
+
108
+ // Create OpenTUI renderer and mount the React tree
109
+ const renderer = await createCliRenderer({
110
+ exitOnCtrlC: true,
111
+ useAlternateScreen: true,
112
+ });
113
+
114
+ createRoot(renderer).render(<App />);
115
+ }
116
+
117
+ main().catch((err) => {
118
+ // Restore terminal state in case OpenTUI already enabled raw mode / alternate screen.
119
+ // These sequences are no-ops if the terminal was never switched, so always safe to write.
120
+ if (process.stdin.setRawMode) {
121
+ process.stdin.setRawMode(false);
122
+ }
123
+ process.stdin.pause();
124
+
125
+ const fs = require("fs");
126
+ const reset = [
127
+ "\x1b[?1003l", // disable all-motion mouse tracking
128
+ "\x1b[?1006l", // disable SGR mouse mode
129
+ "\x1b[?1000l", // disable normal mouse tracking
130
+ "\x1b[?1049l", // switch back to main screen
131
+ "\x1b[?25h", // show cursor
132
+ ].join("");
133
+ fs.writeSync(1, reset);
134
+
135
+ console.error("Fatal error:", err);
136
+ process.exit(1);
137
+ });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Type augmentations for OpenTUI v0.1.87.
3
+ *
4
+ * Fixes ElementClass definition (instance vs constructor mismatch).
5
+ * The original declares `ElementClass extends React.ComponentClass<any>`
6
+ * which incorrectly requires the constructor type rather than the
7
+ * instance type, preventing class components from being used as JSX.
8
+ */
9
+ import type React from "react";
10
+ import type { RGBA } from "@opentui/core";
11
+
12
+ declare module "@opentui/react/jsx-runtime" {
13
+ namespace JSX {
14
+ // Fix: ElementClass should describe instances, not constructors.
15
+ interface ElementClass {
16
+ render(): React.ReactNode;
17
+ }
18
+ }
19
+ }
20
+
21
+ declare module "@opentui/core/renderables/Text" {
22
+ interface TextOptions {
23
+ foregroundColor?: string | RGBA;
24
+ backgroundColor?: string | RGBA;
25
+ bold?: boolean;
26
+ dimColor?: boolean;
27
+ inverse?: boolean;
28
+ underline?: boolean;
29
+ }
30
+ }
31
+
32
+ declare module "@opentui/core/renderables/TextNode" {
33
+ interface TextNodeOptions {
34
+ foregroundColor?: string | RGBA;
35
+ backgroundColor?: string | RGBA;
36
+ bold?: boolean;
37
+ dimColor?: boolean;
38
+ inverse?: boolean;
39
+ underline?: boolean;
40
+ }
41
+ }
42
+
43
+ declare module "@opentui/core" {
44
+ interface TextOptions {
45
+ foregroundColor?: string | RGBA;
46
+ backgroundColor?: string | RGBA;
47
+ bold?: boolean;
48
+ dimColor?: boolean;
49
+ inverse?: boolean;
50
+ underline?: boolean;
51
+ }
52
+
53
+ interface TextNodeOptions {
54
+ foregroundColor?: string | RGBA;
55
+ backgroundColor?: string | RGBA;
56
+ bold?: boolean;
57
+ dimColor?: boolean;
58
+ inverse?: boolean;
59
+ underline?: boolean;
60
+ }
61
+ }