@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,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
|
+
}
|