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