@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.
Files changed (108) hide show
  1. package/AGENTS.md +79 -32
  2. package/README.md +7 -3
  3. package/docs/architecture.md +361 -12
  4. package/package.json +7 -7
  5. package/packages/extension/package.json +7 -2
  6. package/packages/extension/src/__tests__/ask-user-schema-discriminator.test.ts +141 -0
  7. package/packages/extension/src/__tests__/ask-user-tool.test.ts +51 -7
  8. package/packages/extension/src/__tests__/multiselect-dashboard-routing.test.ts +203 -0
  9. package/packages/extension/src/__tests__/multiselect-polyfill.test.ts +92 -0
  10. package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +81 -0
  11. package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +37 -0
  12. package/packages/extension/src/__tests__/ui-decorators.test.ts +309 -0
  13. package/packages/extension/src/__tests__/ui-modules.test.ts +293 -0
  14. package/packages/extension/src/ask-user-tool.ts +165 -57
  15. package/packages/extension/src/bridge.ts +97 -4
  16. package/packages/extension/src/multiselect-decode.ts +40 -0
  17. package/packages/extension/src/multiselect-polyfill.ts +38 -8
  18. package/packages/extension/src/ui-modules.ts +272 -0
  19. package/packages/server/package.json +9 -3
  20. package/packages/server/src/__tests__/auto-attach.test.ts +61 -8
  21. package/packages/server/src/__tests__/browse-endpoint.test.ts +295 -19
  22. package/packages/server/src/__tests__/cli-bootstrap.test.ts +36 -0
  23. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +163 -0
  24. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +315 -0
  25. package/packages/server/src/__tests__/directory-service-toctou.test.ts +303 -0
  26. package/packages/server/src/__tests__/directory-service.test.ts +174 -0
  27. package/packages/server/src/__tests__/installed-package-enricher.test.ts +225 -0
  28. package/packages/server/src/__tests__/package-manager-wrapper-move.test.ts +414 -0
  29. package/packages/server/src/__tests__/package-routes.test.ts +136 -3
  30. package/packages/server/src/__tests__/package-source-helpers.test.ts +101 -0
  31. package/packages/server/src/__tests__/pending-attach-registry.test.ts +123 -0
  32. package/packages/server/src/__tests__/pending-resume-intent-registry.test.ts +138 -0
  33. package/packages/server/src/__tests__/pi-core-checker.test.ts +73 -30
  34. package/packages/server/src/__tests__/pi-gateway-consume-pending-attach.test.ts +112 -0
  35. package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +180 -0
  36. package/packages/server/src/__tests__/post-install-rescan.test.ts +134 -0
  37. package/packages/server/src/__tests__/proposal-attach-naming.test.ts +79 -0
  38. package/packages/server/src/__tests__/session-action-handler-spawn-with-attach.test.ts +108 -0
  39. package/packages/server/src/__tests__/session-order-manager.test.ts +55 -0
  40. package/packages/server/src/__tests__/session-order-reboot.test.ts +242 -0
  41. package/packages/server/src/__tests__/session-scanner.test.ts +44 -0
  42. package/packages/server/src/__tests__/subscription-handler.test.ts +40 -0
  43. package/packages/server/src/__tests__/translate-path-source.test.ts +77 -0
  44. package/packages/server/src/__tests__/ui-decorators-replay.test.ts +209 -0
  45. package/packages/server/src/__tests__/ui-modules-replay.test.ts +221 -0
  46. package/packages/server/src/browse.ts +118 -13
  47. package/packages/server/src/browser-gateway.ts +19 -0
  48. package/packages/server/src/browser-handlers/__tests__/session-meta-handler.test.ts +183 -0
  49. package/packages/server/src/browser-handlers/directory-handler.ts +7 -1
  50. package/packages/server/src/browser-handlers/handler-context.ts +15 -0
  51. package/packages/server/src/browser-handlers/session-action-handler.ts +29 -3
  52. package/packages/server/src/browser-handlers/session-meta-handler.ts +46 -12
  53. package/packages/server/src/browser-handlers/subscription-handler.ts +46 -1
  54. package/packages/server/src/cli.ts +5 -6
  55. package/packages/server/src/directory-service.ts +156 -15
  56. package/packages/server/src/event-wiring.ts +111 -10
  57. package/packages/server/src/installed-package-enricher.ts +143 -0
  58. package/packages/server/src/package-manager-wrapper.ts +305 -8
  59. package/packages/server/src/package-source-helpers.ts +104 -0
  60. package/packages/server/src/pending-attach-registry.ts +112 -0
  61. package/packages/server/src/pending-resume-intent-registry.ts +107 -0
  62. package/packages/server/src/pi-core-checker.ts +9 -14
  63. package/packages/server/src/pi-gateway.ts +14 -0
  64. package/packages/server/src/proposal-attach-naming.ts +47 -0
  65. package/packages/server/src/routes/file-routes.ts +29 -3
  66. package/packages/server/src/routes/package-routes.ts +72 -3
  67. package/packages/server/src/routes/plugin-config-routes.ts +129 -0
  68. package/packages/server/src/routes/system-routes.ts +2 -0
  69. package/packages/server/src/server.ts +339 -10
  70. package/packages/server/src/session-api.ts +30 -5
  71. package/packages/server/src/session-order-manager.ts +22 -0
  72. package/packages/server/src/session-scanner.ts +10 -1
  73. package/packages/shared/package.json +9 -2
  74. package/packages/shared/src/__tests__/browser-protocol-types.test.ts +59 -0
  75. package/packages/shared/src/__tests__/config-plugins.test.ts +68 -0
  76. package/packages/shared/src/__tests__/extension-ui-module-shape.test.ts +265 -0
  77. package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +81 -0
  78. package/packages/shared/src/__tests__/openspec-design-evidence.test.ts +288 -0
  79. package/packages/shared/src/__tests__/openspec-effective-status-script.test.ts +174 -0
  80. package/packages/shared/src/__tests__/openspec-poller-design-override.test.ts +225 -0
  81. package/packages/shared/src/__tests__/openspec-poller-specs-override.test.ts +284 -0
  82. package/packages/shared/src/__tests__/openspec-specs-evidence.test.ts +144 -0
  83. package/packages/shared/src/__tests__/platform/is-appimage-self-hit.test.ts +164 -0
  84. package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +72 -0
  85. package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +113 -0
  86. package/packages/shared/src/__tests__/plugin-config-update-protocol.test.ts +41 -0
  87. package/packages/shared/src/__tests__/recommended-extensions.test.ts +5 -1
  88. package/packages/shared/src/__tests__/spawn-session-attach-proposal.test.ts +47 -0
  89. package/packages/shared/src/__tests__/tool-registry-strategies-appimage.test.ts +118 -0
  90. package/packages/shared/src/browser-protocol.ts +110 -4
  91. package/packages/shared/src/config.ts +45 -0
  92. package/packages/shared/src/dashboard-plugin/index.ts +11 -0
  93. package/packages/shared/src/dashboard-plugin/manifest-types.ts +58 -0
  94. package/packages/shared/src/dashboard-plugin/plugin-status.ts +26 -0
  95. package/packages/shared/src/dashboard-plugin/slot-props.ts +92 -0
  96. package/packages/shared/src/dashboard-plugin/slot-types.ts +151 -0
  97. package/packages/shared/src/openspec-activity-detector.ts +18 -22
  98. package/packages/shared/src/openspec-design-evidence.ts +109 -0
  99. package/packages/shared/src/openspec-poller.ts +117 -3
  100. package/packages/shared/src/openspec-specs-evidence.ts +79 -0
  101. package/packages/shared/src/platform/binary-lookup.ts +96 -1
  102. package/packages/shared/src/plugin-bridge-register.ts +139 -0
  103. package/packages/shared/src/protocol.ts +56 -2
  104. package/packages/shared/src/recommended-extensions.ts +7 -1
  105. package/packages/shared/src/rest-api.ts +68 -3
  106. package/packages/shared/src/state-replay.ts +11 -1
  107. package/packages/shared/src/tool-registry/strategies.ts +17 -3
  108. package/packages/shared/src/types.ts +160 -0
@@ -16,6 +16,7 @@ import { writeSessionMeta } from "@blackbelt-technology/pi-dashboard-shared/sess
16
16
  import type { DashboardSession } from "@blackbelt-technology/pi-dashboard-shared/types.js";
17
17
  import { detectOpenSpecActivity } from "@blackbelt-technology/pi-dashboard-shared/openspec-activity-detector.js";
18
18
  import { extractTurnStats } from "@blackbelt-technology/pi-dashboard-shared/stats-extractor.js";
19
+ import { attachRenameTarget, isNameAutoSetFromAttachment } from "./proposal-attach-naming.js";
19
20
 
20
21
  export interface EventWiringDeps {
21
22
  sessionManager: SessionManager;
@@ -27,6 +28,12 @@ export interface EventWiringDeps {
27
28
  directoryService: DirectoryService;
28
29
  knownSessionIds: Set<string>;
29
30
  pendingDashboardSpawns: Map<string, number>;
31
+ /**
32
+ * Optional pending-attach registry. When provided, the wiring consumes a
33
+ * pending intent on each `session_register` and applies the attach +
34
+ * auto-rename. See change: add-folder-task-checker-and-spawn-attach.
35
+ */
36
+ pendingAttachRegistry?: import("./pending-attach-registry.js").PendingAttachRegistry;
30
37
  }
31
38
 
32
39
  /**
@@ -44,6 +51,7 @@ export function wireEvents(deps: EventWiringDeps): void {
44
51
  directoryService,
45
52
  knownSessionIds,
46
53
  pendingDashboardSpawns,
54
+ pendingAttachRegistry,
47
55
  } = deps;
48
56
 
49
57
  // Broadcast placeholder session to browsers when auto-created from early events
@@ -54,6 +62,28 @@ export function wireEvents(deps: EventWiringDeps): void {
54
62
  }
55
63
  };
56
64
 
65
+ // Consume any pending spawn-with-attach intent for the registering session.
66
+ // See change: add-folder-task-checker-and-spawn-attach.
67
+ piGateway.onSessionRegistered = (sessionId, cwd) => {
68
+ if (!pendingAttachRegistry) return;
69
+ const changeName = pendingAttachRegistry.consume(cwd);
70
+ if (!changeName) return;
71
+ // Lazy import to avoid a circular type dep at module load.
72
+ void import("./browser-handlers/session-meta-handler.js").then(({ applyAttachProposal }) => {
73
+ applyAttachProposal(sessionId, changeName, {
74
+ sessionManager,
75
+ piGateway,
76
+ broadcast: (msg) => {
77
+ // applyAttachProposal only emits `session_updated`; route via the
78
+ // browser gateway's typed helper to match the rest of this file.
79
+ if (msg.type === "session_updated") {
80
+ browserGateway.broadcastSessionUpdated(msg.sessionId, msg.updates);
81
+ }
82
+ },
83
+ });
84
+ });
85
+ };
86
+
57
87
  // Broadcast session ended to browsers when sessions are unregistered
58
88
  sessionManager.onUnregister = (sessionId) => {
59
89
  const session = sessionManager.get(sessionId);
@@ -66,6 +96,11 @@ export function wireEvents(deps: EventWiringDeps): void {
66
96
  }
67
97
  };
68
98
 
99
+ // Per-event cap for `Session.uiDataMap[event]`. Phase-1 spec contract:
100
+ // last-write-wins on overflow; oldest items are discarded.
101
+ // See change: add-extension-ui-modal, design.md §5.
102
+ const UI_DATA_PER_EVENT_CAP = 1000;
103
+
69
104
  // Track sessions replaying history — suppress status broadcasts to avoid card flicker
70
105
  const replayingSessions = new Set<string>();
71
106
  // Sessions whose replay should be discarded (canSkipWipe was true — events already in store)
@@ -139,17 +174,30 @@ export function wireEvents(deps: EventWiringDeps): void {
139
174
  // (write/CLI). Reads are passive (browsing/analysis) and don't trigger attach.
140
175
  // Phase is optional — skills loaded via prompt templates don't emit a SKILL.md read event.
141
176
  const attachUpdates: Partial<DashboardSession> = {};
142
- if (updatedSession?.openspecChange && !updatedSession.attachedProposal && detected.isActive) {
143
- attachUpdates.attachedProposal = updatedSession.openspecChange;
144
- if (!updatedSession.name?.trim()) {
145
- attachUpdates.name = updatedSession.openspecChange;
146
- piGateway.sendToSession(sessionId, {
147
- type: "rename_session",
148
- sessionId,
149
- name: updatedSession.openspecChange,
150
- });
177
+ // Auto-detect parallel path see change: fix-mobile-attach-proposal-display
178
+ // (design.md §"Auto-detect parallel path"). Mirrors the witness rule in
179
+ // session-meta-handler.ts: re-attach when the previous attachment was
180
+ // auto-tracked (name === attachedProposal) AND a different changeName
181
+ // is now detected. Inner rename guard reuses attachRenameTarget.
182
+ if (updatedSession?.openspecChange && detected.isActive) {
183
+ const attachmentWasAutoTracked =
184
+ !updatedSession.attachedProposal ||
185
+ isNameAutoSetFromAttachment(updatedSession);
186
+ const differentChangeDetected =
187
+ updatedSession.attachedProposal !== updatedSession.openspecChange;
188
+ if (attachmentWasAutoTracked && differentChangeDetected) {
189
+ attachUpdates.attachedProposal = updatedSession.openspecChange;
190
+ const newName = attachRenameTarget(updatedSession, updatedSession.openspecChange);
191
+ if (newName !== undefined) {
192
+ attachUpdates.name = newName;
193
+ piGateway.sendToSession(sessionId, {
194
+ type: "rename_session",
195
+ sessionId,
196
+ name: newName,
197
+ });
198
+ }
199
+ sessionManager.update(sessionId, attachUpdates);
151
200
  }
152
- sessionManager.update(sessionId, attachUpdates);
153
201
  }
154
202
  if (!replayingSessions.has(sessionId)) {
155
203
  browserGateway.broadcastSessionUpdated(sessionId, {
@@ -523,6 +571,59 @@ export function wireEvents(deps: EventWiringDeps): void {
523
571
  browserGateway.sendToSubscribers(sessionId, msg as any);
524
572
  }
525
573
 
574
+ // ── Extension UI System (Phase 1): cache + broadcast ──
575
+ // See change: add-extension-ui-modal.
576
+ if (msg.type === "ui_modules_list") {
577
+ sessionManager.update(sessionId, { uiModules: msg.modules });
578
+ browserGateway.sendToSubscribers(sessionId, {
579
+ type: "ui_modules_list",
580
+ sessionId,
581
+ modules: msg.modules,
582
+ } as any);
583
+ }
584
+
585
+ if (msg.type === "ui_data_list") {
586
+ const session = sessionManager.get(sessionId);
587
+ const dataMap = { ...(session?.uiDataMap ?? {}) };
588
+ // Per-event item cap (default N = 1000). Last-write-wins on overflow.
589
+ const items = Array.isArray(msg.items) ? msg.items : [];
590
+ const capped = items.length > UI_DATA_PER_EVENT_CAP
591
+ ? items.slice(items.length - UI_DATA_PER_EVENT_CAP)
592
+ : items;
593
+ dataMap[msg.event] = capped;
594
+ sessionManager.update(sessionId, { uiDataMap: dataMap });
595
+ browserGateway.sendToSubscribers(sessionId, {
596
+ type: "ui_data_list",
597
+ sessionId,
598
+ event: msg.event,
599
+ items: capped,
600
+ } as any);
601
+ }
602
+
603
+ // ── Extension UI System (Phase 2): live decorator cache + broadcast ──
604
+ // See change: add-extension-ui-decorations.
605
+ if (msg.type === "ext_ui_decorator") {
606
+ const session = sessionManager.get(sessionId);
607
+ if (session) {
608
+ const descriptor = msg.descriptor;
609
+ if (descriptor && typeof descriptor.kind === "string" && typeof descriptor.namespace === "string" && typeof descriptor.id === "string") {
610
+ const key = `${descriptor.kind}:${descriptor.namespace}:${descriptor.id}`;
611
+ const next = { ...(session.uiDecorators ?? {}) };
612
+ if (msg.removed === true) delete next[key];
613
+ else next[key] = descriptor;
614
+ sessionManager.update(sessionId, { uiDecorators: next });
615
+ }
616
+ }
617
+ // Broadcast verbatim regardless of whether the session is known — mirrors
618
+ // the Phase-1 contract for `ui_modules_list` / `ui_data_list`.
619
+ browserGateway.sendToSubscribers(sessionId, {
620
+ type: "ext_ui_decorator",
621
+ sessionId,
622
+ descriptor: msg.descriptor,
623
+ ...(msg.removed === true ? { removed: true } : {}),
624
+ } as any);
625
+ }
626
+
526
627
  if (msg.type === "session_name_update") {
527
628
  const nameUpdates = { name: msg.name || undefined };
528
629
  sessionManager.update(sessionId, nameUpdates);
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Enrich rows returned by `packageManagerWrapper.listInstalled()` with
3
+ * version, description, displayName, isRecommended, and isBundled fields.
4
+ *
5
+ * The raw rows from pi's `DefaultPackageManager.listConfiguredPackages()`
6
+ * carry only `{ source, scope, filtered, installedPath }`. The Settings
7
+ * Packages tab needs more to render a friendly identity and badges
8
+ * without a second fetch.
9
+ *
10
+ * Pure helpers (`extractBasenameFromSource`, `computeIsBundled`) are
11
+ * exported for unit tests; the I/O-bearing enricher (`enrichInstalled`)
12
+ * reads the on-disk `package.json` for each row.
13
+ *
14
+ * See change: consolidate-packages-settings-ui.
15
+ */
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import {
19
+ RECOMMENDED_EXTENSIONS,
20
+ BUNDLED_EXTENSION_IDS,
21
+ type RecommendedExtension,
22
+ } from "@blackbelt-technology/pi-dashboard-shared/recommended-extensions.js";
23
+ import { sourcesMatch } from "@blackbelt-technology/pi-dashboard-shared/source-matching.js";
24
+ import type { InstalledPackage } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
25
+
26
+ /** Raw row shape returned by pi's listConfiguredPackages(). */
27
+ export interface RawInstalledRow {
28
+ source: string;
29
+ scope: "user" | "project";
30
+ filtered: boolean;
31
+ installedPath?: string;
32
+ }
33
+
34
+ /** Read package.json#version and #description from a directory.
35
+ * Swallows all errors and returns `{}` on any failure. Exported for tests. */
36
+ export function readPackageJsonMeta(installedPath: string | undefined): {
37
+ version?: string;
38
+ description?: string;
39
+ } {
40
+ if (!installedPath) return {};
41
+ try {
42
+ const pj = path.join(installedPath, "package.json");
43
+ if (!fs.existsSync(pj)) return {};
44
+ const raw = fs.readFileSync(pj, "utf-8");
45
+ const parsed = JSON.parse(raw);
46
+ const version = typeof parsed?.version === "string" ? parsed.version : undefined;
47
+ const description = typeof parsed?.description === "string" ? parsed.description : undefined;
48
+ return { version, description };
49
+ } catch {
50
+ return {};
51
+ }
52
+ }
53
+
54
+ /** Pull a friendly basename out of a raw source string when no
55
+ * RECOMMENDED_EXTENSIONS match is available. Pure. */
56
+ export function extractBasenameFromSource(source: string): string {
57
+ // npm:<name>[@<ver>]
58
+ const npmMatch = source.match(/^npm:(@?[^@]+)(?:@.*)?$/);
59
+ if (npmMatch) return npmMatch[1];
60
+
61
+ // git: strip .git suffix and trailing slash, take last path segment
62
+ const gitMatch = source.match(/[/:]([^/:]+?)(?:\.git)?\/?$/);
63
+ if (gitMatch) return gitMatch[1];
64
+
65
+ // local file:// or path: take last path segment
66
+ const localMatch = source.match(/[/\\]([^/\\]+)\/?$/);
67
+ if (localMatch) return localMatch[1];
68
+
69
+ return source;
70
+ }
71
+
72
+ /** Find the recommended manifest entry whose source matches the row.
73
+ * Pure. */
74
+ export function matchRecommendedEntry(
75
+ source: string,
76
+ manifest: readonly RecommendedExtension[] = RECOMMENDED_EXTENSIONS,
77
+ ): RecommendedExtension | undefined {
78
+ return manifest.find((entry) => sourcesMatch(entry.source, source));
79
+ }
80
+
81
+ /** Compute isBundled. Pure (takes injected resourcesPath + existsSync).
82
+ * Outside Electron (no resourcesPath), always false. */
83
+ export function computeIsBundled(
84
+ id: string,
85
+ resourcesPath: string | undefined,
86
+ existsFn: (p: string) => boolean = fs.existsSync,
87
+ bundledIds: readonly string[] = BUNDLED_EXTENSION_IDS,
88
+ ): boolean {
89
+ if (!resourcesPath) return false;
90
+ if (!bundledIds.includes(id)) return false;
91
+ const dir = path.join(resourcesPath, "bundled-extensions", id);
92
+ return existsFn(dir);
93
+ }
94
+
95
+ /** Enrich a single raw row. Pure dependency injection for tests. */
96
+ export function enrichInstalledRow(
97
+ row: RawInstalledRow,
98
+ opts: {
99
+ resourcesPath?: string;
100
+ manifest?: readonly RecommendedExtension[];
101
+ bundledIds?: readonly string[];
102
+ readMeta?: (p: string | undefined) => { version?: string; description?: string };
103
+ existsFn?: (p: string) => boolean;
104
+ } = {},
105
+ ): InstalledPackage {
106
+ const readMeta = opts.readMeta ?? readPackageJsonMeta;
107
+ const existsFn = opts.existsFn ?? fs.existsSync;
108
+ const manifest = opts.manifest ?? RECOMMENDED_EXTENSIONS;
109
+ const bundledIds = opts.bundledIds ?? BUNDLED_EXTENSION_IDS;
110
+
111
+ const meta = readMeta(row.installedPath);
112
+ const recommended = matchRecommendedEntry(row.source, manifest);
113
+
114
+ const displayName =
115
+ recommended?.displayName ?? extractBasenameFromSource(row.source);
116
+ const description = recommended?.fallbackDescription ?? meta.description;
117
+ const isRecommended = !!recommended;
118
+ const isBundled = recommended
119
+ ? computeIsBundled(recommended.id, opts.resourcesPath, existsFn, bundledIds)
120
+ : false;
121
+
122
+ return {
123
+ source: row.source,
124
+ scope: row.scope,
125
+ filtered: row.filtered,
126
+ installedPath: row.installedPath,
127
+ version: meta.version,
128
+ description,
129
+ displayName,
130
+ isRecommended,
131
+ isBundled,
132
+ };
133
+ }
134
+
135
+ /** Enrich a list of raw rows. Reads the Electron resourcesPath at
136
+ * runtime (or undefined in CLI mode). */
137
+ export function enrichInstalledRows(
138
+ rows: RawInstalledRow[],
139
+ resourcesPath: string | undefined = (process as { resourcesPath?: string })
140
+ .resourcesPath,
141
+ ): InstalledPackage[] {
142
+ return rows.map((row) => enrichInstalledRow(row, { resourcesPath }));
143
+ }