@humain/terminal 0.1.0 → 0.2.1

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 (220) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/bundle/chunks/{anthropic-messages-XULXUBIG.js → anthropic-messages-QFCEKGI3.js} +1 -1
  3. package/dist/bundle/chunks/{azure-openai-responses-JEG2XOYC.js → azure-openai-responses-G4ZMHV37.js} +1 -1
  4. package/dist/bundle/chunks/chunk-7T6XCLJF.js +22 -0
  5. package/dist/bundle/chunks/chunk-D6HHRSB5.js +103 -0
  6. package/dist/bundle/chunks/chunk-EY4DWHQU.js +2 -0
  7. package/dist/bundle/chunks/{chunk-RM336IEI.js → chunk-I47ZWR55.js} +486 -509
  8. package/dist/bundle/chunks/{chunk-JUPSJTFQ.js → chunk-K4JAZYQH.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-HPPQHK3J.js → chunk-KKIB2TVR.js} +1 -1
  10. package/dist/bundle/chunks/chunk-LI2YVPWP.js +2 -0
  11. package/dist/bundle/chunks/{chunk-BZQ53VGE.js → chunk-XSR434CO.js} +1 -1
  12. package/dist/bundle/chunks/compat-QCV26HNS.js +2 -0
  13. package/dist/bundle/chunks/dist-PRPIUY3U.js +2 -0
  14. package/dist/bundle/chunks/esm-E7UVEYTO.js +4 -0
  15. package/dist/bundle/chunks/{extract-TRW5FBY6.js → extract-CTIFQZDJ.js} +15 -14
  16. package/dist/bundle/chunks/{google-generative-ai-OX6VSCA6.js → google-generative-ai-RSUU4MWO.js} +1 -1
  17. package/dist/bundle/chunks/{google-vertex-GW76GSS2.js → google-vertex-CKQPOITO.js} +1 -1
  18. package/dist/bundle/chunks/host-WP3MVKS2.js +2 -0
  19. package/dist/bundle/chunks/{node-KOIP55YZ.js → node-NF43OPII.js} +1 -1
  20. package/dist/bundle/chunks/{node-XY3CBY3C.js → node-UMFM7CDZ.js} +1 -1
  21. package/dist/bundle/chunks/{openai-codex-responses-NPLWP6ZC.js → openai-codex-responses-S34V5ZSD.js} +1 -1
  22. package/dist/bundle/chunks/{openai-completions-OT2GTJKC.js → openai-completions-T2IGVUEI.js} +1 -1
  23. package/dist/bundle/chunks/{openai-responses-5A6CR7GI.js → openai-responses-GYZODCT3.js} +1 -1
  24. package/dist/bundle/chunks/readability-LAGF34IB.js +3 -0
  25. package/dist/bundle/chunks/turndown.cjs-HRMZVRUL.js +2 -0
  26. package/dist/bundle/cli.js +1 -1
  27. package/dist/bundle/index.js +1 -1
  28. package/dist/bundle/rpc-entry.js +1 -1
  29. package/dist/cli.js +1 -5
  30. package/dist/core/agent-session.js +2 -2
  31. package/dist/core/agent-session.js.map +1 -1
  32. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  33. package/dist/core/tools/file-mutation-queue.js +21 -8
  34. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  35. package/dist/core/tools/subagent-env.d.ts +18 -0
  36. package/dist/core/tools/subagent-env.d.ts.map +1 -0
  37. package/dist/core/tools/subagent-env.js +43 -0
  38. package/dist/core/tools/subagent-env.js.map +1 -0
  39. package/dist/core/tools/subagent-scheduler.d.ts +31 -0
  40. package/dist/core/tools/subagent-scheduler.d.ts.map +1 -0
  41. package/dist/core/tools/subagent-scheduler.js +203 -0
  42. package/dist/core/tools/subagent-scheduler.js.map +1 -0
  43. package/dist/core/tools/subagent.d.ts +83 -3
  44. package/dist/core/tools/subagent.d.ts.map +1 -1
  45. package/dist/core/tools/subagent.js +214 -76
  46. package/dist/core/tools/subagent.js.map +1 -1
  47. package/dist/humain/index.d.ts.map +1 -1
  48. package/dist/humain/index.js +2 -0
  49. package/dist/humain/index.js.map +1 -1
  50. package/dist/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
  51. package/dist/humain/mcp-adapter/vendor/README.md +125 -19
  52. package/dist/humain/mcp-adapter/vendor/agent-plugin-loader.ts +69 -30
  53. package/dist/humain/mcp-adapter/vendor/agent-plugin-provenance.ts +36 -0
  54. package/dist/humain/mcp-adapter/vendor/claude-plugin-loader.ts +365 -0
  55. package/dist/humain/mcp-adapter/vendor/commands.ts +13 -15
  56. package/dist/humain/mcp-adapter/vendor/config.ts +198 -30
  57. package/dist/humain/mcp-adapter/vendor/consent-manager.ts +40 -9
  58. package/dist/humain/mcp-adapter/vendor/direct-tool-surface.ts +250 -0
  59. package/dist/humain/mcp-adapter/vendor/direct-tools.ts +9 -228
  60. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js +74 -32
  61. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js.map +1 -1
  62. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.d.ts +1 -0
  63. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js +31 -0
  64. package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js.map +1 -0
  65. package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.d.ts +9 -0
  66. package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js +362 -0
  67. package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js.map +1 -0
  68. package/dist/humain/mcp-adapter/vendor/dist/config.d.ts +3 -1
  69. package/dist/humain/mcp-adapter/vendor/dist/config.js +189 -27
  70. package/dist/humain/mcp-adapter/vendor/dist/config.js.map +1 -1
  71. package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js +7 -16
  72. package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js.map +1 -1
  73. package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js +5 -17
  74. package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js.map +1 -1
  75. package/dist/humain/mcp-adapter/vendor/dist/types.d.ts +30 -4
  76. package/dist/humain/mcp-adapter/vendor/dist/types.js +26 -0
  77. package/dist/humain/mcp-adapter/vendor/dist/types.js.map +1 -1
  78. package/dist/humain/mcp-adapter/vendor/dist/utils.d.ts +6 -0
  79. package/dist/humain/mcp-adapter/vendor/dist/utils.js +65 -6
  80. package/dist/humain/mcp-adapter/vendor/dist/utils.js.map +1 -1
  81. package/dist/humain/mcp-adapter/vendor/host-html-template.ts +20 -19
  82. package/dist/humain/mcp-adapter/vendor/http-ca.ts +84 -0
  83. package/dist/humain/mcp-adapter/vendor/index.bundle.mjs +37102 -15103
  84. package/dist/humain/mcp-adapter/vendor/index.ts +828 -164
  85. package/dist/humain/mcp-adapter/vendor/init.ts +48 -10
  86. package/dist/humain/mcp-adapter/vendor/lazy-loader.ts +7 -0
  87. package/dist/humain/mcp-adapter/vendor/mcp-auth-fetch.ts +113 -0
  88. package/dist/humain/mcp-adapter/vendor/mcp-auth-flow.ts +256 -77
  89. package/dist/humain/mcp-adapter/vendor/mcp-auth.ts +316 -54
  90. package/dist/humain/mcp-adapter/vendor/mcp-code.ts +45 -18
  91. package/dist/humain/mcp-adapter/vendor/mcp-install.ts +60 -0
  92. package/dist/humain/mcp-adapter/vendor/mcp-oauth-provider.ts +70 -6
  93. package/dist/humain/mcp-adapter/vendor/mcp-output-guard.ts +53 -42
  94. package/dist/humain/mcp-adapter/vendor/mcp-panel-theme.ts +105 -0
  95. package/dist/humain/mcp-adapter/vendor/mcp-panel.ts +366 -331
  96. package/dist/humain/mcp-adapter/vendor/mcp-references.ts +9 -14
  97. package/dist/humain/mcp-adapter/vendor/mcp-script-worker.mjs +3 -1
  98. package/dist/humain/mcp-adapter/vendor/mcp-setup-panel.ts +383 -341
  99. package/dist/humain/mcp-adapter/vendor/mcp-status.ts +5 -0
  100. package/dist/humain/mcp-adapter/vendor/metadata-cache.ts +7 -16
  101. package/dist/humain/mcp-adapter/vendor/namespace-tools.ts +61 -3
  102. package/dist/humain/mcp-adapter/vendor/oauth.ts +6 -2
  103. package/dist/humain/mcp-adapter/vendor/package-mcp-loader.ts +5 -17
  104. package/dist/humain/mcp-adapter/vendor/package.json +13 -3
  105. package/dist/humain/mcp-adapter/vendor/prompts.ts +35 -3
  106. package/dist/humain/mcp-adapter/vendor/proxy-modes.ts +108 -7
  107. package/dist/humain/mcp-adapter/vendor/runtime-owner.ts +16 -1
  108. package/dist/humain/mcp-adapter/vendor/sampling-handler.ts +9 -32
  109. package/dist/humain/mcp-adapter/vendor/sandbox-proxy-template.ts +20 -93
  110. package/dist/humain/mcp-adapter/vendor/server-manager.ts +227 -40
  111. package/dist/humain/mcp-adapter/vendor/session-approvals.ts +187 -0
  112. package/dist/humain/mcp-adapter/vendor/skills/mcp-scripting/SKILL.md +9 -1
  113. package/dist/humain/mcp-adapter/vendor/state.ts +10 -1
  114. package/dist/humain/mcp-adapter/vendor/tool-approval.ts +10 -22
  115. package/dist/humain/mcp-adapter/vendor/tool-metadata.ts +14 -0
  116. package/dist/humain/mcp-adapter/vendor/types.ts +57 -4
  117. package/dist/humain/mcp-adapter/vendor/ui-server.ts +36 -28
  118. package/dist/humain/mcp-adapter/vendor/utils.ts +64 -6
  119. package/dist/humain/node-http.d.ts +3 -0
  120. package/dist/humain/node-http.d.ts.map +1 -0
  121. package/dist/humain/node-http.js +119 -0
  122. package/dist/humain/node-http.js.map +1 -0
  123. package/dist/humain/node-provider.d.ts +4 -0
  124. package/dist/humain/node-provider.d.ts.map +1 -0
  125. package/dist/humain/node-provider.js +359 -0
  126. package/dist/humain/node-provider.js.map +1 -0
  127. package/dist/humain/web-access/vendor/abortable.d.ts +3 -0
  128. package/dist/humain/web-access/vendor/abortable.d.ts.map +1 -0
  129. package/dist/humain/web-access/vendor/abortable.js +22 -0
  130. package/dist/humain/web-access/vendor/abortable.js.map +1 -0
  131. package/dist/humain/web-access/vendor/crawl4ai.d.ts +9 -0
  132. package/dist/humain/web-access/vendor/crawl4ai.d.ts.map +1 -0
  133. package/dist/humain/web-access/vendor/crawl4ai.js +187 -0
  134. package/dist/humain/web-access/vendor/crawl4ai.js.map +1 -0
  135. package/dist/humain/web-access/vendor/credential-source.d.ts.map +1 -1
  136. package/dist/humain/web-access/vendor/credential-source.js +1 -0
  137. package/dist/humain/web-access/vendor/credential-source.js.map +1 -1
  138. package/dist/humain/web-access/vendor/curator-page.d.ts.map +1 -1
  139. package/dist/humain/web-access/vendor/curator-page.js +20 -2
  140. package/dist/humain/web-access/vendor/curator-page.js.map +1 -1
  141. package/dist/humain/web-access/vendor/curator-run.d.ts +11 -0
  142. package/dist/humain/web-access/vendor/curator-run.d.ts.map +1 -0
  143. package/dist/humain/web-access/vendor/curator-run.js +38 -0
  144. package/dist/humain/web-access/vendor/curator-run.js.map +1 -0
  145. package/dist/humain/web-access/vendor/curator-server.d.ts +1 -0
  146. package/dist/humain/web-access/vendor/curator-server.d.ts.map +1 -1
  147. package/dist/humain/web-access/vendor/curator-server.js +2 -0
  148. package/dist/humain/web-access/vendor/curator-server.js.map +1 -1
  149. package/dist/humain/web-access/vendor/duckduckgo.d.ts.map +1 -1
  150. package/dist/humain/web-access/vendor/duckduckgo.js +1 -1
  151. package/dist/humain/web-access/vendor/duckduckgo.js.map +1 -1
  152. package/dist/humain/web-access/vendor/extract.d.ts +1 -1
  153. package/dist/humain/web-access/vendor/extract.d.ts.map +1 -1
  154. package/dist/humain/web-access/vendor/extract.js +58 -11
  155. package/dist/humain/web-access/vendor/extract.js.map +1 -1
  156. package/dist/humain/web-access/vendor/fetch-params.js +1 -1
  157. package/dist/humain/web-access/vendor/fetch-params.js.map +1 -1
  158. package/dist/humain/web-access/vendor/gemini-search.d.ts +2 -2
  159. package/dist/humain/web-access/vendor/gemini-search.d.ts.map +1 -1
  160. package/dist/humain/web-access/vendor/gemini-search.js +12 -5
  161. package/dist/humain/web-access/vendor/gemini-search.js.map +1 -1
  162. package/dist/humain/web-access/vendor/index.d.ts.map +1 -1
  163. package/dist/humain/web-access/vendor/index.js +23 -19
  164. package/dist/humain/web-access/vendor/index.js.map +1 -1
  165. package/dist/humain/web-access/vendor/openai-search.d.ts.map +1 -1
  166. package/dist/humain/web-access/vendor/openai-search.js +40 -14
  167. package/dist/humain/web-access/vendor/openai-search.js.map +1 -1
  168. package/dist/humain/web-access/vendor/page-query.d.ts.map +1 -1
  169. package/dist/humain/web-access/vendor/page-query.js +6 -2
  170. package/dist/humain/web-access/vendor/page-query.js.map +1 -1
  171. package/dist/humain/web-access/vendor/query-rewrite.d.ts.map +1 -1
  172. package/dist/humain/web-access/vendor/query-rewrite.js +6 -2
  173. package/dist/humain/web-access/vendor/query-rewrite.js.map +1 -1
  174. package/dist/humain/web-access/vendor/serpapi.d.ts +4 -0
  175. package/dist/humain/web-access/vendor/serpapi.d.ts.map +1 -0
  176. package/dist/humain/web-access/vendor/serpapi.js +215 -0
  177. package/dist/humain/web-access/vendor/serpapi.js.map +1 -0
  178. package/dist/humain/web-access/vendor/ssrf-protection.d.ts.map +1 -1
  179. package/dist/humain/web-access/vendor/ssrf-protection.js +5 -1
  180. package/dist/humain/web-access/vendor/ssrf-protection.js.map +1 -1
  181. package/dist/humain/web-access/vendor/storage.d.ts.map +1 -1
  182. package/dist/humain/web-access/vendor/storage.js +9 -0
  183. package/dist/humain/web-access/vendor/storage.js.map +1 -1
  184. package/dist/humain/web-access/vendor/summary-review.d.ts +1 -1
  185. package/dist/humain/web-access/vendor/summary-review.d.ts.map +1 -1
  186. package/dist/humain/web-access/vendor/summary-review.js +35 -24
  187. package/dist/humain/web-access/vendor/summary-review.js.map +1 -1
  188. package/dist/humain/web-access/vendor/utils.d.ts.map +1 -1
  189. package/dist/humain/web-access/vendor/utils.js +5 -3
  190. package/dist/humain/web-access/vendor/utils.js.map +1 -1
  191. package/dist/index.js +1 -51
  192. package/dist/main.d.ts.map +1 -1
  193. package/dist/main.js +16 -0
  194. package/dist/main.js.map +1 -1
  195. package/dist/modes/interactive/components/login-dialog.d.ts +7 -2
  196. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  197. package/dist/modes/interactive/components/login-dialog.js +46 -21
  198. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  199. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  200. package/dist/modes/interactive/interactive-mode.js +10 -13
  201. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  202. package/dist/modes/json-event.d.ts +8 -0
  203. package/dist/modes/json-event.d.ts.map +1 -1
  204. package/dist/modes/json-event.js +17 -0
  205. package/dist/modes/json-event.js.map +1 -1
  206. package/dist/modes/print-mode.d.ts.map +1 -1
  207. package/dist/modes/print-mode.js +3 -2
  208. package/dist/modes/print-mode.js.map +1 -1
  209. package/docs/environment-variables.md +8 -0
  210. package/docs/humain-terminal.md +6 -0
  211. package/docs/json.md +16 -0
  212. package/docs/providers.md +19 -1
  213. package/npm-shrinkwrap.json +6 -6
  214. package/package.json +2 -2
  215. package/src/humain/mcp-adapter/UPSTREAM.md +56 -7
  216. package/src/humain/mcp-adapter/upstream.json +6 -4
  217. package/src/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
  218. package/src/humain/web-access/UPSTREAM.md +18 -8
  219. package/src/humain/web-access/upstream.json +6 -5
  220. package/dist/bundle/chunks/chunk-2LC3S3VV.js +0 -105
@@ -1,6 +1,8 @@
1
- import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
1
+ import { Container, Text, matchesKey, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
2
+ import { type Theme } from "@earendil-works/pi-coding-agent";
2
3
  import { copyToClipboard } from "../../../utils/clipboard.ts";
3
4
  import { createPanelKeys, type PanelKeybindings, type PanelKeys } from "./panel-keys.ts";
5
+ import { createMcpPanelTheme, McpPanelFrame, type McpPanelTheme } from "./mcp-panel-theme.ts";
4
6
  import { getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.ts";
5
7
  import type { McpConfig, McpPanelCallbacks, McpPanelResult, ServerProvenance, ToolPrefix } from "./types.ts";
6
8
  import { resourceNameToToolName } from "./resource-tools.ts";
@@ -8,57 +10,6 @@ import { sanitizeTerminalText, stripOscSequences } from "./utils.ts";
8
10
  import { isServerCacheValid, type MetadataCache, type ServerCacheEntry, type CachedTool } from "./metadata-cache.ts";
9
11
  import { isUiToolVisibleToModel } from "./ui-tool-visibility.ts";
10
12
 
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
13
  function fuzzyScore(query: string, text: string): number {
63
14
  const lq = query.toLowerCase();
64
15
  const lt = text.toLowerCase();
@@ -150,6 +101,337 @@ interface VisibleItem {
150
101
  toolIndex?: number;
151
102
  }
152
103
 
104
+ interface McpPanelViewState {
105
+ noticeLines: readonly string[];
106
+ servers: readonly ServerState[];
107
+ visibleItems: readonly VisibleItem[];
108
+ cursorIndex: number;
109
+ nameQuery: string;
110
+ descSearchActive: boolean;
111
+ descQuery: string;
112
+ dirty: boolean;
113
+ confirmingDiscard: boolean;
114
+ discardSelected: number;
115
+ importNotice: string | null;
116
+ authNotice: string | null;
117
+ authInFlight: string | null;
118
+ authOnly: boolean;
119
+ saveLabel: string | null;
120
+ }
121
+
122
+ /**
123
+ * The themed MCP view. It owns only component composition and formatting;
124
+ * input routing and callbacks remain on McpPanel.
125
+ */
126
+ class McpPanelView implements Component {
127
+ private readonly container = new Container();
128
+
129
+ constructor(
130
+ private readonly getState: () => McpPanelViewState,
131
+ private readonly theme: McpPanelTheme,
132
+ ) {}
133
+
134
+ render(width: number): string[] {
135
+ const panelWidth = Math.max(0, width);
136
+ const innerWidth = Math.max(0, panelWidth - 2);
137
+ const state = this.getState();
138
+ this.container.clear();
139
+
140
+ const title = state.authOnly ? "MCP OAuth" : "MCP Servers";
141
+ this.container.addChild(new McpPanelFrame(this.theme, "╭", "╮", title));
142
+ this.addRow("", innerWidth);
143
+
144
+ const cursor = this.theme.selected("│");
145
+ const searchIcon = this.theme.border("◎");
146
+ if (state.descSearchActive) {
147
+ this.addRow(`${searchIcon} ${this.theme.needsAuth("desc:")} ${state.descQuery}${cursor}`, innerWidth);
148
+ } else if (state.nameQuery) {
149
+ this.addRow(`${searchIcon} ${state.nameQuery}${cursor}`, innerWidth);
150
+ } else {
151
+ this.addRow(`${searchIcon} ${this.theme.placeholder(this.theme.italic("search..."))}`, innerWidth);
152
+ }
153
+
154
+ this.addRow("", innerWidth);
155
+ if (state.noticeLines.length > 0) {
156
+ for (const notice of state.noticeLines) {
157
+ this.addRow(this.theme.hint(this.theme.italic(sanitizeDisplayText(notice))), innerWidth);
158
+ }
159
+ this.addRow("", innerWidth);
160
+ }
161
+ this.addRule("├", "┤");
162
+
163
+ if (state.servers.length === 0) {
164
+ this.addRow("", innerWidth);
165
+ this.addRow(
166
+ this.theme.hint(
167
+ this.theme.italic(
168
+ state.authOnly ? "No OAuth-capable MCP servers configured." : "No MCP servers configured.",
169
+ ),
170
+ ),
171
+ innerWidth,
172
+ );
173
+ this.addRow("", innerWidth);
174
+ } else {
175
+ const maxVisible = McpPanel.MAX_VISIBLE;
176
+ const total = state.visibleItems.length;
177
+ const startIndex = Math.max(
178
+ 0,
179
+ Math.min(state.cursorIndex - Math.floor(maxVisible / 2), total - maxVisible),
180
+ );
181
+ const endIndex = Math.min(startIndex + maxVisible, total);
182
+
183
+ this.addRow("", innerWidth);
184
+
185
+ for (let index = startIndex; index < endIndex; index++) {
186
+ const item = state.visibleItems[index];
187
+ if (!item) continue;
188
+ const isCursor = index === state.cursorIndex;
189
+ const server = state.servers[item.serverIndex];
190
+ if (!server) continue;
191
+
192
+ if (item.type === "server") {
193
+ this.addRow(this.renderServerRow(state, server, isCursor), innerWidth);
194
+ if (isCursor && server.connectionStatus === "failed" && server.failureMessage) {
195
+ for (const line of this.wrapText(sanitizeDisplayText(server.failureMessage), innerWidth - 6)) {
196
+ this.addRow(` ${this.theme.cancel(line)}`, innerWidth);
197
+ }
198
+ }
199
+ } else if (item.toolIndex !== undefined) {
200
+ const tool = server.tools[item.toolIndex];
201
+ if (tool) this.addRow(this.renderToolRow(tool, isCursor, innerWidth), innerWidth);
202
+ }
203
+ }
204
+
205
+ this.addRow("", innerWidth);
206
+
207
+ if (total > maxVisible) {
208
+ const progress = Math.round(((state.cursorIndex + 1) / total) * 10);
209
+ this.addRow(
210
+ `${this.progressDots(progress, 10)} ${this.theme.hint(`${state.cursorIndex + 1}/${total}`)}`,
211
+ innerWidth,
212
+ );
213
+ this.addRow("", innerWidth);
214
+ }
215
+
216
+ if (state.importNotice) {
217
+ this.addRow(this.theme.needsAuth(this.theme.italic(sanitizeDisplayText(state.importNotice))), innerWidth);
218
+ this.addRow("", innerWidth);
219
+ }
220
+ if (state.authNotice) {
221
+ this.addRow(this.theme.needsAuth(this.theme.italic(sanitizeDisplayText(state.authNotice))), innerWidth);
222
+ this.addRow("", innerWidth);
223
+ }
224
+ }
225
+
226
+ this.addRule("├", "┤");
227
+ this.addRow("", innerWidth);
228
+
229
+ if (state.confirmingDiscard) {
230
+ const discardButton = state.discardSelected === 0
231
+ ? this.theme.inverse(this.theme.bold(this.theme.cancel(" Discard ")))
232
+ : this.theme.hint(" Discard ");
233
+ const keepButton = state.discardSelected === 1
234
+ ? this.theme.inverse(this.theme.bold(this.theme.confirm(" Keep & Close ")))
235
+ : this.theme.hint(" Keep & Close ");
236
+ this.addRow(`Discard unsaved changes? ${discardButton} ${keepButton}`, innerWidth);
237
+ } else if (state.authOnly) {
238
+ this.addRow(this.theme.description("select a server to authenticate"), innerWidth);
239
+ } else {
240
+ let directCount = 0;
241
+ let directTokens = 0;
242
+ for (const server of state.servers) {
243
+ directCount += server.directCount;
244
+ directTokens += server.directTokens;
245
+ }
246
+ const stats = directCount > 0
247
+ ? `${directCount} direct ~${directTokens.toLocaleString()} tokens`
248
+ : "no direct tools";
249
+ this.addRow(
250
+ this.theme.description(stats + (state.dirty ? this.theme.needsAuth(" (unsaved)") : "")),
251
+ innerWidth,
252
+ );
253
+ }
254
+
255
+ this.addRow("", innerWidth);
256
+ const hints = state.authOnly
257
+ ? [
258
+ this.theme.italic("↑↓") + " navigate",
259
+ this.theme.italic("⏎") + " auth",
260
+ this.theme.italic("ctrl+a") + " auth",
261
+ this.theme.italic("esc") + " clear/close",
262
+ this.theme.italic("ctrl+c") + " quit",
263
+ ]
264
+ : [
265
+ this.theme.italic("↑↓") + " navigate",
266
+ this.theme.italic("space") + " toggle",
267
+ this.theme.italic("⏎") + " expand/auth",
268
+ this.theme.italic("ctrl+a") + " auth",
269
+ this.theme.italic("ctrl+r") + " reconnect",
270
+ this.theme.italic("ctrl+d") + " disable/enable",
271
+ ...(this.selectedServerHasFailureMessage(state) ? [this.theme.italic("ctrl+y") + " copy error"] : []),
272
+ this.theme.italic("?") + " desc search",
273
+ ...(state.saveLabel ? [this.theme.italic(state.saveLabel) + " save"] : []),
274
+ this.theme.italic("esc") + " clear/close",
275
+ this.theme.italic("ctrl+c") + " quit",
276
+ ];
277
+ const gap = " ";
278
+ const gapWidth = 2;
279
+ const maxWidth = innerWidth - 2;
280
+ let currentLine = "";
281
+ let currentWidth = 0;
282
+ for (const hint of hints) {
283
+ const hintWidth = visibleWidth(hint);
284
+ const needed = currentWidth === 0 ? hintWidth : gapWidth + hintWidth;
285
+ if (currentWidth > 0 && currentWidth + needed > maxWidth) {
286
+ this.addRow(this.theme.hint(currentLine), innerWidth);
287
+ currentLine = hint;
288
+ currentWidth = hintWidth;
289
+ } else {
290
+ currentLine += (currentWidth > 0 ? gap : "") + hint;
291
+ currentWidth += needed;
292
+ }
293
+ }
294
+ if (currentLine) this.addRow(this.theme.hint(currentLine), innerWidth);
295
+
296
+ this.addRule("╰", "╯");
297
+ return this.container.render(panelWidth);
298
+ }
299
+
300
+ invalidate(): void {
301
+ this.container.invalidate();
302
+ }
303
+
304
+ private addRow(content: string, innerWidth: number): void {
305
+ this.container.addChild(new Text(this.row(content, innerWidth), 0, 0));
306
+ }
307
+
308
+ private addRule(left: string, right: string): void {
309
+ this.container.addChild(new McpPanelFrame(this.theme, left, right));
310
+ }
311
+
312
+ private row(content: string, innerWidth: number): string {
313
+ const fitted = truncateToWidth(" " + sanitizeRowContent(content), innerWidth, "…", true);
314
+ return this.theme.border("│") + fitted + this.theme.border("│");
315
+ }
316
+
317
+ private progressDots(filled: number, total: number): string {
318
+ const dots: string[] = [];
319
+ for (let index = 0; index < total; index++) {
320
+ if (index < filled) {
321
+ dots.push(this.theme.direct("●"));
322
+ } else {
323
+ dots.push(this.theme.hint("○"));
324
+ }
325
+ }
326
+ return dots.join(" ");
327
+ }
328
+
329
+ private renderServerRow(state: McpPanelViewState, server: ServerState, isCursor: boolean): string {
330
+ const expandIcon = server.expanded ? "▾" : "▸";
331
+ const prefix = isCursor
332
+ ? this.theme.selected(expandIcon)
333
+ : this.theme.border(server.expanded ? expandIcon : "·");
334
+
335
+ const serverName = sanitizeDisplayText(server.name);
336
+ const importKind = sanitizeDisplayText(server.importKind ?? "import");
337
+ const name = isCursor
338
+ ? this.theme.bold(this.theme.selected(serverName))
339
+ : serverName;
340
+ const importLabel = server.source === "import" ? this.theme.description(` (${importKind})`) : "";
341
+ const statusLabel = this.renderConnectionStatus(state, server);
342
+
343
+ if (!server.hasCachedData && !state.authOnly) {
344
+ return `${prefix} ${name}${importLabel} ${this.theme.description("(not cached)")}${statusLabel}`;
345
+ }
346
+
347
+ const directCount = server.directCount;
348
+ const totalCount = server.tools.length;
349
+ let toggleIcon = this.theme.description("○");
350
+ if (directCount === totalCount && totalCount > 0) {
351
+ toggleIcon = this.theme.direct("●");
352
+ } else if (directCount > 0) {
353
+ toggleIcon = this.theme.needsAuth("◐");
354
+ }
355
+
356
+ let toolInfo = "";
357
+ if (totalCount > 0) {
358
+ toolInfo = `${directCount}/${totalCount}`;
359
+ if (directCount > 0) toolInfo += ` ~${server.directTokens.toLocaleString()}`;
360
+ toolInfo = this.theme.description(toolInfo);
361
+ }
362
+
363
+ return `${prefix} ${toggleIcon} ${name}${importLabel} ${toolInfo}${statusLabel}`;
364
+ }
365
+
366
+ private selectedServerHasFailureMessage(state: McpPanelViewState): boolean {
367
+ const item = state.visibleItems[state.cursorIndex];
368
+ if (!item) return false;
369
+ const server = state.servers[item.serverIndex];
370
+ return server?.connectionStatus === "failed" && !!server.failureMessage;
371
+ }
372
+
373
+ private wrapText(text: string, width: number): string[] {
374
+ const max = Math.max(8, width);
375
+ const words = text.split(/\s+/).filter(Boolean);
376
+ const lines: string[] = [];
377
+ let current = "";
378
+ const splitLongWord = (word: string): string => {
379
+ let rest = word;
380
+ while (visibleWidth(rest) > max) {
381
+ let take = "";
382
+ let index = 0;
383
+ while (index < rest.length && visibleWidth(take + rest.charAt(index)) <= max) {
384
+ take += rest.charAt(index);
385
+ index++;
386
+ }
387
+ if (!take) take = rest.charAt(0);
388
+ lines.push(take);
389
+ rest = rest.slice(take.length);
390
+ }
391
+ return rest;
392
+ };
393
+
394
+ for (const word of words) {
395
+ const candidate = current ? `${current} ${word}` : word;
396
+ if (visibleWidth(candidate) <= max) {
397
+ current = candidate;
398
+ } else {
399
+ if (current) lines.push(current);
400
+ current = splitLongWord(word);
401
+ }
402
+ }
403
+ if (current) lines.push(current);
404
+ return lines.length > 0 ? lines : [text];
405
+ }
406
+
407
+ private renderConnectionStatus(state: McpPanelViewState, server: ServerState): string {
408
+ if (state.authInFlight === server.name) return ` ${this.theme.needsAuth("authenticating")}`;
409
+ if (server.disabled) return ` ${this.theme.description("disabled")}`;
410
+ if (server.connectionStatus === "needs-auth") return ` ${this.theme.needsAuth("needs auth")}`;
411
+ if (server.connectionStatus === "connecting") return ` ${this.theme.needsAuth("connecting")}`;
412
+ if (server.connectionStatus === "failed") return ` ${this.theme.cancel("failed")}`;
413
+ if (state.authOnly && server.connectionStatus === "connected") return ` ${this.theme.direct("connected")}`;
414
+ if (state.authOnly) return ` ${this.theme.description("idle")}`;
415
+ return "";
416
+ }
417
+
418
+ private renderToolRow(tool: ToolState, isCursor: boolean, innerWidth: number): string {
419
+ const toggleIcon = tool.isDirect ? this.theme.direct("●") : this.theme.description("○");
420
+ const cursor = isCursor ? this.theme.selected("▸") : " ";
421
+ const toolName = sanitizeDisplayText(tool.name);
422
+ const description = sanitizeDisplayText(tool.description);
423
+ const name = isCursor ? this.theme.bold(this.theme.selected(toolName)) : toolName;
424
+
425
+ const prefixLength = 7 + visibleWidth(toolName);
426
+ const maxDescriptionLength = Math.max(0, innerWidth - prefixLength - 8);
427
+ const descriptionText = maxDescriptionLength > 5 && description
428
+ ? this.theme.description(`— ${truncateToWidth(description, maxDescriptionLength, "…")}`)
429
+ : "";
430
+
431
+ return ` ${cursor} ${toggleIcon} ${name} ${descriptionText}`;
432
+ }
433
+ }
434
+
153
435
  class McpPanel {
154
436
  private noticeLines: string[];
155
437
  private prefix: ToolPrefix;
@@ -167,11 +449,11 @@ class McpPanel {
167
449
  private inactivityTimeout: ReturnType<typeof setTimeout> | null = null;
168
450
  private visibleItems: VisibleItem[] = [];
169
451
  private tui: { requestRender(): void };
170
- private t = DEFAULT_THEME;
452
+ private readonly view: McpPanelView;
171
453
  private authOnly: boolean;
172
454
  private keys: PanelKeys;
173
455
 
174
- private static readonly MAX_VISIBLE = 12;
456
+ static readonly MAX_VISIBLE = 12;
175
457
  private static readonly INACTIVITY_MS = 60_000;
176
458
 
177
459
  constructor(
@@ -181,12 +463,13 @@ class McpPanel {
181
463
  private callbacks: McpPanelCallbacks,
182
464
  tui: { requestRender(): void },
183
465
  private done: (result: McpPanelResult) => void,
184
- options: { noticeLines?: string[]; authOnly?: boolean; keybindings?: PanelKeybindings } = {},
466
+ options: { noticeLines?: string[]; authOnly?: boolean; keybindings?: PanelKeybindings; theme?: Theme } = {},
185
467
  ) {
186
468
  this.tui = tui;
187
469
  this.noticeLines = options.noticeLines ?? [];
188
470
  this.authOnly = options.authOnly === true;
189
471
  this.keys = createPanelKeys(options.keybindings);
472
+ this.view = new McpPanelView(() => this.getViewState(), createMcpPanelTheme(options.theme));
190
473
  this.prefix = config.settings?.toolPrefix ?? "server";
191
474
 
192
475
  for (const [serverName, definition] of Object.entries(config.mcpServers)) {
@@ -197,10 +480,11 @@ class McpPanel {
197
480
 
198
481
  const globalDirect = config.settings?.directTools;
199
482
  let toolFilter: true | string[] | false = false;
200
- if (definition.directTools !== undefined) {
201
- toolFilter = definition.directTools;
202
- } else if (globalDirect) {
203
- toolFilter = globalDirect;
483
+ const selected = definition.directTools !== undefined ? definition.directTools : globalDirect;
484
+ if (selected === "search") {
485
+ toolFilter = true; // search-activated tools are still direct tools for the panel
486
+ } else if (selected !== undefined) {
487
+ toolFilter = selected;
204
488
  }
205
489
 
206
490
  const tools: ToolState[] = [];
@@ -599,8 +883,8 @@ class McpPanel {
599
883
  this.cache ??= { version: 1, servers: {} };
600
884
  this.cache.servers[server.name] = entry;
601
885
  this.rebuildServerTools(server, entry);
886
+ server.hasCachedData = true;
602
887
  }
603
- server.hasCachedData = true;
604
888
  }
605
889
  if (options.afterAuth) {
606
890
  this.authNotice = connected && server.connectionStatus === "connected"
@@ -770,283 +1054,34 @@ class McpPanel {
770
1054
  this.updateDirty();
771
1055
  }
772
1056
 
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;
1057
+ private getViewState(): McpPanelViewState {
1058
+ return {
1059
+ noticeLines: this.noticeLines,
1060
+ servers: this.servers,
1061
+ visibleItems: this.visibleItems,
1062
+ cursorIndex: this.cursorIndex,
1063
+ nameQuery: this.nameQuery,
1064
+ descSearchActive: this.descSearchActive,
1065
+ descQuery: this.descQuery,
1066
+ dirty: this.dirty,
1067
+ confirmingDiscard: this.confirmingDiscard,
1068
+ discardSelected: this.discardSelected,
1069
+ importNotice: this.importNotice,
1070
+ authNotice: this.authNotice,
1071
+ authInFlight: this.authInFlight,
1072
+ authOnly: this.authOnly,
1073
+ saveLabel: this.keys.saveLabel(),
1001
1074
  };
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
1075
  }
1015
1076
 
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 "";
1077
+ render(width: number): string[] {
1078
+ return this.view.render(width);
1026
1079
  }
1027
1080
 
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}`;
1081
+ invalidate(): void {
1082
+ this.view.invalidate();
1046
1083
  }
1047
1084
 
1048
- invalidate(): void {}
1049
-
1050
1085
  dispose(): void {
1051
1086
  this.cleanup();
1052
1087
  }
@@ -1059,7 +1094,7 @@ export function createMcpPanel(
1059
1094
  callbacks: McpPanelCallbacks,
1060
1095
  tui: { requestRender(): void },
1061
1096
  done: (result: McpPanelResult) => void,
1062
- options?: { noticeLines?: string[]; authOnly?: boolean; keybindings?: PanelKeybindings },
1097
+ options?: { noticeLines?: string[]; authOnly?: boolean; keybindings?: PanelKeybindings; theme?: Theme },
1063
1098
  ): McpPanel & { dispose(): void } {
1064
1099
  return new McpPanel(config, cache, provenance, callbacks, tui, done, options ?? {});
1065
1100
  }