@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.
- package/CHANGELOG.md +18 -0
- package/dist/types/apps/debug/log-viewer.d.ts +17 -0
- package/dist/types/apps/debug/raw-sse.d.ts +14 -0
- package/dist/types/native/backend.d.ts +8 -0
- package/dist/types/native/encode.d.ts +5 -1
- package/dist/types/native/node.d.ts +14 -1
- package/dist/types/native/reconcile.d.ts +5 -0
- package/dist/types/overlays/extensions/extension-dashboard.d.ts +31 -0
- package/dist/types/overlays/extensions/extension-list.d.ts +37 -0
- package/dist/types/overlays/extensions/inspector-panel.d.ts +8 -0
- package/dist/types/overlays/jobs-panel.d.ts +35 -9
- package/dist/types/prompt/word-completion.d.ts +12 -0
- package/dist/types/terminal.d.ts +7 -0
- package/dist/types/tools/bash-interactive.d.ts +15 -0
- package/dist/types/tools/output-meta.d.ts +8 -2
- package/dist/types/tools/streaming-output.d.ts +15 -10
- package/dist/types/tui.d.ts +5 -0
- package/dist/types/windows-input-mode.d.ts +26 -0
- package/package.json +9 -9
- package/src/apps/debug/log-viewer.ts +236 -2
- package/src/apps/debug/protocol-probe.ts +10 -1
- package/src/apps/debug/raw-sse.ts +210 -28
- package/src/native/apply.ts +2 -1
- package/src/native/backend.ts +22 -0
- package/src/native/encode.ts +7 -2
- package/src/native/node.ts +15 -1
- package/src/native/reconcile.ts +28 -1
- package/src/overlays/extensions/extension-dashboard.ts +275 -23
- package/src/overlays/extensions/extension-list.ts +274 -14
- package/src/overlays/extensions/inspector-panel.ts +368 -0
- package/src/overlays/hook-selector.ts +1 -1
- package/src/overlays/jobs-panel.ts +222 -49
- package/src/prompt/composer-cache.ts +26 -3
- package/src/prompt/word-completion.ts +31 -13
- package/src/terminal.ts +37 -1
- package/src/tools/bash-interactive.ts +85 -16
- package/src/tools/output-meta.ts +13 -5
- package/src/tools/streaming-output.ts +48 -23
- package/src/tui.ts +52 -14
- 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
|
-
|
|
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
|
|
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
|
|
900
|
+
this.activateSelected();
|
|
641
901
|
return;
|
|
642
902
|
}
|
|
643
903
|
|