@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,357 @@
1
+ /**
2
+ * Available tab: lists all registered connectors with auth and mount status.
3
+ *
4
+ * Supports: connector list navigation, auth initiation (opens browser),
5
+ * auth status polling, CLI mount guidance.
6
+ *
7
+ * Mounting connectors requires configuration (credentials, bucket names, etc.)
8
+ * that varies per connector. Instead of trying to collect all config in the TUI,
9
+ * we show the CLI command the user should run, with the required arguments
10
+ * pre-filled from the connector's connection_args.
11
+ */
12
+
13
+ import React, { useState, useEffect, useRef, useCallback } from "react";
14
+ import type { FetchClient } from "@nexus-ai-fs/api-client";
15
+ import { useConnectorsStore } from "../../stores/connectors-store.js";
16
+ import { useGlobalStore } from "../../stores/global-store.js";
17
+ import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
18
+ import { useCopy } from "../../shared/hooks/use-copy.js";
19
+ import { listNavigationBindings } from "../../shared/hooks/use-list-navigation.js";
20
+ import { LoadingIndicator } from "../../shared/components/loading-indicator.js";
21
+ import { ConnectorRow } from "./connector-row.js";
22
+ import { statusColor } from "../../shared/theme.js";
23
+
24
+ /**
25
+ * Copy text to system clipboard using platform-native tools (pbcopy/xclip)
26
+ * and also write to a temp file so it survives TUI exit.
27
+ */
28
+ function copyCommand(text: string): void {
29
+ const { execSync, exec } = require("child_process");
30
+ const fs = require("fs");
31
+
32
+ // Write to temp file so user can retrieve after TUI exit
33
+ try {
34
+ fs.writeFileSync("/tmp/nexus-mount-cmd.txt", text + "\n");
35
+ } catch {}
36
+
37
+ // Copy to system clipboard via platform tool
38
+ try {
39
+ if (process.platform === "darwin") {
40
+ execSync("pbcopy", { input: text, timeout: 2000 });
41
+ } else {
42
+ // Try xclip, then xsel
43
+ try {
44
+ execSync("xclip -selection clipboard", { input: text, timeout: 2000 });
45
+ } catch {
46
+ execSync("xsel --clipboard --input", { input: text, timeout: 2000 });
47
+ }
48
+ }
49
+ } catch {
50
+ // Fall back to OSC 52
51
+ const encoded = Buffer.from(text).toString("base64");
52
+ process.stdout.write(`\x1b]52;c;${encoded}\x07`);
53
+ }
54
+ }
55
+
56
+ interface AvailableTabProps {
57
+ readonly client: FetchClient;
58
+ readonly overlayActive: boolean;
59
+ }
60
+
61
+ const AUTH_POLL_INTERVAL = 3000;
62
+
63
+ export function AvailableTab({ client, overlayActive }: AvailableTabProps): React.ReactNode {
64
+ const connectors = useConnectorsStore((s) => s.availableConnectors);
65
+ const loading = useConnectorsStore((s) => s.availableLoading);
66
+ const selectedIndex = useConnectorsStore((s) => s.selectedAvailableIndex);
67
+ const authFlow = useConnectorsStore((s) => s.authFlow);
68
+
69
+ const setSelectedIndex = useConnectorsStore((s) => s.setSelectedAvailableIndex);
70
+ const fetchAvailable = useConnectorsStore((s) => s.fetchAvailable);
71
+ const initiateAuth = useConnectorsStore((s) => s.initiateAuth);
72
+ const pollAuthStatus = useConnectorsStore((s) => s.pollAuthStatus);
73
+ const cancelAuth = useConnectorsStore((s) => s.cancelAuth);
74
+ const mountConnector = useConnectorsStore((s) => s.mountConnector);
75
+ const mountsLoading = useConnectorsStore((s) => s.mountsLoading);
76
+
77
+ const config = useGlobalStore((s) => s.config);
78
+ const { copy, copied } = useCopy();
79
+ const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
80
+
81
+ // Show CLI mount guide for selected connector
82
+ const [showMountGuide, setShowMountGuide] = useState(false);
83
+
84
+ // Auto-fetch on mount
85
+ useEffect(() => {
86
+ if (connectors.length === 0) {
87
+ fetchAvailable(client);
88
+ }
89
+ }, [client, connectors.length, fetchAvailable]);
90
+
91
+ // Auth polling lifecycle
92
+ useEffect(() => {
93
+ if (authFlow.status === "polling" || authFlow.status === "waiting") {
94
+ pollTimerRef.current = setInterval(() => {
95
+ pollAuthStatus(client);
96
+ }, AUTH_POLL_INTERVAL);
97
+
98
+ return () => {
99
+ if (pollTimerRef.current) clearInterval(pollTimerRef.current);
100
+ };
101
+ }
102
+
103
+ if (pollTimerRef.current) {
104
+ clearInterval(pollTimerRef.current);
105
+ pollTimerRef.current = null;
106
+ }
107
+ }, [authFlow.status, client, pollAuthStatus]);
108
+
109
+ useEffect(() => {
110
+ return () => {
111
+ if (pollTimerRef.current) clearInterval(pollTimerRef.current);
112
+ };
113
+ }, []);
114
+
115
+ const handleAuth = useCallback(() => {
116
+ const selected = connectors[selectedIndex];
117
+ if (selected) {
118
+ initiateAuth(selected.name, client);
119
+ }
120
+ }, [connectors, selectedIndex, initiateAuth, client]);
121
+
122
+ /** Build mount command for storage connectors that need config. */
123
+ const getMountCommand = useCallback((): string => {
124
+ const selected = connectors[selectedIndex];
125
+ if (!selected) return "";
126
+ const baseName = selected.name.replace(/_connector$/, "");
127
+ const mountPath = `/mnt/${baseName}`;
128
+ const url = (config as Record<string, unknown>).baseUrl as string | undefined;
129
+ const apiKey = (config as Record<string, unknown>).apiKey as string | undefined;
130
+
131
+ // Build config template based on connector type
132
+ let configJson = "'{}'";
133
+ if (selected.name.includes("s3")) {
134
+ configJson = '\'{"bucket_name": "<BUCKET>", "access_key_id": "<KEY>", "secret_access_key": "<SECRET>"}\'';
135
+ } else if (selected.name.includes("gcs")) {
136
+ configJson = '\'{"bucket_name": "<BUCKET>", "credentials_path": "<PATH>"}\'';
137
+ } else if (selected.name.includes("local")) {
138
+ configJson = '\'{"local_path": "<PATH>"}\'';
139
+ }
140
+
141
+ // Use eval $(nexus env) prefix so NEXUS_URL, NEXUS_API_KEY, and
142
+ // NEXUS_GRPC_PORT are all set correctly for the CLI
143
+ const nexusDir = process.env.NEXUS_DATA_DIR
144
+ ? `cd ${process.env.NEXUS_DATA_DIR.replace(/\/nexus-data$/, "")} && `
145
+ : "";
146
+ return `${nexusDir}eval $(nexus env) && nexus mounts add ${mountPath} ${selected.name} ${configJson}`;
147
+ }, [connectors, selectedIndex, config]);
148
+
149
+ /** Check if connector can be mounted directly (no config needed). */
150
+ const canDirectMount = useCallback((): boolean => {
151
+ const selected = connectors[selectedIndex];
152
+ if (!selected) return false;
153
+ return selected.category === "cli" || selected.category === "oauth" || selected.category === "api";
154
+ }, [connectors, selectedIndex]);
155
+
156
+ // Mount success flash
157
+ const [mountFlash, setMountFlash] = useState<string | null>(null);
158
+
159
+ /** Mount directly via API for connectors that need no config. */
160
+ const handleDirectMount = useCallback(async () => {
161
+ const selected = connectors[selectedIndex];
162
+ if (!selected) return;
163
+ if (selected.mount_path) {
164
+ // Already mounted — show flash
165
+ setMountFlash(`Already mounted at ${selected.mount_path}`);
166
+ setTimeout(() => setMountFlash(null), 2000);
167
+ return;
168
+ }
169
+ const baseName = selected.name.replace(/_connector$/, "");
170
+ setMountFlash(`Mounting ${baseName}...`);
171
+ await mountConnector(selected.name, `/mnt/${baseName}`, client);
172
+ setMountFlash(`✓ Mounted at /mnt/${baseName}`);
173
+ setTimeout(() => setMountFlash(null), 2000);
174
+ }, [connectors, selectedIndex, mountConnector, client]);
175
+
176
+ /** Enter/m: direct mount if no config needed, otherwise show CLI guide. */
177
+ const handleMountAction = useCallback(() => {
178
+ if (canDirectMount()) {
179
+ handleDirectMount();
180
+ } else {
181
+ setShowMountGuide(!showMountGuide);
182
+ }
183
+ }, [canDirectMount, handleDirectMount, showMountGuide]);
184
+
185
+ const listNav = listNavigationBindings({
186
+ getIndex: () => selectedIndex,
187
+ setIndex: (i) => { setSelectedIndex(i); setShowMountGuide(false); },
188
+ getLength: () => connectors.length,
189
+ onSelect: handleMountAction,
190
+ });
191
+
192
+ useKeyboard(
193
+ overlayActive
194
+ ? {}
195
+ : {
196
+ ...listNav,
197
+ a: handleAuth,
198
+ m: handleMountAction,
199
+ r: () => fetchAvailable(client),
200
+ y: () => {
201
+ if (showMountGuide) {
202
+ const cmd = getMountCommand();
203
+ copyCommand(cmd);
204
+ copy(cmd);
205
+ } else if (authFlow.auth_url) {
206
+ copyCommand(authFlow.auth_url);
207
+ copy(authFlow.auth_url);
208
+ }
209
+ },
210
+ escape: () => {
211
+ if (showMountGuide) {
212
+ setShowMountGuide(false);
213
+ } else if (authFlow.status !== "idle") {
214
+ cancelAuth();
215
+ }
216
+ },
217
+ },
218
+ );
219
+
220
+ if (loading && connectors.length === 0) {
221
+ return <LoadingIndicator message="Loading connectors..." />;
222
+ }
223
+
224
+ const selectedConnector = connectors[selectedIndex];
225
+
226
+ return (
227
+ <box flexDirection="column" height="100%" width="100%">
228
+ {/* Mount flash */}
229
+ {mountFlash && (
230
+ <box height={1} width="100%" marginBottom={1}>
231
+ <text foregroundColor={mountFlash.startsWith("✓") ? statusColor.healthy : statusColor.info}>
232
+ {mountFlash}
233
+ </text>
234
+ </box>
235
+ )}
236
+
237
+ {/* Mount CLI guide */}
238
+ {showMountGuide && selectedConnector && (
239
+ <box flexDirection="column" width="100%" borderStyle="single" marginBottom={1}>
240
+ <box height={1} width="100%">
241
+ <text bold foregroundColor={statusColor.info}>
242
+ {`Mount ${selectedConnector.name.replace(/_connector$/, "")}:`}
243
+ </text>
244
+ </box>
245
+ <box height={1} width="100%">
246
+ <text foregroundColor={statusColor.dim}>Run this command in your terminal:</text>
247
+ </box>
248
+ <box height={1} width="100%">
249
+ <text>
250
+ <span foregroundColor={statusColor.healthy}>{" $ "}</span>
251
+ <span>{getMountCommand()}</span>
252
+ </text>
253
+ </box>
254
+ <box height={1} width="100%">
255
+ <text foregroundColor={statusColor.dim}>
256
+ {" y:copy to clipboard Esc:close (also saved to /tmp/nexus-mount-cmd.txt)"}
257
+ </text>
258
+ </box>
259
+ </box>
260
+ )}
261
+
262
+ {/* Auth flow banner */}
263
+ {authFlow.status !== "idle" && (
264
+ <box flexDirection="column" width="100%" borderStyle="single" marginBottom={1}>
265
+ {authFlow.status === "waiting" && authFlow.auth_url && (
266
+ <>
267
+ <box height={1} width="100%">
268
+ <text foregroundColor={statusColor.warning}>
269
+ {`Auth URL (press y to copy): ${authFlow.auth_url.substring(0, 60)}...`}
270
+ </text>
271
+ </box>
272
+ <box height={1} width="100%">
273
+ <text foregroundColor={statusColor.dim}>
274
+ {authFlow.error_message || "Open this URL in your browser to authenticate."}
275
+ </text>
276
+ </box>
277
+ </>
278
+ )}
279
+ {authFlow.status === "polling" && (
280
+ <box height={1} width="100%">
281
+ <text foregroundColor={statusColor.info}>
282
+ {`⠋ Waiting for ${authFlow.connector_name} authentication... (Escape to cancel)`}
283
+ </text>
284
+ </box>
285
+ )}
286
+ {authFlow.status === "completed" && (
287
+ <box height={1} width="100%">
288
+ <text foregroundColor={statusColor.healthy}>
289
+ {`✓ ${authFlow.connector_name} authenticated successfully!`}
290
+ </text>
291
+ </box>
292
+ )}
293
+ {authFlow.status === "error" && (
294
+ <>
295
+ <box height={1} width="100%">
296
+ <text foregroundColor={statusColor.error}>
297
+ {`✕ Auth failed: ${authFlow.error_message ?? "Unknown error"}`}
298
+ </text>
299
+ </box>
300
+ <box height={1} width="100%">
301
+ <text foregroundColor={statusColor.dim}>
302
+ {" To set up OAuth: configure GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env vars,"}
303
+ </text>
304
+ </box>
305
+ <box height={1} width="100%">
306
+ <text foregroundColor={statusColor.dim}>
307
+ {" then restart the server. Or mount directly: connectors like gws_gmail work without OAuth."}
308
+ </text>
309
+ </box>
310
+ <box height={1} width="100%">
311
+ <text foregroundColor={statusColor.dim}>
312
+ {" a:retry Esc:dismiss"}
313
+ </text>
314
+ </box>
315
+ </>
316
+ )}
317
+ </box>
318
+ )}
319
+
320
+ {/* Connector list */}
321
+ <box flexGrow={1} flexDirection="column">
322
+ {connectors.length === 0 ? (
323
+ <box height={1} width="100%">
324
+ <text foregroundColor={statusColor.dim}>No connectors registered.</text>
325
+ </box>
326
+ ) : (
327
+ connectors.map((c, i) => (
328
+ <ConnectorRow
329
+ key={c.name}
330
+ name={c.name}
331
+ category={c.category}
332
+ authStatus={c.auth_status}
333
+ mountPath={c.mount_path}
334
+ syncStatus={c.sync_status}
335
+ selected={i === selectedIndex}
336
+ showAuth={true}
337
+ showSync={true}
338
+ />
339
+ ))
340
+ )}
341
+ </box>
342
+
343
+ {/* Help bar */}
344
+ <box height={1} width="100%">
345
+ {copied ? (
346
+ <text foregroundColor={statusColor.healthy}>Copied!</text>
347
+ ) : loading || mountsLoading ? (
348
+ <text foregroundColor={statusColor.warning}>⠋ Loading...</text>
349
+ ) : (
350
+ <text foregroundColor={statusColor.dim}>
351
+ j/k:navigate Enter/m:mount a:auth r:refresh y:copy Esc:cancel
352
+ </text>
353
+ )}
354
+ </box>
355
+ </box>
356
+ );
357
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Shared connector row component used by Available and Mounted tabs.
3
+ *
4
+ * Renders a single row: selection prefix, connector name, provider, auth status,
5
+ * mount path, and sync status. Props control column visibility per tab.
6
+ */
7
+
8
+ import React from "react";
9
+ import { statusColor } from "../../shared/theme.js";
10
+
11
+ // =============================================================================
12
+ // Auth status indicator
13
+ // =============================================================================
14
+
15
+ const AUTH_INDICATORS: Record<string, { icon: string; color: string }> = {
16
+ authed: { icon: "●", color: statusColor.healthy },
17
+ expired: { icon: "●", color: statusColor.warning },
18
+ no_auth: { icon: "○", color: statusColor.dim },
19
+ unknown: { icon: "?", color: statusColor.dim },
20
+ error: { icon: "✕", color: statusColor.error },
21
+ };
22
+
23
+ // =============================================================================
24
+ // Sync status indicator
25
+ // =============================================================================
26
+
27
+ const SYNC_INDICATORS: Record<string, { label: string; color: string }> = {
28
+ synced: { label: "synced", color: statusColor.healthy },
29
+ syncing: { label: "syncing", color: statusColor.warning },
30
+ error: { label: "error", color: statusColor.error },
31
+ };
32
+
33
+ // =============================================================================
34
+ // Props
35
+ // =============================================================================
36
+
37
+ export interface ConnectorRowProps {
38
+ /** Connector display name */
39
+ readonly name: string;
40
+ /** Category/provider group (e.g., "gws", "gh") */
41
+ readonly category: string;
42
+ /** Auth status string */
43
+ readonly authStatus: string;
44
+ /** Mount path or null if not mounted */
45
+ readonly mountPath: string | null;
46
+ /** Sync status or null */
47
+ readonly syncStatus: string | null;
48
+ /** Whether this row is selected */
49
+ readonly selected: boolean;
50
+ /** Whether to show the auth status column */
51
+ readonly showAuth?: boolean;
52
+ /** Whether to show the sync status column */
53
+ readonly showSync?: boolean;
54
+ /** Whether connector is currently being synced */
55
+ readonly isSyncing?: boolean;
56
+ }
57
+
58
+ // =============================================================================
59
+ // Component
60
+ // =============================================================================
61
+
62
+ export function ConnectorRow({
63
+ name,
64
+ category,
65
+ authStatus,
66
+ mountPath,
67
+ syncStatus,
68
+ selected,
69
+ showAuth = true,
70
+ showSync = true,
71
+ isSyncing = false,
72
+ }: ConnectorRowProps): React.ReactNode {
73
+ const prefix = selected ? "▶ " : " ";
74
+ const auth = AUTH_INDICATORS[authStatus] ?? AUTH_INDICATORS.unknown;
75
+ const displayName = name.replace(/_connector$/, "");
76
+ const categoryLabel = category ? ` (${category})` : "";
77
+ const mountLabel = mountPath ?? "—";
78
+
79
+ // Build sync label
80
+ let syncLabel = "";
81
+ let syncColor = statusColor.dim;
82
+ if (isSyncing) {
83
+ syncLabel = "syncing…";
84
+ syncColor = statusColor.warning;
85
+ } else if (syncStatus) {
86
+ const indicator = SYNC_INDICATORS[syncStatus];
87
+ syncLabel = indicator?.label ?? syncStatus;
88
+ syncColor = indicator?.color ?? statusColor.dim;
89
+ }
90
+
91
+ return (
92
+ <box height={1} width="100%">
93
+ <text>
94
+ <span foregroundColor={selected ? statusColor.info : undefined}>
95
+ {prefix}
96
+ </span>
97
+ <span bold={selected}>
98
+ {displayName}
99
+ </span>
100
+ <span foregroundColor={statusColor.dim}>
101
+ {categoryLabel}
102
+ </span>
103
+ {showAuth && (
104
+ <>
105
+ <span>{" "}</span>
106
+ <span foregroundColor={auth.color}>{auth.icon}</span>
107
+ <span foregroundColor={statusColor.dim}>{` ${authStatus}`}</span>
108
+ </>
109
+ )}
110
+ <span>{" "}</span>
111
+ <span foregroundColor={statusColor.reference}>{mountLabel}</span>
112
+ {showSync && syncLabel && (
113
+ <>
114
+ <span>{" "}</span>
115
+ <span foregroundColor={syncColor}>{`[${syncLabel}]`}</span>
116
+ </>
117
+ )}
118
+ </text>
119
+ </box>
120
+ );
121
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Connectors panel: tabbed layout for Available, Mounted, Skills, and Write views.
3
+ *
4
+ * Sub-tab routing delegates keyboard context to per-tab components (Decision 8A).
5
+ * Gated on "mount" brick availability.
6
+ */
7
+
8
+ import React, { useEffect } from "react";
9
+ import { useConnectorsStore } from "../../stores/connectors-store.js";
10
+ import type { ConnectorsTab } from "../../stores/connectors-store.js";
11
+ import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
12
+ import { useApi } from "../../shared/hooks/use-api.js";
13
+ import { useUiStore } from "../../stores/ui-store.js";
14
+ import { BrickGate } from "../../shared/components/brick-gate.js";
15
+ import { LoadingIndicator } from "../../shared/components/loading-indicator.js";
16
+ import { AvailableTab } from "./available-tab.js";
17
+ import { MountedTab } from "./mounted-tab.js";
18
+ import { SkillsTab } from "./skills-tab.js";
19
+ import { WriteTab } from "./write-tab.js";
20
+ import { statusColor } from "../../shared/theme.js";
21
+ import { useVisibleTabs, type TabDef } from "../../shared/hooks/use-visible-tabs.js";
22
+ import { SubTabBar } from "../../shared/components/sub-tab-bar.js";
23
+ import { subTabCycleBindings } from "../../shared/components/sub-tab-bar-utils.js";
24
+ import { useTabFallback } from "../../shared/hooks/use-tab-fallback.js";
25
+
26
+ // =============================================================================
27
+ // Tab configuration
28
+ // =============================================================================
29
+
30
+ const ALL_TABS: readonly TabDef<ConnectorsTab>[] = [
31
+ { id: "available", label: "Available", brick: null },
32
+ { id: "mounted", label: "Mounted", brick: null },
33
+ { id: "skills", label: "Skills", brick: null },
34
+ { id: "write", label: "Write", brick: null },
35
+ ];
36
+
37
+ // =============================================================================
38
+ // Panel component
39
+ // =============================================================================
40
+
41
+ export default function ConnectorsPanel(): React.ReactNode {
42
+ const client = useApi();
43
+ const overlayActive = useUiStore((s) => s.overlayActive);
44
+ const activeTab = useConnectorsStore((s) => s.activeTab);
45
+ const setActiveTab = useConnectorsStore((s) => s.setActiveTab);
46
+
47
+ const visibleTabs = useVisibleTabs(ALL_TABS);
48
+ useTabFallback(visibleTabs, activeTab, setActiveTab);
49
+
50
+ // Only the panel root handles Tab key for sub-tab cycling.
51
+ // All other keys are delegated to the active sub-tab component.
52
+ useKeyboard(
53
+ overlayActive
54
+ ? {}
55
+ : {
56
+ ...subTabCycleBindings(visibleTabs, activeTab, setActiveTab),
57
+ },
58
+ );
59
+
60
+ if (!client) {
61
+ return <LoadingIndicator message="Connecting..." />;
62
+ }
63
+
64
+ return (
65
+ <BrickGate brick="storage">
66
+ <box height="100%" width="100%" flexDirection="column">
67
+ {/* Sub-tab bar */}
68
+ <SubTabBar tabs={visibleTabs} activeTab={activeTab} />
69
+
70
+ {/* Active tab content */}
71
+ <box flexGrow={1}>
72
+ {activeTab === "available" && (
73
+ <AvailableTab client={client} overlayActive={overlayActive} />
74
+ )}
75
+ {activeTab === "mounted" && (
76
+ <MountedTab client={client} overlayActive={overlayActive} />
77
+ )}
78
+ {activeTab === "skills" && (
79
+ <SkillsTab client={client} overlayActive={overlayActive} />
80
+ )}
81
+ {activeTab === "write" && (
82
+ <WriteTab client={client} overlayActive={overlayActive} />
83
+ )}
84
+ </box>
85
+ </box>
86
+ </BrickGate>
87
+ );
88
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Parse structured validation errors from backend connector write responses.
3
+ *
4
+ * The backend SkillErrorFormatter produces errors in a predictable format:
5
+ *
6
+ * [CODE] message
7
+ * Field errors:
8
+ * - field_name: error message
9
+ * See: /.skill/SKILL.md#section
10
+ * Fix:
11
+ * ```yaml
12
+ * field: corrected_value
13
+ * ```
14
+ *
15
+ * This parser extracts those sections into structured data for the TUI to
16
+ * render with color-coded field errors and actionable fix hints.
17
+ */
18
+
19
+ // =============================================================================
20
+ // Types
21
+ // =============================================================================
22
+
23
+ export interface ParsedWriteError {
24
+ /** Error code (e.g., "SCHEMA_VALIDATION_ERROR", "MISSING_AGENT_INTENT") */
25
+ readonly code: string | null;
26
+ /** Human-readable error message */
27
+ readonly message: string;
28
+ /** Field-level validation errors */
29
+ readonly fieldErrors: readonly FieldError[];
30
+ /** Skill doc reference path (e.g., "/.skill/SKILL.md#required-format") */
31
+ readonly skillRef: string | null;
32
+ /** YAML fix example (code block content, without fences) */
33
+ readonly fixExample: string | null;
34
+ }
35
+
36
+ export interface FieldError {
37
+ readonly field: string;
38
+ readonly error: string;
39
+ }
40
+
41
+ // =============================================================================
42
+ // Parser
43
+ // =============================================================================
44
+
45
+ /**
46
+ * Parse a backend error string into structured error data.
47
+ *
48
+ * Handles both the structured ValidationError format and plain error strings.
49
+ */
50
+ export function parseWriteError(errorString: string): ParsedWriteError {
51
+ // Extract error code: [CODE] message
52
+ const codeMatch = errorString.match(/^\[(\w+)]\s*(.+?)(?:\n|$)/);
53
+ const code = codeMatch?.[1] ?? null;
54
+ const messageAfterCode = codeMatch?.[2] ?? null;
55
+
56
+ // Extract field errors section
57
+ const fieldErrors: FieldError[] = [];
58
+ const fieldSectionMatch = errorString.match(
59
+ /Field errors:\s*\n((?:\s*-\s*.+\n?)+)/i,
60
+ );
61
+ if (fieldSectionMatch?.[1]) {
62
+ const fieldLines = fieldSectionMatch[1].split("\n");
63
+ for (const line of fieldLines) {
64
+ const fieldMatch = line.match(/^\s*-\s*(\S+):\s*(.+)$/);
65
+ if (fieldMatch?.[1] && fieldMatch[2]) {
66
+ fieldErrors.push({ field: fieldMatch[1], error: fieldMatch[2].trim() });
67
+ }
68
+ }
69
+ }
70
+
71
+ // Extract skill doc reference: See: path#section
72
+ const seeMatch = errorString.match(/See:\s*(\S+)/i);
73
+ const skillRef = seeMatch?.[1] ?? null;
74
+
75
+ // Extract fix example: everything between ```yaml and ```
76
+ let fixExample: string | null = null;
77
+ const fixBlockMatch = errorString.match(
78
+ /Fix:\s*\n```(?:yaml)?\s*\n([\s\S]*?)```/i,
79
+ );
80
+ if (fixBlockMatch?.[1]) {
81
+ fixExample = fixBlockMatch[1].trim();
82
+ } else {
83
+ // Try simpler format: Fix:\n# content (no code fences)
84
+ const simpleFix = errorString.match(/Fix:\s*\n((?:#?\s*.+\n?)+)/i);
85
+ if (simpleFix?.[1]) {
86
+ fixExample = simpleFix[1].trim();
87
+ }
88
+ }
89
+
90
+ // Build the primary message — use the part after [CODE] if available,
91
+ // otherwise use the first line of the error string
92
+ let message: string;
93
+ if (messageAfterCode) {
94
+ message = messageAfterCode;
95
+ } else {
96
+ // Take everything before "Field errors:" or "See:" or "Fix:"
97
+ const firstSectionIdx = Math.min(
98
+ ...[
99
+ errorString.indexOf("Field errors:"),
100
+ errorString.indexOf("See:"),
101
+ errorString.indexOf("Fix:"),
102
+ ]
103
+ .filter((i) => i >= 0)
104
+ .concat([errorString.length]),
105
+ );
106
+ message = errorString.substring(0, firstSectionIdx).trim();
107
+ }
108
+
109
+ return {
110
+ code,
111
+ message,
112
+ fieldErrors,
113
+ skillRef,
114
+ fixExample,
115
+ };
116
+ }