@blackbelt-technology/pi-agent-dashboard 0.2.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +64 -8
- package/README.md +308 -101
- package/docs/architecture.md +515 -16
- package/package.json +14 -7
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +300 -3
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +100 -0
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
- package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
- package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
- package/packages/extension/src/ask-user-tool.ts +289 -20
- package/packages/extension/src/bridge.ts +107 -6
- package/packages/extension/src/command-handler.ts +34 -39
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/pi-env.d.ts +1 -0
- package/packages/extension/src/process-scanner.ts +72 -38
- package/packages/extension/src/prompt-expander.ts +25 -4
- package/packages/extension/src/provider-register.ts +304 -16
- package/packages/extension/src/server-auto-start.ts +27 -1
- package/packages/extension/src/server-launcher.ts +71 -27
- package/packages/server/package.json +17 -2
- package/packages/server/src/__tests__/auto-attach.test.ts +10 -1
- package/packages/server/src/__tests__/auto-shutdown.test.ts +8 -2
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
- package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +246 -10
- package/packages/server/src/__tests__/browser-gateway-handler-errors.test.ts +129 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
- package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
- package/packages/server/src/__tests__/config-api.test.ts +68 -0
- package/packages/server/src/__tests__/cors.test.ts +34 -2
- package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
- package/packages/server/src/__tests__/directory-service.test.ts +234 -8
- package/packages/server/src/__tests__/editor-manager-pid-registry.test.ts +168 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +33 -0
- package/packages/server/src/__tests__/editor-pid-registry.test.ts +191 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +29 -15
- package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
- package/packages/server/src/__tests__/extension-register.test.ts +3 -1
- package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
- package/packages/server/src/__tests__/fix-pty-permissions.test.ts +59 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/git-operations.test.ts +9 -7
- package/packages/server/src/__tests__/health-endpoint.test.ts +11 -13
- package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
- package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
- package/packages/server/src/__tests__/home-lock.test.ts +308 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
- package/packages/server/src/__tests__/node-guard.test.ts +85 -0
- package/packages/server/src/__tests__/openspec-tasks-parser.test.ts +178 -0
- package/packages/server/src/__tests__/openspec-tasks-routes.test.ts +180 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +126 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-core-checker.test.ts +195 -0
- package/packages/server/src/__tests__/pi-core-routes.test.ts +184 -0
- package/packages/server/src/__tests__/pi-core-updater.test.ts +214 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
- package/packages/server/src/__tests__/process-manager.test.ts +45 -18
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +13 -3
- package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
- package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +389 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
- package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
- package/packages/server/src/__tests__/session-file-dedup.test.ts +10 -10
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +8 -2
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +3 -1
- package/packages/server/src/__tests__/smoke-integration.test.ts +10 -10
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/test-server-canary.test.ts +31 -0
- package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
- package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
- package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
- package/packages/server/src/__tests__/tunnel.test.ts +103 -6
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +10 -2
- package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
- package/packages/server/src/bootstrap-queue.ts +130 -0
- package/packages/server/src/bootstrap-state.ts +131 -0
- package/packages/server/src/browse.ts +108 -9
- package/packages/server/src/browser-gateway.ts +16 -3
- package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
- package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
- package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
- package/packages/server/src/cli.ts +256 -32
- package/packages/server/src/config-api.ts +16 -0
- package/packages/server/src/directory-service.ts +270 -39
- package/packages/server/src/editor-detection.ts +12 -9
- package/packages/server/src/editor-manager.ts +39 -5
- package/packages/server/src/editor-pid-registry.ts +199 -0
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/fix-pty-permissions.ts +44 -0
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +16 -20
- package/packages/server/src/home-lock-release.ts +72 -0
- package/packages/server/src/home-lock.ts +389 -0
- package/packages/server/src/node-guard.ts +52 -0
- package/packages/server/src/npm-search-proxy.ts +71 -0
- package/packages/server/src/openspec-tasks.ts +158 -0
- package/packages/server/src/package-manager-wrapper.ts +225 -34
- package/packages/server/src/pi-core-checker.ts +290 -0
- package/packages/server/src/pi-core-updater.ts +172 -0
- package/packages/server/src/pi-gateway.ts +7 -0
- package/packages/server/src/pi-resource-scanner.ts +5 -8
- package/packages/server/src/pi-version-skew.ts +196 -0
- package/packages/server/src/preferences-store.ts +17 -3
- package/packages/server/src/process-manager.ts +403 -222
- package/packages/server/src/provider-probe.ts +234 -0
- package/packages/server/src/restart-helper.ts +130 -0
- package/packages/server/src/routes/bootstrap-routes.ts +88 -0
- package/packages/server/src/routes/file-routes.ts +30 -3
- package/packages/server/src/routes/openspec-routes.ts +107 -1
- package/packages/server/src/routes/pi-core-routes.ts +140 -0
- package/packages/server/src/routes/provider-auth-routes.ts +12 -10
- package/packages/server/src/routes/provider-routes.ts +55 -2
- package/packages/server/src/routes/recommended-routes.ts +225 -0
- package/packages/server/src/routes/system-routes.ts +30 -34
- package/packages/server/src/routes/tool-routes.ts +153 -0
- package/packages/server/src/server-pid.ts +5 -9
- package/packages/server/src/server.ts +363 -26
- package/packages/server/src/session-api.ts +77 -8
- package/packages/server/src/session-bootstrap.ts +17 -3
- package/packages/server/src/session-diff.ts +21 -21
- package/packages/server/src/terminal-manager.ts +65 -20
- package/packages/server/src/test-env-guard.ts +26 -0
- package/packages/server/src/test-support/test-server.ts +63 -0
- package/packages/server/src/tunnel.ts +172 -34
- package/packages/shared/package.json +10 -3
- package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
- package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
- package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
- package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
- package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
- package/packages/shared/src/__tests__/config.test.ts +59 -3
- package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
- package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
- package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
- package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
- package/packages/shared/src/__tests__/openspec-poller.test.ts +44 -0
- package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
- package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
- package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
- package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
- package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
- package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
- package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
- package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
- package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
- package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
- package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +156 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
- package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/source-matching.test.ts +143 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
- package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
- package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
- package/packages/shared/src/bootstrap-install.ts +212 -0
- package/packages/shared/src/bridge-register.ts +87 -20
- package/packages/shared/src/browser-protocol.ts +93 -1
- package/packages/shared/src/config.ts +87 -15
- package/packages/shared/src/managed-paths.ts +31 -4
- package/packages/shared/src/openspec-poller.ts +71 -49
- package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
- package/packages/shared/src/platform/commands.ts +100 -0
- package/packages/shared/src/platform/detached-spawn.ts +305 -0
- package/packages/shared/src/platform/exec.ts +220 -0
- package/packages/shared/src/platform/git.ts +155 -0
- package/packages/shared/src/platform/index.ts +15 -0
- package/packages/shared/src/platform/npm.ts +162 -0
- package/packages/shared/src/platform/openspec.ts +91 -0
- package/packages/shared/src/platform/paths.ts +276 -0
- package/packages/shared/src/platform/process-identify.ts +126 -0
- package/packages/shared/src/platform/process-scan.ts +94 -0
- package/packages/shared/src/platform/process.ts +168 -0
- package/packages/shared/src/platform/runner.ts +369 -0
- package/packages/shared/src/platform/shell.ts +44 -0
- package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
- package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
- package/packages/shared/src/recommended-extensions.ts +196 -0
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +97 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/source-matching.ts +126 -0
- package/packages/shared/src/test-support/setup-home.ts +74 -0
- package/packages/shared/src/tool-registry/definitions.ts +342 -0
- package/packages/shared/src/tool-registry/index.ts +56 -0
- package/packages/shared/src/tool-registry/overrides.ts +118 -0
- package/packages/shared/src/tool-registry/registry.ts +262 -0
- package/packages/shared/src/tool-registry/strategies.ts +198 -0
- package/packages/shared/src/tool-registry/types.ts +180 -0
- package/packages/shared/src/types.ts +7 -0
|
@@ -7,7 +7,114 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
import { Type } from "@sinclair/typebox";
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
12
|
+
// Single-question schema arms (reused inside the batch arm's questions array)
|
|
13
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const ConfirmSchema = Type.Object({
|
|
16
|
+
method: Type.Literal("confirm", { description: "Yes/no question" }),
|
|
17
|
+
title: Type.String({ description: "The question to confirm" }),
|
|
18
|
+
message: Type.Optional(Type.String({ description: "Additional context or detailed question body" })),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const SelectSchema = Type.Object({
|
|
22
|
+
method: Type.Literal("select", { description: "Pick one option from a list" }),
|
|
23
|
+
title: Type.String({ description: "Short title for the question" }),
|
|
24
|
+
options: Type.Array(Type.String(), {
|
|
25
|
+
minItems: 2,
|
|
26
|
+
description: "Options the user chooses between (at least 2; use 'confirm' for yes/no)",
|
|
27
|
+
}),
|
|
28
|
+
message: Type.Optional(Type.String({ description: "Additional context" })),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const MultiselectSchema = Type.Object({
|
|
32
|
+
method: Type.Literal("multiselect", { description: "Pick multiple options from a list" }),
|
|
33
|
+
title: Type.String({ description: "Short title for the question" }),
|
|
34
|
+
options: Type.Array(Type.String(), {
|
|
35
|
+
minItems: 1,
|
|
36
|
+
description: "Options the user can multi-select",
|
|
37
|
+
}),
|
|
38
|
+
message: Type.Optional(Type.String({ description: "Additional context" })),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const InputSchema = Type.Object({
|
|
42
|
+
method: Type.Literal("input", { description: "Free-text input" }),
|
|
43
|
+
title: Type.String({ description: "Short title for the question" }),
|
|
44
|
+
placeholder: Type.Optional(Type.String({ description: "Placeholder text for the input field" })),
|
|
45
|
+
message: Type.Optional(Type.String({ description: "Additional context" })),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Sub-question union deliberately omits the batch arm (no nesting).
|
|
49
|
+
const SubQuestionSchema = Type.Union([ConfirmSchema, SelectSchema, MultiselectSchema, InputSchema], {
|
|
50
|
+
description: "A single question inside a batch. Must not itself be a batch.",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const BatchSchema = Type.Object({
|
|
54
|
+
method: Type.Literal("batch", {
|
|
55
|
+
description: "Ask multiple related questions in one call; answers are returned as an ordered array.",
|
|
56
|
+
}),
|
|
57
|
+
title: Type.String({ description: "Header shown above the sequence of dialogs" }),
|
|
58
|
+
questions: Type.Array(SubQuestionSchema, {
|
|
59
|
+
minItems: 1,
|
|
60
|
+
description: "One or more sub-questions (confirm/select/multiselect/input). Cannot nest batch.",
|
|
61
|
+
}),
|
|
62
|
+
message: Type.Optional(Type.String({ description: "Additional context for the whole batch" })),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
66
|
+
// Argument rescue helpers
|
|
67
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
type NormalizationWarning = string;
|
|
70
|
+
|
|
71
|
+
function normalizeSubQuestion(
|
|
72
|
+
sq: unknown,
|
|
73
|
+
warnings: NormalizationWarning[],
|
|
74
|
+
): Record<string, unknown> {
|
|
75
|
+
if (!sq || typeof sq !== "object" || Array.isArray(sq)) return sq as any;
|
|
76
|
+
let obj = { ...(sq as Record<string, unknown>) };
|
|
77
|
+
|
|
78
|
+
// Flatten `input_type: {method, options, ...}` wrapper if present.
|
|
79
|
+
if (obj.input_type && typeof obj.input_type === "object" && !Array.isArray(obj.input_type)) {
|
|
80
|
+
const inner = obj.input_type as Record<string, unknown>;
|
|
81
|
+
const { input_type: _drop, ...rest } = obj;
|
|
82
|
+
obj = { ...inner, ...rest };
|
|
83
|
+
delete (obj as Record<string, unknown>).input_type;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Rename `question` / `header` → `title` (only if title missing).
|
|
87
|
+
if (obj.title === undefined) {
|
|
88
|
+
if (typeof obj.question === "string") obj.title = obj.question;
|
|
89
|
+
else if (typeof obj.header === "string") obj.title = obj.header;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Parse stringified options.
|
|
93
|
+
if (typeof obj.options === "string") {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(obj.options);
|
|
96
|
+
if (Array.isArray(parsed)) obj.options = parsed;
|
|
97
|
+
} catch {
|
|
98
|
+
/* leave as-is */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Convert options: [{label, value}] → [label, ...] with a warning.
|
|
103
|
+
if (Array.isArray(obj.options) && obj.options.length > 0 && obj.options.every(
|
|
104
|
+
(o) => o && typeof o === "object" && !Array.isArray(o) && typeof (o as any).label === "string",
|
|
105
|
+
)) {
|
|
106
|
+
obj.options = (obj.options as Array<Record<string, unknown>>).map((o) => o.label as string);
|
|
107
|
+
warnings.push(
|
|
108
|
+
"ask_user: options with {label, value} pairs are not supported — only labels were used. Send options as string[].",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return obj;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
116
|
+
// Tool registration
|
|
117
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
11
118
|
|
|
12
119
|
export function registerAskUserTool(pi: ExtensionAPI): void {
|
|
13
120
|
pi.registerTool({
|
|
@@ -16,49 +123,211 @@ export function registerAskUserTool(pi: ExtensionAPI): void {
|
|
|
16
123
|
description:
|
|
17
124
|
"Ask the user a question interactively. Use this when you need clarification, confirmation, or a choice from the user before proceeding.",
|
|
18
125
|
promptSnippet:
|
|
19
|
-
"Ask the user interactive questions (confirm, select, multiselect, or
|
|
126
|
+
"Ask the user interactive questions (confirm, select, multiselect, input, or batch — multiple related questions at once)",
|
|
20
127
|
promptGuidelines: [
|
|
21
128
|
"When you need to ask the user a question, ALWAYS use the ask_user tool instead of writing the question as plain text.",
|
|
22
129
|
"Use method 'confirm' for yes/no questions, 'select' when offering specific choices, 'multiselect' when the user should pick multiple items from a list, and 'input' for open-ended questions.",
|
|
130
|
+
"Use method 'batch' with a `questions` array to ask multiple related questions in one call (e.g. project setup: name + language + init git). Prefer single-method calls for standalone questions.",
|
|
131
|
+
"Do not nest batches. Send `options` as a plain string[] — not [{label, value}].",
|
|
23
132
|
"This applies to all workflows including OpenSpec, planning, and any situation where you need user input before proceeding.",
|
|
24
133
|
],
|
|
25
|
-
parameters: Type.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}),
|
|
30
|
-
title: Type.Optional(Type.String({ description: "Short title for the question (optional, falls back to message)" })),
|
|
31
|
-
message: Type.Optional(Type.String({ description: "Additional context or detailed question body (all methods)" })),
|
|
32
|
-
options: Type.Optional(
|
|
33
|
-
Type.Array(Type.String(), { description: "Options to choose from (for select)" }),
|
|
34
|
-
),
|
|
35
|
-
placeholder: Type.Optional(Type.String({ description: "Placeholder text (for input)" })),
|
|
36
|
-
}),
|
|
134
|
+
parameters: Type.Union(
|
|
135
|
+
[ConfirmSchema, SelectSchema, MultiselectSchema, InputSchema, BatchSchema],
|
|
136
|
+
{ description: "Parameters for ask_user, discriminated by method." },
|
|
137
|
+
),
|
|
37
138
|
prepareArguments(args: unknown) {
|
|
38
|
-
|
|
39
|
-
|
|
139
|
+
let obj = (args && typeof args === "object" ? { ...(args as Record<string, unknown>) } : {}) as Record<string, unknown>;
|
|
140
|
+
|
|
141
|
+
// 1. LLMs sometimes wrap everything under `params` (stringified or object).
|
|
142
|
+
if (obj.params !== undefined) {
|
|
143
|
+
let inner: Record<string, unknown> | undefined;
|
|
144
|
+
if (typeof obj.params === "string") {
|
|
145
|
+
try {
|
|
146
|
+
const parsed = JSON.parse(obj.params);
|
|
147
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
148
|
+
inner = parsed as Record<string, unknown>;
|
|
149
|
+
}
|
|
150
|
+
} catch { /* leave as-is */ }
|
|
151
|
+
} else if (obj.params && typeof obj.params === "object" && !Array.isArray(obj.params)) {
|
|
152
|
+
inner = obj.params as Record<string, unknown>;
|
|
153
|
+
}
|
|
154
|
+
if (inner) {
|
|
155
|
+
const { params: _omit, ...rest } = obj;
|
|
156
|
+
obj = { ...inner, ...rest };
|
|
157
|
+
delete (obj as Record<string, unknown>).params;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 2. `question` → `title` (only if title missing).
|
|
162
|
+
if (obj.title === undefined && typeof obj.question === "string") {
|
|
163
|
+
obj.title = obj.question;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 3. Stringified top-level `options` for single-method calls.
|
|
40
167
|
if (typeof obj.options === "string") {
|
|
41
168
|
try {
|
|
42
169
|
const parsed = JSON.parse(obj.options);
|
|
43
170
|
if (Array.isArray(parsed)) obj.options = parsed;
|
|
44
|
-
} catch { /* leave as-is
|
|
171
|
+
} catch { /* leave as-is */ }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 4. Batch rescue: `questions` as a JSON string → parsed array.
|
|
175
|
+
if (typeof obj.questions === "string") {
|
|
176
|
+
try {
|
|
177
|
+
const parsed = JSON.parse(obj.questions);
|
|
178
|
+
if (Array.isArray(parsed)) obj.questions = parsed;
|
|
179
|
+
} catch { /* leave as-is */ }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 5. If `questions` is a non-empty array and `method` is absent, synthesize `method: "batch"`.
|
|
183
|
+
if (
|
|
184
|
+
!obj.method &&
|
|
185
|
+
Array.isArray(obj.questions) &&
|
|
186
|
+
obj.questions.length > 0
|
|
187
|
+
) {
|
|
188
|
+
obj.method = "batch";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 6. For any batch call (synthesized or explicit), backfill a missing outer `title`
|
|
192
|
+
// from the first sub-question so the schema validates. Opus frequently sends
|
|
193
|
+
// `{method:"batch", questions:[...]}` without an outer `title`.
|
|
194
|
+
if (
|
|
195
|
+
obj.method === "batch" &&
|
|
196
|
+
Array.isArray(obj.questions) &&
|
|
197
|
+
obj.questions.length > 0 &&
|
|
198
|
+
obj.title === undefined
|
|
199
|
+
) {
|
|
200
|
+
const first = obj.questions[0] as Record<string, unknown> | undefined;
|
|
201
|
+
const candidate =
|
|
202
|
+
(first && (first.title ?? first.question ?? first.header)) || "Questions";
|
|
203
|
+
obj.title = typeof candidate === "string" ? candidate : "Questions";
|
|
45
204
|
}
|
|
205
|
+
|
|
206
|
+
// 7. For batch calls, normalize each sub-question (input_type, question/header, {label,value}).
|
|
207
|
+
const warnings: NormalizationWarning[] = [];
|
|
208
|
+
if (obj.method === "batch" && Array.isArray(obj.questions)) {
|
|
209
|
+
obj.questions = obj.questions.map((sq) => normalizeSubQuestion(sq, warnings));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (warnings.length > 0) {
|
|
213
|
+
// Non-enumerable so it doesn't interfere with schema validation.
|
|
214
|
+
Object.defineProperty(obj, "__normalizations", {
|
|
215
|
+
value: warnings,
|
|
216
|
+
enumerable: false,
|
|
217
|
+
configurable: true,
|
|
218
|
+
writable: true,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
46
222
|
return obj as any;
|
|
47
223
|
},
|
|
48
224
|
async execute(_toolCallId: any, params: any, _signal: any, _onUpdate: any, ctx: any) {
|
|
49
|
-
|
|
225
|
+
// ── Batch branch ─────────────────────────────────────────────────
|
|
226
|
+
if (params.method === "batch" && Array.isArray(params.questions)) {
|
|
227
|
+
const results: Array<unknown> = [];
|
|
228
|
+
let cancelled = false;
|
|
50
229
|
|
|
51
|
-
|
|
230
|
+
for (const sq of params.questions) {
|
|
231
|
+
const subTitle = `${params.title} — ${sq.title ?? "Question"}`;
|
|
232
|
+
const subMsg = params.message ? { message: params.message } : undefined;
|
|
233
|
+
|
|
234
|
+
let answer: unknown;
|
|
235
|
+
try {
|
|
236
|
+
switch (sq.method) {
|
|
237
|
+
case "confirm":
|
|
238
|
+
answer = await ctx.ui.confirm(subTitle, sq.message ?? params.message ?? "");
|
|
239
|
+
break;
|
|
240
|
+
case "select": {
|
|
241
|
+
const opts = Array.isArray(sq.options) ? sq.options : [];
|
|
242
|
+
if (opts.length === 0) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`ask_user batch: sub-question method "select" requires a non-empty "options" array.`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
answer = await ctx.ui.select(subTitle, opts, subMsg);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
case "multiselect": {
|
|
251
|
+
const opts = Array.isArray(sq.options) ? sq.options : [];
|
|
252
|
+
if (opts.length === 0) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`ask_user batch: sub-question method "multiselect" requires a non-empty "options" array.`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
answer = await (ctx.ui as any).multiselect(subTitle, opts, subMsg);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case "input":
|
|
261
|
+
answer = await ctx.ui.input(subTitle, sq.placeholder, subMsg);
|
|
262
|
+
break;
|
|
263
|
+
default:
|
|
264
|
+
throw new Error(`ask_user batch: unknown sub-question method "${sq.method}"`);
|
|
265
|
+
}
|
|
266
|
+
} catch (err) {
|
|
267
|
+
// Propagate hard errors (schema/logic bugs); cancellation is signalled by undefined.
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Treat `undefined` from input/select/multiselect as cancellation.
|
|
272
|
+
// (confirm always resolves to a boolean and has no cancel path.)
|
|
273
|
+
if (
|
|
274
|
+
(sq.method === "input" || sq.method === "select" || sq.method === "multiselect") &&
|
|
275
|
+
answer === undefined
|
|
276
|
+
) {
|
|
277
|
+
cancelled = true;
|
|
278
|
+
results.push(null);
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
52
281
|
|
|
282
|
+
results.push(answer);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const warnings: string[] = (params as any).__normalizations ?? [];
|
|
286
|
+
const lines: string[] = [];
|
|
287
|
+
if (cancelled) {
|
|
288
|
+
lines.push(`User cancelled batch after ${results.filter((r) => r !== null).length} of ${params.questions.length} answers.`);
|
|
289
|
+
} else {
|
|
290
|
+
lines.push(`User completed batch (${results.length} answers).`);
|
|
291
|
+
}
|
|
292
|
+
params.questions.forEach((sq: any, i: number) => {
|
|
293
|
+
const ans = i < results.length ? results[i] : "(not asked)";
|
|
294
|
+
lines.push(` ${i + 1}. ${sq.title ?? sq.method}: ${JSON.stringify(ans)}`);
|
|
295
|
+
});
|
|
296
|
+
if (warnings.length > 0) {
|
|
297
|
+
lines.push("", "Warnings:");
|
|
298
|
+
for (const w of warnings) lines.push(` - ${w}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
303
|
+
details: {
|
|
304
|
+
method: "batch",
|
|
305
|
+
results,
|
|
306
|
+
cancelled,
|
|
307
|
+
warnings,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── Single-question branches (unchanged behavior) ────────────────
|
|
313
|
+
let result: unknown;
|
|
314
|
+
const msgOpts = params.message ? { message: params.message } : undefined;
|
|
53
315
|
const title = params.title || params.message || "Question";
|
|
54
316
|
|
|
55
|
-
// LLMs sometimes send options as a JSON string instead of an array
|
|
56
317
|
const options: string[] = Array.isArray(params.options)
|
|
57
318
|
? params.options
|
|
58
319
|
: typeof params.options === "string"
|
|
59
320
|
? (() => { try { const p = JSON.parse(params.options); return Array.isArray(p) ? p : []; } catch { return []; } })()
|
|
60
321
|
: [];
|
|
61
322
|
|
|
323
|
+
if ((params.method === "select" || params.method === "multiselect") && options.length === 0) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`ask_user: method "${params.method}" requires a non-empty "options" array. ` +
|
|
326
|
+
`Received: ${JSON.stringify(params.options)}. ` +
|
|
327
|
+
`If no choices are available, use method "input" instead.`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
62
331
|
switch (params.method) {
|
|
63
332
|
case "confirm":
|
|
64
333
|
result = await ctx.ui.confirm(title, params.message ?? "");
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* forwards all pi events, and relays commands back.
|
|
6
6
|
*/
|
|
7
7
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import { Loader } from "@mariozechner/pi-tui";
|
|
8
9
|
import { ConnectionManager } from "./connection.js";
|
|
9
10
|
import { detectSessionSource } from "./source-detector.js";
|
|
10
11
|
import { mapEventToProtocol } from "./event-forwarder.js";
|
|
@@ -25,7 +26,7 @@ import { expandPromptTemplateFromDisk } from "./prompt-expander.js";
|
|
|
25
26
|
import { PromptBus } from "./prompt-bus.js";
|
|
26
27
|
import { DashboardDefaultAdapter } from "./dashboard-default-adapter.js";
|
|
27
28
|
import { registerAskUserTool } from "./ask-user-tool.js";
|
|
28
|
-
import { activate as activateProviderRegister, onProviderChanged } from "./provider-register.js";
|
|
29
|
+
import { activate as activateProviderRegister, onProviderChanged, reloadProviders } from "./provider-register.js";
|
|
29
30
|
import type { FlowInfo } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
30
31
|
import { startMetricsMonitor, stopMetricsMonitor, collectMetrics } from "./process-metrics.js";
|
|
31
32
|
import { scanChildProcesses } from "./process-scanner.js";
|
|
@@ -74,6 +75,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
74
75
|
// registered before session_start fires and models_list is sent.
|
|
75
76
|
activateProviderRegister(pi);
|
|
76
77
|
|
|
78
|
+
// Anthropic-messages payload transforms (system prompt rewrite + tool
|
|
79
|
+
// filter/remap) are handled by the installed @benvargas/pi-claude-code-use
|
|
80
|
+
// package when present. No local duplication here.
|
|
81
|
+
|
|
77
82
|
initBridge(pi);
|
|
78
83
|
} catch (err) {
|
|
79
84
|
// Never crash the host pi agent — dashboard is non-essential
|
|
@@ -211,7 +216,31 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
211
216
|
// Legacy extension_ui_response removed — now handled by prompt_response → promptBus.respond()
|
|
212
217
|
// Reload auth credentials when dashboard notifies of changes
|
|
213
218
|
if (msg.type === "credentials_updated") {
|
|
214
|
-
try {
|
|
219
|
+
try {
|
|
220
|
+
// Hot-reload providers.json diff BEFORE refreshing the registry,
|
|
221
|
+
// so any newly added providers are registered before getAvailable() runs.
|
|
222
|
+
const diff = await reloadProviders(pi).catch((err) => {
|
|
223
|
+
console.error("[dashboard] reloadProviders failed:", err);
|
|
224
|
+
return { added: [], removed: [], changed: [] };
|
|
225
|
+
});
|
|
226
|
+
if (diff.added.length || diff.removed.length || diff.changed.length) {
|
|
227
|
+
console.log(
|
|
228
|
+
`[dashboard] hot-reloaded providers: added=${JSON.stringify(diff.added)} removed=${JSON.stringify(diff.removed)} changed=${JSON.stringify(diff.changed)}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
cachedModelRegistry?.authStorage?.reload?.();
|
|
232
|
+
cachedModelRegistry?.refresh?.();
|
|
233
|
+
} catch (err) { console.error("[dashboard] credentials reload failed:", err); }
|
|
234
|
+
// Push updated models list to dashboard client
|
|
235
|
+
if (cachedModelRegistry && sessionReady) {
|
|
236
|
+
try {
|
|
237
|
+
const models = cachedModelRegistry.getAvailable().map((m: any) => ({
|
|
238
|
+
provider: m.provider,
|
|
239
|
+
id: m.id,
|
|
240
|
+
}));
|
|
241
|
+
connection.send({ type: "models_list", sessionId, models });
|
|
242
|
+
} catch (err) { console.error("[dashboard] models_list push failed:", err); }
|
|
243
|
+
}
|
|
215
244
|
return;
|
|
216
245
|
}
|
|
217
246
|
// Route flow management actions from dashboard buttons
|
|
@@ -456,7 +485,9 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
456
485
|
}
|
|
457
486
|
// Fallback: send as user message (template-expanded).
|
|
458
487
|
// Uses deliverAs:followUp so it queues properly when agent is streaming.
|
|
459
|
-
|
|
488
|
+
// expandPromptTemplateFromDisk handles skill commands (/skill:xxx) and
|
|
489
|
+
// prompt templates by reading the file content from disk.
|
|
490
|
+
const expanded = expandPromptTemplateFromDisk(text, process.cwd(), pi);
|
|
460
491
|
(pi.sendUserMessage as any)(expanded, { deliverAs: "followUp" });
|
|
461
492
|
},
|
|
462
493
|
});
|
|
@@ -581,8 +612,23 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
581
612
|
}
|
|
582
613
|
}
|
|
583
614
|
|
|
584
|
-
// For message_start
|
|
585
|
-
if (eventType === "message_start"
|
|
615
|
+
// For message_start, enrich with entryId immediately (current leaf)
|
|
616
|
+
if (eventType === "message_start") {
|
|
617
|
+
const entryId = ctx.sessionManager?.getLeafId?.();
|
|
618
|
+
if (entryId) {
|
|
619
|
+
const enriched = { ...event, entryId };
|
|
620
|
+
const msg = mapEventToProtocol(sessionId, enriched);
|
|
621
|
+
connection.send(msg);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// For message_end, defer getLeafId() so it runs after pi core persists the entry.
|
|
627
|
+
// Pi core calls _emit (which invokes this handler) BEFORE appendMessage (which updates leafId).
|
|
628
|
+
// Since _emit doesn't await async handlers, yielding via queueMicrotask lets appendMessage
|
|
629
|
+
// run first, so getLeafId() returns the correct entry ID for the just-persisted message.
|
|
630
|
+
if (eventType === "message_end") {
|
|
631
|
+
await new Promise<void>(resolve => queueMicrotask(resolve));
|
|
586
632
|
const entryId = ctx.sessionManager?.getLeafId?.();
|
|
587
633
|
if (entryId) {
|
|
588
634
|
const enriched = { ...event, entryId };
|
|
@@ -917,17 +963,72 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
917
963
|
}
|
|
918
964
|
|
|
919
965
|
// Discover or auto-start server (non-blocking — connection will reconnect)
|
|
966
|
+
//
|
|
967
|
+
// When a real launchServer() is about to run (not on mDNS/health-check
|
|
968
|
+
// paths), mount an animated TUI widget above the editor using pi-tui's
|
|
969
|
+
// Loader (a real Component, self-animating at 80ms, like pi-flows'
|
|
970
|
+
// architect-widget). The previous implementation used
|
|
971
|
+
// ctx.ui.setStatus(...) which only writes a footer string and relies on
|
|
972
|
+
// the TUI render loop being ticked elsewhere — on the cold-start path
|
|
973
|
+
// nothing else requests renders, so the spinner never animated and often
|
|
974
|
+
// never appeared. setWidget(key, factory, {placement:"aboveEditor"}) gives
|
|
975
|
+
// us a managed component that owns its own render loop and is always
|
|
976
|
+
// visible while the launch is in flight.
|
|
977
|
+
let spinnerTimer: NodeJS.Timeout | null = null;
|
|
978
|
+
let spinnerStart = 0;
|
|
979
|
+
let activeLoader: Loader | null = null;
|
|
980
|
+
const stopSpinner = () => {
|
|
981
|
+
if (spinnerTimer) {
|
|
982
|
+
clearInterval(spinnerTimer);
|
|
983
|
+
spinnerTimer = null;
|
|
984
|
+
}
|
|
985
|
+
activeLoader = null;
|
|
986
|
+
ctx.ui.setWidget("pi-dashboard-launch", undefined);
|
|
987
|
+
};
|
|
920
988
|
autoStartServer(config, {
|
|
921
989
|
discoverDashboard,
|
|
922
990
|
isDashboardRunning,
|
|
923
991
|
launchServer,
|
|
924
992
|
notify: (msg, level) => ctx.ui.notify(msg, level),
|
|
993
|
+
onLaunchStart: () => {
|
|
994
|
+
spinnerStart = Date.now();
|
|
995
|
+
const buildMessage = () => {
|
|
996
|
+
const elapsed = Math.floor((Date.now() - spinnerStart) / 1000);
|
|
997
|
+
return `starting dashboard server … (${elapsed}s)`;
|
|
998
|
+
};
|
|
999
|
+
ctx.ui.setWidget(
|
|
1000
|
+
"pi-dashboard-launch",
|
|
1001
|
+
(tui: unknown, theme: { fg: (role: string, s: string) => string }) => {
|
|
1002
|
+
const loader = new Loader(
|
|
1003
|
+
tui as ConstructorParameters<typeof Loader>[0],
|
|
1004
|
+
(s: string) => theme.fg("accent", s),
|
|
1005
|
+
(s: string) => theme.fg("muted", s),
|
|
1006
|
+
buildMessage(),
|
|
1007
|
+
);
|
|
1008
|
+
activeLoader = loader;
|
|
1009
|
+
// Loader has stop() but no dispose(); wire dispose so that
|
|
1010
|
+
// setExtensionWidget's teardown stops the 80ms animation interval.
|
|
1011
|
+
(loader as Loader & { dispose?: () => void }).dispose = () => loader.stop();
|
|
1012
|
+
return loader;
|
|
1013
|
+
},
|
|
1014
|
+
{ placement: "aboveEditor" },
|
|
1015
|
+
);
|
|
1016
|
+
// Refresh the elapsed-seconds label every second. Frame animation is
|
|
1017
|
+
// driven by the Loader's own 80ms interval.
|
|
1018
|
+
spinnerTimer = setInterval(() => {
|
|
1019
|
+
activeLoader?.setMessage(buildMessage());
|
|
1020
|
+
}, 1000);
|
|
1021
|
+
},
|
|
1022
|
+
onLaunchEnd: () => {
|
|
1023
|
+
stopSpinner();
|
|
1024
|
+
},
|
|
925
1025
|
}).then((result) => {
|
|
1026
|
+
stopSpinner(); // safety net — covers onLaunchEnd not firing
|
|
926
1027
|
if (result.server && result.server.piPort !== config.piPort) {
|
|
927
1028
|
// Server found on a different piPort than configured — update connection URL
|
|
928
1029
|
connection.updateUrl(`ws://${result.server.host === 'localhost' ? 'localhost' : result.server.host}:${result.server.piPort}`);
|
|
929
1030
|
}
|
|
930
|
-
}).catch(() => {});
|
|
1031
|
+
}).catch(() => { stopSpinner(); });
|
|
931
1032
|
|
|
932
1033
|
// Send initial git info
|
|
933
1034
|
sendGitInfoIfChanged(ctx.cwd);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Handles server→extension messages by dispatching to pi API.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { readdirSync } from "node:fs";
|
|
5
|
+
import { join, relative } from "node:path";
|
|
5
6
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
7
|
import type {
|
|
7
8
|
ServerToExtensionMessage,
|
|
@@ -10,47 +11,33 @@ import type {
|
|
|
10
11
|
import { killProcessByPgid } from "./process-scanner.js";
|
|
11
12
|
import type { FileEntry, PiSessionInfo } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
12
13
|
import { filterHiddenCommands } from "./bridge-context.js";
|
|
14
|
+
import { expandPromptTemplateFromDisk } from "./prompt-expander.js";
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17
|
-
}
|
|
16
|
+
const IGNORE_DIRS = new Set([".git", "node_modules", ".next", "dist", "build", ".cache", "__pycache__", ".venv"]);
|
|
17
|
+
const MAX_RESULTS = 20;
|
|
18
18
|
|
|
19
|
-
/** Search files using fd */
|
|
20
19
|
function searchFiles(cwd: string, query: string): FileEntry[] {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
encoding: "utf-8",
|
|
38
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
39
|
-
timeout: 5000,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
if (result.status !== 0 || !result.stdout) {
|
|
43
|
-
return [];
|
|
20
|
+
const results: FileEntry[] = [];
|
|
21
|
+
const lowerQuery = query?.toLowerCase() ?? "";
|
|
22
|
+
|
|
23
|
+
function walk(dir: string, depth: number): void {
|
|
24
|
+
if (results.length >= MAX_RESULTS || depth > 6) return;
|
|
25
|
+
let entries;
|
|
26
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (results.length >= MAX_RESULTS) return;
|
|
29
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
30
|
+
const fullPath = join(dir, entry.name);
|
|
31
|
+
const relPath = relative(cwd, fullPath).replace(/\\/g, "/") + (entry.isDirectory() ? "/" : "");
|
|
32
|
+
if (!lowerQuery || relPath.toLowerCase().includes(lowerQuery)) {
|
|
33
|
+
results.push({ path: relPath, isDirectory: entry.isDirectory() });
|
|
34
|
+
}
|
|
35
|
+
if (entry.isDirectory()) walk(fullPath, depth + 1);
|
|
44
36
|
}
|
|
45
|
-
|
|
46
|
-
return result.stdout.trim().split("\n").filter(Boolean).map((line) => {
|
|
47
|
-
const normalized = line.replace(/\\/g, "/");
|
|
48
|
-
const isDirectory = normalized.endsWith("/");
|
|
49
|
-
return { path: normalized, isDirectory };
|
|
50
|
-
});
|
|
51
|
-
} catch {
|
|
52
|
-
return [];
|
|
53
37
|
}
|
|
38
|
+
|
|
39
|
+
walk(cwd, 0);
|
|
40
|
+
return results;
|
|
54
41
|
}
|
|
55
42
|
|
|
56
43
|
/** Parsed result from parseSendPrompt */
|
|
@@ -288,8 +275,15 @@ export function createCommandHandler(
|
|
|
288
275
|
return undefined;
|
|
289
276
|
}
|
|
290
277
|
|
|
291
|
-
// Passthrough: send as regular user message (with image handling)
|
|
292
|
-
|
|
278
|
+
// Passthrough: send as regular user message (with image handling).
|
|
279
|
+
// Multi-line slash commands (e.g. "/skill:foo\nuser text") are classified as
|
|
280
|
+
// passthrough by parseSendPrompt to preserve images (the slash route strips them),
|
|
281
|
+
// so we expand prompt templates / skills here before sending.
|
|
282
|
+
let outgoing = msg.text;
|
|
283
|
+
if (outgoing.startsWith("/")) {
|
|
284
|
+
outgoing = expandPromptTemplateFromDisk(outgoing, process.cwd(), pi);
|
|
285
|
+
}
|
|
286
|
+
sendUserMessageWithImages(pi, outgoing, msg.images);
|
|
293
287
|
return undefined;
|
|
294
288
|
}
|
|
295
289
|
|
|
@@ -338,6 +332,7 @@ export function createCommandHandler(
|
|
|
338
332
|
const registry = options?.getModelRegistry?.();
|
|
339
333
|
if (registry) {
|
|
340
334
|
try {
|
|
335
|
+
registry.authStorage?.reload?.();
|
|
341
336
|
registry.refresh();
|
|
342
337
|
const models = registry.getAvailable().map((m: any) => ({
|
|
343
338
|
provider: m.provider,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Dev build-on-reload helper.
|
|
3
3
|
* Builds the Vite client and requests server shutdown.
|
|
4
4
|
*/
|
|
5
|
-
import { execSync as defaultExecSync } from "
|
|
5
|
+
import { execSync as defaultExecSync } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
|
|
6
6
|
|
|
7
7
|
export interface DevBuildOptions {
|
|
8
8
|
packageRoot: string;
|