@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.
- package/README.md +30 -0
- package/package.json +48 -0
- package/src/app.tsx +349 -0
- package/src/index.tsx +137 -0
- package/src/opentui-env.d.ts +61 -0
- package/src/panels/access/access-panel.tsx +597 -0
- package/src/panels/access/alert-list.tsx +77 -0
- package/src/panels/access/constraint-creator.tsx +128 -0
- package/src/panels/access/constraint-list.tsx +72 -0
- package/src/panels/access/credential-list.tsx +68 -0
- package/src/panels/access/delegation-chain-view.tsx +110 -0
- package/src/panels/access/delegation-completer.tsx +120 -0
- package/src/panels/access/delegation-creator.tsx +237 -0
- package/src/panels/access/delegation-list.tsx +74 -0
- package/src/panels/access/fraud-score-view.tsx +94 -0
- package/src/panels/access/manifest-creator.tsx +167 -0
- package/src/panels/access/manifest-list.tsx +105 -0
- package/src/panels/access/namespace-config-view.tsx +525 -0
- package/src/panels/access/permission-checker.tsx +231 -0
- package/src/panels/agents/agent-status-view.tsx +196 -0
- package/src/panels/agents/agents-panel.tsx +493 -0
- package/src/panels/agents/delegation-list.tsx +154 -0
- package/src/panels/agents/inbox-view.tsx +96 -0
- package/src/panels/agents/trajectories-tab.tsx +40 -0
- package/src/panels/api-console/api-console-panel.tsx +189 -0
- package/src/panels/api-console/codegen-viewer.tsx +36 -0
- package/src/panels/api-console/codegen.ts +112 -0
- package/src/panels/api-console/endpoint-list.tsx +57 -0
- package/src/panels/api-console/request-builder.tsx +69 -0
- package/src/panels/api-console/response-viewer.tsx +54 -0
- package/src/panels/connectors/available-tab.tsx +357 -0
- package/src/panels/connectors/connector-row.tsx +121 -0
- package/src/panels/connectors/connectors-panel.tsx +88 -0
- package/src/panels/connectors/error-parser.ts +116 -0
- package/src/panels/connectors/mounted-tab.tsx +179 -0
- package/src/panels/connectors/skills-tab.tsx +235 -0
- package/src/panels/connectors/template-generator.ts +211 -0
- package/src/panels/connectors/write-tab.tsx +514 -0
- package/src/panels/events/audit-tab.tsx +69 -0
- package/src/panels/events/audit-trail.tsx +75 -0
- package/src/panels/events/connector-detail.tsx +49 -0
- package/src/panels/events/connector-list.tsx +73 -0
- package/src/panels/events/connectors-tab.tsx +92 -0
- package/src/panels/events/event-replay.tsx +80 -0
- package/src/panels/events/events-panel.tsx +414 -0
- package/src/panels/events/events-tab.tsx +212 -0
- package/src/panels/events/lock-list.tsx +54 -0
- package/src/panels/events/locks-tab.tsx +103 -0
- package/src/panels/events/mcl-replay.tsx +77 -0
- package/src/panels/events/mcl-tab.tsx +83 -0
- package/src/panels/events/operations-tab-wrapper.tsx +62 -0
- package/src/panels/events/operations-tab.tsx +41 -0
- package/src/panels/events/replay-tab.tsx +76 -0
- package/src/panels/events/secrets-audit.tsx +64 -0
- package/src/panels/events/secrets-tab.tsx +75 -0
- package/src/panels/events/subscription-list.tsx +54 -0
- package/src/panels/events/subscriptions-tab.tsx +82 -0
- package/src/panels/files/file-aspects.tsx +93 -0
- package/src/panels/files/file-editor.tsx +160 -0
- package/src/panels/files/file-explorer-keybindings.ts +468 -0
- package/src/panels/files/file-explorer-panel.tsx +545 -0
- package/src/panels/files/file-lineage.tsx +163 -0
- package/src/panels/files/file-list-item.tsx +28 -0
- package/src/panels/files/file-metadata.tsx +62 -0
- package/src/panels/files/file-preview.tsx +108 -0
- package/src/panels/files/file-schema.tsx +89 -0
- package/src/panels/files/file-tree-node.tsx +44 -0
- package/src/panels/files/file-tree.tsx +169 -0
- package/src/panels/files/share-links-tab.tsx +33 -0
- package/src/panels/files/uploads-tab.tsx +45 -0
- package/src/panels/payments/approval-list.tsx +83 -0
- package/src/panels/payments/balance-card.tsx +43 -0
- package/src/panels/payments/budget-card.tsx +70 -0
- package/src/panels/payments/payments-panel.tsx +451 -0
- package/src/panels/payments/policy-list.tsx +64 -0
- package/src/panels/payments/reservation-list.tsx +78 -0
- package/src/panels/payments/transaction-list.tsx +103 -0
- package/src/panels/payments/transfer-form.tsx +109 -0
- package/src/panels/search/column-search.tsx +79 -0
- package/src/panels/search/knowledge-view.tsx +100 -0
- package/src/panels/search/memory-list.tsx +197 -0
- package/src/panels/search/playbook-list.tsx +77 -0
- package/src/panels/search/rlm-answer-view.tsx +105 -0
- package/src/panels/search/search-panel.tsx +405 -0
- package/src/panels/search/search-results.tsx +116 -0
- package/src/panels/stack/stack-panel.tsx +474 -0
- package/src/panels/versions/conflicts-tab.tsx +59 -0
- package/src/panels/versions/entry-detail.tsx +89 -0
- package/src/panels/versions/transaction-actions.tsx +34 -0
- package/src/panels/versions/transaction-list.tsx +90 -0
- package/src/panels/versions/versions-panel.tsx +276 -0
- package/src/panels/workflows/execution-list.tsx +102 -0
- package/src/panels/workflows/scheduler-view.tsx +135 -0
- package/src/panels/workflows/workflow-list.tsx +88 -0
- package/src/panels/workflows/workflows-panel.tsx +295 -0
- package/src/panels/zones/brick-detail.tsx +136 -0
- package/src/panels/zones/brick-list.tsx +56 -0
- package/src/panels/zones/cache-tab.tsx +118 -0
- package/src/panels/zones/drift-view.tsx +97 -0
- package/src/panels/zones/mcp-mounts-tab.tsx +38 -0
- package/src/panels/zones/memories-tab.tsx +37 -0
- package/src/panels/zones/reindex-status.tsx +84 -0
- package/src/panels/zones/workspaces-tab.tsx +37 -0
- package/src/panels/zones/zone-list.tsx +73 -0
- package/src/panels/zones/zones-panel.tsx +559 -0
- package/src/services/command-runner.ts +303 -0
- package/src/shared/accessibility-announcements.ts +44 -0
- package/src/shared/action-registry.ts +466 -0
- package/src/shared/brick-states.ts +91 -0
- package/src/shared/command-palette.ts +35 -0
- package/src/shared/components/announcement-bar.tsx +30 -0
- package/src/shared/components/app-confirm-dialog.tsx +29 -0
- package/src/shared/components/breadcrumb.tsx +21 -0
- package/src/shared/components/brick-gate.tsx +60 -0
- package/src/shared/components/command-output.tsx +95 -0
- package/src/shared/components/command-palette.tsx +97 -0
- package/src/shared/components/confirm-dialog.tsx +61 -0
- package/src/shared/components/diff-viewer.tsx +219 -0
- package/src/shared/components/empty-state.tsx +36 -0
- package/src/shared/components/error-bar.tsx +60 -0
- package/src/shared/components/error-boundary.tsx +53 -0
- package/src/shared/components/help-overlay.tsx +99 -0
- package/src/shared/components/identity-switcher.tsx +168 -0
- package/src/shared/components/loading-indicator.tsx +40 -0
- package/src/shared/components/pagination-bar.tsx +68 -0
- package/src/shared/components/pre-connection-screen.tsx +398 -0
- package/src/shared/components/scroll-indicator.tsx +46 -0
- package/src/shared/components/side-nav-utils.ts +68 -0
- package/src/shared/components/side-nav.tsx +287 -0
- package/src/shared/components/spinner.tsx +26 -0
- package/src/shared/components/status-bar.tsx +117 -0
- package/src/shared/components/styled-text.tsx +72 -0
- package/src/shared/components/sub-tab-bar-utils.ts +100 -0
- package/src/shared/components/sub-tab-bar.tsx +40 -0
- package/src/shared/components/tab-bar-utils.ts +36 -0
- package/src/shared/components/tab-bar.tsx +50 -0
- package/src/shared/components/text-input.tsx +73 -0
- package/src/shared/components/tooltip.tsx +53 -0
- package/src/shared/components/virtual-list.tsx +93 -0
- package/src/shared/components/welcome-screen.tsx +111 -0
- package/src/shared/hooks/use-api.ts +10 -0
- package/src/shared/hooks/use-brick-available.ts +42 -0
- package/src/shared/hooks/use-confirm.ts +66 -0
- package/src/shared/hooks/use-connection-state.ts +67 -0
- package/src/shared/hooks/use-copy.ts +31 -0
- package/src/shared/hooks/use-fresh-server.ts +62 -0
- package/src/shared/hooks/use-keyboard.ts +58 -0
- package/src/shared/hooks/use-list-navigation.ts +106 -0
- package/src/shared/hooks/use-swr.ts +117 -0
- package/src/shared/hooks/use-tab-fallback.ts +32 -0
- package/src/shared/hooks/use-text-input.ts +113 -0
- package/src/shared/hooks/use-visible-tabs.ts +61 -0
- package/src/shared/lib/circular-buffer.ts +82 -0
- package/src/shared/lib/clipboard.ts +14 -0
- package/src/shared/nav-items.ts +73 -0
- package/src/shared/navigation.ts +110 -0
- package/src/shared/status-breadcrumb.ts +74 -0
- package/src/shared/syntax-style.ts +3 -0
- package/src/shared/tab-visibility.ts +15 -0
- package/src/shared/text-style.ts +23 -0
- package/src/shared/theme.ts +179 -0
- package/src/shared/utils/format-size.ts +20 -0
- package/src/shared/utils/format-text.ts +10 -0
- package/src/shared/utils/format-time.ts +72 -0
- package/src/shared/utils/lru-cache.ts +75 -0
- package/src/stores/access-store-types.ts +154 -0
- package/src/stores/access-store.ts +674 -0
- package/src/stores/agents-store.ts +404 -0
- package/src/stores/announcement-store.ts +46 -0
- package/src/stores/api-console-store.ts +476 -0
- package/src/stores/connectors-store.ts +434 -0
- package/src/stores/create-api-action.ts +140 -0
- package/src/stores/delegation-store.ts +300 -0
- package/src/stores/error-store.ts +102 -0
- package/src/stores/events-store.ts +163 -0
- package/src/stores/files-store.ts +630 -0
- package/src/stores/first-run-store.ts +34 -0
- package/src/stores/global-store.ts +255 -0
- package/src/stores/infra-store.ts +461 -0
- package/src/stores/knowledge-store.ts +358 -0
- package/src/stores/lineage-store.ts +126 -0
- package/src/stores/mcp-store.ts +147 -0
- package/src/stores/payments-store.ts +545 -0
- package/src/stores/search-store-types.ts +155 -0
- package/src/stores/search-store.ts +656 -0
- package/src/stores/share-link-store.ts +151 -0
- package/src/stores/stack-store.ts +352 -0
- package/src/stores/ui-store.ts +161 -0
- package/src/stores/upload-store.ts +131 -0
- package/src/stores/versions-store.ts +355 -0
- package/src/stores/workflows-store.ts +402 -0
- package/src/stores/workspace-store.ts +185 -0
- 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
|
+
}
|