@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,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
|
+
}
|