@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,60 @@
1
+ /**
2
+ * Gate component that shows children only when required brick(s) are enabled.
3
+ *
4
+ * When the brick is disabled, shows a standardized "not available" message
5
+ * with profile info and mount guidance (Decision 7A).
6
+ */
7
+
8
+ import React from "react";
9
+ import { useBrickAvailable, useAnyBrickAvailable } from "../hooks/use-brick-available.js";
10
+ import { useGlobalStore } from "../../stores/global-store.js";
11
+ import { Spinner } from "./spinner.js";
12
+ import { textStyle } from "../text-style.js";
13
+
14
+ interface BrickGateProps {
15
+ /** Brick name or array of brick names (any-of semantics). */
16
+ readonly brick: string | readonly string[];
17
+ /** Content to render when the brick is available. */
18
+ readonly children: React.ReactNode;
19
+ /** Custom fallback. Defaults to BrickUnavailable message. */
20
+ readonly fallback?: React.ReactNode;
21
+ }
22
+
23
+ function BrickUnavailableMessage({ names }: { names: readonly string[] }): React.ReactNode {
24
+ const profile = useGlobalStore((s) => s.profile);
25
+ const brickList = names.join(", ");
26
+
27
+ return (
28
+ <box height="100%" width="100%" justifyContent="center" alignItems="center" flexDirection="column">
29
+ <text>{`Feature not available`}</text>
30
+ <text> </text>
31
+ <text style={textStyle({ dim: true })}>{`Required brick${names.length > 1 ? "s" : ""}: ${brickList}`}</text>
32
+ {profile && <text style={textStyle({ dim: true })}>{`Current profile: ${profile}`}</text>}
33
+ <text> </text>
34
+ <text style={textStyle({ dim: true })}>{`To enable: mount the brick via Zones > Bricks`}</text>
35
+ </box>
36
+ );
37
+ }
38
+
39
+ export function BrickGate({ brick, children, fallback }: BrickGateProps): React.ReactNode {
40
+ const bricks = Array.isArray(brick) ? brick : [brick];
41
+
42
+ // Use the appropriate hook based on single vs multiple bricks
43
+ const { available, loading } = bricks.length === 1
44
+ ? useBrickAvailable(bricks[0])
45
+ : useAnyBrickAvailable(bricks);
46
+
47
+ if (loading) {
48
+ return (
49
+ <box height="100%" width="100%" justifyContent="center" alignItems="center">
50
+ <Spinner label="Loading features..." />
51
+ </box>
52
+ );
53
+ }
54
+
55
+ if (!available) {
56
+ return fallback ?? <BrickUnavailableMessage names={bricks} />;
57
+ }
58
+
59
+ return <>{children}</>;
60
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * CommandOutput — renders streaming output from local nexus CLI commands.
3
+ *
4
+ * Uses StyledText for ANSI rendering and the CommandRunnerStore for state.
5
+ * Shows a spinner while running (Decision 15A).
6
+ */
7
+
8
+ import React from "react";
9
+ import { useCommandRunnerStore } from "../../services/command-runner.js";
10
+ import { StyledText } from "./styled-text.js";
11
+ import { Spinner } from "./spinner.js";
12
+ import { statusColor } from "../theme.js";
13
+ import { textStyle } from "../text-style.js";
14
+
15
+ const ERROR_HINTS: ReadonlyArray<{ pattern: RegExp; hint: string }> = [
16
+ { pattern: /authentication required|unauthorized|401/i, hint: "Check your API key (NEXUS_API_KEY or nexus.yaml api_key)" },
17
+ { pattern: /connection refused/i, hint: "Is the server running? Try Shift+U to start it" },
18
+ { pattern: /grpc.*unavailable|failed to connect/i, hint: "gRPC endpoint unreachable — check server logs" },
19
+ { pattern: /timed? ?out|deadline exceeded/i, hint: "Request timed out — the server may be overloaded" },
20
+ { pattern: /address already in use|EADDRINUSE/i, hint: "Port is already in use — stop the existing process or change ports" },
21
+ { pattern: /permission denied|EACCES/i, hint: "Permission denied — check file/directory permissions" },
22
+ { pattern: /no such file|ENOENT|not found/i, hint: "File or command not found — check paths and installation" },
23
+ ];
24
+
25
+ function findErrorHint(lines: readonly string[]): string | null {
26
+ const tail = lines.slice(-20);
27
+ for (const line of tail) {
28
+ for (const { pattern, hint } of ERROR_HINTS) {
29
+ if (pattern.test(line)) return hint;
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+
35
+ export function CommandOutput(): React.ReactNode {
36
+ const status = useCommandRunnerStore((s) => s.status);
37
+ const outputLines = useCommandRunnerStore((s) => s.outputLines);
38
+ const commandLabel = useCommandRunnerStore((s) => s.commandLabel);
39
+ const exitCode = useCommandRunnerStore((s) => s.exitCode);
40
+ const spawnError = useCommandRunnerStore((s) => s.spawnError);
41
+
42
+ if (status === "idle") {
43
+ return null;
44
+ }
45
+
46
+ const output = outputLines.join("\n");
47
+
48
+ return (
49
+ <box flexDirection="column" width="100%">
50
+ {/* Header */}
51
+ <box height={1} width="100%">
52
+ <text>
53
+ <span style={textStyle({ dim: true })}>{"$ "}</span>
54
+ <span style={textStyle({ bold: true })}>{commandLabel}</span>
55
+ </text>
56
+ </box>
57
+
58
+ {/* Output */}
59
+ {output ? (
60
+ <box width="100%">
61
+ <StyledText>{output}</StyledText>
62
+ </box>
63
+ ) : status === "running" ? (
64
+ <Spinner label="Running..." />
65
+ ) : null}
66
+
67
+ {/* Spawn error */}
68
+ {spawnError && (
69
+ <box height={1} width="100%">
70
+ <text style={textStyle({ fg: statusColor.error })}>{"Error: "}{spawnError}</text>
71
+ </box>
72
+ )}
73
+
74
+ {/* Status footer */}
75
+ {status === "success" && (
76
+ <box height={1} width="100%">
77
+ <text style={textStyle({ fg: statusColor.success })}>{"Command completed successfully"}</text>
78
+ </box>
79
+ )}
80
+ {status === "error" && exitCode !== null && (
81
+ <box height={1} width="100%">
82
+ <text style={textStyle({ fg: statusColor.error })}>{`Command failed with exit code ${exitCode}`}</text>
83
+ </box>
84
+ )}
85
+ {status === "error" && (() => {
86
+ const hint = findErrorHint(outputLines);
87
+ return hint ? (
88
+ <box height={1} width="100%">
89
+ <text style={textStyle({ fg: statusColor.warning })}>{` Hint: ${hint}`}</text>
90
+ </box>
91
+ ) : null;
92
+ })()}
93
+ </box>
94
+ );
95
+ }
@@ -0,0 +1,97 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { statusColor } from "../theme.js";
3
+ import { textStyle } from "../text-style.js";
4
+ import { useKeyboard } from "../hooks/use-keyboard.js";
5
+ import {
6
+ filterCommandPaletteItems,
7
+ type CommandPaletteItem,
8
+ } from "../command-palette.js";
9
+
10
+ interface CommandPaletteProps {
11
+ readonly visible: boolean;
12
+ readonly commands: readonly CommandPaletteItem[];
13
+ readonly onClose: () => void;
14
+ }
15
+
16
+ export function CommandPalette({
17
+ visible,
18
+ commands,
19
+ onClose,
20
+ }: CommandPaletteProps): React.ReactNode {
21
+ const [query, setQuery] = useState("");
22
+ const [selectedIndex, setSelectedIndex] = useState(0);
23
+
24
+ useEffect(() => {
25
+ if (!visible) {
26
+ setQuery("");
27
+ setSelectedIndex(0);
28
+ }
29
+ }, [visible]);
30
+
31
+ const filtered = useMemo(
32
+ () => filterCommandPaletteItems(commands, query),
33
+ [commands, query],
34
+ );
35
+
36
+ useEffect(() => {
37
+ setSelectedIndex((prev) => Math.min(prev, Math.max(filtered.length - 1, 0)));
38
+ }, [filtered.length]);
39
+
40
+ const executeSelected = useCallback(() => {
41
+ const selected = filtered[selectedIndex];
42
+ if (!selected) return;
43
+ selected.run();
44
+ onClose();
45
+ }, [filtered, selectedIndex, onClose]);
46
+
47
+ useKeyboard(
48
+ visible
49
+ ? {
50
+ escape: onClose,
51
+ return: executeSelected,
52
+ down: () => setSelectedIndex((prev) => Math.min(prev + 1, Math.max(filtered.length - 1, 0))),
53
+ up: () => setSelectedIndex((prev) => Math.max(prev - 1, 0)),
54
+ j: () => setSelectedIndex((prev) => Math.min(prev + 1, Math.max(filtered.length - 1, 0))),
55
+ k: () => setSelectedIndex((prev) => Math.max(prev - 1, 0)),
56
+ backspace: () => setQuery((prev) => prev.slice(0, -1)),
57
+ }
58
+ : {},
59
+ visible
60
+ ? (key) => {
61
+ if (key.length === 1) {
62
+ setQuery((prev) => prev + key);
63
+ } else if (key === "space") {
64
+ setQuery((prev) => prev + " ");
65
+ }
66
+ }
67
+ : undefined,
68
+ );
69
+
70
+ if (!visible) return null;
71
+
72
+ return (
73
+ <box height="100%" width="100%" justifyContent="center" alignItems="flex-start">
74
+ <box flexDirection="column" borderStyle="double" width={72} padding={1} marginTop={2}>
75
+ <text style={textStyle({ bold: true })}>Command Palette</text>
76
+ <text style={textStyle({ fg: statusColor.info })}>{`> ${query}${query.length >= 0 ? "\u2588" : ""}`}</text>
77
+ <text style={textStyle({ dim: true })}>Type to filter. Enter runs. Esc closes.</text>
78
+ <text>{""}</text>
79
+
80
+ {filtered.length === 0 ? (
81
+ <text style={textStyle({ dim: true })}>No matching commands</text>
82
+ ) : (
83
+ filtered.slice(0, 10).map((command, index) => {
84
+ const selected = index === selectedIndex;
85
+ return (
86
+ <box key={command.id} height={1} width="100%">
87
+ <text style={selected ? textStyle({ inverse: true }) : undefined}>
88
+ {`${selected ? "> " : " "}${command.section.padEnd(8)} ${command.title}${command.hint ? ` [${command.hint}]` : ""}`}
89
+ </text>
90
+ </box>
91
+ );
92
+ })
93
+ )}
94
+ </box>
95
+ </box>
96
+ );
97
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Reusable confirmation dialog for destructive actions.
3
+ *
4
+ * Renders a centered modal overlay with a message and Y/N keybindings.
5
+ * Follows the same overlay pattern as IdentitySwitcher.
6
+ */
7
+
8
+ import React from "react";
9
+ import { useKeyboard } from "../hooks/use-keyboard.js";
10
+
11
+ interface ConfirmDialogProps {
12
+ readonly visible: boolean;
13
+ readonly title: string;
14
+ readonly message: string;
15
+ readonly onConfirm: () => void;
16
+ readonly onCancel: () => void;
17
+ }
18
+
19
+ export function ConfirmDialog({
20
+ visible,
21
+ title,
22
+ message,
23
+ onConfirm,
24
+ onCancel,
25
+ }: ConfirmDialogProps): React.ReactNode {
26
+ useKeyboard(
27
+ visible
28
+ ? {
29
+ y: onConfirm,
30
+ return: onConfirm,
31
+ n: onCancel,
32
+ escape: onCancel,
33
+ }
34
+ : {},
35
+ );
36
+
37
+ if (!visible) return null;
38
+
39
+ return (
40
+ <box
41
+ height="100%"
42
+ width="100%"
43
+ justifyContent="center"
44
+ alignItems="center"
45
+ >
46
+ <box
47
+ flexDirection="column"
48
+ borderStyle="double"
49
+ width={50}
50
+ height={7}
51
+ padding={1}
52
+ >
53
+ <text>{title}</text>
54
+ <text>{""}</text>
55
+ <text>{message}</text>
56
+ <text>{""}</text>
57
+ <text>{"Y:confirm N/Esc:cancel"}</text>
58
+ </box>
59
+ </box>
60
+ );
61
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Reusable diff viewer component.
3
+ *
4
+ * Generates a unified diff from oldText/newText and renders it
5
+ * using OpenTUI's built-in <diff> component for syntax-highlighted,
6
+ * scrollable diff display.
7
+ */
8
+
9
+ import React from "react";
10
+
11
+ // =============================================================================
12
+ // Types
13
+ // =============================================================================
14
+
15
+ interface DiffViewerProps {
16
+ readonly oldText: string;
17
+ readonly newText: string;
18
+ readonly oldLabel?: string;
19
+ readonly newLabel?: string;
20
+ readonly view?: "unified" | "split";
21
+ }
22
+
23
+ // =============================================================================
24
+ // Minimal unified diff generator
25
+ // =============================================================================
26
+
27
+ /**
28
+ * Produce a unified diff string from two texts.
29
+ *
30
+ * Uses a simple LCS-based line diff (O(n*m) but fine for typical file sizes
31
+ * viewed in a TUI). Output follows the standard unified diff format that
32
+ * OpenTUI's DiffRenderable can parse.
33
+ */
34
+ function generateUnifiedDiff(
35
+ oldText: string,
36
+ newText: string,
37
+ oldLabel: string,
38
+ newLabel: string,
39
+ ): string {
40
+ const oldLines = oldText.split("\n");
41
+ const newLines = newText.split("\n");
42
+
43
+ // Build LCS table
44
+ const m = oldLines.length;
45
+ const n = newLines.length;
46
+ const dp: number[][] = Array.from({ length: m + 1 }, () =>
47
+ new Array<number>(n + 1).fill(0),
48
+ );
49
+
50
+ for (let i = 1; i <= m; i++) {
51
+ for (let j = 1; j <= n; j++) {
52
+ if (oldLines[i - 1] === newLines[j - 1]) {
53
+ dp[i]![j] = dp[i - 1]![j - 1]! + 1;
54
+ } else {
55
+ dp[i]![j] = Math.max(dp[i - 1]![j]!, dp[i]![j - 1]!);
56
+ }
57
+ }
58
+ }
59
+
60
+ // Back-trace to produce edit operations
61
+ const ops: Array<{ type: "keep" | "remove" | "add"; line: string }> = [];
62
+ let i = m;
63
+ let j = n;
64
+
65
+ while (i > 0 || j > 0) {
66
+ if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
67
+ ops.push({ type: "keep", line: oldLines[i - 1]! });
68
+ i--;
69
+ j--;
70
+ } else if (j > 0 && (i === 0 || dp[i]![j - 1]! >= dp[i - 1]![j]!)) {
71
+ ops.push({ type: "add", line: newLines[j - 1]! });
72
+ j--;
73
+ } else {
74
+ ops.push({ type: "remove", line: oldLines[i - 1]! });
75
+ i--;
76
+ }
77
+ }
78
+
79
+ ops.reverse();
80
+
81
+ // Group into hunks with 3-line context
82
+ const CONTEXT = 3;
83
+ const hunks: Array<{
84
+ oldStart: number;
85
+ oldCount: number;
86
+ newStart: number;
87
+ newCount: number;
88
+ lines: string[];
89
+ }> = [];
90
+
91
+ let oldIdx = 0;
92
+ let newIdx = 0;
93
+
94
+ let opIdx = 0;
95
+ while (opIdx < ops.length) {
96
+ // Skip context-only regions until we find a change
97
+ if (ops[opIdx]!.type === "keep") {
98
+ oldIdx++;
99
+ newIdx++;
100
+ opIdx++;
101
+ continue;
102
+ }
103
+
104
+ // Start a new hunk with leading context
105
+ const contextStart = Math.max(0, opIdx - CONTEXT);
106
+ let hunkOldStart = oldIdx;
107
+ let hunkNewStart = newIdx;
108
+
109
+ // Rewind counters for leading context
110
+ let rewind = opIdx - contextStart;
111
+ hunkOldStart -= rewind;
112
+ hunkNewStart -= rewind;
113
+
114
+ const hunkLines: string[] = [];
115
+ let hunkOldCount = 0;
116
+ let hunkNewCount = 0;
117
+
118
+ // Add leading context
119
+ for (let c = contextStart; c < opIdx; c++) {
120
+ hunkLines.push(` ${ops[c]!.line}`);
121
+ hunkOldCount++;
122
+ hunkNewCount++;
123
+ }
124
+
125
+ // Add changes and trailing context
126
+ let trailingContext = 0;
127
+ while (opIdx < ops.length) {
128
+ const op = ops[opIdx]!;
129
+ if (op.type === "keep") {
130
+ trailingContext++;
131
+ hunkLines.push(` ${op.line}`);
132
+ hunkOldCount++;
133
+ hunkNewCount++;
134
+ if (trailingContext >= CONTEXT * 2) {
135
+ // Enough trailing context, end hunk
136
+ break;
137
+ }
138
+ } else {
139
+ // Reset trailing context counter on any change
140
+ trailingContext = 0;
141
+ if (op.type === "remove") {
142
+ hunkLines.push(`-${op.line}`);
143
+ hunkOldCount++;
144
+ } else {
145
+ hunkLines.push(`+${op.line}`);
146
+ hunkNewCount++;
147
+ }
148
+ }
149
+ opIdx++;
150
+ }
151
+
152
+ // Trim excess trailing context to CONTEXT lines
153
+ while (trailingContext > CONTEXT) {
154
+ hunkLines.pop();
155
+ hunkOldCount--;
156
+ hunkNewCount--;
157
+ trailingContext--;
158
+ }
159
+
160
+ hunks.push({
161
+ oldStart: hunkOldStart + 1,
162
+ oldCount: hunkOldCount,
163
+ newStart: hunkNewStart + 1,
164
+ newCount: hunkNewCount,
165
+ lines: hunkLines,
166
+ });
167
+
168
+ // Advance past remaining keep ops that were consumed
169
+ oldIdx = hunkOldStart + hunkOldCount;
170
+ newIdx = hunkNewStart + hunkNewCount;
171
+ }
172
+
173
+ if (hunks.length === 0) {
174
+ return "";
175
+ }
176
+
177
+ // Build unified diff output
178
+ const output: string[] = [
179
+ `--- ${oldLabel}`,
180
+ `+++ ${newLabel}`,
181
+ ];
182
+
183
+ for (const hunk of hunks) {
184
+ output.push(
185
+ `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@`,
186
+ );
187
+ output.push(...hunk.lines);
188
+ }
189
+
190
+ return output.join("\n");
191
+ }
192
+
193
+ // =============================================================================
194
+ // Component
195
+ // =============================================================================
196
+
197
+ export function DiffViewer({
198
+ oldText,
199
+ newText,
200
+ oldLabel = "a",
201
+ newLabel = "b",
202
+ view = "unified",
203
+ }: DiffViewerProps): React.ReactNode {
204
+ const diffString = generateUnifiedDiff(oldText, newText, oldLabel, newLabel);
205
+
206
+ if (diffString === "") {
207
+ return (
208
+ <box height="100%" width="100%">
209
+ <text>No differences found.</text>
210
+ </box>
211
+ );
212
+ }
213
+
214
+ return (
215
+ <scrollbox height="100%" width="100%">
216
+ <diff diff={diffString} view={view} showLineNumbers />
217
+ </scrollbox>
218
+ );
219
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Consistent empty state component for panels with no data.
3
+ *
4
+ * Replaces generic "No X found" with actionable messages that tell
5
+ * users what to do next.
6
+ *
7
+ * @see Issue #3066, Phase E10
8
+ */
9
+
10
+ import React from "react";
11
+ import { statusColor } from "../theme.js";
12
+ import { textStyle } from "../text-style.js";
13
+
14
+ interface EmptyStateProps {
15
+ /** Primary message, e.g. "No transactions yet." */
16
+ readonly message: string;
17
+ /** Optional hint showing what to do, e.g. "Press n to begin one." */
18
+ readonly hint?: string;
19
+ }
20
+
21
+ export function EmptyState({ message, hint }: EmptyStateProps): React.ReactNode {
22
+ return (
23
+ <box
24
+ height="100%"
25
+ width="100%"
26
+ justifyContent="center"
27
+ alignItems="center"
28
+ flexDirection="column"
29
+ >
30
+ <text style={textStyle({ dim: true })}>{message}</text>
31
+ {hint && (
32
+ <text style={textStyle({ fg: statusColor.dim })}>{hint}</text>
33
+ )}
34
+ </box>
35
+ );
36
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Centralized error display bar.
3
+ *
4
+ * Renders the most recent error from the error store above the status bar.
5
+ * Supports dismissal (any key) and retry (r key).
6
+ *
7
+ * @see Issue #3066 Architecture Decision 8A
8
+ */
9
+
10
+ import React from "react";
11
+ import { useErrorStore } from "../../stores/error-store.js";
12
+ import { useKeyboard } from "../hooks/use-keyboard.js";
13
+ import { palette } from "../theme.js";
14
+ import { textStyle } from "../text-style.js";
15
+
16
+ const CATEGORY_HINTS: Record<string, string> = {
17
+ network: "Check connection. r:retry",
18
+ validation: "Check input values.",
19
+ server: "Server error. r:retry",
20
+ };
21
+
22
+ export function ErrorBar(): React.ReactNode {
23
+ const errors = useErrorStore((s) => s.errors);
24
+ const dismissError = useErrorStore((s) => s.dismissError);
25
+
26
+ const latest = errors.length > 0 ? errors[errors.length - 1]! : null;
27
+
28
+ useKeyboard(
29
+ latest
30
+ ? {
31
+ r: () => {
32
+ if (latest.retryAction) {
33
+ dismissError(latest.id);
34
+ latest.retryAction();
35
+ }
36
+ },
37
+ escape: () => {
38
+ if (latest.dismissable) dismissError(latest.id);
39
+ },
40
+ }
41
+ : {},
42
+ );
43
+
44
+ if (!latest) return null;
45
+
46
+ const hint = CATEGORY_HINTS[latest.category] ?? "";
47
+ const prefix = errors.length > 1 ? `(${errors.length}) ` : "";
48
+
49
+ return (
50
+ <box height={1} width="100%" flexDirection="row">
51
+ <text>
52
+ <span style={textStyle({ fg: palette.error, bold: true })}>{`${prefix}✗ ${latest.message}`}</span>
53
+ <span style={textStyle({ fg: palette.errorDim })}>{` ${hint}`}</span>
54
+ {latest.dismissable ? (
55
+ <span style={textStyle({ fg: palette.faint })}>{" Esc:dismiss"}</span>
56
+ ) : ""}
57
+ </text>
58
+ </box>
59
+ );
60
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * React error boundary that renders a user-friendly error message
3
+ * instead of crashing the TUI.
4
+ */
5
+
6
+ import React from "react";
7
+
8
+ interface ErrorBoundaryProps {
9
+ readonly children: React.ReactNode;
10
+ readonly fallback?: React.ReactNode;
11
+ }
12
+
13
+ interface ErrorBoundaryState {
14
+ readonly error: Error | null;
15
+ }
16
+
17
+ class ErrorBoundaryClass extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
18
+ constructor(props: ErrorBoundaryProps) {
19
+ super(props);
20
+ this.state = { error: null };
21
+ }
22
+
23
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
24
+ return { error };
25
+ }
26
+
27
+ override render(): React.ReactNode {
28
+ if (this.state.error) {
29
+ if (this.props.fallback) {
30
+ return this.props.fallback;
31
+ }
32
+ return (
33
+ <box flexDirection="column" padding={1}>
34
+ <text>Something went wrong:</text>
35
+ <text>{this.state.error.message}</text>
36
+ </box>
37
+ );
38
+ }
39
+ return this.props.children;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Function component wrapper for the class-based error boundary.
45
+ *
46
+ * OpenTUI v0.1.87 has a bug where JSX.ElementClass extends
47
+ * React.ComponentClass (constructor) instead of the instance type,
48
+ * preventing class components from being used directly as JSX elements.
49
+ * This wrapper sidesteps that issue.
50
+ */
51
+ export function ErrorBoundary(props: ErrorBoundaryProps): React.ReactNode {
52
+ return React.createElement(ErrorBoundaryClass, props);
53
+ }