@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.
- package/README.md +247 -0
- package/bin/hoox-tui.js +35 -0
- package/dist/main.js +17594 -0
- package/package.json +72 -0
- package/src/app.tsx +696 -0
- package/src/cli-bridge-test-double.ts +212 -0
- package/src/components/layout/layout.test.tsx +205 -0
- package/src/components/layout/sidebar.tsx +69 -0
- package/src/components/layout/statusbar.test.tsx +423 -0
- package/src/components/layout/statusbar.tsx +348 -0
- package/src/components/shared/animated-border.tsx +58 -0
- package/src/components/shared/chrome.test.tsx +29 -0
- package/src/components/shared/command-palette.tsx +361 -0
- package/src/components/shared/cool-brackets.tsx +75 -0
- package/src/components/shared/crash-screen.tsx +167 -0
- package/src/components/shared/error-boundary.tsx +99 -0
- package/src/components/shared/keybinding-hint.tsx +37 -0
- package/src/components/shared/panel.tsx +61 -0
- package/src/components/shared/quit-modal.tsx +65 -0
- package/src/components/shared/shared.test.tsx +538 -0
- package/src/components/shared/spinner.tsx +85 -0
- package/src/components/shared/status-dot.tsx +54 -0
- package/src/components/shared/view-header.tsx +53 -0
- package/src/components/ui/connection-toasts.test.ts +50 -0
- package/src/components/ui/connection-toasts.ts +215 -0
- package/src/components/ui/dialog.tsx +281 -0
- package/src/components/ui/select.tsx +197 -0
- package/src/components/ui/toast.tsx +185 -0
- package/src/components/views/ai-chat.test.tsx +81 -0
- package/src/components/views/ai-chat.tsx +576 -0
- package/src/components/views/config-editor/action-bar.tsx +102 -0
- package/src/components/views/config-editor/code-editor.tsx +597 -0
- package/src/components/views/config-editor/file-tree.tsx +113 -0
- package/src/components/views/config-editor/types.ts +37 -0
- package/src/components/views/config-editor.test.tsx +465 -0
- package/src/components/views/config-editor.tsx +709 -0
- package/src/components/views/dashboard/alerts-panel.tsx +154 -0
- package/src/components/views/dashboard/auto-repair-panel.tsx +229 -0
- package/src/components/views/dashboard/model-health-section.tsx +276 -0
- package/src/components/views/dashboard/pyne-health-section.tsx +112 -0
- package/src/components/views/dashboard.test.tsx +347 -0
- package/src/components/views/dashboard.tsx +647 -0
- package/src/components/views/db-query.test.tsx +115 -0
- package/src/components/views/db-query.tsx +849 -0
- package/src/components/views/edge-topology.test.tsx +198 -0
- package/src/components/views/edge-topology.tsx +572 -0
- package/src/components/views/kv-viewer.test.tsx +241 -0
- package/src/components/views/kv-viewer.tsx +717 -0
- package/src/components/views/logs-viewer.test.tsx +273 -0
- package/src/components/views/logs-viewer.tsx +616 -0
- package/src/components/views/queue-depth.test.tsx +221 -0
- package/src/components/views/queue-depth.tsx +376 -0
- package/src/components/views/secrets-viewer.test.tsx +65 -0
- package/src/components/views/secrets-viewer.tsx +548 -0
- package/src/components/views/service-manager.test.tsx +358 -0
- package/src/components/views/service-manager.tsx +946 -0
- package/src/components/views/settings.test.tsx +648 -0
- package/src/components/views/settings.tsx +1497 -0
- package/src/components/views/setup-wizard.test.tsx +297 -0
- package/src/components/views/setup-wizard.tsx +1321 -0
- package/src/components/views/trade-monitor.test.tsx +592 -0
- package/src/components/views/trade-monitor.tsx +745 -0
- package/src/components/views/worker-detail.test.tsx +289 -0
- package/src/components/views/worker-detail.tsx +676 -0
- package/src/components/views/worker-settings.test.tsx +43 -0
- package/src/components/views/worker-settings.tsx +563 -0
- package/src/components/views/workers-overview.test.tsx +260 -0
- package/src/components/views/workers-overview.tsx +631 -0
- package/src/hooks/hooks.test.ts +72 -0
- package/src/hooks/index.ts +11 -0
- package/src/hooks/renderer-ref.ts +22 -0
- package/src/hooks/use-keyboard.ts +130 -0
- package/src/hooks/use-polling.ts +73 -0
- package/src/hooks/use-service-data.ts +18 -0
- package/src/main.tsx +117 -0
- package/src/network-test-double.ts +133 -0
- package/src/opentui-augment.d.ts +45 -0
- package/src/opentui-jsx.d.ts +70 -0
- package/src/services/cli-bridge/index.ts +1662 -0
- package/src/services/cli-bridge/standalone.ts +315 -0
- package/src/services/cli-bridge/types.ts +307 -0
- package/src/services/cli-bridge.test.ts +406 -0
- package/src/services/clipboard.test.ts +57 -0
- package/src/services/clipboard.ts +180 -0
- package/src/services/dashboard-settings-loader.ts +31 -0
- package/src/services/dev-log.test.ts +168 -0
- package/src/services/dev-log.ts +194 -0
- package/src/services/hoox-path-service.test.ts +183 -0
- package/src/services/hoox-path-service.ts +185 -0
- package/src/services/tui-connection.test.ts +212 -0
- package/src/services/tui-connection.ts +236 -0
- package/src/services/tui-storage.test.ts +92 -0
- package/src/services/tui-storage.ts +86 -0
- package/src/stores/config-store.test.ts +360 -0
- package/src/stores/service-store.test.ts +1026 -0
- package/src/stores/session-restore.test.ts +134 -0
- package/src/stores/ui-store.test.ts +276 -0
- package/src/test-setup.ts +265 -0
- package/src/test-utils.ts +179 -0
- package/src/types.ts +153 -0
- package/src/utils/colors.test.ts +326 -0
- package/src/utils/formatters.test.ts +236 -0
- package/src/view-registry.test.ts +132 -0
- 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
|
+
}
|