@blackbelt-technology/pi-agent-dashboard 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/AGENTS.md +26 -5
  2. package/README.md +49 -7
  3. package/docs/architecture.md +129 -1
  4. package/package.json +15 -15
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
  7. package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
  8. package/packages/extension/src/__tests__/command-handler.test.ts +78 -8
  9. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
  10. package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
  11. package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
  12. package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
  13. package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
  14. package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
  15. package/packages/extension/src/__tests__/server-launcher-launch.test.ts +78 -0
  16. package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
  17. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
  18. package/packages/extension/src/ask-user-tool.ts +1 -1
  19. package/packages/extension/src/bridge-context.ts +68 -4
  20. package/packages/extension/src/bridge.ts +79 -11
  21. package/packages/extension/src/command-handler.ts +95 -15
  22. package/packages/extension/src/flow-event-wiring.ts +1 -1
  23. package/packages/extension/src/multiselect-list.ts +1 -1
  24. package/packages/extension/src/pi-env.d.ts +16 -9
  25. package/packages/extension/src/prompt-expander.ts +74 -63
  26. package/packages/extension/src/provider-register.ts +16 -9
  27. package/packages/extension/src/retry-tracker.ts +123 -0
  28. package/packages/extension/src/server-launcher.ts +31 -70
  29. package/packages/extension/src/session-sync.ts +10 -1
  30. package/packages/extension/src/slash-dispatch.ts +123 -0
  31. package/packages/extension/src/usage-limit-orderer.ts +76 -0
  32. package/packages/server/bin/pi-dashboard.mjs +84 -0
  33. package/packages/server/package.json +8 -7
  34. package/packages/server/scripts/fix-pty-permissions.cjs +52 -0
  35. package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
  36. package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
  37. package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
  38. package/packages/server/src/__tests__/cli-parse.test.ts +16 -4
  39. package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -0
  40. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
  41. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
  42. package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
  43. package/packages/server/src/__tests__/directory-service.test.ts +2 -2
  44. package/packages/server/src/__tests__/dispatch-extension-command-router.test.ts +178 -0
  45. package/packages/server/src/__tests__/e2e/model-proxy-google-flash.test.ts +184 -0
  46. package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +68 -1
  47. package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
  48. package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
  49. package/packages/server/src/__tests__/headless-pid-registry.test.ts +316 -0
  50. package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
  51. package/packages/server/src/__tests__/keeper-manager.test.ts +298 -0
  52. package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +149 -0
  53. package/packages/server/src/__tests__/model-proxy-api-key-routes.test.ts +277 -0
  54. package/packages/server/src/__tests__/model-proxy-auth-gate.test.ts +263 -0
  55. package/packages/server/src/__tests__/model-proxy-multi-user.test.ts +169 -0
  56. package/packages/server/src/__tests__/model-proxy-routes.test.ts +286 -0
  57. package/packages/server/src/__tests__/model-proxy-second-port.test.ts +116 -0
  58. package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +64 -8
  59. package/packages/server/src/__tests__/openspec-group-broadcast.test.ts +97 -0
  60. package/packages/server/src/__tests__/openspec-group-join.test.ts +80 -0
  61. package/packages/server/src/__tests__/openspec-group-routes.test.ts +370 -0
  62. package/packages/server/src/__tests__/openspec-group-store.test.ts +496 -0
  63. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
  64. package/packages/server/src/__tests__/package-routes.test.ts +1 -1
  65. package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
  66. package/packages/server/src/__tests__/pi-ai-shape.test.ts +147 -0
  67. package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
  68. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
  69. package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
  70. package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +62 -3
  71. package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
  72. package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
  73. package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
  74. package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
  75. package/packages/server/src/__tests__/process-manager-keeper-spawn.test.ts +206 -0
  76. package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
  77. package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +13 -23
  78. package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
  79. package/packages/server/src/__tests__/recommended-routes.test.ts +3 -3
  80. package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
  81. package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +84 -0
  82. package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
  83. package/packages/server/src/__tests__/tunnel-watchdog.test.ts +139 -0
  84. package/packages/server/src/auth-plugin.ts +3 -0
  85. package/packages/server/src/bootstrap-state.ts +10 -0
  86. package/packages/server/src/browser-gateway.ts +27 -10
  87. package/packages/server/src/browser-handlers/handler-context.ts +9 -0
  88. package/packages/server/src/browser-handlers/session-action-handler.ts +128 -19
  89. package/packages/server/src/changelog-fs.ts +167 -0
  90. package/packages/server/src/changelog-parser.ts +321 -0
  91. package/packages/server/src/changelog-remote.ts +134 -0
  92. package/packages/server/src/cli.ts +62 -82
  93. package/packages/server/src/config-api.ts +14 -2
  94. package/packages/server/src/directory-service.ts +106 -4
  95. package/packages/server/src/event-wiring.ts +90 -6
  96. package/packages/server/src/headless-pid-registry.ts +344 -37
  97. package/packages/server/src/legacy-pi-cleanup.ts +151 -0
  98. package/packages/server/src/model-proxy/__tests__/api-key-store.test.ts +142 -0
  99. package/packages/server/src/model-proxy/__tests__/auth-json-contention.test.ts +98 -0
  100. package/packages/server/src/model-proxy/__tests__/concurrency.test.ts +107 -0
  101. package/packages/server/src/model-proxy/__tests__/failed-auth-backoff.test.ts +46 -0
  102. package/packages/server/src/model-proxy/__tests__/recursion-guard.test.ts +61 -0
  103. package/packages/server/src/model-proxy/__tests__/streamer.test.ts +139 -0
  104. package/packages/server/src/model-proxy/api-key-store.ts +87 -0
  105. package/packages/server/src/model-proxy/auth-gate.ts +116 -0
  106. package/packages/server/src/model-proxy/concurrency.ts +76 -0
  107. package/packages/server/src/model-proxy/convert/UPSTREAM.md +13 -0
  108. package/packages/server/src/model-proxy/convert/__tests__/anthropic-in.test.ts +137 -0
  109. package/packages/server/src/model-proxy/convert/__tests__/anthropic-out.test.ts +183 -0
  110. package/packages/server/src/model-proxy/convert/__tests__/openai-in.test.ts +134 -0
  111. package/packages/server/src/model-proxy/convert/__tests__/openai-out.test.ts +166 -0
  112. package/packages/server/src/model-proxy/convert/anthropic-in.ts +129 -0
  113. package/packages/server/src/model-proxy/convert/anthropic-out.ts +173 -0
  114. package/packages/server/src/model-proxy/convert/index.ts +8 -0
  115. package/packages/server/src/model-proxy/convert/openai-in.ts +119 -0
  116. package/packages/server/src/model-proxy/convert/openai-out.ts +151 -0
  117. package/packages/server/src/model-proxy/convert/types.ts +70 -0
  118. package/packages/server/src/model-proxy/failed-auth-backoff.ts +45 -0
  119. package/packages/server/src/model-proxy/internal-auth-storage.ts +146 -0
  120. package/packages/server/src/model-proxy/internal-registry.ts +157 -0
  121. package/packages/server/src/model-proxy/recursion-guard.ts +72 -0
  122. package/packages/server/src/model-proxy/registry-singleton.ts +109 -0
  123. package/packages/server/src/model-proxy/request-log.ts +53 -0
  124. package/packages/server/src/model-proxy/streamer.ts +59 -0
  125. package/packages/server/src/openspec-group-store.ts +490 -0
  126. package/packages/server/src/pending-client-correlations.ts +73 -0
  127. package/packages/server/src/pending-fork-registry.ts +24 -12
  128. package/packages/server/src/pi-core-checker.ts +77 -17
  129. package/packages/server/src/pi-core-updater.ts +16 -6
  130. package/packages/server/src/pi-dev-version-check.ts +145 -0
  131. package/packages/server/src/pi-gateway.ts +4 -0
  132. package/packages/server/src/pi-version-skew.ts +12 -4
  133. package/packages/server/src/process-manager.ts +182 -11
  134. package/packages/server/src/provider-auth-storage.ts +29 -47
  135. package/packages/server/src/provider-catalogue-cache.ts +24 -18
  136. package/packages/server/src/restart-helper.ts +17 -16
  137. package/packages/server/src/routes/bootstrap-routes.ts +37 -0
  138. package/packages/server/src/routes/jj-routes.ts +3 -0
  139. package/packages/server/src/routes/model-proxy-api-key-routes.ts +168 -0
  140. package/packages/server/src/routes/model-proxy-refresh-routes.ts +24 -0
  141. package/packages/server/src/routes/model-proxy-routes.ts +330 -0
  142. package/packages/server/src/routes/openspec-group-routes.ts +231 -0
  143. package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
  144. package/packages/server/src/routes/pi-core-routes.ts +1 -1
  145. package/packages/server/src/routes/provider-auth-routes.ts +8 -1
  146. package/packages/server/src/routes/provider-routes.ts +28 -5
  147. package/packages/server/src/routes/system-routes.ts +44 -2
  148. package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi-shim.sh +9 -0
  149. package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi.cjs +50 -0
  150. package/packages/server/src/rpc-keeper/__tests__/keeper.test.ts +371 -0
  151. package/packages/server/src/rpc-keeper/dispatch-router.ts +85 -0
  152. package/packages/server/src/rpc-keeper/keeper-manager.ts +364 -0
  153. package/packages/server/src/rpc-keeper/keeper.cjs +313 -0
  154. package/packages/server/src/server.ts +254 -60
  155. package/packages/server/src/session-api.ts +63 -4
  156. package/packages/server/src/session-discovery.ts +1 -1
  157. package/packages/server/src/session-file-reader.ts +1 -1
  158. package/packages/server/src/spawn-register-watchdog.ts +62 -7
  159. package/packages/server/src/spawn-token.ts +20 -0
  160. package/packages/server/src/tunnel-watchdog.ts +230 -0
  161. package/packages/server/src/tunnel.ts +5 -1
  162. package/packages/shared/package.json +1 -1
  163. package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +228 -0
  164. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +24 -17
  165. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
  166. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
  167. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
  168. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
  169. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
  170. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +5 -3
  171. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
  172. package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
  173. package/packages/shared/src/__tests__/config-openspec.test.ts +74 -0
  174. package/packages/shared/src/__tests__/model-proxy-config.test.ts +146 -0
  175. package/packages/shared/src/__tests__/no-raw-node-import.test.ts +7 -5
  176. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
  177. package/packages/shared/src/__tests__/node-spawn.test.ts +51 -0
  178. package/packages/shared/src/__tests__/openspec-groups-types.test.ts +135 -0
  179. package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +96 -0
  180. package/packages/shared/src/__tests__/recommended-extensions.test.ts +11 -3
  181. package/packages/shared/src/__tests__/server-launcher.test.ts +227 -0
  182. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
  183. package/packages/shared/src/bootstrap-install.ts +1 -1
  184. package/packages/shared/src/browser-protocol.ts +70 -0
  185. package/packages/shared/src/changelog-types.ts +111 -0
  186. package/packages/shared/src/config.ts +172 -2
  187. package/packages/shared/src/dashboard-plugin/manifest-types.ts +16 -1
  188. package/packages/shared/src/dashboard-plugin/slot-props.ts +8 -0
  189. package/packages/shared/src/dashboard-plugin/slot-types.ts +57 -0
  190. package/packages/shared/src/platform/binary-lookup.ts +204 -0
  191. package/packages/shared/src/platform/node-spawn.ts +71 -26
  192. package/packages/shared/src/protocol.ts +27 -1
  193. package/packages/shared/src/recommended-extensions.ts +18 -0
  194. package/packages/shared/src/rest-api.ts +219 -1
  195. package/packages/shared/src/server-launcher.ts +277 -0
  196. package/packages/shared/src/skill-block-parser.ts +1 -1
  197. package/packages/shared/src/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
  198. package/packages/shared/src/tool-registry/definitions.ts +15 -3
  199. package/packages/shared/src/types.ts +62 -0
  200. package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -53
  201. package/packages/shared/src/resolve-jiti.ts +0 -102
@@ -57,6 +57,8 @@ export interface AuthConfig {
57
57
  allowedUsers?: string[];
58
58
  bypassUrls?: string[];
59
59
  bypassHosts?: string[];
60
+ /** Admin email override — can list/revoke every user's proxy API keys. */
61
+ admin?: string;
60
62
  }
61
63
 
62
64
  export interface MemoryLimitsConfig {
@@ -75,6 +77,16 @@ export const DEFAULT_MEMORY_LIMITS: MemoryLimitsConfig = {
75
77
  };
76
78
 
77
79
  export interface OpenSpecPollConfig {
80
+ /**
81
+ * Master gate. When `false`, the dashboard treats OpenSpec as fully disabled
82
+ * across the dashboard — no polling, the OPENSPEC session-card subcard hides
83
+ * everywhere, and `openspec_refresh` is a no-op. Other tuning fields below
84
+ * retain their meaning but are ignored at runtime when this is `false`.
85
+ *
86
+ * Default `true` for backwards compatibility. Existing configs without this
87
+ * field behave exactly as before. See change: auto-hide-empty-session-subcards.
88
+ */
89
+ enabled: boolean;
78
90
  /** Poll interval in seconds. Default 30. Clamped to [5, 3600]. */
79
91
  pollIntervalSeconds: number;
80
92
  /** Max concurrent `openspec` CLI invocations across all dirs. Default 3. Clamped to [1, 16]. */
@@ -86,6 +98,7 @@ export interface OpenSpecPollConfig {
86
98
  }
87
99
 
88
100
  export const DEFAULT_OPENSPEC_POLL: OpenSpecPollConfig = {
101
+ enabled: true,
89
102
  pollIntervalSeconds: 30,
90
103
  maxConcurrentSpawns: 3,
91
104
  changeDetection: "mtime",
@@ -113,6 +126,47 @@ export interface KnownServer {
113
126
  addedAt: string; // ISO timestamp
114
127
  }
115
128
 
129
+ // ── Model Proxy ─────────────────────────────────────────────────────
130
+
131
+ export interface ProxyApiKey {
132
+ id: string;
133
+ label: string;
134
+ createdBy?: string;
135
+ scopes?: string[];
136
+ createdAt: number;
137
+ lastUsedAt?: number;
138
+ expiresAt?: number;
139
+ revokedAt?: number;
140
+ hash: string;
141
+ }
142
+
143
+ export interface ModelProxyConfig {
144
+ /** Master toggle. Default true. */
145
+ enabled: boolean;
146
+ /** Default model for requests that omit it. */
147
+ defaultModel?: string;
148
+ /** Optional second port for /v1/* routes (for SDKs that hardcode path-prefix-less base URLs). */
149
+ secondPort?: number;
150
+ /** Server-wide max concurrent streams. Default 16. Clamped [1, 256]. */
151
+ maxConcurrentStreams: number;
152
+ /** Per-API-key max concurrent streams. Default 4. Clamped [1, 64]. */
153
+ perKeyConcurrentStreams: number;
154
+ /** Per-provider concurrency caps. Keys are provider names. */
155
+ perProviderCaps?: Record<string, number>;
156
+ /** Enable JSONL request logging. Default false. */
157
+ logRequests: boolean;
158
+ /** Proxy API keys (stored hashed). */
159
+ apiKeys: ProxyApiKey[];
160
+ }
161
+
162
+ export const DEFAULT_MODEL_PROXY: ModelProxyConfig = {
163
+ enabled: true,
164
+ maxConcurrentStreams: 16,
165
+ perKeyConcurrentStreams: 4,
166
+ logRequests: false,
167
+ apiKeys: [],
168
+ };
169
+
116
170
  /**
117
171
  * Plugin-specific config namespace.
118
172
  * Lives at ~/.pi/dashboard/config.json#plugins.<id>.*
@@ -126,7 +180,16 @@ export interface DashboardConfig {
126
180
  autoShutdown: boolean;
127
181
  shutdownIdleSeconds: number;
128
182
  spawnStrategy: SpawnStrategy;
129
- tunnel: { enabled: boolean; reservedToken?: string };
183
+ tunnel: {
184
+ enabled: boolean;
185
+ reservedToken?: string;
186
+ watchdog?: {
187
+ enabled: boolean;
188
+ intervalMs: number;
189
+ failureThreshold: number;
190
+ probeTimeoutMs: number;
191
+ };
192
+ };
130
193
  devBuildOnReload: boolean;
131
194
  auth?: AuthConfig;
132
195
  defaultModel: string;
@@ -165,6 +228,17 @@ export interface DashboardConfig {
165
228
  * See change: spawn-failure-diagnostics.
166
229
  */
167
230
  spawnRegisterTimeoutMs: number;
231
+ /**
232
+ * EXPERIMENTAL: when true, headless pi sessions are spawned through the
233
+ * RPC keeper sidecar instead of the legacy `sh -c "tail -f /dev/null | pi"`
234
+ * (Unix) or direct-pipe (Windows) paths. The keeper owns pi's stdin and
235
+ * exposes a per-session UDS / named pipe the server writes RPC `prompt`
236
+ * lines to. Enables typed extension slash commands (`/ctx-stats`,
237
+ * `/curator`, `/agents`, `/flows:*`) to dispatch in headless sessions
238
+ * against pi versions that do not yet expose `pi.dispatchCommand`.
239
+ * Default `false`. See change: add-rpc-stdin-dispatch-with-keeper-sidecar.
240
+ */
241
+ useRpcKeeper: boolean;
168
242
  /**
169
243
  * Per-plugin config namespaces. Reserved top-level key.
170
244
  * Each plugin's config lives at plugins.<id>.*
@@ -172,6 +246,8 @@ export interface DashboardConfig {
172
246
  * until each extract-*-as-plugin change migrates them.
173
247
  */
174
248
  plugins: PluginsConfig;
249
+ /** Model proxy configuration (OpenAI/Anthropic-compatible /v1/* endpoints). */
250
+ modelProxy: ModelProxyConfig;
175
251
  }
176
252
 
177
253
  export interface CorsConfig {
@@ -193,13 +269,22 @@ export function clampSpawnRegisterTimeoutMs(v: unknown): number {
193
269
 
194
270
  const DEFAULTS: DashboardConfig = {
195
271
  plugins: {},
272
+ modelProxy: { ...DEFAULT_MODEL_PROXY },
196
273
  port: 8000,
197
274
  piPort: 9999,
198
275
  autoStart: true,
199
276
  autoShutdown: false,
200
277
  shutdownIdleSeconds: 300,
201
278
  spawnStrategy: "headless",
202
- tunnel: { enabled: true },
279
+ tunnel: {
280
+ enabled: true,
281
+ watchdog: {
282
+ enabled: true,
283
+ intervalMs: 60000,
284
+ failureThreshold: 2,
285
+ probeTimeoutMs: 10000,
286
+ },
287
+ },
203
288
  devBuildOnReload: false,
204
289
  defaultModel: "",
205
290
  memoryLimits: { ...DEFAULT_MEMORY_LIMITS },
@@ -213,6 +298,7 @@ const DEFAULTS: DashboardConfig = {
213
298
  askUserPromptTimeoutSeconds: DEFAULT_ASK_USER_PROMPT_TIMEOUT_SECONDS,
214
299
  reattachPlacement: DEFAULT_REATTACH_PLACEMENT,
215
300
  spawnRegisterTimeoutMs: 30000,
301
+ useRpcKeeper: false,
216
302
  };
217
303
 
218
304
  /**
@@ -274,6 +360,7 @@ function parseAuthConfig(raw: any): AuthConfig | undefined {
274
360
  ...(Array.isArray(raw.allowedUsers) ? { allowedUsers: raw.allowedUsers } : Array.isArray(raw.allowedEmails) ? { allowedUsers: raw.allowedEmails } : {}),
275
361
  bypassUrls: Array.isArray(raw.bypassUrls) ? raw.bypassUrls.filter((u: unknown) => typeof u === "string") : [],
276
362
  bypassHosts: Array.isArray(raw.bypassHosts) ? raw.bypassHosts.filter((u: unknown) => typeof u === "string") : [],
363
+ ...(typeof raw.admin === "string" && raw.admin ? { admin: raw.admin } : {}),
277
364
  };
278
365
  }
279
366
 
@@ -300,6 +387,8 @@ function parseOpenSpecPollConfig(raw: any): OpenSpecPollConfig {
300
387
  ? raw.changeDetection
301
388
  : DEFAULT_OPENSPEC_POLL.changeDetection;
302
389
  return {
390
+ enabled:
391
+ typeof raw.enabled === "boolean" ? raw.enabled : DEFAULT_OPENSPEC_POLL.enabled,
303
392
  pollIntervalSeconds: clampNumber(raw.pollIntervalSeconds, DEFAULT_OPENSPEC_POLL.pollIntervalSeconds, 5, 3600),
304
393
  maxConcurrentSpawns: clampNumber(raw.maxConcurrentSpawns, DEFAULT_OPENSPEC_POLL.maxConcurrentSpawns, 1, 16),
305
394
  changeDetection,
@@ -346,6 +435,70 @@ export function getPluginConfig(
346
435
  return config.plugins?.[pluginId] ?? {};
347
436
  }
348
437
 
438
+ export function parseModelProxyConfig(raw: any): ModelProxyConfig {
439
+ if (!raw || typeof raw !== "object") return { ...DEFAULT_MODEL_PROXY };
440
+
441
+ const apiKeys: ProxyApiKey[] = [];
442
+ if (Array.isArray(raw.apiKeys)) {
443
+ for (const entry of raw.apiKeys) {
444
+ if (
445
+ entry &&
446
+ typeof entry === "object" &&
447
+ typeof entry.id === "string" &&
448
+ typeof entry.label === "string" &&
449
+ typeof entry.hash === "string" &&
450
+ typeof entry.createdAt === "number"
451
+ ) {
452
+ apiKeys.push({
453
+ id: entry.id,
454
+ label: entry.label,
455
+ hash: entry.hash,
456
+ createdAt: entry.createdAt,
457
+ ...(typeof entry.createdBy === "string" ? { createdBy: entry.createdBy } : {}),
458
+ ...(Array.isArray(entry.scopes) ? { scopes: entry.scopes.filter((s: unknown) => typeof s === "string") } : {}),
459
+ ...(typeof entry.lastUsedAt === "number" ? { lastUsedAt: entry.lastUsedAt } : {}),
460
+ ...(typeof entry.expiresAt === "number" ? { expiresAt: entry.expiresAt } : {}),
461
+ ...(typeof entry.revokedAt === "number" ? { revokedAt: entry.revokedAt } : {}),
462
+ });
463
+ }
464
+ }
465
+ }
466
+
467
+ let perProviderCaps: Record<string, number> | undefined;
468
+ if (raw.perProviderCaps && typeof raw.perProviderCaps === "object" && !Array.isArray(raw.perProviderCaps)) {
469
+ perProviderCaps = {};
470
+ for (const [key, val] of Object.entries(raw.perProviderCaps)) {
471
+ if (typeof val === "number" && Number.isFinite(val) && val >= 1) {
472
+ perProviderCaps[key] = Math.min(val, 256);
473
+ }
474
+ }
475
+ }
476
+
477
+ return {
478
+ enabled: typeof raw.enabled === "boolean" ? raw.enabled : DEFAULT_MODEL_PROXY.enabled,
479
+ ...(typeof raw.defaultModel === "string" ? { defaultModel: raw.defaultModel } : {}),
480
+ ...(typeof raw.secondPort === "number" && raw.secondPort >= 1024 && raw.secondPort <= 65535
481
+ ? { secondPort: raw.secondPort }
482
+ : {}),
483
+ maxConcurrentStreams: clampNumber(
484
+ raw.maxConcurrentStreams,
485
+ DEFAULT_MODEL_PROXY.maxConcurrentStreams,
486
+ 1,
487
+ 256,
488
+ ),
489
+ perKeyConcurrentStreams: clampNumber(
490
+ raw.perKeyConcurrentStreams,
491
+ DEFAULT_MODEL_PROXY.perKeyConcurrentStreams,
492
+ 1,
493
+ 64,
494
+ ),
495
+ ...(perProviderCaps ? { perProviderCaps } : {}),
496
+ logRequests:
497
+ typeof raw.logRequests === "boolean" ? raw.logRequests : DEFAULT_MODEL_PROXY.logRequests,
498
+ apiKeys,
499
+ };
500
+ }
501
+
349
502
  function parseKnownServers(raw: any): KnownServer[] {
350
503
  if (!Array.isArray(raw)) return [];
351
504
  return raw
@@ -391,6 +544,21 @@ export function loadConfig(): DashboardConfig {
391
544
  tunnel: {
392
545
  enabled: parsed.tunnel?.enabled ?? defaults.tunnel.enabled,
393
546
  ...(parsed.tunnel?.reservedToken ? { reservedToken: parsed.tunnel.reservedToken } : {}),
547
+ watchdog: {
548
+ enabled: parsed.tunnel?.watchdog?.enabled ?? defaults.tunnel.watchdog!.enabled,
549
+ intervalMs:
550
+ typeof parsed.tunnel?.watchdog?.intervalMs === "number" && parsed.tunnel.watchdog.intervalMs > 0
551
+ ? parsed.tunnel.watchdog.intervalMs
552
+ : defaults.tunnel.watchdog!.intervalMs,
553
+ failureThreshold:
554
+ typeof parsed.tunnel?.watchdog?.failureThreshold === "number" && parsed.tunnel.watchdog.failureThreshold > 0
555
+ ? Math.floor(parsed.tunnel.watchdog.failureThreshold)
556
+ : defaults.tunnel.watchdog!.failureThreshold,
557
+ probeTimeoutMs:
558
+ typeof parsed.tunnel?.watchdog?.probeTimeoutMs === "number" && parsed.tunnel.watchdog.probeTimeoutMs > 0
559
+ ? parsed.tunnel.watchdog.probeTimeoutMs
560
+ : defaults.tunnel.watchdog!.probeTimeoutMs,
561
+ },
394
562
  },
395
563
  devBuildOnReload: parsed.devBuildOnReload ?? defaults.devBuildOnReload,
396
564
  defaultModel: typeof parsed.defaultModel === "string" ? parsed.defaultModel : defaults.defaultModel,
@@ -414,6 +582,8 @@ export function loadConfig(): DashboardConfig {
414
582
  ? parsed.askUserPromptTimeoutSeconds
415
583
  : defaults.askUserPromptTimeoutSeconds,
416
584
  spawnRegisterTimeoutMs: clampSpawnRegisterTimeoutMs(parsed.spawnRegisterTimeoutMs),
585
+ useRpcKeeper: parsed.useRpcKeeper === true,
586
+ modelProxy: parseModelProxyConfig(parsed.modelProxy),
417
587
  };
418
588
 
419
589
  // Compute resolvedTrustedNetworks: merge trustedNetworks + auth.bypassHosts
@@ -21,8 +21,23 @@ export interface PluginClaim {
21
21
  tab?: SettingsTab;
22
22
  /** Slot-specific extra config. */
23
23
  config?: Record<string, unknown>;
24
- /** Optional exported predicate function name for filtering contributions. */
24
+ /**
25
+ * Optional exported predicate function name for filtering contributions.
26
+ * Answers "does this claim apply to this target?" — a claim failing its
27
+ * predicate is removed from the slot's claim list entirely.
28
+ */
25
29
  predicate?: string;
30
+ /**
31
+ * Optional exported sync function name. Answers "will this claim's component
32
+ * produce visible output for this target?" — a claim whose shouldRender
33
+ * returns false is NOT mounted and counts as absent for the wrapper-gate
34
+ * helpers (e.g. `useSlotHasClaimsForSession`), so parent subcards hide
35
+ * cleanly. Use when the component conditionally returns `null` based on
36
+ * dynamic state (e.g. "extension not installed"). MUST be synchronous.
37
+ *
38
+ * See change: auto-hide-empty-session-subcards.
39
+ */
40
+ shouldRender?: string;
26
41
  }
27
42
 
28
43
  /**
@@ -38,6 +38,14 @@ export interface SlotPropsMap {
38
38
  session: DashboardSession;
39
39
  pluginContext: AnyPluginContext;
40
40
  };
41
+ "session-card-memory": {
42
+ session: DashboardSession;
43
+ pluginContext: AnyPluginContext;
44
+ };
45
+ "workspace-action-bar": {
46
+ session: DashboardSession;
47
+ pluginContext: AnyPluginContext;
48
+ };
41
49
  "content-view": {
42
50
  session: DashboardSession;
43
51
  routeParams: Record<string, string>;
@@ -6,17 +6,22 @@
6
6
  * Adding a slot: minor (non-breaking).
7
7
  * Removing or renaming a slot: major (breaking).
8
8
  */
9
+ import type { DashboardSession } from "../types.js";
10
+ import type { FolderDescriptor } from "./slot-props.js";
9
11
 
10
12
  /** All valid slot ids (frozen for v0.x). */
11
13
  export type SlotId =
12
14
  // React-only slots
13
15
  | "sidebar-folder-section"
14
16
  | "session-card-action-bar"
17
+ | "workspace-action-bar"
18
+ // (session-card-memory is also react-only; declared below for ordering)
15
19
  | "content-inline-footer"
16
20
  | "anchored-popover"
17
21
  | "command-route"
18
22
  | "tool-renderer"
19
23
  // React-or-descriptor slots
24
+ | "session-card-memory"
20
25
  | "session-card-badge"
21
26
  | "content-view"
22
27
  | "content-header-sticky"
@@ -59,6 +64,16 @@ export const SLOT_DEFINITIONS: Record<SlotId, SlotDefinition> = {
59
64
  payloadTier: "react-only",
60
65
  description: "Action buttons on a session card",
61
66
  },
67
+ "session-card-memory": {
68
+ multiplicity: "many",
69
+ payloadTier: "react-only",
70
+ description: "Memory/Honcho contributions inside the MEMORY subcard of a session card",
71
+ },
72
+ "workspace-action-bar": {
73
+ multiplicity: "many",
74
+ payloadTier: "react-only",
75
+ description: "Action buttons inside the WORKSPACE subcard of a session card (jj/git workspace tooling)",
76
+ },
62
77
  "content-view": {
63
78
  multiplicity: "one-active",
64
79
  payloadTier: "react-or-descriptor",
@@ -149,3 +164,45 @@ export const VALID_SETTINGS_TABS: SettingsTab[] = [
149
164
  "security",
150
165
  "advanced",
151
166
  ];
167
+
168
+ // ── Predicate input classification ──────────────────────────────────────────
169
+ //
170
+ // `ClaimEntry.predicate` / `.shouldRender` are invoked by exactly two filter
171
+ // helpers in the runtime (`forSession`, `forFolder`). The argument shape is
172
+ // therefore determined by the slot id. `SlotPredicateInput<S>` exposes that
173
+ // classification at the type level so the registry contract can be tightened.
174
+ //
175
+ // See change: slot-generic-claim-entry.
176
+
177
+ /** Slot ids whose predicates receive a session. */
178
+ type SessionScopedSlot =
179
+ | "session-card-badge"
180
+ | "session-card-action-bar"
181
+ | "session-card-memory"
182
+ | "workspace-action-bar"
183
+ | "content-view"
184
+ | "content-header-sticky"
185
+ | "content-inline-footer"
186
+ | "command-route";
187
+
188
+ /** Slot ids whose predicates receive a folder descriptor. */
189
+ type FolderScopedSlot = "sidebar-folder-section";
190
+
191
+ /**
192
+ * Map of slot id → predicate input shape. Resolves to `never` for slots that
193
+ * are not filtered by predicate (settings-section, tool-renderer, anchored-
194
+ * popover, every descriptor-only slot). Registering a predicate on a `never`-
195
+ * input slot is therefore a compile-time error.
196
+ */
197
+ export type SlotPredicateInput<S extends SlotId> =
198
+ S extends SessionScopedSlot ? DashboardSession | null | undefined
199
+ : S extends FolderScopedSlot ? FolderDescriptor
200
+ : never;
201
+
202
+ // Type-level test: assert every SlotId is reachable through SlotPredicateInput,
203
+ // either by mapping to a concrete input or explicitly to `never`. This forces a
204
+ // build failure when a new slot id is added without classifying it.
205
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
206
+ type _AssertAllSlotsPredicateClassified = {
207
+ [K in SlotId]: SlotPredicateInput<K>;
208
+ };
@@ -5,10 +5,117 @@
5
5
  */
6
6
  import { execSync, spawnSync, buildSafeArgv } from "./exec.js";
7
7
  import { existsSync, realpathSync } from "node:fs";
8
+ import { createRequire } from "node:module";
9
+ import { pathToFileURL } from "node:url";
8
10
  import path from "node:path";
9
11
  import os from "node:os";
10
12
  import { MANAGED_BIN, MANAGED_DIR } from "../managed-paths.js";
11
13
 
14
+ // ── jiti loader resolution constants ──────────────────────────────────────
15
+
16
+ /**
17
+ * Pi-coding-agent package names searched for a managed-install jiti.
18
+ * Upstream first, legacy fork second. Mirrors the prior
19
+ * `resolveJitiFromPi` wrapper that lived in two electron files.
20
+ */
21
+ export const MANAGED_PI_PACKAGES = [
22
+ "@earendil-works/pi-coding-agent",
23
+ "@mariozechner/pi-coding-agent",
24
+ ] as const;
25
+
26
+ /**
27
+ * jiti provider package names tried inside every anchor's resolution
28
+ * chain. Upstream `jiti` first; legacy `@mariozechner/jiti` fallback.
29
+ * Carried over verbatim from `resolve-jiti.ts`.
30
+ */
31
+ export const JITI_PACKAGES = ["jiti", "@mariozechner/jiti"] as const;
32
+
33
+ /**
34
+ * Test seam: a function that takes a package specifier (e.g.
35
+ * `"jiti/package.json"`) and returns the resolved path. Production
36
+ * supplies `createRequire(<anchor>).resolve`; tests supply a stub.
37
+ * Carried over verbatim from `resolve-jiti.ts`.
38
+ */
39
+ export type JitiResolver = (specifier: string) => string;
40
+
41
+ /**
42
+ * Pure helper: given a jiti package.json path, return the file:// URL
43
+ * of its register hook. Handles Windows-style drive letters regardless
44
+ * of host OS (`pathToFileURL` on POSIX URL-encodes backslashes).
45
+ *
46
+ * Mirrors the prior helper from the deleted `resolve-jiti.ts`.
47
+ */
48
+ function jitiRegisterUrl(pkgJsonPath: string): string {
49
+ const isWindowsStyle = /^[A-Za-z]:[\\/]/.test(pkgJsonPath);
50
+ if (isWindowsStyle) {
51
+ const registerPath = path.win32.join(
52
+ path.win32.dirname(pkgJsonPath),
53
+ "lib",
54
+ "jiti-register.mjs",
55
+ );
56
+ return `file:///${registerPath.replace(/\\/g, "/")}`;
57
+ }
58
+ const registerPath = path.join(path.dirname(pkgJsonPath), "lib", "jiti-register.mjs");
59
+ return pathToFileURL(registerPath).href;
60
+ }
61
+
62
+ /**
63
+ * Walk JITI_PACKAGES with the supplied resolver and return the first
64
+ * hit's register URL. Optional `pathExists` guard rejects hits whose
65
+ * `lib/jiti-register.mjs` is absent on disk (corrupt installs). When
66
+ * `pathExists` is omitted, the package.json hit alone is sufficient
67
+ * — used by the argv/system-pi anchors which already realpath'd a
68
+ * trustworthy node_modules tree.
69
+ */
70
+ function walkJiti(
71
+ resolver: JitiResolver,
72
+ pathExists?: (p: string) => boolean,
73
+ ): string | null {
74
+ for (const jiti of JITI_PACKAGES) {
75
+ try {
76
+ const pkgJson = resolver(`${jiti}/package.json`);
77
+ if (pathExists) {
78
+ const registerPath = path.join(path.dirname(pkgJson), "lib", "jiti-register.mjs");
79
+ if (!pathExists(registerPath)) continue;
80
+ }
81
+ return jitiRegisterUrl(pkgJson);
82
+ } catch { /* next */ }
83
+ }
84
+ return null;
85
+ }
86
+
87
+ /**
88
+ * Internal deps for `ToolResolver.resolveJiti`. Underscore-prefixed
89
+ * fields are test seams — production callers pass only `anchor` /
90
+ * `resolver`.
91
+ */
92
+ export interface ResolveJitiOpts {
93
+ /** Caller-supplied anchor inside a node_modules tree (e.g. cliPath). */
94
+ anchor?: string;
95
+ /**
96
+ * When true, skip the managed-pi / system-pi / argv anchors and try
97
+ * ONLY `opts.anchor`. Used for health-probing a specific extracted
98
+ * tree where finding jiti elsewhere does not prove the tree itself
99
+ * is healthy.
100
+ */
101
+ anchorOnly?: boolean;
102
+ /**
103
+ * Test seam: replaces `createRequire(<anchor>).resolve` at every
104
+ * probe site. Production omits — actual `createRequire` is used.
105
+ */
106
+ resolver?: JitiResolver;
107
+ /** Test seam: replaces `existsSync` for managed-pi pkg.json + register guards. */
108
+ _pathExists?: (p: string) => boolean;
109
+ /** Test seam: replaces `realpathSync` for system-pi + argv anchors. */
110
+ _realpath?: (p: string) => string;
111
+ /** Test seam: replaces this resolver's `which("pi")` call. */
112
+ _whichPi?: () => string | null;
113
+ /** Test seam: replaces `process.argv[1]`. */
114
+ _argv1?: string | undefined;
115
+ /** Test seam: replaces `~/.pi-dashboard` root used to probe managed-pi. */
116
+ _managedDir?: string;
117
+ }
118
+
12
119
  // ── AppImage self-hit guard (Linux power-user mode safety) ────────────────
13
120
 
14
121
  /**
@@ -235,6 +342,103 @@ export class ToolResolver {
235
342
  return this.which("node");
236
343
  }
237
344
 
345
+ /**
346
+ * Resolve pi's jiti register hook as a `file://` URL.
347
+ *
348
+ * Resolution order (first hit wins):
349
+ * 1. Managed pi install — `~/.pi-dashboard/node_modules/<pkg>` for
350
+ * every entry of `MANAGED_PI_PACKAGES` (upstream first, legacy
351
+ * fallback). Each candidate's pkg.json is the createRequire
352
+ * anchor; walk `JITI_PACKAGES` from there.
353
+ * 2. System pi via `this.which("pi")` (realpathed to escape
354
+ * symlinks like `/usr/local/bin/pi → .../dist/cli.js`).
355
+ * 3. Caller-supplied `opts.anchor` (e.g. an electron `cliPath`).
356
+ * 4. `process.argv[1]` (the running pi/node entry; populated for
357
+ * bridge-extension callers).
358
+ *
359
+ * Returns null when none yield a jiti install. Preserves the
360
+ * Windows drive-letter URL-wrapping contract that previously lived
361
+ * on the prior `buildJitiRegisterUrl` helper in `resolve-jiti.ts`.
362
+ *
363
+ * Tests inject `_pathExists` / `_realpath` / `_whichPi` / `_argv1`
364
+ * / `_managedDir` and a flat `resolver` to exercise individual
365
+ * anchors deterministically.
366
+ *
367
+ * Subsumes the previous `resolveJitiImport`, `resolveJitiFromAnchor`,
368
+ * `pickJitiRegisterUrl`, `pickJitiFromAnchor`, `buildJitiRegisterUrl`,
369
+ * and the duplicate `resolveJitiFromPi` wrappers in electron.
370
+ */
371
+ resolveJiti(opts: ResolveJitiOpts = {}): string | null {
372
+ const pathExists = opts._pathExists ?? existsSync;
373
+ const realpath = opts._realpath ?? realpathSync;
374
+ const whichPi = opts._whichPi ?? (() => this.which("pi"));
375
+ const argv1 = "_argv1" in opts ? opts._argv1 : process.argv[1];
376
+ const managedDir = opts._managedDir ?? MANAGED_DIR;
377
+
378
+ const makeResolver = (anchor: string): JitiResolver | null => {
379
+ if (opts.resolver) return opts.resolver;
380
+ try {
381
+ const req = createRequire(anchor);
382
+ return (spec) => req.resolve(spec);
383
+ } catch {
384
+ return null;
385
+ }
386
+ };
387
+
388
+ // anchor-only short-circuit: skip every anchor except opts.anchor.
389
+ if (opts.anchorOnly) {
390
+ if (!opts.anchor) return null;
391
+ if (!pathExists(opts.anchor)) return null;
392
+ const r = makeResolver(opts.anchor);
393
+ if (!r) return null;
394
+ return walkJiti(r, pathExists);
395
+ }
396
+
397
+ // 1. Managed pi installs.
398
+ for (const pkg of MANAGED_PI_PACKAGES) {
399
+ const pkgJson = path.join(managedDir, "node_modules", pkg, "package.json");
400
+ if (!pathExists(pkgJson)) continue;
401
+ const resolver = makeResolver(pkgJson);
402
+ if (!resolver) continue;
403
+ const url = walkJiti(resolver, pathExists);
404
+ if (url) return url;
405
+ }
406
+
407
+ // 2. System pi.
408
+ const piPath = whichPi();
409
+ if (piPath) {
410
+ let realPi: string;
411
+ try { realPi = realpath(piPath); } catch { realPi = piPath; }
412
+ const resolver = makeResolver(realPi);
413
+ if (resolver) {
414
+ const url = walkJiti(resolver, pathExists);
415
+ if (url) return url;
416
+ }
417
+ }
418
+
419
+ // 3. Caller-supplied anchor.
420
+ if (opts.anchor && pathExists(opts.anchor)) {
421
+ const resolver = makeResolver(opts.anchor);
422
+ if (resolver) {
423
+ const url = walkJiti(resolver, pathExists);
424
+ if (url) return url;
425
+ }
426
+ }
427
+
428
+ // 4. process.argv[1] (or test override).
429
+ if (argv1) {
430
+ let realArgv: string;
431
+ try { realArgv = realpath(argv1); } catch { realArgv = argv1; }
432
+ const resolver = makeResolver(realArgv);
433
+ if (resolver) {
434
+ const url = walkJiti(resolver, pathExists);
435
+ if (url) return url;
436
+ }
437
+ }
438
+
439
+ return null;
440
+ }
441
+
238
442
  /**
239
443
  * Build a spawn environment with managed bin, node bin, extra dirs,
240
444
  * and common user bin dirs prepended to PATH.