@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
package/src/native/encode.ts
CHANGED
|
@@ -106,9 +106,13 @@ export function encodeTspJson(verb: TspVerb, value: unknown, params?: TspParams,
|
|
|
106
106
|
return encodeTspMessage(verb, JSON.stringify(value), params, limit);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* The `hello` query; callers follow it with a DA1 sentinel. `features: ["edit"]`
|
|
111
|
+
* tells the terminal that omp applies its `edit` events (TSP §8.5), so it may keep a
|
|
112
|
+
* native selection in omp's editors; without it, every key stays omp's.
|
|
113
|
+
*/
|
|
110
114
|
export function encodeTspHelloQuery(version?: string): string {
|
|
111
|
-
return encodeTspJson("q", { q: "hello", v: [TSP_VERSION], app: "omp", ver: version });
|
|
115
|
+
return encodeTspJson("q", { q: "hello", v: [TSP_VERSION], app: "omp", features: ["edit"], ver: version });
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
/** One decoded APC message: verb, parameters and raw body. */
|
|
@@ -166,6 +170,7 @@ const EVENT_REQUIRED: Readonly<Record<string, Readonly<Record<string, "string" |
|
|
|
166
170
|
activate: { id: "string", item: "string" },
|
|
167
171
|
action: { id: "string", act: "string" },
|
|
168
172
|
edit: { id: "string", from: "number", to: "number", text: "string", cursor: "number", len: "number" },
|
|
173
|
+
focus: { id: "string" },
|
|
169
174
|
error: { msg: "string" },
|
|
170
175
|
gone: { ids: "array" },
|
|
171
176
|
};
|
package/src/native/node.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* See `packages/wire/src/tsp.ts` for the wire vocabulary and
|
|
14
14
|
* `crates/tern/SURFACE_PROTOCOL.md` (Stencil repository) for the spec.
|
|
15
15
|
*/
|
|
16
|
-
import type { TspEvent, TspKind, TspProps, TspSpan } from "@oh-my-pi/pi-wire";
|
|
16
|
+
import type { TspEvent, TspKind, TspProps, TspScrollBy, TspSpan } from "@oh-my-pi/pi-wire";
|
|
17
17
|
import type { Component } from "../tui";
|
|
18
18
|
|
|
19
19
|
/** A described node: a wire node minus its id, with components allowed as children. */
|
|
@@ -33,9 +33,23 @@ export type NativeNode = {
|
|
|
33
33
|
* added. Key a node by what it points at to reveal it again on a move.
|
|
34
34
|
*/
|
|
35
35
|
readonly reveal?: "start" | "end" | "nearest";
|
|
36
|
+
/**
|
|
37
|
+
* Keyboard scrolling forwarded to the terminal (PgUp/PgDn/End reach the
|
|
38
|
+
* program): moves the scroller at or above the node by `by` whenever
|
|
39
|
+
* `n` differs from the previous description of the same node. Bump `n`
|
|
40
|
+
* per key press; presses between two frames repeat the latest `by` once
|
|
41
|
+
* each (`start`/`end` once). A freshly added node never scrolls.
|
|
42
|
+
*/
|
|
43
|
+
readonly scroll?: NativeScroll;
|
|
36
44
|
};
|
|
37
45
|
}[TspKind];
|
|
38
46
|
|
|
47
|
+
/** A {@link NativeNode.scroll} request. */
|
|
48
|
+
export interface NativeScroll {
|
|
49
|
+
readonly by: TspScrollBy;
|
|
50
|
+
readonly n: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
/** A child slot: a described node, or a component that describes itself. */
|
|
40
54
|
export type NativeChild = NativeNode | Component;
|
|
41
55
|
|
package/src/native/reconcile.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* into `layer`; their anchor keypaths are rewritten to wire ids.
|
|
29
29
|
*/
|
|
30
30
|
import * as logger from "@oh-my-pi/pi-utils/logger";
|
|
31
|
-
import { TSP_TEXT_KINDS, type TspKind, type TspNode, type TspOp } from "@oh-my-pi/pi-wire";
|
|
31
|
+
import { TSP_TEXT_KINDS, type TspKind, type TspNode, type TspOp, type TspScrollBy } from "@oh-my-pi/pi-wire";
|
|
32
32
|
import { type Component, Container, CURSOR_MARKER } from "../tui";
|
|
33
33
|
import { normalizeIconProps } from "./icons";
|
|
34
34
|
import type { DescribeContext, NativeChild, NativeNode } from "./node";
|
|
@@ -58,6 +58,8 @@ const REGION_IDS = ["main", "dock", "layer"] as const;
|
|
|
58
58
|
type RegionId = (typeof REGION_IDS)[number];
|
|
59
59
|
const RESERVED_IDS: ReadonlySet<string> = new Set(REGION_IDS);
|
|
60
60
|
const TEXT_KINDS: ReadonlySet<string> = new Set(TSP_TEXT_KINDS);
|
|
61
|
+
/** Most `scroll` ops one node's coalesced key presses send in a frame. */
|
|
62
|
+
const MAX_SCROLL_REPEAT = 32;
|
|
61
63
|
let nextComponentId = 0;
|
|
62
64
|
|
|
63
65
|
/** Stable base-36 wire id of a component instance (its root node's id). */
|
|
@@ -213,6 +215,8 @@ export class Reconciler {
|
|
|
213
215
|
#settles: string[] = [];
|
|
214
216
|
/** Selected list items and added `reveal` nodes sent this frame, revealed once the frame's adds have landed. */
|
|
215
217
|
#reveals: [id: string, at: "start" | "end" | "nearest"][] = [];
|
|
218
|
+
/** `scroll` requests whose `n` moved this frame, sent after the reveals. */
|
|
219
|
+
#scrolls: [id: string, by: TspScrollBy][] = [];
|
|
216
220
|
/** Wire ids `list.selected` keys resolved to (an unresolved key names no node to reveal). */
|
|
217
221
|
#selectedIds = new Set<string>();
|
|
218
222
|
#moves: PendingMove[] = [];
|
|
@@ -250,6 +254,7 @@ export class Reconciler {
|
|
|
250
254
|
this.#dels = [];
|
|
251
255
|
this.#settles = [];
|
|
252
256
|
this.#reveals = [];
|
|
257
|
+
this.#scrolls = [];
|
|
253
258
|
this.#selectedIds.clear();
|
|
254
259
|
this.#framePortals = [];
|
|
255
260
|
this.#rows = 0;
|
|
@@ -293,6 +298,7 @@ export class Reconciler {
|
|
|
293
298
|
this.#parked = [];
|
|
294
299
|
for (const id of this.#settles) this.#ops.push(["settle", id]);
|
|
295
300
|
for (const [id, at] of this.#reveals) this.#ops.push(["reveal", id, at]);
|
|
301
|
+
if (cx.feature("scroll")) for (const [id, by] of this.#scrolls) this.#ops.push(["scroll", id, by]);
|
|
296
302
|
const ops = this.#ops;
|
|
297
303
|
this.#ops = [];
|
|
298
304
|
return this.#gone.size === 0 ? ops : ops.filter(op => !this.#touchesGone(op));
|
|
@@ -339,6 +345,19 @@ export class Reconciler {
|
|
|
339
345
|
return { component: state.comp, keypath };
|
|
340
346
|
}
|
|
341
347
|
|
|
348
|
+
/**
|
|
349
|
+
* The component that described wire node `id`, then each component whose
|
|
350
|
+
* node contains it, innermost first (empty for an unknown id).
|
|
351
|
+
*/
|
|
352
|
+
owners(id: string): Component[] {
|
|
353
|
+
const dot = id.indexOf(".");
|
|
354
|
+
const out: Component[] = [];
|
|
355
|
+
for (let state = this.#byId.get(dot === -1 ? id : id.slice(0, dot)) ?? null; state; state = state.container) {
|
|
356
|
+
out.push(state.comp);
|
|
357
|
+
}
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
|
|
342
361
|
/** The key a list item node was described with (its key, else its child index). */
|
|
343
362
|
itemKey(id: string): string {
|
|
344
363
|
const dot = id.indexOf(".");
|
|
@@ -583,6 +602,14 @@ export class Reconciler {
|
|
|
583
602
|
this.#flushMoves();
|
|
584
603
|
return;
|
|
585
604
|
}
|
|
605
|
+
const scroll = next.node.scroll;
|
|
606
|
+
if (scroll && scroll.n !== old.node.scroll?.n) {
|
|
607
|
+
// Presses described in one frame coalesce: repeat the latest step once per
|
|
608
|
+
// press (an end once), so key repeat keeps its distance.
|
|
609
|
+
const jump = scroll.by === "start" || scroll.by === "end";
|
|
610
|
+
const presses = jump ? 1 : Math.min(Math.max(scroll.n - (old.node.scroll?.n ?? 0), 1), MAX_SCROLL_REPEAT);
|
|
611
|
+
for (let i = 0; i < presses; i++) this.#scrolls.push([next.id, scroll.by]);
|
|
612
|
+
}
|
|
586
613
|
const oldEntries = this.#entries(old.node.c, old.keypath, old.owner, old.hoist, null);
|
|
587
614
|
const newEntries = this.#entries(next.node.c, next.keypath, next.owner, next.hoist, inner);
|
|
588
615
|
this.#diffProps(
|
|
@@ -12,8 +12,19 @@
|
|
|
12
12
|
* - Space/Enter or click: toggle selected item (or provider master switch)
|
|
13
13
|
* - Wheel over the inspector, or PageUp/PageDown when the inspector overflows: scroll the detail pane
|
|
14
14
|
* - Esc: clear search (if active) then close
|
|
15
|
+
*
|
|
16
|
+
* Natively (Tern) it is a `picker` sheet: provider scopes, the inventory as
|
|
17
|
+
* rows grouped by kind, the inspector as the side preview, and Toggle /
|
|
18
|
+
* Expand / Close actions; without `picker` it describes a page with the same
|
|
19
|
+
* parts. Pointer events run the key paths above.
|
|
15
20
|
*/
|
|
21
|
+
import type { TspPickerAction, TspPickerColumn, TspPickerScope } from "@oh-my-pi/pi-wire";
|
|
16
22
|
import type { Component } from "../../tui";
|
|
23
|
+
import { col, keyed, node, span, text } from "../../native/describe";
|
|
24
|
+
import { Memo } from "../../native/memo";
|
|
25
|
+
import type { DescribeContext, NativeNode, NativeScroll, NativeUiEvent } from "../../native/node";
|
|
26
|
+
import { actionBar, actionButton } from "../../native/overlay";
|
|
27
|
+
import { CLOSE_ACTION, type PickerEvent, picker, pickerAction, pickerEvent } from "../../native/picker";
|
|
17
28
|
import { matchesKey } from "../../keys";
|
|
18
29
|
import { parseSgrMouse } from "../../mouse";
|
|
19
30
|
import { SplitPane, type SplitPaneHit } from "../../components/layout/split-pane";
|
|
@@ -31,9 +42,9 @@ import {
|
|
|
31
42
|
} from "../../keybinding-matchers";
|
|
32
43
|
import { expandKeyHint } from "../../render/render-utils";
|
|
33
44
|
import { formatKeyHint, formatKeyHints } from "../../app-keybindings";
|
|
34
|
-
import { editorKeys, interruptKey } from "../../chrome/keybinding-hints";
|
|
45
|
+
import { boundKeys, editorKeys, interruptKey } from "../../chrome/keybinding-hints";
|
|
35
46
|
import { bottomBorder, divider, PanelRows, row, topBorder } from "../../chrome/overlay-box";
|
|
36
|
-
import { ExtensionList } from "./extension-list";
|
|
47
|
+
import { ExtensionList, type ExtensionListSwitch } from "./extension-list";
|
|
37
48
|
import { InspectorPanel, type ToolRuntimeSource } from "./inspector-panel";
|
|
38
49
|
import type { ExtensionInspectorSource } from "./inspector-model";
|
|
39
50
|
import { snapshotToolRuntimeSource } from "./live-tool-session";
|
|
@@ -99,8 +110,50 @@ export function buildTabBarTabs(tabs: ProviderTab[]): Tab[] {
|
|
|
99
110
|
});
|
|
100
111
|
}
|
|
101
112
|
|
|
113
|
+
/** The sheet/page title, shared by the ANSI frame and the native views. */
|
|
114
|
+
const DASHBOARD_TITLE = "Extension Control Center";
|
|
115
|
+
|
|
116
|
+
/** The kind column shown when rows are not grouped under kind headers (a provider scope or a search). */
|
|
117
|
+
const KIND_COLUMNS: readonly TspPickerColumn[] = [{ id: "kind", format: "dim", priority: 1 }];
|
|
118
|
+
|
|
119
|
+
/** Up to two initials of a provider label, for its scope mark. */
|
|
120
|
+
function providerInitials(label: string): string {
|
|
121
|
+
const words = label.split(/[\s()_-]+/).filter(word => word.length > 0);
|
|
122
|
+
return words
|
|
123
|
+
.slice(0, 2)
|
|
124
|
+
.map(word => word[0]?.toUpperCase() ?? "")
|
|
125
|
+
.join("");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Picker scopes from the provider tabs: "all" first, then each provider with
|
|
130
|
+
* its mark and count. Empty enabled providers are disabled (muted in the
|
|
131
|
+
* {@link TabBar}); switched-off providers keep a muted dot and stay
|
|
132
|
+
* selectable so their master switch can be turned back on.
|
|
133
|
+
*/
|
|
134
|
+
export function buildPickerScopes(tabs: ProviderTab[]): TspPickerScope[] {
|
|
135
|
+
return tabs.map(tab => {
|
|
136
|
+
if (tab.id === "all") return { id: tab.id, label: "All", icon: "extension", count: tab.count };
|
|
137
|
+
const emptyEnabled = tab.count === 0 && tab.enabled;
|
|
138
|
+
return {
|
|
139
|
+
id: tab.id,
|
|
140
|
+
label: tab.label,
|
|
141
|
+
mark: { text: providerInitials(tab.label), seed: tab.id },
|
|
142
|
+
count: tab.count,
|
|
143
|
+
group: "Providers",
|
|
144
|
+
dot: !tab.enabled ? "muted" : emptyEnabled ? "warning" : "success",
|
|
145
|
+
disabled: emptyEnabled ? "No extensions" : undefined,
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
102
150
|
export class ExtensionDashboard implements Component {
|
|
103
151
|
#state!: DashboardState;
|
|
152
|
+
/** Bumped by every state change that reaches the screen; keys {@link #native}. */
|
|
153
|
+
#nativeVersion = 0;
|
|
154
|
+
readonly #native = new Memo<NativeNode>();
|
|
155
|
+
/** The last inspector page key, forwarded to the native preview (see {@link NativeNode.scroll}). */
|
|
156
|
+
#inspectorScroll: NativeScroll | undefined;
|
|
104
157
|
#mainList!: ExtensionList;
|
|
105
158
|
#inspector!: InspectorPanel;
|
|
106
159
|
#tabBar!: TabBar;
|
|
@@ -231,7 +284,7 @@ export class ExtensionDashboard implements Component {
|
|
|
231
284
|
this.#mainList.setToolSource(toolFrame);
|
|
232
285
|
this.#inspector.setToolSource(toolFrame);
|
|
233
286
|
|
|
234
|
-
this.#frameTop.setLines([topBorder(width,
|
|
287
|
+
this.#frameTop.setLines([topBorder(width, DASHBOARD_TITLE)]);
|
|
235
288
|
this.#frameTabs.setLines(tabLines.map(line => row(line, width)));
|
|
236
289
|
this.#frameUpperDivider.setLines([divider(width)]);
|
|
237
290
|
this.#frameBody.setLines(this.#body.render(innerWidth).map(line => row(line, width)));
|
|
@@ -242,6 +295,7 @@ export class ExtensionDashboard implements Component {
|
|
|
242
295
|
}
|
|
243
296
|
|
|
244
297
|
invalidate(): void {
|
|
298
|
+
this.#nativeVersion++;
|
|
245
299
|
this.#frame.invalidate();
|
|
246
300
|
this.#tabBar.invalidate();
|
|
247
301
|
this.#mainList.invalidate();
|
|
@@ -280,10 +334,10 @@ export class ExtensionDashboard implements Component {
|
|
|
280
334
|
if (event.wheel !== null) {
|
|
281
335
|
if (overList) {
|
|
282
336
|
this.#mainList.handleWheel(event.wheel);
|
|
283
|
-
this
|
|
337
|
+
this.#requestRender();
|
|
284
338
|
} else if (overInspector) {
|
|
285
339
|
this.#body.scrollInspector(event.wheel);
|
|
286
|
-
this
|
|
340
|
+
this.#requestRender();
|
|
287
341
|
}
|
|
288
342
|
return;
|
|
289
343
|
}
|
|
@@ -292,7 +346,7 @@ export class ExtensionDashboard implements Component {
|
|
|
292
346
|
const hoveredTab = overTabs ? this.#tabBar.tabAt(tabLine, innerCol) : undefined;
|
|
293
347
|
this.#tabBar.setHoverTab(hoveredTab && !hoveredTab.muted ? hoveredTab.id : null);
|
|
294
348
|
this.#mainList.setHoverIndex(overList ? this.#mainList.hitTest(paneLine) : null);
|
|
295
|
-
this
|
|
349
|
+
this.#requestRender();
|
|
296
350
|
return;
|
|
297
351
|
}
|
|
298
352
|
|
|
@@ -305,7 +359,7 @@ export class ExtensionDashboard implements Component {
|
|
|
305
359
|
}
|
|
306
360
|
if (overList) {
|
|
307
361
|
this.#mainList.handleClick(paneLine);
|
|
308
|
-
this
|
|
362
|
+
this.#requestRender();
|
|
309
363
|
}
|
|
310
364
|
}
|
|
311
365
|
|
|
@@ -329,7 +383,7 @@ export class ExtensionDashboard implements Component {
|
|
|
329
383
|
this.#inspector.setExtension(this.#state.selected);
|
|
330
384
|
}
|
|
331
385
|
this.#body.resetInspectorScroll();
|
|
332
|
-
this
|
|
386
|
+
this.#requestRender();
|
|
333
387
|
}
|
|
334
388
|
|
|
335
389
|
#handleProviderToggle(providerId: string): void {
|
|
@@ -479,7 +533,7 @@ export class ExtensionDashboard implements Component {
|
|
|
479
533
|
}
|
|
480
534
|
|
|
481
535
|
this.#tabBar.setTabs(buildTabBarTabs(this.#state.tabs), currentTabId);
|
|
482
|
-
this
|
|
536
|
+
this.#requestRender();
|
|
483
537
|
}
|
|
484
538
|
|
|
485
539
|
#applyDisabledExtensions(disabledIds: string[]): void {
|
|
@@ -489,7 +543,7 @@ export class ExtensionDashboard implements Component {
|
|
|
489
543
|
this.#inspector.setExtension(this.#state.selected);
|
|
490
544
|
}
|
|
491
545
|
this.#tabBar.setTabs(buildTabBarTabs(this.#state.tabs), this.#state.tabs[this.#state.activeTabIndex]?.id);
|
|
492
|
-
this
|
|
546
|
+
this.#requestRender();
|
|
493
547
|
}
|
|
494
548
|
|
|
495
549
|
handleInput(data: string): void {
|
|
@@ -501,34 +555,32 @@ export class ExtensionDashboard implements Component {
|
|
|
501
555
|
|
|
502
556
|
// Ctrl+C - close immediately
|
|
503
557
|
if (matchesKey(data, "ctrl+c")) {
|
|
504
|
-
this
|
|
505
|
-
this.onClose?.();
|
|
558
|
+
this.#close();
|
|
506
559
|
return;
|
|
507
560
|
}
|
|
508
561
|
|
|
509
562
|
// Escape - clear search first, then close
|
|
510
563
|
if (matchesAppInterrupt(data)) {
|
|
511
564
|
if (this.#state.searchQuery.length > 0) {
|
|
512
|
-
this.#
|
|
513
|
-
this.#state.searchFiltered = this.#state.tabFiltered;
|
|
514
|
-
this.#mainList.setExtensions(this.#state.searchFiltered);
|
|
515
|
-
this.#mainList.clearSearch();
|
|
516
|
-
this.onRequestRender?.();
|
|
565
|
+
this.#clearSearch();
|
|
517
566
|
return;
|
|
518
567
|
}
|
|
519
|
-
this
|
|
520
|
-
this.onClose?.();
|
|
568
|
+
this.#close();
|
|
521
569
|
return;
|
|
522
570
|
}
|
|
523
571
|
|
|
524
572
|
if (matchesAppToolsExpand(data)) {
|
|
525
573
|
this.#inspector.toggleExpanded();
|
|
526
|
-
this
|
|
574
|
+
this.#requestRender();
|
|
527
575
|
return;
|
|
528
576
|
}
|
|
529
577
|
|
|
530
|
-
|
|
531
|
-
|
|
578
|
+
const page = matchesSelectPageUp(data) ? -1 : matchesSelectPageDown(data) ? 1 : 0;
|
|
579
|
+
if (page !== 0) {
|
|
580
|
+
this.#inspectorScroll = { by: page < 0 ? "page-up" : "page-down", n: (this.#inspectorScroll?.n ?? 0) + 1 };
|
|
581
|
+
}
|
|
582
|
+
if (this.#body.pageInspector(page)) {
|
|
583
|
+
this.#requestRender();
|
|
532
584
|
return;
|
|
533
585
|
}
|
|
534
586
|
|
|
@@ -546,16 +598,216 @@ export class ExtensionDashboard implements Component {
|
|
|
546
598
|
this.#state.searchQuery = query;
|
|
547
599
|
this.#state.searchFiltered = applyFilter(this.#state.tabFiltered, query);
|
|
548
600
|
}
|
|
601
|
+
this.#requestRender();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
#close(): void {
|
|
605
|
+
this.dispose();
|
|
606
|
+
this.onClose?.();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
#clearSearch(): void {
|
|
610
|
+
this.#state.searchQuery = "";
|
|
611
|
+
this.#state.searchFiltered = this.#state.tabFiltered;
|
|
612
|
+
this.#mainList.setExtensions(this.#state.searchFiltered);
|
|
613
|
+
this.#mainList.clearSearch();
|
|
614
|
+
this.#requestRender();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Every repaint request: the native description is stale too. */
|
|
618
|
+
#requestRender(): void {
|
|
619
|
+
this.#nativeVersion++;
|
|
549
620
|
this.onRequestRender?.();
|
|
550
621
|
}
|
|
551
622
|
|
|
623
|
+
/** The dashboard is a `picker` sheet (scopes = providers, preview = inspector) wherever Tern draws pickers. */
|
|
624
|
+
nativeSheet(cx: DescribeContext): boolean {
|
|
625
|
+
return cx.supports("picker");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* The picker sheet, or without `picker` a page: head, provider tabs,
|
|
630
|
+
* search, the inventory list beside the inspector, and an action bar.
|
|
631
|
+
* Rebuilt when state moves or the live tool set changes (MCP health
|
|
632
|
+
* changes arrive as render requests, which bump the version).
|
|
633
|
+
*/
|
|
634
|
+
describe(cx: DescribeContext): NativeNode {
|
|
635
|
+
const sheet = this.nativeSheet(cx);
|
|
636
|
+
const toolFrame = snapshotToolRuntimeSource(this.#toolSource);
|
|
637
|
+
let tools = "";
|
|
638
|
+
for (const tool of toolFrame?.listLiveTools?.() ?? []) tools += `${tool.name}${tool.hidden ? "!" : ""}\0`;
|
|
639
|
+
return this.#native.get([this.#nativeVersion, sheet, tools, this.#inspectorScroll], () => {
|
|
640
|
+
this.#mainList.setToolSource(toolFrame);
|
|
641
|
+
this.#inspector.setToolSource(toolFrame);
|
|
642
|
+
return sheet ? this.#describePicker() : this.#describePage();
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
#describePicker(): NativeNode {
|
|
647
|
+
const view = this.#mainList.pickerView();
|
|
648
|
+
const activeTab = this.#state.tabs[this.#state.activeTabIndex];
|
|
649
|
+
return picker(
|
|
650
|
+
{
|
|
651
|
+
title: DASHBOARD_TITLE,
|
|
652
|
+
icon: "extension",
|
|
653
|
+
noun: "extensions",
|
|
654
|
+
size: "lg",
|
|
655
|
+
layout: "rows",
|
|
656
|
+
preview: "side",
|
|
657
|
+
...this.#mainList.nativeQuery(),
|
|
658
|
+
placeholder: "Search extensions…",
|
|
659
|
+
scopes: buildPickerScopes(this.#state.tabs),
|
|
660
|
+
scope: activeTab?.id ?? "all",
|
|
661
|
+
columns: view.grouped ? undefined : KIND_COLUMNS,
|
|
662
|
+
items: view.items,
|
|
663
|
+
order: view.order,
|
|
664
|
+
hits: view.hits,
|
|
665
|
+
selected: view.selected,
|
|
666
|
+
total: this.#state.tabFiltered.length,
|
|
667
|
+
empty: "No extensions found for this provider.",
|
|
668
|
+
actions: this.#pickerActions(this.#mainList.selectedSwitch()),
|
|
669
|
+
focus: "list",
|
|
670
|
+
},
|
|
671
|
+
this.#inspectorPreview(),
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** The inspector's preview nodes, the first carrying the page-key scroll so Tern scrolls the pane holding it. */
|
|
676
|
+
#inspectorPreview(): NativeNode[] {
|
|
677
|
+
const preview = this.#inspector.describePreview();
|
|
678
|
+
const scroll = this.#inspectorScroll;
|
|
679
|
+
const [first, ...rest] = preview;
|
|
680
|
+
return scroll && first ? [{ ...first, scroll }, ...rest] : preview;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** Action bar: Toggle (Space/Enter), Expand (the tools-expand key), Close (Esc). */
|
|
684
|
+
#pickerActions(selected: ExtensionListSwitch | undefined): TspPickerAction[] {
|
|
685
|
+
const expanded = this.#inspector.isExpanded();
|
|
686
|
+
return [
|
|
687
|
+
pickerAction("toggle", "Toggle", "space", {
|
|
688
|
+
primary: true,
|
|
689
|
+
on: selected?.on,
|
|
690
|
+
disabled: selected ? selected.blocked : "Select an extension",
|
|
691
|
+
}),
|
|
692
|
+
pickerAction("expand", expanded ? "Collapse" : "Expand", this.#expandKey(), { on: expanded }),
|
|
693
|
+
CLOSE_ACTION,
|
|
694
|
+
];
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
#expandKey(): string {
|
|
698
|
+
return boundKeys("app.tools.expand", ["ctrl+o"])[0] ?? "ctrl+o";
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
#describePage(): NativeNode {
|
|
702
|
+
const selected = this.#mainList.selectedSwitch();
|
|
703
|
+
const query = this.#state.searchQuery;
|
|
704
|
+
const shown = this.#state.searchFiltered.length;
|
|
705
|
+
const total = this.#state.tabFiltered.length;
|
|
706
|
+
const head = node(
|
|
707
|
+
"row",
|
|
708
|
+
{ justify: "between", align: "center", role: "omp.app.head" },
|
|
709
|
+
[
|
|
710
|
+
node("row", { gap: "sm", align: "center", role: "omp.app.where" }, [
|
|
711
|
+
text(DASHBOARD_TITLE, { role: "omp.app.title" }),
|
|
712
|
+
text([span(query ? `${shown} of ${total}` : `${total} extensions`, "muted")], { truncate: "end" }),
|
|
713
|
+
]),
|
|
714
|
+
node("icon", {
|
|
715
|
+
name: "x",
|
|
716
|
+
role: "omp.app.ibtn",
|
|
717
|
+
title: `Close ${interruptKey()}`,
|
|
718
|
+
aria: "Close",
|
|
719
|
+
actions: { click: "close" },
|
|
720
|
+
}),
|
|
721
|
+
],
|
|
722
|
+
"head",
|
|
723
|
+
);
|
|
724
|
+
const search = text([span("Search: ", "muted"), query ? span(query, "accent") : span("type to filter", "dim")], {
|
|
725
|
+
truncate: "end",
|
|
726
|
+
});
|
|
727
|
+
const body = node(
|
|
728
|
+
"row",
|
|
729
|
+
{ gap: "md", grow: 1 },
|
|
730
|
+
[
|
|
731
|
+
this.#mainList.describeList("list"),
|
|
732
|
+
{
|
|
733
|
+
...keyed(col(this.#inspector.describePreview(), { gap: "sm", grow: 1, basis: 0 }), "inspector"),
|
|
734
|
+
scroll: this.#inspectorScroll,
|
|
735
|
+
},
|
|
736
|
+
],
|
|
737
|
+
"body",
|
|
738
|
+
);
|
|
739
|
+
const expanded = this.#inspector.isExpanded();
|
|
740
|
+
const buttons: (NativeNode | null)[] = [
|
|
741
|
+
actionButton(selected?.on === false ? "Enable" : selected ? "Disable" : "Toggle", "toggle", {
|
|
742
|
+
keys: "space",
|
|
743
|
+
tone: "accent",
|
|
744
|
+
title: selected?.blocked ?? (selected ? undefined : "Select an extension"),
|
|
745
|
+
}),
|
|
746
|
+
actionButton(expanded ? "Collapse" : "Expand", "expand", { keys: this.#expandKey() }),
|
|
747
|
+
];
|
|
748
|
+
if (query) buttons.push(actionButton("Clear search", "clear", { keys: "escape" }));
|
|
749
|
+
buttons.push(null, actionButton("Close", "close", { keys: "escape" }));
|
|
750
|
+
return col([head, this.#tabBar, keyed(search, "search"), body, actionBar(buttons)], { gap: "md", grow: 1 });
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Pointer input, each on the path of the key it stands for: a row click
|
|
755
|
+
* selects (click on the selected row or double click toggles, like
|
|
756
|
+
* Space/Enter), a scope switches provider (←/→), `toggle`/`expand` run
|
|
757
|
+
* Space and the expand key, `clear` empties the search (first Esc) and
|
|
758
|
+
* `close` closes. Provider tabs in the page route to the {@link TabBar}.
|
|
759
|
+
*/
|
|
760
|
+
handleNativeEvent(event: NativeUiEvent): void {
|
|
761
|
+
const pick = pickerEvent(event);
|
|
762
|
+
if (pick) {
|
|
763
|
+
this.#handlePick(pick);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (event.type === "action") {
|
|
767
|
+
this.#handlePick({ kind: "action", act: event.act, value: event.value });
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (
|
|
771
|
+
(event.type === "select" || event.type === "activate") &&
|
|
772
|
+
event.key.slice(event.key.lastIndexOf("/") + 1) === "list"
|
|
773
|
+
) {
|
|
774
|
+
this.#handlePick({ kind: event.type, item: event.item });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
#handlePick(pick: PickerEvent): void {
|
|
779
|
+
if (pick.kind !== "action") {
|
|
780
|
+
if (this.#mainList.pickNative(pick.item, pick.kind === "activate")) this.#requestRender();
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
switch (pick.act) {
|
|
784
|
+
case "close":
|
|
785
|
+
this.#close();
|
|
786
|
+
return;
|
|
787
|
+
case "clear":
|
|
788
|
+
this.#clearSearch();
|
|
789
|
+
return;
|
|
790
|
+
case "scope":
|
|
791
|
+
if (pick.value) this.#tabBar.selectTab(pick.value);
|
|
792
|
+
return;
|
|
793
|
+
case "toggle":
|
|
794
|
+
this.#mainList.activateSelected();
|
|
795
|
+
this.#requestRender();
|
|
796
|
+
return;
|
|
797
|
+
case "expand":
|
|
798
|
+
this.#inspector.toggleExpanded();
|
|
799
|
+
this.#requestRender();
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
552
804
|
/**
|
|
553
805
|
* Live MCP health is joined at render time. Connection-status events and
|
|
554
806
|
* list-changed notifications only need to request a repaint — they must not
|
|
555
807
|
* rewrite Extension.raw.
|
|
556
808
|
*/
|
|
557
809
|
#subscribeMcpRuntime(): void {
|
|
558
|
-
this.#unsubscribers.push(...this.#runtime.subscribeMcpChanges(() => this
|
|
810
|
+
this.#unsubscribers.push(...this.#runtime.subscribeMcpChanges(() => this.#requestRender()));
|
|
559
811
|
}
|
|
560
812
|
|
|
561
813
|
dispose(): void {
|