@oh-my-pi/pi-coding-agent 3.4.1337 → 3.5.1337
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 +20 -0
- package/package.json +4 -4
- package/src/core/sdk.ts +14 -1
- package/src/core/settings-manager.ts +33 -0
- package/src/core/system-prompt.ts +15 -0
- package/src/core/title-generator.ts +28 -6
- package/src/core/tools/index.ts +6 -0
- package/src/core/tools/rulebook.ts +124 -0
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +297 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +477 -0
- package/src/modes/interactive/components/extensions/index.ts +9 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
- package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
- package/src/modes/interactive/components/extensions/types.ts +191 -0
- package/src/modes/interactive/components/settings-defs.ts +2 -31
- package/src/modes/interactive/components/settings-selector.ts +0 -1
- package/src/modes/interactive/interactive-mode.ts +24 -296
- package/src/modes/print-mode.ts +34 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExtensionList - Inventory list with Master Switch and fuzzy search.
|
|
3
|
+
*
|
|
4
|
+
* When viewing a specific provider (not "ALL"), Row #0 is the Master Switch
|
|
5
|
+
* that toggles the entire provider. All items below are dimmed when the
|
|
6
|
+
* master switch is off.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type Component,
|
|
11
|
+
isArrowDown,
|
|
12
|
+
isArrowUp,
|
|
13
|
+
isBackspace,
|
|
14
|
+
isEnter,
|
|
15
|
+
truncateToWidth,
|
|
16
|
+
visibleWidth,
|
|
17
|
+
} from "@oh-my-pi/pi-tui";
|
|
18
|
+
import { isProviderEnabled } from "../../../../discovery";
|
|
19
|
+
import { theme } from "../../theme/theme";
|
|
20
|
+
import { applyFilter } from "./state-manager";
|
|
21
|
+
import type { Extension, ExtensionKind, ExtensionState } from "./types";
|
|
22
|
+
|
|
23
|
+
export interface ExtensionListCallbacks {
|
|
24
|
+
/** Called when selection changes */
|
|
25
|
+
onSelectionChange?: (extension: Extension | null) => void;
|
|
26
|
+
/** Called when extension is toggled */
|
|
27
|
+
onToggle?: (extensionId: string, enabled: boolean) => void;
|
|
28
|
+
/** Called when master switch is toggled */
|
|
29
|
+
onMasterToggle?: (providerId: string) => void;
|
|
30
|
+
/** Provider ID for master switch (null = no master switch) */
|
|
31
|
+
masterSwitchProvider?: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const MAX_VISIBLE = 25;
|
|
35
|
+
|
|
36
|
+
/** Flattened list item for rendering */
|
|
37
|
+
type ListItem =
|
|
38
|
+
| { type: "master"; providerId: string; providerName: string; enabled: boolean }
|
|
39
|
+
| { type: "kind-header"; kind: ExtensionKind; label: string; icon: string; count: number }
|
|
40
|
+
| { type: "extension"; item: Extension };
|
|
41
|
+
|
|
42
|
+
export class ExtensionList implements Component {
|
|
43
|
+
private extensions: Extension[] = [];
|
|
44
|
+
private listItems: ListItem[] = [];
|
|
45
|
+
private selectedIndex = 0;
|
|
46
|
+
private scrollOffset = 0;
|
|
47
|
+
private searchQuery = "";
|
|
48
|
+
private focused = false;
|
|
49
|
+
private callbacks: ExtensionListCallbacks;
|
|
50
|
+
private masterSwitchProvider: string | null = null;
|
|
51
|
+
|
|
52
|
+
constructor(extensions: Extension[], callbacks: ExtensionListCallbacks = {}) {
|
|
53
|
+
this.extensions = extensions;
|
|
54
|
+
this.callbacks = callbacks;
|
|
55
|
+
this.masterSwitchProvider = callbacks.masterSwitchProvider ?? null;
|
|
56
|
+
this.rebuildList();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setExtensions(extensions: Extension[]): void {
|
|
60
|
+
this.extensions = extensions;
|
|
61
|
+
this.rebuildList();
|
|
62
|
+
this.clampSelection();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setFocused(focused: boolean): void {
|
|
66
|
+
this.focused = focused;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setMasterSwitchProvider(providerId: string | null): void {
|
|
70
|
+
this.masterSwitchProvider = providerId;
|
|
71
|
+
this.rebuildList();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getSearchQuery(): string {
|
|
75
|
+
return this.searchQuery;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
resetSelection(): void {
|
|
79
|
+
this.selectedIndex = 0;
|
|
80
|
+
this.scrollOffset = 0;
|
|
81
|
+
this.notifySelectionChange();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getSelectedExtension(): Extension | null {
|
|
85
|
+
const item = this.listItems[this.selectedIndex];
|
|
86
|
+
return item?.type === "extension" ? item.item : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Get the currently selected kind header (for preview purposes) */
|
|
90
|
+
getSelectedKind(): ExtensionKind | null {
|
|
91
|
+
const item = this.listItems[this.selectedIndex];
|
|
92
|
+
return item?.type === "kind-header" ? item.kind : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setSearchQuery(query: string): void {
|
|
96
|
+
this.searchQuery = query;
|
|
97
|
+
this.rebuildList();
|
|
98
|
+
this.selectedIndex = 0;
|
|
99
|
+
this.scrollOffset = 0;
|
|
100
|
+
this.notifySelectionChange();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
clearSearch(): void {
|
|
104
|
+
this.setSearchQuery("");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
invalidate(): void {}
|
|
108
|
+
|
|
109
|
+
render(width: number): string[] {
|
|
110
|
+
const lines: string[] = [];
|
|
111
|
+
|
|
112
|
+
// Search bar
|
|
113
|
+
const searchPrefix = theme.fg("muted", "Search: ");
|
|
114
|
+
const searchText = this.searchQuery || (this.focused ? "" : theme.fg("dim", "type to filter"));
|
|
115
|
+
const cursor = this.focused ? theme.fg("accent", "_") : "";
|
|
116
|
+
lines.push(searchPrefix + searchText + cursor);
|
|
117
|
+
lines.push("");
|
|
118
|
+
|
|
119
|
+
if (this.listItems.length === 0) {
|
|
120
|
+
lines.push(theme.fg("muted", " No extensions found for this provider."));
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Determine if master switch is off (for dimming child items)
|
|
125
|
+
const masterDisabled = this.masterSwitchProvider !== null && !isProviderEnabled(this.masterSwitchProvider);
|
|
126
|
+
|
|
127
|
+
// Calculate visible range
|
|
128
|
+
const startIdx = this.scrollOffset;
|
|
129
|
+
const endIdx = Math.min(startIdx + MAX_VISIBLE, this.listItems.length);
|
|
130
|
+
|
|
131
|
+
// Render visible items
|
|
132
|
+
for (let i = startIdx; i < endIdx; i++) {
|
|
133
|
+
const listItem = this.listItems[i];
|
|
134
|
+
const isSelected = this.focused && i === this.selectedIndex;
|
|
135
|
+
|
|
136
|
+
if (listItem.type === "master") {
|
|
137
|
+
lines.push(this.renderMasterSwitch(listItem, isSelected, width));
|
|
138
|
+
} else if (listItem.type === "kind-header") {
|
|
139
|
+
lines.push(this.renderKindHeader(listItem, isSelected, width));
|
|
140
|
+
} else {
|
|
141
|
+
lines.push(this.renderExtensionRow(listItem.item, isSelected, width, masterDisabled));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Scroll indicator
|
|
146
|
+
if (this.listItems.length > MAX_VISIBLE) {
|
|
147
|
+
const indicator = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.listItems.length})`);
|
|
148
|
+
lines.push(indicator);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return lines;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private renderMasterSwitch(item: ListItem & { type: "master" }, isSelected: boolean, width: number): string {
|
|
155
|
+
const checkbox = item.enabled ? theme.fg("success", "[x]") : theme.fg("dim", "[ ]");
|
|
156
|
+
const icon = "📦";
|
|
157
|
+
const label = `Enable ${item.providerName}`;
|
|
158
|
+
const badge = theme.fg("warning", "(Master Switch)");
|
|
159
|
+
|
|
160
|
+
let line = `${checkbox} ${icon} ${label} ${badge}`;
|
|
161
|
+
|
|
162
|
+
if (isSelected) {
|
|
163
|
+
line = theme.bold(theme.fg("accent", line));
|
|
164
|
+
line = theme.bg("selectedBg", line);
|
|
165
|
+
} else if (!item.enabled) {
|
|
166
|
+
line = theme.fg("dim", line);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return truncateToWidth(line, width);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private renderKindHeader(item: ListItem & { type: "kind-header" }, isSelected: boolean, width: number): string {
|
|
173
|
+
const countBadge = theme.fg("muted", `(${item.count})`);
|
|
174
|
+
let line = `${item.icon} ${item.label} ${countBadge}`;
|
|
175
|
+
|
|
176
|
+
if (isSelected) {
|
|
177
|
+
line = theme.bold(theme.fg("accent", line));
|
|
178
|
+
line = theme.bg("selectedBg", line);
|
|
179
|
+
} else {
|
|
180
|
+
line = theme.fg("muted", line);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return truncateToWidth(line, width);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private renderExtensionRow(ext: Extension, isSelected: boolean, width: number, masterDisabled: boolean): string {
|
|
187
|
+
// When master is disabled, all items appear dimmed
|
|
188
|
+
const effectivelyDisabled = masterDisabled || ext.state === "disabled";
|
|
189
|
+
|
|
190
|
+
// Status icon
|
|
191
|
+
const stateIcon = this.getStateIcon(ext.state, masterDisabled);
|
|
192
|
+
|
|
193
|
+
// Name
|
|
194
|
+
let name = ext.displayName;
|
|
195
|
+
const nameWidth = Math.min(24, width - 16);
|
|
196
|
+
|
|
197
|
+
// Build the line with indentation (visually "inside" the master switch)
|
|
198
|
+
let line = ` ${stateIcon} `;
|
|
199
|
+
|
|
200
|
+
if (isSelected && !masterDisabled) {
|
|
201
|
+
name = theme.bold(theme.fg("accent", name));
|
|
202
|
+
} else if (effectivelyDisabled) {
|
|
203
|
+
name = theme.fg("dim", name);
|
|
204
|
+
} else if (ext.state === "shadowed") {
|
|
205
|
+
name = theme.fg("warning", name);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Pad name
|
|
209
|
+
const namePadded = this.padText(name, nameWidth);
|
|
210
|
+
line += namePadded;
|
|
211
|
+
|
|
212
|
+
// Trigger hint
|
|
213
|
+
if (ext.trigger) {
|
|
214
|
+
const triggerStyle = effectivelyDisabled ? "dim" : "muted";
|
|
215
|
+
const remainingWidth = width - visibleWidth(line) - 2;
|
|
216
|
+
if (remainingWidth > 5) {
|
|
217
|
+
line += ` ${truncateToWidth(theme.fg(triggerStyle as "dim" | "muted", ext.trigger), remainingWidth)}`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Apply selection background
|
|
222
|
+
if (isSelected) {
|
|
223
|
+
line = theme.bg("selectedBg", line);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return truncateToWidth(line, width);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private getKindIcon(kind: ExtensionKind): string {
|
|
230
|
+
switch (kind) {
|
|
231
|
+
case "skill":
|
|
232
|
+
return "⚡";
|
|
233
|
+
case "tool":
|
|
234
|
+
return "🔧";
|
|
235
|
+
case "slash-command":
|
|
236
|
+
return "🔗";
|
|
237
|
+
case "mcp":
|
|
238
|
+
return "🔄";
|
|
239
|
+
case "rule":
|
|
240
|
+
return "📋";
|
|
241
|
+
case "hook":
|
|
242
|
+
return "🪝";
|
|
243
|
+
case "prompt":
|
|
244
|
+
return "💬";
|
|
245
|
+
case "context-file":
|
|
246
|
+
return "📄";
|
|
247
|
+
case "instruction":
|
|
248
|
+
return "📌";
|
|
249
|
+
default:
|
|
250
|
+
return "•";
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private getStateIcon(state: ExtensionState, masterDisabled: boolean): string {
|
|
255
|
+
if (masterDisabled) {
|
|
256
|
+
return theme.fg("dim", "○");
|
|
257
|
+
}
|
|
258
|
+
switch (state) {
|
|
259
|
+
case "active":
|
|
260
|
+
return theme.fg("success", "●");
|
|
261
|
+
case "disabled":
|
|
262
|
+
return theme.fg("dim", "⊘");
|
|
263
|
+
case "shadowed":
|
|
264
|
+
return theme.fg("warning", "◐");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private padText(text: string, targetWidth: number): string {
|
|
269
|
+
const width = visibleWidth(text);
|
|
270
|
+
if (width >= targetWidth) {
|
|
271
|
+
return truncateToWidth(text, targetWidth);
|
|
272
|
+
}
|
|
273
|
+
return text + " ".repeat(targetWidth - width);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private rebuildList(): void {
|
|
277
|
+
this.listItems = [];
|
|
278
|
+
|
|
279
|
+
// Apply search filter
|
|
280
|
+
const filtered = this.searchQuery.length > 0 ? applyFilter(this.extensions, this.searchQuery) : this.extensions;
|
|
281
|
+
|
|
282
|
+
// When searching, show flat list
|
|
283
|
+
if (this.searchQuery.length > 0) {
|
|
284
|
+
for (const ext of filtered) {
|
|
285
|
+
this.listItems.push({ type: "extension", item: ext });
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Provider-specific view: Master switch + flat list
|
|
291
|
+
if (this.masterSwitchProvider) {
|
|
292
|
+
const providerName = filtered[0]?.source.providerName ?? this.masterSwitchProvider;
|
|
293
|
+
const enabled = isProviderEnabled(this.masterSwitchProvider);
|
|
294
|
+
|
|
295
|
+
this.listItems.push({
|
|
296
|
+
type: "master",
|
|
297
|
+
providerId: this.masterSwitchProvider,
|
|
298
|
+
providerName,
|
|
299
|
+
enabled,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
for (const ext of filtered) {
|
|
303
|
+
this.listItems.push({ type: "extension", item: ext });
|
|
304
|
+
}
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ALL view: Group by kind with headers
|
|
309
|
+
const byKind = new Map<ExtensionKind, Extension[]>();
|
|
310
|
+
for (const ext of filtered) {
|
|
311
|
+
const list = byKind.get(ext.kind) ?? [];
|
|
312
|
+
list.push(ext);
|
|
313
|
+
byKind.set(ext.kind, list);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const kindOrder: ExtensionKind[] = [
|
|
317
|
+
"skill",
|
|
318
|
+
"tool",
|
|
319
|
+
"slash-command",
|
|
320
|
+
"rule",
|
|
321
|
+
"mcp",
|
|
322
|
+
"hook",
|
|
323
|
+
"prompt",
|
|
324
|
+
"context-file",
|
|
325
|
+
"instruction",
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
for (const kind of kindOrder) {
|
|
329
|
+
const items = byKind.get(kind);
|
|
330
|
+
if (!items || items.length === 0) continue;
|
|
331
|
+
|
|
332
|
+
this.listItems.push({
|
|
333
|
+
type: "kind-header",
|
|
334
|
+
kind,
|
|
335
|
+
label: this.getKindLabel(kind),
|
|
336
|
+
icon: this.getKindIcon(kind),
|
|
337
|
+
count: items.length,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
for (const ext of items) {
|
|
341
|
+
this.listItems.push({ type: "extension", item: ext });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private getKindLabel(kind: ExtensionKind): string {
|
|
347
|
+
switch (kind) {
|
|
348
|
+
case "skill":
|
|
349
|
+
return "Skills";
|
|
350
|
+
case "tool":
|
|
351
|
+
return "Tools";
|
|
352
|
+
case "slash-command":
|
|
353
|
+
return "Commands";
|
|
354
|
+
case "rule":
|
|
355
|
+
return "Rules";
|
|
356
|
+
case "mcp":
|
|
357
|
+
return "MCP Servers";
|
|
358
|
+
case "hook":
|
|
359
|
+
return "Hooks";
|
|
360
|
+
case "prompt":
|
|
361
|
+
return "Prompts";
|
|
362
|
+
case "context-file":
|
|
363
|
+
return "Context";
|
|
364
|
+
case "instruction":
|
|
365
|
+
return "Instructions";
|
|
366
|
+
default:
|
|
367
|
+
return kind;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private clampSelection(): void {
|
|
372
|
+
if (this.listItems.length === 0) {
|
|
373
|
+
this.selectedIndex = 0;
|
|
374
|
+
this.scrollOffset = 0;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
this.selectedIndex = Math.min(this.selectedIndex, this.listItems.length - 1);
|
|
379
|
+
this.selectedIndex = Math.max(0, this.selectedIndex);
|
|
380
|
+
|
|
381
|
+
// Adjust scroll offset
|
|
382
|
+
if (this.selectedIndex < this.scrollOffset) {
|
|
383
|
+
this.scrollOffset = this.selectedIndex;
|
|
384
|
+
} else if (this.selectedIndex >= this.scrollOffset + MAX_VISIBLE) {
|
|
385
|
+
this.scrollOffset = this.selectedIndex - MAX_VISIBLE + 1;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
handleInput(data: string): void {
|
|
390
|
+
const charCode = data.length === 1 ? data.charCodeAt(0) : -1;
|
|
391
|
+
|
|
392
|
+
// Navigation
|
|
393
|
+
if (isArrowUp(data) || data === "k") {
|
|
394
|
+
this.moveSelectionUp();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (isArrowDown(data) || data === "j") {
|
|
399
|
+
this.moveSelectionDown();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Space: Toggle selected item
|
|
404
|
+
if (data === " ") {
|
|
405
|
+
const item = this.listItems[this.selectedIndex];
|
|
406
|
+
if (item?.type === "master") {
|
|
407
|
+
this.callbacks.onMasterToggle?.(item.providerId);
|
|
408
|
+
} else if (item?.type === "extension") {
|
|
409
|
+
// Only allow toggling if master is enabled
|
|
410
|
+
const masterDisabled = this.masterSwitchProvider !== null && !isProviderEnabled(this.masterSwitchProvider);
|
|
411
|
+
if (!masterDisabled) {
|
|
412
|
+
const newEnabled = item.item.state === "disabled";
|
|
413
|
+
this.callbacks.onToggle?.(item.item.id, newEnabled);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Enter: Same as space - toggle selected item
|
|
420
|
+
if (isEnter(data)) {
|
|
421
|
+
const item = this.listItems[this.selectedIndex];
|
|
422
|
+
if (item?.type === "master") {
|
|
423
|
+
this.callbacks.onMasterToggle?.(item.providerId);
|
|
424
|
+
} else if (item?.type === "extension") {
|
|
425
|
+
const masterDisabled = this.masterSwitchProvider !== null && !isProviderEnabled(this.masterSwitchProvider);
|
|
426
|
+
if (!masterDisabled) {
|
|
427
|
+
const newEnabled = item.item.state === "disabled";
|
|
428
|
+
this.callbacks.onToggle?.(item.item.id, newEnabled);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Backspace: Delete from search query
|
|
435
|
+
if (isBackspace(data)) {
|
|
436
|
+
if (this.searchQuery.length > 0) {
|
|
437
|
+
this.setSearchQuery(this.searchQuery.slice(0, -1));
|
|
438
|
+
}
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Printable characters -> search
|
|
443
|
+
if (data.length === 1 && charCode > 32 && charCode < 127) {
|
|
444
|
+
// Skip j/k as they're navigation
|
|
445
|
+
if (data === "j" || data === "k") {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
this.setSearchQuery(this.searchQuery + data);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private moveSelectionUp(): void {
|
|
454
|
+
if (this.selectedIndex > 0) {
|
|
455
|
+
this.selectedIndex--;
|
|
456
|
+
if (this.selectedIndex < this.scrollOffset) {
|
|
457
|
+
this.scrollOffset = this.selectedIndex;
|
|
458
|
+
}
|
|
459
|
+
this.notifySelectionChange();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private moveSelectionDown(): void {
|
|
464
|
+
if (this.selectedIndex < this.listItems.length - 1) {
|
|
465
|
+
this.selectedIndex++;
|
|
466
|
+
if (this.selectedIndex >= this.scrollOffset + MAX_VISIBLE) {
|
|
467
|
+
this.scrollOffset = this.selectedIndex - MAX_VISIBLE + 1;
|
|
468
|
+
}
|
|
469
|
+
this.notifySelectionChange();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private notifySelectionChange(): void {
|
|
474
|
+
const ext = this.getSelectedExtension();
|
|
475
|
+
this.callbacks.onSelectionChange?.(ext);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension Control Center exports.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { ExtensionDashboard } from "./extension-dashboard";
|
|
6
|
+
export { ExtensionList } from "./extension-list";
|
|
7
|
+
export { InspectorPanel } from "./inspector-panel";
|
|
8
|
+
export * from "./state-manager";
|
|
9
|
+
export * from "./types";
|