@duckmind/dm-windows-x64 0.63.4 → 0.63.9

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 (121) hide show
  1. package/dm.exe +0 -0
  2. package/extensions/.dm-extensions.json +236 -13
  3. package/extensions/dm-context/package.json +1 -1
  4. package/extensions/dm-context/skills/context-management/SKILL.md +173 -223
  5. package/extensions/dm-context/skills/context-management/references/development-and-troubleshooting.md +75 -0
  6. package/extensions/dm-context/skills/context-management/references/interleaved-async-work.md +143 -0
  7. package/extensions/dm-context/skills/context-management/references/planning-and-execution.md +81 -0
  8. package/extensions/dm-context/skills/context-management/references/repeated-items-and-batch-work.md +69 -0
  9. package/extensions/dm-context/skills/context-management/references/retry-branch-and-pivot.md +80 -0
  10. package/extensions/dm-context/skills/context-management/references/search-research-and-reading.md +103 -0
  11. package/extensions/dm-context/skills/context-management/references/task-switching-and-cleanup.md +73 -0
  12. package/extensions/dm-context/src/context.js +3 -2
  13. package/extensions/dm-context/src/index.js +168 -84
  14. package/extensions/dm-mcp-adapter/OAUTH.md +3 -0
  15. package/extensions/dm-mcp-adapter/THIRD_PARTY_NOTICES.md +4 -0
  16. package/extensions/dm-mcp-adapter/abort.js +35 -0
  17. package/extensions/dm-mcp-adapter/agent-dir.js +45 -0
  18. package/extensions/dm-mcp-adapter/agent-plugin-loader.js +349 -0
  19. package/extensions/dm-mcp-adapter/app-bridge.bundle.js +5843 -0
  20. package/extensions/dm-mcp-adapter/cli.js +354 -0
  21. package/extensions/dm-mcp-adapter/commands.js +619 -0
  22. package/extensions/dm-mcp-adapter/config.js +1060 -0
  23. package/extensions/dm-mcp-adapter/consent-manager.js +60 -0
  24. package/extensions/dm-mcp-adapter/direct-tools.js +544 -0
  25. package/extensions/dm-mcp-adapter/dist/agent-dir.js +45 -0
  26. package/extensions/dm-mcp-adapter/dist/agent-plugin-loader.js +349 -0
  27. package/extensions/dm-mcp-adapter/dist/config.js +1060 -0
  28. package/extensions/dm-mcp-adapter/dist/mcp-bearer-store.js +311 -0
  29. package/extensions/dm-mcp-adapter/dist/metadata-cache.js +285 -0
  30. package/extensions/dm-mcp-adapter/dist/package-mcp-loader.js +144 -0
  31. package/extensions/dm-mcp-adapter/dist/resource-tools.js +7 -0
  32. package/extensions/dm-mcp-adapter/dist/types.js +247 -0
  33. package/extensions/dm-mcp-adapter/dist/ui-app-bridge-helpers.js +36 -0
  34. package/extensions/dm-mcp-adapter/dist/ui-stream-types.js +58 -0
  35. package/extensions/dm-mcp-adapter/dist/ui-tool-visibility.js +26 -0
  36. package/extensions/dm-mcp-adapter/dist/utils.js +307 -0
  37. package/extensions/dm-mcp-adapter/elicitation-handler.js +306 -0
  38. package/extensions/dm-mcp-adapter/error-signal.js +16 -0
  39. package/extensions/dm-mcp-adapter/errors.js +225 -0
  40. package/extensions/dm-mcp-adapter/failure-backoff.js +19 -0
  41. package/extensions/dm-mcp-adapter/glimpse-ui.js +61 -0
  42. package/extensions/dm-mcp-adapter/host-html-template.js +464 -0
  43. package/extensions/dm-mcp-adapter/index.js +1079 -0
  44. package/extensions/dm-mcp-adapter/init.js +624 -0
  45. package/extensions/dm-mcp-adapter/json-schema-validator.js +53 -0
  46. package/extensions/dm-mcp-adapter/lifecycle.js +385 -0
  47. package/extensions/dm-mcp-adapter/logger.js +115 -0
  48. package/extensions/dm-mcp-adapter/mcp-auth-flow.js +824 -0
  49. package/extensions/dm-mcp-adapter/mcp-auth.js +732 -0
  50. package/extensions/dm-mcp-adapter/mcp-bearer-store.js +311 -0
  51. package/extensions/dm-mcp-adapter/mcp-callback-server.js +388 -0
  52. package/extensions/dm-mcp-adapter/mcp-code.js +291 -0
  53. package/extensions/dm-mcp-adapter/mcp-keyring-helper.cjs +88 -0
  54. package/extensions/dm-mcp-adapter/mcp-oauth-provider.js +490 -0
  55. package/extensions/dm-mcp-adapter/mcp-output-guard.js +386 -0
  56. package/extensions/dm-mcp-adapter/mcp-panel.js +951 -0
  57. package/extensions/dm-mcp-adapter/mcp-probe.js +151 -0
  58. package/extensions/dm-mcp-adapter/mcp-references.js +263 -0
  59. package/extensions/dm-mcp-adapter/mcp-script-worker.mjs +126 -0
  60. package/extensions/dm-mcp-adapter/mcp-setup-panel.js +578 -0
  61. package/extensions/dm-mcp-adapter/mcp-status.js +80 -0
  62. package/extensions/dm-mcp-adapter/mcp-trace.js +215 -0
  63. package/extensions/dm-mcp-adapter/metadata-cache.js +292 -0
  64. package/extensions/dm-mcp-adapter/namespace-tools.js +155 -0
  65. package/extensions/dm-mcp-adapter/npx-resolver.js +465 -0
  66. package/extensions/dm-mcp-adapter/oauth-handler.js +17 -0
  67. package/extensions/dm-mcp-adapter/oauth.js +17 -0
  68. package/extensions/dm-mcp-adapter/onboarding-state.js +62 -0
  69. package/extensions/dm-mcp-adapter/package-mcp-loader.js +144 -0
  70. package/extensions/dm-mcp-adapter/package.json +68 -0
  71. package/extensions/dm-mcp-adapter/panel-keys.js +26 -0
  72. package/extensions/dm-mcp-adapter/prompts.js +258 -0
  73. package/extensions/dm-mcp-adapter/proxy-modes.js +1336 -0
  74. package/extensions/dm-mcp-adapter/request-headers-command.js +293 -0
  75. package/extensions/dm-mcp-adapter/resource-tools.js +7 -0
  76. package/extensions/dm-mcp-adapter/runtime-owner.js +79 -0
  77. package/extensions/dm-mcp-adapter/sampling-handler.js +222 -0
  78. package/extensions/dm-mcp-adapter/sandbox-proxy-template.js +190 -0
  79. package/extensions/dm-mcp-adapter/search-ranking.js +198 -0
  80. package/extensions/dm-mcp-adapter/server-manager.js +1258 -0
  81. package/extensions/dm-mcp-adapter/session-recovery.js +82 -0
  82. package/extensions/dm-mcp-adapter/skills/mcp-scripting/SKILL.md +39 -0
  83. package/extensions/dm-mcp-adapter/state.js +0 -0
  84. package/extensions/dm-mcp-adapter/test-runner.mjs +18 -0
  85. package/extensions/dm-mcp-adapter/tool-approval.js +151 -0
  86. package/extensions/dm-mcp-adapter/tool-metadata.js +247 -0
  87. package/extensions/dm-mcp-adapter/tool-registrar.js +212 -0
  88. package/extensions/dm-mcp-adapter/tool-result-renderer.js +355 -0
  89. package/extensions/dm-mcp-adapter/ts-shape.js +157 -0
  90. package/extensions/dm-mcp-adapter/types.js +264 -0
  91. package/extensions/dm-mcp-adapter/ui-app-bridge-helpers.js +36 -0
  92. package/extensions/dm-mcp-adapter/ui-resource-handler.js +209 -0
  93. package/extensions/dm-mcp-adapter/ui-server.js +815 -0
  94. package/extensions/dm-mcp-adapter/ui-session.js +455 -0
  95. package/extensions/dm-mcp-adapter/ui-stream-types.js +58 -0
  96. package/extensions/dm-mcp-adapter/ui-tool-visibility.js +26 -0
  97. package/extensions/dm-mcp-adapter/unix-socket-transport.js +83 -0
  98. package/extensions/dm-mcp-adapter/utils.js +307 -0
  99. package/extensions/dm-skills-manager/THIRD_PARTY_NOTICES.md +27 -0
  100. package/extensions/dm-skills-manager/extensions/skills-manager/components.js +265 -0
  101. package/extensions/dm-skills-manager/extensions/skills-manager/constants.js +31 -0
  102. package/extensions/dm-skills-manager/extensions/skills-manager/creation-fallback.js +20 -0
  103. package/extensions/dm-skills-manager/extensions/skills-manager/creation.js +145 -0
  104. package/extensions/dm-skills-manager/extensions/skills-manager/dialog.js +738 -0
  105. package/extensions/dm-skills-manager/extensions/skills-manager/dm-ai-compat.js +15 -0
  106. package/extensions/dm-skills-manager/extensions/skills-manager/format.js +125 -0
  107. package/extensions/dm-skills-manager/extensions/skills-manager/glyphs.js +142 -0
  108. package/extensions/dm-skills-manager/extensions/skills-manager/layout.js +93 -0
  109. package/extensions/dm-skills-manager/extensions/skills-manager/paths.js +76 -0
  110. package/extensions/dm-skills-manager/extensions/skills-manager/registry.js +95 -0
  111. package/extensions/dm-skills-manager/extensions/skills-manager/settings.js +93 -0
  112. package/extensions/dm-skills-manager/extensions/skills-manager/startup.js +32 -0
  113. package/extensions/dm-skills-manager/extensions/skills-manager/toggle.js +54 -0
  114. package/extensions/dm-skills-manager/extensions/skills-manager/types.js +14 -0
  115. package/extensions/dm-skills-manager/extensions/skills-manager/ui.js +121 -0
  116. package/extensions/dm-skills-manager/extensions/skills-manager.js +111 -0
  117. package/extensions/dm-skills-manager/package.json +121 -0
  118. package/package.json +13 -1
  119. package/extensions/dm-localllm-provider/detect.test.js +0 -1051
  120. package/extensions/dm-localllm-provider/index.test.js +0 -149
  121. package/extensions/dm-localllm-provider/keychain.test.js +0 -61
@@ -0,0 +1,951 @@
1
+ import { matchesKey, truncateToWidth, visibleWidth } from "@duckmind/dm-tui";
2
+ import { copyToClipboard } from "@duckmind/dm-coding-agent";
3
+ import { createPanelKeys } from "./panel-keys.js";
4
+ import { getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.js";
5
+ import { resourceNameToToolName } from "./resource-tools.js";
6
+ import { sanitizeTerminalText, stripOscSequences } from "./utils.js";
7
+ import { isServerCacheValid } from "./metadata-cache.js";
8
+ import { isUiToolVisibleToModel } from "./ui-tool-visibility.js";
9
+ const DEFAULT_THEME = {
10
+ border: "2",
11
+ title: "2",
12
+ selected: "36",
13
+ direct: "32",
14
+ needsAuth: "33",
15
+ placeholder: "2;3",
16
+ description: "2",
17
+ hint: "2",
18
+ confirm: "32",
19
+ cancel: "31"
20
+ };
21
+ function fg(code, text) {
22
+ if (!code)
23
+ return text;
24
+ return `\x1B[${code}m${text}\x1B[0m`;
25
+ }
26
+ const RAINBOW_COLORS = [
27
+ "38;2;178;129;214",
28
+ "38;2;215;135;175",
29
+ "38;2;254;188;56",
30
+ "38;2;228;192;15",
31
+ "38;2;137;210;129",
32
+ "38;2;0;175;175",
33
+ "38;2;23;143;185"
34
+ ];
35
+ function rainbowProgress(filled, total) {
36
+ const dots = [];
37
+ for (let i = 0;i < total; i++) {
38
+ const color = RAINBOW_COLORS[i % RAINBOW_COLORS.length];
39
+ if (!color)
40
+ continue;
41
+ dots.push(fg(color, i < filled ? "●" : "○"));
42
+ }
43
+ return dots.join(" ");
44
+ }
45
+ function fuzzyScore(query, text) {
46
+ const lq = query.toLowerCase();
47
+ const lt = text.toLowerCase();
48
+ if (lt.includes(lq))
49
+ return 100 + lq.length / lt.length * 50;
50
+ let score = 0;
51
+ let qi = 0;
52
+ let consecutive = 0;
53
+ for (let i = 0;i < lt.length && qi < lq.length; i++) {
54
+ if (lt[i] === lq[qi]) {
55
+ score += 10 + consecutive;
56
+ consecutive += 5;
57
+ qi++;
58
+ } else {
59
+ consecutive = 0;
60
+ }
61
+ }
62
+ return qi === lq.length ? score : 0;
63
+ }
64
+ function sanitizeDisplayText(text) {
65
+ return sanitizeTerminalText(text ?? "");
66
+ }
67
+ function sanitizeRowContent(content) {
68
+ const withoutOsc = stripOscSequences(content);
69
+ let result = "";
70
+ let pendingSpace = false;
71
+ for (let i = 0;i < withoutOsc.length; i++) {
72
+ const rest = withoutOsc.slice(i);
73
+ const ansi = rest.match(/^(?:\x1b\[[0-?]*[ -/]*[@-~]|\x1b[@-Z\\-_])/);
74
+ if (ansi) {
75
+ result += ansi[0];
76
+ i += ansi[0].length - 1;
77
+ continue;
78
+ }
79
+ const code = withoutOsc.charCodeAt(i);
80
+ if (code <= 31 || code === 127 || code >= 128 && code <= 159) {
81
+ pendingSpace = true;
82
+ continue;
83
+ }
84
+ if (pendingSpace && result && !result.endsWith(" ")) {
85
+ result += " ";
86
+ }
87
+ pendingSpace = false;
88
+ result += withoutOsc[i];
89
+ }
90
+ return result;
91
+ }
92
+ function estimateTokens(tool) {
93
+ const schemaLen = JSON.stringify(tool.inputSchema ?? {}).length;
94
+ const descLen = tool.description?.length ?? 0;
95
+ return Math.ceil((tool.name.length + descLen + schemaLen) / 4) + 10;
96
+ }
97
+
98
+ class McpPanel {
99
+ config;
100
+ cache;
101
+ callbacks;
102
+ done;
103
+ noticeLines;
104
+ prefix;
105
+ servers = [];
106
+ cursorIndex = 0;
107
+ nameQuery = "";
108
+ descSearchActive = false;
109
+ descQuery = "";
110
+ dirty = false;
111
+ confirmingDiscard = false;
112
+ discardSelected = 1;
113
+ importNotice = null;
114
+ authNotice = null;
115
+ authInFlight = null;
116
+ inactivityTimeout = null;
117
+ visibleItems = [];
118
+ tui;
119
+ t = DEFAULT_THEME;
120
+ authOnly;
121
+ keys;
122
+ static MAX_VISIBLE = 12;
123
+ static INACTIVITY_MS = 60000;
124
+ constructor(config, cache, provenance, callbacks, tui, done, options = {}) {
125
+ this.config = config;
126
+ this.cache = cache;
127
+ this.callbacks = callbacks;
128
+ this.done = done;
129
+ this.tui = tui;
130
+ this.noticeLines = options.noticeLines ?? [];
131
+ this.authOnly = options.authOnly === true;
132
+ this.keys = createPanelKeys(options.keybindings);
133
+ this.prefix = config.settings?.toolPrefix ?? "server";
134
+ for (const [serverName, definition] of Object.entries(config.mcpServers)) {
135
+ if (this.authOnly && !callbacks.canAuthenticate(serverName))
136
+ continue;
137
+ const prov = provenance.get(serverName);
138
+ const cachedEntry = this.cache?.servers?.[serverName];
139
+ const serverCache = cachedEntry && isServerCacheValid(cachedEntry, definition) ? cachedEntry : undefined;
140
+ const globalDirect = config.settings?.directTools;
141
+ let toolFilter = false;
142
+ if (definition.directTools !== undefined) {
143
+ toolFilter = definition.directTools;
144
+ } else if (globalDirect) {
145
+ toolFilter = globalDirect;
146
+ }
147
+ const tools = [];
148
+ if (serverCache && !this.authOnly && !isServerDisabled(definition)) {
149
+ for (const tool of serverCache.tools ?? []) {
150
+ if (!isUiToolVisibleToModel(tool.uiVisibility))
151
+ continue;
152
+ if (!isToolAllowed(tool.name, serverName, this.prefix, definition.includeTools, definition.excludeTools, this.getOtherCurrentCandidates(serverName, definition, serverCache, tool.name))) {
153
+ continue;
154
+ }
155
+ const isDirect = toolFilter === true || Array.isArray(toolFilter) && toolFilter.includes(tool.name);
156
+ tools.push({
157
+ name: tool.name,
158
+ description: tool.description ?? "",
159
+ isDirect,
160
+ wasDirect: isDirect,
161
+ estimatedTokens: estimateTokens(tool)
162
+ });
163
+ }
164
+ if (definition.exposeResources !== false) {
165
+ for (const resource of serverCache.resources ?? []) {
166
+ const baseName = `read_${resourceNameToToolName(resource.name)}`;
167
+ if (!isToolAllowed(baseName, serverName, this.prefix, definition.includeTools, definition.excludeTools, this.getOtherCurrentCandidates(serverName, definition, serverCache, baseName))) {
168
+ continue;
169
+ }
170
+ const isDirect = toolFilter === true || Array.isArray(toolFilter) && toolFilter.includes(baseName);
171
+ const ct = {
172
+ name: baseName,
173
+ ...resource.description !== undefined ? { description: resource.description } : {}
174
+ };
175
+ tools.push({
176
+ name: baseName,
177
+ description: resource.description ?? `Read resource: ${resource.uri}`,
178
+ isDirect,
179
+ wasDirect: isDirect,
180
+ estimatedTokens: estimateTokens(ct)
181
+ });
182
+ }
183
+ }
184
+ }
185
+ const status = callbacks.getConnectionStatus(serverName);
186
+ const failureMessage = callbacks.getFailureMessage?.(serverName) ?? null;
187
+ const serverDisabled = isServerDisabled(definition);
188
+ let directCount = 0;
189
+ let directTokens = 0;
190
+ for (const tool of tools) {
191
+ if (!tool.isDirect)
192
+ continue;
193
+ directCount++;
194
+ directTokens += tool.estimatedTokens;
195
+ }
196
+ this.servers.push({
197
+ name: serverName,
198
+ expanded: false,
199
+ source: prov?.kind ?? "user",
200
+ ...prov?.importKind !== undefined ? { importKind: prov.importKind } : {},
201
+ ...definition.includeTools !== undefined ? { includeTools: definition.includeTools } : {},
202
+ ...definition.excludeTools !== undefined ? { excludeTools: definition.excludeTools } : {},
203
+ exposeResources: definition.exposeResources !== false,
204
+ disabled: serverDisabled,
205
+ wasDisabled: serverDisabled,
206
+ connectionStatus: status,
207
+ failureMessage,
208
+ tools,
209
+ directCount,
210
+ directTokens,
211
+ hasCachedData: !!serverCache
212
+ });
213
+ }
214
+ this.rebuildVisibleItems();
215
+ this.resetInactivityTimeout();
216
+ }
217
+ resetInactivityTimeout() {
218
+ if (this.inactivityTimeout)
219
+ clearTimeout(this.inactivityTimeout);
220
+ this.inactivityTimeout = setTimeout(() => {
221
+ this.cleanup();
222
+ this.done({ cancelled: true, changes: new Map, disabledChanges: new Map });
223
+ }, McpPanel.INACTIVITY_MS);
224
+ }
225
+ cleanup() {
226
+ if (this.inactivityTimeout) {
227
+ clearTimeout(this.inactivityTimeout);
228
+ this.inactivityTimeout = null;
229
+ }
230
+ }
231
+ rebuildVisibleItems() {
232
+ const query = this.descSearchActive ? this.descQuery : this.nameQuery;
233
+ const mode = this.descSearchActive ? "desc" : "name";
234
+ this.visibleItems = [];
235
+ for (let si = 0;si < this.servers.length; si++) {
236
+ const server = this.servers[si];
237
+ if (!server)
238
+ continue;
239
+ if (query && this.authOnly) {
240
+ const score = mode === "name" ? fuzzyScore(query, server.name) : 0;
241
+ if (score > 0) {
242
+ this.visibleItems.push({ type: "server", serverIndex: si });
243
+ }
244
+ continue;
245
+ }
246
+ this.visibleItems.push({ type: "server", serverIndex: si });
247
+ if (server.expanded || query) {
248
+ for (let ti = 0;ti < server.tools.length; ti++) {
249
+ const tool = server.tools[ti];
250
+ if (!tool)
251
+ continue;
252
+ if (query) {
253
+ const score = mode === "name" ? Math.max(fuzzyScore(query, tool.name), fuzzyScore(query, server.name) * 0.6) : fuzzyScore(query, tool.description);
254
+ if (score === 0)
255
+ continue;
256
+ }
257
+ this.visibleItems.push({ type: "tool", serverIndex: si, toolIndex: ti });
258
+ }
259
+ }
260
+ }
261
+ if (query && !this.authOnly) {
262
+ this.visibleItems = this.visibleItems.filter((item) => {
263
+ if (item.type === "server") {
264
+ return this.visibleItems.some((other) => other.type === "tool" && other.serverIndex === item.serverIndex);
265
+ }
266
+ return true;
267
+ });
268
+ }
269
+ }
270
+ updateDirty() {
271
+ this.dirty = this.servers.some((s) => s.disabled !== s.wasDisabled || s.tools.some((t) => t.isDirect !== t.wasDirect));
272
+ }
273
+ buildResult() {
274
+ const changes = new Map;
275
+ const disabledChanges = new Map;
276
+ for (const server of this.servers) {
277
+ if (server.disabled !== server.wasDisabled) {
278
+ disabledChanges.set(server.name, server.disabled);
279
+ }
280
+ const changed = server.tools.some((t) => t.isDirect !== t.wasDirect);
281
+ if (!changed)
282
+ continue;
283
+ const directTools = server.tools.filter((t) => t.isDirect);
284
+ if (directTools.length === server.tools.length && server.tools.length > 0) {
285
+ changes.set(server.name, true);
286
+ } else if (directTools.length === 0) {
287
+ changes.set(server.name, false);
288
+ } else {
289
+ changes.set(server.name, directTools.map((t) => t.name));
290
+ }
291
+ }
292
+ return { changes, disabledChanges, cancelled: false };
293
+ }
294
+ handleInput(data) {
295
+ this.resetInactivityTimeout();
296
+ this.importNotice = null;
297
+ if (!this.authInFlight)
298
+ this.authNotice = null;
299
+ if (this.confirmingDiscard) {
300
+ this.handleDiscardInput(data);
301
+ return;
302
+ }
303
+ if (matchesKey(data, "ctrl+c")) {
304
+ this.cleanup();
305
+ this.done({ cancelled: true, changes: new Map, disabledChanges: new Map });
306
+ return;
307
+ }
308
+ if (this.keys.save(data)) {
309
+ this.cleanup();
310
+ this.done(this.buildResult());
311
+ return;
312
+ }
313
+ if (this.descSearchActive) {
314
+ if (matchesKey(data, "escape") || this.keys.selectConfirm(data)) {
315
+ this.descSearchActive = false;
316
+ this.descQuery = "";
317
+ this.rebuildVisibleItems();
318
+ this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));
319
+ return;
320
+ }
321
+ if (matchesKey(data, "backspace")) {
322
+ if (this.descQuery.length > 0) {
323
+ this.descQuery = this.descQuery.slice(0, -1);
324
+ this.rebuildVisibleItems();
325
+ this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));
326
+ }
327
+ return;
328
+ }
329
+ if (this.keys.selectUp(data)) {
330
+ this.moveCursor(-1);
331
+ return;
332
+ }
333
+ if (this.keys.selectDown(data)) {
334
+ this.moveCursor(1);
335
+ return;
336
+ }
337
+ if (matchesKey(data, "space")) {
338
+ const item = this.visibleItems[this.cursorIndex];
339
+ if (item)
340
+ this.toggleItem(item);
341
+ return;
342
+ }
343
+ if (data.length === 1 && data.charCodeAt(0) >= 32) {
344
+ this.descQuery += data;
345
+ this.rebuildVisibleItems();
346
+ this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));
347
+ return;
348
+ }
349
+ return;
350
+ }
351
+ if (matchesKey(data, "escape")) {
352
+ if (this.nameQuery) {
353
+ this.nameQuery = "";
354
+ this.rebuildVisibleItems();
355
+ this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));
356
+ return;
357
+ }
358
+ if (this.dirty) {
359
+ this.confirmingDiscard = true;
360
+ this.discardSelected = 1;
361
+ return;
362
+ }
363
+ this.cleanup();
364
+ this.done({ cancelled: true, changes: new Map, disabledChanges: new Map });
365
+ return;
366
+ }
367
+ if (this.keys.selectUp(data)) {
368
+ this.moveCursor(-1);
369
+ return;
370
+ }
371
+ if (this.keys.selectDown(data)) {
372
+ this.moveCursor(1);
373
+ return;
374
+ }
375
+ if (matchesKey(data, "space")) {
376
+ const item = this.visibleItems[this.cursorIndex];
377
+ if (item && !this.authOnly)
378
+ this.toggleItem(item);
379
+ return;
380
+ }
381
+ if (this.keys.selectConfirm(data)) {
382
+ const item = this.visibleItems[this.cursorIndex];
383
+ if (!item)
384
+ return;
385
+ const server = this.servers[item.serverIndex];
386
+ if (!server)
387
+ return;
388
+ if (item.type === "server") {
389
+ if (server.connectionStatus === "disabled")
390
+ return;
391
+ if (this.authOnly || server.connectionStatus === "needs-auth") {
392
+ this.authenticateServer(server);
393
+ return;
394
+ }
395
+ server.expanded = !server.expanded;
396
+ this.rebuildVisibleItems();
397
+ this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));
398
+ } else if (item.toolIndex !== undefined) {
399
+ const tool = server.tools[item.toolIndex];
400
+ if (!tool)
401
+ return;
402
+ this.toggleToolDirect(server, tool);
403
+ if (tool.isDirect && server.source === "import") {
404
+ this.importNotice = `Imported from ${sanitizeDisplayText(server.importKind ?? "external")} — will copy to user config on save`;
405
+ }
406
+ this.updateDirty();
407
+ }
408
+ return;
409
+ }
410
+ if (matchesKey(data, "ctrl+a")) {
411
+ const item = this.visibleItems[this.cursorIndex];
412
+ if (item)
413
+ this.authenticateSelectedServer(item);
414
+ return;
415
+ }
416
+ if (matchesKey(data, "ctrl+r")) {
417
+ const item = this.visibleItems[this.cursorIndex];
418
+ if (!item)
419
+ return;
420
+ const server = this.servers[item.serverIndex];
421
+ if (server)
422
+ this.reconnectServer(server);
423
+ return;
424
+ }
425
+ if (matchesKey(data, "ctrl+d")) {
426
+ const item = this.visibleItems[this.cursorIndex];
427
+ if (!item || item.type !== "server" || this.authOnly)
428
+ return;
429
+ const server = this.servers[item.serverIndex];
430
+ if (!server)
431
+ return;
432
+ server.disabled = !server.disabled;
433
+ this.updateDirty();
434
+ this.tui.requestRender();
435
+ return;
436
+ }
437
+ if (matchesKey(data, "ctrl+y")) {
438
+ const item = this.visibleItems[this.cursorIndex];
439
+ if (!item)
440
+ return;
441
+ const server = this.servers[item.serverIndex];
442
+ if (!server || server.connectionStatus !== "failed" || !server.failureMessage)
443
+ return;
444
+ const serverName = sanitizeDisplayText(server.name);
445
+ const failureMessage = sanitizeDisplayText(server.failureMessage);
446
+ copyToClipboard(failureMessage).then(() => {
447
+ this.authNotice = `Copied error for ${serverName} to clipboard`;
448
+ this.tui.requestRender();
449
+ }).catch((error) => {
450
+ const message = sanitizeDisplayText(error instanceof Error ? error.message : String(error));
451
+ this.authNotice = `Failed to copy error for ${serverName}: ${message}`;
452
+ this.tui.requestRender();
453
+ });
454
+ return;
455
+ }
456
+ if (data === "?") {
457
+ if (this.authOnly)
458
+ return;
459
+ this.descSearchActive = true;
460
+ this.descQuery = "";
461
+ this.rebuildVisibleItems();
462
+ this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));
463
+ return;
464
+ }
465
+ if (matchesKey(data, "backspace")) {
466
+ if (this.nameQuery.length > 0) {
467
+ this.nameQuery = this.nameQuery.slice(0, -1);
468
+ this.rebuildVisibleItems();
469
+ this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));
470
+ }
471
+ return;
472
+ }
473
+ if (data.length === 1 && data.charCodeAt(0) >= 32) {
474
+ this.nameQuery += data;
475
+ this.rebuildVisibleItems();
476
+ this.cursorIndex = Math.min(this.cursorIndex, Math.max(0, this.visibleItems.length - 1));
477
+ return;
478
+ }
479
+ }
480
+ authenticateSelectedServer(item) {
481
+ const server = this.servers[item.serverIndex];
482
+ if (server)
483
+ this.authenticateServer(server);
484
+ }
485
+ authenticateServer(server) {
486
+ if (this.authInFlight)
487
+ return;
488
+ if (server.connectionStatus === "connecting" || server.connectionStatus === "disabled")
489
+ return;
490
+ const serverName = sanitizeDisplayText(server.name);
491
+ if (!this.callbacks.canAuthenticate(server.name)) {
492
+ this.authNotice = `${serverName} does not use OAuth authentication.`;
493
+ return;
494
+ }
495
+ this.authInFlight = server.name;
496
+ this.authNotice = `Authenticating ${serverName}...`;
497
+ this.tui.requestRender();
498
+ this.callbacks.authenticate(server.name).then((result) => {
499
+ server.connectionStatus = this.callbacks.getConnectionStatus(server.name);
500
+ if (result.ok) {
501
+ this.authNotice = `OAuth finished for ${serverName}. Reconnecting...`;
502
+ this.authInFlight = null;
503
+ this.tui.requestRender();
504
+ this.reconnectServer(server, { afterAuth: true });
505
+ return;
506
+ }
507
+ const message = sanitizeDisplayText(result.message);
508
+ this.authNotice = `OAuth failed for ${serverName}${message ? `: ${message}` : ". Check the notification for details."}`;
509
+ this.authInFlight = null;
510
+ this.tui.requestRender();
511
+ }).catch((error) => {
512
+ const message = sanitizeDisplayText(error instanceof Error ? error.message : String(error));
513
+ server.connectionStatus = this.callbacks.getConnectionStatus(server.name);
514
+ this.authNotice = `OAuth failed for ${serverName}: ${message}`;
515
+ this.authInFlight = null;
516
+ this.tui.requestRender();
517
+ });
518
+ }
519
+ reconnectServer(server, options = {}) {
520
+ if (server.connectionStatus === "connecting" || server.connectionStatus === "disabled")
521
+ return;
522
+ const serverName = sanitizeDisplayText(server.name);
523
+ server.connectionStatus = "connecting";
524
+ this.tui.requestRender();
525
+ this.callbacks.reconnect(server.name).then((connected) => {
526
+ server.connectionStatus = this.callbacks.getConnectionStatus(server.name);
527
+ server.failureMessage = this.callbacks.getFailureMessage?.(server.name) ?? null;
528
+ if (server.connectionStatus === "connected") {
529
+ const entry = this.callbacks.refreshCacheAfterReconnect(server.name);
530
+ if (entry) {
531
+ this.cache ??= { version: 1, servers: {} };
532
+ this.cache.servers[server.name] = entry;
533
+ this.rebuildServerTools(server, entry);
534
+ }
535
+ server.hasCachedData = true;
536
+ }
537
+ if (options.afterAuth) {
538
+ this.authNotice = connected && server.connectionStatus === "connected" ? `OAuth finished for ${serverName}. Reconnected.` : `OAuth finished for ${serverName}, but reconnect did not complete. Press ctrl+r to retry.`;
539
+ }
540
+ this.tui.requestRender();
541
+ }).catch((error) => {
542
+ server.connectionStatus = "failed";
543
+ const message = sanitizeDisplayText(error instanceof Error ? error.message : String(error));
544
+ this.authNotice = `Reconnect failed for ${serverName}: ${message}`;
545
+ this.tui.requestRender();
546
+ });
547
+ }
548
+ toggleItem(item) {
549
+ if (this.authOnly)
550
+ return;
551
+ const server = this.servers[item.serverIndex];
552
+ if (!server)
553
+ return;
554
+ if (item.type === "server") {
555
+ const newState = !server.tools.every((t) => t.isDirect);
556
+ if (server.source === "import" && newState) {
557
+ this.importNotice = `Imported from ${sanitizeDisplayText(server.importKind ?? "external")} — will copy to user config on save`;
558
+ }
559
+ let directTokens = 0;
560
+ for (const tool of server.tools) {
561
+ tool.isDirect = newState;
562
+ if (newState)
563
+ directTokens += tool.estimatedTokens;
564
+ }
565
+ server.directCount = newState ? server.tools.length : 0;
566
+ server.directTokens = directTokens;
567
+ } else if (item.toolIndex !== undefined) {
568
+ const tool = server.tools[item.toolIndex];
569
+ if (!tool)
570
+ return;
571
+ this.toggleToolDirect(server, tool);
572
+ if (tool.isDirect && server.source === "import") {
573
+ this.importNotice = `Imported from ${sanitizeDisplayText(server.importKind ?? "external")} — will copy to user config on save`;
574
+ }
575
+ }
576
+ this.updateDirty();
577
+ }
578
+ toggleToolDirect(server, tool) {
579
+ tool.isDirect = !tool.isDirect;
580
+ server.directCount += tool.isDirect ? 1 : -1;
581
+ server.directTokens += tool.isDirect ? tool.estimatedTokens : -tool.estimatedTokens;
582
+ }
583
+ handleDiscardInput(data) {
584
+ if (matchesKey(data, "ctrl+c")) {
585
+ this.cleanup();
586
+ this.done({ cancelled: true, changes: new Map, disabledChanges: new Map });
587
+ return;
588
+ }
589
+ if (matchesKey(data, "escape") || data === "n" || data === "N") {
590
+ this.confirmingDiscard = false;
591
+ return;
592
+ }
593
+ if (this.keys.selectConfirm(data)) {
594
+ this.cleanup();
595
+ if (this.discardSelected === 0) {
596
+ this.done({ cancelled: true, changes: new Map, disabledChanges: new Map });
597
+ } else {
598
+ this.done(this.buildResult());
599
+ }
600
+ return;
601
+ }
602
+ if (data === "y" || data === "Y") {
603
+ this.cleanup();
604
+ this.done({ cancelled: true, changes: new Map, disabledChanges: new Map });
605
+ return;
606
+ }
607
+ if (matchesKey(data, "left") || matchesKey(data, "right") || matchesKey(data, "tab")) {
608
+ this.discardSelected = this.discardSelected === 0 ? 1 : 0;
609
+ }
610
+ }
611
+ moveCursor(delta) {
612
+ if (this.visibleItems.length === 0)
613
+ return;
614
+ this.cursorIndex = Math.max(0, Math.min(this.visibleItems.length - 1, this.cursorIndex + delta));
615
+ }
616
+ getOtherCurrentCandidates(serverName, definition, currentEntry, toolName) {
617
+ const candidates = new Set;
618
+ for (const [otherServerName, otherDefinition] of Object.entries(this.config.mcpServers)) {
619
+ if (isServerDisabled(otherDefinition))
620
+ continue;
621
+ const cachedEntry = this.cache?.servers?.[otherServerName];
622
+ const entry = otherServerName === serverName ? currentEntry : cachedEntry && isServerCacheValid(cachedEntry, otherDefinition) ? cachedEntry : undefined;
623
+ if (!entry)
624
+ continue;
625
+ const otherPrefix = resolveToolPrefix(otherDefinition, this.prefix);
626
+ for (const tool of entry.tools ?? []) {
627
+ if (!isUiToolVisibleToModel(tool.uiVisibility))
628
+ continue;
629
+ for (const candidate of getToolNameCandidates(tool.name, otherServerName, otherPrefix, false))
630
+ candidates.add(candidate);
631
+ }
632
+ if (otherDefinition.exposeResources !== false) {
633
+ for (const resource of entry.resources ?? []) {
634
+ const baseName = `read_${resourceNameToToolName(resource.name)}`;
635
+ for (const candidate of getToolNameCandidates(baseName, otherServerName, otherPrefix, false))
636
+ candidates.add(candidate);
637
+ }
638
+ }
639
+ }
640
+ for (const candidate of getToolNameCandidates(toolName, serverName, resolveToolPrefix(definition, this.prefix), false))
641
+ candidates.delete(candidate);
642
+ return candidates;
643
+ }
644
+ rebuildServerTools(server, entry) {
645
+ const existingState = new Map;
646
+ for (const t of server.tools)
647
+ existingState.set(t.name, t.isDirect);
648
+ const newTools = [];
649
+ for (const tool of entry.tools ?? []) {
650
+ if (!isUiToolVisibleToModel(tool.uiVisibility))
651
+ continue;
652
+ if (!isToolAllowed(tool.name, server.name, this.prefix, server.includeTools, server.excludeTools, this.getOtherCurrentCandidates(server.name, server, entry, tool.name))) {
653
+ continue;
654
+ }
655
+ const prev = existingState.get(tool.name);
656
+ const isDirect = prev !== undefined ? prev : false;
657
+ newTools.push({
658
+ name: tool.name,
659
+ description: tool.description ?? "",
660
+ isDirect,
661
+ wasDirect: prev !== undefined ? server.tools.find((t) => t.name === tool.name)?.wasDirect ?? false : false,
662
+ estimatedTokens: estimateTokens(tool)
663
+ });
664
+ }
665
+ if (server.exposeResources) {
666
+ for (const resource of entry.resources ?? []) {
667
+ const baseName = `read_${resourceNameToToolName(resource.name)}`;
668
+ if (!isToolAllowed(baseName, server.name, this.prefix, server.includeTools, server.excludeTools, this.getOtherCurrentCandidates(server.name, server, entry, baseName))) {
669
+ continue;
670
+ }
671
+ const prev = existingState.get(baseName);
672
+ const isDirect = prev !== undefined ? prev : false;
673
+ const ct = {
674
+ name: baseName,
675
+ ...resource.description !== undefined ? { description: resource.description } : {}
676
+ };
677
+ newTools.push({
678
+ name: baseName,
679
+ description: resource.description ?? `Read resource: ${resource.uri}`,
680
+ isDirect,
681
+ wasDirect: prev !== undefined ? server.tools.find((t) => t.name === baseName)?.wasDirect ?? false : false,
682
+ estimatedTokens: estimateTokens(ct)
683
+ });
684
+ }
685
+ }
686
+ server.tools = newTools;
687
+ server.directCount = 0;
688
+ server.directTokens = 0;
689
+ for (const tool of newTools) {
690
+ if (!tool.isDirect)
691
+ continue;
692
+ server.directCount++;
693
+ server.directTokens += tool.estimatedTokens;
694
+ }
695
+ this.rebuildVisibleItems();
696
+ this.updateDirty();
697
+ }
698
+ render(width) {
699
+ const innerW = width - 2;
700
+ const lines = [];
701
+ const t = this.t;
702
+ const bold = (s) => `\x1B[1m${s}\x1B[22m`;
703
+ const italic = (s) => `\x1B[3m${s}\x1B[23m`;
704
+ const inverse = (s) => `\x1B[7m${s}\x1B[27m`;
705
+ const row = (content) => fg(t.border, "│") + truncateToWidth(" " + sanitizeRowContent(content), innerW, "…", true) + fg(t.border, "│");
706
+ const emptyRow = () => fg(t.border, "│") + " ".repeat(innerW) + fg(t.border, "│");
707
+ const divider = () => fg(t.border, "├" + "─".repeat(innerW) + "┤");
708
+ const titleText = this.authOnly ? " MCP OAuth " : " MCP Servers ";
709
+ const borderLen = innerW - visibleWidth(titleText);
710
+ const leftB = Math.floor(borderLen / 2);
711
+ const rightB = borderLen - leftB;
712
+ lines.push(fg(t.border, "╭" + "─".repeat(leftB)) + fg(t.title, titleText) + fg(t.border, "─".repeat(rightB) + "╮"));
713
+ lines.push(emptyRow());
714
+ const cursor = fg(t.selected, "│");
715
+ const searchIcon = fg(t.border, "◎");
716
+ if (this.descSearchActive) {
717
+ lines.push(row(`${searchIcon} ${fg(t.needsAuth, "desc:")} ${this.descQuery}${cursor}`));
718
+ } else if (this.nameQuery) {
719
+ lines.push(row(`${searchIcon} ${this.nameQuery}${cursor}`));
720
+ } else {
721
+ lines.push(row(`${searchIcon} ${fg(t.placeholder, italic("search..."))}`));
722
+ }
723
+ lines.push(emptyRow());
724
+ if (this.noticeLines.length > 0) {
725
+ for (const notice of this.noticeLines) {
726
+ lines.push(row(fg(t.hint, italic(sanitizeDisplayText(notice)))));
727
+ }
728
+ lines.push(emptyRow());
729
+ }
730
+ lines.push(divider());
731
+ if (this.servers.length === 0) {
732
+ lines.push(emptyRow());
733
+ lines.push(row(fg(t.hint, italic(this.authOnly ? "No OAuth-capable MCP servers configured." : "No MCP servers configured."))));
734
+ lines.push(emptyRow());
735
+ } else {
736
+ const maxVis = McpPanel.MAX_VISIBLE;
737
+ const total = this.visibleItems.length;
738
+ const startIdx = Math.max(0, Math.min(this.cursorIndex - Math.floor(maxVis / 2), total - maxVis));
739
+ const endIdx = Math.min(startIdx + maxVis, total);
740
+ lines.push(emptyRow());
741
+ for (let i = startIdx;i < endIdx; i++) {
742
+ const item = this.visibleItems[i];
743
+ if (!item)
744
+ continue;
745
+ const isCursor = i === this.cursorIndex;
746
+ const server = this.servers[item.serverIndex];
747
+ if (!server)
748
+ continue;
749
+ if (item.type === "server") {
750
+ lines.push(row(this.renderServerRow(server, isCursor)));
751
+ if (isCursor && server.connectionStatus === "failed" && server.failureMessage) {
752
+ for (const line of this.wrapText(sanitizeDisplayText(server.failureMessage), innerW - 6)) {
753
+ lines.push(row(` ${fg(t.cancel, line)}`));
754
+ }
755
+ }
756
+ } else if (item.toolIndex !== undefined) {
757
+ const tool = server.tools[item.toolIndex];
758
+ if (tool)
759
+ lines.push(row(this.renderToolRow(tool, isCursor, innerW)));
760
+ }
761
+ }
762
+ lines.push(emptyRow());
763
+ if (total > maxVis) {
764
+ const prog = Math.round((this.cursorIndex + 1) / total * 10);
765
+ lines.push(row(`${rainbowProgress(prog, 10)} ${fg(t.hint, `${this.cursorIndex + 1}/${total}`)}`));
766
+ lines.push(emptyRow());
767
+ }
768
+ if (this.importNotice) {
769
+ lines.push(row(fg(t.needsAuth, italic(sanitizeDisplayText(this.importNotice)))));
770
+ lines.push(emptyRow());
771
+ }
772
+ if (this.authNotice) {
773
+ lines.push(row(fg(t.needsAuth, italic(sanitizeDisplayText(this.authNotice)))));
774
+ lines.push(emptyRow());
775
+ }
776
+ }
777
+ lines.push(divider());
778
+ lines.push(emptyRow());
779
+ if (this.confirmingDiscard) {
780
+ const discardBtn = this.discardSelected === 0 ? inverse(bold(fg(t.cancel, " Discard "))) : fg(t.hint, " Discard ");
781
+ const keepBtn = this.discardSelected === 1 ? inverse(bold(fg(t.confirm, " Keep & Close "))) : fg(t.hint, " Keep & Close ");
782
+ lines.push(row(`Discard unsaved changes? ${discardBtn} ${keepBtn}`));
783
+ } else {
784
+ if (this.authOnly) {
785
+ lines.push(row(fg(t.description, "select a server to authenticate")));
786
+ } else {
787
+ let directCount = 0;
788
+ let directTokens = 0;
789
+ for (const server of this.servers) {
790
+ directCount += server.directCount;
791
+ directTokens += server.directTokens;
792
+ }
793
+ const stats = directCount > 0 ? `${directCount} direct ~${directTokens.toLocaleString()} tokens` : "no direct tools";
794
+ lines.push(row(fg(t.description, stats + (this.dirty ? fg(t.needsAuth, " (unsaved)") : ""))));
795
+ }
796
+ }
797
+ lines.push(emptyRow());
798
+ const saveLabel = this.keys.saveLabel();
799
+ const hints = this.authOnly ? [
800
+ italic("↑↓") + " navigate",
801
+ italic("⏎") + " auth",
802
+ italic("ctrl+a") + " auth",
803
+ italic("esc") + " clear/close",
804
+ italic("ctrl+c") + " quit"
805
+ ] : [
806
+ italic("↑↓") + " navigate",
807
+ italic("space") + " toggle",
808
+ italic("⏎") + " expand/auth",
809
+ italic("ctrl+a") + " auth",
810
+ italic("ctrl+r") + " reconnect",
811
+ italic("ctrl+d") + " disable/enable",
812
+ ...this.selectedServerHasFailureMessage() ? [italic("ctrl+y") + " copy error"] : [],
813
+ italic("?") + " desc search",
814
+ ...saveLabel ? [italic(saveLabel) + " save"] : [],
815
+ italic("esc") + " clear/close",
816
+ italic("ctrl+c") + " quit"
817
+ ];
818
+ const gap = " ";
819
+ const gapW = 2;
820
+ const maxW = innerW - 2;
821
+ let curLine = "";
822
+ let curW = 0;
823
+ for (const hint of hints) {
824
+ const hw = visibleWidth(hint);
825
+ const needed = curW === 0 ? hw : gapW + hw;
826
+ if (curW > 0 && curW + needed > maxW) {
827
+ lines.push(row(fg(t.hint, curLine)));
828
+ curLine = hint;
829
+ curW = hw;
830
+ } else {
831
+ curLine += (curW > 0 ? gap : "") + hint;
832
+ curW += needed;
833
+ }
834
+ }
835
+ if (curLine)
836
+ lines.push(row(fg(t.hint, curLine)));
837
+ lines.push(fg(t.border, "╰" + "─".repeat(innerW) + "╯"));
838
+ return lines;
839
+ }
840
+ renderServerRow(server, isCursor) {
841
+ const t = this.t;
842
+ const bold = (s) => `\x1B[1m${s}\x1B[22m`;
843
+ const expandIcon = server.expanded ? "▾" : "▸";
844
+ const prefix = isCursor ? fg(t.selected, expandIcon) : fg(t.border, server.expanded ? expandIcon : "·");
845
+ const serverName = sanitizeDisplayText(server.name);
846
+ const importKind = sanitizeDisplayText(server.importKind ?? "import");
847
+ const nameStr = isCursor ? bold(fg(t.selected, serverName)) : serverName;
848
+ const importLabel = server.source === "import" ? fg(t.description, ` (${importKind})`) : "";
849
+ const statusLabel = this.renderConnectionStatus(server);
850
+ if (!server.hasCachedData && !this.authOnly) {
851
+ return `${prefix} ${nameStr}${importLabel} ${fg(t.description, "(not cached)")}${statusLabel}`;
852
+ }
853
+ const directCount = server.directCount;
854
+ const totalCount = server.tools.length;
855
+ let toggleIcon = fg(t.description, "○");
856
+ if (directCount === totalCount && totalCount > 0) {
857
+ toggleIcon = fg(t.direct, "●");
858
+ } else if (directCount > 0) {
859
+ toggleIcon = fg(t.needsAuth, "◐");
860
+ }
861
+ let toolInfo = "";
862
+ if (totalCount > 0) {
863
+ toolInfo = `${directCount}/${totalCount}`;
864
+ if (directCount > 0) {
865
+ toolInfo += ` ~${server.directTokens.toLocaleString()}`;
866
+ }
867
+ toolInfo = fg(t.description, toolInfo);
868
+ }
869
+ return `${prefix} ${toggleIcon} ${nameStr}${importLabel} ${toolInfo}${statusLabel}`;
870
+ }
871
+ selectedServerHasFailureMessage() {
872
+ const item = this.visibleItems[this.cursorIndex];
873
+ if (!item)
874
+ return false;
875
+ const server = this.servers[item.serverIndex];
876
+ return server?.connectionStatus === "failed" && !!server.failureMessage;
877
+ }
878
+ wrapText(text, width) {
879
+ const max = Math.max(8, width);
880
+ const words = text.split(/\s+/).filter(Boolean);
881
+ const lines = [];
882
+ let current = "";
883
+ const splitLongWord = (word) => {
884
+ let rest = word;
885
+ while (visibleWidth(rest) > max) {
886
+ let take = "";
887
+ let index = 0;
888
+ while (index < rest.length && visibleWidth(take + rest.charAt(index)) <= max) {
889
+ take += rest.charAt(index);
890
+ index++;
891
+ }
892
+ if (!take)
893
+ take = rest.charAt(0);
894
+ lines.push(take);
895
+ rest = rest.slice(take.length);
896
+ }
897
+ return rest;
898
+ };
899
+ for (const word of words) {
900
+ const candidate = current ? `${current} ${word}` : word;
901
+ if (visibleWidth(candidate) <= max) {
902
+ current = candidate;
903
+ } else {
904
+ if (current)
905
+ lines.push(current);
906
+ current = splitLongWord(word);
907
+ }
908
+ }
909
+ if (current)
910
+ lines.push(current);
911
+ return lines.length > 0 ? lines : [text];
912
+ }
913
+ renderConnectionStatus(server) {
914
+ const t = this.t;
915
+ if (this.authInFlight === server.name)
916
+ return ` ${fg(t.needsAuth, "authenticating")}`;
917
+ if (server.disabled)
918
+ return ` ${fg(t.description, "disabled")}`;
919
+ if (server.connectionStatus === "needs-auth")
920
+ return ` ${fg(t.needsAuth, "needs auth")}`;
921
+ if (server.connectionStatus === "connecting")
922
+ return ` ${fg(t.needsAuth, "connecting")}`;
923
+ if (server.connectionStatus === "failed")
924
+ return ` ${fg(t.cancel, "failed")}`;
925
+ if (this.authOnly && server.connectionStatus === "connected")
926
+ return ` ${fg(t.direct, "connected")}`;
927
+ if (this.authOnly)
928
+ return ` ${fg(t.description, "idle")}`;
929
+ return "";
930
+ }
931
+ renderToolRow(tool, isCursor, innerW) {
932
+ const t = this.t;
933
+ const bold = (s) => `\x1B[1m${s}\x1B[22m`;
934
+ const toggleIcon = tool.isDirect ? fg(t.direct, "●") : fg(t.description, "○");
935
+ const cursor = isCursor ? fg(t.selected, "▸") : " ";
936
+ const toolName = sanitizeDisplayText(tool.name);
937
+ const description = sanitizeDisplayText(tool.description);
938
+ const nameStr = isCursor ? bold(fg(t.selected, toolName)) : toolName;
939
+ const prefixLen = 7 + visibleWidth(toolName);
940
+ const maxDescLen = Math.max(0, innerW - prefixLen - 8);
941
+ const descStr = maxDescLen > 5 && description ? fg(t.description, "— " + truncateToWidth(description, maxDescLen, "…")) : "";
942
+ return ` ${cursor} ${toggleIcon} ${nameStr} ${descStr}`;
943
+ }
944
+ invalidate() {}
945
+ dispose() {
946
+ this.cleanup();
947
+ }
948
+ }
949
+ export function createMcpPanel(config, cache, provenance, callbacks, tui, done, options) {
950
+ return new McpPanel(config, cache, provenance, callbacks, tui, done, options ?? {});
951
+ }