pi-usereq 0.60.0 → 0.62.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PI-useReq/pi-usereq (0.60.0)
1
+ # PI-useReq/pi-usereq (0.62.0)
2
2
 
3
3
  <p align="center">
4
4
  <img src="https://img.shields.io/badge/python-3.11%2B-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python 3.11+">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-usereq",
3
- "version": "0.60.0",
3
+ "version": "0.62.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Ogekuri/PI-useReq.git"
@@ -32,9 +32,9 @@
32
32
  ]
33
33
  },
34
34
  "peerDependencies": {
35
- "@earendil-works/pi-ai": "^0.80.4",
36
- "@earendil-works/pi-coding-agent": "^0.87.1",
37
- "@earendil-works/pi-tui": "^0.80.4",
35
+ "@earendil-works/pi-ai": "^1.0.0",
36
+ "@earendil-works/pi-coding-agent": "^1.0.0",
37
+ "@earendil-works/pi-tui": "^1.0.0",
38
38
  "@sinclair/typebox": "^0.34.49"
39
39
  },
40
40
  "dependencies": {
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @file
3
+ * @brief Measures runtime character and token sizes of the canonical context files.
4
+ * @details Provides reusable helpers that compute character and `cl100k_base` token estimates for `REQUIREMENTS.md`, `REFERENCES.md`, and `WORKFLOW.md` under `<base-path>/<docs-dir>` so configuration menus and command summaries share one measurement contract. Runtime is O(n) in aggregate context-file size. Side effects are limited to filesystem reads.
5
+ */
6
+
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { DEFAULT_DOCS_DIR, type UseReqConfig } from "./config.js";
10
+ import { normalizeRelativeDirContract } from "./path-context.js";
11
+ import { countFileMetrics } from "./token-counter.js";
12
+
13
+ /**
14
+ * @brief Stores the canonical context-file names in their documented injection order.
15
+ * @details The tuple is the single ordering source reused by measurement helpers and size renderers so every caller reports `REQUIREMENTS.md`, `REFERENCES.md`, and `WORKFLOW.md` consistently. Lookup complexity is O(1).
16
+ * @satisfies REQ-373
17
+ */
18
+ export const CONTEXT_FILE_NAMES = ["REQUIREMENTS.md", "REFERENCES.md", "WORKFLOW.md"] as const;
19
+
20
+ /**
21
+ * @brief Narrows context-file identifiers to the canonical measured document set.
22
+ * @details Compile-time alias derived from `CONTEXT_FILE_NAMES` and reused by the measured-facts record keys. The alias introduces no runtime cost.
23
+ */
24
+ export type ContextFileName = (typeof CONTEXT_FILE_NAMES)[number];
25
+
26
+ /**
27
+ * @brief Describes the measured size facts of one canonical context file.
28
+ * @details Stores the filesystem existence flag plus the exact character count and the `cl100k_base` token estimate computed from the file content. The interface is compile-time only and introduces no runtime cost.
29
+ * @satisfies REQ-374
30
+ */
31
+ export interface ContextFileSizeFacts {
32
+ readonly exists: boolean;
33
+ readonly chars: number;
34
+ readonly tokens: number;
35
+ }
36
+
37
+ /**
38
+ * @brief Stores the canonical missing-file size facts.
39
+ * @details Reused as the deterministic result for missing, non-file, and unreadable targets so callers never receive `undefined` facts. Lookup complexity is O(1).
40
+ * @satisfies REQ-374
41
+ */
42
+ export const MISSING_CONTEXT_FILE_SIZE: ContextFileSizeFacts = Object.freeze({ exists: false, chars: 0, tokens: 0 });
43
+
44
+ /**
45
+ * @brief Resolves one canonical context-file absolute path for one project base.
46
+ * @details Joins the project base with the trailing-slash-free configured `docs-dir` (falling back to `DEFAULT_DOCS_DIR`) and the supplied canonical file name so menus, summaries, and the `%%CONTEXT_FILES%%` renderer address identical targets. Runtime is O(p) in path length. No external state is mutated.
47
+ * @param[in] projectBase {string} Absolute project root path.
48
+ * @param[in] config {UseReqConfig} Effective project configuration supplying the docs directory.
49
+ * @param[in] fileName {ContextFileName} Canonical context-file name.
50
+ * @return {string} Absolute context-file path.
51
+ * @satisfies REQ-373
52
+ */
53
+ export function resolveContextFilePath(
54
+ projectBase: string,
55
+ config: UseReqConfig,
56
+ fileName: ContextFileName,
57
+ ): string {
58
+ const normalizedDocsDir = normalizeRelativeDirContract(config["docs-dir"]) || DEFAULT_DOCS_DIR;
59
+ return path.join(projectBase, normalizedDocsDir, fileName);
60
+ }
61
+
62
+ /**
63
+ * @brief Measures one context file into deterministic size facts.
64
+ * @details Probes filesystem existence and file kind, reads UTF-8 content, and reuses the shared `countFileMetrics` tokenizer for the `cl100k_base` token estimate. Missing, non-file, and unreadable targets return `MISSING_CONTEXT_FILE_SIZE` facts without throwing. Runtime is O(n) in file size. Side effects are limited to filesystem reads.
65
+ * @param[in] filePath {string} Absolute context-file path to measure.
66
+ * @return {ContextFileSizeFacts} Measured size facts for the target.
67
+ * @satisfies REQ-373, REQ-374
68
+ */
69
+ export function measureContextFileSize(filePath: string): ContextFileSizeFacts {
70
+ try {
71
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
72
+ return MISSING_CONTEXT_FILE_SIZE;
73
+ }
74
+ const metrics = countFileMetrics(fs.readFileSync(filePath, "utf8"));
75
+ return { exists: true, chars: metrics.chars, tokens: metrics.tokens };
76
+ } catch {
77
+ return MISSING_CONTEXT_FILE_SIZE;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * @brief Measures every canonical context file for one project base.
83
+ * @details Iterates `CONTEXT_FILE_NAMES` in documented order, resolves each configured `<base-path>/<docs-dir>` target through `resolveContextFilePath`, and returns the keyed facts record consumed by configuration menus and command summaries. Runtime is O(n) in aggregate context-file size. Side effects are limited to filesystem reads.
84
+ * @param[in] projectBase {string} Absolute project root path.
85
+ * @param[in] config {UseReqConfig} Effective project configuration supplying the docs directory.
86
+ * @return {Record<ContextFileName, ContextFileSizeFacts>} Measured size facts keyed by canonical file name.
87
+ * @satisfies REQ-373, REQ-374
88
+ */
89
+ export function measureContextFileSizes(
90
+ projectBase: string,
91
+ config: UseReqConfig,
92
+ ): Record<ContextFileName, ContextFileSizeFacts> {
93
+ const facts = {} as Record<ContextFileName, ContextFileSizeFacts>;
94
+ for (const fileName of CONTEXT_FILE_NAMES) {
95
+ facts[fileName] = measureContextFileSize(resolveContextFilePath(projectBase, config, fileName));
96
+ }
97
+ return facts;
98
+ }
99
+
100
+ /**
101
+ * @brief Formats one measured context-file size as a compact character and token estimate.
102
+ * @details Emits the deterministic `<chars>c/<tokens>t` shape reused by menu rows, the top-level summary, and the command invocation summary so every surface exposes identical size facts. Runtime is O(n) in rendered length. No external state is mutated.
103
+ * @param[in] facts {ContextFileSizeFacts} Measured context-file size facts.
104
+ * @return {string} Compact `<chars>c/<tokens>t` size string.
105
+ * @satisfies REQ-375, REQ-376, REQ-377
106
+ */
107
+ export function formatContextFileSize(facts: ContextFileSizeFacts): string {
108
+ return `${facts.chars}c/${facts.tokens}t`;
109
+ }
@@ -63,6 +63,126 @@ export interface PromptCommandExecutionPlan {
63
63
  worktreeDir?: string;
64
64
  worktreePath?: string;
65
65
  worktreeRootPath?: string;
66
+ originalModel?: PromptCommandModelSelection;
67
+ originalThinkingLevel?: string;
68
+ }
69
+
70
+ /**
71
+ * @brief Describes one serializable model-selection fact captured at prompt-command preflight.
72
+ * @details Stores the active model provider and identifier as plain strings so the selection can travel inside the prompt execution plan and process-scoped persistence across session replacement without retaining live SDK model objects, and MUST NOT be persisted into pi-usereq configuration files per CTN-022. The interface is compile-time only and introduces no runtime cost.
73
+ * @satisfies REQ-366, CTN-022
74
+ */
75
+ export interface PromptCommandModelSelection {
76
+ provider: string;
77
+ modelId: string;
78
+ }
79
+
80
+ /**
81
+ * @brief Describes the loosely-typed runtime surface required to re-apply one captured selection.
82
+ * @details Narrows the pi runtime to the model registry lookup, the active model plus thinking-level probes used by the duplicate-skip guard, and the session-scoped `setModel`/`setThinkingLevel` appliers that leave the configured default model untouched. All members stay optional so legacy hosts and offline replays degrade to a documented warning instead of aborting orchestration. The interface is compile-time only and introduces no runtime cost.
83
+ * @satisfies REQ-367, REQ-370, REQ-371
84
+ */
85
+ export interface PromptCommandModelSelectionSurface {
86
+ modelRegistry?: { find?: (provider: unknown, modelId: unknown) => unknown };
87
+ model?: unknown;
88
+ thinkingLevel?: unknown;
89
+ setModel?: (model: unknown) => Promise<boolean>;
90
+ setThinkingLevel?: (level: unknown) => void;
91
+ }
92
+
93
+ /**
94
+ * @brief Describes the re-apply request consumed by `reapplyPromptCommandSessionSelection`.
95
+ * @details Carries the captured selection plus thinking level, the runtime surface, and the optional stale-context-safe warning callback so callers keep notification ownership inside `src/index.ts` while the helper stays independent from extension UI types per DES-012. The interface is compile-time only and introduces no runtime cost.
96
+ */
97
+ export interface PromptCommandModelSelectionReapplyOptions {
98
+ selection: PromptCommandModelSelection | undefined;
99
+ thinkingLevel: string | undefined;
100
+ surface: PromptCommandModelSelectionSurface | undefined;
101
+ notifyWarning?: (message: string) => void;
102
+ }
103
+
104
+ /**
105
+ * @brief Describes the deterministic outcome facts of one guarded selection re-application.
106
+ * @details Distinguishes not-attempted skips (missing selection, missing surface, or post-switch state already matching the captured selection) from attempted applications, and carries the normalized warning message emitted when resolution, authentication, or the runtime applier fails. The interface is compile-time only and introduces no runtime cost.
107
+ * @satisfies REQ-370, REQ-372
108
+ */
109
+ export interface PromptCommandModelSelectionReapplyResult {
110
+ attempted: boolean;
111
+ applied: boolean;
112
+ skipped: boolean;
113
+ warningMessage?: string;
114
+ }
115
+
116
+ /**
117
+ * @brief Re-applies one captured model selection onto the active post-switch session.
118
+ * @details Resolves the captured provider plus identifier through the surface model registry, skips the whole operation when the selection or surface is unavailable and when the active model and thinking level already match the captured selection so no duplicate `model_change` session entry is emitted, otherwise awaits the session-scoped `setModel` applier, then re-applies the captured thinking level when the active level differs. Every resolution, authentication, and applier failure degrades into one warning notification through the supplied callback without throwing, so prompt orchestration and worktree closure continue unchanged. Runtime is O(1) plus one awaited model mutation. No external state is mutated beyond the applied session selection.
119
+ * @param[in] options {PromptCommandModelSelectionReapplyOptions} Captured selection, runtime surface, and warning callback.
120
+ * @return {Promise<PromptCommandModelSelectionReapplyResult>} Attempted, applied, skipped, and warning facts for the re-application.
121
+ * @satisfies REQ-366, REQ-367, REQ-368, REQ-369, REQ-370, REQ-371, REQ-372, DES-021
122
+ */
123
+ export async function reapplyPromptCommandSessionSelection(
124
+ options: PromptCommandModelSelectionReapplyOptions,
125
+ ): Promise<PromptCommandModelSelectionReapplyResult> {
126
+ const selection = options.selection;
127
+ const surface = options.surface;
128
+ if (!selection || !surface) {
129
+ return { attempted: false, applied: false, skipped: true };
130
+ }
131
+ const notifyWarning = options.notifyWarning;
132
+ const registryFind = surface.modelRegistry?.find;
133
+ const resolvedCandidate = typeof registryFind === "function"
134
+ ? registryFind.call(surface.modelRegistry, selection.provider, selection.modelId)
135
+ : undefined;
136
+ const resolved = resolvedCandidate as { provider?: unknown; id?: unknown } | null | undefined;
137
+ if (!resolved || typeof resolved.provider !== "string" || typeof resolved.id !== "string") {
138
+ const message = `WARNING: Could not re-apply model ${selection.provider}/${selection.modelId}: model is not available in the active model registry.`;
139
+ notifyWarning?.(message);
140
+ return { attempted: true, applied: false, skipped: false, warningMessage: message };
141
+ }
142
+ const currentModel = surface.model as { provider?: unknown; id?: unknown } | null | undefined;
143
+ const modelMatches = currentModel !== null
144
+ && currentModel !== undefined
145
+ && currentModel.provider === selection.provider
146
+ && currentModel.id === selection.modelId;
147
+ const levelMatches = typeof options.thinkingLevel !== "string"
148
+ || surface.thinkingLevel === options.thinkingLevel;
149
+ if (modelMatches && levelMatches) {
150
+ return { attempted: false, applied: false, skipped: true };
151
+ }
152
+ if (!modelMatches) {
153
+ const setModelFn = surface.setModel;
154
+ if (typeof setModelFn !== "function") {
155
+ const message = `WARNING: Could not re-apply model ${selection.provider}/${selection.modelId}: the runtime does not expose setModel.`;
156
+ notifyWarning?.(message);
157
+ return { attempted: true, applied: false, skipped: false, warningMessage: message };
158
+ }
159
+ let setModelResult: unknown;
160
+ try {
161
+ setModelResult = await setModelFn.call(surface, resolved);
162
+ } catch (error) {
163
+ const message = `WARNING: Could not re-apply model ${selection.provider}/${selection.modelId}: ${error instanceof Error ? error.message : String(error)}.`;
164
+ notifyWarning?.(message);
165
+ return { attempted: true, applied: false, skipped: false, warningMessage: message };
166
+ }
167
+ if (setModelResult === false) {
168
+ const message = `WARNING: Could not re-apply model ${selection.provider}/${selection.modelId}: no API key configured for ${selection.provider}.`;
169
+ notifyWarning?.(message);
170
+ return { attempted: true, applied: false, skipped: false, warningMessage: message };
171
+ }
172
+ }
173
+ const setThinkingLevelFn = surface.setThinkingLevel;
174
+ if (
175
+ typeof options.thinkingLevel === "string"
176
+ && surface.thinkingLevel !== options.thinkingLevel
177
+ && typeof setThinkingLevelFn === "function"
178
+ ) {
179
+ try {
180
+ setThinkingLevelFn.call(surface, options.thinkingLevel);
181
+ } catch {
182
+ // REQ-372: thinking-level failures are advisory-only and MUST NOT abort orchestration.
183
+ }
184
+ }
185
+ return { attempted: true, applied: true, skipped: false };
66
186
  }
67
187
 
68
188
  /**
@@ -109,11 +229,11 @@ interface PromptCommandSessionSwitchOptions {
109
229
 
110
230
  /**
111
231
  * @brief Describes the minimal session-bound surface available after session replacement.
112
- * @details Extends the shared prompt-command context with `sendUserMessage(...)` so prompt dispatch can target the replacement session without reusing stale pre-switch runtime objects. The interface is compile-time only and introduces no runtime cost.
232
+ * @details Extends the shared prompt-command context with a string-only `sendUserMessage(...)` probe so prompt dispatch can target the replacement session without reusing stale pre-switch runtime objects. The string content parameter mirrors the prompt-delivery contract consumed by `deliverPromptCommand(...)` while remaining structurally compatible with the pi 1.0.0 replacement-session delivery signatures. The interface is compile-time only and introduces no runtime cost.
113
233
  */
114
234
  interface PromptCommandActiveContext extends PromptCommandSessionContext {
115
235
  sendUserMessage?: (
116
- content: string | Array<{ type: string; text?: string }>,
236
+ content: string,
117
237
  options?: PromptCommandSessionMessageOptions,
118
238
  ) => Promise<void> | void;
119
239
  }
@@ -1584,9 +1704,11 @@ export function validatePromptRequiredDocs(
1584
1704
  * @param[in] currentSessionDir {string | undefined} Active session directory reused when the execution session is forked.
1585
1705
  * @param[in] currentSessionBranch {PromptCommandSessionEntry[] | undefined} Active in-memory session branch copied when the origin session file is not flushed yet.
1586
1706
  * @param[in] debugOptions {PromptCommandDebugOptions | undefined} Optional prompt debug logging context.
1707
+ * @param[in] originalModel {PromptCommandModelSelection | undefined} Active model provider plus identifier captured by the command handler for later re-application.
1708
+ * @param[in] originalThinkingLevel {string | undefined} Active thinking level captured by the command handler for later re-application.
1587
1709
  * @return {PromptCommandExecutionPlan} Prepared execution plan.
1588
1710
  * @throws {ReqError} Throws when repository validation, required-doc validation, worktree creation, or session preparation fails.
1589
- * @satisfies REQ-200, REQ-203, REQ-206, REQ-207, REQ-215, REQ-219, REQ-220, REQ-245, REQ-256, REQ-271
1711
+ * @satisfies REQ-200, REQ-203, REQ-206, REQ-207, REQ-215, REQ-219, REQ-220, REQ-245, REQ-256, REQ-271, REQ-366
1590
1712
  */
1591
1713
  export function preparePromptCommandExecution(
1592
1714
  promptName: PromptCommandName,
@@ -1597,6 +1719,8 @@ export function preparePromptCommandExecution(
1597
1719
  currentSessionDir: string | undefined,
1598
1720
  currentSessionBranch: PromptCommandSessionEntry[] | undefined,
1599
1721
  debugOptions?: PromptCommandDebugOptions,
1722
+ originalModel?: PromptCommandModelSelection,
1723
+ originalThinkingLevel?: string,
1600
1724
  ): PromptCommandExecutionPlan {
1601
1725
  const gitPath = validatePromptGitState(projectBase, config);
1602
1726
  const reuseCurrentSessionFile = isUsablePromptSessionFile(currentSessionFile, projectBase);
@@ -1624,6 +1748,8 @@ export function preparePromptCommandExecution(
1624
1748
  baseDir: staticWorktreePaths.baseDir,
1625
1749
  originalSessionFile,
1626
1750
  executionSessionFile: originalSessionFile,
1751
+ originalModel,
1752
+ originalThinkingLevel,
1627
1753
  };
1628
1754
  }
1629
1755
  const worktreeDir = buildPromptWorktreeName(gitPath, config);
@@ -1663,6 +1789,8 @@ export function preparePromptCommandExecution(
1663
1789
  worktreeDir: worktreePaths.worktreeDir,
1664
1790
  worktreePath: worktreePaths.worktreePath,
1665
1791
  worktreeRootPath: worktreePaths.worktreeRootPath,
1792
+ originalModel,
1793
+ originalThinkingLevel,
1666
1794
  };
1667
1795
  }
1668
1796
 
@@ -209,3 +209,49 @@ export function restorePersistedPromptCommandRuntimeStateForSession(
209
209
  }
210
210
  return undefined;
211
211
  }
212
+
213
+ /**
214
+ * @brief Describes the loosely-typed extension-api surface persisted for model re-application.
215
+ * @details Stores only the session-scoped model appliers plus thinking-level probe needed to re-apply one captured selection after session replacement. The structural shape avoids importing concrete pi SDK types into this process-scoped module. The interface is compile-time only and introduces no runtime cost.
216
+ * @satisfies REQ-367, REQ-368, REQ-369
217
+ */
218
+ export interface PersistedPromptCommandRuntimeApi {
219
+ setModel?: (model: unknown) => unknown;
220
+ setThinkingLevel?: (level: unknown) => unknown;
221
+ getThinkingLevel?: () => unknown;
222
+ }
223
+
224
+ /**
225
+ * @brief Defines the global property used for latest live extension-api persistence.
226
+ * @details The property lives on `globalThis` because pi invalidates the extension api captured by one session's extension instance whenever that session is replaced and reloads the extension against the replacement session while the host process persists. Access complexity is O(1).
227
+ */
228
+ const PI_USEREQ_PROMPT_COMMAND_RUNTIME_API_KEY = "__piUsereqPromptCommandRuntimeApi";
229
+
230
+ /**
231
+ * @brief Writes the latest live extension api into process-scoped persistence.
232
+ * @details Replaces the stored loosely-typed model-api surface so model re-application helpers always bind to the extension instance of the currently active session runner and never to an api invalidated by session replacement. Runtime is O(1). Side effect: mutates process-scoped state.
233
+ * @param[in] runtimeApi {PersistedPromptCommandRuntimeApi | undefined} Latest extension-api surface written at extension bind time, or undefined to clear the stored surface.
234
+ * @return {void} No return value.
235
+ * @satisfies REQ-367, REQ-368, REQ-369
236
+ */
237
+ export function writePersistedPromptCommandRuntimeApi(
238
+ runtimeApi: PersistedPromptCommandRuntimeApi | undefined,
239
+ ): void {
240
+ const globalState = globalThis as typeof globalThis & {
241
+ __piUsereqPromptCommandRuntimeApi?: PersistedPromptCommandRuntimeApi | undefined;
242
+ };
243
+ globalState[PI_USEREQ_PROMPT_COMMAND_RUNTIME_API_KEY] = runtimeApi;
244
+ }
245
+
246
+ /**
247
+ * @brief Reads the latest live extension api from process-scoped persistence.
248
+ * @details Returns the extension-api surface written by the most recent extension bind so post-switch model re-application uses a session-scoped api that remains valid after session replacement, returning undefined so callers degrade to their own captured api. Runtime is O(1). No external state is mutated.
249
+ * @return {PersistedPromptCommandRuntimeApi | undefined} Latest stored extension-api surface, or undefined when no extension has bound in this process.
250
+ * @satisfies REQ-367, REQ-368, REQ-369
251
+ */
252
+ export function readPersistedPromptCommandRuntimeApi(): PersistedPromptCommandRuntimeApi | undefined {
253
+ const globalState = globalThis as typeof globalThis & {
254
+ __piUsereqPromptCommandRuntimeApi?: PersistedPromptCommandRuntimeApi | undefined;
255
+ };
256
+ return globalState[PI_USEREQ_PROMPT_COMMAND_RUNTIME_API_KEY];
257
+ }
@@ -17,6 +17,7 @@ import {
17
17
  comparePiUsereqStartupToolNames,
18
18
  type PiUsereqStartupToolName,
19
19
  } from "./pi-usereq-tools.js";
20
+ import { formatContextFileSize, type ContextFileSizeFacts } from "./context-file-size.js";
20
21
  import { formatRuntimePathForDisplay, normalizeRelativeDirContract } from "./path-context.js";
21
22
  import type {
22
23
  PromptCommandExecutionPlan,
@@ -374,21 +375,27 @@ export const PROMPT_COMMAND_SUMMARY_CUSTOM_TYPE = "pi-usereq-prompt-command";
374
375
 
375
376
  /**
376
377
  * @brief Builds the on-screen command invocation summary for one bundled prompt-backed `req-<prompt>` command.
377
- * @details Renders the command name without the `req-` prefix in uppercase, the user request arguments, and the active configuration fields (`docs-dir`, `src-dir`, `tests-dir`, enabled context files, `AUTO_GIT_COMMIT`, effective `GIT_WORKTREE_ENABLED`, `GIT_WORKTREE_PREFIX`, enabled static-check languages, and `enabled-tools`) so the TUI shows only a compact summary while the full rendered prompt is delivered hidden to the LLM agent. Static-check languages are emitted in canonical `DEFAULT_STATIC_CHECK_LANGUAGES` order; enabled tools are emitted in documented menu order via `comparePiUsereqStartupToolNames`. The `context files`, `static code checks`, and `enabled tools` fields render the literal `none` placeholder whenever their respective enabled-item list is empty so the summary never shows a blank value. Sections are emitted in the order `Command:`, `Configuration:` with its bullet list, then `User's Request:`, each separated by one blank line. Runtime is O(l + t log t) where l is language count and t is enabled-tool count. No external state is mutated.
378
+ * @details Renders the command name without the `req-` prefix in uppercase, the user request arguments, and the active configuration fields (`docs-dir`, `src-dir`, `tests-dir`, enabled context files, `AUTO_GIT_COMMIT`, effective `GIT_WORKTREE_ENABLED`, `GIT_WORKTREE_PREFIX`, enabled static-check languages, and `enabled-tools`) so the TUI shows only a compact summary while the full rendered prompt is delivered hidden to the LLM agent. When measured context-file size facts are supplied, every enabled existing context file in the `context files` field is rendered as `name(<chars>c/<tokens>t)`; without facts, or for enabled missing files, the plain `name` form is rendered. Static-check languages are emitted in canonical `DEFAULT_STATIC_CHECK_LANGUAGES` order; enabled tools are emitted in documented menu order via `comparePiUsereqStartupToolNames`. The `context files`, `static code checks`, and `enabled tools` fields render the literal `none` placeholder whenever their respective enabled-item list is empty so the summary never shows a blank value. Sections are emitted in the order `Command:`, `Configuration:` with its bullet list, then `User's Request:`, each separated by one blank line. Runtime is O(l + t log t) where l is language count and t is enabled-tool count. No external state is mutated.
378
379
  * @param[in] promptName {string} Bundled prompt name without the `req-` prefix.
379
380
  * @param[in] args {string} User request arguments passed to the slash command.
380
381
  * @param[in] config {UseReqConfig} Effective project configuration supplying directory, git, static-check, and tool fields.
382
+ * @param[in] contextFileSizes {Record<string, ContextFileSizeFacts> | undefined} Optional measured context-file size facts keyed by canonical file name, consumed for `context files` size suffixes.
381
383
  * @return {string} Multi-line command invocation summary text.
382
- * @satisfies REQ-335, REQ-336, REQ-337, REQ-338, REQ-353
384
+ * @satisfies REQ-335, REQ-336, REQ-337, REQ-338, REQ-353, REQ-377
383
385
  */
384
386
  export function renderPromptCommandSummary(
385
387
  promptName: string,
386
388
  args: string,
387
389
  config: UseReqConfig,
390
+ contextFileSizes?: Record<string, ContextFileSizeFacts>,
388
391
  ): string {
389
392
  const contextFiles = CONTEXT_FILE_DESCRIPTORS
390
393
  .filter((descriptor) => config[descriptor.flagKey])
391
- .map((descriptor) => descriptor.fileName.replace(/\.md$/, "").toLowerCase())
394
+ .map((descriptor) => {
395
+ const baseName = descriptor.fileName.replace(/\.md$/, "").toLowerCase();
396
+ const facts = contextFileSizes?.[descriptor.fileName];
397
+ return facts?.exists ? `${baseName}(${formatContextFileSize(facts)})` : baseName;
398
+ })
392
399
  .join(", ") || "none";
393
400
  const enabledLanguages = DEFAULT_STATIC_CHECK_LANGUAGES
394
401
  .filter((language) => config["static-check"][language]?.enabled === "enable")
@@ -404,7 +404,7 @@ export class StaticCheckCommand extends StaticCheckBase {
404
404
  throw new ReqError(`Error: external command '${cmd}' not found on PATH.`, 1);
405
405
  }
406
406
  super(inputs, extraArgs, failOnly);
407
- this.cmd = cmd.includes("%%INSTALLATION_PATH%%") ? resolvedCmd : cmd;
407
+ this.cmd = resolvedCmd;
408
408
  this.label = `Command[${cmd}]`;
409
409
  }
410
410
 
package/src/index.ts CHANGED
@@ -83,6 +83,12 @@ import {
83
83
  normalizeEnabledPiUsereqTools,
84
84
  type PiUsereqStartupToolName,
85
85
  } from "./core/pi-usereq-tools.js";
86
+ import {
87
+ formatContextFileSize,
88
+ measureContextFileSizes,
89
+ type ContextFileName,
90
+ type ContextFileSizeFacts,
91
+ } from "./core/context-file-size.js";
86
92
  import {
87
93
  PROMPT_COMMAND_SUMMARY_CUSTOM_TYPE,
88
94
  renderPrompt,
@@ -95,8 +101,11 @@ import {
95
101
  finalizePromptCommandExecution,
96
102
  getPromptCommandErrorContext,
97
103
  preparePromptCommandExecution,
104
+ reapplyPromptCommandSessionSelection,
98
105
  restorePromptCommandExecution,
99
106
  type PromptCommandExecutionPlan,
107
+ type PromptCommandModelSelection,
108
+ type PromptCommandModelSelectionSurface,
100
109
  } from "./core/prompt-command-runtime.js";
101
110
  import {
102
111
  REQ_REFERENCES_COMMAND_DESCRIPTION,
@@ -136,8 +145,11 @@ import { PROMPT_COMMAND_NAMES } from "./core/prompt-command-catalog.js";
136
145
  import { resolveRuntimeGitPath } from "./core/runtime-project-paths.js";
137
146
  import {
138
147
  clearPersistedPromptCommandRuntimeState,
148
+ readPersistedPromptCommandRuntimeApi,
139
149
  readPersistedPromptCommandRuntimeState,
150
+ writePersistedPromptCommandRuntimeApi,
140
151
  writePersistedPromptCommandRuntimeState,
152
+ type PersistedPromptCommandRuntimeApi,
141
153
  } from "./core/prompt-command-state.js";
142
154
  import { ensureBundledResourcesAccessible, readBundledPromptDescription } from "./core/resources.js";
143
155
  import { ReqError } from "./core/errors.js";
@@ -1301,13 +1313,15 @@ function isPiAgentSettledEventSupported(
1301
1313
  /**
1302
1314
  * @brief Finalizes a matched successful worktree-backed prompt at the current lifecycle point.
1303
1315
  * @details Executes the deferred stash-assisted merge, transcript preservation, base-path restore, and worktree plus branch deletion through `finalizePromptCommandExecution(...)`, surfaces `error` or warning-only notifications, clears the pending finalization outcome plus prompt state, and transitions workflow state through `merging` to `idle`. Reused by the `agent_settled` handler on 0.80.4+ hosts and directly by the `agent_end` fallback on legacy hosts that never emit `agent_settled`. Runtime is dominated by git finalization. Side effects include branch merges, worktree deletion, notifications, and workflow-state transitions.
1316
+ * @param[in] pi {ExtensionAPI} Active extension API instance supplying the model re-apply appliers.
1304
1317
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller whose pending and active prompt state is cleared.
1305
1318
  * @param[in] promptRequest {PiUsereqPromptRequest} Matched successful worktree-backed prompt execution plan.
1306
1319
  * @param[in] ctx {ExtensionContext} Active extension context used for finalization and notifications.
1307
1320
  * @return {Promise<void>} Promise resolved when finalization and state transitions complete.
1308
- * @satisfies REQ-208, REQ-228, REQ-229, REQ-230, REQ-282, REQ-291, REQ-292, REQ-354, REQ-355
1321
+ * @satisfies REQ-208, REQ-228, REQ-229, REQ-230, REQ-282, REQ-291, REQ-292, REQ-354, REQ-355, REQ-368, REQ-370, REQ-371, REQ-372
1309
1322
  */
1310
1323
  async function finalizeMatchedPromptSuccess(
1324
+ pi: ExtensionAPI,
1311
1325
  statusController: PiUsereqStatusController,
1312
1326
  promptRequest: PiUsereqPromptRequest,
1313
1327
  ctx: ExtensionContext,
@@ -1333,6 +1347,14 @@ async function finalizeMatchedPromptSuccess(
1333
1347
  : undefined,
1334
1348
  );
1335
1349
  promptContext = (finalization.activeContext ?? promptContext) as typeof ctx;
1350
+ if (finalization.cleanupSucceeded && !finalization.errorMessage) {
1351
+ await reapplyCapturedPromptModelSelection(
1352
+ promptRequest.originalModel,
1353
+ promptRequest.originalThinkingLevel,
1354
+ pi,
1355
+ promptContext,
1356
+ );
1357
+ }
1336
1358
  } catch (error) {
1337
1359
  promptContext = (getPromptCommandErrorContext(error) ?? promptContext) as typeof ctx;
1338
1360
  let errorMessage = error instanceof Error ? error.message : String(error);
@@ -1346,6 +1368,12 @@ async function finalizeMatchedPromptSuccess(
1346
1368
  : undefined,
1347
1369
  ) ?? promptContext) as typeof ctx;
1348
1370
  cleanupSucceeded = true;
1371
+ await reapplyCapturedPromptModelSelection(
1372
+ promptRequest.originalModel,
1373
+ promptRequest.originalThinkingLevel,
1374
+ pi,
1375
+ promptContext,
1376
+ );
1349
1377
  } catch (restoreError) {
1350
1378
  promptContext = (getPromptCommandErrorContext(restoreError) ?? promptContext) as typeof ctx;
1351
1379
  errorMessage = restoreError instanceof Error ? restoreError.message : String(restoreError);
@@ -1451,6 +1479,12 @@ async function handleExtensionStatusEvent(
1451
1479
  const requestForActivation = activePromptRequest ?? pendingPromptRequest;
1452
1480
  if (requestForActivation !== undefined) {
1453
1481
  await activatePromptCommandExecution(requestForActivation, ctx);
1482
+ await reapplyCapturedPromptModelSelection(
1483
+ requestForActivation.originalModel,
1484
+ requestForActivation.originalThinkingLevel,
1485
+ pi,
1486
+ ctx,
1487
+ );
1454
1488
  }
1455
1489
  }
1456
1490
  const notifyRequest: PiNotifyEventRequest | undefined = hookName === "agent_end"
@@ -1555,6 +1589,7 @@ async function handleExtensionStatusEvent(
1555
1589
  // directly at `agent_end` to avoid parking in `merging` forever.
1556
1590
  statusController.state.pendingFinalizationOutcome = outcome;
1557
1591
  await finalizeMatchedPromptSuccess(
1592
+ pi,
1558
1593
  statusController,
1559
1594
  activePromptRequest,
1560
1595
  promptContext,
@@ -1635,7 +1670,7 @@ async function handleExtensionStatusEvent(
1635
1670
  && settledPromptRequest.worktreeDir !== undefined
1636
1671
  && pendingOutcome === "completed"
1637
1672
  ) {
1638
- await finalizeMatchedPromptSuccess(statusController, settledPromptRequest, ctx);
1673
+ await finalizeMatchedPromptSuccess(pi, statusController, settledPromptRequest, ctx);
1639
1674
  }
1640
1675
  }
1641
1676
  if (hookName === "session_shutdown") {
@@ -3041,6 +3076,141 @@ function registerPiNotifyShortcut(
3041
3076
  });
3042
3077
  }
3043
3078
 
3079
+ /**
3080
+ * @brief Describes the loosely-typed pi extension API surface required by model re-application.
3081
+ * @details Narrows the runtime API to the model appliers and thinking-level probe accessed defensively so legacy hosts and offline harnesses without `setModel`, `setThinkingLevel`, or `getThinkingLevel` degrade to a documented warning instead of a runtime exception. The alias is compile-time only and introduces no runtime cost.
3082
+ */
3083
+ type PromptModelSelectionApiSurface = {
3084
+ setModel?: (model: unknown) => unknown;
3085
+ setThinkingLevel?: (level: unknown) => unknown;
3086
+ getThinkingLevel?: () => unknown;
3087
+ };
3088
+
3089
+ /**
3090
+ * @brief Describes the loosely-typed context surface required by model re-application.
3091
+ * @details Narrows extension contexts to the active model, thinking level, and model registry probe used to capture the pre-switch selection and to evaluate the duplicate-skip guard after session replacement. The alias is compile-time only and introduces no runtime cost.
3092
+ */
3093
+ type PromptModelSelectionContextSurface = {
3094
+ model?: unknown;
3095
+ thinkingLevel?: unknown;
3096
+ modelRegistry?: { find?: (provider: unknown, modelId: unknown) => unknown };
3097
+ };
3098
+
3099
+ /**
3100
+ * @brief Captures the active model provider and identifier from one loosely-typed context model.
3101
+ * @details Validates that the runtime model object exposes non-empty string provider and identifier fields and emits the serializable selection fact stored inside the prompt execution plan, returning undefined when the runtime exposes no usable model so preflight capture never throws. Runtime is O(1). No external state is mutated.
3102
+ * @param[in] model {unknown} Active runtime model object from `ctx.model`.
3103
+ * @return {PromptCommandModelSelection | undefined} Serializable provider plus identifier fact, or undefined when unavailable.
3104
+ * @satisfies REQ-366
3105
+ */
3106
+ function resolveCurrentModelSelection(
3107
+ model: unknown,
3108
+ ): PromptCommandModelSelection | undefined {
3109
+ const candidate = model as { provider?: unknown; id?: unknown } | null | undefined;
3110
+ if (candidate === null || candidate === undefined) {
3111
+ return undefined;
3112
+ }
3113
+ if (
3114
+ typeof candidate.provider !== "string"
3115
+ || typeof candidate.id !== "string"
3116
+ || candidate.provider === ""
3117
+ || candidate.id === ""
3118
+ ) {
3119
+ return undefined;
3120
+ }
3121
+ return { provider: candidate.provider, modelId: candidate.id };
3122
+ }
3123
+
3124
+ /**
3125
+ * @brief Captures the active thinking level from the runtime API or fallback context.
3126
+ * @details Prefers `pi.getThinkingLevel()` when the host exposes it and falls back to the context `thinkingLevel` probe, returning undefined for legacy hosts or offline harnesses that provide neither so capture stays advisory. Runtime is O(1). No external state is mutated.
3127
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
3128
+ * @param[in] ctx {unknown} Active extension or command context used as fallback probe.
3129
+ * @return {string | undefined} Active thinking level, or undefined when unavailable.
3130
+ * @satisfies REQ-366, REQ-371
3131
+ */
3132
+ function resolveCurrentThinkingLevel(pi: ExtensionAPI, ctx: unknown): string | undefined {
3133
+ const api = pi as unknown as PromptModelSelectionApiSurface;
3134
+ if (typeof api.getThinkingLevel === "function") {
3135
+ const level = api.getThinkingLevel();
3136
+ if (typeof level === "string" && level !== "") {
3137
+ return level;
3138
+ }
3139
+ }
3140
+ const context = ctx as PromptModelSelectionContextSurface | null | undefined;
3141
+ if (
3142
+ context !== null
3143
+ && context !== undefined
3144
+ && typeof context.thinkingLevel === "string"
3145
+ && context.thinkingLevel !== ""
3146
+ ) {
3147
+ return context.thinkingLevel;
3148
+ }
3149
+ return undefined;
3150
+ }
3151
+
3152
+ /**
3153
+ * @brief Builds the loosely-typed model-selection surface from the runtime API plus context.
3154
+ * @details Wraps `setModel` and `setThinkingLevel` with defensive function probes so the surface only exposes appliers the running host actually implements, and copies the context model, thinking level, and model registry used by the guarded re-apply comparison. The supplied api MUST be the latest live extension api bound to the active session runner because pi invalidates captured extension apis after session replacement. Returns undefined when no context is available so re-application degrades to a not-attempted skip. Runtime is O(1). No external state is mutated.
3155
+ * @param[in] pi {ExtensionAPI} Latest live extension api instance supplying the session-scoped model appliers; after session replacement callers pass the rebound api rather than a captured pre-switch api.
3156
+ * @param[in] ctx {unknown} Active extension or command context supplying model probes.
3157
+ * @return {PromptCommandModelSelectionSurface | undefined} Re-apply surface, or undefined when no context exists.
3158
+ * @satisfies REQ-367, REQ-370, REQ-371
3159
+ */
3160
+ function resolvePromptCommandModelSurface(
3161
+ pi: ExtensionAPI,
3162
+ ctx: unknown,
3163
+ ): PromptCommandModelSelectionSurface | undefined {
3164
+ const context = ctx as PromptModelSelectionContextSurface | null | undefined;
3165
+ if (context === null || context === undefined) {
3166
+ return undefined;
3167
+ }
3168
+ const api = pi as unknown as PromptModelSelectionApiSurface;
3169
+ const setModelFn = api.setModel;
3170
+ const setThinkingLevelFn = api.setThinkingLevel;
3171
+ return {
3172
+ modelRegistry: context.modelRegistry,
3173
+ model: context.model,
3174
+ thinkingLevel: context.thinkingLevel,
3175
+ setModel: typeof setModelFn === "function"
3176
+ ? (model: unknown) => setModelFn.call(pi, model) as Promise<boolean>
3177
+ : undefined,
3178
+ setThinkingLevel: typeof setThinkingLevelFn === "function"
3179
+ ? (level: unknown) => {
3180
+ setThinkingLevelFn.call(pi, level);
3181
+ }
3182
+ : undefined,
3183
+ };
3184
+ }
3185
+
3186
+ /**
3187
+ * @brief Re-applies one captured model selection onto the active post-switch session.
3188
+ * @details Builds the guarded re-apply surface from the latest process-scoped extension api plus context, preferring the api written by the most recent extension bind because pi invalidates the api captured before session replacement, and falls back to the caller-supplied api for hosts that do not rebind. Forwards the captured selection plus thinking level into `reapplyPromptCommandSessionSelection(...)` and routes every warning through `notifyContextSafely(...)` so stale replacement contexts after session replacement never abort orchestration. Runtime is O(1) plus one awaited model mutation. Side effects include session-scoped model and thinking-level mutation plus stale-safe warning notifications.
3189
+ * @param[in] selection {PromptCommandModelSelection | undefined} Captured model provider plus identifier.
3190
+ * @param[in] thinkingLevel {string | undefined} Captured thinking level.
3191
+ * @param[in] pi {ExtensionAPI} Extension api captured by the running handler, used only as fallback when no rebound api is stored.
3192
+ * @param[in] ctx {unknown} Active extension or command context used for probes and warning delivery.
3193
+ * @return {Promise<void>} Promise resolved once the guarded re-application completed.
3194
+ * @satisfies REQ-367, REQ-368, REQ-369, REQ-370, REQ-371, REQ-372
3195
+ */
3196
+ async function reapplyCapturedPromptModelSelection(
3197
+ selection: PromptCommandModelSelection | undefined,
3198
+ thinkingLevel: string | undefined,
3199
+ pi: ExtensionAPI,
3200
+ ctx: unknown,
3201
+ ): Promise<void> {
3202
+ const liveApi = readPersistedPromptCommandRuntimeApi();
3203
+ const activeApi = liveApi ?? pi;
3204
+ await reapplyPromptCommandSessionSelection({
3205
+ selection,
3206
+ thinkingLevel,
3207
+ surface: resolvePromptCommandModelSurface(activeApi as ExtensionAPI, ctx),
3208
+ notifyWarning: (message) => {
3209
+ notifyContextSafely(ctx as ExtensionContext, message, "error");
3210
+ },
3211
+ });
3212
+ }
3213
+
3044
3214
  /**
3045
3215
  * @brief Resolves the prompt execution plan targeted by `req-reset` recovery.
3046
3216
  * @details Prefers the current in-memory active request, then the current in-memory pending request, then the process-scoped persisted prompt runtime state so the dedicated reset command can recover from same-host unclean prompt termination after session replacement. Runtime is O(1). No external state is mutated.
@@ -3091,6 +3261,10 @@ function registerReqResetCommand(
3091
3261
  description: REQ_RESET_COMMAND_DESCRIPTION,
3092
3262
  handler: async (_args, ctx) => {
3093
3263
  const promptRequest = resolveReqResetPromptRequest(statusController);
3264
+ const resetModelSelection = promptRequest?.originalModel
3265
+ ?? resolveCurrentModelSelection((ctx as unknown as PromptModelSelectionContextSurface).model);
3266
+ const resetThinkingLevel = promptRequest?.originalThinkingLevel
3267
+ ?? resolveCurrentThinkingLevel(pi, ctx);
3094
3268
  const commandCwd = resolveLiveBootstrapCwd(ctx.cwd);
3095
3269
  syncContextCwdMirror(ctx, commandCwd);
3096
3270
  const projectBase = path.resolve(promptRequest?.basePath ?? getProjectBase(commandCwd));
@@ -3109,6 +3283,12 @@ function registerReqResetCommand(
3109
3283
  executionPlan = prepareReqResetCommandExecution(projectBase, config, promptRequest);
3110
3284
  executionResult = await executeReqResetCommandExecution(executionPlan, ctx);
3111
3285
  resetContext = (executionResult.activeContext ?? resetContext) as typeof ctx;
3286
+ await reapplyCapturedPromptModelSelection(
3287
+ resetModelSelection,
3288
+ resetThinkingLevel,
3289
+ pi,
3290
+ resetContext,
3291
+ );
3112
3292
  } catch (error) {
3113
3293
  const message = error instanceof Error ? error.message : String(error);
3114
3294
  setPiUsereqWorkflowState(statusController, "error", resetContext);
@@ -3197,7 +3377,7 @@ function registerReqReferencesCommand(
3197
3377
  * @param[in] pi {ExtensionAPI} Active extension API instance.
3198
3378
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
3199
3379
  * @return {void} No return value.
3200
- * @satisfies REQ-004, REQ-067, REQ-068, REQ-169, REQ-200, REQ-201, REQ-202, REQ-203, REQ-206, REQ-207, REQ-219, REQ-220, REQ-221, REQ-224, REQ-225, REQ-226, REQ-227, REQ-245, REQ-246, REQ-247, REQ-277, REQ-281
3380
+ * @satisfies REQ-004, REQ-067, REQ-068, REQ-169, REQ-200, REQ-201, REQ-202, REQ-203, REQ-206, REQ-207, REQ-219, REQ-220, REQ-221, REQ-224, REQ-225, REQ-226, REQ-227, REQ-245, REQ-246, REQ-247, REQ-277, REQ-281, REQ-377
3201
3381
  */
3202
3382
  function registerPromptCommands(
3203
3383
  pi: ExtensionAPI,
@@ -3228,6 +3408,10 @@ function registerPromptCommands(
3228
3408
  ensureBundledResourcesAccessible();
3229
3409
  let executionPlan: PromptCommandExecutionPlan | undefined;
3230
3410
  let promptContext = ctx;
3411
+ const originalModel = resolveCurrentModelSelection(
3412
+ (ctx as unknown as PromptModelSelectionContextSurface).model,
3413
+ );
3414
+ const originalThinkingLevel = resolveCurrentThinkingLevel(pi, ctx);
3231
3415
  try {
3232
3416
  executionPlan = preparePromptCommandExecution(
3233
3417
  promptName,
@@ -3241,6 +3425,8 @@ function registerPromptCommands(
3241
3425
  config,
3242
3426
  workflowState: statusController.state.workflowState,
3243
3427
  },
3428
+ originalModel,
3429
+ originalThinkingLevel,
3244
3430
  );
3245
3431
  const content = renderPrompt(
3246
3432
  promptName,
@@ -3256,6 +3442,12 @@ function registerPromptCommands(
3256
3442
  activePromptRequest: statusController.state.activePromptRequest,
3257
3443
  });
3258
3444
  promptContext = (await activatePromptCommandExecution(executionPlan, ctx) ?? ctx) as typeof ctx;
3445
+ await reapplyCapturedPromptModelSelection(
3446
+ executionPlan.originalModel,
3447
+ executionPlan.originalThinkingLevel,
3448
+ pi,
3449
+ promptContext,
3450
+ );
3259
3451
  logPromptWorkflowEvent(
3260
3452
  projectBase,
3261
3453
  config,
@@ -3277,6 +3469,7 @@ function registerPromptCommands(
3277
3469
  promptName,
3278
3470
  args,
3279
3471
  config,
3472
+ measureContextFileSizes(projectBase, config),
3280
3473
  );
3281
3474
  const promptDelivery = deliverPromptCommand(pi, content, commandSummary, promptContext);
3282
3475
  transitionPromptWorkflowState(
@@ -4352,46 +4545,68 @@ async function configureStaticCheckMenu(
4352
4545
  }
4353
4546
 
4354
4547
  /**
4355
- * @brief Summarizes the `Context Files` flag state for the top-level menu value column.
4356
- * @details Renders the three context-file flags as compact `name:on|off` segments in the documented order so the top-level row reflects the current injection configuration. Runtime is O(1). No external state is mutated.
4548
+ * @brief Summarizes the `Context Files` flag state with measured sizes for the top-level menu value column.
4549
+ * @details Renders the three context-file flags as compact `name:on|off` segments in the documented order, appending `(<chars>c/<tokens>t)` measured size facts to every enabled segment so the top-level row exposes a runtime estimate of the injected context payload while disabled segments stay plain `name:off`. Runtime is O(1) in segment count. No external state is mutated.
4357
4550
  * @param[in] config {UseReqConfig} Effective project configuration.
4551
+ * @param[in] sizes {Record<ContextFileName, ContextFileSizeFacts>} Measured context-file size facts keyed by canonical file name.
4358
4552
  * @return {string} Compact `Context Files` summary string.
4359
- * @satisfies REQ-327
4553
+ * @satisfies REQ-327, REQ-376
4360
4554
  */
4361
- function formatContextFilesSummary(config: UseReqConfig): string {
4362
- return `requirements:${config["context-files-requirements"] ? "on" : "off"} \u2022 references:${config["context-files-references"] ? "on" : "off"} \u2022 workflow:${config["context-files-workflow"] ? "on" : "off"}`;
4555
+ function formatContextFilesSummary(
4556
+ config: UseReqConfig,
4557
+ sizes: Record<ContextFileName, ContextFileSizeFacts>,
4558
+ ): string {
4559
+ const segments = [
4560
+ `requirements:${config["context-files-requirements"] ? `on(${formatContextFileSize(sizes["REQUIREMENTS.md"])})` : "off"}`,
4561
+ `references:${config["context-files-references"] ? `on(${formatContextFileSize(sizes["REFERENCES.md"])})` : "off"}`,
4562
+ `workflow:${config["context-files-workflow"] ? `on(${formatContextFileSize(sizes["WORKFLOW.md"])})` : "off"}`,
4563
+ ];
4564
+ return segments.join(" \u2022 ");
4363
4565
  }
4364
4566
 
4365
4567
  /**
4366
4568
  * @brief Builds the shared settings-menu choices for the `Context Files` submenu.
4367
- * @details Exposes one inline on|off toggle row per context file in the documented `REQUIREMENTS.md`, `REFERENCES.md`, `WORKFLOW.md` order plus a value-less subtree-local `Reset defaults` row. Runtime is O(1). No external state is mutated.
4569
+ * @details Exposes one inline toggle row per context file in the documented `REQUIREMENTS.md`, `REFERENCES.md`, `WORKFLOW.md` order whose value renders `on|off • <chars>c/<tokens>t` measured size facts plus a value-less subtree-local `Reset defaults` row. Cycle values embed the same measured facts so inline toggling keeps the size estimate visible while persisting the on|off state. Runtime is O(1) in row count. No external state is mutated.
4368
4570
  * @param[in] config {UseReqConfig} Effective project configuration.
4571
+ * @param[in] sizes {Record<ContextFileName, ContextFileSizeFacts>} Measured context-file size facts keyed by canonical file name.
4369
4572
  * @return {PiUsereqSettingsMenuChoice[]} Ordered `Context Files` submenu choices.
4370
- * @satisfies REQ-327, REQ-328, REQ-333
4573
+ * @satisfies REQ-327, REQ-328, REQ-333, REQ-375
4371
4574
  */
4372
- function buildContextFilesMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
4575
+ function buildContextFilesMenuChoices(
4576
+ config: UseReqConfig,
4577
+ sizes: Record<ContextFileName, ContextFileSizeFacts>,
4578
+ ): PiUsereqSettingsMenuChoice[] {
4579
+ const buildRow = (
4580
+ id: "context-files-requirements" | "context-files-references" | "context-files-workflow",
4581
+ fileName: ContextFileName,
4582
+ description: string,
4583
+ ): PiUsereqSettingsMenuChoice => {
4584
+ const sizedOn = `on \u2022 ${formatContextFileSize(sizes[fileName])}`;
4585
+ const sizedOff = `off \u2022 ${formatContextFileSize(sizes[fileName])}`;
4586
+ return {
4587
+ id,
4588
+ label: fileName,
4589
+ value: config[id] ? sizedOn : sizedOff,
4590
+ values: [sizedOn, sizedOff],
4591
+ description,
4592
+ };
4593
+ };
4373
4594
  return [
4374
- {
4375
- id: "context-files-requirements",
4376
- label: "REQUIREMENTS.md",
4377
- value: config["context-files-requirements"] ? "on" : "off",
4378
- values: ["on", "off"],
4379
- description: "Toggle injection of REQUIREMENTS.md into the prompt context through `%%CONTEXT_FILES%%`.",
4380
- },
4381
- {
4382
- id: "context-files-references",
4383
- label: "REFERENCES.md",
4384
- value: config["context-files-references"] ? "on" : "off",
4385
- values: ["on", "off"],
4386
- description: "Toggle injection of REFERENCES.md into the prompt context through `%%CONTEXT_FILES%%`.",
4387
- },
4388
- {
4389
- id: "context-files-workflow",
4390
- label: "WORKFLOW.md",
4391
- value: config["context-files-workflow"] ? "on" : "off",
4392
- values: ["on", "off"],
4393
- description: "Toggle injection of WORKFLOW.md into the prompt context through `%%CONTEXT_FILES%%`.",
4394
- },
4595
+ buildRow(
4596
+ "context-files-requirements",
4597
+ "REQUIREMENTS.md",
4598
+ "Toggle injection of REQUIREMENTS.md into the prompt context through `%%CONTEXT_FILES%%`.",
4599
+ ),
4600
+ buildRow(
4601
+ "context-files-references",
4602
+ "REFERENCES.md",
4603
+ "Toggle injection of REFERENCES.md into the prompt context through `%%CONTEXT_FILES%%`.",
4604
+ ),
4605
+ buildRow(
4606
+ "context-files-workflow",
4607
+ "WORKFLOW.md",
4608
+ "Toggle injection of WORKFLOW.md into the prompt context through `%%CONTEXT_FILES%%`.",
4609
+ ),
4395
4610
  ...buildTerminalSettingsMenuChoices({
4396
4611
  resetDefaultsDescription: "Restore the default Context Files configuration (all three files enabled).",
4397
4612
  }),
@@ -4412,6 +4627,7 @@ async function configureContextFilesMenu(
4412
4627
  config: UseReqConfig,
4413
4628
  onConfigChange: () => void,
4414
4629
  ): Promise<void> {
4630
+ const contextFileSizes = measureContextFileSizes(getProjectBase(ctx.cwd), config);
4415
4631
  const setFlag = (flagKey: "context-files-requirements" | "context-files-references" | "context-files-workflow", enabled: boolean): void => {
4416
4632
  config[flagKey] = enabled;
4417
4633
  onConfigChange();
@@ -4419,20 +4635,20 @@ async function configureContextFilesMenu(
4419
4635
  };
4420
4636
  let focusedChoiceId: string | undefined;
4421
4637
  while (true) {
4422
- const choice = await showPiUsereqSettingsMenu(ctx, "Context Files", buildContextFilesMenuChoices(config), {
4638
+ const choice = await showPiUsereqSettingsMenu(ctx, "Context Files", buildContextFilesMenuChoices(config, contextFileSizes), {
4423
4639
  initialSelectedId: focusedChoiceId,
4424
- getChoices: () => buildContextFilesMenuChoices(config),
4640
+ getChoices: () => buildContextFilesMenuChoices(config, contextFileSizes),
4425
4641
  onChange: (choiceId, newValue) => {
4426
4642
  if (choiceId === "context-files-requirements") {
4427
- setFlag("context-files-requirements", newValue === "on");
4643
+ setFlag("context-files-requirements", newValue.startsWith("on"));
4428
4644
  return;
4429
4645
  }
4430
4646
  if (choiceId === "context-files-references") {
4431
- setFlag("context-files-references", newValue === "on");
4647
+ setFlag("context-files-references", newValue.startsWith("on"));
4432
4648
  return;
4433
4649
  }
4434
4650
  if (choiceId === "context-files-workflow") {
4435
- setFlag("context-files-workflow", newValue === "on");
4651
+ setFlag("context-files-workflow", newValue.startsWith("on"));
4436
4652
  }
4437
4653
  },
4438
4654
  });
@@ -4473,7 +4689,7 @@ async function configureContextFilesMenu(
4473
4689
  * @param[in] cwd {string} Current working directory.
4474
4690
  * @param[in] config {UseReqConfig} Effective project configuration.
4475
4691
  * @return {PiUsereqSettingsMenuChoice[]} Ordered top-level menu choices.
4476
- * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-162, REQ-190, REQ-191, REQ-197, REQ-204, REQ-205, REQ-212, REQ-215, REQ-216, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240, REQ-314, REQ-318, REQ-319, REQ-320, REQ-326
4692
+ * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-162, REQ-190, REQ-191, REQ-197, REQ-204, REQ-205, REQ-212, REQ-215, REQ-216, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240, REQ-314, REQ-318, REQ-319, REQ-320, REQ-326, REQ-376
4477
4693
  */
4478
4694
  function buildPiUsereqMenuChoices(
4479
4695
  cwd: string,
@@ -4506,7 +4722,7 @@ function buildPiUsereqMenuChoices(
4506
4722
  {
4507
4723
  id: "context-files",
4508
4724
  label: "Context Files",
4509
- value: formatContextFilesSummary(config),
4725
+ value: formatContextFilesSummary(config, measureContextFileSizes(cwd, config)),
4510
4726
  description: "Toggle injection of REQUIREMENTS.md, REFERENCES.md, and WORKFLOW.md into prompt context through `%%CONTEXT_FILES%%`.",
4511
4727
  },
4512
4728
  {
@@ -4863,7 +5079,7 @@ async function configurePiUsereq(
4863
5079
  { label: "Document directory", previousValue: config["docs-dir"], nextValue: defaultConfig["docs-dir"] },
4864
5080
  { label: "Source-code directories", previousValue: config["src-dir"].join(", "), nextValue: defaultConfig["src-dir"].join(", ") },
4865
5081
  { label: "Unit tests directory", previousValue: config["tests-dir"], nextValue: defaultConfig["tests-dir"] },
4866
- { label: "Context Files", previousValue: formatContextFilesSummary(config), nextValue: formatContextFilesSummary(defaultConfig) },
5082
+ { label: "Context Files", previousValue: formatContextFilesSummary(config, measureContextFileSizes(ctx.cwd, config)), nextValue: formatContextFilesSummary(defaultConfig, measureContextFileSizes(ctx.cwd, defaultConfig)) },
4867
5083
  { label: "Auto git commit", previousValue: config.AUTO_GIT_COMMIT, nextValue: defaultConfig.AUTO_GIT_COMMIT },
4868
5084
  { label: "Git worktree", previousValue: config.GIT_WORKTREE_ENABLED, nextValue: defaultConfig.GIT_WORKTREE_ENABLED },
4869
5085
  { label: "Worktree prefix", previousValue: config.GIT_WORKTREE_PREFIX, nextValue: defaultConfig.GIT_WORKTREE_PREFIX },
@@ -4921,12 +5137,13 @@ function registerConfigCommands(
4921
5137
 
4922
5138
  /**
4923
5139
  * @brief Registers the complete pi-usereq extension.
4924
- * @details Validates installation-owned bundled resources, registers the specialized `req-reset` and `req-references` commands plus bundled prompt-backed commands and agent tools, conditionally registers config-gated debug tool wrapper commands when the current project enables them, registers configuration commands, registers the configurable notification-sound shortcut when the runtime supports shortcuts, and installs shared wrappers for all supported pi lifecycle hooks so status telemetry, context usage, prompt timing, cumulative runtime, prompt-specific Pushover metadata, tool-result debug logging, and prompt-orchestration effects remain synchronized with runtime events. Runtime is O(h) in hook count during registration. Side effects include filesystem reads, command/tool/shortcut registration, UI updates, active-tool changes, optional debug-log writes, and timer scheduling.
5140
+ * @details Persists the live extension api into process-scoped storage on every bind so model re-application after session replacement binds to the api of the currently active session runner instead of an invalidated captured api, then validates installation-owned bundled resources, registers the specialized `req-reset` and `req-references` commands plus bundled prompt-backed commands and agent tools, conditionally registers config-gated debug tool wrapper commands when the current project enables them, registers configuration commands, registers the configurable notification-sound shortcut when the runtime supports shortcuts, and installs shared wrappers for all supported pi lifecycle hooks so status telemetry, context usage, prompt timing, cumulative runtime, prompt-specific Pushover metadata, tool-result debug logging, and prompt-orchestration effects remain synchronized with runtime events. Runtime is O(h) in hook count during registration. Side effects include filesystem reads, command/tool/shortcut registration, UI updates, active-tool changes, process-scoped extension-api persistence, optional debug-log writes, and timer scheduling.
4925
5141
  * @param[in] pi {ExtensionAPI} Active extension API instance.
4926
5142
  * @return {void} No return value.
4927
5143
  * @satisfies DES-002, DES-015, REQ-004, REQ-005, REQ-009, REQ-044, REQ-067, REQ-068, REQ-109, REQ-111, REQ-112, REQ-113, REQ-114, REQ-115, REQ-116, REQ-117, REQ-118, REQ-119, REQ-120, REQ-121, REQ-122, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-131, REQ-132, REQ-133, REQ-134, REQ-137, REQ-159, REQ-163, REQ-164, REQ-165, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-174, REQ-179, REQ-180, REQ-184, REQ-188, REQ-190, REQ-191, REQ-192, REQ-193, REQ-194, REQ-195, REQ-196, REQ-197, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240, REQ-241, REQ-242, REQ-243, REQ-244, REQ-245, REQ-246, REQ-247, REQ-298, REQ-299, REQ-300, REQ-301, REQ-302, REQ-303, REQ-304, REQ-305, REQ-306, REQ-312, REQ-313, REQ-323, REQ-324, REQ-325, REQ-326, REQ-327
4928
5144
  */
4929
5145
  export default function piUsereqExtension(pi: ExtensionAPI): void {
5146
+ writePersistedPromptCommandRuntimeApi(pi as unknown as PersistedPromptCommandRuntimeApi);
4930
5147
  const statusController = createPiUsereqStatusController();
4931
5148
  ensureBundledResourcesAccessible();
4932
5149
  registerReqResetCommand(pi, statusController);