@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,82 @@
1
+ /**
2
+ * Subscriptions tab: event subscription list with delete/test actions.
3
+ *
4
+ * Extracted from events-panel.tsx (Issue 2A).
5
+ */
6
+
7
+ import React, { useEffect } from "react";
8
+ import { useInfraStore } from "../../stores/infra-store.js";
9
+ import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
10
+ import { listNavigationBindings } from "../../shared/hooks/use-list-navigation.js";
11
+ import { useConfirmStore } from "../../shared/hooks/use-confirm.js";
12
+ import { useApi } from "../../shared/hooks/use-api.js";
13
+ import { SubscriptionList } from "./subscription-list.js";
14
+
15
+ interface SubscriptionsTabProps {
16
+ readonly tabBindings: Readonly<Record<string, () => void>>;
17
+ readonly overlayActive: boolean;
18
+ }
19
+
20
+ export function SubscriptionsTab({ tabBindings, overlayActive }: SubscriptionsTabProps): React.ReactNode {
21
+ const client = useApi();
22
+ const confirm = useConfirmStore((s) => s.confirm);
23
+
24
+ const subscriptions = useInfraStore((s) => s.subscriptions);
25
+ const subscriptionsLoading = useInfraStore((s) => s.subscriptionsLoading);
26
+ const selectedSubscriptionIndex = useInfraStore((s) => s.selectedSubscriptionIndex);
27
+ const setSelectedSubscriptionIndex = useInfraStore((s) => s.setSelectedSubscriptionIndex);
28
+ const fetchSubscriptions = useInfraStore((s) => s.fetchSubscriptions);
29
+ const deleteSubscription = useInfraStore((s) => s.deleteSubscription);
30
+ const testSubscription = useInfraStore((s) => s.testSubscription);
31
+
32
+ useEffect(() => {
33
+ if (client) fetchSubscriptions(client);
34
+ }, [client, fetchSubscriptions]);
35
+
36
+ const listNav = listNavigationBindings({
37
+ getIndex: () => selectedSubscriptionIndex,
38
+ setIndex: (i) => setSelectedSubscriptionIndex(i),
39
+ getLength: () => subscriptions.length,
40
+ });
41
+
42
+ useKeyboard(
43
+ overlayActive
44
+ ? {}
45
+ : {
46
+ ...listNav,
47
+ ...tabBindings,
48
+ d: async () => {
49
+ if (client) {
50
+ const sub = subscriptions[selectedSubscriptionIndex];
51
+ if (sub) {
52
+ const ok = await confirm("Delete subscription?", "Delete this event subscription.");
53
+ if (!ok) return;
54
+ deleteSubscription(sub.subscription_id, client);
55
+ }
56
+ }
57
+ },
58
+ t: () => {
59
+ if (client) {
60
+ const sub = subscriptions[selectedSubscriptionIndex];
61
+ if (sub) testSubscription(sub.subscription_id, client);
62
+ }
63
+ },
64
+ r: () => { if (client) fetchSubscriptions(client); },
65
+ },
66
+ );
67
+
68
+ return (
69
+ <box height="100%" width="100%" flexDirection="column">
70
+ <box flexGrow={1} width="100%" borderStyle="single">
71
+ <SubscriptionList
72
+ subscriptions={subscriptions}
73
+ selectedIndex={selectedSubscriptionIndex}
74
+ loading={subscriptionsLoading}
75
+ />
76
+ </box>
77
+ <box height={1} width="100%">
78
+ <text>{"j/k:navigate d:delete t:test r:refresh Tab:switch tab"}</text>
79
+ </box>
80
+ </box>
81
+ );
82
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Aspects sub-view for the Files panel.
3
+ * Shows all aspects attached to the selected file, lazy-loaded from API.
4
+ * Issue #2930.
5
+ */
6
+
7
+ import React, { useEffect } from "react";
8
+ import crypto from "node:crypto";
9
+ import type { FileItem } from "../../stores/files-store.js";
10
+ import { useKnowledgeStore } from "../../stores/knowledge-store.js";
11
+ import { useApi } from "../../shared/hooks/use-api.js";
12
+
13
+ interface FileAspectsProps {
14
+ readonly item: FileItem | null;
15
+ }
16
+
17
+ function computeUrn(item: FileItem): string | null {
18
+ if (!item.path) return null;
19
+ // Use "default" zone when zone isolation is not configured
20
+ const zone = item.zoneId || "default";
21
+ const pathHash = crypto
22
+ .createHash("sha256")
23
+ .update(item.path)
24
+ .digest("hex")
25
+ .slice(0, 32);
26
+ return `urn:nexus:file:${zone}:${pathHash}`;
27
+ }
28
+
29
+ export function FileAspects({ item }: FileAspectsProps): React.ReactNode {
30
+ const client = useApi();
31
+ const aspectsCache = useKnowledgeStore((s) => s.aspectsCache);
32
+ const aspectDetailCache = useKnowledgeStore((s) => s.aspectDetailCache);
33
+ const loading = useKnowledgeStore((s) => s.aspectsLoading);
34
+ const fetchAspects = useKnowledgeStore((s) => s.fetchAspects);
35
+ const fetchAspectDetail = useKnowledgeStore((s) => s.fetchAspectDetail);
36
+
37
+ const urn = item ? computeUrn(item) : null;
38
+
39
+ useEffect(() => {
40
+ if (client && urn) {
41
+ fetchAspects(urn, client);
42
+ }
43
+ }, [client, urn, fetchAspects]);
44
+
45
+ // Fetch detail for each aspect once names are loaded
46
+ const aspectNames = urn ? (aspectsCache.get(urn) ?? []) : [];
47
+ useEffect(() => {
48
+ if (client && urn && aspectNames.length > 0) {
49
+ for (const name of aspectNames) {
50
+ fetchAspectDetail(urn, name, client);
51
+ }
52
+ }
53
+ }, [client, urn, aspectNames.length, fetchAspectDetail]);
54
+
55
+ if (!item) {
56
+ return <text>No file selected</text>;
57
+ }
58
+
59
+ if (!urn) {
60
+ return <text>{"Cannot compute URN (missing zone)"}</text>;
61
+ }
62
+
63
+ if (loading) {
64
+ return <text>Loading aspects...</text>;
65
+ }
66
+
67
+ if (aspectNames.length === 0) {
68
+ return (
69
+ <box flexDirection="column" height="100%" width="100%">
70
+ <text>{"─── Aspects ───"}</text>
71
+ <text>{"No aspects attached"}</text>
72
+ </box>
73
+ );
74
+ }
75
+
76
+ return (
77
+ <box flexDirection="column" height="100%" width="100%">
78
+ <text>{`─── Aspects (${aspectNames.length}) ───`}</text>
79
+ {aspectNames.map((name) => {
80
+ const key = `${urn}::${name}`;
81
+ const detail = aspectDetailCache.get(key);
82
+ return (
83
+ <box key={name} flexDirection="column">
84
+ <text>{` * ${name}`}</text>
85
+ {detail ? (
86
+ <text>{` v${detail.version} by ${detail.createdBy}`}</text>
87
+ ) : null}
88
+ </box>
89
+ );
90
+ })}
91
+ </box>
92
+ );
93
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Full-screen file editor using OpenTUI's <textarea> component.
3
+ *
4
+ * Opens when pressing 'e' on a file in the explorer.
5
+ * - Loads file content from the server
6
+ * - Multi-line editing with undo/redo, cursor nav, selection
7
+ * - Ctrl+S or Meta+Enter to save
8
+ * - Esc to cancel
9
+ */
10
+
11
+ import React, { useEffect, useRef, useState, useCallback } from "react";
12
+ import type { TextareaRenderable } from "@opentui/core";
13
+ import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
14
+ import { useApi } from "../../shared/hooks/use-api.js";
15
+ import { useFilesStore } from "../../stores/files-store.js";
16
+ import { useVersionsStore } from "../../stores/versions-store.js";
17
+ import { Spinner } from "../../shared/components/spinner.js";
18
+ import { textStyle } from "../../shared/text-style.js";
19
+
20
+ interface FileEditorProps {
21
+ readonly path: string;
22
+ readonly onClose: () => void;
23
+ }
24
+
25
+ export function FileEditor({ path, onClose }: FileEditorProps): React.ReactNode {
26
+ const client = useApi();
27
+ const textareaRef = useRef<TextareaRenderable>(null);
28
+ const [loading, setLoading] = useState(true);
29
+ const [saving, setSaving] = useState(false);
30
+ const [error, setError] = useState<string | null>(null);
31
+ const [dirty, setDirty] = useState(false);
32
+ const [initialContent, setInitialContent] = useState("");
33
+ // Must be called at top level (before any conditional returns) to respect Rules of Hooks
34
+ const activeTxn = useVersionsStore((s) => s.selectedTransaction);
35
+ const hasTxn = activeTxn?.status === "active";
36
+
37
+ // Load file content
38
+ useEffect(() => {
39
+ if (!client) return;
40
+ setLoading(true);
41
+ setError(null);
42
+
43
+ client.get<{ content: string }>(`/api/v2/files/read?path=${encodeURIComponent(path)}&include_metadata=false`)
44
+ .then((response) => {
45
+ const content = typeof response === "string" ? response : (response?.content ?? "");
46
+ setInitialContent(content);
47
+ if (textareaRef.current) {
48
+ textareaRef.current.setText(content);
49
+ }
50
+ setLoading(false);
51
+ })
52
+ .catch((err) => {
53
+ // New file — start with empty content
54
+ setInitialContent("");
55
+ if (textareaRef.current) {
56
+ textareaRef.current.setText("");
57
+ }
58
+ setLoading(false);
59
+ });
60
+ }, [client, path]);
61
+
62
+ // Set initial content once textarea mounts
63
+ useEffect(() => {
64
+ if (!loading && textareaRef.current && initialContent) {
65
+ textareaRef.current.setText(initialContent);
66
+ }
67
+ }, [loading, initialContent]);
68
+
69
+ // Save file (with optional transaction tracking)
70
+ const handleSave = useCallback(async () => {
71
+ if (!client || saving) return;
72
+ const content = textareaRef.current?.plainText ?? "";
73
+ setSaving(true);
74
+ setError(null);
75
+ try {
76
+ // If an active transaction exists, pass its ID so the write is tracked
77
+ const activeTxn = useVersionsStore.getState().selectedTransaction;
78
+ const txnParam = activeTxn?.status === "active" ? `?transaction_id=${activeTxn.transaction_id}` : "";
79
+ await client.post(`/api/v2/files/write${txnParam}`, {
80
+ path,
81
+ content,
82
+ });
83
+ setDirty(false);
84
+ // Refresh parent directory in file tree
85
+ const parentDir = path.split("/").slice(0, -1).join("/") || "/";
86
+ useFilesStore.getState().invalidate(parentDir);
87
+ onClose();
88
+ } catch (err) {
89
+ setError(err instanceof Error ? err.message : "Failed to save");
90
+ } finally {
91
+ setSaving(false);
92
+ }
93
+ }, [client, path, saving, onClose]);
94
+
95
+ // Keyboard shortcuts (only when not focused on textarea — textarea handles its own keys)
96
+ useKeyboard({
97
+ "escape": () => {
98
+ onClose();
99
+ },
100
+ "ctrl+s": () => {
101
+ handleSave();
102
+ },
103
+ });
104
+
105
+ if (loading) {
106
+ return (
107
+ <box height="100%" width="100%" justifyContent="center" alignItems="center">
108
+ <Spinner label={`Loading ${path}...`} />
109
+ </box>
110
+ );
111
+ }
112
+
113
+ const fileName = path.split("/").pop() ?? path;
114
+
115
+ return (
116
+ <box height="100%" width="100%" flexDirection="column">
117
+ {/* Header */}
118
+ <box height={1} width="100%">
119
+ <text>
120
+ <span style={textStyle({ fg: "#00d4ff", bold: true })}>{` ${fileName}`}</span>
121
+ <span style={textStyle({ fg: "#666666" })}>{` — ${path}`}</span>
122
+ {dirty ? <span style={textStyle({ fg: "#ffaa00" })}>{" [modified]"}</span> : ""}
123
+ {saving ? <span style={textStyle({ fg: "#ffaa00" })}>{" saving..."}</span> : ""}
124
+ {hasTxn ? <span style={textStyle({ fg: "#4dff88" })}>{` [txn:${activeTxn!.transaction_id.slice(0, 8)}]`}</span> : ""}
125
+ </text>
126
+ </box>
127
+
128
+ {/* Editor */}
129
+ <box flexGrow={1} borderStyle="single" borderColor={dirty ? "#ffaa00" : "#444444"}>
130
+ <textarea
131
+ ref={textareaRef}
132
+ initialValue={initialContent}
133
+ placeholder="Start typing..."
134
+ wrapMode="word"
135
+ focusedTextColor="#ffffff"
136
+ focusedBackgroundColor="#1a1a2e"
137
+ textColor="#cccccc"
138
+ cursorColor="#00d4ff"
139
+ selectionBg="#264f78"
140
+ focused
141
+ onContentChange={() => setDirty(true)}
142
+ onSubmit={() => handleSave()}
143
+ />
144
+ </box>
145
+
146
+ {/* Footer */}
147
+ <box height={1} width="100%">
148
+ <text>
149
+ <span style={textStyle({ fg: "#4dff88", bold: true })}>{" Ctrl+S"}</span>
150
+ <span style={textStyle({ fg: "#888888" })}>{":save "}</span>
151
+ <span style={textStyle({ fg: "#ff4444", bold: true })}>{"Esc"}</span>
152
+ <span style={textStyle({ fg: "#888888" })}>{":cancel "}</span>
153
+ <span style={textStyle({ fg: "#00d4ff" })}>{"Meta+Enter"}</span>
154
+ <span style={textStyle({ fg: "#888888" })}>{":save "}</span>
155
+ {error ? <span style={textStyle({ fg: "#ff4444" })}>{` Error: ${error}`}</span> : ""}
156
+ </text>
157
+ </box>
158
+ </box>
159
+ );
160
+ }