@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,300 @@
1
+ /**
2
+ * Shared delegation store (Decision 5A).
3
+ *
4
+ * Extracted from access-store.ts and agents-store.ts to eliminate duplication.
5
+ * Both the Access panel and Agents panel consume from this single store.
6
+ */
7
+
8
+ import { create } from "zustand";
9
+ import type { FetchClient } from "@nexus-ai-fs/api-client";
10
+ import { createApiAction, categorizeError } from "./create-api-action.js";
11
+ import { useErrorStore } from "./error-store.js";
12
+
13
+ // Canonical DelegationItem type — other stores re-export this.
14
+ export interface DelegationItem {
15
+ readonly delegation_id: string;
16
+ readonly agent_id: string;
17
+ readonly parent_agent_id: string;
18
+ readonly delegation_mode: "copy" | "clean" | "shared";
19
+ readonly status: "active" | "revoked" | "expired" | "completed";
20
+ readonly scope_prefix: string | null;
21
+ readonly lease_expires_at: string | null;
22
+ readonly zone_id: string | null;
23
+ readonly intent: string;
24
+ readonly depth: number;
25
+ readonly can_sub_delegate: boolean;
26
+ readonly created_at: string;
27
+ }
28
+
29
+ export interface DelegationCreateResponse {
30
+ readonly delegation_id: string;
31
+ readonly worker_agent_id: string;
32
+ readonly api_key: string;
33
+ readonly mount_table: readonly string[];
34
+ readonly expires_at: string | null;
35
+ readonly delegation_mode: string;
36
+ }
37
+
38
+ export interface DelegationChainEntry {
39
+ readonly delegation_id: string;
40
+ readonly agent_id: string;
41
+ readonly parent_agent_id: string;
42
+ readonly delegation_mode: string;
43
+ readonly status: string;
44
+ readonly depth: number;
45
+ readonly intent: string;
46
+ readonly created_at: string;
47
+ }
48
+
49
+ export interface DelegationChain {
50
+ readonly chain: readonly DelegationChainEntry[];
51
+ readonly total_depth: number;
52
+ }
53
+
54
+ export interface NamespaceDetail {
55
+ readonly delegation_id: string;
56
+ readonly agent_id: string;
57
+ readonly delegation_mode: string;
58
+ readonly scope_prefix: string | null;
59
+ readonly removed_grants: readonly string[];
60
+ readonly added_grants: readonly string[];
61
+ readonly readonly_paths: readonly string[];
62
+ readonly zone_id: string | null;
63
+ }
64
+
65
+ export interface DelegationState {
66
+ // Data
67
+ readonly delegations: readonly DelegationItem[];
68
+ readonly delegationsLoading: boolean;
69
+ readonly selectedDelegationIndex: number;
70
+ readonly lastDelegationCreate: DelegationCreateResponse | null;
71
+ readonly delegationChain: DelegationChain | null;
72
+ readonly delegationChainLoading: boolean;
73
+ readonly namespaceDetail: NamespaceDetail | null;
74
+ readonly namespaceDetailLoading: boolean;
75
+ readonly error: string | null;
76
+
77
+ // Pagination
78
+ readonly delegationsTotal: number;
79
+ readonly delegationsLimit: number;
80
+ readonly delegationsOffset: number;
81
+ readonly delegationsStatusFilter: string | null;
82
+
83
+ // Actions
84
+ readonly fetchDelegations: (client: FetchClient, options?: {
85
+ limit?: number;
86
+ offset?: number;
87
+ status?: string | null;
88
+ }) => Promise<void>;
89
+ readonly createDelegation: (
90
+ request: {
91
+ readonly worker_id: string;
92
+ readonly worker_name: string;
93
+ readonly namespace_mode: string;
94
+ readonly scope_prefix?: string;
95
+ readonly intent: string;
96
+ readonly can_sub_delegate: boolean;
97
+ readonly ttl_seconds?: number;
98
+ readonly remove_grants?: readonly string[];
99
+ readonly add_grants?: readonly string[];
100
+ readonly readonly_paths?: readonly string[];
101
+ readonly scope?: {
102
+ readonly allowed_operations?: readonly string[];
103
+ readonly resource_patterns?: readonly string[];
104
+ readonly budget_limit?: string;
105
+ readonly max_depth?: number;
106
+ };
107
+ readonly min_trust_score?: number;
108
+ },
109
+ client: FetchClient,
110
+ ) => Promise<void>;
111
+ readonly revokeDelegation: (delegationId: string, client: FetchClient) => Promise<void>;
112
+ readonly completeDelegation: (
113
+ delegationId: string,
114
+ outcome: string,
115
+ qualityScore: number | null,
116
+ client: FetchClient,
117
+ ) => Promise<void>;
118
+ readonly fetchDelegationChain: (delegationId: string, client: FetchClient) => Promise<void>;
119
+ readonly fetchNamespaceDetail: (delegationId: string, client: FetchClient) => Promise<void>;
120
+ readonly updateNamespaceConfig: (
121
+ delegationId: string,
122
+ update: {
123
+ readonly scope_prefix?: string;
124
+ readonly remove_grants?: readonly string[];
125
+ readonly add_grants?: readonly string[];
126
+ readonly readonly_paths?: readonly string[];
127
+ },
128
+ client: FetchClient,
129
+ ) => Promise<void>;
130
+ readonly setSelectedDelegationIndex: (index: number) => void;
131
+ readonly setStatusFilter: (status: string | null) => void;
132
+ }
133
+
134
+ const SOURCE = "delegation";
135
+
136
+ export const useDelegationStore = create<DelegationState>((set, get) => ({
137
+ delegations: [],
138
+ delegationsLoading: false,
139
+ selectedDelegationIndex: 0,
140
+ lastDelegationCreate: null,
141
+ delegationChain: null,
142
+ delegationChainLoading: false,
143
+ namespaceDetail: null,
144
+ namespaceDetailLoading: false,
145
+ error: null,
146
+ delegationsTotal: 0,
147
+ delegationsLimit: 50,
148
+ delegationsOffset: 0,
149
+ delegationsStatusFilter: null,
150
+
151
+ // =========================================================================
152
+ // Actions — inline with error store integration (complex logic/get())
153
+ // =========================================================================
154
+
155
+ fetchDelegations: async (client, options) => {
156
+ const limit = options?.limit ?? get().delegationsLimit;
157
+ const offset = options?.offset ?? get().delegationsOffset;
158
+ const status = options?.status !== undefined ? options.status : get().delegationsStatusFilter;
159
+
160
+ set({ delegationsLoading: true, error: null });
161
+ try {
162
+ let url = `/api/v2/agents/delegate?limit=${limit}&offset=${offset}`;
163
+ if (status) url += `&status=${encodeURIComponent(status)}`;
164
+
165
+ const response = await client.get<{
166
+ readonly delegations: readonly DelegationItem[];
167
+ readonly total: number;
168
+ readonly limit: number;
169
+ readonly offset: number;
170
+ }>(url);
171
+ set({
172
+ delegations: response.delegations,
173
+ delegationsTotal: response.total,
174
+ delegationsLimit: response.limit,
175
+ delegationsOffset: response.offset,
176
+ delegationsLoading: false,
177
+ selectedDelegationIndex: 0,
178
+ delegationsStatusFilter: status,
179
+ });
180
+ } catch (err) {
181
+ const message = err instanceof Error ? err.message : "Failed to fetch delegations";
182
+ set({ delegationsLoading: false, error: message });
183
+ useErrorStore.getState().pushError({ message, category: categorizeError(message), source: SOURCE });
184
+ }
185
+ },
186
+
187
+ createDelegation: async (request, client) => {
188
+ set({ delegationsLoading: true, error: null });
189
+ try {
190
+ const response = await client.post<DelegationCreateResponse>(
191
+ "/api/v2/agents/delegate",
192
+ request,
193
+ );
194
+ set({ lastDelegationCreate: response, delegationsLoading: false });
195
+ } catch (err) {
196
+ const message = err instanceof Error ? err.message : "Failed to create delegation";
197
+ set({ delegationsLoading: false, error: message });
198
+ useErrorStore.getState().pushError({ message, category: categorizeError(message), source: SOURCE });
199
+ return;
200
+ }
201
+ // Re-fetch list
202
+ try {
203
+ const { delegationsLimit, delegationsOffset, delegationsStatusFilter } = get();
204
+ let url = `/api/v2/agents/delegate?limit=${delegationsLimit}&offset=${delegationsOffset}`;
205
+ if (delegationsStatusFilter) url += `&status=${encodeURIComponent(delegationsStatusFilter)}`;
206
+ const listResponse = await client.get<{
207
+ readonly delegations: readonly DelegationItem[];
208
+ readonly total: number;
209
+ }>(url);
210
+ set({ delegations: listResponse.delegations, selectedDelegationIndex: 0 });
211
+ } catch {
212
+ // Non-critical
213
+ }
214
+ },
215
+
216
+ revokeDelegation: async (delegationId, client) => {
217
+ set({ delegationsLoading: true, error: null });
218
+ try {
219
+ await client.delete(`/api/v2/agents/delegate/${encodeURIComponent(delegationId)}`);
220
+ set((state) => ({
221
+ delegations: state.delegations.map((d) =>
222
+ d.delegation_id === delegationId ? { ...d, status: "revoked" } : d,
223
+ ),
224
+ delegationsLoading: false,
225
+ }));
226
+ } catch (err) {
227
+ const message = err instanceof Error ? err.message : "Failed to revoke delegation";
228
+ set({ delegationsLoading: false, error: message });
229
+ useErrorStore.getState().pushError({ message, category: categorizeError(message), source: SOURCE });
230
+ }
231
+ },
232
+
233
+ completeDelegation: async (delegationId, outcome, qualityScore, client) => {
234
+ set({ delegationsLoading: true, error: null });
235
+ try {
236
+ const body: { outcome: string; quality_score?: number } = { outcome };
237
+ if (qualityScore !== null) body.quality_score = qualityScore;
238
+ await client.post(
239
+ `/api/v2/agents/delegate/${encodeURIComponent(delegationId)}/complete`,
240
+ body,
241
+ );
242
+ set((state) => ({
243
+ delegations: state.delegations.map((d) =>
244
+ d.delegation_id === delegationId ? { ...d, status: "completed" } : d,
245
+ ),
246
+ delegationsLoading: false,
247
+ }));
248
+ } catch (err) {
249
+ const message = err instanceof Error ? err.message : "Failed to complete delegation";
250
+ set({ delegationsLoading: false, error: message });
251
+ useErrorStore.getState().pushError({ message, category: categorizeError(message), source: SOURCE });
252
+ }
253
+ },
254
+
255
+ // =========================================================================
256
+ // Actions migrated to createApiAction (Decision 6A)
257
+ // =========================================================================
258
+
259
+ fetchDelegationChain: createApiAction<DelegationState, [string, FetchClient]>(set, {
260
+ loadingKey: "delegationChainLoading",
261
+ source: SOURCE,
262
+ action: async (delegationId, client) => {
263
+ const response = await client.get<DelegationChain>(
264
+ `/api/v2/agents/delegate/${encodeURIComponent(delegationId)}/chain`,
265
+ );
266
+ return { delegationChain: response };
267
+ },
268
+ }),
269
+
270
+ fetchNamespaceDetail: createApiAction<DelegationState, [string, FetchClient]>(set, {
271
+ loadingKey: "namespaceDetailLoading",
272
+ source: SOURCE,
273
+ action: async (delegationId, client) => {
274
+ const response = await client.get<NamespaceDetail>(
275
+ `/api/v2/agents/delegate/${encodeURIComponent(delegationId)}/namespace`,
276
+ );
277
+ return { namespaceDetail: response };
278
+ },
279
+ }),
280
+
281
+ updateNamespaceConfig: createApiAction<DelegationState, [string, { readonly scope_prefix?: string; readonly remove_grants?: readonly string[]; readonly add_grants?: readonly string[]; readonly readonly_paths?: readonly string[] }, FetchClient]>(set, {
282
+ loadingKey: "namespaceDetailLoading",
283
+ source: SOURCE,
284
+ action: async (delegationId, update, client) => {
285
+ const response = await client.patch<NamespaceDetail>(
286
+ `/api/v2/agents/delegate/${encodeURIComponent(delegationId)}/namespace`,
287
+ update,
288
+ );
289
+ return { namespaceDetail: response };
290
+ },
291
+ }),
292
+
293
+ setSelectedDelegationIndex: (index) => {
294
+ set({ selectedDelegationIndex: index });
295
+ },
296
+
297
+ setStatusFilter: (status) => {
298
+ set({ delegationsStatusFilter: status });
299
+ },
300
+ }));
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Centralized structured error store.
3
+ *
4
+ * Replaces fragmented per-store `error: string | null` fields with
5
+ * categorized, dismissable errors that support retry and source filtering.
6
+ *
7
+ * @see Issue #3066 Architecture Decision 8A
8
+ */
9
+
10
+ import { create } from "zustand";
11
+
12
+ // =============================================================================
13
+ // Types
14
+ // =============================================================================
15
+
16
+ export type ErrorCategory = "network" | "validation" | "server";
17
+
18
+ export interface AppError {
19
+ readonly id: string;
20
+ readonly message: string;
21
+ readonly category: ErrorCategory;
22
+ readonly source?: string;
23
+ readonly dismissable: boolean;
24
+ readonly retryAction?: () => void;
25
+ readonly timestamp: number;
26
+ }
27
+
28
+ export interface PushErrorOptions {
29
+ readonly message: string;
30
+ readonly category: ErrorCategory;
31
+ readonly source?: string;
32
+ readonly dismissable?: boolean;
33
+ readonly retryAction?: () => void;
34
+ }
35
+
36
+ export interface ErrorState {
37
+ readonly errors: readonly AppError[];
38
+
39
+ // Actions
40
+ readonly pushError: (options: PushErrorOptions) => void;
41
+ readonly dismissError: (id: string) => void;
42
+ readonly dismissAll: () => void;
43
+ readonly getErrorsForSource: (source: string) => readonly AppError[];
44
+ readonly hasErrors: () => boolean;
45
+ }
46
+
47
+ // =============================================================================
48
+ // Constants
49
+ // =============================================================================
50
+
51
+ const MAX_ERRORS = 50;
52
+
53
+ let errorCounter = 0;
54
+
55
+ // =============================================================================
56
+ // Store
57
+ // =============================================================================
58
+
59
+ export const useErrorStore = create<ErrorState>((set, get) => ({
60
+ errors: [],
61
+
62
+ pushError: (options) => {
63
+ const error: AppError = {
64
+ id: `err-${++errorCounter}-${Date.now()}`,
65
+ message: options.message,
66
+ category: options.category,
67
+ source: options.source,
68
+ dismissable: options.dismissable ?? true,
69
+ retryAction: options.retryAction,
70
+ timestamp: Date.now(),
71
+ };
72
+
73
+ set((state) => {
74
+ const next = [...state.errors, error];
75
+ // Evict oldest if over limit
76
+ if (next.length > MAX_ERRORS) {
77
+ return { errors: next.slice(next.length - MAX_ERRORS) };
78
+ }
79
+ return { errors: next };
80
+ });
81
+ },
82
+
83
+ dismissError: (id) => {
84
+ set((state) => ({
85
+ errors: state.errors.filter((e) => e.id !== id),
86
+ }));
87
+ },
88
+
89
+ dismissAll: () => {
90
+ set((state) => ({
91
+ errors: state.errors.filter((e) => !e.dismissable),
92
+ }));
93
+ },
94
+
95
+ getErrorsForSource: (source) => {
96
+ return get().errors.filter((e) => e.source === source);
97
+ },
98
+
99
+ hasErrors: () => {
100
+ return get().errors.length > 0;
101
+ },
102
+ }));
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Zustand store for real-time SSE events.
3
+ */
4
+
5
+ import { create } from "zustand";
6
+ import type { SseEvent } from "@nexus-ai-fs/api-client";
7
+ import { SseClient } from "@nexus-ai-fs/api-client";
8
+ import { CircularBuffer } from "../shared/lib/circular-buffer.js";
9
+
10
+ const EVENTS_BUFFER_CAPACITY = 10_000;
11
+ const MAX_RECONNECT_ATTEMPTS = 10;
12
+
13
+ export interface EventFilters {
14
+ readonly eventType: string | null;
15
+ readonly search: string | null;
16
+ }
17
+
18
+ export interface SseIdentity {
19
+ readonly agentId?: string;
20
+ readonly subject?: string;
21
+ readonly zoneId?: string;
22
+ }
23
+
24
+ export interface EventsState {
25
+ readonly events: readonly SseEvent[];
26
+ readonly connected: boolean;
27
+ readonly reconnectCount: number;
28
+ readonly reconnectExhausted: boolean;
29
+ readonly filters: EventFilters;
30
+ readonly filteredEvents: readonly SseEvent[];
31
+ readonly eventsOverflowed: boolean;
32
+ readonly evictedCount: number;
33
+
34
+ // Internal circular buffer (not serializable, but that's fine for Zustand)
35
+ readonly eventsBuffer: CircularBuffer<SseEvent>;
36
+
37
+ // SSE client instance (not serializable, but that's fine for Zustand)
38
+ readonly sseClient: SseClient | null;
39
+
40
+ // Last connection params for manual reconnect after exhaustion
41
+ readonly lastConnectParams: { baseUrl: string; apiKey: string; identity?: SseIdentity } | null;
42
+
43
+ // Actions
44
+ readonly connect: (baseUrl: string, apiKey: string, identity?: SseIdentity) => void;
45
+ readonly disconnect: () => void;
46
+ readonly setFilter: (filters: Partial<EventFilters>) => void;
47
+ readonly clearEvents: () => void;
48
+ }
49
+
50
+ export const useEventsStore = create<EventsState>((set, get) => ({
51
+ events: [],
52
+ connected: false,
53
+ reconnectCount: 0,
54
+ reconnectExhausted: false,
55
+ filters: { eventType: null, search: null },
56
+ filteredEvents: [],
57
+ eventsOverflowed: false,
58
+ evictedCount: 0,
59
+ eventsBuffer: new CircularBuffer<SseEvent>(EVENTS_BUFFER_CAPACITY),
60
+ sseClient: null,
61
+ lastConnectParams: null,
62
+
63
+ connect: (baseUrl, apiKey, identity) => {
64
+ // Disconnect existing
65
+ get().sseClient?.disconnect();
66
+
67
+ const client = new SseClient({
68
+ baseUrl,
69
+ apiKey,
70
+ agentId: identity?.agentId,
71
+ subject: identity?.subject,
72
+ zoneId: identity?.zoneId,
73
+ });
74
+
75
+ client.onEvent((newEvents) => {
76
+ set((state) => {
77
+ // Note: eventsBuffer is mutated in-place (push), but consumers read
78
+ // the derived `events` array (new ref each time), so shallow-comparison
79
+ // subscribers re-render correctly. Do NOT subscribe to eventsBuffer directly.
80
+ const buf = state.eventsBuffer;
81
+ for (const event of newEvents) {
82
+ buf.push(event);
83
+ }
84
+ const allEvents = buf.toArray();
85
+ return {
86
+ events: allEvents,
87
+ filteredEvents: applyFilters(allEvents, state.filters),
88
+ eventsOverflowed: buf.hasOverflowed,
89
+ evictedCount: buf.evictedCount,
90
+ connected: true, // confirmed connected on first event
91
+ reconnectCount: 0,
92
+ reconnectExhausted: false,
93
+ };
94
+ });
95
+ });
96
+
97
+ client.onReconnect((attempt) => {
98
+ if (attempt >= MAX_RECONNECT_ATTEMPTS) {
99
+ client.disconnect();
100
+ set({ connected: false, reconnectExhausted: true, sseClient: null, reconnectCount: attempt });
101
+ } else {
102
+ set({ reconnectCount: attempt });
103
+ }
104
+ });
105
+
106
+ client.onError(() => {
107
+ set({ connected: false });
108
+ });
109
+
110
+ set({
111
+ sseClient: client,
112
+ connected: false,
113
+ reconnectCount: 0,
114
+ reconnectExhausted: false,
115
+ lastConnectParams: { baseUrl, apiKey, identity },
116
+ });
117
+
118
+ // Connect async — don't await (fire and forget)
119
+ client.connect("/api/v2/events/stream").catch(() => {
120
+ set({ connected: false });
121
+ });
122
+ },
123
+
124
+ disconnect: () => {
125
+ get().sseClient?.disconnect();
126
+ set({ sseClient: null, connected: false, reconnectCount: 0, reconnectExhausted: false });
127
+ },
128
+
129
+ setFilter: (partial) => {
130
+ set((state) => {
131
+ const filters = { ...state.filters, ...partial };
132
+ return {
133
+ filters,
134
+ filteredEvents: applyFilters(state.events, filters),
135
+ };
136
+ });
137
+ },
138
+
139
+ clearEvents: () => {
140
+ get().sseClient?.clearBuffer();
141
+ get().eventsBuffer.clear();
142
+ set({ events: [], filteredEvents: [], eventsOverflowed: false, evictedCount: 0 });
143
+ },
144
+ }));
145
+
146
+ function applyFilters(
147
+ events: readonly SseEvent[],
148
+ filters: EventFilters,
149
+ ): readonly SseEvent[] {
150
+ let result = events;
151
+
152
+ if (filters.eventType) {
153
+ const type = filters.eventType;
154
+ result = result.filter((e) => e.event === type);
155
+ }
156
+
157
+ if (filters.search) {
158
+ const lower = filters.search.toLowerCase();
159
+ result = result.filter((e) => e.data.toLowerCase().includes(lower));
160
+ }
161
+
162
+ return result;
163
+ }