@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,514 @@
1
+ /**
2
+ * Write tab: template-based YAML write composition with validation.
3
+ *
4
+ * Workflow: select mount → select operation → edit template → submit.
5
+ * Template is generated from the operation schema.
6
+ *
7
+ * Supports inline editing of field values (Enter to edit a line, type to
8
+ * replace, Enter to confirm, Escape to cancel). Commented lines can be
9
+ * uncommented with '#' to enable optional fields.
10
+ *
11
+ * Error display parses backend ValidationError format to show field-level
12
+ * errors, skill doc references, and fix examples.
13
+ */
14
+
15
+ import React, { useState, useEffect, useCallback } from "react";
16
+ import type { FetchClient } from "@nexus-ai-fs/api-client";
17
+ import { useConnectorsStore } from "../../stores/connectors-store.js";
18
+ import { useConfirmStore } from "../../shared/hooks/use-confirm.js";
19
+ import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
20
+ import { useSwr } from "../../shared/hooks/use-swr.js";
21
+ import { listNavigationBindings } from "../../shared/hooks/use-list-navigation.js";
22
+ import { LoadingIndicator } from "../../shared/components/loading-indicator.js";
23
+ import { generateWriteTemplate } from "./template-generator.js";
24
+ import { parseWriteError } from "./error-parser.js";
25
+ import { statusColor } from "../../shared/theme.js";
26
+ import type { SchemaDoc } from "../../stores/connectors-store.js";
27
+
28
+ interface WriteTabProps {
29
+ readonly client: FetchClient;
30
+ readonly overlayActive: boolean;
31
+ }
32
+
33
+ type WriteMode = "select-mount" | "select-op" | "edit" | "result";
34
+
35
+ export function WriteTab({ client, overlayActive }: WriteTabProps): React.ReactNode {
36
+ const mounts = useConnectorsStore((s) => s.mounts);
37
+ const selectedMountIndex = useConnectorsStore((s) => s.selectedWriteMountIndex);
38
+ const selectedOpIndex = useConnectorsStore((s) => s.selectedOperationIndex);
39
+ const writeTemplate = useConnectorsStore((s) => s.writeTemplate);
40
+ const writeResult = useConnectorsStore((s) => s.writeResult);
41
+ const writeLoading = useConnectorsStore((s) => s.writeLoading);
42
+
43
+ const setSelectedMountIndex = useConnectorsStore((s) => s.setSelectedWriteMountIndex);
44
+ const setSelectedOpIndex = useConnectorsStore((s) => s.setSelectedOperationIndex);
45
+ const setWriteTemplate = useConnectorsStore((s) => s.setWriteTemplate);
46
+ const submitWrite = useConnectorsStore((s) => s.submitWrite);
47
+ const clearWriteResult = useConnectorsStore((s) => s.clearWriteResult);
48
+ const fetchMounts = useConnectorsStore((s) => s.fetchMounts);
49
+
50
+ const confirm = useConfirmStore((s) => s.confirm);
51
+
52
+ const [mode, setMode] = useState<WriteMode>("select-mount");
53
+ const [editLine, setEditLine] = useState(0);
54
+ const [lineEditMode, setLineEditMode] = useState(false);
55
+ const [lineEditBuffer, setLineEditBuffer] = useState("");
56
+
57
+ const selectedMount = mounts[selectedMountIndex];
58
+ const operations = selectedMount?.operations ?? [];
59
+ const selectedOp = operations[selectedOpIndex];
60
+
61
+ // Auto-fetch mounts if empty
62
+ useEffect(() => {
63
+ if (mounts.length === 0) {
64
+ fetchMounts(client);
65
+ }
66
+ }, [client, mounts.length, fetchMounts]);
67
+
68
+ // Fetch schema and generate template when operation is selected
69
+ const { data: schemaData } = useSwr<SchemaDoc>(
70
+ selectedMount && selectedOp
71
+ ? `schema-${selectedMount.mount_point}-${selectedOp}`
72
+ : "__disabled__",
73
+ async (signal) => {
74
+ if (!selectedMount || !selectedOp) throw new Error("No selection");
75
+ return client.get<SchemaDoc>(
76
+ `/api/v2/connectors/schema/${selectedMount.mount_point.replace(/^\//, "")}/${selectedOp}`,
77
+ { signal },
78
+ );
79
+ },
80
+ { ttlMs: 300_000, enabled: !!selectedMount && !!selectedOp },
81
+ );
82
+
83
+ // Generate template from schema
84
+ useEffect(() => {
85
+ if (schemaData?.content && selectedOp && mode === "edit") {
86
+ const template = generateWriteTemplate(selectedOp, schemaData.content);
87
+ setWriteTemplate(template);
88
+ setEditLine(0);
89
+ }
90
+ }, [schemaData?.content, selectedOp, mode, setWriteTemplate]);
91
+
92
+ const templateLines = writeTemplate.split("\n");
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Inline line editing helpers
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /** Enter edit mode for the current line's value (after the colon). */
99
+ const startLineEdit = useCallback(() => {
100
+ const line = templateLines[editLine];
101
+ if (!line) return;
102
+ // Extract the value portion after "key:" (skip comments-only and header lines)
103
+ const stripped = line.replace(/^#\s*/, "");
104
+ const colonIdx = stripped.indexOf(":");
105
+ if (colonIdx < 0) return; // not a key: value line
106
+ // Pre-fill buffer with current value (trimmed, without inline comment)
107
+ const rawValue = stripped.substring(colonIdx + 1).split("#")[0]?.trim() ?? "";
108
+ // Strip surrounding quotes for editing comfort
109
+ const unquoted = rawValue.replace(/^["']|["']$/g, "");
110
+ setLineEditBuffer(unquoted);
111
+ setLineEditMode(true);
112
+ }, [templateLines, editLine]);
113
+
114
+ /** Commit the edited value back into the template. */
115
+ const commitLineEdit = useCallback(() => {
116
+ const line = templateLines[editLine];
117
+ if (!line) { setLineEditMode(false); return; }
118
+ const stripped = line.replace(/^#\s*/, "");
119
+ const colonIdx = stripped.indexOf(":");
120
+ if (colonIdx < 0) { setLineEditMode(false); return; }
121
+ const key = stripped.substring(0, colonIdx);
122
+ // Preserve inline comment
123
+ const commentMatch = stripped.match(/#\s*.+$/);
124
+ const comment = commentMatch ? ` ${commentMatch[0]}` : "";
125
+ // Build new line (uncommented — editing implies enabling)
126
+ const value = lineEditBuffer.includes(" ") || lineEditBuffer === ""
127
+ ? `"${lineEditBuffer}"`
128
+ : lineEditBuffer;
129
+ const newLine = `${key}: ${value}${comment}`;
130
+ const newLines = [...templateLines];
131
+ newLines[editLine] = newLine;
132
+ setWriteTemplate(newLines.join("\n"));
133
+ setLineEditMode(false);
134
+ }, [templateLines, editLine, lineEditBuffer, setWriteTemplate]);
135
+
136
+ /** Toggle comment on/off for the current line. */
137
+ const toggleComment = useCallback(() => {
138
+ const line = templateLines[editLine];
139
+ if (!line) return;
140
+ const newLines = [...templateLines];
141
+ if (line.startsWith("# ")) {
142
+ // Uncomment
143
+ newLines[editLine] = line.substring(2);
144
+ } else if (!line.startsWith("#")) {
145
+ // Comment out
146
+ newLines[editLine] = `# ${line}`;
147
+ }
148
+ setWriteTemplate(newLines.join("\n"));
149
+ }, [templateLines, editLine, setWriteTemplate]);
150
+
151
+ const handleSubmit = useCallback(async () => {
152
+ if (!selectedMount || !writeTemplate.trim()) return;
153
+ const ok = await confirm(
154
+ "Submit write operation?",
155
+ `Write to ${selectedMount.mount_point} (${selectedOp}). This may have side effects.`,
156
+ );
157
+ if (!ok) return;
158
+ submitWrite(selectedMount.mount_point, writeTemplate, client);
159
+ setMode("result");
160
+ }, [selectedMount, selectedOp, writeTemplate, submitWrite, client, confirm]);
161
+
162
+ // Parse structured error from write result
163
+ const parsedError = writeResult?.error ? parseWriteError(writeResult.error) : null;
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Keyboard bindings
167
+ // ---------------------------------------------------------------------------
168
+
169
+ const mountNav = listNavigationBindings({
170
+ getIndex: () => selectedMountIndex,
171
+ setIndex: setSelectedMountIndex,
172
+ getLength: () => mounts.length,
173
+ onSelect: () => {
174
+ if (operations.length > 0) {
175
+ setMode("select-op");
176
+ setSelectedOpIndex(0);
177
+ }
178
+ },
179
+ });
180
+
181
+ const opNav = listNavigationBindings({
182
+ getIndex: () => selectedOpIndex,
183
+ setIndex: setSelectedOpIndex,
184
+ getLength: () => operations.length,
185
+ onSelect: () => setMode("edit"),
186
+ });
187
+
188
+ useKeyboard(
189
+ overlayActive
190
+ ? {}
191
+ : mode === "select-mount"
192
+ ? {
193
+ ...mountNav,
194
+ r: () => fetchMounts(client),
195
+ }
196
+ : mode === "select-op"
197
+ ? {
198
+ ...opNav,
199
+ escape: () => setMode("select-mount"),
200
+ }
201
+ : mode === "edit" && lineEditMode
202
+ ? {
203
+ // Line editing mode — capture typed characters
204
+ return: commitLineEdit,
205
+ escape: () => { setLineEditMode(false); setLineEditBuffer(""); },
206
+ backspace: () => { setLineEditBuffer((b) => b.slice(0, -1)); },
207
+ }
208
+ : mode === "edit"
209
+ ? {
210
+ j: () => setEditLine(Math.min(editLine + 1, templateLines.length - 1)),
211
+ k: () => setEditLine(Math.max(editLine - 1, 0)),
212
+ down: () => setEditLine(Math.min(editLine + 1, templateLines.length - 1)),
213
+ up: () => setEditLine(Math.max(editLine - 1, 0)),
214
+ return: startLineEdit,
215
+ "ctrl+s": handleSubmit,
216
+ "#": toggleComment,
217
+ escape: () => setMode("select-op"),
218
+ }
219
+ : {
220
+ // result mode
221
+ escape: () => {
222
+ clearWriteResult();
223
+ setMode("select-op");
224
+ },
225
+ r: () => {
226
+ clearWriteResult();
227
+ setMode("edit");
228
+ },
229
+ e: () => {
230
+ // Jump back to edit with current template to fix errors
231
+ clearWriteResult();
232
+ setMode("edit");
233
+ },
234
+ },
235
+ // onUnhandled: capture typed characters in line edit mode
236
+ (!overlayActive && mode === "edit" && lineEditMode)
237
+ ? (keyName: string) => {
238
+ if (keyName === "space") {
239
+ setLineEditBuffer((b) => b + " ");
240
+ } else if (keyName.length === 1) {
241
+ setLineEditBuffer((b) => b + keyName);
242
+ }
243
+ }
244
+ : undefined,
245
+ );
246
+
247
+ return (
248
+ <box flexDirection="column" height="100%" width="100%">
249
+ {/* Breadcrumb */}
250
+ <box height={1} width="100%">
251
+ <text>
252
+ <span
253
+ foregroundColor={mode === "select-mount" ? statusColor.info : statusColor.dim}
254
+ bold={mode === "select-mount"}
255
+ >
256
+ Mount
257
+ </span>
258
+ <span foregroundColor={statusColor.dim}>{" → "}</span>
259
+ <span
260
+ foregroundColor={mode === "select-op" ? statusColor.info : statusColor.dim}
261
+ bold={mode === "select-op"}
262
+ >
263
+ Operation
264
+ </span>
265
+ <span foregroundColor={statusColor.dim}>{" → "}</span>
266
+ <span
267
+ foregroundColor={mode === "edit" ? statusColor.info : statusColor.dim}
268
+ bold={mode === "edit"}
269
+ >
270
+ Edit
271
+ </span>
272
+ <span foregroundColor={statusColor.dim}>{" → "}</span>
273
+ <span
274
+ foregroundColor={mode === "result" ? statusColor.info : statusColor.dim}
275
+ bold={mode === "result"}
276
+ >
277
+ Result
278
+ </span>
279
+ </text>
280
+ </box>
281
+
282
+ {/* Content area */}
283
+ <box flexGrow={1} borderStyle="single" marginTop={1} flexDirection="column">
284
+ {mode === "select-mount" && (
285
+ <box flexDirection="column" width="100%">
286
+ <box height={1} width="100%">
287
+ <text bold>Select a mount to write to:</text>
288
+ </box>
289
+ {mounts.length === 0 ? (
290
+ <box height={1} width="100%">
291
+ <text foregroundColor={statusColor.dim}>No mounts available.</text>
292
+ </box>
293
+ ) : (
294
+ mounts.map((m, i) => (
295
+ <box key={m.mount_point} height={1} width="100%">
296
+ <text>
297
+ <span foregroundColor={i === selectedMountIndex ? statusColor.info : undefined}>
298
+ {i === selectedMountIndex ? "▶ " : " "}
299
+ </span>
300
+ <span foregroundColor={statusColor.reference}>{m.mount_point}</span>
301
+ {m.readonly && (
302
+ <span foregroundColor={statusColor.error}>{" (read-only)"}</span>
303
+ )}
304
+ {m.operations.length > 0 && (
305
+ <span foregroundColor={statusColor.dim}>
306
+ {` ${m.operations.length} operations`}
307
+ </span>
308
+ )}
309
+ </text>
310
+ </box>
311
+ ))
312
+ )}
313
+ </box>
314
+ )}
315
+
316
+ {mode === "select-op" && (
317
+ <box flexDirection="column" width="100%">
318
+ <box height={1} width="100%">
319
+ <text bold>
320
+ {`Select operation for ${selectedMount?.mount_point ?? ""}:`}
321
+ </text>
322
+ </box>
323
+ {operations.length === 0 ? (
324
+ <box height={1} width="100%">
325
+ <text foregroundColor={statusColor.dim}>No write operations available for this mount.</text>
326
+ </box>
327
+ ) : (
328
+ operations.map((op, i) => (
329
+ <box key={op} height={1} width="100%">
330
+ <text>
331
+ <span foregroundColor={i === selectedOpIndex ? statusColor.info : undefined}>
332
+ {i === selectedOpIndex ? "▶ " : " "}
333
+ </span>
334
+ <span>{op}</span>
335
+ </text>
336
+ </box>
337
+ ))
338
+ )}
339
+ </box>
340
+ )}
341
+
342
+ {mode === "edit" && (
343
+ <box flexDirection="column" width="100%">
344
+ <box height={1} width="100%">
345
+ <text bold>{`Editing: ${selectedOp} → ${selectedMount?.mount_point}`}</text>
346
+ </box>
347
+ {writeLoading ? (
348
+ <LoadingIndicator message="Submitting..." />
349
+ ) : (
350
+ templateLines.map((line, i) => {
351
+ const isActive = i === editLine;
352
+ const isEditing = isActive && lineEditMode;
353
+
354
+ return (
355
+ <box key={i} height={1} width="100%">
356
+ <text>
357
+ <span foregroundColor={statusColor.dim}>
358
+ {String(i + 1).padStart(3, " ")}
359
+ </span>
360
+ <span foregroundColor={isActive ? statusColor.info : undefined}>
361
+ {isActive ? " ▶ " : " "}
362
+ </span>
363
+ {isEditing ? (
364
+ // Show the key + editable value with cursor
365
+ (() => {
366
+ const stripped = line.replace(/^#\s*/, "");
367
+ const colonIdx = stripped.indexOf(":");
368
+ const key = colonIdx >= 0 ? stripped.substring(0, colonIdx) : line;
369
+ return (
370
+ <>
371
+ <span foregroundColor={statusColor.info}>{`${key}: `}</span>
372
+ <span foregroundColor={statusColor.healthy} bold>
373
+ {lineEditBuffer}
374
+ </span>
375
+ <span foregroundColor={statusColor.info}>{"\u2588"}</span>
376
+ </>
377
+ );
378
+ })()
379
+ ) : (
380
+ <span
381
+ foregroundColor={
382
+ line.startsWith("#")
383
+ ? statusColor.dim
384
+ : undefined
385
+ }
386
+ >
387
+ {line}
388
+ </span>
389
+ )}
390
+ </text>
391
+ </box>
392
+ );
393
+ })
394
+ )}
395
+ </box>
396
+ )}
397
+
398
+ {mode === "result" && (
399
+ <box flexDirection="column" width="100%">
400
+ <box height={1} width="100%">
401
+ <text bold>Write Result</text>
402
+ </box>
403
+ {writeResult ? (
404
+ writeResult.success ? (
405
+ <>
406
+ <box height={1} width="100%">
407
+ <text foregroundColor={statusColor.healthy}>✓ Write successful!</text>
408
+ </box>
409
+ {writeResult.content_hash && (
410
+ <box height={1} width="100%">
411
+ <text foregroundColor={statusColor.dim}>{`Hash: ${writeResult.content_hash}`}</text>
412
+ </box>
413
+ )}
414
+ </>
415
+ ) : parsedError ? (
416
+ // Structured error display with self-correcting hints
417
+ <box flexDirection="column" width="100%">
418
+ {/* Error code + message */}
419
+ <box height={1} width="100%">
420
+ <text>
421
+ <span foregroundColor={statusColor.error} bold>
422
+ {parsedError.code ? `[${parsedError.code}] ` : "✕ "}
423
+ </span>
424
+ <span foregroundColor={statusColor.error}>{parsedError.message}</span>
425
+ </text>
426
+ </box>
427
+
428
+ {/* Field-level errors */}
429
+ {parsedError.fieldErrors.length > 0 && (
430
+ <>
431
+ <box height={1} width="100%" marginTop={1}>
432
+ <text bold foregroundColor={statusColor.warning}>Field errors:</text>
433
+ </box>
434
+ {parsedError.fieldErrors.map(({ field, error }) => (
435
+ <box key={field} height={1} width="100%">
436
+ <text>
437
+ <span foregroundColor={statusColor.warning}>{" - "}</span>
438
+ <span foregroundColor={statusColor.info} bold>{field}</span>
439
+ <span foregroundColor={statusColor.dim}>{": "}</span>
440
+ <span>{error}</span>
441
+ </text>
442
+ </box>
443
+ ))}
444
+ </>
445
+ )}
446
+
447
+ {/* Skill doc reference */}
448
+ {parsedError.skillRef && (
449
+ <box height={1} width="100%" marginTop={1}>
450
+ <text>
451
+ <span foregroundColor={statusColor.dim}>{"See: "}</span>
452
+ <span foregroundColor={statusColor.reference}>{parsedError.skillRef}</span>
453
+ </text>
454
+ </box>
455
+ )}
456
+
457
+ {/* Fix example */}
458
+ {parsedError.fixExample && (
459
+ <>
460
+ <box height={1} width="100%" marginTop={1}>
461
+ <text foregroundColor={statusColor.healthy} bold>Fix:</text>
462
+ </box>
463
+ {parsedError.fixExample.split("\n").map((fixLine, i) => (
464
+ <box key={i} height={1} width="100%">
465
+ <text foregroundColor={statusColor.dim}>{` ${fixLine}`}</text>
466
+ </box>
467
+ ))}
468
+ </>
469
+ )}
470
+
471
+ {/* Action hint */}
472
+ <box height={1} width="100%" marginTop={1}>
473
+ <text foregroundColor={statusColor.dim}>
474
+ Press e to edit template with corrections, Esc to go back
475
+ </text>
476
+ </box>
477
+ </box>
478
+ ) : (
479
+ // Unstructured error fallback
480
+ <>
481
+ <box height={1} width="100%">
482
+ <text foregroundColor={statusColor.error}>✕ Write failed</text>
483
+ </box>
484
+ {writeResult.error && (
485
+ <box height={1} width="100%">
486
+ <text foregroundColor={statusColor.error}>{writeResult.error}</text>
487
+ </box>
488
+ )}
489
+ </>
490
+ )
491
+ ) : (
492
+ <LoadingIndicator message="Submitting..." />
493
+ )}
494
+ </box>
495
+ )}
496
+ </box>
497
+
498
+ {/* Help bar */}
499
+ <box height={1} width="100%">
500
+ <text foregroundColor={statusColor.dim}>
501
+ {mode === "select-mount"
502
+ ? "j/k:navigate Enter:select r:refresh"
503
+ : mode === "select-op"
504
+ ? "j/k:navigate Enter:select Esc:back"
505
+ : mode === "edit" && lineEditMode
506
+ ? "type:edit value Enter:confirm Esc:cancel"
507
+ : mode === "edit"
508
+ ? "j/k:navigate Enter:edit line #:toggle comment Ctrl+S:submit Esc:back"
509
+ : "e:edit again Esc:back to operations"}
510
+ </text>
511
+ </box>
512
+ </box>
513
+ );
514
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Audit trail tab: transaction audit log with cursor-based pagination.
3
+ *
4
+ * Extracted from events-panel.tsx (Issue 2A).
5
+ */
6
+
7
+ import React, { useState, useEffect } from "react";
8
+ import { useInfraStore } from "../../stores/infra-store.js";
9
+ import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
10
+ import { listNavigationBindings } from "../../shared/hooks/use-list-navigation.js";
11
+ import { useApi } from "../../shared/hooks/use-api.js";
12
+ import { AuditTrail } from "./audit-trail.js";
13
+
14
+ interface AuditTabProps {
15
+ readonly tabBindings: Readonly<Record<string, () => void>>;
16
+ readonly overlayActive: boolean;
17
+ }
18
+
19
+ export function AuditTab({ tabBindings, overlayActive }: AuditTabProps): React.ReactNode {
20
+ const client = useApi();
21
+ const [selectedAuditIndex, setSelectedAuditIndex] = useState(0);
22
+
23
+ const auditTransactions = useInfraStore((s) => s.auditTransactions);
24
+ const auditLoading = useInfraStore((s) => s.auditLoading);
25
+ const auditHasMore = useInfraStore((s) => s.auditHasMore);
26
+ const auditNextCursor = useInfraStore((s) => s.auditNextCursor);
27
+ const fetchAuditTransactions = useInfraStore((s) => s.fetchAuditTransactions);
28
+
29
+ useEffect(() => {
30
+ if (client) void fetchAuditTransactions({}, client);
31
+ }, [client, fetchAuditTransactions]);
32
+
33
+ const listNav = listNavigationBindings({
34
+ getIndex: () => selectedAuditIndex,
35
+ setIndex: (i) => setSelectedAuditIndex(i),
36
+ getLength: () => auditTransactions.length,
37
+ });
38
+
39
+ useKeyboard(
40
+ overlayActive
41
+ ? {}
42
+ : {
43
+ ...listNav,
44
+ ...tabBindings,
45
+ m: () => {
46
+ if (auditHasMore && auditNextCursor && client) {
47
+ void fetchAuditTransactions({ cursor: auditNextCursor }, client);
48
+ }
49
+ },
50
+ r: () => { if (client) void fetchAuditTransactions({}, client); },
51
+ },
52
+ );
53
+
54
+ return (
55
+ <box height="100%" width="100%" flexDirection="column">
56
+ <box flexGrow={1} width="100%" borderStyle="single">
57
+ <AuditTrail
58
+ transactions={auditTransactions}
59
+ loading={auditLoading}
60
+ hasMore={auditHasMore}
61
+ selectedIndex={selectedAuditIndex}
62
+ />
63
+ </box>
64
+ <box height={1} width="100%">
65
+ <text>{"j/k:navigate m:load more r:refresh Tab:switch tab"}</text>
66
+ </box>
67
+ </box>
68
+ );
69
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Audit trail explorer view.
3
+ *
4
+ * Shows full audit transactions from GET /api/v2/audit/transactions
5
+ * with cursor-based pagination.
6
+ */
7
+
8
+ import React from "react";
9
+ import type { AuditTransaction } from "../../stores/infra-store.js";
10
+ import { Spinner } from "../../shared/components/spinner.js";
11
+ import { EmptyState } from "../../shared/components/empty-state.js";
12
+ import { textStyle } from "../../shared/text-style.js";
13
+ import { formatTimestamp } from "../../shared/utils/format-time.js";
14
+
15
+ export interface AuditTrailProps {
16
+ readonly transactions: readonly AuditTransaction[];
17
+ readonly loading: boolean;
18
+ readonly hasMore: boolean;
19
+ readonly selectedIndex: number;
20
+ }
21
+
22
+ export function AuditTrail({
23
+ transactions,
24
+ loading,
25
+ hasMore,
26
+ selectedIndex,
27
+ }: AuditTrailProps): React.ReactNode {
28
+ if (loading && transactions.length === 0) {
29
+ return <Spinner label="Loading audit transactions..." />;
30
+ }
31
+
32
+ if (transactions.length === 0) {
33
+ return (
34
+ <EmptyState
35
+ message="No audit transactions found."
36
+ hint="Transactions will appear as actions are audited."
37
+ />
38
+ );
39
+ }
40
+
41
+ const displayTransactions = transactions.slice(0, 200);
42
+ const isTruncated = transactions.length > 200;
43
+
44
+ return (
45
+ <box flexDirection="column" height="100%" width="100%">
46
+ {/* Header */}
47
+ <box height={1} width="100%">
48
+ <text>
49
+ {isTruncated
50
+ ? ` Showing first 200 of ${transactions.length} — Action Actor Resource Status Time`
51
+ : " Action Actor Resource Status Time"}
52
+ </text>
53
+ </box>
54
+
55
+ {/* Rows */}
56
+ <scrollbox flexGrow={1} width="100%">
57
+ {displayTransactions.map((tx, i) => {
58
+ const prefix = i === selectedIndex ? "> " : " ";
59
+ const action = tx.action.padEnd(16).slice(0, 16);
60
+ const actor = tx.actor_id.padEnd(18).slice(0, 18);
61
+ const resource = tx.resource.padEnd(30).slice(0, 30);
62
+ const status = tx.status.padEnd(9).slice(0, 9);
63
+ const time = formatTimestamp(tx.timestamp);
64
+ return (
65
+ <box key={tx.transaction_id} height={1} width="100%">
66
+ <text>{`${prefix}${action} ${actor} ${resource} ${status} ${time}`}</text>
67
+ </box>
68
+ );
69
+ })}
70
+ {hasMore && <text style={textStyle({ dim: true })}>{" ... more transactions (press m to load more)"}</text>}
71
+ {loading && transactions.length > 0 && <text style={textStyle({ dim: true })}>{" Loading..."}</text>}
72
+ </scrollbox>
73
+ </box>
74
+ );
75
+ }