@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,559 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zones panel: tabbed layout with Zones list, Bricks health, and Drift report.
|
|
3
|
+
*
|
|
4
|
+
* Keybindings are context-aware — only actions valid for the selected brick's
|
|
5
|
+
* current state are active and displayed in the help bar.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
9
|
+
import { useZonesStore } from "../../stores/zones-store.js";
|
|
10
|
+
import { useWorkspaceStore } from "../../stores/workspace-store.js";
|
|
11
|
+
import { useMcpStore } from "../../stores/mcp-store.js";
|
|
12
|
+
import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
|
|
13
|
+
import { jumpToStart, jumpToEnd } from "../../shared/hooks/use-list-navigation.js";
|
|
14
|
+
import { useApi } from "../../shared/hooks/use-api.js";
|
|
15
|
+
import { useVisibleTabs } from "../../shared/hooks/use-visible-tabs.js";
|
|
16
|
+
import { SubTabBar } from "../../shared/components/sub-tab-bar.js";
|
|
17
|
+
import { subTabCycleBindings } from "../../shared/components/sub-tab-bar-utils.js";
|
|
18
|
+
import { useTabFallback } from "../../shared/hooks/use-tab-fallback.js";
|
|
19
|
+
import { ZoneList } from "./zone-list.js";
|
|
20
|
+
import { BrickList } from "./brick-list.js";
|
|
21
|
+
import { BrickDetail } from "./brick-detail.js";
|
|
22
|
+
import { DriftView } from "./drift-view.js";
|
|
23
|
+
import { ReindexStatus } from "./reindex-status.js";
|
|
24
|
+
import { WorkspacesTab } from "./workspaces-tab.js";
|
|
25
|
+
import { McpMountsTab } from "./mcp-mounts-tab.js";
|
|
26
|
+
import { CacheTab } from "./cache-tab.js";
|
|
27
|
+
import { ConfirmDialog } from "../../shared/components/confirm-dialog.js";
|
|
28
|
+
import { allowedActionsForState } from "../../shared/brick-states.js";
|
|
29
|
+
import { LoadingIndicator } from "../../shared/components/loading-indicator.js";
|
|
30
|
+
import { useUiStore } from "../../stores/ui-store.js";
|
|
31
|
+
import { focusColor } from "../../shared/theme.js";
|
|
32
|
+
import { ZONE_TABS } from "../../shared/navigation.js";
|
|
33
|
+
export default function ZonesPanel(): React.ReactNode {
|
|
34
|
+
const client = useApi();
|
|
35
|
+
const visibleTabs = useVisibleTabs(ZONE_TABS);
|
|
36
|
+
|
|
37
|
+
const zones = useZonesStore((s) => s.zones);
|
|
38
|
+
const zonesLoading = useZonesStore((s) => s.zonesLoading);
|
|
39
|
+
const bricks = useZonesStore((s) => s.bricks);
|
|
40
|
+
const bricksHealth = useZonesStore((s) => s.bricksHealth);
|
|
41
|
+
const selectedIndex = useZonesStore((s) => s.selectedIndex);
|
|
42
|
+
const activeTab = useZonesStore((s) => s.activeTab);
|
|
43
|
+
const isLoading = useZonesStore((s) => s.isLoading);
|
|
44
|
+
const brickDetail = useZonesStore((s) => s.brickDetail);
|
|
45
|
+
const detailLoading = useZonesStore((s) => s.detailLoading);
|
|
46
|
+
const driftReport = useZonesStore((s) => s.driftReport);
|
|
47
|
+
const driftLoading = useZonesStore((s) => s.driftLoading);
|
|
48
|
+
const error = useZonesStore((s) => s.error);
|
|
49
|
+
|
|
50
|
+
const fetchZones = useZonesStore((s) => s.fetchZones);
|
|
51
|
+
const fetchBricks = useZonesStore((s) => s.fetchBricks);
|
|
52
|
+
const fetchBrickDetail = useZonesStore((s) => s.fetchBrickDetail);
|
|
53
|
+
const fetchDrift = useZonesStore((s) => s.fetchDrift);
|
|
54
|
+
const mountBrick = useZonesStore((s) => s.mountBrick);
|
|
55
|
+
const unmountBrick = useZonesStore((s) => s.unmountBrick);
|
|
56
|
+
const unregisterBrick = useZonesStore((s) => s.unregisterBrick);
|
|
57
|
+
const remountBrick = useZonesStore((s) => s.remountBrick);
|
|
58
|
+
const resetBrick = useZonesStore((s) => s.resetBrick);
|
|
59
|
+
const cacheStats = useZonesStore((s) => s.cacheStats);
|
|
60
|
+
const cacheStatsLoading = useZonesStore((s) => s.cacheStatsLoading);
|
|
61
|
+
const hotFiles = useZonesStore((s) => s.hotFiles);
|
|
62
|
+
const hotFilesLoading = useZonesStore((s) => s.hotFilesLoading);
|
|
63
|
+
const fetchCacheStats = useZonesStore((s) => s.fetchCacheStats);
|
|
64
|
+
const fetchHotFiles = useZonesStore((s) => s.fetchHotFiles);
|
|
65
|
+
const warmupCache = useZonesStore((s) => s.warmupCache);
|
|
66
|
+
const setSelectedIndex = useZonesStore((s) => s.setSelectedIndex);
|
|
67
|
+
const setActiveTab = useZonesStore((s) => s.setActiveTab);
|
|
68
|
+
|
|
69
|
+
// Workspace store selectors
|
|
70
|
+
const workspaces = useWorkspaceStore((s) => s.workspaces);
|
|
71
|
+
const workspacesLoading = useWorkspaceStore((s) => s.workspacesLoading);
|
|
72
|
+
const selectedWorkspaceIndex = useWorkspaceStore((s) => s.selectedWorkspaceIndex);
|
|
73
|
+
const fetchWorkspaces = useWorkspaceStore((s) => s.fetchWorkspaces);
|
|
74
|
+
const unregisterWorkspace = useWorkspaceStore((s) => s.unregisterWorkspace);
|
|
75
|
+
const setSelectedWorkspaceIndex = useWorkspaceStore((s) => s.setSelectedWorkspaceIndex);
|
|
76
|
+
const registerWorkspace = useWorkspaceStore((s) => s.registerWorkspace);
|
|
77
|
+
|
|
78
|
+
// MCP store selectors
|
|
79
|
+
const mcpMounts = useMcpStore((s) => s.mounts);
|
|
80
|
+
const mcpMountsLoading = useMcpStore((s) => s.mountsLoading);
|
|
81
|
+
const selectedMountIndex = useMcpStore((s) => s.selectedMountIndex);
|
|
82
|
+
const fetchMcpMounts = useMcpStore((s) => s.fetchMounts);
|
|
83
|
+
const unmountServer = useMcpStore((s) => s.unmountServer);
|
|
84
|
+
const syncServer = useMcpStore((s) => s.syncServer);
|
|
85
|
+
const fetchTools = useMcpStore((s) => s.fetchTools);
|
|
86
|
+
const mountServer = useMcpStore((s) => s.mountServer);
|
|
87
|
+
const setSelectedMountIndex = useMcpStore((s) => s.setSelectedMountIndex);
|
|
88
|
+
|
|
89
|
+
// Focus pane (ui-store)
|
|
90
|
+
const uiFocusPane = useUiStore((s) => s.getFocusPane("zones"));
|
|
91
|
+
const toggleFocus = useUiStore((s) => s.toggleFocusPane);
|
|
92
|
+
const overlayActive = useUiStore((s) => s.overlayActive);
|
|
93
|
+
|
|
94
|
+
useTabFallback(visibleTabs, activeTab, setActiveTab);
|
|
95
|
+
|
|
96
|
+
// Track in-flight brick operations (mount, unmount, reset, etc.)
|
|
97
|
+
const [operationInProgress, setOperationInProgress] = useState(false);
|
|
98
|
+
|
|
99
|
+
// Input mode state for create/register flows (multi-field forms)
|
|
100
|
+
const [inputMode, setInputMode] = useState<"none" | "workspace" | "mcpMount">("none");
|
|
101
|
+
const [inputFields, setInputFields] = useState<Record<string, string>>({});
|
|
102
|
+
const [inputActiveField, setInputActiveField] = useState(0);
|
|
103
|
+
|
|
104
|
+
const WS_FIELDS = ["path", "name", "description", "scope", "ttl_seconds"] as const;
|
|
105
|
+
const MCP_FIELDS = ["name", "command_or_url", "description"] as const;
|
|
106
|
+
|
|
107
|
+
const currentFields = inputMode === "workspace" ? WS_FIELDS
|
|
108
|
+
: inputMode === "mcpMount" ? MCP_FIELDS : [] as const;
|
|
109
|
+
const currentFieldName = currentFields[inputActiveField] ?? "";
|
|
110
|
+
|
|
111
|
+
// Confirmation dialog state for destructive actions
|
|
112
|
+
const [confirmUnregister, setConfirmUnregister] = useState(false);
|
|
113
|
+
const [confirmWorkspaceUnregister, setConfirmWorkspaceUnregister] = useState(false);
|
|
114
|
+
const [confirmMcpUnmount, setConfirmMcpUnmount] = useState(false);
|
|
115
|
+
|
|
116
|
+
const anyDialogOpen = confirmUnregister || confirmWorkspaceUnregister || confirmMcpUnmount;
|
|
117
|
+
|
|
118
|
+
// Currently selected brick (if on bricks tab)
|
|
119
|
+
const selectedBrick = activeTab === "bricks" ? bricks[selectedIndex] ?? null : null;
|
|
120
|
+
|
|
121
|
+
// Allowed actions for the selected brick's current state
|
|
122
|
+
const allowed = useMemo(
|
|
123
|
+
() => (selectedBrick ? allowedActionsForState(selectedBrick.state) : new Set<string>()),
|
|
124
|
+
[selectedBrick?.state],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Refresh data for the current tab
|
|
128
|
+
const refreshActiveTab = useCallback((): void => {
|
|
129
|
+
if (!client) return;
|
|
130
|
+
|
|
131
|
+
if (activeTab === "zones") {
|
|
132
|
+
fetchZones(client);
|
|
133
|
+
} else if (activeTab === "bricks") {
|
|
134
|
+
fetchBricks(client);
|
|
135
|
+
} else if (activeTab === "drift") {
|
|
136
|
+
fetchDrift(client);
|
|
137
|
+
} else if (activeTab === "workspaces") {
|
|
138
|
+
fetchWorkspaces(client);
|
|
139
|
+
} else if (activeTab === "mcp") {
|
|
140
|
+
fetchMcpMounts(client);
|
|
141
|
+
} else if (activeTab === "cache") {
|
|
142
|
+
fetchCacheStats(client);
|
|
143
|
+
fetchHotFiles(client);
|
|
144
|
+
}
|
|
145
|
+
}, [activeTab, client, fetchZones, fetchBricks, fetchDrift, fetchWorkspaces, fetchMcpMounts, fetchCacheStats, fetchHotFiles]);
|
|
146
|
+
|
|
147
|
+
// Auto-fetch data on mount and when tab changes
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
refreshActiveTab();
|
|
150
|
+
}, [refreshActiveTab]);
|
|
151
|
+
|
|
152
|
+
// Fetch brick detail when selection changes in bricks tab
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (!client || activeTab !== "bricks") return;
|
|
155
|
+
const brick = bricks[selectedIndex];
|
|
156
|
+
if (brick) {
|
|
157
|
+
fetchBrickDetail(brick.name, client);
|
|
158
|
+
}
|
|
159
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
160
|
+
}, [selectedIndex, bricks, activeTab, client]);
|
|
161
|
+
|
|
162
|
+
// Confirmation handlers
|
|
163
|
+
const handleConfirmUnregister = useCallback(() => {
|
|
164
|
+
if (!client || !selectedBrick) return;
|
|
165
|
+
unregisterBrick(selectedBrick.name, client);
|
|
166
|
+
setConfirmUnregister(false);
|
|
167
|
+
}, [client, selectedBrick, unregisterBrick]);
|
|
168
|
+
|
|
169
|
+
const handleCancelUnregister = useCallback(() => {
|
|
170
|
+
setConfirmUnregister(false);
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
// Workspace unregister confirmation handlers
|
|
174
|
+
const handleConfirmWorkspaceUnregister = useCallback(() => {
|
|
175
|
+
if (!client) return;
|
|
176
|
+
const ws = workspaces[selectedWorkspaceIndex];
|
|
177
|
+
if (ws) {
|
|
178
|
+
unregisterWorkspace(ws.path, client);
|
|
179
|
+
}
|
|
180
|
+
setConfirmWorkspaceUnregister(false);
|
|
181
|
+
}, [client, workspaces, selectedWorkspaceIndex, unregisterWorkspace]);
|
|
182
|
+
|
|
183
|
+
const handleCancelWorkspaceUnregister = useCallback(() => {
|
|
184
|
+
setConfirmWorkspaceUnregister(false);
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
187
|
+
// MCP unmount confirmation handlers
|
|
188
|
+
const handleConfirmMcpUnmount = useCallback(() => {
|
|
189
|
+
if (!client) return;
|
|
190
|
+
const mount = mcpMounts[selectedMountIndex];
|
|
191
|
+
if (mount) {
|
|
192
|
+
unmountServer(mount.name, client);
|
|
193
|
+
}
|
|
194
|
+
setConfirmMcpUnmount(false);
|
|
195
|
+
}, [client, mcpMounts, selectedMountIndex, unmountServer]);
|
|
196
|
+
|
|
197
|
+
const handleCancelMcpUnmount = useCallback(() => {
|
|
198
|
+
setConfirmMcpUnmount(false);
|
|
199
|
+
}, []);
|
|
200
|
+
|
|
201
|
+
// Build context-aware help text for the bricks tab
|
|
202
|
+
const brickHelpText = useMemo(() => {
|
|
203
|
+
const parts: string[] = ["j/k:navigate", "Tab:switch tab"];
|
|
204
|
+
if (allowed.has("mount")) parts.push("M:mount");
|
|
205
|
+
if (allowed.has("remount")) parts.push("m:remount");
|
|
206
|
+
if (allowed.has("unmount")) parts.push("U:unmount");
|
|
207
|
+
if (allowed.has("unregister")) parts.push("D:unregister");
|
|
208
|
+
if (allowed.has("reset")) parts.push("x:reset");
|
|
209
|
+
parts.push("r:refresh", "q:quit");
|
|
210
|
+
return parts.join(" ");
|
|
211
|
+
}, [allowed]);
|
|
212
|
+
|
|
213
|
+
// Compute current list length and set-index for navigation across all tabs
|
|
214
|
+
const currentListLength = useCallback((): number => {
|
|
215
|
+
if (activeTab === "zones") return zones.length;
|
|
216
|
+
if (activeTab === "bricks") return bricks.length;
|
|
217
|
+
if (activeTab === "workspaces") return workspaces.length;
|
|
218
|
+
if (activeTab === "mcp") return mcpMounts.length;
|
|
219
|
+
return 0;
|
|
220
|
+
}, [activeTab, zones.length, bricks.length, workspaces.length, mcpMounts.length]);
|
|
221
|
+
|
|
222
|
+
const currentNavIndex = useCallback((): number => {
|
|
223
|
+
if (activeTab === "workspaces") return selectedWorkspaceIndex;
|
|
224
|
+
if (activeTab === "mcp") return selectedMountIndex;
|
|
225
|
+
return selectedIndex;
|
|
226
|
+
}, [activeTab, selectedIndex, selectedWorkspaceIndex, selectedMountIndex]);
|
|
227
|
+
|
|
228
|
+
const setCurrentNavIndex = useCallback((index: number): void => {
|
|
229
|
+
if (activeTab === "workspaces") {
|
|
230
|
+
setSelectedWorkspaceIndex(index);
|
|
231
|
+
} else if (activeTab === "mcp") {
|
|
232
|
+
setSelectedMountIndex(index);
|
|
233
|
+
} else {
|
|
234
|
+
setSelectedIndex(index);
|
|
235
|
+
}
|
|
236
|
+
}, [activeTab, setSelectedIndex, setSelectedWorkspaceIndex, setSelectedMountIndex]);
|
|
237
|
+
|
|
238
|
+
// In input mode, capture printable characters into the active field
|
|
239
|
+
const handleUnhandledKey = useCallback(
|
|
240
|
+
(keyName: string) => {
|
|
241
|
+
if (inputMode === "none") return;
|
|
242
|
+
const field = currentFieldName;
|
|
243
|
+
if (!field) return;
|
|
244
|
+
if (keyName.length === 1) {
|
|
245
|
+
setInputFields((f) => ({ ...f, [field]: (f[field] ?? "") + keyName }));
|
|
246
|
+
} else if (keyName === "space") {
|
|
247
|
+
setInputFields((f) => ({ ...f, [field]: (f[field] ?? "") + " " }));
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
[inputMode, currentFieldName],
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
useKeyboard(
|
|
254
|
+
overlayActive
|
|
255
|
+
? {}
|
|
256
|
+
: anyDialogOpen
|
|
257
|
+
? {} // ConfirmDialog handles its own keys when visible
|
|
258
|
+
: inputMode !== "none"
|
|
259
|
+
? {
|
|
260
|
+
return: () => {
|
|
261
|
+
if (!client) { setInputMode("none"); return; }
|
|
262
|
+
const f = inputFields;
|
|
263
|
+
if (inputMode === "workspace") {
|
|
264
|
+
const path = (f.path ?? "").trim();
|
|
265
|
+
if (!path) { setInputMode("none"); return; }
|
|
266
|
+
registerWorkspace({
|
|
267
|
+
path,
|
|
268
|
+
name: (f.name ?? "").trim() || path.split("/").pop() || path,
|
|
269
|
+
description: (f.description ?? "").trim() || undefined,
|
|
270
|
+
scope: (f.scope ?? "").trim() || undefined,
|
|
271
|
+
ttl_seconds: f.ttl_seconds?.trim() ? parseInt(f.ttl_seconds.trim(), 10) : undefined,
|
|
272
|
+
}, client);
|
|
273
|
+
} else if (inputMode === "mcpMount") {
|
|
274
|
+
const val = (f.command_or_url ?? "").trim();
|
|
275
|
+
const name = (f.name ?? "").trim() || val.split(/[\s/]/).pop() || "mcp-server";
|
|
276
|
+
if (!val) { setInputMode("none"); return; }
|
|
277
|
+
if (val.startsWith("http://") || val.startsWith("https://")) {
|
|
278
|
+
mountServer({ name, url: val, description: (f.description ?? "").trim() || undefined }, client);
|
|
279
|
+
} else {
|
|
280
|
+
mountServer({ name, command: val, description: (f.description ?? "").trim() || undefined }, client);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
setInputMode("none");
|
|
284
|
+
setInputFields({});
|
|
285
|
+
setInputActiveField(0);
|
|
286
|
+
},
|
|
287
|
+
escape: () => {
|
|
288
|
+
setInputMode("none");
|
|
289
|
+
setInputFields({});
|
|
290
|
+
setInputActiveField(0);
|
|
291
|
+
},
|
|
292
|
+
backspace: () => {
|
|
293
|
+
const field = currentFieldName;
|
|
294
|
+
if (field) {
|
|
295
|
+
setInputFields((ff) => ({ ...ff, [field]: (ff[field] ?? "").slice(0, -1) }));
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
tab: () => {
|
|
299
|
+
setInputActiveField((i) => (i + 1) % currentFields.length);
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
: {
|
|
303
|
+
j: () => {
|
|
304
|
+
const maxLen = currentListLength();
|
|
305
|
+
if (maxLen > 0) {
|
|
306
|
+
setCurrentNavIndex(Math.min(currentNavIndex() + 1, maxLen - 1));
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
down: () => {
|
|
310
|
+
const maxLen = currentListLength();
|
|
311
|
+
if (maxLen > 0) {
|
|
312
|
+
setCurrentNavIndex(Math.min(currentNavIndex() + 1, maxLen - 1));
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
k: () => {
|
|
316
|
+
setCurrentNavIndex(Math.max(currentNavIndex() - 1, 0));
|
|
317
|
+
},
|
|
318
|
+
up: () => {
|
|
319
|
+
setCurrentNavIndex(Math.max(currentNavIndex() - 1, 0));
|
|
320
|
+
},
|
|
321
|
+
...subTabCycleBindings(visibleTabs, activeTab, setActiveTab),
|
|
322
|
+
"shift+tab": () => toggleFocus("zones"),
|
|
323
|
+
// n: Register workspace or mount MCP server
|
|
324
|
+
n: () => {
|
|
325
|
+
if (activeTab === "workspaces") {
|
|
326
|
+
setInputMode("workspace");
|
|
327
|
+
setInputFields({});
|
|
328
|
+
setInputActiveField(0);
|
|
329
|
+
} else if (activeTab === "mcp") {
|
|
330
|
+
setInputMode("mcpMount");
|
|
331
|
+
setInputFields({});
|
|
332
|
+
setInputActiveField(0);
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
// M (shift+m): Mount — valid for registered/unmounted
|
|
336
|
+
"shift+m": () => {
|
|
337
|
+
if (!client || !selectedBrick || !allowed.has("mount")) return;
|
|
338
|
+
setOperationInProgress(true);
|
|
339
|
+
mountBrick(selectedBrick.name, client).finally(() => setOperationInProgress(false));
|
|
340
|
+
},
|
|
341
|
+
// U: Unmount — valid for active
|
|
342
|
+
"shift+u": () => {
|
|
343
|
+
if (!client || !selectedBrick || !allowed.has("unmount")) return;
|
|
344
|
+
setOperationInProgress(true);
|
|
345
|
+
unmountBrick(selectedBrick.name, client).finally(() => setOperationInProgress(false));
|
|
346
|
+
},
|
|
347
|
+
// D: Unregister — valid for unmounted (with confirmation)
|
|
348
|
+
"shift+d": () => {
|
|
349
|
+
if (!client || !selectedBrick || !allowed.has("unregister")) return;
|
|
350
|
+
setConfirmUnregister(true);
|
|
351
|
+
},
|
|
352
|
+
// m: Remount (existing) — valid for unmounted only
|
|
353
|
+
m: () => {
|
|
354
|
+
if (!client || !selectedBrick || !allowed.has("remount")) return;
|
|
355
|
+
setOperationInProgress(true);
|
|
356
|
+
remountBrick(selectedBrick.name, client).finally(() => setOperationInProgress(false));
|
|
357
|
+
},
|
|
358
|
+
// x: Reset (existing) — valid for failed
|
|
359
|
+
x: () => {
|
|
360
|
+
if (!client || !selectedBrick || !allowed.has("reset")) return;
|
|
361
|
+
setOperationInProgress(true);
|
|
362
|
+
resetBrick(selectedBrick.name, client).finally(() => setOperationInProgress(false));
|
|
363
|
+
},
|
|
364
|
+
// d: Unregister workspace or unmount MCP (with confirmation)
|
|
365
|
+
d: () => {
|
|
366
|
+
if (!client) return;
|
|
367
|
+
if (activeTab === "workspaces") {
|
|
368
|
+
const ws = workspaces[selectedWorkspaceIndex];
|
|
369
|
+
if (ws) setConfirmWorkspaceUnregister(true);
|
|
370
|
+
} else if (activeTab === "mcp") {
|
|
371
|
+
const mount = mcpMounts[selectedMountIndex];
|
|
372
|
+
if (mount) setConfirmMcpUnmount(true);
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
// s: Sync MCP server
|
|
376
|
+
s: () => {
|
|
377
|
+
if (!client || activeTab !== "mcp") return;
|
|
378
|
+
const mount = mcpMounts[selectedMountIndex];
|
|
379
|
+
if (mount) syncServer(mount.name, client);
|
|
380
|
+
},
|
|
381
|
+
// return: Show tools for selected MCP mount
|
|
382
|
+
return: () => {
|
|
383
|
+
if (!client || activeTab !== "mcp") return;
|
|
384
|
+
const mount = mcpMounts[selectedMountIndex];
|
|
385
|
+
if (mount) fetchTools(mount.name, client);
|
|
386
|
+
},
|
|
387
|
+
w: () => {
|
|
388
|
+
// Warmup cache with hot files
|
|
389
|
+
if (activeTab === "cache" && client && hotFiles.length > 0) {
|
|
390
|
+
const paths = hotFiles.map((f) => String((f as Record<string, unknown>).path ?? "")).filter(Boolean);
|
|
391
|
+
if (paths.length > 0) warmupCache(paths, client);
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
r: () => {
|
|
395
|
+
refreshActiveTab();
|
|
396
|
+
},
|
|
397
|
+
g: () => {
|
|
398
|
+
setCurrentNavIndex(jumpToStart());
|
|
399
|
+
},
|
|
400
|
+
"shift+g": () => {
|
|
401
|
+
const len = currentListLength();
|
|
402
|
+
setCurrentNavIndex(jumpToEnd(len));
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
!overlayActive && inputMode !== "none" ? handleUnhandledKey : undefined,
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// Context-aware help text per tab
|
|
409
|
+
const helpText = useMemo((): string => {
|
|
410
|
+
const base = "j/k:navigate Tab:switch tab r:refresh q:quit";
|
|
411
|
+
if (activeTab === "bricks") return brickHelpText;
|
|
412
|
+
if (activeTab === "workspaces") return "j/k:navigate n:register d:unregister Tab:tab r:refresh q:quit";
|
|
413
|
+
if (activeTab === "mcp") return "j/k:navigate n:mount d:unmount s:sync Enter:tools Tab:tab r:refresh q:quit";
|
|
414
|
+
if (activeTab === "cache") return "w:warmup hot files Tab:tab r:refresh q:quit";
|
|
415
|
+
return base;
|
|
416
|
+
}, [activeTab, brickHelpText]);
|
|
417
|
+
|
|
418
|
+
return (
|
|
419
|
+
<box height="100%" width="100%" flexDirection="column">
|
|
420
|
+
<SubTabBar tabs={visibleTabs} activeTab={activeTab} />
|
|
421
|
+
|
|
422
|
+
{/* Multi-field input form for register/mount */}
|
|
423
|
+
{inputMode !== "none" && (
|
|
424
|
+
<box flexDirection="column" width="100%">
|
|
425
|
+
{currentFields.map((field, i) => {
|
|
426
|
+
const isActive = i === inputActiveField;
|
|
427
|
+
const val = inputFields[field] ?? "";
|
|
428
|
+
const hint = field === "scope" ? " (persistent|session)" : field === "ttl_seconds" ? " (seconds, blank=none)" : field === "command_or_url" ? " (URL for SSE, command for stdio)" : "";
|
|
429
|
+
return (
|
|
430
|
+
<box key={field} height={1} width="100%">
|
|
431
|
+
<text>{isActive ? `> ${field}: ${val}\u2588${hint}` : ` ${field}: ${val}`}</text>
|
|
432
|
+
</box>
|
|
433
|
+
);
|
|
434
|
+
})}
|
|
435
|
+
<box height={1} width="100%">
|
|
436
|
+
<text>{"Tab:next field Enter:submit Escape:cancel"}</text>
|
|
437
|
+
</box>
|
|
438
|
+
</box>
|
|
439
|
+
)}
|
|
440
|
+
|
|
441
|
+
{/* Error display */}
|
|
442
|
+
{error && (
|
|
443
|
+
<box height={1} width="100%">
|
|
444
|
+
<text>{`Error: ${error}`}</text>
|
|
445
|
+
</box>
|
|
446
|
+
)}
|
|
447
|
+
|
|
448
|
+
{/* Brick operation in-flight indicator */}
|
|
449
|
+
{operationInProgress && (
|
|
450
|
+
<box height={1} width="100%">
|
|
451
|
+
<LoadingIndicator message="Operation in progress..." centered={false} />
|
|
452
|
+
</box>
|
|
453
|
+
)}
|
|
454
|
+
|
|
455
|
+
{/* Main content */}
|
|
456
|
+
<box flexGrow={1} flexDirection="row">
|
|
457
|
+
{activeTab === "zones" && (
|
|
458
|
+
<ZoneList
|
|
459
|
+
zones={zones}
|
|
460
|
+
selectedIndex={selectedIndex}
|
|
461
|
+
loading={zonesLoading}
|
|
462
|
+
/>
|
|
463
|
+
)}
|
|
464
|
+
|
|
465
|
+
{activeTab === "bricks" && (
|
|
466
|
+
<>
|
|
467
|
+
{/* Left sidebar: brick list (30%) */}
|
|
468
|
+
<box width="30%" height="100%" borderStyle="single" borderColor={uiFocusPane === "left" ? focusColor.activeBorder : focusColor.inactiveBorder} flexDirection="column">
|
|
469
|
+
<box height={1} width="100%">
|
|
470
|
+
<text>
|
|
471
|
+
{bricksHealth
|
|
472
|
+
? `--- Bricks (${bricksHealth.active}/${bricksHealth.total} active, ${bricksHealth.failed} failed) ---`
|
|
473
|
+
: "--- Bricks ---"}
|
|
474
|
+
</text>
|
|
475
|
+
</box>
|
|
476
|
+
|
|
477
|
+
<BrickList
|
|
478
|
+
bricks={bricks}
|
|
479
|
+
selectedIndex={selectedIndex}
|
|
480
|
+
loading={isLoading}
|
|
481
|
+
/>
|
|
482
|
+
</box>
|
|
483
|
+
|
|
484
|
+
{/* Right pane: brick detail (70%) */}
|
|
485
|
+
<box width="70%" height="100%" borderStyle="single" borderColor={uiFocusPane === "right" ? focusColor.activeBorder : focusColor.inactiveBorder}>
|
|
486
|
+
<BrickDetail brick={brickDetail} loading={detailLoading} />
|
|
487
|
+
</box>
|
|
488
|
+
</>
|
|
489
|
+
)}
|
|
490
|
+
|
|
491
|
+
{activeTab === "drift" && (
|
|
492
|
+
<DriftView drift={driftReport} loading={driftLoading} />
|
|
493
|
+
)}
|
|
494
|
+
|
|
495
|
+
{activeTab === "reindex" && <ReindexStatus />}
|
|
496
|
+
|
|
497
|
+
{activeTab === "workspaces" && (
|
|
498
|
+
<WorkspacesTab
|
|
499
|
+
workspaces={workspaces}
|
|
500
|
+
selectedIndex={selectedWorkspaceIndex}
|
|
501
|
+
loading={workspacesLoading}
|
|
502
|
+
/>
|
|
503
|
+
)}
|
|
504
|
+
|
|
505
|
+
{activeTab === "mcp" && (
|
|
506
|
+
<McpMountsTab
|
|
507
|
+
mounts={mcpMounts}
|
|
508
|
+
selectedIndex={selectedMountIndex}
|
|
509
|
+
loading={mcpMountsLoading}
|
|
510
|
+
/>
|
|
511
|
+
)}
|
|
512
|
+
|
|
513
|
+
{activeTab === "cache" && (
|
|
514
|
+
<CacheTab
|
|
515
|
+
stats={cacheStats}
|
|
516
|
+
hotFiles={hotFiles}
|
|
517
|
+
loading={cacheStatsLoading || hotFilesLoading}
|
|
518
|
+
/>
|
|
519
|
+
)}
|
|
520
|
+
</box>
|
|
521
|
+
|
|
522
|
+
{/* Context-aware help bar */}
|
|
523
|
+
<box height={1} width="100%">
|
|
524
|
+
<text>
|
|
525
|
+
{inputMode !== "none"
|
|
526
|
+
? `${inputMode === "workspace" ? "Register Workspace" : "Mount MCP Server"} — Tab:field Enter:submit Escape:cancel`
|
|
527
|
+
: helpText}
|
|
528
|
+
</text>
|
|
529
|
+
</box>
|
|
530
|
+
|
|
531
|
+
{/* Unregister confirmation dialog */}
|
|
532
|
+
<ConfirmDialog
|
|
533
|
+
visible={confirmUnregister}
|
|
534
|
+
title="Unregister Brick"
|
|
535
|
+
message={`Permanently unregister "${selectedBrick?.name ?? ""}"? This cannot be undone.`}
|
|
536
|
+
onConfirm={handleConfirmUnregister}
|
|
537
|
+
onCancel={handleCancelUnregister}
|
|
538
|
+
/>
|
|
539
|
+
|
|
540
|
+
{/* Workspace unregister confirmation dialog */}
|
|
541
|
+
<ConfirmDialog
|
|
542
|
+
visible={confirmWorkspaceUnregister}
|
|
543
|
+
title="Unregister Workspace"
|
|
544
|
+
message={`Unregister workspace "${workspaces[selectedWorkspaceIndex]?.name ?? ""}"?`}
|
|
545
|
+
onConfirm={handleConfirmWorkspaceUnregister}
|
|
546
|
+
onCancel={handleCancelWorkspaceUnregister}
|
|
547
|
+
/>
|
|
548
|
+
|
|
549
|
+
{/* MCP unmount confirmation dialog */}
|
|
550
|
+
<ConfirmDialog
|
|
551
|
+
visible={confirmMcpUnmount}
|
|
552
|
+
title="Unmount MCP Server"
|
|
553
|
+
message={`Unmount MCP server "${mcpMounts[selectedMountIndex]?.name ?? ""}"?`}
|
|
554
|
+
onConfirm={handleConfirmMcpUnmount}
|
|
555
|
+
onCancel={handleCancelMcpUnmount}
|
|
556
|
+
/>
|
|
557
|
+
</box>
|
|
558
|
+
);
|
|
559
|
+
}
|