@oh-my-pi/pi-coding-agent 15.7.5 → 15.8.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/CHANGELOG.md +159 -182
- package/dist/types/async/job-manager.d.ts +3 -3
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/claude-trace-cli.d.ts +54 -0
- package/dist/types/cli/session-picker.d.ts +10 -3
- package/dist/types/cli/update-cli.d.ts +17 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +11 -2
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/eval/__tests__/heartbeat.test.d.ts +1 -0
- package/dist/types/eval/concurrency-bridge.d.ts +26 -0
- package/dist/types/eval/heartbeat.d.ts +45 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +24 -5
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +11 -0
- package/dist/types/lsp/diagnostics-ledger.d.ts +10 -0
- package/dist/types/lsp/index.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +4 -0
- package/dist/types/main.d.ts +5 -0
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/custom-editor.d.ts +3 -2
- package/dist/types/modes/components/hook-selector.d.ts +10 -1
- package/dist/types/modes/components/session-selector.d.ts +32 -5
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -3
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -3
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +5 -3
- package/dist/types/registry/agent-registry.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +12 -3
- package/dist/types/session/history-storage.d.ts +16 -1
- package/dist/types/session/session-manager.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +6 -15
- package/dist/types/tools/ask.d.ts +8 -6
- package/dist/types/tools/eval-backends.d.ts +12 -0
- package/dist/types/tools/eval-render.d.ts +52 -0
- package/dist/types/tools/eval.d.ts +2 -35
- package/dist/types/tools/find.d.ts +0 -9
- package/dist/types/tools/index.d.ts +5 -12
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/sqlite-reader.d.ts +25 -8
- package/dist/types/tui/output-block.d.ts +4 -3
- package/dist/types/utils/clipboard.d.ts +4 -0
- package/dist/types/web/kagi.d.ts +76 -0
- package/dist/types/web/search/providers/exa.d.ts +7 -1
- package/dist/types/web/search/providers/kagi.d.ts +1 -0
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/thinking-note.ts +13 -0
- package/package.json +9 -9
- package/src/async/job-manager.ts +3 -3
- package/src/cli/args.ts +6 -2
- package/src/cli/claude-trace-cli.ts +783 -0
- package/src/cli/session-picker.ts +36 -10
- package/src/cli/update-cli.ts +35 -2
- package/src/commands/launch.ts +3 -0
- package/src/config/keybindings.ts +6 -0
- package/src/config/model-registry.ts +33 -4
- package/src/config/settings-schema.ts +12 -2
- package/src/config/settings.ts +23 -0
- package/src/discovery/claude-plugins.ts +7 -9
- package/src/discovery/claude.ts +41 -22
- package/src/edit/index.ts +23 -3
- package/src/eval/__tests__/agent-bridge.test.ts +148 -4
- package/src/eval/__tests__/heartbeat.test.ts +84 -0
- package/src/eval/__tests__/llm-bridge.test.ts +30 -0
- package/src/eval/agent-bridge.ts +44 -38
- package/src/eval/concurrency-bridge.ts +34 -0
- package/src/eval/heartbeat.ts +74 -0
- package/src/eval/js/executor.ts +13 -9
- package/src/eval/js/shared/prelude.txt +20 -17
- package/src/eval/js/tool-bridge.ts +5 -0
- package/src/eval/llm-bridge.ts +20 -14
- package/src/eval/py/executor.ts +14 -18
- package/src/eval/py/prelude.py +23 -15
- package/src/exec/bash-executor.ts +31 -5
- package/src/extensibility/extensions/loader.ts +16 -18
- package/src/extensibility/extensions/runner.ts +22 -17
- package/src/extensibility/extensions/types.ts +39 -5
- package/src/extensibility/plugins/legacy-pi-compat.ts +115 -131
- package/src/extensibility/skills.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/lsp/diagnostics-ledger.ts +51 -0
- package/src/lsp/index.ts +9 -22
- package/src/lsp/utils.ts +21 -0
- package/src/main.ts +92 -24
- package/src/modes/acp/acp-agent.ts +8 -4
- package/src/modes/acp/acp-event-mapper.ts +54 -4
- package/src/modes/components/assistant-message.ts +28 -1
- package/src/modes/components/custom-editor.ts +19 -7
- package/src/modes/components/hook-selector.ts +229 -44
- package/src/modes/components/oauth-selector.ts +12 -6
- package/src/modes/components/session-selector.ts +179 -24
- package/src/modes/components/tool-execution.ts +36 -7
- package/src/modes/controllers/command-controller.ts +2 -11
- package/src/modes/controllers/event-controller.ts +5 -2
- package/src/modes/controllers/extension-ui-controller.ts +6 -4
- package/src/modes/controllers/input-controller.ts +19 -16
- package/src/modes/controllers/selector-controller.ts +61 -21
- package/src/modes/interactive-mode.ts +127 -16
- package/src/modes/rpc/rpc-mode.ts +17 -6
- package/src/modes/theme/theme-schema.json +30 -0
- package/src/modes/theme/theme.ts +39 -2
- package/src/modes/types.ts +7 -3
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/system/orchestrate-notice.md +5 -3
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/ask.md +2 -1
- package/src/prompts/tools/eval.md +6 -6
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/irc.md +6 -6
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -1
- package/src/registry/agent-registry.ts +1 -1
- package/src/sdk.ts +85 -31
- package/src/session/agent-session.ts +127 -57
- package/src/session/history-storage.ts +56 -12
- package/src/session/session-manager.ts +34 -0
- package/src/task/output-manager.ts +40 -48
- package/src/task/render.ts +3 -8
- package/src/tools/ask.ts +74 -32
- package/src/tools/browser/tab-worker.ts +8 -5
- package/src/tools/eval-backends.ts +38 -0
- package/src/tools/eval-render.ts +750 -0
- package/src/tools/eval.ts +27 -754
- package/src/tools/find.ts +5 -29
- package/src/tools/index.ts +8 -38
- package/src/tools/path-utils.ts +144 -1
- package/src/tools/read.ts +47 -0
- package/src/tools/renderers.ts +1 -1
- package/src/tools/search.ts +2 -27
- package/src/tools/sqlite-reader.ts +92 -9
- package/src/tools/write.ts +9 -1
- package/src/tui/output-block.ts +5 -4
- package/src/utils/clipboard.ts +38 -1
- package/src/utils/open.ts +37 -2
- package/src/web/kagi.ts +168 -49
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/exa.ts +20 -86
- package/src/web/search/providers/kagi.ts +4 -0
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
extractPrintableText,
|
|
8
8
|
fuzzyFilter,
|
|
9
9
|
Markdown,
|
|
10
|
+
type MarkdownTheme,
|
|
10
11
|
matchesKey,
|
|
11
12
|
padding,
|
|
12
13
|
renderInlineMarkdown,
|
|
@@ -14,8 +15,8 @@ import {
|
|
|
14
15
|
Spacer,
|
|
15
16
|
Text,
|
|
16
17
|
type TUI,
|
|
17
|
-
truncateToWidth,
|
|
18
18
|
visibleWidth,
|
|
19
|
+
wrapTextWithAnsi,
|
|
19
20
|
} from "@oh-my-pi/pi-tui";
|
|
20
21
|
import { getMarkdownTheme, type ThemeColor, theme } from "../../modes/theme/theme";
|
|
21
22
|
import {
|
|
@@ -67,6 +68,37 @@ export interface HookSelectorOptions {
|
|
|
67
68
|
onExternalEditor?: () => void;
|
|
68
69
|
helpText?: string;
|
|
69
70
|
slider?: HookSelectorSlider;
|
|
71
|
+
/** Indices into the original options that cannot be selected: they render
|
|
72
|
+
* dimmed, are skipped during navigation, and reject enter/timeout. */
|
|
73
|
+
disabledIndices?: readonly number[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface HookSelectorOption {
|
|
77
|
+
label: string;
|
|
78
|
+
description?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type HookSelectorOptionInput = string | HookSelectorOption;
|
|
82
|
+
|
|
83
|
+
function normalizeHookSelectorOption(option: HookSelectorOptionInput): HookSelectorOption {
|
|
84
|
+
if (typeof option === "string") return { label: option };
|
|
85
|
+
if (option.description?.trim()) {
|
|
86
|
+
return { label: option.label, description: option.description.trim() };
|
|
87
|
+
}
|
|
88
|
+
return { label: option.label };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function splitLeadingSpacesForWrap(line: string, width: number): { indent: string; body: string } {
|
|
92
|
+
let indentLength = 0;
|
|
93
|
+
while (indentLength < line.length && line.charCodeAt(indentLength) === 32) {
|
|
94
|
+
indentLength += 1;
|
|
95
|
+
}
|
|
96
|
+
const maxIndentLength = Math.max(0, width - 1);
|
|
97
|
+
const clampedIndentLength = Math.min(indentLength, maxIndentLength);
|
|
98
|
+
return {
|
|
99
|
+
indent: line.slice(0, clampedIndentLength),
|
|
100
|
+
body: line.slice(indentLength),
|
|
101
|
+
};
|
|
70
102
|
}
|
|
71
103
|
|
|
72
104
|
class OutlinedList extends Container {
|
|
@@ -81,21 +113,33 @@ class OutlinedList extends Container {
|
|
|
81
113
|
const borderColor = (text: string) => theme.fg("border", text);
|
|
82
114
|
const horizontal = borderColor(theme.boxSharp.horizontal.repeat(Math.max(1, width)));
|
|
83
115
|
const innerWidth = Math.max(1, width - 2);
|
|
84
|
-
const content =
|
|
116
|
+
const content: string[] = [];
|
|
117
|
+
for (const line of this.#lines) {
|
|
85
118
|
const normalized = replaceTabs(line);
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
119
|
+
const { indent, body } = splitLeadingSpacesForWrap(normalized, innerWidth);
|
|
120
|
+
const wrapped = wrapTextWithAnsi(body, Math.max(1, innerWidth - visibleWidth(indent)));
|
|
121
|
+
for (const wrappedBody of wrapped.length > 0 ? wrapped : [""]) {
|
|
122
|
+
const wrappedLine = `${indent}${wrappedBody}`;
|
|
123
|
+
const pad = Math.max(0, innerWidth - visibleWidth(wrappedLine));
|
|
124
|
+
content.push(
|
|
125
|
+
`${borderColor(theme.boxSharp.vertical)}${wrappedLine}${padding(pad)}${borderColor(theme.boxSharp.vertical)}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
90
129
|
return [horizontal, ...content, horizontal];
|
|
91
130
|
}
|
|
92
131
|
}
|
|
93
132
|
|
|
133
|
+
/** A filtered option paired with its index into the original options array, so
|
|
134
|
+
* disabled-index lookups survive fuzzy filtering and reordering. */
|
|
135
|
+
type FilteredOption = { option: HookSelectorOption; index: number };
|
|
136
|
+
|
|
94
137
|
export class HookSelectorComponent extends Container {
|
|
95
|
-
#options:
|
|
96
|
-
#filteredOptions:
|
|
138
|
+
#options: HookSelectorOption[];
|
|
139
|
+
#filteredOptions: FilteredOption[];
|
|
97
140
|
#searchQuery = "";
|
|
98
141
|
#selectedIndex: number;
|
|
142
|
+
#disabledIndices: Set<number>;
|
|
99
143
|
#maxVisible: number;
|
|
100
144
|
#listContainer: Container | undefined;
|
|
101
145
|
#outlinedList: OutlinedList | undefined;
|
|
@@ -110,18 +154,24 @@ export class HookSelectorComponent extends Container {
|
|
|
110
154
|
#slider: HookSelectorSlider | undefined;
|
|
111
155
|
#sliderIndex: number = 0;
|
|
112
156
|
#sliderComponent: Text | undefined;
|
|
157
|
+
#lastRenderWidth: number | undefined;
|
|
113
158
|
constructor(
|
|
114
159
|
title: string,
|
|
115
|
-
options:
|
|
160
|
+
options: HookSelectorOptionInput[],
|
|
116
161
|
onSelect: (option: string) => void,
|
|
117
162
|
onCancel: () => void,
|
|
118
163
|
opts?: HookSelectorOptions,
|
|
119
164
|
) {
|
|
120
165
|
super();
|
|
121
166
|
|
|
122
|
-
this.#options = options;
|
|
123
|
-
this.#filteredOptions = options;
|
|
124
|
-
this.#
|
|
167
|
+
this.#options = options.map(normalizeHookSelectorOption);
|
|
168
|
+
this.#filteredOptions = this.#options.map((option, index) => ({ option, index }));
|
|
169
|
+
this.#disabledIndices = new Set(
|
|
170
|
+
(opts?.disabledIndices ?? []).filter(
|
|
171
|
+
index => Number.isInteger(index) && index >= 0 && index < this.#options.length,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
this.#selectedIndex = this.#coerceSelectedIndex(opts?.initialIndex ?? 0);
|
|
125
175
|
this.#maxVisible = Math.max(3, opts?.maxVisible ?? 12);
|
|
126
176
|
this.#onSelectCallback = onSelect;
|
|
127
177
|
this.#onCancelCallback = onCancel;
|
|
@@ -154,9 +204,10 @@ export class HookSelectorComponent extends Container {
|
|
|
154
204
|
s => this.#titleComponent.setText(`${this.#baseTitle} (${s}s)`),
|
|
155
205
|
() => {
|
|
156
206
|
opts?.onTimeout?.();
|
|
207
|
+
// Auto-select current option on timeout (typically the first/recommended option)
|
|
157
208
|
const selected = this.#filteredOptions[this.#selectedIndex];
|
|
158
|
-
if (selected) {
|
|
159
|
-
this.#onSelectCallback(selected);
|
|
209
|
+
if (selected && !this.#isDisabled(selected.index)) {
|
|
210
|
+
this.#onSelectCallback(selected.option.label);
|
|
160
211
|
} else {
|
|
161
212
|
this.#onCancelCallback();
|
|
162
213
|
}
|
|
@@ -180,32 +231,160 @@ export class HookSelectorComponent extends Container {
|
|
|
180
231
|
this.#updateList();
|
|
181
232
|
}
|
|
182
233
|
|
|
183
|
-
#
|
|
234
|
+
#isDisabled(index: number): boolean {
|
|
235
|
+
return this.#disabledIndices.has(index);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Clamp `index` into range, then walk forward (and finally backward) to the
|
|
239
|
+
* nearest enabled option so the cursor never lands on a disabled row. */
|
|
240
|
+
#coerceSelectedIndex(index: number): number {
|
|
241
|
+
if (this.#filteredOptions.length === 0) return -1;
|
|
242
|
+
const maxIndex = this.#filteredOptions.length - 1;
|
|
243
|
+
const clamped = Math.max(0, Math.min(index, maxIndex));
|
|
244
|
+
const clampedOption = this.#filteredOptions[clamped];
|
|
245
|
+
if (clampedOption && !this.#isDisabled(clampedOption.index)) return clamped;
|
|
246
|
+
for (let i = clamped + 1; i <= maxIndex; i++) {
|
|
247
|
+
const option = this.#filteredOptions[i];
|
|
248
|
+
if (option && !this.#isDisabled(option.index)) return i;
|
|
249
|
+
}
|
|
250
|
+
for (let i = clamped - 1; i >= 0; i--) {
|
|
251
|
+
const option = this.#filteredOptions[i];
|
|
252
|
+
if (option && !this.#isDisabled(option.index)) return i;
|
|
253
|
+
}
|
|
254
|
+
return clamped;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Move the cursor by `delta`, skipping disabled rows, stopping at the first
|
|
258
|
+
* enabled option reached or at the list edge. */
|
|
259
|
+
#moveSelection(delta: number): void {
|
|
260
|
+
if (this.#filteredOptions.length === 0) return;
|
|
261
|
+
const maxIndex = this.#filteredOptions.length - 1;
|
|
262
|
+
let index = this.#selectedIndex;
|
|
263
|
+
while (true) {
|
|
264
|
+
const next = Math.max(0, Math.min(index + delta, maxIndex));
|
|
265
|
+
if (next === index) return;
|
|
266
|
+
index = next;
|
|
267
|
+
const option = this.#filteredOptions[index];
|
|
268
|
+
if (option && !this.#isDisabled(option.index)) {
|
|
269
|
+
this.#selectedIndex = index;
|
|
270
|
+
this.#updateList();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
#renderOptionLines(
|
|
277
|
+
option: HookSelectorOption,
|
|
278
|
+
isSelected: boolean,
|
|
279
|
+
isDisabled: boolean,
|
|
280
|
+
mdTheme: MarkdownTheme,
|
|
281
|
+
): string[] {
|
|
282
|
+
const textColor = isDisabled ? "dim" : isSelected ? "accent" : "text";
|
|
283
|
+
const prefixColor = isDisabled ? "dim" : "accent";
|
|
284
|
+
const label = renderInlineMarkdown(option.label, mdTheme, t => theme.fg(textColor, t));
|
|
285
|
+
const prefix = isSelected ? theme.fg(prefixColor, `${theme.nav.cursor} `) : " ";
|
|
286
|
+
const lines = [prefix + label];
|
|
287
|
+
if (option.description) {
|
|
288
|
+
const descriptionColor = isDisabled ? "dim" : "muted";
|
|
289
|
+
const description = renderInlineMarkdown(option.description, mdTheme, t => theme.fg(descriptionColor, t));
|
|
290
|
+
lines.push(` ${description}`);
|
|
291
|
+
}
|
|
292
|
+
return lines;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#renderedLineRowCount(line: string, renderWidth: number): number {
|
|
296
|
+
const normalized = replaceTabs(line);
|
|
297
|
+
if (this.#outlinedList) {
|
|
298
|
+
const innerWidth = Math.max(1, renderWidth - 2);
|
|
299
|
+
const { indent, body } = splitLeadingSpacesForWrap(normalized, innerWidth);
|
|
300
|
+
const wrapped = wrapTextWithAnsi(body, Math.max(1, innerWidth - visibleWidth(indent)));
|
|
301
|
+
return Math.max(1, wrapped.length);
|
|
302
|
+
}
|
|
303
|
+
const wrapped = wrapTextWithAnsi(normalized, Math.max(1, renderWidth - 2));
|
|
304
|
+
return Math.max(1, wrapped.length);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#optionRowCount(
|
|
308
|
+
option: HookSelectorOption,
|
|
309
|
+
renderWidth: number | undefined,
|
|
310
|
+
isSelected: boolean,
|
|
311
|
+
mdTheme: MarkdownTheme,
|
|
312
|
+
): number {
|
|
313
|
+
if (renderWidth === undefined) return option.description ? 2 : 1;
|
|
314
|
+
let rows = 0;
|
|
315
|
+
for (const line of this.#renderOptionLines(option, isSelected, false, mdTheme)) {
|
|
316
|
+
rows += this.#renderedLineRowCount(line, renderWidth);
|
|
317
|
+
}
|
|
318
|
+
return rows;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#totalOptionRows(options: HookSelectorOption[], renderWidth?: number, mdTheme?: MarkdownTheme): number {
|
|
322
|
+
const themeForRows = mdTheme ?? getMarkdownTheme();
|
|
323
|
+
let rows = 0;
|
|
324
|
+
for (const option of options) {
|
|
325
|
+
rows += this.#optionRowCount(option, renderWidth, false, themeForRows);
|
|
326
|
+
}
|
|
327
|
+
return rows;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#getVisibleOptionRange(
|
|
331
|
+
total: number,
|
|
332
|
+
renderWidth?: number,
|
|
333
|
+
mdTheme: MarkdownTheme = getMarkdownTheme(),
|
|
334
|
+
): { startIndex: number; endIndex: number } {
|
|
335
|
+
if (total === 0) return { startIndex: 0, endIndex: 0 };
|
|
336
|
+
|
|
337
|
+
const rowBudget = Math.max(1, this.#maxVisible);
|
|
338
|
+
const selectedIndex = Math.max(0, Math.min(this.#selectedIndex, total - 1));
|
|
339
|
+
let startIndex = selectedIndex;
|
|
340
|
+
let endIndex = selectedIndex + 1;
|
|
341
|
+
let rows = this.#optionRowCount(this.#filteredOptions[selectedIndex]!.option, renderWidth, true, mdTheme);
|
|
342
|
+
let beforeRows = 0;
|
|
343
|
+
const targetBeforeRows = Math.max(0, Math.floor((rowBudget - rows) / 2));
|
|
344
|
+
|
|
345
|
+
while (startIndex > 0) {
|
|
346
|
+
const cost = this.#optionRowCount(this.#filteredOptions[startIndex - 1]!.option, renderWidth, false, mdTheme);
|
|
347
|
+
if (beforeRows + cost > targetBeforeRows || rows + cost > rowBudget) break;
|
|
348
|
+
startIndex--;
|
|
349
|
+
beforeRows += cost;
|
|
350
|
+
rows += cost;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
while (endIndex < total) {
|
|
354
|
+
const cost = this.#optionRowCount(this.#filteredOptions[endIndex]!.option, renderWidth, false, mdTheme);
|
|
355
|
+
if (rows + cost > rowBudget) break;
|
|
356
|
+
endIndex++;
|
|
357
|
+
rows += cost;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
while (startIndex > 0) {
|
|
361
|
+
const cost = this.#optionRowCount(this.#filteredOptions[startIndex - 1]!.option, renderWidth, false, mdTheme);
|
|
362
|
+
if (rows + cost > rowBudget) break;
|
|
363
|
+
startIndex--;
|
|
364
|
+
rows += cost;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return { startIndex, endIndex };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
#updateList(renderWidth = this.#lastRenderWidth): void {
|
|
184
371
|
const lines: string[] = [];
|
|
185
372
|
const total = this.#filteredOptions.length;
|
|
186
|
-
const startIndex = Math.max(
|
|
187
|
-
0,
|
|
188
|
-
Math.min(this.#selectedIndex - Math.floor(this.#maxVisible / 2), total - this.#maxVisible),
|
|
189
|
-
);
|
|
190
|
-
const endIndex = Math.min(startIndex + this.#maxVisible, total);
|
|
191
|
-
|
|
192
373
|
const mdTheme = getMarkdownTheme();
|
|
374
|
+
const { startIndex, endIndex } = this.#getVisibleOptionRange(total, renderWidth, mdTheme);
|
|
375
|
+
|
|
193
376
|
for (let i = startIndex; i < endIndex; i++) {
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
377
|
+
const filtered = this.#filteredOptions[i];
|
|
378
|
+
if (filtered === undefined) continue;
|
|
196
379
|
const isSelected = i === this.#selectedIndex;
|
|
197
|
-
|
|
198
|
-
? renderInlineMarkdown(option, mdTheme, t => theme.fg("accent", t))
|
|
199
|
-
: renderInlineMarkdown(option, mdTheme, t => theme.fg("text", t));
|
|
200
|
-
const prefix = isSelected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
201
|
-
lines.push(prefix + label);
|
|
380
|
+
lines.push(...this.#renderOptionLines(filtered.option, isSelected, this.#isDisabled(filtered.index), mdTheme));
|
|
202
381
|
}
|
|
203
382
|
|
|
204
383
|
if (total === 0) {
|
|
205
384
|
lines.push(theme.fg("dim", " No matching options"));
|
|
206
385
|
}
|
|
207
386
|
|
|
208
|
-
if (startIndex > 0 || endIndex < total || this.#shouldRenderSearchStatus()) {
|
|
387
|
+
if (startIndex > 0 || endIndex < total || this.#shouldRenderSearchStatus(renderWidth, mdTheme)) {
|
|
209
388
|
lines.push(this.#renderStatusLine(total));
|
|
210
389
|
}
|
|
211
390
|
if (this.#outlinedList) {
|
|
@@ -253,12 +432,12 @@ export class HookSelectorComponent extends Container {
|
|
|
253
432
|
slider.onChange?.(next);
|
|
254
433
|
}
|
|
255
434
|
|
|
256
|
-
#isSearchEnabled(): boolean {
|
|
257
|
-
return this.#options
|
|
435
|
+
#isSearchEnabled(renderWidth = this.#lastRenderWidth, mdTheme?: MarkdownTheme): boolean {
|
|
436
|
+
return this.#totalOptionRows(this.#options, renderWidth, mdTheme) > this.#maxVisible;
|
|
258
437
|
}
|
|
259
438
|
|
|
260
|
-
#shouldRenderSearchStatus(): boolean {
|
|
261
|
-
return this.#isSearchEnabled() || this.#searchQuery.length > 0;
|
|
439
|
+
#shouldRenderSearchStatus(renderWidth = this.#lastRenderWidth, mdTheme?: MarkdownTheme): boolean {
|
|
440
|
+
return this.#isSearchEnabled(renderWidth, mdTheme) || this.#searchQuery.length > 0;
|
|
262
441
|
}
|
|
263
442
|
|
|
264
443
|
#renderStatusLine(total: number): string {
|
|
@@ -273,8 +452,11 @@ export class HookSelectorComponent extends Container {
|
|
|
273
452
|
|
|
274
453
|
#setSearchQuery(query: string): void {
|
|
275
454
|
this.#searchQuery = query;
|
|
276
|
-
|
|
277
|
-
this.#
|
|
455
|
+
const indexedOptions = this.#options.map((option, index) => ({ option, index }));
|
|
456
|
+
this.#filteredOptions = query.trim()
|
|
457
|
+
? fuzzyFilter(indexedOptions, query, item => `${item.option.label} ${item.option.description ?? ""}`)
|
|
458
|
+
: indexedOptions;
|
|
459
|
+
this.#selectedIndex = this.#coerceSelectedIndex(0);
|
|
278
460
|
this.#updateList();
|
|
279
461
|
}
|
|
280
462
|
|
|
@@ -311,18 +493,12 @@ export class HookSelectorComponent extends Container {
|
|
|
311
493
|
}
|
|
312
494
|
|
|
313
495
|
if (matchesSelectUp(keyData) || (!this.#isSearchEnabled() && keyData === "k")) {
|
|
314
|
-
|
|
315
|
-
this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
|
|
316
|
-
this.#updateList();
|
|
317
|
-
}
|
|
496
|
+
this.#moveSelection(-1);
|
|
318
497
|
} else if (matchesSelectDown(keyData) || (!this.#isSearchEnabled() && keyData === "j")) {
|
|
319
|
-
|
|
320
|
-
this.#selectedIndex = Math.min(this.#filteredOptions.length - 1, this.#selectedIndex + 1);
|
|
321
|
-
this.#updateList();
|
|
322
|
-
}
|
|
498
|
+
this.#moveSelection(1);
|
|
323
499
|
} else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
324
500
|
const selected = this.#filteredOptions[this.#selectedIndex];
|
|
325
|
-
if (selected) this.#onSelectCallback(selected);
|
|
501
|
+
if (selected && !this.#isDisabled(selected.index)) this.#onSelectCallback(selected.option.label);
|
|
326
502
|
} else if (matchesKey(keyData, "left") || (this.#slider && !this.#isSearchEnabled() && keyData === "h")) {
|
|
327
503
|
if (this.#slider) this.#moveSlider(-1);
|
|
328
504
|
else this.#onLeftCallback?.();
|
|
@@ -334,6 +510,15 @@ export class HookSelectorComponent extends Container {
|
|
|
334
510
|
}
|
|
335
511
|
}
|
|
336
512
|
|
|
513
|
+
override render(width: number): string[] {
|
|
514
|
+
const renderWidth = Math.max(1, width);
|
|
515
|
+
if (this.#lastRenderWidth !== renderWidth) {
|
|
516
|
+
this.#lastRenderWidth = renderWidth;
|
|
517
|
+
this.#updateList(renderWidth);
|
|
518
|
+
}
|
|
519
|
+
return super.render(renderWidth);
|
|
520
|
+
}
|
|
521
|
+
|
|
337
522
|
dispose(): void {
|
|
338
523
|
this.#countdown?.dispose();
|
|
339
524
|
}
|
|
@@ -67,8 +67,14 @@ export class OAuthSelectorComponent extends Container {
|
|
|
67
67
|
this.#validationGeneration += 1;
|
|
68
68
|
this.#stopSpinner();
|
|
69
69
|
}
|
|
70
|
+
#hasSelectableAuth(providerId: string): boolean {
|
|
71
|
+
return this.#mode === "logout" ? this.#authStorage.has(providerId) : this.#authStorage.hasAuth(providerId);
|
|
72
|
+
}
|
|
73
|
+
|
|
70
74
|
#loadProviders(): void {
|
|
71
|
-
|
|
75
|
+
const providers = getOAuthProviders();
|
|
76
|
+
this.#allProviders =
|
|
77
|
+
this.#mode === "logout" ? providers.filter(provider => this.#hasSelectableAuth(provider.id)) : providers;
|
|
72
78
|
this.#filteredProviders = this.#allProviders;
|
|
73
79
|
}
|
|
74
80
|
|
|
@@ -79,7 +85,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
79
85
|
|
|
80
86
|
let pending = 0;
|
|
81
87
|
for (const provider of this.#allProviders) {
|
|
82
|
-
if (!this.#
|
|
88
|
+
if (!this.#hasSelectableAuth(provider.id)) {
|
|
83
89
|
this.#authState.delete(provider.id);
|
|
84
90
|
continue;
|
|
85
91
|
}
|
|
@@ -145,8 +151,9 @@ export class OAuthSelectorComponent extends Container {
|
|
|
145
151
|
if (state === "valid") {
|
|
146
152
|
return theme.fg("success", ` ${theme.status.success} logged in`);
|
|
147
153
|
}
|
|
148
|
-
return this.#
|
|
154
|
+
return this.#hasSelectableAuth(providerId) ? theme.fg("success", ` ${theme.status.success} logged in`) : "";
|
|
149
155
|
}
|
|
156
|
+
|
|
150
157
|
#isSearchEnabled(): boolean {
|
|
151
158
|
return this.#allProviders.length > OAUTH_SELECTOR_MAX_VISIBLE;
|
|
152
159
|
}
|
|
@@ -167,7 +174,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
167
174
|
|
|
168
175
|
#getProviderSearchText(provider: OAuthProviderInfo): string {
|
|
169
176
|
let text = `${provider.name} ${provider.id}`;
|
|
170
|
-
if (this.#
|
|
177
|
+
if (this.#hasSelectableAuth(provider.id)) {
|
|
171
178
|
text += " logged in authenticated";
|
|
172
179
|
}
|
|
173
180
|
if (!provider.available) {
|
|
@@ -240,13 +247,12 @@ export class OAuthSelectorComponent extends Container {
|
|
|
240
247
|
this.#listContainer.addChild(new TruncatedText(this.#renderStatusLine(total), 0, 0));
|
|
241
248
|
}
|
|
242
249
|
|
|
243
|
-
// Show "no providers" if empty
|
|
244
250
|
if (total === 0) {
|
|
245
251
|
const message =
|
|
246
252
|
this.#allProviders.length === 0
|
|
247
253
|
? this.#mode === "login"
|
|
248
254
|
? "No OAuth providers available"
|
|
249
|
-
: "No
|
|
255
|
+
: "No stored provider credentials to log out"
|
|
250
256
|
: "No matching providers";
|
|
251
257
|
this.#listContainer.addChild(new TruncatedText(theme.fg("muted", ` ${message}`), 0, 0));
|
|
252
258
|
}
|