@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,434 @@
1
+ /**
2
+ * Connector management state: discovery, auth, mount, sync, skills, writes.
3
+ *
4
+ * Single store following the one-store-per-panel pattern (Decision 3A).
5
+ */
6
+
7
+ import { create } from "zustand";
8
+ import { createApiAction } from "./create-api-action.js";
9
+ import type { FetchClient } from "@nexus-ai-fs/api-client";
10
+ import { useUiStore } from "./ui-store.js";
11
+
12
+ // =============================================================================
13
+ // Types (wire format — snake_case matches API responses)
14
+ // =============================================================================
15
+
16
+ export interface AvailableConnector {
17
+ readonly name: string;
18
+ readonly description: string;
19
+ readonly category: string;
20
+ readonly capabilities: readonly string[];
21
+ readonly user_scoped: boolean;
22
+ readonly auth_status: string;
23
+ readonly auth_source: string | null;
24
+ readonly mount_path: string | null;
25
+ readonly sync_status: string | null;
26
+ }
27
+
28
+ export interface MountInfo {
29
+ readonly mount_point: string;
30
+ readonly readonly: boolean;
31
+ readonly connector_type: string | null;
32
+ readonly skill_name: string | null;
33
+ readonly operations: readonly string[];
34
+ readonly sync_status: string | null;
35
+ readonly last_sync: string | null;
36
+ }
37
+
38
+ export interface AuthInitResult {
39
+ readonly auth_url: string;
40
+ readonly state_token: string;
41
+ readonly provider: string;
42
+ readonly expires_in: number;
43
+ }
44
+
45
+ export interface AuthStatusResult {
46
+ readonly status: string; // "pending" | "completed" | "denied" | "expired" | "error"
47
+ readonly connector_name: string;
48
+ readonly message: string | null;
49
+ }
50
+
51
+ export interface SyncResult {
52
+ readonly mount_point: string;
53
+ readonly files_scanned: number;
54
+ readonly files_synced: number;
55
+ readonly delta_added: number;
56
+ readonly delta_deleted: number;
57
+ readonly history_id: string | null;
58
+ readonly is_delta: boolean;
59
+ readonly error: string | null;
60
+ }
61
+
62
+ export interface SkillDoc {
63
+ readonly mount_point: string;
64
+ readonly content: string;
65
+ readonly schemas: readonly string[];
66
+ }
67
+
68
+ export interface SchemaDoc {
69
+ readonly mount_point: string;
70
+ readonly operation: string;
71
+ readonly content: string;
72
+ }
73
+
74
+ export interface WriteResult {
75
+ readonly success: boolean;
76
+ readonly content_hash: string | null;
77
+ readonly error: string | null;
78
+ }
79
+
80
+ // =============================================================================
81
+ // Sub-tab type
82
+ // =============================================================================
83
+
84
+ export type ConnectorsTab = "available" | "mounted" | "skills" | "write";
85
+
86
+ // =============================================================================
87
+ // Auth flow state
88
+ // =============================================================================
89
+
90
+ export type AuthFlowStatus = "idle" | "waiting" | "polling" | "completed" | "error";
91
+
92
+ export interface AuthFlowState {
93
+ readonly status: AuthFlowStatus;
94
+ readonly auth_url: string | null;
95
+ readonly state_token: string | null;
96
+ readonly connector_name: string | null;
97
+ readonly error_message: string | null;
98
+ }
99
+
100
+ const INITIAL_AUTH_FLOW: AuthFlowState = {
101
+ status: "idle",
102
+ auth_url: null,
103
+ state_token: null,
104
+ connector_name: null,
105
+ error_message: null,
106
+ };
107
+
108
+ // =============================================================================
109
+ // Store interface
110
+ // =============================================================================
111
+
112
+ export interface ConnectorsState {
113
+ // --- Shared ---
114
+ readonly error: string | null;
115
+
116
+ // --- Available tab ---
117
+ readonly availableConnectors: readonly AvailableConnector[];
118
+ readonly availableLoading: boolean;
119
+ readonly selectedAvailableIndex: number;
120
+ readonly authFlow: AuthFlowState;
121
+
122
+ // --- Mounted tab ---
123
+ readonly mounts: readonly MountInfo[];
124
+ readonly mountsLoading: boolean;
125
+ readonly selectedMountIndex: number;
126
+ readonly syncingMounts: ReadonlySet<string>;
127
+ readonly lastSyncResult: SyncResult | null;
128
+
129
+ // --- Skills tab ---
130
+ readonly selectedSkillMountIndex: number;
131
+ readonly skillDoc: SkillDoc | null;
132
+ readonly skillDocLoading: boolean;
133
+ readonly selectedSchemaIndex: number;
134
+ readonly schemaDoc: SchemaDoc | null;
135
+ readonly schemaDocLoading: boolean;
136
+ readonly skillViewMode: "doc" | "schema";
137
+
138
+ // --- Write tab ---
139
+ readonly selectedWriteMountIndex: number;
140
+ readonly selectedOperationIndex: number;
141
+ readonly writeTemplate: string;
142
+ readonly writeResult: WriteResult | null;
143
+ readonly writeLoading: boolean;
144
+
145
+ // --- Navigation ---
146
+ readonly activeTab: ConnectorsTab;
147
+
148
+ // --- Actions ---
149
+ readonly setActiveTab: (tab: ConnectorsTab) => void;
150
+ readonly setSelectedAvailableIndex: (i: number) => void;
151
+ readonly setSelectedMountIndex: (i: number) => void;
152
+ readonly setSelectedSkillMountIndex: (i: number) => void;
153
+ readonly setSelectedSchemaIndex: (i: number) => void;
154
+ readonly setSkillViewMode: (mode: "doc" | "schema") => void;
155
+ readonly setSelectedWriteMountIndex: (i: number) => void;
156
+ readonly setSelectedOperationIndex: (i: number) => void;
157
+ readonly setWriteTemplate: (template: string) => void;
158
+
159
+ // --- Async actions ---
160
+ readonly fetchAvailable: (client: FetchClient) => Promise<void>;
161
+ readonly fetchMounts: (client: FetchClient) => Promise<void>;
162
+ readonly initiateAuth: (connectorName: string, client: FetchClient) => Promise<void>;
163
+ readonly pollAuthStatus: (client: FetchClient) => Promise<void>;
164
+ readonly cancelAuth: () => void;
165
+ readonly mountConnector: (connectorType: string, mountPoint: string, client: FetchClient) => Promise<void>;
166
+ readonly unmountConnector: (mountPoint: string, client: FetchClient) => Promise<void>;
167
+ readonly triggerSync: (mountPoint: string, client: FetchClient) => Promise<void>;
168
+ readonly fetchSkillDoc: (mountPath: string, client: FetchClient) => Promise<void>;
169
+ readonly fetchSchema: (mountPath: string, operation: string, client: FetchClient) => Promise<void>;
170
+ readonly submitWrite: (mountPath: string, yamlContent: string, client: FetchClient) => Promise<void>;
171
+ readonly clearWriteResult: () => void;
172
+ readonly clearSyncResult: () => void;
173
+ }
174
+
175
+ // =============================================================================
176
+ // Store implementation
177
+ // =============================================================================
178
+
179
+ export const useConnectorsStore = create<ConnectorsState>((set, get) => ({
180
+ // --- Initial state ---
181
+ error: null,
182
+
183
+ availableConnectors: [],
184
+ availableLoading: false,
185
+ selectedAvailableIndex: 0,
186
+ authFlow: INITIAL_AUTH_FLOW,
187
+
188
+ mounts: [],
189
+ mountsLoading: false,
190
+ selectedMountIndex: 0,
191
+ syncingMounts: new Set<string>(),
192
+ lastSyncResult: null,
193
+
194
+ selectedSkillMountIndex: 0,
195
+ skillDoc: null,
196
+ skillDocLoading: false,
197
+ selectedSchemaIndex: 0,
198
+ schemaDoc: null,
199
+ schemaDocLoading: false,
200
+ skillViewMode: "doc" as const,
201
+
202
+ selectedWriteMountIndex: 0,
203
+ selectedOperationIndex: 0,
204
+ writeTemplate: "",
205
+ writeResult: null,
206
+ writeLoading: false,
207
+
208
+ activeTab: "available" as ConnectorsTab,
209
+
210
+ // --- Setters ---
211
+ setActiveTab: (tab) => set({ activeTab: tab }),
212
+ setSelectedAvailableIndex: (i) => set({ selectedAvailableIndex: i }),
213
+ setSelectedMountIndex: (i) => set({ selectedMountIndex: i }),
214
+ setSelectedSkillMountIndex: (i) => set({ selectedSkillMountIndex: i }),
215
+ setSelectedSchemaIndex: (i) => set({ selectedSchemaIndex: i }),
216
+ setSkillViewMode: (mode) => set({ skillViewMode: mode }),
217
+ setSelectedWriteMountIndex: (i) => set({ selectedWriteMountIndex: i }),
218
+ setSelectedOperationIndex: (i) => set({ selectedOperationIndex: i }),
219
+ setWriteTemplate: (template) => set({ writeTemplate: template }),
220
+
221
+ // --- Fetch available connectors ---
222
+ fetchAvailable: createApiAction<ConnectorsState, [FetchClient]>(set, {
223
+ loadingKey: "availableLoading",
224
+ action: async (client) => {
225
+ const data = await client.get<AvailableConnector[]>("/api/v2/connectors/available");
226
+ return { availableConnectors: data };
227
+ },
228
+ source: "connectors",
229
+ retryable: true,
230
+ }),
231
+
232
+ // --- Fetch mounted connectors ---
233
+ fetchMounts: createApiAction<ConnectorsState, [FetchClient]>(set, {
234
+ loadingKey: "mountsLoading",
235
+ action: async (client) => {
236
+ const data = await client.get<MountInfo[]>("/api/v2/connectors/mounts");
237
+ return { mounts: data };
238
+ },
239
+ source: "connectors",
240
+ retryable: true,
241
+ }),
242
+
243
+ // --- OAuth auth flow ---
244
+ initiateAuth: async (connectorName: string, client: FetchClient) => {
245
+ set({
246
+ authFlow: {
247
+ status: "waiting",
248
+ auth_url: null,
249
+ state_token: null,
250
+ connector_name: connectorName,
251
+ error_message: null,
252
+ },
253
+ error: null,
254
+ });
255
+
256
+ try {
257
+ const result = await client.post<AuthInitResult>("/api/v2/connectors/auth/init", {
258
+ connector_name: connectorName,
259
+ });
260
+
261
+ // Try to open browser
262
+ let browserOpened = false;
263
+ try {
264
+ const { exec } = await import("child_process");
265
+ const platform = process.platform;
266
+ const cmd = platform === "darwin"
267
+ ? `open "${result.auth_url}"`
268
+ : platform === "win32"
269
+ ? `start "${result.auth_url}"`
270
+ : `xdg-open "${result.auth_url}"`;
271
+ exec(cmd);
272
+ browserOpened = true;
273
+ } catch {
274
+ browserOpened = false;
275
+ }
276
+
277
+ set({
278
+ authFlow: {
279
+ status: browserOpened ? "polling" : "waiting",
280
+ auth_url: result.auth_url,
281
+ state_token: result.state_token,
282
+ connector_name: connectorName,
283
+ error_message: browserOpened ? null : "Could not open browser. Copy the URL and paste it in your browser.",
284
+ },
285
+ });
286
+ } catch (err) {
287
+ const message = err instanceof Error ? err.message : "Failed to initiate auth";
288
+ set({
289
+ authFlow: {
290
+ status: "error",
291
+ auth_url: null,
292
+ state_token: null,
293
+ connector_name: connectorName,
294
+ error_message: message,
295
+ },
296
+ });
297
+ }
298
+ },
299
+
300
+ pollAuthStatus: async (client: FetchClient) => {
301
+ const { authFlow } = get();
302
+ if (!authFlow.state_token || authFlow.status === "idle" || authFlow.status === "completed") {
303
+ return;
304
+ }
305
+
306
+ try {
307
+ const result = await client.get<AuthStatusResult>(
308
+ `/api/v2/connectors/auth/status?state_token=${authFlow.state_token}`,
309
+ );
310
+
311
+ if (result.status === "completed") {
312
+ set({
313
+ authFlow: { ...authFlow, status: "completed", error_message: null },
314
+ });
315
+ // Refresh available connectors to show updated auth status
316
+ const { fetchAvailable } = get();
317
+ await fetchAvailable(client);
318
+ } else if (result.status === "denied" || result.status === "expired" || result.status === "error") {
319
+ set({
320
+ authFlow: {
321
+ ...authFlow,
322
+ status: "error",
323
+ error_message: result.message || `Auth ${result.status}`,
324
+ },
325
+ });
326
+ }
327
+ // "pending" — do nothing, keep polling
328
+ } catch (err) {
329
+ const message = err instanceof Error ? err.message : "Failed to check auth status";
330
+ set({
331
+ authFlow: { ...authFlow, status: "error", error_message: message },
332
+ });
333
+ }
334
+ },
335
+
336
+ cancelAuth: () => {
337
+ set({ authFlow: INITIAL_AUTH_FLOW });
338
+ },
339
+
340
+ // --- Mount/unmount ---
341
+ mountConnector: createApiAction<ConnectorsState, [string, string, FetchClient]>(set, {
342
+ loadingKey: "mountsLoading",
343
+ action: async (connectorType, mountPoint, client) => {
344
+ await client.post("/api/v2/connectors/mount", {
345
+ connector_type: connectorType,
346
+ mount_point: mountPoint,
347
+ });
348
+ // Refresh mounts and available lists
349
+ const mounts = await client.get<MountInfo[]>("/api/v2/connectors/mounts");
350
+ const available = await client.get<AvailableConnector[]>("/api/v2/connectors/available");
351
+ return { mounts, availableConnectors: available };
352
+ },
353
+ source: "connectors",
354
+ }),
355
+
356
+ unmountConnector: createApiAction<ConnectorsState, [string, FetchClient]>(set, {
357
+ loadingKey: "mountsLoading",
358
+ action: async (mountPoint, client) => {
359
+ await client.post("/api/v2/connectors/unmount", {
360
+ connector_type: "",
361
+ mount_point: mountPoint,
362
+ });
363
+ const mounts = await client.get<MountInfo[]>("/api/v2/connectors/mounts");
364
+ const available = await client.get<AvailableConnector[]>("/api/v2/connectors/available");
365
+ return { mounts, availableConnectors: available };
366
+ },
367
+ source: "connectors",
368
+ }),
369
+
370
+ // --- Sync ---
371
+ triggerSync: async (mountPoint: string, client: FetchClient) => {
372
+ const { syncingMounts } = get();
373
+ const newSyncing = new Set(syncingMounts);
374
+ newSyncing.add(mountPoint);
375
+ set({ syncingMounts: newSyncing, lastSyncResult: null, error: null });
376
+
377
+ try {
378
+ const result = await client.post<SyncResult>("/api/v2/connectors/sync", {
379
+ mount_point: mountPoint,
380
+ });
381
+ const updated = new Set(get().syncingMounts);
382
+ updated.delete(mountPoint);
383
+ set({ syncingMounts: updated, lastSyncResult: result });
384
+
385
+ // Refresh mounts to get updated sync status
386
+ const mounts = await client.get<MountInfo[]>("/api/v2/connectors/mounts");
387
+ set({ mounts });
388
+ useUiStore.getState().markDataUpdated("connectors");
389
+ } catch (err) {
390
+ const updated = new Set(get().syncingMounts);
391
+ updated.delete(mountPoint);
392
+ const message = err instanceof Error ? err.message : "Sync failed";
393
+ set({ syncingMounts: updated, error: message });
394
+ }
395
+ },
396
+
397
+ // --- Skill docs ---
398
+ fetchSkillDoc: createApiAction<ConnectorsState, [string, FetchClient]>(set, {
399
+ loadingKey: "skillDocLoading",
400
+ action: async (mountPath, client) => {
401
+ const doc = await client.get<SkillDoc>(`/api/v2/connectors/skill/${mountPath.replace(/^\//, "")}`);
402
+ return { skillDoc: doc };
403
+ },
404
+ source: "connectors",
405
+ }),
406
+
407
+ // --- Schema ---
408
+ fetchSchema: createApiAction<ConnectorsState, [string, string, FetchClient]>(set, {
409
+ loadingKey: "schemaDocLoading",
410
+ action: async (mountPath, operation, client) => {
411
+ const doc = await client.get<SchemaDoc>(
412
+ `/api/v2/connectors/schema/${mountPath.replace(/^\//, "")}/${operation}`,
413
+ );
414
+ return { schemaDoc: doc };
415
+ },
416
+ source: "connectors",
417
+ }),
418
+
419
+ // --- Write ---
420
+ submitWrite: createApiAction<ConnectorsState, [string, string, FetchClient]>(set, {
421
+ loadingKey: "writeLoading",
422
+ action: async (mountPath, yamlContent, client) => {
423
+ const result = await client.post<WriteResult>(
424
+ `/api/v2/connectors/write/${mountPath.replace(/^\//, "")}`,
425
+ { yaml_content: yamlContent },
426
+ );
427
+ return { writeResult: result };
428
+ },
429
+ source: "connectors",
430
+ }),
431
+
432
+ clearWriteResult: () => set({ writeResult: null }),
433
+ clearSyncResult: () => set({ lastSyncResult: null }),
434
+ }));
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Shared helper to reduce store action boilerplate (Decision 6A).
3
+ *
4
+ * Wraps the common try/catch/loading pattern used by all store fetch actions.
5
+ * Enhanced with onSuccess callback, error categorization, and centralized
6
+ * error store integration (Decision 8A).
7
+ */
8
+
9
+ import type { FetchClient } from "@nexus-ai-fs/api-client";
10
+ import { useErrorStore, type ErrorCategory } from "./error-store.js";
11
+ import { useUiStore } from "./ui-store.js";
12
+ import type { PanelId } from "./global-store.js";
13
+
14
+ type SetState<S> = (partial: Partial<S> | ((state: S) => Partial<S>)) => void;
15
+
16
+ // =============================================================================
17
+ // Error categorization
18
+ // =============================================================================
19
+
20
+ const NETWORK_PATTERNS = [
21
+ /ECONNREFUSED/i,
22
+ /ECONNRESET/i,
23
+ /ETIMEDOUT/i,
24
+ /fetch failed/i,
25
+ /network/i,
26
+ /timeout/i,
27
+ /DNS/i,
28
+ ] as const;
29
+
30
+ const VALIDATION_PATTERNS = [
31
+ /invalid/i,
32
+ /validation/i,
33
+ /bad request/i,
34
+ /422/i,
35
+ /400/i,
36
+ /missing required/i,
37
+ /must be/i,
38
+ ] as const;
39
+
40
+ /**
41
+ * Categorize an error message into network, validation, or server.
42
+ */
43
+ export function categorizeError(message: string): ErrorCategory {
44
+ for (const pattern of NETWORK_PATTERNS) {
45
+ if (pattern.test(message)) return "network";
46
+ }
47
+ for (const pattern of VALIDATION_PATTERNS) {
48
+ if (pattern.test(message)) return "validation";
49
+ }
50
+ return "server";
51
+ }
52
+
53
+ // =============================================================================
54
+ // Action factory (signature-agnostic)
55
+ // =============================================================================
56
+
57
+ /**
58
+ * Create a store action that handles loading/error state automatically.
59
+ *
60
+ * The action function can take any arguments — the wrapper preserves
61
+ * the original function signature.
62
+ *
63
+ * Usage in a Zustand store:
64
+ * ```ts
65
+ * fetchItems: createApiAction<MyState, [client: FetchClient]>(set, {
66
+ * loadingKey: "itemsLoading",
67
+ * action: async (client) => {
68
+ * const data = await client.get<Item[]>("/api/v2/items");
69
+ * return { items: data };
70
+ * },
71
+ * source: "files",
72
+ * }),
73
+ *
74
+ * // Works with any argument signature:
75
+ * fetchStatus: createApiAction<MyState, [agentId: string, client: FetchClient]>(set, {
76
+ * loadingKey: "statusLoading",
77
+ * action: async (agentId, client) => {
78
+ * const data = await client.get(`/api/v2/agents/${agentId}/status`);
79
+ * return { status: data };
80
+ * },
81
+ * }),
82
+ * ```
83
+ */
84
+ export function createApiAction<
85
+ S extends { error: string | null },
86
+ Args extends unknown[] = [FetchClient, ...unknown[]],
87
+ >(
88
+ set: SetState<S>,
89
+ config: {
90
+ /** State key to set to true/false during loading. */
91
+ readonly loadingKey: keyof S & string;
92
+ /** Async function that calls the API and returns partial state to merge. */
93
+ readonly action: (...args: Args) => Promise<Partial<S>>;
94
+ /** Fallback error message used when the thrown value is not an Error instance. */
95
+ readonly errorMessage?: string;
96
+ /** Called after successful action (after state is merged). */
97
+ readonly onSuccess?: () => void;
98
+ /** Source panel ID for error store categorization. */
99
+ readonly source?: string;
100
+ /** Whether to push errors to the centralized error store. Default: true. */
101
+ readonly pushToErrorStore?: boolean;
102
+ /** Whether the action can be retried on failure. Default: false. */
103
+ readonly retryable?: boolean;
104
+ },
105
+ ): (...args: Args) => Promise<void> {
106
+ const pushToErrorStore = config.pushToErrorStore ?? true;
107
+
108
+ return async (...args: Args) => {
109
+ set({ [config.loadingKey]: true, error: null } as Partial<S>);
110
+ try {
111
+ const result = await config.action(...args);
112
+ set({ ...result, [config.loadingKey]: false } as Partial<S>);
113
+ if (config.source) {
114
+ useUiStore.getState().markDataUpdated(config.source as PanelId);
115
+ }
116
+ config.onSuccess?.();
117
+ } catch (err) {
118
+ const message = err instanceof Error
119
+ ? err.message
120
+ : (config.errorMessage ?? "Operation failed");
121
+
122
+ set({
123
+ [config.loadingKey]: false,
124
+ error: message,
125
+ } as Partial<S>);
126
+
127
+ if (pushToErrorStore) {
128
+ const category = categorizeError(message);
129
+ useErrorStore.getState().pushError({
130
+ message,
131
+ category,
132
+ source: config.source,
133
+ retryAction: config.retryable
134
+ ? () => { createApiAction(set, config)(...args); }
135
+ : undefined,
136
+ });
137
+ }
138
+ }
139
+ };
140
+ }