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