@blackbelt-technology/pi-agent-dashboard 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +79 -32
- package/README.md +7 -3
- package/docs/architecture.md +361 -12
- package/package.json +7 -7
- package/packages/extension/package.json +7 -2
- package/packages/extension/src/__tests__/ask-user-schema-discriminator.test.ts +141 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +51 -7
- package/packages/extension/src/__tests__/multiselect-dashboard-routing.test.ts +203 -0
- package/packages/extension/src/__tests__/multiselect-polyfill.test.ts +92 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +81 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +37 -0
- package/packages/extension/src/__tests__/ui-decorators.test.ts +309 -0
- package/packages/extension/src/__tests__/ui-modules.test.ts +293 -0
- package/packages/extension/src/ask-user-tool.ts +165 -57
- package/packages/extension/src/bridge.ts +97 -4
- package/packages/extension/src/multiselect-decode.ts +40 -0
- package/packages/extension/src/multiselect-polyfill.ts +38 -8
- package/packages/extension/src/ui-modules.ts +272 -0
- package/packages/server/package.json +9 -3
- package/packages/server/src/__tests__/auto-attach.test.ts +61 -8
- package/packages/server/src/__tests__/browse-endpoint.test.ts +295 -19
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +36 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +163 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +315 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +303 -0
- package/packages/server/src/__tests__/directory-service.test.ts +174 -0
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +225 -0
- package/packages/server/src/__tests__/package-manager-wrapper-move.test.ts +414 -0
- package/packages/server/src/__tests__/package-routes.test.ts +136 -3
- package/packages/server/src/__tests__/package-source-helpers.test.ts +101 -0
- package/packages/server/src/__tests__/pending-attach-registry.test.ts +123 -0
- package/packages/server/src/__tests__/pending-resume-intent-registry.test.ts +138 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +73 -30
- package/packages/server/src/__tests__/pi-gateway-consume-pending-attach.test.ts +112 -0
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +180 -0
- package/packages/server/src/__tests__/post-install-rescan.test.ts +134 -0
- package/packages/server/src/__tests__/proposal-attach-naming.test.ts +79 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-with-attach.test.ts +108 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +55 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +242 -0
- package/packages/server/src/__tests__/session-scanner.test.ts +44 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +40 -0
- package/packages/server/src/__tests__/translate-path-source.test.ts +77 -0
- package/packages/server/src/__tests__/ui-decorators-replay.test.ts +209 -0
- package/packages/server/src/__tests__/ui-modules-replay.test.ts +221 -0
- package/packages/server/src/browse.ts +118 -13
- package/packages/server/src/browser-gateway.ts +19 -0
- package/packages/server/src/browser-handlers/__tests__/session-meta-handler.test.ts +183 -0
- package/packages/server/src/browser-handlers/directory-handler.ts +7 -1
- package/packages/server/src/browser-handlers/handler-context.ts +15 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +29 -3
- package/packages/server/src/browser-handlers/session-meta-handler.ts +46 -12
- package/packages/server/src/browser-handlers/subscription-handler.ts +46 -1
- package/packages/server/src/cli.ts +5 -6
- package/packages/server/src/directory-service.ts +156 -15
- package/packages/server/src/event-wiring.ts +111 -10
- package/packages/server/src/installed-package-enricher.ts +143 -0
- package/packages/server/src/package-manager-wrapper.ts +305 -8
- package/packages/server/src/package-source-helpers.ts +104 -0
- package/packages/server/src/pending-attach-registry.ts +112 -0
- package/packages/server/src/pending-resume-intent-registry.ts +107 -0
- package/packages/server/src/pi-core-checker.ts +9 -14
- package/packages/server/src/pi-gateway.ts +14 -0
- package/packages/server/src/proposal-attach-naming.ts +47 -0
- package/packages/server/src/routes/file-routes.ts +29 -3
- package/packages/server/src/routes/package-routes.ts +72 -3
- package/packages/server/src/routes/plugin-config-routes.ts +129 -0
- package/packages/server/src/routes/system-routes.ts +2 -0
- package/packages/server/src/server.ts +339 -10
- package/packages/server/src/session-api.ts +30 -5
- package/packages/server/src/session-order-manager.ts +22 -0
- package/packages/server/src/session-scanner.ts +10 -1
- package/packages/shared/package.json +9 -2
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +59 -0
- package/packages/shared/src/__tests__/config-plugins.test.ts +68 -0
- package/packages/shared/src/__tests__/extension-ui-module-shape.test.ts +265 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +81 -0
- package/packages/shared/src/__tests__/openspec-design-evidence.test.ts +288 -0
- package/packages/shared/src/__tests__/openspec-effective-status-script.test.ts +174 -0
- package/packages/shared/src/__tests__/openspec-poller-design-override.test.ts +225 -0
- package/packages/shared/src/__tests__/openspec-poller-specs-override.test.ts +284 -0
- package/packages/shared/src/__tests__/openspec-specs-evidence.test.ts +144 -0
- package/packages/shared/src/__tests__/platform/is-appimage-self-hit.test.ts +164 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +72 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +113 -0
- package/packages/shared/src/__tests__/plugin-config-update-protocol.test.ts +41 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +5 -1
- package/packages/shared/src/__tests__/spawn-session-attach-proposal.test.ts +47 -0
- package/packages/shared/src/__tests__/tool-registry-strategies-appimage.test.ts +118 -0
- package/packages/shared/src/browser-protocol.ts +110 -4
- package/packages/shared/src/config.ts +45 -0
- package/packages/shared/src/dashboard-plugin/index.ts +11 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +58 -0
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +26 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +92 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +151 -0
- package/packages/shared/src/openspec-activity-detector.ts +18 -22
- package/packages/shared/src/openspec-design-evidence.ts +109 -0
- package/packages/shared/src/openspec-poller.ts +117 -3
- package/packages/shared/src/openspec-specs-evidence.ts +79 -0
- package/packages/shared/src/platform/binary-lookup.ts +96 -1
- package/packages/shared/src/plugin-bridge-register.ts +139 -0
- package/packages/shared/src/protocol.ts +56 -2
- package/packages/shared/src/recommended-extensions.ts +7 -1
- package/packages/shared/src/rest-api.ts +68 -3
- package/packages/shared/src/state-replay.ts +11 -1
- package/packages/shared/src/tool-registry/strategies.ts +17 -3
- package/packages/shared/src/types.ts +160 -0
|
@@ -18,6 +18,8 @@ import { createPreferencesStore, type PreferencesStore } from "./preferences-sto
|
|
|
18
18
|
import { createMetaPersistence, type MetaPersistence } from "./meta-persistence.js";
|
|
19
19
|
import { createSessionOrderManager, type SessionOrderManager } from "./session-order-manager.js";
|
|
20
20
|
import { createPendingForkRegistry, type PendingForkRegistry } from "./pending-fork-registry.js";
|
|
21
|
+
import { createPendingAttachRegistry } from "./pending-attach-registry.js";
|
|
22
|
+
import { createPendingResumeIntentRegistry } from "./pending-resume-intent-registry.js";
|
|
21
23
|
|
|
22
24
|
// pending-load-manager removed — server loads sessions directly via DirectoryService
|
|
23
25
|
import { createDirectoryService, type DirectoryService } from "./directory-service.js";
|
|
@@ -35,6 +37,7 @@ import { registerAuthPlugin, validateWsUpgrade } from "./auth-plugin.js";
|
|
|
35
37
|
import { findBundledExtension, registerBridgeExtension } from "@blackbelt-technology/pi-dashboard-shared/bridge-register.js";
|
|
36
38
|
import { createNetworkGuard, isLoopback, isBypassedHost } from "./localhost-guard.js";
|
|
37
39
|
import type { AuthConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
40
|
+
import { loadConfig, CONFIG_FILE } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
38
41
|
import { registerSessionApi } from "./session-api.js";
|
|
39
42
|
import { registerSessionRoutes } from "./routes/session-routes.js";
|
|
40
43
|
import { registerGitRoutes } from "./routes/git-routes.js";
|
|
@@ -59,6 +62,11 @@ import { createEditorManager, type EditorManager } from "./editor-manager.js";
|
|
|
59
62
|
import { createEditorPidRegistry } from "./editor-pid-registry.js";
|
|
60
63
|
import { registerEditorRoutes } from "./routes/editor-routes.js";
|
|
61
64
|
import { registerKnownServersRoutes } from "./routes/known-servers-routes.js";
|
|
65
|
+
import { registerPluginConfigRoutes } from "./routes/plugin-config-routes.js";
|
|
66
|
+
import { loadServerEntries, discoverPlugins, getPluginStatusStore } from "@blackbelt-technology/dashboard-plugin-runtime/server";
|
|
67
|
+
import { createServerPluginContext } from "@blackbelt-technology/dashboard-plugin-runtime/server";
|
|
68
|
+
import { getPluginConfig as getPluginConfigFromFile } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
69
|
+
import { registerAllPluginBridges } from "@blackbelt-technology/pi-dashboard-shared/plugin-bridge-register.js";
|
|
62
70
|
import { registerEditorProxy, handleEditorUpgrade } from "./editor-proxy.js";
|
|
63
71
|
import { detectCodeServerBinary } from "./editor-detection.js";
|
|
64
72
|
|
|
@@ -107,6 +115,128 @@ export interface DashboardServer {
|
|
|
107
115
|
piPort(): number | null;
|
|
108
116
|
}
|
|
109
117
|
|
|
118
|
+
// ── Post-install repair (centralized hook) ─────────────────────────
|
|
119
|
+
// On every `installing → ready` bootstrap-state transition the server
|
|
120
|
+
// re-runs the full ToolRegistry rescan, force-refreshes OpenSpec data
|
|
121
|
+
// for every known directory, and refreshes pi-resources. Without this
|
|
122
|
+
// the OpenSpec session-card buttons stay hidden until the next gated
|
|
123
|
+
// poll tick (or never, if the gate's mtime heuristic declines).
|
|
124
|
+
// See change: fix-openspec-buttons-after-bootstrap-install.
|
|
125
|
+
|
|
126
|
+
import type { OpenSpecData } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
127
|
+
import type { ServerToBrowserMessage } from "@blackbelt-technology/pi-dashboard-shared/browser-protocol.js";
|
|
128
|
+
|
|
129
|
+
export interface PostInstallRepairDeps {
|
|
130
|
+
registry: { rescan(name?: string): void };
|
|
131
|
+
directoryService: {
|
|
132
|
+
knownDirectories(): string[];
|
|
133
|
+
getOpenSpecData(cwd: string): OpenSpecData | undefined;
|
|
134
|
+
refreshOpenSpec(cwd: string): Promise<OpenSpecData>;
|
|
135
|
+
refreshPiResources(cwd: string): Promise<unknown>;
|
|
136
|
+
};
|
|
137
|
+
browserGateway: { broadcastToAll(msg: ServerToBrowserMessage): void };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isOpenSpecDataEmpty(d: OpenSpecData | undefined): boolean {
|
|
141
|
+
if (!d) return true;
|
|
142
|
+
return !d.initialized && (!d.changes || d.changes.length === 0);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Centralized post-install repair work fired on every `installing → ready`
|
|
147
|
+
* bootstrap-state transition. Idempotent. Failures per-cwd are isolated.
|
|
148
|
+
*
|
|
149
|
+
* Steps:
|
|
150
|
+
* 1) `registry.rescan()` (no arg — full registry invalidate). Restores
|
|
151
|
+
* the literal contract from `unified-bootstrap-install` task 4.3.
|
|
152
|
+
* 2) For every `directoryService.knownDirectories()` cwd, force-refresh
|
|
153
|
+
* OpenSpec (bypassing the mtime gate). If the prior cache was empty
|
|
154
|
+
* or the refreshed payload differs, broadcast `openspec_update`.
|
|
155
|
+
* 3) For every cwd, force-refresh pi-resources (silent on failure).
|
|
156
|
+
*
|
|
157
|
+
* The DEBUG=pi-dashboard|openspec-poll envvar enables a single-line
|
|
158
|
+
* diagnostic log on completion, matching the existing daemon-log style.
|
|
159
|
+
*/
|
|
160
|
+
export async function runPostInstallRepair(deps: PostInstallRepairDeps): Promise<void> {
|
|
161
|
+
const debug =
|
|
162
|
+
typeof process !== "undefined" &&
|
|
163
|
+
typeof process.env?.DEBUG === "string" &&
|
|
164
|
+
/pi-dashboard|openspec-poll/.test(process.env.DEBUG);
|
|
165
|
+
|
|
166
|
+
// 1) full registry rescan
|
|
167
|
+
deps.registry.rescan();
|
|
168
|
+
if (debug) console.log("[bootstrap] post-install: rescanned tool registry");
|
|
169
|
+
|
|
170
|
+
const cwds = deps.directoryService.knownDirectories();
|
|
171
|
+
|
|
172
|
+
// 2) per-cwd OpenSpec force-refresh + selective broadcast
|
|
173
|
+
await Promise.all(
|
|
174
|
+
cwds.map(async (cwd) => {
|
|
175
|
+
try {
|
|
176
|
+
const prior = deps.directoryService.getOpenSpecData(cwd);
|
|
177
|
+
const fresh = await deps.directoryService.refreshOpenSpec(cwd);
|
|
178
|
+
const priorEmpty = isOpenSpecDataEmpty(prior);
|
|
179
|
+
const dataDiffers = JSON.stringify(prior) !== JSON.stringify(fresh);
|
|
180
|
+
if (priorEmpty || dataDiffers) {
|
|
181
|
+
deps.browserGateway.broadcastToAll({ type: "openspec_update", cwd, data: fresh });
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.error(
|
|
185
|
+
`[bootstrap] post-install openspec refresh failed for ${cwd}:`,
|
|
186
|
+
err,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// 3) per-cwd pi-resources force-refresh (silent fail)
|
|
193
|
+
await Promise.all(
|
|
194
|
+
cwds.map(async (cwd) => {
|
|
195
|
+
try {
|
|
196
|
+
await deps.directoryService.refreshPiResources(cwd);
|
|
197
|
+
} catch {
|
|
198
|
+
// matches existing pattern in directory-service.ts::schedulePiResourcesTick
|
|
199
|
+
}
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (debug) console.log("[bootstrap] post-install: openspec + pi-resources refresh complete");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface BootstrapTransitionHandlerDeps {
|
|
207
|
+
/** Invoked once per `installing → ready` transition, fire-and-forget. */
|
|
208
|
+
onTransitionToReady: () => Promise<void> | void;
|
|
209
|
+
/** Existing queue flush invoked on the same transition. */
|
|
210
|
+
flushQueue: () => Promise<void> | void;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Returns a stateful handler that drives `onTransitionToReady` and
|
|
215
|
+
* `flushQueue` once per `installing → ready` (or `failed → ready`)
|
|
216
|
+
* transition. The handler ignores the very first ready snapshot
|
|
217
|
+
* because the bootstrap state defaults to ready.
|
|
218
|
+
*
|
|
219
|
+
* Both callbacks run fire-and-forget so the subscribe callback returns
|
|
220
|
+
* synchronously — matches the existing `void bootstrapQueue.flushAll()`
|
|
221
|
+
* pattern in the inline subscribe call site.
|
|
222
|
+
*/
|
|
223
|
+
export function makeBootstrapTransitionHandler(
|
|
224
|
+
deps: BootstrapTransitionHandlerDeps,
|
|
225
|
+
): (snapshot: { status: "ready" | "installing" | "failed" }) => void {
|
|
226
|
+
let last: "ready" | "installing" | "failed" = "ready";
|
|
227
|
+
return (snapshot) => {
|
|
228
|
+
if (last !== "ready" && snapshot.status === "ready") {
|
|
229
|
+
void Promise.resolve(deps.flushQueue()).catch((err) => {
|
|
230
|
+
console.error("[bootstrap] flushQueue failed:", err);
|
|
231
|
+
});
|
|
232
|
+
void Promise.resolve(deps.onTransitionToReady()).catch((err) => {
|
|
233
|
+
console.error("[bootstrap] post-install repair failed:", err);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
last = snapshot.status;
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
110
240
|
export async function createServer(config: ServerConfig): Promise<DashboardServer> {
|
|
111
241
|
// Ensure bridge extension is registered in pi's global settings
|
|
112
242
|
// (needed for bundled installs where pi can't discover it from package.json)
|
|
@@ -176,11 +306,112 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
176
306
|
firstMessage: session.firstMessage,
|
|
177
307
|
cachedAt: Date.now(),
|
|
178
308
|
});
|
|
309
|
+
// When a session ends, drop its id from the persisted drag-reorder list
|
|
310
|
+
// for that cwd. Drag-reorder is meaningful for live sessions only; ended
|
|
311
|
+
// ones must fall to the bottom in their natural endedAt order (rendered
|
|
312
|
+
// top-of-bucket on most-recent-first) rather than retaining a position
|
|
313
|
+
// that interleaves them with active sessions.
|
|
314
|
+
// See change: pin-and-search-sessions, top-of-tier-on-status-change.
|
|
315
|
+
// Status-transition tracking: prune+broadcast runs ONCE per
|
|
316
|
+
// transition to ended. Subsequent `update()` calls on an already-
|
|
317
|
+
// ended session (e.g. heartbeat tail, click-induced state sync,
|
|
318
|
+
// late events from the bridge) do NOT re-trigger the prune —
|
|
319
|
+
// otherwise the card visibly jumps to the tail of the ended group
|
|
320
|
+
// every time the user interacts with it.
|
|
321
|
+
// See change: pin-and-search-sessions.
|
|
322
|
+
const wasEnded = endedSessionIds.has(sessionId);
|
|
323
|
+
const isEnded = session.status === "ended";
|
|
324
|
+
if (isEnded && !wasEnded) {
|
|
325
|
+
// Just transitioned alive→ended.
|
|
326
|
+
endedSessionIds.add(sessionId);
|
|
327
|
+
const orderBefore = sessionOrderManager.getOrder(session.cwd) ?? [];
|
|
328
|
+
sessionOrderManager.remove(session.cwd, sessionId);
|
|
329
|
+
const orderAfter = sessionOrderManager.getOrder(session.cwd) ?? [];
|
|
330
|
+
if (orderBefore.length !== orderAfter.length) {
|
|
331
|
+
browserGateway.broadcastToAll({
|
|
332
|
+
type: "sessions_reordered",
|
|
333
|
+
cwd: session.cwd,
|
|
334
|
+
sessionIds: orderAfter,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
} else if (!isEnded && wasEnded) {
|
|
338
|
+
// Resume: ended→alive. Three real outcomes land here, distinguished
|
|
339
|
+
// by the value `pendingResumeIntents.consume(...)` returns:
|
|
340
|
+
// "front" — Resume button, REST resume, prompt-auto-resume.
|
|
341
|
+
// User wants the card surfaced at the top of alive.
|
|
342
|
+
// "keep" — Drag-to-resume. The dropped slot was already
|
|
343
|
+
// persisted via `reorder_sessions`; do NOT clobber it.
|
|
344
|
+
// null — Bridge auto-reattach (dashboard restarted, pi
|
|
345
|
+
// process still alive, no user intent tagged).
|
|
346
|
+
// Preserve the user's existing layout.
|
|
347
|
+
// We always clear the transition tracker so a future alive→ended
|
|
348
|
+
// for this session fires correctly.
|
|
349
|
+
// See changes: preserve-session-order-on-reboot,
|
|
350
|
+
// top-of-tier-on-status-change,
|
|
351
|
+
// differentiate-resume-intent-by-trigger.
|
|
352
|
+
endedSessionIds.delete(sessionId);
|
|
353
|
+
const intent = pendingResumeIntents.consume(sessionId);
|
|
354
|
+
if (intent === null) {
|
|
355
|
+
// Bridge auto-reattach — leave order alone.
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (intent === "keep") {
|
|
359
|
+
// Drag-to-resume — dropped slot wins; the earlier reorder_sessions
|
|
360
|
+
// already broadcast. Do NOT mutate sessionOrder, do NOT broadcast.
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// intent === "front": move-to-front so the just-resumed card
|
|
364
|
+
// surfaces at the top of the alive tier, even on repeated end →
|
|
365
|
+
// resume cycles where the id might still be in the order.
|
|
366
|
+
sessionOrderManager.moveToFront(session.cwd, sessionId);
|
|
367
|
+
const next = sessionOrderManager.getOrder(session.cwd) ?? [];
|
|
368
|
+
browserGateway.broadcastToAll({
|
|
369
|
+
type: "sessions_reordered",
|
|
370
|
+
cwd: session.cwd,
|
|
371
|
+
sessionIds: next,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
179
374
|
};
|
|
375
|
+
// Track which session ids we've seen as ended at least once, so the
|
|
376
|
+
// onChange hook can detect actual alive→ended transitions vs. mere
|
|
377
|
+
// re-emits of the ended state.
|
|
378
|
+
const endedSessionIds = new Set<string>(
|
|
379
|
+
sessionManager.listAll().filter((s) => s.status === "ended").map((s) => s.id),
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// Startup reconciliation: persisted `sessionOrder` may contain ended
|
|
383
|
+
// session ids from before the alive→ended prune was implemented. Strip
|
|
384
|
+
// them now so the next render sees a consistent state where ended ids
|
|
385
|
+
// never appear in the order pass.
|
|
386
|
+
// See change: pin-and-search-sessions.
|
|
387
|
+
for (const [cwd, ids] of Object.entries(sessionOrderManager.getAllOrders())) {
|
|
388
|
+
const aliveIds = ids.filter((id) => {
|
|
389
|
+
const s = sessionManager.get(id);
|
|
390
|
+
// Keep ids we don't know about — they may belong to other cwds or
|
|
391
|
+
// be live but not yet registered. Strip only the ones explicitly
|
|
392
|
+
// marked ended.
|
|
393
|
+
return !s || s.status !== "ended";
|
|
394
|
+
});
|
|
395
|
+
if (aliveIds.length !== ids.length) {
|
|
396
|
+
sessionOrderManager.reorder(cwd, aliveIds);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
180
399
|
|
|
181
400
|
// Track cwds with pending dashboard-spawned sessions (for writing .meta.json).
|
|
182
401
|
// Uses a counter per cwd to handle multiple spawns and avoid reconnects consuming entries.
|
|
183
402
|
const pendingDashboardSpawns = new Map<string, number>();
|
|
403
|
+
|
|
404
|
+
// Pending spawn-with-attach intents (cwd → FIFO queue of changeNames).
|
|
405
|
+
// Consumed in event-wiring.ts on session_register. See change:
|
|
406
|
+
// add-folder-task-checker-and-spawn-attach.
|
|
407
|
+
const pendingAttachRegistry = createPendingAttachRegistry();
|
|
408
|
+
// Pending user-initiated resume intents (sessionId → timestamp).
|
|
409
|
+
// Consumed by `sessionManager.onChange` in the ended→alive branch to
|
|
410
|
+
// gate the sessionOrder mutation behind explicit user intent so that
|
|
411
|
+
// bridge auto-reattach on dashboard reboot does not mutate the user's
|
|
412
|
+
// drag-order.
|
|
413
|
+
// See change: preserve-session-order-on-reboot.
|
|
414
|
+
const pendingResumeIntents = createPendingResumeIntentRegistry();
|
|
184
415
|
// Track known session IDs so we can distinguish new sessions from reconnections.
|
|
185
416
|
const knownSessionIds = new Set<string>();
|
|
186
417
|
// Populate from persisted sessions
|
|
@@ -237,7 +468,7 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
237
468
|
},
|
|
238
469
|
});
|
|
239
470
|
|
|
240
|
-
const browserGateway = createBrowserGateway(sessionManager, eventStore, piGateway, undefined, pendingForkRegistry, sessionOrderManager, preferencesStore, directoryService, terminalManager, pendingDashboardSpawns, config.maxWsBufferBytes);
|
|
471
|
+
const browserGateway = createBrowserGateway(sessionManager, eventStore, piGateway, undefined, pendingForkRegistry, sessionOrderManager, preferencesStore, directoryService, terminalManager, pendingDashboardSpawns, config.maxWsBufferBytes, pendingAttachRegistry, pendingResumeIntents);
|
|
241
472
|
|
|
242
473
|
// Resolve package version once at startup
|
|
243
474
|
const __require = createRequire(import.meta.url);
|
|
@@ -271,6 +502,7 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
271
502
|
directoryService,
|
|
272
503
|
knownSessionIds,
|
|
273
504
|
pendingDashboardSpawns,
|
|
505
|
+
pendingAttachRegistry,
|
|
274
506
|
});
|
|
275
507
|
|
|
276
508
|
// Auto-shutdown idle timer
|
|
@@ -362,17 +594,24 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
362
594
|
// See change: unified-bootstrap-install.
|
|
363
595
|
const bootstrapState = createBootstrapState();
|
|
364
596
|
const bootstrapQueue = createBootstrapQueue();
|
|
365
|
-
|
|
597
|
+
// Centralized post-install repair: full ToolRegistry rescan +
|
|
598
|
+
// OpenSpec / pi-resources force-refresh on every `installing → ready`
|
|
599
|
+
// transition. See change: fix-openspec-buttons-after-bootstrap-install.
|
|
600
|
+
const handleBootstrapTransition = makeBootstrapTransitionHandler({
|
|
601
|
+
flushQueue: () => bootstrapQueue.flushAll(),
|
|
602
|
+
onTransitionToReady: () =>
|
|
603
|
+
runPostInstallRepair({
|
|
604
|
+
registry: getDefaultRegistry(),
|
|
605
|
+
directoryService,
|
|
606
|
+
browserGateway,
|
|
607
|
+
}),
|
|
608
|
+
});
|
|
366
609
|
const unsubscribeBootstrap = bootstrapState.subscribe((snapshot) => {
|
|
367
610
|
browserGateway.broadcastToAll({
|
|
368
611
|
type: "bootstrap_status_update",
|
|
369
612
|
state: snapshot,
|
|
370
613
|
});
|
|
371
|
-
|
|
372
|
-
if (lastBootstrapStatus !== "ready" && snapshot.status === "ready") {
|
|
373
|
-
void bootstrapQueue.flushAll();
|
|
374
|
-
}
|
|
375
|
-
lastBootstrapStatus = snapshot.status;
|
|
614
|
+
handleBootstrapTransition(snapshot);
|
|
376
615
|
});
|
|
377
616
|
const unsubscribeQueueComplete = bootstrapQueue.onTicketComplete((evt) => {
|
|
378
617
|
browserGateway.broadcastToAll({
|
|
@@ -392,6 +631,7 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
392
631
|
pendingDashboardSpawns,
|
|
393
632
|
bootstrapState,
|
|
394
633
|
bootstrapQueue,
|
|
634
|
+
pendingResumeIntents,
|
|
395
635
|
});
|
|
396
636
|
|
|
397
637
|
// Register route modules
|
|
@@ -521,9 +761,16 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
521
761
|
// Package management
|
|
522
762
|
const packageManagerWrapper = new PackageManagerWrapper();
|
|
523
763
|
|
|
524
|
-
// Forward progress events to all browser clients
|
|
525
|
-
|
|
526
|
-
|
|
764
|
+
// Forward progress events to all browser clients. The third arg
|
|
765
|
+
// (`moveId`) is set when the event is part of a composite move op;
|
|
766
|
+
// clients group events by moveId. See change: unify-package-management-ui.
|
|
767
|
+
packageManagerWrapper.setProgressListener((operationId, event, moveId) => {
|
|
768
|
+
browserGateway.broadcastToAll({
|
|
769
|
+
type: "package_progress",
|
|
770
|
+
operationId,
|
|
771
|
+
...(moveId ? { moveId } : {}),
|
|
772
|
+
event,
|
|
773
|
+
} as any);
|
|
527
774
|
});
|
|
528
775
|
|
|
529
776
|
// On completion: broadcast to browsers + invalidate the recommended cache
|
|
@@ -538,6 +785,8 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
538
785
|
error: result.error,
|
|
539
786
|
diagnostics: result.diagnostics,
|
|
540
787
|
sessionsReloaded: (result as any).sessionsReloaded,
|
|
788
|
+
...(result.moveId ? { moveId: result.moveId } : {}),
|
|
789
|
+
...(result.partialSuccess ? { partialSuccess: result.partialSuccess } : {}),
|
|
541
790
|
} as any);
|
|
542
791
|
if (result.success) invalidateRecommendedCache();
|
|
543
792
|
});
|
|
@@ -626,6 +875,10 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
626
875
|
|
|
627
876
|
registerProviderAuthRoutes(fastify, { piGateway, browserGateway });
|
|
628
877
|
registerKnownServersRoutes(fastify, { networkGuard, getPeerServers: () => peerServers });
|
|
878
|
+
registerPluginConfigRoutes(fastify, {
|
|
879
|
+
networkGuard,
|
|
880
|
+
broadcast: (msg) => browserGateway.broadcast(msg),
|
|
881
|
+
});
|
|
629
882
|
registerProviderRoutes(fastify, { networkGuard, piGateway, browserGateway });
|
|
630
883
|
|
|
631
884
|
// Serve static files / SPA fallback.
|
|
@@ -848,6 +1101,82 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
848
1101
|
// Discover sessions and start OpenSpec polling (async, non-blocking)
|
|
849
1102
|
discoverAndBroadcastSessions({ sessionManager, browserGateway, directoryService });
|
|
850
1103
|
|
|
1104
|
+
// Load plugin server entries (non-blocking; failures isolated per plugin)
|
|
1105
|
+
loadServerEntries({
|
|
1106
|
+
isEnabled: (pluginId) => {
|
|
1107
|
+
const cfg = loadConfig();
|
|
1108
|
+
const pluginCfg = getPluginConfigFromFile(cfg, pluginId) as Record<string, unknown>;
|
|
1109
|
+
return pluginCfg.enabled !== false; // default: enabled
|
|
1110
|
+
},
|
|
1111
|
+
createContext: (plugin) => createServerPluginContext(
|
|
1112
|
+
{
|
|
1113
|
+
fastify,
|
|
1114
|
+
sessionManager: {
|
|
1115
|
+
listActive: () => sessionManager.listActive(),
|
|
1116
|
+
listAll: () => sessionManager.listAll(),
|
|
1117
|
+
getSession: (id: string) => sessionManager.get(id),
|
|
1118
|
+
},
|
|
1119
|
+
eventStore: {
|
|
1120
|
+
getEvents: (sessionId) => eventStore.getEvents(sessionId, 0),
|
|
1121
|
+
getLatestEvent: (sessionId) => {
|
|
1122
|
+
const events = eventStore.getEvents(sessionId, 0);
|
|
1123
|
+
return events.length > 0 ? events[events.length - 1] : undefined;
|
|
1124
|
+
},
|
|
1125
|
+
},
|
|
1126
|
+
broadcastToSubscribers: (msg) => browserGateway.broadcast(msg as any),
|
|
1127
|
+
// Plugin pi/browser handler registration — stub for now;
|
|
1128
|
+
// full dynamic handler registration requires a registry refactor
|
|
1129
|
+
// tracked in extract-*-as-plugin changes.
|
|
1130
|
+
registerPiHandler: (_type, _handler) => {},
|
|
1131
|
+
registerBrowserHandler: (_type, _handler) => {},
|
|
1132
|
+
getPluginConfig: (id) => {
|
|
1133
|
+
const cfg = loadConfig();
|
|
1134
|
+
return getPluginConfigFromFile(cfg, id);
|
|
1135
|
+
},
|
|
1136
|
+
updatePluginConfig: async (id, partial) => {
|
|
1137
|
+
// Inline partial write (reuses CONFIG_FILE path from shared config)
|
|
1138
|
+
const cfg = loadConfig();
|
|
1139
|
+
const current = getPluginConfigFromFile(cfg, id);
|
|
1140
|
+
const merged = { ...current, ...partial };
|
|
1141
|
+
let rawConfig: Record<string, unknown> = {};
|
|
1142
|
+
try {
|
|
1143
|
+
const raw = (await import('node:fs')).default.readFileSync(CONFIG_FILE, 'utf-8');
|
|
1144
|
+
rawConfig = JSON.parse(raw);
|
|
1145
|
+
} catch { /* start fresh */ }
|
|
1146
|
+
rawConfig.plugins = { ...(rawConfig.plugins as Record<string, unknown> ?? {}), [id]: merged };
|
|
1147
|
+
const fs = (await import('node:fs')).default;
|
|
1148
|
+
const tmpFile = CONFIG_FILE + '.tmp.' + process.pid;
|
|
1149
|
+
fs.writeFileSync(tmpFile, JSON.stringify(rawConfig, null, 2) + '\n');
|
|
1150
|
+
fs.renameSync(tmpFile, CONFIG_FILE);
|
|
1151
|
+
browserGateway.broadcast({ type: 'plugin_config_update', id, config: merged } as any);
|
|
1152
|
+
},
|
|
1153
|
+
},
|
|
1154
|
+
plugin.manifest.id,
|
|
1155
|
+
),
|
|
1156
|
+
}).catch((err) => console.error('[plugin-loader] Unexpected error:', err));
|
|
1157
|
+
|
|
1158
|
+
// Auto-register plugin bridge entries
|
|
1159
|
+
const discoveredPlugins = discoverPlugins();
|
|
1160
|
+
const pluginsWithBridges = discoveredPlugins
|
|
1161
|
+
.filter(p => p.bridgeEntryPath)
|
|
1162
|
+
.map(p => ({ pluginId: p.manifest.id, bridgePath: p.bridgeEntryPath! }));
|
|
1163
|
+
if (pluginsWithBridges.length) {
|
|
1164
|
+
const results = registerAllPluginBridges(pluginsWithBridges);
|
|
1165
|
+
for (const [id, result] of Object.entries(results)) {
|
|
1166
|
+
if (result.type === 'conflict') {
|
|
1167
|
+
const store = getPluginStatusStore();
|
|
1168
|
+
const existing = store.getStatus(id);
|
|
1169
|
+
store.setStatus({
|
|
1170
|
+
id,
|
|
1171
|
+
enabled: existing?.enabled ?? true,
|
|
1172
|
+
loaded: existing?.loaded ?? false,
|
|
1173
|
+
error: `Bridge path conflict: existing=${result.existingPath}, new=${result.newPath}`,
|
|
1174
|
+
claims: existing?.claims ?? 0,
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
851
1180
|
idleTimer.start();
|
|
852
1181
|
},
|
|
853
1182
|
|
|
@@ -11,8 +11,10 @@ import type { ApiResponse } from "@blackbelt-technology/pi-dashboard-shared/type
|
|
|
11
11
|
import { spawnPiSession } from "./process-manager.js";
|
|
12
12
|
import { loadConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
13
13
|
import type { PendingForkRegistry } from "./pending-fork-registry.js";
|
|
14
|
+
import type { PendingResumeIntentRegistry } from "./pending-resume-intent-registry.js";
|
|
14
15
|
import type { BootstrapStateStore } from "./bootstrap-state.js";
|
|
15
16
|
import type { BootstrapQueue } from "./bootstrap-queue.js";
|
|
17
|
+
import { attachRenameTarget, detachShouldClearName } from "./proposal-attach-naming.js";
|
|
16
18
|
|
|
17
19
|
export interface SessionApiDeps {
|
|
18
20
|
sessionManager: SessionManager;
|
|
@@ -27,6 +29,13 @@ export interface SessionApiDeps {
|
|
|
27
29
|
*/
|
|
28
30
|
bootstrapState?: BootstrapStateStore;
|
|
29
31
|
bootstrapQueue?: BootstrapQueue;
|
|
32
|
+
/**
|
|
33
|
+
* User-resume-intent registry. Tagged in the resume endpoint so the
|
|
34
|
+
* `sessionManager.onChange` ended→alive branch can distinguish a
|
|
35
|
+
* REST-initiated user resume from a bridge auto-reattach on reboot.
|
|
36
|
+
* See change: preserve-session-order-on-reboot.
|
|
37
|
+
*/
|
|
38
|
+
pendingResumeIntents?: PendingResumeIntentRegistry;
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
type IdParams = { Params: { id: string } };
|
|
@@ -39,7 +48,7 @@ function getSessionOrFail(sessionManager: SessionManager, id: string): { session
|
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
export function registerSessionApi(fastify: FastifyInstance, deps: SessionApiDeps) {
|
|
42
|
-
const { sessionManager, piGateway, browserGateway, pendingForkRegistry, pendingDashboardSpawns, bootstrapState, bootstrapQueue } = deps;
|
|
51
|
+
const { sessionManager, piGateway, browserGateway, pendingForkRegistry, pendingDashboardSpawns, bootstrapState, bootstrapQueue, pendingResumeIntents } = deps;
|
|
43
52
|
|
|
44
53
|
/**
|
|
45
54
|
* Gate pi-dependent operations on bootstrap status. Returns:
|
|
@@ -276,6 +285,12 @@ export function registerSessionApi(fastify: FastifyInstance, deps: SessionApiDep
|
|
|
276
285
|
if (mode === "fork" && pendingForkRegistry) {
|
|
277
286
|
pendingForkRegistry.recordFork(session.cwd, id);
|
|
278
287
|
}
|
|
288
|
+
// Tag the user-resume intent BEFORE spawning. REST resume always
|
|
289
|
+
// uses "front" placement — the only "keep" path is drag-to-resume
|
|
290
|
+
// which goes through the WebSocket handler, not this REST endpoint.
|
|
291
|
+
// See changes: preserve-session-order-on-reboot,
|
|
292
|
+
// differentiate-resume-intent-by-trigger.
|
|
293
|
+
pendingResumeIntents?.record(id, "front");
|
|
279
294
|
const config = loadConfig();
|
|
280
295
|
const spawnResult = await spawnPiSession(session.cwd, {
|
|
281
296
|
sessionFile: session.sessionFile,
|
|
@@ -370,9 +385,11 @@ export function registerSessionApi(fastify: FastifyInstance, deps: SessionApiDep
|
|
|
370
385
|
}
|
|
371
386
|
const updates: Record<string, unknown> = { attachedProposal: changeName };
|
|
372
387
|
const session = result.session;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
388
|
+
// Idempotent auto-rename (see change: fix-mobile-attach-proposal-display).
|
|
389
|
+
const newName = attachRenameTarget(session, changeName);
|
|
390
|
+
if (newName !== undefined) {
|
|
391
|
+
updates.name = newName;
|
|
392
|
+
piGateway.sendToSession(id, { type: "rename_session", sessionId: id, name: newName });
|
|
376
393
|
}
|
|
377
394
|
sessionManager.update(id, updates);
|
|
378
395
|
browserGateway.broadcastSessionUpdated(id, updates);
|
|
@@ -390,7 +407,15 @@ export function registerSessionApi(fastify: FastifyInstance, deps: SessionApiDep
|
|
|
390
407
|
reply.code(404);
|
|
391
408
|
return result.error;
|
|
392
409
|
}
|
|
393
|
-
const
|
|
410
|
+
const session = result.session;
|
|
411
|
+
const updates: Record<string, unknown> = {
|
|
412
|
+
attachedProposal: null, openspecPhase: null, openspecChange: null,
|
|
413
|
+
};
|
|
414
|
+
// Idempotent auto-revert (see change: fix-mobile-attach-proposal-display).
|
|
415
|
+
if (detachShouldClearName(session)) {
|
|
416
|
+
updates.name = undefined;
|
|
417
|
+
piGateway.sendToSession(id, { type: "rename_session", sessionId: id, name: "" });
|
|
418
|
+
}
|
|
394
419
|
sessionManager.update(id, updates);
|
|
395
420
|
browserGateway.broadcastSessionUpdated(id, updates);
|
|
396
421
|
return { success: true } satisfies ApiResponse;
|
|
@@ -11,6 +11,16 @@ export interface SessionOrderManager {
|
|
|
11
11
|
reorder(cwd: string, sessionIds: string[]): void;
|
|
12
12
|
/** Remove a session from its cwd order. */
|
|
13
13
|
remove(cwd: string, sessionId: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* Move a session id to the front (index 0) of its cwd order. Idempotent:
|
|
16
|
+
* if the id is already at the front, the order is unchanged but a persist
|
|
17
|
+
* still fires (callers gate broadcasts on actual mutation).
|
|
18
|
+
* If the id is absent, it is inserted at the front.
|
|
19
|
+
* Used by the user-intent resume path to surface the just-resumed session
|
|
20
|
+
* at the top of the alive tier even on repeated end → resume cycles.
|
|
21
|
+
* See change: top-of-tier-on-status-change.
|
|
22
|
+
*/
|
|
23
|
+
moveToFront(cwd: string, sessionId: string): void;
|
|
14
24
|
/** Get order for a cwd, optionally filtering to only valid IDs. */
|
|
15
25
|
getOrder(cwd: string, validIds?: Set<string>): string[];
|
|
16
26
|
/** Get all cwd→order entries. */
|
|
@@ -59,6 +69,18 @@ export function createSessionOrderManager(preferencesStore: PreferencesStore): S
|
|
|
59
69
|
persist();
|
|
60
70
|
},
|
|
61
71
|
|
|
72
|
+
moveToFront(cwd: string, sessionId: string): void {
|
|
73
|
+
// remove + unshift = move-to-front. Works whether the id was
|
|
74
|
+
// absent, mid-list, or already at index 0.
|
|
75
|
+
// See change: top-of-tier-on-status-change.
|
|
76
|
+
if (!orderMap[cwd]) {
|
|
77
|
+
orderMap[cwd] = [];
|
|
78
|
+
}
|
|
79
|
+
orderMap[cwd] = orderMap[cwd].filter((id) => id !== sessionId);
|
|
80
|
+
orderMap[cwd].unshift(sessionId);
|
|
81
|
+
persist();
|
|
82
|
+
},
|
|
83
|
+
|
|
62
84
|
getOrder(cwd: string, validIds?: Set<string>): string[] {
|
|
63
85
|
const arr = orderMap[cwd];
|
|
64
86
|
if (!arr) return [];
|
|
@@ -138,6 +138,15 @@ export function scanAllSessions(sessionsDir?: string): ScanResult {
|
|
|
138
138
|
// Stale cache — re-extract stats and merge
|
|
139
139
|
const stats = extractSessionStats(sessionFile);
|
|
140
140
|
if (stats) {
|
|
141
|
+
// Pi's JSONL has no turn_end/contextUsage events, so stats.contextWindow
|
|
142
|
+
// is always inferContextWindow(model) — a hardcoded heuristic that pins
|
|
143
|
+
// any Claude model to 200k and ignores 1M Sonnet variants. The persisted
|
|
144
|
+
// meta.contextWindow came from a real live `turn_end` event, so it's
|
|
145
|
+
// authoritative; only fall back to the inferred value when the model
|
|
146
|
+
// changed (persisted value no longer applies) or none was persisted.
|
|
147
|
+
const effectiveModel = stats.model ?? meta.model;
|
|
148
|
+
const preserveContextWindow =
|
|
149
|
+
meta.contextWindow !== undefined && effectiveModel === meta.model;
|
|
141
150
|
const updated: SessionMeta = {
|
|
142
151
|
...meta,
|
|
143
152
|
model: stats.model ?? meta.model,
|
|
@@ -148,7 +157,7 @@ export function scanAllSessions(sessionsDir?: string): ScanResult {
|
|
|
148
157
|
cacheWrite: stats.cacheWrite,
|
|
149
158
|
cost: stats.cost,
|
|
150
159
|
contextTokens: stats.lastTotalTokens,
|
|
151
|
-
contextWindow: stats.contextWindow,
|
|
160
|
+
contextWindow: preserveContextWindow ? meta.contextWindow : stats.contextWindow,
|
|
152
161
|
cachedAt: Date.now(),
|
|
153
162
|
};
|
|
154
163
|
writeSessionMeta(sessionFile, updated);
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blackbelt-technology/pi-dashboard-shared",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Shared types and utilities for pi-dashboard",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/BlackBeltTechnology/pi-agent-dashboard",
|
|
9
|
+
"directory": "packages/shared"
|
|
10
|
+
},
|
|
6
11
|
"publishConfig": {
|
|
7
12
|
"access": "public"
|
|
8
13
|
},
|
|
9
14
|
"exports": {
|
|
10
15
|
"./*.js": "./src/*.ts",
|
|
11
|
-
"./*": "./src/*"
|
|
16
|
+
"./*": "./src/*",
|
|
17
|
+
"./dashboard-plugin/*.js": "./src/dashboard-plugin/*.ts",
|
|
18
|
+
"./dashboard-plugin/*": "./src/dashboard-plugin/*"
|
|
12
19
|
},
|
|
13
20
|
"files": [
|
|
14
21
|
"src/"
|