@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
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
|
+
}
|