@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,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mounted tab: lists mounted connectors with sync control.
|
|
3
|
+
*
|
|
4
|
+
* Supports: mount list navigation, sync trigger, unmount, sync status display.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useEffect, useCallback } from "react";
|
|
8
|
+
import type { FetchClient } from "@nexus-ai-fs/api-client";
|
|
9
|
+
import { useConnectorsStore } from "../../stores/connectors-store.js";
|
|
10
|
+
import { useConfirmStore } from "../../shared/hooks/use-confirm.js";
|
|
11
|
+
import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
|
|
12
|
+
import { listNavigationBindings } from "../../shared/hooks/use-list-navigation.js";
|
|
13
|
+
import { LoadingIndicator } from "../../shared/components/loading-indicator.js";
|
|
14
|
+
import { statusColor } from "../../shared/theme.js";
|
|
15
|
+
|
|
16
|
+
interface MountedTabProps {
|
|
17
|
+
readonly client: FetchClient;
|
|
18
|
+
readonly overlayActive: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function MountedTab({ client, overlayActive }: MountedTabProps): React.ReactNode {
|
|
22
|
+
const mounts = useConnectorsStore((s) => s.mounts);
|
|
23
|
+
const loading = useConnectorsStore((s) => s.mountsLoading);
|
|
24
|
+
const selectedIndex = useConnectorsStore((s) => s.selectedMountIndex);
|
|
25
|
+
const syncingMounts = useConnectorsStore((s) => s.syncingMounts);
|
|
26
|
+
const lastSyncResult = useConnectorsStore((s) => s.lastSyncResult);
|
|
27
|
+
|
|
28
|
+
const setSelectedIndex = useConnectorsStore((s) => s.setSelectedMountIndex);
|
|
29
|
+
const fetchMounts = useConnectorsStore((s) => s.fetchMounts);
|
|
30
|
+
const triggerSync = useConnectorsStore((s) => s.triggerSync);
|
|
31
|
+
const unmountConnector = useConnectorsStore((s) => s.unmountConnector);
|
|
32
|
+
const clearSyncResult = useConnectorsStore((s) => s.clearSyncResult);
|
|
33
|
+
|
|
34
|
+
const confirm = useConfirmStore((s) => s.confirm);
|
|
35
|
+
|
|
36
|
+
// Auto-fetch on mount
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (mounts.length === 0) {
|
|
39
|
+
fetchMounts(client);
|
|
40
|
+
}
|
|
41
|
+
}, [client, mounts.length, fetchMounts]);
|
|
42
|
+
|
|
43
|
+
const handleSync = useCallback(() => {
|
|
44
|
+
const selected = mounts[selectedIndex];
|
|
45
|
+
if (selected) {
|
|
46
|
+
triggerSync(selected.mount_point, client);
|
|
47
|
+
}
|
|
48
|
+
}, [mounts, selectedIndex, triggerSync, client]);
|
|
49
|
+
|
|
50
|
+
const handleUnmount = useCallback(async () => {
|
|
51
|
+
const selected = mounts[selectedIndex];
|
|
52
|
+
if (!selected) return;
|
|
53
|
+
const ok = await confirm(
|
|
54
|
+
"Unmount connector?",
|
|
55
|
+
`Unmount ${selected.mount_point}. Synced data will remain in the VFS.`,
|
|
56
|
+
);
|
|
57
|
+
if (!ok) return;
|
|
58
|
+
unmountConnector(selected.mount_point, client);
|
|
59
|
+
}, [mounts, selectedIndex, unmountConnector, client, confirm]);
|
|
60
|
+
|
|
61
|
+
const listNav = listNavigationBindings({
|
|
62
|
+
getIndex: () => selectedIndex,
|
|
63
|
+
setIndex: setSelectedIndex,
|
|
64
|
+
getLength: () => mounts.length,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
useKeyboard(
|
|
68
|
+
overlayActive
|
|
69
|
+
? {}
|
|
70
|
+
: {
|
|
71
|
+
...listNav,
|
|
72
|
+
s: handleSync,
|
|
73
|
+
u: handleUnmount,
|
|
74
|
+
r: () => fetchMounts(client),
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (loading && mounts.length === 0) {
|
|
79
|
+
return <LoadingIndicator message="Loading mounts..." />;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const selectedMount = mounts[selectedIndex];
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<box flexDirection="column" height="100%" width="100%">
|
|
86
|
+
{/* Sync result banner */}
|
|
87
|
+
{lastSyncResult && (
|
|
88
|
+
<box height={2} width="100%" borderStyle="single" marginBottom={1}>
|
|
89
|
+
{lastSyncResult.error ? (
|
|
90
|
+
<text foregroundColor={statusColor.error}>{`Sync error: ${lastSyncResult.error}`}</text>
|
|
91
|
+
) : (
|
|
92
|
+
<text foregroundColor={statusColor.healthy}>
|
|
93
|
+
{`Synced ${lastSyncResult.files_synced} files`}
|
|
94
|
+
{lastSyncResult.is_delta ? ` (delta: +${lastSyncResult.delta_added} -${lastSyncResult.delta_deleted})` : ""}
|
|
95
|
+
</text>
|
|
96
|
+
)}
|
|
97
|
+
</box>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{/* Mount list */}
|
|
101
|
+
<box flexGrow={1} flexDirection="column">
|
|
102
|
+
{mounts.length === 0 ? (
|
|
103
|
+
<box height={1} width="100%">
|
|
104
|
+
<text foregroundColor={statusColor.dim}>No connectors mounted. Go to Available tab to mount one.</text>
|
|
105
|
+
</box>
|
|
106
|
+
) : (
|
|
107
|
+
mounts.map((m, i) => {
|
|
108
|
+
const isSyncing = syncingMounts.has(m.mount_point);
|
|
109
|
+
const selected = i === selectedIndex;
|
|
110
|
+
const prefix = selected ? "▶ " : " ";
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<box key={m.mount_point} height={1} width="100%">
|
|
114
|
+
<text>
|
|
115
|
+
<span foregroundColor={selected ? statusColor.info : undefined}>{prefix}</span>
|
|
116
|
+
<span bold={selected} foregroundColor={statusColor.reference}>{m.mount_point}</span>
|
|
117
|
+
<span foregroundColor={statusColor.dim}>{m.readonly ? " (ro)" : ""}</span>
|
|
118
|
+
{m.skill_name && (
|
|
119
|
+
<span foregroundColor={statusColor.dim}>{` skill:${m.skill_name}`}</span>
|
|
120
|
+
)}
|
|
121
|
+
{m.operations.length > 0 && (
|
|
122
|
+
<span foregroundColor={statusColor.dim}>{` ops:${m.operations.length}`}</span>
|
|
123
|
+
)}
|
|
124
|
+
{isSyncing && (
|
|
125
|
+
<span foregroundColor={statusColor.warning}>{` [syncing…]`}</span>
|
|
126
|
+
)}
|
|
127
|
+
{m.sync_status && !isSyncing && (
|
|
128
|
+
<span foregroundColor={m.sync_status === "error" ? statusColor.error : statusColor.healthy}>
|
|
129
|
+
{` [${m.sync_status}]`}
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
{m.last_sync && (
|
|
133
|
+
<span foregroundColor={statusColor.dim}>{` last:${m.last_sync}`}</span>
|
|
134
|
+
)}
|
|
135
|
+
</text>
|
|
136
|
+
</box>
|
|
137
|
+
);
|
|
138
|
+
})
|
|
139
|
+
)}
|
|
140
|
+
</box>
|
|
141
|
+
|
|
142
|
+
{/* Details for selected mount */}
|
|
143
|
+
{selectedMount && (
|
|
144
|
+
<box height={3} width="100%" borderStyle="single" marginTop={1}>
|
|
145
|
+
<box flexDirection="column" width="100%">
|
|
146
|
+
<box height={1} width="100%">
|
|
147
|
+
<text>
|
|
148
|
+
<span bold>{selectedMount.mount_point}</span>
|
|
149
|
+
<span foregroundColor={statusColor.dim}>
|
|
150
|
+
{selectedMount.readonly ? " read-only" : " read-write"}
|
|
151
|
+
</span>
|
|
152
|
+
</text>
|
|
153
|
+
</box>
|
|
154
|
+
{selectedMount.operations.length > 0 && (
|
|
155
|
+
<box height={1} width="100%">
|
|
156
|
+
<text foregroundColor={statusColor.dim}>
|
|
157
|
+
{`Operations: ${selectedMount.operations.join(", ")}`}
|
|
158
|
+
</text>
|
|
159
|
+
</box>
|
|
160
|
+
)}
|
|
161
|
+
</box>
|
|
162
|
+
</box>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
{/* Help bar */}
|
|
166
|
+
<box height={1} width="100%">
|
|
167
|
+
{loading ? (
|
|
168
|
+
<text foregroundColor={statusColor.warning}>⠋ Refreshing...</text>
|
|
169
|
+
) : syncingMounts.size > 0 ? (
|
|
170
|
+
<text foregroundColor={statusColor.warning}>{`⠋ Syncing ${syncingMounts.size} mount(s)...`}</text>
|
|
171
|
+
) : (
|
|
172
|
+
<text foregroundColor={statusColor.dim}>
|
|
173
|
+
j/k:navigate s:sync u:unmount r:refresh
|
|
174
|
+
</text>
|
|
175
|
+
)}
|
|
176
|
+
</box>
|
|
177
|
+
</box>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills tab: view SKILL.md docs and browse operation schemas (read-only).
|
|
3
|
+
*
|
|
4
|
+
* Two view modes: "doc" shows SKILL.md content, "schema" shows annotated schema.
|
|
5
|
+
* Select a mount first, then browse its skill docs and schemas.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useEffect, useCallback } from "react";
|
|
9
|
+
import type { FetchClient } from "@nexus-ai-fs/api-client";
|
|
10
|
+
import { useConnectorsStore } from "../../stores/connectors-store.js";
|
|
11
|
+
import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
|
|
12
|
+
import { useCopy } from "../../shared/hooks/use-copy.js";
|
|
13
|
+
import { listNavigationBindings } from "../../shared/hooks/use-list-navigation.js";
|
|
14
|
+
import { LoadingIndicator } from "../../shared/components/loading-indicator.js";
|
|
15
|
+
import { statusColor } from "../../shared/theme.js";
|
|
16
|
+
|
|
17
|
+
interface SkillsTabProps {
|
|
18
|
+
readonly client: FetchClient;
|
|
19
|
+
readonly overlayActive: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function SkillsTab({ client, overlayActive }: SkillsTabProps): React.ReactNode {
|
|
23
|
+
const mounts = useConnectorsStore((s) => s.mounts);
|
|
24
|
+
const selectedMountIndex = useConnectorsStore((s) => s.selectedSkillMountIndex);
|
|
25
|
+
const skillDoc = useConnectorsStore((s) => s.skillDoc);
|
|
26
|
+
const skillDocLoading = useConnectorsStore((s) => s.skillDocLoading);
|
|
27
|
+
const selectedSchemaIndex = useConnectorsStore((s) => s.selectedSchemaIndex);
|
|
28
|
+
const schemaDoc = useConnectorsStore((s) => s.schemaDoc);
|
|
29
|
+
const schemaDocLoading = useConnectorsStore((s) => s.schemaDocLoading);
|
|
30
|
+
const viewMode = useConnectorsStore((s) => s.skillViewMode);
|
|
31
|
+
|
|
32
|
+
const setSelectedMountIndex = useConnectorsStore((s) => s.setSelectedSkillMountIndex);
|
|
33
|
+
const setSelectedSchemaIndex = useConnectorsStore((s) => s.setSelectedSchemaIndex);
|
|
34
|
+
const setSkillViewMode = useConnectorsStore((s) => s.setSkillViewMode);
|
|
35
|
+
const fetchSkillDoc = useConnectorsStore((s) => s.fetchSkillDoc);
|
|
36
|
+
const fetchSchema = useConnectorsStore((s) => s.fetchSchema);
|
|
37
|
+
const fetchMounts = useConnectorsStore((s) => s.fetchMounts);
|
|
38
|
+
|
|
39
|
+
const { copy, copied } = useCopy();
|
|
40
|
+
|
|
41
|
+
const selectedMount = mounts[selectedMountIndex];
|
|
42
|
+
|
|
43
|
+
// Auto-fetch mounts if empty
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (mounts.length === 0) {
|
|
46
|
+
fetchMounts(client);
|
|
47
|
+
}
|
|
48
|
+
}, [client, mounts.length, fetchMounts]);
|
|
49
|
+
|
|
50
|
+
// Auto-fetch skill doc when mount selection changes
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (selectedMount && viewMode === "doc") {
|
|
53
|
+
fetchSkillDoc(selectedMount.mount_point, client);
|
|
54
|
+
}
|
|
55
|
+
}, [selectedMount?.mount_point, viewMode, client, fetchSkillDoc]);
|
|
56
|
+
|
|
57
|
+
// Fetch schema when schema selection changes
|
|
58
|
+
const handleSchemaSelect = useCallback(
|
|
59
|
+
(index: number) => {
|
|
60
|
+
if (!selectedMount || !skillDoc) return;
|
|
61
|
+
const operation = skillDoc.schemas[index];
|
|
62
|
+
if (operation) {
|
|
63
|
+
setSelectedSchemaIndex(index);
|
|
64
|
+
fetchSchema(selectedMount.mount_point, operation, client);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
[selectedMount, skillDoc, setSelectedSchemaIndex, fetchSchema, client],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Build navigation bindings based on view mode
|
|
71
|
+
const mountNav = listNavigationBindings({
|
|
72
|
+
getIndex: () => selectedMountIndex,
|
|
73
|
+
setIndex: (i) => {
|
|
74
|
+
setSelectedMountIndex(i);
|
|
75
|
+
setSelectedSchemaIndex(0);
|
|
76
|
+
},
|
|
77
|
+
getLength: () => mounts.length,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const schemaNav = listNavigationBindings({
|
|
81
|
+
getIndex: () => selectedSchemaIndex,
|
|
82
|
+
setIndex: setSelectedSchemaIndex,
|
|
83
|
+
getLength: () => (skillDoc?.schemas.length ?? 0),
|
|
84
|
+
onSelect: handleSchemaSelect,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
useKeyboard(
|
|
88
|
+
overlayActive
|
|
89
|
+
? {}
|
|
90
|
+
: viewMode === "doc"
|
|
91
|
+
? {
|
|
92
|
+
...mountNav,
|
|
93
|
+
s: () => setSkillViewMode("schema"),
|
|
94
|
+
r: () => {
|
|
95
|
+
if (selectedMount) fetchSkillDoc(selectedMount.mount_point, client);
|
|
96
|
+
},
|
|
97
|
+
y: () => {
|
|
98
|
+
if (skillDoc?.content) copy(skillDoc.content);
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
: {
|
|
102
|
+
...schemaNav,
|
|
103
|
+
d: () => setSkillViewMode("doc"),
|
|
104
|
+
r: () => {
|
|
105
|
+
if (selectedMount && skillDoc) {
|
|
106
|
+
const op = skillDoc.schemas[selectedSchemaIndex];
|
|
107
|
+
if (op) fetchSchema(selectedMount.mount_point, op, client);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
y: () => {
|
|
111
|
+
if (schemaDoc?.content) copy(schemaDoc.content);
|
|
112
|
+
},
|
|
113
|
+
escape: () => setSkillViewMode("doc"),
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<box flexDirection="column" height="100%" width="100%">
|
|
119
|
+
{/* Mount selector (top row) */}
|
|
120
|
+
<box height={1} width="100%">
|
|
121
|
+
<text>
|
|
122
|
+
<span foregroundColor={statusColor.dim}>Mount: </span>
|
|
123
|
+
{mounts.length === 0 ? (
|
|
124
|
+
<span foregroundColor={statusColor.dim}>No mounts</span>
|
|
125
|
+
) : (
|
|
126
|
+
mounts.map((m, i) => (
|
|
127
|
+
<span
|
|
128
|
+
key={m.mount_point}
|
|
129
|
+
foregroundColor={i === selectedMountIndex ? statusColor.info : statusColor.dim}
|
|
130
|
+
bold={i === selectedMountIndex}
|
|
131
|
+
>
|
|
132
|
+
{i === selectedMountIndex ? `[${m.mount_point}]` : ` ${m.mount_point} `}
|
|
133
|
+
</span>
|
|
134
|
+
))
|
|
135
|
+
)}
|
|
136
|
+
<span foregroundColor={statusColor.dim}>{" "}</span>
|
|
137
|
+
<span foregroundColor={viewMode === "doc" ? statusColor.info : statusColor.dim}>
|
|
138
|
+
{viewMode === "doc" ? "[Doc]" : " Doc "}
|
|
139
|
+
</span>
|
|
140
|
+
<span foregroundColor={viewMode === "schema" ? statusColor.info : statusColor.dim}>
|
|
141
|
+
{viewMode === "schema" ? "[Schema]" : " Schema "}
|
|
142
|
+
</span>
|
|
143
|
+
</text>
|
|
144
|
+
</box>
|
|
145
|
+
|
|
146
|
+
{/* Content area */}
|
|
147
|
+
<box flexGrow={1} borderStyle="single" marginTop={1} flexDirection="column">
|
|
148
|
+
{viewMode === "doc" ? (
|
|
149
|
+
// SKILL.md viewer
|
|
150
|
+
skillDocLoading ? (
|
|
151
|
+
<LoadingIndicator message="Loading skill doc..." />
|
|
152
|
+
) : skillDoc?.content ? (
|
|
153
|
+
<box flexDirection="column" width="100%">
|
|
154
|
+
{skillDoc.content.split("\n").slice(0, 30).map((line, i) => (
|
|
155
|
+
<box key={i} height={1} width="100%">
|
|
156
|
+
<text>{line}</text>
|
|
157
|
+
</box>
|
|
158
|
+
))}
|
|
159
|
+
{skillDoc.content.split("\n").length > 30 && (
|
|
160
|
+
<box height={1} width="100%">
|
|
161
|
+
<text foregroundColor={statusColor.dim}>... (truncated, press y to copy full doc)</text>
|
|
162
|
+
</box>
|
|
163
|
+
)}
|
|
164
|
+
</box>
|
|
165
|
+
) : (
|
|
166
|
+
<box height={1} width="100%">
|
|
167
|
+
<text foregroundColor={statusColor.dim}>No skill doc available. Mount a connector with skill support.</text>
|
|
168
|
+
</box>
|
|
169
|
+
)
|
|
170
|
+
) : (
|
|
171
|
+
// Schema browser
|
|
172
|
+
<box flexDirection="row" height="100%" width="100%">
|
|
173
|
+
{/* Schema list (left) */}
|
|
174
|
+
<box width="30%" flexDirection="column" borderStyle="single">
|
|
175
|
+
<box height={1} width="100%">
|
|
176
|
+
<text bold foregroundColor={statusColor.info}>Operations</text>
|
|
177
|
+
</box>
|
|
178
|
+
{skillDoc?.schemas.map((op, i) => (
|
|
179
|
+
<box key={op} height={1} width="100%">
|
|
180
|
+
<text>
|
|
181
|
+
<span foregroundColor={i === selectedSchemaIndex ? statusColor.info : undefined}>
|
|
182
|
+
{i === selectedSchemaIndex ? `▶ ${op}` : ` ${op}`}
|
|
183
|
+
</span>
|
|
184
|
+
</text>
|
|
185
|
+
</box>
|
|
186
|
+
))}
|
|
187
|
+
{(!skillDoc || skillDoc.schemas.length === 0) && (
|
|
188
|
+
<box height={1} width="100%">
|
|
189
|
+
<text foregroundColor={statusColor.dim}>No schemas</text>
|
|
190
|
+
</box>
|
|
191
|
+
)}
|
|
192
|
+
</box>
|
|
193
|
+
|
|
194
|
+
{/* Schema content (right) */}
|
|
195
|
+
<box width="70%" flexDirection="column" paddingLeft={1}>
|
|
196
|
+
{schemaDocLoading ? (
|
|
197
|
+
<LoadingIndicator message="Loading schema..." />
|
|
198
|
+
) : schemaDoc?.content ? (
|
|
199
|
+
<box flexDirection="column" width="100%">
|
|
200
|
+
<box height={1} width="100%">
|
|
201
|
+
<text bold>{schemaDoc.operation}</text>
|
|
202
|
+
</box>
|
|
203
|
+
{schemaDoc.content.split("\n").slice(0, 25).map((line, i) => (
|
|
204
|
+
<box key={i} height={1} width="100%">
|
|
205
|
+
<text foregroundColor={statusColor.dim}>{line}</text>
|
|
206
|
+
</box>
|
|
207
|
+
))}
|
|
208
|
+
</box>
|
|
209
|
+
) : (
|
|
210
|
+
<box height={1} width="100%">
|
|
211
|
+
<text foregroundColor={statusColor.dim}>Select an operation to view its schema.</text>
|
|
212
|
+
</box>
|
|
213
|
+
)}
|
|
214
|
+
</box>
|
|
215
|
+
</box>
|
|
216
|
+
)}
|
|
217
|
+
</box>
|
|
218
|
+
|
|
219
|
+
{/* Help bar */}
|
|
220
|
+
<box height={1} width="100%">
|
|
221
|
+
{copied ? (
|
|
222
|
+
<text foregroundColor={statusColor.healthy}>Copied!</text>
|
|
223
|
+
) : viewMode === "doc" ? (
|
|
224
|
+
<text foregroundColor={statusColor.dim}>
|
|
225
|
+
j/k:select mount s:schemas y:copy r:refresh
|
|
226
|
+
</text>
|
|
227
|
+
) : (
|
|
228
|
+
<text foregroundColor={statusColor.dim}>
|
|
229
|
+
j/k:select operation Enter:view d:doc view y:copy Esc:back r:refresh
|
|
230
|
+
</text>
|
|
231
|
+
)}
|
|
232
|
+
</box>
|
|
233
|
+
</box>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-to-YAML template generator.
|
|
3
|
+
*
|
|
4
|
+
* Pure function that converts an annotated schema (YAML string from the API)
|
|
5
|
+
* into a pre-filled YAML template for the Write tab.
|
|
6
|
+
*
|
|
7
|
+
* The schema format is annotated YAML with comments like:
|
|
8
|
+
* field_name: # (required) Description — type: string
|
|
9
|
+
* optional_field: # (optional) Description — type: integer, default: 0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
export interface SchemaField {
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly type: string;
|
|
19
|
+
readonly required: boolean;
|
|
20
|
+
readonly description: string;
|
|
21
|
+
readonly default_value: string | null;
|
|
22
|
+
readonly enum_values: readonly string[];
|
|
23
|
+
readonly is_nested: boolean;
|
|
24
|
+
readonly children: readonly SchemaField[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Parser: extract fields from annotated schema YAML
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse an annotated schema string into structured fields.
|
|
33
|
+
*
|
|
34
|
+
* Handles formats like:
|
|
35
|
+
* field_name: # (required) Description — type: string
|
|
36
|
+
* field_name: value # (optional) Description — type: integer
|
|
37
|
+
* field_name: # (required) One of: val1, val2, val3
|
|
38
|
+
*/
|
|
39
|
+
export function parseSchemaFields(schemaContent: string): readonly SchemaField[] {
|
|
40
|
+
const lines = schemaContent.split("\n");
|
|
41
|
+
const fields: SchemaField[] = [];
|
|
42
|
+
let currentIndent = -1;
|
|
43
|
+
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
// Skip empty lines and pure comment lines (not field comments)
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
if (!trimmed || (trimmed.startsWith("#") && !trimmed.includes(":"))) continue;
|
|
48
|
+
|
|
49
|
+
// Match field pattern: name: [value] # comment
|
|
50
|
+
const fieldMatch = trimmed.match(
|
|
51
|
+
/^(\w[\w.]*)\s*:\s*(.*?)(?:\s*#\s*(.*))?$/,
|
|
52
|
+
);
|
|
53
|
+
if (!fieldMatch) continue;
|
|
54
|
+
|
|
55
|
+
const [, name, rawValue, comment] = fieldMatch;
|
|
56
|
+
if (!name) continue;
|
|
57
|
+
|
|
58
|
+
// Parse required/optional from comment
|
|
59
|
+
const isRequired = comment ? /\(required\)/i.test(comment) : false;
|
|
60
|
+
|
|
61
|
+
// Parse type from comment
|
|
62
|
+
const typeMatch = comment?.match(/type:\s*(\w+)/i);
|
|
63
|
+
const type = typeMatch?.[1] ?? "string";
|
|
64
|
+
|
|
65
|
+
// Parse description — everything before "type:" or "One of:" or "default:"
|
|
66
|
+
let description = comment ?? "";
|
|
67
|
+
description = description
|
|
68
|
+
.replace(/\(required\)/i, "")
|
|
69
|
+
.replace(/\(optional\)/i, "")
|
|
70
|
+
.replace(/type:\s*\w+/i, "")
|
|
71
|
+
.replace(/default:\s*\S+/i, "")
|
|
72
|
+
.replace(/One of:.*$/i, "")
|
|
73
|
+
.replace(/[—–-]\s*$/, "")
|
|
74
|
+
.trim();
|
|
75
|
+
|
|
76
|
+
// Parse default value
|
|
77
|
+
const defaultMatch = comment?.match(/default:\s*(\S+)/i);
|
|
78
|
+
const defaultValue = defaultMatch?.[1] ?? null;
|
|
79
|
+
|
|
80
|
+
// Parse enum values — strip trailing "default: X" from the match
|
|
81
|
+
const enumMatch = comment?.match(/One of:\s*(.+)$/i);
|
|
82
|
+
const enumValues: string[] = enumMatch
|
|
83
|
+
? enumMatch[1]
|
|
84
|
+
.replace(/,?\s*default:\s*\S+/i, "")
|
|
85
|
+
.split(",")
|
|
86
|
+
.map((v) => v.trim())
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
: [];
|
|
89
|
+
|
|
90
|
+
// Determine nesting from indent
|
|
91
|
+
const indent = line.search(/\S/);
|
|
92
|
+
const valueStr = rawValue.trim();
|
|
93
|
+
const isNested = valueStr === "" && !comment?.includes("type:");
|
|
94
|
+
|
|
95
|
+
fields.push({
|
|
96
|
+
name,
|
|
97
|
+
type,
|
|
98
|
+
required: isRequired,
|
|
99
|
+
description,
|
|
100
|
+
default_value: defaultValue,
|
|
101
|
+
enum_values: enumValues,
|
|
102
|
+
is_nested: isNested,
|
|
103
|
+
children: [],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return fields;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// =============================================================================
|
|
111
|
+
// Template generator
|
|
112
|
+
// =============================================================================
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Generate a YAML template from a schema string.
|
|
116
|
+
*
|
|
117
|
+
* Required fields are shown with placeholder values.
|
|
118
|
+
* Optional fields are commented out with their defaults.
|
|
119
|
+
* Enum fields show valid values as comments.
|
|
120
|
+
*/
|
|
121
|
+
export function generateTemplate(
|
|
122
|
+
schemaContent: string,
|
|
123
|
+
operationName: string,
|
|
124
|
+
): string {
|
|
125
|
+
const fields = parseSchemaFields(schemaContent);
|
|
126
|
+
|
|
127
|
+
if (fields.length === 0) {
|
|
128
|
+
// If we can't parse structured fields, return the schema as-is
|
|
129
|
+
// with a header comment — the user can edit it directly
|
|
130
|
+
return `# ${operationName}\n# Edit the fields below:\n\n${schemaContent}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const lines: string[] = [];
|
|
134
|
+
lines.push(`# ${operationName}`);
|
|
135
|
+
lines.push(`# Required fields are pre-filled. Optional fields are commented out.`);
|
|
136
|
+
lines.push("");
|
|
137
|
+
|
|
138
|
+
for (const field of fields) {
|
|
139
|
+
if (field.is_nested) {
|
|
140
|
+
// Section header
|
|
141
|
+
if (field.required) {
|
|
142
|
+
lines.push(`${field.name}:`);
|
|
143
|
+
} else {
|
|
144
|
+
lines.push(`# ${field.name}:`);
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const enumComment = field.enum_values.length > 0
|
|
150
|
+
? ` # One of: ${field.enum_values.join(", ")}`
|
|
151
|
+
: "";
|
|
152
|
+
|
|
153
|
+
const descComment = field.description
|
|
154
|
+
? ` # ${field.description}`
|
|
155
|
+
: "";
|
|
156
|
+
|
|
157
|
+
const placeholder = getPlaceholder(field);
|
|
158
|
+
|
|
159
|
+
if (field.required) {
|
|
160
|
+
lines.push(`${field.name}: ${placeholder}${enumComment || descComment}`);
|
|
161
|
+
} else {
|
|
162
|
+
// Optional fields are commented out
|
|
163
|
+
const value = field.default_value ?? placeholder;
|
|
164
|
+
lines.push(`# ${field.name}: ${value}${enumComment || descComment}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return lines.join("\n") + "\n";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get a sensible placeholder for a field based on its type.
|
|
173
|
+
*/
|
|
174
|
+
function getPlaceholder(field: SchemaField): string {
|
|
175
|
+
if (field.default_value) return field.default_value;
|
|
176
|
+
if (field.enum_values.length > 0) return field.enum_values[0];
|
|
177
|
+
|
|
178
|
+
switch (field.type.toLowerCase()) {
|
|
179
|
+
case "string":
|
|
180
|
+
return `"<${field.name}>"`;
|
|
181
|
+
case "integer":
|
|
182
|
+
case "int":
|
|
183
|
+
return "0";
|
|
184
|
+
case "number":
|
|
185
|
+
case "float":
|
|
186
|
+
return "0.0";
|
|
187
|
+
case "boolean":
|
|
188
|
+
case "bool":
|
|
189
|
+
return "false";
|
|
190
|
+
case "array":
|
|
191
|
+
case "list":
|
|
192
|
+
return "[]";
|
|
193
|
+
case "object":
|
|
194
|
+
case "dict":
|
|
195
|
+
return "{}";
|
|
196
|
+
default:
|
|
197
|
+
return `"<${field.name}>"`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Generate a template directly from an operation name and raw schema content.
|
|
203
|
+
*
|
|
204
|
+
* Convenience wrapper used by the Write tab.
|
|
205
|
+
*/
|
|
206
|
+
export function generateWriteTemplate(
|
|
207
|
+
operationName: string,
|
|
208
|
+
schemaContent: string,
|
|
209
|
+
): string {
|
|
210
|
+
return generateTemplate(schemaContent, operationName);
|
|
211
|
+
}
|