@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,468 @@
1
+ /**
2
+ * Keybinding builders and help-text helpers for the file explorer panel.
3
+ *
4
+ * Extracted from file-explorer-panel.tsx to separate keybinding logic
5
+ * from component rendering (Decision 6A).
6
+ *
7
+ * @see Issue #3101 — filter/search, bulk ops, move/copy
8
+ * @see Issue #3591 — split oversized TUI modules
9
+ */
10
+
11
+ import {
12
+ useFilesStore,
13
+ type FileItem,
14
+ type TreeNode,
15
+ getEffectiveSelection,
16
+ } from "../../stores/files-store.js";
17
+ import {
18
+ listNavigationBindings,
19
+ } from "../../shared/hooks/use-list-navigation.js";
20
+ import { subTabCycleBindings } from "../../shared/components/sub-tab-bar-utils.js";
21
+ import type { TabDef } from "../../shared/hooks/use-visible-tabs.js";
22
+ import type { useApi } from "../../shared/hooks/use-api.js";
23
+
24
+ // =============================================================================
25
+ // Panel-level tabs
26
+ // =============================================================================
27
+
28
+ export type FilesTab = "explorer" | "shareLinks" | "uploads";
29
+
30
+ // =============================================================================
31
+ // Input mode types
32
+ // =============================================================================
33
+
34
+ export type InputMode = "none" | "mkdir" | "rename" | "filter" | "search" | "paste-dest" | "create";
35
+
36
+ // =============================================================================
37
+ // Keybinding builders — one function per mode (Decision 6A)
38
+ // =============================================================================
39
+
40
+ export interface BindingContext {
41
+ // Active tab
42
+ readonly activeTab: FilesTab;
43
+ // Explorer
44
+ readonly cachedFiles: readonly FileItem[];
45
+ readonly selectedIndex: number;
46
+ readonly selectedItem: FileItem | null;
47
+ readonly selectedNode: TreeNode | null;
48
+ readonly isSentinel: boolean;
49
+ readonly visibleNodeCount: number;
50
+ readonly currentPath: string;
51
+ readonly client: ReturnType<typeof useApi>;
52
+ // Stores
53
+ readonly setSelectedIndex: (i: number) => void;
54
+ readonly toggleNode: (path: string, client: NonNullable<ReturnType<typeof useApi>>) => Promise<void>;
55
+ readonly collapseNode: (path: string) => void;
56
+ readonly fetchPreview: (path: string, client: NonNullable<ReturnType<typeof useApi>>) => Promise<void>;
57
+ readonly setMetadataTab: (tab: "metadata" | "aspects" | "schema" | "lineage") => void;
58
+ readonly catalogAvailable: boolean;
59
+ // Share links
60
+ readonly shareLinks: readonly { link_id: string; status: string }[];
61
+ readonly selectedLinkIndex: number;
62
+ readonly setSelectedLinkIndex: (i: number) => void;
63
+ readonly revokeLink: (id: string, client: NonNullable<ReturnType<typeof useApi>>) => void;
64
+ readonly fetchLinks: (client: NonNullable<ReturnType<typeof useApi>>) => void;
65
+ // Uploads
66
+ readonly uploadSessions: readonly unknown[];
67
+ readonly selectedSessionIndex: number;
68
+ readonly setSelectedSessionIndex: (i: number) => void;
69
+ // Tabs
70
+ readonly visibleTabs: readonly TabDef<FilesTab>[];
71
+ readonly setActiveTab: (tab: FilesTab) => void;
72
+ readonly toggleFocus: (panel: string) => void;
73
+ // Actions
74
+ readonly copy: (text: string) => void;
75
+ readonly setConfirmDelete: (v: boolean) => void;
76
+ readonly setInputMode: (mode: InputMode) => void;
77
+ readonly setInputBuffer: (v: string | ((prev: string) => string)) => void;
78
+ // Selection & clipboard
79
+ readonly selectedPaths: ReadonlySet<string>;
80
+ readonly visualModeAnchor: number | null;
81
+ readonly clipboard: { readonly paths: readonly string[]; readonly operation: "copy" | "cut" } | null;
82
+ readonly toggleSelect: (path: string) => void;
83
+ readonly clearSelection: () => void;
84
+ readonly enterVisualMode: (anchor: number) => void;
85
+ readonly exitVisualMode: () => void;
86
+ readonly yankToClipboard: (paths: readonly string[]) => void;
87
+ readonly cutToClipboard: (paths: readonly string[]) => void;
88
+ readonly clearClipboard: () => void;
89
+ readonly pasteFiles: (destinationDir: string, client: NonNullable<ReturnType<typeof useApi>>) => Promise<void>;
90
+ // Filter
91
+ readonly filterQuery: string;
92
+ readonly setFilterQuery: (v: string | ((prev: string) => string)) => void;
93
+ readonly searchQuery: string;
94
+ readonly setSearchQuery: (v: string | ((prev: string) => string)) => void;
95
+ readonly executeSearch: (query: string) => void;
96
+ // Search results
97
+ readonly searchResults: readonly { path: string; line?: number; content?: string }[] | null;
98
+ readonly setSearchResults: (v: readonly { path: string; line?: number; content?: string }[] | null) => void;
99
+ // Paste destination input
100
+ readonly setInputModeWithCallback: (mode: InputMode, onSubmit: (value: string) => void) => void;
101
+ // Editor
102
+ readonly openEditor: (path: string) => void;
103
+ }
104
+
105
+ /** Navigation bindings for the currently active tab (Decision 5A). */
106
+ function getTabNavBindings(ctx: BindingContext): Record<string, () => void> {
107
+ switch (ctx.activeTab) {
108
+ case "explorer":
109
+ return {
110
+ ...listNavigationBindings({
111
+ getIndex: () => ctx.selectedIndex,
112
+ setIndex: ctx.setSelectedIndex,
113
+ getLength: () => ctx.visibleNodeCount,
114
+ onSelect: (index) => {
115
+ // Sentinel nodes are handled by FileTree's auto-load effect
116
+ if (ctx.isSentinel) return;
117
+ if (ctx.selectedNode && ctx.client) {
118
+ if (ctx.selectedNode.isDirectory) {
119
+ ctx.toggleNode(ctx.selectedNode.path, ctx.client);
120
+ } else {
121
+ ctx.fetchPreview(ctx.selectedNode.path, ctx.client);
122
+ }
123
+ }
124
+ },
125
+ }),
126
+ // g = jump to start (not in listNavigationBindings)
127
+ g: () => ctx.setSelectedIndex(0),
128
+ };
129
+ case "shareLinks":
130
+ return {
131
+ ...listNavigationBindings({
132
+ getIndex: () => ctx.selectedLinkIndex,
133
+ setIndex: ctx.setSelectedLinkIndex,
134
+ getLength: () => ctx.shareLinks.length,
135
+ }),
136
+ g: () => ctx.setSelectedLinkIndex(0),
137
+ };
138
+ case "uploads":
139
+ return {
140
+ ...listNavigationBindings({
141
+ getIndex: () => ctx.selectedSessionIndex,
142
+ setIndex: ctx.setSelectedSessionIndex,
143
+ getLength: () => ctx.uploadSessions.length,
144
+ }),
145
+ g: () => ctx.setSelectedSessionIndex(0),
146
+ };
147
+ }
148
+ }
149
+
150
+ /** Tab cycling (shared across all modes). */
151
+ function getTabCycleBindings(ctx: BindingContext): Record<string, () => void> {
152
+ return {
153
+ ...subTabCycleBindings(ctx.visibleTabs, ctx.activeTab, ctx.setActiveTab),
154
+ "shift+tab": () => ctx.toggleFocus("files"),
155
+ };
156
+ }
157
+
158
+ /** Explorer-specific action bindings (not navigation). */
159
+ function getExplorerActionBindings(ctx: BindingContext): Record<string, () => void> {
160
+ return {
161
+ // Tree navigation
162
+ l: () => {
163
+ if (ctx.isSentinel) return;
164
+ if (ctx.selectedNode?.isDirectory && ctx.client) ctx.toggleNode(ctx.selectedNode.path, ctx.client);
165
+ },
166
+ h: () => {
167
+ if (ctx.isSentinel) return;
168
+ if (ctx.selectedNode?.isDirectory) ctx.collapseNode(ctx.selectedNode.path);
169
+ },
170
+ // Metadata tabs
171
+ m: () => ctx.setMetadataTab("metadata"),
172
+ "shift+l": () => ctx.setMetadataTab("lineage"),
173
+ ...(ctx.catalogAvailable ? {
174
+ a: () => ctx.setMetadataTab("aspects"),
175
+ s: () => ctx.setMetadataTab("schema"),
176
+ } : {}),
177
+ // File operations — bulk delete if selection active, otherwise single item
178
+ d: () => {
179
+ if (ctx.isSentinel) return;
180
+ const effective = getEffectiveSelection(
181
+ ctx.selectedPaths, ctx.visualModeAnchor, ctx.selectedIndex,
182
+ ctx.cachedFiles.map((f) => f.path),
183
+ );
184
+ if (effective.size > 0 || ctx.selectedItem) {
185
+ ctx.setConfirmDelete(true);
186
+ }
187
+ },
188
+ "shift+n": () => { ctx.setInputMode("mkdir"); ctx.setInputBuffer(""); },
189
+ "shift+r": () => {
190
+ if (ctx.isSentinel) return;
191
+ if (ctx.selectedItem) {
192
+ ctx.setInputMode("rename");
193
+ ctx.setInputBuffer(ctx.selectedItem.name);
194
+ }
195
+ },
196
+ // Edit existing file: open full-screen editor
197
+ e: () => {
198
+ if (ctx.selectedItem && !ctx.selectedItem.isDirectory) {
199
+ ctx.openEditor(ctx.selectedItem.path);
200
+ }
201
+ },
202
+ // Create new file: prompt for filename, then open editor
203
+ "shift+e": () => {
204
+ const dir = ctx.selectedItem?.isDirectory
205
+ ? ctx.selectedItem.path
206
+ : ctx.currentPath;
207
+ const prefix = dir === "/" ? "/" : dir + "/";
208
+ ctx.setInputMode("create");
209
+ ctx.setInputBuffer(prefix);
210
+ },
211
+ // Copy path to system clipboard
212
+ y: () => {
213
+ if (ctx.isSentinel) return;
214
+ if (ctx.selectedItem) ctx.copy(ctx.selectedItem.path);
215
+ },
216
+ // Selection
217
+ space: () => {
218
+ if (ctx.isSentinel) return;
219
+ const item = ctx.cachedFiles[ctx.selectedIndex];
220
+ if (item) ctx.toggleSelect(item.path);
221
+ },
222
+ // Visual mode
223
+ v: () => {
224
+ if (ctx.visualModeAnchor !== null) {
225
+ ctx.exitVisualMode();
226
+ } else {
227
+ ctx.enterVisualMode(ctx.selectedIndex);
228
+ }
229
+ },
230
+ // Clipboard: copy/cut/paste
231
+ c: () => {
232
+ const effective = getEffectiveSelection(
233
+ ctx.selectedPaths, ctx.visualModeAnchor, ctx.selectedIndex,
234
+ ctx.cachedFiles.map((f) => f.path),
235
+ );
236
+ if (effective.size > 0) {
237
+ ctx.yankToClipboard([...effective]);
238
+ ctx.clearSelection();
239
+ }
240
+ },
241
+ x: () => {
242
+ const effective = getEffectiveSelection(
243
+ ctx.selectedPaths, ctx.visualModeAnchor, ctx.selectedIndex,
244
+ ctx.cachedFiles.map((f) => f.path),
245
+ );
246
+ if (effective.size > 0) {
247
+ ctx.cutToClipboard([...effective]);
248
+ ctx.clearSelection();
249
+ }
250
+ },
251
+ p: () => {
252
+ if (ctx.clipboard && ctx.client) {
253
+ ctx.pasteFiles(ctx.currentPath, ctx.client);
254
+ }
255
+ },
256
+ // Shift+P: paste to a specific path (prompts for destination)
257
+ "shift+p": () => {
258
+ if (ctx.clipboard && ctx.client) {
259
+ ctx.setInputMode("paste-dest");
260
+ ctx.setInputBuffer(ctx.currentPath);
261
+ }
262
+ },
263
+ // Escape: dismiss search results > exit visual mode > clear selection
264
+ escape: () => {
265
+ if (ctx.searchResults !== null) {
266
+ ctx.setSearchResults(null);
267
+ } else if (ctx.visualModeAnchor !== null) {
268
+ ctx.exitVisualMode();
269
+ } else if (ctx.selectedPaths.size > 0) {
270
+ ctx.clearSelection();
271
+ }
272
+ },
273
+ // Filter & search modes
274
+ "/": () => { ctx.setInputMode("filter"); ctx.setInputBuffer(""); },
275
+ "ctrl+f": () => { ctx.setInputMode("search"); ctx.setInputBuffer(""); },
276
+ };
277
+ }
278
+
279
+ /** ShareLinks-specific action bindings. */
280
+ function getShareLinksActionBindings(ctx: BindingContext): Record<string, () => void> {
281
+ return {
282
+ x: () => {
283
+ if (ctx.client) {
284
+ const link = ctx.shareLinks[ctx.selectedLinkIndex] as { link_id: string; status: string } | undefined;
285
+ if (link && link.status === "active") {
286
+ ctx.revokeLink(link.link_id, ctx.client);
287
+ }
288
+ }
289
+ },
290
+ r: () => { if (ctx.client) ctx.fetchLinks(ctx.client); },
291
+ };
292
+ }
293
+
294
+ /** Input mode bindings (mkdir, rename, filter, search). */
295
+ function getInputModeBindings(
296
+ inputMode: InputMode,
297
+ ctx: BindingContext,
298
+ ): Record<string, () => void> {
299
+ const resetInput = () => {
300
+ ctx.setInputMode("none");
301
+ ctx.setInputBuffer("");
302
+ };
303
+
304
+ const baseBindings: Record<string, () => void> = {
305
+ escape: resetInput,
306
+ backspace: () => ctx.setInputBuffer((b) => b.slice(0, -1)),
307
+ };
308
+
309
+ switch (inputMode) {
310
+ case "mkdir":
311
+ return {
312
+ ...baseBindings,
313
+ return: () => {
314
+ const value = ctx.filterQuery.trim(); // inputBuffer is used but accessed via closure
315
+ if (!value || !ctx.client) { resetInput(); return; }
316
+ const dirPath = ctx.currentPath === "/" ? `/${value}` : `${ctx.currentPath}/${value}`;
317
+ useFilesStore.getState().mkdirFile(dirPath, ctx.client);
318
+ resetInput();
319
+ },
320
+ };
321
+
322
+ case "rename":
323
+ return {
324
+ ...baseBindings,
325
+ return: () => {
326
+ const value = ctx.filterQuery.trim();
327
+ if (!value || !ctx.client || !ctx.selectedItem) { resetInput(); return; }
328
+ const parentPath = ctx.selectedItem.path.split("/").slice(0, -1).join("/") || "/";
329
+ const newPath = parentPath === "/" ? `/${value}` : `${parentPath}/${value}`;
330
+ useFilesStore.getState().renameFile(ctx.selectedItem.path, newPath, ctx.client);
331
+ resetInput();
332
+ },
333
+ };
334
+
335
+ case "filter":
336
+ return {
337
+ ...baseBindings,
338
+ return: resetInput, // confirm filter and return to normal mode (filter stays applied)
339
+ escape: () => {
340
+ ctx.setFilterQuery("");
341
+ resetInput();
342
+ },
343
+ };
344
+
345
+ case "search":
346
+ return {
347
+ ...baseBindings,
348
+ return: () => {
349
+ const query = ctx.searchQuery.trim();
350
+ if (query) ctx.executeSearch(query);
351
+ resetInput();
352
+ },
353
+ escape: () => {
354
+ ctx.setSearchQuery("");
355
+ resetInput();
356
+ },
357
+ };
358
+
359
+ case "paste-dest":
360
+ return {
361
+ ...baseBindings,
362
+ return: () => {
363
+ const dest = ctx.filterQuery.trim();
364
+ if (dest && ctx.client) {
365
+ useFilesStore.getState().pasteFiles(dest, ctx.client);
366
+ }
367
+ resetInput();
368
+ },
369
+ };
370
+
371
+ case "create":
372
+ return {
373
+ ...baseBindings,
374
+ return: () => {
375
+ const filePath = ctx.filterQuery.trim();
376
+ if (!filePath) { resetInput(); return; }
377
+ // Open editor for the new path (editor handles creation on save)
378
+ ctx.openEditor(filePath);
379
+ resetInput();
380
+ },
381
+ };
382
+
383
+ default:
384
+ return {};
385
+ }
386
+ }
387
+
388
+ /** Top-level binding dispatch based on current mode. */
389
+ export function getKeyBindings(
390
+ inputMode: InputMode,
391
+ overlayActive: boolean,
392
+ confirmDelete: boolean,
393
+ editorOpen: boolean,
394
+ ctx: BindingContext,
395
+ ): Record<string, () => void> {
396
+ if (overlayActive || confirmDelete || editorOpen) return {};
397
+
398
+ if (inputMode !== "none") {
399
+ return getInputModeBindings(inputMode, ctx);
400
+ }
401
+
402
+ // Normal mode: navigation + tab cycling + tab-specific actions
403
+ const navBindings = getTabNavBindings(ctx);
404
+ const tabBindings = getTabCycleBindings(ctx);
405
+
406
+ const actionBindings = ctx.activeTab === "explorer"
407
+ ? getExplorerActionBindings(ctx)
408
+ : ctx.activeTab === "shareLinks"
409
+ ? getShareLinksActionBindings(ctx)
410
+ : {};
411
+
412
+ return { ...navBindings, ...tabBindings, ...actionBindings };
413
+ }
414
+
415
+ // =============================================================================
416
+ // Input bar label per mode
417
+ // =============================================================================
418
+
419
+ export function getInputLabel(mode: InputMode, buffer: string): string {
420
+ switch (mode) {
421
+ case "mkdir": return `New directory: ${buffer}\u2588`;
422
+ case "rename": return `Rename to: ${buffer}\u2588`;
423
+ case "filter": return `/${buffer}\u2588`;
424
+ case "search": return `Search (g: glob, r: grep): ${buffer}\u2588`;
425
+ case "paste-dest": return `Paste to: ${buffer}\u2588`;
426
+ case "create": return `New file path: ${buffer}\u2588`;
427
+ default: return "";
428
+ }
429
+ }
430
+
431
+ // =============================================================================
432
+ // Help bar text
433
+ // =============================================================================
434
+
435
+ export function getHelpText(
436
+ inputMode: InputMode,
437
+ activeTab: FilesTab,
438
+ catalogAvailable: boolean,
439
+ visualMode: boolean,
440
+ selectionCount: number,
441
+ clipboard: BindingContext["clipboard"],
442
+ ): string {
443
+ if (inputMode === "filter") return "Type to filter, Enter:keep filter, Escape:clear";
444
+ if (inputMode === "search") return "g:pattern=glob r:pattern=grep plain=deep search Enter:search Esc:cancel";
445
+ if (inputMode === "paste-dest") return "Enter path, Enter:paste, Escape:cancel";
446
+ if (inputMode !== "none") return "Type name, Enter:confirm, Escape:cancel, Backspace:delete";
447
+
448
+ if (activeTab === "explorer") {
449
+ const parts = ["j/k:nav", "l/Enter:expand", "h:collapse"];
450
+ if (visualMode) {
451
+ parts.push("v:exit visual", "c:copy", "x:cut");
452
+ } else if (selectionCount > 0) {
453
+ parts.push(`${selectionCount} selected`, "c:copy", "x:cut", "Esc:clear");
454
+ } else {
455
+ parts.push("/:filter", "Ctrl+F:search", "v:visual", "Space:select");
456
+ }
457
+ if (clipboard) {
458
+ parts.push(`p:paste ${clipboard.paths.length} ${clipboard.operation === "cut" ? "cut" : "copied"}`, "P:paste to path");
459
+ }
460
+ parts.push("d:del", "N:mkdir", "R:rename", "e:edit", "E:new file");
461
+ if (catalogAvailable) parts.push("m/a/s:meta");
462
+ parts.push("?:help");
463
+ return parts.join(" ");
464
+ }
465
+
466
+ if (activeTab === "shareLinks") return "j/k:navigate x:revoke r:refresh Tab:switch tab q:quit";
467
+ return "j/k:navigate Tab:switch tab q:quit";
468
+ }