@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
|
@@ -5,6 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import { loadConfig, type DashboardConfig, type AuthConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
8
|
+
import { refreshModelRegistry } from "./model-proxy/registry-singleton.js";
|
|
8
9
|
|
|
9
10
|
const REDACTED = "***";
|
|
10
11
|
|
|
@@ -114,9 +115,17 @@ export function writeConfigPartial(partial: Record<string, any>): WriteConfigRes
|
|
|
114
115
|
partial.auth = mergedAuth;
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
// Merge tunnel sub-object
|
|
118
|
+
// Merge tunnel sub-object (deep-merge nested watchdog)
|
|
118
119
|
if (partial.tunnel) {
|
|
119
|
-
|
|
120
|
+
const existingTunnel = existing.tunnel ?? {};
|
|
121
|
+
const mergedWatchdog = partial.tunnel.watchdog
|
|
122
|
+
? { ...(existingTunnel.watchdog ?? {}), ...partial.tunnel.watchdog }
|
|
123
|
+
: existingTunnel.watchdog;
|
|
124
|
+
partial.tunnel = {
|
|
125
|
+
...existingTunnel,
|
|
126
|
+
...partial.tunnel,
|
|
127
|
+
...(mergedWatchdog ? { watchdog: mergedWatchdog } : {}),
|
|
128
|
+
};
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
// Merge memoryLimits sub-object
|
|
@@ -139,6 +148,9 @@ export function writeConfigPartial(partial: Record<string, any>): WriteConfigRes
|
|
|
139
148
|
fs.mkdirSync(dir, { recursive: true });
|
|
140
149
|
fs.writeFileSync(file, JSON.stringify(merged, null, 2) + "\n");
|
|
141
150
|
|
|
151
|
+
// Eager-refresh model proxy registry (config may affect proxy settings).
|
|
152
|
+
refreshModelRegistry().catch(() => {});
|
|
153
|
+
|
|
142
154
|
return { success: true, restartRequired };
|
|
143
155
|
} catch (err: any) {
|
|
144
156
|
return { success: false, restartRequired: false, error: err.message };
|
|
@@ -76,6 +76,23 @@ export function hasOpenSpecDir(cwd: string): boolean {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* `true` iff `<cwd>/openspec/` exists and is a directory. Strictly weaker than
|
|
81
|
+
* `hasOpenSpecDir` (which also requires the `changes/` subdir). Used by the
|
|
82
|
+
* WS on-connect snapshot to emit the new `hasOpenspecDir` field on every
|
|
83
|
+
* payload so freshly-initialized projects without `changes/` yet still surface
|
|
84
|
+
* the OPENSPEC subcard as an init/attach affordance on the client.
|
|
85
|
+
*
|
|
86
|
+
* See change: auto-hide-empty-session-subcards.
|
|
87
|
+
*/
|
|
88
|
+
export function hasOpenSpecRoot(cwd: string): boolean {
|
|
89
|
+
try {
|
|
90
|
+
return fs.statSync(path.join(cwd, "openspec")).isDirectory();
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
export interface DirectoryService {
|
|
80
97
|
knownDirectories(): string[];
|
|
81
98
|
discoverSessions(cwd: string): DiscoveredSession[];
|
|
@@ -199,12 +216,25 @@ function emptyDirCache(): DirCache {
|
|
|
199
216
|
return { listMtimeMs: undefined, listResult: undefined, changes: new Map(), data: undefined };
|
|
200
217
|
}
|
|
201
218
|
|
|
219
|
+
export interface DirectoryServiceOptions {
|
|
220
|
+
/**
|
|
221
|
+
* Optional async post-processor applied to `OpenSpecData` after
|
|
222
|
+
* `buildOpenSpecData` and before caching. Used to inject the per-cwd
|
|
223
|
+
* `groupId` join from the OpenSpec change-grouping store.
|
|
224
|
+
* Errors propagate as a logged warning + the unenriched data.
|
|
225
|
+
* See change: add-openspec-change-grouping.
|
|
226
|
+
*/
|
|
227
|
+
enrichOpenSpecData?: (cwd: string, data: OpenSpecData) => Promise<OpenSpecData> | OpenSpecData;
|
|
228
|
+
}
|
|
229
|
+
|
|
202
230
|
export function createDirectoryService(
|
|
203
231
|
preferencesStore: PreferencesStore,
|
|
204
232
|
sessionManager: SessionManager,
|
|
205
233
|
initialConfig?: Partial<OpenSpecPollConfig>,
|
|
234
|
+
options: DirectoryServiceOptions = {},
|
|
206
235
|
): DirectoryService {
|
|
207
236
|
let cfg: OpenSpecPollConfig = { ...DEFAULT_OPENSPEC_POLL, ...(initialConfig ?? {}) };
|
|
237
|
+
const enrichOpenSpecData = options.enrichOpenSpecData;
|
|
208
238
|
|
|
209
239
|
const caches = new Map<string, DirCache>();
|
|
210
240
|
const piResourcesCache = new Map<string, PiResourcesResult>();
|
|
@@ -261,12 +291,22 @@ export function createDirectoryService(
|
|
|
261
291
|
const cache = caches.get(cwd) ?? emptyDirCache();
|
|
262
292
|
const gateEnabled = cfg.changeDetection === "mtime" && !force;
|
|
263
293
|
|
|
294
|
+
const openspecRoot = path.join(cwd, "openspec");
|
|
264
295
|
const changesRoot = path.join(cwd, "openspec", "changes");
|
|
296
|
+
// `hasOpenspecDir` is strictly weaker than `initialized`: it's true when
|
|
297
|
+
// the project is OpenSpec-initialized (`openspec/` dir exists) even if no
|
|
298
|
+
// proposals are authored yet (no `openspec/changes/` subdir). The session
|
|
299
|
+
// card visibility gate uses this signal so a fresh `openspec init` project
|
|
300
|
+
// still shows the OPENSPEC subcard as an init/attach affordance.
|
|
301
|
+
// See change: auto-hide-empty-session-subcards.
|
|
302
|
+
const hasOpenspecDir = statMtimeOr(openspecRoot) !== undefined;
|
|
265
303
|
const rootMtime = statMtimeOr(changesRoot);
|
|
266
304
|
|
|
267
|
-
// If the
|
|
305
|
+
// If the changes/ subdirectory doesn't exist, short-circuit (matches old
|
|
306
|
+
// behavior re: list polling). `hasOpenspecDir` still carries the broader
|
|
307
|
+
// "is this an OpenSpec project?" signal for the client.
|
|
268
308
|
if (rootMtime === undefined) {
|
|
269
|
-
const empty: OpenSpecData = { initialized: false, changes: [] };
|
|
309
|
+
const empty: OpenSpecData = { initialized: false, changes: [], hasOpenspecDir };
|
|
270
310
|
cache.data = empty;
|
|
271
311
|
cache.listMtimeMs = undefined;
|
|
272
312
|
cache.listResult = undefined;
|
|
@@ -294,7 +334,7 @@ export function createDirectoryService(
|
|
|
294
334
|
if (!listCacheValid) {
|
|
295
335
|
const raw = await semaphore.run(() => runOpenSpecList(cwd));
|
|
296
336
|
if (!raw || !Array.isArray(raw.changes)) {
|
|
297
|
-
const empty: OpenSpecData = { initialized: false, changes: [] };
|
|
337
|
+
const empty: OpenSpecData = { initialized: false, changes: [], hasOpenspecDir };
|
|
298
338
|
cache.data = empty;
|
|
299
339
|
cache.listMtimeMs = rootMtime;
|
|
300
340
|
cache.listResult = undefined;
|
|
@@ -377,12 +417,30 @@ export function createDirectoryService(
|
|
|
377
417
|
}));
|
|
378
418
|
|
|
379
419
|
// ── Step 3: build + cache + return ──
|
|
380
|
-
|
|
420
|
+
let data = buildOpenSpecData(
|
|
381
421
|
{ changes: listResult ?? [] },
|
|
382
422
|
statusResults,
|
|
383
423
|
createFsProbeFactory(cwd),
|
|
384
424
|
createFsSpecsProbeFactory(cwd),
|
|
385
425
|
);
|
|
426
|
+
// `hasOpenspecDir` is true here by definition: we only reach Step 3 when
|
|
427
|
+
// `<cwd>/openspec/changes/` exists, which implies `<cwd>/openspec/` exists.
|
|
428
|
+
// See change: auto-hide-empty-session-subcards.
|
|
429
|
+
data = { ...data, hasOpenspecDir };
|
|
430
|
+
if (enrichOpenSpecData) {
|
|
431
|
+
try {
|
|
432
|
+
data = await enrichOpenSpecData(cwd, data);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
// Don\'t fail the whole poll if the enricher (e.g. group-store read)
|
|
435
|
+
// throws — log and continue with the unenriched data. See change:
|
|
436
|
+
// add-openspec-change-grouping.
|
|
437
|
+
// eslint-disable-next-line no-console
|
|
438
|
+
if (typeof process !== "undefined" && /pi-dashboard|openspec-poll/.test(process.env?.DEBUG ?? "")) {
|
|
439
|
+
// eslint-disable-next-line no-console
|
|
440
|
+
console.warn(`[directory-service] enrichOpenSpecData(${cwd}) threw:`, err);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
386
444
|
|
|
387
445
|
// Stamp the cache with the pre-call mtime — i.e. the mtime that
|
|
388
446
|
// demonstrably reflects the file state observed by the CLI. Skip racy
|
|
@@ -401,6 +459,20 @@ export function createDirectoryService(
|
|
|
401
459
|
}
|
|
402
460
|
|
|
403
461
|
async function refreshOpenSpec(cwd: string): Promise<OpenSpecData> {
|
|
462
|
+
// Master gate: when `openspec.enabled` is false, every refresh path is a
|
|
463
|
+
// no-op. Return the cleared-state shape so callers (including
|
|
464
|
+
// `openspec_refresh` browser handler) converge to the disabled UX.
|
|
465
|
+
// See change: auto-hide-empty-session-subcards.
|
|
466
|
+
if (cfg.enabled === false) {
|
|
467
|
+
// Disabled state: `hasOpenspecDir: false` ensures the client wrapper
|
|
468
|
+
// hides the OPENSPEC subcard for every cwd. See change:
|
|
469
|
+
// auto-hide-empty-session-subcards.
|
|
470
|
+
const cleared: OpenSpecData = { initialized: false, pending: false, changes: [], hasOpenspecDir: false };
|
|
471
|
+
const cache = caches.get(cwd) ?? emptyDirCache();
|
|
472
|
+
cache.data = cleared;
|
|
473
|
+
caches.set(cwd, cache);
|
|
474
|
+
return cleared;
|
|
475
|
+
}
|
|
404
476
|
try {
|
|
405
477
|
// User-initiated refresh bypasses the gate. The gate is heuristic; the
|
|
406
478
|
// CLI is authoritative. When the user clicks the OpenSpec refresh icon
|
|
@@ -422,6 +494,15 @@ export function createDirectoryService(
|
|
|
422
494
|
}
|
|
423
495
|
|
|
424
496
|
async function pollDirectoryGated(cwd: string): Promise<OpenSpecData> {
|
|
497
|
+
// Master gate: when `openspec.enabled` is false, never spawn a CLI for the
|
|
498
|
+
// periodic poll path either. See change: auto-hide-empty-session-subcards.
|
|
499
|
+
if (cfg.enabled === false) {
|
|
500
|
+
const cleared: OpenSpecData = { initialized: false, pending: false, changes: [], hasOpenspecDir: false };
|
|
501
|
+
const cache = caches.get(cwd) ?? emptyDirCache();
|
|
502
|
+
cache.data = cleared;
|
|
503
|
+
caches.set(cwd, cache);
|
|
504
|
+
return cleared;
|
|
505
|
+
}
|
|
425
506
|
return pollOne(cwd, false);
|
|
426
507
|
}
|
|
427
508
|
|
|
@@ -439,6 +520,9 @@ export function createDirectoryService(
|
|
|
439
520
|
let openspecTickInFlight = false;
|
|
440
521
|
async function scheduleOpenSpecTick() {
|
|
441
522
|
if (openspecTickInFlight) return;
|
|
523
|
+
// Master gate: when `openspec.enabled` is false, the tick is a no-op.
|
|
524
|
+
// No CLI spawns. See change: auto-hide-empty-session-subcards.
|
|
525
|
+
if (cfg.enabled === false) return;
|
|
442
526
|
openspecTickInFlight = true;
|
|
443
527
|
const tickStart = Date.now();
|
|
444
528
|
let spawnsBefore = 0;
|
|
@@ -542,12 +626,30 @@ export function createDirectoryService(
|
|
|
542
626
|
|
|
543
627
|
reconfigurePolling(newCfg: OpenSpecPollConfig) {
|
|
544
628
|
const oldInterval = cfg.pollIntervalSeconds;
|
|
629
|
+
const wasEnabled = cfg.enabled;
|
|
545
630
|
cfg = { ...newCfg };
|
|
546
631
|
semaphore.setMax(cfg.maxConcurrentSpawns);
|
|
547
632
|
// Only re-install timers if they were running and the interval actually changed.
|
|
548
633
|
if (pollTimer && oldInterval !== cfg.pollIntervalSeconds) {
|
|
549
634
|
installTimers();
|
|
550
635
|
}
|
|
636
|
+
// On the `true → false` transition, clear every per-cwd `OpenSpecData`
|
|
637
|
+
// cache and notify the broadcast channel so connected browsers converge
|
|
638
|
+
// to the disabled-state shape. The `false → true` transition is a
|
|
639
|
+
// no-op here — the next regular poll tick will re-populate caches.
|
|
640
|
+
// See change: auto-hide-empty-session-subcards.
|
|
641
|
+
if (wasEnabled !== false && cfg.enabled === false) {
|
|
642
|
+
const cleared: OpenSpecData = { initialized: false, pending: false, changes: [], hasOpenspecDir: false };
|
|
643
|
+
for (const [cwd, cache] of caches.entries()) {
|
|
644
|
+
cache.data = cleared;
|
|
645
|
+
caches.set(cwd, cache);
|
|
646
|
+
try {
|
|
647
|
+
onChangeCallback?.(cwd, cleared);
|
|
648
|
+
} catch (err) {
|
|
649
|
+
console.warn(`[openspec-poll] onChange after disable failed for ${cwd}:`, err);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
551
653
|
},
|
|
552
654
|
|
|
553
655
|
async onDirectoryAdded(cwd: string): Promise<DirectoryAddedResult> {
|
|
@@ -19,6 +19,8 @@ import type { DashboardSession } from "@blackbelt-technology/pi-dashboard-shared
|
|
|
19
19
|
import { detectOpenSpecActivity, isValidOpenSpecChangeSlug } from "@blackbelt-technology/pi-dashboard-shared/openspec-activity-detector.js";
|
|
20
20
|
import { extractTurnStats } from "@blackbelt-technology/pi-dashboard-shared/stats-extractor.js";
|
|
21
21
|
import { attachRenameTarget, isNameAutoSetFromAttachment } from "./proposal-attach-naming.js";
|
|
22
|
+
import { handleDispatchExtensionCommand } from "./rpc-keeper/dispatch-router.js";
|
|
23
|
+
import { keeperOptsFromSpawnResult } from "./headless-pid-registry.js";
|
|
22
24
|
|
|
23
25
|
export interface EventWiringDeps {
|
|
24
26
|
sessionManager: SessionManager;
|
|
@@ -43,6 +45,14 @@ export interface EventWiringDeps {
|
|
|
43
45
|
* See change: session-card-unread-stripes.
|
|
44
46
|
*/
|
|
45
47
|
viewedSessionTracker?: ViewedSessionTracker;
|
|
48
|
+
/**
|
|
49
|
+
* Optional client-correlation registry. When provided, the wiring
|
|
50
|
+
* consumes the requestId for the resolved spawnToken after a successful
|
|
51
|
+
* three-tier link and surfaces it on `session_added` as `spawnRequestId`,
|
|
52
|
+
* letting the client auto-select / dismiss its placeholder by exact
|
|
53
|
+
* correlation. See change: spawn-correlation-token.
|
|
54
|
+
*/
|
|
55
|
+
pendingClientCorrelations?: import("./pending-client-correlations.js").PendingClientCorrelations;
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
/**
|
|
@@ -62,6 +72,7 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
62
72
|
pendingDashboardSpawns,
|
|
63
73
|
pendingAttachRegistry,
|
|
64
74
|
viewedSessionTracker,
|
|
75
|
+
pendingClientCorrelations,
|
|
65
76
|
} = deps;
|
|
66
77
|
|
|
67
78
|
// Broadcast placeholder session to browsers when auto-created from early events
|
|
@@ -452,7 +463,34 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
452
463
|
}
|
|
453
464
|
}
|
|
454
465
|
|
|
455
|
-
|
|
466
|
+
// Three-tier link: token → pid → cwd-FIFO. Each tier is independently
|
|
467
|
+
// correct; `linkByToken` is the strong identity introduced by
|
|
468
|
+
// `spawn-correlation-token`. cwd-FIFO is the legacy fallback for old
|
|
469
|
+
// bridges that send neither token nor pid (and is logged so we can see
|
|
470
|
+
// when it actually triggers).
|
|
471
|
+
let linked = false;
|
|
472
|
+
if (msg.spawnToken) {
|
|
473
|
+
linked = browserGateway.headlessPidRegistry.linkByToken(msg.spawnToken, sessionId, msg.pid);
|
|
474
|
+
}
|
|
475
|
+
if (!linked && msg.pid !== undefined) {
|
|
476
|
+
linked = browserGateway.headlessPidRegistry.linkByPid(sessionId, msg.pid);
|
|
477
|
+
}
|
|
478
|
+
if (!linked) {
|
|
479
|
+
if (msg.spawnToken || msg.pid !== undefined) {
|
|
480
|
+
console.error(
|
|
481
|
+
`[event-wiring] cwd-FIFO fallback for session ${sessionId} — token=${msg.spawnToken ?? ""} pid=${msg.pid ?? ""} cwd=${msg.cwd}`,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
browserGateway.headlessPidRegistry.linkSession(sessionId, msg.cwd);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Resolve the originating browser `requestId` (when known) so the
|
|
488
|
+
// upcoming session_added broadcast can carry spawnRequestId and the
|
|
489
|
+
// client can auto-select / dismiss its placeholder.
|
|
490
|
+
// See change: spawn-correlation-token.
|
|
491
|
+
const spawnRequestId = (msg.spawnToken && pendingClientCorrelations)
|
|
492
|
+
? pendingClientCorrelations.consume(msg.spawnToken)
|
|
493
|
+
: undefined;
|
|
456
494
|
|
|
457
495
|
const isNewSession = !knownSessionIds.has(sessionId);
|
|
458
496
|
knownSessionIds.add(sessionId);
|
|
@@ -469,7 +507,11 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
469
507
|
}
|
|
470
508
|
}
|
|
471
509
|
|
|
472
|
-
|
|
510
|
+
// Fork-parent lookup is keyed by spawn token (was: cwd, racy on
|
|
511
|
+
// multi-fork-in-same-cwd). See change: spawn-correlation-token.
|
|
512
|
+
const forkParent = msg.spawnToken
|
|
513
|
+
? pendingForkRegistry.consumeFork(msg.spawnToken)
|
|
514
|
+
: undefined;
|
|
473
515
|
sessionOrderManager.insert(msg.cwd, sessionId);
|
|
474
516
|
|
|
475
517
|
if (forkParent) {
|
|
@@ -489,7 +531,7 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
489
531
|
|
|
490
532
|
const updatedSession = sessionManager.get(sessionId);
|
|
491
533
|
if (updatedSession) {
|
|
492
|
-
browserGateway.broadcastSessionAdded(updatedSession);
|
|
534
|
+
browserGateway.broadcastSessionAdded(updatedSession, spawnRequestId ? { spawnRequestId } : undefined);
|
|
493
535
|
}
|
|
494
536
|
|
|
495
537
|
const isNewCwd = !sessionManager.listAll().some(
|
|
@@ -623,9 +665,23 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
623
665
|
if (msg.type === "providers_list") {
|
|
624
666
|
// Cache the bridge-pushed catalogue. Browsers don't subscribe to it
|
|
625
667
|
// directly; they read via GET /api/provider-auth/status.
|
|
626
|
-
//
|
|
668
|
+
// Broadcast `models_refreshed` ONLY when the catalogue contents
|
|
669
|
+
// actually changed. Routine state-syncs (every fork/resume/reconnect/
|
|
670
|
+
// subscribe) re-send identical content; broadcasting unconditionally
|
|
671
|
+
// wipes every browser's modelsMap and — because App.tsx's
|
|
672
|
+
// auto-subscribe effect skips re-requesting models for any session
|
|
673
|
+
// that's already in `subscribedRef`, leaves previously-visited
|
|
674
|
+
// sessions with an empty model selector until reconnect.
|
|
675
|
+
//
|
|
676
|
+
// The catalogue cache is now a pure read consumer for the Settings
|
|
677
|
+
// UI (`GET /api/provider-auth/status`). No broadcast: the model-
|
|
678
|
+
// selector dropdown lives on the independent `models_list` channel
|
|
679
|
+
// which is per-session-broadcast already; per-session updates are
|
|
680
|
+
// self-healing without a global wipe.
|
|
681
|
+
// See changes: replace-hardcoded-provider-lists,
|
|
682
|
+
// fix-providers-list-spurious-models-refreshed,
|
|
683
|
+
// simplify-model-selection-channels.
|
|
627
684
|
setCatalogueForSession(sessionId, msg.providers);
|
|
628
|
-
browserGateway.broadcastToAll({ type: "models_refreshed" } as any);
|
|
629
685
|
}
|
|
630
686
|
|
|
631
687
|
if (msg.type === "roles_list") {
|
|
@@ -757,7 +813,13 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
757
813
|
if (msg.type === "spawn_new_session") {
|
|
758
814
|
spawnPiSession(msg.cwd, { strategy: loadConfig().spawnStrategy }).then((result) => {
|
|
759
815
|
if (result.process && result.pid) {
|
|
760
|
-
browserGateway.headlessPidRegistry.register(
|
|
816
|
+
browserGateway.headlessPidRegistry.register(
|
|
817
|
+
result.pid,
|
|
818
|
+
msg.cwd,
|
|
819
|
+
result.process,
|
|
820
|
+
result.spawnToken,
|
|
821
|
+
keeperOptsFromSpawnResult(result),
|
|
822
|
+
);
|
|
761
823
|
}
|
|
762
824
|
browserGateway.broadcastToAll({
|
|
763
825
|
type: "spawn_result",
|
|
@@ -808,5 +870,27 @@ export function wireEvents(deps: EventWiringDeps): void {
|
|
|
808
870
|
});
|
|
809
871
|
}
|
|
810
872
|
|
|
873
|
+
// RPC keeper dispatch: bridge → server slash command forward.
|
|
874
|
+
// Fire-and-forget; the handler itself emits browser-bound
|
|
875
|
+
// `command_feedback` events on success and on every failure path.
|
|
876
|
+
// The terminal event is persisted via eventStore.insertEvent so it
|
|
877
|
+
// survives browser reattach (otherwise the chat pill stays "in progress").
|
|
878
|
+
// See change: add-rpc-stdin-dispatch-with-keeper-sidecar (Phase 8).
|
|
879
|
+
if (msg.type === "dispatch_extension_command") {
|
|
880
|
+
void handleDispatchExtensionCommand(msg, {
|
|
881
|
+
headlessPidRegistry: browserGateway.headlessPidRegistry,
|
|
882
|
+
emitCommandFeedback: (sid, command, status, message) => {
|
|
883
|
+
const event = {
|
|
884
|
+
eventType: "command_feedback",
|
|
885
|
+
timestamp: Date.now(),
|
|
886
|
+
data: message === undefined ? { command, status } : { command, status, message },
|
|
887
|
+
};
|
|
888
|
+
const seq = eventStore.insertEvent(sid, event);
|
|
889
|
+
const stored = eventStore.getEvent(sid, seq) ?? event;
|
|
890
|
+
browserGateway.broadcastEvent(sid, seq, stored);
|
|
891
|
+
},
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
|
|
811
895
|
};
|
|
812
896
|
}
|