@hoox-sh/hoox-tui 0.3.2 → 0.4.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/dist/main.js +1069 -2734
- package/package.json +3 -3
- package/src/app.tsx +15 -11
- package/src/components/shared/shared.test.tsx +2 -2
- package/src/components/ui/connection-toasts.ts +6 -0
- package/src/components/ui/dialog-state.ts +2 -0
- package/src/components/ui/dialog.tsx +0 -13
- package/src/components/views/ai-chat.tsx +6 -1
- package/src/components/views/config-editor/code-editor.tsx +8 -6
- package/src/components/views/config-editor.test.tsx +20 -17
- package/src/components/views/config-editor.tsx +8 -6
- package/src/components/views/dashboard/model-health-section.tsx +38 -46
- package/src/components/views/dashboard/pyne-health-section.tsx +36 -41
- package/src/components/views/dashboard.tsx +25 -28
- package/src/components/views/kv-viewer.test.tsx +4 -16
- package/src/components/views/kv-viewer.tsx +9 -13
- package/src/components/views/logs-viewer.test.tsx +4 -4
- package/src/components/views/queue-depth.test.tsx +5 -7
- package/src/components/views/queue-depth.tsx +10 -14
- package/src/components/views/secrets-viewer.test.tsx +1 -1
- package/src/components/views/secrets-viewer.tsx +11 -15
- package/src/components/views/service-manager.tsx +1 -0
- package/src/components/views/settings.test.tsx +2 -7
- package/src/components/views/settings.tsx +43 -60
- package/src/components/views/setup-wizard.test.tsx +12 -14
- package/src/components/views/setup-wizard.tsx +61 -59
- package/src/components/views/worker-detail.test.tsx +2 -2
- package/src/components/views/worker-settings.tsx +6 -6
- package/src/components/views/workers-overview.tsx +1 -1
- package/src/hooks/hooks.test.ts +10 -174
- package/src/hooks/index.ts +1 -3
- package/src/hooks/renderer-ref.ts +1 -0
- package/src/hooks/use-polling.ts +12 -0
- package/src/services/cli-bridge/index.ts +56 -12
- package/src/services/cli-bridge/standalone.ts +4 -1
- package/src/services/cli-bridge/types.ts +6 -9
- package/src/services/cli-bridge.test.ts +20 -4
- package/src/services/clipboard.ts +2 -2
- package/src/stores/service-store.test.ts +29 -26
- package/src/stores/session-restore.test.ts +2 -0
- package/src/test-setup.ts +5 -13
- package/src/test-utils.ts +4 -24
- package/src/types.ts +9 -47
- package/src/utils/colors.test.ts +8 -3
- package/src/view-registry.test.ts +2 -19
- package/src/view-registry.tsx +7 -20
- package/src/components/shared/animated-border.tsx +0 -58
- package/src/components/shared/keybinding-hint.tsx +0 -37
- package/src/components/ui/select.tsx +0 -49
- package/src/hooks/use-keyboard.ts +0 -130
- package/src/hooks/use-service-data.ts +0 -35
package/src/view-registry.tsx
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* View registry — single source of truth for view factories, sidebar items,
|
|
9
9
|
* keyboard shortcuts, and command-palette view commands.
|
|
10
10
|
*/
|
|
11
|
-
import type
|
|
11
|
+
import { VIEW_IDS, type ViewId } from "@hoox-sh/hoox-shared";
|
|
12
12
|
import type { DialogHandle } from "./components/ui/dialog";
|
|
13
13
|
import type { CommandEntry } from "./components/shared/command-palette";
|
|
14
14
|
|
|
@@ -247,25 +247,7 @@ export function isRegisteredViewId(id: unknown): id is ViewId {
|
|
|
247
247
|
/** Compile-time exhaustiveness helper (call once at module load). */
|
|
248
248
|
function assertFullCoverage(entries: ViewRegistryEntry[]): void {
|
|
249
249
|
const ids = new Set(entries.map((e) => e.id));
|
|
250
|
-
const
|
|
251
|
-
"dashboard",
|
|
252
|
-
"workers",
|
|
253
|
-
"worker-detail",
|
|
254
|
-
"trade-monitor",
|
|
255
|
-
"logs-viewer",
|
|
256
|
-
"service-manager",
|
|
257
|
-
"config-editor",
|
|
258
|
-
"setup-wizard",
|
|
259
|
-
"settings",
|
|
260
|
-
"queue-depth",
|
|
261
|
-
"kv-viewer",
|
|
262
|
-
"secrets-viewer",
|
|
263
|
-
"db-query",
|
|
264
|
-
"ai-chat",
|
|
265
|
-
"edge-topology",
|
|
266
|
-
"worker-settings",
|
|
267
|
-
];
|
|
268
|
-
for (const id of required) {
|
|
250
|
+
for (const id of VIEW_IDS) {
|
|
269
251
|
if (!ids.has(id)) throw new Error(`view-registry missing ViewId: ${id}`);
|
|
270
252
|
}
|
|
271
253
|
// Unique keyboard bindings within each modifier class
|
|
@@ -298,6 +280,7 @@ export const SIDEBAR_ITEMS: {
|
|
|
298
280
|
shortcut: e.shortcut,
|
|
299
281
|
}));
|
|
300
282
|
|
|
283
|
+
/** Sidebar rows in registry order (id, short label, key hint). */
|
|
301
284
|
export function getSidebarItems(): {
|
|
302
285
|
id: ViewId;
|
|
303
286
|
label: string;
|
|
@@ -306,6 +289,7 @@ export function getSidebarItems(): {
|
|
|
306
289
|
return SIDEBAR_ITEMS;
|
|
307
290
|
}
|
|
308
291
|
|
|
292
|
+
/** Ctrl+digit / letter → view id (primary shortcuts). */
|
|
309
293
|
export function getViewShortcutMap(): Record<string, ViewId> {
|
|
310
294
|
const map: Record<string, ViewId> = {};
|
|
311
295
|
for (const e of VIEW_REGISTRY) {
|
|
@@ -314,6 +298,7 @@ export function getViewShortcutMap(): Record<string, ViewId> {
|
|
|
314
298
|
return map;
|
|
315
299
|
}
|
|
316
300
|
|
|
301
|
+
/** Ctrl+Alt+letter → view id (overflow chords once Ctrl+0-9 are taken). */
|
|
317
302
|
export function getCtrlAltViewMap(): Record<string, ViewId> {
|
|
318
303
|
const map: Record<string, ViewId> = {};
|
|
319
304
|
for (const e of VIEW_REGISTRY) {
|
|
@@ -322,12 +307,14 @@ export function getCtrlAltViewMap(): Record<string, ViewId> {
|
|
|
322
307
|
return map;
|
|
323
308
|
}
|
|
324
309
|
|
|
310
|
+
/** Factory for `id`, or the dashboard factory when the id is unknown. */
|
|
325
311
|
export function getViewFactory(id: ViewId): ViewFactory {
|
|
326
312
|
const entry = VIEW_REGISTRY.find((e) => e.id === id);
|
|
327
313
|
// Fail soft to dashboard when store has a stale/unknown id
|
|
328
314
|
return entry?.factory ?? VIEW_REGISTRY[0]!.factory;
|
|
329
315
|
}
|
|
330
316
|
|
|
317
|
+
/** Command-palette entries for every registered view. */
|
|
331
318
|
export function getViewPaletteCommands(): CommandEntry[] {
|
|
332
319
|
return VIEW_REGISTRY.map((e) => ({
|
|
333
320
|
id: e.id,
|
|
@@ -1,58 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
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
|
-
* KeybindingHint — renders a keyboard shortcut label for the status bar
|
|
9
|
-
* and command palette results.
|
|
10
|
-
*
|
|
11
|
-
* Format: " keys label" — keys in dim/muted, label in foreground.
|
|
12
|
-
* Used as a compact shortcut indicator throughout the TUI.
|
|
13
|
-
*/
|
|
14
|
-
import { Colors } from "@hoox-sh/hoox-shared";
|
|
15
|
-
|
|
16
|
-
// ── Types ──────────────────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
export interface KeybindingHintProps {
|
|
19
|
-
/** The key combination (e.g. "Ctrl+P", "^P", "↑↓") */
|
|
20
|
-
keys: string;
|
|
21
|
-
/** The action description (e.g. "Palette", "Navigate") */
|
|
22
|
-
label: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ── Component ──────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
export function KeybindingHint({ keys, label }: KeybindingHintProps) {
|
|
28
|
-
return (
|
|
29
|
-
<box flexDirection="row" gap={0}>
|
|
30
|
-
<text dim fg={Colors.muted}>
|
|
31
|
-
{keys}
|
|
32
|
-
</text>
|
|
33
|
-
<text> </text>
|
|
34
|
-
<text fg={Colors.foreground}>{label}</text>
|
|
35
|
-
</box>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
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
|
-
* SelectModal — keyboard-navigable option picker via in-house DialogHandle.
|
|
9
|
-
* Does not depend on @opentui-ui/dialog (nested OpenTUI crash on global install).
|
|
10
|
-
*/
|
|
11
|
-
import type { DialogHandle } from "./dialog";
|
|
12
|
-
import { showChoice } from "./dialog";
|
|
13
|
-
|
|
14
|
-
// ── Types ──────────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
/** A single selectable option */
|
|
17
|
-
export interface SelectOption {
|
|
18
|
-
/** Unique key for the option */
|
|
19
|
-
key: string;
|
|
20
|
-
/** Display label shown in the list */
|
|
21
|
-
label: string;
|
|
22
|
-
/** Optional description rendered dimmed below or beside the label */
|
|
23
|
-
description?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Options for the SelectModal */
|
|
27
|
-
export interface SelectModalOptions {
|
|
28
|
-
/** Title displayed at the top of the modal (bold, accent-colored) */
|
|
29
|
-
title: string;
|
|
30
|
-
/** Array of selectable options */
|
|
31
|
-
options: SelectOption[];
|
|
32
|
-
/** Whether clicking outside the modal dismisses it (default: true) */
|
|
33
|
-
closeOnClickOutside?: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Show a modal select dialog with keyboard navigation.
|
|
38
|
-
* Returns the selected option's `key`, or `undefined` if dismissed.
|
|
39
|
-
*/
|
|
40
|
-
export async function showSelectModal(
|
|
41
|
-
dialog: DialogHandle,
|
|
42
|
-
opts: SelectModalOptions
|
|
43
|
-
): Promise<string | undefined> {
|
|
44
|
-
return showChoice(dialog, {
|
|
45
|
-
title: opts.title,
|
|
46
|
-
choices: opts.options,
|
|
47
|
-
closeOnClickOutside: opts.closeOnClickOutside ?? true,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
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 type ServiceStoreState = ReturnType<typeof useServiceStore.getState>;
|
|
15
|
-
export type ServiceStoreSelector<T> = (state: ServiceStoreState) => T;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Store hook used by {@link useServiceData}. Production points at
|
|
19
|
-
* `useServiceStore`; unit tests may swap this for a pure selector runner
|
|
20
|
-
* via {@link __setServiceStoreHookForTests}.
|
|
21
|
-
*/
|
|
22
|
-
let storeHook: <T>(selector: ServiceStoreSelector<T>) => T =
|
|
23
|
-
useServiceStore as <T>(selector: ServiceStoreSelector<T>) => T;
|
|
24
|
-
|
|
25
|
-
/** @internal Test-only seam — pass `null` to restore the real Zustand hook. */
|
|
26
|
-
export function __setServiceStoreHookForTests(
|
|
27
|
-
hook: (<T>(selector: ServiceStoreSelector<T>) => T) | null
|
|
28
|
-
): void {
|
|
29
|
-
storeHook =
|
|
30
|
-
hook ?? (useServiceStore as <T>(selector: ServiceStoreSelector<T>) => T);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function useServiceData<T>(selector: ServiceStoreSelector<T>): T {
|
|
34
|
-
return storeHook(selector);
|
|
35
|
-
}
|