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