@narumitw/pi-usage 0.60.0 → 0.60.2
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/README.md +27 -19
- package/dist/index.ts +804 -322
- package/dist/index.ts.map +4 -4
- package/package.json +1 -1
- package/src/core.ts +28 -2
- package/src/format.ts +37 -23
- package/src/index.ts +13 -0
- package/src/providers/fireworks.ts +112 -0
- package/src/providers/minimax.ts +21 -0
- package/src/query.ts +110 -150
- package/src/settings.ts +155 -8
- package/src/types.ts +39 -2
- package/src/usage-settings-ui.ts +88 -183
- package/src/usage-targets.ts +143 -0
- package/src/usage.ts +419 -107
package/src/types.ts
CHANGED
|
@@ -53,23 +53,51 @@ export interface ResolvedUsageAuth {
|
|
|
53
53
|
fingerprint: string;
|
|
54
54
|
secrets: string[];
|
|
55
55
|
model: PiModel;
|
|
56
|
+
auth?: {
|
|
57
|
+
apiKey?: string;
|
|
58
|
+
headers?: Record<string, string | null>;
|
|
59
|
+
baseUrl?: string;
|
|
60
|
+
};
|
|
61
|
+
env?: Record<string, string>;
|
|
62
|
+
source?: string;
|
|
63
|
+
effectiveBaseUrl?: string;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
export interface UsageQuerySettings {
|
|
59
67
|
fireworksAccountId?: string;
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
export type UsageRequestGuard = () => Promise<void>;
|
|
71
|
+
|
|
72
|
+
export interface UsageProviderTarget {
|
|
73
|
+
id: string;
|
|
74
|
+
label: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface UsageTargetResolver {
|
|
79
|
+
singularLabel: string;
|
|
80
|
+
pluralLabel: string;
|
|
81
|
+
list(
|
|
82
|
+
auth: ResolvedUsageAuth,
|
|
83
|
+
signal: AbortSignal,
|
|
84
|
+
timeoutMs: number,
|
|
85
|
+
guard: UsageRequestGuard,
|
|
86
|
+
): Promise<readonly UsageProviderTarget[]>;
|
|
87
|
+
}
|
|
88
|
+
|
|
62
89
|
export interface UsageProviderAdapter {
|
|
63
90
|
id: string;
|
|
64
91
|
displayName: string;
|
|
65
92
|
semantics: UsageSemantics;
|
|
66
93
|
publishesStatusline?: boolean;
|
|
94
|
+
targets?: UsageTargetResolver;
|
|
67
95
|
query(
|
|
68
96
|
auth: ResolvedUsageAuth,
|
|
69
97
|
signal: AbortSignal,
|
|
70
98
|
timeoutMs: number,
|
|
71
|
-
guard?:
|
|
72
|
-
|
|
99
|
+
guard?: UsageRequestGuard,
|
|
100
|
+
targetId?: string,
|
|
73
101
|
): Promise<UsageReport>;
|
|
74
102
|
}
|
|
75
103
|
|
|
@@ -81,6 +109,15 @@ export type ProviderUsageState =
|
|
|
81
109
|
status: "ready";
|
|
82
110
|
report: UsageReport;
|
|
83
111
|
}
|
|
112
|
+
| {
|
|
113
|
+
providerId: string;
|
|
114
|
+
providerName: string;
|
|
115
|
+
displayState: UsageDisplayState;
|
|
116
|
+
status: "selection-required";
|
|
117
|
+
singularLabel: string;
|
|
118
|
+
pluralLabel: string;
|
|
119
|
+
choices: readonly UsageProviderTarget[];
|
|
120
|
+
}
|
|
84
121
|
| {
|
|
85
122
|
providerId: string;
|
|
86
123
|
providerName: string;
|
package/src/usage-settings-ui.ts
CHANGED
|
@@ -11,16 +11,12 @@ import {
|
|
|
11
11
|
Text,
|
|
12
12
|
} from "@earendil-works/pi-tui";
|
|
13
13
|
import { errorMessage } from "./core.js";
|
|
14
|
-
import {
|
|
15
|
-
import type { UsageSettings, UsageSettingsRuntime } from "./settings.js";
|
|
14
|
+
import type { UsageSettingsRuntime } from "./settings.js";
|
|
16
15
|
|
|
17
|
-
const AUTO = "Auto";
|
|
18
|
-
const EDIT = "Edit…";
|
|
19
16
|
const OFF = "Off";
|
|
20
17
|
const ON = "On";
|
|
21
18
|
|
|
22
|
-
type UsageSettingId =
|
|
23
|
-
type SettingsScreenResult = { changed: boolean; editFireworksAccount: boolean };
|
|
19
|
+
type UsageSettingId = "codexFastMode" | "codexStatusResetCountdown";
|
|
24
20
|
|
|
25
21
|
export async function showUsageSettings(
|
|
26
22
|
ctx: ExtensionCommandContext,
|
|
@@ -33,187 +29,96 @@ export async function showUsageSettings(
|
|
|
33
29
|
if (ctx.hasUI) ctx.ui.notify(`Edit settings manually: ${settingsRuntime.get().path}`, "info");
|
|
34
30
|
return false;
|
|
35
31
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
32
|
+
return (
|
|
33
|
+
(await ctx.ui.custom<boolean>((tui, theme, _keybindings, done) => {
|
|
34
|
+
const localController = new AbortController();
|
|
35
|
+
const signal = AbortSignal.any([parentSignal, localController.signal]);
|
|
36
|
+
let changed = false;
|
|
37
|
+
let closing = false;
|
|
38
|
+
let saveQueue = Promise.resolve();
|
|
39
|
+
const state = settingsRuntime.get();
|
|
40
|
+
const items: SettingItem[] = [
|
|
41
|
+
{
|
|
42
|
+
id: "codexFastMode",
|
|
43
|
+
label: "Codex Fast mode",
|
|
44
|
+
description: "Use faster Codex routing at increased plan allowance consumption.",
|
|
45
|
+
currentValue: state.settings.codexFastMode ? ON : OFF,
|
|
46
|
+
values: [OFF, ON],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "codexStatusResetCountdown",
|
|
50
|
+
label: "Codex reset countdown",
|
|
51
|
+
description: "Show time remaining until each Codex usage limit resets.",
|
|
52
|
+
currentValue: state.settings.codexStatusResetCountdown ? ON : OFF,
|
|
53
|
+
values: [OFF, ON],
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
const container = new Container();
|
|
57
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("pi-usage Settings")), 1, 1));
|
|
52
58
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
id: "codexStatusResetCountdown",
|
|
78
|
-
label: "Codex reset countdown",
|
|
79
|
-
description: "Show time remaining until each Codex usage limit resets.",
|
|
80
|
-
currentValue: state.settings.codexStatusResetCountdown ? ON : OFF,
|
|
81
|
-
values: [OFF, ON],
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
id: "fireworksAccountId",
|
|
85
|
-
label: "Fireworks account",
|
|
86
|
-
description: "Select Edit to enter a visible account slug, or submit blank to clear it.",
|
|
87
|
-
currentValue: fireworksValue,
|
|
88
|
-
values: state.settings.fireworksAccountId
|
|
89
|
-
? [state.settings.fireworksAccountId, EDIT]
|
|
90
|
-
: [AUTO, EDIT],
|
|
91
|
-
},
|
|
92
|
-
];
|
|
93
|
-
const container = new Container();
|
|
94
|
-
container.addChild(new Text(theme.fg("accent", theme.bold("pi-usage Settings")), 1, 1));
|
|
95
|
-
|
|
96
|
-
let settingsList: SettingsList;
|
|
97
|
-
const cancel = () => {
|
|
98
|
-
if (closing) return;
|
|
99
|
-
closing = true;
|
|
100
|
-
localController.abort();
|
|
101
|
-
done({ changed, editFireworksAccount: false });
|
|
102
|
-
};
|
|
103
|
-
const queueUpdate = (
|
|
104
|
-
id: UsageSettingId,
|
|
105
|
-
requested: UsageSettings[UsageSettingId],
|
|
106
|
-
display: string,
|
|
107
|
-
) => {
|
|
108
|
-
saveQueue = saveQueue.then(async () => {
|
|
109
|
-
const previous = settingsRuntime.get().settings[id];
|
|
110
|
-
if (settingsRuntime.get().kind === "invalid") {
|
|
111
|
-
settingsList.updateValue(id, displaySetting(id, previous));
|
|
112
|
-
if (!signal.aborted && isCurrent()) {
|
|
113
|
-
ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
|
|
59
|
+
let settingsList: SettingsList;
|
|
60
|
+
const cancel = () => {
|
|
61
|
+
if (closing) return;
|
|
62
|
+
closing = true;
|
|
63
|
+
localController.abort();
|
|
64
|
+
done(changed);
|
|
65
|
+
};
|
|
66
|
+
const queueUpdate = (id: UsageSettingId, requested: boolean, display: string) => {
|
|
67
|
+
saveQueue = saveQueue.then(async () => {
|
|
68
|
+
const previous = settingsRuntime.get().settings[id];
|
|
69
|
+
if (settingsRuntime.get().kind === "invalid") {
|
|
70
|
+
settingsList.updateValue(id, previous ? ON : OFF);
|
|
71
|
+
if (!signal.aborted && isCurrent()) {
|
|
72
|
+
ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
|
|
73
|
+
tui.requestRender();
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await settingsRuntime.update({ [id]: requested }, signal);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (signal.aborted || !isCurrent()) return;
|
|
81
|
+
settingsList.updateValue(id, previous ? ON : OFF);
|
|
82
|
+
ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
|
|
114
83
|
tui.requestRender();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (previous !== requested) {
|
|
87
|
+
changed = true;
|
|
88
|
+
onApplied(id);
|
|
115
89
|
}
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
try {
|
|
119
|
-
await settingsRuntime.update({ [id]: requested }, signal);
|
|
120
|
-
} catch (error) {
|
|
121
90
|
if (signal.aborted || !isCurrent()) return;
|
|
122
|
-
settingsList.updateValue(id,
|
|
123
|
-
ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
|
|
91
|
+
settingsList.updateValue(id, display);
|
|
124
92
|
tui.requestRender();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
items.length + 2,
|
|
139
|
-
getSettingsListTheme(),
|
|
140
|
-
(id, value) => {
|
|
141
|
-
if (closing || signal.aborted || !isCurrent()) return;
|
|
142
|
-
if (id === "fireworksAccountId") {
|
|
143
|
-
if (value === EDIT) {
|
|
144
|
-
saveQueue = saveQueue.then(() => {
|
|
145
|
-
if (closing || signal.aborted || !isCurrent()) return;
|
|
146
|
-
closing = true;
|
|
147
|
-
done({ changed, editFireworksAccount: true });
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
const settingId = id as "codexFastMode" | "codexStatusResetCountdown";
|
|
153
|
-
queueUpdate(settingId, value !== OFF, value);
|
|
154
|
-
},
|
|
155
|
-
cancel,
|
|
156
|
-
);
|
|
157
|
-
container.addChild(settingsList);
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
settingsList = new SettingsList(
|
|
96
|
+
items,
|
|
97
|
+
items.length + 2,
|
|
98
|
+
getSettingsListTheme(),
|
|
99
|
+
(id, value) => {
|
|
100
|
+
if (closing || signal.aborted || !isCurrent()) return;
|
|
101
|
+
queueUpdate(id as UsageSettingId, value !== OFF, value);
|
|
102
|
+
},
|
|
103
|
+
cancel,
|
|
104
|
+
);
|
|
105
|
+
container.addChild(settingsList);
|
|
158
106
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
async function editFireworksAccount(
|
|
178
|
-
ctx: ExtensionCommandContext,
|
|
179
|
-
settingsRuntime: UsageSettingsRuntime,
|
|
180
|
-
signal: AbortSignal,
|
|
181
|
-
isCurrent: () => boolean,
|
|
182
|
-
onApplied: (id: UsageSettingId) => void,
|
|
183
|
-
): Promise<boolean> {
|
|
184
|
-
while (!signal.aborted && isCurrent()) {
|
|
185
|
-
const state = settingsRuntime.get();
|
|
186
|
-
if (state.kind === "invalid") {
|
|
187
|
-
ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
const entered = await ctx.ui.input(
|
|
191
|
-
"Fireworks account slug · submit blank for Auto",
|
|
192
|
-
state.settings.fireworksAccountId ?? "Example: acme",
|
|
193
|
-
{ signal },
|
|
194
|
-
);
|
|
195
|
-
if (signal.aborted || !isCurrent() || entered === undefined) return false;
|
|
196
|
-
const normalized = entered.trim();
|
|
197
|
-
const requested = normalized || undefined;
|
|
198
|
-
if (requested !== undefined && !isFireworksAccountId(requested)) {
|
|
199
|
-
ctx.ui.notify("Enter a URL-safe Fireworks account slug.", "warning");
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
if (requested === state.settings.fireworksAccountId) return false;
|
|
203
|
-
try {
|
|
204
|
-
await settingsRuntime.update({ fireworksAccountId: requested }, signal);
|
|
205
|
-
} catch (error) {
|
|
206
|
-
if (signal.aborted || !isCurrent()) return false;
|
|
207
|
-
ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
onApplied("fireworksAccountId");
|
|
211
|
-
return true;
|
|
212
|
-
}
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function displaySetting(id: UsageSettingId, value: UsageSettings[UsageSettingId]): string {
|
|
217
|
-
if (id === "fireworksAccountId") return typeof value === "string" ? value : AUTO;
|
|
218
|
-
return value ? ON : OFF;
|
|
107
|
+
parentSignal.addEventListener("abort", cancel, { once: true });
|
|
108
|
+
return {
|
|
109
|
+
render: (width: number) => container.render(width),
|
|
110
|
+
invalidate: () => container.invalidate(),
|
|
111
|
+
handleInput(data: string) {
|
|
112
|
+
if (closing) return;
|
|
113
|
+
if (matchesKey(data, Key.ctrl("c"))) cancel();
|
|
114
|
+
else settingsList.handleInput(data);
|
|
115
|
+
tui.requestRender();
|
|
116
|
+
},
|
|
117
|
+
dispose() {
|
|
118
|
+
localController.abort();
|
|
119
|
+
parentSignal.removeEventListener("abort", cancel);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
})) ?? false
|
|
123
|
+
);
|
|
219
124
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { sanitizeDisplayText } from "./core.js";
|
|
2
|
+
import type {
|
|
3
|
+
ResolvedUsageAuth,
|
|
4
|
+
UsageProviderAdapter,
|
|
5
|
+
UsageProviderTarget,
|
|
6
|
+
UsageRequestGuard,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
|
|
9
|
+
const MAX_TARGETS = 1_000;
|
|
10
|
+
const MAX_TARGET_ID_CHARS = 256;
|
|
11
|
+
const MAX_TARGET_LABEL_CHARS = 120;
|
|
12
|
+
const MAX_TARGET_DESCRIPTION_CHARS = 180;
|
|
13
|
+
|
|
14
|
+
export type UsageTargetResolution =
|
|
15
|
+
| { kind: "selected"; targetId?: string }
|
|
16
|
+
| {
|
|
17
|
+
kind: "selection-required";
|
|
18
|
+
choices: readonly UsageProviderTarget[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface UsageTargetSelectOptions {
|
|
22
|
+
options: readonly string[];
|
|
23
|
+
targetIdFor(option: string): string | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function resolveUsageTarget(
|
|
27
|
+
adapter: UsageProviderAdapter,
|
|
28
|
+
auth: ResolvedUsageAuth,
|
|
29
|
+
rememberedTargetId: string | undefined,
|
|
30
|
+
signal: AbortSignal,
|
|
31
|
+
timeoutMs: number,
|
|
32
|
+
guard: UsageRequestGuard,
|
|
33
|
+
): Promise<UsageTargetResolution> {
|
|
34
|
+
if (!adapter.targets) return { kind: "selected" };
|
|
35
|
+
if (rememberedTargetId !== undefined && !isBoundedTargetId(rememberedTargetId)) {
|
|
36
|
+
throw new Error(`The remembered ${adapter.targets.singularLabel} identifier was invalid.`);
|
|
37
|
+
}
|
|
38
|
+
const choices = await listUsageTargets(adapter, auth, signal, timeoutMs, guard);
|
|
39
|
+
if (rememberedTargetId) {
|
|
40
|
+
return choices.some((choice) => choice.id === rememberedTargetId)
|
|
41
|
+
? { kind: "selected", targetId: rememberedTargetId }
|
|
42
|
+
: { kind: "selection-required", choices };
|
|
43
|
+
}
|
|
44
|
+
if (choices.length === 1) return { kind: "selected", targetId: choices[0]?.id };
|
|
45
|
+
return { kind: "selection-required", choices };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function listUsageTargets(
|
|
49
|
+
adapter: UsageProviderAdapter,
|
|
50
|
+
auth: ResolvedUsageAuth,
|
|
51
|
+
signal: AbortSignal,
|
|
52
|
+
timeoutMs: number,
|
|
53
|
+
guard: UsageRequestGuard,
|
|
54
|
+
): Promise<readonly UsageProviderTarget[]> {
|
|
55
|
+
if (!adapter.targets) return [];
|
|
56
|
+
const startedAt = Date.now();
|
|
57
|
+
await guard();
|
|
58
|
+
const listed = await adapter.targets.list(
|
|
59
|
+
auth,
|
|
60
|
+
signal,
|
|
61
|
+
remainingTargetTimeout(timeoutMs, startedAt),
|
|
62
|
+
guard,
|
|
63
|
+
);
|
|
64
|
+
await guard();
|
|
65
|
+
remainingTargetTimeout(timeoutMs, startedAt);
|
|
66
|
+
const choices = normalizeUsageTargets(listed);
|
|
67
|
+
if (choices.length === 0) {
|
|
68
|
+
throw new Error(`${adapter.targets.pluralLabel} discovery returned no choices.`);
|
|
69
|
+
}
|
|
70
|
+
return choices;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function normalizeUsageTargets(
|
|
74
|
+
targets: readonly UsageProviderTarget[],
|
|
75
|
+
): readonly UsageProviderTarget[] {
|
|
76
|
+
if (!Array.isArray(targets)) throw new Error("Target discovery did not return a choices array.");
|
|
77
|
+
if (targets.length > MAX_TARGETS) {
|
|
78
|
+
throw new Error(`Target discovery exceeded ${MAX_TARGETS} choices.`);
|
|
79
|
+
}
|
|
80
|
+
const seen = new Set<string>();
|
|
81
|
+
return targets.map((target) => {
|
|
82
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) {
|
|
83
|
+
throw new Error("Target discovery returned an invalid choice.");
|
|
84
|
+
}
|
|
85
|
+
const { id, label, description } = target as Partial<UsageProviderTarget>;
|
|
86
|
+
if (!isBoundedTargetId(id)) throw new Error("Target discovery returned an invalid ID.");
|
|
87
|
+
if (seen.has(id)) throw new Error(`Target discovery repeated ${id}.`);
|
|
88
|
+
seen.add(id);
|
|
89
|
+
if (typeof label !== "string") {
|
|
90
|
+
throw new Error("Target discovery returned an invalid display label.");
|
|
91
|
+
}
|
|
92
|
+
if (description !== undefined && typeof description !== "string") {
|
|
93
|
+
throw new Error("Target discovery returned an invalid description.");
|
|
94
|
+
}
|
|
95
|
+
const safeLabel = sanitizeDisplayText(label, MAX_TARGET_LABEL_CHARS);
|
|
96
|
+
if (!safeLabel) throw new Error("Target discovery returned an empty display label.");
|
|
97
|
+
const safeDescription = description
|
|
98
|
+
? sanitizeDisplayText(description, MAX_TARGET_DESCRIPTION_CHARS)
|
|
99
|
+
: undefined;
|
|
100
|
+
return { id, label: safeLabel, ...(safeDescription ? { description: safeDescription } : {}) };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function createUsageTargetSelectOptions(
|
|
105
|
+
targets: readonly UsageProviderTarget[],
|
|
106
|
+
): UsageTargetSelectOptions {
|
|
107
|
+
const normalized = normalizeUsageTargets(targets);
|
|
108
|
+
const ids = new Map<string, string>();
|
|
109
|
+
const options = normalized.map((target) => {
|
|
110
|
+
const base = target.description ? `${target.label} — ${target.description}` : target.label;
|
|
111
|
+
let option = base;
|
|
112
|
+
if (ids.has(option)) {
|
|
113
|
+
const safeId = sanitizeDisplayText(target.id, 80) || "target";
|
|
114
|
+
option = `${base} · ${safeId}`;
|
|
115
|
+
let duplicate = 2;
|
|
116
|
+
while (ids.has(option)) {
|
|
117
|
+
option = `${base} · ${safeId} (${duplicate})`;
|
|
118
|
+
duplicate += 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
ids.set(option, target.id);
|
|
122
|
+
return option;
|
|
123
|
+
});
|
|
124
|
+
return { options, targetIdFor: (option) => ids.get(option) };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function remainingTargetTimeout(timeoutMs: number, startedAt: number): number {
|
|
128
|
+
const remaining = timeoutMs - (Date.now() - startedAt);
|
|
129
|
+
if (remaining <= 0) throw new Error("Timed out while discovering provider targets.");
|
|
130
|
+
return remaining;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function isBoundedTargetId(value: unknown): value is string {
|
|
134
|
+
return (
|
|
135
|
+
typeof value === "string" &&
|
|
136
|
+
value.length > 0 &&
|
|
137
|
+
value.length <= MAX_TARGET_ID_CHARS &&
|
|
138
|
+
![...value].some((character) => {
|
|
139
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
140
|
+
return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
}
|