@blackbelt-technology/pi-agent-dashboard 0.4.0 → 0.4.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/AGENTS.md +104 -35
- package/README.md +390 -494
- package/docs/architecture.md +423 -20
- package/package.json +11 -8
- package/packages/extension/package.json +11 -4
- package/packages/extension/src/__tests__/ask-user-schema-discriminator.test.ts +141 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +91 -15
- package/packages/extension/src/__tests__/bridge-entry-id-pi-070.test.ts +174 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +30 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +64 -76
- package/packages/extension/src/__tests__/multiselect-dashboard-routing.test.ts +203 -0
- package/packages/extension/src/__tests__/multiselect-list.test.ts +137 -0
- package/packages/extension/src/__tests__/multiselect-polyfill.test.ts +92 -0
- package/packages/extension/src/__tests__/no-session-replacement-calls.test.ts +99 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +81 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +37 -0
- package/packages/extension/src/__tests__/ui-decorators.test.ts +309 -0
- package/packages/extension/src/__tests__/ui-modules.test.ts +293 -0
- package/packages/extension/src/ask-user-tool.ts +170 -61
- package/packages/extension/src/bridge.ts +199 -19
- package/packages/extension/src/multiselect-decode.ts +40 -0
- package/packages/extension/src/multiselect-list.ts +146 -0
- package/packages/extension/src/multiselect-polyfill.ts +73 -0
- package/packages/extension/src/server-launcher.ts +15 -3
- package/packages/extension/src/ui-modules.ts +272 -0
- package/packages/server/package.json +11 -5
- package/packages/server/src/__tests__/auto-attach.test.ts +61 -8
- package/packages/server/src/__tests__/browse-endpoint.test.ts +295 -19
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +36 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +163 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +315 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +303 -0
- package/packages/server/src/__tests__/directory-service.test.ts +174 -0
- package/packages/server/src/__tests__/fixtures/fork-jsonl-roundtrip.jsonl +8 -0
- package/packages/server/src/__tests__/fork-jsonl-roundtrip.test.ts +49 -0
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +225 -0
- package/packages/server/src/__tests__/package-manager-wrapper-move.test.ts +414 -0
- package/packages/server/src/__tests__/package-routes.test.ts +136 -3
- package/packages/server/src/__tests__/package-source-helpers.test.ts +101 -0
- package/packages/server/src/__tests__/pending-attach-registry.test.ts +123 -0
- package/packages/server/src/__tests__/pending-resume-intent-registry.test.ts +138 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +73 -30
- package/packages/server/src/__tests__/pi-gateway-consume-pending-attach.test.ts +112 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +72 -0
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +180 -0
- package/packages/server/src/__tests__/post-install-rescan.test.ts +134 -0
- package/packages/server/src/__tests__/proposal-attach-naming.test.ts +79 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +34 -6
- package/packages/server/src/__tests__/session-action-handler-spawn-with-attach.test.ts +108 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +55 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +242 -0
- package/packages/server/src/__tests__/session-scanner.test.ts +44 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +40 -0
- package/packages/server/src/__tests__/translate-path-source.test.ts +77 -0
- package/packages/server/src/__tests__/ui-decorators-replay.test.ts +209 -0
- package/packages/server/src/__tests__/ui-modules-replay.test.ts +221 -0
- package/packages/server/src/browse.ts +118 -13
- package/packages/server/src/browser-gateway.ts +19 -0
- package/packages/server/src/browser-handlers/__tests__/session-meta-handler.test.ts +183 -0
- package/packages/server/src/browser-handlers/directory-handler.ts +7 -1
- package/packages/server/src/browser-handlers/handler-context.ts +15 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +29 -3
- package/packages/server/src/browser-handlers/session-meta-handler.ts +46 -12
- package/packages/server/src/browser-handlers/subscription-handler.ts +46 -1
- package/packages/server/src/cli.ts +61 -15
- package/packages/server/src/directory-service.ts +156 -15
- package/packages/server/src/event-wiring.ts +111 -10
- package/packages/server/src/installed-package-enricher.ts +143 -0
- package/packages/server/src/package-manager-wrapper.ts +305 -8
- package/packages/server/src/package-source-helpers.ts +104 -0
- package/packages/server/src/pending-attach-registry.ts +112 -0
- package/packages/server/src/pending-resume-intent-registry.ts +107 -0
- package/packages/server/src/pi-core-checker.ts +9 -14
- package/packages/server/src/pi-gateway.ts +14 -0
- package/packages/server/src/pi-version-skew.ts +12 -1
- package/packages/server/src/proposal-attach-naming.ts +47 -0
- package/packages/server/src/restart-helper.ts +13 -2
- package/packages/server/src/routes/file-routes.ts +29 -3
- package/packages/server/src/routes/package-routes.ts +72 -3
- package/packages/server/src/routes/plugin-config-routes.ts +129 -0
- package/packages/server/src/routes/system-routes.ts +2 -0
- package/packages/server/src/server.ts +339 -10
- package/packages/server/src/session-api.ts +30 -5
- package/packages/server/src/session-order-manager.ts +22 -0
- package/packages/server/src/session-scanner.ts +10 -1
- package/packages/shared/package.json +9 -2
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +59 -0
- package/packages/shared/src/__tests__/config-plugins.test.ts +68 -0
- package/packages/shared/src/__tests__/extension-ui-module-shape.test.ts +265 -0
- package/packages/shared/src/__tests__/no-hardcoded-node-modules-paths.test.ts +176 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +146 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +81 -0
- package/packages/shared/src/__tests__/node-spawn.test.ts +210 -0
- package/packages/shared/src/__tests__/openspec-design-evidence.test.ts +288 -0
- package/packages/shared/src/__tests__/openspec-effective-status-script.test.ts +174 -0
- package/packages/shared/src/__tests__/openspec-poller-design-override.test.ts +225 -0
- package/packages/shared/src/__tests__/openspec-poller-specs-override.test.ts +284 -0
- package/packages/shared/src/__tests__/openspec-specs-evidence.test.ts +144 -0
- package/packages/shared/src/__tests__/platform/is-appimage-self-hit.test.ts +164 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +72 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +113 -0
- package/packages/shared/src/__tests__/plugin-config-update-protocol.test.ts +41 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +5 -1
- package/packages/shared/src/__tests__/resolve-tool-cli.test.ts +105 -0
- package/packages/shared/src/__tests__/spawn-session-attach-proposal.test.ts +47 -0
- package/packages/shared/src/__tests__/state-replay-entry-id.test.ts +69 -0
- package/packages/shared/src/__tests__/tool-registry-strategies-appimage.test.ts +118 -0
- package/packages/shared/src/browser-protocol.ts +110 -4
- package/packages/shared/src/config.ts +45 -0
- package/packages/shared/src/dashboard-plugin/index.ts +11 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +58 -0
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +26 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +92 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +151 -0
- package/packages/shared/src/openspec-activity-detector.ts +18 -22
- package/packages/shared/src/openspec-design-evidence.ts +109 -0
- package/packages/shared/src/openspec-poller.ts +117 -3
- package/packages/shared/src/openspec-specs-evidence.ts +79 -0
- package/packages/shared/src/platform/binary-lookup.ts +96 -1
- package/packages/shared/src/platform/index.ts +1 -0
- package/packages/shared/src/platform/node-spawn.ts +154 -0
- package/packages/shared/src/plugin-bridge-register.ts +139 -0
- package/packages/shared/src/protocol.ts +79 -2
- package/packages/shared/src/recommended-extensions.ts +7 -1
- package/packages/shared/src/rest-api.ts +68 -3
- package/packages/shared/src/state-replay.ts +20 -1
- package/packages/shared/src/tool-registry/definitions.ts +92 -0
- package/packages/shared/src/tool-registry/strategies.ts +17 -3
- package/packages/shared/src/types.ts +160 -0
|
@@ -26,6 +26,7 @@ import { expandPromptTemplateFromDisk } from "./prompt-expander.js";
|
|
|
26
26
|
import { PromptBus } from "./prompt-bus.js";
|
|
27
27
|
import { DashboardDefaultAdapter } from "./dashboard-default-adapter.js";
|
|
28
28
|
import { registerAskUserTool } from "./ask-user-tool.js";
|
|
29
|
+
import { decodeMultiselectAnswer } from "./multiselect-decode.js";
|
|
29
30
|
import { activate as activateProviderRegister, onProviderChanged, reloadProviders } from "./provider-register.js";
|
|
30
31
|
import type { FlowInfo } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
31
32
|
import { startMetricsMonitor, stopMetricsMonitor, collectMetrics } from "./process-metrics.js";
|
|
@@ -35,6 +36,7 @@ import { filterHiddenCommands, extractFirstMessage, getCurrentModelString } from
|
|
|
35
36
|
import { sendStateSync as _sendStateSync, replaySessionEntries as _replaySessionEntries, handleSessionChange as _handleSessionChange } from "./session-sync.js";
|
|
36
37
|
import { sendModelUpdateIfChanged as _sendModelUpdateIfChanged, sendSessionNameIfChanged as _sendSessionNameIfChanged, sendGitInfoIfChanged as _sendGitInfoIfChanged } from "./model-tracker.js";
|
|
37
38
|
import { registerFlowEventListeners, FLOW_EVENT_MAP, SUBAGENT_EVENT_MAP } from "./flow-event-wiring.js";
|
|
39
|
+
import { refreshUiModules, subscribeUiInvalidate, handleUiManagement, type UiModulesBridgeCtx } from "./ui-modules.js";
|
|
38
40
|
|
|
39
41
|
const HEARTBEAT_INTERVAL = 15_000;
|
|
40
42
|
const GIT_POLL_INTERVAL = 30_000;
|
|
@@ -186,6 +188,61 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
186
188
|
let lastThinkingLevel: string | undefined;
|
|
187
189
|
let promptBus: PromptBus | undefined;
|
|
188
190
|
|
|
191
|
+
// ── Per-message entry id tracking (for fix-per-message-fork) ──
|
|
192
|
+
// Pi 0.69+ awaits extension handlers BEFORE sessionManager.appendMessage runs,
|
|
193
|
+
// which means getLeafId() at emit time returns the previous leaf, not the
|
|
194
|
+
// entry id of the message currently being emitted. We solve this by:
|
|
195
|
+
// 1. Wrapping ctx.sessionManager.appendMessage at session_start to stamp
|
|
196
|
+
// the just-generated entry id onto the message object reference.
|
|
197
|
+
// 2. Deferring the message_end enrichment-and-send via setTimeout(0) so
|
|
198
|
+
// the awaited dispatcher unwinds and appendMessage runs in between.
|
|
199
|
+
// 3. Stamping a nonce on message_start/message_end events; emitting an
|
|
200
|
+
// entry_persisted event after appendMessage so the client reducer can
|
|
201
|
+
// back-fill user-message ChatMessage.entryId.
|
|
202
|
+
// See change: fix-per-message-fork.
|
|
203
|
+
const idByMessage = new WeakMap<object, string>();
|
|
204
|
+
const pendingNonces = new WeakMap<object, string>();
|
|
205
|
+
let nonceCounter = 0;
|
|
206
|
+
const nextNonce = (): string => `n-${++nonceCounter}-${Date.now()}`;
|
|
207
|
+
let appendMessageWrapped = false;
|
|
208
|
+
let lastWrappedSm: any = null;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Wrap ctx.sessionManager.appendMessage once per session so that when pi
|
|
212
|
+
* generates an entry id we capture it in the WeakMap and emit
|
|
213
|
+
* entry_persisted to the server.
|
|
214
|
+
*/
|
|
215
|
+
function wrapAppendMessageForCtx(ctx: any): void {
|
|
216
|
+
const sm = ctx?.sessionManager;
|
|
217
|
+
if (!sm || typeof sm.appendMessage !== "function") return;
|
|
218
|
+
// Re-wrap when sessionManager identity changes (session replacement).
|
|
219
|
+
if (sm === lastWrappedSm && appendMessageWrapped) return;
|
|
220
|
+
const original = sm.appendMessage.bind(sm);
|
|
221
|
+
sm.appendMessage = (msg: any, ...rest: any[]) => {
|
|
222
|
+
const result = original(msg, ...rest);
|
|
223
|
+
try {
|
|
224
|
+
if (msg && typeof msg === "object" && typeof msg.id === "string") {
|
|
225
|
+
idByMessage.set(msg as object, msg.id);
|
|
226
|
+
const nonce = pendingNonces.get(msg as object);
|
|
227
|
+
if (nonce && sessionReady && isActive()) {
|
|
228
|
+
const ev = {
|
|
229
|
+
type: "entry_persisted",
|
|
230
|
+
entryId: msg.id,
|
|
231
|
+
nonce,
|
|
232
|
+
};
|
|
233
|
+
connection.send(mapEventToProtocol(sessionId, ev));
|
|
234
|
+
pendingNonces.delete(msg as object);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.error("[dashboard] entry_persisted emit failed:", err);
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
};
|
|
242
|
+
lastWrappedSm = sm;
|
|
243
|
+
appendMessageWrapped = true;
|
|
244
|
+
}
|
|
245
|
+
|
|
189
246
|
/** Wrap a callback so errors log instead of crashing the host pi agent. */
|
|
190
247
|
function safe<T extends (...args: any[]) => any>(fn: T): T {
|
|
191
248
|
return ((...args: any[]) => {
|
|
@@ -208,11 +265,29 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
208
265
|
const config = loadConfig();
|
|
209
266
|
const dashboardUrl = process.env.PI_DASHBOARD_URL ?? `ws://localhost:${config.piPort}`;
|
|
210
267
|
|
|
268
|
+
// Long-lived ctx wrapper for the Extension UI System (Phase 1) — see
|
|
269
|
+
// change: add-extension-ui-modal. `getSessionId` reads the closed-over
|
|
270
|
+
// `sessionId` so the helper always uses the current value (which is
|
|
271
|
+
// mutated when `event.reason ∈ {"new","fork","resume"}` fires).
|
|
272
|
+
const uiModulesBridgeCtx: UiModulesBridgeCtx = {
|
|
273
|
+
pi: pi as any,
|
|
274
|
+
connection: { send: (msg: unknown) => connection.send(msg) },
|
|
275
|
+
getSessionId: () => sessionId,
|
|
276
|
+
};
|
|
277
|
+
|
|
211
278
|
const connection = new ConnectionManager({
|
|
212
279
|
url: dashboardUrl,
|
|
213
280
|
onMessage: safe(async (data: unknown) => {
|
|
214
281
|
if (!isActive()) return; // Stale listener guard
|
|
215
282
|
const msg = data as ServerToExtensionMessage;
|
|
283
|
+
// Extension UI System (Phase 1): browser-originated action / data
|
|
284
|
+
// request. Re-emit on pi.events; the listener either populates
|
|
285
|
+
// data.items synchronously or calls _reply asynchronously.
|
|
286
|
+
// See change: add-extension-ui-modal.
|
|
287
|
+
if ((msg as any).type === "ui_management") {
|
|
288
|
+
handleUiManagement(uiModulesBridgeCtx, msg as any);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
216
291
|
// Legacy extension_ui_response removed — now handled by prompt_response → promptBus.respond()
|
|
217
292
|
// Reload auth credentials when dashboard notifies of changes
|
|
218
293
|
if (msg.type === "credentials_updated") {
|
|
@@ -408,6 +483,11 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
408
483
|
if (getBridgeState().isAgentStreaming) {
|
|
409
484
|
connection.send(mapEventToProtocol(sessionId, { type: "agent_start" }));
|
|
410
485
|
}
|
|
486
|
+
// Extension UI System (Phase 1): re-probe modules after every
|
|
487
|
+
// reconnect so the server-side cache stays accurate. The probe is
|
|
488
|
+
// synchronous and re-runs the listener stack each call.
|
|
489
|
+
// See change: add-extension-ui-modal.
|
|
490
|
+
refreshUiModules(uiModulesBridgeCtx);
|
|
411
491
|
}),
|
|
412
492
|
});
|
|
413
493
|
|
|
@@ -612,30 +692,53 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
612
692
|
}
|
|
613
693
|
}
|
|
614
694
|
|
|
615
|
-
// For message_start
|
|
695
|
+
// For message_start: stamp a nonce on the event so the client reducer
|
|
696
|
+
// can correlate a later entry_persisted back-fill with this bubble.
|
|
697
|
+
// We do NOT attach entryId here — the message has no id yet on pi
|
|
698
|
+
// 0.69+ (persistence is deferred to message_end). See change:
|
|
699
|
+
// fix-per-message-fork.
|
|
616
700
|
if (eventType === "message_start") {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
701
|
+
wrapAppendMessageForCtx(ctx);
|
|
702
|
+
const messageRef = (event as any).message;
|
|
703
|
+
if (messageRef && typeof messageRef === "object") {
|
|
704
|
+
const nonce = nextNonce();
|
|
705
|
+
pendingNonces.set(messageRef as object, nonce);
|
|
706
|
+
const enriched = { ...event, nonce };
|
|
620
707
|
const msg = mapEventToProtocol(sessionId, enriched);
|
|
621
708
|
connection.send(msg);
|
|
622
709
|
return;
|
|
623
710
|
}
|
|
624
711
|
}
|
|
625
712
|
|
|
626
|
-
// For message_end
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
//
|
|
713
|
+
// For message_end: defer the SEND via setTimeout(0). Pi 0.69+ runs
|
|
714
|
+
// sessionManager.appendMessage AFTER the awaited extension dispatcher
|
|
715
|
+
// returns, so a queueMicrotask deferral is no longer enough. By the
|
|
716
|
+
// time the macrotask fires, appendMessage has run, pi has mutated
|
|
717
|
+
// event.message.id in place, and the wrapped appendMessage above has
|
|
718
|
+
// populated idByMessage. We also stamp a nonce so a downstream
|
|
719
|
+
// entry_persisted can correlate (covers user message_end where the
|
|
720
|
+
// earlier message_start nonce is what the reducer is waiting on).
|
|
721
|
+
// See change: fix-per-message-fork.
|
|
630
722
|
if (eventType === "message_end") {
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
723
|
+
wrapAppendMessageForCtx(ctx);
|
|
724
|
+
const messageRef = (event as any).message;
|
|
725
|
+
const nonce = messageRef && typeof messageRef === "object"
|
|
726
|
+
? (pendingNonces.get(messageRef as object) ?? nextNonce())
|
|
727
|
+
: nextNonce();
|
|
728
|
+
if (messageRef && typeof messageRef === "object" && !pendingNonces.has(messageRef as object)) {
|
|
729
|
+
pendingNonces.set(messageRef as object, nonce);
|
|
638
730
|
}
|
|
731
|
+
setTimeout(() => {
|
|
732
|
+
if (!isActive() || !sessionReady) return;
|
|
733
|
+
const entryId =
|
|
734
|
+
(messageRef && typeof messageRef === "object" && typeof messageRef.id === "string" ? messageRef.id : undefined)
|
|
735
|
+
?? (messageRef ? idByMessage.get(messageRef as object) : undefined)
|
|
736
|
+
?? ctx.sessionManager?.getLeafId?.();
|
|
737
|
+
const enriched = { ...event, entryId, nonce };
|
|
738
|
+
const protoMsg = mapEventToProtocol(sessionId, enriched);
|
|
739
|
+
connection.send(protoMsg);
|
|
740
|
+
}, 0);
|
|
741
|
+
return;
|
|
639
742
|
}
|
|
640
743
|
|
|
641
744
|
const msg = mapEventToProtocol(sessionId, event);
|
|
@@ -694,6 +797,15 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
694
797
|
cachedCtx = ctx;
|
|
695
798
|
sessionId = newSessionId;
|
|
696
799
|
|
|
800
|
+
// Wrap sessionManager.appendMessage so that future message_end events can
|
|
801
|
+
// recover the just-generated entry id, even when their setTimeout(0)
|
|
802
|
+
// fires before pi has finished mutating event.message in place. The
|
|
803
|
+
// helper is idempotent and re-wraps on session replacement.
|
|
804
|
+
// See change: fix-per-message-fork.
|
|
805
|
+
appendMessageWrapped = false;
|
|
806
|
+
lastWrappedSm = null;
|
|
807
|
+
wrapAppendMessageForCtx(ctx);
|
|
808
|
+
|
|
697
809
|
// Register ask_user at runtime (not at load time) to avoid static
|
|
698
810
|
// tool-name conflicts with other extensions like pi-flows.
|
|
699
811
|
registerAskUserTool(pi);
|
|
@@ -743,6 +855,16 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
743
855
|
input: ctx.ui.input?.bind(ctx.ui) as ((q: string, placeholder?: string, extra?: any) => Promise<string | undefined>) | undefined,
|
|
744
856
|
confirm: ctx.ui.confirm?.bind(ctx.ui) as ((q: string, msg: string, extra?: any) => Promise<boolean>) | undefined,
|
|
745
857
|
editor: ctx.ui.editor?.bind(ctx.ui) as ((q: string, prefill?: string, extra?: any) => Promise<string | undefined>) | undefined,
|
|
858
|
+
// NOTE: the `custom` field is intentionally NOT captured here. A
|
|
859
|
+
// previous change (fix-multiselect-auto-cancel-on-dashboard) added a
|
|
860
|
+
// TUI multiselect arm that awaited the original ctx.ui.custom binding,
|
|
861
|
+
// but pi 0.70's RPC mode defines that primitive as a no-op (returns
|
|
862
|
+
// undefined synchronously), causing the TUI adapter to auto-cancel the
|
|
863
|
+
// dashboard-rendered dialog within one event-loop tick. The arm has
|
|
864
|
+
// been removed; see change fix-multiselect-tui-arm-self-cancel for full
|
|
865
|
+
// rationale. A repo lint (no-tui-multiselect-arm-regression.test.ts)
|
|
866
|
+
// prevents reintroduction by banning the co-occurrence of two
|
|
867
|
+
// substrings (the captured original binding and the TUI arm match).
|
|
746
868
|
};
|
|
747
869
|
|
|
748
870
|
// Register TUI adapter — presents prompts in the terminal using original
|
|
@@ -771,6 +893,13 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
771
893
|
} else if (prompt.type === "editor" && originals.editor) {
|
|
772
894
|
answer = await originals.editor(prompt.question, prompt.defaultValue || "", { signal: ac.signal });
|
|
773
895
|
} else {
|
|
896
|
+
// NOTE: there is intentionally no `else if` arm for the
|
|
897
|
+
// multiselect prompt type here. See change
|
|
898
|
+
// fix-multiselect-tui-arm-self-cancel — pi 0.70 RPC mode's
|
|
899
|
+
// ctx.ui.custom primitive is a no-op, so any TUI arm that
|
|
900
|
+
// awaits it auto-cancels the dashboard-rendered dialog. The
|
|
901
|
+
// bus-routed ctx.ui.multiselect patch below + the
|
|
902
|
+
// DashboardDefaultAdapter handle multiselect end-to-end.
|
|
774
903
|
return;
|
|
775
904
|
}
|
|
776
905
|
|
|
@@ -821,22 +950,66 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
821
950
|
// now route through the bus, which distributes to all registered adapters.
|
|
822
951
|
{
|
|
823
952
|
const bus = promptBus;
|
|
953
|
+
// Build a `metadata` envelope for bus.request that includes both
|
|
954
|
+
// `message` (existing) and `toolCallId` (new — added by change
|
|
955
|
+
// `fix-interactive-ui-reorder` so the client reducer can pair the
|
|
956
|
+
// resulting interactiveUi row with its parent toolResult row).
|
|
957
|
+
// Free-floating callers (slash commands, architect prompts) omit
|
|
958
|
+
// `opts.toolCallId` and the metadata field stays undefined.
|
|
959
|
+
const buildMeta = (
|
|
960
|
+
opts: any,
|
|
961
|
+
explicitMessage?: string,
|
|
962
|
+
): Record<string, unknown> | undefined => {
|
|
963
|
+
const message = explicitMessage ?? opts?.message;
|
|
964
|
+
const toolCallId = opts?.toolCallId;
|
|
965
|
+
if (!message && !toolCallId) return undefined;
|
|
966
|
+
const meta: Record<string, unknown> = {};
|
|
967
|
+
if (message) meta.message = message;
|
|
968
|
+
if (toolCallId) meta.toolCallId = toolCallId;
|
|
969
|
+
return meta;
|
|
970
|
+
};
|
|
971
|
+
|
|
824
972
|
(ctx.ui as any).select = (title: string, options: string[], opts?: any) =>
|
|
825
|
-
bus.request({ pipeline: "command", type: "select", question: title, options, metadata: opts
|
|
973
|
+
bus.request({ pipeline: "command", type: "select", question: title, options, metadata: buildMeta(opts) })
|
|
826
974
|
.then(r => r.cancelled ? undefined : r.answer);
|
|
827
975
|
|
|
828
976
|
(ctx.ui as any).input = (title: string, placeholder?: string, opts?: any) =>
|
|
829
|
-
bus.request({ pipeline: "command", type: "input", question: title, defaultValue: placeholder, metadata: opts
|
|
977
|
+
bus.request({ pipeline: "command", type: "input", question: title, defaultValue: placeholder, metadata: buildMeta(opts) })
|
|
830
978
|
.then(r => r.cancelled ? undefined : r.answer);
|
|
831
979
|
|
|
832
980
|
(ctx.ui as any).confirm = (title: string, message?: string, opts?: any) =>
|
|
833
|
-
bus.request({ pipeline: "command", type: "confirm", question: title, metadata: (
|
|
981
|
+
bus.request({ pipeline: "command", type: "confirm", question: title, metadata: buildMeta(opts, message) })
|
|
834
982
|
.then(r => !r.cancelled && r.answer === "true");
|
|
835
983
|
|
|
836
984
|
(ctx.ui as any).editor = (title: string, prefill?: string, opts?: any) =>
|
|
837
|
-
bus.request({ pipeline: "command", type: "editor", question: title, defaultValue: prefill, metadata: opts
|
|
985
|
+
bus.request({ pipeline: "command", type: "editor", question: title, defaultValue: prefill, metadata: buildMeta(opts) })
|
|
838
986
|
.then(r => r.cancelled ? undefined : r.answer);
|
|
839
987
|
|
|
988
|
+
// ── Multiselect ──────────────────────────────────────────────
|
|
989
|
+
// ctx.ui.multiselect is NOT a built-in pi method — we attach it here
|
|
990
|
+
// so that polyfillMultiselect (and any other consumer) routes through
|
|
991
|
+
// PromptBus. The dashboard adapter renders a real browser dialog via
|
|
992
|
+
// MultiselectRenderer; there is intentionally no TUI adapter arm for
|
|
993
|
+
// multiselect (pi 0.70 RPC mode's ctx.ui.custom is a no-op, so any TUI
|
|
994
|
+
// arm would auto-cancel the dashboard render in <1s). See changes
|
|
995
|
+
// fix-multiselect-auto-cancel-on-dashboard (initial bus routing) and
|
|
996
|
+
// fix-multiselect-tui-arm-self-cancel (TUI arm removal).
|
|
997
|
+
if (typeof (ctx.ui as any).multiselect === "function") {
|
|
998
|
+
// Defensive: future upstream pi may add a built-in multiselect.
|
|
999
|
+
// Override is intentional — the bus-routed version is what
|
|
1000
|
+
// participates in PromptBus first-response-wins semantics.
|
|
1001
|
+
// eslint-disable-next-line no-console
|
|
1002
|
+
console.warn("[bridge] ctx.ui.multiselect already exists — overriding for PromptBus routing");
|
|
1003
|
+
}
|
|
1004
|
+
(ctx.ui as any).multiselect = (title: string, options: string[], opts?: any) =>
|
|
1005
|
+
bus.request({
|
|
1006
|
+
pipeline: "command",
|
|
1007
|
+
type: "multiselect",
|
|
1008
|
+
question: title,
|
|
1009
|
+
options,
|
|
1010
|
+
metadata: opts?.message ? { message: opts.message } : undefined,
|
|
1011
|
+
}).then(decodeMultiselectAnswer);
|
|
1012
|
+
|
|
840
1013
|
// Notify is fire-and-forget: call original + forward to dashboard
|
|
841
1014
|
(ctx.ui as any).notify = (message: string, level?: string) => {
|
|
842
1015
|
originalNotify?.(message, level);
|
|
@@ -1073,6 +1246,13 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
1073
1246
|
|
|
1074
1247
|
// Register flow event listeners (pi-flows emits these via pi.events)
|
|
1075
1248
|
registerFlowEventListeners(syncBc(), () => sessionReady, getFlowsList);
|
|
1249
|
+
|
|
1250
|
+
// Extension UI System (Phase 1): subscribe to invalidate once per
|
|
1251
|
+
// session, then run the discovery probe. The probe is synchronous
|
|
1252
|
+
// and re-runs on every reconnect (see `onReconnect` callback above).
|
|
1253
|
+
// See change: add-extension-ui-modal.
|
|
1254
|
+
subscribeUiInvalidate(uiModulesBridgeCtx);
|
|
1255
|
+
refreshUiModules(uiModulesBridgeCtx);
|
|
1076
1256
|
}));
|
|
1077
1257
|
|
|
1078
1258
|
// Shared handler for session changes (new/fork/resume)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* decodeMultiselectAnswer — pure helper that turns a `PromptResponse`
|
|
3
|
+
* (from PromptBus) into the `string[] | undefined` shape expected by
|
|
4
|
+
* `polyfillMultiselect` and other multiselect callers.
|
|
5
|
+
*
|
|
6
|
+
* Contract:
|
|
7
|
+
* • cancelled: true → undefined
|
|
8
|
+
* • cancelled: false, answer: undefined / null / "" → [] (empty selection
|
|
9
|
+
* is a real answer,
|
|
10
|
+
* distinct from
|
|
11
|
+
* cancellation)
|
|
12
|
+
* • cancelled: false, answer: '["a","b"]' → ["a","b"]
|
|
13
|
+
* • cancelled: false, answer: <unparseable> → [] (graceful
|
|
14
|
+
* degradation,
|
|
15
|
+
* never throw)
|
|
16
|
+
*
|
|
17
|
+
* Kept separate from `bridge.ts` so unit tests can exercise it without
|
|
18
|
+
* instantiating a live PromptBus or session context.
|
|
19
|
+
*
|
|
20
|
+
* See change: fix-multiselect-auto-cancel-on-dashboard.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export interface DecodableResponse {
|
|
24
|
+
cancelled?: boolean;
|
|
25
|
+
answer?: string | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function decodeMultiselectAnswer(
|
|
29
|
+
response: DecodableResponse,
|
|
30
|
+
): string[] | undefined {
|
|
31
|
+
if (response.cancelled) return undefined;
|
|
32
|
+
const answer = response.answer;
|
|
33
|
+
if (answer == null || answer === "") return [];
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(answer);
|
|
36
|
+
return Array.isArray(parsed) ? (parsed as string[]) : [];
|
|
37
|
+
} catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MultiSelectList — a TUI multi-select component implementing pi-tui's
|
|
3
|
+
* `Component` interface. Used by `polyfillMultiselect` to emulate the
|
|
4
|
+
* `ctx.ui.multiselect(...)` call that `pi-coding-agent`'s `ExtensionUIContext`
|
|
5
|
+
* does not expose natively.
|
|
6
|
+
*
|
|
7
|
+
* Keyboard contract (intentional — no "select all" binding in TUI):
|
|
8
|
+
* ↑ / k move cursor up
|
|
9
|
+
* ↓ / j move cursor down
|
|
10
|
+
* space toggle the checked state of the current item
|
|
11
|
+
* enter confirm → onConfirm(selected[])
|
|
12
|
+
* esc cancel → onCancel()
|
|
13
|
+
*
|
|
14
|
+
* The selected array preserves the original option order, not toggle order.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface Item {
|
|
18
|
+
value: string;
|
|
19
|
+
label: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
checked: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Minimal shape of pi-tui's `Component` interface — we avoid importing from
|
|
26
|
+
* `@mariozechner/pi-tui` directly so this module stays compile-friendly when
|
|
27
|
+
* that peer dep isn't present (e.g. in unit tests running via vitest without
|
|
28
|
+
* the full pi runtime).
|
|
29
|
+
*/
|
|
30
|
+
export interface ComponentLike {
|
|
31
|
+
render(width: number): string[];
|
|
32
|
+
handleInput?(data: string): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const CURSOR = "▸ ";
|
|
36
|
+
const NO_CURSOR = " ";
|
|
37
|
+
const CHECKED = "[x]";
|
|
38
|
+
const UNCHECKED = "[ ]";
|
|
39
|
+
const FOOTER_HINT = "space toggle · enter confirm · esc cancel";
|
|
40
|
+
|
|
41
|
+
const MAX_VISIBLE = 10;
|
|
42
|
+
|
|
43
|
+
function truncate(text: string, maxWidth: number): string {
|
|
44
|
+
if (maxWidth <= 1) return "";
|
|
45
|
+
if (text.length <= maxWidth) return text;
|
|
46
|
+
if (maxWidth <= 1) return "…";
|
|
47
|
+
return text.slice(0, Math.max(0, maxWidth - 1)) + "…";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class MultiSelectList implements ComponentLike {
|
|
51
|
+
private items: Item[];
|
|
52
|
+
private cursor = 0;
|
|
53
|
+
private scrollOffset = 0;
|
|
54
|
+
|
|
55
|
+
onConfirm?: (selectedValues: string[]) => void;
|
|
56
|
+
onCancel?: () => void;
|
|
57
|
+
|
|
58
|
+
constructor(
|
|
59
|
+
private title: string,
|
|
60
|
+
options: string[],
|
|
61
|
+
private message?: string,
|
|
62
|
+
) {
|
|
63
|
+
this.items = options.map((opt) => ({
|
|
64
|
+
value: opt,
|
|
65
|
+
label: opt,
|
|
66
|
+
checked: false,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Expose current state for testing / adapters. */
|
|
71
|
+
getItems(): readonly Item[] {
|
|
72
|
+
return this.items;
|
|
73
|
+
}
|
|
74
|
+
getCursor(): number {
|
|
75
|
+
return this.cursor;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Return values of currently checked items in original option order. */
|
|
79
|
+
private selectedValues(): string[] {
|
|
80
|
+
return this.items.filter((it) => it.checked).map((it) => it.value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
render(width: number): string[] {
|
|
84
|
+
const lines: string[] = [];
|
|
85
|
+
if (this.title) lines.push(truncate(this.title, width));
|
|
86
|
+
if (this.message) lines.push(truncate(this.message, width));
|
|
87
|
+
if (lines.length > 0) lines.push("");
|
|
88
|
+
|
|
89
|
+
// Scroll window around cursor.
|
|
90
|
+
const visible = Math.min(MAX_VISIBLE, this.items.length);
|
|
91
|
+
if (this.cursor < this.scrollOffset) {
|
|
92
|
+
this.scrollOffset = this.cursor;
|
|
93
|
+
} else if (this.cursor >= this.scrollOffset + visible) {
|
|
94
|
+
this.scrollOffset = this.cursor - visible + 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < visible; i++) {
|
|
98
|
+
const idx = this.scrollOffset + i;
|
|
99
|
+
const item = this.items[idx];
|
|
100
|
+
if (!item) break;
|
|
101
|
+
const marker = idx === this.cursor ? CURSOR : NO_CURSOR;
|
|
102
|
+
const box = item.checked ? CHECKED : UNCHECKED;
|
|
103
|
+
let line = `${marker}${box} ${item.label}`;
|
|
104
|
+
if (item.description) line += ` — ${item.description}`;
|
|
105
|
+
lines.push(truncate(line, width));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (this.items.length > visible) {
|
|
109
|
+
lines.push(` (${this.cursor + 1}/${this.items.length})`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
lines.push("");
|
|
113
|
+
lines.push(truncate(FOOTER_HINT, width));
|
|
114
|
+
return lines;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
handleInput(data: string): void {
|
|
118
|
+
// Escape
|
|
119
|
+
if (data === "\u001b" || data === "\x1b") {
|
|
120
|
+
this.onCancel?.();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Enter (CR or LF)
|
|
124
|
+
if (data === "\r" || data === "\n") {
|
|
125
|
+
this.onConfirm?.(this.selectedValues());
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Space — toggle current
|
|
129
|
+
if (data === " ") {
|
|
130
|
+
const item = this.items[this.cursor];
|
|
131
|
+
if (item) item.checked = !item.checked;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Arrow up / k
|
|
135
|
+
if (data === "\u001b[A" || data === "k") {
|
|
136
|
+
if (this.cursor > 0) this.cursor--;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Arrow down / j
|
|
140
|
+
if (data === "\u001b[B" || data === "j") {
|
|
141
|
+
if (this.cursor < this.items.length - 1) this.cursor++;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Everything else (including "a", "A", bulk-toggle attempts) is a no-op.
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polyfill for `ctx.ui.multiselect(...)` — a method the dashboard bridge's
|
|
3
|
+
* `ask_user` tool advertises but which `pi-coding-agent`'s
|
|
4
|
+
* `ExtensionUIContext` does not expose. Without this, any TUI dispatch of
|
|
5
|
+
* `method: "multiselect"` crashes with `"ctx.ui.multiselect is not a function"`.
|
|
6
|
+
*
|
|
7
|
+
* Fallback chain (introduced by change fix-multiselect-auto-cancel-on-dashboard):
|
|
8
|
+
*
|
|
9
|
+
* 1. PRIMARY — bridge-patched `ctx.ui.multiselect` (PromptBus path).
|
|
10
|
+
* The bridge attaches this method on session_start so the dashboard
|
|
11
|
+
* browser renders a real `MultiselectRenderer` dialog and the TUI
|
|
12
|
+
* adapter renders a `MultiSelectList` overlay in the terminal.
|
|
13
|
+
*
|
|
14
|
+
* 2. FALLBACK — legacy `ctx.ui.custom` + `MultiSelectList` overlay.
|
|
15
|
+
* Reached when (a) running against an older pi without the bridge
|
|
16
|
+
* patch, (b) running outside the bridge entirely, or (c) the bridge
|
|
17
|
+
* patch was removed for some reason. TUI-only — does NOT render a
|
|
18
|
+
* browser dialog in dashboard / RPC mode (which is exactly the bug
|
|
19
|
+
* that motivated the primary path).
|
|
20
|
+
*
|
|
21
|
+
* The result contract is unchanged in either branch:
|
|
22
|
+
* - resolves to `string[]` when the user confirms a selection
|
|
23
|
+
* (possibly empty if nothing is checked)
|
|
24
|
+
* - resolves to `undefined` when the user cancels (Escape / Cancel)
|
|
25
|
+
*
|
|
26
|
+
* See change: fix-multiselect-auto-cancel-on-dashboard.
|
|
27
|
+
*/
|
|
28
|
+
import { MultiSelectList } from "./multiselect-list.js";
|
|
29
|
+
|
|
30
|
+
// Intentionally loose: `ctx` shape varies slightly across pi versions; the
|
|
31
|
+
// polyfill only needs `ctx.ui.multiselect` (primary) or `ctx.ui.custom` (fallback).
|
|
32
|
+
export interface PolyfillCtx {
|
|
33
|
+
ui: {
|
|
34
|
+
multiselect?: (
|
|
35
|
+
title: string,
|
|
36
|
+
options: string[],
|
|
37
|
+
opts?: { message?: string },
|
|
38
|
+
) => Promise<string[] | undefined>;
|
|
39
|
+
custom<T>(
|
|
40
|
+
factory: (tui: unknown, theme: unknown, keybindings: unknown, done: (result: T) => void) => unknown,
|
|
41
|
+
options?: unknown,
|
|
42
|
+
): Promise<T>;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function polyfillMultiselect(
|
|
47
|
+
ctx: PolyfillCtx,
|
|
48
|
+
title: string,
|
|
49
|
+
options: string[],
|
|
50
|
+
opts?: { message?: string },
|
|
51
|
+
): Promise<string[] | undefined> {
|
|
52
|
+
// Primary path: delegate to the bridge-patched ctx.ui.multiselect (which
|
|
53
|
+
// routes through PromptBus → DashboardDefaultAdapter → client
|
|
54
|
+
// MultiselectRenderer). This is the only working path on pi 0.70 RPC mode
|
|
55
|
+
// (dashboard headless sessions).
|
|
56
|
+
const ui = ctx.ui as PolyfillCtx["ui"] & { multiselect?: Function };
|
|
57
|
+
if (typeof ui.multiselect === "function") {
|
|
58
|
+
return Promise.resolve(ui.multiselect(title, options, opts));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Legacy fallback: TUI overlay via ctx.ui.custom. Used when the bridge
|
|
62
|
+
// patch is absent (older pi / non-bridge embedding) OR a future pi version
|
|
63
|
+
// restores ctx.ui.custom in RPC mode. NOTE: in pi 0.70 RPC mode
|
|
64
|
+
// ctx.ui.custom is a no-op that resolves to undefined synchronously, so
|
|
65
|
+
// this branch returns undefined immediately on dashboard headless
|
|
66
|
+
// sessions — the primary path above is the only effective route there.
|
|
67
|
+
return ctx.ui.custom<string[] | undefined>((_tui, _theme, _keybindings, done) => {
|
|
68
|
+
const list = new MultiSelectList(title, options, opts?.message);
|
|
69
|
+
list.onConfirm = (selected) => done(selected);
|
|
70
|
+
list.onCancel = () => done(undefined);
|
|
71
|
+
return list as unknown;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -11,6 +11,7 @@ import { createRequire } from "node:module";
|
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import type { DashboardConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
13
13
|
import { resolveJitiImport } from "@blackbelt-technology/pi-dashboard-shared/resolve-jiti.js";
|
|
14
|
+
import { toFileUrl, shouldUrlWrapEntry } from "@blackbelt-technology/pi-dashboard-shared/platform/node-spawn.js";
|
|
14
15
|
import { isDashboardRunning } from "@blackbelt-technology/pi-dashboard-shared/server-identity.js";
|
|
15
16
|
|
|
16
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -77,11 +78,22 @@ export async function launchServer(config: DashboardConfig): Promise<LaunchResul
|
|
|
77
78
|
);
|
|
78
79
|
} catch { /* if we can't open the log, spawn still works */ }
|
|
79
80
|
|
|
80
|
-
// Spawn server via the detached-spawn primitive.
|
|
81
|
-
//
|
|
81
|
+
// Spawn server via the detached-spawn primitive. The loader is always
|
|
82
|
+
// URL-wrapped (Node needs file:// for --import on Windows drive letters).
|
|
83
|
+
// The entry is URL-wrapped only on Windows + non-tsx loader (Node parses
|
|
84
|
+
// drive letters as URL schemes in argv); on POSIX the entry MUST be raw
|
|
85
|
+
// because jiti's resolver misbehaves on file:// URL entries. See
|
|
86
|
+
// openspec/changes/archive/2026-04-24-fix-windows-entry-script-url.
|
|
87
|
+
const loader = resolveJitiImport();
|
|
88
|
+
const wrapEntry = shouldUrlWrapEntry(loader);
|
|
89
|
+
// entry is gated by shouldUrlWrapEntry(loader): returns true only on
|
|
90
|
+
// Windows + non-tsx (where URL wrap is required); false on POSIX
|
|
91
|
+
// where jiti needs the raw path (file:// URL entries trigger jiti's
|
|
92
|
+
// `<cwd>/file:/...` misresolution bug).
|
|
93
|
+
const entry = wrapEntry ? toFileUrl(cliPath) : cliPath;
|
|
82
94
|
const r = await spawnDetached({
|
|
83
95
|
cmd: process.execPath,
|
|
84
|
-
args: ["--import",
|
|
96
|
+
args: ["--import", loader, entry, ...args], // ban:raw-node-import-ok: entry gated by shouldUrlWrapEntry
|
|
85
97
|
env: { ...process.env },
|
|
86
98
|
logFd,
|
|
87
99
|
});
|