@oh-my-pi/pi-coding-agent 15.11.4 → 15.11.7
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 +82 -1
- package/dist/cli.js +520 -451
- package/dist/types/cli/bench-cli.d.ts +78 -0
- package/dist/types/cli/usage-cli.d.ts +10 -1
- package/dist/types/commands/bench.d.ts +29 -0
- package/dist/types/commands/usage.d.ts +9 -0
- package/dist/types/config/model-resolver.d.ts +3 -2
- package/dist/types/config/settings-schema.d.ts +125 -3
- package/dist/types/edit/renderer.d.ts +1 -0
- package/dist/types/modes/components/oauth-selector.d.ts +10 -1
- package/dist/types/modes/components/reset-usage-selector.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/settings-selector.d.ts +8 -1
- package/dist/types/modes/components/snapcompact-shape-preview.d.ts +31 -0
- package/dist/types/modes/components/tool-execution.d.ts +18 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -0
- package/dist/types/modes/session-observer-registry.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +3 -0
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +10 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +3 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/context-usage.d.ts +6 -1
- package/dist/types/session/agent-session.d.ts +14 -1
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/codex-auto-reset.d.ts +107 -0
- package/dist/types/session/snapcompact-inline.d.ts +107 -4
- package/dist/types/slash-commands/helpers/reset-usage.d.ts +27 -0
- package/dist/types/task/render.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/renderers.d.ts +13 -0
- package/dist/types/tools/ssh.d.ts +1 -0
- package/dist/types/tools/todo.d.ts +0 -11
- package/package.json +11 -11
- package/src/cli/bench-cli.ts +437 -0
- package/src/cli/usage-cli.ts +187 -16
- package/src/cli-commands.ts +1 -0
- package/src/commands/bench.ts +42 -0
- package/src/commands/usage.ts +8 -0
- package/src/config/model-registry.ts +52 -5
- package/src/config/model-resolver.ts +36 -5
- package/src/config/settings-schema.ts +148 -3
- package/src/config/settings.ts +9 -0
- package/src/edit/renderer.ts +5 -0
- package/src/hindsight/client.ts +26 -1
- package/src/hindsight/state.ts +6 -2
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/mcp/transports/stdio.ts +81 -7
- package/src/modes/components/oauth-selector.ts +67 -7
- package/src/modes/components/reset-usage-selector.ts +161 -0
- package/src/modes/components/session-selector.ts +8 -2
- package/src/modes/components/settings-selector.ts +89 -47
- package/src/modes/components/snapcompact-shape-preview-doc.md +11 -0
- package/src/modes/components/snapcompact-shape-preview.ts +192 -0
- package/src/modes/components/tool-execution.ts +26 -0
- package/src/modes/components/transcript-container.ts +23 -1
- package/src/modes/controllers/command-controller.ts +24 -1
- package/src/modes/controllers/input-controller.ts +8 -6
- package/src/modes/controllers/selector-controller.ts +72 -2
- package/src/modes/interactive-mode.ts +83 -0
- package/src/modes/session-observer-registry.ts +61 -3
- package/src/modes/setup-wizard/index.ts +1 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +24 -6
- package/src/modes/setup-wizard/scenes/providers.ts +36 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +10 -1
- package/src/modes/setup-wizard/scenes/theme.ts +28 -1
- package/src/modes/setup-wizard/scenes/types.ts +10 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +22 -6
- package/src/modes/setup-wizard/wizard-overlay.ts +38 -1
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/context-usage.ts +75 -1
- package/src/prompts/bench.md +7 -0
- package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
- package/src/prompts/system/snapcompact-context-stub.md +1 -0
- package/src/prompts/system/snapcompact-toolresult-note.md +1 -1
- package/src/prompts/tools/browser.md +33 -43
- package/src/prompts/tools/eval.md +27 -50
- package/src/prompts/tools/irc.md +29 -31
- package/src/prompts/tools/read.md +31 -37
- package/src/prompts/tools/todo.md +1 -2
- package/src/sdk.ts +4 -2
- package/src/session/agent-session.ts +136 -6
- package/src/session/auth-storage.ts +3 -0
- package/src/session/codex-auto-reset.ts +190 -0
- package/src/session/snapcompact-inline.ts +404 -75
- package/src/slash-commands/builtin-registry.ts +145 -8
- package/src/slash-commands/helpers/context-report.ts +28 -1
- package/src/slash-commands/helpers/reset-usage.ts +66 -0
- package/src/slash-commands/helpers/usage-report.ts +12 -0
- package/src/task/index.ts +30 -7
- package/src/task/render.ts +34 -19
- package/src/tools/bash.ts +3 -0
- package/src/tools/eval-render.ts +4 -0
- package/src/tools/renderers.ts +13 -0
- package/src/tools/ssh.ts +3 -0
- package/src/tools/todo.ts +8 -128
|
@@ -85,27 +85,99 @@ async function resolveWindowsCommandPath(
|
|
|
85
85
|
env: Record<string, string | undefined>,
|
|
86
86
|
): Promise<string | null> {
|
|
87
87
|
const extensions = getWindowsPathExt(env);
|
|
88
|
-
|
|
88
|
+
const hasExt = hasExecutableExtension(command, extensions);
|
|
89
|
+
const candidates = hasExt ? [command] : extensions.map(ext => `${command}${ext}`);
|
|
89
90
|
|
|
90
|
-
const candidates = extensions.map(ext => `${command}${ext}`);
|
|
91
91
|
if (hasPathSegment(command)) {
|
|
92
92
|
for (const candidate of candidates) {
|
|
93
93
|
const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(cwd, candidate);
|
|
94
94
|
if (await fileExists(resolved)) return resolved;
|
|
95
95
|
}
|
|
96
|
-
return null;
|
|
96
|
+
return hasExt ? command : null;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// Match cmd.exe's lookup order for an unqualified name: current directory
|
|
100
|
+
// first, then PATH. Skipping cwd would launch a global shim instead of a
|
|
101
|
+
// project-local one with the same name.
|
|
102
|
+
const searchDirs = [cwd];
|
|
99
103
|
const pathValue = getCaseInsensitiveEnv(env, "PATH");
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
if (pathValue) {
|
|
105
|
+
for (const dir of pathValue.split(";")) {
|
|
106
|
+
if (dir) searchDirs.push(dir);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const dir of searchDirs) {
|
|
103
110
|
for (const candidate of candidates) {
|
|
104
111
|
const resolved = path.join(dir, candidate);
|
|
105
112
|
if (await fileExists(resolved)) return resolved;
|
|
106
113
|
}
|
|
107
114
|
}
|
|
108
|
-
return null;
|
|
115
|
+
return hasExt ? command : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveWindowsShimPath(value: string, shimDir: string): string | null {
|
|
119
|
+
const match = /^%dp0%[\\/]*(.*)$/i.exec(value);
|
|
120
|
+
if (!match) return null;
|
|
121
|
+
const suffix = match[1];
|
|
122
|
+
if (!suffix) return shimDir;
|
|
123
|
+
return path.join(shimDir, ...suffix.split(/[\\/]+/).filter(Boolean));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function extractWindowsNpmShimTarget(content: string): string | null {
|
|
127
|
+
const match = /"%_prog%"\s+"([^"]+)"\s+%\*/i.exec(content);
|
|
128
|
+
return match?.[1] ?? null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extract the shim's PATH-fallback interpreter (`SET "_prog=node"`). The
|
|
133
|
+
* `IF EXIST` branch assigns a `%dp0%`-prefixed value, so requiring a
|
|
134
|
+
* non-`%`-leading value picks the bare program name.
|
|
135
|
+
*/
|
|
136
|
+
function extractWindowsNpmShimProg(content: string): string | null {
|
|
137
|
+
const match = /SET\s+"_prog=([^%"][^"]*)"/i.exec(content);
|
|
138
|
+
return match?.[1] ?? null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function resolveWindowsNpmShimCommand(
|
|
142
|
+
command: string,
|
|
143
|
+
args: readonly string[],
|
|
144
|
+
cwd: string,
|
|
145
|
+
): Promise<StdioSpawnCommand | null> {
|
|
146
|
+
if (!isWindowsBatchCommand(command)) return null;
|
|
147
|
+
if (!hasPathSegment(command)) return null;
|
|
148
|
+
const commandPath = path.resolve(cwd, command);
|
|
149
|
+
|
|
150
|
+
let content: string;
|
|
151
|
+
try {
|
|
152
|
+
content = await Bun.file(commandPath).text();
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// cmd-shim emits the same invocation line for every interpreter; only
|
|
158
|
+
// bypass cmd.exe when the shim's fallback interpreter is actually node.
|
|
159
|
+
const prog = extractWindowsNpmShimProg(content);
|
|
160
|
+
if (
|
|
161
|
+
!prog ||
|
|
162
|
+
path
|
|
163
|
+
.basename(prog)
|
|
164
|
+
.replace(/\.exe$/i, "")
|
|
165
|
+
.toLowerCase() !== "node"
|
|
166
|
+
)
|
|
167
|
+
return null;
|
|
168
|
+
|
|
169
|
+
const rawTarget = extractWindowsNpmShimTarget(content);
|
|
170
|
+
if (!rawTarget) return null;
|
|
171
|
+
|
|
172
|
+
const target = resolveWindowsShimPath(rawTarget, path.dirname(commandPath));
|
|
173
|
+
if (!target) return null;
|
|
174
|
+
|
|
175
|
+
const siblingNode = path.join(path.dirname(commandPath), "node.exe");
|
|
176
|
+
const nodeCommand = (await fileExists(siblingNode)) ? siblingNode : "node";
|
|
177
|
+
return {
|
|
178
|
+
cmd: [nodeCommand, target, ...args],
|
|
179
|
+
windowsHide: true,
|
|
180
|
+
};
|
|
109
181
|
}
|
|
110
182
|
|
|
111
183
|
function quoteCmdArg(value: string): string {
|
|
@@ -150,6 +222,8 @@ export async function resolveStdioSpawnCommand(
|
|
|
150
222
|
|
|
151
223
|
const resolvedCommand =
|
|
152
224
|
(await resolveWindowsCommandPath(config.command, options.cwd, options.env)) ?? config.command;
|
|
225
|
+
const npmShimCommand = await resolveWindowsNpmShimCommand(resolvedCommand, args, options.cwd);
|
|
226
|
+
if (npmShimCommand) return npmShimCommand;
|
|
153
227
|
if (!isWindowsBatchCommand(resolvedCommand)) return { cmd: [resolvedCommand, ...args] };
|
|
154
228
|
|
|
155
229
|
return {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
fuzzyFilter,
|
|
7
7
|
matchesKey,
|
|
8
8
|
ScrollView,
|
|
9
|
+
type SgrMouseEvent,
|
|
9
10
|
Spacer,
|
|
10
11
|
TruncatedText,
|
|
11
12
|
} from "@oh-my-pi/pi-tui";
|
|
@@ -16,6 +17,12 @@ import { DynamicBorder } from "./dynamic-border";
|
|
|
16
17
|
|
|
17
18
|
const OAUTH_SELECTOR_MAX_VISIBLE = 10;
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Rendered lines before the provider rows: top border, spacer, title, spacer
|
|
22
|
+
* (must mirror the constructor's addChild order).
|
|
23
|
+
*/
|
|
24
|
+
const LIST_ROW_OFFSET = 4;
|
|
25
|
+
|
|
19
26
|
/** Compact, human-readable tag for each credential-origin leg. */
|
|
20
27
|
const ORIGIN_LABELS: Record<CredentialOriginKind, string> = {
|
|
21
28
|
runtime: "--api-key",
|
|
@@ -34,6 +41,10 @@ export class OAuthSelectorComponent extends Container {
|
|
|
34
41
|
#filteredProviders: OAuthProviderInfo[] = [];
|
|
35
42
|
#searchQuery = "";
|
|
36
43
|
#selectedIndex: number = 0;
|
|
44
|
+
#hoveredIndex: number | null = null;
|
|
45
|
+
/** First provider index of the visible ScrollView window (last #updateList). */
|
|
46
|
+
#scrollStart = 0;
|
|
47
|
+
#visibleCount = 0;
|
|
37
48
|
#mode: "login" | "logout";
|
|
38
49
|
#authStorage: AuthStorage;
|
|
39
50
|
#onSelectCallback: (providerId: string) => void;
|
|
@@ -252,6 +263,8 @@ export class OAuthSelectorComponent extends Container {
|
|
|
252
263
|
? 0
|
|
253
264
|
: Math.max(0, Math.min(this.#selectedIndex - Math.floor(maxVisible / 2), total - maxVisible));
|
|
254
265
|
const endIndex = Math.min(startIndex + maxVisible, total);
|
|
266
|
+
this.#scrollStart = startIndex;
|
|
267
|
+
this.#visibleCount = endIndex - startIndex;
|
|
255
268
|
|
|
256
269
|
const rows: string[] = [];
|
|
257
270
|
for (let i = startIndex; i < endIndex; i++) {
|
|
@@ -270,6 +283,9 @@ export class OAuthSelectorComponent extends Container {
|
|
|
270
283
|
const text = isAvailable ? ` ${provider.name}` : theme.fg("dim", ` ${provider.name}`);
|
|
271
284
|
line = text + statusIndicator;
|
|
272
285
|
}
|
|
286
|
+
if (!isSelected && i === this.#hoveredIndex) {
|
|
287
|
+
line = theme.bg("selectedBg", line);
|
|
288
|
+
}
|
|
273
289
|
rows.push(line);
|
|
274
290
|
}
|
|
275
291
|
|
|
@@ -354,15 +370,59 @@ export class OAuthSelectorComponent extends Container {
|
|
|
354
370
|
}
|
|
355
371
|
// Enter
|
|
356
372
|
else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
373
|
+
this.#confirmSelection();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Confirm the selected provider (Enter or mouse click). */
|
|
378
|
+
#confirmSelection(): void {
|
|
379
|
+
const selectedProvider = this.#filteredProviders[this.#selectedIndex];
|
|
380
|
+
if (selectedProvider?.available) {
|
|
381
|
+
this.#statusMessage = undefined;
|
|
382
|
+
this.stopValidation();
|
|
383
|
+
this.#onSelectCallback(selectedProvider.id);
|
|
384
|
+
} else if (selectedProvider) {
|
|
385
|
+
this.#statusMessage = "Provider unavailable in this environment.";
|
|
386
|
+
this.#updateList();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Move the selection one step for a wheel notch (clamped, no wrap). */
|
|
391
|
+
handleWheel(delta: -1 | 1): void {
|
|
392
|
+
if (this.#filteredProviders.length === 0) return;
|
|
393
|
+
const next = Math.max(0, Math.min(this.#selectedIndex + delta, this.#filteredProviders.length - 1));
|
|
394
|
+
if (next === this.#selectedIndex) return;
|
|
395
|
+
this.#selectedIndex = next;
|
|
396
|
+
this.#statusMessage = undefined;
|
|
397
|
+
this.#updateList();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Route an SGR mouse report at component-local coordinates. Provider rows
|
|
402
|
+
* start LIST_ROW_OFFSET lines into the render; the ScrollView window shows
|
|
403
|
+
* #visibleCount rows from #scrollStart. Wheel moves the selection, motion
|
|
404
|
+
* drives the hover band, and a left click selects and confirms like Enter.
|
|
405
|
+
*/
|
|
406
|
+
routeMouse(event: SgrMouseEvent, line: number, _col: number): void {
|
|
407
|
+
if (event.wheel !== null) {
|
|
408
|
+
this.handleWheel(event.wheel);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const localRow = line - LIST_ROW_OFFSET;
|
|
412
|
+
const index = localRow >= 0 && localRow < this.#visibleCount ? this.#scrollStart + localRow : undefined;
|
|
413
|
+
const target = index !== undefined && index < this.#filteredProviders.length ? index : null;
|
|
414
|
+
if (event.motion) {
|
|
415
|
+
if (target !== this.#hoveredIndex) {
|
|
416
|
+
this.#hoveredIndex = target;
|
|
364
417
|
this.#updateList();
|
|
365
418
|
}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (!event.leftClick || target === null) return;
|
|
422
|
+
if (target !== this.#selectedIndex) {
|
|
423
|
+
this.#selectedIndex = target;
|
|
424
|
+
this.#statusMessage = undefined;
|
|
366
425
|
}
|
|
426
|
+
this.#confirmSelection();
|
|
367
427
|
}
|
|
368
428
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Container, matchesKey, ScrollView, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { theme } from "../../modes/theme/theme";
|
|
3
|
+
import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
4
|
+
import type { ResetUsageAccount } from "../../slash-commands/helpers/reset-usage";
|
|
5
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
6
|
+
|
|
7
|
+
const RESET_SELECTOR_MAX_VISIBLE = 10;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Account picker for `/usage reset`. Lists Codex accounts with their saved
|
|
11
|
+
* rate-limit reset counts; selecting one redeems a reset. Because a reset is a
|
|
12
|
+
* scarce, irreversible credit, Enter requires a second press to confirm.
|
|
13
|
+
*/
|
|
14
|
+
export class ResetUsageSelectorComponent extends Container {
|
|
15
|
+
#listContainer: Container;
|
|
16
|
+
#accounts: ResetUsageAccount[];
|
|
17
|
+
#selectedIndex = 0;
|
|
18
|
+
#pendingIndex: number | null = null;
|
|
19
|
+
#statusMessage: string | undefined;
|
|
20
|
+
#onSelectCallback: (account: ResetUsageAccount) => void;
|
|
21
|
+
#onCancelCallback: () => void;
|
|
22
|
+
|
|
23
|
+
constructor(accounts: ResetUsageAccount[], onSelect: (account: ResetUsageAccount) => void, onCancel: () => void) {
|
|
24
|
+
super();
|
|
25
|
+
this.#accounts = accounts;
|
|
26
|
+
this.#onSelectCallback = onSelect;
|
|
27
|
+
this.#onCancelCallback = onCancel;
|
|
28
|
+
const firstRedeemable = accounts.findIndex(account => account.availableCount > 0);
|
|
29
|
+
this.#selectedIndex = firstRedeemable >= 0 ? firstRedeemable : 0;
|
|
30
|
+
|
|
31
|
+
this.addChild(new DynamicBorder());
|
|
32
|
+
this.addChild(new Spacer(1));
|
|
33
|
+
this.addChild(new TruncatedText(theme.bold("Spend a saved rate-limit reset:")));
|
|
34
|
+
this.addChild(new Spacer(1));
|
|
35
|
+
this.#listContainer = new Container();
|
|
36
|
+
this.addChild(this.#listContainer);
|
|
37
|
+
this.addChild(new Spacer(1));
|
|
38
|
+
this.addChild(new DynamicBorder());
|
|
39
|
+
this.#updateList();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#updateList(): void {
|
|
43
|
+
this.#listContainer.clear();
|
|
44
|
+
|
|
45
|
+
const total = this.#accounts.length;
|
|
46
|
+
const maxVisible = RESET_SELECTOR_MAX_VISIBLE;
|
|
47
|
+
const startIndex =
|
|
48
|
+
total <= maxVisible
|
|
49
|
+
? 0
|
|
50
|
+
: Math.max(0, Math.min(this.#selectedIndex - Math.floor(maxVisible / 2), total - maxVisible));
|
|
51
|
+
const endIndex = Math.min(startIndex + maxVisible, total);
|
|
52
|
+
|
|
53
|
+
const rows: string[] = [];
|
|
54
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
55
|
+
const account = this.#accounts[i];
|
|
56
|
+
if (!account) continue;
|
|
57
|
+
const isSelected = i === this.#selectedIndex;
|
|
58
|
+
const redeemable = account.availableCount > 0;
|
|
59
|
+
const countLabel = account.error
|
|
60
|
+
? account.error
|
|
61
|
+
: `${account.availableCount} saved reset${account.availableCount === 1 ? "" : "s"}`;
|
|
62
|
+
const countText = account.error
|
|
63
|
+
? theme.fg("error", countLabel)
|
|
64
|
+
: redeemable
|
|
65
|
+
? theme.fg("success", countLabel)
|
|
66
|
+
: theme.fg("dim", countLabel);
|
|
67
|
+
const activeTag = account.active ? theme.fg("muted", " (active)") : "";
|
|
68
|
+
if (isSelected) {
|
|
69
|
+
const name = redeemable ? theme.fg("accent", account.label) : theme.fg("dim", account.label);
|
|
70
|
+
rows.push(`${theme.fg("accent", `${theme.nav.cursor} `)}${name}${activeTag} ${countText}`);
|
|
71
|
+
} else {
|
|
72
|
+
const name = redeemable ? ` ${account.label}` : theme.fg("dim", ` ${account.label}`);
|
|
73
|
+
rows.push(`${name}${activeTag} ${countText}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (rows.length > 0) {
|
|
78
|
+
const sv = new ScrollView(rows, {
|
|
79
|
+
height: rows.length,
|
|
80
|
+
scrollbar: "auto",
|
|
81
|
+
totalRows: total,
|
|
82
|
+
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
83
|
+
});
|
|
84
|
+
sv.setScrollOffset(startIndex);
|
|
85
|
+
this.#listContainer.addChild(sv);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (total === 0) {
|
|
89
|
+
this.#listContainer.addChild(
|
|
90
|
+
new TruncatedText(theme.fg("muted", " No Codex accounts with saved resets"), 0, 0),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const pending = this.#pendingIndex !== null ? this.#accounts[this.#pendingIndex] : undefined;
|
|
95
|
+
const hint = pending
|
|
96
|
+
? theme.fg("warning", ` Press Enter again to spend 1 reset for ${pending.label}, Esc to cancel`)
|
|
97
|
+
: theme.fg("muted", " ↑/↓ select · ↵ spend a reset · Esc cancel");
|
|
98
|
+
this.#listContainer.addChild(new TruncatedText(hint, 0, 0));
|
|
99
|
+
|
|
100
|
+
if (this.#statusMessage) {
|
|
101
|
+
this.#listContainer.addChild(new Spacer(1));
|
|
102
|
+
this.#listContainer.addChild(new TruncatedText(theme.fg("warning", ` ${this.#statusMessage}`), 0, 0));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
handleInput(keyData: string): void {
|
|
107
|
+
if (matchesSelectCancel(keyData)) {
|
|
108
|
+
if (this.#pendingIndex !== null) {
|
|
109
|
+
this.#pendingIndex = null;
|
|
110
|
+
this.#statusMessage = undefined;
|
|
111
|
+
this.#updateList();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.#onCancelCallback();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (matchesSelectUp(keyData)) {
|
|
119
|
+
if (this.#accounts.length > 0) {
|
|
120
|
+
this.#selectedIndex = this.#selectedIndex === 0 ? this.#accounts.length - 1 : this.#selectedIndex - 1;
|
|
121
|
+
}
|
|
122
|
+
this.#pendingIndex = null;
|
|
123
|
+
this.#statusMessage = undefined;
|
|
124
|
+
this.#updateList();
|
|
125
|
+
} else if (matchesSelectDown(keyData)) {
|
|
126
|
+
if (this.#accounts.length > 0) {
|
|
127
|
+
this.#selectedIndex = this.#selectedIndex === this.#accounts.length - 1 ? 0 : this.#selectedIndex + 1;
|
|
128
|
+
}
|
|
129
|
+
this.#pendingIndex = null;
|
|
130
|
+
this.#statusMessage = undefined;
|
|
131
|
+
this.#updateList();
|
|
132
|
+
} else if (matchesKey(keyData, "pageUp")) {
|
|
133
|
+
if (this.#accounts.length > 0) {
|
|
134
|
+
this.#selectedIndex = Math.max(0, this.#selectedIndex - RESET_SELECTOR_MAX_VISIBLE);
|
|
135
|
+
}
|
|
136
|
+
this.#pendingIndex = null;
|
|
137
|
+
this.#updateList();
|
|
138
|
+
} else if (matchesKey(keyData, "pageDown")) {
|
|
139
|
+
if (this.#accounts.length > 0) {
|
|
140
|
+
this.#selectedIndex = Math.min(this.#accounts.length - 1, this.#selectedIndex + RESET_SELECTOR_MAX_VISIBLE);
|
|
141
|
+
}
|
|
142
|
+
this.#pendingIndex = null;
|
|
143
|
+
this.#updateList();
|
|
144
|
+
} else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
145
|
+
const account = this.#accounts[this.#selectedIndex];
|
|
146
|
+
if (!account) return;
|
|
147
|
+
if (account.availableCount <= 0) {
|
|
148
|
+
this.#statusMessage = "That account has no saved resets to spend.";
|
|
149
|
+
this.#updateList();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (this.#pendingIndex === this.#selectedIndex) {
|
|
153
|
+
this.#onSelectCallback(account);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this.#pendingIndex = this.#selectedIndex;
|
|
157
|
+
this.#statusMessage = undefined;
|
|
158
|
+
this.#updateList();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -67,13 +67,15 @@ function compareSessionRecency(a: SessionInfo, b: SessionInfo): number {
|
|
|
67
67
|
return b.modified.getTime() - a.modified.getTime();
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
const MIN_PURE_FUZZY_TOKEN_SCORE = -20;
|
|
71
|
+
|
|
70
72
|
/**
|
|
71
73
|
* Filter and rank session picker search results.
|
|
72
74
|
*
|
|
73
75
|
* Resume search narrows a recency-sorted list: once every query token appears
|
|
74
76
|
* as a literal substring, newer sessions should beat a slightly better fuzzy
|
|
75
77
|
* position match. Pure fuzzy/acronym matches still sort by fuzzy score after
|
|
76
|
-
* literal matches.
|
|
78
|
+
* literal matches, but weak pure fuzzy tokens are dropped as noise.
|
|
77
79
|
*/
|
|
78
80
|
export function rankSessionSearchMatches(allSessions: SessionInfo[], query: string): SessionInfo[] {
|
|
79
81
|
const tokens = tokenizeSessionQuery(query);
|
|
@@ -85,6 +87,7 @@ export function rankSessionSearchMatches(allSessions: SessionInfo[], query: stri
|
|
|
85
87
|
const text = sessionSearchText(session);
|
|
86
88
|
const textLower = text.toLowerCase();
|
|
87
89
|
let score = 0;
|
|
90
|
+
let worstTokenScore = Number.NEGATIVE_INFINITY;
|
|
88
91
|
let literal = true;
|
|
89
92
|
let matches = true;
|
|
90
93
|
|
|
@@ -95,10 +98,13 @@ export function rankSessionSearchMatches(allSessions: SessionInfo[], query: stri
|
|
|
95
98
|
break;
|
|
96
99
|
}
|
|
97
100
|
score += match.score;
|
|
101
|
+
worstTokenScore = Math.max(worstTokenScore, match.score);
|
|
98
102
|
if (!textLower.includes(token)) literal = false;
|
|
99
103
|
}
|
|
100
104
|
|
|
101
|
-
if (matches
|
|
105
|
+
if (matches && (literal || worstTokenScore < MIN_PURE_FUZZY_TOKEN_SCORE)) {
|
|
106
|
+
results.push({ session, score, literal, index });
|
|
107
|
+
}
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
results.sort((a, b) => {
|