@oh-my-pi/pi-coding-agent 16.1.17 → 16.1.19
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 +28 -0
- package/dist/cli.js +17872 -17571
- package/dist/types/cli/usage-cli.d.ts +14 -0
- package/dist/types/commands/token.d.ts +9 -0
- package/dist/types/edit/hashline/filesystem.d.ts +1 -18
- package/dist/types/extensibility/plugins/legacy-pi-bundled-keys.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-bundled-registry.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +43 -27
- package/dist/types/internal-urls/skill-protocol.d.ts +2 -2
- package/dist/types/internal-urls/types.d.ts +3 -0
- package/dist/types/modes/components/custom-editor.d.ts +0 -2
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +26 -10
- package/dist/types/modes/components/extensions/extension-list.d.ts +13 -0
- package/dist/types/modes/controllers/input-controller.d.ts +0 -1
- package/dist/types/tools/path-utils.d.ts +3 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +29 -16
- package/scripts/generate-legacy-pi-bundled-registry.ts +404 -0
- package/src/cli/usage-cli.ts +35 -4
- package/src/commands/token.ts +54 -0
- package/src/edit/hashline/filesystem.ts +14 -0
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +987 -0
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +3352 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +264 -131
- package/src/internal-urls/local-protocol.ts +116 -9
- package/src/internal-urls/skill-protocol.ts +3 -3
- package/src/internal-urls/types.ts +3 -0
- package/src/modes/components/custom-editor.test.ts +7 -5
- package/src/modes/components/custom-editor.ts +6 -16
- package/src/modes/components/extensions/extension-dashboard.ts +221 -165
- package/src/modes/components/extensions/extension-list.ts +66 -33
- package/src/modes/controllers/input-controller.ts +0 -71
- package/src/modes/controllers/selector-controller.ts +4 -0
- package/src/session/messages.ts +70 -47
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/find.ts +1 -0
- package/src/tools/path-utils.ts +4 -0
- package/src/tools/read.ts +43 -17
- package/src/tools/search.ts +4 -0
|
@@ -93,15 +93,17 @@ describe("CustomEditor bracketed path paste", () => {
|
|
|
93
93
|
expect(extractBracketedImagePastePaths(bracketedPaste("/tmp/report.csv"))).toBeUndefined();
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
it("
|
|
96
|
+
it("inserts non-image path pastes as literal text instead of attaching them", () => {
|
|
97
97
|
const { editor } = makeEditor();
|
|
98
|
-
|
|
99
|
-
editor.
|
|
98
|
+
let imagePathCalls = 0;
|
|
99
|
+
editor.onPasteImagePath = () => {
|
|
100
|
+
imagePathCalls++;
|
|
101
|
+
};
|
|
100
102
|
|
|
101
103
|
editor.handleInput(bracketedPaste("/tmp/report.csv"));
|
|
102
104
|
|
|
103
|
-
expect(
|
|
104
|
-
expect(
|
|
105
|
+
expect(editor.getText()).toBe("/tmp/report.csv");
|
|
106
|
+
expect(imagePathCalls).toBe(0);
|
|
105
107
|
});
|
|
106
108
|
});
|
|
107
109
|
|
|
@@ -318,8 +318,6 @@ export class CustomEditor extends Editor {
|
|
|
318
318
|
onPasteImage?: () => Promise<boolean>;
|
|
319
319
|
/** Called when a bracketed paste contains one or more image-file paths. */
|
|
320
320
|
onPasteImagePath?: (path: string) => void | Promise<void>;
|
|
321
|
-
/** Called when a bracketed paste contains one or more non-image file paths. */
|
|
322
|
-
onPasteFilePath?: (path: string) => void | Promise<void>;
|
|
323
321
|
/** Called when the configured raw text-paste shortcut is pressed. */
|
|
324
322
|
onPasteTextRaw?: () => void;
|
|
325
323
|
/** Called when the configured dequeue shortcut is pressed. */
|
|
@@ -505,20 +503,12 @@ export class CustomEditor extends Editor {
|
|
|
505
503
|
return;
|
|
506
504
|
}
|
|
507
505
|
|
|
508
|
-
const
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
);
|
|
513
|
-
|
|
514
|
-
void (async () => {
|
|
515
|
-
for (const path of pastedPaths) {
|
|
516
|
-
if (isImagePath(path)) await this.onPasteImagePath?.(path);
|
|
517
|
-
else await this.onPasteFilePath?.(path);
|
|
518
|
-
}
|
|
519
|
-
})();
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
506
|
+
const pastedImagePaths = extractBracketedImagePastePaths(data);
|
|
507
|
+
if (pastedImagePaths && this.onPasteImagePath) {
|
|
508
|
+
void (async () => {
|
|
509
|
+
for (const path of pastedImagePaths) await this.onPasteImagePath?.(path);
|
|
510
|
+
})();
|
|
511
|
+
return;
|
|
522
512
|
}
|
|
523
513
|
|
|
524
514
|
const parsedKey = parseKey(data);
|
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ExtensionDashboard -
|
|
2
|
+
* ExtensionDashboard - Fullscreen alternate-screen control center for extensions.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Chrome mirrors the `/settings` overlay: a titled rounded box, a shared
|
|
5
|
+
* {@link TabBar} for provider selection, and a two-column body (inventory list |
|
|
6
|
+
* inspector). Both panes are mouse-aware — wheel scrolls, hover highlights, and
|
|
7
|
+
* clicks select/activate — routed from a single SGR-mouse handler.
|
|
7
8
|
*
|
|
8
9
|
* Navigation:
|
|
9
|
-
* -
|
|
10
|
-
* - Up/Down/j/k:
|
|
11
|
-
* - Space:
|
|
12
|
-
* -
|
|
10
|
+
* - Tab/Shift+Tab or ←/→: switch provider tab
|
|
11
|
+
* - Up/Down/j/k or wheel: move list selection
|
|
12
|
+
* - Space/Enter or click: toggle selected item (or provider master switch)
|
|
13
|
+
* - Wheel over the inspector: scroll the detail pane
|
|
14
|
+
* - Esc: clear search (if active) then close
|
|
13
15
|
*/
|
|
14
16
|
import {
|
|
15
17
|
type Component,
|
|
16
|
-
Container,
|
|
17
18
|
matchesKey,
|
|
18
19
|
padding,
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
parseSgrMouse,
|
|
21
|
+
ScrollView,
|
|
22
|
+
type Tab,
|
|
23
|
+
TabBar,
|
|
21
24
|
truncateToWidth,
|
|
22
25
|
visibleWidth,
|
|
23
|
-
wrapTextWithAnsi,
|
|
24
26
|
} from "@oh-my-pi/pi-tui";
|
|
25
27
|
import { Settings } from "../../../config/settings";
|
|
26
|
-
import {
|
|
28
|
+
import { getTabBarTheme } from "../../../modes/shared";
|
|
27
29
|
import { theme } from "../../../modes/theme/theme";
|
|
28
30
|
import { matchesAppInterrupt } from "../../../modes/utils/keybinding-matchers";
|
|
29
|
-
import {
|
|
31
|
+
import { bottomBorder, divider, row, topBorder } from "../overlay-box";
|
|
30
32
|
import { ExtensionList } from "./extension-list";
|
|
31
33
|
import { InspectorPanel } from "./inspector-panel";
|
|
32
34
|
import {
|
|
@@ -37,17 +39,41 @@ import {
|
|
|
37
39
|
refreshState,
|
|
38
40
|
toggleProvider,
|
|
39
41
|
} from "./state-manager";
|
|
40
|
-
import type { DashboardState } from "./types";
|
|
42
|
+
import type { DashboardState, ProviderTab } from "./types";
|
|
41
43
|
|
|
42
|
-
const EXT_FOOTER = " ↑/↓: navigate
|
|
44
|
+
const EXT_FOOTER = " ↑/↓: navigate · Space: toggle · ←/→: provider · Esc: close";
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Map dashboard provider tabs to {@link TabBar} tabs. Empty *enabled* providers
|
|
48
|
+
* are muted — skipped by keyboard nav and unclickable; disabled providers stay
|
|
49
|
+
* selectable (with a leading disabled glyph) so their master switch can be
|
|
50
|
+
* re-enabled from the list. The "all" tab is never muted or marked.
|
|
51
|
+
*/
|
|
52
|
+
export function buildTabBarTabs(tabs: ProviderTab[]): Tab[] {
|
|
53
|
+
return tabs.map(tab => {
|
|
54
|
+
const isAll = tab.id === "all";
|
|
55
|
+
const isEmptyEnabled = tab.count === 0 && tab.enabled && !isAll;
|
|
56
|
+
const isDisabled = !tab.enabled && !isAll;
|
|
57
|
+
let label = tab.label;
|
|
58
|
+
if (tab.count > 0) label += ` (${tab.count})`;
|
|
59
|
+
if (isDisabled) label = `${theme.status.disabled} ${label}`;
|
|
60
|
+
return { id: tab.id, label, short: tab.label, muted: isEmptyEnabled };
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class ExtensionDashboard implements Component {
|
|
45
65
|
#state!: DashboardState;
|
|
46
66
|
#mainList!: ExtensionList;
|
|
47
67
|
#inspector!: InspectorPanel;
|
|
68
|
+
#tabBar!: TabBar;
|
|
69
|
+
#body!: TwoColumnBody;
|
|
48
70
|
#refreshToken = 0;
|
|
49
|
-
|
|
50
|
-
|
|
71
|
+
// Frame geometry from the last render, for SGR mouse hit-testing. The
|
|
72
|
+
// fullscreen overlay paints from screen row 0, so mouse rows map 1:1.
|
|
73
|
+
#tabRowStart = 0;
|
|
74
|
+
#tabRowCount = 0;
|
|
75
|
+
#bodyRowStart = 0;
|
|
76
|
+
#bodyRowCount = 0;
|
|
51
77
|
|
|
52
78
|
onClose?: () => void;
|
|
53
79
|
onRequestRender?: () => void;
|
|
@@ -56,9 +82,7 @@ export class ExtensionDashboard extends Container {
|
|
|
56
82
|
private readonly cwd: string,
|
|
57
83
|
private readonly settings: Settings | null,
|
|
58
84
|
private readonly terminalHeight: number,
|
|
59
|
-
) {
|
|
60
|
-
super();
|
|
61
|
-
}
|
|
85
|
+
) {}
|
|
62
86
|
|
|
63
87
|
static async create(
|
|
64
88
|
cwd: string,
|
|
@@ -75,37 +99,36 @@ export class ExtensionDashboard extends Container {
|
|
|
75
99
|
const disabledIds = sm ? ((sm.get("disabledExtensions") as string[]) ?? []) : [];
|
|
76
100
|
this.#state = await createInitialState(this.cwd, disabledIds);
|
|
77
101
|
|
|
78
|
-
|
|
79
|
-
// Reserve ~10 lines for header, tabs, help text, borders
|
|
80
|
-
const maxVisible = this.#maxVisibleItems();
|
|
81
|
-
|
|
82
|
-
// Create main list - always focused
|
|
102
|
+
const initialMaxVisible = Math.max(3, this.terminalHeight - 9);
|
|
83
103
|
this.#mainList = new ExtensionList(
|
|
84
104
|
this.#state.searchFiltered,
|
|
85
105
|
{
|
|
86
106
|
onSelectionChange: ext => {
|
|
87
107
|
this.#state.selected = ext;
|
|
88
108
|
this.#inspector.setExtension(ext);
|
|
109
|
+
// A fresh selection resets the inspector to the top.
|
|
110
|
+
this.#body.resetInspectorScroll();
|
|
89
111
|
},
|
|
90
|
-
onToggle: (extensionId, enabled) =>
|
|
91
|
-
|
|
92
|
-
},
|
|
93
|
-
onMasterToggle: providerId => {
|
|
94
|
-
this.#handleProviderToggle(providerId);
|
|
95
|
-
},
|
|
112
|
+
onToggle: (extensionId, enabled) => this.#handleExtensionToggle(extensionId, enabled),
|
|
113
|
+
onMasterToggle: providerId => this.#handleProviderToggle(providerId),
|
|
96
114
|
masterSwitchProvider: this.#getActiveProviderId(),
|
|
97
115
|
},
|
|
98
|
-
|
|
116
|
+
initialMaxVisible,
|
|
99
117
|
);
|
|
100
118
|
this.#mainList.setFocused(true);
|
|
101
119
|
|
|
102
|
-
// Create inspector
|
|
103
120
|
this.#inspector = new InspectorPanel();
|
|
104
121
|
if (this.#state.selected) {
|
|
105
122
|
this.#inspector.setExtension(this.#state.selected);
|
|
106
123
|
}
|
|
107
124
|
|
|
108
|
-
this.#
|
|
125
|
+
this.#body = new TwoColumnBody(this.#mainList, this.#inspector, this.terminalHeight);
|
|
126
|
+
|
|
127
|
+
this.#tabBar = new TabBar("", buildTabBarTabs(this.#state.tabs), getTabBarTheme());
|
|
128
|
+
this.#tabBar.showHint = false;
|
|
129
|
+
this.#tabBar.onTabChange = tab => this.#selectProviderById(tab.id);
|
|
130
|
+
const activeId = this.#state.tabs[this.#state.activeTabIndex]?.id;
|
|
131
|
+
if (activeId) this.#tabBar.setActiveById(activeId);
|
|
109
132
|
}
|
|
110
133
|
|
|
111
134
|
#getActiveProviderId(): string | null {
|
|
@@ -118,98 +141,117 @@ export class ExtensionDashboard extends Container {
|
|
|
118
141
|
return process.stdout.rows || this.terminalHeight || 24;
|
|
119
142
|
}
|
|
120
143
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
const
|
|
133
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Fullscreen frame: titled top border, the tab row(s), a divider, the
|
|
146
|
+
* two-column body sized to fill the viewport, a divider, the footer hint, and
|
|
147
|
+
* the bottom border. Records row geometry for mouse hit-testing.
|
|
148
|
+
*/
|
|
149
|
+
render(width: number): readonly string[] {
|
|
150
|
+
const height = Math.max(14, this.#terminalRows());
|
|
151
|
+
const innerWidth = Math.max(1, width - 4);
|
|
152
|
+
|
|
153
|
+
const tabLines = this.#tabBar.render(innerWidth);
|
|
154
|
+
// Fixed chrome: top border + tab rows + divider + divider + footer + bottom border.
|
|
155
|
+
const fixedRows = 1 + tabLines.length + 1 + 1 + 1 + 1;
|
|
156
|
+
const contentRows = Math.max(5, height - fixedRows);
|
|
157
|
+
|
|
158
|
+
this.#mainList.setMaxVisible(Math.max(3, contentRows - 2));
|
|
159
|
+
this.#body.setMaxHeight(contentRows);
|
|
160
|
+
const bodyLines = this.#body.render(innerWidth);
|
|
161
|
+
|
|
162
|
+
const out: string[] = [];
|
|
163
|
+
out.push(topBorder(width, "Extension Control Center"));
|
|
164
|
+
this.#tabRowStart = out.length;
|
|
165
|
+
this.#tabRowCount = tabLines.length;
|
|
166
|
+
for (const line of tabLines) out.push(row(line, width));
|
|
167
|
+
out.push(divider(width));
|
|
168
|
+
this.#bodyRowStart = out.length;
|
|
169
|
+
this.#bodyRowCount = contentRows;
|
|
170
|
+
for (let i = 0; i < contentRows; i++) out.push(row(bodyLines[i] ?? "", width));
|
|
171
|
+
out.push(divider(width));
|
|
172
|
+
out.push(row(theme.fg("dim", EXT_FOOTER), width));
|
|
173
|
+
out.push(bottomBorder(width));
|
|
174
|
+
return out;
|
|
134
175
|
}
|
|
135
176
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
177
|
+
invalidate(): void {
|
|
178
|
+
this.#tabBar.invalidate();
|
|
179
|
+
this.#mainList.invalidate();
|
|
180
|
+
this.#inspector.invalidate();
|
|
139
181
|
}
|
|
140
182
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Route an SGR mouse report against the last render's geometry. Wheel scrolls
|
|
185
|
+
* the pane under the pointer, motion drives hover highlights (tabs + rows),
|
|
186
|
+
* and a left click switches tabs or selects/activates a list row.
|
|
187
|
+
*/
|
|
188
|
+
#handleMouse(data: string): void {
|
|
189
|
+
const event = parseSgrMouse(data);
|
|
190
|
+
if (!event) return;
|
|
191
|
+
|
|
192
|
+
// row() insets content by two columns (border + space).
|
|
193
|
+
const innerCol = event.col - 2;
|
|
194
|
+
const tabLine = event.row - this.#tabRowStart;
|
|
195
|
+
const overTabs = tabLine >= 0 && tabLine < this.#tabRowCount;
|
|
196
|
+
const bodyLine = event.row - this.#bodyRowStart;
|
|
197
|
+
const overBody = bodyLine >= 0 && bodyLine < this.#bodyRowCount;
|
|
198
|
+
const leftWidth = this.#body.leftWidth;
|
|
199
|
+
const overList = overBody && innerCol < leftWidth;
|
|
200
|
+
const overInspector = overBody && innerCol >= leftWidth + 3;
|
|
201
|
+
|
|
202
|
+
if (event.wheel !== null) {
|
|
203
|
+
if (overList) {
|
|
204
|
+
this.#mainList.handleWheel(event.wheel);
|
|
205
|
+
this.onRequestRender?.();
|
|
206
|
+
} else if (overInspector) {
|
|
207
|
+
this.#body.scrollInspector(event.wheel);
|
|
208
|
+
this.onRequestRender?.();
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
146
211
|
}
|
|
147
|
-
const lines = super.render(width);
|
|
148
|
-
// Pad to the full viewport so the dashboard covers the screen instead of
|
|
149
|
-
// letting the transcript peek through below it.
|
|
150
|
-
return padLinesToHeight(lines, this.#terminalRows());
|
|
151
|
-
}
|
|
152
212
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
this.addChild(new Text(theme.bold(theme.fg("accent", " Extension Control Center")), 0, 0));
|
|
161
|
-
|
|
162
|
-
// Tab bar
|
|
163
|
-
this.addChild(new Text(this.#renderTabBar(), 0, 0));
|
|
164
|
-
this.addChild(new Spacer(1));
|
|
165
|
-
|
|
166
|
-
// 2-column body sized to fill the live terminal viewport.
|
|
167
|
-
const bodyMaxHeight = this.#computeBodyHeight();
|
|
168
|
-
this.#mainList.setMaxVisible(this.#maxVisibleItems());
|
|
169
|
-
this.addChild(new TwoColumnBody(this.#mainList, this.#inspector, bodyMaxHeight));
|
|
213
|
+
if (event.motion) {
|
|
214
|
+
const hoveredTab = overTabs ? this.#tabBar.tabAt(tabLine, innerCol) : undefined;
|
|
215
|
+
this.#tabBar.setHoverTab(hoveredTab && !hoveredTab.muted ? hoveredTab.id : null);
|
|
216
|
+
this.#mainList.setHoverIndex(overList ? this.#mainList.hitTest(bodyLine) : null);
|
|
217
|
+
this.onRequestRender?.();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
170
220
|
|
|
171
|
-
|
|
172
|
-
this.addChild(new Text(theme.fg("dim", EXT_FOOTER), 0, 0));
|
|
221
|
+
if (!event.leftClick) return;
|
|
173
222
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
223
|
+
if (overTabs) {
|
|
224
|
+
const tab = this.#tabBar.tabAt(tabLine, innerCol);
|
|
225
|
+
if (tab) this.#tabBar.selectTab(tab.id);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (overList) {
|
|
229
|
+
this.#mainList.handleClick(bodyLine);
|
|
230
|
+
this.onRequestRender?.();
|
|
231
|
+
}
|
|
178
232
|
}
|
|
179
233
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const isActive = i === this.#state.activeTabIndex;
|
|
186
|
-
const isEmpty = tab.count === 0 && tab.id !== "all";
|
|
187
|
-
const isDisabled = !tab.enabled && tab.id !== "all";
|
|
234
|
+
/** Switch to the provider tab with `id`, re-filtering the list around it. */
|
|
235
|
+
#selectProviderById(id: string): void {
|
|
236
|
+
const index = this.#state.tabs.findIndex(t => t.id === id);
|
|
237
|
+
if (index < 0) return;
|
|
238
|
+
this.#state.activeTabIndex = index;
|
|
188
239
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
240
|
+
const tab = this.#state.tabs[index];
|
|
241
|
+
this.#state.tabFiltered = filterByProvider(this.#state.extensions, tab.id);
|
|
242
|
+
this.#state.searchFiltered = applyFilter(this.#state.tabFiltered, this.#state.searchQuery);
|
|
243
|
+
this.#state.listIndex = 0;
|
|
244
|
+
this.#state.scrollOffset = 0;
|
|
245
|
+
this.#state.selected = this.#state.searchFiltered[0] ?? null;
|
|
194
246
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
} else if (isDisabled) {
|
|
201
|
-
// Disabled provider: dim
|
|
202
|
-
parts.push(theme.fg("dim", ` ${displayLabel} `));
|
|
203
|
-
} else if (isEmpty) {
|
|
204
|
-
// Empty enabled provider: very dim, unselectable
|
|
205
|
-
parts.push(theme.fg("dim", ` ${label} `));
|
|
206
|
-
} else {
|
|
207
|
-
// Normal enabled provider
|
|
208
|
-
parts.push(theme.fg("muted", ` ${label} `));
|
|
209
|
-
}
|
|
247
|
+
this.#mainList.setExtensions(this.#state.searchFiltered);
|
|
248
|
+
this.#mainList.setMasterSwitchProvider(this.#getActiveProviderId());
|
|
249
|
+
this.#mainList.resetSelection();
|
|
250
|
+
if (this.#state.selected) {
|
|
251
|
+
this.#inspector.setExtension(this.#state.selected);
|
|
210
252
|
}
|
|
211
|
-
|
|
212
|
-
|
|
253
|
+
this.#body.resetInspectorScroll();
|
|
254
|
+
this.onRequestRender?.();
|
|
213
255
|
}
|
|
214
256
|
|
|
215
257
|
#handleProviderToggle(providerId: string): void {
|
|
@@ -241,7 +283,7 @@ export class ExtensionDashboard extends Container {
|
|
|
241
283
|
|
|
242
284
|
async #refreshFromState(): Promise<void> {
|
|
243
285
|
const refreshToken = ++this.#refreshToken;
|
|
244
|
-
// Remember current tab
|
|
286
|
+
// Remember the current tab so it survives the re-sort.
|
|
245
287
|
const currentTabId = this.#state.tabs[this.#state.activeTabIndex]?.id;
|
|
246
288
|
|
|
247
289
|
const sm = this.settings ?? Settings.instance;
|
|
@@ -250,7 +292,7 @@ export class ExtensionDashboard extends Container {
|
|
|
250
292
|
if (refreshToken !== this.#refreshToken) return;
|
|
251
293
|
this.#state = nextState;
|
|
252
294
|
|
|
253
|
-
//
|
|
295
|
+
// Re-anchor on the same tab id in the (re-sorted) list.
|
|
254
296
|
if (currentTabId) {
|
|
255
297
|
const newIndex = this.#state.tabs.findIndex(t => t.id === currentTabId);
|
|
256
298
|
if (newIndex >= 0) {
|
|
@@ -260,12 +302,11 @@ export class ExtensionDashboard extends Container {
|
|
|
260
302
|
|
|
261
303
|
this.#mainList.setExtensions(this.#state.searchFiltered);
|
|
262
304
|
this.#mainList.setMasterSwitchProvider(this.#getActiveProviderId());
|
|
263
|
-
|
|
264
305
|
if (this.#state.selected) {
|
|
265
306
|
this.#inspector.setExtension(this.#state.selected);
|
|
266
307
|
}
|
|
267
308
|
|
|
268
|
-
this.#
|
|
309
|
+
this.#tabBar.setTabs(buildTabBarTabs(this.#state.tabs), currentTabId);
|
|
269
310
|
this.onRequestRender?.();
|
|
270
311
|
}
|
|
271
312
|
|
|
@@ -275,45 +316,17 @@ export class ExtensionDashboard extends Container {
|
|
|
275
316
|
if (this.#state.selected) {
|
|
276
317
|
this.#inspector.setExtension(this.#state.selected);
|
|
277
318
|
}
|
|
278
|
-
this.#
|
|
319
|
+
this.#tabBar.setTabs(buildTabBarTabs(this.#state.tabs), this.#state.tabs[this.#state.activeTabIndex]?.id);
|
|
279
320
|
this.onRequestRender?.();
|
|
280
321
|
}
|
|
281
322
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
let nextIndex = this.#state.activeTabIndex;
|
|
288
|
-
for (let i = 0; i < numTabs; i++) {
|
|
289
|
-
nextIndex = (nextIndex + direction + numTabs) % numTabs;
|
|
290
|
-
const tab = this.#state.tabs[nextIndex];
|
|
291
|
-
const isEmptyEnabled = tab.count === 0 && tab.enabled && tab.id !== "all";
|
|
292
|
-
if (!isEmptyEnabled) break;
|
|
293
|
-
}
|
|
294
|
-
this.#state.activeTabIndex = nextIndex;
|
|
295
|
-
|
|
296
|
-
// Re-filter for new tab
|
|
297
|
-
const tab = this.#state.tabs[this.#state.activeTabIndex];
|
|
298
|
-
this.#state.tabFiltered = filterByProvider(this.#state.extensions, tab.id);
|
|
299
|
-
this.#state.searchFiltered = applyFilter(this.#state.tabFiltered, this.#state.searchQuery);
|
|
300
|
-
this.#state.listIndex = 0;
|
|
301
|
-
this.#state.scrollOffset = 0;
|
|
302
|
-
this.#state.selected = this.#state.searchFiltered[0] ?? null;
|
|
303
|
-
|
|
304
|
-
// Update list
|
|
305
|
-
this.#mainList.setExtensions(this.#state.searchFiltered);
|
|
306
|
-
this.#mainList.setMasterSwitchProvider(this.#getActiveProviderId());
|
|
307
|
-
this.#mainList.resetSelection();
|
|
308
|
-
|
|
309
|
-
if (this.#state.selected) {
|
|
310
|
-
this.#inspector.setExtension(this.#state.selected);
|
|
323
|
+
handleInput(data: string): void {
|
|
324
|
+
// SGR mouse reports (the fullscreen overlay enables tracking).
|
|
325
|
+
if (data.startsWith("\x1b[<")) {
|
|
326
|
+
this.#handleMouse(data);
|
|
327
|
+
return;
|
|
311
328
|
}
|
|
312
329
|
|
|
313
|
-
this.#buildLayout();
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
handleInput(data: string): void {
|
|
317
330
|
// Ctrl+C - close immediately
|
|
318
331
|
if (matchesKey(data, "ctrl+c")) {
|
|
319
332
|
this.onClose?.();
|
|
@@ -327,56 +340,99 @@ export class ExtensionDashboard extends Container {
|
|
|
327
340
|
this.#state.searchFiltered = this.#state.tabFiltered;
|
|
328
341
|
this.#mainList.setExtensions(this.#state.searchFiltered);
|
|
329
342
|
this.#mainList.clearSearch();
|
|
330
|
-
this
|
|
343
|
+
this.onRequestRender?.();
|
|
331
344
|
return;
|
|
332
345
|
}
|
|
333
346
|
this.onClose?.();
|
|
334
347
|
return;
|
|
335
348
|
}
|
|
336
349
|
|
|
337
|
-
// Tab/Shift+Tab or
|
|
338
|
-
if (
|
|
350
|
+
// Tab/Shift+Tab or ←/→: switch provider tabs (fires onTabChange).
|
|
351
|
+
if (this.#tabBar.handleInput(data)) {
|
|
339
352
|
return;
|
|
340
353
|
}
|
|
341
354
|
|
|
342
|
-
// All other input goes to the list
|
|
355
|
+
// All other input goes to the list.
|
|
343
356
|
this.#mainList.handleInput(data);
|
|
344
357
|
|
|
345
|
-
// Sync search query back to state
|
|
358
|
+
// Sync search query back to state.
|
|
346
359
|
const query = this.#mainList.getSearchQuery();
|
|
347
360
|
if (query !== this.#state.searchQuery) {
|
|
348
361
|
this.#state.searchQuery = query;
|
|
349
362
|
this.#state.searchFiltered = applyFilter(this.#state.tabFiltered, query);
|
|
350
363
|
}
|
|
364
|
+
this.onRequestRender?.();
|
|
351
365
|
}
|
|
352
366
|
}
|
|
353
367
|
|
|
354
368
|
/**
|
|
355
|
-
* Two-column body
|
|
369
|
+
* Two-column body: inventory list on the left, inspector on the right, split by
|
|
370
|
+
* a vertical rule. The inspector is a {@link ScrollView} viewport so long detail
|
|
371
|
+
* panes scroll (wheel) with an auto scrollbar; the left list manages its own
|
|
372
|
+
* windowing. Records the left-column width so the host can hit-test panes.
|
|
356
373
|
*/
|
|
357
374
|
class TwoColumnBody implements Component {
|
|
375
|
+
#maxHeight: number;
|
|
376
|
+
#rightScroll = 0;
|
|
377
|
+
#rightTotal = 0;
|
|
378
|
+
#leftWidth = 0;
|
|
379
|
+
|
|
358
380
|
constructor(
|
|
359
381
|
private readonly leftPane: ExtensionList,
|
|
360
382
|
private readonly rightPane: InspectorPanel,
|
|
361
|
-
|
|
362
|
-
) {
|
|
383
|
+
maxHeight: number,
|
|
384
|
+
) {
|
|
385
|
+
this.#maxHeight = maxHeight;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
setMaxHeight(maxHeight: number): void {
|
|
389
|
+
this.#maxHeight = maxHeight;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Content width of the left (list) column from the last render. */
|
|
393
|
+
get leftWidth(): number {
|
|
394
|
+
return this.#leftWidth;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
resetInspectorScroll(): void {
|
|
398
|
+
this.#rightScroll = 0;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Wheel notch over the inspector pane: scroll its content, clamped. */
|
|
402
|
+
scrollInspector(delta: -1 | 1): void {
|
|
403
|
+
const max = Math.max(0, this.#rightTotal - this.#maxHeight);
|
|
404
|
+
this.#rightScroll = Math.max(0, Math.min(this.#rightScroll + delta, max));
|
|
405
|
+
}
|
|
363
406
|
|
|
364
407
|
render(width: number): readonly string[] {
|
|
365
408
|
const leftWidth = Math.floor(width * 0.5);
|
|
409
|
+
this.#leftWidth = leftWidth;
|
|
366
410
|
const rightWidth = Math.max(0, width - leftWidth - 3);
|
|
411
|
+
const numLines = this.#maxHeight;
|
|
367
412
|
|
|
368
413
|
const leftLines = this.leftPane.render(leftWidth);
|
|
369
414
|
const rightLines = this.rightPane.render(rightWidth);
|
|
415
|
+
this.#rightTotal = rightLines.length;
|
|
416
|
+
const maxScroll = Math.max(0, this.#rightTotal - numLines);
|
|
417
|
+
if (this.#rightScroll > maxScroll) this.#rightScroll = maxScroll;
|
|
418
|
+
|
|
419
|
+
// `totalRows` omitted so the ScrollView windows `rightLines` by the scroll
|
|
420
|
+
// offset (rather than treating them as a pre-windowed slice) and pads short
|
|
421
|
+
// content to exactly `numLines`.
|
|
422
|
+
const rightView = new ScrollView(rightLines, {
|
|
423
|
+
height: numLines,
|
|
424
|
+
scrollbar: "auto",
|
|
425
|
+
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
426
|
+
});
|
|
427
|
+
rightView.setScrollOffset(this.#rightScroll);
|
|
428
|
+
const rightRendered = rightView.render(rightWidth);
|
|
370
429
|
|
|
371
|
-
// Fill the full body height so the dashboard reads as a full-screen view.
|
|
372
|
-
const numLines = this.maxHeight;
|
|
373
430
|
const combined: string[] = [];
|
|
374
431
|
const separator = theme.fg("dim", ` ${theme.boxRound.vertical} `);
|
|
375
|
-
|
|
376
432
|
for (let i = 0; i < numLines; i++) {
|
|
377
433
|
const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
|
|
378
434
|
const leftPadded = left + padding(Math.max(0, leftWidth - visibleWidth(left)));
|
|
379
|
-
const right =
|
|
435
|
+
const right = rightRendered[i] ?? "";
|
|
380
436
|
combined.push(leftPadded + separator + right);
|
|
381
437
|
}
|
|
382
438
|
|