@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.
Files changed (193) hide show
  1. package/README.md +30 -0
  2. package/package.json +48 -0
  3. package/src/app.tsx +349 -0
  4. package/src/index.tsx +137 -0
  5. package/src/opentui-env.d.ts +61 -0
  6. package/src/panels/access/access-panel.tsx +597 -0
  7. package/src/panels/access/alert-list.tsx +77 -0
  8. package/src/panels/access/constraint-creator.tsx +128 -0
  9. package/src/panels/access/constraint-list.tsx +72 -0
  10. package/src/panels/access/credential-list.tsx +68 -0
  11. package/src/panels/access/delegation-chain-view.tsx +110 -0
  12. package/src/panels/access/delegation-completer.tsx +120 -0
  13. package/src/panels/access/delegation-creator.tsx +237 -0
  14. package/src/panels/access/delegation-list.tsx +74 -0
  15. package/src/panels/access/fraud-score-view.tsx +94 -0
  16. package/src/panels/access/manifest-creator.tsx +167 -0
  17. package/src/panels/access/manifest-list.tsx +105 -0
  18. package/src/panels/access/namespace-config-view.tsx +525 -0
  19. package/src/panels/access/permission-checker.tsx +231 -0
  20. package/src/panels/agents/agent-status-view.tsx +196 -0
  21. package/src/panels/agents/agents-panel.tsx +493 -0
  22. package/src/panels/agents/delegation-list.tsx +154 -0
  23. package/src/panels/agents/inbox-view.tsx +96 -0
  24. package/src/panels/agents/trajectories-tab.tsx +40 -0
  25. package/src/panels/api-console/api-console-panel.tsx +189 -0
  26. package/src/panels/api-console/codegen-viewer.tsx +36 -0
  27. package/src/panels/api-console/codegen.ts +112 -0
  28. package/src/panels/api-console/endpoint-list.tsx +57 -0
  29. package/src/panels/api-console/request-builder.tsx +69 -0
  30. package/src/panels/api-console/response-viewer.tsx +54 -0
  31. package/src/panels/connectors/available-tab.tsx +357 -0
  32. package/src/panels/connectors/connector-row.tsx +121 -0
  33. package/src/panels/connectors/connectors-panel.tsx +88 -0
  34. package/src/panels/connectors/error-parser.ts +116 -0
  35. package/src/panels/connectors/mounted-tab.tsx +179 -0
  36. package/src/panels/connectors/skills-tab.tsx +235 -0
  37. package/src/panels/connectors/template-generator.ts +211 -0
  38. package/src/panels/connectors/write-tab.tsx +514 -0
  39. package/src/panels/events/audit-tab.tsx +69 -0
  40. package/src/panels/events/audit-trail.tsx +75 -0
  41. package/src/panels/events/connector-detail.tsx +49 -0
  42. package/src/panels/events/connector-list.tsx +73 -0
  43. package/src/panels/events/connectors-tab.tsx +92 -0
  44. package/src/panels/events/event-replay.tsx +80 -0
  45. package/src/panels/events/events-panel.tsx +414 -0
  46. package/src/panels/events/events-tab.tsx +212 -0
  47. package/src/panels/events/lock-list.tsx +54 -0
  48. package/src/panels/events/locks-tab.tsx +103 -0
  49. package/src/panels/events/mcl-replay.tsx +77 -0
  50. package/src/panels/events/mcl-tab.tsx +83 -0
  51. package/src/panels/events/operations-tab-wrapper.tsx +62 -0
  52. package/src/panels/events/operations-tab.tsx +41 -0
  53. package/src/panels/events/replay-tab.tsx +76 -0
  54. package/src/panels/events/secrets-audit.tsx +64 -0
  55. package/src/panels/events/secrets-tab.tsx +75 -0
  56. package/src/panels/events/subscription-list.tsx +54 -0
  57. package/src/panels/events/subscriptions-tab.tsx +82 -0
  58. package/src/panels/files/file-aspects.tsx +93 -0
  59. package/src/panels/files/file-editor.tsx +160 -0
  60. package/src/panels/files/file-explorer-keybindings.ts +468 -0
  61. package/src/panels/files/file-explorer-panel.tsx +545 -0
  62. package/src/panels/files/file-lineage.tsx +163 -0
  63. package/src/panels/files/file-list-item.tsx +28 -0
  64. package/src/panels/files/file-metadata.tsx +62 -0
  65. package/src/panels/files/file-preview.tsx +108 -0
  66. package/src/panels/files/file-schema.tsx +89 -0
  67. package/src/panels/files/file-tree-node.tsx +44 -0
  68. package/src/panels/files/file-tree.tsx +169 -0
  69. package/src/panels/files/share-links-tab.tsx +33 -0
  70. package/src/panels/files/uploads-tab.tsx +45 -0
  71. package/src/panels/payments/approval-list.tsx +83 -0
  72. package/src/panels/payments/balance-card.tsx +43 -0
  73. package/src/panels/payments/budget-card.tsx +70 -0
  74. package/src/panels/payments/payments-panel.tsx +451 -0
  75. package/src/panels/payments/policy-list.tsx +64 -0
  76. package/src/panels/payments/reservation-list.tsx +78 -0
  77. package/src/panels/payments/transaction-list.tsx +103 -0
  78. package/src/panels/payments/transfer-form.tsx +109 -0
  79. package/src/panels/search/column-search.tsx +79 -0
  80. package/src/panels/search/knowledge-view.tsx +100 -0
  81. package/src/panels/search/memory-list.tsx +197 -0
  82. package/src/panels/search/playbook-list.tsx +77 -0
  83. package/src/panels/search/rlm-answer-view.tsx +105 -0
  84. package/src/panels/search/search-panel.tsx +405 -0
  85. package/src/panels/search/search-results.tsx +116 -0
  86. package/src/panels/stack/stack-panel.tsx +474 -0
  87. package/src/panels/versions/conflicts-tab.tsx +59 -0
  88. package/src/panels/versions/entry-detail.tsx +89 -0
  89. package/src/panels/versions/transaction-actions.tsx +34 -0
  90. package/src/panels/versions/transaction-list.tsx +90 -0
  91. package/src/panels/versions/versions-panel.tsx +276 -0
  92. package/src/panels/workflows/execution-list.tsx +102 -0
  93. package/src/panels/workflows/scheduler-view.tsx +135 -0
  94. package/src/panels/workflows/workflow-list.tsx +88 -0
  95. package/src/panels/workflows/workflows-panel.tsx +295 -0
  96. package/src/panels/zones/brick-detail.tsx +136 -0
  97. package/src/panels/zones/brick-list.tsx +56 -0
  98. package/src/panels/zones/cache-tab.tsx +118 -0
  99. package/src/panels/zones/drift-view.tsx +97 -0
  100. package/src/panels/zones/mcp-mounts-tab.tsx +38 -0
  101. package/src/panels/zones/memories-tab.tsx +37 -0
  102. package/src/panels/zones/reindex-status.tsx +84 -0
  103. package/src/panels/zones/workspaces-tab.tsx +37 -0
  104. package/src/panels/zones/zone-list.tsx +73 -0
  105. package/src/panels/zones/zones-panel.tsx +559 -0
  106. package/src/services/command-runner.ts +303 -0
  107. package/src/shared/accessibility-announcements.ts +44 -0
  108. package/src/shared/action-registry.ts +466 -0
  109. package/src/shared/brick-states.ts +91 -0
  110. package/src/shared/command-palette.ts +35 -0
  111. package/src/shared/components/announcement-bar.tsx +30 -0
  112. package/src/shared/components/app-confirm-dialog.tsx +29 -0
  113. package/src/shared/components/breadcrumb.tsx +21 -0
  114. package/src/shared/components/brick-gate.tsx +60 -0
  115. package/src/shared/components/command-output.tsx +95 -0
  116. package/src/shared/components/command-palette.tsx +97 -0
  117. package/src/shared/components/confirm-dialog.tsx +61 -0
  118. package/src/shared/components/diff-viewer.tsx +219 -0
  119. package/src/shared/components/empty-state.tsx +36 -0
  120. package/src/shared/components/error-bar.tsx +60 -0
  121. package/src/shared/components/error-boundary.tsx +53 -0
  122. package/src/shared/components/help-overlay.tsx +99 -0
  123. package/src/shared/components/identity-switcher.tsx +168 -0
  124. package/src/shared/components/loading-indicator.tsx +40 -0
  125. package/src/shared/components/pagination-bar.tsx +68 -0
  126. package/src/shared/components/pre-connection-screen.tsx +398 -0
  127. package/src/shared/components/scroll-indicator.tsx +46 -0
  128. package/src/shared/components/side-nav-utils.ts +68 -0
  129. package/src/shared/components/side-nav.tsx +287 -0
  130. package/src/shared/components/spinner.tsx +26 -0
  131. package/src/shared/components/status-bar.tsx +117 -0
  132. package/src/shared/components/styled-text.tsx +72 -0
  133. package/src/shared/components/sub-tab-bar-utils.ts +100 -0
  134. package/src/shared/components/sub-tab-bar.tsx +40 -0
  135. package/src/shared/components/tab-bar-utils.ts +36 -0
  136. package/src/shared/components/tab-bar.tsx +50 -0
  137. package/src/shared/components/text-input.tsx +73 -0
  138. package/src/shared/components/tooltip.tsx +53 -0
  139. package/src/shared/components/virtual-list.tsx +93 -0
  140. package/src/shared/components/welcome-screen.tsx +111 -0
  141. package/src/shared/hooks/use-api.ts +10 -0
  142. package/src/shared/hooks/use-brick-available.ts +42 -0
  143. package/src/shared/hooks/use-confirm.ts +66 -0
  144. package/src/shared/hooks/use-connection-state.ts +67 -0
  145. package/src/shared/hooks/use-copy.ts +31 -0
  146. package/src/shared/hooks/use-fresh-server.ts +62 -0
  147. package/src/shared/hooks/use-keyboard.ts +58 -0
  148. package/src/shared/hooks/use-list-navigation.ts +106 -0
  149. package/src/shared/hooks/use-swr.ts +117 -0
  150. package/src/shared/hooks/use-tab-fallback.ts +32 -0
  151. package/src/shared/hooks/use-text-input.ts +113 -0
  152. package/src/shared/hooks/use-visible-tabs.ts +61 -0
  153. package/src/shared/lib/circular-buffer.ts +82 -0
  154. package/src/shared/lib/clipboard.ts +14 -0
  155. package/src/shared/nav-items.ts +73 -0
  156. package/src/shared/navigation.ts +110 -0
  157. package/src/shared/status-breadcrumb.ts +74 -0
  158. package/src/shared/syntax-style.ts +3 -0
  159. package/src/shared/tab-visibility.ts +15 -0
  160. package/src/shared/text-style.ts +23 -0
  161. package/src/shared/theme.ts +179 -0
  162. package/src/shared/utils/format-size.ts +20 -0
  163. package/src/shared/utils/format-text.ts +10 -0
  164. package/src/shared/utils/format-time.ts +72 -0
  165. package/src/shared/utils/lru-cache.ts +75 -0
  166. package/src/stores/access-store-types.ts +154 -0
  167. package/src/stores/access-store.ts +674 -0
  168. package/src/stores/agents-store.ts +404 -0
  169. package/src/stores/announcement-store.ts +46 -0
  170. package/src/stores/api-console-store.ts +476 -0
  171. package/src/stores/connectors-store.ts +434 -0
  172. package/src/stores/create-api-action.ts +140 -0
  173. package/src/stores/delegation-store.ts +300 -0
  174. package/src/stores/error-store.ts +102 -0
  175. package/src/stores/events-store.ts +163 -0
  176. package/src/stores/files-store.ts +630 -0
  177. package/src/stores/first-run-store.ts +34 -0
  178. package/src/stores/global-store.ts +255 -0
  179. package/src/stores/infra-store.ts +461 -0
  180. package/src/stores/knowledge-store.ts +358 -0
  181. package/src/stores/lineage-store.ts +126 -0
  182. package/src/stores/mcp-store.ts +147 -0
  183. package/src/stores/payments-store.ts +545 -0
  184. package/src/stores/search-store-types.ts +155 -0
  185. package/src/stores/search-store.ts +656 -0
  186. package/src/stores/share-link-store.ts +151 -0
  187. package/src/stores/stack-store.ts +352 -0
  188. package/src/stores/ui-store.ts +161 -0
  189. package/src/stores/upload-store.ts +131 -0
  190. package/src/stores/versions-store.ts +355 -0
  191. package/src/stores/workflows-store.ts +402 -0
  192. package/src/stores/workspace-store.ts +185 -0
  193. package/src/stores/zones-store.ts +378 -0
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Horizontal tab bar for switching between panels.
3
+ *
4
+ * Enhanced with semantic colors (Phase A1).
5
+ */
6
+
7
+ import React from "react";
8
+ import { palette } from "../theme.js";
9
+ import { textStyle } from "../text-style.js";
10
+
11
+ export interface Tab {
12
+ readonly id: string;
13
+ readonly label: string;
14
+ readonly shortcut: string;
15
+ }
16
+
17
+ interface TabBarProps {
18
+ readonly tabs: readonly Tab[];
19
+ readonly activeTab: string;
20
+ readonly onSelect: (id: string) => void;
21
+ }
22
+
23
+ export function TabBar({ tabs, activeTab }: TabBarProps): React.ReactNode {
24
+ return (
25
+ <box height={1} width="100%">
26
+ <text>
27
+ {tabs.map((tab, index) => {
28
+ const isActive = tab.id === activeTab;
29
+ const suffix = index < tabs.length - 1 ? " │ " : "";
30
+ if (isActive) {
31
+ return (
32
+ <span key={tab.id}>
33
+ <span style={textStyle({ fg: palette.accent, bold: true })}>{"▸ "}</span>
34
+ <span style={textStyle({ fg: palette.muted })}>{`${tab.shortcut}:`}</span>
35
+ <span style={textStyle({ fg: palette.accent, bold: true })}>{tab.label}</span>
36
+ <span style={textStyle({ fg: palette.faint })}>{suffix}</span>
37
+ </span>
38
+ );
39
+ }
40
+ return (
41
+ <span key={tab.id}>
42
+ <span style={textStyle({ fg: palette.muted })}>{` ${tab.shortcut}:${tab.label}`}</span>
43
+ <span style={textStyle({ fg: palette.faint })}>{suffix}</span>
44
+ </span>
45
+ );
46
+ })}
47
+ </text>
48
+ </box>
49
+ );
50
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Shared text input component for consistent input behavior.
3
+ *
4
+ * Handles: character input, backspace, enter (submit), escape (cancel).
5
+ * Can be used standalone or composed in forms.
6
+ *
7
+ * @see Issue #3066, Phase E7
8
+ */
9
+
10
+ import React from "react";
11
+ import { useKeyboard } from "../hooks/use-keyboard.js";
12
+ import { statusColor } from "../theme.js";
13
+ import { textStyle } from "../text-style.js";
14
+
15
+ interface TextInputProps {
16
+ /** Current input value */
17
+ readonly value: string;
18
+ /** Called when value changes */
19
+ readonly onChange: (value: string) => void;
20
+ /** Called when Enter is pressed */
21
+ readonly onSubmit?: (value: string) => void;
22
+ /** Called when Escape is pressed */
23
+ readonly onCancel?: () => void;
24
+ /** Label shown before the input */
25
+ readonly label?: string;
26
+ /** Placeholder text when empty */
27
+ readonly placeholder?: string;
28
+ /** Whether input is active (receives key events) */
29
+ readonly active?: boolean;
30
+ }
31
+
32
+ export function TextInput({
33
+ value,
34
+ onChange,
35
+ onSubmit,
36
+ onCancel,
37
+ label,
38
+ placeholder,
39
+ active = true,
40
+ }: TextInputProps): React.ReactNode {
41
+ useKeyboard(
42
+ active
43
+ ? {
44
+ return: () => onSubmit?.(value),
45
+ escape: () => onCancel?.(),
46
+ backspace: () => onChange(value.slice(0, -1)),
47
+ }
48
+ : {},
49
+ active
50
+ ? (key) => {
51
+ // Only append printable characters (single char)
52
+ if (key.length === 1) {
53
+ onChange(value + key);
54
+ }
55
+ }
56
+ : undefined,
57
+ );
58
+
59
+ const displayValue = value || (placeholder ? placeholder : "");
60
+ const isDimmed = !value && placeholder;
61
+
62
+ return (
63
+ <box flexDirection="row" height={1}>
64
+ {label && (
65
+ <text style={textStyle({ fg: statusColor.info })}>{`${label}: `}</text>
66
+ )}
67
+ <text style={textStyle({ dim: !!isDimmed })}>
68
+ {displayValue}
69
+ {active && <span style={textStyle({ fg: statusColor.info })}>{"█"}</span>}
70
+ </text>
71
+ </box>
72
+ );
73
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Dismissible first-run tooltip.
3
+ *
4
+ * Shows a brief tip message at the top of a panel on first visit.
5
+ * Dismissed by any key press, tracked via the first-run store so
6
+ * it only shows once per session per key.
7
+ *
8
+ * NOTE: This tooltip is cosmetic-only. It dismisses on any key press but
9
+ * does NOT block parent keyboard handlers. This is intentional -- the
10
+ * tooltip is a hint overlay, not a modal dialog. Parent key bindings
11
+ * (j/k/tab/etc.) continue to work while the tooltip is visible.
12
+ */
13
+
14
+ import React from "react";
15
+ import { useFirstRunStore } from "../../stores/first-run-store.js";
16
+ import { useKeyboard } from "../hooks/use-keyboard.js";
17
+ import { statusColor } from "../theme.js";
18
+ import { textStyle } from "../text-style.js";
19
+
20
+ interface TooltipProps {
21
+ /** Unique key for this tooltip (e.g. "search-panel", "events-panel"). */
22
+ readonly tooltipKey: string;
23
+ /** The message to display. */
24
+ readonly message: string;
25
+ }
26
+
27
+ export function Tooltip({ tooltipKey, message }: TooltipProps): React.ReactNode {
28
+ const shouldShow = useFirstRunStore((s) => s.shouldShow);
29
+ const dismiss = useFirstRunStore((s) => s.dismiss);
30
+
31
+ const visible = shouldShow(tooltipKey);
32
+
33
+ // Dismiss on any keypress via onUnhandled only -- no explicit key bindings
34
+ // so we don't intercept j/k/tab/etc. from the parent panel.
35
+ useKeyboard(
36
+ {},
37
+ visible
38
+ ? (keyName: string) => {
39
+ if (keyName) dismiss(tooltipKey);
40
+ }
41
+ : undefined,
42
+ );
43
+
44
+ if (!visible) return null;
45
+
46
+ return (
47
+ <box height={1} width="100%">
48
+ <text style={textStyle({ fg: statusColor.info })}>
49
+ {`${message} (press any key to dismiss)`}
50
+ </text>
51
+ </box>
52
+ );
53
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Windowed list rendering for terminal UIs.
3
+ *
4
+ * Only renders the visible rows plus an overscan buffer, instead of
5
+ * materializing all items. This keeps the terminal responsive even for
6
+ * 10k+ item lists.
7
+ *
8
+ * @see Issue #3102, Decision 1A
9
+ */
10
+
11
+ import React, { useMemo } from "react";
12
+
13
+ export interface VirtualListProps<T> {
14
+ /** Full (flat) list of items. */
15
+ readonly items: readonly T[];
16
+ /** Render callback for a single item row. */
17
+ readonly renderItem: (item: T, index: number) => React.ReactNode;
18
+ /** Height of each item in terminal rows. Default: 1 */
19
+ readonly itemHeight?: number;
20
+ /** Maximum number of visible rows in the viewport. */
21
+ readonly viewportHeight: number;
22
+ /** Currently selected/focused index (drives scroll position). */
23
+ readonly selectedIndex: number;
24
+ /** Extra rows rendered above and below the viewport. Default: 5 */
25
+ readonly overscan?: number;
26
+ }
27
+
28
+ /**
29
+ * Calculate the visible window for the given parameters.
30
+ *
31
+ * Exported for unit testing the pure math separately from React rendering.
32
+ */
33
+ export function calculateWindow(
34
+ totalItems: number,
35
+ viewportHeight: number,
36
+ selectedIndex: number,
37
+ overscan: number,
38
+ ): { startIndex: number; endIndex: number; scrollOffset: number } {
39
+ if (totalItems === 0) {
40
+ return { startIndex: 0, endIndex: 0, scrollOffset: 0 };
41
+ }
42
+
43
+ // Determine the scroll offset so the selected item is visible.
44
+ // We keep a "follow" strategy: if selectedIndex would be outside the
45
+ // current viewport, we shift the scroll offset to bring it into view.
46
+ let scrollOffset = 0;
47
+
48
+ if (totalItems <= viewportHeight) {
49
+ // Everything fits — no scrolling needed
50
+ scrollOffset = 0;
51
+ } else {
52
+ // Center the selected item in the viewport, clamped to valid range
53
+ scrollOffset = Math.max(0, selectedIndex - Math.floor(viewportHeight / 2));
54
+ const maxOffset = totalItems - viewportHeight;
55
+ scrollOffset = Math.min(scrollOffset, maxOffset);
56
+ }
57
+
58
+ // Apply overscan to render a buffer above/below
59
+ const startIndex = Math.max(0, scrollOffset - overscan);
60
+ const endIndex = Math.min(totalItems, scrollOffset + viewportHeight + overscan);
61
+
62
+ return { startIndex, endIndex, scrollOffset };
63
+ }
64
+
65
+ export function VirtualList<T>({
66
+ items,
67
+ renderItem,
68
+ itemHeight = 1,
69
+ viewportHeight,
70
+ selectedIndex,
71
+ overscan = 5,
72
+ }: VirtualListProps<T>): React.ReactNode {
73
+ const { startIndex, endIndex } = useMemo(
74
+ () => calculateWindow(items.length, viewportHeight, selectedIndex, overscan),
75
+ [items.length, viewportHeight, selectedIndex, overscan],
76
+ );
77
+
78
+ // Slice the items to only the visible window
79
+ const visibleItems = useMemo(
80
+ () => items.slice(startIndex, endIndex),
81
+ [items, startIndex, endIndex],
82
+ );
83
+
84
+ if (items.length === 0) {
85
+ return null;
86
+ }
87
+
88
+ return (
89
+ <box flexDirection="column" height={viewportHeight * itemHeight} width="100%">
90
+ {visibleItems.map((item, i) => renderItem(item, startIndex + i))}
91
+ </box>
92
+ );
93
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Welcome screen shown on fresh servers.
3
+ * Offers to seed demo data or start with empty server.
4
+ */
5
+ import React, { useState } from "react";
6
+ import { useKeyboard } from "../hooks/use-keyboard.js";
7
+ import { useGlobalStore } from "../../stores/global-store.js";
8
+ import { statusColor } from "../theme.js";
9
+ import { Spinner } from "./spinner.js";
10
+ import { textStyle } from "../text-style.js";
11
+
12
+ interface WelcomeScreenProps {
13
+ readonly onDismiss: () => void;
14
+ }
15
+
16
+ export function WelcomeScreen({ onDismiss }: WelcomeScreenProps): React.ReactNode {
17
+ const client = useGlobalStore((s) => s.client);
18
+ const config = useGlobalStore((s) => s.config);
19
+ const serverVersion = useGlobalStore((s) => s.serverVersion);
20
+ const [seeding, setSeeding] = useState(false);
21
+ const [seedError, setSeedError] = useState<string | null>(null);
22
+ const [showDetails, setShowDetails] = useState(false);
23
+
24
+ useKeyboard(seeding ? {} : {
25
+ "y": async () => {
26
+ if (!client) return;
27
+ setSeeding(true);
28
+ setSeedError(null);
29
+ try {
30
+ await client.post("/api/v2/admin/demo/seed", {});
31
+ onDismiss();
32
+ } catch {
33
+ setSeeding(false);
34
+ setSeedError("Seeding failed. Press Y to retry or N to skip.");
35
+ }
36
+ },
37
+ "n": onDismiss,
38
+ "?": () => setShowDetails((prev) => !prev),
39
+ "escape": onDismiss,
40
+ });
41
+
42
+ const baseUrl = config.baseUrl ?? "localhost:2026";
43
+
44
+ return (
45
+ <box height="100%" width="100%" justifyContent="center" alignItems="center">
46
+ <box
47
+ flexDirection="column"
48
+ borderStyle="double"
49
+ width={56}
50
+ padding={1}
51
+ >
52
+ <text style={textStyle({ fg: "#00d4ff", bold: true })}>
53
+ {" _ _ _____ __ __ _ _ ____"}
54
+ </text>
55
+ <text style={textStyle({ fg: "#00b8ff", bold: true })}>
56
+ {" | \\ | | ____| \\/ | | | / ___|"}
57
+ </text>
58
+ <text style={textStyle({ fg: "#4d8eff", bold: true })}>
59
+ {" | \\| | _| >\\/< | | | \\___ \\"}
60
+ </text>
61
+ <text style={textStyle({ fg: "#8066ff", bold: true })}>
62
+ {" | |\\ | |___/ /\\ \\| |_| |___) |"}
63
+ </text>
64
+ <text style={textStyle({ fg: "#b44dff", bold: true })}>
65
+ {" |_| \\_|_____/_/ \\_\\\\___/|____/"}
66
+ </text>
67
+ <text>{""}</text>
68
+ <text style={textStyle({ dim: true })}>{` Connected to ${baseUrl}${serverVersion ? ` (v${serverVersion})` : ""}`}</text>
69
+ <text>{""}</text>
70
+ <text>{" This server has no data yet. Would you like"}</text>
71
+ <text>{" to seed demo content?"}</text>
72
+ <text>{""}</text>
73
+ {seeding ? (
74
+ <Spinner label=" Seeding demo data..." />
75
+ ) : (
76
+ <>
77
+ <text>
78
+ <span style={textStyle({ fg: statusColor.info })}>{" [Y]"}</span>
79
+ <span>{" Seed demo data (files, agents, permissions)"}</span>
80
+ </text>
81
+ <text>
82
+ <span style={textStyle({ fg: statusColor.dim })}>{" [N]"}</span>
83
+ <span>{" Start with empty server"}</span>
84
+ </text>
85
+ <text>
86
+ <span style={textStyle({ fg: statusColor.dim })}>{" [?]"}</span>
87
+ <span>{" What's in the demo data?"}</span>
88
+ </text>
89
+ </>
90
+ )}
91
+ {seedError && (
92
+ <>
93
+ <text>{""}</text>
94
+ <text style={textStyle({ fg: statusColor.error })}>{" "}{seedError}</text>
95
+ </>
96
+ )}
97
+ {showDetails && (
98
+ <>
99
+ <text>{""}</text>
100
+ <text style={textStyle({ dim: true })}>{" Demo data includes:"}</text>
101
+ <text style={textStyle({ dim: true })}>{" \u2022 12 sample files (markdown, code, data)"}</text>
102
+ <text style={textStyle({ dim: true })}>{" \u2022 2 user identities"}</text>
103
+ <text style={textStyle({ dim: true })}>{" \u2022 1 agent with trajectories"}</text>
104
+ <text style={textStyle({ dim: true })}>{" \u2022 HERB evaluation corpus"}</text>
105
+ <text style={textStyle({ dim: true })}>{" \u2022 ReBAC permission policies"}</text>
106
+ </>
107
+ )}
108
+ </box>
109
+ </box>
110
+ );
111
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Hook to get the configured FetchClient from the global store.
3
+ */
4
+
5
+ import { useGlobalStore } from "../../stores/global-store.js";
6
+ import type { FetchClient } from "@nexus-ai-fs/api-client";
7
+
8
+ export function useApi(): FetchClient | null {
9
+ return useGlobalStore((state) => state.client);
10
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Hook to check if a brick is available (enabled) in the current deployment.
3
+ *
4
+ * Returns { available, loading } to distinguish "not loaded yet" from
5
+ * "definitively disabled" (Decision 1A).
6
+ */
7
+
8
+ import { useGlobalStore } from "../../stores/global-store.js";
9
+
10
+ interface BrickAvailability {
11
+ readonly available: boolean;
12
+ readonly loading: boolean;
13
+ }
14
+
15
+ /**
16
+ * Check if a specific brick is enabled in the current deployment profile.
17
+ *
18
+ * During initial feature loading, returns { available: false, loading: true }
19
+ * to prevent flash of "not available" messages.
20
+ */
21
+ export function useBrickAvailable(brickName: string): BrickAvailability {
22
+ const enabledBricks = useGlobalStore((s) => s.enabledBricks);
23
+ const featuresLoaded = useGlobalStore((s) => s.featuresLoaded);
24
+
25
+ return {
26
+ available: enabledBricks.includes(brickName),
27
+ loading: !featuresLoaded,
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Check if any of the specified bricks are available (OR semantics).
33
+ */
34
+ export function useAnyBrickAvailable(brickNames: readonly string[]): BrickAvailability {
35
+ const enabledBricks = useGlobalStore((s) => s.enabledBricks);
36
+ const featuresLoaded = useGlobalStore((s) => s.featuresLoaded);
37
+
38
+ return {
39
+ available: brickNames.some((name) => enabledBricks.includes(name)),
40
+ loading: !featuresLoaded,
41
+ };
42
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Imperative confirmation dialog system.
3
+ *
4
+ * Usage in any component:
5
+ * ```ts
6
+ * const confirm = useConfirmStore((s) => s.confirm);
7
+ * const ok = await confirm("Delete file?", "This cannot be undone.");
8
+ * if (ok) { doDelete(); }
9
+ * ```
10
+ *
11
+ * A single <ConfirmDialog> instance at the App level reads from this store.
12
+ *
13
+ * @see Issue #3066 Architecture Decision 3A
14
+ */
15
+
16
+ import { create } from "zustand";
17
+
18
+ // =============================================================================
19
+ // Types
20
+ // =============================================================================
21
+
22
+ export interface ConfirmState {
23
+ readonly visible: boolean;
24
+ readonly title: string;
25
+ readonly message: string;
26
+ readonly resolve: ((value: boolean) => void) | null;
27
+
28
+ /**
29
+ * Show a confirmation dialog and wait for the user's response.
30
+ * Returns true if confirmed, false if cancelled.
31
+ *
32
+ * If a previous confirmation is pending, it is auto-rejected (returns false).
33
+ */
34
+ readonly confirm: (title: string, message: string) => Promise<boolean>;
35
+ }
36
+
37
+ // =============================================================================
38
+ // Store
39
+ // =============================================================================
40
+
41
+ export const useConfirmStore = create<ConfirmState>((set, get) => ({
42
+ visible: false,
43
+ title: "",
44
+ message: "",
45
+ resolve: null,
46
+
47
+ confirm: (title, message) => {
48
+ // Reject any pending confirmation
49
+ const prev = get().resolve;
50
+ if (prev) {
51
+ prev(false);
52
+ }
53
+
54
+ return new Promise<boolean>((resolve) => {
55
+ set({
56
+ visible: true,
57
+ title,
58
+ message,
59
+ resolve: (value: boolean) => {
60
+ resolve(value);
61
+ set({ visible: false, title: "", message: "", resolve: null });
62
+ },
63
+ });
64
+ });
65
+ },
66
+ }));
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Connection state detection for the PreConnectionScreen (Decision 12A).
3
+ *
4
+ * Pure function exported for testing — follows the detectFreshServer pattern.
5
+ */
6
+
7
+ import type { ConnectionStatus } from "../../stores/global-store.js";
8
+ import type { NexusClientOptions } from "@nexus-ai-fs/api-client";
9
+
10
+ /**
11
+ * Describes why the TUI cannot connect, guiding the PreConnectionScreen UI.
12
+ *
13
+ * "no-config" — No API key configured (client is null)
14
+ * "no-server" — Server unreachable (connection error)
15
+ * "auth-failed" — Server reachable but authentication failed
16
+ * "connecting" — Still trying to connect
17
+ * "ready" — Connected and authenticated
18
+ */
19
+ export type ConnectionState =
20
+ | "no-config"
21
+ | "no-server"
22
+ | "auth-failed"
23
+ | "connecting"
24
+ | "ready";
25
+
26
+ /**
27
+ * Derive a high-level connection state from store values.
28
+ * Pure function — no side effects, fully testable.
29
+ */
30
+ export function detectConnectionState(
31
+ connectionStatus: ConnectionStatus,
32
+ connectionError: string | null,
33
+ config: NexusClientOptions,
34
+ ): ConnectionState {
35
+ // No API key → client was never created
36
+ if (!config.apiKey) {
37
+ return "no-config";
38
+ }
39
+
40
+ switch (connectionStatus) {
41
+ case "connected":
42
+ return "ready";
43
+
44
+ case "connecting":
45
+ case "disconnected":
46
+ return "connecting";
47
+
48
+ case "error": {
49
+ if (!connectionError) return "no-server";
50
+
51
+ // Distinguish auth failures from network failures
52
+ const lower = connectionError.toLowerCase();
53
+ if (
54
+ lower.includes("unauthorized") ||
55
+ lower.includes("forbidden") ||
56
+ lower.includes("401") ||
57
+ lower.includes("403")
58
+ ) {
59
+ return "auth-failed";
60
+ }
61
+ return "no-server";
62
+ }
63
+
64
+ default:
65
+ return "connecting";
66
+ }
67
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Hook for clipboard copy with visual feedback.
3
+ * Shows "Copied!" flash in the status area briefly.
4
+ */
5
+ import { useState, useCallback, useRef, useEffect } from "react";
6
+ import { copyToClipboard } from "../lib/clipboard.js";
7
+ import { useAnnouncementStore } from "../../stores/announcement-store.js";
8
+ import { formatSuccessAnnouncement } from "../accessibility-announcements.js";
9
+
10
+ export function useCopy() {
11
+ const [copied, setCopied] = useState(false);
12
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
13
+ const announce = useAnnouncementStore((s) => s.announce);
14
+
15
+ // Cleanup on unmount
16
+ useEffect(() => {
17
+ return () => {
18
+ if (timerRef.current) clearTimeout(timerRef.current);
19
+ };
20
+ }, []);
21
+
22
+ const copy = useCallback((text: string) => {
23
+ copyToClipboard(text);
24
+ announce(formatSuccessAnnouncement("Copied to clipboard"), "success");
25
+ setCopied(true);
26
+ if (timerRef.current) clearTimeout(timerRef.current);
27
+ timerRef.current = setTimeout(() => setCopied(false), 1500);
28
+ }, [announce]);
29
+
30
+ return { copy, copied };
31
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Detects if the connected server is "fresh" (no user data yet).
3
+ * Used to trigger the welcome screen on first run.
4
+ */
5
+ import { useState, useEffect } from "react";
6
+ import { useGlobalStore } from "../../stores/global-store.js";
7
+
8
+ interface FreshCheckClient {
9
+ get<T>(url: string): Promise<T>;
10
+ }
11
+
12
+ /**
13
+ * Pure detection logic — exported for testing without React hooks.
14
+ * Returns true if the server has no files and no agents.
15
+ */
16
+ export async function detectFreshServer(client: FreshCheckClient): Promise<boolean> {
17
+ try {
18
+ const [files, agents] = await Promise.all([
19
+ client.get<{ entries: unknown[] }>("/api/v2/files?path=/&limit=5"),
20
+ client.get<{ agents: unknown[] }>("/api/v2/agents?limit=1&offset=0"),
21
+ ]);
22
+ const hasFiles = (files.entries?.length ?? 0) > 0;
23
+ const hasAgents = (agents.agents?.length ?? 0) > 0;
24
+ return !hasFiles && !hasAgents;
25
+ } catch {
26
+ return false; // Assume not fresh on error
27
+ }
28
+ }
29
+
30
+ export function useFreshServer(): { isFresh: boolean | null; loading: boolean } {
31
+ const client = useGlobalStore((s) => s.client);
32
+ const connectionStatus = useGlobalStore((s) => s.connectionStatus);
33
+ const [isFresh, setIsFresh] = useState<boolean | null>(null);
34
+ const [loading, setLoading] = useState(false);
35
+
36
+ useEffect(() => {
37
+ if (connectionStatus !== "connected" || !client) {
38
+ setIsFresh(null);
39
+ return;
40
+ }
41
+
42
+ let cancelled = false;
43
+ setLoading(true);
44
+
45
+ // Check if server has any user-created data
46
+ // A "fresh" server has no files beyond defaults and no agents
47
+ (async () => {
48
+ try {
49
+ const result = await detectFreshServer(client);
50
+ if (!cancelled) setIsFresh(result);
51
+ } catch {
52
+ if (!cancelled) setIsFresh(false);
53
+ } finally {
54
+ if (!cancelled) setLoading(false);
55
+ }
56
+ })();
57
+
58
+ return () => { cancelled = true; };
59
+ }, [client, connectionStatus]);
60
+
61
+ return { isFresh, loading };
62
+ }