@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,303 @@
1
+ /**
2
+ * CommandRunner — executes local `nexus` CLI subcommands via Bun.spawn().
3
+ *
4
+ * Decisions implemented:
5
+ * 2A: Shell out to the Python `nexus` binary
6
+ * 4A: Strict allowlist (validated in parseCommand, enforced here as defense-in-depth)
7
+ * 6A: Process lifecycle management with cleanup on shutdown
8
+ * 7A: Accumulator buffer for streaming output
9
+ * 13A+C: Windowed rendering (last MAX_OUTPUT_LINES) + throttled state updates
10
+ * 15A: Show spinner immediately (handled by consumer component)
11
+ */
12
+
13
+ import { create } from "zustand";
14
+
15
+ // =============================================================================
16
+ // Constants
17
+ // =============================================================================
18
+
19
+ /** Maximum lines retained in the output buffer (Decision 13A). */
20
+ const MAX_OUTPUT_LINES = 200;
21
+
22
+ /** Minimum interval between state updates in ms (Decision 13C). */
23
+ const THROTTLE_MS = 100;
24
+
25
+ /** Defense-in-depth: re-validate the subcommand even though parseCommand already checks. */
26
+ const ALLOWED_COMMANDS = new Set(["init", "build", "demo", "brick", "agent", "up"]);
27
+
28
+ // =============================================================================
29
+ // Types
30
+ // =============================================================================
31
+
32
+ export type CommandStatus = "idle" | "running" | "success" | "error";
33
+
34
+ export interface CommandRunnerState {
35
+ /** Current command status. */
36
+ readonly status: CommandStatus;
37
+ /** Output lines (windowed to last MAX_OUTPUT_LINES). */
38
+ readonly outputLines: readonly string[];
39
+ /** Exit code of the last command (null while running). */
40
+ readonly exitCode: number | null;
41
+ /** The command string being/was executed. */
42
+ readonly commandLabel: string;
43
+ /** Error message if the command failed to spawn. */
44
+ readonly spawnError: string | null;
45
+ }
46
+
47
+ export interface CommandRunnerStore extends CommandRunnerState {
48
+ readonly appendOutput: (chunk: string) => void;
49
+ readonly setStatus: (status: CommandStatus, exitCode?: number | null) => void;
50
+ readonly setSpawnError: (error: string) => void;
51
+ readonly reset: () => void;
52
+ }
53
+
54
+ // =============================================================================
55
+ // Store
56
+ // =============================================================================
57
+
58
+ const INITIAL_STATE: CommandRunnerState = {
59
+ status: "idle",
60
+ outputLines: [],
61
+ exitCode: null,
62
+ commandLabel: "",
63
+ spawnError: null,
64
+ };
65
+
66
+ export const useCommandRunnerStore = create<CommandRunnerStore>((set) => ({
67
+ ...INITIAL_STATE,
68
+
69
+ appendOutput: (chunk) => {
70
+ set((state) => {
71
+ // Split chunk into lines, preserving partial last line
72
+ const newLines = chunk.split("\n");
73
+ const combined = [...state.outputLines];
74
+
75
+ // Append first fragment to the last existing line (handles partial lines)
76
+ if (combined.length > 0 && newLines.length > 0) {
77
+ combined[combined.length - 1] = combined[combined.length - 1]! + newLines[0]!;
78
+ newLines.shift();
79
+ }
80
+
81
+ combined.push(...newLines);
82
+
83
+ // Window to last MAX_OUTPUT_LINES (Decision 13A)
84
+ const windowed = combined.length > MAX_OUTPUT_LINES
85
+ ? combined.slice(-MAX_OUTPUT_LINES)
86
+ : combined;
87
+
88
+ return { outputLines: windowed };
89
+ });
90
+ },
91
+
92
+ setStatus: (status, exitCode) => {
93
+ set({ status, exitCode: exitCode ?? null });
94
+ },
95
+
96
+ setSpawnError: (error) => {
97
+ set({ status: "error", spawnError: error });
98
+ },
99
+
100
+ reset: () => {
101
+ set(INITIAL_STATE);
102
+ },
103
+ }));
104
+
105
+ // =============================================================================
106
+ // Process management (Decision 6A)
107
+ // =============================================================================
108
+
109
+ /** Set of currently running child processes for cleanup on shutdown. */
110
+ const activeProcesses = new Set<{ kill(): void }>();
111
+
112
+ /**
113
+ * Kill all running child processes. Called from the shutdown handler.
114
+ */
115
+ export function killAllProcesses(): void {
116
+ for (const proc of activeProcesses) {
117
+ try {
118
+ proc.kill();
119
+ } catch {
120
+ // Process may have already exited
121
+ }
122
+ }
123
+ activeProcesses.clear();
124
+ }
125
+
126
+ // =============================================================================
127
+ // Execute local command
128
+ // =============================================================================
129
+
130
+ /**
131
+ * Find the project root by walking up from CWD looking for .git (file or dir).
132
+ * This resolves correctly for both normal repos and git worktrees.
133
+ * Falls back to CWD if not found.
134
+ */
135
+ function findProjectRoot(): string {
136
+ const path = require("node:path");
137
+ const nodeFs = require("node:fs");
138
+ let dir = process.cwd();
139
+ for (let i = 0; i < 20; i++) {
140
+ if (nodeFs.existsSync(path.join(dir, ".git"))) return dir;
141
+ const parent = path.dirname(dir);
142
+ if (parent === dir) break;
143
+ dir = parent;
144
+ }
145
+ return process.cwd();
146
+ }
147
+
148
+ /**
149
+ * Execute a local nexus subcommand via Bun.spawn().
150
+ *
151
+ * Output is streamed into the CommandRunnerStore for rendering by CommandOutput.
152
+ */
153
+ export function executeLocalCommand(command: string, args: readonly string[]): void {
154
+ // Defense-in-depth: re-validate allowlist (already checked in parseCommand)
155
+ if (!ALLOWED_COMMANDS.has(command)) {
156
+ useCommandRunnerStore.getState().setSpawnError(
157
+ `Command "${command}" is not in the allowlist. Allowed: ${[...ALLOWED_COMMANDS].join(", ")}`,
158
+ );
159
+ return;
160
+ }
161
+
162
+ const store = useCommandRunnerStore.getState();
163
+
164
+ // Don't start a new command if one is already running
165
+ if (store.status === "running") {
166
+ return;
167
+ }
168
+
169
+ // Resolve project root (git/worktree root) so commands like `nexus init`
170
+ // create nexus.yaml at the right level, not inside packages/nexus-tui/.
171
+ const projectRoot = findProjectRoot();
172
+
173
+ // Reset state
174
+ useCommandRunnerStore.setState({
175
+ ...INITIAL_STATE,
176
+ status: "running",
177
+ commandLabel: `nexus ${command} ${args.join(" ")}`.trim(),
178
+ });
179
+
180
+ // Prefer .venv/bin/nexus (project venv) over system PATH to avoid picking up
181
+ // stale installs (e.g. /opt/anaconda3/bin/nexus which lacks the `up` command).
182
+ // Walk up from project root to find .venv/bin/nexus.
183
+ const path = require("node:path");
184
+ const nodeFs = require("node:fs");
185
+ let nexusBin = "nexus";
186
+ let searchDir = projectRoot;
187
+ for (let i = 0; i < 5; i++) {
188
+ const candidate = path.join(searchDir, ".venv", "bin", "nexus");
189
+ if (nodeFs.existsSync(candidate)) {
190
+ nexusBin = candidate;
191
+ break;
192
+ }
193
+ const parent = path.dirname(searchDir);
194
+ if (parent === searchDir) break;
195
+ searchDir = parent;
196
+ }
197
+ const fullArgs = [nexusBin, command, ...args];
198
+
199
+ // Read nexus.yaml from project root to pass NEXUS_URL and NEXUS_API_KEY
200
+ // to subcommands like `nexus demo init`.
201
+ const spawnEnv = { ...process.env };
202
+ try {
203
+ const fs = require("node:fs");
204
+ const yaml = fs.readFileSync(path.join(projectRoot, "nexus.yaml"), "utf-8") as string;
205
+ const portMatch = yaml.match(/ports:\s*\n(?:\s+\w+:[^\n]*\n)*?\s+http:\s*(\d+)/);
206
+ const keyMatch = yaml.match(/^api_key:\s*["']?([^"'\n]+)["']?/m);
207
+ if (portMatch?.[1] && !spawnEnv.NEXUS_URL) {
208
+ spawnEnv.NEXUS_URL = `http://localhost:${portMatch[1]}`;
209
+ }
210
+ if (keyMatch?.[1] && !spawnEnv.NEXUS_API_KEY) {
211
+ spawnEnv.NEXUS_API_KEY = keyMatch[1];
212
+ }
213
+ } catch {
214
+ // nexus.yaml not found yet (will be created by nexus init)
215
+ }
216
+
217
+ try {
218
+ // Commands run from project root so nexus.yaml is created at the right level
219
+ const proc = Bun.spawn(fullArgs, {
220
+ cwd: projectRoot,
221
+ stdout: "pipe",
222
+ stderr: "pipe",
223
+ env: spawnEnv,
224
+ });
225
+
226
+ activeProcesses.add(proc);
227
+
228
+ // Throttled output flushing (Decision 13C)
229
+ let pendingChunks = "";
230
+ let flushTimer: ReturnType<typeof setTimeout> | null = null;
231
+
232
+ function flushOutput(): void {
233
+ if (pendingChunks) {
234
+ useCommandRunnerStore.getState().appendOutput(pendingChunks);
235
+ pendingChunks = "";
236
+ }
237
+ flushTimer = null;
238
+ }
239
+
240
+ function bufferChunk(text: string): void {
241
+ pendingChunks += text;
242
+ if (!flushTimer) {
243
+ flushTimer = setTimeout(flushOutput, THROTTLE_MS);
244
+ }
245
+ }
246
+
247
+ // Stream stdout
248
+ (async () => {
249
+ try {
250
+ const reader = proc.stdout.getReader();
251
+ const decoder = new TextDecoder();
252
+ while (true) {
253
+ const { done, value } = await reader.read();
254
+ if (done) break;
255
+ bufferChunk(decoder.decode(value, { stream: true }));
256
+ }
257
+ } catch {
258
+ // Stream closed
259
+ }
260
+ })();
261
+
262
+ // Stream stderr (interleaved with stdout)
263
+ (async () => {
264
+ try {
265
+ const reader = proc.stderr.getReader();
266
+ const decoder = new TextDecoder();
267
+ while (true) {
268
+ const { done, value } = await reader.read();
269
+ if (done) break;
270
+ bufferChunk(decoder.decode(value, { stream: true }));
271
+ }
272
+ } catch {
273
+ // Stream closed
274
+ }
275
+ })();
276
+
277
+ // Wait for process to complete
278
+ proc.exited.then((exitCode) => {
279
+ activeProcesses.delete(proc);
280
+ // Flush any remaining buffered output
281
+ if (flushTimer) {
282
+ clearTimeout(flushTimer);
283
+ }
284
+ flushOutput();
285
+
286
+ useCommandRunnerStore.getState().setStatus(
287
+ exitCode === 0 ? "success" : "error",
288
+ exitCode,
289
+ );
290
+ });
291
+ } catch (err) {
292
+ const message = err instanceof Error ? err.message : "Failed to spawn command";
293
+
294
+ // Common case: `nexus` binary not found
295
+ if (message.includes("ENOENT") || message.includes("not found")) {
296
+ useCommandRunnerStore.getState().setSpawnError(
297
+ `"nexus" command not found on PATH. Install the Nexus CLI: pip install nexus`,
298
+ );
299
+ } else {
300
+ useCommandRunnerStore.getState().setSpawnError(message);
301
+ }
302
+ }
303
+ }
@@ -0,0 +1,44 @@
1
+ import type { ConnectionStatus } from "../stores/global-store.js";
2
+
3
+ export type AnnouncementLevel = "info" | "success" | "error";
4
+
5
+ export function normalizeAnnouncementMessage(message: string): string {
6
+ return message.replace(/\s+/g, " ").trim();
7
+ }
8
+
9
+ export function formatPanelAnnouncement(label: string): string {
10
+ return normalizeAnnouncementMessage(`Panel ${label}`);
11
+ }
12
+
13
+ export function formatConnectionAnnouncement(
14
+ status: ConnectionStatus,
15
+ error?: string | null,
16
+ ): string {
17
+ switch (status) {
18
+ case "connected":
19
+ return "Connected";
20
+ case "connecting":
21
+ return "Connecting";
22
+ case "disconnected":
23
+ return "Disconnected";
24
+ case "error":
25
+ return normalizeAnnouncementMessage(`Connection error${error ? `: ${error}` : ""}`);
26
+ }
27
+ }
28
+
29
+ export function formatDirectoryAnnouncement(path: string, count: number): string {
30
+ const noun = count === 1 ? "item" : "items";
31
+ return normalizeAnnouncementMessage(`${count} ${noun} in ${path}`);
32
+ }
33
+
34
+ export function formatSelectionAnnouncement(name: string, isDirectory: boolean): string {
35
+ return normalizeAnnouncementMessage(`Selected ${isDirectory ? "folder" : "file"} ${name}`);
36
+ }
37
+
38
+ export function formatErrorAnnouncement(message: string): string {
39
+ return normalizeAnnouncementMessage(`Error: ${message}`);
40
+ }
41
+
42
+ export function formatSuccessAnnouncement(message: string): string {
43
+ return normalizeAnnouncementMessage(message);
44
+ }