@nklisch/pi-enhanced 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/node_modules/@nklisch/pi-background-tasks/CHANGELOG.md +7 -0
- package/node_modules/@nklisch/pi-background-tasks/extensions/background-tasks.test.ts +105 -0
- package/node_modules/@nklisch/pi-background-tasks/extensions/background-tasks.ts +131 -44
- package/node_modules/@nklisch/pi-background-tasks/package.json +1 -1
- package/node_modules/@nklisch/pi-background-tasks/skills/background-tasks/SKILL.md +6 -7
- package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-x64-msvc.node +0 -0
- package/node_modules/@nklisch/pi-clearance/package.json +1 -1
- package/node_modules/@nklisch/pi-clearance/src/config/config-command-writer.ts +34 -17
- package/node_modules/@nklisch/pi-clearance/src/index.ts +34 -3
- package/node_modules/@nklisch/pi-clearance/src/packs/package-registration.ts +63 -36
- package/node_modules/@nklisch/pi-clearance/src/runtime/allow-request-message.ts +40 -1
- package/node_modules/@nklisch/pi-clearance/src/runtime/command-registry.ts +22 -2
- package/node_modules/@nklisch/pi-clearance/src/runtime/command-transforms.ts +50 -29
- package/node_modules/@nklisch/pi-clearance/src/runtime/config-commands/settings/native-ui.ts +233 -49
- package/node_modules/@nklisch/pi-clearance/src/runtime/policy-cache.ts +19 -5
- package/node_modules/@nklisch/pi-conveniences/extensions/context-window-footer.ts +48 -13
- package/node_modules/@nklisch/pi-conveniences/package.json +1 -1
- package/node_modules/@nklisch/pi-fff-compat/extensions/fff-compat-search.ts +49 -42
- package/node_modules/@nklisch/pi-fff-compat/extensions/finder-lifecycle.ts +92 -0
- package/node_modules/@nklisch/pi-fff-compat/package.json +2 -2
- package/node_modules/@nklisch/pi-model-modes/package.json +1 -1
- package/node_modules/@nklisch/pi-model-modes/src/command-message.ts +30 -0
- package/node_modules/@nklisch/pi-model-modes/src/commands.ts +31 -15
- package/node_modules/@nklisch/pi-model-modes/src/style-command.ts +25 -14
- package/node_modules/@nklisch/pi-plugins/README.md +2 -2
- package/node_modules/@nklisch/pi-plugins/dist/application/subagent-hook-runtime.js +4 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/subagent-hook-runtime.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/composition/background-update-coordinator.js +12 -3
- package/node_modules/@nklisch/pi-plugins/dist/composition/background-update-coordinator.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/process/command-runner.d.ts +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/process/command-runner.js +19 -11
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/process/command-runner.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/hooks/pi-hook-decision-adapter.js +36 -7
- package/node_modules/@nklisch/pi-plugins/dist/pi/hooks/pi-hook-decision-adapter.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-component.js +14 -3
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-component.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-session.js +153 -54
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-session.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager-lifecycle.js +35 -8
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager-lifecycle.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/runtime/mcp/pi-mcp-adapter-package.js +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/runtime/mcp/pi-mcp-adapter-runtime.js +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/runtime/subagents/pi-subagents-package.js +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/CHANGELOG.md +7 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/package.json +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/custom-agents.ts +22 -17
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/debug.ts +18 -0
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/child-lifecycle.ts +22 -4
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/concurrency-limiter.ts +32 -8
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/create-subagent-session.ts +8 -7
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/lifecycle-interceptor.ts +25 -6
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/run-listeners.ts +6 -2
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-manager.ts +31 -16
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-session.ts +70 -32
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent.ts +55 -50
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/workspace-bracket.ts +6 -2
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/record-observer.ts +37 -34
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/subagent-events-observer.ts +15 -14
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/foreground-runner.ts +31 -26
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/agent-widget.ts +51 -26
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/session-navigator.ts +100 -46
- package/node_modules/@nklisch/pi-plugins/package.json +3 -3
- package/package.json +4 -4
- package/test/verify-bundle.mjs +8 -0
|
@@ -152,51 +152,72 @@ export function createPackageRegistrationStore(
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
function notify(): void {
|
|
155
|
-
|
|
155
|
+
try {
|
|
156
|
+
onChange?.();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
// The change hook invalidates another in-memory subsystem. It is part of
|
|
159
|
+
// this callback's recovery path, so a broken hook must remain observable
|
|
160
|
+
// without escaping into the host event bus.
|
|
161
|
+
recordListenerIssue("change-notification-error", error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function recordListenerIssue(code: string, error: unknown): void {
|
|
166
|
+
const message = `package registration event handler failed: ${errorMessage(error)}`;
|
|
167
|
+
issues.push({ severity: "error", code, path: "event", message });
|
|
168
|
+
console.error(`Pi Clearance ${message}`);
|
|
156
169
|
}
|
|
157
170
|
|
|
158
171
|
// Register-event listener. Classifies each incoming payload by request id:
|
|
159
172
|
// accept unsolicited (no requestId) and current-request responses; warn and
|
|
160
|
-
// ignore stale responses for older request ids. The
|
|
161
|
-
//
|
|
162
|
-
//
|
|
173
|
+
// ignore stale responses for older request ids. The event bus is an
|
|
174
|
+
// out-of-band callback boundary: payload property getters and normalizers
|
|
175
|
+
// operate inside the containment block so hostile contributor input cannot
|
|
176
|
+
// escape through Pi's shared event bus.
|
|
163
177
|
const unsubscribeRegister = bus.on(
|
|
164
178
|
AUTO_REVIEWER_PACKS_REGISTER_EVENT,
|
|
165
179
|
(data: unknown) => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
180
|
+
try {
|
|
181
|
+
if (disposed) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const rawRequestId = readRequestId(data);
|
|
169
186
|
|
|
170
|
-
|
|
187
|
+
if (rawRequestId !== undefined && rawRequestId !== currentRequestId) {
|
|
188
|
+
// Stale: a response for a request we are no longer servicing (e.g. an
|
|
189
|
+
// older request id after reload). Record a warning and contribute
|
|
190
|
+
// nothing, so stale data can never widen available packs.
|
|
191
|
+
issues.push({
|
|
192
|
+
severity: "warning",
|
|
193
|
+
code: "stale-registration",
|
|
194
|
+
path: "event",
|
|
195
|
+
message: buildStaleMessage(rawRequestId, currentRequestId),
|
|
196
|
+
});
|
|
197
|
+
if (!isCollecting) {
|
|
198
|
+
notify();
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
171
202
|
|
|
172
|
-
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
203
|
+
// Accept: matches the current request id, or unsolicited (no request
|
|
204
|
+
// id). Unsolicited registrations support manual tests and contributors
|
|
205
|
+
// that emit during their own session_start.
|
|
206
|
+
const result = normalizePackagePackRegistration(data);
|
|
207
|
+
for (const pack of result.packs) {
|
|
208
|
+
packs.push(pack);
|
|
209
|
+
}
|
|
210
|
+
for (const issue of result.issues) {
|
|
211
|
+
issues.push(issue);
|
|
212
|
+
}
|
|
213
|
+
if (!isCollecting) {
|
|
214
|
+
notify();
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
recordListenerIssue("listener-error", error);
|
|
182
218
|
if (!isCollecting) {
|
|
183
219
|
notify();
|
|
184
220
|
}
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Accept: matches the current request id, or unsolicited (no request id).
|
|
189
|
-
// Unsolicited registrations support manual tests and contributors that
|
|
190
|
-
// emit during their own session_start.
|
|
191
|
-
const result = normalizePackagePackRegistration(data);
|
|
192
|
-
for (const pack of result.packs) {
|
|
193
|
-
packs.push(pack);
|
|
194
|
-
}
|
|
195
|
-
for (const issue of result.issues) {
|
|
196
|
-
issues.push(issue);
|
|
197
|
-
}
|
|
198
|
-
if (!isCollecting) {
|
|
199
|
-
notify();
|
|
200
221
|
}
|
|
201
222
|
},
|
|
202
223
|
);
|
|
@@ -233,9 +254,7 @@ export function createPackageRegistrationStore(
|
|
|
233
254
|
severity: "error",
|
|
234
255
|
code: "collection-error",
|
|
235
256
|
path: "event",
|
|
236
|
-
message: `package registration request failed: ${
|
|
237
|
-
error instanceof Error ? error.message : String(error)
|
|
238
|
-
}`,
|
|
257
|
+
message: `package registration request failed: ${errorMessage(error)}`,
|
|
239
258
|
});
|
|
240
259
|
} finally {
|
|
241
260
|
isCollecting = false;
|
|
@@ -297,3 +316,11 @@ function buildStaleMessage(
|
|
|
297
316
|
? `Ignoring package registration for request id "${rawRequestId}" (no active collection).`
|
|
298
317
|
: `Ignoring package registration for stale request id "${rawRequestId}" (current "${currentRequestId}").`;
|
|
299
318
|
}
|
|
319
|
+
|
|
320
|
+
function errorMessage(error: unknown): string {
|
|
321
|
+
try {
|
|
322
|
+
return error instanceof Error ? error.message : String(error);
|
|
323
|
+
} catch {
|
|
324
|
+
return "unknown error";
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -16,6 +16,45 @@ export interface ClearanceAllowRequestDetails {
|
|
|
16
16
|
export const renderClearanceAllowRequest: MessageRenderer<
|
|
17
17
|
ClearanceAllowRequestDetails
|
|
18
18
|
> = (message) => {
|
|
19
|
-
const form = message
|
|
19
|
+
const form = readAllowRequestForm(message);
|
|
20
20
|
return new Text(`[Pi Clearance] Allow request handed to agent (${form})`);
|
|
21
21
|
};
|
|
22
|
+
|
|
23
|
+
function readAllowRequestForm(message: unknown):
|
|
24
|
+
| ClearanceAllowRequestDetails["form"]
|
|
25
|
+
| "request" {
|
|
26
|
+
try {
|
|
27
|
+
if (!isRecord(message)) return "request";
|
|
28
|
+
const details = message.details;
|
|
29
|
+
if (!isRecord(details)) return "request";
|
|
30
|
+
const form = details.form;
|
|
31
|
+
return isAllowRequestForm(form) ? form : "request";
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// Message details come from the persisted/custom-message boundary. A
|
|
34
|
+
// hostile getter must degrade the transcript row, not escape the renderer.
|
|
35
|
+
console.error(
|
|
36
|
+
`Pi Clearance allow-request details validation failed: ${errorMessage(error)}`,
|
|
37
|
+
);
|
|
38
|
+
return "request";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isAllowRequestForm(value: unknown): value is ClearanceAllowRequestDetails["form"] {
|
|
43
|
+
return (
|
|
44
|
+
value === "free-text" ||
|
|
45
|
+
value === "recent-command" ||
|
|
46
|
+
value === "no-recent-command"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
51
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function errorMessage(error: unknown): string {
|
|
55
|
+
try {
|
|
56
|
+
return error instanceof Error ? error.message : String(error);
|
|
57
|
+
} catch {
|
|
58
|
+
return "unknown error";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -87,8 +87,20 @@ export function buildClearanceCommandOptions(
|
|
|
87
87
|
return getClearanceArgumentCompletions(argumentPrefix, deps);
|
|
88
88
|
},
|
|
89
89
|
async handler(args, ctx) {
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
try {
|
|
91
|
+
const report = await handleClearanceCommand(args, ctx, pi, deps);
|
|
92
|
+
notify(ctx, report.markdown, report.level ?? "info");
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// Pi catches registered command promises, but that host-level catch does
|
|
95
|
+
// not provide a useful slash-command recovery message. Do not convert
|
|
96
|
+
// this into a write result or attempt rollback: any completed durable
|
|
97
|
+
// write remains completed, and the user gets an observable diagnostic.
|
|
98
|
+
notify(
|
|
99
|
+
ctx,
|
|
100
|
+
`Pi Clearance command failed: ${errorMessage(error)}`,
|
|
101
|
+
"error",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
92
104
|
},
|
|
93
105
|
};
|
|
94
106
|
}
|
|
@@ -364,3 +376,11 @@ function splitCompletionPrefix(argumentPrefix: string): {
|
|
|
364
376
|
current: tokens.at(-1) ?? "",
|
|
365
377
|
};
|
|
366
378
|
}
|
|
379
|
+
|
|
380
|
+
function errorMessage(error: unknown): string {
|
|
381
|
+
try {
|
|
382
|
+
return error instanceof Error ? error.message : String(error);
|
|
383
|
+
} catch {
|
|
384
|
+
return "unknown error";
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -170,36 +170,51 @@ export function createCommandTransformStore(
|
|
|
170
170
|
currentRequestId = null;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
function recordListenerIssue(code: string, error: unknown): void {
|
|
174
|
+
const message = `command transform registration event handler failed: ${errorMessage(error)}`;
|
|
175
|
+
issues.push({ severity: "error", code, path: "event", message });
|
|
176
|
+
console.error(`Pi Clearance ${message}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
173
179
|
const unsubscribeRegister = on(
|
|
174
180
|
AUTO_REVIEWER_TRANSFORMS_REGISTER_EVENT,
|
|
175
181
|
(data: unknown) => {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
try {
|
|
183
|
+
if (disposed) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
179
186
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
187
|
+
// This is a raw event-bus callback. Keep request-id getters,
|
|
188
|
+
// normalization, and array updates in one containment block: a
|
|
189
|
+
// contributor can supply a Proxy/getter rather than a plain object.
|
|
190
|
+
const rawRequestId = readRequestId(data);
|
|
191
|
+
if (rawRequestId !== undefined && rawRequestId !== currentRequestId) {
|
|
192
|
+
// Stale response for a request we are no longer servicing. Ignore it
|
|
193
|
+
// so stale data can never widen the transform set.
|
|
194
|
+
issues.push({
|
|
195
|
+
severity: "warning",
|
|
196
|
+
code: "stale-registration",
|
|
197
|
+
path: "event",
|
|
198
|
+
message: buildStaleMessage(rawRequestId, currentRequestId),
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
192
202
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
+
const result = normalizeCommandTransformRegistration(data);
|
|
204
|
+
if (result.transform !== null) {
|
|
205
|
+
transforms.push({
|
|
206
|
+
id: result.transform.id,
|
|
207
|
+
description: result.transform.description,
|
|
208
|
+
run: result.transform.transform,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
for (const issue of result.issues) {
|
|
212
|
+
issues.push(issue);
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
// Do not rely on Pi's event bus swallowing errors. This listener is an
|
|
216
|
+
// extension-owned out-of-band boundary and must be total on its own.
|
|
217
|
+
recordListenerIssue("listener-error", error);
|
|
203
218
|
}
|
|
204
219
|
},
|
|
205
220
|
);
|
|
@@ -227,9 +242,7 @@ export function createCommandTransformStore(
|
|
|
227
242
|
severity: "error",
|
|
228
243
|
code: "collection-error",
|
|
229
244
|
path: "event",
|
|
230
|
-
message: `transform registration request failed: ${
|
|
231
|
-
error instanceof Error ? error.message : String(error)
|
|
232
|
-
}`,
|
|
245
|
+
message: `transform registration request failed: ${errorMessage(error)}`,
|
|
233
246
|
});
|
|
234
247
|
}
|
|
235
248
|
|
|
@@ -310,7 +323,7 @@ export function createCommandTransformStore(
|
|
|
310
323
|
lastNote = {
|
|
311
324
|
id: transform.id,
|
|
312
325
|
kind: "error",
|
|
313
|
-
message:
|
|
326
|
+
message: errorMessage(error),
|
|
314
327
|
};
|
|
315
328
|
// fail open: keep current command
|
|
316
329
|
}
|
|
@@ -371,3 +384,11 @@ function buildStaleMessage(
|
|
|
371
384
|
? `Ignoring transform registration for request id "${rawRequestId}" (no active collection).`
|
|
372
385
|
: `Ignoring transform registration for stale request id "${rawRequestId}" (current "${currentRequestId}").`;
|
|
373
386
|
}
|
|
387
|
+
|
|
388
|
+
function errorMessage(error: unknown): string {
|
|
389
|
+
try {
|
|
390
|
+
return error instanceof Error ? error.message : String(error);
|
|
391
|
+
} catch {
|
|
392
|
+
return "unknown error";
|
|
393
|
+
}
|
|
394
|
+
}
|
package/node_modules/@nklisch/pi-clearance/src/runtime/config-commands/settings/native-ui.ts
CHANGED
|
@@ -146,10 +146,14 @@ interface MinimalTui {
|
|
|
146
146
|
const DOSSIER_VISIBLE_LINES = 12;
|
|
147
147
|
|
|
148
148
|
export function canOpenSettingsNativeUi(ctx: ExtensionCommandContext): boolean {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
try {
|
|
150
|
+
const custom = (
|
|
151
|
+
ctx as unknown as { readonly ui?: { readonly custom?: unknown } }
|
|
152
|
+
).ui?.custom;
|
|
153
|
+
return ctx.hasUI === true && typeof custom === "function";
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
153
157
|
}
|
|
154
158
|
|
|
155
159
|
export async function openSettingsNativeUi(
|
|
@@ -179,15 +183,27 @@ export async function openSettingsNativeUi(
|
|
|
179
183
|
const handled: SettingsNativeUiHandledAction[] = [];
|
|
180
184
|
|
|
181
185
|
while (true) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
186
|
+
let interaction: NativeSettingsUiInteraction;
|
|
187
|
+
try {
|
|
188
|
+
interaction = await presentSettingsNativeUi({
|
|
189
|
+
ctx: options.ctx,
|
|
190
|
+
model,
|
|
191
|
+
screen,
|
|
192
|
+
selected,
|
|
193
|
+
dossierScroll,
|
|
194
|
+
dossierOrigin,
|
|
195
|
+
...(message === undefined ? {} : { message }),
|
|
196
|
+
});
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
report: nativeSettingsFailureReport(
|
|
201
|
+
"presentation",
|
|
202
|
+
error,
|
|
203
|
+
handled.length,
|
|
204
|
+
),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
191
207
|
const selection = interaction.selection;
|
|
192
208
|
if (selection.kind === "close") {
|
|
193
209
|
return {
|
|
@@ -201,11 +217,16 @@ export async function openSettingsNativeUi(
|
|
|
201
217
|
};
|
|
202
218
|
}
|
|
203
219
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
220
|
+
let report: CommandReport;
|
|
221
|
+
try {
|
|
222
|
+
report = await dispatchNativeSettingsSelection(
|
|
223
|
+
selection,
|
|
224
|
+
options.ctx,
|
|
225
|
+
options.deps,
|
|
226
|
+
);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
report = nativeSettingsFailureReport("action", error, handled.length);
|
|
229
|
+
}
|
|
209
230
|
const handledAction: SettingsNativeUiHandledAction = {
|
|
210
231
|
label: selection.label,
|
|
211
232
|
reportTitle: report.title,
|
|
@@ -232,7 +253,15 @@ export async function openSettingsNativeUi(
|
|
|
232
253
|
selected = dossier === undefined ? interaction.selected : 0;
|
|
233
254
|
dossierScroll = dossier === undefined ? interaction.dossierScroll : 0;
|
|
234
255
|
|
|
235
|
-
|
|
256
|
+
let reloaded: Awaited<ReturnType<SettingsNativeUiReload>>;
|
|
257
|
+
try {
|
|
258
|
+
reloaded = await options.reload();
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return {
|
|
261
|
+
ok: false,
|
|
262
|
+
report: nativeSettingsFailureReport("reload", error, handled.length),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
236
265
|
if (!reloaded.ok) {
|
|
237
266
|
return { ok: false, report: reloaded.report };
|
|
238
267
|
}
|
|
@@ -252,25 +281,86 @@ async function presentSettingsNativeUi(input: {
|
|
|
252
281
|
const custom = input.ctx.ui.custom.bind(input.ctx.ui);
|
|
253
282
|
return await custom<NativeSettingsUiInteraction>(
|
|
254
283
|
(tui, theme, _keybindings, done) => {
|
|
255
|
-
let component
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
284
|
+
let component: SettingsNativeUiComponent | undefined;
|
|
285
|
+
let lastFailure: string | undefined;
|
|
286
|
+
const fallbackNavigation = {
|
|
287
|
+
screen: input.screen,
|
|
288
|
+
selected: input.selected,
|
|
289
|
+
dossierScroll: input.dossierScroll,
|
|
290
|
+
} satisfies Omit<NativeSettingsUiInteraction, "selection">;
|
|
291
|
+
|
|
292
|
+
const reportFailure = (phase: string, error: unknown): void => {
|
|
293
|
+
const diagnostic = `Pi Clearance native settings ${phase} failed: ${errorMessage(error)}`;
|
|
294
|
+
if (diagnostic === lastFailure) return;
|
|
295
|
+
lastFailure = diagnostic;
|
|
296
|
+
console.error(diagnostic);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const safeDone = (selection: SettingsNativeUiSelection): void => {
|
|
300
|
+
try {
|
|
301
|
+
done({
|
|
302
|
+
selection,
|
|
303
|
+
...(component?.getNavigationState() ?? fallbackNavigation),
|
|
304
|
+
});
|
|
305
|
+
} catch (error) {
|
|
306
|
+
reportFailure("completion", error);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
component = new SettingsNativeUiComponent({
|
|
312
|
+
model: input.model,
|
|
313
|
+
theme: theme as MinimalTheme,
|
|
314
|
+
done: safeDone,
|
|
315
|
+
initialScreen: input.screen,
|
|
316
|
+
initialSelected: input.selected,
|
|
317
|
+
initialDossierScroll: input.dossierScroll,
|
|
318
|
+
dossierOrigin: input.dossierOrigin,
|
|
319
|
+
...(input.message === undefined ? {} : { message: input.message }),
|
|
320
|
+
});
|
|
321
|
+
} catch (error) {
|
|
322
|
+
reportFailure("component construction", error);
|
|
323
|
+
}
|
|
324
|
+
|
|
267
325
|
const minimalTui = tui as MinimalTui;
|
|
268
326
|
return {
|
|
269
|
-
render: (width: number) =>
|
|
270
|
-
|
|
327
|
+
render: (width: number) => {
|
|
328
|
+
try {
|
|
329
|
+
if (component === undefined) {
|
|
330
|
+
return renderNativeSettingsFallback(width, lastFailure);
|
|
331
|
+
}
|
|
332
|
+
return component.render(width);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
reportFailure("render", error);
|
|
335
|
+
return renderNativeSettingsFallback(width, lastFailure);
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
invalidate: () => {
|
|
339
|
+
try {
|
|
340
|
+
component?.invalidate();
|
|
341
|
+
} catch (error) {
|
|
342
|
+
reportFailure("invalidation", error);
|
|
343
|
+
}
|
|
344
|
+
},
|
|
271
345
|
handleInput: (data: string) => {
|
|
272
|
-
|
|
273
|
-
|
|
346
|
+
try {
|
|
347
|
+
if (component === undefined) {
|
|
348
|
+
if (isClose(data)) safeDone({ kind: "close" });
|
|
349
|
+
} else {
|
|
350
|
+
component.handleInput(data);
|
|
351
|
+
}
|
|
352
|
+
} catch (error) {
|
|
353
|
+
reportFailure("input", error);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// requestRender is a host callback too; a broken TUI refresh must
|
|
357
|
+
// not prevent the component from accepting a later close/recovery
|
|
358
|
+
// input or turn a completed settings write into a failed command.
|
|
359
|
+
try {
|
|
360
|
+
minimalTui.requestRender?.call(tui);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
reportFailure("input refresh", error);
|
|
363
|
+
}
|
|
274
364
|
},
|
|
275
365
|
};
|
|
276
366
|
},
|
|
@@ -320,16 +410,29 @@ async function readNativeSettingsInput(
|
|
|
320
410
|
| { readonly ok: true; readonly value: string | null }
|
|
321
411
|
| { readonly ok: false; readonly report: CommandReport }
|
|
322
412
|
> {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
413
|
+
let input:
|
|
414
|
+
| ((
|
|
415
|
+
title: string,
|
|
416
|
+
placeholder?: string,
|
|
417
|
+
) => Promise<string | undefined> | string | undefined)
|
|
418
|
+
| undefined;
|
|
419
|
+
try {
|
|
420
|
+
input = (
|
|
421
|
+
ctx as unknown as {
|
|
422
|
+
readonly ui?: {
|
|
423
|
+
readonly input?: (
|
|
424
|
+
title: string,
|
|
425
|
+
placeholder?: string,
|
|
426
|
+
) => Promise<string | undefined> | string | undefined;
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
).ui?.input;
|
|
430
|
+
} catch (error) {
|
|
431
|
+
return {
|
|
432
|
+
ok: false,
|
|
433
|
+
report: nativeInputRefusedReport(selection, "failed", error),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
333
436
|
|
|
334
437
|
if (typeof input !== "function") {
|
|
335
438
|
return {
|
|
@@ -338,7 +441,29 @@ async function readNativeSettingsInput(
|
|
|
338
441
|
};
|
|
339
442
|
}
|
|
340
443
|
|
|
341
|
-
|
|
444
|
+
let value: string | undefined;
|
|
445
|
+
try {
|
|
446
|
+
const rawValue = await input(
|
|
447
|
+
selection.prompt,
|
|
448
|
+
selection.placeholder ?? "",
|
|
449
|
+
);
|
|
450
|
+
if (rawValue !== undefined && typeof rawValue !== "string") {
|
|
451
|
+
return {
|
|
452
|
+
ok: false,
|
|
453
|
+
report: nativeInputRefusedReport(
|
|
454
|
+
selection,
|
|
455
|
+
"failed",
|
|
456
|
+
new Error("Pi UI input returned a non-string value"),
|
|
457
|
+
),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
value = rawValue;
|
|
461
|
+
} catch (error) {
|
|
462
|
+
return {
|
|
463
|
+
ok: false,
|
|
464
|
+
report: nativeInputRefusedReport(selection, "failed", error),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
342
467
|
if (value === undefined) {
|
|
343
468
|
return {
|
|
344
469
|
ok: false,
|
|
@@ -346,7 +471,15 @@ async function readNativeSettingsInput(
|
|
|
346
471
|
};
|
|
347
472
|
}
|
|
348
473
|
|
|
349
|
-
|
|
474
|
+
let trimmed: string;
|
|
475
|
+
try {
|
|
476
|
+
trimmed = value.trim();
|
|
477
|
+
} catch (error) {
|
|
478
|
+
return {
|
|
479
|
+
ok: false,
|
|
480
|
+
report: nativeInputRefusedReport(selection, "failed", error),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
350
483
|
if (trimmed.length === 0) {
|
|
351
484
|
if (selection.empty === "null") {
|
|
352
485
|
return { ok: true, value: null };
|
|
@@ -365,7 +498,8 @@ function nativeInputRefusedReport(
|
|
|
365
498
|
SettingsNativeUiSelection,
|
|
366
499
|
{ readonly kind: "input-action" }
|
|
367
500
|
>,
|
|
368
|
-
reason: "unavailable" | "cancelled" | "empty",
|
|
501
|
+
reason: "unavailable" | "cancelled" | "empty" | "failed",
|
|
502
|
+
error?: unknown,
|
|
369
503
|
): CommandReport {
|
|
370
504
|
const summary = (() => {
|
|
371
505
|
switch (reason) {
|
|
@@ -375,6 +509,8 @@ function nativeInputRefusedReport(
|
|
|
375
509
|
return "Text input was cancelled; no config changes were written.";
|
|
376
510
|
case "empty":
|
|
377
511
|
return "Text input was empty; no config changes were written.";
|
|
512
|
+
case "failed":
|
|
513
|
+
return `Text input failed: ${errorMessage(error)}; no config changes were written.`;
|
|
378
514
|
}
|
|
379
515
|
})();
|
|
380
516
|
|
|
@@ -390,10 +526,50 @@ function nativeInputRefusedReport(
|
|
|
390
526
|
"- No config changes were written.",
|
|
391
527
|
].join("\n"),
|
|
392
528
|
details: { reason: `native-input-${reason}`, selection },
|
|
393
|
-
level: reason === "unavailable" ? "error" : "warning",
|
|
529
|
+
level: reason === "unavailable" || reason === "failed" ? "error" : "warning",
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function nativeSettingsFailureReport(
|
|
534
|
+
phase: string,
|
|
535
|
+
error: unknown,
|
|
536
|
+
actionsHandled: number,
|
|
537
|
+
): CommandReport {
|
|
538
|
+
const message = errorMessage(error);
|
|
539
|
+
const summary = `Pi Clearance settings ${phase} failed: ${message}. Previously confirmed actions, if any, remain applied; no rollback was attempted.`;
|
|
540
|
+
return {
|
|
541
|
+
title: "Pi Clearance settings failed",
|
|
542
|
+
summary,
|
|
543
|
+
markdown: [
|
|
544
|
+
"# Pi Clearance settings failed",
|
|
545
|
+
"",
|
|
546
|
+
`- Phase: ${phase}`,
|
|
547
|
+
`- Error: ${message}`,
|
|
548
|
+
`- Actions already handled: ${actionsHandled}`,
|
|
549
|
+
"- Reopen status to verify any previously confirmed writes.",
|
|
550
|
+
].join("\n"),
|
|
551
|
+
details: { reason: `native-ui-${phase}-failed`, actionsHandled, error: message },
|
|
552
|
+
level: "error",
|
|
394
553
|
};
|
|
395
554
|
}
|
|
396
555
|
|
|
556
|
+
function renderNativeSettingsFallback(
|
|
557
|
+
width: number,
|
|
558
|
+
diagnostic: string | undefined,
|
|
559
|
+
): string[] {
|
|
560
|
+
const lines = [
|
|
561
|
+
"Pi Clearance settings UI encountered an error.",
|
|
562
|
+
diagnostic ?? "The settings component could not be rendered.",
|
|
563
|
+
"Previously confirmed actions, if any, remain applied.",
|
|
564
|
+
"Press q or Esc to close.",
|
|
565
|
+
];
|
|
566
|
+
try {
|
|
567
|
+
return lines.map((line) => truncateAnsi(line, Math.max(1, width)));
|
|
568
|
+
} catch {
|
|
569
|
+
return ["Pi Clearance settings UI unavailable.", "Press q or Esc to close."];
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
397
573
|
interface NativeSettingsMessage {
|
|
398
574
|
readonly level: "info" | "warning" | "error";
|
|
399
575
|
readonly text: string;
|
|
@@ -1045,3 +1221,11 @@ function isClose(data: string): boolean {
|
|
|
1045
1221
|
export function truncateAnsi(input: string, width: number): string {
|
|
1046
1222
|
return truncateToWidth(input, width, width >= 2 ? "…" : "");
|
|
1047
1223
|
}
|
|
1224
|
+
|
|
1225
|
+
function errorMessage(error: unknown): string {
|
|
1226
|
+
try {
|
|
1227
|
+
return error instanceof Error ? error.message : String(error);
|
|
1228
|
+
} catch {
|
|
1229
|
+
return "unknown error";
|
|
1230
|
+
}
|
|
1231
|
+
}
|