@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.
- package/README.md +7 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +63 -15
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +78 -16
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +13 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +17 -6
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +4 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +154 -96
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +3 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +7 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +3 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +137 -96
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +44 -32
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +56 -34
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +50 -4
- package/node_modules/@narumitw/pi-btw/package.json +2 -2
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
- package/node_modules/pi-lens/CHANGELOG.md +90 -0
- package/node_modules/pi-lens/README.md +106 -0
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +97 -6
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +16 -6
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +14 -0
- package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +22 -1
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +8 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +10 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +19 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +35 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +8 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +110 -23
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +117 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +16 -14
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +8 -9
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +22 -0
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +5 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +7 -2
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +27 -2
- package/node_modules/pi-lens/dist/clients/lsp/client.js +144 -5
- package/node_modules/pi-lens/dist/clients/lsp/index.js +151 -6
- package/node_modules/pi-lens/dist/clients/module-report.js +14 -1
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +18 -9
- package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +7 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +88 -19
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +44 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +41 -0
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +47 -2
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
- package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
- package/node_modules/pi-lens/dist/index.js +1224 -393
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +10 -3
- package/node_modules/pi-lens/package.json +18 -5
- package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
- package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
- package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
- package/node_modules/pi-web-access/CHANGELOG.md +14 -0
- package/node_modules/pi-web-access/README.md +6 -5
- package/node_modules/pi-web-access/chrome-cookies.ts +86 -15
- package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
- package/node_modules/pi-web-access/extract.ts +12 -2
- package/node_modules/pi-web-access/firecrawl.ts +18 -2
- package/node_modules/pi-web-access/github-extract.ts +65 -36
- package/node_modules/pi-web-access/index.ts +19 -4
- package/node_modules/pi-web-access/openai-search.ts +17 -6
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/pdf-extract.ts +18 -5
- package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
- 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
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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>`,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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.
|
|
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.
|
|
97
|
+
"@juicesharp/rpiv-config": "^2.7.0",
|
|
98
98
|
"typebox": "^1.1.24"
|
|
99
99
|
}
|
|
100
100
|
}
|