@hachej/boring-workspace 0.1.31 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -21,10 +21,11 @@ function createInMemoryBridge() {
21
21
  async postCommand(cmd) {
22
22
  const seq = nextSeq++;
23
23
  const annotated = { ...cmd, seq };
24
- if (subscribers.size === 0) enqueuePending(annotated);
24
+ let delivered = false;
25
25
  for (const handler of subscribers) {
26
- handler(annotated);
26
+ if (handler(annotated) !== false) delivered = true;
27
27
  }
28
+ if (!delivered) enqueuePending(annotated);
28
29
  return { seq, status: "ok" };
29
30
  },
30
31
  subscribeCommands(handler) {
@@ -41,18 +42,170 @@ function createInMemoryBridge() {
41
42
  }
42
43
 
43
44
  // src/server/ui-control/http/uiRoutes.ts
45
+ import { z as z2 } from "zod";
46
+
47
+ // src/server/ui-control/panelStatus/paneRenderStatusStore.ts
48
+ var DEFAULT_STATUS_TTL_MS = 5 * 6e4;
49
+ var DEFAULT_UI_CONTACT_TTL_MS = 3e4;
50
+ var DEFAULT_WORKSPACE_ID = "default";
51
+ var MAX_ERROR_MESSAGE_LENGTH = 500;
52
+ function normalizeWorkspaceId(workspaceId) {
53
+ const trimmed = workspaceId?.trim();
54
+ return trimmed || DEFAULT_WORKSPACE_ID;
55
+ }
56
+ function statusKey(input) {
57
+ return `${input.workspaceId}\0${input.pluginId}\0${input.panelId}\0${input.panelInstanceId}`;
58
+ }
59
+ function redactMessage(message) {
60
+ return message.replace(/\s+/g, " ").trim().slice(0, MAX_ERROR_MESSAGE_LENGTH);
61
+ }
62
+ function createPaneRenderStatusStore(options = {}) {
63
+ const ttlMs = options.ttlMs ?? DEFAULT_STATUS_TTL_MS;
64
+ const uiContactTtlMs = options.uiContactTtlMs ?? DEFAULT_UI_CONTACT_TTL_MS;
65
+ const now = options.now ?? (() => Date.now());
66
+ const statuses = /* @__PURE__ */ new Map();
67
+ const lastUiContactByWorkspace = /* @__PURE__ */ new Map();
68
+ function pruneExpired(current = now()) {
69
+ for (const [key, status] of statuses) {
70
+ const reportedAtMs = Date.parse(status.reportedAt);
71
+ if (!Number.isFinite(reportedAtMs) || current - reportedAtMs > ttlMs) {
72
+ statuses.delete(key);
73
+ }
74
+ }
75
+ }
76
+ function touchUi(workspaceId) {
77
+ lastUiContactByWorkspace.set(normalizeWorkspaceId(workspaceId), now());
78
+ }
79
+ function hasRecentUiContact(workspaceId) {
80
+ const last = lastUiContactByWorkspace.get(normalizeWorkspaceId(workspaceId));
81
+ return last !== void 0 && now() - last <= uiContactTtlMs;
82
+ }
83
+ return {
84
+ touchUi,
85
+ hasRecentUiContact,
86
+ report(input) {
87
+ pruneExpired();
88
+ const workspaceId = normalizeWorkspaceId(input.workspaceId);
89
+ touchUi(workspaceId);
90
+ const snapshot = {
91
+ workspaceId,
92
+ pluginId: input.pluginId,
93
+ panelId: input.panelId,
94
+ panelInstanceId: input.panelInstanceId,
95
+ state: input.state,
96
+ reportedAt: new Date(now()).toISOString(),
97
+ ...input.revision !== void 0 ? { revision: input.revision } : {},
98
+ ...input.error ? { error: { code: input.error.code, message: redactMessage(input.error.message) } } : {}
99
+ };
100
+ statuses.set(statusKey(snapshot), snapshot);
101
+ return snapshot;
102
+ },
103
+ get(input) {
104
+ pruneExpired();
105
+ const workspaceId = normalizeWorkspaceId(input.workspaceId);
106
+ if (input.pluginId && input.panelId) {
107
+ return statuses.get(statusKey({ workspaceId, pluginId: input.pluginId, panelId: input.panelId, panelInstanceId: input.panelInstanceId }));
108
+ }
109
+ for (const status of statuses.values()) {
110
+ if (status.workspaceId !== workspaceId) continue;
111
+ if (status.panelInstanceId !== input.panelInstanceId) continue;
112
+ if (input.pluginId && status.pluginId !== input.pluginId) continue;
113
+ if (input.panelId && status.panelId !== input.panelId) continue;
114
+ return status;
115
+ }
116
+ return void 0;
117
+ }
118
+ };
119
+ }
120
+
121
+ // src/server/ui-control/http/paneRenderStatusRoutes.ts
44
122
  import { z } from "zod";
123
+ var reportBodySchema = z.object({
124
+ workspaceId: z.string().optional(),
125
+ pluginId: z.string().min(1),
126
+ panelId: z.string().min(1),
127
+ panelInstanceId: z.string().min(1),
128
+ revision: z.number().optional(),
129
+ state: z.enum(["loading", "ready", "error", "missing"]),
130
+ error: z.object({
131
+ code: z.string().min(1),
132
+ message: z.string().min(1)
133
+ }).optional()
134
+ });
135
+ function createBodyValidator(schema) {
136
+ return async function validateBody(request, reply) {
137
+ const parsed = schema.safeParse(request.body);
138
+ if (!parsed.success) {
139
+ const firstIssue = parsed.error.issues[0];
140
+ const fieldName = firstIssue?.path?.map((segment) => String(segment)).join(".");
141
+ reply.code(400).send({
142
+ error: "validation_error",
143
+ message: firstIssue?.message ?? "Invalid request body",
144
+ field: fieldName || void 0
145
+ });
146
+ return;
147
+ }
148
+ request.body = parsed.data;
149
+ };
150
+ }
151
+ function resolvePaneStatusWorkspaceId(request) {
152
+ const headers = request.headers;
153
+ const header = headers["x-boring-workspace-id"] ?? headers["X-Boring-Workspace-Id"];
154
+ if (Array.isArray(header)) return header[0];
155
+ if (typeof header === "string" && header.trim()) return header;
156
+ const query = request.query;
157
+ const workspaceId = query?.workspaceId;
158
+ return typeof workspaceId === "string" && workspaceId.trim() ? workspaceId : void 0;
159
+ }
160
+ function paneRenderStatusRoutes(app, opts = {}, done) {
161
+ const store = opts.store ?? createPaneRenderStatusStore();
162
+ const validateReport = createBodyValidator(reportBodySchema);
163
+ const getWorkspaceId = async (request) => {
164
+ return await opts.getWorkspaceId?.(request) ?? resolvePaneStatusWorkspaceId(request);
165
+ };
166
+ app.put(
167
+ "/api/v1/ui/panels/status",
168
+ { preHandler: validateReport },
169
+ async (request, reply) => {
170
+ const body = request.body;
171
+ const workspaceId = await getWorkspaceId(request) ?? body.workspaceId;
172
+ const status = store.report({ ...body, workspaceId });
173
+ return reply.code(200).send({ ok: true, status });
174
+ }
175
+ );
176
+ app.get("/api/v1/ui/panels/status", async (request, reply) => {
177
+ const query = request.query;
178
+ const panelInstanceId = query.panelInstanceId;
179
+ if (typeof panelInstanceId !== "string" || !panelInstanceId.trim()) {
180
+ return reply.code(400).send({ error: "validation_error", message: "panelInstanceId is required", field: "panelInstanceId" });
181
+ }
182
+ const workspaceId = await getWorkspaceId(request);
183
+ const pluginId = typeof query.pluginId === "string" ? query.pluginId : void 0;
184
+ const panelId = typeof query.panelId === "string" ? query.panelId : void 0;
185
+ const status = store.get({ workspaceId, panelInstanceId, pluginId, panelId });
186
+ const connected = store.hasRecentUiContact(workspaceId);
187
+ return {
188
+ ok: true,
189
+ connected,
190
+ state: status?.state ?? (connected ? "missing" : "no-browser-connected"),
191
+ ...status ? { status } : {}
192
+ };
193
+ });
194
+ done();
195
+ }
196
+
197
+ // src/server/ui-control/http/uiRoutes.ts
45
198
  var UI_BRIDGE_PROTOCOL_VERSION = 1;
46
199
  var HEARTBEAT_MS = 15e3;
47
- var setStateBodySchema = z.object({
48
- state: z.record(z.unknown()),
49
- causedBy: z.enum(["user", "agent", "restore"]).optional()
200
+ var setStateBodySchema = z2.object({
201
+ state: z2.record(z2.unknown()),
202
+ causedBy: z2.enum(["user", "agent", "restore"]).optional()
50
203
  });
51
- var postCommandBodySchema = z.object({
52
- kind: z.string().min(1),
53
- params: z.record(z.unknown()).default({})
204
+ var postCommandBodySchema = z2.object({
205
+ kind: z2.string().min(1),
206
+ params: z2.record(z2.unknown()).default({})
54
207
  });
55
- function createBodyValidator(schema) {
208
+ function createBodyValidator2(schema) {
56
209
  return async function validateBody(request, reply) {
57
210
  const parsed = schema.safeParse(request.body);
58
211
  if (!parsed.success) {
@@ -70,8 +223,13 @@ function createBodyValidator(schema) {
70
223
  }
71
224
  function uiRoutes(app, opts, done) {
72
225
  const fallbackBridge = opts.bridge;
73
- const validateSetState = createBodyValidator(setStateBodySchema);
74
- const validatePostCommand = createBodyValidator(postCommandBodySchema);
226
+ const paneStatusStore = opts.paneStatusStore ?? createPaneRenderStatusStore();
227
+ const getPaneWorkspaceId = async (request) => await opts.getWorkspaceId?.(request) ?? resolvePaneStatusWorkspaceId(request);
228
+ const touchUi = async (request) => {
229
+ paneStatusStore.touchUi(await getPaneWorkspaceId(request));
230
+ };
231
+ const validateSetState = createBodyValidator2(setStateBodySchema);
232
+ const validatePostCommand = createBodyValidator2(postCommandBodySchema);
75
233
  const resolveBridge = async (request) => {
76
234
  if (opts.getBridge) return await opts.getBridge(request);
77
235
  if (fallbackBridge) return fallbackBridge;
@@ -83,7 +241,10 @@ function uiRoutes(app, opts, done) {
83
241
  kind: cmd.kind,
84
242
  params: cmd.params
85
243
  });
244
+ paneRenderStatusRoutes(app, { store: paneStatusStore, getWorkspaceId: getPaneWorkspaceId }, () => {
245
+ });
86
246
  app.get("/api/v1/ui/state", async (request) => {
247
+ await touchUi(request);
87
248
  const bridge = await resolveBridge(request);
88
249
  return await bridge.getState() ?? {};
89
250
  });
@@ -91,6 +252,7 @@ function uiRoutes(app, opts, done) {
91
252
  "/api/v1/ui/state",
92
253
  { preHandler: validateSetState },
93
254
  async (request, reply) => {
255
+ await touchUi(request);
94
256
  const body = request.body;
95
257
  const bridge = await resolveBridge(request);
96
258
  const current = await bridge.getState() ?? {};
@@ -112,6 +274,7 @@ function uiRoutes(app, opts, done) {
112
274
  }
113
275
  );
114
276
  app.get("/api/v1/ui/commands/next", async (request, reply) => {
277
+ await touchUi(request);
115
278
  const bridge = await resolveBridge(request);
116
279
  const query = request.query;
117
280
  if (query.poll === "true") {
@@ -141,13 +304,20 @@ data: ${JSON.stringify(encodeCommand(cmd))}
141
304
  }
142
305
  }
143
306
  const unsub = bridge.subscribeCommands((cmd) => {
144
- reply.raw.write(`event: command
307
+ if (reply.raw.destroyed || reply.raw.writableEnded) return false;
308
+ try {
309
+ reply.raw.write(`event: command
145
310
  data: ${JSON.stringify(encodeCommand(cmd))}
146
311
 
147
312
  `);
313
+ return true;
314
+ } catch {
315
+ return false;
316
+ }
148
317
  });
149
318
  const heartbeat = setInterval(() => {
150
319
  if (reply.raw.writableEnded) return;
320
+ void touchUi(request);
151
321
  reply.raw.write(
152
322
  `event: heartbeat
153
323
  data: ${JSON.stringify({ v: UI_BRIDGE_PROTOCOL_VERSION })}
@@ -282,9 +452,9 @@ function isVerified(kind, params, state) {
282
452
  }
283
453
  function createExecUiTool(uiBridge, opts = {}) {
284
454
  const { workspaceRoot, resolvePathKind } = opts;
285
- const verifyDelayMs = opts.verifyDelayMs ?? 200;
286
- const verifyRetries = opts.verifyRetries ?? 2;
287
- const verifyIntervalMs = opts.verifyIntervalMs ?? 200;
455
+ const verifyDelayMs = opts.verifyDelayMs ?? 250;
456
+ const verifyRetries = opts.verifyRetries ?? 20;
457
+ const verifyIntervalMs = opts.verifyIntervalMs ?? 250;
288
458
  return {
289
459
  name: "exec_ui",
290
460
  readinessRequirements: ["ui-bridge"],
@@ -356,11 +526,12 @@ function createExecUiTool(uiBridge, opts = {}) {
356
526
  " showNotification params: { msg: string, level?: 'info'|'warn'|'error' }",
357
527
  "",
358
528
  "Returns { seq, status, uiState? }. For openFile / openPanel / openSurface /",
359
- "closePanel the response includes a `uiState` snapshot taken ~400ms after",
360
- "dispatch \u2014 check uiState.openTabs to confirm the action took effect.",
529
+ "closePanel the response includes a `uiState` snapshot after waiting up",
530
+ "to a few seconds for the browser to dispatch the command \u2014 check",
531
+ "uiState.openTabs to confirm the action took effect.",
361
532
  "If the expected tab is missing from openTabs the frontend silently",
362
533
  "rejected the command (unknown panel component, unregistered surface",
363
- "resolver, or surface not yet ready). For other kinds only { seq, status }",
534
+ "resolver, or disconnected browser). For other kinds only { seq, status }",
364
535
  "is returned. To open a FILE prefer openFile (path-aware) over openPanel",
365
536
  "(which is for non-file panes like charts)."
366
537
  ].join("\n"),
@@ -448,7 +619,7 @@ function createExecUiTool(uiBridge, opts = {}) {
448
619
  await new Promise((r) => setTimeout(r, verifyDelayMs));
449
620
  let uiState = await uiBridge.getState();
450
621
  for (let i = 0; i < verifyRetries; i++) {
451
- if (isVerified(effectiveKind, cmdParams, uiState)) break;
622
+ if (uiState === null || isVerified(effectiveKind, cmdParams, uiState)) break;
452
623
  await new Promise((r) => setTimeout(r, verifyIntervalMs));
453
624
  uiState = await uiBridge.getState();
454
625
  }
@@ -546,6 +717,23 @@ function validateSkills(pluginId, skills) {
546
717
  }
547
718
  }
548
719
  }
720
+ function validatePluginAssets(pluginId, assets) {
721
+ for (let i = 0; i < assets.length; i++) {
722
+ const asset = assets[i];
723
+ if (!asset || typeof asset !== "object") {
724
+ fail(pluginId, `assets[${i}] must be an object`);
725
+ }
726
+ if (!asset.name || typeof asset.name !== "string") {
727
+ fail(pluginId, `assets[${i}].name must be a non-empty string`);
728
+ }
729
+ if (!isPathLike(asset.source)) {
730
+ fail(pluginId, `assets[${i}].source must be a string or URL`);
731
+ }
732
+ if (asset.target !== void 0 && (!asset.target || typeof asset.target !== "string")) {
733
+ fail(pluginId, `assets[${i}].target must be a non-empty string when provided`);
734
+ }
735
+ }
736
+ }
549
737
  function validateProvisioning(pluginId, provisioning) {
550
738
  if (!provisioning || typeof provisioning !== "object") {
551
739
  fail(pluginId, "provisioning must be an object");
@@ -659,6 +847,12 @@ function validateServerPlugin(plugin) {
659
847
  }
660
848
  plugin.agentTools.forEach((tool, index) => validateAgentTool(plugin.id, tool, index));
661
849
  }
850
+ if (plugin.assets !== void 0) {
851
+ if (!Array.isArray(plugin.assets)) {
852
+ fail(plugin.id, "assets must be an array when provided");
853
+ }
854
+ validatePluginAssets(plugin.id, plugin.assets);
855
+ }
662
856
  if (plugin.routes !== void 0 && typeof plugin.routes !== "function") {
663
857
  fail(plugin.id, "routes must be a Fastify plugin function when provided");
664
858
  }
@@ -676,6 +870,20 @@ function defineServerPlugin(plugin) {
676
870
  return { ...plugin };
677
871
  }
678
872
 
873
+ // src/server/plugins/assets.ts
874
+ import { dirname, join } from "path";
875
+ import { fileURLToPath } from "url";
876
+ function definePluginAsset(importMetaUrl, name, relativeSource, options = {}) {
877
+ return {
878
+ name,
879
+ source: new URL(relativeSource, importMetaUrl),
880
+ ...options.target ? { target: options.target } : {}
881
+ };
882
+ }
883
+ function resolvePluginAssetPath(importMetaUrl, assetName) {
884
+ return join(dirname(fileURLToPath(importMetaUrl)), assetName);
885
+ }
886
+
679
887
  // src/server/plugins/bootstrapServer.ts
680
888
  function bootstrapServer(options) {
681
889
  const excludedDefaults = new Set(options.excludeDefaults ?? []);
@@ -723,13 +931,13 @@ function bootstrapServer(options) {
723
931
 
724
932
  // src/server/boringSystemPrompt.ts
725
933
  import { createRequire } from "module";
726
- import { dirname, join } from "path";
934
+ import { dirname as dirname2, join as join2 } from "path";
727
935
  var require2 = createRequire(import.meta.url);
728
936
  function resolveBoringPiRoot(override) {
729
937
  if (override === null) return null;
730
938
  if (override) return override;
731
939
  try {
732
- return dirname(require2.resolve("@hachej/boring-pi/package.json"));
940
+ return dirname2(require2.resolve("@hachej/boring-pi/package.json"));
733
941
  } catch {
734
942
  return null;
735
943
  }
@@ -738,19 +946,19 @@ function buildDocsRefs(boringPiRoot) {
738
946
  return [
739
947
  {
740
948
  topic: "Workflow + how-to + full plugin authoring reference",
741
- path: join(boringPiRoot, "skills/boring-plugin-authoring/SKILL.md")
949
+ path: join2(boringPiRoot, "skills/boring-plugin-authoring/SKILL.md")
742
950
  },
743
951
  {
744
952
  topic: "Panels (registration, dockview, layout)",
745
- path: join(boringPiRoot, "references/workspace/panels.md")
953
+ path: join2(boringPiRoot, "references/workspace/panels.md")
746
954
  },
747
955
  {
748
956
  topic: "Bridge / UI control (get_ui_state, exec_ui)",
749
- path: join(boringPiRoot, "references/workspace/bridge.md")
957
+ path: join2(boringPiRoot, "references/workspace/bridge.md")
750
958
  },
751
959
  {
752
960
  topic: "Server plugins (defineServerPlugin, routes, agent tools)",
753
- path: join(boringPiRoot, "references/workspace/plugins.md")
961
+ path: join2(boringPiRoot, "references/workspace/plugins.md")
754
962
  }
755
963
  ];
756
964
  }
@@ -762,7 +970,7 @@ function buildBoringSystemPrompt(opts) {
762
970
  if (opts.scaffoldCommand) {
763
971
  n += 1;
764
972
  steps.push(
765
- `**${n}. Check plugin-root support, then scaffold.** Bash \`boring-ui plugin-status --json\`; continue only if \`workspaceLocalPluginRoots\` is \`true\`. Then bash \`${opts.scaffoldCommand} <kebab-name> "$BORING_AGENT_WORKSPACE_ROOT"\`. Read generated \`package.json\` + \`front/index.tsx\`; do NOT write from memory.`
973
+ `**${n}. Check plugin-root support, then scaffold.** Bash \`boring-ui-plugin status --json\`; continue only if \`workspaceLocalPluginRoots\` is \`true\`. Then bash \`${opts.scaffoldCommand} <kebab-name> "$BORING_AGENT_WORKSPACE_ROOT"\`. Read generated \`package.json\` + \`front/index.tsx\`; do NOT write from memory.`
766
974
  );
767
975
  } else {
768
976
  n += 1;
@@ -772,7 +980,11 @@ function buildBoringSystemPrompt(opts) {
772
980
  }
773
981
  n += 1;
774
982
  steps.push(
775
- opts.scaffoldCommand ? `**${n}. Edit the generated files to implement the request.** Keep the scaffold imports, \`definePlugin\` shape, and manifest layout; replace only placeholder content/ids/labels with the real implementation.` : `**${n}. Create or edit the plugin files to implement what the user asked for.** Use the boring-plugin-authoring skill as the canonical source for imports, the \`definePlugin\` call shape, and the manifest layout.`
983
+ opts.scaffoldCommand ? `**${n}. Edit the generated files.** Keep scaffold imports/layout. Use \`@hachej/boring-ui-kit\` + workspace primitives for native UI; avoid ad-hoc inline UI.` : `**${n}. Create or edit plugin files.** Use the boring-plugin-authoring skill for imports, \`definePlugin\`, manifest layout, and boring-ui-kit design defaults.`
984
+ );
985
+ n += 1;
986
+ steps.push(
987
+ `**${n}. Install plugin-local deps only when needed.** If adding a browser package, bash \`cd "$BORING_AGENT_WORKSPACE_ROOT/.pi/extensions/<kebab-name>" && npm install <dep>\`; never install at workspace root. \`/reload\` never installs packages.`
776
988
  );
777
989
  n += 1;
778
990
  if (verify) {
@@ -796,7 +1008,7 @@ function buildBoringSystemPrompt(opts) {
796
1008
  "The `boring-plugin-authoring` skill listed under `<available_skills>` is the authoritative reference (read its `<location>`). Additional reference docs (`panels.md`, `bridge.md`, `plugins.md`) are unavailable on this host \u2014 `@hachej/boring-pi` is not installed."
797
1009
  ].join("\n");
798
1010
  return [
799
- "You are operating inside boring-ui. Before `.pi/extensions/<name>/`, run `boring-ui plugin-status --json`; continue only when `workspaceLocalPluginRoots` is `true`. Default to `.pi/extensions/<name>/`. Global `~/.pi/agent/extensions/` only for explicit requests.",
1011
+ "You are operating inside boring-ui. Before `.pi/extensions/<name>/`, run `boring-ui-plugin status --json`; continue only when `workspaceLocalPluginRoots` is `true`. Default to `.pi/extensions/<name>/`. Global `~/.pi/agent/extensions/` only for explicit requests.",
800
1012
  [
801
1013
  "## Plugin authoring \u2014 required workflow",
802
1014
  "",
@@ -811,6 +1023,7 @@ function buildBoringSystemPrompt(opts) {
811
1023
  '- Server/Pi tool method: `handler` \u2014 use `execute`. Return shape: `{ content: [{ type: "text", text }] }` (NEVER a bare string).',
812
1024
  "- Manifest values: `boring.server: true` \u2014 use `false`/omit for hot-reload user plugins, or a relative path string only for advanced boot-time/static server integration.",
813
1025
  "- File layout: files at the package root, or `src/` / `dist/` / `lib/` subdirectories \u2014 the scaffold's hot-reload layout (`front/index.tsx`, optional `agent/index.ts` declared in `pi.extensions`) is the one the workspace refreshes on `/reload`.",
1026
+ "- Dependency installs: do NOT install plugin UI dependencies at the workspace root. Install them inside `.pi/extensions/<name>/` and keep React/workspace/boring-ui-kit imports as host singletons, not plugin dependencies.",
814
1027
  "- Hot-reload agent tools: do NOT put them in `.pi/extensions/<name>/server/index.ts`; use `pi.extensions` instead. `boring.server` requires static composition plus process restart."
815
1028
  ].join("\n"),
816
1029
  docsBlock
@@ -820,7 +1033,7 @@ function buildBoringSystemPrompt(opts) {
820
1033
  // src/server/agentPlugins/manager.ts
821
1034
  import { createHash } from "crypto";
822
1035
  import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync as realpathSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
823
- import { dirname as dirname5, isAbsolute as isAbsolute3, join as join4, relative as relative3, resolve as resolve5 } from "path";
1036
+ import { dirname as dirname6, isAbsolute as isAbsolute3, join as join5, relative as relative3, resolve as resolve5 } from "path";
824
1037
 
825
1038
  // src/shared/plugins/manifest.ts
826
1039
  var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
@@ -977,11 +1190,11 @@ function validateBoringPluginManifest(raw) {
977
1190
 
978
1191
  // src/server/agentPlugins/scan.ts
979
1192
  import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "fs";
980
- import { basename, dirname as dirname3, join as join2, resolve as resolve3 } from "path";
1193
+ import { basename, dirname as dirname4, join as join3, resolve as resolve3 } from "path";
981
1194
 
982
1195
  // src/server/agentPlugins/pluginPaths.ts
983
1196
  import { existsSync, realpathSync } from "fs";
984
- import { dirname as dirname2, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2 } from "path";
1197
+ import { dirname as dirname3, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2 } from "path";
985
1198
  function isInsideRoot(rootReal, targetReal) {
986
1199
  const rel = relative2(rootReal, targetReal);
987
1200
  return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
@@ -990,7 +1203,7 @@ function nearestExistingAncestor(path, rootDir) {
990
1203
  let current = path;
991
1204
  const root = resolve2(rootDir);
992
1205
  while (!existsSync(current)) {
993
- const parent = dirname2(current);
1206
+ const parent = dirname3(current);
994
1207
  if (parent === current) return void 0;
995
1208
  if (!isInsideRoot(root, parent) && parent !== root) return void 0;
996
1209
  current = parent;
@@ -1025,7 +1238,7 @@ function safePluginIdFromPackageJson(pkg, rootDir) {
1025
1238
  return isValidBoringPluginId(id) ? id : void 0;
1026
1239
  }
1027
1240
  function parsePackageJson(rootDir) {
1028
- return JSON.parse(readFileSync(join2(rootDir, "package.json"), "utf8"));
1241
+ return JSON.parse(readFileSync(join3(rootDir, "package.json"), "utf8"));
1029
1242
  }
1030
1243
  function hasPluginMetadata(pkg) {
1031
1244
  return pkg.boring !== void 0 || pkg.pi !== void 0;
@@ -1079,12 +1292,12 @@ function discoverBoringPluginDirs(pluginDirs) {
1079
1292
  if (!existsSync2(dir)) continue;
1080
1293
  const info = statSync(dir);
1081
1294
  if (!info.isDirectory()) continue;
1082
- const hasPackageJson = existsSync2(join2(dir, "package.json"));
1295
+ const hasPackageJson = existsSync2(join3(dir, "package.json"));
1083
1296
  const childPackageDirs = [];
1084
1297
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
1085
1298
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
1086
- const child = join2(dir, entry.name);
1087
- if (existsSync2(join2(child, "package.json"))) childPackageDirs.push(child);
1299
+ const child = join3(dir, entry.name);
1300
+ if (existsSync2(join3(child, "package.json"))) childPackageDirs.push(child);
1088
1301
  }
1089
1302
  if (hasPackageJson) out.add(dir);
1090
1303
  for (const child of childPackageDirs) out.add(child);
@@ -1192,7 +1405,7 @@ function readBoringPlugins(pluginDirs) {
1192
1405
 
1193
1406
  // src/server/agentPlugins/signatureCache.ts
1194
1407
  import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, rmSync, statSync as statSync2, writeFileSync } from "fs";
1195
- import { dirname as dirname4, join as join3 } from "path";
1408
+ import { dirname as dirname5, join as join4 } from "path";
1196
1409
  var PLUGIN_SIGNATURE_CACHE_FILE = ".boring-signature.json";
1197
1410
  function pluginFileSignature(path) {
1198
1411
  if (!path || !existsSync3(path)) return "missing";
@@ -1200,7 +1413,7 @@ function pluginFileSignature(path) {
1200
1413
  return `${stat2.mtimeMs}:${stat2.size}`;
1201
1414
  }
1202
1415
  function cachePath(pluginRootDir) {
1203
- return join3(pluginRootDir, PLUGIN_SIGNATURE_CACHE_FILE);
1416
+ return join4(pluginRootDir, PLUGIN_SIGNATURE_CACHE_FILE);
1204
1417
  }
1205
1418
  function writePluginSignatureCache(pluginRootDir, payload) {
1206
1419
  const full = {
@@ -1209,7 +1422,7 @@ function writePluginSignatureCache(pluginRootDir, payload) {
1209
1422
  loadedAt: payload.loadedAt ?? Date.now()
1210
1423
  };
1211
1424
  const path = cachePath(pluginRootDir);
1212
- mkdirSync(dirname4(path), { recursive: true });
1425
+ mkdirSync(dirname5(path), { recursive: true });
1213
1426
  writeFileSync(path, `${JSON.stringify(full, null, 2)}
1214
1427
  `, "utf8");
1215
1428
  }
@@ -1281,7 +1494,7 @@ function normalizeBoringPluginPiPackages(plugins) {
1281
1494
 
1282
1495
  // src/server/agentPlugins/manager.ts
1283
1496
  function skillPathForPiLoader(path) {
1284
- return existsSync4(join4(path, "SKILL.md")) ? dirname5(path) : path;
1497
+ return existsSync4(join5(path, "SKILL.md")) ? dirname6(path) : path;
1285
1498
  }
1286
1499
  function preflightErrorId(pluginDir) {
1287
1500
  return `preflight-${createHash("sha256").update(pluginDir).digest("hex").slice(0, 12)}`;
@@ -1303,7 +1516,7 @@ function directorySignature(root) {
1303
1516
  const entries = readdirSync2(dir, { withFileTypes: true }).filter((entry) => !entry.name.startsWith(".") && entry.name !== "node_modules").sort((a, b) => a.name.localeCompare(b.name));
1304
1517
  for (const entry of entries) {
1305
1518
  count++;
1306
- const path = join4(dir, entry.name);
1519
+ const path = join5(dir, entry.name);
1307
1520
  const rel = relative3(root, path);
1308
1521
  const stat2 = lstatSync(path);
1309
1522
  if (stat2.isSymbolicLink()) {
@@ -1346,12 +1559,12 @@ function normalizePluginSubpath(rootDir, path) {
1346
1559
  }
1347
1560
  function frontSignatureRoot(plugin) {
1348
1561
  if (!plugin.frontPath) return void 0;
1349
- const frontRoot = join4(plugin.rootDir, "front");
1562
+ const frontRoot = join5(plugin.rootDir, "front");
1350
1563
  const rel = relative3(frontRoot, plugin.frontPath);
1351
- return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel) ? frontRoot : dirname5(plugin.frontPath);
1564
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel) ? frontRoot : dirname6(plugin.frontPath);
1352
1565
  }
1353
1566
  function pluginSignature(plugin) {
1354
- return createHash("sha256").update(JSON.stringify(plugin.boring)).update(JSON.stringify(plugin.pi ?? {})).update(plugin.version).update(plugin.frontPath ?? "").update(pluginFileSignature(plugin.frontPath)).update(directorySignature(frontSignatureRoot(plugin))).update(directorySignature(join4(plugin.rootDir, "shared"))).update(plugin.serverPath ?? "").update(pluginFileSignature(plugin.serverPath)).update(directorySignature(plugin.serverPath ? dirname5(plugin.serverPath) : void 0)).update((plugin.extensionPaths ?? []).join("\0")).update((plugin.skillPaths ?? []).join("\0")).digest("hex");
1567
+ return createHash("sha256").update(JSON.stringify(plugin.boring)).update(JSON.stringify(plugin.pi ?? {})).update(plugin.version).update(plugin.frontPath ?? "").update(pluginFileSignature(plugin.frontPath)).update(directorySignature(frontSignatureRoot(plugin))).update(directorySignature(join5(plugin.rootDir, "shared"))).update(plugin.serverPath ?? "").update(pluginFileSignature(plugin.serverPath)).update(directorySignature(plugin.serverPath ? dirname6(plugin.serverPath) : void 0)).update((plugin.extensionPaths ?? []).join("\0")).update((plugin.skillPaths ?? []).join("\0")).digest("hex");
1355
1568
  }
1356
1569
  function computeRequiresRestart(previous, next) {
1357
1570
  if (!previous) return [];
@@ -1376,7 +1589,7 @@ var BoringPluginAssetManager = class {
1376
1589
  reloadQueued = false;
1377
1590
  constructor(options) {
1378
1591
  this.pluginDirs = options.pluginDirs;
1379
- this.errorRoot = options.errorRoot ?? join4(process.cwd(), ".pi", "extensions");
1592
+ this.errorRoot = options.errorRoot ?? join5(process.cwd(), ".pi", "extensions");
1380
1593
  this.frontTargetResolver = options.frontTargetResolver;
1381
1594
  this.includeLegacyFrontUrl = options.includeLegacyFrontUrl ?? true;
1382
1595
  }
@@ -1580,7 +1793,7 @@ Plugin dir: ${error.pluginDir}`;
1580
1793
  writeError(pluginId, message) {
1581
1794
  const path = this.errorPath(pluginId);
1582
1795
  if (!path) return;
1583
- mkdirSync2(dirname5(path), { recursive: true });
1796
+ mkdirSync2(dirname6(path), { recursive: true });
1584
1797
  writeFileSync2(path, message, "utf8");
1585
1798
  }
1586
1799
  clearError(pluginId) {
@@ -1717,11 +1930,13 @@ export {
1717
1930
  createGetUiStateTool,
1718
1931
  createInMemoryBridge,
1719
1932
  createWorkspaceUiTools,
1933
+ definePluginAsset,
1720
1934
  defineServerPlugin,
1721
1935
  pluginFileSignature,
1722
1936
  preflightBoringPlugins,
1723
1937
  readBoringPlugins,
1724
1938
  readPluginSignatureCache,
1939
+ resolvePluginAssetPath,
1725
1940
  scanBoringPlugins,
1726
1941
  uiRoutes,
1727
1942
  validateServerPlugin,
package/dist/shared.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { A as AgentTool, C as CommandResult, J as JSONSchema, T as ToolExecContext, c as ToolResult, U as UiBridge, a as UiCommand, b as UiState } from './ui-bridge-Bdgl2hR8.js';
2
- export { C as CommandConfig, P as PaneProps, a as PanelConfig, b as PanelRegistration, S as SurfaceOpenRequest, c as SurfacePanelResolution, d as SurfaceResolverConfig, e as SurfaceResolverRegistration, W as WORKSPACE_OPEN_PATH_SURFACE_KIND, f as definePanel } from './surface-CEEkd81D.js';
1
+ export { A as AgentTool, C as CommandResult, J as JSONSchema, T as ToolExecContext, c as ToolResult, U as UiBridge, a as UiCommand, b as UiState } from './ui-bridge-DFNem0df.js';
2
+ export { C as CommandConfig, P as PaneProps, a as PanelConfig, b as PanelRegistration, S as SurfaceOpenRequest, c as SurfacePanelResolution, d as SurfaceResolverConfig, e as SurfaceResolverRegistration, W as WORKSPACE_OPEN_PATH_SURFACE_KIND, f as definePanel } from './surface-obE7YwJk.js';
3
3
  import 'react';
4
4
  import 'dockview-react';
5
5
 
@@ -50,6 +50,8 @@ interface PanelConfig<T = any> {
50
50
  /** Source: "builtin" | "app" */
51
51
  source?: string;
52
52
  pluginId?: string;
53
+ /** Revision emitted by the runtime plugin asset manager for hot-loaded panels. */
54
+ pluginRevision?: number;
53
55
  /**
54
56
  * Whether to wrap the component with React.lazy + Suspense. Omit to let
55
57
  * the registry auto-detect: zero-arg functions (factories) are treated as
package/dist/testing.d.ts CHANGED
@@ -234,6 +234,8 @@ declare interface PanelConfig<T = any> {
234
234
  /** Source: "builtin" | "app" */
235
235
  source?: string;
236
236
  pluginId?: string;
237
+ /** Revision emitted by the runtime plugin asset manager for hot-loaded panels. */
238
+ pluginRevision?: number;
237
239
  /**
238
240
  * Whether to wrap the component with React.lazy + Suspense. Omit to let
239
241
  * the registry auto-detect: zero-arg functions (factories) are treated as
package/dist/testing.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx as Ba } from "react/jsx-runtime";
2
2
  import * as Pa from "react";
3
3
  import { createElement as is, useMemo as wn, useLayoutEffect as us, isValidElement as ss, cloneElement as ds, useSyncExternalStore as Gi } from "react";
4
- import { h as cs, p as fs, o as ps } from "./WorkspaceProvider-0V-2x7AH.js";
4
+ import { h as cs, q as fs, o as ps } from "./WorkspaceProvider-CuIZx1ua.js";
5
5
  import { d as ms } from "./panel-DnvDNQac.js";
6
6
  import * as bs from "react-dom/test-utils";
7
7
  import ka from "react-dom";
@@ -1,5 +1,5 @@
1
1
  type JSONSchema = Record<string, unknown>;
2
- type ToolReadinessRequirement = 'workspace-fs' | 'sandbox-exec' | 'ui-bridge';
2
+ type ToolReadinessRequirement = 'workspace-fs' | 'sandbox-exec' | 'ui-bridge' | 'runtime-dependencies' | `runtime:${string}`;
3
3
  interface ToolExecContext {
4
4
  abortSignal: AbortSignal;
5
5
  toolCallId: string;
@@ -35,7 +35,7 @@ interface UiBridge {
35
35
  postCommand(cmd: UiCommand): Promise<CommandResult>;
36
36
  subscribeCommands(handler: (cmd: UiCommand & {
37
37
  seq: number;
38
- }) => void): () => void;
38
+ }) => unknown): () => void;
39
39
  drainCommands?(): Promise<Array<UiCommand & {
40
40
  seq: number;
41
41
  }>>;