@oh-my-pi/pi-tui 18.4.8 → 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 (40) hide show
  1. package/CHANGELOG.md +18 -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 +8 -0
  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/tools/bash-interactive.d.ts +15 -0
  15. package/dist/types/tools/output-meta.d.ts +8 -2
  16. package/dist/types/tools/streaming-output.d.ts +15 -10
  17. package/dist/types/tui.d.ts +5 -0
  18. package/dist/types/windows-input-mode.d.ts +26 -0
  19. package/package.json +9 -9
  20. package/src/apps/debug/log-viewer.ts +236 -2
  21. package/src/apps/debug/protocol-probe.ts +10 -1
  22. package/src/apps/debug/raw-sse.ts +210 -28
  23. package/src/native/apply.ts +2 -1
  24. package/src/native/backend.ts +22 -0
  25. package/src/native/encode.ts +7 -2
  26. package/src/native/node.ts +15 -1
  27. package/src/native/reconcile.ts +28 -1
  28. package/src/overlays/extensions/extension-dashboard.ts +275 -23
  29. package/src/overlays/extensions/extension-list.ts +274 -14
  30. package/src/overlays/extensions/inspector-panel.ts +368 -0
  31. package/src/overlays/hook-selector.ts +1 -1
  32. package/src/overlays/jobs-panel.ts +222 -49
  33. package/src/prompt/composer-cache.ts +26 -3
  34. package/src/prompt/word-completion.ts +31 -13
  35. package/src/terminal.ts +37 -1
  36. package/src/tools/bash-interactive.ts +85 -16
  37. package/src/tools/output-meta.ts +13 -5
  38. package/src/tools/streaming-output.ts +48 -23
  39. package/src/tui.ts +52 -14
  40. package/src/windows-input-mode.ts +206 -0
@@ -5,7 +5,12 @@
5
5
  * that toggles the entire provider. All items below are dimmed when the
6
6
  * master switch is off.
7
7
  */
8
+ import type { TspPickerGroup, TspPickerItem, TspProps, TspSpan, TspTone } from "@oh-my-pi/pi-wire";
8
9
  import type { Component } from "../../tui";
10
+ import { formatKeyHint } from "../../app-keybindings";
11
+ import { node, span, stableKey } from "../../native/describe";
12
+ import type { NativeNode } from "../../native/node";
13
+ import { pickerFuzzyHits, pickerQuery } from "../../native/picker";
9
14
  import { matchesKey } from "../../keys";
10
15
  import { padding, truncateToWidth, visibleWidth } from "../../utils";
11
16
  import { theme } from "../../theme";
@@ -27,6 +32,7 @@ import {
27
32
  formatMcpListHint,
28
33
  isDiscoveredMcpServer,
29
34
  type MCPConnectionHealth,
35
+ type MCPRuntimeSnapshot,
30
36
  type MCPRuntimeSource,
31
37
  snapshotMcpRuntime,
32
38
  } from "./mcp-runtime";
@@ -67,6 +73,11 @@ function getListItemKey(item: ListItem): string {
67
73
  }
68
74
  }
69
75
 
76
+ /** Native item/scope id of a flattened row: its hashed {@link getListItemKey}, safe inside `/` keypaths. */
77
+ function nativeItemId(item: ListItem): string {
78
+ return stableKey(getListItemKey(item));
79
+ }
80
+
70
81
  /** Searchable text for a flattened row (the menu filter rebuilds via applyFilter; this covers headers/switches). */
71
82
  function getListItemSearchText(item: ListItem): string {
72
83
  switch (item.type) {
@@ -81,6 +92,24 @@ function getListItemSearchText(item: ListItem): string {
81
92
  }
82
93
  }
83
94
 
95
+ /** The inventory as `picker` data: rows, display order with kind groups, the selection and search hits. */
96
+ export interface ExtensionListPickerView {
97
+ readonly items: TspPickerItem[];
98
+ /** Item ids in display order; kind headers become group entries. */
99
+ readonly order: (string | TspPickerGroup)[];
100
+ /** Selected item id; null when nothing or a kind header is selected. */
101
+ readonly selected: string | null;
102
+ readonly hits: Record<string, [number, number][]>;
103
+ /** Whether kind headers group the rows (the unfiltered ALL view). */
104
+ readonly grouped: boolean;
105
+ }
106
+
107
+ /** The selected row's switch: its state, and why it cannot flip when it can't. */
108
+ export interface ExtensionListSwitch {
109
+ readonly on: boolean;
110
+ readonly blocked?: string;
111
+ }
112
+
84
113
  /** Flattened list item for rendering */
85
114
  type ListItem =
86
115
  | { type: "master"; providerId: string; providerName: string; enabled: boolean }
@@ -88,6 +117,14 @@ type ListItem =
88
117
  | { type: "kind-header"; kind: ExtensionKind; label: string; icon: string; count: number }
89
118
  | { type: "extension"; item: Extension };
90
119
 
120
+ /** Picker dot tone of each MCP connection health, matching the ANSI health glyph colours. */
121
+ const MCP_HEALTH_TONE: Record<MCPConnectionHealth, TspTone> = {
122
+ connected: "success",
123
+ connecting: "pending",
124
+ disconnected: "muted",
125
+ inactive: "warning",
126
+ };
127
+
91
128
  export class ExtensionList implements Component {
92
129
  #menu: MenuSelection<ListItem>;
93
130
  #scrollOffset = 0;
@@ -156,6 +193,11 @@ export class ExtensionList implements Component {
156
193
  return this.#menu.query;
157
194
  }
158
195
 
196
+ /** The search field's text and caret for a native `picker` head. */
197
+ nativeQuery(): Pick<TspProps<"picker">, "query" | "cursor"> {
198
+ return pickerQuery(this.#search);
199
+ }
200
+
159
201
  resetSelection(): void {
160
202
  this.#menu.moveToBoundary("first");
161
203
  this.#scrollOffset = 0;
@@ -256,6 +298,232 @@ export class ExtensionList implements Component {
256
298
  return lines;
257
299
  }
258
300
 
301
+ /** The visible rows as `picker` data, joined with live MCP health and tool runtime like {@link render}. */
302
+ pickerView(): ExtensionListPickerView {
303
+ this.#toolFrame = snapshotToolRuntimeSource(this.#toolSource);
304
+ const masterDisabled = this.#isMasterDisabled();
305
+ const query = this.#menu.query;
306
+ const items: TspPickerItem[] = [];
307
+ const order: (string | TspPickerGroup)[] = [];
308
+ const hits: Record<string, [number, number][]> = {};
309
+ let grouped = false;
310
+ for (const entry of this.#menu.visibleItems) {
311
+ if (entry.type === "kind-header") {
312
+ grouped = true;
313
+ order.push({ group: nativeItemId(entry), label: entry.label, count: entry.count });
314
+ continue;
315
+ }
316
+ const item = this.#pickerItem(entry, masterDisabled);
317
+ items.push(item);
318
+ order.push(item.id);
319
+ if (query && typeof item.label === "string") {
320
+ const marked = pickerFuzzyHits(item.label, query);
321
+ if (marked) hits[item.id] = marked;
322
+ }
323
+ }
324
+ const selected = this.#menu.selectedItem;
325
+ return {
326
+ items,
327
+ order,
328
+ selected: selected && selected.type !== "kind-header" ? nativeItemId(selected) : null,
329
+ hits,
330
+ grouped,
331
+ };
332
+ }
333
+
334
+ /**
335
+ * The visible rows as a native `list` (kind headers included, so the
336
+ * selection matches the keyboard's), for terminals without `picker`.
337
+ */
338
+ describeList(key: string): NativeNode {
339
+ this.#toolFrame = snapshotToolRuntimeSource(this.#toolSource);
340
+ const masterDisabled = this.#isMasterDisabled();
341
+ const rows: NativeNode[] = [];
342
+ for (const entry of this.#menu.visibleItems) {
343
+ if (entry.type === "kind-header") {
344
+ rows.push(
345
+ node(
346
+ "item",
347
+ {
348
+ label: [span(entry.label, "muted strong")],
349
+ value: [span(String(entry.count), "dim")],
350
+ },
351
+ undefined,
352
+ nativeItemId(entry),
353
+ ),
354
+ );
355
+ continue;
356
+ }
357
+ const item = this.#pickerItem(entry, masterDisabled);
358
+ const label: TspSpan[] = [
359
+ span(`${this.#nativeGlyph(item.dot)} `, item.dot ?? "dim"),
360
+ span(typeof item.label === "string" ? item.label : "", item.tone === "muted" ? "dim" : "strong"),
361
+ ];
362
+ for (const badge of item.badges ?? []) label.push(span(` ${badge.text}`, badge.tone ?? "muted"));
363
+ rows.push(
364
+ node(
365
+ "item",
366
+ {
367
+ label,
368
+ detail: typeof item.detail === "string" ? [span(item.detail, "dim")] : item.detail,
369
+ disabled: item.disabled !== undefined,
370
+ },
371
+ undefined,
372
+ item.id,
373
+ ),
374
+ );
375
+ }
376
+ const selected = this.#menu.selectedItem;
377
+ return node(
378
+ "list",
379
+ {
380
+ selected: selected ? nativeItemId(selected) : null,
381
+ empty: "No extensions found for this provider.",
382
+ virtual: true,
383
+ grow: 1,
384
+ basis: 0,
385
+ actions: { click: "select", dblclick: "activate" },
386
+ aria: "Extensions",
387
+ },
388
+ rows,
389
+ key,
390
+ );
391
+ }
392
+
393
+ /**
394
+ * A pointer pick of the row with native id `id`: select it, then toggle it
395
+ * when `activate` (the click-on-selected / double-click path). Returns
396
+ * whether the id named a visible row.
397
+ */
398
+ pickNative(id: string, activate: boolean): boolean {
399
+ const index = this.#menu.visibleItems.findIndex(item => nativeItemId(item) === id);
400
+ if (index < 0) return false;
401
+ if (index !== this.#menu.selectedIndex) {
402
+ this.#menu.setSelectedIndex(index);
403
+ this.#syncScroll();
404
+ this.#notifySelectionChange();
405
+ }
406
+ if (activate) this.activateSelected();
407
+ return true;
408
+ }
409
+
410
+ /** The selected row's switch, or undefined on a kind header or an empty list. */
411
+ selectedSwitch(): ExtensionListSwitch | undefined {
412
+ const item = this.#menu.selectedItem;
413
+ if (!item || item.type === "kind-header") return undefined;
414
+ if (item.type === "master") return { on: item.enabled };
415
+ if (item.type === "user-source") {
416
+ const providerOff =
417
+ this.#callbacks.getProviders?.().find(provider => provider.id === item.providerId)?.enabled === false;
418
+ return providerOff ? { on: item.enabled, blocked: "Provider disabled" } : { on: item.enabled };
419
+ }
420
+ const on = item.item.state !== "disabled";
421
+ if (isShadowedExtension(item.item)) return { on, blocked: "Shadowed by another source" };
422
+ if (this.#isMasterDisabled()) return { on, blocked: "Provider disabled" };
423
+ return { on };
424
+ }
425
+
426
+ #isMasterDisabled(): boolean {
427
+ return (
428
+ this.#masterSwitchProvider !== null &&
429
+ this.#callbacks.getProviders?.().find(provider => provider.id === this.#masterSwitchProvider)?.enabled ===
430
+ false
431
+ );
432
+ }
433
+
434
+ /** A text status glyph for the `list` fallback, matching the ANSI row icons. */
435
+ #nativeGlyph(dot: TspTone | undefined): string {
436
+ switch (dot) {
437
+ case "success":
438
+ return theme.status.enabled;
439
+ case "warning":
440
+ return theme.status.shadowed;
441
+ case "pending":
442
+ return theme.status.running;
443
+ default:
444
+ return theme.status.disabled;
445
+ }
446
+ }
447
+
448
+ #pickerItem(entry: Exclude<ListItem, { type: "kind-header" }>, masterDisabled: boolean): TspPickerItem {
449
+ const id = nativeItemId(entry);
450
+ if (entry.type === "master") {
451
+ return {
452
+ id,
453
+ label: `Enable ${sanitizeDisplayLine(entry.providerName)}`,
454
+ icon: "power",
455
+ dot: entry.enabled ? "success" : "muted",
456
+ badges: [{ text: "Master Switch", tone: "warning" }],
457
+ tone: entry.enabled ? undefined : "muted",
458
+ };
459
+ }
460
+ if (entry.type === "user-source") {
461
+ const off = !entry.enabled || masterDisabled;
462
+ return {
463
+ id,
464
+ label: `Load ~/ ${sanitizeDisplayLine(entry.providerName)} config`,
465
+ icon: "folder",
466
+ detail: "opt-in; project config always loads",
467
+ dot: entry.enabled ? "success" : "muted",
468
+ tone: off ? "muted" : undefined,
469
+ disabled: masterDisabled ? "Provider disabled" : undefined,
470
+ };
471
+ }
472
+ const ext = entry.item;
473
+ const shadowed = isShadowedExtension(ext);
474
+ const effectivelyDisabled = masterDisabled || ext.state === "disabled";
475
+ const mcpSnap = this.#mcpSnapshot(ext, shadowed, effectivelyDisabled);
476
+ const hint = this.#extensionHint(ext, mcpSnap);
477
+ const dot: TspTone = masterDisabled
478
+ ? "muted"
479
+ : shadowed
480
+ ? "warning"
481
+ : mcpSnap
482
+ ? MCP_HEALTH_TONE[mcpSnap.health]
483
+ : ext.state === "active"
484
+ ? "success"
485
+ : "muted";
486
+ return {
487
+ id,
488
+ label: sanitizeDisplayLine(ext.displayName),
489
+ detail: hint ? sanitizeDisplayLine(hint) : undefined,
490
+ dot,
491
+ tone: effectivelyDisabled ? "muted" : shadowed ? "warning" : undefined,
492
+ badges: shadowed
493
+ ? [
494
+ {
495
+ text: "shadowed",
496
+ tone: "warning",
497
+ title: ext.shadowedBy ? `Shadowed by ${sanitizeDisplayLine(ext.shadowedBy)}` : undefined,
498
+ },
499
+ ]
500
+ : undefined,
501
+ facts: { kind: this.#getKindLabel(ext.kind) },
502
+ disabled: effectivelyDisabled
503
+ ? masterDisabled
504
+ ? "Provider disabled"
505
+ : `Disabled · ${formatKeyHint("space")} to enable`
506
+ : undefined,
507
+ };
508
+ }
509
+
510
+ /** Live MCP health for an unshadowed discovered server row. */
511
+ #mcpSnapshot(ext: Extension, shadowed: boolean, effectivelyDisabled: boolean): MCPRuntimeSnapshot | undefined {
512
+ return ext.kind === "mcp" && isDiscoveredMcpServer(ext.raw) && !shadowed
513
+ ? snapshotMcpRuntime(ext.raw, this.#mcpSource, {
514
+ enabled: !effectivelyDisabled,
515
+ shadowed: false,
516
+ })
517
+ : undefined;
518
+ }
519
+
520
+ /** The trailing row hint: MCP health/project, or the kind's own list hint. */
521
+ #extensionHint(ext: Extension, mcpSnap: MCPRuntimeSnapshot | undefined): string | undefined {
522
+ return mcpSnap
523
+ ? joinListHints(formatMcpListHint(mcpSnap), projectListHint(ext))
524
+ : formatExtensionListHint(ext, ext.kind === "tool" ? liveToolsForExtension(ext, this.#toolFrame) : []);
525
+ }
526
+
259
527
  #renderUserSourceSwitch(
260
528
  item: ListItem & { type: "user-source" },
261
529
  isSelected: boolean,
@@ -317,13 +585,7 @@ export class ExtensionList implements Component {
317
585
  #renderExtensionRow(ext: Extension, isSelected: boolean, width: number, masterDisabled: boolean): string {
318
586
  const shadowed = isShadowedExtension(ext);
319
587
  const effectivelyDisabled = masterDisabled || ext.state === "disabled";
320
- const mcpSnap =
321
- ext.kind === "mcp" && isDiscoveredMcpServer(ext.raw) && !shadowed
322
- ? snapshotMcpRuntime(ext.raw, this.#mcpSource, {
323
- enabled: !effectivelyDisabled,
324
- shadowed: false,
325
- })
326
- : undefined;
588
+ const mcpSnap = this.#mcpSnapshot(ext, shadowed, effectivelyDisabled);
327
589
 
328
590
  const stateIcon = shadowed
329
591
  ? this.#getStateIcon("shadowed", masterDisabled)
@@ -348,9 +610,7 @@ export class ExtensionList implements Component {
348
610
  const namePadded = this.#padText(name, nameWidth);
349
611
  line += namePadded;
350
612
 
351
- const hint = mcpSnap
352
- ? joinListHints(formatMcpListHint(mcpSnap), projectListHint(ext))
353
- : formatExtensionListHint(ext, ext.kind === "tool" ? liveToolsForExtension(ext, this.#toolFrame) : []);
613
+ const hint = this.#extensionHint(ext, mcpSnap);
354
614
  if (hint) {
355
615
  const triggerStyle = effectivelyDisabled
356
616
  ? "dim"
@@ -562,8 +822,8 @@ export class ExtensionList implements Component {
562
822
  );
563
823
  }
564
824
 
565
- /** Toggle the selected item, or flip the provider master switch when on it. */
566
- #activateSelected(): void {
825
+ /** Toggle the selected item, or flip the provider master switch when on it (Space/Enter). */
826
+ activateSelected(): void {
567
827
  const item = this.#menu.selectedItem;
568
828
  if (item?.type === "master") {
569
829
  this.#callbacks.onMasterToggle?.(item.providerId);
@@ -614,7 +874,7 @@ export class ExtensionList implements Component {
614
874
  const index = this.hitTest(line);
615
875
  if (index === null) return;
616
876
  if (index === this.#menu.selectedIndex) {
617
- this.#activateSelected();
877
+ this.activateSelected();
618
878
  return;
619
879
  }
620
880
  this.#menu.setSelectedIndex(index);
@@ -637,7 +897,7 @@ export class ExtensionList implements Component {
637
897
 
638
898
  // Space or Enter: activate the selected row (toggle item / master switch)
639
899
  if (data === " " || matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
640
- this.#activateSelected();
900
+ this.activateSelected();
641
901
  return;
642
902
  }
643
903