@oh-my-pi/pi-tui 18.4.6 → 18.4.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 (147) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/types/apps/debug/log-viewer.d.ts +17 -0
  3. package/dist/types/apps/debug/raw-sse.d.ts +14 -0
  4. package/dist/types/native/backend.d.ts +11 -2
  5. package/dist/types/native/encode.d.ts +5 -1
  6. package/dist/types/native/node.d.ts +14 -1
  7. package/dist/types/native/reconcile.d.ts +5 -0
  8. package/dist/types/overlays/extensions/extension-dashboard.d.ts +31 -0
  9. package/dist/types/overlays/extensions/extension-list.d.ts +37 -0
  10. package/dist/types/overlays/extensions/inspector-panel.d.ts +8 -0
  11. package/dist/types/overlays/jobs-panel.d.ts +35 -9
  12. package/dist/types/prompt/word-completion.d.ts +12 -0
  13. package/dist/types/terminal.d.ts +7 -0
  14. package/dist/types/theme/defaults/index.d.ts +700 -0
  15. package/dist/types/theme/schema.d.ts +18 -0
  16. package/dist/types/tools/bash-interactive.d.ts +15 -0
  17. package/dist/types/tools/output-meta.d.ts +8 -2
  18. package/dist/types/tools/streaming-output.d.ts +15 -10
  19. package/dist/types/tui.d.ts +5 -0
  20. package/dist/types/windows-input-mode.d.ts +26 -0
  21. package/package.json +9 -9
  22. package/src/apps/debug/log-viewer.ts +236 -2
  23. package/src/apps/debug/protocol-probe.ts +10 -1
  24. package/src/apps/debug/raw-sse.ts +210 -28
  25. package/src/native/apply.ts +2 -1
  26. package/src/native/backend.ts +66 -7
  27. package/src/native/encode.ts +7 -2
  28. package/src/native/node.ts +15 -1
  29. package/src/native/reconcile.ts +28 -1
  30. package/src/overlays/extensions/extension-dashboard.ts +275 -23
  31. package/src/overlays/extensions/extension-list.ts +274 -14
  32. package/src/overlays/extensions/inspector-panel.ts +368 -0
  33. package/src/overlays/hook-selector.ts +1 -1
  34. package/src/overlays/jobs-panel.ts +222 -49
  35. package/src/prompt/composer-cache.ts +26 -3
  36. package/src/prompt/word-completion.ts +31 -13
  37. package/src/terminal.ts +37 -1
  38. package/src/theme/dark.json +10 -0
  39. package/src/theme/defaults/alabaster.json +10 -0
  40. package/src/theme/defaults/amethyst.json +10 -0
  41. package/src/theme/defaults/anthracite.json +10 -0
  42. package/src/theme/defaults/basalt.json +10 -0
  43. package/src/theme/defaults/birch.json +10 -0
  44. package/src/theme/defaults/dark-abyss.json +10 -0
  45. package/src/theme/defaults/dark-arctic.json +10 -0
  46. package/src/theme/defaults/dark-aurora.json +10 -0
  47. package/src/theme/defaults/dark-catppuccin.json +10 -0
  48. package/src/theme/defaults/dark-cavern.json +10 -0
  49. package/src/theme/defaults/dark-celestial.json +10 -0
  50. package/src/theme/defaults/dark-copper.json +10 -0
  51. package/src/theme/defaults/dark-cosmos.json +10 -0
  52. package/src/theme/defaults/dark-cyberpunk.json +10 -0
  53. package/src/theme/defaults/dark-dracula.json +10 -0
  54. package/src/theme/defaults/dark-eclipse.json +10 -0
  55. package/src/theme/defaults/dark-ember.json +10 -0
  56. package/src/theme/defaults/dark-equinox.json +10 -0
  57. package/src/theme/defaults/dark-forest.json +10 -0
  58. package/src/theme/defaults/dark-github.json +10 -0
  59. package/src/theme/defaults/dark-gruvbox.json +10 -0
  60. package/src/theme/defaults/dark-lavender.json +10 -0
  61. package/src/theme/defaults/dark-lunar.json +10 -0
  62. package/src/theme/defaults/dark-midnight.json +10 -0
  63. package/src/theme/defaults/dark-monochrome.json +10 -0
  64. package/src/theme/defaults/dark-monokai.json +10 -0
  65. package/src/theme/defaults/dark-nebula.json +10 -0
  66. package/src/theme/defaults/dark-neon-noir.json +10 -0
  67. package/src/theme/defaults/dark-nord.json +10 -0
  68. package/src/theme/defaults/dark-ocean.json +10 -0
  69. package/src/theme/defaults/dark-one.json +10 -0
  70. package/src/theme/defaults/dark-poimandres.json +10 -0
  71. package/src/theme/defaults/dark-rainforest.json +10 -0
  72. package/src/theme/defaults/dark-reef.json +10 -0
  73. package/src/theme/defaults/dark-retro.json +10 -0
  74. package/src/theme/defaults/dark-rose-pine.json +10 -0
  75. package/src/theme/defaults/dark-sakura.json +10 -0
  76. package/src/theme/defaults/dark-slate.json +10 -0
  77. package/src/theme/defaults/dark-solarized.json +10 -0
  78. package/src/theme/defaults/dark-solstice.json +10 -0
  79. package/src/theme/defaults/dark-starfall.json +10 -0
  80. package/src/theme/defaults/dark-sunset.json +10 -0
  81. package/src/theme/defaults/dark-swamp.json +10 -0
  82. package/src/theme/defaults/dark-synthwave.json +10 -0
  83. package/src/theme/defaults/dark-taiga.json +10 -0
  84. package/src/theme/defaults/dark-terminal.json +10 -0
  85. package/src/theme/defaults/dark-tokyo-night.json +10 -0
  86. package/src/theme/defaults/dark-tundra.json +10 -0
  87. package/src/theme/defaults/dark-twilight.json +10 -0
  88. package/src/theme/defaults/dark-volcanic.json +10 -0
  89. package/src/theme/defaults/graphite.json +10 -0
  90. package/src/theme/defaults/light-arctic.json +10 -0
  91. package/src/theme/defaults/light-aurora-day.json +10 -0
  92. package/src/theme/defaults/light-canyon.json +10 -0
  93. package/src/theme/defaults/light-catppuccin.json +10 -0
  94. package/src/theme/defaults/light-cirrus.json +10 -0
  95. package/src/theme/defaults/light-coral.json +10 -0
  96. package/src/theme/defaults/light-cyberpunk.json +10 -0
  97. package/src/theme/defaults/light-dawn.json +10 -0
  98. package/src/theme/defaults/light-dunes.json +10 -0
  99. package/src/theme/defaults/light-eucalyptus.json +10 -0
  100. package/src/theme/defaults/light-forest.json +10 -0
  101. package/src/theme/defaults/light-frost.json +10 -0
  102. package/src/theme/defaults/light-github.json +10 -0
  103. package/src/theme/defaults/light-glacier.json +10 -0
  104. package/src/theme/defaults/light-gruvbox.json +10 -0
  105. package/src/theme/defaults/light-haze.json +10 -0
  106. package/src/theme/defaults/light-honeycomb.json +10 -0
  107. package/src/theme/defaults/light-lagoon.json +10 -0
  108. package/src/theme/defaults/light-lavender.json +10 -0
  109. package/src/theme/defaults/light-meadow.json +10 -0
  110. package/src/theme/defaults/light-mint.json +10 -0
  111. package/src/theme/defaults/light-monochrome.json +10 -0
  112. package/src/theme/defaults/light-ocean.json +10 -0
  113. package/src/theme/defaults/light-one.json +10 -0
  114. package/src/theme/defaults/light-opal.json +10 -0
  115. package/src/theme/defaults/light-orchard.json +10 -0
  116. package/src/theme/defaults/light-paper.json +10 -0
  117. package/src/theme/defaults/light-poimandres.json +10 -0
  118. package/src/theme/defaults/light-prism.json +10 -0
  119. package/src/theme/defaults/light-retro.json +10 -0
  120. package/src/theme/defaults/light-sand.json +10 -0
  121. package/src/theme/defaults/light-savanna.json +10 -0
  122. package/src/theme/defaults/light-solarized.json +10 -0
  123. package/src/theme/defaults/light-soleil.json +10 -0
  124. package/src/theme/defaults/light-sunset.json +10 -0
  125. package/src/theme/defaults/light-synthwave.json +10 -0
  126. package/src/theme/defaults/light-tokyo-night.json +10 -0
  127. package/src/theme/defaults/light-wetland.json +10 -0
  128. package/src/theme/defaults/light-zenith.json +10 -0
  129. package/src/theme/defaults/limestone.json +10 -0
  130. package/src/theme/defaults/mahogany.json +10 -0
  131. package/src/theme/defaults/marble.json +10 -0
  132. package/src/theme/defaults/obsidian.json +10 -0
  133. package/src/theme/defaults/onyx.json +10 -0
  134. package/src/theme/defaults/pearl.json +10 -0
  135. package/src/theme/defaults/porcelain.json +10 -0
  136. package/src/theme/defaults/quartz.json +10 -0
  137. package/src/theme/defaults/sandstone.json +10 -0
  138. package/src/theme/defaults/titanium.json +10 -0
  139. package/src/theme/light.json +10 -0
  140. package/src/theme/schema-validation.ts +7 -0
  141. package/src/theme/schema.ts +18 -0
  142. package/src/theme/theme-schema.json +30 -0
  143. package/src/tools/bash-interactive.ts +85 -16
  144. package/src/tools/output-meta.ts +13 -5
  145. package/src/tools/streaming-output.ts +48 -23
  146. package/src/tui.ts +56 -15
  147. package/src/windows-input-mode.ts +206 -0
@@ -6,7 +6,10 @@
6
6
  * kind-specific surface → contents → boring config.
7
7
  */
8
8
  import * as os from "node:os";
9
+ import type { TspSpan, TspText } from "@oh-my-pi/pi-wire";
9
10
  import type { Component } from "../../tui";
11
+ import { code, col, compact, keyed, kv, node, span, stableKey, text } from "../../native/describe";
12
+ import type { NativeChild, NativeNode } from "../../native/node";
10
13
  import { visibleWidth, wrapTextWithAnsi } from "../../utils";
11
14
  import { KeyValueList } from "../../components/key-value-list";
12
15
  import { Section } from "../../components/section";
@@ -41,6 +44,7 @@ import {
41
44
  formatMcpHealthLabel,
42
45
  isDiscoveredMcpServer,
43
46
  type MCPConnectionHealth,
47
+ type MCPRuntimeCatalogItem,
44
48
  type MCPRuntimeSource,
45
49
  snapshotMcpRuntime,
46
50
  visibleMcpTools,
@@ -62,6 +66,27 @@ interface KindView {
62
66
  config: string[];
63
67
  }
64
68
 
69
+ /** {@link KindView} as native nodes: the same grammar, semantic instead of pre-styled lines. */
70
+ interface NativeKindView {
71
+ title?: string;
72
+ description?: string;
73
+ guidance?: string;
74
+ /** Runtime facts ahead of the enablement status (MCP health, server, website, skill visibility). */
75
+ runtime: [string, TspText | undefined][];
76
+ /** The runtime facts carry a health line: the status shows only when not active (the ANSI `runtimeLine`). */
77
+ health?: boolean;
78
+ sections: NativeNode[];
79
+ preview?: { heading: string; text: string };
80
+ config: [string, TspText | undefined][];
81
+ }
82
+
83
+ const MCP_HEALTH_STYLE: Record<MCPConnectionHealth, string> = {
84
+ connected: "success",
85
+ connecting: "muted",
86
+ disconnected: "dim",
87
+ inactive: "warning",
88
+ };
89
+
65
90
  const PREVIEW_LINE_BUDGET = PREVIEW_LIMITS.EXPANDED_LINES;
66
91
  const MCP_TOOL_BUDGET = PREVIEW_LIMITS.COLLAPSED_ITEMS;
67
92
  const MCP_INLINE_ARG_LIMIT = 3;
@@ -124,6 +149,341 @@ export class InspectorPanel implements Component {
124
149
  return this.#renderExtension(this.#extension, width);
125
150
  }
126
151
 
152
+ /**
153
+ * The selected extension as native preview nodes, in the same order as
154
+ * {@link render}: identity, runtime/enablement and origin facts,
155
+ * description, guidance, kind sections, content preview, config. The
156
+ * collapsed budgets follow {@link toggleExpanded} like the ANSI view.
157
+ */
158
+ describePreview(): NativeNode[] {
159
+ const ext = this.#extension;
160
+ if (!ext) {
161
+ return [text([span("Select an extension to view details", "muted")], { wrap: "word" })];
162
+ }
163
+ this.#toolFrame = snapshotToolRuntimeSource(this.#toolSource);
164
+ const view = this.#nativeKindView(ext);
165
+ const name = sanitizeDisplayLine(ext.displayName);
166
+ const title = sanitizeDisplayLineField(view.title);
167
+ const levelLabel = ext.source.level === "user" ? "User" : ext.source.level === "project" ? "Project" : "Native";
168
+ const filePath = sanitizeDisplayText(ext.path);
169
+ const facts = kv([
170
+ ...view.runtime,
171
+ ["Status", view.health && ext.state === "active" ? undefined : this.#nativeStatus(ext)],
172
+ ["Origin", `via ${sanitizeDisplayLine(ext.source.providerName)} (${levelLabel})`],
173
+ [
174
+ "Path",
175
+ [
176
+ span(
177
+ sanitizeDisplayText(shortenPath(ext.path, os.homedir())),
178
+ "path dim",
179
+ filePath.startsWith("/") ? { href: `file://${filePath}` } : undefined,
180
+ ),
181
+ ],
182
+ ],
183
+ ]);
184
+ const out: NativeNode[] = compact<NativeNode>([
185
+ node("text", { text: name, role: "omp.picker.title" }, undefined, "title"),
186
+ title && title !== name ? node("text", { spans: [span(title, "muted")] }, undefined, "subtitle") : undefined,
187
+ facts && keyed(facts, "facts"),
188
+ this.#nativeShortText(view.description, "description"),
189
+ this.#nativeShortText(view.guidance, "guidance"),
190
+ ...view.sections,
191
+ ]);
192
+ if (view.preview) out.push(this.#nativePreviewSection(view.preview.heading, view.preview.text));
193
+ const config = kv(view.config);
194
+ if (config) out.push(keyed(config, "config"));
195
+ return out;
196
+ }
197
+
198
+ #nativeStatus(ext: Extension): TspSpan[] {
199
+ switch (ext.state) {
200
+ case "active":
201
+ return [span(`${theme.status.enabled} ${enablementLabel(ext.state)}`, "success")];
202
+ case "disabled":
203
+ return [span(`${theme.status.disabled} ${enablementLabel(ext.state, ext.disabledReason)}`, "dim")];
204
+ case "shadowed":
205
+ return [
206
+ span(
207
+ `${theme.status.shadowed} ${enablementLabel(ext.state, ext.disabledReason, ext.shadowedBy)}`,
208
+ "warning",
209
+ ),
210
+ ];
211
+ }
212
+ }
213
+
214
+ /** Description/guidance prose, clamped to the inline budget until expanded. */
215
+ #nativeShortText(value: string | undefined, key: string): NativeNode | undefined {
216
+ const cleaned = sanitizeDisplayField(value);
217
+ if (!cleaned) return undefined;
218
+ return node(
219
+ "text",
220
+ {
221
+ text: sanitizeDisplayText(cleaned),
222
+ wrap: "word",
223
+ lines: this.#expanded ? undefined : MCP_INLINE_DESC_LINES,
224
+ },
225
+ undefined,
226
+ key,
227
+ );
228
+ }
229
+
230
+ #nativeSection(head: string, children: readonly NativeChild[]): NativeNode {
231
+ return node("section", { head: [span(head, "muted")] }, children, stableKey(`section:${head}`));
232
+ }
233
+
234
+ /** File content as markdown source, cut to the preview budget until expanded. */
235
+ #nativePreviewSection(heading: string, raw: string): NativeNode {
236
+ if (!raw) return this.#nativeSection(heading, [text([span("(empty)", "dim")])]);
237
+ const lines = sanitizeDisplayText(raw).split("\n");
238
+ const cut = !this.#expanded && lines.length > PREVIEW_LINE_BUDGET;
239
+ const shown = cut ? lines.slice(0, PREVIEW_LINE_BUDGET - 1) : lines;
240
+ const children: NativeChild[] = [code(shown.join("\n"), { lang: "md", wrap: true })];
241
+ if (cut) children.push(this.#nativeMore(lines.length - shown.length, "lines"));
242
+ return this.#nativeSection(heading, children);
243
+ }
244
+
245
+ #nativeMore(hidden: number, noun: string): NativeNode {
246
+ return text([span(`… ${hidden} more ${noun} (${expandKeyHint()} to expand)`, "dim")]);
247
+ }
248
+
249
+ /** Tool/argument rows: name, type and required flag, then the description. */
250
+ #nativeParams(params: ToolParamView[]): NativeNode {
251
+ if (params.length === 0) return text([span("(no arguments)", "dim")]);
252
+ return node("kv", {
253
+ items: params.map(param => ({
254
+ k: [span(param.name, "accent mono")],
255
+ v: compact<TspSpan>([
256
+ span(param.type, "muted"),
257
+ span(` ${param.flag}`, param.required ? "warning" : "dim"),
258
+ param.description ? span(` ${sanitizeDisplayLine(param.description)}`) : undefined,
259
+ ]),
260
+ })),
261
+ });
262
+ }
263
+
264
+ /** One catalog entry (MCP tool or factory tool): name, title, description, arguments. */
265
+ #nativeCatalogEntry(
266
+ name: string,
267
+ title: string | undefined,
268
+ description: string | undefined,
269
+ parameters: unknown,
270
+ ): NativeNode {
271
+ const params = toolParamsFromSchema(parameters);
272
+ const inline = this.#expanded || params.length <= MCP_INLINE_ARG_LIMIT;
273
+ return col(
274
+ compact<NativeChild>([
275
+ text(
276
+ compact<TspSpan>([
277
+ span(sanitizeDisplayLine(name), "accent mono"),
278
+ title && title !== name ? span(` ${sanitizeDisplayLine(title)}`, "muted") : undefined,
279
+ ]),
280
+ ),
281
+ description ? text(sanitizeDisplayText(description), { wrap: "word" }) : undefined,
282
+ inline
283
+ ? this.#nativeParams(params)
284
+ : text([span(`${params.length} args (${expandKeyHint()} to expand)`, "dim")]),
285
+ ]),
286
+ { gap: "xs" },
287
+ );
288
+ }
289
+
290
+ /** A name-only catalog (MCP resources/prompts) under its budget until expanded. */
291
+ #nativeNameList(head: string, entries: MCPRuntimeCatalogItem[]): NativeNode {
292
+ const { shown, hidden } = visibleMcpTools(entries, this.#expanded ? entries.length : MCP_TOOL_BUDGET);
293
+ const children: NativeChild[] = shown.map(entry => text([span(entry.name, "accent mono")]));
294
+ if (hidden > 0) children.push(this.#nativeMore(hidden, ""));
295
+ return this.#nativeSection(head, children);
296
+ }
297
+
298
+ #nativeKindView(ext: Extension): NativeKindView {
299
+ const base: NativeKindView = { runtime: [], sections: [], config: [] };
300
+ switch (ext.kind) {
301
+ case "mcp":
302
+ return this.#nativeMcpKind(ext);
303
+ case "tool": {
304
+ const lives = liveToolsForExtension(ext, this.#toolFrame);
305
+ const data = toolInspectorData(ext, lives, this.#source);
306
+ if (data.factory.length > 1) {
307
+ return {
308
+ ...base,
309
+ description: data.description,
310
+ sections: [
311
+ this.#nativeSection(
312
+ "Tools",
313
+ data.factory.map(tool =>
314
+ this.#nativeCatalogEntry(tool.name, tool.label, tool.description, tool.parameters),
315
+ ),
316
+ ),
317
+ ],
318
+ };
319
+ }
320
+ if (lives.length === 0 && data.params.length === 0) return { ...base, description: data.description };
321
+ return {
322
+ ...base,
323
+ title: data.label,
324
+ description: data.description,
325
+ sections: [this.#nativeSection("Arguments", [this.#nativeParams(data.params)])],
326
+ };
327
+ }
328
+ case "rule": {
329
+ const data = ruleInspectorData(ext, this.#source);
330
+ const applies = kv([
331
+ ["always", data.alwaysApply ? [span("always", "accent")] : undefined],
332
+ ["globs", data.globs?.join(", ")],
333
+ ["condition", this.#nativeList(data.condition)],
334
+ ["ast", this.#nativeList(data.astCondition)],
335
+ ["scope", this.#nativeList(data.scope)],
336
+ ["agents", this.#nativeList(data.agents)],
337
+ ["interrupt", data.interruptMode ? [span(data.interruptMode, "dim")] : undefined],
338
+ ]);
339
+ return {
340
+ ...base,
341
+ description: data.description,
342
+ sections: [this.#nativeSection("Applies", [applies ?? text([span("(no apply conditions)", "dim")])])],
343
+ preview: { heading: "Rule", text: data.content },
344
+ };
345
+ }
346
+ case "skill": {
347
+ const data = skillInspectorData(ext);
348
+ const surface = kv([
349
+ ["apply", data.alwaysApply ? [span("always apply", "accent")] : undefined],
350
+ ["globs", data.globs?.join(", ")],
351
+ ]);
352
+ return {
353
+ ...base,
354
+ description: data.description,
355
+ runtime: data.hidden
356
+ ? [["Visibility", [span("hidden", "warning"), span(" omitted from the system-prompt skill list")]]]
357
+ : [],
358
+ sections: surface ? [keyed(surface, "surface")] : [],
359
+ preview: { heading: "Instruction", text: data.content },
360
+ };
361
+ }
362
+ case "slash-command": {
363
+ const data = commandInspectorData(ext);
364
+ const invocation: NativeChild[] = [text([span(`/${sanitizeDisplayText(ext.name)}`, "accent mono")])];
365
+ const facts = kv([
366
+ ["hint", data.argumentHint ? [span(sanitizeDisplayText(data.argumentHint), "dim")] : undefined],
367
+ ]);
368
+ if (facts) invocation.push(facts);
369
+ if (data.usesArguments) invocation.push(text([span("accepts $ARGUMENTS", "dim")]));
370
+ return {
371
+ ...base,
372
+ description: data.description,
373
+ sections: [this.#nativeSection("Invocation", invocation)],
374
+ preview: { heading: "Template", text: data.body },
375
+ };
376
+ }
377
+ case "hook": {
378
+ const data = hookInspectorData(ext);
379
+ const facts = kv([
380
+ ["when", data.hookType],
381
+ ["tool", data.tool],
382
+ ]);
383
+ return {
384
+ ...base,
385
+ description: ext.description,
386
+ sections: [this.#nativeSection("Hook", facts ? [facts] : [])],
387
+ };
388
+ }
389
+ case "prompt":
390
+ return {
391
+ ...base,
392
+ description: ext.description,
393
+ preview: { heading: "Prompt", text: promptInspectorData(ext).content },
394
+ };
395
+ case "context-file":
396
+ return {
397
+ ...base,
398
+ description: ext.description,
399
+ preview: { heading: "Preview", text: contextInspectorData(ext).content },
400
+ };
401
+ case "instruction": {
402
+ const data = instructionInspectorData(ext);
403
+ const files = data.applyTo ? kv([["files", data.applyTo]]) : undefined;
404
+ return {
405
+ ...base,
406
+ description: ext.description,
407
+ sections: files ? [this.#nativeSection("Applies", [files])] : [],
408
+ preview: { heading: "Instruction", text: data.content },
409
+ };
410
+ }
411
+ default:
412
+ return {
413
+ ...base,
414
+ description: ext.description,
415
+ sections: ext.trigger
416
+ ? [this.#nativeSection("Trigger", [text([span(sanitizeDisplayText(ext.trigger), "accent mono")])])]
417
+ : [],
418
+ };
419
+ }
420
+ }
421
+
422
+ /** A pattern list for a `kv` value, capped until expanded. */
423
+ #nativeList(items: string[] | undefined): TspSpan[] | undefined {
424
+ if (!items || items.length === 0) return undefined;
425
+ const cap = this.#expanded ? items.length : PREVIEW_LIMITS.COLLAPSED_LINES;
426
+ const shown = items.slice(0, cap).map(item => sanitizeDisplayText(item));
427
+ const hidden = items.length - shown.length;
428
+ const spans: TspSpan[] = [span(shown.join("\n"), "accent")];
429
+ if (hidden > 0) spans.push(span(`\n… ${hidden} more (${expandKeyHint()} to expand)`, "dim"));
430
+ return spans;
431
+ }
432
+
433
+ #nativeMcpKind(ext: Extension): NativeKindView {
434
+ const shadowed = isShadowedExtension(ext);
435
+ if (shadowed) {
436
+ const command =
437
+ isDiscoveredMcpServer(ext.raw) && ext.raw.command
438
+ ? shortenPath(sanitizeDisplayText(ext.raw.command), os.homedir())
439
+ : undefined;
440
+ return { runtime: [], sections: [], config: [["Command", command ? [span(command, "mono")] : undefined]] };
441
+ }
442
+ const snap = isDiscoveredMcpServer(ext.raw)
443
+ ? snapshotMcpRuntime(ext.raw, this.#mcpSource, { enabled: ext.state !== "disabled" })
444
+ : undefined;
445
+ const health: MCPConnectionHealth = snap?.health ?? "disconnected";
446
+ const runtime: [string, TspText | undefined][] = [
447
+ [
448
+ "Health",
449
+ [
450
+ span(`${MCP_HEALTH_GLYPH[health]()} ${formatMcpHealthLabel(health)}`, MCP_HEALTH_STYLE[health]),
451
+ span(` ${snap?.transport ?? "stdio"}`, "muted"),
452
+ ],
453
+ ],
454
+ ];
455
+ if (snap?.implementationName) {
456
+ const version = snap.implementationVersion ? ` ${snap.implementationVersion}` : "";
457
+ runtime.push(["Server", [span(`${snap.implementationName}${version}`, "dim")]]);
458
+ if (snap.websiteUrl) runtime.push(["Website", [span(snap.websiteUrl, "dim", { href: snap.websiteUrl })]]);
459
+ }
460
+ const sections: NativeNode[] = [];
461
+ if (snap && snap.tools.length > 0) {
462
+ const { shown, hidden } = visibleMcpTools(snap.tools, this.#expanded ? snap.tools.length : MCP_TOOL_BUDGET);
463
+ const entries: NativeChild[] = shown.map(tool =>
464
+ this.#nativeCatalogEntry(tool.name, tool.title, tool.description, tool.parameters),
465
+ );
466
+ if (hidden > 0) entries.push(this.#nativeMore(hidden, "tools"));
467
+ sections.push(this.#nativeSection("Tools", entries));
468
+ }
469
+ if (snap && snap.resources.length > 0) sections.push(this.#nativeNameList("Resources", snap.resources));
470
+ if (snap && snap.prompts.length > 0) sections.push(this.#nativeNameList("Prompts", snap.prompts));
471
+ return {
472
+ title: snap?.title,
473
+ description: snap?.description,
474
+ guidance: snap?.instructions,
475
+ runtime,
476
+ health: true,
477
+ sections,
478
+ config: [
479
+ ["Command", snap?.command ? [span(shortenPath(snap.command, os.homedir()), "mono")] : undefined],
480
+ ["URL", snap?.url ? [span(snap.url, "mono", { href: snap.url })] : undefined],
481
+ ["Args", snap?.args && snap.args.length > 0 ? [span(snap.args.join(" "), "mono dim")] : undefined],
482
+ ["Env vars", snap && snap.envCount > 0 ? [span(`${snap.envCount} defined`, "dim")] : undefined],
483
+ ],
484
+ };
485
+ }
486
+
127
487
  #renderExtension(ext: Extension, width: number): string[] {
128
488
  const lines: string[] = [];
129
489
  const kind = this.#kindView(ext);
@@ -663,6 +1023,14 @@ export class InspectorPanel implements Component {
663
1023
  }
664
1024
  }
665
1025
 
1026
+ /** Glyph of each MCP health, read at call time so glyph presets apply. */
1027
+ const MCP_HEALTH_GLYPH: Record<MCPConnectionHealth, () => string> = {
1028
+ connected: () => theme.status.enabled,
1029
+ connecting: () => theme.status.running,
1030
+ disconnected: () => theme.status.shadowed,
1031
+ inactive: () => theme.status.disabled,
1032
+ };
1033
+
666
1034
  function inspectorExtensionKey(extension: Extension | null): string | null {
667
1035
  if (!extension) return null;
668
1036
  return `${extension.kind}:${extension.id}:${extension.path}`;
@@ -707,7 +707,7 @@ export class HookSelectorComponent extends OverlayPanel {
707
707
  const checkbox = this.#selectionMarker === "checkbox";
708
708
  const result = dockedPicker({
709
709
  title: fixed.title,
710
- ...(this.#detailLines.length > 0 ? { subtitle: plainText(this.#detailLines.join(" ")) } : {}),
710
+ ...(this.#detailLines.length > 0 ? { subtitle: plainText(this.#detailLines.join("\n")) } : {}),
711
711
  noun: "options",
712
712
  size: "md",
713
713
  layout: "rows",