@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,96 @@
1
+ /**
2
+ * IPC message view for an agent — inbox, processed, and dead_letter.
3
+ */
4
+
5
+ import React from "react";
6
+ import type { InboxMessage } from "../../stores/agents-store.js";
7
+ import { LoadingIndicator } from "../../shared/components/loading-indicator.js";
8
+ import { statusColor } from "../../shared/theme.js";
9
+ import { textStyle } from "../../shared/text-style.js";
10
+
11
+ interface InboxViewProps {
12
+ readonly messages: readonly InboxMessage[];
13
+ readonly count: number;
14
+ readonly processedMessages: readonly InboxMessage[];
15
+ readonly deadLetterMessages: readonly InboxMessage[];
16
+ readonly loading: boolean;
17
+ readonly selectedIndex?: number;
18
+ readonly previewContent?: string | null;
19
+ }
20
+
21
+ function parseFilename(filename: string): { label: string; ext: string } {
22
+ const lastDot = filename.lastIndexOf(".");
23
+ const ext = lastDot >= 0 ? filename.slice(lastDot + 1).toUpperCase() : "";
24
+ const base = lastDot >= 0 ? filename.slice(0, lastDot) : filename;
25
+ const label = base.replace(/[-_]/g, " ").slice(0, 40);
26
+ return { label, ext };
27
+ }
28
+
29
+ function MessageList({ messages, emptyText }: { messages: readonly InboxMessage[]; emptyText: string }): React.ReactNode {
30
+ if (messages.length === 0) {
31
+ return <text style={textStyle({ dim: true })}>{` ${emptyText}`}</text>;
32
+ }
33
+ return (
34
+ <>
35
+ {messages.map((msg, i) => {
36
+ const { label, ext } = parseFilename(msg.filename);
37
+ const extTag = ext ? ` [${ext}]` : "";
38
+ return (
39
+ <box key={`msg-${i}`} height={1} width="100%">
40
+ <text>{` ${i + 1}. ${label}${extTag}`}</text>
41
+ </box>
42
+ );
43
+ })}
44
+ </>
45
+ );
46
+ }
47
+
48
+ export function InboxView({ messages, count, processedMessages, deadLetterMessages, loading }: InboxViewProps): React.ReactNode {
49
+ if (loading) {
50
+ return <LoadingIndicator message="Loading messages..." />;
51
+ }
52
+
53
+ const totalAll = count + processedMessages.length + deadLetterMessages.length;
54
+
55
+ return (
56
+ <box height="100%" width="100%" flexDirection="column">
57
+ <scrollbox flexGrow={1} width="100%">
58
+ {/* Inbox section */}
59
+ <box height={1} width="100%">
60
+ <text>
61
+ <span style={textStyle({ fg: statusColor.info, bold: true })}>{`Inbox (${count})`}</span>
62
+ <span style={textStyle({ dim: true })}>{" — pending messages"}</span>
63
+ </text>
64
+ </box>
65
+ <MessageList messages={messages} emptyText="No pending messages" />
66
+
67
+ {/* Processed section */}
68
+ <text>{""}</text>
69
+ <box height={1} width="100%">
70
+ <text>
71
+ <span style={textStyle({ fg: statusColor.healthy, bold: true })}>{`Processed (${processedMessages.length})`}</span>
72
+ <span style={textStyle({ dim: true })}>{" — consumed by agent"}</span>
73
+ </text>
74
+ </box>
75
+ <MessageList messages={processedMessages} emptyText="No processed messages" />
76
+
77
+ {/* Dead letter section */}
78
+ <text>{""}</text>
79
+ <box height={1} width="100%">
80
+ <text>
81
+ <span style={textStyle({ fg: statusColor.error, bold: true })}>{`Dead Letter (${deadLetterMessages.length})`}</span>
82
+ <span style={textStyle({ dim: true })}>{" — expired or failed"}</span>
83
+ </text>
84
+ </box>
85
+ <MessageList messages={deadLetterMessages} emptyText="No dead letter messages" />
86
+
87
+ {totalAll === 0 && (
88
+ <>
89
+ <text>{""}</text>
90
+ <text style={textStyle({ dim: true })}>{" No IPC messages. Send messages with POST /api/v2/ipc/send"}</text>
91
+ </>
92
+ )}
93
+ </scrollbox>
94
+ </box>
95
+ );
96
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Trajectories tab: simple list view of agent trajectories.
3
+ *
4
+ * Displays trace_id, agent_id, status, started_at, and step_count.
5
+ */
6
+
7
+ import React from "react";
8
+ import type { TrajectoryItem } from "../../stores/agents-store.js";
9
+ import { StyledText } from "../../shared/components/styled-text.js";
10
+ import { LoadingIndicator } from "../../shared/components/loading-indicator.js";
11
+
12
+ interface TrajectoriesTabProps {
13
+ readonly trajectories: readonly TrajectoryItem[];
14
+ readonly loading: boolean;
15
+ }
16
+
17
+ export function TrajectoriesTab({ trajectories, loading }: TrajectoriesTabProps): React.ReactNode {
18
+ if (loading) return <LoadingIndicator message="Loading trajectories..." />;
19
+ if (trajectories.length === 0) return <text>No trajectories found.</text>;
20
+
21
+ return (
22
+ <scrollbox height="100%" width="100%">
23
+ {/* Header */}
24
+ <box height={1} width="100%">
25
+ <text>{" TRACE_ID AGENT_ID STATUS STARTED STEPS"}</text>
26
+ </box>
27
+
28
+ {trajectories.map((traj) => {
29
+ const traceShort = traj.trace_id.slice(0, 16) + "...";
30
+ const agentShort = traj.agent_id.slice(0, 14);
31
+ const started = traj.started_at ? traj.started_at.slice(0, 19) : "n/a";
32
+ return (
33
+ <box key={traj.trace_id} height={1} width="100%">
34
+ <StyledText>{` ${traceShort} ${agentShort.padEnd(16)} ${traj.status.padEnd(10)} ${started} ${traj.step_count}`}</StyledText>
35
+ </box>
36
+ );
37
+ })}
38
+ </scrollbox>
39
+ );
40
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * API Console panel: endpoint list + request builder + response viewer.
3
+ *
4
+ * Press ":" to enter command input mode.
5
+ * Supports CLI-like syntax (ls, cat, stat, rm, mkdir) and raw HTTP methods.
6
+ * Arrow up/down navigates command history in input mode.
7
+ */
8
+
9
+ import React, { useEffect, useCallback } from "react";
10
+ import { useApi } from "../../shared/hooks/use-api.js";
11
+ import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
12
+ import { listNavigationBindings } from "../../shared/hooks/use-list-navigation.js";
13
+ import { useTextInput } from "../../shared/hooks/use-text-input.js";
14
+ import { useApiConsoleStore } from "../../stores/api-console-store.js";
15
+ import { EndpointList } from "./endpoint-list.js";
16
+ import { RequestBuilder } from "./request-builder.js";
17
+ import { ResponseViewer } from "./response-viewer.js";
18
+ import { CommandOutput } from "../../shared/components/command-output.js";
19
+ import { useCommandRunnerStore, executeLocalCommand } from "../../services/command-runner.js";
20
+ import { useUiStore } from "../../stores/ui-store.js";
21
+ import { focusColor } from "../../shared/theme.js";
22
+ import { Tooltip } from "../../shared/components/tooltip.js";
23
+
24
+ export default function ApiConsolePanel(): React.ReactNode {
25
+ const client = useApi();
26
+ // Reactive subscription to command runner status (Codex finding 2)
27
+ const commandRunnerStatus = useCommandRunnerStore((s) => s.status);
28
+ const endpoints = useApiConsoleStore((s) => s.endpoints);
29
+ const filteredEndpoints = useApiConsoleStore((s) => s.filteredEndpoints);
30
+ const selectedEndpoint = useApiConsoleStore((s) => s.selectedEndpoint);
31
+ const selectEndpoint = useApiConsoleStore((s) => s.selectEndpoint);
32
+ const executeRequest = useApiConsoleStore((s) => s.executeRequest);
33
+ const executeCommand = useApiConsoleStore((s) => s.executeCommand);
34
+ const fetchOpenApiSpec = useApiConsoleStore((s) => s.fetchOpenApiSpec);
35
+ const commandHistory = useApiConsoleStore((s) => s.commandHistory);
36
+ const commandInputMode = useApiConsoleStore((s) => s.commandInputMode);
37
+ const commandInputBuffer = useApiConsoleStore((s) => s.commandInputBuffer);
38
+ const setCommandInputMode = useApiConsoleStore((s) => s.setCommandInputMode);
39
+ const setCommandInputBuffer = useApiConsoleStore((s) => s.setCommandInputBuffer);
40
+ const navigateHistory = useApiConsoleStore((s) => s.navigateHistory);
41
+
42
+ const setSearchQuery = useApiConsoleStore((s) => s.setSearchQuery);
43
+
44
+ // Focus pane (ui-store)
45
+ const uiFocusPane = useUiStore((s) => s.getFocusPane("console"));
46
+ const toggleFocus = useUiStore((s) => s.toggleFocusPane);
47
+ const overlayActive = useUiStore((s) => s.overlayActive);
48
+
49
+ // Auto-load endpoints from OpenAPI spec on mount
50
+ useEffect(() => {
51
+ if (client && endpoints.length === 0) {
52
+ fetchOpenApiSpec(client);
53
+ }
54
+ }, [client, endpoints.length, fetchOpenApiSpec]);
55
+
56
+ // Find current selection index
57
+ const selectedIdx = selectedEndpoint
58
+ ? filteredEndpoints.findIndex((ep) => ep.path === selectedEndpoint.path && ep.method === selectedEndpoint.method)
59
+ : -1;
60
+
61
+ // Shared list navigation (j/k/up/down/g/G)
62
+ const listNav = listNavigationBindings({
63
+ getIndex: () => selectedIdx,
64
+ setIndex: (i) => {
65
+ const ep = filteredEndpoints[i];
66
+ if (ep) selectEndpoint(ep);
67
+ },
68
+ getLength: () => filteredEndpoints.length,
69
+ });
70
+
71
+ // Endpoint filter input (replaces local endpointFilterMode/endpointFilter state)
72
+ const endpointFilter = useTextInput({
73
+ onSubmit: (val) => setSearchQuery(val.trim()),
74
+ onCancel: () => { setSearchQuery(""); },
75
+ });
76
+
77
+ // Handle printable characters in command input mode
78
+ const handleUnhandledKey = useCallback(
79
+ (keyName: string) => {
80
+ if (!commandInputMode) return;
81
+ if (keyName.length === 1) {
82
+ setCommandInputBuffer(commandInputBuffer + keyName);
83
+ } else if (keyName === "space") {
84
+ setCommandInputBuffer(commandInputBuffer + " ");
85
+ }
86
+ },
87
+ [commandInputMode, commandInputBuffer, setCommandInputBuffer],
88
+ );
89
+
90
+ useKeyboard(
91
+ overlayActive
92
+ ? {}
93
+ : endpointFilter.active
94
+ ? endpointFilter.inputBindings
95
+ : commandInputMode
96
+ ? {
97
+ return: () => {
98
+ setCommandInputMode(false);
99
+ if (client && commandInputBuffer.trim()) {
100
+ executeCommand(commandInputBuffer, client);
101
+ }
102
+ },
103
+ escape: () => {
104
+ setCommandInputMode(false);
105
+ },
106
+ backspace: () => {
107
+ setCommandInputBuffer(commandInputBuffer.slice(0, -1));
108
+ },
109
+ up: () => navigateHistory("up"),
110
+ down: () => navigateHistory("down"),
111
+ }
112
+ : {
113
+ ...listNav,
114
+ return: () => {
115
+ if (client) executeRequest(client);
116
+ },
117
+ "/": () => {
118
+ endpointFilter.activate(endpointFilter.buffer);
119
+ },
120
+ ":": () => {
121
+ setCommandInputMode(true);
122
+ },
123
+ // Issue #3078: Shift+B to run nexus build from Console
124
+ "shift+b": () => {
125
+ useCommandRunnerStore.getState().reset();
126
+ executeLocalCommand("build", []);
127
+ },
128
+ tab: () => toggleFocus("console"),
129
+ },
130
+ overlayActive
131
+ ? undefined
132
+ : endpointFilter.active
133
+ ? endpointFilter.onUnhandled
134
+ : handleUnhandledKey,
135
+ );
136
+
137
+ return (
138
+ <box height="100%" width="100%" flexDirection="column">
139
+ <Tooltip tooltipKey="api-console-panel" message="Tip: Press ? for keybinding help" />
140
+ <box flexGrow={1} width="100%" flexDirection="row">
141
+ {/* Left: Endpoint list (30%) */}
142
+ <box width="30%" height="100%" borderStyle="single" borderColor={uiFocusPane === "left" ? focusColor.activeBorder : focusColor.inactiveBorder} flexDirection="column">
143
+ <box height={1} width="100%">
144
+ <text>{`─── Endpoints ─── (history: ${commandHistory.length})`}</text>
145
+ </box>
146
+ <box height={1} width="100%">
147
+ <text>
148
+ {endpointFilter.active
149
+ ? `Filter: ${endpointFilter.buffer}\u2588`
150
+ : endpointFilter.buffer
151
+ ? `Filter: ${endpointFilter.buffer} (Esc to clear)`
152
+ : "/:filter endpoints"}
153
+ </text>
154
+ </box>
155
+ <EndpointList />
156
+ </box>
157
+
158
+ {/* Right: Request + Response (70%) */}
159
+ <box width="70%" height="100%" borderStyle="single" borderColor={uiFocusPane === "right" ? focusColor.activeBorder : focusColor.inactiveBorder} flexDirection="column">
160
+ {/* Command input bar */}
161
+ <box height={1} width="100%">
162
+ <text>
163
+ {commandInputMode
164
+ ? `> ${commandInputBuffer}█`
165
+ : `Press ":" for command input | "!" prefix for local commands | Shift+B:build | history: ${commandHistory.length}`}
166
+ </text>
167
+ </box>
168
+
169
+ {/* Local command output (when running via !command or Shift+B) */}
170
+ {commandRunnerStatus !== "idle" && (
171
+ <box borderStyle="single" height={8} width="100%">
172
+ <CommandOutput />
173
+ </box>
174
+ )}
175
+
176
+ {/* Request builder (top 40%) */}
177
+ <box flexGrow={4} borderStyle="single">
178
+ <RequestBuilder />
179
+ </box>
180
+
181
+ {/* Response viewer (bottom 60%) */}
182
+ <box flexGrow={6} borderStyle="single">
183
+ <ResponseViewer />
184
+ </box>
185
+ </box>
186
+ </box>
187
+ </box>
188
+ );
189
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Code generation panel showing request as curl/fetch/python snippets.
3
+ */
4
+
5
+ import React, { useState } from "react";
6
+ import { useApiConsoleStore } from "../../stores/api-console-store.js";
7
+ import { useGlobalStore } from "../../stores/global-store.js";
8
+ import { generateCode, type CodegenLanguage } from "./codegen.js";
9
+ import { defaultSyntaxStyle } from "../../shared/syntax-style.js";
10
+
11
+ const LANGUAGES: readonly CodegenLanguage[] = ["curl", "fetch", "python"];
12
+
13
+ export function CodegenViewer(): React.ReactNode {
14
+ const [lang, setLang] = useState<CodegenLanguage>("curl");
15
+ const request = useApiConsoleStore((s) => s.request);
16
+ const baseUrl = useGlobalStore((s) => s.config.baseUrl ?? "http://localhost:2026");
17
+
18
+ const code = generateCode(lang, request, baseUrl);
19
+
20
+ return (
21
+ <box height="100%" width="100%" flexDirection="column">
22
+ {/* Language selector */}
23
+ <box height={1} width="100%" flexDirection="row">
24
+ {LANGUAGES.map((l) => {
25
+ const active = l === lang ? "▸ " : " ";
26
+ return <text key={l}>{`${active}${l}`}</text>;
27
+ })}
28
+ </box>
29
+
30
+ {/* Code output */}
31
+ <scrollbox flexGrow={1} width="100%">
32
+ <code content={code} filetype={lang === "curl" ? "bash" : lang === "fetch" ? "javascript" : "python"} syntaxStyle={defaultSyntaxStyle} />
33
+ </scrollbox>
34
+ </box>
35
+ );
36
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Pure functions for generating code snippets from a RequestState.
3
+ */
4
+
5
+ import type { RequestState } from "../../stores/api-console-store.js";
6
+
7
+ export type CodegenLanguage = "curl" | "fetch" | "python";
8
+
9
+ export function generateCode(
10
+ lang: CodegenLanguage,
11
+ request: RequestState,
12
+ baseUrl: string,
13
+ ): string {
14
+ switch (lang) {
15
+ case "curl":
16
+ return generateCurl(request, baseUrl);
17
+ case "fetch":
18
+ return generateFetch(request, baseUrl);
19
+ case "python":
20
+ return generatePython(request, baseUrl);
21
+ }
22
+ }
23
+
24
+ export function generateCurl(request: RequestState, baseUrl: string): string {
25
+ const url = buildUrl(request, baseUrl);
26
+ const parts = [`curl -X ${request.method} '${url}'`];
27
+
28
+ for (const [key, value] of Object.entries(request.headers)) {
29
+ parts.push(` -H '${key}: ${value}'`);
30
+ }
31
+
32
+ if (request.body && request.method !== "GET" && request.method !== "HEAD") {
33
+ parts.push(` -H 'Content-Type: application/json'`);
34
+ parts.push(` -d '${request.body}'`);
35
+ }
36
+
37
+ return parts.join(" \\\n");
38
+ }
39
+
40
+ export function generateFetch(request: RequestState, baseUrl: string): string {
41
+ const url = buildUrl(request, baseUrl);
42
+ const lines: string[] = [];
43
+
44
+ lines.push(`const response = await fetch('${url}', {`);
45
+ lines.push(` method: '${request.method}',`);
46
+
47
+ const headers = { ...request.headers };
48
+ if (request.body && request.method !== "GET" && request.method !== "HEAD") {
49
+ headers["Content-Type"] = "application/json";
50
+ }
51
+
52
+ if (Object.keys(headers).length > 0) {
53
+ lines.push(" headers: {");
54
+ for (const [key, value] of Object.entries(headers)) {
55
+ lines.push(` '${key}': '${value}',`);
56
+ }
57
+ lines.push(" },");
58
+ }
59
+
60
+ if (request.body && request.method !== "GET" && request.method !== "HEAD") {
61
+ lines.push(` body: JSON.stringify(${request.body}),`);
62
+ }
63
+
64
+ lines.push("});");
65
+ lines.push("");
66
+ lines.push("const data = await response.json();");
67
+ lines.push("console.log(data);");
68
+
69
+ return lines.join("\n");
70
+ }
71
+
72
+ export function generatePython(request: RequestState, baseUrl: string): string {
73
+ const url = buildUrl(request, baseUrl);
74
+ const lines: string[] = [];
75
+
76
+ lines.push("import httpx");
77
+ lines.push("");
78
+
79
+ if (Object.keys(request.headers).length > 0) {
80
+ lines.push("headers = {");
81
+ for (const [key, value] of Object.entries(request.headers)) {
82
+ lines.push(` "${key}": "${value}",`);
83
+ }
84
+ lines.push("}");
85
+ lines.push("");
86
+ }
87
+
88
+ const method = request.method.toLowerCase();
89
+ const hasBody = request.body && request.method !== "GET" && request.method !== "HEAD";
90
+ const headerArg = Object.keys(request.headers).length > 0 ? ", headers=headers" : "";
91
+ const bodyArg = hasBody ? `, json=${request.body}` : "";
92
+
93
+ lines.push(`response = httpx.${method}("${url}"${headerArg}${bodyArg})`);
94
+ lines.push("print(response.json())");
95
+
96
+ return lines.join("\n");
97
+ }
98
+
99
+ function buildUrl(request: RequestState, baseUrl: string): string {
100
+ let path = request.path;
101
+ for (const [key, value] of Object.entries(request.pathParams)) {
102
+ path = path.replace(`{${key}}`, encodeURIComponent(value));
103
+ }
104
+
105
+ const queryEntries = Object.entries(request.queryParams).filter(([, v]) => v !== "");
106
+ if (queryEntries.length > 0) {
107
+ const params = new URLSearchParams(queryEntries);
108
+ path += `?${params.toString()}`;
109
+ }
110
+
111
+ return `${baseUrl}${path}`;
112
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Searchable, filterable list of all API endpoints.
3
+ */
4
+
5
+ import React from "react";
6
+ import { useApiConsoleStore, type EndpointInfo } from "../../stores/api-console-store.js";
7
+ import { EmptyState } from "../../shared/components/empty-state.js";
8
+
9
+ const METHOD_BADGE: Record<string, string> = {
10
+ GET: "GET ",
11
+ POST: "POST ",
12
+ PUT: "PUT ",
13
+ DELETE: "DELETE",
14
+ PATCH: "PATCH ",
15
+ HEAD: "HEAD ",
16
+ OPTIONS: "OPT ",
17
+ };
18
+
19
+ export function EndpointList(): React.ReactNode {
20
+ const endpoints = useApiConsoleStore((s) => s.filteredEndpoints);
21
+ const selectedEndpoint = useApiConsoleStore((s) => s.selectedEndpoint);
22
+ const searchQuery = useApiConsoleStore((s) => s.searchQuery);
23
+
24
+ if (endpoints.length === 0) {
25
+ return searchQuery
26
+ ? <EmptyState message="No endpoints match your search." />
27
+ : <EmptyState message="No endpoints available." hint="Check server connection." />;
28
+ }
29
+
30
+ return (
31
+ <scrollbox height="100%" width="100%">
32
+ {endpoints.map((ep) => {
33
+ const isSelected = selectedEndpoint?.path === ep.path && selectedEndpoint?.method === ep.method;
34
+ return (
35
+ <EndpointRow key={`${ep.method}:${ep.path}`} endpoint={ep} selected={isSelected} />
36
+ );
37
+ })}
38
+ </scrollbox>
39
+ );
40
+ }
41
+
42
+ function EndpointRow({
43
+ endpoint,
44
+ selected,
45
+ }: {
46
+ endpoint: EndpointInfo;
47
+ selected: boolean;
48
+ }): React.ReactNode {
49
+ const prefix = selected ? "▸ " : " ";
50
+ const badge = METHOD_BADGE[endpoint.method] ?? endpoint.method;
51
+
52
+ return (
53
+ <box height={1} width="100%" flexDirection="row">
54
+ <text>{`${prefix}${badge} ${endpoint.path}`}</text>
55
+ </box>
56
+ );
57
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Dynamic form for building API requests.
3
+ */
4
+
5
+ import React from "react";
6
+ import { useApiConsoleStore } from "../../stores/api-console-store.js";
7
+ import { Spinner } from "../../shared/components/spinner.js";
8
+
9
+ export function RequestBuilder(): React.ReactNode {
10
+ const request = useApiConsoleStore((s) => s.request);
11
+ const selectedEndpoint = useApiConsoleStore((s) => s.selectedEndpoint);
12
+ const isLoading = useApiConsoleStore((s) => s.isLoading);
13
+
14
+ if (!selectedEndpoint) {
15
+ return (
16
+ <box height="100%" width="100%" justifyContent="center" alignItems="center">
17
+ <text>Select an endpoint from the list</text>
18
+ </box>
19
+ );
20
+ }
21
+
22
+ // Extract path parameters (e.g., {id}, {name})
23
+ const pathParamNames = [...request.path.matchAll(/\{(\w+)\}/g)].map((m) => m[1]!);
24
+
25
+ return (
26
+ <box height="100%" width="100%" flexDirection="column">
27
+ {/* Method + Path */}
28
+ <box height={1} width="100%">
29
+ <text>{`${request.method} ${request.path}`}</text>
30
+ </box>
31
+
32
+ {/* Summary */}
33
+ {selectedEndpoint.summary && (
34
+ <box height={1} width="100%">
35
+ <text>{selectedEndpoint.summary}</text>
36
+ </box>
37
+ )}
38
+
39
+ {/* Path Parameters */}
40
+ {pathParamNames.length > 0 && (
41
+ <box flexDirection="column">
42
+ <text>{"─── Path Parameters ───"}</text>
43
+ {pathParamNames.map((name) => (
44
+ <box key={name} height={1} width="100%">
45
+ <text>{` ${name}: ${request.pathParams[name] ?? ""}`}</text>
46
+ </box>
47
+ ))}
48
+ </box>
49
+ )}
50
+
51
+ {/* Request Body */}
52
+ {request.method !== "GET" && request.method !== "HEAD" && (
53
+ <box flexDirection="column" flexGrow={1}>
54
+ <text>{"─── Request Body (JSON) ───"}</text>
55
+ <text>{request.body || "{}"}</text>
56
+ </box>
57
+ )}
58
+
59
+ {/* Send button area */}
60
+ <box height={1} width="100%">
61
+ {isLoading ? (
62
+ <Spinner label="Sending..." />
63
+ ) : (
64
+ <text>{"Enter to send request"}</text>
65
+ )}
66
+ </box>
67
+ </box>
68
+ );
69
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Displays the API response with syntax highlighting and status info.
3
+ */
4
+
5
+ import React from "react";
6
+ import { useApiConsoleStore } from "../../stores/api-console-store.js";
7
+ import { httpStatusColor } from "../../shared/theme.js";
8
+ import { StyledText } from "../../shared/components/styled-text.js";
9
+ import { textStyle } from "../../shared/text-style.js";
10
+ import { defaultSyntaxStyle } from "../../shared/syntax-style.js";
11
+
12
+ export function ResponseViewer(): React.ReactNode {
13
+ const response = useApiConsoleStore((s) => s.response);
14
+
15
+ if (!response) {
16
+ return (
17
+ <box height="100%" width="100%" justifyContent="center" alignItems="center">
18
+ <text>No response yet</text>
19
+ </box>
20
+ );
21
+ }
22
+
23
+ if (response.error) {
24
+ return (
25
+ <box height="100%" width="100%" flexDirection="column">
26
+ <text>{`Error: ${response.error}`}</text>
27
+ <text>{`Time: ${response.timeMs.toFixed(0)}ms`}</text>
28
+ </box>
29
+ );
30
+ }
31
+
32
+ const statusCategory = Math.floor(response.status / 100);
33
+ const statusPrefix = statusCategory === 2 ? "✓" : statusCategory === 4 ? "✗" : "!";
34
+
35
+ return (
36
+ <box height="100%" width="100%" flexDirection="column">
37
+ {/* Status line */}
38
+ <box height={1} width="100%">
39
+ <text>{`${statusPrefix} `}</text>
40
+ <text style={textStyle({ fg: httpStatusColor(response.status) })}>{`${response.status}`}</text>
41
+ <text>{` ${response.statusText} — ${response.timeMs.toFixed(0)}ms`}</text>
42
+ </box>
43
+
44
+ {/* Response body with syntax highlighting (ANSI-aware) */}
45
+ <scrollbox flexGrow={1} width="100%">
46
+ {response.body.includes("\x1b[") ? (
47
+ <StyledText>{response.body}</StyledText>
48
+ ) : (
49
+ <code content={response.body} filetype="json" syntaxStyle={defaultSyntaxStyle} />
50
+ )}
51
+ </scrollbox>
52
+ </box>
53
+ );
54
+ }