@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,656 @@
1
+ /**
2
+ * Zustand store for Search & Knowledge panel.
3
+ *
4
+ * Manages unified search, knowledge graph exploration,
5
+ * and agent memories.
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
+ import { useUiStore } from "./ui-store.js";
13
+
14
+ // Re-export all public types for backward compatibility
15
+ export type {
16
+ SearchResult, KnowledgeEntity, NeighborEntry, Memory,
17
+ PlaybookRecord, MemoryVersion, MemoryHistory, MemoryDiff,
18
+ RlmStep, RlmAnswer, SearchTab, SearchMode,
19
+ } from "./search-store-types.js";
20
+
21
+ import type {
22
+ SearchResult, KnowledgeEntity, NeighborEntry, Memory,
23
+ PlaybookRecord, MemoryVersion, MemoryHistory, MemoryDiff,
24
+ RlmStep, RlmAnswer, SearchTab, SearchMode,
25
+ SearchQueryResponse, EntityResponse, NeighborsResponse,
26
+ KnowledgeSearchResponse, MemorySearchResponse, MemoryDetailResponse,
27
+ PlaybooksListResponse, MemoryHistoryResponse, MemoryDiffResponse,
28
+ MemoryRollbackResponse,
29
+ } from "./search-store-types.js";
30
+
31
+ const SEARCH_MODE_ORDER: readonly SearchMode[] = ["keyword", "semantic", "hybrid"];
32
+
33
+ // =============================================================================
34
+ // Store
35
+ // =============================================================================
36
+
37
+ export interface SearchState {
38
+ // Search
39
+ readonly searchQuery: string;
40
+ readonly searchResults: readonly SearchResult[];
41
+ readonly searchTotal: number;
42
+ readonly selectedResultIndex: number;
43
+ readonly searchLoading: boolean;
44
+ // Expanded file content (Enter on search result)
45
+ readonly expandedContent: string | null;
46
+ readonly expandedPath: string | null;
47
+
48
+ // Knowledge graph
49
+ readonly selectedEntity: KnowledgeEntity | null;
50
+ readonly neighbors: readonly NeighborEntry[];
51
+ readonly knowledgeSearchResult: KnowledgeEntity | null;
52
+ readonly knowledgeLoading: boolean;
53
+
54
+ // Memories
55
+ readonly memories: readonly Memory[];
56
+ readonly selectedMemoryIndex: number;
57
+ readonly memoriesLoading: boolean;
58
+
59
+ // Playbooks
60
+ readonly playbooks: readonly PlaybookRecord[];
61
+ readonly playbooksLoading: boolean;
62
+ readonly selectedPlaybookIndex: number;
63
+
64
+ // Memory versioning
65
+ readonly memoryHistory: MemoryHistory | null;
66
+ readonly memoryHistoryLoading: boolean;
67
+ readonly memoryDiff: MemoryDiff | null;
68
+ readonly memoryDiffLoading: boolean;
69
+
70
+ // RLM Q&A (document-scoped via context_paths)
71
+ readonly rlmAnswer: RlmAnswer | null;
72
+ readonly rlmLoading: boolean;
73
+ readonly rlmContextPaths: readonly string[];
74
+
75
+ // Search mode
76
+ readonly searchMode: SearchMode;
77
+
78
+ // Input buffer (persists across tab switches)
79
+ readonly inputBuffer: string;
80
+
81
+ // Shared
82
+ readonly activeTab: SearchTab;
83
+ readonly error: string | null;
84
+
85
+ // Actions
86
+ readonly search: (query: string, client: FetchClient) => Promise<void>;
87
+ readonly fetchEntity: (id: string, client: FetchClient) => Promise<void>;
88
+ readonly fetchNeighbors: (id: string, client: FetchClient) => Promise<void>;
89
+ readonly searchKnowledge: (name: string, client: FetchClient) => Promise<void>;
90
+ readonly fetchMemories: (query: string, client: FetchClient) => Promise<void>;
91
+ readonly fetchMemoryDetail: (id: string, client: FetchClient) => Promise<void>;
92
+ readonly setActiveTab: (tab: SearchTab) => void;
93
+ readonly setSelectedResultIndex: (index: number) => void;
94
+ readonly setSelectedMemoryIndex: (index: number) => void;
95
+ readonly setSearchQuery: (query: string) => void;
96
+ readonly setInputBuffer: (buffer: string) => void;
97
+ readonly setSearchMode: (mode: SearchMode) => void;
98
+ readonly cycleSearchMode: () => void;
99
+ readonly fetchPlaybooks: (query: string, client: FetchClient) => Promise<void>;
100
+ readonly deletePlaybook: (id: string, client: FetchClient) => Promise<void>;
101
+ readonly setSelectedPlaybookIndex: (index: number) => void;
102
+ readonly fetchMemoryHistory: (memoryId: string, client: FetchClient) => Promise<void>;
103
+ readonly fetchMemoryDiff: (memoryId: string, v1: number, v2: number, client: FetchClient) => Promise<void>;
104
+ readonly rollbackMemory: (memoryId: string, version: number, client: FetchClient) => Promise<void>;
105
+ readonly clearMemoryHistory: () => void;
106
+ readonly clearMemoryDiff: () => void;
107
+ readonly addRlmContextPath: (path: string) => void;
108
+ readonly removeRlmContextPath: (path: string) => void;
109
+ readonly clearRlmContextPaths: () => void;
110
+ readonly askRlm: (query: string, client: FetchClient, zoneId?: string) => Promise<void>;
111
+
112
+ // Memory mutations
113
+ readonly createMemory: (content: string, metadata: Record<string, unknown>, client: FetchClient) => Promise<void>;
114
+ readonly updateMemory: (memoryId: string, content: string, client: FetchClient) => Promise<void>;
115
+ readonly deleteMemory: (memoryId: string, client: FetchClient) => Promise<void>;
116
+ }
117
+
118
+ const SOURCE = "search";
119
+
120
+ let activeAskController: AbortController | null = null;
121
+
122
+ export const useSearchStore = create<SearchState>((set, get) => ({
123
+ searchQuery: "",
124
+ searchResults: [],
125
+ searchTotal: 0,
126
+ expandedContent: null,
127
+ expandedPath: null,
128
+ selectedResultIndex: 0,
129
+ searchLoading: false,
130
+
131
+ selectedEntity: null,
132
+ neighbors: [],
133
+ knowledgeSearchResult: null,
134
+ knowledgeLoading: false,
135
+
136
+ memories: [],
137
+ selectedMemoryIndex: 0,
138
+ memoriesLoading: false,
139
+
140
+ playbooks: [],
141
+ playbooksLoading: false,
142
+ selectedPlaybookIndex: 0,
143
+
144
+ memoryHistory: null,
145
+ memoryHistoryLoading: false,
146
+ memoryDiff: null,
147
+ memoryDiffLoading: false,
148
+
149
+ rlmAnswer: null,
150
+ rlmLoading: false,
151
+ rlmContextPaths: [],
152
+
153
+ inputBuffer: "",
154
+
155
+ searchMode: "hybrid",
156
+
157
+ activeTab: "search",
158
+ error: null,
159
+
160
+ // =========================================================================
161
+ // Actions migrated to createApiAction (Decision 6A)
162
+ // =========================================================================
163
+
164
+ search: async (query, client) => {
165
+ set({ searchLoading: true, error: null, searchQuery: query });
166
+
167
+ try {
168
+ const { searchMode } = get();
169
+ const params = new URLSearchParams({
170
+ q: query,
171
+ type: searchMode,
172
+ limit: "10",
173
+ });
174
+ const response = await client.get<SearchQueryResponse>(
175
+ `/api/v2/search/query?${params.toString()}`,
176
+ );
177
+
178
+ const results = response.results ?? [];
179
+ set({
180
+ searchResults: results,
181
+ searchTotal: response.total ?? results.length,
182
+ selectedResultIndex: 0,
183
+ searchLoading: false,
184
+ });
185
+ useUiStore.getState().markDataUpdated("search");
186
+ } catch (err) {
187
+ const message = err instanceof Error ? err.message : "Failed to search";
188
+ set({ searchLoading: false, error: message });
189
+ useErrorStore.getState().pushError({ message, category: categorizeError(message), source: SOURCE });
190
+ }
191
+ },
192
+
193
+ fetchEntity: createApiAction<SearchState, [string, FetchClient]>(set, {
194
+ loadingKey: "knowledgeLoading",
195
+ source: SOURCE,
196
+ action: async (id, client) => {
197
+ const response = await client.get<EntityResponse>(
198
+ `/api/v2/graph/entity/${encodeURIComponent(id)}`,
199
+ );
200
+ return { selectedEntity: response.entity ?? null };
201
+ },
202
+ }),
203
+
204
+ fetchNeighbors: async (id, client) => {
205
+ set({ knowledgeLoading: true, error: null });
206
+
207
+ try {
208
+ const response = await client.get<NeighborsResponse>(
209
+ `/api/v2/graph/entity/${encodeURIComponent(id)}/neighbors?hops=1&direction=both`,
210
+ );
211
+ set({
212
+ neighbors: response.neighbors ?? [],
213
+ knowledgeLoading: false,
214
+ });
215
+ useUiStore.getState().markDataUpdated("search");
216
+ } catch (err) {
217
+ const message = err instanceof Error ? err.message : "Failed to fetch neighbors";
218
+ set({
219
+ neighbors: [],
220
+ knowledgeLoading: false,
221
+ error: message,
222
+ });
223
+ useErrorStore.getState().pushError({ message, category: categorizeError(message), source: SOURCE });
224
+ }
225
+ },
226
+
227
+ searchKnowledge: async (name, client) => {
228
+ set({ knowledgeLoading: true, error: null });
229
+
230
+ try {
231
+ const params = new URLSearchParams({ name, fuzzy: "false" });
232
+ const response = await client.get<KnowledgeSearchResponse>(
233
+ `/api/v2/graph/search?${params.toString()}`,
234
+ );
235
+ set({
236
+ knowledgeSearchResult: response.entity ?? null,
237
+ knowledgeLoading: false,
238
+ });
239
+ useUiStore.getState().markDataUpdated("search");
240
+ } catch (err) {
241
+ const message = err instanceof Error ? err.message : "Failed to search knowledge graph";
242
+ set({
243
+ knowledgeSearchResult: null,
244
+ knowledgeLoading: false,
245
+ error: message,
246
+ });
247
+ useErrorStore.getState().pushError({ message, category: categorizeError(message), source: SOURCE });
248
+ }
249
+ },
250
+
251
+ fetchMemories: createApiAction<SearchState, [string, FetchClient]>(set, {
252
+ loadingKey: "memoriesLoading",
253
+ source: SOURCE,
254
+ action: async (query, client) => {
255
+ const response = await client.post<MemorySearchResponse>(
256
+ "/api/v2/memories/search",
257
+ { query },
258
+ );
259
+ return {
260
+ memories: response.memories ?? [],
261
+ selectedMemoryIndex: 0,
262
+ };
263
+ },
264
+ }),
265
+
266
+ fetchMemoryDetail: async (id, client) => {
267
+ set({ memoriesLoading: true, error: null });
268
+
269
+ try {
270
+ const response = await client.get<MemoryDetailResponse>(
271
+ `/api/v2/memories/${encodeURIComponent(id)}`,
272
+ );
273
+ const memory = response.memory;
274
+
275
+ set((state) => {
276
+ const memoryId = (memory as Record<string, unknown>).memory_id;
277
+ const updated = state.memories.map((m) =>
278
+ (m as Record<string, unknown>).memory_id === memoryId ? memory : m,
279
+ );
280
+ return { memories: updated, memoriesLoading: false };
281
+ });
282
+ useUiStore.getState().markDataUpdated("search");
283
+ } catch (err) {
284
+ const message = err instanceof Error ? err.message : "Failed to fetch memory detail";
285
+ set({ memoriesLoading: false, error: message });
286
+ useErrorStore.getState().pushError({ message, category: categorizeError(message), source: SOURCE });
287
+ }
288
+ },
289
+
290
+ setActiveTab: (tab) => {
291
+ set({ activeTab: tab, error: null });
292
+ },
293
+
294
+ setSelectedResultIndex: (index) => {
295
+ set({ selectedResultIndex: index });
296
+ },
297
+
298
+ setSelectedMemoryIndex: (index) => {
299
+ set({ selectedMemoryIndex: index });
300
+ },
301
+
302
+ setSearchQuery: (query) => {
303
+ set({ searchQuery: query });
304
+ },
305
+
306
+ setInputBuffer: (buffer) => {
307
+ set({ inputBuffer: buffer });
308
+ },
309
+
310
+ setSearchMode: (mode) => {
311
+ set({ searchMode: mode });
312
+ },
313
+
314
+ cycleSearchMode: () => {
315
+ const { searchMode } = get();
316
+ const currentIdx = SEARCH_MODE_ORDER.indexOf(searchMode);
317
+ const nextIdx = (currentIdx + 1) % SEARCH_MODE_ORDER.length;
318
+ const nextMode = SEARCH_MODE_ORDER[nextIdx];
319
+ if (nextMode) {
320
+ set({ searchMode: nextMode });
321
+ }
322
+ },
323
+
324
+ fetchPlaybooks: createApiAction<SearchState, [string, FetchClient]>(set, {
325
+ loadingKey: "playbooksLoading",
326
+ source: SOURCE,
327
+ action: async (query, client) => {
328
+ const params = new URLSearchParams({ name_pattern: query });
329
+ const response = await client.get<PlaybooksListResponse>(
330
+ `/api/v2/playbooks?${params.toString()}`,
331
+ );
332
+ return {
333
+ playbooks: response.playbooks ?? [],
334
+ selectedPlaybookIndex: 0,
335
+ };
336
+ },
337
+ }),
338
+
339
+ deletePlaybook: async (id, client) => {
340
+ try {
341
+ await client.delete(`/api/v2/playbooks/${encodeURIComponent(id)}`);
342
+ set((state) => ({
343
+ playbooks: state.playbooks.filter((p) => p.playbook_id !== id),
344
+ selectedPlaybookIndex: Math.min(
345
+ state.selectedPlaybookIndex,
346
+ Math.max(state.playbooks.length - 2, 0),
347
+ ),
348
+ error: null,
349
+ }));
350
+ } catch (err) {
351
+ const message = err instanceof Error ? err.message : "Failed to delete playbook";
352
+ set({ error: message });
353
+ useErrorStore.getState().pushError({ message, category: categorizeError(message), source: SOURCE });
354
+ }
355
+ },
356
+
357
+ setSelectedPlaybookIndex: (index) => {
358
+ set({ selectedPlaybookIndex: index });
359
+ },
360
+
361
+ fetchMemoryHistory: createApiAction<SearchState, [string, FetchClient]>(set, {
362
+ loadingKey: "memoryHistoryLoading",
363
+ source: SOURCE,
364
+ action: async (memoryId, client) => {
365
+ const response = await client.get<MemoryHistoryResponse>(
366
+ `/api/v2/memories/${encodeURIComponent(memoryId)}/history`,
367
+ );
368
+ return {
369
+ memoryHistory: {
370
+ memory_id: response.memory_id,
371
+ current_version: response.current_version,
372
+ versions: response.versions ?? [],
373
+ },
374
+ };
375
+ },
376
+ }),
377
+
378
+ fetchMemoryDiff: createApiAction<SearchState, [string, number, number, FetchClient]>(set, {
379
+ loadingKey: "memoryDiffLoading",
380
+ source: SOURCE,
381
+ action: async (memoryId, v1, v2, client) => {
382
+ const params = new URLSearchParams({
383
+ v1: String(v1),
384
+ v2: String(v2),
385
+ mode: "content",
386
+ });
387
+ const response = await client.get<MemoryDiffResponse>(
388
+ `/api/v2/memories/${encodeURIComponent(memoryId)}/diff?${params.toString()}`,
389
+ );
390
+ return {
391
+ memoryDiff: {
392
+ diff: response.diff,
393
+ mode: response.mode,
394
+ v1: response.v1,
395
+ v2: response.v2,
396
+ },
397
+ };
398
+ },
399
+ }),
400
+
401
+ rollbackMemory: async (memoryId, version, client) => {
402
+ set({ memoriesLoading: true, error: null });
403
+
404
+ try {
405
+ // Backend takes version as query param (memories.py:480), not body
406
+ await client.post<MemoryRollbackResponse>(
407
+ `/api/v2/memories/${encodeURIComponent(memoryId)}/rollback?version=${version}`,
408
+ {},
409
+ );
410
+
411
+ // Clear versioning state and refresh memories list
412
+ set({
413
+ memoriesLoading: false,
414
+ memoryHistory: null,
415
+ memoryDiff: null,
416
+ });
417
+
418
+ // Re-fetch memories to get updated state
419
+ const query = get().searchQuery;
420
+ if (query) {
421
+ await get().fetchMemories(query, client);
422
+ }
423
+ } catch (err) {
424
+ const message = err instanceof Error ? err.message : "Failed to rollback memory";
425
+ set({ memoriesLoading: false, error: message });
426
+ useErrorStore.getState().pushError({ message, category: categorizeError(message), source: SOURCE });
427
+ }
428
+ },
429
+
430
+ clearMemoryHistory: () => {
431
+ set({ memoryHistory: null });
432
+ },
433
+
434
+ clearMemoryDiff: () => {
435
+ set({ memoryDiff: null });
436
+ },
437
+
438
+ addRlmContextPath: (path) => {
439
+ set((state) => {
440
+ if (state.rlmContextPaths.includes(path)) return state;
441
+ return { rlmContextPaths: [...state.rlmContextPaths, path] };
442
+ });
443
+ },
444
+
445
+ removeRlmContextPath: (path) => {
446
+ set((state) => ({
447
+ rlmContextPaths: state.rlmContextPaths.filter((p) => p !== path),
448
+ }));
449
+ },
450
+
451
+ clearRlmContextPaths: () => {
452
+ set({ rlmContextPaths: [] });
453
+ },
454
+
455
+ // =========================================================================
456
+ // SSE streaming — left as-is with error store integration
457
+ // =========================================================================
458
+
459
+ askRlm: async (query, client, zoneId) => {
460
+ // Cancel previous request
461
+ activeAskController?.abort();
462
+ activeAskController = new AbortController();
463
+ const signal = activeAskController.signal;
464
+
465
+ const initial: RlmAnswer = {
466
+ status: "streaming",
467
+ answer: null,
468
+ total_tokens: 0,
469
+ total_duration_seconds: 0,
470
+ iterations: 0,
471
+ error_message: null,
472
+ steps: [],
473
+ model: null,
474
+ };
475
+ set({ rlmLoading: true, error: null, rlmAnswer: initial });
476
+
477
+ try {
478
+ const { rlmContextPaths } = get();
479
+ const body: Record<string, unknown> = { query, stream: true };
480
+ if (zoneId) {
481
+ body.zone_id = zoneId;
482
+ }
483
+ if (rlmContextPaths.length > 0) {
484
+ body.context_paths = rlmContextPaths;
485
+ }
486
+
487
+ const response = await client.rawRequest(
488
+ "POST",
489
+ "/api/v2/rlm/infer",
490
+ JSON.stringify(body),
491
+ );
492
+
493
+ if (!response.ok) {
494
+ const errText = await response.text();
495
+ set({
496
+ rlmLoading: false,
497
+ rlmAnswer: { ...initial, status: "error", error_message: `HTTP ${response.status}: ${errText}` },
498
+ });
499
+ return;
500
+ }
501
+
502
+ const reader = response.body?.getReader();
503
+ if (!reader) {
504
+ set({ rlmLoading: false, error: "No response body from RLM" });
505
+ return;
506
+ }
507
+
508
+ const decoder = new TextDecoder();
509
+ let buffer = "";
510
+
511
+ for (;;) {
512
+ if (signal.aborted) break;
513
+ const { done, value } = await reader.read();
514
+ if (done) break;
515
+
516
+ buffer += decoder.decode(value, { stream: true });
517
+ const parts = buffer.split("\n\n");
518
+ buffer = parts.pop() ?? "";
519
+
520
+ for (const part of parts) {
521
+ if (signal.aborted) break;
522
+ let eventName = "";
523
+ let dataStr = "";
524
+ for (const line of part.split("\n")) {
525
+ if (line.startsWith("event: ")) {
526
+ eventName = line.slice(7).trim();
527
+ } else if (line.startsWith("data: ")) {
528
+ dataStr += line.slice(6);
529
+ }
530
+ }
531
+ if (!eventName || !dataStr) continue;
532
+
533
+ try {
534
+ const data = JSON.parse(dataStr) as Record<string, unknown>;
535
+ if (signal.aborted) break;
536
+ const current = get().rlmAnswer ?? initial;
537
+
538
+ if (eventName === "rlm.started") {
539
+ set({
540
+ rlmAnswer: { ...current, model: (data.model as string) ?? null },
541
+ });
542
+ } else if (eventName === "rlm.iteration") {
543
+ const step: RlmStep = {
544
+ step: data.step as number,
545
+ code_executed: data.code_executed as string,
546
+ output_summary: data.output_summary as string,
547
+ tokens_used: data.tokens_used as number,
548
+ duration_seconds: data.duration_seconds as number,
549
+ };
550
+ set({
551
+ rlmAnswer: {
552
+ ...current,
553
+ steps: [...current.steps, step],
554
+ iterations: data.step as number,
555
+ total_tokens: current.total_tokens + (data.tokens_used as number),
556
+ },
557
+ });
558
+ } else if (eventName === "rlm.final_answer") {
559
+ set({
560
+ rlmAnswer: {
561
+ ...current,
562
+ status: "completed",
563
+ answer: data.answer as string,
564
+ total_tokens: data.total_tokens as number,
565
+ total_duration_seconds: data.total_duration_seconds as number,
566
+ iterations: data.iterations as number,
567
+ },
568
+ rlmLoading: false,
569
+ });
570
+ } else if (eventName === "rlm.budget_exceeded") {
571
+ set({
572
+ rlmAnswer: {
573
+ ...current,
574
+ status: "budget_exceeded",
575
+ error_message: data.reason as string,
576
+ total_tokens: data.total_tokens as number,
577
+ iterations: data.iterations as number,
578
+ },
579
+ rlmLoading: false,
580
+ });
581
+ } else if (eventName === "rlm.error") {
582
+ set({
583
+ rlmAnswer: {
584
+ ...current,
585
+ status: "error",
586
+ error_message: data.error as string,
587
+ },
588
+ rlmLoading: false,
589
+ });
590
+ }
591
+ } catch {
592
+ // Skip malformed SSE events
593
+ }
594
+ }
595
+ }
596
+
597
+ // If stream ended without a terminal event, mark complete
598
+ if (!signal.aborted && get().rlmLoading) {
599
+ set({ rlmLoading: false });
600
+ }
601
+ } catch (err) {
602
+ if (signal.aborted) return; // Request was cancelled; ignore error
603
+ const message = err instanceof Error ? err.message : "Failed to query RLM";
604
+ set({ rlmLoading: false, error: message });
605
+ useErrorStore.getState().pushError({ message, category: categorizeError(message), source: SOURCE });
606
+ }
607
+ },
608
+
609
+ // =========================================================================
610
+ // Memory mutations — inline with error store integration
611
+ // =========================================================================
612
+
613
+ createMemory: async (content, metadata, client) => {
614
+ set({ memoriesLoading: true, error: null });
615
+ try {
616
+ await client.post("/api/v2/memories", { content, metadata });
617
+ // Re-fetch to get updated list
618
+ const query = get().searchQuery;
619
+ await get().fetchMemories(query || "", client);
620
+ } catch (err) {
621
+ const message = err instanceof Error ? err.message : "Failed to create memory";
622
+ set({ memoriesLoading: false, error: message });
623
+ useErrorStore.getState().pushError({ message, category: categorizeError(message), source: SOURCE });
624
+ }
625
+ },
626
+
627
+ updateMemory: async (memoryId, content, client) => {
628
+ set({ memoriesLoading: true, error: null });
629
+ try {
630
+ await client.put(`/api/v2/memories/${encodeURIComponent(memoryId)}`, { content });
631
+ set({ memoriesLoading: false });
632
+ // Re-fetch detail
633
+ await get().fetchMemoryDetail(memoryId, client);
634
+ } catch (err) {
635
+ const message = err instanceof Error ? err.message : "Failed to update memory";
636
+ set({ memoriesLoading: false, error: message });
637
+ useErrorStore.getState().pushError({ message, category: categorizeError(message), source: SOURCE });
638
+ }
639
+ },
640
+
641
+ deleteMemory: async (memoryId, client) => {
642
+ set({ memoriesLoading: true, error: null });
643
+ try {
644
+ await client.delete(`/api/v2/memories/${encodeURIComponent(memoryId)}`);
645
+ set((state) => ({
646
+ memories: state.memories.filter((m) => (m as Record<string, unknown>).memory_id !== memoryId),
647
+ selectedMemoryIndex: Math.min(state.selectedMemoryIndex, Math.max(state.memories.length - 2, 0)),
648
+ memoriesLoading: false,
649
+ }));
650
+ } catch (err) {
651
+ const message = err instanceof Error ? err.message : "Failed to delete memory";
652
+ set({ memoriesLoading: false, error: message });
653
+ useErrorStore.getState().pushError({ message, category: categorizeError(message), source: SOURCE });
654
+ }
655
+ },
656
+ }));