@herbertgao/pi-extensions 2026.9.4 → 2026.9.6

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 (146) hide show
  1. package/README.md +3 -15
  2. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  3. package/node_modules/@dietrichgebert/ponytail/README.es.md +33 -2
  4. package/node_modules/@dietrichgebert/ponytail/README.ko.md +33 -2
  5. package/node_modules/@dietrichgebert/ponytail/README.md +49 -5
  6. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  7. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +4 -0
  8. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +4 -0
  9. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +0 -3
  10. package/node_modules/@dietrichgebert/ponytail/hooks/cursor-hooks.json +17 -0
  11. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +22 -3
  12. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +27 -2
  13. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +61 -2
  14. package/node_modules/@dietrichgebert/ponytail/package.json +2 -1
  15. package/node_modules/@dietrichgebert/ponytail/scripts/cursor-hooks.js +135 -0
  16. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -4
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +33 -15
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +6 -2
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +23 -1
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +69 -14
  21. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  22. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
  23. package/node_modules/@herbertgao/sol-pi/package.json +1 -1
  24. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +0 -5
  25. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +4 -4
  26. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +41 -1
  27. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +3 -1
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +5 -1
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/events.ts +5 -1
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  31. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/normalize-params.ts +56 -0
  32. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/option-list-view.ts +1 -1
  33. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/tab-bar.ts +1 -2
  34. package/node_modules/@pi-plugins/fast-mode/README.md +31 -44
  35. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20914 -30
  36. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  37. package/node_modules/@pi-plugins/fast-mode/package.json +5 -7
  38. package/node_modules/@tifan/pi-copy-response/README.md +2 -2
  39. package/node_modules/@tifan/pi-copy-response/package.json +3 -2
  40. package/node_modules/@tifan/pi-inline-skills/README.md +2 -2
  41. package/node_modules/@tifan/pi-inline-skills/package.json +3 -2
  42. package/node_modules/@tifan/pi-mermaid-open/README.md +1 -1
  43. package/node_modules/@tifan/pi-mermaid-open/package.json +3 -2
  44. package/node_modules/@tifan/pi-preferred-thinking/package.json +3 -2
  45. package/node_modules/@tifan/pi-recap/package.json +3 -2
  46. package/node_modules/pi-mcp-adapter/CHANGELOG.md +78 -0
  47. package/node_modules/pi-mcp-adapter/README.md +125 -19
  48. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +69 -30
  49. package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +36 -0
  50. package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +365 -0
  51. package/node_modules/pi-mcp-adapter/commands.ts +13 -15
  52. package/node_modules/pi-mcp-adapter/config.ts +198 -30
  53. package/node_modules/pi-mcp-adapter/consent-manager.ts +40 -9
  54. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +250 -0
  55. package/node_modules/pi-mcp-adapter/direct-tools.ts +9 -228
  56. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +74 -32
  57. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +1 -1
  58. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +1 -0
  59. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +31 -0
  60. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +1 -0
  61. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +9 -0
  62. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +362 -0
  63. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +1 -0
  64. package/node_modules/pi-mcp-adapter/dist/config.d.ts +3 -1
  65. package/node_modules/pi-mcp-adapter/dist/config.js +189 -27
  66. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  67. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +7 -16
  68. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  69. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +5 -17
  70. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +1 -1
  71. package/node_modules/pi-mcp-adapter/dist/types.d.ts +30 -4
  72. package/node_modules/pi-mcp-adapter/dist/types.js +26 -0
  73. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  74. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +6 -0
  75. package/node_modules/pi-mcp-adapter/dist/utils.js +65 -6
  76. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  77. package/node_modules/pi-mcp-adapter/host-html-template.ts +20 -19
  78. package/node_modules/pi-mcp-adapter/http-ca.ts +84 -0
  79. package/node_modules/pi-mcp-adapter/index.ts +827 -164
  80. package/node_modules/pi-mcp-adapter/init.ts +48 -10
  81. package/node_modules/pi-mcp-adapter/lazy-loader.ts +7 -0
  82. package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +113 -0
  83. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +256 -77
  84. package/node_modules/pi-mcp-adapter/mcp-auth.ts +316 -54
  85. package/node_modules/pi-mcp-adapter/mcp-code.ts +45 -18
  86. package/node_modules/pi-mcp-adapter/mcp-install.ts +60 -0
  87. package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +70 -6
  88. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +53 -42
  89. package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +104 -0
  90. package/node_modules/pi-mcp-adapter/mcp-panel.ts +366 -332
  91. package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -14
  92. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +3 -1
  93. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +383 -341
  94. package/node_modules/pi-mcp-adapter/mcp-status.ts +5 -0
  95. package/node_modules/pi-mcp-adapter/metadata-cache.ts +7 -16
  96. package/node_modules/pi-mcp-adapter/namespace-tools.ts +61 -3
  97. package/node_modules/pi-mcp-adapter/oauth.ts +6 -2
  98. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +5 -17
  99. package/node_modules/pi-mcp-adapter/package.json +13 -3
  100. package/node_modules/pi-mcp-adapter/prompts.ts +35 -3
  101. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -7
  102. package/node_modules/pi-mcp-adapter/runtime-owner.ts +16 -1
  103. package/node_modules/pi-mcp-adapter/sampling-handler.ts +9 -32
  104. package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +20 -93
  105. package/node_modules/pi-mcp-adapter/server-manager.ts +227 -40
  106. package/node_modules/pi-mcp-adapter/session-approvals.ts +187 -0
  107. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +9 -1
  108. package/node_modules/pi-mcp-adapter/state.ts +10 -1
  109. package/node_modules/pi-mcp-adapter/tool-approval.ts +10 -22
  110. package/node_modules/pi-mcp-adapter/tool-metadata.ts +14 -0
  111. package/node_modules/pi-mcp-adapter/types.ts +57 -4
  112. package/node_modules/pi-mcp-adapter/ui-server.ts +36 -28
  113. package/node_modules/pi-mcp-adapter/utils.ts +64 -6
  114. package/package.json +13 -21
  115. package/examples/pi-footer.json +0 -275
  116. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +0 -70
  117. package/node_modules/@czottmann/pi-automode/LICENSE.md +0 -21
  118. package/node_modules/@czottmann/pi-automode/README.md +0 -215
  119. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +0 -39
  120. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +0 -46
  121. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-002-global-config-in-extension-data-directory.md +0 -60
  122. package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +0 -6
  123. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +0 -428
  124. package/node_modules/@czottmann/pi-automode/docs/configuration.md +0 -171
  125. package/node_modules/@czottmann/pi-automode/docs/defaults.md +0 -178
  126. package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +0 -90
  127. package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +0 -160
  128. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +0 -44
  129. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/bash.ts +0 -692
  130. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +0 -852
  131. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +0 -904
  132. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +0 -228
  133. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +0 -954
  134. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +0 -429
  135. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +0 -173
  136. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model-selector.ts +0 -113
  137. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model.ts +0 -13
  138. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +0 -297
  139. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +0 -623
  140. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +0 -102
  141. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/transcript.ts +0 -236
  142. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +0 -205
  143. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +0 -54
  144. package/node_modules/@czottmann/pi-automode/extensions/auto-mode.ts +0 -26
  145. package/node_modules/@czottmann/pi-automode/package.json +0 -61
  146. package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +0 -63
@@ -1,6 +1,7 @@
1
- import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
- import { copyToClipboard } from "@earendil-works/pi-coding-agent";
1
+ import { Container, Text, matchesKey, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
2
+ import { copyToClipboard, type Theme } from "@earendil-works/pi-coding-agent";
3
3
  import { createPanelKeys, type PanelKeybindings, type PanelKeys } from "./panel-keys.ts";
4
+ import { createMcpPanelTheme, McpPanelFrame, type McpPanelTheme } from "./mcp-panel-theme.ts";
4
5
  import { getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.ts";
5
6
  import type { McpConfig, McpPanelCallbacks, McpPanelResult, ServerProvenance, ToolPrefix } from "./types.ts";
6
7
  import { resourceNameToToolName } from "./resource-tools.ts";
@@ -8,57 +9,6 @@ import { sanitizeTerminalText, stripOscSequences } from "./utils.ts";
8
9
  import { isServerCacheValid, type MetadataCache, type ServerCacheEntry, type CachedTool } from "./metadata-cache.ts";
9
10
  import { isUiToolVisibleToModel } from "./ui-tool-visibility.ts";
10
11
 
11
- interface PanelTheme {
12
- border: string;
13
- title: string;
14
- selected: string;
15
- direct: string;
16
- needsAuth: string;
17
- placeholder: string;
18
- description: string;
19
- hint: string;
20
- confirm: string;
21
- cancel: string;
22
- }
23
-
24
- const DEFAULT_THEME: PanelTheme = {
25
- border: "2",
26
- title: "2",
27
- selected: "36",
28
- direct: "32",
29
- needsAuth: "33",
30
- placeholder: "2;3",
31
- description: "2",
32
- hint: "2",
33
- confirm: "32",
34
- cancel: "31",
35
- };
36
-
37
- function fg(code: string, text: string): string {
38
- if (!code) return text;
39
- return `\x1b[${code}m${text}\x1b[0m`;
40
- }
41
-
42
- const RAINBOW_COLORS = [
43
- "38;2;178;129;214",
44
- "38;2;215;135;175",
45
- "38;2;254;188;56",
46
- "38;2;228;192;15",
47
- "38;2;137;210;129",
48
- "38;2;0;175;175",
49
- "38;2;23;143;185",
50
- ];
51
-
52
- function rainbowProgress(filled: number, total: number): string {
53
- const dots: string[] = [];
54
- for (let i = 0; i < total; i++) {
55
- const color = RAINBOW_COLORS[i % RAINBOW_COLORS.length];
56
- if (!color) continue;
57
- dots.push(fg(color, i < filled ? "●" : "○"));
58
- }
59
- return dots.join(" ");
60
- }
61
-
62
12
  function fuzzyScore(query: string, text: string): number {
63
13
  const lq = query.toLowerCase();
64
14
  const lt = text.toLowerCase();
@@ -150,6 +100,337 @@ interface VisibleItem {
150
100
  toolIndex?: number;
151
101
  }
152
102
 
103
+ interface McpPanelViewState {
104
+ noticeLines: readonly string[];
105
+ servers: readonly ServerState[];
106
+ visibleItems: readonly VisibleItem[];
107
+ cursorIndex: number;
108
+ nameQuery: string;
109
+ descSearchActive: boolean;
110
+ descQuery: string;
111
+ dirty: boolean;
112
+ confirmingDiscard: boolean;
113
+ discardSelected: number;
114
+ importNotice: string | null;
115
+ authNotice: string | null;
116
+ authInFlight: string | null;
117
+ authOnly: boolean;
118
+ saveLabel: string | null;
119
+ }
120
+
121
+ /**
122
+ * The themed MCP view. It owns only component composition and formatting;
123
+ * input routing and callbacks remain on McpPanel.
124
+ */
125
+ class McpPanelView implements Component {
126
+ private readonly container = new Container();
127
+
128
+ constructor(
129
+ private readonly getState: () => McpPanelViewState,
130
+ private readonly theme: McpPanelTheme,
131
+ ) {}
132
+
133
+ render(width: number): string[] {
134
+ const panelWidth = Math.max(0, width);
135
+ const innerWidth = Math.max(0, panelWidth - 2);
136
+ const state = this.getState();
137
+ this.container.clear();
138
+
139
+ const title = state.authOnly ? "MCP OAuth" : "MCP Servers";
140
+ this.container.addChild(new McpPanelFrame(this.theme, "╭", "╮", title));
141
+ this.addRow("", innerWidth);
142
+
143
+ const cursor = this.theme.selected("│");
144
+ const searchIcon = this.theme.border("◎");
145
+ if (state.descSearchActive) {
146
+ this.addRow(`${searchIcon} ${this.theme.needsAuth("desc:")} ${state.descQuery}${cursor}`, innerWidth);
147
+ } else if (state.nameQuery) {
148
+ this.addRow(`${searchIcon} ${state.nameQuery}${cursor}`, innerWidth);
149
+ } else {
150
+ this.addRow(`${searchIcon} ${this.theme.placeholder(this.theme.italic("search..."))}`, innerWidth);
151
+ }
152
+
153
+ this.addRow("", innerWidth);
154
+ if (state.noticeLines.length > 0) {
155
+ for (const notice of state.noticeLines) {
156
+ this.addRow(this.theme.hint(this.theme.italic(sanitizeDisplayText(notice))), innerWidth);
157
+ }
158
+ this.addRow("", innerWidth);
159
+ }
160
+ this.addRule("├", "┤");
161
+
162
+ if (state.servers.length === 0) {
163
+ this.addRow("", innerWidth);
164
+ this.addRow(
165
+ this.theme.hint(
166
+ this.theme.italic(
167
+ state.authOnly ? "No OAuth-capable MCP servers configured." : "No MCP servers configured.",
168
+ ),
169
+ ),
170
+ innerWidth,
171
+ );
172
+ this.addRow("", innerWidth);
173
+ } else {
174
+ const maxVisible = McpPanel.MAX_VISIBLE;
175
+ const total = state.visibleItems.length;
176
+ const startIndex = Math.max(
177
+ 0,
178
+ Math.min(state.cursorIndex - Math.floor(maxVisible / 2), total - maxVisible),
179
+ );
180
+ const endIndex = Math.min(startIndex + maxVisible, total);
181
+
182
+ this.addRow("", innerWidth);
183
+
184
+ for (let index = startIndex; index < endIndex; index++) {
185
+ const item = state.visibleItems[index];
186
+ if (!item) continue;
187
+ const isCursor = index === state.cursorIndex;
188
+ const server = state.servers[item.serverIndex];
189
+ if (!server) continue;
190
+
191
+ if (item.type === "server") {
192
+ this.addRow(this.renderServerRow(state, server, isCursor), innerWidth);
193
+ if (isCursor && server.connectionStatus === "failed" && server.failureMessage) {
194
+ for (const line of this.wrapText(sanitizeDisplayText(server.failureMessage), innerWidth - 6)) {
195
+ this.addRow(` ${this.theme.cancel(line)}`, innerWidth);
196
+ }
197
+ }
198
+ } else if (item.toolIndex !== undefined) {
199
+ const tool = server.tools[item.toolIndex];
200
+ if (tool) this.addRow(this.renderToolRow(tool, isCursor, innerWidth), innerWidth);
201
+ }
202
+ }
203
+
204
+ this.addRow("", innerWidth);
205
+
206
+ if (total > maxVisible) {
207
+ const progress = Math.round(((state.cursorIndex + 1) / total) * 10);
208
+ this.addRow(
209
+ `${this.progressDots(progress, 10)} ${this.theme.hint(`${state.cursorIndex + 1}/${total}`)}`,
210
+ innerWidth,
211
+ );
212
+ this.addRow("", innerWidth);
213
+ }
214
+
215
+ if (state.importNotice) {
216
+ this.addRow(this.theme.needsAuth(this.theme.italic(sanitizeDisplayText(state.importNotice))), innerWidth);
217
+ this.addRow("", innerWidth);
218
+ }
219
+ if (state.authNotice) {
220
+ this.addRow(this.theme.needsAuth(this.theme.italic(sanitizeDisplayText(state.authNotice))), innerWidth);
221
+ this.addRow("", innerWidth);
222
+ }
223
+ }
224
+
225
+ this.addRule("├", "┤");
226
+ this.addRow("", innerWidth);
227
+
228
+ if (state.confirmingDiscard) {
229
+ const discardButton = state.discardSelected === 0
230
+ ? this.theme.inverse(this.theme.bold(this.theme.cancel(" Discard ")))
231
+ : this.theme.hint(" Discard ");
232
+ const keepButton = state.discardSelected === 1
233
+ ? this.theme.inverse(this.theme.bold(this.theme.confirm(" Keep & Close ")))
234
+ : this.theme.hint(" Keep & Close ");
235
+ this.addRow(`Discard unsaved changes? ${discardButton} ${keepButton}`, innerWidth);
236
+ } else if (state.authOnly) {
237
+ this.addRow(this.theme.description("select a server to authenticate"), innerWidth);
238
+ } else {
239
+ let directCount = 0;
240
+ let directTokens = 0;
241
+ for (const server of state.servers) {
242
+ directCount += server.directCount;
243
+ directTokens += server.directTokens;
244
+ }
245
+ const stats = directCount > 0
246
+ ? `${directCount} direct ~${directTokens.toLocaleString()} tokens`
247
+ : "no direct tools";
248
+ this.addRow(
249
+ this.theme.description(stats + (state.dirty ? this.theme.needsAuth(" (unsaved)") : "")),
250
+ innerWidth,
251
+ );
252
+ }
253
+
254
+ this.addRow("", innerWidth);
255
+ const hints = state.authOnly
256
+ ? [
257
+ this.theme.italic("↑↓") + " navigate",
258
+ this.theme.italic("⏎") + " auth",
259
+ this.theme.italic("ctrl+a") + " auth",
260
+ this.theme.italic("esc") + " clear/close",
261
+ this.theme.italic("ctrl+c") + " quit",
262
+ ]
263
+ : [
264
+ this.theme.italic("↑↓") + " navigate",
265
+ this.theme.italic("space") + " toggle",
266
+ this.theme.italic("⏎") + " expand/auth",
267
+ this.theme.italic("ctrl+a") + " auth",
268
+ this.theme.italic("ctrl+r") + " reconnect",
269
+ this.theme.italic("ctrl+d") + " disable/enable",
270
+ ...(this.selectedServerHasFailureMessage(state) ? [this.theme.italic("ctrl+y") + " copy error"] : []),
271
+ this.theme.italic("?") + " desc search",
272
+ ...(state.saveLabel ? [this.theme.italic(state.saveLabel) + " save"] : []),
273
+ this.theme.italic("esc") + " clear/close",
274
+ this.theme.italic("ctrl+c") + " quit",
275
+ ];
276
+ const gap = " ";
277
+ const gapWidth = 2;
278
+ const maxWidth = innerWidth - 2;
279
+ let currentLine = "";
280
+ let currentWidth = 0;
281
+ for (const hint of hints) {
282
+ const hintWidth = visibleWidth(hint);
283
+ const needed = currentWidth === 0 ? hintWidth : gapWidth + hintWidth;
284
+ if (currentWidth > 0 && currentWidth + needed > maxWidth) {
285
+ this.addRow(this.theme.hint(currentLine), innerWidth);
286
+ currentLine = hint;
287
+ currentWidth = hintWidth;
288
+ } else {
289
+ currentLine += (currentWidth > 0 ? gap : "") + hint;
290
+ currentWidth += needed;
291
+ }
292
+ }
293
+ if (currentLine) this.addRow(this.theme.hint(currentLine), innerWidth);
294
+
295
+ this.addRule("╰", "╯");
296
+ return this.container.render(panelWidth);
297
+ }
298
+
299
+ invalidate(): void {
300
+ this.container.invalidate();
301
+ }
302
+
303
+ private addRow(content: string, innerWidth: number): void {
304
+ this.container.addChild(new Text(this.row(content, innerWidth), 0, 0));
305
+ }
306
+
307
+ private addRule(left: string, right: string): void {
308
+ this.container.addChild(new McpPanelFrame(this.theme, left, right));
309
+ }
310
+
311
+ private row(content: string, innerWidth: number): string {
312
+ const fitted = truncateToWidth(" " + sanitizeRowContent(content), innerWidth, "…", true);
313
+ return this.theme.border("│") + fitted + this.theme.border("│");
314
+ }
315
+
316
+ private progressDots(filled: number, total: number): string {
317
+ const dots: string[] = [];
318
+ for (let index = 0; index < total; index++) {
319
+ if (index < filled) {
320
+ dots.push(this.theme.direct("●"));
321
+ } else {
322
+ dots.push(this.theme.hint("○"));
323
+ }
324
+ }
325
+ return dots.join(" ");
326
+ }
327
+
328
+ private renderServerRow(state: McpPanelViewState, server: ServerState, isCursor: boolean): string {
329
+ const expandIcon = server.expanded ? "▾" : "▸";
330
+ const prefix = isCursor
331
+ ? this.theme.selected(expandIcon)
332
+ : this.theme.border(server.expanded ? expandIcon : "·");
333
+
334
+ const serverName = sanitizeDisplayText(server.name);
335
+ const importKind = sanitizeDisplayText(server.importKind ?? "import");
336
+ const name = isCursor
337
+ ? this.theme.bold(this.theme.selected(serverName))
338
+ : serverName;
339
+ const importLabel = server.source === "import" ? this.theme.description(` (${importKind})`) : "";
340
+ const statusLabel = this.renderConnectionStatus(state, server);
341
+
342
+ if (!server.hasCachedData && !state.authOnly) {
343
+ return `${prefix} ${name}${importLabel} ${this.theme.description("(not cached)")}${statusLabel}`;
344
+ }
345
+
346
+ const directCount = server.directCount;
347
+ const totalCount = server.tools.length;
348
+ let toggleIcon = this.theme.description("○");
349
+ if (directCount === totalCount && totalCount > 0) {
350
+ toggleIcon = this.theme.direct("●");
351
+ } else if (directCount > 0) {
352
+ toggleIcon = this.theme.needsAuth("◐");
353
+ }
354
+
355
+ let toolInfo = "";
356
+ if (totalCount > 0) {
357
+ toolInfo = `${directCount}/${totalCount}`;
358
+ if (directCount > 0) toolInfo += ` ~${server.directTokens.toLocaleString()}`;
359
+ toolInfo = this.theme.description(toolInfo);
360
+ }
361
+
362
+ return `${prefix} ${toggleIcon} ${name}${importLabel} ${toolInfo}${statusLabel}`;
363
+ }
364
+
365
+ private selectedServerHasFailureMessage(state: McpPanelViewState): boolean {
366
+ const item = state.visibleItems[state.cursorIndex];
367
+ if (!item) return false;
368
+ const server = state.servers[item.serverIndex];
369
+ return server?.connectionStatus === "failed" && !!server.failureMessage;
370
+ }
371
+
372
+ private wrapText(text: string, width: number): string[] {
373
+ const max = Math.max(8, width);
374
+ const words = text.split(/\s+/).filter(Boolean);
375
+ const lines: string[] = [];
376
+ let current = "";
377
+ const splitLongWord = (word: string): string => {
378
+ let rest = word;
379
+ while (visibleWidth(rest) > max) {
380
+ let take = "";
381
+ let index = 0;
382
+ while (index < rest.length && visibleWidth(take + rest.charAt(index)) <= max) {
383
+ take += rest.charAt(index);
384
+ index++;
385
+ }
386
+ if (!take) take = rest.charAt(0);
387
+ lines.push(take);
388
+ rest = rest.slice(take.length);
389
+ }
390
+ return rest;
391
+ };
392
+
393
+ for (const word of words) {
394
+ const candidate = current ? `${current} ${word}` : word;
395
+ if (visibleWidth(candidate) <= max) {
396
+ current = candidate;
397
+ } else {
398
+ if (current) lines.push(current);
399
+ current = splitLongWord(word);
400
+ }
401
+ }
402
+ if (current) lines.push(current);
403
+ return lines.length > 0 ? lines : [text];
404
+ }
405
+
406
+ private renderConnectionStatus(state: McpPanelViewState, server: ServerState): string {
407
+ if (state.authInFlight === server.name) return ` ${this.theme.needsAuth("authenticating")}`;
408
+ if (server.disabled) return ` ${this.theme.description("disabled")}`;
409
+ if (server.connectionStatus === "needs-auth") return ` ${this.theme.needsAuth("needs auth")}`;
410
+ if (server.connectionStatus === "connecting") return ` ${this.theme.needsAuth("connecting")}`;
411
+ if (server.connectionStatus === "failed") return ` ${this.theme.cancel("failed")}`;
412
+ if (state.authOnly && server.connectionStatus === "connected") return ` ${this.theme.direct("connected")}`;
413
+ if (state.authOnly) return ` ${this.theme.description("idle")}`;
414
+ return "";
415
+ }
416
+
417
+ private renderToolRow(tool: ToolState, isCursor: boolean, innerWidth: number): string {
418
+ const toggleIcon = tool.isDirect ? this.theme.direct("●") : this.theme.description("○");
419
+ const cursor = isCursor ? this.theme.selected("▸") : " ";
420
+ const toolName = sanitizeDisplayText(tool.name);
421
+ const description = sanitizeDisplayText(tool.description);
422
+ const name = isCursor ? this.theme.bold(this.theme.selected(toolName)) : toolName;
423
+
424
+ const prefixLength = 7 + visibleWidth(toolName);
425
+ const maxDescriptionLength = Math.max(0, innerWidth - prefixLength - 8);
426
+ const descriptionText = maxDescriptionLength > 5 && description
427
+ ? this.theme.description(`— ${truncateToWidth(description, maxDescriptionLength, "…")}`)
428
+ : "";
429
+
430
+ return ` ${cursor} ${toggleIcon} ${name} ${descriptionText}`;
431
+ }
432
+ }
433
+
153
434
  class McpPanel {
154
435
  private noticeLines: string[];
155
436
  private prefix: ToolPrefix;
@@ -167,11 +448,11 @@ class McpPanel {
167
448
  private inactivityTimeout: ReturnType<typeof setTimeout> | null = null;
168
449
  private visibleItems: VisibleItem[] = [];
169
450
  private tui: { requestRender(): void };
170
- private t = DEFAULT_THEME;
451
+ private readonly view: McpPanelView;
171
452
  private authOnly: boolean;
172
453
  private keys: PanelKeys;
173
454
 
174
- private static readonly MAX_VISIBLE = 12;
455
+ static readonly MAX_VISIBLE = 12;
175
456
  private static readonly INACTIVITY_MS = 60_000;
176
457
 
177
458
  constructor(
@@ -181,12 +462,13 @@ class McpPanel {
181
462
  private callbacks: McpPanelCallbacks,
182
463
  tui: { requestRender(): void },
183
464
  private done: (result: McpPanelResult) => void,
184
- options: { noticeLines?: string[]; authOnly?: boolean; keybindings?: PanelKeybindings } = {},
465
+ options: { noticeLines?: string[]; authOnly?: boolean; keybindings?: PanelKeybindings; theme?: Theme } = {},
185
466
  ) {
186
467
  this.tui = tui;
187
468
  this.noticeLines = options.noticeLines ?? [];
188
469
  this.authOnly = options.authOnly === true;
189
470
  this.keys = createPanelKeys(options.keybindings);
471
+ this.view = new McpPanelView(() => this.getViewState(), createMcpPanelTheme(options.theme));
190
472
  this.prefix = config.settings?.toolPrefix ?? "server";
191
473
 
192
474
  for (const [serverName, definition] of Object.entries(config.mcpServers)) {
@@ -197,10 +479,11 @@ class McpPanel {
197
479
 
198
480
  const globalDirect = config.settings?.directTools;
199
481
  let toolFilter: true | string[] | false = false;
200
- if (definition.directTools !== undefined) {
201
- toolFilter = definition.directTools;
202
- } else if (globalDirect) {
203
- toolFilter = globalDirect;
482
+ const selected = definition.directTools !== undefined ? definition.directTools : globalDirect;
483
+ if (selected === "search") {
484
+ toolFilter = true; // search-activated tools are still direct tools for the panel
485
+ } else if (selected !== undefined) {
486
+ toolFilter = selected;
204
487
  }
205
488
 
206
489
  const tools: ToolState[] = [];
@@ -599,8 +882,8 @@ class McpPanel {
599
882
  this.cache ??= { version: 1, servers: {} };
600
883
  this.cache.servers[server.name] = entry;
601
884
  this.rebuildServerTools(server, entry);
885
+ server.hasCachedData = true;
602
886
  }
603
- server.hasCachedData = true;
604
887
  }
605
888
  if (options.afterAuth) {
606
889
  this.authNotice = connected && server.connectionStatus === "connected"
@@ -770,283 +1053,34 @@ class McpPanel {
770
1053
  this.updateDirty();
771
1054
  }
772
1055
 
773
- render(width: number): string[] {
774
- const innerW = width - 2;
775
- const lines: string[] = [];
776
- const t = this.t;
777
- const bold = (s: string) => `\x1b[1m${s}\x1b[22m`;
778
- const italic = (s: string) => `\x1b[3m${s}\x1b[23m`;
779
- const inverse = (s: string) => `\x1b[7m${s}\x1b[27m`;
780
-
781
- const row = (content: string) =>
782
- fg(t.border, "│") + truncateToWidth(" " + sanitizeRowContent(content), innerW, "…", true) + fg(t.border, "│");
783
- const emptyRow = () => fg(t.border, "│") + " ".repeat(innerW) + fg(t.border, "│");
784
- const divider = () => fg(t.border, "├" + "─".repeat(innerW) + "┤");
785
-
786
- const titleText = this.authOnly ? " MCP OAuth " : " MCP Servers ";
787
- const borderLen = innerW - visibleWidth(titleText);
788
- const leftB = Math.floor(borderLen / 2);
789
- const rightB = borderLen - leftB;
790
- lines.push(fg(t.border, "╭" + "─".repeat(leftB)) + fg(t.title, titleText) + fg(t.border, "─".repeat(rightB) + "╮"));
791
-
792
- lines.push(emptyRow());
793
-
794
- const cursor = fg(t.selected, "│");
795
- const searchIcon = fg(t.border, "◎");
796
- if (this.descSearchActive) {
797
- lines.push(row(`${searchIcon} ${fg(t.needsAuth, "desc:")} ${this.descQuery}${cursor}`));
798
- } else if (this.nameQuery) {
799
- lines.push(row(`${searchIcon} ${this.nameQuery}${cursor}`));
800
- } else {
801
- lines.push(row(`${searchIcon} ${fg(t.placeholder, italic("search..."))}`));
802
- }
803
-
804
- lines.push(emptyRow());
805
- if (this.noticeLines.length > 0) {
806
- for (const notice of this.noticeLines) {
807
- lines.push(row(fg(t.hint, italic(sanitizeDisplayText(notice)))));
808
- }
809
- lines.push(emptyRow());
810
- }
811
- lines.push(divider());
812
-
813
- if (this.servers.length === 0) {
814
- lines.push(emptyRow());
815
- lines.push(row(fg(t.hint, italic(this.authOnly ? "No OAuth-capable MCP servers configured." : "No MCP servers configured."))));
816
- lines.push(emptyRow());
817
- } else {
818
- const maxVis = McpPanel.MAX_VISIBLE;
819
- const total = this.visibleItems.length;
820
- const startIdx = Math.max(0, Math.min(this.cursorIndex - Math.floor(maxVis / 2), total - maxVis));
821
- const endIdx = Math.min(startIdx + maxVis, total);
822
-
823
- lines.push(emptyRow());
824
-
825
- for (let i = startIdx; i < endIdx; i++) {
826
- const item = this.visibleItems[i];
827
- if (!item) continue;
828
- const isCursor = i === this.cursorIndex;
829
- const server = this.servers[item.serverIndex];
830
- if (!server) continue;
831
-
832
- if (item.type === "server") {
833
- lines.push(row(this.renderServerRow(server, isCursor)));
834
- if (isCursor && server.connectionStatus === "failed" && server.failureMessage) {
835
- for (const line of this.wrapText(sanitizeDisplayText(server.failureMessage), innerW - 6)) {
836
- lines.push(row(` ${fg(t.cancel, line)}`));
837
- }
838
- }
839
- } else if (item.toolIndex !== undefined) {
840
- const tool = server.tools[item.toolIndex];
841
- if (tool) lines.push(row(this.renderToolRow(tool, isCursor, innerW)));
842
- }
843
- }
844
-
845
- lines.push(emptyRow());
846
-
847
- if (total > maxVis) {
848
- const prog = Math.round(((this.cursorIndex + 1) / total) * 10);
849
- lines.push(row(`${rainbowProgress(prog, 10)} ${fg(t.hint, `${this.cursorIndex + 1}/${total}`)}`));
850
- lines.push(emptyRow());
851
- }
852
-
853
- if (this.importNotice) {
854
- lines.push(row(fg(t.needsAuth, italic(sanitizeDisplayText(this.importNotice)))));
855
- lines.push(emptyRow());
856
- }
857
- if (this.authNotice) {
858
- lines.push(row(fg(t.needsAuth, italic(sanitizeDisplayText(this.authNotice)))));
859
- lines.push(emptyRow());
860
- }
861
- }
862
-
863
- lines.push(divider());
864
- lines.push(emptyRow());
865
-
866
- if (this.confirmingDiscard) {
867
- const discardBtn = this.discardSelected === 0
868
- ? inverse(bold(fg(t.cancel, " Discard ")))
869
- : fg(t.hint, " Discard ");
870
- const keepBtn = this.discardSelected === 1
871
- ? inverse(bold(fg(t.confirm, " Keep & Close ")))
872
- : fg(t.hint, " Keep & Close ");
873
- lines.push(row(`Discard unsaved changes? ${discardBtn} ${keepBtn}`));
874
- } else {
875
- if (this.authOnly) {
876
- lines.push(row(fg(t.description, "select a server to authenticate")));
877
- } else {
878
- let directCount = 0;
879
- let directTokens = 0;
880
- for (const server of this.servers) {
881
- directCount += server.directCount;
882
- directTokens += server.directTokens;
883
- }
884
- const stats =
885
- directCount > 0 ? `${directCount} direct ~${directTokens.toLocaleString()} tokens` : "no direct tools";
886
- lines.push(row(fg(t.description, stats + (this.dirty ? fg(t.needsAuth, " (unsaved)") : ""))));
887
- }
888
- }
889
-
890
- lines.push(emptyRow());
891
- const saveLabel = this.keys.saveLabel();
892
- const hints = this.authOnly
893
- ? [
894
- italic("↑↓") + " navigate",
895
- italic("⏎") + " auth",
896
- italic("ctrl+a") + " auth",
897
- italic("esc") + " clear/close",
898
- italic("ctrl+c") + " quit",
899
- ]
900
- : [
901
- italic("↑↓") + " navigate",
902
- italic("space") + " toggle",
903
- italic("⏎") + " expand/auth",
904
- italic("ctrl+a") + " auth",
905
- italic("ctrl+r") + " reconnect",
906
- italic("ctrl+d") + " disable/enable",
907
- ...(this.selectedServerHasFailureMessage() ? [italic("ctrl+y") + " copy error"] : []),
908
- italic("?") + " desc search",
909
- ...(saveLabel ? [italic(saveLabel) + " save"] : []),
910
- italic("esc") + " clear/close",
911
- italic("ctrl+c") + " quit",
912
- ];
913
- const gap = " ";
914
- const gapW = 2;
915
- const maxW = innerW - 2;
916
- let curLine = "";
917
- let curW = 0;
918
- for (const hint of hints) {
919
- const hw = visibleWidth(hint);
920
- const needed = curW === 0 ? hw : gapW + hw;
921
- if (curW > 0 && curW + needed > maxW) {
922
- lines.push(row(fg(t.hint, curLine)));
923
- curLine = hint;
924
- curW = hw;
925
- } else {
926
- curLine += (curW > 0 ? gap : "") + hint;
927
- curW += needed;
928
- }
929
- }
930
- if (curLine) lines.push(row(fg(t.hint, curLine)));
931
-
932
- lines.push(fg(t.border, "╰" + "─".repeat(innerW) + "╯"));
933
-
934
- return lines;
935
- }
936
-
937
- private renderServerRow(server: ServerState, isCursor: boolean): string {
938
- const t = this.t;
939
- const bold = (s: string) => `\x1b[1m${s}\x1b[22m`;
940
-
941
- const expandIcon = server.expanded ? "▾" : "▸";
942
- const prefix = isCursor ? fg(t.selected, expandIcon) : fg(t.border, server.expanded ? expandIcon : "·");
943
-
944
- const serverName = sanitizeDisplayText(server.name);
945
- const importKind = sanitizeDisplayText(server.importKind ?? "import");
946
- const nameStr = isCursor ? bold(fg(t.selected, serverName)) : serverName;
947
- const importLabel = server.source === "import" ? fg(t.description, ` (${importKind})`) : "";
948
- const statusLabel = this.renderConnectionStatus(server);
949
-
950
- if (!server.hasCachedData && !this.authOnly) {
951
- return `${prefix} ${nameStr}${importLabel} ${fg(t.description, "(not cached)")}${statusLabel}`;
952
- }
953
-
954
- const directCount = server.directCount;
955
- const totalCount = server.tools.length;
956
- let toggleIcon = fg(t.description, "○");
957
- if (directCount === totalCount && totalCount > 0) {
958
- toggleIcon = fg(t.direct, "●");
959
- } else if (directCount > 0) {
960
- toggleIcon = fg(t.needsAuth, "◐");
961
- }
962
-
963
- let toolInfo = "";
964
- if (totalCount > 0) {
965
- toolInfo = `${directCount}/${totalCount}`;
966
- if (directCount > 0) {
967
- toolInfo += ` ~${server.directTokens.toLocaleString()}`;
968
- }
969
- toolInfo = fg(t.description, toolInfo);
970
- }
971
-
972
- return `${prefix} ${toggleIcon} ${nameStr}${importLabel} ${toolInfo}${statusLabel}`;
973
- }
974
-
975
- private selectedServerHasFailureMessage(): boolean {
976
- const item = this.visibleItems[this.cursorIndex];
977
- if (!item) return false;
978
- const server = this.servers[item.serverIndex];
979
- return server?.connectionStatus === "failed" && !!server.failureMessage;
980
- }
981
-
982
- private wrapText(text: string, width: number): string[] {
983
- const max = Math.max(8, width);
984
- const words = text.split(/\s+/).filter(Boolean);
985
- const lines: string[] = [];
986
- let current = "";
987
- const splitLongWord = (word: string): string => {
988
- let rest = word;
989
- while (visibleWidth(rest) > max) {
990
- let take = "";
991
- let index = 0;
992
- while (index < rest.length && visibleWidth(take + rest.charAt(index)) <= max) {
993
- take += rest.charAt(index);
994
- index++;
995
- }
996
- if (!take) take = rest.charAt(0);
997
- lines.push(take);
998
- rest = rest.slice(take.length);
999
- }
1000
- return rest;
1056
+ private getViewState(): McpPanelViewState {
1057
+ return {
1058
+ noticeLines: this.noticeLines,
1059
+ servers: this.servers,
1060
+ visibleItems: this.visibleItems,
1061
+ cursorIndex: this.cursorIndex,
1062
+ nameQuery: this.nameQuery,
1063
+ descSearchActive: this.descSearchActive,
1064
+ descQuery: this.descQuery,
1065
+ dirty: this.dirty,
1066
+ confirmingDiscard: this.confirmingDiscard,
1067
+ discardSelected: this.discardSelected,
1068
+ importNotice: this.importNotice,
1069
+ authNotice: this.authNotice,
1070
+ authInFlight: this.authInFlight,
1071
+ authOnly: this.authOnly,
1072
+ saveLabel: this.keys.saveLabel(),
1001
1073
  };
1002
-
1003
- for (const word of words) {
1004
- const candidate = current ? `${current} ${word}` : word;
1005
- if (visibleWidth(candidate) <= max) {
1006
- current = candidate;
1007
- } else {
1008
- if (current) lines.push(current);
1009
- current = splitLongWord(word);
1010
- }
1011
- }
1012
- if (current) lines.push(current);
1013
- return lines.length > 0 ? lines : [text];
1014
1074
  }
1015
1075
 
1016
- private renderConnectionStatus(server: ServerState): string {
1017
- const t = this.t;
1018
- if (this.authInFlight === server.name) return ` ${fg(t.needsAuth, "authenticating")}`;
1019
- if (server.disabled) return ` ${fg(t.description, "disabled")}`;
1020
- if (server.connectionStatus === "needs-auth") return ` ${fg(t.needsAuth, "needs auth")}`;
1021
- if (server.connectionStatus === "connecting") return ` ${fg(t.needsAuth, "connecting")}`;
1022
- if (server.connectionStatus === "failed") return ` ${fg(t.cancel, "failed")}`;
1023
- if (this.authOnly && server.connectionStatus === "connected") return ` ${fg(t.direct, "connected")}`;
1024
- if (this.authOnly) return ` ${fg(t.description, "idle")}`;
1025
- return "";
1076
+ render(width: number): string[] {
1077
+ return this.view.render(width);
1026
1078
  }
1027
1079
 
1028
- private renderToolRow(tool: ToolState, isCursor: boolean, innerW: number): string {
1029
- const t = this.t;
1030
- const bold = (s: string) => `\x1b[1m${s}\x1b[22m`;
1031
-
1032
- const toggleIcon = tool.isDirect ? fg(t.direct, "●") : fg(t.description, "○");
1033
- const cursor = isCursor ? fg(t.selected, "▸") : " ";
1034
- const toolName = sanitizeDisplayText(tool.name);
1035
- const description = sanitizeDisplayText(tool.description);
1036
- const nameStr = isCursor ? bold(fg(t.selected, toolName)) : toolName;
1037
-
1038
- const prefixLen = 7 + visibleWidth(toolName);
1039
- const maxDescLen = Math.max(0, innerW - prefixLen - 8);
1040
- const descStr =
1041
- maxDescLen > 5 && description
1042
- ? fg(t.description, "— " + truncateToWidth(description, maxDescLen, "…"))
1043
- : "";
1044
-
1045
- return ` ${cursor} ${toggleIcon} ${nameStr} ${descStr}`;
1080
+ invalidate(): void {
1081
+ this.view.invalidate();
1046
1082
  }
1047
1083
 
1048
- invalidate(): void {}
1049
-
1050
1084
  dispose(): void {
1051
1085
  this.cleanup();
1052
1086
  }
@@ -1059,7 +1093,7 @@ export function createMcpPanel(
1059
1093
  callbacks: McpPanelCallbacks,
1060
1094
  tui: { requestRender(): void },
1061
1095
  done: (result: McpPanelResult) => void,
1062
- options?: { noticeLines?: string[]; authOnly?: boolean; keybindings?: PanelKeybindings },
1096
+ options?: { noticeLines?: string[]; authOnly?: boolean; keybindings?: PanelKeybindings; theme?: Theme },
1063
1097
  ): McpPanel & { dispose(): void } {
1064
1098
  return new McpPanel(config, cache, provenance, callbacks, tui, done, options ?? {});
1065
1099
  }