@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,90 @@
1
+ /**
2
+ * Scrollable transaction list with status badges and selection highlight.
3
+ */
4
+
5
+ import React from "react";
6
+ import type { Transaction } from "../../stores/versions-store.js";
7
+ import { EmptyState } from "../../shared/components/empty-state.js";
8
+ import { textStyle } from "../../shared/text-style.js";
9
+ import { transactionStatusColor } from "../../shared/theme.js";
10
+ import { ScrollIndicator } from "../../shared/components/scroll-indicator.js";
11
+
12
+ // =============================================================================
13
+ // Status badges
14
+ // =============================================================================
15
+
16
+ const STATUS_BADGE: Readonly<Record<Transaction["status"], string>> = {
17
+ active: "\u25CF", // filled circle
18
+ committed: "\u2713", // check mark
19
+ rolled_back: "\u2717", // ballot x
20
+ expired: "\u25CB", // empty circle
21
+ };
22
+
23
+ function statusBadge(status: Transaction["status"]): string {
24
+ return STATUS_BADGE[status];
25
+ }
26
+
27
+ function truncateId(id: string): string {
28
+ return id.length > 8 ? id.slice(0, 8) : id;
29
+ }
30
+
31
+ function formatTime(iso: string): string {
32
+ try {
33
+ const date = new Date(iso);
34
+ return date.toLocaleTimeString(undefined, {
35
+ hour: "2-digit",
36
+ minute: "2-digit",
37
+ });
38
+ } catch {
39
+ return iso;
40
+ }
41
+ }
42
+
43
+ // =============================================================================
44
+ // Component
45
+ // =============================================================================
46
+
47
+ interface TransactionListProps {
48
+ readonly transactions: readonly Transaction[];
49
+ readonly selectedIndex: number;
50
+ }
51
+
52
+ export function TransactionList({
53
+ transactions,
54
+ selectedIndex,
55
+ }: TransactionListProps): React.ReactNode {
56
+ if (transactions.length === 0) {
57
+ return (
58
+ <EmptyState
59
+ message="No transactions yet."
60
+ hint="Press n to begin one, or write a file to create an auto-transaction."
61
+ />
62
+ );
63
+ }
64
+
65
+ return (
66
+ <ScrollIndicator selectedIndex={selectedIndex} totalItems={transactions.length} visibleItems={20}>
67
+ <scrollbox flexGrow={1} width="100%">
68
+ {transactions.map((txn, index) => {
69
+ const selected = index === selectedIndex;
70
+ const prefix = selected ? "\u25B8 " : " ";
71
+ const badge = statusBadge(txn.status);
72
+ const desc = txn.description ?? "";
73
+ const id = truncateId(txn.transaction_id);
74
+ const time = formatTime(txn.created_at);
75
+ const entries = `${txn.entry_count} entries`;
76
+
77
+ return (
78
+ <box key={txn.transaction_id} height={1} width="100%">
79
+ <text>{prefix}</text>
80
+ <text style={textStyle({ fg: transactionStatusColor[txn.status] })}>{badge}</text>
81
+ <text>
82
+ {` ${id} ${desc ? desc + " " : ""}${entries} ${time}`}
83
+ </text>
84
+ </box>
85
+ );
86
+ })}
87
+ </scrollbox>
88
+ </ScrollIndicator>
89
+ );
90
+ }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Versions & Snapshots panel.
3
+ *
4
+ * Left pane: transaction list with status badges.
5
+ * Right pane: entry detail for the selected transaction.
6
+ * Bottom: keyboard shortcut hints.
7
+ */
8
+
9
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
10
+ import {
11
+ useVersionsStore,
12
+ nextStatusFilter,
13
+ } from "../../stores/versions-store.js";
14
+ import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
15
+ import { useCopy } from "../../shared/hooks/use-copy.js";
16
+ import { jumpToStart, jumpToEnd } from "../../shared/hooks/use-list-navigation.js";
17
+ import { useApi } from "../../shared/hooks/use-api.js";
18
+ import { BrickGate } from "../../shared/components/brick-gate.js";
19
+ import { TransactionList } from "./transaction-list.js";
20
+ import { EntryDetail } from "./entry-detail.js";
21
+ import { ConflictsView } from "./conflicts-tab.js";
22
+ import { useUiStore } from "../../stores/ui-store.js";
23
+ import { focusColor } from "../../shared/theme.js";
24
+ import { textStyle } from "../../shared/text-style.js";
25
+ import { formatActionHints, getVersionsFooterBindings } from "../../shared/action-registry.js";
26
+
27
+ export default function VersionsPanel(): React.ReactNode {
28
+ const client = useApi();
29
+
30
+ const transactions = useVersionsStore((s) => s.transactions);
31
+ const selectedIndex = useVersionsStore((s) => s.selectedIndex);
32
+ const statusFilter = useVersionsStore((s) => s.statusFilter);
33
+ const isLoading = useVersionsStore((s) => s.isLoading);
34
+ const error = useVersionsStore((s) => s.error);
35
+ const entries = useVersionsStore((s) => s.entries);
36
+ const entriesLoading = useVersionsStore((s) => s.entriesLoading);
37
+
38
+ const conflicts = useVersionsStore((s) => s.conflicts);
39
+ const conflictsLoading = useVersionsStore((s) => s.conflictsLoading);
40
+ const showConflicts = useVersionsStore((s) => s.showConflicts);
41
+
42
+ const transactionDetail = useVersionsStore((s) => s.transactionDetail);
43
+ const transactionDetailLoading = useVersionsStore((s) => s.transactionDetailLoading);
44
+ const diffContent = useVersionsStore((s) => s.diffContent);
45
+ const diffLoading = useVersionsStore((s) => s.diffLoading);
46
+ const fetchTransactionDetail = useVersionsStore((s) => s.fetchTransactionDetail);
47
+
48
+ const fetchTransactions = useVersionsStore((s) => s.fetchTransactions);
49
+ const setSelectedIndex = useVersionsStore((s) => s.setSelectedIndex);
50
+ const setStatusFilter = useVersionsStore((s) => s.setStatusFilter);
51
+ const fetchEntries = useVersionsStore((s) => s.fetchEntries);
52
+ const fetchDiff = useVersionsStore((s) => s.fetchDiff);
53
+ const beginTransaction = useVersionsStore((s) => s.beginTransaction);
54
+ const commitTransaction = useVersionsStore((s) => s.commitTransaction);
55
+ const rollbackTransaction = useVersionsStore((s) => s.rollbackTransaction);
56
+ const fetchConflicts = useVersionsStore((s) => s.fetchConflicts);
57
+ const toggleConflicts = useVersionsStore((s) => s.toggleConflicts);
58
+
59
+ // Clipboard copy
60
+ const { copy, copied } = useCopy();
61
+
62
+ // Focus pane (ui-store)
63
+ const uiFocusPane = useUiStore((s) => s.getFocusPane("versions"));
64
+ const toggleFocus = useUiStore((s) => s.toggleFocusPane);
65
+ const overlayActive = useUiStore((s) => s.overlayActive);
66
+
67
+ // Transaction search/filter
68
+ const [txnFilterMode, setTxnFilterMode] = useState(false);
69
+ const [txnFilter, setTxnFilter] = useState("");
70
+
71
+ const filteredTransactions = useMemo(() => {
72
+ if (!txnFilter) return transactions;
73
+ const lower = txnFilter.toLowerCase();
74
+ return transactions.filter(
75
+ (t) =>
76
+ t.transaction_id.toLowerCase().includes(lower) ||
77
+ (t.description ?? "").toLowerCase().includes(lower),
78
+ );
79
+ }, [transactions, txnFilter]);
80
+
81
+ // Derive selectedTransaction from filtered list so the index always maps correctly
82
+ const selectedTransaction = filteredTransactions[selectedIndex] ?? null;
83
+
84
+ const handleFilterKey = useCallback(
85
+ (keyName: string) => {
86
+ if (!txnFilterMode) return;
87
+ if (keyName.length === 1) {
88
+ setTxnFilter((b) => b + keyName);
89
+ } else if (keyName === "space") {
90
+ setTxnFilter((b) => b + " ");
91
+ }
92
+ },
93
+ [txnFilterMode],
94
+ );
95
+
96
+ // Fetch transactions on mount and when filter changes
97
+ useEffect(() => {
98
+ if (client) {
99
+ fetchTransactions(client);
100
+ }
101
+ }, [client, statusFilter, fetchTransactions]);
102
+
103
+ // Fetch entries and transaction detail when selection changes
104
+ useEffect(() => {
105
+ if (client && selectedTransaction) {
106
+ fetchEntries(selectedTransaction.transaction_id, client);
107
+ fetchTransactionDetail(selectedTransaction.transaction_id, client);
108
+ }
109
+ }, [client, selectedTransaction, fetchEntries, fetchTransactionDetail]);
110
+
111
+ // Keyboard navigation
112
+ useKeyboard(
113
+ overlayActive
114
+ ? {}
115
+ : txnFilterMode
116
+ ? {
117
+ return: () => {
118
+ setTxnFilterMode(false);
119
+ setSelectedIndex(0);
120
+ },
121
+ escape: () => {
122
+ setTxnFilterMode(false);
123
+ setTxnFilter("");
124
+ setSelectedIndex(0);
125
+ },
126
+ backspace: () => {
127
+ setTxnFilter((b) => b.slice(0, -1));
128
+ },
129
+ }
130
+ : {
131
+ "j": () => {
132
+ if (filteredTransactions.length === 0) return;
133
+ setSelectedIndex(Math.max(0, Math.min(selectedIndex + 1, filteredTransactions.length - 1)));
134
+ },
135
+ "down": () => {
136
+ if (filteredTransactions.length === 0) return;
137
+ setSelectedIndex(Math.max(0, Math.min(selectedIndex + 1, filteredTransactions.length - 1)));
138
+ },
139
+ "k": () => setSelectedIndex(Math.max(selectedIndex - 1, 0)),
140
+ "up": () => setSelectedIndex(Math.max(selectedIndex - 1, 0)),
141
+ "return": () => {
142
+ if (selectedTransaction?.status === "active" && client) {
143
+ commitTransaction(selectedTransaction.transaction_id, client);
144
+ }
145
+ },
146
+ "backspace": () => {
147
+ if (selectedTransaction?.status === "active" && client) {
148
+ rollbackTransaction(selectedTransaction.transaction_id, client);
149
+ }
150
+ },
151
+ "n": () => {
152
+ if (client) {
153
+ beginTransaction(client);
154
+ }
155
+ },
156
+ "f": () => {
157
+ const next = nextStatusFilter(statusFilter);
158
+ setStatusFilter(next);
159
+ },
160
+ "/": () => {
161
+ setTxnFilterMode(true);
162
+ setTxnFilter("");
163
+ },
164
+ "v": () => {
165
+ // View diff for the first entry of the selected transaction
166
+ if (!client || !selectedTransaction || entries.length === 0) return;
167
+ const entry = entries[0];
168
+ if (entry && entry.original_hash && entry.new_hash) {
169
+ fetchDiff(entry.path, entry.original_hash, entry.new_hash, client);
170
+ }
171
+ },
172
+ "c": () => {
173
+ // Toggle conflicts view; fetch on first open
174
+ toggleConflicts();
175
+ if (!showConflicts && client) {
176
+ fetchConflicts(client);
177
+ }
178
+ },
179
+ "tab": () => toggleFocus("versions"),
180
+ "g": () => setSelectedIndex(jumpToStart()),
181
+ "shift+g": () => setSelectedIndex(jumpToEnd(filteredTransactions.length)),
182
+ "y": () => {
183
+ if (selectedTransaction) {
184
+ copy(selectedTransaction.transaction_id);
185
+ }
186
+ },
187
+ },
188
+ txnFilterMode ? handleFilterKey : undefined,
189
+ );
190
+
191
+ const filterLabel = statusFilter ? ` [${statusFilter}]` : " [all]";
192
+
193
+ return (
194
+ <BrickGate brick="versioning">
195
+ <box height="100%" width="100%" flexDirection="column">
196
+ {/* Title bar */}
197
+ <box height={1} width="100%">
198
+ <text>
199
+ {isLoading
200
+ ? `Versions & Snapshots${filterLabel} -- loading...`
201
+ : error
202
+ ? `Versions & Snapshots${filterLabel} -- error: ${error}`
203
+ : `Versions & Snapshots${filterLabel} -- ${filteredTransactions.length} transactions${txnFilter ? ` (filtered)` : ""}`}
204
+ </text>
205
+ </box>
206
+
207
+ {/* Filter bar */}
208
+ {txnFilterMode && (
209
+ <box height={1} width="100%">
210
+ <text>{`Search: ${txnFilter}\u2588`}</text>
211
+ </box>
212
+ )}
213
+ {!txnFilterMode && txnFilter && (
214
+ <box height={1} width="100%">
215
+ <text>{`Filter: "${txnFilter}" (/ to change, Esc in filter to clear)`}</text>
216
+ </box>
217
+ )}
218
+
219
+ {/* Main content: transaction list + entry detail */}
220
+ <box flexGrow={1} flexDirection="row">
221
+ {/* Left pane: transaction list (40%) */}
222
+ <box width="40%" height="100%" borderStyle="single" borderColor={uiFocusPane === "left" ? focusColor.activeBorder : focusColor.inactiveBorder}>
223
+ <TransactionList
224
+ transactions={filteredTransactions}
225
+ selectedIndex={selectedIndex}
226
+ />
227
+ </box>
228
+
229
+ {/* Right pane: entry detail (60%) */}
230
+ <box width="60%" height="100%" borderStyle="single" borderColor={uiFocusPane === "right" ? focusColor.activeBorder : focusColor.inactiveBorder}>
231
+ <EntryDetail
232
+ transaction={selectedTransaction}
233
+ entries={entries}
234
+ isLoading={entriesLoading}
235
+ />
236
+ </box>
237
+ </box>
238
+
239
+ {/* Transaction detail (below entry detail) */}
240
+ {transactionDetail && !transactionDetailLoading && (
241
+ <box height={3} width="100%">
242
+ <text>
243
+ {`Detail: zone=${transactionDetail.zone_id} agent=${transactionDetail.agent_id ?? "n/a"} entries=${transactionDetail.entry_count} created=${transactionDetail.created_at} expires=${transactionDetail.expires_at}`}
244
+ </text>
245
+ </box>
246
+ )}
247
+
248
+ {/* Diff viewer */}
249
+ {diffContent && !diffLoading && (
250
+ <box height={8} width="100%" borderStyle="single" flexDirection="column">
251
+ <box height={1} width="100%"><text>--- Old ---</text></box>
252
+ <scrollbox flexGrow={1} width="100%"><text>{diffContent.old}</text></scrollbox>
253
+ <box height={1} width="100%"><text>--- New ---</text></box>
254
+ <scrollbox flexGrow={1} width="100%"><text>{diffContent.new}</text></scrollbox>
255
+ </box>
256
+ )}
257
+
258
+ {/* Conflicts pane (toggleable) */}
259
+ <ConflictsView
260
+ conflicts={conflicts}
261
+ loading={conflictsLoading}
262
+ visible={showConflicts}
263
+ />
264
+
265
+ {/* Help bar */}
266
+ <box height={1} width="100%">
267
+ {copied
268
+ ? <text style={textStyle({ fg: "green" })}>Copied!</text>
269
+ : <text>
270
+ {formatActionHints(getVersionsFooterBindings({ txnFilterMode }))}
271
+ </text>}
272
+ </box>
273
+ </box>
274
+ </BrickGate>
275
+ );
276
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Execution list view: execution_id, trigger_type, status, started_at,
3
+ * completed_at, actions progress, error_message.
4
+ */
5
+
6
+ import React, { useCallback } from "react";
7
+ import type { ExecutionSummary } from "../../stores/workflows-store.js";
8
+ import { EmptyState } from "../../shared/components/empty-state.js";
9
+ import { VirtualList } from "../../shared/components/virtual-list.js";
10
+
11
+ const VIEWPORT_HEIGHT = 20;
12
+
13
+ interface ExecutionListProps {
14
+ readonly executions: readonly ExecutionSummary[];
15
+ readonly selectedIndex: number;
16
+ readonly loading: boolean;
17
+ }
18
+
19
+ function formatTimestamp(ts: string | null): string {
20
+ if (!ts) return "---";
21
+ try {
22
+ return new Date(ts).toLocaleString();
23
+ } catch {
24
+ return ts;
25
+ }
26
+ }
27
+
28
+ function shortId(id: string): string {
29
+ if (id.length <= 10) return id;
30
+ return `${id.slice(0, 8)}..`;
31
+ }
32
+
33
+ function truncate(text: string, maxLen: number): string {
34
+ if (text.length <= maxLen) return text;
35
+ return `${text.slice(0, maxLen - 3)}...`;
36
+ }
37
+
38
+ export function ExecutionList({
39
+ executions,
40
+ selectedIndex,
41
+ loading,
42
+ }: ExecutionListProps): React.ReactNode {
43
+ if (loading) {
44
+ return (
45
+ <box height="100%" width="100%" justifyContent="center" alignItems="center">
46
+ <text>Loading executions...</text>
47
+ </box>
48
+ );
49
+ }
50
+
51
+ if (executions.length === 0) {
52
+ return (
53
+ <EmptyState
54
+ message="No executions found."
55
+ hint="Select a workflow and press e to execute it."
56
+ />
57
+ );
58
+ }
59
+
60
+ const renderExecution = useCallback(
61
+ (ex: ExecutionSummary, i: number) => {
62
+ const isSelected = i === selectedIndex;
63
+ const id = shortId(ex.execution_id);
64
+ const status = truncate(ex.status, 9);
65
+ const trigger = truncate(ex.trigger_type, 12);
66
+ const progress = `${ex.actions_completed}/${ex.actions_total}`;
67
+ const errorText = ex.error_message
68
+ ? truncate(ex.error_message, 20)
69
+ : "";
70
+ const prefix = isSelected ? "> " : " ";
71
+
72
+ return (
73
+ <box key={ex.execution_id} height={1} width="100%">
74
+ <text>
75
+ {`${prefix}${id.padEnd(10)} ${status.padEnd(9)} ${trigger.padEnd(12)} ${progress.padEnd(8)} ${formatTimestamp(ex.started_at).padEnd(19)} ${errorText}`}
76
+ </text>
77
+ </box>
78
+ );
79
+ },
80
+ [selectedIndex],
81
+ );
82
+
83
+ return (
84
+ <box height="100%" width="100%" flexDirection="column">
85
+ {/* Header */}
86
+ <box height={1} width="100%">
87
+ <text>{" ID STATUS TRIGGER PROGRESS STARTED ERROR"}</text>
88
+ </box>
89
+ <box height={1} width="100%">
90
+ <text>{" ---------- --------- ------------ -------- ------------------- -----"}</text>
91
+ </box>
92
+
93
+ {/* Rows */}
94
+ <VirtualList
95
+ items={executions}
96
+ renderItem={renderExecution}
97
+ viewportHeight={VIEWPORT_HEIGHT}
98
+ selectedIndex={selectedIndex}
99
+ />
100
+ </box>
101
+ );
102
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Scheduler metrics dashboard: queued, running, completed, failed, throughput.
3
+ */
4
+
5
+ import React from "react";
6
+ import type { SchedulerMetrics } from "../../stores/workflows-store.js";
7
+
8
+ interface SchedulerViewProps {
9
+ readonly metrics: SchedulerMetrics | null;
10
+ readonly loading: boolean;
11
+ }
12
+
13
+ function formatMs(ms: number): string {
14
+ if (ms < 1000) return `${ms.toFixed(0)}ms`;
15
+ return `${(ms / 1000).toFixed(1)}s`;
16
+ }
17
+
18
+ export function SchedulerView({ metrics, loading }: SchedulerViewProps): React.ReactNode {
19
+ if (loading) {
20
+ return (
21
+ <box height="100%" width="100%" justifyContent="center" alignItems="center">
22
+ <text>Loading scheduler metrics...</text>
23
+ </box>
24
+ );
25
+ }
26
+
27
+ if (!metrics) {
28
+ return (
29
+ <box height="100%" width="100%" justifyContent="center" alignItems="center">
30
+ <text>No scheduler metrics available</text>
31
+ </box>
32
+ );
33
+ }
34
+
35
+ const total = metrics.queued_tasks + metrics.running_tasks + metrics.completed_tasks + metrics.failed_tasks;
36
+
37
+ return (
38
+ <scrollbox height="100%" width="100%">
39
+ {/* Scheduler type */}
40
+ <box height={1} width="100%">
41
+ <text>{`--- Astraea Scheduler (${metrics.use_hrrn ? "HRRN" : "FIFO"}) ---`}</text>
42
+ </box>
43
+ <box height={1} width="100%">
44
+ <text>{""}</text>
45
+ </box>
46
+
47
+ {/* Task counts */}
48
+ <box height={1} width="100%">
49
+ <text>{" Task Counts:"}</text>
50
+ </box>
51
+ <box height={1} width="100%">
52
+ <text>{` Queued: ${metrics.queued_tasks}`}</text>
53
+ </box>
54
+ <box height={1} width="100%">
55
+ <text>{` Running: ${metrics.running_tasks}`}</text>
56
+ </box>
57
+ <box height={1} width="100%">
58
+ <text>{` Completed: ${metrics.completed_tasks}`}</text>
59
+ </box>
60
+ <box height={1} width="100%">
61
+ <text>{` Failed: ${metrics.failed_tasks}`}</text>
62
+ </box>
63
+ <box height={1} width="100%">
64
+ <text>{` Total: ${total}`}</text>
65
+ </box>
66
+
67
+ {/* Queue by priority class */}
68
+ {metrics.queue_by_class && metrics.queue_by_class.length > 0 && (
69
+ <>
70
+ <box height={1} width="100%">
71
+ <text>{""}</text>
72
+ </box>
73
+ <box height={1} width="100%">
74
+ <text>{" Queue by Priority:"}</text>
75
+ </box>
76
+ {metrics.queue_by_class.map((c, i) => (
77
+ <box key={i} height={1} width="100%">
78
+ <text>{` ${(c.priority_class ?? "unknown").padEnd(12)} ${c.count} tasks`}</text>
79
+ </box>
80
+ ))}
81
+ </>
82
+ )}
83
+
84
+ {/* Fair share */}
85
+ {metrics.fair_share && Object.keys(metrics.fair_share).length > 0 && (
86
+ <>
87
+ <box height={1} width="100%">
88
+ <text>{""}</text>
89
+ </box>
90
+ <box height={1} width="100%">
91
+ <text>{" Fair Share Allocation:"}</text>
92
+ </box>
93
+ {Object.entries(metrics.fair_share).map(([agent, share], i) => (
94
+ <box key={i} height={1} width="100%">
95
+ <text>{` ${agent.padEnd(20)} ${JSON.stringify(share)}`}</text>
96
+ </box>
97
+ ))}
98
+ </>
99
+ )}
100
+
101
+ {/* Performance */}
102
+ {(metrics.avg_wait_ms > 0 || metrics.throughput_per_minute > 0) && (
103
+ <>
104
+ <box height={1} width="100%">
105
+ <text>{""}</text>
106
+ </box>
107
+ <box height={1} width="100%">
108
+ <text>{" Performance:"}</text>
109
+ </box>
110
+ <box height={1} width="100%">
111
+ <text>{` Avg wait: ${formatMs(metrics.avg_wait_ms)}`}</text>
112
+ </box>
113
+ <box height={1} width="100%">
114
+ <text>{` Avg duration: ${formatMs(metrics.avg_duration_ms)}`}</text>
115
+ </box>
116
+ <box height={1} width="100%">
117
+ <text>{` Throughput: ${metrics.throughput_per_minute.toFixed(1)}/min`}</text>
118
+ </box>
119
+ </>
120
+ )}
121
+
122
+ {/* Success rate */}
123
+ {total > 0 && (
124
+ <>
125
+ <box height={1} width="100%">
126
+ <text>{""}</text>
127
+ </box>
128
+ <box height={1} width="100%">
129
+ <text>{` Success: ${((metrics.completed_tasks / total) * 100).toFixed(1)}% | Failed: ${((metrics.failed_tasks / total) * 100).toFixed(1)}%`}</text>
130
+ </box>
131
+ </>
132
+ )}
133
+ </scrollbox>
134
+ );
135
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Workflow list view: name, version, enabled, triggers count, actions count, description.
3
+ */
4
+
5
+ import React, { useCallback } from "react";
6
+ import type { WorkflowSummary } from "../../stores/workflows-store.js";
7
+ import { textStyle } from "../../shared/text-style.js";
8
+ import { statusColor } from "../../shared/theme.js";
9
+ import { EmptyState } from "../../shared/components/empty-state.js";
10
+ import { VirtualList } from "../../shared/components/virtual-list.js";
11
+
12
+ const VIEWPORT_HEIGHT = 20;
13
+
14
+ interface WorkflowListProps {
15
+ readonly workflows: readonly WorkflowSummary[];
16
+ readonly selectedIndex: number;
17
+ readonly loading: boolean;
18
+ }
19
+
20
+ function truncate(text: string, maxLen: number): string {
21
+ if (text.length <= maxLen) return text;
22
+ return `${text.slice(0, maxLen - 3)}...`;
23
+ }
24
+
25
+ export function WorkflowList({
26
+ workflows,
27
+ selectedIndex,
28
+ loading,
29
+ }: WorkflowListProps): React.ReactNode {
30
+ if (loading) {
31
+ return (
32
+ <box height="100%" width="100%" justifyContent="center" alignItems="center">
33
+ <text>Loading workflows...</text>
34
+ </box>
35
+ );
36
+ }
37
+
38
+ if (workflows.length === 0) {
39
+ return (
40
+ <EmptyState
41
+ message="No workflows defined."
42
+ hint="Create one via the API: POST /api/v2/workflows"
43
+ />
44
+ );
45
+ }
46
+
47
+ const renderWorkflow = useCallback(
48
+ (w: WorkflowSummary, i: number) => {
49
+ const isSelected = i === selectedIndex;
50
+ const enabledBadge = w.enabled ? "[ON]" : "[--]";
51
+ const name = truncate(w.name, 19);
52
+ const version = truncate(w.version, 8);
53
+ const desc = w.description ? truncate(w.description, 30) : "";
54
+ const prefix = isSelected ? "> " : " ";
55
+
56
+ return (
57
+ <box key={w.name} height={1} width="100%">
58
+ <text>
59
+ <span>{prefix}</span>
60
+ <span style={textStyle({ fg: w.enabled ? statusColor.healthy : statusColor.dim })}>{enabledBadge.padEnd(5)}</span>
61
+ <span>{`${name.padEnd(19)} ${version.padEnd(8)} ${String(w.triggers).padEnd(4)} ${String(w.actions).padEnd(3)} ${desc}`}</span>
62
+ </text>
63
+ </box>
64
+ );
65
+ },
66
+ [selectedIndex],
67
+ );
68
+
69
+ return (
70
+ <box height="100%" width="100%" flexDirection="column">
71
+ {/* Header */}
72
+ <box height={1} width="100%">
73
+ <text>{" EN NAME VERSION TRIG ACT DESCRIPTION"}</text>
74
+ </box>
75
+ <box height={1} width="100%">
76
+ <text>{" --- ------------------- -------- ---- --- -----------"}</text>
77
+ </box>
78
+
79
+ {/* Rows */}
80
+ <VirtualList
81
+ items={workflows}
82
+ renderItem={renderWorkflow}
83
+ viewportHeight={VIEWPORT_HEIGHT}
84
+ selectedIndex={selectedIndex}
85
+ />
86
+ </box>
87
+ );
88
+ }