@nexus-ai-fs/tui 0.9.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +30 -0
  2. package/package.json +48 -0
  3. package/src/app.tsx +349 -0
  4. package/src/index.tsx +137 -0
  5. package/src/opentui-env.d.ts +61 -0
  6. package/src/panels/access/access-panel.tsx +597 -0
  7. package/src/panels/access/alert-list.tsx +77 -0
  8. package/src/panels/access/constraint-creator.tsx +128 -0
  9. package/src/panels/access/constraint-list.tsx +72 -0
  10. package/src/panels/access/credential-list.tsx +68 -0
  11. package/src/panels/access/delegation-chain-view.tsx +110 -0
  12. package/src/panels/access/delegation-completer.tsx +120 -0
  13. package/src/panels/access/delegation-creator.tsx +237 -0
  14. package/src/panels/access/delegation-list.tsx +74 -0
  15. package/src/panels/access/fraud-score-view.tsx +94 -0
  16. package/src/panels/access/manifest-creator.tsx +167 -0
  17. package/src/panels/access/manifest-list.tsx +105 -0
  18. package/src/panels/access/namespace-config-view.tsx +525 -0
  19. package/src/panels/access/permission-checker.tsx +231 -0
  20. package/src/panels/agents/agent-status-view.tsx +196 -0
  21. package/src/panels/agents/agents-panel.tsx +493 -0
  22. package/src/panels/agents/delegation-list.tsx +154 -0
  23. package/src/panels/agents/inbox-view.tsx +96 -0
  24. package/src/panels/agents/trajectories-tab.tsx +40 -0
  25. package/src/panels/api-console/api-console-panel.tsx +189 -0
  26. package/src/panels/api-console/codegen-viewer.tsx +36 -0
  27. package/src/panels/api-console/codegen.ts +112 -0
  28. package/src/panels/api-console/endpoint-list.tsx +57 -0
  29. package/src/panels/api-console/request-builder.tsx +69 -0
  30. package/src/panels/api-console/response-viewer.tsx +54 -0
  31. package/src/panels/connectors/available-tab.tsx +357 -0
  32. package/src/panels/connectors/connector-row.tsx +121 -0
  33. package/src/panels/connectors/connectors-panel.tsx +88 -0
  34. package/src/panels/connectors/error-parser.ts +116 -0
  35. package/src/panels/connectors/mounted-tab.tsx +179 -0
  36. package/src/panels/connectors/skills-tab.tsx +235 -0
  37. package/src/panels/connectors/template-generator.ts +211 -0
  38. package/src/panels/connectors/write-tab.tsx +514 -0
  39. package/src/panels/events/audit-tab.tsx +69 -0
  40. package/src/panels/events/audit-trail.tsx +75 -0
  41. package/src/panels/events/connector-detail.tsx +49 -0
  42. package/src/panels/events/connector-list.tsx +73 -0
  43. package/src/panels/events/connectors-tab.tsx +92 -0
  44. package/src/panels/events/event-replay.tsx +80 -0
  45. package/src/panels/events/events-panel.tsx +414 -0
  46. package/src/panels/events/events-tab.tsx +212 -0
  47. package/src/panels/events/lock-list.tsx +54 -0
  48. package/src/panels/events/locks-tab.tsx +103 -0
  49. package/src/panels/events/mcl-replay.tsx +77 -0
  50. package/src/panels/events/mcl-tab.tsx +83 -0
  51. package/src/panels/events/operations-tab-wrapper.tsx +62 -0
  52. package/src/panels/events/operations-tab.tsx +41 -0
  53. package/src/panels/events/replay-tab.tsx +76 -0
  54. package/src/panels/events/secrets-audit.tsx +64 -0
  55. package/src/panels/events/secrets-tab.tsx +75 -0
  56. package/src/panels/events/subscription-list.tsx +54 -0
  57. package/src/panels/events/subscriptions-tab.tsx +82 -0
  58. package/src/panels/files/file-aspects.tsx +93 -0
  59. package/src/panels/files/file-editor.tsx +160 -0
  60. package/src/panels/files/file-explorer-keybindings.ts +468 -0
  61. package/src/panels/files/file-explorer-panel.tsx +545 -0
  62. package/src/panels/files/file-lineage.tsx +163 -0
  63. package/src/panels/files/file-list-item.tsx +28 -0
  64. package/src/panels/files/file-metadata.tsx +62 -0
  65. package/src/panels/files/file-preview.tsx +108 -0
  66. package/src/panels/files/file-schema.tsx +89 -0
  67. package/src/panels/files/file-tree-node.tsx +44 -0
  68. package/src/panels/files/file-tree.tsx +169 -0
  69. package/src/panels/files/share-links-tab.tsx +33 -0
  70. package/src/panels/files/uploads-tab.tsx +45 -0
  71. package/src/panels/payments/approval-list.tsx +83 -0
  72. package/src/panels/payments/balance-card.tsx +43 -0
  73. package/src/panels/payments/budget-card.tsx +70 -0
  74. package/src/panels/payments/payments-panel.tsx +451 -0
  75. package/src/panels/payments/policy-list.tsx +64 -0
  76. package/src/panels/payments/reservation-list.tsx +78 -0
  77. package/src/panels/payments/transaction-list.tsx +103 -0
  78. package/src/panels/payments/transfer-form.tsx +109 -0
  79. package/src/panels/search/column-search.tsx +79 -0
  80. package/src/panels/search/knowledge-view.tsx +100 -0
  81. package/src/panels/search/memory-list.tsx +197 -0
  82. package/src/panels/search/playbook-list.tsx +77 -0
  83. package/src/panels/search/rlm-answer-view.tsx +105 -0
  84. package/src/panels/search/search-panel.tsx +405 -0
  85. package/src/panels/search/search-results.tsx +116 -0
  86. package/src/panels/stack/stack-panel.tsx +474 -0
  87. package/src/panels/versions/conflicts-tab.tsx +59 -0
  88. package/src/panels/versions/entry-detail.tsx +89 -0
  89. package/src/panels/versions/transaction-actions.tsx +34 -0
  90. package/src/panels/versions/transaction-list.tsx +90 -0
  91. package/src/panels/versions/versions-panel.tsx +276 -0
  92. package/src/panels/workflows/execution-list.tsx +102 -0
  93. package/src/panels/workflows/scheduler-view.tsx +135 -0
  94. package/src/panels/workflows/workflow-list.tsx +88 -0
  95. package/src/panels/workflows/workflows-panel.tsx +295 -0
  96. package/src/panels/zones/brick-detail.tsx +136 -0
  97. package/src/panels/zones/brick-list.tsx +56 -0
  98. package/src/panels/zones/cache-tab.tsx +118 -0
  99. package/src/panels/zones/drift-view.tsx +97 -0
  100. package/src/panels/zones/mcp-mounts-tab.tsx +38 -0
  101. package/src/panels/zones/memories-tab.tsx +37 -0
  102. package/src/panels/zones/reindex-status.tsx +84 -0
  103. package/src/panels/zones/workspaces-tab.tsx +37 -0
  104. package/src/panels/zones/zone-list.tsx +73 -0
  105. package/src/panels/zones/zones-panel.tsx +559 -0
  106. package/src/services/command-runner.ts +303 -0
  107. package/src/shared/accessibility-announcements.ts +44 -0
  108. package/src/shared/action-registry.ts +466 -0
  109. package/src/shared/brick-states.ts +91 -0
  110. package/src/shared/command-palette.ts +35 -0
  111. package/src/shared/components/announcement-bar.tsx +30 -0
  112. package/src/shared/components/app-confirm-dialog.tsx +29 -0
  113. package/src/shared/components/breadcrumb.tsx +21 -0
  114. package/src/shared/components/brick-gate.tsx +60 -0
  115. package/src/shared/components/command-output.tsx +95 -0
  116. package/src/shared/components/command-palette.tsx +97 -0
  117. package/src/shared/components/confirm-dialog.tsx +61 -0
  118. package/src/shared/components/diff-viewer.tsx +219 -0
  119. package/src/shared/components/empty-state.tsx +36 -0
  120. package/src/shared/components/error-bar.tsx +60 -0
  121. package/src/shared/components/error-boundary.tsx +53 -0
  122. package/src/shared/components/help-overlay.tsx +99 -0
  123. package/src/shared/components/identity-switcher.tsx +168 -0
  124. package/src/shared/components/loading-indicator.tsx +40 -0
  125. package/src/shared/components/pagination-bar.tsx +68 -0
  126. package/src/shared/components/pre-connection-screen.tsx +398 -0
  127. package/src/shared/components/scroll-indicator.tsx +46 -0
  128. package/src/shared/components/side-nav-utils.ts +68 -0
  129. package/src/shared/components/side-nav.tsx +287 -0
  130. package/src/shared/components/spinner.tsx +26 -0
  131. package/src/shared/components/status-bar.tsx +117 -0
  132. package/src/shared/components/styled-text.tsx +72 -0
  133. package/src/shared/components/sub-tab-bar-utils.ts +100 -0
  134. package/src/shared/components/sub-tab-bar.tsx +40 -0
  135. package/src/shared/components/tab-bar-utils.ts +36 -0
  136. package/src/shared/components/tab-bar.tsx +50 -0
  137. package/src/shared/components/text-input.tsx +73 -0
  138. package/src/shared/components/tooltip.tsx +53 -0
  139. package/src/shared/components/virtual-list.tsx +93 -0
  140. package/src/shared/components/welcome-screen.tsx +111 -0
  141. package/src/shared/hooks/use-api.ts +10 -0
  142. package/src/shared/hooks/use-brick-available.ts +42 -0
  143. package/src/shared/hooks/use-confirm.ts +66 -0
  144. package/src/shared/hooks/use-connection-state.ts +67 -0
  145. package/src/shared/hooks/use-copy.ts +31 -0
  146. package/src/shared/hooks/use-fresh-server.ts +62 -0
  147. package/src/shared/hooks/use-keyboard.ts +58 -0
  148. package/src/shared/hooks/use-list-navigation.ts +106 -0
  149. package/src/shared/hooks/use-swr.ts +117 -0
  150. package/src/shared/hooks/use-tab-fallback.ts +32 -0
  151. package/src/shared/hooks/use-text-input.ts +113 -0
  152. package/src/shared/hooks/use-visible-tabs.ts +61 -0
  153. package/src/shared/lib/circular-buffer.ts +82 -0
  154. package/src/shared/lib/clipboard.ts +14 -0
  155. package/src/shared/nav-items.ts +73 -0
  156. package/src/shared/navigation.ts +110 -0
  157. package/src/shared/status-breadcrumb.ts +74 -0
  158. package/src/shared/syntax-style.ts +3 -0
  159. package/src/shared/tab-visibility.ts +15 -0
  160. package/src/shared/text-style.ts +23 -0
  161. package/src/shared/theme.ts +179 -0
  162. package/src/shared/utils/format-size.ts +20 -0
  163. package/src/shared/utils/format-text.ts +10 -0
  164. package/src/shared/utils/format-time.ts +72 -0
  165. package/src/shared/utils/lru-cache.ts +75 -0
  166. package/src/stores/access-store-types.ts +154 -0
  167. package/src/stores/access-store.ts +674 -0
  168. package/src/stores/agents-store.ts +404 -0
  169. package/src/stores/announcement-store.ts +46 -0
  170. package/src/stores/api-console-store.ts +476 -0
  171. package/src/stores/connectors-store.ts +434 -0
  172. package/src/stores/create-api-action.ts +140 -0
  173. package/src/stores/delegation-store.ts +300 -0
  174. package/src/stores/error-store.ts +102 -0
  175. package/src/stores/events-store.ts +163 -0
  176. package/src/stores/files-store.ts +630 -0
  177. package/src/stores/first-run-store.ts +34 -0
  178. package/src/stores/global-store.ts +255 -0
  179. package/src/stores/infra-store.ts +461 -0
  180. package/src/stores/knowledge-store.ts +358 -0
  181. package/src/stores/lineage-store.ts +126 -0
  182. package/src/stores/mcp-store.ts +147 -0
  183. package/src/stores/payments-store.ts +545 -0
  184. package/src/stores/search-store-types.ts +155 -0
  185. package/src/stores/search-store.ts +656 -0
  186. package/src/stores/share-link-store.ts +151 -0
  187. package/src/stores/stack-store.ts +352 -0
  188. package/src/stores/ui-store.ts +161 -0
  189. package/src/stores/upload-store.ts +131 -0
  190. package/src/stores/versions-store.ts +355 -0
  191. package/src/stores/workflows-store.ts +402 -0
  192. package/src/stores/workspace-store.ts +185 -0
  193. package/src/stores/zones-store.ts +378 -0
@@ -0,0 +1,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
+ }