pi-usereq 0.6.0 → 0.7.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.
@@ -26,10 +26,21 @@ export interface PiUsereqSettingsMenuChoice {
26
26
  export interface PiUsereqSettingsMenuBridge {
27
27
  title: string;
28
28
  choices: PiUsereqSettingsMenuChoice[];
29
+ selectedChoiceId?: string;
29
30
  selectByLabel: (label: string) => boolean;
30
31
  cancel: () => void;
31
32
  }
32
33
 
34
+ /**
35
+ * @brief Describes optional behavior overrides for one settings-menu render.
36
+ * @details Carries the caller-selected initial focus row so menu re-renders can
37
+ * preserve selection after an in-place toggle or value edit. The interface is
38
+ * compile-time only and introduces no runtime cost.
39
+ */
40
+ export interface PiUsereqSettingsMenuOptions {
41
+ initialSelectedId?: string;
42
+ }
43
+
33
44
  /**
34
45
  * @brief Represents a custom menu component augmented with the offline bridge.
35
46
  * @details Extends the generic TUI `Component` contract with one optional bridge field consumed only by deterministic test and debug harness adapters. The interface is compile-time only and introduces no runtime cost.
@@ -177,13 +188,15 @@ function buildSettingItems(
177
188
  * @param[in] ctx {ExtensionCommandContext} Active command context.
178
189
  * @param[in] title {string} Menu title displayed in the heading and offline bridge.
179
190
  * @param[in] choices {PiUsereqSettingsMenuChoice[]} Ordered menu-choice vector.
191
+ * @param[in] options {PiUsereqSettingsMenuOptions | undefined} Optional initial-focus override.
180
192
  * @return {Promise<string | undefined>} Selected choice identifier or `undefined` when cancelled.
181
- * @satisfies REQ-151, REQ-152, REQ-153, REQ-154, REQ-156
193
+ * @satisfies REQ-151, REQ-152, REQ-153, REQ-154, REQ-156, REQ-192
182
194
  */
183
195
  export async function showPiUsereqSettingsMenu(
184
196
  ctx: ExtensionCommandContext,
185
197
  title: string,
186
198
  choices: PiUsereqSettingsMenuChoice[],
199
+ options: PiUsereqSettingsMenuOptions = {},
187
200
  ): Promise<string | undefined> {
188
201
  return ctx.ui.custom<string | undefined>((tui, theme, _keybindings, done) => {
189
202
  const container = new Container();
@@ -199,6 +212,12 @@ export async function showPiUsereqSettingsMenu(
199
212
  () => undefined,
200
213
  () => done(undefined),
201
214
  );
215
+ const initialSelectedIndex = options.initialSelectedId === undefined
216
+ ? 0
217
+ : choices.findIndex((choice) => choice.id === options.initialSelectedId);
218
+ if (initialSelectedIndex >= 0) {
219
+ (settingsList as SettingsList & { selectedIndex: number }).selectedIndex = initialSelectedIndex;
220
+ }
202
221
  container.addChild(titleText);
203
222
  container.addChild(new Text("", 0, 0));
204
223
  container.addChild(settingsList);
@@ -218,6 +237,7 @@ export async function showPiUsereqSettingsMenu(
218
237
  __piUsereqSettingsMenu: {
219
238
  title,
220
239
  choices,
240
+ selectedChoiceId: initialSelectedIndex >= 0 ? choices[initialSelectedIndex]?.id : undefined,
221
241
  selectByLabel(label: string): boolean {
222
242
  const choice = choices.find(
223
243
  (candidate) => candidate.label === label || candidate.id === label,
@@ -6,9 +6,10 @@
6
6
 
7
7
  import fs from "node:fs";
8
8
  import path from "node:path";
9
- import { getEncoding } from "js-tiktoken";
9
+ import { createRequire } from "node:module";
10
10
  import { detectLanguage as detectSourceLanguage } from "./compress.js";
11
11
  import { parseDoxygenComment, type DoxygenFieldMap } from "./doxygen-parser.js";
12
+ import { ReqError } from "./errors.js";
12
13
 
13
14
  /**
14
15
  * @brief Declares the tokenizer encoding used by token-count workflows.
@@ -196,6 +197,57 @@ export interface BuildTokenToolPayloadOptions {
196
197
  encodingName?: string;
197
198
  }
198
199
 
200
+ type TokenCounterEncoding = {
201
+ encode(content: string): ArrayLike<number>;
202
+ };
203
+
204
+ type JsTiktokenModule = {
205
+ getEncoding(encodingName: string): TokenCounterEncoding;
206
+ };
207
+
208
+ const require = createRequire(import.meta.url);
209
+
210
+ function defaultJsTiktokenModuleLoader(): JsTiktokenModule {
211
+ return require("js-tiktoken") as JsTiktokenModule;
212
+ }
213
+
214
+ let jsTiktokenModuleLoader: () => JsTiktokenModule = defaultJsTiktokenModuleLoader;
215
+
216
+ /**
217
+ * @brief Overrides the `js-tiktoken` loader for tests.
218
+ * @details Enables deterministic dependency-failure tests without mutating repository dependencies on disk. Runtime is O(1). Side effects are limited to module-local test state.
219
+ * @param[in] loader {(() => JsTiktokenModule) | undefined} Replacement loader, or `undefined` to restore the default loader.
220
+ * @return {void} No return value.
221
+ */
222
+ export function setJsTiktokenModuleLoaderForTests(loader?: () => JsTiktokenModule): void {
223
+ jsTiktokenModuleLoader = loader ?? defaultJsTiktokenModuleLoader;
224
+ }
225
+
226
+ /**
227
+ * @brief Loads the `js-tiktoken` module on demand.
228
+ * @details Defers dependency resolution until token counting is requested so extension registration can succeed even when the optional runtime dependency has not yet been installed. Runtime is O(1) plus module resolution cost. Side effects are limited to Node module loading.
229
+ * @return {JsTiktokenModule} Loaded tokenizer module.
230
+ * @throws {ReqError} Throws when `js-tiktoken` is unavailable.
231
+ */
232
+ function loadJsTiktokenModule(): JsTiktokenModule {
233
+ try {
234
+ const module = jsTiktokenModuleLoader();
235
+ if (!module || typeof module.getEncoding !== "function") {
236
+ throw new ReqError("Error: token-count dependency 'js-tiktoken' is unavailable. Run `npm ci` in the PI-useReq repository.", 1);
237
+ }
238
+ return module;
239
+ } catch (error) {
240
+ if (error instanceof ReqError) {
241
+ throw error;
242
+ }
243
+ const message = error instanceof Error ? error.message : String(error);
244
+ if (/Cannot find (module|package) 'js-tiktoken'|ERR_MODULE_NOT_FOUND/.test(message)) {
245
+ throw new ReqError("Error: token-count dependency 'js-tiktoken' is not installed. Run `npm ci` in the PI-useReq repository.", 1);
246
+ }
247
+ throw error;
248
+ }
249
+ }
250
+
199
251
  /**
200
252
  * @brief Encapsulates one tokenizer instance for repeated token counting.
201
253
  * @details Caches a `js-tiktoken` encoding object so multiple documents can be counted without repeated encoding lookup. Counting cost is O(n) in content length. The class mutates only instance state during construction.
@@ -205,7 +257,7 @@ export class TokenCounter {
205
257
  * @brief Stores the tokenizer implementation used for subsequent counts.
206
258
  * @details The field holds the encoder returned by `getEncoding`. Access complexity is O(1). The value is initialized once per instance.
207
259
  */
208
- private encoding;
260
+ private encoding: TokenCounterEncoding;
209
261
 
210
262
  /**
211
263
  * @brief Initializes a token counter for one encoding family.
@@ -214,7 +266,7 @@ export class TokenCounter {
214
266
  * @return {TokenCounter} New token counter instance.
215
267
  */
216
268
  constructor(encodingName = TOKEN_COUNTER_ENCODING) {
217
- this.encoding = getEncoding(encodingName);
269
+ this.encoding = loadJsTiktokenModule().getEncoding(encodingName);
218
270
  }
219
271
 
220
272
  /**