@hoox-sh/hoox-tui 0.2.0

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 (104) hide show
  1. package/README.md +247 -0
  2. package/bin/hoox-tui.js +35 -0
  3. package/dist/main.js +17594 -0
  4. package/package.json +72 -0
  5. package/src/app.tsx +696 -0
  6. package/src/cli-bridge-test-double.ts +212 -0
  7. package/src/components/layout/layout.test.tsx +205 -0
  8. package/src/components/layout/sidebar.tsx +69 -0
  9. package/src/components/layout/statusbar.test.tsx +423 -0
  10. package/src/components/layout/statusbar.tsx +348 -0
  11. package/src/components/shared/animated-border.tsx +58 -0
  12. package/src/components/shared/chrome.test.tsx +29 -0
  13. package/src/components/shared/command-palette.tsx +361 -0
  14. package/src/components/shared/cool-brackets.tsx +75 -0
  15. package/src/components/shared/crash-screen.tsx +167 -0
  16. package/src/components/shared/error-boundary.tsx +99 -0
  17. package/src/components/shared/keybinding-hint.tsx +37 -0
  18. package/src/components/shared/panel.tsx +61 -0
  19. package/src/components/shared/quit-modal.tsx +65 -0
  20. package/src/components/shared/shared.test.tsx +538 -0
  21. package/src/components/shared/spinner.tsx +85 -0
  22. package/src/components/shared/status-dot.tsx +54 -0
  23. package/src/components/shared/view-header.tsx +53 -0
  24. package/src/components/ui/connection-toasts.test.ts +50 -0
  25. package/src/components/ui/connection-toasts.ts +215 -0
  26. package/src/components/ui/dialog.tsx +281 -0
  27. package/src/components/ui/select.tsx +197 -0
  28. package/src/components/ui/toast.tsx +185 -0
  29. package/src/components/views/ai-chat.test.tsx +81 -0
  30. package/src/components/views/ai-chat.tsx +576 -0
  31. package/src/components/views/config-editor/action-bar.tsx +102 -0
  32. package/src/components/views/config-editor/code-editor.tsx +597 -0
  33. package/src/components/views/config-editor/file-tree.tsx +113 -0
  34. package/src/components/views/config-editor/types.ts +37 -0
  35. package/src/components/views/config-editor.test.tsx +465 -0
  36. package/src/components/views/config-editor.tsx +709 -0
  37. package/src/components/views/dashboard/alerts-panel.tsx +154 -0
  38. package/src/components/views/dashboard/auto-repair-panel.tsx +229 -0
  39. package/src/components/views/dashboard/model-health-section.tsx +276 -0
  40. package/src/components/views/dashboard/pyne-health-section.tsx +112 -0
  41. package/src/components/views/dashboard.test.tsx +347 -0
  42. package/src/components/views/dashboard.tsx +647 -0
  43. package/src/components/views/db-query.test.tsx +115 -0
  44. package/src/components/views/db-query.tsx +849 -0
  45. package/src/components/views/edge-topology.test.tsx +198 -0
  46. package/src/components/views/edge-topology.tsx +572 -0
  47. package/src/components/views/kv-viewer.test.tsx +241 -0
  48. package/src/components/views/kv-viewer.tsx +717 -0
  49. package/src/components/views/logs-viewer.test.tsx +273 -0
  50. package/src/components/views/logs-viewer.tsx +616 -0
  51. package/src/components/views/queue-depth.test.tsx +221 -0
  52. package/src/components/views/queue-depth.tsx +376 -0
  53. package/src/components/views/secrets-viewer.test.tsx +65 -0
  54. package/src/components/views/secrets-viewer.tsx +548 -0
  55. package/src/components/views/service-manager.test.tsx +358 -0
  56. package/src/components/views/service-manager.tsx +946 -0
  57. package/src/components/views/settings.test.tsx +648 -0
  58. package/src/components/views/settings.tsx +1497 -0
  59. package/src/components/views/setup-wizard.test.tsx +297 -0
  60. package/src/components/views/setup-wizard.tsx +1321 -0
  61. package/src/components/views/trade-monitor.test.tsx +592 -0
  62. package/src/components/views/trade-monitor.tsx +745 -0
  63. package/src/components/views/worker-detail.test.tsx +289 -0
  64. package/src/components/views/worker-detail.tsx +676 -0
  65. package/src/components/views/worker-settings.test.tsx +43 -0
  66. package/src/components/views/worker-settings.tsx +563 -0
  67. package/src/components/views/workers-overview.test.tsx +260 -0
  68. package/src/components/views/workers-overview.tsx +631 -0
  69. package/src/hooks/hooks.test.ts +72 -0
  70. package/src/hooks/index.ts +11 -0
  71. package/src/hooks/renderer-ref.ts +22 -0
  72. package/src/hooks/use-keyboard.ts +130 -0
  73. package/src/hooks/use-polling.ts +73 -0
  74. package/src/hooks/use-service-data.ts +18 -0
  75. package/src/main.tsx +117 -0
  76. package/src/network-test-double.ts +133 -0
  77. package/src/opentui-augment.d.ts +45 -0
  78. package/src/opentui-jsx.d.ts +70 -0
  79. package/src/services/cli-bridge/index.ts +1662 -0
  80. package/src/services/cli-bridge/standalone.ts +315 -0
  81. package/src/services/cli-bridge/types.ts +307 -0
  82. package/src/services/cli-bridge.test.ts +406 -0
  83. package/src/services/clipboard.test.ts +57 -0
  84. package/src/services/clipboard.ts +180 -0
  85. package/src/services/dashboard-settings-loader.ts +31 -0
  86. package/src/services/dev-log.test.ts +168 -0
  87. package/src/services/dev-log.ts +194 -0
  88. package/src/services/hoox-path-service.test.ts +183 -0
  89. package/src/services/hoox-path-service.ts +185 -0
  90. package/src/services/tui-connection.test.ts +212 -0
  91. package/src/services/tui-connection.ts +236 -0
  92. package/src/services/tui-storage.test.ts +92 -0
  93. package/src/services/tui-storage.ts +86 -0
  94. package/src/stores/config-store.test.ts +360 -0
  95. package/src/stores/service-store.test.ts +1026 -0
  96. package/src/stores/session-restore.test.ts +134 -0
  97. package/src/stores/ui-store.test.ts +276 -0
  98. package/src/test-setup.ts +265 -0
  99. package/src/test-utils.ts +179 -0
  100. package/src/types.ts +153 -0
  101. package/src/utils/colors.test.ts +326 -0
  102. package/src/utils/formatters.test.ts +236 -0
  103. package/src/view-registry.test.ts +132 -0
  104. package/src/view-registry.tsx +368 -0
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /** @jsxImportSource @opentui/react */
7
+ /**
8
+ * StatusBar — bottom bar showing connection status, stale data indicator,
9
+ * last-updated timestamp, and (on failure) a click-to-expand error panel.
10
+ *
11
+ * Behaviour:
12
+ * - One-line summary always visible: `[OFFLINE] | <short message>`
13
+ * - Click the OFFLINE / RECONNECTING pill to toggle the full error panel
14
+ * below the bar, showing command, exit code, stderr, stdout, and a
15
+ * recovery hint.
16
+ * - Panel text is `selectable: true` so the user can copy the full
17
+ * diagnostics with the terminal mouse selection. A visible hint at
18
+ * the bottom of the panel surfaces this affordance.
19
+ * - A contextual recovery hint (e.g. "Run `bun install`") appears for
20
+ * known error types so the user gets a suggested next step, not just
21
+ * the raw error.
22
+ */
23
+ import { useState, useCallback, memo } from "react";
24
+ import {
25
+ Colors,
26
+ ConnectionStatusColor,
27
+ useServiceStore,
28
+ formatRelativeTimeFromTime as formatRelativeTime,
29
+ type CliErrorType,
30
+ type CliErrorDetails,
31
+ } from "@hoox-sh/hoox-shared";
32
+ import {
33
+ classifyConnectionError,
34
+ hasApiToken,
35
+ resolveTuiConnectionEnv,
36
+ } from "../../services/tui-connection";
37
+ import { redactSecretsInText } from "../../services/dev-log";
38
+ import { CoolGlyph } from "../shared/cool-brackets";
39
+
40
+ /** Map each CliErrorType to a short, human-readable label. */
41
+ const ERROR_TYPE_LABELS: Record<CliErrorType, string> = {
42
+ "binary-not-found": "CLI binary not found",
43
+ timeout: "Command timed out",
44
+ aborted: "Command aborted",
45
+ "non-zero-exit": "Command failed",
46
+ "spawn-error": "Failed to start CLI",
47
+ };
48
+
49
+ /**
50
+ * Per-error-type recovery hint shown just below the error type. Keeps
51
+ * the user from staring at "non-zero-exit" with no idea what to do.
52
+ */
53
+ const RECOVERY_HINTS: Record<CliErrorType, string> = {
54
+ "binary-not-found":
55
+ "Install the CLI (`bun install -g ./packages/cli`) or set HOOX_CLI=/path/to/hoox.",
56
+ timeout: "The command exceeded its timeout. Retry or raise the limit.",
57
+ aborted: "The command was cancelled. Retry to run it again.",
58
+ "non-zero-exit": "Inspect stderr below for the underlying cause, then retry.",
59
+ "spawn-error":
60
+ "Bun.spawn failed (permission or path issue). Check HOOX_CLI / PATH (hx, hoox).",
61
+ };
62
+
63
+ /** Host label for the status bar (no scheme; falls back to raw URL). */
64
+ export function resolveApiHostLabel(apiUrl: string): string {
65
+ try {
66
+ return new URL(apiUrl).host || apiUrl;
67
+ } catch {
68
+ return apiUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "") || apiUrl;
69
+ }
70
+ }
71
+
72
+ /** Count non-empty lines in a string for the section header badge. */
73
+ function countLines(text: string): number {
74
+ if (!text) return 0;
75
+ return text.split("\n").filter((line) => line.length > 0).length;
76
+ }
77
+
78
+ /** Format a timestamp as a compact "HH:MM:SS" string. */
79
+ function formatClockTime(ts: number): string {
80
+ return new Date(ts).toLocaleTimeString([], {
81
+ hour12: false,
82
+ hour: "2-digit",
83
+ minute: "2-digit",
84
+ second: "2-digit",
85
+ });
86
+ }
87
+
88
+ // ─── Expanded Error Panel ─────────────────────────────────────────────────────
89
+
90
+ interface ExpandedErrorPanelProps {
91
+ details: CliErrorDetails;
92
+ }
93
+
94
+ /**
95
+ * ExpandedErrorPanel — full diagnostic context for a CLI bridge failure.
96
+ * Extracted to keep the parent `StatusBar` small and to make this view
97
+ * independently testable.
98
+ *
99
+ * Exported for direct unit testing — production code should consume it
100
+ * via {@link StatusBar} (which manages the expansion state).
101
+ */
102
+ export const ExpandedErrorPanel = memo(function ExpandedErrorPanel({
103
+ details,
104
+ }: ExpandedErrorPanelProps) {
105
+ // Scrub secret-like patterns before display/copy (never leak tokens to terminal)
106
+ const safeCommand = redactSecretsInText(
107
+ details.command || "(unknown — binary not found)"
108
+ );
109
+ const safeStderr = details.stderr ? redactSecretsInText(details.stderr) : "";
110
+ const safeStdout = details.stdout ? redactSecretsInText(details.stdout) : "";
111
+ const stderrLines = countLines(safeStderr);
112
+ const stdoutLines = countLines(safeStdout);
113
+ const showStdout = safeStdout.length > 0 && safeStdout !== safeStderr;
114
+
115
+ return (
116
+ <box
117
+ flexDirection="column"
118
+ paddingLeft={2}
119
+ paddingRight={2}
120
+ paddingTop={1}
121
+ paddingBottom={1}
122
+ backgroundColor={Colors.card}
123
+ >
124
+ {/* Header — classification + recovery hint (sibling <text>, never nested) */}
125
+ <box flexDirection="column">
126
+ <box flexDirection="row" gap={1}>
127
+ <text fg={Colors.error} bold>
128
+ {`✗ ${ERROR_TYPE_LABELS[details.errorType]}`}
129
+ </text>
130
+ <text fg={Colors["muted-foreground"]}>
131
+ {`(${details.errorType})`}
132
+ </text>
133
+ </box>
134
+ <text fg={Colors["muted-foreground"]}>
135
+ {RECOVERY_HINTS[details.errorType]}
136
+ </text>
137
+ </box>
138
+
139
+ {/* Spacer */}
140
+ <text> </text>
141
+
142
+ {/* Command — own line so a long path doesn't crowd the metadata row */}
143
+ <box flexDirection="column">
144
+ <text fg={Colors.muted}>Command</text>
145
+ {/* Selectable: user can copy the command (secrets already scrubbed). */}
146
+ <text fg={Colors.foreground} selectable>
147
+ {safeCommand}
148
+ </text>
149
+ </box>
150
+
151
+ {/* Metadata row — exit code, duration, timestamp */}
152
+ <box flexDirection="row" gap={1}>
153
+ <text fg={Colors.muted}>Exit code:</text>
154
+ <text fg={details.exitCode === 0 ? Colors.success : Colors.warning}>
155
+ {details.exitCode}
156
+ </text>
157
+ <text fg={Colors.muted}>·</text>
158
+ <text fg={Colors.muted}>Duration:</text>
159
+ <text fg={Colors.foreground}>{details.duration.toFixed(0)}ms</text>
160
+ <text fg={Colors.muted}>·</text>
161
+ <text fg={Colors.muted}>Time:</text>
162
+ <text fg={Colors.foreground}>{formatClockTime(details.timestamp)}</text>
163
+ </box>
164
+
165
+ {/* stderr — line count badge, then selectable text */}
166
+ {safeStderr ? (
167
+ <box flexDirection="column">
168
+ <text fg={Colors.muted}>
169
+ stderr ({stderrLines} {stderrLines === 1 ? "line" : "lines"})
170
+ </text>
171
+ <text fg={Colors.foreground} selectable>
172
+ {safeStderr}
173
+ </text>
174
+ </box>
175
+ ) : null}
176
+
177
+ {/* stdout — only show if distinct from stderr */}
178
+ {showStdout ? (
179
+ <box flexDirection="column">
180
+ <text fg={Colors.muted}>
181
+ stdout ({stdoutLines} {stdoutLines === 1 ? "line" : "lines"})
182
+ </text>
183
+ <text fg={Colors.foreground} selectable>
184
+ {safeStdout}
185
+ </text>
186
+ </box>
187
+ ) : null}
188
+
189
+ {/* Footer — copy affordance + dismiss hint */}
190
+ <text fg={Colors["muted-foreground"]}>
191
+ Drag-select text to auto-copy · click header to collapse
192
+ </text>
193
+ </box>
194
+ );
195
+ });
196
+
197
+ // ─── One-line Summary ─────────────────────────────────────────────────────────
198
+
199
+ interface SummaryLineProps {
200
+ statusColor: string;
201
+ pillInteractive: boolean;
202
+ onClick: () => void;
203
+ expandHint: string;
204
+ children: string;
205
+ }
206
+
207
+ const SummaryLine = memo(function SummaryLine({
208
+ statusColor,
209
+ pillInteractive,
210
+ onClick,
211
+ expandHint,
212
+ children,
213
+ }: SummaryLineProps) {
214
+ if (pillInteractive) {
215
+ return (
216
+ <text fg={statusColor} onMouseUp={onClick}>
217
+ {children}
218
+ {expandHint ? ` ${expandHint}` : ""}
219
+ </text>
220
+ );
221
+ }
222
+ return <text fg={statusColor}>{children}</text>;
223
+ });
224
+
225
+ // ─── StatusBar ────────────────────────────────────────────────────────────────
226
+
227
+ export function StatusBar() {
228
+ const connectionStatus = useServiceStore((s) => s.connectionStatus);
229
+ const lastUpdated = useServiceStore((s) => s.lastUpdated);
230
+ const lastError = useServiceStore((s) => s.lastError);
231
+ const lastErrorDetails = useServiceStore((s) => s.lastErrorDetails);
232
+ const retryCount = useServiceStore((s) => s.retryCount);
233
+ const reconnectDelay = useServiceStore((s) => s.reconnectDelay);
234
+
235
+ const conn = resolveTuiConnectionEnv();
236
+ const tuiMode = conn.mode;
237
+ const apiHost = conn.apiHost;
238
+ const tokenPresent = hasApiToken();
239
+ const errorKind = classifyConnectionError(lastError);
240
+ // Compact AUTH cue: missing token (remote) or auth error — keep short for host room
241
+ const showAuthHint =
242
+ tuiMode === "remote" &&
243
+ (!tokenPresent ||
244
+ errorKind === "auth" ||
245
+ Boolean(lastError?.includes("401")));
246
+
247
+ // Local UI state — expansion is purely a presentation concern, so it
248
+ // lives in component state rather than the global store.
249
+ const [errorExpanded, setErrorExpanded] = useState<boolean>(false);
250
+
251
+ const toggleError = useCallback(() => {
252
+ setErrorExpanded((prev) => !prev);
253
+ }, []);
254
+
255
+ const statusLabel: Record<string, string> = {
256
+ connected: "CONNECTED",
257
+ polling: "POLLING",
258
+ reconnecting: "RECONNECTING",
259
+ offline: "OFFLINE",
260
+ };
261
+
262
+ const relativeTime = lastUpdated > 0 ? formatRelativeTime(lastUpdated) : "—";
263
+
264
+ // The status pill is clickable when there are error details to show —
265
+ // this is the affordance for the "click OFFLINE to expand" requirement.
266
+ const hasDetails = lastErrorDetails !== null;
267
+ const isErrorState =
268
+ connectionStatus === "offline" || connectionStatus === "reconnecting";
269
+ const pillInteractive = hasDetails && isErrorState;
270
+
271
+ const modeColor =
272
+ tuiMode === "remote" ? Colors.info : Colors["muted-foreground"];
273
+ const modeLabel = tuiMode === "remote" ? "REMOTE" : "LOCAL";
274
+
275
+ const parts: string[] = [
276
+ `[${statusLabel[connectionStatus] ?? connectionStatus.toUpperCase()}]`,
277
+ ];
278
+
279
+ if (connectionStatus === "reconnecting") {
280
+ parts.push(`retry ${retryCount}/5 (${reconnectDelay}ms)`);
281
+ }
282
+
283
+ parts.push(
284
+ isErrorState ? `Last updated: ${relativeTime}` : `Updated: ${relativeTime}`
285
+ );
286
+
287
+ if (showAuthHint) {
288
+ parts.push(!tokenPresent ? "| AUTH?" : "| AUTH!");
289
+ }
290
+
291
+ if (lastError && isErrorState && errorKind !== "auth") {
292
+ // Short message (secrets scrubbed). Expand panel for full diagnostics.
293
+ parts.push(`| ${redactSecretsInText(lastError)}`);
294
+ }
295
+
296
+ // The expand hint appears whenever a clickable error is available so
297
+ // users know they can drill in.
298
+ const expandHint = pillInteractive
299
+ ? errorExpanded
300
+ ? "▾ click to collapse"
301
+ : "▸ click for details"
302
+ : "";
303
+
304
+ return (
305
+ <box flexDirection="column" width="100%">
306
+ {/* ── One-line summary ──────────────────────────────────────────────── */}
307
+ <box
308
+ flexDirection="row"
309
+ height={1}
310
+ justifyContent="space-between"
311
+ paddingLeft={1}
312
+ paddingRight={1}
313
+ backgroundColor={Colors.card}
314
+ >
315
+ <box flexDirection="row" gap={1}>
316
+ <CoolGlyph char="┌" />
317
+ <text fg={modeColor} bold>
318
+ [{modeLabel}]
319
+ </text>
320
+ <text fg={Colors["muted-foreground"]} dim>
321
+ {apiHost}
322
+ </text>
323
+ <SummaryLine
324
+ statusColor={
325
+ ConnectionStatusColor[
326
+ connectionStatus as keyof typeof ConnectionStatusColor
327
+ ] ?? Colors.muted
328
+ }
329
+ pillInteractive={pillInteractive}
330
+ onClick={toggleError}
331
+ expandHint={expandHint}
332
+ >
333
+ {parts.join(" ")}
334
+ </SummaryLine>
335
+ <CoolGlyph char="┐" />
336
+ </box>
337
+ <text fg={Colors.dim} dim>
338
+ ^P PALETTE · ^B SIDEBAR · ^Q QUIT
339
+ </text>
340
+ </box>
341
+
342
+ {/* ── Expanded error panel (visible on click) ───────────────────────── */}
343
+ {pillInteractive && errorExpanded && lastErrorDetails && (
344
+ <ExpandedErrorPanel details={lastErrorDetails} />
345
+ )}
346
+ </box>
347
+ );
348
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /** @jsxImportSource @opentui/react */
7
+ /**
8
+ * AnimatedBorder — bordered panel chrome with static accent on focus.
9
+ * (Name kept for call-site compatibility; no color animation.)
10
+ *
11
+ * On focus: border uses Colors.accent + double style.
12
+ * Title: CoolBrackets around uppercase HUD label.
13
+ */
14
+ import { type ReactNode } from "react";
15
+ import { Colors } from "@hoox-sh/hoox-shared";
16
+ import { CoolBrackets } from "./cool-brackets";
17
+
18
+ // ── Types ──────────────────────────────────────────────────────────────────
19
+
20
+ export interface AnimatedBorderProps {
21
+ /** Content to wrap with the border */
22
+ children: ReactNode;
23
+ /** Whether the contained element is focused/highlighted */
24
+ focused?: boolean;
25
+ /** Optional HUD-style panel title (rendered uppercase with accent brackets) */
26
+ title?: string;
27
+ }
28
+
29
+ // ── Component ──────────────────────────────────────────────────────────────
30
+
31
+ export function AnimatedBorder({
32
+ children,
33
+ focused = false,
34
+ title,
35
+ }: AnimatedBorderProps) {
36
+ return (
37
+ <box flexDirection="column" flexGrow={1}>
38
+ {title && (
39
+ <box flexDirection="row" gap={1} paddingLeft={1} paddingBottom={0}>
40
+ <CoolBrackets open="┌" close="┐" gap={1}>
41
+ <text fg={Colors["muted-foreground"]} bold>
42
+ {title.toUpperCase()}
43
+ </text>
44
+ </CoolBrackets>
45
+ </box>
46
+ )}
47
+ <box
48
+ border={true}
49
+ borderStyle={focused ? "double" : "single"}
50
+ borderColor={focused ? Colors.accent : Colors.border}
51
+ backgroundColor={Colors.card}
52
+ flexGrow={1}
53
+ >
54
+ {children}
55
+ </box>
56
+ </box>
57
+ );
58
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Chrome component export tests — ViewHeader + Panel + CoolBrackets.
8
+ */
9
+ import { describe, it, expect } from "bun:test";
10
+ import { ViewHeader } from "./view-header";
11
+ import { Panel } from "./panel";
12
+ import { CoolBrackets, CoolGlyph, useCoolHue } from "./cool-brackets";
13
+ import { Colors } from "@hoox-sh/hoox-shared";
14
+
15
+ describe("chrome", () => {
16
+ it("exports ViewHeader and Panel", () => {
17
+ expect(typeof ViewHeader).toBe("function");
18
+ expect(typeof Panel).toBe("function");
19
+ });
20
+
21
+ it("exports cool bracket chrome (static accent, no animation)", () => {
22
+ expect(typeof CoolBrackets).toBe("function");
23
+ expect(typeof CoolGlyph).toBe("function");
24
+ expect(typeof useCoolHue).toBe("function");
25
+ const hue = useCoolHue(100, true);
26
+ expect(hue.color).toBe(Colors.accent);
27
+ expect(hue.index).toBe(0);
28
+ });
29
+ });