@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,11 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ export { useKeyboard, registerGlobalHandler } from "./use-keyboard";
7
+ export type { KeyEvent, UseKeyboardOptions } from "./use-keyboard";
8
+ export { usePolling } from "./use-polling";
9
+ export type { UsePollingOptions } from "./use-polling";
10
+ export { useServiceData } from "./use-service-data";
11
+ export { setRendererRef, getRendererRef } from "./renderer-ref";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * renderer-ref — Module-level singleton for the CLI renderer.
8
+ *
9
+ * Avoids circular imports between main.ts and hooks.
10
+ */
11
+ import type { CliRenderer } from "@opentui/core";
12
+
13
+ let _renderer: CliRenderer | null = null;
14
+
15
+ /** Install (or clear with `null`) the process-wide CLI renderer singleton. */
16
+ export function setRendererRef(renderer: CliRenderer | null): void {
17
+ _renderer = renderer;
18
+ }
19
+
20
+ export function getRendererRef(): CliRenderer | null {
21
+ return _renderer;
22
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * useKeyboard — Priority-ordered keyboard handler hook.
8
+ *
9
+ * Registers a keypress handler with a priority. Higher priority handlers
10
+ * run first. Cleans up on unmount.
11
+ *
12
+ * A **single** renderer keypress subscription is shared by all consumers
13
+ * (avoids N× handlers each re-dispatching the full list — O(n²) fires).
14
+ *
15
+ * Priority convention:
16
+ * 0 — Modal/dialog (highest)
17
+ * 10 — View-local shortcuts
18
+ * 50 — App-global shortcuts
19
+ * 100 — Default/passthrough
20
+ */
21
+ import { useEffect, useRef } from "react";
22
+ import { getRendererRef } from "./renderer-ref";
23
+
24
+ export interface KeyEvent {
25
+ name: string;
26
+ ctrl: boolean;
27
+ shift: boolean;
28
+ alt: boolean;
29
+ meta: boolean;
30
+ sequence: string;
31
+ }
32
+
33
+ export interface UseKeyboardOptions {
34
+ priority?: number;
35
+ enabled?: boolean;
36
+ }
37
+
38
+ type KeyHandler = (key: KeyEvent) => void;
39
+
40
+ interface RegisteredHandler {
41
+ handler: KeyHandler;
42
+ priority: number;
43
+ }
44
+
45
+ const globalHandlers: RegisteredHandler[] = [];
46
+
47
+ /** One shared keypress subscription for the whole process. */
48
+ let keyInputCleanup: (() => void) | null = null;
49
+
50
+ function sortHandlers(): void {
51
+ globalHandlers.sort((a, b) => a.priority - b.priority);
52
+ }
53
+
54
+ function dispatchKey(key: unknown): void {
55
+ // Snapshot so unsubscribes during dispatch cannot skip handlers
56
+ const snapshot = globalHandlers.slice();
57
+ for (const { handler: h } of snapshot) {
58
+ h(key as KeyEvent);
59
+ }
60
+ }
61
+
62
+ function attachKeyInputIfNeeded(): void {
63
+ if (keyInputCleanup) return;
64
+ const renderer = getRendererRef();
65
+ if (!renderer?.keyInput) return;
66
+ const off = renderer.keyInput.on("keypress", dispatchKey);
67
+ keyInputCleanup =
68
+ typeof off === "function"
69
+ ? off
70
+ : () => {
71
+ // OpenTUI may return void; best-effort detach via off if present
72
+ try {
73
+ (
74
+ renderer.keyInput as { off?: (e: string, fn: unknown) => void }
75
+ ).off?.("keypress", dispatchKey);
76
+ } catch {
77
+ // ignore
78
+ }
79
+ };
80
+ }
81
+
82
+ function detachKeyInputIfIdle(): void {
83
+ if (globalHandlers.length > 0 || !keyInputCleanup) return;
84
+ try {
85
+ keyInputCleanup();
86
+ } catch {
87
+ // already torn down
88
+ }
89
+ keyInputCleanup = null;
90
+ }
91
+
92
+ export function registerGlobalHandler(
93
+ handler: KeyHandler,
94
+ priority = 50
95
+ ): () => void {
96
+ const entry: RegisteredHandler = { handler, priority };
97
+ globalHandlers.push(entry);
98
+ sortHandlers();
99
+ attachKeyInputIfNeeded();
100
+ return () => {
101
+ const idx = globalHandlers.indexOf(entry);
102
+ if (idx >= 0) globalHandlers.splice(idx, 1);
103
+ detachKeyInputIfIdle();
104
+ };
105
+ }
106
+
107
+ export function useKeyboard(
108
+ handler: KeyHandler,
109
+ options: UseKeyboardOptions = {}
110
+ ): void {
111
+ const { priority = 10, enabled = true } = options;
112
+ const handlerRef = useRef(handler);
113
+ handlerRef.current = handler;
114
+
115
+ useEffect(() => {
116
+ if (!enabled) return;
117
+
118
+ const wrapped: KeyHandler = (key) => handlerRef.current(key);
119
+ const entry: RegisteredHandler = { handler: wrapped, priority };
120
+ globalHandlers.push(entry);
121
+ sortHandlers();
122
+ attachKeyInputIfNeeded();
123
+
124
+ return () => {
125
+ const idx = globalHandlers.indexOf(entry);
126
+ if (idx >= 0) globalHandlers.splice(idx, 1);
127
+ detachKeyInputIfIdle();
128
+ };
129
+ }, [priority, enabled]);
130
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * usePolling — Configurable polling with exponential backoff.
8
+ *
9
+ * Reads refreshIntervalMs from config store. Doubles interval on failure
10
+ * (up to 16s max). Resets on success. Pause/resume via enabled toggle.
11
+ */
12
+ import { useEffect, useRef } from "react";
13
+ import { useConfigStore } from "@hoox-sh/hoox-shared";
14
+
15
+ export interface UsePollingOptions {
16
+ callback: () => Promise<void>;
17
+ enabled?: boolean;
18
+ immediate?: boolean;
19
+ }
20
+
21
+ export function usePolling(options: UsePollingOptions): void {
22
+ const { callback, enabled = true, immediate = true } = options;
23
+ const intervalMs = useConfigStore((s) => s.refreshIntervalMs);
24
+ const callbackRef = useRef(callback);
25
+ const retryCount = useRef(0);
26
+ callbackRef.current = callback;
27
+
28
+ useEffect(() => {
29
+ if (!enabled) {
30
+ // Reset backoff when disabled so resume starts clean
31
+ retryCount.current = 0;
32
+ return;
33
+ }
34
+
35
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
36
+ let cancelled = false;
37
+ const MAX_BACKOFF = 16_000;
38
+
39
+ const poll = async () => {
40
+ if (cancelled) return;
41
+ try {
42
+ await callbackRef.current();
43
+ if (cancelled) return;
44
+ retryCount.current = 0;
45
+ } catch {
46
+ if (cancelled) return;
47
+ retryCount.current++;
48
+ }
49
+ if (cancelled) return;
50
+ // Read fresh from store on every iteration so config changes take effect
51
+ const currentInterval = useConfigStore.getState().refreshIntervalMs;
52
+ const backoff = Math.min(
53
+ currentInterval * Math.pow(2, retryCount.current),
54
+ MAX_BACKOFF
55
+ );
56
+ timeoutId = setTimeout(poll, backoff);
57
+ };
58
+
59
+ if (immediate) {
60
+ void poll();
61
+ } else {
62
+ const initialInterval = useConfigStore.getState().refreshIntervalMs;
63
+ timeoutId = setTimeout(() => {
64
+ void poll();
65
+ }, initialInterval);
66
+ }
67
+
68
+ return () => {
69
+ cancelled = true;
70
+ if (timeoutId !== undefined) clearTimeout(timeoutId);
71
+ };
72
+ }, [intervalMs, enabled, immediate]);
73
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * useServiceData — Thin typed wrapper over Zustand useServiceStore.
8
+ *
9
+ * Usage: useServiceData(s => s.workers)
10
+ * useServiceData(s => s.connectionStatus)
11
+ */
12
+ import { useServiceStore } from "@hoox-sh/hoox-shared";
13
+
14
+ export function useServiceData<T>(
15
+ selector: (state: ReturnType<typeof useServiceStore.getState>) => T
16
+ ): T {
17
+ return useServiceStore(selector);
18
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /** @jsxImportSource @opentui/react */
7
+ /**
8
+ * HOOX TUI — Terminal Operations Center
9
+ * Entry point: initializes the OpenTUI CLI renderer and mounts the React root.
10
+ *
11
+ * Usage: bun run packages/tui/src/main.ts
12
+ * or: HOOX_API_URL=http://localhost:8787 bun run packages/tui/src/main.ts
13
+ */
14
+ import { createCliRenderer } from "@opentui/core";
15
+ import { createRoot } from "@opentui/react";
16
+ import { CrashRecoveryApp } from "./app";
17
+ import {
18
+ Colors,
19
+ saveSession,
20
+ useServiceStore,
21
+ useUIStore,
22
+ } from "@hoox-sh/hoox-shared";
23
+ import { setRendererRef } from "./hooks";
24
+ import { enableAutoCopyOnSelection } from "./services/clipboard";
25
+ import { ensureTuiStateDir } from "./services/hoox-path-service";
26
+ import { getDevLogPath, isDevLogEnabled, tuiDevLog } from "./services/dev-log";
27
+ import { resolveTuiConnectionEnv } from "./services/tui-connection";
28
+
29
+ /** CLI `hoox tui --fps N` → env TUI_FPS; clamp to a sane range. */
30
+ function resolveTargetFps(): number {
31
+ const raw = Number(process.env.TUI_FPS ?? 30);
32
+ if (!Number.isFinite(raw)) return 30;
33
+ return Math.min(120, Math.max(5, Math.round(raw)));
34
+ }
35
+
36
+ /** CLI `hoox tui --no-mouse` → env TUI_MOUSE=0. */
37
+ function resolveUseMouse(): boolean {
38
+ const v = process.env.TUI_MOUSE;
39
+ if (v === "0" || v === "false" || v === "off") return false;
40
+ return true;
41
+ }
42
+
43
+ const targetFps = resolveTargetFps();
44
+
45
+ const RENDERER_CONFIG = {
46
+ screenMode: "alternate-screen" as const,
47
+ exitOnCtrlC: false,
48
+ targetFps,
49
+ maxFps: Math.max(60, targetFps),
50
+ useMouse: resolveUseMouse(),
51
+ backgroundColor: Colors.background,
52
+ useKittyKeyboard: {
53
+ disambiguate: true,
54
+ alternateKeys: true,
55
+ events: true,
56
+ },
57
+ };
58
+
59
+ async function main() {
60
+ // Ensure TUI state directory exists ($HOME/.hoox/.tui-state or fallback)
61
+ await ensureTuiStateDir();
62
+
63
+ const conn = resolveTuiConnectionEnv();
64
+ await tuiDevLog.info("startup", "TUI process starting", {
65
+ mode: conn.mode,
66
+ apiUrl: conn.apiUrl,
67
+ apiHost: conn.apiHost,
68
+ hasToken: conn.hasToken,
69
+ allowCliFallback: conn.allowCliFallback,
70
+ fps: targetFps,
71
+ mouse: resolveUseMouse(),
72
+ debug: isDevLogEnabled(),
73
+ debugLogPath: isDevLogEnabled() ? getDevLogPath() : undefined,
74
+ entry: import.meta.path,
75
+ });
76
+
77
+ const renderer = await createCliRenderer(RENDERER_CONFIG);
78
+
79
+ // Persist the *current* UI state on teardown — never hard-code dashboard.
80
+ // (A previous bug always rewrote session.json to dashboard on exit.)
81
+ renderer.on("destroy", () => {
82
+ try {
83
+ const ui = useUIStore.getState();
84
+ const lastUpdated = useServiceStore.getState().lastUpdated;
85
+ saveSession(
86
+ ui.activeView,
87
+ ui.sidebarExpanded,
88
+ { cols: 80, rows: 24 },
89
+ lastUpdated > 0 ? lastUpdated : Date.now()
90
+ ).catch(() => {
91
+ // Non-fatal: session save failures are silent
92
+ });
93
+ } catch {
94
+ // Stores unavailable during catastrophic teardown
95
+ }
96
+ });
97
+
98
+ renderer.on("resize", (_width: unknown, _height: unknown) => {
99
+ // Layout auto-adjusts via flexbox
100
+ });
101
+
102
+ // Set renderer ref so hooks + components can access it via getRendererRef()
103
+ setRendererRef(renderer);
104
+
105
+ // Mouse drag-select → clipboard (OSC 52 + system tools)
106
+ enableAutoCopyOnSelection(
107
+ renderer as unknown as import("./services/clipboard").ClipboardRenderer
108
+ );
109
+
110
+ createRoot(renderer).render(<CrashRecoveryApp />);
111
+ renderer.start();
112
+ }
113
+
114
+ main().catch((err) => {
115
+ console.error("Fatal: Failed to start HOOX TUI:", err);
116
+ process.exit(1);
117
+ });
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Shared network-layer test doubles for TUI unit tests.
8
+ *
9
+ * Installed once from `test-setup.ts` so `service-store` (and any view that
10
+ * triggers fetchWorkers / SSE) never hits a real API. Controllable via
11
+ * exported state + mocks — no per-file mock.module for api-client / sse.
12
+ */
13
+ import { mock } from "bun:test";
14
+
15
+ /** Fixture workers returned by the default hooxFetch mock. */
16
+ export let mockApiData: unknown[] = [];
17
+ /** When true, hooxFetch throws. */
18
+ export let mockApiShouldFail = false;
19
+ export let mockApiErrorMessage = "Network error";
20
+
21
+ /** Per-subscription SSE entry (supports abort removing the callback). */
22
+ export interface SseSubscriptionEntry {
23
+ path: string;
24
+ callback: (data: unknown) => void;
25
+ aborted: boolean;
26
+ abort: () => void;
27
+ }
28
+
29
+ /** Active SSE subscriptions registered by subscribeSSE mock. */
30
+ export let sseSubscriptions: SseSubscriptionEntry[] = [];
31
+
32
+ /** @deprecated Prefer `sseSubscriptions` — flat callback list for emitSseEvent. */
33
+ export let sseCallbacks: Array<(data: unknown) => void> = [];
34
+
35
+ /** Optional delay (ms) applied before hooxFetch resolves/rejects (race tests). */
36
+ export let mockApiDelayMs = 0;
37
+
38
+ export const hooxFetchMock = mock(async (_path: string) => {
39
+ if (mockApiDelayMs > 0) {
40
+ await new Promise((r) => setTimeout(r, mockApiDelayMs));
41
+ }
42
+ if (mockApiShouldFail) {
43
+ throw new Error(mockApiErrorMessage);
44
+ }
45
+ return mockApiData;
46
+ });
47
+
48
+ function pushSubscription(
49
+ path: string,
50
+ callback: (data: unknown) => void
51
+ ): { abort: () => void } {
52
+ const entry: SseSubscriptionEntry = {
53
+ path,
54
+ callback,
55
+ aborted: false,
56
+ abort: () => {
57
+ entry.aborted = true;
58
+ sseSubscriptions = sseSubscriptions.filter((s) => s !== entry);
59
+ sseCallbacks = sseCallbacks.filter((cb) => cb !== callback);
60
+ },
61
+ };
62
+ sseSubscriptions.push(entry);
63
+ sseCallbacks.push(callback);
64
+ return { abort: entry.abort };
65
+ }
66
+
67
+ export const subscribeSSEMock = mock(
68
+ async <T>(path: string, callback: (data: T) => void) => {
69
+ return pushSubscription(path, callback as (data: unknown) => void);
70
+ }
71
+ );
72
+
73
+ export function resetNetworkDoubles(): void {
74
+ mockApiData = [];
75
+ mockApiShouldFail = false;
76
+ mockApiErrorMessage = "Network error";
77
+ mockApiDelayMs = 0;
78
+ sseSubscriptions = [];
79
+ sseCallbacks = [];
80
+ hooxFetchMock.mockClear();
81
+ subscribeSSEMock.mockClear();
82
+ hooxFetchMock.mockImplementation(async (_path: string) => {
83
+ if (mockApiDelayMs > 0) {
84
+ await new Promise((r) => setTimeout(r, mockApiDelayMs));
85
+ }
86
+ if (mockApiShouldFail) {
87
+ throw new Error(mockApiErrorMessage);
88
+ }
89
+ return mockApiData;
90
+ });
91
+ subscribeSSEMock.mockImplementation(
92
+ async <T>(path: string, callback: (data: T) => void) => {
93
+ return pushSubscription(path, callback as (data: unknown) => void);
94
+ }
95
+ );
96
+ }
97
+
98
+ export function setMockApiData(data: unknown[]): void {
99
+ mockApiData = data;
100
+ }
101
+
102
+ export function setMockApiFailure(
103
+ fail: boolean,
104
+ message = "Network error"
105
+ ): void {
106
+ mockApiShouldFail = fail;
107
+ mockApiErrorMessage = message;
108
+ }
109
+
110
+ /** Set artificial latency for hooxFetch (race / ordering tests). */
111
+ export function setMockApiDelay(ms: number): void {
112
+ mockApiDelayMs = Math.max(0, ms);
113
+ }
114
+
115
+ /**
116
+ * Push an SSE event to active (non-aborted) callbacks.
117
+ * Optionally filter by path prefix (e.g. "/v1/trades").
118
+ */
119
+ export function emitSseEvent(data: unknown, pathFilter?: string): void {
120
+ const targets = pathFilter
121
+ ? sseSubscriptions.filter((s) => s.path.includes(pathFilter) && !s.aborted)
122
+ : sseSubscriptions.filter((s) => !s.aborted);
123
+ for (const sub of targets) {
124
+ sub.callback(data);
125
+ }
126
+ }
127
+
128
+ /** Abort every open SSE mock subscription (test isolation helper). */
129
+ export function abortAllSseSubscriptions(): void {
130
+ for (const sub of [...sseSubscriptions]) {
131
+ sub.abort();
132
+ }
133
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Augment @opentui/core types that aren't resolved correctly through Bun's symlink chain.
8
+ * This file fixes TS2305 errors by declaring the missing exports that exist at runtime.
9
+ */
10
+
11
+ // CliRenderer and createCliRenderer are exported from @opentui/core/renderer.d.ts
12
+ // but the re-export through index.d.ts (export * from "./renderer.js") may not
13
+ // resolve correctly due to Bun's symlinked package resolution.
14
+ declare module "@opentui/core" {
15
+ export interface CliRendererConfig {
16
+ screenMode?: "main-screen" | "alternate-screen";
17
+ exitOnCtrlC?: boolean;
18
+ targetFps?: number;
19
+ maxFps?: number;
20
+ useMouse?: boolean;
21
+ backgroundColor?: string;
22
+ useKittyKeyboard?:
23
+ | boolean
24
+ | {
25
+ disambiguate?: boolean;
26
+ alternateKeys?: boolean;
27
+ events?: boolean;
28
+ };
29
+ [key: string]: unknown;
30
+ }
31
+
32
+ export interface CliRenderer {
33
+ on(event: string, handler: (...args: unknown[]) => void): void;
34
+ destroy(): void;
35
+ start(): void;
36
+ keyInput: {
37
+ on(event: string, handler: (key: unknown) => void): () => void;
38
+ };
39
+ [key: string]: unknown;
40
+ }
41
+
42
+ export function createCliRenderer(
43
+ config?: CliRendererConfig
44
+ ): Promise<CliRenderer>;
45
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Augment @opentui/react types.
8
+ *
9
+ * 1. Loose JSX intrinsic element types: OpenTUI v0.2.12's `BoxProps` and `TextProps`
10
+ * don't include all layout/style props (fg, bg, bold, dim, flexDirection, paddingLeft, etc.)
11
+ * that are valid at runtime. This augmentation makes all intrinsic elements accept any props
12
+ * to avoid TS2322 errors throughout the codebase.
13
+ *
14
+ * 2. Named exports: `createRoot` and `useKeyboard` are exported from @opentui/react but
15
+ * may not resolve correctly through Bun's symlinked package resolution. This augmentation
16
+ * declares them to fix TS2305 errors.
17
+ */
18
+
19
+ declare module "@opentui/react" {
20
+ // JSX namespace augmentation for loose intrinsic element types
21
+ export namespace JSX {
22
+ interface IntrinsicElements {
23
+ text: Record<string, unknown>;
24
+ box: Record<string, unknown>;
25
+ span: Record<string, unknown>;
26
+ code: Record<string, unknown>;
27
+ diff: Record<string, unknown>;
28
+ markdown: Record<string, unknown>;
29
+ input: Record<string, unknown>;
30
+ textarea: Record<string, unknown>;
31
+ select: Record<string, unknown>;
32
+ scrollbox: Record<string, unknown>;
33
+ "ascii-font": Record<string, unknown>;
34
+ "tab-select": Record<string, unknown>;
35
+ "line-number": Record<string, unknown>;
36
+ b: Record<string, unknown>;
37
+ i: Record<string, unknown>;
38
+ u: Record<string, unknown>;
39
+ strong: Record<string, unknown>;
40
+ em: Record<string, unknown>;
41
+ br: Record<string, unknown>;
42
+ a: Record<string, unknown>;
43
+ }
44
+ }
45
+
46
+ // Key event object structure from useKeyboard hook
47
+ export interface KeyEvent {
48
+ name: string;
49
+ sequence: string;
50
+ ctrl: boolean;
51
+ alt: boolean;
52
+ meta: boolean;
53
+ shift: boolean;
54
+ [key: string]: unknown;
55
+ }
56
+
57
+ // Named exports that may not resolve through Bun's symlink chain
58
+ export function createRoot(
59
+ container: unknown,
60
+ options?: unknown
61
+ ): {
62
+ render(element: unknown): void;
63
+ unmount(): void;
64
+ };
65
+
66
+ export function useKeyboard(
67
+ handler?: (key: KeyEvent) => void,
68
+ options?: unknown
69
+ ): unknown;
70
+ }