@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,474 @@
1
+ /**
2
+ * Stack panel: Docker container status, nexus.yaml config, .state.json runtime,
3
+ * and server health — all in one place for debugging.
4
+ *
5
+ * Tabs: Containers | Config | State
6
+ * Keybindings: Tab to switch, r to refresh, j/k to scroll.
7
+ */
8
+
9
+ import React, { useEffect, useState } from "react";
10
+ import { useStackStore, type StackTab, type ContainerInfo, type StackPaths } from "../../stores/stack-store.js";
11
+ import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
12
+ import { useApi } from "../../shared/hooks/use-api.js";
13
+ import { useUiStore } from "../../stores/ui-store.js";
14
+ import { useGlobalStore } from "../../stores/global-store.js";
15
+ import { EmptyState } from "../../shared/components/empty-state.js";
16
+ import { LoadingIndicator } from "../../shared/components/loading-indicator.js";
17
+ import { statusColor } from "../../shared/theme.js";
18
+
19
+ // =============================================================================
20
+ // Tab definitions
21
+ // =============================================================================
22
+
23
+ const TAB_ORDER: readonly StackTab[] = ["containers", "config", "state"];
24
+
25
+ const TAB_LABELS: Readonly<Record<StackTab, string>> = {
26
+ containers: "Containers",
27
+ config: "Config",
28
+ state: "State",
29
+ };
30
+
31
+ // =============================================================================
32
+ // Container status colors
33
+ // =============================================================================
34
+
35
+ const CONTAINER_STATE_COLOR: Record<string, string> = {
36
+ running: statusColor.healthy,
37
+ exited: statusColor.error,
38
+ paused: statusColor.warning,
39
+ restarting: statusColor.warning,
40
+ dead: statusColor.error,
41
+ created: statusColor.dim,
42
+ };
43
+
44
+ const HEALTH_COLOR: Record<string, string> = {
45
+ healthy: statusColor.healthy,
46
+ unhealthy: statusColor.error,
47
+ starting: statusColor.warning,
48
+ };
49
+
50
+ // =============================================================================
51
+ // Sub-components
52
+ // =============================================================================
53
+
54
+ function ContainerList({
55
+ containers,
56
+ loading,
57
+ selectedIndex,
58
+ }: {
59
+ containers: readonly ContainerInfo[];
60
+ loading: boolean;
61
+ selectedIndex: number;
62
+ }): React.ReactNode {
63
+ if (loading) {
64
+ return <LoadingIndicator message="Querying Docker..." />;
65
+ }
66
+
67
+ if (containers.length === 0) {
68
+ return <EmptyState message="No containers found." hint="Start the stack with: nexus up" />;
69
+ }
70
+
71
+ return (
72
+ <scrollbox height="100%" width="100%">
73
+ {/* Header */}
74
+ <box height={1} width="100%">
75
+ <text>{" CONTAINER NAME SERVICE STATE HEALTH PORTS IMAGE"}</text>
76
+ </box>
77
+ <box height={1} width="100%">
78
+ <text>{" ------------------------------------ ----------- ---------- ---------- ----------------------- -------------------------"}</text>
79
+ </box>
80
+
81
+ {/* Rows */}
82
+ {containers.map((c, i) => {
83
+ const isSelected = i === selectedIndex;
84
+ const prefix = isSelected ? "> " : " ";
85
+ const stateColor = CONTAINER_STATE_COLOR[c.state] ?? statusColor.dim;
86
+ const hColor = HEALTH_COLOR[c.health] ?? statusColor.dim;
87
+ const name = c.name.length > 36 ? c.name.slice(0, 33) + "..." : c.name;
88
+ const image = c.image.length > 25 ? c.image.slice(0, 22) + "..." : c.image;
89
+ const ports = c.ports.length > 23 ? c.ports.slice(0, 20) + "..." : c.ports;
90
+
91
+ return (
92
+ <box key={c.name} height={1} width="100%">
93
+ <text>
94
+ {`${prefix}${name.padEnd(36)} ${c.service.padEnd(11)} `}
95
+ <span foregroundColor={stateColor}>{c.state.padEnd(10)}</span>
96
+ {" "}
97
+ <span foregroundColor={hColor}>{(c.health || "-").padEnd(10)}</span>
98
+ {` ${ports.padEnd(23)} ${image}`}
99
+ </text>
100
+ </box>
101
+ );
102
+ })}
103
+ </scrollbox>
104
+ );
105
+ }
106
+
107
+ function ConfigView({
108
+ yaml,
109
+ loading,
110
+ scrollOffset,
111
+ }: {
112
+ yaml: string;
113
+ loading: boolean;
114
+ scrollOffset: number;
115
+ }): React.ReactNode {
116
+ if (loading) {
117
+ return <LoadingIndicator message="Reading nexus.yaml..." />;
118
+ }
119
+
120
+ if (!yaml) {
121
+ return <EmptyState message="No nexus.yaml found." hint="Run: nexus init --preset shared" />;
122
+ }
123
+
124
+ const lines = yaml.split("\n");
125
+
126
+ return (
127
+ <scrollbox height="100%" width="100%">
128
+ <box height={1} width="100%">
129
+ <text foregroundColor={statusColor.info}>{" nexus.yaml"}</text>
130
+ </box>
131
+ <box height={1} width="100%">
132
+ <text dimColor>{" " + "─".repeat(60)}</text>
133
+ </box>
134
+ {lines.slice(scrollOffset).map((line, i) => (
135
+ <box key={i} height={1} width="100%">
136
+ <text>
137
+ <span dimColor>{` ${String(scrollOffset + i + 1).padStart(3)} `}</span>
138
+ {line}
139
+ </text>
140
+ </box>
141
+ ))}
142
+ </scrollbox>
143
+ );
144
+ }
145
+
146
+ function StateView({
147
+ stateJson,
148
+ loading,
149
+ projectName,
150
+ scrollOffset,
151
+ }: {
152
+ stateJson: Record<string, unknown> | null;
153
+ loading: boolean;
154
+ projectName: string | null;
155
+ scrollOffset: number;
156
+ }): React.ReactNode {
157
+ if (loading) {
158
+ return <LoadingIndicator message="Reading .state.json..." />;
159
+ }
160
+
161
+ if (!stateJson) {
162
+ return <EmptyState message="No .state.json found." hint="Start the stack first." />;
163
+ }
164
+
165
+ // Render key-value pairs with nested object support
166
+ const lines: { key: string; value: string; indent: number }[] = [];
167
+
168
+ function flatten(obj: Record<string, unknown>, indent: number): void {
169
+ for (const [key, val] of Object.entries(obj)) {
170
+ if (val && typeof val === "object" && !Array.isArray(val)) {
171
+ lines.push({ key, value: "", indent });
172
+ flatten(val as Record<string, unknown>, indent + 1);
173
+ } else {
174
+ lines.push({ key, value: String(val), indent });
175
+ }
176
+ }
177
+ }
178
+ flatten(stateJson, 0);
179
+
180
+ return (
181
+ <scrollbox height="100%" width="100%">
182
+ <box height={1} width="100%">
183
+ <text foregroundColor={statusColor.info}>{" .state.json (runtime)"}</text>
184
+ </box>
185
+ {projectName && (
186
+ <box height={1} width="100%">
187
+ <text>
188
+ {" project_name: "}
189
+ <span foregroundColor={statusColor.identity}>{projectName}</span>
190
+ </text>
191
+ </box>
192
+ )}
193
+ <box height={1} width="100%">
194
+ <text dimColor>{" " + "─".repeat(60)}</text>
195
+ </box>
196
+ {lines.slice(scrollOffset).map((line, i) => {
197
+ const pad = " ".repeat(line.indent);
198
+ return (
199
+ <box key={i} height={1} width="100%">
200
+ <text>
201
+ {" "}{pad}
202
+ <span foregroundColor={statusColor.info}>{line.key}</span>
203
+ {line.value ? ": " : ""}
204
+ {line.value}
205
+ </text>
206
+ </box>
207
+ );
208
+ })}
209
+ </scrollbox>
210
+ );
211
+ }
212
+
213
+ function PathsBar({ paths }: { paths: StackPaths | null }): React.ReactNode {
214
+ if (!paths) return null;
215
+
216
+ return (
217
+ <box height={4} width="100%" flexDirection="column">
218
+ <box height={1} width="100%">
219
+ <text dimColor>{" Paths:"}</text>
220
+ </box>
221
+ <box height={1} width="100%">
222
+ <text>
223
+ {" nexus.yaml "}
224
+ <span foregroundColor={statusColor.reference}>{paths.nexusYaml}</span>
225
+ </text>
226
+ </box>
227
+ <box height={1} width="100%">
228
+ <text>
229
+ {" state.json "}
230
+ <span foregroundColor={statusColor.reference}>{paths.stateJson}</span>
231
+ </text>
232
+ </box>
233
+ <box height={1} width="100%">
234
+ <text>
235
+ {" compose "}
236
+ <span foregroundColor={statusColor.reference}>{paths.composeFile}</span>
237
+ <span dimColor>{" │ data: "}</span>
238
+ <span foregroundColor={statusColor.reference}>{paths.dataDir}</span>
239
+ </text>
240
+ </box>
241
+ </box>
242
+ );
243
+ }
244
+
245
+ function HealthSummary({
246
+ healthDetails,
247
+ uptime,
248
+ serverVersion,
249
+ }: {
250
+ healthDetails: { status: string; components: Record<string, { status: string; detail?: string }> } | null;
251
+ uptime: number | null;
252
+ serverVersion: string | null;
253
+ }): React.ReactNode {
254
+ if (!healthDetails) return null;
255
+
256
+ const color = healthDetails.status === "healthy"
257
+ ? statusColor.healthy
258
+ : healthDetails.status === "degraded"
259
+ ? statusColor.warning
260
+ : statusColor.error;
261
+
262
+ const uptimeStr = uptime != null
263
+ ? `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`
264
+ : "-";
265
+
266
+ const componentEntries = Object.entries(healthDetails.components);
267
+
268
+ return (
269
+ <box height={componentEntries.length > 0 ? componentEntries.length + 3 : 2} width="100%" flexDirection="column">
270
+ <box height={1} width="100%">
271
+ <text>
272
+ {" Server: "}
273
+ <span foregroundColor={color}>{healthDetails.status}</span>
274
+ {" │ uptime: "}{uptimeStr}
275
+ {serverVersion ? ` │ v${serverVersion}` : ""}
276
+ </text>
277
+ </box>
278
+ {componentEntries.length > 0 && (
279
+ <>
280
+ <box height={1} width="100%">
281
+ <text dimColor>{" Components:"}</text>
282
+ </box>
283
+ {componentEntries.map(([name, comp]) => {
284
+ const cColor = comp.status === "healthy" || comp.status === "ok"
285
+ ? statusColor.healthy
286
+ : comp.status === "degraded"
287
+ ? statusColor.warning
288
+ : statusColor.error;
289
+ return (
290
+ <box key={name} height={1} width="100%">
291
+ <text>
292
+ {" "}
293
+ <span foregroundColor={cColor}>{"●"}</span>
294
+ {` ${name.padEnd(24)} `}
295
+ <span foregroundColor={cColor}>{comp.status}</span>
296
+ {comp.detail ? ` ${comp.detail}` : ""}
297
+ </text>
298
+ </box>
299
+ );
300
+ })}
301
+ </>
302
+ )}
303
+ </box>
304
+ );
305
+ }
306
+
307
+ // =============================================================================
308
+ // Main panel
309
+ // =============================================================================
310
+
311
+ export default function StackPanel(): React.ReactNode {
312
+ const client = useApi();
313
+ const overlayActive = useUiStore((s) => s.overlayActive);
314
+ const serverVersion = useGlobalStore((s) => s.serverVersion);
315
+ const uptime = useGlobalStore((s) => s.uptime);
316
+
317
+ const activeTab = useStackStore((s) => s.activeTab);
318
+ const setActiveTab = useStackStore((s) => s.setActiveTab);
319
+ const containers = useStackStore((s) => s.containers);
320
+ const containersLoading = useStackStore((s) => s.containersLoading);
321
+ const configYaml = useStackStore((s) => s.configYaml);
322
+ const configLoading = useStackStore((s) => s.configLoading);
323
+ const stateJson = useStackStore((s) => s.stateJson);
324
+ const stateLoading = useStackStore((s) => s.stateLoading);
325
+ const healthDetails = useStackStore((s) => s.healthDetails);
326
+ const paths = useStackStore((s) => s.paths);
327
+ const error = useStackStore((s) => s.error);
328
+ const refreshAll = useStackStore((s) => s.refreshAll);
329
+
330
+ const [selectedIndex, setSelectedIndex] = useState(0);
331
+ const [scrollOffset, setScrollOffset] = useState(0);
332
+
333
+ // Derive project name from state.json
334
+ const projectName = stateJson?.project_name as string | null ?? null;
335
+
336
+ // Initial fetch
337
+ useEffect(() => {
338
+ refreshAll(client);
339
+ // eslint-disable-next-line react-hooks/exhaustive-deps
340
+ }, [client]);
341
+
342
+ // Reset selection/scroll on tab change
343
+ useEffect(() => {
344
+ setSelectedIndex(0);
345
+ setScrollOffset(0);
346
+ }, [activeTab]);
347
+
348
+ // List length for current tab
349
+ const listLength = activeTab === "containers"
350
+ ? containers.length
351
+ : activeTab === "config"
352
+ ? configYaml.split("\n").length
353
+ : stateJson
354
+ ? Object.keys(stateJson).length * 3 // approximate
355
+ : 0;
356
+
357
+ useKeyboard(
358
+ overlayActive
359
+ ? {}
360
+ : {
361
+ tab: () => {
362
+ const idx = TAB_ORDER.indexOf(activeTab);
363
+ const next = TAB_ORDER[(idx + 1) % TAB_ORDER.length]!;
364
+ setActiveTab(next);
365
+ },
366
+ j: () => {
367
+ if (activeTab === "containers") {
368
+ setSelectedIndex((i) => Math.min(i + 1, containers.length - 1));
369
+ } else {
370
+ setScrollOffset((o) => Math.min(o + 1, Math.max(listLength - 5, 0)));
371
+ }
372
+ },
373
+ down: () => {
374
+ if (activeTab === "containers") {
375
+ setSelectedIndex((i) => Math.min(i + 1, containers.length - 1));
376
+ } else {
377
+ setScrollOffset((o) => Math.min(o + 1, Math.max(listLength - 5, 0)));
378
+ }
379
+ },
380
+ k: () => {
381
+ if (activeTab === "containers") {
382
+ setSelectedIndex((i) => Math.max(i - 1, 0));
383
+ } else {
384
+ setScrollOffset((o) => Math.max(o - 1, 0));
385
+ }
386
+ },
387
+ up: () => {
388
+ if (activeTab === "containers") {
389
+ setSelectedIndex((i) => Math.max(i - 1, 0));
390
+ } else {
391
+ setScrollOffset((o) => Math.max(o - 1, 0));
392
+ }
393
+ },
394
+ r: () => {
395
+ refreshAll(client);
396
+ },
397
+ g: () => {
398
+ setSelectedIndex(0);
399
+ setScrollOffset(0);
400
+ },
401
+ "shift+g": () => {
402
+ if (activeTab === "containers") {
403
+ setSelectedIndex(Math.max(containers.length - 1, 0));
404
+ } else {
405
+ setScrollOffset(Math.max(listLength - 5, 0));
406
+ }
407
+ },
408
+ },
409
+ );
410
+
411
+ return (
412
+ <box height="100%" width="100%" flexDirection="column">
413
+ {/* Tab bar */}
414
+ <box height={1} width="100%">
415
+ <text>
416
+ {TAB_ORDER.map((tab) => {
417
+ const label = TAB_LABELS[tab];
418
+ return tab === activeTab ? `[${label}]` : ` ${label} `;
419
+ }).join(" ")}
420
+ </text>
421
+ </box>
422
+
423
+ {/* Error display */}
424
+ {error && (
425
+ <box height={1} width="100%">
426
+ <text foregroundColor={statusColor.error}>{` Error: ${error}`}</text>
427
+ </box>
428
+ )}
429
+
430
+ {/* Health summary (always visible) */}
431
+ <HealthSummary
432
+ healthDetails={healthDetails}
433
+ uptime={uptime}
434
+ serverVersion={serverVersion}
435
+ />
436
+
437
+ {/* File paths */}
438
+ <PathsBar paths={paths} />
439
+
440
+ {/* Main content */}
441
+ <box flexGrow={1} borderStyle="single">
442
+ {activeTab === "containers" && (
443
+ <ContainerList
444
+ containers={containers}
445
+ loading={containersLoading}
446
+ selectedIndex={selectedIndex}
447
+ />
448
+ )}
449
+ {activeTab === "config" && (
450
+ <ConfigView
451
+ yaml={configYaml}
452
+ loading={configLoading}
453
+ scrollOffset={scrollOffset}
454
+ />
455
+ )}
456
+ {activeTab === "state" && (
457
+ <StateView
458
+ stateJson={stateJson}
459
+ loading={stateLoading}
460
+ projectName={projectName}
461
+ scrollOffset={scrollOffset}
462
+ />
463
+ )}
464
+ </box>
465
+
466
+ {/* Help bar */}
467
+ <box height={1} width="100%">
468
+ <text dimColor>
469
+ {" j/k:navigate Tab:switch tab r:refresh g/G:top/bottom q:quit"}
470
+ </text>
471
+ </box>
472
+ </box>
473
+ );
474
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Conflicts view for the Versions & Snapshots panel.
3
+ * Displayed as a toggleable bottom pane (press 'c' to show/hide).
4
+ */
5
+
6
+ import React from "react";
7
+ import type { ConflictItem } from "../../stores/versions-store.js";
8
+
9
+ interface ConflictsViewProps {
10
+ readonly conflicts: readonly ConflictItem[];
11
+ readonly loading: boolean;
12
+ readonly visible: boolean;
13
+ }
14
+
15
+ export function ConflictsView({
16
+ conflicts,
17
+ loading,
18
+ visible,
19
+ }: ConflictsViewProps): React.ReactNode {
20
+ if (!visible) return null;
21
+
22
+ if (loading) {
23
+ return (
24
+ <box height={6} width="100%" borderStyle="single" flexDirection="column">
25
+ <text>{"--- Conflicts ---"}</text>
26
+ <text>Loading conflicts...</text>
27
+ </box>
28
+ );
29
+ }
30
+
31
+ if (conflicts.length === 0) {
32
+ return (
33
+ <box height={4} width="100%" borderStyle="single" flexDirection="column">
34
+ <text>{"--- Conflicts ---"}</text>
35
+ <text>No conflicts detected.</text>
36
+ </box>
37
+ );
38
+ }
39
+
40
+ return (
41
+ <box
42
+ height={Math.min(conflicts.length + 3, 12)}
43
+ width="100%"
44
+ borderStyle="single"
45
+ flexDirection="column"
46
+ >
47
+ <text>{"--- Conflicts ---"}</text>
48
+ <scrollbox height="100%" width="100%">
49
+ {conflicts.map((conflict, i) => (
50
+ <box key={`${conflict.path}-${i}`} height={1} width="100%">
51
+ <text>
52
+ {` ${conflict.path} ${conflict.reason} expected:${conflict.expected_hash ?? "n/a"} current:${conflict.current_hash ?? "n/a"} txn:${conflict.transaction_id ?? "n/a"}`}
53
+ </text>
54
+ </box>
55
+ ))}
56
+ </scrollbox>
57
+ </box>
58
+ );
59
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Snapshot entry detail view for the selected transaction.
3
+ *
4
+ * Shows each entry's operation, path, and hash changes.
5
+ */
6
+
7
+ import React from "react";
8
+ import type { SnapshotEntry, Transaction } from "../../stores/versions-store.js";
9
+
10
+ // =============================================================================
11
+ // Operation badges
12
+ // =============================================================================
13
+
14
+ const OPERATION_BADGE: Readonly<Record<SnapshotEntry["operation"], string>> = {
15
+ write: "W",
16
+ delete: "D",
17
+ rename: "R",
18
+ };
19
+
20
+ function truncateHash(hash: string | null): string {
21
+ if (!hash) return "-";
22
+ return hash.length > 8 ? hash.slice(0, 8) : hash;
23
+ }
24
+
25
+ // =============================================================================
26
+ // Component
27
+ // =============================================================================
28
+
29
+ interface EntryDetailProps {
30
+ readonly transaction: Transaction | null;
31
+ readonly entries: readonly SnapshotEntry[];
32
+ readonly isLoading: boolean;
33
+ }
34
+
35
+ export function EntryDetail({
36
+ transaction,
37
+ entries,
38
+ isLoading,
39
+ }: EntryDetailProps): React.ReactNode {
40
+ if (!transaction) {
41
+ return (
42
+ <box height="100%" width="100%" justifyContent="center" alignItems="center">
43
+ <text>Select a transaction to view entries</text>
44
+ </box>
45
+ );
46
+ }
47
+
48
+ return (
49
+ <box height="100%" width="100%" flexDirection="column">
50
+ {/* Header */}
51
+ <box height={2} width="100%" flexDirection="column">
52
+ <text>{`Transaction: ${transaction.transaction_id}`}</text>
53
+ <text>{`Status: ${transaction.status} Entries: ${transaction.entry_count}`}</text>
54
+ </box>
55
+
56
+ {/* Entry list */}
57
+ {isLoading ? (
58
+ <box flexGrow={1} justifyContent="center" alignItems="center">
59
+ <text>Loading entries...</text>
60
+ </box>
61
+ ) : entries.length === 0 ? (
62
+ <box flexGrow={1} justifyContent="center" alignItems="center">
63
+ <text>No entries in this transaction</text>
64
+ </box>
65
+ ) : (
66
+ <scrollbox flexGrow={1} width="100%">
67
+ {/* Column headers */}
68
+ <box height={1} width="100%">
69
+ <text>{" OP PATH OLD_HASH NEW_HASH"}</text>
70
+ </box>
71
+ <box height={1} width="100%">
72
+ <text>{" -- ------------------------------- ---------- ----------"}</text>
73
+ </box>
74
+ {entries.map((entry) => {
75
+ const badge = OPERATION_BADGE[entry.operation];
76
+ const original = truncateHash(entry.original_hash);
77
+ const next = truncateHash(entry.new_hash);
78
+
79
+ return (
80
+ <box key={entry.entry_id} height={1} width="100%">
81
+ <text>{` [${badge}] ${entry.path} ${original} ${next}`}</text>
82
+ </box>
83
+ );
84
+ })}
85
+ </scrollbox>
86
+ )}
87
+ </box>
88
+ );
89
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Action hints for the selected transaction.
3
+ *
4
+ * Shows available keyboard shortcuts based on transaction status.
5
+ */
6
+
7
+ import React from "react";
8
+ import type { Transaction } from "../../stores/versions-store.js";
9
+
10
+ interface TransactionActionsProps {
11
+ readonly transaction: Transaction | null;
12
+ }
13
+
14
+ export function TransactionActions({
15
+ transaction,
16
+ }: TransactionActionsProps): React.ReactNode {
17
+ if (!transaction) {
18
+ return <text>{"n:new transaction f:filter q:quit"}</text>;
19
+ }
20
+
21
+ if (transaction.status === "active") {
22
+ return (
23
+ <text>
24
+ {"Enter:commit Backspace:rollback n:new transaction f:filter q:quit"}
25
+ </text>
26
+ );
27
+ }
28
+
29
+ return (
30
+ <text>
31
+ {"(read-only) n:new transaction f:filter q:quit"}
32
+ </text>
33
+ );
34
+ }