pi-cliproxyapi 0.2.0 → 0.3.0

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/src/ui-overlay.ts DELETED
@@ -1,235 +0,0 @@
1
- // Read-only scrollable overlay used by /cliproxy-usage and /cliproxy-doctor.
2
- //
3
- // Pure presentational — never touches the agent session or context. The
4
- // caller may supply boolean "toggles" (one keystroke each) that re-render the
5
- // body when flipped. The overlay shell handles scrolling and dismissal.
6
- //
7
- // Built-in keys:
8
- // ↑ / k scroll up one line (held = repeat via Kitty kbd protocol)
9
- // ↓ / j scroll down one line
10
- // PageUp / b scroll up one page
11
- // PageDown / f scroll down one page (also Space)
12
- // Home / g jump to top
13
- // End / G jump to bottom
14
- // Esc / q / Enter / Ctrl+C close
15
- //
16
- // Plus any caller-defined toggles.
17
-
18
- import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
19
- import {
20
- type Component,
21
- getKeybindings,
22
- matchesKey,
23
- visibleWidth,
24
- } from "@earendil-works/pi-tui";
25
-
26
- import { frame } from "./ui-frame.ts";
27
-
28
- interface Theme {
29
- fg(name: string, s: string): string;
30
- bold(s: string): string;
31
- }
32
-
33
- interface OverlayTui {
34
- requestRender(): void;
35
- rows?: number;
36
- cols?: number;
37
- }
38
-
39
- export interface OverlayToggle {
40
- /** Single key id passed to matchesKey() (e.g. "d", "v"). */
41
- key: string;
42
- /** Short hint shown in the footer (e.g. "d disabled"). */
43
- hint: string;
44
- /** Initial state. Defaults to false. */
45
- initial?: boolean;
46
- }
47
-
48
- export interface OverlayOptions {
49
- title: string;
50
- /** Re-rendered every time a toggle flips. Receives current toggle state. */
51
- render: (state: Record<string, boolean>) => string;
52
- toggles?: OverlayToggle[];
53
- }
54
-
55
- interface BuildProps extends OverlayOptions {
56
- done: (value: void) => void;
57
- theme: Theme;
58
- }
59
-
60
- function buildOverlay(
61
- tui: OverlayTui,
62
- props: BuildProps,
63
- ): Component & { handleInput(data: string): void } {
64
- const state: Record<string, boolean> = {};
65
- for (const t of props.toggles ?? []) state[t.key] = t.initial ?? false;
66
- let lines: string[] = props.render(state).split("\n");
67
- let offset = 0;
68
- let lastRenderHeight = 20;
69
- let lastRenderWidth = 80;
70
-
71
- const t = props.theme;
72
-
73
- const recompute = (): void => {
74
- lines = props.render(state).split("\n");
75
- const visible = Math.max(1, lastRenderHeight - 2);
76
- const max = Math.max(0, lines.length - visible);
77
- if (offset > max) offset = max;
78
- };
79
-
80
- const renderFrame = (width: number, height: number): string[] => {
81
- const visible = Math.max(1, height - 2);
82
- const total = lines.length;
83
- const maxOffset = Math.max(0, total - visible);
84
- if (offset > maxOffset) offset = maxOffset;
85
- const slice = lines.slice(offset, offset + visible);
86
- while (slice.length < visible) slice.push("");
87
- const pct =
88
- total > visible
89
- ? Math.min(100, Math.round(((offset + visible) / total) * 100))
90
- : 100;
91
- const hintBase = "\u2191\u2193 \u00b7 pgUp/pgDn \u00b7 g/G";
92
- const togglesText = (props.toggles ?? [])
93
- .map((tg) =>
94
- state[tg.key] === true
95
- ? t.fg("success", `[${tg.hint}]`)
96
- : t.fg("dim", tg.hint),
97
- )
98
- .join(" \u00b7 ");
99
- const hint = ` ${hintBase}${togglesText ? " " + togglesText : ""} `;
100
- const badge = ` ${offset + 1}\u2013${Math.min(total, offset + visible)} of ${total} ${pct}% `;
101
- return frame(t, {
102
- width,
103
- title: ` ${props.title} `,
104
- lines: slice.map((ln) => ` ${ln}`),
105
- footer: { hint, badge },
106
- });
107
- };
108
-
109
- return {
110
- render(width: number): string[] {
111
- lastRenderWidth = width;
112
- lastRenderHeight = Math.max(
113
- 10,
114
- Math.min(lines.length + 2, (tui.rows ?? 40) - 6),
115
- );
116
- return renderFrame(lastRenderWidth, lastRenderHeight);
117
- },
118
- invalidate(): void {
119
- /* stateless render */
120
- },
121
- handleInput(data: string): void {
122
- const visible = Math.max(1, lastRenderHeight - 2);
123
- const max = Math.max(0, lines.length - visible);
124
- const kb = getKeybindings();
125
-
126
- // Caller toggles run first so they shadow letter-key scroll bindings.
127
- for (const tg of props.toggles ?? []) {
128
- if (matchesKey(data, tg.key as never)) {
129
- state[tg.key] = !state[tg.key];
130
- recompute();
131
- tui.requestRender();
132
- return;
133
- }
134
- }
135
-
136
- if (
137
- kb.matches(data, "tui.select.cancel") ||
138
- matchesKey(data, "q") ||
139
- matchesKey(data, "shift+q") ||
140
- matchesKey(data, "enter") ||
141
- matchesKey(data, "return")
142
- ) {
143
- props.done();
144
- return;
145
- }
146
- let next = offset;
147
- if (matchesKey(data, "up") || matchesKey(data, "k")) {
148
- next = Math.max(0, offset - 1);
149
- } else if (matchesKey(data, "down") || matchesKey(data, "j")) {
150
- next = Math.min(max, offset + 1);
151
- } else if (matchesKey(data, "pageUp") || matchesKey(data, "b")) {
152
- next = Math.max(0, offset - visible);
153
- } else if (
154
- matchesKey(data, "pageDown") ||
155
- matchesKey(data, "f") ||
156
- matchesKey(data, "space")
157
- ) {
158
- next = Math.min(max, offset + visible);
159
- } else if (matchesKey(data, "home") || matchesKey(data, "g")) {
160
- next = 0;
161
- } else if (matchesKey(data, "end") || matchesKey(data, "shift+g")) {
162
- next = max;
163
- } else {
164
- return;
165
- }
166
- if (next !== offset) {
167
- offset = next;
168
- tui.requestRender();
169
- }
170
- },
171
- };
172
- }
173
-
174
- // --------------------------------------------------------------------------- public API
175
-
176
- /**
177
- * Show a scrollable overlay with optional toggles.
178
- *
179
- * If you don't need toggles, pass a string for the body and we'll wrap it.
180
- */
181
- export async function showOverlay(
182
- ctx: ExtensionCommandContext,
183
- title: string,
184
- bodyOrOptions: string | Omit<OverlayOptions, "title">,
185
- ): Promise<void> {
186
- const opts: OverlayOptions =
187
- typeof bodyOrOptions === "string"
188
- ? { title, render: () => bodyOrOptions }
189
- : { title, ...bodyOrOptions };
190
-
191
- if (!ctx.hasUI) {
192
- ctx.ui.notify(`${title}\n\n${opts.render({})}`, "info");
193
- return;
194
- }
195
-
196
- // Probe widest line across all toggle combinations so the overlay doesn't
197
- // resize as the user flips switches. With N toggles we check 2^N states,
198
- // but in practice we only ever pass 0-2 toggles.
199
- const probedWidth = probeMaxWidth(opts);
200
- const desiredCols = Math.min(140, Math.max(72, probedWidth + 6));
201
-
202
- await ctx.ui.custom<void>(
203
- (tui, theme, _kb, done) =>
204
- buildOverlay(tui as unknown as OverlayTui, {
205
- ...opts,
206
- done,
207
- theme: theme as unknown as Theme,
208
- }),
209
- {
210
- overlay: true,
211
- overlayOptions: {
212
- width: desiredCols,
213
- maxHeight: "80%",
214
- },
215
- },
216
- );
217
- }
218
-
219
- function probeMaxWidth(opts: OverlayOptions): number {
220
- const toggles = opts.toggles ?? [];
221
- const combos = 1 << toggles.length;
222
- let max = 0;
223
- for (let i = 0; i < combos; i++) {
224
- const state: Record<string, boolean> = {};
225
- toggles.forEach((tg, j) => {
226
- state[tg.key] = (i & (1 << j)) !== 0;
227
- });
228
- const body = opts.render(state);
229
- for (const ln of body.split("\n")) {
230
- const w = visibleWidth(ln);
231
- if (w > max) max = w;
232
- }
233
- }
234
- return max;
235
- }
@@ -1,35 +0,0 @@
1
- // Public entry for the /cliproxy three-panel picker.
2
-
3
- import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
4
-
5
- import type { ProxyConfig } from "../config.ts";
6
- import type { Discovery } from "../fetch-models.ts";
7
- import { buildPicker } from "./picker.ts";
8
- import type { OverlayTui, Theme } from "./types.ts";
9
-
10
- export async function runPicker(
11
- ctx: ExtensionCommandContext,
12
- cfg: ProxyConfig,
13
- discovery: Discovery,
14
- ): Promise<ProxyConfig | null> {
15
- if (!ctx.hasUI) {
16
- ctx.ui.notify("interactive UI required for /cliproxy", "warning");
17
- return null;
18
- }
19
- const draft: ProxyConfig = structuredClone(cfg);
20
- return ctx.ui.custom<ProxyConfig | null>(
21
- (tui, theme, _kb, done) =>
22
- buildPicker(
23
- tui as unknown as OverlayTui,
24
- theme as unknown as Theme,
25
- draft,
26
- discovery,
27
- ctx,
28
- done,
29
- ),
30
- {
31
- overlay: true,
32
- overlayOptions: { width: 170, maxHeight: "94%" },
33
- },
34
- );
35
- }
@@ -1,432 +0,0 @@
1
- // Render + input dispatch for the three-panel picker. Kept separate from
2
- // picker.ts so each file stays under ~300 lines.
3
-
4
- import type { Component } from "@earendil-works/pi-tui";
5
-
6
- import type { ProxyConfig } from "../config.ts";
7
- import { frame, type FrameFooter } from "../ui-frame.ts";
8
- import { pad } from "./render-text.ts";
9
- import {
10
- renderEmpty,
11
- renderModelRow,
12
- renderNewProviderRow,
13
- renderPanelHeader,
14
- renderProviderRow,
15
- renderSubheader,
16
- } from "./rows.ts";
17
- import type {
18
- CatalogIndex,
19
- OverlayTui,
20
- PanelId,
21
- ProviderEntry,
22
- Theme,
23
- } from "./types.ts";
24
- import {
25
- visibleWidth,
26
- matchesKey,
27
- getKeybindings,
28
- } from "@earendil-works/pi-tui";
29
-
30
- export interface AssembleArgs {
31
- tui: OverlayTui;
32
- theme: Theme;
33
- cfg: ProxyConfig;
34
- readOnly?: never;
35
- opts?: never;
36
- catalog: CatalogIndex;
37
- getProviders: () => ProviderEntry[];
38
- getFocus: () => PanelId;
39
- setFocus: (f: PanelId) => void;
40
- getProviderCursor: () => number;
41
- getAssignedCursor: () => number;
42
- getPoolCursor: () => number;
43
- getProviderScroll: () => number;
44
- setProviderScroll: (v: number) => void;
45
- getAssignedScroll: () => number;
46
- setAssignedScroll: (v: number) => void;
47
- getPoolScroll: () => number;
48
- setPoolScroll: (v: number) => void;
49
- selectedProvider: () => ProviderEntry | null;
50
- moveCursor: (delta: number) => void;
51
- onTab: (back: boolean) => void;
52
- onActivate: () => Promise<void>;
53
- onDelete: () => Promise<void>;
54
- ensureVisible: (cursor: number, scroll: number, visible: number) => number;
55
- finish: (result: ProxyConfig | null) => void;
56
- poolGrouper: (ids: string[]) => Array<{ label: string; ids: string[] }>;
57
- assignedIdsFor: (p: ProviderEntry) => string[];
58
- poolFor: (p: ProviderEntry) => string[];
59
- }
60
-
61
- export function assembleComponent(
62
- a: AssembleArgs,
63
- ): Component & { handleInput(data: string): void } {
64
- const render = (width: number): string[] => render3Panel(a, width);
65
- return {
66
- render(width: number): string[] {
67
- return render(width);
68
- },
69
- invalidate(): void {
70
- /* stateless */
71
- },
72
- handleInput(data: string): void {
73
- handleInput(a, data);
74
- },
75
- };
76
- }
77
-
78
- // --------------------------------------------------------------------------- render
79
-
80
- function render3Panel(a: AssembleArgs, width: number): string[] {
81
- const { theme, tui, cfg, catalog } = a;
82
- const totalRows = tui.rows ?? 40;
83
- const height = Math.max(16, Math.min(totalRows - 6, 38));
84
- // frame() adds its own outer "│ … │" walls and accounts for `width` cells
85
- // total. So our body lines must be exactly `width - 2` cells wide.
86
- const inner = Math.max(70, width - 2);
87
- const leftW = Math.min(56, Math.max(34, Math.floor(inner * 0.4)));
88
- const rightW = inner - leftW - 1; // -1 for the vertical splitter
89
- const upperH = Math.max(5, Math.floor((height - 2) / 2));
90
- const lowerH = height - 2 - upperH;
91
-
92
- const title = " /cliproxy ";
93
-
94
- const prov = a.selectedProvider();
95
- const focus = a.getFocus();
96
-
97
- // Track each panel's cursor line + the run of context lines (subheaders /
98
- // panel-headers / blank empties) immediately above it. The context block
99
- // must stay visible together with the cursor so groups don't "jump" past
100
- // invisible owned_by labels.
101
- let leftCursorLine = 0;
102
- let leftCursorTop = 0;
103
- let assignedCursorLine = 0;
104
- let assignedCursorTop = 0;
105
- let poolCursorLine = 0;
106
- let poolCursorTop = 0;
107
-
108
- // LEFT
109
- const leftLines: string[] = [];
110
- leftLines.push(
111
- panelHeaderBar(theme, " providers ", leftW, focus === "providers"),
112
- );
113
- const providers = a.getProviders();
114
- for (let i = 0; i < providers.length; i++) {
115
- const p = providers[i]!;
116
- const isCursor = focus === "providers" && i === a.getProviderCursor();
117
- if (isCursor) {
118
- leftCursorLine = leftLines.length;
119
- leftCursorTop = leftCursorLine;
120
- }
121
- leftLines.push(
122
- renderProviderRow(p, cfg, {
123
- theme,
124
- width: leftW,
125
- isCursor,
126
- isFocused: focus === "providers",
127
- }),
128
- );
129
- }
130
- {
131
- const isCursor =
132
- focus === "providers" && a.getProviderCursor() === providers.length;
133
- if (isCursor) {
134
- leftCursorLine = leftLines.length;
135
- leftCursorTop = leftCursorLine;
136
- }
137
- leftLines.push(
138
- renderNewProviderRow(theme, isCursor, focus === "providers", leftW),
139
- );
140
- }
141
-
142
- // RIGHT TOP (assigned)
143
- const assignedLines: string[] = [];
144
- const assignedHeader = prov
145
- ? `assigned to ${theme.bold(prov.name)} ${theme.fg("dim", `\u00b7 ${prov.api}`)}`
146
- : theme.fg("dim", "no provider selected");
147
- assignedLines.push(
148
- panelHeaderBar(theme, " assigned ", rightW, focus === "assigned"),
149
- );
150
- assignedLines.push(
151
- renderPanelHeader(theme, assignedHeader, rightW, focus === "assigned"),
152
- );
153
- if (prov) {
154
- const ids = a.assignedIdsFor(prov);
155
- if (ids.length === 0) {
156
- assignedLines.push(renderEmpty(theme, "(nothing assigned yet)", rightW));
157
- }
158
- for (let i = 0; i < ids.length; i++) {
159
- const id = ids[i]!;
160
- const m = catalog.byId.get(id);
161
- const isCursor = focus === "assigned" && i === a.getAssignedCursor();
162
- if (isCursor) {
163
- assignedCursorLine = assignedLines.length;
164
- assignedCursorTop = assignedCursorLine;
165
- }
166
- const compatWarn = m ? !a.apiCompatible(prov.api, m.suggestedApi) : false;
167
- assignedLines.push(
168
- renderModelRow(id, m, "assigned", compatWarn, {
169
- theme,
170
- width: rightW,
171
- isCursor,
172
- isFocused: focus === "assigned",
173
- }),
174
- );
175
- }
176
- }
177
-
178
- // RIGHT BOTTOM (pool, grouped by ownedBy)
179
- const poolLines: string[] = [];
180
- poolLines.push(
181
- panelHeaderBar(theme, " available pool ", rightW, focus === "pool"),
182
- );
183
- if (prov) {
184
- const ids = a.poolFor(prov);
185
- if (ids.length === 0) {
186
- poolLines.push(renderEmpty(theme, "(no models available)", rightW));
187
- } else {
188
- let cursorIdx = 0;
189
- for (const grp of a.poolGrouper(ids)) {
190
- const groupHeaderLine = poolLines.length;
191
- poolLines.push(renderSubheader(theme, grp.label, rightW));
192
- for (let gi = 0; gi < grp.ids.length; gi++) {
193
- const id = grp.ids[gi]!;
194
- const m = catalog.byId.get(id);
195
- const isCursor = focus === "pool" && cursorIdx === a.getPoolCursor();
196
- if (isCursor) {
197
- poolCursorLine = poolLines.length;
198
- // First model of a group — pin the owned_by subheader too.
199
- poolCursorTop = gi === 0 ? groupHeaderLine : poolCursorLine;
200
- }
201
- const compatWarn = m
202
- ? !a.apiCompatible(prov.api, m.suggestedApi)
203
- : false;
204
- poolLines.push(
205
- renderModelRow(id, m, "pool", compatWarn, {
206
- theme,
207
- width: rightW,
208
- isCursor,
209
- isFocused: focus === "pool",
210
- }),
211
- );
212
- cursorIdx++;
213
- }
214
- }
215
- }
216
- }
217
-
218
- // Scroll math. Each panel has a sticky panel-header at index 0; assigned
219
- // has a second sticky line for "assigned to <prov>". We keep those locked
220
- // at the top and only scroll the body rows below them. scrollFor() also
221
- // clamps so we never push the cursor past the visible area or leave a gap
222
- // when scrolling back to the top of a long list.
223
- const poolUsableH = Math.max(1, lowerH - 1); // -1 for the horizontal divider
224
- const leftScroll = scrollFor(
225
- leftCursorTop,
226
- leftCursorLine,
227
- a.getProviderScroll(),
228
- height - 2,
229
- leftLines.length,
230
- 1,
231
- );
232
- const aScroll = scrollFor(
233
- focus === "assigned" ? assignedCursorTop : 0,
234
- focus === "assigned" ? assignedCursorLine : 0,
235
- a.getAssignedScroll(),
236
- upperH,
237
- assignedLines.length,
238
- 2,
239
- );
240
- const pScroll = scrollFor(
241
- focus === "pool" ? poolCursorTop : 0,
242
- focus === "pool" ? poolCursorLine : 0,
243
- a.getPoolScroll(),
244
- poolUsableH,
245
- poolLines.length,
246
- 1,
247
- );
248
- a.setProviderScroll(leftScroll);
249
- a.setAssignedScroll(aScroll);
250
- a.setPoolScroll(pScroll);
251
-
252
- const leftSlice = takeSlice(leftLines, leftScroll, height - 2);
253
- const aSlice = takeSlice(assignedLines, aScroll, upperH);
254
- const pSlice = takeSlice(poolLines, pScroll, lowerH);
255
-
256
- const vsplit = theme.fg("borderAccent", "\u2502");
257
- const bodyLines: string[] = [];
258
- for (let i = 0; i < height - 2; i++) {
259
- const l = pad(leftSlice[i] ?? "", leftW);
260
- const rRaw = i < upperH ? aSlice[i]! : pSlice[i - upperH]!;
261
- const r = pad(rRaw, rightW);
262
- bodyLines.push(`${l}${vsplit}${r}`);
263
- }
264
- const divIdx = upperH;
265
- if (divIdx > 0 && divIdx < height - 2) {
266
- const left = pad(leftSlice[divIdx] ?? "", leftW);
267
- const horiz = theme.fg("borderAccent", "\u2500".repeat(rightW));
268
- bodyLines[divIdx] = `${left}${vsplit}${horiz}`;
269
- }
270
-
271
- return frame(theme, {
272
- width,
273
- title,
274
- lines: bodyLines,
275
- footer: footerFor(focus),
276
- });
277
- }
278
-
279
- function takeSlice(lines: string[], scroll: number, count: number): string[] {
280
- const s = lines.slice(scroll, scroll + count);
281
- while (s.length < count) s.push("");
282
- return s;
283
- }
284
-
285
- /**
286
- * Scroll a list so the cursor line stays visible inside `visible` rows.
287
- *
288
- * Two cursor coords:
289
- * - cursorTop: the highest line that must remain visible (usually a group
290
- * subheader sitting right above the cursor on the first row of a group).
291
- * - cursorBottom: the cursor row itself.
292
- *
293
- * Constraints:
294
- * - Lines [0..stickyCount) are panel headers we want pinned at the top, so
295
- * `scroll` is at most `total - visible` AND at most `cursorTop -
296
- * stickyCount` when the cursor block is below them.
297
- * - When the block fits entirely inside the visible window we keep `prev`
298
- * to avoid jitter.
299
- * - We never leave a gap of empty rows above (the "3 missing rows when
300
- * scrolling back up" bug) or push the cursor below the panel (the
301
- * "cursor goes 3 lines below" bug).
302
- */
303
- function scrollFor(
304
- cursorTop: number,
305
- cursorBottom: number,
306
- prev: number,
307
- visible: number,
308
- total: number,
309
- stickyCount: number,
310
- ): number {
311
- if (total <= visible) return 0;
312
- const maxScroll = Math.max(0, total - visible);
313
- if (cursorBottom < stickyCount) return 0;
314
- let scroll = Math.max(0, Math.min(prev, maxScroll));
315
- const topRow = scroll + stickyCount;
316
- if (cursorTop < topRow) {
317
- scroll = Math.max(0, cursorTop - stickyCount);
318
- }
319
- const bottomRow = scroll + visible - 1;
320
- if (cursorBottom > bottomRow) {
321
- scroll = Math.min(maxScroll, cursorBottom - (visible - 1));
322
- }
323
- return scroll;
324
- }
325
-
326
- function panelHeaderBar(
327
- theme: Theme,
328
- label: string,
329
- width: number,
330
- isFocused: boolean,
331
- ): string {
332
- const bar = isFocused
333
- ? theme.bold(theme.fg("accent", label))
334
- : theme.fg("muted", label);
335
- const fill = theme.fg(
336
- "borderAccent",
337
- "\u2500".repeat(Math.max(0, width - visibleWidth(label) - 1)),
338
- );
339
- return pad(`${bar}${fill}`, width);
340
- }
341
-
342
- function footerFor(focus: PanelId): FrameFooter {
343
- const hint =
344
- " tab \u2194 panel \u2191\u2193 nav \u21b5/space move d remove group s save q cancel ";
345
- return { hint, badge: ` focus: ${focus} ` };
346
- }
347
-
348
- // --------------------------------------------------------------------------- input
349
-
350
- function handleInput(a: AssembleArgs, data: string): void {
351
- const { tui, finish, cfg } = a;
352
- const kb = getKeybindings();
353
- if (
354
- kb.matches(data, "tui.select.cancel") ||
355
- matchesKey(data, "q") ||
356
- matchesKey(data, "shift+q") ||
357
- matchesKey(data, "escape")
358
- ) {
359
- finish(null);
360
- return;
361
- }
362
- if (matchesKey(data, "tab")) {
363
- a.onTab(false);
364
- tui.requestRender();
365
- return;
366
- }
367
- if (matchesKey(data, "shift+tab")) {
368
- a.onTab(true);
369
- tui.requestRender();
370
- return;
371
- }
372
- if (matchesKey(data, "s")) {
373
- finish(cfg);
374
- return;
375
- }
376
- if (
377
- matchesKey(data, "enter") ||
378
- matchesKey(data, "return") ||
379
- matchesKey(data, "space")
380
- ) {
381
- void a.onActivate().then(() => tui.requestRender());
382
- return;
383
- }
384
- if (
385
- matchesKey(data, "d") ||
386
- matchesKey(data, "delete") ||
387
- matchesKey(data, "backspace")
388
- ) {
389
- void a.onDelete();
390
- return;
391
- }
392
- if (matchesKey(data, "up") || matchesKey(data, "k")) {
393
- a.moveCursor(-1);
394
- tui.requestRender();
395
- return;
396
- }
397
- if (matchesKey(data, "down") || matchesKey(data, "j")) {
398
- a.moveCursor(1);
399
- tui.requestRender();
400
- return;
401
- }
402
- if (matchesKey(data, "pageUp") || matchesKey(data, "b")) {
403
- a.moveCursor(-8);
404
- tui.requestRender();
405
- return;
406
- }
407
- if (matchesKey(data, "pageDown") || matchesKey(data, "f")) {
408
- a.moveCursor(8);
409
- tui.requestRender();
410
- return;
411
- }
412
- if (matchesKey(data, "left") || matchesKey(data, "h")) {
413
- a.setFocus("providers");
414
- tui.requestRender();
415
- return;
416
- }
417
- if (matchesKey(data, "right") || matchesKey(data, "l")) {
418
- const f = a.getFocus();
419
- if (f === "providers") a.setFocus("assigned");
420
- else if (f === "assigned") a.setFocus("pool");
421
- tui.requestRender();
422
- return;
423
- }
424
- }
425
-
426
- // Provide a tiny apiCompatible binding so we can call it from render.
427
- // The grouping/api functions are passed in directly via AssembleArgs.
428
- declare module "./picker-component.ts" {
429
- interface AssembleArgs {
430
- apiCompatible: (provApi: string, modelApi: string) => boolean;
431
- }
432
- }