@nexus-ai-fs/tui 0.9.18
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 +30 -0
- package/package.json +48 -0
- package/src/app.tsx +349 -0
- package/src/index.tsx +137 -0
- package/src/opentui-env.d.ts +61 -0
- package/src/panels/access/access-panel.tsx +597 -0
- package/src/panels/access/alert-list.tsx +77 -0
- package/src/panels/access/constraint-creator.tsx +128 -0
- package/src/panels/access/constraint-list.tsx +72 -0
- package/src/panels/access/credential-list.tsx +68 -0
- package/src/panels/access/delegation-chain-view.tsx +110 -0
- package/src/panels/access/delegation-completer.tsx +120 -0
- package/src/panels/access/delegation-creator.tsx +237 -0
- package/src/panels/access/delegation-list.tsx +74 -0
- package/src/panels/access/fraud-score-view.tsx +94 -0
- package/src/panels/access/manifest-creator.tsx +167 -0
- package/src/panels/access/manifest-list.tsx +105 -0
- package/src/panels/access/namespace-config-view.tsx +525 -0
- package/src/panels/access/permission-checker.tsx +231 -0
- package/src/panels/agents/agent-status-view.tsx +196 -0
- package/src/panels/agents/agents-panel.tsx +493 -0
- package/src/panels/agents/delegation-list.tsx +154 -0
- package/src/panels/agents/inbox-view.tsx +96 -0
- package/src/panels/agents/trajectories-tab.tsx +40 -0
- package/src/panels/api-console/api-console-panel.tsx +189 -0
- package/src/panels/api-console/codegen-viewer.tsx +36 -0
- package/src/panels/api-console/codegen.ts +112 -0
- package/src/panels/api-console/endpoint-list.tsx +57 -0
- package/src/panels/api-console/request-builder.tsx +69 -0
- package/src/panels/api-console/response-viewer.tsx +54 -0
- package/src/panels/connectors/available-tab.tsx +357 -0
- package/src/panels/connectors/connector-row.tsx +121 -0
- package/src/panels/connectors/connectors-panel.tsx +88 -0
- package/src/panels/connectors/error-parser.ts +116 -0
- package/src/panels/connectors/mounted-tab.tsx +179 -0
- package/src/panels/connectors/skills-tab.tsx +235 -0
- package/src/panels/connectors/template-generator.ts +211 -0
- package/src/panels/connectors/write-tab.tsx +514 -0
- package/src/panels/events/audit-tab.tsx +69 -0
- package/src/panels/events/audit-trail.tsx +75 -0
- package/src/panels/events/connector-detail.tsx +49 -0
- package/src/panels/events/connector-list.tsx +73 -0
- package/src/panels/events/connectors-tab.tsx +92 -0
- package/src/panels/events/event-replay.tsx +80 -0
- package/src/panels/events/events-panel.tsx +414 -0
- package/src/panels/events/events-tab.tsx +212 -0
- package/src/panels/events/lock-list.tsx +54 -0
- package/src/panels/events/locks-tab.tsx +103 -0
- package/src/panels/events/mcl-replay.tsx +77 -0
- package/src/panels/events/mcl-tab.tsx +83 -0
- package/src/panels/events/operations-tab-wrapper.tsx +62 -0
- package/src/panels/events/operations-tab.tsx +41 -0
- package/src/panels/events/replay-tab.tsx +76 -0
- package/src/panels/events/secrets-audit.tsx +64 -0
- package/src/panels/events/secrets-tab.tsx +75 -0
- package/src/panels/events/subscription-list.tsx +54 -0
- package/src/panels/events/subscriptions-tab.tsx +82 -0
- package/src/panels/files/file-aspects.tsx +93 -0
- package/src/panels/files/file-editor.tsx +160 -0
- package/src/panels/files/file-explorer-keybindings.ts +468 -0
- package/src/panels/files/file-explorer-panel.tsx +545 -0
- package/src/panels/files/file-lineage.tsx +163 -0
- package/src/panels/files/file-list-item.tsx +28 -0
- package/src/panels/files/file-metadata.tsx +62 -0
- package/src/panels/files/file-preview.tsx +108 -0
- package/src/panels/files/file-schema.tsx +89 -0
- package/src/panels/files/file-tree-node.tsx +44 -0
- package/src/panels/files/file-tree.tsx +169 -0
- package/src/panels/files/share-links-tab.tsx +33 -0
- package/src/panels/files/uploads-tab.tsx +45 -0
- package/src/panels/payments/approval-list.tsx +83 -0
- package/src/panels/payments/balance-card.tsx +43 -0
- package/src/panels/payments/budget-card.tsx +70 -0
- package/src/panels/payments/payments-panel.tsx +451 -0
- package/src/panels/payments/policy-list.tsx +64 -0
- package/src/panels/payments/reservation-list.tsx +78 -0
- package/src/panels/payments/transaction-list.tsx +103 -0
- package/src/panels/payments/transfer-form.tsx +109 -0
- package/src/panels/search/column-search.tsx +79 -0
- package/src/panels/search/knowledge-view.tsx +100 -0
- package/src/panels/search/memory-list.tsx +197 -0
- package/src/panels/search/playbook-list.tsx +77 -0
- package/src/panels/search/rlm-answer-view.tsx +105 -0
- package/src/panels/search/search-panel.tsx +405 -0
- package/src/panels/search/search-results.tsx +116 -0
- package/src/panels/stack/stack-panel.tsx +474 -0
- package/src/panels/versions/conflicts-tab.tsx +59 -0
- package/src/panels/versions/entry-detail.tsx +89 -0
- package/src/panels/versions/transaction-actions.tsx +34 -0
- package/src/panels/versions/transaction-list.tsx +90 -0
- package/src/panels/versions/versions-panel.tsx +276 -0
- package/src/panels/workflows/execution-list.tsx +102 -0
- package/src/panels/workflows/scheduler-view.tsx +135 -0
- package/src/panels/workflows/workflow-list.tsx +88 -0
- package/src/panels/workflows/workflows-panel.tsx +295 -0
- package/src/panels/zones/brick-detail.tsx +136 -0
- package/src/panels/zones/brick-list.tsx +56 -0
- package/src/panels/zones/cache-tab.tsx +118 -0
- package/src/panels/zones/drift-view.tsx +97 -0
- package/src/panels/zones/mcp-mounts-tab.tsx +38 -0
- package/src/panels/zones/memories-tab.tsx +37 -0
- package/src/panels/zones/reindex-status.tsx +84 -0
- package/src/panels/zones/workspaces-tab.tsx +37 -0
- package/src/panels/zones/zone-list.tsx +73 -0
- package/src/panels/zones/zones-panel.tsx +559 -0
- package/src/services/command-runner.ts +303 -0
- package/src/shared/accessibility-announcements.ts +44 -0
- package/src/shared/action-registry.ts +466 -0
- package/src/shared/brick-states.ts +91 -0
- package/src/shared/command-palette.ts +35 -0
- package/src/shared/components/announcement-bar.tsx +30 -0
- package/src/shared/components/app-confirm-dialog.tsx +29 -0
- package/src/shared/components/breadcrumb.tsx +21 -0
- package/src/shared/components/brick-gate.tsx +60 -0
- package/src/shared/components/command-output.tsx +95 -0
- package/src/shared/components/command-palette.tsx +97 -0
- package/src/shared/components/confirm-dialog.tsx +61 -0
- package/src/shared/components/diff-viewer.tsx +219 -0
- package/src/shared/components/empty-state.tsx +36 -0
- package/src/shared/components/error-bar.tsx +60 -0
- package/src/shared/components/error-boundary.tsx +53 -0
- package/src/shared/components/help-overlay.tsx +99 -0
- package/src/shared/components/identity-switcher.tsx +168 -0
- package/src/shared/components/loading-indicator.tsx +40 -0
- package/src/shared/components/pagination-bar.tsx +68 -0
- package/src/shared/components/pre-connection-screen.tsx +398 -0
- package/src/shared/components/scroll-indicator.tsx +46 -0
- package/src/shared/components/side-nav-utils.ts +68 -0
- package/src/shared/components/side-nav.tsx +287 -0
- package/src/shared/components/spinner.tsx +26 -0
- package/src/shared/components/status-bar.tsx +117 -0
- package/src/shared/components/styled-text.tsx +72 -0
- package/src/shared/components/sub-tab-bar-utils.ts +100 -0
- package/src/shared/components/sub-tab-bar.tsx +40 -0
- package/src/shared/components/tab-bar-utils.ts +36 -0
- package/src/shared/components/tab-bar.tsx +50 -0
- package/src/shared/components/text-input.tsx +73 -0
- package/src/shared/components/tooltip.tsx +53 -0
- package/src/shared/components/virtual-list.tsx +93 -0
- package/src/shared/components/welcome-screen.tsx +111 -0
- package/src/shared/hooks/use-api.ts +10 -0
- package/src/shared/hooks/use-brick-available.ts +42 -0
- package/src/shared/hooks/use-confirm.ts +66 -0
- package/src/shared/hooks/use-connection-state.ts +67 -0
- package/src/shared/hooks/use-copy.ts +31 -0
- package/src/shared/hooks/use-fresh-server.ts +62 -0
- package/src/shared/hooks/use-keyboard.ts +58 -0
- package/src/shared/hooks/use-list-navigation.ts +106 -0
- package/src/shared/hooks/use-swr.ts +117 -0
- package/src/shared/hooks/use-tab-fallback.ts +32 -0
- package/src/shared/hooks/use-text-input.ts +113 -0
- package/src/shared/hooks/use-visible-tabs.ts +61 -0
- package/src/shared/lib/circular-buffer.ts +82 -0
- package/src/shared/lib/clipboard.ts +14 -0
- package/src/shared/nav-items.ts +73 -0
- package/src/shared/navigation.ts +110 -0
- package/src/shared/status-breadcrumb.ts +74 -0
- package/src/shared/syntax-style.ts +3 -0
- package/src/shared/tab-visibility.ts +15 -0
- package/src/shared/text-style.ts +23 -0
- package/src/shared/theme.ts +179 -0
- package/src/shared/utils/format-size.ts +20 -0
- package/src/shared/utils/format-text.ts +10 -0
- package/src/shared/utils/format-time.ts +72 -0
- package/src/shared/utils/lru-cache.ts +75 -0
- package/src/stores/access-store-types.ts +154 -0
- package/src/stores/access-store.ts +674 -0
- package/src/stores/agents-store.ts +404 -0
- package/src/stores/announcement-store.ts +46 -0
- package/src/stores/api-console-store.ts +476 -0
- package/src/stores/connectors-store.ts +434 -0
- package/src/stores/create-api-action.ts +140 -0
- package/src/stores/delegation-store.ts +300 -0
- package/src/stores/error-store.ts +102 -0
- package/src/stores/events-store.ts +163 -0
- package/src/stores/files-store.ts +630 -0
- package/src/stores/first-run-store.ts +34 -0
- package/src/stores/global-store.ts +255 -0
- package/src/stores/infra-store.ts +461 -0
- package/src/stores/knowledge-store.ts +358 -0
- package/src/stores/lineage-store.ts +126 -0
- package/src/stores/mcp-store.ts +147 -0
- package/src/stores/payments-store.ts +545 -0
- package/src/stores/search-store-types.ts +155 -0
- package/src/stores/search-store.ts +656 -0
- package/src/stores/share-link-store.ts +151 -0
- package/src/stores/stack-store.ts +352 -0
- package/src/stores/ui-store.ts +161 -0
- package/src/stores/upload-store.ts +131 -0
- package/src/stores/versions-store.ts +355 -0
- package/src/stores/workflows-store.ts +402 -0
- package/src/stores/workspace-store.ts +185 -0
- package/src/stores/zones-store.ts +378 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard navigation hook wrapping OpenTUI's useKeyboard.
|
|
3
|
+
*
|
|
4
|
+
* Provides a simple key-name -> handler abstraction with cleanup on unmount.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useCallback, useRef } from "react";
|
|
8
|
+
import { useKeyboard as useOpenTuiKeyboard } from "@opentui/react";
|
|
9
|
+
import type { KeyEvent } from "@opentui/core";
|
|
10
|
+
|
|
11
|
+
export type KeyHandler = () => void;
|
|
12
|
+
export type KeyBindings = Readonly<Record<string, KeyHandler>>;
|
|
13
|
+
export type UnhandledKeyHandler = (key: string) => void;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Register keyboard shortcut handlers.
|
|
17
|
+
*
|
|
18
|
+
* Key format matches OpenTUI key event names:
|
|
19
|
+
* - Letters: "a", "b", ..., "z"
|
|
20
|
+
* - Numbers: "1", "2", "3"
|
|
21
|
+
* - Navigation: "up", "down", "left", "right"
|
|
22
|
+
* - Actions: "return", "escape", "tab", "space", "backspace"
|
|
23
|
+
* - Modified keys: prefix with "ctrl+" or "shift+" (e.g. "ctrl+c")
|
|
24
|
+
*
|
|
25
|
+
* Bindings are cleaned up automatically on unmount.
|
|
26
|
+
*
|
|
27
|
+
* @param bindings - Map of key names to handler functions
|
|
28
|
+
* @param onUnhandled - Optional handler for keys not in the bindings map.
|
|
29
|
+
* Receives the raw key name. Useful for text input mode where any
|
|
30
|
+
* printable character should be captured.
|
|
31
|
+
*/
|
|
32
|
+
export function useKeyboard(
|
|
33
|
+
bindings: KeyBindings,
|
|
34
|
+
onUnhandled?: UnhandledKeyHandler,
|
|
35
|
+
): void {
|
|
36
|
+
const bindingsRef = useRef(bindings);
|
|
37
|
+
bindingsRef.current = bindings;
|
|
38
|
+
|
|
39
|
+
const onUnhandledRef = useRef(onUnhandled);
|
|
40
|
+
onUnhandledRef.current = onUnhandled;
|
|
41
|
+
|
|
42
|
+
const handler = useCallback((key: KeyEvent) => {
|
|
43
|
+
// Build normalized key string
|
|
44
|
+
let keyStr = key.name;
|
|
45
|
+
if (key.ctrl) keyStr = `ctrl+${keyStr}`;
|
|
46
|
+
if (key.shift) keyStr = `shift+${keyStr}`;
|
|
47
|
+
if (key.meta) keyStr = `meta+${keyStr}`;
|
|
48
|
+
|
|
49
|
+
const fn = bindingsRef.current[keyStr];
|
|
50
|
+
if (fn) {
|
|
51
|
+
fn();
|
|
52
|
+
} else if (onUnhandledRef.current) {
|
|
53
|
+
onUnhandledRef.current(key.name);
|
|
54
|
+
}
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
useOpenTuiKeyboard(handler);
|
|
58
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared list navigation logic.
|
|
3
|
+
*
|
|
4
|
+
* Pure helper functions for cursor-in-list navigation used across all panels.
|
|
5
|
+
* These are extracted from the repeated j/k/gg/G pattern to ensure consistency.
|
|
6
|
+
*
|
|
7
|
+
* For React integration, panels use these helpers within their useKeyboard bindings.
|
|
8
|
+
*
|
|
9
|
+
* @see Issue #3066 Architecture Decision 6A
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Pure navigation helpers
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Clamp an index to valid range [0, length - 1].
|
|
18
|
+
* Returns 0 for empty lists.
|
|
19
|
+
*/
|
|
20
|
+
export function clampIndex(index: number, length: number): number {
|
|
21
|
+
if (length <= 0) return 0;
|
|
22
|
+
return Math.max(0, Math.min(length - 1, index));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Move index by delta, clamping to valid range.
|
|
27
|
+
*
|
|
28
|
+
* @param current - Current selected index
|
|
29
|
+
* @param delta - Movement (+1 = down, -1 = up, +10 = page down, etc.)
|
|
30
|
+
* @param length - Total number of items
|
|
31
|
+
*/
|
|
32
|
+
export function moveIndex(current: number, delta: number, length: number): number {
|
|
33
|
+
return clampIndex(current + delta, length);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Jump to the first item (gg in vim).
|
|
38
|
+
*/
|
|
39
|
+
export function jumpToStart(): number {
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Jump to the last item (G in vim).
|
|
45
|
+
*/
|
|
46
|
+
export function jumpToEnd(length: number): number {
|
|
47
|
+
if (length <= 0) return 0;
|
|
48
|
+
return length - 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// Keybinding builder
|
|
53
|
+
// =============================================================================
|
|
54
|
+
|
|
55
|
+
export interface ListNavigationOptions {
|
|
56
|
+
/** Get the current selected index. */
|
|
57
|
+
readonly getIndex: () => number;
|
|
58
|
+
/** Set the new selected index. */
|
|
59
|
+
readonly setIndex: (index: number) => void;
|
|
60
|
+
/** Get the current list length. */
|
|
61
|
+
readonly getLength: () => number;
|
|
62
|
+
/** Called when Enter is pressed on the selected item. */
|
|
63
|
+
readonly onSelect?: (index: number) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build keyboard bindings for standard list navigation.
|
|
68
|
+
*
|
|
69
|
+
* Returns a Record<string, () => void> suitable for useKeyboard().
|
|
70
|
+
* Panels merge these with their own panel-specific bindings.
|
|
71
|
+
*
|
|
72
|
+
* Includes: j/k (move), up/down (move), gg/G (jump), Enter (select)
|
|
73
|
+
*/
|
|
74
|
+
export function listNavigationBindings(
|
|
75
|
+
options: ListNavigationOptions,
|
|
76
|
+
): Record<string, () => void> {
|
|
77
|
+
const { getIndex, setIndex, getLength, onSelect } = options;
|
|
78
|
+
|
|
79
|
+
const move = (delta: number) => () => {
|
|
80
|
+
setIndex(moveIndex(getIndex(), delta, getLength()));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const bindings: Record<string, () => void> = {
|
|
84
|
+
j: move(1),
|
|
85
|
+
k: move(-1),
|
|
86
|
+
down: move(1),
|
|
87
|
+
up: move(-1),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// g = jump to start, G (shift+g) = jump to end
|
|
91
|
+
bindings["g"] = () => setIndex(jumpToStart());
|
|
92
|
+
bindings["shift+g"] = () => setIndex(jumpToEnd(getLength()));
|
|
93
|
+
|
|
94
|
+
if (onSelect) {
|
|
95
|
+
bindings["return"] = () => onSelect(getIndex());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return bindings;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Render prefix for a list item: `> ` for selected, ` ` for others.
|
|
103
|
+
*/
|
|
104
|
+
export function selectionPrefix(index: number, selectedIndex: number): string {
|
|
105
|
+
return index === selectedIndex ? "> " : " ";
|
|
106
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic stale-while-revalidate hook for data fetching.
|
|
3
|
+
*
|
|
4
|
+
* - Immediately returns cached data if available (even if stale)
|
|
5
|
+
* - Triggers background revalidation if data is stale
|
|
6
|
+
* - Reports loading/error states
|
|
7
|
+
* - Aborts in-flight requests on key change or unmount (Issue #3102)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
11
|
+
import { LruCache } from "../utils/lru-cache.js";
|
|
12
|
+
|
|
13
|
+
interface SwrOptions {
|
|
14
|
+
/** Time in ms before cached data is considered stale. Default: 30000 */
|
|
15
|
+
readonly ttlMs?: number;
|
|
16
|
+
/** Whether to fetch immediately on mount. Default: true */
|
|
17
|
+
readonly enabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface SwrResult<T> {
|
|
21
|
+
readonly data: T | undefined;
|
|
22
|
+
readonly error: Error | undefined;
|
|
23
|
+
readonly isLoading: boolean;
|
|
24
|
+
readonly isStale: boolean;
|
|
25
|
+
readonly mutate: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Module-level cache shared across hook instances (Decision 7A)
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
/** @internal Exported for testing LRU behavior */
|
|
33
|
+
export const swrCache = new LruCache(200);
|
|
34
|
+
|
|
35
|
+
export function useSwr<T>(
|
|
36
|
+
key: string,
|
|
37
|
+
fetcher: (signal: AbortSignal) => Promise<T>,
|
|
38
|
+
options?: SwrOptions,
|
|
39
|
+
): SwrResult<T> {
|
|
40
|
+
const ttlMs = options?.ttlMs ?? 30_000;
|
|
41
|
+
const enabled = options?.enabled ?? true;
|
|
42
|
+
|
|
43
|
+
const [data, setData] = useState<T | undefined>(() => {
|
|
44
|
+
const cached = swrCache.get(key) as { data: T; fetchedAt: number } | undefined;
|
|
45
|
+
return cached?.data;
|
|
46
|
+
});
|
|
47
|
+
const [error, setError] = useState<Error | undefined>();
|
|
48
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
49
|
+
const fetcherRef = useRef(fetcher);
|
|
50
|
+
fetcherRef.current = fetcher;
|
|
51
|
+
|
|
52
|
+
// Track the active key so in-flight fetches for stale keys are discarded
|
|
53
|
+
const activeKeyRef = useRef(key);
|
|
54
|
+
activeKeyRef.current = key;
|
|
55
|
+
|
|
56
|
+
// Track the active AbortController for cancellation (Issue #3102, Decision 3A)
|
|
57
|
+
const controllerRef = useRef<AbortController | null>(null);
|
|
58
|
+
|
|
59
|
+
// Reset data to cached value (or undefined) when key changes
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const cached = swrCache.get(key) as { data: T; fetchedAt: number } | undefined;
|
|
62
|
+
setData(cached?.data);
|
|
63
|
+
}, [key]);
|
|
64
|
+
|
|
65
|
+
const isStale = (() => {
|
|
66
|
+
const cached = swrCache.get(key);
|
|
67
|
+
if (!cached) return true;
|
|
68
|
+
return Date.now() - cached.fetchedAt > ttlMs;
|
|
69
|
+
})();
|
|
70
|
+
|
|
71
|
+
const doFetch = useCallback(async () => {
|
|
72
|
+
const fetchKey = key; // capture for closure
|
|
73
|
+
|
|
74
|
+
// Abort any previous in-flight fetch
|
|
75
|
+
controllerRef.current?.abort();
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
controllerRef.current = controller;
|
|
78
|
+
|
|
79
|
+
setIsLoading(true);
|
|
80
|
+
setError(undefined);
|
|
81
|
+
try {
|
|
82
|
+
const result = await fetcherRef.current(controller.signal);
|
|
83
|
+
// Only update if this key is still the active one
|
|
84
|
+
if (activeKeyRef.current !== fetchKey) return;
|
|
85
|
+
swrCache.set(key, { data: result, fetchedAt: Date.now() });
|
|
86
|
+
setData(result);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
// Suppress AbortError — it's expected when we cancel
|
|
89
|
+
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
90
|
+
if (activeKeyRef.current !== fetchKey) return;
|
|
91
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
92
|
+
} finally {
|
|
93
|
+
if (activeKeyRef.current === fetchKey) {
|
|
94
|
+
setIsLoading(false);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}, [key]);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!enabled) return;
|
|
101
|
+
if (isStale) {
|
|
102
|
+
doFetch();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Abort in-flight request on unmount or key change
|
|
106
|
+
return () => {
|
|
107
|
+
controllerRef.current?.abort();
|
|
108
|
+
};
|
|
109
|
+
}, [key, enabled, isStale, doFetch]);
|
|
110
|
+
|
|
111
|
+
const mutate = useCallback(() => {
|
|
112
|
+
swrCache.delete(key);
|
|
113
|
+
doFetch();
|
|
114
|
+
}, [key, doFetch]);
|
|
115
|
+
|
|
116
|
+
return { data, error, isLoading, isStale, mutate };
|
|
117
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook to fall back to the first visible tab when the active tab
|
|
3
|
+
* becomes hidden (e.g. its brick was disabled).
|
|
4
|
+
*
|
|
5
|
+
* Replaces the inline useEffect that was duplicated across 6+ panels
|
|
6
|
+
* with inconsistent dependency arrays.
|
|
7
|
+
*
|
|
8
|
+
* @see Issue #3498
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useEffect } from "react";
|
|
12
|
+
import { tabFallback } from "../components/sub-tab-bar-utils.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* If activeTab is not in visibleTabs, switch to the first visible tab.
|
|
16
|
+
*
|
|
17
|
+
* Uses visibleIds.join(",") as the dependency key to match the established
|
|
18
|
+
* codebase convention (see zones-panel, events-panel, etc.).
|
|
19
|
+
*/
|
|
20
|
+
export function useTabFallback<T extends string>(
|
|
21
|
+
visibleTabs: readonly { readonly id: T }[],
|
|
22
|
+
activeTab: T,
|
|
23
|
+
setActiveTab: (tab: T) => void,
|
|
24
|
+
): void {
|
|
25
|
+
const visibleIds = visibleTabs.map((t) => t.id);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const target = tabFallback(visibleIds, activeTab);
|
|
29
|
+
if (target !== null) setActiveTab(target);
|
|
30
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
31
|
+
}, [visibleIds.join(","), activeTab]);
|
|
32
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared hook for text input mode in terminal panels.
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates the repeated pattern of: activate input mode → capture
|
|
5
|
+
* keystrokes into a buffer → submit on Enter → cancel on Escape →
|
|
6
|
+
* delete on Backspace.
|
|
7
|
+
*
|
|
8
|
+
* Panels integrate this with useKeyboard by spreading inputBindings
|
|
9
|
+
* when the input is active, and passing onUnhandled as the second arg.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const textInput = useTextInput({
|
|
14
|
+
* onSubmit: (val) => doSearch(val),
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* useKeyboard(
|
|
18
|
+
* textInput.active
|
|
19
|
+
* ? textInput.inputBindings
|
|
20
|
+
* : { ...normalBindings },
|
|
21
|
+
* textInput.active ? textInput.onUnhandled : undefined,
|
|
22
|
+
* );
|
|
23
|
+
*
|
|
24
|
+
* // Render: textInput.active ? `Search: ${textInput.buffer}█` : "..."
|
|
25
|
+
* // Activate: textInput.activate(existingQuery)
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { useState, useCallback, useRef } from "react";
|
|
30
|
+
|
|
31
|
+
export interface UseTextInputOptions {
|
|
32
|
+
/** Called when Enter is pressed. Receives the trimmed buffer value. */
|
|
33
|
+
readonly onSubmit: (value: string) => void;
|
|
34
|
+
/** Called when Escape is pressed. Defaults to no-op. */
|
|
35
|
+
readonly onCancel?: () => void;
|
|
36
|
+
/**
|
|
37
|
+
* Character filter — return true to accept, false to reject.
|
|
38
|
+
* Only called for single printable characters, not "space".
|
|
39
|
+
* Defaults to accepting all characters.
|
|
40
|
+
*/
|
|
41
|
+
readonly filter?: (char: string) => boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface UseTextInputReturn {
|
|
45
|
+
/** Whether input mode is currently active. */
|
|
46
|
+
readonly active: boolean;
|
|
47
|
+
/** Current buffer contents. */
|
|
48
|
+
readonly buffer: string;
|
|
49
|
+
/** Activate input mode with an optional initial value. */
|
|
50
|
+
readonly activate: (initialValue?: string) => void;
|
|
51
|
+
/** Deactivate input mode programmatically (clears buffer). */
|
|
52
|
+
readonly deactivate: () => void;
|
|
53
|
+
/**
|
|
54
|
+
* Keyboard bindings for input mode.
|
|
55
|
+
* Spread into useKeyboard when active.
|
|
56
|
+
* Includes: return, escape, backspace.
|
|
57
|
+
*/
|
|
58
|
+
readonly inputBindings: Readonly<Record<string, () => void>>;
|
|
59
|
+
/**
|
|
60
|
+
* Unhandled key handler for capturing printable characters.
|
|
61
|
+
* Pass as the second arg to useKeyboard when active.
|
|
62
|
+
*/
|
|
63
|
+
readonly onUnhandled: (keyName: string) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function useTextInput(options: UseTextInputOptions): UseTextInputReturn {
|
|
67
|
+
const [active, setActive] = useState(false);
|
|
68
|
+
const [buffer, setBuffer] = useState("");
|
|
69
|
+
const optionsRef = useRef(options);
|
|
70
|
+
optionsRef.current = options;
|
|
71
|
+
|
|
72
|
+
const activate = useCallback((initialValue?: string) => {
|
|
73
|
+
setBuffer(initialValue ?? "");
|
|
74
|
+
setActive(true);
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
const deactivate = useCallback(() => {
|
|
78
|
+
setBuffer("");
|
|
79
|
+
setActive(false);
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const inputBindings: Record<string, () => void> = {
|
|
83
|
+
return: () => {
|
|
84
|
+
setActive(false);
|
|
85
|
+
optionsRef.current.onSubmit(buffer);
|
|
86
|
+
},
|
|
87
|
+
escape: () => {
|
|
88
|
+
setActive(false);
|
|
89
|
+
setBuffer("");
|
|
90
|
+
optionsRef.current.onCancel?.();
|
|
91
|
+
},
|
|
92
|
+
backspace: () => {
|
|
93
|
+
setBuffer((b) => b.slice(0, -1));
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const onUnhandled = useCallback(
|
|
98
|
+
(keyName: string) => {
|
|
99
|
+
if (!active) return;
|
|
100
|
+
if (keyName === "space") {
|
|
101
|
+
setBuffer((b) => b + " ");
|
|
102
|
+
} else if (keyName.length === 1) {
|
|
103
|
+
const filter = optionsRef.current.filter;
|
|
104
|
+
if (!filter || filter(keyName)) {
|
|
105
|
+
setBuffer((b) => b + keyName);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
[active],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return { active, buffer, activate, deactivate, inputBindings, onUnhandled };
|
|
113
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared hook for filtering panel sub-tabs based on enabled bricks.
|
|
3
|
+
*
|
|
4
|
+
* Provides the TabDef type and useVisibleTabs hook (Decision 2A: hybrid
|
|
5
|
+
* shared type, per-panel ownership).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useMemo } from "react";
|
|
9
|
+
import { useGlobalStore } from "../../stores/global-store.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Definition of a sub-tab within a panel.
|
|
13
|
+
*
|
|
14
|
+
* Each panel defines its own ALL_TABS: TabDef[] array. The useVisibleTabs
|
|
15
|
+
* hook filters this array based on which bricks are currently enabled.
|
|
16
|
+
*/
|
|
17
|
+
export interface TabDef<T extends string = string> {
|
|
18
|
+
/** Tab identifier (matches the panel's tab union type). */
|
|
19
|
+
readonly id: T;
|
|
20
|
+
/** Display label shown in the sub-tab bar. */
|
|
21
|
+
readonly label: string;
|
|
22
|
+
/**
|
|
23
|
+
* Brick(s) required for this tab to be visible.
|
|
24
|
+
* - string: single brick must be enabled
|
|
25
|
+
* - string[]: any of the listed bricks must be enabled (OR semantics)
|
|
26
|
+
* - null: tab is always visible (no brick dependency)
|
|
27
|
+
*/
|
|
28
|
+
readonly brick: string | readonly string[] | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function filterTabs<T extends string>(
|
|
32
|
+
allTabs: readonly TabDef<T>[],
|
|
33
|
+
enabledBricks: readonly string[],
|
|
34
|
+
featuresLoaded: boolean,
|
|
35
|
+
): readonly TabDef<T>[] {
|
|
36
|
+
if (!featuresLoaded) return allTabs;
|
|
37
|
+
|
|
38
|
+
return allTabs.filter((tab) => {
|
|
39
|
+
if (tab.brick === null) return true;
|
|
40
|
+
if (typeof tab.brick === "string") return enabledBricks.includes(tab.brick);
|
|
41
|
+
return tab.brick.some((b) => enabledBricks.includes(b));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Filter a panel's tab definitions to only those whose required bricks
|
|
47
|
+
* are currently enabled.
|
|
48
|
+
*
|
|
49
|
+
* Uses shallow comparison on enabledBricks to avoid unnecessary re-renders
|
|
50
|
+
* (Decision 15A).
|
|
51
|
+
*/
|
|
52
|
+
export function useVisibleTabs<T extends string>(
|
|
53
|
+
allTabs: readonly TabDef<T>[],
|
|
54
|
+
): readonly TabDef<T>[] {
|
|
55
|
+
const enabledBricks = useGlobalStore((s) => s.enabledBricks);
|
|
56
|
+
const featuresLoaded = useGlobalStore((s) => s.featuresLoaded);
|
|
57
|
+
|
|
58
|
+
return useMemo(() => {
|
|
59
|
+
return filterTabs(allTabs, enabledBricks, featuresLoaded);
|
|
60
|
+
}, [allTabs, enabledBricks, featuresLoaded]);
|
|
61
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixed-capacity circular buffer for bounded collections.
|
|
3
|
+
*
|
|
4
|
+
* Primary use: SSE event stream (prevents unbounded memory growth).
|
|
5
|
+
* When the buffer is full, the oldest item is silently evicted.
|
|
6
|
+
*
|
|
7
|
+
* @see Issue #3066, Phase D9 (SSE stream bounding)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class CircularBuffer<T> {
|
|
11
|
+
private readonly _items: Array<T | undefined>;
|
|
12
|
+
private _head = 0; // next write position
|
|
13
|
+
private _size = 0;
|
|
14
|
+
private _totalAdded = 0;
|
|
15
|
+
|
|
16
|
+
constructor(readonly capacity: number) {
|
|
17
|
+
if (capacity < 1) throw new Error("CircularBuffer capacity must be >= 1");
|
|
18
|
+
this._items = new Array<T | undefined>(capacity);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Number of items currently in the buffer. */
|
|
22
|
+
get size(): number {
|
|
23
|
+
return this._size;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Total items ever added (including evicted). */
|
|
27
|
+
get totalAdded(): number {
|
|
28
|
+
return this._totalAdded;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Number of items that have been evicted. */
|
|
32
|
+
get evictedCount(): number {
|
|
33
|
+
return this._totalAdded - this._size;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Whether the buffer has evicted any items. */
|
|
37
|
+
get hasOverflowed(): boolean {
|
|
38
|
+
return this._totalAdded > this.capacity;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Add an item. If full, the oldest item is evicted. */
|
|
42
|
+
push(item: T): void {
|
|
43
|
+
this._items[this._head] = item;
|
|
44
|
+
this._head = (this._head + 1) % this.capacity;
|
|
45
|
+
this._totalAdded++;
|
|
46
|
+
if (this._size < this.capacity) {
|
|
47
|
+
this._size++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Get item by index (0 = oldest). Throws if out of range. */
|
|
52
|
+
get(index: number): T {
|
|
53
|
+
if (index < 0 || index >= this._size) {
|
|
54
|
+
throw new RangeError(`Index ${index} out of range [0, ${this._size})`);
|
|
55
|
+
}
|
|
56
|
+
const start = this._size < this.capacity
|
|
57
|
+
? 0
|
|
58
|
+
: this._head; // head points to oldest when full
|
|
59
|
+
const actual = (start + index) % this.capacity;
|
|
60
|
+
return this._items[actual] as T;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Remove all items and reset counters. */
|
|
64
|
+
clear(): void {
|
|
65
|
+
this._items.fill(undefined);
|
|
66
|
+
this._head = 0;
|
|
67
|
+
this._size = 0;
|
|
68
|
+
this._totalAdded = 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Iterate from oldest to newest. */
|
|
72
|
+
*[Symbol.iterator](): Iterator<T> {
|
|
73
|
+
for (let i = 0; i < this._size; i++) {
|
|
74
|
+
yield this.get(i);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Return all items as an array (oldest first). */
|
|
79
|
+
toArray(): T[] {
|
|
80
|
+
return Array.from(this);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clipboard support via OSC 52 terminal escape sequence.
|
|
3
|
+
* @see Issue #3066, Phase E3
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Copy text to the system clipboard using OSC 52.
|
|
8
|
+
* Works in terminals that support OSC 52 (most modern terminals).
|
|
9
|
+
* Silently no-ops if the terminal doesn't support it.
|
|
10
|
+
*/
|
|
11
|
+
export function copyToClipboard(text: string): void {
|
|
12
|
+
const encoded = Buffer.from(text).toString("base64");
|
|
13
|
+
process.stdout.write(`\x1b]52;c;${encoded}\x07`);
|
|
14
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel navigation items — single source of truth for SideNav rendering,
|
|
3
|
+
* keyboard shortcuts, and panel-to-store indicator mapping.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from app.tsx to keep layout concerns separate from data.
|
|
6
|
+
*
|
|
7
|
+
* @see Issue #3497
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PanelId } from "../stores/global-store.js";
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
export interface NavItem {
|
|
17
|
+
readonly id: PanelId;
|
|
18
|
+
/** Short label for collapsed / narrow terminals */
|
|
19
|
+
readonly label: string;
|
|
20
|
+
/** Full label for wide terminals */
|
|
21
|
+
readonly fullLabel: string;
|
|
22
|
+
/** Keyboard shortcut displayed in the nav */
|
|
23
|
+
readonly shortcut: string;
|
|
24
|
+
/** Single-char icon for collapsed mode */
|
|
25
|
+
readonly icon: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Nav items
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
export const NAV_ITEMS: readonly NavItem[] = [
|
|
33
|
+
{ id: "files", label: "Files", fullLabel: "Files", shortcut: "1", icon: "□" },
|
|
34
|
+
{ id: "versions", label: "Ver", fullLabel: "Versions", shortcut: "2", icon: "◎" },
|
|
35
|
+
{ id: "agents", label: "Agent", fullLabel: "Agents", shortcut: "3", icon: "◇" },
|
|
36
|
+
{ id: "zones", label: "Zone", fullLabel: "Zones", shortcut: "4", icon: "⬡" },
|
|
37
|
+
{ id: "access", label: "ACL", fullLabel: "Access", shortcut: "5", icon: "⊕" },
|
|
38
|
+
{ id: "payments", label: "Pay", fullLabel: "Payments", shortcut: "6", icon: "◈" },
|
|
39
|
+
{ id: "search", label: "Find", fullLabel: "Search", shortcut: "7", icon: "⊘" },
|
|
40
|
+
{ id: "workflows", label: "Flow", fullLabel: "Workflows", shortcut: "8", icon: "⟲" },
|
|
41
|
+
{ id: "infrastructure", label: "Event", fullLabel: "Events", shortcut: "9", icon: "◉" },
|
|
42
|
+
{ id: "console", label: "CLI", fullLabel: "Console", shortcut: "0", icon: "▶" },
|
|
43
|
+
{ id: "connectors", label: "Conn", fullLabel: "Connectors", shortcut: "C", icon: "⊞" },
|
|
44
|
+
{ id: "stack", label: "Stack", fullLabel: "Stack", shortcut: "S", icon: "▦" },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// Per-panel indicator selectors (Decision 1A: thin adapter map)
|
|
49
|
+
//
|
|
50
|
+
// Only stores that expose loading/error state are wired up.
|
|
51
|
+
// Panels without indicators simply return false.
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Zustand-compatible selector: reads loading state from a store.
|
|
56
|
+
* Returns a boolean for use with Object.is equality (Decision 4A).
|
|
57
|
+
*/
|
|
58
|
+
export type IndicatorSelector<S> = (state: S) => boolean;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Mapping from PanelId to the store hook + selector needed to read
|
|
62
|
+
* loading and error state. Stored as getState() thunks so they can
|
|
63
|
+
* be called outside React (in tests) or inside hooks.
|
|
64
|
+
*
|
|
65
|
+
* Lazy-imported in side-nav.tsx to avoid circular deps.
|
|
66
|
+
*/
|
|
67
|
+
export interface PanelIndicators {
|
|
68
|
+
readonly loading: boolean;
|
|
69
|
+
readonly error: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Default indicators for panels whose stores don't expose loading/error. */
|
|
73
|
+
export const NO_INDICATORS: PanelIndicators = { loading: false, error: false };
|