@blackbelt-technology/pi-agent-dashboard 0.4.0 → 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 (129) hide show
  1. package/AGENTS.md +104 -35
  2. package/README.md +390 -494
  3. package/docs/architecture.md +423 -20
  4. package/package.json +11 -8
  5. package/packages/extension/package.json +11 -4
  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 +91 -15
  8. package/packages/extension/src/__tests__/bridge-entry-id-pi-070.test.ts +174 -0
  9. package/packages/extension/src/__tests__/event-forwarder.test.ts +30 -0
  10. package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +64 -76
  11. package/packages/extension/src/__tests__/multiselect-dashboard-routing.test.ts +203 -0
  12. package/packages/extension/src/__tests__/multiselect-list.test.ts +137 -0
  13. package/packages/extension/src/__tests__/multiselect-polyfill.test.ts +92 -0
  14. package/packages/extension/src/__tests__/no-session-replacement-calls.test.ts +99 -0
  15. package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +81 -0
  16. package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +37 -0
  17. package/packages/extension/src/__tests__/ui-decorators.test.ts +309 -0
  18. package/packages/extension/src/__tests__/ui-modules.test.ts +293 -0
  19. package/packages/extension/src/ask-user-tool.ts +170 -61
  20. package/packages/extension/src/bridge.ts +199 -19
  21. package/packages/extension/src/multiselect-decode.ts +40 -0
  22. package/packages/extension/src/multiselect-list.ts +146 -0
  23. package/packages/extension/src/multiselect-polyfill.ts +73 -0
  24. package/packages/extension/src/server-launcher.ts +15 -3
  25. package/packages/extension/src/ui-modules.ts +272 -0
  26. package/packages/server/package.json +11 -5
  27. package/packages/server/src/__tests__/auto-attach.test.ts +61 -8
  28. package/packages/server/src/__tests__/browse-endpoint.test.ts +295 -19
  29. package/packages/server/src/__tests__/cli-bootstrap.test.ts +36 -0
  30. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +163 -0
  31. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +315 -0
  32. package/packages/server/src/__tests__/directory-service-toctou.test.ts +303 -0
  33. package/packages/server/src/__tests__/directory-service.test.ts +174 -0
  34. package/packages/server/src/__tests__/fixtures/fork-jsonl-roundtrip.jsonl +8 -0
  35. package/packages/server/src/__tests__/fork-jsonl-roundtrip.test.ts +49 -0
  36. package/packages/server/src/__tests__/installed-package-enricher.test.ts +225 -0
  37. package/packages/server/src/__tests__/package-manager-wrapper-move.test.ts +414 -0
  38. package/packages/server/src/__tests__/package-routes.test.ts +136 -3
  39. package/packages/server/src/__tests__/package-source-helpers.test.ts +101 -0
  40. package/packages/server/src/__tests__/pending-attach-registry.test.ts +123 -0
  41. package/packages/server/src/__tests__/pending-resume-intent-registry.test.ts +138 -0
  42. package/packages/server/src/__tests__/pi-core-checker.test.ts +73 -30
  43. package/packages/server/src/__tests__/pi-gateway-consume-pending-attach.test.ts +112 -0
  44. package/packages/server/src/__tests__/pi-version-skew.test.ts +72 -0
  45. package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +180 -0
  46. package/packages/server/src/__tests__/post-install-rescan.test.ts +134 -0
  47. package/packages/server/src/__tests__/proposal-attach-naming.test.ts +79 -0
  48. package/packages/server/src/__tests__/restart-helper.test.ts +34 -6
  49. package/packages/server/src/__tests__/session-action-handler-spawn-with-attach.test.ts +108 -0
  50. package/packages/server/src/__tests__/session-order-manager.test.ts +55 -0
  51. package/packages/server/src/__tests__/session-order-reboot.test.ts +242 -0
  52. package/packages/server/src/__tests__/session-scanner.test.ts +44 -0
  53. package/packages/server/src/__tests__/subscription-handler.test.ts +40 -0
  54. package/packages/server/src/__tests__/translate-path-source.test.ts +77 -0
  55. package/packages/server/src/__tests__/ui-decorators-replay.test.ts +209 -0
  56. package/packages/server/src/__tests__/ui-modules-replay.test.ts +221 -0
  57. package/packages/server/src/browse.ts +118 -13
  58. package/packages/server/src/browser-gateway.ts +19 -0
  59. package/packages/server/src/browser-handlers/__tests__/session-meta-handler.test.ts +183 -0
  60. package/packages/server/src/browser-handlers/directory-handler.ts +7 -1
  61. package/packages/server/src/browser-handlers/handler-context.ts +15 -0
  62. package/packages/server/src/browser-handlers/session-action-handler.ts +29 -3
  63. package/packages/server/src/browser-handlers/session-meta-handler.ts +46 -12
  64. package/packages/server/src/browser-handlers/subscription-handler.ts +46 -1
  65. package/packages/server/src/cli.ts +61 -15
  66. package/packages/server/src/directory-service.ts +156 -15
  67. package/packages/server/src/event-wiring.ts +111 -10
  68. package/packages/server/src/installed-package-enricher.ts +143 -0
  69. package/packages/server/src/package-manager-wrapper.ts +305 -8
  70. package/packages/server/src/package-source-helpers.ts +104 -0
  71. package/packages/server/src/pending-attach-registry.ts +112 -0
  72. package/packages/server/src/pending-resume-intent-registry.ts +107 -0
  73. package/packages/server/src/pi-core-checker.ts +9 -14
  74. package/packages/server/src/pi-gateway.ts +14 -0
  75. package/packages/server/src/pi-version-skew.ts +12 -1
  76. package/packages/server/src/proposal-attach-naming.ts +47 -0
  77. package/packages/server/src/restart-helper.ts +13 -2
  78. package/packages/server/src/routes/file-routes.ts +29 -3
  79. package/packages/server/src/routes/package-routes.ts +72 -3
  80. package/packages/server/src/routes/plugin-config-routes.ts +129 -0
  81. package/packages/server/src/routes/system-routes.ts +2 -0
  82. package/packages/server/src/server.ts +339 -10
  83. package/packages/server/src/session-api.ts +30 -5
  84. package/packages/server/src/session-order-manager.ts +22 -0
  85. package/packages/server/src/session-scanner.ts +10 -1
  86. package/packages/shared/package.json +9 -2
  87. package/packages/shared/src/__tests__/browser-protocol-types.test.ts +59 -0
  88. package/packages/shared/src/__tests__/config-plugins.test.ts +68 -0
  89. package/packages/shared/src/__tests__/extension-ui-module-shape.test.ts +265 -0
  90. package/packages/shared/src/__tests__/no-hardcoded-node-modules-paths.test.ts +176 -0
  91. package/packages/shared/src/__tests__/no-raw-node-import.test.ts +146 -0
  92. package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +81 -0
  93. package/packages/shared/src/__tests__/node-spawn.test.ts +210 -0
  94. package/packages/shared/src/__tests__/openspec-design-evidence.test.ts +288 -0
  95. package/packages/shared/src/__tests__/openspec-effective-status-script.test.ts +174 -0
  96. package/packages/shared/src/__tests__/openspec-poller-design-override.test.ts +225 -0
  97. package/packages/shared/src/__tests__/openspec-poller-specs-override.test.ts +284 -0
  98. package/packages/shared/src/__tests__/openspec-specs-evidence.test.ts +144 -0
  99. package/packages/shared/src/__tests__/platform/is-appimage-self-hit.test.ts +164 -0
  100. package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +72 -0
  101. package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +113 -0
  102. package/packages/shared/src/__tests__/plugin-config-update-protocol.test.ts +41 -0
  103. package/packages/shared/src/__tests__/recommended-extensions.test.ts +5 -1
  104. package/packages/shared/src/__tests__/resolve-tool-cli.test.ts +105 -0
  105. package/packages/shared/src/__tests__/spawn-session-attach-proposal.test.ts +47 -0
  106. package/packages/shared/src/__tests__/state-replay-entry-id.test.ts +69 -0
  107. package/packages/shared/src/__tests__/tool-registry-strategies-appimage.test.ts +118 -0
  108. package/packages/shared/src/browser-protocol.ts +110 -4
  109. package/packages/shared/src/config.ts +45 -0
  110. package/packages/shared/src/dashboard-plugin/index.ts +11 -0
  111. package/packages/shared/src/dashboard-plugin/manifest-types.ts +58 -0
  112. package/packages/shared/src/dashboard-plugin/plugin-status.ts +26 -0
  113. package/packages/shared/src/dashboard-plugin/slot-props.ts +92 -0
  114. package/packages/shared/src/dashboard-plugin/slot-types.ts +151 -0
  115. package/packages/shared/src/openspec-activity-detector.ts +18 -22
  116. package/packages/shared/src/openspec-design-evidence.ts +109 -0
  117. package/packages/shared/src/openspec-poller.ts +117 -3
  118. package/packages/shared/src/openspec-specs-evidence.ts +79 -0
  119. package/packages/shared/src/platform/binary-lookup.ts +96 -1
  120. package/packages/shared/src/platform/index.ts +1 -0
  121. package/packages/shared/src/platform/node-spawn.ts +154 -0
  122. package/packages/shared/src/plugin-bridge-register.ts +139 -0
  123. package/packages/shared/src/protocol.ts +79 -2
  124. package/packages/shared/src/recommended-extensions.ts +7 -1
  125. package/packages/shared/src/rest-api.ts +68 -3
  126. package/packages/shared/src/state-replay.ts +20 -1
  127. package/packages/shared/src/tool-registry/definitions.ts +92 -0
  128. package/packages/shared/src/tool-registry/strategies.ts +17 -3
  129. package/packages/shared/src/types.ts +160 -0
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Extension ↔ Server WebSocket protocol messages.
3
3
  */
4
- import type { DashboardEvent, CommandInfo, FlowInfo, SessionSource, ImageContent, FileEntry, TurnUsage, ContextUsage, ModelInfo, PiSessionInfo, OpenSpecPhase, RoleInfo } from "./types.js";
4
+ import type { DashboardEvent, CommandInfo, FlowInfo, SessionSource, ImageContent, FileEntry, TurnUsage, ContextUsage, ModelInfo, PiSessionInfo, OpenSpecPhase, RoleInfo, ExtensionUiModule, DecoratorDescriptor } from "./types.js";
5
5
 
6
6
  // ── Extension → Server ──────────────────────────────────────────────
7
7
 
@@ -57,6 +57,29 @@ export interface EventForwardMessage {
57
57
  event: DashboardEvent;
58
58
  }
59
59
 
60
+ /**
61
+ * Conventions on `event_forward` payloads relevant to per-message fork:
62
+ *
63
+ * - `message_start` and `message_end` events MAY carry an optional
64
+ * `data.nonce: string` stamped by the bridge. The reducer carries it
65
+ * onto the resulting ChatMessage so a later `entry_persisted` event
66
+ * can back-fill the entry id.
67
+ * - `entry_persisted` events have shape:
68
+ * {
69
+ * eventType: "entry_persisted",
70
+ * timestamp,
71
+ * data: { type: "entry_persisted", entryId: string, nonce: string }
72
+ * }
73
+ * They are emitted by the bridge after pi calls
74
+ * `sessionManager.appendMessage` and the entry id has been generated.
75
+ * See change: fix-per-message-fork.
76
+ */
77
+ export interface EntryPersistedEventData {
78
+ type: "entry_persisted";
79
+ entryId: string;
80
+ nonce: string;
81
+ }
82
+
60
83
  export interface CommandsListMessage {
61
84
  type: "commands_list";
62
85
  sessionId: string;
@@ -205,6 +228,39 @@ export interface ProcessListMessage {
205
228
 
206
229
  // LoadSessionEventsResultMessage and LoadSessionEventsErrorMessage removed — server loads directly
207
230
 
231
+ // ── Extension UI System (Phase 1) ──
232
+ // Pull-discovered, schema-driven UI modules. See change: add-extension-ui-modal.
233
+
234
+ export interface UiModulesListMessage {
235
+ type: "ui_modules_list";
236
+ sessionId: string;
237
+ modules: ExtensionUiModule[];
238
+ }
239
+
240
+ export interface UiDataListMessage {
241
+ type: "ui_data_list";
242
+ sessionId: string;
243
+ /** Matches some `module.view.dataEvent`. */
244
+ event: string;
245
+ items: unknown[];
246
+ }
247
+
248
+ // ── Extension UI System (Phase 2: live in-page decorations) ──
249
+ // See change: add-extension-ui-decorations.
250
+
251
+ /**
252
+ * Extension → server: a single live decorator descriptor. Cache key
253
+ * `${kind}:${namespace}:${id}` MUST be unique within a session; `removed: true`
254
+ * deletes the cache entry instead of upserting.
255
+ */
256
+ export interface ExtUiDecoratorMessage {
257
+ type: "ext_ui_decorator";
258
+ sessionId: string;
259
+ descriptor: DecoratorDescriptor;
260
+ /** When true, server deletes the cached descriptor under the matching key. */
261
+ removed?: boolean;
262
+ }
263
+
208
264
  export type ExtensionToServerMessage =
209
265
  | SessionRegisterMessage
210
266
  | SessionUnregisterMessage
@@ -227,7 +283,10 @@ export type ExtensionToServerMessage =
227
283
  | FirstMessageUpdateMessage
228
284
  | RolesListMessage
229
285
  | SpawnNewSessionMessage
230
- | ProcessListMessage;
286
+ | ProcessListMessage
287
+ | UiModulesListMessage
288
+ | UiDataListMessage
289
+ | ExtUiDecoratorMessage;
231
290
 
232
291
  // ── Server → Extension ──────────────────────────────────────────────
233
292
 
@@ -378,6 +437,23 @@ export interface ExtensionUiResponseMessage {
378
437
  cancelled?: boolean;
379
438
  }
380
439
 
440
+ /**
441
+ * Server → extension: a browser invoked an action / requested data on a
442
+ * Phase-1 management-modal module. The bridge re-emits this on `pi.events`
443
+ * as `pi.events.emit(msg.event, { ...msg.params, action: msg.action, _reply })`.
444
+ * Extensions either populate `data.items` synchronously (for `action: "list"`
445
+ * data fetches) or perform side-effects and emit `ui:invalidate` to refresh.
446
+ */
447
+ export interface UiManagementMessage {
448
+ type: "ui_management";
449
+ sessionId: string;
450
+ /** Action id (matches some `UiAction.id`) or `"list"` for data fetch. */
451
+ action: string;
452
+ /** Event name to emit (matches `view.dataEvent` or `UiAction.event`). */
453
+ event: string;
454
+ params?: Record<string, unknown>;
455
+ }
456
+
381
457
  export interface PromptResponseServerMessage {
382
458
  type: "prompt_response";
383
459
  sessionId: string;
@@ -412,4 +488,5 @@ export type ServerToExtensionMessage =
412
488
  | RolePresetSaveExtensionMessage
413
489
  | RolePresetDeleteExtensionMessage
414
490
  | RequestRolesMessage
491
+ | UiManagementMessage
415
492
  | KillProcessMessage;
@@ -180,7 +180,13 @@ export const RECOMMENDED_EXTENSIONS: readonly RecommendedExtension[] = [
180
180
  */
181
181
  export const BUNDLED_EXTENSION_IDS: readonly string[] = [
182
182
  "pi-anthropic-messages",
183
- "pi-flows",
183
+ // "pi-flows" is intentionally NOT bundled until the upstream repo declares
184
+ // an SPDX-conformant license (`LICENSE` file or `package.json#license`).
185
+ // The bundle-recommended-extensions.sh license allowlist enforcement
186
+ // (MIT/Apache-2.0/BSD-2-Clause/BSD-3-Clause/ISC) correctly rejects it.
187
+ // Re-add this entry once https://github.com/BlackBeltTechnology/pi-flows
188
+ // has a license declared. See: openspec/changes/archive/
189
+ // 2026-04-21-bundle-first-party-extensions/design.md §"License blockers".
184
190
  ];
185
191
 
186
192
  /** Retrieve a recommended entry by id, or `undefined`. */
@@ -64,17 +64,35 @@ export type FileReadResponse = ApiResponse<FileReadResult>;
64
64
  export interface BrowseEntry {
65
65
  name: string;
66
66
  path: string;
67
- isGit: boolean;
68
- isPi: boolean;
67
+ /**
68
+ * Set only when the request used `detect=1`. When the response was
69
+ * produced without `detect=1`, this field is absent (undefined) —
70
+ * meaning "not classified", NOT "classified as not-git". Consumers
71
+ * that need badges SHOULD call `GET /api/browse/flags` to fill in
72
+ * the flags lazily.
73
+ *
74
+ * See change: split-browse-flags.
75
+ */
76
+ isGit?: boolean;
77
+ /** See `isGit` — same opt-in / detect-gated semantics. */
78
+ isPi?: boolean;
69
79
  }
70
80
 
71
81
  /**
72
- * Response shape for `GET /api/browse?path=<dir>&q=<query>`.
82
+ * Response shape for `GET /api/browse?path=<dir>&q=<query>&detect=<0|1>`.
73
83
  *
74
84
  * The optional `q` query parameter, when present and non-empty, causes the
75
85
  * server to filter entries by case-insensitive substring on `name` and rank
76
86
  * them (exact → prefix → word-boundary → substring) before the 200-entry cap.
77
87
  * When omitted or whitespace-only, entries are sorted alphabetically.
88
+ *
89
+ * The optional `detect` query parameter (only the literal string `"1"` is
90
+ * truthy) opts into eager `.git` / `.pi` classification on every entry. When
91
+ * absent (the default), per-entry `isGit` / `isPi` are omitted and no
92
+ * filesystem probes run — use the bulk `GET /api/browse/flags` endpoint to
93
+ * classify entries lazily.
94
+ *
95
+ * See change: split-browse-flags.
78
96
  */
79
97
  export interface BrowseResult {
80
98
  entries: BrowseEntry[];
@@ -94,6 +112,43 @@ export interface BrowseResult {
94
112
 
95
113
  export type BrowseResponse = ApiResponse<BrowseResult>;
96
114
 
115
+ // ── Browse flags (bulk classifier) ──────────────────────────────────
116
+
117
+ /**
118
+ * Per-path classification record returned by `GET /api/browse/flags`.
119
+ * Booleans only — any probe failure (ENOENT, EACCES, ELOOP, race-on-
120
+ * deletion, …) maps to `false` for that flag, never an error.
121
+ *
122
+ * See change: split-browse-flags.
123
+ */
124
+ export interface BrowseFlagEntry {
125
+ isGit: boolean;
126
+ isPi: boolean;
127
+ }
128
+
129
+ /**
130
+ * Wire shape passed via the `paths` query parameter on
131
+ * `GET /api/browse/flags?paths=<json-array>`. The value MUST be a
132
+ * URL-encoded JSON array of absolute path strings (length ≤ 100).
133
+ * Provided here for type-only documentation — the request itself is a
134
+ * GET, so this interface is not serialized as a body.
135
+ */
136
+ export interface BrowseFlagsRequest {
137
+ paths: string[];
138
+ }
139
+
140
+ /** Successful response payload for `GET /api/browse/flags`. */
141
+ export interface BrowseFlagsResult {
142
+ /**
143
+ * Map keyed by the absolute paths that were requested. The key set
144
+ * SHALL equal the input `paths` set — one classification per input
145
+ * path, no extras, no omissions.
146
+ */
147
+ flags: Record<string, BrowseFlagEntry>;
148
+ }
149
+
150
+ export type BrowseFlagsResponse = ApiResponse<BrowseFlagsResult>;
151
+
97
152
  /** Request body for `POST /api/browse/mkdir`. */
98
153
  export interface MkdirRequest {
99
154
  parent: string;
@@ -254,6 +309,16 @@ export interface InstalledPackage {
254
309
  installedPath?: string;
255
310
  /** Set after check-updates: true if newer version available */
256
311
  updateAvailable?: boolean;
312
+ /** Version read from `<installedPath>/package.json#version`. Undefined if missing/unreadable. */
313
+ version?: string;
314
+ /** Description read from `<installedPath>/package.json#description`. */
315
+ description?: string;
316
+ /** Friendly name. From RECOMMENDED_EXTENSIONS displayName when matched, else basename of source. */
317
+ displayName?: string;
318
+ /** True when this row matches a RECOMMENDED_EXTENSIONS entry (via sourcesMatch). */
319
+ isRecommended?: boolean;
320
+ /** True when isRecommended AND id is in BUNDLED_EXTENSION_IDS AND bundle subtree exists. */
321
+ isBundled?: boolean;
257
322
  }
258
323
 
259
324
  export type InstalledPackagesResponse = ApiResponse<InstalledPackage[]>;
@@ -13,10 +13,29 @@ import type { EventForwardMessage } from "./protocol.js";
13
13
  * - message_update + message_end for assistant messages
14
14
  * - tool_execution_start / tool_execution_end for tool calls
15
15
  * - model_select for model changes
16
+ *
17
+ * NOTE on entryId (per change: fix-per-message-fork):
18
+ * Replay reads from the persisted JSONL, so each entry already has a
19
+ * stable `id`. We attach it directly as `entryId` on both `message_start`
20
+ * (user) and `message_end` (assistant) events. Replay therefore does NOT
21
+ * need to emit an `entry_persisted` follow-up — the back-fill protocol
22
+ * exists to bridge a timing gap that only happens for LIVE pi events on
23
+ * pi 0.69+, where the bridge sees `message_start` before pi has assigned
24
+ * the entry id. Replay has no such gap.
25
+ */
26
+ /**
27
+ * @param knownContextWindow Optional override for the context window size,
28
+ * typically `session.contextWindow` from `.meta.json` (which was persisted
29
+ * from a live `turn_end` event). When provided, it is used in place of the
30
+ * `inferContextWindow(modelId)` heuristic for every synthesized
31
+ * `stats_update` event. The heuristic ignores Sonnet's 1M variant and
32
+ * pins Claude to 200k, so passing the persisted value avoids a brief
33
+ * 200k flicker on reload before the next live `turn_end` arrives.
16
34
  */
17
35
  export function replayEntriesAsEvents(
18
36
  sessionId: string,
19
37
  entries: any[],
38
+ knownContextWindow?: number,
20
39
  ): EventForwardMessage[] {
21
40
  const messages: EventForwardMessage[] = [];
22
41
  const openToolCalls = new Set<string>(); // track tool calls without results
@@ -77,7 +96,7 @@ export function replayEntriesAsEvents(
77
96
  if (totalTokens && totalTokens > 0) {
78
97
  statsData.contextUsage = {
79
98
  tokens: totalTokens,
80
- contextWindow: inferContextWindow(currentModel),
99
+ contextWindow: knownContextWindow ?? inferContextWindow(currentModel),
81
100
  };
82
101
  }
83
102
  messages.push(makeEvent(sessionId, "stats_update", ts, statsData));
@@ -22,6 +22,7 @@ import {
22
22
  overrideStrategy,
23
23
  whereStrategy,
24
24
  } from "./strategies.js";
25
+ import type { Strategy } from "./types.js";
25
26
 
26
27
  // ── Classifier ──────────────────────────────────────────────────────────────
27
28
 
@@ -66,6 +67,68 @@ function moduleDefWithAliases(
66
67
  return { name: canonicalName, kind: "module", strategies, classify };
67
68
  }
68
69
 
70
+ // ── Build-time module definitions (electron, node-pty) ────────────────────
71
+
72
+ /**
73
+ * Bare-import strategy that resolves `<pkg>/package.json` and returns the
74
+ * containing directory. Used for build-time tools whose useful artifact is
75
+ * a sibling file of `package.json` (e.g. `electron/install.js`,
76
+ * `node-pty/prebuilds/`). Mirrors the semantics that build-time consumers
77
+ * (`publish.yml`, `Dockerfile.build`, `scripts/fix-pty-permissions.cjs`)
78
+ * need — see change: register-build-time-tools.
79
+ *
80
+ * `searchPaths` are passed to Node's resolver as the `paths` option,
81
+ * making the lookup work whether the package is hoisted to the repo root
82
+ * or nested under a workspace.
83
+ */
84
+ function bareImportPackageDirStrategy(
85
+ pkgName: string,
86
+ searchPaths?: readonly string[],
87
+ deps?: StrategyDeps,
88
+ ): Strategy {
89
+ const fallbackResolve = (id: string, from: string): string | null => {
90
+ try {
91
+ if (searchPaths && searchPaths.length > 0) {
92
+ const req = createRequire(from) as unknown as {
93
+ resolve(id: string, opts?: { paths?: readonly string[] }): string;
94
+ };
95
+ return req.resolve(id, { paths: searchPaths });
96
+ }
97
+ return createRequire(from).resolve(id);
98
+ } catch {
99
+ return null;
100
+ }
101
+ };
102
+ const resolveModule = deps?.resolveModule ?? fallbackResolve;
103
+ return {
104
+ name: "bare-import",
105
+ run() {
106
+ const pkgJson = resolveModule(`${pkgName}/package.json`, import.meta.url);
107
+ if (!pkgJson) {
108
+ return { ok: false, reason: `cannot resolve ${pkgName}/package.json` };
109
+ }
110
+ return { ok: true, path: path.dirname(pkgJson) };
111
+ },
112
+ };
113
+ }
114
+
115
+ /** Module def that returns the package directory (containing package.json). */
116
+ function packageDirModuleDef(
117
+ toolName: string,
118
+ pkgName: string,
119
+ options: { searchPaths?: readonly string[]; includeManaged?: boolean },
120
+ deps?: StrategyDeps,
121
+ ): ToolDefinition {
122
+ const strategies: Strategy[] = [
123
+ overrideStrategy(toolName, deps),
124
+ bareImportPackageDirStrategy(pkgName, options.searchPaths, deps),
125
+ ];
126
+ if (options.includeManaged) {
127
+ strategies.push(managedModuleStrategy(pkgName, "package.json", deps));
128
+ }
129
+ return { name: toolName, kind: "module", strategies, classify };
130
+ }
131
+
69
132
  // ── Registration ─────────────────────────────────────────────────
70
133
 
71
134
  // Tools intentionally NOT registered:
@@ -76,6 +139,14 @@ function moduleDefWithAliases(
76
139
  // - `pi-dashboard` — that's the package this code is part of.
77
140
  // "Is it installed" is a bootstrap concern handled directly in
78
141
  // `packages/electron/src/lib/dependency-detector.ts`.
142
+ //
143
+ // Build-time tools (see change: register-build-time-tools):
144
+ // - `electron` — module, returns the package directory containing
145
+ // `install.js`. Resolved with paths anchored at
146
+ // `packages/electron` to handle hoisted vs. nested
147
+ // layouts uniformly.
148
+ // - `node-pty` — module, returns the package directory containing
149
+ // `prebuilds/`. Standard module resolution suffices.
79
150
  // See change: consolidate-tool-resolution (follow-up).
80
151
 
81
152
  /**
@@ -333,6 +404,27 @@ export function registerDefaultTools(registry: ToolRegistry, deps?: StrategyDeps
333
404
  deps,
334
405
  ),
335
406
  );
407
+
408
+ // Build-time tools (see change: register-build-time-tools).
409
+ registry.register(
410
+ packageDirModuleDef(
411
+ "electron",
412
+ "electron",
413
+ {
414
+ searchPaths: [path.resolve("packages/electron")],
415
+ includeManaged: true,
416
+ },
417
+ deps,
418
+ ),
419
+ );
420
+ registry.register(
421
+ packageDirModuleDef(
422
+ "node-pty",
423
+ "node-pty",
424
+ { includeManaged: false },
425
+ deps,
426
+ ),
427
+ );
336
428
  }
337
429
 
338
430
  /** Handy re-exports for callers that want raw definitions for testing. */
@@ -11,7 +11,7 @@
11
11
  import { existsSync } from "node:fs";
12
12
  import { createRequire } from "node:module";
13
13
  import path from "node:path";
14
- import { ToolResolver } from "../platform/binary-lookup.js";
14
+ import { ToolResolver, isAppImageSelfHit } from "../platform/binary-lookup.js";
15
15
  import { getManagedBin, getManagedDir } from "../managed-paths.js";
16
16
  import * as npm from "../platform/npm.js";
17
17
  import type { Strategy, StrategyCtx, StrategyResult } from "./types.js";
@@ -152,6 +152,17 @@ export function npmGlobalStrategy(
152
152
  /**
153
153
  * PATH search via `ToolResolver.which()`. This is the plain-old "is it
154
154
  * on PATH" strategy and should appear last in most chains.
155
+ *
156
+ * Filters AppImage self-hits via `isAppImageSelfHit` — when the host
157
+ * runs as a Linux AppImage with `executableName: "pi-dashboard"`, the
158
+ * AppImage runtime prepends its squashfs mount to PATH, so the first
159
+ * `which pi-dashboard` hit can be the Electron launcher itself.
160
+ * Trusting that result spawns the Electron app recursively as if it
161
+ * were the dashboard CLI, which never opens the dashboard port and
162
+ * causes the loading screen to hang. Every tool registered via
163
+ * `whereStrategy` inherits this guard transparently.
164
+ *
165
+ * See change: fix-electron-appimage-cli-self-detection (D2).
155
166
  */
156
167
  export function whereStrategy(binaryName: string, deps?: StrategyDeps): Strategy {
157
168
  const { which } = d(deps);
@@ -159,8 +170,11 @@ export function whereStrategy(binaryName: string, deps?: StrategyDeps): Strategy
159
170
  name: "where",
160
171
  run(): StrategyResult {
161
172
  const p = which(binaryName);
162
- if (p) return { ok: true, path: p };
163
- return { ok: false, reason: `not found on PATH` };
173
+ if (!p) return { ok: false, reason: `not found on PATH` };
174
+ if (isAppImageSelfHit(p)) {
175
+ return { ok: false, reason: `appimage-self-hit: ${p}` };
176
+ }
177
+ return { ok: true, path: p };
164
178
  },
165
179
  };
166
180
  }
@@ -61,8 +61,168 @@ export interface DashboardSession {
61
61
  /** Timestamp when metrics were last received */
62
62
  updatedAt: number;
63
63
  };
64
+ /** Extension-declared UI modules (Phase 1: management-modal slot). */
65
+ uiModules?: ExtensionUiModule[];
66
+ /** Cached row data per `view.dataEvent` for table/grid views. Per-event item cap is enforced server-side. */
67
+ uiDataMap?: Record<string, unknown[]>;
68
+ /**
69
+ * Phase-2 live in-page decorations (footer-segment, agent-metric, breadcrumb,
70
+ * gate, toast). Keyed by `${kind}:${namespace}:${id}`. Last-write-wins on
71
+ * upsert; explicit removal via `ext_ui_decorator { removed: true }` deletes
72
+ * the entry. See change: add-extension-ui-decorations.
73
+ */
74
+ uiDecorators?: Record<string, DecoratorDescriptor>;
75
+ }
76
+
77
+ // ── Extension UI System (Phase 1: management-modal slot) ───────────
78
+ // Per `extension-ui-system` design + `add-extension-ui-modal` change.
79
+ // Field/type names match PR #15 verbatim so any later archival diff stays small.
80
+
81
+ export type UiViewKind = "table" | "grid" | "form";
82
+
83
+ export type UiFieldKind =
84
+ | "text"
85
+ | "number"
86
+ | "boolean"
87
+ | "select"
88
+ | "code"
89
+ | "datetime"
90
+ | "textarea";
91
+
92
+ export interface UiField {
93
+ /** Dot-path into row / form-state. */
94
+ key: string;
95
+ label: string;
96
+ kind: UiFieldKind;
97
+ /** For kind: "select". */
98
+ options?: string[];
99
+ placeholder?: string;
100
+ required?: boolean;
101
+ readOnly?: boolean;
102
+ /** Legacy alias for kind: "textarea". Prefer `kind: "textarea"`. */
103
+ multiline?: boolean;
104
+ /** Display-only: table column width. */
105
+ width?: string | number;
106
+ /** For kind: "code". Hint to syntax highlighter. */
107
+ language?: string;
64
108
  }
65
109
 
110
+ export interface UiAction {
111
+ /** Action id, echoed back as the `action` field on the `ui_management` message. */
112
+ id: string;
113
+ label: string;
114
+ /** MDI icon key from `@mdi/js` (e.g. `"mdiCheckCircle"`). Unknown keys render no icon. */
115
+ icon?: string;
116
+ variant?: "primary" | "secondary" | "danger";
117
+ /** Event name re-emitted on the extension's `pi.events` bus when the action fires. */
118
+ event: string;
119
+ params?: Record<string, unknown>;
120
+ /** If present, dashboard mounts ConfirmDialog with this message before dispatching. */
121
+ confirm?: string;
122
+ }
123
+
124
+ export interface UiSection {
125
+ id: string;
126
+ title?: string;
127
+ description?: string;
128
+ fields: UiField[];
129
+ }
130
+
131
+ export interface UiView {
132
+ kind: UiViewKind;
133
+ /** Table/grid columns; form fields when no `sections` provided. */
134
+ fields?: UiField[];
135
+ /** For form view: grouped fields. Mutually exclusive with top-level `fields`. */
136
+ sections?: UiSection[];
137
+ /** Event name to request rows; required for `table`/`grid`. */
138
+ dataEvent?: string;
139
+ /** Unique-row field for `table`/`grid` (default: `"id"`). */
140
+ rowKey?: string;
141
+ /** Per-row actions for `table`/`grid`. */
142
+ rowActions?: UiAction[];
143
+ /** Shown when `items.length === 0`. */
144
+ emptyState?: string;
145
+ /** Top-of-modal toolbar actions. */
146
+ actions?: UiAction[];
147
+ }
148
+
149
+ export interface ExtensionUiModule {
150
+ /** Phase 1: only `"management-modal"`. */
151
+ kind: "management-modal";
152
+ /** Unique within the session. Last-write-wins on collision. */
153
+ id: string;
154
+ /** Exact slash command (case-sensitive). */
155
+ command: string;
156
+ title: string;
157
+ description?: string;
158
+ /** MDI icon key from `@mdi/js`. */
159
+ icon?: string;
160
+ /** Free-form group label (sidebar grouping in future). */
161
+ category?: string;
162
+ view: UiView;
163
+ }
164
+
165
+ // ── Extension UI System (Phase 2: live in-page decorations) ──────
166
+ // Per `extension-ui-system` design + `add-extension-ui-decorations` change.
167
+ // Single discriminated union forwarded as one `ext_ui_decorator` message per
168
+ // descriptor. Cache key: `${kind}:${namespace}:${id}`. `namespace` MUST match
169
+ // `/^[a-z0-9-]+$/`; the bridge drops malformed namespaces with a warning.
170
+
171
+ export type DecoratorKind =
172
+ | "footer-segment"
173
+ | "agent-metric"
174
+ | "breadcrumb"
175
+ | "gate"
176
+ | "toast";
177
+
178
+ export interface FooterSegmentPayload {
179
+ text: string;
180
+ tooltip?: string;
181
+ /** MDI icon key from `@mdi/js`. Unknown keys render no icon. */
182
+ icon?: string;
183
+ }
184
+
185
+ export interface AgentMetricPayload {
186
+ /** Matches the agent id rendered by `FlowAgentCard`. */
187
+ agentId: string;
188
+ text: string;
189
+ tooltip?: string;
190
+ }
191
+
192
+ export interface BreadcrumbStep {
193
+ id: string;
194
+ label: string;
195
+ status: "pending" | "active" | "done" | "error";
196
+ }
197
+
198
+ export interface BreadcrumbPayload {
199
+ steps: BreadcrumbStep[];
200
+ /** Step id of the currently-active step (overrides `status: "active"` selection). */
201
+ current?: string;
202
+ }
203
+
204
+ export interface GatePayload {
205
+ /** Matches the flow id rendered in `FlowLaunchDialog`. */
206
+ flowId: string;
207
+ available: boolean;
208
+ /** Reason rendered as a tooltip when `available: false`. */
209
+ reason?: string;
210
+ }
211
+
212
+ export interface ToastPayload {
213
+ level: "info" | "success" | "warn" | "error";
214
+ message: string;
215
+ /** Auto-dismiss after this many ms. Default 5000; `0` = sticky. */
216
+ durationMs?: number;
217
+ }
218
+
219
+ export type DecoratorDescriptor =
220
+ | { kind: "footer-segment"; namespace: string; id: string; payload: FooterSegmentPayload }
221
+ | { kind: "agent-metric"; namespace: string; id: string; payload: AgentMetricPayload }
222
+ | { kind: "breadcrumb"; namespace: string; id: string; payload: BreadcrumbPayload }
223
+ | { kind: "gate"; namespace: string; id: string; payload: GatePayload }
224
+ | { kind: "toast"; namespace: string; id: string; payload: ToastPayload };
225
+
66
226
  /** An event forwarded from a pi session */
67
227
  export interface DashboardEvent {
68
228
  eventType: string;