@herbertgao/pi-extensions 2026.8.7 → 2026.8.8

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.
Files changed (99) hide show
  1. package/README.md +7 -5
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +63 -15
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +78 -16
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +13 -0
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +17 -6
  8. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  9. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +4 -4
  10. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +154 -96
  11. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +3 -0
  12. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
  13. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +7 -5
  14. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
  15. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +3 -0
  16. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  17. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +137 -96
  18. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +44 -32
  19. package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
  20. package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
  21. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
  22. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
  23. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
  24. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
  25. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
  26. package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +56 -34
  27. package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +50 -4
  28. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  29. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
  30. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  31. package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
  32. package/node_modules/pi-lens/CHANGELOG.md +90 -0
  33. package/node_modules/pi-lens/README.md +106 -0
  34. package/node_modules/pi-lens/dist/clients/bash-file-access.js +97 -6
  35. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +16 -6
  36. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +14 -0
  37. package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
  38. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +22 -1
  39. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
  40. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
  41. package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
  43. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
  44. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
  45. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +8 -10
  46. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +10 -10
  47. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
  48. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
  49. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -15
  50. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +19 -11
  51. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +35 -13
  52. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +8 -8
  53. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +110 -23
  54. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
  55. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +117 -1
  56. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +16 -14
  57. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +8 -9
  58. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +22 -0
  59. package/node_modules/pi-lens/dist/clients/jscpd-client.js +5 -2
  60. package/node_modules/pi-lens/dist/clients/language-profile.js +7 -2
  61. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +27 -2
  62. package/node_modules/pi-lens/dist/clients/lsp/client.js +144 -5
  63. package/node_modules/pi-lens/dist/clients/lsp/index.js +151 -6
  64. package/node_modules/pi-lens/dist/clients/module-report.js +14 -1
  65. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
  66. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +18 -9
  67. package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
  68. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +7 -2
  69. package/node_modules/pi-lens/dist/clients/read-guard.js +88 -19
  70. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
  71. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
  72. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
  73. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +44 -0
  74. package/node_modules/pi-lens/dist/clients/runtime-session.js +41 -0
  75. package/node_modules/pi-lens/dist/clients/runtime-turn.js +47 -2
  76. package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
  77. package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
  78. package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
  79. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
  80. package/node_modules/pi-lens/dist/index.js +1224 -393
  81. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +10 -3
  82. package/node_modules/pi-lens/package.json +18 -5
  83. package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
  84. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
  85. package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
  86. package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
  87. package/node_modules/pi-web-access/CHANGELOG.md +14 -0
  88. package/node_modules/pi-web-access/README.md +6 -5
  89. package/node_modules/pi-web-access/chrome-cookies.ts +86 -15
  90. package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
  91. package/node_modules/pi-web-access/extract.ts +12 -2
  92. package/node_modules/pi-web-access/firecrawl.ts +18 -2
  93. package/node_modules/pi-web-access/github-extract.ts +65 -36
  94. package/node_modules/pi-web-access/index.ts +19 -4
  95. package/node_modules/pi-web-access/openai-search.ts +17 -6
  96. package/node_modules/pi-web-access/package.json +1 -1
  97. package/node_modules/pi-web-access/pdf-extract.ts +18 -5
  98. package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
  99. package/package.json +9 -9
@@ -1,5 +1,5 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { isKeyRelease, isKeyRepeat, matchesKey } from "@earendil-works/pi-tui";
1
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { isKeyRelease, isKeyRepeat, matchesKey, type OverlayHandle, type TUI } from "@earendil-works/pi-tui";
3
3
  import {
4
4
  COLLAPSE_KEY_OFF,
5
5
  formatKeySpecForDisplay,
@@ -15,7 +15,7 @@ import {
15
15
  } from "./events.js";
16
16
  // Static import is fine — rpc-fallback pulls only types + the i18n bridge,
17
17
  // none of the ~560ms TUI render graph that QuestionnaireSession lazy-loads.
18
- import { hasDialogUI, runRpcQuestionnaire } from "./rpc-fallback.js";
18
+ import { type DialogUI, hasDialogUI, runRpcQuestionnaire } from "./rpc-fallback.js";
19
19
  import { displayLabel, t } from "./state/i18n-bridge.js";
20
20
  import { sentinelsToAppend } from "./state/row-intent.js";
21
21
  import { buildQuestionnaireResponse, buildToolResult } from "./tool/response-envelope.js";
@@ -53,6 +53,22 @@ function emitAskUserBlockedEvent(pi: ExtensionAPI, active: boolean): void {
53
53
  pi.events.emit(ASK_USER_BLOCKED_EVENT, payload);
54
54
  }
55
55
 
56
+ /** Non-interactive host backstop (the reconciler normally strips the tool first). */
57
+ function rejectWithoutUi() {
58
+ return buildToolResult(ERROR_NO_UI, { answers: [], cancelled: true, error: "no_ui" });
59
+ }
60
+
61
+ /** Sequential native-dialog walker for RPC hosts; brackets it with the blocked-event pair + terminal bell. */
62
+ async function runRpcPath(pi: ExtensionAPI, ui: DialogUI, typed: QuestionParams) {
63
+ emitAskUserBlockedEvent(pi, true);
64
+ try {
65
+ emitTerminalAttention();
66
+ return buildQuestionnaireResponse(await runRpcQuestionnaire(ui, typed), typed);
67
+ } finally {
68
+ emitAskUserBlockedEvent(pi, false);
69
+ }
70
+ }
71
+
56
72
  /** Canonical tool name — single source of truth shared with the reconcile module. */
57
73
  export const ASK_USER_QUESTION_TOOL_NAME = "ask_user_question";
58
74
 
@@ -90,6 +106,9 @@ export const PREWARM_DELAY_MS = 2000;
90
106
 
91
107
  type SessionModule = typeof import("./state/questionnaire-session.js");
92
108
 
109
+ type SessionRef = { current: import("./state/questionnaire-session.js").QuestionnaireSession | null };
110
+ type OverlayHandleRef = { current: OverlayHandle | undefined };
111
+
93
112
  type SessionLoad =
94
113
  | { ok: true; module: SessionModule }
95
114
  | { ok: false; error: Extract<QuestionnaireError, "session_load_failed" | "stale_module_cache">; message: string };
@@ -124,6 +143,122 @@ export async function loadQuestionnaireSession(): Promise<SessionLoad> {
124
143
  return { ok: true, module: mod };
125
144
  }
126
145
 
146
+ /**
147
+ * Register the raw terminal listener that toggles collapse while the overlay is hidden.
148
+ * Returns the remover, or undefined when the key is off / the host has no raw input hook —
149
+ * callers derive `canReopenWhileHidden` from that.
150
+ */
151
+ function registerCollapseKeyListener(
152
+ ctx: ExtensionContext,
153
+ collapseKey: string,
154
+ sessionRef: SessionRef,
155
+ overlayHandleRef: OverlayHandleRef,
156
+ ): (() => void) | undefined {
157
+ if (collapseKey === COLLAPSE_KEY_OFF || typeof ctx.ui.onTerminalInput !== "function") return undefined;
158
+ let hasAnnouncedHide = false;
159
+ return ctx.ui.onTerminalInput((data) => {
160
+ const handle = overlayHandleRef.current;
161
+ if (!handle) return undefined;
162
+ // Only act while the questionnaire is hidden (its handleInput is
163
+ // unreachable) or actually focused. When some other overlay is on
164
+ // top (e.g. `/btw`), leave the keystroke to that overlay instead of
165
+ // toggling the questionnaire from underneath it.
166
+ if (!handle.isHidden() && !handle.isFocused()) return undefined;
167
+ if (!matchesKey(data, collapseKey as Parameters<typeof matchesKey>[1])) return undefined;
168
+ // Kitty-protocol terminals report press, repeat, and release separately.
169
+ // Toggle only on the initial press so a tap does not immediately reopen
170
+ // the overlay and a held key does not toggle it repeatedly.
171
+ if (isKeyRelease(data) || isKeyRepeat(data)) return { consume: true };
172
+ sessionRef.current?.toggleCollapsedExternal();
173
+ if (handle.isHidden() && !hasAnnouncedHide) {
174
+ hasAnnouncedHide = true;
175
+ ctx.ui.notify?.(`ask_user_question hidden — press ${formatKeySpecForDisplay(collapseKey)} to reopen`, "info");
176
+ }
177
+ return { consume: true };
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Build the `ctx.ui.custom` component factory: constructs the session (capturing it in
183
+ * `sessionRef`) and exposes its component. `editInput` keeps its two dynamic imports —
184
+ * they must stay lazy per-invocation.
185
+ */
186
+ function makeSessionFactory(config: {
187
+ ctx: ExtensionContext;
188
+ typed: QuestionParams;
189
+ itemsByTab: WrappingSelectItem[][];
190
+ collapseKey: string;
191
+ canReopenWhileHidden: boolean;
192
+ sessionRef: SessionRef;
193
+ Session: SessionModule["QuestionnaireSession"];
194
+ }) {
195
+ const { ctx, typed, itemsByTab, collapseKey, canReopenWhileHidden, sessionRef, Session } = config;
196
+ return (
197
+ tui: TUI,
198
+ theme: Theme,
199
+ keybindings: import("./state/questionnaire-session.js").QuestionnaireSessionConfig["keybindings"],
200
+ done: (result: QuestionnaireResult) => void,
201
+ ): import("./state/questionnaire-session.js").QuestionnaireSessionComponent => {
202
+ const session = new Session({
203
+ tui,
204
+ theme,
205
+ params: typed,
206
+ itemsByTab,
207
+ done,
208
+ keybindings,
209
+ editInput: async (value) => {
210
+ try {
211
+ const [{ SettingsManager }, { editWithExternalEditor }] = await Promise.all([
212
+ import("@earendil-works/pi-coding-agent"),
213
+ import("./state/external-editor.js"),
214
+ ]);
215
+ const editorCommand = SettingsManager.create(ctx.cwd, undefined, {
216
+ projectTrusted: ctx.isProjectTrusted(),
217
+ }).getExternalEditorCommand();
218
+ if (!editorCommand) throw new Error("No external editor command is configured");
219
+ return await editWithExternalEditor(tui, editorCommand, value);
220
+ } catch (error) {
221
+ const message = error instanceof Error ? error.message : String(error);
222
+ ctx.ui.notify(`${t("editor.failed", "External editor failed")}: ${message}`, "error");
223
+ return undefined;
224
+ }
225
+ },
226
+ collapseKey,
227
+ canReopenWhileHidden,
228
+ });
229
+ sessionRef.current = session;
230
+ return session.component;
231
+ };
232
+ }
233
+
234
+ /**
235
+ * A TUI questionnaire ALWAYS resolves a QuestionnaireResult (cancel included), so
236
+ * `undefined` uniquely means "host cannot render", never "user declined". RPC builds
237
+ * that predate ctx.mode land here: run the dialog walker when the host has the
238
+ * primitives; otherwise tell the model the user never saw the questions.
239
+ */
240
+ async function resolveUndefinedResult(ctx: ExtensionContext, typed: QuestionParams) {
241
+ if (hasDialogUI(ctx.ui)) {
242
+ return buildQuestionnaireResponse(await runRpcQuestionnaire(ctx.ui, typed), typed);
243
+ }
244
+ return buildToolResult(ERROR_NO_CUSTOM_UI, { answers: [], cancelled: true, error: "no_custom_ui" });
245
+ }
246
+
247
+ /**
248
+ * Pre-warm the lazy session graph once startup settles (#107). A graph
249
+ * evaluated while the paths Pi resolved at boot still exist stays in memory
250
+ * for the process lifetime, so later on-disk dependency churn (e.g. `pnpm
251
+ * install --force` replacing the store mid-session) can no longer poison
252
+ * jiti's graph cache. Swallowed failure is safe: the first real call
253
+ * re-imports and surfaces it through loadQuestionnaireSession's structured
254
+ * envelope. unref keeps the timer from holding a non-TUI embedder's process
255
+ * open.
256
+ */
257
+ function prewarmSessionGraph(): void {
258
+ const timer = setTimeout(() => void loadQuestionnaireSession().catch(() => undefined), PREWARM_DELAY_MS);
259
+ timer.unref?.();
260
+ }
261
+
127
262
  export function buildItemsForQuestion(question: QuestionData): WrappingSelectItem[] {
128
263
  const items: WrappingSelectItem[] = question.options.map((o) => ({
129
264
  kind: "option",
@@ -176,7 +311,7 @@ export function registerAskUserQuestionTool(pi: ExtensionAPI): void {
176
311
 
177
312
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
178
313
  const typed = params as unknown as QuestionParams;
179
- if (!ctx.hasUI) return buildToolResult(ERROR_NO_UI, { answers: [], cancelled: true, error: "no_ui" });
314
+ if (!ctx.hasUI) return rejectWithoutUi();
180
315
 
181
316
  const validation = validateQuestionnaire(typed);
182
317
  if (!validation.ok) {
@@ -197,13 +332,7 @@ export function registerAskUserQuestionTool(pi: ExtensionAPI): void {
197
332
  // import entirely; RPC builds that predate ctx.mode are caught by the
198
333
  // custom()-resolved-undefined backstop below. See ./rpc-fallback.ts.
199
334
  if ((ctx as { mode?: string }).mode === "rpc" && hasDialogUI(ctx.ui)) {
200
- emitAskUserBlockedEvent(pi, true);
201
- try {
202
- emitTerminalAttention();
203
- return buildQuestionnaireResponse(await runRpcQuestionnaire(ctx.ui, typed), typed);
204
- } finally {
205
- emitAskUserBlockedEvent(pi, false);
206
- }
335
+ return runRpcPath(pi, ctx.ui, typed);
207
336
  }
208
337
 
209
338
  const itemsByTab: WrappingSelectItem[][] = typed.questions.map((q) => buildItemsForQuestion(q));
@@ -225,40 +354,9 @@ export function registerAskUserQuestionTool(pi: ExtensionAPI): void {
225
354
  // user toggles collapse, and register a raw terminal input listener for the
226
355
  // same key so the toggle still works while the overlay is hidden (pi-tui does
227
356
  // not route input to a hidden overlay's `component.handleInput`).
228
- const sessionRef: {
229
- current: import("./state/questionnaire-session.js").QuestionnaireSession | null;
230
- } = { current: null };
231
- const overlayHandleRef: { current: import("@earendil-works/pi-tui").OverlayHandle | undefined } = {
232
- current: undefined,
233
- };
234
- let hasAnnouncedHide = false;
235
- let removeOverlayInputListener: (() => void) | undefined;
236
-
237
- if (collapseKey !== COLLAPSE_KEY_OFF && typeof ctx.ui.onTerminalInput === "function") {
238
- removeOverlayInputListener = ctx.ui.onTerminalInput((data) => {
239
- const handle = overlayHandleRef.current;
240
- if (!handle) return undefined;
241
- // Only act while the questionnaire is hidden (its handleInput is
242
- // unreachable) or actually focused. When some other overlay is on
243
- // top (e.g. `/btw`), leave the keystroke to that overlay instead of
244
- // toggling the questionnaire from underneath it.
245
- if (!handle.isHidden() && !handle.isFocused()) return undefined;
246
- if (!matchesKey(data, collapseKey as Parameters<typeof matchesKey>[1])) return undefined;
247
- // Kitty-protocol terminals report press, repeat, and release separately.
248
- // Toggle only on the initial press so a tap does not immediately reopen
249
- // the overlay and a held key does not toggle it repeatedly.
250
- if (isKeyRelease(data) || isKeyRepeat(data)) return { consume: true };
251
- sessionRef.current?.toggleCollapsedExternal();
252
- if (handle.isHidden() && !hasAnnouncedHide) {
253
- hasAnnouncedHide = true;
254
- ctx.ui.notify?.(
255
- `ask_user_question hidden — press ${formatKeySpecForDisplay(collapseKey)} to reopen`,
256
- "info",
257
- );
258
- }
259
- return { consume: true };
260
- });
261
- }
357
+ const sessionRef: SessionRef = { current: null };
358
+ const overlayHandleRef: OverlayHandleRef = { current: undefined };
359
+ const removeOverlayInputListener = registerCollapseKeyListener(ctx, collapseKey, sessionRef, overlayHandleRef);
262
360
  // Hiding the overlay is only reversible through the raw listener above, so
263
361
  // the session may emit `setHidden` only when it was actually registered;
264
362
  // otherwise collapse falls back to the visible one-line row.
@@ -268,37 +366,15 @@ export function registerAskUserQuestionTool(pi: ExtensionAPI): void {
268
366
  try {
269
367
  emitTerminalAttention();
270
368
  const result = await ctx.ui.custom<QuestionnaireResult>(
271
- (tui, theme, keybindings, done) => {
272
- const session = new QuestionnaireSession({
273
- tui,
274
- theme,
275
- params: typed,
276
- itemsByTab,
277
- done,
278
- keybindings,
279
- editInput: async (value) => {
280
- try {
281
- const [{ SettingsManager }, { editWithExternalEditor }] = await Promise.all([
282
- import("@earendil-works/pi-coding-agent"),
283
- import("./state/external-editor.js"),
284
- ]);
285
- const editorCommand = SettingsManager.create(ctx.cwd, undefined, {
286
- projectTrusted: ctx.isProjectTrusted(),
287
- }).getExternalEditorCommand();
288
- if (!editorCommand) throw new Error("No external editor command is configured");
289
- return await editWithExternalEditor(tui, editorCommand, value);
290
- } catch (error) {
291
- const message = error instanceof Error ? error.message : String(error);
292
- ctx.ui.notify(`${t("editor.failed", "External editor failed")}: ${message}`, "error");
293
- return undefined;
294
- }
295
- },
296
- collapseKey,
297
- canReopenWhileHidden,
298
- });
299
- sessionRef.current = session;
300
- return session.component;
301
- },
369
+ makeSessionFactory({
370
+ ctx,
371
+ typed,
372
+ itemsByTab,
373
+ collapseKey,
374
+ canReopenWhileHidden,
375
+ sessionRef,
376
+ Session: QuestionnaireSession,
377
+ }),
302
378
  {
303
379
  overlay: true,
304
380
  overlayOptions: {
@@ -314,17 +390,8 @@ export function registerAskUserQuestionTool(pi: ExtensionAPI): void {
314
390
  },
315
391
  );
316
392
 
317
- // A TUI questionnaire ALWAYS resolves a QuestionnaireResult (cancel
318
- // included — state-reducer emits `{ answers, cancelled }`), so
319
- // `undefined` uniquely means "host cannot render", never "user
320
- // declined". RPC builds that predate ctx.mode land here: run the
321
- // dialog walker when the host has the primitives; otherwise tell the
322
- // model the user never saw the questions.
323
393
  if (result === undefined) {
324
- if (hasDialogUI(ctx.ui)) {
325
- return buildQuestionnaireResponse(await runRpcQuestionnaire(ctx.ui, typed), typed);
326
- }
327
- return buildToolResult(ERROR_NO_CUSTOM_UI, { answers: [], cancelled: true, error: "no_custom_ui" });
394
+ return resolveUndefinedResult(ctx, typed);
328
395
  }
329
396
 
330
397
  return buildQuestionnaireResponse(result, typed);
@@ -335,16 +402,7 @@ export function registerAskUserQuestionTool(pi: ExtensionAPI): void {
335
402
  },
336
403
  });
337
404
 
338
- // Pre-warm the lazy session graph once startup settles (#107). A graph
339
- // evaluated while the paths Pi resolved at boot still exist stays in memory
340
- // for the process lifetime, so later on-disk dependency churn (e.g. `pnpm
341
- // install --force` replacing the store mid-session) can no longer poison
342
- // jiti's graph cache. Swallowed failure is safe: the first real call
343
- // re-imports and surfaces it through loadQuestionnaireSession's structured
344
- // envelope. unref keeps the timer from holding a non-TUI embedder's process
345
- // open.
346
- const timer = setTimeout(() => void loadQuestionnaireSession().catch(() => undefined), PREWARM_DELAY_MS);
347
- timer.unref?.();
405
+ prewarmSessionGraph();
348
406
  }
349
407
 
350
408
  export { buildQuestionnaireResponse, buildToolResult };
@@ -43,6 +43,9 @@ TUI produces. Trade-offs inherent to the native primitives:
43
43
  - No side-by-side preview pane. Previews are folded into the dialog title instead,
44
44
  truncated at 600 characters each.
45
45
  - No tab bar and no Submit review tab — one dialog per question, in order.
46
+ - No notes. Both note kinds — per-question `n` on a question tab and global `n` on the
47
+ Submit tab — are terminal-only; the host's native `select` and `input` primitives carry
48
+ no note field.
46
49
  - Multi-select is a free-text input: type the option numbers, comma-separated
47
50
  (`1,3`). Any token that is not a valid option index is treated as a typed custom answer,
48
51
  which is how the `Type something.` escape survives. An empty input commits an empty
@@ -13,7 +13,7 @@ adapts to the size of your terminal.
13
13
  | `Esc` | Cancel the whole questionnaire. | Everywhere except the notes editor, where it closes notes |
14
14
  | `Tab` / `Shift+Tab` | Next / previous tab, wrapping. `→` / `←` do the same. | Multi-question dialogs only |
15
15
  | `Space` | Toggle the focused checkbox. | Multi-select questions |
16
- | `n` | Open the notes editor for the current question. | Every question tab |
16
+ | `n` | Open the notes editor for the focused question — or, on the Submit tab, the global note for the whole questionnaire. | Every question tab; the Submit tab in multi-question dialogs |
17
17
  | `Ctrl+G` | Open Pi's configured external editor with the current custom-answer draft. | `Type something.` input |
18
18
  | `Ctrl+U` | Clear the current custom-answer draft. | `Type something.` input |
19
19
  | `Ctrl+]` | Collapse or expand the dialog. Configurable via `collapseKey`. | Everywhere, including while collapsed |
@@ -62,6 +62,11 @@ side-band keyed by tab index, not inside the answer, so writing a note does not
62
62
  question as answered — the Submit tab still lists it as outstanding. The note merges into
63
63
  the answer when you confirm it, and reaches the model as `user notes: <text>`.
64
64
 
65
+ On the Submit tab, `n` opens the global note editor instead — one note covering the whole
66
+ questionnaire. It lives outside every answer, so it survives tab switches and never marks
67
+ a question as answered; it reaches the model as `global note: <text>`, and submitting with
68
+ nothing but a global note still returns an answered result rather than a decline.
69
+
65
70
  Inside the editor, `Shift+Enter` inserts a newline, while `Esc` and `Enter` close it; other
66
71
  keystrokes edit the buffer, so `n` types an `n`. Pasted line breaks are preserved.
67
72
 
@@ -98,6 +103,9 @@ content above, `↓` for content below, `↕` for both.
98
103
 
99
104
  The footer hint line adapts to context — it drops the notes hint and appends the
100
105
  `Shift+Enter` newline hint whenever a text editor has the keyboard, with `Ctrl+U` still at
101
- the far right for custom answers. It adds the tab hint only in multi-question dialogs.
106
+ the far right for custom answers. It adds the tab hint only in multi-question dialogs. The
107
+ Submit tab follows the same idiom: its bottom hint row sits below the picker and carries
108
+ an `n to add a note` part that gives way to the `Shift+Enter` newline hint while the
109
+ global-note editor is open; a committed note shows as a `Note` entry in the review list.
102
110
  `Ctrl+G` remains Pi's global external-editor shortcut and is not repeated there. On narrow
103
111
  terminals the right edge clips with `…` so the core hints survive.
@@ -52,11 +52,13 @@ Nine locale files ship in `locales/`:
52
52
  ## What is translated, and what is not
53
53
 
54
54
  Translation covers the chrome you read: the `Type something.` and `Next` sentinel rows,
55
- the footer hint segments (including the collapsed-state line), the Submit picker labels,
56
- the review-tab heading with its submit-readiness prompt and incomplete-answers warning, the
57
- preview pane's empty and notes-affordance lines, the notes header, the external-editor
58
- failure notification, and the two RPC dialog prompts. Twenty-three keys in total, all under the namespace
59
- `@juicesharp/rpiv-ask-user-question`.
55
+ the footer hint segments (including the collapsed-state line and the Submit tab's
56
+ `n to add a note` hint), the Submit picker labels, the review-tab heading with its
57
+ submit-readiness prompt, incomplete-answers warning, and committed-note `Note` label, the
58
+ preview pane's empty and notes-affordance lines, the notes header and the global-note
59
+ header on the Submit tab, the
60
+ external-editor failure notification, and the two RPC dialog prompts. Twenty-six keys in total,
61
+ all under the namespace `@juicesharp/rpiv-ask-user-question`.
60
62
 
61
63
  Everything the *model* reads stays English by design: the tool description, the parameter
62
64
  schema descriptions, error messages, and the reserved-label list. Those are prompt inputs,
@@ -83,6 +83,7 @@ code. The `content[0].text` string is written for the model, not for a log.
83
83
  preview?: string, // echoed back when the chosen option carried a preview
84
84
  }>,
85
85
  cancelled: boolean,
86
+ globalNote?: string, // Submit-tab note; present even when cancelled is true
86
87
  error?: QuestionnaireError, // one of the codes above
87
88
  }
88
89
  }
@@ -92,11 +93,16 @@ code. The `content[0].text` string is written for the model, not for a log.
92
93
 
93
94
  On success the text reads `User has answered your questions: "<question>"="<answer>". …
94
95
  You can now continue with the user's answers in mind.` A chosen option's `preview` is
95
- appended as `selected preview: <markdown>`, and a note as `user notes: <text>`.
96
-
97
- Cancelling, and any result that produces no answer segments, both collapse to the single
98
- string `User declined to answer questions` so the model sees one canonical signal.
99
- Partial submission is allowed: unanswered questions simply contribute no segment.
96
+ appended as `selected preview: <markdown>`, a per-question note as `user notes: <text>`,
97
+ and the Submit tab's global note as a trailing `global note: <text>` segment. A global
98
+ note alone still yields the answered envelope it counts as an answer even when every
99
+ question is blank.
100
+
101
+ Cancelling, and any result with neither answer segments nor a global note, both collapse
102
+ to the single string `User declined to answer questions` so the model sees one canonical
103
+ signal. Partial submission is allowed: unanswered questions simply contribute no segment.
104
+ A cancelled result always reads as the decline in text; its note, if any, survives only
105
+ in `details.globalNote`.
100
106
 
101
107
  ## Event contract
102
108
 
@@ -19,10 +19,13 @@
19
19
  "review.heading": "Review your answers",
20
20
  "review.ready": "Ready to submit your answers?",
21
21
  "review.incomplete": "⚠ Answer remaining questions before submitting:",
22
+ "review.global_hint": "n to add a note",
23
+ "review.note_label": "Note",
22
24
 
23
25
  "preview.no_preview": "No preview available",
24
26
  "preview.notes_affordance": "Notes: press n to add notes",
25
27
  "notes.header": "Notes:",
28
+ "notes.global_header": "Global note:",
26
29
 
27
30
  "editor.failed": "External editor failed",
28
31
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-ask-user-question",
3
- "version": "2.6.4",
3
+ "version": "2.7.0",
4
4
  "description": "Pi extension. A structured questionnaire the model can put to you when it would otherwise guess, with typed options instead of free-form replies.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -94,7 +94,7 @@
94
94
  }
95
95
  },
96
96
  "dependencies": {
97
- "@juicesharp/rpiv-config": "^2.6.4",
97
+ "@juicesharp/rpiv-config": "^2.7.0",
98
98
  "typebox": "^1.1.24"
99
99
  }
100
100
  }