@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.
Files changed (51) hide show
  1. package/dist/main.js +1069 -2734
  2. package/package.json +3 -3
  3. package/src/app.tsx +15 -11
  4. package/src/components/shared/shared.test.tsx +2 -2
  5. package/src/components/ui/connection-toasts.ts +6 -0
  6. package/src/components/ui/dialog-state.ts +2 -0
  7. package/src/components/ui/dialog.tsx +0 -13
  8. package/src/components/views/ai-chat.tsx +6 -1
  9. package/src/components/views/config-editor/code-editor.tsx +8 -6
  10. package/src/components/views/config-editor.test.tsx +20 -17
  11. package/src/components/views/config-editor.tsx +8 -6
  12. package/src/components/views/dashboard/model-health-section.tsx +38 -46
  13. package/src/components/views/dashboard/pyne-health-section.tsx +36 -41
  14. package/src/components/views/dashboard.tsx +25 -28
  15. package/src/components/views/kv-viewer.test.tsx +4 -16
  16. package/src/components/views/kv-viewer.tsx +9 -13
  17. package/src/components/views/logs-viewer.test.tsx +4 -4
  18. package/src/components/views/queue-depth.test.tsx +5 -7
  19. package/src/components/views/queue-depth.tsx +10 -14
  20. package/src/components/views/secrets-viewer.test.tsx +1 -1
  21. package/src/components/views/secrets-viewer.tsx +11 -15
  22. package/src/components/views/service-manager.tsx +1 -0
  23. package/src/components/views/settings.test.tsx +2 -7
  24. package/src/components/views/settings.tsx +43 -60
  25. package/src/components/views/setup-wizard.test.tsx +12 -14
  26. package/src/components/views/setup-wizard.tsx +61 -59
  27. package/src/components/views/worker-detail.test.tsx +2 -2
  28. package/src/components/views/worker-settings.tsx +6 -6
  29. package/src/components/views/workers-overview.tsx +1 -1
  30. package/src/hooks/hooks.test.ts +10 -174
  31. package/src/hooks/index.ts +1 -3
  32. package/src/hooks/renderer-ref.ts +1 -0
  33. package/src/hooks/use-polling.ts +12 -0
  34. package/src/services/cli-bridge/index.ts +56 -12
  35. package/src/services/cli-bridge/standalone.ts +4 -1
  36. package/src/services/cli-bridge/types.ts +6 -9
  37. package/src/services/cli-bridge.test.ts +20 -4
  38. package/src/services/clipboard.ts +2 -2
  39. package/src/stores/service-store.test.ts +29 -26
  40. package/src/stores/session-restore.test.ts +2 -0
  41. package/src/test-setup.ts +5 -13
  42. package/src/test-utils.ts +4 -24
  43. package/src/types.ts +9 -47
  44. package/src/utils/colors.test.ts +8 -3
  45. package/src/view-registry.test.ts +2 -19
  46. package/src/view-registry.tsx +7 -20
  47. package/src/components/shared/animated-border.tsx +0 -58
  48. package/src/components/shared/keybinding-hint.tsx +0 -37
  49. package/src/components/ui/select.tsx +0 -49
  50. package/src/hooks/use-keyboard.ts +0 -130
  51. package/src/hooks/use-service-data.ts +0 -35
@@ -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 { ViewId } from "@hoox-sh/hoox-shared";
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 required: ViewId[] = [
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
- }