@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,476 @@
1
+ /**
2
+ * Zustand store for the API Console panel.
3
+ */
4
+
5
+ import { create } from "zustand";
6
+ import type { FetchClient } from "@nexus-ai-fs/api-client";
7
+ import { categorizeError } from "./create-api-action.js";
8
+ import { useErrorStore } from "./error-store.js";
9
+ import { useUiStore } from "./ui-store.js";
10
+
11
+ /** Minimal OpenAPI 3.x spec shape — only what we parse. */
12
+ interface OpenApiSpec {
13
+ readonly paths?: Readonly<Record<string, Record<string, unknown>>>;
14
+ }
15
+
16
+ // =============================================================================
17
+ // Types
18
+ // =============================================================================
19
+
20
+ export interface EndpointInfo {
21
+ readonly method: string;
22
+ readonly path: string;
23
+ readonly summary: string;
24
+ readonly tags: readonly string[];
25
+ }
26
+
27
+ export interface RequestState {
28
+ readonly method: string;
29
+ readonly path: string;
30
+ readonly pathParams: Readonly<Record<string, string>>;
31
+ readonly queryParams: Readonly<Record<string, string>>;
32
+ readonly headers: Readonly<Record<string, string>>;
33
+ readonly body: string;
34
+ }
35
+
36
+ export interface ResponseState {
37
+ readonly status: number;
38
+ readonly statusText: string;
39
+ readonly headers: Readonly<Record<string, string>>;
40
+ readonly body: string;
41
+ readonly timeMs: number;
42
+ readonly error?: string;
43
+ }
44
+
45
+ export interface ConsoleHistoryEntry {
46
+ readonly request: RequestState;
47
+ readonly response: ResponseState;
48
+ readonly timestamp: number;
49
+ }
50
+
51
+ const EMPTY_REQUEST: RequestState = {
52
+ method: "GET",
53
+ path: "",
54
+ pathParams: {},
55
+ queryParams: {},
56
+ headers: {},
57
+ body: "",
58
+ };
59
+
60
+ const MAX_HISTORY = 50;
61
+ const MAX_COMMAND_HISTORY = 100;
62
+
63
+ // =============================================================================
64
+ // CLI-like command parsing (Decision 8A: discriminated union)
65
+ // =============================================================================
66
+
67
+ /** An HTTP API request (CLI shorthand or raw HTTP method). */
68
+ export interface HttpCommand {
69
+ readonly type: "http";
70
+ readonly method: string;
71
+ readonly path: string;
72
+ readonly body: string;
73
+ }
74
+
75
+ /** A local nexus CLI command to execute via Bun.spawn() (Decision 4A: allowlist). */
76
+ export interface LocalCommand {
77
+ readonly type: "local";
78
+ readonly command: string;
79
+ readonly args: readonly string[];
80
+ }
81
+
82
+ export type ParsedCommand = HttpCommand | LocalCommand;
83
+
84
+ const CLI_COMMANDS: Readonly<
85
+ Record<string, { readonly method: string; readonly pathFn: (arg: string) => string; readonly bodyFn?: (arg: string) => string }>
86
+ > = {
87
+ ls: { method: "GET", pathFn: (p) => `/api/v2/files/list?path=${encodeURIComponent(p)}` },
88
+ cat: { method: "GET", pathFn: (p) => `/api/v2/files/read?path=${encodeURIComponent(p)}` },
89
+ stat: { method: "GET", pathFn: (p) => `/api/v2/files/metadata?path=${encodeURIComponent(p)}` },
90
+ rm: { method: "DELETE", pathFn: (p) => `/api/v2/files?path=${encodeURIComponent(p)}` },
91
+ mkdir: {
92
+ method: "POST",
93
+ pathFn: () => "/api/v2/files/mkdir",
94
+ bodyFn: (p) => JSON.stringify({ path: p }),
95
+ },
96
+ };
97
+
98
+ const HTTP_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
99
+
100
+ /**
101
+ * Allowlist of nexus subcommands that can be run locally from the TUI (Decision 4A).
102
+ * Each entry is the first token after `nexus` — compound commands like `demo init`
103
+ * are validated by checking the first token only (the CLI handles the rest).
104
+ */
105
+ const ALLOWED_LOCAL_COMMANDS = new Set(["init", "build", "demo", "brick", "agent", "up"]);
106
+
107
+ /**
108
+ * Parse a command string into an HTTP command or local command.
109
+ *
110
+ * Input formats:
111
+ * - CLI shorthand: `ls /path`, `cat /file`, `stat /file`, `rm /file`, `mkdir /dir`
112
+ * - Raw HTTP: `GET /api/v2/health`, `POST /api/v2/files/write {"body": true}`
113
+ * - Local command: `!init --preset shared`, `!build`, `!demo init`
114
+ *
115
+ * Returns `null` if the input cannot be parsed.
116
+ */
117
+ export function parseCommand(input: string): ParsedCommand | null {
118
+ const trimmed = input.trim();
119
+ if (!trimmed) return null;
120
+
121
+ // Local command: starts with "!" prefix (Decision 4A)
122
+ if (trimmed.startsWith("!")) {
123
+ const cmdStr = trimmed.slice(1).trim();
124
+ if (!cmdStr) return null;
125
+
126
+ const parts = cmdStr.split(/\s+/);
127
+ const subcommand = parts[0]!;
128
+
129
+ if (!ALLOWED_LOCAL_COMMANDS.has(subcommand)) {
130
+ return null; // Not in allowlist
131
+ }
132
+
133
+ return {
134
+ type: "local",
135
+ command: subcommand,
136
+ args: parts.slice(1),
137
+ };
138
+ }
139
+
140
+ // Split on first space
141
+ const spaceIdx = trimmed.indexOf(" ");
142
+ if (spaceIdx === -1) return null;
143
+
144
+ const firstWord = trimmed.slice(0, spaceIdx);
145
+ const rest = trimmed.slice(spaceIdx + 1).trim();
146
+
147
+ // CLI shorthand: ls, cat, stat, rm, mkdir
148
+ const cmd = CLI_COMMANDS[firstWord];
149
+ if (cmd) {
150
+ return {
151
+ type: "http",
152
+ method: cmd.method,
153
+ path: cmd.pathFn(rest),
154
+ body: cmd.bodyFn ? cmd.bodyFn(rest) : "",
155
+ };
156
+ }
157
+
158
+ // Raw HTTP: METHOD /path [{body}]
159
+ if (HTTP_METHODS.has(firstWord.toUpperCase())) {
160
+ const method = firstWord.toUpperCase();
161
+ // Check if rest has a JSON body after the path
162
+ const bodyMatch = rest.match(/^(\S+)\s+(\{[\s\S]*\})$/);
163
+ if (bodyMatch) {
164
+ return { type: "http", method, path: bodyMatch[1] ?? "", body: bodyMatch[2] ?? "" };
165
+ }
166
+ return { type: "http", method, path: rest, body: "" };
167
+ }
168
+
169
+ return null;
170
+ }
171
+
172
+ // =============================================================================
173
+ // Store
174
+ // =============================================================================
175
+
176
+ export interface ApiConsoleState {
177
+ // Endpoint registry
178
+ readonly endpoints: readonly EndpointInfo[];
179
+ readonly filteredEndpoints: readonly EndpointInfo[];
180
+ readonly selectedEndpoint: EndpointInfo | null;
181
+ readonly tagFilter: string | null;
182
+ readonly searchQuery: string;
183
+
184
+ // Request/response
185
+ readonly request: RequestState;
186
+ readonly response: ResponseState | null;
187
+ readonly isLoading: boolean;
188
+
189
+ // History
190
+ readonly history: readonly ConsoleHistoryEntry[];
191
+
192
+ // Command history (for arrow-key navigation)
193
+ readonly commandHistory: readonly string[];
194
+ readonly historyIndex: number;
195
+
196
+ // Command input mode
197
+ readonly commandInputMode: boolean;
198
+ readonly commandInputBuffer: string;
199
+
200
+ // Actions
201
+ readonly setEndpoints: (endpoints: readonly EndpointInfo[]) => void;
202
+ readonly selectEndpoint: (ep: EndpointInfo) => void;
203
+ readonly updateRequest: (partial: Partial<RequestState>) => void;
204
+ readonly setTagFilter: (tag: string | null) => void;
205
+ readonly setSearchQuery: (q: string) => void;
206
+ readonly executeRequest: (client: FetchClient) => Promise<void>;
207
+ readonly executeCommand: (input: string, client: FetchClient) => Promise<void>;
208
+ readonly fetchOpenApiSpec: (client: FetchClient) => Promise<void>;
209
+ readonly clearResponse: () => void;
210
+ readonly navigateHistory: (direction: "up" | "down") => void;
211
+ readonly setCommandInputMode: (enabled: boolean) => void;
212
+ readonly setCommandInputBuffer: (buffer: string) => void;
213
+ }
214
+
215
+ const SOURCE = "console";
216
+
217
+ export const useApiConsoleStore = create<ApiConsoleState>((set, get) => ({
218
+ endpoints: [],
219
+ filteredEndpoints: [],
220
+ selectedEndpoint: null,
221
+ tagFilter: null,
222
+ searchQuery: "",
223
+ request: EMPTY_REQUEST,
224
+ response: null,
225
+ isLoading: false,
226
+ history: [],
227
+ commandHistory: [],
228
+ historyIndex: -1,
229
+ commandInputMode: false,
230
+ commandInputBuffer: "",
231
+
232
+ setEndpoints: (endpoints) => {
233
+ set({ endpoints, filteredEndpoints: endpoints });
234
+ },
235
+
236
+ selectEndpoint: (ep) => {
237
+ set({
238
+ selectedEndpoint: ep,
239
+ request: {
240
+ method: ep.method,
241
+ path: ep.path,
242
+ pathParams: {},
243
+ queryParams: {},
244
+ headers: {},
245
+ body: "",
246
+ },
247
+ response: null,
248
+ });
249
+ },
250
+
251
+ updateRequest: (partial) => {
252
+ set((state) => ({
253
+ request: { ...state.request, ...partial },
254
+ }));
255
+ },
256
+
257
+ setTagFilter: (tag) => {
258
+ const { endpoints, searchQuery } = get();
259
+ const filtered = filterEndpoints(endpoints, tag, searchQuery);
260
+ set({ tagFilter: tag, filteredEndpoints: filtered });
261
+ },
262
+
263
+ setSearchQuery: (q) => {
264
+ const { endpoints, tagFilter } = get();
265
+ const filtered = filterEndpoints(endpoints, tagFilter, q);
266
+ set({ searchQuery: q, filteredEndpoints: filtered });
267
+ },
268
+
269
+ executeRequest: async (client) => {
270
+ const { request } = get();
271
+ set({ isLoading: true, response: null });
272
+
273
+ // Build the actual path with path params substituted
274
+ let resolvedPath = request.path;
275
+ for (const [key, value] of Object.entries(request.pathParams)) {
276
+ resolvedPath = resolvedPath.replace(`{${key}}`, encodeURIComponent(value));
277
+ }
278
+
279
+ // Add query params
280
+ const queryEntries = Object.entries(request.queryParams).filter(([, v]) => v !== "");
281
+ if (queryEntries.length > 0) {
282
+ const params = new URLSearchParams(queryEntries);
283
+ resolvedPath += `?${params.toString()}`;
284
+ }
285
+
286
+ const start = performance.now();
287
+
288
+ try {
289
+ const body = request.body && request.method !== "GET" && request.method !== "HEAD"
290
+ ? request.body
291
+ : undefined;
292
+
293
+ // Use FetchClient.rawRequest — auth and identity headers injected automatically
294
+ const resp = await client.rawRequest(request.method, resolvedPath, body, {
295
+ headers: request.headers,
296
+ });
297
+ const timeMs = performance.now() - start;
298
+
299
+ let respBody: string;
300
+ const contentType = resp.headers.get("Content-Type") ?? "";
301
+ if (contentType.includes("json")) {
302
+ const json = await resp.json();
303
+ respBody = JSON.stringify(json, null, 2);
304
+ } else {
305
+ respBody = await resp.text();
306
+ }
307
+
308
+ const responseHeaders: Record<string, string> = {};
309
+ resp.headers.forEach((value, key) => {
310
+ responseHeaders[key] = value;
311
+ });
312
+
313
+ const responseState: ResponseState = {
314
+ status: resp.status,
315
+ statusText: resp.statusText,
316
+ headers: responseHeaders,
317
+ body: respBody,
318
+ timeMs,
319
+ };
320
+
321
+ const entry: ConsoleHistoryEntry = {
322
+ request,
323
+ response: responseState,
324
+ timestamp: Date.now(),
325
+ };
326
+
327
+ const commandStr = `${request.method} ${request.path}`;
328
+ set((state) => ({
329
+ response: responseState,
330
+ isLoading: false,
331
+ history: [entry, ...state.history.slice(0, MAX_HISTORY - 1)],
332
+ commandHistory: [
333
+ ...state.commandHistory,
334
+ commandStr,
335
+ ].slice(-MAX_COMMAND_HISTORY),
336
+ historyIndex: -1,
337
+ }));
338
+ useUiStore.getState().markDataUpdated("console");
339
+ } catch (err) {
340
+ const timeMs = performance.now() - start;
341
+ const message = err instanceof Error ? err.message : "Request failed";
342
+ const responseState: ResponseState = {
343
+ status: 0,
344
+ statusText: "Network Error",
345
+ headers: {},
346
+ body: "",
347
+ timeMs,
348
+ error: message,
349
+ };
350
+
351
+ set({ response: responseState, isLoading: false });
352
+ useErrorStore.getState().pushError({ message, category: categorizeError(message), source: SOURCE });
353
+ }
354
+ },
355
+
356
+ fetchOpenApiSpec: async (client) => {
357
+ try {
358
+ const spec = await client.get<OpenApiSpec>("/openapi.json");
359
+ const endpoints: EndpointInfo[] = [];
360
+
361
+ for (const [path, methods] of Object.entries(spec.paths ?? {})) {
362
+ for (const [method, operation] of Object.entries(methods ?? {})) {
363
+ if (method === "parameters" || typeof operation !== "object" || operation === null) continue;
364
+ const op = operation as { summary?: string; tags?: string[] };
365
+ endpoints.push({
366
+ method: method.toUpperCase(),
367
+ path,
368
+ summary: op.summary ?? "",
369
+ tags: op.tags ?? [],
370
+ });
371
+ }
372
+ }
373
+
374
+ // Sort: by path, then by method
375
+ endpoints.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
376
+ get().setEndpoints(endpoints);
377
+ useUiStore.getState().markDataUpdated("console");
378
+ } catch (err) {
379
+ const message = err instanceof Error ? err.message : "Failed to fetch OpenAPI spec";
380
+ useErrorStore.getState().pushError({ message, category: categorizeError(message), source: SOURCE });
381
+ }
382
+ },
383
+
384
+ executeCommand: async (input, client) => {
385
+ const parsed = parseCommand(input);
386
+ if (!parsed) return;
387
+
388
+ if (parsed.type === "local") {
389
+ // Local command — dispatch to CommandRunner (Phase 2)
390
+ const { executeLocalCommand } = await import("../services/command-runner.js");
391
+ executeLocalCommand(parsed.command, parsed.args);
392
+ return;
393
+ }
394
+
395
+ // HTTP command — existing flow
396
+ get().updateRequest({
397
+ method: parsed.method,
398
+ path: parsed.path,
399
+ body: parsed.body,
400
+ pathParams: {},
401
+ queryParams: {},
402
+ headers: {},
403
+ });
404
+
405
+ // Wait one tick so state is flushed before executing
406
+ await Promise.resolve();
407
+ await get().executeRequest(client);
408
+ },
409
+
410
+ navigateHistory: (direction) => {
411
+ const { commandHistory, historyIndex } = get();
412
+ if (commandHistory.length === 0) return;
413
+
414
+ let newIndex: number;
415
+ if (direction === "up") {
416
+ // Move backward through history (toward older commands)
417
+ if (historyIndex === -1) {
418
+ newIndex = commandHistory.length - 1;
419
+ } else {
420
+ newIndex = Math.max(historyIndex - 1, 0);
421
+ }
422
+ } else {
423
+ // Move forward through history (toward newer commands)
424
+ if (historyIndex === -1) return;
425
+ newIndex = historyIndex + 1;
426
+ if (newIndex >= commandHistory.length) {
427
+ // Past the newest entry — clear
428
+ set({ historyIndex: -1, commandInputBuffer: "" });
429
+ return;
430
+ }
431
+ }
432
+
433
+ const entry = commandHistory[newIndex];
434
+ set({
435
+ historyIndex: newIndex,
436
+ commandInputBuffer: entry ?? "",
437
+ });
438
+ },
439
+
440
+ setCommandInputMode: (enabled) => {
441
+ set({ commandInputMode: enabled, historyIndex: -1 });
442
+ if (enabled) {
443
+ set({ commandInputBuffer: "" });
444
+ }
445
+ },
446
+
447
+ setCommandInputBuffer: (buffer) => {
448
+ set({ commandInputBuffer: buffer });
449
+ },
450
+
451
+ clearResponse: () => set({ response: null }),
452
+ }));
453
+
454
+ function filterEndpoints(
455
+ endpoints: readonly EndpointInfo[],
456
+ tag: string | null,
457
+ query: string,
458
+ ): readonly EndpointInfo[] {
459
+ let result = endpoints;
460
+
461
+ if (tag) {
462
+ result = result.filter((ep) => ep.tags.includes(tag));
463
+ }
464
+
465
+ if (query) {
466
+ const lower = query.toLowerCase();
467
+ result = result.filter(
468
+ (ep) =>
469
+ ep.path.toLowerCase().includes(lower) ||
470
+ ep.summary.toLowerCase().includes(lower) ||
471
+ ep.method.toLowerCase().includes(lower),
472
+ );
473
+ }
474
+
475
+ return result;
476
+ }