@blackbelt-technology/pi-agent-dashboard 0.5.0 → 0.5.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 +26 -5
- package/README.md +49 -7
- package/docs/architecture.md +129 -1
- package/package.json +15 -15
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +78 -8
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
- package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
- package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
- package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
- package/packages/extension/src/__tests__/server-launcher-launch.test.ts +78 -0
- package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
- package/packages/extension/src/ask-user-tool.ts +1 -1
- package/packages/extension/src/bridge-context.ts +68 -4
- package/packages/extension/src/bridge.ts +79 -11
- package/packages/extension/src/command-handler.ts +95 -15
- package/packages/extension/src/flow-event-wiring.ts +1 -1
- package/packages/extension/src/multiselect-list.ts +1 -1
- package/packages/extension/src/pi-env.d.ts +16 -9
- package/packages/extension/src/prompt-expander.ts +74 -63
- package/packages/extension/src/provider-register.ts +16 -9
- package/packages/extension/src/retry-tracker.ts +123 -0
- package/packages/extension/src/server-launcher.ts +31 -70
- package/packages/extension/src/session-sync.ts +10 -1
- package/packages/extension/src/slash-dispatch.ts +123 -0
- package/packages/extension/src/usage-limit-orderer.ts +76 -0
- package/packages/server/bin/pi-dashboard.mjs +84 -0
- package/packages/server/package.json +8 -7
- package/packages/server/scripts/fix-pty-permissions.cjs +52 -0
- package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
- package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
- package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +16 -4
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service.test.ts +2 -2
- package/packages/server/src/__tests__/dispatch-extension-command-router.test.ts +178 -0
- package/packages/server/src/__tests__/e2e/model-proxy-google-flash.test.ts +184 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +68 -1
- package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
- package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +316 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
- package/packages/server/src/__tests__/keeper-manager.test.ts +298 -0
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +149 -0
- package/packages/server/src/__tests__/model-proxy-api-key-routes.test.ts +277 -0
- package/packages/server/src/__tests__/model-proxy-auth-gate.test.ts +263 -0
- package/packages/server/src/__tests__/model-proxy-multi-user.test.ts +169 -0
- package/packages/server/src/__tests__/model-proxy-routes.test.ts +286 -0
- package/packages/server/src/__tests__/model-proxy-second-port.test.ts +116 -0
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +64 -8
- package/packages/server/src/__tests__/openspec-group-broadcast.test.ts +97 -0
- package/packages/server/src/__tests__/openspec-group-join.test.ts +80 -0
- package/packages/server/src/__tests__/openspec-group-routes.test.ts +370 -0
- package/packages/server/src/__tests__/openspec-group-store.test.ts +496 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
- package/packages/server/src/__tests__/package-routes.test.ts +1 -1
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
- package/packages/server/src/__tests__/pi-ai-shape.test.ts +147 -0
- package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
- package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +62 -3
- package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
- package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
- package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
- package/packages/server/src/__tests__/process-manager-keeper-spawn.test.ts +206 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +13 -23
- package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +3 -3
- package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +84 -0
- package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
- package/packages/server/src/__tests__/tunnel-watchdog.test.ts +139 -0
- package/packages/server/src/auth-plugin.ts +3 -0
- package/packages/server/src/bootstrap-state.ts +10 -0
- package/packages/server/src/browser-gateway.ts +27 -10
- package/packages/server/src/browser-handlers/handler-context.ts +9 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +128 -19
- package/packages/server/src/changelog-fs.ts +167 -0
- package/packages/server/src/changelog-parser.ts +321 -0
- package/packages/server/src/changelog-remote.ts +134 -0
- package/packages/server/src/cli.ts +62 -82
- package/packages/server/src/config-api.ts +14 -2
- package/packages/server/src/directory-service.ts +106 -4
- package/packages/server/src/event-wiring.ts +90 -6
- package/packages/server/src/headless-pid-registry.ts +344 -37
- package/packages/server/src/legacy-pi-cleanup.ts +151 -0
- package/packages/server/src/model-proxy/__tests__/api-key-store.test.ts +142 -0
- package/packages/server/src/model-proxy/__tests__/auth-json-contention.test.ts +98 -0
- package/packages/server/src/model-proxy/__tests__/concurrency.test.ts +107 -0
- package/packages/server/src/model-proxy/__tests__/failed-auth-backoff.test.ts +46 -0
- package/packages/server/src/model-proxy/__tests__/recursion-guard.test.ts +61 -0
- package/packages/server/src/model-proxy/__tests__/streamer.test.ts +139 -0
- package/packages/server/src/model-proxy/api-key-store.ts +87 -0
- package/packages/server/src/model-proxy/auth-gate.ts +116 -0
- package/packages/server/src/model-proxy/concurrency.ts +76 -0
- package/packages/server/src/model-proxy/convert/UPSTREAM.md +13 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-in.test.ts +137 -0
- package/packages/server/src/model-proxy/convert/__tests__/anthropic-out.test.ts +183 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-in.test.ts +134 -0
- package/packages/server/src/model-proxy/convert/__tests__/openai-out.test.ts +166 -0
- package/packages/server/src/model-proxy/convert/anthropic-in.ts +129 -0
- package/packages/server/src/model-proxy/convert/anthropic-out.ts +173 -0
- package/packages/server/src/model-proxy/convert/index.ts +8 -0
- package/packages/server/src/model-proxy/convert/openai-in.ts +119 -0
- package/packages/server/src/model-proxy/convert/openai-out.ts +151 -0
- package/packages/server/src/model-proxy/convert/types.ts +70 -0
- package/packages/server/src/model-proxy/failed-auth-backoff.ts +45 -0
- package/packages/server/src/model-proxy/internal-auth-storage.ts +146 -0
- package/packages/server/src/model-proxy/internal-registry.ts +157 -0
- package/packages/server/src/model-proxy/recursion-guard.ts +72 -0
- package/packages/server/src/model-proxy/registry-singleton.ts +109 -0
- package/packages/server/src/model-proxy/request-log.ts +53 -0
- package/packages/server/src/model-proxy/streamer.ts +59 -0
- package/packages/server/src/openspec-group-store.ts +490 -0
- package/packages/server/src/pending-client-correlations.ts +73 -0
- package/packages/server/src/pending-fork-registry.ts +24 -12
- package/packages/server/src/pi-core-checker.ts +77 -17
- package/packages/server/src/pi-core-updater.ts +16 -6
- package/packages/server/src/pi-dev-version-check.ts +145 -0
- package/packages/server/src/pi-gateway.ts +4 -0
- package/packages/server/src/pi-version-skew.ts +12 -4
- package/packages/server/src/process-manager.ts +182 -11
- package/packages/server/src/provider-auth-storage.ts +29 -47
- package/packages/server/src/provider-catalogue-cache.ts +24 -18
- package/packages/server/src/restart-helper.ts +17 -16
- package/packages/server/src/routes/bootstrap-routes.ts +37 -0
- package/packages/server/src/routes/jj-routes.ts +3 -0
- package/packages/server/src/routes/model-proxy-api-key-routes.ts +168 -0
- package/packages/server/src/routes/model-proxy-refresh-routes.ts +24 -0
- package/packages/server/src/routes/model-proxy-routes.ts +330 -0
- package/packages/server/src/routes/openspec-group-routes.ts +231 -0
- package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
- package/packages/server/src/routes/pi-core-routes.ts +1 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -1
- package/packages/server/src/routes/provider-routes.ts +28 -5
- package/packages/server/src/routes/system-routes.ts +44 -2
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi-shim.sh +9 -0
- package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi.cjs +50 -0
- package/packages/server/src/rpc-keeper/__tests__/keeper.test.ts +371 -0
- package/packages/server/src/rpc-keeper/dispatch-router.ts +85 -0
- package/packages/server/src/rpc-keeper/keeper-manager.ts +364 -0
- package/packages/server/src/rpc-keeper/keeper.cjs +313 -0
- package/packages/server/src/server.ts +254 -60
- package/packages/server/src/session-api.ts +63 -4
- package/packages/server/src/session-discovery.ts +1 -1
- package/packages/server/src/session-file-reader.ts +1 -1
- package/packages/server/src/spawn-register-watchdog.ts +62 -7
- package/packages/server/src/spawn-token.ts +20 -0
- package/packages/server/src/tunnel-watchdog.ts +230 -0
- package/packages/server/src/tunnel.ts +5 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +228 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +24 -17
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +5 -3
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
- package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
- package/packages/shared/src/__tests__/config-openspec.test.ts +74 -0
- package/packages/shared/src/__tests__/model-proxy-config.test.ts +146 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +7 -5
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
- package/packages/shared/src/__tests__/node-spawn.test.ts +51 -0
- package/packages/shared/src/__tests__/openspec-groups-types.test.ts +135 -0
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +96 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +11 -3
- package/packages/shared/src/__tests__/server-launcher.test.ts +227 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
- package/packages/shared/src/bootstrap-install.ts +1 -1
- package/packages/shared/src/browser-protocol.ts +70 -0
- package/packages/shared/src/changelog-types.ts +111 -0
- package/packages/shared/src/config.ts +172 -2
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +16 -1
- package/packages/shared/src/dashboard-plugin/slot-props.ts +8 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +57 -0
- package/packages/shared/src/platform/binary-lookup.ts +204 -0
- package/packages/shared/src/platform/node-spawn.ts +71 -26
- package/packages/shared/src/protocol.ts +27 -1
- package/packages/shared/src/recommended-extensions.ts +18 -0
- package/packages/shared/src/rest-api.ts +219 -1
- package/packages/shared/src/server-launcher.ts +277 -0
- package/packages/shared/src/skill-block-parser.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
- package/packages/shared/src/tool-registry/definitions.ts +15 -3
- package/packages/shared/src/types.ts +62 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -53
- package/packages/shared/src/resolve-jiti.ts +0 -102
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared mutable state for bridge modules.
|
|
3
3
|
* Avoids passing 14+ closure variables to every extracted function.
|
|
4
4
|
*/
|
|
5
|
-
import type { ExtensionAPI } from "@
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import type { ConnectionManager } from "./connection.js";
|
|
7
7
|
|
|
8
8
|
export interface BridgeContext {
|
|
@@ -40,9 +40,11 @@ export interface BridgeContext {
|
|
|
40
40
|
hasRegisteredOnce: boolean;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// Commands that the dashboard handles natively with superior UX
|
|
44
|
-
//
|
|
45
|
-
|
|
43
|
+
// Commands that the dashboard handles natively with superior UX, filtered from
|
|
44
|
+
// the command list sent to dashboard clients AND from extension-slash detection.
|
|
45
|
+
// Current set: { "roles" }. Bridge-registered `__dashboard_reload` is filtered
|
|
46
|
+
// separately by the `__`-prefix rule. See change: fix-extension-slash-commands-in-dashboard.
|
|
47
|
+
export const DASHBOARD_NATIVE_COMMANDS = new Set(["roles"]);
|
|
46
48
|
|
|
47
49
|
/** Filter out hidden commands (names starting with __) and dashboard-native commands from commands list */
|
|
48
50
|
export function filterHiddenCommands(commands: any[]): any[] {
|
|
@@ -52,6 +54,68 @@ export function filterHiddenCommands(commands: any[]): any[] {
|
|
|
52
54
|
);
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Pure predicate: does `text` name an extension-registered slash command?
|
|
59
|
+
*
|
|
60
|
+
* Returns true iff:
|
|
61
|
+
* - `text` starts with `/` and contains no embedded newline
|
|
62
|
+
* - the token after `/` (up to first space or end) appears in `commandList`
|
|
63
|
+
* with `source === "extension"`
|
|
64
|
+
* - that token is NOT in `DASHBOARD_NATIVE_COMMANDS` (and not `__`-prefixed)
|
|
65
|
+
*
|
|
66
|
+
* Pure: no pi calls, no mutation. See change: fix-extension-slash-commands-in-dashboard.
|
|
67
|
+
*/
|
|
68
|
+
export function isExtensionSlashCommand(
|
|
69
|
+
text: string,
|
|
70
|
+
commandList: ReadonlyArray<{ name: string; source?: string }>,
|
|
71
|
+
): boolean {
|
|
72
|
+
if (typeof text !== "string" || !text.startsWith("/")) return false;
|
|
73
|
+
if (text.includes("\n")) return false;
|
|
74
|
+
const rest = text.slice(1);
|
|
75
|
+
const spaceIdx = rest.indexOf(" ");
|
|
76
|
+
const cmdName = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
|
|
77
|
+
if (!cmdName) return false;
|
|
78
|
+
if (cmdName.startsWith("__")) return false;
|
|
79
|
+
if (DASHBOARD_NATIVE_COMMANDS.has(cmdName)) return false;
|
|
80
|
+
return commandList.some((c) => c?.name === cmdName && c?.source === "extension");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Feature-detect upstream `pi.dispatchCommand(text, opts)` (pi 0.71+).
|
|
85
|
+
* Returns true iff the field is a function on the supplied object.
|
|
86
|
+
* See change: fix-extension-slash-commands-in-dashboard.
|
|
87
|
+
*/
|
|
88
|
+
export function hasDispatchCommand(pi: unknown): boolean {
|
|
89
|
+
return typeof (pi as any)?.dispatchCommand === "function";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Pure predicate: is this bridge running inside a dashboard-spawned
|
|
94
|
+
* headless `pi --mode rpc` session?
|
|
95
|
+
*
|
|
96
|
+
* Both probes MUST be true:
|
|
97
|
+
* 1. `process.env.PI_DASHBOARD_SPAWNED === "1"` (set by
|
|
98
|
+
* `process-manager.ts::buildSpawnEnv` for every dashboard-spawned session).
|
|
99
|
+
* 2. `process.argv` contains `--mode` adjacent to `rpc`.
|
|
100
|
+
*
|
|
101
|
+
* Either alone is insufficient: env-only matches dashboard-spawned tmux
|
|
102
|
+
* sessions; argv-only matches non-dashboard RPC invocations.
|
|
103
|
+
*
|
|
104
|
+
* Optional `env` / `argv` parameters exist purely for unit testing
|
|
105
|
+
* (defaulting to the live process state). See change:
|
|
106
|
+
* add-rpc-stdin-dispatch-with-keeper-sidecar (task 7.1).
|
|
107
|
+
*/
|
|
108
|
+
export function isHeadlessRpcSession(
|
|
109
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
110
|
+
argv: ReadonlyArray<string> = process.argv,
|
|
111
|
+
): boolean {
|
|
112
|
+
if (env.PI_DASHBOARD_SPAWNED !== "1") return false;
|
|
113
|
+
for (let i = 0; i < argv.length - 1; i++) {
|
|
114
|
+
if (argv[i] === "--mode" && argv[i + 1] === "rpc") return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
55
119
|
/** Extract first user message text from session entries */
|
|
56
120
|
export function extractFirstMessage(ctx: any): string | undefined {
|
|
57
121
|
try {
|
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
* Global extension that connects to the dashboard server,
|
|
5
5
|
* forwards all pi events, and relays commands back.
|
|
6
6
|
*/
|
|
7
|
-
import type { ExtensionAPI } from "@
|
|
8
|
-
import { Loader } from "@
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { Loader } from "@earendil-works/pi-tui";
|
|
9
9
|
import { ConnectionManager } from "./connection.js";
|
|
10
10
|
import { detectSessionSource } from "./source-detector.js";
|
|
11
11
|
import { mapEventToProtocol } from "./event-forwarder.js";
|
|
12
12
|
import { createCommandHandler } from "./command-handler.js";
|
|
13
|
+
import { RetryTracker } from "./retry-tracker.js";
|
|
14
|
+
import { UsageLimitOrderer } from "./usage-limit-orderer.js";
|
|
13
15
|
import fs from "node:fs";
|
|
14
16
|
import os from "node:os";
|
|
15
17
|
import path from "node:path";
|
|
@@ -33,6 +35,7 @@ import { startMetricsMonitor, stopMetricsMonitor, collectMetrics } from "./proce
|
|
|
33
35
|
import { scanChildProcesses } from "./process-scanner.js";
|
|
34
36
|
import type { BridgeContext } from "./bridge-context.js";
|
|
35
37
|
import { filterHiddenCommands, extractFirstMessage, getCurrentModelString } from "./bridge-context.js";
|
|
38
|
+
import { tryDispatchExtensionCommand } from "./slash-dispatch.js";
|
|
36
39
|
import { sendStateSync as _sendStateSync, replaySessionEntries as _replaySessionEntries, handleSessionChange as _handleSessionChange } from "./session-sync.js";
|
|
37
40
|
import { sendModelUpdateIfChanged as _sendModelUpdateIfChanged, sendSessionNameIfChanged as _sendSessionNameIfChanged, sendGitInfoIfChanged as _sendGitInfoIfChanged, sendJjStateIfChanged as _sendJjStateIfChanged, resetReconnectCaches as _resetReconnectCaches } from "./model-tracker.js";
|
|
38
41
|
import { registerFlowEventListeners, FLOW_EVENT_MAP, SUBAGENT_EVENT_MAP } from "./flow-event-wiring.js";
|
|
@@ -191,6 +194,22 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
191
194
|
let hasRegisteredOnce = false; // see change: reattach-move-to-front
|
|
192
195
|
let promptBus: PromptBus | undefined;
|
|
193
196
|
|
|
197
|
+
// Provider-retry synthesis trackers. pi's ExtensionAPI does not expose
|
|
198
|
+
// `auto_retry_*` events, so the bridge synthesizes them from observed
|
|
199
|
+
// `message_end` / `agent_end` events. See change: fix-provider-retry-infinite-loop.
|
|
200
|
+
const retryTracker = new RetryTracker();
|
|
201
|
+
const usageLimitOrderer = new UsageLimitOrderer();
|
|
202
|
+
|
|
203
|
+
/** Forward a synthesized auto_retry_* event using the standard event_forward shape. */
|
|
204
|
+
const sendSyntheticRetryEvent = (eventType: string, data: Record<string, unknown>): void => {
|
|
205
|
+
if (!isActive() || !sessionReady) return;
|
|
206
|
+
connection.send({
|
|
207
|
+
type: "event_forward",
|
|
208
|
+
sessionId,
|
|
209
|
+
event: { eventType, timestamp: Date.now(), data },
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
|
|
194
213
|
// ── Per-message entry id tracking (for fix-per-message-fork) ──
|
|
195
214
|
// Pi 0.69+ awaits extension handlers BEFORE sessionManager.appendMessage runs,
|
|
196
215
|
// which means getLeafId() at emit time returns the previous leaf, not the
|
|
@@ -648,6 +667,14 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
648
667
|
if (cachedCtx?.abort) {
|
|
649
668
|
cachedCtx.abort();
|
|
650
669
|
}
|
|
670
|
+
// Clear retry-synthesis trackers — the user-initiated abort path
|
|
671
|
+
// already synthesizes its own auto_retry_end via command-handler.
|
|
672
|
+
// See change: fix-provider-retry-infinite-loop.
|
|
673
|
+
retryTracker.noteAbort(sessionId);
|
|
674
|
+
usageLimitOrderer.noteRetryEnd(sessionId);
|
|
675
|
+
},
|
|
676
|
+
isIdle: () => {
|
|
677
|
+
try { return cachedCtx?.isIdle?.() ?? false; } catch { return false; }
|
|
651
678
|
},
|
|
652
679
|
eventSink: (msg) => connection.send(msg),
|
|
653
680
|
compact: (opts) => {
|
|
@@ -668,26 +695,37 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
668
695
|
spawnNew: () => {
|
|
669
696
|
connection.send({ type: "spawn_new_session", sessionId, cwd: process.cwd() });
|
|
670
697
|
},
|
|
671
|
-
sessionPrompt: (text) => {
|
|
672
|
-
// Route slash commands: management events, flow:run, then fallback
|
|
698
|
+
sessionPrompt: async (text) => {
|
|
699
|
+
// Route slash commands: management events, flow:run, extension dispatch, then fallback.
|
|
700
|
+
// See change: fix-extension-slash-commands-in-dashboard.
|
|
673
701
|
if (text.startsWith("/") && pi.events) {
|
|
674
702
|
const cmdText = text.slice(1);
|
|
675
703
|
const spaceIdx = cmdText.indexOf(" ");
|
|
676
704
|
const cmdName = spaceIdx === -1 ? cmdText : cmdText.slice(0, spaceIdx);
|
|
677
705
|
const cmdArgs = spaceIdx === -1 ? "" : cmdText.slice(spaceIdx + 1);
|
|
678
706
|
|
|
679
|
-
// Flow
|
|
680
|
-
// Typed /flows:new, /flows:edit, /flows:delete in chat input fall through
|
|
681
|
-
// to the slash command handler below, which invokes pi's command system
|
|
682
|
-
// via pi.sendUserMessage (with ui-proxy handling ctx.ui calls).
|
|
683
|
-
|
|
684
|
-
// Check if it's a user-defined flow via flow:list-flows
|
|
707
|
+
// Flow fast-path: typed /<user-defined-flow-name> wins over extension dispatch.
|
|
685
708
|
const flowsList = getFlowsList();
|
|
686
709
|
if (flowsList.some(f => f.name === cmdName)) {
|
|
687
710
|
pi.events.emit("flow:run", { flowName: cmdName, task: cmdArgs.trim() || undefined });
|
|
688
711
|
return;
|
|
689
712
|
}
|
|
690
713
|
}
|
|
714
|
+
|
|
715
|
+
// Extension-command dispatch (routing step 9). When matched, the helper
|
|
716
|
+
// emits its own command_feedback events and we MUST NOT fall through.
|
|
717
|
+
// The `connection` arg enables Path C (headless RPC → server-routed
|
|
718
|
+
// dispatch via the keeper UDS); see change:
|
|
719
|
+
// add-rpc-stdin-dispatch-with-keeper-sidecar.
|
|
720
|
+
const handled = await tryDispatchExtensionCommand(
|
|
721
|
+
pi,
|
|
722
|
+
text,
|
|
723
|
+
sessionId,
|
|
724
|
+
(msg) => connection.send(msg),
|
|
725
|
+
connection,
|
|
726
|
+
);
|
|
727
|
+
if (handled) return;
|
|
728
|
+
|
|
691
729
|
// Fallback: send as user message (template-expanded).
|
|
692
730
|
// Uses deliverAs:followUp so it queues properly when agent is streaming.
|
|
693
731
|
// expandPromptTemplateFromDisk handles skill commands (/skill:xxx) and
|
|
@@ -802,7 +840,25 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
802
840
|
if (!sessionReady) return;
|
|
803
841
|
// Track agent streaming state (survives reconnect/reload)
|
|
804
842
|
if (eventType === "agent_start") getBridgeState().isAgentStreaming = true;
|
|
805
|
-
if (eventType === "agent_end")
|
|
843
|
+
if (eventType === "agent_end") {
|
|
844
|
+
getBridgeState().isAgentStreaming = false;
|
|
845
|
+
// Provider-retry synthesis: forward auto_retry_end BEFORE agent_end
|
|
846
|
+
// when retries were in flight, so the dashboard's retry banner
|
|
847
|
+
// clears before the error banner appears. The usage-limit orderer
|
|
848
|
+
// takes precedence (it carries the actual error string); the retry
|
|
849
|
+
// tracker handles the non-usage-limit case. See change:
|
|
850
|
+
// fix-provider-retry-infinite-loop.
|
|
851
|
+
const orderedSynth = usageLimitOrderer.maybeSynthesize(sessionId, (event as any));
|
|
852
|
+
if (orderedSynth) {
|
|
853
|
+
sendSyntheticRetryEvent(orderedSynth.eventType, orderedSynth.data);
|
|
854
|
+
retryTracker.noteAbort(sessionId); // clear tracker; orderer's event is authoritative
|
|
855
|
+
} else {
|
|
856
|
+
const trackerSynth = retryTracker.observeAgentEnd(sessionId, event as any);
|
|
857
|
+
if (trackerSynth) {
|
|
858
|
+
sendSyntheticRetryEvent(trackerSynth.eventType, trackerSynth.data);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
806
862
|
// For model_select, enrich the event data with thinkingLevel
|
|
807
863
|
if (eventType === "model_select") {
|
|
808
864
|
const enriched = { ...event, thinkingLevel: (pi as any).getThinkingLevel?.() };
|
|
@@ -872,6 +928,18 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
872
928
|
const enriched = { ...event, entryId, nonce };
|
|
873
929
|
const protoMsg = mapEventToProtocol(sessionId, enriched);
|
|
874
930
|
connection.send(protoMsg);
|
|
931
|
+
// After forwarding the original message_end, ask the retry tracker
|
|
932
|
+
// whether to synthesize an auto_retry_* event. See change:
|
|
933
|
+
// fix-provider-retry-infinite-loop.
|
|
934
|
+
const synthetic = retryTracker.observeMessageEnd(sessionId, messageRef as any);
|
|
935
|
+
if (synthetic) {
|
|
936
|
+
sendSyntheticRetryEvent(synthetic.eventType, synthetic.data);
|
|
937
|
+
if (synthetic.eventType === "auto_retry_start") {
|
|
938
|
+
usageLimitOrderer.noteRetryStart(sessionId);
|
|
939
|
+
} else {
|
|
940
|
+
usageLimitOrderer.noteRetryEnd(sessionId);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
875
943
|
}, 0);
|
|
876
944
|
return;
|
|
877
945
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { readdirSync } from "node:fs";
|
|
5
5
|
import { join, relative } from "node:path";
|
|
6
|
-
import type { ExtensionAPI } from "@
|
|
6
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import type {
|
|
8
8
|
ServerToExtensionMessage,
|
|
9
9
|
ExtensionToServerMessage,
|
|
@@ -12,6 +12,7 @@ import { killProcessByPgid } from "./process-scanner.js";
|
|
|
12
12
|
import type { FileEntry, PiSessionInfo } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
13
13
|
import { filterHiddenCommands } from "./bridge-context.js";
|
|
14
14
|
import { expandPromptTemplateFromDisk } from "./prompt-expander.js";
|
|
15
|
+
import { tryDispatchExtensionCommand } from "./slash-dispatch.js";
|
|
15
16
|
import { buildProviderCatalogue } from "./provider-register.js";
|
|
16
17
|
|
|
17
18
|
const IGNORE_DIRS = new Set([".git", "node_modules", ".next", "dist", "build", ".cache", "__pycache__", ".venv"]);
|
|
@@ -145,6 +146,11 @@ export function createCommandHandler(
|
|
|
145
146
|
getThinkingLevel?: () => string | undefined;
|
|
146
147
|
shutdown?: () => void;
|
|
147
148
|
abort?: () => void;
|
|
149
|
+
/**
|
|
150
|
+
* Probe agent idleness for the persistent-abort scheduler.
|
|
151
|
+
* See change: fix-provider-retry-infinite-loop.
|
|
152
|
+
*/
|
|
153
|
+
isIdle?: () => boolean;
|
|
148
154
|
getCwd?: () => string;
|
|
149
155
|
/** Callback to send events (e.g., bash_output, command_feedback) back to server */
|
|
150
156
|
eventSink?: (msg: ExtensionToServerMessage) => void;
|
|
@@ -156,11 +162,44 @@ export function createCommandHandler(
|
|
|
156
162
|
spawnNew?: () => void;
|
|
157
163
|
/** Switch model via pi.setModel() */
|
|
158
164
|
setModel?: (provider: string, modelId: string) => Promise<void>;
|
|
159
|
-
/**
|
|
160
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Route slash commands through pi's command system. May be sync or async.
|
|
167
|
+
* In bridge wiring this also runs the extension-command dispatch branch
|
|
168
|
+
* (see slash-dispatch.ts). The handler awaits the result so command_feedback
|
|
169
|
+
* events emitted by the dispatch path arrive before this turn returns.
|
|
170
|
+
* See change: fix-extension-slash-commands-in-dashboard.
|
|
171
|
+
*/
|
|
172
|
+
sessionPrompt?: (text: string) => void | Promise<void>;
|
|
161
173
|
},
|
|
162
174
|
): CommandHandler {
|
|
163
175
|
const getSessionId = typeof sessionIdOrGetter === "function" ? sessionIdOrGetter : () => sessionIdOrGetter;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Persistent-abort scheduler. Re-invokes `options.abort()` at 200ms
|
|
179
|
+
* intervals for up to 2 seconds, breaking early when `options.isIdle()`
|
|
180
|
+
* returns true. Closes the retry race window in pi-coding-agent.
|
|
181
|
+
* See change: fix-provider-retry-infinite-loop.
|
|
182
|
+
*/
|
|
183
|
+
const PERSISTENT_ABORT_INTERVAL_MS = 200;
|
|
184
|
+
const PERSISTENT_ABORT_MAX_MS = 2000;
|
|
185
|
+
function schedulePersistentAbort(opts: NonNullable<typeof options>): void {
|
|
186
|
+
if (!opts.abort) return;
|
|
187
|
+
const startedAt = Date.now();
|
|
188
|
+
const interval = setInterval(() => {
|
|
189
|
+
if (Date.now() - startedAt >= PERSISTENT_ABORT_MAX_MS) {
|
|
190
|
+
clearInterval(interval);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
if (opts.isIdle?.()) {
|
|
195
|
+
clearInterval(interval);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
} catch { /* probe failure — keep trying */ }
|
|
199
|
+
try { opts.abort?.(); } catch { /* idempotent */ }
|
|
200
|
+
}, PERSISTENT_ABORT_INTERVAL_MS);
|
|
201
|
+
}
|
|
202
|
+
|
|
164
203
|
return {
|
|
165
204
|
async handle(msg: ServerToExtensionMessage): Promise<ExtensionToServerMessage | undefined> {
|
|
166
205
|
const sessionId = getSessionId();
|
|
@@ -260,19 +299,29 @@ export function createCommandHandler(
|
|
|
260
299
|
|
|
261
300
|
if (parsed.type === "slash") {
|
|
262
301
|
if (options?.sessionPrompt) {
|
|
263
|
-
|
|
302
|
+
// sessionPrompt (bridge) owns slash-dispatch + flow fast-path +
|
|
303
|
+
// template expansion. It also owns command_feedback emission for
|
|
304
|
+
// extension-command dispatch. Do NOT emit completed here — would
|
|
305
|
+
// duplicate the dispatch path's terminal event.
|
|
306
|
+
// See change: fix-extension-slash-commands-in-dashboard.
|
|
307
|
+
await options.sessionPrompt(parsed.text);
|
|
264
308
|
} else {
|
|
265
|
-
|
|
309
|
+
// Test / non-bridge callers: apply the extension-command dispatch
|
|
310
|
+
// branch inline before falling through to sendUserMessage. Keeps
|
|
311
|
+
// both call sites in lockstep per spec routing-step 9.
|
|
312
|
+
const handled = await tryDispatchExtensionCommand(
|
|
313
|
+
pi,
|
|
314
|
+
parsed.text,
|
|
315
|
+
sessionId,
|
|
316
|
+
options?.eventSink,
|
|
317
|
+
);
|
|
318
|
+
if (!handled) {
|
|
319
|
+
// sendUserMessage exempt from gating: only typed single-line
|
|
320
|
+
// slashes that are NOT extension commands reach this — i.e.
|
|
321
|
+
// skills, prompt templates, unrecognized slashes.
|
|
322
|
+
pi.sendUserMessage(parsed.text);
|
|
323
|
+
}
|
|
266
324
|
}
|
|
267
|
-
options?.eventSink?.({
|
|
268
|
-
type: "event_forward",
|
|
269
|
-
sessionId,
|
|
270
|
-
event: {
|
|
271
|
-
eventType: "command_feedback",
|
|
272
|
-
timestamp: Date.now(),
|
|
273
|
-
data: { command: parsed.text, status: "completed" },
|
|
274
|
-
},
|
|
275
|
-
});
|
|
276
325
|
return undefined;
|
|
277
326
|
}
|
|
278
327
|
|
|
@@ -280,6 +329,12 @@ export function createCommandHandler(
|
|
|
280
329
|
// Multi-line slash commands (e.g. "/skill:foo\nuser text") are classified as
|
|
281
330
|
// passthrough by parseSendPrompt to preserve images (the slash route strips them),
|
|
282
331
|
// so we expand prompt templates / skills here before sending.
|
|
332
|
+
//
|
|
333
|
+
// sendUserMessage exempt from extension-dispatch gating: this path handles
|
|
334
|
+
// multi-line slashes and image-bearing messages. Per spec, only typed
|
|
335
|
+
// single-line slash text gates through extension dispatch — multi-line and
|
|
336
|
+
// image-bearing messages go raw to the LLM as before.
|
|
337
|
+
// See change: fix-extension-slash-commands-in-dashboard.
|
|
283
338
|
let outgoing = msg.text;
|
|
284
339
|
if (outgoing.startsWith("/")) {
|
|
285
340
|
outgoing = expandPromptTemplateFromDisk(outgoing, process.cwd(), pi);
|
|
@@ -292,6 +347,31 @@ export function createCommandHandler(
|
|
|
292
347
|
if (options?.abort) {
|
|
293
348
|
options.abort();
|
|
294
349
|
}
|
|
350
|
+
// Synthesize an immediate auto_retry_end so the dashboard clears
|
|
351
|
+
// any in-flight retry banner without waiting for pi's natural
|
|
352
|
+
// auto_retry_end (which is delayed by the abortable-sleep cancel
|
|
353
|
+
// window AND, on extension API, never reaches us at all — see
|
|
354
|
+
// https://github.com/badlogic/pi-mono/discussions/2073). The
|
|
355
|
+
// reducer no-ops auto_retry_end when retryState is undefined,
|
|
356
|
+
// so this is idempotent against later events.
|
|
357
|
+
if (options?.eventSink) {
|
|
358
|
+
options.eventSink({
|
|
359
|
+
type: "event_forward",
|
|
360
|
+
sessionId,
|
|
361
|
+
event: {
|
|
362
|
+
eventType: "auto_retry_end",
|
|
363
|
+
timestamp: Date.now(),
|
|
364
|
+
data: { success: false, attempt: -1, finalError: "Aborted by user" },
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
// Persistent-abort scheduler: pi-coding-agent's _retryAbortController
|
|
369
|
+
// is briefly `undefined` between sleep-end and the next
|
|
370
|
+
// agent.continue() call. An abort that arrives in that window is
|
|
371
|
+
// a no-op against the retry. Re-invoke abort every 200ms for up
|
|
372
|
+
// to 2s, breaking early when the agent is idle.
|
|
373
|
+
// See change: fix-provider-retry-infinite-loop.
|
|
374
|
+
if (options) schedulePersistentAbort(options);
|
|
295
375
|
return undefined;
|
|
296
376
|
|
|
297
377
|
case "request_commands": {
|
|
@@ -394,7 +474,7 @@ export function createCommandHandler(
|
|
|
394
474
|
case "list_sessions": {
|
|
395
475
|
try {
|
|
396
476
|
// Dynamic import to avoid hard dependency at module load
|
|
397
|
-
const { SessionManager } = await import("@
|
|
477
|
+
const { SessionManager } = await import("@earendil-works/pi-coding-agent") as any;
|
|
398
478
|
const cwd = msg.cwd || options?.getCwd?.() || process.cwd();
|
|
399
479
|
const sessionInfos = await SessionManager.list(cwd);
|
|
400
480
|
const sessions: PiSessionInfo[] = (sessionInfos || []).map((s: any) => ({
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Flow event wiring: registers listeners for pi-flows events
|
|
3
3
|
* and forwards them as protocol messages to the dashboard server.
|
|
4
4
|
*/
|
|
5
|
-
import type { ExtensionAPI } from "@
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import type { BridgeContext } from "./bridge-context.js";
|
|
7
7
|
import { filterHiddenCommands } from "./bridge-context.js";
|
|
8
8
|
import type { FlowInfo } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
@@ -23,7 +23,7 @@ interface Item {
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Minimal shape of pi-tui's `Component` interface — we avoid importing from
|
|
26
|
-
* `@
|
|
26
|
+
* `@earendil-works/pi-tui` directly so this module stays compile-friendly when
|
|
27
27
|
* that peer dep isn't present (e.g. in unit tests running via vitest without
|
|
28
28
|
* the full pi runtime).
|
|
29
29
|
*/
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
// Ambient declarations for pi runtime packages.
|
|
2
|
-
// The actual types are provided by whichever host
|
|
2
|
+
// The actual types are provided by whichever host loads this extension.
|
|
3
3
|
// tsconfig paths handles resolution when one of the packages is installed;
|
|
4
4
|
// these declarations serve as fallback when neither is available (e.g. CI, dev without pi).
|
|
5
|
-
declare module "@
|
|
6
|
-
export type ExtensionAPI = import("@oh-my-pi/pi-coding-agent").ExtensionAPI;
|
|
7
|
-
}
|
|
8
|
-
declare module "@mariozechner/pi-ai" {
|
|
9
|
-
export function StringEnum<T extends readonly string[]>(values: T, schema?: Record<string, unknown>): any;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
declare module "@oh-my-pi/pi-coding-agent" {
|
|
5
|
+
declare module "@earendil-works/pi-coding-agent" {
|
|
13
6
|
export interface ModelRegistry {
|
|
14
7
|
getAvailable(): Array<{ provider: string; id: string }>;
|
|
15
8
|
refresh(): void;
|
|
@@ -35,3 +28,17 @@ declare module "@oh-my-pi/pi-coding-agent" {
|
|
|
35
28
|
events: EventBus;
|
|
36
29
|
}
|
|
37
30
|
}
|
|
31
|
+
|
|
32
|
+
// Legacy fork — re-exports the same ExtensionAPI shape so existing installs still type-check.
|
|
33
|
+
declare module "@mariozechner/pi-coding-agent" {
|
|
34
|
+
export type ExtensionAPI = import("@earendil-works/pi-coding-agent").ExtensionAPI;
|
|
35
|
+
export type ModelRegistry = import("@earendil-works/pi-coding-agent").ModelRegistry;
|
|
36
|
+
export type EventBus = import("@earendil-works/pi-coding-agent").EventBus;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
declare module "@earendil-works/pi-ai" {
|
|
40
|
+
export function StringEnum<T extends readonly string[]>(values: T, schema?: Record<string, unknown>): any;
|
|
41
|
+
}
|
|
42
|
+
declare module "@mariozechner/pi-ai" {
|
|
43
|
+
export function StringEnum<T extends readonly string[]>(values: T, schema?: Record<string, unknown>): any;
|
|
44
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* by reading template/skill files directly and expanding them.
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync, existsSync } from "node:fs";
|
|
9
|
-
import { dirname, join
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
10
|
import { readdirSync, statSync } from "node:fs";
|
|
11
11
|
import { buildSkillBlock } from "@blackbelt-technology/pi-dashboard-shared/skill-block-parser.js";
|
|
12
12
|
|
|
@@ -54,6 +54,69 @@ function readTemplate(filePath: string): string {
|
|
|
54
54
|
return match ? match[1].trim() : content.trim();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Build the deduped, ordered list of candidate names for `:` ↔ `-` alias resolution.
|
|
59
|
+
* Original form always comes first, preserving the user's typed punctuation as
|
|
60
|
+
* authoritative intent (see design Decision 4: original-form-first precedence).
|
|
61
|
+
*/
|
|
62
|
+
function candidateNames(name: string): string[] {
|
|
63
|
+
const variants = new Set<string>();
|
|
64
|
+
variants.add(name);
|
|
65
|
+
if (name.includes(":")) variants.add(name.replace(/:/g, "-"));
|
|
66
|
+
if (name.includes("-")) variants.add(name.replace(/-/g, ":"));
|
|
67
|
+
return [...variants];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type Resolution = {
|
|
71
|
+
filePath: string;
|
|
72
|
+
source: "prompt" | "skill";
|
|
73
|
+
resolvedName: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve `templateName` against (a) local prompt/skill scan and (b) pi.getCommands().
|
|
78
|
+
*
|
|
79
|
+
* Probe order is OUTER-loop over candidate-name variants, INNER probe over the
|
|
80
|
+
* three stores. This guarantees original-form-first precedence: every store is
|
|
81
|
+
* consulted on the typed form before any remapped variant is consulted on any
|
|
82
|
+
* store. See design Decision 4.
|
|
83
|
+
*/
|
|
84
|
+
function resolveTemplate(
|
|
85
|
+
templateName: string,
|
|
86
|
+
templates: Map<string, string>,
|
|
87
|
+
pi: any | undefined,
|
|
88
|
+
): Resolution | null {
|
|
89
|
+
for (const cand of candidateNames(templateName)) {
|
|
90
|
+
// Step 1: local-scan prompt/skill key (may be `skill:<dir>` for SKILL.md dirs).
|
|
91
|
+
const local = templates.get(cand);
|
|
92
|
+
if (local) {
|
|
93
|
+
return {
|
|
94
|
+
filePath: local,
|
|
95
|
+
source: cand.startsWith("skill:") ? "skill" : "prompt",
|
|
96
|
+
resolvedName: cand,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// Step 2: local SKILL.md directory keyed as `skill:<cand>`.
|
|
100
|
+
const localSkill = templates.get(`skill:${cand}`);
|
|
101
|
+
if (localSkill) {
|
|
102
|
+
return { filePath: localSkill, source: "skill", resolvedName: cand };
|
|
103
|
+
}
|
|
104
|
+
// Step 3: pi.getCommands() registry skill.
|
|
105
|
+
if (pi?.getCommands) {
|
|
106
|
+
try {
|
|
107
|
+
const commands = pi.getCommands();
|
|
108
|
+
const skill = commands.find(
|
|
109
|
+
(c: any) => c.name === cand && c.source === "skill" && c.path,
|
|
110
|
+
);
|
|
111
|
+
if (skill?.path && existsSync(skill.path)) {
|
|
112
|
+
return { filePath: skill.path, source: "skill", resolvedName: cand };
|
|
113
|
+
}
|
|
114
|
+
} catch { /* ignore */ }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
57
120
|
/**
|
|
58
121
|
* Expand a slash command by finding and reading the prompt template from disk.
|
|
59
122
|
* Returns the expanded text, or the original text if no template found.
|
|
@@ -73,45 +136,21 @@ export function expandPromptTemplateFromDisk(text: string, cwd: string, pi?: any
|
|
|
73
136
|
const argsString = m?.[2] ?? "";
|
|
74
137
|
|
|
75
138
|
const templates = findPromptTemplates(cwd);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// Support colon as alias for hyphen (e.g. /opsx:continue → opsx-continue)
|
|
79
|
-
if (!filePath && templateName.includes(":")) {
|
|
80
|
-
filePath = templates.get(templateName.replace(/:/g, "-"));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Fallback: check pi.getCommands() for globally installed skills and package skills
|
|
84
|
-
// that aren't in the local .pi/skills/ directory.
|
|
85
|
-
if (!filePath && pi?.getCommands) {
|
|
86
|
-
try {
|
|
87
|
-
const commands = pi.getCommands();
|
|
88
|
-
const skill = commands.find(
|
|
89
|
-
(c: any) => c.name === templateName && c.source === "skill" && c.path,
|
|
90
|
-
);
|
|
91
|
-
if (skill?.path && existsSync(skill.path)) {
|
|
92
|
-
filePath = skill.path;
|
|
93
|
-
}
|
|
94
|
-
} catch { /* ignore */ }
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (!filePath) return text;
|
|
139
|
+
const resolution = resolveTemplate(templateName, templates, pi);
|
|
140
|
+
if (!resolution) return text;
|
|
98
141
|
|
|
99
142
|
try {
|
|
100
|
-
const content = readTemplate(filePath);
|
|
143
|
+
const content = readTemplate(resolution.filePath);
|
|
101
144
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// See change: render-skill-invocations-collapsibly.
|
|
108
|
-
const isSkill = isSkillResolution(templateName, filePath, pi);
|
|
109
|
-
if (isSkill) {
|
|
110
|
-
const bareName = templateName.replace(/^skill:/, "");
|
|
145
|
+
if (resolution.source === "skill") {
|
|
146
|
+
// Strip leading `skill:` prefix (only present for local-scan step-1 hits
|
|
147
|
+
// whose key was `skill:<dir>`); registry hits and step-2 hits already
|
|
148
|
+
// hold the bare name.
|
|
149
|
+
const bareName = resolution.resolvedName.replace(/^skill:/, "");
|
|
111
150
|
return buildSkillBlock({
|
|
112
151
|
name: bareName,
|
|
113
|
-
filePath,
|
|
114
|
-
baseDir: dirname(filePath),
|
|
152
|
+
filePath: resolution.filePath,
|
|
153
|
+
baseDir: dirname(resolution.filePath),
|
|
115
154
|
body: content,
|
|
116
155
|
userArgs: argsString || undefined,
|
|
117
156
|
});
|
|
@@ -126,31 +165,3 @@ export function expandPromptTemplateFromDisk(text: string, cwd: string, pi?: any
|
|
|
126
165
|
return text;
|
|
127
166
|
}
|
|
128
167
|
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Detect whether the resolved `filePath` came from a skill source.
|
|
132
|
-
*
|
|
133
|
-
* The local-scan key tells us directly when it starts with `skill:`. For the
|
|
134
|
-
* pi.getCommands() fallback we re-query and check `source === "skill"` against
|
|
135
|
-
* the same templateName.
|
|
136
|
-
*/
|
|
137
|
-
function isSkillResolution(
|
|
138
|
-
templateName: string,
|
|
139
|
-
filePath: string,
|
|
140
|
-
pi: any | undefined,
|
|
141
|
-
): boolean {
|
|
142
|
-
if (templateName.startsWith("skill:")) return true;
|
|
143
|
-
// The colon-alias path (e.g. /opsx:continue) maps to a hyphen filename and is
|
|
144
|
-
// a prompt template, not a skill. Skills always use the `skill:` prefix in
|
|
145
|
-
// both the local scan and pi.getCommands().
|
|
146
|
-
if (!pi?.getCommands) return false;
|
|
147
|
-
try {
|
|
148
|
-
const commands = pi.getCommands();
|
|
149
|
-
const match = commands.find(
|
|
150
|
-
(c: any) => c.name === templateName && c.source === "skill" && c.path === filePath,
|
|
151
|
-
);
|
|
152
|
-
return !!match;
|
|
153
|
-
} catch {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
}
|