@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,545 @@
1
+ /**
2
+ * Full file explorer layout: left pane (tree) + right pane (preview/metadata).
3
+ *
4
+ * This is the main files panel, loaded lazily by the app.
5
+ *
6
+ * Panel-level tabs: Explorer | Share Links | Uploads
7
+ *
8
+ * Keyboard modes:
9
+ * - "none" → normal navigation (j/k, expand/collapse, etc.)
10
+ * - "mkdir" → text input for new directory name
11
+ * - "rename" → text input for renaming
12
+ * - "filter" → fuzzy filter on visible tree (client-side)
13
+ * - "search" → power search input (g: glob, r: grep, plain = deep search)
14
+ * - "visual" → visual mode for range selection (not an input mode)
15
+ *
16
+ * @see Issue #3101 — filter/search, bulk ops, move/copy
17
+ * @see Issue #3102 — TUI rendering & data-fetching performance
18
+ */
19
+
20
+ import React, { useState, useCallback, useEffect, useMemo, useRef } from "react";
21
+ import {
22
+ useFilesStore,
23
+ type FileItem,
24
+ getEffectiveSelection,
25
+ } from "../../stores/files-store.js";
26
+ import { useGlobalStore } from "../../stores/global-store.js";
27
+ import { useShareLinkStore } from "../../stores/share-link-store.js";
28
+ import { useUploadStore } from "../../stores/upload-store.js";
29
+ import { Breadcrumb } from "../../shared/components/breadcrumb.js";
30
+ import { ConfirmDialog } from "../../shared/components/confirm-dialog.js";
31
+ import { FileTree, flattenVisibleNodes, LOAD_MORE_SENTINEL } from "./file-tree.js";
32
+ import { FilePreview } from "./file-preview.js";
33
+ import { FileEditor } from "./file-editor.js";
34
+ import { FileMetadata } from "./file-metadata.js";
35
+ import { FileAspects } from "./file-aspects.js";
36
+ import { FileLineage } from "./file-lineage.js";
37
+ import { FileSchema } from "./file-schema.js";
38
+ import { ShareLinksTab } from "./share-links-tab.js";
39
+ import { UploadsTab } from "./uploads-tab.js";
40
+ import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
41
+ import { useCopy } from "../../shared/hooks/use-copy.js";
42
+ import { useApi } from "../../shared/hooks/use-api.js";
43
+ import { useBrickAvailable } from "../../shared/hooks/use-brick-available.js";
44
+ import { useVisibleTabs, type TabDef } from "../../shared/hooks/use-visible-tabs.js";
45
+ import { SubTabBar } from "../../shared/components/sub-tab-bar.js";
46
+ import { useTabFallback } from "../../shared/hooks/use-tab-fallback.js";
47
+ import { useKnowledgeStore } from "../../stores/knowledge-store.js";
48
+ import { useUiStore } from "../../stores/ui-store.js";
49
+ import { useAnnouncementStore } from "../../stores/announcement-store.js";
50
+ import { focusColor, statusColor } from "../../shared/theme.js";
51
+ import {
52
+ formatDirectoryAnnouncement,
53
+ formatSelectionAnnouncement,
54
+ formatSuccessAnnouncement,
55
+ } from "../../shared/accessibility-announcements.js";
56
+ import crypto from "node:crypto";
57
+ import {
58
+ getKeyBindings, getInputLabel, getHelpText,
59
+ } from "./file-explorer-keybindings.js";
60
+ import type { InputMode, BindingContext, FilesTab } from "./file-explorer-keybindings.js";
61
+
62
+ // =============================================================================
63
+ // Panel-level tabs
64
+ // =============================================================================
65
+
66
+ const ALL_TABS: readonly TabDef<FilesTab>[] = [
67
+ { id: "explorer", label: "Explorer", brick: null },
68
+ { id: "shareLinks", label: "Share Links", brick: "share_link" },
69
+ { id: "uploads", label: "Uploads", brick: "uploads" },
70
+ ];
71
+
72
+ // =============================================================================
73
+ // Component
74
+ // =============================================================================
75
+
76
+ export default function FileExplorerPanel(): React.ReactNode {
77
+ const client = useApi();
78
+ const visibleTabs = useVisibleTabs(ALL_TABS);
79
+
80
+ // Panel-level active tab
81
+ const [activeTab, setActiveTab] = useState<FilesTab>("explorer");
82
+
83
+ useTabFallback(visibleTabs, activeTab, setActiveTab);
84
+
85
+ // Files store
86
+ const currentPath = useFilesStore((s) => s.currentPath);
87
+ const setCurrentPath = useFilesStore((s) => s.setCurrentPath);
88
+ const treeNodes = useFilesStore((s) => s.treeNodes);
89
+ const fileCacheRevision = useFilesStore((s) => s.fileCacheRevision);
90
+ const getCachedFiles = useFilesStore((s) => s.getCachedFiles);
91
+ const abortAll = useFilesStore((s) => s.abortAllInFlight);
92
+ const selectedIndex = useFilesStore((s) => s.selectedIndex);
93
+ const toggleNode = useFilesStore((s) => s.toggleNode);
94
+ const collapseNode = useFilesStore((s) => s.collapseNode);
95
+ const setSelectedIndex = useFilesStore((s) => s.setSelectedIndex);
96
+ const fetchPreview = useFilesStore((s) => s.fetchPreview);
97
+
98
+ // Cancel all in-flight file requests when panel unmounts (Issue #3102)
99
+ useEffect(() => {
100
+ return () => { abortAll(); };
101
+ }, [abortAll]);
102
+
103
+ // Selection & clipboard store
104
+ const selectedPaths = useFilesStore((s) => s.selectedPaths);
105
+ const visualModeAnchor = useFilesStore((s) => s.visualModeAnchor);
106
+ const clipboard = useFilesStore((s) => s.clipboard);
107
+ const toggleSelect = useFilesStore((s) => s.toggleSelect);
108
+ const clearSelection = useFilesStore((s) => s.clearSelection);
109
+ const enterVisualMode = useFilesStore((s) => s.enterVisualMode);
110
+ const exitVisualMode = useFilesStore((s) => s.exitVisualMode);
111
+ const yankToClipboard = useFilesStore((s) => s.yankToClipboard);
112
+ const cutToClipboard = useFilesStore((s) => s.cutToClipboard);
113
+ const clearClipboard = useFilesStore((s) => s.clearClipboard);
114
+ const pasteFiles = useFilesStore((s) => s.pasteFiles);
115
+ const pasteProgress = useFilesStore((s) => s.pasteProgress);
116
+ const announce = useAnnouncementStore((s) => s.announce);
117
+
118
+ // Share link store
119
+ const shareLinks = useShareLinkStore((s) => s.links);
120
+ const shareLinksLoading = useShareLinkStore((s) => s.linksLoading);
121
+ const selectedLinkIndex = useShareLinkStore((s) => s.selectedLinkIndex);
122
+ const fetchLinks = useShareLinkStore((s) => s.fetchLinks);
123
+ const setSelectedLinkIndex = useShareLinkStore((s) => s.setSelectedLinkIndex);
124
+
125
+ // Upload store
126
+ const uploadSessions = useUploadStore((s) => s.sessions);
127
+ const selectedSessionIndex = useUploadStore((s) => s.selectedSessionIndex);
128
+ const setSelectedSessionIndex = useUploadStore((s) => s.setSelectedSessionIndex);
129
+ const revokeLink = useShareLinkStore((s) => s.revokeLink);
130
+
131
+ // UI store
132
+ const uiFocusPane = useUiStore((s) => s.getFocusPane("files"));
133
+ const toggleFocus = useUiStore((s) => s.toggleFocusPane);
134
+ const overlayActive = useUiStore((s) => s.overlayActive);
135
+ const setOverlayActive = useUiStore((s) => s.setOverlayActive);
136
+
137
+ // Catalog brick availability
138
+ const { available: catalogAvailable } = useBrickAvailable("catalog");
139
+
140
+ // Active metadata sub-tab
141
+ const [metadataTab, setMetadataTab] = React.useState<"metadata" | "aspects" | "schema" | "lineage">("metadata");
142
+ React.useEffect(() => {
143
+ if (!catalogAvailable && (metadataTab === "aspects" || metadataTab === "schema")) {
144
+ setMetadataTab("metadata");
145
+ }
146
+ }, [catalogAvailable, metadataTab]);
147
+
148
+ // Flattened visible tree nodes — the source of truth for explorer navigation.
149
+ const visibleNodes = useMemo(
150
+ () => flattenVisibleNodes(currentPath, treeNodes),
151
+ [currentPath, treeNodes],
152
+ );
153
+
154
+ const selectedNode = visibleNodes[selectedIndex] ?? null;
155
+ const isSentinel = selectedNode?.path.endsWith(LOAD_MORE_SENTINEL) ?? false;
156
+ const currentTreeNode = treeNodes.get(currentPath);
157
+ const lastDirectoryAnnouncementRef = useRef<string | null>(null);
158
+ const lastSelectionAnnouncementRef = useRef<string | null>(null);
159
+ const lastPasteAnnouncementRef = useRef<string | null>(null);
160
+
161
+ // For metadata/actions, look up FileItem from parent's file cache first,
162
+ // then fall back to constructing a minimal FileItem from the tree node.
163
+ // The fallback ensures metadata pane works even if the file cache is empty
164
+ // (e.g. requests were aborted during rapid navigation).
165
+ const selectedItem: FileItem | null = useMemo(() => {
166
+ if (!selectedNode || isSentinel) return null;
167
+ const parentDir = selectedNode.path.split("/").slice(0, -1).join("/") || "/";
168
+ const parentFiles = getCachedFiles(parentDir);
169
+ const cached = parentFiles?.find((f) => f.path === selectedNode.path);
170
+ if (cached) return cached;
171
+ // Fallback: construct from tree node, using global zoneId from health check
172
+ return {
173
+ name: selectedNode.name,
174
+ path: selectedNode.path,
175
+ isDirectory: selectedNode.isDirectory,
176
+ size: selectedNode.size ?? 0,
177
+ modifiedAt: null,
178
+ etag: null,
179
+ mimeType: null,
180
+ version: null,
181
+ owner: null,
182
+ permissions: null,
183
+ zoneId: useGlobalStore.getState().zoneId,
184
+ };
185
+ }, [selectedNode, isSentinel, getCachedFiles, fileCacheRevision]);
186
+
187
+ const visibleNodeCount = visibleNodes.length;
188
+ // Keep cachedFiles for backward compat with BindingContext (selection uses it)
189
+ const cachedFiles = fileCacheRevision >= 0 ? (getCachedFiles(currentPath) ?? []) : [];
190
+
191
+ useEffect(() => {
192
+ lastSelectionAnnouncementRef.current = null;
193
+ }, [currentPath]);
194
+
195
+ useEffect(() => {
196
+ if (pasteProgress === null) {
197
+ lastPasteAnnouncementRef.current = null;
198
+ }
199
+ }, [pasteProgress]);
200
+
201
+ useEffect(() => {
202
+ if (!currentTreeNode || currentTreeNode.loading) return;
203
+ const key = `${currentPath}:${cachedFiles.length}:${fileCacheRevision}`;
204
+ if (lastDirectoryAnnouncementRef.current === key) return;
205
+ lastDirectoryAnnouncementRef.current = key;
206
+ announce(formatDirectoryAnnouncement(currentPath, cachedFiles.length));
207
+ }, [currentTreeNode, currentPath, cachedFiles.length, fileCacheRevision, announce]);
208
+
209
+ useEffect(() => {
210
+ if (!selectedNode || isSentinel) return;
211
+ if (lastSelectionAnnouncementRef.current === null) {
212
+ lastSelectionAnnouncementRef.current = selectedNode.path;
213
+ return;
214
+ }
215
+ if (lastSelectionAnnouncementRef.current === selectedNode.path) return;
216
+ lastSelectionAnnouncementRef.current = selectedNode.path;
217
+ announce(formatSelectionAnnouncement(selectedNode.name, selectedNode.isDirectory));
218
+ }, [selectedNode, isSentinel, announce]);
219
+
220
+ useEffect(() => {
221
+ if (!pasteProgress) return;
222
+ const completed = pasteProgress.completed + pasteProgress.failed;
223
+ if (completed < pasteProgress.total) return;
224
+ const key = `${pasteProgress.total}:${pasteProgress.completed}:${pasteProgress.failed}:${clipboard?.operation ?? "none"}`;
225
+ if (lastPasteAnnouncementRef.current === key) return;
226
+ lastPasteAnnouncementRef.current = key;
227
+ announce(
228
+ formatSuccessAnnouncement(
229
+ pasteProgress.failed > 0
230
+ ? `Paste complete: ${pasteProgress.completed} succeeded, ${pasteProgress.failed} failed`
231
+ : `Paste complete: ${pasteProgress.completed} items`,
232
+ ),
233
+ pasteProgress.failed > 0 ? "error" : "success",
234
+ );
235
+ }, [pasteProgress, clipboard?.operation, announce]);
236
+
237
+ // Aspect count badge
238
+ const aspectsCache = useKnowledgeStore((s) => s.aspectsCache);
239
+ const selectedUrn = selectedItem?.path
240
+ ? `urn:nexus:file:${selectedItem.zoneId || "default"}:${crypto.createHash("sha256").update(selectedItem.path).digest("hex").slice(0, 32)}`
241
+ : null;
242
+ const aspectCount = selectedUrn ? (aspectsCache.get(selectedUrn)?.length ?? 0) : 0;
243
+
244
+ // Clipboard copy (system)
245
+ const { copy, copied } = useCopy();
246
+
247
+ // Editor overlay state — suppress global panel-switch keys while editor is open
248
+ const [editorPath, setEditorPath] = useState<string | null>(null);
249
+ const openEditor = useCallback((path: string) => {
250
+ useUiStore.getState().setFileEditorOpen(true);
251
+ setEditorPath(path);
252
+ }, []);
253
+ const closeEditor = useCallback(() => {
254
+ useUiStore.getState().setFileEditorOpen(false);
255
+ setEditorPath(null);
256
+ }, []);
257
+
258
+ // Dialog state
259
+ const [confirmDelete, setConfirmDelete] = useState(false);
260
+
261
+ // Input mode
262
+ const [inputMode, setInputMode] = useState<InputMode>("none");
263
+ const [inputBuffer, setInputBuffer] = useState("");
264
+
265
+ // Filter & search state
266
+ const [filterQuery, setFilterQuery] = useState("");
267
+ const [searchQuery, setSearchQuery] = useState("");
268
+ const [searchResults, setSearchResults] = useState<readonly { path: string; line?: number; content?: string }[] | null>(null);
269
+
270
+ // Effective selection count for display
271
+ const effectiveSelection = useMemo(() => {
272
+ if (activeTab !== "explorer") return new Set<string>();
273
+ return getEffectiveSelection(
274
+ selectedPaths, visualModeAnchor, selectedIndex,
275
+ cachedFiles.map((f) => f.path),
276
+ );
277
+ }, [activeTab, selectedPaths, visualModeAnchor, selectedIndex, cachedFiles]);
278
+
279
+ // Fetch share links when switching to that tab
280
+ useEffect(() => {
281
+ if (!client) return;
282
+ if (activeTab === "shareLinks") fetchLinks(client);
283
+ }, [activeTab, client, fetchLinks]);
284
+
285
+ // Search execution
286
+ const executeSearch = useCallback(async (query: string) => {
287
+ if (!client) return;
288
+ setSearchResults(null);
289
+
290
+ try {
291
+ if (query.startsWith("g:")) {
292
+ // Glob search
293
+ const pattern = query.slice(2).trim();
294
+ if (!pattern) return;
295
+ const res = await client.get<{ matches: string[]; total: number; truncated: boolean }>(
296
+ `/api/v2/files/glob?pattern=${encodeURIComponent(pattern)}&path=${encodeURIComponent(currentPath)}&limit=100`,
297
+ );
298
+ setSearchResults(res.matches.map((p: string) => ({ path: p })));
299
+ } else if (query.startsWith("r:")) {
300
+ // Grep search
301
+ const pattern = query.slice(2).trim();
302
+ if (!pattern) return;
303
+ const res = await client.get<{ matches: { file: string; line: number; content: string }[]; total: number; truncated: boolean }>(
304
+ `/api/v2/files/grep?pattern=${encodeURIComponent(pattern)}&path=${encodeURIComponent(currentPath)}&limit=100`,
305
+ );
306
+ setSearchResults(res.matches.map((m: { file: string; line: number; content: string }) => ({ path: m.file, line: m.line, content: m.content })));
307
+ } else {
308
+ // Deep search via search API
309
+ const res = await client.get<{ results: { path: string }[] }>(
310
+ `/api/v2/search/query?q=${encodeURIComponent(query)}&path=${encodeURIComponent(currentPath)}&limit=100`,
311
+ );
312
+ setSearchResults(res.results.map((r: { path: string }) => ({ path: r.path })));
313
+ }
314
+ } catch {
315
+ setSearchResults([]);
316
+ }
317
+ }, [client, currentPath]);
318
+
319
+ // Build input buffer reference for the binding context
320
+ // The input buffer needs to be passed through the context for mkdir/rename
321
+ // to access the current value in their return handlers
322
+ const inputBufferRef = inputMode === "filter" ? filterQuery
323
+ : inputMode === "search" ? searchQuery
324
+ : inputMode === "paste-dest" ? inputBuffer
325
+ : inputBuffer;
326
+
327
+ // Handle unhandled keys for text input modes
328
+ const handleUnhandledKey = useCallback(
329
+ (keyName: string) => {
330
+ if (inputMode === "none") return;
331
+ const setter = inputMode === "filter" ? setFilterQuery
332
+ : inputMode === "search" ? setSearchQuery
333
+ : setInputBuffer;
334
+ if (keyName.length === 1) {
335
+ setter((b) => b + keyName);
336
+ } else if (keyName === "space") {
337
+ setter((b) => b + " ");
338
+ }
339
+ },
340
+ [inputMode],
341
+ );
342
+
343
+ // Build binding context
344
+ const ctx: BindingContext = {
345
+ activeTab, cachedFiles, selectedIndex, selectedItem, selectedNode, isSentinel,
346
+ visibleNodeCount, currentPath, client, setSelectedIndex, toggleNode, collapseNode,
347
+ fetchPreview, setMetadataTab, catalogAvailable,
348
+ shareLinks, selectedLinkIndex, setSelectedLinkIndex, revokeLink, fetchLinks,
349
+ uploadSessions, selectedSessionIndex, setSelectedSessionIndex,
350
+ visibleTabs, setActiveTab, toggleFocus, copy, setConfirmDelete,
351
+ setInputMode, setInputBuffer,
352
+ selectedPaths, visualModeAnchor, clipboard,
353
+ toggleSelect, clearSelection, enterVisualMode, exitVisualMode,
354
+ yankToClipboard, cutToClipboard, clearClipboard, pasteFiles,
355
+ filterQuery: inputBufferRef, setFilterQuery, searchQuery, setSearchQuery,
356
+ executeSearch,
357
+ searchResults, setSearchResults,
358
+ setInputModeWithCallback: setInputMode as BindingContext["setInputModeWithCallback"],
359
+ openEditor,
360
+ };
361
+
362
+ useKeyboard(
363
+ getKeyBindings(inputMode, overlayActive, confirmDelete, editorPath !== null, ctx),
364
+ !overlayActive && inputMode !== "none" && editorPath === null ? handleUnhandledKey : undefined,
365
+ );
366
+
367
+ const handleConfirmDelete = (): void => {
368
+ setConfirmDelete(false);
369
+ if (!client) return;
370
+ // Bulk delete: delete all selected files, then fall back to single item
371
+ const effective = getEffectiveSelection(
372
+ selectedPaths, visualModeAnchor, selectedIndex,
373
+ cachedFiles.map((f) => f.path),
374
+ );
375
+ if (effective.size > 0) {
376
+ for (const path of effective) {
377
+ useFilesStore.getState().deleteFile(path, client);
378
+ }
379
+ clearSelection();
380
+ } else if (selectedItem) {
381
+ useFilesStore.getState().deleteFile(selectedItem.path, client);
382
+ }
383
+ };
384
+
385
+ const handleCancelDelete = (): void => {
386
+ setConfirmDelete(false);
387
+ };
388
+
389
+ // Determine which input buffer to display
390
+ const displayBuffer = inputMode === "filter" ? filterQuery
391
+ : inputMode === "search" ? searchQuery
392
+ : inputBuffer;
393
+
394
+ return (
395
+ <box height="100%" width="100%" flexDirection="column">
396
+ {/* Full-screen file editor */}
397
+ {editorPath ? (
398
+ <FileEditor path={editorPath} onClose={closeEditor} />
399
+ ) : <>
400
+
401
+ {/* Panel-level tab bar */}
402
+ <SubTabBar tabs={visibleTabs} activeTab={activeTab} />
403
+
404
+ {/* Input bar for text modes */}
405
+ {inputMode !== "none" && (
406
+ <box height={1} width="100%">
407
+ <text>{getInputLabel(inputMode, displayBuffer)}</text>
408
+ </box>
409
+ )}
410
+
411
+ {/* Paste progress indicator */}
412
+ {pasteProgress && (
413
+ <box height={1} width="100%">
414
+ <text foregroundColor={statusColor.info}>
415
+ {pasteProgress.completed + pasteProgress.failed >= pasteProgress.total
416
+ ? `Paste complete: ${pasteProgress.completed}/${pasteProgress.total}${pasteProgress.failed > 0 ? ` (${pasteProgress.failed} failed)` : ""}`
417
+ : `Pasting... ${pasteProgress.completed + pasteProgress.failed}/${pasteProgress.total}${pasteProgress.failed > 0 ? ` (${pasteProgress.failed} failed)` : ""}`}
418
+ </text>
419
+ </box>
420
+ )}
421
+
422
+ {/* Clipboard indicator (only when not actively pasting) */}
423
+ {clipboard && !pasteProgress && inputMode === "none" && (
424
+ <box height={1} width="100%">
425
+ <text foregroundColor={statusColor.warning}>
426
+ {`${clipboard.paths.length} file${clipboard.paths.length > 1 ? "s" : ""} ${clipboard.operation === "cut" ? "cut" : "copied"} — press p to paste`}
427
+ </text>
428
+ </box>
429
+ )}
430
+
431
+ {/* Explorer tab */}
432
+ {activeTab === "explorer" && (
433
+ <box flexGrow={1} flexDirection="column">
434
+ {/* Breadcrumb navigation */}
435
+ <Breadcrumb path={currentPath} onNavigate={setCurrentPath} />
436
+
437
+ {/* Search results overlay */}
438
+ {searchResults !== null ? (
439
+ <box flexGrow={1} borderStyle="single">
440
+ <scrollbox height="100%" width="100%">
441
+ {searchResults.length === 0
442
+ ? <text>No results found</text>
443
+ : searchResults.map((result, i) => (
444
+ <box key={`${result.path}:${result.line ?? i}`} height={1} width="100%">
445
+ <text>
446
+ {result.line !== undefined
447
+ ? `${result.path}:${result.line} ${result.content ?? ""}`
448
+ : result.path}
449
+ </text>
450
+ </box>
451
+ ))}
452
+ <box height={1}>
453
+ <text dimColor>Press Escape to return to explorer</text>
454
+ </box>
455
+ </scrollbox>
456
+ </box>
457
+ ) : (
458
+ /* Main content: tree + preview */
459
+ <box flexGrow={1} flexDirection="row">
460
+ {/* Left pane: file tree (40%) */}
461
+ <box width="40%" height="100%" borderStyle="single" borderColor={uiFocusPane === "left" ? focusColor.activeBorder : focusColor.inactiveBorder}>
462
+ <FileTree
463
+ filterQuery={filterQuery}
464
+ effectiveSelection={effectiveSelection}
465
+ />
466
+ </box>
467
+
468
+ {/* Right pane: preview + metadata (60%) */}
469
+ <box width="60%" height="100%" flexDirection="column" borderStyle="single" borderColor={uiFocusPane === "right" ? focusColor.activeBorder : focusColor.inactiveBorder}>
470
+ {/* File preview (top 70%) */}
471
+ <box flexGrow={7} borderStyle="single">
472
+ <FilePreview />
473
+ </box>
474
+
475
+ {/* Metadata tab bar with aspect count badge */}
476
+ <box height={1} width="100%">
477
+ <text>
478
+ {` ${metadataTab === "metadata" ? "[Metadata]" : " Metadata "} ${metadataTab === "lineage" ? "[Lineage]" : " Lineage "}${catalogAvailable ? ` ${metadataTab === "aspects" ? `[Aspects${aspectCount > 0 ? ` (${aspectCount})` : ""}]` : ` Aspects${aspectCount > 0 ? ` (${aspectCount})` : ""} `} ${metadataTab === "schema" ? "[Schema]" : " Schema "}` : ""}`}
479
+ </text>
480
+ </box>
481
+
482
+ {/* Metadata sidebar (bottom 30%) */}
483
+ <box flexGrow={3} borderStyle="single">
484
+ {metadataTab === "metadata" && <FileMetadata item={selectedItem} />}
485
+ {metadataTab === "lineage" && <FileLineage item={selectedItem} />}
486
+ {metadataTab === "aspects" && catalogAvailable && <FileAspects item={selectedItem} />}
487
+ {metadataTab === "schema" && catalogAvailable && <FileSchema item={selectedItem} />}
488
+ </box>
489
+ </box>
490
+ </box>
491
+ )}
492
+ </box>
493
+ )}
494
+
495
+ {/* Share Links tab */}
496
+ {activeTab === "shareLinks" && (
497
+ <box flexGrow={1} borderStyle="single">
498
+ <ShareLinksTab
499
+ links={shareLinks}
500
+ selectedIndex={selectedLinkIndex}
501
+ loading={shareLinksLoading}
502
+ />
503
+ </box>
504
+ )}
505
+
506
+ {/* Uploads tab */}
507
+ {activeTab === "uploads" && (
508
+ <box flexGrow={1} borderStyle="single">
509
+ <UploadsTab
510
+ sessions={uploadSessions}
511
+ selectedIndex={selectedSessionIndex}
512
+ loading={false}
513
+ />
514
+ </box>
515
+ )}
516
+
517
+ {/* Help bar */}
518
+ <box height={1} width="100%">
519
+ {copied
520
+ ? <text foregroundColor={statusColor.healthy}>Copied!</text>
521
+ : <text>
522
+ {getHelpText(
523
+ inputMode, activeTab, catalogAvailable,
524
+ visualModeAnchor !== null, effectiveSelection.size,
525
+ clipboard,
526
+ )}
527
+ </text>}
528
+ </box>
529
+
530
+ {/* Delete confirmation dialog */}
531
+ <ConfirmDialog
532
+ visible={confirmDelete}
533
+ title={effectiveSelection.size > 0 ? "Delete Selected" : "Delete File"}
534
+ message={effectiveSelection.size > 1
535
+ ? `Delete ${effectiveSelection.size} selected files?`
536
+ : effectiveSelection.size === 1
537
+ ? `Delete "${[...effectiveSelection][0]!.split("/").pop()}"?`
538
+ : `Delete "${selectedItem?.name ?? ""}"?`}
539
+ onConfirm={handleConfirmDelete}
540
+ onCancel={handleCancelDelete}
541
+ />
542
+ </>}
543
+ </box>
544
+ );
545
+ }